2020import com .github .eirslett .maven .plugins .frontend .lib .InstallationException ;
2121import com .github .eirslett .maven .plugins .frontend .lib .ProxyConfig ;
2222import com .github .eirslett .maven .plugins .frontend .lib .TaskRunnerException ;
23+ import java .io .BufferedReader ;
2324import java .io .File ;
2425import java .io .IOException ;
26+ import java .io .InputStream ;
27+ import java .io .InputStreamReader ;
2528import java .io .Serializable ;
2629import java .nio .file .Files ;
2730import java .nio .file .StandardCopyOption ;
31+ import java .util .ArrayList ;
2832import java .util .Collections ;
33+ import java .util .List ;
34+ import java .util .Map ;
35+ import java .util .concurrent .CompletableFuture ;
36+ import org .gradle .api .GradleException ;
2937
3038class SetupCleanupNode implements Serializable {
3139 public String nodeVersion ;
@@ -49,9 +57,92 @@ private static File keyFile(File projectDir) {
4957 return new File (projectDir , "build/node_modules/.gradle-state" );
5058 }
5159
60+ public void executeNpmCommand (String command , String ... args ) throws Exception {
61+ List <String > commandArgs = new ArrayList <>();
62+ commandArgs .add (command );
63+ for (String arg : args ) {
64+ commandArgs .add (arg );
65+ }
66+ executeNpmCommand (commandArgs , Collections .emptyMap ());
67+ }
68+
69+ public void executeNpmCommand (List <String > commandArgs , Map <String , String > environment ) throws Exception {
70+ // Use ProcessBuilder for direct console output instead of NpmRunner
71+ File npmExe ;
72+ if (System .getProperty ("os.name" ).toLowerCase ().contains ("win" )) {
73+ npmExe = new File (installDir , "node/npm.cmd" );
74+ } else {
75+ npmExe = new File (installDir , "node/npm" );
76+ }
77+
78+ List <String > fullCommand = new ArrayList <>();
79+ fullCommand .add (npmExe .getAbsolutePath ());
80+ fullCommand .addAll (commandArgs );
81+
82+ ProcessBuilder processBuilder = new ProcessBuilder (fullCommand );
83+ processBuilder .directory (workingDir );
84+
85+ addNodeToPath (processBuilder , installDir );
86+ processBuilder .environment ().putAll (environment );
87+ Process process = processBuilder .start ();
88+
89+ // Buffer output to only show on failure
90+ List <String > stdoutLines = new ArrayList <>();
91+ List <String > stderrLines = new ArrayList <>();
92+
93+ // Create threads to read stdout and stderr concurrently
94+ CompletableFuture <Void > stdoutFuture = readStream (process .getInputStream (), stdoutLines , "stdout" );
95+ CompletableFuture <Void > stderrFuture = readStream (process .getErrorStream (), stderrLines , "stderr" );
96+ int exitCode = process .waitFor ();
97+ CompletableFuture .allOf (stdoutFuture , stderrFuture ).join ();
98+ if (exitCode == 0 ) {
99+ return ;
100+ }
101+
102+ var cmd = new StringBuilder ().append ("> npm " ).append (String .join (" " , commandArgs )).append (" FAILED\n " );
103+ environment .forEach ((key , value ) -> cmd .append (" env " ).append (key ).append ("=" ).append (value ).append ("\n " ));
104+ for (String line : stdoutLines ) {
105+ cmd .append (line ).append ("\n " );
106+ }
107+ for (String line : stderrLines ) {
108+ cmd .append (line ).append ("\n " );
109+ }
110+ throw new GradleException (cmd .toString ());
111+ }
112+
113+ private static CompletableFuture <Void > readStream (InputStream inputStream , List <String > outputLines , String streamName ) {
114+ return CompletableFuture .runAsync (() -> {
115+ try (BufferedReader reader = new BufferedReader (new InputStreamReader (inputStream ))) {
116+ String line ;
117+ while ((line = reader .readLine ()) != null ) {
118+ synchronized (outputLines ) {
119+ outputLines .add (line );
120+ }
121+ }
122+ } catch (IOException e ) {
123+ synchronized (outputLines ) {
124+ outputLines .add ("Error reading " + streamName + ": " + e .getMessage ());
125+ }
126+ }
127+ });
128+ }
129+
130+ private static void addNodeToPath (ProcessBuilder processBuilder , File installDir ) {
131+ // Add node binary directory to PATH for npm to find node executable
132+ File nodeDir = new File (installDir , "node" );
133+ String currentPath = processBuilder .environment ().get ("PATH" );
134+ if (currentPath == null ) {
135+ currentPath = processBuilder .environment ().get ("Path" ); // Windows
136+ }
137+ String nodeBinPath = nodeDir .getAbsolutePath ();
138+ String pathSeparator = System .getProperty ("path.separator" );
139+ String newPath = nodeBinPath + pathSeparator + (currentPath != null ? currentPath : "" );
140+ processBuilder .environment ().put ("PATH" , newPath );
141+ }
142+
52143 private static class Impl extends SetupCleanup <SetupCleanupNode > {
53144 @ Override
54- protected void doStart (SetupCleanupNode key ) throws TaskRunnerException , InstallationException {
145+ protected void doStart (SetupCleanupNode key ) throws TaskRunnerException , InstallationException , Exception {
55146 ProxyConfig proxyConfig = new ProxyConfig (Collections .emptyList ());
56147 FrontendPluginFactory factory = key .factory ();
57148 factory .getNodeInstaller (proxyConfig )
@@ -68,8 +159,7 @@ protected void doStart(SetupCleanupNode key) throws TaskRunnerException, Install
68159 throw new RuntimeException (e );
69160 }
70161 }
71- factory .getNpmRunner (proxyConfig , null )
72- .execute ("ci" , null );
162+ key .executeNpmCommand ("ci" );
73163 }
74164
75165 @ Override
0 commit comments