mirror of
https://github.com/nodejs/node.git
synced 2025-04-29 22:40:57 +00:00

Previously we only had an internal assertion to ensure certain code is executed before any user-provided CJS is run. This patch adds another assertion for ESM. Note that this internal state is not updated during source text module execution via vm because to run any code via vm, some user JS code must have already been executed anyway. In addition this patch moves the states into internal/modules/helpers to avoid circular dependencies. Also moves toggling the states to true *right before* user code execution instead of after in case we are half-way in the execution when internals try to check them. PR-URL: https://github.com/nodejs/node/pull/51748 Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com> Reviewed-By: Geoffrey Booth <webadmin@geoffreybooth.com>
357 lines
11 KiB
JavaScript
357 lines
11 KiB
JavaScript
'use strict';
|
|
|
|
const {
|
|
ArrayPrototypeForEach,
|
|
ArrayPrototypeJoin,
|
|
ObjectDefineProperty,
|
|
ObjectPrototypeHasOwnProperty,
|
|
SafeMap,
|
|
SafeSet,
|
|
StringPrototypeCharCodeAt,
|
|
StringPrototypeIncludes,
|
|
StringPrototypeSlice,
|
|
StringPrototypeStartsWith,
|
|
} = primordials;
|
|
const {
|
|
ERR_INVALID_ARG_TYPE,
|
|
ERR_MANIFEST_DEPENDENCY_MISSING,
|
|
ERR_UNKNOWN_BUILTIN_MODULE,
|
|
} = require('internal/errors').codes;
|
|
const { BuiltinModule } = require('internal/bootstrap/realm');
|
|
|
|
const { validateString } = require('internal/validators');
|
|
const fs = require('fs'); // Import all of `fs` so that it can be monkey-patched.
|
|
const internalFS = require('internal/fs/utils');
|
|
const path = require('path');
|
|
const { pathToFileURL, fileURLToPath } = require('internal/url');
|
|
const assert = require('internal/assert');
|
|
|
|
const { getOptionValue } = require('internal/options');
|
|
const { setOwnProperty } = require('internal/util');
|
|
const { inspect } = require('internal/util/inspect');
|
|
|
|
const {
|
|
privateSymbols: {
|
|
require_private_symbol,
|
|
},
|
|
} = internalBinding('util');
|
|
const { canParse: URLCanParse } = internalBinding('url');
|
|
|
|
let debug = require('internal/util/debuglog').debuglog('module', (fn) => {
|
|
debug = fn;
|
|
});
|
|
|
|
/** @typedef {import('internal/modules/cjs/loader.js').Module} Module */
|
|
|
|
/**
|
|
* Cache for storing resolved real paths of modules.
|
|
* In order to minimize unnecessary lstat() calls, this cache is a list of known-real paths.
|
|
* Set to an empty Map to reset.
|
|
* @type {Map<string, string>}
|
|
*/
|
|
const realpathCache = new SafeMap();
|
|
/**
|
|
* Resolves the path of a given `require` specifier, following symlinks.
|
|
* @param {string} requestPath The `require` specifier
|
|
*/
|
|
function toRealPath(requestPath) {
|
|
return fs.realpathSync(requestPath, {
|
|
[internalFS.realpathCacheKey]: realpathCache,
|
|
});
|
|
}
|
|
|
|
/** @type {Set<string>} */
|
|
let cjsConditions;
|
|
/**
|
|
* Define the conditions that apply to the CommonJS loader.
|
|
*/
|
|
function initializeCjsConditions() {
|
|
const userConditions = getOptionValue('--conditions');
|
|
const noAddons = getOptionValue('--no-addons');
|
|
const addonConditions = noAddons ? [] : ['node-addons'];
|
|
// TODO: Use this set when resolving pkg#exports conditions in loader.js.
|
|
cjsConditions = new SafeSet([
|
|
'require',
|
|
'node',
|
|
...addonConditions,
|
|
...userConditions,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Get the conditions that apply to the CommonJS loader.
|
|
*/
|
|
function getCjsConditions() {
|
|
if (cjsConditions === undefined) {
|
|
initializeCjsConditions();
|
|
}
|
|
return cjsConditions;
|
|
}
|
|
|
|
/**
|
|
* Provide one of Node.js' public modules to user code.
|
|
* @param {string} id - The identifier/specifier of the builtin module to load
|
|
* @param {string} request - The module requiring or importing the builtin module
|
|
*/
|
|
function loadBuiltinModule(id, request) {
|
|
if (!BuiltinModule.canBeRequiredByUsers(id)) {
|
|
return;
|
|
}
|
|
/** @type {import('internal/bootstrap/realm.js').BuiltinModule} */
|
|
const mod = BuiltinModule.map.get(id);
|
|
debug('load built-in module %s', request);
|
|
// compileForPublicLoader() throws if canBeRequiredByUsers is false:
|
|
mod.compileForPublicLoader();
|
|
return mod;
|
|
}
|
|
|
|
/** @type {Module} */
|
|
let $Module = null;
|
|
/**
|
|
* Import the Module class on first use.
|
|
*/
|
|
function lazyModule() {
|
|
$Module = $Module || require('internal/modules/cjs/loader').Module;
|
|
return $Module;
|
|
}
|
|
|
|
/**
|
|
* Invoke with `makeRequireFunction(module)` where `module` is the `Module` object to use as the context for the
|
|
* `require()` function.
|
|
* Use redirects to set up a mapping from a policy and restrict dependencies.
|
|
*/
|
|
const urlToFileCache = new SafeMap();
|
|
/**
|
|
* Create the module-scoped `require` function to pass into CommonJS modules.
|
|
* @param {Module} mod - The module to create the `require` function for.
|
|
* @param {ReturnType<import('internal/policy/manifest.js').Manifest['getDependencyMapper']>} redirects
|
|
* @typedef {(specifier: string) => unknown} RequireFunction
|
|
*/
|
|
function makeRequireFunction(mod, redirects) {
|
|
// lazy due to cycle
|
|
const Module = lazyModule();
|
|
if (mod instanceof Module !== true) {
|
|
throw new ERR_INVALID_ARG_TYPE('mod', 'Module', mod);
|
|
}
|
|
|
|
/** @type {RequireFunction} */
|
|
let require;
|
|
if (redirects) {
|
|
const id = mod.filename || mod.id;
|
|
const conditions = getCjsConditions();
|
|
const { resolve, reaction } = redirects;
|
|
require = function require(specifier) {
|
|
let missing = true;
|
|
const destination = resolve(specifier, conditions);
|
|
if (destination === true) {
|
|
missing = false;
|
|
} else if (destination) {
|
|
const { href, protocol } = destination;
|
|
if (protocol === 'node:') {
|
|
const specifier = destination.pathname;
|
|
|
|
if (BuiltinModule.canBeRequiredByUsers(specifier)) {
|
|
const mod = loadBuiltinModule(specifier, href);
|
|
return mod.exports;
|
|
}
|
|
throw new ERR_UNKNOWN_BUILTIN_MODULE(specifier);
|
|
} else if (protocol === 'file:') {
|
|
let filepath = urlToFileCache.get(href);
|
|
if (!filepath) {
|
|
filepath = fileURLToPath(destination);
|
|
urlToFileCache.set(href, filepath);
|
|
}
|
|
return mod[require_private_symbol](mod, filepath);
|
|
}
|
|
}
|
|
if (missing) {
|
|
reaction(new ERR_MANIFEST_DEPENDENCY_MISSING(
|
|
id,
|
|
specifier,
|
|
ArrayPrototypeJoin([...conditions], ', '),
|
|
));
|
|
}
|
|
return mod[require_private_symbol](mod, specifier);
|
|
};
|
|
} else {
|
|
require = function require(path) {
|
|
// When no policy manifest, the original prototype.require is sustained
|
|
return mod.require(path);
|
|
};
|
|
}
|
|
|
|
/**
|
|
* The `resolve` method that gets attached to module-scope `require`.
|
|
* @param {string} request
|
|
* @param {Parameters<Module['_resolveFilename']>[3]} options
|
|
*/
|
|
function resolve(request, options) {
|
|
validateString(request, 'request');
|
|
return Module._resolveFilename(request, mod, false, options);
|
|
}
|
|
|
|
require.resolve = resolve;
|
|
|
|
/**
|
|
* The `paths` method that gets attached to module-scope `require`.
|
|
* @param {string} request
|
|
*/
|
|
function paths(request) {
|
|
validateString(request, 'request');
|
|
return Module._resolveLookupPaths(request, mod);
|
|
}
|
|
|
|
resolve.paths = paths;
|
|
|
|
setOwnProperty(require, 'main', process.mainModule);
|
|
|
|
// Enable support to add extra extension types.
|
|
require.extensions = Module._extensions;
|
|
|
|
require.cache = Module._cache;
|
|
|
|
return require;
|
|
}
|
|
|
|
/**
|
|
* Remove byte order marker. This catches EF BB BF (the UTF-8 BOM)
|
|
* because the buffer-to-string conversion in `fs.readFileSync()`
|
|
* translates it to FEFF, the UTF-16 BOM.
|
|
* @param {string} content
|
|
*/
|
|
function stripBOM(content) {
|
|
if (StringPrototypeCharCodeAt(content) === 0xFEFF) {
|
|
content = StringPrototypeSlice(content, 1);
|
|
}
|
|
return content;
|
|
}
|
|
|
|
/**
|
|
* Add built-in modules to a global or REPL scope object.
|
|
* @param {Record<string, unknown>} object - The object such as `globalThis` to add the built-in modules to.
|
|
* @param {string} dummyModuleName - The label representing the set of built-in modules to add.
|
|
*/
|
|
function addBuiltinLibsToObject(object, dummyModuleName) {
|
|
// Make built-in modules available directly (loaded lazily).
|
|
const Module = require('internal/modules/cjs/loader').Module;
|
|
const { builtinModules } = Module;
|
|
|
|
// To require built-in modules in user-land and ignore modules whose
|
|
// `canBeRequiredByUsers` is false. So we create a dummy module object and not
|
|
// use `require()` directly.
|
|
const dummyModule = new Module(dummyModuleName);
|
|
|
|
ArrayPrototypeForEach(builtinModules, (name) => {
|
|
// Neither add underscored modules, nor ones that contain slashes (e.g.,
|
|
// 'fs/promises') or ones that are already defined.
|
|
if (StringPrototypeStartsWith(name, '_') ||
|
|
StringPrototypeIncludes(name, '/') ||
|
|
ObjectPrototypeHasOwnProperty(object, name)) {
|
|
return;
|
|
}
|
|
// Goals of this mechanism are:
|
|
// - Lazy loading of built-in modules
|
|
// - Having all built-in modules available as non-enumerable properties
|
|
// - Allowing the user to re-assign these variables as if there were no
|
|
// pre-existing globals with the same name.
|
|
|
|
const setReal = (val) => {
|
|
// Deleting the property before re-assigning it disables the
|
|
// getter/setter mechanism.
|
|
delete object[name];
|
|
object[name] = val;
|
|
};
|
|
|
|
ObjectDefineProperty(object, name, {
|
|
__proto__: null,
|
|
get: () => {
|
|
const lib = dummyModule.require(name);
|
|
|
|
try {
|
|
// Override the current getter/setter and set up a new
|
|
// non-enumerable property.
|
|
ObjectDefineProperty(object, name, {
|
|
__proto__: null,
|
|
get: () => lib,
|
|
set: setReal,
|
|
configurable: true,
|
|
enumerable: false,
|
|
});
|
|
} catch {
|
|
// If the property is no longer configurable, ignore the error.
|
|
}
|
|
|
|
return lib;
|
|
},
|
|
set: setReal,
|
|
configurable: true,
|
|
enumerable: false,
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Normalize the referrer name as a URL.
|
|
* If it's a string containing an absolute path or a URL it's normalized as
|
|
* a URL string.
|
|
* Otherwise it's returned as undefined.
|
|
* @param {string | null | undefined} referrerName
|
|
* @returns {string | undefined}
|
|
*/
|
|
function normalizeReferrerURL(referrerName) {
|
|
if (referrerName === null || referrerName === undefined) {
|
|
return undefined;
|
|
}
|
|
|
|
if (typeof referrerName === 'string') {
|
|
if (path.isAbsolute(referrerName)) {
|
|
return pathToFileURL(referrerName).href;
|
|
}
|
|
|
|
if (StringPrototypeStartsWith(referrerName, 'file://') ||
|
|
URLCanParse(referrerName)) {
|
|
return referrerName;
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
assert.fail('Unreachable code reached by ' + inspect(referrerName));
|
|
}
|
|
|
|
|
|
// Whether we have started executing any user-provided CJS code.
|
|
// This is set right before we call the wrapped CJS code (not after,
|
|
// in case we are half-way in the execution when internals check this).
|
|
// Used for internal assertions.
|
|
let _hasStartedUserCJSExecution = false;
|
|
// Similar to _hasStartedUserCJSExecution but for ESM. This is set
|
|
// right before ESM evaluation in the default ESM loader. We do not
|
|
// update this during vm SourceTextModule execution because at that point
|
|
// some user code must already have been run to execute code via vm
|
|
// there is little value checking whether any user JS code is run anyway.
|
|
let _hasStartedUserESMExecution = false;
|
|
|
|
module.exports = {
|
|
addBuiltinLibsToObject,
|
|
getCjsConditions,
|
|
initializeCjsConditions,
|
|
loadBuiltinModule,
|
|
makeRequireFunction,
|
|
normalizeReferrerURL,
|
|
stripBOM,
|
|
toRealPath,
|
|
hasStartedUserCJSExecution() {
|
|
return _hasStartedUserCJSExecution;
|
|
},
|
|
setHasStartedUserCJSExecution() {
|
|
_hasStartedUserCJSExecution = true;
|
|
},
|
|
hasStartedUserESMExecution() {
|
|
return _hasStartedUserESMExecution;
|
|
},
|
|
setHasStartedUserESMExecution() {
|
|
_hasStartedUserESMExecution = true;
|
|
},
|
|
};
|