From c39da34550162b50dfb1cc9a23f279bf6a26fa58 Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Sun, 24 Jul 2022 21:43:28 +0200 Subject: [PATCH 1/2] module: add support for `nodeEntryPointConfig.loaders` in `package.json` --- doc/api/packages.md | 28 +++++++++++++++++++ lib/internal/modules/cjs/loader.js | 2 ++ lib/internal/modules/esm/resolve.js | 6 ++-- lib/internal/modules/run_main.js | 17 +++++++++++ test/es-module/test-esm-loader-chaining.mjs | 24 ++++++++++++++++ .../entryPoint | 3 ++ .../extensionless-loader/index.js | 9 ++++++ .../extensionless-loader/package.json | 5 ++++ .../package.json | 6 ++++ .../entryPoint | 3 ++ .../load-extensionless-as-esm.mjs | 9 ++++++ .../package.json | 6 ++++ 12 files changed, 115 insertions(+), 3 deletions(-) create mode 100755 test/fixtures/es-module-loaders/package.json-loader-package-name/entryPoint create mode 100644 test/fixtures/es-module-loaders/package.json-loader-package-name/node_modules/extensionless-loader/index.js create mode 100644 test/fixtures/es-module-loaders/package.json-loader-package-name/node_modules/extensionless-loader/package.json create mode 100644 test/fixtures/es-module-loaders/package.json-loader-package-name/package.json create mode 100755 test/fixtures/es-module-loaders/package.json-loader-relative-url/entryPoint create mode 100644 test/fixtures/es-module-loaders/package.json-loader-relative-url/load-extensionless-as-esm.mjs create mode 100644 test/fixtures/es-module-loaders/package.json-loader-relative-url/package.json diff --git a/doc/api/packages.md b/doc/api/packages.md index 2dce76f8539c43..b8b8a68d0cc653 100644 --- a/doc/api/packages.md +++ b/doc/api/packages.md @@ -1315,10 +1315,36 @@ Package imports permit mapping to external packages. This field defines [subpath imports][] for the current package. +### `nodeEntryPointConfig` + + + +* Type: {Object} + +Additional configuration on how to start the Node.js processes inside a package +(does not apply if the entry point is using `.mjs` or `.cjs` file extension). + +#### `nodeEntryPointConfig.loaders` + + + +* Type: {string\[]} + +Specify the `module` of a custom experimental [ECMAScript module loader][]. +`module` may be any string accepted as an [`import` specifier][] resolved +relative to the `package.json` file. +This field is ignored if the [`--experimental-loader`][] CLI flag is used. + + [Babel]: https://babeljs.io/ [CommonJS]: modules.md [Conditional exports]: #conditional-exports [Corepack]: corepack.md +[ECMAScript module loader]: esm.md#loaders [ES module]: esm.md [ES modules]: esm.md [Node.js documentation for this section]: https://github.com/nodejs/node/blob/HEAD/doc/api/packages.md#conditions-definitions @@ -1329,9 +1355,11 @@ This field defines [subpath imports][] for the current package. [`"packageManager"`]: #packagemanager [`"type"`]: #type [`--conditions` / `-C` flag]: #resolving-user-conditions +[`--experimental-loader`]: cli.md#--experimental-loadermodule [`--no-addons` flag]: cli.md#--no-addons [`ERR_PACKAGE_PATH_NOT_EXPORTED`]: errors.md#err_package_path_not_exported [`esm`]: https://github.com/standard-things/esm#readme +[`import` specifier]: esm.md#import-specifiers [`package.json`]: #nodejs-packagejson-field-definitions [entry points]: #package-entry-points [folders as modules]: modules.md#folders-as-modules diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js index 711589894d5d19..7648940dd8b0f9 100644 --- a/lib/internal/modules/cjs/loader.js +++ b/lib/internal/modules/cjs/loader.js @@ -314,10 +314,12 @@ function readPackage(requestPath) { try { const parsed = JSONParse(json); const filtered = { + __proto__: null, name: parsed.name, main: parsed.main, exports: parsed.exports, imports: parsed.imports, + nodeEntryPointConfig: parsed.nodeEntryPointConfig, type: parsed.type }; packageJsonCache.set(jsonPath, filtered); diff --git a/lib/internal/modules/esm/resolve.js b/lib/internal/modules/esm/resolve.js index 435a6f279e9fd1..64a1ab0d77c570 100644 --- a/lib/internal/modules/esm/resolve.js +++ b/lib/internal/modules/esm/resolve.js @@ -1080,7 +1080,7 @@ function throwIfUnsupportedURLScheme(parsed, experimentalNetworkImports) { } } -async function defaultResolve(specifier, context = {}) { +function defaultResolve(specifier, context = {}) { let { parentURL, conditions } = context; if (parentURL && policy?.manifest) { const redirects = policy.manifest.getDependencyMapper(parentURL); @@ -1226,11 +1226,11 @@ const { if (policy) { const $defaultResolve = defaultResolve; - module.exports.defaultResolve = async function defaultResolve( + module.exports.defaultResolve = function defaultResolve( specifier, context ) { - const ret = await $defaultResolve(specifier, context); + const ret = $defaultResolve(specifier, context); // This is a preflight check to avoid data exfiltration by query params etc. policy.manifest.mightAllow(ret.url, () => new ERR_MANIFEST_DEPENDENCY_MISSING( diff --git a/lib/internal/modules/run_main.js b/lib/internal/modules/run_main.js index 5a50d5d6afab6e..b5fc35b58d4a2b 100644 --- a/lib/internal/modules/run_main.js +++ b/lib/internal/modules/run_main.js @@ -1,6 +1,8 @@ 'use strict'; const { + ArrayPrototypeMap, + ArrayPrototypePushApply, ObjectCreate, StringPrototypeEndsWith, } = primordials; @@ -11,6 +13,7 @@ const path = require('path'); const { handleProcessExit, } = require('internal/modules/esm/handle_process_exit'); +const { validateArray } = require('internal/validators'); function resolveMainPath(main) { // Note extension resolution for the main entry point can be deprecated in a @@ -45,6 +48,20 @@ function shouldUseESMLoader(mainPath) { if (!mainPath || StringPrototypeEndsWith(mainPath, '.cjs')) return false; const pkg = readPackageScope(mainPath); + if (typeof pkg.data.nodeEntryPointConfig === 'object') { + const { loaders } = pkg.data.nodeEntryPointConfig; + if (loaders != null) { + const { pathToFileURL } = require('internal/url'); + const { + defaultResolve, + DEFAULT_CONDITIONS, + } = require('internal/modules/esm/resolve'); + validateArray(loaders, 'nodeEntryPointConfig.loaders'); + const context = { parentURL: pathToFileURL(pkg.path).href + '/', conditions: DEFAULT_CONDITIONS }; + ArrayPrototypePushApply(userLoaders, ArrayPrototypeMap(loaders, (loaderSpecifier) => defaultResolve(loaderSpecifier, context).url)); + return true; + } + } return pkg && pkg.data.type === 'module'; } diff --git a/test/es-module/test-esm-loader-chaining.mjs b/test/es-module/test-esm-loader-chaining.mjs index f1ea13495ca5c4..db560395a00f41 100644 --- a/test/es-module/test-esm-loader-chaining.mjs +++ b/test/es-module/test-esm-loader-chaining.mjs @@ -431,3 +431,27 @@ const commonArgs = [ assert.match(stderr, /'load' hook's nextLoad\(\) context/); assert.strictEqual(status, 1); } + +{ + const { status } = spawnSync( + process.execPath, + [ + fixtures.path('es-module-loaders', 'package.json-loader-relative-url', 'entryPoint'), + ], + { encoding: 'utf8' }, + ); + + assert.strictEqual(status, 0); +} + +{ + const { status } = spawnSync( + process.execPath, + [ + fixtures.path('es-module-loaders', 'package.json-loader-package-name', 'entryPoint'), + ], + { encoding: 'utf8' }, + ); + + assert.strictEqual(status, 0); +} diff --git a/test/fixtures/es-module-loaders/package.json-loader-package-name/entryPoint b/test/fixtures/es-module-loaders/package.json-loader-package-name/entryPoint new file mode 100755 index 00000000000000..5e5724839216fa --- /dev/null +++ b/test/fixtures/es-module-loaders/package.json-loader-package-name/entryPoint @@ -0,0 +1,3 @@ +#!/usr/bin/env node + +export {}; diff --git a/test/fixtures/es-module-loaders/package.json-loader-package-name/node_modules/extensionless-loader/index.js b/test/fixtures/es-module-loaders/package.json-loader-package-name/node_modules/extensionless-loader/index.js new file mode 100644 index 00000000000000..612ec767eb9924 --- /dev/null +++ b/test/fixtures/es-module-loaders/package.json-loader-package-name/node_modules/extensionless-loader/index.js @@ -0,0 +1,9 @@ +const extensionlessPath = /\/[^.]+$/; + +export async function resolve(specifier, context, next) { + const n = await next(specifier.toString(), context); + if (n.format == null && extensionlessPath.test(n.url)) { + n.format = "module"; + } + return n; +} diff --git a/test/fixtures/es-module-loaders/package.json-loader-package-name/node_modules/extensionless-loader/package.json b/test/fixtures/es-module-loaders/package.json-loader-package-name/node_modules/extensionless-loader/package.json new file mode 100644 index 00000000000000..5b247cf028da60 --- /dev/null +++ b/test/fixtures/es-module-loaders/package.json-loader-package-name/node_modules/extensionless-loader/package.json @@ -0,0 +1,5 @@ +{ + "name":"extensioless-loader", + "type": "module", + "main": "./index.js" +} diff --git a/test/fixtures/es-module-loaders/package.json-loader-package-name/package.json b/test/fixtures/es-module-loaders/package.json-loader-package-name/package.json new file mode 100644 index 00000000000000..1209a1478500a3 --- /dev/null +++ b/test/fixtures/es-module-loaders/package.json-loader-package-name/package.json @@ -0,0 +1,6 @@ +{ + "name": "somePackage", + "nodeEntryPointConfig": { + "loaders": ["extensionless-loader"] + } +} diff --git a/test/fixtures/es-module-loaders/package.json-loader-relative-url/entryPoint b/test/fixtures/es-module-loaders/package.json-loader-relative-url/entryPoint new file mode 100755 index 00000000000000..5e5724839216fa --- /dev/null +++ b/test/fixtures/es-module-loaders/package.json-loader-relative-url/entryPoint @@ -0,0 +1,3 @@ +#!/usr/bin/env node + +export {}; diff --git a/test/fixtures/es-module-loaders/package.json-loader-relative-url/load-extensionless-as-esm.mjs b/test/fixtures/es-module-loaders/package.json-loader-relative-url/load-extensionless-as-esm.mjs new file mode 100644 index 00000000000000..612ec767eb9924 --- /dev/null +++ b/test/fixtures/es-module-loaders/package.json-loader-relative-url/load-extensionless-as-esm.mjs @@ -0,0 +1,9 @@ +const extensionlessPath = /\/[^.]+$/; + +export async function resolve(specifier, context, next) { + const n = await next(specifier.toString(), context); + if (n.format == null && extensionlessPath.test(n.url)) { + n.format = "module"; + } + return n; +} diff --git a/test/fixtures/es-module-loaders/package.json-loader-relative-url/package.json b/test/fixtures/es-module-loaders/package.json-loader-relative-url/package.json new file mode 100644 index 00000000000000..1cb6fbcc3bcf0f --- /dev/null +++ b/test/fixtures/es-module-loaders/package.json-loader-relative-url/package.json @@ -0,0 +1,6 @@ +{ + "name": "package.json-loader-relative-url", + "nodeEntryPointConfig": { + "loaders": ["./load-extensionless-as-esm.mjs"] + } +} From 87e8014d0d712bcc77e76c4ab00dd86fb2f20a11 Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Sun, 24 Jul 2022 22:12:40 +0200 Subject: [PATCH 2/2] fixup! module: add support for `nodeEntryPointConfig.loaders` in `package.json` --- lib/internal/modules/run_main.js | 6 ++++-- test/es-module/test-esm-loader-search.js | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/internal/modules/run_main.js b/lib/internal/modules/run_main.js index b5fc35b58d4a2b..82c8171c4b22ab 100644 --- a/lib/internal/modules/run_main.js +++ b/lib/internal/modules/run_main.js @@ -48,7 +48,7 @@ function shouldUseESMLoader(mainPath) { if (!mainPath || StringPrototypeEndsWith(mainPath, '.cjs')) return false; const pkg = readPackageScope(mainPath); - if (typeof pkg.data.nodeEntryPointConfig === 'object') { + if (typeof pkg.data?.nodeEntryPointConfig === 'object') { const { loaders } = pkg.data.nodeEntryPointConfig; if (loaders != null) { const { pathToFileURL } = require('internal/url'); @@ -58,7 +58,9 @@ function shouldUseESMLoader(mainPath) { } = require('internal/modules/esm/resolve'); validateArray(loaders, 'nodeEntryPointConfig.loaders'); const context = { parentURL: pathToFileURL(pkg.path).href + '/', conditions: DEFAULT_CONDITIONS }; - ArrayPrototypePushApply(userLoaders, ArrayPrototypeMap(loaders, (loaderSpecifier) => defaultResolve(loaderSpecifier, context).url)); + const resolvedLoaders = + ArrayPrototypeMap(loaders, (loaderSpecifier) => defaultResolve(loaderSpecifier, context).url); + ArrayPrototypePushApply(userLoaders, resolvedLoaders); return true; } } diff --git a/test/es-module/test-esm-loader-search.js b/test/es-module/test-esm-loader-search.js index 0440d3d7775cff..3c451409b356db 100644 --- a/test/es-module/test-esm-loader-search.js +++ b/test/es-module/test-esm-loader-search.js @@ -10,8 +10,8 @@ const { defaultResolve: resolve } = require('internal/modules/esm/resolve'); -assert.rejects( - resolve('target'), +assert.throws( + () => resolve('target'), { code: 'ERR_MODULE_NOT_FOUND', name: 'Error',