deno/std/node/require.ts

1190 lines
35 KiB
TypeScript

// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
import * as path from "../path/mod.ts";
import { assert } from "../testing/asserts.ts";
const CHAR_FORWARD_SLASH = "/".charCodeAt(0);
const CHAR_BACKWARD_SLASH = "\\".charCodeAt(0);
const CHAR_COLON = ":".charCodeAt(0);
const isWindows = path.isWindows;
const relativeResolveCache = Object.create(null);
let requireDepth = 0;
let statCache = null;
type StatResult = -1 | 0 | 1;
// Returns 0 if the path refers to
// a file, 1 when it's a directory or < 0 on error.
function stat(filename: string): StatResult {
filename = path.toNamespacedPath(filename);
if (statCache !== null) {
const result = statCache.get(filename);
if (result !== undefined) return result;
}
try {
const info = Deno.statSync(filename);
const result = info.isFile() ? 0 : 1;
if (statCache !== null) statCache.set(filename, result);
return result;
} catch (e) {
return -1;
}
}
function updateChildren(parent: Module, child: Module, scan: boolean): void {
const children = parent && parent.children;
if (children && !(scan && children.includes(child))) {
children.push(child);
}
}
class Module {
id: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
exports: any;
parent?: Module;
filename: string;
loaded: boolean;
children: Module[];
paths: string[];
path: string;
constructor(id = "", parent?: Module) {
this.id = id;
this.exports = {};
this.parent = parent;
updateChildren(parent, this, false);
this.filename = null;
this.loaded = false;
this.children = [];
this.paths = [];
this.path = path.dirname(id);
}
// TODO: populate this with polyfills!
static builtinModules: Module[] = [];
static _extensions: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: (module: Module, filename: string) => any;
} = Object.create(null);
static _cache: { [key: string]: Module } = Object.create(null);
static _pathCache = Object.create(null);
static globalPaths: string[] = [];
// Proxy related code removed.
static wrapper = [
"(function (exports, require, module, __filename, __dirname) { ",
"\n});"
];
// Loads a module at the given file path. Returns that module's
// `exports` property.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
require(id: string): any {
if (id === "") {
throw new Error(`id '${id}' must be a non-empty string`);
}
requireDepth++;
try {
return Module._load(id, this, /* isMain */ false);
} finally {
requireDepth--;
}
}
// Given a file name, pass it to the proper extension handler.
load(filename: string): void {
assert(!this.loaded);
this.filename = filename;
this.paths = Module._nodeModulePaths(path.dirname(filename));
const extension = findLongestRegisteredExtension(filename);
// Removed ESM code
Module._extensions[extension](this, filename);
this.loaded = true;
// Removed ESM code
}
// Run the file contents in the correct scope or sandbox. Expose
// the correct helper variables (require, module, exports) to
// the file.
// Returns exception, if any.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
_compile(content: string, filename: string): any {
// manifest code removed
const compiledWrapper = wrapSafe(filename, content);
// inspector code remove
const dirname = path.dirname(filename);
const require = makeRequireFunction(this);
const exports = this.exports;
const thisValue = exports;
if (requireDepth === 0) {
statCache = new Map();
}
const result = compiledWrapper.call(
thisValue,
exports,
require,
this,
filename,
dirname
);
if (requireDepth === 0) {
statCache = null;
}
return result;
}
static _resolveLookupPaths(
request: string,
parent: Module | null
): string[] | null {
// Check for node modules paths.
if (
request.charAt(0) !== "." ||
(request.length > 1 &&
request.charAt(1) !== "." &&
request.charAt(1) !== "/" &&
(!isWindows || request.charAt(1) !== "\\"))
) {
let paths = modulePaths;
if (parent !== null && parent.paths && parent.paths.length) {
paths = parent.paths.concat(paths);
}
return paths.length > 0 ? paths : null;
}
// With --eval, parent.id is not set and parent.filename is null.
if (!parent || !parent.id || !parent.filename) {
// Make require('./path/to/foo') work - normally the path is taken
// from realpath(__filename) but with eval there is no filename
const mainPaths = ["."].concat(Module._nodeModulePaths("."), modulePaths);
return mainPaths;
}
const parentDir = [path.dirname(parent.filename)];
return parentDir;
}
static _resolveFilename(
request: string,
parent: Module,
isMain: boolean,
options?: { paths: string[] }
): string {
// Native module code removed
let paths: string[];
if (typeof options === "object" && options !== null) {
if (Array.isArray(options.paths)) {
const isRelative =
request.startsWith("./") ||
request.startsWith("../") ||
(isWindows && request.startsWith(".\\")) ||
request.startsWith("..\\");
if (isRelative) {
paths = options.paths;
} else {
const fakeParent = new Module("", null);
paths = [];
for (let i = 0; i < options.paths.length; i++) {
const path = options.paths[i];
fakeParent.paths = Module._nodeModulePaths(path);
const lookupPaths = Module._resolveLookupPaths(request, fakeParent);
for (let j = 0; j < lookupPaths.length; j++) {
if (!paths.includes(lookupPaths[j])) paths.push(lookupPaths[j]);
}
}
}
} else if (options.paths === undefined) {
paths = Module._resolveLookupPaths(request, parent);
} else {
throw new Error("options.paths is invalid");
}
} else {
paths = Module._resolveLookupPaths(request, parent);
}
// Look up the filename first, since that's the cache key.
const filename = Module._findPath(request, paths, isMain);
if (!filename) {
const requireStack = [];
for (let cursor = parent; cursor; cursor = cursor.parent) {
requireStack.push(cursor.filename || cursor.id);
}
let message = `Cannot find module '${request}'`;
if (requireStack.length > 0) {
message = message + "\nRequire stack:\n- " + requireStack.join("\n- ");
}
const err = new Error(message);
// @ts-ignore
err.code = "MODULE_NOT_FOUND";
// @ts-ignore
err.requireStack = requireStack;
throw err;
}
return filename as string;
}
static _findPath(
request: string,
paths: string[],
isMain: boolean
): string | boolean {
const absoluteRequest = path.isAbsolute(request);
if (absoluteRequest) {
paths = [""];
} else if (!paths || paths.length === 0) {
return false;
}
const cacheKey =
request + "\x00" + (paths.length === 1 ? paths[0] : paths.join("\x00"));
const entry = Module._pathCache[cacheKey];
if (entry) {
return entry;
}
let exts;
let trailingSlash =
request.length > 0 &&
request.charCodeAt(request.length - 1) === CHAR_FORWARD_SLASH;
if (!trailingSlash) {
trailingSlash = /(?:^|\/)\.?\.$/.test(request);
}
// For each path
for (let i = 0; i < paths.length; i++) {
// Don't search further if path doesn't exist
const curPath = paths[i];
if (curPath && stat(curPath) < 1) continue;
const basePath = resolveExports(curPath, request, absoluteRequest);
let filename;
const rc = stat(basePath);
if (!trailingSlash) {
if (rc === 0) {
// File.
// preserveSymlinks removed
filename = toRealPath(basePath);
}
if (!filename) {
// Try it with each of the extensions
if (exts === undefined) exts = Object.keys(Module._extensions);
filename = tryExtensions(basePath, exts, isMain);
}
}
if (!filename && rc === 1) {
// Directory.
// try it with each of the extensions at "index"
if (exts === undefined) exts = Object.keys(Module._extensions);
filename = tryPackage(basePath, exts, isMain, request);
}
if (filename) {
Module._pathCache[cacheKey] = filename;
return filename;
}
}
// trySelf removed.
return false;
}
// Check the cache for the requested file.
// 1. If a module already exists in the cache: return its exports object.
// 2. If the module is native: call
// `NativeModule.prototype.compileForPublicLoader()` and return the exports.
// 3. Otherwise, create a new module for the file and save it to the cache.
// Then have it load the file contents before returning its exports
// object.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
static _load(request: string, parent: Module, isMain: boolean): any {
let relResolveCacheIdentifier;
if (parent) {
// Fast path for (lazy loaded) modules in the same directory. The indirect
// caching is required to allow cache invalidation without changing the old
// cache key names.
relResolveCacheIdentifier = `${parent.path}\x00${request}`;
const filename = relativeResolveCache[relResolveCacheIdentifier];
if (filename !== undefined) {
const cachedModule = Module._cache[filename];
if (cachedModule !== undefined) {
updateChildren(parent, cachedModule, true);
if (!cachedModule.loaded)
return getExportsForCircularRequire(cachedModule);
return cachedModule.exports;
}
delete relativeResolveCache[relResolveCacheIdentifier];
}
}
const filename = Module._resolveFilename(request, parent, isMain);
const cachedModule = Module._cache[filename];
if (cachedModule !== undefined) {
updateChildren(parent, cachedModule, true);
if (!cachedModule.loaded)
return getExportsForCircularRequire(cachedModule);
return cachedModule.exports;
}
// Native module NOT supported.
// Don't call updateChildren(), Module constructor already does.
const module = new Module(filename, parent);
if (isMain) {
// TODO: set process info
// process.mainModule = module;
module.id = ".";
}
Module._cache[filename] = module;
if (parent !== undefined) {
relativeResolveCache[relResolveCacheIdentifier] = filename;
}
let threw = true;
try {
// Source map code removed
module.load(filename);
threw = false;
} finally {
if (threw) {
delete Module._cache[filename];
if (parent !== undefined) {
delete relativeResolveCache[relResolveCacheIdentifier];
}
} else if (
module.exports &&
Object.getPrototypeOf(module.exports) ===
CircularRequirePrototypeWarningProxy
) {
Object.setPrototypeOf(module.exports, PublicObjectPrototype);
}
}
return module.exports;
}
static wrap(script: string): string {
return `${Module.wrapper[0]}${script}${Module.wrapper[1]}`;
}
static _nodeModulePaths(from: string): string[] {
if (isWindows) {
// Guarantee that 'from' is absolute.
from = path.resolve(from);
// note: this approach *only* works when the path is guaranteed
// to be absolute. Doing a fully-edge-case-correct path.split
// that works on both Windows and Posix is non-trivial.
// return root node_modules when path is 'D:\\'.
// path.resolve will make sure from.length >=3 in Windows.
if (
from.charCodeAt(from.length - 1) === CHAR_BACKWARD_SLASH &&
from.charCodeAt(from.length - 2) === CHAR_COLON
)
return [from + "node_modules"];
const paths = [];
for (let i = from.length - 1, p = 0, last = from.length; i >= 0; --i) {
const code = from.charCodeAt(i);
// The path segment separator check ('\' and '/') was used to get
// node_modules path for every path segment.
// Use colon as an extra condition since we can get node_modules
// path for drive root like 'C:\node_modules' and don't need to
// parse drive name.
if (
code === CHAR_BACKWARD_SLASH ||
code === CHAR_FORWARD_SLASH ||
code === CHAR_COLON
) {
if (p !== nmLen) paths.push(from.slice(0, last) + "\\node_modules");
last = i;
p = 0;
} else if (p !== -1) {
if (nmChars[p] === code) {
++p;
} else {
p = -1;
}
}
}
return paths;
} else {
// posix
// Guarantee that 'from' is absolute.
from = path.resolve(from);
// Return early not only to avoid unnecessary work, but to *avoid* returning
// an array of two items for a root: [ '//node_modules', '/node_modules' ]
if (from === "/") return ["/node_modules"];
// note: this approach *only* works when the path is guaranteed
// to be absolute. Doing a fully-edge-case-correct path.split
// that works on both Windows and Posix is non-trivial.
const paths = [];
for (let i = from.length - 1, p = 0, last = from.length; i >= 0; --i) {
const code = from.charCodeAt(i);
if (code === CHAR_FORWARD_SLASH) {
if (p !== nmLen) paths.push(from.slice(0, last) + "/node_modules");
last = i;
p = 0;
} else if (p !== -1) {
if (nmChars[p] === code) {
++p;
} else {
p = -1;
}
}
}
// Append /node_modules to handle root paths.
paths.push("/node_modules");
return paths;
}
}
static createRequire(filename: string | URL): RequireFunction {
let filepath: string;
if (
filename instanceof URL ||
(typeof filename === "string" && !path.isAbsolute(filename))
) {
filepath = fileURLToPath(filename);
} else if (typeof filename !== "string") {
throw new Error("filename should be a string");
} else {
filepath = filename;
}
return createRequireFromPath(filepath);
}
static _initPaths(): void {
const homeDir = Deno.env("HOME");
const nodePath = Deno.env("NODE_PATH");
// Removed $PREFIX/bin/node case
let paths = [];
if (homeDir) {
paths.unshift(path.resolve(homeDir, ".node_libraries"));
paths.unshift(path.resolve(homeDir, ".node_modules"));
}
if (nodePath) {
paths = nodePath
.split(path.delimiter)
.filter(function pathsFilterCB(path) {
return !!path;
})
.concat(paths);
}
modulePaths = paths;
// Clone as a shallow copy, for introspection.
Module.globalPaths = modulePaths.slice(0);
}
static _preloadModules(requests: string[]): void {
if (!Array.isArray(requests)) {
return;
}
// Preloaded modules have a dummy parent module which is deemed to exist
// in the current working directory. This seeds the search path for
// preloaded modules.
const parent = new Module("internal/preload", null);
try {
parent.paths = Module._nodeModulePaths(Deno.cwd());
} catch (e) {
if (e.code !== "ENOENT") {
throw e;
}
}
for (let n = 0; n < requests.length; n++) {
parent.require(requests[n]);
}
}
}
let modulePaths = [];
// Given a module name, and a list of paths to test, returns the first
// matching file in the following precedence.
//
// require("a.<ext>")
// -> a.<ext>
//
// require("a")
// -> a
// -> a.<ext>
// -> a/index.<ext>
const packageJsonCache = new Map<string, PackageInfo | null>();
interface PackageInfo {
name?: string;
main?: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
exports?: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type?: any;
}
function readPackage(requestPath: string): PackageInfo | null {
const jsonPath = path.resolve(requestPath, "package.json");
const existing = packageJsonCache.get(jsonPath);
if (existing !== undefined) {
return existing;
}
let json: string | undefined;
try {
json = new TextDecoder().decode(
Deno.readFileSync(path.toNamespacedPath(jsonPath))
);
} catch {}
if (json === undefined) {
packageJsonCache.set(jsonPath, null);
return null;
}
try {
const parsed = JSON.parse(json);
const filtered = {
name: parsed.name,
main: parsed.main,
exports: parsed.exports,
type: parsed.type
};
packageJsonCache.set(jsonPath, filtered);
return filtered;
} catch (e) {
e.path = jsonPath;
e.message = "Error parsing " + jsonPath + ": " + e.message;
throw e;
}
}
function readPackageScope(
checkPath
): { path: string; data: PackageInfo } | false {
const rootSeparatorIndex = checkPath.indexOf(path.sep);
let separatorIndex;
while (
(separatorIndex = checkPath.lastIndexOf(path.sep)) > rootSeparatorIndex
) {
checkPath = checkPath.slice(0, separatorIndex);
if (checkPath.endsWith(path.sep + "node_modules")) return false;
const pjson = readPackage(checkPath);
if (pjson)
return {
path: checkPath,
data: pjson
};
}
return false;
}
function readPackageMain(requestPath: string): string | undefined {
const pkg = readPackage(requestPath);
return pkg ? pkg.main : undefined;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function readPackageExports(requestPath: string): any | undefined {
const pkg = readPackage(requestPath);
return pkg ? pkg.exports : undefined;
}
function tryPackage(
requestPath: string,
exts: string[],
isMain: boolean,
_originalPath: string
): string | false {
const pkg = readPackageMain(requestPath);
if (!pkg) {
return tryExtensions(path.resolve(requestPath, "index"), exts, isMain);
}
const filename = path.resolve(requestPath, pkg);
let actual =
tryFile(filename, isMain) ||
tryExtensions(filename, exts, isMain) ||
tryExtensions(path.resolve(filename, "index"), exts, isMain);
if (actual === false) {
actual = tryExtensions(path.resolve(requestPath, "index"), exts, isMain);
if (!actual) {
// eslint-disable-next-line no-restricted-syntax
const err = new Error(
`Cannot find module '${filename}'. ` +
'Please verify that the package.json has a valid "main" entry'
);
// @ts-ignore
err.code = "MODULE_NOT_FOUND";
throw err;
}
}
return actual;
}
// Check if the file exists and is not a directory
// if using --preserve-symlinks and isMain is false,
// keep symlinks intact, otherwise resolve to the
// absolute realpath.
function tryFile(requestPath: string, _isMain: boolean): string | false {
const rc = stat(requestPath);
return rc === 0 && toRealPath(requestPath);
}
function toRealPath(requestPath: string): string {
// Deno does not have realpath implemented yet.
let fullPath = requestPath;
while (true) {
try {
fullPath = Deno.readlinkSync(fullPath);
} catch {
break;
}
}
return path.resolve(requestPath);
}
// Given a path, check if the file exists with any of the set extensions
function tryExtensions(
p: string,
exts: string[],
isMain: boolean
): string | false {
for (let i = 0; i < exts.length; i++) {
const filename = tryFile(p + exts[i], isMain);
if (filename) {
return filename;
}
}
return false;
}
// Find the longest (possibly multi-dot) extension registered in
// Module._extensions
function findLongestRegisteredExtension(filename: string): string {
const name = path.basename(filename);
let currentExtension;
let index;
let startIndex = 0;
while ((index = name.indexOf(".", startIndex)) !== -1) {
startIndex = index + 1;
if (index === 0) continue; // Skip dotfiles like .gitignore
currentExtension = name.slice(index);
if (Module._extensions[currentExtension]) return currentExtension;
}
return ".js";
}
// --experimental-resolve-self trySelf() support removed.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function isConditionalDotExportSugar(exports: any, _basePath: string): boolean {
if (typeof exports === "string") return true;
if (Array.isArray(exports)) return true;
if (typeof exports !== "object") return false;
let isConditional = false;
let firstCheck = true;
for (const key of Object.keys(exports)) {
const curIsConditional = key[0] !== ".";
if (firstCheck) {
firstCheck = false;
isConditional = curIsConditional;
} else if (isConditional !== curIsConditional) {
throw new Error(
'"exports" cannot ' +
"contain some keys starting with '.' and some not. The exports " +
"object must either be an object of package subpath keys or an " +
"object of main entry condition name keys only."
);
}
}
return isConditional;
}
function applyExports(basePath: string, expansion: string): string {
const mappingKey = `.${expansion}`;
let pkgExports = readPackageExports(basePath);
if (pkgExports === undefined || pkgExports === null)
return path.resolve(basePath, mappingKey);
if (isConditionalDotExportSugar(pkgExports, basePath))
pkgExports = { ".": pkgExports };
if (typeof pkgExports === "object") {
if (pkgExports.hasOwnProperty(mappingKey)) {
const mapping = pkgExports[mappingKey];
return resolveExportsTarget(
pathToFileURL(basePath + "/"),
mapping,
"",
basePath,
mappingKey
);
}
// Fallback to CJS main lookup when no main export is defined
if (mappingKey === ".") return basePath;
let dirMatch = "";
for (const candidateKey of Object.keys(pkgExports)) {
if (candidateKey[candidateKey.length - 1] !== "/") continue;
if (
candidateKey.length > dirMatch.length &&
mappingKey.startsWith(candidateKey)
) {
dirMatch = candidateKey;
}
}
if (dirMatch !== "") {
const mapping = pkgExports[dirMatch];
const subpath = mappingKey.slice(dirMatch.length);
return resolveExportsTarget(
pathToFileURL(basePath + "/"),
mapping,
subpath,
basePath,
mappingKey
);
}
}
// Fallback to CJS main lookup when no main export is defined
if (mappingKey === ".") return basePath;
const e = new Error(
`Package exports for '${basePath}' do not define ` +
`a '${mappingKey}' subpath`
);
// @ts-ignore
e.code = "MODULE_NOT_FOUND";
throw e;
}
// This only applies to requests of a specific form:
// 1. name/.*
// 2. @scope/name/.*
const EXPORTS_PATTERN = /^((?:@[^/\\%]+\/)?[^./\\%][^/\\%]*)(\/.*)?$/;
function resolveExports(
nmPath: string,
request: string,
absoluteRequest: boolean
): string {
// The implementation's behavior is meant to mirror resolution in ESM.
if (!absoluteRequest) {
const [, name, expansion = ""] = request.match(EXPORTS_PATTERN) || [];
if (!name) {
return path.resolve(nmPath, request);
}
const basePath = path.resolve(nmPath, name);
return applyExports(basePath, expansion);
}
return path.resolve(nmPath, request);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function resolveExportsTarget(
pkgPath: URL,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
target: any,
subpath: string,
basePath: string,
mappingKey: string
): string {
if (typeof target === "string") {
if (
target.startsWith("./") &&
(subpath.length === 0 || target.endsWith("/"))
) {
const resolvedTarget = new URL(target, pkgPath);
const pkgPathPath = pkgPath.pathname;
const resolvedTargetPath = resolvedTarget.pathname;
if (
resolvedTargetPath.startsWith(pkgPathPath) &&
resolvedTargetPath.indexOf("/node_modules/", pkgPathPath.length - 1) ===
-1
) {
const resolved = new URL(subpath, resolvedTarget);
const resolvedPath = resolved.pathname;
if (
resolvedPath.startsWith(resolvedTargetPath) &&
resolvedPath.indexOf("/node_modules/", pkgPathPath.length - 1) === -1
) {
return fileURLToPath(resolved);
}
}
}
} else if (Array.isArray(target)) {
for (const targetValue of target) {
if (Array.isArray(targetValue)) continue;
try {
return resolveExportsTarget(
pkgPath,
targetValue,
subpath,
basePath,
mappingKey
);
} catch (e) {
if (e.code !== "MODULE_NOT_FOUND") throw e;
}
}
} else if (typeof target === "object" && target !== null) {
// removed experimentalConditionalExports
if (target.hasOwnProperty("default")) {
try {
return resolveExportsTarget(
pkgPath,
target.default,
subpath,
basePath,
mappingKey
);
} catch (e) {
if (e.code !== "MODULE_NOT_FOUND") throw e;
}
}
}
let e: Error;
if (mappingKey !== ".") {
e = new Error(
`Package exports for '${basePath}' do not define a ` +
`valid '${mappingKey}' target${subpath ? " for " + subpath : ""}`
);
} else {
e = new Error(`No valid exports main found for '${basePath}'`);
}
// @ts-ignore
e.code = "MODULE_NOT_FOUND";
throw e;
}
// 'node_modules' character codes reversed
const nmChars = [115, 101, 108, 117, 100, 111, 109, 95, 101, 100, 111, 110];
const nmLen = nmChars.length;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function emitCircularRequireWarning(prop: any): void {
console.error(
`Accessing non-existent property '${String(
prop
)}' of module exports inside circular dependency`
);
}
// A Proxy that can be used as the prototype of a module.exports object and
// warns when non-existend properties are accessed.
const CircularRequirePrototypeWarningProxy = new Proxy(
{},
{
// eslint-disable-next-line @typescript-eslint/no-explicit-any
get(target, prop): any {
if (prop in target) return target[prop];
emitCircularRequireWarning(prop);
return undefined;
},
getOwnPropertyDescriptor(target, prop): PropertyDescriptor | undefined {
if (target.hasOwnProperty(prop))
return Object.getOwnPropertyDescriptor(target, prop);
emitCircularRequireWarning(prop);
return undefined;
}
}
);
// Object.prototype and ObjectProtoype refer to our 'primordials' versions
// and are not identical to the versions on the global object.
const PublicObjectPrototype = window.Object.prototype;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function getExportsForCircularRequire(module: Module): any {
if (
module.exports &&
Object.getPrototypeOf(module.exports) === PublicObjectPrototype &&
// Exclude transpiled ES6 modules / TypeScript code because those may
// employ unusual patterns for accessing 'module.exports'. That should be
// okay because ES6 modules have a different approach to circular
// dependencies anyway.
!module.exports.__esModule
) {
// This is later unset once the module is done loading.
Object.setPrototypeOf(module.exports, CircularRequirePrototypeWarningProxy);
}
return module.exports;
}
type RequireWrapper = (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
exports: any,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
require: any,
module: Module,
__filename: string,
__dirname: string
) => void;
function wrapSafe(filename_: string, content: string): RequireWrapper {
// TODO: fix this
const wrapper = Module.wrap(content);
// @ts-ignore
const [f, err] = Deno.core.evalContext(wrapper);
if (err) {
throw err;
}
return f;
// ESM code removed.
}
// Native extension for .js
Module._extensions[".js"] = (module: Module, filename: string): void => {
if (filename.endsWith(".js")) {
const pkg = readPackageScope(filename);
if (pkg !== false && pkg.data && pkg.data.type === "module") {
throw new Error("Importing ESM module");
}
}
const content = new TextDecoder().decode(Deno.readFileSync(filename));
module._compile(content, filename);
};
// Native extension for .json
Module._extensions[".json"] = (module: Module, filename: string): void => {
const content = new TextDecoder().decode(Deno.readFileSync(filename));
// manifest code removed
try {
module.exports = JSON.parse(stripBOM(content));
} catch (err) {
err.message = filename + ": " + err.message;
throw err;
}
};
// .node extension is not supported
function createRequireFromPath(filename: string): RequireFunction {
// Allow a directory to be passed as the filename
const trailingSlash =
filename.endsWith("/") || (isWindows && filename.endsWith("\\"));
const proxyPath = trailingSlash ? path.join(filename, "noop.js") : filename;
const m = new Module(proxyPath);
m.filename = proxyPath;
m.paths = Module._nodeModulePaths(m.path);
return makeRequireFunction(m);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type Require = (id: string) => any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type RequireResolve = (request: string, options: any) => string;
interface RequireResolveFunction extends RequireResolve {
paths: (request: string) => string[] | null;
}
interface RequireFunction extends Require {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
resolve: RequireResolveFunction;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
extensions: { [key: string]: (module: Module, filename: string) => any };
cache: { [key: string]: Module };
}
function makeRequireFunction(mod: Module): RequireFunction {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const require = function require(path: string): any {
return mod.require(path);
};
function resolve(request: string, options?: { paths: string[] }): string {
return Module._resolveFilename(request, mod, false, options);
}
require.resolve = resolve;
function paths(request: string): string[] | null {
return Module._resolveLookupPaths(request, mod);
}
resolve.paths = paths;
// TODO: set main
// 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.
*/
function stripBOM(content: string): string {
if (content.charCodeAt(0) === 0xfeff) {
content = content.slice(1);
}
return content;
}
const forwardSlashRegEx = /\//g;
const CHAR_LOWERCASE_A = "a".charCodeAt(0);
const CHAR_LOWERCASE_Z = "z".charCodeAt(0);
function getPathFromURLWin32(url: URL): string {
// const hostname = url.hostname;
let pathname = url.pathname;
for (let n = 0; n < pathname.length; n++) {
if (pathname[n] === "%") {
const third = pathname.codePointAt(n + 2) | 0x20;
if (
(pathname[n + 1] === "2" && third === 102) || // 2f 2F /
(pathname[n + 1] === "5" && third === 99)
) {
// 5c 5C \
throw new Error(
"Invalid file url path: must not include encoded \\ or / characters"
);
}
}
}
pathname = pathname.replace(forwardSlashRegEx, "\\");
pathname = decodeURIComponent(pathname);
// TODO: handle windows hostname case (needs bindings)
const letter = pathname.codePointAt(1) | 0x20;
const sep = pathname[2];
if (
letter < CHAR_LOWERCASE_A ||
letter > CHAR_LOWERCASE_Z || // a..z A..Z
sep !== ":"
) {
throw new Error("Invalid file URL path: must be absolute");
}
return pathname.slice(1);
}
function getPathFromURLPosix(url: URL): string {
if (url.hostname !== "") {
throw new Error("Invalid file URL host");
}
const pathname = url.pathname;
for (let n = 0; n < pathname.length; n++) {
if (pathname[n] === "%") {
const third = pathname.codePointAt(n + 2) | 0x20;
if (pathname[n + 1] === "2" && third === 102) {
throw new Error(
"Invalid file URL path: must not include encoded / characters"
);
}
}
}
return decodeURIComponent(pathname);
}
function fileURLToPath(path: string | URL): string {
if (typeof path === "string") {
path = new URL(path);
}
if (path.protocol !== "file:") {
throw new Error("Protocol has to be file://");
}
return isWindows ? getPathFromURLWin32(path) : getPathFromURLPosix(path);
}
const percentRegEx = /%/g;
const backslashRegEx = /\\/g;
const newlineRegEx = /\n/g;
const carriageReturnRegEx = /\r/g;
const tabRegEx = /\t/g;
function pathToFileURL(filepath: string): URL {
let resolved = path.resolve(filepath);
// path.resolve strips trailing slashes so we must add them back
const filePathLast = filepath.charCodeAt(filepath.length - 1);
if (
(filePathLast === CHAR_FORWARD_SLASH ||
(isWindows && filePathLast === CHAR_BACKWARD_SLASH)) &&
resolved[resolved.length - 1] !== path.sep
)
resolved += "/";
const outURL = new URL("file://");
if (resolved.includes("%")) resolved = resolved.replace(percentRegEx, "%25");
// In posix, "/" is a valid character in paths
if (!isWindows && resolved.includes("\\"))
resolved = resolved.replace(backslashRegEx, "%5C");
if (resolved.includes("\n")) resolved = resolved.replace(newlineRegEx, "%0A");
if (resolved.includes("\r"))
resolved = resolved.replace(carriageReturnRegEx, "%0D");
if (resolved.includes("\t")) resolved = resolved.replace(tabRegEx, "%09");
outURL.pathname = resolved;
return outURL;
}
/**
* Create a `require` function that can be used to import CJS modules
* @param path path of this module
*/
function makeRequire(filePath: string): RequireFunction {
let mod: Module;
const fullPath = path.resolve(filePath);
if (fullPath in Module._cache) {
mod = Module._cache[fullPath];
} else {
mod = new Module(fullPath);
}
return makeRequireFunction(mod);
}
export { makeRequire };