Skip to content

Commit 41da6d5

Browse files
authored
add ability to use npm and node executables installed by node gradle plugin (#1522)
2 parents afab288 + 012e4ae commit 41da6d5

14 files changed

+205
-83
lines changed

CHANGES.md

+2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (
2323
* ** POTENTIALLY BREAKING** Removed support for KtLint 0.3x and 0.45.2 ([#1475](https://github.com/diffplug/spotless/pull/1475))
2424
* `KtLint` does not maintain a stable API - before this PR, we supported every breaking change in the API since 2019.
2525
* From now on, we will support no more than 2 breaking changes at a time.
26+
* NpmFormatterStepStateBase delays `npm install` call until the formatter is first used. This enables better integration
27+
with `gradle-node-plugin`. ([#1522](https://github.com/diffplug/spotless/pull/1522))
2628

2729
## [2.32.0] - 2023-01-13
2830
### Added

lib/src/main/java/com/diffplug/spotless/npm/EslintFormatterStep.java

+21-14
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@
3939
import com.diffplug.spotless.ThrowingEx;
4040
import com.diffplug.spotless.npm.EslintRestService.FormatOption;
4141

42+
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
43+
4244
public class EslintFormatterStep {
4345

4446
private static final Logger logger = LoggerFactory.getLogger(EslintFormatterStep.class);
@@ -81,7 +83,10 @@ public static FormatterStep create(Map<String, String> devDependencies, Provisio
8183
private static class State extends NpmFormatterStepStateBase implements Serializable {
8284

8385
private static final long serialVersionUID = -539537027004745812L;
84-
private final EslintConfig eslintConfig;
86+
private final EslintConfig origEslintConfig;
87+
88+
@SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED")
89+
private transient EslintConfig eslintConfigInUse;
8590

8691
State(String stepName, Map<String, String> devDependencies, File projectDir, File buildDir, NpmPathResolver npmPathResolver, EslintConfig eslintConfig) throws IOException {
8792
super(stepName,
@@ -97,21 +102,23 @@ private static class State extends NpmFormatterStepStateBase implements Serializ
97102
new NpmFormatterStepLocations(
98103
projectDir,
99104
buildDir,
100-
npmPathResolver.resolveNpmExecutable(),
101-
npmPathResolver.resolveNodeExecutable()));
102-
this.eslintConfig = localCopyFiles(requireNonNull(eslintConfig));
105+
npmPathResolver::resolveNpmExecutable,
106+
npmPathResolver::resolveNodeExecutable));
107+
this.origEslintConfig = requireNonNull(eslintConfig.verify());
108+
this.eslintConfigInUse = eslintConfig;
103109
}
104110

105-
private EslintConfig localCopyFiles(EslintConfig orig) {
106-
if (orig.getEslintConfigPath() == null) {
107-
return orig.verify();
111+
@Override
112+
protected void prepareNodeServerLayout() throws IOException {
113+
super.prepareNodeServerLayout();
114+
if (origEslintConfig.getEslintConfigPath() != null) {
115+
// If any config files are provided, we need to make sure they are at the same location as the node modules
116+
// as eslint will try to resolve plugin/config names relatively to the config file location and some
117+
// eslint configs contain relative paths to additional config files (such as tsconfig.json e.g.)
118+
FormattedPrinter.SYSOUT.print("Copying config file <%s> to <%s> and using the copy", origEslintConfig.getEslintConfigPath(), nodeServerLayout.nodeModulesDir());
119+
File configFileCopy = NpmResourceHelper.copyFileToDir(origEslintConfig.getEslintConfigPath(), nodeServerLayout.nodeModulesDir());
120+
this.eslintConfigInUse = this.origEslintConfig.withEslintConfigPath(configFileCopy).verify();
108121
}
109-
// If any config files are provided, we need to make sure they are at the same location as the node modules
110-
// as eslint will try to resolve plugin/config names relatively to the config file location and some
111-
// eslint configs contain relative paths to additional config files (such as tsconfig.json e.g.)
112-
FormattedPrinter.SYSOUT.print("Copying config file <%s> to <%s> and using the copy", orig.getEslintConfigPath(), nodeModulesDir);
113-
File configFileCopy = NpmResourceHelper.copyFileToDir(orig.getEslintConfigPath(), nodeModulesDir);
114-
return orig.withEslintConfigPath(configFileCopy).verify();
115122
}
116123

117124
@Override
@@ -121,7 +128,7 @@ public FormatterFunc createFormatterFunc() {
121128
FormattedPrinter.SYSOUT.print("creating formatter function (starting server)");
122129
ServerProcessInfo eslintRestServer = npmRunServer();
123130
EslintRestService restService = new EslintRestService(eslintRestServer.getBaseUrl());
124-
return Closeable.ofDangerous(() -> endServer(restService, eslintRestServer), new EslintFilePathPassingFormatterFunc(locations.projectDir(), nodeModulesDir, eslintConfig, restService));
131+
return Closeable.ofDangerous(() -> endServer(restService, eslintRestServer), new EslintFilePathPassingFormatterFunc(locations.projectDir(), nodeServerLayout.nodeModulesDir(), eslintConfigInUse, restService));
125132
} catch (IOException e) {
126133
throw ThrowingEx.asRuntime(e);
127134
}

lib/src/main/java/com/diffplug/spotless/npm/NodeServerLayout.java

+33-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020-2021 DiffPlug
2+
* Copyright 2020-2023 DiffPlug
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,6 +16,11 @@
1616
package com.diffplug.spotless.npm;
1717

1818
import java.io.File;
19+
import java.nio.file.Files;
20+
import java.nio.file.Path;
21+
import java.util.stream.Stream;
22+
23+
import com.diffplug.spotless.ThrowingEx;
1924

2025
class NodeServerLayout {
2126

@@ -50,4 +55,31 @@ public File npmrcFile() {
5055
static File getBuildDirFromNodeModulesDir(File nodeModulesDir) {
5156
return nodeModulesDir.getParentFile();
5257
}
58+
59+
public boolean isLayoutPrepared() {
60+
if (!nodeModulesDir().isDirectory()) {
61+
return false;
62+
}
63+
if (!packageJsonFile().isFile()) {
64+
return false;
65+
}
66+
if (!serveJsFile().isFile()) {
67+
return false;
68+
}
69+
// npmrc is optional, so must not be checked here
70+
return true;
71+
}
72+
73+
public boolean isNodeModulesPrepared() {
74+
Path nodeModulesInstallDirPath = new File(nodeModulesDir(), "node_modules").toPath();
75+
if (!Files.isDirectory(nodeModulesInstallDirPath)) {
76+
return false;
77+
}
78+
// check if it is NOT empty
79+
return ThrowingEx.get(() -> {
80+
try (Stream<Path> entries = Files.list(nodeModulesInstallDirPath)) {
81+
return entries.findFirst().isPresent();
82+
}
83+
});
84+
}
5385
}

lib/src/main/java/com/diffplug/spotless/npm/NpmFormatterStepLocations.java

+6-5
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
import java.io.File;
2121
import java.io.Serializable;
22+
import java.util.function.Supplier;
2223

2324
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
2425

@@ -32,12 +33,12 @@ class NpmFormatterStepLocations implements Serializable {
3233
private final transient File buildDir;
3334

3435
@SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED")
35-
private final transient File npmExecutable;
36+
private final transient Supplier<File> npmExecutable;
3637

3738
@SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED")
38-
private final transient File nodeExecutable;
39+
private final transient Supplier<File> nodeExecutable;
3940

40-
public NpmFormatterStepLocations(File projectDir, File buildDir, File npmExecutable, File nodeExecutable) {
41+
public NpmFormatterStepLocations(File projectDir, File buildDir, Supplier<File> npmExecutable, Supplier<File> nodeExecutable) {
4142
this.projectDir = requireNonNull(projectDir);
4243
this.buildDir = requireNonNull(buildDir);
4344
this.npmExecutable = requireNonNull(npmExecutable);
@@ -53,10 +54,10 @@ public File buildDir() {
5354
}
5455

5556
public File npmExecutable() {
56-
return npmExecutable;
57+
return npmExecutable.get();
5758
}
5859

5960
public File nodeExecutable() {
60-
return nodeExecutable;
61+
return nodeExecutable.get();
6162
}
6263
}

lib/src/main/java/com/diffplug/spotless/npm/NpmFormatterStepStateBase.java

+38-24
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,9 @@
3131
import org.slf4j.Logger;
3232
import org.slf4j.LoggerFactory;
3333

34-
import com.diffplug.spotless.FileSignature;
3534
import com.diffplug.spotless.FormatterFunc;
35+
import com.diffplug.spotless.ProcessRunner.LongRunningProcess;
36+
import com.diffplug.spotless.ThrowingEx;
3637

3738
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
3839

@@ -42,11 +43,8 @@ abstract class NpmFormatterStepStateBase implements Serializable {
4243

4344
private static final long serialVersionUID = 1460749955865959948L;
4445

45-
@SuppressWarnings("unused")
46-
private final FileSignature packageJsonSignature;
47-
4846
@SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED")
49-
public final transient File nodeModulesDir;
47+
protected final transient NodeServerLayout nodeServerLayout;
5048

5149
public final NpmFormatterStepLocations locations;
5250

@@ -58,45 +56,61 @@ protected NpmFormatterStepStateBase(String stepName, NpmConfig npmConfig, NpmFor
5856
this.stepName = requireNonNull(stepName);
5957
this.npmConfig = requireNonNull(npmConfig);
6058
this.locations = locations;
61-
NodeServerLayout layout = prepareNodeServer(locations.buildDir());
62-
this.nodeModulesDir = layout.nodeModulesDir();
63-
this.packageJsonSignature = FileSignature.signAsList(layout.packageJsonFile());
59+
this.nodeServerLayout = new NodeServerLayout(locations.buildDir(), stepName);
6460
}
6561

66-
private NodeServerLayout prepareNodeServer(File buildDir) throws IOException {
67-
NodeServerLayout layout = new NodeServerLayout(buildDir, stepName);
68-
NpmResourceHelper.assertDirectoryExists(layout.nodeModulesDir());
69-
NpmResourceHelper.writeUtf8StringToFile(layout.packageJsonFile(),
62+
protected void prepareNodeServerLayout() throws IOException {
63+
NpmResourceHelper.assertDirectoryExists(nodeServerLayout.nodeModulesDir());
64+
NpmResourceHelper.writeUtf8StringToFile(nodeServerLayout.packageJsonFile(),
7065
this.npmConfig.getPackageJsonContent());
7166
NpmResourceHelper
72-
.writeUtf8StringToFile(layout.serveJsFile(), this.npmConfig.getServeScriptContent());
67+
.writeUtf8StringToFile(nodeServerLayout.serveJsFile(), this.npmConfig.getServeScriptContent());
7368
if (this.npmConfig.getNpmrcContent() != null) {
74-
NpmResourceHelper.writeUtf8StringToFile(layout.npmrcFile(), this.npmConfig.getNpmrcContent());
69+
NpmResourceHelper.writeUtf8StringToFile(nodeServerLayout.npmrcFile(), this.npmConfig.getNpmrcContent());
7570
} else {
76-
NpmResourceHelper.deleteFileIfExists(layout.npmrcFile());
71+
NpmResourceHelper.deleteFileIfExists(nodeServerLayout.npmrcFile());
7772
}
73+
}
74+
75+
protected void prepareNodeServer() throws IOException {
7876
FormattedPrinter.SYSOUT.print("running npm install");
79-
runNpmInstall(layout.nodeModulesDir());
77+
runNpmInstall(nodeServerLayout.nodeModulesDir());
8078
FormattedPrinter.SYSOUT.print("npm install finished");
81-
return layout;
8279
}
8380

8481
private void runNpmInstall(File npmProjectDir) throws IOException {
8582
new NpmProcess(npmProjectDir, this.locations.npmExecutable(), this.locations.nodeExecutable()).install();
8683
}
8784

88-
protected ServerProcessInfo npmRunServer() throws ServerStartException, IOException {
89-
if (!this.nodeModulesDir.exists()) {
90-
prepareNodeServer(NodeServerLayout.getBuildDirFromNodeModulesDir(this.nodeModulesDir));
85+
protected void assertNodeServerDirReady() throws IOException {
86+
if (needsPrepareNodeServerLayout()) {
87+
// reinstall if missing
88+
prepareNodeServerLayout();
89+
}
90+
if (needsPrepareNodeServer()) {
91+
// run npm install if node_modules is missing
92+
prepareNodeServer();
9193
}
94+
}
9295

96+
protected boolean needsPrepareNodeServer() {
97+
return !this.nodeServerLayout.isNodeModulesPrepared();
98+
}
99+
100+
protected boolean needsPrepareNodeServerLayout() {
101+
return !this.nodeServerLayout.isLayoutPrepared();
102+
}
103+
104+
protected ServerProcessInfo npmRunServer() throws ServerStartException, IOException {
105+
assertNodeServerDirReady();
106+
LongRunningProcess server = null;
93107
try {
94108
// The npm process will output the randomly selected port of the http server process to 'server.port' file
95109
// so in order to be safe, remove such a file if it exists before starting.
96-
final File serverPortFile = new File(this.nodeModulesDir, "server.port");
110+
final File serverPortFile = new File(this.nodeServerLayout.nodeModulesDir(), "server.port");
97111
NpmResourceHelper.deleteFileIfExists(serverPortFile);
98112
// start the http server in node
99-
Process server = new NpmProcess(this.nodeModulesDir, this.locations.npmExecutable(), this.locations.nodeExecutable()).start();
113+
server = new NpmProcess(this.nodeServerLayout.nodeModulesDir(), this.locations.npmExecutable(), this.locations.nodeExecutable()).start();
100114

101115
// await the readiness of the http server - wait for at most 60 seconds
102116
try {
@@ -117,7 +131,7 @@ protected ServerProcessInfo npmRunServer() throws ServerStartException, IOExcept
117131
String serverPort = NpmResourceHelper.readUtf8StringFromFile(serverPortFile).trim();
118132
return new ServerProcessInfo(server, serverPort, serverPortFile);
119133
} catch (IOException | TimeoutException e) {
120-
throw new ServerStartException(e);
134+
throw new ServerStartException("Starting server failed." + (server != null ? "\n\nProcess result:\n" + ThrowingEx.get(server::result) : ""), e);
121135
}
122136
}
123137

@@ -186,7 +200,7 @@ public void close() throws Exception {
186200
protected static class ServerStartException extends RuntimeException {
187201
private static final long serialVersionUID = -8803977379866483002L;
188202

189-
public ServerStartException(Throwable cause) {
203+
public ServerStartException(String message, Throwable cause) {
190204
super(cause);
191205
}
192206
}

lib/src/main/java/com/diffplug/spotless/npm/PrettierFormatterStep.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,8 @@ private static class State extends NpmFormatterStepStateBase implements Serializ
7777
new NpmFormatterStepLocations(
7878
projectDir,
7979
buildDir,
80-
npmPathResolver.resolveNpmExecutable(),
81-
npmPathResolver.resolveNodeExecutable()));
80+
npmPathResolver::resolveNpmExecutable,
81+
npmPathResolver::resolveNodeExecutable));
8282
this.prettierConfig = requireNonNull(prettierConfig);
8383
}
8484

lib/src/main/java/com/diffplug/spotless/npm/TsFmtFormatterStep.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,8 @@ public State(String stepName, Map<String, String> versions, File projectDir, Fil
8383
new NpmFormatterStepLocations(
8484
projectDir,
8585
buildDir,
86-
npmPathResolver.resolveNpmExecutable(),
87-
npmPathResolver.resolveNodeExecutable()));
86+
npmPathResolver::resolveNpmExecutable,
87+
npmPathResolver::resolveNodeExecutable));
8888
this.buildDir = requireNonNull(buildDir);
8989
this.configFile = configFile;
9090
this.inlineTsFmtSettings = inlineTsFmtSettings == null ? new TreeMap<>() : new TreeMap<>(inlineTsFmtSettings);

plugin-gradle/CHANGES.md

+3
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (
1515
* **POTENTIALLY BREAKING** Removed support for KtLint 0.3x and 0.45.2 ([#1475](https://github.com/diffplug/spotless/pull/1475))
1616
* `KtLint` does not maintain a stable API - before this PR, we supported every breaking change in the API since 2019.
1717
* From now on, we will support no more than 2 breaking changes at a time.
18+
* `npm`-based formatters `ESLint`, `prettier` and `tsfmt` delay their `npm install` call until the formatters are first
19+
used. For gradle this effectively moves the `npm install` call out of the configuration phase and as such enables
20+
better integration with `gradle-node-plugin`. ([#1522](https://github.com/diffplug/spotless/pull/1522))
1821

1922
## [6.13.0] - 2023-01-14
2023
### Added

plugin-gradle/README.md

+7
Original file line numberDiff line numberDiff line change
@@ -907,6 +907,13 @@ spotless {
907907
If you provide both `npmExecutable` and `nodeExecutable`, spotless will use these paths. If you specify only one of the
908908
two, spotless will assume the other one is in the same directory.
909909
910+
If you use the `gradle-node-plugin` ([github](https://github.com/node-gradle/gradle-node-plugin)), it is possible to use the
911+
node- and npm-binaries dynamically installed by this plugin. See
912+
[this](https://github.com/diffplug/spotless/blob/main/plugin-gradle/src/test/resources/com/diffplug/gradle/spotless/NpmTestsWithoutNpmInstallationTest_gradle_node_plugin_example_1.gradle)
913+
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.
914+
915+
```gradle
916+
910917
### `.npmrc` detection
911918
912919
Spotless picks up npm configuration stored in a `.npmrc` file either in the project directory or in your user home.

plugin-gradle/src/main/java/com/diffplug/gradle/spotless/TypescriptExtension.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ protected FormatterStep createStep() {
166166

167167
private void fixParserToTypescript() {
168168
if (this.prettierConfig == null) {
169-
this.prettierConfig = Collections.singletonMap("parser", "typescript");
169+
this.prettierConfig = new TreeMap<>(Collections.singletonMap("parser", "typescript"));
170170
} else {
171171
final Object replaced = this.prettierConfig.put("parser", "typescript");
172172
if (replaced != null) {

0 commit comments

Comments
 (0)