From 334e2829b3a7b351e2bdcbec7e62cfaea1158620 Mon Sep 17 00:00:00 2001 From: Alexey Andreev Date: Tue, 1 Aug 2023 20:56:58 +0200 Subject: [PATCH] JS: supports module imports in JSBody --- .../backend/javascript/JavaScriptTarget.java | 132 ++++++++++++++++-- .../backend/javascript/ProviderContext.java | 3 +- .../javascript/TeaVMJavaScriptHost.java | 3 + .../javascript/rendering/Renderer.java | 5 + .../rendering/RenderingContext.java | 4 +- .../rendering/StatementRenderer.java | 5 + .../javascript/spi/GeneratorContext.java | 2 + .../javascript/spi/InjectorContext.java | 2 + .../javascript/spi/ModuleImporter.java | 20 +++ .../javascript/spi/ModuleImporterContext.java | 36 +++++ .../model/analysis/BaseTypeInference.java | 2 +- .../src/main/java/org/teavm/jso/JSBody.java | 2 + .../main/java/org/teavm/jso/JSBodyImport.java | 26 ++++ .../main/java/org/teavm/jso/impl/Imports.java | 27 ++++ .../org/teavm/jso/impl/JSBodyAstEmitter.java | 17 ++- .../teavm/jso/impl/JSBodyBloatedEmitter.java | 69 ++++++++- .../org/teavm/jso/impl/JSBodyRepository.java | 13 +- .../org/teavm/jso/impl/JSClassProcessor.java | 22 ++- .../java/org/teavm/jso/impl/JSOPlugin.java | 5 + .../org/teavm/jso/impl/JsBodyImportInfo.java | 26 ++++ .../jso/impl/JsBodyImportsContributor.java | 34 +++++ .../org/teavm/jso/test/ImportModuleTest.java | 52 +++++++ .../test/resources/org/teavm/jso/test/amd.js | 47 +++++++ .../resources/org/teavm/jso/test/amdModule.js | 23 +++ .../resources/org/teavm/jso/test/commonjs.js | 36 +++++ .../org/teavm/junit/AttachJavaScript.java | 27 ++++ .../org/teavm/junit/BrowserRunStrategy.java | 64 +++++++-- .../src/main/resources/test-server/frame.js | 3 +- 28 files changed, 663 insertions(+), 44 deletions(-) create mode 100644 core/src/main/java/org/teavm/backend/javascript/spi/ModuleImporter.java create mode 100644 core/src/main/java/org/teavm/backend/javascript/spi/ModuleImporterContext.java create mode 100644 jso/core/src/main/java/org/teavm/jso/JSBodyImport.java create mode 100644 jso/impl/src/main/java/org/teavm/jso/impl/Imports.java create mode 100644 jso/impl/src/main/java/org/teavm/jso/impl/JsBodyImportInfo.java create mode 100644 jso/impl/src/main/java/org/teavm/jso/impl/JsBodyImportsContributor.java create mode 100644 tests/src/test/java/org/teavm/jso/test/ImportModuleTest.java create mode 100644 tests/src/test/resources/org/teavm/jso/test/amd.js create mode 100644 tests/src/test/resources/org/teavm/jso/test/amdModule.js create mode 100644 tests/src/test/resources/org/teavm/jso/test/commonjs.js create mode 100644 tools/junit/src/main/java/org/teavm/junit/AttachJavaScript.java diff --git a/core/src/main/java/org/teavm/backend/javascript/JavaScriptTarget.java b/core/src/main/java/org/teavm/backend/javascript/JavaScriptTarget.java index f7c66026e..99aa1180c 100644 --- a/core/src/main/java/org/teavm/backend/javascript/JavaScriptTarget.java +++ b/core/src/main/java/org/teavm/backend/javascript/JavaScriptTarget.java @@ -30,8 +30,10 @@ import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Properties; import java.util.Set; import java.util.function.Function; import org.teavm.ast.AsyncMethodNode; @@ -50,11 +52,14 @@ import org.teavm.backend.javascript.decompile.PreparedClass; import org.teavm.backend.javascript.decompile.PreparedMethod; import org.teavm.backend.javascript.rendering.Renderer; import org.teavm.backend.javascript.rendering.RenderingContext; +import org.teavm.backend.javascript.rendering.RenderingUtil; import org.teavm.backend.javascript.rendering.RuntimeRenderer; import org.teavm.backend.javascript.spi.GeneratedBy; import org.teavm.backend.javascript.spi.Generator; import org.teavm.backend.javascript.spi.InjectedBy; import org.teavm.backend.javascript.spi.Injector; +import org.teavm.backend.javascript.spi.ModuleImporter; +import org.teavm.backend.javascript.spi.ModuleImporterContext; import org.teavm.backend.javascript.spi.VirtualMethodContributor; import org.teavm.backend.javascript.spi.VirtualMethodContributorContext; import org.teavm.cache.AstCacheEntry; @@ -68,6 +73,7 @@ import org.teavm.debugging.information.SourceLocation; import org.teavm.dependency.AbstractDependencyListener; import org.teavm.dependency.DependencyAgent; import org.teavm.dependency.DependencyAnalyzer; +import org.teavm.dependency.DependencyInfo; import org.teavm.dependency.DependencyListener; import org.teavm.dependency.DependencyType; import org.teavm.dependency.MethodDependency; @@ -83,6 +89,7 @@ import org.teavm.model.ClassReaderSource; import org.teavm.model.ElementModifier; import org.teavm.model.FieldReference; import org.teavm.model.ListableClassHolderSource; +import org.teavm.model.ListableClassReaderSource; import org.teavm.model.MethodHolder; import org.teavm.model.MethodReader; import org.teavm.model.MethodReference; @@ -120,6 +127,7 @@ public class JavaScriptTarget implements TeaVMTarget, TeaVMJavaScriptHost { private final Map methodInjectors = new HashMap<>(); private final List> generatorProviders = new ArrayList<>(); private final List> injectorProviders = new ArrayList<>(); + private final List> moduleImporterProviders = new ArrayList<>(); private final List rendererListeners = new ArrayList<>(); private DebugInformationEmitter debugEmitter; private MethodNodeCache astCache = EmptyMethodNodeCache.INSTANCE; @@ -131,6 +139,7 @@ public class JavaScriptTarget implements TeaVMTarget, TeaVMJavaScriptHost { private boolean strict; private BoundCheckInsertion boundCheckInsertion = new BoundCheckInsertion(); private NullCheckInsertion nullCheckInsertion = new NullCheckInsertion(NullCheckFilter.EMPTY); + private final Map importedModules = new LinkedHashMap<>(); @Override public List getTransformers() { @@ -172,6 +181,11 @@ public class JavaScriptTarget implements TeaVMTarget, TeaVMJavaScriptHost { injectorProviders.add(provider); } + @Override + public void addModuleImporterProvider(Function provider) { + moduleImporterProviders.add(provider); + } + /** * Specifies whether this TeaVM instance uses obfuscation when generating the JavaScript code. * @@ -361,6 +375,7 @@ public class JavaScriptTarget implements TeaVMTarget, TeaVMJavaScriptHost { private void emit(ListableClassHolderSource classes, Writer writer, BuildTarget target) { List clsNodes = modelToAst(classes); + prepareModules(classes); if (controller.wasCancelled()) { return; } @@ -382,7 +397,13 @@ public class JavaScriptTarget implements TeaVMTarget, TeaVMJavaScriptHost { controller.getUnprocessedClassSource(), classes, controller.getClassLoader(), controller.getServices(), controller.getProperties(), naming, controller.getDependencyInfo(), m -> isVirtual(virtualMethodContributorContext, m), - controller.getClassInitializerInfo(), strict); + controller.getClassInitializerInfo(), strict + ) { + @Override + public String importModule(String name) { + return JavaScriptTarget.this.importModule(name); + } + }; renderingContext.setMinifying(obfuscated); Renderer renderer = new Renderer(sourceWriter, asyncMethods, asyncFamilyMethods, controller.getDiagnostics(), renderingContext); @@ -404,7 +425,7 @@ public class JavaScriptTarget implements TeaVMTarget, TeaVMJavaScriptHost { renderer.setDebugEmitter(debugEmitter); } renderer.getDebugEmitter().setLocationProvider(sourceWriter); - for (Map.Entry entry : methodInjectors.entrySet()) { + for (var entry : methodInjectors.entrySet()) { renderingContext.addInjector(entry.getKey(), entry.getValue()); } try { @@ -462,21 +483,45 @@ public class JavaScriptTarget implements TeaVMTarget, TeaVMJavaScriptHost { private void printWrapperStart(SourceWriter writer) throws IOException { writer.append("\"use strict\";").newLine(); printUmdStart(writer); - writer.append("function($rt_globals,").ws().append("$rt_exports)").appendBlockStart(); + writer.append("function($rt_globals,").ws().append("$rt_exports"); + for (var moduleName : importedModules.values()) { + writer.append(",").ws().appendFunction(moduleName); + } + writer.append(")").appendBlockStart(); + } + + private String importModule(String name) { + return importedModules.get(name); } private void printUmdStart(SourceWriter writer) throws IOException { writer.append("(function(root,").ws().append("module)").appendBlockStart(); writer.appendIf().append("typeof define").ws().append("===").ws().append("'function'") .ws().append("&&").ws().append("define.amd)").appendBlockStart(); - writer.append("define(['exports'],").ws().append("function(exports)").ws().appendBlockStart(); - writer.append("module(root,").ws().append("exports);").softNewLine(); + writer.append("define(['exports'"); + for (var moduleName : importedModules.keySet()) { + writer.append(',').ws().append('"').append(RenderingUtil.escapeString(moduleName)).append('"'); + } + writer.append("],").ws().append("function(exports"); + for (var moduleAlias : importedModules.values()) { + writer.append(',').ws().appendFunction(moduleAlias); + } + writer.append(")").ws().appendBlockStart(); + writer.append("module(root,").ws().append("exports"); + for (var moduleAlias : importedModules.values()) { + writer.append(',').ws().appendFunction(moduleAlias); + } + writer.append(");").softNewLine(); writer.outdent().append("});").softNewLine(); writer.appendElseIf().append("typeof exports").ws() .append("===").ws().append("'object'").ws().append("&&").ws() .append("typeof exports.nodeName").ws().append("!==").ws().append("'string')").appendBlockStart(); - writer.append("module(global,").ws().append("exports);").softNewLine(); + writer.append("module(global,").ws().append("exports"); + for (var moduleName : importedModules.keySet()) { + writer.append(',').ws().append("require(\"").append(RenderingUtil.escapeString(moduleName)).append("\")"); + } + writer.append(");").softNewLine(); writer.appendElse(); writer.append("module(root,").ws().append("root);").softNewLine(); @@ -519,6 +564,28 @@ public class JavaScriptTarget implements TeaVMTarget, TeaVMJavaScriptHost { return STATS_NUM_FORMAT.format(size) + " (" + STATS_PERCENT_FORMAT.format((double) size / totalSize) + ")"; } + private void prepareModules(ListableClassHolderSource classes) { + var context = new ImporterContext(classes); + for (var className : classes.getClassNames()) { + var cls = classes.get(className); + for (var method : cls.getMethods()) { + if (method.getModifiers().contains(ElementModifier.ABSTRACT)) { + continue; + } + + var providerContext = new ProviderContextImpl(method.getReference()); + for (var provider : moduleImporterProviders) { + var importer = provider.apply(providerContext); + if (importer != null) { + context.method = method; + importer.importModules(context); + context.method = null; + } + } + } + } + } + private List modelToAst(ListableClassHolderSource classes) { AsyncMethodFinder asyncFinder = new AsyncMethodFinder(controller.getDependencyInfo().getCallGraph(), controller.getDependencyInfo()); @@ -676,7 +743,7 @@ public class JavaScriptTarget implements TeaVMTarget, TeaVMJavaScriptHost { boolean found = false; ProviderContext context = new ProviderContextImpl(method.getReference()); - for (Function provider : generatorProviders) { + for (var provider : generatorProviders) { Generator generator = provider.apply(context); if (generator != null) { methodGenerators.put(method.getReference(), generator); @@ -684,7 +751,7 @@ public class JavaScriptTarget implements TeaVMTarget, TeaVMJavaScriptHost { break; } } - for (Function provider : injectorProviders) { + for (var provider : injectorProviders) { Injector injector = provider.apply(context); if (injector != null) { methodInjectors.put(method.getReference(), injector); @@ -756,6 +823,11 @@ public class JavaScriptTarget implements TeaVMTarget, TeaVMJavaScriptHost { public ClassReaderSource getClassSource() { return controller.getUnprocessedClassSource(); } + + @Override + public T getService(Class type) { + return controller.getServices().getService(type); + } } @PlatformMarker @@ -820,4 +892,48 @@ public class JavaScriptTarget implements TeaVMTarget, TeaVMJavaScriptHost { return classSource; } } + + class ImporterContext implements ModuleImporterContext { + private ListableClassReaderSource classSource; + MethodReader method; + + ImporterContext(ListableClassReaderSource classSource) { + this.classSource = classSource; + } + + @Override + public MethodReader getMethod() { + return method; + } + + @Override + public void importModule(String name) { + importedModules.computeIfAbsent(name, n -> "$rt_import_" + importedModules.size()); + } + + @Override + public ListableClassReaderSource getClassSource() { + return classSource; + } + + @Override + public ClassLoader getClassLoader() { + return controller.getClassLoader(); + } + + @Override + public Properties getProperties() { + return JavaScriptTarget.this.controller.getProperties(); + } + + @Override + public DependencyInfo getDependency() { + return controller.getDependencyInfo(); + } + + @Override + public T getService(Class type) { + return controller.getServices().getService(type); + } + } } diff --git a/core/src/main/java/org/teavm/backend/javascript/ProviderContext.java b/core/src/main/java/org/teavm/backend/javascript/ProviderContext.java index 4712fa8ae..aa8eeba01 100644 --- a/core/src/main/java/org/teavm/backend/javascript/ProviderContext.java +++ b/core/src/main/java/org/teavm/backend/javascript/ProviderContext.java @@ -15,10 +15,11 @@ */ package org.teavm.backend.javascript; +import org.teavm.common.ServiceRepository; import org.teavm.model.ClassReaderSource; import org.teavm.model.MethodReference; -public interface ProviderContext { +public interface ProviderContext extends ServiceRepository { MethodReference getMethod(); ClassReaderSource getClassSource(); diff --git a/core/src/main/java/org/teavm/backend/javascript/TeaVMJavaScriptHost.java b/core/src/main/java/org/teavm/backend/javascript/TeaVMJavaScriptHost.java index d7684fee5..381f1e686 100644 --- a/core/src/main/java/org/teavm/backend/javascript/TeaVMJavaScriptHost.java +++ b/core/src/main/java/org/teavm/backend/javascript/TeaVMJavaScriptHost.java @@ -18,6 +18,7 @@ package org.teavm.backend.javascript; import java.util.function.Function; import org.teavm.backend.javascript.spi.Generator; import org.teavm.backend.javascript.spi.Injector; +import org.teavm.backend.javascript.spi.ModuleImporter; import org.teavm.backend.javascript.spi.VirtualMethodContributor; import org.teavm.model.MethodReference; import org.teavm.vm.spi.RendererListener; @@ -32,6 +33,8 @@ public interface TeaVMJavaScriptHost extends TeaVMHostExtension { void addInjectorProvider(Function provider); + void addModuleImporterProvider(Function provider); + void add(RendererListener listener); void addVirtualMethods(VirtualMethodContributor virtualMethods); diff --git a/core/src/main/java/org/teavm/backend/javascript/rendering/Renderer.java b/core/src/main/java/org/teavm/backend/javascript/rendering/Renderer.java index f2cc4e754..1e45d4632 100644 --- a/core/src/main/java/org/teavm/backend/javascript/rendering/Renderer.java +++ b/core/src/main/java/org/teavm/backend/javascript/rendering/Renderer.java @@ -1212,6 +1212,11 @@ public class Renderer implements RenderingManager { public boolean isDynamicInitializer(String className) { return context.isDynamicInitializer(className); } + + @Override + public String importModule(String name) { + return context.importModule(name); + } } private void appendMonitor(StatementRenderer statementRenderer, MethodNode methodNode) throws IOException { diff --git a/core/src/main/java/org/teavm/backend/javascript/rendering/RenderingContext.java b/core/src/main/java/org/teavm/backend/javascript/rendering/RenderingContext.java index 17b8dbc62..f82751ef5 100644 --- a/core/src/main/java/org/teavm/backend/javascript/rendering/RenderingContext.java +++ b/core/src/main/java/org/teavm/backend/javascript/rendering/RenderingContext.java @@ -47,7 +47,7 @@ import org.teavm.model.TextLocation; import org.teavm.model.ValueType; import org.teavm.model.analysis.ClassInitializerInfo; -public class RenderingContext { +public abstract class RenderingContext { private final DebugInformationEmitter debugEmitter; private ClassReaderSource initialClassSource; private ListableClassReaderSource classSource; @@ -406,6 +406,8 @@ public class RenderingContext { return strict; } + public abstract String importModule(String name); + @PlatformMarker private static boolean isBootstrap() { return false; diff --git a/core/src/main/java/org/teavm/backend/javascript/rendering/StatementRenderer.java b/core/src/main/java/org/teavm/backend/javascript/rendering/StatementRenderer.java index eb87f6d67..0f65b6c2e 100644 --- a/core/src/main/java/org/teavm/backend/javascript/rendering/StatementRenderer.java +++ b/core/src/main/java/org/teavm/backend/javascript/rendering/StatementRenderer.java @@ -1747,5 +1747,10 @@ public class StatementRenderer implements ExprVisitor, StatementVisitor { public ListableClassReaderSource getClassSource() { return context.getClassSource(); } + + @Override + public String importModule(String name) { + return context.importModule(name); + } } } diff --git a/core/src/main/java/org/teavm/backend/javascript/spi/GeneratorContext.java b/core/src/main/java/org/teavm/backend/javascript/spi/GeneratorContext.java index fb612fde5..256a1b394 100644 --- a/core/src/main/java/org/teavm/backend/javascript/spi/GeneratorContext.java +++ b/core/src/main/java/org/teavm/backend/javascript/spi/GeneratorContext.java @@ -28,6 +28,8 @@ import org.teavm.model.ValueType; public interface GeneratorContext extends ServiceRepository { String getParameterName(int index); + String importModule(String name); + ClassReaderSource getInitialClassSource(); ListableClassReaderSource getClassSource(); diff --git a/core/src/main/java/org/teavm/backend/javascript/spi/InjectorContext.java b/core/src/main/java/org/teavm/backend/javascript/spi/InjectorContext.java index 16f318fb0..8facb3ec9 100644 --- a/core/src/main/java/org/teavm/backend/javascript/spi/InjectorContext.java +++ b/core/src/main/java/org/teavm/backend/javascript/spi/InjectorContext.java @@ -27,6 +27,8 @@ import org.teavm.model.ValueType; public interface InjectorContext extends ServiceRepository { Expr getArgument(int index); + String importModule(String name); + int argumentCount(); boolean isMinifying(); diff --git a/core/src/main/java/org/teavm/backend/javascript/spi/ModuleImporter.java b/core/src/main/java/org/teavm/backend/javascript/spi/ModuleImporter.java new file mode 100644 index 000000000..a478e6447 --- /dev/null +++ b/core/src/main/java/org/teavm/backend/javascript/spi/ModuleImporter.java @@ -0,0 +1,20 @@ +/* + * Copyright 2023 konsoletyper. + * + * 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.teavm.backend.javascript.spi; + +public interface ModuleImporter { + void importModules(ModuleImporterContext context); +} diff --git a/core/src/main/java/org/teavm/backend/javascript/spi/ModuleImporterContext.java b/core/src/main/java/org/teavm/backend/javascript/spi/ModuleImporterContext.java new file mode 100644 index 000000000..bab706e59 --- /dev/null +++ b/core/src/main/java/org/teavm/backend/javascript/spi/ModuleImporterContext.java @@ -0,0 +1,36 @@ +/* + * Copyright 2023 konsoletyper. + * + * 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.teavm.backend.javascript.spi; + +import java.util.Properties; +import org.teavm.common.ServiceRepository; +import org.teavm.dependency.DependencyInfo; +import org.teavm.model.ListableClassReaderSource; +import org.teavm.model.MethodReader; + +public interface ModuleImporterContext extends ServiceRepository { + MethodReader getMethod(); + + void importModule(String name); + + ListableClassReaderSource getClassSource(); + + ClassLoader getClassLoader(); + + Properties getProperties(); + + DependencyInfo getDependency(); +} diff --git a/core/src/main/java/org/teavm/model/analysis/BaseTypeInference.java b/core/src/main/java/org/teavm/model/analysis/BaseTypeInference.java index 744040ad0..0684b4d39 100644 --- a/core/src/main/java/org/teavm/model/analysis/BaseTypeInference.java +++ b/core/src/main/java/org/teavm/model/analysis/BaseTypeInference.java @@ -106,7 +106,7 @@ public abstract class BaseTypeInference { continue; } type = doMerge(type, formerType); - if (Objects.equals(type, formerType)) { + if (Objects.equals(type, formerType) || type == null) { continue; } types[variable] = type; diff --git a/jso/core/src/main/java/org/teavm/jso/JSBody.java b/jso/core/src/main/java/org/teavm/jso/JSBody.java index 862e91485..5df77b27a 100644 --- a/jso/core/src/main/java/org/teavm/jso/JSBody.java +++ b/jso/core/src/main/java/org/teavm/jso/JSBody.java @@ -133,4 +133,6 @@ public @interface JSBody { *

JavaScript code.

*/ String script(); + + JSBodyImport[] imports() default {}; } diff --git a/jso/core/src/main/java/org/teavm/jso/JSBodyImport.java b/jso/core/src/main/java/org/teavm/jso/JSBodyImport.java new file mode 100644 index 000000000..af8aaa558 --- /dev/null +++ b/jso/core/src/main/java/org/teavm/jso/JSBodyImport.java @@ -0,0 +1,26 @@ +/* + * Copyright 2023 Alexey Andreev. + * + * 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.teavm.jso; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.CLASS) +public @interface JSBodyImport { + String alias(); + + String fromModule(); +} diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/Imports.java b/jso/impl/src/main/java/org/teavm/jso/impl/Imports.java new file mode 100644 index 000000000..169344237 --- /dev/null +++ b/jso/impl/src/main/java/org/teavm/jso/impl/Imports.java @@ -0,0 +1,27 @@ +/* + * Copyright 2023 konsoletyper. + * + * 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.teavm.jso.impl; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.CLASS) +@Target(ElementType.METHOD) +@interface Imports { + String[] value(); +} diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JSBodyAstEmitter.java b/jso/impl/src/main/java/org/teavm/jso/impl/JSBodyAstEmitter.java index 022b6473c..497164c3e 100644 --- a/jso/impl/src/main/java/org/teavm/jso/impl/JSBodyAstEmitter.java +++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSBodyAstEmitter.java @@ -32,12 +32,15 @@ class JSBodyAstEmitter implements JSBodyEmitter { private AstNode ast; private AstNode rootAst; private String[] parameterNames; + private JsBodyImportInfo[] imports; - JSBodyAstEmitter(boolean isStatic, AstNode ast, AstNode rootAst, String[] parameterNames) { + JSBodyAstEmitter(boolean isStatic, AstNode ast, AstNode rootAst, String[] parameterNames, + JsBodyImportInfo[] imports) { this.isStatic = isStatic; this.ast = ast; this.rootAst = rootAst; this.parameterNames = parameterNames; + this.imports = imports; } @Override @@ -54,6 +57,10 @@ class JSBodyAstEmitter implements JSBodyEmitter { astWriter.declareNameEmitter(parameterNames[i], prec -> context.writeExpr(context.getArgument(index), convert(prec))); } + for (var importInfo : imports) { + astWriter.declareNameEmitter(importInfo.alias, + prec -> context.getWriter().appendFunction(context.importModule(importInfo.fromModule))); + } astWriter.hoist(rootAst); astWriter.print(ast, convert(context.getPrecedence())); } @@ -148,9 +155,13 @@ class JSBodyAstEmitter implements JSBodyEmitter { int index = paramIndex++; astWriter.declareNameEmitter("this", prec -> writer.append(context.getParameterName(index))); } - for (int i = 0; i < parameterNames.length; ++i) { + for (var parameterName : parameterNames) { int index = paramIndex++; - astWriter.declareNameEmitter(parameterNames[i], prec -> writer.append(context.getParameterName(index))); + astWriter.declareNameEmitter(parameterName, prec -> writer.append(context.getParameterName(index))); + } + for (var importInfo : imports) { + astWriter.declareNameEmitter(importInfo.alias, + prec -> writer.appendFunction(context.importModule(importInfo.fromModule))); } astWriter.hoist(rootAst); if (ast instanceof Block) { diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JSBodyBloatedEmitter.java b/jso/impl/src/main/java/org/teavm/jso/impl/JSBodyBloatedEmitter.java index c2588e218..287bdc382 100644 --- a/jso/impl/src/main/java/org/teavm/jso/impl/JSBodyBloatedEmitter.java +++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSBodyBloatedEmitter.java @@ -26,22 +26,45 @@ class JSBodyBloatedEmitter implements JSBodyEmitter { private MethodReference method; private String script; private String[] parameterNames; + private JsBodyImportInfo[] imports; - public JSBodyBloatedEmitter(boolean isStatic, MethodReference method, String script, String[] parameterNames) { + JSBodyBloatedEmitter(boolean isStatic, MethodReference method, String script, String[] parameterNames, + JsBodyImportInfo[] imports) { this.isStatic = isStatic; this.method = method; this.script = script; this.parameterNames = parameterNames; + this.imports = imports; } @Override public void emit(InjectorContext context) throws IOException { - emit(context.getWriter(), index -> context.writeExpr(context.getArgument(index))); + emit(context.getWriter(), new EmissionStrategy() { + @Override + public void emitArgument(int argument) { + context.writeExpr(context.getArgument(argument)); + } + + @Override + public void emitModule(String name) throws IOException { + context.getWriter().append(context.importModule(name)); + } + }); } @Override public void emit(GeneratorContext context, SourceWriter writer, MethodReference methodRef) throws IOException { - emit(writer, index -> writer.append(context.getParameterName(index + 1))); + emit(writer, new EmissionStrategy() { + @Override + public void emitArgument(int argument) throws IOException { + writer.append(context.getParameterName(argument + 1)); + } + + @Override + public void emitModule(String name) throws IOException { + writer.append(context.importModule(name)); + } + }); } private void emit(SourceWriter writer, EmissionStrategy strategy) throws IOException { @@ -50,22 +73,43 @@ class JSBodyBloatedEmitter implements JSBodyEmitter { writer.append("if (!").appendMethodBody(method).append(".$native)").ws().append('{').indent().newLine(); writer.appendMethodBody(method).append(".$native").ws().append('=').ws().append("function("); int count = method.parameterCount(); + + var first = true; for (int i = 0; i < count; ++i) { - if (i > 0) { + if (!first) { writer.append(',').ws(); } + first = false; writer.append('_').append(i); } + for (var i = 0; i < imports.length; ++i) { + if (!first) { + writer.append(',').ws(); + } + first = false; + writer.append("_i").append(i); + } writer.append(')').ws().append('{').softNewLine().indent(); writer.append("return (function("); + + first = true; for (int i = 0; i < bodyParamCount; ++i) { - if (i > 0) { + if (!first) { writer.append(',').ws(); } + first = false; String name = parameterNames[i]; writer.append(name); } + for (var importInfo : imports) { + if (!first) { + writer.append(',').ws(); + } + first = false; + writer.append(importInfo.alias); + } + writer.append(')').ws().append('{').softNewLine().indent(); writer.append(script).softNewLine(); writer.outdent().append("})"); @@ -73,12 +117,23 @@ class JSBodyBloatedEmitter implements JSBodyEmitter { writer.append(".call"); } writer.append('('); + + first = true; for (int i = 0; i < count; ++i) { - if (i > 0) { + if (!first) { writer.append(',').ws(); } + first = false; writer.append('_').append(i); } + for (var i = 0; i < imports.length; ++i) { + if (!first) { + writer.append(',').ws(); + } + first = false; + writer.append("_i").append(i); + } + writer.append(");").softNewLine(); writer.outdent().append("};").softNewLine(); writer.appendMethodBody(method).ws().append('=').ws().appendMethodBody(method).append(".$native;") @@ -97,5 +152,7 @@ class JSBodyBloatedEmitter implements JSBodyEmitter { interface EmissionStrategy { void emitArgument(int argument) throws IOException; + + void emitModule(String name) throws IOException; } } diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JSBodyRepository.java b/jso/impl/src/main/java/org/teavm/jso/impl/JSBodyRepository.java index 24b803226..b93db9ba0 100644 --- a/jso/impl/src/main/java/org/teavm/jso/impl/JSBodyRepository.java +++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSBodyRepository.java @@ -22,10 +22,11 @@ import java.util.Set; import org.teavm.model.MethodReference; class JSBodyRepository { - public final Map emitters = new HashMap<>(); - public final Map methodMap = new HashMap<>(); - public final Set processedMethods = new HashSet<>(); - public final Set inlineMethods = new HashSet<>(); - public final Map callbackCallees = new HashMap<>(); - public final Map> callbackMethods = new HashMap<>(); + final Map emitters = new HashMap<>(); + final Map imports = new HashMap<>(); + final Map methodMap = new HashMap<>(); + final Set processedMethods = new HashSet<>(); + final Set inlineMethods = new HashSet<>(); + final Map callbackCallees = new HashMap<>(); + final Map> callbackMethods = new HashMap<>(); } diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JSClassProcessor.java b/jso/impl/src/main/java/org/teavm/jso/impl/JSClassProcessor.java index 397875824..f4107a802 100644 --- a/jso/impl/src/main/java/org/teavm/jso/impl/JSClassProcessor.java +++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSClassProcessor.java @@ -780,10 +780,24 @@ class JSClassProcessor { } var body = ((FunctionNode) rootNode.getFirstChild()).getBody(); + JsBodyImportInfo[] imports; + var importsValue = bodyAnnot.getValue("imports"); + if (importsValue != null) { + var importsList = importsValue.getList(); + imports = new JsBodyImportInfo[importsList.size()]; + for (var i = 0; i < importsList.size(); ++i) { + var importAnnot = importsList.get(0).getAnnotation(); + imports[i] = new JsBodyImportInfo(importAnnot.getValue("alias").getString(), + importAnnot.getValue("fromModule").getString()); + } + } else { + imports = new JsBodyImportInfo[0]; + } + repository.methodMap.put(methodToProcess.getReference(), proxyMethod); if (errorReporter.hasErrors()) { repository.emitters.put(proxyMethod, new JSBodyBloatedEmitter(isStatic, proxyMethod, - script, parameterNames)); + script, parameterNames, imports)); } else { var expr = JSBodyInlineUtil.isSuitableForInlining(methodToProcess.getReference(), parameterNames, body); @@ -793,7 +807,11 @@ class JSClassProcessor { expr = body; } javaInvocationProcessor.process(location, expr); - repository.emitters.put(proxyMethod, new JSBodyAstEmitter(isStatic, expr, rootNode, parameterNames)); + var emitter = new JSBodyAstEmitter(isStatic, expr, rootNode, parameterNames, imports); + repository.emitters.put(proxyMethod, emitter); + } + if (imports.length > 0) { + repository.imports.put(proxyMethod, imports); } } diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JSOPlugin.java b/jso/impl/src/main/java/org/teavm/jso/impl/JSOPlugin.java index f291b02f8..aa5a02d93 100644 --- a/jso/impl/src/main/java/org/teavm/jso/impl/JSOPlugin.java +++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSOPlugin.java @@ -78,5 +78,10 @@ public class JSOPlugin implements TeaVMPlugin { wrapperGenerator); TeaVMPluginUtil.handleNatives(host, JS.class); + + jsHost.addModuleImporterProvider(providerContext -> { + var imports = repository.imports.get(providerContext.getMethod()); + return imports != null ? new JsBodyImportsContributor(imports) : null; + }); } } diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JsBodyImportInfo.java b/jso/impl/src/main/java/org/teavm/jso/impl/JsBodyImportInfo.java new file mode 100644 index 000000000..3c4200b12 --- /dev/null +++ b/jso/impl/src/main/java/org/teavm/jso/impl/JsBodyImportInfo.java @@ -0,0 +1,26 @@ +/* + * Copyright 2023 Alexey Andreev. + * + * 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.teavm.jso.impl; + +class JsBodyImportInfo { + final String alias; + final String fromModule; + + JsBodyImportInfo(String alias, String fromModule) { + this.alias = alias; + this.fromModule = fromModule; + } +} diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JsBodyImportsContributor.java b/jso/impl/src/main/java/org/teavm/jso/impl/JsBodyImportsContributor.java new file mode 100644 index 000000000..2f730b2eb --- /dev/null +++ b/jso/impl/src/main/java/org/teavm/jso/impl/JsBodyImportsContributor.java @@ -0,0 +1,34 @@ +/* + * Copyright 2023 konsoletyper. + * + * 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.teavm.jso.impl; + +import org.teavm.backend.javascript.spi.ModuleImporter; +import org.teavm.backend.javascript.spi.ModuleImporterContext; + +class JsBodyImportsContributor implements ModuleImporter { + private JsBodyImportInfo[] imports; + + JsBodyImportsContributor(JsBodyImportInfo[] imports) { + this.imports = imports; + } + + @Override + public void importModules(ModuleImporterContext context) { + for (var importInfo : imports) { + context.importModule(importInfo.fromModule); + } + } +} diff --git a/tests/src/test/java/org/teavm/jso/test/ImportModuleTest.java b/tests/src/test/java/org/teavm/jso/test/ImportModuleTest.java new file mode 100644 index 000000000..dee379cfa --- /dev/null +++ b/tests/src/test/java/org/teavm/jso/test/ImportModuleTest.java @@ -0,0 +1,52 @@ +/* + * Copyright 2023 konsoletyper. + * + * 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.teavm.jso.test; + +import static org.junit.Assert.assertEquals; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.teavm.jso.JSBody; +import org.teavm.jso.JSBodyImport; +import org.teavm.junit.AttachJavaScript; +import org.teavm.junit.SkipJVM; +import org.teavm.junit.TeaVMTestRunner; +import org.teavm.junit.WholeClassCompilation; + +@RunWith(TeaVMTestRunner.class) +@SkipJVM +@WholeClassCompilation +public class ImportModuleTest { + @Test + @AttachJavaScript({ + "org/teavm/jso/test/amd.js", + "org/teavm/jso/test/amdModule.js" + }) + public void amd() { + assertEquals(23, runTestFunction()); + } + + @Test + @AttachJavaScript("org/teavm/jso/test/commonjs.js") + public void commonjs() { + assertEquals(23, runTestFunction()); + } + + @JSBody( + script = "return testModule.foo();", + imports = @JSBodyImport(alias = "testModule", fromModule = "testModule.js") + ) + private static native int runTestFunction(); +} diff --git a/tests/src/test/resources/org/teavm/jso/test/amd.js b/tests/src/test/resources/org/teavm/jso/test/amd.js new file mode 100644 index 000000000..d88a93b18 --- /dev/null +++ b/tests/src/test/resources/org/teavm/jso/test/amd.js @@ -0,0 +1,47 @@ +/* + * Copyright 2023 konsoletyper. + * + * 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. + */ + +let main; +const define = (function() { + const modules = new Map(); + function def() { + let index = 0; + const moduleName = typeof arguments[index] === 'string' ? arguments[index++] : null; + const deps = arguments[index++]; + const module = arguments[index++]; + + const exports = Object.create(null); + const args = []; + for (const dep of deps) { + if (dep === 'exports') { + args.push(exports); + } else { + args.push(modules[dep]); + } + } + let result = module.apply(this, args); + if (typeof result === 'undefined') { + result = exports; + } + if (moduleName !== null) { + modules[moduleName] = result; + } else { + main = result.main; + } + } + def.amd = {}; + return def; +})(); \ No newline at end of file diff --git a/tests/src/test/resources/org/teavm/jso/test/amdModule.js b/tests/src/test/resources/org/teavm/jso/test/amdModule.js new file mode 100644 index 000000000..f3558cc3d --- /dev/null +++ b/tests/src/test/resources/org/teavm/jso/test/amdModule.js @@ -0,0 +1,23 @@ +/* + * Copyright 2023 konsoletyper. + * + * 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. + */ + +define("testModule.js", [], () => { + return { + foo() { + return 23; + } + } +}); \ No newline at end of file diff --git a/tests/src/test/resources/org/teavm/jso/test/commonjs.js b/tests/src/test/resources/org/teavm/jso/test/commonjs.js new file mode 100644 index 000000000..a0df8790e --- /dev/null +++ b/tests/src/test/resources/org/teavm/jso/test/commonjs.js @@ -0,0 +1,36 @@ +/* + * Copyright 2023 konsoletyper. + * + * 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. + */ + +function require(name) { + switch (name) { + case "testModule.js": { + return { + foo() { + return 23; + } + } + } + default: + throw new Error("Unknown module: " + name); + } +} + +let global = this; +let exports = {}; + +function main() { + exports.main.apply(this, arguments); +} \ No newline at end of file diff --git a/tools/junit/src/main/java/org/teavm/junit/AttachJavaScript.java b/tools/junit/src/main/java/org/teavm/junit/AttachJavaScript.java new file mode 100644 index 000000000..ce3db67fb --- /dev/null +++ b/tools/junit/src/main/java/org/teavm/junit/AttachJavaScript.java @@ -0,0 +1,27 @@ +/* + * Copyright 2023 konsoletyper. + * + * 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.teavm.junit; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +public @interface AttachJavaScript { + String[] value(); +} diff --git a/tools/junit/src/main/java/org/teavm/junit/BrowserRunStrategy.java b/tools/junit/src/main/java/org/teavm/junit/BrowserRunStrategy.java index 723a7979b..8a66de29c 100644 --- a/tools/junit/src/main/java/org/teavm/junit/BrowserRunStrategy.java +++ b/tools/junit/src/main/java/org/teavm/junit/BrowserRunStrategy.java @@ -25,10 +25,11 @@ import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; -import java.io.OutputStream; import java.io.Reader; import java.io.StringReader; import java.nio.charset.StandardCharsets; +import java.util.LinkedHashSet; +import java.util.List; import java.util.Map; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ConcurrentHashMap; @@ -66,7 +67,7 @@ class BrowserRunStrategy implements TestRunStrategy { private ConcurrentMap awaitingRuns = new ConcurrentHashMap<>(); private ObjectMapper objectMapper = new ObjectMapper(); - public BrowserRunStrategy(File baseDir, String type, Function browserRunner) { + BrowserRunStrategy(File baseDir, String type, Function browserRunner) { this.baseDir = baseDir; this.type = type; this.browserRunner = browserRunner; @@ -186,6 +187,16 @@ class BrowserRunStrategy implements TestRunStrategy { testNode.set("type", nf.textNode(type)); testNode.set("name", nf.textNode(run.getFileName())); testNode.set("file", nf.textNode("tests/" + relPath)); + + var additionalJs = additionalJs(run); + if (additionalJs.length > 0) { + var additionalJsJson = nf.arrayNode(); + for (var additionalFile : additionalJs) { + additionalJsJson.add("resources/" + additionalFile); + } + testNode.set("additionalFiles", additionalJsJson); + } + if (run.getArgument() != null) { testNode.set("argument", nf.textNode(run.getArgument())); } @@ -207,6 +218,27 @@ class BrowserRunStrategy implements TestRunStrategy { return !callbackWrapper.shouldRepeat; } + private String[] additionalJs(TestRun run) { + var result = new LinkedHashSet(); + + var method = run.getMethod(); + var attachAnnot = method.getAnnotation(AttachJavaScript.class); + if (attachAnnot != null) { + result.addAll(List.of(attachAnnot.value())); + } + + var cls = method.getDeclaringClass(); + while (cls != null) { + var classAttachAnnot = cls.getAnnotation(AttachJavaScript.class); + if (classAttachAnnot != null) { + result.addAll(List.of(attachAnnot.value())); + } + cls = cls.getSuperclass(); + } + + return result.toArray(new String[0]); + } + class TestCodeServlet extends HttpServlet { private WebSocketServletFactory wsFactory; private Map contentCache = new ConcurrentHashMap<>(); @@ -225,7 +257,7 @@ class BrowserRunStrategy implements TestRunStrategy { } @Override - protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException { String path = req.getRequestURI(); if (path != null) { if (!path.startsWith("/")) { @@ -270,7 +302,20 @@ class BrowserRunStrategy implements TestRunStrategy { resp.setContentType("application/wasm"); } try (FileInputStream input = new FileInputStream(file)) { - copy(input, resp.getOutputStream()); + input.transferTo(resp.getOutputStream()); + } + resp.getOutputStream().flush(); + } + } + if (path.startsWith("/resources/")) { + var relPath = path.substring("/resources/".length()); + var classLoader = BrowserRunStrategy.class.getClassLoader(); + try (var input = classLoader.getResourceAsStream(relPath)) { + if (input != null) { + resp.setStatus(HttpServletResponse.SC_OK); + input.transferTo(resp.getOutputStream()); + } else { + resp.setStatus(HttpServletResponse.SC_NOT_FOUND); } resp.getOutputStream().flush(); } @@ -309,17 +354,6 @@ class BrowserRunStrategy implements TestRunStrategy { } }); } - - private void copy(InputStream input, OutputStream output) throws IOException { - byte[] buffer = new byte[2048]; - while (true) { - int bytes = input.read(buffer); - if (bytes < 0) { - break; - } - output.write(buffer, 0, bytes); - } - } } class TestCodeSocket extends WebSocketAdapter { diff --git a/tools/junit/src/main/resources/test-server/frame.js b/tools/junit/src/main/resources/test-server/frame.js index 4bcafb8c6..b35737426 100644 --- a/tools/junit/src/main/resources/test-server/frame.js +++ b/tools/junit/src/main/resources/test-server/frame.js @@ -20,7 +20,8 @@ window.addEventListener("message", event => { let request = event.data; switch (request.type) { case "JAVASCRIPT": - appendFiles([request.file], 0, () => { + const files = request.additionalFiles ? [...request.additionalFiles, request.file] : [request.file]; + appendFiles(files, 0, () => { launchTest(request.argument, response => { event.source.postMessage(response, "*"); });