Socket
Socket
Sign inDemoInstall

resolve.exports

Package Overview
Dependencies
0
Maintainers
1
Versions
11
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 0.0.1 to 1.0.0

56

dist/index.js

@@ -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",

@@ -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

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc