module: expose `resolveLoadAndCache` API

This commit is contained in:
Antoine du Hamel 2024-11-06 17:12:22 +01:00
parent 6f12f1e500
commit 327af5a19c
4 changed files with 156 additions and 4 deletions

View File

@ -352,6 +352,34 @@ changes:
Register a module that exports [hooks][] that customize Node.js module
resolution and loading behavior. See [Customization hooks][].
### `module.resolveLoadAndCache(specifier[, parentURL[, importAttributes[, conditions]]])`
<!-- YAML
added: REPLACEME
-->
> Stability: 1 - Experimental
* `specifier` {string|URL} Customization hooks to be registered; this should be
the same string that would be passed to `import()`, except that if it is
relative, it is resolved relative to `parentURL`.
* `parentURL` {string|URL} If you want to resolve `specifier` relative to a base
URL, such as `import.meta.url`, you can pass that URL here. **Default:**
`'data:'`.
* `importAttributes` {object}
* `conditions` {Array}
* Returns: {Promise} fulfills with an object with the following properties:
* `url` {string} The absolute URL for that module
* `format` {string} The format this module will be parsed as.
* `source` {null|TypedArray}
This API tells you how a specific URL will be loaded by the ECMAScript loader if
it was imported from the `parentURL` in the current process. If the module was
already imported before `resolveLoadAndCache` is called, the cached version is
returned; if not, it will populate the cache so future calls to
`resolveLoadAndCache` or `import` do re-do the work.
## `module.stripTypeScriptTypes(code[, options])`
<!-- YAML

View File

@ -26,7 +26,7 @@ const { pathToFileURL, fileURLToPath } = require('internal/url');
const assert = require('internal/assert');
const { getOptionValue } = require('internal/options');
const { setOwnProperty, getLazy } = require('internal/util');
const { setOwnProperty, getLazy, kEmptyObject } = require('internal/util');
const { inspect } = require('internal/util/inspect');
const lazyTmpdir = getLazy(() => require('os').tmpdir());
@ -392,6 +392,24 @@ ObjectFreeze(compileCacheStatus);
const constants = { __proto__: null, compileCacheStatus };
ObjectFreeze(constants);
async function resolveLoadAndCache(specifier, base = 'data:', importAttributes = kEmptyObject, conditions) {
const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader();
// TODO: this should hit the cache (and populate it if it finds nothing)
const { url, format: resolveFormat } = await cascadedLoader.resolve(`${specifier}`, `${base}`, importAttributes);
const { format, source } = await cascadedLoader.load(url, {
__proto__: null,
importAttributes,
format: resolveFormat,
conditions,
});
return {
__proto__: null,
format,
source,
url,
};
}
/**
* Get the compile cache directory if on-disk compile cache is enabled.
* @returns {string|undefined} Path to the module compile cache directory if it is enabled,
@ -414,6 +432,7 @@ module.exports = {
loadBuiltinModule,
makeRequireFunction,
normalizeReferrerURL,
resolveLoadAndCache,
stringify,
stripBOM,
toRealPath,

View File

@ -9,20 +9,22 @@ const {
enableCompileCache,
flushCompileCache,
getCompileCacheDir,
resolveLoadAndCache,
} = require('internal/modules/helpers');
const {
findPackageJSON,
} = require('internal/modules/package_json_reader');
const { stripTypeScriptTypes } = require('internal/modules/typescript');
Module.findSourceMap = findSourceMap;
Module.register = register;
Module.SourceMap = SourceMap;
Module.constants = constants;
Module.enableCompileCache = enableCompileCache;
Module.findPackageJSON = findPackageJSON;
Module.findSourceMap = findSourceMap;
Module.flushCompileCache = flushCompileCache;
Module.getCompileCacheDir = getCompileCacheDir;
Module.register = register;
Module.resolveLoadAndCache = resolveLoadAndCache;
Module.SourceMap = SourceMap;
Module.stripTypeScriptTypes = stripTypeScriptTypes;
module.exports = Module;

View File

@ -0,0 +1,103 @@
'use strict';
const { spawnPromisified } = require('../common');
const fixtures = require('../common/fixtures');
const assert = require('node:assert');
const { resolveLoadAndCache } = require('node:module');
const { describe, it } = require('node:test');
describe('resolveLoadAndCache', () => { // Throws when no arguments are provided
it('should return null source for CJS module and built-in modules', async () => {
assert.deepStrictEqual(await resolveLoadAndCache(fixtures.fileURL('empty.js')), {
__proto__: null,
url: `${fixtures.fileURL('empty.js')}`,
format: 'commonjs',
source: null,
});
assert.deepStrictEqual(await resolveLoadAndCache('node:fs'), {
__proto__: null,
url: 'node:fs',
format: 'builtin',
source: null,
});
});
it('should return full source for ESM module', async () => {
assert.deepStrictEqual(await resolveLoadAndCache(fixtures.fileURL('es-modules/print-3.mjs')), {
__proto__: null,
url: `${fixtures.fileURL('es-modules/print-3.mjs')}`,
format: 'module',
source: Buffer.from('console.log(3);\n'),
});
});
it('should accept relative path when a base URL is provided', async () => {
assert.deepStrictEqual(await resolveLoadAndCache('./print-3.mjs', fixtures.fileURL('es-modules/loop.mjs')), {
__proto__: null,
url: `${fixtures.fileURL('es-modules/print-3.mjs')}`,
format: 'module',
source: Buffer.from('console.log(3);\n'),
});
});
it('should throw when parentLocation is invalid', async () => {
for (const invalid of [null, {}, [], () => {}, true, false, 1, 0]) {
await assert.rejects(
() => resolveLoadAndCache('', invalid),
{ code: 'ERR_INVALID_URL' },
);
}
await assert.rejects(resolveLoadAndCache('', Symbol()), {
name: 'TypeError',
});
});
it('should accept a file URL (string), like from `import.meta.resolve()`', async () => {
const url = `${fixtures.fileURL('es-modules/print-3.mjs')}`;
assert.deepStrictEqual(await resolveLoadAndCache(url), {
__proto__: null,
url,
format: 'module',
source: Buffer.from('console.log(3);\n'),
});
assert.deepStrictEqual(await resolveLoadAndCache('./print-3.mjs', fixtures.fileURL('es-modules/loop.mjs').href), {
__proto__: null,
url,
format: 'module',
source: Buffer.from('console.log(3);\n'),
});
});
it('should require import attribute to load JSON files', async () => {
const url = 'data:application/json,{}';
await assert.rejects(resolveLoadAndCache(url), { code: 'ERR_IMPORT_ATTRIBUTE_MISSING' });
assert.deepStrictEqual(await resolveLoadAndCache(url, undefined, { type: 'json' }), {
__proto__: null,
format: 'json',
url,
source: Buffer.from('{}'),
});
});
it.skip('should use the existing cache', async () => {
const url = fixtures.fileURL('es-modules/print-3.mjs');
assert.deepStrictEqual(await spawnPromisified(process.execPath, [
'--no-warnings',
'--loader',
fixtures.fileURL('es-module-loaders/loader-resolve-passthru.mjs'),
'--loader',
fixtures.fileURL('es-module-loaders/loader-load-passthru.mjs'),
'-p',
`import(${JSON.stringify(url)}).then(() => module.resolveLoadAndCache(${JSON.stringify(url)}, url.pathToFileURL(__filename)))`,
]), {
stderr: '',
stdout: `Promise <>`,
code: 0,
signal: null,
});
});
it.skip('should populate the cache', async () => {});
});