Skip to content

Commit

Permalink
feat(esm): support nested loader chains
Browse files Browse the repository at this point in the history
Fixes #48515
  • Loading branch information
izaakschroeder committed Jun 26, 2023
1 parent 42d8143 commit 26b9ab4
Show file tree
Hide file tree
Showing 3 changed files with 104 additions and 23 deletions.
48 changes: 45 additions & 3 deletions lib/internal/modules/esm/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ const {
const {
getDefaultConditions,
loaderWorkerId,
createHooksLoader,
} = require('internal/modules/esm/utils');
const { deserializeError } = require('internal/error_serdes');
const {
Expand Down Expand Up @@ -136,6 +137,10 @@ class Hooks {
this.addCustomLoader(urlOrSpecifier, keyedExports);
}

getChains() {
return this.#chains;
}

/**
* Collect custom/user-defined module loader hook(s).
* After all hooks have been collected, the global preload hook(s) must be initialized.
Expand Down Expand Up @@ -220,16 +225,25 @@ class Hooks {
originalSpecifier,
parentURL,
importAssertions = { __proto__: null },
) {
return this.resolveWithChain(this.#chains.resolve, originalSpecifier, parentURL, importAssertions);
}

async resolveWithChain(
chain,
originalSpecifier,
parentURL,
importAssertions = { __proto__: null },
) {
throwIfInvalidParentURL(parentURL);

const chain = this.#chains.resolve;
const context = {
conditions: getDefaultConditions(),
importAssertions,
parentURL,
};
const meta = {
hooks: this,
chainFinished: null,
context,
hookErrIdentifier: '',
Expand Down Expand Up @@ -344,8 +358,12 @@ class Hooks {
* @returns {Promise<{ format: ModuleFormat, source: ModuleSource }>}
*/
async load(url, context = {}) {
const chain = this.#chains.load;
return this.loadWithChain(this.#chains.load, url, context)
}

async loadWithChain(chain, url, context = {}) {
const meta = {
hooks: this,
chainFinished: null,
context,
hookErrIdentifier: '',
Expand Down Expand Up @@ -749,7 +767,31 @@ function nextHookFactory(chain, meta, { validateArgs, validateOutput }) {
ObjectAssign(meta.context, context);
}

const output = await hook(arg0, meta.context, nextNextHook);
const withESMLoader = require('internal/process/esm_loader').withESMLoader;

const chains = meta.hooks.getChains();
const loadChain = chain === chains.load ? chains.load.slice(0, generatedHookIndex) : chains.load;
const resolveChain = chain === chains.resolve ? chains.resolve.slice(0, generatedHookIndex) : chains.resolve;
const loader = createHooksLoader({
async resolve(
originalSpecifier,
parentURL,
importAssertions = { __proto__: null }
) {
return await meta.hooks.resolveWithChain(
resolveChain,
originalSpecifier,
parentURL,
importAssertions,
);
},
async load(url, context = {}) {
return await meta.hooks.loadWithChain(loadChain, url, context);
},
})
const output = await withESMLoader(loader, async () => {
return await hook(arg0, meta.context, nextNextHook);
});

validateOutput(outputErrIdentifier, output);

Expand Down
70 changes: 50 additions & 20 deletions lib/internal/modules/esm/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const {
} = require('internal/vm/module');
const assert = require('internal/assert');


const callbackMap = new SafeWeakMap();
function setCallbackForWrap(wrap, data) {
callbackMap.set(wrap, data);
Expand Down Expand Up @@ -107,26 +108,19 @@ function isLoaderWorker() {
return _isLoaderWorker;
}

async function initializeHooks() {
const customLoaderURLs = getOptionValue('--experimental-loader');

let cwd;
try {
// `process.cwd()` can fail if the parent directory is deleted while the process runs.
cwd = process.cwd() + '/';
} catch {
cwd = '/';
}


const { Hooks } = require('internal/modules/esm/hooks');
const hooks = new Hooks();

const createHooksLoader = (hooks) => {
// TODO: HACK: `DefaultModuleLoader` depends on `getDefaultConditions` defined in
// this file so we have a circular reference going on. If that function was in
// it's on file we could just expose this class generically.
const { DefaultModuleLoader } = require('internal/modules/esm/loader');
class ModuleLoader extends DefaultModuleLoader {
loaderType = 'internal';
class HooksModuleLoader extends DefaultModuleLoader {
#hooks;
constructor(hooks) {
super();
this.#hooks = hooks;
}
async #getModuleJob(specifier, parentURL, importAssertions) {
const resolveResult = await hooks.resolve(specifier, parentURL, importAssertions);
const resolveResult = await this.#hooks.resolve(specifier, parentURL, importAssertions);
return this.getJobFromResolveResult(resolveResult, parentURL, importAssertions);
}
getModuleJob(specifier, parentURL, importAssertions) {
Expand All @@ -143,9 +137,44 @@ async function initializeHooks() {
},
};
}
load(url, context) { return hooks.load(url, context); }
resolve(
originalSpecifier,
parentURL,
importAssertions = { __proto__: null },
) {
console.log('PRIVATE RESOLVE', originalSpecifier);
return this.#hooks.resolve(
originalSpecifier,
parentURL,
importAssertions
);
}
load(url, context = {}) {
console.log('PRIVATE LOAD', url);
return this.#hooks.load(url, context);
}
}
const privateModuleLoader = new ModuleLoader();
return new HooksModuleLoader(hooks);
}

async function initializeHooks() {
const customLoaderURLs = getOptionValue('--experimental-loader');

let cwd;
try {
// `process.cwd()` can fail if the parent directory is deleted while the process runs.
cwd = process.cwd() + '/';
} catch {
cwd = '/';
}


const { Hooks } = require('internal/modules/esm/hooks');
const hooks = new Hooks();


const privateModuleLoader = createHooksLoader(hooks);
privateModuleLoader.loaderType = 'internal';
const parentURL = pathToFileURL(cwd).href;

// TODO(jlenon7): reuse the `Hooks.register()` method for registering loaders.
Expand Down Expand Up @@ -175,4 +204,5 @@ module.exports = {
getConditionsSet,
loaderWorkerId: 'internal/modules/esm/worker',
isLoaderWorker,
createHooksLoader,
};
9 changes: 9 additions & 0 deletions lib/internal/process/esm_loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@ const { kEmptyObject } = require('internal/util');
let esmLoader;

module.exports = {
async withESMLoader(loader, fn) {
const oldLoader = esmLoader;
esmLoader = loader;
try {
return await fn();
} finally {
esmLoader = oldLoader;
}
},
get esmLoader() {
return esmLoader ??= createModuleLoader(true);
},
Expand Down

0 comments on commit 26b9ab4

Please sign in to comment.