11import * as replacements from 'module-replacements' ;
2- import type { ManifestModule , ModuleReplacement } from 'module-replacements' ;
2+ import type {
3+ ManifestModule ,
4+ ModuleReplacement ,
5+ EngineConstraint ,
6+ KnownUrl
7+ } from 'module-replacements' ;
38import type { ReportPluginResult , AnalysisContext } from '../types.js' ;
49import { fixableReplacements } from '../commands/fixable-replacements.js' ;
510import { getPackageJson } from '../utils/package-json.js' ;
@@ -13,27 +18,74 @@ import {
1318import { LocalFileSystem } from '../local-file-system.js' ;
1419
1520/**
16- * Generates a standard URL to the docs of a given rule
17- * @param {string } name Rule name
18- * @return {string }
21+ * Resolves a v3 KnownUrl to a full URL string.
1922 */
20- export function getDocsUrl ( name : string ) : string {
21- return `https://github.com/es-tooling/module-replacements/blob/main/docs/modules/${ name } .md` ;
23+ export function resolveUrl ( url : KnownUrl ) : string {
24+ if ( typeof url === 'string' ) return url ;
25+ switch ( url . type ) {
26+ case 'mdn' :
27+ return `https://developer.mozilla.org/en-US/docs/${ url . id } ` ;
28+ case 'node' :
29+ return `https://nodejs.org/docs/latest/${ url . id } ` ;
30+ case 'e18e' :
31+ return `https://github.com/es-tooling/module-replacements/blob/main/docs/modules/${ url . id } .md` ;
32+ }
2233}
2334
24- /**
25- * Generates a URL for the given path on MDN
26- * @param {string } path Docs path
27- * @return {string }
28- */
29- export function getMdnUrl ( path : string ) : string {
30- return `https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/${ path } ` ;
35+ function getNodeMinVersion ( engines ?: EngineConstraint [ ] ) : string | undefined {
36+ return engines ?. find ( ( e ) => e . engine === 'nodejs' ) ?. minVersion ;
37+ }
38+
39+ function isNodeEngineCompatible (
40+ requiredNode : string ,
41+ enginesNode : string
42+ ) : boolean {
43+ const requiredRange = validRange ( requiredNode ) ;
44+ const engineRange = validRange ( enginesNode ) ;
45+
46+ if ( ! requiredRange || ! engineRange ) {
47+ return true ;
48+ }
49+
50+ const requiredMin = minVersion ( requiredRange ) ;
51+ if ( ! requiredMin ) {
52+ return true ;
53+ }
54+
55+ return (
56+ semverLessThan ( requiredMin . version , engineRange ) ||
57+ semverSatisfies ( requiredMin . version , engineRange )
58+ ) ;
59+ }
60+
61+ function findFirstCompatibleReplacement (
62+ replacementIds : string [ ] ,
63+ defs : Record < string , ModuleReplacement > ,
64+ enginesNode : string | undefined
65+ ) : ModuleReplacement | undefined {
66+ for ( const id of replacementIds ) {
67+ const replacement = defs [ id ] ;
68+ if ( ! replacement ) continue ;
69+
70+ if ( replacement . type === 'native' && enginesNode ) {
71+ const nodeVersion = getNodeMinVersion ( replacement . engines ) ;
72+ if ( nodeVersion && ! isNodeEngineCompatible ( nodeVersion , enginesNode ) ) {
73+ continue ;
74+ }
75+ }
76+
77+ return replacement ;
78+ }
79+ return undefined ;
3180}
3281
3382async function loadCustomManifests (
3483 manifestPaths : string [ ]
35- ) : Promise < ModuleReplacement [ ] > {
36- const customReplacements : ModuleReplacement [ ] = [ ] ;
84+ ) : Promise < ManifestModule > {
85+ const result : ManifestModule = {
86+ mappings : { } ,
87+ replacements : { }
88+ } ;
3789
3890 for ( const manifestPath of manifestPaths ) {
3991 try {
@@ -46,11 +98,11 @@ async function loadCustomManifests(
4698 ) ;
4799 const manifest : ManifestModule = JSON . parse ( manifestContent ) ;
48100
49- if (
50- manifest . moduleReplacements &&
51- Array . isArray ( manifest . moduleReplacements )
52- ) {
53- customReplacements . push ( ... manifest . moduleReplacements ) ;
101+ if ( manifest . mappings ) {
102+ Object . assign ( result . mappings , manifest . mappings ) ;
103+ }
104+ if ( manifest . replacements ) {
105+ Object . assign ( result . replacements , manifest . replacements ) ;
54106 }
55107 } catch ( error ) {
56108 console . warn (
@@ -59,29 +111,7 @@ async function loadCustomManifests(
59111 }
60112 }
61113
62- return customReplacements ;
63- }
64-
65- function isNodeEngineCompatible (
66- requiredNode : string ,
67- enginesNode : string
68- ) : boolean {
69- const requiredRange = validRange ( requiredNode ) ;
70- const engineRange = validRange ( enginesNode ) ;
71-
72- if ( ! requiredRange || ! engineRange ) {
73- return true ;
74- }
75-
76- const requiredMin = minVersion ( requiredRange ) ;
77- if ( ! requiredMin ) {
78- return true ;
79- }
80-
81- return (
82- semverLessThan ( requiredMin . version , engineRange ) ||
83- semverSatisfies ( requiredMin . version , engineRange )
84- ) ;
114+ return result ;
85115}
86116
87117export async function runReplacements (
@@ -94,89 +124,77 @@ export async function runReplacements(
94124 const packageJson = await getPackageJson ( context . fs ) ;
95125
96126 if ( ! packageJson || ! packageJson . dependencies ) {
97- // No dependencies
98127 return result ;
99128 }
100129
101- // Load custom manifests
102- const customReplacements = context . options ?. manifest
130+ const customManifest = context . options ?. manifest
103131 ? await loadCustomManifests ( context . options . manifest )
104- : [ ] ;
132+ : { mappings : { } , replacements : { } } ;
105133
106- // Combine custom and built-in replacements
107- const allReplacements = [
108- ...customReplacements ,
109- ...replacements . all . moduleReplacements
110- ] ;
134+ // Custom mappings take precedence over built-in
135+ const allMappings = {
136+ ...replacements . all . mappings ,
137+ ...customManifest . mappings
138+ } ;
139+ const allReplacementDefs : Record < string , ModuleReplacement > = {
140+ ...replacements . all . replacements ,
141+ ...customManifest . replacements
142+ } ;
111143
112144 const fixableByMigrate = new Set ( fixableReplacements . map ( ( r ) => r . from ) ) ;
145+ const enginesNode = packageJson . engines ?. node ;
113146
114147 for ( const name of Object . keys ( packageJson . dependencies ) ) {
115- // Find replacement (custom replacements take precedence due to order)
116- const replacement = allReplacements . find (
117- ( replacement ) => replacement . moduleName === name
118- ) ;
148+ const mapping = allMappings [ name ] ;
149+ if ( ! mapping ?. replacements ?. length ) {
150+ continue ;
151+ }
119152
120- if ( ! replacement ) {
153+ const firstCompatible = findFirstCompatibleReplacement (
154+ mapping . replacements ,
155+ allReplacementDefs ,
156+ enginesNode
157+ ) ;
158+ if ( ! firstCompatible ) {
121159 continue ;
122160 }
123161
124162 const fixableBy = fixableByMigrate . has ( name ) ? 'migrate' : undefined ;
125-
126- // Handle each replacement type using the same logic for both custom and built-in
127- if ( replacement . type === 'none' ) {
128- result . messages . push ( {
129- severity : 'warning' ,
130- score : 0 ,
131- message : `Module "${ name } " can be removed, and native functionality used instead` ,
132- ...( fixableBy && { fixableBy} )
133- } ) ;
134- } else if ( replacement . type === 'simple' ) {
135- result . messages . push ( {
136- severity : 'warning' ,
137- score : 0 ,
138- message : `Module "${ name } " can be replaced. ${ replacement . replacement } .` ,
139- ...( fixableBy && { fixableBy} )
140- } ) ;
141- } else if ( replacement . type === 'native' ) {
142- const enginesNode = packageJson . engines ?. node ;
143- let supported = true ;
144-
145- if ( replacement . nodeVersion && enginesNode ) {
146- supported = isNodeEngineCompatible (
147- replacement . nodeVersion ,
148- enginesNode
149- ) ;
150- }
151-
152- if ( ! supported ) {
153- continue ;
163+ const mappingUrl = mapping . url ? resolveUrl ( mapping . url ) : undefined ;
164+
165+ let message : string ;
166+ switch ( firstCompatible . type ) {
167+ case 'removal' :
168+ message = `Module "${ name } " can be removed, and native functionality used instead` ;
169+ break ;
170+ case 'simple' :
171+ message = `Module "${ name } " can be replaced with inline native syntax. ${ firstCompatible . description } .` ;
172+ break ;
173+ case 'native' : {
174+ const nodeVersion = getNodeMinVersion ( firstCompatible . engines ) ;
175+ const requires =
176+ nodeVersion && ! enginesNode
177+ ? ` Required Node >= ${ nodeVersion } .`
178+ : '' ;
179+ const urlStr = resolveUrl ( firstCompatible . url ) ;
180+ message = `Module "${ name } " can be replaced with native functionality.${ requires } You can read more at ${ urlStr } .` ;
181+ break ;
154182 }
155-
156- const mdnPath = getMdnUrl ( replacement . mdnPath ) ;
157- const requires =
158- replacement . nodeVersion && ! enginesNode
159- ? ` Required Node >= ${ replacement . nodeVersion } .`
160- : '' ;
161- const message = `Module "${ name } " can be replaced with native functionality. Use "${ replacement . replacement } " instead.${ requires } ` ;
162- const fullMessage = `${ message } You can read more at ${ mdnPath } .` ;
163- result . messages . push ( {
164- severity : 'warning' ,
165- score : 0 ,
166- message : fullMessage ,
167- ...( fixableBy && { fixableBy} )
168- } ) ;
169- } else if ( replacement . type === 'documented' ) {
170- const docUrl = getDocsUrl ( replacement . docPath ) ;
171- const message = `Module "${ name } " can be replaced with a more performant alternative.` ;
172- const fullMessage = `${ message } See the list of available alternatives at ${ docUrl } .` ;
173- result . messages . push ( {
174- severity : 'warning' ,
175- score : 0 ,
176- message : fullMessage ,
177- ...( fixableBy && { fixableBy} )
178- } ) ;
183+ case 'documented' :
184+ message = `Module "${ name } " can be replaced with a more performant alternative.` ;
185+ break ;
186+ default :
187+ message = `Module "${ name } " can be replaced with a more performant alternative.` ;
188+ }
189+ if ( mappingUrl ) {
190+ message += ` See more at ${ mappingUrl } .` ;
179191 }
192+ result . messages . push ( {
193+ severity : 'warning' ,
194+ score : 0 ,
195+ message,
196+ ...( fixableBy && { fixableBy} )
197+ } ) ;
180198 }
181199
182200 return result ;
0 commit comments