@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,261 @@
|
|
|
1
|
+
import { mkdir, mkdtemp, readdir, readFile, rm, writeFile } from 'node:fs/promises'
|
|
2
|
+
import { tmpdir } from 'node:os'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, test } from 'vitest'
|
|
5
|
+
import { bump } from '../src/commands/bump.ts'
|
|
6
|
+
|
|
7
|
+
type FixtureOptions = {
|
|
8
|
+
version?: string
|
|
9
|
+
versions?: Record<string, string>
|
|
10
|
+
withChangeset?: boolean
|
|
11
|
+
changesetBody?: string
|
|
12
|
+
multiPkg?: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function setupFixture(tmp: string, opts: FixtureOptions = {}) {
|
|
16
|
+
const version = opts.version ?? '0.2.0'
|
|
17
|
+
const packages = opts.multiPkg
|
|
18
|
+
? [
|
|
19
|
+
{ name: '@test/core', path: 'packages/core', type: 'lib' },
|
|
20
|
+
{ name: '@test/cli', path: 'packages/cli', type: 'cli' },
|
|
21
|
+
]
|
|
22
|
+
: [{ name: '@test/core', path: 'packages/core', type: 'lib' }]
|
|
23
|
+
|
|
24
|
+
const { stringify } = await import('yaml')
|
|
25
|
+
await writeFile(join(tmp, 'proman.yaml'), stringify({ packages }))
|
|
26
|
+
|
|
27
|
+
for (const pkg of packages) {
|
|
28
|
+
const dir = join(tmp, pkg.path)
|
|
29
|
+
await mkdir(dir, { recursive: true })
|
|
30
|
+
const pkgVersion = opts.versions?.[pkg.name] ?? version
|
|
31
|
+
await writeFile(
|
|
32
|
+
join(dir, 'package.json'),
|
|
33
|
+
`${JSON.stringify({ name: pkg.name, version: pkgVersion }, null, 2)}\n`,
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (opts.withChangeset) {
|
|
38
|
+
const csDir = join(tmp, '.changeset')
|
|
39
|
+
await mkdir(csDir, { recursive: true })
|
|
40
|
+
const body = opts.changesetBody ?? '---\n"@test/core": minor\n---\nAdd feature X\n'
|
|
41
|
+
await writeFile(join(csDir, 'add-feature.md'), body)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let tmp: string
|
|
46
|
+
|
|
47
|
+
beforeEach(async () => {
|
|
48
|
+
tmp = await mkdtemp(join(tmpdir(), 'proman-bump-'))
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
afterEach(async () => {
|
|
52
|
+
await rm(tmp, { recursive: true })
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
// ── bump command ──
|
|
56
|
+
|
|
57
|
+
describe('bump', () => {
|
|
58
|
+
test('explicit --type patch bumps all packages', async () => {
|
|
59
|
+
await setupFixture(tmp, { version: '1.2.3' })
|
|
60
|
+
const bumped = await bump({ type: 'patch', cwd: tmp })
|
|
61
|
+
expect(bumped).toEqual({ '@test/core': '1.2.4' })
|
|
62
|
+
|
|
63
|
+
const pkg = JSON.parse(await readFile(join(tmp, 'packages/core/package.json'), 'utf8'))
|
|
64
|
+
expect(pkg.version).toBe('1.2.4')
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('explicit --type minor', async () => {
|
|
68
|
+
await setupFixture(tmp, { version: '1.2.3' })
|
|
69
|
+
const bumped = await bump({ type: 'minor', cwd: tmp })
|
|
70
|
+
expect(bumped).toEqual({ '@test/core': '1.3.0' })
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('explicit --type major', async () => {
|
|
74
|
+
await setupFixture(tmp, { version: '1.2.3' })
|
|
75
|
+
const bumped = await bump({ type: 'major', cwd: tmp })
|
|
76
|
+
expect(bumped).toEqual({ '@test/core': '2.0.0' })
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
test('auto-infer from changesets (only bumps mentioned package)', async () => {
|
|
80
|
+
await setupFixture(tmp, { withChangeset: true, version: '0.2.0' })
|
|
81
|
+
const bumped = await bump({ cwd: tmp })
|
|
82
|
+
expect(bumped).toEqual({ '@test/core': '0.3.0' }) // minor from changeset
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
test('rejects when no type and no changesets', async () => {
|
|
86
|
+
await setupFixture(tmp)
|
|
87
|
+
await expect(bump({ cwd: tmp })).rejects.toThrow('no --type specified')
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
test('explicit --type bumps all packages in monorepo', async () => {
|
|
91
|
+
await setupFixture(tmp, { multiPkg: true, version: '1.0.0' })
|
|
92
|
+
const bumped = await bump({ type: 'patch', cwd: tmp })
|
|
93
|
+
expect(bumped).toEqual({ '@test/core': '1.0.1', '@test/cli': '1.0.1' })
|
|
94
|
+
|
|
95
|
+
const core = JSON.parse(await readFile(join(tmp, 'packages/core/package.json'), 'utf8'))
|
|
96
|
+
const cli = JSON.parse(await readFile(join(tmp, 'packages/cli/package.json'), 'utf8'))
|
|
97
|
+
expect(core.version).toBe('1.0.1')
|
|
98
|
+
expect(cli.version).toBe('1.0.1')
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test('changeset only bumps mentioned packages, leaves others unchanged', async () => {
|
|
102
|
+
await setupFixture(tmp, {
|
|
103
|
+
multiPkg: true,
|
|
104
|
+
version: '1.0.0',
|
|
105
|
+
withChangeset: true,
|
|
106
|
+
changesetBody: '---\n"@test/core": patch\n---\nFix bug\n',
|
|
107
|
+
})
|
|
108
|
+
const bumped = await bump({ cwd: tmp })
|
|
109
|
+
// Only core is bumped
|
|
110
|
+
expect(bumped).toEqual({ '@test/core': '1.0.1' })
|
|
111
|
+
|
|
112
|
+
const core = JSON.parse(await readFile(join(tmp, 'packages/core/package.json'), 'utf8'))
|
|
113
|
+
const cli = JSON.parse(await readFile(join(tmp, 'packages/cli/package.json'), 'utf8'))
|
|
114
|
+
expect(core.version).toBe('1.0.1')
|
|
115
|
+
expect(cli.version).toBe('1.0.0') // unchanged
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
test('changeset bumps multiple packages independently', async () => {
|
|
119
|
+
await setupFixture(tmp, {
|
|
120
|
+
multiPkg: true,
|
|
121
|
+
versions: { '@test/core': '1.0.0', '@test/cli': '2.0.0' },
|
|
122
|
+
withChangeset: true,
|
|
123
|
+
changesetBody: '---\n"@test/core": minor\n"@test/cli": patch\n---\nMixed changes\n',
|
|
124
|
+
})
|
|
125
|
+
const bumped = await bump({ cwd: tmp })
|
|
126
|
+
expect(bumped).toEqual({ '@test/core': '1.1.0', '@test/cli': '2.0.1' })
|
|
127
|
+
|
|
128
|
+
const core = JSON.parse(await readFile(join(tmp, 'packages/core/package.json'), 'utf8'))
|
|
129
|
+
const cli = JSON.parse(await readFile(join(tmp, 'packages/cli/package.json'), 'utf8'))
|
|
130
|
+
expect(core.version).toBe('1.1.0')
|
|
131
|
+
expect(cli.version).toBe('2.0.1')
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
test('does not touch workspace:* deps', async () => {
|
|
135
|
+
await setupFixture(tmp, { multiPkg: true })
|
|
136
|
+
// Add workspace dep
|
|
137
|
+
const cliPkg = join(tmp, 'packages/cli/package.json')
|
|
138
|
+
const json = JSON.parse(await readFile(cliPkg, 'utf8'))
|
|
139
|
+
json.dependencies = { '@test/core': 'workspace:*' }
|
|
140
|
+
await writeFile(cliPkg, `${JSON.stringify(json, null, 2)}\n`)
|
|
141
|
+
|
|
142
|
+
await bump({ type: 'patch', cwd: tmp })
|
|
143
|
+
|
|
144
|
+
const updated = JSON.parse(await readFile(cliPkg, 'utf8'))
|
|
145
|
+
expect(updated.dependencies['@test/core']).toBe('workspace:*')
|
|
146
|
+
})
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
// ── changelog generation (issue #74) ──
|
|
150
|
+
|
|
151
|
+
describe('bump changelog', () => {
|
|
152
|
+
test('changeset-infer bump generates CHANGELOG.md', async () => {
|
|
153
|
+
await setupFixture(tmp, { withChangeset: true, version: '0.2.0' })
|
|
154
|
+
const bumped = await bump({ cwd: tmp, now: () => new Date('2026-06-08T00:00:00Z') })
|
|
155
|
+
|
|
156
|
+
expect(bumped).toEqual({ '@test/core': '0.3.0' })
|
|
157
|
+
|
|
158
|
+
const changelog = await readFile(join(tmp, 'packages/core/CHANGELOG.md'), 'utf8')
|
|
159
|
+
expect(changelog).toContain('0.3.0')
|
|
160
|
+
expect(changelog).toContain('2026-06-08')
|
|
161
|
+
expect(changelog).toContain('Add feature X')
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
test('changeset-infer bump deletes consumed changeset files', async () => {
|
|
165
|
+
await setupFixture(tmp, { withChangeset: true, version: '0.2.0' })
|
|
166
|
+
await bump({ cwd: tmp })
|
|
167
|
+
|
|
168
|
+
const csDir = join(tmp, '.changeset')
|
|
169
|
+
const files = await readdir(csDir)
|
|
170
|
+
expect(files.filter((f) => f.endsWith('.md') && f.toLowerCase() !== 'readme.md')).toHaveLength(
|
|
171
|
+
0,
|
|
172
|
+
)
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
test('explicit --type does not generate changelog or delete changesets', async () => {
|
|
176
|
+
await setupFixture(tmp, { withChangeset: true, version: '1.0.0' })
|
|
177
|
+
await bump({ type: 'patch', cwd: tmp })
|
|
178
|
+
|
|
179
|
+
// Changeset file should still exist
|
|
180
|
+
const csDir = join(tmp, '.changeset')
|
|
181
|
+
const files = await readdir(csDir)
|
|
182
|
+
expect(files).toContain('add-feature.md')
|
|
183
|
+
|
|
184
|
+
// No CHANGELOG.md generated
|
|
185
|
+
const changelogExists = await readFile(join(tmp, 'packages/core/CHANGELOG.md'), 'utf8').catch(
|
|
186
|
+
() => null,
|
|
187
|
+
)
|
|
188
|
+
expect(changelogExists).toBeNull()
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
test('changeset-infer bump merges multiple changesets into changelog', async () => {
|
|
192
|
+
await setupFixture(tmp, { version: '1.0.0' })
|
|
193
|
+
|
|
194
|
+
// Create two changeset files
|
|
195
|
+
const csDir = join(tmp, '.changeset')
|
|
196
|
+
await mkdir(csDir, { recursive: true })
|
|
197
|
+
await writeFile(join(csDir, 'first.md'), '---\n"@test/core": minor\n---\nAdd feature A\n')
|
|
198
|
+
await writeFile(join(csDir, 'second.md'), '---\n"@test/core": patch\n---\nFix bug B\n')
|
|
199
|
+
|
|
200
|
+
const bumped = await bump({ cwd: tmp, now: () => new Date('2026-06-08T00:00:00Z') })
|
|
201
|
+
// minor wins over patch
|
|
202
|
+
expect(bumped).toEqual({ '@test/core': '1.1.0' })
|
|
203
|
+
|
|
204
|
+
const changelog = await readFile(join(tmp, 'packages/core/CHANGELOG.md'), 'utf8')
|
|
205
|
+
expect(changelog).toContain('1.1.0')
|
|
206
|
+
expect(changelog).toContain('Add feature A')
|
|
207
|
+
expect(changelog).toContain('Fix bug B')
|
|
208
|
+
|
|
209
|
+
// Both changeset files deleted
|
|
210
|
+
const files = await readdir(csDir)
|
|
211
|
+
expect(files.filter((f) => f.endsWith('.md'))).toHaveLength(0)
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
test('changeset-infer bump prepends to existing CHANGELOG.md', async () => {
|
|
215
|
+
await setupFixture(tmp, { withChangeset: true, version: '0.2.0' })
|
|
216
|
+
|
|
217
|
+
// Pre-existing changelog
|
|
218
|
+
const changelogPath = join(tmp, 'packages/core/CHANGELOG.md')
|
|
219
|
+
await writeFile(changelogPath, '# Changelog\n\n## 0.1.0 — 2026-01-01\n\n- Initial release\n')
|
|
220
|
+
|
|
221
|
+
await bump({ cwd: tmp, now: () => new Date('2026-06-08T00:00:00Z') })
|
|
222
|
+
|
|
223
|
+
const changelog = await readFile(changelogPath, 'utf8')
|
|
224
|
+
// New entry should come before old entry
|
|
225
|
+
const newIdx = changelog.indexOf('0.3.0')
|
|
226
|
+
const oldIdx = changelog.indexOf('0.1.0')
|
|
227
|
+
expect(newIdx).toBeLessThan(oldIdx)
|
|
228
|
+
expect(changelog).toContain('Add feature X')
|
|
229
|
+
expect(changelog).toContain('Initial release')
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
test('multi-package: only generates changelog for changeset-mentioned packages', async () => {
|
|
233
|
+
await setupFixture(tmp, {
|
|
234
|
+
multiPkg: true,
|
|
235
|
+
version: '1.0.0',
|
|
236
|
+
withChangeset: true,
|
|
237
|
+
changesetBody: '---\n"@test/core": patch\n---\nFix core bug\n',
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
await bump({ cwd: tmp, now: () => new Date('2026-06-08T00:00:00Z') })
|
|
241
|
+
|
|
242
|
+
// core gets changelog
|
|
243
|
+
const coreChangelog = await readFile(join(tmp, 'packages/core/CHANGELOG.md'), 'utf8')
|
|
244
|
+
expect(coreChangelog).toContain('1.0.1')
|
|
245
|
+
expect(coreChangelog).toContain('Fix core bug')
|
|
246
|
+
|
|
247
|
+
// cli does NOT get changelog
|
|
248
|
+
const cliChangelog = await readFile(join(tmp, 'packages/cli/CHANGELOG.md'), 'utf8').catch(
|
|
249
|
+
() => null,
|
|
250
|
+
)
|
|
251
|
+
expect(cliChangelog).toBeNull()
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
test('changeset-infer bump uses injected now() for changelog date', async () => {
|
|
255
|
+
await setupFixture(tmp, { withChangeset: true, version: '0.2.0' })
|
|
256
|
+
await bump({ cwd: tmp, now: () => new Date('2025-12-25T00:00:00Z') })
|
|
257
|
+
|
|
258
|
+
const changelog = await readFile(join(tmp, 'packages/core/CHANGELOG.md'), 'utf8')
|
|
259
|
+
expect(changelog).toContain('2025-12-25')
|
|
260
|
+
})
|
|
261
|
+
})
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
|
|
2
|
+
import { tmpdir } from 'node:os'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, test } from 'vitest'
|
|
5
|
+
import {
|
|
6
|
+
buildChangelogEntry,
|
|
7
|
+
parseChangeset,
|
|
8
|
+
prependChangelog,
|
|
9
|
+
readChangesets,
|
|
10
|
+
} from '../src/utils/changeset.ts'
|
|
11
|
+
|
|
12
|
+
describe('parseChangeset', () => {
|
|
13
|
+
test('parses single-package frontmatter', () => {
|
|
14
|
+
const raw = `---\n'pkg-a': minor\n---\n\nAdd new API.\n`
|
|
15
|
+
const cs = parseChangeset(raw, '/tmp/x.md')
|
|
16
|
+
expect(cs.packages).toEqual({ 'pkg-a': 'minor' })
|
|
17
|
+
expect(cs.body).toBe('Add new API.')
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test('parses multi-package frontmatter (quoted and unquoted)', () => {
|
|
21
|
+
const raw = `---\npkg-a: patch\n"pkg-b": minor\n---\nBody line.\n`
|
|
22
|
+
const cs = parseChangeset(raw, '/tmp/x.md')
|
|
23
|
+
expect(cs.packages).toEqual({ 'pkg-a': 'patch', 'pkg-b': 'minor' })
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('trims body of leading/trailing blank lines', () => {
|
|
27
|
+
const raw = '---\npkg-a: patch\n---\n\n\nThe body.\n\n\n'
|
|
28
|
+
const cs = parseChangeset(raw, '/tmp/x.md')
|
|
29
|
+
expect(cs.body).toBe('The body.')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('rejects unknown bump type', () => {
|
|
33
|
+
const raw = '---\npkg-a: weird\n---\nbody\n'
|
|
34
|
+
expect(() => parseChangeset(raw, '/tmp/x.md')).toThrow(/invalid bump/)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('rejects missing frontmatter', () => {
|
|
38
|
+
expect(() => parseChangeset('no frontmatter here', '/tmp/x.md')).toThrow(/frontmatter/)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('stores file exactly as passed', () => {
|
|
42
|
+
const raw = '---\npkg-a: patch\n---\nbody\n'
|
|
43
|
+
const cs = parseChangeset(raw, '/abs/path/cs.md')
|
|
44
|
+
expect(cs.file).toBe('/abs/path/cs.md')
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
describe('readChangesets', () => {
|
|
49
|
+
let tmp: string
|
|
50
|
+
beforeEach(async () => {
|
|
51
|
+
tmp = await mkdtemp(join(tmpdir(), 'proman-cs-'))
|
|
52
|
+
})
|
|
53
|
+
afterEach(async () => {
|
|
54
|
+
await rm(tmp, { recursive: true, force: true })
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('returns empty array when .changeset/ does not exist', async () => {
|
|
58
|
+
expect(await readChangesets(tmp)).toEqual([])
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('returns empty array when only config.json exists', async () => {
|
|
62
|
+
await mkdir(join(tmp, '.changeset'))
|
|
63
|
+
await writeFile(join(tmp, '.changeset/config.json'), '{}')
|
|
64
|
+
expect(await readChangesets(tmp)).toEqual([])
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('lists only *.md (excludes config.json, README.md, non-md)', async () => {
|
|
68
|
+
await mkdir(join(tmp, '.changeset'))
|
|
69
|
+
await writeFile(join(tmp, '.changeset/funny-fox.md'), '---\npkg-a: minor\n---\nA\n')
|
|
70
|
+
await writeFile(join(tmp, '.changeset/brave-bear.md'), '---\npkg-b: patch\n---\nB\n')
|
|
71
|
+
await writeFile(join(tmp, '.changeset/config.json'), '{}')
|
|
72
|
+
await writeFile(join(tmp, '.changeset/README.md'), 'readme')
|
|
73
|
+
await writeFile(join(tmp, '.changeset/notes.txt'), 'x')
|
|
74
|
+
const cs = await readChangesets(tmp)
|
|
75
|
+
expect(cs.length).toBe(2)
|
|
76
|
+
expect(cs.map((c) => c.file.split('/').pop())).toEqual(['brave-bear.md', 'funny-fox.md'])
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
test('sorts results by filename', async () => {
|
|
80
|
+
await mkdir(join(tmp, '.changeset'))
|
|
81
|
+
await writeFile(join(tmp, '.changeset/zebra.md'), '---\npkg-a: minor\n---\nZ\n')
|
|
82
|
+
await writeFile(join(tmp, '.changeset/alpha.md'), '---\npkg-a: minor\n---\nA\n')
|
|
83
|
+
const cs = await readChangesets(tmp)
|
|
84
|
+
expect(cs.map((c) => c.file.split('/').pop())).toEqual(['alpha.md', 'zebra.md'])
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test('throws on .md without frontmatter', async () => {
|
|
88
|
+
await mkdir(join(tmp, '.changeset'))
|
|
89
|
+
await writeFile(join(tmp, '.changeset/bad.md'), 'no frontmatter here\n')
|
|
90
|
+
await expect(readChangesets(tmp)).rejects.toThrow(/frontmatter/)
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
describe('buildChangelogEntry', () => {
|
|
95
|
+
test('single body', () => {
|
|
96
|
+
const out = buildChangelogEntry({ version: '0.3.0', date: '2026-06-02', bodies: ['Add API.'] })
|
|
97
|
+
expect(out).toBe('## 0.3.0 — 2026-06-02\n\n- Add API.\n\n')
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
test('multi-line body uses two-space continuation indent', () => {
|
|
101
|
+
const out = buildChangelogEntry({
|
|
102
|
+
version: '0.3.0',
|
|
103
|
+
date: '2026-06-02',
|
|
104
|
+
bodies: ['L1\nL2'],
|
|
105
|
+
})
|
|
106
|
+
expect(out).toContain('- L1\n L2')
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test('multiple bodies become multiple bullets in order', () => {
|
|
110
|
+
const out = buildChangelogEntry({
|
|
111
|
+
version: '0.3.0',
|
|
112
|
+
date: '2026-06-02',
|
|
113
|
+
bodies: ['First', 'Second'],
|
|
114
|
+
})
|
|
115
|
+
const firstIdx = out.indexOf('- First')
|
|
116
|
+
const secondIdx = out.indexOf('- Second')
|
|
117
|
+
expect(firstIdx).toBeGreaterThan(0)
|
|
118
|
+
expect(secondIdx).toBeGreaterThan(firstIdx)
|
|
119
|
+
})
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
describe('prependChangelog', () => {
|
|
123
|
+
test('null existing yields # Changelog header + entry', () => {
|
|
124
|
+
const out = prependChangelog(null, '## 0.3.0 — 2026-06-02\n\n- A\n\n')
|
|
125
|
+
expect(out).toBe('# Changelog\n\n## 0.3.0 — 2026-06-02\n\n- A\n\n')
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
test('inserts entry after existing # Changelog heading', () => {
|
|
129
|
+
const existing = '# Changelog\n\n## 0.2.0 — 2025-01-01\n\n- old\n'
|
|
130
|
+
const out = prependChangelog(existing, '## 0.3.0 — 2026-06-02\n\n- new\n\n')
|
|
131
|
+
expect(out.startsWith('# Changelog\n\n## 0.3.0 — 2026-06-02')).toBe(true)
|
|
132
|
+
expect(out).toContain('## 0.2.0')
|
|
133
|
+
expect(out).toContain('- old')
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
test('no heading: prepends entry above existing content', () => {
|
|
137
|
+
const existing = 'just some text\n'
|
|
138
|
+
const out = prependChangelog(existing, '## 0.3.0 — 2026-06-02\n\n- n\n\n')
|
|
139
|
+
expect(out.startsWith('## 0.3.0')).toBe(true)
|
|
140
|
+
expect(out).toContain('just some text')
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
test('empty existing treated like null', () => {
|
|
144
|
+
const out = prependChangelog('', '## 0.3.0\n\n- n\n\n')
|
|
145
|
+
expect(out.startsWith('# Changelog')).toBe(true)
|
|
146
|
+
})
|
|
147
|
+
})
|
|
@@ -0,0 +1,98 @@
|
|
|
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 { deploy } from '../src/commands/deploy.ts'
|
|
8
|
+
import type { SpawnFn } from '../src/utils/npm.ts'
|
|
9
|
+
|
|
10
|
+
const FIX = (name: string) => resolve(__dirname, 'fixtures', name)
|
|
11
|
+
|
|
12
|
+
type Call = { argv: string[]; cwd: string }
|
|
13
|
+
|
|
14
|
+
function makeSpawn(code = 0, stdout = '', stderr = '') {
|
|
15
|
+
const calls: Call[] = []
|
|
16
|
+
const fn: SpawnFn = async (argv, cwd) => {
|
|
17
|
+
calls.push({ argv, cwd })
|
|
18
|
+
return { code, stdout, stderr }
|
|
19
|
+
}
|
|
20
|
+
return { spawn: fn, calls }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('deploy command', () => {
|
|
24
|
+
test('DEP1: typed fixture, no flags, deploys webui then api', async () => {
|
|
25
|
+
const { spawn, calls } = makeSpawn()
|
|
26
|
+
await deploy({ cwd: FIX('typed'), spawn })
|
|
27
|
+
expect(calls).toHaveLength(2)
|
|
28
|
+
// webui
|
|
29
|
+
expect(calls[0]?.argv).toEqual(['pnpm', 'exec', 'wrangler', 'pages', 'deploy', 'dist'])
|
|
30
|
+
expect(calls[0]?.cwd).toBe(resolve(FIX('typed'), 'packages/dashboard'))
|
|
31
|
+
// api
|
|
32
|
+
expect(calls[1]?.argv).toEqual(['pnpm', 'exec', 'wrangler', 'deploy'])
|
|
33
|
+
expect(calls[1]?.cwd).toBe(resolve(FIX('typed'), 'packages/api'))
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('DEP2: --env staging appends to both', async () => {
|
|
37
|
+
const { spawn, calls } = makeSpawn()
|
|
38
|
+
await deploy({ cwd: FIX('typed'), spawn, env: 'staging' })
|
|
39
|
+
expect(calls).toHaveLength(2)
|
|
40
|
+
expect(calls[0]?.argv).toEqual([
|
|
41
|
+
'pnpm',
|
|
42
|
+
'exec',
|
|
43
|
+
'wrangler',
|
|
44
|
+
'pages',
|
|
45
|
+
'deploy',
|
|
46
|
+
'dist',
|
|
47
|
+
'--env',
|
|
48
|
+
'staging',
|
|
49
|
+
])
|
|
50
|
+
expect(calls[1]?.argv).toEqual(['pnpm', 'exec', 'wrangler', 'deploy', '--env', 'staging'])
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test('DEP3: --package selects only one webui', async () => {
|
|
54
|
+
const { spawn, calls } = makeSpawn()
|
|
55
|
+
await deploy({ cwd: FIX('typed'), spawn, pkg: '@myapp/dashboard' })
|
|
56
|
+
expect(calls).toHaveLength(1)
|
|
57
|
+
expect(calls[0]?.argv).toEqual(['pnpm', 'exec', 'wrangler', 'pages', 'deploy', 'dist'])
|
|
58
|
+
expect(calls[0]?.cwd).toBe(resolve(FIX('typed'), 'packages/dashboard'))
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('DEP4: --package on a lib throws not-deployable', async () => {
|
|
62
|
+
const { spawn } = makeSpawn()
|
|
63
|
+
await expect(deploy({ cwd: FIX('typed'), spawn, pkg: '@ocas/core' })).rejects.toThrow(
|
|
64
|
+
/not deployable|cannot deploy/i,
|
|
65
|
+
)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('DEP5: --package not found throws', async () => {
|
|
69
|
+
const { spawn } = makeSpawn()
|
|
70
|
+
await expect(deploy({ cwd: FIX('typed'), spawn, pkg: 'does-not-exist' })).rejects.toThrow(
|
|
71
|
+
/not found|unknown package/i,
|
|
72
|
+
)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('DEP6: no-deployable fixture, no flags → no spawn, no throw', async () => {
|
|
76
|
+
const { spawn, calls } = makeSpawn()
|
|
77
|
+
await deploy({ cwd: FIX('no-deployable'), spawn })
|
|
78
|
+
expect(calls).toHaveLength(0)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test('DEP7: non-zero spawn exit throws', async () => {
|
|
82
|
+
const { spawn } = makeSpawn(1, '', 'fail')
|
|
83
|
+
await expect(deploy({ cwd: FIX('typed'), spawn })).rejects.toThrow()
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test('DEP8: --env production --package @myapp/api', async () => {
|
|
87
|
+
const { spawn, calls } = makeSpawn()
|
|
88
|
+
await deploy({
|
|
89
|
+
cwd: FIX('typed'),
|
|
90
|
+
spawn,
|
|
91
|
+
pkg: '@myapp/api',
|
|
92
|
+
env: 'production',
|
|
93
|
+
})
|
|
94
|
+
expect(calls).toHaveLength(1)
|
|
95
|
+
expect(calls[0]?.argv).toEqual(['pnpm', 'exec', 'wrangler', 'deploy', '--env', 'production'])
|
|
96
|
+
expect(calls[0]?.cwd).toBe(resolve(FIX('typed'), 'packages/api'))
|
|
97
|
+
})
|
|
98
|
+
})
|