@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,214 @@
|
|
|
1
|
+
import { chmodSync, existsSync, readdirSync, readFileSync, rmSync } from 'node:fs'
|
|
2
|
+
import { join, resolve } from 'node:path'
|
|
3
|
+
import { loadConfig } from '../config/index.js'
|
|
4
|
+
import {
|
|
5
|
+
computeBuildFingerprints,
|
|
6
|
+
computeRootFingerprint,
|
|
7
|
+
fingerprintPath,
|
|
8
|
+
readFingerprint,
|
|
9
|
+
writeFingerprint,
|
|
10
|
+
} from '../utils/fingerprint.js'
|
|
11
|
+
import { defaultSpawn, runOrThrow, type SpawnFn } from '../utils/npm.js'
|
|
12
|
+
|
|
13
|
+
export type DevCommandOptions = {
|
|
14
|
+
cwd: string
|
|
15
|
+
spawn?: SpawnFn
|
|
16
|
+
/** When provided, enables fingerprint caching.
|
|
17
|
+
* - false: check fingerprint, skip if match
|
|
18
|
+
* - true: always run (--force / CI)
|
|
19
|
+
* - undefined: legacy behavior — always run, no fingerprint logic */
|
|
20
|
+
force?: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function pnpmExec(bin: string, ...args: string[]): string[] {
|
|
24
|
+
return ['pnpm', 'exec', bin, ...args]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function build(opts: DevCommandOptions): Promise<void> {
|
|
28
|
+
const spawn = opts.spawn ?? defaultSpawn
|
|
29
|
+
const cwd = resolve(opts.cwd)
|
|
30
|
+
const cfg = loadConfig(cwd)
|
|
31
|
+
const useFingerprint = opts.force !== undefined
|
|
32
|
+
const force = opts.force ?? false
|
|
33
|
+
|
|
34
|
+
// Compute fingerprints only when fingerprint caching is enabled
|
|
35
|
+
const fingerprints = useFingerprint ? computeBuildFingerprints(cwd, cfg.packages) : null
|
|
36
|
+
|
|
37
|
+
// Determine which packages to build
|
|
38
|
+
const toRun: { idx: number; pkgDir: string; fpPath: string; fpValue: string }[] = []
|
|
39
|
+
|
|
40
|
+
for (let i = 0; i < cfg.packages.length; i++) {
|
|
41
|
+
const pkg = cfg.packages[i] as (typeof cfg.packages)[number]
|
|
42
|
+
const pkgDir = resolve(cwd, pkg.path)
|
|
43
|
+
|
|
44
|
+
const fpPath = fingerprintPath(pkgDir, 'build', pkg.name)
|
|
45
|
+
const fpValue = fingerprints?.get(pkg.name) ?? ''
|
|
46
|
+
|
|
47
|
+
if (useFingerprint && !force) {
|
|
48
|
+
const stored = readFingerprint(fpPath)
|
|
49
|
+
if (stored === fpValue) {
|
|
50
|
+
console.log(`⏭ build: ${pkg.name} (unchanged)`)
|
|
51
|
+
continue // skip — fingerprint matches
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
toRun.push({ idx: i, pkgDir, fpPath, fpValue })
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Execute builds
|
|
59
|
+
for (const { idx, pkgDir } of toRun) {
|
|
60
|
+
const pkg = cfg.packages[idx] as (typeof cfg.packages)[number]
|
|
61
|
+
// Clean output dir + tsbuildinfo before build to prevent stale artifacts.
|
|
62
|
+
// Note: since build fingerprints live inside dist/ (see fingerprintPath()),
|
|
63
|
+
// removing dist/ intentionally invalidates the build cache for this package.
|
|
64
|
+
const outDir = join(pkgDir, 'dist')
|
|
65
|
+
if (existsSync(outDir)) {
|
|
66
|
+
rmSync(outDir, { recursive: true })
|
|
67
|
+
}
|
|
68
|
+
const buildInfo = join(pkgDir, 'tsconfig.tsbuildinfo')
|
|
69
|
+
if (existsSync(buildInfo)) {
|
|
70
|
+
rmSync(buildInfo)
|
|
71
|
+
}
|
|
72
|
+
let argv: string[]
|
|
73
|
+
switch (pkg.type) {
|
|
74
|
+
case 'webui':
|
|
75
|
+
argv = pnpmExec('vite', 'build')
|
|
76
|
+
break
|
|
77
|
+
case 'cli':
|
|
78
|
+
// cli → use package's own build script (may be esbuild, tsc, etc.)
|
|
79
|
+
argv = ['pnpm', 'run', 'build']
|
|
80
|
+
break
|
|
81
|
+
default:
|
|
82
|
+
// lib | api → tsc --build
|
|
83
|
+
argv = pnpmExec('tsc', '--build')
|
|
84
|
+
break
|
|
85
|
+
}
|
|
86
|
+
await runOrThrow(spawn, argv, pkgDir)
|
|
87
|
+
|
|
88
|
+
// chmod +x bin entries so linked CLIs survive tsc rebuild
|
|
89
|
+
chmodBinEntries(pkgDir)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Write fingerprints only after ALL builds succeed (and only when enabled)
|
|
93
|
+
if (useFingerprint) {
|
|
94
|
+
for (const { fpPath, fpValue } of toRun) {
|
|
95
|
+
writeFingerprint(fpPath, fpValue)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function chmodBinEntries(pkgDir: string): void {
|
|
101
|
+
const pkgJsonPath = join(pkgDir, 'package.json')
|
|
102
|
+
if (!existsSync(pkgJsonPath)) return
|
|
103
|
+
const json = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'))
|
|
104
|
+
const bin: unknown = json.bin
|
|
105
|
+
if (bin == null) return
|
|
106
|
+
const paths =
|
|
107
|
+
typeof bin === 'string'
|
|
108
|
+
? [bin]
|
|
109
|
+
: typeof bin === 'object'
|
|
110
|
+
? Object.values(bin as Record<string, string>)
|
|
111
|
+
: []
|
|
112
|
+
for (const rel of paths) {
|
|
113
|
+
const abs = resolve(pkgDir, rel)
|
|
114
|
+
if (existsSync(abs)) {
|
|
115
|
+
chmodSync(abs, 0o755)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function runTests(opts: DevCommandOptions): Promise<void> {
|
|
121
|
+
const spawn = opts.spawn ?? defaultSpawn
|
|
122
|
+
const cwd = resolve(opts.cwd)
|
|
123
|
+
const useFingerprint = opts.force !== undefined
|
|
124
|
+
const force = opts.force ?? false
|
|
125
|
+
|
|
126
|
+
if (useFingerprint) {
|
|
127
|
+
const fpPath = fingerprintPath(cwd, 'test')
|
|
128
|
+
const fpValue = computeRootFingerprint(cwd, 'test')
|
|
129
|
+
|
|
130
|
+
if (!force) {
|
|
131
|
+
const stored = readFingerprint(fpPath)
|
|
132
|
+
if (stored === fpValue) {
|
|
133
|
+
console.log('⏭ test (unchanged)')
|
|
134
|
+
return // skip
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
await runOrThrow(spawn, pnpmExec('vitest', 'run'), cwd)
|
|
139
|
+
writeFingerprint(fpPath, fpValue)
|
|
140
|
+
} else {
|
|
141
|
+
await runOrThrow(spawn, pnpmExec('vitest', 'run'), cwd)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export async function check(opts: DevCommandOptions): Promise<void> {
|
|
146
|
+
const spawn = opts.spawn ?? defaultSpawn
|
|
147
|
+
const cwd = resolve(opts.cwd)
|
|
148
|
+
const useFingerprint = opts.force !== undefined
|
|
149
|
+
const force = opts.force ?? false
|
|
150
|
+
|
|
151
|
+
if (useFingerprint) {
|
|
152
|
+
const fpPath = fingerprintPath(cwd, 'check')
|
|
153
|
+
const fpValue = computeRootFingerprint(cwd, 'check')
|
|
154
|
+
|
|
155
|
+
if (!force) {
|
|
156
|
+
const stored = readFingerprint(fpPath)
|
|
157
|
+
if (stored === fpValue) {
|
|
158
|
+
console.log('⏭ check (unchanged)')
|
|
159
|
+
return // skip
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
await runOrThrow(spawn, pnpmExec('biome', 'check', '.'), cwd)
|
|
164
|
+
await validateWorkflows(spawn, cwd)
|
|
165
|
+
writeFingerprint(fpPath, fpValue)
|
|
166
|
+
} else {
|
|
167
|
+
await runOrThrow(spawn, pnpmExec('biome', 'check', '.'), cwd)
|
|
168
|
+
await validateWorkflows(spawn, cwd)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export async function format(opts: DevCommandOptions): Promise<void> {
|
|
173
|
+
const spawn = opts.spawn ?? defaultSpawn
|
|
174
|
+
const cwd = resolve(opts.cwd)
|
|
175
|
+
await runOrThrow(spawn, pnpmExec('biome', 'format', '--write', '.'), cwd)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Discover .workflows/*.yaml and validate each with `uwf workflow validate`. Skips if uwf is not installed. */
|
|
179
|
+
async function validateWorkflows(spawn: SpawnFn, cwd: string): Promise<void> {
|
|
180
|
+
const dirs = [join(cwd, '.workflows'), join(cwd, '.workflow')]
|
|
181
|
+
const files: string[] = []
|
|
182
|
+
|
|
183
|
+
for (const dir of dirs) {
|
|
184
|
+
if (!existsSync(dir)) continue
|
|
185
|
+
for (const entry of readdirSync(dir)) {
|
|
186
|
+
if (entry.endsWith('.yaml') || entry.endsWith('.yml')) {
|
|
187
|
+
files.push(join(dir, entry))
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (files.length === 0) return
|
|
193
|
+
|
|
194
|
+
// Check if uwf is available
|
|
195
|
+
const { code: uwfCheck } = await spawn(['which', 'uwf'], cwd)
|
|
196
|
+
if (uwfCheck !== 0) {
|
|
197
|
+
console.log('⚠ uwf not installed, skipping workflow validation')
|
|
198
|
+
return
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const errors: string[] = []
|
|
202
|
+
for (const file of files) {
|
|
203
|
+
const { code, stderr } = await spawn(['uwf', 'workflow', 'validate', file], cwd)
|
|
204
|
+
if (code !== 0) {
|
|
205
|
+
errors.push(`${file}: ${stderr.trim() || 'validation failed'}`)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (errors.length > 0) {
|
|
210
|
+
throw new Error(`Workflow validation failed:\n${errors.join('\n')}`)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
console.log(`✓ ${files.length} workflow(s) validated`)
|
|
214
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Export all command functions
|
|
2
|
+
export { type BumpOptions, bump } from './bump.js'
|
|
3
|
+
export { type DeployCommandOptions, deploy } from './deploy.js'
|
|
4
|
+
export { build, check, type DevCommandOptions, format, runTests } from './dev.js'
|
|
5
|
+
export { type InitOptions, init } from './init.js'
|
|
6
|
+
export { type LinkCommandOptions, link, linkStatus, unlink } from './link.js'
|
|
7
|
+
export { type PublishOptions, publish } from './publish.js'
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process'
|
|
2
|
+
import { existsSync, mkdirSync, rmSync } from 'node:fs'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import { afterAll, beforeAll, describe, expect, test } from 'vitest'
|
|
6
|
+
|
|
7
|
+
// Skip: beforeAll pnpm install times out on CI (cold cache, 60s limit)
|
|
8
|
+
// Unit tests in init.test.ts cover structure; this only adds smoke-test of generated project
|
|
9
|
+
// See: https://git.shazhou.work/shazhou/proman/issues/171
|
|
10
|
+
describe.skip('proman init integration', () => {
|
|
11
|
+
let testDir: string
|
|
12
|
+
let projectDir: string
|
|
13
|
+
|
|
14
|
+
beforeAll(() => {
|
|
15
|
+
// Create a unique temp directory for integration test
|
|
16
|
+
testDir = join(
|
|
17
|
+
tmpdir(),
|
|
18
|
+
`proman-init-integration-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
19
|
+
)
|
|
20
|
+
mkdirSync(testDir, { recursive: true })
|
|
21
|
+
projectDir = join(testDir, 'test-project')
|
|
22
|
+
|
|
23
|
+
// Get the path to the proman CLI (used only for init — the generated project uses its own)
|
|
24
|
+
const promanBin = join(process.cwd(), 'dist', 'cli.js')
|
|
25
|
+
|
|
26
|
+
// Run proman init
|
|
27
|
+
execSync(`node ${promanBin} init test-project`, { cwd: testDir })
|
|
28
|
+
|
|
29
|
+
// Install dependencies — this installs @shazhou/proman as a devDep inside the generated project.
|
|
30
|
+
// 60s timeout: pnpm install can be slow on first run (cold cache, registry fetch).
|
|
31
|
+
execSync('pnpm install', { cwd: projectDir, stdio: 'inherit' })
|
|
32
|
+
}, 60_000)
|
|
33
|
+
|
|
34
|
+
afterAll(() => {
|
|
35
|
+
// Clean up the test directory
|
|
36
|
+
if (existsSync(testDir)) {
|
|
37
|
+
rmSync(testDir, { recursive: true, force: true })
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('proman build succeeds', () => {
|
|
42
|
+
// Use the generated project's own proman (installed as devDep), not the parent's
|
|
43
|
+
expect(() => {
|
|
44
|
+
execSync('pnpm exec proman build', { cwd: projectDir, stdio: 'inherit' })
|
|
45
|
+
}).not.toThrow()
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test('proman test succeeds', () => {
|
|
49
|
+
expect(() => {
|
|
50
|
+
execSync('pnpm exec proman test', { cwd: projectDir, stdio: 'inherit' })
|
|
51
|
+
}).not.toThrow()
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('proman check succeeds', () => {
|
|
55
|
+
expect(() => {
|
|
56
|
+
execSync('pnpm exec proman check', { cwd: projectDir, stdio: 'inherit' })
|
|
57
|
+
}).not.toThrow()
|
|
58
|
+
})
|
|
59
|
+
})
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { tmpdir } from 'node:os'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, test } from 'vitest'
|
|
5
|
+
import { init } from './init.js'
|
|
6
|
+
|
|
7
|
+
describe('proman init', () => {
|
|
8
|
+
let testDir: string
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
// Create a unique temp directory for each test
|
|
12
|
+
testDir = join(
|
|
13
|
+
tmpdir(),
|
|
14
|
+
`proman-init-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
15
|
+
)
|
|
16
|
+
mkdirSync(testDir, { recursive: true })
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
// Clean up the test directory
|
|
21
|
+
if (existsSync(testDir)) {
|
|
22
|
+
rmSync(testDir, { recursive: true, force: true })
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('creates full monorepo structure with target directory', async () => {
|
|
27
|
+
const projectDir = join(testDir, 'my-project')
|
|
28
|
+
|
|
29
|
+
await init({ targetDir: projectDir })
|
|
30
|
+
|
|
31
|
+
// Verify root directory exists
|
|
32
|
+
expect(existsSync(projectDir)).toBe(true)
|
|
33
|
+
|
|
34
|
+
// Verify root files
|
|
35
|
+
const rootPackageJson = JSON.parse(readFileSync(join(projectDir, 'package.json'), 'utf-8'))
|
|
36
|
+
expect(rootPackageJson.private).toBe(true)
|
|
37
|
+
expect(rootPackageJson.scripts).toBeDefined()
|
|
38
|
+
expect(rootPackageJson.scripts.build).toBe('proman build')
|
|
39
|
+
expect(rootPackageJson.scripts.test).toBe('proman test')
|
|
40
|
+
expect(rootPackageJson.scripts.check).toBe('proman check')
|
|
41
|
+
expect(rootPackageJson.scripts.format).toBe('proman format')
|
|
42
|
+
expect(rootPackageJson.devDependencies['@shazhou/proman']).toBeDefined()
|
|
43
|
+
|
|
44
|
+
expect(existsSync(join(projectDir, 'proman.yaml'))).toBe(true)
|
|
45
|
+
expect(existsSync(join(projectDir, 'pnpm-workspace.yaml'))).toBe(true)
|
|
46
|
+
expect(existsSync(join(projectDir, 'biome.json'))).toBe(true)
|
|
47
|
+
expect(existsSync(join(projectDir, 'tsconfig.json'))).toBe(true)
|
|
48
|
+
expect(existsSync(join(projectDir, '.gitignore'))).toBe(true)
|
|
49
|
+
|
|
50
|
+
// Verify gitignore content
|
|
51
|
+
const gitignore = readFileSync(join(projectDir, '.gitignore'), 'utf-8')
|
|
52
|
+
expect(gitignore).toContain('node_modules')
|
|
53
|
+
expect(gitignore).toContain('dist')
|
|
54
|
+
expect(gitignore).toContain('.proman')
|
|
55
|
+
expect(gitignore).toContain('*.tsbuildinfo')
|
|
56
|
+
|
|
57
|
+
// Verify pnpm-workspace.yaml
|
|
58
|
+
const workspace = readFileSync(join(projectDir, 'pnpm-workspace.yaml'), 'utf-8')
|
|
59
|
+
expect(workspace).toContain('packages/*')
|
|
60
|
+
|
|
61
|
+
// Verify core package
|
|
62
|
+
const corePackageJson = JSON.parse(
|
|
63
|
+
readFileSync(join(projectDir, 'packages/core/package.json'), 'utf-8'),
|
|
64
|
+
)
|
|
65
|
+
expect(corePackageJson.name).toBe('@my-project/core')
|
|
66
|
+
expect(corePackageJson.type).toBe('module')
|
|
67
|
+
expect(corePackageJson.exports).toBeDefined()
|
|
68
|
+
|
|
69
|
+
expect(existsSync(join(projectDir, 'packages/core/tsconfig.json'))).toBe(true)
|
|
70
|
+
expect(existsSync(join(projectDir, 'packages/core/src/index.ts'))).toBe(true)
|
|
71
|
+
expect(existsSync(join(projectDir, 'packages/core/src/index.test.ts'))).toBe(true)
|
|
72
|
+
|
|
73
|
+
const coreIndex = readFileSync(join(projectDir, 'packages/core/src/index.ts'), 'utf-8')
|
|
74
|
+
expect(coreIndex).toContain('export function hello()')
|
|
75
|
+
|
|
76
|
+
const coreTest = readFileSync(join(projectDir, 'packages/core/src/index.test.ts'), 'utf-8')
|
|
77
|
+
expect(coreTest).toContain('import')
|
|
78
|
+
expect(coreTest).toContain('hello')
|
|
79
|
+
|
|
80
|
+
// Verify CLI package
|
|
81
|
+
const cliPackageJson = JSON.parse(
|
|
82
|
+
readFileSync(join(projectDir, 'packages/cli/package.json'), 'utf-8'),
|
|
83
|
+
)
|
|
84
|
+
expect(cliPackageJson.name).toBe('@my-project/cli')
|
|
85
|
+
expect(cliPackageJson.bin).toEqual({ 'my-project': 'dist/cli.js' })
|
|
86
|
+
expect(cliPackageJson.dependencies).toEqual({ '@my-project/core': 'workspace:*' })
|
|
87
|
+
|
|
88
|
+
expect(existsSync(join(projectDir, 'packages/cli/tsconfig.json'))).toBe(true)
|
|
89
|
+
expect(existsSync(join(projectDir, 'packages/cli/src/cli.ts'))).toBe(true)
|
|
90
|
+
expect(existsSync(join(projectDir, 'packages/cli/src/cli.test.ts'))).toBe(true)
|
|
91
|
+
|
|
92
|
+
const cliSrc = readFileSync(join(projectDir, 'packages/cli/src/cli.ts'), 'utf-8')
|
|
93
|
+
expect(cliSrc).toContain('#!/usr/bin/env node')
|
|
94
|
+
expect(cliSrc).toContain('@my-project/core')
|
|
95
|
+
|
|
96
|
+
const cliTest = readFileSync(join(projectDir, 'packages/cli/src/cli.test.ts'), 'utf-8')
|
|
97
|
+
expect(cliTest).toContain('test')
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
test('init in current directory when no arg provided', async () => {
|
|
101
|
+
const projectDir = join(testDir, 'test-repo')
|
|
102
|
+
mkdirSync(projectDir)
|
|
103
|
+
|
|
104
|
+
await init({ targetDir: projectDir })
|
|
105
|
+
|
|
106
|
+
// Verify packages use directory name
|
|
107
|
+
const corePackageJson = JSON.parse(
|
|
108
|
+
readFileSync(join(projectDir, 'packages/core/package.json'), 'utf-8'),
|
|
109
|
+
)
|
|
110
|
+
expect(corePackageJson.name).toBe('@test-repo/core')
|
|
111
|
+
|
|
112
|
+
const cliPackageJson = JSON.parse(
|
|
113
|
+
readFileSync(join(projectDir, 'packages/cli/package.json'), 'utf-8'),
|
|
114
|
+
)
|
|
115
|
+
expect(cliPackageJson.name).toBe('@test-repo/cli')
|
|
116
|
+
expect(cliPackageJson.bin).toEqual({ 'test-repo': 'dist/cli.js' })
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
test('fails when target directory is not empty', async () => {
|
|
120
|
+
const projectDir = join(testDir, 'existing-project')
|
|
121
|
+
mkdirSync(projectDir, { recursive: true })
|
|
122
|
+
writeFileSync(join(projectDir, 'README.md'), '# existing')
|
|
123
|
+
|
|
124
|
+
await expect(init({ targetDir: projectDir })).rejects.toThrow(
|
|
125
|
+
/Directory is not empty.*existing-project/,
|
|
126
|
+
)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
test('fails when target directory exists with content', async () => {
|
|
130
|
+
const projectDir = join(testDir, 'my-project')
|
|
131
|
+
mkdirSync(projectDir, { recursive: true })
|
|
132
|
+
writeFileSync(join(projectDir, 'package.json'), '{}')
|
|
133
|
+
|
|
134
|
+
await expect(init({ targetDir: projectDir })).rejects.toThrow(/not empty/)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
test('sanitizes directory name with uppercase and special chars', async () => {
|
|
138
|
+
const projectDir = join(testDir, 'My-Project_v2!')
|
|
139
|
+
await init({ targetDir: projectDir })
|
|
140
|
+
|
|
141
|
+
const corePackageJson = JSON.parse(
|
|
142
|
+
readFileSync(join(projectDir, 'packages/core/package.json'), 'utf-8'),
|
|
143
|
+
)
|
|
144
|
+
expect(corePackageJson.name).toBe('@my-project_v2-/core')
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
test('sanitizes directory name starting with dot', async () => {
|
|
148
|
+
const projectDir = join(testDir, '.hidden-project')
|
|
149
|
+
await init({ targetDir: projectDir })
|
|
150
|
+
|
|
151
|
+
const corePackageJson = JSON.parse(
|
|
152
|
+
readFileSync(join(projectDir, 'packages/core/package.json'), 'utf-8'),
|
|
153
|
+
)
|
|
154
|
+
expect(corePackageJson.name).toBe('@hidden-project/core')
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
test('strips tilde from directory name', async () => {
|
|
158
|
+
const projectDir = join(testDir, 'my~project')
|
|
159
|
+
await init({ targetDir: projectDir })
|
|
160
|
+
|
|
161
|
+
const corePackageJson = JSON.parse(
|
|
162
|
+
readFileSync(join(projectDir, 'packages/core/package.json'), 'utf-8'),
|
|
163
|
+
)
|
|
164
|
+
expect(corePackageJson.name).toBe('@my-project/core')
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
test('truncates package name at 214 characters', async () => {
|
|
168
|
+
const longName = 'a'.repeat(250)
|
|
169
|
+
const projectDir = join(testDir, longName)
|
|
170
|
+
await init({ targetDir: projectDir })
|
|
171
|
+
|
|
172
|
+
const corePackageJson = JSON.parse(
|
|
173
|
+
readFileSync(join(projectDir, 'packages/core/package.json'), 'utf-8'),
|
|
174
|
+
)
|
|
175
|
+
// scope '@' + name + '/core' — the name segment itself is capped at 214
|
|
176
|
+
const nameSegment = corePackageJson.name.replace(/^@/, '').replace(/\/core$/, '')
|
|
177
|
+
expect(nameSegment.length).toBeLessThanOrEqual(214)
|
|
178
|
+
})
|
|
179
|
+
})
|