whatwg / html

HTML Standard
https://html.spec.whatwg.org/multipage/
Other
7.98k stars 2.61k forks source link

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

Open annevk opened 8 years ago

annevk commented 8 years ago

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 commented 8 years ago

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.

matthewp commented 7 years ago

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 commented 7 years ago

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 commented 7 years ago

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 commented 7 years ago

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 commented 7 years ago

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.

domenic commented 7 years ago

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 commented 7 years ago

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

bicknellr commented 7 years ago

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

domenic commented 7 years ago

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

ajklein commented 7 years ago

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 commented 7 years ago

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 commented 7 years ago

@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 commented 7 years ago

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 commented 7 years ago

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 commented 7 years ago

@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 commented 7 years ago

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

domenic commented 7 years ago

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.

domenic commented 7 years ago

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

bmeck commented 7 years ago

I'm not super keen on "module" being in there if it also works in Scripts via import()

caridy commented 7 years ago

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

bmeck commented 7 years ago

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

Pauan commented 7 years ago

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.

bmeck commented 7 years ago

@Pauan as per Ecma262 modules are idempotent per Source Text / Import Specifier pair. They are not idempotent per global environment / realm.

domenic commented 7 years ago

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

Pauan commented 7 years ago

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

rniwa commented 7 years ago

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.

bmeck commented 7 years ago

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

domenic commented 7 years ago

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?

annevk commented 7 years ago

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

bmeck commented 7 years ago

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

rniwa commented 7 years ago

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.

bmeck commented 7 years ago

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

annevk commented 7 years ago

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

rniwa commented 7 years ago

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.

domenic commented 7 years ago

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.

caridy commented 7 years ago

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.

bmeck commented 7 years ago

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

Pauan commented 7 years ago

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.

Pauan commented 7 years ago

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.

domenic commented 7 years ago

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.

bmeck commented 7 years ago

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

dherman commented 7 years ago

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

bmeck commented 7 years ago

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

DanielHerr commented 7 years ago

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

domenic commented 7 years ago

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

rauschma commented 7 years ago

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

Jamesernator commented 7 years ago

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

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.

domenic commented 7 years ago

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.

bmeck commented 7 years ago

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.