Skip to content

module: expose resolveLoadAndCache API #55756

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

aduh95
Copy link
Contributor

@aduh95 aduh95 commented Nov 6, 2024

Opening as draft as I'd like to get some feedback on the API design first. /cc @nodejs/loaders

Use case for this is to get "final" format of the module that would result from importing a specific specifier. The cache part is primordial as otherwise you'd have no guarantee that the result would be accurate (if there's a user loader involved, we have no reason to believe it would be stable over time). However, caching a call that's sometimes async, sometimes sync is more or less impossible in JS, and on top of that there are loaders out there that rely on resolve NOT being cached (e.g the test runner .mock util), so instead I decided it'd be more useful to provide a way to skip the resolve call.

@nodejs-github-bot
Copy link
Collaborator

Review requested:

  • @nodejs/loaders

@nodejs-github-bot nodejs-github-bot added module Issues and PRs related to the module subsystem. needs-ci PRs that need a full CI run. labels Nov 6, 2024
@JakobJingleheimer JakobJingleheimer self-requested a review January 5, 2025 13:36
@@ -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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is already doing that, no? I see gets and sets

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or, it appears to do for ModuleLoader but not CustomizedModuleLoader. Is that what you mean?

});
return {
__proto__: null,
format,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When --experimental-strip-types is enabled and performing this operation on a .ts file, this format may contains commonjs-typescript and module-typescript which are not documented at https://nodejs.org/api/module.html#loadurl-context-nextload as final formats.

I think we should convert commonjs-typescript and module-typescript to commonjs and module respectively. Alternatively, document the other two and make them public.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we probably want to document it, as it's certainly going to be helpful for the ecosystem to know which files are TypeScript

@aduh95 aduh95 force-pushed the resolve-load-cache branch from 5600325 to 2c153f8 Compare April 11, 2025 17:19
@aduh95 aduh95 marked this pull request as ready for review April 11, 2025 17:24
Copy link

codecov bot commented Apr 11, 2025

Codecov Report

Attention: Patch coverage is 89.58333% with 5 lines in your changes missing coverage. Please review.

Project coverage is 90.21%. Comparing base (c712dd2) to head (434cbf2).
Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
lib/internal/modules/helpers.js 87.17% 5 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #55756      +/-   ##
==========================================
+ Coverage   89.66%   90.21%   +0.55%     
==========================================
  Files         630      630              
  Lines      186389   186437      +48     
  Branches    36286    36612     +326     
==========================================
+ Hits       167124   168198    +1074     
+ Misses      12049    11060     -989     
+ Partials     7216     7179      -37     
Files with missing lines Coverage Δ
lib/module.js 100.00% <100.00%> (ø)
lib/internal/modules/helpers.js 97.87% <87.17%> (-0.05%) ⬇️

... and 100 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Module.constants = constants;
Module.enableCompileCache = enableCompileCache;
Module.findPackageJSON = findPackageJSON;
Module.findSourceMap = findSourceMap;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like unrelated changes.

relative, it is resolved relative to `parentURL`.
* `parentURL` {string|URL|undefined} If you want to resolve `specifier` relative to a base
URL, such as `import.meta.url`, you can pass that URL here. If not provided,
the `resolve` step will be skipped. **Default:** {undefined}.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a fan of implicitly skip resolve when parentURL is undefined. This also contradicts with the API name resolve, load, and cache.

We have several conventions in APIs about how to resolve the parentURL:

  • new Worker(filename) resolves with base to process.cwd(),
  • module.register(specifier[, parentURL]) resolves with base to data: if parentURL is not set.

I think as part of module loader API, it is better to be explicit, e.g. throw if parentURL is missing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • module.register(specifier[, parentURL]) resolves with base to data: if parentURL is not set.

I think as part of module loader API, it is better to be explicit, e.g. throw if parentURL is missing.

Note that this way of doing things is consistent with module.register, i.e. it will throw if specifier is not a full URL and parentURL is missing (because load only accepts full URLs).

Not a fan of implicitly skip resolve when parentURL is undefined. This also contradicts with the API name resolve, load, and cache.

The name is not great, I'm certainly fine changing it to something else. IMO being able to skip resolve is important as it allows to get results guaranteed to be true for the lifetime of the process (at least until we expose a way to clear the load cache), I'm open to suggestions if you think we can make a clearer API

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I'm concerned about is that using a full specifier URL and an implicit parentURL to indicate skipping resolve is not alike with import:

import 'test:./foobar'

With import statements or import calls, a resolve hook will be invoked with a full specifier URL and implicit parentURL.

export function resolve(specifier, context, next) {
  if (specifier.startsWith('test:')) {
    // Translate custom protocol and use the default loader.
    return next(specifier.slice(5), context);
  }
  return next(specifier, context);
}

Copy link
Contributor Author

@aduh95 aduh95 Apr 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We cannot make it work like import, unless we define it on import.meta, but I get what you're saying. What do you think about going back to using data: as default parentURL, and expose some symbol to explicitly skip the resolve call if needed?

// does not go through `resolve` hook:
resolveLoadAndCache('full://module/url', module.SKIP_RESOLVE_CALL);
// or maybe
resolveLoadAndCache('full://module/url', Symbol.for('nodejs.skipResolveCall'));

// does go through `resolve` hook with `data:` as `parentURL`:
resolveLoadAndCache('full://module/url');
// does go through `resolve` hook just like `import`/`import()` would:
resolveLoadAndCache('full://module/url', import.meta.url);

Copy link
Member

@legendecas legendecas Apr 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I agree that this API does not infer parentURL implicitly to the caller module (it is not practical anyway). I think a possible alternative to Symbol based API is separating the motivation of invoking each of the loader hook into individual APIs:

interface ModuleLoaderAPI {
  resolve(specifier, parentURL, attributes): Promise<{ url, format }>;
  load(url, attributes): Promise<{ format, source }>;

  resolveAndLoad(specifier, parentURL, attributes): Promise<{ url, format, source }>;
}

In this way, the API intention is explicit.

@aduh95 aduh95 force-pushed the resolve-load-cache branch from 2c153f8 to e04c622 Compare April 27, 2025 20:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
module Issues and PRs related to the module subsystem. needs-ci PRs that need a full CI run.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants