Skip to content

Commit

Permalink
Merge pull request #9 from hildjj/process.env
Browse files Browse the repository at this point in the history
Ability to overwrite process.env.  Ensure env changes don't leak.
  • Loading branch information
hildjj authored Feb 27, 2024
2 parents 1cc664d + 265ac21 commit b8b6459
Show file tree
Hide file tree
Showing 5 changed files with 97 additions and 39 deletions.
28 changes: 15 additions & 13 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, any> | 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<string, any> | 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.
Expand Down
62 changes: 41 additions & 21 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,26 +15,27 @@ 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[
/** @type {keyof typeof global} */ (k)
]])
);

// In node <15, console is in vmGlobals.
globalContext.console = console;

/**
Expand All @@ -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<string, any>} [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<string, any>} [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
Expand Down Expand Up @@ -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,
Expand All @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
8 changes: 4 additions & 4 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

36 changes: 36 additions & 0 deletions test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});

0 comments on commit b8b6459

Please sign in to comment.