diff --git a/recipes/correct-ts-specifiers/src/workflow.ts b/recipes/correct-ts-specifiers/src/workflow.ts index 436eff80..129476cf 100644 --- a/recipes/correct-ts-specifiers/src/workflow.ts +++ b/recipes/correct-ts-specifiers/src/workflow.ts @@ -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) { diff --git a/utils/src/logger.test.snap.cjs b/utils/src/logger.test.snap.cjs index 2ccff3c1..8200e0d7 100644 --- a/utils/src/logger.test.snap.cjs +++ b/utils/src/logger.test.snap.cjs @@ -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" `; diff --git a/utils/src/logger.test.ts b/utils/src/logger.test.ts index 6e08dcd9..ada9f6a6 100644 --- a/utils/src/logger.test.ts +++ b/utils/src/logger.test.ts @@ -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'); @@ -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'); @@ -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); + }); }); diff --git a/utils/src/logger.ts b/utils/src/logger.ts index 4ca042a3..15766ff5 100644 --- a/utils/src/logger.ts +++ b/utils/src/logger.ts @@ -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(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(logs.has(source) ? logs.get(source) : []); - fileLog.add({ msg, type }); - logs.set(source, fileLog); + fileLog.add({ msg, type, codemodName: name }); + logs.set(source, fileLog); }; /** @@ -23,21 +33,47 @@ const logs = new Map>(); 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>>(); + + 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; }