From ae975b75a0738273e18e78356941d9433dd430dd Mon Sep 17 00:00:00 2001 From: Luke Edwards Date: Wed, 11 Jan 2023 14:57:17 -0800 Subject: [PATCH 01/35] rename file --- src/{index.js => index.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/{index.js => index.ts} (100%) diff --git a/src/index.js b/src/index.ts similarity index 100% rename from src/index.js rename to src/index.ts From 2f6ca63b6aae98a1400358e6dda3944788a9426e Mon Sep 17 00:00:00 2001 From: Luke Edwards Date: Wed, 11 Jan 2023 17:17:51 -0800 Subject: [PATCH 02/35] chore: convert to typescript --- index.d.ts | 5 +- package.json | 8 +- src/index.ts | 100 ++++++---- test/{legacy.js => legacy.ts} | 28 +-- test/{resolve.js => resolve.ts} | 311 +++++++++++++++++--------------- 5 files changed, 244 insertions(+), 208 deletions(-) rename test/{legacy.js => legacy.ts} (91%) rename test/{resolve.js => resolve.ts} (77%) diff --git a/index.d.ts b/index.d.ts index 8fb9ec4..ff8b5d7 100644 --- a/index.d.ts +++ b/index.d.ts @@ -2,10 +2,7 @@ export type Options = { browser?: boolean; conditions?: readonly string[]; require?: boolean; - unsafe?: false; -} | { - conditions?: readonly string[]; - unsafe?: true; + unsafe?: boolean; } export function resolve(pkg: T, entry: string, options?: Options): string | void; diff --git a/package.json b/package.json index 45b2011..3530742 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ }, "scripts": { "build": "bundt", - "test": "uvu -r esm test" + "test": "uvu -r tsm test" }, "files": [ "*.d.ts", @@ -41,8 +41,8 @@ "resolve" ], "devDependencies": { - "bundt": "1.1.2", - "esm": "3.2.25", - "uvu": "0.5.1" + "bundt": "next", + "tsm": "2.3.0", + "uvu": "0.5.4" } } diff --git a/src/index.ts b/src/index.ts index b27095d..95b197b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,14 +1,47 @@ -/** - * @param {object} exports - * @param {Set} keys - */ -function loop(exports, keys) { +import type { Options } from '../index.d'; + +type Condition = string; + +export type Entry = `#${string}`; // imports + +export type Path = `.${string}`; +export type Subpath = `./${string}`; + +export type Imports = { + [entry: Entry]: Value | null; +} + +export type Exports = Subpath | { + [path: Path]: Value | null; + [cond: Condition]: Value | null; +}; + +export type Value = { + [c: Condition]: Value | null; +} | Subpath | Value[]; + +export type Browser = string[] | { + [file: Subpath | string]: string | false; +} + +export type Package = { + name: string; + version?: string; + module?: string; + main?: string; + imports?: Imports; + exports?: Exports; + browser?: Browser | string; + [key: string]: any; +}; + +function loop(exports: Exports | Value[] | null, keys: Set): Subpath|void { if (typeof exports === 'string') { return exports; } if (exports) { - let idx, tmp; + let idx: number | string, tmp: File|Value|void; if (Array.isArray(exports)) { for (idx=0; idx < exports.length; idx++) { if (tmp = loop(exports[idx], keys)) return tmp; @@ -24,11 +57,11 @@ function loop(exports, keys) { } /** - * @param {string} name The package name - * @param {string} entry The target entry, eg "." - * @param {number} [condition] Unmatched condition? + * @param name The package name + * @param entry The target entry, eg "." + * @param condition Unmatched condition? */ -function bail(name, entry, condition) { +function bail(name: string, entry: Path, condition?: number): never { throw new Error( condition ? `No known conditions for "${entry}" entry in "${name}" package` @@ -37,33 +70,26 @@ function bail(name, entry, condition) { } /** - * @param {string} name the package name - * @param {string} entry the target path/import + * @param name the package name + * @param entry the target path/import */ -function toName(name, entry) { - return entry === name ? '.' +function toName(name: string, entry: string): Path { + return ( + entry === name ? '.' : entry[0] === '.' ? entry - : entry.replace(new RegExp('^' + name + '\/'), './'); + : entry.replace(new RegExp('^' + name + '\/'), './') + ) as Path; } -/** - * @param {object} pkg package.json contents - * @param {string} [entry] entry name or import path - * @param {object} [options] - * @param {boolean} [options.browser] - * @param {boolean} [options.require] - * @param {string[]} [options.conditions] - * @param {boolean} [options.unsafe] - */ -export function resolve(pkg, entry='.', options={}) { +export function resolve(pkg: Package, entry = '.', options?: Options) { let { name, exports } = pkg; if (exports) { - let { browser, require, unsafe, conditions=[] } = options; + let { browser, require, unsafe, conditions=[] } = options || {}; let target = toName(name, entry); if (target !== '.' && !target.startsWith('./')) { - target = './' + target; // ".ini" => "./.ini" + target = './' + target as Subpath; // ".ini" => "./.ini" } if (typeof exports === 'string') { @@ -128,14 +154,14 @@ export function resolve(pkg, entry='.', options={}) { } } -/** - * @param {object} pkg - * @param {object} [options] - * @param {string|boolean} [options.browser] - * @param {string[]} [options.fields] - */ -export function legacy(pkg, options={}) { - let i=0, value, +type LegacyOptions = { + fields?: string[]; + browser?: string | boolean; +} + +export function legacy(pkg: Package, options: LegacyOptions = {}): Subpath | Browser | void { + let i=0, + value: Package['module' | 'main' | 'browser'], browser = options.browser, fields = options.fields || ['module', 'main']; @@ -150,14 +176,14 @@ export function legacy(pkg, options={}) { } else if (typeof value == 'object' && fields[i] == 'browser') { if (typeof browser == 'string') { value = value[browser=toName(pkg.name, browser)]; - if (value == null) return browser; + if (value == null) return browser as Subpath; } } else { continue; } return typeof value == 'string' - ? ('./' + value.replace(/^\.?\//, '')) + ? ('./' + value.replace(/^\.?\//, '')) as Subpath : value; } } diff --git a/test/legacy.js b/test/legacy.ts similarity index 91% rename from test/legacy.js rename to test/legacy.ts index 224c539..dc9117d 100644 --- a/test/legacy.js +++ b/test/legacy.ts @@ -2,6 +2,8 @@ import { suite } from 'uvu'; import * as assert from 'uvu/assert'; import * as $exports from '../src'; +import type { Package } from '../src'; + const legacy = suite('$.legacy'); legacy('should be a function', () => { @@ -9,7 +11,7 @@ legacy('should be a function', () => { }); legacy('should prefer "module" > "main" entry', () => { - let pkg = { + let pkg: Package = { "name": "foobar", "module": "build/module.js", "main": "build/main.js", @@ -20,7 +22,7 @@ legacy('should prefer "module" > "main" entry', () => { }); legacy('should read "main" field', () => { - let pkg = { + let pkg: Package = { "name": "foobar", "main": "build/main.js", }; @@ -30,7 +32,7 @@ legacy('should read "main" field', () => { }); legacy('should return nothing when no fields', () => { - let pkg = { + let pkg: Package = { "name": "foobar" }; @@ -44,7 +46,7 @@ legacy('should ignore boolean-type field values', () => { "main": "main.js" }; - let output = $exports.legacy(pkg); + let output = $exports.legacy(pkg as any); assert.is(output, './main.js'); }); @@ -52,7 +54,7 @@ legacy.run(); // --- -const fields = suite('options.fields', { +const fields = suite('options.fields', { "name": "foobar", "module": "build/module.js", "browser": "build/browser.js", @@ -83,7 +85,7 @@ fields.run(); // --- -const browser = suite('options.browser', { +const browser = suite('options.browser', { "name": "foobar", "module": "build/module.js", "browser": "build/browser.js", @@ -110,7 +112,7 @@ browser('should respect existing "browser" order in custom fields', pkg => { // https://github.com/defunctzombie/package-browser-field-spec browser('should resolve object format', () => { - let pkg = { + let pkg: Package = { "name": "foobar", "browser": { "module-a": "./shims/module-a.js", @@ -135,7 +137,7 @@ browser('should resolve object format', () => { }); browser('should allow object format to "ignore" modules/files :: string', () => { - let pkg = { + let pkg: Package = { "name": "foobar", "browser": { "module-a": false, @@ -160,7 +162,7 @@ browser('should allow object format to "ignore" modules/files :: string', () => }); browser('should return the `browser` string (entry) if no custom mapping :: string', () => { - let pkg = { + let pkg: Package = { "name": "foobar", "browser": { // @@ -183,7 +185,7 @@ browser('should return the `browser` string (entry) if no custom mapping :: stri }); browser('should return the full "browser" object :: true', () => { - let pkg = { + let pkg: Package = { "name": "foobar", "browser": { "./other.js": "./world.js" @@ -198,12 +200,12 @@ browser('should return the full "browser" object :: true', () => { }); browser('still ensures string output is made relative', () => { - let pkg = { + let pkg: Package = { "name": "foobar", "browser": { - "./foo.js": 'bar.js', + "./foo.js": "bar.js", } - }; + } as any; assert.is( $exports.legacy(pkg, { diff --git a/test/resolve.js b/test/resolve.ts similarity index 77% rename from test/resolve.js rename to test/resolve.ts index 0cb2963..24c1775 100644 --- a/test/resolve.js +++ b/test/resolve.ts @@ -2,14 +2,17 @@ import { suite } from 'uvu'; import * as assert from 'uvu/assert'; import * as $exports from '../src'; -function pass(pkg, expects, ...args) { - let out = $exports.resolve(pkg, ...args); +import type { Package, Path } from '../src'; +import type { Options } from '../index.d'; + +function pass(pkg: Package, expects: Path, entry?: string, options?: Options) { + let out = $exports.resolve(pkg, entry, options); assert.is(out, expects); } -function fail(pkg, target, ...args) { +function fail(pkg: Package, target: Path, entry?: string, options?: Options) { try { - $exports.resolve(pkg, ...args); + $exports.resolve(pkg, entry, options); assert.unreachable(); } catch (err) { assert.instance(err, Error); @@ -26,14 +29,14 @@ resolve('should be a function', () => { }); resolve('exports=string', () => { - let pkg = { + let pkg: Package = { "name": "foobar", - "exports": "$string", + "exports": "./$string", }; - pass(pkg, '$string'); - pass(pkg, '$string', '.'); - pass(pkg, '$string', 'foobar'); + pass(pkg, './$string'); + pass(pkg, './$string', '.'); + pass(pkg, './$string', 'foobar'); fail(pkg, './other', 'other'); fail(pkg, './other', 'foobar/other'); @@ -41,17 +44,17 @@ resolve('exports=string', () => { }); resolve('exports = { self }', () => { - let pkg = { + let pkg: Package = { "name": "foobar", "exports": { - "import": "$import", - "require": "$require", + "import": "./$import", + "require": "./$require", } }; - pass(pkg, '$import'); - pass(pkg, '$import', '.'); - pass(pkg, '$import', 'foobar'); + pass(pkg, './$import'); + pass(pkg, './$import', '.'); + pass(pkg, './$import', 'foobar'); fail(pkg, './other', 'other'); fail(pkg, './other', 'foobar/other'); @@ -59,16 +62,16 @@ resolve('exports = { self }', () => { }); resolve('exports["."] = string', () => { - let pkg = { + let pkg: Package = { "name": "foobar", "exports": { - ".": "$self", + ".": "./$self", } }; - pass(pkg, '$self'); - pass(pkg, '$self', '.'); - pass(pkg, '$self', 'foobar'); + pass(pkg, './$self'); + pass(pkg, './$self', '.'); + pass(pkg, './$self', 'foobar'); fail(pkg, './other', 'other'); fail(pkg, './other', 'foobar/other'); @@ -76,19 +79,19 @@ resolve('exports["."] = string', () => { }); resolve('exports["."] = object', () => { - let pkg = { + let pkg: Package = { "name": "foobar", "exports": { ".": { - "import": "$import", - "require": "$require", + "import": "./$import", + "require": "./$require", } } }; - pass(pkg, '$import'); - pass(pkg, '$import', '.'); - pass(pkg, '$import', 'foobar'); + pass(pkg, './$import'); + pass(pkg, './$import', '.'); + pass(pkg, './$import', 'foobar'); fail(pkg, './other', 'other'); fail(pkg, './other', 'foobar/other'); @@ -96,15 +99,15 @@ resolve('exports["."] = object', () => { }); resolve('exports["./foo"] = string', () => { - let pkg = { + let pkg: Package = { "name": "foobar", "exports": { - "./foo": "$import", + "./foo": "./$import", } }; - pass(pkg, '$import', './foo'); - pass(pkg, '$import', 'foobar/foo'); + pass(pkg, './$import', './foo'); + pass(pkg, './$import', 'foobar/foo'); fail(pkg, '.'); fail(pkg, '.', 'foobar'); @@ -112,18 +115,18 @@ resolve('exports["./foo"] = string', () => { }); resolve('exports["./foo"] = object', () => { - let pkg = { + let pkg: Package = { "name": "foobar", "exports": { "./foo": { - "import": "$import", - "require": "$require", + "import": "./$import", + "require": "./$require", } } }; - pass(pkg, '$import', './foo'); - pass(pkg, '$import', 'foobar/foo'); + pass(pkg, './$import', './foo'); + pass(pkg, './$import', 'foobar/foo'); fail(pkg, '.'); fail(pkg, '.', 'foobar'); @@ -132,23 +135,23 @@ resolve('exports["./foo"] = object', () => { // https://nodejs.org/api/packages.html#packages_nested_conditions resolve('nested conditions', () => { - let pkg = { + let pkg: Package = { "name": "foobar", "exports": { "node": { - "import": "$node.import", - "require": "$node.require" + "import": "././$node.import", + "require": "././$node.require" }, - "default": "$default", + "default": "./$default", } }; - pass(pkg, '$node.import'); - pass(pkg, '$node.import', 'foobar'); + pass(pkg, '././$node.import'); + pass(pkg, '././$node.import', 'foobar'); // browser => no "node" key - pass(pkg, '$default', '.', { browser: true }); - pass(pkg, '$default', 'foobar', { browser: true }); + pass(pkg, './$default', '.', { browser: true }); + pass(pkg, './$default', 'foobar', { browser: true }); fail(pkg, './hello', './hello'); fail(pkg, './other', 'foobar/other'); @@ -156,70 +159,70 @@ resolve('nested conditions', () => { }); resolve('nested conditions :: subpath', () => { - let pkg = { + let pkg: Package = { "name": "foobar", "exports": { "./lite": { "node": { - "import": "$node.import", - "require": "$node.require" + "import": "././$node.import", + "require": "././$node.require" }, "browser": { - "import": "$browser.import", - "require": "$browser.require" + "import": "././$browser.import", + "require": "././$browser.require" }, } } }; - pass(pkg, '$node.import', 'foobar/lite'); - pass(pkg, '$node.require', 'foobar/lite', { require: true }); + pass(pkg, '././$node.import', 'foobar/lite'); + pass(pkg, '././$node.require', 'foobar/lite', { require: true }); - pass(pkg, '$browser.import', 'foobar/lite', { browser: true }); - pass(pkg, '$browser.require', 'foobar/lite', { browser: true, require: true }); + pass(pkg, '././$browser.import', 'foobar/lite', { browser: true }); + pass(pkg, '././$browser.require', 'foobar/lite', { browser: true, require: true }); }); resolve('nested conditions :: subpath :: inverse', () => { - let pkg = { + let pkg: Package = { "name": "foobar", "exports": { "./lite": { "import": { - "browser": "$browser.import", - "node": "$node.import", + "browser": "././$browser.import", + "node": "././$node.import", }, "require": { - "browser": "$browser.require", - "node": "$node.require", + "browser": "././$browser.require", + "node": "././$node.require", } } } }; - pass(pkg, '$node.import', 'foobar/lite'); - pass(pkg, '$node.require', 'foobar/lite', { require: true }); + pass(pkg, '././$node.import', 'foobar/lite'); + pass(pkg, '././$node.require', 'foobar/lite', { require: true }); - pass(pkg, '$browser.import', 'foobar/lite', { browser: true }); - pass(pkg, '$browser.require', 'foobar/lite', { browser: true, require: true }); + pass(pkg, '././$browser.import', 'foobar/lite', { browser: true }); + pass(pkg, '././$browser.require', 'foobar/lite', { browser: true, require: true }); }); // https://nodejs.org/api/packages.html#packages_subpath_folder_mappings resolve('exports["./"]', () => { - let pkg = { + let pkg: Package = { "name": "foobar", "exports": { ".": { - "require": "$require", - "import": "$import" + "require": "./$require", + "import": "./$import" }, "./package.json": "./package.json", "./": "./" } }; - pass(pkg, '$import'); - pass(pkg, '$import', 'foobar'); - pass(pkg, '$require', 'foobar', { require: true }); + pass(pkg, './$import'); + pass(pkg, './$import', 'foobar'); + pass(pkg, './$require', 'foobar', { require: true }); pass(pkg, './package.json', 'package.json'); pass(pkg, './package.json', 'foobar/package.json'); @@ -232,7 +235,7 @@ resolve('exports["./"]', () => { }); resolve('exports["./"] :: w/o "." key', () => { - let pkg = { + let pkg: Package = { "name": "foobar", "exports": { "./package.json": "./package.json", @@ -255,7 +258,7 @@ resolve('exports["./"] :: w/o "." key', () => { // https://nodejs.org/api/packages.html#packages_subpath_folder_mappings resolve('exports["./*"]', () => { - let pkg = { + let pkg: Package = { "name": "foobar", "exports": { "./*": "./cheese/*.mjs" @@ -276,7 +279,7 @@ resolve('exports["./*"]', () => { }); resolve('exports["./dir*"]', () => { - let pkg = { + let pkg: Package = { "name": "foobar", "exports": { "./dir*": "./cheese/*.mjs" @@ -295,7 +298,7 @@ resolve('exports["./dir*"]', () => { // https://github.com/lukeed/resolve.exports/issues/9 resolve('exports["./dir*"] :: repeat "*" value', () => { - let pkg = { + let pkg: Package = { "name": "foobar", "exports": { "./dir*": "./*sub/dir*/file.js" @@ -313,7 +316,7 @@ resolve('exports["./dir*"] :: repeat "*" value', () => { }); resolve('exports["./dir*"] :: share "name" start', () => { - let pkg = { + let pkg: Package = { "name": "director", "exports": { "./dir*": "./*sub/dir*/file.js" @@ -337,7 +340,7 @@ resolve('exports["./dir*"] :: share "name" start', () => { * @see https://nodejs.org/docs/latest-v16.x/api/packages.html#subpath-folder-mappings */ resolve('exports["./features/"]', () => { - let pkg = { + let pkg: Package = { "name": "foobar", "exports": { "./features/": "./features/" @@ -359,7 +362,7 @@ resolve('exports["./features/"]', () => { // https://nodejs.org/api/packages.html#packages_subpath_folder_mappings resolve('exports["./features/"] :: with "./" key', () => { - let pkg = { + let pkg: Package = { "name": "foobar", "exports": { "./features/": "./features/", @@ -386,7 +389,7 @@ resolve('exports["./features/"] :: with "./" key', () => { }); resolve('exports["./features/"] :: conditions', () => { - let pkg = { + let pkg: Package = { "name": "foobar", "exports": { "./features/": { @@ -424,7 +427,7 @@ resolve('exports["./features/"] :: conditions', () => { // https://nodejs.org/api/packages.html#packages_subpath_folder_mappings resolve('exports["./features/*"]', () => { - let pkg = { + let pkg: Package = { "name": "foobar", "exports": { "./features/*": "./features/*.js", @@ -455,7 +458,7 @@ resolve('exports["./features/*"]', () => { // https://nodejs.org/api/packages.html#packages_subpath_folder_mappings resolve('exports["./features/*"] :: with "./" key', () => { - let pkg = { + let pkg: Package = { "name": "foobar", "exports": { "./features/*": "./features/*.js", @@ -487,7 +490,7 @@ resolve('exports["./features/*"] :: with "./" key', () => { // https://github.com/lukeed/resolve.exports/issues/7 resolve('exports["./features/*"] :: with "./" key first', () => { - let pkg = { + let pkg: Package = { "name": "foobar", "exports": { "./": "./", @@ -519,7 +522,7 @@ resolve('exports["./features/*"] :: with "./" key first', () => { // https://github.com/lukeed/resolve.exports/issues/16 resolve('exports["./features/*"] :: with `null` internals', () => { - let pkg = { + let pkg: Package = { "name": "foobar", "exports": { "./features/*": "./src/features/*.js", @@ -541,7 +544,7 @@ resolve('exports["./features/*"] :: with `null` internals', () => { // https://github.com/lukeed/resolve.exports/issues/16 resolve('exports["./features/*"] :: with `null` internals first', () => { - let pkg = { + let pkg: Package = { "name": "foobar", "exports": { "./features/internal/*": null, @@ -563,7 +566,7 @@ resolve('exports["./features/*"] :: with `null` internals first', () => { // https://nodejs.org/docs/latest-v18.x/api/packages.html#package-entry-points resolve('exports["./features/*"] :: with "./features/*.js" key', () => { - let pkg = { + let pkg: Package = { "name": "foobar", "exports": { "./features/*": "./features/*.js", @@ -593,7 +596,7 @@ resolve('exports["./features/*"] :: with "./features/*.js" key', () => { }); resolve('exports["./features/*"] :: conditions', () => { - let pkg = { + let pkg: Package = { "name": "foobar", "exports": { "./features/*": { @@ -630,38 +633,38 @@ resolve('exports["./features/*"] :: conditions', () => { }); resolve('should handle mixed path/conditions', () => { - let pkg = { + let pkg: Package = { "name": "foobar", "exports": { ".": [ { - "import": "$root.import", + "import": "./$root.import", }, - "$root.string" + "./$root.string" ], "./foo": [ { - "require": "$foo.require" + "require": "./$foo.require" }, - "$foo.string" + "./$foo.string" ] } } - pass(pkg, '$root.import'); - pass(pkg, '$root.import', 'foobar'); + pass(pkg, './$root.import'); + pass(pkg, './$root.import', 'foobar'); - pass(pkg, '$foo.string', 'foo'); - pass(pkg, '$foo.string', 'foobar/foo'); - pass(pkg, '$foo.string', './foo'); + pass(pkg, './$foo.string', 'foo'); + pass(pkg, './$foo.string', 'foobar/foo'); + pass(pkg, './$foo.string', './foo'); - pass(pkg, '$foo.require', 'foo', { require: true }); - pass(pkg, '$foo.require', 'foobar/foo', { require: true }); - pass(pkg, '$foo.require', './foo', { require: true }); + pass(pkg, './$foo.require', 'foo', { require: true }); + pass(pkg, './$foo.require', 'foobar/foo', { require: true }); + pass(pkg, './$foo.require', './foo', { require: true }); }); resolve('should handle file with leading dot', () => { - let pkg = { + let pkg: Package = { "version": "2.41.0", "name": "aws-cdk-lib", "exports": { @@ -680,95 +683,101 @@ resolve.run(); // --- -const requires = suite('options.requires', { +const requires = suite('options.requires', { + "name": "r", "exports": { - "require": "$require", - "import": "$import", + "require": "./$require", + "import": "./$import", } }); requires('should ignore "require" keys by default', pkg => { - pass(pkg, '$import'); + pass(pkg, './$import'); }); requires('should use "require" key when defined first', pkg => { - pass(pkg, '$require', '.', { require: true }); + pass(pkg, './$require', '.', { require: true }); }); requires('should ignore "import" key when enabled', () => { - let pkg = { + let pkg: Package = { + "name": "r", "exports": { - "import": "$import", - "require": "$require", + "import": "./$import", + "require": "./$require", } }; - pass(pkg, '$require', '.', { require: true }); - pass(pkg, '$import', '.'); + pass(pkg, './$require', '.', { require: true }); + pass(pkg, './$import', '.'); }); requires('should match "default" if "require" is after', () => { - let pkg = { + let pkg: Package = { + "name": "r", "exports": { - "default": "$default", - "require": "$require", + "default": "./$default", + "require": "./$require", } }; - pass(pkg, '$default', '.', { require: true }); + pass(pkg, './$default', '.', { require: true }); }); requires.run(); // --- -const browser = suite('options.browser', { +const browser = suite('options.browser', { + "name": "b", "exports": { - "browser": "$browser", - "node": "$node", + "browser": "./$browser", + "node": "./$node", } }); browser('should ignore "browser" keys by default', pkg => { - pass(pkg, '$node'); + pass(pkg, './$node'); }); browser('should use "browser" key when defined first', pkg => { - pass(pkg, '$browser', '.', { browser: true }); + pass(pkg, './$browser', '.', { browser: true }); }); browser('should ignore "node" key when enabled', () => { - let pkg = { + let pkg: Package = { + "name": "b", "exports": { - "node": "$node", - "import": "$import", - "browser": "$browser", + "node": "./$node", + "import": "./$import", + "browser": "./$browser", } }; // import defined before browser - pass(pkg, '$import', '.', { browser: true }); + pass(pkg, './$import', '.', { browser: true }); }); browser.run(); // --- -const conditions = suite('options.conditions', { +const conditions = suite('options.conditions', { + "name": "c", "exports": { - "production": "$prod", - "development": "$dev", - "default": "$default", + "production": "./$prod", + "development": "./$dev", + "default": "./$default", } }); conditions('should ignore unknown conditions by default', pkg => { - pass(pkg, '$default'); + pass(pkg, './$default'); }); conditions('should recognize custom field(s) when specified', pkg => { - pass(pkg, '$dev', '.', { + pass(pkg, './$dev', '.', { conditions: ['development'] }); - pass(pkg, '$prod', '.', { + pass(pkg, './$prod', '.', { conditions: ['development', 'production'] }); }); @@ -777,6 +786,7 @@ conditions('should throw an error if no known conditions', ctx => { let pkg = { "name": "hello", "exports": { + // @ts-ignore ...ctx.exports }, }; @@ -796,52 +806,53 @@ conditions.run(); // --- -const unsafe = suite('options.unsafe', { +const unsafe = suite('options.unsafe', { + "name": "unsafe", "exports": { ".": { - "production": "$prod", - "development": "$dev", - "default": "$default", + "production": "./$prod", + "development": "./$dev", + "default": "./$default", }, "./spec/type": { - "import": "$import", - "require": "$require", - "default": "$default" + "import": "./$import", + "require": "./$require", + "default": "./$default" }, "./spec/env": { "worker": { - "default": "$worker" + "default": "./$worker" }, - "browser": "$browser", - "node": "$node", - "default": "$default" + "browser": "./$browser", + "node": "./$node", + "default": "./$default" } } }); unsafe('should ignore unknown conditions by default', pkg => { - pass(pkg, '$default', '.', { + pass(pkg, './$default', '.', { unsafe: true, }); }); unsafe('should ignore "import" and "require" conditions by default', pkg => { - pass(pkg, '$default', './spec/type', { + pass(pkg, './$default', './spec/type', { unsafe: true, }); - pass(pkg, '$default', './spec/type', { + pass(pkg, './$default', './spec/type', { unsafe: true, require: true, }); }); unsafe('should ignore "node" and "browser" conditions by default', pkg => { - pass(pkg, '$default', './spec/type', { + pass(pkg, './$default', './spec/type', { unsafe: true, }); - pass(pkg, '$default', './spec/type', { + pass(pkg, './$default', './spec/type', { unsafe: true, browser: true, }); @@ -849,43 +860,43 @@ unsafe('should ignore "node" and "browser" conditions by default', pkg => { unsafe('should respect/accept any custom condition(s) when specified', pkg => { // root, dev only - pass(pkg, '$dev', '.', { + pass(pkg, './$dev', '.', { unsafe: true, conditions: ['development'] }); // root, defined order - pass(pkg, '$prod', '.', { + pass(pkg, './$prod', '.', { unsafe: true, conditions: ['development', 'production'] }); // import vs require, defined order - pass(pkg, '$require', './spec/type', { + pass(pkg, './$require', './spec/type', { unsafe: true, conditions: ['require'] }); // import vs require, defined order - pass(pkg, '$import', './spec/type', { + pass(pkg, './$import', './spec/type', { unsafe: true, conditions: ['import', 'require'] }); // import vs require, defined order - pass(pkg, '$node', './spec/env', { + pass(pkg, './$node', './spec/env', { unsafe: true, conditions: ['node'] }); // import vs require, defined order - pass(pkg, '$browser', './spec/env', { + pass(pkg, './$browser', './spec/env', { unsafe: true, conditions: ['browser', 'node'] }); // import vs require, defined order - pass(pkg, '$worker', './spec/env', { + pass(pkg, './$worker', './spec/env', { unsafe: true, conditions: ['browser', 'node', 'worker'] }); From 1b567edf6d8c399269653011a51ba08e743cf73c Mon Sep 17 00:00:00 2001 From: Luke Edwards Date: Thu, 12 Jan 2023 10:44:45 -0800 Subject: [PATCH 03/35] chore: strict types --- .github/workflows/ci.yml | 6 ++++++ package.json | 4 +++- src/index.ts | 16 ++++++++++++---- test/resolve.ts | 4 ++-- tsconfig.json | 22 ++++++++++++++++++++++ 5 files changed, 45 insertions(+), 7 deletions(-) create mode 100644 tsconfig.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a482ac2..7cd55dd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,6 +34,12 @@ jobs: npm install npm install -g nyc + - name: Compile + run: npm run build + + - name: Type Check + run: npm run types + - name: Test w/ Coverage run: nyc --include=src npm test diff --git a/package.json b/package.json index 3530742..45a0155 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,8 @@ "node": ">=10" }, "scripts": { - "build": "bundt", + "build": "bundt -m", + "types": "tsc --noEmit", "test": "uvu -r tsm test" }, "files": [ @@ -43,6 +44,7 @@ "devDependencies": { "bundt": "next", "tsm": "2.3.0", + "typescript": "4.9.4", "uvu": "0.5.4" } } diff --git a/src/index.ts b/src/index.ts index 95b197b..d2a59f7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -100,7 +100,12 @@ export function resolve(pkg: Package, entry = '.', options?: Options) { unsafe || allows.add(require ? 'require' : 'import'); unsafe || allows.add(browser ? 'browser' : 'node'); - let key, m, k, kv, tmp, isSingle=false; + let key: Path | string, + m: RegExpExecArray | null, + k: Path | undefined, + kv: string | undefined | null, + tmp: any, // mixed + isSingle = false; for (key in exports) { isSingle = key[0] !== '.'; @@ -123,7 +128,7 @@ export function resolve(pkg: Package, entry = '.', options?: Options) { // do not allow "./" to match if already matched "./foo*" key } else if (key[key.length - 1] === '/' && target.startsWith(key)) { kv = target.substring(key.length); - k = key; + k = key as Path; } else { tmp = key.indexOf('*', 2); if (!!~tmp) { @@ -133,7 +138,7 @@ export function resolve(pkg: Package, entry = '.', options?: Options) { if (m && m[1]) { kv = m[1]; - k = key; + k = key as Path; } } } @@ -175,7 +180,10 @@ export function legacy(pkg: Package, options: LegacyOptions = {}): Subpath | Bro // } else if (typeof value == 'object' && fields[i] == 'browser') { if (typeof browser == 'string') { - value = value[browser=toName(pkg.name, browser)]; + // TODO: is this right? browser object format so loose + value = (value as Record)[ + browser = toName(pkg.name, browser) + ]; if (value == null) return browser as Subpath; } } else { diff --git a/test/resolve.ts b/test/resolve.ts index 24c1775..2f65a71 100644 --- a/test/resolve.ts +++ b/test/resolve.ts @@ -16,7 +16,7 @@ function fail(pkg: Package, target: Path, entry?: string, options?: Options) { assert.unreachable(); } catch (err) { assert.instance(err, Error); - assert.is(err.message, `Missing "${target}" export in "${pkg.name}" package`); + assert.is((err as Error).message, `Missing "${target}" export in "${pkg.name}" package`); } } @@ -798,7 +798,7 @@ conditions('should throw an error if no known conditions', ctx => { assert.unreachable(); } catch (err) { assert.instance(err, Error); - assert.is(err.message, `No known conditions for "." entry in "hello" package`); + assert.is((err as Error).message, `No known conditions for "." entry in "hello" package`); } }); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..7928b88 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "strict": true, + "target": "esnext", + "strictBindCallApply": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "nodenext", + "strictFunctionTypes": true, + "strictNullChecks": true, + "noImplicitThis": true, + "noImplicitAny": true, + "alwaysStrict": true, + "module": "esnext", + "noEmit": true, + "paths": { + "resolve.exports": ["./index.d.ts"] + } + }, + "include": [ + "src", "test" + ] +} From 7937282c438b2fd063301dd7e16a5d4324c29ae8 Mon Sep 17 00:00:00 2001 From: Luke Edwards Date: Thu, 12 Jan 2023 10:47:36 -0800 Subject: [PATCH 04/35] fix: remove node 10 (bcuz tsm,bundt) --- .github/workflows/ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7cd55dd..89f2a75 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,7 +22,8 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - nodejs: [10, 12, 14, 16, 18] + # Node 10.x not supported by tsm & bundt + nodejs: [12, 14, 16, 18] steps: - uses: actions/checkout@v2 - uses: actions/setup-node@v1 From ce32f0fdf49ce2e8f260940764f1d05d3281605a Mon Sep 17 00:00:00 2001 From: Luke Edwards Date: Thu, 12 Jan 2023 10:50:54 -0800 Subject: [PATCH 05/35] fix(ci): compile in 18.x only --- .github/workflows/ci.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 89f2a75..1068065 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,15 +35,16 @@ jobs: npm install npm install -g nyc - - name: Compile - run: npm run build - - name: Type Check run: npm run types - name: Test w/ Coverage run: nyc --include=src npm test + - name: Compile + if: matrix.nodejs >= 18 + run: npm run build + - name: Report if: matrix.nodejs >= 18 run: | From 2af951624b132ca47da3d03ad992627282ec7247 Mon Sep 17 00:00:00 2001 From: Luke Edwards Date: Thu, 12 Jan 2023 12:55:58 -0800 Subject: [PATCH 06/35] feat: export Package/Exports/Imports types --- index.d.ts | 60 +++++++++++++++++++++++++++++++++--- src/index.ts | 82 +++++++++++++++++-------------------------------- test/legacy.ts | 50 +++++++++++++++--------------- test/resolve.ts | 17 +++++----- 4 files changed, 117 insertions(+), 92 deletions(-) diff --git a/index.d.ts b/index.d.ts index ff8b5d7..c6ac98f 100644 --- a/index.d.ts +++ b/index.d.ts @@ -7,12 +7,64 @@ export type Options = { export function resolve(pkg: T, entry: string, options?: Options): string | void; -export type BrowserFiles = Record; - -export function legacy(pkg: T, options: { browser: true, fields?: readonly string[] }): BrowserFiles | string | void; +export function legacy(pkg: T, options: { browser: true, fields?: readonly string[] }): Browser | void; export function legacy(pkg: T, options: { browser: string, fields?: readonly string[] }): string | false | void; export function legacy(pkg: T, options: { browser: false, fields?: readonly string[] }): string | void; export function legacy(pkg: T, options?: { browser?: boolean | string; fields?: readonly string[]; -}): BrowserFiles | string | false | void; +}): Browser | string; + +// --- + +/** + * A resolve condition + * @example "node", "default", "production" + */ +export type Condition = string; + +/** An internal file path */ +export type Path = `./${string}`; + +export type Imports = { + [entry: Imports.Entry]: Imports.Value; +} + +export namespace Imports { + export type Entry = `#${string}`; + + /** string ~> dependency OR internal path */ + export type Value = string | null | { + [c: Condition]: Value; + } | Value[]; +} + +export type Exports = Path | { + [path: Exports.Entry]: Exports.Value; + [cond: Condition]: Exports.Value; +} + +export namespace Exports { + /** Allows "." and "./{name}" */ + export type Entry = `.${string}`; + + /** string ~> internal path */ + export type Value = Path | null | { + [c: Condition]: Value; + } | Value[]; +} + +export type Package = { + name: string; + version?: string; + module?: string; + main?: string; + imports?: Imports; + exports?: Exports; + browser?: Browser; + [key: string]: any; +} + +export type Browser = string[] | string | { + [file: Path | string]: string | false; +} diff --git a/src/index.ts b/src/index.ts index d2a59f7..c571ea6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,48 +1,14 @@ -import type { Options } from '../index.d'; +import type * as t from 'resolve.exports'; -type Condition = string; - -export type Entry = `#${string}`; // imports - -export type Path = `.${string}`; -export type Subpath = `./${string}`; - -export type Imports = { - [entry: Entry]: Value | null; -} - -export type Exports = Subpath | { - [path: Path]: Value | null; - [cond: Condition]: Value | null; -}; - -export type Value = { - [c: Condition]: Value | null; -} | Subpath | Value[]; - -export type Browser = string[] | { - [file: Subpath | string]: string | false; -} - -export type Package = { - name: string; - version?: string; - module?: string; - main?: string; - imports?: Imports; - exports?: Exports; - browser?: Browser | string; - [key: string]: any; -}; - -function loop(exports: Exports | Value[] | null, keys: Set): Subpath|void { +function loop(exports: t.Exports.Value, keys: Set): t.Path | void { if (typeof exports === 'string') { return exports; } if (exports) { - let idx: number | string, tmp: File|Value|void; + let idx: number | string, tmp: t.Path | void; if (Array.isArray(exports)) { + // TODO: return all resolved truthys (flatten) for (idx=0; idx < exports.length; idx++) { if (tmp = loop(exports[idx], keys)) return tmp; } @@ -61,7 +27,7 @@ function loop(exports: Exports | Value[] | null, keys: Set): Subpath| * @param entry The target entry, eg "." * @param condition Unmatched condition? */ -function bail(name: string, entry: Path, condition?: number): never { +function bail(name: string, entry: string, condition?: number): never { throw new Error( condition ? `No known conditions for "${entry}" entry in "${name}" package` @@ -73,23 +39,23 @@ function bail(name: string, entry: Path, condition?: number): never { * @param name the package name * @param entry the target path/import */ -function toName(name: string, entry: string): Path { +function toName(name: string, entry: string): t.Exports.Entry { return ( entry === name ? '.' : entry[0] === '.' ? entry : entry.replace(new RegExp('^' + name + '\/'), './') - ) as Path; + ) as t.Exports.Entry; } -export function resolve(pkg: Package, entry = '.', options?: Options) { +export function resolve(pkg: t.Package, entry?: string, options?: t.Options) { let { name, exports } = pkg; if (exports) { let { browser, require, unsafe, conditions=[] } = options || {}; - let target = toName(name, entry); + let target = toName(name, entry || '.'); if (target !== '.' && !target.startsWith('./')) { - target = './' + target as Subpath; // ".ini" => "./.ini" + target = './' + target as t.Path; // ".ini" => "./.ini" } if (typeof exports === 'string') { @@ -100,9 +66,9 @@ export function resolve(pkg: Package, entry = '.', options?: Options) { unsafe || allows.add(require ? 'require' : 'import'); unsafe || allows.add(browser ? 'browser' : 'node'); - let key: Path | string, + let key: t.Exports.Entry | string, m: RegExpExecArray | null, - k: Path | undefined, + k: t.Exports.Entry | undefined, kv: string | undefined | null, tmp: any, // mixed isSingle = false; @@ -128,7 +94,7 @@ export function resolve(pkg: Package, entry = '.', options?: Options) { // do not allow "./" to match if already matched "./foo*" key } else if (key[key.length - 1] === '/' && target.startsWith(key)) { kv = target.substring(key.length); - k = key as Path; + k = key as t.Exports.Entry; } else { tmp = key.indexOf('*', 2); if (!!~tmp) { @@ -138,7 +104,7 @@ export function resolve(pkg: Package, entry = '.', options?: Options) { if (m && m[1]) { kv = m[1]; - k = key as Path; + k = key as t.Exports.Entry; } } } @@ -159,14 +125,23 @@ export function resolve(pkg: Package, entry = '.', options?: Options) { } } +// --- +// --- +// --- + type LegacyOptions = { fields?: string[]; browser?: string | boolean; } -export function legacy(pkg: Package, options: LegacyOptions = {}): Subpath | Browser | void { +type BrowserObject = { + // TODO: is this right? browser object format so loose + [file: string]: string | undefined; +} + +export function legacy(pkg: t.Package, options: LegacyOptions = {}): t.Path | t.Browser | void { let i=0, - value: Package['module' | 'main' | 'browser'], + value: string | t.Browser | undefined, browser = options.browser, fields = options.fields || ['module', 'main']; @@ -180,18 +155,17 @@ export function legacy(pkg: Package, options: LegacyOptions = {}): Subpath | Bro // } else if (typeof value == 'object' && fields[i] == 'browser') { if (typeof browser == 'string') { - // TODO: is this right? browser object format so loose - value = (value as Record)[ + value = (value as BrowserObject)[ browser = toName(pkg.name, browser) ]; - if (value == null) return browser as Subpath; + if (value == null) return browser as t.Path; } } else { continue; } return typeof value == 'string' - ? ('./' + value.replace(/^\.?\//, '')) as Subpath + ? ('./' + value.replace(/^\.?\//, '')) as t.Path : value; } } diff --git a/test/legacy.ts b/test/legacy.ts index dc9117d..dd6e9ac 100644 --- a/test/legacy.ts +++ b/test/legacy.ts @@ -1,13 +1,13 @@ import { suite } from 'uvu'; import * as assert from 'uvu/assert'; -import * as $exports from '../src'; +import * as lib from '../src'; -import type { Package } from '../src'; +import type { Package } from 'resolve.exports'; const legacy = suite('$.legacy'); legacy('should be a function', () => { - assert.type($exports.legacy, 'function'); + assert.type(lib.legacy, 'function'); }); legacy('should prefer "module" > "main" entry', () => { @@ -17,7 +17,7 @@ legacy('should prefer "module" > "main" entry', () => { "main": "build/main.js", }; - let output = $exports.legacy(pkg); + let output = lib.legacy(pkg); assert.is(output, './build/module.js'); }); @@ -27,7 +27,7 @@ legacy('should read "main" field', () => { "main": "build/main.js", }; - let output = $exports.legacy(pkg); + let output = lib.legacy(pkg); assert.is(output, './build/main.js'); }); @@ -36,7 +36,7 @@ legacy('should return nothing when no fields', () => { "name": "foobar" }; - let output = $exports.legacy(pkg); + let output = lib.legacy(pkg); assert.is(output, undefined); }); @@ -46,7 +46,7 @@ legacy('should ignore boolean-type field values', () => { "main": "main.js" }; - let output = $exports.legacy(pkg as any); + let output = lib.legacy(pkg as any); assert.is(output, './main.js'); }); @@ -63,18 +63,18 @@ const fields = suite('options.fields', { }); fields('should customize field search order', pkg => { - let output = $exports.legacy(pkg); + let output = lib.legacy(pkg); assert.is(output, './build/module.js', 'default: module'); - output = $exports.legacy(pkg, { fields: ['main'] }); + output = lib.legacy(pkg, { fields: ['main'] }); assert.is(output, './build/main.js', 'custom: main only'); - output = $exports.legacy(pkg, { fields: ['custom', 'main', 'module'] }); + output = lib.legacy(pkg, { fields: ['custom', 'main', 'module'] }); assert.is(output, './build/custom.js', 'custom: custom > main > module'); }); fields('should return first *resolved* field', pkg => { - let output = $exports.legacy(pkg, { + let output = lib.legacy(pkg, { fields: ['howdy', 'partner', 'hello', 'world', 'main'] }); @@ -94,15 +94,15 @@ const browser = suite('options.browser', { }); browser('should prioritize "browser" field when defined', pkg => { - let output = $exports.legacy(pkg); + let output = lib.legacy(pkg); assert.is(output, './build/module.js'); - output = $exports.legacy(pkg, { browser: true }); + output = lib.legacy(pkg, { browser: true }); assert.is(output, './build/browser.js'); }); browser('should respect existing "browser" order in custom fields', pkg => { - let output = $exports.legacy(pkg, { + let output = lib.legacy(pkg, { fields: ['main', 'browser'], browser: true, }); @@ -121,17 +121,17 @@ browser('should resolve object format', () => { }; assert.is( - $exports.legacy(pkg, { browser: 'module-a' }), + lib.legacy(pkg, { browser: 'module-a' }), './shims/module-a.js' ); assert.is( - $exports.legacy(pkg, { browser: './server/only.js' }), + lib.legacy(pkg, { browser: './server/only.js' }), './shims/client-only.js' ); assert.is( - $exports.legacy(pkg, { browser: 'foobar/server/only.js' }), + lib.legacy(pkg, { browser: 'foobar/server/only.js' }), './shims/client-only.js' ); }); @@ -146,17 +146,17 @@ browser('should allow object format to "ignore" modules/files :: string', () => }; assert.is( - $exports.legacy(pkg, { browser: 'module-a' }), + lib.legacy(pkg, { browser: 'module-a' }), false ); assert.is( - $exports.legacy(pkg, { browser: './foo.js' }), + lib.legacy(pkg, { browser: './foo.js' }), false ); assert.is( - $exports.legacy(pkg, { browser: 'foobar/foo.js' }), + lib.legacy(pkg, { browser: 'foobar/foo.js' }), false ); }); @@ -170,14 +170,14 @@ browser('should return the `browser` string (entry) if no custom mapping :: stri }; assert.is( - $exports.legacy(pkg, { + lib.legacy(pkg, { browser: './hello.js' }), './hello.js' ); assert.is( - $exports.legacy(pkg, { + lib.legacy(pkg, { browser: 'foobar/hello.js' }), './hello.js' @@ -192,7 +192,7 @@ browser('should return the full "browser" object :: true', () => { } }; - let output = $exports.legacy(pkg, { + let output = lib.legacy(pkg, { browser: true }); @@ -208,14 +208,14 @@ browser('still ensures string output is made relative', () => { } as any; assert.is( - $exports.legacy(pkg, { + lib.legacy(pkg, { browser: './foo.js' }), './bar.js' ); assert.is( - $exports.legacy(pkg, { + lib.legacy(pkg, { browser: 'foobar/foo.js' }), './bar.js' diff --git a/test/resolve.ts b/test/resolve.ts index 2f65a71..dec38b7 100644 --- a/test/resolve.ts +++ b/test/resolve.ts @@ -1,18 +1,17 @@ import { suite } from 'uvu'; import * as assert from 'uvu/assert'; -import * as $exports from '../src'; +import * as lib from '../src'; -import type { Package, Path } from '../src'; -import type { Options } from '../index.d'; +import type { Package, Exports, Options } from 'resolve.exports'; -function pass(pkg: Package, expects: Path, entry?: string, options?: Options) { - let out = $exports.resolve(pkg, entry, options); +function pass(pkg: Package, expects: Exports.Entry, entry?: string, options?: Options) { + let out = lib.resolve(pkg, entry, options); assert.is(out, expects); } -function fail(pkg: Package, target: Path, entry?: string, options?: Options) { +function fail(pkg: Package, target: Exports.Entry, entry?: string, options?: Options) { try { - $exports.resolve(pkg, entry, options); + lib.resolve(pkg, entry, options); assert.unreachable(); } catch (err) { assert.instance(err, Error); @@ -25,7 +24,7 @@ function fail(pkg: Package, target: Path, entry?: string, options?: Options) { const resolve = suite('$.resolve'); resolve('should be a function', () => { - assert.type($exports.resolve, 'function'); + assert.type(lib.resolve, 'function'); }); resolve('exports=string', () => { @@ -794,7 +793,7 @@ conditions('should throw an error if no known conditions', ctx => { delete pkg.exports.default; try { - $exports.resolve(pkg); + lib.resolve(pkg); assert.unreachable(); } catch (err) { assert.instance(err, Error); From db4139f4fb69927622f097e816a1b30b6be5a0a6 Mon Sep 17 00:00:00 2001 From: Luke Edwards Date: Thu, 12 Jan 2023 23:27:52 -0800 Subject: [PATCH 07/35] chore: extract `utils` file w/ tests & revamp --- index.d.ts | 9 ++- src/index.ts | 162 ++++++++++++++++++++++++++------------------------ src/utils.ts | 22 +++++++ test/utils.ts | 55 +++++++++++++++++ 4 files changed, 170 insertions(+), 78 deletions(-) create mode 100644 src/utils.ts create mode 100644 test/utils.ts diff --git a/index.d.ts b/index.d.ts index c6ac98f..978a63a 100644 --- a/index.d.ts +++ b/index.d.ts @@ -33,10 +33,15 @@ export type Imports = { export namespace Imports { export type Entry = `#${string}`; + type External = string; + /** string ~> dependency OR internal path */ - export type Value = string | null | { + export type Value = External | Path | null | { [c: Condition]: Value; } | Value[]; + + + export type Output = Array | External | Path; } export type Exports = Path | { @@ -52,6 +57,8 @@ export namespace Exports { export type Value = Path | null | { [c: Condition]: Value; } | Value[]; + + export type Output = Path[] | Path; } export type Package = { diff --git a/src/index.ts b/src/index.ts index c571ea6..75392fd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,5 @@ +import * as $ from './utils'; + import type * as t from 'resolve.exports'; function loop(exports: t.Exports.Value, keys: Set): t.Path | void { @@ -35,94 +37,97 @@ function bail(name: string, entry: string, condition?: number): never { ); } -/** - * @param name the package name - * @param entry the target path/import - */ -function toName(name: string, entry: string): t.Exports.Entry { - return ( - entry === name ? '.' - : entry[0] === '.' ? entry - : entry.replace(new RegExp('^' + name + '\/'), './') - ) as t.Exports.Entry; -} - -export function resolve(pkg: t.Package, entry?: string, options?: t.Options) { - let { name, exports } = pkg; +export function resolve(pkg: t.Package, input?: string, options?: t.Options): string[] | string | void { + let entry = input && input !== '.' + ? $.toEntry(pkg.name, input, true) + : '.'; - if (exports) { - let { browser, require, unsafe, conditions=[] } = options || {}; + if (entry[0] === '#') return imports(pkg, entry as t.Imports.Entry, options); + if (entry[0] === '.') return exports(pkg, entry as t.Exports.Entry, options); +} - let target = toName(name, entry || '.'); - if (target !== '.' && !target.startsWith('./')) { - target = './' + target as t.Path; // ".ini" => "./.ini" - } +export function imports(pkg: t.Package, key: t.Imports.Entry, options?: t.Options): t.Imports.Output | void { + // +} - if (typeof exports === 'string') { - return target === '.' ? exports : bail(name, target); - } +export function exports(pkg: t.Package, target: t.Exports.Entry, options?: t.Options): t.Exports.Output | void { + let + name = pkg.name, + entry = $.toEntry(name, target), + isROOT = entry === '.', + map = pkg.exports; - let allows = new Set(['default', ...conditions]); - unsafe || allows.add(require ? 'require' : 'import'); - unsafe || allows.add(browser ? 'browser' : 'node'); + // ".ini" => "./.ini" + if (!isROOT && !entry.startsWith('./')) { + entry = './' + entry as t.Path; + } - let key: t.Exports.Entry | string, - m: RegExpExecArray | null, - k: t.Exports.Entry | undefined, - kv: string | undefined | null, - tmp: any, // mixed - isSingle = false; + if (!map) return; + if (typeof map === 'string') { + return isROOT ? map : bail(name, entry); + } - for (key in exports) { - isSingle = key[0] !== '.'; - break; - } + let o = options || {}, + allows = new Set([ 'default', ...o.conditions||[] ]), + key: t.Exports.Entry | string, + match: RegExpExecArray | null, + longest: t.Exports.Entry | undefined, + value: string | undefined | null, + tmp: any, // mixed + isSingle = false; + + o.unsafe || allows.add(o.require ? 'require' : 'import'); + o.unsafe || allows.add(o.browser ? 'browser' : 'node'); + + for (key in map) { + isSingle = key[0] !== '.'; + break; + } - if (isSingle) { - return target === '.' - ? loop(exports, allows) || bail(name, target, 1) - : bail(name, target); - } + if (isSingle) { + return isROOT + ? loop(map, allows) || bail(name, entry, 1) + : bail(name, entry); + } - if (tmp = exports[target]) { - return loop(tmp, allows) || bail(name, target, 1); - } + if (tmp = map[entry]) { + return loop(tmp, allows) || bail(name, entry, 1); + } - if (target !== '.') { - for (key in exports) { - if (k && key.length < k.length) { - // do not allow "./" to match if already matched "./foo*" key - } else if (key[key.length - 1] === '/' && target.startsWith(key)) { - kv = target.substring(key.length); - k = key as t.Exports.Entry; - } else { - tmp = key.indexOf('*', 2); - if (!!~tmp) { - m = RegExp( - '^\.\/' + key.substring(2, tmp) + '(.*)' + key.substring(1+tmp) - ).exec(target); - - if (m && m[1]) { - kv = m[1]; - k = key as t.Exports.Entry; - } + if (!isROOT) { + for (key in map) { + if (longest && key.length < longest.length) { + // do not allow "./" to match if already matched "./foo*" key + } else if (key[key.length - 1] === '/' && entry.startsWith(key)) { + value = entry.substring(key.length); + longest = key as t.Exports.Entry; + } else { + tmp = key.indexOf('*', 2); + if (!!~tmp) { + match = RegExp( + '^\.\/' + key.substring(2, tmp) + '(.*)' + key.substring(1+tmp) + ).exec(entry); + + if (match && match[1]) { + value = match[1]; + longest = key as t.Exports.Entry; } } } + } - if (k && kv) { - // must have value - tmp = loop(exports[k], allows); - if (!tmp) return bail(name, target); + if (longest && value) { + // must have a value + tmp = loop(map[longest], allows); + if (!tmp) return bail(name, entry); - return tmp.includes('*') - ? tmp.replace(/[*]/g, kv) - : tmp + kv; - } + return tmp.includes('*') + ? tmp.replace(/[*]/g, value) + : tmp + value; } - - return bail(name, target); } + + return bail(name, entry); } // --- @@ -143,10 +148,15 @@ export function legacy(pkg: t.Package, options: LegacyOptions = {}): t.Path | t. let i=0, value: string | t.Browser | undefined, browser = options.browser, - fields = options.fields || ['module', 'main']; + fields = options.fields || ['module', 'main'], + isSTRING = typeof browser == 'string'; if (browser && !fields.includes('browser')) { fields.unshift('browser'); + // "module-a" -> "module-a" + // "./path/file.js" -> "./path/file.js" + // "foobar/path/file.js" -> "./path/file.js" + if (isSTRING) browser = $.toEntry(pkg.name, browser as string, false); } for (; i < fields.length; i++) { @@ -154,10 +164,8 @@ export function legacy(pkg: t.Package, options: LegacyOptions = {}): t.Path | t. if (typeof value == 'string') { // } else if (typeof value == 'object' && fields[i] == 'browser') { - if (typeof browser == 'string') { - value = (value as BrowserObject)[ - browser = toName(pkg.name, browser) - ]; + if (isSTRING) { + value = (value as BrowserObject)[browser as string]; if (value == null) return browser as t.Path; } } else { diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..824f216 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,22 @@ +import type * as t from 'resolve.exports'; + +/** + * @param name package name + * @param ident entry identifier + * @see https://esbench.com/bench/59fa3e6799634800a0349382 + */ +export function toEntry(name: string, ident: string, force?: true): t.Exports.Entry | t.Imports.Entry; +export function toEntry(name: string, ident: string, force?: false): t.Exports.Entry | t.Imports.Entry | string; +export function toEntry(name: string, ident: string, force?: boolean): t.Exports.Entry | t.Imports.Entry | string { + if (name === ident) return '.'; + + let root = name+'/', len = root.length; + let bool = ident.slice(0, len) === root; + + let output = bool ? ident.slice(len) : ident; + if (output[0] === '#') return output as t.Imports.Entry; + + return (bool || force) + ? (output.slice(0,2) === './' ? output : './' + output) as t.Path + : output as string | t.Exports.Entry; +} diff --git a/test/utils.ts b/test/utils.ts new file mode 100644 index 0000000..e32822e --- /dev/null +++ b/test/utils.ts @@ -0,0 +1,55 @@ +import * as uvu from 'uvu'; +import * as assert from 'uvu/assert'; +import * as $ from '../src/utils'; + +function describe( + name: string, + cb: (it: uvu.Test) => void +) { + let t = uvu.suite(name); + cb(t); + t.run(); +} + +describe('utils.toEntry', it => { + const PKG = 'foobar'; + function run(input: string, expect: string) { + let output = $.toEntry(PKG, input); + assert.type(output, 'string'); + assert.is(output, expect); + } + + it('should be a function', () => { + assert.type($.toEntry, 'function'); + }); + + it('should return "." if given package name', () => { + run(PKG, '.'); + }); + + it('should return "#ident" if given "#ident" input', () => { + run('#hello', '#hello'); + }); + + it('should echo if given package subpath', () => { + run('.', '.'); + run('./', './'); + run('./foo', './foo'); + }); + + // handle "import 'lib/lib';" case + it('should echo if given "./" input', () => { + let input = './' + PKG; + run(input, input); + }); + + it('should return "./" for "/" input', () => { + let input = PKG + '/other'; + run(input, './other'); + }); + + it('should return "#" for "/#" input', () => { + let input = PKG + '/#inner'; + run(input, '#inner'); + }); +}); From f6a2760e853d471922ae76e7e7eb2415fd82e353 Mon Sep 17 00:00:00 2001 From: Luke Edwards Date: Fri, 13 Jan 2023 00:01:41 -0800 Subject: [PATCH 08/35] chore: move `loop` to utils file --- src/index.ts | 30 ++++-------------------------- src/utils.ts | 22 ++++++++++++++++++++++ 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/src/index.ts b/src/index.ts index 75392fd..949108e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,28 +2,6 @@ import * as $ from './utils'; import type * as t from 'resolve.exports'; -function loop(exports: t.Exports.Value, keys: Set): t.Path | void { - if (typeof exports === 'string') { - return exports; - } - - if (exports) { - let idx: number | string, tmp: t.Path | void; - if (Array.isArray(exports)) { - // TODO: return all resolved truthys (flatten) - for (idx=0; idx < exports.length; idx++) { - if (tmp = loop(exports[idx], keys)) return tmp; - } - } else { - for (idx in exports) { - if (keys.has(idx)) { - return loop(exports[idx], keys); - } - } - } - } -} - /** * @param name The package name * @param entry The target entry, eg "." @@ -86,12 +64,12 @@ export function exports(pkg: t.Package, target: t.Exports.Entry, options?: t.Opt if (isSingle) { return isROOT - ? loop(map, allows) || bail(name, entry, 1) + ? $.loop(map, allows) || bail(name, entry, 1) : bail(name, entry); } if (tmp = map[entry]) { - return loop(tmp, allows) || bail(name, entry, 1); + return $.loop(tmp, allows) || bail(name, entry, 1); } if (!isROOT) { @@ -118,7 +96,7 @@ export function exports(pkg: t.Package, target: t.Exports.Entry, options?: t.Opt if (longest && value) { // must have a value - tmp = loop(map[longest], allows); + tmp = $.loop(map[longest], allows); if (!tmp) return bail(name, entry); return tmp.includes('*') @@ -166,7 +144,7 @@ export function legacy(pkg: t.Package, options: LegacyOptions = {}): t.Path | t. } else if (typeof value == 'object' && fields[i] == 'browser') { if (isSTRING) { value = (value as BrowserObject)[browser as string]; - if (value == null) return browser as t.Path; + if (value == null) return browser as string; } } else { continue; diff --git a/src/utils.ts b/src/utils.ts index 824f216..cf822c3 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -20,3 +20,25 @@ export function toEntry(name: string, ident: string, force?: boolean): t.Exports ? (output.slice(0,2) === './' ? output : './' + output) as t.Path : output as string | t.Exports.Entry; } + +export function loop(exports: t.Exports.Value, keys: Set): t.Path | void { + if (typeof exports === 'string') { + return exports; + } + + if (exports) { + let idx: number | string, tmp: t.Path | void; + if (Array.isArray(exports)) { + // TODO: return all resolved truthys (flatten) + for (idx=0; idx < exports.length; idx++) { + if (tmp = loop(exports[idx], keys)) return tmp; + } + } else { + for (idx in exports) { + if (keys.has(idx)) { + return loop(exports[idx], keys); + } + } + } + } +} From a8faf8a68f99e2ef4eccc2ab255f285fad250f70 Mon Sep 17 00:00:00 2001 From: Luke Edwards Date: Fri, 13 Jan 2023 00:02:10 -0800 Subject: [PATCH 09/35] chore: more `toEntry` tests --- src/index.ts | 7 +--- src/utils.ts | 4 +- test/utils.ts | 103 +++++++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 97 insertions(+), 17 deletions(-) diff --git a/src/index.ts b/src/index.ts index 949108e..5e71625 100644 --- a/src/index.ts +++ b/src/index.ts @@ -31,15 +31,10 @@ export function imports(pkg: t.Package, key: t.Imports.Entry, options?: t.Option export function exports(pkg: t.Package, target: t.Exports.Entry, options?: t.Options): t.Exports.Output | void { let name = pkg.name, - entry = $.toEntry(name, target), + entry = $.toEntry(name, target, true), isROOT = entry === '.', map = pkg.exports; - // ".ini" => "./.ini" - if (!isROOT && !entry.startsWith('./')) { - entry = './' + entry as t.Path; - } - if (!map) return; if (typeof map === 'string') { return isROOT ? map : bail(name, entry); diff --git a/src/utils.ts b/src/utils.ts index cf822c3..0d0f742 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -5,10 +5,10 @@ import type * as t from 'resolve.exports'; * @param ident entry identifier * @see https://esbench.com/bench/59fa3e6799634800a0349382 */ -export function toEntry(name: string, ident: string, force?: true): t.Exports.Entry | t.Imports.Entry; +export function toEntry(name: string, ident: string, force: true): t.Exports.Entry | t.Imports.Entry; export function toEntry(name: string, ident: string, force?: false): t.Exports.Entry | t.Imports.Entry | string; export function toEntry(name: string, ident: string, force?: boolean): t.Exports.Entry | t.Imports.Entry | string { - if (name === ident) return '.'; + if (name === ident || ident === '.') return '.'; let root = name+'/', len = root.length; let bool = ident.slice(0, len) === root; diff --git a/test/utils.ts b/test/utils.ts index e32822e..8b744ff 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -13,8 +13,11 @@ function describe( describe('utils.toEntry', it => { const PKG = 'foobar'; - function run(input: string, expect: string) { - let output = $.toEntry(PKG, input); + const EXTERNAL = 'rollup'; + + function run(input: string, expect: string, force?: boolean) { + // @ts-expect-error; overload issue + let output = $.toEntry(PKG, input, force); assert.type(output, 'string'); assert.is(output, expect); } @@ -23,33 +26,115 @@ describe('utils.toEntry', it => { assert.type($.toEntry, 'function'); }); - it('should return "." if given package name', () => { + it('PKG -> .', () => { run(PKG, '.'); }); - it('should return "#ident" if given "#ident" input', () => { - run('#hello', '#hello'); + it('PKG -> . :: force', () => { + run(PKG, '.', true); }); - it('should echo if given package subpath', () => { + it('. -> .', () => { run('.', '.'); + }); + + it('. -> . :: force', () => { + run('.', '.', true); + }); + + it('./ -> ./', () => { run('./', './'); + }); + + it('./ -> ./ :: force', () => { + run('./', './', true); + }); + + it('#inner -> #inner', () => { + run('#inner', '#inner'); + }); + + it('#inner -> ./#inner :: force', () => { + run('#inner', '#inner', true); + }); + + it('./foo -> ./foo', () => { run('./foo', './foo'); }); + it('./foo -> ./foo :: force', () => { + run('./foo', './foo', true); + }); + + // partial `name` match + // should be like EXTERNAL + it('foo -> foo', () => { + run('foo', 'foo'); + }); + + it('foo -> ./foo :: force', () => { + run('foo', './foo', true); + }); + + // treats as external + it('.ini -> ./.ini', () => { + run('.ini', '.ini'); + }); + + it('.ini -> ./.ini :: force', () => { + run('.ini', './.ini', true); + }); + + it('foo -> ./foo :: force', () => { + run('foo', './foo', true); + }); + // handle "import 'lib/lib';" case - it('should echo if given "./" input', () => { + it('./PKG -> ./PKG', () => { let input = './' + PKG; run(input, input); }); - it('should return "./" for "/" input', () => { + it('./PKG -> ./PKG :: force', () => { + let input = './' + PKG; + run(input, input, true); + }); + + it('PKG/subpath -> ./subpath', () => { let input = PKG + '/other'; run(input, './other'); }); - it('should return "#" for "/#" input', () => { + it('PKG/subpath -> ./subpath :: force', () => { + let input = PKG + '/other'; + run(input, './other', true); + }); + + it('PKG/#inner -> #inner', () => { let input = PKG + '/#inner'; run(input, '#inner'); }); + + it('PKG/#inner -> ./#inner :: force', () => { + let input = PKG + '/#inner'; + run(input, '#inner', true); + }); + + it('PKG/.ini -> ./.ini', () => { + let input = PKG + '/.ini'; + run(input, './.ini'); + }); + + it('PKG/.ini -> ./.ini :: force', () => { + let input = PKG + '/.ini'; + run(input, './.ini', true); + }); + + it('EXTERNAL -> EXTERNAL', () => { + run(EXTERNAL, EXTERNAL); + }); + + it('EXTERNAL -> ./EXTERNAL :: force', () => { + run(EXTERNAL, './'+EXTERNAL, true); + }); }); From 8751b0e7190739e5ed851c10479e979d220e4e74 Mon Sep 17 00:00:00 2001 From: Luke Edwards Date: Fri, 13 Jan 2023 00:20:44 -0800 Subject: [PATCH 10/35] chore: invert `toEntry` boolean default --- src/index.ts | 6 ++-- src/utils.ts | 9 ++--- test/utils.ts | 99 ++++++++++++++------------------------------------- 3 files changed, 34 insertions(+), 80 deletions(-) diff --git a/src/index.ts b/src/index.ts index 5e71625..343b0f1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -17,7 +17,7 @@ function bail(name: string, entry: string, condition?: number): never { export function resolve(pkg: t.Package, input?: string, options?: t.Options): string[] | string | void { let entry = input && input !== '.' - ? $.toEntry(pkg.name, input, true) + ? $.toEntry(pkg.name, input) : '.'; if (entry[0] === '#') return imports(pkg, entry as t.Imports.Entry, options); @@ -31,7 +31,7 @@ export function imports(pkg: t.Package, key: t.Imports.Entry, options?: t.Option export function exports(pkg: t.Package, target: t.Exports.Entry, options?: t.Options): t.Exports.Output | void { let name = pkg.name, - entry = $.toEntry(name, target, true), + entry = $.toEntry(name, target), isROOT = entry === '.', map = pkg.exports; @@ -129,7 +129,7 @@ export function legacy(pkg: t.Package, options: LegacyOptions = {}): t.Path | t. // "module-a" -> "module-a" // "./path/file.js" -> "./path/file.js" // "foobar/path/file.js" -> "./path/file.js" - if (isSTRING) browser = $.toEntry(pkg.name, browser as string, false); + if (isSTRING) browser = $.toEntry(pkg.name, browser as string, true); } for (; i < fields.length; i++) { diff --git a/src/utils.ts b/src/utils.ts index 0d0f742..11fa651 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -3,11 +3,12 @@ import type * as t from 'resolve.exports'; /** * @param name package name * @param ident entry identifier + * @param externals allow non-path (external) result * @see https://esbench.com/bench/59fa3e6799634800a0349382 */ -export function toEntry(name: string, ident: string, force: true): t.Exports.Entry | t.Imports.Entry; -export function toEntry(name: string, ident: string, force?: false): t.Exports.Entry | t.Imports.Entry | string; -export function toEntry(name: string, ident: string, force?: boolean): t.Exports.Entry | t.Imports.Entry | string { +export function toEntry(name: string, ident: string, externals?: false): t.Exports.Entry | t.Imports.Entry; +export function toEntry(name: string, ident: string, externals: true): t.Exports.Entry | t.Imports.Entry | string; +export function toEntry(name: string, ident: string, externals?: boolean): t.Exports.Entry | t.Imports.Entry | string { if (name === ident || ident === '.') return '.'; let root = name+'/', len = root.length; @@ -16,7 +17,7 @@ export function toEntry(name: string, ident: string, force?: boolean): t.Exports let output = bool ? ident.slice(len) : ident; if (output[0] === '#') return output as t.Imports.Entry; - return (bool || force) + return (bool || !externals) ? (output.slice(0,2) === './' ? output : './' + output) as t.Path : output as string | t.Exports.Entry; } diff --git a/test/utils.ts b/test/utils.ts index 8b744ff..d595644 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -12,129 +12,82 @@ function describe( } describe('utils.toEntry', it => { - const PKG = 'foobar'; - const EXTERNAL = 'rollup'; - - function run(input: string, expect: string, force?: boolean) { - // @ts-expect-error; overload issue - let output = $.toEntry(PKG, input, force); - assert.type(output, 'string'); - assert.is(output, expect); + const PKG = 'PACKAGE'; + const EXTERNAL = 'EXTERNAL'; + + function run(input: string, expect: string, externals?: boolean) { + // overloading not working -,- + let output = externals ? $.toEntry(PKG, input, true) : $.toEntry(PKG, input); + let msg = `"${input}" -> "${expect}"` + (externals ? ' (externals)' : ''); + assert.is(output, expect, msg); } it('should be a function', () => { assert.type($.toEntry, 'function'); }); - it('PKG -> .', () => { + it('PKG', () => { run(PKG, '.'); - }); - - it('PKG -> . :: force', () => { run(PKG, '.', true); }); - it('. -> .', () => { + it('.', () => { run('.', '.'); - }); - - it('. -> . :: force', () => { run('.', '.', true); }); - it('./ -> ./', () => { + it('./', () => { run('./', './'); - }); - - it('./ -> ./ :: force', () => { run('./', './', true); }); - it('#inner -> #inner', () => { + it('#inner', () => { run('#inner', '#inner'); - }); - - it('#inner -> ./#inner :: force', () => { run('#inner', '#inner', true); }); - it('./foo -> ./foo', () => { + it('./foo', () => { run('./foo', './foo'); - }); - - it('./foo -> ./foo :: force', () => { run('./foo', './foo', true); }); - // partial `name` match - // should be like EXTERNAL - it('foo -> foo', () => { - run('foo', 'foo'); - }); - - it('foo -> ./foo :: force', () => { - run('foo', './foo', true); - }); - - // treats as external - it('.ini -> ./.ini', () => { - run('.ini', '.ini'); - }); - - it('.ini -> ./.ini :: force', () => { - run('.ini', './.ini', true); + it('foo', () => { + run('foo', './foo'); // forces path by default + run('foo', 'foo', true); }); - it('foo -> ./foo :: force', () => { - run('foo', './foo', true); + it('.ini', () => { + run('.ini', './.ini'); // forces path by default + run('.ini', '.ini', true); }); // handle "import 'lib/lib';" case - it('./PKG -> ./PKG', () => { + it('./PKG', () => { let input = './' + PKG; run(input, input); - }); - - it('./PKG -> ./PKG :: force', () => { - let input = './' + PKG; run(input, input, true); }); - it('PKG/subpath -> ./subpath', () => { + it('PKG/subpath', () => { let input = PKG + '/other'; run(input, './other'); - }); - - it('PKG/subpath -> ./subpath :: force', () => { - let input = PKG + '/other'; run(input, './other', true); }); - it('PKG/#inner -> #inner', () => { + it('PKG/#inner', () => { let input = PKG + '/#inner'; run(input, '#inner'); - }); - - it('PKG/#inner -> ./#inner :: force', () => { - let input = PKG + '/#inner'; run(input, '#inner', true); }); - it('PKG/.ini -> ./.ini', () => { + it('PKG/.ini', () => { let input = PKG + '/.ini'; run(input, './.ini'); - }); - - it('PKG/.ini -> ./.ini :: force', () => { - let input = PKG + '/.ini'; run(input, './.ini', true); }); - it('EXTERNAL -> EXTERNAL', () => { - run(EXTERNAL, EXTERNAL); - }); - - it('EXTERNAL -> ./EXTERNAL :: force', () => { - run(EXTERNAL, './'+EXTERNAL, true); + it('EXTERNAL', () => { + run(EXTERNAL, './'+EXTERNAL); // forces path by default + run(EXTERNAL, EXTERNAL, true); }); }); From c658ad52b9fa81d1515e3072764ed6a259a0e755 Mon Sep 17 00:00:00 2001 From: Luke Edwards Date: Fri, 13 Jan 2023 01:00:33 -0800 Subject: [PATCH 11/35] chore: add `utils.loop` tests --- test/utils.ts | 194 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 193 insertions(+), 1 deletion(-) diff --git a/test/utils.ts b/test/utils.ts index d595644..d2ed6e8 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -2,6 +2,8 @@ import * as uvu from 'uvu'; import * as assert from 'uvu/assert'; import * as $ from '../src/utils'; +import type * as t from 'resolve.exports'; + function describe( name: string, cb: (it: uvu.Test) => void @@ -11,7 +13,7 @@ function describe( t.run(); } -describe('utils.toEntry', it => { +describe('$.toEntry', it => { const PKG = 'PACKAGE'; const EXTERNAL = 'EXTERNAL'; @@ -91,3 +93,193 @@ describe('utils.toEntry', it => { run(EXTERNAL, EXTERNAL, true); }); }); + +describe('$.loop', it => { + const FILE = './file.js'; + const DEFAULT = './foobar.js'; + + type Expect = string | string[] | null | undefined; + function run(expect: Expect, map: t.Exports.Value, conditions?: string[]) { + let output = $.loop(map, new Set([ 'default', ...conditions||[] ])); + assert.equal(output, expect); + } + + it('should be a function', () => { + assert.type($.loop, 'function'); + }); + + it('string', () => { + // @ts-expect-error + run('', ''); + + // @ts-expect-error + run('.', '.'); + + run('./foo.mjs', './foo.mjs'); + }); + + it('empties', () => { + run(undefined, null); // TODO: expect null + run(undefined, []); + run(undefined, {}); + }); + + it('{ default }', () => { + run(FILE, { + default: FILE, + }); + + run(FILE, { + other: './unknown.js', + default: FILE, + }); + + run(undefined, { + other: './unknown.js', + }); + + run(FILE, { + foo: './foo.js', + default: { + bar: './bar.js', + default: { + baz: './baz.js', + default: FILE, + } + } + }); + }); + + it('{ custom }', () => { + let conditions = ['custom']; + + run(DEFAULT, { + default: DEFAULT, + custom: FILE, + }, conditions); + + run(FILE, { + custom: FILE, + default: DEFAULT, + }, conditions); + + run(undefined, { + foo: './foo.js', + bar: './bar.js', + }, conditions); + + run(FILE, { + foo: './foo.js', + custom: { + default: { + custom: FILE, + default: DEFAULT, + } + }, + default: { + custom: './bar.js' + } + }, conditions); + }); + + it('[ string ]', () => { + // TODO: expect array + run(DEFAULT, [ + DEFAULT, + FILE + ]); + + run(undefined, [ + null, + ]); + + // TODO: expect truthy array + run(DEFAULT, [ + null, + DEFAULT, + FILE + ]); + + // TODO: expect truthy array + run(DEFAULT, [ + DEFAULT, + null, + FILE + ]); + }); + + it('[{ default }]', () => { + // TODO: expect string[] + run(DEFAULT, [ + { + default: DEFAULT, + }, + FILE + ]); + + // TODO: expect string[] + run(FILE, [ + FILE, + { + default: DEFAULT, + }, + ]); + + // TODO: expect string[] + run(DEFAULT, [ + { + default: { + default: { + default: DEFAULT, + } + } + }, + null, + FILE + ]); + + // TODO: expect string[] + run(DEFAULT, [ + { + default: { + default: DEFAULT, + } + }, + null, + { + default: { + default: DEFAULT, + } + }, + null, + ]); + }); + + it('{ [mixed] }', () => { + // TODO: expect string[] + run(DEFAULT, { + default: [DEFAULT, FILE] + }); + + // TODO: expect string[] + run(DEFAULT, { + default: [null, DEFAULT, FILE] + }); + + // TODO: expect string[] + run(DEFAULT, { + default: [null, { + default: DEFAULT + }, FILE] + }); + + // TODO: expect string[] + run(FILE, { + default: { + custom: [{ + default: [FILE, FILE, null, DEFAULT] + }, null, DEFAULT, FILE] + } + }, ['custom']); + }); +}); From 83045066b1e4292d34da75847a62876350062135 Mon Sep 17 00:00:00 2001 From: Luke Edwards Date: Fri, 13 Jan 2023 01:37:03 -0800 Subject: [PATCH 12/35] break: possible return `string[]` output; - Closes #17 --- index.d.ts | 10 +++---- src/index.ts | 4 +-- src/utils.ts | 40 +++++++++++++++---------- test/resolve.ts | 22 +++++++------- test/utils.ts | 78 ++++++++++++++++++++++++------------------------- 5 files changed, 82 insertions(+), 72 deletions(-) diff --git a/index.d.ts b/index.d.ts index 978a63a..c55b6e5 100644 --- a/index.d.ts +++ b/index.d.ts @@ -5,12 +5,12 @@ export type Options = { unsafe?: boolean; } -export function resolve(pkg: T, entry: string, options?: Options): string | void; +export function resolve(pkg: T, entry: string, options?: Options): Imports.Output | Exports.Output | void; -export function legacy(pkg: T, options: { browser: true, fields?: readonly string[] }): Browser | void; -export function legacy(pkg: T, options: { browser: string, fields?: readonly string[] }): string | false | void; -export function legacy(pkg: T, options: { browser: false, fields?: readonly string[] }): string | void; -export function legacy(pkg: T, options?: { +export function legacy(pkg: T, options: { browser: true, fields?: readonly string[] }): Browser | void; +export function legacy(pkg: T, options: { browser: string, fields?: readonly string[] }): string | false | void; +export function legacy(pkg: T, options: { browser: false, fields?: readonly string[] }): string | void; +export function legacy(pkg: T, options?: { browser?: boolean | string; fields?: readonly string[]; }): Browser | string; diff --git a/src/index.ts b/src/index.ts index 343b0f1..6c7cec9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -59,12 +59,12 @@ export function exports(pkg: t.Package, target: t.Exports.Entry, options?: t.Opt if (isSingle) { return isROOT - ? $.loop(map, allows) || bail(name, entry, 1) + ? $.loop(map, allows) as t.Exports.Output || bail(name, entry, 1) : bail(name, entry); } if (tmp = map[entry]) { - return $.loop(tmp, allows) || bail(name, entry, 1); + return $.loop(tmp, allows) as t.Exports.Output || bail(name, entry, 1); } if (!isROOT) { diff --git a/src/utils.ts b/src/utils.ts index 11fa651..dfb9689 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -22,23 +22,33 @@ export function toEntry(name: string, ident: string, externals?: boolean): t.Exp : output as string | t.Exports.Entry; } -export function loop(exports: t.Exports.Value, keys: Set): t.Path | void { - if (typeof exports === 'string') { - return exports; - } +type Output = string[] | string | void; +export function loop(m: t.Exports.Value, keys: Set, result?: Set): Output { + if (m) { + if (typeof m === 'string') { + return m; + } + + let + idx: number | string, + arr: Set, + tmp: Output; + + if (Array.isArray(m)) { + arr = result || new Set; + + for (idx=0; idx < m.length; idx++) { + tmp = loop(m[idx], keys, arr); + if (tmp) arr.add(tmp as string); + } - if (exports) { - let idx: number | string, tmp: t.Path | void; - if (Array.isArray(exports)) { - // TODO: return all resolved truthys (flatten) - for (idx=0; idx < exports.length; idx++) { - if (tmp = loop(exports[idx], keys)) return tmp; + // TODO: send string if len=1? + if (!result && arr.size) { + return [...arr]; } - } else { - for (idx in exports) { - if (keys.has(idx)) { - return loop(exports[idx], keys); - } + } else for (idx in m) { + if (keys.has(idx)) { + return loop(m[idx], keys, result); } } } diff --git a/test/resolve.ts b/test/resolve.ts index dec38b7..b0a7b80 100644 --- a/test/resolve.ts +++ b/test/resolve.ts @@ -4,9 +4,10 @@ import * as lib from '../src'; import type { Package, Exports, Options } from 'resolve.exports'; -function pass(pkg: Package, expects: Exports.Entry, entry?: string, options?: Options) { +function pass(pkg: Package, expects: Exports.Entry|Exports.Entry[], entry?: string, options?: Options) { let out = lib.resolve(pkg, entry, options); - assert.is(out, expects); + if (Array.isArray(expects)) assert.equal(out, expects); + else assert.is(out, expects); } function fail(pkg: Package, target: Exports.Entry, entry?: string, options?: Options) { @@ -650,16 +651,17 @@ resolve('should handle mixed path/conditions', () => { } } - pass(pkg, './$root.import'); - pass(pkg, './$root.import', 'foobar'); + pass(pkg, ['./$root.import', './$root.string']); + pass(pkg, ['./$root.import', './$root.string'], 'foobar'); - pass(pkg, './$foo.string', 'foo'); - pass(pkg, './$foo.string', 'foobar/foo'); - pass(pkg, './$foo.string', './foo'); + // TODO? if len==1 then single? + pass(pkg, ['./$foo.string'], 'foo'); + pass(pkg, ['./$foo.string'], 'foobar/foo'); + pass(pkg, ['./$foo.string'], './foo'); - pass(pkg, './$foo.require', 'foo', { require: true }); - pass(pkg, './$foo.require', 'foobar/foo', { require: true }); - pass(pkg, './$foo.require', './foo', { require: true }); + pass(pkg, ['./$foo.require', './$foo.string'], 'foo', { require: true }); + pass(pkg, ['./$foo.require', './$foo.string'], 'foobar/foo', { require: true }); + pass(pkg, ['./$foo.require', './$foo.string'], './foo', { require: true }); }); resolve('should handle file with leading dot', () => { diff --git a/test/utils.ts b/test/utils.ts index d2ed6e8..d6a8ba4 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -109,17 +109,15 @@ describe('$.loop', it => { }); it('string', () => { - // @ts-expect-error - run('', ''); - + run('./foo.mjs', './foo.mjs'); // @ts-expect-error run('.', '.'); - - run('./foo.mjs', './foo.mjs'); }); it('empties', () => { - run(undefined, null); // TODO: expect null + // @ts-expect-error + run(undefined, ''); + run(undefined, null); run(undefined, []); run(undefined, {}); }); @@ -183,50 +181,43 @@ describe('$.loop', it => { }); it('[ string ]', () => { - // TODO: expect array - run(DEFAULT, [ - DEFAULT, - FILE - ]); + run( + [DEFAULT, FILE], + [DEFAULT, FILE] + ); run(undefined, [ null, ]); - // TODO: expect truthy array - run(DEFAULT, [ - null, - DEFAULT, - FILE - ]); + run( + [DEFAULT, FILE], + [null, DEFAULT, FILE] + ); - // TODO: expect truthy array - run(DEFAULT, [ - DEFAULT, - null, - FILE - ]); + run( + [DEFAULT, FILE], + [DEFAULT, null, FILE] + ); }); it('[{ default }]', () => { - // TODO: expect string[] - run(DEFAULT, [ + run([DEFAULT, FILE], [ { default: DEFAULT, }, FILE ]); - // TODO: expect string[] - run(FILE, [ + run([FILE, DEFAULT], [ FILE, + null, { default: DEFAULT, }, ]); - // TODO: expect string[] - run(DEFAULT, [ + run([DEFAULT, FILE], [ { default: { default: { @@ -238,8 +229,7 @@ describe('$.loop', it => { FILE ]); - // TODO: expect string[] - run(DEFAULT, [ + run([DEFAULT, FILE, './foo.js'], [ { default: { default: DEFAULT, @@ -251,32 +241,40 @@ describe('$.loop', it => { default: DEFAULT, } }, - null, + FILE, + { + default: './foo.js' + } ]); }); it('{ [mixed] }', () => { - // TODO: expect string[] - run(DEFAULT, { + run([DEFAULT, FILE], { default: [DEFAULT, FILE] }); - // TODO: expect string[] - run(DEFAULT, { + run([DEFAULT, FILE], { default: [null, DEFAULT, FILE] }); - // TODO: expect string[] - run(DEFAULT, { + run([DEFAULT, FILE], { default: [null, { default: DEFAULT }, FILE] }); - // TODO: expect string[] - run(FILE, { + run([FILE, DEFAULT], { + default: { + custom: [{ + default: [FILE, FILE, null, DEFAULT] + }, null, DEFAULT, FILE] + } + }, ['custom']); + + run([DEFAULT, FILE], { default: { custom: [{ + custom: [DEFAULT, null], default: [FILE, FILE, null, DEFAULT] }, null, DEFAULT, FILE] } From c53b0c0fb21fe35a1c309c1d4392a02cd4a3c88c Mon Sep 17 00:00:00 2001 From: Luke Edwards Date: Fri, 13 Jan 2023 11:05:26 -0800 Subject: [PATCH 13/35] chore: extract `conditions` to utils --- src/index.ts | 13 +++---- src/utils.ts | 7 ++++ test/utils.ts | 98 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 110 insertions(+), 8 deletions(-) diff --git a/src/index.ts b/src/index.ts index 6c7cec9..8938850 100644 --- a/src/index.ts +++ b/src/index.ts @@ -40,24 +40,21 @@ export function exports(pkg: t.Package, target: t.Exports.Entry, options?: t.Opt return isROOT ? map : bail(name, entry); } - let o = options || {}, - allows = new Set([ 'default', ...o.conditions||[] ]), + let + allows = $.conditions(options||{}), key: t.Exports.Entry | string, match: RegExpExecArray | null, longest: t.Exports.Entry | undefined, value: string | undefined | null, tmp: any, // mixed - isSingle = false; - - o.unsafe || allows.add(o.require ? 'require' : 'import'); - o.unsafe || allows.add(o.browser ? 'browser' : 'node'); + isONE = false; for (key in map) { - isSingle = key[0] !== '.'; + isONE = key[0] !== '.'; break; } - if (isSingle) { + if (isONE) { return isROOT ? $.loop(map, allows) as t.Exports.Output || bail(name, entry, 1) : bail(name, entry); diff --git a/src/utils.ts b/src/utils.ts index dfb9689..1c00f15 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,12 @@ import type * as t from 'resolve.exports'; +export function conditions(options: t.Options): Set { + let out = new Set([ 'default', ...options.conditions || [] ]); + options.unsafe || out.add(options.require ? 'require' : 'import'); + options.unsafe || out.add(options.browser ? 'browser' : 'node'); + return out; +} + /** * @param name package name * @param ident entry identifier diff --git a/test/utils.ts b/test/utils.ts index d6a8ba4..e2791fe 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -13,6 +13,104 @@ function describe( t.run(); } +describe('$.conditions', it => { + const EMPTY = {}; + + function run(o?: t.Options): string[] { + return [...$.conditions(o||{})]; + } + + it('should be a function', () => { + assert.type($.conditions, 'function'); + }); + + it('should return `Set` instance', () => { + let output = $.conditions(EMPTY); + assert.instance(output, Set); + }); + + it('default conditions', () => { + assert.equal( + [ ...$.conditions(EMPTY) ], + ['default', 'import', 'node'] + ); + }); + + it('default conditions :: unsafe', () => { + assert.equal( + run({ unsafe: true }), + ['default'] + ); + }); + + it('option.browser', () => { + assert.equal( + run({ browser: true }), + ['default', 'import', 'browser'] + ); + }); + + // unsafe ignores all but conditions + it('option.browser :: unsafe', () => { + let output = run({ + browser: true, + unsafe: true, + }); + assert.equal(output, ['default']); + }); + + it('option.require', () => { + assert.equal( + run({ require: true }), + ['default', 'require', 'node'] + ); + }); + + // unsafe ignores all but conditions + it('option.require :: unsafe', () => { + let output = run({ + require: true, + unsafe: true, + }); + assert.equal(output, ['default']); + }); + + it('option.conditions', () => { + let output = run({ conditions: ['foo', 'bar'] }); + assert.equal(output, ['default', 'foo', 'bar', 'import', 'node']); + }); + + it('option.conditions :: order', () => { + let output = run({ conditions: ['node', 'import', 'foobar'] }); + assert.equal(output, ['default', 'node', 'import', 'foobar']); + }); + + it('option.conditions :: unsafe', () => { + let output = run({ unsafe: true, conditions: ['foo', 'bar'] }); + assert.equal(output, ['default', 'foo', 'bar']); + }); + + it('option.conditions :: browser', () => { + let output = run({ browser: true, conditions: ['foo', 'bar'] }); + assert.equal(output, ['default', 'foo', 'bar', 'import', 'browser']); + }); + + it('option.conditions :: browser :: order', () => { + let output = run({ browser: true, conditions: ['browser', 'foobar'] }); + assert.equal(output, ['default', 'browser', 'foobar', 'import']); + }); + + it('option.conditions :: require', () => { + let output = run({ require: true, conditions: ['foo', 'bar'] }); + assert.equal(output, ['default', 'foo', 'bar', 'require', 'node']); + }); + + it('option.conditions :: require :: order', () => { + let output = run({ require: true, conditions: ['require', 'foobar'] }); + assert.equal(output, ['default', 'require', 'foobar', 'node']); + }); +}); + describe('$.toEntry', it => { const PKG = 'PACKAGE'; const EXTERNAL = 'EXTERNAL'; From 5093256f5ae4accf71e67ec749f257d6abb15313 Mon Sep 17 00:00:00 2001 From: Luke Edwards Date: Sat, 14 Jan 2023 16:54:51 -0800 Subject: [PATCH 14/35] chore: rewrite for shared walker logic --- src/alt.ts | 109 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 src/alt.ts diff --git a/src/alt.ts b/src/alt.ts new file mode 100644 index 0000000..ebbde82 --- /dev/null +++ b/src/alt.ts @@ -0,0 +1,109 @@ +import * as $ from './utils'; + +import type * as t from 'resolve.exports'; + +// function throws(msg: string): never { +// throw new Error(msg); +// } + +function throws(name: string, entry: Entry, condition?: number): never { + throw new Error( + condition + ? `No known conditions for "${entry}" entry in "${name}" package` + : `Missing "${entry}" export in "${name}" package` + ); +} + +export function exports(pkg: t.Package, input?: string, options?: t.Options): string[] | string | void { + let map = pkg.exports, + k: string; + + if (map) { + if (typeof map === 'string') { + map = { '.': map }; + } else for (k in map) { + // convert {conditions} to "."={condtions} + if (k[0] !== '.') map = { '.': map }; + break; + } + + return walk(pkg.name, map, input||'.', options); + } +} + +export function imports(pkg: t.Package, input: string, options?: t.Options): string[] | string | void { + if (pkg.imports) return walk(pkg.name, pkg.imports, input, options); +} + +type Entry = t.Exports.Entry | t.Imports.Entry; +type Value = t.Exports.Value | t.Imports.Value; +type Mapping = Record; + +function walk(name: string, mapping: Mapping, input: string, options?: t.Options): string[] | string { + let entry = $.toEntry(name, input); + let c = $.conditions(options || {}); + + let m: Value | undefined = mapping[entry]; + let replace: string | undefined; + let exact = m !== void 0; + + if (!exact) { + // loop for longest key match + let match: RegExpExecArray|null; + let longest: Entry|undefined; + let tmp: string|number; + let key: Entry; + + for (key in mapping) { + if (longest && key.length < longest.length) { + // do not allow "./" to match if already matched "./foo*" key + } else if (key[key.length - 1] === '/' && entry.startsWith(key)) { + replace = entry.substring(key.length); + longest = key; + } else { + // TODO: key.length > 1 (prevents ".") + // TODO: RegExp().exec everything? + tmp = key.indexOf('*', 2); + + if (!!~tmp) { + match = RegExp( + // TODO: this doesnt work for #import keys + '^\.\/' + key.substring(2, tmp) + '(.*)' + key.substring(1+tmp) + ).exec(entry); + + if (match && match[1]) { + replace = match[1]; + longest = key; + } + } + } + } + + m = mapping[longest!]; + } + + if (!m) { + // missing export + throws(name, entry); + } + + let v = $.loop(m, c); + // if (!v) throws('unknown condition'); + if (!v) throws(name, entry, 1); + + return (exact || !replace) ? v : injects(v, replace); +} + +function injects(item: string[]|string, value: string): string[]|string { + let bool = Array.isArray(item); + let arr: string[] = bool ? item as string[] : [item as string]; + let i=0, len=arr.length, rgx=/[*]/g, tmp: string; + + for (; i < len; i++) { + arr[i] = rgx.test(tmp = arr[i]) + ? tmp.replace(rgx, value) + : (tmp+value); + } + + return bool ? arr : arr[0]; +} From 707843bfa0a52dcfbc3656d742553de0a28e87f5 Mon Sep 17 00:00:00 2001 From: Luke Edwards Date: Sat, 14 Jan 2023 17:06:54 -0800 Subject: [PATCH 15/35] chore: shuffle internal file exports --- src/alt.ts | 92 ++------------------------------- src/index.ts | 53 ++----------------- src/legacy.ts | 46 +++++++++++++++++ src/utils.ts | 135 +++++++++++++++++++++++++++++++++++++++++++++--- test/legacy.ts | 2 +- test/resolve.ts | 2 +- 6 files changed, 185 insertions(+), 145 deletions(-) create mode 100644 src/legacy.ts diff --git a/src/alt.ts b/src/alt.ts index ebbde82..3036dcc 100644 --- a/src/alt.ts +++ b/src/alt.ts @@ -1,20 +1,11 @@ -import * as $ from './utils'; - +import { walk } from './utils'; import type * as t from 'resolve.exports'; -// function throws(msg: string): never { -// throw new Error(msg); -// } +export { legacy } from './legacy'; -function throws(name: string, entry: Entry, condition?: number): never { - throw new Error( - condition - ? `No known conditions for "${entry}" entry in "${name}" package` - : `Missing "${entry}" export in "${name}" package` - ); -} +type Output = string[] | string; -export function exports(pkg: t.Package, input?: string, options?: t.Options): string[] | string | void { +export function exports(pkg: t.Package, input?: string, options?: t.Options): Output | void { let map = pkg.exports, k: string; @@ -31,79 +22,6 @@ export function exports(pkg: t.Package, input?: string, options?: t.Options): st } } -export function imports(pkg: t.Package, input: string, options?: t.Options): string[] | string | void { +export function imports(pkg: t.Package, input: string, options?: t.Options): Output | void { if (pkg.imports) return walk(pkg.name, pkg.imports, input, options); } - -type Entry = t.Exports.Entry | t.Imports.Entry; -type Value = t.Exports.Value | t.Imports.Value; -type Mapping = Record; - -function walk(name: string, mapping: Mapping, input: string, options?: t.Options): string[] | string { - let entry = $.toEntry(name, input); - let c = $.conditions(options || {}); - - let m: Value | undefined = mapping[entry]; - let replace: string | undefined; - let exact = m !== void 0; - - if (!exact) { - // loop for longest key match - let match: RegExpExecArray|null; - let longest: Entry|undefined; - let tmp: string|number; - let key: Entry; - - for (key in mapping) { - if (longest && key.length < longest.length) { - // do not allow "./" to match if already matched "./foo*" key - } else if (key[key.length - 1] === '/' && entry.startsWith(key)) { - replace = entry.substring(key.length); - longest = key; - } else { - // TODO: key.length > 1 (prevents ".") - // TODO: RegExp().exec everything? - tmp = key.indexOf('*', 2); - - if (!!~tmp) { - match = RegExp( - // TODO: this doesnt work for #import keys - '^\.\/' + key.substring(2, tmp) + '(.*)' + key.substring(1+tmp) - ).exec(entry); - - if (match && match[1]) { - replace = match[1]; - longest = key; - } - } - } - } - - m = mapping[longest!]; - } - - if (!m) { - // missing export - throws(name, entry); - } - - let v = $.loop(m, c); - // if (!v) throws('unknown condition'); - if (!v) throws(name, entry, 1); - - return (exact || !replace) ? v : injects(v, replace); -} - -function injects(item: string[]|string, value: string): string[]|string { - let bool = Array.isArray(item); - let arr: string[] = bool ? item as string[] : [item as string]; - let i=0, len=arr.length, rgx=/[*]/g, tmp: string; - - for (; i < len; i++) { - arr[i] = rgx.test(tmp = arr[i]) - ? tmp.replace(rgx, value) - : (tmp+value); - } - - return bool ? arr : arr[0]; -} diff --git a/src/index.ts b/src/index.ts index 8938850..3d75b4b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,8 @@ import * as $ from './utils'; import type * as t from 'resolve.exports'; +export { legacy } from './legacy'; + /** * @param name The package name * @param entry The target entry, eg "." @@ -43,9 +45,9 @@ export function exports(pkg: t.Package, target: t.Exports.Entry, options?: t.Opt let allows = $.conditions(options||{}), key: t.Exports.Entry | string, - match: RegExpExecArray | null, longest: t.Exports.Entry | undefined, value: string | undefined | null, + match: RegExpExecArray | null, tmp: any, // mixed isONE = false; @@ -99,52 +101,3 @@ export function exports(pkg: t.Package, target: t.Exports.Entry, options?: t.Opt return bail(name, entry); } - -// --- -// --- -// --- - -type LegacyOptions = { - fields?: string[]; - browser?: string | boolean; -} - -type BrowserObject = { - // TODO: is this right? browser object format so loose - [file: string]: string | undefined; -} - -export function legacy(pkg: t.Package, options: LegacyOptions = {}): t.Path | t.Browser | void { - let i=0, - value: string | t.Browser | undefined, - browser = options.browser, - fields = options.fields || ['module', 'main'], - isSTRING = typeof browser == 'string'; - - if (browser && !fields.includes('browser')) { - fields.unshift('browser'); - // "module-a" -> "module-a" - // "./path/file.js" -> "./path/file.js" - // "foobar/path/file.js" -> "./path/file.js" - if (isSTRING) browser = $.toEntry(pkg.name, browser as string, true); - } - - for (; i < fields.length; i++) { - if (value = pkg[fields[i]]) { - if (typeof value == 'string') { - // - } else if (typeof value == 'object' && fields[i] == 'browser') { - if (isSTRING) { - value = (value as BrowserObject)[browser as string]; - if (value == null) return browser as string; - } - } else { - continue; - } - - return typeof value == 'string' - ? ('./' + value.replace(/^\.?\//, '')) as t.Path - : value; - } - } -} diff --git a/src/legacy.ts b/src/legacy.ts new file mode 100644 index 0000000..b19328e --- /dev/null +++ b/src/legacy.ts @@ -0,0 +1,46 @@ +import * as $ from './utils'; +import type * as t from 'resolve.exports'; + +type LegacyOptions = { + fields?: string[]; + browser?: string | boolean; +} + +type BrowserObject = { + [file: string]: string | undefined; +} + +export function legacy(pkg: t.Package, options: LegacyOptions = {}): t.Path | t.Browser | void { + let i=0, + value: string | t.Browser | undefined, + browser = options.browser, + fields = options.fields || ['module', 'main'], + isSTRING = typeof browser == 'string'; + + if (browser && !fields.includes('browser')) { + fields.unshift('browser'); + // "module-a" -> "module-a" + // "./path/file.js" -> "./path/file.js" + // "foobar/path/file.js" -> "./path/file.js" + if (isSTRING) browser = $.toEntry(pkg.name, browser as string, true); + } + + for (; i < fields.length; i++) { + if (value = pkg[fields[i]]) { + if (typeof value == 'string') { + // + } else if (typeof value == 'object' && fields[i] == 'browser') { + if (isSTRING) { + value = (value as BrowserObject)[browser as string]; + if (value == null) return browser as string; + } + } else { + continue; + } + + return typeof value == 'string' + ? ('./' + value.replace(/^\.?\//, '')) as t.Path + : value; + } + } +} diff --git a/src/utils.ts b/src/utils.ts index 1c00f15..e2ff1f2 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,17 @@ import type * as t from 'resolve.exports'; +export type Entry = t.Exports.Entry | t.Imports.Entry; +export type Value = t.Exports.Value | t.Imports.Value; +export type Mapping = Record; + +export function throws(name: string, entry: Entry, condition?: number): never { + throw new Error( + condition + ? `No known conditions for "${entry}" entry in "${name}" package` + : `Missing "${entry}" export in "${name}" package` + ); +} + export function conditions(options: t.Options): Set { let out = new Set([ 'default', ...options.conditions || [] ]); options.unsafe || out.add(options.require ? 'require' : 'import'); @@ -7,15 +19,82 @@ export function conditions(options: t.Options): Set { return out; } +export function walk(name: string, mapping: Mapping, input: string, options?: t.Options): string[] | string { + let entry = toEntry(name, input); + let c = conditions(options || {}); + + let m: Value | undefined = mapping[entry]; + let replace: string | undefined; + let exact = m !== void 0; + + if (!exact) { + // loop for longest key match + let match: RegExpExecArray|null; + let longest: Entry|undefined; + let tmp: string|number; + let key: Entry; + + for (key in mapping) { + if (longest && key.length < longest.length) { + // do not allow "./" to match if already matched "./foo*" key + } else if (key[key.length - 1] === '/' && entry.startsWith(key)) { + replace = entry.substring(key.length); + longest = key; + } else if (key.length > 1) { + // TODO: RegExp().exec everything? + tmp = key.indexOf('*', 2); + + if (!!~tmp) { + match = RegExp( + '^' + key.substring(0, tmp) + '(.*)' + key.substring(1+tmp) + ).exec(entry); + + if (match && match[1]) { + replace = match[1]; + longest = key; + } + } + } + } + + m = mapping[longest!]; + } + + if (!m) { + // missing export + throws(name, entry); + } + + let v = loop(m, c); + // if (!v) throws('unknown condition'); + if (!v) throws(name, entry, 1); + + return (exact || !replace) ? v : injects(v, replace); +} + +export function injects(item: string[]|string, value: string): string[]|string { + let bool = Array.isArray(item); + let arr: string[] = bool ? item as string[] : [item as string]; + let i=0, len=arr.length, rgx=/[*]/g, tmp: string; + + for (; i < len; i++) { + arr[i] = rgx.test(tmp = arr[i]) + ? tmp.replace(rgx, value) + : (tmp+value); + } + + return bool ? arr : arr[0]; +} + /** * @param name package name * @param ident entry identifier * @param externals allow non-path (external) result * @see https://esbench.com/bench/59fa3e6799634800a0349382 */ -export function toEntry(name: string, ident: string, externals?: false): t.Exports.Entry | t.Imports.Entry; -export function toEntry(name: string, ident: string, externals: true): t.Exports.Entry | t.Imports.Entry | string; -export function toEntry(name: string, ident: string, externals?: boolean): t.Exports.Entry | t.Imports.Entry | string { +export function toEntry(name: string, ident: string, externals?: false): Entry; +export function toEntry(name: string, ident: string, externals: true): Entry | string; +export function toEntry(name: string, ident: string, externals?: boolean): Entry | string { if (name === ident || ident === '.') return '.'; let root = name+'/', len = root.length; @@ -29,8 +108,7 @@ export function toEntry(name: string, ident: string, externals?: boolean): t.Exp : output as string | t.Exports.Entry; } -type Output = string[] | string | void; -export function loop(m: t.Exports.Value, keys: Set, result?: Set): Output { +export function loop(m: Value, keys: Set, result?: Set): string[] | string | void { if (m) { if (typeof m === 'string') { return m; @@ -39,7 +117,7 @@ export function loop(m: t.Exports.Value, keys: Set, result?: Set, - tmp: Output; + tmp: string[] | string | void; if (Array.isArray(m)) { arr = result || new Set; @@ -60,3 +138,48 @@ export function loop(m: t.Exports.Value, keys: Set, result?: Set [string,] +export function longest(map: Record|null, entry: Entry): void | [Entry, string] { + let key: string; + let match: RegExpExecArray|null; + let longest: Entry|undefined; + let value: string|undefined; + let tmp: string|number; + + for (key in map) { + if (longest && key.length < longest.length) { + // do not allow "./" to match if already matched "./foo*" key + } else if (key[key.length - 1] === '/' && entry.startsWith(key)) { + value = entry.substring(key.length); + longest = key as t.Exports.Entry; + } else { + tmp = key.indexOf('*', 2); + if (!!~tmp) { + match = RegExp( + '^\.\/' + key.substring(2, tmp) + '(.*)' + key.substring(1+tmp) + ).exec(entry); + + if (match && match[1]) { + value = match[1]; + longest = key as t.Exports.Entry; + } + } + } + } + + // must have a value + if (longest && value) { + return [longest, value]; + } + + // if (longest && value) { + // // must have a value + // tmp = loop(map[longest], allows); + // if (!tmp) return bail(name, entry); + + // return tmp.includes('*') + // ? tmp.replace(/[*]/g, value) + // : tmp + value; + // } +} diff --git a/test/legacy.ts b/test/legacy.ts index dd6e9ac..1d7249c 100644 --- a/test/legacy.ts +++ b/test/legacy.ts @@ -4,7 +4,7 @@ import * as lib from '../src'; import type { Package } from 'resolve.exports'; -const legacy = suite('$.legacy'); +const legacy = suite('lib.legacy'); legacy('should be a function', () => { assert.type(lib.legacy, 'function'); diff --git a/test/resolve.ts b/test/resolve.ts index b0a7b80..f5633dc 100644 --- a/test/resolve.ts +++ b/test/resolve.ts @@ -22,7 +22,7 @@ function fail(pkg: Package, target: Exports.Entry, entry?: string, options?: Opt // --- -const resolve = suite('$.resolve'); +const resolve = suite('lib.resolve'); resolve('should be a function', () => { assert.type(lib.resolve, 'function'); From 293fe345700e9160aacd22184cf2884efbcff42e Mon Sep 17 00:00:00 2001 From: Luke Edwards Date: Sat, 14 Jan 2023 17:07:14 -0800 Subject: [PATCH 16/35] chore: add `imports` and `exports` to dts file --- index.d.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/index.d.ts b/index.d.ts index c55b6e5..c54b337 100644 --- a/index.d.ts +++ b/index.d.ts @@ -7,6 +7,11 @@ export type Options = { export function resolve(pkg: T, entry: string, options?: Options): Imports.Output | Exports.Output | void; +type WithName = `${string}/${T}`; + +export function imports(pkg: T, entry: Imports.Entry|WithName, options?: Options): Imports.Output | void; +export function exports(pkg: T, target: Exports.Entry|WithName, options?: Options): Exports.Output | void; + export function legacy(pkg: T, options: { browser: true, fields?: readonly string[] }): Browser | void; export function legacy(pkg: T, options: { browser: string, fields?: readonly string[] }): string | false | void; export function legacy(pkg: T, options: { browser: false, fields?: readonly string[] }): string | void; From 62c782db055404abe117663e95f2d4e372e0d414 Mon Sep 17 00:00:00 2001 From: Luke Edwards Date: Sat, 14 Jan 2023 23:39:23 -0800 Subject: [PATCH 17/35] chore: move tests to `describe` syntax; - note: will be part of uvu@0.6 --- test/legacy.ts | 375 ++++++------- test/resolve.ts | 1405 ++++++++++++++++++++++++----------------------- 2 files changed, 892 insertions(+), 888 deletions(-) diff --git a/test/legacy.ts b/test/legacy.ts index 1d7249c..d923abd 100644 --- a/test/legacy.ts +++ b/test/legacy.ts @@ -1,225 +1,228 @@ -import { suite } from 'uvu'; +import * as uvu from 'uvu'; import * as assert from 'uvu/assert'; -import * as lib from '../src'; +import { legacy } from '../src/legacy'; import type { Package } from 'resolve.exports'; -const legacy = suite('lib.legacy'); - -legacy('should be a function', () => { - assert.type(lib.legacy, 'function'); -}); - -legacy('should prefer "module" > "main" entry', () => { - let pkg: Package = { - "name": "foobar", - "module": "build/module.js", - "main": "build/main.js", - }; - - let output = lib.legacy(pkg); - assert.is(output, './build/module.js'); -}); - -legacy('should read "main" field', () => { - let pkg: Package = { - "name": "foobar", - "main": "build/main.js", - }; - - let output = lib.legacy(pkg); - assert.is(output, './build/main.js'); -}); - -legacy('should return nothing when no fields', () => { - let pkg: Package = { - "name": "foobar" - }; - - let output = lib.legacy(pkg); - assert.is(output, undefined); -}); - -legacy('should ignore boolean-type field values', () => { - let pkg = { - "module": true, - "main": "main.js" - }; +function describe( + name: string, + cb: (it: uvu.Test) => void +) { + let t = uvu.suite(name); + cb(t); + t.run(); +} + +describe('lib.legacy', it => { + it('should be a function', () => { + assert.type(legacy, 'function'); + }); - let output = lib.legacy(pkg as any); - assert.is(output, './main.js'); -}); + it('should prefer "module" > "main" entry', () => { + let pkg: Package = { + "name": "foobar", + "module": "build/module.js", + "main": "build/main.js", + }; -legacy.run(); + let output = legacy(pkg); + assert.is(output, './build/module.js'); + }); -// --- + it('should read "main" field', () => { + let pkg: Package = { + "name": "foobar", + "main": "build/main.js", + }; -const fields = suite('options.fields', { - "name": "foobar", - "module": "build/module.js", - "browser": "build/browser.js", - "custom": "build/custom.js", - "main": "build/main.js", -}); + let output = legacy(pkg); + assert.is(output, './build/main.js'); + }); -fields('should customize field search order', pkg => { - let output = lib.legacy(pkg); - assert.is(output, './build/module.js', 'default: module'); + it('should return nothing when no fields', () => { + let pkg: Package = { + "name": "foobar" + }; - output = lib.legacy(pkg, { fields: ['main'] }); - assert.is(output, './build/main.js', 'custom: main only'); + let output = legacy(pkg); + assert.is(output, undefined); + }); - output = lib.legacy(pkg, { fields: ['custom', 'main', 'module'] }); - assert.is(output, './build/custom.js', 'custom: custom > main > module'); -}); + it('should ignore boolean-type field values', () => { + let pkg = { + "module": true, + "main": "main.js" + }; -fields('should return first *resolved* field', pkg => { - let output = lib.legacy(pkg, { - fields: ['howdy', 'partner', 'hello', 'world', 'main'] + let output = legacy(pkg as any); + assert.is(output, './main.js'); }); - - assert.is(output, './build/main.js'); }); -fields.run(); +describe('options.fields', it => { + let pkg: Package = { + "name": "foobar", + "module": "build/module.js", + "browser": "build/browser.js", + "custom": "build/custom.js", + "main": "build/main.js", + }; -// --- + it('should customize field search order', () => { + let output = legacy(pkg); + assert.is(output, './build/module.js', 'default: module'); -const browser = suite('options.browser', { - "name": "foobar", - "module": "build/module.js", - "browser": "build/browser.js", - "unpkg": "build/unpkg.js", - "main": "build/main.js", -}); + output = legacy(pkg, { fields: ['main'] }); + assert.is(output, './build/main.js', 'custom: main only'); -browser('should prioritize "browser" field when defined', pkg => { - let output = lib.legacy(pkg); - assert.is(output, './build/module.js'); + output = legacy(pkg, { fields: ['custom', 'main', 'module'] }); + assert.is(output, './build/custom.js', 'custom: custom > main > module'); + }); - output = lib.legacy(pkg, { browser: true }); - assert.is(output, './build/browser.js'); -}); + it('should return first *resolved* field', () => { + let output = legacy(pkg, { + fields: ['howdy', 'partner', 'hello', 'world', 'main'] + }); -browser('should respect existing "browser" order in custom fields', pkg => { - let output = lib.legacy(pkg, { - fields: ['main', 'browser'], - browser: true, + assert.is(output, './build/main.js'); }); - - assert.is(output, './build/main.js'); }); -// https://github.com/defunctzombie/package-browser-field-spec -browser('should resolve object format', () => { +describe('options.browser', it => { let pkg: Package = { "name": "foobar", - "browser": { - "module-a": "./shims/module-a.js", - "./server/only.js": "./shims/client-only.js" - } + "module": "build/module.js", + "browser": "build/browser.js", + "unpkg": "build/unpkg.js", + "main": "build/main.js", }; - assert.is( - lib.legacy(pkg, { browser: 'module-a' }), - './shims/module-a.js' - ); + it('should prioritize "browser" field when defined', () => { + let output = legacy(pkg); + assert.is(output, './build/module.js'); - assert.is( - lib.legacy(pkg, { browser: './server/only.js' }), - './shims/client-only.js' - ); - - assert.is( - lib.legacy(pkg, { browser: 'foobar/server/only.js' }), - './shims/client-only.js' - ); -}); + output = legacy(pkg, { browser: true }); + assert.is(output, './build/browser.js'); + }); -browser('should allow object format to "ignore" modules/files :: string', () => { - let pkg: Package = { - "name": "foobar", - "browser": { - "module-a": false, - "./foo.js": false, - } - }; + it('should respect existing "browser" order in custom fields', () => { + let output = legacy(pkg, { + fields: ['main', 'browser'], + browser: true, + }); - assert.is( - lib.legacy(pkg, { browser: 'module-a' }), - false - ); + assert.is(output, './build/main.js'); + }); - assert.is( - lib.legacy(pkg, { browser: './foo.js' }), - false - ); + // https://github.com/defunctzombie/package-browser-field-spec + it('should resolve object format', () => { + let pkg: Package = { + "name": "foobar", + "browser": { + "module-a": "./shims/module-a.js", + "./server/only.js": "./shims/client-only.js" + } + }; + + assert.is( + legacy(pkg, { browser: 'module-a' }), + './shims/module-a.js' + ); + + assert.is( + legacy(pkg, { browser: './server/only.js' }), + './shims/client-only.js' + ); + + assert.is( + legacy(pkg, { browser: 'foobar/server/only.js' }), + './shims/client-only.js' + ); + }); - assert.is( - lib.legacy(pkg, { browser: 'foobar/foo.js' }), - false - ); -}); + it('should allow object format to "ignore" modules/files :: string', () => { + let pkg: Package = { + "name": "foobar", + "browser": { + "module-a": false, + "./foo.js": false, + } + }; + + assert.is( + legacy(pkg, { browser: 'module-a' }), + false + ); + + assert.is( + legacy(pkg, { browser: './foo.js' }), + false + ); + + assert.is( + legacy(pkg, { browser: 'foobar/foo.js' }), + false + ); + }); -browser('should return the `browser` string (entry) if no custom mapping :: string', () => { - let pkg: Package = { - "name": "foobar", - "browser": { - // - } - }; + it('should return the `browser` string (entry) if no custom mapping :: string', () => { + let pkg: Package = { + "name": "foobar", + "browser": { + // + } + }; + + assert.is( + legacy(pkg, { + browser: './hello.js' + }), + './hello.js' + ); + + assert.is( + legacy(pkg, { + browser: 'foobar/hello.js' + }), + './hello.js' + ); + }); - assert.is( - lib.legacy(pkg, { - browser: './hello.js' - }), - './hello.js' - ); - - assert.is( - lib.legacy(pkg, { - browser: 'foobar/hello.js' - }), - './hello.js' - ); -}); + it('should return the full "browser" object :: true', () => { + let pkg: Package = { + "name": "foobar", + "browser": { + "./other.js": "./world.js" + } + }; -browser('should return the full "browser" object :: true', () => { - let pkg: Package = { - "name": "foobar", - "browser": { - "./other.js": "./world.js" - } - }; + let output = legacy(pkg, { + browser: true + }); - let output = lib.legacy(pkg, { - browser: true + assert.equal(output, pkg.browser); }); - assert.equal(output, pkg.browser); -}); - -browser('still ensures string output is made relative', () => { - let pkg: Package = { - "name": "foobar", - "browser": { - "./foo.js": "bar.js", - } - } as any; - - assert.is( - lib.legacy(pkg, { - browser: './foo.js' - }), - './bar.js' - ); - - assert.is( - lib.legacy(pkg, { - browser: 'foobar/foo.js' - }), - './bar.js' - ); + it('still ensures string output is made relative', () => { + let pkg: Package = { + "name": "foobar", + "browser": { + "./foo.js": "bar.js", + } + } as any; + + assert.is( + legacy(pkg, { + browser: './foo.js' + }), + './bar.js' + ); + + assert.is( + legacy(pkg, { + browser: 'foobar/foo.js' + }), + './bar.js' + ); + }); }); - -browser.run(); diff --git a/test/resolve.ts b/test/resolve.ts index f5633dc..f94599b 100644 --- a/test/resolve.ts +++ b/test/resolve.ts @@ -1,4 +1,4 @@ -import { suite } from 'uvu'; +import * as uvu from 'uvu'; import * as assert from 'uvu/assert'; import * as lib from '../src'; @@ -20,887 +20,888 @@ function fail(pkg: Package, target: Exports.Entry, entry?: string, options?: Opt } } -// --- - -const resolve = suite('lib.resolve'); - -resolve('should be a function', () => { - assert.type(lib.resolve, 'function'); -}); - -resolve('exports=string', () => { - let pkg: Package = { - "name": "foobar", - "exports": "./$string", - }; - - pass(pkg, './$string'); - pass(pkg, './$string', '.'); - pass(pkg, './$string', 'foobar'); - - fail(pkg, './other', 'other'); - fail(pkg, './other', 'foobar/other'); - fail(pkg, './hello', './hello'); -}); - -resolve('exports = { self }', () => { - let pkg: Package = { - "name": "foobar", - "exports": { - "import": "./$import", - "require": "./$require", - } - }; +function describe( + name: string, + cb: (it: uvu.Test) => void +) { + let t = uvu.suite(name); + cb(t); + t.run(); +} - pass(pkg, './$import'); - pass(pkg, './$import', '.'); - pass(pkg, './$import', 'foobar'); +// --- - fail(pkg, './other', 'other'); - fail(pkg, './other', 'foobar/other'); - fail(pkg, './hello', './hello'); -}); +describe('lib.resolve', it => { + it('should be a function', () => { + assert.type(lib.resolve, 'function'); + }); -resolve('exports["."] = string', () => { - let pkg: Package = { - "name": "foobar", - "exports": { - ".": "./$self", - } - }; + it('exports=string', () => { + let pkg: Package = { + "name": "foobar", + "exports": "./$string", + }; - pass(pkg, './$self'); - pass(pkg, './$self', '.'); - pass(pkg, './$self', 'foobar'); + pass(pkg, './$string'); + pass(pkg, './$string', '.'); + pass(pkg, './$string', 'foobar'); - fail(pkg, './other', 'other'); - fail(pkg, './other', 'foobar/other'); - fail(pkg, './hello', './hello'); -}); + fail(pkg, './other', 'other'); + fail(pkg, './other', 'foobar/other'); + fail(pkg, './hello', './hello'); + }); -resolve('exports["."] = object', () => { - let pkg: Package = { - "name": "foobar", - "exports": { - ".": { + it('exports = { self }', () => { + let pkg: Package = { + "name": "foobar", + "exports": { "import": "./$import", "require": "./$require", } - } - }; + }; - pass(pkg, './$import'); - pass(pkg, './$import', '.'); - pass(pkg, './$import', 'foobar'); + pass(pkg, './$import'); + pass(pkg, './$import', '.'); + pass(pkg, './$import', 'foobar'); - fail(pkg, './other', 'other'); - fail(pkg, './other', 'foobar/other'); - fail(pkg, './hello', './hello'); -}); + fail(pkg, './other', 'other'); + fail(pkg, './other', 'foobar/other'); + fail(pkg, './hello', './hello'); + }); -resolve('exports["./foo"] = string', () => { - let pkg: Package = { - "name": "foobar", - "exports": { - "./foo": "./$import", - } - }; + it('exports["."] = string', () => { + let pkg: Package = { + "name": "foobar", + "exports": { + ".": "./$self", + } + }; - pass(pkg, './$import', './foo'); - pass(pkg, './$import', 'foobar/foo'); + pass(pkg, './$self'); + pass(pkg, './$self', '.'); + pass(pkg, './$self', 'foobar'); - fail(pkg, '.'); - fail(pkg, '.', 'foobar'); - fail(pkg, './other', 'foobar/other'); -}); + fail(pkg, './other', 'other'); + fail(pkg, './other', 'foobar/other'); + fail(pkg, './hello', './hello'); + }); -resolve('exports["./foo"] = object', () => { - let pkg: Package = { - "name": "foobar", - "exports": { - "./foo": { - "import": "./$import", - "require": "./$require", + it('exports["."] = object', () => { + let pkg: Package = { + "name": "foobar", + "exports": { + ".": { + "import": "./$import", + "require": "./$require", + } } - } - }; + }; - pass(pkg, './$import', './foo'); - pass(pkg, './$import', 'foobar/foo'); + pass(pkg, './$import'); + pass(pkg, './$import', '.'); + pass(pkg, './$import', 'foobar'); - fail(pkg, '.'); - fail(pkg, '.', 'foobar'); - fail(pkg, './other', 'foobar/other'); -}); + fail(pkg, './other', 'other'); + fail(pkg, './other', 'foobar/other'); + fail(pkg, './hello', './hello'); + }); -// https://nodejs.org/api/packages.html#packages_nested_conditions -resolve('nested conditions', () => { - let pkg: Package = { - "name": "foobar", - "exports": { - "node": { - "import": "././$node.import", - "require": "././$node.require" - }, - "default": "./$default", - } - }; + it('exports["./foo"] = string', () => { + let pkg: Package = { + "name": "foobar", + "exports": { + "./foo": "./$import", + } + }; - pass(pkg, '././$node.import'); - pass(pkg, '././$node.import', 'foobar'); + pass(pkg, './$import', './foo'); + pass(pkg, './$import', 'foobar/foo'); - // browser => no "node" key - pass(pkg, './$default', '.', { browser: true }); - pass(pkg, './$default', 'foobar', { browser: true }); + fail(pkg, '.'); + fail(pkg, '.', 'foobar'); + fail(pkg, './other', 'foobar/other'); + }); - fail(pkg, './hello', './hello'); - fail(pkg, './other', 'foobar/other'); - fail(pkg, './other', 'other'); -}); + it('exports["./foo"] = object', () => { + let pkg: Package = { + "name": "foobar", + "exports": { + "./foo": { + "import": "./$import", + "require": "./$require", + } + } + }; -resolve('nested conditions :: subpath', () => { - let pkg: Package = { - "name": "foobar", - "exports": { - "./lite": { + pass(pkg, './$import', './foo'); + pass(pkg, './$import', 'foobar/foo'); + + fail(pkg, '.'); + fail(pkg, '.', 'foobar'); + fail(pkg, './other', 'foobar/other'); + }); + + // https://nodejs.org/api/packages.html#packages_nested_conditions + it('nested conditions', () => { + let pkg: Package = { + "name": "foobar", + "exports": { "node": { "import": "././$node.import", "require": "././$node.require" }, - "browser": { - "import": "././$browser.import", - "require": "././$browser.require" - }, + "default": "./$default", } - } - }; + }; - pass(pkg, '././$node.import', 'foobar/lite'); - pass(pkg, '././$node.require', 'foobar/lite', { require: true }); + pass(pkg, '././$node.import'); + pass(pkg, '././$node.import', 'foobar'); - pass(pkg, '././$browser.import', 'foobar/lite', { browser: true }); - pass(pkg, '././$browser.require', 'foobar/lite', { browser: true, require: true }); -}); + // browser => no "node" key + pass(pkg, './$default', '.', { browser: true }); + pass(pkg, './$default', 'foobar', { browser: true }); -resolve('nested conditions :: subpath :: inverse', () => { - let pkg: Package = { - "name": "foobar", - "exports": { - "./lite": { - "import": { - "browser": "././$browser.import", - "node": "././$node.import", - }, - "require": { - "browser": "././$browser.require", - "node": "././$node.require", + fail(pkg, './hello', './hello'); + fail(pkg, './other', 'foobar/other'); + fail(pkg, './other', 'other'); + }); + + it('nested conditions :: subpath', () => { + let pkg: Package = { + "name": "foobar", + "exports": { + "./lite": { + "node": { + "import": "././$node.import", + "require": "././$node.require" + }, + "browser": { + "import": "././$browser.import", + "require": "././$browser.require" + }, } } - } - }; + }; - pass(pkg, '././$node.import', 'foobar/lite'); - pass(pkg, '././$node.require', 'foobar/lite', { require: true }); + pass(pkg, '././$node.import', 'foobar/lite'); + pass(pkg, '././$node.require', 'foobar/lite', { require: true }); - pass(pkg, '././$browser.import', 'foobar/lite', { browser: true }); - pass(pkg, '././$browser.require', 'foobar/lite', { browser: true, require: true }); -}); + pass(pkg, '././$browser.import', 'foobar/lite', { browser: true }); + pass(pkg, '././$browser.require', 'foobar/lite', { browser: true, require: true }); + }); -// https://nodejs.org/api/packages.html#packages_subpath_folder_mappings -resolve('exports["./"]', () => { - let pkg: Package = { - "name": "foobar", - "exports": { - ".": { - "require": "./$require", - "import": "./$import" - }, - "./package.json": "./package.json", - "./": "./" - } - }; + it('nested conditions :: subpath :: inverse', () => { + let pkg: Package = { + "name": "foobar", + "exports": { + "./lite": { + "import": { + "browser": "././$browser.import", + "node": "././$node.import", + }, + "require": { + "browser": "././$browser.require", + "node": "././$node.require", + } + } + } + }; - pass(pkg, './$import'); - pass(pkg, './$import', 'foobar'); - pass(pkg, './$require', 'foobar', { require: true }); + pass(pkg, '././$node.import', 'foobar/lite'); + pass(pkg, '././$node.require', 'foobar/lite', { require: true }); - pass(pkg, './package.json', 'package.json'); - pass(pkg, './package.json', 'foobar/package.json'); - pass(pkg, './package.json', './package.json'); + pass(pkg, '././$browser.import', 'foobar/lite', { browser: true }); + pass(pkg, '././$browser.require', 'foobar/lite', { browser: true, require: true }); + }); - // "loose" / everything exposed - pass(pkg, './hello.js', 'hello.js'); - pass(pkg, './hello.js', 'foobar/hello.js'); - pass(pkg, './hello/world.js', './hello/world.js'); -}); + // https://nodejs.org/api/packages.html#packages_subpath_folder_mappings + it('exports["./"]', () => { + let pkg: Package = { + "name": "foobar", + "exports": { + ".": { + "require": "./$require", + "import": "./$import" + }, + "./package.json": "./package.json", + "./": "./" + } + }; -resolve('exports["./"] :: w/o "." key', () => { - let pkg: Package = { - "name": "foobar", - "exports": { - "./package.json": "./package.json", - "./": "./" - } - }; + pass(pkg, './$import'); + pass(pkg, './$import', 'foobar'); + pass(pkg, './$require', 'foobar', { require: true }); - fail(pkg, '.', "."); - fail(pkg, '.', "foobar"); + pass(pkg, './package.json', 'package.json'); + pass(pkg, './package.json', 'foobar/package.json'); + pass(pkg, './package.json', './package.json'); - pass(pkg, './package.json', 'package.json'); - pass(pkg, './package.json', 'foobar/package.json'); - pass(pkg, './package.json', './package.json'); + // "loose" / everything exposed + pass(pkg, './hello.js', 'hello.js'); + pass(pkg, './hello.js', 'foobar/hello.js'); + pass(pkg, './hello/world.js', './hello/world.js'); + }); - // "loose" / everything exposed - pass(pkg, './hello.js', 'hello.js'); - pass(pkg, './hello.js', 'foobar/hello.js'); - pass(pkg, './hello/world.js', './hello/world.js'); -}); + it('exports["./"] :: w/o "." key', () => { + let pkg: Package = { + "name": "foobar", + "exports": { + "./package.json": "./package.json", + "./": "./" + } + }; -// https://nodejs.org/api/packages.html#packages_subpath_folder_mappings -resolve('exports["./*"]', () => { - let pkg: Package = { - "name": "foobar", - "exports": { - "./*": "./cheese/*.mjs" - } - }; + fail(pkg, '.', "."); + fail(pkg, '.', "foobar"); - fail(pkg, '.', "."); - fail(pkg, '.', "foobar"); + pass(pkg, './package.json', 'package.json'); + pass(pkg, './package.json', 'foobar/package.json'); + pass(pkg, './package.json', './package.json'); - pass(pkg, './cheese/hello.mjs', 'hello'); - pass(pkg, './cheese/hello.mjs', 'foobar/hello'); - pass(pkg, './cheese/hello/world.mjs', './hello/world'); + // "loose" / everything exposed + pass(pkg, './hello.js', 'hello.js'); + pass(pkg, './hello.js', 'foobar/hello.js'); + pass(pkg, './hello/world.js', './hello/world.js'); + }); - // evaluate as defined, not wrong - pass(pkg, './cheese/hello.js.mjs', 'hello.js'); - pass(pkg, './cheese/hello.js.mjs', 'foobar/hello.js'); - pass(pkg, './cheese/hello/world.js.mjs', './hello/world.js'); -}); + // https://nodejs.org/api/packages.html#packages_subpath_folder_mappings + it('exports["./*"]', () => { + let pkg: Package = { + "name": "foobar", + "exports": { + "./*": "./cheese/*.mjs" + } + }; -resolve('exports["./dir*"]', () => { - let pkg: Package = { - "name": "foobar", - "exports": { - "./dir*": "./cheese/*.mjs" - } - }; + fail(pkg, '.', "."); + fail(pkg, '.', "foobar"); - fail(pkg, '.', "."); - fail(pkg, '.', "foobar"); + pass(pkg, './cheese/hello.mjs', 'hello'); + pass(pkg, './cheese/hello.mjs', 'foobar/hello'); + pass(pkg, './cheese/hello/world.mjs', './hello/world'); - pass(pkg, './cheese/test.mjs', 'dirtest'); - pass(pkg, './cheese/test.mjs', 'foobar/dirtest'); + // evaluate as defined, not wrong + pass(pkg, './cheese/hello.js.mjs', 'hello.js'); + pass(pkg, './cheese/hello.js.mjs', 'foobar/hello.js'); + pass(pkg, './cheese/hello/world.js.mjs', './hello/world.js'); + }); - pass(pkg, './cheese/test/wheel.mjs', 'dirtest/wheel'); - pass(pkg, './cheese/test/wheel.mjs', 'foobar/dirtest/wheel'); -}); + it('exports["./dir*"]', () => { + let pkg: Package = { + "name": "foobar", + "exports": { + "./dir*": "./cheese/*.mjs" + } + }; -// https://github.com/lukeed/resolve.exports/issues/9 -resolve('exports["./dir*"] :: repeat "*" value', () => { - let pkg: Package = { - "name": "foobar", - "exports": { - "./dir*": "./*sub/dir*/file.js" - } - }; + fail(pkg, '.', "."); + fail(pkg, '.', "foobar"); - fail(pkg, '.', "."); - fail(pkg, '.', "foobar"); + pass(pkg, './cheese/test.mjs', 'dirtest'); + pass(pkg, './cheese/test.mjs', 'foobar/dirtest'); - pass(pkg, './testsub/dirtest/file.js', 'dirtest'); - pass(pkg, './testsub/dirtest/file.js', 'foobar/dirtest'); + pass(pkg, './cheese/test/wheel.mjs', 'dirtest/wheel'); + pass(pkg, './cheese/test/wheel.mjs', 'foobar/dirtest/wheel'); + }); - pass(pkg, './test/innersub/dirtest/inner/file.js', 'dirtest/inner'); - pass(pkg, './test/innersub/dirtest/inner/file.js', 'foobar/dirtest/inner'); -}); + // https://github.com/lukeed/resolve.exports/issues/9 + it('exports["./dir*"] :: repeat "*" value', () => { + let pkg: Package = { + "name": "foobar", + "exports": { + "./dir*": "./*sub/dir*/file.js" + } + }; -resolve('exports["./dir*"] :: share "name" start', () => { - let pkg: Package = { - "name": "director", - "exports": { - "./dir*": "./*sub/dir*/file.js" - } - }; + fail(pkg, '.', "."); + fail(pkg, '.', "foobar"); - fail(pkg, '.', "."); - fail(pkg, '.', "director"); + pass(pkg, './testsub/dirtest/file.js', 'dirtest'); + pass(pkg, './testsub/dirtest/file.js', 'foobar/dirtest'); - pass(pkg, './testsub/dirtest/file.js', 'dirtest'); - pass(pkg, './testsub/dirtest/file.js', 'director/dirtest'); + pass(pkg, './test/innersub/dirtest/inner/file.js', 'dirtest/inner'); + pass(pkg, './test/innersub/dirtest/inner/file.js', 'foobar/dirtest/inner'); + }); - pass(pkg, './test/innersub/dirtest/inner/file.js', 'dirtest/inner'); - pass(pkg, './test/innersub/dirtest/inner/file.js', 'director/dirtest/inner'); -}); + it('exports["./dir*"] :: share "name" start', () => { + let pkg: Package = { + "name": "director", + "exports": { + "./dir*": "./*sub/dir*/file.js" + } + }; -/** - * @deprecated Documentation-only deprecation in Node 14.13 - * @deprecated Runtime deprecation in Node 16.0 - * @removed Removed in Node 18.0 - * @see https://nodejs.org/docs/latest-v16.x/api/packages.html#subpath-folder-mappings - */ -resolve('exports["./features/"]', () => { - let pkg: Package = { - "name": "foobar", - "exports": { - "./features/": "./features/" - } - }; + fail(pkg, '.', "."); + fail(pkg, '.', "director"); - pass(pkg, './features/', 'features/'); - pass(pkg, './features/', 'foobar/features/'); + pass(pkg, './testsub/dirtest/file.js', 'dirtest'); + pass(pkg, './testsub/dirtest/file.js', 'director/dirtest'); - pass(pkg, './features/hello.js', 'foobar/features/hello.js'); + pass(pkg, './test/innersub/dirtest/inner/file.js', 'dirtest/inner'); + pass(pkg, './test/innersub/dirtest/inner/file.js', 'director/dirtest/inner'); + }); - fail(pkg, './features', 'features'); - fail(pkg, './features', 'foobar/features'); + /** + * @deprecated Documentation-only deprecation in Node 14.13 + * @deprecated Runtime deprecation in Node 16.0 + * @removed Removed in Node 18.0 + * @see https://nodejs.org/docs/latest-v16.x/api/packages.html#subpath-folder-mappings + */ + it('exports["./features/"]', () => { + let pkg: Package = { + "name": "foobar", + "exports": { + "./features/": "./features/" + } + }; - fail(pkg, './package.json', 'package.json'); - fail(pkg, './package.json', 'foobar/package.json'); - fail(pkg, './package.json', './package.json'); -}); + pass(pkg, './features/', 'features/'); + pass(pkg, './features/', 'foobar/features/'); -// https://nodejs.org/api/packages.html#packages_subpath_folder_mappings -resolve('exports["./features/"] :: with "./" key', () => { - let pkg: Package = { - "name": "foobar", - "exports": { - "./features/": "./features/", - "./package.json": "./package.json", - "./": "./" - } - }; + pass(pkg, './features/hello.js', 'foobar/features/hello.js'); - pass(pkg, './features', 'features'); // via "./" - pass(pkg, './features', 'foobar/features'); // via "./" + fail(pkg, './features', 'features'); + fail(pkg, './features', 'foobar/features'); - pass(pkg, './features/', 'features/'); // via "./features/" - pass(pkg, './features/', 'foobar/features/'); // via "./features/" + fail(pkg, './package.json', 'package.json'); + fail(pkg, './package.json', 'foobar/package.json'); + fail(pkg, './package.json', './package.json'); + }); - pass(pkg, './features/hello.js', 'foobar/features/hello.js'); + // https://nodejs.org/api/packages.html#packages_subpath_folder_mappings + it('exports["./features/"] :: with "./" key', () => { + let pkg: Package = { + "name": "foobar", + "exports": { + "./features/": "./features/", + "./package.json": "./package.json", + "./": "./" + } + }; - pass(pkg, './package.json', 'package.json'); - pass(pkg, './package.json', 'foobar/package.json'); - pass(pkg, './package.json', './package.json'); + pass(pkg, './features', 'features'); // via "./" + pass(pkg, './features', 'foobar/features'); // via "./" - // Does NOT hit "./" (match Node) - fail(pkg, '.', '.'); - fail(pkg, '.', 'foobar'); -}); + pass(pkg, './features/', 'features/'); // via "./features/" + pass(pkg, './features/', 'foobar/features/'); // via "./features/" -resolve('exports["./features/"] :: conditions', () => { - let pkg: Package = { - "name": "foobar", - "exports": { - "./features/": { - "browser": { - "import": "./browser.import/", - "require": "./browser.require/", - }, - "import": "./import/", - "require": "./require/", - }, - } - }; + pass(pkg, './features/hello.js', 'foobar/features/hello.js'); - // import - pass(pkg, './import/', 'features/'); - pass(pkg, './import/', 'foobar/features/'); + pass(pkg, './package.json', 'package.json'); + pass(pkg, './package.json', 'foobar/package.json'); + pass(pkg, './package.json', './package.json'); - pass(pkg, './import/hello.js', './features/hello.js'); - pass(pkg, './import/hello.js', 'foobar/features/hello.js'); + // Does NOT hit "./" (match Node) + fail(pkg, '.', '.'); + fail(pkg, '.', 'foobar'); + }); - // require - pass(pkg, './require/', 'features/', { require: true }); - pass(pkg, './require/', 'foobar/features/', { require: true }); + it('exports["./features/"] :: conditions', () => { + let pkg: Package = { + "name": "foobar", + "exports": { + "./features/": { + "browser": { + "import": "./browser.import/", + "require": "./browser.require/", + }, + "import": "./import/", + "require": "./require/", + }, + } + }; - pass(pkg, './require/hello.js', './features/hello.js', { require: true }); - pass(pkg, './require/hello.js', 'foobar/features/hello.js', { require: true }); + // import + pass(pkg, './import/', 'features/'); + pass(pkg, './import/', 'foobar/features/'); - // require + browser - pass(pkg, './browser.require/', 'features/', { browser: true, require: true }); - pass(pkg, './browser.require/', 'foobar/features/', { browser: true, require: true }); + pass(pkg, './import/hello.js', './features/hello.js'); + pass(pkg, './import/hello.js', 'foobar/features/hello.js'); - pass(pkg, './browser.require/hello.js', './features/hello.js', { browser: true, require: true }); - pass(pkg, './browser.require/hello.js', 'foobar/features/hello.js', { browser: true, require: true }); -}); + // require + pass(pkg, './require/', 'features/', { require: true }); + pass(pkg, './require/', 'foobar/features/', { require: true }); -// https://nodejs.org/api/packages.html#packages_subpath_folder_mappings -resolve('exports["./features/*"]', () => { - let pkg: Package = { - "name": "foobar", - "exports": { - "./features/*": "./features/*.js", - } - }; + pass(pkg, './require/hello.js', './features/hello.js', { require: true }); + pass(pkg, './require/hello.js', 'foobar/features/hello.js', { require: true }); - fail(pkg, './features', 'features'); - fail(pkg, './features', 'foobar/features'); + // require + browser + pass(pkg, './browser.require/', 'features/', { browser: true, require: true }); + pass(pkg, './browser.require/', 'foobar/features/', { browser: true, require: true }); - fail(pkg, './features/', 'features/'); - fail(pkg, './features/', 'foobar/features/'); + pass(pkg, './browser.require/hello.js', './features/hello.js', { browser: true, require: true }); + pass(pkg, './browser.require/hello.js', 'foobar/features/hello.js', { browser: true, require: true }); + }); - pass(pkg, './features/a.js', 'foobar/features/a'); - pass(pkg, './features/ab.js', 'foobar/features/ab'); - pass(pkg, './features/abc.js', 'foobar/features/abc'); + // https://nodejs.org/api/packages.html#packages_subpath_folder_mappings + it('exports["./features/*"]', () => { + let pkg: Package = { + "name": "foobar", + "exports": { + "./features/*": "./features/*.js", + } + }; - pass(pkg, './features/hello.js', 'foobar/features/hello'); - pass(pkg, './features/foo/bar.js', 'foobar/features/foo/bar'); + fail(pkg, './features', 'features'); + fail(pkg, './features', 'foobar/features'); - // Valid: Pattern trailers allow any exact substrings to be matched - pass(pkg, './features/hello.js.js', 'foobar/features/hello.js'); - pass(pkg, './features/foo/bar.js.js', 'foobar/features/foo/bar.js'); + fail(pkg, './features/', 'features/'); + fail(pkg, './features/', 'foobar/features/'); - fail(pkg, './package.json', 'package.json'); - fail(pkg, './package.json', 'foobar/package.json'); - fail(pkg, './package.json', './package.json'); -}); + pass(pkg, './features/a.js', 'foobar/features/a'); + pass(pkg, './features/ab.js', 'foobar/features/ab'); + pass(pkg, './features/abc.js', 'foobar/features/abc'); -// https://nodejs.org/api/packages.html#packages_subpath_folder_mappings -resolve('exports["./features/*"] :: with "./" key', () => { - let pkg: Package = { - "name": "foobar", - "exports": { - "./features/*": "./features/*.js", - "./": "./" - } - }; + pass(pkg, './features/hello.js', 'foobar/features/hello'); + pass(pkg, './features/foo/bar.js', 'foobar/features/foo/bar'); - pass(pkg, './features', 'features'); // via "./" - pass(pkg, './features', 'foobar/features'); // via "./" + // Valid: Pattern trailers allow any exact substrings to be matched + pass(pkg, './features/hello.js.js', 'foobar/features/hello.js'); + pass(pkg, './features/foo/bar.js.js', 'foobar/features/foo/bar.js'); - pass(pkg, './features/', 'features/'); // via "./" - pass(pkg, './features/', 'foobar/features/'); // via "./" + fail(pkg, './package.json', 'package.json'); + fail(pkg, './package.json', 'foobar/package.json'); + fail(pkg, './package.json', './package.json'); + }); - pass(pkg, './features/hello.js', 'foobar/features/hello'); - pass(pkg, './features/foo/bar.js', 'foobar/features/foo/bar'); + // https://nodejs.org/api/packages.html#packages_subpath_folder_mappings + it('exports["./features/*"] :: with "./" key', () => { + let pkg: Package = { + "name": "foobar", + "exports": { + "./features/*": "./features/*.js", + "./": "./" + } + }; - // Valid: Pattern trailers allow any exact substrings to be matched - pass(pkg, './features/hello.js.js', 'foobar/features/hello.js'); - pass(pkg, './features/foo/bar.js.js', 'foobar/features/foo/bar.js'); + pass(pkg, './features', 'features'); // via "./" + pass(pkg, './features', 'foobar/features'); // via "./" - pass(pkg, './package.json', 'package.json'); - pass(pkg, './package.json', 'foobar/package.json'); - pass(pkg, './package.json', './package.json'); + pass(pkg, './features/', 'features/'); // via "./" + pass(pkg, './features/', 'foobar/features/'); // via "./" - // Does NOT hit "./" (match Node) - fail(pkg, '.', '.'); - fail(pkg, '.', 'foobar'); -}); + pass(pkg, './features/hello.js', 'foobar/features/hello'); + pass(pkg, './features/foo/bar.js', 'foobar/features/foo/bar'); -// https://github.com/lukeed/resolve.exports/issues/7 -resolve('exports["./features/*"] :: with "./" key first', () => { - let pkg: Package = { - "name": "foobar", - "exports": { - "./": "./", - "./features/*": "./features/*.js" - } - }; + // Valid: Pattern trailers allow any exact substrings to be matched + pass(pkg, './features/hello.js.js', 'foobar/features/hello.js'); + pass(pkg, './features/foo/bar.js.js', 'foobar/features/foo/bar.js'); - pass(pkg, './features', 'features'); // via "./" - pass(pkg, './features', 'foobar/features'); // via "./" + pass(pkg, './package.json', 'package.json'); + pass(pkg, './package.json', 'foobar/package.json'); + pass(pkg, './package.json', './package.json'); - pass(pkg, './features/', 'features/'); // via "./" - pass(pkg, './features/', 'foobar/features/'); // via "./" + // Does NOT hit "./" (match Node) + fail(pkg, '.', '.'); + fail(pkg, '.', 'foobar'); + }); - pass(pkg, './features/hello.js', 'foobar/features/hello'); - pass(pkg, './features/foo/bar.js', 'foobar/features/foo/bar'); + // https://github.com/lukeed/resolve.exports/issues/7 + it('exports["./features/*"] :: with "./" key first', () => { + let pkg: Package = { + "name": "foobar", + "exports": { + "./": "./", + "./features/*": "./features/*.js" + } + }; - // Valid: Pattern trailers allow any exact substrings to be matched - pass(pkg, './features/hello.js.js', 'foobar/features/hello.js'); - pass(pkg, './features/foo/bar.js.js', 'foobar/features/foo/bar.js'); + pass(pkg, './features', 'features'); // via "./" + pass(pkg, './features', 'foobar/features'); // via "./" - pass(pkg, './package.json', 'package.json'); - pass(pkg, './package.json', 'foobar/package.json'); - pass(pkg, './package.json', './package.json'); + pass(pkg, './features/', 'features/'); // via "./" + pass(pkg, './features/', 'foobar/features/'); // via "./" - // Does NOT hit "./" (match Node) - fail(pkg, '.', '.'); - fail(pkg, '.', 'foobar'); -}); + pass(pkg, './features/hello.js', 'foobar/features/hello'); + pass(pkg, './features/foo/bar.js', 'foobar/features/foo/bar'); -// https://github.com/lukeed/resolve.exports/issues/16 -resolve('exports["./features/*"] :: with `null` internals', () => { - let pkg: Package = { - "name": "foobar", - "exports": { - "./features/*": "./src/features/*.js", - "./features/internal/*": null - } - }; + // Valid: Pattern trailers allow any exact substrings to be matched + pass(pkg, './features/hello.js.js', 'foobar/features/hello.js'); + pass(pkg, './features/foo/bar.js.js', 'foobar/features/foo/bar.js'); - pass(pkg, './src/features/hello.js', 'features/hello'); - pass(pkg, './src/features/hello.js', 'foobar/features/hello'); + pass(pkg, './package.json', 'package.json'); + pass(pkg, './package.json', 'foobar/package.json'); + pass(pkg, './package.json', './package.json'); - pass(pkg, './src/features/foo/bar.js', 'features/foo/bar'); - pass(pkg, './src/features/foo/bar.js', 'foobar/features/foo/bar'); + // Does NOT hit "./" (match Node) + fail(pkg, '.', '.'); + fail(pkg, '.', 'foobar'); + }); - // TODO? Native throws `ERR_PACKAGE_PATH_NOT_EXPORTED` - // Currently throwing `Missing "%s" export in "$s" package` - fail(pkg, './features/internal/hello', 'features/internal/hello'); - fail(pkg, './features/internal/foo/bar', 'features/internal/foo/bar'); -}); + // https://github.com/lukeed/resolve.exports/issues/16 + it('exports["./features/*"] :: with `null` internals', () => { + let pkg: Package = { + "name": "foobar", + "exports": { + "./features/*": "./src/features/*.js", + "./features/internal/*": null + } + }; -// https://github.com/lukeed/resolve.exports/issues/16 -resolve('exports["./features/*"] :: with `null` internals first', () => { - let pkg: Package = { - "name": "foobar", - "exports": { - "./features/internal/*": null, - "./features/*": "./src/features/*.js", - } - }; + pass(pkg, './src/features/hello.js', 'features/hello'); + pass(pkg, './src/features/hello.js', 'foobar/features/hello'); - pass(pkg, './src/features/hello.js', 'features/hello'); - pass(pkg, './src/features/hello.js', 'foobar/features/hello'); + pass(pkg, './src/features/foo/bar.js', 'features/foo/bar'); + pass(pkg, './src/features/foo/bar.js', 'foobar/features/foo/bar'); - pass(pkg, './src/features/foo/bar.js', 'features/foo/bar'); - pass(pkg, './src/features/foo/bar.js', 'foobar/features/foo/bar'); + // TODO? Native throws `ERR_PACKAGE_PATH_NOT_EXPORTED` + // Currently throwing `Missing "%s" export in "$s" package` + fail(pkg, './features/internal/hello', 'features/internal/hello'); + fail(pkg, './features/internal/foo/bar', 'features/internal/foo/bar'); + }); - // TODO? Native throws `ERR_PACKAGE_PATH_NOT_EXPORTED` - // Currently throwing `Missing "%s" export in "$s" package` - fail(pkg, './features/internal/hello', 'features/internal/hello'); - fail(pkg, './features/internal/foo/bar', 'features/internal/foo/bar'); -}); + // https://github.com/lukeed/resolve.exports/issues/16 + it('exports["./features/*"] :: with `null` internals first', () => { + let pkg: Package = { + "name": "foobar", + "exports": { + "./features/internal/*": null, + "./features/*": "./src/features/*.js", + } + }; -// https://nodejs.org/docs/latest-v18.x/api/packages.html#package-entry-points -resolve('exports["./features/*"] :: with "./features/*.js" key', () => { - let pkg: Package = { - "name": "foobar", - "exports": { - "./features/*": "./features/*.js", - "./features/*.js": "./features/*.js", - } - }; + pass(pkg, './src/features/hello.js', 'features/hello'); + pass(pkg, './src/features/hello.js', 'foobar/features/hello'); - fail(pkg, './features', 'features'); - fail(pkg, './features', 'foobar/features'); + pass(pkg, './src/features/foo/bar.js', 'features/foo/bar'); + pass(pkg, './src/features/foo/bar.js', 'foobar/features/foo/bar'); - fail(pkg, './features/', 'features/'); - fail(pkg, './features/', 'foobar/features/'); + // TODO? Native throws `ERR_PACKAGE_PATH_NOT_EXPORTED` + // Currently throwing `Missing "%s" export in "$s" package` + fail(pkg, './features/internal/hello', 'features/internal/hello'); + fail(pkg, './features/internal/foo/bar', 'features/internal/foo/bar'); + }); - pass(pkg, './features/a.js', 'foobar/features/a'); - pass(pkg, './features/ab.js', 'foobar/features/ab'); - pass(pkg, './features/abc.js', 'foobar/features/abc'); + // https://nodejs.org/docs/latest-v18.x/api/packages.html#package-entry-points + it('exports["./features/*"] :: with "./features/*.js" key', () => { + let pkg: Package = { + "name": "foobar", + "exports": { + "./features/*": "./features/*.js", + "./features/*.js": "./features/*.js", + } + }; - pass(pkg, './features/hello.js', 'foobar/features/hello'); - pass(pkg, './features/hello.js', 'foobar/features/hello.js'); + fail(pkg, './features', 'features'); + fail(pkg, './features', 'foobar/features'); - pass(pkg, './features/foo/bar.js', 'foobar/features/foo/bar'); - pass(pkg, './features/foo/bar.js', 'foobar/features/foo/bar.js'); + fail(pkg, './features/', 'features/'); + fail(pkg, './features/', 'foobar/features/'); - fail(pkg, './package.json', 'package.json'); - fail(pkg, './package.json', 'foobar/package.json'); - fail(pkg, './package.json', './package.json'); -}); + pass(pkg, './features/a.js', 'foobar/features/a'); + pass(pkg, './features/ab.js', 'foobar/features/ab'); + pass(pkg, './features/abc.js', 'foobar/features/abc'); -resolve('exports["./features/*"] :: conditions', () => { - let pkg: Package = { - "name": "foobar", - "exports": { - "./features/*": { - "browser": { - "import": "./browser.import/*.mjs", - "require": "./browser.require/*.js", - }, - "import": "./import/*.mjs", - "require": "./require/*.js", - }, - } - }; + pass(pkg, './features/hello.js', 'foobar/features/hello'); + pass(pkg, './features/hello.js', 'foobar/features/hello.js'); - // import - fail(pkg, './features/', 'features/'); // no file - fail(pkg, './features/', 'foobar/features/'); // no file + pass(pkg, './features/foo/bar.js', 'foobar/features/foo/bar'); + pass(pkg, './features/foo/bar.js', 'foobar/features/foo/bar.js'); - pass(pkg, './import/hello.mjs', './features/hello'); - pass(pkg, './import/hello.mjs', 'foobar/features/hello'); + fail(pkg, './package.json', 'package.json'); + fail(pkg, './package.json', 'foobar/package.json'); + fail(pkg, './package.json', './package.json'); + }); - // require - fail(pkg, './features/', 'features/', { require: true }); // no file - fail(pkg, './features/', 'foobar/features/', { require: true }); // no file + it('exports["./features/*"] :: conditions', () => { + let pkg: Package = { + "name": "foobar", + "exports": { + "./features/*": { + "browser": { + "import": "./browser.import/*.mjs", + "require": "./browser.require/*.js", + }, + "import": "./import/*.mjs", + "require": "./require/*.js", + }, + } + }; - pass(pkg, './require/hello.js', './features/hello', { require: true }); - pass(pkg, './require/hello.js', 'foobar/features/hello', { require: true }); + // import + fail(pkg, './features/', 'features/'); // no file + fail(pkg, './features/', 'foobar/features/'); // no file - // require + browser - fail(pkg, './features/', 'features/', { browser: true, require: true }); // no file - fail(pkg, './features/', 'foobar/features/', { browser: true, require: true }); // no file + pass(pkg, './import/hello.mjs', './features/hello'); + pass(pkg, './import/hello.mjs', 'foobar/features/hello'); - pass(pkg, './browser.require/hello.js', './features/hello', { browser: true, require: true }); - pass(pkg, './browser.require/hello.js', 'foobar/features/hello', { browser: true, require: true }); -}); + // require + fail(pkg, './features/', 'features/', { require: true }); // no file + fail(pkg, './features/', 'foobar/features/', { require: true }); // no file -resolve('should handle mixed path/conditions', () => { - let pkg: Package = { - "name": "foobar", - "exports": { - ".": [ - { - "import": "./$root.import", - }, - "./$root.string" - ], - "./foo": [ - { - "require": "./$foo.require" - }, - "./$foo.string" - ] - } - } + pass(pkg, './require/hello.js', './features/hello', { require: true }); + pass(pkg, './require/hello.js', 'foobar/features/hello', { require: true }); - pass(pkg, ['./$root.import', './$root.string']); - pass(pkg, ['./$root.import', './$root.string'], 'foobar'); + // require + browser + fail(pkg, './features/', 'features/', { browser: true, require: true }); // no file + fail(pkg, './features/', 'foobar/features/', { browser: true, require: true }); // no file - // TODO? if len==1 then single? - pass(pkg, ['./$foo.string'], 'foo'); - pass(pkg, ['./$foo.string'], 'foobar/foo'); - pass(pkg, ['./$foo.string'], './foo'); - - pass(pkg, ['./$foo.require', './$foo.string'], 'foo', { require: true }); - pass(pkg, ['./$foo.require', './$foo.string'], 'foobar/foo', { require: true }); - pass(pkg, ['./$foo.require', './$foo.string'], './foo', { require: true }); -}); + pass(pkg, './browser.require/hello.js', './features/hello', { browser: true, require: true }); + pass(pkg, './browser.require/hello.js', 'foobar/features/hello', { browser: true, require: true }); + }); -resolve('should handle file with leading dot', () => { - let pkg: Package = { - "version": "2.41.0", - "name": "aws-cdk-lib", - "exports": { - ".": "./index.js", - "./package.json": "./package.json", - "./.jsii": "./.jsii", - "./.warnings.jsii.js": "./.warnings.jsii.js", - "./alexa-ask": "./alexa-ask/index.js" + it('should handle mixed path/conditions', () => { + let pkg: Package = { + "name": "foobar", + "exports": { + ".": [ + { + "import": "./$root.import", + }, + "./$root.string" + ], + "./foo": [ + { + "require": "./$foo.require" + }, + "./$foo.string" + ] + } } - }; - - pass(pkg, "./.warnings.jsii.js", ".warnings.jsii.js"); -}); -resolve.run(); + pass(pkg, ['./$root.import', './$root.string']); + pass(pkg, ['./$root.import', './$root.string'], 'foobar'); -// --- + // TODO? if len==1 then single? + pass(pkg, ['./$foo.string'], 'foo'); + pass(pkg, ['./$foo.string'], 'foobar/foo'); + pass(pkg, ['./$foo.string'], './foo'); -const requires = suite('options.requires', { - "name": "r", - "exports": { - "require": "./$require", - "import": "./$import", - } -}); - -requires('should ignore "require" keys by default', pkg => { - pass(pkg, './$import'); -}); + pass(pkg, ['./$foo.require', './$foo.string'], 'foo', { require: true }); + pass(pkg, ['./$foo.require', './$foo.string'], 'foobar/foo', { require: true }); + pass(pkg, ['./$foo.require', './$foo.string'], './foo', { require: true }); + }); -requires('should use "require" key when defined first', pkg => { - pass(pkg, './$require', '.', { require: true }); -}); + it('should handle file with leading dot', () => { + let pkg: Package = { + "version": "2.41.0", + "name": "aws-cdk-lib", + "exports": { + ".": "./index.js", + "./package.json": "./package.json", + "./.jsii": "./.jsii", + "./.warnings.jsii.js": "./.warnings.jsii.js", + "./alexa-ask": "./alexa-ask/index.js" + } + }; -requires('should ignore "import" key when enabled', () => { - let pkg: Package = { - "name": "r", - "exports": { - "import": "./$import", - "require": "./$require", - } - }; - pass(pkg, './$require', '.', { require: true }); - pass(pkg, './$import', '.'); + pass(pkg, "./.warnings.jsii.js", ".warnings.jsii.js"); + }); }); -requires('should match "default" if "require" is after', () => { +describe('options.requires', it => { let pkg: Package = { "name": "r", "exports": { - "default": "./$default", "require": "./$require", + "import": "./$import", } }; - pass(pkg, './$default', '.', { require: true }); -}); -requires.run(); - -// --- + it('should ignore "require" keys by default', () => { + pass(pkg, './$import'); + }); -const browser = suite('options.browser', { - "name": "b", - "exports": { - "browser": "./$browser", - "node": "./$node", - } -}); + it('should use "require" key when defined first', () => { + pass(pkg, './$require', '.', { require: true }); + }); -browser('should ignore "browser" keys by default', pkg => { - pass(pkg, './$node'); -}); + it('should ignore "import" key when enabled', () => { + let pkg: Package = { + "name": "r", + "exports": { + "import": "./$import", + "require": "./$require", + } + }; + pass(pkg, './$require', '.', { require: true }); + pass(pkg, './$import', '.'); + }); -browser('should use "browser" key when defined first', pkg => { - pass(pkg, './$browser', '.', { browser: true }); + it('should match "default" if "require" is after', () => { + let pkg: Package = { + "name": "r", + "exports": { + "default": "./$default", + "require": "./$require", + } + }; + pass(pkg, './$default', '.', { require: true }); + }); }); -browser('should ignore "node" key when enabled', () => { +describe('options.browser', it => { let pkg: Package = { "name": "b", "exports": { - "node": "./$node", - "import": "./$import", "browser": "./$browser", + "node": "./$node", } }; - // import defined before browser - pass(pkg, './$import', '.', { browser: true }); -}); - -browser.run(); - -// --- -const conditions = suite('options.conditions', { - "name": "c", - "exports": { - "production": "./$prod", - "development": "./$dev", - "default": "./$default", - } -}); - -conditions('should ignore unknown conditions by default', pkg => { - pass(pkg, './$default'); -}); - -conditions('should recognize custom field(s) when specified', pkg => { - pass(pkg, './$dev', '.', { - conditions: ['development'] + it('should ignore "browser" keys by default', () => { + pass(pkg, './$node'); }); - pass(pkg, './$prod', '.', { - conditions: ['development', 'production'] + it('should use "browser" key when defined first', () => { + pass(pkg, './$browser', '.', { browser: true }); }); -}); - -conditions('should throw an error if no known conditions', ctx => { - let pkg = { - "name": "hello", - "exports": { - // @ts-ignore - ...ctx.exports - }, - }; - delete pkg.exports.default; + it('should ignore "node" key when enabled', () => { + let pkg: Package = { + "name": "b", + "exports": { + "node": "./$node", + "import": "./$import", + "browser": "./$browser", + } + }; - try { - lib.resolve(pkg); - assert.unreachable(); - } catch (err) { - assert.instance(err, Error); - assert.is((err as Error).message, `No known conditions for "." entry in "hello" package`); - } + // import defined before browser + pass(pkg, './$import', '.', { browser: true }); + }); }); -conditions.run(); - -// --- - -const unsafe = suite('options.unsafe', { - "name": "unsafe", - "exports": { - ".": { +describe('options.conditions', it => { + const pkg: Package = { + "name": "c", + "exports": { "production": "./$prod", "development": "./$dev", "default": "./$default", - }, - "./spec/type": { - "import": "./$import", - "require": "./$require", - "default": "./$default" - }, - "./spec/env": { - "worker": { - "default": "./$worker" - }, - "browser": "./$browser", - "node": "./$node", - "default": "./$default" } - } -}); + }; -unsafe('should ignore unknown conditions by default', pkg => { - pass(pkg, './$default', '.', { - unsafe: true, + it('should ignore unknown conditions by default', () => { + pass(pkg, './$default'); }); -}); -unsafe('should ignore "import" and "require" conditions by default', pkg => { - pass(pkg, './$default', './spec/type', { - unsafe: true, - }); + it('should recognize custom field(s) when specified', () => { + pass(pkg, './$dev', '.', { + conditions: ['development'] + }); - pass(pkg, './$default', './spec/type', { - unsafe: true, - require: true, + pass(pkg, './$prod', '.', { + conditions: ['development', 'production'] + }); }); -}); -unsafe('should ignore "node" and "browser" conditions by default', pkg => { - pass(pkg, './$default', './spec/type', { - unsafe: true, - }); + it('should throw an error if no known conditions', () => { + let ctx: Package = { + "name": "hello", + "exports": { + // @ts-ignore + ...pkg.exports + }, + }; + + // @ts-ignore + delete ctx.exports.default; - pass(pkg, './$default', './spec/type', { - unsafe: true, - browser: true, + try { + lib.resolve(ctx); + assert.unreachable(); + } catch (err) { + assert.instance(err, Error); + assert.is((err as Error).message, `No known conditions for "." entry in "hello" package`); + } }); }); -unsafe('should respect/accept any custom condition(s) when specified', pkg => { - // root, dev only - pass(pkg, './$dev', '.', { - unsafe: true, - conditions: ['development'] - }); +describe('options.unsafe', it => { + let pkg: Package = { + "name": "unsafe", + "exports": { + ".": { + "production": "./$prod", + "development": "./$dev", + "default": "./$default", + }, + "./spec/type": { + "import": "./$import", + "require": "./$require", + "default": "./$default" + }, + "./spec/env": { + "worker": { + "default": "./$worker" + }, + "browser": "./$browser", + "node": "./$node", + "default": "./$default" + } + } + }; - // root, defined order - pass(pkg, './$prod', '.', { - unsafe: true, - conditions: ['development', 'production'] + it('should ignore unknown conditions by default', () => { + pass(pkg, './$default', '.', { + unsafe: true, + }); }); - // import vs require, defined order - pass(pkg, './$require', './spec/type', { - unsafe: true, - conditions: ['require'] - }); + it('should ignore "import" and "require" conditions by default', () => { + pass(pkg, './$default', './spec/type', { + unsafe: true, + }); - // import vs require, defined order - pass(pkg, './$import', './spec/type', { - unsafe: true, - conditions: ['import', 'require'] + pass(pkg, './$default', './spec/type', { + unsafe: true, + require: true, + }); }); - // import vs require, defined order - pass(pkg, './$node', './spec/env', { - unsafe: true, - conditions: ['node'] - }); + it('should ignore "node" and "browser" conditions by default', () => { + pass(pkg, './$default', './spec/type', { + unsafe: true, + }); - // import vs require, defined order - pass(pkg, './$browser', './spec/env', { - unsafe: true, - conditions: ['browser', 'node'] + pass(pkg, './$default', './spec/type', { + unsafe: true, + browser: true, + }); }); - // import vs require, defined order - pass(pkg, './$worker', './spec/env', { - unsafe: true, - conditions: ['browser', 'node', 'worker'] + it('should respect/accept any custom condition(s) when specified', () => { + // root, dev only + pass(pkg, './$dev', '.', { + unsafe: true, + conditions: ['development'] + }); + + // root, defined order + pass(pkg, './$prod', '.', { + unsafe: true, + conditions: ['development', 'production'] + }); + + // import vs require, defined order + pass(pkg, './$require', './spec/type', { + unsafe: true, + conditions: ['require'] + }); + + // import vs require, defined order + pass(pkg, './$import', './spec/type', { + unsafe: true, + conditions: ['import', 'require'] + }); + + // import vs require, defined order + pass(pkg, './$node', './spec/env', { + unsafe: true, + conditions: ['node'] + }); + + // import vs require, defined order + pass(pkg, './$browser', './spec/env', { + unsafe: true, + conditions: ['browser', 'node'] + }); + + // import vs require, defined order + pass(pkg, './$worker', './spec/env', { + unsafe: true, + conditions: ['browser', 'node', 'worker'] + }); }); }); - -unsafe.run(); From f4774c73576068af269ddac2d66c3a0b7a2a0be8 Mon Sep 17 00:00:00 2001 From: Luke Edwards Date: Sat, 14 Jan 2023 23:53:46 -0800 Subject: [PATCH 18/35] fix(alt): add `resolve` export --- src/alt.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/alt.ts b/src/alt.ts index 3036dcc..6c7eedb 100644 --- a/src/alt.ts +++ b/src/alt.ts @@ -1,4 +1,4 @@ -import { walk } from './utils'; +import { toEntry, walk } from './utils'; import type * as t from 'resolve.exports'; export { legacy } from './legacy'; @@ -25,3 +25,12 @@ export function exports(pkg: t.Package, input?: string, options?: t.Options): Ou export function imports(pkg: t.Package, input: string, options?: t.Options): Output | void { if (pkg.imports) return walk(pkg.name, pkg.imports, input, options); } + +export function resolve(pkg: t.Package, input?: string, options?: t.Options): Output | void { + // let entry = input && input !== '.' + // ? toEntry(pkg.name, input) + // : '.'; + let entry = toEntry(pkg.name, input || '.'); + if (entry[0] === '#') return imports(pkg, entry, options); + if (entry[0] === '.') return exports(pkg, entry, options); +} From 8b2b9e79b9cd4c223536d783820666849ecc0791 Mon Sep 17 00:00:00 2001 From: Luke Edwards Date: Sat, 14 Jan 2023 23:56:10 -0800 Subject: [PATCH 19/35] chore: move alt -> index --- src/alt.ts | 36 ---------------- src/index.ts | 117 +++++++++++---------------------------------------- 2 files changed, 25 insertions(+), 128 deletions(-) delete mode 100644 src/alt.ts diff --git a/src/alt.ts b/src/alt.ts deleted file mode 100644 index 6c7eedb..0000000 --- a/src/alt.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { toEntry, walk } from './utils'; -import type * as t from 'resolve.exports'; - -export { legacy } from './legacy'; - -type Output = string[] | string; - -export function exports(pkg: t.Package, input?: string, options?: t.Options): Output | void { - let map = pkg.exports, - k: string; - - if (map) { - if (typeof map === 'string') { - map = { '.': map }; - } else for (k in map) { - // convert {conditions} to "."={condtions} - if (k[0] !== '.') map = { '.': map }; - break; - } - - return walk(pkg.name, map, input||'.', options); - } -} - -export function imports(pkg: t.Package, input: string, options?: t.Options): Output | void { - if (pkg.imports) return walk(pkg.name, pkg.imports, input, options); -} - -export function resolve(pkg: t.Package, input?: string, options?: t.Options): Output | void { - // let entry = input && input !== '.' - // ? toEntry(pkg.name, input) - // : '.'; - let entry = toEntry(pkg.name, input || '.'); - if (entry[0] === '#') return imports(pkg, entry, options); - if (entry[0] === '.') return exports(pkg, entry, options); -} diff --git a/src/index.ts b/src/index.ts index 3d75b4b..6c7eedb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,103 +1,36 @@ -import * as $ from './utils'; - +import { toEntry, walk } from './utils'; import type * as t from 'resolve.exports'; export { legacy } from './legacy'; -/** - * @param name The package name - * @param entry The target entry, eg "." - * @param condition Unmatched condition? - */ -function bail(name: string, entry: string, condition?: number): never { - throw new Error( - condition - ? `No known conditions for "${entry}" entry in "${name}" package` - : `Missing "${entry}" export in "${name}" package` - ); -} - -export function resolve(pkg: t.Package, input?: string, options?: t.Options): string[] | string | void { - let entry = input && input !== '.' - ? $.toEntry(pkg.name, input) - : '.'; - - if (entry[0] === '#') return imports(pkg, entry as t.Imports.Entry, options); - if (entry[0] === '.') return exports(pkg, entry as t.Exports.Entry, options); -} - -export function imports(pkg: t.Package, key: t.Imports.Entry, options?: t.Options): t.Imports.Output | void { - // -} - -export function exports(pkg: t.Package, target: t.Exports.Entry, options?: t.Options): t.Exports.Output | void { - let - name = pkg.name, - entry = $.toEntry(name, target), - isROOT = entry === '.', - map = pkg.exports; - - if (!map) return; - if (typeof map === 'string') { - return isROOT ? map : bail(name, entry); - } - - let - allows = $.conditions(options||{}), - key: t.Exports.Entry | string, - longest: t.Exports.Entry | undefined, - value: string | undefined | null, - match: RegExpExecArray | null, - tmp: any, // mixed - isONE = false; +type Output = string[] | string; - for (key in map) { - isONE = key[0] !== '.'; - break; - } - - if (isONE) { - return isROOT - ? $.loop(map, allows) as t.Exports.Output || bail(name, entry, 1) - : bail(name, entry); - } - - if (tmp = map[entry]) { - return $.loop(tmp, allows) as t.Exports.Output || bail(name, entry, 1); - } - - if (!isROOT) { - for (key in map) { - if (longest && key.length < longest.length) { - // do not allow "./" to match if already matched "./foo*" key - } else if (key[key.length - 1] === '/' && entry.startsWith(key)) { - value = entry.substring(key.length); - longest = key as t.Exports.Entry; - } else { - tmp = key.indexOf('*', 2); - if (!!~tmp) { - match = RegExp( - '^\.\/' + key.substring(2, tmp) + '(.*)' + key.substring(1+tmp) - ).exec(entry); +export function exports(pkg: t.Package, input?: string, options?: t.Options): Output | void { + let map = pkg.exports, + k: string; - if (match && match[1]) { - value = match[1]; - longest = key as t.Exports.Entry; - } - } - } + if (map) { + if (typeof map === 'string') { + map = { '.': map }; + } else for (k in map) { + // convert {conditions} to "."={condtions} + if (k[0] !== '.') map = { '.': map }; + break; } - if (longest && value) { - // must have a value - tmp = $.loop(map[longest], allows); - if (!tmp) return bail(name, entry); - - return tmp.includes('*') - ? tmp.replace(/[*]/g, value) - : tmp + value; - } + return walk(pkg.name, map, input||'.', options); } +} + +export function imports(pkg: t.Package, input: string, options?: t.Options): Output | void { + if (pkg.imports) return walk(pkg.name, pkg.imports, input, options); +} - return bail(name, entry); +export function resolve(pkg: t.Package, input?: string, options?: t.Options): Output | void { + // let entry = input && input !== '.' + // ? toEntry(pkg.name, input) + // : '.'; + let entry = toEntry(pkg.name, input || '.'); + if (entry[0] === '#') return imports(pkg, entry, options); + if (entry[0] === '.') return exports(pkg, entry, options); } From 38980a8a5c97596a24d91c81c63eda47badb83a7 Mon Sep 17 00:00:00 2001 From: Luke Edwards Date: Sun, 15 Jan 2023 05:52:17 -0800 Subject: [PATCH 20/35] chore: add `imports` tests --- src/utils.ts | 2 +- test/resolve.ts | 448 +++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 425 insertions(+), 25 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index e2ff1f2..7140e2e 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -66,7 +66,7 @@ export function walk(name: string, mapping: Mapping, input: string, options?: t. } let v = loop(m, c); - // if (!v) throws('unknown condition'); + // unknown condition(s) if (!v) throws(name, entry, 1); return (exact || !replace) ? v : injects(v, replace); diff --git a/test/resolve.ts b/test/resolve.ts index f94599b..b668768 100644 --- a/test/resolve.ts +++ b/test/resolve.ts @@ -2,15 +2,19 @@ import * as uvu from 'uvu'; import * as assert from 'uvu/assert'; import * as lib from '../src'; -import type { Package, Exports, Options } from 'resolve.exports'; +import type * as t from 'resolve.exports'; -function pass(pkg: Package, expects: Exports.Entry|Exports.Entry[], entry?: string, options?: Options) { +type Package = t.Package; +type Entry = t.Exports.Entry | t.Imports.Entry; +type Options = t.Options; + +function pass(pkg: Package, expects: string|string[], entry?: string, options?: Options) { let out = lib.resolve(pkg, entry, options); if (Array.isArray(expects)) assert.equal(out, expects); else assert.is(out, expects); } -function fail(pkg: Package, target: Exports.Entry, entry?: string, options?: Options) { +function fail(pkg: Package, target: Entry, entry?: string, options?: Options) { try { lib.resolve(pkg, entry, options); assert.unreachable(); @@ -31,7 +35,7 @@ function describe( // --- -describe('lib.resolve', it => { +describe('$.resolve', it => { it('should be a function', () => { assert.type(lib.resolve, 'function'); }); @@ -147,15 +151,15 @@ describe('lib.resolve', it => { "name": "foobar", "exports": { "node": { - "import": "././$node.import", - "require": "././$node.require" + "import": "./$node.import", + "require": "./$node.require" }, "default": "./$default", } }; - pass(pkg, '././$node.import'); - pass(pkg, '././$node.import', 'foobar'); + pass(pkg, './$node.import'); + pass(pkg, './$node.import', 'foobar'); // browser => no "node" key pass(pkg, './$default', '.', { browser: true }); @@ -172,22 +176,22 @@ describe('lib.resolve', it => { "exports": { "./lite": { "node": { - "import": "././$node.import", - "require": "././$node.require" + "import": "./$node.import", + "require": "./$node.require" }, "browser": { - "import": "././$browser.import", - "require": "././$browser.require" + "import": "./$browser.import", + "require": "./$browser.require" }, } } }; - pass(pkg, '././$node.import', 'foobar/lite'); - pass(pkg, '././$node.require', 'foobar/lite', { require: true }); + pass(pkg, './$node.import', 'foobar/lite'); + pass(pkg, './$node.require', 'foobar/lite', { require: true }); - pass(pkg, '././$browser.import', 'foobar/lite', { browser: true }); - pass(pkg, '././$browser.require', 'foobar/lite', { browser: true, require: true }); + pass(pkg, './$browser.import', 'foobar/lite', { browser: true }); + pass(pkg, './$browser.require', 'foobar/lite', { browser: true, require: true }); }); it('nested conditions :: subpath :: inverse', () => { @@ -196,22 +200,22 @@ describe('lib.resolve', it => { "exports": { "./lite": { "import": { - "browser": "././$browser.import", - "node": "././$node.import", + "browser": "./$browser.import", + "node": "./$node.import", }, "require": { - "browser": "././$browser.require", - "node": "././$node.require", + "browser": "./$browser.require", + "node": "./$node.require", } } } }; - pass(pkg, '././$node.import', 'foobar/lite'); - pass(pkg, '././$node.require', 'foobar/lite', { require: true }); + pass(pkg, './$node.import', 'foobar/lite'); + pass(pkg, './$node.require', 'foobar/lite', { require: true }); - pass(pkg, '././$browser.import', 'foobar/lite', { browser: true }); - pass(pkg, '././$browser.require', 'foobar/lite', { browser: true, require: true }); + pass(pkg, './$browser.import', 'foobar/lite', { browser: true }); + pass(pkg, './$browser.require', 'foobar/lite', { browser: true, require: true }); }); // https://nodejs.org/api/packages.html#packages_subpath_folder_mappings @@ -905,3 +909,399 @@ describe('options.unsafe', it => { }); }); }); + +describe('$.imports', it => { + it('should be a function', () => { + assert.type(lib.imports, 'function'); + }); + + it('imports["#foo"] = string', () => { + let pkg: Package = { + "name": "foobar", + "imports": { + "#foo": "./$import", + "#bar": "module-a", + } + }; + + pass(pkg, './$import', '#foo'); + pass(pkg, './$import', 'foobar/#foo'); + + pass(pkg, 'module-a', '#bar'); + pass(pkg, 'module-a', 'foobar/#bar'); + + fail(pkg, '#other', 'foobar/#other'); + }); + + it('imports["#foo"] = object', () => { + let pkg: Package = { + "name": "foobar", + "imports": { + "#foo": { + "import": "./$import", + "require": "./$require", + } + } + }; + + pass(pkg, './$import', '#foo'); + pass(pkg, './$import', 'foobar/#foo'); + + fail(pkg, '#other', 'foobar/#other'); + }); + + it('nested conditions :: subpath', () => { + let pkg: Package = { + "name": "foobar", + "imports": { + "#lite": { + "node": { + "import": "./$node.import", + "require": "./$node.require" + }, + "browser": { + "import": "./$browser.import", + "require": "./$browser.require" + }, + } + } + }; + + pass(pkg, './$node.import', 'foobar/#lite'); + pass(pkg, './$node.require', 'foobar/#lite', { require: true }); + + pass(pkg, './$browser.import', 'foobar/#lite', { browser: true }); + pass(pkg, './$browser.require', 'foobar/#lite', { browser: true, require: true }); + }); + + it('nested conditions :: subpath :: inverse', () => { + let pkg: Package = { + "name": "foobar", + "imports": { + "#lite": { + "import": { + "browser": "./$browser.import", + "node": "./$node.import", + }, + "require": { + "browser": "./$browser.require", + "node": "./$node.require", + } + } + } + }; + + pass(pkg, './$node.import', 'foobar/#lite'); + pass(pkg, './$node.require', 'foobar/#lite', { require: true }); + + pass(pkg, './$browser.import', 'foobar/#lite', { browser: true }); + pass(pkg, './$browser.require', 'foobar/#lite', { browser: true, require: true }); + }); + + // https://nodejs.org/api/packages.html#packages_subpath_folder_mappings + it('imports["#key/*"]', () => { + let pkg: Package = { + "name": "foobar", + "imports": { + "#key/*": "./cheese/*.mjs" + } + }; + + pass(pkg, './cheese/hello.mjs', 'foobar/#key/hello'); + pass(pkg, './cheese/hello/world.mjs', '#key/hello/world'); + + // evaluate as defined, not wrong + pass(pkg, './cheese/hello.js.mjs', '#key/hello.js'); + pass(pkg, './cheese/hello.js.mjs', 'foobar/#key/hello.js'); + pass(pkg, './cheese/hello/world.js.mjs', '#key/hello/world.js'); + }); + + it('imports["#key/dir*"]', () => { + let pkg: Package = { + "name": "foobar", + "imports": { + "#key/dir*": "./cheese/*.mjs" + } + }; + + pass(pkg, './cheese/test.mjs', '#key/dirtest'); + pass(pkg, './cheese/test.mjs', 'foobar/#key/dirtest'); + + pass(pkg, './cheese/test/wheel.mjs', '#key/dirtest/wheel'); + pass(pkg, './cheese/test/wheel.mjs', 'foobar/#key/dirtest/wheel'); + }); + + // https://github.com/lukeed/resolve.exports/issues/9 + it('imports["#key/dir*"] :: repeat "*" value', () => { + let pkg: Package = { + "name": "foobar", + "imports": { + "#key/dir*": "./*sub/dir*/file.js" + } + }; + + pass(pkg, './testsub/dirtest/file.js', '#key/dirtest'); + pass(pkg, './testsub/dirtest/file.js', 'foobar/#key/dirtest'); + + pass(pkg, './test/innersub/dirtest/inner/file.js', '#key/dirtest/inner'); + pass(pkg, './test/innersub/dirtest/inner/file.js', 'foobar/#key/dirtest/inner'); + }); + + /** + * @deprecated Documentation-only deprecation in Node 14.13 + * @deprecated Runtime deprecation in Node 16.0 + * @removed Removed in Node 18.0 + * @see https://nodejs.org/docs/latest-v16.x/api/packages.html#subpath-folder-mappings + */ + it('imports["#features/"]', () => { + let pkg: Package = { + "name": "foobar", + "imports": { + "#features/": "./features/" + } + }; + + pass(pkg, './features/', '#features/'); + pass(pkg, './features/', 'foobar/#features/'); + + pass(pkg, './features/hello.js', 'foobar/#features/hello.js'); + + fail(pkg, '#features', '#features'); + fail(pkg, '#features', 'foobar/#features'); + }); + + it('imports["#features/"] :: conditions', () => { + let pkg: Package = { + "name": "foobar", + "imports": { + "#features/": { + "browser": { + "import": "./browser.import/", + "require": "./browser.require/", + }, + "import": "./import/", + "require": "./require/", + }, + } + }; + + // import + pass(pkg, './import/', '#features/'); + pass(pkg, './import/', 'foobar/#features/'); + + pass(pkg, './import/hello.js', '#features/hello.js'); + pass(pkg, './import/hello.js', 'foobar/#features/hello.js'); + + // require + pass(pkg, './require/', '#features/', { require: true }); + pass(pkg, './require/', 'foobar/#features/', { require: true }); + + pass(pkg, './require/hello.js', '#features/hello.js', { require: true }); + pass(pkg, './require/hello.js', 'foobar/#features/hello.js', { require: true }); + + // require + browser + pass(pkg, './browser.require/', '#features/', { browser: true, require: true }); + pass(pkg, './browser.require/', 'foobar/#features/', { browser: true, require: true }); + + pass(pkg, './browser.require/hello.js', '#features/hello.js', { browser: true, require: true }); + pass(pkg, './browser.require/hello.js', 'foobar/#features/hello.js', { browser: true, require: true }); + }); + + // https://nodejs.org/api/packages.html#packages_subpath_folder_mappings + it('imports["#features/*"]', () => { + let pkg: Package = { + "name": "foobar", + "imports": { + "#features/*": "./features/*.js", + } + }; + + fail(pkg, '#features', '#features'); + fail(pkg, '#features', 'foobar/#features'); + + fail(pkg, '#features/', '#features/'); + fail(pkg, '#features/', 'foobar/#features/'); + + pass(pkg, './features/a.js', 'foobar/#features/a'); + pass(pkg, './features/ab.js', 'foobar/#features/ab'); + pass(pkg, './features/abc.js', 'foobar/#features/abc'); + + pass(pkg, './features/hello.js', 'foobar/#features/hello'); + pass(pkg, './features/foo/bar.js', 'foobar/#features/foo/bar'); + + // Valid: Pattern trailers allow any exact substrings to be matched + pass(pkg, './features/hello.js.js', 'foobar/#features/hello.js'); + pass(pkg, './features/foo/bar.js.js', 'foobar/#features/foo/bar.js'); + }); + + // https://nodejs.org/api/packages.html#packages_subpath_folder_mappings + it('exports["#fooba*"] :: with "#foo*" key', () => { + let pkg: Package = { + "name": "foobar", + "imports": { + "#fooba*": "./features/*.js", + "#foo*": "./" + } + }; + + pass(pkg, './features/r.js', '#foobar'); + pass(pkg, './features/r.js', 'foobar/#foobar'); + + pass(pkg, './features/r/hello.js', 'foobar/#foobar/hello'); + pass(pkg, './features/r/foo/bar.js', 'foobar/#foobar/foo/bar'); + + // Valid: Pattern trailers allow any exact substrings to be matched + pass(pkg, './features/r/hello.js.js', 'foobar/#foobar/hello.js'); + pass(pkg, './features/r/foo/bar.js.js', 'foobar/#foobar/foo/bar.js'); + }); + + // https://github.com/lukeed/resolve.exports/issues/7 + it('imports["#fooba*"] :: with "#foo*" key first', () => { + let pkg: Package = { + "name": "foobar", + "imports": { + "#foo*": "./", + "#fooba*": "./features/*.js" + } + }; + + pass(pkg, './features/r.js', '#foobar'); + pass(pkg, './features/r.js', 'foobar/#foobar'); + + pass(pkg, './features/r/hello.js', 'foobar/#foobar/hello'); + pass(pkg, './features/r/foo/bar.js', 'foobar/#foobar/foo/bar'); + + // Valid: Pattern trailers allow any exact substrings to be matched + pass(pkg, './features/r/hello.js.js', 'foobar/#foobar/hello.js'); + pass(pkg, './features/r/foo/bar.js.js', 'foobar/#foobar/foo/bar.js'); + }); + + // https://github.com/lukeed/resolve.exports/issues/16 + it('imports["#features/*"] :: with `null` internals', () => { + let pkg: Package = { + "name": "foobar", + "imports": { + "#features/*": "./src/features/*.js", + "#features/internal/*": null + } + }; + + pass(pkg, './src/features/hello.js', '#features/hello'); + pass(pkg, './src/features/hello.js', 'foobar/#features/hello'); + + pass(pkg, './src/features/foo/bar.js', '#features/foo/bar'); + pass(pkg, './src/features/foo/bar.js', 'foobar/#features/foo/bar'); + + // TODO? Native throws `ERR_PACKAGE_PATH_NOT_EXPORTED` + // Currently throwing `Missing "%s" export in "$s" package` + fail(pkg, '#features/internal/hello', '#features/internal/hello'); + fail(pkg, '#features/internal/foo/bar', '#features/internal/foo/bar'); + }); + + // https://github.com/lukeed/resolve.exports/issues/16 + it('imports["#features/*"] :: with `null` internals first', () => { + let pkg: Package = { + "name": "foobar", + "imports": { + "#features/internal/*": null, + "#features/*": "./src/features/*.js", + } + }; + + pass(pkg, './src/features/hello.js', '#features/hello'); + pass(pkg, './src/features/hello.js', 'foobar/#features/hello'); + + pass(pkg, './src/features/foo/bar.js', '#features/foo/bar'); + pass(pkg, './src/features/foo/bar.js', 'foobar/#features/foo/bar'); + + // TODO? Native throws `ERR_PACKAGE_PATH_NOT_EXPORTED` + // Currently throwing `Missing "%s" export in "$s" package` + fail(pkg, '#features/internal/hello', '#features/internal/hello'); + fail(pkg, '#features/internal/foo/bar', '#features/internal/foo/bar'); + }); + + // https://nodejs.org/docs/latest-v18.x/api/packages.html#package-entry-points + it('imports["#features/*"] :: with "#features/*.js" key', () => { + let pkg: Package = { + "name": "foobar", + "imports": { + "#features/*": "./features/*.js", + "#features/*.js": "./features/*.js", + } + }; + + fail(pkg, '#features', '#features'); + fail(pkg, '#features', 'foobar/#features'); + + fail(pkg, '#features/', '#features/'); + fail(pkg, '#features/', 'foobar/#features/'); + + pass(pkg, './features/a.js', 'foobar/#features/a'); + pass(pkg, './features/ab.js', 'foobar/#features/ab'); + pass(pkg, './features/abc.js', 'foobar/#features/abc'); + + pass(pkg, './features/hello.js', 'foobar/#features/hello'); + pass(pkg, './features/hello.js', 'foobar/#features/hello.js'); + + pass(pkg, './features/foo/bar.js', 'foobar/#features/foo/bar'); + pass(pkg, './features/foo/bar.js', 'foobar/#features/foo/bar.js'); + }); + + it('imports["#features/*"] :: conditions', () => { + let pkg: Package = { + "name": "foobar", + "imports": { + "#features/*": { + "browser": { + "import": "./browser.import/*.mjs", + "require": "./browser.require/*.js", + }, + "import": "./import/*.mjs", + "require": "./require/*.js", + }, + } + }; + + // import + fail(pkg, '#features/', '#features/'); // no file + fail(pkg, '#features/', 'foobar/#features/'); // no file + + pass(pkg, './import/hello.mjs', '#features/hello'); + pass(pkg, './import/hello.mjs', 'foobar/#features/hello'); + + // require + fail(pkg, '#features/', '#features/', { require: true }); // no file + fail(pkg, '#features/', 'foobar/#features/', { require: true }); // no file + + pass(pkg, './require/hello.js', '#features/hello', { require: true }); + pass(pkg, './require/hello.js', 'foobar/#features/hello', { require: true }); + + // require + browser + fail(pkg, '#features/', '#features/', { browser: true, require: true }); // no file + fail(pkg, '#features/', 'foobar/#features/', { browser: true, require: true }); // no file + + pass(pkg, './browser.require/hello.js', '#features/hello', { browser: true, require: true }); + pass(pkg, './browser.require/hello.js', 'foobar/#features/hello', { browser: true, require: true }); + }); + + it('should handle mixed path/conditions', () => { + let pkg: Package = { + "name": "foobar", + "imports": { + "#foo": [ + { + "require": "./$foo.require" + }, + "./$foo.string" + ] + } + }; + + // TODO? if len==1 then single? + pass(pkg, ['./$foo.string'], '#foo'); + pass(pkg, ['./$foo.string'], 'foobar/#foo'); + + pass(pkg, ['./$foo.require', './$foo.string'], '#foo', { require: true }); + pass(pkg, ['./$foo.require', './$foo.string'], 'foobar/#foo', { require: true }); + }); +}) From 4c3af1f2df977586c67a23a82912fbff42caa00a Mon Sep 17 00:00:00 2001 From: Luke Edwards Date: Sun, 15 Jan 2023 06:07:11 -0800 Subject: [PATCH 21/35] chore: improve test coverage --- src/utils.ts | 45 -- test/resolve.ts | 1636 +++++++++++++++++++++++++---------------------- 2 files changed, 854 insertions(+), 827 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index 7140e2e..bf37242 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -138,48 +138,3 @@ export function loop(m: Value, keys: Set, result?: Set): st } } } - -// TODO: match exact key too -> [string,] -export function longest(map: Record|null, entry: Entry): void | [Entry, string] { - let key: string; - let match: RegExpExecArray|null; - let longest: Entry|undefined; - let value: string|undefined; - let tmp: string|number; - - for (key in map) { - if (longest && key.length < longest.length) { - // do not allow "./" to match if already matched "./foo*" key - } else if (key[key.length - 1] === '/' && entry.startsWith(key)) { - value = entry.substring(key.length); - longest = key as t.Exports.Entry; - } else { - tmp = key.indexOf('*', 2); - if (!!~tmp) { - match = RegExp( - '^\.\/' + key.substring(2, tmp) + '(.*)' + key.substring(1+tmp) - ).exec(entry); - - if (match && match[1]) { - value = match[1]; - longest = key as t.Exports.Entry; - } - } - } - } - - // must have a value - if (longest && value) { - return [longest, value]; - } - - // if (longest && value) { - // // must have a value - // tmp = loop(map[longest], allows); - // if (!tmp) return bail(name, entry); - - // return tmp.includes('*') - // ? tmp.replace(/[*]/g, value) - // : tmp + value; - // } -} diff --git a/test/resolve.ts b/test/resolve.ts index b668768..07b3221 100644 --- a/test/resolve.ts +++ b/test/resolve.ts @@ -40,141 +40,109 @@ describe('$.resolve', it => { assert.type(lib.resolve, 'function'); }); - it('exports=string', () => { - let pkg: Package = { - "name": "foobar", - "exports": "./$string", - }; - - pass(pkg, './$string'); - pass(pkg, './$string', '.'); - pass(pkg, './$string', 'foobar'); - - fail(pkg, './other', 'other'); - fail(pkg, './other', 'foobar/other'); - fail(pkg, './hello', './hello'); + it('should return nothing if no maps', () => { + let output = lib.resolve({ + "name": "foobar" + }); + assert.is(output, undefined); }); - it('exports = { self }', () => { + it('should default to `$.exports` handler', () => { let pkg: Package = { "name": "foobar", - "exports": { - "import": "./$import", - "require": "./$require", - } + "exports": "./hello.mjs" }; - pass(pkg, './$import'); - pass(pkg, './$import', '.'); - pass(pkg, './$import', 'foobar'); + let output = lib.resolve(pkg); + assert.is(output, './hello.mjs'); - fail(pkg, './other', 'other'); - fail(pkg, './other', 'foobar/other'); - fail(pkg, './hello', './hello'); + try { + lib.resolve(pkg, './other'); + assert.unreachable(); + } catch (err) { + assert.instance(err, Error); + assert.is((err as Error).message, `Missing "./other" export in "foobar" package`); + } }); - it('exports["."] = string', () => { + it('should run `$.imports` if given #ident', () => { let pkg: Package = { "name": "foobar", - "exports": { - ".": "./$self", + "imports": { + "#foo": "./foo.mjs" } }; - pass(pkg, './$self'); - pass(pkg, './$self', '.'); - pass(pkg, './$self', 'foobar'); + let output = lib.resolve(pkg, '#foo'); + assert.is(output, './foo.mjs'); - fail(pkg, './other', 'other'); - fail(pkg, './other', 'foobar/other'); - fail(pkg, './hello', './hello'); + output = lib.resolve(pkg, 'foobar/#foo'); + assert.is(output, './foo.mjs'); + + try { + lib.resolve(pkg, '#bar'); + assert.unreachable(); + } catch (err) { + assert.instance(err, Error); + assert.is((err as Error).message, `Missing "#bar" export in "foobar" package`); + } }); +}); - it('exports["."] = object', () => { +describe('$.imports', it => { + it('should be a function', () => { + assert.type(lib.imports, 'function'); + }); + + it('should return nothing if no "imports" map', () => { let pkg: Package = { - "name": "foobar", - "exports": { - ".": { - "import": "./$import", - "require": "./$require", - } - } + "name": "foobar" }; - pass(pkg, './$import'); - pass(pkg, './$import', '.'); - pass(pkg, './$import', 'foobar'); - - fail(pkg, './other', 'other'); - fail(pkg, './other', 'foobar/other'); - fail(pkg, './hello', './hello'); + let output = lib.imports(pkg, '#any'); + assert.is(output, undefined); }); - it('exports["./foo"] = string', () => { + it('imports["#foo"] = string', () => { let pkg: Package = { "name": "foobar", - "exports": { - "./foo": "./$import", + "imports": { + "#foo": "./$import", + "#bar": "module-a", } }; - pass(pkg, './$import', './foo'); - pass(pkg, './$import', 'foobar/foo'); + pass(pkg, './$import', '#foo'); + pass(pkg, './$import', 'foobar/#foo'); - fail(pkg, '.'); - fail(pkg, '.', 'foobar'); - fail(pkg, './other', 'foobar/other'); + pass(pkg, 'module-a', '#bar'); + pass(pkg, 'module-a', 'foobar/#bar'); + + fail(pkg, '#other', 'foobar/#other'); }); - it('exports["./foo"] = object', () => { + it('imports["#foo"] = object', () => { let pkg: Package = { "name": "foobar", - "exports": { - "./foo": { + "imports": { + "#foo": { "import": "./$import", "require": "./$require", } } }; - pass(pkg, './$import', './foo'); - pass(pkg, './$import', 'foobar/foo'); - - fail(pkg, '.'); - fail(pkg, '.', 'foobar'); - fail(pkg, './other', 'foobar/other'); - }); - - // https://nodejs.org/api/packages.html#packages_nested_conditions - it('nested conditions', () => { - let pkg: Package = { - "name": "foobar", - "exports": { - "node": { - "import": "./$node.import", - "require": "./$node.require" - }, - "default": "./$default", - } - }; - - pass(pkg, './$node.import'); - pass(pkg, './$node.import', 'foobar'); - - // browser => no "node" key - pass(pkg, './$default', '.', { browser: true }); - pass(pkg, './$default', 'foobar', { browser: true }); + pass(pkg, './$import', '#foo'); + pass(pkg, './$import', 'foobar/#foo'); - fail(pkg, './hello', './hello'); - fail(pkg, './other', 'foobar/other'); - fail(pkg, './other', 'other'); + fail(pkg, '#other', 'foobar/#other'); }); it('nested conditions :: subpath', () => { let pkg: Package = { "name": "foobar", - "exports": { - "./lite": { + "imports": { + "#lite": { "node": { "import": "./$node.import", "require": "./$node.require" @@ -187,18 +155,18 @@ describe('$.resolve', it => { } }; - pass(pkg, './$node.import', 'foobar/lite'); - pass(pkg, './$node.require', 'foobar/lite', { require: true }); + pass(pkg, './$node.import', 'foobar/#lite'); + pass(pkg, './$node.require', 'foobar/#lite', { require: true }); - pass(pkg, './$browser.import', 'foobar/lite', { browser: true }); - pass(pkg, './$browser.require', 'foobar/lite', { browser: true, require: true }); + pass(pkg, './$browser.import', 'foobar/#lite', { browser: true }); + pass(pkg, './$browser.require', 'foobar/#lite', { browser: true, require: true }); }); it('nested conditions :: subpath :: inverse', () => { let pkg: Package = { "name": "foobar", - "exports": { - "./lite": { + "imports": { + "#lite": { "import": { "browser": "./$browser.import", "node": "./$node.import", @@ -211,407 +179,267 @@ describe('$.resolve', it => { } }; - pass(pkg, './$node.import', 'foobar/lite'); - pass(pkg, './$node.require', 'foobar/lite', { require: true }); + pass(pkg, './$node.import', 'foobar/#lite'); + pass(pkg, './$node.require', 'foobar/#lite', { require: true }); - pass(pkg, './$browser.import', 'foobar/lite', { browser: true }); - pass(pkg, './$browser.require', 'foobar/lite', { browser: true, require: true }); + pass(pkg, './$browser.import', 'foobar/#lite', { browser: true }); + pass(pkg, './$browser.require', 'foobar/#lite', { browser: true, require: true }); }); // https://nodejs.org/api/packages.html#packages_subpath_folder_mappings - it('exports["./"]', () => { + it('imports["#key/*"]', () => { let pkg: Package = { "name": "foobar", - "exports": { - ".": { - "require": "./$require", - "import": "./$import" - }, - "./package.json": "./package.json", - "./": "./" + "imports": { + "#key/*": "./cheese/*.mjs" } }; - pass(pkg, './$import'); - pass(pkg, './$import', 'foobar'); - pass(pkg, './$require', 'foobar', { require: true }); - - pass(pkg, './package.json', 'package.json'); - pass(pkg, './package.json', 'foobar/package.json'); - pass(pkg, './package.json', './package.json'); + pass(pkg, './cheese/hello.mjs', 'foobar/#key/hello'); + pass(pkg, './cheese/hello/world.mjs', '#key/hello/world'); - // "loose" / everything exposed - pass(pkg, './hello.js', 'hello.js'); - pass(pkg, './hello.js', 'foobar/hello.js'); - pass(pkg, './hello/world.js', './hello/world.js'); + // evaluate as defined, not wrong + pass(pkg, './cheese/hello.js.mjs', '#key/hello.js'); + pass(pkg, './cheese/hello.js.mjs', 'foobar/#key/hello.js'); + pass(pkg, './cheese/hello/world.js.mjs', '#key/hello/world.js'); }); - it('exports["./"] :: w/o "." key', () => { + it('imports["#key/dir*"]', () => { let pkg: Package = { "name": "foobar", - "exports": { - "./package.json": "./package.json", - "./": "./" + "imports": { + "#key/dir*": "./cheese/*.mjs" } }; - fail(pkg, '.', "."); - fail(pkg, '.', "foobar"); - - pass(pkg, './package.json', 'package.json'); - pass(pkg, './package.json', 'foobar/package.json'); - pass(pkg, './package.json', './package.json'); + pass(pkg, './cheese/test.mjs', '#key/dirtest'); + pass(pkg, './cheese/test.mjs', 'foobar/#key/dirtest'); - // "loose" / everything exposed - pass(pkg, './hello.js', 'hello.js'); - pass(pkg, './hello.js', 'foobar/hello.js'); - pass(pkg, './hello/world.js', './hello/world.js'); + pass(pkg, './cheese/test/wheel.mjs', '#key/dirtest/wheel'); + pass(pkg, './cheese/test/wheel.mjs', 'foobar/#key/dirtest/wheel'); }); - // https://nodejs.org/api/packages.html#packages_subpath_folder_mappings - it('exports["./*"]', () => { + // https://github.com/lukeed/resolve.exports/issues/9 + it('imports["#key/dir*"] :: repeat "*" value', () => { let pkg: Package = { "name": "foobar", - "exports": { - "./*": "./cheese/*.mjs" + "imports": { + "#key/dir*": "./*sub/dir*/file.js" } }; - fail(pkg, '.', "."); - fail(pkg, '.', "foobar"); - - pass(pkg, './cheese/hello.mjs', 'hello'); - pass(pkg, './cheese/hello.mjs', 'foobar/hello'); - pass(pkg, './cheese/hello/world.mjs', './hello/world'); + pass(pkg, './testsub/dirtest/file.js', '#key/dirtest'); + pass(pkg, './testsub/dirtest/file.js', 'foobar/#key/dirtest'); - // evaluate as defined, not wrong - pass(pkg, './cheese/hello.js.mjs', 'hello.js'); - pass(pkg, './cheese/hello.js.mjs', 'foobar/hello.js'); - pass(pkg, './cheese/hello/world.js.mjs', './hello/world.js'); + pass(pkg, './test/innersub/dirtest/inner/file.js', '#key/dirtest/inner'); + pass(pkg, './test/innersub/dirtest/inner/file.js', 'foobar/#key/dirtest/inner'); }); - it('exports["./dir*"]', () => { + /** + * @deprecated Documentation-only deprecation in Node 14.13 + * @deprecated Runtime deprecation in Node 16.0 + * @removed Removed in Node 18.0 + * @see https://nodejs.org/docs/latest-v16.x/api/packages.html#subpath-folder-mappings + */ + it('imports["#features/"]', () => { let pkg: Package = { "name": "foobar", - "exports": { - "./dir*": "./cheese/*.mjs" + "imports": { + "#features/": "./features/" } }; - fail(pkg, '.', "."); - fail(pkg, '.', "foobar"); + pass(pkg, './features/', '#features/'); + pass(pkg, './features/', 'foobar/#features/'); - pass(pkg, './cheese/test.mjs', 'dirtest'); - pass(pkg, './cheese/test.mjs', 'foobar/dirtest'); + pass(pkg, './features/hello.js', 'foobar/#features/hello.js'); - pass(pkg, './cheese/test/wheel.mjs', 'dirtest/wheel'); - pass(pkg, './cheese/test/wheel.mjs', 'foobar/dirtest/wheel'); + fail(pkg, '#features', '#features'); + fail(pkg, '#features', 'foobar/#features'); }); - // https://github.com/lukeed/resolve.exports/issues/9 - it('exports["./dir*"] :: repeat "*" value', () => { + it('imports["#features/"] :: conditions', () => { let pkg: Package = { "name": "foobar", - "exports": { - "./dir*": "./*sub/dir*/file.js" - } - }; - - fail(pkg, '.', "."); - fail(pkg, '.', "foobar"); - - pass(pkg, './testsub/dirtest/file.js', 'dirtest'); - pass(pkg, './testsub/dirtest/file.js', 'foobar/dirtest'); - - pass(pkg, './test/innersub/dirtest/inner/file.js', 'dirtest/inner'); - pass(pkg, './test/innersub/dirtest/inner/file.js', 'foobar/dirtest/inner'); - }); - - it('exports["./dir*"] :: share "name" start', () => { - let pkg: Package = { - "name": "director", - "exports": { - "./dir*": "./*sub/dir*/file.js" - } - }; - - fail(pkg, '.', "."); - fail(pkg, '.', "director"); - - pass(pkg, './testsub/dirtest/file.js', 'dirtest'); - pass(pkg, './testsub/dirtest/file.js', 'director/dirtest'); - - pass(pkg, './test/innersub/dirtest/inner/file.js', 'dirtest/inner'); - pass(pkg, './test/innersub/dirtest/inner/file.js', 'director/dirtest/inner'); - }); - - /** - * @deprecated Documentation-only deprecation in Node 14.13 - * @deprecated Runtime deprecation in Node 16.0 - * @removed Removed in Node 18.0 - * @see https://nodejs.org/docs/latest-v16.x/api/packages.html#subpath-folder-mappings - */ - it('exports["./features/"]', () => { - let pkg: Package = { - "name": "foobar", - "exports": { - "./features/": "./features/" - } - }; - - pass(pkg, './features/', 'features/'); - pass(pkg, './features/', 'foobar/features/'); - - pass(pkg, './features/hello.js', 'foobar/features/hello.js'); - - fail(pkg, './features', 'features'); - fail(pkg, './features', 'foobar/features'); - - fail(pkg, './package.json', 'package.json'); - fail(pkg, './package.json', 'foobar/package.json'); - fail(pkg, './package.json', './package.json'); - }); - - // https://nodejs.org/api/packages.html#packages_subpath_folder_mappings - it('exports["./features/"] :: with "./" key', () => { - let pkg: Package = { - "name": "foobar", - "exports": { - "./features/": "./features/", - "./package.json": "./package.json", - "./": "./" - } - }; - - pass(pkg, './features', 'features'); // via "./" - pass(pkg, './features', 'foobar/features'); // via "./" - - pass(pkg, './features/', 'features/'); // via "./features/" - pass(pkg, './features/', 'foobar/features/'); // via "./features/" - - pass(pkg, './features/hello.js', 'foobar/features/hello.js'); - - pass(pkg, './package.json', 'package.json'); - pass(pkg, './package.json', 'foobar/package.json'); - pass(pkg, './package.json', './package.json'); - - // Does NOT hit "./" (match Node) - fail(pkg, '.', '.'); - fail(pkg, '.', 'foobar'); - }); - - it('exports["./features/"] :: conditions', () => { - let pkg: Package = { - "name": "foobar", - "exports": { - "./features/": { - "browser": { - "import": "./browser.import/", - "require": "./browser.require/", - }, - "import": "./import/", - "require": "./require/", - }, + "imports": { + "#features/": { + "browser": { + "import": "./browser.import/", + "require": "./browser.require/", + }, + "import": "./import/", + "require": "./require/", + }, } }; // import - pass(pkg, './import/', 'features/'); - pass(pkg, './import/', 'foobar/features/'); + pass(pkg, './import/', '#features/'); + pass(pkg, './import/', 'foobar/#features/'); - pass(pkg, './import/hello.js', './features/hello.js'); - pass(pkg, './import/hello.js', 'foobar/features/hello.js'); + pass(pkg, './import/hello.js', '#features/hello.js'); + pass(pkg, './import/hello.js', 'foobar/#features/hello.js'); // require - pass(pkg, './require/', 'features/', { require: true }); - pass(pkg, './require/', 'foobar/features/', { require: true }); + pass(pkg, './require/', '#features/', { require: true }); + pass(pkg, './require/', 'foobar/#features/', { require: true }); - pass(pkg, './require/hello.js', './features/hello.js', { require: true }); - pass(pkg, './require/hello.js', 'foobar/features/hello.js', { require: true }); + pass(pkg, './require/hello.js', '#features/hello.js', { require: true }); + pass(pkg, './require/hello.js', 'foobar/#features/hello.js', { require: true }); // require + browser - pass(pkg, './browser.require/', 'features/', { browser: true, require: true }); - pass(pkg, './browser.require/', 'foobar/features/', { browser: true, require: true }); + pass(pkg, './browser.require/', '#features/', { browser: true, require: true }); + pass(pkg, './browser.require/', 'foobar/#features/', { browser: true, require: true }); - pass(pkg, './browser.require/hello.js', './features/hello.js', { browser: true, require: true }); - pass(pkg, './browser.require/hello.js', 'foobar/features/hello.js', { browser: true, require: true }); + pass(pkg, './browser.require/hello.js', '#features/hello.js', { browser: true, require: true }); + pass(pkg, './browser.require/hello.js', 'foobar/#features/hello.js', { browser: true, require: true }); }); // https://nodejs.org/api/packages.html#packages_subpath_folder_mappings - it('exports["./features/*"]', () => { + it('imports["#features/*"]', () => { let pkg: Package = { "name": "foobar", - "exports": { - "./features/*": "./features/*.js", + "imports": { + "#features/*": "./features/*.js", } }; - fail(pkg, './features', 'features'); - fail(pkg, './features', 'foobar/features'); + fail(pkg, '#features', '#features'); + fail(pkg, '#features', 'foobar/#features'); - fail(pkg, './features/', 'features/'); - fail(pkg, './features/', 'foobar/features/'); + fail(pkg, '#features/', '#features/'); + fail(pkg, '#features/', 'foobar/#features/'); - pass(pkg, './features/a.js', 'foobar/features/a'); - pass(pkg, './features/ab.js', 'foobar/features/ab'); - pass(pkg, './features/abc.js', 'foobar/features/abc'); + pass(pkg, './features/a.js', 'foobar/#features/a'); + pass(pkg, './features/ab.js', 'foobar/#features/ab'); + pass(pkg, './features/abc.js', 'foobar/#features/abc'); - pass(pkg, './features/hello.js', 'foobar/features/hello'); - pass(pkg, './features/foo/bar.js', 'foobar/features/foo/bar'); + pass(pkg, './features/hello.js', 'foobar/#features/hello'); + pass(pkg, './features/foo/bar.js', 'foobar/#features/foo/bar'); // Valid: Pattern trailers allow any exact substrings to be matched - pass(pkg, './features/hello.js.js', 'foobar/features/hello.js'); - pass(pkg, './features/foo/bar.js.js', 'foobar/features/foo/bar.js'); - - fail(pkg, './package.json', 'package.json'); - fail(pkg, './package.json', 'foobar/package.json'); - fail(pkg, './package.json', './package.json'); + pass(pkg, './features/hello.js.js', 'foobar/#features/hello.js'); + pass(pkg, './features/foo/bar.js.js', 'foobar/#features/foo/bar.js'); }); // https://nodejs.org/api/packages.html#packages_subpath_folder_mappings - it('exports["./features/*"] :: with "./" key', () => { + it('exports["#fooba*"] :: with "#foo*" key', () => { let pkg: Package = { "name": "foobar", - "exports": { - "./features/*": "./features/*.js", - "./": "./" + "imports": { + "#fooba*": "./features/*.js", + "#foo*": "./" } }; - pass(pkg, './features', 'features'); // via "./" - pass(pkg, './features', 'foobar/features'); // via "./" - - pass(pkg, './features/', 'features/'); // via "./" - pass(pkg, './features/', 'foobar/features/'); // via "./" + pass(pkg, './features/r.js', '#foobar'); + pass(pkg, './features/r.js', 'foobar/#foobar'); - pass(pkg, './features/hello.js', 'foobar/features/hello'); - pass(pkg, './features/foo/bar.js', 'foobar/features/foo/bar'); + pass(pkg, './features/r/hello.js', 'foobar/#foobar/hello'); + pass(pkg, './features/r/foo/bar.js', 'foobar/#foobar/foo/bar'); // Valid: Pattern trailers allow any exact substrings to be matched - pass(pkg, './features/hello.js.js', 'foobar/features/hello.js'); - pass(pkg, './features/foo/bar.js.js', 'foobar/features/foo/bar.js'); - - pass(pkg, './package.json', 'package.json'); - pass(pkg, './package.json', 'foobar/package.json'); - pass(pkg, './package.json', './package.json'); - - // Does NOT hit "./" (match Node) - fail(pkg, '.', '.'); - fail(pkg, '.', 'foobar'); + pass(pkg, './features/r/hello.js.js', 'foobar/#foobar/hello.js'); + pass(pkg, './features/r/foo/bar.js.js', 'foobar/#foobar/foo/bar.js'); }); // https://github.com/lukeed/resolve.exports/issues/7 - it('exports["./features/*"] :: with "./" key first', () => { + it('imports["#fooba*"] :: with "#foo*" key first', () => { let pkg: Package = { "name": "foobar", - "exports": { - "./": "./", - "./features/*": "./features/*.js" + "imports": { + "#foo*": "./", + "#fooba*": "./features/*.js" } }; - pass(pkg, './features', 'features'); // via "./" - pass(pkg, './features', 'foobar/features'); // via "./" - - pass(pkg, './features/', 'features/'); // via "./" - pass(pkg, './features/', 'foobar/features/'); // via "./" + pass(pkg, './features/r.js', '#foobar'); + pass(pkg, './features/r.js', 'foobar/#foobar'); - pass(pkg, './features/hello.js', 'foobar/features/hello'); - pass(pkg, './features/foo/bar.js', 'foobar/features/foo/bar'); + pass(pkg, './features/r/hello.js', 'foobar/#foobar/hello'); + pass(pkg, './features/r/foo/bar.js', 'foobar/#foobar/foo/bar'); // Valid: Pattern trailers allow any exact substrings to be matched - pass(pkg, './features/hello.js.js', 'foobar/features/hello.js'); - pass(pkg, './features/foo/bar.js.js', 'foobar/features/foo/bar.js'); - - pass(pkg, './package.json', 'package.json'); - pass(pkg, './package.json', 'foobar/package.json'); - pass(pkg, './package.json', './package.json'); - - // Does NOT hit "./" (match Node) - fail(pkg, '.', '.'); - fail(pkg, '.', 'foobar'); + pass(pkg, './features/r/hello.js.js', 'foobar/#foobar/hello.js'); + pass(pkg, './features/r/foo/bar.js.js', 'foobar/#foobar/foo/bar.js'); }); // https://github.com/lukeed/resolve.exports/issues/16 - it('exports["./features/*"] :: with `null` internals', () => { + it('imports["#features/*"] :: with `null` internals', () => { let pkg: Package = { "name": "foobar", - "exports": { - "./features/*": "./src/features/*.js", - "./features/internal/*": null + "imports": { + "#features/*": "./src/features/*.js", + "#features/internal/*": null } }; - pass(pkg, './src/features/hello.js', 'features/hello'); - pass(pkg, './src/features/hello.js', 'foobar/features/hello'); + pass(pkg, './src/features/hello.js', '#features/hello'); + pass(pkg, './src/features/hello.js', 'foobar/#features/hello'); - pass(pkg, './src/features/foo/bar.js', 'features/foo/bar'); - pass(pkg, './src/features/foo/bar.js', 'foobar/features/foo/bar'); + pass(pkg, './src/features/foo/bar.js', '#features/foo/bar'); + pass(pkg, './src/features/foo/bar.js', 'foobar/#features/foo/bar'); // TODO? Native throws `ERR_PACKAGE_PATH_NOT_EXPORTED` // Currently throwing `Missing "%s" export in "$s" package` - fail(pkg, './features/internal/hello', 'features/internal/hello'); - fail(pkg, './features/internal/foo/bar', 'features/internal/foo/bar'); + fail(pkg, '#features/internal/hello', '#features/internal/hello'); + fail(pkg, '#features/internal/foo/bar', '#features/internal/foo/bar'); }); // https://github.com/lukeed/resolve.exports/issues/16 - it('exports["./features/*"] :: with `null` internals first', () => { + it('imports["#features/*"] :: with `null` internals first', () => { let pkg: Package = { "name": "foobar", - "exports": { - "./features/internal/*": null, - "./features/*": "./src/features/*.js", + "imports": { + "#features/internal/*": null, + "#features/*": "./src/features/*.js", } }; - pass(pkg, './src/features/hello.js', 'features/hello'); - pass(pkg, './src/features/hello.js', 'foobar/features/hello'); + pass(pkg, './src/features/hello.js', '#features/hello'); + pass(pkg, './src/features/hello.js', 'foobar/#features/hello'); - pass(pkg, './src/features/foo/bar.js', 'features/foo/bar'); - pass(pkg, './src/features/foo/bar.js', 'foobar/features/foo/bar'); + pass(pkg, './src/features/foo/bar.js', '#features/foo/bar'); + pass(pkg, './src/features/foo/bar.js', 'foobar/#features/foo/bar'); // TODO? Native throws `ERR_PACKAGE_PATH_NOT_EXPORTED` // Currently throwing `Missing "%s" export in "$s" package` - fail(pkg, './features/internal/hello', 'features/internal/hello'); - fail(pkg, './features/internal/foo/bar', 'features/internal/foo/bar'); + fail(pkg, '#features/internal/hello', '#features/internal/hello'); + fail(pkg, '#features/internal/foo/bar', '#features/internal/foo/bar'); }); // https://nodejs.org/docs/latest-v18.x/api/packages.html#package-entry-points - it('exports["./features/*"] :: with "./features/*.js" key', () => { + it('imports["#features/*"] :: with "#features/*.js" key', () => { let pkg: Package = { "name": "foobar", - "exports": { - "./features/*": "./features/*.js", - "./features/*.js": "./features/*.js", + "imports": { + "#features/*": "./features/*.js", + "#features/*.js": "./features/*.js", } }; - fail(pkg, './features', 'features'); - fail(pkg, './features', 'foobar/features'); - - fail(pkg, './features/', 'features/'); - fail(pkg, './features/', 'foobar/features/'); + fail(pkg, '#features', '#features'); + fail(pkg, '#features', 'foobar/#features'); - pass(pkg, './features/a.js', 'foobar/features/a'); - pass(pkg, './features/ab.js', 'foobar/features/ab'); - pass(pkg, './features/abc.js', 'foobar/features/abc'); + fail(pkg, '#features/', '#features/'); + fail(pkg, '#features/', 'foobar/#features/'); - pass(pkg, './features/hello.js', 'foobar/features/hello'); - pass(pkg, './features/hello.js', 'foobar/features/hello.js'); + pass(pkg, './features/a.js', 'foobar/#features/a'); + pass(pkg, './features/ab.js', 'foobar/#features/ab'); + pass(pkg, './features/abc.js', 'foobar/#features/abc'); - pass(pkg, './features/foo/bar.js', 'foobar/features/foo/bar'); - pass(pkg, './features/foo/bar.js', 'foobar/features/foo/bar.js'); + pass(pkg, './features/hello.js', 'foobar/#features/hello'); + pass(pkg, './features/hello.js', 'foobar/#features/hello.js'); - fail(pkg, './package.json', 'package.json'); - fail(pkg, './package.json', 'foobar/package.json'); - fail(pkg, './package.json', './package.json'); + pass(pkg, './features/foo/bar.js', 'foobar/#features/foo/bar'); + pass(pkg, './features/foo/bar.js', 'foobar/#features/foo/bar.js'); }); - it('exports["./features/*"] :: conditions', () => { + it('imports["#features/*"] :: conditions', () => { let pkg: Package = { "name": "foobar", - "exports": { - "./features/*": { + "imports": { + "#features/*": { "browser": { "import": "./browser.import/*.mjs", "require": "./browser.require/*.js", @@ -623,338 +451,198 @@ describe('$.resolve', it => { }; // import - fail(pkg, './features/', 'features/'); // no file - fail(pkg, './features/', 'foobar/features/'); // no file + fail(pkg, '#features/', '#features/'); // no file + fail(pkg, '#features/', 'foobar/#features/'); // no file - pass(pkg, './import/hello.mjs', './features/hello'); - pass(pkg, './import/hello.mjs', 'foobar/features/hello'); + pass(pkg, './import/hello.mjs', '#features/hello'); + pass(pkg, './import/hello.mjs', 'foobar/#features/hello'); // require - fail(pkg, './features/', 'features/', { require: true }); // no file - fail(pkg, './features/', 'foobar/features/', { require: true }); // no file + fail(pkg, '#features/', '#features/', { require: true }); // no file + fail(pkg, '#features/', 'foobar/#features/', { require: true }); // no file - pass(pkg, './require/hello.js', './features/hello', { require: true }); - pass(pkg, './require/hello.js', 'foobar/features/hello', { require: true }); + pass(pkg, './require/hello.js', '#features/hello', { require: true }); + pass(pkg, './require/hello.js', 'foobar/#features/hello', { require: true }); // require + browser - fail(pkg, './features/', 'features/', { browser: true, require: true }); // no file - fail(pkg, './features/', 'foobar/features/', { browser: true, require: true }); // no file + fail(pkg, '#features/', '#features/', { browser: true, require: true }); // no file + fail(pkg, '#features/', 'foobar/#features/', { browser: true, require: true }); // no file - pass(pkg, './browser.require/hello.js', './features/hello', { browser: true, require: true }); - pass(pkg, './browser.require/hello.js', 'foobar/features/hello', { browser: true, require: true }); + pass(pkg, './browser.require/hello.js', '#features/hello', { browser: true, require: true }); + pass(pkg, './browser.require/hello.js', 'foobar/#features/hello', { browser: true, require: true }); }); it('should handle mixed path/conditions', () => { let pkg: Package = { "name": "foobar", - "exports": { - ".": [ - { - "import": "./$root.import", - }, - "./$root.string" - ], - "./foo": [ + "imports": { + "#foo": [ { "require": "./$foo.require" }, "./$foo.string" ] } - } - - pass(pkg, ['./$root.import', './$root.string']); - pass(pkg, ['./$root.import', './$root.string'], 'foobar'); + }; // TODO? if len==1 then single? - pass(pkg, ['./$foo.string'], 'foo'); - pass(pkg, ['./$foo.string'], 'foobar/foo'); - pass(pkg, ['./$foo.string'], './foo'); + pass(pkg, ['./$foo.string'], '#foo'); + pass(pkg, ['./$foo.string'], 'foobar/#foo'); - pass(pkg, ['./$foo.require', './$foo.string'], 'foo', { require: true }); - pass(pkg, ['./$foo.require', './$foo.string'], 'foobar/foo', { require: true }); - pass(pkg, ['./$foo.require', './$foo.string'], './foo', { require: true }); + pass(pkg, ['./$foo.require', './$foo.string'], '#foo', { require: true }); + pass(pkg, ['./$foo.require', './$foo.string'], 'foobar/#foo', { require: true }); }); +}); - it('should handle file with leading dot', () => { +describe('$.exports', it => { + it('should be a function', () => { + assert.type(lib.exports, 'function'); + }); + + it('should return nothing if no "exports" map', () => { let pkg: Package = { - "version": "2.41.0", - "name": "aws-cdk-lib", - "exports": { - ".": "./index.js", - "./package.json": "./package.json", - "./.jsii": "./.jsii", - "./.warnings.jsii.js": "./.warnings.jsii.js", - "./alexa-ask": "./alexa-ask/index.js" - } + "name": "foobar" }; - pass(pkg, "./.warnings.jsii.js", ".warnings.jsii.js"); + let output = lib.exports(pkg, '#any'); + assert.is(output, undefined); }); -}); -describe('options.requires', it => { - let pkg: Package = { - "name": "r", - "exports": { - "require": "./$require", - "import": "./$import", - } - }; + it('exports=string', () => { + let pkg: Package = { + "name": "foobar", + "exports": "./$string", + }; - it('should ignore "require" keys by default', () => { - pass(pkg, './$import'); - }); + pass(pkg, './$string'); + pass(pkg, './$string', '.'); + pass(pkg, './$string', 'foobar'); - it('should use "require" key when defined first', () => { - pass(pkg, './$require', '.', { require: true }); + fail(pkg, './other', 'other'); + fail(pkg, './other', 'foobar/other'); + fail(pkg, './hello', './hello'); }); - it('should ignore "import" key when enabled', () => { + it('exports = { self }', () => { let pkg: Package = { - "name": "r", + "name": "foobar", "exports": { "import": "./$import", "require": "./$require", } }; - pass(pkg, './$require', '.', { require: true }); + + pass(pkg, './$import'); pass(pkg, './$import', '.'); + pass(pkg, './$import', 'foobar'); + + fail(pkg, './other', 'other'); + fail(pkg, './other', 'foobar/other'); + fail(pkg, './hello', './hello'); }); - it('should match "default" if "require" is after', () => { + it('exports["."] = string', () => { let pkg: Package = { - "name": "r", + "name": "foobar", "exports": { - "default": "./$default", - "require": "./$require", + ".": "./$self", } }; - pass(pkg, './$default', '.', { require: true }); - }); -}); - -describe('options.browser', it => { - let pkg: Package = { - "name": "b", - "exports": { - "browser": "./$browser", - "node": "./$node", - } - }; - it('should ignore "browser" keys by default', () => { - pass(pkg, './$node'); - }); + pass(pkg, './$self'); + pass(pkg, './$self', '.'); + pass(pkg, './$self', 'foobar'); - it('should use "browser" key when defined first', () => { - pass(pkg, './$browser', '.', { browser: true }); + fail(pkg, './other', 'other'); + fail(pkg, './other', 'foobar/other'); + fail(pkg, './hello', './hello'); }); - it('should ignore "node" key when enabled', () => { + it('exports["."] = object', () => { let pkg: Package = { - "name": "b", + "name": "foobar", "exports": { - "node": "./$node", - "import": "./$import", - "browser": "./$browser", + ".": { + "import": "./$import", + "require": "./$require", + } } }; - // import defined before browser - pass(pkg, './$import', '.', { browser: true }); - }); -}); - -describe('options.conditions', it => { - const pkg: Package = { - "name": "c", - "exports": { - "production": "./$prod", - "development": "./$dev", - "default": "./$default", - } - }; + pass(pkg, './$import'); + pass(pkg, './$import', '.'); + pass(pkg, './$import', 'foobar'); - it('should ignore unknown conditions by default', () => { - pass(pkg, './$default'); + fail(pkg, './other', 'other'); + fail(pkg, './other', 'foobar/other'); + fail(pkg, './hello', './hello'); }); - it('should recognize custom field(s) when specified', () => { - pass(pkg, './$dev', '.', { - conditions: ['development'] - }); + it('exports["./foo"] = string', () => { + let pkg: Package = { + "name": "foobar", + "exports": { + "./foo": "./$import", + } + }; - pass(pkg, './$prod', '.', { - conditions: ['development', 'production'] - }); + pass(pkg, './$import', './foo'); + pass(pkg, './$import', 'foobar/foo'); + + fail(pkg, '.'); + fail(pkg, '.', 'foobar'); + fail(pkg, './other', 'foobar/other'); }); - it('should throw an error if no known conditions', () => { - let ctx: Package = { - "name": "hello", + it('exports["./foo"] = object', () => { + let pkg: Package = { + "name": "foobar", "exports": { - // @ts-ignore - ...pkg.exports - }, + "./foo": { + "import": "./$import", + "require": "./$require", + } + } }; - // @ts-ignore - delete ctx.exports.default; + pass(pkg, './$import', './foo'); + pass(pkg, './$import', 'foobar/foo'); - try { - lib.resolve(ctx); - assert.unreachable(); - } catch (err) { - assert.instance(err, Error); - assert.is((err as Error).message, `No known conditions for "." entry in "hello" package`); - } + fail(pkg, '.'); + fail(pkg, '.', 'foobar'); + fail(pkg, './other', 'foobar/other'); }); -}); -describe('options.unsafe', it => { - let pkg: Package = { - "name": "unsafe", - "exports": { - ".": { - "production": "./$prod", - "development": "./$dev", - "default": "./$default", - }, - "./spec/type": { - "import": "./$import", - "require": "./$require", - "default": "./$default" - }, - "./spec/env": { - "worker": { - "default": "./$worker" - }, - "browser": "./$browser", - "node": "./$node", - "default": "./$default" - } - } - }; - - it('should ignore unknown conditions by default', () => { - pass(pkg, './$default', '.', { - unsafe: true, - }); - }); - - it('should ignore "import" and "require" conditions by default', () => { - pass(pkg, './$default', './spec/type', { - unsafe: true, - }); - - pass(pkg, './$default', './spec/type', { - unsafe: true, - require: true, - }); - }); - - it('should ignore "node" and "browser" conditions by default', () => { - pass(pkg, './$default', './spec/type', { - unsafe: true, - }); - - pass(pkg, './$default', './spec/type', { - unsafe: true, - browser: true, - }); - }); - - it('should respect/accept any custom condition(s) when specified', () => { - // root, dev only - pass(pkg, './$dev', '.', { - unsafe: true, - conditions: ['development'] - }); - - // root, defined order - pass(pkg, './$prod', '.', { - unsafe: true, - conditions: ['development', 'production'] - }); - - // import vs require, defined order - pass(pkg, './$require', './spec/type', { - unsafe: true, - conditions: ['require'] - }); - - // import vs require, defined order - pass(pkg, './$import', './spec/type', { - unsafe: true, - conditions: ['import', 'require'] - }); - - // import vs require, defined order - pass(pkg, './$node', './spec/env', { - unsafe: true, - conditions: ['node'] - }); - - // import vs require, defined order - pass(pkg, './$browser', './spec/env', { - unsafe: true, - conditions: ['browser', 'node'] - }); - - // import vs require, defined order - pass(pkg, './$worker', './spec/env', { - unsafe: true, - conditions: ['browser', 'node', 'worker'] - }); - }); -}); - -describe('$.imports', it => { - it('should be a function', () => { - assert.type(lib.imports, 'function'); - }); - - it('imports["#foo"] = string', () => { + // https://nodejs.org/api/packages.html#packages_nested_conditions + it('nested conditions', () => { let pkg: Package = { "name": "foobar", - "imports": { - "#foo": "./$import", - "#bar": "module-a", + "exports": { + "node": { + "import": "./$node.import", + "require": "./$node.require" + }, + "default": "./$default", } }; - pass(pkg, './$import', '#foo'); - pass(pkg, './$import', 'foobar/#foo'); - - pass(pkg, 'module-a', '#bar'); - pass(pkg, 'module-a', 'foobar/#bar'); - - fail(pkg, '#other', 'foobar/#other'); - }); - - it('imports["#foo"] = object', () => { - let pkg: Package = { - "name": "foobar", - "imports": { - "#foo": { - "import": "./$import", - "require": "./$require", - } - } - }; + pass(pkg, './$node.import'); + pass(pkg, './$node.import', 'foobar'); - pass(pkg, './$import', '#foo'); - pass(pkg, './$import', 'foobar/#foo'); + // browser => no "node" key + pass(pkg, './$default', '.', { browser: true }); + pass(pkg, './$default', 'foobar', { browser: true }); - fail(pkg, '#other', 'foobar/#other'); + fail(pkg, './hello', './hello'); + fail(pkg, './other', 'foobar/other'); + fail(pkg, './other', 'other'); }); it('nested conditions :: subpath', () => { let pkg: Package = { "name": "foobar", - "imports": { - "#lite": { + "exports": { + "./lite": { "node": { "import": "./$node.import", "require": "./$node.require" @@ -967,18 +655,18 @@ describe('$.imports', it => { } }; - pass(pkg, './$node.import', 'foobar/#lite'); - pass(pkg, './$node.require', 'foobar/#lite', { require: true }); + pass(pkg, './$node.import', 'foobar/lite'); + pass(pkg, './$node.require', 'foobar/lite', { require: true }); - pass(pkg, './$browser.import', 'foobar/#lite', { browser: true }); - pass(pkg, './$browser.require', 'foobar/#lite', { browser: true, require: true }); + pass(pkg, './$browser.import', 'foobar/lite', { browser: true }); + pass(pkg, './$browser.require', 'foobar/lite', { browser: true, require: true }); }); it('nested conditions :: subpath :: inverse', () => { let pkg: Package = { "name": "foobar", - "imports": { - "#lite": { + "exports": { + "./lite": { "import": { "browser": "./$browser.import", "node": "./$node.import", @@ -991,90 +679,200 @@ describe('$.imports', it => { } }; - pass(pkg, './$node.import', 'foobar/#lite'); - pass(pkg, './$node.require', 'foobar/#lite', { require: true }); + pass(pkg, './$node.import', 'foobar/lite'); + pass(pkg, './$node.require', 'foobar/lite', { require: true }); - pass(pkg, './$browser.import', 'foobar/#lite', { browser: true }); - pass(pkg, './$browser.require', 'foobar/#lite', { browser: true, require: true }); + pass(pkg, './$browser.import', 'foobar/lite', { browser: true }); + pass(pkg, './$browser.require', 'foobar/lite', { browser: true, require: true }); }); // https://nodejs.org/api/packages.html#packages_subpath_folder_mappings - it('imports["#key/*"]', () => { + it('exports["./"]', () => { let pkg: Package = { "name": "foobar", - "imports": { - "#key/*": "./cheese/*.mjs" + "exports": { + ".": { + "require": "./$require", + "import": "./$import" + }, + "./package.json": "./package.json", + "./": "./" } }; - pass(pkg, './cheese/hello.mjs', 'foobar/#key/hello'); - pass(pkg, './cheese/hello/world.mjs', '#key/hello/world'); + pass(pkg, './$import'); + pass(pkg, './$import', 'foobar'); + pass(pkg, './$require', 'foobar', { require: true }); - // evaluate as defined, not wrong - pass(pkg, './cheese/hello.js.mjs', '#key/hello.js'); - pass(pkg, './cheese/hello.js.mjs', 'foobar/#key/hello.js'); - pass(pkg, './cheese/hello/world.js.mjs', '#key/hello/world.js'); + pass(pkg, './package.json', 'package.json'); + pass(pkg, './package.json', 'foobar/package.json'); + pass(pkg, './package.json', './package.json'); + + // "loose" / everything exposed + pass(pkg, './hello.js', 'hello.js'); + pass(pkg, './hello.js', 'foobar/hello.js'); + pass(pkg, './hello/world.js', './hello/world.js'); }); - it('imports["#key/dir*"]', () => { + it('exports["./"] :: w/o "." key', () => { let pkg: Package = { "name": "foobar", - "imports": { - "#key/dir*": "./cheese/*.mjs" + "exports": { + "./package.json": "./package.json", + "./": "./" } }; - pass(pkg, './cheese/test.mjs', '#key/dirtest'); - pass(pkg, './cheese/test.mjs', 'foobar/#key/dirtest'); + fail(pkg, '.', "."); + fail(pkg, '.', "foobar"); - pass(pkg, './cheese/test/wheel.mjs', '#key/dirtest/wheel'); - pass(pkg, './cheese/test/wheel.mjs', 'foobar/#key/dirtest/wheel'); + pass(pkg, './package.json', 'package.json'); + pass(pkg, './package.json', 'foobar/package.json'); + pass(pkg, './package.json', './package.json'); + + // "loose" / everything exposed + pass(pkg, './hello.js', 'hello.js'); + pass(pkg, './hello.js', 'foobar/hello.js'); + pass(pkg, './hello/world.js', './hello/world.js'); }); - // https://github.com/lukeed/resolve.exports/issues/9 - it('imports["#key/dir*"] :: repeat "*" value', () => { + // https://nodejs.org/api/packages.html#packages_subpath_folder_mappings + it('exports["./*"]', () => { let pkg: Package = { "name": "foobar", - "imports": { - "#key/dir*": "./*sub/dir*/file.js" + "exports": { + "./*": "./cheese/*.mjs" } }; - pass(pkg, './testsub/dirtest/file.js', '#key/dirtest'); - pass(pkg, './testsub/dirtest/file.js', 'foobar/#key/dirtest'); + fail(pkg, '.', "."); + fail(pkg, '.', "foobar"); - pass(pkg, './test/innersub/dirtest/inner/file.js', '#key/dirtest/inner'); - pass(pkg, './test/innersub/dirtest/inner/file.js', 'foobar/#key/dirtest/inner'); + pass(pkg, './cheese/hello.mjs', 'hello'); + pass(pkg, './cheese/hello.mjs', 'foobar/hello'); + pass(pkg, './cheese/hello/world.mjs', './hello/world'); + + // evaluate as defined, not wrong + pass(pkg, './cheese/hello.js.mjs', 'hello.js'); + pass(pkg, './cheese/hello.js.mjs', 'foobar/hello.js'); + pass(pkg, './cheese/hello/world.js.mjs', './hello/world.js'); }); - /** - * @deprecated Documentation-only deprecation in Node 14.13 - * @deprecated Runtime deprecation in Node 16.0 - * @removed Removed in Node 18.0 - * @see https://nodejs.org/docs/latest-v16.x/api/packages.html#subpath-folder-mappings - */ - it('imports["#features/"]', () => { + it('exports["./dir*"]', () => { let pkg: Package = { "name": "foobar", - "imports": { - "#features/": "./features/" + "exports": { + "./dir*": "./cheese/*.mjs" } }; - pass(pkg, './features/', '#features/'); - pass(pkg, './features/', 'foobar/#features/'); + fail(pkg, '.', "."); + fail(pkg, '.', "foobar"); - pass(pkg, './features/hello.js', 'foobar/#features/hello.js'); + pass(pkg, './cheese/test.mjs', 'dirtest'); + pass(pkg, './cheese/test.mjs', 'foobar/dirtest'); - fail(pkg, '#features', '#features'); - fail(pkg, '#features', 'foobar/#features'); + pass(pkg, './cheese/test/wheel.mjs', 'dirtest/wheel'); + pass(pkg, './cheese/test/wheel.mjs', 'foobar/dirtest/wheel'); }); - it('imports["#features/"] :: conditions', () => { + // https://github.com/lukeed/resolve.exports/issues/9 + it('exports["./dir*"] :: repeat "*" value', () => { let pkg: Package = { "name": "foobar", - "imports": { - "#features/": { + "exports": { + "./dir*": "./*sub/dir*/file.js" + } + }; + + fail(pkg, '.', "."); + fail(pkg, '.', "foobar"); + + pass(pkg, './testsub/dirtest/file.js', 'dirtest'); + pass(pkg, './testsub/dirtest/file.js', 'foobar/dirtest'); + + pass(pkg, './test/innersub/dirtest/inner/file.js', 'dirtest/inner'); + pass(pkg, './test/innersub/dirtest/inner/file.js', 'foobar/dirtest/inner'); + }); + + it('exports["./dir*"] :: share "name" start', () => { + let pkg: Package = { + "name": "director", + "exports": { + "./dir*": "./*sub/dir*/file.js" + } + }; + + fail(pkg, '.', "."); + fail(pkg, '.', "director"); + + pass(pkg, './testsub/dirtest/file.js', 'dirtest'); + pass(pkg, './testsub/dirtest/file.js', 'director/dirtest'); + + pass(pkg, './test/innersub/dirtest/inner/file.js', 'dirtest/inner'); + pass(pkg, './test/innersub/dirtest/inner/file.js', 'director/dirtest/inner'); + }); + + /** + * @deprecated Documentation-only deprecation in Node 14.13 + * @deprecated Runtime deprecation in Node 16.0 + * @removed Removed in Node 18.0 + * @see https://nodejs.org/docs/latest-v16.x/api/packages.html#subpath-folder-mappings + */ + it('exports["./features/"]', () => { + let pkg: Package = { + "name": "foobar", + "exports": { + "./features/": "./features/" + } + }; + + pass(pkg, './features/', 'features/'); + pass(pkg, './features/', 'foobar/features/'); + + pass(pkg, './features/hello.js', 'foobar/features/hello.js'); + + fail(pkg, './features', 'features'); + fail(pkg, './features', 'foobar/features'); + + fail(pkg, './package.json', 'package.json'); + fail(pkg, './package.json', 'foobar/package.json'); + fail(pkg, './package.json', './package.json'); + }); + + // https://nodejs.org/api/packages.html#packages_subpath_folder_mappings + it('exports["./features/"] :: with "./" key', () => { + let pkg: Package = { + "name": "foobar", + "exports": { + "./features/": "./features/", + "./package.json": "./package.json", + "./": "./" + } + }; + + pass(pkg, './features', 'features'); // via "./" + pass(pkg, './features', 'foobar/features'); // via "./" + + pass(pkg, './features/', 'features/'); // via "./features/" + pass(pkg, './features/', 'foobar/features/'); // via "./features/" + + pass(pkg, './features/hello.js', 'foobar/features/hello.js'); + + pass(pkg, './package.json', 'package.json'); + pass(pkg, './package.json', 'foobar/package.json'); + pass(pkg, './package.json', './package.json'); + + // Does NOT hit "./" (match Node) + fail(pkg, '.', '.'); + fail(pkg, '.', 'foobar'); + }); + + it('exports["./features/"] :: conditions', () => { + let pkg: Package = { + "name": "foobar", + "exports": { + "./features/": { "browser": { "import": "./browser.import/", "require": "./browser.require/", @@ -1086,172 +884,202 @@ describe('$.imports', it => { }; // import - pass(pkg, './import/', '#features/'); - pass(pkg, './import/', 'foobar/#features/'); + pass(pkg, './import/', 'features/'); + pass(pkg, './import/', 'foobar/features/'); - pass(pkg, './import/hello.js', '#features/hello.js'); - pass(pkg, './import/hello.js', 'foobar/#features/hello.js'); + pass(pkg, './import/hello.js', './features/hello.js'); + pass(pkg, './import/hello.js', 'foobar/features/hello.js'); // require - pass(pkg, './require/', '#features/', { require: true }); - pass(pkg, './require/', 'foobar/#features/', { require: true }); + pass(pkg, './require/', 'features/', { require: true }); + pass(pkg, './require/', 'foobar/features/', { require: true }); - pass(pkg, './require/hello.js', '#features/hello.js', { require: true }); - pass(pkg, './require/hello.js', 'foobar/#features/hello.js', { require: true }); + pass(pkg, './require/hello.js', './features/hello.js', { require: true }); + pass(pkg, './require/hello.js', 'foobar/features/hello.js', { require: true }); // require + browser - pass(pkg, './browser.require/', '#features/', { browser: true, require: true }); - pass(pkg, './browser.require/', 'foobar/#features/', { browser: true, require: true }); + pass(pkg, './browser.require/', 'features/', { browser: true, require: true }); + pass(pkg, './browser.require/', 'foobar/features/', { browser: true, require: true }); - pass(pkg, './browser.require/hello.js', '#features/hello.js', { browser: true, require: true }); - pass(pkg, './browser.require/hello.js', 'foobar/#features/hello.js', { browser: true, require: true }); + pass(pkg, './browser.require/hello.js', './features/hello.js', { browser: true, require: true }); + pass(pkg, './browser.require/hello.js', 'foobar/features/hello.js', { browser: true, require: true }); }); // https://nodejs.org/api/packages.html#packages_subpath_folder_mappings - it('imports["#features/*"]', () => { + it('exports["./features/*"]', () => { let pkg: Package = { "name": "foobar", - "imports": { - "#features/*": "./features/*.js", + "exports": { + "./features/*": "./features/*.js", } }; - fail(pkg, '#features', '#features'); - fail(pkg, '#features', 'foobar/#features'); + fail(pkg, './features', 'features'); + fail(pkg, './features', 'foobar/features'); - fail(pkg, '#features/', '#features/'); - fail(pkg, '#features/', 'foobar/#features/'); + fail(pkg, './features/', 'features/'); + fail(pkg, './features/', 'foobar/features/'); - pass(pkg, './features/a.js', 'foobar/#features/a'); - pass(pkg, './features/ab.js', 'foobar/#features/ab'); - pass(pkg, './features/abc.js', 'foobar/#features/abc'); + pass(pkg, './features/a.js', 'foobar/features/a'); + pass(pkg, './features/ab.js', 'foobar/features/ab'); + pass(pkg, './features/abc.js', 'foobar/features/abc'); - pass(pkg, './features/hello.js', 'foobar/#features/hello'); - pass(pkg, './features/foo/bar.js', 'foobar/#features/foo/bar'); + pass(pkg, './features/hello.js', 'foobar/features/hello'); + pass(pkg, './features/foo/bar.js', 'foobar/features/foo/bar'); // Valid: Pattern trailers allow any exact substrings to be matched - pass(pkg, './features/hello.js.js', 'foobar/#features/hello.js'); - pass(pkg, './features/foo/bar.js.js', 'foobar/#features/foo/bar.js'); + pass(pkg, './features/hello.js.js', 'foobar/features/hello.js'); + pass(pkg, './features/foo/bar.js.js', 'foobar/features/foo/bar.js'); + + fail(pkg, './package.json', 'package.json'); + fail(pkg, './package.json', 'foobar/package.json'); + fail(pkg, './package.json', './package.json'); }); // https://nodejs.org/api/packages.html#packages_subpath_folder_mappings - it('exports["#fooba*"] :: with "#foo*" key', () => { + it('exports["./features/*"] :: with "./" key', () => { let pkg: Package = { "name": "foobar", - "imports": { - "#fooba*": "./features/*.js", - "#foo*": "./" + "exports": { + "./features/*": "./features/*.js", + "./": "./" } }; - pass(pkg, './features/r.js', '#foobar'); - pass(pkg, './features/r.js', 'foobar/#foobar'); + pass(pkg, './features', 'features'); // via "./" + pass(pkg, './features', 'foobar/features'); // via "./" - pass(pkg, './features/r/hello.js', 'foobar/#foobar/hello'); - pass(pkg, './features/r/foo/bar.js', 'foobar/#foobar/foo/bar'); + pass(pkg, './features/', 'features/'); // via "./" + pass(pkg, './features/', 'foobar/features/'); // via "./" + + pass(pkg, './features/hello.js', 'foobar/features/hello'); + pass(pkg, './features/foo/bar.js', 'foobar/features/foo/bar'); // Valid: Pattern trailers allow any exact substrings to be matched - pass(pkg, './features/r/hello.js.js', 'foobar/#foobar/hello.js'); - pass(pkg, './features/r/foo/bar.js.js', 'foobar/#foobar/foo/bar.js'); + pass(pkg, './features/hello.js.js', 'foobar/features/hello.js'); + pass(pkg, './features/foo/bar.js.js', 'foobar/features/foo/bar.js'); + + pass(pkg, './package.json', 'package.json'); + pass(pkg, './package.json', 'foobar/package.json'); + pass(pkg, './package.json', './package.json'); + + // Does NOT hit "./" (match Node) + fail(pkg, '.', '.'); + fail(pkg, '.', 'foobar'); }); // https://github.com/lukeed/resolve.exports/issues/7 - it('imports["#fooba*"] :: with "#foo*" key first', () => { + it('exports["./features/*"] :: with "./" key first', () => { let pkg: Package = { "name": "foobar", - "imports": { - "#foo*": "./", - "#fooba*": "./features/*.js" + "exports": { + "./": "./", + "./features/*": "./features/*.js" } }; - pass(pkg, './features/r.js', '#foobar'); - pass(pkg, './features/r.js', 'foobar/#foobar'); + pass(pkg, './features', 'features'); // via "./" + pass(pkg, './features', 'foobar/features'); // via "./" - pass(pkg, './features/r/hello.js', 'foobar/#foobar/hello'); - pass(pkg, './features/r/foo/bar.js', 'foobar/#foobar/foo/bar'); + pass(pkg, './features/', 'features/'); // via "./" + pass(pkg, './features/', 'foobar/features/'); // via "./" + + pass(pkg, './features/hello.js', 'foobar/features/hello'); + pass(pkg, './features/foo/bar.js', 'foobar/features/foo/bar'); // Valid: Pattern trailers allow any exact substrings to be matched - pass(pkg, './features/r/hello.js.js', 'foobar/#foobar/hello.js'); - pass(pkg, './features/r/foo/bar.js.js', 'foobar/#foobar/foo/bar.js'); + pass(pkg, './features/hello.js.js', 'foobar/features/hello.js'); + pass(pkg, './features/foo/bar.js.js', 'foobar/features/foo/bar.js'); + + pass(pkg, './package.json', 'package.json'); + pass(pkg, './package.json', 'foobar/package.json'); + pass(pkg, './package.json', './package.json'); + + // Does NOT hit "./" (match Node) + fail(pkg, '.', '.'); + fail(pkg, '.', 'foobar'); }); // https://github.com/lukeed/resolve.exports/issues/16 - it('imports["#features/*"] :: with `null` internals', () => { + it('exports["./features/*"] :: with `null` internals', () => { let pkg: Package = { "name": "foobar", - "imports": { - "#features/*": "./src/features/*.js", - "#features/internal/*": null + "exports": { + "./features/*": "./src/features/*.js", + "./features/internal/*": null } }; - pass(pkg, './src/features/hello.js', '#features/hello'); - pass(pkg, './src/features/hello.js', 'foobar/#features/hello'); + pass(pkg, './src/features/hello.js', 'features/hello'); + pass(pkg, './src/features/hello.js', 'foobar/features/hello'); - pass(pkg, './src/features/foo/bar.js', '#features/foo/bar'); - pass(pkg, './src/features/foo/bar.js', 'foobar/#features/foo/bar'); + pass(pkg, './src/features/foo/bar.js', 'features/foo/bar'); + pass(pkg, './src/features/foo/bar.js', 'foobar/features/foo/bar'); // TODO? Native throws `ERR_PACKAGE_PATH_NOT_EXPORTED` // Currently throwing `Missing "%s" export in "$s" package` - fail(pkg, '#features/internal/hello', '#features/internal/hello'); - fail(pkg, '#features/internal/foo/bar', '#features/internal/foo/bar'); + fail(pkg, './features/internal/hello', 'features/internal/hello'); + fail(pkg, './features/internal/foo/bar', 'features/internal/foo/bar'); }); // https://github.com/lukeed/resolve.exports/issues/16 - it('imports["#features/*"] :: with `null` internals first', () => { + it('exports["./features/*"] :: with `null` internals first', () => { let pkg: Package = { "name": "foobar", - "imports": { - "#features/internal/*": null, - "#features/*": "./src/features/*.js", + "exports": { + "./features/internal/*": null, + "./features/*": "./src/features/*.js", } }; - pass(pkg, './src/features/hello.js', '#features/hello'); - pass(pkg, './src/features/hello.js', 'foobar/#features/hello'); + pass(pkg, './src/features/hello.js', 'features/hello'); + pass(pkg, './src/features/hello.js', 'foobar/features/hello'); - pass(pkg, './src/features/foo/bar.js', '#features/foo/bar'); - pass(pkg, './src/features/foo/bar.js', 'foobar/#features/foo/bar'); + pass(pkg, './src/features/foo/bar.js', 'features/foo/bar'); + pass(pkg, './src/features/foo/bar.js', 'foobar/features/foo/bar'); // TODO? Native throws `ERR_PACKAGE_PATH_NOT_EXPORTED` // Currently throwing `Missing "%s" export in "$s" package` - fail(pkg, '#features/internal/hello', '#features/internal/hello'); - fail(pkg, '#features/internal/foo/bar', '#features/internal/foo/bar'); + fail(pkg, './features/internal/hello', 'features/internal/hello'); + fail(pkg, './features/internal/foo/bar', 'features/internal/foo/bar'); }); // https://nodejs.org/docs/latest-v18.x/api/packages.html#package-entry-points - it('imports["#features/*"] :: with "#features/*.js" key', () => { + it('exports["./features/*"] :: with "./features/*.js" key', () => { let pkg: Package = { "name": "foobar", - "imports": { - "#features/*": "./features/*.js", - "#features/*.js": "./features/*.js", + "exports": { + "./features/*": "./features/*.js", + "./features/*.js": "./features/*.js", } }; - fail(pkg, '#features', '#features'); - fail(pkg, '#features', 'foobar/#features'); + fail(pkg, './features', 'features'); + fail(pkg, './features', 'foobar/features'); - fail(pkg, '#features/', '#features/'); - fail(pkg, '#features/', 'foobar/#features/'); + fail(pkg, './features/', 'features/'); + fail(pkg, './features/', 'foobar/features/'); - pass(pkg, './features/a.js', 'foobar/#features/a'); - pass(pkg, './features/ab.js', 'foobar/#features/ab'); - pass(pkg, './features/abc.js', 'foobar/#features/abc'); + pass(pkg, './features/a.js', 'foobar/features/a'); + pass(pkg, './features/ab.js', 'foobar/features/ab'); + pass(pkg, './features/abc.js', 'foobar/features/abc'); - pass(pkg, './features/hello.js', 'foobar/#features/hello'); - pass(pkg, './features/hello.js', 'foobar/#features/hello.js'); + pass(pkg, './features/hello.js', 'foobar/features/hello'); + pass(pkg, './features/hello.js', 'foobar/features/hello.js'); - pass(pkg, './features/foo/bar.js', 'foobar/#features/foo/bar'); - pass(pkg, './features/foo/bar.js', 'foobar/#features/foo/bar.js'); + pass(pkg, './features/foo/bar.js', 'foobar/features/foo/bar'); + pass(pkg, './features/foo/bar.js', 'foobar/features/foo/bar.js'); + + fail(pkg, './package.json', 'package.json'); + fail(pkg, './package.json', 'foobar/package.json'); + fail(pkg, './package.json', './package.json'); }); - it('imports["#features/*"] :: conditions', () => { + it('exports["./features/*"] :: conditions', () => { let pkg: Package = { "name": "foobar", - "imports": { - "#features/*": { + "exports": { + "./features/*": { "browser": { "import": "./browser.import/*.mjs", "require": "./browser.require/*.js", @@ -1263,45 +1091,289 @@ describe('$.imports', it => { }; // import - fail(pkg, '#features/', '#features/'); // no file - fail(pkg, '#features/', 'foobar/#features/'); // no file + fail(pkg, './features/', 'features/'); // no file + fail(pkg, './features/', 'foobar/features/'); // no file - pass(pkg, './import/hello.mjs', '#features/hello'); - pass(pkg, './import/hello.mjs', 'foobar/#features/hello'); + pass(pkg, './import/hello.mjs', './features/hello'); + pass(pkg, './import/hello.mjs', 'foobar/features/hello'); // require - fail(pkg, '#features/', '#features/', { require: true }); // no file - fail(pkg, '#features/', 'foobar/#features/', { require: true }); // no file + fail(pkg, './features/', 'features/', { require: true }); // no file + fail(pkg, './features/', 'foobar/features/', { require: true }); // no file - pass(pkg, './require/hello.js', '#features/hello', { require: true }); - pass(pkg, './require/hello.js', 'foobar/#features/hello', { require: true }); + pass(pkg, './require/hello.js', './features/hello', { require: true }); + pass(pkg, './require/hello.js', 'foobar/features/hello', { require: true }); // require + browser - fail(pkg, '#features/', '#features/', { browser: true, require: true }); // no file - fail(pkg, '#features/', 'foobar/#features/', { browser: true, require: true }); // no file + fail(pkg, './features/', 'features/', { browser: true, require: true }); // no file + fail(pkg, './features/', 'foobar/features/', { browser: true, require: true }); // no file - pass(pkg, './browser.require/hello.js', '#features/hello', { browser: true, require: true }); - pass(pkg, './browser.require/hello.js', 'foobar/#features/hello', { browser: true, require: true }); + pass(pkg, './browser.require/hello.js', './features/hello', { browser: true, require: true }); + pass(pkg, './browser.require/hello.js', 'foobar/features/hello', { browser: true, require: true }); }); it('should handle mixed path/conditions', () => { let pkg: Package = { "name": "foobar", - "imports": { - "#foo": [ + "exports": { + ".": [ + { + "import": "./$root.import", + }, + "./$root.string" + ], + "./foo": [ { "require": "./$foo.require" }, "./$foo.string" ] } - }; + } + + pass(pkg, ['./$root.import', './$root.string']); + pass(pkg, ['./$root.import', './$root.string'], 'foobar'); // TODO? if len==1 then single? - pass(pkg, ['./$foo.string'], '#foo'); - pass(pkg, ['./$foo.string'], 'foobar/#foo'); + pass(pkg, ['./$foo.string'], 'foo'); + pass(pkg, ['./$foo.string'], 'foobar/foo'); + pass(pkg, ['./$foo.string'], './foo'); - pass(pkg, ['./$foo.require', './$foo.string'], '#foo', { require: true }); - pass(pkg, ['./$foo.require', './$foo.string'], 'foobar/#foo', { require: true }); + pass(pkg, ['./$foo.require', './$foo.string'], 'foo', { require: true }); + pass(pkg, ['./$foo.require', './$foo.string'], 'foobar/foo', { require: true }); + pass(pkg, ['./$foo.require', './$foo.string'], './foo', { require: true }); + }); + + it('should handle file with leading dot', () => { + let pkg: Package = { + "version": "2.41.0", + "name": "aws-cdk-lib", + "exports": { + ".": "./index.js", + "./package.json": "./package.json", + "./.jsii": "./.jsii", + "./.warnings.jsii.js": "./.warnings.jsii.js", + "./alexa-ask": "./alexa-ask/index.js" + } + }; + + pass(pkg, "./.warnings.jsii.js", ".warnings.jsii.js"); + }); +}); + +describe('options.requires', it => { + let pkg: Package = { + "name": "r", + "exports": { + "require": "./$require", + "import": "./$import", + } + }; + + it('should ignore "require" keys by default', () => { + pass(pkg, './$import'); + }); + + it('should use "require" key when defined first', () => { + pass(pkg, './$require', '.', { require: true }); + }); + + it('should ignore "import" key when enabled', () => { + let pkg: Package = { + "name": "r", + "exports": { + "import": "./$import", + "require": "./$require", + } + }; + pass(pkg, './$require', '.', { require: true }); + pass(pkg, './$import', '.'); + }); + + it('should match "default" if "require" is after', () => { + let pkg: Package = { + "name": "r", + "exports": { + "default": "./$default", + "require": "./$require", + } + }; + pass(pkg, './$default', '.', { require: true }); + }); +}); + +describe('options.browser', it => { + let pkg: Package = { + "name": "b", + "exports": { + "browser": "./$browser", + "node": "./$node", + } + }; + + it('should ignore "browser" keys by default', () => { + pass(pkg, './$node'); }); -}) + + it('should use "browser" key when defined first', () => { + pass(pkg, './$browser', '.', { browser: true }); + }); + + it('should ignore "node" key when enabled', () => { + let pkg: Package = { + "name": "b", + "exports": { + "node": "./$node", + "import": "./$import", + "browser": "./$browser", + } + }; + + // import defined before browser + pass(pkg, './$import', '.', { browser: true }); + }); +}); + +describe('options.conditions', it => { + const pkg: Package = { + "name": "c", + "exports": { + "production": "./$prod", + "development": "./$dev", + "default": "./$default", + } + }; + + it('should ignore unknown conditions by default', () => { + pass(pkg, './$default'); + }); + + it('should recognize custom field(s) when specified', () => { + pass(pkg, './$dev', '.', { + conditions: ['development'] + }); + + pass(pkg, './$prod', '.', { + conditions: ['development', 'production'] + }); + }); + + it('should throw an error if no known conditions', () => { + let ctx: Package = { + "name": "hello", + "exports": { + // @ts-ignore + ...pkg.exports + }, + }; + + // @ts-ignore + delete ctx.exports.default; + + try { + lib.resolve(ctx); + assert.unreachable(); + } catch (err) { + assert.instance(err, Error); + assert.is((err as Error).message, `No known conditions for "." entry in "hello" package`); + } + }); +}); + +describe('options.unsafe', it => { + let pkg: Package = { + "name": "unsafe", + "exports": { + ".": { + "production": "./$prod", + "development": "./$dev", + "default": "./$default", + }, + "./spec/type": { + "import": "./$import", + "require": "./$require", + "default": "./$default" + }, + "./spec/env": { + "worker": { + "default": "./$worker" + }, + "browser": "./$browser", + "node": "./$node", + "default": "./$default" + } + } + }; + + it('should ignore unknown conditions by default', () => { + pass(pkg, './$default', '.', { + unsafe: true, + }); + }); + + it('should ignore "import" and "require" conditions by default', () => { + pass(pkg, './$default', './spec/type', { + unsafe: true, + }); + + pass(pkg, './$default', './spec/type', { + unsafe: true, + require: true, + }); + }); + + it('should ignore "node" and "browser" conditions by default', () => { + pass(pkg, './$default', './spec/type', { + unsafe: true, + }); + + pass(pkg, './$default', './spec/type', { + unsafe: true, + browser: true, + }); + }); + + it('should respect/accept any custom condition(s) when specified', () => { + // root, dev only + pass(pkg, './$dev', '.', { + unsafe: true, + conditions: ['development'] + }); + + // root, defined order + pass(pkg, './$prod', '.', { + unsafe: true, + conditions: ['development', 'production'] + }); + + // import vs require, defined order + pass(pkg, './$require', './spec/type', { + unsafe: true, + conditions: ['require'] + }); + + // import vs require, defined order + pass(pkg, './$import', './spec/type', { + unsafe: true, + conditions: ['import', 'require'] + }); + + // import vs require, defined order + pass(pkg, './$node', './spec/env', { + unsafe: true, + conditions: ['node'] + }); + + // import vs require, defined order + pass(pkg, './$browser', './spec/env', { + unsafe: true, + conditions: ['browser', 'node'] + }); + + // import vs require, defined order + pass(pkg, './$worker', './spec/env', { + unsafe: true, + conditions: ['browser', 'node', 'worker'] + }); + }); +}); From 93b8215a1a0b72e34053299d56ba983b1cbfc959 Mon Sep 17 00:00:00 2001 From: Luke Edwards Date: Sun, 15 Jan 2023 06:08:36 -0800 Subject: [PATCH 22/35] chore: rename test file --- test/{resolve.ts => index.ts} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename test/{resolve.ts => index.ts} (99%) diff --git a/test/resolve.ts b/test/index.ts similarity index 99% rename from test/resolve.ts rename to test/index.ts index 07b3221..3aeab5a 100644 --- a/test/resolve.ts +++ b/test/index.ts @@ -1,6 +1,6 @@ import * as uvu from 'uvu'; import * as assert from 'uvu/assert'; -import * as lib from '../src'; +import * as lib from '../src/index'; import type * as t from 'resolve.exports'; From 03c14c5730f5fcab60229e18c441a3fe05b4fcb7 Mon Sep 17 00:00:00 2001 From: Luke Edwards Date: Sun, 15 Jan 2023 06:21:58 -0800 Subject: [PATCH 23/35] chore: more tests --- test/index.ts | 21 +++++++++++++++++++++ test/utils.ts | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/test/index.ts b/test/index.ts index 3aeab5a..fde0655 100644 --- a/test/index.ts +++ b/test/index.ts @@ -56,6 +56,9 @@ describe('$.resolve', it => { let output = lib.resolve(pkg); assert.is(output, './hello.mjs'); + output = lib.resolve(pkg, '.'); + assert.is(output, './hello.mjs'); + try { lib.resolve(pkg, './other'); assert.unreachable(); @@ -508,6 +511,24 @@ describe('$.exports', it => { assert.is(output, undefined); }); + it('should default to "." target input', () => { + let pkg: Package = { + "name": "foobar", + "exports": { + ".": "./hello.mjs" + } + }; + + let output = lib.exports(pkg); + assert.is(output, './hello.mjs'); + + output = lib.exports(pkg, '.'); + assert.is(output, './hello.mjs'); + + output = lib.exports(pkg, 'foobar'); + assert.is(output, './hello.mjs'); + }); + it('exports=string', () => { let pkg: Package = { "name": "foobar", diff --git a/test/utils.ts b/test/utils.ts index e2791fe..0098713 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -192,6 +192,39 @@ describe('$.toEntry', it => { }); }); +describe('$.injects', it => { + function run(input: T, value: string, expect: T) { + let output = $.injects(input, value); + if (Array.isArray(expect)) assert.equal(output, expect); + else assert.is(output, expect); + } + + it('should be a function', () => { + assert.type($.injects, 'function'); + }); + + it('should replace "*" character in string input', () => { + run('./foo*.jpg', 'bar', './foobar.jpg'); + }); + + it('should replace multiple "*" characters w/ same value', () => { + run('./*/foo-*.jpg', 'bar', './bar/foo-bar.jpg'); + }); + + // for the "./features/" => "./src/features/" scenario + it('should append `value` if missing "*" character', () => { + run('./src/features/', 'app.js', './src/features/app.js'); + }); + + it('should accept string[] input', () => { + run( + ['./foo/', './esm/*.mjs', './build/*/index-*.js'], + 'xyz', + ['./foo/xyz', './esm/xyz.mjs', './build/xyz/index-xyz.js'], + ); + }); +}); + describe('$.loop', it => { const FILE = './file.js'; const DEFAULT = './foobar.js'; From 7fb205e9eb9ee9644b70e17500b02a0677a83c36 Mon Sep 17 00:00:00 2001 From: Luke Edwards Date: Sun, 15 Jan 2023 06:37:57 -0800 Subject: [PATCH 24/35] chore: error message "entry" -> "specifier" --- src/utils.ts | 4 ++-- test/index.ts | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index bf37242..13195e3 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -7,8 +7,8 @@ export type Mapping = Record; export function throws(name: string, entry: Entry, condition?: number): never { throw new Error( condition - ? `No known conditions for "${entry}" entry in "${name}" package` - : `Missing "${entry}" export in "${name}" package` + ? `No known conditions for "${entry}" specifier in "${name}" package` + : `Missing "${entry}" specifier in "${name}" package` ); } diff --git a/test/index.ts b/test/index.ts index fde0655..f0056af 100644 --- a/test/index.ts +++ b/test/index.ts @@ -20,7 +20,7 @@ function fail(pkg: Package, target: Entry, entry?: string, options?: Options) { assert.unreachable(); } catch (err) { assert.instance(err, Error); - assert.is((err as Error).message, `Missing "${target}" export in "${pkg.name}" package`); + assert.is((err as Error).message, `Missing "${target}" specifier in "${pkg.name}" package`); } } @@ -64,7 +64,7 @@ describe('$.resolve', it => { assert.unreachable(); } catch (err) { assert.instance(err, Error); - assert.is((err as Error).message, `Missing "./other" export in "foobar" package`); + assert.is((err as Error).message, `Missing "./other" specifier in "foobar" package`); } }); @@ -87,7 +87,7 @@ describe('$.resolve', it => { assert.unreachable(); } catch (err) { assert.instance(err, Error); - assert.is((err as Error).message, `Missing "#bar" export in "foobar" package`); + assert.is((err as Error).message, `Missing "#bar" specifier in "foobar" package`); } }); }); @@ -384,7 +384,7 @@ describe('$.imports', it => { pass(pkg, './src/features/foo/bar.js', 'foobar/#features/foo/bar'); // TODO? Native throws `ERR_PACKAGE_PATH_NOT_EXPORTED` - // Currently throwing `Missing "%s" export in "$s" package` + // Currently throwing `Missing "%s" specifier in "$s" package` fail(pkg, '#features/internal/hello', '#features/internal/hello'); fail(pkg, '#features/internal/foo/bar', '#features/internal/foo/bar'); }); @@ -406,7 +406,7 @@ describe('$.imports', it => { pass(pkg, './src/features/foo/bar.js', 'foobar/#features/foo/bar'); // TODO? Native throws `ERR_PACKAGE_PATH_NOT_EXPORTED` - // Currently throwing `Missing "%s" export in "$s" package` + // Currently throwing `Missing "%s" specifier in "$s" package` fail(pkg, '#features/internal/hello', '#features/internal/hello'); fail(pkg, '#features/internal/foo/bar', '#features/internal/foo/bar'); }); @@ -1038,7 +1038,7 @@ describe('$.exports', it => { pass(pkg, './src/features/foo/bar.js', 'foobar/features/foo/bar'); // TODO? Native throws `ERR_PACKAGE_PATH_NOT_EXPORTED` - // Currently throwing `Missing "%s" export in "$s" package` + // Currently throwing `Missing "%s" specifier in "$s" package` fail(pkg, './features/internal/hello', 'features/internal/hello'); fail(pkg, './features/internal/foo/bar', 'features/internal/foo/bar'); }); @@ -1060,7 +1060,7 @@ describe('$.exports', it => { pass(pkg, './src/features/foo/bar.js', 'foobar/features/foo/bar'); // TODO? Native throws `ERR_PACKAGE_PATH_NOT_EXPORTED` - // Currently throwing `Missing "%s" export in "$s" package` + // Currently throwing `Missing "%s" specifier in "$s" package` fail(pkg, './features/internal/hello', 'features/internal/hello'); fail(pkg, './features/internal/foo/bar', 'features/internal/foo/bar'); }); @@ -1296,7 +1296,7 @@ describe('options.conditions', it => { assert.unreachable(); } catch (err) { assert.instance(err, Error); - assert.is((err as Error).message, `No known conditions for "." entry in "hello" package`); + assert.is((err as Error).message, `No known conditions for "." specifier in "hello" package`); } }); }); From ccaf9ffd76b48ae3086eaa4f1d5f313b61c56ace Mon Sep 17 00:00:00 2001 From: Luke Edwards Date: Sun, 15 Jan 2023 08:01:48 -0800 Subject: [PATCH 25/35] chore: update docs --- index.d.ts | 27 +++++- readme.md | 250 ++++++++++++++++++++++++++++++++++++++++------------- 2 files changed, 214 insertions(+), 63 deletions(-) diff --git a/index.d.ts b/index.d.ts index c54b337..aee3056 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,11 +1,32 @@ export type Options = { + /** + * When true, adds the "browser" conditions. + * Otherwise the "node" condition is enabled. + * @default false + */ browser?: boolean; + /** + * Any custom conditions to match. + * @note Array order does not matter. Priority is determined by the key-order of conditions defined within a package's imports/exports mapping. + * @default [] + */ conditions?: readonly string[]; + /** + * When true, adds the "require" condition. + * Otherwise the "import" condition is enabled. + * @default false + */ require?: boolean; + /** + * Prevents "require", "import", "browser", and/or "node" conditions from being added automatically. + * When enabled, only `options.conditions` are added alongside the "default" condition. + * @important Enabling this deviates from Node.js default behavior. + * @default false + */ unsafe?: boolean; } -export function resolve(pkg: T, entry: string, options?: Options): Imports.Output | Exports.Output | void; +export function resolve(pkg: T, entry?: string, options?: Options): Imports.Output | Exports.Output | void; type WithName = `${string}/${T}`; @@ -40,7 +61,7 @@ export namespace Imports { type External = string; - /** string ~> dependency OR internal path */ + /** strings are dependency names OR internal paths */ export type Value = External | Path | null | { [c: Condition]: Value; } | Value[]; @@ -58,7 +79,7 @@ export namespace Exports { /** Allows "." and "./{name}" */ export type Entry = `.${string}`; - /** string ~> internal path */ + /** strings must be internal paths */ export type Value = Path | null | { [c: Condition]: Value; } | Value[]; diff --git a/readme.md b/readme.md index 715c8b6..823b3d0 100644 --- a/readme.md +++ b/readme.md @@ -1,29 +1,15 @@ # resolve.exports [![CI](https://github.com/lukeed/resolve.exports/workflows/CI/badge.svg)](https://github.com/lukeed/resolve.exports/actions) [![codecov](https://codecov.io/gh/lukeed/resolve.exports/branch/master/graph/badge.svg?token=4P7d4Omw2h)](https://codecov.io/gh/lukeed/resolve.exports) -> A tiny (813b), correct, general-purpose, and configurable `"exports"` resolver without file-system reliance +> A tiny (987b), correct, general-purpose, and configurable `"exports"` and `"imports"` resolver without file-system reliance ***Why?*** Hopefully, this module may serve as a reference point (and/or be used directly) so that the varying tools and bundlers within the ecosystem can share a common approach with one another **as well as** with the native Node.js implementation. -With the push for ESM, we must be _very_ careful and avoid fragmentation. If we, as a community, begin propagating different _dialects_ of `"exports"` resolution, then we're headed for deep trouble. It will make supporting (and using) `"exports"` nearly impossible, which may force its abandonment and along with it, its benefits. +With the push for ESM, we must be _very_ careful and avoid fragmentation. If we, as a community, begin propagating different _dialects_ of the resolution algorithm, then we're headed for deep trouble. It will make supporting (and using) `"exports"` nearly impossible, which may force its abandonment and along with it, its benefits. Let's have nice things. -***TODO*** - -- [x] exports string -- [x] exports object (single entry) -- [x] exports object (multi entry) -- [x] nested / recursive conditions -- [x] exports arrayable -- [x] directory mapping (`./foobar/` => `/foobar/`) -- [x] directory mapping (`./foobar/*` => `./other/*.js`) -- [x] directory mapping w/ conditions -- [x] directory mapping w/ nested conditions -- [x] legacy fields (`main` vs `module` vs ...) -- [x] legacy "browser" files object - ## Install ```sh @@ -35,12 +21,22 @@ $ npm install resolve.exports > Please see [`/test/`](/test) for examples. ```js -import { resolve, legacy } from 'resolve.exports'; +import * as resolve from 'resolve.exports'; -const contents = { +// package.json contents +const pkg = { "name": "foobar", "module": "dist/module.mjs", "main": "dist/require.js", + "imports": { + "#hash": { + "import": { + "browser": "./hash/web.mjs", + "node": "./hash/node.mjs", + }, + "default": "./hash/detect.js" + } + }, "exports": { ".": { "import": "./dist/module.mjs", @@ -57,64 +53,159 @@ const contents = { } }; -// Assumes `.` as default entry -// Assumes `import` as default condition -resolve(contents); //=> "./dist/module.mjs" +// --- +// Exports +// --- -// entry: nullish === "foobar" === "." -resolve(contents, 'foobar'); //=> "./dist/module.mjs" -resolve(contents, '.'); //=> "./dist/module.mjs" +// entry: "foobar" === "." === default +// conditions: ["default", "import", "node"] +resolve.exports(pkg); +resolve.exports(pkg, '.'); +resolve.exports(pkg, 'foobar'); +//=> "./dist/module.mjs" // entry: "foobar/lite" === "./lite" -resolve(contents, 'foobar/lite'); //=> "./lite/module.mjs" -resolve(contents, './lite'); //=> "./lite/module.mjs" +// conditions: ["default", "import", "node"] +resolve.exports(pkg, 'foobar/lite'); +resolve.exports(pkg, './lite'); +//=> "./lite/module.mjs" -// Assume `require` usage -resolve(contents, 'foobar', { require: true }); //=> "./dist/require.js" -resolve(contents, './lite', { require: true }); //=> "./lite/require.js" +// Enable `require` condition +// conditions: ["default", "require", "node"] +resolve.exports(pkg, 'foobar', { require: true }); //=> "./dist/require.js" +resolve.exports(pkg, './lite', { require: true }); //=> "./lite/require.js" -// Throws "Missing export in package" Error -resolve(contents, 'foobar/hello'); -resolve(contents, './hello/world'); +// Throws "Missing specifier in package" Error +resolve.exports(pkg, 'foobar/hello'); +resolve.exports(pkg, './hello/world'); // Add custom condition(s) -resolve(contents, 'foobar/lite', { +// conditions: ["default", "worker", "import", "node"] +resolve.exports(pkg, 'foobar/lite', { conditions: ['worker'] -}); // => "./lite/worker.node.js" +}); //=> "./lite/worker.node.js" // Toggle "browser" condition -resolve(contents, 'foobar/lite', { +// conditions: ["default", "worker", "import", "browser"] +resolve.exports(pkg, 'foobar/lite', { conditions: ['worker'], browser: true -}); // => "./lite/worker.browser.js" +}); //=> "./lite/worker.browser.js" + +// Disable non-"default" condition activate +// NOTE: breaks from Node.js default behavior +// conditions: ["default", "custom"] +resolve.exports(pkg, 'foobar/lite', { + conditions: ['custom'], + unsafe: true, +}); +//=> Error: No known conditions for "./lite" specifier in "foobar" package + +// --- +// Imports +// --- + +// conditions: ["default", "import", "node"] +resolve.imports(pkg, '#hash'); +resolve.imports(pkg, 'foobar/#hash'); +//=> "./hash/node.mjs" + +// conditions: ["default", "import", "browser"] +resolve.imports(pkg, '#hash', { browser: true }); +resolve.imports(pkg, 'foobar/#hash'); +//=> "./hash/web.mjs" + +// conditions: ["default"] +resolve.imports(pkg, '#hash', { unsafe: true }); +resolve.imports(pkg, 'foobar/#hash'); +//=> "./hash/web.mjs" + +resolve.imports(pkg, '#hello/world'); +resolve.imports(pkg, 'foobar/#hello/world'); +//=> Error: Missing "#hello/world" specifier in "foobar" package // --- // Legacy // --- // prefer "module" > "main" (default) -legacy(contents); //=> "dist/module.mjs" +resolve.legacy(pkg); //=> "dist/module.mjs" // customize fields order -legacy(contents, { +resolve.legacy(pkg, { fields: ['main', 'module'] }); //=> "dist/require.js" ``` ## API +The [`resolve()`](#resolvepkg-entry-options), [`exports()`](#exportspkg-entry-options), and [`imports()`](#importspkg-target-options) functions share similar API signatures: + +```ts +type Output = string[] | string | undefined; +export function resolve(pkg: Pacakge, entry?: string, options?: Options): Output; +export function exports(pkg: Pacakge, entry?: string, options?: Options): Output; +export function imports(pkg: Pacakge, target: string, options?: Options): Output; +// ^ not optional! +``` + +All three: +* accept a `package.json` file's contents as a JSON object +* accept a target/entry identifier +* may accept an [Options](#options) object +* return `string[]`, `string`, or `undefined` + +The only difference is that `imports()` must accept a target identifier as there can be no inferred default. + +See below for further API descriptions. + +> **Note:** There is also a [Legacy Resolver API](#legacy-resolver) + +--- + ### resolve(pkg, entry?, options?) -Returns: `string` or `undefined` +Returns: `string[]` or `string` or `undefined` + +A convenience helper which automatically reroutes to [`exports()`](#exportspkg-entry-options) or [`imports()`](#importspkg-target-options) depending on the `entry` value. + +When unspecified, `entry` defaults to the `"."` identifier, which means that `exports()` will be invoked. + +```js +import * as r from 'resolve.exports'; + +let pkg = { + name: 'foobar', + // ... +}; + +r.resolve(pkg); +//~> r.exports(pkg, '.'); + +r.resolve(pkg, 'foobar'); +//~> r.exports(pkg, '.'); + +r.resolve(pkg, 'foobar/subpath'); +//~> r.exports(pkg, './subpath'); + +r.resolve(pkg, '#hash/md5'); +//~> r.imports(pkg, '#hash/md5'); + +r.resolve(pkg, 'foobar/#hash/md5'); +//~> r.imports(pkg, '#hash/md5'); +``` + +### exports(pkg, entry?, options?) +Returns: `string[]` or `string` or `undefined` Traverse the `"exports"` within the contents of a `package.json` file.
If the contents _does not_ contain an `"exports"` map, then `undefined` will be returned. -Successful resolutions will always result in a string value. This will be the value of the resolved mapping itself – which means that the output is a relative file path. +Successful resolutions will always result in a `string` or `string[]` value. This will be the value of the resolved mapping itself – which means that the output is a relative file path. This function may throw an Error if: * the requested `entry` cannot be resolved (aka, not defined in the `"exports"` map) -* an `entry` _was_ resolved but no known conditions were found (see [`options.conditions`](#optionsconditions)) +* an `entry` _is_ defined but no known conditions were matched (see [`options.conditions`](#optionsconditions)) #### pkg Type: `object`
@@ -149,6 +240,41 @@ Assume we have a module named "foobar" and whose `pkg` contains `"name": "foobar | `'lite'` | `'./lite'` | value was not relative & did not have `pkg.name` prefix | +### imports(pkg, target, options?) +Returns: `string[]` or `string` or `undefined` + +Traverse the `"imports"` within the contents of a `package.json` file.
+If the contents _does not_ contain an `"imports"` map, then `undefined` will be returned. + +Successful resolutions will always result in a `string` or `string[]` value. This will be the value of the resolved mapping itself – which means that the output is a relative file path. + +This function may throw an Error if: + +* the requested `target` cannot be resolved (aka, not defined in the `"imports"` map) +* an `target` _is_ defined but no known conditions were matched (see [`options.conditions`](#optionsconditions)) + +#### pkg +Type: `object`
+Required: `true` + +The `package.json` contents. + +#### target +Type: `string`
+Required: `true` + +The target import identifier; for example, `#hash` or `#hash/md5`. + +Import specifiers _must_ begin with the `#` character, as required by the resolution specification. However, if `target` begins with the package name (determined by the `pkg.name` value), then `resolve.exports` will trim it from the `target` identifier. For example, `"foobar/#hash/md5"` will be treated as `"#hash/md5"` for the `"foobar"` package. + +## Options + +The [`resolve()`](#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. + +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). + +> **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))_ + #### options.require Type: `boolean`
Default: `false` @@ -174,8 +300,8 @@ Provide a list of additional/custom conditions that should be accepted when seen For example, you may choose to accept a `"production"` condition in certain environments. Given the following `pkg` content: ```js -const contents = { - // ... +const pkg = { + // package.json ... "exports": { "worker": "./index.worker.js", "require": "./index.require.js", @@ -184,24 +310,24 @@ const contents = { } }; -resolve(contents, '.'); +resolve.exports(pkg, '.'); //=> "./index.import.mjs" -resolve(contents, '.', { +resolve.exports(pkg, '.', { conditions: ['production'] }); //=> "./index.prod.js" -resolve(contents, '.', { +resolve.exports(pkg, '.', { conditions: ['production'], require: true, }); //=> "./index.require.js" -resolve(contents, '.', { +resolve.exports(pkg, '.', { conditions: ['production', 'worker'], require: true, }); //=> "./index.worker.js" -resolve(contents, '.', { +resolve.exports(pkg, '.', { conditions: ['production', 'worker'] }); //=> "./index.worker.js" ``` @@ -215,13 +341,13 @@ Default: `false` When enabled, this option will ignore **all other options** except [`options.conditions`](#optionsconditions). This is because, when enabled, `options.unsafe` **does not** assume or provide any default conditions except the `"default"` condition. ```js -resolve(contents); +resolve(pkg); //=> Conditions: ["default", "import", "node"] -resolve(contents, { unsafe: true }); +resolve(pkg, { unsafe: true }); //=> Conditions: ["default"] -resolve(contents, { unsafe: true, require: true, browser: true }); +resolve(pkg, { unsafe: true, require: true, browser: true }); //=> Conditions: ["default"] ``` @@ -241,12 +367,13 @@ resolve(contents, { //=> Conditions: ["default", "browser", "require", "custom123"] ``` +## Legacy Resolver + +Also included is a "legacy" method for resolving non-`"exports"` package fields. This may be used as a fallback method when for when no `"exports"` mapping is defined. In other words, it's completely optional (and tree-shakeable). ### legacy(pkg, options?) Returns: `string` or `undefined` -Also included is a "legacy" method for resolving non-`"exports"` package fields. This may be used as a fallback method when for when no `"exports"` mapping is defined. In other words, it's completely optional (and tree-shakeable). - You may customize the field priority via [`options.fields`](#optionsfields). When a field is found, its value is returned _as written_.
@@ -278,36 +405,39 @@ A list of fields to accept. The order of the array determines the priority/impor By default, the `legacy()` method will accept any `"module"` and/or "main" fields if they are defined. However, if both fields are defined, then "module" will be returned. ```js -const contents = { +import { legacy } from 'resolve.exports'; + +// package.json +const pkg = { "name": "...", "worker": "worker.js", "module": "module.mjs", "browser": "browser.js", "main": "main.js", -} +}; -legacy(contents); +legacy(pkg); // fields = [module, main] //=> "module.mjs" -legacy(contents, { browser: true }); +legacy(pkg, { browser: true }); // fields = [browser, module, main] //=> "browser.mjs" -legacy(contents, { +legacy(pkg, { fields: ['missing', 'worker', 'module', 'main'] }); // fields = [missing, worker, module, main] //=> "worker.js" -legacy(contents, { +legacy(pkg, { fields: ['missing', 'worker', 'module', 'main'], browser: true, }); // fields = [browser, missing, worker, module, main] //=> "browser.js" -legacy(contents, { +legacy(pkg, { fields: ['module', 'browser', 'main'], browser: true, }); From 5d7a61393979184ca3ce56325ca70ade291c4f87 Mon Sep 17 00:00:00 2001 From: Luke Edwards Date: Sun, 15 Jan 2023 08:07:22 -0800 Subject: [PATCH 26/35] chore: fix typos --- readme.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/readme.md b/readme.md index 823b3d0..c8abb59 100644 --- a/readme.md +++ b/readme.md @@ -118,7 +118,7 @@ resolve.imports(pkg, 'foobar/#hash'); // conditions: ["default"] resolve.imports(pkg, '#hash', { unsafe: true }); resolve.imports(pkg, 'foobar/#hash'); -//=> "./hash/web.mjs" +//=> "./hash/detect.mjs" resolve.imports(pkg, '#hello/world'); resolve.imports(pkg, 'foobar/#hello/world'); @@ -143,9 +143,9 @@ The [`resolve()`](#resolvepkg-entry-options), [`exports()`](#exportspkg-entry-op ```ts type Output = string[] | string | undefined; -export function resolve(pkg: Pacakge, entry?: string, options?: Options): Output; -export function exports(pkg: Pacakge, entry?: string, options?: Options): Output; -export function imports(pkg: Pacakge, target: string, options?: Options): Output; +export function resolve(pkg: Package, entry?: string, options?: Options): Output; +export function exports(pkg: Package, entry?: string, options?: Options): Output; +export function imports(pkg: Package, target: string, options?: Options): Output; // ^ not optional! ``` @@ -341,26 +341,26 @@ Default: `false` When enabled, this option will ignore **all other options** except [`options.conditions`](#optionsconditions). This is because, when enabled, `options.unsafe` **does not** assume or provide any default conditions except the `"default"` condition. ```js -resolve(pkg); +resolve.exports(pkg, '.'); //=> Conditions: ["default", "import", "node"] -resolve(pkg, { unsafe: true }); +resolve.exports(pkg, '.', { unsafe: true }); //=> Conditions: ["default"] -resolve(pkg, { unsafe: true, require: true, browser: true }); +resolve.exports(pkg, '.', { unsafe: true, require: true, browser: true }); //=> Conditions: ["default"] ``` In other words, this means that trying to use `options.require` or `options.browser` alongside `options.unsafe` will have no effect. In order to enable these conditions, you must provide them manually into the `options.conditions` list: ```js -resolve(contents, { +resolve.exports(pkg, '.', { unsafe: true, conditions: ["require"] }); //=> Conditions: ["default", "require"] -resolve(contents, { +resolve.exports(pkg, '.', { unsafe: true, conditions: ["browser", "require", "custom123"] }); From 1390ab6c962056e2fda3340f65fda6445d4c9296 Mon Sep 17 00:00:00 2001 From: Luke Edwards Date: Sun, 15 Jan 2023 08:08:29 -0800 Subject: [PATCH 27/35] chore: pkg desc --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 45a0155..253d427 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "version": "1.1.1", "name": "resolve.exports", "repository": "lukeed/resolve.exports", - "description": "A tiny (813b), correct, general-purpose, and configurable \"exports\" resolver without file-system reliance", + "description": "A tiny (987b), correct, general-purpose, and configurable \"exports\" and \"imports\" resolver without file-system reliance", "module": "dist/index.mjs", "main": "dist/index.js", "types": "index.d.ts", From 168ba52c4b0fa267cbae3e98fe0036f3d7b47a5c Mon Sep 17 00:00:00 2001 From: Luke Edwards Date: Sun, 15 Jan 2023 08:08:39 -0800 Subject: [PATCH 28/35] 2.0.0-next.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 253d427..18a794b 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "1.1.1", + "version": "2.0.0-next.0", "name": "resolve.exports", "repository": "lukeed/resolve.exports", "description": "A tiny (987b), correct, general-purpose, and configurable \"exports\" and \"imports\" resolver without file-system reliance", From 282863d03c95a48820e526c5d5a6dceba2e95053 Mon Sep 17 00:00:00 2001 From: Luke Edwards Date: Sun, 15 Jan 2023 11:35:52 -0800 Subject: [PATCH 29/35] fix(types): loosen entry input types --- index.d.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/index.d.ts b/index.d.ts index aee3056..aaf16f4 100644 --- a/index.d.ts +++ b/index.d.ts @@ -27,11 +27,8 @@ export type Options = { } export function resolve(pkg: T, entry?: string, options?: Options): Imports.Output | Exports.Output | void; - -type WithName = `${string}/${T}`; - -export function imports(pkg: T, entry: Imports.Entry|WithName, options?: Options): Imports.Output | void; -export function exports(pkg: T, target: Exports.Entry|WithName, options?: Options): Exports.Output | void; +export function imports(pkg: T, entry?: string, options?: Options): Imports.Output | void; +export function exports(pkg: T, target: string, options?: Options): Exports.Output | void; export function legacy(pkg: T, options: { browser: true, fields?: readonly string[] }): Browser | void; export function legacy(pkg: T, options: { browser: string, fields?: readonly string[] }): string | false | void; From 1bbfd69687bdafaa92bb6758633050ccdcc3a6b6 Mon Sep 17 00:00:00 2001 From: Luke Edwards Date: Mon, 16 Jan 2023 12:45:14 -0800 Subject: [PATCH 30/35] chore: update readme --- readme.md | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/readme.md b/readme.md index c8abb59..b735379 100644 --- a/readme.md +++ b/readme.md @@ -279,21 +279,19 @@ Collectively, the `options` are used to assemble a list of [conditions](https:// Type: `boolean`
Default: `false` -When truthy, the `"require"` field is added to the list of allowed/known conditions. - -When falsey, the `"import"` field is added to the list of allowed/known conditions instead. +When truthy, the `"require"` field is added to the list of allowed/known conditions. Otherwise the `"import"` field is added instead. #### options.browser Type: `boolean`
Default: `false` -When truthy, the `"browser"` field is added to the list of allowed/known conditions. +When truthy, the `"browser"` field is added to the list of allowed/known conditions. Otherwise the `"node"` field is added instead. #### options.conditions Type: `string[]`
Default: `[]` -Provide a list of additional/custom conditions that should be accepted when seen. +A list of additional/custom conditions that should be accepted when seen. > **Important:** The order specified within `options.conditions` does not matter.
The matching order/priority is **always** determined by the `"exports"` map's key order. @@ -303,33 +301,42 @@ For example, you may choose to accept a `"production"` condition in certain envi const pkg = { // package.json ... "exports": { - "worker": "./index.worker.js", - "require": "./index.require.js", - "production": "./index.prod.js", - "import": "./index.import.mjs", + "worker": "./$worker.js", + "require": "./$require.js", + "production": "./$production.js", + "import": "./$import.mjs", } }; resolve.exports(pkg, '.'); -//=> "./index.import.mjs" +// Conditions: ["default", "import", "node"] +//=> "./$import.mjs" resolve.exports(pkg, '.', { conditions: ['production'] -}); //=> "./index.prod.js" +}); +// Conditions: ["default", "production", "import", "node"] +//=> "./$production.js" resolve.exports(pkg, '.', { conditions: ['production'], require: true, -}); //=> "./index.require.js" +}); +// Conditions: ["default", "production", "require", "node"] +//=> "./$require.js" resolve.exports(pkg, '.', { conditions: ['production', 'worker'], require: true, -}); //=> "./index.worker.js" +}); +// Conditions: ["default", "production", "worker", "require", "node"] +//=> "./$worker.js" resolve.exports(pkg, '.', { conditions: ['production', 'worker'] -}); //=> "./index.worker.js" +}); +// Conditions: ["default", "production", "worker", "import", "node"] +//=> "./$worker.js" ``` #### options.unsafe From 2067a5e94b567c3d3c1d5a998d9e8c8e44487763 Mon Sep 17 00:00:00 2001 From: Luke Edwards Date: Mon, 16 Jan 2023 12:46:00 -0800 Subject: [PATCH 31/35] chore: readme spacing --- readme.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/readme.md b/readme.md index b735379..431184b 100644 --- a/readme.md +++ b/readme.md @@ -279,13 +279,15 @@ Collectively, the `options` are used to assemble a list of [conditions](https:// Type: `boolean`
Default: `false` -When truthy, the `"require"` field is added to the list of allowed/known conditions. Otherwise the `"import"` field is added instead. +When truthy, the `"require"` field is added to the list of allowed/known conditions.
+Otherwise the `"import"` field is added instead. #### options.browser Type: `boolean`
Default: `false` -When truthy, the `"browser"` field is added to the list of allowed/known conditions. Otherwise the `"node"` field is added instead. +When truthy, the `"browser"` field is added to the list of allowed/known conditions.
+Otherwise the `"node"` field is added instead. #### options.conditions Type: `string[]`
From e75e741d2bfb5a3e23c5f09500d38d4c14e42bef Mon Sep 17 00:00:00 2001 From: Luke Edwards Date: Mon, 16 Jan 2023 13:40:40 -0800 Subject: [PATCH 32/35] break: only return `string[]` or `undefined` outputs --- index.d.ts | 4 ++-- readme.md | 41 ++++++++++++++++++++--------------------- src/index.ts | 8 +++----- src/utils.ts | 25 +++++++++++-------------- test/index.ts | 24 +++++++++++++++--------- test/utils.ts | 27 ++++++++++++++++----------- 6 files changed, 67 insertions(+), 62 deletions(-) diff --git a/index.d.ts b/index.d.ts index aaf16f4..269dfb0 100644 --- a/index.d.ts +++ b/index.d.ts @@ -64,7 +64,7 @@ export namespace Imports { } | Value[]; - export type Output = Array | External | Path; + export type Output = Array; } export type Exports = Path | { @@ -81,7 +81,7 @@ export namespace Exports { [c: Condition]: Value; } | Value[]; - export type Output = Path[] | Path; + export type Output = Path[]; } export type Package = { diff --git a/readme.md b/readme.md index 431184b..d5a166a 100644 --- a/readme.md +++ b/readme.md @@ -62,18 +62,18 @@ const pkg = { resolve.exports(pkg); resolve.exports(pkg, '.'); resolve.exports(pkg, 'foobar'); -//=> "./dist/module.mjs" +//=> ["./dist/module.mjs"] // entry: "foobar/lite" === "./lite" // conditions: ["default", "import", "node"] resolve.exports(pkg, 'foobar/lite'); resolve.exports(pkg, './lite'); -//=> "./lite/module.mjs" +//=> ["./lite/module.mjs"] // Enable `require` condition // conditions: ["default", "require", "node"] -resolve.exports(pkg, 'foobar', { require: true }); //=> "./dist/require.js" -resolve.exports(pkg, './lite', { require: true }); //=> "./lite/require.js" +resolve.exports(pkg, 'foobar', { require: true }); //=> ["./dist/require.js"] +resolve.exports(pkg, './lite', { require: true }); //=> ["./lite/require.js"] // Throws "Missing specifier in package" Error resolve.exports(pkg, 'foobar/hello'); @@ -83,14 +83,14 @@ resolve.exports(pkg, './hello/world'); // conditions: ["default", "worker", "import", "node"] resolve.exports(pkg, 'foobar/lite', { conditions: ['worker'] -}); //=> "./lite/worker.node.js" +}); //=> ["./lite/worker.node.js"] // Toggle "browser" condition // conditions: ["default", "worker", "import", "browser"] resolve.exports(pkg, 'foobar/lite', { conditions: ['worker'], browser: true -}); //=> "./lite/worker.browser.js" +}); //=> ["./lite/worker.browser.js"] // Disable non-"default" condition activate // NOTE: breaks from Node.js default behavior @@ -108,17 +108,17 @@ resolve.exports(pkg, 'foobar/lite', { // conditions: ["default", "import", "node"] resolve.imports(pkg, '#hash'); resolve.imports(pkg, 'foobar/#hash'); -//=> "./hash/node.mjs" +//=> ["./hash/node.mjs"] // conditions: ["default", "import", "browser"] resolve.imports(pkg, '#hash', { browser: true }); resolve.imports(pkg, 'foobar/#hash'); -//=> "./hash/web.mjs" +//=> ["./hash/web.mjs"] // conditions: ["default"] resolve.imports(pkg, '#hash', { unsafe: true }); resolve.imports(pkg, 'foobar/#hash'); -//=> "./hash/detect.mjs" +//=> ["./hash/detect.mjs"] resolve.imports(pkg, '#hello/world'); resolve.imports(pkg, 'foobar/#hello/world'); @@ -142,10 +142,9 @@ resolve.legacy(pkg, { The [`resolve()`](#resolvepkg-entry-options), [`exports()`](#exportspkg-entry-options), and [`imports()`](#importspkg-target-options) functions share similar API signatures: ```ts -type Output = string[] | string | undefined; -export function resolve(pkg: Package, entry?: string, options?: Options): Output; -export function exports(pkg: Package, entry?: string, options?: Options): Output; -export function imports(pkg: Package, target: string, options?: Options): Output; +export function resolve(pkg: Package, entry?: string, options?: Options): string[] | undefined; +export function exports(pkg: Package, entry?: string, options?: Options): string[] | undefined; +export function imports(pkg: Package, target: string, options?: Options): string[] | undefined; // ^ not optional! ``` @@ -164,7 +163,7 @@ See below for further API descriptions. --- ### resolve(pkg, entry?, options?) -Returns: `string[]` or `string` or `undefined` +Returns: `string[]` or `undefined` A convenience helper which automatically reroutes to [`exports()`](#exportspkg-entry-options) or [`imports()`](#importspkg-target-options) depending on the `entry` value. @@ -195,7 +194,7 @@ r.resolve(pkg, 'foobar/#hash/md5'); ``` ### exports(pkg, entry?, options?) -Returns: `string[]` or `string` or `undefined` +Returns: `string[]` or `undefined` Traverse the `"exports"` within the contents of a `package.json` file.
If the contents _does not_ contain an `"exports"` map, then `undefined` will be returned. @@ -241,7 +240,7 @@ Assume we have a module named "foobar" and whose `pkg` contains `"name": "foobar ### imports(pkg, target, options?) -Returns: `string[]` or `string` or `undefined` +Returns: `string[]` or `undefined` Traverse the `"imports"` within the contents of a `package.json` file.
If the contents _does not_ contain an `"imports"` map, then `undefined` will be returned. @@ -312,33 +311,33 @@ const pkg = { resolve.exports(pkg, '.'); // Conditions: ["default", "import", "node"] -//=> "./$import.mjs" +//=> ["./$import.mjs"] resolve.exports(pkg, '.', { conditions: ['production'] }); // Conditions: ["default", "production", "import", "node"] -//=> "./$production.js" +//=> ["./$production.js"] resolve.exports(pkg, '.', { conditions: ['production'], require: true, }); // Conditions: ["default", "production", "require", "node"] -//=> "./$require.js" +//=> ["./$require.js"] resolve.exports(pkg, '.', { conditions: ['production', 'worker'], require: true, }); // Conditions: ["default", "production", "worker", "require", "node"] -//=> "./$worker.js" +//=> ["./$worker.js"] resolve.exports(pkg, '.', { conditions: ['production', 'worker'] }); // Conditions: ["default", "production", "worker", "import", "node"] -//=> "./$worker.js" +//=> ["./$worker.js"] ``` #### options.unsafe diff --git a/src/index.ts b/src/index.ts index 6c7eedb..3bba45e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,9 +3,7 @@ import type * as t from 'resolve.exports'; export { legacy } from './legacy'; -type Output = string[] | string; - -export function exports(pkg: t.Package, input?: string, options?: t.Options): Output | void { +export function exports(pkg: t.Package, input?: string, options?: t.Options): string[] | void { let map = pkg.exports, k: string; @@ -22,11 +20,11 @@ export function exports(pkg: t.Package, input?: string, options?: t.Options): Ou } } -export function imports(pkg: t.Package, input: string, options?: t.Options): Output | void { +export function imports(pkg: t.Package, input: string, options?: t.Options): string[] | void { if (pkg.imports) return walk(pkg.name, pkg.imports, input, options); } -export function resolve(pkg: t.Package, input?: string, options?: t.Options): Output | void { +export function resolve(pkg: t.Package, input?: string, options?: t.Options): string[] | void { // let entry = input && input !== '.' // ? toEntry(pkg.name, input) // : '.'; diff --git a/src/utils.ts b/src/utils.ts index 13195e3..7165d98 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -19,7 +19,7 @@ export function conditions(options: t.Options): Set { return out; } -export function walk(name: string, mapping: Mapping, input: string, options?: t.Options): string[] | string { +export function walk(name: string, mapping: Mapping, input: string, options?: t.Options): string[] { let entry = toEntry(name, input); let c = conditions(options || {}); @@ -72,18 +72,16 @@ export function walk(name: string, mapping: Mapping, input: string, options?: t. return (exact || !replace) ? v : injects(v, replace); } -export function injects(item: string[]|string, value: string): string[]|string { - let bool = Array.isArray(item); - let arr: string[] = bool ? item as string[] : [item as string]; - let i=0, len=arr.length, rgx=/[*]/g, tmp: string; +export function injects(items: string[], value: string): string[] { + let i=0, len=items.length, rgx=/[*]/g, tmp: string; for (; i < len; i++) { - arr[i] = rgx.test(tmp = arr[i]) + items[i] = rgx.test(tmp = items[i]) ? tmp.replace(rgx, value) : (tmp+value); } - return bool ? arr : arr[0]; + return items; } /** @@ -108,26 +106,25 @@ export function toEntry(name: string, ident: string, externals?: boolean): Entry : output as string | t.Exports.Entry; } -export function loop(m: Value, keys: Set, result?: Set): string[] | string | void { +export function loop(m: Value, keys: Set, result?: Set): string[] | void { if (m) { if (typeof m === 'string') { - return m; + if (result) result.add(m); + return [m]; } let idx: number | string, - arr: Set, - tmp: string[] | string | void; + arr: Set; if (Array.isArray(m)) { arr = result || new Set; for (idx=0; idx < m.length; idx++) { - tmp = loop(m[idx], keys, arr); - if (tmp) arr.add(tmp as string); + loop(m[idx], keys, arr); } - // TODO: send string if len=1? + // return if initialized set if (!result && arr.size) { return [...arr]; } diff --git a/test/index.ts b/test/index.ts index f0056af..720f677 100644 --- a/test/index.ts +++ b/test/index.ts @@ -10,8 +10,14 @@ type Options = t.Options; function pass(pkg: Package, expects: string|string[], entry?: string, options?: Options) { let out = lib.resolve(pkg, entry, options); - if (Array.isArray(expects)) assert.equal(out, expects); - else assert.is(out, expects); + if (typeof expects === 'string') { + assert.ok(Array.isArray(out)); + assert.is(out[0], expects); + assert.is(out.length, 1); + } else { + // Array | null | undefined + assert.equal(out, expects); + } } function fail(pkg: Package, target: Entry, entry?: string, options?: Options) { @@ -54,10 +60,10 @@ describe('$.resolve', it => { }; let output = lib.resolve(pkg); - assert.is(output, './hello.mjs'); + assert.equal(output, ['./hello.mjs']); output = lib.resolve(pkg, '.'); - assert.is(output, './hello.mjs'); + assert.equal(output, ['./hello.mjs']); try { lib.resolve(pkg, './other'); @@ -77,10 +83,10 @@ describe('$.resolve', it => { }; let output = lib.resolve(pkg, '#foo'); - assert.is(output, './foo.mjs'); + assert.equal(output, ['./foo.mjs']); output = lib.resolve(pkg, 'foobar/#foo'); - assert.is(output, './foo.mjs'); + assert.equal(output, ['./foo.mjs']); try { lib.resolve(pkg, '#bar'); @@ -520,13 +526,13 @@ describe('$.exports', it => { }; let output = lib.exports(pkg); - assert.is(output, './hello.mjs'); + assert.equal(output, ['./hello.mjs']); output = lib.exports(pkg, '.'); - assert.is(output, './hello.mjs'); + assert.equal(output, ['./hello.mjs']); output = lib.exports(pkg, 'foobar'); - assert.is(output, './hello.mjs'); + assert.equal(output, ['./hello.mjs']); }); it('exports=string', () => { diff --git a/test/utils.ts b/test/utils.ts index 0098713..876bd26 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -193,33 +193,31 @@ describe('$.toEntry', it => { }); describe('$.injects', it => { - function run(input: T, value: string, expect: T) { + function run(value: string, input: T, expect: T) { let output = $.injects(input, value); - if (Array.isArray(expect)) assert.equal(output, expect); - else assert.is(output, expect); + assert.equal(output, expect); } it('should be a function', () => { assert.type($.injects, 'function'); }); - it('should replace "*" character in string input', () => { - run('./foo*.jpg', 'bar', './foobar.jpg'); + it('should replace "*" character', () => { + run('bar', ['./foo*.jpg'], ['./foobar.jpg']); }); it('should replace multiple "*" characters w/ same value', () => { - run('./*/foo-*.jpg', 'bar', './bar/foo-bar.jpg'); + run('bar', ['./*/foo-*.jpg'], ['./bar/foo-bar.jpg']); }); // for the "./features/" => "./src/features/" scenario it('should append `value` if missing "*" character', () => { - run('./src/features/', 'app.js', './src/features/app.js'); + run('app.js', ['./src/features/'], ['./src/features/app.js']); }); - it('should accept string[] input', () => { - run( + it('should handle mixed array input', () => { + run('xyz', ['./foo/', './esm/*.mjs', './build/*/index-*.js'], - 'xyz', ['./foo/xyz', './esm/xyz.mjs', './build/xyz/index-xyz.js'], ); }); @@ -232,7 +230,14 @@ describe('$.loop', it => { type Expect = string | string[] | null | undefined; function run(expect: Expect, map: t.Exports.Value, conditions?: string[]) { let output = $.loop(map, new Set([ 'default', ...conditions||[] ])); - assert.equal(output, expect); + if (typeof expect == 'string') { + assert.ok(Array.isArray(output)); + assert.is(output[0], expect); + assert.is(output.length, 1); + } else { + // Array, null, undefined + assert.equal(output, expect); + } } it('should be a function', () => { From 584334ac1b8823dea7557ec2de15743da13d0ac4 Mon Sep 17 00:00:00 2001 From: Luke Edwards Date: Mon, 16 Jan 2023 13:41:10 -0800 Subject: [PATCH 33/35] chore: update module size --- package.json | 2 +- readme.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 18a794b..6ebcce5 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "version": "2.0.0-next.0", "name": "resolve.exports", "repository": "lukeed/resolve.exports", - "description": "A tiny (987b), correct, general-purpose, and configurable \"exports\" and \"imports\" resolver without file-system reliance", + "description": "A tiny (970b), correct, general-purpose, and configurable \"exports\" and \"imports\" resolver without file-system reliance", "module": "dist/index.mjs", "main": "dist/index.js", "types": "index.d.ts", diff --git a/readme.md b/readme.md index d5a166a..14ec553 100644 --- a/readme.md +++ b/readme.md @@ -1,6 +1,6 @@ # resolve.exports [![CI](https://github.com/lukeed/resolve.exports/workflows/CI/badge.svg)](https://github.com/lukeed/resolve.exports/actions) [![codecov](https://codecov.io/gh/lukeed/resolve.exports/branch/master/graph/badge.svg?token=4P7d4Omw2h)](https://codecov.io/gh/lukeed/resolve.exports) -> A tiny (987b), correct, general-purpose, and configurable `"exports"` and `"imports"` resolver without file-system reliance +> A tiny (970b), correct, general-purpose, and configurable `"exports"` and `"imports"` resolver without file-system reliance ***Why?*** From a9255fd4c73dff99088db740f93aab63a5379a27 Mon Sep 17 00:00:00 2001 From: Luke Edwards Date: Mon, 16 Jan 2023 13:48:27 -0800 Subject: [PATCH 34/35] chore: shave 11b --- package.json | 2 +- readme.md | 2 +- src/utils.ts | 19 +++++++++---------- test/utils.ts | 3 ++- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index 6ebcce5..f5a8df9 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "version": "2.0.0-next.0", "name": "resolve.exports", "repository": "lukeed/resolve.exports", - "description": "A tiny (970b), correct, general-purpose, and configurable \"exports\" and \"imports\" resolver without file-system reliance", + "description": "A tiny (959b), correct, general-purpose, and configurable \"exports\" and \"imports\" resolver without file-system reliance", "module": "dist/index.mjs", "main": "dist/index.js", "types": "index.d.ts", diff --git a/readme.md b/readme.md index 14ec553..ac7533a 100644 --- a/readme.md +++ b/readme.md @@ -1,6 +1,6 @@ # resolve.exports [![CI](https://github.com/lukeed/resolve.exports/workflows/CI/badge.svg)](https://github.com/lukeed/resolve.exports/actions) [![codecov](https://codecov.io/gh/lukeed/resolve.exports/branch/master/graph/badge.svg?token=4P7d4Omw2h)](https://codecov.io/gh/lukeed/resolve.exports) -> A tiny (970b), correct, general-purpose, and configurable `"exports"` and `"imports"` resolver without file-system reliance +> A tiny (959b), correct, general-purpose, and configurable `"exports"` and `"imports"` resolver without file-system reliance ***Why?*** diff --git a/src/utils.ts b/src/utils.ts index 7165d98..36897dc 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -23,11 +23,10 @@ export function walk(name: string, mapping: Mapping, input: string, options?: t. let entry = toEntry(name, input); let c = conditions(options || {}); - let m: Value | undefined = mapping[entry]; - let replace: string | undefined; - let exact = m !== void 0; + let m: Value|void = mapping[entry]; + let v: string[]|void, replace: string|void; - if (!exact) { + if (m === void 0) { // loop for longest key match let match: RegExpExecArray|null; let longest: Entry|undefined; @@ -41,7 +40,6 @@ export function walk(name: string, mapping: Mapping, input: string, options?: t. replace = entry.substring(key.length); longest = key; } else if (key.length > 1) { - // TODO: RegExp().exec everything? tmp = key.indexOf('*', 2); if (!!~tmp) { @@ -65,14 +63,17 @@ export function walk(name: string, mapping: Mapping, input: string, options?: t. throws(name, entry); } - let v = loop(m, c); + v = loop(m, c); + // unknown condition(s) if (!v) throws(name, entry, 1); + if (replace) injects(v, replace); - return (exact || !replace) ? v : injects(v, replace); + return v; } -export function injects(items: string[], value: string): string[] { +/** @note: mutates! */ +export function injects(items: string[], value: string): void { let i=0, len=items.length, rgx=/[*]/g, tmp: string; for (; i < len; i++) { @@ -80,8 +81,6 @@ export function injects(items: string[], value: string): string[] { ? tmp.replace(rgx, value) : (tmp+value); } - - return items; } /** diff --git a/test/utils.ts b/test/utils.ts index 876bd26..72f356d 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -195,7 +195,8 @@ describe('$.toEntry', it => { describe('$.injects', it => { function run(value: string, input: T, expect: T) { let output = $.injects(input, value); - assert.equal(output, expect); + assert.is(output, undefined); + assert.equal(input, expect); } it('should be a function', () => { From 23b6e2da26e9312a34462aede435816c7c739bb2 Mon Sep 17 00:00:00 2001 From: Luke Edwards Date: Mon, 16 Jan 2023 14:00:51 -0800 Subject: [PATCH 35/35] chore: add tests for clarity & shave 7b --- package.json | 2 +- readme.md | 2 +- src/index.ts | 7 ++++--- test/index.ts | 36 ++++++++++++++++++++++++++++++++++++ 4 files changed, 42 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index f5a8df9..8fdacf9 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "version": "2.0.0-next.0", "name": "resolve.exports", "repository": "lukeed/resolve.exports", - "description": "A tiny (959b), correct, general-purpose, and configurable \"exports\" and \"imports\" resolver without file-system reliance", + "description": "A tiny (952b), correct, general-purpose, and configurable \"exports\" and \"imports\" resolver without file-system reliance", "module": "dist/index.mjs", "main": "dist/index.js", "types": "index.d.ts", diff --git a/readme.md b/readme.md index ac7533a..c00b33b 100644 --- a/readme.md +++ b/readme.md @@ -1,6 +1,6 @@ # resolve.exports [![CI](https://github.com/lukeed/resolve.exports/workflows/CI/badge.svg)](https://github.com/lukeed/resolve.exports/actions) [![codecov](https://codecov.io/gh/lukeed/resolve.exports/branch/master/graph/badge.svg?token=4P7d4Omw2h)](https://codecov.io/gh/lukeed/resolve.exports) -> A tiny (959b), correct, general-purpose, and configurable `"exports"` and `"imports"` resolver without file-system reliance +> A tiny (952b), correct, general-purpose, and configurable `"exports"` and `"imports"` resolver without file-system reliance ***Why?*** diff --git a/src/index.ts b/src/index.ts index 3bba45e..eec7440 100644 --- a/src/index.ts +++ b/src/index.ts @@ -28,7 +28,8 @@ export function resolve(pkg: t.Package, input?: string, options?: t.Options): st // let entry = input && input !== '.' // ? toEntry(pkg.name, input) // : '.'; - let entry = toEntry(pkg.name, input || '.'); - if (entry[0] === '#') return imports(pkg, entry, options); - if (entry[0] === '.') return exports(pkg, entry, options); + input = toEntry(pkg.name, input || '.'); + return input[0] === '#' + ? imports(pkg, input, options) + : exports(pkg, input, options); } diff --git a/test/index.ts b/test/index.ts index 720f677..e8f7f7b 100644 --- a/test/index.ts +++ b/test/index.ts @@ -96,6 +96,42 @@ describe('$.resolve', it => { assert.is((err as Error).message, `Missing "#bar" specifier in "foobar" package`); } }); + + it('should run `$.export` if given "external" identifier', () => { + let pkg: Package = { + "name": "foobar", + "exports": { + ".": "./foo.mjs" + } + }; + + try { + lib.resolve(pkg, 'external'); + assert.unreachable(); + } catch (err) { + assert.instance(err, Error); + // IMPORTANT: treats "external" as "./external" + assert.is((err as Error).message, `Missing "./external" specifier in "foobar" package`); + } + }); + + it('should run `$.export` if given "external/subpath" identifier', () => { + let pkg: Package = { + "name": "foobar", + "exports": { + ".": "./foo.mjs" + } + }; + + try { + lib.resolve(pkg, 'external/subpath'); + assert.unreachable(); + } catch (err) { + assert.instance(err, Error); + // IMPORTANT: treats "external/subpath" as "./external/subpath" + assert.is((err as Error).message, `Missing "./external/subpath" specifier in "foobar" package`); + } + }); }); describe('$.imports', it => {