Rename the components

This commit is contained in:
2026-06-11 08:56:40 +08:00
parent 1a38577817
commit 4fcd58b5ed
24 changed files with 116 additions and 74 deletions
+39
View File
@@ -0,0 +1,39 @@
FROM maven:3.9-eclipse-temurin-21 AS build
WORKDIR /src
ARG JAVA_SRC_DIR
ARG CLOSURE_NAME
# Copy pom first so Docker can cache dependencies.
COPY $JAVA_SRC_DIR/pom.xml .
RUN --mount=type=cache,target=/root/.m2 \
mvn -B -DskipTests dependency:go-offline
COPY $JAVA_SRC_DIR/src ./src
RUN --mount=type=cache,target=/root/.m2 \
mvn -B -DskipTests package
FROM eclipse-temurin:21-jre
WORKDIR /app
ARG JAVA_SRC_DIR
ARG CLOSURE_NAME
RUN useradd -r -u 10001 closure
COPY --from=build /src/target/${CLOSURE_NAME}-0.1.0.jar /app/runtime.jar
COPY $JAVA_SRC_DIR/example ./example
USER closure
ENV CLOSURED_ROOT=/work
ENV CLOSURED_PORT=8080
ENV CLOSURED_WORKERS=2
EXPOSE 8080
ENTRYPOINT ["java", "-Xms256m", "-Xmx2g", "-jar", "/app/runtime.jar"]
+30
View File
@@ -0,0 +1,30 @@
# closure-api-min
Tiny Closure Compiler HTTP daemon using Java's built-in HTTP server.
## Build
```sh
mvn -DskipTests package
```
## Run
```sh
CLOSURED_ROOT=$PWD \
CLOSURED_PORT=8080 \
CLOSURED_WORKERS=2 \
java -Xms256m -Xmx2g -jar target/closure-api-0.1.0.jar
```
## Test
```sh
curl -s http://127.0.0.1:8080/compile \
-H 'Content-Type: application/json' \
-d '{
"externs": ["example/externs/browser.js"],
"js": ["example/js/hello.js"],
"defines": {"DEBUG": false}
}'
```
+3
View File
@@ -0,0 +1,3 @@
/** @externs */
var window = {};
window.demoHello = function(name) {};
+13
View File
@@ -0,0 +1,13 @@
/** @define {boolean} */
var DEBUG = false;
function internalNameThatShouldDisappear(name) {
if (DEBUG) {
console.log('debug mode');
}
return 'hello ' + name;
}
window.demoHello = function(name) {
return internalNameThatShouldDisappear(name);
};
+61
View File
@@ -0,0 +1,61 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>dev.tgckpg</groupId>
<artifactId>closure-api</artifactId>
<version>0.1.0</version>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.google.javascript</groupId>
<artifactId>closure-compiler</artifactId>
<version>v20260526</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.19.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.14.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.6.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>dev.tgckpg.closured.Main</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,192 @@
package dev.tgckpg.closured;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.javascript.jscomp.CompilationLevel;
import com.google.javascript.jscomp.CommandLineRunner;
import com.google.javascript.jscomp.CompilerOptions;
import com.google.javascript.jscomp.JSError;
import com.google.javascript.jscomp.Result;
import com.google.javascript.jscomp.SourceFile;
import com.google.javascript.jscomp.WarningLevel;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executors;
public final class Main {
private static final ObjectMapper JSON = new ObjectMapper();
private static final Path ROOT = Path.of(System.getenv().getOrDefault("CLOSURED_ROOT", ".")).toAbsolutePath().normalize();
private static final int PORT = Integer.parseInt(System.getenv().getOrDefault("CLOSURED_PORT", "8080"));
private static final int WORKERS = Integer.parseInt(System.getenv().getOrDefault("CLOSURED_WORKERS", "2"));
public static void main(String[] args) throws Exception {
HttpServer server = HttpServer.create(new InetSocketAddress("0.0.0.0", PORT), 128);
server.setExecutor(Executors.newFixedThreadPool(WORKERS));
server.createContext("/health", Main::health);
server.createContext("/compile", Main::compile);
server.start();
System.err.printf("closure-api listening on http://0.0.0.0:%d root=%s workers=%d%n", PORT, ROOT, WORKERS);
}
private static void health(HttpExchange ex) throws IOException {
sendJson(ex, 200, JSON.createObjectNode().put("ok", true));
}
private static void compile(HttpExchange ex) throws IOException {
if (!"POST".equalsIgnoreCase(ex.getRequestMethod())) {
sendJson(ex, 405, JSON.createObjectNode().put("ok", false).put("error", "POST required"));
return;
}
JsonNode req;
try (InputStream in = ex.getRequestBody()) {
req = JSON.readTree(in);
} catch (Exception e) {
sendJson(ex, 400, JSON.createObjectNode().put("ok", false).put("error", "invalid JSON"));
return;
}
try {
CompileOutput out = compileRequest(req);
ObjectNode res = JSON.createObjectNode();
res.put("ok", out.success);
res.put("js", out.js);
res.set("warnings", JSON.valueToTree(out.warnings));
res.set("errors", JSON.valueToTree(out.errors));
sendJson(ex, out.success ? 200 : 422, res);
} catch (BadRequest e) {
sendJson(ex, 400, JSON.createObjectNode().put("ok", false).put("error", e.getMessage()));
} catch (Exception e) {
e.printStackTrace();
sendJson(ex, 500, JSON.createObjectNode().put("ok", false).put("error", e.toString()));
}
}
private static CompileOutput compileRequest(JsonNode req) throws IOException, BadRequest {
CompilerOptions options = new CompilerOptions();
CompilationLevel.ADVANCED_OPTIMIZATIONS.setOptionsForCompilationLevel(options);
WarningLevel.DEFAULT.setOptionsForWarningLevel(options);
options.setEnvironment(CompilerOptions.Environment.BROWSER);
options.setLanguageIn(CompilerOptions.LanguageMode.ECMASCRIPT_NEXT);
options.setLanguageOut(CompilerOptions.LanguageMode.ECMASCRIPT_2019);
List<SourceFile> externs = new ArrayList<>();
externs.addAll(CommandLineRunner.getBuiltinExterns(options.getEnvironment()));
externs.addAll(readFiles(req, "externs"));
List<SourceFile> inputs = readFiles(req, "js");
if (inputs.isEmpty()) {
throw new BadRequest("request must include at least one js file");
}
com.google.javascript.jscomp.Compiler compiler =
new com.google.javascript.jscomp.Compiler();
JsonNode defines = req.get("defines");
if (defines != null && defines.isObject()) {
Iterator<Map.Entry<String, JsonNode>> fields = defines.fields();
while (fields.hasNext()) {
Map.Entry<String, JsonNode> entry = fields.next();
String name = entry.getKey();
JsonNode value = entry.getValue();
if (value.isBoolean()) {
options.setDefineToBooleanLiteral(name, value.booleanValue());
} else if (value.isNumber()) {
if (!value.canConvertToInt()) {
throw new BadRequest("numeric define must be an integer: " + name);
}
options.setDefineToNumberLiteral(name, value.intValue());
} else if (value.isTextual()) {
options.setDefineToStringLiteral(name, value.textValue());
} else {
throw new BadRequest("define must be boolean, number, or string: " + name);
}
}
}
Result result = compiler.compile(externs, inputs, options);
return new CompileOutput(
result.success,
result.success ? compiler.toSource() : "",
toStrings(compiler.getWarnings()),
toStrings(compiler.getErrors())
);
}
private static List<SourceFile> readFiles(JsonNode req, String field) throws IOException, BadRequest {
List<SourceFile> out = new ArrayList<>();
JsonNode arr = req.get(field);
if (arr == null) {
return out;
}
if (!arr.isArray()) {
throw new BadRequest(field + " must be an array");
}
for (JsonNode item : arr) {
if (!item.isTextual()) {
throw new BadRequest(field + " entries must be strings");
}
Path p = safePath(item.textValue());
out.add(SourceFile.fromFile(p.toString()));
}
return out;
}
private static Path safePath(String value) throws BadRequest {
Path p = ROOT.resolve(value).normalize();
if (!p.startsWith(ROOT)) {
throw new BadRequest("path escapes root: " + value);
}
if (!Files.isRegularFile(p)) {
throw new BadRequest("file not found: " + value);
}
return p;
}
private static List<String> toStrings(Iterable<JSError> errors) {
List<String> out = new ArrayList<>();
for (JSError e : errors) {
out.add(e.toString());
}
return out;
}
private static void sendJson(HttpExchange ex, int status, JsonNode body) throws IOException {
byte[] bytes = JSON.writerWithDefaultPrettyPrinter().writeValueAsBytes(body);
ex.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8");
ex.sendResponseHeaders(status, bytes.length);
try (OutputStream os = ex.getResponseBody()) {
os.write(bytes);
}
}
private record CompileOutput(boolean success, String js, List<String> warnings, List<String> errors) {}
private static final class BadRequest extends Exception {
BadRequest(String message) {
super(message);
}
}
}