1+ #!/usr/bin/env node
2+
3+ const { spawn } = require ( 'child_process' ) ;
4+ const path = require ( 'path' ) ;
5+ const minimist = require ( 'minimist' ) ;
6+ const fs = require ( 'fs' ) ;
7+
8+ /**
9+ * Convert task glob pattern to pnpm workspace filters using path-based approach
10+ * @param {string } taskPattern - Task pattern (e.g., "@(TaskA|TaskB|TaskC)" or "TaskName")
11+ * @returns {string[] } - Array of --filter arguments for pnpm
12+ */
13+ function convertTaskPatternToWorkspaceFilters ( taskPattern ) {
14+ if ( ! taskPattern ) {
15+ return [ ] ;
16+ }
17+
18+ // Handle @(task1|task2|task3) pattern - convert to single filter with path pattern
19+ if ( taskPattern . startsWith ( '@(' ) && taskPattern . endsWith ( ')' ) ) {
20+ const tasksString = taskPattern . slice ( 2 , - 1 ) ; // Remove @( and )
21+ const tasks = tasksString . split ( '|' ) . map ( task => task . trim ( ) ) ;
22+
23+ if ( tasks . length === 0 ) {
24+ return [ ] ;
25+ } else if ( tasks . length === 1 ) {
26+ return [ '--filter' , `"./Tasks/${ tasks [ 0 ] } "` ] ;
27+ } else {
28+ // Create a single filter pattern that matches multiple task paths
29+ const pathPattern = `./Tasks/{${ tasks . join ( ',' ) } }` ;
30+ return [ '--filter' , `"${ pathPattern } "` ] ;
31+ }
32+ }
33+
34+ // Handle single task or other patterns - use path-based filter
35+ return [ '--filter' , `"./Tasks/${ taskPattern } "` ] ;
36+ }
37+
38+ /**
39+ * Convert parsed arguments to workspace filter arguments for pnpm
40+ * @param {object } argv - Parsed minimist arguments
41+ * @returns {string[] } - Converted arguments for pnpm
42+ */
43+ function convertArgsToWorkspaceArgs ( argv ) {
44+ const convertedArgs = [ ] ;
45+
46+ // Handle task parameter
47+ if ( argv . task ) {
48+ const taskFilters = convertTaskPatternToWorkspaceFilters ( argv . task ) ;
49+ convertedArgs . push ( ...taskFilters ) ;
50+ }
51+
52+ // Handle other arguments (excluding task and the command)
53+ Object . keys ( argv ) . forEach ( key => {
54+ if ( key !== 'task' && key !== '_' ) {
55+ const value = argv [ key ] ;
56+ if ( typeof value === 'boolean' && value ) {
57+ convertedArgs . push ( `--${ key } ` ) ;
58+ } else if ( typeof value !== 'boolean' ) {
59+ convertedArgs . push ( `--${ key } =${ value } ` ) ;
60+ }
61+ }
62+ } ) ;
63+
64+ return convertedArgs ;
65+ }
66+
67+ /**
68+ * Executes a command with arguments and returns a promise
69+ * @param {string } command - The command to execute
70+ * @param {string[] } args - Array of arguments
71+ * @param {object } options - Spawn options
72+ * @returns {Promise<number> } - Exit code
73+ */
74+ function executeCommand ( command , args , options = { } ) {
75+ return new Promise ( ( resolve , reject ) => {
76+ // For shell commands, we need to properly quote arguments that contain special characters
77+ const quotedArgs = args . map ( arg => {
78+ // Quote arguments that contain pipes, parentheses, or spaces
79+ if ( arg . includes ( '|' ) || arg . includes ( '(' ) || arg . includes ( ')' ) || arg . includes ( ' ' ) ) {
80+ return `"${ arg } "` ;
81+ }
82+ return arg ;
83+ } ) ;
84+
85+ const child = spawn ( command , quotedArgs , {
86+ stdio : 'inherit' ,
87+ shell : true ,
88+ ...options
89+ } ) ;
90+
91+ child . on ( 'close' , ( code ) => {
92+ resolve ( code ) ;
93+ } ) ;
94+
95+ child . on ( 'error' , ( error ) => {
96+ reject ( error ) ;
97+ } ) ;
98+ } ) ;
99+ }
100+
101+ /**
102+ * Run parallel build command
103+ * @param {string[] } additionalArgs - Additional arguments to pass to the commands
104+ */
105+ async function runBuild ( additionalArgs = [ ] ) {
106+ // Parse arguments using minimist (same as make.js)
107+ const argv = minimist ( additionalArgs ) ;
108+
109+ failIfTasksMissing ( argv ) ;
110+
111+ // First run pre-build steps
112+ console . log ( 'Running pre-build steps...' ) ;
113+ try {
114+ const preBuildArgs = [ 'make.js' , 'build' , '--onlyPreBuildSteps' , '--enableConcurrentTaskBuild' , ...additionalArgs ] ;
115+ const preBuildExitCode = await executeCommand ( 'node' , preBuildArgs ) ;
116+ if ( preBuildExitCode !== 0 ) {
117+ console . error ( 'Pre-build steps failed' ) ;
118+ process . exit ( preBuildExitCode ) ;
119+ }
120+ } catch ( error ) {
121+ console . error ( 'Error running pre-build steps:' , error . message ) ;
122+ process . exit ( 1 ) ;
123+ }
124+
125+ // Then run parallel build
126+ const pnpmPath = path . join ( __dirname , 'node_modules' , '.bin' , 'pnpm' ) ;
127+ const workspaceArgs = convertArgsToWorkspaceArgs ( argv ) ;
128+ const args = [
129+ '-r' ,
130+ '--report-summary' ,
131+ '--aggregate-output' ,
132+ '--reporter=append-only' ,
133+ '--workspace-concurrency=2' ,
134+ ...workspaceArgs , // Move workspace filters before 'run'
135+ 'run' ,
136+ 'build' ,
137+ '--skipPrebuildSteps' ,
138+ '--enableConcurrentTaskBuild'
139+ ] ;
140+
141+ console . log ( 'Running parallel build...' ) ;
142+ console . log ( 'pnpm command:' , 'pnpm' , args . join ( ' ' ) ) ;
143+ try {
144+ const exitCode = await executeCommand ( pnpmPath , args ) ;
145+ printBuildSummary ( ) ;
146+ process . exit ( exitCode ) ;
147+ } catch ( error ) {
148+ console . error ( 'Error running build:' , error . message ) ;
149+ process . exit ( 1 ) ;
150+ }
151+ }
152+
153+ /**
154+ * Run parallel server build command
155+ * @param {string[] } additionalArgs - Additional arguments to pass to the commands
156+ */
157+ async function runServerBuild ( additionalArgs = [ ] ) {
158+ // Parse arguments using minimist (same as make.js)
159+ const argv = minimist ( additionalArgs ) ;
160+ failIfTasksMissing ( argv ) ;
161+
162+ // First run pre-build steps
163+ console . log ( 'Running pre-build steps for server build...' ) ;
164+ try {
165+ const preBuildArgs = [ 'make.js' , 'build' , '--onlyPreBuildSteps' , '--enableConcurrentTaskBuild' , ...additionalArgs ] ;
166+ const preBuildExitCode = await executeCommand ( 'node' , preBuildArgs ) ;
167+ if ( preBuildExitCode !== 0 ) {
168+ console . error ( 'Pre-build steps failed for server build' ) ;
169+ process . exit ( preBuildExitCode ) ;
170+ }
171+ } catch ( error ) {
172+ console . error ( 'Error running pre-build steps for server build:' , error . message ) ;
173+ process . exit ( 1 ) ;
174+ }
175+
176+ // Then run parallel server build
177+ const pnpmPath = path . join ( __dirname , 'node_modules' , '.bin' , 'pnpm' ) ;
178+ const workspaceArgs = convertArgsToWorkspaceArgs ( argv ) ;
179+ const args = [
180+ '-r' ,
181+ '--report-summary' ,
182+ '--aggregate-output' ,
183+ '--reporter=append-only' ,
184+ '--workspace-concurrency=2' ,
185+ ...workspaceArgs , // Move workspace filters before 'run'
186+ 'run' ,
187+ 'serverBuild' ,
188+ '--skipPrebuildSteps' ,
189+ '--enableConcurrentTaskBuild'
190+ ] ;
191+
192+ console . log ( 'Running parallel server build...' ) ;
193+ console . log ( 'pnpm command:' , 'pnpm' , args . join ( ' ' ) ) ;
194+ try {
195+ const exitCode = await executeCommand ( pnpmPath , args ) ;
196+ printBuildSummary ( ) ;
197+ process . exit ( exitCode ) ;
198+ } catch ( error ) {
199+ console . error ( 'Error running server build:' , error . message ) ;
200+ process . exit ( 1 ) ;
201+ }
202+ }
203+
204+ // Parse command line arguments
205+ const command = process . argv [ 2 ] ;
206+ const additionalArgs = process . argv . slice ( 3 ) ; // Capture all arguments after the command
207+
208+ switch ( command ) {
209+ case 'build' :
210+ runBuild ( additionalArgs ) ;
211+ break ;
212+ case 'serverBuild' :
213+ runServerBuild ( additionalArgs ) ;
214+ break ;
215+ default :
216+ console . log ( 'Usage: node build-parallel.js [build|serverBuild] [additional arguments...]' ) ;
217+ console . log ( 'Commands:' ) ;
218+ console . log ( ' build - Run parallel build' ) ;
219+ console . log ( ' serverBuild - Run parallel server build' ) ;
220+ console . log ( '' ) ;
221+ console . log ( 'Examples:' ) ;
222+ console . log ( ' node build-parallel.js build --task "MyTask"' ) ;
223+ console . log ( ' node build-parallel.js serverBuild --task "@(TaskA|TaskB|TaskC)"' ) ;
224+ console . log ( '' ) ;
225+ console . log ( 'Note: Task patterns are converted to path-based --filter arguments for pnpm workspace filtering' ) ;
226+ console . log ( ' Multiple tasks use a single filter pattern:' ) ;
227+ console . log ( ' - Glob pattern: --task "@(Task1|Task2|Task3)" → --filter "./Tasks/{Task1,Task2,Task3}"' ) ;
228+ console . log ( ' - Single task: --task "Task1" → --filter "./Tasks/Task1"' ) ;
229+ process . exit ( 1 ) ;
230+ }
231+
232+ // Print build summary
233+ const printBuildSummary = ( ) => {
234+ const summaryPath = path . join ( __dirname , 'pnpm-exec-summary.json' ) ;
235+ if ( ! fs . existsSync ( summaryPath ) ) {
236+ console . log ( 'No build summary found.' ) ;
237+ return ;
238+ }
239+ const summary = JSON . parse ( fs . readFileSync ( summaryPath , 'utf8' ) ) ;
240+ const execStatus = summary . executionStatus || { } ;
241+
242+ let total = 0 , success = 0 , failed = 0 , skipped = 0 , running = 0 ;
243+ for ( const task in execStatus ) {
244+ total ++ ;
245+ const status = execStatus [ task ] . status ;
246+ if ( status === 'passed' ) success ++ ;
247+ else if ( status === 'failure' ) failed ++ ;
248+ else if ( status === 'running' ) running ++ ;
249+ else skipped ++ ;
250+ }
251+
252+ console . log ( '' ) ;
253+ console . log ( '📊 BUILD SUMMARY' ) ;
254+ console . log ( '================================================================================' ) ;
255+ console . log ( ` Total tasks built: ${ total } ` ) ;
256+ console . log ( ` ✅ Successful: ${ success } ` ) ;
257+ console . log ( ` ❌ Failed: ${ failed } ` ) ;
258+ console . log ( ` ⏭️ Skipped: ${ skipped } ` ) ;
259+ console . log ( ` 🟡 Running: ${ running } ` ) ;
260+ console . log ( '===================================' ) ;
261+ } ;
262+
263+ /**
264+ * Checks if all requested tasks exist in ./Tasks directory. Exits with error if any are missing.
265+ */
266+ function failIfTasksMissing ( argv ) {
267+ const argvTask = argv . task ;
268+ let requestedTasks = [ ] ;
269+ if ( argvTask ) {
270+ if ( argvTask . startsWith ( '@(' ) && argvTask . endsWith ( ')' ) ) {
271+ requestedTasks = argvTask . slice ( 2 , - 1 ) . split ( '|' ) . map ( t => t . trim ( ) ) ;
272+ } else {
273+ requestedTasks = [ argvTask ] ;
274+ }
275+ }
276+ if ( requestedTasks . length ) {
277+ // Check existence in ./Tasks directory
278+ const tasksDir = path . join ( __dirname , 'Tasks' ) ;
279+ let missingTasks = [ ] ;
280+ for ( const t of requestedTasks ) {
281+ const taskPath = path . join ( tasksDir , t ) ;
282+ if ( ! fs . existsSync ( taskPath ) ) {
283+ missingTasks . push ( t ) ;
284+ }
285+ }
286+ if ( missingTasks . length ) {
287+ console . error ( `Error: The following tasks do not exist: ${ missingTasks . join ( ', ' ) } ` ) ;
288+ process . exit ( 2 ) ;
289+ }
290+ }
291+ }
0 commit comments