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,209 @@
|
|
|
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 { BunPackageManager } from './bun.ts';
|
|
5
|
+
|
|
6
|
+
describe('BunPackageManager', () => {
|
|
7
|
+
const testDir = join(process.cwd(), 'test-temp-bun');
|
|
8
|
+
let bunManager: BunPackageManager;
|
|
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
|
+
bunManager = new BunPackageManager();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
// Clean up test directory
|
|
21
|
+
if (existsSync(testDir)) {
|
|
22
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('name', () => {
|
|
27
|
+
it('should return "bun"', () => {
|
|
28
|
+
expect(bunManager.name).toBe('bun');
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('isActive', () => {
|
|
33
|
+
it('should return true when bun.lockb exists', () => {
|
|
34
|
+
writeFileSync(join(testDir, 'bun.lockb'), '');
|
|
35
|
+
|
|
36
|
+
expect(bunManager.isActive(testDir)).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should return true when bunfig.toml exists', () => {
|
|
40
|
+
writeFileSync(
|
|
41
|
+
join(testDir, 'bunfig.toml'),
|
|
42
|
+
'[install]\nregistry = "https://registry.npmjs.org/"'
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
expect(bunManager.isActive(testDir)).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should return true when package.json has bun field', () => {
|
|
49
|
+
writeFileSync(
|
|
50
|
+
join(testDir, 'package.json'),
|
|
51
|
+
JSON.stringify({
|
|
52
|
+
name: 'test',
|
|
53
|
+
bun: {
|
|
54
|
+
install: {
|
|
55
|
+
dev: true,
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
})
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
expect(bunManager.isActive(testDir)).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should return true when package.json has trustedDependencies field', () => {
|
|
65
|
+
writeFileSync(
|
|
66
|
+
join(testDir, 'package.json'),
|
|
67
|
+
JSON.stringify({
|
|
68
|
+
name: 'test',
|
|
69
|
+
trustedDependencies: ['some-package'],
|
|
70
|
+
})
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
expect(bunManager.isActive(testDir)).toBe(true);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should return false when no bun indicators exist', () => {
|
|
77
|
+
writeFileSync(
|
|
78
|
+
join(testDir, 'package.json'),
|
|
79
|
+
JSON.stringify({
|
|
80
|
+
name: 'test',
|
|
81
|
+
dependencies: {},
|
|
82
|
+
})
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
expect(bunManager.isActive(testDir)).toBe(false);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should return false when package.json is invalid', () => {
|
|
89
|
+
writeFileSync(join(testDir, 'package.json'), 'invalid json');
|
|
90
|
+
|
|
91
|
+
expect(bunManager.isActive(testDir)).toBe(false);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should return false when no files exist', () => {
|
|
95
|
+
expect(bunManager.isActive(testDir)).toBe(false);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe('getRunCommand', () => {
|
|
100
|
+
it('should return correct bun run command', () => {
|
|
101
|
+
const result = bunManager.getRunCommand('test');
|
|
102
|
+
|
|
103
|
+
expect(result).toEqual({
|
|
104
|
+
command: 'bun',
|
|
105
|
+
args: ['run', 'test'],
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should work with different script names', () => {
|
|
110
|
+
expect(bunManager.getRunCommand('build')).toEqual({
|
|
111
|
+
command: 'bun',
|
|
112
|
+
args: ['run', 'build'],
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
expect(bunManager.getRunCommand('dev')).toEqual({
|
|
116
|
+
command: 'bun',
|
|
117
|
+
args: ['run', 'dev'],
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe('parseWorkspaceConfig', () => {
|
|
123
|
+
it('should parse workspaces array from package.json', () => {
|
|
124
|
+
writeFileSync(
|
|
125
|
+
join(testDir, 'package.json'),
|
|
126
|
+
JSON.stringify({
|
|
127
|
+
name: 'test-workspace',
|
|
128
|
+
workspaces: ['packages/*', 'apps/*'],
|
|
129
|
+
})
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const result = bunManager.parseWorkspaceConfig(testDir);
|
|
133
|
+
|
|
134
|
+
expect(result.packages).toEqual(['packages/*', 'apps/*']);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should parse workspaces object from package.json', () => {
|
|
138
|
+
writeFileSync(
|
|
139
|
+
join(testDir, 'package.json'),
|
|
140
|
+
JSON.stringify({
|
|
141
|
+
name: 'test-workspace',
|
|
142
|
+
workspaces: {
|
|
143
|
+
packages: ['packages/*', 'tools/*'],
|
|
144
|
+
},
|
|
145
|
+
})
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
const result = bunManager.parseWorkspaceConfig(testDir);
|
|
149
|
+
|
|
150
|
+
expect(result.packages).toEqual(['packages/*', 'tools/*']);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('should throw error when package.json does not exist', () => {
|
|
154
|
+
expect(() => bunManager.parseWorkspaceConfig(testDir)).toThrow(
|
|
155
|
+
'No package.json found in workspace root'
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should throw error when no workspaces field exists', () => {
|
|
160
|
+
writeFileSync(
|
|
161
|
+
join(testDir, 'package.json'),
|
|
162
|
+
JSON.stringify({
|
|
163
|
+
name: 'test-workspace',
|
|
164
|
+
dependencies: {},
|
|
165
|
+
})
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
expect(() => bunManager.parseWorkspaceConfig(testDir)).toThrow(
|
|
169
|
+
'No workspaces configuration found in package.json'
|
|
170
|
+
);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('should throw error when workspaces is not an array or valid object', () => {
|
|
174
|
+
writeFileSync(
|
|
175
|
+
join(testDir, 'package.json'),
|
|
176
|
+
JSON.stringify({
|
|
177
|
+
name: 'test-workspace',
|
|
178
|
+
workspaces: 'invalid',
|
|
179
|
+
})
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
expect(() => bunManager.parseWorkspaceConfig(testDir)).toThrow(
|
|
183
|
+
'Invalid workspaces configuration: must be an array or object with packages array'
|
|
184
|
+
);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('should throw error when workspaces object lacks packages field', () => {
|
|
188
|
+
writeFileSync(
|
|
189
|
+
join(testDir, 'package.json'),
|
|
190
|
+
JSON.stringify({
|
|
191
|
+
name: 'test-workspace',
|
|
192
|
+
workspaces: {
|
|
193
|
+
notPackages: ['packages/*'],
|
|
194
|
+
},
|
|
195
|
+
})
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
expect(() => bunManager.parseWorkspaceConfig(testDir)).toThrow(
|
|
199
|
+
'Invalid workspaces configuration: must be an array or object with packages array'
|
|
200
|
+
);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
describe('getLockFileName', () => {
|
|
205
|
+
it('should return "bun.lockb"', () => {
|
|
206
|
+
expect(bunManager.getLockFileName()).toBe('bun.lockb');
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import type { PackageManager, WorkspaceConfig } from './types.ts';
|
|
4
|
+
|
|
5
|
+
export class BunPackageManager implements PackageManager {
|
|
6
|
+
readonly name = 'bun';
|
|
7
|
+
|
|
8
|
+
isActive(workspaceRoot: string): boolean {
|
|
9
|
+
// Check for bun.lockb file
|
|
10
|
+
const lockFile = join(workspaceRoot, 'bun.lockb');
|
|
11
|
+
if (existsSync(lockFile)) {
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Check for bunfig.toml
|
|
16
|
+
const bunConfig = join(workspaceRoot, 'bunfig.toml');
|
|
17
|
+
if (existsSync(bunConfig)) {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Check if package.json has bun-specific fields
|
|
22
|
+
const packageJsonPath = join(workspaceRoot, 'package.json');
|
|
23
|
+
if (existsSync(packageJsonPath)) {
|
|
24
|
+
try {
|
|
25
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')) as Record<
|
|
26
|
+
string,
|
|
27
|
+
unknown
|
|
28
|
+
>;
|
|
29
|
+
// Check for bun-specific fields
|
|
30
|
+
if (packageJson.bun || packageJson.trustedDependencies) {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
} catch {
|
|
34
|
+
// Ignore JSON parse errors
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
getRunCommand(scriptName: string): { command: string; args: string[] } {
|
|
42
|
+
return {
|
|
43
|
+
command: 'bun',
|
|
44
|
+
args: ['run', scriptName],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
parseWorkspaceConfig(workspaceRoot: string): WorkspaceConfig {
|
|
49
|
+
const packageJsonPath = join(workspaceRoot, 'package.json');
|
|
50
|
+
|
|
51
|
+
if (!existsSync(packageJsonPath)) {
|
|
52
|
+
throw new Error('No package.json found in workspace root');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')) as Record<
|
|
56
|
+
string,
|
|
57
|
+
unknown
|
|
58
|
+
>;
|
|
59
|
+
|
|
60
|
+
if (!packageJson.workspaces) {
|
|
61
|
+
throw new Error('No workspaces configuration found in package.json');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const workspaces = packageJson.workspaces as string[] | { packages: string[] };
|
|
65
|
+
const packages = Array.isArray(workspaces) ? workspaces : workspaces.packages;
|
|
66
|
+
|
|
67
|
+
if (!Array.isArray(packages)) {
|
|
68
|
+
throw new Error(
|
|
69
|
+
'Invalid workspaces configuration: must be an array or object with packages array'
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return { packages };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
getLockFileName(): string {
|
|
77
|
+
return 'bun.lockb';
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
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 { PackageManagerDetector } from './detector.ts';
|
|
5
|
+
import { BunPackageManager } from './bun.ts';
|
|
6
|
+
import { PnpmPackageManager } from './pnpm.ts';
|
|
7
|
+
import { NpmPackageManager } from './npm.ts';
|
|
8
|
+
|
|
9
|
+
describe('PackageManagerDetector', () => {
|
|
10
|
+
const testDir = join(process.cwd(), 'test-temp');
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
// Clean up test directory if it exists
|
|
14
|
+
if (existsSync(testDir)) {
|
|
15
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
16
|
+
}
|
|
17
|
+
mkdirSync(testDir, { recursive: true });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
// Clean up test directory
|
|
22
|
+
if (existsSync(testDir)) {
|
|
23
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('detect', () => {
|
|
28
|
+
it('should detect Bun when bun.lockb exists', () => {
|
|
29
|
+
// Create bun.lockb file
|
|
30
|
+
writeFileSync(join(testDir, 'bun.lockb'), '');
|
|
31
|
+
writeFileSync(
|
|
32
|
+
join(testDir, 'package.json'),
|
|
33
|
+
JSON.stringify({
|
|
34
|
+
name: 'test',
|
|
35
|
+
workspaces: ['packages/*'],
|
|
36
|
+
})
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const result = PackageManagerDetector.detect(testDir);
|
|
40
|
+
|
|
41
|
+
expect(result.packageManager).toBeInstanceOf(BunPackageManager);
|
|
42
|
+
expect(result.confidence).toBeGreaterThan(100);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should detect pnpm when pnpm-lock.yaml exists', () => {
|
|
46
|
+
// Create pnpm-lock.yaml file
|
|
47
|
+
writeFileSync(join(testDir, 'pnpm-lock.yaml'), 'lockfileVersion: 6.0');
|
|
48
|
+
writeFileSync(
|
|
49
|
+
join(testDir, 'package.json'),
|
|
50
|
+
JSON.stringify({
|
|
51
|
+
name: 'test',
|
|
52
|
+
workspaces: ['packages/*'],
|
|
53
|
+
})
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const result = PackageManagerDetector.detect(testDir);
|
|
57
|
+
|
|
58
|
+
expect(result.packageManager).toBeInstanceOf(PnpmPackageManager);
|
|
59
|
+
expect(result.confidence).toBeGreaterThan(100);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should detect npm when package-lock.json exists', () => {
|
|
63
|
+
// Create package-lock.json file
|
|
64
|
+
writeFileSync(
|
|
65
|
+
join(testDir, 'package-lock.json'),
|
|
66
|
+
JSON.stringify({
|
|
67
|
+
name: 'test',
|
|
68
|
+
lockfileVersion: 2,
|
|
69
|
+
})
|
|
70
|
+
);
|
|
71
|
+
writeFileSync(
|
|
72
|
+
join(testDir, 'package.json'),
|
|
73
|
+
JSON.stringify({
|
|
74
|
+
name: 'test',
|
|
75
|
+
workspaces: ['packages/*'],
|
|
76
|
+
})
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const result = PackageManagerDetector.detect(testDir);
|
|
80
|
+
|
|
81
|
+
expect(result.packageManager).toBeInstanceOf(NpmPackageManager);
|
|
82
|
+
expect(result.confidence).toBeGreaterThan(100);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should prefer pnpm when pnpm-workspace.yaml exists', () => {
|
|
86
|
+
// Create both pnpm and npm indicators
|
|
87
|
+
writeFileSync(join(testDir, 'pnpm-lock.yaml'), 'lockfileVersion: 6.0');
|
|
88
|
+
writeFileSync(join(testDir, 'package-lock.json'), '{}');
|
|
89
|
+
writeFileSync(join(testDir, 'pnpm-workspace.yaml'), 'packages:\n - packages/*');
|
|
90
|
+
|
|
91
|
+
const result = PackageManagerDetector.detect(testDir);
|
|
92
|
+
|
|
93
|
+
expect(result.packageManager).toBeInstanceOf(PnpmPackageManager);
|
|
94
|
+
expect(result.confidence).toBeGreaterThan(150); // Lock file + workspace file
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should detect Bun with bunfig.toml', () => {
|
|
98
|
+
writeFileSync(
|
|
99
|
+
join(testDir, 'bunfig.toml'),
|
|
100
|
+
'[install]\nregistry = "https://registry.npmjs.org/"'
|
|
101
|
+
);
|
|
102
|
+
writeFileSync(
|
|
103
|
+
join(testDir, 'package.json'),
|
|
104
|
+
JSON.stringify({
|
|
105
|
+
name: 'test',
|
|
106
|
+
workspaces: ['packages/*'],
|
|
107
|
+
})
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const result = PackageManagerDetector.detect(testDir);
|
|
111
|
+
|
|
112
|
+
expect(result.packageManager).toBeInstanceOf(BunPackageManager);
|
|
113
|
+
expect(result.confidence).toBeGreaterThan(50);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should detect Bun with bun-specific package.json fields', () => {
|
|
117
|
+
writeFileSync(
|
|
118
|
+
join(testDir, 'package.json'),
|
|
119
|
+
JSON.stringify({
|
|
120
|
+
name: 'test',
|
|
121
|
+
workspaces: ['packages/*'],
|
|
122
|
+
bun: {
|
|
123
|
+
install: {
|
|
124
|
+
dev: true,
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
})
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const result = PackageManagerDetector.detect(testDir);
|
|
131
|
+
|
|
132
|
+
expect(result.packageManager).toBeInstanceOf(BunPackageManager);
|
|
133
|
+
expect(result.confidence).toBeGreaterThanOrEqual(20);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should throw error when no package manager is detected', () => {
|
|
137
|
+
expect(() => PackageManagerDetector.detect(testDir)).toThrow(
|
|
138
|
+
'No package manager detected. Please ensure you have a lock file (bun.lockb, pnpm-lock.yaml, or package-lock.json) or workspace configuration in your project.'
|
|
139
|
+
);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('should handle multiple package managers and choose highest confidence', () => {
|
|
143
|
+
// Create indicators for multiple package managers
|
|
144
|
+
writeFileSync(join(testDir, 'package-lock.json'), '{}'); // npm - confidence 100
|
|
145
|
+
writeFileSync(join(testDir, 'pnpm-lock.yaml'), 'lockfileVersion: 6.0'); // pnpm - confidence 100
|
|
146
|
+
writeFileSync(join(testDir, 'pnpm-workspace.yaml'), 'packages:\n - packages/*'); // pnpm +80
|
|
147
|
+
writeFileSync(
|
|
148
|
+
join(testDir, 'package.json'),
|
|
149
|
+
JSON.stringify({
|
|
150
|
+
name: 'test',
|
|
151
|
+
workspaces: ['packages/*'],
|
|
152
|
+
})
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
const result = PackageManagerDetector.detect(testDir);
|
|
156
|
+
|
|
157
|
+
// pnpm should win with 200 confidence (100 + 80 + 20)
|
|
158
|
+
expect(result.packageManager).toBeInstanceOf(PnpmPackageManager);
|
|
159
|
+
expect(result.confidence).toBe(200);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe('getPackageManager', () => {
|
|
164
|
+
it('should return correct package manager by name', () => {
|
|
165
|
+
expect(PackageManagerDetector.getPackageManager('bun')).toBeInstanceOf(BunPackageManager);
|
|
166
|
+
expect(PackageManagerDetector.getPackageManager('pnpm')).toBeInstanceOf(PnpmPackageManager);
|
|
167
|
+
expect(PackageManagerDetector.getPackageManager('npm')).toBeInstanceOf(NpmPackageManager);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('should throw error for unknown package manager', () => {
|
|
171
|
+
expect(() => PackageManagerDetector.getPackageManager('unknown')).toThrow(
|
|
172
|
+
'Unknown package manager: unknown'
|
|
173
|
+
);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe('getSupportedPackageManagers', () => {
|
|
178
|
+
it('should return all supported package managers', () => {
|
|
179
|
+
const managers = PackageManagerDetector.getSupportedPackageManagers();
|
|
180
|
+
|
|
181
|
+
expect(managers).toHaveLength(3);
|
|
182
|
+
expect(managers[0]).toBeInstanceOf(BunPackageManager);
|
|
183
|
+
expect(managers[1]).toBeInstanceOf(PnpmPackageManager);
|
|
184
|
+
expect(managers[2]).toBeInstanceOf(NpmPackageManager);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe('isPackageManagerAvailable', () => {
|
|
189
|
+
it('should check if bun is available', async () => {
|
|
190
|
+
const isAvailable = await PackageManagerDetector.isPackageManagerAvailable('bun');
|
|
191
|
+
expect(typeof isAvailable).toBe('boolean');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('should return false for non-existent package manager', async () => {
|
|
195
|
+
const isAvailable = await PackageManagerDetector.isPackageManagerAvailable('nonexistent-pm');
|
|
196
|
+
expect(isAvailable).toBe(false);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
});
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { existsSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import type { PackageManager, PackageManagerDetectionResult } from './types.ts';
|
|
4
|
+
import { BunPackageManager } from './bun.ts';
|
|
5
|
+
import { PnpmPackageManager } from './pnpm.ts';
|
|
6
|
+
import { NpmPackageManager } from './npm.ts';
|
|
7
|
+
|
|
8
|
+
export class PackageManagerDetector {
|
|
9
|
+
private static readonly packageManagers = [
|
|
10
|
+
new BunPackageManager(),
|
|
11
|
+
new PnpmPackageManager(),
|
|
12
|
+
new NpmPackageManager(),
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Detect the package manager being used in the workspace
|
|
17
|
+
*/
|
|
18
|
+
static detect(workspaceRoot: string): PackageManagerDetectionResult {
|
|
19
|
+
const results: PackageManagerDetectionResult[] = [];
|
|
20
|
+
|
|
21
|
+
for (const pm of this.packageManagers) {
|
|
22
|
+
if (pm.isActive(workspaceRoot)) {
|
|
23
|
+
const confidence = this.calculateConfidence(pm, workspaceRoot);
|
|
24
|
+
results.push({ packageManager: pm, confidence });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Sort by confidence and return the highest
|
|
29
|
+
results.sort((a, b) => b.confidence - a.confidence);
|
|
30
|
+
|
|
31
|
+
const [result] = results;
|
|
32
|
+
if (!result) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
'No package manager detected. Please ensure you have a lock file (bun.lockb, pnpm-lock.yaml, or package-lock.json) or workspace configuration in your project.'
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get a specific package manager by name
|
|
43
|
+
*/
|
|
44
|
+
static getPackageManager(name: string): PackageManager {
|
|
45
|
+
const pm = this.packageManagers.find(pm => pm.name === name);
|
|
46
|
+
if (!pm) {
|
|
47
|
+
throw new Error(`Unknown package manager: ${name}`);
|
|
48
|
+
}
|
|
49
|
+
return pm;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get all supported package managers
|
|
54
|
+
*/
|
|
55
|
+
static getSupportedPackageManagers(): PackageManager[] {
|
|
56
|
+
return [...this.packageManagers];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Calculate confidence score for a package manager
|
|
61
|
+
*/
|
|
62
|
+
private static calculateConfidence(pm: PackageManager, workspaceRoot: string): number {
|
|
63
|
+
let confidence = 0;
|
|
64
|
+
|
|
65
|
+
// Check for lock file (highest confidence)
|
|
66
|
+
const lockFile = join(workspaceRoot, pm.getLockFileName());
|
|
67
|
+
if (existsSync(lockFile)) {
|
|
68
|
+
confidence += 100;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Check for package manager specific files
|
|
72
|
+
switch (pm.name) {
|
|
73
|
+
case 'bun':
|
|
74
|
+
if (existsSync(join(workspaceRoot, 'bunfig.toml'))) confidence += 50;
|
|
75
|
+
break;
|
|
76
|
+
case 'pnpm':
|
|
77
|
+
if (existsSync(join(workspaceRoot, 'pnpm-workspace.yaml'))) confidence += 80;
|
|
78
|
+
if (existsSync(join(workspaceRoot, '.pnpmfile.cjs'))) confidence += 30;
|
|
79
|
+
break;
|
|
80
|
+
case 'npm':
|
|
81
|
+
if (existsSync(join(workspaceRoot, '.npmrc'))) confidence += 30;
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Check for workspace configuration
|
|
86
|
+
try {
|
|
87
|
+
pm.parseWorkspaceConfig(workspaceRoot);
|
|
88
|
+
confidence += 20;
|
|
89
|
+
} catch {
|
|
90
|
+
// Workspace config not found or invalid
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return confidence;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Check if a specific package manager is available in the system
|
|
98
|
+
*/
|
|
99
|
+
static async isPackageManagerAvailable(name: string): Promise<boolean> {
|
|
100
|
+
try {
|
|
101
|
+
const { spawn } = await import('child_process');
|
|
102
|
+
return new Promise(resolve => {
|
|
103
|
+
const child = spawn(name, ['--version'], { stdio: 'ignore' });
|
|
104
|
+
child.on('close', code => resolve(code === 0));
|
|
105
|
+
child.on('error', () => resolve(false));
|
|
106
|
+
});
|
|
107
|
+
} catch {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { BunPackageManager } from './bun.ts';
|
|
2
|
+
export { PnpmPackageManager } from './pnpm.ts';
|
|
3
|
+
export { NpmPackageManager } from './npm.ts';
|
|
4
|
+
export { PackageManagerDetector } from './detector.ts';
|
|
5
|
+
export type {
|
|
6
|
+
PackageManager,
|
|
7
|
+
PackageManagerConfig,
|
|
8
|
+
WorkspaceConfig,
|
|
9
|
+
PackageManagerDetectionResult,
|
|
10
|
+
} from './types.ts';
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import type { PackageManager, WorkspaceConfig } from './types.ts';
|
|
4
|
+
|
|
5
|
+
export class NpmPackageManager implements PackageManager {
|
|
6
|
+
readonly name = 'npm';
|
|
7
|
+
|
|
8
|
+
isActive(workspaceRoot: string): boolean {
|
|
9
|
+
// Check for package-lock.json file
|
|
10
|
+
const lockFile = join(workspaceRoot, 'package-lock.json');
|
|
11
|
+
if (existsSync(lockFile)) {
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Check for .npmrc file
|
|
16
|
+
const npmrcFile = join(workspaceRoot, '.npmrc');
|
|
17
|
+
if (existsSync(npmrcFile)) {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Check if package.json has npm-specific fields or workspaces
|
|
22
|
+
const packageJsonPath = join(workspaceRoot, 'package.json');
|
|
23
|
+
if (existsSync(packageJsonPath)) {
|
|
24
|
+
try {
|
|
25
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')) as Record<
|
|
26
|
+
string,
|
|
27
|
+
unknown
|
|
28
|
+
>;
|
|
29
|
+
// Check for npm-specific fields or workspaces
|
|
30
|
+
if (packageJson.workspaces || packageJson.publishConfig) {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
} catch {
|
|
34
|
+
// Ignore JSON parse errors
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
getRunCommand(scriptName: string): { command: string; args: string[] } {
|
|
42
|
+
return {
|
|
43
|
+
command: 'npm',
|
|
44
|
+
args: ['run', scriptName],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
parseWorkspaceConfig(workspaceRoot: string): WorkspaceConfig {
|
|
49
|
+
const packageJsonPath = join(workspaceRoot, 'package.json');
|
|
50
|
+
|
|
51
|
+
if (!existsSync(packageJsonPath)) {
|
|
52
|
+
throw new Error('No package.json found in workspace root');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')) as Record<
|
|
56
|
+
string,
|
|
57
|
+
unknown
|
|
58
|
+
>;
|
|
59
|
+
|
|
60
|
+
if (!packageJson.workspaces) {
|
|
61
|
+
throw new Error('No workspaces configuration found in package.json');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const workspaces = packageJson.workspaces as string[] | { packages: string[] };
|
|
65
|
+
const packages = Array.isArray(workspaces) ? workspaces : workspaces.packages;
|
|
66
|
+
|
|
67
|
+
if (!Array.isArray(packages)) {
|
|
68
|
+
throw new Error(
|
|
69
|
+
'Invalid workspaces configuration: must be an array or object with packages array'
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return { packages };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
getLockFileName(): string {
|
|
77
|
+
return 'package-lock.json';
|
|
78
|
+
}
|
|
79
|
+
}
|