@beechcms/cli 0.6.0-preview.4 → 0.6.1

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.
@@ -1,80 +0,0 @@
1
- // SPDX-License-Identifier: MIT
2
- // Copyright (c) 2024–2026 Flavio De Musso
3
-
4
- import pc from 'picocolors'
5
- import type { Seed } from '@beechcms/core'
6
- import { SEED_REGISTRY, validateSeedDefinitions } from '@beechcms/core'
7
-
8
- export interface ValidateOptions {
9
- registry?: Record<string, Seed> | null
10
- }
11
-
12
- export interface SeedValidationError {
13
- slug: string
14
- messages: string[]
15
- /** true = abort seed:load; false = warning only */
16
- fatal: boolean
17
- }
18
-
19
- export function validateSeeds(registry: Record<string, Seed>): SeedValidationError[] {
20
- return validateSeedDefinitions(Object.values(registry))
21
- }
22
-
23
- export async function validate(args: ValidateOptions): Promise<void> {
24
- const registry = args.registry ?? SEED_REGISTRY
25
-
26
- if (Object.keys(registry).length === 0) {
27
- console.warn(pc.yellow('\n Warning: SEED_REGISTRY is empty. Create a seeds.ts in your project root.\n'))
28
- return
29
- }
30
-
31
- console.log(pc.cyan('\n beech validate — checking seeds\n'))
32
-
33
- const errors = validateSeeds(registry)
34
- const fatalErrors = errors.filter(e => e.fatal)
35
- const warnings = errors.filter(e => !e.fatal)
36
-
37
- // Print fatal errors first
38
- for (const e of fatalErrors) {
39
- console.log(pc.red(` ✗ ${e.slug} (fatal)`))
40
- for (const msg of e.messages) {
41
- console.log(pc.red(` → ${msg}`))
42
- }
43
- }
44
-
45
- // Print per-seed warnings
46
- const warningMap = new Map(warnings.map(e => [e.slug, e.messages]))
47
- const allWarningSlugsSeen = new Set(warnings.map(e => e.slug))
48
-
49
- for (const seed of Object.values(registry)) {
50
- const msgs = warningMap.get(seed.slug)
51
- if (!msgs) {
52
- if (!allWarningSlugsSeen.has(seed.slug)) {
53
- // only print ✓ if no fatal error for this slug either
54
- const hasFatal = fatalErrors.some(e => e.slug === seed.slug)
55
- if (!hasFatal) console.log(pc.green(` ✓ ${seed.slug}`))
56
- }
57
- } else {
58
- console.log(pc.yellow(` ⚠ ${seed.slug}`))
59
- for (const msg of msgs) {
60
- console.log(pc.yellow(` → ${msg}`))
61
- }
62
- }
63
- }
64
-
65
- console.log('')
66
-
67
- const totalFatal = fatalErrors.reduce((n, e) => n + e.messages.length, 0)
68
- const totalWarnings = warnings.reduce((n, e) => n + e.messages.length, 0)
69
-
70
- if (totalFatal > 0) {
71
- const s = totalFatal !== 1 ? 's' : ''
72
- console.log(pc.red(` Found ${totalFatal} fatal error${s}. Fix before loading.\n`))
73
- process.exit(1)
74
- } else if (totalWarnings > 0) {
75
- const s = totalWarnings !== 1 ? 's' : ''
76
- console.log(pc.yellow(` Found ${totalWarnings} warning${s}. Review seeds above.\n`))
77
- } else {
78
- console.log(pc.green(' All seeds valid.\n'))
79
- }
80
- }
package/src/index.ts DELETED
@@ -1,24 +0,0 @@
1
- // SPDX-License-Identifier: MIT
2
- // Copyright (c) 2024–2026 Flavio De Musso
3
-
4
- export { seedLoad } from './commands/seed-load.js'
5
- export type { SeedLoadOptions } from './commands/seed-load.js'
6
- export { init } from './commands/init.js'
7
- export type { InitOptions } from './commands/init.js'
8
- export { validate, validateSeeds } from './commands/validate.js'
9
- export type { ValidateOptions, SeedValidationError } from './commands/validate.js'
10
- export { seedCreate } from './commands/seed-create.js'
11
- export type { SeedCreateOptions } from './commands/seed-create.js'
12
- export { deploy } from './commands/deploy.js'
13
- export type { DeployOptions } from './commands/deploy.js'
14
- export { onboard } from './commands/onboard.js'
15
- export type { OnboardOptions } from './commands/onboard.js'
16
- export { update } from './commands/update.js'
17
- export type { UpdateOptions } from './commands/update.js'
18
- export { reset } from './commands/reset.js'
19
- export type { ResetOptions } from './commands/reset.js'
20
- export { generateTypes } from './commands/generate-types.js'
21
- export type { GenerateTypesOptions } from './commands/generate-types.js'
22
- export { schemaDiff } from './commands/schema-diff.js'
23
- export type { SchemaDiffOptions } from './commands/schema-diff.js'
24
-
@@ -1,106 +0,0 @@
1
- // SPDX-License-Identifier: MIT
2
- // Copyright (c) 2024–2026 Flavio De Musso
3
-
4
- import { readdirSync, writeFileSync, existsSync, mkdirSync } from 'node:fs'
5
- import { join } from 'node:path'
6
- import {
7
- generateAddColumn,
8
- generateIndexes,
9
- planCreateSeed,
10
- type Seed,
11
- } from '@beechcms/core'
12
- import type { SeedDiff } from './schema-diff.js'
13
-
14
- /** Statuses we will NOT auto-migrate — they need human review in a hand-written file. */
15
- const DESTRUCTIVE: ReadonlySet<SeedDiff['columns'][number]['status']> = new Set([
16
- 'extra', 'type_mismatch', 'fk_mismatch',
17
- ])
18
-
19
- export interface MigrationPlan {
20
- /** Full SQL file body (may be empty if no additive changes). */
21
- sql: string
22
- /** Number of executable (additive) statements emitted. */
23
- additiveCount: number
24
- /** Slugs whose drift includes destructive changes that were NOT emitted. */
25
- destructiveSlugs: string[]
26
- }
27
-
28
- /** Scans a migrations dir and returns the next zero-padded 4-digit prefix (e.g. "0034"). */
29
- export function nextMigrationIndex(migrationsDir: string): string {
30
- if (!existsSync(migrationsDir)) return '0000'
31
- let max = -1
32
- for (const f of readdirSync(migrationsDir)) {
33
- const m = /^(\d{4})_/.exec(f)
34
- if (m) max = Math.max(max, Number(m[1]))
35
- }
36
- return String(max + 1).padStart(4, '0')
37
- }
38
-
39
- /**
40
- * Build an ADDITIVE migration from already-computed diffs.
41
- * - tableExists === false → full planCreateSeed(seed)
42
- * - status 'missing' → generateAddColumn(seed, branch)
43
- * - status 'index_missing' → matching CREATE INDEX IF NOT EXISTS line
44
- * - destructive statuses → emitted as a commented -- ⚠ block, never executable
45
- * `seeds` MUST be dependency-sorted by the caller (sortSeedsByDependencies).
46
- */
47
- export function buildMigrationSql(
48
- diffs: SeedDiff[],
49
- registry: Record<string, Seed>,
50
- ): MigrationPlan {
51
- const lines: string[] = []
52
- let additiveCount = 0
53
- const destructiveSlugs: string[] = []
54
-
55
- for (const diff of diffs) {
56
- const seed = registry[diff.slug]
57
- if (!seed) continue
58
-
59
- if (!diff.tableExists) {
60
- lines.push(`-- ${diff.slug}: create table from scratch`)
61
- for (const stmt of planCreateSeed(seed)) { lines.push(stmt); additiveCount++ }
62
- lines.push('')
63
- continue
64
- }
65
-
66
- const missing = diff.columns.filter(c => c.status === 'missing')
67
- const idxMissing = diff.columns.filter(c => c.status === 'index_missing')
68
- const destructive = diff.columns.filter(c => DESTRUCTIVE.has(c.status))
69
-
70
- if (missing.length || idxMissing.length) {
71
- lines.push(`-- ${diff.slug}: additive changes`)
72
- for (const col of missing) {
73
- const branch = seed.branches.find(b => b.alias === col.name)
74
- if (branch) { lines.push(generateAddColumn(seed, branch)); additiveCount++ }
75
- }
76
- // generateIndexes is idempotent (CREATE INDEX IF NOT EXISTS) — re-emitting is safe.
77
- if (idxMissing.length) {
78
- for (const stmt of generateIndexes(seed)) { lines.push(stmt); additiveCount++ }
79
- }
80
- lines.push('')
81
- }
82
-
83
- if (destructive.length) {
84
- destructiveSlugs.push(diff.slug)
85
- lines.push(`-- ⚠ ${diff.slug}: DESTRUCTIVE drift NOT auto-migrated — review manually:`)
86
- for (const col of destructive) {
87
- lines.push(`-- ${col.status}: ${col.name}` +
88
- (col.actualType ? ` (db: ${col.actualType})` : ''))
89
- }
90
- lines.push('')
91
- }
92
- }
93
-
94
- return { sql: lines.join('\n').trimEnd() + '\n', additiveCount, destructiveSlugs }
95
- }
96
-
97
- /** Writes the migration file and returns its absolute path. */
98
- export function writeMigrationFile(
99
- migrationsDir: string, index: string, name: string, sql: string,
100
- ): string {
101
- if (!existsSync(migrationsDir)) mkdirSync(migrationsDir, { recursive: true })
102
- const safe = name.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_|_$/g, '') || 'schema_sync'
103
- const file = join(migrationsDir, `${index}_${safe}.sql`)
104
- writeFileSync(file, sql, 'utf-8')
105
- return file
106
- }
@@ -1,175 +0,0 @@
1
- // SPDX-License-Identifier: MIT
2
- // Copyright (c) 2024–2026 Flavio De Musso
3
-
4
- import pc from 'picocolors'
5
- import type { Seed } from '@beechcms/core'
6
- import { getExpectedColumns } from '@beechcms/core'
7
- import type { WranglerOptions, D1Row } from './wrangler.js'
8
- import { queryD1 } from './wrangler.js'
9
-
10
- interface PragmaRow extends D1Row {
11
- name: string
12
- type: string
13
- notnull: number
14
- pk: number
15
- }
16
-
17
- interface FkRow extends D1Row {
18
- id: number
19
- seq: number
20
- table: string
21
- from: string
22
- to: string
23
- on_update: string
24
- on_delete: string
25
- match: string
26
- }
27
-
28
- interface IndexRow extends D1Row {
29
- seq: number
30
- name: string
31
- unique: number
32
- }
33
-
34
- export interface ColumnDiff {
35
- name: string
36
- status: 'ok' | 'missing' | 'extra' | 'type_mismatch' | 'fk_missing' | 'fk_mismatch' | 'index_missing'
37
- expectedType?: string
38
- actualType?: string
39
- /** For fk_missing/fk_mismatch: expected FK target table */
40
- expectedTarget?: string
41
- /** For fk_mismatch: what the DB actually has */
42
- expected?: string
43
- actual?: string
44
- }
45
-
46
- export interface SeedDiff {
47
- slug: string
48
- tableExists: boolean
49
- columns: ColumnDiff[]
50
- }
51
-
52
- /** Returns true if the seed's table fully matches its Seed (no drift). */
53
- export function isSeedClean(diff: SeedDiff): boolean {
54
- return diff.tableExists && diff.columns.every(c => c.status === 'ok')
55
- }
56
-
57
- /** Human-readable drift report for one seed. Pure formatting — no I/O decisions. */
58
- export function renderSeedDiff(diff: SeedDiff): void {
59
- const table = `content_${diff.slug}`
60
- if (!diff.tableExists) { console.log(pc.red(` ✗ ${table} — table missing`)); return }
61
- const problems = diff.columns.filter(c => c.status !== 'ok')
62
- if (problems.length === 0) { console.log(pc.green(` ✓ ${table}`)); return }
63
- console.log(pc.yellow(` ⚠ ${table}`))
64
- for (const col of problems) {
65
- switch (col.status) {
66
- case 'missing': console.log(pc.red(` + missing column: ${col.name} ${col.expectedType}`)); break
67
- case 'extra': console.log(pc.dim(` ~ orphaned column: "${col.name}" (${col.actualType}) — in DB, not in seeds.ts`)); break
68
- case 'type_mismatch': console.log(pc.red(` ≠ type mismatch: ${col.name} (expected ${col.expectedType}, got ${col.actualType})`)); break
69
- case 'fk_missing': console.log(pc.red(` ⤬ missing FK: ${col.name} → content_${col.expectedTarget}(id)`)); break
70
- case 'fk_mismatch': console.log(pc.yellow(` ⤬ FK mismatch: ${col.name} expected ${col.expected}, got ${col.actual}`)); break
71
- case 'index_missing': console.log(pc.yellow(` ⊘ missing index on ${col.name}`)); break
72
- }
73
- }
74
- }
75
-
76
- export async function diffSeed(seed: Seed, options: WranglerOptions): Promise<SeedDiff> {
77
- const tableName = `content_${seed.slug}`
78
- const expected = getExpectedColumns(seed)
79
-
80
- let actual: PragmaRow[]
81
- try {
82
- actual = queryD1<PragmaRow>(`PRAGMA table_info(${tableName})`, options)
83
- } catch {
84
- return { slug: seed.slug, tableExists: false, columns: expected.map(c => ({ name: c.name, status: 'missing', expectedType: c.sqlType })) }
85
- }
86
-
87
- if (actual.length === 0) {
88
- return { slug: seed.slug, tableExists: false, columns: expected.map(c => ({ name: c.name, status: 'missing', expectedType: c.sqlType })) }
89
- }
90
-
91
- const actualMap = new Map<string, PragmaRow>(actual.map(r => [r.name, r]))
92
- const expectedSet = new Set<string>(expected.map(c => c.name))
93
-
94
- const columns: ColumnDiff[] = []
95
-
96
- // ── Column presence + type checks ────────────────────────────────────────
97
- for (const col of expected) {
98
- const actualRow = actualMap.get(col.name)
99
- if (!actualRow) {
100
- columns.push({ name: col.name, status: 'missing', expectedType: col.sqlType })
101
- } else if (actualRow.type.toUpperCase() !== col.sqlType) {
102
- columns.push({ name: col.name, status: 'type_mismatch', expectedType: col.sqlType, actualType: actualRow.type })
103
- } else {
104
- columns.push({ name: col.name, status: 'ok' })
105
- }
106
- }
107
-
108
- for (const row of actual) {
109
- if (!expectedSet.has(row.name)) {
110
- columns.push({ name: row.name, status: 'extra', actualType: row.type })
111
- }
112
- }
113
-
114
- // ── FK + index checks for relation branches ──────────────────────────────
115
- const relationBranches = seed.branches.filter(b => b.type === 'relation' && b.targetSeed)
116
-
117
- if (relationBranches.length > 0) {
118
- let fkList: FkRow[] = []
119
- let indexList: IndexRow[] = []
120
- try {
121
- fkList = queryD1<FkRow>(`PRAGMA foreign_key_list(${tableName})`, options)
122
- indexList = queryD1<IndexRow>(`PRAGMA index_list(${tableName})`, options)
123
- } catch {
124
- // If PRAGMA fails (table may not exist yet), skip FK checks
125
- }
126
-
127
- // Build maps for fast lookup
128
- // fkList has one row per FK column; `from` = local col, `table` = referenced table
129
- const fkByCol = new Map<string, FkRow>()
130
- for (const fk of fkList) {
131
- fkByCol.set(fk.from, fk)
132
- }
133
- const indexNames = new Set(indexList.map(i => i.name))
134
-
135
- for (const branch of relationBranches) {
136
- const expectedFkTable = `content_${branch.targetSeed}`
137
- const expectedOnDelete = (branch.onDelete ?? 'SET NULL').toUpperCase()
138
- const expectedIndexName = `idx_${seed.slug}_${branch.alias}`
139
-
140
- // Find column diff entry for this branch (already evaluated above)
141
- const colDiff = columns.find(c => c.name === branch.alias)
142
- if (!colDiff || colDiff.status === 'missing') continue // already flagged
143
-
144
- const fk = fkByCol.get(branch.alias)
145
-
146
- if (!fk) {
147
- // Column exists but no FK
148
- colDiff.status = 'fk_missing'
149
- colDiff.expectedTarget = branch.targetSeed
150
- } else {
151
- const actualTable = fk.table
152
- const actualOnDelete = fk.on_delete.toUpperCase()
153
- if (actualTable !== expectedFkTable || actualOnDelete !== expectedOnDelete) {
154
- colDiff.status = 'fk_mismatch'
155
- colDiff.expected = `→ ${expectedFkTable}(id) ON DELETE ${expectedOnDelete}`
156
- colDiff.actual = `→ ${actualTable}(id) ON DELETE ${actualOnDelete}`
157
- colDiff.expectedTarget = branch.targetSeed
158
- }
159
- }
160
-
161
- // Check index separately (can coexist with fk status)
162
- if (!indexNames.has(expectedIndexName)) {
163
- // Only add index_missing if the column is otherwise OK (FK issue takes precedence)
164
- if (colDiff.status === 'ok') {
165
- colDiff.status = 'index_missing'
166
- } else {
167
- // Append index info to the existing diff row as a separate entry
168
- columns.push({ name: branch.alias, status: 'index_missing' })
169
- }
170
- }
171
- }
172
- }
173
-
174
- return { slug: seed.slug, tableExists: true, columns }
175
- }
@@ -1,129 +0,0 @@
1
- // SPDX-License-Identifier: MIT
2
- // Copyright (c) 2024–2026 Flavio De Musso
3
-
4
- import { spawnSync, type SpawnSyncReturns } from 'node:child_process'
5
- import { writeFileSync, rmSync, existsSync, readFileSync } from 'node:fs'
6
- import { tmpdir } from 'node:os'
7
- import { join, resolve } from 'node:path'
8
-
9
- export interface WranglerOptions {
10
- db: string
11
- local: boolean
12
- configPath: string | null
13
- }
14
-
15
- export interface D1Row {
16
- [key: string]: unknown
17
- }
18
-
19
- interface WranglerResult {
20
- results: D1Row[]
21
- success: boolean
22
- error?: string
23
- }
24
-
25
- function buildArgs(options: WranglerOptions): string[] {
26
- const args: string[] = []
27
- if (options.configPath) args.push('--config', options.configPath)
28
- if (options.local) args.push('--local')
29
- else args.push('--remote')
30
- return args
31
- }
32
-
33
- /** Esegue SQL da file temporaneo via `wrangler d1 execute --file`. Returns true on success. */
34
- export function executeD1File(sql: string, options: WranglerOptions): boolean {
35
- const tmpFile = join(tmpdir(), `beech-seed-${Date.now()}.sql`)
36
- try {
37
- writeFileSync(tmpFile, sql, 'utf-8')
38
- const args = ['d1', 'execute', options.db, '--file', tmpFile, ...buildArgs(options)]
39
- const result = spawnSync('npx', ['wrangler', ...args], { stdio: 'inherit', cwd: process.cwd(), shell: true })
40
- return result.status === 0
41
- } finally {
42
- try { rmSync(tmpFile) } catch {}
43
- }
44
- }
45
-
46
- /**
47
- * Esegue una query SQL e ritorna i risultati come array di oggetti (--json).
48
- *
49
- * Passa il SQL via file temporaneo (`--file`) anziché `--command`: su Windows,
50
- * `spawnSync` con `shell: true` non preserva le virgolette/spazi/virgole interni
51
- * a un argomento inline, e wrangler riceve il comando spezzato in token sciolti
52
- * ("Unknown arguments: name, FROM, sqlite_master, …"). Il file evita del tutto
53
- * il riquoting della shell — stesso approccio di `executeD1File`.
54
- */
55
- export function queryD1<T extends D1Row = D1Row>(sql: string, options: WranglerOptions): T[] {
56
- const tmpFile = join(tmpdir(), `beech-query-${Date.now()}.sql`)
57
- let result: SpawnSyncReturns<string>
58
- try {
59
- writeFileSync(tmpFile, sql, 'utf-8')
60
- const args = ['d1', 'execute', options.db, '--file', tmpFile, '--json', ...buildArgs(options)]
61
- result = spawnSync('npx', ['wrangler', ...args], { encoding: 'utf-8', cwd: process.cwd(), shell: true })
62
- } finally {
63
- try { rmSync(tmpFile) } catch {}
64
- }
65
-
66
- if (result.status !== 0) {
67
- throw new Error(`wrangler d1 execute failed:\n${result.stderr}`)
68
- }
69
-
70
- try {
71
- const parsed: WranglerResult[] = JSON.parse(result.stdout)
72
- return (parsed[0]?.results ?? []) as T[]
73
- } catch {
74
- throw new Error(`Failed to parse wrangler JSON output:\n${result.stdout}`)
75
- }
76
- }
77
-
78
- /** Trova il path di wrangler.jsonc risalendo l'albero da CWD fino alla root del filesystem. */
79
- export function findWranglerConfig(): string | null {
80
- let dir = process.cwd()
81
- while (true) {
82
- for (const name of ['wrangler.jsonc', 'wrangler.json', 'wrangler.toml']) {
83
- const p = resolve(dir, name)
84
- if (existsSync(p)) return p
85
- }
86
- for (const name of ['wrangler.jsonc', 'wrangler.json', 'wrangler.toml']) {
87
- const p = resolve(dir, 'apps', 'api', name)
88
- if (existsSync(p)) return p
89
- }
90
- const parent = resolve(dir, '..')
91
- if (parent === dir) break
92
- dir = parent
93
- }
94
- return null
95
- }
96
-
97
- /** Wraps a string value in single quotes, escaping internal single quotes for SQL literals. */
98
- export function sqlQuote(value: string): string {
99
- return `'${value.replace(/'/g, "''")}'`
100
- }
101
-
102
- /** Risolve il nome del database D1 da wrangler.jsonc (stripping JSONC comments). */
103
- export function resolveDbName(configPath: string | null): string {
104
- if (!configPath) return 'beech-db'
105
- try {
106
- const raw = readFileSync(configPath, 'utf-8')
107
-
108
- if (configPath.endsWith('.toml')) {
109
- // Basic regex-based TOML parsing for d1_databases
110
- // Matches both [d1_databases] and [[d1_databases]]
111
- const d1SectionMatch = raw.match(/\[\[?d1_databases\]\]?[\s\S]*?(?=\n\[|$)/)
112
- if (d1SectionMatch) {
113
- const section = d1SectionMatch[0]
114
- const dbNameMatch = section.match(/database_name\s*=\s*["'](.+?)["']/)
115
- if (dbNameMatch) return dbNameMatch[1]
116
- }
117
- return 'beech-db'
118
- }
119
-
120
- const stripped = raw
121
- .replace(/\/\/[^\n]*/g, '')
122
- .replace(/\/\*[\s\S]*?\*\//g, '')
123
- const parsed = JSON.parse(stripped)
124
- const bindings: { database_name?: string }[] = parsed?.d1_databases ?? []
125
- return bindings[0]?.database_name ?? 'beech-db'
126
- } catch {
127
- return 'beech-db'
128
- }
129
- }
@@ -1,58 +0,0 @@
1
- // SPDX-License-Identifier: MIT
2
- // Copyright (c) 2024–2026 Flavio De Musso
3
-
4
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
5
- import { existsSync, readFileSync, rmSync } from 'node:fs'
6
- import { resolve } from 'node:path'
7
- import { tmpdir } from 'node:os'
8
- import type { Seed } from '@beechcms/core'
9
- import { generateTypes } from '../commands/generate-types.js'
10
-
11
- const ARTICLES_SEED: Seed = {
12
- slug: 'articles',
13
- label: 'Articles',
14
- displayNameAlias: 'title',
15
- branches: [
16
- { alias: 'title', label: 'Title', type: 'text', requiredOnCreate: true },
17
- { alias: 'body', label: 'Body', type: 'richtext' },
18
- ],
19
- } as Seed
20
-
21
- const registry: Record<string, Seed> = { articles: ARTICLES_SEED }
22
-
23
- const outPath = resolve(tmpdir(), `beech-test-${Date.now()}.ts`)
24
-
25
- afterEach(() => {
26
- if (existsSync(outPath)) rmSync(outPath)
27
- })
28
-
29
- describe('generateTypes — local registry (injected)', () => {
30
- it('writes the output file', async () => {
31
- await generateTypes({ out: outPath, local: true, registry })
32
- expect(existsSync(outPath)).toBe(true)
33
- })
34
-
35
- it('output contains the interface', async () => {
36
- await generateTypes({ out: outPath, local: true, registry })
37
- const content = readFileSync(outPath, 'utf-8')
38
- expect(content).toContain('export interface Articles {')
39
- })
40
-
41
- it('output starts with the auto-generated banner', async () => {
42
- await generateTypes({ out: outPath, local: true, registry })
43
- const content = readFileSync(outPath, 'utf-8')
44
- expect(content).toContain('generato automaticamente')
45
- })
46
-
47
- it('output contains SeedRegistryTypes', async () => {
48
- await generateTypes({ out: outPath, local: true, registry })
49
- const content = readFileSync(outPath, 'utf-8')
50
- expect(content).toContain('export interface SeedRegistryTypes {')
51
- })
52
-
53
- it('exits with an error when registry is empty', async () => {
54
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => { throw new Error('exit') })
55
- await expect(generateTypes({ out: outPath, local: true, registry: {} })).rejects.toThrow('exit')
56
- exitSpy.mockRestore()
57
- })
58
- })