findDownloaded(String version) throws IOException {
+ var platform = Platform.guess();
+ var executablePath = getExecutablePath(version, platform);
+ logger.debug("Checking rome executable at {}", executablePath);
+ return checkFileWithChecksum(executablePath) ? Optional.ofNullable(executablePath) : Optional.empty();
+ }
+
+ /**
+ * Checks whether the given file exists and matches the checksum. The checksum
+ * must be contained in a file next to the file to check.
+ *
+ * @param filePath File to check.
+ * @return true
if the file exists and matches the checksum,
+ * false
otherwise.
+ */
+ private boolean checkFileWithChecksum(Path filePath) {
+ if (!Files.exists(filePath)) {
+ logger.debug("File '{}' does not exist yet", filePath);
+ return false;
+ }
+ if (Files.isDirectory(filePath)) {
+ logger.debug("File '{}' exists, but is a directory", filePath);
+ return false;
+ }
+ var checksumPath = getChecksumPath(filePath);
+ if (!Files.exists(checksumPath)) {
+ logger.debug("File '{}' exists, but checksum file '{}' does not", filePath, checksumPath);
+ return false;
+ }
+ if (Files.isDirectory(checksumPath)) {
+ logger.debug("Checksum file '{}' exists, but is a directory", checksumPath);
+ return false;
+ }
+ try {
+ var actualChecksum = computeChecksum(filePath, CHECKSUM_ALGORITHM);
+ var expectedChecksum = readTextFile(checksumPath, StandardCharsets.ISO_8859_1);
+ logger.debug("Expected checksum: {}, actual checksum: {}", expectedChecksum, actualChecksum);
+ return Objects.equals(expectedChecksum, actualChecksum);
+ } catch (final IOException ignored) {
+ return false;
+ }
+ }
+
+ /**
+ * Computes the checksum of the given file.
+ *
+ * @param file File to process.
+ * @param algorithm The checksum algorithm to use.
+ * @return The checksum of the given file.
+ * @throws IOException When the file does not exist or could not be read.
+ */
+ private String computeChecksum(Path file, String algorithm) throws IOException {
+ var buffer = new byte[4192];
+ try (var in = Files.newInputStream(file, READ_OPTIONS)) {
+ var digest = MessageDigest.getInstance(algorithm);
+ int result;
+ while ((result = in.read(buffer, 0, buffer.length)) != -1) {
+ digest.update(buffer, 0, result);
+ }
+ var bytes = digest.digest();
+ return String.format("%0" + (bytes.length * 2) + "X", new BigInteger(1, bytes));
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Finds the code name for the given operating system used by the Rome
+ * executable download URL.
+ *
+ * @param os Desired operating system.
+ * @return Code name for the Rome download URL.
+ * @throws IOException When the given OS is not supported by Rome.
+ */
+ private String getArchitectureCodeName(Architecture architecture) throws IOException {
+ switch (architecture) {
+ case ARM64:
+ return "arm64";
+ case X64:
+ return "x64";
+ default:
+ throw new IOException("Unsupported architecture: " + architecture);
+ }
+ }
+
+ /**
+ * Derives a path for the file which contains the checksum of the given file.
+ *
+ * @param file A file for which to derive the checksum file path.
+ * @return The path with the checksum for the given file.
+ */
+ private Path getChecksumPath(Path file) {
+ var parent = file.getParent();
+ var base = parent != null ? parent : file;
+ var fileName = file.getFileName();
+ var checksumName = fileName != null ? fileName.toString() + ".sha256" : "checksum.sha256";
+ return base.resolve(checksumName);
+ }
+
+ /**
+ * Finds the URL from which the Rome executable can be downloaded.
+ *
+ * @param version Desired Rome version.
+ * @param platform Desired platform.
+ * @return The URL for the Rome executable.
+ * @throws IOException When the platform is not supported by Rome.
+ */
+ private String getDownloadUrl(String version, Platform platform) throws IOException {
+ var osCodeName = getOsCodeName(platform.getOs());
+ var architectureCodeName = getArchitectureCodeName(platform.getArchitecture());
+ var extension = getDownloadUrlExtension(platform.getOs());
+ var platformString = String.format(PLATFORM_PATTERN, osCodeName, architectureCodeName, extension);
+ return String.format(URL_PATTERN, version, platformString);
+ }
+
+ /**
+ * Finds the file extension of the Rome download URL for the given operating
+ * system.
+ *
+ * @param os Desired operating system.
+ * @return Extension for the Rome download URL.
+ * @throws IOException When the given OS is not supported by Rome.
+ */
+ private String getDownloadUrlExtension(OS os) throws IOException {
+ switch (os) {
+ case LINUX:
+ return "";
+ case MAC_OS:
+ return "";
+ case WINDOWS:
+ return ".exe";
+ default:
+ throw new IOException("Unsupported OS: " + os);
+ }
+ }
+
+ /**
+ * Finds the path on the file system for the Rome executable with a given
+ * version and platform.
+ *
+ * @param version Desired Rome version.
+ * @param platform Desired platform.
+ * @return The path for the Rome executable.
+ */
+ private Path getExecutablePath(String version, Platform platform) {
+ var os = platform.getOs().name().toLowerCase(Locale.ROOT);
+ var arch = platform.getArchitecture().name().toLowerCase(Locale.ROOT);
+ var fileName = String.format(DOWNLOAD_FILE_PATTERN, os, arch, version);
+ return downloadDir.resolve(fileName);
+ }
+
+ /**
+ * Finds the code name for the given operating system used by the Rome
+ * executable download URL.
+ *
+ * @param os Desired operating system.
+ * @return Code name for the Rome download URL.
+ * @throws IOException When the given OS is not supported by Rome.
+ */
+ private String getOsCodeName(OS os) throws IOException {
+ switch (os) {
+ case LINUX:
+ return "linux";
+ case MAC_OS:
+ return "darwin";
+ case WINDOWS:
+ return "win32";
+ default:
+ throw new IOException("Unsupported OS: " + os);
+ }
+ }
+
+ /**
+ * Reads a plain text file with the given encoding into a string.
+ *
+ * @param file File to read.
+ * @param charset Encoding to use.
+ * @return The contents of the file as a string.
+ * @throws IOException When the file could not be read.
+ */
+ private String readTextFile(Path file, Charset charset) throws IOException {
+ try (var in = Files.newInputStream(file, READ_OPTIONS)) {
+ return new String(in.readAllBytes(), charset);
+ }
+ }
+
+ /**
+ * Computes the checksum of the given file and writes it to the target checksum
+ * file, using the {@code ISO_8859_1} encoding.
+ *
+ * @param file
+ * @param checksumPath
+ * @throws IOException
+ */
+ private void writeChecksumFile(Path file, Path checksumPath) throws IOException {
+ var checksum = computeChecksum(file, CHECKSUM_ALGORITHM);
+ try (var out = Files.newOutputStream(checksumPath, WRITE_OPTIONS)) {
+ out.write(checksum.getBytes(StandardCharsets.ISO_8859_1));
+ }
+ }
+}
diff --git a/lib/src/main/java/com/diffplug/spotless/rome/RomeStep.java b/lib/src/main/java/com/diffplug/spotless/rome/RomeStep.java
new file mode 100644
index 0000000000..d6a3e62669
--- /dev/null
+++ b/lib/src/main/java/com/diffplug/spotless/rome/RomeStep.java
@@ -0,0 +1,475 @@
+/*
+ * Copyright 2016-2023 DiffPlug
+ *
+ * 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 com.diffplug.spotless.rome;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.Serializable;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.attribute.PosixFilePermission;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.diffplug.spotless.FileSignature;
+import com.diffplug.spotless.ForeignExe;
+import com.diffplug.spotless.FormatterFunc;
+import com.diffplug.spotless.FormatterStep;
+import com.diffplug.spotless.ProcessRunner;
+
+/**
+ * formatter step that formats JavaScript and TypeScript code with Rome:
+ * https://github.com/rome/tools.
+ * It delegates to the Rome executable. The Rome executable is downloaded from
+ * the network when no executable path is provided explicitly.
+ */
+public class RomeStep {
+ private static final Logger logger = LoggerFactory.getLogger(RomeStep.class);
+
+ /**
+ * Path to the directory with the {@code rome.json} config file, can be
+ * null
, in which case the defaults are used.
+ */
+ private String configPath;
+
+ /**
+ * The language (syntax) of the input files to format. When null
or
+ * the empty string, the language is detected automatically from the file name.
+ * Currently the following languages are supported by Rome:
+ *
+ * - js (JavaScript)
+ * - jsx (JavaScript + JSX)
+ * - js? (JavaScript or JavaScript + JSX, depending on the file
+ * extension)
+ * - ts (TypeScript)
+ * - tsx (TypeScript + JSX)
+ * - ts? (TypeScript or TypeScript + JSX, depending on the file
+ * extension)
+ * - json (JSON)
+ *
+ */
+ private String language;
+
+ /**
+ * Path to the Rome executable. Can be null
, but either a path to
+ * the executable of a download directory and version must be given. The path
+ * must be either an absolute path, or a file name without path separators. If
+ * the latter, it is interpreted as a command in the user's path.
+ */
+ private final String pathToExe;
+
+ /**
+ * Absolute path to the download directory for storing the download Rome
+ * executable. Can be null
, but either a path to the executable of
+ * a download directory and version must be given.
+ */
+ private final String downloadDir;
+
+ /**
+ * Version of Rome to download. Can be null
, but either a path to
+ * the executable of a download directory and version must be given.
+ */
+ private final String version;
+
+ /**
+ * @return The name of this format step, i.e. {@code rome}.
+ */
+ public static String name() {
+ return "rome";
+ }
+
+ /**
+ * Creates a Rome step that format code by downloading to the given Rome
+ * version. The executable is downloaded from the network.
+ *
+ * @param version Version of the Rome executable to download.
+ * @param downloadDir Directory where to place the downloaded executable.
+ * @return A new Rome step that download the executable from the network.
+ */
+ public static RomeStep withExeDownload(String version, String downloadDir) {
+ return new RomeStep(version, null, downloadDir);
+ }
+
+ /**
+ * Creates a Rome step that formats code by delegating to the Rome executable
+ * located at the given path.
+ *
+ * @param pathToExe Path to the Rome executable to use.
+ * @return A new Rome step that format with the given executable.
+ */
+ public static RomeStep withExePath(String pathToExe) {
+ return new RomeStep(null, pathToExe, null);
+ }
+
+ /**
+ * Attempts to add a POSIX permission to the given file, ignoring any errors.
+ * All existing permissions on the file are preserved and the new permission is
+ * added, if possible.
+ *
+ * @param file File or directory to which to add a permission.
+ * @param permission The POSIX permission to add.
+ */
+ private static void attemptToAddPosixPermission(Path file, PosixFilePermission permission) {
+ try {
+ var newPermissions = new HashSet<>(Files.getPosixFilePermissions(file));
+ newPermissions.add(permission);
+ Files.setPosixFilePermissions(file, newPermissions);
+ } catch (final Exception ignore) {
+ logger.debug("Unable to add POSIX permission '{}' to file '{}'", permission, file);
+ }
+ }
+
+ /**
+ * Finds the default version for Rome when no version is specified explicitly.
+ * Over time this will become outdated -- people should always specify the
+ * version explicitly!
+ *
+ * @return The default version for Rome.
+ */
+ private static String defaultVersion() {
+ return "12.0.0";
+ }
+
+ /**
+ * Attempts to make the given file executable. This is a best-effort attempt,
+ * any errors are swallowed. Depending on the OS, the file might still be
+ * executable even if this method fails. The user will get a descriptive error
+ * later when we attempt to execute the Rome executable.
+ *
+ * @param filePath Path to the file to make executable.
+ */
+ private static void makeExecutable(String filePath) {
+ var exePath = Paths.get(filePath);
+ attemptToAddPosixPermission(exePath, PosixFilePermission.GROUP_EXECUTE);
+ attemptToAddPosixPermission(exePath, PosixFilePermission.OTHERS_EXECUTE);
+ attemptToAddPosixPermission(exePath, PosixFilePermission.OWNER_EXECUTE);
+ }
+
+ /**
+ * Finds the absolute path of a command on the user's path. Uses {@code which}
+ * for Linux and {@code where} for Windows.
+ *
+ * @param name Name of the command to resolve.
+ * @return The absolute path of the command's executable.
+ * @throws IOException When the command could not be resolved.
+ * @throws InterruptedException When this thread was interrupted while waiting
+ * to the which command to finish.
+ */
+ private static String resolveNameAgainstPath(String name) throws IOException, InterruptedException {
+ try (var runner = new ProcessRunner()) {
+ var cmdWhich = runner.shellWinUnix("where " + name, "which " + name);
+ if (cmdWhich.exitNotZero()) {
+ throw new IOException("Unable to find " + name + " on path via command " + cmdWhich);
+ } else {
+ return cmdWhich.assertExitZero(Charset.defaultCharset()).trim();
+ }
+ }
+ }
+
+ /**
+ * Checks the Rome config path. When the config path does not exist or when it
+ * does not contain a file named {@code rome.json}, an error is thrown.
+ */
+ private static void validateRomeConfigPath(String configPath) {
+ if (configPath == null) {
+ return;
+ }
+ var path = Paths.get(configPath);
+ var config = path.resolve("rome.json");
+ if (!Files.exists(path)) {
+ throw new IllegalArgumentException("Rome config directory does not exist: " + path);
+ }
+ if (!Files.exists(config)) {
+ throw new IllegalArgumentException("Rome config does not exist: " + config);
+ }
+ }
+
+ /**
+ * Checks the Rome executable file. When the file does not exist, an error is
+ * thrown.
+ */
+ private static void validateRomeExecutable(String resolvedPathToExe) {
+ if (!new File(resolvedPathToExe).isFile()) {
+ throw new IllegalArgumentException("Rome executable does not exist: " + resolvedPathToExe);
+ }
+ }
+
+ /**
+ * Creates a new Rome step with the configuration from the given builder.
+ *
+ * @param builder Builder with the configuration to use.
+ */
+ private RomeStep(String version, String pathToExe, String downloadDir) {
+ this.version = version != null && !version.isBlank() ? version : defaultVersion();
+ this.pathToExe = pathToExe;
+ this.downloadDir = downloadDir;
+ }
+
+ /**
+ * Creates a formatter step with the current configuration, which formats code
+ * by passing it to the Rome executable.
+ *
+ * @return A new formatter step for formatting with Rome.
+ */
+ public FormatterStep create() {
+ return FormatterStep.createLazy(name(), this::createState, State::toFunc);
+ }
+
+ /**
+ * Sets the path to the directory with the {@code rome.json} config file. When
+ * no config path is set, the default configuration is used.
+ *
+ * @param configPath Config path to use. Must point to a directory which contain
+ * a file named {@code rome.json}.
+ * @return This builder instance for chaining method calls.
+ */
+ public RomeStep withConfigPath(String configPath) {
+ this.configPath = configPath;
+ return this;
+ }
+
+ /**
+ * Sets the language of the files to format When no language is set, it is
+ * determined automatically from the file name. The following languages are
+ * currently supported by Rome.
+ *
+ *
+ * - js (JavaScript)
+ * - jsx (JavaScript + JSX)
+ * - js? (JavaScript or JavaScript + JSX, depending on the file
+ * extension)
+ * - ts (TypeScript)
+ * - tsx (TypeScript + JSX)
+ * - ts? (TypeScript or TypeScript + JSX, depending on the file
+ * extension)
+ * - json (JSON)
+ *
+ *
+ * @param language The language of the files to format.
+ * @return This builder instance for chaining method calls.
+ */
+ public RomeStep withLanguage(String language) {
+ this.language = language;
+ return this;
+ }
+
+ /**
+ * Resolves the Rome executable, possibly downloading it from the network, and
+ * creates a new state instance with the resolved executable that can format
+ * code via Rome.
+ *
+ * @return The state instance for formatting code via Rome.
+ * @throws IOException When any file system or network operations
+ * failed, such as when the Rome executable could
+ * not be downloaded, or when the given executable
+ * does not exist.
+ * @throws InterruptedException When the Rome executable needs to be downloaded
+ * and this thread was interrupted while waiting
+ * for the download to complete.
+ */
+ private State createState() throws IOException, InterruptedException {
+ var resolvedPathToExe = resolveExe();
+ validateRomeExecutable(resolvedPathToExe);
+ validateRomeConfigPath(configPath);
+ logger.debug("Using Rome executable located at '{}'", resolvedPathToExe);
+ var exeSignature = FileSignature.signAsList(Collections.singleton(new File(resolvedPathToExe)));
+ makeExecutable(resolvedPathToExe);
+ return new State(resolvedPathToExe, exeSignature, configPath, language);
+ }
+
+ /**
+ * Resolves the path to the Rome executable, given the configuration of this
+ * step. When the path to the Rome executable is given explicitly, that path is
+ * used as-is. Otherwise, at attempt is made to download the Rome executable for
+ * the configured version from the network, unless it was already downloaded and
+ * is available in the cache.
+ *
+ * @return The path to the resolved Rome executable.
+ * @throws IOException When any file system or network operations
+ * failed, such as when the Rome executable could
+ * not be downloaded.
+ * @throws InterruptedException When the Rome executable needs to be downloaded
+ * and this thread was interrupted while waiting
+ * for the download to complete.
+ */
+ private String resolveExe() throws IOException, InterruptedException {
+ new ForeignExe();
+ if (pathToExe != null) {
+ if (Paths.get(pathToExe).getNameCount() == 1) {
+ return resolveNameAgainstPath(pathToExe);
+ } else {
+ return pathToExe;
+ }
+ } else {
+ var downloader = new RomeExecutableDownloader(Paths.get(downloadDir));
+ var downloaded = downloader.ensureDownloaded(version).toString();
+ makeExecutable(downloaded);
+ return downloaded;
+ }
+ }
+
+ /**
+ * The internal state used by the Rome formatter. A state instance is created
+ * when the spotless plugin for Maven or Gradle is executed, and reused for all
+ * formatting requests for different files. The lifetime of the instance ends
+ * when the Maven or Gradle plugin was successfully executed.
+ *
+ * The state encapsulated a particular executable. It is serializable for
+ * caching purposes. Spotless keeps a cache of which files need to be formatted.
+ * The cache is busted when the serialized form of a state instance changes.
+ */
+ private static class State implements Serializable {
+ private static final long serialVersionUID = 6846790911054484379L;
+
+ /** Path to the exe file */
+ private final String pathToExe;
+
+ /** The signature of the exe file, if any, used for caching. */
+ @SuppressWarnings("unused")
+ private final FileSignature exeSignature;
+
+ /**
+ * The optional path to the directory with the {@code rome.json} config file.
+ */
+ private final String configPath;
+
+ /**
+ * The language of the files to format. When null
or the empty
+ * string, the language is detected from the file name.
+ */
+ private final String language;
+
+ /**
+ * Creates a new state for instance which can format code with the given Rome
+ * executable.
+ *
+ * @param exe Path to the Rome executable.
+ * @param exeSignature Signature (e.g. SHA-256 checksum) of the Rome executable.
+ * @param configPath Path to the optional directory with the {@code rome.json}
+ * config file, can be null
, in which case the
+ * defaults are used.
+ */
+ private State(String exe, FileSignature exeSignature, String configPath, String language) {
+ this.pathToExe = exe;
+ this.exeSignature = exeSignature;
+ this.configPath = configPath;
+ this.language = language;
+ }
+
+ /**
+ * Builds the list of arguments for the command that executes Rome to format a
+ * piece of code passed via stdin.
+ *
+ * @param file File to format.
+ * @return The Rome command to use for formatting code.
+ */
+ private String[] buildRomeCommand(File file) {
+ var fileName = resolveFileName(file);
+ var argList = new ArrayList();
+ argList.add(pathToExe);
+ argList.add("format");
+ argList.add("--stdin-file-path");
+ argList.add(fileName);
+ if (configPath != null) {
+ argList.add("--config-path");
+ argList.add(configPath);
+ }
+ return argList.toArray(String[]::new);
+ }
+
+ /**
+ * Formats the given piece of code by delegating to the Rome executable. The
+ * code is passed to Rome via stdin, the file name is used by Rome only to
+ * determine the code syntax (e.g. JavaScript or TypeScript).
+ *
+ * @param runner Process runner for invoking the Rome executable.
+ * @param input Code to format.
+ * @param file File to format.
+ * @return The formatted code.
+ * @throws IOException When a file system error occurred while
+ * executing Rome.
+ * @throws InterruptedException When this thread was interrupted while waiting
+ * for Rome to finish formatting.
+ */
+ private String format(ProcessRunner runner, String input, File file) throws IOException, InterruptedException {
+ var stdin = input.getBytes(StandardCharsets.UTF_8);
+ var args = buildRomeCommand(file);
+ if (logger.isDebugEnabled()) {
+ logger.debug("Running Rome comand to format code: '{}'", String.join(", ", args));
+ }
+ return runner.exec(stdin, args).assertExitZero(StandardCharsets.UTF_8);
+ }
+
+ /**
+ * The Rome executable currently does not have a parameter to specify the
+ * expected language / syntax. Rome always determined the language from the file
+ * extension. This method returns the file name for the desired language when a
+ * language was requested explicitly, or the file name of the input file for
+ * auto detection.
+ *
+ * @param file File to be formatted.
+ * @return The file name to pass to the Rome executable.
+ */
+ private String resolveFileName(File file) {
+ var name = file.getName();
+ if (language == null || language.isBlank()) {
+ return name;
+ }
+ var dot = name.lastIndexOf(".");
+ var ext = dot >= 0 ? name.substring(dot + 1) : name;
+ switch (language) {
+ case "js?":
+ return "jsx".equals(ext) || "js".equals(ext) || "mjs".equals(ext) || "cjs".equals(ext) ? name
+ : "file.js";
+ case "ts?":
+ return "tsx".equals(ext) || "ts".equals(ext) || "mts".equals(ext) || "cts".equals(ext) ? name
+ : "file.js";
+ case "js":
+ return "js".equals(ext) || "mjs".equals(ext) || "cjs".equals(ext) ? name : "file.js";
+ case "jsx":
+ return "jsx".equals(ext) ? name : "file.jsx";
+ case "ts":
+ return "ts".equals(ext) || "mts".equals(ext) || "cts".equals(ext) ? name : "file.ts";
+ case "tsx":
+ return "tsx".equals(ext) ? name : "file.tsx";
+ case "json":
+ return "json".equals(ext) ? name : "file.json";
+ // so that we can support new languages such as css or yaml when Rome adds
+ // support for them without having to change the code
+ default:
+ return "file." + language;
+ }
+ }
+
+ /**
+ * Creates a new formatter function for formatting a piece of code by delegating
+ * to the Rome executable.
+ *
+ * @return A formatter function for formatting code.
+ */
+ private FormatterFunc.Closeable toFunc() {
+ var runner = new ProcessRunner();
+ return FormatterFunc.Closeable.of(runner, this::format);
+ }
+ }
+}
diff --git a/plugin-gradle/CHANGES.md b/plugin-gradle/CHANGES.md
index 0be3e2584d..5d68193da9 100644
--- a/plugin-gradle/CHANGES.md
+++ b/plugin-gradle/CHANGES.md
@@ -3,8 +3,10 @@
We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `3.27.0`).
## [Unreleased]
+### Added
+* Support Rome as a formatter for JavaScript and TypeScript code. Adds a new `rome` step to `javascript` and `typescript` formatter configurations. ([#1663](https://github.com/diffplug/spotless/pull/1663))
### Fixed
-* Added `@DisableCachingByDefault` to `RegisterDependenciesTask`.
+* Added `@DisableCachingByDefault` to `RegisterDependenciesTask`. ([#1666](https://github.com/diffplug/spotless/pull/1666))
* When P2 download fails, indicate the responsible formatter. ([#1698](https://github.com/diffplug/spotless/issues/1698))
### Changes
* Bump default sortpom version to latest `3.0.0` -> `3.2.1`. ([#1675](https://github.com/diffplug/spotless/pull/1675))
diff --git a/plugin-gradle/README.md b/plugin-gradle/README.md
index d84645e51d..c853f82a3f 100644
--- a/plugin-gradle/README.md
+++ b/plugin-gradle/README.md
@@ -63,8 +63,8 @@ Spotless supports all of Gradle's built-in performance features (incremental bui
- [FreshMark](#freshmark) aka markdown
- [Antlr4](#antlr4) ([antlr4formatter](#antlr4formatter))
- [SQL](#sql) ([dbeaver](#dbeaver), [prettier](#prettier))
- - [Typescript](#typescript) ([tsfmt](#tsfmt), [prettier](#prettier), [ESLint](#eslint-typescript))
- - [Javascript](#javascript) ([prettier](#prettier), [ESLint](#eslint-javascript))
+ - [Typescript](#typescript) ([tsfmt](#tsfmt), [prettier](#prettier), [ESLint](#eslint-typescript), [Rome](#rome))
+ - [Javascript](#javascript) ([prettier](#prettier), [ESLint](#eslint-javascript), [Rome](#rome))
- [JSON](#json)
- [YAML](#yaml)
- [Gherkin](#gherkin)
@@ -75,6 +75,7 @@ Spotless supports all of Gradle's built-in performance features (incremental bui
- c, c++, c#, objective-c, protobuf, javascript, java
- [eclipse web tools platform](#eclipse-web-tools-platform)
- css, html, js, json, xml
+ - [Rome](#rome) ([binary detection](#rome-binary), [config file](#rome-configuration-file), [input language](#rome-input-language))
- **Language independent**
- [Generic steps](#generic-steps)
- [License header](#license-header) ([slurp year from git](#retroactively-slurp-years-from-git-history))
@@ -614,7 +615,8 @@ spotless {
tsfmt() // has its own section below
prettier() // has its own section below
- eslint() // has its own section below
+ eslint() // has its own section below
+ rome() // has its own section below
licenseHeader '/* (C) $YEAR */', '(import|const|declare|export|var) ' // or licenseHeaderFile
// note the '(import|const|...' argument - this is a regex which identifies the top
@@ -706,7 +708,8 @@ spotless {
target 'src/**/*.js' // you have to set the target manually
prettier() // has its own section below
- eslint() // has its own section below
+ eslint() // has its own section below
+ rome() // has its own section below
licenseHeader '/* (C) $YEAR */', 'REGEX_TO_DEFINE_TOP_OF_FILE' // or licenseHeaderFile
}
@@ -773,6 +776,7 @@ spotless {
eclipseWtp('json') // see Eclipse web tools platform section
gson() // has its own section below
jackson() // has its own section below
+ rome() // has its own section below
}
}
```
@@ -1065,6 +1069,171 @@ Unlike Eclipse, Spotless WTP ignores per default external URIs in schema locatio
external entities. To allow the access of external URIs, set the property `resolveExternalURI`
to true.
+## Rome
+
+[homepage](https://rome.tools/). [changelog](https://github.com/rome/tools/blob/main/CHANGELOG.md). Rome is a formatter that for the Frontend written in Rust, which has a native binary,
+does not require Node.js and as such, is pretty fast. It can currently format
+JavaScript, TypeScript, JSX, and JSON, and may support
+[more frontend languages](https://docs.rome.tools/internals/language_support/)
+such as CSS in the future.
+
+You can use rome in any language-specific format for supported languages, but
+usually you will be creating a generic format.
+
+```gradle
+spotless {
+ format 'styling', {
+ // you have to set the target manually
+ target 'src/*/webapp/**/*.js'
+
+ // Download Rome from the network if not already downloaded, see below for more info
+ rome('12.0.0')
+
+ // (optional) Path to the directory with the rome.json conig file
+ rome('12.0.0').configPath("path/config/dir")
+
+ // (optional) Rome will auto detect the language based on the file extension.
+ // See below for possible values.
+ rome('12.0.0').language("js")
+ }
+}
+```
+
+**Limitations:**
+- The auto-discovery of config files (up the file tree) will not work when using
+ Rome within spotless.
+
+To apply Rome to more kinds of files with a different configuration, just add
+more formats:
+
+```gradle
+spotless {
+ format 'rome-js', {
+ target '**/*.js'
+ rome('12.0.0')
+ }
+ format 'rome-ts', {
+ target '**/*.ts'
+ rome('12.0.0')
+ }
+ format 'rome-json', {
+ target '**/*.json'
+ rome('12.0.0')
+ }
+}
+```
+
+### Rome binary
+
+To format with Rome, spotless needs to find the Rome binary. By default,
+spotless downloads the binary for the given version from the network. This
+should be fine in most cases, but may not work e.g. when there is not connection
+to the internet.
+
+To download the Rome binary from the network, just specify a version:
+
+```gradle
+spotless {
+ format 'rome', {
+ target '**/*.js','**/*.ts','**/*.json'
+ rome('12.0.0')
+ }
+}
+```
+
+Spotless uses a default version when you do not specfiy a version, but this
+may change at any time, so we recommend that you always set the Rome version
+you want to use. Optionally, you can also specify a directory for the downloaded
+Rome binaries (defaults to `~/.m2/repository/com/diffplug/spotless/spotless-data/rome`):
+
+```gradle
+spotless {
+ format 'rome', {
+ target '**/*.js','**/*.ts','**/*.json'
+ // Relative paths are resolved against the project's base directory
+ rome('12.0.0').downloadDir("${project.gradle.gradleUserHomeDir}/rome")
+ }
+}
+```
+
+To use a fixed binary, omit the `version` and specify a `pathToExe`:
+
+```gradle
+spotless {
+ format 'rome', {
+ target '**/*.js','**/*.ts','**/*.json'
+ rome().pathToExe("${project.buildDir.absolutePath}/bin/rome")
+ }
+}
+```
+
+Absolute paths are used as-is. Relative paths are resolved against the project's
+base directory. To use a pre-installed Rome binary on the user's path, specify
+just a name without any slashes / backslashes:
+
+```gradle
+spotless {
+ format 'rome', {
+ target '**/*.js','**/*.ts','**/*.json'
+ // Uses the "rome" command, which must be on the user's path. -->
+ rome().pathToExe('rome')
+ }
+}
+```
+
+### Rome configuration file
+
+Rome is a biased formatter and linter without many options, but there are a few
+basic options. Rome uses a file named [rome.json](https://docs.rome.tools/configuration/)
+for its configuration. When none is specified, the default configuration from
+Rome is used. To use a custom configuration:
+
+```gradle
+spotless {
+ format 'rome', {
+ target '**/*.js','**/*.ts','**/*.json'
+ // Must point to the directory with the "rome.json" config file -->
+ // Relative paths are resolved against the project's base directory -->
+ rome('12.0.0').configPath('./config')
+ }
+}
+```
+
+### Rome input language
+
+By default, Rome detects the language / syntax of the files to format
+automatically from the file extension. This may fail if your source code files
+have unusual extensions for some reason. If you are using the generic format,
+you can force a certain language like this:
+
+```xml
+
+
+
+
+ src/**/typescript/**/*.mjson
+
+
+
+ 12.0.0
+ json
+
+
+
+
+
+```
+
+The following languages are currently recognized:
+
+* `js` -- JavaScript
+* `jsx` -- JavaScript + JSX (React)
+* `js?` -- JavaScript, with or without JSX, depending on the file extension
+* `ts` -- TypeScript
+* `tsx` -- TypeScript + JSX (React)
+* `ts?` -- TypeScript, with or without JSX, depending on the file extension
+* `json` -- JSON
+
## Generic steps
[Prettier](#prettier), [eclipse wtp](#eclipse-web-tools-platform), and [license header](#license-header) are available in every format, and they each have their own section. As mentioned in the [quickstart](#quickstart), there are a variety of simple generic steps which are also available in every format, here are examples of these:
diff --git a/plugin-gradle/build.gradle b/plugin-gradle/build.gradle
index 9a725dbf5d..021155abfe 100644
--- a/plugin-gradle/build.gradle
+++ b/plugin-gradle/build.gradle
@@ -25,6 +25,7 @@ dependencies {
testImplementation "org.junit.jupiter:junit-jupiter:${VER_JUNIT}"
testImplementation "org.assertj:assertj-core:${VER_ASSERTJ}"
testImplementation "com.diffplug.durian:durian-testlib:${VER_DURIAN}"
+ testImplementation 'org.owasp.encoder:encoder:1.2.3'
}
apply from: rootProject.file('gradle/special-tests.gradle')
diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FormatExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FormatExtension.java
index fd613e82e1..4fa74bc2ea 100644
--- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FormatExtension.java
+++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FormatExtension.java
@@ -229,7 +229,7 @@ protected final FileCollection parseTarget(Object target) {
private final FileCollection parseTargetIsExclude(Object target, boolean isExclude) {
if (target instanceof Collection) {
- return parseTargetsIsExclude(((Collection) target).toArray(), isExclude);
+ return parseTargetsIsExclude(((Collection>) target).toArray(), isExclude);
} else if (target instanceof FileCollection) {
return (FileCollection) target;
} else if (target instanceof String) {
@@ -649,6 +649,58 @@ protected FormatterStep createStep() {
}
}
+ /**
+ * Generic Rome formatter step that detects the language of the input file from
+ * the file name. It should be specified as a formatter step for a generic
+ * format{ ... }
.
+ */
+ public class RomeGeneric extends RomeStepConfig {
+ @Nullable
+ String language;
+
+ /**
+ * Creates a new Rome config that downloads the Rome executable for the given version from the network.
+ * @param version Rome version to use. The default version is used when null
.
+ */
+ public RomeGeneric(String version) {
+ super(getProject(), FormatExtension.this::replaceStep, version);
+ }
+
+ /**
+ * Sets the language (syntax) of the input files to format. When
+ * null
or the empty string, the language is detected automatically
+ * from the file name. Currently the following languages are supported by Rome:
+ *
+ * - js (JavaScript)
+ * - jsx (JavaScript + JSX)
+ * - js? (JavaScript or JavaScript + JSX, depending on the file
+ * extension)
+ * - ts (TypeScript)
+ * - tsx (TypeScript + JSX)
+ * - ts? (TypeScript or TypeScript + JSX, depending on the file
+ * extension)
+ * - json (JSON)
+ *
+ * @param language The language of the files to format.
+ * @return This step for further configuration.
+ */
+ public RomeGeneric language(String language) {
+ this.language = language;
+ replaceStep();
+ return this;
+ }
+
+ @Override
+ protected String getLanguage() {
+ return language;
+ }
+
+ @Override
+ protected RomeGeneric getThis() {
+ return this;
+ }
+ }
+
/** Uses the default version of prettier. */
public PrettierConfig prettier() {
return prettier(PrettierFormatterStep.defaultDevDependencies());
@@ -666,6 +718,22 @@ public PrettierConfig prettier(Map devDependencies) {
return prettierConfig;
}
+ /**
+ * Defaults to downloading the default Rome version from the network. To work
+ * offline, you can specify the path to the Rome executable via
+ * {@code rome().pathToExe(...)}.
+ */
+ public RomeStepConfig> rome() {
+ return rome(null);
+ }
+
+ /** Downloads the given Rome version from the network. */
+ public RomeStepConfig> rome(String version) {
+ var romeConfig = new RomeGeneric(version);
+ addStep(romeConfig.createStep());
+ return romeConfig;
+ }
+
/** Uses the default version of clang-format. */
public ClangFormatConfig clangFormat() {
return clangFormat(ClangFormatStep.defaultVersion());
diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JavascriptExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JavascriptExtension.java
index e8a76166bc..76ad5650ca 100644
--- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JavascriptExtension.java
+++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JavascriptExtension.java
@@ -138,9 +138,51 @@ public PrettierConfig prettier(Map devDependencies) {
return prettierConfig;
}
+ /**
+ * Defaults to downloading the default Rome version from the network. To work
+ * offline, you can specify the path to the Rome executable via
+ * {@code rome().pathToExe(...)}.
+ */
+ public RomeJs rome() {
+ return rome(null);
+ }
+
+ /** Downloads the given Rome version from the network. */
+ public RomeJs rome(String version) {
+ var romeConfig = new RomeJs(version);
+ addStep(romeConfig.createStep());
+ return romeConfig;
+ }
+
private static final String DEFAULT_PRETTIER_JS_PARSER = "babel";
private static final ImmutableList PRETTIER_JS_PARSERS = ImmutableList.of(DEFAULT_PRETTIER_JS_PARSER, "babel-flow", "flow");
+ /**
+ * Rome formatter step for JavaScript.
+ */
+ public class RomeJs extends RomeStepConfig {
+
+ /**
+ * Creates a new Rome formatter step config for formatting JavaScript files. Unless
+ * overwritten, the given Rome version is downloaded from the network.
+ *
+ * @param version Rome version to use.
+ */
+ public RomeJs(String version) {
+ super(getProject(), JavascriptExtension.this::replaceStep, version);
+ }
+
+ @Override
+ protected String getLanguage() {
+ return "js?";
+ }
+
+ @Override
+ protected RomeJs getThis() {
+ return this;
+ }
+ }
+
/**
* Overrides the parser to be set to a js parser.
*/
diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JsonExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JsonExtension.java
index 39b158ce1e..510ac529e8 100644
--- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JsonExtension.java
+++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JsonExtension.java
@@ -55,6 +55,22 @@ public JacksonJsonGradleConfig jackson() {
return new JacksonJsonGradleConfig(this);
}
+ /**
+ * Defaults to downloading the default Rome version from the network. To work
+ * offline, you can specify the path to the Rome executable via
+ * {@code rome().pathToExe(...)}.
+ */
+ public RomeJson rome() {
+ return rome(null);
+ }
+
+ /** Downloads the given Rome version from the network. */
+ public RomeJson rome(String version) {
+ var romeConfig = new RomeJson(version);
+ addStep(romeConfig.createStep());
+ return romeConfig;
+ }
+
public class SimpleConfig {
private int indent;
@@ -145,4 +161,29 @@ protected final FormatterStep createStep() {
return JacksonJsonStep.create(jacksonConfig, version, formatExtension.provisioner());
}
}
+
+ /**
+ * Rome formatter step for JSON.
+ */
+ public class RomeJson extends RomeStepConfig {
+ /**
+ * Creates a new Rome formatter step config for formatting JSON files. Unless
+ * overwritten, the given Rome version is downloaded from the network.
+ *
+ * @param version Rome version to use.
+ */
+ public RomeJson(String version) {
+ super(getProject(), JsonExtension.this::replaceStep, version);
+ }
+
+ @Override
+ protected String getLanguage() {
+ return "json";
+ }
+
+ @Override
+ protected RomeJson getThis() {
+ return this;
+ }
+ }
}
diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/RomeStepConfig.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/RomeStepConfig.java
new file mode 100644
index 0000000000..f739e922d2
--- /dev/null
+++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/RomeStepConfig.java
@@ -0,0 +1,271 @@
+/*
+ * Copyright 2023 DiffPlug
+ *
+ * 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 com.diffplug.gradle.spotless;
+
+import static java.util.Objects.requireNonNull;
+
+import java.io.File;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.function.Consumer;
+
+import javax.annotation.Nullable;
+
+import org.gradle.api.Project;
+import org.gradle.api.artifacts.repositories.MavenArtifactRepository;
+
+import com.diffplug.spotless.FormatterStep;
+import com.diffplug.spotless.rome.RomeStep;
+
+public abstract class RomeStepConfig> {
+ /**
+ * Optional path to the directory with configuration file for Rome. The file
+ * must be named {@code rome.json}. When none is given, the default
+ * configuration is used. If this is a relative path, it is resolved against the
+ * project's base directory.
+ */
+ @Nullable
+ private Object configPath;
+
+ /**
+ * Optional directory where the downloaded Rome executable is placed. If this is
+ * a relative path, it is resolved against the project's base directory.
+ * Defaults to
+ * ~/.m2/repository/com/diffplug/spotless/spotless-data/rome
.
+ */
+ @Nullable
+ private Object downloadDir;
+
+ /**
+ * Optional path to the Rome executable. Either a version
or a
+ * pathToExe
should be specified. When not given, an attempt is
+ * made to download the executable for the given version from the network. When
+ * given, the executable is used and the version
parameter is
+ * ignored.
+ *
+ * When an absolute path is given, that path is used as-is. When a relative path
+ * is given, it is resolved against the project's base directory. When only a
+ * file name (i.e. without any slashes or back slash path separators such as
+ * {@code rome}) is given, this is interpreted as the name of a command with
+ * executable that is in your {@code path} environment variable. Use
+ * {@code ./executable-name} if you want to use an executable in the project's
+ * base directory.
+ */
+ @Nullable
+ private Object pathToExe;
+
+ /**
+ * A reference to the Gradle project for which spotless is executed.
+ */
+ private final Project project;
+
+ /**
+ * Replaces the current Rome formatter step with the given step.
+ */
+ private final Consumer replaceStep;
+
+ /**
+ * Rome version to download, applies only when no pathToExe
is
+ * specified explicitly. Either a version
or a
+ * pathToExe
should be specified. When not given, a default known
+ * version is used. For stable builds, it is recommended that you always set the
+ * version explicitly. This parameter is ignored when you specify a
+ * pathToExe
explicitly.
+ */
+ @Nullable
+ private String version;
+
+ protected RomeStepConfig(Project project, Consumer replaceStep, String version) {
+ this.project = requireNonNull(project);
+ this.replaceStep = requireNonNull(replaceStep);
+ this.version = version;
+ }
+
+ /**
+ * Optional path to the directory with configuration file for Rome. The file
+ * must be named {@code rome.json}. When none is given, the default
+ * configuration is used. If this is a relative path, it is resolved against the
+ * project's base directory.
+ *
+ * @return This step for further configuration.
+ */
+ public Self configPath(Object configPath) {
+ this.configPath = configPath;
+ replaceStep();
+ return getThis();
+ }
+
+ /**
+ * Optional directory where the downloaded Rome executable is placed. If this is
+ * a relative path, it is resolved against the project's base directory.
+ * Defaults to
+ * ~/.m2/repository/com/diffplug/spotless/spotless-data/rome
.
+ *
+ * @return This step for further configuration.
+ */
+ public Self downloadDir(Object downloadDir) {
+ this.downloadDir = downloadDir;
+ replaceStep();
+ return getThis();
+ }
+
+ /**
+ * Optional path to the Rome executable. Overwrites the configured version. No
+ * attempt is made to download the Rome executable from the network.
+ *
+ * When an absolute path is given, that path is used as-is. When a relative path
+ * is given, it is resolved against the project's base directory. When only a
+ * file name (i.e. without any slashes or back slash path separators such as
+ * {@code rome}) is given, this is interpreted as the name of a command with
+ * executable that is in your {@code path} environment variable. Use
+ * {@code ./executable-name} if you want to use an executable in the project's
+ * base directory.
+ *
+ * @return This step for further configuration.
+ */
+ public Self pathToExe(Object pathToExe) {
+ this.pathToExe = pathToExe;
+ replaceStep();
+ return getThis();
+ }
+
+ /**
+ * Creates a new formatter step that formats code by calling the Rome
+ * executable, using the current configuration.
+ *
+ * @return A new formatter step for the Rome formatter.
+ */
+ protected FormatterStep createStep() {
+ var builder = newBuilder();
+ if (configPath != null) {
+ var resolvedConfigPath = project.file(configPath);
+ builder.withConfigPath(resolvedConfigPath.toString());
+ }
+ builder.withLanguage(getLanguage());
+ return builder.create();
+ }
+
+ /**
+ * Gets the language (syntax) of the input files to format. When
+ * null
or the empty string, the language is detected automatically
+ * from the file name. Currently the following languages are supported by Rome:
+ *
+ * - js (JavaScript)
+ * - jsx (JavaScript + JSX)
+ * - js? (JavaScript or JavaScript + JSX, depending on the file
+ * extension)
+ * - ts (TypeScript)
+ * - tsx (TypeScript + JSX)
+ * - ts? (TypeScript or TypeScript + JSX, depending on the file
+ * extension)
+ * - json (JSON)
+ *
+ *
+ * @return The language of the input files.
+ */
+ protected abstract String getLanguage();
+
+ /**
+ * @return This Rome config instance.
+ */
+ protected abstract Self getThis();
+
+ /**
+ * Creates a new Rome step and replaces the existing Rome step in the list of
+ * format steps.
+ */
+ protected void replaceStep() {
+ replaceStep.accept(createStep());
+ }
+
+ /**
+ * Finds the data directory that can be used for storing shared data such as
+ * Rome executable globally. This is a directory in the local repository, e.g.
+ * ~/.m2/repository/com/diffplus/spotless/spotless-data.
+ *
+ * @return The directory for storing shared data.
+ */
+ private File findDataDir() {
+ var currentRepo = project.getRepositories().stream()
+ .filter(r -> r instanceof MavenArtifactRepository)
+ .map(r -> (MavenArtifactRepository) r)
+ .filter(r -> "file".equals(r.getUrl().getScheme()))
+ .findAny().orElse(null);
+ // Temporarily add mavenLocal() repository to get its file URL
+ var localRepo = currentRepo != null ? (MavenArtifactRepository) currentRepo : project.getRepositories().mavenLocal();
+ try {
+ // e.g. ~/.m2/repository/
+ var repoPath = Path.of(localRepo.getUrl());
+ var dataPath = repoPath.resolve("com").resolve("diffplug").resolve("spotless").resolve("spotless-data");
+ return dataPath.toAbsolutePath().toFile();
+ } finally {
+ // Remove mavenLocal() repository again if it was not part of the project
+ if (currentRepo == null) {
+ project.getRepositories().remove(localRepo);
+ }
+ }
+ }
+
+ /**
+ * A new builder for configuring a Rome step that either downloads the Rome
+ * executable with the given version from the network, or uses the executable
+ * from the given path.
+ *
+ * @return A builder for a Rome step.
+ */
+ private RomeStep newBuilder() {
+ if (pathToExe != null) {
+ var resolvedPathToExe = resolvePathToExe();
+ return RomeStep.withExePath(resolvedPathToExe);
+ } else {
+ var downloadDir = resolveDownloadDir();
+ return RomeStep.withExeDownload(version, downloadDir);
+ }
+ }
+
+ /**
+ * Resolves the path to the Rome executable. When the path is only a file name,
+ * do not perform any resolution and interpret it as a command that must be on
+ * the user's path. Otherwise resolve the executable path against the project's
+ * base directory.
+ *
+ * @return The resolved path to the Rome executable.
+ */
+ private String resolvePathToExe() {
+ var fileNameOnly = pathToExe instanceof String && Paths.get(pathToExe.toString()).getNameCount() == 1;
+ if (fileNameOnly) {
+ return pathToExe.toString();
+ } else {
+ return project.file(pathToExe).toString();
+ }
+ }
+
+ /**
+ * Resolves the directory to use for storing downloaded Rome executable. When a
+ * {@link #downloadDir} is given, use that directory, resolved against the
+ * current project's directory. Otherwise, use the {@code Rome} sub folder in
+ * the shared data directory.
+ *
+ * @return The download directory for the Rome executable.
+ */
+ private String resolveDownloadDir() {
+ if (downloadDir != null) {
+ return project.file(downloadDir).toString();
+ } else {
+ return findDataDir().toPath().resolve("rome").toString();
+ }
+ }
+}
diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/TypescriptExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/TypescriptExtension.java
index 9f1d04abfd..ddb7fbfc10 100644
--- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/TypescriptExtension.java
+++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/TypescriptExtension.java
@@ -229,6 +229,47 @@ protected EslintConfig eslintConfig() {
}
}
+ /**
+ * Defaults to downloading the default Rome version from the network. To work
+ * offline, you can specify the path to the Rome executable via
+ * {@code rome().pathToExe(...)}.
+ */
+ public RomeTs rome() {
+ return rome(null);
+ }
+
+ /** Downloads the given Rome version from the network. */
+ public RomeTs rome(String version) {
+ var romeConfig = new RomeTs(version);
+ addStep(romeConfig.createStep());
+ return romeConfig;
+ }
+
+ /**
+ * Rome formatter step for TypeScript.
+ */
+ public class RomeTs extends RomeStepConfig {
+ /**
+ * Creates a new Rome formatter step config for formatting TypeScript files. Unless
+ * overwritten, the given Rome version is downloaded from the network.
+ *
+ * @param version Rome version to use.
+ */
+ public RomeTs(String version) {
+ super(getProject(), TypescriptExtension.this::replaceStep, version);
+ }
+
+ @Override
+ protected String getLanguage() {
+ return "ts?";
+ }
+
+ @Override
+ protected RomeTs getThis() {
+ return this;
+ }
+ }
+
@Override
protected void setupTask(SpotlessTask task) {
if (target == null) {
diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/RomeIntegrationTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/RomeIntegrationTest.java
new file mode 100644
index 0000000000..4464b1cec7
--- /dev/null
+++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/RomeIntegrationTest.java
@@ -0,0 +1,343 @@
+/*
+ * Copyright 2023 DiffPlug
+ *
+ * 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 com.diffplug.gradle.spotless;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.IOException;
+
+import org.junit.jupiter.api.Test;
+import org.owasp.encoder.Encode;
+
+class RomeIntegrationTest extends GradleIntegrationHarness {
+ /**
+ * Tests that rome can be used as a generic formatting step.
+ *
+ * @throws Exception When a test failure occurs.
+ */
+ @Test
+ void asGenericStep() throws IOException {
+ setFile("build.gradle").toLines(
+ "plugins {",
+ " id 'com.diffplug.spotless'",
+ "}",
+ "repositories { mavenCentral() }",
+ "spotless {",
+ " format 'myrome', {",
+ " target '**/*.js'",
+ " rome('12.0.0')",
+ " }",
+ "}");
+ setFile("rome_test.js").toResource("rome/js/fileBefore.js");
+
+ var spotlessApply = gradleRunner().withArguments("--stacktrace", "spotlessApply").build();
+ assertThat(spotlessApply.getOutput()).contains("BUILD SUCCESSFUL");
+ assertFile("rome_test.js").sameAsResource("rome/js/fileAfter.js");
+ }
+
+ /**
+ * Tests that rome can be used as a JavaScript formatting step.
+ *
+ * @throws Exception When a test failure occurs.
+ */
+ @Test
+ void asJavaScriptStep() throws Exception {
+ setFile("build.gradle").toLines(
+ "plugins {",
+ " id 'com.diffplug.spotless'",
+ "}",
+ "repositories { mavenCentral() }",
+ "spotless {",
+ " javascript {",
+ " target '**/*.js'",
+ " rome('12.0.0')",
+ " }",
+ "}");
+ setFile("rome_test.js").toResource("rome/js/fileBefore.js");
+
+ var spotlessApply = gradleRunner().withArguments("--stacktrace", "spotlessApply").build();
+ assertThat(spotlessApply.getOutput()).contains("BUILD SUCCESSFUL");
+ assertFile("rome_test.js").sameAsResource("rome/js/fileAfter.js");
+ }
+
+ /**
+ * Tests that rome can be used as a JSON formatting step.
+ *
+ * @throws Exception When a test failure occurs.
+ */
+ @Test
+ void asJsonStep() throws Exception {
+ setFile("build.gradle").toLines(
+ "plugins {",
+ " id 'com.diffplug.spotless'",
+ "}",
+ "repositories { mavenCentral() }",
+ "spotless {",
+ " json {",
+ " target '**/*.json'",
+ " rome('12.0.0')",
+ " }",
+ "}");
+ setFile("rome_test.json").toResource("rome/json/fileBefore.json");
+
+ var spotlessApply = gradleRunner().withArguments("--stacktrace", "spotlessApply").build();
+ assertThat(spotlessApply.getOutput()).contains("BUILD SUCCESSFUL");
+ assertFile("rome_test.json").sameAsResource("rome/json/fileAfter.json");
+ }
+
+ /**
+ * Tests that rome can be used as a TypeScript formatting step.
+ *
+ * @throws Exception When a test failure occurs.
+ */
+ @Test
+ void asTypeScriptStep() throws Exception {
+ setFile("build.gradle").toLines(
+ "plugins {",
+ " id 'com.diffplug.spotless'",
+ "}",
+ "repositories { mavenCentral() }",
+ "spotless {",
+ " typescript {",
+ " target '**/*.ts'",
+ " rome('12.0.0')",
+ " }",
+ "}");
+ setFile("rome_test.ts").toResource("rome/ts/fileBefore.ts");
+
+ var spotlessApply = gradleRunner().withArguments("--stacktrace", "spotlessApply").build();
+ assertThat(spotlessApply.getOutput()).contains("BUILD SUCCESSFUL");
+ assertFile("rome_test.ts").sameAsResource("rome/ts/fileAfter.ts");
+ }
+
+ /**
+ * Tests that the language can be specified for the generic format step.
+ *
+ * @throws Exception When a test failure occurs.
+ */
+ @Test
+ void canSetLanguageForGenericStep() throws Exception {
+ setFile("build.gradle").toLines(
+ "plugins {",
+ " id 'com.diffplug.spotless'",
+ "}",
+ "repositories { mavenCentral() }",
+ "spotless {",
+ " format 'myrome', {",
+ " target '**/*.nosj'",
+ " rome('12.0.0').language('json')",
+ " }",
+ "}");
+ setFile("rome_test.nosj").toResource("rome/json/fileBefore.json");
+
+ var spotlessApply = gradleRunner().withArguments("--stacktrace", "spotlessApply").build();
+ assertThat(spotlessApply.getOutput()).contains("BUILD SUCCESSFUL");
+ assertFile("rome_test.nosj").sameAsResource("rome/json/fileAfter.json");
+ }
+
+ /**
+ * Tests that an absolute config path can be specified.
+ *
+ * @throws Exception When a test failure occurs.
+ */
+ @Test
+ void configPathAbsolute() throws Exception {
+ var path = newFile("configs").getAbsolutePath();
+ setFile("build.gradle").toLines(
+ "plugins {",
+ " id 'com.diffplug.spotless'",
+ "}",
+ "repositories { mavenCentral() }",
+ "spotless {",
+ " format 'myrome', {",
+ " target '**/*.js'",
+ " rome('12.0.0').configPath('" + Encode.forJava(path) + "')",
+ " }",
+ "}");
+ setFile("rome_test.js").toResource("rome/js/longLineBefore.js");
+ setFile("configs/rome.json").toResource("rome/config/line-width-120.json");
+
+ var spotlessApply = gradleRunner().withArguments("--stacktrace", "spotlessApply").build();
+ assertThat(spotlessApply.getOutput()).contains("BUILD SUCCESSFUL");
+ assertFile("rome_test.js").sameAsResource("rome/js/longLineAfter120.js");
+ }
+
+ /**
+ * Tests that a path to the directory with the rome.json config file can be
+ * specified. Uses a config file with a line width of 120.
+ *
+ * @throws Exception When a test failure occurs.
+ */
+ @Test
+ void configPathLineWidth120() throws Exception {
+ setFile("build.gradle").toLines(
+ "plugins {",
+ " id 'com.diffplug.spotless'",
+ "}",
+ "repositories { mavenCentral() }",
+ "spotless {",
+ " format 'myrome', {",
+ " target '**/*.js'",
+ " rome('12.0.0').configPath('configs')",
+ " }",
+ "}");
+ setFile("rome_test.js").toResource("rome/js/longLineBefore.js");
+ setFile("configs/rome.json").toResource("rome/config/line-width-120.json");
+
+ var spotlessApply = gradleRunner().withArguments("--stacktrace", "spotlessApply").build();
+ assertThat(spotlessApply.getOutput()).contains("BUILD SUCCESSFUL");
+ assertFile("rome_test.js").sameAsResource("rome/js/longLineAfter120.js");
+ }
+
+ /**
+ * Tests that a path to the directory with the rome.json config file can be
+ * specified. Uses a config file with a line width of 80.
+ *
+ * @throws Exception When a test failure occurs.
+ */
+ @Test
+ void configPathLineWidth80() throws Exception {
+ setFile("build.gradle").toLines(
+ "plugins {",
+ " id 'com.diffplug.spotless'",
+ "}",
+ "repositories { mavenCentral() }",
+ "spotless {",
+ " format 'myrome', {",
+ " target '**/*.js'",
+ " rome('12.0.0').configPath('configs')",
+ " }",
+ "}");
+ setFile("rome_test.js").toResource("rome/js/longLineBefore.js");
+ setFile("configs/rome.json").toResource("rome/config/line-width-80.json");
+
+ var spotlessApply = gradleRunner().withArguments("--stacktrace", "spotlessApply").build();
+ assertThat(spotlessApply.getOutput()).contains("BUILD SUCCESSFUL");
+ assertFile("rome_test.js").sameAsResource("rome/js/longLineAfter80.js");
+ }
+
+ /**
+ * Tests that the download directory can be an absolute path.
+ *
+ * @throws Exception When a test failure occurs.
+ */
+ @Test
+ void downloadDirAbsolute() throws Exception {
+ var path = newFile("target/bin/rome").getAbsoluteFile().toString();
+ setFile("build.gradle").toLines(
+ "plugins {",
+ " id 'com.diffplug.spotless'",
+ "}",
+ "repositories { mavenCentral() }",
+ "spotless {",
+ " format 'myrome', {",
+ " target '**/*.js'",
+ " rome('12.0.0').downloadDir('" + Encode.forJava(path) + "')",
+ " }",
+ "}");
+ setFile("rome_test.js").toResource("rome/js/fileBefore.js");
+ assertTrue(!newFile("target/bin/rome").exists() || newFile("target/bin/rome").list().length == 0);
+
+ var spotlessApply = gradleRunner().withArguments("--stacktrace", "spotlessApply").build();
+ assertThat(spotlessApply.getOutput()).contains("BUILD SUCCESSFUL");
+ assertFile("rome_test.js").sameAsResource("rome/js/fileAfter.js");
+ assertEquals(2, newFile("target/bin/rome").list().length);
+ }
+
+ /**
+ * Tests that the download directory can be changed to a path relative to the
+ * project's base directory.
+ *
+ * @throws Exception When a test failure occurs.
+ */
+ @Test
+ void downloadDirRelative() throws Exception {
+ setFile("build.gradle").toLines(
+ "plugins {",
+ " id 'com.diffplug.spotless'",
+ "}",
+ "repositories { mavenCentral() }",
+ "spotless {",
+ " format 'myrome', {",
+ " target '**/*.js'",
+ " rome('12.0.0').downloadDir('target/bin/rome')",
+ " }",
+ "}");
+ setFile("rome_test.js").toResource("rome/js/fileBefore.js");
+ assertTrue(!newFile("target/bin/rome").exists() || newFile("target/bin/rome").list().length == 0);
+
+ var spotlessApply = gradleRunner().withArguments("--stacktrace", "spotlessApply").build();
+ assertThat(spotlessApply.getOutput()).contains("BUILD SUCCESSFUL");
+ assertFile("rome_test.js").sameAsResource("rome/js/fileAfter.js");
+ assertEquals(2, newFile("target/bin/rome").list().length);
+ }
+
+ /**
+ * Tests that the build fails when given Rome executable does not exist.
+ *
+ * @throws Exception When a test failure occurs.
+ */
+ @Test
+ void failureWhenExeNotFound() throws Exception {
+ setFile("build.gradle").toLines(
+ "plugins {",
+ " id 'com.diffplug.spotless'",
+ "}",
+ "repositories { mavenCentral() }",
+ "spotless {",
+ " format 'myrome', {",
+ " target '**/*.js'",
+ " rome('12.0.0').pathToExe('rome/is/missing')",
+ " }",
+ "}");
+ setFile("rome_test.js").toResource("rome/js/fileBefore.js");
+
+ var spotlessApply = gradleRunner().withArguments("--stacktrace", "spotlessApply").buildAndFail();
+ assertThat(spotlessApply.getOutput()).contains("Build failed with an exception");
+ assertFile("rome_test.js").sameAsResource("rome/js/fileBefore.js");
+ assertThat(spotlessApply.getOutput()).contains("Could not create task ':spotlessMyromeApply'");
+ assertThat(spotlessApply.getOutput()).contains("Rome executable does not exist");
+ }
+
+ /**
+ * Tests that the build fails when the input file could not be parsed.
+ *
+ * @throws Exception When a test failure occurs.
+ */
+ @Test
+ void failureWhenNotParseable() throws Exception {
+ setFile("build.gradle").toLines(
+ "plugins {",
+ " id 'com.diffplug.spotless'",
+ "}",
+ "repositories { mavenCentral() }",
+ "spotless {",
+ " format 'myrome', {",
+ " target '**/*.js'",
+ " rome('12.0.0').language('json')",
+ " }",
+ "}");
+ setFile("rome_test.js").toResource("rome/js/fileBefore.js");
+
+ var spotlessApply = gradleRunner().withArguments("--stacktrace", "spotlessApply").buildAndFail();
+ assertThat(spotlessApply.getOutput()).contains("spotlessMyrome FAILED");
+ assertFile("rome_test.js").sameAsResource("rome/js/fileBefore.js");
+ assertThat(spotlessApply.getOutput()).contains("Format with errors is disabled.");
+ assertThat(spotlessApply.getOutput()).contains("Step 'rome' found problem in 'rome_test.js'");
+ }
+}
diff --git a/plugin-maven/CHANGES.md b/plugin-maven/CHANGES.md
index 4e7edddeef..993404f1fe 100644
--- a/plugin-maven/CHANGES.md
+++ b/plugin-maven/CHANGES.md
@@ -3,11 +3,13 @@
We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `1.27.0`).
## [Unreleased]
-### Changes
-* Bump default sortpom version to latest `3.0.0` -> `3.2.1`. ([#1675](https://github.com/diffplug/spotless/pull/1675))
+### Added
+* Support Rome as a formatter for JavaScript and TypeScript code. Adds a new `rome` step to `javascript` and `typescript` formatter configurations. ([#1663](https://github.com/diffplug/spotless/pull/1663))
### Fixed
* `palantir` step now accepts a `style` parameter, which is documentation had already claimed to do. ([#1694](https://github.com/diffplug/spotless/pull/1694))
* When P2 download fails, indicate the responsible formatter. ([#1698](https://github.com/diffplug/spotless/issues/1698))
+### Changes
+* Bump default sortpom version to latest `3.0.0` -> `3.2.1`. ([#1675](https://github.com/diffplug/spotless/pull/1675))
## [2.36.0] - 2023-04-06
### Added
diff --git a/plugin-maven/README.md b/plugin-maven/README.md
index 505eafeedd..b11e2c164d 100644
--- a/plugin-maven/README.md
+++ b/plugin-maven/README.md
@@ -49,14 +49,15 @@ user@machine repo % mvn spotless:check
- [Sql](#sql) ([dbeaver](#dbeaver))
- [Maven Pom](#maven-pom) ([sortPom](#sortpom))
- [Markdown](#markdown) ([flexmark](#flexmark))
- - [Typescript](#typescript) ([tsfmt](#tsfmt), [prettier](#prettier), [ESLint](#eslint-typescript))
- - [Javascript](#javascript) ([prettier](#prettier), [ESLint](#eslint-javascript))
+ - [Typescript](#typescript) ([tsfmt](#tsfmt), [prettier](#prettier), [ESLint](#eslint-typescript), [Rome](#rome))
+ - [Javascript](#javascript) ([prettier](#prettier), [ESLint](#eslint-javascript), [Rome](#rome))
- [JSON](#json)
- [YAML](#yaml)
- [Gherkin](#gherkin)
- Multiple languages
- [Prettier](#prettier) ([plugins](#prettier-plugins), [npm detection](#npm-detection), [`.npmrc` detection](#npmrc-detection), [caching `npm install` results](#caching-results-of-npm-install))
- [eclipse web tools platform](#eclipse-web-tools-platform)
+ - [Rome](#rome) ([binary detection](#rome-binary), [config file](#rome-configuration-file), [input language](#rome-input-language))
- **Language independent**
- [Generic steps](#generic-steps)
- [License header](#license-header) ([slurp year from git](#retroactively-slurp-years-from-git-history))
@@ -704,6 +705,7 @@ Currently, none of the available options can be configured yet. It uses only the
+
/* (C)$YEAR */
@@ -814,6 +816,7 @@ For details, see the [npm detection](#npm-detection), [`.npmrc` detection](#npmr
+
/* (C)$YEAR */
@@ -890,6 +893,7 @@ For details, see the [npm detection](#npm-detection), [`.npmrc` detection](#npmr
+
```
@@ -1220,6 +1224,155 @@ to true.
+## Rome
+
+[homepage](https://rome.tools/). [changelog](https://github.com/rome/tools/blob/main/CHANGELOG.md). Rome is a formatter that for the Frontend written in Rust, which has a native binary,
+does not require Node.js and as such, is pretty fast. It can currently format
+JavaScript, TypeScript, JSX, and JSON, and may support
+[more frontend languages](https://docs.rome.tools/internals/language_support/)
+such as CSS in the future.
+
+You can use rome in any language-specific format for supported languages, but
+usually you will be creating a generic format.
+
+```xml
+
+
+
+
+ src/**/typescript/**/*.ts
+
+
+
+
+ 12.0.0
+
+
+ ${project.basedir}/path/to/config/dir
+
+
+
+ ts
+
+
+
+
+
+```
+
+**Limitations:**
+- The auto-discovery of config files (up the file tree) will not work when using
+ Rome within spotless.
+
+To apply Rome to more kinds of files with a different configuration, just add
+more formats
+
+```xml
+
+
+ src/**/*.ts
+ src/**/*.js
+
+```
+
+### Rome binary
+
+To format with Rome, spotless needs to find the Rome binary. By default,
+spotless downloads the binary for the given version from the network. This
+should be fine in most cases, but may not work e.g. when there is not connection
+to the internet.
+
+To download the Rome binary from the network, just specify a version:
+
+```xml
+
+ 12.0.0
+
+```
+
+Spotless uses a default version when you do not specfiy a version, but this
+may change at any time, so we recommend that you always set the Rome version
+you want to use. Optionally, you can also specify a directory for the downloaded
+Rome binaries (defaults to `~/.m2/repository/com/diffplug/spotless/spotless-data/rome`):
+
+```xml
+
+ 12.0.0
+
+ ${user.home}/rome
+
+```
+
+To use a fixed binary, omit the `version` and specify a `pathToExe`:
+
+```xml
+
+ ${project.basedir}/bin/rome
+
+```
+
+Absolute paths are used as-is. Relative paths are resolved against the project's
+base directory. To use a pre-installed Rome binary on the user's path, specify
+just a name without any slashes / backslashes:
+
+
+```xml
+
+
+ rome
+
+```
+
+### Rome configuration file
+
+Rome is a biased formatter and linter without many options, but there are a few
+basic options. Rome uses a file named [rome.json](https://docs.rome.tools/configuration/)
+for its configuration. When none is specified, the default configuration from
+Rome is used. To use a custom configuration:
+
+```xml
+
+
+
+ ${project.basedir}
+
+```
+
+### Rome input language
+
+By default, Rome detects the language / syntax of the files to format
+automatically from the file extension. This may fail if your source code files
+have unusual extensions for some reason. If you are using the generic format,
+you can force a certain language like this:
+
+```xml
+
+
+
+
+ src/**/typescript/**/*.mjson
+
+
+
+ 12.0.0
+ json
+
+
+
+
+
+```
+
+The following languages are currently recognized:
+
+* `js` -- JavaScript
+* `jsx` -- JavaScript + JSX (React)
+* `js?` -- JavaScript, with or without JSX, depending on the file extension
+* `ts` -- TypeScript
+* `tsx` -- TypeScript + JSX (React)
+* `ts?` -- TypeScript, with or without JSX, depending on the file extension
+* `json` -- JSON
+
## Generic steps
[Prettier](#prettier), [eclipse wtp](#eclipse-web-tools-platform), and [license header](#license-header) are available in every format, and they each have their own section. As mentioned in the [quickstart](#quickstart), there are a variety of simple generic steps which are also available in every format, here are examples of these:
diff --git a/plugin-maven/build.gradle b/plugin-maven/build.gradle
index a8604c6ed9..dda98ad79a 100644
--- a/plugin-maven/build.gradle
+++ b/plugin-maven/build.gradle
@@ -50,6 +50,7 @@ dependencies {
testImplementation "org.mockito:mockito-core:${VER_MOCKITO}"
testImplementation "com.diffplug.durian:durian-io:${VER_DURIAN}"
testImplementation 'com.github.spullara.mustache.java:compiler:0.9.10'
+ testImplementation 'org.owasp.encoder:encoder:1.2.3'
testImplementation "org.apache.maven:maven-plugin-api:${VER_MAVEN_API}"
testImplementation "org.eclipse.aether:aether-api:${VER_ECLIPSE_AETHER}"
testImplementation "org.codehaus.plexus:plexus-resources:${VER_PLEXUS_RESOURCES}"
diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/FileLocator.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/FileLocator.java
index 7ea998dc95..088c35a3d0 100644
--- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/FileLocator.java
+++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/FileLocator.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2016-2020 DiffPlug
+ * Copyright 2016-2023 DiffPlug
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,6 +19,10 @@
import static java.nio.charset.StandardCharsets.UTF_8;
import java.io.File;
+import java.net.URISyntaxException;
+import java.nio.file.FileSystemNotFoundException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
@@ -34,17 +38,18 @@ public class FileLocator {
static final String TMP_RESOURCE_FILE_PREFIX = "spotless-resource-";
private final ResourceManager resourceManager;
- private final File baseDir, buildDir;
+ private final File baseDir, buildDir, dataDir;
public FileLocator(ResourceManager resourceManager, File baseDir, File buildDir) {
this.resourceManager = Objects.requireNonNull(resourceManager);
this.baseDir = Objects.requireNonNull(baseDir);
this.buildDir = Objects.requireNonNull(buildDir);
+ this.dataDir = findDataDir();
}
/**
- * If the given path is a local file returns it as such unchanged,
- * otherwise extracts the given resource to a randomly-named file in the build folder.
+ * If the given path is a local file returns it as such unchanged, otherwise
+ * extracts the given resource to a randomly-named file in the build folder.
*/
public File locateFile(String path) {
if (isNullOrEmpty(path)) {
@@ -62,18 +67,42 @@ public File locateFile(String path) {
} catch (ResourceNotFoundException e) {
throw new RuntimeException("Unable to locate file with path: " + path, e);
} catch (FileResourceCreationException e) {
- throw new RuntimeException("Unable to create temporary file '" + outputFile + "' in the output directory", e);
+ throw new RuntimeException("Unable to create temporary file '" + outputFile + "' in the output directory",
+ e);
}
}
+ /**
+ * Finds the base directory of the Maven or Gradle project on which spotless is
+ * currently being executed.
+ *
+ * @return The base directory of the current Maven or Gradel project.
+ */
public File getBaseDir() {
return baseDir;
}
+ /**
+ * Finds the build directory (e.g. /target
) of the Maven or Gradle
+ * project on which spotless is currently being executed.
+ *
+ * @return The project build directory of the current Maven or Gradle project.
+ */
public File getBuildDir() {
return buildDir;
}
+ /**
+ * Finds the data directory that can be used for storing shared data such as
+ * downloaded files globally. This is a directory in the local repository, e.g.
+ * ~/.m2/repository/com/diffplus/spotless/spotless-data.
+ *
+ * @return The directory for storing shared data.
+ */
+ public File getDataDir() {
+ return dataDir;
+ }
+
private static String tmpOutputFileName(String path) {
String extension = FileUtils.extension(path);
byte[] pathHash = hash(path);
@@ -91,4 +120,33 @@ private static byte[] hash(String value) {
messageDigest.update(value.getBytes(UTF_8));
return messageDigest.digest();
}
+
+ private static File findDataDir() {
+ try {
+ // JAR path is e.g.
+ // ~/.m2/repository/com/diffplug/spotless/spotless-plugin-maven/1.2.3/spotless-plugin-maven-1.2.3.jar
+ var codeSource = FileLocator.class.getProtectionDomain().getCodeSource();
+ var location = codeSource != null ? codeSource.getLocation() : null;
+ var locationUri = location != null ? location.toURI() : null;
+ var jarPath = locationUri != null && "file".equals(locationUri.getScheme()) ? Path.of(locationUri) : null;
+ var parent1 = jarPath != null ? jarPath.getParent() : null;
+ var parent2 = parent1 != null ? parent1.getParent() : null;
+ var base = parent2 != null ? parent2.getParent() : null;
+ var sub = base != null ? base.resolve("spotless-data") : null;
+ if (sub != null) {
+ return sub.toAbsolutePath().toFile();
+ } else {
+ return findUserHome();
+ }
+ } catch (final SecurityException e) {
+ return findUserHome();
+ } catch (final URISyntaxException | FileSystemNotFoundException | IllegalArgumentException e) {
+ throw new RuntimeException("Unable to determine data directory in local Maven repository", e);
+ }
+ }
+
+ private static File findUserHome() {
+ var home = Paths.get(System.getenv("user.home"));
+ return home.resolve(".rome").toAbsolutePath().toFile();
+ }
}
diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/generic/Format.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/generic/Format.java
index a696e13ffb..ed08718c65 100644
--- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/generic/Format.java
+++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/generic/Format.java
@@ -40,4 +40,8 @@ public String licenseHeaderDelimiter() {
// do not specify a default delimiter
return null;
}
+
+ public void addRome(Rome rome) {
+ addStepFactory(rome);
+ }
}
diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/generic/Rome.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/generic/Rome.java
new file mode 100644
index 0000000000..62d9d3fdec
--- /dev/null
+++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/generic/Rome.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2016-2023 DiffPlug
+ *
+ * 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 com.diffplug.spotless.maven.generic;
+
+import org.apache.maven.plugins.annotations.Parameter;
+
+import com.diffplug.spotless.maven.rome.AbstractRome;
+
+/**
+ * Generic Rome formatter step that detects the language of the input file from
+ * the file name. It should be specified as a formatter step for a generic
+ * {@code }.
+ */
+public class Rome extends AbstractRome {
+ /**
+ * Gets the language (syntax) of the input files to format. When
+ * null
or the empty string, the language is detected automatically
+ * from the file name. Currently the following languages are supported by Rome:
+ *
+ *
+ * - js (JavaScript)
+ * - jsx (JavaScript + JSX)
+ * - js? (JavaScript or JavaScript + JSX, depending on the file
+ * extension)
+ * - ts (TypeScript)
+ * - tsx (TypeScript + JSX)
+ * - ts? (TypeScript or TypeScript + JSX, depending on the file
+ * extension)
+ * - json (JSON)
+ *
+ *
+ *
+ * @return The language of the input files.
+ */
+ @Parameter
+ private String language;
+
+ @Override
+ protected String getLanguage() {
+ return language;
+ }
+}
diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/javascript/Javascript.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/javascript/Javascript.java
index 31a5917e06..b254110486 100644
--- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/javascript/Javascript.java
+++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/javascript/Javascript.java
@@ -41,4 +41,8 @@ public String licenseHeaderDelimiter() {
public void addEslint(EslintJs eslint) {
addStepFactory(eslint);
}
+
+ public void addRome(RomeJs rome) {
+ addStepFactory(rome);
+ }
}
diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/javascript/RomeJs.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/javascript/RomeJs.java
new file mode 100644
index 0000000000..60fb7077df
--- /dev/null
+++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/javascript/RomeJs.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2016-2023 DiffPlug
+ *
+ * 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 com.diffplug.spotless.maven.javascript;
+
+import com.diffplug.spotless.maven.rome.AbstractRome;
+
+/**
+ * Rome formatter step for JavaScript.
+ */
+public class RomeJs extends AbstractRome {
+ @Override
+ protected String getLanguage() {
+ return "js?";
+ }
+}
diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/json/Json.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/json/Json.java
index 5bb7c17b3a..adbb2b7883 100644
--- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/json/Json.java
+++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/json/Json.java
@@ -50,4 +50,7 @@ public void addJackson(JacksonJson jackson) {
addStepFactory(jackson);
}
+ public void addRome(RomeJson rome) {
+ addStepFactory(rome);
+ }
}
diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/json/RomeJson.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/json/RomeJson.java
new file mode 100644
index 0000000000..1cd044b759
--- /dev/null
+++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/json/RomeJson.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2016-2023 DiffPlug
+ *
+ * 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 com.diffplug.spotless.maven.json;
+
+import com.diffplug.spotless.maven.rome.AbstractRome;
+
+/**
+ * Rome formatter step for JSON.
+ */
+public class RomeJson extends AbstractRome {
+ @Override
+ protected String getLanguage() {
+ return "json";
+ }
+}
diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/rome/AbstractRome.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/rome/AbstractRome.java
new file mode 100644
index 0000000000..33567b0a20
--- /dev/null
+++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/rome/AbstractRome.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright 2016-2023 DiffPlug
+ *
+ * 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 com.diffplug.spotless.maven.rome;
+
+import java.nio.file.Paths;
+
+import org.apache.maven.plugins.annotations.Parameter;
+
+import com.diffplug.spotless.FormatterStep;
+import com.diffplug.spotless.maven.FormatterStepConfig;
+import com.diffplug.spotless.maven.FormatterStepFactory;
+import com.diffplug.spotless.rome.RomeStep;
+
+/**
+ * Factory for creating the Rome formatter step that can format format code in
+ * various types of language with Rome. Currently Rome support JavaScript,
+ * TypeScript, JSX, TSX, and JSON. See also
+ * https://github.com/rome/tools.
+ * It delegates to the Rome CLI executable.
+ */
+public abstract class AbstractRome implements FormatterStepFactory {
+ /**
+ * Optional path to the directory with configuration file for Rome. The file
+ * must be named {@code rome.json}. When none is given, the default
+ * configuration is used. If this is a relative path, it is resolved against the
+ * project's base directory.
+ */
+ @Parameter
+ private String configPath;
+
+ /**
+ * Optional directory where the downloaded Rome executable is placed. If this is
+ * a relative path, it is resolved against the project's base directory.
+ * Defaults to
+ * ~/.m2/repository/com/diffplug/spotless/spotless-data/rome
.
+ *
+ * You can use an expression like ${user.home}/rome
if you want to
+ * use the home directory, or ${project.build.directory
if you want
+ * to use the target directory of the current project.
+ */
+ @Parameter
+ private String downloadDir;
+
+ /**
+ * Optional path to the Rome executable. Either a version
or a
+ * pathToExe
should be specified. When not given, an attempt is
+ * made to download the executable for the given version from the network. When
+ * given, the executable is used and the version
parameter is
+ * ignored.
+ *
+ * When an absolute path is given, that path is used as-is. When a relative path
+ * is given, it is resolved against the project's base directory. When only a
+ * file name (i.e. without any slashes or back slash path separators such as
+ * {@code rome}) is given, this is interpreted as the name of a command with
+ * executable that is in your {@code path} environment variable. Use
+ * {@code ./executable-name} if you want to use an executable in the project's
+ * base directory.
+ */
+ @Parameter
+ private String pathToExe;
+
+ /**
+ * Rome version to download, applies only when no pathToExe
is
+ * specified explicitly. Either a version
or a
+ * pathToExe
should be specified. When not given, a default known
+ * version is used. For stable builds, it is recommended that you always set the
+ * version explicitly. This parameter is ignored when you specify a
+ * pathToExe
explicitly.
+ */
+ @Parameter
+ private String version;
+
+ @Override
+ public FormatterStep newFormatterStep(FormatterStepConfig config) {
+ var builder = newBuilder(config);
+ if (configPath != null) {
+ var resolvedConfigFile = resolveConfigFile(config);
+ builder.withConfigPath(resolvedConfigFile);
+ }
+ if (getLanguage() != null) {
+ builder.withLanguage(getLanguage());
+ }
+ return builder.create();
+ }
+
+ /**
+ * Gets the language (syntax) of the input files to format. When
+ * null
or the empty string, the language is detected automatically
+ * from the file name. Currently the following languages are supported by Rome:
+ *
+ * - js (JavaScript)
+ * - jsx (JavaScript + JSX)
+ * - js? (JavaScript or JavaScript + JSX, depending on the file
+ * extension)
+ * - ts (TypeScript)
+ * - tsx (TypeScript + JSX)
+ * - ts? (TypeScript or TypeScript + JSX, depending on the file
+ * extension)
+ * - json (JSON)
+ *
+ *
+ * @return The language of the input files.
+ */
+ protected abstract String getLanguage();
+
+ /**
+ * A new builder for configuring a Rome step that either downloads the Rome
+ * executable with the given version from the network, or uses the executable
+ * from the given path.
+ *
+ * @param config Configuration from the Maven Mojo execution with details about
+ * the currently executed project.
+ * @return A builder for a Rome step.
+ */
+ private RomeStep newBuilder(FormatterStepConfig config) {
+ if (pathToExe != null) {
+ var resolvedExePath = resolveExePath(config);
+ return RomeStep.withExePath(resolvedExePath);
+ } else {
+ var downloadDir = resolveDownloadDir(config);
+ return RomeStep.withExeDownload(version, downloadDir);
+ }
+ }
+
+ /**
+ * Resolves the path to the configuration file for Rome. Relative paths are
+ * resolved against the project's base directory.
+ *
+ * @param config Configuration from the Maven Mojo execution with details about
+ * the currently executed project.
+ * @return The resolved path to the configuration file.
+ */
+ private String resolveConfigFile(FormatterStepConfig config) {
+ return config.getFileLocator().getBaseDir().toPath().resolve(configPath).toAbsolutePath().toString();
+ }
+
+ /**
+ * Resolves the path to the Rome executable. When the path is only a file name,
+ * do not perform any resolution and interpret it as a command that must be on
+ * the user's path. Otherwise resolve the executable path against the project's
+ * base directory.
+ *
+ * @param config Configuration from the Maven Mojo execution with details about
+ * the currently executed project.
+ * @return The resolved path to the Rome executable.
+ */
+ private String resolveExePath(FormatterStepConfig config) {
+ var path = Paths.get(pathToExe);
+ if (path.getNameCount() == 1) {
+ return path.toString();
+ } else {
+ return config.getFileLocator().getBaseDir().toPath().resolve(path).toAbsolutePath().toString();
+ }
+ }
+
+ /**
+ * Resolves the directory to use for storing downloaded Rome executable. When a
+ * {@link #downloadDir} is given, use that directory, resolved against the
+ * current project's directory. Otherwise, use the {@code Rome} sub folder in
+ * the shared data directory.
+ *
+ * @param config Configuration for this step.
+ * @return The download directory for the Rome executable.
+ */
+ private String resolveDownloadDir(FormatterStepConfig config) {
+ final var fileLocator = config.getFileLocator();
+ if (downloadDir != null && !downloadDir.isBlank()) {
+ return fileLocator.getBaseDir().toPath().resolve(downloadDir).toAbsolutePath().toString();
+ } else {
+ return fileLocator.getDataDir().toPath().resolve("rome").toString();
+ }
+ }
+}
diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/typescript/RomeTs.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/typescript/RomeTs.java
new file mode 100644
index 0000000000..ccee83744a
--- /dev/null
+++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/typescript/RomeTs.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2016-2023 DiffPlug
+ *
+ * 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 com.diffplug.spotless.maven.typescript;
+
+import com.diffplug.spotless.maven.rome.AbstractRome;
+
+/**
+ * Rome formatter step for TypeScript.
+ */
+public class RomeTs extends AbstractRome {
+ @Override
+ protected String getLanguage() {
+ return "ts?";
+ }
+}
diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/typescript/Typescript.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/typescript/Typescript.java
index 6ba45ab719..ddae74db82 100644
--- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/typescript/Typescript.java
+++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/typescript/Typescript.java
@@ -45,4 +45,8 @@ public void addTsfmt(Tsfmt tsfmt) {
public void addEslint(EslintTs eslint) {
addStepFactory(eslint);
}
+
+ public void addRome(RomeTs rome) {
+ addStepFactory(rome);
+ }
}
diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/MavenIntegrationHarness.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/MavenIntegrationHarness.java
index 216502bc07..398932dd19 100644
--- a/plugin-maven/src/test/java/com/diffplug/spotless/maven/MavenIntegrationHarness.java
+++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/MavenIntegrationHarness.java
@@ -154,6 +154,10 @@ protected void writePomWithPrettierSteps(String includes, String... steps) throw
writePom(formats(groupWithSteps("format", including(includes), steps)));
}
+ protected void writePomWithRomeSteps(String includes, String... steps) throws IOException {
+ writePom(formats(groupWithSteps("format", including(includes), steps)));
+ }
+
protected void writePomWithPrettierSteps(String[] plugins, String includes, String... steps) throws IOException {
writePom(null, formats(groupWithSteps("format", including(includes), steps)), null, plugins);
}
diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/rome/RomeMavenTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/rome/RomeMavenTest.java
new file mode 100644
index 0000000000..7ca3f7d981
--- /dev/null
+++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/rome/RomeMavenTest.java
@@ -0,0 +1,202 @@
+/*
+ * Copyright 2023 DiffPlug
+ *
+ * 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 com.diffplug.spotless.maven.rome;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.owasp.encoder.Encode.forXml;
+
+import org.junit.jupiter.api.Test;
+
+import com.diffplug.spotless.maven.MavenIntegrationHarness;
+
+class RomeMavenTest extends MavenIntegrationHarness {
+ /**
+ * Tests that rome can be used as a generic formatting step.
+ *
+ * @throws Exception When a test failure occurs.
+ */
+ @Test
+ void asGenericStep() throws Exception {
+ writePomWithRomeSteps("**/*.js", "12.0.0");
+ setFile("rome_test.js").toResource("rome/js/fileBefore.js");
+ mavenRunner().withArguments("spotless:apply").runNoError();
+ assertFile("rome_test.js").sameAsResource("rome/js/fileAfter.js");
+ }
+
+ /**
+ * Tests that rome can be used as a JavaScript formatting step.
+ *
+ * @throws Exception When a test failure occurs.
+ */
+ @Test
+ void asJavaScriptStep() throws Exception {
+ writePomWithJavascriptSteps("**/*.js", "12.0.0");
+ setFile("rome_test.js").toResource("rome/js/fileBefore.js");
+ mavenRunner().withArguments("spotless:apply").runNoError();
+ assertFile("rome_test.js").sameAsResource("rome/js/fileAfter.js");
+ }
+
+ /**
+ * Tests that rome can be used as a JSON formatting step.
+ *
+ * @throws Exception When a test failure occurs.
+ */
+ @Test
+ void asJsonStep() throws Exception {
+ writePomWithJsonSteps("**/*.json", "12.0.0");
+ setFile("rome_test.json").toResource("rome/json/fileBefore.json");
+ mavenRunner().withArguments("spotless:apply").runNoError();
+ assertFile("rome_test.json").sameAsResource("rome/json/fileAfter.json");
+ }
+
+ /**
+ * Tests that rome can be used as a TypeScript formatting step.
+ *
+ * @throws Exception When a test failure occurs.
+ */
+ @Test
+ void asTypeScriptStep() throws Exception {
+ writePomWithTypescriptSteps("**/*.ts", "12.0.0");
+ setFile("rome_test.ts").toResource("rome/ts/fileBefore.ts");
+ mavenRunner().withArguments("spotless:apply").runNoError();
+ assertFile("rome_test.ts").sameAsResource("rome/ts/fileAfter.ts");
+ }
+
+ /**
+ * Tests that the language can be specified for the generic format step.
+ *
+ * @throws Exception When a test failure occurs.
+ */
+ @Test
+ void canSetLanguageForGenericStep() throws Exception {
+ writePomWithRomeSteps("**/*.nosj", "12.0.0json");
+ setFile("rome_test.nosj").toResource("rome/json/fileBefore.json");
+ mavenRunner().withArguments("spotless:apply").runNoError();
+ assertFile("rome_test.nosj").sameAsResource("rome/json/fileAfter.json");
+ }
+
+ /**
+ * Tests that an absolute config path can be specified.
+ *
+ * @throws Exception When a test failure occurs.
+ */
+ @Test
+ void configPathAbsolute() throws Exception {
+ var path = newFile("configs").getAbsolutePath();
+ writePomWithRomeSteps("**/*.js",
+ "12.0.0" + forXml(path) + "");
+ setFile("rome_test.js").toResource("rome/js/longLineBefore.js");
+ setFile("configs/rome.json").toResource("rome/config/line-width-120.json");
+ mavenRunner().withArguments("spotless:apply").runNoError();
+ assertFile("rome_test.js").sameAsResource("rome/js/longLineAfter120.js");
+ }
+
+ /**
+ * Tests that a path to the directory with the rome.json config file can be
+ * specified. Uses a config file with a line width of 120.
+ *
+ * @throws Exception When a test failure occurs.
+ */
+ @Test
+ void configPathLineWidth120() throws Exception {
+ writePomWithRomeSteps("**/*.js", "12.0.0configs");
+ setFile("rome_test.js").toResource("rome/js/longLineBefore.js");
+ setFile("configs/rome.json").toResource("rome/config/line-width-120.json");
+ mavenRunner().withArguments("spotless:apply").runNoError();
+ assertFile("rome_test.js").sameAsResource("rome/js/longLineAfter120.js");
+ }
+
+ /**
+ * Tests that a path to the directory with the rome.json config file can be
+ * specified. Uses a config file with a line width of 80.
+ *
+ * @throws Exception When a test failure occurs.
+ */
+ @Test
+ void configPathLineWidth80() throws Exception {
+ writePomWithRomeSteps("**/*.js", "12.0.0configs");
+ setFile("rome_test.js").toResource("rome/js/longLineBefore.js");
+ setFile("configs/rome.json").toResource("rome/config/line-width-80.json");
+ mavenRunner().withArguments("spotless:apply").runNoError();
+ assertFile("rome_test.js").sameAsResource("rome/js/longLineAfter80.js");
+ }
+
+ /**
+ * Tests that the download directory can be an absolute path.
+ *
+ * @throws Exception When a test failure occurs.
+ */
+ @Test
+ void downloadDirAbsolute() throws Exception {
+ var path = newFile("target/bin/rome").getAbsoluteFile().toString();
+ writePomWithRomeSteps("**/*.js",
+ "12.0.0" + forXml(path) + "");
+ setFile("rome_test.js").toResource("rome/js/fileBefore.js");
+ assertTrue(!newFile("target/bin/rome").exists() || newFile("target/bin/rome").list().length == 0);
+ mavenRunner().withArguments("spotless:apply").runNoError();
+ assertFile("rome_test.js").sameAsResource("rome/js/fileAfter.js");
+ assertEquals(2, newFile("target/bin/rome").list().length);
+ }
+
+ /**
+ * Tests that the download directory can be changed to a path relative to the
+ * project's base directory.
+ *
+ * @throws Exception When a test failure occurs.
+ */
+ @Test
+ void downloadDirRelative() throws Exception {
+ writePomWithRomeSteps("**/*.js",
+ "12.0.0target/bin/rome");
+ setFile("rome_test.js").toResource("rome/js/fileBefore.js");
+ assertTrue(!newFile("target/bin/rome").exists() || newFile("target/bin/rome").list().length == 0);
+ mavenRunner().withArguments("spotless:apply").runNoError();
+ assertFile("rome_test.js").sameAsResource("rome/js/fileAfter.js");
+ assertEquals(2, newFile("target/bin/rome").list().length);
+ }
+
+ /**
+ * Tests that the build fails when the input file could not be parsed.
+ *
+ * @throws Exception When a test failure occurs.
+ */
+ @Test
+ void failureWhenExeNotFound() throws Exception {
+ writePomWithRomeSteps("**/*.js", "12.0.0rome/is/missing");
+ setFile("rome_test.js").toResource("rome/js/fileBefore.js");
+ var result = mavenRunner().withArguments("spotless:apply").runHasError();
+ assertFile("rome_test.js").sameAsResource("rome/js/fileBefore.js");
+ assertThat(result.stdOutUtf8()).contains("Rome executable does not exist");
+ }
+
+ /**
+ * Tests that the build fails when the input file could not be parsed.
+ *
+ * @throws Exception When a test failure occurs.
+ */
+ @Test
+ void failureWhenNotParseable() throws Exception {
+ writePomWithRomeSteps("**/*.js", "12.0.0json");
+ setFile("rome_test.js").toResource("rome/js/fileBefore.js");
+ var result = mavenRunner().withArguments("spotless:apply").runHasError();
+ assertFile("rome_test.js").sameAsResource("rome/js/fileBefore.js");
+ assertThat(result.stdOutUtf8()).contains("Format with errors is disabled.");
+ assertThat(result.stdOutUtf8()).contains("Unable to format file");
+ assertThat(result.stdOutUtf8()).contains("Step 'rome' found problem in 'rome_test.js'");
+ }
+}
diff --git a/testlib/src/main/resources/rome/config/line-width-120.json b/testlib/src/main/resources/rome/config/line-width-120.json
new file mode 100644
index 0000000000..8f14afa3f8
--- /dev/null
+++ b/testlib/src/main/resources/rome/config/line-width-120.json
@@ -0,0 +1,11 @@
+{
+ "formatter": {
+ "enabled": true,
+ "indentStyle": "tab",
+ "lineWidth": 120,
+ "formatWithErrors": false
+ },
+ "linter": {
+ "enabled": false
+ }
+ }
\ No newline at end of file
diff --git a/testlib/src/main/resources/rome/config/line-width-80.json b/testlib/src/main/resources/rome/config/line-width-80.json
new file mode 100644
index 0000000000..5ec998bd97
--- /dev/null
+++ b/testlib/src/main/resources/rome/config/line-width-80.json
@@ -0,0 +1,11 @@
+{
+ "formatter": {
+ "enabled": true,
+ "indentStyle": "tab",
+ "lineWidth": 80,
+ "formatWithErrors": false
+ },
+ "linter": {
+ "enabled": false
+ }
+ }
\ No newline at end of file
diff --git a/testlib/src/main/resources/rome/js/fileAfter.cjs b/testlib/src/main/resources/rome/js/fileAfter.cjs
new file mode 100644
index 0000000000..defc9c85eb
--- /dev/null
+++ b/testlib/src/main/resources/rome/js/fileAfter.cjs
@@ -0,0 +1,3 @@
+function foo(name = "World") {
+ return "Hello " + name;
+}
diff --git a/testlib/src/main/resources/rome/js/fileAfter.js b/testlib/src/main/resources/rome/js/fileAfter.js
new file mode 100644
index 0000000000..defc9c85eb
--- /dev/null
+++ b/testlib/src/main/resources/rome/js/fileAfter.js
@@ -0,0 +1,3 @@
+function foo(name = "World") {
+ return "Hello " + name;
+}
diff --git a/testlib/src/main/resources/rome/js/fileAfter.jsx b/testlib/src/main/resources/rome/js/fileAfter.jsx
new file mode 100644
index 0000000000..313aceb6ed
--- /dev/null
+++ b/testlib/src/main/resources/rome/js/fileAfter.jsx
@@ -0,0 +1,3 @@
+export function Panel(cfg = {}) {
+ return {1 + 2}
;
+}
diff --git a/testlib/src/main/resources/rome/js/fileAfter.mjs b/testlib/src/main/resources/rome/js/fileAfter.mjs
new file mode 100644
index 0000000000..defc9c85eb
--- /dev/null
+++ b/testlib/src/main/resources/rome/js/fileAfter.mjs
@@ -0,0 +1,3 @@
+function foo(name = "World") {
+ return "Hello " + name;
+}
diff --git a/testlib/src/main/resources/rome/js/fileBefore.cjs b/testlib/src/main/resources/rome/js/fileBefore.cjs
new file mode 100644
index 0000000000..92539ba751
--- /dev/null
+++ b/testlib/src/main/resources/rome/js/fileBefore.cjs
@@ -0,0 +1,3 @@
+function foo ( name="World"){
+ return "Hello "+name ;
+ }
\ No newline at end of file
diff --git a/testlib/src/main/resources/rome/js/fileBefore.js b/testlib/src/main/resources/rome/js/fileBefore.js
new file mode 100644
index 0000000000..92539ba751
--- /dev/null
+++ b/testlib/src/main/resources/rome/js/fileBefore.js
@@ -0,0 +1,3 @@
+function foo ( name="World"){
+ return "Hello "+name ;
+ }
\ No newline at end of file
diff --git a/testlib/src/main/resources/rome/js/fileBefore.jsx b/testlib/src/main/resources/rome/js/fileBefore.jsx
new file mode 100644
index 0000000000..8e5d9834bc
--- /dev/null
+++ b/testlib/src/main/resources/rome/js/fileBefore.jsx
@@ -0,0 +1,4 @@
+export function Panel ( cfg={}){
+ return ({1+2}
+ ) ;
+ }
\ No newline at end of file
diff --git a/testlib/src/main/resources/rome/js/fileBefore.mjs b/testlib/src/main/resources/rome/js/fileBefore.mjs
new file mode 100644
index 0000000000..92539ba751
--- /dev/null
+++ b/testlib/src/main/resources/rome/js/fileBefore.mjs
@@ -0,0 +1,3 @@
+function foo ( name="World"){
+ return "Hello "+name ;
+ }
\ No newline at end of file
diff --git a/testlib/src/main/resources/rome/js/longLineAfter120.js b/testlib/src/main/resources/rome/js/longLineAfter120.js
new file mode 100644
index 0000000000..68addc4b65
--- /dev/null
+++ b/testlib/src/main/resources/rome/js/longLineAfter120.js
@@ -0,0 +1 @@
+const x = ["Hello", "World", "How", "Are", "You", "Doing", "Today", "Such", "A", "Wondrous", "Sunshine"];
diff --git a/testlib/src/main/resources/rome/js/longLineAfter80.js b/testlib/src/main/resources/rome/js/longLineAfter80.js
new file mode 100644
index 0000000000..dbdbd157e9
--- /dev/null
+++ b/testlib/src/main/resources/rome/js/longLineAfter80.js
@@ -0,0 +1,13 @@
+const x = [
+ "Hello",
+ "World",
+ "How",
+ "Are",
+ "You",
+ "Doing",
+ "Today",
+ "Such",
+ "A",
+ "Wondrous",
+ "Sunshine",
+];
diff --git a/testlib/src/main/resources/rome/js/longLineBefore.js b/testlib/src/main/resources/rome/js/longLineBefore.js
new file mode 100644
index 0000000000..fd59e429c2
--- /dev/null
+++ b/testlib/src/main/resources/rome/js/longLineBefore.js
@@ -0,0 +1 @@
+const x = ["Hello", "World", "How", "Are", "You", "Doing", "Today", "Such", "A", "Wondrous", "Sunshine"];
\ No newline at end of file
diff --git a/testlib/src/main/resources/rome/json/fileAfter.json b/testlib/src/main/resources/rome/json/fileAfter.json
new file mode 100644
index 0000000000..468dac3297
--- /dev/null
+++ b/testlib/src/main/resources/rome/json/fileAfter.json
@@ -0,0 +1,5 @@
+{
+ "a": [1, 2, 3],
+ "b": 9,
+ "c": null
+}
diff --git a/testlib/src/main/resources/rome/json/fileBefore.json b/testlib/src/main/resources/rome/json/fileBefore.json
new file mode 100644
index 0000000000..77182284c7
--- /dev/null
+++ b/testlib/src/main/resources/rome/json/fileBefore.json
@@ -0,0 +1,7 @@
+ {
+ "a":[1,2,3
+
+],
+ "b":9,
+ "c" : null
+ }
\ No newline at end of file
diff --git a/testlib/src/main/resources/rome/ts/fileAfter.cts b/testlib/src/main/resources/rome/ts/fileAfter.cts
new file mode 100644
index 0000000000..f854953234
--- /dev/null
+++ b/testlib/src/main/resources/rome/ts/fileAfter.cts
@@ -0,0 +1,4 @@
+type Name = "World" | "Maven" | "Gradle";
+const foo = (name: Name = "World", v: T): string => {
+ return "Hello " + name;
+};
diff --git a/testlib/src/main/resources/rome/ts/fileAfter.mts b/testlib/src/main/resources/rome/ts/fileAfter.mts
new file mode 100644
index 0000000000..e6563e3030
--- /dev/null
+++ b/testlib/src/main/resources/rome/ts/fileAfter.mts
@@ -0,0 +1,4 @@
+export type Name = "World" | "Maven" | "Gradle";
+export const foo = (name: Name = "World", v: T): string => {
+ return "Hello " + name;
+};
diff --git a/testlib/src/main/resources/rome/ts/fileAfter.ts b/testlib/src/main/resources/rome/ts/fileAfter.ts
new file mode 100644
index 0000000000..e6563e3030
--- /dev/null
+++ b/testlib/src/main/resources/rome/ts/fileAfter.ts
@@ -0,0 +1,4 @@
+export type Name = "World" | "Maven" | "Gradle";
+export const foo = (name: Name = "World", v: T): string => {
+ return "Hello " + name;
+};
diff --git a/testlib/src/main/resources/rome/ts/fileAfter.tsx b/testlib/src/main/resources/rome/ts/fileAfter.tsx
new file mode 100644
index 0000000000..15ef316142
--- /dev/null
+++ b/testlib/src/main/resources/rome/ts/fileAfter.tsx
@@ -0,0 +1,7 @@
+export interface Cfg {
+ classname: string;
+ message: T;
+}
+const Panel = (cfg: Cfg): JSX.Element => {
+ return {String(cfg.message)}
;
+};
diff --git a/testlib/src/main/resources/rome/ts/fileBefore.cts b/testlib/src/main/resources/rome/ts/fileBefore.cts
new file mode 100644
index 0000000000..d4304287c0
--- /dev/null
+++ b/testlib/src/main/resources/rome/ts/fileBefore.cts
@@ -0,0 +1,4 @@
+type Name = "World" | "Maven"|"Gradle";
+const foo = ( name: Name="World", v: T): string => {
+ return "Hello " + name;
+ }
\ No newline at end of file
diff --git a/testlib/src/main/resources/rome/ts/fileBefore.mts b/testlib/src/main/resources/rome/ts/fileBefore.mts
new file mode 100644
index 0000000000..96837762a3
--- /dev/null
+++ b/testlib/src/main/resources/rome/ts/fileBefore.mts
@@ -0,0 +1,5 @@
+export
+ type Name = "World" | "Maven"|"Gradle";
+export const foo = ( name: Name="World", v: T): string => {
+ return "Hello " + name;
+ }
\ No newline at end of file
diff --git a/testlib/src/main/resources/rome/ts/fileBefore.ts b/testlib/src/main/resources/rome/ts/fileBefore.ts
new file mode 100644
index 0000000000..96837762a3
--- /dev/null
+++ b/testlib/src/main/resources/rome/ts/fileBefore.ts
@@ -0,0 +1,5 @@
+export
+ type Name = "World" | "Maven"|"Gradle";
+export const foo = ( name: Name="World", v: T): string => {
+ return "Hello " + name;
+ }
\ No newline at end of file
diff --git a/testlib/src/main/resources/rome/ts/fileBefore.tsx b/testlib/src/main/resources/rome/ts/fileBefore.tsx
new file mode 100644
index 0000000000..38f24f8440
--- /dev/null
+++ b/testlib/src/main/resources/rome/ts/fileBefore.tsx
@@ -0,0 +1,8 @@
+export interface Cfg{
+classname:string,
+message:T,
+}
+const Panel = ( cfg:Cfg):JSX.Element =>{
+ return ({String(cfg.message)}
+ ) ;
+ }
\ No newline at end of file
diff --git a/testlib/src/test/java/com/diffplug/spotless/rome/RomeStepTest.java b/testlib/src/test/java/com/diffplug/spotless/rome/RomeStepTest.java
new file mode 100644
index 0000000000..5c4801d50a
--- /dev/null
+++ b/testlib/src/test/java/com/diffplug/spotless/rome/RomeStepTest.java
@@ -0,0 +1,284 @@
+/*
+ * Copyright 2023 DiffPlug
+ *
+ * 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 com.diffplug.spotless.rome;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import com.diffplug.common.base.StandardSystemProperty;
+import com.diffplug.spotless.ResourceHarness;
+import com.diffplug.spotless.StepHarnessWithFile;
+import com.diffplug.spotless.ThrowingEx;
+
+class RomeStepTest extends ResourceHarness {
+ private static String downloadDir;
+
+ @BeforeAll
+ static void createDownloadDir() throws IOException {
+ // We do not want to download Rome each time we execute a test
+ var userHome = Paths.get(StandardSystemProperty.USER_HOME.value());
+ downloadDir = userHome.resolve(".gradle").resolve("rome-dl-test").toAbsolutePath().normalize().toString();
+ }
+
+ /**
+ * Tests that files can be formatted without setting the input language
+ * explicitly.
+ */
+ @Nested
+ class AutoDetectLanguage {
+ /**
+ * Tests that a *.cjs file can be formatted without setting the input language
+ * explicitly.
+ */
+ @Test
+ void testAutoDetectCjs() {
+ var step = RomeStep.withExeDownload("12.0.0", downloadDir.toString()).create();
+ var stepHarness = StepHarnessWithFile.forStep(RomeStepTest.this, step);
+ stepHarness.testResource("rome/js/fileBefore.cjs", "rome/js/fileAfter.cjs");
+ }
+
+ /**
+ * Tests that a *.cts file can be formatted without setting the input language
+ * explicitly.
+ */
+ @Test
+ void testAutoDetectCts() {
+ var step = RomeStep.withExeDownload("12.0.0", downloadDir.toString()).create();
+ var stepHarness = StepHarnessWithFile.forStep(RomeStepTest.this, step);
+ stepHarness.testResource("rome/ts/fileBefore.cts", "rome/ts/fileAfter.cts");
+ }
+
+ /**
+ * Tests that a *.js file can be formatted without setting the input language
+ * explicitly.
+ */
+ @Test
+ void testAutoDetectJs() {
+ var step = RomeStep.withExeDownload("12.0.0", downloadDir.toString()).create();
+ var stepHarness = StepHarnessWithFile.forStep(RomeStepTest.this, step);
+ stepHarness.testResource("rome/js/fileBefore.js", "rome/js/fileAfter.js");
+ }
+
+ /**
+ * Tests that a *.js file can be formatted without setting the input language
+ * explicitly.
+ */
+ @Test
+ void testAutoDetectJson() {
+ var step = RomeStep.withExeDownload("12.0.0", downloadDir.toString()).create();
+ var stepHarness = StepHarnessWithFile.forStep(RomeStepTest.this, step);
+ stepHarness.testResource("rome/json/fileBefore.json", "rome/json/fileAfter.json");
+ }
+
+ /**
+ * Tests that a *.jsx file can be formatted without setting the input language
+ * explicitly.
+ */
+ @Test
+ void testAutoDetectJsx() {
+ var step = RomeStep.withExeDownload("12.0.0", downloadDir.toString()).create();
+ var stepHarness = StepHarnessWithFile.forStep(RomeStepTest.this, step);
+ stepHarness.testResource("rome/js/fileBefore.jsx", "rome/js/fileAfter.jsx");
+ }
+
+ /**
+ * Tests that a *.mjs file can be formatted without setting the input language
+ * explicitly.
+ */
+ @Test
+ void testAutoDetectMjs() {
+ var step = RomeStep.withExeDownload("12.0.0", downloadDir.toString()).create();
+ var stepHarness = StepHarnessWithFile.forStep(RomeStepTest.this, step);
+ stepHarness.testResource("rome/js/fileBefore.mjs", "rome/js/fileAfter.mjs");
+ }
+
+ /**
+ * Tests that a *.mts file can be formatted without setting the input language
+ * explicitly.
+ */
+ @Test
+ void testAutoDetectMts() {
+ var step = RomeStep.withExeDownload("12.0.0", downloadDir.toString()).create();
+ var stepHarness = StepHarnessWithFile.forStep(RomeStepTest.this, step);
+ stepHarness.testResource("rome/ts/fileBefore.mts", "rome/ts/fileAfter.mts");
+ }
+
+ /**
+ * Tests that a *.ts file can be formatted without setting the input language
+ * explicitly.
+ */
+ @Test
+ void testAutoDetectTs() {
+ var step = RomeStep.withExeDownload("12.0.0", downloadDir.toString()).create();
+ var stepHarness = StepHarnessWithFile.forStep(RomeStepTest.this, step);
+ stepHarness.testResource("rome/ts/fileBefore.ts", "rome/ts/fileAfter.ts");
+ }
+
+ /**
+ * Tests that a *.tsx file can be formatted without setting the input language
+ * explicitly.
+ */
+ @Test
+ void testAutoDetectTsx() {
+ var step = RomeStep.withExeDownload("12.0.0", downloadDir.toString()).create();
+ var stepHarness = StepHarnessWithFile.forStep(RomeStepTest.this, step);
+ stepHarness.testResource("rome/ts/fileBefore.tsx", "rome/ts/fileAfter.tsx");
+ }
+ }
+
+ @Nested
+ class ConfigFile {
+ /**
+ * Test formatting with the line width in the config file set to 120.
+ */
+ @Test
+ void testLineWidth120() {
+ var path = createRomeConfig("rome/config/line-width-120.json");
+ var step = RomeStep.withExeDownload("12.0.0", downloadDir.toString()).withConfigPath(path).create();
+ var stepHarness = StepHarnessWithFile.forStep(RomeStepTest.this, step);
+ stepHarness.testResource("rome/js/longLineBefore.js", "rome/js/longLineAfter120.js");
+ }
+
+ /**
+ * Test formatting with the line width in the config file set to 120.
+ */
+ @Test
+ void testLineWidth80() {
+ var path = createRomeConfig("rome/config/line-width-80.json");
+ var step = RomeStep.withExeDownload("12.0.0", downloadDir.toString()).withConfigPath(path).create();
+ var stepHarness = StepHarnessWithFile.forStep(RomeStepTest.this, step);
+ stepHarness.testResource("rome/js/longLineBefore.js", "rome/js/longLineAfter80.js");
+ }
+
+ private String createRomeConfig(String name) {
+ var config = createTestFile(name).toPath();
+ var dir = config.getParent();
+ var rome = dir.resolve("rome.json");
+ ThrowingEx.run(() -> Files.copy(config, rome));
+ return dir.toString();
+ }
+ }
+
+ /**
+ * Tests that files can be formatted when setting the input language explicitly.
+ */
+ @Nested
+ class ExplicitLanguage {
+ /**
+ * Tests that a *.cjs file can be formatted when setting the input language
+ * explicitly.
+ */
+ @Test
+ void testAutoDetectCjs() {
+ var step = RomeStep.withExeDownload("12.0.0", downloadDir.toString()).withLanguage("js").create();
+ var stepHarness = StepHarnessWithFile.forStep(RomeStepTest.this, step);
+ stepHarness.testResource("rome/js/fileBefore.cjs", "rome/js/fileAfter.cjs");
+ }
+
+ /**
+ * Tests that a *.cts file can be formatted when setting the input language
+ * explicitly.
+ */
+ @Test
+ void testAutoDetectCts() {
+ var step = RomeStep.withExeDownload("12.0.0", downloadDir.toString()).withLanguage("ts").create();
+ var stepHarness = StepHarnessWithFile.forStep(RomeStepTest.this, step);
+ stepHarness.testResource("rome/ts/fileBefore.cts", "rome/ts/fileAfter.cts");
+ }
+
+ /**
+ * Tests that a *.js file can be formatted when setting the input language
+ * explicitly.
+ */
+ @Test
+ void testAutoDetectJs() {
+ var step = RomeStep.withExeDownload("12.0.0", downloadDir.toString()).withLanguage("js").create();
+ var stepHarness = StepHarnessWithFile.forStep(RomeStepTest.this, step);
+ stepHarness.testResource("rome/js/fileBefore.js", "rome/js/fileAfter.js");
+ }
+
+ /**
+ * Tests that a *.json file can be formatted when setting the input language
+ * explicitly.
+ */
+ @Test
+ void testAutoDetectJson() {
+ var step = RomeStep.withExeDownload("12.0.0", downloadDir.toString()).withLanguage("json").create();
+ var stepHarness = StepHarnessWithFile.forStep(RomeStepTest.this, step);
+ stepHarness.testResource("rome/json/fileBefore.json", "rome/json/fileAfter.json");
+ }
+
+ /**
+ * Tests that a *.jsx file can be formatted when setting the input language
+ * explicitly.
+ */
+ @Test
+ void testAutoDetectJsx() {
+ var step = RomeStep.withExeDownload("12.0.0", downloadDir.toString()).withLanguage("jsx").create();
+ var stepHarness = StepHarnessWithFile.forStep(RomeStepTest.this, step);
+ stepHarness.testResource("rome/js/fileBefore.jsx", "rome/js/fileAfter.jsx");
+ }
+
+ /**
+ * Tests that a *.mjs file can be formatted without setting the input language
+ * explicitly.
+ */
+ @Test
+ void testAutoDetectMjs() {
+ var step = RomeStep.withExeDownload("12.0.0", downloadDir.toString()).withLanguage("js").create();
+ var stepHarness = StepHarnessWithFile.forStep(RomeStepTest.this, step);
+ stepHarness.testResource("rome/js/fileBefore.mjs", "rome/js/fileAfter.mjs");
+ }
+
+ /**
+ * Tests that a *.mts file can be formatted when setting the input language
+ * explicitly.
+ */
+ @Test
+ void testAutoDetectMts() {
+ var step = RomeStep.withExeDownload("12.0.0", downloadDir.toString()).withLanguage("ts").create();
+ var stepHarness = StepHarnessWithFile.forStep(RomeStepTest.this, step);
+ stepHarness.testResource("rome/ts/fileBefore.mts", "rome/ts/fileAfter.mts");
+ }
+
+ /**
+ * Tests that a *.ts file can be formatted when setting the input language
+ * explicitly.
+ */
+ @Test
+ void testAutoDetectTs() {
+ var step = RomeStep.withExeDownload("12.0.0", downloadDir.toString()).withLanguage("ts").create();
+ var stepHarness = StepHarnessWithFile.forStep(RomeStepTest.this, step);
+ stepHarness.testResource("rome/ts/fileBefore.ts", "rome/ts/fileAfter.ts");
+ }
+
+ /**
+ * Tests that a *.tsx file can be formatted when setting the input language
+ * explicitly.
+ */
+ @Test
+ void testAutoDetectTsx() {
+ var step = RomeStep.withExeDownload("12.0.0", downloadDir.toString()).withLanguage("tsx").create();
+ var stepHarness = StepHarnessWithFile.forStep(RomeStepTest.this, step);
+ stepHarness.testResource("rome/ts/fileBefore.tsx", "rome/ts/fileAfter.tsx");
+ }
+ }
+}