diff --git a/package-lock.json b/package-lock.json index 6b4469f..69a5519 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.1", "@types/picomatch": "^4.0.2", + "@types/semver": "^7.7.0", "@vitest/coverage-v8": "^3.2.3", "eslint": "^9.33.0", "pkg-pr-new": "^0.0.54", @@ -2298,6 +2300,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 98f1d5e..1b8a6db 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.1", "@types/picomatch": "^4.0.2", + "@types/semver": "^7.7.0", "@vitest/coverage-v8": "^3.2.3", "eslint": "^9.33.0", "pkg-pr-new": "^0.0.54", diff --git a/src/analyze/replacements.ts b/src/analyze/replacements.ts index bf4875c..7d9b40f 100644 --- a/src/analyze/replacements.ts +++ b/src/analyze/replacements.ts @@ -2,6 +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 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 @@ -21,6 +24,28 @@ export function getMdnUrl(path: string): string { return `https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/${path}`; } +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) + ); +} + export async function runReplacements( fileSystem: FileSystem ): Promise { @@ -57,13 +82,29 @@ export async function runReplacements( 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 + ); + } + + 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 {