From 462917ff0f3ac8dfeaa392995752b2e9017e790f Mon Sep 17 00:00:00 2001 From: Paul Valladares <85648028+dreyfus92@users.noreply.github.com> Date: Mon, 11 Aug 2025 15:44:32 -0500 Subject: [PATCH 1/9] feat: add support for custom manifests in module replacements analysis --- src/analyze/replacements.ts | 58 ++++++++++++++++++--- src/analyze/report.ts | 6 ++- src/commands/analyze.meta.ts | 5 ++ src/commands/analyze.ts | 10 +++- src/test/custom-manifests.test.ts | 75 ++++++++++++++++++++++++++++ src/types.ts | 14 ++++++ test/fixtures/custom-manifest-2.json | 14 ++++++ test/fixtures/custom-manifest.json | 31 ++++++++++++ 8 files changed, 202 insertions(+), 11 deletions(-) create mode 100644 src/test/custom-manifests.test.ts create mode 100644 test/fixtures/custom-manifest-2.json create mode 100644 test/fixtures/custom-manifest.json diff --git a/src/analyze/replacements.ts b/src/analyze/replacements.ts index bf4875c..99e8a14 100644 --- a/src/analyze/replacements.ts +++ b/src/analyze/replacements.ts @@ -1,7 +1,9 @@ import * as replacements from 'module-replacements'; -import {ReportPluginResult} from '../types.js'; +import {ReportPluginResult, CustomManifest, CustomReplacement} from '../types.js'; import type {FileSystem} from '../file-system.js'; import {getPackageJson} from '../file-system-utils.js'; +import {readFile} from 'node:fs/promises'; +import {resolve} from 'node:path'; /** * Generates a standard URL to the docs of a given rule @@ -21,8 +23,33 @@ export function getMdnUrl(path: string): string { return `https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/${path}`; } +async function loadCustomManifests(manifestPaths?: string[]): Promise { + if (!manifestPaths || manifestPaths.length === 0) { + return []; + } + + const customReplacements: CustomReplacement[] = []; + + for (const manifestPath of manifestPaths) { + try { + const absolutePath = resolve(manifestPath); + const manifestContent = await readFile(absolutePath, 'utf8'); + const manifest: CustomManifest = JSON.parse(manifestContent); + + if (manifest.replacements && Array.isArray(manifest.replacements)) { + customReplacements.push(...manifest.replacements); + } + } catch (error) { + console.warn(`Warning: Failed to load custom manifest from ${manifestPath}: ${error}`); + } + } + + return customReplacements; +} + export async function runReplacements( - fileSystem: FileSystem + fileSystem: FileSystem, + customManifests?: string[] ): Promise { const result: ReportPluginResult = { messages: [] @@ -35,11 +62,22 @@ export async function runReplacements( return result; } + // Load custom manifests + const customReplacements = await loadCustomManifests(customManifests); + for (const name of Object.keys(packageJson.dependencies)) { - const replacement = replacements.all.moduleReplacements.find( + // Check custom replacements first + let replacement = customReplacements.find( (replacement) => replacement.moduleName === name ); + // If no custom replacement found, check built-in replacements + if (!replacement) { + replacement = replacements.all.moduleReplacements.find( + (replacement) => replacement.moduleName === name + ); + } + if (!replacement) { continue; } @@ -54,23 +92,27 @@ export async function runReplacements( result.messages.push({ severity: 'warning', score: 0, - message: `Module "${name}" can be replaced. ${replacement.replacement}.` + message: `Module "${name}" can be replaced. ${replacement.replacement || 'See documentation for details'}.` }); } else if (replacement.type === 'native') { - const mdnPath = getMdnUrl(replacement.mdnPath); + const mdnPath = replacement.mdnPath ? getMdnUrl(replacement.mdnPath) : undefined; // TODO (43081j): support `nodeVersion` here, check it against the // packageJson.engines field, if there is one. + const message = `Module "${name}" can be replaced with native functionality. Use "${replacement.replacement || 'native alternative'}" instead.`; + const fullMessage = mdnPath ? `${message} You can read more at ${mdnPath}.` : message; result.messages.push({ severity: 'warning', score: 0, - message: `Module "${name}" can be replaced with native functionality. Use "${replacement.replacement}" instead. You can read more at ${mdnPath}.` + message: fullMessage }); } else if (replacement.type === 'documented') { - const docUrl = getDocsUrl(replacement.docPath); + const docUrl = replacement.docPath ? getDocsUrl(replacement.docPath) : undefined; + const message = `Module "${name}" can be replaced with a more performant alternative.`; + const fullMessage = docUrl ? `${message} See the list of available alternatives at ${docUrl}.` : message; result.messages.push({ severity: 'warning', score: 0, - message: `Module "${name}" can be replaced with a more performant alternative. See the list of available alternatives at ${docUrl}.` + message: fullMessage }); } } diff --git a/src/analyze/report.ts b/src/analyze/report.ts index b7f839f..36804ed 100644 --- a/src/analyze/report.ts +++ b/src/analyze/report.ts @@ -31,7 +31,7 @@ async function computeInfo(fileSystem: FileSystem) { } export async function report(options: Options) { - const {root = process.cwd(), pack = 'auto'} = options ?? {}; + const {root = process.cwd(), pack = 'auto', customManifests} = options ?? {}; let fileSystem: FileSystem; const messages: Message[] = []; @@ -65,7 +65,9 @@ export async function report(options: Options) { } for (const plugin of plugins) { - const result = await plugin(fileSystem); + const result = await (plugin === runReplacements + ? runReplacements(fileSystem, customManifests) + : plugin(fileSystem)); for (const message of result.messages) { messages.push(message); diff --git a/src/commands/analyze.meta.ts b/src/commands/analyze.meta.ts index e7bfd03..0e784c4 100644 --- a/src/commands/analyze.meta.ts +++ b/src/commands/analyze.meta.ts @@ -22,6 +22,11 @@ export const meta = { choices: ['debug', 'info', 'warn', 'error'], default: 'info', description: 'Set the log level (debug | info | warn | error)' + }, + 'custom-manifests': { + type: 'string', + array: true, + description: 'Path(s) to custom manifest file(s) for module replacements analysis' } } } as const; diff --git a/src/commands/analyze.ts b/src/commands/analyze.ts index 984ec69..5298760 100644 --- a/src/commands/analyze.ts +++ b/src/commands/analyze.ts @@ -71,7 +71,15 @@ export async function run(ctx: CommandContext) { } // Then analyze the tarball - const {stats, messages} = await report({root, pack}); + const rawCustomManifests = ctx.values['custom-manifests']; + + // NOTE: Gunshi quirk - array arguments are sometimes returned as single strings + // when only one value is provided, so we need to handle both cases + const customManifests = Array.isArray(rawCustomManifests) + ? rawCustomManifests + : (rawCustomManifests ? [rawCustomManifests] : []); + + const {stats, messages} = await report({root, pack, customManifests}); prompts.log.info('Summary'); diff --git a/src/test/custom-manifests.test.ts b/src/test/custom-manifests.test.ts new file mode 100644 index 0000000..8495a55 --- /dev/null +++ b/src/test/custom-manifests.test.ts @@ -0,0 +1,75 @@ +import {describe, it, expect} from 'vitest'; +import {runReplacements} from '../analyze/replacements.js'; +import {LocalFileSystem} from '../local-file-system.js'; +import {join} from 'node:path'; +import {fileURLToPath} from 'node:url'; +import {dirname} from 'node:path'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +describe('Custom Manifests', () => { + it('should load and use custom manifest files', async () => { + const testDir = join(__dirname, '../../test/fixtures/basic-chalk'); + const fileSystem = new LocalFileSystem(testDir); + const customManifestPath = join(__dirname, '../../test/fixtures/custom-manifest.json'); + + const result = await runReplacements(fileSystem, [customManifestPath]); + + // Should have messages from custom manifest + expect(result.messages.length).toBeGreaterThan(0); + + // Check that custom replacement messages are included + const hasCustomMessage = result.messages.some(msg => + msg.message.includes('chalk') || + msg.message.includes('lodash') || + msg.message.includes('moment') || + msg.message.includes('request') || + msg.message.includes('bluebird') + ); + + expect(hasCustomMessage).toBe(true); + }); + + it('should handle invalid manifest files gracefully', async () => { + const testDir = join(__dirname, '../../test/fixtures/basic-chalk'); + const fileSystem = new LocalFileSystem(testDir); + const invalidManifestPath = 'non-existent-file.json'; + + const result = await runReplacements(fileSystem, [invalidManifestPath]); + + // Should still work without crashing + expect(result.messages).toBeDefined(); + }); + + it('should prioritize custom replacements over built-in ones', async () => { + const testDir = join(__dirname, '../../test/fixtures/basic-chalk'); + const fileSystem = new LocalFileSystem(testDir); + const customManifestPath = join(__dirname, '../../test/fixtures/custom-manifest.json'); + + const resultWithCustom = await runReplacements(fileSystem, [customManifestPath]); + const resultWithoutCustom = await runReplacements(fileSystem); + + // Custom manifest should provide additional or different messages + expect(resultWithCustom.messages.length).toBeGreaterThanOrEqual(resultWithoutCustom.messages.length); + }); + + it('should load multiple manifest files', async () => { + const testDir = join(__dirname, '../../test/fixtures/basic-chalk'); + const fileSystem = new LocalFileSystem(testDir); + const manifest1Path = join(__dirname, '../../test/fixtures/custom-manifest.json'); + const manifest2Path = join(__dirname, '../../test/fixtures/custom-manifest-2.json'); + + const result = await runReplacements(fileSystem, [manifest1Path, manifest2Path]); + + // Should have messages from both manifests + expect(result.messages.length).toBeGreaterThan(0); + + // Check that replacements from both manifests are included + const hasChalkMessage = result.messages.some(msg => msg.message.includes('chalk')); + const hasExpressMessage = result.messages.some(msg => msg.message.includes('express')); + + // Note: express won't be found since it's not in the test fixture dependencies + // but chalk should be found + expect(hasChalkMessage).toBe(true); + }); +}); diff --git a/src/types.ts b/src/types.ts index a985b10..672629b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -18,6 +18,7 @@ export type PackType = export interface Options { root?: string; pack?: PackType; + customManifests?: string[]; } export interface StatLike { @@ -70,3 +71,16 @@ export interface ReportPluginResult { export type ReportPlugin = ( fileSystem: FileSystem ) => Promise; + +export interface CustomReplacement { + moduleName: string; + type: 'none' | 'simple' | 'native' | 'documented'; + replacement?: string; + mdnPath?: string; + docPath?: string; + nodeVersion?: string; +} + +export interface CustomManifest { + replacements: CustomReplacement[]; +} diff --git a/test/fixtures/custom-manifest-2.json b/test/fixtures/custom-manifest-2.json new file mode 100644 index 0000000..268aa9e --- /dev/null +++ b/test/fixtures/custom-manifest-2.json @@ -0,0 +1,14 @@ +{ + "replacements": [ + { + "moduleName": "express", + "type": "simple", + "replacement": "Use Fastify, Koa, or native Node.js http module" + }, + { + "moduleName": "body-parser", + "type": "none", + "replacement": "Use native JSON.parse or built-in middleware" + } + ] +} diff --git a/test/fixtures/custom-manifest.json b/test/fixtures/custom-manifest.json new file mode 100644 index 0000000..62737e8 --- /dev/null +++ b/test/fixtures/custom-manifest.json @@ -0,0 +1,31 @@ +{ + "replacements": [ + { + "moduleName": "chalk", + "type": "simple", + "replacement": "Use picocolors, kleur, or native console styling" + }, + { + "moduleName": "lodash", + "type": "simple", + "replacement": "Use native JavaScript methods or specific lodash functions" + }, + { + "moduleName": "moment", + "type": "native", + "replacement": "Intl.DateTimeFormat or Date methods", + "mdnPath": "Global_Objects/Intl/DateTimeFormat" + }, + { + "moduleName": "request", + "type": "documented", + "replacement": "node-fetch, axios, or native fetch", + "docPath": "request-alternatives" + }, + { + "moduleName": "bluebird", + "type": "none", + "replacement": "Use native Promise methods" + } + ] +} From 504c3325c9ac1f4acf1aabb99e17e358ed5f890c Mon Sep 17 00:00:00 2001 From: Paul Valladares <85648028+dreyfus92@users.noreply.github.com> Date: Mon, 11 Aug 2025 15:46:08 -0500 Subject: [PATCH 2/9] format --- src/analyze/replacements.ts | 32 ++++++++++++---- src/analyze/report.ts | 2 +- src/commands/analyze.meta.ts | 3 +- src/commands/analyze.ts | 12 +++--- src/test/custom-manifests.test.ts | 62 +++++++++++++++++++++---------- 5 files changed, 77 insertions(+), 34 deletions(-) diff --git a/src/analyze/replacements.ts b/src/analyze/replacements.ts index 99e8a14..3d19745 100644 --- a/src/analyze/replacements.ts +++ b/src/analyze/replacements.ts @@ -1,5 +1,9 @@ import * as replacements from 'module-replacements'; -import {ReportPluginResult, CustomManifest, CustomReplacement} from '../types.js'; +import { + ReportPluginResult, + CustomManifest, + CustomReplacement +} from '../types.js'; import type {FileSystem} from '../file-system.js'; import {getPackageJson} from '../file-system-utils.js'; import {readFile} from 'node:fs/promises'; @@ -23,7 +27,9 @@ export function getMdnUrl(path: string): string { return `https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/${path}`; } -async function loadCustomManifests(manifestPaths?: string[]): Promise { +async function loadCustomManifests( + manifestPaths?: string[] +): Promise { if (!manifestPaths || manifestPaths.length === 0) { return []; } @@ -35,12 +41,14 @@ async function loadCustomManifests(manifestPaths?: string[]): Promise) { // Then analyze the tarball const rawCustomManifests = ctx.values['custom-manifests']; - + // NOTE: Gunshi quirk - array arguments are sometimes returned as single strings // when only one value is provided, so we need to handle both cases - const customManifests = Array.isArray(rawCustomManifests) - ? rawCustomManifests - : (rawCustomManifests ? [rawCustomManifests] : []); - + const customManifests = Array.isArray(rawCustomManifests) + ? rawCustomManifests + : rawCustomManifests + ? [rawCustomManifests] + : []; + const {stats, messages} = await report({root, pack, customManifests}); prompts.log.info('Summary'); diff --git a/src/test/custom-manifests.test.ts b/src/test/custom-manifests.test.ts index 8495a55..031c6e5 100644 --- a/src/test/custom-manifests.test.ts +++ b/src/test/custom-manifests.test.ts @@ -11,22 +11,26 @@ describe('Custom Manifests', () => { it('should load and use custom manifest files', async () => { const testDir = join(__dirname, '../../test/fixtures/basic-chalk'); const fileSystem = new LocalFileSystem(testDir); - const customManifestPath = join(__dirname, '../../test/fixtures/custom-manifest.json'); + const customManifestPath = join( + __dirname, + '../../test/fixtures/custom-manifest.json' + ); const result = await runReplacements(fileSystem, [customManifestPath]); // Should have messages from custom manifest expect(result.messages.length).toBeGreaterThan(0); - + // Check that custom replacement messages are included - const hasCustomMessage = result.messages.some(msg => - msg.message.includes('chalk') || - msg.message.includes('lodash') || - msg.message.includes('moment') || - msg.message.includes('request') || - msg.message.includes('bluebird') + const hasCustomMessage = result.messages.some( + (msg) => + msg.message.includes('chalk') || + msg.message.includes('lodash') || + msg.message.includes('moment') || + msg.message.includes('request') || + msg.message.includes('bluebird') ); - + expect(hasCustomMessage).toBe(true); }); @@ -44,30 +48,50 @@ describe('Custom Manifests', () => { it('should prioritize custom replacements over built-in ones', async () => { const testDir = join(__dirname, '../../test/fixtures/basic-chalk'); const fileSystem = new LocalFileSystem(testDir); - const customManifestPath = join(__dirname, '../../test/fixtures/custom-manifest.json'); + const customManifestPath = join( + __dirname, + '../../test/fixtures/custom-manifest.json' + ); - const resultWithCustom = await runReplacements(fileSystem, [customManifestPath]); + const resultWithCustom = await runReplacements(fileSystem, [ + customManifestPath + ]); const resultWithoutCustom = await runReplacements(fileSystem); // Custom manifest should provide additional or different messages - expect(resultWithCustom.messages.length).toBeGreaterThanOrEqual(resultWithoutCustom.messages.length); + expect(resultWithCustom.messages.length).toBeGreaterThanOrEqual( + resultWithoutCustom.messages.length + ); }); it('should load multiple manifest files', async () => { const testDir = join(__dirname, '../../test/fixtures/basic-chalk'); const fileSystem = new LocalFileSystem(testDir); - const manifest1Path = join(__dirname, '../../test/fixtures/custom-manifest.json'); - const manifest2Path = join(__dirname, '../../test/fixtures/custom-manifest-2.json'); + const manifest1Path = join( + __dirname, + '../../test/fixtures/custom-manifest.json' + ); + const manifest2Path = join( + __dirname, + '../../test/fixtures/custom-manifest-2.json' + ); - const result = await runReplacements(fileSystem, [manifest1Path, manifest2Path]); + const result = await runReplacements(fileSystem, [ + manifest1Path, + manifest2Path + ]); // Should have messages from both manifests expect(result.messages.length).toBeGreaterThan(0); - + // Check that replacements from both manifests are included - const hasChalkMessage = result.messages.some(msg => msg.message.includes('chalk')); - const hasExpressMessage = result.messages.some(msg => msg.message.includes('express')); - + const hasChalkMessage = result.messages.some((msg) => + msg.message.includes('chalk') + ); + const hasExpressMessage = result.messages.some((msg) => + msg.message.includes('express') + ); + // Note: express won't be found since it's not in the test fixture dependencies // but chalk should be found expect(hasChalkMessage).toBe(true); From f7130b1a4bdb7b58b26f7f049b29f7f85b7ef3d5 Mon Sep 17 00:00:00 2001 From: Paul Valladares <85648028+dreyfus92@users.noreply.github.com> Date: Mon, 11 Aug 2025 15:48:45 -0500 Subject: [PATCH 3/9] remove unused express message check in custom manifests test --- src/test/custom-manifests.test.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/test/custom-manifests.test.ts b/src/test/custom-manifests.test.ts index 031c6e5..ae3c931 100644 --- a/src/test/custom-manifests.test.ts +++ b/src/test/custom-manifests.test.ts @@ -88,10 +88,7 @@ describe('Custom Manifests', () => { const hasChalkMessage = result.messages.some((msg) => msg.message.includes('chalk') ); - const hasExpressMessage = result.messages.some((msg) => - msg.message.includes('express') - ); - + // Note: express won't be found since it's not in the test fixture dependencies // but chalk should be found expect(hasChalkMessage).toBe(true); From 566566c7ce946c9535f4b805d513aca15422d7eb Mon Sep 17 00:00:00 2001 From: Paul Valladares <85648028+dreyfus92@users.noreply.github.com> Date: Mon, 11 Aug 2025 15:51:42 -0500 Subject: [PATCH 4/9] format --- src/test/custom-manifests.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/custom-manifests.test.ts b/src/test/custom-manifests.test.ts index ae3c931..a1d19ee 100644 --- a/src/test/custom-manifests.test.ts +++ b/src/test/custom-manifests.test.ts @@ -88,7 +88,7 @@ describe('Custom Manifests', () => { const hasChalkMessage = result.messages.some((msg) => msg.message.includes('chalk') ); - + // Note: express won't be found since it's not in the test fixture dependencies // but chalk should be found expect(hasChalkMessage).toBe(true); From 68cc0dfde79b84252e8f48df8eb7363cb415d9b4 Mon Sep 17 00:00:00 2001 From: Paul Valladares <85648028+dreyfus92@users.noreply.github.com> Date: Tue, 19 Aug 2025 13:25:25 -0500 Subject: [PATCH 5/9] Apply suggestions --- src/analyze/attw.ts | 6 +- src/analyze/dependencies.ts | 7 +- src/analyze/publint.ts | 6 +- src/analyze/replacements.ts | 166 ++++++++++++++------------- src/analyze/report.ts | 6 +- src/commands/analyze.meta.ts | 2 +- src/commands/analyze.ts | 4 +- src/test/custom-manifests.test.ts | 17 ++- src/types.ts | 16 +-- test/fixtures/custom-manifest-2.json | 2 +- test/fixtures/custom-manifest.json | 5 +- 11 files changed, 120 insertions(+), 117 deletions(-) diff --git a/src/analyze/attw.ts b/src/analyze/attw.ts index adccb7e..051ad0a 100644 --- a/src/analyze/attw.ts +++ b/src/analyze/attw.ts @@ -5,12 +5,14 @@ import { } from '@arethetypeswrong/core'; import {groupProblemsByKind} from '@arethetypeswrong/core/utils'; import {filterProblems, problemKindInfo} from '@arethetypeswrong/core/problems'; -import {ReportPluginResult} from '../types.js'; +import {ReportPluginResult, type Options} from '../types.js'; import type {FileSystem} from '../file-system.js'; import {TarballFileSystem} from '../tarball-file-system.js'; export async function runAttw( - fileSystem: FileSystem + fileSystem: FileSystem, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _?: Options ): Promise { const result: ReportPluginResult = { messages: [] diff --git a/src/analyze/dependencies.ts b/src/analyze/dependencies.ts index 4e0810a..cf28d15 100644 --- a/src/analyze/dependencies.ts +++ b/src/analyze/dependencies.ts @@ -4,7 +4,8 @@ import type { PackageJsonLike, ReportPluginResult, Message, - Stats + Stats, + Options } from '../types.js'; import {FileSystem} from '../file-system.js'; import {normalizePath} from '../utils/path.js'; @@ -147,7 +148,9 @@ async function parsePackageJson( // Keep the existing tarball analysis for backward compatibility export async function runDependencyAnalysis( - fileSystem: FileSystem + fileSystem: FileSystem, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _?: Options ): Promise { const packageFiles = await fileSystem.listPackageFiles(); const rootDir = await fileSystem.getRootDir(); diff --git a/src/analyze/publint.ts b/src/analyze/publint.ts index ad80798..a1364e1 100644 --- a/src/analyze/publint.ts +++ b/src/analyze/publint.ts @@ -1,11 +1,13 @@ import {publint} from 'publint'; import {formatMessage} from 'publint/utils'; -import {ReportPluginResult} from '../types.js'; +import {ReportPluginResult, type Options} from '../types.js'; import type {FileSystem} from '../file-system.js'; import {TarballFileSystem} from '../tarball-file-system.js'; export async function runPublint( - fileSystem: FileSystem + fileSystem: FileSystem, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _?: Options ): Promise { const result: ReportPluginResult = { messages: [] diff --git a/src/analyze/replacements.ts b/src/analyze/replacements.ts index e217244..18c3189 100644 --- a/src/analyze/replacements.ts +++ b/src/analyze/replacements.ts @@ -1,16 +1,14 @@ import * as replacements from 'module-replacements'; +import type {ManifestModule, ModuleReplacement} from 'module-replacements'; import { - ReportPluginResult, - CustomManifest, - CustomReplacement + ReportPluginResult } from '../types.js'; import type {FileSystem} from '../file-system.js'; import {getPackageJson} from '../file-system-utils.js'; -import {readFile} from 'node:fs/promises'; -import {resolve} from 'node:path'; -import semverSatisfies from 'semver/functions/satisfies.js'; -import semverLessThan from 'semver/ranges/ltr.js'; -import {minVersion, validRange} from 'semver'; +import {resolve, dirname} from 'node:path'; +import {satisfies as semverSatisfies, ltr as semverLessThan, minVersion, validRange} from 'semver'; +import {LocalFileSystem} from '../local-file-system.js'; +import type {Options} from '../types.js'; /** * Generates a standard URL to the docs of a given rule @@ -32,21 +30,24 @@ export function getMdnUrl(path: string): string { async function loadCustomManifests( manifestPaths?: string[] -): Promise { +): Promise { if (!manifestPaths || manifestPaths.length === 0) { return []; } - const customReplacements: CustomReplacement[] = []; + const customReplacements: ModuleReplacement[] = []; for (const manifestPath of manifestPaths) { try { const absolutePath = resolve(manifestPath); - const manifestContent = await readFile(absolutePath, 'utf8'); - const manifest: CustomManifest = JSON.parse(manifestContent); - - if (manifest.replacements && Array.isArray(manifest.replacements)) { - customReplacements.push(...manifest.replacements); + const manifestDir = dirname(absolutePath); + const manifestFileName = absolutePath.substring(manifestDir.length + 1); + const localFileSystem = new LocalFileSystem(manifestDir); + const manifestContent = await localFileSystem.readFile(`/${manifestFileName}`); + const manifest: ManifestModule = JSON.parse(manifestContent); + + if (manifest.moduleReplacements && Array.isArray(manifest.moduleReplacements)) { + customReplacements.push(...manifest.moduleReplacements); } } catch (error) { console.warn( @@ -82,7 +83,7 @@ function isNodeEngineCompatible( export async function runReplacements( fileSystem: FileSystem, - customManifests?: string[] + options?: Options ): Promise { const result: ReportPluginResult = { messages: [] @@ -96,81 +97,88 @@ export async function runReplacements( } // Load custom manifests - const customReplacements = await loadCustomManifests(customManifests); + const customReplacements = await loadCustomManifests(options?.manifest); + + // Combine custom and built-in replacements + const allReplacements = [...customReplacements, ...replacements.all.moduleReplacements]; for (const name of Object.keys(packageJson.dependencies)) { - // Check custom replacements first - let replacement = customReplacements.find( + // Find replacement (custom replacements take precedence due to order) + const replacement = allReplacements.find( (replacement) => replacement.moduleName === name ); - // If no custom replacement found, check built-in replacements - if (!replacement) { - replacement = replacements.all.moduleReplacements.find( - (replacement) => replacement.moduleName === name - ); - } - if (!replacement) { continue; } - if (replacement.type === 'none') { - result.messages.push({ - severity: 'warning', - score: 0, - message: `Module "${name}" can be removed, and native functionality used instead` - }); - } else if (replacement.type === 'simple') { - result.messages.push({ - severity: 'warning', - score: 0, - message: `Module "${name}" can be replaced. ${replacement.replacement || 'See documentation for details'}.` - }); - } else if (replacement.type === 'native') { - const enginesNode = packageJson.engines?.node; - let supported = true; - - if (replacement.nodeVersion && enginesNode) { - supported = isNodeEngineCompatible( - replacement.nodeVersion, - enginesNode - ); + // Handle each replacement type using the same logic for both custom and built-in + switch (replacement.type) { + case 'none': + result.messages.push({ + severity: 'warning', + score: 0, + message: `Module "${name}" can be removed, and native functionality used instead` + }); + break; + + case 'simple': + result.messages.push({ + severity: 'warning', + score: 0, + message: `Module "${name}" can be replaced. ${replacement.replacement || 'See documentation for details'}.` + }); + break; + + case 'native': { + const enginesNode = packageJson.engines?.node; + let supported = true; + + if (replacement.nodeVersion && enginesNode) { + supported = isNodeEngineCompatible( + replacement.nodeVersion, + enginesNode + ); + } + + if (!supported) { + continue; + } + + const mdnPath = replacement.mdnPath + ? getMdnUrl(replacement.mdnPath) + : undefined; + const requires = + replacement.nodeVersion && !enginesNode + ? ` Required Node >= ${replacement.nodeVersion}.` + : ''; + const message = `Module "${name}" can be replaced with native functionality. Use "${replacement.replacement || 'native alternative'}" instead.${requires}`; + const fullMessage = mdnPath + ? `${message} You can read more at ${mdnPath}.` + : message; + result.messages.push({ + severity: 'warning', + score: 0, + message: fullMessage + }); + break; } - if (!supported) { - continue; + case 'documented': { + const docUrl = replacement.docPath + ? getDocsUrl(replacement.docPath) + : undefined; + const message = `Module "${name}" can be replaced with a more performant alternative.`; + const fullMessage = docUrl + ? `${message} See the list of available alternatives at ${docUrl}.` + : message; + result.messages.push({ + severity: 'warning', + score: 0, + message: fullMessage + }); + break; } - - const mdnPath = replacement.mdnPath - ? getMdnUrl(replacement.mdnPath) - : undefined; - const requires = - replacement.nodeVersion && !enginesNode - ? ` Required Node >= ${replacement.nodeVersion}.` - : ''; - const message = `Module "${name}" can be replaced with native functionality. Use "${replacement.replacement || 'native alternative'}" instead.`; - const fullMessage = mdnPath - ? `${message} You can read more at ${mdnPath}.` - : message; - result.messages.push({ - severity: 'warning', - score: 0, - message: `Module "${name}" can be replaced with native functionality. Use "${replacement.replacement}" instead. You can read more at ${mdnPath}.${requires}` - }); - } else if (replacement.type === 'documented') { - const docUrl = replacement.docPath - ? getDocsUrl(replacement.docPath) - : undefined; - const message = `Module "${name}" can be replaced with a more performant alternative.`; - const fullMessage = docUrl - ? `${message} See the list of available alternatives at ${docUrl}.` - : message; - result.messages.push({ - severity: 'warning', - score: 0, - message: fullMessage - }); } } diff --git a/src/analyze/report.ts b/src/analyze/report.ts index 375a2e7..2c5d0c6 100644 --- a/src/analyze/report.ts +++ b/src/analyze/report.ts @@ -31,7 +31,7 @@ async function computeInfo(fileSystem: FileSystem) { } export async function report(options: Options) { - const {root = process.cwd(), pack = 'auto', customManifests} = options ?? {}; + const {root = process.cwd(), pack = 'auto'} = options ?? {}; let fileSystem: FileSystem; const messages: Message[] = []; @@ -65,9 +65,7 @@ export async function report(options: Options) { } for (const plugin of plugins) { - const result = await (plugin === runReplacements - ? runReplacements(fileSystem, customManifests) - : plugin(fileSystem)); + const result = await plugin(fileSystem, options); for (const message of result.messages) { messages.push(message); diff --git a/src/commands/analyze.meta.ts b/src/commands/analyze.meta.ts index 02b9f4e..a2d0692 100644 --- a/src/commands/analyze.meta.ts +++ b/src/commands/analyze.meta.ts @@ -23,7 +23,7 @@ export const meta = { default: 'info', description: 'Set the log level (debug | info | warn | error)' }, - 'custom-manifests': { + 'manifest': { type: 'string', array: true, description: diff --git a/src/commands/analyze.ts b/src/commands/analyze.ts index 0dc9455..716cc9e 100644 --- a/src/commands/analyze.ts +++ b/src/commands/analyze.ts @@ -78,7 +78,7 @@ export async function run(ctx: CommandContext) { } // Then analyze the tarball - const rawCustomManifests = ctx.values['custom-manifests']; + const rawCustomManifests = ctx.values['manifest']; // NOTE: Gunshi quirk - array arguments are sometimes returned as single strings // when only one value is provided, so we need to handle both cases @@ -88,7 +88,7 @@ export async function run(ctx: CommandContext) { ? [rawCustomManifests] : []; - const {stats, messages} = await report({root, pack, customManifests}); + const {stats, messages} = await report({root, pack, manifest: customManifests}); prompts.log.info('Summary'); diff --git a/src/test/custom-manifests.test.ts b/src/test/custom-manifests.test.ts index a1d19ee..686edeb 100644 --- a/src/test/custom-manifests.test.ts +++ b/src/test/custom-manifests.test.ts @@ -16,7 +16,7 @@ describe('Custom Manifests', () => { '../../test/fixtures/custom-manifest.json' ); - const result = await runReplacements(fileSystem, [customManifestPath]); + const result = await runReplacements(fileSystem, {manifest: [customManifestPath]}); // Should have messages from custom manifest expect(result.messages.length).toBeGreaterThan(0); @@ -39,7 +39,7 @@ describe('Custom Manifests', () => { const fileSystem = new LocalFileSystem(testDir); const invalidManifestPath = 'non-existent-file.json'; - const result = await runReplacements(fileSystem, [invalidManifestPath]); + const result = await runReplacements(fileSystem, {manifest: [invalidManifestPath]}); // Should still work without crashing expect(result.messages).toBeDefined(); @@ -53,9 +53,9 @@ describe('Custom Manifests', () => { '../../test/fixtures/custom-manifest.json' ); - const resultWithCustom = await runReplacements(fileSystem, [ - customManifestPath - ]); + const resultWithCustom = await runReplacements(fileSystem, { + manifest: [customManifestPath] + }); const resultWithoutCustom = await runReplacements(fileSystem); // Custom manifest should provide additional or different messages @@ -76,10 +76,9 @@ describe('Custom Manifests', () => { '../../test/fixtures/custom-manifest-2.json' ); - const result = await runReplacements(fileSystem, [ - manifest1Path, - manifest2Path - ]); + const result = await runReplacements(fileSystem, { + manifest: [manifest1Path, manifest2Path] + }); // Should have messages from both manifests expect(result.messages.length).toBeGreaterThan(0); diff --git a/src/types.ts b/src/types.ts index 45ca750..8ccf85b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -18,7 +18,7 @@ export type PackType = export interface Options { root?: string; pack?: PackType; - customManifests?: string[]; + manifest?: string[]; } export interface StatLike { @@ -73,18 +73,8 @@ export interface ReportPluginResult { } export type ReportPlugin = ( - fileSystem: FileSystem + fileSystem: FileSystem, + options?: Options ) => Promise; -export interface CustomReplacement { - moduleName: string; - type: 'none' | 'simple' | 'native' | 'documented'; - replacement?: string; - mdnPath?: string; - docPath?: string; - nodeVersion?: string; -} -export interface CustomManifest { - replacements: CustomReplacement[]; -} diff --git a/test/fixtures/custom-manifest-2.json b/test/fixtures/custom-manifest-2.json index 268aa9e..70d771b 100644 --- a/test/fixtures/custom-manifest-2.json +++ b/test/fixtures/custom-manifest-2.json @@ -1,5 +1,5 @@ { - "replacements": [ + "moduleReplacements": [ { "moduleName": "express", "type": "simple", diff --git a/test/fixtures/custom-manifest.json b/test/fixtures/custom-manifest.json index 62737e8..d1968a0 100644 --- a/test/fixtures/custom-manifest.json +++ b/test/fixtures/custom-manifest.json @@ -1,5 +1,5 @@ { - "replacements": [ + "moduleReplacements": [ { "moduleName": "chalk", "type": "simple", @@ -14,7 +14,8 @@ "moduleName": "moment", "type": "native", "replacement": "Intl.DateTimeFormat or Date methods", - "mdnPath": "Global_Objects/Intl/DateTimeFormat" + "mdnPath": "Global_Objects/Intl/DateTimeFormat", + "nodeVersion": "12.0.0" }, { "moduleName": "request", From 58046d1b826d2b72ba75b359b33ca225a325b6b3 Mon Sep 17 00:00:00 2001 From: Paul Valladares <85648028+dreyfus92@users.noreply.github.com> Date: Tue, 19 Aug 2025 13:26:09 -0500 Subject: [PATCH 6/9] format --- src/analyze/replacements.ts | 25 ++++++++++++++++++------- src/commands/analyze.meta.ts | 2 +- src/commands/analyze.ts | 6 +++++- src/test/custom-manifests.test.ts | 8 ++++++-- src/types.ts | 2 -- 5 files changed, 30 insertions(+), 13 deletions(-) diff --git a/src/analyze/replacements.ts b/src/analyze/replacements.ts index 18c3189..6784930 100644 --- a/src/analyze/replacements.ts +++ b/src/analyze/replacements.ts @@ -1,12 +1,15 @@ import * as replacements from 'module-replacements'; import type {ManifestModule, ModuleReplacement} from 'module-replacements'; -import { - ReportPluginResult -} from '../types.js'; +import {ReportPluginResult} from '../types.js'; import type {FileSystem} from '../file-system.js'; import {getPackageJson} from '../file-system-utils.js'; import {resolve, dirname} from 'node:path'; -import {satisfies as semverSatisfies, ltr as semverLessThan, minVersion, validRange} from 'semver'; +import { + satisfies as semverSatisfies, + ltr as semverLessThan, + minVersion, + validRange +} from 'semver'; import {LocalFileSystem} from '../local-file-system.js'; import type {Options} from '../types.js'; @@ -43,10 +46,15 @@ async function loadCustomManifests( const manifestDir = dirname(absolutePath); const manifestFileName = absolutePath.substring(manifestDir.length + 1); const localFileSystem = new LocalFileSystem(manifestDir); - const manifestContent = await localFileSystem.readFile(`/${manifestFileName}`); + const manifestContent = await localFileSystem.readFile( + `/${manifestFileName}` + ); const manifest: ManifestModule = JSON.parse(manifestContent); - if (manifest.moduleReplacements && Array.isArray(manifest.moduleReplacements)) { + if ( + manifest.moduleReplacements && + Array.isArray(manifest.moduleReplacements) + ) { customReplacements.push(...manifest.moduleReplacements); } } catch (error) { @@ -100,7 +108,10 @@ export async function runReplacements( const customReplacements = await loadCustomManifests(options?.manifest); // Combine custom and built-in replacements - const allReplacements = [...customReplacements, ...replacements.all.moduleReplacements]; + const allReplacements = [ + ...customReplacements, + ...replacements.all.moduleReplacements + ]; for (const name of Object.keys(packageJson.dependencies)) { // Find replacement (custom replacements take precedence due to order) diff --git a/src/commands/analyze.meta.ts b/src/commands/analyze.meta.ts index a2d0692..4071898 100644 --- a/src/commands/analyze.meta.ts +++ b/src/commands/analyze.meta.ts @@ -23,7 +23,7 @@ export const meta = { default: 'info', description: 'Set the log level (debug | info | warn | error)' }, - 'manifest': { + manifest: { type: 'string', array: true, description: diff --git a/src/commands/analyze.ts b/src/commands/analyze.ts index 716cc9e..717b1c1 100644 --- a/src/commands/analyze.ts +++ b/src/commands/analyze.ts @@ -88,7 +88,11 @@ export async function run(ctx: CommandContext) { ? [rawCustomManifests] : []; - const {stats, messages} = await report({root, pack, manifest: customManifests}); + const {stats, messages} = await report({ + root, + pack, + manifest: customManifests + }); prompts.log.info('Summary'); diff --git a/src/test/custom-manifests.test.ts b/src/test/custom-manifests.test.ts index 686edeb..34026d6 100644 --- a/src/test/custom-manifests.test.ts +++ b/src/test/custom-manifests.test.ts @@ -16,7 +16,9 @@ describe('Custom Manifests', () => { '../../test/fixtures/custom-manifest.json' ); - const result = await runReplacements(fileSystem, {manifest: [customManifestPath]}); + const result = await runReplacements(fileSystem, { + manifest: [customManifestPath] + }); // Should have messages from custom manifest expect(result.messages.length).toBeGreaterThan(0); @@ -39,7 +41,9 @@ describe('Custom Manifests', () => { const fileSystem = new LocalFileSystem(testDir); const invalidManifestPath = 'non-existent-file.json'; - const result = await runReplacements(fileSystem, {manifest: [invalidManifestPath]}); + const result = await runReplacements(fileSystem, { + manifest: [invalidManifestPath] + }); // Should still work without crashing expect(result.messages).toBeDefined(); diff --git a/src/types.ts b/src/types.ts index 8ccf85b..3e5ac8e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -76,5 +76,3 @@ export type ReportPlugin = ( fileSystem: FileSystem, options?: Options ) => Promise; - - From 9ee4c5fafb4b1ab79129d5bbd01b4f0850486afd Mon Sep 17 00:00:00 2001 From: Paul Valladares <85648028+dreyfus92@users.noreply.github.com> Date: Tue, 19 Aug 2025 17:05:51 -0500 Subject: [PATCH 7/9] Apply more suggestions. --- eslint.config.js | 2 +- src/analyze/attw.ts | 3 +- src/analyze/dependencies.ts | 3 +- src/analyze/publint.ts | 3 +- src/analyze/replacements.ts | 120 +++++++++++++++++------------------- 5 files changed, 59 insertions(+), 72 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index dedda10..5e5c704 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -14,7 +14,7 @@ export default [ '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-unused-vars': [ 'error', - {varsIgnorePattern: '^[A-Z_]'} + {varsIgnorePattern: '^[A-Z_]', argsIgnorePattern: '^_'} ] } } diff --git a/src/analyze/attw.ts b/src/analyze/attw.ts index 051ad0a..78e0fdc 100644 --- a/src/analyze/attw.ts +++ b/src/analyze/attw.ts @@ -11,8 +11,7 @@ import {TarballFileSystem} from '../tarball-file-system.js'; export async function runAttw( fileSystem: FileSystem, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - _?: Options + _options?: Options ): Promise { const result: ReportPluginResult = { messages: [] diff --git a/src/analyze/dependencies.ts b/src/analyze/dependencies.ts index cf28d15..12bb722 100644 --- a/src/analyze/dependencies.ts +++ b/src/analyze/dependencies.ts @@ -149,8 +149,7 @@ async function parsePackageJson( // Keep the existing tarball analysis for backward compatibility export async function runDependencyAnalysis( fileSystem: FileSystem, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - _?: Options + _options?: Options ): Promise { const packageFiles = await fileSystem.listPackageFiles(); const rootDir = await fileSystem.getRootDir(); diff --git a/src/analyze/publint.ts b/src/analyze/publint.ts index a1364e1..bba8fce 100644 --- a/src/analyze/publint.ts +++ b/src/analyze/publint.ts @@ -6,8 +6,7 @@ import {TarballFileSystem} from '../tarball-file-system.js'; export async function runPublint( fileSystem: FileSystem, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - _?: Options + _options?: Options ): Promise { const result: ReportPluginResult = { messages: [] diff --git a/src/analyze/replacements.ts b/src/analyze/replacements.ts index 6784930..f4c15b5 100644 --- a/src/analyze/replacements.ts +++ b/src/analyze/replacements.ts @@ -3,7 +3,7 @@ import type {ManifestModule, ModuleReplacement} from 'module-replacements'; import {ReportPluginResult} from '../types.js'; import type {FileSystem} from '../file-system.js'; import {getPackageJson} from '../file-system-utils.js'; -import {resolve, dirname} from 'node:path'; +import {resolve, dirname, basename} from 'node:path'; import { satisfies as semverSatisfies, ltr as semverLessThan, @@ -44,7 +44,7 @@ async function loadCustomManifests( try { const absolutePath = resolve(manifestPath); const manifestDir = dirname(absolutePath); - const manifestFileName = absolutePath.substring(manifestDir.length + 1); + const manifestFileName = basename(absolutePath); const localFileSystem = new LocalFileSystem(manifestDir); const manifestContent = await localFileSystem.readFile( `/${manifestFileName}` @@ -124,72 +124,62 @@ export async function runReplacements( } // Handle each replacement type using the same logic for both custom and built-in - switch (replacement.type) { - case 'none': - result.messages.push({ - severity: 'warning', - score: 0, - message: `Module "${name}" can be removed, and native functionality used instead` - }); - break; - - case 'simple': - result.messages.push({ - severity: 'warning', - score: 0, - message: `Module "${name}" can be replaced. ${replacement.replacement || 'See documentation for details'}.` - }); - break; - - case 'native': { - const enginesNode = packageJson.engines?.node; - let supported = true; - - if (replacement.nodeVersion && enginesNode) { - supported = isNodeEngineCompatible( - replacement.nodeVersion, - enginesNode - ); - } - - if (!supported) { - continue; - } - - const mdnPath = replacement.mdnPath - ? getMdnUrl(replacement.mdnPath) - : undefined; - const requires = - replacement.nodeVersion && !enginesNode - ? ` Required Node >= ${replacement.nodeVersion}.` - : ''; - const message = `Module "${name}" can be replaced with native functionality. Use "${replacement.replacement || 'native alternative'}" instead.${requires}`; - const fullMessage = mdnPath - ? `${message} You can read more at ${mdnPath}.` - : message; - result.messages.push({ - severity: 'warning', - score: 0, - message: fullMessage - }); - break; + if (replacement.type === 'none') { + result.messages.push({ + severity: 'warning', + score: 0, + message: `Module "${name}" can be removed, and native functionality used instead` + }); + } else if (replacement.type === 'simple') { + result.messages.push({ + severity: 'warning', + score: 0, + message: `Module "${name}" can be replaced. ${replacement.replacement}.` + }); + } else if (replacement.type === 'native') { + const enginesNode = packageJson.engines?.node; + let supported = true; + + if (replacement.nodeVersion && enginesNode) { + supported = isNodeEngineCompatible( + replacement.nodeVersion, + enginesNode + ); } - case 'documented': { - const docUrl = replacement.docPath - ? getDocsUrl(replacement.docPath) - : undefined; - const message = `Module "${name}" can be replaced with a more performant alternative.`; - const fullMessage = docUrl - ? `${message} See the list of available alternatives at ${docUrl}.` - : message; - result.messages.push({ - severity: 'warning', - score: 0, - message: fullMessage - }); - break; + if (!supported) { + continue; } + + const mdnPath = replacement.mdnPath + ? getMdnUrl(replacement.mdnPath) + : undefined; + const requires = + replacement.nodeVersion && !enginesNode + ? ` Required Node >= ${replacement.nodeVersion}.` + : ''; + const message = `Module "${name}" can be replaced with native functionality. Use "${replacement.replacement}" instead.${requires}`; + const fullMessage = mdnPath + ? `${message} You can read more at ${mdnPath}.` + : message; + result.messages.push({ + severity: 'warning', + score: 0, + message: fullMessage + }); + } else if (replacement.type === 'documented') { + const docUrl = replacement.docPath + ? getDocsUrl(replacement.docPath) + : undefined; + const message = `Module "${name}" can be replaced with a more performant alternative.`; + const fullMessage = docUrl + ? `${message} See the list of available alternatives at ${docUrl}.` + : message; + result.messages.push({ + severity: 'warning', + score: 0, + message: fullMessage + }); } } From 6ea5f906df4b52bbd95825a08657fcadf1128ed9 Mon Sep 17 00:00:00 2001 From: Paul Valladares <85648028+dreyfus92@users.noreply.github.com> Date: Thu, 21 Aug 2025 09:03:39 -0500 Subject: [PATCH 8/9] cleanup --- src/analyze/replacements.ts | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/analyze/replacements.ts b/src/analyze/replacements.ts index f4c15b5..7f0ee00 100644 --- a/src/analyze/replacements.ts +++ b/src/analyze/replacements.ts @@ -151,30 +151,22 @@ export async function runReplacements( continue; } - const mdnPath = replacement.mdnPath - ? getMdnUrl(replacement.mdnPath) - : undefined; + const mdnPath = getMdnUrl(replacement.mdnPath); const requires = replacement.nodeVersion && !enginesNode ? ` Required Node >= ${replacement.nodeVersion}.` : ''; const message = `Module "${name}" can be replaced with native functionality. Use "${replacement.replacement}" instead.${requires}`; - const fullMessage = mdnPath - ? `${message} You can read more at ${mdnPath}.` - : message; + const fullMessage = `${message} You can read more at ${mdnPath}.`; result.messages.push({ severity: 'warning', score: 0, message: fullMessage }); } else if (replacement.type === 'documented') { - const docUrl = replacement.docPath - ? getDocsUrl(replacement.docPath) - : undefined; + const docUrl = getDocsUrl(replacement.docPath); const message = `Module "${name}" can be replaced with a more performant alternative.`; - const fullMessage = docUrl - ? `${message} See the list of available alternatives at ${docUrl}.` - : message; + const fullMessage = `${message} See the list of available alternatives at ${docUrl}.`; result.messages.push({ severity: 'warning', score: 0, From 1c5eab4505aaf9afcfa9979a15efbedfff0d947b Mon Sep 17 00:00:00 2001 From: Paul Valladares <85648028+dreyfus92@users.noreply.github.com> Date: Thu, 21 Aug 2025 09:26:06 -0500 Subject: [PATCH 9/9] tests: use fake modules in fixtures and convert to snapshot testing --- .../custom-manifests.test.ts.snap | 106 ++++++++++++++++++ src/test/custom-manifests.test.ts | 46 ++------ test/fixtures/custom-manifest-2.json | 4 +- test/fixtures/custom-manifest.json | 10 +- test/fixtures/fake-modules/package.json | 13 +++ 5 files changed, 137 insertions(+), 42 deletions(-) create mode 100644 src/test/__snapshots__/custom-manifests.test.ts.snap create mode 100644 test/fixtures/fake-modules/package.json diff --git a/src/test/__snapshots__/custom-manifests.test.ts.snap b/src/test/__snapshots__/custom-manifests.test.ts.snap new file mode 100644 index 0000000..2ebaff8 --- /dev/null +++ b/src/test/__snapshots__/custom-manifests.test.ts.snap @@ -0,0 +1,106 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Custom Manifests > should handle invalid manifest files gracefully 1`] = `[]`; + +exports[`Custom Manifests > should load and use custom manifest files 1`] = ` +[ + { + "message": "Module "@e18e/fake-0" can be replaced. Use picocolors, kleur, or native console styling.", + "score": 0, + "severity": "warning", + }, + { + "message": "Module "@e18e/fake-1" can be replaced. Use native JavaScript methods or specific lodash functions.", + "score": 0, + "severity": "warning", + }, + { + "message": "Module "@e18e/fake-2" can be replaced with native functionality. Use "Intl.DateTimeFormat or Date methods" instead. Required Node >= 12.0.0. You can read more at https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat.", + "score": 0, + "severity": "warning", + }, + { + "message": "Module "@e18e/fake-3" can be replaced with a more performant alternative. See the list of available alternatives at https://github.com/es-tooling/eslint-plugin-depend/blob/main/docs/rules/request-alternatives.md.", + "score": 0, + "severity": "warning", + }, + { + "message": "Module "@e18e/fake-4" can be removed, and native functionality used instead", + "score": 0, + "severity": "warning", + }, +] +`; + +exports[`Custom Manifests > should load multiple manifest files 1`] = ` +[ + { + "message": "Module "@e18e/fake-0" can be replaced. Use picocolors, kleur, or native console styling.", + "score": 0, + "severity": "warning", + }, + { + "message": "Module "@e18e/fake-1" can be replaced. Use native JavaScript methods or specific lodash functions.", + "score": 0, + "severity": "warning", + }, + { + "message": "Module "@e18e/fake-2" can be replaced with native functionality. Use "Intl.DateTimeFormat or Date methods" instead. Required Node >= 12.0.0. You can read more at https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat.", + "score": 0, + "severity": "warning", + }, + { + "message": "Module "@e18e/fake-3" can be replaced with a more performant alternative. See the list of available alternatives at https://github.com/es-tooling/eslint-plugin-depend/blob/main/docs/rules/request-alternatives.md.", + "score": 0, + "severity": "warning", + }, + { + "message": "Module "@e18e/fake-4" can be removed, and native functionality used instead", + "score": 0, + "severity": "warning", + }, + { + "message": "Module "@e18e/fake-5" can be replaced. Use Fastify, Koa, or native Node.js http module.", + "score": 0, + "severity": "warning", + }, + { + "message": "Module "@e18e/fake-6" can be removed, and native functionality used instead", + "score": 0, + "severity": "warning", + }, +] +`; + +exports[`Custom Manifests > should prioritize custom replacements over built-in ones 1`] = ` +{ + "withCustom": [ + { + "message": "Module "@e18e/fake-0" can be replaced. Use picocolors, kleur, or native console styling.", + "score": 0, + "severity": "warning", + }, + { + "message": "Module "@e18e/fake-1" can be replaced. Use native JavaScript methods or specific lodash functions.", + "score": 0, + "severity": "warning", + }, + { + "message": "Module "@e18e/fake-2" can be replaced with native functionality. Use "Intl.DateTimeFormat or Date methods" instead. Required Node >= 12.0.0. You can read more at https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat.", + "score": 0, + "severity": "warning", + }, + { + "message": "Module "@e18e/fake-3" can be replaced with a more performant alternative. See the list of available alternatives at https://github.com/es-tooling/eslint-plugin-depend/blob/main/docs/rules/request-alternatives.md.", + "score": 0, + "severity": "warning", + }, + { + "message": "Module "@e18e/fake-4" can be removed, and native functionality used instead", + "score": 0, + "severity": "warning", + }, + ], + "withoutCustom": [], +} +`; diff --git a/src/test/custom-manifests.test.ts b/src/test/custom-manifests.test.ts index 34026d6..03808b8 100644 --- a/src/test/custom-manifests.test.ts +++ b/src/test/custom-manifests.test.ts @@ -9,7 +9,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url)); describe('Custom Manifests', () => { it('should load and use custom manifest files', async () => { - const testDir = join(__dirname, '../../test/fixtures/basic-chalk'); + const testDir = join(__dirname, '../../test/fixtures/fake-modules'); const fileSystem = new LocalFileSystem(testDir); const customManifestPath = join( __dirname, @@ -20,24 +20,11 @@ describe('Custom Manifests', () => { manifest: [customManifestPath] }); - // Should have messages from custom manifest - expect(result.messages.length).toBeGreaterThan(0); - - // Check that custom replacement messages are included - const hasCustomMessage = result.messages.some( - (msg) => - msg.message.includes('chalk') || - msg.message.includes('lodash') || - msg.message.includes('moment') || - msg.message.includes('request') || - msg.message.includes('bluebird') - ); - - expect(hasCustomMessage).toBe(true); + expect(result.messages).toMatchSnapshot(); }); it('should handle invalid manifest files gracefully', async () => { - const testDir = join(__dirname, '../../test/fixtures/basic-chalk'); + const testDir = join(__dirname, '../../test/fixtures/fake-modules'); const fileSystem = new LocalFileSystem(testDir); const invalidManifestPath = 'non-existent-file.json'; @@ -45,12 +32,11 @@ describe('Custom Manifests', () => { manifest: [invalidManifestPath] }); - // Should still work without crashing - expect(result.messages).toBeDefined(); + expect(result.messages).toMatchSnapshot(); }); it('should prioritize custom replacements over built-in ones', async () => { - const testDir = join(__dirname, '../../test/fixtures/basic-chalk'); + const testDir = join(__dirname, '../../test/fixtures/fake-modules'); const fileSystem = new LocalFileSystem(testDir); const customManifestPath = join( __dirname, @@ -62,14 +48,14 @@ describe('Custom Manifests', () => { }); const resultWithoutCustom = await runReplacements(fileSystem); - // Custom manifest should provide additional or different messages - expect(resultWithCustom.messages.length).toBeGreaterThanOrEqual( - resultWithoutCustom.messages.length - ); + expect({ + withCustom: resultWithCustom.messages, + withoutCustom: resultWithoutCustom.messages + }).toMatchSnapshot(); }); it('should load multiple manifest files', async () => { - const testDir = join(__dirname, '../../test/fixtures/basic-chalk'); + const testDir = join(__dirname, '../../test/fixtures/fake-modules'); const fileSystem = new LocalFileSystem(testDir); const manifest1Path = join( __dirname, @@ -84,16 +70,6 @@ describe('Custom Manifests', () => { manifest: [manifest1Path, manifest2Path] }); - // Should have messages from both manifests - expect(result.messages.length).toBeGreaterThan(0); - - // Check that replacements from both manifests are included - const hasChalkMessage = result.messages.some((msg) => - msg.message.includes('chalk') - ); - - // Note: express won't be found since it's not in the test fixture dependencies - // but chalk should be found - expect(hasChalkMessage).toBe(true); + expect(result.messages).toMatchSnapshot(); }); }); diff --git a/test/fixtures/custom-manifest-2.json b/test/fixtures/custom-manifest-2.json index 70d771b..a979190 100644 --- a/test/fixtures/custom-manifest-2.json +++ b/test/fixtures/custom-manifest-2.json @@ -1,12 +1,12 @@ { "moduleReplacements": [ { - "moduleName": "express", + "moduleName": "@e18e/fake-5", "type": "simple", "replacement": "Use Fastify, Koa, or native Node.js http module" }, { - "moduleName": "body-parser", + "moduleName": "@e18e/fake-6", "type": "none", "replacement": "Use native JSON.parse or built-in middleware" } diff --git a/test/fixtures/custom-manifest.json b/test/fixtures/custom-manifest.json index d1968a0..6170e1a 100644 --- a/test/fixtures/custom-manifest.json +++ b/test/fixtures/custom-manifest.json @@ -1,30 +1,30 @@ { "moduleReplacements": [ { - "moduleName": "chalk", + "moduleName": "@e18e/fake-0", "type": "simple", "replacement": "Use picocolors, kleur, or native console styling" }, { - "moduleName": "lodash", + "moduleName": "@e18e/fake-1", "type": "simple", "replacement": "Use native JavaScript methods or specific lodash functions" }, { - "moduleName": "moment", + "moduleName": "@e18e/fake-2", "type": "native", "replacement": "Intl.DateTimeFormat or Date methods", "mdnPath": "Global_Objects/Intl/DateTimeFormat", "nodeVersion": "12.0.0" }, { - "moduleName": "request", + "moduleName": "@e18e/fake-3", "type": "documented", "replacement": "node-fetch, axios, or native fetch", "docPath": "request-alternatives" }, { - "moduleName": "bluebird", + "moduleName": "@e18e/fake-4", "type": "none", "replacement": "Use native Promise methods" } diff --git a/test/fixtures/fake-modules/package.json b/test/fixtures/fake-modules/package.json new file mode 100644 index 0000000..9e757cf --- /dev/null +++ b/test/fixtures/fake-modules/package.json @@ -0,0 +1,13 @@ +{ + "name": "fake-modules-test", + "version": "1.0.0", + "dependencies": { + "@e18e/fake-0": "^1.0.0", + "@e18e/fake-1": "^2.0.0", + "@e18e/fake-2": "^3.0.0", + "@e18e/fake-3": "^4.0.0", + "@e18e/fake-4": "^5.0.0", + "@e18e/fake-5": "^6.0.0", + "@e18e/fake-6": "^7.0.0" + } +}