This is an automated email from the ASF dual-hosted git repository.

thiagohp pushed a commit to branch feature/es-module-support
in repository https://gitbox.apache.org/repos/asf/tapestry-5.git

commit 7b55625592f161301e476b69237542c7dddf7017
Author: Thiago H. de Paula Figueiredo <thi...@arsmachina.com.br>
AuthorDate: Tue Apr 1 15:31:17 2025 -0300

    TAP5-2803: foundation work for ES module support
---
 5_10_RELEASE_NOTES.md                              |  23 ++
 .../java/org/apache/tapestry5/SymbolConstants.java |  14 +-
 .../internal/services/DocumentLinker.java          |  16 ++
 .../internal/services/DocumentLinkerImpl.java      |  41 +++-
 .../internal/services/EsModuleInitsManager.java    |  60 ++++++
 .../services/PartialMarkupDocumentLinker.java      |  17 +-
 .../services/ajax/JavaScriptSupportImpl.java       | 125 +++++++++--
 .../services/javascript/EsModuleManagerImpl.java   | 238 +++++++++++++++++++++
 .../apache/tapestry5/modules/JavaScriptModule.java |   4 +
 .../apache/tapestry5/modules/TapestryModule.java   |  16 +-
 ...ialization.java => AbstractInitialization.java} |  20 +-
 .../javascript/EsModuleConfigurationCallback.java  |  62 ++++++
 .../javascript/EsModuleInitialization.java         | 113 ++++++++++
 .../services/javascript/EsModuleManager.java       |  57 +++++
 .../services/javascript/ImportPlacement.java       |  37 ++++
 .../services/javascript/Initialization.java        |   2 +-
 .../services/javascript/JavaScriptSupport.java     |  19 ++
 .../javascript/ModuleConfigurationCallback.java    |   2 +-
 .../tapestry5/integration/app1/EsModuleTests.java  | 153 +++++++++++++
 .../integration/app1/pages/EsModuleDemo.java       |  86 ++++++++
 .../tapestry5/integration/app1/pages/Index.java    |   4 +-
 .../integration/app1/services/AppModule.java       |  40 ++++
 .../META-INF/assets/es-modules/foo/bar.js          |   2 +
 .../assets/es-modules/placement/body-bottom.js     |   2 +
 .../assets/es-modules/placement/body-top.js        |   2 +
 .../META-INF/assets/es-modules/placement/head.js   |   2 +
 .../META-INF/assets/es-modules/root-folder.js      |   2 +
 .../META-INF/assets/es-modules/show-import-map.js  |   2 +
 .../integration/app1/es-module-outside-metainf.js  |   2 +
 .../integration/app1/pages/EsModuleDemo.tml        |  30 +++
 30 files changed, 1145 insertions(+), 48 deletions(-)

diff --git a/5_10_RELEASE_NOTES.md b/5_10_RELEASE_NOTES.md
new file mode 100644
index 000000000..b99e05690
--- /dev/null
+++ b/5_10_RELEASE_NOTES.md
@@ -0,0 +1,23 @@
+Scratch pad for changes destined for the 5.10.0 release notes page.
+
+# Added configuration symbols
+
+* `tapestry.es-module-path-prefix` (`SymbolConstants.ES_MODULE_PATH_PREFIX`)
+
+
+# Added methods
+
+* `JavaScriptSupport.importEsModule(String moduleName)`
+* 
`JavaScriptSupport.addEsModuleConfigurationCallback(EsModuleConfigurationCallback
 callback)`
+
+# Added types
+
+* `org.apache.tapestry5.services.javascript.EsModuleInitialization`
+* `org.apache.tapestry5.services.javascript.ImportPlacement`
+* `org.apache.tapestry5.services.javascript.EsModuleConfigurationCallback`
+* `org.apache.tapestry5.services.javascript.EsModuleManager`
+
+# Non-backward-compatible changes (but that probably won't cause problems)
+
+
+# Overall notes
diff --git 
a/tapestry-core/src/main/java/org/apache/tapestry5/SymbolConstants.java 
b/tapestry-core/src/main/java/org/apache/tapestry5/SymbolConstants.java
index dc19b2756..51a304c2c 100644
--- a/tapestry-core/src/main/java/org/apache/tapestry5/SymbolConstants.java
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/SymbolConstants.java
@@ -376,7 +376,7 @@ public class SymbolConstants
 
 
     /**
-     * Prefix used for all module resources. This may contain slashes, but 
should not being or end with one.
+     * Prefix used for all Require.js module resources. This may contain 
slashes, but should not being or end with one.
      * Tapestry will create two {@link 
org.apache.tapestry5.http.services.Dispatcher}s from this: one for normal
      * modules, the other for GZip compressed modules (by appending ".gz" to 
this value).
      *
@@ -385,6 +385,18 @@ public class SymbolConstants
      * @since 5.4
      */
     public static final String MODULE_PATH_PREFIX = 
"tapestry.module-path-prefix";
+    
+    /**
+     * Prefix used for automatically configured ES module resources. 
+     * This may contain slashes, but should not being or end with one.
+     *
+     * The default is "es-modules".
+     * 
+     * TODO remove
+     *
+     * @since 5.4
+     */
+    public static final String ES_MODULE_PATH_PREFIX = 
"tapestry.es-module-path-prefix";
 
     /**
      * Identifies the context path of the application, as determined from 
{@link javax.servlet.ServletContext#getContextPath()}.
diff --git 
a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/DocumentLinker.java
 
b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/DocumentLinker.java
index e63b014ab..9326f12f0 100644
--- 
a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/DocumentLinker.java
+++ 
b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/DocumentLinker.java
@@ -13,6 +13,8 @@
 package org.apache.tapestry5.internal.services;
 
 import org.apache.tapestry5.json.JSONArray;
+import org.apache.tapestry5.services.javascript.EsModuleConfigurationCallback;
+import org.apache.tapestry5.services.javascript.EsModuleInitialization;
 import org.apache.tapestry5.services.javascript.InitializationPriority;
 import org.apache.tapestry5.services.javascript.ModuleConfigurationCallback;
 import org.apache.tapestry5.services.javascript.StylesheetLink;
@@ -55,6 +57,14 @@ public interface DocumentLinker
      * @since 5.4
      */
     void addModuleConfigurationCallback(ModuleConfigurationCallback callback);
+    
+    /**
+     * Adds an ES module configuration callback for this request.
+     * 
+     * @param callback a {@link EsModuleConfigurationCallback}. It cannot be 
null.
+     * @since 5.10.0
+     */
+    void addEsModuleConfigurationCallback(EsModuleConfigurationCallback 
callback);
 
     /**
      * Adds JavaScript code. The code is collected into a single block that is 
injected just before the close body tag
@@ -88,4 +98,10 @@ public interface DocumentLinker
                            String moduleName,
                            String functionName,
                            JSONArray arguments);
+    
+    /**
+     * Adds ES module initialization.
+     * @since 5.10.0
+     */
+    void addEsModuleInitialization(EsModuleInitialization initialization);
 }
diff --git 
a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/DocumentLinkerImpl.java
 
b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/DocumentLinkerImpl.java
index fc0449818..8814e7faf 100644
--- 
a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/DocumentLinkerImpl.java
+++ 
b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/DocumentLinkerImpl.java
@@ -16,6 +16,9 @@ import org.apache.tapestry5.commons.util.CollectionFactory;
 import org.apache.tapestry5.dom.Document;
 import org.apache.tapestry5.dom.Element;
 import org.apache.tapestry5.json.JSONArray;
+import org.apache.tapestry5.services.javascript.EsModuleConfigurationCallback;
+import org.apache.tapestry5.services.javascript.EsModuleInitialization;
+import org.apache.tapestry5.services.javascript.EsModuleManager;
 import org.apache.tapestry5.services.javascript.InitializationPriority;
 import org.apache.tapestry5.services.javascript.ModuleConfigurationCallback;
 import org.apache.tapestry5.services.javascript.ModuleManager;
@@ -34,12 +37,18 @@ public class DocumentLinkerImpl implements DocumentLinker
     private final List<String> libraryURLs = CollectionFactory.newList();
 
     private final ModuleInitsManager initsManager = new ModuleInitsManager();
+    
+    private final EsModuleInitsManager esModulesinitsManager = new 
EsModuleInitsManager();
 
     private final List<ModuleConfigurationCallback> 
moduleConfigurationCallbacks = CollectionFactory.newList();
-
+    
+    private final List<EsModuleConfigurationCallback> 
esModuleConfigurationCallbacks = CollectionFactory.newList();
+    
     private final List<StylesheetLink> includedStylesheets = 
CollectionFactory.newList();
 
     private final ModuleManager moduleManager;
+    
+    private final EsModuleManager esModuleManager;
 
     private final boolean omitGeneratorMetaTag, enablePageloadingMask;
 
