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

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


The following commit(s) were added to refs/heads/master by this push:
     new eebe32598 TAP5-2803: foundation work for ES module support
eebe32598 is described below

commit eebe32598f67ddc9c6152b6efb7b21d4e67a53ea
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
    
    TAP5-2803: fixing JavaDoc errors
    
    TAP5-2803: adding @Import(esModule)
    
    TAP5-2803: fixing DocumentLinkerImplTest failures
    
    TAP5-2803: fixing live reloading of files in /META-INF/es-assets
    
    TAP5-2803: fixing broken commit_with_no_javascript test
    
    TAP5-2803: Implemented EsModuleInitialization.invoke() and .with().
    
    TAP5-2803: supporting Number and Boolean in ES module's .with()
    
    plus tests to verify all values were passed correctly
---
 5_10_RELEASE_NOTES.md                              |  24 ++
 .../java/org/apache/tapestry5/SymbolConstants.java |  14 +-
 .../org/apache/tapestry5/annotations/Import.java   |  10 +
 .../internal/services/DocumentLinker.java          |  16 +
 .../internal/services/DocumentLinkerImpl.java      |  41 ++-
 .../internal/services/EsModuleInitsManager.java    |  61 ++++
 .../services/PartialMarkupDocumentLinker.java      |  17 +-
 .../internal/services/ajax/BaseInitialization.java |  27 ++
 .../services/ajax/EsModuleInitializationImpl.java  |  67 ++++
 .../internal/services/ajax/InitializationImpl.java |  37 +++
 .../services/ajax/JavaScriptSupportImpl.java       |  93 +++---
 .../services/assets/ResourceChangeTrackerImpl.java |   3 +-
 .../services/javascript/EsModuleManagerImpl.java   | 345 +++++++++++++++++++++
 .../tapestry5/internal/transform/ImportWorker.java |  48 ++-
 .../apache/tapestry5/modules/JavaScriptModule.java |   4 +
 .../apache/tapestry5/modules/TapestryModule.java   |  16 +-
 ...ialization.java => AbstractInitialization.java} |  20 +-
 .../javascript/EsModuleConfigurationCallback.java  |  62 ++++
 .../javascript/EsModuleInitialization.java         |  96 ++++++
 .../services/javascript/EsModuleManager.java       |  57 ++++
 .../services/javascript/ImportPlacement.java       |  37 +++
 .../services/javascript/Initialization.java        |   2 +-
 .../services/javascript/JavaScriptSupport.java     |  19 ++
 .../javascript/ModuleConfigurationCallback.java    |   2 +-
 .../services/DocumentLinkerImplTest.groovy         |  30 +-
 .../tapestry5/integration/app1/EsModuleTests.java  | 221 +++++++++++++
 .../integration/app1/pages/EsModuleDemo.java       | 107 +++++++
 .../tapestry5/integration/app1/pages/Index.java    |   4 +-
 .../integration/app1/services/AppModule.java       |  40 +++
 .../javascript/EsModuleManagerImplTest.java        |  93 ++++++
 .../META-INF/assets/es-modules/default-export.js   |   3 +
 .../META-INF/assets/es-modules/foo/bar.js          |   2 +
 .../assets/es-modules/non-default-export.js        |   6 +
 .../es-modules/parameter-type-default-export.js    |  14 +
 .../es-modules/parameterless-default-export.js     |   3 +
 .../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        |  34 ++
 .../ioc/internal/util/URLChangeTracker.java        |  21 ++
 43 files changed, 1605 insertions(+), 103 deletions(-)

