This is an automated email from the ASF dual-hosted git repository.
thiagohp pushed a commit to branch javax
in repository https://gitbox.apache.org/repos/asf/tapestry-5.git
The following commit(s) were added to refs/heads/javax by this push:
new 209680b59 TAP5-2803: foundation work for ES module support
209680b59 is described below
commit 209680b59a1f361abd8d46e9c28da355af7daccc
Author: Thiago H. de Paula Figueiredo <[email protected]>
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 dc19b2756..51a304c2c 100644
--- a/tapestry-core/src/main/java/org/apache/tapestry5/SymbolConstants.java
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/SymbolConstants.java
@@ -376,7 +376,7 @@ public class SymbolConstants
/**
- * Prefix used for all module resources. This may contain slashes, but
should not being or end with one.
+ * Prefix used for all Require.js module resources. This may contain
slashes, but should not being or end with one.
* Tapestry will create two {@link
org.apache.tapestry5.http.services.Dispatcher}s from this: one for normal
* modules, the other for GZip compressed modules (by appending ".gz" to
this value).
*
@@ -385,6 +385,18 @@ public class SymbolConstants
* @since 5.4
*/
public static final String MODULE_PATH_PREFIX =
"tapestry.module-path-prefix";
+
+ /**
+ * Prefix used for automatically configured ES module resources.
+ * This may contain slashes, but should not being or end with one.
+ *
+ * The default is "es-modules".
+ *
+ * TODO remove
+ *
+ * @since 5.4
+ */
+ public static final String ES_MODULE_PATH_PREFIX =
"tapestry.es-module-path-prefix";
/**
* Identifies the context path of the application, as determined from
{@link javax.servlet.ServletContext#getContextPath()}.
diff --git
a/tapestry-core/src/main/java/org/apache/tapestry5/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 e3c47e86f..7f339ee65 100644
---
a/tapestry-core/src/main/java/org/apache/tapestry5/modules/TapestryModule.java
+++
b/tapestry-core/src/main/java/org/apache/tapestry5/modules/TapestryModule.java
@@ -20,19 +20,15 @@ import java.math.BigInteger;
import java.net.URL;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
-import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
-import java.util.Collections;
import java.util.Date;
-import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.regex.Pattern;
-import java.util.stream.Collectors;
import org.apache.tapestry5.Asset;
import org.apache.tapestry5.BindingConstants;
@@ -108,12 +104,7 @@ import org.apache.tapestry5.commons.services.TypeCoercer;
import org.apache.tapestry5.commons.util.AvailableValues;
import org.apache.tapestry5.commons.util.CollectionFactory;
import org.apache.tapestry5.commons.util.StrategyRegistry;
-import org.apache.tapestry5.corelib.components.BeanEditor;
-import org.apache.tapestry5.corelib.components.PropertyDisplay;
-import org.apache.tapestry5.corelib.components.PropertyEditor;
import org.apache.tapestry5.corelib.data.SecureOption;
-import org.apache.tapestry5.corelib.pages.PropertyDisplayBlocks;
-import org.apache.tapestry5.corelib.pages.PropertyEditBlocks;
import org.apache.tapestry5.grid.GridConstants;
import org.apache.tapestry5.grid.GridDataSource;
import org.apache.tapestry5.http.Link;
@@ -174,7 +165,6 @@ import org.apache.tapestry5.internal.services.*;
import org.apache.tapestry5.internal.services.ajax.AjaxFormUpdateFilter;
import org.apache.tapestry5.internal.services.ajax.AjaxResponseRendererImpl;
import
org.apache.tapestry5.internal.services.ajax.MultiZoneUpdateEventResultProcessor;
-import org.apache.tapestry5.internal.services.assets.ResourceChangeTracker;
import
org.apache.tapestry5.internal.services.exceptions.ExceptionReportWriterImpl;
import org.apache.tapestry5.internal.services.exceptions.ExceptionReporterImpl;
import
org.apache.tapestry5.internal.services.linktransform.LinkTransformerImpl;
@@ -365,6 +355,7 @@ import org.apache.tapestry5.services.ValueLabelProvider;
import org.apache.tapestry5.services.ajax.AjaxResponseRenderer;
import org.apache.tapestry5.services.dynamic.DynamicTemplate;
import org.apache.tapestry5.services.dynamic.DynamicTemplateParser;
+import org.apache.tapestry5.services.javascript.EsModuleManager;
import org.apache.tapestry5.services.javascript.JavaScriptSupport;
import org.apache.tapestry5.services.javascript.ModuleManager;
import
org.apache.tapestry5.services.linktransform.ComponentEventLinkTransformer;
@@ -376,7 +367,6 @@ import org.apache.tapestry5.services.meta.FixedExtractor;
import org.apache.tapestry5.services.meta.MetaDataExtractor;
import org.apache.tapestry5.services.meta.MetaWorker;
import org.apache.tapestry5.services.pageload.PageClassLoaderContextManager;
-import
org.apache.tapestry5.services.pageload.PageClassLoaderContextManagerImpl;
import org.apache.tapestry5.services.pageload.PreloaderMode;
import org.apache.tapestry5.services.rest.MappedEntityManager;
import org.apache.tapestry5.services.rest.OpenApiDescriptionGenerator;
@@ -1803,6 +1793,8 @@ public final class TapestryModule
public void
contributeMarkupRenderer(OrderedConfiguration<MarkupRendererFilter>
configuration,
final ModuleManager moduleManager,
+
+ final EsModuleManager esModuleManager,
@Symbol(SymbolConstants.OMIT_GENERATOR_META)
final boolean omitGeneratorMeta,
@@ -1825,7 +1817,7 @@ public final class TapestryModule
{
public void renderMarkup(MarkupWriter writer, MarkupRenderer
renderer)
{
- DocumentLinkerImpl linker = new
DocumentLinkerImpl(moduleManager, omitGeneratorMeta, enablePageloadingMask,
tapestryVersion);
+ DocumentLinkerImpl linker = new
DocumentLinkerImpl(moduleManager, esModuleManager, omitGeneratorMeta,
enablePageloadingMask, tapestryVersion);
environment.push(DocumentLinker.class, linker);
diff --git
a/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/Initialization.java
b/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/AbstractInitialization.java
similarity index 72%
copy from
tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/Initialization.java
copy to
tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/AbstractInitialization.java
index a633722fc..6aa58d866 100644
---
a/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/Initialization.java
+++
b/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/AbstractInitialization.java
@@ -13,12 +13,11 @@
package org.apache.tapestry5.services.javascript;
/**
- * Provided by {@link JavaScriptSupport#require(String)} to allow additional,
optional, details of the module-based page initialization
- * to be configured.
+ * Superinterface with the parts shared by {@linkplain Initialization} and
{@linkplain EsModuleInitialization}.
*
- * @since 5.4
+ * @since 5.10.0
*/
-public interface Initialization
+public interface AbstractInitialization<T extends AbstractInitialization<?>>
{
/**
@@ -29,18 +28,7 @@ public interface Initialization
* name of a function exported by the module.
* @return this Initialization, for further configuration
*/
- Initialization invoke(String functionName);
-
- /**
- * Changes the initialization priority of the initialization from its
default, {@link InitializationPriority#NORMAL}.
- *
- * Note: it is possible that this method may be removed before release 5.4
is final.
- *
- * @param priority
- * new priority
- * @return this Initialization, for further configuration
- */
- Initialization priority(InitializationPriority priority);
+ T invoke(String functionName);
/**
* Specifies the arguments to be passed to the function. Often, just a
single {@link org.apache.tapestry5.json.JSONObject}
diff --git
a/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/EsModuleConfigurationCallback.java
b/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/EsModuleConfigurationCallback.java
new file mode 100644
index 000000000..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><script></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><head></code> HTML element.
+ */
+ HEAD,
+
+ /**
+ * Towards the top of the <code><body></code> HTML element.
+ */
+ BODY_TOP,
+
+ /**
+ * Towards the bottom of the <code><body></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 025b7eb07..cbbc34c5d 100644
---
a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/Index.java
+++
b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/Index.java
@@ -630,7 +630,9 @@ public class Index
new Item("RecursiveDemo","Recursive Demo","Recursive
component example"),
- new Item("SelfRecursiveDemo", "Self-Recursive Demo",
"check for handling of self-recursive components")
+ new Item("SelfRecursiveDemo", "Self-Recursive Demo",
"check for handling of self-recursive components"),
+
+ new Item("EsModuleDemo", "ES Module Demo", "tests and
demonstrations for the ES module support")
);
static
diff --git
a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/services/AppModule.java
b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/services/AppModule.java
index 6801928b8..e8b91f4d0 100644
---
a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/services/AppModule.java
+++
b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/services/AppModule.java
@@ -50,6 +50,7 @@ import org.apache.tapestry5.ioc.annotations.Contribute;
import org.apache.tapestry5.ioc.annotations.Marker;
import org.apache.tapestry5.ioc.annotations.Value;
import org.apache.tapestry5.ioc.services.ServiceOverride;
+import org.apache.tapestry5.services.AssetSource;
import org.apache.tapestry5.services.BeanBlockContribution;
import org.apache.tapestry5.services.BeanBlockSource;
import org.apache.tapestry5.services.ComponentClassResolver;
@@ -58,6 +59,7 @@ import org.apache.tapestry5.services.LibraryMapping;
import org.apache.tapestry5.services.ResourceDigestGenerator;
import org.apache.tapestry5.services.ValueEncoderFactory;
import org.apache.tapestry5.services.ValueLabelProvider;
+import org.apache.tapestry5.services.javascript.EsModuleConfigurationCallback;
import org.apache.tapestry5.services.pageload.PageCachingReferenceTypeService;
import org.apache.tapestry5.services.pageload.PagePreloader;
import org.apache.tapestry5.services.pageload.PreloaderMode;
@@ -476,5 +478,43 @@ public class AppModule
}
}
+
+ public static final String NON_OVERRIDDEN_ES_MODULE_ID = "nonOverriden";
+
+ public static final String NON_OVERRIDDEN_ES_MODULE_URL =
"/nonOverridenURL";
+
+ public static final String OVERRIDDEN_ES_MODULE_ID = "overriden";
+
+ public static final String OVERRIDDEN_ES_MODULE_ORIGINAL_URL =
"/originalURL";
+
+ public static final String OVERRIDDEN_ES_MODULE_NEW_URL = "/overridenURL";
+
+ public static void contributeEsModuleManager(
+ OrderedConfiguration<EsModuleConfigurationCallback> configuration,
+ AssetSource assetSource)
+ {
+ final String original = "OriginalCallback";
+ final String override = "OverrideCallback";
+
+ configuration.add(override,
+ o -> EsModuleConfigurationCallback.setImport(o,
OVERRIDDEN_ES_MODULE_ID, OVERRIDDEN_ES_MODULE_NEW_URL),
+ "after:" + original);
+
+ configuration.add(original,
+ o -> {
+ EsModuleConfigurationCallback.setImport(o,
NON_OVERRIDDEN_ES_MODULE_ID, NON_OVERRIDDEN_ES_MODULE_URL);
+ EsModuleConfigurationCallback.setImport(o,
OVERRIDDEN_ES_MODULE_ID, OVERRIDDEN_ES_MODULE_ORIGINAL_URL);
+ });
+
+ configuration.add("Outside META-INF", o ->
+ EsModuleConfigurationCallback.setImport(o, "outside-metainf",
+
assetSource.getClasspathAsset("/org/apache/tapestry5/integration/app1/es-module-outside-metainf.js").toClientURL())
+ );
+
+ configuration.add("External URL", o ->
+ EsModuleConfigurationCallback.setImport(o, "external/url",
"https://example.com/module.js")
+ );
+
+ }
}
diff --git
a/tapestry-core/src/test/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 (<body> 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 (<body> 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 (<head>)!";
\ 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
{