node-retrieve-globals
Advanced tools
Comparing version 4.0.0 to 5.0.0
{ | ||
"name": "node-retrieve-globals", | ||
"version": "4.0.0", | ||
"version": "5.0.0", | ||
"description": "Execute a string of JavaScript using Node.js and return the global variable values and functions.", | ||
@@ -8,3 +8,3 @@ "type": "module", | ||
"scripts": { | ||
"test": "npx ava" | ||
"test": "npx ava && cross-env NODE_OPTIONS='--experimental-vm-modules' npx ava" | ||
}, | ||
@@ -23,9 +23,10 @@ "repository": { | ||
"@zachleat/noop": "^1.0.3", | ||
"ava": "^5.2.0" | ||
"ava": "^6.0.1", | ||
"cross-env": "^7.0.3" | ||
}, | ||
"dependencies": { | ||
"acorn": "^8.8.2", | ||
"acorn-walk": "^8.2.0", | ||
"acorn-walk": "^8.3.1", | ||
"esm-import-transformer": "^3.0.2" | ||
} | ||
} |
@@ -7,10 +7,12 @@ # node-retrieve-globals | ||
* Uses `var`, `let`, `const`, `function`, Array and Object [destructuring assignment](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment). | ||
* Async-friendly but synchronous methods are available. | ||
* Async-only as of v5.0. | ||
* Can return any valid JS data type (including functions). | ||
* Can provide an external data object as context to the local execution scope | ||
* Transforms ESM import statements to work with current CommonJS limitations in Node’s `vm`. | ||
* Uses [Node’s `vm` module to execute JavaScript](https://nodejs.org/api/vm.html#vmruninthiscontextcode-options) | ||
* ⚠️ The `node:vm` module is not a security mechanism. Do not use it to run untrusted code. | ||
* `codeGeneration` (e.g. `eval`) is disabled by default; use `setCreateContextOptions({codeGeneration: { strings: true, wasm: true } })` to re-enable. | ||
* Works _with or without_ `--experimental-vm-modules` flag (for `vm.Module` support). _(v5.0.0 and newer)_ | ||
* Future-friendly feature tests for when `vm.Module` is stable and `--experimental-vm-modules` is no longer necessary. _(v5.0.0 and newer)_ | ||
* In use on: | ||
* [JavaScript in Eleventy Front Matter](https://www.11ty.dev/docs/data-frontmatter-customize/#example-use-javascript-in-your-front-matter) (and [Demo](https://github.com/11ty/demo-eleventy-js-front-matter)) | ||
@@ -47,5 +49,2 @@ * [WebC’s `<script webc:setup>`](https://www.11ty.dev/docs/languages/webc/#using-javascript-to-setup-your-component-webcsetup) | ||
await vm.getGlobalContext(); | ||
// or sync: | ||
// vm.getGlobalContextSync(); | ||
``` | ||
@@ -67,5 +66,2 @@ | ||
await vm.getGlobalContext({ myData: "hello" }); | ||
// or sync: | ||
// vm.getGlobalContextSync({ myData: "hello" }); | ||
``` | ||
@@ -86,2 +82,4 @@ | ||
dynamicImport: false, // allows `import()` | ||
addRequire: false, // allows `require()` | ||
experimentalModuleApi: false, // uses Module#_compile instead of `vm` (you probably don’t want this and it is bypassed by default when vm.Module is supported) | ||
}; | ||
@@ -88,0 +86,0 @@ |
@@ -7,3 +7,7 @@ import vm from "vm"; | ||
// TODO option to change `require` home base | ||
import { isSupported } from "./vmModules.js"; | ||
const IS_VM_MODULES_SUPPORTED = isSupported(); | ||
// TODO (feature) option to change `require` home base | ||
const customRequire = createRequire(import.meta.url); | ||
@@ -27,13 +31,17 @@ | ||
if(IS_VM_MODULES_SUPPORTED) { | ||
// Override: no code transformations if vm.Module works | ||
this.options.transformEsmImports = false; | ||
} | ||
// set defaults | ||
let acornOptions = {}; | ||
if(this.options.transformEsmImports) { | ||
if(IS_VM_MODULES_SUPPORTED || this.options.transformEsmImports) { | ||
acornOptions.sourceType = "module"; | ||
} | ||
this.setAcornOptions(acornOptions); | ||
this.setCreateContextOptions(); | ||
// transform `import ___ from ___` | ||
// to `const ___ = await import(___)` | ||
// to emulate *some* import syntax. | ||
// transform `import ___ from ___` to `const ___ = await import(___)` to emulate *some* import syntax. | ||
// Doesn’t currently work with aliases (mod as name) or namespaced imports (* as name). | ||
@@ -88,8 +96,8 @@ if(this.options.transformEsmImports) { | ||
// (our acorn walker could be improved to skip non-global declarations, but this method is easier for now) | ||
static _getGlobalVariablesReturnString(names) { | ||
let s = [`let globals = {};`]; | ||
static _getGlobalVariablesReturnString(names, mode = "cjs") { | ||
let s = [`let __globals = {};`]; | ||
for(let name of names) { | ||
s.push(`if( typeof ${name} !== "undefined") { globals.${name} = ${name}; }`); | ||
s.push(`if( typeof ${name} !== "undefined") { __globals.${name} = ${name}; }`); | ||
} | ||
return `${s.join("\n")}; return globals;` | ||
return `${s.join("\n")};${mode === "esm" ? "\nexport default __globals;" : "return __globals;"}` | ||
} | ||
@@ -114,3 +122,3 @@ | ||
static _getCode(code, options) { | ||
_getCode(code, options) { | ||
let { async: isAsync, globalNames, experimentalModuleApi, data } = Object.assign({ | ||
@@ -120,3 +128,9 @@ async: true | ||
let prefix = ""; | ||
if(IS_VM_MODULES_SUPPORTED) { | ||
return `${code} | ||
${globalNames ? RetrieveGlobals._getGlobalVariablesReturnString(globalNames, "esm") : ""}`; | ||
} | ||
let prefix = []; | ||
let argKeys = ""; | ||
@@ -143,11 +157,65 @@ let argValues = ""; | ||
let finalCode = `${prefix}(${isAsync ? "async " : ""}function(${argKeys}) { | ||
return `${prefix}(${isAsync ? "async " : ""}function(${argKeys}) { | ||
${code} | ||
${globalNames ? RetrieveGlobals._getGlobalVariablesReturnString(globalNames) : ""} | ||
${globalNames ? RetrieveGlobals._getGlobalVariablesReturnString(globalNames, "cjs") : ""} | ||
})(${argValues});`; | ||
return finalCode; | ||
} | ||
_getGlobalContext(data, options) { | ||
getGlobalNames(parsedAst) { | ||
let globalNames = new Set(); | ||
let types = { | ||
FunctionDeclaration(node) { | ||
globalNames.add(node.id.name); | ||
}, | ||
VariableDeclarator(node) { | ||
// destructuring assignment Array | ||
if(node.id.type === "ArrayPattern") { | ||
for(let prop of node.id.elements) { | ||
if(prop.type === "Identifier") { | ||
globalNames.add(prop.name); | ||
} | ||
} | ||
} else if(node.id.type === "ObjectPattern") { | ||
// destructuring assignment Object | ||
for(let prop of node.id.properties) { | ||
if(prop.type === "Property") { | ||
globalNames.add(prop.value.name); | ||
} | ||
} | ||
} else if(node.id.name) { | ||
globalNames.add(node.id.name); | ||
} | ||
}, | ||
// if imports aren’t being transformed to variables assignment, we need those too | ||
ImportSpecifier(node) { | ||
globalNames.add(node.imported.name); | ||
} | ||
}; | ||
walk.simple(parsedAst, types); | ||
return globalNames; | ||
} | ||
_getParseError(code, err) { | ||
// Acorn parsing error on script | ||
let metadata = []; | ||
if(this.options.filePath) { | ||
metadata.push(`file: ${this.options.filePath}`); | ||
} | ||
if(err?.loc?.line) { | ||
metadata.push(`line: ${err.loc.line}`); | ||
} | ||
if(err?.loc?.column) { | ||
metadata.push(`column: ${err.loc.column}`); | ||
} | ||
return new Error(`Had trouble parsing with "acorn"${metadata.length ? ` (${metadata.join(", ")})` : ""}: | ||
Message: ${err.message} | ||
${code}`); | ||
} | ||
async _getGlobalContext(data, options) { | ||
let { | ||
@@ -176,7 +244,13 @@ async: isAsync, | ||
// Warning: This method requires input `data` to be JSON stringify friendly. | ||
// Only use this if the code has `import`: | ||
experimentalModuleApi: this.transformer.hasImports(), | ||
// Don’t use this if vm.Module is supported | ||
// Don’t use this if the code does not contain `import`s | ||
experimentalModuleApi: !IS_VM_MODULES_SUPPORTED && this.transformer.hasImports(), | ||
}, options); | ||
// these things are already supported by Module._compile | ||
if(IS_VM_MODULES_SUPPORTED) { | ||
// Override: don’t use this when modules are allowed. | ||
experimentalModuleApi = false; | ||
} | ||
// These options are already supported by Module._compile | ||
if(experimentalModuleApi) { | ||
@@ -193,6 +267,8 @@ addRequire = false; | ||
}); | ||
} else { | ||
data = data || {}; | ||
} | ||
if(!data) { | ||
data = {}; | ||
} | ||
let context; | ||
@@ -209,55 +285,14 @@ if(experimentalModuleApi || vm.isContext(data)) { | ||
try { | ||
parseCode = RetrieveGlobals._getCode(this.code, { | ||
parseCode = this._getCode(this.code, { | ||
async: isAsync, | ||
}, this.cache); | ||
}); | ||
let parsed = acorn.parse(parseCode, this.acornOptions); | ||
globalNames = new Set(); | ||
walk.simple(parsed, { | ||
FunctionDeclaration(node) { | ||
globalNames.add(node.id.name); | ||
}, | ||
VariableDeclarator(node) { | ||
// destructuring assignment Array | ||
if(node.id.type === "ArrayPattern") { | ||
for(let prop of node.id.elements) { | ||
if(prop.type === "Identifier") { | ||
globalNames.add(prop.name); | ||
} | ||
} | ||
} else if(node.id.type === "ObjectPattern") { | ||
// destructuring assignment Object | ||
for(let prop of node.id.properties) { | ||
if(prop.type === "Property") { | ||
globalNames.add(prop.value.name); | ||
} | ||
} | ||
} else if(node.id.name) { | ||
globalNames.add(node.id.name); | ||
} | ||
} | ||
}); | ||
let parsedAst = acorn.parse(parseCode, this.acornOptions); | ||
globalNames = this.getGlobalNames(parsedAst); | ||
} catch(e) { | ||
// Acorn parsing error on script | ||
let metadata = []; | ||
if(this.options.filePath) { | ||
metadata.push(`file: ${this.options.filePath}`); | ||
} | ||
if(e?.loc?.line) { | ||
metadata.push(`line: ${e.loc.line}`); | ||
} | ||
if(e?.loc?.column) { | ||
metadata.push(`column: ${e.loc.column}`); | ||
} | ||
throw new Error(`Had trouble parsing with "acorn"${metadata.length ? ` (${metadata.join(", ")})` : ""}: | ||
Message: ${e.message} | ||
${parseCode}`); | ||
throw this._getParseError(parseCode, e); | ||
} | ||
try { | ||
let execCode = RetrieveGlobals._getCode(this.code, { | ||
let execCode = this._getCode(this.code, { | ||
async: isAsync, | ||
@@ -269,4 +304,9 @@ globalNames, | ||
if(experimentalModuleApi) { | ||
let m = new Module(); | ||
m._compile(execCode, import.meta.url); | ||
return m.exports; | ||
} | ||
let execOptions = {}; | ||
if(dynamicImport) { | ||
@@ -279,10 +319,46 @@ // Warning: this option is part of the experimental modules API | ||
if(experimentalModuleApi) { | ||
let m = new Module(); | ||
m._compile(execCode, import.meta.url); | ||
return m.exports; | ||
if(IS_VM_MODULES_SUPPORTED) { | ||
// options.initializeImportMeta | ||
let m = new vm.SourceTextModule(execCode, { | ||
context, | ||
initializeImportMeta: (meta, module) => { | ||
meta.url = this.options.filePath || module.identifier; | ||
}, | ||
...execOptions, | ||
}); | ||
// Thank you! https://stackoverflow.com/a/73282303/16711 | ||
await m.link(async (specifier, referencingModule) => { | ||
const mod = await import(specifier); | ||
const exportNames = Object.keys(mod); | ||
return new vm.SyntheticModule( | ||
exportNames, | ||
function () { | ||
exportNames.forEach(key => { | ||
this.setExport(key, mod[key]) | ||
}); | ||
}, | ||
{ | ||
identifier: specifier, | ||
context: referencingModule.context | ||
} | ||
); | ||
}); | ||
await m.evaluate(); | ||
// TODO (feature) incorporate other esm `exports` here | ||
return m.namespace.default; | ||
} | ||
return vm.runInContext(execCode, context, execOptions); | ||
} catch(e) { | ||
throw new Error(`Had trouble executing script in Node: | ||
let type = "cjs"; | ||
if(IS_VM_MODULES_SUPPORTED) { | ||
type = "esm"; | ||
} else if(experimentalModuleApi) { | ||
type = "cjs-experimental"; | ||
} | ||
throw new Error(`Had trouble executing Node script (type: ${type}): | ||
Message: ${e.message} | ||
@@ -294,14 +370,6 @@ | ||
getGlobalContextSync(data, options) { | ||
let ret = this._getGlobalContext(data, Object.assign({ | ||
async: false, | ||
}, options)); | ||
this._setContextPrototype(ret); | ||
return ret; | ||
} | ||
async getGlobalContext(data, options) { | ||
let ret = await this._getGlobalContext(data, Object.assign({ | ||
// whether or not the target code is executed asynchronously | ||
// note that vm.Module will always be async-friendly | ||
async: true, | ||
@@ -308,0 +376,0 @@ }, options)); |
import test from "ava"; | ||
import { RetrieveGlobals } from "../retrieveGlobals.js"; | ||
import { isSupported } from "../vmModules.js"; | ||
test("var", t => { | ||
let vm = new RetrieveGlobals("var a = 1;"); | ||
t.deepEqual(vm.getGlobalContextSync(), { a: 1 }); | ||
const IS_VM_MODULES_SUPPORTED = isSupported(); | ||
test("var", async t => { | ||
let vm = new RetrieveGlobals(`var a = 1;`); | ||
t.deepEqual(await vm.getGlobalContext(), { a: 1 }); | ||
}); | ||
test("isPlainObject", t => { | ||
test("isPlainObject", async t => { | ||
// from eleventy-utils | ||
@@ -20,6 +23,6 @@ function isPlainObject(value) { | ||
let vm = new RetrieveGlobals("var a = 1;"); | ||
t.true(isPlainObject(vm.getGlobalContextSync())); | ||
t.true(isPlainObject(await vm.getGlobalContext())); | ||
}); | ||
test("isPlainObject deep", t => { | ||
test("isPlainObject deep", async t => { | ||
// from eleventy-utils | ||
@@ -35,3 +38,3 @@ function isPlainObject(value) { | ||
let vm = new RetrieveGlobals("var a = { b: 1, c: { d: {} } };"); | ||
let obj = vm.getGlobalContextSync(); | ||
let obj = await vm.getGlobalContext(); | ||
t.true(isPlainObject(obj.a.c)); | ||
@@ -41,3 +44,3 @@ t.true(isPlainObject(obj.a.c.d)); | ||
test("isPlainObject deep circular", t => { | ||
test("isPlainObject deep circular", async t => { | ||
// from eleventy-utils | ||
@@ -57,3 +60,3 @@ function isPlainObject(value) { | ||
`); | ||
let obj = vm.getGlobalContextSync(); | ||
let obj = await vm.getGlobalContext(); | ||
t.true(isPlainObject(obj.a.b)); | ||
@@ -64,20 +67,20 @@ t.true(isPlainObject(obj.b.b)); | ||
test("var with data", t => { | ||
test("var with data", async t => { | ||
let vm = new RetrieveGlobals("var a = b;"); | ||
t.deepEqual(vm.getGlobalContextSync({ b: 2 }), { a: 2 }); | ||
t.deepEqual(await vm.getGlobalContext({ b: 2 }), { a: 2 }); | ||
}); | ||
test("let with data", t => { | ||
test("let with data", async t => { | ||
let vm = new RetrieveGlobals("let a = b;"); | ||
t.deepEqual(vm.getGlobalContextSync({ b: 2 }), { a: 2 }); | ||
t.deepEqual(await vm.getGlobalContext({ b: 2 }), { a: 2 }); | ||
}); | ||
test("const with data", t => { | ||
test("const with data", async t => { | ||
let vm = new RetrieveGlobals("const a = b;"); | ||
t.deepEqual(vm.getGlobalContextSync({ b: 2 }), { a: 2 }); | ||
t.deepEqual(await vm.getGlobalContext({ b: 2 }), { a: 2 }); | ||
}); | ||
test("function", t => { | ||
test("function", async t => { | ||
let vm = new RetrieveGlobals("function testFunction() {}"); | ||
let ret = vm.getGlobalContextSync(); | ||
let ret = await vm.getGlobalContext(); | ||
t.true(typeof ret.testFunction === "function"); | ||
@@ -122,3 +125,3 @@ }); | ||
test("global: Same URL", async t => { | ||
let vm = new RetrieveGlobals(`const b = URL`); | ||
let vm = new RetrieveGlobals(`const b = URL;`); | ||
let ret = await vm.getGlobalContext(undefined, { | ||
@@ -130,5 +133,5 @@ reuseGlobal: true | ||
test("return array", t => { | ||
test("return array", async t => { | ||
let vm = new RetrieveGlobals("let b = [1,2,3];"); | ||
let globals = vm.getGlobalContextSync(); | ||
let globals = await vm.getGlobalContext(); | ||
t.true(Array.isArray(globals.b)); | ||
@@ -148,10 +151,12 @@ t.deepEqual(globals.b, [1,2,3]); | ||
// TODO we detect `await import()` and use `experimentalModuleApi: true` | ||
test.skip("dynamic import (no code transformation) (requires --experimental-vm-modules in Node v20.10)", async t => { | ||
let vm = new RetrieveGlobals(`const { noop } = await import("@zachleat/noop");`); | ||
let ret = await vm.getGlobalContext(undefined, { | ||
dynamicImport: true | ||
// Works with --experimental-vm-modules, remove this when modules are stable | ||
if(IS_VM_MODULES_SUPPORTED) { | ||
test("dynamic import (no code transformation) (requires --experimental-vm-modules in Node v20.10)", async t => { | ||
let vm = new RetrieveGlobals(`const { noop } = await import("@zachleat/noop");`); | ||
let ret = await vm.getGlobalContext(undefined, { | ||
dynamicImport: true | ||
}); | ||
t.is(typeof ret.noop, "function"); | ||
}); | ||
t.is(typeof ret.noop, "function"); | ||
}); | ||
} | ||
@@ -161,2 +166,3 @@ test("dynamic import (no code transformation) (experimentalModuleApi explicit true)", async t => { | ||
let ret = await vm.getGlobalContext(undefined, { | ||
dynamicImport: true, // irrelevant for fallback case, important for --experimental-vm-modules support case | ||
experimentalModuleApi: true, // Needs to be true here because there are no top level `import` | ||
@@ -196,8 +202,9 @@ }); | ||
test("ESM import (experimentalModuleApi explicit true)", async t => { | ||
// let vm = new RetrieveGlobals(`import { noop } from "@zachleat/noop"; | ||
let vm = new RetrieveGlobals(`import { noop } from "@zachleat/noop"; | ||
const b = 1;`, { | ||
transformEsmImports: true, | ||
transformEsmImports: true, // overridden to false when --experimental-vm-modules | ||
}); | ||
let ret = await vm.getGlobalContext(undefined, { | ||
experimentalModuleApi: true, | ||
experimentalModuleApi: true, // overridden to false when --experimental-vm-modules | ||
}); | ||
@@ -237,7 +244,28 @@ t.is(typeof ret.noop, "function"); | ||
}); | ||
await t.throwsAsync(async () => { | ||
if(IS_VM_MODULES_SUPPORTED) { | ||
// Works fine with --experimental-vm-modules | ||
let ret = await vm.getGlobalContext({ fn: function() {} }, { | ||
// experimentalModuleApi: true, // implied true | ||
// experimentalModuleApi: true, // implied false | ||
}); | ||
t.is(typeof ret.b, "function"); | ||
} else { | ||
// This throws if --experimental-vm-modules not set | ||
await t.throwsAsync(async () => { | ||
let ret = await vm.getGlobalContext({ fn: function() {} }, { | ||
// experimentalModuleApi: true, // implied true | ||
}); | ||
}); | ||
} | ||
}); | ||
if(IS_VM_MODULES_SUPPORTED) { | ||
test("import.meta.url works", async t => { | ||
let vm = new RetrieveGlobals(`const b = import.meta.url;`, { | ||
filePath: import.meta.url | ||
}); | ||
let ret = await vm.getGlobalContext(); | ||
t.is(ret.b, import.meta.url); | ||
}); | ||
}); | ||
} |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
23813
7
558
3
85
4
Updatedacorn-walk@^8.3.1