A tiny (952b), correct, general-purpose, and configurable `"exports"` and `"imports"` resolver without file-system reliance
Installations
npm install resolve.exports
Developer
lukeed
Developer Guide
Module System
CommonJS, ESM
Min. Node Version
>=10
Typescript Support
Yes
Node Version
18.12.1
NPM Version
8.19.2
Statistics
368 Stars
90 Commits
15 Forks
11 Watching
2 Branches
8 Contributors
Updated on 11 Nov 2024
Bundle Size
2.08 kB
Minified
1.02 kB
Minified + Gzipped
Languages
TypeScript (100%)
Total Downloads
Cumulative downloads
Total Downloads
2,197,043,914
Last day
-1.4%
4,870,191
Compared to previous day
Last week
3.4%
25,789,226
Compared to previous week
Last month
15.4%
104,987,403
Compared to previous month
Last year
37.7%
1,052,364,031
Compared to previous year
Daily Downloads
Weekly Downloads
Monthly Downloads
Yearly Downloads
Dev Dependencies
4
resolve.exports
A tiny (952b), correct, general-purpose, and configurable
"exports"
and"imports"
resolver without file-system reliance
Why?
Hopefully, this module may serve as a reference point (and/or be used directly) so that the varying tools and bundlers within the ecosystem can share a common approach with one another as well as with the native Node.js implementation.
With the push for ESM, we must be very careful and avoid fragmentation. If we, as a community, begin propagating different dialects of the resolution algorithm, then we're headed for deep trouble. It will make supporting (and using) "exports"
nearly impossible, which may force its abandonment and along with it, its benefits.
Let's have nice things.
Install
1$ npm install resolve.exports
Usage
Please see
/test/
for examples.
1import * as resolve from 'resolve.exports'; 2 3// package.json contents 4const pkg = { 5 "name": "foobar", 6 "module": "dist/module.mjs", 7 "main": "dist/require.js", 8 "imports": { 9 "#hash": { 10 "import": { 11 "browser": "./hash/web.mjs", 12 "node": "./hash/node.mjs", 13 }, 14 "default": "./hash/detect.js" 15 } 16 }, 17 "exports": { 18 ".": { 19 "import": "./dist/module.mjs", 20 "require": "./dist/require.js" 21 }, 22 "./lite": { 23 "worker": { 24 "browser": "./lite/worker.browser.js", 25 "node": "./lite/worker.node.js" 26 }, 27 "import": "./lite/module.mjs", 28 "require": "./lite/require.js" 29 } 30 } 31}; 32 33// --- 34// Exports 35// --- 36 37// entry: "foobar" === "." === default 38// conditions: ["default", "import", "node"] 39resolve.exports(pkg); 40resolve.exports(pkg, '.'); 41resolve.exports(pkg, 'foobar'); 42//=> ["./dist/module.mjs"] 43 44// entry: "foobar/lite" === "./lite" 45// conditions: ["default", "import", "node"] 46resolve.exports(pkg, 'foobar/lite'); 47resolve.exports(pkg, './lite'); 48//=> ["./lite/module.mjs"] 49 50// Enable `require` condition 51// conditions: ["default", "require", "node"] 52resolve.exports(pkg, 'foobar', { require: true }); //=> ["./dist/require.js"] 53resolve.exports(pkg, './lite', { require: true }); //=> ["./lite/require.js"] 54 55// Throws "Missing <entry> specifier in <name> package" Error 56resolve.exports(pkg, 'foobar/hello'); 57resolve.exports(pkg, './hello/world'); 58 59// Add custom condition(s) 60// conditions: ["default", "worker", "import", "node"] 61resolve.exports(pkg, 'foobar/lite', { 62 conditions: ['worker'] 63}); //=> ["./lite/worker.node.js"] 64 65// Toggle "browser" condition 66// conditions: ["default", "worker", "import", "browser"] 67resolve.exports(pkg, 'foobar/lite', { 68 conditions: ['worker'], 69 browser: true 70}); //=> ["./lite/worker.browser.js"] 71 72// Disable non-"default" condition activate 73// NOTE: breaks from Node.js default behavior 74// conditions: ["default", "custom"] 75resolve.exports(pkg, 'foobar/lite', { 76 conditions: ['custom'], 77 unsafe: true, 78}); 79//=> Error: No known conditions for "./lite" specifier in "foobar" package 80 81// --- 82// Imports 83// --- 84 85// conditions: ["default", "import", "node"] 86resolve.imports(pkg, '#hash'); 87resolve.imports(pkg, 'foobar/#hash'); 88//=> ["./hash/node.mjs"] 89 90// conditions: ["default", "import", "browser"] 91resolve.imports(pkg, '#hash', { browser: true }); 92resolve.imports(pkg, 'foobar/#hash'); 93//=> ["./hash/web.mjs"] 94 95// conditions: ["default"] 96resolve.imports(pkg, '#hash', { unsafe: true }); 97resolve.imports(pkg, 'foobar/#hash'); 98//=> ["./hash/detect.mjs"] 99 100resolve.imports(pkg, '#hello/world'); 101resolve.imports(pkg, 'foobar/#hello/world'); 102//=> Error: Missing "#hello/world" specifier in "foobar" package 103 104// --- 105// Legacy 106// --- 107 108// prefer "module" > "main" (default) 109resolve.legacy(pkg); //=> "dist/module.mjs" 110 111// customize fields order 112resolve.legacy(pkg, { 113 fields: ['main', 'module'] 114}); //=> "dist/require.js"
API
The resolve()
, exports()
, and imports()
functions share similar API signatures:
1export function resolve(pkg: Package, entry?: string, options?: Options): string[] | undefined; 2export function exports(pkg: Package, entry?: string, options?: Options): string[] | undefined; 3export function imports(pkg: Package, target: string, options?: Options): string[] | undefined; 4// ^ not optional!
All three:
- accept a
package.json
file's contents as a JSON object - accept a target/entry identifier
- may accept an Options object
- return
string[]
,string
, orundefined
The only difference is that imports()
must accept a target identifier as there can be no inferred default.
See below for further API descriptions.
Note: There is also a Legacy Resolver API
resolve(pkg, entry?, options?)
Returns: string[]
or undefined
A convenience helper which automatically reroutes to exports()
or imports()
depending on the entry
value.
When unspecified, entry
defaults to the "."
identifier, which means that exports()
will be invoked.
1import * as r from 'resolve.exports'; 2 3let pkg = { 4 name: 'foobar', 5 // ... 6}; 7 8r.resolve(pkg); 9//~> r.exports(pkg, '.'); 10 11r.resolve(pkg, 'foobar'); 12//~> r.exports(pkg, '.'); 13 14r.resolve(pkg, 'foobar/subpath'); 15//~> r.exports(pkg, './subpath'); 16 17r.resolve(pkg, '#hash/md5'); 18//~> r.imports(pkg, '#hash/md5'); 19 20r.resolve(pkg, 'foobar/#hash/md5'); 21//~> r.imports(pkg, '#hash/md5');
exports(pkg, entry?, options?)
Returns: string[]
or undefined
Traverse the "exports"
within the contents of a package.json
file.
If the contents does not contain an "exports"
map, then undefined
will be returned.
Successful resolutions will always result in a string
or string[]
value. This will be the value of the resolved mapping itself – which means that the output is a relative file path.
This function may throw an Error if:
- the requested
entry
cannot be resolved (aka, not defined in the"exports"
map) - an
entry
is defined but no known conditions were matched (seeoptions.conditions
)
pkg
Type: object
Required: true
The package.json
contents.
entry
Type: string
Required: false
Default: .
(aka, root)
The desired target entry, or the original import
path.
When entry
is not a relative path (aka, does not start with '.'
), then entry
is given the './'
prefix.
When entry
begins with the package name (determined via the pkg.name
value), then entry
is truncated and made relative.
When entry
is already relative, it is accepted as is.
Examples
Assume we have a module named "foobar" and whose pkg
contains "name": "foobar"
.
entry value | treated as | reason |
---|---|---|
null / undefined | '.' | default |
'.' | '.' | value was relative |
'foobar' | '.' | value was pkg.name |
'foobar/lite' | './lite' | value had pkg.name prefix |
'./lite' | './lite' | value was relative |
'lite' | './lite' | value was not relative & did not have pkg.name prefix |
imports(pkg, target, options?)
Returns: string[]
or undefined
Traverse the "imports"
within the contents of a package.json
file.
If the contents does not contain an "imports"
map, then undefined
will be returned.
Successful resolutions will always result in a string
or string[]
value. This will be the value of the resolved mapping itself – which means that the output is a relative file path.
This function may throw an Error if:
- the requested
target
cannot be resolved (aka, not defined in the"imports"
map) - an
target
is defined but no known conditions were matched (seeoptions.conditions
)
pkg
Type: object
Required: true
The package.json
contents.
target
Type: string
Required: true
The target import identifier; for example, #hash
or #hash/md5
.
Import specifiers must begin with the #
character, as required by the resolution specification. However, if target
begins with the package name (determined by the pkg.name
value), then resolve.exports
will trim it from the target
identifier. For example, "foobar/#hash/md5"
will be treated as "#hash/md5"
for the "foobar"
package.
Options
The resolve()
, imports()
, and exports()
functions share these options. All properties are optional and you are not required to pass an options
argument.
Collectively, the options
are used to assemble a list of conditions that should be activated while resolving your target(s).
Note: Although the Node.js documentation primarily showcases conditions alongside
"exports"
usage, they also apply to"imports"
maps too. (example)
options.require
Type: boolean
Default: false
When truthy, the "require"
field is added to the list of allowed/known conditions.
Otherwise the "import"
field is added instead.
options.browser
Type: boolean
Default: false
When truthy, the "browser"
field is added to the list of allowed/known conditions.
Otherwise the "node"
field is added instead.
options.conditions
Type: string[]
Default: []
A list of additional/custom conditions that should be accepted when seen.
Important: The order specified within
options.conditions
does not matter.
The matching order/priority is always determined by the"exports"
map's key order.
For example, you may choose to accept a "production"
condition in certain environments. Given the following pkg
content:
1const pkg = { 2 // package.json ... 3 "exports": { 4 "worker": "./$worker.js", 5 "require": "./$require.js", 6 "production": "./$production.js", 7 "import": "./$import.mjs", 8 } 9}; 10 11resolve.exports(pkg, '.'); 12// Conditions: ["default", "import", "node"] 13//=> ["./$import.mjs"] 14 15resolve.exports(pkg, '.', { 16 conditions: ['production'] 17}); 18// Conditions: ["default", "production", "import", "node"] 19//=> ["./$production.js"] 20 21resolve.exports(pkg, '.', { 22 conditions: ['production'], 23 require: true, 24}); 25// Conditions: ["default", "production", "require", "node"] 26//=> ["./$require.js"] 27 28resolve.exports(pkg, '.', { 29 conditions: ['production', 'worker'], 30 require: true, 31}); 32// Conditions: ["default", "production", "worker", "require", "node"] 33//=> ["./$worker.js"] 34 35resolve.exports(pkg, '.', { 36 conditions: ['production', 'worker'] 37}); 38// Conditions: ["default", "production", "worker", "import", "node"] 39//=> ["./$worker.js"]
options.unsafe
Type: boolean
Default: false
Important: You probably do not want this option!
It will break out of Node's default resolution conditions.
When enabled, this option will ignore all other options except options.conditions
. This is because, when enabled, options.unsafe
does not assume or provide any default conditions except the "default"
condition.
1resolve.exports(pkg, '.'); 2//=> Conditions: ["default", "import", "node"] 3 4resolve.exports(pkg, '.', { unsafe: true }); 5//=> Conditions: ["default"] 6 7resolve.exports(pkg, '.', { unsafe: true, require: true, browser: true }); 8//=> Conditions: ["default"]
In other words, this means that trying to use options.require
or options.browser
alongside options.unsafe
will have no effect. In order to enable these conditions, you must provide them manually into the options.conditions
list:
1resolve.exports(pkg, '.', { 2 unsafe: true, 3 conditions: ["require"] 4}); 5//=> Conditions: ["default", "require"] 6 7resolve.exports(pkg, '.', { 8 unsafe: true, 9 conditions: ["browser", "require", "custom123"] 10}); 11//=> Conditions: ["default", "browser", "require", "custom123"]
Legacy Resolver
Also included is a "legacy" method for resolving non-"exports"
package fields. This may be used as a fallback method when for when no "exports"
mapping is defined. In other words, it's completely optional (and tree-shakeable).
legacy(pkg, options?)
Returns: string
or undefined
You may customize the field priority via options.fields
.
When a field is found, its value is returned as written.
When no fields were found, undefined
is returned. If you wish to mimic Node.js behavior, you can assume this means 'index.js'
– but this module does not make that assumption for you.
options.browser
Type: boolean
or string
Default: false
When truthy, ensures that the 'browser'
field is part of the acceptable fields
list.
Important: If your custom
options.fields
value includes'browser'
, then your order is respected.
Otherwise, when truthy,options.browser
will move'browser'
to the front of the list, making it the top priority.
When true
and "browser"
is an object, then legacy()
will return the the entire "browser"
object.
You may also pass a string value, which will be treated as an import/file path. When this is the case and "browser"
is an object, then legacy()
may return:
false
– if the package author decided a file should be ignored; or- your
options.browser
string value – but made relative, if not already
See the `"browser" field specification for more information.
options.fields
Type: string[]
Default: ['module', 'main']
A list of fields to accept. The order of the array determines the priority/importance of each field, with the most important fields at the beginning of the list.
By default, the legacy()
method will accept any "module"
and/or "main" fields if they are defined. However, if both fields are defined, then "module" will be returned.
1import { legacy } from 'resolve.exports'; 2 3// package.json 4const pkg = { 5 "name": "...", 6 "worker": "worker.js", 7 "module": "module.mjs", 8 "browser": "browser.js", 9 "main": "main.js", 10}; 11 12legacy(pkg); 13// fields = [module, main] 14//=> "module.mjs" 15 16legacy(pkg, { browser: true }); 17// fields = [browser, module, main] 18//=> "browser.mjs" 19 20legacy(pkg, { 21 fields: ['missing', 'worker', 'module', 'main'] 22}); 23// fields = [missing, worker, module, main] 24//=> "worker.js" 25 26legacy(pkg, { 27 fields: ['missing', 'worker', 'module', 'main'], 28 browser: true, 29}); 30// fields = [browser, missing, worker, module, main] 31//=> "browser.js" 32 33legacy(pkg, { 34 fields: ['module', 'browser', 'main'], 35 browser: true, 36}); 37// fields = [module, browser, main] 38//=> "module.mjs"
License
MIT © Luke Edwards
No vulnerabilities found.
Reason
no binaries found in the repo
Reason
no dangerous workflow patterns detected
Reason
license file detected
Details
- Info: project has a license file: license:0
- Info: FSF or OSI recognized license: MIT License: license:0
Reason
0 existing vulnerabilities detected
Reason
Found 2/9 approved changesets -- score normalized to 2
Reason
0 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0
Reason
detected GitHub workflow tokens with excessive permissions
Details
- Warn: no topLevel permission defined: .github/workflows/ci.yml:1
- Info: no jobLevel write permissions found
Reason
dependency not pinned by hash detected -- score normalized to 0
Details
- Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ci.yml:28: update your workflow using https://app.stepsecurity.io/secureworkflow/lukeed/resolve.exports/ci.yml/master?enable=pin
- Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ci.yml:29: update your workflow using https://app.stepsecurity.io/secureworkflow/lukeed/resolve.exports/ci.yml/master?enable=pin
- Warn: npmCommand not pinned by hash: .github/workflows/ci.yml:35
- Warn: npmCommand not pinned by hash: .github/workflows/ci.yml:36
- Warn: downloadThenRun not pinned by hash: .github/workflows/ci.yml:52
- Info: 0 out of 2 GitHub-owned GitHubAction dependencies pinned
- Info: 0 out of 1 downloadThenRun dependencies pinned
- Info: 0 out of 2 npmCommand dependencies pinned
Reason
no effort to earn an OpenSSF best practices badge detected
Reason
project is not fuzzed
Details
- Warn: no fuzzer integrations found
Reason
security policy file not detected
Details
- Warn: no security policy file detected
- Warn: no security file to analyze
- Warn: no security file to analyze
- Warn: no security file to analyze
Reason
branch protection not enabled on development/release branches
Details
- Warn: branch protection not enabled for branch 'master'
Reason
SAST tool is not run on all commits -- score normalized to 0
Details
- Warn: 0 commits out of 25 are checked with a SAST tool
Score
3.6
/10
Last Scanned on 2024-11-18
The Open Source Security Foundation is a cross-industry collaboration to improve the security of open source software (OSS). The Scorecard provides security health metrics for open source projects.
Learn MoreOther packages similar to resolve.exports
eslint-import-resolver-exports
`package.json` `exports` field resolver for `eslint-plugin-import`
@okikio/resolve.imports
A tiny (613b), correct, general-purpose, and configurable subpath "imports" resolver without file-system reliance (forked from @lukeed's `resolve.exports`)
@alloc/resolve.exports
A tiny (854b), correct, general-purpose, and configurable "exports" resolver without file-system reliance
package-exports-resolver-kernel
A general-purpose package.json "exports" resolver without file-system reliance.