JS: supports module imports in JSBody

This commit is contained in:
Alexey Andreev 2023-08-01 20:56:58 +02:00
parent a97e0ef45c
commit 334e2829b3
28 changed files with 663 additions and 44 deletions

View File

@ -30,8 +30,10 @@ import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Properties;
import java.util.Set; import java.util.Set;
import java.util.function.Function; import java.util.function.Function;
import org.teavm.ast.AsyncMethodNode; 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.decompile.PreparedMethod;
import org.teavm.backend.javascript.rendering.Renderer; import org.teavm.backend.javascript.rendering.Renderer;
import org.teavm.backend.javascript.rendering.RenderingContext; 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.rendering.RuntimeRenderer;
import org.teavm.backend.javascript.spi.GeneratedBy; import org.teavm.backend.javascript.spi.GeneratedBy;
import org.teavm.backend.javascript.spi.Generator; import org.teavm.backend.javascript.spi.Generator;
import org.teavm.backend.javascript.spi.InjectedBy; import org.teavm.backend.javascript.spi.InjectedBy;
import org.teavm.backend.javascript.spi.Injector; 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.VirtualMethodContributor;
import org.teavm.backend.javascript.spi.VirtualMethodContributorContext; import org.teavm.backend.javascript.spi.VirtualMethodContributorContext;
import org.teavm.cache.AstCacheEntry; import org.teavm.cache.AstCacheEntry;
@ -68,6 +73,7 @@ import org.teavm.debugging.information.SourceLocation;
import org.teavm.dependency.AbstractDependencyListener; import org.teavm.dependency.AbstractDependencyListener;
import org.teavm.dependency.DependencyAgent; import org.teavm.dependency.DependencyAgent;
import org.teavm.dependency.DependencyAnalyzer; import org.teavm.dependency.DependencyAnalyzer;
import org.teavm.dependency.DependencyInfo;
import org.teavm.dependency.DependencyListener; import org.teavm.dependency.DependencyListener;
import org.teavm.dependency.DependencyType; import org.teavm.dependency.DependencyType;
import org.teavm.dependency.MethodDependency; import org.teavm.dependency.MethodDependency;
@ -83,6 +89,7 @@ import org.teavm.model.ClassReaderSource;
import org.teavm.model.ElementModifier; import org.teavm.model.ElementModifier;
import org.teavm.model.FieldReference; import org.teavm.model.FieldReference;
import org.teavm.model.ListableClassHolderSource; import org.teavm.model.ListableClassHolderSource;
import org.teavm.model.ListableClassReaderSource;
import org.teavm.model.MethodHolder; import org.teavm.model.MethodHolder;
import org.teavm.model.MethodReader; import org.teavm.model.MethodReader;
import org.teavm.model.MethodReference; import org.teavm.model.MethodReference;
@ -120,6 +127,7 @@ public class JavaScriptTarget implements TeaVMTarget, TeaVMJavaScriptHost {
private final Map<MethodReference, Injector> methodInjectors = new HashMap<>(); private final Map<MethodReference, Injector> methodInjectors = new HashMap<>();
private final List<Function<ProviderContext, Generator>> generatorProviders = new ArrayList<>(); private final List<Function<ProviderContext, Generator>> generatorProviders = new ArrayList<>();
private final List<Function<ProviderContext, Injector>> injectorProviders = new ArrayList<>(); private final List<Function<ProviderContext, Injector>> injectorProviders = new ArrayList<>();
private final List<Function<ProviderContext, ModuleImporter>> moduleImporterProviders = new ArrayList<>();
private final List<RendererListener> rendererListeners = new ArrayList<>(); private final List<RendererListener> rendererListeners = new ArrayList<>();
private DebugInformationEmitter debugEmitter; private DebugInformationEmitter debugEmitter;
private MethodNodeCache astCache = EmptyMethodNodeCache.INSTANCE; private MethodNodeCache astCache = EmptyMethodNodeCache.INSTANCE;
@ -131,6 +139,7 @@ public class JavaScriptTarget implements TeaVMTarget, TeaVMJavaScriptHost {
private boolean strict; private boolean strict;
private BoundCheckInsertion boundCheckInsertion = new BoundCheckInsertion(); private BoundCheckInsertion boundCheckInsertion = new BoundCheckInsertion();
private NullCheckInsertion nullCheckInsertion = new NullCheckInsertion(NullCheckFilter.EMPTY); private NullCheckInsertion nullCheckInsertion = new NullCheckInsertion(NullCheckFilter.EMPTY);
private final Map<String, String> importedModules = new LinkedHashMap<>();
@Override @Override
public List<ClassHolderTransformer> getTransformers() { public List<ClassHolderTransformer> getTransformers() {
@ -172,6 +181,11 @@ public class JavaScriptTarget implements TeaVMTarget, TeaVMJavaScriptHost {
injectorProviders.add(provider); injectorProviders.add(provider);
} }
@Override
public void addModuleImporterProvider(Function<ProviderContext, ModuleImporter> provider) {
moduleImporterProviders.add(provider);
}
/** /**
* Specifies whether this TeaVM instance uses obfuscation when generating the JavaScript code. * 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) { private void emit(ListableClassHolderSource classes, Writer writer, BuildTarget target) {
List<PreparedClass> clsNodes = modelToAst(classes); List<PreparedClass> clsNodes = modelToAst(classes);
prepareModules(classes);
if (controller.wasCancelled()) { if (controller.wasCancelled()) {
return; return;
} }
@ -382,7 +397,13 @@ public class JavaScriptTarget implements TeaVMTarget, TeaVMJavaScriptHost {
controller.getUnprocessedClassSource(), classes, controller.getUnprocessedClassSource(), classes,
controller.getClassLoader(), controller.getServices(), controller.getProperties(), naming, controller.getClassLoader(), controller.getServices(), controller.getProperties(), naming,
controller.getDependencyInfo(), m -> isVirtual(virtualMethodContributorContext, m), 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); renderingContext.setMinifying(obfuscated);
Renderer renderer = new Renderer(sourceWriter, asyncMethods, asyncFamilyMethods, Renderer renderer = new Renderer(sourceWriter, asyncMethods, asyncFamilyMethods,
controller.getDiagnostics(), renderingContext); controller.getDiagnostics(), renderingContext);
@ -404,7 +425,7 @@ public class JavaScriptTarget implements TeaVMTarget, TeaVMJavaScriptHost {
renderer.setDebugEmitter(debugEmitter); renderer.setDebugEmitter(debugEmitter);
} }
renderer.getDebugEmitter().setLocationProvider(sourceWriter); renderer.getDebugEmitter().setLocationProvider(sourceWriter);
for (Map.Entry<MethodReference, Injector> entry : methodInjectors.entrySet()) { for (var entry : methodInjectors.entrySet()) {
renderingContext.addInjector(entry.getKey(), entry.getValue()); renderingContext.addInjector(entry.getKey(), entry.getValue());
} }
try { try {
@ -462,21 +483,45 @@ public class JavaScriptTarget implements TeaVMTarget, TeaVMJavaScriptHost {
private void printWrapperStart(SourceWriter writer) throws IOException { private void printWrapperStart(SourceWriter writer) throws IOException {
writer.append("\"use strict\";").newLine(); writer.append("\"use strict\";").newLine();
printUmdStart(writer); 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 { private void printUmdStart(SourceWriter writer) throws IOException {
writer.append("(function(root,").ws().append("module)").appendBlockStart(); writer.append("(function(root,").ws().append("module)").appendBlockStart();
writer.appendIf().append("typeof define").ws().append("===").ws().append("'function'") writer.appendIf().append("typeof define").ws().append("===").ws().append("'function'")
.ws().append("&&").ws().append("define.amd)").appendBlockStart(); .ws().append("&&").ws().append("define.amd)").appendBlockStart();
writer.append("define(['exports'],").ws().append("function(exports)").ws().appendBlockStart(); writer.append("define(['exports'");
writer.append("module(root,").ws().append("exports);").softNewLine(); 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.outdent().append("});").softNewLine();
writer.appendElseIf().append("typeof exports").ws() writer.appendElseIf().append("typeof exports").ws()
.append("===").ws().append("'object'").ws().append("&&").ws() .append("===").ws().append("'object'").ws().append("&&").ws()
.append("typeof exports.nodeName").ws().append("!==").ws().append("'string')").appendBlockStart(); .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.appendElse();
writer.append("module(root,").ws().append("root);").softNewLine(); 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) + ")"; 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<PreparedClass> modelToAst(ListableClassHolderSource classes) { private List<PreparedClass> modelToAst(ListableClassHolderSource classes) {
AsyncMethodFinder asyncFinder = new AsyncMethodFinder(controller.getDependencyInfo().getCallGraph(), AsyncMethodFinder asyncFinder = new AsyncMethodFinder(controller.getDependencyInfo().getCallGraph(),
controller.getDependencyInfo()); controller.getDependencyInfo());
@ -676,7 +743,7 @@ public class JavaScriptTarget implements TeaVMTarget, TeaVMJavaScriptHost {
boolean found = false; boolean found = false;
ProviderContext context = new ProviderContextImpl(method.getReference()); ProviderContext context = new ProviderContextImpl(method.getReference());
for (Function<ProviderContext, Generator> provider : generatorProviders) { for (var provider : generatorProviders) {
Generator generator = provider.apply(context); Generator generator = provider.apply(context);
if (generator != null) { if (generator != null) {
methodGenerators.put(method.getReference(), generator); methodGenerators.put(method.getReference(), generator);
@ -684,7 +751,7 @@ public class JavaScriptTarget implements TeaVMTarget, TeaVMJavaScriptHost {
break; break;
} }
} }
for (Function<ProviderContext, Injector> provider : injectorProviders) { for (var provider : injectorProviders) {
Injector injector = provider.apply(context); Injector injector = provider.apply(context);
if (injector != null) { if (injector != null) {
methodInjectors.put(method.getReference(), injector); methodInjectors.put(method.getReference(), injector);
@ -756,6 +823,11 @@ public class JavaScriptTarget implements TeaVMTarget, TeaVMJavaScriptHost {
public ClassReaderSource getClassSource() { public ClassReaderSource getClassSource() {
return controller.getUnprocessedClassSource(); return controller.getUnprocessedClassSource();
} }
@Override
public <T> T getService(Class<T> type) {
return controller.getServices().getService(type);
}
} }
@PlatformMarker @PlatformMarker
@ -820,4 +892,48 @@ public class JavaScriptTarget implements TeaVMTarget, TeaVMJavaScriptHost {
return classSource; 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> T getService(Class<T> type) {
return controller.getServices().getService(type);
}
}
} }

View File

@ -15,10 +15,11 @@
*/ */
package org.teavm.backend.javascript; package org.teavm.backend.javascript;
import org.teavm.common.ServiceRepository;
import org.teavm.model.ClassReaderSource; import org.teavm.model.ClassReaderSource;
import org.teavm.model.MethodReference; import org.teavm.model.MethodReference;
public interface ProviderContext { public interface ProviderContext extends ServiceRepository {
MethodReference getMethod(); MethodReference getMethod();
ClassReaderSource getClassSource(); ClassReaderSource getClassSource();

View File

@ -18,6 +18,7 @@ package org.teavm.backend.javascript;
import java.util.function.Function; import java.util.function.Function;
import org.teavm.backend.javascript.spi.Generator; import org.teavm.backend.javascript.spi.Generator;
import org.teavm.backend.javascript.spi.Injector; import org.teavm.backend.javascript.spi.Injector;
import org.teavm.backend.javascript.spi.ModuleImporter;
import org.teavm.backend.javascript.spi.VirtualMethodContributor; import org.teavm.backend.javascript.spi.VirtualMethodContributor;
import org.teavm.model.MethodReference; import org.teavm.model.MethodReference;
import org.teavm.vm.spi.RendererListener; import org.teavm.vm.spi.RendererListener;
@ -32,6 +33,8 @@ public interface TeaVMJavaScriptHost extends TeaVMHostExtension {
void addInjectorProvider(Function<ProviderContext, Injector> provider); void addInjectorProvider(Function<ProviderContext, Injector> provider);
void addModuleImporterProvider(Function<ProviderContext, ModuleImporter> provider);
void add(RendererListener listener); void add(RendererListener listener);
void addVirtualMethods(VirtualMethodContributor virtualMethods); void addVirtualMethods(VirtualMethodContributor virtualMethods);

View File

@ -1212,6 +1212,11 @@ public class Renderer implements RenderingManager {
public boolean isDynamicInitializer(String className) { public boolean isDynamicInitializer(String className) {
return context.isDynamicInitializer(className); return context.isDynamicInitializer(className);
} }
@Override
public String importModule(String name) {
return context.importModule(name);
}
} }
private void appendMonitor(StatementRenderer statementRenderer, MethodNode methodNode) throws IOException { private void appendMonitor(StatementRenderer statementRenderer, MethodNode methodNode) throws IOException {

View File

@ -47,7 +47,7 @@ import org.teavm.model.TextLocation;
import org.teavm.model.ValueType; import org.teavm.model.ValueType;
import org.teavm.model.analysis.ClassInitializerInfo; import org.teavm.model.analysis.ClassInitializerInfo;
public class RenderingContext { public abstract class RenderingContext {
private final DebugInformationEmitter debugEmitter; private final DebugInformationEmitter debugEmitter;
private ClassReaderSource initialClassSource; private ClassReaderSource initialClassSource;
private ListableClassReaderSource classSource; private ListableClassReaderSource classSource;
@ -406,6 +406,8 @@ public class RenderingContext {
return strict; return strict;
} }
public abstract String importModule(String name);
@PlatformMarker @PlatformMarker
private static boolean isBootstrap() { private static boolean isBootstrap() {
return false; return false;

View File

@ -1747,5 +1747,10 @@ public class StatementRenderer implements ExprVisitor, StatementVisitor {
public ListableClassReaderSource getClassSource() { public ListableClassReaderSource getClassSource() {
return context.getClassSource(); return context.getClassSource();
} }
@Override
public String importModule(String name) {
return context.importModule(name);
}
} }
} }

View File

@ -28,6 +28,8 @@ import org.teavm.model.ValueType;
public interface GeneratorContext extends ServiceRepository { public interface GeneratorContext extends ServiceRepository {
String getParameterName(int index); String getParameterName(int index);
String importModule(String name);
ClassReaderSource getInitialClassSource(); ClassReaderSource getInitialClassSource();
ListableClassReaderSource getClassSource(); ListableClassReaderSource getClassSource();

View File

@ -27,6 +27,8 @@ import org.teavm.model.ValueType;
public interface InjectorContext extends ServiceRepository { public interface InjectorContext extends ServiceRepository {
Expr getArgument(int index); Expr getArgument(int index);
String importModule(String name);
int argumentCount(); int argumentCount();
boolean isMinifying(); boolean isMinifying();

View File

@ -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);
}

View File

@ -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();
}

View File

@ -106,7 +106,7 @@ public abstract class BaseTypeInference<T> {
continue; continue;
} }
type = doMerge(type, formerType); type = doMerge(type, formerType);
if (Objects.equals(type, formerType)) { if (Objects.equals(type, formerType) || type == null) {
continue; continue;
} }
types[variable] = type; types[variable] = type;

View File

@ -133,4 +133,6 @@ public @interface JSBody {
* <p>JavaScript code.</p> * <p>JavaScript code.</p>
*/ */
String script(); String script();
JSBodyImport[] imports() default {};
} }

View File

@ -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();
}

View File

@ -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();
}

View File

@ -32,12 +32,15 @@ class JSBodyAstEmitter implements JSBodyEmitter {
private AstNode ast; private AstNode ast;
private AstNode rootAst; private AstNode rootAst;
private String[] parameterNames; 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.isStatic = isStatic;
this.ast = ast; this.ast = ast;
this.rootAst = rootAst; this.rootAst = rootAst;
this.parameterNames = parameterNames; this.parameterNames = parameterNames;
this.imports = imports;
} }
@Override @Override
@ -54,6 +57,10 @@ class JSBodyAstEmitter implements JSBodyEmitter {
astWriter.declareNameEmitter(parameterNames[i], astWriter.declareNameEmitter(parameterNames[i],
prec -> context.writeExpr(context.getArgument(index), convert(prec))); 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.hoist(rootAst);
astWriter.print(ast, convert(context.getPrecedence())); astWriter.print(ast, convert(context.getPrecedence()));
} }
@ -148,9 +155,13 @@ class JSBodyAstEmitter implements JSBodyEmitter {
int index = paramIndex++; int index = paramIndex++;
astWriter.declareNameEmitter("this", prec -> writer.append(context.getParameterName(index))); astWriter.declareNameEmitter("this", prec -> writer.append(context.getParameterName(index)));
} }
for (int i = 0; i < parameterNames.length; ++i) { for (var parameterName : parameterNames) {
int index = paramIndex++; 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); astWriter.hoist(rootAst);
if (ast instanceof Block) { if (ast instanceof Block) {

View File

@ -26,22 +26,45 @@ class JSBodyBloatedEmitter implements JSBodyEmitter {
private MethodReference method; private MethodReference method;
private String script; private String script;
private String[] parameterNames; 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.isStatic = isStatic;
this.method = method; this.method = method;
this.script = script; this.script = script;
this.parameterNames = parameterNames; this.parameterNames = parameterNames;
this.imports = imports;
} }
@Override @Override
public void emit(InjectorContext context) throws IOException { 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 @Override
public void emit(GeneratorContext context, SourceWriter writer, MethodReference methodRef) throws IOException { 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 { 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.append("if (!").appendMethodBody(method).append(".$native)").ws().append('{').indent().newLine();
writer.appendMethodBody(method).append(".$native").ws().append('=').ws().append("function("); writer.appendMethodBody(method).append(".$native").ws().append('=').ws().append("function(");
int count = method.parameterCount(); int count = method.parameterCount();
var first = true;
for (int i = 0; i < count; ++i) { for (int i = 0; i < count; ++i) {
if (i > 0) { if (!first) {
writer.append(',').ws(); writer.append(',').ws();
} }
first = false;
writer.append('_').append(i); 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(')').ws().append('{').softNewLine().indent();
writer.append("return (function("); writer.append("return (function(");
first = true;
for (int i = 0; i < bodyParamCount; ++i) { for (int i = 0; i < bodyParamCount; ++i) {
if (i > 0) { if (!first) {
writer.append(',').ws(); writer.append(',').ws();
} }
first = false;
String name = parameterNames[i]; String name = parameterNames[i];
writer.append(name); 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(')').ws().append('{').softNewLine().indent();
writer.append(script).softNewLine(); writer.append(script).softNewLine();
writer.outdent().append("})"); writer.outdent().append("})");
@ -73,12 +117,23 @@ class JSBodyBloatedEmitter implements JSBodyEmitter {
writer.append(".call"); writer.append(".call");
} }
writer.append('('); writer.append('(');
first = true;
for (int i = 0; i < count; ++i) { for (int i = 0; i < count; ++i) {
if (i > 0) { if (!first) {
writer.append(',').ws(); writer.append(',').ws();
} }
first = false;
writer.append('_').append(i); 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.append(");").softNewLine();
writer.outdent().append("};").softNewLine(); writer.outdent().append("};").softNewLine();
writer.appendMethodBody(method).ws().append('=').ws().appendMethodBody(method).append(".$native;") writer.appendMethodBody(method).ws().append('=').ws().appendMethodBody(method).append(".$native;")
@ -97,5 +152,7 @@ class JSBodyBloatedEmitter implements JSBodyEmitter {
interface EmissionStrategy { interface EmissionStrategy {
void emitArgument(int argument) throws IOException; void emitArgument(int argument) throws IOException;
void emitModule(String name) throws IOException;
} }
} }

View File

@ -22,10 +22,11 @@ import java.util.Set;
import org.teavm.model.MethodReference; import org.teavm.model.MethodReference;
class JSBodyRepository { class JSBodyRepository {
public final Map<MethodReference, JSBodyEmitter> emitters = new HashMap<>(); final Map<MethodReference, JSBodyEmitter> emitters = new HashMap<>();
public final Map<MethodReference, MethodReference> methodMap = new HashMap<>(); final Map<MethodReference, JsBodyImportInfo[]> imports = new HashMap<>();
public final Set<MethodReference> processedMethods = new HashSet<>(); final Map<MethodReference, MethodReference> methodMap = new HashMap<>();
public final Set<MethodReference> inlineMethods = new HashSet<>(); final Set<MethodReference> processedMethods = new HashSet<>();
public final Map<MethodReference, MethodReference> callbackCallees = new HashMap<>(); final Set<MethodReference> inlineMethods = new HashSet<>();
public final Map<MethodReference, Set<MethodReference>> callbackMethods = new HashMap<>(); final Map<MethodReference, MethodReference> callbackCallees = new HashMap<>();
final Map<MethodReference, Set<MethodReference>> callbackMethods = new HashMap<>();
} }

View File

@ -780,10 +780,24 @@ class JSClassProcessor {
} }
var body = ((FunctionNode) rootNode.getFirstChild()).getBody(); 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); repository.methodMap.put(methodToProcess.getReference(), proxyMethod);
if (errorReporter.hasErrors()) { if (errorReporter.hasErrors()) {
repository.emitters.put(proxyMethod, new JSBodyBloatedEmitter(isStatic, proxyMethod, repository.emitters.put(proxyMethod, new JSBodyBloatedEmitter(isStatic, proxyMethod,
script, parameterNames)); script, parameterNames, imports));
} else { } else {
var expr = JSBodyInlineUtil.isSuitableForInlining(methodToProcess.getReference(), var expr = JSBodyInlineUtil.isSuitableForInlining(methodToProcess.getReference(),
parameterNames, body); parameterNames, body);
@ -793,7 +807,11 @@ class JSClassProcessor {
expr = body; expr = body;
} }
javaInvocationProcessor.process(location, expr); 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);
} }
} }

View File

@ -78,5 +78,10 @@ public class JSOPlugin implements TeaVMPlugin {
wrapperGenerator); wrapperGenerator);
TeaVMPluginUtil.handleNatives(host, JS.class); TeaVMPluginUtil.handleNatives(host, JS.class);
jsHost.addModuleImporterProvider(providerContext -> {
var imports = repository.imports.get(providerContext.getMethod());
return imports != null ? new JsBodyImportsContributor(imports) : null;
});
} }
} }

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -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();
}

