Previously when managing the importModuleDynamically callback of vm.compileFunction(), we use an ID number as the host defined option and maintain a per-Environment ID -> CompiledFnEntry map to retain the top-level referrer function returned by vm.compileFunction() in order to pass it back to the callback, but it would leak because with how we used v8::Persistent to maintain this reference, V8 would not be able to understand the cycle and would just think that the CompiledFnEntry was supposed to live forever. We made an attempt to make that reference known to V8 by making the CompiledFnEntry weak and using a private symbol to make CompiledFnEntry strongly references the top-level referrer function in https://github.com/nodejs/node/pull/46785, but that turned out to be unsound, because the there's no guarantee that the top-level function must be alive while import() can still be initiated from that function, since V8 could discard the top-level function and only keep inner functions alive, so relying on the top-level function to keep the CompiledFnEntry alive could result in use-after-free which caused a revert of that fix. With this patch we use a symbol in the host defined options instead of a number, because with the stage-3 symbol-as-weakmap-keys proposal we could directly use that symbol to keep the referrer alive using a WeakMap. As a bonus this also keeps the other kinds of referrers alive as long as import() can still be initiated from that Script/Module, so this also fixes the long-standing crash caused by vm.Script being GC'ed too early when its importModuleDynamically callback still needs it. PR-URL: https://github.com/nodejs/node/pull/48510 Backport-PR-URL: https://github.com/nodejs/node/pull/51004 Refs: https://github.com/nodejs/node/issues/44211 Refs: https://github.com/nodejs/node/issues/42080 Refs: https://github.com/nodejs/node/issues/47096 Refs: https://github.com/nodejs/node/issues/43205 Refs: https://github.com/nodejs/node/issues/38695 Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: Stephen Belanger <admin@stephenbelanger.com>
97 lines
2.9 KiB
JavaScript
97 lines
2.9 KiB
JavaScript
'use strict';
|
|
|
|
const {
|
|
ArrayPrototypeJoin,
|
|
ArrayPrototypeMap,
|
|
JSONStringify,
|
|
ObjectCreate,
|
|
SafeSet,
|
|
} = primordials;
|
|
|
|
let debug = require('internal/util/debuglog').debuglog('esm', (fn) => {
|
|
debug = fn;
|
|
});
|
|
|
|
/**
|
|
* Creates an import statement for a given module path and index.
|
|
* @param {string} impt - The module path to import.
|
|
* @param {number} index - The index of the import statement.
|
|
*/
|
|
function createImport(impt, index) {
|
|
const imptPath = JSONStringify(impt);
|
|
return `import * as $import_${index} from ${imptPath};
|
|
import.meta.imports[${imptPath}] = $import_${index};`;
|
|
}
|
|
|
|
/**
|
|
* Creates an export for a given module.
|
|
* @param {string} expt - The name of the export.
|
|
* @param {number} index - The index of the export statement.
|
|
*/
|
|
function createExport(expt, index) {
|
|
const nameStringLit = JSONStringify(expt);
|
|
return `let $export_${index};
|
|
export { $export_${index} as ${nameStringLit} };
|
|
import.meta.exports[${nameStringLit}] = {
|
|
get: () => $export_${index},
|
|
set: (v) => $export_${index} = v,
|
|
};`;
|
|
}
|
|
|
|
/**
|
|
* Creates a dynamic module with the given imports, exports, URL, and evaluate function.
|
|
* @param {string[]} imports - An array of imports.
|
|
* @param {string[]} exports - An array of exports.
|
|
* @param {string} [url=''] - The URL of the module.
|
|
* @param {(reflect: DynamicModuleReflect) => void} evaluate - The function to evaluate the module.
|
|
* @typedef {object} DynamicModuleReflect
|
|
* @property {string[]} imports - The imports of the module.
|
|
* @property {string[]} exports - The exports of the module.
|
|
* @property {(cb: (reflect: DynamicModuleReflect) => void) => void} onReady - Callback to evaluate the module.
|
|
*/
|
|
const createDynamicModule = (imports, exports, url = '', evaluate) => {
|
|
debug('creating ESM facade for %s with exports: %j', url, exports);
|
|
const source = `
|
|
${ArrayPrototypeJoin(ArrayPrototypeMap(imports, createImport), '\n')}
|
|
${ArrayPrototypeJoin(ArrayPrototypeMap(exports, createExport), '\n')}
|
|
import.meta.done();
|
|
`;
|
|
const { ModuleWrap } = internalBinding('module_wrap');
|
|
const m = new ModuleWrap(`${url}`, undefined, source, 0, 0);
|
|
|
|
const readyfns = new SafeSet();
|
|
/** @type {DynamicModuleReflect} */
|
|
const reflect = {
|
|
exports: ObjectCreate(null),
|
|
onReady: (cb) => { readyfns.add(cb); },
|
|
};
|
|
|
|
if (imports.length) {
|
|
reflect.imports = { __proto__: null };
|
|
}
|
|
const { registerModule } = require('internal/modules/esm/utils');
|
|
registerModule(m, {
|
|
__proto__: null,
|
|
initializeImportMeta: (meta, wrap) => {
|
|
meta.exports = reflect.exports;
|
|
if (reflect.imports) {
|
|
meta.imports = reflect.imports;
|
|
}
|
|
meta.done = () => {
|
|
evaluate(reflect);
|
|
reflect.onReady = (cb) => cb(reflect);
|
|
for (const fn of readyfns) {
|
|
readyfns.delete(fn);
|
|
fn(reflect);
|
|
}
|
|
};
|
|
},
|
|
});
|
|
|
|
return {
|
|
module: m,
|
|
reflect,
|
|
};
|
|
};
|
|
|
|
module.exports = createDynamicModule;
|