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

Order/Priority of multiple import maps #114

Closed
joeldenning opened this issue Mar 13, 2019 · 20 comments · Fixed by #143
Closed

Order/Priority of multiple import maps #114

joeldenning opened this issue Mar 13, 2019 · 20 comments · Fixed by #143
Assignees
Milestone

Comments

@joeldenning
Copy link
Contributor

When there are multiple import maps present on a page, which one takes precedence? Is "document order" employed to determine the order?

Is the document order enforced at the moment when "acquiring import maps" is set to false? Or is document order enforced during initial loading of the page, and then dynamically injected import maps ignore document order and are applied in the order in which they are injected into the document?

Scenario 1 - Two fetched import maps during initial load/processing

<script type="importmap" src="/import-map1.json"></script>
<script type="importmap" src="/import-map2.json"></script>
<script type="module" src="import:widget"></script>

If import-map1.json takes longer to fetch than import-map2.json, which import map takes precedence? Meaning if both define the same module, which import map wins?

Scenario 2 - No import maps during initial load, two dynamically injected in reverse document order

<script>
  const importMap1 = document.createElement('script')
  importMap1.type = 'importmap'
  importMap1.textContent = JSON.stringify(getDynamicMap1FromSomewhere())
  document.head.appendChild(importMap1)

  const importMap2 = document.createElement('script')
  importMap2.type = 'importmap'
  importMap2.textContent = JSON.stringify(getDynamicMap1FromSomewhere())
  document.head.insertBefore(importMap2, importMap1)
</script>
<script type="module" src="import:widget"></script>

Import Map 2 is first in the DOM, but it was injected into the DOM second. Which one takes priority?

Scenario 3 - Dynamic import injected after initial page load

<script id="importmap1" type="importmap" src="import-map.json"></script>
<script>
  setTimeout(() => {
    const importMap = document.createElement('script')
    importMap.type = 'importmap'
    importMap.textContent = JSON.stringify(getDynamicMap1FromSomewhere())
    document.head.insertBefore(importMap, document.getElementById('importmap1'))
    import('import:widget')
  }, 3000)
</script>

Very similar to Scenario 2, except accentuating the difference in timing of when these import maps get put into the DOM. Which import map will take priority?

@matthewp
Copy link

Have you looked at this: https://github.com/WICG/import-maps/blob/master/spec.md#installation ? It doesn't directly answer your questions but I think most can be inferred. For 1) I would definitely expect /import-map2.json to be merged on top of (and "win") the first.

@joeldenning
Copy link
Contributor Author

joeldenning commented Mar 14, 2019

Yes, I have read that section, but I don't actually see anything related to order or priority of import maps. What sentences are you referring to? All I see is this:

Encountering a <script type="importmap"> while acquiring import maps is true will kick off a procedure

Which seems to indicate that "encountering" an import map is what kicks of the procedure, but it doesn't explain what that means. Is it an encounter during initial load? Is it in document order? What about dynamically injected import maps, do they take priority when they are "encountered" in the DOM? Does that respect document order? What if fetching one import map takes longer than another?

The spec is quite unclear as far as I can tell, which is why I created this issue 😄

@guybedford
Copy link
Collaborator

guybedford commented Mar 14, 2019 via email

@joeldenning
Copy link
Contributor Author

@guybedford what is script processing order? When I say document order, I am referring to the definition here. I assume script processing order would be the same? Although maybe respecting the defer/async attributes?

Also, how does that relate to Scenario 3?

@guybedford
Copy link
Collaborator

guybedford commented Mar 14, 2019 via email

@joeldenning
Copy link
Contributor Author

There is absolutely no support for dynamically injecting import maps in the spec so there aren’t any edge cases in those scenarios.

What about https://github.com/WICG/import-maps#dynamic-import-map-example, which shows how to do dynamic imports? Is the Readme out of date?

@guybedford
Copy link
Collaborator

guybedford commented Mar 14, 2019 via email

@joeldenning
Copy link
Contributor Author

I don't believe that mutation observers would be necessary to implement this in a userland polyfill, unless the polyfill wanted to implement the following part of the spec:

If a <script type="importmap"> is encountered when acquiring import maps is false, then the developer has made an error. We will signal this by firing an error event on the <script type="importmap"> element, and implementations should also display the error in the developer console.

Supporting this ^ would require a MutationObserver, I think, but in the non-error case I think that you can get away without a MutationObserver. Instead, a document.querySelectorAll('script[type="importmap"]') that occurs either during initial loading of the page, the first <script type="module"> that is encountered, or both (if you want to start prefetching import maps before processing).

Back to the original question, though, I'm still looking for answers on how to handle Scenarios 1, 2, and 3.

@domenic
Copy link
Collaborator