View File

@ -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;
})();

View File

@ -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;
}
}
});

View File

@ -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);
}

View File

@ -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();
}

View File

@ -25,10 +25,11 @@ import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader; import java.io.Reader;
import java.io.StringReader; import java.io.StringReader;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.BlockingQueue; import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
@ -66,7 +67,7 @@ class BrowserRunStrategy implements TestRunStrategy {
private ConcurrentMap<Integer, CallbackWrapper> awaitingRuns = new ConcurrentHashMap<>(); private ConcurrentMap<Integer, CallbackWrapper> awaitingRuns = new ConcurrentHashMap<>();
private ObjectMapper objectMapper = new ObjectMapper(); private ObjectMapper objectMapper = new ObjectMapper();
public BrowserRunStrategy(File baseDir, String type, Function<String, Process> browserRunner) { BrowserRunStrategy(File baseDir, String type, Function<String, Process> browserRunner) {
this.baseDir = baseDir; this.baseDir = baseDir;
this.type = type; this.type = type;
this.browserRunner = browserRunner; this.browserRunner = browserRunner;
@ -186,6 +187,16 @@ class BrowserRunStrategy implements TestRunStrategy {
testNode.set("type", nf.textNode(type)); testNode.set("type", nf.textNode(type));
testNode.set("name", nf.textNode(run.getFileName())); testNode.set("name", nf.textNode(run.getFileName()));
testNode.set("file", nf.textNode("tests/" + relPath)); 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) { if (run.getArgument() != null) {
testNode.set("argument", nf.textNode(run.getArgument())); testNode.set("argument", nf.textNode(run.getArgument()));
} }
@ -207,6 +218,27 @@ class BrowserRunStrategy implements TestRunStrategy {
return !callbackWrapper.shouldRepeat; return !callbackWrapper.shouldRepeat;
} }
private String[] additionalJs(TestRun run) {
var result = new LinkedHashSet<String>();
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 { class TestCodeServlet extends HttpServlet {
private WebSocketServletFactory wsFactory; private WebSocketServletFactory wsFactory;
private Map<String, String> contentCache = new ConcurrentHashMap<>(); private Map<String, String> contentCache = new ConcurrentHashMap<>();
@ -225,7 +257,7 @@ class BrowserRunStrategy implements TestRunStrategy {
} }
@Override @Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException {
String path = req.getRequestURI(); String path = req.getRequestURI();
if (path != null) { if (path != null) {
if (!path.startsWith("/")) { if (!path.startsWith("/")) {
@ -270,7 +302,20 @@ class BrowserRunStrategy implements TestRunStrategy {
resp.setContentType("application/wasm"); resp.setContentType("application/wasm");
} }
try (FileInputStream input = new FileInputStream(file)) { 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(); 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 { class TestCodeSocket extends WebSocketAdapter {

View File

@ -20,7 +20,8 @@ window.addEventListener("message", event => {
let request = event.data; let request = event.data;
switch (request.type) { switch (request.type) {
case "JAVASCRIPT": case "JAVASCRIPT":
appendFiles([request.file], 0, () => { const files = request.additionalFiles ? [...request.additionalFiles, request.file] : [request.file];
appendFiles(files, 0, () => {
launchTest(request.argument, response => { launchTest(request.argument, response => {
event.source.postMessage(response, "*"); event.source.postMessage(response, "*");
}); });