Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions recipes/correct-ts-specifiers/src/workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ import module from 'node:module';

import { type Api, api } from '@codemod.com/workflow';
import type { Helpers } from '@codemod.com/workflow/dist/jsFam.d.ts';
import { setCodemodName } from '@nodejs/codemod-utils/logger';

import { mapImports } from './map-imports.ts';
import type { FSAbsolutePath } from './index.d.ts';

import * as aliasLoader from '@nodejs-loaders/alias/alias.loader.mjs';

setCodemodName('correct-ts-specifiers');

module.registerHooks(aliasLoader);

export async function workflow({ contexts, files }: Api) {
Expand Down
8 changes: 6 additions & 2 deletions utils/src/logger.test.snap.cjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
exports[`logger > should emit error entries to standard error, collated by source module 1`] = `
" • sh*t happened\\n • maybe bad\\n • sh*t happened\\n • maybe other bad\\n[Codemod: correct-ts-specifiers]: migration incomplete!\\n"
" • sh*t happened\\n • maybe bad\\n • sh*t happened\\n • maybe other bad\\n[Codemod: test-codemod]: migration incomplete!\\n"
`;

exports[`logger > should emit non-error entries to standard out, collated by source module 1`] = `
"[Codemod: correct-ts-specifiers]: /tmp/foo.js\\n • maybe don’t\\n • maybe not that either\\n • still maybe don’t\\n • more maybe not\\n[Codemod: correct-ts-specifiers]: migration complete!\\n"
"[Codemod: test-codemod]: /tmp/foo.js\\n • maybe don’t\\n • maybe not that either\\n • still maybe don’t\\n • more maybe not\\n[Codemod: test-codemod]: migration complete!\\n"
`;

exports[`logger > should work without a codemod name 1`] = `
"[Codemod: nodjs-codemod]: /tmp/foo.js\\n • maybe don’t\\n • maybe not that either\\n • still maybe don’t\\n • more maybe not\\n[Codemod: nodjs-codemod]: migration complete!\\n"
`;
70 changes: 68 additions & 2 deletions utils/src/logger.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ describe('logger', { concurrency: true }, () => {
'--experimental-strip-types',
'-e',
dedent`
import { logger } from './logger.ts';
import { logger, setCodemodName } from './logger.ts';

setCodemodName('test-codemod');

const source1 = '/tmp/foo.js';
logger(source1, 'log', 'maybe don’t');
Expand Down Expand Up @@ -41,7 +43,9 @@ describe('logger', { concurrency: true }, () => {
'--experimental-strip-types',
'-e',
dedent`
import { logger } from './logger.ts';
import { logger, setCodemodName } from './logger.ts';

setCodemodName('test-codemod');

const source1 = '/tmp/foo.js';
logger(source1, 'error', 'sh*t happened');
Expand All @@ -60,4 +64,66 @@ describe('logger', { concurrency: true }, () => {
t.assert.snapshot(stderr);
assert.equal(code, 1);
});

it('should work without a codemod name', async (t) => {
const { code, stdout } = await spawnPromisified(
execPath,
[
'--no-warnings',
'--experimental-strip-types',
'-e',
dedent`
import { logger } from './logger.ts';

const source1 = '/tmp/foo.js';
logger(source1, 'log', 'maybe don’t');
logger(source1, 'log', 'maybe not that either');

const source2 = '/tmp/foo.js';
logger(source2, 'log', 'still maybe don’t');
logger(source2, 'log', 'more maybe not');
`,
],
{
cwd: import.meta.dirname,
},
);

t.assert.snapshot(stdout);
assert.equal(code, 0);
});

it('should handle multiple codemods with different names correctly', async () => {
const { code, stdout } = await spawnPromisified(
execPath,
[
'--no-warnings',
'--experimental-strip-types',
'-e',
dedent`
import { logger, setCodemodName } from './logger.ts';

// Simulate first codemod
setCodemodName('codemod-a');
logger('/tmp/file1.js', 'log', 'Message from codemod A');

// Simulate second codemod (this would previously overwrite the name)
logger('/tmp/file2.js', 'log', 'Message from codemod B', 'codemod-b');

// Another message from first codemod (should still show as codemod-a)
logger('/tmp/file3.js', 'log', 'Another message from codemod A');
`,
],
{
cwd: import.meta.dirname,
},
);

// Should show both codemod names in output
assert(stdout.includes('[Codemod: codemod-a]'));
assert(stdout.includes('[Codemod: codemod-b]'));
assert(stdout.includes('Message from codemod A'));
assert(stdout.includes('Message from codemod B'));
assert.equal(code, 0);
});
});
80 changes: 58 additions & 22 deletions utils/src/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,27 @@ import process from 'node:process';

type LogMsg = string;
type LogType = 'error' | 'log' | 'warn';
type FileLog = { msg: LogMsg; type: LogType };
type FileLog = { msg: LogMsg; type: LogType; codemodName: string };
type Source = URL['pathname'];

let defaultCodemodName = 'nodjs-codemod';

/**
* Set the default codemod name for logging output
*/
export const setCodemodName = (name: string) => {
defaultCodemodName = name;
};

/**
* Collect log entries and report them at the end, collated by source module.
*/
export const logger = (source: Source, type: LogType, msg: LogMsg) => {
const fileLog = new Set<FileLog>(logs.has(source) ? logs.get(source) : []);
export const logger = (source: Source, type: LogType, msg: LogMsg, codemodName?: string) => {
const name = codemodName ?? defaultCodemodName;
const fileLog = new Set<FileLog>(logs.has(source) ? logs.get(source) : []);

fileLog.add({ msg, type });
logs.set(source, fileLog);
fileLog.add({ msg, type, codemodName: name });
logs.set(source, fileLog);
};

/**
Expand All @@ -23,21 +33,47 @@ const logs = new Map<Source, Set<FileLog>>();
process.once('beforeExit', emitLogs);

function emitLogs() {
let hasError = false;

for (const [sourceFile, fileLog] of logs.entries()) {
console.log('[Codemod: correct-ts-specifiers]:', sourceFile);
for (const { msg, type } of fileLog) {
console[type](' •', msg);
if (type === 'error') hasError = true;
}
}

if (hasError) {
console.error('[Codemod: correct-ts-specifiers]: migration incomplete!');
process.exitCode = 1;
} else {
process.exitCode = 0;
console.log('[Codemod: correct-ts-specifiers]: migration complete!');
}
let hasError = false;

// Group logs by codemod name first, then by source file
const logsByCodemod = new Map<string, Map<Source, Set<FileLog>>>();

for (const [sourceFile, fileLogs] of logs.entries()) {
for (const fileLog of fileLogs) {
if (!logsByCodemod.has(fileLog.codemodName)) {
logsByCodemod.set(fileLog.codemodName, new Map());
}
const codemodLogs = logsByCodemod.get(fileLog.codemodName)!;
if (!codemodLogs.has(sourceFile)) {
codemodLogs.set(sourceFile, new Set());
}
codemodLogs.get(sourceFile)!.add(fileLog);
}
}

for (const [codemodName, codemodLogs] of logsByCodemod.entries()) {
for (const [sourceFile, fileLogs] of codemodLogs.entries()) {
console.log(`[Codemod: ${codemodName}]:`, sourceFile);
for (const { msg, type } of fileLogs) {
console[type](' •', msg);
if (type === 'error') hasError = true;
}
}

if (hasError) {
console.error(`[Codemod: ${codemodName}]: migration incomplete!`);
} else {
console.log(`[Codemod: ${codemodName}]: migration complete!`);
}
hasError = false; // Reset for next codemod
}

// Set overall exit code based on any errors
process.exitCode = Array.from(logsByCodemod.values())
.some(codemodLogs =>
Array.from(codemodLogs.values())
.some(fileLogs =>
Array.from(fileLogs).some(log => log.type === 'error')
)
) ? 1 : 0;
}
Loading