Skip to content
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

How could import.meta.resolve() behave? #79

Closed
domenic opened this issue Nov 26, 2018 · 9 comments
Closed

How could import.meta.resolve() behave? #79

domenic opened this issue Nov 26, 2018 · 9 comments

Comments

@domenic
Copy link
Collaborator

domenic commented Nov 26, 2018

Related: #75, #18, #76, whatwg/html#3871.

Background: What is import.meta.resolve()?

The original idea of import.meta.resolveURL(x) was that it would be sugar for (new URL(x, import.meta.url)).href. So, you could do import.meta.resolveURL("./x") inside https://example.com/path/to/y and you would get "https://example.com/path/to/x".

But note that in this formulation, you could also do import.meta.resolveURL("x") and get the same result. That's a little strange, since import "x" would not give you that URL.

It would also be ideal if we could do, like Node.js's require.resolve(), a function that gave you a URL from a "package name", of the type provided by import maps. Let's call this import.meta.resolveMapped(). So for example, given the simple example map from the readme at https://example.com/index.html, import.meta.resolveMapped("lodash") would give "https://example.com/node_modules/lodash-es/lodash.js".

We then noticed that having two functions is confusing and redundant. Let's merge them! The result is import.meta.resolve(x), which gets the URL that would be fetched by the module system if you did import(x).

Side note: what are the use cases?

We don't have a great list of canonical use cases for this function. whatwg/html#3871 has some, and elsewhere in this issue tracker people like @bicknellr and @justinfagnani have mentioned web components-related use cases.

Maybe gardening up a master list, with code examples, would be a good next step.

The problem: fallbacks

This proposal's introduction of fallbacks for user-supplied packages throws a wrinkle into our plans. In particular, there's no longer a synchronous way to figure out what to do in the "package" case. E.g. in that example, it's no longer clear what import.meta.resolveMapped("jquery"), or import.meta.resolve("jquery"), should return.

Solution ideas

Return an array

That is, import.meta.resolve("jquery") returns an array containing the normalized absolute URLs that would be potentially used, e.g. ["https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js", "https://example.com/node_modules/jquery/dist/jquery.js"].

This seems hard for consumers to code against, and makes it hard to refactor between fallbacks and no-fallbacks.

Return a time-varying value

For example, it could return null, or an array like the above, while resolution is pending. Once resolution happens (via some actual fetch), it could return the resolved-to URL.

This similarly seems hard for consumers to code against.

Return a promise

The drawback here is developer ergonomics. E.g. if you are trying to use this inside a component, in order to programmatically set up linkEl.href = import.meta.resolve("jquery"), you no longer can; you need to make that async.

This could become less-terrible with top-level await, plus of course the usual mitigations like web packaging/preload/etc. to make sure that top-level await is not overly blocking.

Lazy

In this variant, calling import.meta.resolve() does not do any fetches itself. Instead it waits for some actual fetch to happen (e.g. via import() or the use of an import: URL). The promise may then end up pending for a long time.

This feels like a bit of a footgun.

Eager

In this variant, calling import.meta.resolve() causes a fetch to happen, which updates the appropriate resolution cache (see #76 for some discussions on what that means).

This feels kind of inappropriate for a resolution function to perform I/O.

Punt on this

For example, return null for all fallback cases (except maybe we could do built-in module cases?).

This still makes refactoring hard; you could break parts of your app by moving from no-fallback to fallback.

Return a request, or a promise for a response

In this variant we get even further from what is typically thought of as a "resolution" function. Instead we return something which could be consumed by various endpoints that currently take a URL. This is a long-standing idea, see whatwg/fetch#49 and whatwg/html#3972. But it might solve all the same use cases.

So far I like this the most. It needs a bit more exploration, but it seems like it could work.

@jkrems
Copy link
Contributor

jkrems commented Nov 26, 2018

Would another alternative be "return a resolution token"? E.g. something that can be passed into import or fetch or other APIs? Strawman:

<script type="module">
// Inside of https://example.com/foo.html
const resolution = import.meta.resolve('jquery');
linkEl.href = resolution; // "import:https://example.com/foo.html:jquery"
</script>

EDIT: The browser would be free to replace/resolve these so that copy link gets the resolved value etc..

@domenic
Copy link
Collaborator Author

domenic commented Nov 26, 2018

I think that's strictly inferior to the last option, as it means creating a new opaque useless type that just needs to get translated into the useful type (Request), adding a bunch of indirection throughout the stack.

In particular, I don't think there'd be much support for implementing that in browsers, whereas there's definite existing support for the Request/Promise<Response> variant.

@guybedford
Copy link
Collaborator

As mentioned in #84 (comment) I'm still not sure we should support network round-trip fallbacks due to their performance concerns.

If we were to restrict fallbacks to only working for checks that can be done synchronously such as std: and other synchronous protocol-based checks, then perhaps the concern here around import.meta.resolve would no longer apply?

@Jamesernator
Copy link

Jamesernator commented May 30, 2019

I think that @jkrems approach would be more useful is if instead of an opaque type there was a new type like URLBundle that contains a sequence of urls in preferential order to try. However this would mean existing APIs would need to now accept this type e.g. new Worker, location.href, script.src, etc etc would all need to learn how to retry a failed load (and how for synchronous APIs like location.href? Simply don't accept this new type maybe?).

@benjamn
Copy link

benjamn commented Feb 17, 2020

I could definitely work with a version of import.meta.resolve that returned a Promise. That seems future-proof, and easy to explain, even without top-level await to help with the ergonomics.

I share @guybedford's concerns about fallback performance/predictability and would probably avoid using fallbacks personally, but I appreciate the importance of making the API consistently async. In the future, we might come up with additional reasons (even more compelling than fallback support) why module resolution needs to be async, and I think we'd be glad we chose a generic solution to the problem (such as returning a Promise) rather than something specific to fallbacks. Returning an array of possible URLs feels somewhat fallback-specific, for example.

On a different note, would it be possible to do import.meta.resolve(identifier, parentURL) to use a parent URL that's different from import.meta.url?

@guybedford
Copy link
Collaborator

@benjamn the latest version of Node.js 13 being released soon will be shipping with a flagged implementation of --experimental-import-meta-resolve, exactly supporting the async API with a parentURL argument permitted as well. See nodejs/node#31032 for more info here.

@benjamn
Copy link

benjamn commented Feb 18, 2020

@guybedford Thank you so much for working on that!

@adrianhelvik
Copy link

adrianhelvik commented Jun 20, 2021

I think the primary reason to use import.meta.resolve is to resolve an asset such as an image or CSS file.

The only overlap I could imagine actually happening with fallbacks and resolve is if either a built in module provides images or fonts or if increased resilience is needed. If assets served using Fastly had a fallback, a lot of headache could have been prevented during the recent incident.

A more appropriate solution than to have resolve() be async would be to introduce a fallback URI scheme.

The URL could be something like https://example.com/widget.js:https://fallback.com/widget.js.

If this is considered a viable future path I propose having import.meta.resolve() throw if there are fallbacks for a module, until fallback URLs are taken further.

As you mentioned, http requests as a side effect of calling a resolver function is pretty iffy.

Having the resolve() be async will also make it harder to use in certain cases. For instance, React components must build the layout synchronously, so async resolve() will require some extra scaffolding.

I believe this presents a pretty solid case for synchronous import.meta.resolve(). What do you think?

@adrianhelvik
Copy link

import.meta.resolveSync could be more future resilient for a synchronous function though.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
6 participants