From dc4c6c6d2c9f16aacdf01ba9402ab804d3fa0c1e Mon Sep 17 00:00:00 2001 From: Paul Valladares <85648028+dreyfus92@users.noreply.github.com> Date: Tue, 3 Mar 2026 18:48:14 -0600 Subject: [PATCH 01/10] feat: migrate to module-replacements v3 --- package-lock.json | 8 +- package.json | 2 +- scripts/generate-fixable-replacements.ts | 31 +++-- src/analyze/replacements.ts | 159 +++++++++++++---------- src/test/__snapshots__/cli.test.ts.snap | 64 +++++---- test/fixtures/custom-manifest-2.json | 28 ++-- test/fixtures/custom-manifest.json | 75 ++++++++--- 7 files changed, 224 insertions(+), 143 deletions(-) diff --git a/package-lock.json b/package-lock.json index 48c14a5..3c8db43 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "fdir": "^6.5.0", "gunshi": "^0.29.2", "lockparse": "^0.5.0", - "module-replacements": "^2.11.0", + "module-replacements": "^3.0.0-beta.0", "module-replacements-codemods": "^1.2.0", "obug": "^2.1.1", "package-manager-detector": "^1.6.0", @@ -4054,9 +4054,9 @@ } }, "node_modules/module-replacements": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/module-replacements/-/module-replacements-2.11.0.tgz", - "integrity": "sha512-j5sNQm3VCpQQ7nTqGeOZtoJtV3uKERgCBm9QRhmGRiXiqkf7iRFOkfxdJRZWLkqYY8PNf4cDQF/WfXUYLENrRA==", + "version": "3.0.0-beta.0", + "resolved": "https://registry.npmjs.org/module-replacements/-/module-replacements-3.0.0-beta.0.tgz", + "integrity": "sha512-T/REIieQTHKscHccdxRd5A2mYIPPIVuKq652gyAZK1D2hmRCWUi7whDskKiA87knsoCWDDMzjcJHqsCXNWr0Qw==", "license": "MIT" }, "node_modules/module-replacements-codemods": { diff --git a/package.json b/package.json index 0bbb896..cb907fb 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "fdir": "^6.5.0", "gunshi": "^0.29.2", "lockparse": "^0.5.0", - "module-replacements": "^2.11.0", + "module-replacements": "^3.0.0-beta.0", "module-replacements-codemods": "^1.2.0", "obug": "^2.1.1", "package-manager-detector": "^1.6.0", diff --git a/scripts/generate-fixable-replacements.ts b/scripts/generate-fixable-replacements.ts index 0d8f084..03a0f26 100644 --- a/scripts/generate-fixable-replacements.ts +++ b/scripts/generate-fixable-replacements.ts @@ -3,29 +3,38 @@ import {join, dirname} from 'node:path'; import {fileURLToPath} from 'node:url'; import {all} from 'module-replacements'; import {codemods} from 'module-replacements-codemods'; -import {fixableReplacements} from '../lib/commands/fixable-replacements.js'; const __dirname = dirname(fileURLToPath(import.meta.url)); -async function generateFixableReplacements() { - const existingReplacements = new Map( - fixableReplacements.map((r) => [r.from, r]) - ); +function getReplacementTarget(moduleName: string): string { + const mapping = all.mappings[moduleName]; + if (!mapping?.replacements?.length) return moduleName; + + const firstId = mapping.replacements[0]!; + const replacement = all.replacements[firstId]; + if (!replacement) return firstId; + + if (replacement.type === 'documented' && replacement.replacementModule) { + return replacement.replacementModule; + } + + return replacement.id; +} +async function generateFixableReplacements() { let newCode = `import type { Replacement } from '../types.js';\n`; newCode += `import { codemods } from 'module-replacements-codemods';\n\n`; newCode += `export const fixableReplacements: Replacement[] = [\n`; let count = 0; - for (const replacement of all.moduleReplacements) { - if (replacement.moduleName in codemods) { - const existing = existingReplacements.get(replacement.moduleName); - const to = existing?.to ?? 'TODO'; + for (const moduleName of Object.keys(all.mappings)) { + if (moduleName in codemods) { + const to = getReplacementTarget(moduleName); newCode += ` {\n`; - newCode += ` from: '${replacement.moduleName}',\n`; + newCode += ` from: '${moduleName}',\n`; newCode += ` to: '${to}',\n`; - newCode += ` factory: codemods['${replacement.moduleName}']\n`; + newCode += ` factory: codemods['${moduleName}']\n`; newCode += ` },\n`; count++; } diff --git a/src/analyze/replacements.ts b/src/analyze/replacements.ts index a3e5e44..b7c5a28 100644 --- a/src/analyze/replacements.ts +++ b/src/analyze/replacements.ts @@ -1,5 +1,10 @@ import * as replacements from 'module-replacements'; -import type {ManifestModule, ModuleReplacement} from 'module-replacements'; +import type { + ManifestModule, + ModuleReplacement, + EngineConstraint, + KnownUrl +} from 'module-replacements'; import type {ReportPluginResult, AnalysisContext} from '../types.js'; import {fixableReplacements} from '../commands/fixable-replacements.js'; import {getPackageJson} from '../utils/package-json.js'; @@ -13,27 +18,55 @@ import { import {LocalFileSystem} from '../local-file-system.js'; /** - * Generates a standard URL to the docs of a given rule - * @param {string} name Rule name - * @return {string} + * Resolves a v3 KnownUrl to a full URL string. */ -export function getDocsUrl(name: string): string { - return `https://github.com/es-tooling/module-replacements/blob/main/docs/modules/${name}.md`; +export function resolveUrl(url: KnownUrl): string { + if (typeof url === 'string') return url; + switch (url.type) { + case 'mdn': + return `https://developer.mozilla.org/en-US/docs/${url.id}`; + case 'node': + return `https://nodejs.org/docs/latest/${url.id}`; + case 'e18e': + return `https://github.com/es-tooling/module-replacements/blob/main/docs/modules/${url.id}.md`; + } } -/** - * Generates a URL for the given path on MDN - * @param {string} path Docs path - * @return {string} - */ -export function getMdnUrl(path: string): string { - return `https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/${path}`; +function getNodeMinVersion( + engines?: EngineConstraint[] +): string | undefined { + return engines?.find((e) => e.engine === 'nodejs')?.minVersion; +} + +function isNodeEngineCompatible( + requiredNode: string, + enginesNode: string +): boolean { + const requiredRange = validRange(requiredNode); + const engineRange = validRange(enginesNode); + + if (!requiredRange || !engineRange) { + return true; + } + + const requiredMin = minVersion(requiredRange); + if (!requiredMin) { + return true; + } + + return ( + semverLessThan(requiredMin.version, engineRange) || + semverSatisfies(requiredMin.version, engineRange) + ); } async function loadCustomManifests( manifestPaths: string[] -): Promise { - const customReplacements: ModuleReplacement[] = []; +): Promise { + const result: ManifestModule = { + mappings: {}, + replacements: {} + }; for (const manifestPath of manifestPaths) { try { @@ -46,11 +79,11 @@ async function loadCustomManifests( ); const manifest: ManifestModule = JSON.parse(manifestContent); - if ( - manifest.moduleReplacements && - Array.isArray(manifest.moduleReplacements) - ) { - customReplacements.push(...manifest.moduleReplacements); + if (manifest.mappings) { + Object.assign(result.mappings, manifest.mappings); + } + if (manifest.replacements) { + Object.assign(result.replacements, manifest.replacements); } } catch (error) { console.warn( @@ -59,29 +92,7 @@ async function loadCustomManifests( } } - return customReplacements; -} - -function isNodeEngineCompatible( - requiredNode: string, - enginesNode: string -): boolean { - const requiredRange = validRange(requiredNode); - const engineRange = validRange(enginesNode); - - if (!requiredRange || !engineRange) { - return true; - } - - const requiredMin = minVersion(requiredRange); - if (!requiredMin) { - return true; - } - - return ( - semverLessThan(requiredMin.version, engineRange) || - semverSatisfies(requiredMin.version, engineRange) - ); + return result; } export async function runReplacements( @@ -94,37 +105,44 @@ export async function runReplacements( const packageJson = await getPackageJson(context.fs); if (!packageJson || !packageJson.dependencies) { - // No dependencies return result; } - // Load custom manifests - const customReplacements = context.options?.manifest + const customManifest = context.options?.manifest ? await loadCustomManifests(context.options.manifest) - : []; + : {mappings: {}, replacements: {}}; - // Combine custom and built-in replacements - const allReplacements = [ - ...customReplacements, - ...replacements.all.moduleReplacements - ]; + // Custom mappings take precedence over built-in + const allMappings = { + ...replacements.all.mappings, + ...customManifest.mappings + }; + const allReplacementDefs: Record = { + ...replacements.all.replacements, + ...customManifest.replacements + }; const fixableByMigrate = new Set(fixableReplacements.map((r) => r.from)); for (const name of Object.keys(packageJson.dependencies)) { - // Find replacement (custom replacements take precedence due to order) - const replacement = allReplacements.find( - (replacement) => replacement.moduleName === name - ); + const mapping = allMappings[name]; + if (!mapping) { + continue; + } + + const replacementIds = mapping.replacements; + if (!replacementIds || replacementIds.length === 0) { + continue; + } + const replacement = allReplacementDefs[replacementIds[0]]; if (!replacement) { continue; } const fixableBy = fixableByMigrate.has(name) ? 'migrate' : undefined; - // Handle each replacement type using the same logic for both custom and built-in - if (replacement.type === 'none') { + if (replacement.type === 'removal') { result.messages.push({ severity: 'warning', score: 0, @@ -135,31 +153,30 @@ 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.description}.`, ...(fixableBy && {fixableBy}) }); } else if (replacement.type === 'native') { const enginesNode = packageJson.engines?.node; + const nodeVersion = getNodeMinVersion(replacement.engines); let supported = true; - if (replacement.nodeVersion && enginesNode) { - supported = isNodeEngineCompatible( - replacement.nodeVersion, - enginesNode - ); + if (nodeVersion && enginesNode) { + supported = isNodeEngineCompatible(nodeVersion, enginesNode); } if (!supported) { continue; } - const mdnPath = getMdnUrl(replacement.mdnPath); + const urlStr = resolveUrl(replacement.url); const requires = - replacement.nodeVersion && !enginesNode - ? ` Required Node >= ${replacement.nodeVersion}.` + nodeVersion && !enginesNode + ? ` Required Node >= ${nodeVersion}.` : ''; - const message = `Module "${name}" can be replaced with native functionality. Use "${replacement.replacement}" instead.${requires}`; - const fullMessage = `${message} You can read more at ${mdnPath}.`; + const description = replacement.description ?? replacement.id; + const message = `Module "${name}" can be replaced with native functionality. Use "${description}" instead.${requires}`; + const fullMessage = `${message} You can read more at ${urlStr}.`; result.messages.push({ severity: 'warning', score: 0, @@ -167,9 +184,9 @@ export async function runReplacements( ...(fixableBy && {fixableBy}) }); } else if (replacement.type === 'documented') { - const docUrl = getDocsUrl(replacement.docPath); + const urlStr = resolveUrl(replacement.url); const message = `Module "${name}" can be replaced with a more performant alternative.`; - const fullMessage = `${message} See the list of available alternatives at ${docUrl}.`; + const fullMessage = `${message} See the list of available alternatives at ${urlStr}.`; result.messages.push({ severity: 'warning', score: 0, diff --git a/src/test/__snapshots__/cli.test.ts.snap b/src/test/__snapshots__/cli.test.ts.snap index bd6e3f4..d76b67b 100644 --- a/src/test/__snapshots__/cli.test.ts.snap +++ b/src/test/__snapshots__/cli.test.ts.snap @@ -3,43 +3,51 @@ exports[`CLI > should display package report 1`] = ` "e18e (cli ) -┌ Analyzing... -│ -● Summary -│ Package Name mock-package -│ Version 1.0.0 -│ Install Size 53.0 B -│ Dependencies 1 (1 production, 0 development) -│ Duplicate Dependency Count 0 -│ -● Results: -│ -│ -└ Done! +┌ Analyzing... +│ +● Summary +│ Package Name mock-package +│ Version 1.0.0 +│ Install Size 53.0 B +│ Dependencies 1 (1 production, 0 development) +│ Duplicate Dependency Count 0 +│ +● Results: +│ +│ +└ Done! " `; -exports[`CLI > should display package report 2`] = `""`; +exports[`CLI > should display package report 2`] = ` +"(node:) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set. +(Use \`node --trace-warnings ...\` to show where the warning was created) +" +`; exports[`CLI > should run successfully with default options 1`] = ` "e18e (cli ) -┌ Analyzing... -│ -● Summary -│ Package Name mock-package -│ Version 1.0.0 -│ Install Size 53.0 B -│ Dependencies 1 (1 production, 0 development) -│ Duplicate Dependency Count 0 -│ -● Results: -│ -│ -└ Done! +┌ Analyzing... +│ +● Summary +│ Package Name mock-package +│ Version 1.0.0 +│ Install Size 53.0 B +│ Dependencies 1 (1 production, 0 development) +│ Duplicate Dependency Count 0 +│ +● Results: +│ +│ +└ Done! " `; -exports[`CLI > should run successfully with default options 2`] = `""`; +exports[`CLI > should run successfully with default options 2`] = ` +"(node:) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set. +(Use \`node --trace-warnings ...\` to show where the warning was created) +" +`; diff --git a/test/fixtures/custom-manifest-2.json b/test/fixtures/custom-manifest-2.json index a979190..bd828f1 100644 --- a/test/fixtures/custom-manifest-2.json +++ b/test/fixtures/custom-manifest-2.json @@ -1,14 +1,26 @@ { - "moduleReplacements": [ - { + "mappings": { + "@e18e/fake-5": { + "type": "module", "moduleName": "@e18e/fake-5", - "type": "simple", - "replacement": "Use Fastify, Koa, or native Node.js http module" + "replacements": ["fake-5-simple"] }, - { + "@e18e/fake-6": { + "type": "module", "moduleName": "@e18e/fake-6", - "type": "none", - "replacement": "Use native JSON.parse or built-in middleware" + "replacements": ["fake-6-removal"] + } + }, + "replacements": { + "fake-5-simple": { + "id": "fake-5-simple", + "type": "simple", + "description": "Use Fastify, Koa, or native Node.js http module" + }, + "fake-6-removal": { + "id": "fake-6-removal", + "type": "removal", + "description": "Use native JSON.parse or built-in middleware" } - ] + } } diff --git a/test/fixtures/custom-manifest.json b/test/fixtures/custom-manifest.json index 6170e1a..741df4e 100644 --- a/test/fixtures/custom-manifest.json +++ b/test/fixtures/custom-manifest.json @@ -1,32 +1,67 @@ { - "moduleReplacements": [ - { + "mappings": { + "@e18e/fake-0": { + "type": "module", "moduleName": "@e18e/fake-0", - "type": "simple", - "replacement": "Use picocolors, kleur, or native console styling" + "replacements": ["fake-0-simple"] }, - { + "@e18e/fake-1": { + "type": "module", "moduleName": "@e18e/fake-1", - "type": "simple", - "replacement": "Use native JavaScript methods or specific lodash functions" + "replacements": ["fake-1-simple"] }, - { + "@e18e/fake-2": { + "type": "module", "moduleName": "@e18e/fake-2", - "type": "native", - "replacement": "Intl.DateTimeFormat or Date methods", - "mdnPath": "Global_Objects/Intl/DateTimeFormat", - "nodeVersion": "12.0.0" + "replacements": ["fake-2-native"] }, - { + "@e18e/fake-3": { + "type": "module", "moduleName": "@e18e/fake-3", - "type": "documented", - "replacement": "node-fetch, axios, or native fetch", - "docPath": "request-alternatives" + "replacements": ["fake-3-documented"] }, - { + "@e18e/fake-4": { + "type": "module", "moduleName": "@e18e/fake-4", - "type": "none", - "replacement": "Use native Promise methods" + "replacements": ["fake-4-removal"] + } + }, + "replacements": { + "fake-0-simple": { + "id": "fake-0-simple", + "type": "simple", + "description": "Use picocolors, kleur, or native console styling" + }, + "fake-1-simple": { + "id": "fake-1-simple", + "type": "simple", + "description": "Use native JavaScript methods or specific lodash functions" + }, + "fake-2-native": { + "id": "fake-2-native", + "type": "native", + "description": "Intl.DateTimeFormat or Date methods", + "url": { + "type": "mdn", + "id": "Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat" + }, + "engines": [ + { "engine": "nodejs", "minVersion": "12.0.0" } + ] + }, + "fake-3-documented": { + "id": "fake-3-documented", + "type": "documented", + "url": { + "type": "e18e", + "id": "request-alternatives" + }, + "replacementModule": "node-fetch" + }, + "fake-4-removal": { + "id": "fake-4-removal", + "type": "removal", + "description": "Use native Promise methods" } - ] + } } From 36fa461549b77c3937fc3ee2f07e0a577104fad0 Mon Sep 17 00:00:00 2001 From: Paul Valladares <85648028+dreyfus92@users.noreply.github.com> Date: Tue, 3 Mar 2026 18:49:10 -0600 Subject: [PATCH 02/10] format --- src/analyze/replacements.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/analyze/replacements.ts b/src/analyze/replacements.ts index b7c5a28..d4c8512 100644 --- a/src/analyze/replacements.ts +++ b/src/analyze/replacements.ts @@ -32,9 +32,7 @@ export function resolveUrl(url: KnownUrl): string { } } -function getNodeMinVersion( - engines?: EngineConstraint[] -): string | undefined { +function getNodeMinVersion(engines?: EngineConstraint[]): string | undefined { return engines?.find((e) => e.engine === 'nodejs')?.minVersion; } @@ -171,9 +169,7 @@ export async function runReplacements( const urlStr = resolveUrl(replacement.url); const requires = - nodeVersion && !enginesNode - ? ` Required Node >= ${nodeVersion}.` - : ''; + nodeVersion && !enginesNode ? ` Required Node >= ${nodeVersion}.` : ''; const description = replacement.description ?? replacement.id; const message = `Module "${name}" can be replaced with native functionality. Use "${description}" instead.${requires}`; const fullMessage = `${message} You can read more at ${urlStr}.`; From ab14030cf724035c8a4e25528322f43d42c9dfda Mon Sep 17 00:00:00 2001 From: Paul Valladares <85648028+dreyfus92@users.noreply.github.com> Date: Tue, 3 Mar 2026 18:53:18 -0600 Subject: [PATCH 03/10] update: snapshopts --- src/test/__snapshots__/cli.test.ts.snap | 64 +++++++++++-------------- 1 file changed, 28 insertions(+), 36 deletions(-) diff --git a/src/test/__snapshots__/cli.test.ts.snap b/src/test/__snapshots__/cli.test.ts.snap index d76b67b..bd6e3f4 100644 --- a/src/test/__snapshots__/cli.test.ts.snap +++ b/src/test/__snapshots__/cli.test.ts.snap @@ -3,51 +3,43 @@ exports[`CLI > should display package report 1`] = ` "e18e (cli ) -┌ Analyzing... -│ -● Summary -│ Package Name mock-package -│ Version 1.0.0 -│ Install Size 53.0 B -│ Dependencies 1 (1 production, 0 development) -│ Duplicate Dependency Count 0 -│ -● Results: -│ -│ -└ Done! +┌ Analyzing... +│ +● Summary +│ Package Name mock-package +│ Version 1.0.0 +│ Install Size 53.0 B +│ Dependencies 1 (1 production, 0 development) +│ Duplicate Dependency Count 0 +│ +● Results: +│ +│ +└ Done! " `; -exports[`CLI > should display package report 2`] = ` -"(node:) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set. -(Use \`node --trace-warnings ...\` to show where the warning was created) -" -`; +exports[`CLI > should display package report 2`] = `""`; exports[`CLI > should run successfully with default options 1`] = ` "e18e (cli ) -┌ Analyzing... -│ -● Summary -│ Package Name mock-package -│ Version 1.0.0 -│ Install Size 53.0 B -│ Dependencies 1 (1 production, 0 development) -│ Duplicate Dependency Count 0 -│ -● Results: -│ -│ -└ Done! +┌ Analyzing... +│ +● Summary +│ Package Name mock-package +│ Version 1.0.0 +│ Install Size 53.0 B +│ Dependencies 1 (1 production, 0 development) +│ Duplicate Dependency Count 0 +│ +● Results: +│ +│ +└ Done! " `; -exports[`CLI > should run successfully with default options 2`] = ` -"(node:) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set. -(Use \`node --trace-warnings ...\` to show where the warning was created) -" -`; +exports[`CLI > should run successfully with default options 2`] = `""`; From 255977008e01a3387b46b3bf4b7e40e41a4426c3 Mon Sep 17 00:00:00 2001 From: Paul Valladares <85648028+dreyfus92@users.noreply.github.com> Date: Wed, 4 Mar 2026 11:51:09 -0600 Subject: [PATCH 04/10] fix: drop description from native replacement messages --- src/analyze/replacements.ts | 3 +-- src/test/__snapshots__/custom-manifests.test.ts.snap | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/analyze/replacements.ts b/src/analyze/replacements.ts index d4c8512..d958ce3 100644 --- a/src/analyze/replacements.ts +++ b/src/analyze/replacements.ts @@ -170,8 +170,7 @@ export async function runReplacements( const urlStr = resolveUrl(replacement.url); const requires = nodeVersion && !enginesNode ? ` Required Node >= ${nodeVersion}.` : ''; - const description = replacement.description ?? replacement.id; - const message = `Module "${name}" can be replaced with native functionality. Use "${description}" instead.${requires}`; + const message = `Module "${name}" can be replaced with native functionality.${requires}`; const fullMessage = `${message} You can read more at ${urlStr}.`; result.messages.push({ severity: 'warning', diff --git a/src/test/__snapshots__/custom-manifests.test.ts.snap b/src/test/__snapshots__/custom-manifests.test.ts.snap index 9c4d3f3..e8e147d 100644 --- a/src/test/__snapshots__/custom-manifests.test.ts.snap +++ b/src/test/__snapshots__/custom-manifests.test.ts.snap @@ -15,7 +15,7 @@ exports[`Custom Manifests > should load and use custom manifest files 1`] = ` "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.", + "message": "Module "@e18e/fake-2" can be replaced with native functionality. 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", }, @@ -45,7 +45,7 @@ exports[`Custom Manifests > should load multiple manifest files 1`] = ` "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.", + "message": "Module "@e18e/fake-2" can be replaced with native functionality. 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", }, @@ -86,7 +86,7 @@ exports[`Custom Manifests > should prioritize custom replacements over built-in "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.", + "message": "Module "@e18e/fake-2" can be replaced with native functionality. 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", }, From 8659565f86152add818b0fc52b7b91b9ba1231fc Mon Sep 17 00:00:00 2001 From: Paul Valladares <85648028+dreyfus92@users.noreply.github.com> Date: Wed, 4 Mar 2026 14:08:02 -0600 Subject: [PATCH 05/10] refactor: prefer mapping url as canonical docs link --- src/analyze/replacements.ts | 90 ++++++++++++------- .../custom-manifests.test.ts.snap | 12 +-- test/fixtures/custom-manifest.json | 6 +- 3 files changed, 66 insertions(+), 42 deletions(-) diff --git a/src/analyze/replacements.ts b/src/analyze/replacements.ts index d958ce3..d2cda4d 100644 --- a/src/analyze/replacements.ts +++ b/src/analyze/replacements.ts @@ -58,6 +58,27 @@ function isNodeEngineCompatible( ); } +function findFirstCompatibleReplacement( + replacementIds: string[], + defs: Record, + enginesNode: string | undefined +): ModuleReplacement | undefined { + for (const id of replacementIds) { + const replacement = defs[id]; + if (!replacement) continue; + + if (replacement.type === 'native' && enginesNode) { + const nodeVersion = getNodeMinVersion(replacement.engines); + if (nodeVersion && !isNodeEngineCompatible(nodeVersion, enginesNode)) { + continue; + } + } + + return replacement; + } + return undefined; +} + async function loadCustomManifests( manifestPaths: string[] ): Promise { @@ -121,71 +142,72 @@ export async function runReplacements( }; const fixableByMigrate = new Set(fixableReplacements.map((r) => r.from)); + const enginesNode = packageJson.engines?.node; for (const name of Object.keys(packageJson.dependencies)) { const mapping = allMappings[name]; - if (!mapping) { - continue; - } - - const replacementIds = mapping.replacements; - if (!replacementIds || replacementIds.length === 0) { + if (!mapping?.replacements?.length) { continue; } - const replacement = allReplacementDefs[replacementIds[0]]; - if (!replacement) { + const firstCompatible = findFirstCompatibleReplacement( + mapping.replacements, + allReplacementDefs, + enginesNode + ); + if (!firstCompatible) { continue; } const fixableBy = fixableByMigrate.has(name) ? 'migrate' : undefined; - - if (replacement.type === 'removal') { + const mappingUrl = mapping.url ? resolveUrl(mapping.url) : undefined; + + if (mappingUrl) { + let message = `Module "${name}" can be replaced.`; + if ( + firstCompatible.type === 'documented' && + firstCompatible.replacementModule + ) { + message += ` We recommend switching to "${firstCompatible.replacementModule}".`; + } + message += ` See more at ${mappingUrl}.`; + result.messages.push({ + severity: 'warning', + score: 0, + message, + ...(fixableBy && {fixableBy}) + }); + } else if (firstCompatible.type === 'removal') { result.messages.push({ severity: 'warning', score: 0, message: `Module "${name}" can be removed, and native functionality used instead`, ...(fixableBy && {fixableBy}) }); - } else if (replacement.type === 'simple') { + } else if (firstCompatible.type === 'simple') { result.messages.push({ severity: 'warning', score: 0, - message: `Module "${name}" can be replaced. ${replacement.description}.`, + message: `Module "${name}" can be replaced. ${firstCompatible.description}.`, ...(fixableBy && {fixableBy}) }); - } else if (replacement.type === 'native') { - const enginesNode = packageJson.engines?.node; - const nodeVersion = getNodeMinVersion(replacement.engines); - let supported = true; - - if (nodeVersion && enginesNode) { - supported = isNodeEngineCompatible(nodeVersion, enginesNode); - } - - if (!supported) { - continue; - } - - const urlStr = resolveUrl(replacement.url); + } else if (firstCompatible.type === 'native') { + const nodeVersion = getNodeMinVersion(firstCompatible.engines); const requires = nodeVersion && !enginesNode ? ` Required Node >= ${nodeVersion}.` : ''; - const message = `Module "${name}" can be replaced with native functionality.${requires}`; - const fullMessage = `${message} You can read more at ${urlStr}.`; + const urlStr = resolveUrl(firstCompatible.url); result.messages.push({ severity: 'warning', score: 0, - message: fullMessage, + message: `Module "${name}" can be replaced with native functionality.${requires} You can read more at ${urlStr}.`, ...(fixableBy && {fixableBy}) }); - } else if (replacement.type === 'documented') { - const urlStr = resolveUrl(replacement.url); - const message = `Module "${name}" can be replaced with a more performant alternative.`; - const fullMessage = `${message} See the list of available alternatives at ${urlStr}.`; + } else if (firstCompatible.type === 'documented') { + const urlStr = resolveUrl(firstCompatible.url); result.messages.push({ severity: 'warning', score: 0, - message: fullMessage, + message: `Module "${name}" can be replaced with a more performant alternative. See the list of available alternatives at ${urlStr}.`, ...(fixableBy && {fixableBy}) }); } diff --git a/src/test/__snapshots__/custom-manifests.test.ts.snap b/src/test/__snapshots__/custom-manifests.test.ts.snap index e8e147d..5d2b442 100644 --- a/src/test/__snapshots__/custom-manifests.test.ts.snap +++ b/src/test/__snapshots__/custom-manifests.test.ts.snap @@ -5,7 +5,7 @@ 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.", + "message": "Module "@e18e/fake-0" can be replaced. See more at https://github.com/es-tooling/module-replacements/blob/main/docs/modules/fake-0.md.", "score": 0, "severity": "warning", }, @@ -20,7 +20,7 @@ exports[`Custom Manifests > should load and use custom manifest files 1`] = ` "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/module-replacements/blob/main/docs/modules/request-alternatives.md.", + "message": "Module "@e18e/fake-3" can be replaced. We recommend switching to "node-fetch". See more at https://github.com/es-tooling/module-replacements/blob/main/docs/modules/request-alternatives.md.", "score": 0, "severity": "warning", }, @@ -35,7 +35,7 @@ exports[`Custom Manifests > should load and use custom manifest files 1`] = ` exports[`Custom Manifests > should load multiple manifest files 1`] = ` [ { - "message": "Module "@e18e/fake-0" can be replaced. Use picocolors, kleur, or native console styling.", + "message": "Module "@e18e/fake-0" can be replaced. See more at https://github.com/es-tooling/module-replacements/blob/main/docs/modules/fake-0.md.", "score": 0, "severity": "warning", }, @@ -50,7 +50,7 @@ exports[`Custom Manifests > should load multiple manifest files 1`] = ` "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/module-replacements/blob/main/docs/modules/request-alternatives.md.", + "message": "Module "@e18e/fake-3" can be replaced. We recommend switching to "node-fetch". See more at https://github.com/es-tooling/module-replacements/blob/main/docs/modules/request-alternatives.md.", "score": 0, "severity": "warning", }, @@ -76,7 +76,7 @@ exports[`Custom Manifests > should prioritize custom replacements over built-in { "withCustom": [ { - "message": "Module "@e18e/fake-0" can be replaced. Use picocolors, kleur, or native console styling.", + "message": "Module "@e18e/fake-0" can be replaced. See more at https://github.com/es-tooling/module-replacements/blob/main/docs/modules/fake-0.md.", "score": 0, "severity": "warning", }, @@ -91,7 +91,7 @@ exports[`Custom Manifests > should prioritize custom replacements over built-in "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/module-replacements/blob/main/docs/modules/request-alternatives.md.", + "message": "Module "@e18e/fake-3" can be replaced. We recommend switching to "node-fetch". See more at https://github.com/es-tooling/module-replacements/blob/main/docs/modules/request-alternatives.md.", "score": 0, "severity": "warning", }, diff --git a/test/fixtures/custom-manifest.json b/test/fixtures/custom-manifest.json index 741df4e..dcc138f 100644 --- a/test/fixtures/custom-manifest.json +++ b/test/fixtures/custom-manifest.json @@ -3,7 +3,8 @@ "@e18e/fake-0": { "type": "module", "moduleName": "@e18e/fake-0", - "replacements": ["fake-0-simple"] + "replacements": ["fake-0-simple"], + "url": { "type": "e18e", "id": "fake-0" } }, "@e18e/fake-1": { "type": "module", @@ -18,7 +19,8 @@ "@e18e/fake-3": { "type": "module", "moduleName": "@e18e/fake-3", - "replacements": ["fake-3-documented"] + "replacements": ["fake-3-documented"], + "url": { "type": "e18e", "id": "request-alternatives" } }, "@e18e/fake-4": { "type": "module", From c2a9330d64eb85943f0c838e91c0ac366dce096f Mon Sep 17 00:00:00 2001 From: paul valladares <85648028+dreyfus92@users.noreply.github.com> Date: Fri, 6 Mar 2026 09:26:16 -0600 Subject: [PATCH 06/10] Update src/analyze/replacements.ts Co-authored-by: James Garbutt <43081j@users.noreply.github.com> --- src/analyze/replacements.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/analyze/replacements.ts b/src/analyze/replacements.ts index d2cda4d..1dede84 100644 --- a/src/analyze/replacements.ts +++ b/src/analyze/replacements.ts @@ -188,7 +188,7 @@ export async function runReplacements( result.messages.push({ severity: 'warning', score: 0, - message: `Module "${name}" can be replaced. ${firstCompatible.description}.`, + message: `Module "${name}" can be replaced with inline native syntax. ${firstCompatible.description}.`, ...(fixableBy && {fixableBy}) }); } else if (firstCompatible.type === 'native') { From 0d239bb20cc5b30e9b7de2d4fab65128f227cad7 Mon Sep 17 00:00:00 2001 From: Paul Valladares <85648028+dreyfus92@users.noreply.github.com> Date: Sun, 8 Mar 2026 15:26:17 -0500 Subject: [PATCH 07/10] update: snapshots --- src/test/__snapshots__/cli.test.ts.snap | 64 +++++++++++-------- .../custom-manifests.test.ts.snap | 8 +-- 2 files changed, 40 insertions(+), 32 deletions(-) diff --git a/src/test/__snapshots__/cli.test.ts.snap b/src/test/__snapshots__/cli.test.ts.snap index bd6e3f4..d76b67b 100644 --- a/src/test/__snapshots__/cli.test.ts.snap +++ b/src/test/__snapshots__/cli.test.ts.snap @@ -3,43 +3,51 @@ exports[`CLI > should display package report 1`] = ` "e18e (cli ) -┌ Analyzing... -│ -● Summary -│ Package Name mock-package -│ Version 1.0.0 -│ Install Size 53.0 B -│ Dependencies 1 (1 production, 0 development) -│ Duplicate Dependency Count 0 -│ -● Results: -│ -│ -└ Done! +┌ Analyzing... +│ +● Summary +│ Package Name mock-package +│ Version 1.0.0 +│ Install Size 53.0 B +│ Dependencies 1 (1 production, 0 development) +│ Duplicate Dependency Count 0 +│ +● Results: +│ +│ +└ Done! " `; -exports[`CLI > should display package report 2`] = `""`; +exports[`CLI > should display package report 2`] = ` +"(node:) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set. +(Use \`node --trace-warnings ...\` to show where the warning was created) +" +`; exports[`CLI > should run successfully with default options 1`] = ` "e18e (cli ) -┌ Analyzing... -│ -● Summary -│ Package Name mock-package -│ Version 1.0.0 -│ Install Size 53.0 B -│ Dependencies 1 (1 production, 0 development) -│ Duplicate Dependency Count 0 -│ -● Results: -│ -│ -└ Done! +┌ Analyzing... +│ +● Summary +│ Package Name mock-package +│ Version 1.0.0 +│ Install Size 53.0 B +│ Dependencies 1 (1 production, 0 development) +│ Duplicate Dependency Count 0 +│ +● Results: +│ +│ +└ Done! " `; -exports[`CLI > should run successfully with default options 2`] = `""`; +exports[`CLI > should run successfully with default options 2`] = ` +"(node:) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set. +(Use \`node --trace-warnings ...\` to show where the warning was created) +" +`; diff --git a/src/test/__snapshots__/custom-manifests.test.ts.snap b/src/test/__snapshots__/custom-manifests.test.ts.snap index 5d2b442..936c7c4 100644 --- a/src/test/__snapshots__/custom-manifests.test.ts.snap +++ b/src/test/__snapshots__/custom-manifests.test.ts.snap @@ -10,7 +10,7 @@ exports[`Custom Manifests > should load and use custom manifest files 1`] = ` "severity": "warning", }, { - "message": "Module "@e18e/fake-1" can be replaced. Use native JavaScript methods or specific lodash functions.", + "message": "Module "@e18e/fake-1" can be replaced with inline native syntax. Use native JavaScript methods or specific lodash functions.", "score": 0, "severity": "warning", }, @@ -40,7 +40,7 @@ exports[`Custom Manifests > should load multiple manifest files 1`] = ` "severity": "warning", }, { - "message": "Module "@e18e/fake-1" can be replaced. Use native JavaScript methods or specific lodash functions.", + "message": "Module "@e18e/fake-1" can be replaced with inline native syntax. Use native JavaScript methods or specific lodash functions.", "score": 0, "severity": "warning", }, @@ -60,7 +60,7 @@ exports[`Custom Manifests > should load multiple manifest files 1`] = ` "severity": "warning", }, { - "message": "Module "@e18e/fake-5" can be replaced. Use Fastify, Koa, or native Node.js http module.", + "message": "Module "@e18e/fake-5" can be replaced with inline native syntax. Use Fastify, Koa, or native Node.js http module.", "score": 0, "severity": "warning", }, @@ -81,7 +81,7 @@ exports[`Custom Manifests > should prioritize custom replacements over built-in "severity": "warning", }, { - "message": "Module "@e18e/fake-1" can be replaced. Use native JavaScript methods or specific lodash functions.", + "message": "Module "@e18e/fake-1" can be replaced with inline native syntax. Use native JavaScript methods or specific lodash functions.", "score": 0, "severity": "warning", }, From 15508504663bd67ae197b4bba56dd31bebbda815 Mon Sep 17 00:00:00 2001 From: Paul Valladares <85648028+dreyfus92@users.noreply.github.com> Date: Sun, 8 Mar 2026 15:31:14 -0500 Subject: [PATCH 08/10] cleanup --- src/test/__snapshots__/cli.test.ts.snap | 64 +++++++++++-------------- 1 file changed, 28 insertions(+), 36 deletions(-) diff --git a/src/test/__snapshots__/cli.test.ts.snap b/src/test/__snapshots__/cli.test.ts.snap index d76b67b..bd6e3f4 100644 --- a/src/test/__snapshots__/cli.test.ts.snap +++ b/src/test/__snapshots__/cli.test.ts.snap @@ -3,51 +3,43 @@ exports[`CLI > should display package report 1`] = ` "e18e (cli ) -┌ Analyzing... -│ -● Summary -│ Package Name mock-package -│ Version 1.0.0 -│ Install Size 53.0 B -│ Dependencies 1 (1 production, 0 development) -│ Duplicate Dependency Count 0 -│ -● Results: -│ -│ -└ Done! +┌ Analyzing... +│ +● Summary +│ Package Name mock-package +│ Version 1.0.0 +│ Install Size 53.0 B +│ Dependencies 1 (1 production, 0 development) +│ Duplicate Dependency Count 0 +│ +● Results: +│ +│ +└ Done! " `; -exports[`CLI > should display package report 2`] = ` -"(node:) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set. -(Use \`node --trace-warnings ...\` to show where the warning was created) -" -`; +exports[`CLI > should display package report 2`] = `""`; exports[`CLI > should run successfully with default options 1`] = ` "e18e (cli ) -┌ Analyzing... -│ -● Summary -│ Package Name mock-package -│ Version 1.0.0 -│ Install Size 53.0 B -│ Dependencies 1 (1 production, 0 development) -│ Duplicate Dependency Count 0 -│ -● Results: -│ -│ -└ Done! +┌ Analyzing... +│ +● Summary +│ Package Name mock-package +│ Version 1.0.0 +│ Install Size 53.0 B +│ Dependencies 1 (1 production, 0 development) +│ Duplicate Dependency Count 0 +│ +● Results: +│ +│ +└ Done! " `; -exports[`CLI > should run successfully with default options 2`] = ` -"(node:) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set. -(Use \`node --trace-warnings ...\` to show where the warning was created) -" -`; +exports[`CLI > should run successfully with default options 2`] = `""`; From 9df94c2e40ab0a4ed63043bcf31dfb6bcdbd5128 Mon Sep 17 00:00:00 2001 From: Paul Valladares <85648028+dreyfus92@users.noreply.github.com> Date: Mon, 9 Mar 2026 18:02:35 -0500 Subject: [PATCH 09/10] refactor(replacements): use type-specific messaging and append mapping URL per type --- src/analyze/replacements.ts | 79 ++++++++----------- .../custom-manifests.test.ts.snap | 6 +- 2 files changed, 36 insertions(+), 49 deletions(-) diff --git a/src/analyze/replacements.ts b/src/analyze/replacements.ts index 1dede84..2af70f9 100644 --- a/src/analyze/replacements.ts +++ b/src/analyze/replacements.ts @@ -162,55 +162,42 @@ export async function runReplacements( const fixableBy = fixableByMigrate.has(name) ? 'migrate' : undefined; const mappingUrl = mapping.url ? resolveUrl(mapping.url) : undefined; - if (mappingUrl) { - let message = `Module "${name}" can be replaced.`; - if ( - firstCompatible.type === 'documented' && - firstCompatible.replacementModule - ) { - message += ` We recommend switching to "${firstCompatible.replacementModule}".`; + let message: string; + switch (firstCompatible.type) { + case 'removal': + message = `Module "${name}" can be removed, and native functionality used instead`; + break; + case 'simple': + message = `Module "${name}" can be replaced with inline native syntax. ${firstCompatible.description}.`; + break; + case 'native': { + const nodeVersion = getNodeMinVersion(firstCompatible.engines); + const requires = + nodeVersion && !enginesNode ? ` Required Node >= ${nodeVersion}.` : ''; + const urlStr = resolveUrl(firstCompatible.url); + message = `Module "${name}" can be replaced with native functionality.${requires} You can read more at ${urlStr}.`; + break; } + case 'documented': + if (firstCompatible.replacementModule) { + message = `Module "${name}" can be replaced. We recommend switching to "${firstCompatible.replacementModule}".`; + } else { + const urlStr = resolveUrl(firstCompatible.url); + message = `Module "${name}" can be replaced with a more performant alternative. See the list of available alternatives at ${urlStr}.`; + } + break; + default: + message = `Module "${name}" can be replaced.`; + } + if (mappingUrl) { message += ` See more at ${mappingUrl}.`; - result.messages.push({ - severity: 'warning', - score: 0, - message, - ...(fixableBy && {fixableBy}) - }); - } else if (firstCompatible.type === 'removal') { - result.messages.push({ - severity: 'warning', - score: 0, - message: `Module "${name}" can be removed, and native functionality used instead`, - ...(fixableBy && {fixableBy}) - }); - } else if (firstCompatible.type === 'simple') { - result.messages.push({ - severity: 'warning', - score: 0, - message: `Module "${name}" can be replaced with inline native syntax. ${firstCompatible.description}.`, - ...(fixableBy && {fixableBy}) - }); - } else if (firstCompatible.type === 'native') { - const nodeVersion = getNodeMinVersion(firstCompatible.engines); - const requires = - nodeVersion && !enginesNode ? ` Required Node >= ${nodeVersion}.` : ''; - const urlStr = resolveUrl(firstCompatible.url); - result.messages.push({ - severity: 'warning', - score: 0, - message: `Module "${name}" can be replaced with native functionality.${requires} You can read more at ${urlStr}.`, - ...(fixableBy && {fixableBy}) - }); - } else if (firstCompatible.type === 'documented') { - const urlStr = resolveUrl(firstCompatible.url); - 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 ${urlStr}.`, - ...(fixableBy && {fixableBy}) - }); } + result.messages.push({ + severity: 'warning', + score: 0, + message, + ...(fixableBy && {fixableBy}) + }); } return result; diff --git a/src/test/__snapshots__/custom-manifests.test.ts.snap b/src/test/__snapshots__/custom-manifests.test.ts.snap index 936c7c4..0200bda 100644 --- a/src/test/__snapshots__/custom-manifests.test.ts.snap +++ b/src/test/__snapshots__/custom-manifests.test.ts.snap @@ -5,7 +5,7 @@ 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. See more at https://github.com/es-tooling/module-replacements/blob/main/docs/modules/fake-0.md.", + "message": "Module "@e18e/fake-0" can be replaced with inline native syntax. Use picocolors, kleur, or native console styling. See more at https://github.com/es-tooling/module-replacements/blob/main/docs/modules/fake-0.md.", "score": 0, "severity": "warning", }, @@ -35,7 +35,7 @@ exports[`Custom Manifests > should load and use custom manifest files 1`] = ` exports[`Custom Manifests > should load multiple manifest files 1`] = ` [ { - "message": "Module "@e18e/fake-0" can be replaced. See more at https://github.com/es-tooling/module-replacements/blob/main/docs/modules/fake-0.md.", + "message": "Module "@e18e/fake-0" can be replaced with inline native syntax. Use picocolors, kleur, or native console styling. See more at https://github.com/es-tooling/module-replacements/blob/main/docs/modules/fake-0.md.", "score": 0, "severity": "warning", }, @@ -76,7 +76,7 @@ exports[`Custom Manifests > should prioritize custom replacements over built-in { "withCustom": [ { - "message": "Module "@e18e/fake-0" can be replaced. See more at https://github.com/es-tooling/module-replacements/blob/main/docs/modules/fake-0.md.", + "message": "Module "@e18e/fake-0" can be replaced with inline native syntax. Use picocolors, kleur, or native console styling. See more at https://github.com/es-tooling/module-replacements/blob/main/docs/modules/fake-0.md.", "score": 0, "severity": "warning", }, From 0a12319723d128067459ec29c0e716a1b0a4b475 Mon Sep 17 00:00:00 2001 From: Paul Valladares <85648028+dreyfus92@users.noreply.github.com> Date: Mon, 9 Mar 2026 18:08:38 -0500 Subject: [PATCH 10/10] format --- src/analyze/replacements.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/analyze/replacements.ts b/src/analyze/replacements.ts index 2af70f9..0e09bef 100644 --- a/src/analyze/replacements.ts +++ b/src/analyze/replacements.ts @@ -173,7 +173,9 @@ export async function runReplacements( case 'native': { const nodeVersion = getNodeMinVersion(firstCompatible.engines); const requires = - nodeVersion && !enginesNode ? ` Required Node >= ${nodeVersion}.` : ''; + nodeVersion && !enginesNode + ? ` Required Node >= ${nodeVersion}.` + : ''; const urlStr = resolveUrl(firstCompatible.url); message = `Module "${name}" can be replaced with native functionality.${requires} You can read more at ${urlStr}.`; break;