domenic commented Mar 14, 2019

I just want to say these are all good questions, and I don't have good answers yet. I'm hoping to write a more formal spec soon-ish, but until then I can only offer vague intuition, plus the offer to test with Chrome's experimental implementation to get some idea of what at least one implementer thought was possible and relatively easy to implement.

Stay tuned!

@matthewp
Copy link

@joeldenning

Yes, I have read that section, but I don't actually see anything related to order or priority of import maps. What sentences are you referring to?

There isn't one that directly answers your question. I was just pointing out the link in case you hadn't seen it. The Merging import maps section gives a general idea of prioritization in that the "last one wins".

My intuition is that this should behave the same as JavaScript scripts/modules. If we have 2 scripts that both define the global window.Foo you would have a similar problem to the scenarios you described.

@joeldenning
Copy link
Contributor Author

👍 thanks for the update.

As someone attempting a polyfill implementation in SystemJS, I'll throw in my two cents on an approach the spec could take.

I think it would make most sense for import maps to begin fetching as soon as possible, but that the priority of the import maps should be the document order of the <script type="importmap"> elements at the moment when "acquiring import maps" changes from true to false. Doing it this way gives more power to javascript developer, because they can insert/remove/reorder import maps however they want to before that moment when "acquiring import maps" switches to false.

An alternative approach would be to use document order for the initial page load but then just immediately apply dynamic imports as they get injected after the initial page load, regardless of their document order. I think that this is more confusing for the html/javascript developer, because the final merged import map cannot be reasoned about or reconstructed by looking at the document order in the DOM.

@hiroshige-g
Copy link
Collaborator

Thanks for inputs!
Brainstorming some of my thoughts.

[1] order of fetch completion

