• Stars
    star
    366
  • Rank 116,547 (Top 3 %)
  • Language
    TypeScript
  • License
    MIT License
  • Created almost 4 years ago
  • Updated 10 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

A tiny (952b), correct, general-purpose, and configurable `"exports"` and `"imports"` resolver without file-system reliance

resolve.exports CI codecov

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

$ npm install resolve.exports

Usage

Please see /test/ for examples.

import * as resolve from 'resolve.exports';

// package.json contents
const pkg = {
  "name": "foobar",
  "module": "dist/module.mjs",
  "main": "dist/require.js",
  "imports": {
    "#hash": {
      "import": {
        "browser": "./hash/web.mjs",
        "node": "./hash/node.mjs",
      },
      "default": "./hash/detect.js"
    }
  },
  "exports": {
    ".": {
      "import": "./dist/module.mjs",
      "require": "./dist/require.js"
    },
    "./lite": {
      "worker": {
        "browser": "./lite/worker.browser.js",
        "node": "./lite/worker.node.js"
      },
      "import": "./lite/module.mjs",
      "require": "./lite/require.js"
    }
  }
};

// ---
// Exports
// ---

// entry: "foobar" === "." === default
// conditions: ["default", "import", "node"]
resolve.exports(pkg);
resolve.exports(pkg, '.');
resolve.exports(pkg, 'foobar');
//=> ["./dist/module.mjs"]

// entry: "foobar/lite" === "./lite"
// conditions: ["default", "import", "node"]
resolve.exports(pkg, 'foobar/lite');
resolve.exports(pkg, './lite');
//=> ["./lite/module.mjs"]

// Enable `require` condition
// conditions: ["default", "require", "node"]
resolve.exports(pkg, 'foobar', { require: true }); //=> ["./dist/require.js"]
resolve.exports(pkg, './lite', { require: true }); //=> ["./lite/require.js"]

// Throws "Missing <entry> specifier in <name> package" Error
resolve.exports(pkg, 'foobar/hello');
resolve.exports(pkg, './hello/world');

// Add custom condition(s)
// conditions: ["default", "worker", "import", "node"]
resolve.exports(pkg, 'foobar/lite', {
  conditions: ['worker']
}); //=> ["./lite/worker.node.js"]

// Toggle "browser" condition
// conditions: ["default", "worker", "import", "browser"]
resolve.exports(pkg, 'foobar/lite', {
  conditions: ['worker'],
  browser: true
}); //=> ["./lite/worker.browser.js"]

// Disable non-"default" condition activate
// NOTE: breaks from Node.js default behavior
// conditions: ["default", "custom"]
resolve.exports(pkg, 'foobar/lite', {
  conditions: ['custom'],
  unsafe: true,
});
//=> Error: No known conditions for "./lite" specifier in "foobar" package

// ---
// Imports
// ---

// conditions: ["default", "import", "node"]
resolve.imports(pkg, '#hash');
resolve.imports(pkg, 'foobar/#hash');
//=> ["./hash/node.mjs"]

// conditions: ["default", "import", "browser"]
resolve.imports(pkg, '#hash', { browser: true });
resolve.imports(pkg, 'foobar/#hash');
//=> ["./hash/web.mjs"]

// conditions: ["default"]
resolve.imports(pkg, '#hash', { unsafe: true });
resolve.imports(pkg, 'foobar/#hash');
//=> ["./hash/detect.mjs"]

resolve.imports(pkg, '#hello/world');
resolve.imports(pkg, 'foobar/#hello/world');
//=> Error: Missing "#hello/world" specifier in "foobar" package

// ---
// Legacy
// ---

// prefer "module" > "main" (default)
resolve.legacy(pkg); //=> "dist/module.mjs"

// customize fields order
resolve.legacy(pkg, {
  fields: ['main', 'module']
}); //=> "dist/require.js"

API

The resolve(), exports(), and imports() functions share similar API signatures:

export function resolve(pkg: Package, entry?: string, options?: Options): string[] | undefined;
export function exports(pkg: Package, entry?: string, options?: Options): string[] | undefined;
export function imports(pkg: Package, target: string, options?: Options): string[] | undefined;
//                                         ^ 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, or undefined

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.

import * as r from 'resolve.exports';

let pkg = {
  name: 'foobar',
  // ...
};

r.resolve(pkg);
//~> r.exports(pkg, '.');

r.resolve(pkg, 'foobar');
//~> r.exports(pkg, '.');

