workspace-utils 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/.github/workflows/mdbook.yml +64 -0
  2. package/.prettierignore +22 -0
  3. package/.prettierrc +13 -0
  4. package/LICENSE +21 -0
  5. package/README.md +278 -0
  6. package/docs/book.toml +10 -0
  7. package/docs/src/SUMMARY.md +24 -0
  8. package/docs/src/commands/build.md +110 -0
  9. package/docs/src/commands/dev.md +118 -0
  10. package/docs/src/commands/overview.md +239 -0
  11. package/docs/src/commands/run.md +153 -0
  12. package/docs/src/configuration.md +249 -0
  13. package/docs/src/examples.md +567 -0
  14. package/docs/src/installation.md +148 -0
  15. package/docs/src/introduction.md +117 -0
  16. package/docs/src/quick-start.md +278 -0
  17. package/docs/src/troubleshooting.md +533 -0
  18. package/index.ts +84 -0
  19. package/package.json +54 -0
  20. package/src/commands/build.ts +158 -0
  21. package/src/commands/dev.ts +192 -0
  22. package/src/commands/run.test.ts +329 -0
  23. package/src/commands/run.ts +118 -0
  24. package/src/core/dependency-graph.ts +262 -0
  25. package/src/core/process-runner.ts +355 -0
  26. package/src/core/workspace.test.ts +404 -0
  27. package/src/core/workspace.ts +228 -0
  28. package/src/package-managers/bun.test.ts +209 -0
  29. package/src/package-managers/bun.ts +79 -0
  30. package/src/package-managers/detector.test.ts +199 -0
  31. package/src/package-managers/detector.ts +111 -0
  32. package/src/package-managers/index.ts +10 -0
  33. package/src/package-managers/npm.ts +79 -0
  34. package/src/package-managers/pnpm.ts +101 -0
  35. package/src/package-managers/types.ts +42 -0
  36. package/src/utils/output.ts +301 -0
  37. package/src/utils/package-utils.ts +243 -0
  38. package/tests/bun-workspace/apps/web-app/package.json +18 -0
  39. package/tests/bun-workspace/bun.lockb +0 -0
  40. package/tests/bun-workspace/package.json +18 -0
  41. package/tests/bun-workspace/packages/shared-utils/package.json +15 -0
  42. package/tests/bun-workspace/packages/ui-components/package.json +17 -0
  43. package/tests/npm-workspace/package-lock.json +0 -0
  44. package/tests/npm-workspace/package.json +18 -0
  45. package/tests/npm-workspace/packages/core/package.json +15 -0
  46. package/tests/pnpm-workspace/package.json +14 -0
  47. package/tests/pnpm-workspace/packages/utils/package.json +15 -0
  48. package/tests/pnpm-workspace/pnpm-workspace.yaml +3 -0
  49. package/tsconfig.json +29 -0