In the initial spec draft #136, the priority of import maps is the order of fetch completion (external import maps) or insertion to DOM (inline import maps).
I agree this is not good for users: There should be determinism (I just didn't notice this issue while writing the initial draft).

  • Scenario 1: Depends on which is loaded from the network first.
  • Scenario 2: importMap1 < importMap2
  • Scenario 3: Depends on when importmap1 is loaded from the network.

(a < b means b wins/overrides a)

[2] order of #prepare-a-script

How about: the priority of the import maps is the order of #prepare-a-script, i.e. roughly the order of insertion of <script type="importmap"> to the DOM.

  • Scenario 1: import-map1.json < import-map2.json
  • Scenario 2: importMap1 < importMap2
  • Scenario 3: importmap1 < importMap

Probably something like:

  • Document will have an ordered list of pending import maps
  • In prepare-a-script, the script element is added at the end of the ordered list.
  • At the moment when "acquiring import maps" changes from true to false, the import maps in the ordered list are merged into a single import map which is registered to the Document.

I feel this is consistent and easy to implement in terms of spec (and probably Chromium implementation).
This is defer-ish, or #list-of-scripts-that-will-execute-in-order-as-soon-as-possible-ish (which is almost never used), where <script> are executed in the order of #prepare-a-script, not the document order at the time of HTML parsing finish.

[3] document order

the priority of the import maps should be the document order of the <script type="importmap"> elements at the moment when "acquiring import maps" changes from true to false.

  • Scenario 1: import-map1.json < import-map2.json
  • Scenario 2: importMap2 < importMap1
  • Scenario 3: importmap1 < importMap

This also looks nice, but I feel some tricky things are needed.
Basically spec/impl would be similar to [2], and additionally we reorder the import maps according to the document order.
In the current spec, the state of DOM (document order, attributes, etc.) after #prepare-a-script is basically not used and the state of DOM is used only when #prepare-a-script, so I feel the use of the document order at the time of clearing "acquiring import maps" is a little tricky.
(we shouldn't rely only on DOM traversal, because in corner cases there can be <script type="importmap"> in the DOM which wasn't prepared as an import map, e.g.

<script id="scr1">console.log("Some scripts are executed");</script>
<script>
  scr1.setAttribute('type', 'importmap');
  scr1.innerText = '{}';
</script>

)

Also, the exact timing of when "acquiring import maps" changes from true to false is affected by network, and thus there would be nondeterminism if the document order changes while import maps is loading (this is also corner cases that I don't expect to occur in real valid cases).

An alternative approach would be to use document order for the initial page load but then just immediately apply dynamic imports as they get injected after the initial page load, regardless of their document order. I think that this is more confusing for the html/javascript developer

I feel this is also harder to implement in spec and in Chromium.
There are no existing mechanisms that do this kind of hybrid ordering.

@domenic
Copy link
Collaborator

domenic commented Jun 25, 2019

(2) makes a ton of sense to me. It basically means the import maps get "evaluated" (i.e. parsed and merged into the realm's overall import map) like classic scripts are.

@joeldenning
Copy link
Contributor Author

joeldenning commented Jun 25, 2019

I think that Option 2 is confusing for a js developer debugging what the final resolved URL is for a module. "Why did this import map outprioritize the other?" cannot be understood without mutation observer or stepping carefully through code.

Additionally, Option 2 is much more difficult to polyfill (afaict, mutation observers would be required), although polyfillability might not be an overriding concern.

One thing I like about Option 2 is that it isn't tied to the "acquiring import maps" boolean, which I think is great because that boolean seems unnecessarily limiting to the js developer and I don't see a technical constraint that calls for it. (That's a separate topic discussed in #92, though)

Perhaps the debuggability concern I raised could be solved through a future API that exposes all the import maps, including their injection order and the final merged map. Option 2 sounds okay to me, I think my perspective has changed since my last post a few months ago 👍

@hiroshige-g
Copy link
Collaborator

"Why did this import map outprioritize the other?" cannot be understood without mutation observer or stepping carefully through code.

I think this is the same as defer/module scripts, which are executed in the order of prepare-a-script, regardless of the final document order in the DOM. ("the same as existing scripting mechaism" doesn't necessarily mean that it is good though)

it isn't tied to the "acquiring import maps" boolean

Do you mean that the priority of import maps doesn't depend on the timing when "acquiring import maps" boolean is cleared? Or any other specific aspects? (Still "acquiring import maps" boolean forbids new import maps once cleared)

@hiroshige-g
Copy link
Collaborator

Additionally, Option 2 is much more difficult to polyfill (afaict, mutation observers would be required)

Good point.

I'm wondering how strictly we should keep the interoperability between browsers/spec and polyfills.
There are many corner cases where implementing the #prepare-a-script order is hard (even with mutation observers), including modifying the <script>'s attribute after insertion, moving <script> from another Document's DOM, and subtle behavior of #prepare-a-script. I don't expect these occur or are needed for usual cases, and perhaps it's fine for polyfills to use the document order.

This complexity might be a side effect of specifying import maps as a part of <script>.
<script> specification is complicated to handle many scheduling types and corner cases. I specified the import maps so that it is consistent with existing <script> things, but existing <script> specification isn't necessary optimal. (I don't have clear alternatives in my mind though)

@joeldenning
Copy link
Contributor Author

Do you mean that the priority of import maps doesn't depend on the timing when "acquiring import maps" boolean is cleared? Or any other specific aspects? (Still "acquiring import maps" boolean forbids new import maps once cleared)

Yep that's what I mean. Option 3 has to delay processing until the "acquiring import maps" boolean changes from true to false.

I don't expect these occur or are needed for usual cases, and perhaps it's fine for polyfills to use the document order.

Agreed. And yeah I think a polyfill being slightly noncompliant with this part of the spec will be okay. My implementation within SystemJS has already landed, using document order to prioritize them. I doubt that many (or any) of systemjs users will notice or complain about the deviation.

I specified the import maps so that it is consistent with existing <script> things, but existing <script> specification isn't necessary optimal. (I don't have clear alternatives in my mind though)

Yeah I agree. Sticking to a complex, but well established, pattern for script loading sounds like a better thing to do. I'm fully onboard with Option 2 now.

@ljharb
Copy link

ljharb commented Jun 26, 2019

If it’s at all non compliant, it’s not a polyfill.

Polyfillability doesn’t have to block a feature, but it should simply not be polyfilled (shimmed) if it can’t be done accurately (shammed).

@joeldenning
Copy link
Contributor Author

Haha well feel free to PR systemjs or create another polyfill 😄

Like @hiroshige-g said, polyfilling this would be pretty hard:

#prepare-a-script order is hard (even with mutation observers), including modifying the <script>'s attribute after insertion, moving <script> from another Document's DOM, and subtle behavior of #prepare-a-script

SystemJS is optimized to be the smallest js library it can be in terms of bytes, so mutation observers with nuanced behavior might be too much bloat for the project. @guybedford is the primary maintainer of systemjs so he'd be the one to decide.

@domenic
Copy link
Collaborator

domenic commented Jun 27, 2019

Agreed; I really appreciate the efforts of the SystemJS folks here, and I don't think gatekeeping on the definition of polyfill is a conducive way to move the ecosystem forward.

@domenic domenic added this to the MVP milestone Jul 3, 2019
domenic pushed a commit that referenced this issue Jul 16, 2019
Previously, import maps were processed when they are fetched, and therefore the priority of import maps and order of error events was nondeterministic.

This commit introduces a mechanism to process import maps in the order of "prepare a script", and thus fixes #114. To do so, it expands the general integration of import maps with "prepare a script" to make them more symmetrical to module and classic scripts in that regard.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants