whatwg / html

HTML Standard
https://html.spec.whatwg.org/multipage/
Other
8.05k stars 2.64k forks source link

Proposal: baseURL argument to `import.meta.resolve` #8077

Open guybedford opened 2 years ago

guybedford commented 2 years ago

import.meta.resolve provides a contextual resolver at the module level allowing custom module resolution to be exposed to users without actually importing those modules. This allows resolving URLs that are not necessarily modules as well as determining the resolved URL for any module specifier.

Problem Statement

import.meta.resolve has limited functionality in that it only permits resolving bare specifiers within the scope of the current module. When wanting to resolve bare specifiers which exist in other scopes, one would need to load a module within one of those other scopes first to get hold of an import.meta.resolve with the ability to resolve relative to that scope.

This could be useful for determining the resolution of a dependency of a dependency when the import map is not known. This is not otherwise possible since the root import map cannot be retrieved from the HTML page.

Proposal

The proposal is the addition of an optional baseURL argument to resolve. In https://github.com/whatwg/html/pull/8074#issuecomment-1174730636 this is done via a direct second argument, although as @domenic mentions it might be worth making this an options bag to support future extensibility scenarios:

import.meta.resolve(specifier, { baseURL })

When not provided, the default baseURL of the current module context would be maintained per the current implementation.

Implementation Interest

Node.js has been shipping this feature since Feb 2020 (https://nodejs.org/dist/latest-v18.x/docs/api/esm.html#importmetaresolvespecifier-parent). Deno has an open PR to support import.meta.resolve along with this feature (https://github.com/denoland/deno/pull/15074).

Compatibility Concerns

For implementers providing this feature, the options bag approach is a great suggestion. If some implementers ship import.meta.resolve(specifier, baseURL) and others ship import.meta.resolve(specifier, { baseURL }) that risks creating an interop hazard between runtimes. For this reason standardizing on an approach can ensure users get consistent support when performing custom resolutions.

Alternative Designs

One benefit of this proposal is its simplicity in gaining cross-platform support for the feature in a way that can interop between runtimes since import.meta.resolve as it is designed today simply provides a default baseURL.

Alternative designs might include a global or host-specific method. Alternatively there may just be implementation divergence, the point of posting this issue is exactly to avoid these scenarios where server-side runtimes and browsers have differing behaviour. Significant work is already being done in Node.js to update our import.meta.resolve implementation to align with browsers, and this forms part of that effort.

domenic commented 2 years ago

So, let's remember https://whatwg.org/faq#adding-new-features step 1.

I'd like more detail on the problem statement. What are some examples of (browser) code which would use this feature? "determining the resolution of a dependency of a dependency when the import map is not known" is basically just restating the functionality of the feature. What is an example of web app code that would need to do this? Can you point to examples that exist today on the web, using e.g. AMD modules or transpiled Node.js modules?

nayeemrmn commented 2 years ago

One benefit of a global resolver is that it makes it easier for behaviour-preserving bundlers to hardcode import.meta.resolve(singleArg) (which of course is now already specced):

// input `https://example.com/foo.js`:
console.log(import.meta.resolve("./bar.js"));

// bundled `https://example.com/foo.js`:
const __importMeta = {
  url: "https://example.com/foo.js",
  resolve(specifier, options = {}) {
    return import.meta.resolve(specifier, { baseURL: "https://example.com/foo.js", ...options });
  }
};
console.log(__importMeta.resolve("./bar.js"));

From https://github.com/whatwg/html/pull/8074#issuecomment-1174730636:

  • I don't think a global resolver belongs on import.meta, because it's no longer specific to the current module like all existing import.meta properties are. Instead you just seem to be using it as a sort of weird global namespace. Something like self.resolveModuleSpecifier() or HTMLScriptElement.resolve() would make more sense to me.

I think this is a very good point, to add to it I don't understand what the benefit was of import.meta.resolve("./bar.js") over self.resolveModuleSpecifier("./bar.js", { baseURL: import.meta.url }). Problems like the above are resolved naturally, and of course a global resolver API would be eventually be proposed. Is there a semantic problem with this I'm not seeing? If this was discussed somewhere I missed it.