module: use v8 synthetic modules

PR-URL: https://github.com/nodejs/node/pull/29846
Reviewed-By: Gus Caplan <me@gus.host>
Reviewed-By: Minwoo Jung <minwoo@nodesource.com>
This commit is contained in:
Guy Bedford 2019-10-04 13:37:27 -04:00
parent 0521a98fd6
commit ffd22e8198
7 changed files with 173 additions and 96 deletions

View File

@ -150,8 +150,7 @@ function NativeModule(id) {
this.filename = `${id}.js`;
this.id = id;
this.exports = {};
this.reflect = undefined;
this.esmFacade = undefined;
this.module = undefined;
this.exportKeys = undefined;
this.loaded = false;
this.loading = false;
@ -240,16 +239,18 @@ NativeModule.prototype.getURL = function() {
};
NativeModule.prototype.getESMFacade = function() {
if (this.esmFacade) return this.esmFacade;
const createDynamicModule = nativeModuleRequire(
'internal/modules/esm/create_dynamic_module');
if (this.module) return this.module;
const { ModuleWrap } = internalBinding('module_wrap');
const url = this.getURL();
return this.esmFacade = createDynamicModule(
[], [...this.exportKeys, 'default'], url, (reflect) => {
this.reflect = reflect;
this.syncExports();
reflect.exports.default.set(this.exports);
});
const nativeModule = this;
this.module = new ModuleWrap(function() {
nativeModule.syncExports();
this.setExport('default', nativeModule.exports);
}, [...this.exportKeys, 'default'], url);
// Ensure immediate sync execution to capture exports now
this.module.instantiate();
this.module.evaluate(-1, false);
return this.module;
};
// Provide named exports for all builtin libraries so that the libraries
@ -258,13 +259,12 @@ NativeModule.prototype.getESMFacade = function() {
// called so that APMs and other behavior are supported.
NativeModule.prototype.syncExports = function() {
const names = this.exportKeys;
if (this.reflect) {
if (this.module) {
for (let i = 0; i < names.length; i++) {
const exportName = names[i];
if (exportName === 'default') continue;
this.reflect.exports[exportName].set(
getOwn(this.exports, exportName, this.exports)
);
this.module.setExport(exportName,
getOwn(this.exports, exportName, this.exports));
}
}
};

View File

@ -74,9 +74,7 @@ const experimentalExports = getOptionValue('--experimental-exports');
module.exports = { wrapSafe, Module };
let asyncESM;
let ModuleJob;
let createDynamicModule;
let asyncESM, ModuleJob, ModuleWrap, kInstantiated;
const {
CHAR_FORWARD_SLASH,
@ -819,21 +817,18 @@ Module.prototype.load = function(filename) {
const module = ESMLoader.moduleMap.get(url);
// Create module entry at load time to snapshot exports correctly
const exports = this.exports;
if (module !== undefined) { // Called from cjs translator
if (module.reflect) {
module.reflect.onReady((reflect) => {
reflect.exports.default.set(exports);
});
}
// Called from cjs translator
if (module !== undefined && module.module !== undefined) {
if (module.module.getStatus() >= kInstantiated)
module.module.setExport('default', exports);
} else { // preemptively cache
ESMLoader.moduleMap.set(
url,
new ModuleJob(ESMLoader, url, async () => {
return createDynamicModule(
[], ['default'], url, (reflect) => {
reflect.exports.default.set(exports);
});
})
new ModuleJob(ESMLoader, url, () =>
new ModuleWrap(function() {
this.setExport('default', exports);
}, ['default'], url)
)
);
}
}
@ -1150,6 +1145,5 @@ Module.Module = Module;
if (experimentalModules) {
asyncESM = require('internal/process/esm_loader');
ModuleJob = require('internal/modules/esm/module_job');
createDynamicModule = require(
'internal/modules/esm/create_dynamic_module');
({ ModuleWrap, kInstantiated } = internalBinding('module_wrap'));
}

View File

@ -117,12 +117,7 @@ class Loader {
source,
url = pathToFileURL(`${process.cwd()}/[eval${++this.evalIndex}]`).href
) {
const evalInstance = async (url) => {
return {
module: new ModuleWrap(source, url),
reflect: undefined
};
};
const evalInstance = (url) => new ModuleWrap(source, url);
const job = new ModuleJob(this, url, evalInstance, false);
this.moduleMap.set(url, job);
const { module, result } = await job.run();
@ -165,7 +160,7 @@ class Loader {
return createDynamicModule([], exports, url, (reflect) => {
debug(`Loading dynamic ${url}`);
execute(reflect.exports);
});
}).module;
};
} else {
if (!translators.has(format))

View File

@ -30,19 +30,17 @@ class ModuleJob {
// onto `this` by `link()` below once it has been resolved.
this.modulePromise = moduleProvider.call(loader, url, isMain);
this.module = undefined;
this.reflect = undefined;
// Wait for the ModuleWrap instance being linked with all dependencies.
const link = async () => {
({ module: this.module,
reflect: this.reflect } = await this.modulePromise);
this.module = await this.modulePromise;
assert(this.module instanceof ModuleWrap);
const dependencyJobs = [];
const promises = this.module.link(async (specifier) => {
const jobPromise = this.loader.getModuleJob(specifier, url);
dependencyJobs.push(jobPromise);
return (await (await jobPromise).modulePromise).module;
return (await jobPromise).modulePromise;
});
if (promises !== undefined)

View File

@ -32,6 +32,8 @@ const {
const readFileAsync = promisify(fs.readFile);
const JsonParse = JSON.parse;
const { maybeCacheSourceMap } = require('internal/source_map/source_map_cache');
const moduleWrap = internalBinding('module_wrap');
const { ModuleWrap } = moduleWrap;
const debug = debuglog('esm');
@ -77,22 +79,18 @@ translators.set('module', async function moduleStrategy(url) {
const source = `${await getSource(url)}`;
maybeCacheSourceMap(url, source);
debug(`Translating StandardModule ${url}`);
const { ModuleWrap, callbackMap } = internalBinding('module_wrap');
const module = new ModuleWrap(source, url);
callbackMap.set(module, {
moduleWrap.callbackMap.set(module, {
initializeImportMeta,
importModuleDynamically,
});
return {
module,
reflect: undefined,
};
return module;
});
// Strategy for loading a node-style CommonJS module
const isWindows = process.platform === 'win32';
const winSepRegEx = /\//g;
translators.set('commonjs', async function commonjsStrategy(url, isMain) {
translators.set('commonjs', function commonjsStrategy(url, isMain) {
debug(`Translating CJSModule ${url}`);
const pathname = internalURLModule.fileURLToPath(new URL(url));
const cached = this.cjsCache.get(url);
@ -105,17 +103,17 @@ translators.set('commonjs', async function commonjsStrategy(url, isMain) {
];
if (module && module.loaded) {
const exports = module.exports;
return createDynamicModule([], ['default'], url, (reflect) => {
reflect.exports.default.set(exports);
});
return new ModuleWrap(function() {
this.setExport('default', exports);
}, ['default'], url);
}
return createDynamicModule([], ['default'], url, () => {
return new ModuleWrap(function() {
debug(`Loading CJSModule ${url}`);
// We don't care about the return val of _load here because Module#load
// will handle it for us by checking the loader registry and filling the
// exports like above
CJSModule._load(pathname, undefined, isMain);
});
}, ['default'], url);
});
// Strategy for loading a node builtin CommonJS module that isn't
@ -145,9 +143,9 @@ translators.set('json', async function jsonStrategy(url) {
module = CJSModule._cache[modulePath];
if (module && module.loaded) {
const exports = module.exports;
return createDynamicModule([], ['default'], url, (reflect) => {
reflect.exports.default.set(exports);
});
return new ModuleWrap(function() {
this.setExport('default', exports);
}, ['default'], url);
}
}
const content = `${await getSource(url)}`;
@ -158,9 +156,9 @@ translators.set('json', async function jsonStrategy(url) {
module = CJSModule._cache[modulePath];
if (module && module.loaded) {
const exports = module.exports;
return createDynamicModule(['default'], url, (reflect) => {
reflect.exports.default.set(exports);
});
return new ModuleWrap(function() {
this.setExport('default', exports);
}, ['default'], url);
}
}
try {
@ -180,10 +178,10 @@ translators.set('json', async function jsonStrategy(url) {
if (pathname) {
CJSModule._cache[modulePath] = module;
}
return createDynamicModule([], ['default'], url, (reflect) => {
return new ModuleWrap(function() {
debug(`Parsing JSONModule ${url}`);
reflect.exports.default.set(module.exports);
});
this.setExport('default', module.exports);
}, ['default'], url);
});
// Strategy for loading a wasm module
@ -206,5 +204,5 @@ translators.set('wasm', async function(url) {
const { exports } = new WebAssembly.Instance(compiled, reflect.imports);
for (const expt of Object.keys(exports))
reflect.exports[expt].set(exports[expt]);
});
}).module;
});

View File

@ -98,6 +98,9 @@ ModuleWrap* ModuleWrap::GetFromID(Environment* env, uint32_t id) {
return module_wrap_it->second;
}
// new ModuleWrap(source, url)
// new ModuleWrap(source, url, context?, lineOffset, columnOffset)
// new ModuleWrap(syntheticExecutionFunction, export_names, url)
void ModuleWrap::New(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
Isolate* isolate = env->isolate();
@ -108,12 +111,6 @@ void ModuleWrap::New(const FunctionCallbackInfo<Value>& args) {
const int argc = args.Length();
CHECK_GE(argc, 2);
CHECK(args[0]->IsString());
Local<String> source_text = args[0].As<String>();
CHECK(args[1]->IsString());
Local<String> url = args[1].As<String>();
Local<Context> context;
Local<Integer> line_offset;
Local<Integer> column_offset;
@ -143,8 +140,7 @@ void ModuleWrap::New(const FunctionCallbackInfo<Value>& args) {
column_offset = Integer::New(isolate, 0);
}
ShouldNotAbortOnUncaughtScope no_abort_scope(env);
TryCatchScope try_catch(env);
Local<String> url;
Local<Module> module;
Local<PrimitiveArray> host_defined_options =
@ -152,29 +148,60 @@ void ModuleWrap::New(const FunctionCallbackInfo<Value>& args) {
host_defined_options->Set(isolate, HostDefinedOptions::kType,
Number::New(isolate, ScriptType::kModule));
// compile
{
ScriptOrigin origin(url,
line_offset, // line offset
column_offset, // column offset
True(isolate), // is cross origin
Local<Integer>(), // script id
Local<Value>(), // source map URL
False(isolate), // is opaque (?)
False(isolate), // is WASM
True(isolate), // is ES Module
host_defined_options);
Context::Scope context_scope(context);
ScriptCompiler::Source source(source_text, origin);
if (!ScriptCompiler::CompileModule(isolate, &source).ToLocal(&module)) {
if (try_catch.HasCaught() && !try_catch.HasTerminated()) {
CHECK(!try_catch.Message().IsEmpty());
CHECK(!try_catch.Exception().IsEmpty());
AppendExceptionLine(env, try_catch.Exception(), try_catch.Message(),
ErrorHandlingMode::MODULE_ERROR);
try_catch.ReThrow();
// new ModuleWrap(syntheticExecutionFunction, export_names, url)
bool synthetic = args[0]->IsFunction();
if (synthetic) {
CHECK(args[1]->IsArray());
Local<Array> export_names_arr = args[1].As<Array>();
uint32_t len = export_names_arr->Length();
std::vector<Local<String>> export_names(len);
for (uint32_t i = 0; i < len; i++) {
Local<Value> export_name_val =
export_names_arr->Get(context, i).ToLocalChecked();
CHECK(export_name_val->IsString());
export_names[i] = export_name_val.As<String>();
}
CHECK(args[2]->IsString());
url = args[2].As<String>();
module = Module::CreateSyntheticModule(isolate, url, export_names,
SyntheticModuleEvaluationStepsCallback);
// Compile
} else {
CHECK(args[0]->IsString());
Local<String> source_text = args[0].As<String>();
CHECK(args[1]->IsString());
url = args[1].As<String>();
ShouldNotAbortOnUncaughtScope no_abort_scope(env);
TryCatchScope try_catch(env);
{
ScriptOrigin origin(url,
line_offset, // line offset
column_offset, // column offset
True(isolate), // is cross origin
Local<Integer>(), // script id
Local<Value>(), // source map URL
False(isolate), // is opaque (?)
False(isolate), // is WASM
True(isolate), // is ES Module
host_defined_options);
Context::Scope context_scope(context);
ScriptCompiler::Source source(source_text, origin);
if (!ScriptCompiler::CompileModule(isolate, &source).ToLocal(&module)) {
if (try_catch.HasCaught() && !try_catch.HasTerminated()) {
CHECK(!try_catch.Message().IsEmpty());
CHECK(!try_catch.Exception().IsEmpty());
AppendExceptionLine(env, try_catch.Exception(), try_catch.Message(),
ErrorHandlingMode::MODULE_ERROR);
try_catch.ReThrow();
}
return;
}
return;
}
}
@ -183,6 +210,13 @@ void ModuleWrap::New(const FunctionCallbackInfo<Value>& args) {
}
ModuleWrap* obj = new ModuleWrap(env, that, module, url);
if (synthetic) {
obj->synthetic_ = true;
obj->synthetic_evaluation_steps_.Reset(
env->isolate(), args[0].As<Function>());
}
obj->context_.Reset(isolate, context);
env->hash_to_module_map.emplace(module->GetIdentityHash(), obj);
@ -307,6 +341,10 @@ void ModuleWrap::Evaluate(const FunctionCallbackInfo<Value>& args) {
result = module->Evaluate(context);
}
if (result.IsEmpty()) {
CHECK(try_catch.HasCaught());
}
// Convert the termination exception into a regular exception.
if (timed_out || received_signal) {
if (!env->is_main_thread() && env->is_stopping())
@ -1295,7 +1333,7 @@ static MaybeLocal<Promise> ImportModuleDynamically(
Local<Value> result;
if (import_callback->Call(
context,
v8::Undefined(iso),
Undefined(iso),
arraysize(import_args),
import_args).ToLocal(&result)) {
CHECK(result->IsPromise());
@ -1355,6 +1393,52 @@ void ModuleWrap::SetInitializeImportMetaObjectCallback(
HostInitializeImportMetaObjectCallback);
}
MaybeLocal<Value> ModuleWrap::SyntheticModuleEvaluationStepsCallback(
Local<Context> context, Local<Module> module) {
Environment* env = Environment::GetCurrent(context);
Isolate* isolate = env->isolate();
ModuleWrap* obj = GetFromModule(env, module);
TryCatchScope try_catch(env);
Local<Function> synthetic_evaluation_steps =
obj->synthetic_evaluation_steps_.Get(isolate);
MaybeLocal<Value> ret = synthetic_evaluation_steps->Call(context,
obj->object(), 0, nullptr);
if (ret.IsEmpty()) {
CHECK(try_catch.HasCaught());
}
obj->synthetic_evaluation_steps_.Reset();
if (try_catch.HasCaught() && !try_catch.HasTerminated()) {
CHECK(!try_catch.Message().IsEmpty());
CHECK(!try_catch.Exception().IsEmpty());
try_catch.ReThrow();
return MaybeLocal<Value>();
}
return Undefined(isolate);
}
void ModuleWrap::SetSyntheticExport(
const v8::FunctionCallbackInfo<v8::Value>& args) {
Isolate* isolate = args.GetIsolate();
Local<Object> that = args.This();
ModuleWrap* obj;
ASSIGN_OR_RETURN_UNWRAP(&obj, that);
CHECK(obj->synthetic_);
CHECK_EQ(args.Length(), 2);
CHECK(args[0]->IsString());
Local<String> export_name = args[0].As<String>();
Local<Value> export_value = args[1];
Local<Module> module = obj->module_.Get(isolate);
module->SetSyntheticModuleExport(export_name, export_value);
}
void ModuleWrap::Initialize(Local<Object> target,
Local<Value> unused,
Local<Context> context,
@ -1369,6 +1453,7 @@ void ModuleWrap::Initialize(Local<Object> target,
env->SetProtoMethod(tpl, "link", Link);
env->SetProtoMethod(tpl, "instantiate", Instantiate);
env->SetProtoMethod(tpl, "evaluate", Evaluate);
env->SetProtoMethod(tpl, "setExport", SetSyntheticExport);
env->SetProtoMethodNoSideEffect(tpl, "getNamespace", GetNamespace);
env->SetProtoMethodNoSideEffect(tpl, "getStatus", GetStatus);
env->SetProtoMethodNoSideEffect(tpl, "getError", GetError);

View File

@ -69,12 +69,19 @@ class ModuleWrap : public BaseObject {
const v8::FunctionCallbackInfo<v8::Value>& args);
static void SetInitializeImportMetaObjectCallback(
const v8::FunctionCallbackInfo<v8::Value>& args);
static v8::MaybeLocal<v8::Value> SyntheticModuleEvaluationStepsCallback(
v8::Local<v8::Context> context, v8::Local<v8::Module> module);
static void SetSyntheticExport(
const v8::FunctionCallbackInfo<v8::Value>& args);
static v8::MaybeLocal<v8::Module> ResolveCallback(
v8::Local<v8::Context> context,
v8::Local<v8::String> specifier,
v8::Local<v8::Module> referrer);
static ModuleWrap* GetFromModule(node::Environment*, v8::Local<v8::Module>);
v8::Global<v8::Function> synthetic_evaluation_steps_;
bool synthetic_ = false;
v8::Global<v8::Module> module_;
v8::Global<v8::String> url_;
bool linked_ = false;