Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add ability to specify node executable for npm based formatters #1500

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (
## [Unreleased]
### Added
* `ProcessRunner` has added some convenience methods so it can be used for maven testing. ([#1496](https://github.com/diffplug/spotless/pull/1496))
* Allow to specify node executable for node-based formatters using `nodeExecutable` parameter ([#1500](https://github.com/diffplug/spotless/pull/1500))
### Fixed
* The default list of type annotations used by `formatAnnotations` has had 8 more annotations from the Checker Framework added [#1494](https://github.com/diffplug/spotless/pull/1494)
### Changes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,11 @@ private static class State extends NpmFormatterStepStateBase implements Serializ
"/com/diffplug/spotless/npm/common-serve.js",
"/com/diffplug/spotless/npm/eslint-serve.js"),
npmPathResolver.resolveNpmrcContent()),
projectDir,
buildDir,
npmPathResolver.resolveNpmExecutable());
new NpmFormatterStepLocations(
projectDir,
buildDir,
npmPathResolver.resolveNpmExecutable(),
npmPathResolver.resolveNodeExecutable()));
this.eslintConfig = localCopyFiles(requireNonNull(eslintConfig));
}

Expand All @@ -119,7 +121,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(projectDir, nodeModulesDir, eslintConfig, restService));
return Closeable.ofDangerous(() -> endServer(restService, eslintRestServer), new EslintFilePathPassingFormatterFunc(locations.projectDir(), nodeModulesDir, eslintConfig, restService));
} catch (IOException e) {
throw ThrowingEx.asRuntime(e);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* 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.npm;

import java.io.File;
import java.util.Optional;

class NodeExecutableResolver {

private NodeExecutableResolver() {
// no instance
}

static String nodeExecutableName() {
String nodeName = "node";
if (PlatformInfo.normalizedOS() == PlatformInfo.OS.WINDOWS) {
nodeName += ".exe";
}
return nodeName;
}

static Optional<File> tryFindNextTo(File npmExecutable) {
if (npmExecutable == null) {
return Optional.empty();
}
File nodeExecutable = new File(npmExecutable.getParentFile(), nodeExecutableName());
if (nodeExecutable.exists() && nodeExecutable.isFile() && nodeExecutable.canExecute()) {
return Optional.of(nodeExecutable);
}
return Optional.empty();
}

public static String explainMessage() {
return "Spotless was unable to find a node executable.\n" +
"Either specify the node executable explicitly or make sure it can be found next to the npm executable.";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* 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.npm;

import static java.util.Objects.requireNonNull;

import java.io.File;
import java.io.Serializable;

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

class NpmFormatterStepLocations implements Serializable {

private static final long serialVersionUID = -1055408537924029969L;
@SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED")
private final transient File projectDir;

@SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED")
private final transient File buildDir;

@SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED")
private final transient File npmExecutable;

@SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED")
private final transient File nodeExecutable;

public NpmFormatterStepLocations(File projectDir, File buildDir, File npmExecutable, File nodeExecutable) {
this.projectDir = requireNonNull(projectDir);
this.buildDir = requireNonNull(buildDir);
this.npmExecutable = requireNonNull(npmExecutable);
this.nodeExecutable = requireNonNull(nodeExecutable);
}

public File projectDir() {
return projectDir;
}

public File buildDir() {
return buildDir;
}

public File npmExecutable() {
return npmExecutable;
}

public File nodeExecutable() {
return nodeExecutable;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,23 +48,17 @@ abstract class NpmFormatterStepStateBase implements Serializable {
@SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED")
public final transient File nodeModulesDir;

@SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED")
private final transient File npmExecutable;

@SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED")
public final transient File projectDir;
public final NpmFormatterStepLocations locations;

private final NpmConfig npmConfig;

private final String stepName;

protected NpmFormatterStepStateBase(String stepName, NpmConfig npmConfig, File projectDir, File buildDir, File npm) throws IOException {
protected NpmFormatterStepStateBase(String stepName, NpmConfig npmConfig, NpmFormatterStepLocations locations) throws IOException {
this.stepName = requireNonNull(stepName);
this.npmConfig = requireNonNull(npmConfig);
this.projectDir = requireNonNull(projectDir);
this.npmExecutable = npm;

NodeServerLayout layout = prepareNodeServer(buildDir);
this.locations = locations;
NodeServerLayout layout = prepareNodeServer(locations.buildDir());
this.nodeModulesDir = layout.nodeModulesDir();
this.packageJsonSignature = FileSignature.signAsList(layout.packageJsonFile());
}
Expand All @@ -88,7 +82,7 @@ private NodeServerLayout prepareNodeServer(File buildDir) throws IOException {
}

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

protected ServerProcessInfo npmRunServer() throws ServerStartException, IOException {
Expand All @@ -102,7 +96,7 @@ protected ServerProcessInfo npmRunServer() throws ServerStartException, IOExcept
final File serverPortFile = new File(this.nodeModulesDir, "server.port");
NpmResourceHelper.deleteFileIfExists(serverPortFile);
// start the http server in node
Process server = new NpmProcess(this.nodeModulesDir, this.npmExecutable).start();
Process server = new NpmProcess(this.nodeModulesDir, this.locations.npmExecutable(), this.locations.nodeExecutable()).start();

// await the readiness of the http server - wait for at most 60 seconds
try {
Expand Down
53 changes: 46 additions & 7 deletions lib/src/main/java/com/diffplug/spotless/npm/NpmPathResolver.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2020 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.
Expand All @@ -16,28 +16,67 @@
package com.diffplug.spotless.npm;

import java.io.File;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;

public class NpmPathResolver {

private final File explicitNpmExecutable;

private final File explicitNodeExecutable;

private final File explicitNpmrcFile;

private final List<File> additionalNpmrcLocations;

public NpmPathResolver(File explicitNpmExecutable, File explicitNpmrcFile, File... additionalNpmrcLocations) {
public NpmPathResolver(File explicitNpmExecutable, File explicitNodeExecutable, File explicitNpmrcFile, List<File> additionalNpmrcLocations) {
this.explicitNpmExecutable = explicitNpmExecutable;
this.explicitNodeExecutable = explicitNodeExecutable;
this.explicitNpmrcFile = explicitNpmrcFile;
this.additionalNpmrcLocations = Arrays.asList(additionalNpmrcLocations);
this.additionalNpmrcLocations = Collections.unmodifiableList(new ArrayList<>(additionalNpmrcLocations));
}

/**
* Finds the npm executable to use.
* <br>
* Either the explicit npm executable is returned, or - if an explicit node executable is configured - tries to find
* the npm executable relative to the node executable.
* Falls back to looking for npm on the user's system using {@link NpmExecutableResolver}
*
* @return the npm executable to use
* @throws IllegalStateException if no npm executable could be found
*/
public File resolveNpmExecutable() {
return Optional.ofNullable(this.explicitNpmExecutable)
.orElseGet(() -> NpmExecutableResolver.tryFind()
.orElseThrow(() -> new IllegalStateException("Can't automatically determine npm executable and none was specifically supplied!\n\n" + NpmExecutableResolver.explainMessage())));
if (this.explicitNpmExecutable != null) {
return this.explicitNpmExecutable;
}
if (this.explicitNodeExecutable != null) {
File nodeExecutableCandidate = new File(this.explicitNodeExecutable.getParentFile(), NpmExecutableResolver.npmExecutableName());
if (nodeExecutableCandidate.canExecute()) {
return nodeExecutableCandidate;
}
}
return NpmExecutableResolver.tryFind()
.orElseThrow(() -> new IllegalStateException("Can't automatically determine npm executable and none was specifically supplied!\n\n" + NpmExecutableResolver.explainMessage()));
}

/**
* Finds the node executable to use.
* <br>
* Either the explicit node executable is returned, or tries to find the node executable relative to the npm executable
* found by {@link #resolveNpmExecutable()}.
* @return the node executable to use
* @throws IllegalStateException if no node executable could be found
*/
public File resolveNodeExecutable() {
if (this.explicitNodeExecutable != null) {
return this.explicitNodeExecutable;
}
File npmExecutable = resolveNpmExecutable();
return NodeExecutableResolver.tryFindNextTo(npmExecutable)
.orElseThrow(() -> new IllegalStateException("Can't automatically determine node executable and none was specifically supplied!\n\n" + NodeExecutableResolver.explainMessage()));
}

public String resolveNpmrcContent() {
Expand Down
16 changes: 12 additions & 4 deletions lib/src/main/java/com/diffplug/spotless/npm/NpmProcess.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,12 @@ class NpmProcess {

private final File npmExecutable;

NpmProcess(File workingDir, File npmExecutable) {
private final File nodeExecutable;

NpmProcess(File workingDir, File npmExecutable, File nodeExecutable) {
this.workingDir = workingDir;
this.npmExecutable = npmExecutable;
this.nodeExecutable = nodeExecutable;
}

void install() {
Expand Down Expand Up @@ -61,11 +64,12 @@ private void npmAwait(String... args) {
private Process npm(String... args) {
List<String> processCommand = processCommand(args);
try {
return new ProcessBuilder()
ProcessBuilder processBuilder = new ProcessBuilder()
.inheritIO()
.directory(this.workingDir)
.command(processCommand)
.start();
.command(processCommand);
addEnvironmentVariables(processBuilder);
return processBuilder.start();
} catch (IOException e) {
throw new NpmProcessException("Failed to launch npm command '" + commandLine(args) + "'.", e);
}
Expand All @@ -78,6 +82,10 @@ private List<String> processCommand(String... args) {
return command;
}

private void addEnvironmentVariables(ProcessBuilder processBuilder) {
processBuilder.environment().put("PATH", this.nodeExecutable.getParentFile().getAbsolutePath() + File.pathSeparator + System.getenv("PATH"));
}

private String commandLine(String... args) {
return "npm " + Arrays.stream(args).collect(Collectors.joining(" "));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,11 @@ private static class State extends NpmFormatterStepStateBase implements Serializ
"/com/diffplug/spotless/npm/common-serve.js",
"/com/diffplug/spotless/npm/prettier-serve.js"),
npmPathResolver.resolveNpmrcContent()),
projectDir,
buildDir,
npmPathResolver.resolveNpmExecutable());
new NpmFormatterStepLocations(
projectDir,
buildDir,
npmPathResolver.resolveNpmExecutable(),
npmPathResolver.resolveNodeExecutable()));
this.prettierConfig = requireNonNull(prettierConfig);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,11 @@ public State(String stepName, Map<String, String> versions, File projectDir, Fil
"/com/diffplug/spotless/npm/common-serve.js",
"/com/diffplug/spotless/npm/tsfmt-serve.js"),
npmPathResolver.resolveNpmrcContent()),
projectDir,
buildDir,
npmPathResolver.resolveNpmExecutable());
new NpmFormatterStepLocations(
projectDir,
buildDir,
npmPathResolver.resolveNpmExecutable(),
npmPathResolver.resolveNodeExecutable()));
this.buildDir = requireNonNull(buildDir);
this.configFile = configFile;
this.inlineTsFmtSettings = inlineTsFmtSettings == null ? new TreeMap<>() : new TreeMap<>(inlineTsFmtSettings);
Expand Down
1 change: 1 addition & 0 deletions plugin-gradle/CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (

## [Unreleased]
### Added
* Allow to specify node executable for node-based formatters using `nodeExecutable` parameter ([#1500](https://github.com/diffplug/spotless/pull/1500))
### Fixed
* The default list of type annotations used by `formatAnnotations` has had 8 more annotations from the Checker Framework added [#1494](https://github.com/diffplug/spotless/pull/1494)
### Changes
Expand Down
11 changes: 8 additions & 3 deletions plugin-gradle/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -844,18 +844,23 @@ spotless {
### npm detection

Prettier is based on NodeJS, so a working NodeJS installation (especially npm) is required on the host running spotless.
Spotless will try to auto-discover an npm installation. If that is not working for you, it is possible to directly configure the npm binary to use.
Spotless will try to auto-discover an npm installation. If that is not working for you, it is possible to directly configure the npm
and/or node binary to use.

```gradle
spotless {
format 'javascript', {
prettier().npmExecutable('/usr/bin/npm').config(...)
prettier().npmExecutable('/usr/bin/npm').nodeExecutable('/usr/bin/node').config(...)
```

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.

### `.npmrc` detection

Spotless picks up npm configuration stored in a `.npmrc` file either in the project directory or in your user home.
Alternatively you can supply spotless with a location of the `.npmrc` file to use. (This can be combined with `npmExecutable`, of course.)
Alternatively you can supply spotless with a location of the `.npmrc` file to use. (This can be combined with
`npmExecutable` and `nodeExecutable`, of course.)

```gradle
spotless {
Expand Down
Loading