Transpile using Babel

This is pretty slow right now, but works as a proof-of-concept.
This commit is contained in:
Octavia Togami 2020-04-24 21:46:03 -04:00 committed by MattBDev
parent 4f8ac056ab
commit 4307e3a671
7 changed files with 265 additions and 12 deletions

View File

@ -31,7 +31,7 @@ dependencies {
"compile"(project(":worldedit-libs:core")) "compile"(project(":worldedit-libs:core"))
"compile"("de.schlichtherle:truezip:6.8.3") "compile"("de.schlichtherle:truezip:6.8.3")
"compile"("net.java.truevfs:truevfs-profile-default_2.13:0.12.1") "compile"("net.java.truevfs:truevfs-profile-default_2.13:0.12.1")
"compile"("org.mozilla:rhino:1.7.11") "compile"("org.mozilla:rhino-runtime:1.7.12")
"compile"("org.yaml:snakeyaml:1.23") "compile"("org.yaml:snakeyaml:1.23")
"compile"("com.google.guava:guava:21.0") "compile"("com.google.guava:guava:21.0")
"compile"("com.google.code.findbugs:jsr305:3.0.2") "compile"("com.google.code.findbugs:jsr305:3.0.2")

View File

@ -19,7 +19,10 @@
package com.sk89q.worldedit.scripting; package com.sk89q.worldedit.scripting;
import com.google.common.io.CharStreams;
import com.sk89q.worldedit.WorldEditException; import com.sk89q.worldedit.WorldEditException;
import com.sk89q.worldedit.scripting.compat.BabelScriptTranspiler;
import com.sk89q.worldedit.scripting.compat.ScriptTranspiler;
import org.mozilla.javascript.Context; import org.mozilla.javascript.Context;
import org.mozilla.javascript.ImporterTopLevel; import org.mozilla.javascript.ImporterTopLevel;
import org.mozilla.javascript.JavaScriptException; import org.mozilla.javascript.JavaScriptException;
@ -28,11 +31,13 @@ import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.ScriptableObject; import org.mozilla.javascript.ScriptableObject;
import org.mozilla.javascript.WrappedException; import org.mozilla.javascript.WrappedException;
import javax.script.ScriptException;
import java.io.StringReader;
import java.util.Map; import java.util.Map;
import javax.script.ScriptException;
public class RhinoCraftScriptEngine implements CraftScriptEngine { public class RhinoCraftScriptEngine implements CraftScriptEngine {
private static final ScriptTranspiler TRANSPILER = new BabelScriptTranspiler();
private int timeLimit; private int timeLimit;
@Override @Override
@ -48,6 +53,7 @@ public class RhinoCraftScriptEngine implements CraftScriptEngine {
@Override @Override
public Object evaluate(String script, String filename, Map<String, Object> args) public Object evaluate(String script, String filename, Map<String, Object> args)
throws ScriptException, Throwable { throws ScriptException, Throwable {
String transpiled = CharStreams.toString(TRANSPILER.transpile(new StringReader(script)));
RhinoContextFactory factory = new RhinoContextFactory(timeLimit); RhinoContextFactory factory = new RhinoContextFactory(timeLimit);
Context cx = factory.enterContext(); Context cx = factory.enterContext();
cx.setClassShutter(new MinecraftHidingClassShutter()); cx.setClassShutter(new MinecraftHidingClassShutter());
@ -59,7 +65,7 @@ public class RhinoCraftScriptEngine implements CraftScriptEngine {
Context.javaToJS(entry.getValue(), scope)); Context.javaToJS(entry.getValue(), scope));
} }
try { try {
return cx.evaluateString(scope, script, filename, 1, null); return cx.evaluateString(scope, transpiled, filename, 1, null);
} catch (Error e) { } catch (Error e) {
throw new ScriptException(e.getMessage()); throw new ScriptException(e.getMessage());
} catch (RhinoException e) { } catch (RhinoException e) {

View File

@ -0,0 +1,83 @@
/*
* WorldEdit, a Minecraft world manipulation toolkit
* Copyright (C) sk89q <http://www.sk89q.com>
* Copyright (C) WorldEdit team and contributors
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by the
* Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
* for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.sk89q.worldedit.scripting.compat;
import com.google.common.io.CharStreams;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.ContextFactory;
import org.mozilla.javascript.Function;
import org.mozilla.javascript.Scriptable;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.util.concurrent.TimeUnit;
public class BabelScriptTranspiler implements ScriptTranspiler {
private static final RemoteScript BABEL = new RemoteScript(
"https://unpkg.com/@babel/standalone@7.9/babel.min.js",
"babel.min.js",
new RemoteScript(
"https://unpkg.com/core-js-bundle@3.6.5/index.js",
"core-js-bundle.js"
),
new RemoteScript(
"https://unpkg.com/regenerator-runtime@0.13.5/runtime.js",
"regenerator-runtime.js"
)
);
private final ContextFactory contextFactory = new ContextFactory() {
@Override
protected Context makeContext() {
Context context = super.makeContext();
context.setLanguageVersion(Context.VERSION_ES6);
return context;
}
};
private final Function executeBabel;
public BabelScriptTranspiler() {
Scriptable babel = BABEL.getScope();
executeBabel = contextFactory.call(ctx -> {
ctx.setOptimizationLevel(9);
String execBabelSource = "function(source) {\n" +
"return Babel.transform(source, { presets: ['env'] }).code;\n" +
"}\n";
return ctx.compileFunction(
babel, execBabelSource, "<execute-babel>", 1, null
);
});
}
@Override
public Reader transpile(Reader script) throws IOException {
long startTranspile = System.nanoTime();
Scriptable babel = BABEL.getScope();
String source = CharStreams.toString(script);
String result = (String) contextFactory.call(ctx ->
executeBabel.call(ctx, babel, null, new Object[] { source })
);
System.err.println(result);
System.err.println("Took " + TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTranspile));
return new StringReader(result);
}
}

View File

@ -0,0 +1,120 @@
package com.sk89q.worldedit.scripting.compat;
import com.google.common.collect.ImmutableList;
import com.sk89q.worldedit.WorldEdit;
import com.sk89q.worldedit.util.net.HttpRequest;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.ContextFactory;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.ScriptableObject;
import org.mozilla.javascript.TopLevel;
import java.io.IOException;
import java.io.Reader;
import java.io.UncheckedIOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static com.google.common.base.Preconditions.checkState;
public class RemoteScript {
private static final int MAX_REDIRECTS = 100;
private final ContextFactory contextFactory = new ContextFactory() {
@Override
protected boolean hasFeature(Context cx, int featureIndex) {
if (featureIndex == Context.FEATURE_OLD_UNDEF_NULL_THIS) {
return true;
}
return super.hasFeature(cx, featureIndex);
}
@Override
protected Context makeContext() {
Context context = super.makeContext();
context.setLanguageVersion(Context.VERSION_ES6);
return context;
}
};
private final Path cacheDir = WorldEdit.getInstance()
.getWorkingDirectoryFile("craftscripts/.cache").toPath();
private final URL source;
private final String cacheFileName;
private final Path cachePath;
private final List<RemoteScript> dependencies;
private volatile Scriptable cachedScope;
public RemoteScript(String source, String cacheFileName, RemoteScript... dependencies) {
this.source = HttpRequest.url(source);
this.cacheFileName = cacheFileName;
this.cachePath = cacheDir.resolve(cacheFileName);
this.dependencies = ImmutableList.copyOf(dependencies);
}
private synchronized void ensureCached() throws IOException {
if (!Files.exists(cacheDir)) {
Files.createDirectories(cacheDir);
}
if (!Files.exists(cachePath)) {
boolean downloadedBabel = false;
int redirects = 0;
URL url = source;
while (redirects < MAX_REDIRECTS && !downloadedBabel) {
try (HttpRequest request = HttpRequest.get(url)) {
request.execute();
request.expectResponseCode(200, 301, 302);
if (request.getResponseCode() > 300) {
redirects++;
url = HttpRequest.url(request.getSingleHeaderValue("Location"));
continue;
}
request.saveContent(cachePath.toFile());
downloadedBabel = true;
}
}
checkState(downloadedBabel, "Too many redirects following: %s", url);
checkState(Files.exists(cachePath), "Failed to actually download %s", cacheFileName);
}
}
protected synchronized void loadIntoScope(Context ctx, Scriptable scope) {
try {
ensureCached();
try (Reader reader = Files.newBufferedReader(cachePath)) {
ctx.evaluateReader(scope, reader, cacheFileName, 1, null);
}
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
/**
* Get a scope that the script has been evaluated in.
*
* @return the scope
*/
public synchronized Scriptable getScope() {
if (cachedScope != null) {
return cachedScope;
}
// parse + execute standalone script to load it into the scope
cachedScope = contextFactory.call(ctx -> {
ScriptableObject scriptable = new TopLevel();
Scriptable newScope = ctx.initStandardObjects(scriptable);
ctx.setOptimizationLevel(9);
for (RemoteScript dependency : dependencies) {
dependency.loadIntoScope(ctx, newScope);
}
loadIntoScope(ctx, newScope);
return newScope;
});
return cachedScope;
}
}

View File

@ -0,0 +1,38 @@
/*
* WorldEdit, a Minecraft world manipulation toolkit
* Copyright (C) sk89q <http://www.sk89q.com>
* Copyright (C) WorldEdit team and contributors
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by the
* Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
* for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.sk89q.worldedit.scripting.compat;
import java.io.IOException;
import java.io.Reader;
/**
* Transpile a script from one (version) of a language to another.
*/
public interface ScriptTranspiler {
/**
* Given input {@code script}, return the transpiled script.
*
* @param script the script to transpile
* @return the new script
*/
Reader transpile(Reader script) throws IOException;
}

View File

@ -43,6 +43,8 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import static com.google.common.base.Preconditions.checkState;
public class HttpRequest implements Closeable { public class HttpRequest implements Closeable {
private static final int CONNECT_TIMEOUT = 1000 * 5; private static final int CONNECT_TIMEOUT = 1000 * 5;
@ -200,6 +202,13 @@ public class HttpRequest implements Closeable {
return conn.getResponseCode(); return conn.getResponseCode();
} }
public String getSingleHeaderValue(String header) {
checkState(conn != null, "No connection has been made");
// maybe we should check for multi-header?
return conn.getHeaderField(header);
}
/** /**
* Get the input stream. * Get the input stream.
* *
@ -214,9 +223,8 @@ public class HttpRequest implements Closeable {
* *
* @return the buffered response * @return the buffered response
* @throws java.io.IOException on I/O error * @throws java.io.IOException on I/O error
* @throws InterruptedException on interruption
*/ */
public BufferedResponse returnContent() throws IOException, InterruptedException { public BufferedResponse returnContent() throws IOException {
if (inputStream == null) { if (inputStream == null) {
throw new IllegalArgumentException("No input stream available"); throw new IllegalArgumentException("No input stream available");
} }
@ -239,9 +247,8 @@ public class HttpRequest implements Closeable {
* @param file the file * @param file the file
* @return this object * @return this object
* @throws java.io.IOException on I/O error * @throws java.io.IOException on I/O error
* @throws InterruptedException on interruption
*/ */
public HttpRequest saveContent(File file) throws IOException, InterruptedException { public HttpRequest saveContent(File file) throws IOException {
Closer closer = Closer.create(); Closer closer = Closer.create();
try { try {
@ -262,9 +269,8 @@ public class HttpRequest implements Closeable {
* @param out the output stream * @param out the output stream
* @return this object * @return this object
* @throws java.io.IOException on I/O error * @throws java.io.IOException on I/O error
* @throws InterruptedException on interruption
*/ */
public HttpRequest saveContent(OutputStream out) throws IOException, InterruptedException { public HttpRequest saveContent(OutputStream out) throws IOException {
BufferedInputStream bis; BufferedInputStream bis;
try { try {

View File

@ -88,10 +88,10 @@ tasks.named<ShadowJar>("shadowJar") {
include(dependency("org.apache.logging.log4j:log4j-slf4j-impl")) include(dependency("org.apache.logging.log4j:log4j-slf4j-impl"))
include(dependency("de.schlichtherle:truezip")) include(dependency("de.schlichtherle:truezip"))
include(dependency("net.java.truevfs:truevfs-profile-default_2.13")) include(dependency("net.java.truevfs:truevfs-profile-default_2.13"))
include(dependency("org.mozilla:rhino")) include(dependency("org.mozilla:rhino-runtime"))
} }
minimize { minimize {
exclude(dependency("org.mozilla:rhino")) exclude(dependency("org.mozilla:rhino-runtime"))
} }
} }