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

Introduce pointer to <script> element in module scripts #1013

Open
annevk opened this issue Apr 8, 2016 · 113 comments
Open

Introduce pointer to <script> element in module scripts #1013

annevk opened this issue Apr 8, 2016 · 113 comments
Labels
addition/proposal topic: script

Comments

@annevk
Copy link
Member

@annevk annevk commented Apr 8, 2016

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.

@domenic domenic added the addition/proposal label Apr 8, 2016
@domenic
Copy link
Member

@domenic domenic commented Apr 8, 2016

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.

domenic added a commit that referenced this issue Apr 11, 2016
They no longer set document.currentScript, or fire beforescriptexecute
and afterscriptexecute events.

Closes #997. See #1013 for a future alternative to
document.currentScript, and #943 for more discussion of the events.
domenic added a commit that referenced this issue Apr 13, 2016
They set document.currentScript to null, and no longer fire
beforescriptexecute and afterscriptexecute events.

Closes #997. See #1013 for a future alternative to
document.currentScript, and #943 for more discussion of the events.
domenic added a commit that referenced this issue Apr 14, 2016
They set document.currentScript to null, and no longer fire
beforescriptexecute and afterscriptexecute events.

Closes #997. See #1013 for a future alternative to
document.currentScript, and #943 for more discussion of the events.
annevk pushed a commit that referenced this issue Apr 15, 2016
Make module scripts modify less global script state. They set document.currentScript to null, and no longer fire beforescriptexecute and afterscriptexecute events.

Closes #997. See #1013 for a future alternative to document.currentScript, and #943 for more discussion of the events.
annevk added a commit that referenced this issue Apr 22, 2016
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.
@annevk annevk mentioned this issue Apr 22, 2016
domenic pushed a commit that referenced this issue Apr 22, 2016
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.
annevk added a commit that referenced this issue Apr 23, 2016
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.
domenic pushed a commit that referenced this issue Apr 26, 2016
This makes <script> elements work when used in shadow trees.

Note that document.currentScript is set to null while running a <script>
in a shadow tree; see #1013 for details.

This takes care of most of #762, but it remains to make the "load" event
scoped, so we'll leave that issue open for now.
@matthewp
Copy link

@matthewp matthewp commented Oct 26, 2016

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.

@domenic
Copy link
Member

@domenic domenic commented Oct 26, 2016

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.

@domenic
Copy link
Member

@domenic domenic commented Nov 10, 2016

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";

@guybedford
Copy link

@guybedford guybedford commented Jan 12, 2017

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?

@Constellation
Copy link
Member

@Constellation Constellation commented Feb 9, 2017

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.

@domenic
Copy link
Member

@domenic domenic commented Feb 22, 2017

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

@bmeck
Copy link

@bmeck bmeck commented Feb 22, 2017

"js:context" looks fine but we are open to bikeshedding from Node.js side.

@bicknellr
Copy link
Contributor

@bicknellr bicknellr commented Feb 22, 2017

Maybe it would be worth carving out the entire js: protocol-like-prefix for future stuff like this?

@domenic
Copy link
Member

@domenic domenic commented Feb 22, 2017

It effectively already is carved out, since fetching any URL that starts with js: will fail per today's specs.

@ajklein
Copy link
Contributor

@ajklein ajklein commented Feb 22, 2017

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

@rniwa
Copy link
Collaborator

@rniwa rniwa commented Feb 22, 2017

That seems like such a hack. There is a proposal for global which is encountering some compatibility issue. Perhaps global should also live there?

@bmeck
Copy link

@bmeck bmeck commented Feb 22, 2017

@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())?

@domenic
Copy link
Member

@domenic domenic commented Feb 22, 2017

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?

@rniwa
Copy link
Collaborator

@rniwa rniwa commented Feb 23, 2017

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"?

@bmeck
Copy link

@bmeck bmeck commented Feb 23, 2017

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

@matthewp
Copy link

@matthewp matthewp commented Feb 23, 2017

"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?

@nektro
Copy link

@nektro nektro commented Oct 13, 2018

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

@tomalec
Copy link

@tomalec tomalec commented Oct 15, 2018

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.

@sashafirsov
Copy link

@sashafirsov sashafirsov commented Nov 1, 2018

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.

@sashafirsov
Copy link

@sashafirsov sashafirsov commented Nov 1, 2018

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.

@rniwa
Copy link
Collaborator

@rniwa rniwa commented Nov 6, 2018

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.

@mk-pmb
Copy link

@mk-pmb mk-pmb commented Nov 6, 2018

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.

@besworks
Copy link

@besworks besworks commented Nov 10, 2018

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

alice pushed a commit to alice/html that referenced this issue Jan 8, 2019
This integrates with the stage 3 import.meta proposal located at
https://github.com/tc39/proposal-import-meta/. This is based on
https://github.com/tc39/proposal-import-meta/blob/f5d39bc471a5bf2791708f9a3fec943380d9e3ee/HTML%20Integration.md
although it only includes the easier part, import.meta.url.
import.meta.scriptElement is still being discussed, at whatwg#1013, and as
such is excluded for now.
@d3x0r
Copy link

@d3x0r d3x0r commented Jun 11, 2021

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'?

@samthor
Copy link

@samthor samthor commented Jun 11, 2021

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

