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

HTML Modules: multiple scripts exporting same named export #831

Closed
joeldenning opened this issue Aug 28, 2019 · 6 comments
Closed

HTML Modules: multiple scripts exporting same named export #831

joeldenning opened this issue Aug 28, 2019 · 6 comments
Labels

Comments

@joeldenning
Copy link

I just implemented a polyfill for HTML modules in systemjs/systemjs#2011. In doing so, the spec wasn't clear to me about how to handle the follow situation:

<script type="module">
  export const foo = 1;
</script>
<script type="module">
  export const foo = 2;
</script>

This section of the spec did not clarify this for me.

Here's the languaging for default exports (but not named exports):

Additionally, if an inline script element specifies a default export it will be passed through as the default export of the HTML Module (multiple inline scripts specifying a default will result in an instantiation error for the HTML Module). If no script specifies a default export, the HTML Module's document will be the implicit default export of the module. This will allow declarative content of a module to be consumed without the use of inline script elements in the module to specify exports.

And here's the languaging about exports in general:

An HTML Module will specify its exports using its inline script elements. Inline Script Modules in an HTML Module document will have their exports redirected via the HTML Module Record such that the importer of the HTML Module can access them. This is done by computing the exports for the HTML Module's record during its instantiation phase as per the following pseudocode:

for (ModuleScript in HtmlModuleRecord.[[RequestedModules]]) {
    if (ModuleScript.IsFromInlineScriptElement) {
        export * from ModuleScript;
    }
}

The pseudocode comes closest to explaining, but I'm not sure whether it should (1) throw when encountering the same export, or (2) "last script's export wins". If (2), then you have to account for live bindings of the export -- can the first module update the live binding even though it's initial value was ignored? Note that afaik default exports do not have this problem because default exports aren't really a live binding (no way of updating them after the initial export)

@dandclark
Copy link
Contributor

Hi Joel,

Considering your example HTML module:

<script type="module">
  export const foo = 1;
</script>
<script type="module">
  export const foo = 2;
</script>

The above will throw during import/export resolution if and only if the importer of this HTML module imports foo. That is, this will trigger an error:
import {foo} from "./html-module.html"
but this will not:
import "./html-module.html"

One way of looking at this is that the HTML module example is roughly equivalent to this code written with only JavaScript modules:
module.js:

export * from "./leaf1.js";
export * from "./leaf2.js";

leaf1.js:

export const foo = 1;

leaf2.js:

export const foo = 2;

The following will throw, as the conflict between the two 'foo' exports will be detected during import resolution:
import {foo} from "./module.js"
Whereas importing in the following manner will execute the module tree without errors:
import "./module.js"

This is because module import/export linking is primarily import-driven; if nothing imports the duplicated name then no ambiguity is detected, which is fine because no one can observe either of the conflicting bindings.

The HTML module case is a little less intuitive because both export const foo statements occur in the same file. However, the module graph built from it contains 3 distinct module records: the HTML module itself, the JS module for the first inline script, and the JS module for the second inline script.

Hope that helps!

@joeldenning
Copy link
Author

Thanks for the clarification. Would this clarification be useful in the spec itself? Or does the pseudocode in the docs make it clear for those more familiar than me with export * and es module linking?

For the SystemJS polyfill, I don't think we'll be able to be 100% spec compliant with this, since our module linking passes the entire module to the dependent module, without knowing which of its exports are being used or not (cc @guybedford who might be able to clarify).

But we can detect when the same thing is exported twice by two different modules, and can throw when that happens. So the SystemJS polyfill would end up a bit stricter than the spec -- it would always throw even if you aren't importing that specific export.

@dandclark
Copy link
Contributor

I can add a note clarifying scenario to that spec/explainer doc. I suppose that the pseudocode implies this behavior, but it is by no means obvious (at least to me) even in my JS-module-only example above that things should fail only if a conflicting export is actually imported at some point.

Being stricter than the spec seems reasonable here. I assume SystemJS differs from the native JavaScript module behavior in the same way? If it isn't a problem for JS modules then I don't expect that it would be a problem for HTML modules.

@joeldenning
Copy link
Author

I assume SystemJS differs from the native JavaScript module behavior in the same way

Yeah I think so -- all js modules loadable by systemjs are ultimately converted to the System.register format, which is an es5 format that emulates almost all of the js module nuances, including live bindings via the setters property. Up until now, I thought it perfectly emulated everything about js modules, but your explanation clarifies that it does not handle this one particular edge case. But notice that the module dependencies only specify the module name, not which exports from the module they need. And that the setters property is given the entire module, not just the imports it asked for.

I consider my question to be answered, but will leave this issue open in case you want to use it to track that clarifying scenario in the spec explainer doc.

@dandclark
Copy link
Contributor

The PR here updated the explainer doc to include some general clarifying comments with a link back to this issue. Should be fine to close out this issue now.

@joeldenning
Copy link
Author

👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants