diff --git a/CHANGES.md b/CHANGES.md index e8af5aec03..d41f40aaa6 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -23,6 +23,8 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( * ** POTENTIALLY BREAKING** Removed support for KtLint 0.3x and 0.45.2 ([#1475](https://github.com/diffplug/spotless/pull/1475)) * `KtLint` does not maintain a stable API - before this PR, we supported every breaking change in the API since 2019. * From now on, we will support no more than 2 breaking changes at a time. +* NpmFormatterStepStateBase delays `npm install` call until the formatter is first used. This enables better integration + with `gradle-node-plugin`. ([#1522](https://github.com/diffplug/spotless/pull/1522)) ## [2.32.0] - 2023-01-13 ### Added diff --git a/lib/src/main/java/com/diffplug/spotless/npm/EslintFormatterStep.java b/lib/src/main/java/com/diffplug/spotless/npm/EslintFormatterStep.java index 74979d90aa..221a745ad7 100644 --- a/lib/src/main/java/com/diffplug/spotless/npm/EslintFormatterStep.java +++ b/lib/src/main/java/com/diffplug/spotless/npm/EslintFormatterStep.java @@ -39,6 +39,8 @@ import com.diffplug.spotless.ThrowingEx; import com.diffplug.spotless.npm.EslintRestService.FormatOption; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + public class EslintFormatterStep { private static final Logger logger = LoggerFactory.getLogger(EslintFormatterStep.class); @@ -81,7 +83,10 @@ public static FormatterStep create(Map devDependencies, Provisio private static class State extends NpmFormatterStepStateBase implements Serializable { private static final long serialVersionUID = -539537027004745812L; - private final EslintConfig eslintConfig; + private final EslintConfig origEslintConfig; + + @SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED") + private transient EslintConfig eslintConfigInUse; State(String stepName, Map devDependencies, File projectDir, File buildDir, NpmPathResolver npmPathResolver, EslintConfig eslintConfig) throws IOException { super(stepName, @@ -97,21 +102,23 @@ private static class State extends NpmFormatterStepStateBase implements Serializ new NpmFormatterStepLocations( projectDir, buildDir, - npmPathResolver.resolveNpmExecutable(), - npmPathResolver.resolveNodeExecutable())); - this.eslintConfig = localCopyFiles(requireNonNull(eslintConfig)); + npmPathResolver::resolveNpmExecutable, + npmPathResolver::resolveNodeExecutable)); + this.origEslintConfig = requireNonNull(eslintConfig.verify()); + this.eslintConfigInUse = eslintConfig; } - private EslintConfig localCopyFiles(EslintConfig orig) { - if (orig.getEslintConfigPath() == null) { - return orig.verify(); + @Override + protected void prepareNodeServerLayout() throws IOException { + super.prepareNodeServerLayout(); + if (origEslintConfig.getEslintConfigPath() != null) { + // If any config files are provided, we need to make sure they are at the same location as the node modules + // as eslint will try to resolve plugin/config names relatively to the config file location and some + // eslint configs contain relative paths to additional config files (such as tsconfig.json e.g.) + FormattedPrinter.SYSOUT.print("Copying config file <%s> to <%s> and using the copy", origEslintConfig.getEslintConfigPath(), nodeServerLayout.nodeModulesDir()); + File configFileCopy = NpmResourceHelper.copyFileToDir(origEslintConfig.getEslintConfigPath(), nodeServerLayout.nodeModulesDir()); + this.eslintConfigInUse = this.origEslintConfig.withEslintConfigPath(configFileCopy).verify(); } - // If any config files are provided, we need to make sure they are at the same location as the node modules - // as eslint will try to resolve plugin/config names relatively to the config file location and some - // eslint configs contain relative paths to additional config files (such as tsconfig.json e.g.) - FormattedPrinter.SYSOUT.print("Copying config file <%s> to <%s> and using the copy", orig.getEslintConfigPath(), nodeModulesDir); - File configFileCopy = NpmResourceHelper.copyFileToDir(orig.getEslintConfigPath(), nodeModulesDir); - return orig.withEslintConfigPath(configFileCopy).verify(); } @Override @@ -121,7 +128,7 @@ public FormatterFunc createFormatterFunc() { FormattedPrinter.SYSOUT.print("creating formatter function (starting server)"); ServerProcessInfo eslintRestServer = npmRunServer(); EslintRestService restService = new EslintRestService(eslintRestServer.getBaseUrl()); - return Closeable.ofDangerous(() -> endServer(restService, eslintRestServer), new EslintFilePathPassingFormatterFunc(locations.projectDir(), nodeModulesDir, eslintConfig, restService)); + return Closeable.ofDangerous(() -> endServer(restService, eslintRestServer), new EslintFilePathPassingFormatterFunc(locations.projectDir(), nodeServerLayout.nodeModulesDir(), eslintConfigInUse, restService)); } catch (IOException e) { throw ThrowingEx.asRuntime(e); } diff --git a/lib/src/main/java/com/diffplug/spotless/npm/NodeServerLayout.java b/lib/src/main/java/com/diffplug/spotless/npm/NodeServerLayout.java index a175080517..63a6a923da 100644 --- a/lib/src/main/java/com/diffplug/spotless/npm/NodeServerLayout.java +++ b/lib/src/main/java/com/diffplug/spotless/npm/NodeServerLayout.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 DiffPlug + * Copyright 2020-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,11 @@ package com.diffplug.spotless.npm; import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.stream.Stream; + +import com.diffplug.spotless.ThrowingEx; class NodeServerLayout { @@ -50,4 +55,31 @@ public File npmrcFile() { static File getBuildDirFromNodeModulesDir(File nodeModulesDir) { return nodeModulesDir.getParentFile(); } + + public boolean isLayoutPrepared() { + if (!nodeModulesDir().isDirectory()) { + return false; + } + if (!packageJsonFile().isFile()) { + return false; + } + if (!serveJsFile().isFile()) { + return false; + } + // npmrc is optional, so must not be checked here + return true; + } + + public boolean isNodeModulesPrepared() { + Path nodeModulesInstallDirPath = new File(nodeModulesDir(), "node_modules").toPath(); + if (!Files.isDirectory(nodeModulesInstallDirPath)) { + return false; + } + // check if it is NOT empty + return ThrowingEx.get(() -> { + try (Stream entries = Files.list(nodeModulesInstallDirPath)) { + return entries.findFirst().isPresent(); + } + }); + } } diff --git a/lib/src/main/java/com/diffplug/spotless/npm/NpmFormatterStepLocations.java b/lib/src/main/java/com/diffplug/spotless/npm/NpmFormatterStepLocations.java index 0c417733e8..0e99e1afab 100644 --- a/lib/src/main/java/com/diffplug/spotless/npm/NpmFormatterStepLocations.java +++ b/lib/src/main/java/com/diffplug/spotless/npm/NpmFormatterStepLocations.java @@ -19,6 +19,7 @@ import java.io.File; import java.io.Serializable; +import java.util.function.Supplier; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; @@ -32,12 +33,12 @@ class NpmFormatterStepLocations implements Serializable { private final transient File buildDir; @SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED") - private final transient File npmExecutable; + private final transient Supplier npmExecutable; @SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED") - private final transient File nodeExecutable; + private final transient Supplier nodeExecutable; - public NpmFormatterStepLocations(File projectDir, File buildDir, File npmExecutable, File nodeExecutable) { + public NpmFormatterStepLocations(File projectDir, File buildDir, Supplier npmExecutable, Supplier nodeExecutable) { this.projectDir = requireNonNull(projectDir); this.buildDir = requireNonNull(buildDir); this.npmExecutable = requireNonNull(npmExecutable); @@ -53,10 +54,10 @@ public File buildDir() { } public File npmExecutable() { - return npmExecutable; + return npmExecutable.get(); } public File nodeExecutable() { - return nodeExecutable; + return nodeExecutable.get(); } } diff --git a/lib/src/main/java/com/diffplug/spotless/npm/NpmFormatterStepStateBase.java b/lib/src/main/java/com/diffplug/spotless/npm/NpmFormatterStepStateBase.java index 4f555a177e..92ac7df6f2 100644 --- a/lib/src/main/java/com/diffplug/spotless/npm/NpmFormatterStepStateBase.java +++ b/lib/src/main/java/com/diffplug/spotless/npm/NpmFormatterStepStateBase.java @@ -31,8 +31,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.diffplug.spotless.FileSignature; import com.diffplug.spotless.FormatterFunc; +import com.diffplug.spotless.ProcessRunner.LongRunningProcess; +import com.diffplug.spotless.ThrowingEx; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; @@ -42,11 +43,8 @@ abstract class NpmFormatterStepStateBase implements Serializable { private static final long serialVersionUID = 1460749955865959948L; - @SuppressWarnings("unused") - private final FileSignature packageJsonSignature; - @SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED") - public final transient File nodeModulesDir; + protected final transient NodeServerLayout nodeServerLayout; public final NpmFormatterStepLocations locations; @@ -58,45 +56,61 @@ protected NpmFormatterStepStateBase(String stepName, NpmConfig npmConfig, NpmFor this.stepName = requireNonNull(stepName); this.npmConfig = requireNonNull(npmConfig); this.locations = locations; - NodeServerLayout layout = prepareNodeServer(locations.buildDir()); - this.nodeModulesDir = layout.nodeModulesDir(); - this.packageJsonSignature = FileSignature.signAsList(layout.packageJsonFile()); + this.nodeServerLayout = new NodeServerLayout(locations.buildDir(), stepName); } - private NodeServerLayout prepareNodeServer(File buildDir) throws IOException { - NodeServerLayout layout = new NodeServerLayout(buildDir, stepName); - NpmResourceHelper.assertDirectoryExists(layout.nodeModulesDir()); - NpmResourceHelper.writeUtf8StringToFile(layout.packageJsonFile(), + protected void prepareNodeServerLayout() throws IOException { + NpmResourceHelper.assertDirectoryExists(nodeServerLayout.nodeModulesDir()); + NpmResourceHelper.writeUtf8StringToFile(nodeServerLayout.packageJsonFile(), this.npmConfig.getPackageJsonContent()); NpmResourceHelper - .writeUtf8StringToFile(layout.serveJsFile(), this.npmConfig.getServeScriptContent()); + .writeUtf8StringToFile(nodeServerLayout.serveJsFile(), this.npmConfig.getServeScriptContent()); if (this.npmConfig.getNpmrcContent() != null) { - NpmResourceHelper.writeUtf8StringToFile(layout.npmrcFile(), this.npmConfig.getNpmrcContent()); + NpmResourceHelper.writeUtf8StringToFile(nodeServerLayout.npmrcFile(), this.npmConfig.getNpmrcContent()); } else { - NpmResourceHelper.deleteFileIfExists(layout.npmrcFile()); + NpmResourceHelper.deleteFileIfExists(nodeServerLayout.npmrcFile()); } + } + + protected void prepareNodeServer() throws IOException { FormattedPrinter.SYSOUT.print("running npm install"); - runNpmInstall(layout.nodeModulesDir()); + runNpmInstall(nodeServerLayout.nodeModulesDir()); FormattedPrinter.SYSOUT.print("npm install finished"); - return layout; } private void runNpmInstall(File npmProjectDir) throws IOException { new NpmProcess(npmProjectDir, this.locations.npmExecutable(), this.locations.nodeExecutable()).install(); } - protected ServerProcessInfo npmRunServer() throws ServerStartException, IOException { - if (!this.nodeModulesDir.exists()) { - prepareNodeServer(NodeServerLayout.getBuildDirFromNodeModulesDir(this.nodeModulesDir)); + protected void assertNodeServerDirReady() throws IOException { + if (needsPrepareNodeServerLayout()) { + // reinstall if missing + prepareNodeServerLayout(); + } + if (needsPrepareNodeServer()) { + // run npm install if node_modules is missing + prepareNodeServer(); } + } + protected boolean needsPrepareNodeServer() { + return !this.nodeServerLayout.isNodeModulesPrepared(); + } + + protected boolean needsPrepareNodeServerLayout() { + return !this.nodeServerLayout.isLayoutPrepared(); + } + + protected ServerProcessInfo npmRunServer() throws ServerStartException, IOException { + assertNodeServerDirReady(); + LongRunningProcess server = null; try { // The npm process will output the randomly selected port of the http server process to 'server.port' file // so in order to be safe, remove such a file if it exists before starting. - final File serverPortFile = new File(this.nodeModulesDir, "server.port"); + final File serverPortFile = new File(this.nodeServerLayout.nodeModulesDir(), "server.port"); NpmResourceHelper.deleteFileIfExists(serverPortFile); // start the http server in node - Process server = new NpmProcess(this.nodeModulesDir, this.locations.npmExecutable(), this.locations.nodeExecutable()).start(); + server = new NpmProcess(this.nodeServerLayout.nodeModulesDir(), this.locations.npmExecutable(), this.locations.nodeExecutable()).start(); // await the readiness of the http server - wait for at most 60 seconds try { @@ -117,7 +131,7 @@ protected ServerProcessInfo npmRunServer() throws ServerStartException, IOExcept String serverPort = NpmResourceHelper.readUtf8StringFromFile(serverPortFile).trim(); return new ServerProcessInfo(server, serverPort, serverPortFile); } catch (IOException | TimeoutException e) { - throw new ServerStartException(e); + throw new ServerStartException("Starting server failed." + (server != null ? "\n\nProcess result:\n" + ThrowingEx.get(server::result) : ""), e); } } @@ -186,7 +200,7 @@ public void close() throws Exception { protected static class ServerStartException extends RuntimeException { private static final long serialVersionUID = -8803977379866483002L; - public ServerStartException(Throwable cause) { + public ServerStartException(String message, Throwable cause) { super(cause); } } diff --git a/lib/src/main/java/com/diffplug/spotless/npm/PrettierFormatterStep.java b/lib/src/main/java/com/diffplug/spotless/npm/PrettierFormatterStep.java index 22dd4586ee..22f1a69e7c 100644 --- a/lib/src/main/java/com/diffplug/spotless/npm/PrettierFormatterStep.java +++ b/lib/src/main/java/com/diffplug/spotless/npm/PrettierFormatterStep.java @@ -77,8 +77,8 @@ private static class State extends NpmFormatterStepStateBase implements Serializ new NpmFormatterStepLocations( projectDir, buildDir, - npmPathResolver.resolveNpmExecutable(), - npmPathResolver.resolveNodeExecutable())); + npmPathResolver::resolveNpmExecutable, + npmPathResolver::resolveNodeExecutable)); this.prettierConfig = requireNonNull(prettierConfig); } diff --git a/lib/src/main/java/com/diffplug/spotless/npm/TsFmtFormatterStep.java b/lib/src/main/java/com/diffplug/spotless/npm/TsFmtFormatterStep.java index 14d6b1bbd0..4bd665c764 100644 --- a/lib/src/main/java/com/diffplug/spotless/npm/TsFmtFormatterStep.java +++ b/lib/src/main/java/com/diffplug/spotless/npm/TsFmtFormatterStep.java @@ -83,8 +83,8 @@ public State(String stepName, Map versions, File projectDir, Fil new NpmFormatterStepLocations( projectDir, buildDir, - npmPathResolver.resolveNpmExecutable(), - npmPathResolver.resolveNodeExecutable())); + npmPathResolver::resolveNpmExecutable, + npmPathResolver::resolveNodeExecutable)); this.buildDir = requireNonNull(buildDir); this.configFile = configFile; this.inlineTsFmtSettings = inlineTsFmtSettings == null ? new TreeMap<>() : new TreeMap<>(inlineTsFmtSettings); diff --git a/plugin-gradle/CHANGES.md b/plugin-gradle/CHANGES.md index 37d08f9b75..cbcd3bd953 100644 --- a/plugin-gradle/CHANGES.md +++ b/plugin-gradle/CHANGES.md @@ -15,6 +15,9 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( * **POTENTIALLY BREAKING** Removed support for KtLint 0.3x and 0.45.2 ([#1475](https://github.com/diffplug/spotless/pull/1475)) * `KtLint` does not maintain a stable API - before this PR, we supported every breaking change in the API since 2019. * From now on, we will support no more than 2 breaking changes at a time. +* `npm`-based formatters `ESLint`, `prettier` and `tsfmt` delay their `npm install` call until the formatters are first + used. For gradle this effectively moves the `npm install` call out of the configuration phase and as such enables + better integration with `gradle-node-plugin`. ([#1522](https://github.com/diffplug/spotless/pull/1522)) ## [6.13.0] - 2023-01-14 ### Added diff --git a/plugin-gradle/README.md b/plugin-gradle/README.md index b6e95807d9..7e486719e7 100644 --- a/plugin-gradle/README.md +++ b/plugin-gradle/README.md @@ -907,6 +907,13 @@ spotless { If you provide both `npmExecutable` and `nodeExecutable`, spotless will use these paths. If you specify only one of the two, spotless will assume the other one is in the same directory. +If you use the `gradle-node-plugin` ([github](https://github.com/node-gradle/gradle-node-plugin)), it is possible to use the +node- and npm-binaries dynamically installed by this plugin. See +[this](https://github.com/diffplug/spotless/blob/main/plugin-gradle/src/test/resources/com/diffplug/gradle/spotless/NpmTestsWithoutNpmInstallationTest_gradle_node_plugin_example_1.gradle) +or [this](https://github.com/diffplug/spotless/blob/main/plugin-gradle/src/test/resources/com/diffplug/gradle/spotless/NpmTestsWithoutNpmInstallationTest_gradle_node_plugin_example_2.gradle) example. + +```gradle + ### `.npmrc` detection Spotless picks up npm configuration stored in a `.npmrc` file either in the project directory or in your user home. 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 2283afccff..a11f1b0c39 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 @@ -166,7 +166,7 @@ protected FormatterStep createStep() { private void fixParserToTypescript() { if (this.prettierConfig == null) { - this.prettierConfig = Collections.singletonMap("parser", "typescript"); + this.prettierConfig = new TreeMap<>(Collections.singletonMap("parser", "typescript")); } else { final Object replaced = this.prettierConfig.put("parser", "typescript"); if (replaced != null) { diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/NpmTestsWithoutNpmInstallationTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/NpmTestsWithoutNpmInstallationTest.java index 916d853920..03d14292e9 100644 --- a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/NpmTestsWithoutNpmInstallationTest.java +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/NpmTestsWithoutNpmInstallationTest.java @@ -17,7 +17,6 @@ import org.assertj.core.api.Assertions; import org.gradle.testkit.runner.BuildResult; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import com.diffplug.common.base.Predicates; @@ -74,40 +73,23 @@ private void printContents() { } @Test - @Disabled("This test is disabled because we currently don't support using npm/node installed by the" + - "node-gradle-plugin as the installation takes place in the *execution* phase, but spotless needs it in the *configuration* phase.") - void useNodeAndNpmFromNodeGradlePluginInOneSweep() throws Exception { + void useNodeAndNpmFromNodeGradlePlugin_example1() throws Exception { try { - setFile("build.gradle").toLines( - "plugins {", - " id 'com.diffplug.spotless'", - " id 'com.github.node-gradle.node' version '3.5.1'", - "}", - "repositories { mavenCentral() }", - "node {", - " download = true", - " version = '18.13.0'", - " npmVersion = '8.19.2'", - " workDir = file(\"${buildDir}/nodejs\")", - " npmWorkDir = file(\"${buildDir}/npm\")", - "}", - "def prettierConfig = [:]", - "prettierConfig['printWidth'] = 50", - "prettierConfig['parser'] = 'typescript'", - "def npmExec = System.getProperty('os.name').toLowerCase().contains('windows') ? '/npm.cmd' : '/bin/npm'", - "def nodeExec = System.getProperty('os.name').toLowerCase().contains('windows') ? '/node.exe' : '/bin/node'", - "spotless {", - " format 'mytypescript', {", - " target 'test.ts'", - " prettier()", - " .npmExecutable(\"${tasks.named('npmSetup').get().npmDir.get()}${npmExec}\")", - " .nodeExecutable(\"${tasks.named('nodeSetup').get().nodeDir.get()}${nodeExec}\")", - " .config(prettierConfig)", - " }", - "}", - "tasks.named('spotlessMytypescript').configure {", - " it.dependsOn('nodeSetup', 'npmSetup')", - "}"); + setFile("build.gradle").toResource("com/diffplug/gradle/spotless/NpmTestsWithoutNpmInstallationTest_gradle_node_plugin_example_1.gradle"); + setFile("test.ts").toResource("npm/prettier/config/typescript.dirty"); + final BuildResult spotlessApply = gradleRunner().withArguments("--stacktrace", "spotlessApply").build(); + Assertions.assertThat(spotlessApply.getOutput()).contains("BUILD SUCCESSFUL"); + assertFile("test.ts").sameAsResource("npm/prettier/config/typescript.configfile.clean"); + } catch (Exception e) { + printContents(); + throw e; + } + } + + @Test + void useNpmFromNodeGradlePlugin_example2() throws Exception { + try { + setFile("build.gradle").toResource("com/diffplug/gradle/spotless/NpmTestsWithoutNpmInstallationTest_gradle_node_plugin_example_2.gradle"); setFile("test.ts").toResource("npm/prettier/config/typescript.dirty"); final BuildResult spotlessApply = gradleRunner().withArguments("--stacktrace", "spotlessApply").build(); Assertions.assertThat(spotlessApply.getOutput()).contains("BUILD SUCCESSFUL"); diff --git a/plugin-gradle/src/test/resources/com/diffplug/gradle/spotless/NpmTestsWithoutNpmInstallationTest_gradle_node_plugin_example_1.gradle b/plugin-gradle/src/test/resources/com/diffplug/gradle/spotless/NpmTestsWithoutNpmInstallationTest_gradle_node_plugin_example_1.gradle new file mode 100644 index 0000000000..7ec7a2c592 --- /dev/null +++ b/plugin-gradle/src/test/resources/com/diffplug/gradle/spotless/NpmTestsWithoutNpmInstallationTest_gradle_node_plugin_example_1.gradle @@ -0,0 +1,38 @@ +/** + * This example shows how to use the gradle-node-plugin to install node and npm in separate directories + * and use these binaries with the prettier formatter. + */ +plugins { + id 'com.diffplug.spotless' + id 'com.github.node-gradle.node' version '3.5.1' +} +repositories { mavenCentral() } +node { + download = true + version = '18.13.0' + npmVersion = '8.19.2' + // when setting both these directories, npm and node will be in separate directories + workDir = file("${buildDir}/nodejs") + npmWorkDir = file("${buildDir}/npm") +} +def prettierConfig = [:] +prettierConfig['printWidth'] = 50 +prettierConfig['parser'] = 'typescript' + +// the executable names +def npmExec = System.getProperty('os.name').toLowerCase().contains('windows') ? '/npm.cmd' : '/bin/npm' +def nodeExec = System.getProperty('os.name').toLowerCase().contains('windows') ? '/node.exe' : '/bin/node' + +spotless { + format 'mytypescript', { + target 'test.ts' + prettier() + .npmExecutable("${tasks.named('npmSetup').get().npmDir.get()}${npmExec}") // get the npm executable path from gradle-node-plugin + .nodeExecutable("${tasks.named('nodeSetup').get().nodeDir.get()}${nodeExec}") // get the node executable path from gradle-node-plugin + .config(prettierConfig) + } +} + +tasks.named('spotlessMytypescript').configure { + it.dependsOn('nodeSetup', 'npmSetup') +} diff --git a/plugin-gradle/src/test/resources/com/diffplug/gradle/spotless/NpmTestsWithoutNpmInstallationTest_gradle_node_plugin_example_2.gradle b/plugin-gradle/src/test/resources/com/diffplug/gradle/spotless/NpmTestsWithoutNpmInstallationTest_gradle_node_plugin_example_2.gradle new file mode 100644 index 0000000000..eb59b85cf0 --- /dev/null +++ b/plugin-gradle/src/test/resources/com/diffplug/gradle/spotless/NpmTestsWithoutNpmInstallationTest_gradle_node_plugin_example_2.gradle @@ -0,0 +1,35 @@ +/** + * This example shows how to use the gradle-node-plugin to install node and npm together and + * use these binaries with the prettier formatter. + */ +plugins { + id 'com.diffplug.spotless' + id 'com.github.node-gradle.node' version '3.5.1' +} +repositories { mavenCentral() } +node { + download = true + version = '18.13.0' + // when not setting an explicit `npmWorkDir`, the npm binary will be installed next to the node binary + workDir = file("${buildDir}/nodejs") +} +def prettierConfig = [:] +prettierConfig['printWidth'] = 50 +prettierConfig['parser'] = 'typescript' + +// the executable name +def npmExec = System.getProperty('os.name').toLowerCase().contains('windows') ? '/npm.cmd' : '/bin/npm' + +spotless { + typescript { + target 'test.ts' + prettier() + .npmExecutable("${tasks.named('npmSetup').get().npmDir.get()}${npmExec}") // get the npm executable path from gradle-node-plugin + // setting the nodeExecutable is not necessary, since it will be found in the same directory as the npm executable (see above) + .config(prettierConfig) + } +} + +tasks.named('spotlessTypescript').configure { + it.dependsOn('nodeSetup', 'npmSetup') +} diff --git a/plugin-maven/CHANGES.md b/plugin-maven/CHANGES.md index 2a23405d35..93dd005ab6 100644 --- a/plugin-maven/CHANGES.md +++ b/plugin-maven/CHANGES.md @@ -18,6 +18,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( * **POTENTIALLY BREAKING** Removed support for KtLint 0.3x and 0.45.2 ([#1475](https://github.com/diffplug/spotless/pull/1475)) * `KtLint` does not maintain a stable API - before this PR, we supported every breaking change in the API since 2019. * From now on, we will support no more than 2 breaking changes at a time. +* `npm`-based formatters `ESLint`, `prettier` and `tsfmt` delay their `npm install` call until the formatters are first used. ([#1522](https://github.com/diffplug/spotless/pull/1522) ## [2.30.0] - 2023-01-13 ### Added