-
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 b60b205
Showing
34 changed files
with
645 additions
and
53 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,170 @@ | ||
'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) { | ||
// 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; | ||
if (isURL(spec)) { | ||
spec = spec.href; | ||
} else if (hasSlash !== -1) { | ||
hasSlash += 1; | ||
subSpec = StringPrototypeSlice(spec, hasSlash); | ||
spec = StringPrototypeSlice(spec, 0, hasSlash); | ||
} | ||
|
||
let _mappedSpecifier = this.#imports.get(spec); | ||
if (_mappedSpecifier) { | ||
// Re-assemble sub spec | ||
if (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
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
Oops, something went wrong.