r.resolve(pkg, 'foobar/subpath');
//~> r.exports(pkg, './subpath');

r.resolve(pkg, '#hash/md5');
//~> r.imports(pkg, '#hash/md5');

r.resolve(pkg, 'foobar/#hash/md5');
//~> 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 (see options.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 (see options.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:

const pkg = {
  // package.json ...
  "exports": {
    "worker": "./$worker.js",
    "require": "./$require.js",
    "production": "./$production.js",
    "import": "./$import.mjs",
  }
};

resolve.exports(pkg, '.');
// Conditions: ["default", "import", "node"]
//=> ["./$import.mjs"]

resolve.exports(pkg, '.', {
  conditions: ['production']
});
// Conditions: ["default", "production", "import", "node"]
//=> ["./$production.js"]

resolve.exports(pkg, '.', {
  conditions: ['production'],
  require: true,
});
// Conditions: ["default", "production", "require", "node"]
//=> ["./$require.js"]

resolve.exports(pkg, '.', {
  conditions: ['production', 'worker'],
  require: true,
});
// Conditions: ["default", "production", "worker", "require", "node"]
//=> ["./$worker.js"]

resolve.exports(pkg, '.', {
  conditions: ['production', 'worker']
});
// Conditions: ["default", "production", "worker", "import", "node"]
//=> ["./$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.

resolve.exports(pkg, '.');
//=> Conditions: ["default", "import", "node"]

resolve.exports(pkg, '.', { unsafe: true });
//=> Conditions: ["default"]

resolve.exports(pkg, '.', { unsafe: true, require: true, browser: true });
//=> 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:

resolve.exports(pkg, '.', {
  unsafe: true,
  conditions: ["require"]
});
//=> Conditions: ["default", "require"]

resolve.exports(pkg, '.', {
  unsafe: true,
  conditions: ["browser", "require", "custom123"]
});
//=> 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.

import { legacy } from 'resolve.exports';

// package.json
const pkg = {
  "name": "...",
  "worker": "worker.js",
  "module": "module.mjs",
  "browser": "browser.js",
  "main": "main.js",
};

legacy(pkg);
// fields = [module, main]
//=> "module.mjs"

legacy(pkg, { browser: true });
// fields = [browser, module, main]
//=> "browser.mjs"

legacy(pkg, {
  fields: ['missing', 'worker', 'module', 'main']
});
// fields = [missing, worker, module, main]
//=> "worker.js"

legacy(pkg, {
  fields: ['missing', 'worker', 'module', 'main'],
  browser: true,
});
// fields = [browser, missing, worker, module, main]
//=> "browser.js"

legacy(pkg, {
  fields: ['module', 'browser', 'main'],
  browser: true,
});
// fields = [module, browser, main]
//=> "module.mjs"

License

MIT © Luke Edwards

More Repositories

1

clsx

A tiny (239B) utility for constructing `className` strings conditionally.
JavaScript
8,212
star
2

polka

A micro web server so fast, it'll make you dance! 👯
JavaScript
5,266
star
3

pwa

(WIP) Universal PWA Builder
JavaScript
3,127
star
4

uvu

uvu is an extremely fast and lightweight test runner for Node.js and the browser
JavaScript
2,970
star
5

taskr

A fast, concurrency-focused task automation tool.
JavaScript
2,528
star
6

sockette

The cutest little WebSocket wrapper! 🧦
JavaScript
2,398
star
7

worktop

The next generation web framework for Cloudflare Workers
TypeScript
1,652
star
8

kleur

The fastest Node.js library for formatting terminal text with ANSI colors~!
JavaScript
1,616
star
9

klona

A tiny (240B to 501B) and fast utility to "deep clone" Objects, Arrays, Dates, RegExps, and more!
JavaScript
1,602
star
10

dequal

A tiny (304B to 489B) utility to check for deep equality
JavaScript
1,365
star
11

tsm

TypeScript Module Loader
TypeScript
1,179
star
12

tinydate

A tiny (349B) reusable date formatter. Extremely fast!
JavaScript
1,060
star
13

sirv

An optimized middleware & CLI application for serving static files~!
JavaScript
1,059
star
14

sade

Smooth (CLI) Operator 🎶
JavaScript
1,009
star
15

rosetta

A general purpose internationalization library in 292 bytes
JavaScript
788
star
16

navaid

A navigation aid (aka, router) for the browser in 850 bytes~!
JavaScript
775
star
17

dset

A tiny (194B) utility for safely writing deep Object values~!
JavaScript
754
star
18

tschema

