Skip to content

Commit

Permalink
Syncify esm instantiation phase to allow cjs loader to return esm mod…
Browse files Browse the repository at this point in the history
…ules
  • Loading branch information
weswigham committed Mar 3, 2019
1 parent 7e2e82c commit 3e6a768
Show file tree
Hide file tree
Showing 7 changed files with 143 additions and 10 deletions.
40 changes: 34 additions & 6 deletions lib/internal/modules/cjs/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,18 @@ const manifest = getOptionValue('--experimental-policy') ?
require('internal/process/policy').manifest :
null;
const { compileFunction } = internalBinding('contextify');
const { executeWithinNewLoop } = internalBinding('task_queue');

const {
ERR_INVALID_ARG_VALUE,
ERR_REQUIRE_ESM
ERR_INVALID_ARG_VALUE
} = require('internal/errors').codes;
const { validateString } = require('internal/validators');

module.exports = Module;

/**
* @type {import("internal/process/esm_loader")}
*/
let asyncESM;
let ModuleJob;
let createDynamicModule;
Expand Down Expand Up @@ -102,6 +105,10 @@ function updateChildren(parent, child, scan) {
children.push(child);
}

/**
* @param {string} id
* @param {Module | null} parent
*/
function Module(id, parent) {
this.id = id;
this.exports = {};
Expand Down Expand Up @@ -674,7 +681,7 @@ Module.prototype.load = function(filename) {
Module._extensions[extension](this, filename);
this.loaded = true;

if (experimentalModules) {
if (experimentalModules && Module._extensions[extension] !== mjsRequire) {
if (asyncESM === undefined) lazyLoadESM();
const ESMLoader = asyncESM.ESMLoader;
const url = `${pathToFileURL(filename)}`;
Expand Down Expand Up @@ -850,9 +857,30 @@ Module._extensions['.node'] = function(module, filename) {

if (experimentalModules) {
if (asyncESM === undefined) lazyLoadESM();
Module._extensions['.mjs'] = function(module, filename) {
throw new ERR_REQUIRE_ESM(filename);
};
Module._extensions['.mjs'] = mjsRequire;
}

/**
* @param {Module} module
* @param {string} filename
*/
function mjsRequire(module, filename) {
if (asyncESM === undefined) lazyLoadESM();

let instantiated;
executeWithinNewLoop((sync) => {
const job = asyncESM.ESMLoader.getModuleJobWorker(
`${pathToFileURL(filename)}`,
'esm'
);
instantiated = sync(job.instantiate());
if (instantiated instanceof Error) {
throw instantiated;
}
});

module.exports = instantiated.namespace();
instantiated.evaluate(-1, false);
}

// bootstrap main module.
Expand Down
10 changes: 7 additions & 3 deletions lib/internal/modules/esm/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class Loader {
// Registry of loaded modules, akin to `require.cache`
this.moduleMap = new ModuleMap();

// map of already-loaded CJS modules to use
// Map of already-loaded CJS modules to use
this.cjsCache = new Map();

// The resolver has the signature
Expand Down Expand Up @@ -132,8 +132,7 @@ class Loader {
this._dynamicInstantiate = FunctionBind(dynamicInstantiate, null);
}

async getModuleJob(specifier, parentURL) {
const { url, format } = await this.resolve(specifier, parentURL);
getModuleJobWorker(url, format, parentURL) {
let job = this.moduleMap.get(url);
if (job !== undefined)
return job;
Expand Down Expand Up @@ -162,6 +161,11 @@ class Loader {
this.moduleMap.set(url, job);
return job;
}

async getModuleJob(specifier, parentURL) {
const { url, format } = await this.resolve(specifier, parentURL);
return this.getModuleJobWorker(url, format, parentURL);
}
}

Object.setPrototypeOf(Loader.prototype, null);
Expand Down
13 changes: 13 additions & 0 deletions lib/internal/modules/esm/module_map.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,19 @@ const { validateString } = require('internal/validators');

// Tracks the state of the loader-level module cache
class ModuleMap extends SafeMap {
/**
* @param {string} url
* @returns {ModuleJob | undefined}
*/
get(url) {
validateString(url, 'url');
return super.get(url);
}
/**
* @param {string} url
* @param {ModuleJob} job
* @returns {this}
*/
set(url, job) {
validateString(url, 'url');
if (job instanceof ModuleJob !== true) {
Expand All @@ -22,6 +31,10 @@ class ModuleMap extends SafeMap {
debug(`Storing ${url} in ModuleMap`);
return super.set(url, job);
}
/**
* @param {string} url
* @returns {boolean}
*/
has(url) {
validateString(url, 'url');
return super.has(url);
Expand Down
6 changes: 6 additions & 0 deletions lib/internal/process/esm_loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ if (type && type !== 'commonjs' && type !== 'module')
throw new ERR_INVALID_TYPE_FLAG(type);
exports.typeFlag = type;

/**
* @type {import("../modules/esm/loader")}
*/
const { Loader } = require('internal/modules/esm/loader');
const {
wrapToModuleMap,
Expand Down Expand Up @@ -41,6 +44,9 @@ exports.importModuleDynamicallyCallback = async function(wrap, specifier) {
let loaderResolve;
exports.loaderPromise = new Promise((resolve) => loaderResolve = resolve);

/**
* @type {Loader}
*/
exports.ESMLoader = undefined;

exports.initializeLoader = function(cwd, userLoader) {
Expand Down
4 changes: 4 additions & 0 deletions src/env-inl.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ inline uv_loop_t* IsolateData::event_loop() const {
return event_loop_;
}

inline void IsolateData::set_event_loop(uv_loop_t* loop) {
event_loop_ = loop;
}

inline bool IsolateData::uses_node_allocator() const {
return uses_node_allocator_;
}
Expand Down
4 changes: 3 additions & 1 deletion src/env.h
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,7 @@ class IsolateData {
ArrayBufferAllocator* node_allocator = nullptr);
~IsolateData();
inline uv_loop_t* event_loop() const;
inline void set_event_loop(uv_loop_t* loop);
inline MultiIsolatePlatform* platform() const;
inline std::shared_ptr<PerIsolateOptions> options();
inline void set_options(std::shared_ptr<PerIsolateOptions> options);
Expand Down Expand Up @@ -449,7 +450,7 @@ class IsolateData {
#undef VP

v8::Isolate* const isolate_;
uv_loop_t* const event_loop_;
uv_loop_t* event_loop_;
v8::ArrayBuffer::Allocator* const allocator_;
ArrayBufferAllocator* const node_allocator_;
const bool uses_node_allocator_;
Expand Down Expand Up @@ -717,6 +718,7 @@ class Environment {

inline v8::Isolate* isolate() const;
inline uv_loop_t* event_loop() const;
inline void set_event_loop(uv_loop_t* loop);
inline void TryLoadAddon(
const char* filename,
int flags,
Expand Down
76 changes: 76 additions & 0 deletions src/node_task_queue.cc
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,80 @@ static void SetPromiseRejectCallback(
env->set_promise_reject_callback(args[0].As<Function>());
}

static void Sync(const FunctionCallbackInfo<Value>& args) {
CHECK(args[0]->IsPromise());
v8::Local<v8::Promise> promise = args[0].As<v8::Promise>();
if (promise->State() == v8::Promise::kFulfilled) {
args.GetReturnValue().Set(promise->Result());
return;
}

Isolate* isolate = args.GetIsolate();
Environment* env = Environment::GetCurrent(args);

uv_loop_t* loop = env->event_loop();
int state = promise->State();
while (state == v8::Promise::kPending) {
isolate->RunMicrotasks();
if (uv_loop_alive(loop)) {
uv_run(loop, UV_RUN_ONCE);
}
state = promise->State();
}

args.GetReturnValue().Set(promise->Result());
}

static void ExecuteWithinNewLoop(const FunctionCallbackInfo<Value>& args) {
CHECK(args[0]->IsFunction());

v8::Local<v8::Function> func = args[0].As<v8::Function>();
Isolate* isolate = args.GetIsolate();
Environment* env = Environment::GetCurrent(args);

v8::Local<v8::Function> function =
env->NewFunctionTemplate(Sync, v8::Local<v8::Signature>(),
v8::ConstructorBehavior::kAllow,
v8::SideEffectType::kHasSideEffect)
->GetFunction(isolate->GetCurrentContext())
.ToLocalChecked();

v8::Local<v8::Value> argv[] = {function};

// Make a new event loop and swap out the isolate's event loop for it
uv_loop_t* loop = env->event_loop();
uv_stop(loop);
uv_loop_t newLoop;
uv_loop_init(&newLoop);
env->isolate_data()->set_event_loop(&newLoop);

// Call callback with `sync` parameter for synchronizing promises
// made within the new loop (WARNING: If `sync` callback is called
// on a promise made before entering into the synchronization context,
// it will likely hang, as the underlying events driving that promise
// are paused - only new events made within the callback should be safe
// to `sync`)
v8::MaybeLocal<v8::Value> result = func->Call(
env->context(),
v8::Undefined(isolate),
arraysize(argv),
argv);

// Run new loop to completion even after result from callback
while (uv_loop_alive(&newLoop)) {
isolate->RunMicrotasks();
uv_run(&newLoop, UV_RUN_ONCE);
}

// Close new loop and set isolate handle back to old one
uv_loop_close(&newLoop);
env->isolate_data()->set_event_loop(loop);

if (!result.IsEmpty()) {
args.GetReturnValue().Set(result.ToLocalChecked());
}
}

static void Initialize(Local<Object> target,
Local<Value> unused,
Local<Context> context,
Expand All @@ -120,6 +194,8 @@ static void Initialize(Local<Object> target,
env->SetMethod(target,
"setPromiseRejectCallback",
SetPromiseRejectCallback);

env->SetMethod(target, "executeWithinNewLoop", ExecuteWithinNewLoop);
}

} // namespace task_queue
Expand Down

0 comments on commit 3e6a768

Please sign in to comment.