@shazhou/proman-core 0.9.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/CHANGELOG.md +26 -0
- package/LICENSE +18 -0
- package/dist/commands/bump.d.ts +13 -0
- package/dist/commands/bump.d.ts.map +1 -0
- package/dist/commands/bump.js +115 -0
- package/dist/commands/deploy.d.ts +9 -0
- package/dist/commands/deploy.d.ts.map +1 -0
- package/dist/commands/deploy.js +42 -0
- package/dist/commands/dev.d.ts +15 -0
- package/dist/commands/dev.d.ts.map +1 -0
- package/dist/commands/dev.js +175 -0
- package/dist/commands/index.d.ts +7 -0
- package/dist/commands/index.d.ts.map +1 -0
- package/dist/commands/index.js +7 -0
- package/dist/commands/init.d.ts +5 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +262 -0
- package/dist/commands/link.d.ts +19 -0
- package/dist/commands/link.d.ts.map +1 -0
- package/dist/commands/link.js +155 -0
- package/dist/commands/publish.d.ts +18 -0
- package/dist/commands/publish.d.ts.map +1 -0
- package/dist/commands/publish.js +125 -0
- package/dist/config/index.d.ts +4 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +2 -0
- package/dist/config/load-config.d.ts +6 -0
- package/dist/config/load-config.d.ts.map +1 -0
- package/dist/config/load-config.js +29 -0
- package/dist/config/types.d.ts +17 -0
- package/dist/config/types.d.ts.map +1 -0
- package/dist/config/types.js +1 -0
- package/dist/config/validate-config.d.ts +7 -0
- package/dist/config/validate-config.d.ts.map +1 -0
- package/dist/config/validate-config.js +72 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/utils/changeset.d.ts +16 -0
- package/dist/utils/changeset.d.ts.map +1 -0
- package/dist/utils/changeset.js +80 -0
- package/dist/utils/fingerprint.d.ts +38 -0
- package/dist/utils/fingerprint.d.ts.map +1 -0
- package/dist/utils/fingerprint.js +182 -0
- package/dist/utils/git.d.ts +23 -0
- package/dist/utils/git.d.ts.map +1 -0
- package/dist/utils/git.js +105 -0
- package/dist/utils/index.d.ts +8 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +8 -0
- package/dist/utils/npm.d.ts +30 -0
- package/dist/utils/npm.d.ts.map +1 -0
- package/dist/utils/npm.js +85 -0
- package/dist/utils/smoke-test.d.ts +7 -0
- package/dist/utils/smoke-test.d.ts.map +1 -0
- package/dist/utils/smoke-test.js +59 -0
- package/dist/utils/version.d.ts +5 -0
- package/dist/utils/version.d.ts.map +1 -0
- package/dist/utils/version.js +36 -0
- package/dist/utils/workspace.d.ts +21 -0
- package/dist/utils/workspace.d.ts.map +1 -0
- package/dist/utils/workspace.js +73 -0
- package/package.json +45 -0
- package/src/commands/bump.ts +131 -0
- package/src/commands/deploy.ts +52 -0
- package/src/commands/dev.ts +214 -0
- package/src/commands/index.ts +7 -0
- package/src/commands/init.integration.test.ts +59 -0
- package/src/commands/init.test.ts +179 -0
- package/src/commands/init.ts +290 -0
- package/src/commands/link.ts +195 -0
- package/src/commands/publish.ts +168 -0
- package/src/config/index.ts +8 -0
- package/src/config/load-config.ts +33 -0
- package/src/config/types.ts +19 -0
- package/src/config/validate-config.ts +81 -0
- package/src/index.ts +29 -0
- package/src/utils/changeset.ts +98 -0
- package/src/utils/fingerprint.ts +199 -0
- package/src/utils/git.ts +119 -0
- package/src/utils/index.ts +8 -0
- package/src/utils/npm.ts +110 -0
- package/src/utils/smoke-test.ts +79 -0
- package/src/utils/version.ts +41 -0
- package/src/utils/workspace.ts +94 -0
- package/tests/build-fingerprint-integration.test.ts +403 -0
- package/tests/bump.test.ts +261 -0
- package/tests/changeset.test.ts +147 -0
- package/tests/deploy.test.ts +98 -0
- package/tests/dev.test.ts +756 -0
- package/tests/fingerprint.test.ts +316 -0
- package/tests/fixtures/api-only/packages/api/.gitkeep +0 -0
- package/tests/fixtures/api-only/proman.yaml +4 -0
- package/tests/fixtures/bad-packages/proman.yaml +1 -0
- package/tests/fixtures/bun-project/packages/a/.gitkeep +0 -0
- package/tests/fixtures/bun-project/proman.yaml +4 -0
- package/tests/fixtures/defaults/proman.yaml +3 -0
- package/tests/fixtures/no-deployable/packages/core/.gitkeep +0 -0
- package/tests/fixtures/no-deployable/packages/mycli/.gitkeep +0 -0
- package/tests/fixtures/no-deployable/proman.yaml +7 -0
- package/tests/fixtures/node-runtime/packages/a/package.json +5 -0
- package/tests/fixtures/node-runtime/proman.yaml +3 -0
- package/tests/fixtures/pnpm-project/packages/a/package.json +1 -0
- package/tests/fixtures/pnpm-project/pnpm-lock.yaml +0 -0
- package/tests/fixtures/pnpm-project/proman.yaml +3 -0
- package/tests/fixtures/typed/packages/api/.gitkeep +0 -0
- package/tests/fixtures/typed/packages/core/.gitkeep +0 -0
- package/tests/fixtures/typed/packages/dashboard/.gitkeep +0 -0
- package/tests/fixtures/typed/packages/mycli/.gitkeep +0 -0
- package/tests/fixtures/typed/proman.yaml +13 -0
- package/tests/fixtures/valid/packages/cli/package.json +5 -0
- package/tests/fixtures/valid/packages/core/package.json +5 -0
- package/tests/fixtures/valid/packages/fs/package.json +5 -0
- package/tests/fixtures/valid/proman.yaml +13 -0
- package/tests/fixtures/webui-only/packages/dashboard/.gitkeep +0 -0
- package/tests/fixtures/webui-only/proman.yaml +4 -0
- package/tests/link.test.ts +419 -0
- package/tests/load-config.test.ts +44 -0
- package/tests/npm.test.ts +199 -0
- package/tests/publish.test.ts +599 -0
- package/tests/smoke-test.test.ts +211 -0
- package/tests/validate-config.test.ts +67 -0
- package/tests/version.test.ts +86 -0
- package/tests/workflow-schema.test.ts +72 -0
- package/tests/workspace.test.ts +160 -0
- package/tsconfig.build.json +14 -0
- package/tsconfig.json +8 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
import { mkdirSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { tmpdir } from 'node:os'
|
|
3
|
+
import { join, resolve } from 'node:path'
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, test } from 'vitest'
|
|
5
|
+
import { link, linkStatus, unlink } from '../src/commands/link.ts'
|
|
6
|
+
import type { SpawnFn } from '../src/utils/npm.ts'
|
|
7
|
+
|
|
8
|
+
type Call = { argv: string[]; cwd: string }
|
|
9
|
+
|
|
10
|
+
function makeSpawn(code = 0, stdout = '', stderr = '') {
|
|
11
|
+
const calls: Call[] = []
|
|
12
|
+
const fn: SpawnFn = async (argv, cwd) => {
|
|
13
|
+
calls.push({ argv, cwd })
|
|
14
|
+
return { code, stdout, stderr }
|
|
15
|
+
}
|
|
16
|
+
return { spawn: fn, calls }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe('link command - provider mode (without args)', () => {
|
|
20
|
+
let tmpDir: string
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
tmpDir = resolve(
|
|
24
|
+
tmpdir(),
|
|
25
|
+
`proman-link-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
26
|
+
)
|
|
27
|
+
mkdirSync(tmpDir, { recursive: true })
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
rmSync(tmpDir, { recursive: true, force: true })
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('Test 1: links current package globally with build artifacts', async () => {
|
|
35
|
+
// Given: package directory with dist/ folder
|
|
36
|
+
const pkgDir = tmpDir
|
|
37
|
+
mkdirSync(join(pkgDir, 'dist'), { recursive: true })
|
|
38
|
+
writeFileSync(join(pkgDir, 'dist/index.js'), 'export const x = 1')
|
|
39
|
+
writeFileSync(
|
|
40
|
+
join(pkgDir, 'package.json'),
|
|
41
|
+
JSON.stringify({ name: '@scope/test-pkg', version: '1.0.0' }),
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
const { spawn, calls } = makeSpawn()
|
|
45
|
+
|
|
46
|
+
// When: user runs proman link
|
|
47
|
+
await link({ cwd: pkgDir, spawn })
|
|
48
|
+
|
|
49
|
+
// Then: pnpm link --global is called
|
|
50
|
+
expect(calls).toHaveLength(1)
|
|
51
|
+
expect(calls[0]?.argv).toEqual(['pnpm', 'link', '--global'])
|
|
52
|
+
expect(calls[0]?.cwd).toBe(pkgDir)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('Test 2: throws error when no build artifacts exist', async () => {
|
|
56
|
+
// Given: package directory without dist/ folder
|
|
57
|
+
const pkgDir = tmpDir
|
|
58
|
+
writeFileSync(
|
|
59
|
+
join(pkgDir, 'package.json'),
|
|
60
|
+
JSON.stringify({ name: '@scope/test-pkg', version: '1.0.0' }),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
const { spawn } = makeSpawn()
|
|
64
|
+
|
|
65
|
+
// When/Then: should throw error about missing build
|
|
66
|
+
await expect(link({ cwd: pkgDir, spawn })).rejects.toThrow('No dist/ folder in')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('Test 3: throws error when not in package directory', async () => {
|
|
70
|
+
// Given: directory without package.json
|
|
71
|
+
const pkgDir = tmpDir
|
|
72
|
+
|
|
73
|
+
const { spawn } = makeSpawn()
|
|
74
|
+
|
|
75
|
+
// When/Then: should throw error
|
|
76
|
+
await expect(link({ cwd: pkgDir, spawn })).rejects.toThrow('Missing package.json in')
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
test('Test 4: throws error when package.json has no name field', async () => {
|
|
80
|
+
// Given: package.json without name
|
|
81
|
+
const pkgDir = tmpDir
|
|
82
|
+
mkdirSync(join(pkgDir, 'dist'), { recursive: true })
|
|
83
|
+
writeFileSync(join(pkgDir, 'package.json'), JSON.stringify({ version: '1.0.0' }))
|
|
84
|
+
|
|
85
|
+
const { spawn } = makeSpawn()
|
|
86
|
+
|
|
87
|
+
// When/Then: should throw error
|
|
88
|
+
await expect(link({ cwd: pkgDir, spawn })).rejects.toThrow('missing a "name" field')
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
describe('link command - consumer mode (with package arg)', () => {
|
|
93
|
+
let tmpDir: string
|
|
94
|
+
|
|
95
|
+
beforeEach(() => {
|
|
96
|
+
tmpDir = resolve(
|
|
97
|
+
tmpdir(),
|
|
98
|
+
`proman-link-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
99
|
+
)
|
|
100
|
+
mkdirSync(tmpDir, { recursive: true })
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
afterEach(() => {
|
|
104
|
+
rmSync(tmpDir, { recursive: true, force: true })
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
test('Test 4: links specific package from global registry', async () => {
|
|
108
|
+
// Given: consumer project with package in dependencies
|
|
109
|
+
const consumerDir = tmpDir
|
|
110
|
+
writeFileSync(
|
|
111
|
+
join(consumerDir, 'package.json'),
|
|
112
|
+
JSON.stringify({
|
|
113
|
+
name: '@consumer/app',
|
|
114
|
+
version: '1.0.0',
|
|
115
|
+
dependencies: { '@scope/some-package': '^1.0.0' },
|
|
116
|
+
}),
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
const { spawn, calls } = makeSpawn()
|
|
120
|
+
|
|
121
|
+
// When: user runs proman link @scope/some-package
|
|
122
|
+
await link({ cwd: consumerDir, packageName: '@scope/some-package', spawn })
|
|
123
|
+
|
|
124
|
+
// Then: pnpm link --global @scope/some-package is called
|
|
125
|
+
expect(calls).toHaveLength(1)
|
|
126
|
+
expect(calls[0]?.argv).toEqual(['pnpm', 'link', '--global', '@scope/some-package'])
|
|
127
|
+
expect(calls[0]?.cwd).toBe(consumerDir)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
test('Test 5: throws error when package not in dependencies', async () => {
|
|
131
|
+
// Given: consumer project without the package in deps
|
|
132
|
+
const consumerDir = tmpDir
|
|
133
|
+
writeFileSync(
|
|
134
|
+
join(consumerDir, 'package.json'),
|
|
135
|
+
JSON.stringify({
|
|
136
|
+
name: '@consumer/app',
|
|
137
|
+
version: '1.0.0',
|
|
138
|
+
dependencies: {},
|
|
139
|
+
}),
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
const { spawn } = makeSpawn()
|
|
143
|
+
|
|
144
|
+
// When/Then: should throw error
|
|
145
|
+
await expect(
|
|
146
|
+
link({ cwd: consumerDir, packageName: '@scope/unknown-package', spawn }),
|
|
147
|
+
).rejects.toThrow('not found in dependencies or devDependencies')
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
test('Test 5b: allows linking package in devDependencies', async () => {
|
|
151
|
+
// Given: consumer project with package in devDependencies
|
|
152
|
+
const consumerDir = tmpDir
|
|
153
|
+
writeFileSync(
|
|
154
|
+
join(consumerDir, 'package.json'),
|
|
155
|
+
JSON.stringify({
|
|
156
|
+
name: '@consumer/app',
|
|
157
|
+
version: '1.0.0',
|
|
158
|
+
devDependencies: { '@scope/dev-package': '^1.0.0' },
|
|
159
|
+
}),
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
const { spawn, calls } = makeSpawn()
|
|
163
|
+
|
|
164
|
+
// When: user runs proman link @scope/dev-package
|
|
165
|
+
await link({ cwd: consumerDir, packageName: '@scope/dev-package', spawn })
|
|
166
|
+
|
|
167
|
+
// Then: command succeeds
|
|
168
|
+
expect(calls).toHaveLength(1)
|
|
169
|
+
expect(calls[0]?.argv).toEqual(['pnpm', 'link', '--global', '@scope/dev-package'])
|
|
170
|
+
})
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
describe('linkStatus command', () => {
|
|
174
|
+
let tmpDir: string
|
|
175
|
+
|
|
176
|
+
beforeEach(() => {
|
|
177
|
+
tmpDir = resolve(
|
|
178
|
+
tmpdir(),
|
|
179
|
+
`proman-link-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
180
|
+
)
|
|
181
|
+
mkdirSync(tmpDir, { recursive: true })
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
afterEach(() => {
|
|
185
|
+
rmSync(tmpDir, { recursive: true, force: true })
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
test('Test 6: shows linked packages with source paths', async () => {
|
|
189
|
+
// Given: project with linked packages
|
|
190
|
+
const consumerDir = tmpDir
|
|
191
|
+
writeFileSync(
|
|
192
|
+
join(consumerDir, 'package.json'),
|
|
193
|
+
JSON.stringify({ name: '@consumer/app', version: '1.0.0' }),
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
// Create node_modules with symlinks
|
|
197
|
+
const nodeModulesDir = join(consumerDir, 'node_modules')
|
|
198
|
+
mkdirSync(join(nodeModulesDir, '@scope'), { recursive: true })
|
|
199
|
+
|
|
200
|
+
const targetA = join(tmpdir(), 'monorepo/packages/a')
|
|
201
|
+
const targetB = join(tmpdir(), 'monorepo/packages/b')
|
|
202
|
+
mkdirSync(targetA, { recursive: true })
|
|
203
|
+
mkdirSync(targetB, { recursive: true })
|
|
204
|
+
writeFileSync(join(targetA, 'package.json'), JSON.stringify({ name: '@scope/package-a' }))
|
|
205
|
+
writeFileSync(join(targetB, 'package.json'), JSON.stringify({ name: '@scope/package-b' }))
|
|
206
|
+
|
|
207
|
+
symlinkSync(targetA, join(nodeModulesDir, '@scope', 'package-a'), 'dir')
|
|
208
|
+
symlinkSync(targetB, join(nodeModulesDir, '@scope', 'package-b'), 'dir')
|
|
209
|
+
|
|
210
|
+
const { spawn } = makeSpawn()
|
|
211
|
+
|
|
212
|
+
// When: user runs proman link --status
|
|
213
|
+
const result = await linkStatus({ cwd: consumerDir, spawn })
|
|
214
|
+
|
|
215
|
+
// Then: output shows both packages
|
|
216
|
+
expect(result).toContain('@scope/package-a')
|
|
217
|
+
expect(result).toContain('@scope/package-b')
|
|
218
|
+
expect(result).toContain(targetA)
|
|
219
|
+
expect(result).toContain(targetB)
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
test('Test 7: shows no linked packages message', async () => {
|
|
223
|
+
// Given: project with no linked packages
|
|
224
|
+
const consumerDir = tmpDir
|
|
225
|
+
writeFileSync(
|
|
226
|
+
join(consumerDir, 'package.json'),
|
|
227
|
+
JSON.stringify({ name: '@consumer/app', version: '1.0.0' }),
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
const { spawn } = makeSpawn()
|
|
231
|
+
|
|
232
|
+
// When: user runs proman link --status
|
|
233
|
+
const result = await linkStatus({ cwd: consumerDir, spawn })
|
|
234
|
+
|
|
235
|
+
// Then: message shows no linked packages
|
|
236
|
+
expect(result).toBe('No linked packages found')
|
|
237
|
+
})
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
describe('unlink command', () => {
|
|
241
|
+
let tmpDir: string
|
|
242
|
+
|
|
243
|
+
beforeEach(() => {
|
|
244
|
+
tmpDir = resolve(
|
|
245
|
+
tmpdir(),
|
|
246
|
+
`proman-link-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
247
|
+
)
|
|
248
|
+
mkdirSync(tmpDir, { recursive: true })
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
afterEach(() => {
|
|
252
|
+
rmSync(tmpDir, { recursive: true, force: true })
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
test('Test 8: unlinks all packages', async () => {
|
|
256
|
+
// Given: project with 2 linked packages
|
|
257
|
+
const consumerDir = tmpDir
|
|
258
|
+
writeFileSync(
|
|
259
|
+
join(consumerDir, 'package.json'),
|
|
260
|
+
JSON.stringify({ name: '@consumer/app', version: '1.0.0' }),
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
// Create symlinks
|
|
264
|
+
const nodeModulesDir = join(consumerDir, 'node_modules')
|
|
265
|
+
mkdirSync(join(nodeModulesDir, '@scope'), { recursive: true })
|
|
266
|
+
|
|
267
|
+
const targetA = join(tmpdir(), `temp-a-${Date.now()}`)
|
|
268
|
+
const targetB = join(tmpdir(), `temp-b-${Date.now()}`)
|
|
269
|
+
mkdirSync(targetA, { recursive: true })
|
|
270
|
+
mkdirSync(targetB, { recursive: true })
|
|
271
|
+
writeFileSync(join(targetA, 'package.json'), JSON.stringify({ name: '@scope/package-a' }))
|
|
272
|
+
writeFileSync(join(targetB, 'package.json'), JSON.stringify({ name: '@scope/package-b' }))
|
|
273
|
+
|
|
274
|
+
symlinkSync(targetA, join(nodeModulesDir, '@scope', 'package-a'), 'dir')
|
|
275
|
+
symlinkSync(targetB, join(nodeModulesDir, '@scope', 'package-b'), 'dir')
|
|
276
|
+
|
|
277
|
+
const { spawn, calls } = makeSpawn()
|
|
278
|
+
|
|
279
|
+
// When: user runs proman unlink
|
|
280
|
+
await unlink({ cwd: consumerDir, spawn })
|
|
281
|
+
|
|
282
|
+
// Then: pnpm unlink called for each + pnpm install
|
|
283
|
+
expect(calls.length).toBe(3)
|
|
284
|
+
expect(calls[0]?.argv).toEqual(['pnpm', 'unlink', '@scope/package-a'])
|
|
285
|
+
expect(calls[1]?.argv).toEqual(['pnpm', 'unlink', '@scope/package-b'])
|
|
286
|
+
expect(calls[2]?.argv).toEqual(['pnpm', 'install'])
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
test('Test 9: unlinks specific package', async () => {
|
|
290
|
+
// Given: project with linked package
|
|
291
|
+
const consumerDir = tmpDir
|
|
292
|
+
writeFileSync(
|
|
293
|
+
join(consumerDir, 'package.json'),
|
|
294
|
+
JSON.stringify({ name: '@consumer/app', version: '1.0.0' }),
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
const { spawn, calls } = makeSpawn()
|
|
298
|
+
|
|
299
|
+
// When: user runs proman unlink @scope/package-a
|
|
300
|
+
await unlink({ cwd: consumerDir, packageName: '@scope/package-a', spawn })
|
|
301
|
+
|
|
302
|
+
// Then: pnpm unlink + pnpm install for that package
|
|
303
|
+
expect(calls).toHaveLength(2)
|
|
304
|
+
expect(calls[0]?.argv).toEqual(['pnpm', 'unlink', '@scope/package-a'])
|
|
305
|
+
expect(calls[1]?.argv).toEqual(['pnpm', 'install', '@scope/package-a'])
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
test('Test 10: message when no packages linked', async () => {
|
|
309
|
+
// Given: project with no linked packages
|
|
310
|
+
const consumerDir = tmpDir
|
|
311
|
+
writeFileSync(
|
|
312
|
+
join(consumerDir, 'package.json'),
|
|
313
|
+
JSON.stringify({ name: '@consumer/app', version: '1.0.0' }),
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
const { spawn, calls } = makeSpawn()
|
|
317
|
+
|
|
318
|
+
// When: user runs proman unlink
|
|
319
|
+
await unlink({ cwd: consumerDir, spawn })
|
|
320
|
+
|
|
321
|
+
// Then: no commands executed, only message
|
|
322
|
+
expect(calls).toHaveLength(0)
|
|
323
|
+
})
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
describe('readPackageJson validation', () => {
|
|
327
|
+
let tmpDir: string
|
|
328
|
+
|
|
329
|
+
beforeEach(() => {
|
|
330
|
+
tmpDir = resolve(
|
|
331
|
+
tmpdir(),
|
|
332
|
+
`proman-link-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
333
|
+
)
|
|
334
|
+
mkdirSync(tmpDir, { recursive: true })
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
afterEach(() => {
|
|
338
|
+
rmSync(tmpDir, { recursive: true, force: true })
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
test('Test 1: throws error for primitive string value', async () => {
|
|
342
|
+
// Given: package.json with primitive string
|
|
343
|
+
const pkgDir = tmpDir
|
|
344
|
+
mkdirSync(join(pkgDir, 'dist'), { recursive: true })
|
|
345
|
+
writeFileSync(join(pkgDir, 'package.json'), '"just a string"')
|
|
346
|
+
writeFileSync(join(pkgDir, 'dist/index.js'), '')
|
|
347
|
+
|
|
348
|
+
const { spawn } = makeSpawn()
|
|
349
|
+
|
|
350
|
+
// When/Then: should throw validation error
|
|
351
|
+
await expect(link({ cwd: pkgDir, spawn })).rejects.toThrow(
|
|
352
|
+
`Invalid package.json at ${join(pkgDir, 'package.json')}`,
|
|
353
|
+
)
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
test('Test 2: throws error for array value', async () => {
|
|
357
|
+
// Given: package.json with array
|
|
358
|
+
const pkgDir = tmpDir
|
|
359
|
+
mkdirSync(join(pkgDir, 'dist'), { recursive: true })
|
|
360
|
+
writeFileSync(join(pkgDir, 'package.json'), '[1, 2, 3]')
|
|
361
|
+
writeFileSync(join(pkgDir, 'dist/index.js'), '')
|
|
362
|
+
|
|
363
|
+
const { spawn } = makeSpawn()
|
|
364
|
+
|
|
365
|
+
// When/Then: should throw validation error
|
|
366
|
+
await expect(link({ cwd: pkgDir, spawn })).rejects.toThrow(
|
|
367
|
+
`Invalid package.json at ${join(pkgDir, 'package.json')}`,
|
|
368
|
+
)
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
test('Test 3: throws error for null value', async () => {
|
|
372
|
+
// Given: package.json with null
|
|
373
|
+
const pkgDir = tmpDir
|
|
374
|
+
mkdirSync(join(pkgDir, 'dist'), { recursive: true })
|
|
375
|
+
writeFileSync(join(pkgDir, 'package.json'), 'null')
|
|
376
|
+
writeFileSync(join(pkgDir, 'dist/index.js'), '')
|
|
377
|
+
|
|
378
|
+
const { spawn } = makeSpawn()
|
|
379
|
+
|
|
380
|
+
// When/Then: should throw validation error
|
|
381
|
+
await expect(link({ cwd: pkgDir, spawn })).rejects.toThrow(
|
|
382
|
+
`Invalid package.json at ${join(pkgDir, 'package.json')}`,
|
|
383
|
+
)
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
test('Test 4: succeeds for valid object with name', async () => {
|
|
387
|
+
// Given: package.json with valid object
|
|
388
|
+
const pkgDir = tmpDir
|
|
389
|
+
writeFileSync(
|
|
390
|
+
join(pkgDir, 'package.json'),
|
|
391
|
+
JSON.stringify({ name: 'test-package', version: '1.0.0' }),
|
|
392
|
+
)
|
|
393
|
+
mkdirSync(join(pkgDir, 'dist'), { recursive: true })
|
|
394
|
+
writeFileSync(join(pkgDir, 'dist/index.js'), '')
|
|
395
|
+
|
|
396
|
+
const { spawn, calls } = makeSpawn()
|
|
397
|
+
|
|
398
|
+
// When: readPackageJson is called via link command
|
|
399
|
+
await link({ cwd: pkgDir, spawn })
|
|
400
|
+
|
|
401
|
+
// Then: should succeed without throwing
|
|
402
|
+
expect(calls).toHaveLength(1)
|
|
403
|
+
expect(calls[0]?.argv).toEqual(['pnpm', 'link', '--global'])
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
test('Test 5: succeeds for empty object', async () => {
|
|
407
|
+
// Given: package.json with empty object
|
|
408
|
+
const pkgDir = tmpDir
|
|
409
|
+
writeFileSync(join(pkgDir, 'package.json'), '{}')
|
|
410
|
+
mkdirSync(join(pkgDir, 'dist'), { recursive: true })
|
|
411
|
+
writeFileSync(join(pkgDir, 'dist/index.js'), '')
|
|
412
|
+
|
|
413
|
+
const { spawn } = makeSpawn()
|
|
414
|
+
|
|
415
|
+
// When/Then: readPackageJson succeeds, but link fails due to missing name
|
|
416
|
+
// This validates that readPackageJson accepts empty objects
|
|
417
|
+
await expect(link({ cwd: pkgDir, spawn })).rejects.toThrow('missing a "name" field')
|
|
418
|
+
})
|
|
419
|
+
})
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { dirname, resolve } from 'node:path'
|
|
2
|
+
import { fileURLToPath } from 'node:url'
|
|
3
|
+
|
|
4
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
5
|
+
|
|
6
|
+
import { describe, expect, test } from 'vitest'
|
|
7
|
+
import { loadConfig } from '../src/config/index.ts'
|
|
8
|
+
|
|
9
|
+
const FIX = (name: string) => resolve(__dirname, 'fixtures', name)
|
|
10
|
+
|
|
11
|
+
describe('loadConfig', () => {
|
|
12
|
+
test('happy path — loads issue example fixture', () => {
|
|
13
|
+
const cfg = loadConfig(FIX('valid'))
|
|
14
|
+
expect(cfg.packages).toHaveLength(3)
|
|
15
|
+
expect(cfg.packages.map((p) => p.name)).toEqual(['@ocas/core', '@ocas/fs', '@ocas/cli'])
|
|
16
|
+
expect(cfg.release?.registry).toBe('https://registry.npmjs.org')
|
|
17
|
+
expect(cfg.release?.access).toBe('public')
|
|
18
|
+
expect(cfg.release?.gitTagPrefix).toBe('v')
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
test('applies registry + gitTagPrefix defaults when release omitted', () => {
|
|
22
|
+
const cfg = loadConfig(FIX('defaults'))
|
|
23
|
+
expect(cfg.release?.registry).toBe('https://registry.npmjs.org')
|
|
24
|
+
expect(cfg.release?.gitTagPrefix).toBe('v')
|
|
25
|
+
expect(cfg.release?.access).toBeUndefined()
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test('rejects when proman.yaml is missing', () => {
|
|
29
|
+
expect(() => loadConfig(resolve(__dirname, 'fixtures', 'no-such-dir'))).toThrow(
|
|
30
|
+
/proman\.yaml not found/,
|
|
31
|
+
)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('rejects empty packages', () => {
|
|
35
|
+
expect(() => loadConfig(FIX('bad-packages'))).toThrow(/packages/i)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('T5: defaults each package type to lib when omitted', () => {
|
|
39
|
+
const cfg = loadConfig(FIX('valid'))
|
|
40
|
+
for (const p of cfg.packages) {
|
|
41
|
+
expect(p.type).toBe('lib')
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
})
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
createNpmRunner,
|
|
4
|
+
defaultSpawn,
|
|
5
|
+
formatRcVersion,
|
|
6
|
+
nextRcNumber,
|
|
7
|
+
parseReleaseBranch,
|
|
8
|
+
runOrThrow,
|
|
9
|
+
type SpawnFn,
|
|
10
|
+
} from '../src/utils/npm.ts'
|
|
11
|
+
|
|
12
|
+
describe('parseReleaseBranch', () => {
|
|
13
|
+
test('parses release/0.3.0', () => {
|
|
14
|
+
expect(parseReleaseBranch('release/0.3.0')).toBe('0.3.0')
|
|
15
|
+
})
|
|
16
|
+
test('parses prerelease branches', () => {
|
|
17
|
+
expect(parseReleaseBranch('release/1.2.3-beta.1')).toBe('1.2.3-beta.1')
|
|
18
|
+
})
|
|
19
|
+
test('throws on main', () => {
|
|
20
|
+
expect(() => parseReleaseBranch('main')).toThrow()
|
|
21
|
+
})
|
|
22
|
+
test('throws on feature/x', () => {
|
|
23
|
+
expect(() => parseReleaseBranch('feature/x')).toThrow()
|
|
24
|
+
})
|
|
25
|
+
test('throws on release/ (empty)', () => {
|
|
26
|
+
expect(() => parseReleaseBranch('release/')).toThrow()
|
|
27
|
+
})
|
|
28
|
+
test('throws on empty', () => {
|
|
29
|
+
expect(() => parseReleaseBranch('')).toThrow()
|
|
30
|
+
})
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
describe('nextRcNumber', () => {
|
|
34
|
+
test('empty registry => 1', () => {
|
|
35
|
+
expect(nextRcNumber({ baseVersion: '0.3.0', existing: [] })).toBe(1)
|
|
36
|
+
})
|
|
37
|
+
test('mixed versions, single rc', () => {
|
|
38
|
+
expect(nextRcNumber({ baseVersion: '0.3.0', existing: ['0.3.0', '0.2.0', '0.3.0-rc.1'] })).toBe(
|
|
39
|
+
2,
|
|
40
|
+
)
|
|
41
|
+
})
|
|
42
|
+
test('finds max across multiple rcs', () => {
|
|
43
|
+
expect(
|
|
44
|
+
nextRcNumber({
|
|
45
|
+
baseVersion: '0.3.0',
|
|
46
|
+
existing: ['0.3.0-rc.1', '0.3.0-rc.2', '0.3.0-rc.5'],
|
|
47
|
+
}),
|
|
48
|
+
).toBe(6)
|
|
49
|
+
})
|
|
50
|
+
test('ignores rc for other base', () => {
|
|
51
|
+
expect(
|
|
52
|
+
nextRcNumber({
|
|
53
|
+
baseVersion: '0.3.0',
|
|
54
|
+
existing: ['0.2.0-rc.9', '0.3.0-rc.1'],
|
|
55
|
+
}),
|
|
56
|
+
).toBe(2)
|
|
57
|
+
})
|
|
58
|
+
test('ignores non-rc prereleases', () => {
|
|
59
|
+
expect(
|
|
60
|
+
nextRcNumber({
|
|
61
|
+
baseVersion: '0.3.0',
|
|
62
|
+
existing: ['0.3.0-beta.1', '0.3.0-alpha.4'],
|
|
63
|
+
}),
|
|
64
|
+
).toBe(1)
|
|
65
|
+
})
|
|
66
|
+
test('handles non-numeric rc tag', () => {
|
|
67
|
+
expect(
|
|
68
|
+
nextRcNumber({
|
|
69
|
+
baseVersion: '0.3.0',
|
|
70
|
+
existing: ['0.3.0-rc.x', '0.3.0-rc.2'],
|
|
71
|
+
}),
|
|
72
|
+
).toBe(3)
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
describe('formatRcVersion', () => {
|
|
77
|
+
test('basic', () => {
|
|
78
|
+
expect(formatRcVersion('0.3.0', 1)).toBe('0.3.0-rc.1')
|
|
79
|
+
})
|
|
80
|
+
test('higher number', () => {
|
|
81
|
+
expect(formatRcVersion('1.0.0', 7)).toBe('1.0.0-rc.7')
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
describe('runOrThrow', () => {
|
|
86
|
+
test('should export runOrThrow from npm utils', () => {
|
|
87
|
+
expect(runOrThrow).toBeDefined()
|
|
88
|
+
expect(typeof runOrThrow).toBe('function')
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test('should succeed when spawn returns code 0', async () => {
|
|
92
|
+
const spawn: SpawnFn = async () => ({ code: 0, stdout: '', stderr: '' })
|
|
93
|
+
await expect(runOrThrow(spawn, ['echo', 'hello'], '/tmp')).resolves.toBeUndefined()
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
test('should throw when spawn returns non-zero', async () => {
|
|
97
|
+
const spawn: SpawnFn = async () => ({ code: 1, stdout: '', stderr: 'error' })
|
|
98
|
+
await expect(runOrThrow(spawn, ['fail'], '/tmp')).rejects.toThrow(/fail failed: error/)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test('should include stdout in error when stderr is empty', async () => {
|
|
102
|
+
const spawn: SpawnFn = async () => ({ code: 1, stdout: 'stdout detail', stderr: '' })
|
|
103
|
+
await expect(runOrThrow(spawn, ['fail'], '/tmp')).rejects.toThrow(/fail failed: stdout detail/)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
test('should format error message with command name', async () => {
|
|
107
|
+
const spawn: SpawnFn = async () => ({ code: 1, stdout: '', stderr: '' })
|
|
108
|
+
await expect(runOrThrow(spawn, ['pnpm', 'build'], '/tmp')).rejects.toThrow(/pnpm build failed/)
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
describe('createNpmRunner format argv', () => {
|
|
113
|
+
function makeSpawn(code = 0) {
|
|
114
|
+
const calls: string[][] = []
|
|
115
|
+
const fn: SpawnFn = async (argv, _cwd) => {
|
|
116
|
+
calls.push(argv)
|
|
117
|
+
return { code, stdout: '', stderr: 'boom' }
|
|
118
|
+
}
|
|
119
|
+
return { spawn: fn, calls }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
test('B3: format is a function', () => {
|
|
123
|
+
const { spawn } = makeSpawn()
|
|
124
|
+
const runner = createNpmRunner('/root', spawn)
|
|
125
|
+
expect(typeof runner.format).toBe('function')
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
test('runs format via pnpm', async () => {
|
|
129
|
+
const { spawn, calls } = makeSpawn()
|
|
130
|
+
const runner = createNpmRunner('/root', spawn)
|
|
131
|
+
await runner.format()
|
|
132
|
+
expect(calls[0]).toEqual(['pnpm', 'run', 'format'])
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
test('B4: non-zero exit throws with argv', async () => {
|
|
136
|
+
const { spawn } = makeSpawn(1)
|
|
137
|
+
const runner = createNpmRunner('/root', spawn)
|
|
138
|
+
await expect(runner.format()).rejects.toThrow(/pnpm run format/)
|
|
139
|
+
})
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
describe('createNpmRunner publish argv', () => {
|
|
143
|
+
function makeSpawn() {
|
|
144
|
+
const calls: string[][] = []
|
|
145
|
+
const fn: SpawnFn = async (argv, _cwd) => {
|
|
146
|
+
calls.push(argv)
|
|
147
|
+
return { code: 0, stdout: '', stderr: '' }
|
|
148
|
+
}
|
|
149
|
+
return { spawn: fn, calls }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
test('publishes via pnpm with --no-git-checks', async () => {
|
|
153
|
+
const { spawn, calls } = makeSpawn()
|
|
154
|
+
const runner = createNpmRunner('/root', spawn)
|
|
155
|
+
await runner.publish('/root/packages/a', { tag: 'rc' })
|
|
156
|
+
const last = calls[calls.length - 1] as string[]
|
|
157
|
+
expect(last[0]).toBe('pnpm')
|
|
158
|
+
expect(last[1]).toBe('publish')
|
|
159
|
+
expect(last).toContain('--no-git-checks')
|
|
160
|
+
expect(last).toContain('--tag')
|
|
161
|
+
expect(last).toContain('rc')
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
test('passes --access public', async () => {
|
|
165
|
+
const { spawn, calls } = makeSpawn()
|
|
166
|
+
const runner = createNpmRunner('/root', spawn)
|
|
167
|
+
await runner.publish('/root/packages/a', { tag: 'rc', access: 'public' })
|
|
168
|
+
const last = calls[calls.length - 1] as string[]
|
|
169
|
+
expect(last).toContain('--access')
|
|
170
|
+
expect(last).toContain('public')
|
|
171
|
+
})
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
describe('defaultSpawn captures output via pipe', () => {
|
|
175
|
+
test('captures stdout from a real process', async () => {
|
|
176
|
+
const result = await defaultSpawn(['echo', 'hello world'], process.cwd())
|
|
177
|
+
expect(result.code).toBe(0)
|
|
178
|
+
expect(result.stdout.trim()).toBe('hello world')
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
test('captures stderr from a real process', async () => {
|
|
182
|
+
const result = await defaultSpawn(['node', '-e', 'process.stderr.write("oops")'], process.cwd())
|
|
183
|
+
expect(result.code).toBe(0)
|
|
184
|
+
expect(result.stderr).toBe('oops')
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
test('returns non-zero exit code with stderr captured', async () => {
|
|
188
|
+
const result = await defaultSpawn(
|
|
189
|
+
[
|
|
190
|
+
'node',
|
|
191
|
+
'-e',
|
|
192
|
+
'process.stderr.write("You cannot publish over the previously published versions: 0.1.1"); process.exit(1)',
|
|
193
|
+
],
|
|
194
|
+
process.cwd(),
|
|
195
|
+
)
|
|
196
|
+
expect(result.code).toBe(1)
|
|
197
|
+
expect(result.stderr).toContain('cannot publish over the previously published versions')
|
|
198
|
+
})
|
|
199
|
+
})
|