| 1 | # resolve.exports [](https://github.com/lukeed/resolve.exports/actions) [](https://codecov.io/gh/lukeed/resolve.exports)
|
|---|
| 2 |
|
|---|
| 3 | > A tiny (813b), correct, general-purpose, and configurable `"exports"` resolver without file-system reliance
|
|---|
| 4 |
|
|---|
| 5 | ***Why?***
|
|---|
| 6 |
|
|---|
| 7 | 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.
|
|---|
| 8 |
|
|---|
| 9 | 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.
|
|---|
| 10 |
|
|---|
| 11 | Let's have nice things.
|
|---|
| 12 |
|
|---|
| 13 | ***TODO***
|
|---|
| 14 |
|
|---|
| 15 | - [x] exports string
|
|---|
| 16 | - [x] exports object (single entry)
|
|---|
| 17 | - [x] exports object (multi entry)
|
|---|
| 18 | - [x] nested / recursive conditions
|
|---|
| 19 | - [x] exports arrayable
|
|---|
| 20 | - [x] directory mapping (`./foobar/` => `/foobar/`)
|
|---|
| 21 | - [x] directory mapping (`./foobar/*` => `./other/*.js`)
|
|---|
| 22 | - [x] directory mapping w/ conditions
|
|---|
| 23 | - [x] directory mapping w/ nested conditions
|
|---|
| 24 | - [x] legacy fields (`main` vs `module` vs ...)
|
|---|
| 25 | - [x] legacy "browser" files object
|
|---|
| 26 |
|
|---|
| 27 | ## Install
|
|---|
| 28 |
|
|---|
| 29 | ```sh
|
|---|
| 30 | $ npm install resolve.exports
|
|---|
| 31 | ```
|
|---|
| 32 |
|
|---|
| 33 | ## Usage
|
|---|
| 34 |
|
|---|
| 35 | > Please see [`/test/`](/test) for examples.
|
|---|
| 36 |
|
|---|
| 37 | ```js
|
|---|
| 38 | import { resolve, legacy } from 'resolve.exports';
|
|---|
| 39 |
|
|---|
| 40 | const contents = {
|
|---|
| 41 | "name": "foobar",
|
|---|
| 42 | "module": "dist/module.mjs",
|
|---|
| 43 | "main": "dist/require.js",
|
|---|
| 44 | "exports": {
|
|---|
| 45 | ".": {
|
|---|
| 46 | "import": "./dist/module.mjs",
|
|---|
| 47 | "require": "./dist/require.js"
|
|---|
| 48 | },
|
|---|
| 49 | "./lite": {
|
|---|
| 50 | "worker": {
|
|---|
| 51 | "browser": "./lite/worker.brower.js",
|
|---|
| 52 | "node": "./lite/worker.node.js"
|
|---|
| 53 | },
|
|---|
| 54 | "import": "./lite/module.mjs",
|
|---|
| 55 | "require": "./lite/require.js"
|
|---|
| 56 | }
|
|---|
| 57 | }
|
|---|
| 58 | };
|
|---|
| 59 |
|
|---|
| 60 | // Assumes `.` as default entry
|
|---|
| 61 | // Assumes `import` as default condition
|
|---|
| 62 | resolve(contents); //=> "./dist/module.mjs"
|
|---|
| 63 |
|
|---|
| 64 | // entry: nullish === "foobar" === "."
|
|---|
| 65 | resolve(contents, 'foobar'); //=> "./dist/module.mjs"
|
|---|
| 66 | resolve(contents, '.'); //=> "./dist/module.mjs"
|
|---|
| 67 |
|
|---|
| 68 | // entry: "foobar/lite" === "./lite"
|
|---|
| 69 | resolve(contents, 'foobar/lite'); //=> "./lite/module.mjs"
|
|---|
| 70 | resolve(contents, './lite'); //=> "./lite/module.mjs"
|
|---|
| 71 |
|
|---|
| 72 | // Assume `require` usage
|
|---|
| 73 | resolve(contents, 'foobar', { require: true }); //=> "./dist/require.js"
|
|---|
| 74 | resolve(contents, './lite', { require: true }); //=> "./lite/require.js"
|
|---|
| 75 |
|
|---|
| 76 | // Throws "Missing <entry> export in <name> package" Error
|
|---|
| 77 | resolve(contents, 'foobar/hello');
|
|---|
| 78 | resolve(contents, './hello/world');
|
|---|
| 79 |
|
|---|
| 80 | // Add custom condition(s)
|
|---|
| 81 | resolve(contents, 'foobar/lite', {
|
|---|
| 82 | conditions: ['worker']
|
|---|
| 83 | }); // => "./lite/worker.node.js"
|
|---|
| 84 |
|
|---|
| 85 | // Toggle "browser" condition
|
|---|
| 86 | resolve(contents, 'foobar/lite', {
|
|---|
| 87 | conditions: ['worker'],
|
|---|
| 88 | browser: true
|
|---|
| 89 | }); // => "./lite/worker.browser.js"
|
|---|
| 90 |
|
|---|
| 91 | // ---
|
|---|
| 92 | // Legacy
|
|---|
| 93 | // ---
|
|---|
| 94 |
|
|---|
| 95 | // prefer "module" > "main" (default)
|
|---|
| 96 | legacy(contents); //=> "dist/module.mjs"
|
|---|
| 97 |
|
|---|
| 98 | // customize fields order
|
|---|
| 99 | legacy(contents, {
|
|---|
| 100 | fields: ['main', 'module']
|
|---|
| 101 | }); //=> "dist/require.js"
|
|---|
| 102 | ```
|
|---|
| 103 |
|
|---|
| 104 | ## API
|
|---|
| 105 |
|
|---|
| 106 | ### resolve(pkg, entry?, options?)
|
|---|
| 107 | Returns: `string` or `undefined`
|
|---|
| 108 |
|
|---|
| 109 | Traverse the `"exports"` within the contents of a `package.json` file. <br>
|
|---|
| 110 | If the contents _does not_ contain an `"exports"` map, then `undefined` will be returned.
|
|---|
| 111 |
|
|---|
| 112 | 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.
|
|---|
| 113 |
|
|---|
| 114 | This function may throw an Error if:
|
|---|
| 115 |
|
|---|
| 116 | * the requested `entry` cannot be resolved (aka, not defined in the `"exports"` map)
|
|---|
| 117 | * an `entry` _was_ resolved but no known conditions were found (see [`options.conditions`](#optionsconditions))
|
|---|
| 118 |
|
|---|
| 119 | #### pkg
|
|---|
| 120 | Type: `object` <br>
|
|---|
| 121 | Required: `true`
|
|---|
| 122 |
|
|---|
| 123 | The `package.json` contents.
|
|---|
| 124 |
|
|---|
| 125 | #### entry
|
|---|
| 126 | Type: `string` <br>
|
|---|
| 127 | Required: `false` <br>
|
|---|
| 128 | Default: `.` (aka, root)
|
|---|
| 129 |
|
|---|
| 130 | The desired target entry, or the original `import` path.
|
|---|
| 131 |
|
|---|
| 132 | When `entry` _is not_ a relative path (aka, does not start with `'.'`), then `entry` is given the `'./'` prefix.
|
|---|
| 133 |
|
|---|
| 134 | When `entry` begins with the package name (determined via the `pkg.name` value), then `entry` is truncated and made relative.
|
|---|
| 135 |
|
|---|
| 136 | When `entry` is already relative, it is accepted as is.
|
|---|
| 137 |
|
|---|
| 138 | ***Examples***
|
|---|
| 139 |
|
|---|
| 140 | Assume we have a module named "foobar" and whose `pkg` contains `"name": "foobar"`.
|
|---|
| 141 |
|
|---|
| 142 | | `entry` value | treated as | reason |
|
|---|
| 143 | |-|-|-|
|
|---|
| 144 | | `null` / `undefined` | `'.'` | default |
|
|---|
| 145 | | `'.'` | `'.'` | value was relative |
|
|---|
| 146 | | `'foobar'` | `'.'` | value was `pkg.name` |
|
|---|
| 147 | | `'foobar/lite'` | `'./lite'` | value had `pkg.name` prefix |
|
|---|
| 148 | | `'./lite'` | `'./lite'` | value was relative |
|
|---|
| 149 | | `'lite'` | `'./lite'` | value was not relative & did not have `pkg.name` prefix |
|
|---|
| 150 |
|
|---|
| 151 |
|
|---|
| 152 | #### options.require
|
|---|
| 153 | Type: `boolean` <br>
|
|---|
| 154 | Default: `false`
|
|---|
| 155 |
|
|---|
| 156 | When truthy, the `"require"` field is added to the list of allowed/known conditions.
|
|---|
| 157 |
|
|---|
| 158 | When falsey, the `"import"` field is added to the list of allowed/known conditions instead.
|
|---|
| 159 |
|
|---|
| 160 | #### options.browser
|
|---|
| 161 | Type: `boolean` <br>
|
|---|
| 162 | Default: `false`
|
|---|
| 163 |
|
|---|
| 164 | When truthy, the `"browser"` field is added to the list of allowed/known conditions.
|
|---|
| 165 |
|
|---|
| 166 | #### options.conditions
|
|---|
| 167 | Type: `string[]` <br>
|
|---|
| 168 | Default: `[]`
|
|---|
| 169 |
|
|---|
| 170 | Provide a list of additional/custom conditions that should be accepted when seen.
|
|---|
| 171 |
|
|---|
| 172 | > **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.
|
|---|
| 173 |
|
|---|
| 174 | For example, you may choose to accept a `"production"` condition in certain environments. Given the following `pkg` content:
|
|---|
| 175 |
|
|---|
| 176 | ```js
|
|---|
| 177 | const contents = {
|
|---|
| 178 | // ...
|
|---|
| 179 | "exports": {
|
|---|
| 180 | "worker": "./index.worker.js",
|
|---|
| 181 | "require": "./index.require.js",
|
|---|
| 182 | "production": "./index.prod.js",
|
|---|
| 183 | "import": "./index.import.mjs",
|
|---|
| 184 | }
|
|---|
| 185 | };
|
|---|
| 186 |
|
|---|
| 187 | resolve(contents, '.');
|
|---|
| 188 | //=> "./index.import.mjs"
|
|---|
| 189 |
|
|---|
| 190 | resolve(contents, '.', {
|
|---|
| 191 | conditions: ['production']
|
|---|
| 192 | }); //=> "./index.prod.js"
|
|---|
| 193 |
|
|---|
| 194 | resolve(contents, '.', {
|
|---|
| 195 | conditions: ['production'],
|
|---|
| 196 | require: true,
|
|---|
| 197 | }); //=> "./index.require.js"
|
|---|
| 198 |
|
|---|
| 199 | resolve(contents, '.', {
|
|---|
| 200 | conditions: ['production', 'worker'],
|
|---|
| 201 | require: true,
|
|---|
| 202 | }); //=> "./index.worker.js"
|
|---|
| 203 |
|
|---|
| 204 | resolve(contents, '.', {
|
|---|
| 205 | conditions: ['production', 'worker']
|
|---|
| 206 | }); //=> "./index.worker.js"
|
|---|
| 207 | ```
|
|---|
| 208 |
|
|---|
| 209 | #### options.unsafe
|
|---|
| 210 | Type: `boolean` <br>
|
|---|
| 211 | Default: `false`
|
|---|
| 212 |
|
|---|
| 213 | > **Important:** You probably do not want this option! <br>It will break out of Node's default resolution conditions.
|
|---|
| 214 |
|
|---|
| 215 | 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.
|
|---|
| 216 |
|
|---|
| 217 | ```js
|
|---|
| 218 | resolve(contents);
|
|---|
| 219 | //=> Conditions: ["default", "import", "node"]
|
|---|
| 220 |
|
|---|
| 221 | resolve(contents, { unsafe: true });
|
|---|
| 222 | //=> Conditions: ["default"]
|
|---|
| 223 |
|
|---|
| 224 | resolve(contents, { unsafe: true, require: true, browser: true });
|
|---|
| 225 | //=> Conditions: ["default"]
|
|---|
| 226 | ```
|
|---|
| 227 |
|
|---|
| 228 | 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:
|
|---|
| 229 |
|
|---|
| 230 | ```js
|
|---|
| 231 | resolve(contents, {
|
|---|
| 232 | unsafe: true,
|
|---|
| 233 | conditions: ["require"]
|
|---|
| 234 | });
|
|---|
| 235 | //=> Conditions: ["default", "require"]
|
|---|
| 236 |
|
|---|
| 237 | resolve(contents, {
|
|---|
| 238 | unsafe: true,
|
|---|
| 239 | conditions: ["browser", "require", "custom123"]
|
|---|
| 240 | });
|
|---|
| 241 | //=> Conditions: ["default", "browser", "require", "custom123"]
|
|---|
| 242 | ```
|
|---|
| 243 |
|
|---|
| 244 |
|
|---|
| 245 | ### legacy(pkg, options?)
|
|---|
| 246 | Returns: `string` or `undefined`
|
|---|
| 247 |
|
|---|
| 248 | 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).
|
|---|
| 249 |
|
|---|
| 250 | You may customize the field priority via [`options.fields`](#optionsfields).
|
|---|
| 251 |
|
|---|
| 252 | When a field is found, its value is returned _as written_. <br>
|
|---|
| 253 | 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.
|
|---|
| 254 |
|
|---|
| 255 | #### options.browser
|
|---|
| 256 | Type: `boolean` or `string` <br>
|
|---|
| 257 | Default: `false`
|
|---|
| 258 |
|
|---|
| 259 | When truthy, ensures that the `'browser'` field is part of the acceptable `fields` list.
|
|---|
| 260 |
|
|---|
| 261 | > **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.
|
|---|
| 262 |
|
|---|
| 263 | When `true` and `"browser"` is an object, then `legacy()` will return the the entire `"browser"` object.
|
|---|
| 264 |
|
|---|
| 265 | 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:
|
|---|
| 266 |
|
|---|
| 267 | * `false` – if the package author decided a file should be ignored; or
|
|---|
| 268 | * your `options.browser` string value – but made relative, if not already
|
|---|
| 269 |
|
|---|
| 270 | > See the [`"browser" field specification](https://github.com/defunctzombie/package-browser-field-spec) for more information.
|
|---|
| 271 |
|
|---|
| 272 | #### options.fields
|
|---|
| 273 | Type: `string[]` <br>
|
|---|
| 274 | Default: `['module', 'main']`
|
|---|
| 275 |
|
|---|
| 276 | 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.
|
|---|
| 277 |
|
|---|
| 278 | 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.
|
|---|
| 279 |
|
|---|
| 280 | ```js
|
|---|
| 281 | const contents = {
|
|---|
| 282 | "name": "...",
|
|---|
| 283 | "worker": "worker.js",
|
|---|
| 284 | "module": "module.mjs",
|
|---|
| 285 | "browser": "browser.js",
|
|---|
| 286 | "main": "main.js",
|
|---|
| 287 | }
|
|---|
| 288 |
|
|---|
| 289 | legacy(contents);
|
|---|
| 290 | // fields = [module, main]
|
|---|
| 291 | //=> "module.mjs"
|
|---|
| 292 |
|
|---|
| 293 | legacy(contents, { browser: true });
|
|---|
| 294 | // fields = [browser, module, main]
|
|---|
| 295 | //=> "browser.mjs"
|
|---|
| 296 |
|
|---|
| 297 | legacy(contents, {
|
|---|
| 298 | fields: ['missing', 'worker', 'module', 'main']
|
|---|
| 299 | });
|
|---|
| 300 | // fields = [missing, worker, module, main]
|
|---|
| 301 | //=> "worker.js"
|
|---|
| 302 |
|
|---|
| 303 | legacy(contents, {
|
|---|
| 304 | fields: ['missing', 'worker', 'module', 'main'],
|
|---|
| 305 | browser: true,
|
|---|
| 306 | });
|
|---|
| 307 | // fields = [browser, missing, worker, module, main]
|
|---|
| 308 | //=> "browser.js"
|
|---|
| 309 |
|
|---|
| 310 | legacy(contents, {
|
|---|
| 311 | fields: ['module', 'browser', 'main'],
|
|---|
| 312 | browser: true,
|
|---|
| 313 | });
|
|---|
| 314 | // fields = [module, browser, main]
|
|---|
| 315 | //=> "module.mjs"
|
|---|
| 316 | ```
|
|---|
| 317 |
|
|---|
| 318 | ## License
|
|---|
| 319 |
|
|---|
| 320 | MIT © [Luke Edwards](https://lukeed.com)
|
|---|