A tiny (500b) utility to build JSON schema types.
TypeScript
697
star
19

uid

A tiny (130B to 205B) and fast utility to generate random IDs of fixed length
JavaScript
652
star
20

httpie

A Node.js HTTP client as easy as pie! 🥧
JavaScript
579
star
21

ganalytics

A tiny (312B) client-side module for tracking with Google Analytics
JavaScript
575
star
22

regexparam

A tiny (394B) utility that converts route patterns into RegExp. Limited alternative to `path-to-regexp` 🙇‍♂️
JavaScript
565
star
23

trouter

🐟 A fast, small-but-mighty, familiar fish...errr, router*
JavaScript
563
star
24

dimport

Run ES Module syntax (`import`, `import()`, and `export`) in any browser – even IE!
JavaScript
548
star
25

mri

Quickly scan for CLI flags and arguments
JavaScript
533
star
26

tempura

A light, crispy, and delicious template engine 🍤
JavaScript
527
star
27

calendarize

A tiny (202B) utility to generate calendar views.
JavaScript
478
star
28

formee

A tiny (532B) library for handling <form> elements.
JavaScript
441
star
29

qss

A tiny (294b) browser utility for encoding & decoding a querystring.
JavaScript
408
star
30

uuid

A tiny (~230B)and fast UUID (V4) generator for Node and the browser
JavaScript
396
star
31

preact-starter

Webpack3 boilerplate for building SPA / PWA / offline front-end apps with Preact
JavaScript
387
star
32

fetch-event-stream

A tiny (736b) utility for Server Sent Event (SSE) streaming via `fetch` and Web Streams API
TypeScript
374
star
33

vegemite

A Pub/Sub state manager you'll love... or hate
JavaScript
373
star
34

polkadot

The tiny HTTP server that gets out of your way! ・
JavaScript
325
star
35

matchit

Quickly parse & match URLs
JavaScript
321
star
36

flru

A tiny (215B) and fast Least Recently Used (LRU) cache
JavaScript
313
star
37

mrmime

A tiny (2.8kB) and fast utility for getting a MIME type from an extension or filename
TypeScript
312
star
38

watchlist

Recursively watch a list of directories & run a command on any file system changes
JavaScript
262
star
39

ley

(WIP) Driver-agnostic database migrations
JavaScript
261
star
40

arr

A collection of tiny, highly performant Array.prototype alternatives
JavaScript
255
star
41

flattie

A tiny (203B) and fast utility to flatten an object with customizable glue
JavaScript
254
star
42

webpack-messages

Beautifully format Webpack messages throughout your bundle lifecycle(s)!
JavaScript
246
star
43

obj-str

A tiny (96B) library for serializing Object values to Strings.
JavaScript
225
star
44

templite

Lightweight templating in 150 bytes
JavaScript
224
star
45

empathic

A set of small Node.js utilities to understand your pathing needs.
TypeScript
221
star
46

ms

A tiny (414B) and fast utility to convert milliseconds to and from strings.
JavaScript
215
star
47

nestie

A tiny (215B) and fast utility to expand a flattened object
JavaScript
201
star
48

throttles

A tiny (139B to 204B) utility to regulate the execution rate of your functions
JavaScript
199
star
49

hexoid

A tiny (190B) and extremely fast utility to generate random IDs of fixed length
JavaScript
193
star
50

astray

Walk an AST without being led astray
JavaScript
184
star
51

fromnow

A tiny (339B) utility for human-readable time differences between now and past or future dates.
JavaScript
178
star
52

tmp-cache

A least-recently-used cache in 35 lines of code~!
JavaScript
177
star
53

bundt

A simple bundler for your delicious modules
JavaScript
169
star
54

wrr

A tiny (148B) weighted round robin utility
JavaScript
164
star
55

freshie

(WIP) A fresh take on building universal applications with support for pluggable frontends and backends.
TypeScript
155
star
56

svelte-ssr-worker

A quick demo for rendering Svelte server-side (SSR), but within a Cloudflare Worker!
JavaScript
154
star
57

totalist

A tiny (195B to 224B) utility to recursively list all (total) files in a directory
JavaScript
152
star
58

escalade

A tiny (183B to 210B) and fast utility to ascend parent directories
JavaScript
148
star
59

typescript-module

Template repository for authoring npm modules via TypeScript
TypeScript
143
star
60

sublet

Reactive leases for data subscriptions
JavaScript
140
star
61

webpack-route-manifest

Generate an asset manifest file, keyed by route patterns!
JavaScript
127
star
62

