Improve the importmap declaration to support eargerly loaded modules

Hello all,

Since a few versions, we have a support for importmap in XWiki (see the doc).

Currently, the only thing developers can do is to define mapping betwteen module ids, and webjar references.
But, for anything to be actually loaded in the browser, developers need to manually import the module from another piece of javascript (i.e., use import "mymoduleid" in some script).
While for some modules, we definitely want to wait for code to import it before load (e.g., not loading Vue when no code in the page needs it).
For other use cases, it is interesting to force the module to be loaded eagerly.

For instance:

  1. a module registering client-side components (proposal for this topic coming soon).
  2. a module register polyfills to support older browsers

As a reminder, the current format to declared importmaps from webjars is:

<xwiki.extension.javascript.modules.importmap>{
  "vue": "org.webjars.npm:vue/dist/vue.runtime.esm-browser.prod.js"
}</xwiki.extension.javascript.modules.importmap>

And this translates to the following script element added to the head of the pages:

<script type="importmap">{
  "imports":{
    "vue":"\/xwiki\/webjars\/wiki%3Axwiki\/vue\/3.5.23\/dist\/vue.runtime.esm-browser.prod.js?r=1"
  }
}</script>

If vue was marked as preloaded, it would be

<script type="importmap">{
  "imports":{
    "vue":"\/xwiki\/webjars\/wiki%3Axwiki\/vue\/3.5.23\/dist\/vue.runtime.esm-browser.prod.js?r=1"
  }
}</script>
<!-- The module is loaded eagerly. -->
<script type="module" src="/xwiki/webjars/wiki%3Axwiki/vue/3.5.23/dist/vue.runtime.esm-browser.prod.js?r=1"></script>

Option 1: special prefix

An easy option is to simply introduce a special character. I have in mind !.
When an entry value in the importmap starts by !, it is loaded eagerly. I’m proposing to set it as the value because is not a valid groupId character, therefore not a breaking change.

Example:

<xwiki.extension.javascript.modules.importmap>{
  "vue": "!org.webjars.npm:vue/dist/vue.runtime.esm-browser.prod.js"
}</xwiki.extension.javascript.modules.importmap>

Pros:

  • Very easy to use as a developer.

Cons:

  • This could become a limitation if we want to introduce more concepts to the importmap later.

Option 2: introduce more structure for special cases

Keep support for a simple string for basic cases, but also allow for an object for advanced cases

Example:

<xwiki.extension.javascript.modules.importmap>{
  "vue": {
    "webjarPath": "org.webjars.npm:vue/dist/vue.runtime.esm-browser.prod.js",
    "eager": true
  }
}</xwiki.extension.javascript.modules.importmap>

Cons:

  • More verbose.

Pros:

  • Easier to extends with more concepts over time (e.g., concepts related to resource load priority could become important to fine tune page rendering time).

Conclusion

WDYT? Thanks

Option 1 does not really prevent doing Option 2 later if we have the need (at which point there would simply be two ways to indicate it).

I’m fine both on my side, but I’m clearly not the one who will use that the most.

Yes, technically option 1 can be seen as syntactic sugar for option 2.

Are these always mutually exclusive? I’m thinking of modules that have an export but also some side effects on load. But maybe that’s not a good practice though.

I’d start with option 1 and add 2 later.

Thanks,
Marius

To be clear what is not supported currently is to declare an “anonymous” module and to only declare it be loaded eagerly without giving it a name.
This is indeed the case for modules providing some initialization logic without exporting anything.

The same options apply, with some variations. Let me rephrase them below.

Common example

I will use the same example for all the options.
Each option will present how the use case is declared, but the output html presented just below will be the same for all options

moduleDeclaration is only declared to be imported later, moduleDeclarationEager is declared to be imported later as well, but also loaded eagerly , and anonymousModuleEager is only loaded eagerly without giving it a name in the importmap (i.e., the new use case).

Output html:

<!-- the anonymous module is not declared. -->
<script type="importmap">{
  "imports":{
    "moduleDeclaration": ".../artifactId1/file.js",
    "moduleDeclarationEager": ".../artifactId2/file.js",
  }
}</script>
<!-- The module is loaded eagerly. -->
<script type="module" src=".../artifactId2/file.js"></script>
<script type="module" src=".../artifactId3/file.js"></script>

Option 1: special prefixes

  • Introduction of ! to load modules eagerly
  • Introduce _ as a module name prefix to make them anonymous.

The declaration is:

<xwiki.extension.javascript.modules.importmap>{
  "moduleDeclaration": "groupId:artifactId1/file.js",
  "moduleDeclarationEager": "!groupId:artifactId2/file.js"
  "_anonymousModuleEager": "!groupId:artifactId3/file.js"
}</xwiki.extension.javascript.modules.importmap>

Option 2: introduce more structure for special cases

<xwiki.extension.javascript.modules.importmap>{
  "moduleDeclaration": "groupId:artifactId1/file.js",
  "moduleDeclarationEager": {
    "webjarPath": "groupId:artifactId2/file.js",
    "eager": true
  },
  "anonymousModuleEager": {
    "webjarPath": "!groupId:artifactId3/file.js",
    "eager": true,
    "anonymous": true
   }
}</xwiki.extension.javascript.modules.importmap>

Note: currently there is a conflict resolution for named modules when importmaps are merged. This wouldn’t be the case for anonymous modules that would be stored in a separate set.

PS: here again, the two options are not exclusion, and we could decide to have both with option 1 as a syntactic sugar for option 2, which can easily be done by a simple json object transformation. Consequently, avoiding code duplication.