whatwg / html

HTML Standard

Home Page:https://html.spec.whatwg.org/multipage/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Introduce pointer to <script> element in module scripts

annevk opened this issue · comments

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 whatwg/loader#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 tc39/proposal-dynamic-import#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")).

commented

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.

commented

@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.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.

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.

@annevk I was talking about intercepting the HTTP(S) request with the import 'foo'; and replacing import 'foo'; with import 'foobar';. I am not talking about intercepting the request for foo, there are other problems like losing the url of the dependent Module if you wait for the fetch of foo.

First off, this is something we'll be stuck for the next 20 years or so. I'd like to make sure it's the best API we can come up with. A good question to ask is if you look back in 20 years from now, do we still see this API as the most natural API?

If we don't want to add the global currentModule, then my proposal would be to have import.context.url like new.target in ECMA and let the import.context be extensible in the host language like HTML so that it can add more stuff. I don't understand why using the module machinery is desirable here at all.

We're talking about the API to figure out the URL of the current module, and the script element which imported it. It's very counterintuitive to then import another module with the special name js:context in order to figure out the information about the current module. Heck, I might be writing a hybrid app and overridden js scheme to refer to random resources in my app, and it would be impossible to tell why on the Earth js:context is special.

@rniwa there have been desires by TC39 to reserve a syntax/specifier space for "builtin" modules such as the newly proposed temporal type addition. If you don't want a URL that seems fine, but even without this, there is a high likelyhood that TC39 will reserve specifiers in some way (though it may not be using URLs as we have pointed above). This has been discussed for more than a year in TC39's issue tracker and variations are iterated in there if you have preferences regarding problems with reserving specifiers. I have a strong opinion that import.* has no advantages over import {url} from "bikeshed". In addition, I am not convinced that new syntax makes things more understandable as it adds language complexity; in addition, as @domenic pointed out, ESM is already contextual in what specifiers refer to.

I am perfectly open to bikeshedding a specifier, but would need to be given an argument on the technical advantage/features not provided by a regular import that are solved by adding syntax to the language.

@rniwa what kind of syntax would you expect for builtin modules? That's what "js:context" (and in particular "js:..." seems like to me. A namespace for builtin things. Given how "resolve a module specifier" works there's numerous other strings we could use, but at the end of that day using a string of sorts there is much easier than changing the syntax (and I believe also more in line with the plans for that space).

Putting url/scriptElement aside, it seems to me the most natural way to expose a builtin module would be to use a symbol as the module name. e.g. import stdev from Builtins.Statistics.

If we don't want to add the global currentModule, then my proposal would be to have import.context.url like new.target in ECMA and let the import.context be extensible in the host language like HTML so that it can add more stuff. I don't understand why using the module machinery is desirable here at all.

This has been discussed a few times by TC39. Every time it is, the conclusion is that there already exists a mechanism in modules for getting contextual information: an import declaration. Thus, it's just a matter of us, the host environment, picking an appropriate string. That's why I'm hoping for your help in doing so.

Putting url/scriptElement aside, it seems to me the most natural way to expose a builtin module would be to use a symbol as the module name. e.g. import stdev from Builtins.Statistics.

There was some interest in using different syntax for built-in modules, but the conclusion was that strings are sufficiently flexible and that the platform shouldn't be privileged in syntax, just in what namespace it uses within the space of strings. (This is important for e.g. creating polyfills.)

So again, I am hoping you can help us come up with a good string, given that you don't like our proposal.

If we don't want to add the global currentModule, then my proposal would be to have import.context.url like new.target in ECMA and let the import.context be extensible in the host language like HTML so that it can add more stuff. I don't understand why using the module machinery is desirable here at all.

This has been discussed a few times by TC39. Every time it is, the conclusion is that there already exists a mechanism in modules for getting contextual information: an import declaration. Thus, it's just a matter of us, the host environment, picking an appropriate string. That's why I'm hoping for your help in doing so.

