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.
- package/.github/workflows/mdbook.yml +64 -0
- package/.prettierignore +22 -0
- package/.prettierrc +13 -0
- package/LICENSE +21 -0
- package/README.md +278 -0
- package/docs/book.toml +10 -0
- package/docs/src/SUMMARY.md +24 -0
- package/docs/src/commands/build.md +110 -0
- package/docs/src/commands/dev.md +118 -0
- package/docs/src/commands/overview.md +239 -0
- package/docs/src/commands/run.md +153 -0
- package/docs/src/configuration.md +249 -0
- package/docs/src/examples.md +567 -0
- package/docs/src/installation.md +148 -0
- package/docs/src/introduction.md +117 -0
- package/docs/src/quick-start.md +278 -0
- package/docs/src/troubleshooting.md +533 -0
- package/index.ts +84 -0
- package/package.json +54 -0
- package/src/commands/build.ts +158 -0
- package/src/commands/dev.ts +192 -0
- package/src/commands/run.test.ts +329 -0
- package/src/commands/run.ts +118 -0
- package/src/core/dependency-graph.ts +262 -0
- package/src/core/process-runner.ts +355 -0
- package/src/core/workspace.test.ts +404 -0
- package/src/core/workspace.ts +228 -0
- package/src/package-managers/bun.test.ts +209 -0
- package/src/package-managers/bun.ts +79 -0
- package/src/package-managers/detector.test.ts +199 -0
- package/src/package-managers/detector.ts +111 -0
- package/src/package-managers/index.ts +10 -0
- package/src/package-managers/npm.ts +79 -0
- package/src/package-managers/pnpm.ts +101 -0
- package/src/package-managers/types.ts +42 -0
- package/src/utils/output.ts +301 -0
- package/src/utils/package-utils.ts +243 -0
- package/tests/bun-workspace/apps/web-app/package.json +18 -0
- package/tests/bun-workspace/bun.lockb +0 -0
- package/tests/bun-workspace/package.json +18 -0
- package/tests/bun-workspace/packages/shared-utils/package.json +15 -0
- package/tests/bun-workspace/packages/ui-components/package.json +17 -0
- package/tests/npm-workspace/package-lock.json +0 -0
- package/tests/npm-workspace/package.json +18 -0
- package/tests/npm-workspace/packages/core/package.json +15 -0
- package/tests/pnpm-workspace/package.json +14 -0
- package/tests/pnpm-workspace/packages/utils/package.json +15 -0
- package/tests/pnpm-workspace/pnpm-workspace.yaml +3 -0
- package/tsconfig.json +29 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
import { WorkspaceParser } from '../core/workspace.ts';
|
|
3
|
+
import { validatePackagesHaveScript, prepareCommandExecution } from '../utils/package-utils.ts';
|
|
4
|
+
import { ProcessRunner } from '../core/process-runner.ts';
|
|
5
|
+
import { Output } from '../utils/output.ts';
|
|
6
|
+
|
|
7
|
+
interface RunCommandOptions {
|
|
8
|
+
parallel?: boolean;
|
|
9
|
+
concurrency?: string;
|
|
10
|
+
filter?: string;
|
|
11
|
+
sequential?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function runCommand(scriptName: string, options: RunCommandOptions): Promise<void> {
|
|
15
|
+
try {
|
|
16
|
+
Output.info(`Running script "${scriptName}" across packages...\n`);
|
|
17
|
+
|
|
18
|
+
// Parse workspace
|
|
19
|
+
const parser = new WorkspaceParser();
|
|
20
|
+
const workspace = await parser.parseWorkspace();
|
|
21
|
+
|
|
22
|
+
Output.dim(`Workspace root: ${workspace.root}`, 'folder');
|
|
23
|
+
Output.dim(`Found ${workspace.packages.length} packages\n`, 'package');
|
|
24
|
+
|
|
25
|
+
// Filter packages if pattern provided
|
|
26
|
+
let targetPackages = workspace.packages;
|
|
27
|
+
if (options.filter) {
|
|
28
|
+
targetPackages = parser.filterPackages(workspace.packages, options.filter);
|
|
29
|
+
Output.log(
|
|
30
|
+
`Filtered to ${targetPackages.length} packages matching "${options.filter}"`,
|
|
31
|
+
'magnifying',
|
|
32
|
+
'yellow'
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Validate packages have the script
|
|
37
|
+
const { valid: packagesWithScript, invalid: packagesWithoutScript } =
|
|
38
|
+
validatePackagesHaveScript(targetPackages, scriptName);
|
|
39
|
+
|
|
40
|
+
if (packagesWithoutScript.length > 0) {
|
|
41
|
+
Output.warning(`The following packages don't have the "${scriptName}" script:`);
|
|
42
|
+
packagesWithoutScript.forEach(pkg => {
|
|
43
|
+
Output.listItem(pkg.name);
|
|
44
|
+
});
|
|
45
|
+
console.log();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (packagesWithScript.length === 0) {
|
|
49
|
+
Output.error(`No packages found with the "${scriptName}" script.`);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
Output.success(`Running "${scriptName}" in ${packagesWithScript.length} packages:`);
|
|
54
|
+
packagesWithScript.forEach(pkg => {
|
|
55
|
+
Output.listItem(pkg.name);
|
|
56
|
+
});
|
|
57
|
+
console.log();
|
|
58
|
+
|
|
59
|
+
// Determine execution mode (parallel by default unless explicitly sequential)
|
|
60
|
+
const isParallel = !options.sequential;
|
|
61
|
+
const concurrency = parseInt(options.concurrency || '4', 10);
|
|
62
|
+
|
|
63
|
+
Output.log(`Package manager: ${workspace.packageManager.name}`, 'wrench', 'blue');
|
|
64
|
+
Output.log(
|
|
65
|
+
`Execution mode: ${isParallel ? `parallel (concurrency: ${concurrency})` : 'sequential'}`,
|
|
66
|
+
'lightning',
|
|
67
|
+
'blue'
|
|
68
|
+
);
|
|
69
|
+
console.log();
|
|
70
|
+
|
|
71
|
+
// Prepare command execution
|
|
72
|
+
const commands = prepareCommandExecution(
|
|
73
|
+
packagesWithScript,
|
|
74
|
+
scriptName,
|
|
75
|
+
workspace.packageManager
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
// Execute commands
|
|
79
|
+
const startTime = Date.now();
|
|
80
|
+
let results;
|
|
81
|
+
|
|
82
|
+
if (isParallel) {
|
|
83
|
+
results = await ProcessRunner.runParallel(commands, concurrency);
|
|
84
|
+
} else {
|
|
85
|
+
results = await ProcessRunner.runSequential(commands);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const totalDuration = Date.now() - startTime;
|
|
89
|
+
|
|
90
|
+
// Print summary
|
|
91
|
+
const successful = results.filter(r => r.success);
|
|
92
|
+
const failed = results.filter(r => !r.success);
|
|
93
|
+
|
|
94
|
+
Output.executionSummary(successful.length, failed.length, totalDuration);
|
|
95
|
+
|
|
96
|
+
if (failed.length > 0) {
|
|
97
|
+
console.log(pc.red('\nFailed packages:'));
|
|
98
|
+
failed.forEach(f => {
|
|
99
|
+
Output.listItem(`${f.packageName} (exit code ${f.exitCode})`);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (successful.length > 0) {
|
|
104
|
+
const avgDuration = Math.round(
|
|
105
|
+
successful.reduce((sum, r) => sum + r.duration, 0) / successful.length
|
|
106
|
+
);
|
|
107
|
+
Output.dim(`Average package duration: ${Output.formatDuration(avgDuration)}`, 'chart');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Exit with error code if any commands failed
|
|
111
|
+
if (failed.length > 0) {
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
} catch (error) {
|
|
115
|
+
Output.log(`Error: ${error instanceof Error ? error.message : String(error)}`, 'fire', 'red');
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
export interface DependencyNode {
|
|
2
|
+
name: string;
|
|
3
|
+
dependencies: Set<string>;
|
|
4
|
+
dependents: Set<string>;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface TopologicalResult {
|
|
8
|
+
order: string[];
|
|
9
|
+
cycles: string[][];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class DependencyGraph {
|
|
13
|
+
private nodes: Map<string, DependencyNode> = new Map();
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Add a package to the dependency graph
|
|
17
|
+
*/
|
|
18
|
+
addPackage(packageName: string): void {
|
|
19
|
+
if (!this.nodes.has(packageName)) {
|
|
20
|
+
this.nodes.set(packageName, {
|
|
21
|
+
name: packageName,
|
|
22
|
+
dependencies: new Set(),
|
|
23
|
+
dependents: new Set(),
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Add a dependency relationship between two packages
|
|
30
|
+
*/
|
|
31
|
+
addDependency(packageName: string, dependencyName: string): void {
|
|
32
|
+
this.addPackage(packageName);
|
|
33
|
+
this.addPackage(dependencyName);
|
|
34
|
+
|
|
35
|
+
const packageNode = this.nodes.get(packageName)!;
|
|
36
|
+
const dependencyNode = this.nodes.get(dependencyName)!;
|
|
37
|
+
|
|
38
|
+
packageNode.dependencies.add(dependencyName);
|
|
39
|
+
dependencyNode.dependents.add(packageName);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get all packages in the graph
|
|
44
|
+
*/
|
|
45
|
+
getPackages(): string[] {
|
|
46
|
+
return Array.from(this.nodes.keys());
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get direct dependencies of a package
|
|
51
|
+
*/
|
|
52
|
+
getDependencies(packageName: string): string[] {
|
|
53
|
+
const node = this.nodes.get(packageName);
|
|
54
|
+
return node ? Array.from(node.dependencies) : [];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get direct dependents of a package
|
|
59
|
+
*/
|
|
60
|
+
getDependents(packageName: string): string[] {
|
|
61
|
+
const node = this.nodes.get(packageName);
|
|
62
|
+
return node ? Array.from(node.dependents) : [];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Perform topological sort using Kahn's algorithm
|
|
67
|
+
* Returns the build order and any detected cycles
|
|
68
|
+
*/
|
|
69
|
+
topologicalSort(): TopologicalResult {
|
|
70
|
+
const result: string[] = [];
|
|
71
|
+
const inDegree: Map<string, number> = new Map();
|
|
72
|
+
const queue: string[] = [];
|
|
73
|
+
|
|
74
|
+
// Initialize in-degree count for all nodes
|
|
75
|
+
for (const [packageName, node] of this.nodes) {
|
|
76
|
+
inDegree.set(packageName, node.dependencies.size);
|
|
77
|
+
|
|
78
|
+
// Add nodes with no dependencies to the queue
|
|
79
|
+
if (node.dependencies.size === 0) {
|
|
80
|
+
queue.push(packageName);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Process nodes with no incoming edges
|
|
85
|
+
while (queue.length > 0) {
|
|
86
|
+
const currentPackage = queue.shift()!;
|
|
87
|
+
result.push(currentPackage);
|
|
88
|
+
|
|
89
|
+
// Reduce in-degree for all dependents
|
|
90
|
+
const dependents = this.getDependents(currentPackage);
|
|
91
|
+
for (const dependent of dependents) {
|
|
92
|
+
const currentInDegree = inDegree.get(dependent)! - 1;
|
|
93
|
+
inDegree.set(dependent, currentInDegree);
|
|
94
|
+
|
|
95
|
+
// If no more dependencies, add to queue
|
|
96
|
+
if (currentInDegree === 0) {
|
|
97
|
+
queue.push(dependent);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Detect cycles
|
|
103
|
+
const cycles: string[][] = [];
|
|
104
|
+
if (result.length !== this.nodes.size) {
|
|
105
|
+
const remainingNodes = this.getPackages().filter(pkg => !result.includes(pkg));
|
|
106
|
+
const detectedCycles = this.detectCycles(remainingNodes);
|
|
107
|
+
cycles.push(...detectedCycles);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return { order: result, cycles };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Detect cycles in the remaining nodes using DFS
|
|
115
|
+
*/
|
|
116
|
+
private detectCycles(remainingNodes: string[]): string[][] {
|
|
117
|
+
const cycles: string[][] = [];
|
|
118
|
+
const visited = new Set<string>();
|
|
119
|
+
const recStack = new Set<string>();
|
|
120
|
+
|
|
121
|
+
const dfs = (node: string, path: string[]): void => {
|
|
122
|
+
if (recStack.has(node)) {
|
|
123
|
+
// Found a cycle
|
|
124
|
+
const cycleStart = path.indexOf(node);
|
|
125
|
+
if (cycleStart !== -1) {
|
|
126
|
+
cycles.push(path.slice(cycleStart).concat([node]));
|
|
127
|
+
}
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (visited.has(node)) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
visited.add(node);
|
|
136
|
+
recStack.add(node);
|
|
137
|
+
|
|
138
|
+
const dependencies = this.getDependencies(node);
|
|
139
|
+
for (const dep of dependencies) {
|
|
140
|
+
if (remainingNodes.includes(dep)) {
|
|
141
|
+
dfs(dep, [...path, node]);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
recStack.delete(node);
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
for (const node of remainingNodes) {
|
|
149
|
+
if (!visited.has(node)) {
|
|
150
|
+
dfs(node, []);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return cycles;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Get build batches - packages that can be built in parallel
|
|
159
|
+
*/
|
|
160
|
+
getBuildBatches(): string[][] {
|
|
161
|
+
const { order, cycles } = this.topologicalSort();
|
|
162
|
+
|
|
163
|
+
if (cycles.length > 0) {
|
|
164
|
+
throw new Error(
|
|
165
|
+
`Circular dependencies detected: ${cycles.map(cycle => cycle.join(' -> ')).join(', ')}`
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const batches: string[][] = [];
|
|
170
|
+
const processed = new Set<string>();
|
|
171
|
+
|
|
172
|
+
while (processed.size < order.length) {
|
|
173
|
+
const currentBatch: string[] = [];
|
|
174
|
+
|
|
175
|
+
for (const packageName of order) {
|
|
176
|
+
if (processed.has(packageName)) {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Check if all dependencies are already processed
|
|
181
|
+
const dependencies = this.getDependencies(packageName);
|
|
182
|
+
const allDepsProcessed = dependencies.every(dep => processed.has(dep));
|
|
183
|
+
|
|
184
|
+
if (allDepsProcessed) {
|
|
185
|
+
currentBatch.push(packageName);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (currentBatch.length === 0) {
|
|
190
|
+
// This shouldn't happen if topological sort worked correctly
|
|
191
|
+
throw new Error('Unable to determine build order - possible circular dependency');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
batches.push(currentBatch);
|
|
195
|
+
currentBatch.forEach(pkg => processed.add(pkg));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return batches;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Filter the graph to only include specified packages
|
|
203
|
+
*/
|
|
204
|
+
filterGraph(packageNames: string[]): DependencyGraph {
|
|
205
|
+
const filteredGraph = new DependencyGraph();
|
|
206
|
+
const packageSet = new Set(packageNames);
|
|
207
|
+
|
|
208
|
+
// Add all specified packages
|
|
209
|
+
for (const packageName of packageNames) {
|
|
210
|
+
if (this.nodes.has(packageName)) {
|
|
211
|
+
filteredGraph.addPackage(packageName);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Add dependencies between filtered packages
|
|
216
|
+
for (const packageName of packageNames) {
|
|
217
|
+
if (this.nodes.has(packageName)) {
|
|
218
|
+
const dependencies = this.getDependencies(packageName);
|
|
219
|
+
for (const dep of dependencies) {
|
|
220
|
+
if (packageSet.has(dep)) {
|
|
221
|
+
filteredGraph.addDependency(packageName, dep);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return filteredGraph;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Get packages that have no dependencies (root packages)
|
|
232
|
+
*/
|
|
233
|
+
getRootPackages(): string[] {
|
|
234
|
+
return this.getPackages().filter(pkg => this.getDependencies(pkg).length === 0);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Get packages that have no dependents (leaf packages)
|
|
239
|
+
*/
|
|
240
|
+
getLeafPackages(): string[] {
|
|
241
|
+
return this.getPackages().filter(pkg => this.getDependents(pkg).length === 0);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Print the dependency graph for debugging
|
|
246
|
+
*/
|
|
247
|
+
printGraph(): void {
|
|
248
|
+
console.log('\n📊 Dependency Graph:');
|
|
249
|
+
for (const [packageName, node] of this.nodes) {
|
|
250
|
+
const deps = Array.from(node.dependencies);
|
|
251
|
+
const dependents = Array.from(node.dependents);
|
|
252
|
+
|
|
253
|
+
console.log(`\n📦 ${packageName}`);
|
|
254
|
+
if (deps.length > 0) {
|
|
255
|
+
console.log(` ⬇️ Dependencies: ${deps.join(', ')}`);
|
|
256
|
+
}
|
|
257
|
+
if (dependents.length > 0) {
|
|
258
|
+
console.log(` ⬆️ Dependents: ${dependents.join(', ')}`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|