@sqldoc/sqldoc 0.0.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.
- package/package.json +37 -0
- package/src/__tests__/detect-pm.test.ts +67 -0
- package/src/__tests__/find-sqldoc.test.ts +60 -0
- package/src/__tests__/generate-config-types.test.ts +121 -0
- package/src/__tests__/init.test.ts +117 -0
- package/src/__tests__/prompt.test.ts +164 -0
- package/src/__tests__/scaffold.test.ts +115 -0
- package/src/commands/add.ts +48 -0
- package/src/commands/init.ts +210 -0
- package/src/commands/upgrade.ts +45 -0
- package/src/delegate.ts +44 -0
- package/src/detect-pm.ts +17 -0
- package/src/find-sqldoc.ts +22 -0
- package/src/generate-config-types.ts +77 -0
- package/src/index.ts +127 -0
- package/src/prompt.ts +92 -0
- package/src/runtime.ts +38 -0
- package/src/scaffold.ts +231 -0
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "module",
|
|
3
|
+
"name": "@sqldoc/sqldoc",
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"description": "Global CLI shim for sqldoc -- finds .sqldoc/ and delegates to project-local @sqldoc/cli",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./src/index.ts",
|
|
9
|
+
"import": "./src/index.ts",
|
|
10
|
+
"default": "./src/index.ts"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"main": "./src/index.ts",
|
|
14
|
+
"types": "./src/index.ts",
|
|
15
|
+
"bin": {
|
|
16
|
+
"sqldoc": "./src/index.ts"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"src",
|
|
20
|
+
"package.json"
|
|
21
|
+
],
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=18"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"picocolors": "^1.1.1"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "^25.5.0",
|
|
30
|
+
"typescript": "^5.9.3",
|
|
31
|
+
"vitest": "^4.1.0"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"test": "vitest run",
|
|
35
|
+
"typecheck": "tsc --noEmit"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { tmpdir } from 'node:os'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import { afterEach, describe, expect, it } from 'vitest'
|
|
5
|
+
import { detectPM } from '../detect-pm.ts'
|
|
6
|
+
|
|
7
|
+
describe('detectPM', () => {
|
|
8
|
+
const tempDirs: string[] = []
|
|
9
|
+
|
|
10
|
+
function makeTempDir(): string {
|
|
11
|
+
const dir = mkdtempSync(join(tmpdir(), 'sqldoc-pm-test-'))
|
|
12
|
+
tempDirs.push(dir)
|
|
13
|
+
return dir
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
for (const dir of tempDirs) {
|
|
18
|
+
rmSync(dir, { recursive: true, force: true })
|
|
19
|
+
}
|
|
20
|
+
tempDirs.length = 0
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it("returns 'pnpm' when pnpm-lock.yaml exists", () => {
|
|
24
|
+
const root = makeTempDir()
|
|
25
|
+
writeFileSync(join(root, 'pnpm-lock.yaml'), '')
|
|
26
|
+
expect(detectPM(root)).toBe('pnpm')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it("returns 'yarn' when yarn.lock exists", () => {
|
|
30
|
+
const root = makeTempDir()
|
|
31
|
+
writeFileSync(join(root, 'yarn.lock'), '')
|
|
32
|
+
expect(detectPM(root)).toBe('yarn')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it("returns 'bun' when bun.lockb exists", () => {
|
|
36
|
+
const root = makeTempDir()
|
|
37
|
+
writeFileSync(join(root, 'bun.lockb'), '')
|
|
38
|
+
expect(detectPM(root)).toBe('bun')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it("returns 'bun' when bun.lock exists", () => {
|
|
42
|
+
const root = makeTempDir()
|
|
43
|
+
writeFileSync(join(root, 'bun.lock'), '')
|
|
44
|
+
expect(detectPM(root)).toBe('bun')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it("returns 'npm' when package-lock.json exists", () => {
|
|
48
|
+
const root = makeTempDir()
|
|
49
|
+
writeFileSync(join(root, 'package-lock.json'), '')
|
|
50
|
+
expect(detectPM(root)).toBe('npm')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it("returns 'npm' as fallback when no lockfile found", () => {
|
|
54
|
+
const root = makeTempDir()
|
|
55
|
+
expect(detectPM(root)).toBe('npm')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('checks in priority order: pnpm > yarn > bun > npm', () => {
|
|
59
|
+
const root = makeTempDir()
|
|
60
|
+
// When all lockfiles exist, pnpm should win
|
|
61
|
+
writeFileSync(join(root, 'pnpm-lock.yaml'), '')
|
|
62
|
+
writeFileSync(join(root, 'yarn.lock'), '')
|
|
63
|
+
writeFileSync(join(root, 'bun.lockb'), '')
|
|
64
|
+
writeFileSync(join(root, 'package-lock.json'), '')
|
|
65
|
+
expect(detectPM(root)).toBe('pnpm')
|
|
66
|
+
})
|
|
67
|
+
})
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, rmSync } from 'node:fs'
|
|
2
|
+
import { tmpdir } from 'node:os'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import { afterEach, describe, expect, it } from 'vitest'
|
|
5
|
+
import { findSqldocDir } from '../find-sqldoc.ts'
|
|
6
|
+
|
|
7
|
+
describe('findSqldocDir', () => {
|
|
8
|
+
const tempDirs: string[] = []
|
|
9
|
+
|
|
10
|
+
function makeTempDir(): string {
|
|
11
|
+
const dir = mkdtempSync(join(tmpdir(), 'sqldoc-test-'))
|
|
12
|
+
tempDirs.push(dir)
|
|
13
|
+
return dir
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
for (const dir of tempDirs) {
|
|
18
|
+
rmSync(dir, { recursive: true, force: true })
|
|
19
|
+
}
|
|
20
|
+
tempDirs.length = 0
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('returns absolute path when .sqldoc/ exists in startDir', () => {
|
|
24
|
+
const root = makeTempDir()
|
|
25
|
+
const sqldocDir = join(root, '.sqldoc')
|
|
26
|
+
mkdirSync(sqldocDir)
|
|
27
|
+
|
|
28
|
+
const result = findSqldocDir(root)
|
|
29
|
+
expect(result).toBe(sqldocDir)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('returns absolute path when .sqldoc/ exists in a parent directory', () => {
|
|
33
|
+
const root = makeTempDir()
|
|
34
|
+
const sqldocDir = join(root, '.sqldoc')
|
|
35
|
+
mkdirSync(sqldocDir)
|
|
36
|
+
|
|
37
|
+
const nested = join(root, 'a', 'b', 'c')
|
|
38
|
+
mkdirSync(nested, { recursive: true })
|
|
39
|
+
|
|
40
|
+
const result = findSqldocDir(nested)
|
|
41
|
+
expect(result).toBe(sqldocDir)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('returns null when .sqldoc/ does not exist up to filesystem root', () => {
|
|
45
|
+
const root = makeTempDir()
|
|
46
|
+
const nested = join(root, 'no-sqldoc', 'deep')
|
|
47
|
+
mkdirSync(nested, { recursive: true })
|
|
48
|
+
|
|
49
|
+
// Start from a deep dir with no .sqldoc anywhere above
|
|
50
|
+
// Use the temp dir itself (no .sqldoc) -- will walk up to / and find nothing
|
|
51
|
+
const result = findSqldocDir(nested)
|
|
52
|
+
expect(result).toBeNull()
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('uses process.cwd() as default startDir', () => {
|
|
56
|
+
// Just verify it doesn't throw when called without args
|
|
57
|
+
const result = findSqldocDir()
|
|
58
|
+
expect(result === null || typeof result === 'string').toBe(true)
|
|
59
|
+
})
|
|
60
|
+
})
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { tmpdir } from 'node:os'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import { afterEach, describe, expect, it } from 'vitest'
|
|
5
|
+
import { generateConfigTypes } from '../generate-config-types.ts'
|
|
6
|
+
|
|
7
|
+
describe('generateConfigTypes', () => {
|
|
8
|
+
const tempDirs: string[] = []
|
|
9
|
+
|
|
10
|
+
function makeTempDir(): string {
|
|
11
|
+
const dir = mkdtempSync(join(tmpdir(), 'sqldoc-config-types-test-'))
|
|
12
|
+
tempDirs.push(dir)
|
|
13
|
+
return dir
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
for (const dir of tempDirs) {
|
|
18
|
+
rmSync(dir, { recursive: true, force: true })
|
|
19
|
+
}
|
|
20
|
+
tempDirs.length = 0
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('does nothing when node_modules/@sqldoc does not exist', () => {
|
|
24
|
+
const sqldocDir = makeTempDir()
|
|
25
|
+
generateConfigTypes(sqldocDir)
|
|
26
|
+
expect(existsSync(join(sqldocDir, 'config.d.ts'))).toBe(false)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('generates config.d.ts with base fields when no ns-* packages', () => {
|
|
30
|
+
const sqldocDir = makeTempDir()
|
|
31
|
+
mkdirSync(join(sqldocDir, 'node_modules', '@sqldoc'), { recursive: true })
|
|
32
|
+
|
|
33
|
+
generateConfigTypes(sqldocDir)
|
|
34
|
+
|
|
35
|
+
const content = readFileSync(join(sqldocDir, 'config.d.ts'), 'utf-8')
|
|
36
|
+
expect(content).toContain('Auto-generated by sqldoc')
|
|
37
|
+
expect(content).toContain('interface ProjectConfig extends BaseProjectConfig')
|
|
38
|
+
expect(content).toContain('export type SqldocConfig = ProjectConfig | ProjectConfig[]')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('detects ns-* packages and adds namespace fields', () => {
|
|
42
|
+
const sqldocDir = makeTempDir()
|
|
43
|
+
const nsDir = join(sqldocDir, 'node_modules', '@sqldoc', 'ns-audit')
|
|
44
|
+
mkdirSync(nsDir, { recursive: true })
|
|
45
|
+
writeFileSync(join(nsDir, 'package.json'), JSON.stringify({ name: '@sqldoc/ns-audit' }))
|
|
46
|
+
|
|
47
|
+
generateConfigTypes(sqldocDir)
|
|
48
|
+
|
|
49
|
+
const content = readFileSync(join(sqldocDir, 'config.d.ts'), 'utf-8')
|
|
50
|
+
expect(content).toContain('namespaces?:')
|
|
51
|
+
expect(content).toContain('audit?: Record<string, unknown>')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('picks up exported Config types from namespace source', () => {
|
|
55
|
+
const sqldocDir = makeTempDir()
|
|
56
|
+
const nsDir = join(sqldocDir, 'node_modules', '@sqldoc', 'ns-docs')
|
|
57
|
+
const srcDir = join(nsDir, 'src')
|
|
58
|
+
mkdirSync(srcDir, { recursive: true })
|
|
59
|
+
writeFileSync(join(nsDir, 'package.json'), JSON.stringify({ name: '@sqldoc/ns-docs' }))
|
|
60
|
+
writeFileSync(
|
|
61
|
+
join(srcDir, 'index.ts'),
|
|
62
|
+
`
|
|
63
|
+
export default plugin
|
|
64
|
+
export type { DocsConfig }
|
|
65
|
+
`,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
generateConfigTypes(sqldocDir)
|
|
69
|
+
|
|
70
|
+
const content = readFileSync(join(sqldocDir, 'config.d.ts'), 'utf-8')
|
|
71
|
+
expect(content).toContain("import type { DocsConfig } from '@sqldoc/ns-docs'")
|
|
72
|
+
expect(content).toContain('docs?: Partial<DocsConfig>')
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('handles multiple namespace packages sorted alphabetically', () => {
|
|
76
|
+
const sqldocDir = makeTempDir()
|
|
77
|
+
const sqldocNm = join(sqldocDir, 'node_modules', '@sqldoc')
|
|
78
|
+
|
|
79
|
+
// Create ns-validate (no config type)
|
|
80
|
+
const validateDir = join(sqldocNm, 'ns-validate')
|
|
81
|
+
mkdirSync(validateDir, { recursive: true })
|
|
82
|
+
writeFileSync(join(validateDir, 'package.json'), JSON.stringify({ name: '@sqldoc/ns-validate' }))
|
|
83
|
+
|
|
84
|
+
// Create ns-codegen (with config type)
|
|
85
|
+
const codegenDir = join(sqldocNm, 'ns-codegen')
|
|
86
|
+
const codegenSrc = join(codegenDir, 'src')
|
|
87
|
+
mkdirSync(codegenSrc, { recursive: true })
|
|
88
|
+
writeFileSync(join(codegenDir, 'package.json'), JSON.stringify({ name: '@sqldoc/ns-codegen' }))
|
|
89
|
+
writeFileSync(join(codegenSrc, 'index.ts'), `export type { CodegenConfig } from './types'`)
|
|
90
|
+
|
|
91
|
+
generateConfigTypes(sqldocDir)
|
|
92
|
+
|
|
93
|
+
const content = readFileSync(join(sqldocDir, 'config.d.ts'), 'utf-8')
|
|
94
|
+
// codegen comes before validate alphabetically
|
|
95
|
+
expect(content).toContain("import type { CodegenConfig } from '@sqldoc/ns-codegen'")
|
|
96
|
+
expect(content).toContain('codegen?: Partial<CodegenConfig>')
|
|
97
|
+
expect(content).toContain('validate?: Record<string, unknown>')
|
|
98
|
+
|
|
99
|
+
// codegen should appear before validate in the file
|
|
100
|
+
const codegenIdx = content.indexOf('codegen?:')
|
|
101
|
+
const validateIdx = content.indexOf('validate?:')
|
|
102
|
+
expect(codegenIdx).toBeLessThan(validateIdx)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('skips non-ns packages like cli and templates', () => {
|
|
106
|
+
const sqldocDir = makeTempDir()
|
|
107
|
+
const sqldocNm = join(sqldocDir, 'node_modules', '@sqldoc')
|
|
108
|
+
|
|
109
|
+
mkdirSync(join(sqldocNm, 'cli'), { recursive: true })
|
|
110
|
+
writeFileSync(join(sqldocNm, 'cli', 'package.json'), JSON.stringify({ name: '@sqldoc/cli' }))
|
|
111
|
+
|
|
112
|
+
mkdirSync(join(sqldocNm, 'templates'), { recursive: true })
|
|
113
|
+
writeFileSync(join(sqldocNm, 'templates', 'package.json'), JSON.stringify({ name: '@sqldoc/templates' }))
|
|
114
|
+
|
|
115
|
+
generateConfigTypes(sqldocDir)
|
|
116
|
+
|
|
117
|
+
const content = readFileSync(join(sqldocDir, 'config.d.ts'), 'utf-8')
|
|
118
|
+
expect(content).not.toContain('cli')
|
|
119
|
+
expect(content).not.toContain('templates')
|
|
120
|
+
})
|
|
121
|
+
})
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { tmpdir } from 'node:os'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
5
|
+
|
|
6
|
+
describe('initCommand', () => {
|
|
7
|
+
const tempDirs: string[] = []
|
|
8
|
+
let exitSpy: ReturnType<typeof vi.spyOn>
|
|
9
|
+
let consoleLogSpy: ReturnType<typeof vi.spyOn>
|
|
10
|
+
let consoleErrorSpy: ReturnType<typeof vi.spyOn>
|
|
11
|
+
|
|
12
|
+
function makeTempDir(): string {
|
|
13
|
+
const dir = mkdtempSync(join(tmpdir(), 'sqldoc-init-test-'))
|
|
14
|
+
tempDirs.push(dir)
|
|
15
|
+
return dir
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Create a minimal fake repo so devPath mode has something to link */
|
|
19
|
+
function makeFakeRepo(): string {
|
|
20
|
+
const repo = makeTempDir()
|
|
21
|
+
const pkgDir = join(repo, 'packages', 'cli')
|
|
22
|
+
mkdirSync(pkgDir, { recursive: true })
|
|
23
|
+
writeFileSync(join(pkgDir, 'package.json'), JSON.stringify({ name: '@sqldoc/cli' }))
|
|
24
|
+
return repo
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => {}) as any)
|
|
29
|
+
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
30
|
+
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
for (const dir of tempDirs) {
|
|
35
|
+
rmSync(dir, { recursive: true, force: true })
|
|
36
|
+
}
|
|
37
|
+
tempDirs.length = 0
|
|
38
|
+
exitSpy.mockRestore()
|
|
39
|
+
consoleLogSpy.mockRestore()
|
|
40
|
+
consoleErrorSpy.mockRestore()
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('creates .sqldoc/ directory', async () => {
|
|
44
|
+
const root = makeTempDir()
|
|
45
|
+
const repo = makeFakeRepo()
|
|
46
|
+
const { initCommand } = await import('../commands/init.ts')
|
|
47
|
+
await initCommand(root, repo)
|
|
48
|
+
|
|
49
|
+
expect(existsSync(join(root, '.sqldoc'))).toBe(true)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('creates .sqldoc/package.json with correct structure', async () => {
|
|
53
|
+
const root = makeTempDir()
|
|
54
|
+
const repo = makeFakeRepo()
|
|
55
|
+
const { initCommand } = await import('../commands/init.ts')
|
|
56
|
+
await initCommand(root, repo)
|
|
57
|
+
|
|
58
|
+
const pkgPath = join(root, '.sqldoc', 'package.json')
|
|
59
|
+
expect(existsSync(pkgPath)).toBe(true)
|
|
60
|
+
|
|
61
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
|
|
62
|
+
expect(pkg.name).toBe('sqldoc-local')
|
|
63
|
+
expect(pkg.private).toBe(true)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('creates .sqldoc/.gitignore containing node_modules/', async () => {
|
|
67
|
+
const root = makeTempDir()
|
|
68
|
+
const repo = makeFakeRepo()
|
|
69
|
+
const { initCommand } = await import('../commands/init.ts')
|
|
70
|
+
await initCommand(root, repo)
|
|
71
|
+
|
|
72
|
+
const gitignorePath = join(root, '.sqldoc', '.gitignore')
|
|
73
|
+
expect(existsSync(gitignorePath)).toBe(true)
|
|
74
|
+
|
|
75
|
+
const content = readFileSync(gitignorePath, 'utf-8')
|
|
76
|
+
expect(content).toContain('node_modules/')
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('errors when .sqldoc/ already exists', async () => {
|
|
80
|
+
const root = makeTempDir()
|
|
81
|
+
mkdirSync(join(root, '.sqldoc'))
|
|
82
|
+
|
|
83
|
+
const { initCommand } = await import('../commands/init.ts')
|
|
84
|
+
await initCommand(root)
|
|
85
|
+
|
|
86
|
+
expect(exitSpy).toHaveBeenCalledWith(1)
|
|
87
|
+
const errorOutput = consoleErrorSpy.mock.calls.map((c: any[]) => String(c[0])).join(' ')
|
|
88
|
+
expect(errorOutput).toContain('already exists')
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('creates sqldoc.config.ts when none exists', async () => {
|
|
92
|
+
const root = makeTempDir()
|
|
93
|
+
const repo = makeFakeRepo()
|
|
94
|
+
const { initCommand } = await import('../commands/init.ts')
|
|
95
|
+
await initCommand(root, repo)
|
|
96
|
+
|
|
97
|
+
const configPath = join(root, 'sqldoc.config.ts')
|
|
98
|
+
expect(existsSync(configPath)).toBe(true)
|
|
99
|
+
|
|
100
|
+
const content = readFileSync(configPath, 'utf-8')
|
|
101
|
+
expect(content).toContain('export default')
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('does not overwrite existing sqldoc.config.ts', async () => {
|
|
105
|
+
const root = makeTempDir()
|
|
106
|
+
const repo = makeFakeRepo()
|
|
107
|
+
const configPath = join(root, 'sqldoc.config.ts')
|
|
108
|
+
const originalContent = 'export default { custom: true }\n'
|
|
109
|
+
writeFileSync(configPath, originalContent)
|
|
110
|
+
|
|
111
|
+
const { initCommand } = await import('../commands/init.ts')
|
|
112
|
+
await initCommand(root, repo)
|
|
113
|
+
|
|
114
|
+
const content = readFileSync(configPath, 'utf-8')
|
|
115
|
+
expect(content).toBe(originalContent)
|
|
116
|
+
})
|
|
117
|
+
})
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import * as readline from 'node:readline'
|
|
2
|
+
import { Readable, Writable } from 'node:stream'
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
4
|
+
import { promptCheckbox, promptConfirm, promptSelect } from '../prompt.ts'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Create a readline interface backed by a mock input stream.
|
|
8
|
+
* Call `type(answer)` to feed the answer before the prompt reads it.
|
|
9
|
+
*/
|
|
10
|
+
function mockRL() {
|
|
11
|
+
const input = new Readable({ read() {} })
|
|
12
|
+
const output = new Writable({
|
|
13
|
+
write(_chunk, _enc, cb) {
|
|
14
|
+
cb()
|
|
15
|
+
},
|
|
16
|
+
})
|
|
17
|
+
const rl = readline.createInterface({ input, output })
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
rl,
|
|
21
|
+
type(answer: string) {
|
|
22
|
+
// Push the answer followed by newline into the readable stream
|
|
23
|
+
input.push(`${answer}\n`)
|
|
24
|
+
},
|
|
25
|
+
close() {
|
|
26
|
+
rl.close()
|
|
27
|
+
input.destroy()
|
|
28
|
+
},
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('promptSelect', () => {
|
|
33
|
+
let consoleSpy: ReturnType<typeof vi.spyOn>
|
|
34
|
+
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
consoleSpy.mockRestore()
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('returns default when user presses Enter', async () => {
|
|
44
|
+
const { rl, type, close } = mockRL()
|
|
45
|
+
const p = promptSelect(
|
|
46
|
+
rl,
|
|
47
|
+
'Pick:',
|
|
48
|
+
[
|
|
49
|
+
{ value: 'a', label: 'Option A' },
|
|
50
|
+
{ value: 'b', label: 'Option B' },
|
|
51
|
+
],
|
|
52
|
+
'b',
|
|
53
|
+
)
|
|
54
|
+
type('')
|
|
55
|
+
const result = await p
|
|
56
|
+
expect(result).toBe('b')
|
|
57
|
+
close()
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('returns selected option by number', async () => {
|
|
61
|
+
const { rl, type, close } = mockRL()
|
|
62
|
+
const p = promptSelect(
|
|
63
|
+
rl,
|
|
64
|
+
'Pick:',
|
|
65
|
+
[
|
|
66
|
+
{ value: 'a', label: 'Option A' },
|
|
67
|
+
{ value: 'b', label: 'Option B' },
|
|
68
|
+
],
|
|
69
|
+
'a',
|
|
70
|
+
)
|
|
71
|
+
type('2')
|
|
72
|
+
const result = await p
|
|
73
|
+
expect(result).toBe('b')
|
|
74
|
+
close()
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('returns default for invalid input', async () => {
|
|
78
|
+
const { rl, type, close } = mockRL()
|
|
79
|
+
const p = promptSelect(rl, 'Pick:', [{ value: 'a', label: 'Option A' }], 'a')
|
|
80
|
+
type('99')
|
|
81
|
+
const result = await p
|
|
82
|
+
expect(result).toBe('a')
|
|
83
|
+
close()
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
describe('promptCheckbox', () => {
|
|
88
|
+
let consoleSpy: ReturnType<typeof vi.spyOn>
|
|
89
|
+
|
|
90
|
+
beforeEach(() => {
|
|
91
|
+
consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
afterEach(() => {
|
|
95
|
+
consoleSpy.mockRestore()
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('returns defaults when user presses Enter', async () => {
|
|
99
|
+
const { rl, type, close } = mockRL()
|
|
100
|
+
const p = promptCheckbox(rl, 'Pick:', [
|
|
101
|
+
{ value: 'a', label: 'A', checked: true },
|
|
102
|
+
{ value: 'b', label: 'B', checked: false },
|
|
103
|
+
{ value: 'c', label: 'C', checked: true },
|
|
104
|
+
])
|
|
105
|
+
type('')
|
|
106
|
+
const result = await p
|
|
107
|
+
expect(result).toEqual(['a', 'c'])
|
|
108
|
+
close()
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('returns selected items by number', async () => {
|
|
112
|
+
const { rl, type, close } = mockRL()
|
|
113
|
+
const p = promptCheckbox(rl, 'Pick:', [
|
|
114
|
+
{ value: 'a', label: 'A' },
|
|
115
|
+
{ value: 'b', label: 'B' },
|
|
116
|
+
{ value: 'c', label: 'C' },
|
|
117
|
+
])
|
|
118
|
+
type('1,3')
|
|
119
|
+
const result = await p
|
|
120
|
+
expect(result).toEqual(['a', 'c'])
|
|
121
|
+
close()
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('handles spaces in comma-separated input', async () => {
|
|
125
|
+
const { rl, type, close } = mockRL()
|
|
126
|
+
const p = promptCheckbox(rl, 'Pick:', [
|
|
127
|
+
{ value: 'a', label: 'A' },
|
|
128
|
+
{ value: 'b', label: 'B' },
|
|
129
|
+
])
|
|
130
|
+
type('1, 2')
|
|
131
|
+
const result = await p
|
|
132
|
+
expect(result).toEqual(['a', 'b'])
|
|
133
|
+
close()
|
|
134
|
+
})
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
describe('promptConfirm', () => {
|
|
138
|
+
it('returns true on empty input (Enter)', async () => {
|
|
139
|
+
const { rl, type, close } = mockRL()
|
|
140
|
+
const p = promptConfirm(rl, 'Continue?')
|
|
141
|
+
type('')
|
|
142
|
+
const result = await p
|
|
143
|
+
expect(result).toBe(true)
|
|
144
|
+
close()
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('returns true on "y"', async () => {
|
|
148
|
+
const { rl, type, close } = mockRL()
|
|
149
|
+
const p = promptConfirm(rl, 'Continue?')
|
|
150
|
+
type('y')
|
|
151
|
+
const result = await p
|
|
152
|
+
expect(result).toBe(true)
|
|
153
|
+
close()
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('returns false on "n"', async () => {
|
|
157
|
+
const { rl, type, close } = mockRL()
|
|
158
|
+
const p = promptConfirm(rl, 'Continue?')
|
|
159
|
+
type('n')
|
|
160
|
+
const result = await p
|
|
161
|
+
expect(result).toBe(false)
|
|
162
|
+
close()
|
|
163
|
+
})
|
|
164
|
+
})
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { namespacesToInstall, scaffoldConfig, scaffoldExample } from '../scaffold.ts'
|
|
3
|
+
|
|
4
|
+
describe('scaffoldConfig', () => {
|
|
5
|
+
it('generates minimal config with default postgres dialect', () => {
|
|
6
|
+
const result = scaffoldConfig({ dialect: 'postgres', namespaces: [], templates: [] })
|
|
7
|
+
expect(result).toContain("import type { SqldocConfig } from './.sqldoc/config'")
|
|
8
|
+
expect(result).toContain('const config: SqldocConfig')
|
|
9
|
+
expect(result).toContain('export default config')
|
|
10
|
+
// Dialect is always emitted (mandatory field)
|
|
11
|
+
expect(result).toContain("dialect: 'postgres'")
|
|
12
|
+
// Should have devUrl hint
|
|
13
|
+
expect(result).toContain('// devUrl:')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('sets dialect for non-default', () => {
|
|
17
|
+
const result = scaffoldConfig({ dialect: 'mysql', namespaces: [], templates: [] })
|
|
18
|
+
expect(result).toContain("dialect: 'mysql'")
|
|
19
|
+
expect(result).toContain('mysql://localhost')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('includes namespace config sections', () => {
|
|
23
|
+
const result = scaffoldConfig({ dialect: 'postgres', namespaces: ['audit', 'validate'], templates: [] })
|
|
24
|
+
expect(result).toContain('namespaces: {')
|
|
25
|
+
expect(result).toContain('audit: {}')
|
|
26
|
+
expect(result).toContain('validate: {}')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('includes docs config with defaults', () => {
|
|
30
|
+
const result = scaffoldConfig({ dialect: 'postgres', namespaces: ['docs'], templates: [] })
|
|
31
|
+
expect(result).toContain("format: 'html'")
|
|
32
|
+
expect(result).toContain("output: 'docs/schema.html'")
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('includes codegen config with templates', () => {
|
|
36
|
+
const result = scaffoldConfig({ dialect: 'postgres', namespaces: ['codegen'], templates: ['typescript', 'zod'] })
|
|
37
|
+
expect(result).toContain('templates: [')
|
|
38
|
+
expect(result).toContain("'@sqldoc/templates/typescript'")
|
|
39
|
+
expect(result).toContain("'@sqldoc/templates/zod'")
|
|
40
|
+
expect(result).toContain("'generated/types.ts'")
|
|
41
|
+
expect(result).toContain("'generated/schemas.ts'")
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('generates sqlite config', () => {
|
|
45
|
+
const result = scaffoldConfig({ dialect: 'sqlite', namespaces: [], templates: [] })
|
|
46
|
+
expect(result).toContain("dialect: 'sqlite'")
|
|
47
|
+
expect(result).toContain('sqlite://dev.db')
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
describe('scaffoldExample', () => {
|
|
52
|
+
it('generates imports for selected namespaces', () => {
|
|
53
|
+
const result = scaffoldExample({ dialect: 'postgres', namespaces: ['audit', 'validate'], templates: [] })
|
|
54
|
+
expect(result).toContain('-- @import @sqldoc/ns-audit')
|
|
55
|
+
expect(result).toContain('-- @import @sqldoc/ns-validate')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('includes table-level tags', () => {
|
|
59
|
+
const result = scaffoldExample({ dialect: 'postgres', namespaces: ['audit', 'rls'], templates: [] })
|
|
60
|
+
expect(result).toContain('-- @audit')
|
|
61
|
+
expect(result).toContain('-- @rls')
|
|
62
|
+
expect(result).toContain('CREATE TABLE users')
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('includes column-level tags', () => {
|
|
66
|
+
const result = scaffoldExample({ dialect: 'postgres', namespaces: ['validate'], templates: [] })
|
|
67
|
+
expect(result).toContain('@validate.check')
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('includes anon tags', () => {
|
|
71
|
+
const result = scaffoldExample({ dialect: 'postgres', namespaces: ['anon'], templates: [] })
|
|
72
|
+
expect(result).toContain('@anon.mask')
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('adapts to mysql dialect', () => {
|
|
76
|
+
const result = scaffoldExample({ dialect: 'mysql', namespaces: ['validate'], templates: [] })
|
|
77
|
+
expect(result).toContain('INT AUTO_INCREMENT')
|
|
78
|
+
expect(result).toContain('VARCHAR(255)')
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('adapts to sqlite dialect', () => {
|
|
82
|
+
const result = scaffoldExample({ dialect: 'sqlite', namespaces: ['validate'], templates: [] })
|
|
83
|
+
expect(result).toContain('INTEGER')
|
|
84
|
+
expect(result).toContain('TEXT DEFAULT CURRENT_TIMESTAMP')
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('generates rls policy example for postgres', () => {
|
|
88
|
+
const result = scaffoldExample({ dialect: 'postgres', namespaces: ['rls'], templates: [] })
|
|
89
|
+
expect(result).toContain('@rls.policy')
|
|
90
|
+
})
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
describe('namespacesToInstall', () => {
|
|
94
|
+
it('always includes @sqldoc/cli', () => {
|
|
95
|
+
expect(namespacesToInstall([], [])).toContain('@sqldoc/cli')
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('maps namespace names to packages', () => {
|
|
99
|
+
const result = namespacesToInstall(['audit', 'validate'], [])
|
|
100
|
+
expect(result).toContain('@sqldoc/ns-audit')
|
|
101
|
+
expect(result).toContain('@sqldoc/ns-validate')
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('includes templates package when codegen has templates', () => {
|
|
105
|
+
const result = namespacesToInstall(['codegen'], ['typescript'])
|
|
106
|
+
expect(result).toContain('@sqldoc/ns-codegen')
|
|
107
|
+
expect(result).toContain('@sqldoc/templates')
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('does not include templates when codegen has no templates', () => {
|
|
111
|
+
const result = namespacesToInstall(['codegen'], [])
|
|
112
|
+
expect(result).toContain('@sqldoc/ns-codegen')
|
|
113
|
+
expect(result).not.toContain('@sqldoc/templates')
|
|
114
|
+
})
|
|
115
|
+
})
|