aboutsummaryrefslogtreecommitdiff
path: root/node_modules/resolve.exports/readme.md
blob: f265c4643baaf835224a499321f09fae2d762ae0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
# 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)

> A tiny (737b), 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***

- [x] exports string
- [x] exports object (single entry)
- [x] exports object (multi entry)
- [x] nested / recursive conditions
- [x] exports arrayable
- [x] directory mapping (`./foobar/` => `/foobar/`)
- [x] directory mapping (`./foobar/*` => `./other/*.js`)
- [x] directory mapping w/ conditions
- [x] directory mapping w/ nested conditions
- [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"
```

#### options.unsafe
Type: `boolean` <br>
Default: `false`

> **Important:** You probably do not want this option! <br>It will break out of Node's default resolution conditions.

When enabled, this option will ignore **all other options** except [`options.conditions`](#optionsconditions). This is because, when enabled, `options.unsafe` **does not** assume or provide any default conditions except the `"default"` condition.

```js
resolve(contents);
//=> Conditions: ["default", "import", "node"]

resolve(contents, { unsafe: true });
//=> Conditions: ["default"]

resolve(contents, { 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:

```js
resolve(contents, {
  unsafe: true,
  conditions: ["require"]
});
//=> Conditions: ["default", "require"]

resolve(contents, {
  unsafe: true,
  conditions: ["browser", "require", "custom123"]
});
//=> Conditions: ["default", "browser", "require", "custom123"]
```


### 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)