diff --git a/README.md b/README.md index c5d719d..576b31b 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ A web application serving as both a user-friendly web interface and a REST API f ## Getting Started ### Prerequisites -- java 17 (GraalVM for native image) +- java 21 (GraalVM for native image) - maven 3.8.3 - or Docker/Podman (optional) diff --git a/pom.xml b/pom.xml index 1cc5d6e..41e8647 100644 --- a/pom.xml +++ b/pom.xml @@ -51,7 +51,7 @@ io.quarkus.platform 3.19.1 - v20240317 + v20250407 2.18.0 3.5.0 @@ -114,6 +114,14 @@ io.quarkus quarkus-hibernate-validator + + io.quarkus + quarkus-smallrye-openapi + + + io.quarkus + quarkus-micrometer-registry-prometheus + io.quarkus quarkus-junit5 @@ -251,24 +259,13 @@ - - com.spotify.fmt - fmt-maven-plugin - ${maven.fmt.plugin} - - - - check - - - - maven-surefire-plugin ${surefire-plugin.version} org.jboss.logmanager.LogManager + 20971520 diff --git a/src/main/java/org/treblereel/javascript/compiler/domain/CompileRequest.java b/src/main/java/org/treblereel/javascript/compiler/domain/CompileRequest.java index 7df2f0c..2da2267 100644 --- a/src/main/java/org/treblereel/javascript/compiler/domain/CompileRequest.java +++ b/src/main/java/org/treblereel/javascript/compiler/domain/CompileRequest.java @@ -21,21 +21,45 @@ import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; +import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; +import org.eclipse.microprofile.openapi.annotations.media.Schema; import org.treblereel.javascript.compiler.validation.MaxPayloadSize; import org.treblereel.javascript.compiler.validation.ValidFileName; + +@Schema(name = "CompileRequest", + description = "CompileRequest is a request to compile JavaScript code.") public class CompileRequest { + + @Schema(description = "Compilation level. Possible values: WHITESPACE, SIMPLE, ADVANCED.", enumeration = "WHITESPACE, SIMPLE, ADVANCED") private String compilationLevel; + @Schema(description = "Warning level. Possible values: QUIET, DEFAULT, VERBOSE.", enumeration = "QUIET, DEFAULT, VERBOSE") private String warningLevel; - @NotNull @ValidFileName private String outputFileName; + @NotNull + @ValidFileName + @Schema(description = "Output file name. Must be a valid file name.") + private String outputFileName; + + @Schema(description = "Language input and output", + implementation = LanguageInOut.class) + private LanguageInOut language; - @MaxPayloadSize private String payload; + @MaxPayloadSize + @Schema(description = "JavaScript code to be compiled.", + type = SchemaType.STRING) + private String payload; + + @Schema(description = "Formatting options.", implementation = Formatting.class) private Formatting formatting; - @Valid private ExternalScripts externalScripts; + @Valid + @Schema(description = "External scripts to be included in the compilation.", + implementation = ExternalScripts.class) + private ExternalScripts externalScripts; - public CompileRequest() {} + public CompileRequest() { + } public String getCompilationLevel() { return compilationLevel; @@ -85,27 +109,12 @@ public void setExternalScripts(ExternalScripts externalScripts) { this.externalScripts = externalScripts; } - public String toString() { - return "CompileRequest{" - + "compilationLevel='" - + compilationLevel - + '\'' - + ", warningLevel='" - + warningLevel - + '\'' - + ", outputFileName='" - + outputFileName - + '\'' - + ", formatting='" - + formatting - + '\'' - + ", workload='" - + payload - + '\'' - + ", externalScripts='" - + externalScripts - + '\'' - + '}'; + public LanguageInOut getLanguage() { + return language; + } + + public void setLanguage(LanguageInOut language) { + this.language = language; } @Override @@ -113,17 +122,24 @@ public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; CompileRequest that = (CompileRequest) o; - return Objects.equals(compilationLevel, that.compilationLevel) - && Objects.equals(warningLevel, that.warningLevel) - && Objects.equals(outputFileName, that.outputFileName) - && Objects.equals(payload, that.payload) - && Objects.equals(formatting, that.formatting) - && Objects.equals(externalScripts, that.externalScripts); + return Objects.equals(compilationLevel, that.compilationLevel) && Objects.equals(warningLevel, that.warningLevel) && Objects.equals(outputFileName, that.outputFileName) && Objects.equals(language, that.language) && Objects.equals(payload, that.payload) && Objects.equals(formatting, that.formatting) && Objects.equals(externalScripts, that.externalScripts); } @Override public int hashCode() { - return Objects.hash( - compilationLevel, warningLevel, outputFileName, payload, formatting, externalScripts); + return Objects.hash(compilationLevel, warningLevel, outputFileName, language, payload, formatting, externalScripts); + } + + @Override + public String toString() { + return "CompileRequest{" + + "compilationLevel='" + compilationLevel + '\'' + + ", warningLevel='" + warningLevel + '\'' + + ", outputFileName='" + outputFileName + '\'' + + ", language='" + language + '\'' + + ", payload='" + payload + '\'' + + ", formatting=" + formatting + + ", externalScripts=" + externalScripts + + '}'; } } diff --git a/src/main/java/org/treblereel/javascript/compiler/domain/CompileResponse.java b/src/main/java/org/treblereel/javascript/compiler/domain/CompileResponse.java index eac79d3..55f5dd6 100644 --- a/src/main/java/org/treblereel/javascript/compiler/domain/CompileResponse.java +++ b/src/main/java/org/treblereel/javascript/compiler/domain/CompileResponse.java @@ -20,25 +20,32 @@ import java.util.Objects; import io.quarkus.runtime.annotations.RegisterForReflection; +import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; +import org.eclipse.microprofile.openapi.annotations.media.Schema; @RegisterForReflection +@Schema(name = "CompileResponse", description = "CompileResponse is a response to the compilation request. It contains the compiled code, download ID, warnings, errors, and statistics.") public class CompileResponse { + @Schema(description = "Compiled JavaScript code.", type = SchemaType.STRING) private String compiledCode; + @Schema(description = "Download ID for the compiled code.", type = SchemaType.STRING) private String downloadId; + + @Schema(description = "List of warnings generated during compilation.", type = SchemaType.ARRAY, implementation = String.class) private List warnings; + + @Schema(description = "List of errors generated during compilation.", type = SchemaType.ARRAY, implementation = String.class) private List errors; + + @Schema(description = "Compilation statistics.", implementation = Statistics.class) private Statistics statistics; - public CompileResponse() {} + public CompileResponse() { + } - public CompileResponse( - String compiledCode, - String downloadId, - List warnings, - List errors, - Statistics statistics) { + public CompileResponse(String compiledCode, String downloadId, List warnings, List errors, Statistics statistics) { this.compiledCode = compiledCode; this.downloadId = downloadId; this.warnings = warnings; @@ -91,11 +98,7 @@ public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; CompileResponse that = (CompileResponse) o; - return Objects.equals(compiledCode, that.compiledCode) - && Objects.equals(downloadId, that.downloadId) - && Objects.equals(warnings, that.warnings) - && Objects.equals(errors, that.errors) - && Objects.equals(statistics, that.statistics); + return Objects.equals(compiledCode, that.compiledCode) && Objects.equals(downloadId, that.downloadId) && Objects.equals(warnings, that.warnings) && Objects.equals(errors, that.errors) && Objects.equals(statistics, that.statistics); } @Override @@ -105,19 +108,6 @@ public int hashCode() { @Override public String toString() { - return "CompileResponse{" - + "compiledCode='" - + compiledCode - + '\'' - + ", downloadId='" - + downloadId - + '\'' - + ", warnings=" - + warnings - + ", errors=" - + errors - + ", statistics=" - + statistics - + '}'; + return "CompileResponse{" + "compiledCode='" + compiledCode + '\'' + ", downloadId='" + downloadId + '\'' + ", warnings=" + warnings + ", errors=" + errors + ", statistics=" + statistics + '}'; } } diff --git a/src/main/java/org/treblereel/javascript/compiler/domain/ExternalScripts.java b/src/main/java/org/treblereel/javascript/compiler/domain/ExternalScripts.java index 58935f8..76a648d 100644 --- a/src/main/java/org/treblereel/javascript/compiler/domain/ExternalScripts.java +++ b/src/main/java/org/treblereel/javascript/compiler/domain/ExternalScripts.java @@ -20,11 +20,22 @@ import java.util.List; import java.util.Objects; +import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; +import org.eclipse.microprofile.openapi.annotations.media.Schema; import org.treblereel.javascript.compiler.validation.MaxExternalUrls; +@Schema( + name = "ExternalScripts", + description = + "ExternalScripts is a class that represents the external scripts to be included in the compilation.") public class ExternalScripts { - @MaxExternalUrls private List urls; + @MaxExternalUrls + @Schema( + description = "List of external scripts to be included in the compilation.", + type = SchemaType.ARRAY, + implementation = String.class) + private List urls; public ExternalScripts() { urls = new ArrayList<>(); diff --git a/src/main/java/org/treblereel/javascript/compiler/domain/Formatting.java b/src/main/java/org/treblereel/javascript/compiler/domain/Formatting.java index fcf0811..14a16ce 100644 --- a/src/main/java/org/treblereel/javascript/compiler/domain/Formatting.java +++ b/src/main/java/org/treblereel/javascript/compiler/domain/Formatting.java @@ -18,12 +18,26 @@ import java.util.Objects; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +@Schema( + name = "Formatting", + description = + "Formatting is a class that represents the formatting options for JavaScript compilation.") public class Formatting { + @Schema( + description = "If true, the output will be pretty-printed.", + defaultValue = "false") public boolean prettyPrint; + + @Schema( + description = "If true, the input delimiter will be printed.", + defaultValue = "false") public boolean printInputDelimiter; - public Formatting() {} + public Formatting() { + } public Formatting(boolean prettyPrint, boolean printInputDelimiter) { this.prettyPrint = prettyPrint; @@ -48,11 +62,11 @@ public void setPrintInputDelimiter(boolean printInputDelimiter) { public String toString() { return "Formatting{" - + "prettyPrint=" - + prettyPrint - + ", printInputDelimiter=" - + printInputDelimiter - + '}'; + + "prettyPrint=" + + prettyPrint + + ", printInputDelimiter=" + + printInputDelimiter + + '}'; } @Override diff --git a/src/main/java/org/treblereel/javascript/compiler/domain/LanguageInOut.java b/src/main/java/org/treblereel/javascript/compiler/domain/LanguageInOut.java new file mode 100644 index 0000000..25488df --- /dev/null +++ b/src/main/java/org/treblereel/javascript/compiler/domain/LanguageInOut.java @@ -0,0 +1,80 @@ +/* + * Copyright © 2025 Treblereel + * + * 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.treblereel.javascript.compiler.domain; + +import java.util.Objects; + +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +@Schema(name = "LanguageInOut", + description = "LanguageInOut is a class that represents the input and output languages for JavaScript compilation.") +public class LanguageInOut { + + + @Schema(description = "Input language. Possible values: ECMASCRIPT3, ECMASCRIPT5, ECMASCRIPT5_STRICT, ECMASCRIPT_2015, ECMASCRIPT_2016, ECMASCRIPT_2017, ECMASCRIPT_2018, ECMASCRIPT_2019, ECMASCRIPT_2020, ECMASCRIPT_2021, ECMASCRIPT_NEXT and STABLE", + enumeration = {"ECMASCRIPT3", "ECMASCRIPT5", "ECMASCRIPT5_STRICT", "ECMASCRIPT_2015", "ECMASCRIPT_2016", "ECMASCRIPT_2017", "ECMASCRIPT_2018", "ECMASCRIPT_2019", "ECMASCRIPT_2020", "ECMASCRIPT_2021", "ECMASCRIPT_NEXT", "STABLE"}) + private String languageIn; + + @Schema(description = "Output language. Possible values: ECMASCRIPT3, ECMASCRIPT5, ECMASCRIPT5_STRICT, ECMASCRIPT_2015, ECMASCRIPT_2016, ECMASCRIPT_2017, ECMASCRIPT_2018, ECMASCRIPT_2019, ECMASCRIPT_2020, ECMASCRIPT_2021, ECMASCRIPT_NEXT and STABLE", + enumeration = {"ECMASCRIPT3", "ECMASCRIPT5", "ECMASCRIPT5_STRICT", "ECMASCRIPT_2015", "ECMASCRIPT_2016", "ECMASCRIPT_2017", "ECMASCRIPT_2018", "ECMASCRIPT_2019", "ECMASCRIPT_2020", "ECMASCRIPT_2021", "ECMASCRIPT_NEXT", "STABLE"}) + private String languageOut; + + public LanguageInOut() { + } + + public LanguageInOut(String languageIn, String languageOut) { + this.languageIn = languageIn; + this.languageOut = languageOut; + } + + public String getLanguageIn() { + return languageIn; + } + + public void setLanguageIn(String languageIn) { + this.languageIn = languageIn; + } + + public String getLanguageOut() { + return languageOut; + } + + public void setLanguageOut(String languageOut) { + this.languageOut = languageOut; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + LanguageInOut that = (LanguageInOut) o; + return Objects.equals(languageIn, that.languageIn) && Objects.equals(languageOut, that.languageOut); + } + + @Override + public int hashCode() { + return Objects.hash(languageIn, languageOut); + } + + @Override + public String toString() { + return "LanguageInOut{" + + "languageIn='" + languageIn + '\'' + + ", languageOut='" + languageOut + '\'' + + '}'; + } +} diff --git a/src/main/java/org/treblereel/javascript/compiler/domain/Statistics.java b/src/main/java/org/treblereel/javascript/compiler/domain/Statistics.java index 1f0019b..848f19a 100644 --- a/src/main/java/org/treblereel/javascript/compiler/domain/Statistics.java +++ b/src/main/java/org/treblereel/javascript/compiler/domain/Statistics.java @@ -19,14 +19,22 @@ import java.util.Objects; import io.quarkus.runtime.annotations.RegisterForReflection; +import org.eclipse.microprofile.openapi.annotations.media.Schema; @RegisterForReflection +@Schema( + name = "Statistics", + description = "Statistics is a class that represents the statistics of the compilation process.") public class Statistics { + @Schema(description = "Size of the original JavaScript code in bytes.") private long originalSize; + + @Schema(description = "Size of the compiled JavaScript code in bytes.") private long compiledSize; - public Statistics() {} + public Statistics() { + } public Statistics(long originalSize, long compiledSize) { this.originalSize = originalSize; diff --git a/src/main/java/org/treblereel/javascript/compiler/downloader/FileDownloader.java b/src/main/java/org/treblereel/javascript/compiler/downloader/FileDownloader.java index aa786f3..e34efdb 100644 --- a/src/main/java/org/treblereel/javascript/compiler/downloader/FileDownloader.java +++ b/src/main/java/org/treblereel/javascript/compiler/downloader/FileDownloader.java @@ -94,12 +94,9 @@ public String downloadFileToTemp(String fileUrl) throws IOException { if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) { long contentLength = connection.getContentLengthLong(); if (contentLength > serverConfig.downloadFileMaxSize()) { - throw new IOException( - "File is too large to download: " - + contentLength - + " bytes (max: " - + serverConfig.downloadFileMaxSize() - + " bytes)"); + throw new IOException(String.format( + "File %s is too large to download: %d bytes (max: %d bytes)", + fileUrl, contentLength, serverConfig.downloadFileMaxSize())); } try (InputStream inputStream = connection.getInputStream()) { @@ -112,11 +109,9 @@ public String downloadFileToTemp(String fileUrl) throws IOException { + (e.getMessage() != null ? " - " + e.getMessage() : "")); } } else { - throw new IOException( - "Failed to download file: HTTP " - + connection.getResponseCode() - + " - " - + connection.getResponseMessage()); + throw new IOException(String.format( + "Failed to download file %s: HTTP %d - %s", + fileUrl, connection.getResponseCode(), connection.getResponseMessage())); } } diff --git a/src/main/java/org/treblereel/javascript/compiler/rest/CompilerResource.java b/src/main/java/org/treblereel/javascript/compiler/rest/CompilerResource.java index a24ac6d..016ebb7 100644 --- a/src/main/java/org/treblereel/javascript/compiler/rest/CompilerResource.java +++ b/src/main/java/org/treblereel/javascript/compiler/rest/CompilerResource.java @@ -44,9 +44,22 @@ import com.google.javascript.jscomp.Result; import com.google.javascript.jscomp.SourceFile; import com.google.javascript.jscomp.WarningLevel; +import io.micrometer.core.annotation.Counted; +import io.micrometer.core.annotation.Timed; import io.vertx.ext.web.RoutingContext; import org.eclipse.microprofile.faulttolerance.Bulkhead; import org.eclipse.microprofile.faulttolerance.Timeout; +import org.eclipse.microprofile.openapi.annotations.OpenAPIDefinition; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.enums.ParameterIn; +import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; +import org.eclipse.microprofile.openapi.annotations.info.Info; +import org.eclipse.microprofile.openapi.annotations.media.Content; +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; +import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.servers.Server; import org.jboss.logging.Logger; import org.treblereel.javascript.compiler.cache.FileCache; import org.treblereel.javascript.compiler.config.ServerConfig; @@ -57,19 +70,33 @@ import org.treblereel.javascript.compiler.externs.ExternsProcessor; @Path("/compile") +@OpenAPIDefinition( + info = @Info( + title = "JavaScript Compiler API", + version = "0.5", + description = "API for compiling JavaScript code using Google Closure Compiler" + ), + servers = @Server(url = "https://jscompressor.treblereel.dev/", description = "Production server") +) public class CompilerResource { - @Inject ExternsProcessor externsProcessor; + @Inject + ExternsProcessor externsProcessor; - @Inject RoutingContext context; + @Inject + RoutingContext context; - @Inject Logger logger; + @Inject + Logger logger; - @Inject FileDownloader fileDownloader; + @Inject + FileDownloader fileDownloader; - @Inject FileCache cache; + @Inject + FileCache cache; - @Inject ServerConfig serverConfig; + @Inject + ServerConfig serverConfig; @PostConstruct public void init() { @@ -83,6 +110,27 @@ public void init() { @Produces(MediaType.APPLICATION_JSON) @Bulkhead(value = 5, waitingTaskQueue = 5) @Timeout(value = 120, unit = ChronoUnit.SECONDS) + @Timed(value = "compile", extraTags = {"method", "POST"}, description = "Time taken to compile") + @Counted(value = "compile", extraTags = {"method", "POST"}, description = "Number of compilations") + @Operation( + summary = "Compile JavaScript code", + description = "Compiles JavaScript code using Google Closure Compiler", + operationId = "compileJavaScript" + ) + @RequestBody( + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = CompileRequest.class) + ) + ) + @APIResponse( + responseCode = "200", + description = "OK", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = CompileResponse.class) + ) + ) public Response compile(@Valid CompileRequest request) { logger.info("received request from " + context.request().remoteAddress().host()); @@ -127,16 +175,26 @@ public Response compile(@Valid CompileRequest request) { externalSourceSize += file.getCode().length(); } catch (IOException e) { return Response.status(Response.Status.BAD_REQUEST) - .entity(Map.of("error", "Failed to download file: " + e.getMessage())) - .type(MediaType.APPLICATION_JSON) - .build(); + .entity(Map.of("error", "Failed to download file: " + e.getMessage())) + .type(MediaType.APPLICATION_JSON) + .build(); } } options.setEnvironment(CompilerOptions.Environment.BROWSER); options.setDependencyOptions(DependencyOptions.sortOnly()); - options.setLanguageIn(CompilerOptions.LanguageMode.ECMASCRIPT_2021); - options.setLanguageOut(CompilerOptions.LanguageMode.ECMASCRIPT_2021); + + if (request.getLanguage() == null) { + options.setLanguageIn(CompilerOptions.LanguageMode.ECMASCRIPT_2021); + options.setLanguageOut(CompilerOptions.LanguageMode.ECMASCRIPT_2021); + } else { + if (request.getLanguage().getLanguageIn() != null) { + options.setLanguageIn(CompilerOptions.LanguageMode.fromString(request.getLanguage().getLanguageIn())); + } + if (request.getLanguage().getLanguageOut() != null) { + options.setLanguageOut(CompilerOptions.LanguageMode.fromString(request.getLanguage().getLanguageOut())); + } + } if (request.getFormatting() != null) { options.setPrettyPrint(request.getFormatting().prettyPrint); @@ -162,9 +220,9 @@ public Response compile(@Valid CompileRequest request) { cache.put(hash, compiler.toSource().getBytes(StandardCharsets.UTF_8)); } catch (Exception e) { return Response.status(Response.Status.BAD_REQUEST) - .entity(Map.of("error", "Failed to cache compiled code: " + e.getMessage())) - .type(MediaType.APPLICATION_JSON) - .build(); + .entity(Map.of("error", "Failed to cache compiled code: " + e.getMessage())) + .type(MediaType.APPLICATION_JSON) + .build(); } response.setDownloadId(hash); } else { @@ -172,9 +230,9 @@ public Response compile(@Valid CompileRequest request) { } List warnings = - result.warnings.stream().map(JSError::toString).collect(Collectors.toList()); + result.warnings.stream().map(JSError::toString).collect(Collectors.toList()); List errors = - result.errors.stream().map(JSError::toString).collect(Collectors.toList()); + result.errors.stream().map(JSError::toString).collect(Collectors.toList()); response.setWarnings(warnings); response.setErrors(errors); @@ -182,17 +240,17 @@ public Response compile(@Valid CompileRequest request) { Statistics stats = new Statistics(); stats.setOriginalSize(request.getPayload().length() + externalSourceSize); stats.setCompiledSize( - response.getCompiledCode() != null ? response.getCompiledCode().length() : 0); + response.getCompiledCode() != null ? response.getCompiledCode().length() : 0); response.setStatistics(stats); logger.info( - "compilation took " - + (System.currentTimeMillis() - start) - + "ms, payload size: " - + stats.getOriginalSize() - + " bytes, compiled size: " - + stats.getCompiledSize() - + " bytes"); + "compilation took " + + (System.currentTimeMillis() - start) + + "ms, payload size: " + + stats.getOriginalSize() + + " bytes, compiled size: " + + stats.getCompiledSize() + + " bytes"); return Response.ok(response).build(); } @@ -201,22 +259,44 @@ public Response compile(@Valid CompileRequest request) { @Produces(MediaType.APPLICATION_OCTET_STREAM) @Bulkhead(value = 10, waitingTaskQueue = 5) @Timeout(value = 20, unit = ChronoUnit.SECONDS) + @Timed(value = "read", extraTags = {"method", "GET"}, description = "Time taken to read") + @Counted(value = "read", extraTags = {"method", "GET"}, description = "Number of reads") + @Operation( + summary = "Fetch compiled code", + description = "Reads compiled code from cache using the provided hash", + operationId = "readCompiledCode" + ) + @Parameter( + name = "hash", + description = "Hash‑code of the compiled JavaScript file", + required = true, + in = ParameterIn.PATH, + schema = @Schema(type = SchemaType.STRING) + ) + @APIResponse( + responseCode = "200", + description = "Javascript file (binary)", + content = @Content( + mediaType = "application/octet-stream", + schema = @Schema(type = SchemaType.STRING, format = "binary") + ) + ) public Response read(@PathParam("hash") String hash) { byte[] bytes = new byte[0]; try { bytes = cache.get(hash); } catch (Exception e) { return Response.status(Response.Status.NOT_FOUND) - .entity("No compiled code found for hash " + hash) - .build(); + .entity("No compiled code found for hash " + hash) + .build(); } if (bytes == null) { return Response.status(Response.Status.NOT_FOUND) - .entity("No compiled code found for hash " + hash) - .build(); + .entity("No compiled code found for hash " + hash) + .build(); } return Response.ok(bytes) - .header("Content-Disposition", "attachment; filename=\"default.js\"") - .build(); + .header("Content-Disposition", "attachment; filename=\"default.js\"") + .build(); } } diff --git a/src/main/java/org/treblereel/javascript/compiler/validation/JsLanguageLevel.java b/src/main/java/org/treblereel/javascript/compiler/validation/JsLanguageLevel.java new file mode 100644 index 0000000..e7427ef --- /dev/null +++ b/src/main/java/org/treblereel/javascript/compiler/validation/JsLanguageLevel.java @@ -0,0 +1,30 @@ +/* + * Copyright © 2025 Treblereel + * + * 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.treblereel.javascript.compiler.validation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import jakarta.validation.Constraint; + +@Constraint(validatedBy = JsLanguageLevelValidator.class) +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface JsLanguageLevel { +} diff --git a/src/main/java/org/treblereel/javascript/compiler/validation/JsLanguageLevelValidator.java b/src/main/java/org/treblereel/javascript/compiler/validation/JsLanguageLevelValidator.java new file mode 100644 index 0000000..53420e9 --- /dev/null +++ b/src/main/java/org/treblereel/javascript/compiler/validation/JsLanguageLevelValidator.java @@ -0,0 +1,49 @@ +/* + * Copyright © 2025 Treblereel + * + * 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.treblereel.javascript.compiler.validation; + +import java.util.Set; + +import jakarta.validation.ConstraintValidator; + +import com.google.javascript.jscomp.CompilerOptions; + +public class JsLanguageLevelValidator implements ConstraintValidator { + + private Set languageModes = Set.of(CompilerOptions.LanguageMode.ECMASCRIPT3, + CompilerOptions.LanguageMode.ECMASCRIPT5, + CompilerOptions.LanguageMode.ECMASCRIPT5_STRICT, + CompilerOptions.LanguageMode.ECMASCRIPT_2015, + CompilerOptions.LanguageMode.ECMASCRIPT_2016, + CompilerOptions.LanguageMode.ECMASCRIPT_2017, + CompilerOptions.LanguageMode.ECMASCRIPT_2018, + CompilerOptions.LanguageMode.ECMASCRIPT_2019, + CompilerOptions.LanguageMode.ECMASCRIPT_2020, + CompilerOptions.LanguageMode.ECMASCRIPT_2021, + CompilerOptions.LanguageMode.ECMASCRIPT_NEXT, + CompilerOptions.LanguageMode.STABLE, + CompilerOptions.LanguageMode.ECMASCRIPT_NEXT); + + @Override + public boolean isValid(String value, jakarta.validation.ConstraintValidatorContext context) { + if (value == null || value.isEmpty()) { + return true; + } + // Add your validation logic here + return languageModes.contains(CompilerOptions.LanguageMode.fromString(value)); + } +} diff --git a/src/main/java/org/treblereel/javascript/compiler/validation/MaxExternalUrlsValidator.java b/src/main/java/org/treblereel/javascript/compiler/validation/MaxExternalUrlsValidator.java index 63e1379..e25ee7e 100644 --- a/src/main/java/org/treblereel/javascript/compiler/validation/MaxExternalUrlsValidator.java +++ b/src/main/java/org/treblereel/javascript/compiler/validation/MaxExternalUrlsValidator.java @@ -20,6 +20,7 @@ import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; +import jakarta.inject.Provider; import jakarta.validation.ConstraintValidator; import jakarta.validation.ConstraintValidatorContext; @@ -29,13 +30,13 @@ public class MaxExternalUrlsValidator implements ConstraintValidator> { - @Inject ServerConfig serverConfig; + @Inject Provider serverConfig; @Override public boolean isValid(List urls, ConstraintValidatorContext context) { if (urls == null) { return true; } - return urls.size() <= serverConfig.downloadUrlsPreRequest(); + return urls.size() <= serverConfig.get().downloadUrlsPreRequest(); } } diff --git a/src/main/resources/META-INF/resources/buildin/externs.zip b/src/main/resources/META-INF/resources/buildin/externs.zip index d7d2f87..38f4a6c 100644 Binary files a/src/main/resources/META-INF/resources/buildin/externs.zip and b/src/main/resources/META-INF/resources/buildin/externs.zip differ diff --git a/src/main/resources/META-INF/resources/index.html b/src/main/resources/META-INF/resources/index.html index e15ea23..aa05eff 100644 --- a/src/main/resources/META-INF/resources/index.html +++ b/src/main/resources/META-INF/resources/index.html @@ -1,5 +1,5 @@ - + @@ -47,7 +47,10 @@ optimization: 'Whitespace only', prettyPrint: false, printInputDelimiter: false, + languageIn: 'ECMASCRIPT_2021', + languageOut: 'ECMASCRIPT_2021', selectedKeys: [], + showFormatting: false, loading: false, success: false, @@ -84,6 +87,10 @@ compilationLevel: this.optimization, warningLevel: 'DEFAULT', outputFileName: 'default.js', + language: { + languageIn: this.languageIn, + languageOut: this.languageOut + }, formatting: { prettyPrint: this.prettyPrint, printInputDelimiter: this.printInputDelimiter @@ -341,25 +348,105 @@
-

