Files
Foundry-VTT-Docker/resources/app/node_modules/@peggyjs/from-mem/index.js
2025-01-04 00:34:03 +01:00

305 lines
8.8 KiB
JavaScript

"use strict";
// Import or require module text from memory, rather than disk. Runs
// in a node vm, very similar to how node loads modules.
//
// Ideas taken from the "module-from-string" and "eval" modules, neither of
// which were situated correctly to be used as-is.
const { Module } = require("node:module");
const fs = require("node:fs/promises");
const path = require("node:path");
const semver = require("semver");
const url = require("node:url");
const vm = require("node:vm");
// These already exist in a new, blank VM. Date, JSON, NaN, etc.
// Things from the core language.
const vmGlobals = new vm
.Script("Object.getOwnPropertyNames(globalThis)")
.runInNewContext()
.sort();
vmGlobals.push("global", "globalThis", "sys");
// These are the things that are normally in the environment, that vm doesn't
// make available. This that you expect to be available in a node environment
// that aren't in the laguage itself.
const neededKeys = Object
.getOwnPropertyNames(global)
.filter(k => !vmGlobals.includes(k))
.sort();
const globalContext = Object.fromEntries(
neededKeys.map(k => [k, global[
/** @type {keyof typeof global} */ (k)
]])
);
// In node <15, console is in vmGlobals.
globalContext.console = console;
/**
* @typedef {"amd"
* | "bare"
* | "cjs"
* | "commonjs"
* | "es"
* | "es6"
* | "esm"
* | "globals"
* | "guess"
* | "mjs"
* | "module"
* | "umd" } SourceFormat
*/
/**
* Options for how to process code.
*
* @typedef {object} FromMemOptions
* @property {SourceFormat} [format="commonjs"]
* What format does the code have? "guess" means to read the closest
* package.json file looking for the "type" key.
* @property {string} filename What is the fully-qualified synthetic
* filename for the code? Most important is the directory, which is used to
* find modules that the code import's or require's.
* @property {object} [context={}] Variables to make availble in the global
* scope while code is being evaluated.
* @property {boolean} [includeGlobals=true] Include the typical global
* properties that node gives to all modules. (e.g. Buffer, process).
* @property {string} [globalExport=null] For type "globals", what name is
* exported from the module?
* @property {number} [lineOffset=0] Specifies the line number offset that is
* displayed in stack traces produced by this script.
* @property {number} [columnOffset=0] Specifies the first-line column number
* offset that is displayed in stack traces produced by this script.
*/
/**
* Treat the given code as a node module as if require() had been called
* on a file containing the code.
*
* @param {string} code Source code in commonjs format.
* @param {string} dirname Used for __dirname.
* @param {FromMemOptions} options
* @returns {object} The module exports from code
*/
function requireString(code, dirname, options) {
// @ts-expect-error This isn't correct.
const m = new Module(options.filename, module); // Current module is parent.
// This is the function that will be called by `require()` in the parser.
m.require = Module.createRequire(options.filename);
const script = new vm.Script(code, {
filename: options.filename,
lineOffset: options.lineOffset,
columnOffset: options.columnOffset,
});
return script.runInNewContext({
module: m,
exports: m.exports,
require: m.require,
__dirname: dirname,
__filename: options.filename,
...options.context,
});
}
/**
* If the given specifier starts with a ".", path.resolve it to the given
* directory. Otherwise, it's a fully-qualified path, a node internal
* module name, an npm-provided module name, or a URL.
*
* @param {string} dirname Owning directory
* @param {string} specifier String from the rightmost side of an import statement
* @returns {string} Resolved path name or original string
*/
function resolveIfNeeded(dirname, specifier) {
if (specifier.startsWith(".")) {
specifier = new URL(specifier, dirname).toString();
}
return specifier;
}
/**
* Treat the given code as a node module as if import had been called
* on a file containing the code.
*
* @param {string} code Source code in es6 format.
* @param {string} dirname Where the synthetic file would have lived.
* @param {FromMemOptions} options
* @returns {Promise<unknown>} The module exports from code
*/
async function importString(code, dirname, options) {
if (!vm.SourceTextModule) {
throw new Error("Start node with --experimental-vm-modules for this to work");
}
if (!semver.satisfies(process.version, ">=20.8")) {
throw new Error("Requires node.js 20.8+ or 21.");
}
const fileUrl = options.filename.startsWith("file:")
? options.filename
: url.pathToFileURL(options.filename).toString();
const dirUrl = dirname.startsWith("file:")
? dirname + "/"
: url.pathToFileURL(dirname).toString() + "/";
const mod = new vm.SourceTextModule(code, {
identifier: fileUrl,
lineOffset: options.lineOffset,
columnOffset: options.columnOffset,
context: vm.createContext(options.context),
initializeImportMeta(meta) {
meta.url = fileUrl;
},
// @ts-expect-error Types in @types/node are wrong.
importModuleDynamically(specifier) {
return import(resolveIfNeeded(dirUrl, specifier));
},
});
await mod.link(async(specifier, referencingModule) => {
const resolvedSpecifier = resolveIfNeeded(dirUrl, specifier);
const targetModule = await import(resolvedSpecifier);
const exports = Object.keys(targetModule);
// DO NOT change function to () =>, or `this` will be wrong.
return new vm.SyntheticModule(exports, function() {
for (const e of exports) {
this.setExport(e, targetModule[e]);
}
}, {
context: referencingModule.context,
});
});
await mod.evaluate();
return mod.namespace;
}
/**
* @typedef {"commonjs"|"es"} ModuleType
*/
/**
* @type Record<string, ModuleType>
*/
let cache = {};
/**
* Figure out the module type for the given file. If no package.json is
* found, default to "commonjs".
*
* @param {string} filename Fully-qualified filename to start from.
* @returns {Promise<ModuleType>}
* @throws On invalid package.json
*/
async function guessModuleType(filename) {
const fp = path.parse(filename);
switch (fp.ext) {
case ".cjs": return "commonjs";
case ".mjs": return "es";
default:
// Fall-through
}
/** @type {ModuleType} */
let res = "commonjs";
let dir = fp.dir;
let prev = undefined;
const pending = [];
while (dir !== prev) {
const cached = cache[dir];
if (cached) {
return cached;
}
pending.push(dir);
try {
const pkg = await fs.readFile(path.join(dir, "package.json"), "utf8");
const pkgj = JSON.parse(pkg);
res = (pkgj.type === "module") ? "es" : "commonjs";
break;
} catch (err) {
// If the file just didn't exist, keep going.
if (/** @type {NodeJS.ErrnoException} */ (err).code !== "ENOENT") {
throw err;
}
}
prev = dir;
dir = path.dirname(dir);
}
for (const p of pending) {
cache[p] = res;
}
return res;
}
guessModuleType.clearCache = function clearCache() {
cache = {};
};
/**
* Import or require the given code from memory. Knows about the different
* Peggy output formats. Returns the exports of the module.
*
* @param {string} code Code to import
* @param {FromMemOptions} options Options. Most important is filename.
* @returns {Promise<unknown>} The evaluated code.
*/
async function fromMem(code, options) {
options = {
format: "commonjs",
context: {},
includeGlobals: true,
globalExport: undefined,
lineOffset: 0,
columnOffset: 0,
...options,
};
if (options.includeGlobals) {
options.context = {
...globalContext,
...options.context,
};
}
// @ts-expect-error Context is always non-null
options.context.global = options.context;
// @ts-expect-error Context is always non-null
options.context.globalThis = options.context;
if (!options.filename) {
throw new TypeError("filename is required");
}
if (!options.filename.startsWith("file:")) {
// File URLs must be already resolved.
options.filename = path.resolve(options.filename);
}
const dirname = path.dirname(options.filename);
if (options.format === "guess") {
options.format = await guessModuleType(options.filename);
}
switch (options.format) {
case "bare":
case "cjs":
case "commonjs":
case "umd":
return requireString(code, dirname, options);
case "es":
case "es6":
case "esm":
case "module":
case "mjs":
// Returns promise
return importString(code, dirname, options);
// I don't care enough about amd and globals to figure out how to load them.
default:
throw new Error(`Unsupported output format: "${options.format}"`);
}
}
fromMem.guessModuleType = guessModuleType;
module.exports = fromMem;