diff --git a/5_10_RELEASE_NOTES.md b/5_10_RELEASE_NOTES.md
new file mode 100644
index 000000000..e7d1c36da
--- /dev/null
+++ b/5_10_RELEASE_NOTES.md
@@ -0,0 +1,24 @@
+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)`
+* `org.apache.tapestry5.annotations.Import.esModule()`
+
+# 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 1e556bb12..47a5cadd7 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 jakarta.servlet.ServletContext#getContextPath()}.
diff --git 
a/tapestry-core/src/main/java/org/apache/tapestry5/annotations/Import.java 
b/tapestry-core/src/main/java/org/apache/tapestry5/annotations/Import.java
index 2e50e3f7c..9eec6ac5d 100644
--- a/tapestry-core/src/main/java/org/apache/tapestry5/annotations/Import.java
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/annotations/Import.java
@@ -82,4 +82,14 @@ public @interface Import
      * @since 5.4
      */
     String[] module() default {};
+    
+    /**
+     * Ids of ES modules to import. 
+     *
+     * @see org.apache.tapestry5.services.javascript.EsModuleManager
+     * @see JavaScriptSupport#importEsModule(String)
+     * @since 5.10.0
+     */
+    String[] esModule() default {};
+    
 }
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..e3fc767fe
--- /dev/null
+++ 
b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/EsModuleInitsManager.java
@@ -0,0 +1,61 @@
+// 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.internal.services.ajax.EsModuleInitializationImpl;
+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 = ((EsModuleInitializationImpl) 
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/BaseInitialization.java
 
b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ajax/BaseInitialization.java
new file mode 100644
index 000000000..589a8850a
--- /dev/null
+++ 
b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ajax/BaseInitialization.java
@@ -0,0 +1,27 @@
+package org.apache.tapestry5.internal.services.ajax;
+
+import org.apache.tapestry5.ioc.internal.util.InternalUtils;
+import org.apache.tapestry5.services.javascript.AbstractInitialization;
+
+abstract class BaseInitialization<T extends AbstractInitialization<?>> 
implements AbstractInitialization<T>
+{
+    final String moduleName;
+
+    protected String functionName;
+
+    BaseInitialization(String moduleName)
+    {
+        this.moduleName = moduleName;
+    }
+
+    @SuppressWarnings("unchecked")
+    public T invoke(String functionName)
+    {
+        assert InternalUtils.isNonBlank(functionName);
+
+        this.functionName = functionName;
+
+        return (T) this;
+    }
+    
+}
\ No newline at end of file
diff --git 
a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ajax/EsModuleInitializationImpl.java
 
b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ajax/EsModuleInitializationImpl.java
new file mode 100644
index 000000000..3b3bfeb4a
--- /dev/null
+++ 
b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ajax/EsModuleInitializationImpl.java
@@ -0,0 +1,67 @@
+package org.apache.tapestry5.internal.services.ajax;
+
+import java.util.Collections;
+import java.util.Map;
+
+import org.apache.tapestry5.commons.util.CollectionFactory;
+import org.apache.tapestry5.services.javascript.EsModuleInitialization;
+import org.apache.tapestry5.services.javascript.ImportPlacement;
+
+public class EsModuleInitializationImpl extends 
BaseInitialization<EsModuleInitialization> implements EsModuleInitialization
+{
+    
+    private Map<String, String> attributes;
+    private ImportPlacement placement = ImportPlacement.BODY_BOTTOM;
+    private Object[] arguments;
+    
+    EsModuleInitializationImpl(String moduleName) 
+    {
+        super(moduleName);
+    }
+
+    public EsModuleInitialization withAttribute(String id, String value) 
+    {
+        if (attributes == null)
+        {
+            attributes = CollectionFactory.newMap();
+        }
+        attributes.put(id, value);
+        return this;
+    }
+
+    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();
+    }
+
+    public ImportPlacement getPlacement() {
+        return placement;
+    }
+    
+    public String getFunctionName() {
+        return functionName;
+    }
+
+    @Override
+    public void with(Object... arguments) 
+    {
+        this.arguments = arguments;
+    }
+    
+    public Object[] getArguments() 
+    {
+        return arguments;
+    }
+    
+}
\ No newline at end of file
diff --git 
a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ajax/InitializationImpl.java
 
b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ajax/InitializationImpl.java
new file mode 100644
index 000000000..59e2ff974
--- /dev/null
+++ 
b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ajax/InitializationImpl.java
@@ -0,0 +1,37 @@
+package org.apache.tapestry5.internal.services.ajax;
+
+import org.apache.tapestry5.json.JSONArray;
+import org.apache.tapestry5.services.javascript.Initialization;
+import org.apache.tapestry5.services.javascript.InitializationPriority;
+
+class InitializationImpl extends BaseInitialization<Initialization> implements 
Initialization
+{
+    
+    JSONArray arguments;
+    
+    InitializationPriority priority = InitializationPriority.NORMAL;
+
+    public InitializationImpl(String moduleName) 
+    {
+        super(moduleName);
+    }
+    
+    public Initialization priority(InitializationPriority priority)
+    {
+        assert priority != null;
+
+        this.priority = priority;
+
+        return this;
+    }
+    
+    @Override
+    public void with(Object... arguments)
+    {
+        assert arguments != null;
+
+        this.arguments = new JSONArray(arguments);
+    }
+
+    
+}
\ No newline at end of file
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..5cf4dbd3b 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,12 @@
 
 package org.apache.tapestry5.internal.services.ajax;
 
+import java.util.ArrayList;
+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 +32,15 @@ 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.EsModuleConfigurationCallback;
+import org.apache.tapestry5.services.javascript.EsModuleInitialization;
+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 +59,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,47 +78,6 @@ public class JavaScriptSupportImpl implements 
JavaScriptSupport
 
     private Map<String, String> libraryURLToStackName, moduleNameToStackName;
 
-    class InitializationImpl implements Initialization
-    {
-        InitializationPriority priority = InitializationPriority.NORMAL;
-
-        final String moduleName;
-
-        String functionName;
-
-        JSONArray arguments;
-
-        InitializationImpl(String moduleName)
-        {
-            this.moduleName = moduleName;
-        }
-
-        public Initialization invoke(String functionName)
-        {
-            assert InternalUtils.isNonBlank(functionName);
-
-            this.functionName = functionName;
-
-            return this;
-        }
-
-        public Initialization priority(InitializationPriority priority)
-        {
-            assert priority != null;
-
-            this.priority = priority;
-
-            return this;
-        }
-
-        public void with(Object... arguments)
-        {
-            assert arguments != null;
-
-            this.arguments = new JSONArray(arguments);
-        }
-    }
-
     public JavaScriptSupportImpl(DocumentLinker linker, JavaScriptStackSource 
javascriptStackSource,
                                  JavaScriptStackPathConstructor 
stackPathConstructor, BooleanHook suppressCoreStylesheetsHook)
     {
@@ -150,6 +125,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 +153,11 @@ public class JavaScriptSupportImpl implements 
JavaScriptSupport
                 linker.addInitialization(element.priority, element.moduleName, 
element.functionName, element.arguments);
             }
         });
+        
+        if (!esModuleInits.isEmpty())
+        {
+            esModuleInits.stream().forEach(linker::addEsModuleInitialization);
+        }
     }
 
     public void addInitializerCall(InitializationPriority priority, String 
functionName, JSONObject parameter)
@@ -462,4 +444,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/assets/ResourceChangeTrackerImpl.java
 
b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/assets/ResourceChangeTrackerImpl.java
index 73d4d2644..54e3bc485 100644
--- 
a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/assets/ResourceChangeTrackerImpl.java
+++ 
b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/assets/ResourceChangeTrackerImpl.java
@@ -92,12 +92,11 @@ public class ResourceChangeTrackerImpl extends 
InvalidationEventHubImpl implemen
 
     public void forceInvalidationEvent()
     {
-        fireInvalidationEvent();
-
         if (tracker != null)
         {
             tracker.clear();
         }
+        fireInvalidationEvent();
     }
 
     public void checkForUpdates()
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..9f9bf5aea
--- /dev/null
+++ 
b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/javascript/EsModuleManagerImpl.java
@@ -0,0 +1,345 @@
+// 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.Asset;
+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.ajax.EsModuleInitializationImpl;
+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.JSONCollection;
+import org.apache.tapestry5.json.JSONLiteral;
+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
+{
+
+    private static final String GENERIC_IMPORTED_VARIABLE = "m";
+
+    /**
+     * 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 ResourceChangeTracker resourceChangeTracker;
+    
+    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,
+                             ResourceChangeTracker resourceChangeTracker)
+    {
+        this.compactJSON = compactJSON;
+        this.assetSource = assetSource;
+        this.classpathScanner = classpathScanner;
+        this.globalCallbacks = globalCallbacks;
+        this.productionMode = productionMode;
+        this.resourceChangeTracker = resourceChangeTracker;
+        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);
+        
+        resourceChangeTracker.addInvalidationCallback(this::createImportMap);
+        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('.'));
+                            
+                final Asset asset = assetSource.getClasspathAsset(file);
+                resourceChangeTracker.trackResource(asset.getResource());
+                imports.put(id, asset.toClientURL());
+            }
+        } catch (IOException e) 
+        {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @PostInjection
+    public void setupInvalidation(ResourceChangeTracker tracker)
+    {
+        
+    }
+
+    @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;
+        EsModuleInitializationImpl init;
+        String functionName;
+        Object[] arguments;
+        
+        for (EsModuleInitialization i : inits) 
+        {
+            
+            init = (EsModuleInitializationImpl) i;
+            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);
+                }
+            }
+            
+            writeAttributes(script, init);
+            script.attribute("src", url);
+            
+            functionName = init.getFunctionName();
+            arguments = init.getArguments();
+            
+            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);
+            }
+            
+            // If we have not only the import, but also an automatic function 
call
+            if (arguments != null || functionName != null)
+            {
+                final Element moduleFunctionCall = script.element("script");
+                
+                moduleFunctionCall.moveAfter(script);
+                
+                final String moduleFunctionCallFormat = 
+                        "import %s from '%s';\n"
+                        + "%s(%s);";
+                
+                final String importName = functionName != null ? functionName 
: GENERIC_IMPORTED_VARIABLE;
+                final String importDeclaration = functionName != null ? 
+                        "{ " + functionName + " }": 
+                            GENERIC_IMPORTED_VARIABLE;
+                
+                
moduleFunctionCall.text(String.format(moduleFunctionCallFormat, 
+                        importDeclaration, moduleId, importName,
+                        convertToJsFunctionParameters(arguments, 
compactJSON)));
+                
+                writeAttributes(moduleFunctionCall, init);
+                
+                // Avoiding duplicated ids
+                final String id = moduleFunctionCall.getAttribute("id");
+                if (id != null)
+                {
+                    moduleFunctionCall.forceAttributes("id", id + 
"-function-call");
+                }
+                
+            }
+            
+        }
+        
+    }
+    
+    static String convertToJsFunctionParameters(Object[] arguments, boolean 
compactJSON)
+    {
+        String result;
+        if (arguments == null || arguments.length == 0)
+        {
+            result = "";
+        }
+        else if (arguments.length == 1)
+        {
+            result = convertToJsFunctionParameter(arguments[0], compactJSON);
+        }
+        else {
+            StringBuilder builder = new StringBuilder();
+            for (int i = 0; i < arguments.length; i++) 
+            {
+                if (i > 0)
+                {
+                    builder.append(", ");
+                }
+                builder.append(convertToJsFunctionParameter(arguments[i], 
compactJSON));
+            }
+            result = builder.toString();
+        }
+        return result;
+    }
+
+    static String convertToJsFunctionParameter(Object object, boolean 
compactJSON) 
+    {
+        String result;
+        
+        if (object == null)
+        {
+            result = null;
+        }
+        else if (object instanceof String || object instanceof JSONLiteral)
+        {
+            result = "'" + object.toString() + "'";
+        }
+        else if (object instanceof Number || object instanceof Boolean)
+        {
+            result = object.toString();
+        }
+        else if (object instanceof JSONCollection)
+        {
+            result = ((JSONCollection) object).toString(compactJSON);
+        }
+        else
+        {
+            throw new IllegalArgumentException(String.format(
+                    "Unsupported value: %s (type %s)", object.toString(), 
object.getClass().getName()));
+        }
+        
+        return result;
+    }
+
+    private void writeAttributes(Element script, EsModuleInitializationImpl 
init) {
+        final Map<String, String> attributes = init.getAttributes();
+        for (String name : attributes.keySet())
+        {
+            script.attribute(name, attributes.get(name));
+        }
+        
+        script.attribute("type", "module");
+    }
+
+    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/internal/transform/ImportWorker.java
 
b/tapestry-core/src/main/java/org/apache/tapestry5/internal/transform/ImportWorker.java
index 36130ddaf..cc93a7b95 100644
--- 
a/tapestry-core/src/main/java/org/apache/tapestry5/internal/transform/ImportWorker.java
+++ 
b/tapestry-core/src/main/java/org/apache/tapestry5/internal/transform/ImportWorker.java
@@ -12,6 +12,11 @@
 
 package org.apache.tapestry5.internal.transform;
 
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
 import org.apache.tapestry5.Asset;
 import org.apache.tapestry5.ComponentResources;
 import org.apache.tapestry5.SymbolConstants;
@@ -23,8 +28,18 @@ import org.apache.tapestry5.func.Worker;
 import org.apache.tapestry5.internal.services.assets.ResourceChangeTracker;
 import org.apache.tapestry5.ioc.services.SymbolSource;
 import org.apache.tapestry5.model.MutableComponentModel;
-import org.apache.tapestry5.plastic.*;
+import org.apache.tapestry5.plastic.ComputedValue;
+import org.apache.tapestry5.plastic.FieldHandle;
+import org.apache.tapestry5.plastic.InstanceContext;
+import org.apache.tapestry5.plastic.MethodAdvice;
+import org.apache.tapestry5.plastic.MethodInvocation;
+import org.apache.tapestry5.plastic.PlasticClass;
+import org.apache.tapestry5.plastic.PlasticField;
+import org.apache.tapestry5.plastic.PlasticMethod;
+import org.apache.tapestry5.plastic.PlasticUtils;
 import org.apache.tapestry5.plastic.PlasticUtils.FieldInfo;
+import org.apache.tapestry5.plastic.PropertyAccessType;
+import org.apache.tapestry5.plastic.PropertyValueProvider;
 import org.apache.tapestry5.services.AssetSource;
 import org.apache.tapestry5.services.TransformConstants;
 import org.apache.tapestry5.services.javascript.Initialization;
@@ -32,11 +47,6 @@ import 
org.apache.tapestry5.services.javascript.JavaScriptSupport;
 import org.apache.tapestry5.services.transform.ComponentClassTransformWorker2;
 import org.apache.tapestry5.services.transform.TransformationSupport;
 
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-
 /**
  * Implements the {@link Import} annotation, both at the class and at the 
method level.
  *
@@ -154,6 +164,8 @@ public class ImportWorker implements 
ComponentClassTransformWorker2
         importStylesheets(componentClass, model, method, 
annotation.stylesheet(), fieldInfos);
 
         importModules(method, annotation.module());
+        
+        importEsModules(method, annotation.esModule());
     }
 
     private void importStacks(PlasticMethod method, String[] stacks)
@@ -179,6 +191,14 @@ public class ImportWorker implements 
ComponentClassTransformWorker2
             }
         };
     }
+    
+    private void importEsModules(PlasticMethod method, String[] moduleIds)
+    {
+        if (moduleIds.length != 0)
+        {
+            method.addAdvice(createImportEsModulesAdvice(moduleIds));
+        }
+    }
 
     private void importModules(PlasticMethod method, String[] moduleNames)
     {
@@ -208,6 +228,22 @@ public class ImportWorker implements 
ComponentClassTransformWorker2
             }
         }
     }
+    
+    private MethodAdvice createImportEsModulesAdvice(final String[] moduleIds)
+    {
+        return new MethodAdvice()
+        {
+            public void advise(MethodInvocation invocation)
+            {
+                for (String moduleId : moduleIds)
+                {
+                    javascriptSupport.importEsModule(moduleId);
+                }
+
+                invocation.proceed();
+            }
+        };        
+    }
 
     private MethodAdvice createImportModulesAdvice(final String[] moduleNames)
     {
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 4d126a3bd..ff6075d85 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..eb0e6adf9
--- /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#addEsModuleConfigurationCallback(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..f56479398
--- /dev/null
+++ 
b/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/EsModuleInitialization.java
@@ -0,0 +1,96 @@
+// 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. 
+     *
+     * @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);
+    
+}
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..19cfec30b
--- /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 head
+     *         {@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/groovy/org/apache/tapestry5/internal/services/DocumentLinkerImplTest.groovy
 
b/tapestry-core/src/test/groovy/org/apache/tapestry5/internal/services/DocumentLinkerImplTest.groovy
index 11c1b4e55..a9eafe7a7 100644
--- 
a/tapestry-core/src/test/groovy/org/apache/tapestry5/internal/services/DocumentLinkerImplTest.groovy
+++ 
b/tapestry-core/src/test/groovy/org/apache/tapestry5/internal/services/DocumentLinkerImplTest.groovy
@@ -32,7 +32,7 @@ class DocumentLinkerImplTest extends InternalBaseTestCase {
 
         document.newRootElement("not-html").text("not an HTML document")
 
-        DocumentLinkerImpl linker = new DocumentLinkerImpl(null, true, false, 
"1.2.3")
+        DocumentLinkerImpl linker = new DocumentLinkerImpl(null, null, true, 
false, "1.2.3")
 
         // Only checked if there's something to link.
 
@@ -55,7 +55,7 @@ class DocumentLinkerImplTest extends InternalBaseTestCase {
 
         document.newRootElement("not-html").text("not an HTML document")
 
-        DocumentLinkerImpl linker = new DocumentLinkerImpl(null, true, false, 
"1.2.3")
+        DocumentLinkerImpl linker = new DocumentLinkerImpl(null, null, true, 
false, "1.2.3")
 
         // Only checked if there's something to link.
 
@@ -76,7 +76,7 @@ class DocumentLinkerImplTest extends InternalBaseTestCase {
     void missing_root_element_is_a_noop() {
         Document document = new Document()
 
-        DocumentLinkerImpl linker = new DocumentLinkerImpl(null, true, false, 
"1.2.3")
+        DocumentLinkerImpl linker = new DocumentLinkerImpl(null, null, true, 
false, "1.2.3")
 
         linker.addLibrary("foo.js")
         linker.addScript(InitializationPriority.NORMAL, "doSomething();")
@@ -94,7 +94,7 @@ class DocumentLinkerImplTest extends InternalBaseTestCase {
 
         def manager = mockModuleManager(["core.js", "foo.js", "bar/baz.js"], 
[new JSONArray("t5/core/pageinit:evalJavaScript", "pageINIT();")])
 
-        DocumentLinkerImpl linker = new DocumentLinkerImpl(manager, true, 
false, "1.2.3")
+        DocumentLinkerImpl linker = new DocumentLinkerImpl(manager, null, 
true, false, "1.2.3")
 
         replay()
 
@@ -122,7 +122,7 @@ class DocumentLinkerImplTest extends InternalBaseTestCase {
 
         
document.newRootElement("html").element("body").element("p").text("Ready to be 
marked with generator meta.")
 
-        DocumentLinkerImpl linker = new DocumentLinkerImpl(null, false, false, 
"1.2.3")
+        DocumentLinkerImpl linker = new DocumentLinkerImpl(null, null, false, 
false, "1.2.3")
 
         linker.updateDocument(document)
 
@@ -141,7 +141,7 @@ class DocumentLinkerImplTest extends InternalBaseTestCase {
 
         document.newRootElement("no_html").text("Generator meta only added if 
root is html tag.")
 
-        DocumentLinkerImpl linker = new DocumentLinkerImpl(null, false, false, 
"1.2.3")
+        DocumentLinkerImpl linker = new DocumentLinkerImpl(null, null, false, 
false, "1.2.3")
 
         linker.updateDocument(document)
 
@@ -158,7 +158,7 @@ class DocumentLinkerImplTest extends InternalBaseTestCase {
 
         
document.newRootElement("html").element("body").element("p").text("Ready to be 
updated with styles.")
 
-        DocumentLinkerImpl linker = new DocumentLinkerImpl(null, true, false, 
"1.2.3")
+        DocumentLinkerImpl linker = new DocumentLinkerImpl(null, null, true, 
false, "1.2.3")
 
         linker.addStylesheetLink(new StylesheetLink("foo.css"))
         linker.addStylesheetLink(new StylesheetLink("bar/baz.css", new 
StylesheetOptions("print")))
@@ -178,7 +178,7 @@ class DocumentLinkerImplTest extends InternalBaseTestCase {
         document.newRootElement("html").element("head").comment(" existing 
head ").container.element("body").text(
             "body content")
 
-        DocumentLinkerImpl linker = new DocumentLinkerImpl(null, true, false, 
"1.2.3")
+        DocumentLinkerImpl linker = new DocumentLinkerImpl(null, null, true, 
false, "1.2.3")
 
         linker.addStylesheetLink(new StylesheetLink("foo.css"))
 
@@ -198,7 +198,7 @@ class DocumentLinkerImplTest extends InternalBaseTestCase {
 
         def manager = mockModuleManager([], [new 
JSONArray("t5/core/pageinit:evalJavaScript", "doSomething();")])
 
-        DocumentLinkerImpl linker = new DocumentLinkerImpl(manager, true, 
true, "1.2.3")
+        DocumentLinkerImpl linker = new DocumentLinkerImpl(manager, null, 
true, true, "1.2.3")
 
         replay()
 
@@ -224,7 +224,7 @@ class DocumentLinkerImplTest extends InternalBaseTestCase {
 
         def manager = mockModuleManager(["foo.js"], [])
 
-        DocumentLinkerImpl linker = new DocumentLinkerImpl(manager, true, 
false, "1.2.3")
+        DocumentLinkerImpl linker = new DocumentLinkerImpl(manager, null, 
true, false, "1.2.3")
 
         replay()
 
@@ -251,7 +251,7 @@ class DocumentLinkerImplTest extends InternalBaseTestCase {
 
         def manager = mockModuleManager([], [new 
JSONArray("['immediate/module:myfunc', {'fred':'barney'}]")])
 
-        DocumentLinkerImpl linker = new DocumentLinkerImpl(manager, true, 
false, "1.2.3")
+        DocumentLinkerImpl linker = new DocumentLinkerImpl(manager, null, 
true, false, "1.2.3")
 
         replay()
 
@@ -273,7 +273,7 @@ class DocumentLinkerImplTest extends InternalBaseTestCase {
 
         document.newRootElement("html")
 
-        DocumentLinkerImpl linker = new DocumentLinkerImpl(null, true, false, 
"1.2.3")
+        DocumentLinkerImpl linker = new DocumentLinkerImpl(null, null, true, 
false, "1.2.3")
 
         linker.addStylesheetLink(new StylesheetLink("everybody.css"))
         linker.addStylesheetLink(new StylesheetLink("just_ie.css", new 
StylesheetOptions().withCondition("IE")))
@@ -295,7 +295,7 @@ class DocumentLinkerImplTest extends InternalBaseTestCase {
 
         document.newRootElement("html")
 
-        DocumentLinkerImpl linker = new DocumentLinkerImpl(null, true, false, 
"1.2.3")
+        DocumentLinkerImpl linker = new DocumentLinkerImpl(null, null, true, 
false, "1.2.3")
 
         linker.addStylesheetLink(new StylesheetLink("whatever.css"))
         linker.addStylesheetLink(new StylesheetLink("insertion-point.css", new 
StylesheetOptions().asAjaxInsertionPoint()))
@@ -319,7 +319,7 @@ class DocumentLinkerImplTest extends InternalBaseTestCase {
             new JSONArray("my/other/module:normal", 111, 222),
             new JSONArray("my/other/module:late", 333, 444)])
 
-        DocumentLinkerImpl linker = new DocumentLinkerImpl(manager, true, 
false, "1.2.3")
+        DocumentLinkerImpl linker = new DocumentLinkerImpl(manager, null, 
true, false, "1.2.3")
 
         replay()
 
@@ -347,7 +347,7 @@ class DocumentLinkerImplTest extends InternalBaseTestCase {
         def manager = mockModuleManager([], ["my/module",
             new JSONArray("my/other/module:normal", 111, 222)])
 
-        DocumentLinkerImpl linker = new DocumentLinkerImpl(manager, true, 
false, "1.2.3")
+        DocumentLinkerImpl linker = new DocumentLinkerImpl(manager, null,  
true, false, "1.2.3")
 
         replay()
 
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..786d9ce0b
--- /dev/null
+++ 
b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/EsModuleTests.java
@@ -0,0 +1,221 @@
+// 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.annotations.Import;
+import org.apache.tapestry5.integration.app1.pages.EsModuleDemo;
+import org.apache.tapestry5.internal.transform.ImportWorker;
+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.EsModuleInitialization;
+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("outside-metainf-message"), "ES module correctly 
imported from outside /META-INF/assets/es-modules!");
+
+    }
+    
+    /**
+     * Tests importing ES modules through <code>@Import(esModule = ...)</code>.
+     * @see ImportWorker
+     * @see Import#esModule()
+     */
+    @Test
+    public void at_import_esModule() throws InterruptedException
+    {
+        openLinks(PAGE_NAME);
+        assertEquals(getText("root-folder-message"), "ES module imported 
correctly from the root folder!");
+    }
+    
+    /**
+     * Tests using {@link EsModuleInitialization#with(Object...)} without using
+     * {@link EsModuleInitialization#invoke(String)} (i.e. invoking the default
+     * exported function with at least one parameter).
+     */
+    @Test
+    public void invoking_default_exported_function() throws 
InterruptedException
+    {
+        openLinks(PAGE_NAME);
+        assertEquals(
+                getText(EsModuleDemo.DEFAULT_EXPORT_MESSAGE), 
+                EsModuleDemo.DEFAULT_EXPORT_PARAMETER);
+    }
+    
+    /**
+     * Tests using {@link EsModuleInitialization#with(Object...)} without using
+     * {@link EsModuleInitialization#invoke(String)} (i.e. invoking the default
+     * exported function). In order words,
+     * {@code javaScriptSupport.importEsModule("foo").with(...)}
+     */
+    @Test
+    public void invoking_non_default_exported_function() throws 
InterruptedException
+    {
+        openLinks(PAGE_NAME);
+        assertEquals(
+                getText(EsModuleDemo.DEFAULT_EXPORT_MESSAGE), 
+                EsModuleDemo.DEFAULT_EXPORT_PARAMETER);
+    }
+    
+    /**
+     * Tests using {@code javaScriptSupport.importEsModule("foo").with()}
+     * (i.e. invoking the default withot parameters)
+     */
+    @Test
+    public void invoking_non_default_exported_function_without_parameters() 
throws InterruptedException
+    {
+        openLinks(PAGE_NAME);
+        assertEquals(
+                getText("parameterless-default-export-message"), 
+                "Parameterless default export!");
+    }
+    
+    /**
+     * Tests using whether parameter types are correctly passed to JS.
+     */
+    @Test
+    public void parameter_types() throws InterruptedException
+    {
+        openLinks(PAGE_NAME);
+        assertEquals(
+                getText("parameter-type-default-export-message"), 
+                "Parameter types passed correctly!");
+    }
+    
+    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..49c38710d
--- /dev/null
+++ 
b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/EsModuleDemo.java
@@ -0,0 +1,107 @@
+// 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.Import;
+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.json.JSONArray;
+import org.apache.tapestry5.json.JSONObject;
+import org.apache.tapestry5.services.javascript.EsModuleConfigurationCallback;
+import org.apache.tapestry5.services.javascript.ImportPlacement;
+import org.apache.tapestry5.services.javascript.JavaScriptSupport;
+
+@Import(esModule = {"root-folder"})
+public class EsModuleDemo
+{
+    public static final String DEFAULT_EXPORT_MESSAGE = 
"default-export-message";
+
+    public static final String DEFAULT_EXPORT_PARAMETER = "Importing module 
exporting single function!";
+
+    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("outside-metainf");        
+        javaScriptSupport.importEsModule("show-import-map");
+        
+        javaScriptSupport.importEsModule("default-export")
+            .with(EsModuleDemo.DEFAULT_EXPORT_MESSAGE, 
EsModuleDemo.DEFAULT_EXPORT_PARAMETER);
+        
+        javaScriptSupport.importEsModule("non-default-export")
+            .invoke("setMessage");
+        
+        // Both .with() and .invoke() cause the function to be invoked
+        javaScriptSupport.importEsModule("parameterless-default-export")
+            .with();
+
+        javaScriptSupport.importEsModule("parameter-type-default-export")
+            .with(null, true, false, Math.PI * Math.E, "string", "jsonLiteral",
+                    new JSONObject("key", "value"), new JSONArray(1, "2"));
+
+        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 eb447d758..fcad59e0a 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/java/org/apache/tapestry5/internal/services/javascript/EsModuleManagerImplTest.java
 
b/tapestry-core/src/test/java/org/apache/tapestry5/internal/services/javascript/EsModuleManagerImplTest.java
new file mode 100644
index 000000000..0e41bc12e
--- /dev/null
+++ 
b/tapestry-core/src/test/java/org/apache/tapestry5/internal/services/javascript/EsModuleManagerImplTest.java
@@ -0,0 +1,93 @@
+// 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 static org.testng.Assert.assertEquals;
+
+import org.apache.tapestry5.json.JSONArray;
+import org.apache.tapestry5.json.JSONLiteral;
+import org.apache.tapestry5.json.JSONObject;
+import org.testng.annotations.Test;
+
+public class EsModuleManagerImplTest 
+{
+    private static final String STRING = "asdfasdfasdfadsf";
+
+    private static final JSONLiteral JSON_LITERAL = new 
JSONLiteral("literally");
+    private static final JSONArray JSON_ARRAY = new JSONArray("1", "true");
+    private static JSONObject JSON_OBJECT = new JSONObject("something", 
"else", "array",
+            JSON_ARRAY, "literal", JSON_LITERAL);
+    private static Number NUMBER = Math.PI * Math.E;
+
+    @Test
+    public void test_null_arguments()
+    {
+        assertEquals(convert(null, true), "");
+        assertEquals(convert(null, false), "");
+    }
+    
+    @Test
+    public void test_empty_arguments()
+    {
+        assertEquals(convert(new Object[0], true), "");
+        assertEquals(convert(new Object[0], false), "");
+    }
+    
+    @Test
+    public void test_one_argument()
+    {
+        assertEquals(convert(new Object[] {null}, false), null);
+        
+        assertEquals(convert(new Object[] {STRING}, false), quote(STRING));
+        
+        assertEquals(convert(new Object[] {NUMBER}, false), NUMBER.toString());
+        assertEquals(convert(new Object[] {Boolean.TRUE}, false), 
Boolean.TRUE.toString());
+        assertEquals(convert(new Object[] {Boolean.FALSE}, false), 
Boolean.FALSE.toString());
+
+        assertEquals(convert(new Object[] {JSON_LITERAL}, false), 
quote(JSON_LITERAL.toString()));
+
+        assertEquals(convert(new Object[] {JSON_ARRAY}, false), 
JSON_ARRAY.toString(false));
+        assertEquals(convert(new Object[] {JSON_ARRAY}, true), 
JSON_ARRAY.toString(true));
+
+        assertEquals(convert(new Object[] {JSON_OBJECT}, false), 
JSON_OBJECT.toString(false));
+        assertEquals(convert(new Object[] {JSON_OBJECT}, true), 
JSON_OBJECT.toString(true));
+        
+    }
+    
+    @Test
+    public void test_multiple_arguments()
+    {
+        Object[] arguments = new Object[] { null, STRING, JSON_LITERAL, 
JSON_ARRAY, JSON_OBJECT };
+        final String format = "null, '%s', '%s', %s, %s";
+        
+        assertEquals(convert(arguments, false), 
+                String.format(format, STRING, JSON_LITERAL, 
+                        JSON_ARRAY.toString(false), 
JSON_OBJECT.toString(false)));
+        
+        assertEquals(convert(arguments, true), 
+                String.format(format, STRING, JSON_LITERAL, 
+                        JSON_ARRAY.toString(true), 
JSON_OBJECT.toString(true)));
+
+    }
+    
+    private String quote(String string)
+    {
+        return "'" + string + "'";
+    }
+
+    private String convert(Object[] blah, boolean compactJSON) 
+    {
+        return EsModuleManagerImpl.convertToJsFunctionParameters(blah, 
compactJSON);
+    }
+    
+}
diff --git 
a/tapestry-core/src/test/resources/META-INF/assets/es-modules/default-export.js 
b/tapestry-core/src/test/resources/META-INF/assets/es-modules/default-export.js
new file mode 100644
index 000000000..671a2c21c
--- /dev/null
+++ 
b/tapestry-core/src/test/resources/META-INF/assets/es-modules/default-export.js
@@ -0,0 +1,3 @@
+export default function(id, message) {
+       document.getElementById(id).innerHTML = message;
+}
\ No newline at end of file
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/non-default-export.js
 
b/tapestry-core/src/test/resources/META-INF/assets/es-modules/non-default-export.js
new file mode 100644
index 000000000..f6cf336a0
--- /dev/null
+++ 
b/tapestry-core/src/test/resources/META-INF/assets/es-modules/non-default-export.js
@@ -0,0 +1,6 @@
+function setMessage() {
+       document.getElementById("non-default-export-message").innerHTML = 
+               "Non-default exported function!";
+}
+
+export { setMessage };
\ No newline at end of file
diff --git 
a/tapestry-core/src/test/resources/META-INF/assets/es-modules/parameter-type-default-export.js
 
b/tapestry-core/src/test/resources/META-INF/assets/es-modules/parameter-type-default-export.js
new file mode 100644
index 000000000..e4c3272e6
--- /dev/null
+++ 
b/tapestry-core/src/test/resources/META-INF/assets/es-modules/parameter-type-default-export.js
@@ -0,0 +1,14 @@
+export default function(nullValue, trueValue, falseValue, piTimesE, 
stringValue, jsonLiteralValue,
+       objectValue, arrayValue) {
+               
+       if (nullValue === null && (typeof trueValue === "boolean") && trueValue 
=== true &&
+               (typeof falseValue === "boolean") && falseValue === false &&
+               (typeof piTimesE === "number") && piTimesE === Math.PI * Math.E 
&&
+               (typeof jsonLiteralValue === "string") && jsonLiteralValue === 
"jsonLiteral" &&
+               (typeof objectValue === "object") && objectValue.key === 
"value" &&
+               arrayValue.constructor === Array && arrayValue[0] === 1 && 
arrayValue[1] === "2") {
+                       
+               
document.getElementById("parameter-type-default-export-message").innerHTML = 
"Parameter types passed correctly!";
+               
+       }
+}
\ No newline at end of file
diff --git 
a/tapestry-core/src/test/resources/META-INF/assets/es-modules/parameterless-default-export.js
 
b/tapestry-core/src/test/resources/META-INF/assets/es-modules/parameterless-default-export.js
new file mode 100644
index 000000000..32d1a2c0e
--- /dev/null
+++ 
b/tapestry-core/src/test/resources/META-INF/assets/es-modules/parameterless-default-export.js
@@ -0,0 +1,3 @@
+export default function() {
+       
document.getElementById("parameterless-default-export-message").innerHTML = 
"Parameterless default export!";
+}
\ 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..702834fb8
--- /dev/null
+++ 
b/tapestry-core/src/test/resources/org/apache/tapestry5/integration/app1/pages/EsModuleDemo.tml
@@ -0,0 +1,34 @@
+<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 id="${DEFAULT_EXPORT_MESSAGE}"/>
+       <p id="non-default-export-message"/>
+       <p id="parameterless-default-export-message"/>
+       <p id="parameter-type-default-export-message"/>
+       
+       <p>
+               Import map:
+       </p>
+       <pre id="import-map-listing"/>
+       
+       <p id="last-body-element"></p>
+       
+</html>
\ No newline at end of file
diff --git 
a/tapestry-ioc/src/main/java/org/apache/tapestry5/ioc/internal/util/URLChangeTracker.java
 
b/tapestry-ioc/src/main/java/org/apache/tapestry5/ioc/internal/util/URLChangeTracker.java
index 4baafe32c..af365db75 100644
--- 
a/tapestry-ioc/src/main/java/org/apache/tapestry5/ioc/internal/util/URLChangeTracker.java
+++ 
b/tapestry-ioc/src/main/java/org/apache/tapestry5/ioc/internal/util/URLChangeTracker.java
@@ -18,7 +18,10 @@ import java.io.File;
 import java.io.IOException;
 import java.net.URISyntaxException;
 import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
@@ -314,6 +317,24 @@ public class URLChangeTracker<T>
         return fileToTimestamp.size();
     }
     
+    public String toString()
+    {
+        StringBuilder builder = new StringBuilder();
+
+        final List<File> files = new ArrayList<>(fileToTimestamp.keySet());
+        Collections.sort(files, (f1, f2) -> 
f1.getName().compareTo(f2.getName()));
+        
+        for (File file : files)
+        {
+            builder.append(file.getName());
+            builder.append(": ");
+            builder.append(fileToTimestamp.get(file));
+            builder.append("\n");
+        }
+        
+        return builder.toString();
+    }
+    
     private final class TrackingInfo
     {
         

Reply via email to