Formatting:

-
- + - +
+ +
+

Formatting:

+
+ + + +
+
+ + +
+

JavaScript Versions:

+ +
+ +
+ + +

Input JavaScript code version

+
+ + +
+ + +

Generated JavaScript code version

+
+
+
diff --git a/src/main/resources/META-INF/resources/openapi.yaml b/src/main/resources/META-INF/resources/openapi.yaml index df840e2..65be3cb 100644 --- a/src/main/resources/META-INF/resources/openapi.yaml +++ b/src/main/resources/META-INF/resources/openapi.yaml @@ -43,6 +43,10 @@ paths: components: schemas: + LanguageInOut: + type: string + description: "Language input and output" + Formatting: type: "object" properties: @@ -102,3 +106,5 @@ components: $ref: "#/components/schemas/Formatting" externalScripts: $ref: "#/components/schemas/ExternalScripts" + language: + $ref: "#/components/schemas/LanguageInOut" diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 455d526..f021689 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -12,9 +12,13 @@ quarkus.http.cors.origins=* quarkus.http.cors.methods=GET,POST quarkus.http.cors.headers=Content-Type,Accept +//quarkus.micrometer.enabled=false +quarkus.micrometer.binders.jvm.enabled=false + server.download-file-max-size=${MAX_DOWNLOAD_FILE_SIZE:1048576} server.download-urls-pre-request=${MAX_DOWNLOAD_URLS_PER_REQUEST:10} server.cache-max-size = ${MAX_CACHE_DIR_SIZE:1073741824} server.cache-location=${CACHE_DIR:cache} +