@@ -14,6 +14,8 @@ import resolveImportMap from "./resolve-import-map/resolveImportMap.mjs";
1414
1515/** @typedef {Map<any, any> | AsyncMap } AsyncMapLike */
1616
17+ /** @typedef {(specifier: string) => string } ResolveSpecifier */
18+
1719// The import map parser requries a base url. We don't require one for our purposes,
1820// but it allows us to use the parser without modifying the source. One quirk is that it will try map
1921// this url to files locally if it's specified, but no one should do that.
@@ -46,18 +48,67 @@ async function exists(filePath) {
4648 }
4749}
4850
51+ /**
52+ * Takes a specifier and resolves through an import map.
53+ * @param {string } specifier Import specifier.
54+ * @param {object } options Options.
55+ * @param {string } options.url The module URL to resolve.
56+ * @param {object } [options.parsedImportMap] A parsed import map.
57+ * @param {ResolveSpecifier } [options.resolveSpecifierOverride] Override specifier resolution.
58+ */
59+ function resolveSpecifier (
60+ specifier ,
61+ { url, parsedImportMap, resolveSpecifierOverride = ( x ) => x } ,
62+ ) {
63+ // If an import map is supplied, everything resolves through it.
64+ if ( parsedImportMap ) {
65+ const importMapResolved = resolveImportMap (
66+ specifier ,
67+ parsedImportMap ,
68+ new URL ( url , `https://${ DUMMY_HOSTNAME } ` ) ,
69+ ) ;
70+
71+ if ( importMapResolved . hostname === DUMMY_HOSTNAME ) {
72+ // It will match if it's a local module.
73+ return {
74+ importMap : true ,
75+ importMapResolved,
76+ specifier : resolveSpecifierOverride ( importMapResolved . pathname ) ,
77+ } ;
78+ }
79+ }
80+
81+ return {
82+ importMap : false ,
83+ specifier : resolveSpecifierOverride ( specifier ) ,
84+ } ;
85+ }
86+
4987/**
5088 * Recursively parses and resolves a module's imports.
5189 * @param {string } module The path to the module.
5290 * @param {object } options Options.
5391 * @param {string } options.url The module URL to resolve.
5492 * @param {object } [options.parsedImportMap] A parsed import map.
93+ * @param {ResolveSpecifier } [options.resolveSpecifierOverride] Override specifier resolution.
94+ * @param {string } options.rootPath The absolute path to the specified application root.
5595 * @param {boolean } [root] Whether the module is the root module.
56- * @returns An array containing paths to modules that can be preloaded.
96+ * @returns { Promise<Set<string>> } A `Set` containing paths to modules that can be preloaded, or otherwise `undefined` .
5797 */
58- async function resolveImports ( module , { url, parsedImportMap } , root = true ) {
59- /** @type {Array<string> } */
60- let modules = [ ] ;
98+ async function resolveImports (
99+ module ,
100+ { url, parsedImportMap, resolveSpecifierOverride, rootPath } ,
101+ root = true ,
102+ visited = new Set ( ) ,
103+ ) {
104+ /** @type {Set<string> } */
105+ const modules = new Set ( ) ;
106+
107+ if ( visited . has ( module ) ) {
108+ return modules ;
109+ }
110+
111+ visited . add ( module ) ;
61112
62113 const source = await tryReadFile ( module ) ;
63114
@@ -71,46 +122,40 @@ async function resolveImports(module, { url, parsedImportMap }, root = true) {
71122 imports . map ( async ( { n : specifier , d } ) => {
72123 const dynamic = d > - 1 ;
73124 if ( specifier && ! dynamic ) {
74- let importMapResolved = null ;
75-
76- // If an import map is supplied, everything resolves through it.
77- if ( parsedImportMap ) {
78- importMapResolved = resolveImportMap (
79- specifier ,
80- parsedImportMap ,
81- new URL ( url , `https://${ DUMMY_HOSTNAME } ` ) ,
82- ) ;
83- }
84-
85- let resolvedModule ;
86-
87- // Are we resolving with an import map?
88- if ( importMapResolved !== null ) {
89- // It will match if it's a local module.
90- if ( importMapResolved . hostname === DUMMY_HOSTNAME ) {
91- resolvedModule = path . resolve (
92- path . dirname ( module ) ,
93- `.${ importMapResolved . pathname } ` ,
94- ) ;
95- }
96- } else {
97- resolvedModule = path . resolve ( path . dirname ( module ) , specifier ) ;
98- }
125+ const resolvedSpecifier = resolveSpecifier ( specifier , {
126+ url,
127+ parsedImportMap,
128+ resolveSpecifierOverride,
129+ } ) ;
130+ const resolvedModule = path . join (
131+ resolvedSpecifier . importMap ? rootPath : path . dirname ( module ) ,
132+ resolvedSpecifier . specifier ,
133+ ) ;
99134
100135 // If the module has resolved to a local file (and it exists), then it's preloadable.
101- if ( resolvedModule && ( await exists ( resolvedModule ) ) ) {
136+ if (
137+ resolvedModule &&
138+ resolvedModule . startsWith ( rootPath ) &&
139+ ( await exists ( resolvedModule ) )
140+ ) {
102141 if ( ! root ) {
103- modules . push ( resolvedModule ) ;
142+ modules . add ( resolvedModule ) ;
104143 }
105144
106145 const graph = await resolveImports (
107146 resolvedModule ,
108- { parsedImportMap, url } ,
147+ {
148+ parsedImportMap,
149+ url : resolvedSpecifier . importMapResolved ?. pathname || url ,
150+ resolveSpecifierOverride,
151+ rootPath,
152+ } ,
109153 false ,
154+ visited ,
110155 ) ;
111156
112- if ( graph . length > 0 ) {
113- graph . forEach ( ( module ) => modules . push ( module ) ) ;
157+ if ( graph . size > 0 ) {
158+ graph . forEach ( ( module ) => modules . add ( module ) ) ;
114159 }
115160 }
116161 }
@@ -127,17 +172,27 @@ async function resolveImports(module, { url, parsedImportMap }, root = true) {
127172 * @param {AsyncMapLike } options.cache Resolved imports cache.
128173 * @param {string } options.url The module URL to resolve.
129174 * @param {object } [options.parsedImportMap] A parsed import map.
130- * @returns An array containing paths to modules that can be preloaded, or otherwise `undefined`.
175+ * @param {ResolveSpecifier } [options.resolveSpecifierOverride] Override specifier resolution.
176+ * @param {string } options.rootPath The absolute path to the specified application root.
177+ * @returns {Promise<Set<string> | undefined> } A `Set` containing paths to modules that can be preloaded, or otherwise `undefined`.
131178 */
132- async function resolveImportsCached ( module , { cache, url, parsedImportMap } ) {
179+ async function resolveImportsCached (
180+ module ,
181+ { cache, url, parsedImportMap, resolveSpecifierOverride, rootPath } ,
182+ ) {
133183 const paths = await cache . get ( module ) ;
134184
135185 if ( paths ) {
136186 return paths ;
137187 } else {
138- const graph = await resolveImports ( module , { parsedImportMap, url } ) ;
188+ const graph = await resolveImports ( module , {
189+ parsedImportMap,
190+ url,
191+ resolveSpecifierOverride,
192+ rootPath,
193+ } ) ;
139194
140- if ( graph . length > 0 ) {
195+ if ( graph . size > 0 ) {
141196 await cache . set ( module , graph ) ;
142197 return graph ;
143198 }
@@ -156,33 +211,46 @@ export default function createResolveLinkRelations(
156211 appPath ,
157212 { importMap : importMapString , cache = new Map ( ) } = { } ,
158213) {
214+ /** @type {object } */
215+ let parsedImportMap ;
216+
217+ if ( importMapString !== undefined ) {
218+ parsedImportMap = parseFromString (
219+ importMapString ,
220+ `https://${ DUMMY_HOSTNAME } ` ,
221+ ) ;
222+ }
223+
159224 /**
160225 * Resolves link relations for a given URL.
161226 * @param {string } url The module URL to resolve.
162- * @returns An array containing relative paths to modules that can be preloaded, or otherwise `undefined`.
227+ * @param {object } [options] Options.
228+ * @param {ResolveSpecifier } [options.resolveSpecifier] Override specifier resolution.
229+ * @returns {Promise<Array<string> | undefined> } An array containing relative paths to modules that can be preloaded, or otherwise `undefined`.
163230 */
164- return async function resolveLinkRelations ( url ) {
165- let parsedImportMap ;
166-
167- if ( importMapString !== undefined ) {
168- parsedImportMap = parseFromString (
169- importMapString ,
170- `https://${ DUMMY_HOSTNAME } ` ,
171- ) ;
172- }
173-
231+ return async function resolveLinkRelations (
232+ url ,
233+ { resolveSpecifier : resolveSpecifierOverride } = { } ,
234+ ) {
174235 const rootPath = path . resolve ( appPath ) ;
175- const resolvedFile = path . join ( rootPath , url ) ;
236+ const resolvedSpecifier = resolveSpecifier ( url , {
237+ url,
238+ parsedImportMap,
239+ resolveSpecifierOverride,
240+ } ) ;
241+ const resolvedModule = path . join ( rootPath , resolvedSpecifier . specifier ) ;
176242
177- if ( resolvedFile . startsWith ( rootPath ) ) {
178- const modules = await resolveImportsCached ( resolvedFile , {
243+ if ( resolvedModule . startsWith ( rootPath ) ) {
244+ const modules = await resolveImportsCached ( resolvedModule , {
179245 cache,
180246 url,
181247 parsedImportMap,
248+ resolveSpecifierOverride,
249+ rootPath,
182250 } ) ;
183251
184- if ( Array . isArray ( modules ) && modules . length > 0 ) {
185- const resolvedModules = modules . map ( ( module ) => {
252+ if ( modules && modules . size > 0 ) {
253+ const resolvedModules = Array . from ( modules ) . map ( ( module ) => {
186254 return "/" + path . relative ( rootPath , module ) ;
187255 } ) ;
188256
0 commit comments