@@ -56,9 +65,11 @@ public class DocumentLinkerImpl implements DocumentLinker
      * @param enablePageloadingMask
      * @param tapestryVersion
      */
-    public DocumentLinkerImpl(ModuleManager moduleManager, boolean 
omitGeneratorMetaTag, boolean enablePageloadingMask, String tapestryVersion)
+    public DocumentLinkerImpl(ModuleManager moduleManager, EsModuleManager 
esModuleManager,
+            boolean omitGeneratorMetaTag, boolean enablePageloadingMask, 
String tapestryVersion)
     {
         this.moduleManager = moduleManager;
+        this.esModuleManager = esModuleManager;
         this.omitGeneratorMetaTag = omitGeneratorMetaTag;
         this.enablePageloadingMask = enablePageloadingMask;
 
@@ -85,6 +96,7 @@ public class DocumentLinkerImpl implements DocumentLinker
         hasScriptsOrInitializations = true;
     }
 
+    @SuppressWarnings("deprecation")
     public void addScript(InitializationPriority priority, String script)
     {
         addInitialization(priority, "t5/core/pageinit", "evalJavaScript", new 
JSONArray().put(script));
@@ -114,6 +126,7 @@ public class DocumentLinkerImpl implements DocumentLinker
             return;
         }
 
+
         // TAP5-2200: Generating XML from pages and templates is not possible 
anymore
         // only add JavaScript and CSS if we're actually generating 
         final String mimeType = document.getMimeType();
@@ -121,7 +134,7 @@ public class DocumentLinkerImpl implements DocumentLinker
         {
             return;
         }
-
+        
         addStylesheetsToHead(root, includedStylesheets);
 
         // only add the generator meta only to html documents
@@ -138,6 +151,14 @@ public class DocumentLinkerImpl implements DocumentLinker
         }
 
         addScriptElements(root);
+        
+        final List<EsModuleInitialization> esModuleInits = 
esModulesinitsManager.getInits();
+        if (isHtmlRoot && !esModuleInits.isEmpty())
+        {
+            esModuleManager.writeImportMap(root.find("head"), 
esModuleConfigurationCallbacks);
+            esModuleManager.writeImports(root, esModuleInits);
+        }
+        
     }
 
     private static Element addElementBefore(Element container, Element 
insertionPoint, String name, String... namesAndValues)
@@ -305,5 +326,19 @@ public class DocumentLinkerImpl implements DocumentLinker
         assert callback != null;
         moduleConfigurationCallbacks.add(callback);
     }
+    
+    public void addEsModuleConfigurationCallback(EsModuleConfigurationCallback 
callback)
+    {
+        assert callback != null;
+        esModuleConfigurationCallbacks.add(callback);
+    }
 
+    @Override
+    public void addEsModuleInitialization(EsModuleInitialization 
initialization) 
+    {
+        assert initialization != null;
+        esModulesinitsManager.add(initialization);
+        hasScriptsOrInitializations = true;
+    }
+    
 }
diff --git 
a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/EsModuleInitsManager.java
 
b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/EsModuleInitsManager.java
new file mode 100644
index 000000000..4116679c9
--- /dev/null
+++ 
b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/EsModuleInitsManager.java
@@ -0,0 +1,60 @@
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.apache.tapestry5.internal.services;
+
+import java.util.List;
+import java.util.Set;
+
+import org.apache.tapestry5.commons.util.CollectionFactory;
+import org.apache.tapestry5.services.javascript.EsModuleInitialization;
+
+public class EsModuleInitsManager
+{
+    private final Set<String> modules = CollectionFactory.newSet();
+    
+    private final List<EsModuleInitialization> initializations = 
CollectionFactory.newList();
+
+    public void add(EsModuleInitialization initialization)
+    {
+        assert initialization != null;
+
+        // We ignore a module being added again.
+        final String moduleName = initialization.getModuleId();
+        if (!modules.contains(moduleName))
+        {
+            initializations.add(initialization);
+            modules.add(moduleName);
+        }
+    }
+
+    /**
+     * Returns all previously added inits.
+     */
+    public List<EsModuleInitialization> getInits()
+    {
+        return initializations;
+    }
+}
diff --git 
a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/PartialMarkupDocumentLinker.java
 
b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/PartialMarkupDocumentLinker.java
index 9cde60c17..9ccf81629 100644
--- 
a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/PartialMarkupDocumentLinker.java
+++ 
b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/PartialMarkupDocumentLinker.java
@@ -17,6 +17,9 @@ package org.apache.tapestry5.internal.services;
 import org.apache.tapestry5.internal.InternalConstants;
 import org.apache.tapestry5.json.JSONArray;
 import org.apache.tapestry5.json.JSONObject;
+import org.apache.tapestry5.services.javascript.EsModuleConfigurationCallback;
+import org.apache.tapestry5.services.javascript.EsModuleInitialization;
+import org.apache.tapestry5.services.javascript.EsModuleManager;
 import org.apache.tapestry5.services.javascript.InitializationPriority;
 import org.apache.tapestry5.services.javascript.ModuleConfigurationCallback;
 import org.apache.tapestry5.services.javascript.StylesheetLink;
@@ -30,7 +33,7 @@ public class PartialMarkupDocumentLinker implements 
DocumentLinker
     private final JSONArray stylesheets = new JSONArray();
 
     private final ModuleInitsManager initsManager = new ModuleInitsManager();
-
+    
     public void addCoreLibrary(String libraryURL)
     {
         notImplemented("addCoreLibrary");
@@ -72,6 +75,18 @@ public class PartialMarkupDocumentLinker implements 
DocumentLinker
         initsManager.addInitialization(priority, moduleName, functionName, 
arguments);
     }
 
