Skip to content
Open
Show file tree
Hide file tree
Changes from all 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