diff --git a/index.d.ts b/index.d.ts index f2d7013..44795a7 100644 --- a/index.d.ts +++ b/index.d.ts @@ -16,31 +16,33 @@ declare namespace fromMem { */ type FromMemOptions = { /** - * What format does the code have? "guess" means to read the closest - * package.json file looking for the "type" key. + * What format does the code + * have? "guess" means to read the closest package.json file looking for + * the "type" key. "globals", "amd", and "bare" are not actually supported. */ format?: SourceFormat | undefined; /** - * 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. + * If specified, use this instead of the + * current values in process.env. Works if includeGlobals is false by + * creating an otherwise-empty process instance. + */ + env?: Record | undefined; + /** + * 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. */ filename: string; /** - * Variables to make availble in the global - * scope while code is being evaluated. + * Variables to make availble in + * the global scope while code is being evaluated. */ - context?: object | undefined; + context?: Record | undefined; /** * Include the typical global * properties that node gives to all modules. (e.g. Buffer, process). */ includeGlobals?: boolean | undefined; - /** - * For type "globals", what name is - * exported from the module? - */ - globalExport?: string | undefined; /** * Specifies the line number offset that is * displayed in stack traces produced by this script. diff --git a/index.js b/index.js index 162a232..f211c93 100644 --- a/index.js +++ b/index.js @@ -15,18 +15,20 @@ 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 +const vmGlobals = new Set(new vm .Script("Object.getOwnPropertyNames(globalThis)") - .runInNewContext() - .sort(); -vmGlobals.push("global", "globalThis", "sys"); + .runInNewContext()); +vmGlobals.add("global"); +vmGlobals.add("globalThis"); +vmGlobals.add("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. +// that aren't in the laguage itself. There are a lot more things in this list +// than you expect, like setTimeout and structuredClone. const neededKeys = Object .getOwnPropertyNames(global) - .filter(k => !vmGlobals.includes(k)) + .filter(k => !vmGlobals.has(k)) .sort(); const globalContext = Object.fromEntries( neededKeys.map(k => [k, global[ @@ -34,7 +36,6 @@ const globalContext = Object.fromEntries( ]]) ); -// In node <15, console is in vmGlobals. globalContext.console = console; /** @@ -56,18 +57,19 @@ globalContext.console = console; * 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 {SourceFormat} [format="commonjs"] What format does the code + * have? "guess" means to read the closest package.json file looking for + * the "type" key. "globals", "amd", and "bare" are not actually supported. + * @property {Record} [env] If specified, use this instead of the + * current values in process.env. Works if includeGlobals is false by + * creating an otherwise-empty process instance. + * @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 {Record} [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 @@ -248,9 +250,8 @@ guessModuleType.clearCache = function clearCache() { async function fromMem(code, options) { options = { format: "commonjs", - context: {}, + env: undefined, includeGlobals: true, - globalExport: undefined, lineOffset: 0, columnOffset: 0, ...options, @@ -261,11 +262,30 @@ async function fromMem(code, options) { ...globalContext, ...options.context, }; + } else { + // Put this here instead of in the defaults above so that typescript + // can see it. + options.context = options.context || {}; + } + + // Make sure env changes don't stick. This isn't a security measure, it's + // to prevent mistakes. There are probably a few other places where + // mistakes are likely, and the same treatment should be given. + if (options.context.process) { + if (options.context.process === process) { + options.context.process = { ...process }; + } + options.context.process.env = options.env || { + ...options.context.process.env, + }; + } else if (options.env) { + options.context.process = { + version: process.version, + env: { ...options.env }, + }; } - // @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) { diff --git a/package.json b/package.json index 43099ba..bd1b843 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "semver": "7.6.0" }, "devDependencies": { - "@peggyjs/eslint-config": "3.2.3", + "@peggyjs/eslint-config": "3.2.4", "@types/node": "20.11.20", "@types/semver": "7.5.8", "c8": "9.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fa76355..5ccf1c6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,8 +11,8 @@ dependencies: devDependencies: '@peggyjs/eslint-config': - specifier: 3.2.3 - version: 3.2.3(eslint@8.57.0)(typescript@5.3.3) + specifier: 3.2.4 + version: 3.2.4(eslint@8.57.0)(typescript@5.3.3) '@types/node': specifier: 20.11.20 version: 20.11.20 @@ -139,8 +139,8 @@ packages: fastq: 1.17.1 dev: true - /@peggyjs/eslint-config@3.2.3(eslint@8.57.0)(typescript@5.3.3): - resolution: {integrity: sha512-NlYOPUHuR9lSmd8OimdrH+buCrtVILfsEun63ZmJqokpiVBE+Cpb2z9joIsS1LHGCZWsYQdr79xFbOstq0U/ew==} + /@peggyjs/eslint-config@3.2.4(eslint@8.57.0)(typescript@5.3.3): + resolution: {integrity: sha512-Mj6F0U2Sf8Pc3c9HA3N+4GswSWf4yaeiRQV7AmdUZNLUzieDllMLf9QWzV/vSkjo/FiDa9Yu2DOTmiEdpxJ5nA==} engines: {node: '>=18'} peerDependencies: '@typescript-eslint/eslint-plugin': ~7 diff --git a/test/index.test.js b/test/index.test.js index 4f52058..c02d128 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -181,3 +181,39 @@ test("no SourceTextModule", async() => { // Reset vm.SourceTextModule = stm; }); + +test("process.env", async() => { + // No process gives the right error + await assert.rejects(() => fromMem("module.exports = process", { + filename: join(__dirname, "test11.js"), + format: "cjs", + includeGlobals: false, + }), /process is not defined/); + + // Pick up current value + process.env.___TEST1___ = "12"; + assert.equal((await fromMem("module.exports = process.env.___TEST1___", { + filename: join(__dirname, "test12.js"), + format: "cjs", + })), "12"); + delete process.env.___TEST1___; + + // Anti-pollution + assert.equal((await fromMem(` +process.env.___TEST2___ = "13"; +module.exports = process.env.___TEST2___`, { + filename: join(__dirname, "test13.js"), + format: "cjs", + })), "13"); + assert.equal(typeof process.env.___TEST2___, "undefined"); + + // Fake process + assert.equal((await fromMem("module.exports = process.env.___TEST3___", { + filename: join(__dirname, "test14.js"), + format: "cjs", + includeGlobals: false, + env: { + ___TEST3___: "14", + }, + })), "14"); +});