bare-module-resolve
Low-level module resolution algorithm for Bare. The algorithm is implemented as a generator function that yields either package manifests to be read or resolution candidates to be tested by the caller. As a convenience, the main export is a synchronous and asynchronous iterable that relies on package manifests being read by a callback. For asynchronous iteration, the callback may return promises which will be awaited before being passed to the generator.
npm i bare-module-resolve
Usage
For synchronous resolution:
const resolve = require('bare-module-resolve')
function readPackage(url) {
}
for (const resolution of resolve('./file.js', new URL('file:///directory/'), readPackage)) {
console.log(resolution)
}
For asynchronous resolution:
const resolve = require('bare-module-resolve')
async function readPackage(url) {
}
for await (const resolution of resolve('./file.js', new URL('file:///directory/'), readPackage)) {
console.log(resolution)
}
API
const resolver = resolve(specifier, parentURL[, options][, readPackage])
Resolve specifier relative to parentURL, which must be a WHATWG URL instance. readPackage is called with a URL instance for every package manifest to be read and must either return the parsed JSON package manifest, if it exists, or null. If readPackage returns a promise, synchronous iteration is not supported.
Options include:
options = {
imports,
builtins: [],
builtinProtocol: 'builtin:',
defer: [],
deferredProtocol: 'deferred:',
conditions: [],
matchedConditions: [],
engines: {},
extensions: [],
resolutions
}
for (const resolution of resolver)
Synchronously iterate the module resolution candidates. The resolved module is the first candidate that exists, either as a file on a file system, a resource at a URL, or something else entirely.
for await (const resolution of resolver)
Asynchronously iterate the module resolution candidates. If readPackage returns promises, these will be awaited. The same comments as for (const resolution of resolver) apply.
Algorithm
The following generator functions implement the resolution algorithm, which has been adapted from the Node.js resolution algorithms for CommonJS and ES modules. Unlike Node.js, Bare uses the same resolution algorithm for both module formats. The yielded values have the following shape:
Package manifest
next.value = {
package: URL
}
If the package manifest identified by next.value.package exists, generator.next() must be passed the parsed JSON value of the manifest. If it does not exist, pass null instead.
Resolution candidate
next.value = {
resolution: URL
}
If the module identified by next.value.resolution exists, generator.next() may be passed true to signal that the resolution for the current set of conditions has been identified. If it does not exist, pass false instead.
To drive the generator functions, a loop like the following can be used:
const generator = resolve.module(specifier, parentURL)
let next = generator.next()
while (next.done !== true) {
const value = next.value
if (value.package) {
let info
next = generator.next(info)
} else {
const resolution = value.resolution
let resolved
next = generator.next(resolved)
}
}
Options are the same as resolve() for all functions.
[!WARNING]
These functions are currently subject to change between minor releases. If using them directly, make sure to specify a tilde range (~1.2.3) when declaring the module dependency.
const generator = resolve.module(specifier, parentURL[, options])
- If
specifier starts with a Windows drive letter:
- If
options.resolutions is set:
- If
preresolved(specifier, options.resolutions, parentURL, options) yields, return.
- If
url(specifier, parentURL, options) yields, return.
- If
packageImports(specifier, parentURL, options) yields, return.
- If
specifier equals . or .., or if specifier starts with /, \, ./, .\, ../, or ..\:
- If
options.imports is set:
- If
packageImportsExports(specifier, options.imports, parentURL, true, options) yields, return.
- If
deferred(specifier, options) yields, return.
- If
file(specifier, parentURL, false, options) resolves, return.
- Return
directory(specifier, parentURL, options).
- Return
package(specifier, parentURL, options).
const generator = resolve.url(url, parentURL[, options])
- If
url is not a valid URL, return.
- If
options.imports is set:
- If
packageImportsExports(url.href, options.imports, parentURL, true, options) yields, return.
- If
url.protocol equals options.deferredProtocol:
- Let
specifier be url.pathname.
- If
options.resolutions is set:
- Let
imports be options.resolutions[parentURL].
- If
imports is a non-null object:
- Set
imports[specifier] to null.
- Return
module(specifier, parentURL, options).
- If
url.protocol equals node::
- Let
specifier be url.pathname.
- If
specifier equals . or .., or if specifier starts with /, \, ./, .\, ../, or ..\, throw.
- Return
package(specifier, parentURL, options).
- Yield
url.
const generator = resolve.preresolved(specifier, resolutions, parentURL[, options])
- Let
imports be resolutions[parentURL].
- If
imports is a non-null object:
- Return
packageImportsExports(specifier, imports, parentURL, true, options).
const generator = resolve.deferred(specifier[, options])
- If
options.defer includes specifier:
- Yield
options.deferredProtocol concatenated with specifier and return.
const generator = resolve.package(packageSpecifier, parentURL[, options])
- If
packageSpecifier is the empty string, throw.
- If
packageSpecifier does not start with @:
- Set
packageName to the substring of packageSpecifier until the first / or the end of the string.
- Let
packageName be undefined.
- Otherwise:
- If
packageSpecifier does not include /, throw.
- Set
packageName to the substring of packageSpecifier until the second / or the end of the string.
- If
packageName starts with . or includes \ or %, throw.
- If
builtinTarget(packageSpecifier, null, options.builtins, options) yields, return.
- If
deferred(packageSpecifier, options) yields, return.
- Let
packageSubpath be . concatenated with the substring of packageSpecifier from the position at the length of packageName.
- If
packageSelf(packageName, packageSubpath, parentURL, options) yields, return.
- For each value
packageURL of lookupPackageRoot(packageName, parentURL, options):
- Let
info be the result of yielding packageURL.
- If
info is not null:
- If
info.engines is set:
- Call
validateEngines(packageURL, info.engines, options).
- If
info.exports is set:
- Return
packageExports(packageURL, packageSubpath, info.exports, options).
- If
packageSubpath is .:
- If
info.main is a non-empty string:
- Set
packageSubpath to info.main.
- Otherwise:
- Return
file('index', packageURL, true, options).
- If
file(packageSubpath, packageURL, false, options) resolves, return.
- Return
directory(packageSubpath, packageURL, options).
const generator = resolve.packageSelf(packageName, packageSubpath, parentURL[, options])
- For each value
packageURL of lookupPackageScope(parentURL, options):
- Let
info be the result of yielding packageURL.
- If
info is not null:
- If
info.name does not equal packageName, return.
- If
info.exports is set:
- Return
packageExports(packageURL, packageSubpath, info.exports, options).
- If
packageSubpath is .:
- If
info.main is a non-empty string:
- Set
packageSubpath to info.main.
- Otherwise:
- Return
file('index', packageURL, true, options).
- If
file(packageSubpath, packageURL, false, options) resolves, return.
- Return
directory(packageSubpath, packageURL, options).
const generator = resolve.packageExports(packageURL, subpath, exports[, options])
- If
subpath is .:
- Let
mainExport be undefined.
- If
exports is a string or an array:
- Set
mainExport to exports.
- If
exports is a non-null object:
- If some keys of
exports start with .:
- If
. is a key of exports:
- Set
mainExport to exports['.'].
- Otherwise:
- Set
mainExport to exports.
- If
mainExport is not undefined:
- If
packageTarget(packageURL, mainExport, null, false, options) yields, return.
- Otherwise, if
exports is a non-null object:
- If every key of
exports starts with .:
- If
packageImportsExports(subpath, exports, packageURL, false, options) yields, return.
- Throw.
const generator = resolve.packageImports(specifier, parentURL[, options])
- If
specifier is # or starts with #/, throw.
- For each value
packageURL of lookupPackageScope(parentURL, options):
- Let
info be the result of yielding packageURL.
- If
info is not null:
- If
info.imports is set:
- If
packageImportsExports(specifier, info.imports, packageURL, true, options) yields, return.
- If specifier starts with
#, throw.
- Return.
- If
options.imports is set:
- If
packageImportsExports(url.href, options.imports, parentURL, true, options) yields, return.
const generator = resolve.packageImportsExports(matchKey, matchObject, packageURL, isImports[, options])
- If
matchKey is a key of matchObject and matchKey does not include *:
- Let
target be matchObject[matchKey].
- Return
packageTarget(packageURL, target, null, isImports, options).
- Let
expansionKeys be the keys of matchObject that include * sorted by patternKeyCompare.
- For each value
expansionKey of expansionKeys:
- Let
patternBase be the substring of expansionKey until the first *.
- If
matchKey starts with but isn't equal to patternBase:
- Let
patternTrailer be the substring of expansionKey from the position at the index after the first *.
- If
patternTrailer is the empty string, or if matchKey ends with patternTrailer and the length of matchKey is greater than or equal to the length of expansionKey:
- Let
target be matchObject[expansionKey].
- Let
patternMatch be the substring of matchKey from the position at the length of patternBase until the length of matchKey minus the length of patternTrailer.
- Return
packageTarget(packageURL, target, patternMatch, isImports, options).
const generator = resolve.packageTarget(packageURL, target, patternMatch, isImports[, options])
- If
target is a string:
- If
target does not start with ./ and isImports is false, throw.
- If
patternMatch is not null:
- Replace every instance of
* in target with patternMatch.
- If
url(target, packageURL, options) yields, return.
- If
target equals . or .., or if target starts with /, ./, or ../:
- Yield the resolution of
target relative to packageURL and return.
- Return
package(target, packageURL, options).
- If
target is an array:
- For each value
targetValue of target:
- If
packageTarget(packageURL, targetValue, patternMatch, isImports, options) yields, return.
- If
target is a non-null object:
- For each key
condition of target:
- If
condition equals default or if options.conditions includes condition:
- Let
targetValue be target[condition].
- Return
packageTarget(packageURL, targetValue, patternMatch, isImports, options).
const generator = resolve.builtinTarget(packageSpecifier, packageVersion, target[, options])
- If
target is a string:
- If
target does not start with @:
- Let
targetName be the substring of target until the first @ or the end of the string.
- Let
targetVersion be the substring of target from the character following the first @ and to the end of string, or null if no such substring exists.
- Otherwise:
- Let
targetName be the substring of target until the second @ or the end of the string.
- Let
targetVersion be the substring of target from the character following the second @ and to the end of string, or null if no such substring exists.
- If
packageSpecifier equals targetName:
- If
packageVersion is null and targetVersion is null:
- Yield
options.builtinProtocol concatenated with packageSpecifier and return.
- Let
version be null.
- If
packageVersion is null, let version be targetVersion.
- Otherwise, if
targetVersion is either null or equals packageVersion, let version be packageVersion
- If
version is not null:
- Yield
options.builtinProtocol concatenated with packageSpecifier, @, and version and return.
- If
target is an array:
- For each value
targetValue of target:
- If
builtinTarget(packageSpecifier, packageVersion, targetValue, options) yields, return.
- If
target is a non-null object:
- For each key
condition of target:
- If
condition equals default or if options.conditions includes condition:
- Let
targetValue be target[condition].
- Return
builtinTarget(packageSpecifier, packageVersion, targetValue, options).
const generator = resolve.file(filename, parentURL, isIndex[, options])
- If
filename equals . or .., or if filename ends with / or \, return.
- If
parentURL is a file: URL and filename includes encoded / or \, throw.
- If
isIndex is false:
- Yield the resolution of
filename relative to parentURL.
- For each value
ext of options.extensions:
- If
filename ends with ext, continue.
- Yield the resolution of
filename concatenated with ext relative to parentURL.
const generator = resolve.directory(dirname, parentURL[, options])
- Let
directoryURL be undefined.
- If
dirname ends with / or \:
- Set
directoryURL to the resolution of dirname relative to parentURL.
- Otherwise:
- Set
directoryURL to the resolution of dirname concatenated with / relative to parentURL.
- Let
info be the result of yielding the resolution of package.json relative to directoryURL.
- If
info is not null:
- If
info.exports is set:
- Return
packageExports(directoryURL, '.', info.exports, options).
- If
info.main is a non-empty string:
- If
file(info.main, directoryURL, false, options) resolves, return.
- Return
directory(info.main, directoryURL, options).
- Return
file('index', directoryURL, true, options).
License
Apache-2.0