workgraph 0.0.1
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/LICENSE +21 -0
- package/README.md +252 -0
- package/dist/affected.d.ts +7 -0
- package/dist/affected.js +61 -0
- package/dist/affected.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +329 -0
- package/dist/cli.js.map +1 -0
- package/dist/executor.d.ts +3 -0
- package/dist/executor.js +156 -0
- package/dist/executor.js.map +1 -0
- package/dist/graph.d.ts +5 -0
- package/dist/graph.js +98 -0
- package/dist/graph.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/planner.d.ts +4 -0
- package/dist/planner.js +58 -0
- package/dist/planner.js.map +1 -0
- package/dist/types.d.ts +69 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/watcher.d.ts +4 -0
- package/dist/watcher.js +109 -0
- package/dist/watcher.js.map +1 -0
- package/dist/workspace.d.ts +4 -0
- package/dist/workspace.js +117 -0
- package/dist/workspace.js.map +1 -0
- package/package.json +50 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
# workgraph
|
|
2
|
+
|
|
3
|
+
Workspace dependency analyzer and parallel build orchestrator for npm/yarn/pnpm monorepos.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Dependency Graph Analysis** - Scans workspace projects and builds a directed dependency graph
|
|
8
|
+
- **Cycle Detection** - Detects circular dependencies using DFS with coloring
|
|
9
|
+
- **Affected Project Detection** - Determines which projects are affected by changes (transitive dependents)
|
|
10
|
+
- **Parallel Build Planning** - Uses Kahn's algorithm to plan build waves (parallel within wave, sequential between waves)
|
|
11
|
+
- **File Watching** - Monitors file changes with debouncing and triggers rebuilds
|
|
12
|
+
- **Dev Server Management** - Start and manage multiple dev servers with prefixed output
|
|
13
|
+
- **Concurrent Execution** - Executes builds with configurable concurrency limits
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install -g workgraph
|
|
19
|
+
# or
|
|
20
|
+
npm install -D workgraph
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## CLI Usage
|
|
24
|
+
|
|
25
|
+
### Analyze Dependencies
|
|
26
|
+
|
|
27
|
+
Show the dependency graph and detect cycles:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
workgraph analyze
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Output:
|
|
34
|
+
```
|
|
35
|
+
Analyzing workspace at: /path/to/workspace
|
|
36
|
+
|
|
37
|
+
Found 6 projects
|
|
38
|
+
|
|
39
|
+
Dependency Graph:
|
|
40
|
+
@myorg/api (apps/api)
|
|
41
|
+
-> @myorg/auth
|
|
42
|
+
@myorg/auth (libs/auth)
|
|
43
|
+
(no dependencies)
|
|
44
|
+
@myorg/web (apps/web)
|
|
45
|
+
-> @myorg/api
|
|
46
|
+
|
|
47
|
+
No cycles detected
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Plan Build
|
|
51
|
+
|
|
52
|
+
Show what would be built for specific changes:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
# By package name
|
|
56
|
+
workgraph plan -c @myorg/auth
|
|
57
|
+
|
|
58
|
+
# By shorthand name
|
|
59
|
+
workgraph plan -c auth
|
|
60
|
+
|
|
61
|
+
# By path
|
|
62
|
+
workgraph plan -c libs/auth
|
|
63
|
+
|
|
64
|
+
# Multiple changes
|
|
65
|
+
workgraph plan -c auth -c utils
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Output:
|
|
69
|
+
```
|
|
70
|
+
Changed: @myorg/auth, @myorg/utils
|
|
71
|
+
Affected: @myorg/api, @myorg/auth, @myorg/utils, @myorg/web
|
|
72
|
+
|
|
73
|
+
Build Plan:
|
|
74
|
+
Wave 1 (parallel): @myorg/auth, @myorg/utils
|
|
75
|
+
Wave 2: @myorg/api
|
|
76
|
+
Wave 3: @myorg/web
|
|
77
|
+
|
|
78
|
+
Total: 4 projects in 3 waves
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Build Affected Projects
|
|
82
|
+
|
|
83
|
+
Execute the build plan:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
# Build all
|
|
87
|
+
workgraph build
|
|
88
|
+
|
|
89
|
+
# Build affected by specific changes
|
|
90
|
+
workgraph build -c auth
|
|
91
|
+
|
|
92
|
+
# Dry run (show plan without executing)
|
|
93
|
+
workgraph build -c auth --dry-run
|
|
94
|
+
|
|
95
|
+
# With custom concurrency
|
|
96
|
+
workgraph build -c auth --concurrency 2
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Watch Mode
|
|
100
|
+
|
|
101
|
+
Watch for file changes and automatically rebuild affected projects:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
# Watch all projects
|
|
105
|
+
workgraph watch
|
|
106
|
+
|
|
107
|
+
# Watch all, but only rebuild libs (for dev with app servers)
|
|
108
|
+
workgraph watch --filter 'libs/*'
|
|
109
|
+
|
|
110
|
+
# Watch libs + start app dev servers
|
|
111
|
+
workgraph watch --filter 'libs/*' api web
|
|
112
|
+
|
|
113
|
+
# Dry run mode
|
|
114
|
+
workgraph watch --dry-run
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**Dev workflow (single terminal):**
|
|
118
|
+
```bash
|
|
119
|
+
# Watch libs + start API and web dev servers
|
|
120
|
+
workgraph watch --filter 'libs/*' api web
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Output:
|
|
124
|
+
```
|
|
125
|
+
Starting dev server: @myorg/api
|
|
126
|
+
Starting dev server: @myorg/web
|
|
127
|
+
|
|
128
|
+
Watching 7 projects for changes...
|
|
129
|
+
Building only projects matching: libs/* (4 projects)
|
|
130
|
+
Press Ctrl+C to stop
|
|
131
|
+
|
|
132
|
+
[api] [Nest] Starting Nest application...
|
|
133
|
+
[web] ➜ Local: http://localhost:3000
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## CLI Options
|
|
137
|
+
|
|
138
|
+
| Option | Description | Default |
|
|
139
|
+
|--------|-------------|---------|
|
|
140
|
+
| `-r, --root <path>` | Workspace root directory | `process.cwd()` |
|
|
141
|
+
| `-c, --changed <projects...>` | Changed projects (names or paths) | `[]` |
|
|
142
|
+
| `--concurrency <number>` | Max parallel builds | CPU count - 1 |
|
|
143
|
+
| `--debounce <ms>` | Debounce time for watch mode | `200` |
|
|
144
|
+
| `--dry-run` | Show plan without executing | `false` |
|
|
145
|
+
| `--filter <pattern>` | Only build projects matching pattern (e.g., `libs/*`) | - |
|
|
146
|
+
|
|
147
|
+
## Programmatic API
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
import {
|
|
151
|
+
loadWorkspaceProjects,
|
|
152
|
+
buildGraph,
|
|
153
|
+
detectCycles,
|
|
154
|
+
getAffectedProjects,
|
|
155
|
+
planWaves,
|
|
156
|
+
executePlan,
|
|
157
|
+
createWatcher,
|
|
158
|
+
} from 'workgraph';
|
|
159
|
+
|
|
160
|
+
// Load workspace projects
|
|
161
|
+
const projects = await loadWorkspaceProjects('/path/to/workspace');
|
|
162
|
+
|
|
163
|
+
// Build dependency graph
|
|
164
|
+
const graph = buildGraph(projects);
|
|
165
|
+
|
|
166
|
+
// Check for cycles
|
|
167
|
+
const cycles = detectCycles(graph);
|
|
168
|
+
if (cycles) {
|
|
169
|
+
console.error('Cycles detected:', cycles);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Get affected projects
|
|
173
|
+
const changed = new Set(['@myorg/auth']);
|
|
174
|
+
const affected = getAffectedProjects(changed, graph.rdeps);
|
|
175
|
+
|
|
176
|
+
// Plan build waves
|
|
177
|
+
const waves = planWaves(affected, graph.deps);
|
|
178
|
+
// Result: [['@myorg/auth'], ['@myorg/api'], ['@myorg/web']]
|
|
179
|
+
|
|
180
|
+
// Execute build plan
|
|
181
|
+
const result = await executePlan(waves, projects, '/path/to/workspace', {
|
|
182
|
+
concurrency: 4,
|
|
183
|
+
dryRun: false,
|
|
184
|
+
});
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Algorithm Details
|
|
188
|
+
|
|
189
|
+
### Dependency Graph
|
|
190
|
+
|
|
191
|
+
The library builds two maps:
|
|
192
|
+
- `deps[A] = Set<B>` - A depends on B
|
|
193
|
+
- `rdeps[B] = Set<A>` - B is a dependency of A (reverse graph)
|
|
194
|
+
|
|
195
|
+
Dependencies are collected from:
|
|
196
|
+
- `dependencies`
|
|
197
|
+
- `devDependencies`
|
|
198
|
+
- `peerDependencies`
|
|
199
|
+
- `optionalDependencies`
|
|
200
|
+
|
|
201
|
+
### Cycle Detection
|
|
202
|
+
|
|
203
|
+
Uses DFS with three-color marking:
|
|
204
|
+
- WHITE (0): Unvisited
|
|
205
|
+
- GRAY (1): Currently visiting (in stack)
|
|
206
|
+
- BLACK (2): Fully visited
|
|
207
|
+
|
|
208
|
+
A back edge to a GRAY node indicates a cycle.
|
|
209
|
+
|
|
210
|
+
### Wave Planning (Kahn's Algorithm)
|
|
211
|
+
|
|
212
|
+
1. Build induced subgraph from affected projects
|
|
213
|
+
2. Calculate in-degree for each node (count of unbuilt dependencies)
|
|
214
|
+
3. Repeat until all nodes processed:
|
|
215
|
+
- Wave N = all nodes with in-degree 0
|
|
216
|
+
- Remove wave nodes and decrement dependents' in-degrees
|
|
217
|
+
|
|
218
|
+
Projects in the same wave have no dependencies on each other and can be built in parallel.
|
|
219
|
+
|
|
220
|
+
### Affected Detection
|
|
221
|
+
|
|
222
|
+
Uses BFS on the reverse dependency graph:
|
|
223
|
+
1. Start with changed projects
|
|
224
|
+
2. Add all transitive dependents via `rdeps`
|
|
225
|
+
3. Result includes original changes + all projects that depend on them
|
|
226
|
+
|
|
227
|
+
## Ignored Paths
|
|
228
|
+
|
|
229
|
+
The watcher ignores these patterns by default:
|
|
230
|
+
- `**/node_modules/**`
|
|
231
|
+
- `**/dist/**`
|
|
232
|
+
- `**/.angular/**`
|
|
233
|
+
- `**/.nx/**`
|
|
234
|
+
- `**/coverage/**`
|
|
235
|
+
- `**/*.log`
|
|
236
|
+
- `**/.git/**`
|
|
237
|
+
- `**/tmp/**`
|
|
238
|
+
- `**/.cache/**`
|
|
239
|
+
|
|
240
|
+
## Root Config Changes
|
|
241
|
+
|
|
242
|
+
Changes to root configuration files trigger a rebuild of all projects:
|
|
243
|
+
- `package.json`
|
|
244
|
+
- `package-lock.json`
|
|
245
|
+
- `pnpm-lock.yaml`
|
|
246
|
+
- `yarn.lock`
|
|
247
|
+
- `tsconfig.json`
|
|
248
|
+
- `tsconfig.base.json`
|
|
249
|
+
|
|
250
|
+
## License
|
|
251
|
+
|
|
252
|
+
MIT
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { DependencyGraph, Project } from './types';
|
|
2
|
+
export declare function getAffectedProjects(changedProjects: Set<string>, rdeps: Map<string, Set<string>>): Set<string>;
|
|
3
|
+
export declare function getChangedProjectsFromFiles(changedFiles: string[], projects: Map<string, Project>, root: string): {
|
|
4
|
+
changedProjects: Set<string>;
|
|
5
|
+
isGlobalChange: boolean;
|
|
6
|
+
};
|
|
7
|
+
export declare function resolveProjectNames(projectIdentifiers: string[], graph: DependencyGraph): Set<string>;
|
package/dist/affected.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getAffectedProjects = getAffectedProjects;
|
|
4
|
+
exports.getChangedProjectsFromFiles = getChangedProjectsFromFiles;
|
|
5
|
+
exports.resolveProjectNames = resolveProjectNames;
|
|
6
|
+
const workspace_1 = require("./workspace");
|
|
7
|
+
function getAffectedProjects(changedProjects, rdeps) {
|
|
8
|
+
const affected = new Set();
|
|
9
|
+
const queue = [...changedProjects];
|
|
10
|
+
while (queue.length > 0) {
|
|
11
|
+
const current = queue.shift();
|
|
12
|
+
if (affected.has(current))
|
|
13
|
+
continue;
|
|
14
|
+
affected.add(current);
|
|
15
|
+
const dependents = rdeps.get(current) || new Set();
|
|
16
|
+
for (const dependent of dependents) {
|
|
17
|
+
if (!affected.has(dependent)) {
|
|
18
|
+
queue.push(dependent);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return affected;
|
|
23
|
+
}
|
|
24
|
+
function getChangedProjectsFromFiles(changedFiles, projects, root) {
|
|
25
|
+
const changedProjects = new Set();
|
|
26
|
+
let isGlobalChange = false;
|
|
27
|
+
for (const file of changedFiles) {
|
|
28
|
+
if ((0, workspace_1.isRootConfig)(file, root)) {
|
|
29
|
+
isGlobalChange = true;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const project = (0, workspace_1.getProjectFromPath)(file, projects, root);
|
|
33
|
+
if (project) {
|
|
34
|
+
changedProjects.add(project);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return { changedProjects, isGlobalChange };
|
|
38
|
+
}
|
|
39
|
+
function resolveProjectNames(projectIdentifiers, graph) {
|
|
40
|
+
const resolved = new Set();
|
|
41
|
+
for (const identifier of projectIdentifiers) {
|
|
42
|
+
if (graph.projects.has(identifier)) {
|
|
43
|
+
resolved.add(identifier);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
for (const [name, project] of graph.projects) {
|
|
47
|
+
if (project.path === identifier || project.absolutePath === identifier) {
|
|
48
|
+
resolved.add(name);
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
for (const name of graph.projects.keys()) {
|
|
53
|
+
if (name.endsWith('/' + identifier) || name === identifier) {
|
|
54
|
+
resolved.add(name);
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return resolved;
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=affected.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"affected.js","sourceRoot":"","sources":["../src/affected.ts"],"names":[],"mappings":";;AAGA,kDAsBC;AAED,kEAqBC;AAED,kDA+BC;AAhFD,2CAA+D;AAE/D,SAAgB,mBAAmB,CACjC,eAA4B,EAC5B,KAA+B;IAE/B,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAU,CAAC;IACnC,MAAM,KAAK,GAAG,CAAC,GAAG,eAAe,CAAC,CAAC;IAEnC,OAAO,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACxB,MAAM,OAAO,GAAG,KAAK,CAAC,KAAK,EAAG,CAAC;QAE/B,IAAI,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC;YAAE,SAAS;QACpC,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QAEtB,MAAM,UAAU,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,IAAI,GAAG,EAAE,CAAC;QACnD,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;YACnC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC;gBAC7B,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YACxB,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,SAAgB,2BAA2B,CACzC,YAAsB,EACtB,QAA8B,EAC9B,IAAY;IAEZ,MAAM,eAAe,GAAG,IAAI,GAAG,EAAU,CAAC;IAC1C,IAAI,cAAc,GAAG,KAAK,CAAC;IAE3B,KAAK,MAAM,IAAI,IAAI,YAAY,EAAE,CAAC;QAChC,IAAI,IAAA,wBAAY,EAAC,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC;YAC7B,cAAc,GAAG,IAAI,CAAC;YACtB,SAAS;QACX,CAAC;QAED,MAAM,OAAO,GAAG,IAAA,8BAAkB,EAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAC;QACzD,IAAI,OAAO,EAAE,CAAC;YACZ,eAAe,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QAC/B,CAAC;IACH,CAAC;IAED,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,CAAC;AAC7C,CAAC;AAED,SAAgB,mBAAmB,CACjC,kBAA4B,EAC5B,KAAsB;IAEtB,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAU,CAAC;IAEnC,KAAK,MAAM,UAAU,IAAI,kBAAkB,EAAE,CAAC;QAE5C,IAAI,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC;YACnC,QAAQ,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;YACzB,SAAS;QACX,CAAC;QAGD,KAAK,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC;YAC7C,IAAI,OAAO,CAAC,IAAI,KAAK,UAAU,IAAI,OAAO,CAAC,YAAY,KAAK,UAAU,EAAE,CAAC;gBACvE,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;gBACnB,MAAM;YACR,CAAC;QACH,CAAC;QAGD,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC;YACzC,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,GAAG,UAAU,CAAC,IAAI,IAAI,KAAK,UAAU,EAAE,CAAC;gBAC3D,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;gBACnB,MAAM;YACR,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC"}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
36
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
const commander_1 = require("commander");
|
|
38
|
+
const child_process_1 = require("child_process");
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const workspace_1 = require("./workspace");
|
|
41
|
+
const graph_1 = require("./graph");
|
|
42
|
+
const affected_1 = require("./affected");
|
|
43
|
+
const planner_1 = require("./planner");
|
|
44
|
+
const watcher_1 = require("./watcher");
|
|
45
|
+
const executor_1 = require("./executor");
|
|
46
|
+
const program = new commander_1.Command();
|
|
47
|
+
program
|
|
48
|
+
.name('workgraph')
|
|
49
|
+
.description('Lightweight workspace dependency graph and parallel build orchestrator for monorepos')
|
|
50
|
+
.version('0.0.1');
|
|
51
|
+
program
|
|
52
|
+
.command('analyze')
|
|
53
|
+
.description('Analyze workspace dependencies and show graph')
|
|
54
|
+
.option('-r, --root <path>', 'Workspace root directory', process.cwd())
|
|
55
|
+
.action(async (options) => {
|
|
56
|
+
try {
|
|
57
|
+
const root = path.resolve(options.root);
|
|
58
|
+
console.log(`Analyzing workspace at: ${root}\n`);
|
|
59
|
+
const projects = await (0, workspace_1.loadWorkspaceProjects)(root);
|
|
60
|
+
console.log(`Found ${projects.size} projects\n`);
|
|
61
|
+
const graph = (0, graph_1.buildGraph)(projects);
|
|
62
|
+
console.log('Dependency Graph:');
|
|
63
|
+
console.log((0, graph_1.formatGraph)(graph));
|
|
64
|
+
console.log();
|
|
65
|
+
const cycles = (0, graph_1.detectCycles)(graph);
|
|
66
|
+
if (cycles) {
|
|
67
|
+
console.log('Cycles detected:');
|
|
68
|
+
for (const cycle of cycles) {
|
|
69
|
+
console.log(` ${cycle.join(' -> ')}`);
|
|
70
|
+
}
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
console.log('No cycles detected');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
console.error('Error:', error.message);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
program
|
|
83
|
+
.command('plan')
|
|
84
|
+
.description('Show build plan for changed projects')
|
|
85
|
+
.option('-r, --root <path>', 'Workspace root directory', process.cwd())
|
|
86
|
+
.option('-c, --changed <projects...>', 'Changed projects (names or paths)', [])
|
|
87
|
+
.action(async (options) => {
|
|
88
|
+
try {
|
|
89
|
+
const root = path.resolve(options.root);
|
|
90
|
+
const projects = await (0, workspace_1.loadWorkspaceProjects)(root);
|
|
91
|
+
const graph = (0, graph_1.buildGraph)(projects);
|
|
92
|
+
const cycles = (0, graph_1.detectCycles)(graph);
|
|
93
|
+
if (cycles) {
|
|
94
|
+
console.error('Cannot plan: cycles detected in dependency graph');
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
let changedProjects;
|
|
98
|
+
if (options.changed.length === 0) {
|
|
99
|
+
console.log('No --changed specified, showing plan for all projects\n');
|
|
100
|
+
changedProjects = new Set(projects.keys());
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
changedProjects = (0, affected_1.resolveProjectNames)(options.changed, graph);
|
|
104
|
+
if (changedProjects.size === 0) {
|
|
105
|
+
console.error('Could not resolve any projects from:', options.changed);
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
console.log(`Changed: ${[...changedProjects].join(', ')}\n`);
|
|
109
|
+
}
|
|
110
|
+
const affected = (0, affected_1.getAffectedProjects)(changedProjects, graph.rdeps);
|
|
111
|
+
const plan = (0, planner_1.createBuildPlan)(affected, graph.deps);
|
|
112
|
+
console.log((0, planner_1.formatBuildPlan)(plan));
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
console.error('Error:', error.message);
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
program
|
|
120
|
+
.command('build')
|
|
121
|
+
.description('Build affected projects')
|
|
122
|
+
.option('-r, --root <path>', 'Workspace root directory', process.cwd())
|
|
123
|
+
.option('-c, --changed <projects...>', 'Changed projects (names or paths)', [])
|
|
124
|
+
.option('--concurrency <number>', 'Max parallel builds', String(4))
|
|
125
|
+
.option('--dry-run', 'Show what would be built without executing')
|
|
126
|
+
.action(async (options) => {
|
|
127
|
+
try {
|
|
128
|
+
const root = path.resolve(options.root);
|
|
129
|
+
const projects = await (0, workspace_1.loadWorkspaceProjects)(root);
|
|
130
|
+
const graph = (0, graph_1.buildGraph)(projects);
|
|
131
|
+
const cycles = (0, graph_1.detectCycles)(graph);
|
|
132
|
+
if (cycles) {
|
|
133
|
+
console.error('Cannot build: cycles detected in dependency graph');
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
let changedProjects;
|
|
137
|
+
if (options.changed.length === 0) {
|
|
138
|
+
changedProjects = new Set(projects.keys());
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
changedProjects = (0, affected_1.resolveProjectNames)(options.changed, graph);
|
|
142
|
+
}
|
|
143
|
+
const affected = (0, affected_1.getAffectedProjects)(changedProjects, graph.rdeps);
|
|
144
|
+
const plan = (0, planner_1.createBuildPlan)(affected, graph.deps);
|
|
145
|
+
console.log((0, planner_1.formatBuildPlan)(plan));
|
|
146
|
+
console.log();
|
|
147
|
+
if (plan.waves.length === 0) {
|
|
148
|
+
console.log('Nothing to build');
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const result = await (0, executor_1.executePlan)(plan.waves, projects, root, {
|
|
152
|
+
concurrency: parseInt(options.concurrency, 10),
|
|
153
|
+
dryRun: options.dryRun,
|
|
154
|
+
onStart: (project) => {
|
|
155
|
+
console.log(`[${(0, watcher_1.formatTimestamp)()}] Building: ${project}`);
|
|
156
|
+
},
|
|
157
|
+
onComplete: (result) => {
|
|
158
|
+
const status = result.success ? 'done' : 'FAILED';
|
|
159
|
+
console.log(`[${(0, watcher_1.formatTimestamp)()}] ${result.project}: ${status} (${result.duration}ms)`);
|
|
160
|
+
if (!result.success && result.error) {
|
|
161
|
+
console.error(result.error);
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
console.log();
|
|
166
|
+
if (result.success) {
|
|
167
|
+
console.log(`Build complete in ${result.duration}ms`);
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
console.error(`Build failed after ${result.duration}ms`);
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
console.error('Error:', error.message);
|
|
176
|
+
process.exit(1);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
program
|
|
180
|
+
.command('watch')
|
|
181
|
+
.description('Watch for changes and rebuild affected projects')
|
|
182
|
+
.argument('[apps...]', 'Apps to run dev servers for (e.g., api web-angular)')
|
|
183
|
+
.option('-r, --root <path>', 'Workspace root directory', process.cwd())
|
|
184
|
+
.option('--concurrency <number>', 'Max parallel builds', String(4))
|
|
185
|
+
.option('--debounce <ms>', 'Debounce time in milliseconds', String(200))
|
|
186
|
+
.option('--dry-run', 'Show what would be built without executing')
|
|
187
|
+
.option('--filter <pattern>', 'Only build projects matching pattern (e.g., "libs/*")')
|
|
188
|
+
.action(async (apps, options) => {
|
|
189
|
+
try {
|
|
190
|
+
const root = path.resolve(options.root);
|
|
191
|
+
const projects = await (0, workspace_1.loadWorkspaceProjects)(root);
|
|
192
|
+
const graph = (0, graph_1.buildGraph)(projects);
|
|
193
|
+
const cycles = (0, graph_1.detectCycles)(graph);
|
|
194
|
+
if (cycles) {
|
|
195
|
+
console.error('Cannot watch: cycles detected in dependency graph');
|
|
196
|
+
process.exit(1);
|
|
197
|
+
}
|
|
198
|
+
const devProcesses = [];
|
|
199
|
+
if (apps.length > 0) {
|
|
200
|
+
const resolvedApps = (0, affected_1.resolveProjectNames)(apps, graph);
|
|
201
|
+
if (resolvedApps.size === 0) {
|
|
202
|
+
console.error('Could not resolve any apps from:', apps);
|
|
203
|
+
process.exit(1);
|
|
204
|
+
}
|
|
205
|
+
for (const appName of resolvedApps) {
|
|
206
|
+
const project = projects.get(appName);
|
|
207
|
+
if (!project)
|
|
208
|
+
continue;
|
|
209
|
+
const hasDevScript = project.packageJson.scripts?.dev;
|
|
210
|
+
if (!hasDevScript) {
|
|
211
|
+
console.warn(`Warning: ${appName} has no dev script, skipping`);
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
console.log(`Starting dev server: ${appName}`);
|
|
215
|
+
const proc = (0, child_process_1.spawn)('npm', ['run', 'dev', '-w', appName], {
|
|
216
|
+
cwd: root,
|
|
217
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
218
|
+
shell: true,
|
|
219
|
+
});
|
|
220
|
+
const shortName = appName.includes('/') ? appName.split('/').pop() : appName;
|
|
221
|
+
const prefix = `[${shortName}]`;
|
|
222
|
+
proc.stdout?.on('data', (data) => {
|
|
223
|
+
const lines = data.toString().trim().split('\n');
|
|
224
|
+
for (const line of lines) {
|
|
225
|
+
console.log(`${prefix} ${line}`);
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
proc.stderr?.on('data', (data) => {
|
|
229
|
+
const lines = data.toString().trim().split('\n');
|
|
230
|
+
for (const line of lines) {
|
|
231
|
+
console.error(`${prefix} ${line}`);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
proc.on('close', (code) => {
|
|
235
|
+
console.log(`${prefix} exited with code ${code}`);
|
|
236
|
+
});
|
|
237
|
+
devProcesses.push(proc);
|
|
238
|
+
}
|
|
239
|
+
const cleanup = () => {
|
|
240
|
+
for (const proc of devProcesses) {
|
|
241
|
+
proc.kill();
|
|
242
|
+
}
|
|
243
|
+
process.exit(0);
|
|
244
|
+
};
|
|
245
|
+
process.on('SIGINT', cleanup);
|
|
246
|
+
process.on('SIGTERM', cleanup);
|
|
247
|
+
console.log();
|
|
248
|
+
}
|
|
249
|
+
const filterPattern = options.filter;
|
|
250
|
+
const matchesFilter = (projectName) => {
|
|
251
|
+
if (!filterPattern)
|
|
252
|
+
return true;
|
|
253
|
+
const project = projects.get(projectName);
|
|
254
|
+
if (!project)
|
|
255
|
+
return false;
|
|
256
|
+
if (filterPattern.includes('*')) {
|
|
257
|
+
const regex = new RegExp('^' + filterPattern.replace(/\*/g, '.*') + '$');
|
|
258
|
+
return regex.test(project.path);
|
|
259
|
+
}
|
|
260
|
+
return project.path.startsWith(filterPattern) || projectName.includes(filterPattern);
|
|
261
|
+
};
|
|
262
|
+
const filteredCount = filterPattern
|
|
263
|
+
? [...projects.keys()].filter(matchesFilter).length
|
|
264
|
+
: projects.size;
|
|
265
|
+
console.log(`Watching ${projects.size} projects for changes...`);
|
|
266
|
+
if (filterPattern) {
|
|
267
|
+
console.log(`Building only projects matching: ${filterPattern} (${filteredCount} projects)`);
|
|
268
|
+
}
|
|
269
|
+
console.log('Press Ctrl+C to stop\n');
|
|
270
|
+
let isBuilding = false;
|
|
271
|
+
let pendingChanges = null;
|
|
272
|
+
const handleChanges = async (changedProjects) => {
|
|
273
|
+
if (isBuilding) {
|
|
274
|
+
pendingChanges = pendingChanges || new Set();
|
|
275
|
+
for (const p of changedProjects) {
|
|
276
|
+
pendingChanges.add(p);
|
|
277
|
+
}
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
isBuilding = true;
|
|
281
|
+
console.log(`[${(0, watcher_1.formatTimestamp)()}] Changes detected: ${[...changedProjects].join(', ')}`);
|
|
282
|
+
const affected = (0, affected_1.getAffectedProjects)(changedProjects, graph.rdeps);
|
|
283
|
+
const filteredAffected = new Set([...affected].filter(matchesFilter));
|
|
284
|
+
if (filteredAffected.size === 0) {
|
|
285
|
+
console.log(`[${(0, watcher_1.formatTimestamp)()}] No matching projects to build\n`);
|
|
286
|
+
isBuilding = false;
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
const plan = (0, planner_1.createBuildPlan)(filteredAffected, graph.deps);
|
|
290
|
+
console.log(`[${(0, watcher_1.formatTimestamp)()}] Building: ${[...filteredAffected].join(', ')}`);
|
|
291
|
+
if (plan.waves.length > 0) {
|
|
292
|
+
const result = await (0, executor_1.executePlan)(plan.waves, projects, root, {
|
|
293
|
+
concurrency: parseInt(options.concurrency, 10),
|
|
294
|
+
dryRun: options.dryRun,
|
|
295
|
+
onStart: (project) => {
|
|
296
|
+
console.log(`[${(0, watcher_1.formatTimestamp)()}] Building: ${project}`);
|
|
297
|
+
},
|
|
298
|
+
onComplete: (buildResult) => {
|
|
299
|
+
const status = buildResult.success ? 'done' : 'FAILED';
|
|
300
|
+
console.log(`[${(0, watcher_1.formatTimestamp)()}] ${buildResult.project}: ${status} (${buildResult.duration}ms)`);
|
|
301
|
+
},
|
|
302
|
+
});
|
|
303
|
+
if (result.success) {
|
|
304
|
+
console.log(`[${(0, watcher_1.formatTimestamp)()}] Build complete\n`);
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
console.error(`[${(0, watcher_1.formatTimestamp)()}] Build failed\n`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
isBuilding = false;
|
|
311
|
+
if (pendingChanges && pendingChanges.size > 0) {
|
|
312
|
+
const next = pendingChanges;
|
|
313
|
+
pendingChanges = null;
|
|
314
|
+
await handleChanges(next);
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
(0, watcher_1.createWatcher)({
|
|
318
|
+
root,
|
|
319
|
+
debounceMs: parseInt(options.debounce, 10),
|
|
320
|
+
onChange: handleChanges,
|
|
321
|
+
}, projects);
|
|
322
|
+
}
|
|
323
|
+
catch (error) {
|
|
324
|
+
console.error('Error:', error.message);
|
|
325
|
+
process.exit(1);
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
program.parse();
|
|
329
|
+
//# sourceMappingURL=cli.js.map
|