@@ -0,0 +1,355 @@
1
+ import { spawn } from 'child_process';
2
+ import pc from 'picocolors';
3
+
4
+ export interface ProcessOptions {
5
+ cwd: string;
6
+ env?: Record<string, string>;
7
+ stdio?: 'inherit' | 'pipe';
8
+ }
9
+
10
+ export interface LogOptions {
11
+ prefix: string;
12
+ color: string;
13
+ showTimestamp?: boolean;
14
+ }
15
+
16
+ export interface ProcessResult {
17
+ success: boolean;
18
+ exitCode: number;
19
+ packageName: string;
20
+ command: string;
21
+ duration: number;
22
+ }
23
+
24
+ export class ProcessRunner {
25
+ private static colorPalette: string[] = [
26
+ 'red',
27
+ 'green',
28
+ 'yellow',
29
+ 'blue',
30
+ 'magenta',
31
+ 'cyan',
32
+ 'gray',
33
+ 'redBright',
34
+ 'greenBright',
35
+ 'yellowBright',
36
+ 'blueBright',
37
+ 'magentaBright',
38
+ 'cyanBright',
39
+ ];
40
+
41
+ private static assignedColors = new Map<string, string>();
42
+ private static colorIndex = 0;
43
+
44
+ /**
45
+ * Get a consistent color for a package
46
+ */
47
+ static getPackageColor(packageName: string): string {
48
+ if (!this.assignedColors.has(packageName)) {
49
+ const colorIndex = this.colorIndex % this.colorPalette.length;
50
+ const color = this.colorPalette[colorIndex] || 'white';
51
+ this.assignedColors.set(packageName, color);
52
+ this.colorIndex++;
53
+ }
54
+ const color = this.assignedColors.get(packageName);
55
+ return color || 'white';
56
+ }
57
+
58
+ /**
59
+ * Get color function from picocolors
60
+ */
61
+ private static getColorFn(color: string) {
62
+ switch (color) {
63
+ case 'red':
64
+ return pc.red;
65
+ case 'green':
66
+ return pc.green;
67
+ case 'yellow':
68
+ return pc.yellow;
69
+ case 'blue':
70
+ return pc.blue;
71
+ case 'magenta':
72
+ return pc.magenta;
73
+ case 'cyan':
74
+ return pc.cyan;
75
+ case 'gray':
76
+ return pc.gray;
77
+ case 'redBright':
78
+ return pc.redBright;
79
+ case 'greenBright':
80
+ return pc.greenBright;
81
+ case 'yellowBright':
82
+ return pc.yellowBright;
83
+ case 'blueBright':
84
+ return pc.blueBright;
85
+ case 'magentaBright':
86
+ return pc.magentaBright;
87
+ case 'cyanBright':
88
+ return pc.cyanBright;
89
+ default:
90
+ return pc.white;
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Run a single command with real-time log streaming
96
+ */
97
+ static async runCommand(
98
+ command: string,
99
+ args: string[],
100
+ options: ProcessOptions,
101
+ logOptions: LogOptions
102
+ ): Promise<ProcessResult> {
103
+ const startTime = Date.now();
104
+ const fullCommand = [command, ...args].join(' ');
105
+
106
+ const colorFn = this.getColorFn(logOptions.color);
107
+ console.log(`[${colorFn(logOptions.prefix)}] ` + pc.gray(`Starting: ${fullCommand}`));
108
+
109
+ return new Promise(resolve => {
110
+ const childProcess = spawn(command, args, {
111
+ cwd: options.cwd,
112
+ env: { ...process.env, ...options.env },
113
+ stdio: ['inherit', 'pipe', 'pipe'],
114
+ });
115
+
116
+ // Stream stdout with prefix and color
117
+ if (childProcess.stdout) {
118
+ childProcess.stdout.on('data', data => {
119
+ const lines = data.toString().split('\n');
120
+ lines.forEach((line: string) => {
121
+ if (line.trim()) {
122
+ this.logLine(line, logOptions, false);
123
+ }
124
+ });
125
+ });
126
+ }
127
+
128
+ // Stream stderr with prefix and color
129
+ if (childProcess.stderr) {
130
+ childProcess.stderr.on('data', data => {
131
+ const lines = data.toString().split('\n');
132
+ lines.forEach((line: string) => {
133
+ if (line.trim()) {
134
+ this.logLine(line, logOptions, true);
135
+ }
136
+ });
137
+ });
138
+ }
139
+
140
+ childProcess.on('close', exitCode => {
141
+ const duration = Date.now() - startTime;
142
+ const code = exitCode || 0;
143
+
144
+ const result: ProcessResult = {
145
+ success: code === 0,
146
+ exitCode: code,
147
+ packageName: logOptions.prefix,
148
+ command: fullCommand,
149
+ duration,
150
+ };
151
+
152
+ const colorFn = this.getColorFn(logOptions.color);
153
+ if (code === 0) {
154
+ console.log(
155
+ `[${colorFn(logOptions.prefix)}] ` + pc.green(`āœ… Completed in ${duration}ms`)
156
+ );
157
+ } else {
158
+ console.log(
159
+ `[${colorFn(logOptions.prefix)}] ` +
160
+ pc.red(`āŒ Failed with exit code ${code} (${duration}ms)`)
161
+ );
162
+ }
163
+
164
+ resolve(result);
165
+ });
166
+
167
+ childProcess.on('error', error => {
168
+ const duration = Date.now() - startTime;
169
+ const colorFn = this.getColorFn(logOptions.color);
170
+ console.error(`[${colorFn(logOptions.prefix)}] ` + pc.red(`šŸ’„ Error: ${error.message}`));
171
+
172
+ resolve({
173
+ success: false,
174
+ exitCode: 1,
175
+ packageName: logOptions.prefix,
176
+ command: fullCommand,
177
+ duration,
178
+ });
179
+ });
180
+ });
181
+ }
182
+
183
+ /**
184
+ * Log a single line with prefix and color
185
+ */
186
+ private static logLine(line: string, logOptions: LogOptions, isError = false): void {
187
+ const timestamp = logOptions.showTimestamp ? pc.dim(`[${new Date().toISOString()}] `) : '';
188
+
189
+ // Only color the package name in brackets, not the entire line
190
+ const colorFn = this.getColorFn(logOptions.color);
191
+ const coloredPrefix = `[${colorFn(logOptions.prefix)}] `;
192
+
193
+ // Apply error color only to the actual log content if it's an error
194
+ const logContent = isError ? pc.red(line) : line;
195
+
196
+ console.log(timestamp + coloredPrefix + logContent);
197
+ }
198
+
199
+ /**
200
+ * Run multiple commands in parallel with concurrency limit
201
+ */
202
+ static async runParallel(
203
+ commands: Array<{
204
+ command: string;
205
+ args: string[];
206
+ options: ProcessOptions;
207
+ logOptions: LogOptions;
208
+ }>,
209
+ concurrency = 4
210
+ ): Promise<ProcessResult[]> {
211
+ const results: ProcessResult[] = [];
212
+ const executing: Promise<ProcessResult>[] = [];
213
+
214
+ for (let i = 0; i < commands.length; i++) {
215
+ const cmd = commands[i];
216
+ if (!cmd) continue;
217
+
218
+ // Start the command
219
+ const promise = this.runCommand(cmd.command, cmd.args, cmd.options, cmd.logOptions);
220
+
221
+ executing.push(promise);
222
+
223
+ // If we've reached the concurrency limit or this is the last command
224
+ if (executing.length >= concurrency || i === commands.length - 1) {
225
+ // Wait for at least one to complete
226
+ const completedIndex = await this.waitForAny(executing);
227
+ const completedPromise = executing[completedIndex];
228
+ if (completedPromise) {
229
+ const completed = await completedPromise;
230
+ results.push(completed);
231
+ }
232
+
233
+ // Remove completed promise from executing array
234
+ executing.splice(completedIndex, 1);
235
+ }
236
+ }
237
+
238
+ // Wait for all remaining commands to complete
239
+ const remainingResults = await Promise.all(executing);
240
+ results.push(...remainingResults);
241
+
242
+ return results;
243
+ }
244
+
245
+ /**
246
+ * Wait for any promise to complete and return its index
247
+ */
248
+ private static async waitForAny(promises: Promise<ProcessResult>[]): Promise<number> {
249
+ return new Promise(resolve => {
250
+ promises.forEach((promise, index) => {
251
+ promise.then(() => resolve(index));
252
+ });
253
+ });
254
+ }
255
+
256
+ /**
257
+ * Run commands sequentially (one after another)
258
+ */
259
+ static async runSequential(
260
+ commands: Array<{
261
+ command: string;
262
+ args: string[];
263
+ options: ProcessOptions;
264
+ logOptions: LogOptions;
265
+ }>
266
+ ): Promise<ProcessResult[]> {
267
+ const results: ProcessResult[] = [];
268
+
269
+ for (const cmd of commands) {
270
+ const result = await this.runCommand(cmd.command, cmd.args, cmd.options, cmd.logOptions);
271
+
272
+ results.push(result);
273
+
274
+ // Stop on first failure unless explicitly configured to continue
275
+ if (!result.success) {
276
+ console.log(pc.red(`\nāŒ Stopping execution due to failure in ${result.packageName}`));
277
+ break;
278
+ }
279
+ }
280
+
281
+ return results;
282
+ }
283
+
284
+ /**
285
+ * Run commands in batches (for dependency-aware execution)
286
+ */
287
+ static async runBatches(
288
+ batches: Array<
289
+ Array<{
290
+ command: string;
291
+ args: string[];
292
+ options: ProcessOptions;
293
+ logOptions: LogOptions;
294
+ }>
295
+ >,
296
+ concurrency = 4
297
+ ): Promise<ProcessResult[]> {
298
+ const allResults: ProcessResult[] = [];
299
+
300
+ for (let i = 0; i < batches.length; i++) {
301
+ const batch = batches[i];
302
+ if (!batch) continue;
303
+
304
+ console.log(
305
+ pc.blue(`\nšŸ”„ Running batch ${i + 1}/${batches.length} (${batch.length} packages)`)
306
+ );
307
+
308
+ // Run all commands in this batch in parallel
309
+ const batchResults = await this.runParallel(batch, concurrency);
310
+ allResults.push(...batchResults);
311
+
312
+ // Check if any command in this batch failed
313
+ const failures = batchResults.filter(r => !r.success);
314
+ if (failures.length > 0) {
315
+ console.log(pc.red(`\nāŒ Batch ${i + 1} failed. The following packages failed:`));
316
+ failures.forEach(f => {
317
+ console.log(pc.red(` • ${f.packageName}: ${f.command}`));
318
+ });
319
+ console.log(pc.red(`\nStopping execution due to batch failure.`));
320
+ break;
321
+ }
322
+
323
+ console.log(pc.green(`āœ… Batch ${i + 1} completed successfully`));
324
+ }
325
+
326
+ return allResults;
327
+ }
328
+
329
+ /**
330
+ * Print execution summary
331
+ */
332
+ static printSummary(results: ProcessResult[]): void {
333
+ const successful = results.filter(r => r.success);
334
+ const failed = results.filter(r => !r.success);
335
+ const totalDuration = results.reduce((sum, r) => sum + r.duration, 0);
336
+
337
+ console.log(pc.bold('\nšŸ“Š Execution Summary:'));
338
+ console.log(pc.green(`āœ… Successful: ${successful.length}`));
339
+
340
+ if (failed.length > 0) {
341
+ console.log(pc.red(`āŒ Failed: ${failed.length}`));
342
+ console.log(pc.red('\nFailed packages:'));
343
+ failed.forEach(f => {
344
+ console.log(pc.red(` • ${f.packageName} (exit code ${f.exitCode})`));
345
+ });
346
+ }
347
+
348
+ console.log(pc.blue(`ā±ļø Total duration: ${totalDuration}ms`));
349
+
350
+ if (successful.length > 0) {
351
+ const avgDuration = Math.round(totalDuration / successful.length);
352
+ console.log(pc.dim(`šŸ“ˆ Average duration: ${avgDuration}ms`));
353
+ }
354
+ }
355
+ }