@d3x0r
Copy link

@d3x0r d3x0r commented Jun 11, 2021

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.

@rniwa
Copy link
Collaborator

@rniwa rniwa commented Jun 11, 2021

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.

@whaaaley
Copy link

@whaaaley whaaaley commented Feb 11, 2022

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.

@besworks
Copy link

@besworks besworks commented May 14, 2022

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

connectedCallback() {
  const scopedEval = (script) => Function(script).bind(this)();
  const scripts = this.shadowRoot.querySelectorAll('script');
  scripts.forEach(s => scopedEval(s.innerHTML));
}

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.

@bathos
Copy link

@bathos bathos commented May 14, 2022

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

@besworks
Copy link

@besworks besworks commented May 14, 2022

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

@whaaaley
Copy link

@whaaaley whaaaley commented May 15, 2022

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/react@18.1.0/umd/react.production.min.js'
        import 'https://unpkg.com/react-dom@18.1.0/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 this inside of script modules undefined? But this usually refers to window. Would it not make way more sense for this to return some type of object pertaining to the module, and if that script is loaded within a shadowRoot, then it would have a property like this.shadowRootDocument??? Or even just this.document for compatibility with old libraries?

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 import.meta is just a weird syntax to make up for the lack of this inside scoped contexts like a shadowDOM. Maybe someone can educate me on why this is the case. I'd actually like to know.

@besworks
Copy link

@besworks besworks commented May 15, 2022

@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 document.currentScript issue discussed here seems related but is actually a byproduct of what you're describing, which is ultimately an issue with how the evaluated scripts are scoped.

@Jamesernator
Copy link

@Jamesernator Jamesernator commented May 15, 2022

but to me it sounds to me like import.meta is just a weird syntax to make up for the lack of this inside scoped contexts like a shadowDOM.

Kind've, basically this was used in all sorts of ways in classic scripts, which is why strict module and modules narrowed their uses to be more the usual intent of this (i.e. the object for methods). import.meta was added as a way to more clearly associate something per-module rather than overloading this to have yet another special-cased behaviour.

The advantage of having a separate thing, is that import.meta can even be used inside methods that might have a different this (i.e. in classes).

It would be plausible to add import.meta.script for inline scripts, but not for src scripts as in general many <script src= could point to the same module (or the module might've even been loaded with import(...)). (And exposing the first script, would lead to very race-condition prone code).

If HTML modules ever get implemented, there would be a similar inline-only script feature (import.meta.document) that is pretty similar, so it wouldn't be particularly strange to expose a import.meta.scriptElement for inline scripts.

@mk-pmb
Copy link

@mk-pmb mk-pmb commented May 15, 2022

import.meta.scriptElement sounds reasonable to me.

@whaaaley
Copy link

@whaaaley whaaaley commented May 15, 2022

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 import.meta as well? It seems to me the primary reason for wanting scriptElement is just so we can traverse upwards to the current document because that's how it used to be done in v0 of shadow dom api... So why not just add import.meta.currentDocument which refers to the document the script is loaded in and avoid the traversal step all together? Then there's also no confusion with which script element to return in the case of multiple script imports. Let the developers query it themselves once they have the current document.

@whaaaley
Copy link

@whaaaley whaaaley commented May 15, 2022

@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 import.meta. Though I did really like your idea of configuring how shadowRoot scripts work with attachShadow options. That should definitely be expanded on as well.

@besworks
Copy link

@besworks besworks commented May 15, 2022

I agree that import.meta.currentDocument would be a good property to have. As long as it actually returned the shadowRoot and not the document that the shadowRoot is connected to. I think that import.meta.scriptElement would definitely be useful too. Then we could do import.meta.scriptElement.ownerDocument or access siblings, etc. These would be great for many use cases, but the way scripts in a shadowRoot are currently evaluated, their ownerDocument is actually the host document. So implementing either of these properties alone is not enough. I believe more work needs to be done to make the ShadowRoot interface behave more like a wrapper for the Document interface instead of just the DocumentFragment interface which it currently extends.

@whaaaley
Copy link

@whaaaley whaaaley commented May 15, 2022

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

@besworks
Copy link

@besworks besworks commented May 15, 2022

Here's an example of what I mean :

<div id="host"></div>
<script type="module">
  let host = document.querySelector('#host');
  let shadow = host.attachShadow({ mode:'closed' });
  let script = document.createElement('script');
  script.textContent = `console.log(document.querySelector('#host'))`;
  shadow.append(script); console.log(script.ownerDocument);
</script>

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 import.meta.currentDocument from inside a shadowRoot, you would still get a reference to the document that contains your component and not the shadowRoot like you want unless special handling was put in place for this scenario. This may still prove to be the best route and less complex than messing with the ShadowRoot interface, though I do still believe there is room for improvement there.

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

Scripting is disabled for a node when its node document browsing context is null.

Which is exactly the case when using document.implementation.createHTMLDocument() and DOMParser() and the same with new Document() ...

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.

@mk-pmb
Copy link

@mk-pmb mk-pmb commented May 16, 2022

It seems to me the primary reason for wanting scriptElement is just so we can traverse upwards

I for one want to scriptElement.getAttribute(…).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
addition/proposal topic: script
Development

No branches or pull requests