diff options
Diffstat (limited to 'node_modules/node-retrieve-globals/retrieveGlobals.js')
| -rw-r--r-- | node_modules/node-retrieve-globals/retrieveGlobals.js | 376 |
1 files changed, 376 insertions, 0 deletions
diff --git a/node_modules/node-retrieve-globals/retrieveGlobals.js b/node_modules/node-retrieve-globals/retrieveGlobals.js new file mode 100644 index 0000000..64571a8 --- /dev/null +++ b/node_modules/node-retrieve-globals/retrieveGlobals.js @@ -0,0 +1,376 @@ +import vm from "vm"; +import * as acorn from "acorn"; +import * as walk from "acorn-walk"; +import { ImportTransformer } from "esm-import-transformer"; +import { createRequire, Module } from "module"; + +import { getWorkingDirectory } from "./util/getWorkingDirectory.js"; +import { isSupported } from "./util/vmModules.js"; + +const IS_VM_MODULES_SUPPORTED = isSupported(); + +// `import` and `require` should both be relative to working directory (not this file) +const WORKING_DIRECTORY = getWorkingDirectory(); + +// TODO (feature) option to change `require` home base +const customRequire = createRequire(WORKING_DIRECTORY); + +class RetrieveGlobals { + constructor(code, options) { + this.originalCode = code; + + // backwards compat + if(typeof options === "string") { + options = { + filePath: options + }; + } + + this.options = Object.assign({ + filePath: null, + transformEsmImports: false, + }, options); + + if(IS_VM_MODULES_SUPPORTED) { + // Override: no code transformations if vm.Module works + this.options.transformEsmImports = false; + } + + // set defaults + let acornOptions = {}; + 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. + // Doesn’t currently work with aliases (mod as name) or namespaced imports (* as name). + if(this.options.transformEsmImports) { + this.code = this.transformer.transformToDynamicImport(); + } else { + this.code = this.originalCode; + } + } + + get transformer() { + if(!this._transformer) { + this._transformer = new ImportTransformer(this.originalCode); + } + return this._transformer; + } + + setAcornOptions(acornOptions) { + this.acornOptions = Object.assign({ + ecmaVersion: "latest", + }, acornOptions ); + } + + setCreateContextOptions(contextOptions) { + this.createContextOptions = Object.assign({ + codeGeneration: { + strings: false, + wasm: false, + } + }, contextOptions ); + } + + static _getProxiedContext(context = {}, options = {}) { + return new Proxy(context, { + get(target, propertyName) { + if(Reflect.has(target, propertyName)) { + return Reflect.get(target, propertyName); + } + + if(options.reuseGlobal && Reflect.has(global, propertyName)) { + return global[propertyName]; + } + if(options.addRequire && propertyName === "require") { + return customRequire; + } + } + }); + } + + // We prune function and variable declarations that aren’t globally declared + // (our acorn walker could be improved to skip non-global declarations, but this method is easier for now) + static _getGlobalVariablesReturnString(names, mode = "cjs") { + let s = [`let __globals = {};`]; + for(let name of names) { + s.push(`if( typeof ${name} !== "undefined") { __globals.${name} = ${name}; }`); + } + return `${s.join("\n")};${mode === "esm" ? "\nexport default __globals;" : "return __globals;"}` + } + + _setContextPrototype(context) { + // Context will fail isPlainObject and won’t be merged in the data cascade properly without this prototype set + // See https://github.com/11ty/eleventy-utils/blob/main/src/IsPlainObject.js + if(!context || typeof context !== "object" || Array.isArray(context)) { + return; + } + if(context instanceof Date) { + return; + } + + if(!Object.getPrototypeOf(context).isPrototypeOf(Object.create({}))) { + Object.setPrototypeOf(context, Object.prototype); + // Go deep + for(let key in context) { + this._setContextPrototype(context[key]); + } + } + } + + _getCode(code, options) { + let { async: isAsync, globalNames, experimentalModuleApi, data } = Object.assign({ + async: true + }, options); + + if(IS_VM_MODULES_SUPPORTED) { + return `${code} + +${globalNames ? RetrieveGlobals._getGlobalVariablesReturnString(globalNames, "esm") : ""}`; + } + + let prefix = []; + let argKeys = ""; + let argValues = ""; + + // Don’t use this when vm.Module is stable (or if the code doesn’t have any imports!) + if(experimentalModuleApi) { + prefix = "module.exports = "; + + if(typeof data === "object") { + let dataKeys = Object.keys(data); + if(dataKeys) { + argKeys = `{${dataKeys.join(",")}}`; + argValues = JSON.stringify(data, function replacer(key, value) { + if(typeof value === "function") { + throw new Error(`When using \`experimentalModuleApi\`, context data must be JSON.stringify friendly. The "${key}" property was type \`function\`.`); + } + return value; + }); + } + } + } + + return `${prefix}(${isAsync ? "async " : ""}function(${argKeys}) { + ${code} + ${globalNames ? RetrieveGlobals._getGlobalVariablesReturnString(globalNames, "cjs") : ""} +})(${argValues});`; + } + + 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 { + async: isAsync, + reuseGlobal, + dynamicImport, + addRequire, + experimentalModuleApi, + } = Object.assign({ + // defaults + async: true, + + reuseGlobal: false, + + // adds support for `require` + addRequire: false, + + // allows dynamic import in `vm` (requires --experimental-vm-modules in Node v20.10+) + // https://github.com/nodejs/node/issues/51154 + // TODO Another workaround possibility: We could use `import` outside of `vm` and inject the dependencies into context `data` + dynamicImport: false, + + // Use Module._compile instead of vm + // Workaround for: https://github.com/zachleat/node-retrieve-globals/issues/2 + // Warning: This method requires input `data` to be JSON stringify friendly. + // 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); + + 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) { + addRequire = false; + dynamicImport = false; + } + + if(reuseGlobal || addRequire) { + // Re-use the parent `global` https://nodejs.org/api/globals.html + data = RetrieveGlobals._getProxiedContext(data || {}, { + reuseGlobal, + addRequire, + }); + } + + if(!data) { + data = {}; + } + + let context; + if(experimentalModuleApi || vm.isContext(data)) { + context = data; + } else { + context = vm.createContext(data, this.createContextOptions); + } + + let parseCode; + let globalNames; + + try { + parseCode = this._getCode(this.code, { + async: isAsync, + }); + + let parsedAst = acorn.parse(parseCode, this.acornOptions); + globalNames = this.getGlobalNames(parsedAst); + } catch(e) { + throw this._getParseError(parseCode, e); + } + + try { + let execCode = this._getCode(this.code, { + async: isAsync, + globalNames, + experimentalModuleApi, + data: context, + }); + + if(experimentalModuleApi) { + let m = new Module(); + m._compile(execCode, WORKING_DIRECTORY); + return m.exports; + } + + let execOptions = {}; + if(dynamicImport) { + // Warning: this option is part of the experimental modules API + execOptions.importModuleDynamically = (specifier) => import(specifier); + } + + if(IS_VM_MODULES_SUPPORTED) { + // options.initializeImportMeta + let m = new vm.SourceTextModule(execCode, { + context, + initializeImportMeta: (meta, module) => { + meta.url = this.options.filePath || WORKING_DIRECTORY || 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) { + 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} + +${this.code}`); + } + } + + 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, + }, options)); + + this._setContextPrototype(ret); + + return ret; + } +} + +export { RetrieveGlobals }; |
