resolve.exports
Advanced tools
Comparing version 0.0.1 to 1.0.0
@@ -40,2 +40,12 @@ /** | ||
/** | ||
* @param {string} name the package name | ||
* @param {string} entry the target path/import | ||
*/ | ||
function toName(name, entry) { | ||
return entry === name ? '.' | ||
: entry[0] === '.' ? entry | ||
: entry.replace(new RegExp('^' + name + '\/'), './'); | ||
} | ||
/** | ||
* @param {object} pkg package.json contents | ||
@@ -45,4 +55,4 @@ * @param {string} [entry] entry name or import path | ||
* @param {boolean} [options.browser] | ||
* @param {boolean} [options.requires] | ||
* @param {string[]} [options.fields] | ||
* @param {boolean} [options.require] | ||
* @param {string[]} [options.conditions] | ||
*/ | ||
@@ -53,12 +63,7 @@ function resolve(pkg, entry='.', options={}) { | ||
if (exports) { | ||
let { browser, requires, fields=[] } = options; | ||
let { browser, require, conditions=[] } = options; | ||
let target = entry === name ? '.' | ||
: entry[0] === '.' ? entry | ||
: entry.replace(new RegExp('^' + name + '\/'), './'); | ||
let target = toName(name, entry); | ||
if (target[0] !== '.') target = './' + target; | ||
if (target[0] !== '.') { | ||
target = './' + target; | ||
} | ||
if (typeof exports === 'string') { | ||
@@ -68,6 +73,4 @@ return target === '.' ? exports : bail(name, target); | ||
let allows = new Set(['import', 'default', ...fields]); | ||
// TODO: should either/or import? | ||
if (requires) allows.add('require'); | ||
let allows = new Set(['default', ...conditions]); | ||
allows.add(require ? 'require' : 'import'); | ||
allows.add(browser ? 'browser' : 'node'); | ||
@@ -112,9 +115,11 @@ | ||
* @param {object} [options] | ||
* @param {boolean} [options.browser] | ||
* @param {string|boolean} [options.browser] | ||
* @param {string[]} [options.fields] | ||
*/ | ||
function legacy(pkg, options={}) { | ||
let i=0, tmp, fields = options.fields || ['module', 'main']; | ||
let i=0, value, | ||
browser = options.browser, | ||
fields = options.fields || ['module', 'main']; | ||
if (options.browser && !fields.includes('browser')) { | ||
if (browser && !fields.includes('browser')) { | ||
fields.unshift('browser'); | ||
@@ -124,4 +129,17 @@ } | ||
for (; i < fields.length; i++) { | ||
if ((tmp = pkg[fields[i]]) && typeof tmp === 'string') { | ||
return './' + tmp.replace(/^\.?\//, ''); | ||
if (value = pkg[fields[i]]) { | ||
if (typeof value == 'string') { | ||
// | ||
} else if (typeof value == 'object' && fields[i] == 'browser') { | ||
if (typeof browser == 'string') { | ||
value = value[browser=toName(pkg.name, browser)]; | ||
if (value == null) return browser; | ||
} | ||
} else { | ||
continue; | ||
} | ||
return typeof value == 'string' | ||
? ('./' + value.replace(/^\.?\//, '')) | ||
: value; | ||
} | ||
@@ -128,0 +146,0 @@ } |
export interface Options { | ||
requires?: boolean; | ||
browser?: boolean; | ||
fields?: string[]; | ||
conditions?: string[]; | ||
require?: boolean; | ||
} | ||
export function resolve<T=any>(pkg: T, entry: string, options?: Options): string | void; | ||
export function legacy<T=any>(pkg: T, options?: Omit<Options, 'requires'>): string | void; | ||
export type BrowserFiles = Record<string, string | false>; | ||
export function legacy<T=any>(pkg: T, options: { browser: true, fields?: string[] }): BrowserFiles | string | void; | ||
export function legacy<T=any>(pkg: T, options: { browser: string, fields?: string[] }): string | false | void; | ||
export function legacy<T=any>(pkg: T, options: { browser: false, fields?: string[] }): string | void; | ||
export function legacy<T=any>(pkg: T, options?: { | ||
browser?: boolean | string; | ||
fields?: string[]; | ||
}): BrowserFiles | string | false | void; |
{ | ||
"version": "0.0.1", | ||
"version": "1.0.0", | ||
"name": "resolve.exports", | ||
"repository": "lukeed/resolve.exports", | ||
"description": "WIP", | ||
"description": "A tiny (710b), correct, general-purpose, and configurable \"exports\" resolver without file-system reliance", | ||
"module": "dist/index.mjs", | ||
@@ -20,3 +20,2 @@ "main": "dist/index.js", | ||
"build": "bundt", | ||
"pretest": "npm run build", | ||
"test": "uvu -r esm test" | ||
@@ -35,3 +34,11 @@ }, | ||
}, | ||
"keywords": [], | ||
"keywords": [ | ||
"esm", | ||
"exports", | ||
"esmodules", | ||
"fields", | ||
"modules", | ||
"resolution", | ||
"resolve" | ||
], | ||
"devDependencies": { | ||
@@ -38,0 +45,0 @@ "bundt": "1.1.2", |
272
readme.md
@@ -1,5 +0,13 @@ | ||
# resolve.exports [![CI](https://github.com/lukeed/resolve.exports/workflows/CI/badge.svg)](https://github.com/lukeed/resolve.exports/actions) | ||
# resolve.exports [![CI](https://github.com/lukeed/resolve.exports/workflows/CI/badge.svg)](https://github.com/lukeed/resolve.exports/actions) [![codecov](https://badgen.net/codecov/c/github/lukeed/resolve.exports)](https://codecov.io/gh/lukeed/resolve.exports) | ||
> WIP | ||
> A tiny (710b), correct, general-purpose, and configurable `"exports"` 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 `"exports"` resolution, 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. | ||
***TODO*** | ||
@@ -10,10 +18,266 @@ | ||
- [x] exports object (multi entry) | ||
- [x] nested conditions | ||
- [x] nested / recursive conditions | ||
- [x] exports arrayable | ||
- [x] directory mapping (`./foobar/` => `/foobar/`) | ||
- [x] directory mapping (`./foobar/*` => `./other/*.js`) | ||
- [x] legacy fields | ||
- [x] legacy fields (`main` vs `module` vs ...) | ||
- [x] legacy "browser" files object | ||
## Install | ||
```sh | ||
$ npm install resolve.exports | ||
``` | ||
## Usage | ||
> Please see [`/test/`](/test) for examples. | ||
```js | ||
import { resolve, legacy } from 'resolve.exports'; | ||
const contents = { | ||
"name": "foobar", | ||
"module": "dist/module.mjs", | ||
"main": "dist/require.js", | ||
"exports": { | ||
".": { | ||
"import": "./dist/module.mjs", | ||
"require": "./dist/require.js" | ||
}, | ||
"./lite": { | ||
"worker": { | ||
"browser": "./lite/worker.brower.js", | ||
"node": "./lite/worker.node.js" | ||
}, | ||
"import": "./lite/module.mjs", | ||
"require": "./lite/require.js" | ||
} | ||
} | ||
}; | ||
// Assumes `.` as default entry | ||
// Assumes `import` as default condition | ||
resolve(contents); //=> "./dist/module.mjs" | ||
// entry: nullish === "foobar" === "." | ||
resolve(contents, 'foobar'); //=> "./dist/module.mjs" | ||
resolve(contents, '.'); //=> "./dist/module.mjs" | ||
// entry: "foobar/lite" === "./lite" | ||
resolve(contents, 'foobar/lite'); //=> "./lite/module.mjs" | ||
resolve(contents, './lite'); //=> "./lite/module.mjs" | ||
// Assume `require` usage | ||
resolve(contents, 'foobar', { require: true }); //=> "./dist/require.js" | ||
resolve(contents, './lite', { require: true }); //=> "./lite/require.js" | ||
// Throws "Missing <entry> export in <name> package" Error | ||
resolve(contents, 'foobar/hello'); | ||
resolve(contents, './hello/world'); | ||
// Add custom condition(s) | ||
resolve(contents, 'foobar/lite', { | ||
conditions: ['worker'] | ||
}); // => "./lite/worker.node.js" | ||
// Toggle "browser" condition | ||
resolve(contents, 'foobar/lite', { | ||
conditions: ['worker'], | ||
browser: true | ||
}); // => "./lite/worker.browser.js" | ||
// --- | ||
// Legacy | ||
// --- | ||
// prefer "module" > "main" (default) | ||
legacy(contents); //=> "dist/module.mjs" | ||
// customize fields order | ||
legacy(contents, { | ||
fields: ['main', 'module'] | ||
}); //=> "dist/require.js" | ||
``` | ||
## API | ||
### resolve(pkg, entry?, options?) | ||
Returns: `string` or `undefined` | ||
Traverse the `"exports"` within the contents of a `package.json` file. <br> | ||
If the contents _does not_ contain an `"exports"` map, then `undefined` will be returned. | ||
Successful resolutions will always result in a 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` _was_ resolved but no known conditions were found (see [`options.conditions`](#optionsconditions)) | ||
#### pkg | ||
Type: `object` <br> | ||
Required: `true` | ||
The `package.json` contents. | ||
#### entry | ||
Type: `string` <br> | ||
Required: `false` <br> | ||
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 | | ||
#### options.require | ||
Type: `boolean` <br> | ||
Default: `false` | ||
When truthy, the `"require"` field is added to the list of allowed/known conditions. | ||
When falsey, the `"import"` field is added to the list of allowed/known conditions instead. | ||
#### options.browser | ||
Type: `boolean` <br> | ||
Default: `false` | ||
When truthy, the `"browser"` field is added to the list of allowed/known conditions. | ||
#### options.conditions | ||
Type: `string[]` <br> | ||
Default: `[]` | ||
Provide a list of additional/custom conditions that should be accepted when seen. | ||
> **Important:** The order specified within `options.conditions` does not matter. <br>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: | ||
```js | ||
const contents = { | ||
// ... | ||
"exports": { | ||
"worker": "./index.worker.js", | ||
"require": "./index.require.js", | ||
"production": "./index.prod.js", | ||
"import": "./index.import.mjs", | ||
} | ||
}; | ||
resolve(contents, '.'); | ||
//=> "./index.import.mjs" | ||
resolve(contents, '.', { | ||
conditions: ['production'] | ||
}); //=> "./index.prod.js" | ||
resolve(contents, '.', { | ||
conditions: ['production'], | ||
require: true, | ||
}); //=> "./index.require.js" | ||
resolve(contents, '.', { | ||
conditions: ['production', 'worker'], | ||
require: true, | ||
}); //=> "./index.worker.js" | ||
resolve(contents, '.', { | ||
conditions: ['production', 'worker'] | ||
}); //=> "./index.worker.js" | ||
``` | ||
### legacy(pkg, options?) | ||
Returns: `string` or `undefined` | ||
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). | ||
You may customize the field priority via [`options.fields`](#optionsfields). | ||
When a field is found, its value is returned _as written_. <br> | ||
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` <br> | ||
Default: `false` | ||
When truthy, ensures that the `'browser'` field is part of the acceptable `fields` list. | ||
> **Important:** If your custom [`options.fields`](#optionsfields) value includes `'browser'`, then _your_ order is respected. <br>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](https://github.com/defunctzombie/package-browser-field-spec) for more information. | ||
#### options.fields | ||
Type: `string[]` <br> | ||
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. | ||
```js | ||
const contents = { | ||
"name": "...", | ||
"worker": "worker.js", | ||
"module": "module.mjs", | ||
"browser": "browser.js", | ||
"main": "main.js", | ||
} | ||
legacy(contents); | ||
// fields = [module, main] | ||
//=> "module.mjs" | ||
legacy(contents, { browser: true }); | ||
// fields = [browser, module, main] | ||
//=> "browser.mjs" | ||
legacy(contents, { | ||
fields: ['missing', 'worker', 'module', 'main'] | ||
}); | ||
// fields = [missing, worker, module, main] | ||
//=> "worker.js" | ||
legacy(contents, { | ||
fields: ['missing', 'worker', 'module', 'main'], | ||
browser: true, | ||
}); | ||
// fields = [browser, missing, worker, module, main] | ||
//=> "browser.js" | ||
legacy(contents, { | ||
fields: ['module', 'browser', 'main'], | ||
browser: true, | ||
}); | ||
// fields = [module, browser, main] | ||
//=> "module.mjs" | ||
``` | ||
## License | ||
MIT © [Luke Edwards](https://lukeed.com) |
Sorry, the diff of this file is not supported yet
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
18491
261
0
283
2