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:',
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.
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) {
const info = ;
next = generator.next(info)
} else {
const resolution = value.resolution
next = generator.next()
}
}
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:
- Prepend
/
to specifier
.
- If
options.resolutions
is set:
- If
preresolved(specifier, options.resolutions, parentURL, options)
returns true
:
- Return
true
.
- If
url(specifier, parentURL, options)
returns true
:
- Return
true
.
- If
packageImports(specifier, parentURL, options)
returns true
:
- Return
true
.
- If
specifier
equals .
or ..
, or if specifier
starts with /
, \
, ./
, .\
, ../
, or ..\
:
- If
options.imports
is set:
- If
packageImportsExports(specifier, options.imports, parentURL, true, options)
returns true
:
- Return
true
.
- Let
yielded
be false
. - If
file(specifier, parentURL, false, options)
returns true
:
- Set
yielded
to true
.
- If
directory(specifier, parentURL, options)
returns true
:
- Set
yielded
to true
.
- Return
yielded
.
- Return
package(specifier, parentURL, options)
.
const generator = resolve.url(url, parentURL[, options])
- If
url
is not a valid URL:
- Return
false
.
- If
options.imports
is set:
- If
packageImportsExports(url.href, options.imports, parentURL, true, options)
returns true
:
- Return
true
.
- 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
and return true
.
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)
- Return
false
.
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)
returns true
:
- Return
true
.
- Let
packageSubpath
be .
concatenated with the substring of packageSpecifier
from the position at the length of packageName
. - If
packageSelf(packageName, packageSubpath, parentURL, options)
returns true
:
- Return
true
.
- Repeat:
- Let
packageURL
be the resolution of node_modules/
concatenated with packageName
and /
relative to parentURL
. - Set
parentURL
to the substring of parentURL
until the last /
. - Let
info
be the result of yielding the resolution of package.json
relative to 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)
.
- Let
yielded
be false
.
- If
file(packageSubpath, packageURL, false, options)
returns true
:
- Set
yielded
to true
.
- If
directory(packageSubpath, packageURL, options)
returns true
:
- Set
yielded
to true
.
- Return
yielded
.
- If
parentURL
is the file system root:
- Return
false
.
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 not
info.name
equals packageName
:
- Return
false
.
- 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)
.
- Let
yielded
be false
.
- If
file(packageSubpath, packageURL, false, options)
returns true
:
- Set
yielded
to true
.
- If
directory(packageSubpath, packageURL, options)
returns true
:
- Set
yielded
to true
.
- Return
yielded
.
- Return
false
.
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)
returns true
:
- Return
true
.
- Otherwise, if
exports
is a non-null
object:
- If every key of
exports
starts with .
:
- If
packageImportsExports(subpath, exports, packageURL, false, options)
returns true
:
- Return
true
.
- Throw.
const generator = resolve.packageImports(specifier, parentURL[, options])
- If
specifier
is #
or starts with #/
, throw. - For each value
packageURL
of lookupPackageScope(parentURL, opions)
:
- 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)
returns true
:
- Return
true
.
- If specifier starts with
#
, throw. - Return
false
.
- If
options.imports
is set:
- If
packageImportsExports(url.href, options.imports, parentURL, true, options)
returns true
:
- Return
true
.
- Return
false
.
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)
.
- Return
false
.
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)
returns true
:
- Return
true
.
- If
target
equals .
or ..
, or if target
starts with /
, ./
, or ../
:
- Yield the resolution of
target
relative to packageURL
and return true
.
- Return
package(target, packageURL, options)
.
- If
target
is an array:
- For each value
targetValue
of target
:
- If
packageTarget(packageURL, targetValue, patternMatch, isImports, options)
returns true
:
- Return
true
.
- If
target
is a non-null
object:
- For each key
p
of target
:
- If
p
equals default
or if options.conditions
includes p
:
- Let
targetValue
be target[p]
. - Return
packageTarget(packageURL, targetValue, patternMatch, isImports, options)
.
- Return
false
.
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 true
.
- 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 true
.
- If
target
is an array:
- For each value
targetValue
of target
:
- If
builtinTarget(packageSpecifier, packageVersion, targetValue, options)
returns true
:
- Return
true
.
- If
target
is a non-null
object:
- For each key
p
of target
:
- If
p
equals default
or if options.conditions
includes p
:
- Let
targetValue
be target[p]
. - Return
builtinTarget(packageSpecifier, packageVersion, targetValue, options)
.
- Return
false
.
const generator = resolve.file(filename, parentURL, isIndex[, options])
- If
filename
equals .
or ..
, or if filename
ends with /
or \
:
- Return
false
.
- 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
:
- Yield the resolution of
filename
concatenated with ext
relative to parentURL
.
- If
isIndex
is false
or options.extensions
is non-empty:
- Return
true
.
- Return
false
.
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:
- Let
yielded
be false
. - If
file(info.main, directoryURL, false, options)
returns true
:
- Set
yielded
to true
.
- If
directory(info.main, directoryURL, options)
returns true
:
- Set
yielded
to true
.
- Return
yielded
.
- Return
file('index', directoryURL, true, options)
.
License
Apache-2.0