From 57063e2a6fab7391282cd122f0a6008acef6db83 Mon Sep 17 00:00:00 2001 From: ntwigg Date: Mon, 28 Jul 2025 15:11:14 -0700 Subject: [PATCH 1/4] First cut. --- .../diffplug/webtools/node/NodePlugin.java | 124 +++++++++++++++++- 1 file changed, 120 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/diffplug/webtools/node/NodePlugin.java b/src/main/java/com/diffplug/webtools/node/NodePlugin.java index 43166ea..88923b9 100644 --- a/src/main/java/com/diffplug/webtools/node/NodePlugin.java +++ b/src/main/java/com/diffplug/webtools/node/NodePlugin.java @@ -16,13 +16,18 @@ package com.diffplug.webtools.node; import com.github.eirslett.maven.plugins.frontend.lib.ProxyConfig; +import java.io.BufferedReader; import java.io.File; import java.io.IOException; +import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.util.ArrayList; import java.util.Collections; +import java.util.List; import java.util.Objects; import java.util.TreeMap; +import java.util.concurrent.CompletableFuture; import org.gradle.api.Action; import org.gradle.api.DefaultTask; import org.gradle.api.Plugin; @@ -97,11 +102,122 @@ public TreeMap getEnvironment() { @TaskAction public void npmCiRunTask() throws Exception { SetupCleanupNode setup = getSetup().get(); + File projectDir = getProjectDir().get().getAsFile(); + + System.out.println("=== NPM Task: " + npmTaskName + " ==="); + System.out.println("Installing Node.js dependencies..."); + // install node, npm, and package-lock.json - setup.start(getProjectDir().get().getAsFile()); - // run the gulp task - ProxyConfig proxyConfig = new ProxyConfig(Collections.emptyList()); - setup.factory().getNpmRunner(proxyConfig, null).execute("run " + npmTaskName, environment); + setup.start(projectDir); + + System.out.println("Running: npm run " + npmTaskName); + if (!environment.isEmpty()) { + System.out.println("Environment variables:"); + environment.forEach((key, value) -> System.out.println(" " + key + "=" + value)); + } + System.out.println(); + + try { + // Use ProcessBuilder for direct console output instead of NpmRunner + File installDir = new File(projectDir, "build/node-install"); + File npmExe; + + // Based on frontend-maven-plugin structure, npm is directly in the node directory + File nodeExe = new File(installDir, "node/node"); + if (System.getProperty("os.name").toLowerCase().contains("win")) { + nodeExe = new File(installDir, "node/node.exe"); + npmExe = new File(installDir, "node/npm.cmd"); + } else { + npmExe = new File(installDir, "node/npm"); + } + + System.out.println("Using node executable: " + nodeExe.getAbsolutePath()); + System.out.println("Node executable exists: " + nodeExe.exists()); + System.out.println("Using npm executable: " + npmExe.getAbsolutePath()); + System.out.println("NPM executable exists: " + npmExe.exists()); + + ProcessBuilder processBuilder = new ProcessBuilder(npmExe.getAbsolutePath(), "run", npmTaskName); + + processBuilder.directory(projectDir); + processBuilder.environment().putAll(environment); + + Process process = processBuilder.start(); + + // Buffer output to only show on failure + List stdoutLines = new ArrayList<>(); + List stderrLines = new ArrayList<>(); + + // Create threads to read stdout and stderr concurrently + CompletableFuture stdoutFuture = CompletableFuture.runAsync(() -> { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { + String line; + while ((line = reader.readLine()) != null) { + synchronized (stdoutLines) { + stdoutLines.add(line); + } + } + } catch (IOException e) { + synchronized (stdoutLines) { + stdoutLines.add("Error reading stdout: " + e.getMessage()); + } + } + }); + + CompletableFuture stderrFuture = CompletableFuture.runAsync(() -> { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) { + String line; + while ((line = reader.readLine()) != null) { + synchronized (stderrLines) { + stderrLines.add(line); + } + } + } catch (IOException e) { + synchronized (stderrLines) { + stderrLines.add("Error reading stderr: " + e.getMessage()); + } + } + }); + + int exitCode = process.waitFor(); + + // Wait for output streams to finish + CompletableFuture.allOf(stdoutFuture, stderrFuture).join(); + + System.out.println(); + if (exitCode == 0) { + System.out.println("✓ NPM task '" + npmTaskName + "' completed successfully"); + } else { + System.out.println("✗ NPM task '" + npmTaskName + "' failed with exit code " + exitCode); + System.out.println(); + System.out.println("=== NPM OUTPUT ==="); + + // Print stdout if there's any + if (!stdoutLines.isEmpty()) { + System.out.println("STDOUT:"); + for (String line : stdoutLines) { + System.out.println(line); + } + System.out.println(); + } + + // Print stderr if there's any + if (!stderrLines.isEmpty()) { + System.out.println("STDERR:"); + for (String line : stderrLines) { + System.out.println(line); + } + } + + System.out.println("=================="); + throw new RuntimeException("npm run " + npmTaskName + " failed with exit code " + exitCode); + } + } catch (Exception e) { + System.out.println(); + System.out.println("✗ NPM task '" + npmTaskName + "' failed"); + System.out.println("Command: npm run " + npmTaskName); + System.out.println("Error: " + e.getMessage()); + throw e; + } } } From 087d4b87b385752f6742855e95782b6c4c5f251d Mon Sep 17 00:00:00 2001 From: ntwigg Date: Mon, 28 Jul 2025 15:27:54 -0700 Subject: [PATCH 2/4] Cleaned up. --- .../diffplug/webtools/node/NodePlugin.java | 171 ++++++------------ 1 file changed, 54 insertions(+), 117 deletions(-) diff --git a/src/main/java/com/diffplug/webtools/node/NodePlugin.java b/src/main/java/com/diffplug/webtools/node/NodePlugin.java index 88923b9..b600772 100644 --- a/src/main/java/com/diffplug/webtools/node/NodePlugin.java +++ b/src/main/java/com/diffplug/webtools/node/NodePlugin.java @@ -15,23 +15,19 @@ */ package com.diffplug.webtools.node; -import com.github.eirslett.maven.plugins.frontend.lib.ProxyConfig; import java.io.BufferedReader; import java.io.File; import java.io.IOException; +import java.io.InputStream; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.TreeMap; import java.util.concurrent.CompletableFuture; -import org.gradle.api.Action; -import org.gradle.api.DefaultTask; -import org.gradle.api.Plugin; -import org.gradle.api.Project; +import org.gradle.api.*; import org.gradle.api.file.DirectoryProperty; import org.gradle.api.provider.Property; import org.gradle.api.tasks.CacheableTask; @@ -99,125 +95,66 @@ public TreeMap getEnvironment() { @Internal public abstract DirectoryProperty getProjectDir(); + private static CompletableFuture readStream(InputStream inputStream, List outputLines, String streamName) { + return CompletableFuture.runAsync(() -> { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) { + String line; + while ((line = reader.readLine()) != null) { + synchronized (outputLines) { + outputLines.add(line); + } + } + } catch (IOException e) { + synchronized (outputLines) { + outputLines.add("Error reading " + streamName + ": " + e.getMessage()); + } + } + }); + } + @TaskAction public void npmCiRunTask() throws Exception { SetupCleanupNode setup = getSetup().get(); File projectDir = getProjectDir().get().getAsFile(); - - System.out.println("=== NPM Task: " + npmTaskName + " ==="); - System.out.println("Installing Node.js dependencies..."); - // install node, npm, and package-lock.json setup.start(projectDir); - - System.out.println("Running: npm run " + npmTaskName); - if (!environment.isEmpty()) { - System.out.println("Environment variables:"); - environment.forEach((key, value) -> System.out.println(" " + key + "=" + value)); + + // Use ProcessBuilder for direct console output instead of NpmRunner + File installDir = new File(projectDir, "build/node-install"); + File npmExe; + if (System.getProperty("os.name").toLowerCase().contains("win")) { + npmExe = new File(installDir, "node/npm.cmd"); + } else { + npmExe = new File(installDir, "node/npm"); } - System.out.println(); - - try { - // Use ProcessBuilder for direct console output instead of NpmRunner - File installDir = new File(projectDir, "build/node-install"); - File npmExe; - - // Based on frontend-maven-plugin structure, npm is directly in the node directory - File nodeExe = new File(installDir, "node/node"); - if (System.getProperty("os.name").toLowerCase().contains("win")) { - nodeExe = new File(installDir, "node/node.exe"); - npmExe = new File(installDir, "node/npm.cmd"); - } else { - npmExe = new File(installDir, "node/npm"); - } - - System.out.println("Using node executable: " + nodeExe.getAbsolutePath()); - System.out.println("Node executable exists: " + nodeExe.exists()); - System.out.println("Using npm executable: " + npmExe.getAbsolutePath()); - System.out.println("NPM executable exists: " + npmExe.exists()); - - ProcessBuilder processBuilder = new ProcessBuilder(npmExe.getAbsolutePath(), "run", npmTaskName); - - processBuilder.directory(projectDir); - processBuilder.environment().putAll(environment); - - Process process = processBuilder.start(); - - // Buffer output to only show on failure - List stdoutLines = new ArrayList<>(); - List stderrLines = new ArrayList<>(); - - // Create threads to read stdout and stderr concurrently - CompletableFuture stdoutFuture = CompletableFuture.runAsync(() -> { - try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { - String line; - while ((line = reader.readLine()) != null) { - synchronized (stdoutLines) { - stdoutLines.add(line); - } - } - } catch (IOException e) { - synchronized (stdoutLines) { - stdoutLines.add("Error reading stdout: " + e.getMessage()); - } - } - }); - - CompletableFuture stderrFuture = CompletableFuture.runAsync(() -> { - try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) { - String line; - while ((line = reader.readLine()) != null) { - synchronized (stderrLines) { - stderrLines.add(line); - } - } - } catch (IOException e) { - synchronized (stderrLines) { - stderrLines.add("Error reading stderr: " + e.getMessage()); - } - } - }); - - int exitCode = process.waitFor(); - - // Wait for output streams to finish - CompletableFuture.allOf(stdoutFuture, stderrFuture).join(); - - System.out.println(); - if (exitCode == 0) { - System.out.println("✓ NPM task '" + npmTaskName + "' completed successfully"); - } else { - System.out.println("✗ NPM task '" + npmTaskName + "' failed with exit code " + exitCode); - System.out.println(); - System.out.println("=== NPM OUTPUT ==="); - - // Print stdout if there's any - if (!stdoutLines.isEmpty()) { - System.out.println("STDOUT:"); - for (String line : stdoutLines) { - System.out.println(line); - } - System.out.println(); - } - - // Print stderr if there's any - if (!stderrLines.isEmpty()) { - System.out.println("STDERR:"); - for (String line : stderrLines) { - System.out.println(line); - } - } - - System.out.println("=================="); - throw new RuntimeException("npm run " + npmTaskName + " failed with exit code " + exitCode); - } - } catch (Exception e) { - System.out.println(); - System.out.println("✗ NPM task '" + npmTaskName + "' failed"); - System.out.println("Command: npm run " + npmTaskName); - System.out.println("Error: " + e.getMessage()); - throw e; + + ProcessBuilder processBuilder = new ProcessBuilder(npmExe.getAbsolutePath(), "run", npmTaskName); + processBuilder.directory(projectDir); + processBuilder.environment().putAll(environment); + Process process = processBuilder.start(); + + // Buffer output to only show on failure + List stdoutLines = new ArrayList<>(); + List stderrLines = new ArrayList<>(); + + // Create threads to read stdout and stderr concurrently + CompletableFuture stdoutFuture = readStream(process.getInputStream(), stdoutLines, "stdout"); + CompletableFuture stderrFuture = readStream(process.getErrorStream(), stderrLines, "stderr"); + int exitCode = process.waitFor(); + CompletableFuture.allOf(stdoutFuture, stderrFuture).join(); + if (exitCode == 0) { + return; + } + + var cmd = new StringBuilder().append("> npm run ").append(npmTaskName).append(" FAILED\n"); + environment.forEach((key, value) -> cmd.append(" env ").append(key).append("=").append(value).append("\n")); + for (String line : stdoutLines) { + cmd.append(line).append("\n"); + } + for (String line : stderrLines) { + cmd.append(line).append("\n"); } + throw new GradleException(cmd.toString()); } } From f0aab6cf83ef4f1069d2047eda6406fcc3dd9cfc Mon Sep 17 00:00:00 2001 From: ntwigg Date: Mon, 28 Jul 2025 15:31:08 -0700 Subject: [PATCH 3/4] spotlessAPply --- src/main/java/com/diffplug/webtools/node/SetupCleanup.java | 2 +- src/main/java/com/diffplug/webtools/node/SetupCleanupNode.java | 2 +- src/main/java/com/diffplug/webtools/serve/StaticServerTask.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/diffplug/webtools/node/SetupCleanup.java b/src/main/java/com/diffplug/webtools/node/SetupCleanup.java index dd13a9b..c31ac10 100644 --- a/src/main/java/com/diffplug/webtools/node/SetupCleanup.java +++ b/src/main/java/com/diffplug/webtools/node/SetupCleanup.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024 DiffPlug + * Copyright (C) 2024-2025 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/com/diffplug/webtools/node/SetupCleanupNode.java b/src/main/java/com/diffplug/webtools/node/SetupCleanupNode.java index 4bccc36..500cb4a 100644 --- a/src/main/java/com/diffplug/webtools/node/SetupCleanupNode.java +++ b/src/main/java/com/diffplug/webtools/node/SetupCleanupNode.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024 DiffPlug + * Copyright (C) 2024-2025 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/com/diffplug/webtools/serve/StaticServerTask.java b/src/main/java/com/diffplug/webtools/serve/StaticServerTask.java index 5cca50a..6eb74d3 100644 --- a/src/main/java/com/diffplug/webtools/serve/StaticServerTask.java +++ b/src/main/java/com/diffplug/webtools/serve/StaticServerTask.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024 DiffPlug + * Copyright (C) 2024-2025 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. From 666c403024eb1b5e2f19f60daa21103913f765f0 Mon Sep 17 00:00:00 2001 From: ntwigg Date: Mon, 28 Jul 2025 15:33:05 -0700 Subject: [PATCH 4/4] Update the changelog. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ead6b56..2064c48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## [Unreleased] ### Added - Task like `npm run lint:fix` get turned into `npm_run_lint-fix` (so the colons don't screw up Gradle) +- When `npm run` commands fail, they dump their console output as a Gradle error. ## [1.1.0] - 2024-08-04 ### Added