+    @Override
+    public void addEsModuleConfigurationCallback(EsModuleConfigurationCallback 
callback) 
+    {
+        notImplemented("moduleConfigurationCallback");
+    }
+
+    @Override
+    public void addEsModuleInitialization(EsModuleInitialization 
initialization) 
+    {
+        notImplemented("addEsModuleInitialization");
+    }
+
     /**
      * Commits changes, adding one or more keys to the reply.
      *
diff --git 
a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ajax/JavaScriptSupportImpl.java
 
b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ajax/JavaScriptSupportImpl.java
index 3a98ac74d..64fbadf78 100644
--- 
a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ajax/JavaScriptSupportImpl.java
+++ 
b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ajax/JavaScriptSupportImpl.java
@@ -12,6 +12,13 @@
 
 package org.apache.tapestry5.internal.services.ajax;
 
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
 import org.apache.tapestry5.Asset;
 import org.apache.tapestry5.BooleanHook;
 import org.apache.tapestry5.ComponentResources;
@@ -26,9 +33,17 @@ import org.apache.tapestry5.ioc.internal.util.InternalUtils;
 import org.apache.tapestry5.ioc.util.IdAllocator;
 import org.apache.tapestry5.json.JSONArray;
 import org.apache.tapestry5.json.JSONObject;
-import org.apache.tapestry5.services.javascript.*;
-
-import java.util.*;
+import org.apache.tapestry5.services.javascript.AbstractInitialization;
+import org.apache.tapestry5.services.javascript.EsModuleConfigurationCallback;
+import org.apache.tapestry5.services.javascript.EsModuleInitialization;
+import org.apache.tapestry5.services.javascript.ImportPlacement;
+import org.apache.tapestry5.services.javascript.Initialization;
+import org.apache.tapestry5.services.javascript.InitializationPriority;
+import org.apache.tapestry5.services.javascript.JavaScriptStack;
+import org.apache.tapestry5.services.javascript.JavaScriptStackSource;
+import org.apache.tapestry5.services.javascript.JavaScriptSupport;
+import org.apache.tapestry5.services.javascript.ModuleConfigurationCallback;
+import org.apache.tapestry5.services.javascript.StylesheetLink;
 
 public class JavaScriptSupportImpl implements JavaScriptSupport
 {
@@ -47,6 +62,10 @@ public class JavaScriptSupportImpl implements 
JavaScriptSupport
     private final List<StylesheetLink> stylesheetLinks = 
CollectionFactory.newList();
 
     private final List<InitializationImpl> inits = CollectionFactory.newList();
+    
+    private final List<EsModuleInitialization> esModuleInits = 
CollectionFactory.newList();
+    
+    private final Set<String> esModulesImported = CollectionFactory.newSet();
 
     private final JavaScriptStackSource javascriptStackSource;
 
@@ -62,30 +81,46 @@ public class JavaScriptSupportImpl implements 
JavaScriptSupport
 
     private Map<String, String> libraryURLToStackName, moduleNameToStackName;
 
-    class InitializationImpl implements Initialization
+    abstract class BaseInitialization<T extends AbstractInitialization<?>> 
implements AbstractInitialization<T>
     {
-        InitializationPriority priority = InitializationPriority.NORMAL;
-
         final String moduleName;
 
         String functionName;
 
         JSONArray arguments;
 
-        InitializationImpl(String moduleName)
+        BaseInitialization(String moduleName)
         {
             this.moduleName = moduleName;
         }
 
-        public Initialization invoke(String functionName)
+        public T invoke(String functionName)
         {
             assert InternalUtils.isNonBlank(functionName);
 
             this.functionName = functionName;
 
-            return this;
+            return (T) this;
+        }
+
+        public void with(Object... arguments)
+        {
+            assert arguments != null;
+
+            this.arguments = new JSONArray(arguments);
         }
+    }
+    
+    class InitializationImpl extends BaseInitialization<Initialization> 
implements Initialization
+    {
+        
+        InitializationPriority priority = InitializationPriority.NORMAL;
 
+        public InitializationImpl(String moduleName) 
+        {
+            super(moduleName);
+        }
+        
         public Initialization priority(InitializationPriority priority)
         {
             assert priority != null;
@@ -94,15 +129,54 @@ public class JavaScriptSupportImpl implements 
JavaScriptSupport
 
             return this;
         }
+        
+    }
+    
+    class EsModuleInitializationImpl extends 
BaseInitialization<EsModuleInitialization> implements EsModuleInitialization
+    {
+        
+        Map<String, String> attributes;
+        ImportPlacement placement = ImportPlacement.BODY_BOTTOM;
+        
+        EsModuleInitializationImpl(String moduleName) 
+        {
+            super(moduleName);
+        }
 
-        public void with(Object... arguments)
+        public EsModuleInitialization withAttribute(String id, String value) 
         {
-            assert arguments != null;
+            if (attributes == null)
+            {
+                attributes = CollectionFactory.newMap();
+            }
+            attributes.put(id, value);
+            return this;
+        }
 
-            this.arguments = new JSONArray(arguments);
+        public EsModuleInitialization placement(ImportPlacement placement) 
+        {
+            this.placement = placement;
+            return null;
+        }
+
+        public String getModuleId() {
+            return moduleName;
+        }
+
+        public Map<String, String> getAttributes() {
+            return attributes != null ? 
+                    Collections.unmodifiableMap(attributes) : 
+                        Collections.emptyMap();
         }
+
+        @Override
+        public ImportPlacement getPlacement() {
+            return placement;
+        }
+
     }
 
+
     public JavaScriptSupportImpl(DocumentLinker linker, JavaScriptStackSource 
javascriptStackSource,
                                  JavaScriptStackPathConstructor 
stackPathConstructor, BooleanHook suppressCoreStylesheetsHook)
     {
@@ -150,6 +224,8 @@ public class JavaScriptSupportImpl implements 
JavaScriptSupport
 
     public void commit()
     {
+        
+        // TODO make no Require.js version of this
         if (focusFieldId != null)
         {
             require("t5/core/pageinit").invoke("focus").with(focusFieldId);
@@ -176,6 +252,8 @@ public class JavaScriptSupportImpl implements 
JavaScriptSupport
                 linker.addInitialization(element.priority, element.moduleName, 
element.functionName, element.arguments);
             }
         });
+        
+        esModuleInits.stream().forEach(linker::addEsModuleInitialization);
     }
 
     public void addInitializerCall(InitializationPriority priority, String 
functionName, JSONObject parameter)
@@ -462,4 +540,27 @@ public class JavaScriptSupportImpl implements 
JavaScriptSupport
         return init;
     }
 
+    @Override
+    public EsModuleInitialization importEsModule(String moduleName) 
+    {
+        
+        assert InternalUtils.isNonBlank(moduleName);
+        
+        // TODO import core libraries (jQuery, 
Prototype/Scriptaculous/Underscore)
+
+        EsModuleInitialization init = new 
EsModuleInitializationImpl(moduleName);
+        if (!esModulesImported.contains(moduleName))
+        {
+            esModuleInits.add(init);
+        }
+        
+        return init;
+    }
+
+    @Override
+    public void addEsModuleConfigurationCallback(EsModuleConfigurationCallback 
callback) 
+    {
+        linker.addEsModuleConfigurationCallback(callback);
+    }
+
 }
diff --git 
a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/javascript/EsModuleManagerImpl.java
 
b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/javascript/EsModuleManagerImpl.java
new file mode 100644
index 000000000..dff737ee6
--- /dev/null
+++ 
b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/javascript/EsModuleManagerImpl.java
@@ -0,0 +1,238 @@
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.apache.tapestry5.internal.services.javascript;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.tapestry5.SymbolConstants;
+import org.apache.tapestry5.commons.util.AvailableValues;
+import org.apache.tapestry5.commons.util.CollectionFactory;
+import org.apache.tapestry5.commons.util.UnknownValueException;
+import org.apache.tapestry5.dom.Element;
+import org.apache.tapestry5.http.TapestryHttpSymbolConstants;
+import org.apache.tapestry5.internal.InternalConstants;
+import org.apache.tapestry5.internal.services.assets.ResourceChangeTracker;
+import org.apache.tapestry5.ioc.annotations.PostInjection;
+import org.apache.tapestry5.ioc.annotations.Symbol;
+import org.apache.tapestry5.ioc.services.ClasspathMatcher;
+import org.apache.tapestry5.ioc.services.ClasspathScanner;
+import org.apache.tapestry5.json.JSONObject;
+import org.apache.tapestry5.services.AssetSource;
+import org.apache.tapestry5.services.assets.StreamableResourceSource;
+import org.apache.tapestry5.services.javascript.EsModuleConfigurationCallback;
+import org.apache.tapestry5.services.javascript.EsModuleInitialization;
+import org.apache.tapestry5.services.javascript.EsModuleManager;
+import org.apache.tapestry5.services.javascript.ImportPlacement;
+
+public class EsModuleManagerImpl implements EsModuleManager
+{
+
+    /**
+     * Name of the JSON object property containing the imports in an import 
map.
+     */
+    public static final String IMPORTS_ATTRIBUTE = 
EsModuleConfigurationCallback.IMPORTS_ATTRIBUTE;
+
+    private static final String CLASSPATH_ROOT = "META-INF/assets/es-modules/";
+
+    private final boolean compactJSON;
+
+    private final boolean productionMode;
+
+    private final Set<String> extensions;
+    
+    private final AssetSource assetSource;
+
+    // Note: ConcurrentHashMap does not support null as a value, alas. We use 
classpathRoot as a null.
+    private final Map<String, String> cache = 
CollectionFactory.newConcurrentMap();
+    
+    private final ClasspathScanner classpathScanner;
+    
+    private JSONObject importMap;
+    
+    private final List<EsModuleConfigurationCallback> globalCallbacks;
+
+    public EsModuleManagerImpl(
+                             List<EsModuleConfigurationCallback> 
globalCallbacks,
+                             AssetSource assetSource,
+                             StreamableResourceSource streamableResourceSource,
+                             @Symbol(SymbolConstants.COMPACT_JSON)
+                             boolean compactJSON,
+                             
@Symbol(TapestryHttpSymbolConstants.PRODUCTION_MODE)
+                             boolean productionMode,
+                             ClasspathScanner classpathScanner)
+    {
+        this.compactJSON = compactJSON;
+        this.assetSource = assetSource;
+        this.classpathScanner = classpathScanner;
+        this.globalCallbacks = globalCallbacks;
+        this.productionMode = productionMode;
+        importMap = new JSONObject();
+
+        extensions = CollectionFactory.newSet("js");
+        
extensions.addAll(streamableResourceSource.fileExtensionsForContentType(InternalConstants.JAVASCRIPT_CONTENT_TYPE));
+        
+        createImportMap();
+
+    }
+    
+    private void createImportMap()
+    {
+        
+        JSONObject importMap = new JSONObject();
+        JSONObject imports = importMap.in(IMPORTS_ATTRIBUTE);
+        
+        cache.clear();
+
+        loadBaseModuleList(imports);
+        
+        for (String name : cache.keySet())
+        {
+            imports.put(name, cache.get(name));
+        }
+        
+        this.importMap = executeCallbacks(importMap, globalCallbacks);
+        
+        for (String id : imports.keySet()) 
+        {
+            cache.put(id, imports.getString(id));
+        }
+            
+    }
+
+    private void loadBaseModuleList(JSONObject imports) 
+    {
+        ClasspathMatcher matcher = (packagePath, fileName) -> 
+            extensions.stream().anyMatch(e -> fileName.endsWith(e));
+        try 
+        {
+            final Set<String> scan = classpathScanner.scan(CLASSPATH_ROOT, 
matcher);
+            for (String file : scan) 
+            {
+                String id = file.replace(CLASSPATH_ROOT, "");
+                id = id.substring(0, id.lastIndexOf('.'));
+                            
+                imports.put(id, 
assetSource.getClasspathAsset(file).toClientURL());
+            }
+        } catch (IOException e) 
+        {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @PostInjection
+    public void setupInvalidation(ResourceChangeTracker tracker)
+    {
+        
+//        Live class reloading of ES modules (failing at the moment)
+        
+        // TODO make invalidations smarter (and work)
+        tracker.addInvalidationCallback(this::createImportMap);
+    }
+
+    @Override
+    public void writeImportMap(Element head, 
List<EsModuleConfigurationCallback> moduleConfigurationCallbacks) {
+        
+        // Cloning the original import map JSON object
+        final JSONObject imports = ((JSONObject) 
importMap.get(EsModuleConfigurationCallback.IMPORTS_ATTRIBUTE))
+                .copy();
+        JSONObject newImportMap = new JSONObject(
+                EsModuleConfigurationCallback.IMPORTS_ATTRIBUTE, imports);
+        
+        newImportMap = executeCallbacks(newImportMap, 
moduleConfigurationCallbacks);
+        
+        head.element("script")
+                .attribute("type", "importmap")
+                .text(newImportMap.toString(compactJSON));
+    }
+        
+    @Override
+    public void writeImports(Element root, List<EsModuleInitialization> inits) 
+    {
+        Element script;
+        Element body = null;
+        Element head = null;
+        ImportPlacement placement;
+        for (EsModuleInitialization init : inits) 
+        {
+            
+            final String moduleId = init.getModuleId();
+            // Making sure the user doesn't shoot heir own foot
+            final String url = cache.get(moduleId);
+            if (url == null)
+            {
+                throw new UnknownValueException("ES module not found: " + 
moduleId, 
+                        new AvailableValues("String", cache));
+            }
+            
+            placement = init.getPlacement();
+            if (placement.equals(ImportPlacement.HEAD))
+            {
+                if (head == null) 
+                {
+                    head = root.find("head");
+                }
+                script = head.element("script");
+            }
+            else {
+                if (body == null)
+                {
+                    body = root.find("body");
+                }
+                if (placement.equals(ImportPlacement.BODY_BOTTOM)) {
+                    script = body.element("script");
+                }
+                else if (placement.equals(ImportPlacement.BODY_TOP))
+                {
+                    script = body.elementAt(0, "script");
+                }
+                else
+                {
+                    throw new IllegalArgumentException("Unknown import 
placement: " + placement);
+                }
+            }
+            
+            final Map<String, String> attributes = init.getAttributes();
+            for (String name : attributes.keySet())
+            {
+                script.attribute(name, attributes.get(name));
+            }
+            
+            script.attribute("src", url);
+            script.attribute("type", "module");
+            
+            if (!productionMode)
+            {
+                script.attribute("data-module-id", moduleId);
+                final Element log = script.element("script", "type", 
"text/javascript");
+                log.text(String.format("console.debug('Imported ES module 
%s');", moduleId));
+                log.moveBefore(script);
+            }
+            
+        }
+        
+    }
+
+    private JSONObject executeCallbacks(JSONObject importMap, 
List<EsModuleConfigurationCallback> callbacks) 
+    {
+        for (EsModuleConfigurationCallback callback : callbacks) 
+        {
+            callback.configure(importMap);
+        }
+        
+        return importMap;
+    }
+
+}
diff --git 
a/tapestry-core/src/main/java/org/apache/tapestry5/modules/JavaScriptModule.java
 
b/tapestry-core/src/main/java/org/apache/tapestry5/modules/JavaScriptModule.java
index 955d964fe..b452932f5 100644
--- 
a/tapestry-core/src/main/java/org/apache/tapestry5/modules/JavaScriptModule.java
+++ 
b/tapestry-core/src/main/java/org/apache/tapestry5/modules/JavaScriptModule.java
@@ -32,6 +32,7 @@ import 
org.apache.tapestry5.internal.services.ajax.JavaScriptSupportImpl;
 import org.apache.tapestry5.internal.services.assets.ResourceChangeTracker;
 import 
org.apache.tapestry5.internal.services.javascript.AddBrowserCompatibilityStyles;
 import 
org.apache.tapestry5.internal.services.javascript.ConfigureHTMLElementFilter;
+import org.apache.tapestry5.internal.services.javascript.EsModuleManagerImpl;
 import org.apache.tapestry5.internal.services.javascript.Internal;
 import 
org.apache.tapestry5.internal.services.javascript.JavaScriptStackPathConstructor;
 import 
org.apache.tapestry5.internal.services.javascript.JavaScriptStackSourceImpl;
@@ -60,6 +61,7 @@ import org.apache.tapestry5.services.PathConstructor;
 import org.apache.tapestry5.services.compatibility.Compatibility;
 import org.apache.tapestry5.services.compatibility.Trait;
 import org.apache.tapestry5.services.javascript.AMDWrapper;
+import org.apache.tapestry5.services.javascript.EsModuleManager;
 import org.apache.tapestry5.services.javascript.ExtensibleJavaScriptStack;
 import org.apache.tapestry5.services.javascript.JavaScriptModuleConfiguration;
 import org.apache.tapestry5.services.javascript.JavaScriptStack;
@@ -92,6 +94,7 @@ public class JavaScriptModule
     public static void bind(ServiceBinder binder)
     {
         binder.bind(ModuleManager.class, ModuleManagerImpl.class);
+        binder.bind(EsModuleManager.class, EsModuleManagerImpl.class);
         binder.bind(JavaScriptStackSource.class, 
JavaScriptStackSourceImpl.class);
         binder.bind(JavaScriptStack.class, 
ExtensibleJavaScriptStack.class).withMarker(Core.class).withId("CoreJavaScriptStack");
         binder.bind(JavaScriptStack.class, 
ExtensibleJavaScriptStack.class).withMarker(Internal.class).withId("InternalJavaScriptStack");
@@ -490,6 +493,7 @@ public class JavaScriptModule
     {
         configuration.add(SymbolConstants.JAVASCRIPT_INFRASTRUCTURE_PROVIDER, 
"prototype");
         configuration.add(SymbolConstants.MODULE_PATH_PREFIX, "modules");
+        configuration.add(SymbolConstants.ES_MODULE_PATH_PREFIX, "es-modules");
     }
 
     @Contribute(ModuleManager.class)
diff --git 
a/tapestry-core/src/main/java/org/apache/tapestry5/modules/TapestryModule.java 
b/tapestry-core/src/main/java/org/apache/tapestry5/modules/TapestryModule.java
index e3c47e86f..7f339ee65 100644
--- 
a/tapestry-core/src/main/java/org/apache/tapestry5/modules/TapestryModule.java
+++ 
b/tapestry-core/src/main/java/org/apache/tapestry5/modules/TapestryModule.java
@@ -20,19 +20,15 @@ import java.math.BigInteger;
 import java.net.URL;
 import java.text.DateFormat;
 import java.text.SimpleDateFormat;
-import java.util.Arrays;
 import java.util.Calendar;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.Date;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Properties;
 import java.util.Set;
 import java.util.regex.Pattern;
-import java.util.stream.Collectors;
 
 import org.apache.tapestry5.Asset;
 import org.apache.tapestry5.BindingConstants;
@@ -108,12 +104,7 @@ import org.apache.tapestry5.commons.services.TypeCoercer;
 import org.apache.tapestry5.commons.util.AvailableValues;
 import org.apache.tapestry5.commons.util.CollectionFactory;
 import org.apache.tapestry5.commons.util.StrategyRegistry;
-import org.apache.tapestry5.corelib.components.BeanEditor;
-import org.apache.tapestry5.corelib.components.PropertyDisplay;
-import org.apache.tapestry5.corelib.components.PropertyEditor;
 import org.apache.tapestry5.corelib.data.SecureOption;
-import org.apache.tapestry5.corelib.pages.PropertyDisplayBlocks;
-import org.apache.tapestry5.corelib.pages.PropertyEditBlocks;
 import org.apache.tapestry5.grid.GridConstants;
 import org.apache.tapestry5.grid.GridDataSource;
 import org.apache.tapestry5.http.Link;
@@ -174,7 +165,6 @@ import org.apache.tapestry5.internal.services.*;
 import org.apache.tapestry5.internal.services.ajax.AjaxFormUpdateFilter;
 import org.apache.tapestry5.internal.services.ajax.AjaxResponseRendererImpl;
 import 
org.apache.tapestry5.internal.services.ajax.MultiZoneUpdateEventResultProcessor;
-import org.apache.tapestry5.internal.services.assets.ResourceChangeTracker;
 import 
org.apache.tapestry5.internal.services.exceptions.ExceptionReportWriterImpl;
 import org.apache.tapestry5.internal.services.exceptions.ExceptionReporterImpl;
 import 
org.apache.tapestry5.internal.services.linktransform.LinkTransformerImpl;
@@ -365,6 +355,7 @@ import org.apache.tapestry5.services.ValueLabelProvider;
 import org.apache.tapestry5.services.ajax.AjaxResponseRenderer;
 import org.apache.tapestry5.services.dynamic.DynamicTemplate;
 import org.apache.tapestry5.services.dynamic.DynamicTemplateParser;
+import org.apache.tapestry5.services.javascript.EsModuleManager;
 import org.apache.tapestry5.services.javascript.JavaScriptSupport;
 import org.apache.tapestry5.services.javascript.ModuleManager;
 import 
org.apache.tapestry5.services.linktransform.ComponentEventLinkTransformer;
@@ -376,7 +367,6 @@ import org.apache.tapestry5.services.meta.FixedExtractor;
 import org.apache.tapestry5.services.meta.MetaDataExtractor;
 import org.apache.tapestry5.services.meta.MetaWorker;
 import org.apache.tapestry5.services.pageload.PageClassLoaderContextManager;
-import 
org.apache.tapestry5.services.pageload.PageClassLoaderContextManagerImpl;
 import org.apache.tapestry5.services.pageload.PreloaderMode;
 import org.apache.tapestry5.services.rest.MappedEntityManager;
 import org.apache.tapestry5.services.rest.OpenApiDescriptionGenerator;
@@ -1803,6 +1793,8 @@ public final class TapestryModule
     public void 
contributeMarkupRenderer(OrderedConfiguration<MarkupRendererFilter> 
configuration,
 
                                          final ModuleManager moduleManager,
+                                         
+                                         final EsModuleManager esModuleManager,
 
                                          
@Symbol(SymbolConstants.OMIT_GENERATOR_META)
                                          final boolean omitGeneratorMeta,
@@ -1825,7 +1817,7 @@ public final class TapestryModule
         {
             public void renderMarkup(MarkupWriter writer, MarkupRenderer 
renderer)
             {
-                DocumentLinkerImpl linker = new 
DocumentLinkerImpl(moduleManager, omitGeneratorMeta, enablePageloadingMask, 
tapestryVersion);
+                DocumentLinkerImpl linker = new 
DocumentLinkerImpl(moduleManager, esModuleManager, omitGeneratorMeta, 
enablePageloadingMask, tapestryVersion);
 
                 environment.push(DocumentLinker.class, linker);
 
diff --git 
a/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/Initialization.java
 
b/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/AbstractInitialization.java
similarity index 72%
copy from 
tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/Initialization.java
copy to 
tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/AbstractInitialization.java
index a633722fc..6aa58d866 100644
--- 
a/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/Initialization.java
+++ 
b/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/AbstractInitialization.java
@@ -13,12 +13,11 @@
 package org.apache.tapestry5.services.javascript;
 
 /**
- * Provided by {@link JavaScriptSupport#require(String)} to allow additional, 
optional, details of the module-based page initialization
- * to be configured.
+ * Superinterface with the parts shared by {@linkplain Initialization} and 
{@linkplain EsModuleInitialization}.
  *
- * @since 5.4
+ * @since 5.10.0
  */
-public interface Initialization
+public interface AbstractInitialization<T extends AbstractInitialization<?>>
 {
 
     /**
@@ -29,18 +28,7 @@ public interface Initialization
      *         name of a function exported by the module.
      * @return this Initialization, for further configuration
      */
-    Initialization invoke(String functionName);
-
-    /**
-     * Changes the initialization priority of the initialization from its 
default, {@link InitializationPriority#NORMAL}.
-     *
-     * Note: it is possible that this method may be removed before release 5.4 
is final.
-     *
-     * @param priority
-     *         new priority
-     * @return this Initialization, for further configuration
-     */
-    Initialization priority(InitializationPriority priority);
+    T invoke(String functionName);
 
     /**
      * Specifies the arguments to be passed to the function. Often, just a 
single {@link org.apache.tapestry5.json.JSONObject}
diff --git 
a/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/EsModuleConfigurationCallback.java
 
b/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/EsModuleConfigurationCallback.java
new file mode 100644
index 000000000..12182bae8
--- /dev/null
+++ 
b/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/EsModuleConfigurationCallback.java
@@ -0,0 +1,62 @@
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.apache.tapestry5.services.javascript;
+
+import org.apache.tapestry5.json.JSONObject;
+
+/**
+ * Interface used to  to change the JSON configuration object which will be 
used in the
+ * import map to be generated by the {@linkplain ModuleManager} service at 2 
different times:
+ * <ol>
+ *     <li>
+ *          During webapp, based on on contributions to {@linkplain 
EsModuleManager}.
+ *          These are considered global callbacks, since they affect the base
+ *          import map used in all requests.
+ *     </li>
+ *     <li>
+ *          During page rendering, allowing components, pages and base 
components
+ *          to further customize the base import map by for that specific 
request in 
+ *          a per-request basis by using the 
+ *          {@linkplain 
JavaScriptSupport#addModuleConfigurationCallback(EsModuleConfigurationCallback)}
 method.
+ *     </li>
+ * </ol>
+ *
+ * @see 
JavaScriptSupport#addEsModuleConfigurationCallback(EsModuleConfigurationCallback)
+ * @since 5.10.0
+ */
+public interface EsModuleConfigurationCallback
+{
+    /**
+     * Name of the JSON object property containing the imports in an import 
map.
+     */
+    String IMPORTS_ATTRIBUTE = "imports";
+
+    /**
+     * Receives the current configuration, which can be copied or returned, 
or, more typically, modified and returned.
+     *
+     * @param configuration
+     *         a {@link JSONObject} containing the current configuration.
+     */
+    void configure(JSONObject configuration);
+    
+    /**
+     * Utility method to set or override a module and its URL.
+     * @param object the {@link JSONObject}.
+     * @param id the module id.
+     * @param url the module URL.
+     */
+    public static void setImport(JSONObject object, String id, String url) 
+    {
+        object.in(IMPORTS_ATTRIBUTE).put(id, url);
+    }
+}
diff --git 
a/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/EsModuleInitialization.java
 
b/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/EsModuleInitialization.java
new file mode 100644
index 000000000..5894bdab3
--- /dev/null
+++ 
b/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/EsModuleInitialization.java
@@ -0,0 +1,113 @@
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.apache.tapestry5.services.javascript;
+
+import java.util.Map;
+
+/**
+ * Provided by {@link JavaScriptSupport#importEsModule(String)} to allow 
additional, optional, 
+ * details of the ES module import.
+ *
+ * @since 5.10.0
+ */
+public interface EsModuleInitialization extends 
AbstractInitialization<EsModuleInitialization>
+{
+
+    /**
+     * Defines an attribute name and value to be added to the corresponding
+     * <code>&lt;script&gt;</code> element. If the attribute was already set,
+     * its value will be overwritten.
+     * 
+     * @param name The attribute name. It cannot be null nor empty.
+     * @param value The attribute value. It cannot be null nor empty.
+     * @return this <code>EsModuleInitialization</code> for further 
configuration.
+     */
+    EsModuleInitialization withAttribute(String name, String value);
+    
+    /**
+     * Same as <code>withAttribute(name, name)</code>. Useful for attributes
+     * without values, such as <code>defer</code> and <code>async</code>.
+     * 
+     * @param name The attribute name. It cannot be null nor empty.
+     * @return this <code>EsModuleInitialization</code> for further 
configuration.
+     */
+    default EsModuleInitialization withAttribute(String name)
+    {
+        return withAttribute(name, name);
+    }
+    
+    /**
+     * Utility method for adding the <code>defer</code> attribute.
+     * @return this <code>EsModuleInitialization</code> for further 
configuration.
+     */
+    default EsModuleInitialization withDefer()
+    {
+        return withAttribute("defer");
+    }
+    
+    /**
+     * Utility method for adding the <code>async</code> attribute.
+     * @return this <code>EsModuleInitialization</code> for further 
configuration.
+     */
+    default EsModuleInitialization withAsync()
+    {
+        return withAttribute("async");
+    }
+    
+    /**
+     * Defines where this import should be done.
+     * @param placement an {@linkplain ImportPlacement} instance. It cannot be 
null.
+     * @return this <code>EsModuleInitialization</code> for further 
configuration.
+     */
+    EsModuleInitialization placement(ImportPlacement placement);
+    
+    /**
+     * Specifies the name of an module exported function to invoke. 
+     * If this method is not invoked, then the module is expected to export
+     * just a single function (which may, or may not, take {@linkplain 
#with(Object...) parameters}).
+     *
+     * @param functionName
+     *         name of a function exported by the module.
+     * @return this <code>EsModuleInitialization</code>, for further 
configuration.
+     */
+    EsModuleInitialization invoke(String functionName);
+    
+    /**
+     * Specifies the arguments to be passed to the function. Often, just a 
single {@link org.apache.tapestry5.json.JSONObject}
+     * is passed. When multiple Initializations exist with the same function 
name (or no function name), and no arguments,
+     * they are coalesced into a single EsModuleInitialization: it is assumed 
that an initialization with no parameters needs to
+     * only be invoked once.
+     *
+     * @param arguments
+     *         any number of values. Each value may be one of: null, String, 
Boolean, Number,
+     *         {@link org.apache.tapestry5.json.JSONObject}, {@link 
org.apache.tapestry5.json.JSONArray}, or
+     *         {@link org.apache.tapestry5.json.JSONLiteral}.
+     */
+    void with(Object... arguments);
+    
+    /**
+     * Returns the module id.
+     */
+    String getModuleId();
+    
+    /**
+     * Returns an unmodifiable view of the attributes.
+     */
+    Map<String, String> getAttributes();
+    
+    /**
+     * Returns the import placement.
+     */
+    ImportPlacement getPlacement();
+    
+}
diff --git 
a/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/EsModuleManager.java
 
b/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/EsModuleManager.java
new file mode 100644
index 000000000..262ae4c3b
--- /dev/null
+++ 
b/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/EsModuleManager.java
@@ -0,0 +1,57 @@
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.apache.tapestry5.services.javascript;
+
+import java.util.List;
+
+import org.apache.tapestry5.dom.Element;
+import org.apache.tapestry5.ioc.annotations.UsesOrderedConfiguration;
+
+/**
+ * Responsible for managing access to the ES modules. This service's 
distributed
+ * configuration allows the initial import map JSON object to be customized
+ * in a webapp-wide basis.
+ *
+ * @since 5.10.0
+ * @see EsModuleConfigurationCallback
+ */
+@UsesOrderedConfiguration(EsModuleConfigurationCallback.class)
+public interface EsModuleManager
+{
+    /**
+     * Invoked by the internal {@link 
org.apache.tapestry5.internal.services.DocumentLinker} service to 
+     * write the import map into the page.
+     *
+     * @param body
+     *         {@code <body>} element of the page, to which new {@code 
<script>} element(s) may be added.
+     * @param moduleConfigurationCallbacks
+     *         a list of {@link 
org.apache.tapestry5.services.javascript.ModuleConfigurationCallback}s, which
+     *         is used to customize the configuration before it is written.
+     */
+    void writeImportMap(Element head,
+                            List<EsModuleConfigurationCallback> 
moduleConfigurationCallbacks);
+
+    /**
+     * Invoked by the internal {@link 
org.apache.tapestry5.internal.services.DocumentLinker} service to write the 
+     * ES module imports (as per {@link 
JavaScriptSupport#importEsModule(String)} into the page. 
+     * this occurs after the ES module infrastructure
+     * has been written into the page, along with the core libraries.
+     *
+     * @param root
+     *         {@code <root>} element of the page.
+     * @param inits
+     *         specify initialization on the page, based on loading modules, 
extacting functions from modules, and invoking those functions
+     */
+    void writeImports(Element root, List<EsModuleInitialization> inits);
+
+}
diff --git 
a/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/ImportPlacement.java
 
b/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/ImportPlacement.java
new file mode 100644
index 000000000..c53585ffd
--- /dev/null
+++ 
b/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/ImportPlacement.java
@@ -0,0 +1,37 @@
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.apache.tapestry5.services.javascript;
+
+/**
+ * Enumeration class defining the possible placements of JavaScript imports.
+ *
+ * @since 5.10.0
+ */
+public enum ImportPlacement
+{
+    /**
+     * Inside the <code>&lt;head&gt;</code> HTML element.
+     */
+    HEAD,
+    
+    /**
+     * Towards the top of the <code>&lt;body&gt;</code> HTML element.
+     */
+    BODY_TOP,
+
+    /**
+     * Towards the bottom of the <code>&lt;body&gt;</code> HTML element.
+     */
+    BODY_BOTTOM
+
+}
diff --git 
a/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/Initialization.java
 
b/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/Initialization.java
index a633722fc..6f7e414e5 100644
--- 
a/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/Initialization.java
+++ 
b/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/Initialization.java
@@ -18,7 +18,7 @@ package org.apache.tapestry5.services.javascript;
  *
  * @since 5.4
  */
-public interface Initialization
+public interface Initialization extends AbstractInitialization<Initialization>
 {
 
     /**
diff --git 
a/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/JavaScriptSupport.java
 
b/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/JavaScriptSupport.java
index 526144da1..7624497b0 100644
--- 
a/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/JavaScriptSupport.java
+++ 
b/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/JavaScriptSupport.java
@@ -273,4 +273,23 @@ public interface JavaScriptSupport
      */
     void addModuleConfigurationCallback(ModuleConfigurationCallback callback);
 
+    /**
+     * Imports an <a 
href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules";>ES 
module</a>.
+     * @param moduleId the id of the module to import.
+     * @return an <code>EsModuleInitialization</code> instance to optionally 
configure
+     * the import and add a call an exported function, with or without 
parameters.
+     * @since 5.10.0
+     */
+    EsModuleInitialization importEsModule(String moduleId);
+
+    /**
+     * Adds an ES module configuration callback for this request.
+     *
+     * @param callback
+     *         a {@link ModuleConfigurationCallback}. It cannot be null.
+     * @see 
DocumentLinker#addModuleConfigurationCallback(ModuleConfigurationCallback)
+     * @since 5.10.0
+     */
+    void addEsModuleConfigurationCallback(EsModuleConfigurationCallback 
callback);
+
 }
diff --git 
a/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/ModuleConfigurationCallback.java
 
b/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/ModuleConfigurationCallback.java
index 79c5d00f8..790aaef2d 100644
--- 
a/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/ModuleConfigurationCallback.java
+++ 
b/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/ModuleConfigurationCallback.java
@@ -33,7 +33,7 @@ import org.apache.tapestry5.json.JSONObject;
 public interface ModuleConfigurationCallback
 {
     /**
-     * Receives the current configuration, which can be copied or returned, or 
(more typically) modified and returned.
+     * Receives the current configuration, which can be copied or returned, 
or, more typically, modified and returned.
      *
      * @param configuration
      *         a {@link JSONObject} containing the current configuration.
diff --git 
a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/EsModuleTests.java
 
b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/EsModuleTests.java
new file mode 100644
index 000000000..54be632ac
--- /dev/null
+++ 
b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/EsModuleTests.java
@@ -0,0 +1,153 @@
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.apache.tapestry5.integration.app1;
+
+import static 
org.apache.tapestry5.integration.app1.services.AppModule.NON_OVERRIDDEN_ES_MODULE_ID;
+import static 
org.apache.tapestry5.integration.app1.services.AppModule.NON_OVERRIDDEN_ES_MODULE_URL;
+import static 
org.apache.tapestry5.integration.app1.services.AppModule.OVERRIDDEN_ES_MODULE_ID;
+import static 
org.apache.tapestry5.integration.app1.services.AppModule.OVERRIDDEN_ES_MODULE_NEW_URL;
+
+import org.apache.tapestry5.integration.app1.pages.EsModuleDemo;
+import org.apache.tapestry5.ioc.annotations.Inject;
+import org.apache.tapestry5.json.JSONObject;
+import org.apache.tapestry5.services.AssetSource;
+import org.apache.tapestry5.services.javascript.EsModuleConfigurationCallback;
+import org.apache.tapestry5.services.javascript.EsModuleManager;
+import org.apache.tapestry5.services.javascript.JavaScriptSupport;
+import org.testng.annotations.Test;
+
+/**
+ * ES module tests.
+ */
+public class EsModuleTests extends App1TestCase
+{
+    private static final String PAGE_NAME = "ES Module Demo";
+    
+    private static final String REQUEST_CALLBACK_SWITCHER = "css=.switch";
+    
+    @Inject
+    private AssetSource assetSource;
+
+    /**
+     * Tests whether ES modules placed in /META-INF/es-modules are 
automatically
+     * added to import maps.
+     */
+    @Test
+    public void automatic_modules()
+    {
+        openLinks(PAGE_NAME);
+        JSONObject importMap = getImportMap();
+        assertModuleUrlSuffix("foo/bar", "/es-modules/foo/bar.js", importMap);
+        assertModuleUrlSuffix("root-folder", "/es-modules/root-folder.js", 
importMap);
+    }
+    
+    /**
+     * Tests whether ES modules added or overriden through global callbacks
+     * (i.e. ones contributed to {@link EsModuleManager} configuration)
+     * are being actually included in the generated import map.
+     */
+    @Test
+    public void modules_added_by_global_callbacks()
+    {
+        openLinks(PAGE_NAME);
+        JSONObject importMap = getImportMap();
+        assertModulesDefinedByGlobalCallbacks(importMap);
+    }
+    
+    /**
+     * Tests whether ES modules added or overriden through request callbacks
+     * (i.e. ones added through {@link 
JavaScriptSupport#addEsModuleConfigurationCallback(EsModuleConfigurationCallback)})
+     * are being actually included in the generated import map.
+     * @throws InterruptedException 
+     */
+    @Test
+    public void modules_added_by_request_callbacks()
+    {
+        openLinks(PAGE_NAME);
+        
+        // With import map changed by request callbacks.
+        clickAndWait(REQUEST_CALLBACK_SWITCHER);
+        JSONObject importMap = getImportMap();
+        assertModuleUrl(NON_OVERRIDDEN_ES_MODULE_ID, 
NON_OVERRIDDEN_ES_MODULE_URL, importMap);
+        assertModuleUrl(OVERRIDDEN_ES_MODULE_ID, 
EsModuleDemo.REQUEST_OVERRIDEN_MODULE_URL, importMap);
+        
+        // Now without import map changed by request callbacks, so we can test
+        // the global import map wasn't affected.
+        clickAndWait(REQUEST_CALLBACK_SWITCHER);
+        importMap = getImportMap();
+        assertModulesDefinedByGlobalCallbacks(importMap);
+        
+    }
+    
+    /**
+     * Tests {@link JavaScriptSupport#importEsModule(String)}.
+     */
+    @Test
+    public void javascript_support_importEsModule() throws InterruptedException
+    {
+
+        openLinks(PAGE_NAME);
+        
+        // Module imported with specified attributes.
+        assertTrue(isElementPresent("//script[@type='module'][contains(@src, 
'foo/bar.js')][@defer='defer'][@async='async'][@something='else'][@foo='foo']"));
+        
+        // Module imported with no placement (default body bottom) or
+        // BODY_BOTTOM should be after the last <div> in this webapp's template
+        
assertTrue(isElementPresent("//body/div[last()][following-sibling::script[@type='module'][contains(@src,
 '/placement/body-bottom.js')]]"));
+        
+        // Module imported with placement BODY_TOP should be before 
+        // the last <div> in this webapp's template (the first one comes from 
JS).
+        
assertTrue(isElementPresent("//body/div[@role='navigation'][preceding-sibling::script[@type='module'][contains(@src,
 '/placement/body-top.js')]]"));   
+
+        // Module imported with placement HEAD
+        
assertTrue(isElementPresent("//head/script[@type='module'][contains(@src, 
'/placement/head.js')]"));
+        
+        // Checking results of running the modules, not just their inclusion 
in HTML
+        assertEquals(getText("message"), "ES module foo/bar imported 
correctly!");
+        assertEquals(getText("head-message"), "ES module imported correctly 
(<head>)!");
+        assertEquals(getText("body-top-message"), "ES module imported 
correctly (<body> top)!");
+        assertEquals(getText("body-bottom-message"), "ES module imported 
correctly (<body> bottom)!");
+        assertEquals(getText("root-folder-message"), "ES module imported 
correctly from the root folder!");
+        assertEquals(getText("outside-metainf-message"), "ES module correctly 
imported from outside /META-INF/assets/es-modules!");
+
+    }
+
+    private void assertModulesDefinedByGlobalCallbacks(JSONObject importMap) {
+        assertModuleUrl(NON_OVERRIDDEN_ES_MODULE_ID, 
NON_OVERRIDDEN_ES_MODULE_URL, importMap);
+        assertModuleUrl(OVERRIDDEN_ES_MODULE_ID, OVERRIDDEN_ES_MODULE_NEW_URL, 
importMap);
+    }
+    
+    private void assertModuleUrlSuffix(String id, String urlSuffix, JSONObject 
importMap)
+    {
+        final JSONObject imports = (JSONObject) 
importMap.get(EsModuleConfigurationCallback.IMPORTS_ATTRIBUTE);
+        final String url = imports.getString(id);
+        
+        assertNotNull(url, String.format("Module %s not found in import 
map\n%s", id, importMap.toString(false)));
+        assertTrue(url.endsWith(urlSuffix), String.format("Unexpected URL %s 
for module %s (expected %s suffix)", url, id, urlSuffix));
+    }
+    
+    private void assertModuleUrl(String id, String urlSuffix, JSONObject 
importMap)
+    {
+        final JSONObject imports = (JSONObject) 
importMap.get(EsModuleConfigurationCallback.IMPORTS_ATTRIBUTE);
+        final String url = imports.getString(id);
+        
+        assertNotNull(url, String.format("Module %s not found in import 
map\n%s", id, importMap.toString(false)));
+        assertEquals(url, urlSuffix, String.format("Unexpected URL %s for 
module %s (expected %s suffix)", url, id, urlSuffix));
+    }
+    
+    private JSONObject getImportMap()
+    {
+        return new JSONObject(getText("import-map-listing"));
+    }
+
+}
diff --git 
a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/EsModuleDemo.java
 
b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/EsModuleDemo.java
new file mode 100644
index 000000000..e53bd964b
--- /dev/null
+++ 
b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/EsModuleDemo.java
@@ -0,0 +1,86 @@
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.apache.tapestry5.integration.app1.pages;
+
+import org.apache.tapestry5.annotations.Property;
+import org.apache.tapestry5.annotations.SetupRender;
+import org.apache.tapestry5.integration.app1.services.AppModule;
+import org.apache.tapestry5.ioc.annotations.Inject;
+import org.apache.tapestry5.services.javascript.EsModuleConfigurationCallback;
+import org.apache.tapestry5.services.javascript.ImportPlacement;
+import org.apache.tapestry5.services.javascript.JavaScriptSupport;
+
+public class EsModuleDemo
+{
+    public static final String REQUEST_OVERRIDEN_MODULE_URL = 
"/overridenAgainURL";
+
+    @Inject
+    private JavaScriptSupport javaScriptSupport;
+    
+    @Property
+    Boolean overrideEsModuleImportAgain;
+
+    @SetupRender
+    void importEsModule()
+    {
+        // Checking each module is only imported once.
+        javaScriptSupport.importEsModule("foo/bar")
+            .withDefer()
+            .withAsync()
+            .withAttribute("foo")
+            .withAttribute("something", "else");
+        javaScriptSupport.importEsModule("foo/bar");
+
+        javaScriptSupport.importEsModule("placement/body-bottom")
+            .placement(ImportPlacement.BODY_BOTTOM);
+        javaScriptSupport.importEsModule("placement/body-top")
+            .placement(ImportPlacement.BODY_TOP);
+        javaScriptSupport.importEsModule("placement/head")
+            .placement(ImportPlacement.HEAD);
+        javaScriptSupport.importEsModule("root-folder");
+        javaScriptSupport.importEsModule("outside-metainf");        
+        javaScriptSupport.importEsModule("show-import-map");
+
+        if (overrideEsModuleImportAgain != null && overrideEsModuleImportAgain)
+        {
+            javaScriptSupport.addEsModuleConfigurationCallback(
+                    o -> EsModuleConfigurationCallback.setImport(o, 
+                            AppModule.OVERRIDDEN_ES_MODULE_ID, 
REQUEST_OVERRIDEN_MODULE_URL));
+        }
+        
+    }
+
+    void onActivate(boolean overrideEsModuleImportAgain)
+    {
+        this.overrideEsModuleImportAgain = overrideEsModuleImportAgain;
+    }
+    
+    Object onEnableOverride()
+    {
+        overrideEsModuleImportAgain = true;
+        return this;
+    }
+    
+    void onDisableOverride()
+    {
+        overrideEsModuleImportAgain = false;
+    }
+    
+    Object[] onPassivate()
+    {
+        return overrideEsModuleImportAgain != null && 
overrideEsModuleImportAgain
+                ? new Object[] { overrideEsModuleImportAgain } 
+                : null;
+    }
+    
+}
diff --git 
a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/Index.java
 
b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/Index.java
index 025b7eb07..cbbc34c5d 100644
--- 
a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/Index.java
+++ 
b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/Index.java
@@ -630,7 +630,9 @@ public class Index
                     
                     new Item("RecursiveDemo","Recursive Demo","Recursive 
component example"),
                     
-                    new Item("SelfRecursiveDemo", "Self-Recursive Demo", 
"check for handling of self-recursive components")
+                    new Item("SelfRecursiveDemo", "Self-Recursive Demo", 
"check for handling of self-recursive components"),
+                    
+                    new Item("EsModuleDemo", "ES Module Demo", "tests and 
demonstrations for the ES module support")
                 );
 
     static
diff --git 
a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/services/AppModule.java
 
b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/services/AppModule.java
index 6801928b8..e8b91f4d0 100644
--- 
a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/services/AppModule.java
+++ 
b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/services/AppModule.java
@@ -50,6 +50,7 @@ import org.apache.tapestry5.ioc.annotations.Contribute;
 import org.apache.tapestry5.ioc.annotations.Marker;
 import org.apache.tapestry5.ioc.annotations.Value;
 import org.apache.tapestry5.ioc.services.ServiceOverride;
+import org.apache.tapestry5.services.AssetSource;
 import org.apache.tapestry5.services.BeanBlockContribution;
 import org.apache.tapestry5.services.BeanBlockSource;
 import org.apache.tapestry5.services.ComponentClassResolver;
@@ -58,6 +59,7 @@ import org.apache.tapestry5.services.LibraryMapping;
 import org.apache.tapestry5.services.ResourceDigestGenerator;
 import org.apache.tapestry5.services.ValueEncoderFactory;
 import org.apache.tapestry5.services.ValueLabelProvider;
+import org.apache.tapestry5.services.javascript.EsModuleConfigurationCallback;
 import org.apache.tapestry5.services.pageload.PageCachingReferenceTypeService;
 import org.apache.tapestry5.services.pageload.PagePreloader;
 import org.apache.tapestry5.services.pageload.PreloaderMode;
@@ -476,5 +478,43 @@ public class AppModule
         }
         
     }
+    
+    public static final String NON_OVERRIDDEN_ES_MODULE_ID = "nonOverriden";
+    
+    public static final String NON_OVERRIDDEN_ES_MODULE_URL = 
"/nonOverridenURL";
+    
+    public static final String OVERRIDDEN_ES_MODULE_ID = "overriden";
+
+    public static final String OVERRIDDEN_ES_MODULE_ORIGINAL_URL = 
"/originalURL";
+    
+    public static final String OVERRIDDEN_ES_MODULE_NEW_URL = "/overridenURL";
+    
+    public static void contributeEsModuleManager(
+            OrderedConfiguration<EsModuleConfigurationCallback> configuration,
+            AssetSource assetSource)
+    {
+        final String original = "OriginalCallback";
+        final String override = "OverrideCallback";
+        
+        configuration.add(override, 
+                o -> EsModuleConfigurationCallback.setImport(o, 
OVERRIDDEN_ES_MODULE_ID, OVERRIDDEN_ES_MODULE_NEW_URL),
+                "after:" + original);
+        
+        configuration.add(original, 
+                o -> { 
+                    EsModuleConfigurationCallback.setImport(o, 
NON_OVERRIDDEN_ES_MODULE_ID, NON_OVERRIDDEN_ES_MODULE_URL);
+                    EsModuleConfigurationCallback.setImport(o, 
OVERRIDDEN_ES_MODULE_ID, OVERRIDDEN_ES_MODULE_ORIGINAL_URL);
+                });
+        
+        configuration.add("Outside META-INF", o -> 
+            EsModuleConfigurationCallback.setImport(o, "outside-metainf", 
+                    
assetSource.getClasspathAsset("/org/apache/tapestry5/integration/app1/es-module-outside-metainf.js").toClientURL())
+        );
+
+        configuration.add("External URL", o -> 
+            EsModuleConfigurationCallback.setImport(o, "external/url", 
"https://example.com/module.js";)
+        );
+
+    }
 
 }
diff --git 
a/tapestry-core/src/test/resources/META-INF/assets/es-modules/foo/bar.js 
b/tapestry-core/src/test/resources/META-INF/assets/es-modules/foo/bar.js
new file mode 100644
index 000000000..14a37c87b
--- /dev/null
+++ b/tapestry-core/src/test/resources/META-INF/assets/es-modules/foo/bar.js
@@ -0,0 +1,2 @@
+console.log("Yup, I'm an ES module!");
+document.getElementById("message").innerHTML = "ES module foo/bar imported 
correctly!";
\ No newline at end of file
diff --git 
a/tapestry-core/src/test/resources/META-INF/assets/es-modules/placement/body-bottom.js
 
b/tapestry-core/src/test/resources/META-INF/assets/es-modules/placement/body-bottom.js
new file mode 100644
index 000000000..2fbf023fd
--- /dev/null
+++ 
b/tapestry-core/src/test/resources/META-INF/assets/es-modules/placement/body-bottom.js
@@ -0,0 +1,2 @@
+console.log("I should go into the bottom of <body>!");
+document.getElementById("body-bottom-message").innerHTML = "ES module imported 
correctly (&lt;body&gt; bottom)!";
\ No newline at end of file
diff --git 
a/tapestry-core/src/test/resources/META-INF/assets/es-modules/placement/body-top.js
 
b/tapestry-core/src/test/resources/META-INF/assets/es-modules/placement/body-top.js
new file mode 100644
index 000000000..4b9e268c3
--- /dev/null
+++ 
b/tapestry-core/src/test/resources/META-INF/assets/es-modules/placement/body-top.js
@@ -0,0 +1,2 @@
+console.log("I should go into the top of <body>!");
+document.getElementById("body-top-message").innerHTML = "ES module imported 
correctly (&lt;body&gt; top)!";
\ No newline at end of file
diff --git 
a/tapestry-core/src/test/resources/META-INF/assets/es-modules/placement/head.js 
b/tapestry-core/src/test/resources/META-INF/assets/es-modules/placement/head.js
new file mode 100644
index 000000000..ac86164ef
--- /dev/null
+++ 
b/tapestry-core/src/test/resources/META-INF/assets/es-modules/placement/head.js
@@ -0,0 +1,2 @@
+console.log("I should go into <head>!");
+document.getElementById("head-message").innerHTML = "ES module imported 
correctly (&lt;head&gt;)!";
\ No newline at end of file
diff --git 
a/tapestry-core/src/test/resources/META-INF/assets/es-modules/root-folder.js 
b/tapestry-core/src/test/resources/META-INF/assets/es-modules/root-folder.js
new file mode 100644
index 000000000..db38f9256
--- /dev/null
+++ b/tapestry-core/src/test/resources/META-INF/assets/es-modules/root-folder.js
@@ -0,0 +1,2 @@
+console.log("I'm in the root folder!");
+document.getElementById("root-folder-message").innerHTML = "ES module imported 
correctly from the root folder!";
\ No newline at end of file
diff --git 
a/tapestry-core/src/test/resources/META-INF/assets/es-modules/show-import-map.js
 
b/tapestry-core/src/test/resources/META-INF/assets/es-modules/show-import-map.js
new file mode 100644
index 000000000..7bfd34b48
--- /dev/null
+++ 
b/tapestry-core/src/test/resources/META-INF/assets/es-modules/show-import-map.js
@@ -0,0 +1,2 @@
+let importMap = document.querySelector("script[type=importmap]").innerHTML;
+document.getElementById("import-map-listing").innerHTML = importMap;
\ No newline at end of file
diff --git 
a/tapestry-core/src/test/resources/org/apache/tapestry5/integration/app1/es-module-outside-metainf.js
 
b/tapestry-core/src/test/resources/org/apache/tapestry5/integration/app1/es-module-outside-metainf.js
new file mode 100644
index 000000000..f41c552ca
--- /dev/null
+++ 
b/tapestry-core/src/test/resources/org/apache/tapestry5/integration/app1/es-module-outside-metainf.js
@@ -0,0 +1,2 @@
+console.log("ES module outside /META-INF/assets/es-modules/");
+document.getElementById("outside-metainf-message").innerHTML = "ES module 
correctly imported from outside /META-INF/assets/es-modules!";
\ No newline at end of file
diff --git 
a/tapestry-core/src/test/resources/org/apache/tapestry5/integration/app1/pages/EsModuleDemo.tml
 
b/tapestry-core/src/test/resources/org/apache/tapestry5/integration/app1/pages/EsModuleDemo.tml
new file mode 100644
index 000000000..4c6c3fde5
--- /dev/null
+++ 
b/tapestry-core/src/test/resources/org/apache/tapestry5/integration/app1/pages/EsModuleDemo.tml
@@ -0,0 +1,30 @@
+<html t:type="Border" 
xmlns:t="http://tapestry.apache.org/schema/tapestry_5_3.xsd"; 
xmlns:p="tapestry:parameter">
+
+       <h1>ES Module Import Demo</h1>
+       
+       <p>
+               <t:if test="overrideEsModuleImportAgain">
+                       <p:then>
+                               <a t:type="EventLink" event="disableOverride" 
class="switch">Don't override ES module import again.</a>
+                       </p:then>
+                       <p:else>
+                               <a t:type="EventLink" event="enableOverride" 
class="switch">Override ES module import again.</a>
+                       </p:else>
+               </t:if>
+       </p>
+       
+       <p id="message"/>
+       <p id="head-message"/>
+       <p id="body-top-message"/>
+       <p id="body-bottom-message"/>
+       <p id="root-folder-message"/>
+       <p id="outside-metainf-message"/>
+       
+       <p>
+               Import map:
+       </p>
+       <pre id="import-map-listing"/>
+       
+       <p id="last-body-element"></p>
+
+</html>
\ No newline at end of file

Reply via email to