From 55641721b760bbdd36a1f1d86845ebf176211649 Mon Sep 17 00:00:00 2001 From: outslept <135520429+outslept@users.noreply.github.com> Date: Fri, 8 Aug 2025 23:57:59 +0300 Subject: [PATCH 1/4] feat: add support for nodeVersion --- package-lock.json | 9 +++++++++ package.json | 2 ++ src/analyze/replacements.ts | 34 +++++++++++++++++++++++++++++++--- src/types.ts | 4 ++++ 4 files changed, 46 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index e2ffae1..a6951df 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "package-manager-detector": "^1.3.0", "picocolors": "^1.1.1", "publint": "^0.3.12", + "semver": "^7.7.2", "tinyglobby": "^0.2.14" }, "bin": { @@ -30,6 +31,7 @@ "@types/debug": "^4.1.12", "@types/node": "^24.2.0", "@types/picomatch": "^4.0.2", + "@types/semver": "^7.7.0", "@vitest/coverage-v8": "^3.2.3", "eslint": "^9.32.0", "pkg-pr-new": "^0.0.54", @@ -2297,6 +2299,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/semver": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.0.tgz", + "integrity": "sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==", + "dev": true, + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.39.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.39.0.tgz", diff --git a/package.json b/package.json index 878671b..249e8b3 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "package-manager-detector": "^1.3.0", "picocolors": "^1.1.1", "publint": "^0.3.12", + "semver": "^7.7.2", "tinyglobby": "^0.2.14" }, "devDependencies": { @@ -72,6 +73,7 @@ "@types/debug": "^4.1.12", "@types/node": "^24.2.0", "@types/picomatch": "^4.0.2", + "@types/semver": "^7.7.0", "@vitest/coverage-v8": "^3.2.3", "eslint": "^9.32.0", "pkg-pr-new": "^0.0.54", diff --git a/src/analyze/replacements.ts b/src/analyze/replacements.ts index bf4875c..a972993 100644 --- a/src/analyze/replacements.ts +++ b/src/analyze/replacements.ts @@ -2,6 +2,7 @@ import * as replacements from 'module-replacements'; import {ReportPluginResult} from '../types.js'; import type {FileSystem} from '../file-system.js'; import {getPackageJson} from '../file-system-utils.js'; +import {gte, minVersion, validRange} from 'semver'; /** * Generates a standard URL to the docs of a given rule @@ -21,6 +22,23 @@ export function getMdnUrl(path: string): string { return `https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/${path}`; } +function isNodeEngineCompatible( + requiredNode: string | undefined, + enginesNode: string | undefined +): boolean { + const requiredRange = validRange(requiredNode) ?? `>=${requiredNode}`; + const requiredMin = minVersion(requiredRange); + if (!requiredMin) return true; + + const engineRange = validRange(enginesNode); + if (!engineRange) return true; + + const engineMin = minVersion(engineRange); + if (!engineMin) return true; + + return gte(engineMin, requiredMin); +} + export async function runReplacements( fileSystem: FileSystem ): Promise { @@ -57,13 +75,23 @@ export async function runReplacements( message: `Module "${name}" can be replaced. ${replacement.replacement}.` }); } else if (replacement.type === 'native') { + const enginesNode = packageJson.engines?.node; + const supported = isNodeEngineCompatible( + replacement.nodeVersion, + enginesNode + ); + if (!supported) { + continue; + } const mdnPath = getMdnUrl(replacement.mdnPath); - // TODO (43081j): support `nodeVersion` here, check it against the - // packageJson.engines field, if there is one. + const requires = + replacement.nodeVersion && !enginesNode + ? `Required Node >= ${replacement.nodeVersion}` + : ''; 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: `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 = getDocsUrl(replacement.docPath); diff --git a/src/types.ts b/src/types.ts index a985b10..8244e26 100644 --- a/src/types.ts +++ b/src/types.ts @@ -53,6 +53,10 @@ export interface PackageJsonLike { version: string; dependencies?: Record; devDependencies?: Record; + engines?: { + node?: string; + [engineName: string]: string | undefined; + }; } export interface Replacement { From aceb5cdb3523e4b13c714d7b8541faa4cb90021e Mon Sep 17 00:00:00 2001 From: outslept <135520429+outslept@users.noreply.github.com> Date: Fri, 8 Aug 2025 23:58:41 +0300 Subject: [PATCH 2/4] chore: early return --- src/analyze/replacements.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/analyze/replacements.ts b/src/analyze/replacements.ts index a972993..4009a72 100644 --- a/src/analyze/replacements.ts +++ b/src/analyze/replacements.ts @@ -26,6 +26,8 @@ function isNodeEngineCompatible( requiredNode: string | undefined, enginesNode: string | undefined ): boolean { + if (!requiredNode || !enginesNode) return true; + const requiredRange = validRange(requiredNode) ?? `>=${requiredNode}`; const requiredMin = minVersion(requiredRange); if (!requiredMin) return true; From bb4b9bfcc6339fdafc6550e7e26e77dc8d4abfba Mon Sep 17 00:00:00 2001 From: outslept <135520429+outslept@users.noreply.github.com> Date: Fri, 8 Aug 2025 23:59:25 +0300 Subject: [PATCH 3/4] chore: extra formatting --- 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 4009a72..53dba4e 100644 --- a/src/analyze/replacements.ts +++ b/src/analyze/replacements.ts @@ -88,7 +88,7 @@ export async function runReplacements( const mdnPath = getMdnUrl(replacement.mdnPath); const requires = replacement.nodeVersion && !enginesNode - ? `Required Node >= ${replacement.nodeVersion}` + ? ` Required Node >= ${replacement.nodeVersion}.` : ''; result.messages.push({ severity: 'warning', From ad0c0f6516236250320892458cef109d2919053f Mon Sep 17 00:00:00 2001 From: outslept <135520429+outslept@users.noreply.github.com> Date: Tue, 12 Aug 2025 00:52:38 +0300 Subject: [PATCH 4/4] fix: align implementation with eslint-plugin-depend addresses review suggestions by @43081j --- src/analyze/replacements.ts | 45 +++++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/src/analyze/replacements.ts b/src/analyze/replacements.ts index 53dba4e..7d9b40f 100644 --- a/src/analyze/replacements.ts +++ b/src/analyze/replacements.ts @@ -2,7 +2,9 @@ import * as replacements from 'module-replacements'; import {ReportPluginResult} from '../types.js'; import type {FileSystem} from '../file-system.js'; import {getPackageJson} from '../file-system-utils.js'; -import {gte, minVersion, validRange} from 'semver'; +import semverSatisfies from 'semver/functions/satisfies.js'; +import semverLessThan from 'semver/ranges/ltr.js'; +import {minVersion, validRange} from 'semver'; /** * Generates a standard URL to the docs of a given rule @@ -23,22 +25,25 @@ export function getMdnUrl(path: string): string { } function isNodeEngineCompatible( - requiredNode: string | undefined, - enginesNode: string | undefined + requiredNode: string, + enginesNode: string ): boolean { - if (!requiredNode || !enginesNode) return true; - - const requiredRange = validRange(requiredNode) ?? `>=${requiredNode}`; - const requiredMin = minVersion(requiredRange); - if (!requiredMin) return true; - + const requiredRange = validRange(requiredNode); const engineRange = validRange(enginesNode); - if (!engineRange) return true; - const engineMin = minVersion(engineRange); - if (!engineMin) return true; + if (!requiredRange || !engineRange) { + return true; + } - return gte(engineMin, requiredMin); + const requiredMin = minVersion(requiredRange); + if (!requiredMin) { + return true; + } + + return ( + semverLessThan(requiredMin.version, engineRange) || + semverSatisfies(requiredMin.version, engineRange) + ); } export async function runReplacements( @@ -78,13 +83,19 @@ export async function runReplacements( }); } else if (replacement.type === 'native') { const enginesNode = packageJson.engines?.node; - const supported = isNodeEngineCompatible( - replacement.nodeVersion, - enginesNode - ); + let supported = true; + + if (replacement.nodeVersion && enginesNode) { + supported = isNodeEngineCompatible( + replacement.nodeVersion, + enginesNode + ); + } + if (!supported) { continue; } + const mdnPath = getMdnUrl(replacement.mdnPath); const requires = replacement.nodeVersion && !enginesNode