semiver

A tiny (153B) utility to compare semver strings.
JavaScript
123
star
63

url-shim

A 1.5kB browser polyfill for the Node.js `URL` and `URLSearchParams` classes.
JavaScript
123
star
64

svelte-demo

Multi-page demo built Svelte 3.x and Rollup with code-splitting
Svelte
114
star
65

saturated

A tiny (203B) utility to enqueue items for batch processing and/or satisfying rate limits.
JavaScript
112
star
66

webpack-format-messages

Beautiful formatting for Webpack messages; ported from Create React App!
JavaScript
111
star
67

gittar

🎸 Download and/or Extract git repositories (GitHub, GitLab, BitBucket). Cross-platform and Offline-first!
JavaScript
111
star
68

cfw

(WIP) A build and deploy utility for Cloudflare Workers.
TypeScript
109
star
69

webpack-critical

Extracts & inlines Critical CSS with Wepack
JavaScript
109
star
70

pages-fullstack

Demo SvelteKit application running on Cloudflare Pages
Svelte
101
star
71

sort-isostring

A tiny (110B) and fast utility to sort ISO 8601 Date strings
JavaScript
98
star
72

colors-app

🎨 A PWA for copying values from popular color palettes. Supports HEX, RGB, and HSL formats.
JavaScript
95
star
73

salteen

A snappy and lightweight (259B) utility to encrypt and decrypt values with salt.
JavaScript
95
star
74

is-offline

A tiny (174B) library to detect `offline` status & respond to changes in the browser.
JavaScript
91
star
75

seolint

(WIP) A robust and configurable SEO linter
TypeScript
87
star
76

rafps

A tiny (178B) helper for playing, pausing, and setting `requestAnimationFrame` frame rates
JavaScript
82
star
77

preact-cli-ssr

A quick demo for adding SSR to a Preact CLI app
JavaScript
79
star
78

webpack-modules

Handle `.mjs` files correctly within webpack
JavaScript
71
star
79

csprng

A tiny (~90B) isomorphic wrapper for `crypto.randomBytes` in Node.js and browsers.
JavaScript
68
star
80

premove

A tiny (201B to 247B) utility to remove items recursively
JavaScript
66
star
81

classico

A tiny (255B) shim when Element.classList cannot be used~!
JavaScript
62
star
82

mk-dirs

A tiny (381B to 419B) utility to make a directory and its parents, recursively
JavaScript
54
star
83

primeval

A tiny (128B) utility to check if a value is a prime number
JavaScript
52
star
84

loadr

Quickly attach multiple ESM Loaders and/or Require Hooks together but without the repetitive `--experimental-loader` and/or `--require` Node flags
JavaScript
49
star
85

preact-progress

Simple and lightweight (~590 bytes gzip) progress bar component for Preact
JavaScript
49
star
86

route-manifest

A tiny (412B) runtime to retrieve the correct entry from a Route Manifest file.
JavaScript
46
star
87

svelte-preprocess-esbuild

A Svelte Preprocessor to compile TypeScript via esbuild!
TypeScript
45
star
88

rollup-route-manifest

A Rollup plugin to generate an asset manifest, keyed by route patterns ("route manifest")
JavaScript
41
star
89

preact-scroll-header

A (800b gzip) header that will show/hide while scrolling for Preact
JavaScript
41
star
90

inferno-starter

Webpack2 boilerplate for building SPA / PWA / offline front-end apps with Inferno.js
JavaScript
41
star
91

onloaded

A tiny (350B) library to detect when images have loaded.
JavaScript
38
star
92

route-sort

A tiny (200B) utility to sort route patterns by specificity
JavaScript
36
star
93

webpack-plugin-replace

Replace content while bundling.
JavaScript
36
star
94

scorta

A tiny (330B to 357B) and fast utility to find a package's hidden supply / cache directory.
JavaScript
34
star
95

local-hostname

A tiny (171B) utility to check if a hostname is local
JavaScript
32
star
96

taskr-outdated

A generator & coroutine-based task runner. Fasten your seatbelt. 🚀
JavaScript
32
star
97

rewrite-imports

Rewrite `import` statements as `require()`s; via RegExp
JavaScript
31
star
98

preact-offline

A (300b gzip) component to render alerts/messages when offline.
JavaScript
29
star
99

fly-kit-preact

A starter kit for building offline / SPA / PWA apps with Preact
JavaScript
28
star
100

fannypack

The tool belt for front-end developers
JavaScript
28
star