Introduce pointer to <script> element in module scripts
There was an offline discussion about document.currentScript and shadow trees and the suggestion came up to solve it in a different way for module scripts so that the global object would not be involved and the value would only be accessible to the script currently executing.
I think this would belong in the "module meta" idea. It was detailed in more detail somewhere, but alluded to in https://github.com/whatwg/loader/issues/38. @dherman might know more. The idea would be something like:
import { currentScript, url } from this module;
At other times people have suggested using metaproperties on import (currently the only metaproperty in the language is new.target; this would introduce a few more):
import.meta.currentScript; // or just import.currentScript?
Unfortunately the base extensibility hook here kind of needs a TC39 proposal for someone to champion, which is a whole process.
Is it correct to say that document.currentScript is not set, even for module scripts with no imports like:
<script type="module">
document.currentScript == null;
</script>
Even if it were set in this case, I'd still want something similar to currentScript within imported modules. One use-case I have is to insert a template directly after the module script.
I like a variant of import.currentScript personally; I think introducing a special meta would invite questions like whether it is mutable or not. Maybe a better name came be thought of than currentScript (is it really current?) like ownerScript perhaps.
That's correct.
It should really be current; I don't see why it wouldn't be. We're still kind of waiting on TC39 to figure out the import metaproperty stuff though.
It's worth mentioning that we have a path forward here without waiting on TC39, which is to reserve a special module name. So then we'd do something like
import { currentScript, url } from "js:context";
import.currentScript sounds ideal here. With the dynamic import landed, perhaps a spec here can start to gain momentum. Landing features such as these soon will help significantly to avoid fragmentation of the adoption path features.
Note also feature detection here also seems tricky. I assume typeof import.currentScript wouldn't work, so it would just fall back to a syntax error if it isn't supported?
It's not directly related to this, I would like to note that I would like to have the similar thing in the module script in workers. See detailed thing why I would like to get it even in the workers. in https://github.com/tc39/proposal-dynamic-import/issues/37.
I think we should spec something here soon that people are willing to ship. Node.js is also interested in getting agreement on this.
I'd like to propose
import { url, currentScript } from "js:context";
to get the current script URL (which could be that of a HTML file for inline scripts), and the current <script> element (which could be null if no <script> is currently executing).
How does that sound to implementers? /cc @whatwg/modules
"js:context" looks fine but we are open to bikeshedding from Node.js side.
Maybe it would be worth carving out the entire js: protocol-like-prefix for future stuff like this?
It effectively already is carved out, since fetching any URL that starts with js: will fail per today's specs.
Hate to tie this in to other bike-shedding, but it would be good if whatever naming scheme we choose matches TC39's naming scheme for internal modules (if they do arise).
That seems like such a hack. There is a proposal for global which is encountering some compatibility issue. Perhaps global should also live there?
@rniwa how is it a hack if it needs context to the environment it was invoked from (in this case, the location of import or import())?
I don't think global makes sense to put in the context-specific module, but people have proposed putting it in a built-in module before. I'd suggest discussing that in the global repo.
@rniwa, putting global aside, I can't tell if you think the import { url } from "js:context" proposal is good or not?
I don't like it. Conceptually, what you're trying to figure out the information about the current module, not about "js:context". Why is the url of "js:context" not "js:context"?
@rniwa you are grabbing an export of "js:context" named url, I don't understand why you would think it is the url of "js:context" given people can do things like export let url = "foo" in modules. Would a name different from url be sufficient?
"js:context" is a contextual module specifier. I can see the confusion because it looks like a URL and not a specifier. Given that it is a specifier distinct to the module loading it, what does it resolve to? What's the algorithm?
Let's pretend for a second that the algorithm resolves to "js:context:https://example.com/foo.js" where "https://example.com/foo.js" is the importing module. Would this mean that, I could do:
import { url, currentScript } from "js:context:https://example.com/bar.js"
To grab another module's url/currentScript? Or would this be restricted?
Yeah, I think given that you can do import { url } from "./foo.js" and the result can be anything (not just foo.js), it shouldn't be surprising that import { url } from "js:context" does not give "js:context".
We could consider names like currentURL or contextURL but I kind of feel that instead of prefixing everything with such prefixes it'd be better to just put it in the name of the place you're importing from.
@matthewp
Given that it is a specifier distinct to the module loading it, what does it resolve to? What's the algorithm?
Specifiers resolve to modules, not to URLs. One of the ways they can do this is via first transforming the specifier into a URL, then looking it up in the module map. That's not what's going on here; we're going directly from specifier to module.
So let's not pretend it resolves to some weird URL like js:context:https://example.com/foo.js. That is definitely not what's going on here.
@rniwa given
Conceptually, what you're trying to figure out the information about the current module, not about "js:context"
would maybe calling it "js:currentmodule" be better? We've all indicated we're open to bikeshedding on the name; context seemed nice and short, but nobody insists on it.
I'm not super keen on "module" being in there if it also works in Scripts via import()
Last time we discussed this (a while ago), import ... from this module; didn't make the cut. We were more interested in something like import.something as a way to access meta information about the current module. Specifying what goes there should not be a big endeavor, we just need a champion :)
@caridy I am not sure we need a new syntax for this. It also is kind of nice not to need to generate the values unless they are demanded (even if they are demanded late like import("js:context")).
I don't like import { url } from "js:context";
Consider this code:
// foo.js
import { url } from "js:context";
// bar.js
import { url } from "js:context";
<script type="module" src="foo.js"></script>
<script type="module" src="bar.js"></script>
Even though modules are supposed to be cached (and therefore importing the same module multiple times returns the same module object), in this situation we would actually be importing different modules with a different value for url.
There's also another concern, similar to the concern raised with the dynamic import proposal: how does the "js:context" module know which module is importing it? Obviously this requires hidden information inside the JS engine.
It all feels very magical and inconsistent with how regular modules work.
So I prefer something like import.url because it doesn't break the programmer's mental model of how module imports work.
@Pauan as per Ecma262 modules are idempotent per Source Text / Import Specifier pair. They are not idempotent per global environment / realm.
@Pauan it's actually extremely consistent with how modules work, and the programmer's mental model. The information is not implicit or hidden or concerning. (Certainly not as implicit as in import.url!).
To see this, consider a slight modification of your example:
// foo/foo.js
import { baz } from "./baz.js";
// bar/bar.js
import { baz } from "./baz.js";
<script type="module" src="foo/foo.js"></script>
<script type="module" src="bar/bar.js"></script>
Now consider your statement:
Even though modules are supposed to be cached (and therefore importing the same module multiple times returns the same module object), in this situation we would actually be importing different modules with a different value for url.
The same applies here. The modules are cached. Importing the same module multiple times returns the same module object. But our two import statements import different modules with different values for baz.
That's just how imports work: they always take into account the current module, and work relative to that. js:context would be exactly like that.
In contrast, something like import.url is not at all clear how it works. There's nothing in that syntax that says it's relative to the current module, whereas it's very clear in any syntax that imports (both import declarations and import()), since imports are always relative to the current module.
@domenic I thought about it further, and you are correct. Even with Node, import { foo } from "foo" can have different behavior depending on where the importing module is (because your program can have multiple node_modules/foo folders).
In that case, I can't quite explain why I think import { foo } from "foo" is okay, but import { url } from "js:context" feels wrong. Perhaps it is just because of my familiarity and expectations with existing systems.
In contrast, something like
import.urlis not at all clear how it works. There's nothing in that syntax that says it's relative to the current module, whereas it's very clear in any syntax that imports (both import declarations andimport()), since imports are always relative to the current module.
import.url makes it clear that it's a builtin language feature (although I don't think that's the best name). import X from "js:context" makes it look like it's loading a module by the name of context by js scheme. It's very strange to hijack a protocol name like that. Given modern OS lets native (and Web) apps register their own scheme including js:, I don't think we should do this.
@rniwa would using an invalid specifier that is not a URL be better? I am open to almost any specifier.
import.url confuses me because .url is provided by the environment not the language.
I thought scheme overrides were discouraged with the arrival of universal links? It should also be noted that things like ServiceWorker would allow changing any import specifier prior to the page loading the source text. I don't think the argument that these specifiers could have collisions is compelling when they can be completely rewritten.
Yeah, I don't think there is a technical problem with a scheme; Fetch only allows a limited subset after all. I can appreciate how some people might find it confusing, although I believe that's a matter of opinion. But @rniwa's opinion is important as an implementer.
@rniwa, we have two implementers (Chrome and Node) very interested in moving forward on this issue, and we are agnostic to the name. You seem to have strong opinions on the name. Can you help us choose one so that the ecosystem can move forward?
@bmeck a service worker would not be able to rewrite "js:context". A service worker only gets handed HTTP(S) URLs (and pretty much only HTTPS since there's very few exceptions for HTTP and those are not even implemented). (@domenic said this to some extent already, but I figured it'd call this out explicitly.)
And as for registerProtocolHandler(), we haven't quite decided where it's relevant, but what we seem to be leaning towards is only using it for navigation, not subresource fetches.