workgraph 0.0.5 → 0.0.7
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.
- package/README.md +70 -19
- package/dist/cli.js +217 -67
- package/dist/cli.js.map +1 -1
- package/dist/executor.d.ts +1 -1
- package/dist/executor.js +49 -17
- package/dist/executor.js.map +1 -1
- package/dist/graph.js +1 -1
- package/dist/graph.js.map +1 -1
- package/dist/planner.js +2 -2
- package/dist/planner.js.map +1 -1
- package/dist/source-scanner.js +32 -6
- package/dist/source-scanner.js.map +1 -1
- package/dist/types.d.ts +28 -27
- package/dist/ui.d.ts +26 -0
- package/dist/ui.js +254 -0
- package/dist/ui.js.map +1 -0
- package/dist/watcher.d.ts +10 -2
- package/dist/watcher.js +2 -3
- package/dist/watcher.js.map +1 -1
- package/dist/workspace.js +18 -3
- package/dist/workspace.js.map +1 -1
- package/package.json +11 -2
package/README.md
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
<div align="center">
|
|
2
|
+
<picture>
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="assets/logo-dark.png">
|
|
4
|
+
<source media="(prefers-color-scheme: light)" srcset="assets/logo.png">
|
|
5
|
+
<img src="assets/logo.png" alt="workgraph Logo" width="500">
|
|
6
|
+
</picture>
|
|
7
|
+
</div>
|
|
4
8
|
|
|
5
9
|
## Features
|
|
6
10
|
|
|
@@ -12,16 +16,17 @@ Workspace dependency analyzer and parallel build orchestrator for npm/yarn/pnpm
|
|
|
12
16
|
- **Dev Server Management** - Start and manage multiple dev servers with prefixed output
|
|
13
17
|
- **Concurrent Execution** - Executes builds with configurable concurrency limits
|
|
14
18
|
- **Source Generation** - Automatically run code generators for missing/generated sources
|
|
19
|
+
- **Terminal UI** - Split-screen interface with tasks panel, build logs, and task output
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
15
22
|
|
|
16
|
-
|
|
23
|
+
Run directly with npx:
|
|
17
24
|
|
|
18
25
|
```bash
|
|
19
|
-
|
|
20
|
-
# or
|
|
21
|
-
npm install -D workgraph
|
|
26
|
+
npx workgraph <command>
|
|
22
27
|
```
|
|
23
28
|
|
|
24
|
-
## CLI
|
|
29
|
+
## CLI Commands
|
|
25
30
|
|
|
26
31
|
### Analyze Dependencies
|
|
27
32
|
|
|
@@ -160,17 +165,35 @@ workgraph watch --dry-run
|
|
|
160
165
|
workgraph watch --filter 'libs/*' api web
|
|
161
166
|
```
|
|
162
167
|
|
|
163
|
-
|
|
168
|
+
### Terminal UI
|
|
169
|
+
|
|
170
|
+
Watch mode displays a 3-panel terminal interface:
|
|
171
|
+
|
|
172
|
+
```
|
|
173
|
+
┌─ Tasks ─────┬─ Workgraph ──────────────┬─ Task Output ──────────────┐
|
|
174
|
+
│ ● api 12345 │ [14:32:01] Watching... │ [api] Server started │
|
|
175
|
+
│ ● web 12346 │ [14:32:05] Building auth │ [web] Compiled successfully│
|
|
176
|
+
│ ○ build:auth│ [14:32:06] auth: done │ │
|
|
177
|
+
└─────────────┴──────────────────────────┴────────────────────────────┘
|
|
164
178
|
```
|
|
165
|
-
Starting dev server: @myorg/api
|
|
166
|
-
Starting dev server: @myorg/web
|
|
167
179
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
180
|
+
**Panels:**
|
|
181
|
+
- **Tasks** - Running processes with status icons and PIDs
|
|
182
|
+
- **Workgraph** - Build logs with timestamps
|
|
183
|
+
- **Task Output** - Dev server and build output
|
|
171
184
|
|
|
172
|
-
|
|
173
|
-
|
|
185
|
+
**Task Status Icons:**
|
|
186
|
+
- `●` green - Running
|
|
187
|
+
- `○` gray - Stopped
|
|
188
|
+
- `✖` red - Error
|
|
189
|
+
|
|
190
|
+
**Navigation:**
|
|
191
|
+
- `Tab` - Switch focus between panels
|
|
192
|
+
- `Ctrl+C` - Stop all processes and exit
|
|
193
|
+
|
|
194
|
+
To disable the UI and use plain console output:
|
|
195
|
+
```bash
|
|
196
|
+
workgraph watch --no-ui
|
|
174
197
|
```
|
|
175
198
|
|
|
176
199
|
## CLI Options
|
|
@@ -179,10 +202,12 @@ Press Ctrl+C to stop
|
|
|
179
202
|
|--------|-------------|---------|
|
|
180
203
|
| `-r, --root <path>` | Workspace root directory | `process.cwd()` |
|
|
181
204
|
| `-c, --changed <projects...>` | Changed projects (names or paths) | `[]` |
|
|
182
|
-
| `--concurrency <number>` | Max parallel builds |
|
|
205
|
+
| `--concurrency <number>` | Max parallel builds | `4` |
|
|
183
206
|
| `--debounce <ms>` | Debounce time for watch mode | `200` |
|
|
184
207
|
| `--dry-run` | Show plan without executing | `false` |
|
|
185
208
|
| `--filter <pattern>` | Only build projects matching pattern (e.g., `libs/*`) | - |
|
|
209
|
+
| `--no-ui` | Disable split-screen terminal UI (watch mode) | `false` |
|
|
210
|
+
| `--verbose` | Show detailed watcher output | `false` |
|
|
186
211
|
|
|
187
212
|
## Source Generation
|
|
188
213
|
|
|
@@ -196,15 +221,41 @@ Add `workgraph.sources` to your root `package.json`:
|
|
|
196
221
|
{
|
|
197
222
|
"workgraph": {
|
|
198
223
|
"sources": {
|
|
199
|
-
"apps/web-angular/src/generated":
|
|
224
|
+
"apps/web-angular/src/generated": {
|
|
225
|
+
"command": "npx exposify-codegen api --output ./apps/web-angular/src/generated --target angular",
|
|
226
|
+
"deps": ["api"]
|
|
227
|
+
},
|
|
228
|
+
"apps/web-preact/src/generated": {
|
|
229
|
+
"command": "npx exposify-codegen api --output ./apps/web-preact/src/generated --target preact",
|
|
230
|
+
"deps": ["api"]
|
|
231
|
+
}
|
|
200
232
|
}
|
|
201
233
|
}
|
|
202
234
|
}
|
|
203
235
|
```
|
|
204
236
|
|
|
237
|
+
You can also use the shorthand string format for simple generators:
|
|
238
|
+
|
|
239
|
+
```json
|
|
240
|
+
{
|
|
241
|
+
"workgraph": {
|
|
242
|
+
"sources": {
|
|
243
|
+
"libs/types/src/generated": "npx generate-types"
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Configuration Options
|
|
250
|
+
|
|
251
|
+
| Field | Description |
|
|
252
|
+
|-------|-------------|
|
|
253
|
+
| `command` | The shell command to run for generation |
|
|
254
|
+
| `deps` | Array of project names/paths that trigger this generator when changed |
|
|
255
|
+
|
|
205
256
|
### How It Works
|
|
206
257
|
|
|
207
|
-
1. When `build` or `watch` runs, workgraph checks if any affected project
|
|
258
|
+
1. When `build` or `watch` runs, workgraph checks if any affected project matches a generator's `deps`
|
|
208
259
|
2. If so, the source generation command runs **before** the build
|
|
209
260
|
3. This ensures generated code is up-to-date before compilation
|
|
210
261
|
|
package/dist/cli.js
CHANGED
|
@@ -44,14 +44,15 @@ const planner_1 = require("./planner");
|
|
|
44
44
|
const watcher_1 = require("./watcher");
|
|
45
45
|
const executor_1 = require("./executor");
|
|
46
46
|
const source_scanner_1 = require("./source-scanner");
|
|
47
|
+
const ui_1 = require("./ui");
|
|
47
48
|
function normalizeSourceConfig(config) {
|
|
48
49
|
if (typeof config === 'string') {
|
|
49
|
-
return { command: config };
|
|
50
|
+
return { command: config, deps: [] };
|
|
50
51
|
}
|
|
51
52
|
return config;
|
|
52
53
|
}
|
|
53
54
|
function shouldRunGenerator(sourcePath, sourceConfig, affectedProjects, projects, root) {
|
|
54
|
-
if (sourceConfig.deps
|
|
55
|
+
if (sourceConfig.deps.length > 0) {
|
|
55
56
|
for (const dep of sourceConfig.deps) {
|
|
56
57
|
if (affectedProjects.has(dep)) {
|
|
57
58
|
return true;
|
|
@@ -78,18 +79,25 @@ function shouldRunGenerator(sourcePath, sourceConfig, affectedProjects, projects
|
|
|
78
79
|
}
|
|
79
80
|
return false;
|
|
80
81
|
}
|
|
81
|
-
async function
|
|
82
|
+
async function runSourceGeneratorsWithUI(root, affectedProjects, projects, dryRun = false, callbacks = {
|
|
83
|
+
log: (msg) => console.log(`[${(0, watcher_1.formatTimestamp)()}] ${msg}`),
|
|
84
|
+
taskLog: console.log,
|
|
85
|
+
addTask: () => { },
|
|
86
|
+
updateTask: () => { },
|
|
87
|
+
}) {
|
|
88
|
+
const { log, taskLog, addTask, updateTask } = callbacks;
|
|
82
89
|
const config = (0, source_scanner_1.loadWorkgraphConfig)(root);
|
|
83
|
-
const sources = config.sources
|
|
90
|
+
const sources = config.sources;
|
|
84
91
|
const generated = [];
|
|
85
92
|
for (const [sourcePath, rawConfig] of Object.entries(sources)) {
|
|
86
93
|
const sourceConfig = normalizeSourceConfig(rawConfig);
|
|
87
94
|
if (!shouldRunGenerator(sourcePath, sourceConfig, affectedProjects, projects, root)) {
|
|
88
95
|
continue;
|
|
89
96
|
}
|
|
90
|
-
|
|
97
|
+
const taskId = `gen-${sourcePath}`;
|
|
98
|
+
log(`Generating: ${sourcePath}`);
|
|
99
|
+
taskLog(`\x1b[33m$ ${sourceConfig.command}\x1b[0m`);
|
|
91
100
|
if (dryRun) {
|
|
92
|
-
console.log(`[${(0, watcher_1.formatTimestamp)()}] [dry-run] Would run: ${sourceConfig.command}`);
|
|
93
101
|
generated.push(sourcePath);
|
|
94
102
|
continue;
|
|
95
103
|
}
|
|
@@ -100,28 +108,46 @@ async function runSourceGenerators(root, affectedProjects, projects, dryRun = fa
|
|
|
100
108
|
shell: true,
|
|
101
109
|
stdio: 'pipe',
|
|
102
110
|
});
|
|
111
|
+
if (proc.pid) {
|
|
112
|
+
addTask({ id: taskId, name: `gen:${sourcePath}`, pid: proc.pid, status: 'running' });
|
|
113
|
+
}
|
|
103
114
|
let output = '';
|
|
104
|
-
proc.stdout?.on('data', (data) =>
|
|
105
|
-
|
|
115
|
+
proc.stdout?.on('data', (data) => {
|
|
116
|
+
const text = data.toString();
|
|
117
|
+
output += text;
|
|
118
|
+
text.split('\n').forEach((line) => {
|
|
119
|
+
if (line.trim())
|
|
120
|
+
taskLog(line);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
proc.stderr?.on('data', (data) => {
|
|
124
|
+
const text = data.toString();
|
|
125
|
+
output += text;
|
|
126
|
+
text.split('\n').forEach((line) => {
|
|
127
|
+
if (line.trim())
|
|
128
|
+
taskLog(line);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
106
131
|
proc.on('close', (code) => {
|
|
132
|
+
updateTask(taskId, code === 0 ? 'stopped' : 'error');
|
|
107
133
|
resolve({ success: code === 0, output });
|
|
108
134
|
});
|
|
109
135
|
proc.on('error', (err) => {
|
|
136
|
+
updateTask(taskId, 'error');
|
|
110
137
|
resolve({ success: false, output: err.message });
|
|
111
138
|
});
|
|
112
139
|
});
|
|
113
140
|
if (result.success) {
|
|
114
|
-
|
|
141
|
+
log('Generated successfully');
|
|
115
142
|
generated.push(sourcePath);
|
|
116
143
|
}
|
|
117
144
|
else {
|
|
118
|
-
|
|
119
|
-
console.error(result.output);
|
|
145
|
+
log('Generation FAILED');
|
|
120
146
|
return { success: false, generated };
|
|
121
147
|
}
|
|
122
148
|
}
|
|
123
149
|
catch (error) {
|
|
124
|
-
|
|
150
|
+
log(`Generation FAILED: ${error.message}`);
|
|
125
151
|
return { success: false, generated };
|
|
126
152
|
}
|
|
127
153
|
}
|
|
@@ -179,6 +205,8 @@ program
|
|
|
179
205
|
const config = (0, source_scanner_1.loadWorkgraphConfig)(root);
|
|
180
206
|
for (const sourcePath of result.configuredSources) {
|
|
181
207
|
const sourceConfig = config.sources[sourcePath];
|
|
208
|
+
if (!sourceConfig)
|
|
209
|
+
continue;
|
|
182
210
|
const command = typeof sourceConfig === 'string' ? sourceConfig : sourceConfig.command;
|
|
183
211
|
console.log(` ${sourcePath}`);
|
|
184
212
|
console.log(` -> ${command}`);
|
|
@@ -242,7 +270,11 @@ program
|
|
|
242
270
|
.option('-c, --changed <projects...>', 'Changed projects (names or paths)', [])
|
|
243
271
|
.option('--concurrency <number>', 'Max parallel builds', String(4))
|
|
244
272
|
.option('--dry-run', 'Show what would be built without executing')
|
|
245
|
-
.action(async (
|
|
273
|
+
.action(async (rawOptions) => {
|
|
274
|
+
const options = {
|
|
275
|
+
...rawOptions,
|
|
276
|
+
dryRun: rawOptions.dryRun === true,
|
|
277
|
+
};
|
|
246
278
|
try {
|
|
247
279
|
const root = path.resolve(options.root);
|
|
248
280
|
const projects = await (0, workspace_1.loadWorkspaceProjects)(root);
|
|
@@ -267,7 +299,7 @@ program
|
|
|
267
299
|
console.log('Nothing to build');
|
|
268
300
|
return;
|
|
269
301
|
}
|
|
270
|
-
const sourceResult = await
|
|
302
|
+
const sourceResult = await runSourceGeneratorsWithUI(root, affected, projects, options.dryRun);
|
|
271
303
|
if (!sourceResult.success) {
|
|
272
304
|
console.error('Source generation failed, aborting build');
|
|
273
305
|
process.exit(1);
|
|
@@ -282,11 +314,11 @@ program
|
|
|
282
314
|
const mode = info.isParallel ? 'parallel' : 'sequential';
|
|
283
315
|
console.log(`[${(0, watcher_1.formatTimestamp)()}] Building: ${info.project} (wave ${info.wave}/${info.totalWaves} ${mode}, step ${info.step}/${info.totalSteps})`);
|
|
284
316
|
},
|
|
285
|
-
onComplete: (
|
|
286
|
-
const status =
|
|
287
|
-
console.log(`[${(0, watcher_1.formatTimestamp)()}] ${
|
|
288
|
-
if (!
|
|
289
|
-
console.error(
|
|
317
|
+
onComplete: (buildResult) => {
|
|
318
|
+
const status = buildResult.success ? 'done' : 'FAILED';
|
|
319
|
+
console.log(`[${(0, watcher_1.formatTimestamp)()}] ${buildResult.project}: ${status} (${buildResult.duration}ms)`);
|
|
320
|
+
if (!buildResult.success && buildResult.error) {
|
|
321
|
+
console.error(buildResult.error);
|
|
290
322
|
}
|
|
291
323
|
},
|
|
292
324
|
});
|
|
@@ -314,7 +346,14 @@ program
|
|
|
314
346
|
.option('--dry-run', 'Show what would be built without executing')
|
|
315
347
|
.option('--filter <pattern>', 'Only build projects matching pattern (e.g., "libs/*")')
|
|
316
348
|
.option('--verbose', 'Show detailed watcher and build output')
|
|
317
|
-
.
|
|
349
|
+
.option('--no-ui', 'Disable split-screen UI')
|
|
350
|
+
.action(async (apps, rawOptions) => {
|
|
351
|
+
const options = {
|
|
352
|
+
...rawOptions,
|
|
353
|
+
dryRun: rawOptions.dryRun === true,
|
|
354
|
+
filter: rawOptions.filter !== undefined ? rawOptions.filter : '',
|
|
355
|
+
verbose: rawOptions.verbose === true,
|
|
356
|
+
};
|
|
318
357
|
try {
|
|
319
358
|
const root = path.resolve(options.root);
|
|
320
359
|
const projects = await (0, workspace_1.loadWorkspaceProjects)(root);
|
|
@@ -324,7 +363,52 @@ program
|
|
|
324
363
|
console.error('Cannot watch: cycles detected in dependency graph');
|
|
325
364
|
process.exit(1);
|
|
326
365
|
}
|
|
366
|
+
const ui = options.ui !== false ? (0, ui_1.createUI)() : null;
|
|
367
|
+
const log = ui ? ui.log : (msg) => console.log(`[${(0, watcher_1.formatTimestamp)()}] ${msg}`);
|
|
368
|
+
const logRaw = ui ? (msg) => ui.leftPane.log(msg) : (msg) => console.log(msg);
|
|
369
|
+
const taskLog = ui ? ui.taskLog : (msg) => console.log(msg);
|
|
370
|
+
const setStatus = ui ? ui.setStatus : () => { };
|
|
371
|
+
const addTask = ui ? ui.addTask : () => { };
|
|
372
|
+
const updateTask = ui ? ui.updateTask : () => { };
|
|
373
|
+
const updateTaskPort = ui ? ui.updateTaskPort : () => { };
|
|
327
374
|
const devProcesses = [];
|
|
375
|
+
const cleanup = () => {
|
|
376
|
+
if (ui) {
|
|
377
|
+
ui.destroy();
|
|
378
|
+
}
|
|
379
|
+
const dim = '\x1b[2m';
|
|
380
|
+
const green = '\x1b[32m';
|
|
381
|
+
const red = '\x1b[31m';
|
|
382
|
+
const reset = '\x1b[0m';
|
|
383
|
+
process.stdout.write('\x1b[2J\x1b[H');
|
|
384
|
+
const output = [''];
|
|
385
|
+
if (devProcesses.length > 0) {
|
|
386
|
+
output.push(`${dim}Stopping ${devProcesses.length} dev server(s)${reset}`);
|
|
387
|
+
for (const { proc, name } of devProcesses) {
|
|
388
|
+
if (proc.pid) {
|
|
389
|
+
const shortName = name.includes('/') ? name.split('/').pop() : name;
|
|
390
|
+
try {
|
|
391
|
+
process.kill(-proc.pid, 'SIGKILL');
|
|
392
|
+
output.push(` ${green}✓${reset} ${shortName}`);
|
|
393
|
+
}
|
|
394
|
+
catch {
|
|
395
|
+
try {
|
|
396
|
+
process.kill(proc.pid, 'SIGKILL');
|
|
397
|
+
output.push(` ${green}✓${reset} ${shortName}`);
|
|
398
|
+
}
|
|
399
|
+
catch {
|
|
400
|
+
output.push(` ${red}✗${reset} ${shortName} ${dim}(already stopped)${reset}`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
output.push('');
|
|
407
|
+
process.stdout.write(output.join('\n'));
|
|
408
|
+
process.exit(0);
|
|
409
|
+
};
|
|
410
|
+
process.on('SIGINT', cleanup);
|
|
411
|
+
process.on('SIGTERM', cleanup);
|
|
328
412
|
if (apps.length > 0) {
|
|
329
413
|
const resolvedApps = (0, affected_1.resolveProjectNames)(apps, graph);
|
|
330
414
|
if (resolvedApps.size === 0) {
|
|
@@ -333,17 +417,17 @@ program
|
|
|
333
417
|
}
|
|
334
418
|
const depsToBuilt = new Set();
|
|
335
419
|
for (const appName of resolvedApps) {
|
|
336
|
-
const appDeps = graph.deps.get(appName)
|
|
420
|
+
const appDeps = graph.deps.get(appName) ?? new Set();
|
|
337
421
|
for (const dep of appDeps) {
|
|
338
422
|
depsToBuilt.add(dep);
|
|
339
|
-
const transitiveDeps = graph.deps.get(dep)
|
|
423
|
+
const transitiveDeps = graph.deps.get(dep) ?? new Set();
|
|
340
424
|
for (const td of transitiveDeps) {
|
|
341
425
|
depsToBuilt.add(td);
|
|
342
426
|
}
|
|
343
427
|
}
|
|
344
428
|
}
|
|
345
429
|
if (depsToBuilt.size > 0) {
|
|
346
|
-
|
|
430
|
+
log(`Building ${depsToBuilt.size} dependencies first...`);
|
|
347
431
|
const plan = (0, planner_1.createBuildPlan)(depsToBuilt, graph.deps);
|
|
348
432
|
if (plan.waves.length > 0) {
|
|
349
433
|
const result = await (0, executor_1.executePlan)(plan.waves, projects, root, {
|
|
@@ -351,74 +435,90 @@ program
|
|
|
351
435
|
dryRun: options.dryRun,
|
|
352
436
|
onStart: (info) => {
|
|
353
437
|
const mode = info.isParallel ? 'parallel' : 'sequential';
|
|
354
|
-
|
|
438
|
+
log(`Building: ${info.project} (wave ${info.wave}/${info.totalWaves} ${mode}, step ${info.step}/${info.totalSteps})`);
|
|
439
|
+
const shortName = info.project.includes('/') ? info.project.split('/').pop() : info.project;
|
|
440
|
+
addTask({ id: `build-${info.project}`, name: `build:${shortName}`, pid: 0, status: 'running' });
|
|
355
441
|
},
|
|
356
442
|
onComplete: (buildResult) => {
|
|
357
443
|
const status = buildResult.success ? 'done' : 'FAILED';
|
|
358
|
-
|
|
444
|
+
log(`${buildResult.project}: ${status} (${buildResult.duration}ms)`);
|
|
445
|
+
updateTask(`build-${buildResult.project}`, buildResult.success ? 'stopped' : 'error');
|
|
359
446
|
if (!buildResult.success && buildResult.error) {
|
|
360
|
-
|
|
447
|
+
taskLog(buildResult.error);
|
|
361
448
|
}
|
|
362
449
|
},
|
|
450
|
+
onOutput: taskLog,
|
|
363
451
|
});
|
|
364
452
|
if (result.success) {
|
|
365
|
-
|
|
453
|
+
log('Dependencies built successfully');
|
|
366
454
|
}
|
|
367
455
|
else {
|
|
368
|
-
|
|
456
|
+
log('Dependency build had errors, continuing anyway...');
|
|
369
457
|
}
|
|
370
458
|
}
|
|
371
459
|
}
|
|
372
460
|
const affected = new Set([...resolvedApps, ...depsToBuilt]);
|
|
373
|
-
const sourceResult = await
|
|
461
|
+
const sourceResult = await runSourceGeneratorsWithUI(root, affected, projects, options.dryRun, { log, taskLog, addTask, updateTask });
|
|
374
462
|
if (!sourceResult.success) {
|
|
375
|
-
|
|
463
|
+
log('Source generation failed, continuing anyway...');
|
|
376
464
|
}
|
|
377
465
|
for (const appName of resolvedApps) {
|
|
378
466
|
const project = projects.get(appName);
|
|
379
467
|
if (!project)
|
|
380
468
|
continue;
|
|
381
|
-
const hasDevScript = project.packageJson.scripts
|
|
469
|
+
const hasDevScript = project.packageJson.scripts['dev'];
|
|
382
470
|
if (!hasDevScript) {
|
|
383
|
-
|
|
471
|
+
log(`Warning: ${appName} has no dev script, skipping`);
|
|
384
472
|
continue;
|
|
385
473
|
}
|
|
386
|
-
|
|
474
|
+
const devCmd = `npm run dev -w ${appName}`;
|
|
475
|
+
log(`Starting dev server: ${appName}`);
|
|
476
|
+
taskLog(`\x1b[33m$ ${devCmd}\x1b[0m`);
|
|
387
477
|
const proc = (0, child_process_1.spawn)('npm', ['run', 'dev', '-w', appName], {
|
|
388
478
|
cwd: root,
|
|
389
479
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
390
|
-
|
|
480
|
+
detached: true,
|
|
391
481
|
});
|
|
392
482
|
const shortName = appName.includes('/') ? appName.split('/').pop() : appName;
|
|
393
483
|
const prefix = `[${shortName}]`;
|
|
394
|
-
const
|
|
484
|
+
const taskId = `dev-${appName}`;
|
|
485
|
+
if (!proc.pid) {
|
|
486
|
+
throw new Error(`Failed to start dev server for ${appName}: no pid`);
|
|
487
|
+
}
|
|
488
|
+
addTask({ id: taskId, name: shortName ?? appName, pid: proc.pid, status: 'running' });
|
|
489
|
+
const ESC = '\x1b';
|
|
490
|
+
const clearRegex = new RegExp(`${ESC}\\[\\d*J|${ESC}\\[\\d*H|${ESC}c`, 'g');
|
|
491
|
+
const stripClearCodes = (text) => text.replace(clearRegex, '');
|
|
395
492
|
proc.stdout?.on('data', (data) => {
|
|
396
493
|
const lines = stripClearCodes(data.toString()).trim().split('\n');
|
|
397
494
|
for (const line of lines) {
|
|
398
|
-
if (line)
|
|
399
|
-
|
|
495
|
+
if (line) {
|
|
496
|
+
taskLog(`${prefix} ${line}`);
|
|
497
|
+
const port = (0, ui_1.detectPort)(line);
|
|
498
|
+
if (port > 0) {
|
|
499
|
+
updateTaskPort(taskId, port);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
400
502
|
}
|
|
401
503
|
});
|
|
402
504
|
proc.stderr?.on('data', (data) => {
|
|
403
505
|
const lines = stripClearCodes(data.toString()).trim().split('\n');
|
|
404
506
|
for (const line of lines) {
|
|
405
|
-
if (line)
|
|
406
|
-
|
|
507
|
+
if (line) {
|
|
508
|
+
taskLog(`${prefix} ${line}`);
|
|
509
|
+
const port = (0, ui_1.detectPort)(line);
|
|
510
|
+
if (port > 0) {
|
|
511
|
+
updateTaskPort(taskId, port);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
407
514
|
}
|
|
408
515
|
});
|
|
409
516
|
proc.on('close', (code) => {
|
|
410
|
-
|
|
517
|
+
log(`${prefix} exited with code ${code}`);
|
|
518
|
+
updateTask(taskId, code === 0 ? 'stopped' : 'error');
|
|
411
519
|
});
|
|
412
|
-
devProcesses.push(proc);
|
|
520
|
+
devProcesses.push({ proc, name: appName, command: devCmd });
|
|
413
521
|
}
|
|
414
|
-
const cleanup = () => {
|
|
415
|
-
for (const proc of devProcesses) {
|
|
416
|
-
proc.kill();
|
|
417
|
-
}
|
|
418
|
-
process.exit(0);
|
|
419
|
-
};
|
|
420
|
-
process.on('SIGINT', cleanup);
|
|
421
|
-
process.on('SIGTERM', cleanup);
|
|
422
522
|
console.log();
|
|
423
523
|
}
|
|
424
524
|
const filterPattern = options.filter;
|
|
@@ -437,14 +537,15 @@ program
|
|
|
437
537
|
const filteredCount = filterPattern
|
|
438
538
|
? [...projects.keys()].filter(matchesFilter).length
|
|
439
539
|
: projects.size;
|
|
440
|
-
|
|
540
|
+
log(`Watching ${projects.size} projects for changes...`);
|
|
441
541
|
if (filterPattern) {
|
|
442
|
-
|
|
542
|
+
log(`Building only projects matching: ${filterPattern} (${filteredCount} projects)`);
|
|
443
543
|
}
|
|
444
|
-
|
|
544
|
+
log('Press Ctrl+C to stop');
|
|
445
545
|
let isBuilding = false;
|
|
446
546
|
let pendingChanges = null;
|
|
447
|
-
|
|
547
|
+
let buildCount = 0;
|
|
548
|
+
const handleChanges = async (changedProjects, changedFiles) => {
|
|
448
549
|
if (isBuilding) {
|
|
449
550
|
pendingChanges = pendingChanges || new Set();
|
|
450
551
|
for (const p of changedProjects) {
|
|
@@ -453,42 +554,90 @@ program
|
|
|
453
554
|
return;
|
|
454
555
|
}
|
|
455
556
|
isBuilding = true;
|
|
456
|
-
|
|
557
|
+
log('Changes detected:');
|
|
558
|
+
if (changedFiles) {
|
|
559
|
+
for (const [project, files] of changedFiles) {
|
|
560
|
+
for (const file of files) {
|
|
561
|
+
const relativePath = file.replace(root + '/', '');
|
|
562
|
+
logRaw(` ${relativePath} (${project})`);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
else {
|
|
567
|
+
logRaw(` ${[...changedProjects].join(', ')} (pending)`);
|
|
568
|
+
}
|
|
457
569
|
const affected = (0, affected_1.getAffectedProjects)(changedProjects, graph.rdeps);
|
|
458
570
|
const filteredAffected = new Set([...affected].filter(matchesFilter));
|
|
459
571
|
if (filteredAffected.size === 0) {
|
|
460
|
-
|
|
572
|
+
log('No matching projects to build');
|
|
461
573
|
isBuilding = false;
|
|
462
574
|
return;
|
|
463
575
|
}
|
|
576
|
+
buildCount++;
|
|
577
|
+
setStatus(`build #${buildCount}`);
|
|
578
|
+
logRaw('');
|
|
464
579
|
const plan = (0, planner_1.createBuildPlan)(filteredAffected, graph.deps);
|
|
465
|
-
const
|
|
580
|
+
const totalSteps = plan.waves.reduce((sum, w) => sum + w.length, 0);
|
|
581
|
+
logRaw('Affected dependencies:');
|
|
582
|
+
for (const proj of [...filteredAffected].sort((a, b) => a.localeCompare(b))) {
|
|
583
|
+
const deps = graph.deps.get(proj) ?? new Set();
|
|
584
|
+
const affectedDeps = [...deps].filter(d => filteredAffected.has(d));
|
|
585
|
+
if (affectedDeps.length > 0) {
|
|
586
|
+
logRaw(` ${proj} -> ${affectedDeps.join(', ')}`);
|
|
587
|
+
}
|
|
588
|
+
else {
|
|
589
|
+
logRaw(` ${proj} (no affected dependencies)`);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
logRaw('Compilation plan:');
|
|
593
|
+
let stepCounter = 0;
|
|
594
|
+
for (let i = 0; i < plan.waves.length; i++) {
|
|
595
|
+
const wave = plan.waves[i];
|
|
596
|
+
const mode = wave.length > 1 ? 'parallel' : 'sequential';
|
|
597
|
+
for (const proj of wave) {
|
|
598
|
+
stepCounter++;
|
|
599
|
+
logRaw(` [${stepCounter}/${totalSteps}] ${proj} (wave ${i + 1}/${plan.waves.length}, ${mode})`);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
logRaw('');
|
|
603
|
+
const sourceResult = await runSourceGeneratorsWithUI(root, affected, projects, options.dryRun, { log, taskLog, addTask, updateTask });
|
|
466
604
|
if (!sourceResult.success) {
|
|
467
|
-
|
|
605
|
+
log('Source generation failed');
|
|
606
|
+
setStatus(`build #${buildCount} FAILED`);
|
|
468
607
|
isBuilding = false;
|
|
469
608
|
return;
|
|
470
609
|
}
|
|
471
|
-
|
|
610
|
+
log(`Building: ${[...filteredAffected].join(', ')}`);
|
|
472
611
|
if (plan.waves.length > 0) {
|
|
473
612
|
const result = await (0, executor_1.executePlan)(plan.waves, projects, root, {
|
|
474
613
|
concurrency: parseInt(options.concurrency, 10),
|
|
475
614
|
dryRun: options.dryRun,
|
|
476
615
|
onStart: (info) => {
|
|
477
616
|
const mode = info.isParallel ? 'parallel' : 'sequential';
|
|
478
|
-
|
|
617
|
+
log(`Building: ${info.project} (wave ${info.wave}/${info.totalWaves} ${mode}, step ${info.step}/${info.totalSteps})`);
|
|
618
|
+
const shortName = info.project.includes('/') ? info.project.split('/').pop() : info.project;
|
|
619
|
+
addTask({ id: `build-${info.project}`, name: `build:${shortName}`, pid: 0, status: 'running' });
|
|
479
620
|
},
|
|
621
|
+
onOutput: taskLog,
|
|
480
622
|
onComplete: (buildResult) => {
|
|
481
623
|
const status = buildResult.success ? 'done' : 'FAILED';
|
|
482
|
-
|
|
624
|
+
log(`${buildResult.project}: ${status} (${buildResult.duration}ms)`);
|
|
625
|
+
updateTask(`build-${buildResult.project}`, buildResult.success ? 'stopped' : 'error');
|
|
483
626
|
},
|
|
484
627
|
});
|
|
485
628
|
if (result.success) {
|
|
486
|
-
|
|
629
|
+
setStatus(`build #${buildCount} done`);
|
|
630
|
+
log('Build complete');
|
|
487
631
|
}
|
|
488
632
|
else {
|
|
489
|
-
|
|
633
|
+
setStatus(`build #${buildCount} FAILED`);
|
|
634
|
+
log('Build failed');
|
|
490
635
|
}
|
|
491
636
|
}
|
|
637
|
+
else {
|
|
638
|
+
setStatus(`build #${buildCount} done`);
|
|
639
|
+
log('Build complete');
|
|
640
|
+
}
|
|
492
641
|
isBuilding = false;
|
|
493
642
|
if (pendingChanges && pendingChanges.size > 0) {
|
|
494
643
|
const next = pendingChanges;
|
|
@@ -497,18 +646,19 @@ program
|
|
|
497
646
|
}
|
|
498
647
|
};
|
|
499
648
|
const config = (0, source_scanner_1.loadWorkgraphConfig)(root);
|
|
500
|
-
const sourcePaths = Object.keys(config.sources
|
|
649
|
+
const sourcePaths = Object.keys(config.sources).map(p => `**/${p}/**`);
|
|
501
650
|
if (sourcePaths.length > 0) {
|
|
502
|
-
|
|
651
|
+
log('Ignoring generated source paths (to prevent rebuild loops):');
|
|
503
652
|
for (const p of sourcePaths) {
|
|
504
|
-
|
|
653
|
+
logRaw(` ${p}`);
|
|
505
654
|
}
|
|
506
|
-
console.log();
|
|
507
655
|
}
|
|
508
656
|
(0, watcher_1.createWatcher)({
|
|
509
657
|
root,
|
|
510
658
|
debounceMs: parseInt(options.debounce, 10),
|
|
511
|
-
onChange:
|
|
659
|
+
onChange: (changedProjects, changedFiles) => {
|
|
660
|
+
void handleChanges(changedProjects, changedFiles);
|
|
661
|
+
},
|
|
512
662
|
ignorePatterns: sourcePaths,
|
|
513
663
|
verbose: options.verbose,
|
|
514
664
|
}, projects);
|