-
Notifications
You must be signed in to change notification settings - Fork 29.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
33704c4
commit 0afe83f
Showing
35 changed files
with
707 additions
and
54 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,177 @@ | ||
'use strict'; | ||
const { isURL, URL } = require('internal/url'); | ||
const { | ||
ObjectEntries, | ||
ObjectKeys, | ||
SafeMap, | ||
ArrayIsArray, | ||
StringPrototypeStartsWith, | ||
StringPrototypeEndsWith, | ||
StringPrototypeSlice, | ||
ArrayPrototypeReverse, | ||
ArrayPrototypeSort, | ||
} = primordials; | ||
const { codes: { ERR_INVALID_IMPORT_MAP } } = require('internal/errors'); | ||
const { shouldBeTreatedAsRelativeOrAbsolutePath } = require('internal/modules/helpers'); | ||
|
||
class ImportMap { | ||
#baseURL; | ||
#imports = new SafeMap(); | ||
#scopes = new SafeMap(); | ||
#specifiers = new SafeMap() | ||
|
||
constructor(raw, baseURL) { | ||
this.#baseURL = baseURL; | ||
this.process(raw, this.#baseURL); | ||
} | ||
|
||
// These are convinenince methods mostly for tests | ||
get baseURL() { | ||
return this.#baseURL; | ||
} | ||
|
||
get imports() { | ||
return this.#imports; | ||
} | ||
|
||
get scopes() { | ||
return this.#scopes; | ||
} | ||
|
||
#getMappedSpecifier(_mappedSpecifier) { | ||
let mappedSpecifier = this.#specifiers.get(_mappedSpecifier); | ||
|
||
// Specifiers are processed and cached in this.#specifiers | ||
if (!mappedSpecifier) { | ||
// Try processing as a url, fall back for bare specifiers | ||
try { | ||
if (shouldBeTreatedAsRelativeOrAbsolutePath(_mappedSpecifier)) { | ||
mappedSpecifier = new URL(_mappedSpecifier, this.#baseURL); | ||
} else { | ||
mappedSpecifier = new URL(_mappedSpecifier); | ||
} | ||
} catch { | ||
// Ignore exception | ||
mappedSpecifier = _mappedSpecifier; | ||
} | ||
this.#specifiers.set(_mappedSpecifier, mappedSpecifier); | ||
} | ||
return mappedSpecifier; | ||
} | ||
|
||
resolve(specifier, parentURL = this.#baseURL) { | ||
// When using the customized loader the parent | ||
// will be a string (for transferring to the worker) | ||
// so just handle that here | ||
if (!isURL(parentURL)) { | ||
parentURL = new URL(parentURL); | ||
} | ||
|
||
// Process scopes | ||
for (const { 0: prefix, 1: mapping } of this.#scopes) { | ||
const _mappedSpecifier = mapping.get(specifier); | ||
if (StringPrototypeStartsWith(parentURL.pathname, prefix.pathname) && _mappedSpecifier) { | ||
const mappedSpecifier = this.#getMappedSpecifier(_mappedSpecifier); | ||
if (mappedSpecifier !== _mappedSpecifier) { | ||
mapping.set(specifier, mappedSpecifier); | ||
} | ||
specifier = mappedSpecifier; | ||
break; | ||
} | ||
} | ||
|
||
// Handle bare specifiers with sub paths | ||
let spec = specifier; | ||
let hasSlash = (typeof specifier === 'string' && specifier.indexOf('/')) || -1; | ||
let subSpec; | ||
let bareSpec; | ||
if (isURL(spec)) { | ||
spec = spec.href; | ||
} else if (hasSlash !== -1) { | ||
hasSlash += 1; | ||
subSpec = StringPrototypeSlice(spec, hasSlash); | ||
bareSpec = StringPrototypeSlice(spec, 0, hasSlash); | ||
} | ||
|
||
let _mappedSpecifier = this.#imports.get(bareSpec) || this.#imports.get(spec); | ||
if (_mappedSpecifier) { | ||
// Re-assemble sub spec | ||
if (_mappedSpecifier === spec && subSpec) { | ||
_mappedSpecifier += subSpec; | ||
} | ||
const mappedSpecifier = this.#getMappedSpecifier(_mappedSpecifier); | ||
|
||
if (mappedSpecifier !== _mappedSpecifier) { | ||
this.imports.set(specifier, mappedSpecifier); | ||
} | ||
specifier = mappedSpecifier; | ||
} | ||
|
||
return specifier; | ||
} | ||
|
||
process(raw) { | ||
if (!raw) { | ||
throw new ERR_INVALID_IMPORT_MAP('top level must be a plain object'); | ||
} | ||
|
||
// Validation and normalization | ||
if (raw.imports === null || typeof raw.imports !== 'object' || ArrayIsArray(raw.imports)) { | ||
throw new ERR_INVALID_IMPORT_MAP('top level key "imports" is required and must be a plain object'); | ||
} | ||
if (raw.scopes === null || typeof raw.scopes !== 'object' || ArrayIsArray(raw.scopes)) { | ||
throw new ERR_INVALID_IMPORT_MAP('top level key "scopes" is required and must be a plain object'); | ||
} | ||
|
||
// Normalize imports | ||
const importsEntries = ObjectEntries(raw.imports); | ||
for (let i = 0; i < importsEntries.length; i++) { | ||
const { 0: specifier, 1: mapping } = importsEntries[i]; | ||
if (!specifier || typeof specifier !== 'string') { | ||
throw new ERR_INVALID_IMPORT_MAP('module specifier keys must be non-empty strings'); | ||
} | ||
if (!mapping || typeof mapping !== 'string') { | ||
throw new ERR_INVALID_IMPORT_MAP('module specifier values must be non-empty strings'); | ||
} | ||
if (StringPrototypeEndsWith(specifier, '/') && !StringPrototypeEndsWith(mapping, '/')) { | ||
throw new ERR_INVALID_IMPORT_MAP('module specifier keys ending with "/" must have values that end with "/"'); | ||
} | ||
|
||
this.imports.set(specifier, mapping); | ||
} | ||
|
||
// Normalize scopes | ||
// Sort the keys according to spec and add to the map in order | ||
// which preserves the sorted map requirement | ||
const sortedScopes = ArrayPrototypeReverse(ArrayPrototypeSort(ObjectKeys(raw.scopes))); | ||
for (let i = 0; i < sortedScopes.length; i++) { | ||
let scope = sortedScopes[i]; | ||
const _scopeMap = raw.scopes[scope]; | ||
if (!scope || typeof scope !== 'string') { | ||
throw new ERR_INVALID_IMPORT_MAP('import map scopes keys must be non-empty strings'); | ||
} | ||
if (!_scopeMap || typeof _scopeMap !== 'object') { | ||
throw new ERR_INVALID_IMPORT_MAP(`scope values must be plain objects (${scope} is ${typeof _scopeMap})`); | ||
} | ||
|
||
// Normalize scope | ||
scope = new URL(scope, this.#baseURL); | ||
|
||
const scopeMap = new SafeMap(); | ||
const scopeEntries = ObjectEntries(_scopeMap); | ||
for (let i = 0; i < scopeEntries.length; i++) { | ||
const { 0: specifier, 1: mapping } = scopeEntries[i]; | ||
if (StringPrototypeEndsWith(specifier, '/') && !StringPrototypeEndsWith(mapping, '/')) { | ||
throw new ERR_INVALID_IMPORT_MAP('module specifier keys ending with "/" must have values that end with "/"'); | ||
} | ||
scopeMap.set(specifier, mapping); | ||
} | ||
|
||
this.scopes.set(scope, scopeMap); | ||
} | ||
} | ||
} | ||
|
||
module.exports = { | ||
ImportMap, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.