mirror of https://github.com/nodejs/node.git
module: implement "exports" proposal for CommonJS
Refs: https://github.com/jkrems/proposal-pkg-exports/issues/36 Refs: https://github.com/nodejs/node/pull/28568 PR-URL: https://github.com/nodejs/node/pull/28759 Reviewed-By: Guy Bedford <guybedford@gmail.com> Reviewed-By: Bradley Farias <bradley.meck@gmail.com>
This commit is contained in:
parent
4fc7cd9bc1
commit
dcb6929183
|
@ -1585,6 +1585,13 @@ compiled with ICU support.
|
|||
|
||||
A given value is out of the accepted range.
|
||||
|
||||
<a id="ERR_PATH_NOT_EXPORTED"></a>
|
||||
### ERR_PATH_NOT_EXPORTED
|
||||
|
||||
> Stability: 1 - Experimental
|
||||
|
||||
An attempt was made to load a protected path from a package using `exports`.
|
||||
|
||||
<a id="ERR_REQUIRE_ESM"></a>
|
||||
### ERR_REQUIRE_ESM
|
||||
|
||||
|
|
|
@ -202,6 +202,39 @@ NODE_MODULES_PATHS(START)
|
|||
5. return DIRS
|
||||
```
|
||||
|
||||
If `--experimental-exports` is enabled,
|
||||
node allows packages loaded via `LOAD_NODE_MODULES` to explicitly declare
|
||||
which filepaths to expose and how they should be interpreted.
|
||||
This expands on the control packages already had using the `main` field.
|
||||
With this feature enabled, the `LOAD_NODE_MODULES` changes as follows:
|
||||
|
||||
```txt
|
||||
LOAD_NODE_MODULES(X, START)
|
||||
1. let DIRS = NODE_MODULES_PATHS(START)
|
||||
2. for each DIR in DIRS:
|
||||
a. let FILE_PATH = RESOLVE_BARE_SPECIFIER(DIR, X)
|
||||
a. LOAD_AS_FILE(FILE_PATH)
|
||||
b. LOAD_AS_DIRECTORY(FILE_PATH)
|
||||
|
||||
RESOLVE_BARE_SPECIFIER(DIR, X)
|
||||
1. Try to interpret X as a combination of name and subpath where the name
|
||||
may have a @scope/ prefix and the subpath begins with a slash (`/`).
|
||||
2. If X matches this pattern and DIR/name/package.json is a file:
|
||||
a. Parse DIR/name/package.json, and look for "exports" field.
|
||||
b. If "exports" is null or undefined, GOTO 3.
|
||||
c. Find the longest key in "exports" that the subpath starts with.
|
||||
d. If no such key can be found, throw "not exported".
|
||||
e. If the key matches the subpath entirely, return DIR/name/${exports[key]}.
|
||||
f. If either the key or exports[key] do not end with a slash (`/`),
|
||||
throw "not exported".
|
||||
g. Return DIR/name/${exports[key]}${subpath.slice(key.length)}.
|
||||
3. return DIR/X
|
||||
```
|
||||
|
||||
`"exports"` is only honored when loading a package "name" as defined above. Any
|
||||
`"exports"` values within nested directories and packages must be declared by
|
||||
the `package.json` responsible for the "name".
|
||||
|
||||
## Caching
|
||||
|
||||
<!--type=misc-->
|
||||
|
|
|
@ -1098,6 +1098,8 @@ E('ERR_OUT_OF_RANGE',
|
|||
msg += ` It must be ${range}. Received ${received}`;
|
||||
return msg;
|
||||
}, RangeError);
|
||||
E('ERR_PATH_NOT_EXPORTED',
|
||||
'Package exports for \'%s\' do not define a \'%s\' subpath', Error);
|
||||
E('ERR_REQUIRE_ESM', 'Must use import to load ES Module: %s', Error);
|
||||
E('ERR_SCRIPT_EXECUTION_INTERRUPTED',
|
||||
'Script execution was interrupted by `SIGINT`', Error);
|
||||
|
|
|
@ -21,7 +21,13 @@
|
|||
|
||||
'use strict';
|
||||
|
||||
const { JSON, Object, Reflect } = primordials;
|
||||
const {
|
||||
JSON,
|
||||
Object,
|
||||
Reflect,
|
||||
SafeMap,
|
||||
StringPrototype,
|
||||
} = primordials;
|
||||
|
||||
const { NativeModule } = require('internal/bootstrap/loaders');
|
||||
const { pathToFileURL, fileURLToPath, URL } = require('internal/url');
|
||||
|
@ -53,10 +59,12 @@ const { compileFunction } = internalBinding('contextify');
|
|||
const {
|
||||
ERR_INVALID_ARG_VALUE,
|
||||
ERR_INVALID_OPT_VALUE,
|
||||
ERR_PATH_NOT_EXPORTED,
|
||||
ERR_REQUIRE_ESM
|
||||
} = require('internal/errors').codes;
|
||||
const { validateString } = require('internal/validators');
|
||||
const pendingDeprecation = getOptionValue('--pending-deprecation');
|
||||
const experimentalExports = getOptionValue('--experimental-exports');
|
||||
|
||||
module.exports = { wrapSafe, Module };
|
||||
|
||||
|
@ -182,12 +190,10 @@ Module._debug = deprecate(debug, 'Module._debug is deprecated.', 'DEP0077');
|
|||
|
||||
// Check if the directory is a package.json dir.
|
||||
const packageMainCache = Object.create(null);
|
||||
// Explicit exports from package.json files
|
||||
const packageExportsCache = new SafeMap();
|
||||
|
||||
function readPackage(requestPath) {
|
||||
const entry = packageMainCache[requestPath];
|
||||
if (entry)
|
||||
return entry;
|
||||
|
||||
function readPackageRaw(requestPath) {
|
||||
const jsonPath = path.resolve(requestPath, 'package.json');
|
||||
const json = internalModuleReadJSON(path.toNamespacedPath(jsonPath));
|
||||
|
||||
|
@ -201,7 +207,12 @@ function readPackage(requestPath) {
|
|||
}
|
||||
|
||||
try {
|
||||
return packageMainCache[requestPath] = JSON.parse(json).main;
|
||||
const parsed = JSON.parse(json);
|
||||
packageMainCache[requestPath] = parsed.main;
|
||||
if (experimentalExports) {
|
||||
packageExportsCache.set(requestPath, parsed.exports);
|
||||
}
|
||||
return parsed;
|
||||
} catch (e) {
|
||||
e.path = jsonPath;
|
||||
e.message = 'Error parsing ' + jsonPath + ': ' + e.message;
|
||||
|
@ -209,6 +220,31 @@ function readPackage(requestPath) {
|
|||
}
|
||||
}
|
||||
|
||||
function readPackage(requestPath) {
|
||||
const entry = packageMainCache[requestPath];
|
||||
if (entry)
|
||||
return entry;
|
||||
|
||||
const pkg = readPackageRaw(requestPath);
|
||||
if (pkg === false) return false;
|
||||
|
||||
return pkg.main;
|
||||
}
|
||||
|
||||
function readExports(requestPath) {
|
||||
if (packageExportsCache.has(requestPath)) {
|
||||
return packageExportsCache.get(requestPath);
|
||||
}
|
||||
|
||||
const pkg = readPackageRaw(requestPath);
|
||||
if (!pkg) {
|
||||
packageExportsCache.set(requestPath, null);
|
||||
return null;
|
||||
}
|
||||
|
||||
return pkg.exports;
|
||||
}
|
||||
|
||||
function tryPackage(requestPath, exts, isMain, originalPath) {
|
||||
const pkg = readPackage(requestPath);
|
||||
|
||||
|
@ -297,8 +333,59 @@ function findLongestRegisteredExtension(filename) {
|
|||
return '.js';
|
||||
}
|
||||
|
||||
// This only applies to requests of a specific form:
|
||||
// 1. name/.*
|
||||
// 2. @scope/name/.*
|
||||
const EXPORTS_PATTERN = /^((?:@[^./@\\][^/@\\]*\/)?[^@./\\][^/\\]*)(\/.*)$/;
|
||||
function resolveExports(nmPath, request, absoluteRequest) {
|
||||
// The implementation's behavior is meant to mirror resolution in ESM.
|
||||
if (experimentalExports && !absoluteRequest) {
|
||||
const [, name, expansion] =
|
||||
StringPrototype.match(request, EXPORTS_PATTERN) || [];
|
||||
if (!name) {
|
||||
return path.resolve(nmPath, request);
|
||||
}
|
||||
|
||||
const basePath = path.resolve(nmPath, name);
|
||||
const pkgExports = readExports(basePath);
|
||||
|
||||
if (pkgExports != null) {
|
||||
const mappingKey = `.${expansion}`;
|
||||
const mapping = pkgExports[mappingKey];
|
||||
if (typeof mapping === 'string') {
|
||||
return fileURLToPath(new URL(mapping, `${pathToFileURL(basePath)}/`));
|
||||
}
|
||||
|
||||
let dirMatch = '';
|
||||
for (const [candidateKey, candidateValue] of Object.entries(pkgExports)) {
|
||||
if (candidateKey[candidateKey.length - 1] !== '/') continue;
|
||||
if (candidateValue[candidateValue.length - 1] !== '/') continue;
|
||||
if (candidateKey.length > dirMatch.length &&
|
||||
StringPrototype.startsWith(mappingKey, candidateKey)) {
|
||||
dirMatch = candidateKey;
|
||||
}
|
||||
}
|
||||
|
||||
if (dirMatch !== '') {
|
||||
const dirMapping = pkgExports[dirMatch];
|
||||
const remainder = StringPrototype.slice(mappingKey, dirMatch.length);
|
||||
const expectedPrefix =
|
||||
new URL(dirMapping, `${pathToFileURL(basePath)}/`);
|
||||
const resolved = new URL(remainder, expectedPrefix).href;
|
||||
if (StringPrototype.startsWith(resolved, expectedPrefix.href)) {
|
||||
return fileURLToPath(resolved);
|
||||
}
|
||||
}
|
||||
throw new ERR_PATH_NOT_EXPORTED(basePath, mappingKey);
|
||||
}
|
||||
}
|
||||
|
||||
return path.resolve(nmPath, request);
|
||||
}
|
||||
|
||||
Module._findPath = function(request, paths, isMain) {
|
||||
if (path.isAbsolute(request)) {
|
||||
const absoluteRequest = path.isAbsolute(request);
|
||||
if (absoluteRequest) {
|
||||
paths = [''];
|
||||
} else if (!paths || paths.length === 0) {
|
||||
return false;
|
||||
|
@ -322,7 +409,7 @@ Module._findPath = function(request, paths, isMain) {
|
|||
// Don't search further if path doesn't exist
|
||||
const curPath = paths[i];
|
||||
if (curPath && stat(curPath) < 1) continue;
|
||||
var basePath = path.resolve(curPath, request);
|
||||
var basePath = resolveExports(curPath, request, absoluteRequest);
|
||||
var filename;
|
||||
|
||||
var rc = stat(basePath);
|
||||
|
|
|
@ -856,7 +856,7 @@ Maybe<URL> PackageExportsResolve(Environment* env,
|
|||
std::string msg = "Package exports for '" +
|
||||
URL(".", pjson_url).ToFilePath() + "' do not define a '" + pkg_subpath +
|
||||
"' subpath, imported from " + base.ToFilePath();
|
||||
node::THROW_ERR_MODULE_NOT_FOUND(env, msg.c_str());
|
||||
node::THROW_ERR_PATH_NOT_EXPORTED(env, msg.c_str());
|
||||
return Nothing<URL>();
|
||||
}
|
||||
|
||||
|
|
|
@ -53,6 +53,7 @@ void PrintErrorString(const char* format, ...);
|
|||
V(ERR_MISSING_PLATFORM_FOR_WORKER, Error) \
|
||||
V(ERR_MODULE_NOT_FOUND, Error) \
|
||||
V(ERR_OUT_OF_RANGE, RangeError) \
|
||||
V(ERR_PATH_NOT_EXPORTED, Error) \
|
||||
V(ERR_SCRIPT_EXECUTION_INTERRUPTED, Error) \
|
||||
V(ERR_SCRIPT_EXECUTION_TIMEOUT, Error) \
|
||||
V(ERR_STRING_TOO_LONG, Error) \
|
||||
|
|
|
@ -872,7 +872,9 @@ static void InternalModuleReadJSON(const FunctionCallbackInfo<Value>& args) {
|
|||
}
|
||||
|
||||
const size_t size = offset - start;
|
||||
if (size == 0 || size == SearchString(&chars[start], size, "\"main\"")) {
|
||||
if (size == 0 || (
|
||||
size == SearchString(&chars[start], size, "\"main\"") &&
|
||||
size == SearchString(&chars[start], size, "\"exports\""))) {
|
||||
return;
|
||||
} else {
|
||||
Local<String> chars_string =
|
||||
|
|
|
@ -319,6 +319,7 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
|
|||
"experimental ES Module support and caching modules",
|
||||
&EnvironmentOptions::experimental_modules,
|
||||
kAllowedInEnvironment);
|
||||
Implies("--experimental-modules", "--experimental-exports");
|
||||
AddOption("--experimental-wasm-modules",
|
||||
"experimental ES Module support for webassembly modules",
|
||||
&EnvironmentOptions::experimental_wasm_modules,
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
// Flags: --experimental-modules --experimental-exports
|
||||
// Flags: --experimental-modules
|
||||
|
||||
import { mustCall } from '../common/index.mjs';
|
||||
import { ok, strictEqual } from 'assert';
|
||||
|
||||
import { asdf, asdf2 } from '../fixtures/pkgexports.mjs';
|
||||
import { asdf, asdf2, space } from '../fixtures/pkgexports.mjs';
|
||||
import {
|
||||
loadMissing,
|
||||
loadFromNumber,
|
||||
|
@ -12,6 +12,7 @@ import {
|
|||
|
||||
strictEqual(asdf, 'asdf');
|
||||
strictEqual(asdf2, 'asdf');
|
||||
strictEqual(space, 'encoded path');
|
||||
|
||||
loadMissing().catch(mustCall((err) => {
|
||||
ok(err.message.toString().startsWith('Package exports'));
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
{
|
||||
"exports": {
|
||||
".": "./asdf.js",
|
||||
"./space": "./sp%20ce.js",
|
||||
"./asdf": "./asdf.js",
|
||||
"./valid-cjs": "./asdf.js",
|
||||
"./sub/": "./"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
'use strict';
|
||||
|
||||
module.exports = 'encoded path';
|
|
@ -1,2 +1,3 @@
|
|||
export { default as asdf } from 'pkgexports/asdf';
|
||||
export { default as asdf2 } from 'pkgexports/sub/asdf.js';
|
||||
export { default as space } from 'pkgexports/space';
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
// Flags: --experimental-exports
|
||||
'use strict';
|
||||
|
||||
require('../common');
|
||||
|
||||
const assert = require('assert');
|
||||
const { createRequire } = require('module');
|
||||
const path = require('path');
|
||||
|
||||
const fixtureRequire =
|
||||
createRequire(path.resolve(__dirname, '../fixtures/imaginary.js'));
|
||||
|
||||
assert.strictEqual(fixtureRequire('pkgexports/valid-cjs'), 'asdf');
|
||||
|
||||
assert.strictEqual(fixtureRequire('baz/index'), 'eye catcher');
|
||||
|
||||
assert.strictEqual(fixtureRequire('pkgexports/sub/asdf.js'), 'asdf');
|
||||
|
||||
assert.strictEqual(fixtureRequire('pkgexports/space'), 'encoded path');
|
||||
|
||||
assert.throws(
|
||||
() => fixtureRequire('pkgexports/not-a-known-entry'),
|
||||
(e) => {
|
||||
assert.strictEqual(e.code, 'ERR_PATH_NOT_EXPORTED');
|
||||
return true;
|
||||
});
|
||||
|
||||
assert.throws(
|
||||
() => fixtureRequire('pkgexports-number/hidden.js'),
|
||||
(e) => {
|
||||
assert.strictEqual(e.code, 'ERR_PATH_NOT_EXPORTED');
|
||||
return true;
|
||||
});
|
||||
|
||||
assert.throws(
|
||||
() => fixtureRequire('pkgexports/sub/not-a-file.js'),
|
||||
(e) => {
|
||||
assert.strictEqual(e.code, 'MODULE_NOT_FOUND');
|
||||
return true;
|
||||
});
|
||||
|
||||
assert.throws(
|
||||
() => fixtureRequire('pkgexports/sub/./../asdf.js'),
|
||||
(e) => {
|
||||
assert.strictEqual(e.code, 'ERR_PATH_NOT_EXPORTED');
|
||||
return true;
|
||||
});
|
Loading…
Reference in New Issue