├── .github ├── FUNDING.yml └── workflows │ └── ci.yml ├── .gitignore ├── .editorconfig ├── tsconfig.json ├── src ├── index.ts ├── legacy.ts └── utils.ts ├── license ├── package.json ├── index.d.ts ├── test ├── legacy.ts ├── utils.ts └── index.ts └── readme.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: lukeed 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | *-lock.* 4 | *.lock 5 | *.log 6 | 7 | /dist 8 | /lite 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_size = 2 6 | indent_style = tab 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.{json,yml,md}] 13 | indent_style = space 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "target": "esnext", 5 | "strictBindCallApply": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "moduleResolution": "nodenext", 8 | "strictFunctionTypes": true, 9 | "strictNullChecks": true, 10 | "noImplicitThis": true, 11 | "noImplicitAny": true, 12 | "alwaysStrict": true, 13 | "module": "esnext", 14 | "noEmit": true, 15 | "paths": { 16 | "resolve.exports": ["./index.d.ts"] 17 | } 18 | }, 19 | "include": [ 20 | "src", "test" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { toEntry, walk } from './utils'; 2 | import type * as t from 'resolve.exports'; 3 | 4 | export { legacy } from './legacy'; 5 | 6 | export function exports(pkg: t.Package, input?: string, options?: t.Options): string[] | void { 7 | let map = pkg.exports, 8 | k: string; 9 | 10 | if (map) { 11 | if (typeof map === 'string') { 12 | map = { '.': map }; 13 | } else for (k in map) { 14 | // convert {conditions} to "."={condtions} 15 | if (k[0] !== '.') map = { '.': map }; 16 | break; 17 | } 18 | 19 | return walk(pkg.name, map, input||'.', options); 20 | } 21 | } 22 | 23 | export function imports(pkg: t.Package, input: string, options?: t.Options): string[] | void { 24 | if (pkg.imports) return walk(pkg.name, pkg.imports, input, options); 25 | } 26 | 27 | export function resolve(pkg: t.Package, input?: string, options?: t.Options): string[] | void { 28 | // let entry = input && input !== '.' 29 | // ? toEntry(pkg.name, input) 30 | // : '.'; 31 | input = toEntry(pkg.name, input || '.'); 32 | return input[0] === '#' 33 | ? imports(pkg, input, options) 34 | : exports(pkg, input, options); 35 | } 36 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Luke Edwards (lukeed.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.3", 3 | "name": "resolve.exports", 4 | "repository": "lukeed/resolve.exports", 5 | "description": "A tiny (952b), correct, general-purpose, and configurable \"exports\" and \"imports\" resolver without file-system reliance", 6 | "module": "dist/index.mjs", 7 | "main": "dist/index.js", 8 | "types": "index.d.ts", 9 | "license": "MIT", 10 | "author": { 11 | "name": "Luke Edwards", 12 | "email": "luke.edwards05@gmail.com", 13 | "url": "https://lukeed.com" 14 | }, 15 | "engines": { 16 | "node": ">=10" 17 | }, 18 | "scripts": { 19 | "build": "bundt -m", 20 | "types": "tsc --noEmit", 21 | "test": "uvu -r tsm test" 22 | }, 23 | "files": [ 24 | "*.d.ts", 25 | "dist" 26 | ], 27 | "exports": { 28 | ".": { 29 | "types": "./index.d.ts", 30 | "import": "./dist/index.mjs", 31 | "require": "./dist/index.js" 32 | }, 33 | "./package.json": "./package.json" 34 | }, 35 | "keywords": [ 36 | "esm", 37 | "exports", 38 | "esmodules", 39 | "fields", 40 | "modules", 41 | "resolution", 42 | "resolve" 43 | ], 44 | "devDependencies": { 45 | "bundt": "next", 46 | "tsm": "2.3.0", 47 | "typescript": "4.9.4", 48 | "uvu": "0.5.4" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/legacy.ts: -------------------------------------------------------------------------------- 1 | import * as $ from './utils'; 2 | import type * as t from 'resolve.exports'; 3 | 4 | type LegacyOptions = { 5 | fields?: string[]; 6 | browser?: string | boolean; 7 | } 8 | 9 | type BrowserObject = { 10 | [file: string]: string | undefined; 11 | } 12 | 13 | export function legacy(pkg: t.Package, options: LegacyOptions = {}): t.Path | t.Browser | void { 14 | let i=0, 15 | value: string | t.Browser | undefined, 16 | browser = options.browser, 17 | fields = options.fields || ['module', 'main'], 18 | isSTRING = typeof browser == 'string'; 19 | 20 | if (browser && !fields.includes('browser')) { 21 | fields.unshift('browser'); 22 | // "module-a" -> "module-a" 23 | // "./path/file.js" -> "./path/file.js" 24 | // "foobar/path/file.js" -> "./path/file.js" 25 | if (isSTRING) browser = $.toEntry(pkg.name, browser as string, true); 26 | } 27 | 28 | for (; i < fields.length; i++) { 29 | if (value = pkg[fields[i]]) { 30 | if (typeof value == 'string') { 31 | // 32 | } else if (typeof value == 'object' && fields[i] == 'browser') { 33 | if (isSTRING) { 34 | value = (value as BrowserObject)[browser as string]; 35 | if (value == null) return browser as string; 36 | } 37 | } else { 38 | continue; 39 | } 40 | 41 | return typeof value == 'string' 42 | ? ('./' + value.replace(/^\.?\//, '')) as t.Path 43 | : value; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - 'bench/**' 7 | - '*.md' 8 | branches: 9 | - '**' 10 | tags-ignore: 11 | - '**' 12 | pull_request: 13 | paths-ignore: 14 | - 'bench/**' 15 | - '*.md' 16 | branches: 17 | - master 18 | 19 | jobs: 20 | test: 21 | name: Node.js v${{ matrix.nodejs }} 22 | runs-on: ubuntu-latest 23 | strategy: 24 | matrix: 25 | # Node 10.x not supported by tsm & bundt 26 | nodejs: [12, 14, 16, 18] 27 | steps: 28 | - uses: actions/checkout@v2 29 | - uses: actions/setup-node@v1 30 | with: 31 | node-version: ${{ matrix.nodejs }} 32 | 33 | - name: Install 34 | run: npm install 35 | 36 | - name: Type Check 37 | run: npm run types 38 | 39 | - name: Test 40 | if: matrix.nodejs < 18 41 | run: npm test 42 | 43 | - name: Test w/ Coverage 44 | if: matrix.nodejs >= 18 45 | run: | 46 | npm install -g nyc 47 | nyc --include=src npm test 48 | 49 | - name: Compile 50 | if: matrix.nodejs >= 18 51 | run: npm run build 52 | 53 | - name: Report 54 | if: matrix.nodejs >= 18 55 | run: | 56 | nyc report --reporter=text-lcov > coverage.lcov 57 | bash <(curl -s https://codecov.io/bash) 58 | env: 59 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 60 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | export type Options = { 2 | /** 3 | * When true, adds the "browser" conditions. 4 | * Otherwise the "node" condition is enabled. 5 | * @default false 6 | */ 7 | browser?: boolean; 8 | /** 9 | * Any custom conditions to match. 10 | * @note Array order does not matter. Priority is determined by the key-order of conditions defined within a package's imports/exports mapping. 11 | * @default [] 12 | */ 13 | conditions?: readonly string[]; 14 | /** 15 | * When true, adds the "require" condition. 16 | * Otherwise the "import" condition is enabled. 17 | * @default false 18 | */ 19 | require?: boolean; 20 | /** 21 | * Prevents "require", "import", "browser", and/or "node" conditions from being added automatically. 22 | * When enabled, only `options.conditions` are added alongside the "default" condition. 23 | * @important Enabling this deviates from Node.js default behavior. 24 | * @default false 25 | */ 26 | unsafe?: boolean; 27 | } 28 | 29 | export function resolve(pkg: T, entry?: string, options?: Options): Imports.Output | Exports.Output | void; 30 | export function imports(pkg: T, entry?: string, options?: Options): Imports.Output | void; 31 | export function exports(pkg: T, target: string, options?: Options): Exports.Output | void; 32 | 33 | export function legacy(pkg: T, options: { browser: true, fields?: readonly string[] }): Browser | void; 34 | export function legacy(pkg: T, options: { browser: string, fields?: readonly string[] }): string | false | void; 35 | export function legacy(pkg: T, options: { browser: false, fields?: readonly string[] }): string | void; 36 | export function legacy(pkg: T, options?: { 37 | browser?: boolean | string; 38 | fields?: readonly string[]; 39 | }): Browser | string; 40 | 41 | // --- 42 | 43 | /** 44 | * A resolve condition 45 | * @example "node", "default", "production" 46 | */ 47 | export type Condition = string; 48 | 49 | /** An internal file path */ 50 | export type Path = `./${string}`; 51 | 52 | export type Imports = { 53 | [entry: Imports.Entry]: Imports.Value; 54 | } 55 | 56 | export namespace Imports { 57 | export type Entry = `#${string}`; 58 | 59 | type External = string; 60 | 61 | /** strings are dependency names OR internal paths */ 62 | export type Value = External | Path | null | { 63 | [c: Condition]: Value; 64 | } | Value[]; 65 | 66 | 67 | export type Output = Array; 68 | } 69 | 70 | export type Exports = Path | { 71 | [path: Exports.Entry]: Exports.Value; 72 | [cond: Condition]: Exports.Value; 73 | } 74 | 75 | export namespace Exports { 76 | /** Allows "." and "./{name}" */ 77 | export type Entry = `.${string}`; 78 | 79 | /** strings must be internal paths */ 80 | export type Value = Path | null | { 81 | [c: Condition]: Value; 82 | } | Value[]; 83 | 84 | export type Output = Path[]; 85 | } 86 | 87 | export type Package = { 88 | name: string; 89 | version?: string; 90 | module?: string; 91 | main?: string; 92 | imports?: Imports; 93 | exports?: Exports; 94 | browser?: Browser; 95 | [key: string]: any; 96 | } 97 | 98 | export type Browser = string[] | string | { 99 | [file: Path | string]: string | false; 100 | } 101 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import type * as t from 'resolve.exports'; 2 | 3 | export type Entry = t.Exports.Entry | t.Imports.Entry; 4 | export type Value = t.Exports.Value | t.Imports.Value; 5 | export type Mapping = Record; 6 | 7 | export function throws(name: string, entry: Entry, condition?: number): never { 8 | throw new Error( 9 | condition 10 | ? `No known conditions for "${entry}" specifier in "${name}" package` 11 | : `Missing "${entry}" specifier in "${name}" package` 12 | ); 13 | } 14 | 15 | export function conditions(options: t.Options): Set { 16 | let out = new Set([ 'default', ...options.conditions || [] ]); 17 | options.unsafe || out.add(options.require ? 'require' : 'import'); 18 | options.unsafe || out.add(options.browser ? 'browser' : 'node'); 19 | return out; 20 | } 21 | 22 | export function walk(name: string, mapping: Mapping, input: string, options?: t.Options): string[] { 23 | let entry = toEntry(name, input); 24 | let c = conditions(options || {}); 25 | 26 | let m: Value|void = mapping[entry]; 27 | let v: string[]|void, replace: string|void; 28 | 29 | if (m === void 0) { 30 | // loop for longest key match 31 | let match: RegExpExecArray|null; 32 | let longest: Entry|undefined; 33 | let tmp: string|number; 34 | let key: Entry; 35 | 36 | for (key in mapping) { 37 | if (longest && key.length < longest.length) { 38 | // do not allow "./" to match if already matched "./foo*" key 39 | } else if (key[key.length - 1] === '/' && entry.startsWith(key)) { 40 | replace = entry.substring(key.length); 41 | longest = key; 42 | } else if (key.length > 1) { 43 | tmp = key.indexOf('*', 1); 44 | 45 | if (!!~tmp) { 46 | match = RegExp( 47 | '^' + key.substring(0, tmp) + '(.*)' + key.substring(1+tmp) + '$' 48 | ).exec(entry); 49 | 50 | if (match && match[1]) { 51 | replace = match[1]; 52 | longest = key; 53 | } 54 | } 55 | } 56 | } 57 | 58 | m = mapping[longest!]; 59 | } 60 | 61 | if (!m) { 62 | // missing export 63 | throws(name, entry); 64 | } 65 | 66 | v = loop(m, c); 67 | 68 | // unknown condition(s) 69 | if (!v) throws(name, entry, 1); 70 | if (replace) injects(v, replace); 71 | 72 | return v; 73 | } 74 | 75 | /** @note: mutates! */ 76 | export function injects(items: string[], value: string): void { 77 | let i=0, len=items.length, tmp: string; 78 | let rgx1=/[*]/g, rgx2 = /[/]$/; 79 | 80 | for (; i < len; i++) { 81 | items[i] = rgx1.test(tmp = items[i]) 82 | ? tmp.replace(rgx1, value) 83 | : rgx2.test(tmp) 84 | ? (tmp+value) 85 | : tmp; 86 | } 87 | } 88 | 89 | /** 90 | * @param name package name 91 | * @param ident entry identifier 92 | * @param externals allow non-path (external) result 93 | * @see https://esbench.com/bench/59fa3e6799634800a0349382 94 | */ 95 | export function toEntry(name: string, ident: string, externals?: false): Entry; 96 | export function toEntry(name: string, ident: string, externals: true): Entry | string; 97 | export function toEntry(name: string, ident: string, externals?: boolean): Entry | string { 98 | if (name === ident || ident === '.') return '.'; 99 | 100 | let root = name+'/', len = root.length; 101 | let bool = ident.slice(0, len) === root; 102 | 103 | let output = bool ? ident.slice(len) : ident; 104 | if (output[0] === '#') return output as t.Imports.Entry; 105 | 106 | return (bool || !externals) 107 | ? (output.slice(0,2) === './' ? output : './' + output) as t.Path 108 | : output as string | t.Exports.Entry; 109 | } 110 | 111 | export function loop(m: Value, keys: Set, result?: Set): string[] | void { 112 | if (m) { 113 | if (typeof m === 'string') { 114 | if (result) result.add(m); 115 | return [m]; 116 | } 117 | 118 | let 119 | idx: number | string, 120 | arr: Set; 121 | 122 | if (Array.isArray(m)) { 123 | arr = result || new Set; 124 | 125 | for (idx=0; idx < m.length; idx++) { 126 | loop(m[idx], keys, arr); 127 | } 128 | 129 | // return if initialized set 130 | if (!result && arr.size) { 131 | return [...arr]; 132 | } 133 | } else for (idx in m) { 134 | if (keys.has(idx)) { 135 | return loop(m[idx], keys, result); 136 | } 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /test/legacy.ts: -------------------------------------------------------------------------------- 1 | import * as uvu from 'uvu'; 2 | import * as assert from 'uvu/assert'; 3 | import { legacy } from '../src/legacy'; 4 | 5 | import type { Package } from 'resolve.exports'; 6 | 7 | function describe( 8 | name: string, 9 | cb: (it: uvu.Test) => void 10 | ) { 11 | let t = uvu.suite(name); 12 | cb(t); 13 | t.run(); 14 | } 15 | 16 | describe('lib.legacy', it => { 17 | it('should be a function', () => { 18 | assert.type(legacy, 'function'); 19 | }); 20 | 21 | it('should prefer "module" > "main" entry', () => { 22 | let pkg: Package = { 23 | "name": "foobar", 24 | "module": "build/module.js", 25 | "main": "build/main.js", 26 | }; 27 | 28 | let output = legacy(pkg); 29 | assert.is(output, './build/module.js'); 30 | }); 31 | 32 | it('should read "main" field', () => { 33 | let pkg: Package = { 34 | "name": "foobar", 35 | "main": "build/main.js", 36 | }; 37 | 38 | let output = legacy(pkg); 39 | assert.is(output, './build/main.js'); 40 | }); 41 | 42 | it('should return nothing when no fields', () => { 43 | let pkg: Package = { 44 | "name": "foobar" 45 | }; 46 | 47 | let output = legacy(pkg); 48 | assert.is(output, undefined); 49 | }); 50 | 51 | it('should ignore boolean-type field values', () => { 52 | let pkg = { 53 | "module": true, 54 | "main": "main.js" 55 | }; 56 | 57 | let output = legacy(pkg as any); 58 | assert.is(output, './main.js'); 59 | }); 60 | }); 61 | 62 | describe('options.fields', it => { 63 | let pkg: Package = { 64 | "name": "foobar", 65 | "module": "build/module.js", 66 | "browser": "build/browser.js", 67 | "custom": "build/custom.js", 68 | "main": "build/main.js", 69 | }; 70 | 71 | it('should customize field search order', () => { 72 | let output = legacy(pkg); 73 | assert.is(output, './build/module.js', 'default: module'); 74 | 75 | output = legacy(pkg, { fields: ['main'] }); 76 | assert.is(output, './build/main.js', 'custom: main only'); 77 | 78 | output = legacy(pkg, { fields: ['custom', 'main', 'module'] }); 79 | assert.is(output, './build/custom.js', 'custom: custom > main > module'); 80 | }); 81 | 82 | it('should return first *resolved* field', () => { 83 | let output = legacy(pkg, { 84 | fields: ['howdy', 'partner', 'hello', 'world', 'main'] 85 | }); 86 | 87 | assert.is(output, './build/main.js'); 88 | }); 89 | }); 90 | 91 | describe('options.browser', it => { 92 | let pkg: Package = { 93 | "name": "foobar", 94 | "module": "build/module.js", 95 | "browser": "build/browser.js", 96 | "unpkg": "build/unpkg.js", 97 | "main": "build/main.js", 98 | }; 99 | 100 | it('should prioritize "browser" field when defined', () => { 101 | let output = legacy(pkg); 102 | assert.is(output, './build/module.js'); 103 | 104 | output = legacy(pkg, { browser: true }); 105 | assert.is(output, './build/browser.js'); 106 | }); 107 | 108 | it('should respect existing "browser" order in custom fields', () => { 109 | let output = legacy(pkg, { 110 | fields: ['main', 'browser'], 111 | browser: true, 112 | }); 113 | 114 | assert.is(output, './build/main.js'); 115 | }); 116 | 117 | // https://github.com/defunctzombie/package-browser-field-spec 118 | it('should resolve object format', () => { 119 | let pkg: Package = { 120 | "name": "foobar", 121 | "browser": { 122 | "module-a": "./shims/module-a.js", 123 | "./server/only.js": "./shims/client-only.js" 124 | } 125 | }; 126 | 127 | assert.is( 128 | legacy(pkg, { browser: 'module-a' }), 129 | './shims/module-a.js' 130 | ); 131 | 132 | assert.is( 133 | legacy(pkg, { browser: './server/only.js' }), 134 | './shims/client-only.js' 135 | ); 136 | 137 | assert.is( 138 | legacy(pkg, { browser: 'foobar/server/only.js' }), 139 | './shims/client-only.js' 140 | ); 141 | }); 142 | 143 | it('should allow object format to "ignore" modules/files :: string', () => { 144 | let pkg: Package = { 145 | "name": "foobar", 146 | "browser": { 147 | "module-a": false, 148 | "./foo.js": false, 149 | } 150 | }; 151 | 152 | assert.is( 153 | legacy(pkg, { browser: 'module-a' }), 154 | false 155 | ); 156 | 157 | assert.is( 158 | legacy(pkg, { browser: './foo.js' }), 159 | false 160 | ); 161 | 162 | assert.is( 163 | legacy(pkg, { browser: 'foobar/foo.js' }), 164 | false 165 | ); 166 | }); 167 | 168 | it('should return the `browser` string (entry) if no custom mapping :: string', () => { 169 | let pkg: Package = { 170 | "name": "foobar", 171 | "browser": { 172 | // 173 | } 174 | }; 175 | 176 | assert.is( 177 | legacy(pkg, { 178 | browser: './hello.js' 179 | }), 180 | './hello.js' 181 | ); 182 | 183 | assert.is( 184 | legacy(pkg, { 185 | browser: 'foobar/hello.js' 186 | }), 187 | './hello.js' 188 | ); 189 | }); 190 | 191 | it('should return the full "browser" object :: true', () => { 192 | let pkg: Package = { 193 | "name": "foobar", 194 | "browser": { 195 | "./other.js": "./world.js" 196 | } 197 | }; 198 | 199 | let output = legacy(pkg, { 200 | browser: true 201 | }); 202 | 203 | assert.equal(output, pkg.browser); 204 | }); 205 | 206 | it('still ensures string output is made relative', () => { 207 | let pkg: Package = { 208 | "name": "foobar", 209 | "browser": { 210 | "./foo.js": "bar.js", 211 | } 212 | } as any; 213 | 214 | assert.is( 215 | legacy(pkg, { 216 | browser: './foo.js' 217 | }), 218 | './bar.js' 219 | ); 220 | 221 | assert.is( 222 | legacy(pkg, { 223 | browser: 'foobar/foo.js' 224 | }), 225 | './bar.js' 226 | ); 227 | }); 228 | }); 229 | -------------------------------------------------------------------------------- /test/utils.ts: -------------------------------------------------------------------------------- 1 | import * as uvu from 'uvu'; 2 | import * as assert from 'uvu/assert'; 3 | import * as $ from '../src/utils'; 4 | 5 | import type * as t from 'resolve.exports'; 6 | 7 | function describe( 8 | name: string, 9 | cb: (it: uvu.Test) => void 10 | ) { 11 | let t = uvu.suite(name); 12 | cb(t); 13 | t.run(); 14 | } 15 | 16 | describe('$.conditions', it => { 17 | const EMPTY = {}; 18 | 19 | function run(o?: t.Options): string[] { 20 | return [...$.conditions(o||{})]; 21 | } 22 | 23 | it('should be a function', () => { 24 | assert.type($.conditions, 'function'); 25 | }); 26 | 27 | it('should return `Set` instance', () => { 28 | let output = $.conditions(EMPTY); 29 | assert.instance(output, Set); 30 | }); 31 | 32 | it('default conditions', () => { 33 | assert.equal( 34 | [ ...$.conditions(EMPTY) ], 35 | ['default', 'import', 'node'] 36 | ); 37 | }); 38 | 39 | it('default conditions :: unsafe', () => { 40 | assert.equal( 41 | run({ unsafe: true }), 42 | ['default'] 43 | ); 44 | }); 45 | 46 | it('option.browser', () => { 47 | assert.equal( 48 | run({ browser: true }), 49 | ['default', 'import', 'browser'] 50 | ); 51 | }); 52 | 53 | // unsafe ignores all but conditions 54 | it('option.browser :: unsafe', () => { 55 | let output = run({ 56 | browser: true, 57 | unsafe: true, 58 | }); 59 | assert.equal(output, ['default']); 60 | }); 61 | 62 | it('option.require', () => { 63 | assert.equal( 64 | run({ require: true }), 65 | ['default', 'require', 'node'] 66 | ); 67 | }); 68 | 69 | // unsafe ignores all but conditions 70 | it('option.require :: unsafe', () => { 71 | let output = run({ 72 | require: true, 73 | unsafe: true, 74 | }); 75 | assert.equal(output, ['default']); 76 | }); 77 | 78 | it('option.conditions', () => { 79 | let output = run({ conditions: ['foo', 'bar'] }); 80 | assert.equal(output, ['default', 'foo', 'bar', 'import', 'node']); 81 | }); 82 | 83 | it('option.conditions :: order', () => { 84 | let output = run({ conditions: ['node', 'import', 'foobar'] }); 85 | assert.equal(output, ['default', 'node', 'import', 'foobar']); 86 | }); 87 | 88 | it('option.conditions :: unsafe', () => { 89 | let output = run({ unsafe: true, conditions: ['foo', 'bar'] }); 90 | assert.equal(output, ['default', 'foo', 'bar']); 91 | }); 92 | 93 | it('option.conditions :: browser', () => { 94 | let output = run({ browser: true, conditions: ['foo', 'bar'] }); 95 | assert.equal(output, ['default', 'foo', 'bar', 'import', 'browser']); 96 | }); 97 | 98 | it('option.conditions :: browser :: order', () => { 99 | let output = run({ browser: true, conditions: ['browser', 'foobar'] }); 100 | assert.equal(output, ['default', 'browser', 'foobar', 'import']); 101 | }); 102 | 103 | it('option.conditions :: require', () => { 104 | let output = run({ require: true, conditions: ['foo', 'bar'] }); 105 | assert.equal(output, ['default', 'foo', 'bar', 'require', 'node']); 106 | }); 107 | 108 | it('option.conditions :: require :: order', () => { 109 | let output = run({ require: true, conditions: ['require', 'foobar'] }); 110 | assert.equal(output, ['default', 'require', 'foobar', 'node']); 111 | }); 112 | }); 113 | 114 | describe('$.toEntry', it => { 115 | const PKG = 'PACKAGE'; 116 | const EXTERNAL = 'EXTERNAL'; 117 | 118 | function run(input: string, expect: string, externals?: boolean) { 119 | // overloading not working -,- 120 | let output = externals ? $.toEntry(PKG, input, true) : $.toEntry(PKG, input); 121 | let msg = `"${input}" -> "${expect}"` + (externals ? ' (externals)' : ''); 122 | assert.is(output, expect, msg); 123 | } 124 | 125 | it('should be a function', () => { 126 | assert.type($.toEntry, 'function'); 127 | }); 128 | 129 | it('PKG', () => { 130 | run(PKG, '.'); 131 | run(PKG, '.', true); 132 | }); 133 | 134 | it('.', () => { 135 | run('.', '.'); 136 | run('.', '.', true); 137 | }); 138 | 139 | it('./', () => { 140 | run('./', './'); 141 | run('./', './', true); 142 | }); 143 | 144 | it('#inner', () => { 145 | run('#inner', '#inner'); 146 | run('#inner', '#inner', true); 147 | }); 148 | 149 | it('./foo', () => { 150 | run('./foo', './foo'); 151 | run('./foo', './foo', true); 152 | }); 153 | 154 | it('foo', () => { 155 | run('foo', './foo'); // forces path by default 156 | run('foo', 'foo', true); 157 | }); 158 | 159 | it('.ini', () => { 160 | run('.ini', './.ini'); // forces path by default 161 | run('.ini', '.ini', true); 162 | }); 163 | 164 | // handle "import 'lib/lib';" case 165 | it('./PKG', () => { 166 | let input = './' + PKG; 167 | run(input, input); 168 | run(input, input, true); 169 | }); 170 | 171 | it('PKG/subpath', () => { 172 | let input = PKG + '/other'; 173 | run(input, './other'); 174 | run(input, './other', true); 175 | }); 176 | 177 | it('PKG/#inner', () => { 178 | let input = PKG + '/#inner'; 179 | run(input, '#inner'); 180 | run(input, '#inner', true); 181 | }); 182 | 183 | it('PKG/.ini', () => { 184 | let input = PKG + '/.ini'; 185 | run(input, './.ini'); 186 | run(input, './.ini', true); 187 | }); 188 | 189 | it('EXTERNAL', () => { 190 | run(EXTERNAL, './'+EXTERNAL); // forces path by default 191 | run(EXTERNAL, EXTERNAL, true); 192 | }); 193 | }); 194 | 195 | describe('$.injects', it => { 196 | function run(value: string, input: T, expect: T) { 197 | let output = $.injects(input, value); 198 | assert.is(output, undefined); 199 | assert.equal(input, expect); 200 | } 201 | 202 | it('should be a function', () => { 203 | assert.type($.injects, 'function'); 204 | }); 205 | 206 | it('should replace "*" character', () => { 207 | run('bar', ['./foo*.jpg'], ['./foobar.jpg']); 208 | }); 209 | 210 | it('should replace multiple "*" characters w/ same value', () => { 211 | run('bar', ['./*/foo-*.jpg'], ['./bar/foo-bar.jpg']); 212 | }); 213 | 214 | // for the "./features/" => "./src/features/" scenario 215 | it('should append `value` if missing "*" character', () => { 216 | run('app.js', ['./src/features/'], ['./src/features/app.js']); 217 | }); 218 | 219 | it('should handle mixed array input', () => { 220 | run('xyz', 221 | ['./foo/', './esm/*.mjs', './build/*/index-*.js'], 222 | ['./foo/xyz', './esm/xyz.mjs', './build/xyz/index-xyz.js'], 223 | ); 224 | }); 225 | }); 226 | 227 | describe('$.loop', it => { 228 | const FILE = './file.js'; 229 | const DEFAULT = './foobar.js'; 230 | 231 | type Expect = string | string[] | null | undefined; 232 | function run(expect: Expect, map: t.Exports.Value, conditions?: string[]) { 233 | let output = $.loop(map, new Set([ 'default', ...conditions||[] ])); 234 | if (typeof expect == 'string') { 235 | assert.ok(Array.isArray(output)); 236 | assert.is(output[0], expect); 237 | assert.is(output.length, 1); 238 | } else { 239 | // Array, null, undefined 240 | assert.equal(output, expect); 241 | } 242 | } 243 | 244 | it('should be a function', () => { 245 | assert.type($.loop, 'function'); 246 | }); 247 | 248 | it('string', () => { 249 | run('./foo.mjs', './foo.mjs'); 250 | // @ts-expect-error 251 | run('.', '.'); 252 | }); 253 | 254 | it('empties', () => { 255 | // @ts-expect-error 256 | run(undefined, ''); 257 | run(undefined, null); 258 | run(undefined, []); 259 | run(undefined, {}); 260 | }); 261 | 262 | it('{ default }', () => { 263 | run(FILE, { 264 | default: FILE, 265 | }); 266 | 267 | run(FILE, { 268 | other: './unknown.js', 269 | default: FILE, 270 | }); 271 | 272 | run(undefined, { 273 | other: './unknown.js', 274 | }); 275 | 276 | run(FILE, { 277 | foo: './foo.js', 278 | default: { 279 | bar: './bar.js', 280 | default: { 281 | baz: './baz.js', 282 | default: FILE, 283 | } 284 | } 285 | }); 286 | }); 287 | 288 | it('{ custom }', () => { 289 | let conditions = ['custom']; 290 | 291 | run(DEFAULT, { 292 | default: DEFAULT, 293 | custom: FILE, 294 | }, conditions); 295 | 296 | run(FILE, { 297 | custom: FILE, 298 | default: DEFAULT, 299 | }, conditions); 300 | 301 | run(undefined, { 302 | foo: './foo.js', 303 | bar: './bar.js', 304 | }, conditions); 305 | 306 | run(FILE, { 307 | foo: './foo.js', 308 | custom: { 309 | default: { 310 | custom: FILE, 311 | default: DEFAULT, 312 | } 313 | }, 314 | default: { 315 | custom: './bar.js' 316 | } 317 | }, conditions); 318 | }); 319 | 320 | it('[ string ]', () => { 321 | run( 322 | [DEFAULT, FILE], 323 | [DEFAULT, FILE] 324 | ); 325 | 326 | run(undefined, [ 327 | null, 328 | ]); 329 | 330 | run( 331 | [DEFAULT, FILE], 332 | [null, DEFAULT, FILE] 333 | ); 334 | 335 | run( 336 | [DEFAULT, FILE], 337 | [DEFAULT, null, FILE] 338 | ); 339 | }); 340 | 341 | it('[{ default }]', () => { 342 | run([DEFAULT, FILE], [ 343 | { 344 | default: DEFAULT, 345 | }, 346 | FILE 347 | ]); 348 | 349 | run([FILE, DEFAULT], [ 350 | FILE, 351 | null, 352 | { 353 | default: DEFAULT, 354 | }, 355 | ]); 356 | 357 | run([DEFAULT, FILE], [ 358 | { 359 | default: { 360 | default: { 361 | default: DEFAULT, 362 | } 363 | } 364 | }, 365 | null, 366 | FILE 367 | ]); 368 | 369 | run([DEFAULT, FILE, './foo.js'], [ 370 | { 371 | default: { 372 | default: DEFAULT, 373 | } 374 | }, 375 | null, 376 | { 377 | default: { 378 | default: DEFAULT, 379 | } 380 | }, 381 | FILE, 382 | { 383 | default: './foo.js' 384 | } 385 | ]); 386 | }); 387 | 388 | it('{ [mixed] }', () => { 389 | run([DEFAULT, FILE], { 390 | default: [DEFAULT, FILE] 391 | }); 392 | 393 | run([DEFAULT, FILE], { 394 | default: [null, DEFAULT, FILE] 395 | }); 396 | 397 | run([DEFAULT, FILE], { 398 | default: [null, { 399 | default: DEFAULT 400 | }, FILE] 401 | }); 402 | 403 | run([FILE, DEFAULT], { 404 | default: { 405 | custom: [{ 406 | default: [FILE, FILE, null, DEFAULT] 407 | }, null, DEFAULT, FILE] 408 | } 409 | }, ['custom']); 410 | 411 | run([DEFAULT, FILE], { 412 | default: { 413 | custom: [{ 414 | custom: [DEFAULT, null], 415 | default: [FILE, FILE, null, DEFAULT] 416 | }, null, DEFAULT, FILE] 417 | } 418 | }, ['custom']); 419 | }); 420 | }); 421 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # resolve.exports [![CI](https://github.com/lukeed/resolve.exports/workflows/CI/badge.svg)](https://github.com/lukeed/resolve.exports/actions) [![licenses](https://licenses.dev/b/npm/resolve.exports)](https://licenses.dev/npm/resolve.exports) [![codecov](https://codecov.io/gh/lukeed/resolve.exports/branch/master/graph/badge.svg?token=4P7d4Omw2h)](https://codecov.io/gh/lukeed/resolve.exports) 2 | 3 | > A tiny (952b), correct, general-purpose, and configurable `"exports"` and `"imports"` 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 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. 10 | 11 | Let's have nice things. 12 | 13 | ## Install 14 | 15 | ```sh 16 | $ npm install resolve.exports 17 | ``` 18 | 19 | ## Usage 20 | 21 | > Please see [`/test/`](/test) for examples. 22 | 23 | ```js 24 | import * as resolve from 'resolve.exports'; 25 | 26 | // package.json contents 27 | const pkg = { 28 | "name": "foobar", 29 | "module": "dist/module.mjs", 30 | "main": "dist/require.js", 31 | "imports": { 32 | "#hash": { 33 | "import": { 34 | "browser": "./hash/web.mjs", 35 | "node": "./hash/node.mjs", 36 | }, 37 | "default": "./hash/detect.js" 38 | } 39 | }, 40 | "exports": { 41 | ".": { 42 | "import": "./dist/module.mjs", 43 | "require": "./dist/require.js" 44 | }, 45 | "./lite": { 46 | "worker": { 47 | "browser": "./lite/worker.browser.js", 48 | "node": "./lite/worker.node.js" 49 | }, 50 | "import": "./lite/module.mjs", 51 | "require": "./lite/require.js" 52 | } 53 | } 54 | }; 55 | 56 | // --- 57 | // Exports 58 | // --- 59 | 60 | // entry: "foobar" === "." === default 61 | // conditions: ["default", "import", "node"] 62 | resolve.exports(pkg); 63 | resolve.exports(pkg, '.'); 64 | resolve.exports(pkg, 'foobar'); 65 | //=> ["./dist/module.mjs"] 66 | 67 | // entry: "foobar/lite" === "./lite" 68 | // conditions: ["default", "import", "node"] 69 | resolve.exports(pkg, 'foobar/lite'); 70 | resolve.exports(pkg, './lite'); 71 | //=> ["./lite/module.mjs"] 72 | 73 | // Enable `require` condition 74 | // conditions: ["default", "require", "node"] 75 | resolve.exports(pkg, 'foobar', { require: true }); //=> ["./dist/require.js"] 76 | resolve.exports(pkg, './lite', { require: true }); //=> ["./lite/require.js"] 77 | 78 | // Throws "Missing specifier in package" Error 79 | resolve.exports(pkg, 'foobar/hello'); 80 | resolve.exports(pkg, './hello/world'); 81 | 82 | // Add custom condition(s) 83 | // conditions: ["default", "worker", "import", "node"] 84 | resolve.exports(pkg, 'foobar/lite', { 85 | conditions: ['worker'] 86 | }); //=> ["./lite/worker.node.js"] 87 | 88 | // Toggle "browser" condition 89 | // conditions: ["default", "worker", "import", "browser"] 90 | resolve.exports(pkg, 'foobar/lite', { 91 | conditions: ['worker'], 92 | browser: true 93 | }); //=> ["./lite/worker.browser.js"] 94 | 95 | // Disable non-"default" condition activate 96 | // NOTE: breaks from Node.js default behavior 97 | // conditions: ["default", "custom"] 98 | resolve.exports(pkg, 'foobar/lite', { 99 | conditions: ['custom'], 100 | unsafe: true, 101 | }); 102 | //=> Error: No known conditions for "./lite" specifier in "foobar" package 103 | 104 | // --- 105 | // Imports 106 | // --- 107 | 108 | // conditions: ["default", "import", "node"] 109 | resolve.imports(pkg, '#hash'); 110 | resolve.imports(pkg, 'foobar/#hash'); 111 | //=> ["./hash/node.mjs"] 112 | 113 | // conditions: ["default", "import", "browser"] 114 | resolve.imports(pkg, '#hash', { browser: true }); 115 | resolve.imports(pkg, 'foobar/#hash'); 116 | //=> ["./hash/web.mjs"] 117 | 118 | // conditions: ["default"] 119 | resolve.imports(pkg, '#hash', { unsafe: true }); 120 | resolve.imports(pkg, 'foobar/#hash'); 121 | //=> ["./hash/detect.mjs"] 122 | 123 | resolve.imports(pkg, '#hello/world'); 124 | resolve.imports(pkg, 'foobar/#hello/world'); 125 | //=> Error: Missing "#hello/world" specifier in "foobar" package 126 | 127 | // --- 128 | // Legacy 129 | // --- 130 | 131 | // prefer "module" > "main" (default) 132 | resolve.legacy(pkg); //=> "dist/module.mjs" 133 | 134 | // customize fields order 135 | resolve.legacy(pkg, { 136 | fields: ['main', 'module'] 137 | }); //=> "dist/require.js" 138 | ``` 139 | 140 | ## API 141 | 142 | The [`resolve()`](#resolvepkg-entry-options), [`exports()`](#exportspkg-entry-options), and [`imports()`](#importspkg-target-options) functions share similar API signatures: 143 | 144 | ```ts 145 | export function resolve(pkg: Package, entry?: string, options?: Options): string[] | undefined; 146 | export function exports(pkg: Package, entry?: string, options?: Options): string[] | undefined; 147 | export function imports(pkg: Package, target: string, options?: Options): string[] | undefined; 148 | // ^ not optional! 149 | ``` 150 | 151 | All three: 152 | * accept a `package.json` file's contents as a JSON object 153 | * accept a target/entry identifier 154 | * may accept an [Options](#options) object 155 | * return `string[]`, `string`, or `undefined` 156 | 157 | The only difference is that `imports()` must accept a target identifier as there can be no inferred default. 158 | 159 | See below for further API descriptions. 160 | 161 | > **Note:** There is also a [Legacy Resolver API](#legacy-resolver) 162 | 163 | --- 164 | 165 | ### resolve(pkg, entry?, options?) 166 | Returns: `string[]` or `undefined` 167 | 168 | A convenience helper which automatically reroutes to [`exports()`](#exportspkg-entry-options) or [`imports()`](#importspkg-target-options) depending on the `entry` value. 169 | 170 | When unspecified, `entry` defaults to the `"."` identifier, which means that `exports()` will be invoked. 171 | 172 | ```js 173 | import * as r from 'resolve.exports'; 174 | 175 | let pkg = { 176 | name: 'foobar', 177 | // ... 178 | }; 179 | 180 | r.resolve(pkg); 181 | //~> r.exports(pkg, '.'); 182 | 183 | r.resolve(pkg, 'foobar'); 184 | //~> r.exports(pkg, '.'); 185 | 186 | r.resolve(pkg, 'foobar/subpath'); 187 | //~> r.exports(pkg, './subpath'); 188 | 189 | r.resolve(pkg, '#hash/md5'); 190 | //~> r.imports(pkg, '#hash/md5'); 191 | 192 | r.resolve(pkg, 'foobar/#hash/md5'); 193 | //~> r.imports(pkg, '#hash/md5'); 194 | ``` 195 | 196 | ### exports(pkg, entry?, options?) 197 | Returns: `string[]` or `undefined` 198 | 199 | Traverse the `"exports"` within the contents of a `package.json` file.
200 | If the contents _does not_ contain an `"exports"` map, then `undefined` will be returned. 201 | 202 | 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. 203 | 204 | This function may throw an Error if: 205 | 206 | * the requested `entry` cannot be resolved (aka, not defined in the `"exports"` map) 207 | * an `entry` _is_ defined but no known conditions were matched (see [`options.conditions`](#optionsconditions)) 208 | 209 | #### pkg 210 | Type: `object`
211 | Required: `true` 212 | 213 | The `package.json` contents. 214 | 215 | #### entry 216 | Type: `string`
217 | Required: `false`
218 | Default: `.` (aka, root) 219 | 220 | The desired target entry, or the original `import` path. 221 | 222 | When `entry` _is not_ a relative path (aka, does not start with `'.'`), then `entry` is given the `'./'` prefix. 223 | 224 | When `entry` begins with the package name (determined via the `pkg.name` value), then `entry` is truncated and made relative. 225 | 226 | When `entry` is already relative, it is accepted as is. 227 | 228 | ***Examples*** 229 | 230 | Assume we have a module named "foobar" and whose `pkg` contains `"name": "foobar"`. 231 | 232 | | `entry` value | treated as | reason | 233 | |-|-|-| 234 | | `null` / `undefined` | `'.'` | default | 235 | | `'.'` | `'.'` | value was relative | 236 | | `'foobar'` | `'.'` | value was `pkg.name` | 237 | | `'foobar/lite'` | `'./lite'` | value had `pkg.name` prefix | 238 | | `'./lite'` | `'./lite'` | value was relative | 239 | | `'lite'` | `'./lite'` | value was not relative & did not have `pkg.name` prefix | 240 | 241 | 242 | ### imports(pkg, target, options?) 243 | Returns: `string[]` or `undefined` 244 | 245 | Traverse the `"imports"` within the contents of a `package.json` file.
246 | If the contents _does not_ contain an `"imports"` map, then `undefined` will be returned. 247 | 248 | 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. 249 | 250 | This function may throw an Error if: 251 | 252 | * the requested `target` cannot be resolved (aka, not defined in the `"imports"` map) 253 | * an `target` _is_ defined but no known conditions were matched (see [`options.conditions`](#optionsconditions)) 254 | 255 | #### pkg 256 | Type: `object`
257 | Required: `true` 258 | 259 | The `package.json` contents. 260 | 261 | #### target 262 | Type: `string`
263 | Required: `true` 264 | 265 | The target import identifier; for example, `#hash` or `#hash/md5`. 266 | 267 | 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. 268 | 269 | ## Options 270 | 271 | The [`resolve()`](#resolvepkg-entry-options), [`imports()`](#importspkg-target-options), and [`exports()`](#exportspkg-entry-options) functions share these options. All properties are optional and you are not required to pass an `options` argument. 272 | 273 | Collectively, the `options` are used to assemble a list of [conditions](https://nodejs.org/docs/latest-v18.x/api/packages.html#conditional-exports) that should be activated while resolving your target(s). 274 | 275 | > **Note:** Although the Node.js documentation primarily showcases conditions alongside `"exports"` usage, they also apply to `"imports"` maps too. _([example](https://nodejs.org/docs/latest-v18.x/api/packages.html#subpath-imports))_ 276 | 277 | #### options.require 278 | Type: `boolean`
279 | Default: `false` 280 | 281 | When truthy, the `"require"` field is added to the list of allowed/known conditions.
282 | Otherwise the `"import"` field is added instead. 283 | 284 | #### options.browser 285 | Type: `boolean`
286 | Default: `false` 287 | 288 | When truthy, the `"browser"` field is added to the list of allowed/known conditions.
289 | Otherwise the `"node"` field is added instead. 290 | 291 | #### options.conditions 292 | Type: `string[]`
293 | Default: `[]` 294 | 295 | A list of additional/custom conditions that should be accepted when seen. 296 | 297 | > **Important:** The order specified within `options.conditions` does not matter.
The matching order/priority is **always** determined by the `"exports"` map's key order. 298 | 299 | For example, you may choose to accept a `"production"` condition in certain environments. Given the following `pkg` content: 300 | 301 | ```js 302 | const pkg = { 303 | // package.json ... 304 | "exports": { 305 | "worker": "./$worker.js", 306 | "require": "./$require.js", 307 | "production": "./$production.js", 308 | "import": "./$import.mjs", 309 | } 310 | }; 311 | 312 | resolve.exports(pkg, '.'); 313 | // Conditions: ["default", "import", "node"] 314 | //=> ["./$import.mjs"] 315 | 316 | resolve.exports(pkg, '.', { 317 | conditions: ['production'] 318 | }); 319 | // Conditions: ["default", "production", "import", "node"] 320 | //=> ["./$production.js"] 321 | 322 | resolve.exports(pkg, '.', { 323 | conditions: ['production'], 324 | require: true, 325 | }); 326 | // Conditions: ["default", "production", "require", "node"] 327 | //=> ["./$require.js"] 328 | 329 | resolve.exports(pkg, '.', { 330 | conditions: ['production', 'worker'], 331 | require: true, 332 | }); 333 | // Conditions: ["default", "production", "worker", "require", "node"] 334 | //=> ["./$worker.js"] 335 | 336 | resolve.exports(pkg, '.', { 337 | conditions: ['production', 'worker'] 338 | }); 339 | // Conditions: ["default", "production", "worker", "import", "node"] 340 | //=> ["./$worker.js"] 341 | ``` 342 | 343 | #### options.unsafe 344 | Type: `boolean`
345 | Default: `false` 346 | 347 | > **Important:** You probably do not want this option!
It will break out of Node's default resolution conditions. 348 | 349 | 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. 350 | 351 | ```js 352 | resolve.exports(pkg, '.'); 353 | //=> Conditions: ["default", "import", "node"] 354 | 355 | resolve.exports(pkg, '.', { unsafe: true }); 356 | //=> Conditions: ["default"] 357 | 358 | resolve.exports(pkg, '.', { unsafe: true, require: true, browser: true }); 359 | //=> Conditions: ["default"] 360 | ``` 361 | 362 | 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: 363 | 364 | ```js 365 | resolve.exports(pkg, '.', { 366 | unsafe: true, 367 | conditions: ["require"] 368 | }); 369 | //=> Conditions: ["default", "require"] 370 | 371 | resolve.exports(pkg, '.', { 372 | unsafe: true, 373 | conditions: ["browser", "require", "custom123"] 374 | }); 375 | //=> Conditions: ["default", "browser", "require", "custom123"] 376 | ``` 377 | 378 | ## Legacy Resolver 379 | 380 | 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). 381 | 382 | ### legacy(pkg, options?) 383 | Returns: `string` or `undefined` 384 | 385 | You may customize the field priority via [`options.fields`](#optionsfields). 386 | 387 | When a field is found, its value is returned _as written_.
388 | 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. 389 | 390 | #### options.browser 391 | Type: `boolean` or `string`
392 | Default: `false` 393 | 394 | When truthy, ensures that the `'browser'` field is part of the acceptable `fields` list. 395 | 396 | > **Important:** If your custom [`options.fields`](#optionsfields) 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. 397 | 398 | When `true` and `"browser"` is an object, then `legacy()` will return the the entire `"browser"` object. 399 | 400 | 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: 401 | 402 | * `false` – if the package author decided a file should be ignored; or 403 | * your `options.browser` string value – but made relative, if not already 404 | 405 | > See the [`"browser" field specification](https://github.com/defunctzombie/package-browser-field-spec) for more information. 406 | 407 | #### options.fields 408 | Type: `string[]`
409 | Default: `['module', 'main']` 410 | 411 | 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. 412 | 413 | 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. 414 | 415 | ```js 416 | import { legacy } from 'resolve.exports'; 417 | 418 | // package.json 419 | const pkg = { 420 | "name": "...", 421 | "worker": "worker.js", 422 | "module": "module.mjs", 423 | "browser": "browser.js", 424 | "main": "main.js", 425 | }; 426 | 427 | legacy(pkg); 428 | // fields = [module, main] 429 | //=> "module.mjs" 430 | 431 | legacy(pkg, { browser: true }); 432 | // fields = [browser, module, main] 433 | //=> "browser.mjs" 434 | 435 | legacy(pkg, { 436 | fields: ['missing', 'worker', 'module', 'main'] 437 | }); 438 | // fields = [missing, worker, module, main] 439 | //=> "worker.js" 440 | 441 | legacy(pkg, { 442 | fields: ['missing', 'worker', 'module', 'main'], 443 | browser: true, 444 | }); 445 | // fields = [browser, missing, worker, module, main] 446 | //=> "browser.js" 447 | 448 | legacy(pkg, { 449 | fields: ['module', 'browser', 'main'], 450 | browser: true, 451 | }); 452 | // fields = [module, browser, main] 453 | //=> "module.mjs" 454 | ``` 455 | 456 | ## License 457 | 458 | MIT © [Luke Edwards](https://lukeed.com) 459 | -------------------------------------------------------------------------------- /test/index.ts: -------------------------------------------------------------------------------- 1 | import * as uvu from 'uvu'; 2 | import * as assert from 'uvu/assert'; 3 | import * as lib from '../src/index'; 4 | 5 | import type * as t from 'resolve.exports'; 6 | 7 | type Package = t.Package; 8 | type Entry = t.Exports.Entry | t.Imports.Entry; 9 | type Options = t.Options; 10 | 11 | function pass(pkg: Package, expects: string|string[], entry?: string, options?: Options) { 12 | let out = lib.resolve(pkg, entry, options); 13 | if (typeof expects === 'string') { 14 | assert.ok(Array.isArray(out)); 15 | assert.is(out[0], expects); 16 | assert.is(out.length, 1); 17 | } else { 18 | // Array | null | undefined 19 | assert.equal(out, expects); 20 | } 21 | } 22 | 23 | function fail(pkg: Package, target: Entry, entry?: string, options?: Options) { 24 | try { 25 | lib.resolve(pkg, entry, options); 26 | assert.unreachable(); 27 | } catch (err) { 28 | assert.instance(err, Error); 29 | assert.is((err as Error).message, `Missing "${target}" specifier in "${pkg.name}" package`); 30 | } 31 | } 32 | 33 | function describe( 34 | name: string, 35 | cb: (it: uvu.Test) => void 36 | ) { 37 | let t = uvu.suite(name); 38 | cb(t); 39 | t.run(); 40 | } 41 | 42 | // --- 43 | 44 | describe('$.resolve', it => { 45 | it('should be a function', () => { 46 | assert.type(lib.resolve, 'function'); 47 | }); 48 | 49 | it('should return nothing if no maps', () => { 50 | let output = lib.resolve({ 51 | "name": "foobar" 52 | }); 53 | assert.is(output, undefined); 54 | }); 55 | 56 | it('should default to `$.exports` handler', () => { 57 | let pkg: Package = { 58 | "name": "foobar", 59 | "exports": "./hello.mjs" 60 | }; 61 | 62 | let output = lib.resolve(pkg); 63 | assert.equal(output, ['./hello.mjs']); 64 | 65 | output = lib.resolve(pkg, '.'); 66 | assert.equal(output, ['./hello.mjs']); 67 | 68 | try { 69 | lib.resolve(pkg, './other'); 70 | assert.unreachable(); 71 | } catch (err) { 72 | assert.instance(err, Error); 73 | assert.is((err as Error).message, `Missing "./other" specifier in "foobar" package`); 74 | } 75 | }); 76 | 77 | it('should run `$.imports` if given #ident', () => { 78 | let pkg: Package = { 79 | "name": "foobar", 80 | "imports": { 81 | "#foo": "./foo.mjs" 82 | } 83 | }; 84 | 85 | let output = lib.resolve(pkg, '#foo'); 86 | assert.equal(output, ['./foo.mjs']); 87 | 88 | output = lib.resolve(pkg, 'foobar/#foo'); 89 | assert.equal(output, ['./foo.mjs']); 90 | 91 | try { 92 | lib.resolve(pkg, '#bar'); 93 | assert.unreachable(); 94 | } catch (err) { 95 | assert.instance(err, Error); 96 | assert.is((err as Error).message, `Missing "#bar" specifier in "foobar" package`); 97 | } 98 | }); 99 | 100 | it('should run `$.export` if given "external" identifier', () => { 101 | let pkg: Package = { 102 | "name": "foobar", 103 | "exports": { 104 | ".": "./foo.mjs" 105 | } 106 | }; 107 | 108 | try { 109 | lib.resolve(pkg, 'external'); 110 | assert.unreachable(); 111 | } catch (err) { 112 | assert.instance(err, Error); 113 | // IMPORTANT: treats "external" as "./external" 114 | assert.is((err as Error).message, `Missing "./external" specifier in "foobar" package`); 115 | } 116 | }); 117 | 118 | it('should run `$.export` if given "external/subpath" identifier', () => { 119 | let pkg: Package = { 120 | "name": "foobar", 121 | "exports": { 122 | ".": "./foo.mjs" 123 | } 124 | }; 125 | 126 | try { 127 | lib.resolve(pkg, 'external/subpath'); 128 | assert.unreachable(); 129 | } catch (err) { 130 | assert.instance(err, Error); 131 | // IMPORTANT: treats "external/subpath" as "./external/subpath" 132 | assert.is((err as Error).message, `Missing "./external/subpath" specifier in "foobar" package`); 133 | } 134 | }); 135 | }); 136 | 137 | describe('$.imports', it => { 138 | it('should be a function', () => { 139 | assert.type(lib.imports, 'function'); 140 | }); 141 | 142 | it('should return nothing if no "imports" map', () => { 143 | let pkg: Package = { 144 | "name": "foobar" 145 | }; 146 | 147 | let output = lib.imports(pkg, '#any'); 148 | assert.is(output, undefined); 149 | }); 150 | 151 | it('imports["#foo"] = string', () => { 152 | let pkg: Package = { 153 | "name": "foobar", 154 | "imports": { 155 | "#foo": "./$import", 156 | "#bar": "module-a", 157 | } 158 | }; 159 | 160 | pass(pkg, './$import', '#foo'); 161 | pass(pkg, './$import', 'foobar/#foo'); 162 | 163 | pass(pkg, 'module-a', '#bar'); 164 | pass(pkg, 'module-a', 'foobar/#bar'); 165 | 166 | fail(pkg, '#other', 'foobar/#other'); 167 | }); 168 | 169 | it('imports["#foo"] = object', () => { 170 | let pkg: Package = { 171 | "name": "foobar", 172 | "imports": { 173 | "#foo": { 174 | "import": "./$import", 175 | "require": "./$require", 176 | } 177 | } 178 | }; 179 | 180 | pass(pkg, './$import', '#foo'); 181 | pass(pkg, './$import', 'foobar/#foo'); 182 | 183 | fail(pkg, '#other', 'foobar/#other'); 184 | }); 185 | 186 | it('nested conditions :: subpath', () => { 187 | let pkg: Package = { 188 | "name": "foobar", 189 | "imports": { 190 | "#lite": { 191 | "node": { 192 | "import": "./$node.import", 193 | "require": "./$node.require" 194 | }, 195 | "browser": { 196 | "import": "./$browser.import", 197 | "require": "./$browser.require" 198 | }, 199 | } 200 | } 201 | }; 202 | 203 | pass(pkg, './$node.import', 'foobar/#lite'); 204 | pass(pkg, './$node.require', 'foobar/#lite', { require: true }); 205 | 206 | pass(pkg, './$browser.import', 'foobar/#lite', { browser: true }); 207 | pass(pkg, './$browser.require', 'foobar/#lite', { browser: true, require: true }); 208 | }); 209 | 210 | it('nested conditions :: subpath :: inverse', () => { 211 | let pkg: Package = { 212 | "name": "foobar", 213 | "imports": { 214 | "#lite": { 215 | "import": { 216 | "browser": "./$browser.import", 217 | "node": "./$node.import", 218 | }, 219 | "require": { 220 | "browser": "./$browser.require", 221 | "node": "./$node.require", 222 | } 223 | } 224 | } 225 | }; 226 | 227 | pass(pkg, './$node.import', 'foobar/#lite'); 228 | pass(pkg, './$node.require', 'foobar/#lite', { require: true }); 229 | 230 | pass(pkg, './$browser.import', 'foobar/#lite', { browser: true }); 231 | pass(pkg, './$browser.require', 'foobar/#lite', { browser: true, require: true }); 232 | }); 233 | 234 | // https://nodejs.org/api/packages.html#packages_subpath_folder_mappings 235 | it('imports["#key/*"]', () => { 236 | let pkg: Package = { 237 | "name": "foobar", 238 | "imports": { 239 | "#key/*": "./cheese/*.mjs" 240 | } 241 | }; 242 | 243 | pass(pkg, './cheese/hello.mjs', 'foobar/#key/hello'); 244 | pass(pkg, './cheese/hello/world.mjs', '#key/hello/world'); 245 | 246 | // evaluate as defined, not wrong 247 | pass(pkg, './cheese/hello.js.mjs', '#key/hello.js'); 248 | pass(pkg, './cheese/hello.js.mjs', 'foobar/#key/hello.js'); 249 | pass(pkg, './cheese/hello/world.js.mjs', '#key/hello/world.js'); 250 | }); 251 | 252 | it('imports["#key/dir*"]', () => { 253 | let pkg: Package = { 254 | "name": "foobar", 255 | "imports": { 256 | "#key/dir*": "./cheese/*.mjs" 257 | } 258 | }; 259 | 260 | pass(pkg, './cheese/test.mjs', '#key/dirtest'); 261 | pass(pkg, './cheese/test.mjs', 'foobar/#key/dirtest'); 262 | 263 | pass(pkg, './cheese/test/wheel.mjs', '#key/dirtest/wheel'); 264 | pass(pkg, './cheese/test/wheel.mjs', 'foobar/#key/dirtest/wheel'); 265 | }); 266 | 267 | // https://github.com/lukeed/resolve.exports/issues/9 268 | it('imports["#key/dir*"] :: repeat "*" value', () => { 269 | let pkg: Package = { 270 | "name": "foobar", 271 | "imports": { 272 | "#key/dir*": "./*sub/dir*/file.js" 273 | } 274 | }; 275 | 276 | pass(pkg, './testsub/dirtest/file.js', '#key/dirtest'); 277 | pass(pkg, './testsub/dirtest/file.js', 'foobar/#key/dirtest'); 278 | 279 | pass(pkg, './test/innersub/dirtest/inner/file.js', '#key/dirtest/inner'); 280 | pass(pkg, './test/innersub/dirtest/inner/file.js', 'foobar/#key/dirtest/inner'); 281 | }); 282 | 283 | /** 284 | * @deprecated Documentation-only deprecation in Node 14.13 285 | * @deprecated Runtime deprecation in Node 16.0 286 | * @removed Removed in Node 18.0 287 | * @see https://nodejs.org/docs/latest-v16.x/api/packages.html#subpath-folder-mappings 288 | */ 289 | it('imports["#features/"]', () => { 290 | let pkg: Package = { 291 | "name": "foobar", 292 | "imports": { 293 | "#features/": "./features/" 294 | } 295 | }; 296 | 297 | pass(pkg, './features/', '#features/'); 298 | pass(pkg, './features/', 'foobar/#features/'); 299 | 300 | pass(pkg, './features/hello.js', 'foobar/#features/hello.js'); 301 | 302 | fail(pkg, '#features', '#features'); 303 | fail(pkg, '#features', 'foobar/#features'); 304 | }); 305 | 306 | it('imports["#features/"] :: conditions', () => { 307 | let pkg: Package = { 308 | "name": "foobar", 309 | "imports": { 310 | "#features/": { 311 | "browser": { 312 | "import": "./browser.import/", 313 | "require": "./browser.require/", 314 | }, 315 | "import": "./import/", 316 | "require": "./require/", 317 | }, 318 | } 319 | }; 320 | 321 | // import 322 | pass(pkg, './import/', '#features/'); 323 | pass(pkg, './import/', 'foobar/#features/'); 324 | 325 | pass(pkg, './import/hello.js', '#features/hello.js'); 326 | pass(pkg, './import/hello.js', 'foobar/#features/hello.js'); 327 | 328 | // require 329 | pass(pkg, './require/', '#features/', { require: true }); 330 | pass(pkg, './require/', 'foobar/#features/', { require: true }); 331 | 332 | pass(pkg, './require/hello.js', '#features/hello.js', { require: true }); 333 | pass(pkg, './require/hello.js', 'foobar/#features/hello.js', { require: true }); 334 | 335 | // require + browser 336 | pass(pkg, './browser.require/', '#features/', { browser: true, require: true }); 337 | pass(pkg, './browser.require/', 'foobar/#features/', { browser: true, require: true }); 338 | 339 | pass(pkg, './browser.require/hello.js', '#features/hello.js', { browser: true, require: true }); 340 | pass(pkg, './browser.require/hello.js', 'foobar/#features/hello.js', { browser: true, require: true }); 341 | }); 342 | 343 | // https://nodejs.org/api/packages.html#packages_subpath_folder_mappings 344 | it('imports["#features/*"]', () => { 345 | let pkg: Package = { 346 | "name": "foobar", 347 | "imports": { 348 | "#features/*": "./features/*.js", 349 | } 350 | }; 351 | 352 | fail(pkg, '#features', '#features'); 353 | fail(pkg, '#features', 'foobar/#features'); 354 | 355 | fail(pkg, '#features/', '#features/'); 356 | fail(pkg, '#features/', 'foobar/#features/'); 357 | 358 | pass(pkg, './features/a.js', 'foobar/#features/a'); 359 | pass(pkg, './features/ab.js', 'foobar/#features/ab'); 360 | pass(pkg, './features/abc.js', 'foobar/#features/abc'); 361 | 362 | pass(pkg, './features/hello.js', 'foobar/#features/hello'); 363 | pass(pkg, './features/foo/bar.js', 'foobar/#features/foo/bar'); 364 | 365 | // Valid: Pattern trailers allow any exact substrings to be matched 366 | pass(pkg, './features/hello.js.js', 'foobar/#features/hello.js'); 367 | pass(pkg, './features/foo/bar.js.js', 'foobar/#features/foo/bar.js'); 368 | }); 369 | 370 | // https://nodejs.org/api/packages.html#packages_subpath_folder_mappings 371 | it('imports["#fooba*"] :: with "#foo*" key', () => { 372 | let pkg: Package = { 373 | "name": "foobar", 374 | "imports": { 375 | "#fooba*": "./features/*.js", 376 | "#foo*": "./" 377 | } 378 | }; 379 | 380 | pass(pkg, './features/r.js', '#foobar'); 381 | pass(pkg, './features/r.js', 'foobar/#foobar'); 382 | 383 | pass(pkg, './features/r/hello.js', 'foobar/#foobar/hello'); 384 | pass(pkg, './features/r/foo/bar.js', 'foobar/#foobar/foo/bar'); 385 | 386 | // Valid: Pattern trailers allow any exact substrings to be matched 387 | pass(pkg, './features/r/hello.js.js', 'foobar/#foobar/hello.js'); 388 | pass(pkg, './features/r/foo/bar.js.js', 'foobar/#foobar/foo/bar.js'); 389 | }); 390 | 391 | // https://github.com/lukeed/resolve.exports/issues/7 392 | it('imports["#fooba*"] :: with "#foo*" key first', () => { 393 | let pkg: Package = { 394 | "name": "foobar", 395 | "imports": { 396 | "#foo*": "./", 397 | "#fooba*": "./features/*.js" 398 | } 399 | }; 400 | 401 | pass(pkg, './features/r.js', '#foobar'); 402 | pass(pkg, './features/r.js', 'foobar/#foobar'); 403 | 404 | pass(pkg, './features/r/hello.js', 'foobar/#foobar/hello'); 405 | pass(pkg, './features/r/foo/bar.js', 'foobar/#foobar/foo/bar'); 406 | 407 | // Valid: Pattern trailers allow any exact substrings to be matched 408 | pass(pkg, './features/r/hello.js.js', 'foobar/#foobar/hello.js'); 409 | pass(pkg, './features/r/foo/bar.js.js', 'foobar/#foobar/foo/bar.js'); 410 | }); 411 | 412 | // https://github.com/lukeed/resolve.exports/issues/27 413 | it('imports["#*"] :: with "#foo*" key', () => { 414 | let pkg: Package = { 415 | "name": "foobar", 416 | "imports": { 417 | "#*": "./root/*.js", 418 | "#foo*": "./foo/*.js" 419 | } 420 | }; 421 | 422 | // "#foo*" 423 | pass(pkg, './foo/bar.js', '#foobar'); 424 | pass(pkg, './foo/bar.js', 'foobar/#foobar'); 425 | 426 | pass(pkg, './foo/bar/hello.js', 'foobar/#foobar/hello'); 427 | pass(pkg, './foo/bar/foo/bar.js', 'foobar/#foobar/foo/bar'); 428 | 429 | // Valid: Pattern trailers allow any exact substrings to be matched 430 | pass(pkg, './foo/bar/hello.js.js', 'foobar/#foobar/hello.js'); 431 | pass(pkg, './foo/bar/foo/bar.js.js', 'foobar/#foobar/foo/bar.js'); 432 | 433 | // "#*" 434 | pass(pkg, './root/other.js', '#other'); 435 | pass(pkg, './root/other.js', 'foobar/#other'); 436 | 437 | pass(pkg, './root/other/hello.js', 'foobar/#other/hello'); 438 | pass(pkg, './root/other/foo/bar.js', 'foobar/#other/foo/bar'); 439 | 440 | // Valid: Pattern trailers allow any exact substrings to be matched 441 | pass(pkg, './root/other/hello.js.js', 'foobar/#other/hello.js'); 442 | pass(pkg, './root/other/foo/bar.js.js', 'foobar/#other/foo/bar.js'); 443 | }); 444 | 445 | // https://github.com/lukeed/resolve.exports/issues/27 446 | it('imports["#*"] :: with "#foo*" key first', () => { 447 | let pkg: Package = { 448 | "name": "foobar", 449 | "imports": { 450 | "#foo*": "./foo/*.js", 451 | "#*": "./root/*.js" 452 | } 453 | }; 454 | 455 | // "#foo*" 456 | pass(pkg, './foo/bar.js', '#foobar'); 457 | pass(pkg, './foo/bar.js', 'foobar/#foobar'); 458 | 459 | pass(pkg, './foo/bar/hello.js', 'foobar/#foobar/hello'); 460 | pass(pkg, './foo/bar/foo/bar.js', 'foobar/#foobar/foo/bar'); 461 | 462 | // Valid: Pattern trailers allow any exact substrings to be matched 463 | pass(pkg, './foo/bar/hello.js.js', 'foobar/#foobar/hello.js'); 464 | pass(pkg, './foo/bar/foo/bar.js.js', 'foobar/#foobar/foo/bar.js'); 465 | 466 | // "#*" 467 | pass(pkg, './root/other.js', '#other'); 468 | pass(pkg, './root/other.js', 'foobar/#other'); 469 | 470 | pass(pkg, './root/other/hello.js', 'foobar/#other/hello'); 471 | pass(pkg, './root/other/foo/bar.js', 'foobar/#other/foo/bar'); 472 | 473 | // Valid: Pattern trailers allow any exact substrings to be matched 474 | pass(pkg, './root/other/hello.js.js', 'foobar/#other/hello.js'); 475 | pass(pkg, './root/other/foo/bar.js.js', 'foobar/#other/foo/bar.js'); 476 | }); 477 | 478 | it('imports["#*"] :: with "#a" static key', () => { 479 | let pkg: Package = { 480 | "name": "foobar", 481 | "imports": { 482 | "#*": "./root/*.js", 483 | "#a": "./a.js", 484 | } 485 | }; 486 | 487 | pass(pkg, './root/other.js', '#other'); 488 | pass(pkg, './root/other.js', 'foobar/#other'); 489 | 490 | pass(pkg, './a.js', '#a'); 491 | pass(pkg, './a.js', 'foobar/#a'); 492 | }); 493 | 494 | it('imports["#*"] :: with "#a" static key first', () => { 495 | let pkg: Package = { 496 | "name": "foobar", 497 | "imports": { 498 | "#a": "./a.js", 499 | "#*": "./root/*.js", 500 | } 501 | }; 502 | 503 | pass(pkg, './root/other.js', '#other'); 504 | pass(pkg, './root/other.js', 'foobar/#other'); 505 | 506 | pass(pkg, './a.js', '#a'); 507 | pass(pkg, './a.js', 'foobar/#a'); 508 | }); 509 | 510 | // https://github.com/lukeed/resolve.exports/issues/16 511 | it('imports["#features/*"] :: with `null` internals', () => { 512 | let pkg: Package = { 513 | "name": "foobar", 514 | "imports": { 515 | "#features/*": "./src/features/*.js", 516 | "#features/internal/*": null 517 | } 518 | }; 519 | 520 | pass(pkg, './src/features/hello.js', '#features/hello'); 521 | pass(pkg, './src/features/hello.js', 'foobar/#features/hello'); 522 | 523 | pass(pkg, './src/features/foo/bar.js', '#features/foo/bar'); 524 | pass(pkg, './src/features/foo/bar.js', 'foobar/#features/foo/bar'); 525 | 526 | // TODO? Native throws `ERR_PACKAGE_PATH_NOT_EXPORTED` 527 | // Currently throwing `Missing "%s" specifier in "$s" package` 528 | fail(pkg, '#features/internal/hello', '#features/internal/hello'); 529 | fail(pkg, '#features/internal/foo/bar', '#features/internal/foo/bar'); 530 | }); 531 | 532 | // https://github.com/lukeed/resolve.exports/issues/16 533 | it('imports["#features/*"] :: with `null` internals first', () => { 534 | let pkg: Package = { 535 | "name": "foobar", 536 | "imports": { 537 | "#features/internal/*": null, 538 | "#features/*": "./src/features/*.js", 539 | } 540 | }; 541 | 542 | pass(pkg, './src/features/hello.js', '#features/hello'); 543 | pass(pkg, './src/features/hello.js', 'foobar/#features/hello'); 544 | 545 | pass(pkg, './src/features/foo/bar.js', '#features/foo/bar'); 546 | pass(pkg, './src/features/foo/bar.js', 'foobar/#features/foo/bar'); 547 | 548 | // TODO? Native throws `ERR_PACKAGE_PATH_NOT_EXPORTED` 549 | // Currently throwing `Missing "%s" specifier in "$s" package` 550 | fail(pkg, '#features/internal/hello', '#features/internal/hello'); 551 | fail(pkg, '#features/internal/foo/bar', '#features/internal/foo/bar'); 552 | }); 553 | 554 | // https://nodejs.org/docs/latest-v18.x/api/packages.html#package-entry-points 555 | it('imports["#features/*"] :: with "#features/*.js" key', () => { 556 | let pkg: Package = { 557 | "name": "foobar", 558 | "imports": { 559 | "#features/*": "./features/*.js", 560 | "#features/*.js": "./features/*.js", 561 | } 562 | }; 563 | 564 | fail(pkg, '#features', '#features'); 565 | fail(pkg, '#features', 'foobar/#features'); 566 | 567 | fail(pkg, '#features/', '#features/'); 568 | fail(pkg, '#features/', 'foobar/#features/'); 569 | 570 | pass(pkg, './features/a.js', 'foobar/#features/a'); 571 | pass(pkg, './features/ab.js', 'foobar/#features/ab'); 572 | pass(pkg, './features/abc.js', 'foobar/#features/abc'); 573 | 574 | pass(pkg, './features/hello.js', 'foobar/#features/hello'); 575 | pass(pkg, './features/hello.js', 'foobar/#features/hello.js'); 576 | 577 | pass(pkg, './features/foo/bar.js', 'foobar/#features/foo/bar'); 578 | pass(pkg, './features/foo/bar.js', 'foobar/#features/foo/bar.js'); 579 | }); 580 | 581 | it('imports["#features/*"] :: conditions', () => { 582 | let pkg: Package = { 583 | "name": "foobar", 584 | "imports": { 585 | "#features/*": { 586 | "browser": { 587 | "import": "./browser.import/*.mjs", 588 | "require": "./browser.require/*.js", 589 | }, 590 | "import": "./import/*.mjs", 591 | "require": "./require/*.js", 592 | }, 593 | } 594 | }; 595 | 596 | // import 597 | fail(pkg, '#features/', '#features/'); // no file 598 | fail(pkg, '#features/', 'foobar/#features/'); // no file 599 | 600 | pass(pkg, './import/hello.mjs', '#features/hello'); 601 | pass(pkg, './import/hello.mjs', 'foobar/#features/hello'); 602 | 603 | // require 604 | fail(pkg, '#features/', '#features/', { require: true }); // no file 605 | fail(pkg, '#features/', 'foobar/#features/', { require: true }); // no file 606 | 607 | pass(pkg, './require/hello.js', '#features/hello', { require: true }); 608 | pass(pkg, './require/hello.js', 'foobar/#features/hello', { require: true }); 609 | 610 | // require + browser 611 | fail(pkg, '#features/', '#features/', { browser: true, require: true }); // no file 612 | fail(pkg, '#features/', 'foobar/#features/', { browser: true, require: true }); // no file 613 | 614 | pass(pkg, './browser.require/hello.js', '#features/hello', { browser: true, require: true }); 615 | pass(pkg, './browser.require/hello.js', 'foobar/#features/hello', { browser: true, require: true }); 616 | }); 617 | 618 | it('should handle mixed path/conditions', () => { 619 | let pkg: Package = { 620 | "name": "foobar", 621 | "imports": { 622 | "#foo": [ 623 | { 624 | "require": "./$foo.require" 625 | }, 626 | "./$foo.string" 627 | ] 628 | } 629 | }; 630 | 631 | // TODO? if len==1 then single? 632 | pass(pkg, ['./$foo.string'], '#foo'); 633 | pass(pkg, ['./$foo.string'], 'foobar/#foo'); 634 | 635 | pass(pkg, ['./$foo.require', './$foo.string'], '#foo', { require: true }); 636 | pass(pkg, ['./$foo.require', './$foo.string'], 'foobar/#foo', { require: true }); 637 | }); 638 | 639 | // https://github.com/lukeed/resolve.exports/issues/34 640 | it('imports["#features/*"] :: avoid lazy matching', () => { 641 | let pkg: Package = { 642 | name: 'test', 643 | imports: { 644 | '#features/*.css': './src/*.css', 645 | '#features/*.ts': './src/*.ts', 646 | } 647 | }; 648 | pass(pkg, './src/asdf/css.ts', '#features/asdf/css.ts'); 649 | }); 650 | }); 651 | 652 | describe('$.exports', it => { 653 | it('should be a function', () => { 654 | assert.type(lib.exports, 'function'); 655 | }); 656 | 657 | it('should return nothing if no "exports" map', () => { 658 | let pkg: Package = { 659 | "name": "foobar" 660 | }; 661 | 662 | let output = lib.exports(pkg, '#any'); 663 | assert.is(output, undefined); 664 | }); 665 | 666 | it('should default to "." target input', () => { 667 | let pkg: Package = { 668 | "name": "foobar", 669 | "exports": { 670 | ".": "./hello.mjs" 671 | } 672 | }; 673 | 674 | let output = lib.exports(pkg); 675 | assert.equal(output, ['./hello.mjs']); 676 | 677 | output = lib.exports(pkg, '.'); 678 | assert.equal(output, ['./hello.mjs']); 679 | 680 | output = lib.exports(pkg, 'foobar'); 681 | assert.equal(output, ['./hello.mjs']); 682 | }); 683 | 684 | it('exports=string', () => { 685 | let pkg: Package = { 686 | "name": "foobar", 687 | "exports": "./$string", 688 | }; 689 | 690 | pass(pkg, './$string'); 691 | pass(pkg, './$string', '.'); 692 | pass(pkg, './$string', 'foobar'); 693 | 694 | fail(pkg, './other', 'other'); 695 | fail(pkg, './other', 'foobar/other'); 696 | fail(pkg, './hello', './hello'); 697 | }); 698 | 699 | it('exports = { self }', () => { 700 | let pkg: Package = { 701 | "name": "foobar", 702 | "exports": { 703 | "import": "./$import", 704 | "require": "./$require", 705 | } 706 | }; 707 | 708 | pass(pkg, './$import'); 709 | pass(pkg, './$import', '.'); 710 | pass(pkg, './$import', 'foobar'); 711 | 712 | fail(pkg, './other', 'other'); 713 | fail(pkg, './other', 'foobar/other'); 714 | fail(pkg, './hello', './hello'); 715 | }); 716 | 717 | it('exports["."] = string', () => { 718 | let pkg: Package = { 719 | "name": "foobar", 720 | "exports": { 721 | ".": "./$self", 722 | } 723 | }; 724 | 725 | pass(pkg, './$self'); 726 | pass(pkg, './$self', '.'); 727 | pass(pkg, './$self', 'foobar'); 728 | 729 | fail(pkg, './other', 'other'); 730 | fail(pkg, './other', 'foobar/other'); 731 | fail(pkg, './hello', './hello'); 732 | }); 733 | 734 | it('exports["."] = object', () => { 735 | let pkg: Package = { 736 | "name": "foobar", 737 | "exports": { 738 | ".": { 739 | "import": "./$import", 740 | "require": "./$require", 741 | } 742 | } 743 | }; 744 | 745 | pass(pkg, './$import'); 746 | pass(pkg, './$import', '.'); 747 | pass(pkg, './$import', 'foobar'); 748 | 749 | fail(pkg, './other', 'other'); 750 | fail(pkg, './other', 'foobar/other'); 751 | fail(pkg, './hello', './hello'); 752 | }); 753 | 754 | it('exports["./foo"] = string', () => { 755 | let pkg: Package = { 756 | "name": "foobar", 757 | "exports": { 758 | "./foo": "./$import", 759 | } 760 | }; 761 | 762 | pass(pkg, './$import', './foo'); 763 | pass(pkg, './$import', 'foobar/foo'); 764 | 765 | fail(pkg, '.'); 766 | fail(pkg, '.', 'foobar'); 767 | fail(pkg, './other', 'foobar/other'); 768 | }); 769 | 770 | it('exports["./foo"] = object', () => { 771 | let pkg: Package = { 772 | "name": "foobar", 773 | "exports": { 774 | "./foo": { 775 | "import": "./$import", 776 | "require": "./$require", 777 | } 778 | } 779 | }; 780 | 781 | pass(pkg, './$import', './foo'); 782 | pass(pkg, './$import', 'foobar/foo'); 783 | 784 | fail(pkg, '.'); 785 | fail(pkg, '.', 'foobar'); 786 | fail(pkg, './other', 'foobar/other'); 787 | }); 788 | 789 | // https://nodejs.org/api/packages.html#packages_nested_conditions 790 | it('nested conditions', () => { 791 | let pkg: Package = { 792 | "name": "foobar", 793 | "exports": { 794 | "node": { 795 | "import": "./$node.import", 796 | "require": "./$node.require" 797 | }, 798 | "default": "./$default", 799 | } 800 | }; 801 | 802 | pass(pkg, './$node.import'); 803 | pass(pkg, './$node.import', 'foobar'); 804 | 805 | // browser => no "node" key 806 | pass(pkg, './$default', '.', { browser: true }); 807 | pass(pkg, './$default', 'foobar', { browser: true }); 808 | 809 | fail(pkg, './hello', './hello'); 810 | fail(pkg, './other', 'foobar/other'); 811 | fail(pkg, './other', 'other'); 812 | }); 813 | 814 | it('nested conditions :: subpath', () => { 815 | let pkg: Package = { 816 | "name": "foobar", 817 | "exports": { 818 | "./lite": { 819 | "node": { 820 | "import": "./$node.import", 821 | "require": "./$node.require" 822 | }, 823 | "browser": { 824 | "import": "./$browser.import", 825 | "require": "./$browser.require" 826 | }, 827 | } 828 | } 829 | }; 830 | 831 | pass(pkg, './$node.import', 'foobar/lite'); 832 | pass(pkg, './$node.require', 'foobar/lite', { require: true }); 833 | 834 | pass(pkg, './$browser.import', 'foobar/lite', { browser: true }); 835 | pass(pkg, './$browser.require', 'foobar/lite', { browser: true, require: true }); 836 | }); 837 | 838 | it('nested conditions :: subpath :: inverse', () => { 839 | let pkg: Package = { 840 | "name": "foobar", 841 | "exports": { 842 | "./lite": { 843 | "import": { 844 | "browser": "./$browser.import", 845 | "node": "./$node.import", 846 | }, 847 | "require": { 848 | "browser": "./$browser.require", 849 | "node": "./$node.require", 850 | } 851 | } 852 | } 853 | }; 854 | 855 | pass(pkg, './$node.import', 'foobar/lite'); 856 | pass(pkg, './$node.require', 'foobar/lite', { require: true }); 857 | 858 | pass(pkg, './$browser.import', 'foobar/lite', { browser: true }); 859 | pass(pkg, './$browser.require', 'foobar/lite', { browser: true, require: true }); 860 | }); 861 | 862 | // https://nodejs.org/api/packages.html#packages_subpath_folder_mappings 863 | it('exports["./"]', () => { 864 | let pkg: Package = { 865 | "name": "foobar", 866 | "exports": { 867 | ".": { 868 | "require": "./$require", 869 | "import": "./$import" 870 | }, 871 | "./package.json": "./package.json", 872 | "./": "./" 873 | } 874 | }; 875 | 876 | pass(pkg, './$import'); 877 | pass(pkg, './$import', 'foobar'); 878 | pass(pkg, './$require', 'foobar', { require: true }); 879 | 880 | pass(pkg, './package.json', 'package.json'); 881 | pass(pkg, './package.json', 'foobar/package.json'); 882 | pass(pkg, './package.json', './package.json'); 883 | 884 | // "loose" / everything exposed 885 | pass(pkg, './hello.js', 'hello.js'); 886 | pass(pkg, './hello.js', 'foobar/hello.js'); 887 | pass(pkg, './hello/world.js', './hello/world.js'); 888 | }); 889 | 890 | it('exports["./"] :: w/o "." key', () => { 891 | let pkg: Package = { 892 | "name": "foobar", 893 | "exports": { 894 | "./package.json": "./package.json", 895 | "./": "./" 896 | } 897 | }; 898 | 899 | fail(pkg, '.', "."); 900 | fail(pkg, '.', "foobar"); 901 | 902 | pass(pkg, './package.json', 'package.json'); 903 | pass(pkg, './package.json', 'foobar/package.json'); 904 | pass(pkg, './package.json', './package.json'); 905 | 906 | // "loose" / everything exposed 907 | pass(pkg, './hello.js', 'hello.js'); 908 | pass(pkg, './hello.js', 'foobar/hello.js'); 909 | pass(pkg, './hello/world.js', './hello/world.js'); 910 | }); 911 | 912 | // https://nodejs.org/api/packages.html#packages_subpath_folder_mappings 913 | it('exports["./*"]', () => { 914 | let pkg: Package = { 915 | "name": "foobar", 916 | "exports": { 917 | "./*": "./cheese/*.mjs" 918 | } 919 | }; 920 | 921 | fail(pkg, '.', "."); 922 | fail(pkg, '.', "foobar"); 923 | 924 | pass(pkg, './cheese/hello.mjs', 'hello'); 925 | pass(pkg, './cheese/hello.mjs', 'foobar/hello'); 926 | pass(pkg, './cheese/hello/world.mjs', './hello/world'); 927 | 928 | // evaluate as defined, not wrong 929 | pass(pkg, './cheese/hello.js.mjs', 'hello.js'); 930 | pass(pkg, './cheese/hello.js.mjs', 'foobar/hello.js'); 931 | pass(pkg, './cheese/hello/world.js.mjs', './hello/world.js'); 932 | }); 933 | 934 | it('exports["./dir*"]', () => { 935 | let pkg: Package = { 936 | "name": "foobar", 937 | "exports": { 938 | "./dir*": "./cheese/*.mjs" 939 | } 940 | }; 941 | 942 | fail(pkg, '.', "."); 943 | fail(pkg, '.', "foobar"); 944 | 945 | pass(pkg, './cheese/test.mjs', 'dirtest'); 946 | pass(pkg, './cheese/test.mjs', 'foobar/dirtest'); 947 | 948 | pass(pkg, './cheese/test/wheel.mjs', 'dirtest/wheel'); 949 | pass(pkg, './cheese/test/wheel.mjs', 'foobar/dirtest/wheel'); 950 | }); 951 | 952 | // https://github.com/lukeed/resolve.exports/issues/9 953 | it('exports["./dir*"] :: repeat "*" value', () => { 954 | let pkg: Package = { 955 | "name": "foobar", 956 | "exports": { 957 | "./dir*": "./*sub/dir*/file.js" 958 | } 959 | }; 960 | 961 | fail(pkg, '.', "."); 962 | fail(pkg, '.', "foobar"); 963 | 964 | pass(pkg, './testsub/dirtest/file.js', 'dirtest'); 965 | pass(pkg, './testsub/dirtest/file.js', 'foobar/dirtest'); 966 | 967 | pass(pkg, './test/innersub/dirtest/inner/file.js', 'dirtest/inner'); 968 | pass(pkg, './test/innersub/dirtest/inner/file.js', 'foobar/dirtest/inner'); 969 | }); 970 | 971 | it('exports["./dir/*"] :: "*" value', () => { 972 | let pkg: Package = { 973 | "name": "foobar", 974 | "exports": { 975 | ".": "./dir/index.js", 976 | "./dir": "./dir/index.js", 977 | "./dir/*": "./dir/index.js" 978 | } 979 | }; 980 | 981 | pass(pkg, './dir/index.js', 'foobar'); 982 | pass(pkg, './dir/index.js', 'foobar/dir'); 983 | pass(pkg, './dir/index.js', 'foobar/dir/profile'); 984 | }); 985 | 986 | it('exports["./dir*"] :: share "name" start', () => { 987 | let pkg: Package = { 988 | "name": "director", 989 | "exports": { 990 | "./dir*": "./*sub/dir*/file.js" 991 | } 992 | }; 993 | 994 | fail(pkg, '.', "."); 995 | fail(pkg, '.', "director"); 996 | 997 | pass(pkg, './testsub/dirtest/file.js', 'dirtest'); 998 | pass(pkg, './testsub/dirtest/file.js', 'director/dirtest'); 999 | 1000 | pass(pkg, './test/innersub/dirtest/inner/file.js', 'dirtest/inner'); 1001 | pass(pkg, './test/innersub/dirtest/inner/file.js', 'director/dirtest/inner'); 1002 | }); 1003 | 1004 | /** 1005 | * @deprecated Documentation-only deprecation in Node 14.13 1006 | * @deprecated Runtime deprecation in Node 16.0 1007 | * @removed Removed in Node 18.0 1008 | * @see https://nodejs.org/docs/latest-v16.x/api/packages.html#subpath-folder-mappings 1009 | */ 1010 | it('exports["./features/"]', () => { 1011 | let pkg: Package = { 1012 | "name": "foobar", 1013 | "exports": { 1014 | "./features/": "./features/" 1015 | } 1016 | }; 1017 | 1018 | pass(pkg, './features/', 'features/'); 1019 | pass(pkg, './features/', 'foobar/features/'); 1020 | 1021 | pass(pkg, './features/hello.js', 'foobar/features/hello.js'); 1022 | 1023 | fail(pkg, './features', 'features'); 1024 | fail(pkg, './features', 'foobar/features'); 1025 | 1026 | fail(pkg, './package.json', 'package.json'); 1027 | fail(pkg, './package.json', 'foobar/package.json'); 1028 | fail(pkg, './package.json', './package.json'); 1029 | }); 1030 | 1031 | // https://nodejs.org/api/packages.html#packages_subpath_folder_mappings 1032 | it('exports["./features/"] :: with "./" key', () => { 1033 | let pkg: Package = { 1034 | "name": "foobar", 1035 | "exports": { 1036 | "./features/": "./features/", 1037 | "./package.json": "./package.json", 1038 | "./": "./" 1039 | } 1040 | }; 1041 | 1042 | pass(pkg, './features', 'features'); // via "./" 1043 | pass(pkg, './features', 'foobar/features'); // via "./" 1044 | 1045 | pass(pkg, './features/', 'features/'); // via "./features/" 1046 | pass(pkg, './features/', 'foobar/features/'); // via "./features/" 1047 | 1048 | pass(pkg, './features/hello.js', 'foobar/features/hello.js'); 1049 | 1050 | pass(pkg, './package.json', 'package.json'); 1051 | pass(pkg, './package.json', 'foobar/package.json'); 1052 | pass(pkg, './package.json', './package.json'); 1053 | 1054 | // Does NOT hit "./" (match Node) 1055 | fail(pkg, '.', '.'); 1056 | fail(pkg, '.', 'foobar'); 1057 | }); 1058 | 1059 | it('exports["./features/"] :: conditions', () => { 1060 | let pkg: Package = { 1061 | "name": "foobar", 1062 | "exports": { 1063 | "./features/": { 1064 | "browser": { 1065 | "import": "./browser.import/", 1066 | "require": "./browser.require/", 1067 | }, 1068 | "import": "./import/", 1069 | "require": "./require/", 1070 | }, 1071 | } 1072 | }; 1073 | 1074 | // import 1075 | pass(pkg, './import/', 'features/'); 1076 | pass(pkg, './import/', 'foobar/features/'); 1077 | 1078 | pass(pkg, './import/hello.js', './features/hello.js'); 1079 | pass(pkg, './import/hello.js', 'foobar/features/hello.js'); 1080 | 1081 | // require 1082 | pass(pkg, './require/', 'features/', { require: true }); 1083 | pass(pkg, './require/', 'foobar/features/', { require: true }); 1084 | 1085 | pass(pkg, './require/hello.js', './features/hello.js', { require: true }); 1086 | pass(pkg, './require/hello.js', 'foobar/features/hello.js', { require: true }); 1087 | 1088 | // require + browser 1089 | pass(pkg, './browser.require/', 'features/', { browser: true, require: true }); 1090 | pass(pkg, './browser.require/', 'foobar/features/', { browser: true, require: true }); 1091 | 1092 | pass(pkg, './browser.require/hello.js', './features/hello.js', { browser: true, require: true }); 1093 | pass(pkg, './browser.require/hello.js', 'foobar/features/hello.js', { browser: true, require: true }); 1094 | }); 1095 | 1096 | // https://nodejs.org/api/packages.html#packages_subpath_folder_mappings 1097 | it('exports["./features/*"]', () => { 1098 | let pkg: Package = { 1099 | "name": "foobar", 1100 | "exports": { 1101 | "./features/*": "./features/*.js", 1102 | } 1103 | }; 1104 | 1105 | fail(pkg, './features', 'features'); 1106 | fail(pkg, './features', 'foobar/features'); 1107 | 1108 | fail(pkg, './features/', 'features/'); 1109 | fail(pkg, './features/', 'foobar/features/'); 1110 | 1111 | pass(pkg, './features/a.js', 'foobar/features/a'); 1112 | pass(pkg, './features/ab.js', 'foobar/features/ab'); 1113 | pass(pkg, './features/abc.js', 'foobar/features/abc'); 1114 | 1115 | pass(pkg, './features/hello.js', 'foobar/features/hello'); 1116 | pass(pkg, './features/foo/bar.js', 'foobar/features/foo/bar'); 1117 | 1118 | // Valid: Pattern trailers allow any exact substrings to be matched 1119 | pass(pkg, './features/hello.js.js', 'foobar/features/hello.js'); 1120 | pass(pkg, './features/foo/bar.js.js', 'foobar/features/foo/bar.js'); 1121 | 1122 | fail(pkg, './package.json', 'package.json'); 1123 | fail(pkg, './package.json', 'foobar/package.json'); 1124 | fail(pkg, './package.json', './package.json'); 1125 | }); 1126 | 1127 | // https://nodejs.org/api/packages.html#packages_subpath_folder_mappings 1128 | it('exports["./features/*"] :: with "./" key', () => { 1129 | let pkg: Package = { 1130 | "name": "foobar", 1131 | "exports": { 1132 | "./features/*": "./features/*.js", 1133 | "./": "./" 1134 | } 1135 | }; 1136 | 1137 | pass(pkg, './features', 'features'); // via "./" 1138 | pass(pkg, './features', 'foobar/features'); // via "./" 1139 | 1140 | pass(pkg, './features/', 'features/'); // via "./" 1141 | pass(pkg, './features/', 'foobar/features/'); // via "./" 1142 | 1143 | pass(pkg, './features/hello.js', 'foobar/features/hello'); 1144 | pass(pkg, './features/foo/bar.js', 'foobar/features/foo/bar'); 1145 | 1146 | // Valid: Pattern trailers allow any exact substrings to be matched 1147 | pass(pkg, './features/hello.js.js', 'foobar/features/hello.js'); 1148 | pass(pkg, './features/foo/bar.js.js', 'foobar/features/foo/bar.js'); 1149 | 1150 | pass(pkg, './package.json', 'package.json'); 1151 | pass(pkg, './package.json', 'foobar/package.json'); 1152 | pass(pkg, './package.json', './package.json'); 1153 | 1154 | // Does NOT hit "./" (match Node) 1155 | fail(pkg, '.', '.'); 1156 | fail(pkg, '.', 'foobar'); 1157 | }); 1158 | 1159 | // https://github.com/lukeed/resolve.exports/issues/7 1160 | it('exports["./features/*"] :: with "./" key first', () => { 1161 | let pkg: Package = { 1162 | "name": "foobar", 1163 | "exports": { 1164 | "./": "./", 1165 | "./features/*": "./features/*.js" 1166 | } 1167 | }; 1168 | 1169 | pass(pkg, './features', 'features'); // via "./" 1170 | pass(pkg, './features', 'foobar/features'); // via "./" 1171 | 1172 | pass(pkg, './features/', 'features/'); // via "./" 1173 | pass(pkg, './features/', 'foobar/features/'); // via "./" 1174 | 1175 | pass(pkg, './features/hello.js', 'foobar/features/hello'); 1176 | pass(pkg, './features/foo/bar.js', 'foobar/features/foo/bar'); 1177 | 1178 | // Valid: Pattern trailers allow any exact substrings to be matched 1179 | pass(pkg, './features/hello.js.js', 'foobar/features/hello.js'); 1180 | pass(pkg, './features/foo/bar.js.js', 'foobar/features/foo/bar.js'); 1181 | 1182 | pass(pkg, './package.json', 'package.json'); 1183 | pass(pkg, './package.json', 'foobar/package.json'); 1184 | pass(pkg, './package.json', './package.json'); 1185 | 1186 | // Does NOT hit "./" (match Node) 1187 | fail(pkg, '.', '.'); 1188 | fail(pkg, '.', 'foobar'); 1189 | }); 1190 | 1191 | // https://github.com/lukeed/resolve.exports/issues/16 1192 | it('exports["./features/*"] :: with `null` internals', () => { 1193 | let pkg: Package = { 1194 | "name": "foobar", 1195 | "exports": { 1196 | "./features/*": "./src/features/*.js", 1197 | "./features/internal/*": null 1198 | } 1199 | }; 1200 | 1201 | pass(pkg, './src/features/hello.js', 'features/hello'); 1202 | pass(pkg, './src/features/hello.js', 'foobar/features/hello'); 1203 | 1204 | pass(pkg, './src/features/foo/bar.js', 'features/foo/bar'); 1205 | pass(pkg, './src/features/foo/bar.js', 'foobar/features/foo/bar'); 1206 | 1207 | // TODO? Native throws `ERR_PACKAGE_PATH_NOT_EXPORTED` 1208 | // Currently throwing `Missing "%s" specifier in "$s" package` 1209 | fail(pkg, './features/internal/hello', 'features/internal/hello'); 1210 | fail(pkg, './features/internal/foo/bar', 'features/internal/foo/bar'); 1211 | }); 1212 | 1213 | // https://github.com/lukeed/resolve.exports/issues/16 1214 | it('exports["./features/*"] :: with `null` internals first', () => { 1215 | let pkg: Package = { 1216 | "name": "foobar", 1217 | "exports": { 1218 | "./features/internal/*": null, 1219 | "./features/*": "./src/features/*.js", 1220 | } 1221 | }; 1222 | 1223 | pass(pkg, './src/features/hello.js', 'features/hello'); 1224 | pass(pkg, './src/features/hello.js', 'foobar/features/hello'); 1225 | 1226 | pass(pkg, './src/features/foo/bar.js', 'features/foo/bar'); 1227 | pass(pkg, './src/features/foo/bar.js', 'foobar/features/foo/bar'); 1228 | 1229 | // TODO? Native throws `ERR_PACKAGE_PATH_NOT_EXPORTED` 1230 | // Currently throwing `Missing "%s" specifier in "$s" package` 1231 | fail(pkg, './features/internal/hello', 'features/internal/hello'); 1232 | fail(pkg, './features/internal/foo/bar', 'features/internal/foo/bar'); 1233 | }); 1234 | 1235 | // https://nodejs.org/docs/latest-v18.x/api/packages.html#package-entry-points 1236 | it('exports["./features/*"] :: with "./features/*.js" key', () => { 1237 | let pkg: Package = { 1238 | "name": "foobar", 1239 | "exports": { 1240 | "./features/*": "./features/*.js", 1241 | "./features/*.js": "./features/*.js", 1242 | } 1243 | }; 1244 | 1245 | fail(pkg, './features', 'features'); 1246 | fail(pkg, './features', 'foobar/features'); 1247 | 1248 | fail(pkg, './features/', 'features/'); 1249 | fail(pkg, './features/', 'foobar/features/'); 1250 | 1251 | pass(pkg, './features/a.js', 'foobar/features/a'); 1252 | pass(pkg, './features/ab.js', 'foobar/features/ab'); 1253 | pass(pkg, './features/abc.js', 'foobar/features/abc'); 1254 | 1255 | pass(pkg, './features/hello.js', 'foobar/features/hello'); 1256 | pass(pkg, './features/hello.js', 'foobar/features/hello.js'); 1257 | 1258 | pass(pkg, './features/foo/bar.js', 'foobar/features/foo/bar'); 1259 | pass(pkg, './features/foo/bar.js', 'foobar/features/foo/bar.js'); 1260 | 1261 | fail(pkg, './package.json', 'package.json'); 1262 | fail(pkg, './package.json', 'foobar/package.json'); 1263 | fail(pkg, './package.json', './package.json'); 1264 | }); 1265 | 1266 | it('exports["./features/*"] :: conditions', () => { 1267 | let pkg: Package = { 1268 | "name": "foobar", 1269 | "exports": { 1270 | "./features/*": { 1271 | "browser": { 1272 | "import": "./browser.import/*.mjs", 1273 | "require": "./browser.require/*.js", 1274 | }, 1275 | "import": "./import/*.mjs", 1276 | "require": "./require/*.js", 1277 | }, 1278 | } 1279 | }; 1280 | 1281 | // import 1282 | fail(pkg, './features/', 'features/'); // no file 1283 | fail(pkg, './features/', 'foobar/features/'); // no file 1284 | 1285 | pass(pkg, './import/hello.mjs', './features/hello'); 1286 | pass(pkg, './import/hello.mjs', 'foobar/features/hello'); 1287 | 1288 | // require 1289 | fail(pkg, './features/', 'features/', { require: true }); // no file 1290 | fail(pkg, './features/', 'foobar/features/', { require: true }); // no file 1291 | 1292 | pass(pkg, './require/hello.js', './features/hello', { require: true }); 1293 | pass(pkg, './require/hello.js', 'foobar/features/hello', { require: true }); 1294 | 1295 | // require + browser 1296 | fail(pkg, './features/', 'features/', { browser: true, require: true }); // no file 1297 | fail(pkg, './features/', 'foobar/features/', { browser: true, require: true }); // no file 1298 | 1299 | pass(pkg, './browser.require/hello.js', './features/hello', { browser: true, require: true }); 1300 | pass(pkg, './browser.require/hello.js', 'foobar/features/hello', { browser: true, require: true }); 1301 | }); 1302 | 1303 | it('should handle mixed path/conditions', () => { 1304 | let pkg: Package = { 1305 | "name": "foobar", 1306 | "exports": { 1307 | ".": [ 1308 | { 1309 | "import": "./$root.import", 1310 | }, 1311 | "./$root.string" 1312 | ], 1313 | "./foo": [ 1314 | { 1315 | "require": "./$foo.require" 1316 | }, 1317 | "./$foo.string" 1318 | ] 1319 | } 1320 | } 1321 | 1322 | pass(pkg, ['./$root.import', './$root.string']); 1323 | pass(pkg, ['./$root.import', './$root.string'], 'foobar'); 1324 | 1325 | // TODO? if len==1 then single? 1326 | pass(pkg, ['./$foo.string'], 'foo'); 1327 | pass(pkg, ['./$foo.string'], 'foobar/foo'); 1328 | pass(pkg, ['./$foo.string'], './foo'); 1329 | 1330 | pass(pkg, ['./$foo.require', './$foo.string'], 'foo', { require: true }); 1331 | pass(pkg, ['./$foo.require', './$foo.string'], 'foobar/foo', { require: true }); 1332 | pass(pkg, ['./$foo.require', './$foo.string'], './foo', { require: true }); 1333 | }); 1334 | 1335 | it('should handle file with leading dot', () => { 1336 | let pkg: Package = { 1337 | "version": "2.41.0", 1338 | "name": "aws-cdk-lib", 1339 | "exports": { 1340 | ".": "./index.js", 1341 | "./package.json": "./package.json", 1342 | "./.jsii": "./.jsii", 1343 | "./.warnings.jsii.js": "./.warnings.jsii.js", 1344 | "./alexa-ask": "./alexa-ask/index.js" 1345 | } 1346 | }; 1347 | 1348 | pass(pkg, "./.warnings.jsii.js", ".warnings.jsii.js"); 1349 | }); 1350 | }); 1351 | 1352 | describe('options.requires', it => { 1353 | let pkg: Package = { 1354 | "name": "r", 1355 | "exports": { 1356 | "require": "./$require", 1357 | "import": "./$import", 1358 | } 1359 | }; 1360 | 1361 | it('should ignore "require" keys by default', () => { 1362 | pass(pkg, './$import'); 1363 | }); 1364 | 1365 | it('should use "require" key when defined first', () => { 1366 | pass(pkg, './$require', '.', { require: true }); 1367 | }); 1368 | 1369 | it('should ignore "import" key when enabled', () => { 1370 | let pkg: Package = { 1371 | "name": "r", 1372 | "exports": { 1373 | "import": "./$import", 1374 | "require": "./$require", 1375 | } 1376 | }; 1377 | pass(pkg, './$require', '.', { require: true }); 1378 | pass(pkg, './$import', '.'); 1379 | }); 1380 | 1381 | it('should match "default" if "require" is after', () => { 1382 | let pkg: Package = { 1383 | "name": "r", 1384 | "exports": { 1385 | "default": "./$default", 1386 | "require": "./$require", 1387 | } 1388 | }; 1389 | pass(pkg, './$default', '.', { require: true }); 1390 | }); 1391 | }); 1392 | 1393 | describe('options.browser', it => { 1394 | let pkg: Package = { 1395 | "name": "b", 1396 | "exports": { 1397 | "browser": "./$browser", 1398 | "node": "./$node", 1399 | } 1400 | }; 1401 | 1402 | it('should ignore "browser" keys by default', () => { 1403 | pass(pkg, './$node'); 1404 | }); 1405 | 1406 | it('should use "browser" key when defined first', () => { 1407 | pass(pkg, './$browser', '.', { browser: true }); 1408 | }); 1409 | 1410 | it('should ignore "node" key when enabled', () => { 1411 | let pkg: Package = { 1412 | "name": "b", 1413 | "exports": { 1414 | "node": "./$node", 1415 | "import": "./$import", 1416 | "browser": "./$browser", 1417 | } 1418 | }; 1419 | 1420 | // import defined before browser 1421 | pass(pkg, './$import', '.', { browser: true }); 1422 | }); 1423 | }); 1424 | 1425 | describe('options.conditions', it => { 1426 | const pkg: Package = { 1427 | "name": "c", 1428 | "exports": { 1429 | "production": "./$prod", 1430 | "development": "./$dev", 1431 | "default": "./$default", 1432 | } 1433 | }; 1434 | 1435 | it('should ignore unknown conditions by default', () => { 1436 | pass(pkg, './$default'); 1437 | }); 1438 | 1439 | it('should recognize custom field(s) when specified', () => { 1440 | pass(pkg, './$dev', '.', { 1441 | conditions: ['development'] 1442 | }); 1443 | 1444 | pass(pkg, './$prod', '.', { 1445 | conditions: ['development', 'production'] 1446 | }); 1447 | }); 1448 | 1449 | it('should throw an error if no known conditions', () => { 1450 | let ctx: Package = { 1451 | "name": "hello", 1452 | "exports": { 1453 | // @ts-ignore 1454 | ...pkg.exports 1455 | }, 1456 | }; 1457 | 1458 | // @ts-ignore 1459 | delete ctx.exports.default; 1460 | 1461 | try { 1462 | lib.resolve(ctx); 1463 | assert.unreachable(); 1464 | } catch (err) { 1465 | assert.instance(err, Error); 1466 | assert.is((err as Error).message, `No known conditions for "." specifier in "hello" package`); 1467 | } 1468 | }); 1469 | }); 1470 | 1471 | describe('options.unsafe', it => { 1472 | let pkg: Package = { 1473 | "name": "unsafe", 1474 | "exports": { 1475 | ".": { 1476 | "production": "./$prod", 1477 | "development": "./$dev", 1478 | "default": "./$default", 1479 | }, 1480 | "./spec/type": { 1481 | "import": "./$import", 1482 | "require": "./$require", 1483 | "default": "./$default" 1484 | }, 1485 | "./spec/env": { 1486 | "worker": { 1487 | "default": "./$worker" 1488 | }, 1489 | "browser": "./$browser", 1490 | "node": "./$node", 1491 | "default": "./$default" 1492 | } 1493 | } 1494 | }; 1495 | 1496 | it('should ignore unknown conditions by default', () => { 1497 | pass(pkg, './$default', '.', { 1498 | unsafe: true, 1499 | }); 1500 | }); 1501 | 1502 | it('should ignore "import" and "require" conditions by default', () => { 1503 | pass(pkg, './$default', './spec/type', { 1504 | unsafe: true, 1505 | }); 1506 | 1507 | pass(pkg, './$default', './spec/type', { 1508 | unsafe: true, 1509 | require: true, 1510 | }); 1511 | }); 1512 | 1513 | it('should ignore "node" and "browser" conditions by default', () => { 1514 | pass(pkg, './$default', './spec/type', { 1515 | unsafe: true, 1516 | }); 1517 | 1518 | pass(pkg, './$default', './spec/type', { 1519 | unsafe: true, 1520 | browser: true, 1521 | }); 1522 | }); 1523 | 1524 | it('should respect/accept any custom condition(s) when specified', () => { 1525 | // root, dev only 1526 | pass(pkg, './$dev', '.', { 1527 | unsafe: true, 1528 | conditions: ['development'] 1529 | }); 1530 | 1531 | // root, defined order 1532 | pass(pkg, './$prod', '.', { 1533 | unsafe: true, 1534 | conditions: ['development', 'production'] 1535 | }); 1536 | 1537 | // import vs require, defined order 1538 | pass(pkg, './$require', './spec/type', { 1539 | unsafe: true, 1540 | conditions: ['require'] 1541 | }); 1542 | 1543 | // import vs require, defined order 1544 | pass(pkg, './$import', './spec/type', { 1545 | unsafe: true, 1546 | conditions: ['import', 'require'] 1547 | }); 1548 | 1549 | // import vs require, defined order 1550 | pass(pkg, './$node', './spec/env', { 1551 | unsafe: true, 1552 | conditions: ['node'] 1553 | }); 1554 | 1555 | // import vs require, defined order 1556 | pass(pkg, './$browser', './spec/env', { 1557 | unsafe: true, 1558 | conditions: ['browser', 'node'] 1559 | }); 1560 | 1561 | // import vs require, defined order 1562 | pass(pkg, './$worker', './spec/env', { 1563 | unsafe: true, 1564 | conditions: ['browser', 'node', 'worker'] 1565 | }); 1566 | }); 1567 | }); 1568 | --------------------------------------------------------------------------------