workspace-utils 1.0.0 → 1.0.2
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/dist/index.js +15464 -0
- package/dist/package.json +57 -0
- package/package.json +4 -1
- package/.github/workflows/mdbook.yml +0 -64
- package/.prettierignore +0 -22
- package/.prettierrc +0 -13
- package/docs/book.toml +0 -10
- package/docs/src/SUMMARY.md +0 -24
- package/docs/src/commands/build.md +0 -110
- package/docs/src/commands/dev.md +0 -118
- package/docs/src/commands/overview.md +0 -239
- package/docs/src/commands/run.md +0 -153
- package/docs/src/configuration.md +0 -249
- package/docs/src/examples.md +0 -567
- package/docs/src/installation.md +0 -148
- package/docs/src/introduction.md +0 -117
- package/docs/src/quick-start.md +0 -278
- package/docs/src/troubleshooting.md +0 -533
- package/index.ts +0 -84
- package/src/commands/build.ts +0 -158
- package/src/commands/dev.ts +0 -192
- package/src/commands/run.test.ts +0 -329
- package/src/commands/run.ts +0 -118
- package/src/core/dependency-graph.ts +0 -262
- package/src/core/process-runner.ts +0 -355
- package/src/core/workspace.test.ts +0 -404
- package/src/core/workspace.ts +0 -228
- package/src/package-managers/bun.test.ts +0 -209
- package/src/package-managers/bun.ts +0 -79
- package/src/package-managers/detector.test.ts +0 -199
- package/src/package-managers/detector.ts +0 -111
- package/src/package-managers/index.ts +0 -10
- package/src/package-managers/npm.ts +0 -79
- package/src/package-managers/pnpm.ts +0 -101
- package/src/package-managers/types.ts +0 -42
- package/src/utils/output.ts +0 -301
- package/src/utils/package-utils.ts +0 -243
- package/tests/bun-workspace/apps/web-app/package.json +0 -18
- package/tests/bun-workspace/bun.lockb +0 -0
- package/tests/bun-workspace/package.json +0 -18
- package/tests/bun-workspace/packages/shared-utils/package.json +0 -15
- package/tests/bun-workspace/packages/ui-components/package.json +0 -17
- package/tests/npm-workspace/package-lock.json +0 -0
- package/tests/npm-workspace/package.json +0 -18
- package/tests/npm-workspace/packages/core/package.json +0 -15
- package/tests/pnpm-workspace/package.json +0 -14
- package/tests/pnpm-workspace/packages/utils/package.json +0 -15
- package/tests/pnpm-workspace/pnpm-workspace.yaml +0 -3
- package/tsconfig.json +0 -29
package/src/commands/build.ts
DELETED
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
import pc from 'picocolors';
|
|
2
|
-
import { WorkspaceParser } from '../core/workspace.ts';
|
|
3
|
-
import {
|
|
4
|
-
buildDependencyGraph,
|
|
5
|
-
validatePackagesHaveScript,
|
|
6
|
-
prepareCommandExecution,
|
|
7
|
-
} from '../utils/package-utils.ts';
|
|
8
|
-
import { ProcessRunner } from '../core/process-runner.ts';
|
|
9
|
-
import { Output } from '../utils/output.ts';
|
|
10
|
-
|
|
11
|
-
interface BuildCommandOptions {
|
|
12
|
-
filter?: string;
|
|
13
|
-
concurrency?: string;
|
|
14
|
-
skipUnchanged?: boolean;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export async function buildCommand(options: BuildCommandOptions): Promise<void> {
|
|
18
|
-
try {
|
|
19
|
-
Output.build('Building packages in dependency order...\n');
|
|
20
|
-
|
|
21
|
-
// Parse workspace
|
|
22
|
-
const parser = new WorkspaceParser();
|
|
23
|
-
const workspace = await parser.parseWorkspace();
|
|
24
|
-
|
|
25
|
-
Output.dim(`Workspace root: ${workspace.root}`, 'folder');
|
|
26
|
-
Output.dim(`Found ${workspace.packages.length} packages\n`, 'package');
|
|
27
|
-
|
|
28
|
-
// Filter packages if pattern provided
|
|
29
|
-
let targetPackages = workspace.packages;
|
|
30
|
-
if (options.filter) {
|
|
31
|
-
targetPackages = parser.filterPackages(workspace.packages, options.filter);
|
|
32
|
-
Output.log(
|
|
33
|
-
`Filtered to ${targetPackages.length} packages matching "${options.filter}"`,
|
|
34
|
-
'magnifying',
|
|
35
|
-
'yellow'
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Validate packages have the build script
|
|
40
|
-
const { valid: packagesWithBuild, invalid: packagesWithoutBuild } = validatePackagesHaveScript(
|
|
41
|
-
targetPackages,
|
|
42
|
-
'build'
|
|
43
|
-
);
|
|
44
|
-
|
|
45
|
-
if (packagesWithoutBuild.length > 0) {
|
|
46
|
-
Output.warning(`The following packages don't have a "build" script:`);
|
|
47
|
-
packagesWithoutBuild.forEach(pkg => {
|
|
48
|
-
Output.listItem(pkg.name);
|
|
49
|
-
});
|
|
50
|
-
console.log();
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (packagesWithBuild.length === 0) {
|
|
54
|
-
Output.error('No packages found with a "build" script.');
|
|
55
|
-
process.exit(1);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Build dependency graph
|
|
59
|
-
Output.log('Building dependency graph...', 'chart', 'blue');
|
|
60
|
-
const dependencyGraph = buildDependencyGraph(packagesWithBuild);
|
|
61
|
-
|
|
62
|
-
// Filter graph to only include packages that need to be built
|
|
63
|
-
const packageNames = packagesWithBuild.map(pkg => pkg.name);
|
|
64
|
-
const filteredGraph = dependencyGraph.filterGraph(packageNames);
|
|
65
|
-
|
|
66
|
-
// Get build batches (topological order)
|
|
67
|
-
let buildBatches;
|
|
68
|
-
try {
|
|
69
|
-
buildBatches = filteredGraph.getBuildBatches();
|
|
70
|
-
} catch (error) {
|
|
71
|
-
Output.error(
|
|
72
|
-
`Dependency cycle detected: ${error instanceof Error ? error.message : String(error)}`
|
|
73
|
-
);
|
|
74
|
-
Output.tip('Check for circular dependencies between packages.');
|
|
75
|
-
process.exit(1);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
Output.success(`Build order determined: ${buildBatches.length} batches`);
|
|
79
|
-
|
|
80
|
-
// Display build plan
|
|
81
|
-
console.log(pc.blue(`\n${Output.getSymbol('books')} Build Plan:`));
|
|
82
|
-
buildBatches.forEach((batch, index) => {
|
|
83
|
-
Output.listItem(`Batch ${index + 1}: ${batch.join(', ')}`);
|
|
84
|
-
});
|
|
85
|
-
console.log();
|
|
86
|
-
|
|
87
|
-
const concurrency = parseInt(options.concurrency || '4', 10);
|
|
88
|
-
|
|
89
|
-
Output.log(`Package manager: ${workspace.packageManager.name}`, 'wrench', 'blue');
|
|
90
|
-
Output.log(`Batch concurrency: ${concurrency}`, 'lightning', 'blue');
|
|
91
|
-
console.log();
|
|
92
|
-
|
|
93
|
-
// Prepare commands organized by batches
|
|
94
|
-
const packageMap = new Map(packagesWithBuild.map(pkg => [pkg.name, pkg]));
|
|
95
|
-
const commandBatches = buildBatches.map(batch => {
|
|
96
|
-
return batch
|
|
97
|
-
.map(packageName => packageMap.get(packageName))
|
|
98
|
-
.filter((pkg): pkg is NonNullable<typeof pkg> => pkg !== undefined)
|
|
99
|
-
.map(pkg => {
|
|
100
|
-
const commands = prepareCommandExecution([pkg], 'build', workspace.packageManager);
|
|
101
|
-
return commands[0];
|
|
102
|
-
})
|
|
103
|
-
.filter((cmd): cmd is NonNullable<typeof cmd> => cmd !== undefined);
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
// Execute builds in batches
|
|
107
|
-
const startTime = Date.now();
|
|
108
|
-
const allResults = await ProcessRunner.runBatches(commandBatches, concurrency);
|
|
109
|
-
const totalDuration = Date.now() - startTime;
|
|
110
|
-
|
|
111
|
-
// Print final summary
|
|
112
|
-
const successful = allResults.filter(r => r.success);
|
|
113
|
-
const failed = allResults.filter(r => !r.success);
|
|
114
|
-
|
|
115
|
-
Output.buildSummary(successful.length, failed.length, totalDuration);
|
|
116
|
-
|
|
117
|
-
if (failed.length > 0) {
|
|
118
|
-
console.log(pc.red('\nFailed packages:'));
|
|
119
|
-
failed.forEach(f => {
|
|
120
|
-
Output.listItem(`${f.packageName} (exit code ${f.exitCode})`);
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
if (successful.length > 0) {
|
|
125
|
-
const avgDuration = Math.round(
|
|
126
|
-
successful.reduce((sum, r) => sum + r.duration, 0) / successful.length
|
|
127
|
-
);
|
|
128
|
-
Output.dim(`Average package build time: ${Output.formatDuration(avgDuration)}`, 'chart');
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Show dependency chain info
|
|
132
|
-
const rootPackages = filteredGraph.getRootPackages();
|
|
133
|
-
const leafPackages = filteredGraph.getLeafPackages();
|
|
134
|
-
|
|
135
|
-
if (rootPackages.length > 0) {
|
|
136
|
-
Output.dim(`Root packages (no dependencies): ${rootPackages.join(', ')}`, 'seedling');
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
if (leafPackages.length > 0) {
|
|
140
|
-
Output.dim(`Leaf packages (no dependents): ${leafPackages.join(', ')}`, 'leaf');
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// Exit with error code if any builds failed
|
|
144
|
-
if (failed.length > 0) {
|
|
145
|
-
Output.log('\nBuild failed due to package failures.', 'fire', 'red');
|
|
146
|
-
process.exit(1);
|
|
147
|
-
} else {
|
|
148
|
-
Output.celebrate('\nAll packages built successfully!');
|
|
149
|
-
}
|
|
150
|
-
} catch (error) {
|
|
151
|
-
Output.log(
|
|
152
|
-
`Build error: ${error instanceof Error ? error.message : String(error)}`,
|
|
153
|
-
'fire',
|
|
154
|
-
'red'
|
|
155
|
-
);
|
|
156
|
-
process.exit(1);
|
|
157
|
-
}
|
|
158
|
-
}
|
package/src/commands/dev.ts
DELETED
|
@@ -1,192 +0,0 @@
|
|
|
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 DevCommandOptions {
|
|
8
|
-
filter?: string;
|
|
9
|
-
concurrency?: string;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export async function devCommand(options: DevCommandOptions): Promise<void> {
|
|
13
|
-
try {
|
|
14
|
-
Output.dev('Starting development servers with live log streaming...\n');
|
|
15
|
-
|
|
16
|
-
// Parse workspace
|
|
17
|
-
const parser = new WorkspaceParser();
|
|
18
|
-
const workspace = await parser.parseWorkspace();
|
|
19
|
-
|
|
20
|
-
Output.dim(`Workspace root: ${workspace.root}`, 'folder');
|
|
21
|
-
Output.dim(`Found ${workspace.packages.length} packages\n`, 'package');
|
|
22
|
-
|
|
23
|
-
// Filter packages if pattern provided
|
|
24
|
-
let targetPackages = workspace.packages;
|
|
25
|
-
if (options.filter) {
|
|
26
|
-
targetPackages = parser.filterPackages(workspace.packages, options.filter);
|
|
27
|
-
Output.log(
|
|
28
|
-
`Filtered to ${targetPackages.length} packages matching "${options.filter}"`,
|
|
29
|
-
'magnifying',
|
|
30
|
-
'yellow'
|
|
31
|
-
);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// Validate packages have the dev script
|
|
35
|
-
const { valid: packagesWithDev, invalid: packagesWithoutDev } = validatePackagesHaveScript(
|
|
36
|
-
targetPackages,
|
|
37
|
-
'dev'
|
|
38
|
-
);
|
|
39
|
-
|
|
40
|
-
if (packagesWithoutDev.length > 0) {
|
|
41
|
-
Output.warning(`The following packages don't have a "dev" script:`);
|
|
42
|
-
packagesWithoutDev.forEach(pkg => {
|
|
43
|
-
Output.listItem(pkg.name);
|
|
44
|
-
});
|
|
45
|
-
console.log();
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if (packagesWithDev.length === 0) {
|
|
49
|
-
Output.error('No packages found with a "dev" script.');
|
|
50
|
-
process.exit(1);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
Output.success(`Starting dev servers for ${packagesWithDev.length} packages:`);
|
|
54
|
-
packagesWithDev.forEach(pkg => {
|
|
55
|
-
const color = ProcessRunner.getPackageColor(pkg.name);
|
|
56
|
-
const getColorFn = (colorName: string) => {
|
|
57
|
-
switch (colorName) {
|
|
58
|
-
case 'red':
|
|
59
|
-
return pc.red;
|
|
60
|
-
case 'green':
|
|
61
|
-
return pc.green;
|
|
62
|
-
case 'yellow':
|
|
63
|
-
return pc.yellow;
|
|
64
|
-
case 'blue':
|
|
65
|
-
return pc.blue;
|
|
66
|
-
case 'magenta':
|
|
67
|
-
return pc.magenta;
|
|
68
|
-
case 'cyan':
|
|
69
|
-
return pc.cyan;
|
|
70
|
-
case 'gray':
|
|
71
|
-
return pc.gray;
|
|
72
|
-
case 'redBright':
|
|
73
|
-
return pc.redBright;
|
|
74
|
-
case 'greenBright':
|
|
75
|
-
return pc.greenBright;
|
|
76
|
-
case 'yellowBright':
|
|
77
|
-
return pc.yellowBright;
|
|
78
|
-
case 'blueBright':
|
|
79
|
-
return pc.blueBright;
|
|
80
|
-
case 'magentaBright':
|
|
81
|
-
return pc.magentaBright;
|
|
82
|
-
case 'cyanBright':
|
|
83
|
-
return pc.cyanBright;
|
|
84
|
-
default:
|
|
85
|
-
return pc.white;
|
|
86
|
-
}
|
|
87
|
-
};
|
|
88
|
-
const colorFn = getColorFn(color);
|
|
89
|
-
console.log(` • ${colorFn(pkg.name)}`);
|
|
90
|
-
});
|
|
91
|
-
console.log();
|
|
92
|
-
|
|
93
|
-
const concurrency = parseInt(options.concurrency || '4', 10);
|
|
94
|
-
|
|
95
|
-
Output.log(`Package manager: ${workspace.packageManager.name}`, 'wrench', 'blue');
|
|
96
|
-
Output.log(
|
|
97
|
-
`Running ${Math.min(packagesWithDev.length, concurrency)} dev servers simultaneously`,
|
|
98
|
-
'lightning',
|
|
99
|
-
'blue'
|
|
100
|
-
);
|
|
101
|
-
Output.tip('Use Ctrl+C to stop all development servers\n');
|
|
102
|
-
|
|
103
|
-
// Prepare command execution with enhanced logging for dev mode
|
|
104
|
-
const commands = prepareCommandExecution(packagesWithDev, 'dev', workspace.packageManager).map(
|
|
105
|
-
cmd => ({
|
|
106
|
-
...cmd,
|
|
107
|
-
logOptions: {
|
|
108
|
-
...cmd.logOptions,
|
|
109
|
-
showTimestamp: false, // Disable timestamps for dev mode
|
|
110
|
-
},
|
|
111
|
-
})
|
|
112
|
-
);
|
|
113
|
-
|
|
114
|
-
// Set up graceful shutdown
|
|
115
|
-
let isShuttingDown = false;
|
|
116
|
-
const shutdown = () => {
|
|
117
|
-
if (isShuttingDown) return;
|
|
118
|
-
isShuttingDown = true;
|
|
119
|
-
|
|
120
|
-
Output.log('\n\nShutting down development servers...', 'warning', 'yellow');
|
|
121
|
-
Output.dim('This may take a moment to gracefully stop all processes.\n');
|
|
122
|
-
|
|
123
|
-
// Force exit after a timeout
|
|
124
|
-
setTimeout(() => {
|
|
125
|
-
Output.log('Timeout reached, forcing exit...', 'clock', 'red');
|
|
126
|
-
process.exit(0);
|
|
127
|
-
}, 5000);
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
process.on('SIGINT', shutdown);
|
|
131
|
-
process.on('SIGTERM', shutdown);
|
|
132
|
-
|
|
133
|
-
Output.log('Starting development servers...\n', 'movie', 'green');
|
|
134
|
-
Output.separator();
|
|
135
|
-
|
|
136
|
-
// Execute all dev commands in parallel (they're meant to run indefinitely)
|
|
137
|
-
const startTime = Date.now();
|
|
138
|
-
|
|
139
|
-
try {
|
|
140
|
-
// For dev mode, we don't wait for completion since these are long-running processes
|
|
141
|
-
const promises = commands.map(cmd =>
|
|
142
|
-
ProcessRunner.runCommand(cmd.command, cmd.args, cmd.options, cmd.logOptions).catch(
|
|
143
|
-
error => {
|
|
144
|
-
Output.log(`Error in ${cmd.logOptions.prefix}: ${error}`, 'fire', 'red');
|
|
145
|
-
return {
|
|
146
|
-
success: false,
|
|
147
|
-
exitCode: 1,
|
|
148
|
-
packageName: cmd.logOptions.prefix,
|
|
149
|
-
command: `${cmd.command} ${cmd.args.join(' ')}`,
|
|
150
|
-
duration: Date.now() - startTime,
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
)
|
|
154
|
-
);
|
|
155
|
-
|
|
156
|
-
// Wait for all processes to start
|
|
157
|
-
await Promise.allSettled(promises.slice(0, Math.min(concurrency, promises.length)));
|
|
158
|
-
|
|
159
|
-
// If we reach here, some processes may have exited unexpectedly
|
|
160
|
-
if (!isShuttingDown) {
|
|
161
|
-
Output.warning('\nSome development servers may have stopped unexpectedly.');
|
|
162
|
-
Output.dim('Check the logs above for any error messages.\n');
|
|
163
|
-
}
|
|
164
|
-
} catch (error) {
|
|
165
|
-
if (!isShuttingDown) {
|
|
166
|
-
Output.log(
|
|
167
|
-
`Development server error: ${error instanceof Error ? error.message : String(error)}`,
|
|
168
|
-
'fire',
|
|
169
|
-
'red'
|
|
170
|
-
);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const totalDuration = Date.now() - startTime;
|
|
175
|
-
|
|
176
|
-
if (!isShuttingDown) {
|
|
177
|
-
Output.log(
|
|
178
|
-
`\nDevelopment session lasted: ${Output.formatDuration(totalDuration)}`,
|
|
179
|
-
'chart',
|
|
180
|
-
'blue'
|
|
181
|
-
);
|
|
182
|
-
Output.success('All development servers have stopped.');
|
|
183
|
-
}
|
|
184
|
-
} catch (error) {
|
|
185
|
-
Output.log(
|
|
186
|
-
`Dev command error: ${error instanceof Error ? error.message : String(error)}`,
|
|
187
|
-
'fire',
|
|
188
|
-
'red'
|
|
189
|
-
);
|
|
190
|
-
process.exit(1);
|
|
191
|
-
}
|
|
192
|
-
}
|
package/src/commands/run.test.ts
DELETED
|
@@ -1,329 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
|
|
2
|
-
import { mkdirSync, writeFileSync, rmSync, existsSync } from 'fs';
|
|
3
|
-
import { join } from 'path';
|
|
4
|
-
import { runCommand } from './run.ts';
|
|
5
|
-
import { spyOn } from 'bun:test';
|
|
6
|
-
|
|
7
|
-
describe('runCommand', () => {
|
|
8
|
-
const testDir = join(process.cwd(), 'test-temp-run');
|
|
9
|
-
|
|
10
|
-
beforeEach(() => {
|
|
11
|
-
// Clean up test directory if it exists
|
|
12
|
-
if (existsSync(testDir)) {
|
|
13
|
-
rmSync(testDir, { recursive: true, force: true });
|
|
14
|
-
}
|
|
15
|
-
mkdirSync(testDir, { recursive: true });
|
|
16
|
-
|
|
17
|
-
// Change to test directory
|
|
18
|
-
process.chdir(testDir);
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
afterEach(() => {
|
|
22
|
-
// Change back to original directory
|
|
23
|
-
process.chdir(process.cwd().replace('/test-temp-run', ''));
|
|
24
|
-
|
|
25
|
-
// Clean up test directory
|
|
26
|
-
if (existsSync(testDir)) {
|
|
27
|
-
rmSync(testDir, { recursive: true, force: true });
|
|
28
|
-
}
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
describe('Bun workspace', () => {
|
|
32
|
-
beforeEach(() => {
|
|
33
|
-
// Create Bun workspace
|
|
34
|
-
writeFileSync(join(testDir, 'bun.lockb'), '');
|
|
35
|
-
writeFileSync(
|
|
36
|
-
join(testDir, 'package.json'),
|
|
37
|
-
JSON.stringify({
|
|
38
|
-
name: 'test-workspace',
|
|
39
|
-
workspaces: ['packages/*'],
|
|
40
|
-
})
|
|
41
|
-
);
|
|
42
|
-
|
|
43
|
-
// Create test packages
|
|
44
|
-
mkdirSync(join(testDir, 'packages', 'pkg1'), { recursive: true });
|
|
45
|
-
writeFileSync(
|
|
46
|
-
join(testDir, 'packages', 'pkg1', 'package.json'),
|
|
47
|
-
JSON.stringify({
|
|
48
|
-
name: '@test/pkg1',
|
|
49
|
-
version: '1.0.0',
|
|
50
|
-
scripts: {
|
|
51
|
-
test: 'echo "Testing pkg1"',
|
|
52
|
-
build: 'echo "Building pkg1"',
|
|
53
|
-
lint: 'echo "Linting pkg1"',
|
|
54
|
-
},
|
|
55
|
-
})
|
|
56
|
-
);
|
|
57
|
-
|
|
58
|
-
mkdirSync(join(testDir, 'packages', 'pkg2'), { recursive: true });
|
|
59
|
-
writeFileSync(
|
|
60
|
-
join(testDir, 'packages', 'pkg2', 'package.json'),
|
|
61
|
-
JSON.stringify({
|
|
62
|
-
name: '@test/pkg2',
|
|
63
|
-
version: '1.0.0',
|
|
64
|
-
scripts: {
|
|
65
|
-
test: 'echo "Testing pkg2"',
|
|
66
|
-
build: 'echo "Building pkg2"',
|
|
67
|
-
},
|
|
68
|
-
})
|
|
69
|
-
);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it('should run test script in parallel by default', async () => {
|
|
73
|
-
const consoleSpy = spyOn(console, 'log');
|
|
74
|
-
const processExitSpy = spyOn(process, 'exit').mockImplementation(() => {
|
|
75
|
-
throw new Error('process.exit called');
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
const options = {};
|
|
79
|
-
|
|
80
|
-
try {
|
|
81
|
-
await runCommand('test', options);
|
|
82
|
-
} catch {
|
|
83
|
-
// Expected to throw due to process.exit mock
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('🔧 Package manager: bun'));
|
|
87
|
-
expect(consoleSpy).toHaveBeenCalledWith(
|
|
88
|
-
expect.stringContaining('⚡ Execution mode: parallel (concurrency: 4)')
|
|
89
|
-
);
|
|
90
|
-
|
|
91
|
-
consoleSpy.mockRestore();
|
|
92
|
-
processExitSpy.mockRestore();
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it('should run script sequentially when --sequential flag is used', async () => {
|
|
96
|
-
const consoleSpy = spyOn(console, 'log');
|
|
97
|
-
const processExitSpy = spyOn(process, 'exit').mockImplementation(() => {
|
|
98
|
-
throw new Error('process.exit called');
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
const options = { sequential: true };
|
|
102
|
-
|
|
103
|
-
try {
|
|
104
|
-
await runCommand('test', options);
|
|
105
|
-
} catch {
|
|
106
|
-
// Expected to throw due to process.exit mock
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
expect(consoleSpy).toHaveBeenCalledWith(
|
|
110
|
-
expect.stringContaining('⚡ Execution mode: sequential')
|
|
111
|
-
);
|
|
112
|
-
|
|
113
|
-
consoleSpy.mockRestore();
|
|
114
|
-
processExitSpy.mockRestore();
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it('should filter packages when filter option is provided', async () => {
|
|
118
|
-
const consoleSpy = spyOn(console, 'log');
|
|
119
|
-
const processExitSpy = spyOn(process, 'exit').mockImplementation(() => {
|
|
120
|
-
throw new Error('process.exit called');
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
const options = { filter: '*pkg1*' };
|
|
124
|
-
|
|
125
|
-
try {
|
|
126
|
-
await runCommand('test', options);
|
|
127
|
-
} catch {
|
|
128
|
-
// Expected to throw due to process.exit mock
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
expect(consoleSpy).toHaveBeenCalledWith(
|
|
132
|
-
expect.stringContaining('🔍 Filtered to 1 packages matching "*pkg1*"')
|
|
133
|
-
);
|
|
134
|
-
expect(consoleSpy).toHaveBeenCalledWith(
|
|
135
|
-
expect.stringContaining('✅ Running "test" in 1 packages:')
|
|
136
|
-
);
|
|
137
|
-
|
|
138
|
-
consoleSpy.mockRestore();
|
|
139
|
-
processExitSpy.mockRestore();
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
it('should handle packages without the specified script', async () => {
|
|
143
|
-
const consoleSpy = spyOn(console, 'log');
|
|
144
|
-
const processExitSpy = spyOn(process, 'exit').mockImplementation(() => {
|
|
145
|
-
throw new Error('process.exit called');
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
const options = {};
|
|
149
|
-
|
|
150
|
-
try {
|
|
151
|
-
await runCommand('lint', options);
|
|
152
|
-
} catch {
|
|
153
|
-
// Expected to throw due to process.exit mock
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
expect(consoleSpy).toHaveBeenCalledWith(
|
|
157
|
-
expect.stringContaining('⚠️ The following packages don\'t have the "lint" script:')
|
|
158
|
-
);
|
|
159
|
-
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('• @test/pkg2'));
|
|
160
|
-
|
|
161
|
-
consoleSpy.mockRestore();
|
|
162
|
-
processExitSpy.mockRestore();
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it('should exit with error when no packages have the script', async () => {
|
|
166
|
-
const consoleSpy = spyOn(console, 'log');
|
|
167
|
-
const processExitSpy = spyOn(process, 'exit').mockImplementation(() => {
|
|
168
|
-
throw new Error('process.exit called');
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
const options = {};
|
|
172
|
-
|
|
173
|
-
try {
|
|
174
|
-
await runCommand('nonexistent', options);
|
|
175
|
-
} catch (error) {
|
|
176
|
-
expect((error as Error).message).toBe('process.exit called');
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
expect(consoleSpy).toHaveBeenCalledWith(
|
|
180
|
-
expect.stringContaining('❌ No packages found with the "nonexistent" script.')
|
|
181
|
-
);
|
|
182
|
-
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
183
|
-
|
|
184
|
-
consoleSpy.mockRestore();
|
|
185
|
-
processExitSpy.mockRestore();
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
it('should use custom concurrency when specified', async () => {
|
|
189
|
-
const consoleSpy = spyOn(console, 'log');
|
|
190
|
-
const processExitSpy = spyOn(process, 'exit').mockImplementation(() => {
|
|
191
|
-
throw new Error('process.exit called');
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
const options = { concurrency: '8' };
|
|
195
|
-
|
|
196
|
-
try {
|
|
197
|
-
await runCommand('test', options);
|
|
198
|
-
} catch {
|
|
199
|
-
// Expected to throw due to process.exit mock
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
expect(consoleSpy).toHaveBeenCalledWith(
|
|
203
|
-
expect.stringContaining('⚡ Execution mode: parallel (concurrency: 8)')
|
|
204
|
-
);
|
|
205
|
-
|
|
206
|
-
consoleSpy.mockRestore();
|
|
207
|
-
processExitSpy.mockRestore();
|
|
208
|
-
});
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
describe('pnpm workspace', () => {
|
|
212
|
-
beforeEach(() => {
|
|
213
|
-
// Create pnpm workspace
|
|
214
|
-
writeFileSync(join(testDir, 'pnpm-lock.yaml'), 'lockfileVersion: 6.0');
|
|
215
|
-
writeFileSync(join(testDir, 'pnpm-workspace.yaml'), 'packages:\n - packages/*');
|
|
216
|
-
|
|
217
|
-
// Create test package
|
|
218
|
-
mkdirSync(join(testDir, 'packages', 'utils'), { recursive: true });
|
|
219
|
-
writeFileSync(
|
|
220
|
-
join(testDir, 'packages', 'utils', 'package.json'),
|
|
221
|
-
JSON.stringify({
|
|
222
|
-
name: '@test/utils',
|
|
223
|
-
version: '1.0.0',
|
|
224
|
-
scripts: {
|
|
225
|
-
test: 'echo "Testing utils with pnpm"',
|
|
226
|
-
},
|
|
227
|
-
})
|
|
228
|
-
);
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
it('should detect pnpm as package manager', async () => {
|
|
232
|
-
const consoleSpy = spyOn(console, 'log');
|
|
233
|
-
const processExitSpy = spyOn(process, 'exit').mockImplementation(() => {
|
|
234
|
-
throw new Error('process.exit called');
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
const options = {};
|
|
238
|
-
|
|
239
|
-
try {
|
|
240
|
-
await runCommand('test', options);
|
|
241
|
-
} catch {
|
|
242
|
-
// Expected to throw due to process.exit mock
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('🔧 Package manager: pnpm'));
|
|
246
|
-
|
|
247
|
-
consoleSpy.mockRestore();
|
|
248
|
-
processExitSpy.mockRestore();
|
|
249
|
-
});
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
describe('npm workspace', () => {
|
|
253
|
-
beforeEach(() => {
|
|
254
|
-
// Create npm workspace
|
|
255
|
-
writeFileSync(join(testDir, 'package-lock.json'), '{}');
|
|
256
|
-
writeFileSync(
|
|
257
|
-
join(testDir, 'package.json'),
|
|
258
|
-
JSON.stringify({
|
|
259
|
-
name: 'test-workspace',
|
|
260
|
-
workspaces: ['libs/*'],
|
|
261
|
-
})
|
|
262
|
-
);
|
|
263
|
-
|
|
264
|
-
// Create test package
|
|
265
|
-
mkdirSync(join(testDir, 'libs', 'core'), { recursive: true });
|
|
266
|
-
writeFileSync(
|
|
267
|
-
join(testDir, 'libs', 'core', 'package.json'),
|
|
268
|
-
JSON.stringify({
|
|
269
|
-
name: 'core',
|
|
270
|
-
version: '1.0.0',
|
|
271
|
-
scripts: {
|
|
272
|
-
test: 'echo "Testing core with npm"',
|
|
273
|
-
},
|
|
274
|
-
})
|
|
275
|
-
);
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
it('should detect npm as package manager', async () => {
|
|
279
|
-
const consoleSpy = spyOn(console, 'log');
|
|
280
|
-
const processExitSpy = spyOn(process, 'exit').mockImplementation(() => {
|
|
281
|
-
throw new Error('process.exit called');
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
const options = {};
|
|
285
|
-
|
|
286
|
-
try {
|
|
287
|
-
await runCommand('test', options);
|
|
288
|
-
} catch {
|
|
289
|
-
// Expected to throw due to process.exit mock
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('🔧 Package manager: npm'));
|
|
293
|
-
|
|
294
|
-
consoleSpy.mockRestore();
|
|
295
|
-
processExitSpy.mockRestore();
|
|
296
|
-
});
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
describe('error handling', () => {
|
|
300
|
-
it('should handle workspace parsing errors gracefully', async () => {
|
|
301
|
-
// Create invalid workspace (no workspaces config)
|
|
302
|
-
writeFileSync(
|
|
303
|
-
join(testDir, 'package.json'),
|
|
304
|
-
JSON.stringify({
|
|
305
|
-
name: 'invalid-workspace',
|
|
306
|
-
})
|
|
307
|
-
);
|
|
308
|
-
|
|
309
|
-
const consoleSpy = spyOn(console, 'error');
|
|
310
|
-
const processExitSpy = spyOn(process, 'exit').mockImplementation(() => {
|
|
311
|
-
throw new Error('process.exit called');
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
const options = {};
|
|
315
|
-
|
|
316
|
-
try {
|
|
317
|
-
await runCommand('test', options);
|
|
318
|
-
} catch (error) {
|
|
319
|
-
expect((error as Error).message).toBe('process.exit called');
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('💥 Error:'));
|
|
323
|
-
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
324
|
-
|
|
325
|
-
consoleSpy.mockRestore();
|
|
326
|
-
processExitSpy.mockRestore();
|
|
327
|
-
});
|
|
328
|
-
});
|
|
329
|
-
});
|