That's not exactly how I remember it. We definitely agree that the host could use the module machinery to do all kind of stuff, including the definition of various schemes for the module specifier. But for the module's contextual information (e.g.: dirname, filename), we were not sure, and using something like import.foo might be more appropriate since we probably want to formalize that, in which case it is probably easier than using a magic module specifier. My recommendation is to be very cautions about making the wrong call here. Maybe we can bring this up in the next meeting, and test the water before making a final call here.

@caridy @domenic we can move it to TC39 meeting in May if someone wants to propose a syntax. But without additional functionality not capable by a regular import, I would need convincing it has more merit than a well defined specifier; particularly since builtin modules appears to have gone for well defined specifier syntax. For now, our prototype in Node will continue to use the bikeshed specifier as this functionality is required to ship ESM in Node. I also don't see a problem even if we ship the reserved specifier since it doesn't appear the new syntax has any special attributes vs a regular module namespace (I likely am missing something though)?

commented

I can finally articulate why I dislike import { url } from "js:context"

I'm not a spec writer, I'm just a long-time JavaScript developer, so that is the perspective I'll be using.

When using an import statement, the purpose is always to import another module. In other words, you are importing something else into the current module. (Technically you can import the current module into the current module, but that's completely useless so nobody does it.)

But with import { url } from "js:context", it doesn't load "js:context", you aren't importing something else. Instead, you are retrieving the metadata for the current module. This is inconsistent with the purpose of import

The idea that import is used to retrieve other things is so deeply ingrained into me, which is why importing from "js:context" feels so wrong. And I'm sure I'm not the only JavaScript developer who feels that way.

On the other hand, import.url doesn't have that problem. It is clearly special built-in syntax which only applies to the current module. It doesn't have any connotations of loading something else. There's a reason CommonJS uses module.id and not require("js:module").id

So even though it might be very elegant and easy in the spec to use "js:context", from my perspective as a JavaScript developer, it feels deeply wrong. The import statement should be for loading other modules, it shouldn't be for retrieving information about the current module.

commented

Also, given my above perspective, I'm fine with things like import stdev from Builtins.Statistics or import { url } from this module, because they are clearly special syntax, and they do not have the connotation of loading something else.

Thanks for your perspective. I'd suggest realigning your intuition with the reality that import statements are just something that grabs data from the browser given the current context and the given string.

@domenic correct, however I would like to state imports like import "future"; that might be intended gow a module source text is loaded while allowed are very much not in the spirit. It is good to keep the understand that importing is just pulling variables and setup of those variables into your module scope.

I think this would be a good topic for the next TC39 meeting and I'll make sure we add it to the agenda.

@dherman seems fine. I won't be attending, but may find a proxy.

What about creating a module object containing url and element properties, scoped to the current module? Using import feels like the wrong name.

The point is to not have it be globally accessible (as any object created would be), but instead only contextual to the module (which is what import is designed for).

Wherever things end up, module metadata and builtin modules (e.g. Math and JSON as modules) should use different mechanisms, IMO.

I think it's fine for HTML to use js:context as a specifier for HTML specific data as HTML in this case is the host for modules and is specifying host specific modules.

However because of Realms I'd rather not see builtin modules being added to string namespace.

One of the really nice things that was added to the Realms proposal recently is the ability to add import hooks so that any behaviour can be used. As one of the main use cases (and one I'm most interested in) for Realms is plugins, the ability to have these import hooks is powerful.

Some plugins might want to use a naming scheme of <some-name>:<some-other-name> for example perhaps plugins are shareable between users then they might define a scheme where people can import other people's plugins/functions whatever from a <username>:<importedThing> namespace.

Now if some of the namespace were reserved for JavaScript use the Realms API could do one of two things

  • Make it so that the import hook isn't called for the builtin namespace
  • Allow overriding the builtin behaviour anyway

Now in the former case this is bad because suddenly if there's ever a user called js then suddenly things don't work for them because the developer's might not have been aware of whatever scheme JavaScript is specifically reserving.

In the latter case overriding the builtin behaviour means whole parts of the language would become unavailable to all users because of the initial choice to use such a scheme.


While I'm fine with js:context as part of HTML. Personally I'd rather see the meta-property idea, import.currentScript makes reasonable sense. While I don't want to suggest any behaviour for the package keyword itself package.<metadata> meta-properties would make a lot of intuitive sense.

I sincerely doubt the Realms API is going to go anywhere if it insists on including its own module customization options. But that is off-topic for this thread, so please take any such discussions to another venue.

Agree with @domenic . Will state that with builtins Realms will still need to be able to remove access to any "Power Tools", so if they have import hooks, they need to be completely configurable including overwriting builtins.

I anticipate import.module.* would make Node/CommonJS folks happy, especially if you motivate it by saying it's like the CommonJS module object, except it can't be accidentally shadowed by a local variable.

By contrast, Node/CommonJS folks have a strong intuition that module identifiers refer to exactly one other module, with relatively stable exports, and the flexibility of using absolute/relative/top-level identifier syntax is an abstraction that makes it a little easier to move code around without breaking your imports.

A totally subjective source module that exports different values depending on the importing module will be a difficult sell to that crowd, even if you repeatedly tell them to "realign their intuitions."

I don't imagine that import.module.* properties would act like imports. The host environment would initialize import.module before evaluating the module, much as CommonJS instantiates its module object before evaluating a module. I will note, however, that this thread has not yet discussed when/how the host environment would do that initialization. Regardless of which syntax is chosen for consuming this information, where the information comes from seems like a more pressing question.

A totally subjective source module that exports different values depending on the importing module will be a difficult sell to that crowd, even if you repeatedly tell them to "realign their intuitions."

Module specifiers are already contextual in CommonJS. If you use a bare specifier in Node you are referring to a package. That same package name might be imported elsewhere in the application but refers to a different value (a different module).

Can you clarify how

> I don't imagine that import.module.* properties would act like imports

is different than the proposal. The proposal's initialization follows standard ESM initialization.

@bmeck I was responding to your phrase "some new meta property that acts like an import," though I may have misunderstood what you meant? If you didn't mean to say import.module.* acts like an import, then I wasn't proposing anything new.

@benjamn it does act like an import to everything I can see:

  1. it is contextual
  2. it is created prior to evaluation
  3. it comes from host env

I am curious how import.module.* acts different from a module namespace. Also note, CJS module is not in ESM impl for node due to oddities, I would be curious what that point was towards.

I was raising a concern that motivating the "js:context" pseudo-module to the Node community may be harder than you think (even though you yourself are happy with it), while also casting a bikeshed vote in favor of import.module.* as opposed to import.meta.* or import.context.* (though I would not TC39 block consensus for any of those import.{module,meta,context,...}.* options).

As for the differences from the CommonJS module namespace, I imagine that properties of import.module would be read-only from the perspective of module code, at the very least. Also, import.module is inherently more reliable than a free module variable, which can be shadowed by user-defined variables, which I think is an easy answer to folks who wonder why we don't just reuse the CommonJS module variable.

@benjamn it is in the EP rewrite so it would need a vote to not accept it currently.

nodejs/node-eps#39

As for the differences with CJS, there are other more problematic things like frozen view of exports, parent doesn't work when parallel loading occurs, etc. Even with a read-only object it will not be exactly like CJS.

Looks like the committee preferred import.meta: https://github.com/domenic/proposal-import-meta

I'm glad with the result. The "js:context" was obviously a desire to bypass the TC39 lengthy process. By what I read here, I worry some of you — very important people — just seem to be pushing for your personal/company-specific interests, ahead of the common good.
I thank to @rniwa (and @Pauan) for having been there to force things to a good outcome.

@dcleao I would disagree with that conclusion, if the goal was to bypass TC39 entirely it would not have been brought up at all in TC39 since this is host provided meta-data.

@dcleao I don't think such accusations are fair or accurate; please mind your conduct going forward.

@domenic great work, very pleased to see this moving forward! Out of interest what exactly did @dcleao do to be seen as misconduct here? Surely opinions as to interests (even if they are wrong) are valid discussion points?

Ascribing selfish or corporate-greed motivated interests to individuals who are all working to make the web better in good faith, especially with loaded descriptors such as "obviously", is not welcome in this community. Similarly, encouraging others to "force" an outcome is pretty borderline as well.

Conduct discussions have a way of derailing otherwise technical threads; if this needs to be discussed further, please contact me directly by email, and do not engage here.

Commenting as a user and former PL researcher, the import.meta syntax is a bit jarring because that same syntax usually references properties. In my expectation a language statement doesn't usually have them. As to special URL, what about about:, which most users already expect to reveal metadata?

@Grimmr new.target, super.foo, etc, are existing precedents in the language (as of ES6/ES2015).

Yup, and my first encounter with new.target left me feeling a bit 😜. It does provide reasonable precedent but it also differs in attaching the metadata to a directly related language construct. That doesn't quite hold for import.meta, which inquires about the current module through the syntax for linking to another module, which makes for a more awkward fit.

So import.meta gives you info about how it was imported (how import was used), just like new.target gives you info about how it was constructed (how new was used)? Seems consistent to me :-)

how is a bit off here, it could be mutated later by the environment and might have things that mutate over time on purpose (pendingDeps or something for circular deps, isTopLevel, etc.).

about:blank is a new page, not some meta data about the document which loaded it so that doesn't work either. Please read the section about Using a particular module specifier, and raise your concern in TC39 if you have a very compelling reason to object to import.meta.

OK, so, import.meta is a thing! I have a tentative design for import.meta.scriptElement at https://github.com/tc39/proposal-import-meta/blob/master/HTML%20Integration.md . However, I wanted to check the design with this audience.

In particular, I think the biggest question is what is the value of import.meta.scriptElement inside descendants of the top-level module script. I think there are two possible answers:

  • The script element of the first-instantiated module script graph they appeared in. This can be nondeterministic based on what fetches first. (This is what is in the current HTML Integration draft linked above.)
  • null

I am strongly leaning toward null. In particular consider the situation

<script type=module src=a.mjs id=a></script>
<script type=module src=b.mjs id=b></script>

where both a.mjs and b.mjs do import "./c.mjs". Inside c.mjs, what is import.meta.scriptElement? I think it's very confusing for it to be nondeterministically either the a or b script elements. So just null seems better.

The counterargument is that in some larger percentage of cases, module scripts will not be shared among multiple graphs, and in such cases, it's really nice to be able to transparently refactor code into a sub-module without having to pass through import.meta explicitly.

Somewhat relatedly, what happens if a.mjs was imported twice as in:

<script type=module src=a.mjs></script>
<script type=module src=a.mjs></script>

Does module.meta.scriptElement refers to the first element? (I think this is what happens in your proposal).

What happens if we had:

<script type=module src=a.mjs></script>
<script type=module src=c.mjs></script>

where a.mjs imports c.mjs. Would c.mjs's import.meta.scriptElement start to refer to the second script element after we had discovered that there's a script element loading it? Or would it be null because the initiator was a.mjs?

what happens if a.mjs was imported twice as in:

Oh, wow, you're right, that's an even simpler case where things get messy :(. In my proposal as written right now I think it's nondeterministic since either one of those will be instantiated first... that's terrible, of course, and it should be the first one unless that's somehow infeasible with the spec/impl architecture.

What happens if we had:

I will ignore my proposal as it stands, as clearly I have not thought through all these cases very well, so any answers the proposal gives are accidental. Instead let's talk about what we want to be true.

I lean toward null, because to me the simplest mental model is that import.meta.scriptElement gets frozen the first time a script is evaluated. What do you (and others) think?

I guess we need to understand what the use cases of import.meta.scriptElement are. One recurring use case is that people want to insert some DOM nodes into the document at where the importing script element appears. If that's the one we'd like to support, then it seems like we'd always have to provide some script element as long as it's pulled in by a script element.

Let's say you have c.js which inserts a DOM node at where it's imported. Then it seems strange that when a.js imports c.js, it can no longer insert the same DOM node at where c.js is imported. More broadly, if b.js also imports c.js and both a.js and b.js are imported by script elements, it conceptually makes sense for c.js to insert the same DOM node to both places for the cases whereZ a.js and b.js's script elements might be inserted into two different shadow trees. But that really depends on what kind of DOM node is getting inserted by c.js as well.

We need to enumerate concrete use cases and study them more carefully before we can judge which proposal/behavior makes sense.

Agreed use cases will be useful. I've asked @justinfagnani to comment for Polymer which has plans to use this.

I anticipate most people won't be reusing a module script within multiple graphs on the page, so for their cases these issues won't matter that much. But I welcome correction from users.


Another issue raised by @nyaxt is that any design of import.meta.scriptElement implies holding a reference to the script element basically forever. Since the script element then has a pointer back to the Document (and that to Window), it keeps the entire DOM tree alive. I guess we could make it throw/return null if the page is navigated...

Is it being an Array not an option?

@matthewp one that mutates dynamically over time? (Also, what if someone pushes into it?) It seems a bit strange, especially since the script is only evaluated once, so it doesn't really have multiple script elements that actually affected the world, just one that did and a bunch of other no-ops.

Do you have a use case where a mutating-over-time array would work much better?

Probably a live NodeList instead of an array. I don't have any use-cases in favor of a list here, just the normal use cases for currentScript like @rniwa mentioned above. I use it to get the ownerDocument some times, but this is probably overly defensive.

I only mentioned a list structure to solve the problem of scriptElement possibly changing. I'd prefer a list that mutates over the reference changing to a different element. If it referred to only the first element, that would make sense to me as well.

So my use-cases would be around finding <template>s and other DOM resources associated with an element, test, etc. We have at times used more declarative styles of element and test definition where a script automatically finds its container or sibling. I know other projects have used currentScript to read attributes off the script element itself.

For finding templates, do you only care about the first script element which imports a given module script? Or do you care about every script element that imports your script?

Every script. The case would look something like this, simplified:

<x-declarative-element>
  <template>...</template>
  <script type="module">
    import {findTemplate} from '../x-element/x-element.js';
    const template = findTemplate(import.meta);
  </script>
</x-declarative-element>

Where findTemplate would use import.meta.scriptElement to find it's container, or sibling, etc.

I have also a use case for Shadow DOM. To do any DOM related scripting scoped to the shadow root <script> element is in.
And to let the script "avoid affecting the document tree" and work with actually encapsulated one.

<button>Pink like a deco umbrella</button>
<div>
  <!--shadowroot-->
    <red-door>I want it painted black</red-door>
    <script type="module">
      import {paintitblack} from '../stones.js';
      paintitblack(import.meta.scriptElement.getRootNode().querySelector('*'));
    </script>
  <!--/shadow-root-->
</div>

More at WICG/webcomponents#717

Something that seems to be the prevailing use-case is to access things near the script tag (whether shadow DOM/document/siblings/etc), but given that modules are only executed once per unique URL I think we'd also need another flag on script[type="module"] tags for these sort've cases.

For example perhaps a unique attribute that causes the script to be executed as if it were a unique script (similar to how every inline script is unique).

e.g. In this example the script would be executed twice, once with the first script tag and again with the second script tag, effectively as if they were both written as inline script tags containing the contents of the script they were loading (although a nice bonus is it would only need to fetch once):

<script unique type="module" src="./someScriptToBeExecutedAgainstCurrentDocumentOrShadowRoot.js"></script>

<div>
   <!--Shadowroot-->
   <script unique type="module" src="./someScriptToBe..."></script>
   <!--/ShadowRoot-->
</div>

Of course this could just be solved by using inline scripts directly as that's effectively what my suggestion is equivalent to, but it leads to quite a bit of boilerplate e.g.:

<script type="module">
    import someCodeToBeExecuted from "./someCodeToBeExecuted.js";
    someCodeToBeExecuted(import.meta.scriptElement);
</script>
<div>
   <!--ShadowRoot-->
   <script type="module">
      import someCodeToBeExecuted from "./someCodeToBeExecuted.js";
      someCodeToBeExecuted(import.meta.scriptElement);
   </script>
   <!--/ShadowRoot-->
</div>
commented

I often use a reference to the script tag to

  • read its innerHTML where I put configuration data in a block comment.
  • insert DOM nodes before the script tag
  • then remove the script tag to work around some edge cases when saving the page and loading it from disk again.

Since I like to use weak netbooks while traveling, I fear that having a long-lived reference to the script tag will invite implementation scenarios where memory is wasted without good reason. I'd thus prefer some getter function so scripts have to opt-in to the reference, and any script that doesn't care, won't stall garbage collection.

commented

For example perhaps a unique attribute

I consider that name confusing, as it's conceptually near "once", so it could just as well mean its opposite. How about "again", "run-again", "init-again" or sth. like that?
Update: Nope, that's confusing when it appears on the first script tag for that URL. Can't use "nocache" either because that might be interpreted as being about the transport layer.

Leaving aside implementation challenges for the moment, it is not clear that what is semantically incorrect about reusing 'document.currentScript'?
(Searched this thread for the word 'semantic'. Also went through the opening post and it did not give much clue).

commented

Meanwhile, for the "unique" attribute idea, I came to the conclusion that an "all or nothing" decision on whether to run the module again probably is suboptimal. In most use cases, we'd probably want the performance benefits that we can get from having the module cached and having the JS engine able to reuse optimization data that it might have collected on the module's behavior.

To avoid the boilerplate described by @Jamesernator, I suggest we make an attribute that specifies a name of an export that should be invoked as a function. Omitting the value or having it be the empty string should use the default export. I don't know a good name for that attribute so I suggest initfunc and hope "call" until someone can come up with a better name.

Update 1: Maybe it should be on… (onparse? onencounter?) so that various HTML sanitizer libs can detect it as potentially evil. Then again, it would be on a script tag that has an src attribute. In an ideal world, it wouldn't matter.
Update 2: Probably neither onparse nor onload, as they could sound related to the module instead of the script tag.

Update 3: FUD is a bad reason for an on prefix, and it's misleading because the existing on… attributes are to carry JS code, not just an identifier. How about just call? (Because "init" would imply an assumption about what the func does, and func is redundant.)

What's the timeline on import.meta.scriptElement being available / why not just make document.currentScript a parent/childless reference? To not X/Y problem why I'm looking for this, I was really looking for document.currentScript.dataset

why not just make document.currentScript a parent/childless reference?

Because current <script> may be placed in a shadow root, then another script would get access to this shadow root, what breaks the encapsulation. See WICG/webcomponents#717 (comment)

We a module needs a way to get the reference to the <script> element used in shadow root.

I guess another use case is worth to be mentioned here. From the script inside of shadow root there is a need to get some elements. Note the script could be defined outside either loaded dynamically or inline(via slot).
The script then need to access its owner element and than do the query. Pseudo-code:

let scriptNode = document.currentScript
,     parentComponent = recursiveUpLookup(scriptNode) // or scriptNode.getRootNode()
,     chart = parentComponent.querySelector('.chart');
// draw chart here

At this stage there is no way to identify where the script is called from as document.currentScript is null.

alternative proposition would be to imply the scope in 'document.getRootNode()' call. If the call originated from script within shadow dom, return its ownerElement instead of global document object.

Okay, people commenting on this thread need to read my comments in #1013 (comment) and #1013 (comment)

Because a given module gets executed exactly once regardless of how many times it gets referenced / imported, I don't think returning a single element is enough for address many use cases enumerated here. We need a mechanism to enumerate all script elements which references a given module.

commented

With a function that is triggered when the script element is encountered, it would suffice to set a property for the duration of that function call, or have some other function return the script tag reference for the duration of the trigger function call. The trigger function would be responsible for storing that ref if needed.

This issue is not just applicable to <script type="module">. Any script element inside a shadow root, whether it is a module or not, should have some way to access that shadow root. Consider this use case:

I have a widget where all the css, markup and logic is contained in a single html file. This widget is meant to be injected as a custom element into third party pages that I have no control over. Without a quick reference to the shadow root I have to jump through hoops to pass my own reference in.

My current workaround is to have the page owner include a single script tag at the location they want the widget to appear. This script adds a small helper framework to the document head which includes a global function that I call from within the widget script. This global function adds an event listener to the document body that executes the provided callback function. The initial loader script then replaces itself with a custom element and when the connectedCallback is triggered a bubbling event is fired that executes the widget logic.

Maybe I'm overcomplicating things, but that's a pretty convoluted dance to go through all because there is no document.currentShadowRoot or similar property...

Here's a link to the repo containing my demo if anyone wants to see exactly what I'm doing. https://github.com/besworks/widget-injector

commented

Why can't we just know the current node the script is in? Especially if it's the top level script being included... certainly for imports of imported scripts the currentNode shouldn't apply...

'use import.meta' is only helpful if the source of document.currentScript was... but I'd like to know what the parent node is of the current script to know what part of the HTML DOM to search with something like

<html>
<head>
</head>
<body>

<div id="profileContainer"></div>

</body>

<script type="module">

const rootNode = document.currentScript.parentNode;
/* get something... */ rootNode.querySelector("#profileContainer") 


</script>
</html>

Otherwise I guess I can select all scripts and somehow figure out if one is 'me'?

@d3x0r you say "top-level script", but read this comment (and there's others in the thread above) as to why that's a fuzzy concept here—that top-level script is only evaluated once, no matter how many times you include it:

<script type="module" src="entrypoint.js"></script>
<script type="module" src="entrypoint.js"></script>
<script type="module"> import './entrypoint.js'; </script>

So what's the currentScript?

Yes, for the inline case I guess there'll only ever be one script. But at that point it seems like you already know "where you are". Just my 2c.

commented

the first two are their own tag; the third, entrypoint.js doesn't get the script...

https://github.com/d3x0r/sack.vfs/tree/master/tests/objstore/userDb/ui/profile Is what I was playing with.

Part ...
index.html imports a script, and in that case, because the 'profile.js' can't get document... does the document lookup on itself, and this is a root sort of page, so it is obvious to find the location
profile.js Imports a utility library that makes popup windows, and extends that class, so when initialized loads the popup content from a .html page
profileForm.html The HTML template to fill the popup frame with; also has script tags which are modules, and I would expect their script tags to be themselves if they were src="script.js"; it loads profileForm.js
profileForm.js The JS part of the form; this needs to find the controls in profileForm.html ; and this is the script that wants to know it's relative location - which are tags in profileForm.html, or the container that ends up filled with profileForm.html.

The popup library fillFormURL(popup,url) function does a fetch of the URL, and then sets the .innerHTML of a div within the popup (which are approx. 5 nested frames), The script tags that are found are replaced with script tags that are the same, but will actually end up loading.

The index.html will eventually be some other service, which just includes "login.js" sort of script, and provide a 'login-with' sort of functionality... so there's no telling what the outside framing will really be.

certainly I can hear people saying 'don't do that' without a single answer to 'why' so... religious opinions aside... that's what I was attempting to do.

As a workaround <script> tags with textContent can have information prepended to them on the replace function to add a unique ID for the script itself, which allows scripts in the HTML to find their relative root... but cannot from any .js file itself; as would be loaded by a src attribute.

the first two are their own tag; the third, entrypoint.js doesn't get the script...

I'm not sure what do you mean there. In the example given, entrypoint.js will execute exactly once. Please go read the whole issue, especially #1013 (comment) and the subsequent discussion. It's not productive to keep repeating the same discussion over and over.

I would also like a way to get the ShadowRoot object from with in the shadow DOM's own scripts. Wasn't this the whole point of encapsulation? Am I missing something? I honestly don't know.