-
Notifications
You must be signed in to change notification settings - Fork 2.7k
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
Introduce pointer to <script> element in module scripts #1013
Comments
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.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. |
This makes <script> elements work when used in shadow trees. The beforescriptexecute and afterscriptexecute events won’t work, since they are already disabled for modules and only makes sense together with currentScript, which cannot be made to work for shadow scripts. See #1013 for details. Fixes #762.
This makes <script> elements work when used in shadow trees. The beforescriptexecute and afterscriptexecute events won’t work, since they are already disabled for modules and only makes sense together with currentScript, which cannot be made to work for shadow scripts. See #1013 for details. Fixes #762.
This makes <script> elements work when used in shadow trees. The beforescriptexecute and afterscriptexecute events won’t work, since they are already disabled for modules and only makes sense together with currentScript, which cannot be made to work for shadow scripts. See #1013 for details. Fixes #762.
Is it correct to say that <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 |
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"; |
Note also feature detection here also seems tricky. I assume |
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 How does that sound to implementers? /cc @whatwg/modules |
|
Maybe it would be worth carving out the entire |
It effectively already is carved out, since fetching any URL that starts with |
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 |
@rniwa how is it a hack if it needs context to the environment it was invoked from (in this case, the location of |
I don't think @rniwa, putting |
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 |
"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? |
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 <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 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. |
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.
The popup library 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 |
I'm not sure what do you mean there. In the example given, |
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. |
@whaaaley and others, maybe this will help you out. I can't remember where/when I came up with this method but it definitely belongs in this thread :
This runs any script elements in the shadowRoot of a custom element with the host object as the scope. Here's a more thorough example using a closed shadowRoot. |
@besworks the number of reasons that’s unsound are too many to enumerate. it may suit a particular case but should not be promoted as a general solution. |
@bathos, obviously the correct solution in most cases would be to simply not use script tags inside your shadow dom at all but for those few cases that it's necessary (like importing a clunky old library) this trick is like magic. The snippet above is clearly a simplified demonstration. As you can see in the jsfiddle I used #private class properties and methods to make sure this cannot be executed from outside the component. It also works with a closed shadow root so there's no worry about script injection that way. Can you think of any other ways this could be abused? |
The reason I want this is because I've been experimenting with different ways to load random components built with modern frameworks into shadow roots. Assuming you're using some kind of virtual dom framework in the parent application you could do something like this with a global. function React () {
// eslint-disable-next-line no-return-assign
return <ShadowRoot id='react'>
{window._reactRef = <div id='root'></div>}
<script type='module'>
{`
import 'https://unpkg.com/[email protected]/umd/react.production.min.js'
import 'https://unpkg.com/[email protected]/umd/react-dom.production.min.js'
const root = ReactDOM.createRoot(window._reactRef.node)
root.render(
React.createElement(React.StrictMode, {},
React.createElement('div', {}, 'hello from react')
)
)
`}
</script>
</ShadowRoot>
} I started to wonder though. Why is I mean don't mind me. I don't know much about the details of the module system, but ergonomics wise, and for symmetry with how you'd do this without shadowDOM, this makes sense to me over something to do with the import statement. Maybe this is naive, but to me it sounds to me like |
@whaaaley, I've done some experimenting with various ways to inject scripts into shadowRoots as well and just posted a very thorough overview of this problem in WICG/webcomponents#717 (comment) I believe this discussion more appropriately belongs in that thread, even though it's closed. The |
Kind've, basically The advantage of having a separate thing, is that It would be plausible to add If HTML modules ever get implemented, there would be a similar inline-only script feature ( |
|
I'm down for any solution that will give me a path to get access to the shadow root document. Is there any chance we can get access to the current document in |
@besworks I read your post in the other thread. I can see both ideas being correct. However, I think the path of least resistance is probably slapping more stuff onto |
I agree that |
@besworks The host document or the host node? 🤔 There's some stuff I could do with a host node to get some desired behavior at least. But not with a host document. |
Here's an example of what I mean :
You might expect that after the script was appended to the shadowRoot it would no longer be part of the host document. But this not the case. The shadowRoot is not actually a separate Document but rather a DocumentFragment contained as a property of the host element. We have full access to the host document scope and every element in it but no access to the scope the script is actually connected to, unless you drill down into it from outside, which you can't do if you use a closed shadowRoot. So, even if you were to access the hypothetical One technique I experimented with involved building a virtual document to process my scripts in with the intention of appending the output to the shadowRoot afterwards. This didn't work out because according to section 8.1.3.4 of the spec
Which is exactly the case when using So, with all that in mind, the best workaround I've come up with, is the spooky dark magic method that always seems to ruffle everyone's feathers. But it gets the job done and, if used responsibly, overcomes all of these problems. |
I for one want to |
not sure if it's entirely related, but adding a use case here anyway. i'm trying to write some isolated code examples for a documentation site. i wanted to use declarative shadow dom for this, so styles and IDs do not leak out. but i also wanted to include some scripts as part of demos e.g. <div class="example">
<template shadowrootmode="open">
<style>
button {
color: red;
}
</style>
<button>click me</button>
<script>
// how to select the button?
</script>
</template>
</div> e.g. here i wanted to log a click on the button. but there's no easy way to get a reference to the button which doesn't completely detract from the example itself. it would be nice if there were something like |
One of my favourite patterns (because its very robust) is to do the following: <script data-foo="someOptionAddedByServerSideTemplatingEngine">
(function(options){
console.log(options.foo);
})(document.currentScript.dataset);
</script> I know - this seems complicated but - trust me - i have use-cases, where this is the most robust way (dynamic generation of script tags, template engines, escaping, ide code handling, lsp support, and so on). But with this pattern i am stuck to regular script types. I would really like to convert this to <script type="module" data-foo="someOptionAddedByServerSideTemplatingEngine">
const options = import.meta.currentScript.dataset;
console.log(options);
</script> |
Would having It seems to me that "what script element caused the loading of this module?" and/or "what script elements are connected to this module in the import graph?" are more complex and less useful questions to ask compared to "what script element contains this code?" Conceptually, I think of Does that make sense? |
I think this makes some sense on the surface, but there's a big hazard with things changing so much depending on whether the script is inline. Tools may change a script from inline to external or vice-versa and break things. You would get roughly the same effect if |
As @rniwa has pointed out, though, there isn't really a well-defined answer to "what script tag imported this module?" (nor, even, for "what module imported this module?") because a module can be imported from a bunch of different places at different times. That's why I think focusing on where the module is defined is a more well-founded approach. The hazard of module behaviour changing depending on whether the script is inline applies to For external modules, this pattern might be reasonable: <script type="module">
import something from 'module.js'
something(import.meta.scriptElement)
</script> The 1:N definition:import relationship has to get dealt with somewhere, but this way your The only other answer I can think of would be some kind of import hook that a module could use to know when it's been imported and what by? That seems like a whole can of worms though... |
I could imagine a world in which the script tag has a getter for the exports of the imported module and we could use <script type="module" src="http://example.net/opaque.js" onload="
const scriptTag = this;
scriptTag.getExport('default').then(m => m.install(scriptTag));
">/* … arbitrary data, e.g. config settings … */</script> Maybe we can even convince browsers to support a <script type="module" src="http://example.net/opaque.js" run="install">/* …data… */</script> Or when it's empty, it means that the default export is expected to be the entrypoint function, i.e. <script type="module" src="http://example.net/opaque.js" run>/* …data… */</script> would be the same as <script type="module" src="http://example.net/opaque.js" onload="
const scriptTag = this;
scriptTag.getExport('default').then(m => m(scriptTag));
">/* …data… */</script> I think the four bytes " run" could become one of webdev's most beloved HTML additions. (In before "you can even omit the space!") |
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.The text was updated successfully, but these errors were encountered: