Skip to content

Commit 4aacfe6

Browse files
committed
feat: add doctor command
chore: wip
1 parent d3570b9 commit 4aacfe6

File tree

2 files changed

+198
-0
lines changed

2 files changed

+198
-0
lines changed

packages/bunfig/bin/cli.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
2+
import { dirname, resolve } from 'node:path'
13
import process from 'node:process'
24
import { CAC } from 'cac'
35
import { version } from '../package.json'
@@ -48,6 +50,95 @@ cli.command('version', 'Show the version of the Bunfig CLI').action(() => {
4850
console.log(version)
4951
})
5052

53+
function stripJsonComments(jsonText: string): string {
54+
return jsonText
55+
// block comments
56+
.replace(/\/\*[\s\S]*?\*\//g, '')
57+
// line comments
58+
.replace(/(^|\s)\/\/.*$/gm, '$1')
59+
}
60+
61+
function findNearestTsconfig(startDir: string): string | null {
62+
let dir = startDir
63+
while (true) {
64+
const candidate = resolve(dir, 'tsconfig.json')
65+
if (existsSync(candidate))
66+
return candidate
67+
const parent = dirname(dir)
68+
if (parent === dir)
69+
return null
70+
dir = parent
71+
}
72+
}
73+
74+
function ensureArray<T>(value: T | T[] | undefined): T[] {
75+
if (!value)
76+
return []
77+
return Array.isArray(value) ? value : [value]
78+
}
79+
80+
cli
81+
.command('doctor', 'Check and set up tsconfig for bunfig')
82+
.option('--tsconfig <path>', 'Path to tsconfig.json (defaults to nearest)')
83+
.option('--fix', 'Automatically apply recommended changes')
84+
.action(async (options: { tsconfig?: string, fix?: boolean }) => {
85+
const cwd = process.cwd()
86+
const tsconfigPath = options.tsconfig
87+
? resolve(cwd, options.tsconfig)
88+
: findNearestTsconfig(cwd)
89+
90+
if (!tsconfigPath || !existsSync(tsconfigPath)) {
91+
console.error('No tsconfig.json found. Create one with:')
92+
console.error(' bunx --bun tsc --init')
93+
process.exit(options.fix ? 1 : 0)
94+
return
95+
}
96+
97+
const raw = readFileSync(tsconfigPath, 'utf8')
98+
const parsed = JSON.parse(stripJsonComments(raw)) as Record<string, any>
99+
const before = JSON.stringify(parsed)
100+
101+
parsed.compilerOptions = parsed.compilerOptions || {}
102+
103+
// 1) Ensure TS Language Service plugin is configured
104+
const plugins = ensureArray(parsed.compilerOptions.plugins)
105+
const hasPlugin = plugins.some((p: any) => p && (p.name === 'bunfig/ts-plugin' || p.name === '@bunfig/ts-plugin'))
106+
if (!hasPlugin) {
107+
plugins.push({ name: 'bunfig/ts-plugin' })
108+
parsed.compilerOptions.plugins = plugins
109+
console.log('Added bunfig TS plugin to compilerOptions.plugins')
110+
}
111+
112+
// 2) Recommend DOM lib if browser APIs are used
113+
const libs = ensureArray(parsed.compilerOptions.lib)
114+
if (!libs.includes('dom')) {
115+
console.warn('Note: Add "dom" to compilerOptions.lib if you use bunfig/browser:')
116+
console.warn(' "lib": ["esnext", "dom"]')
117+
}
118+
119+
// 4) Optional: types entry for ambient fallback (not required)
120+
// We do not force-add to avoid collisions, just inform
121+
122+
const after = JSON.stringify(parsed)
123+
if (before !== after) {
124+
if (options.fix) {
125+
writeFileSync(tsconfigPath, `${JSON.stringify(parsed, null, 2)}\n`, 'utf8')
126+
console.log(`Updated ${tsconfigPath}`)
127+
}
128+
else {
129+
console.log('Changes needed. Re-run with --fix to apply updates.')
130+
}
131+
}
132+
else {
133+
console.log('Looks good! No changes needed.')
134+
}
135+
136+
// Summary
137+
console.log('\nChecks:')
138+
console.log(`- tsconfig: ${tsconfigPath}`)
139+
console.log(`- plugin: ${hasPlugin ? 'OK' : 'added'}`)
140+
})
141+
51142
cli.version(version)
52143
cli.help()
53144
cli.parse()
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { afterEach, beforeEach, describe, expect, it } from 'bun:test'
2+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
3+
import { dirname, resolve } from 'node:path'
4+
import process from 'node:process'
5+
6+
describe('doctor', () => {
7+
const tmpRoot = resolve(process.cwd(), 'test/tmp/doctor')
8+
const tsconfigPath = resolve(tmpRoot, 'tsconfig.json')
9+
10+
beforeEach(() => {
11+
if (existsSync(tmpRoot))
12+
rmSync(tmpRoot, { recursive: true })
13+
mkdirSync(tmpRoot, { recursive: true, mode: 0o777 })
14+
})
15+
16+
afterEach(() => {
17+
if (existsSync(tmpRoot))
18+
rmSync(tmpRoot, { recursive: true })
19+
})
20+
21+
it('should report required changes without --fix', async () => {
22+
// minimal tsconfig missing plugin/paths
23+
writeFileSync(tsconfigPath, JSON.stringify({ compilerOptions: {} }, null, 2))
24+
25+
const proc = Bun.spawn([
26+
'./bunfig',
27+
'doctor',
28+
'--tsconfig',
29+
tsconfigPath,
30+
], {
31+
cwd: process.cwd(),
32+
})
33+
34+
const output = await new Response(proc.stdout).text()
35+
await proc.exited
36+
37+
expect(output).toContain('Added bunfig TS plugin')
38+
expect(output).toContain('Changes needed')
39+
40+
// file should remain unchanged without --fix
41+
const content = readFileSync(tsconfigPath, 'utf8')
42+
expect(content).not.toContain('bunfig/ts-plugin')
43+
})
44+
45+
it('should apply fixes with --fix', async () => {
46+
// ensure parent exists
47+
if (!existsSync(dirname(tsconfigPath)))
48+
mkdirSync(dirname(tsconfigPath), { recursive: true, mode: 0o777 })
49+
50+
writeFileSync(tsconfigPath, JSON.stringify({ compilerOptions: {} }, null, 2))
51+
52+
const proc = Bun.spawn([
53+
'./bunfig',
54+
'doctor',
55+
'--tsconfig',
56+
tsconfigPath,
57+
'--fix',
58+
], {
59+
cwd: process.cwd(),
60+
})
61+
62+
const output = await new Response(proc.stdout).text()
63+
await proc.exited
64+
65+
expect(output).toContain('Updated')
66+
67+
const content = JSON.parse(readFileSync(tsconfigPath, 'utf8')) as any
68+
const plugins = content.compilerOptions?.plugins || []
69+
const hasPlugin = plugins.some((p: any) => p?.name === 'bunfig/ts-plugin')
70+
expect(hasPlugin).toBe(true)
71+
72+
if (existsSync(resolve(process.cwd(), 'packages/bunfig/src'))) {
73+
// In monorepo, paths should be ensured
74+
const paths = content.compilerOptions?.paths || {}
75+
expect(paths.bunfig).toBeTruthy()
76+
expect(paths['bunfig/*']).toBeTruthy()
77+
}
78+
})
79+
80+
it('should be a no-op when already configured', async () => {
81+
const preconfigured = {
82+
compilerOptions: {
83+
plugins: [{ name: 'bunfig/ts-plugin' }],
84+
baseUrl: '.',
85+
paths: {
86+
'bunfig': ['packages/bunfig/src'],
87+
'bunfig/*': ['packages/bunfig/src/*'],
88+
},
89+
},
90+
}
91+
writeFileSync(tsconfigPath, JSON.stringify(preconfigured, null, 2))
92+
93+
const proc = Bun.spawn([
94+
'./bunfig',
95+
'doctor',
96+
'--tsconfig',
97+
tsconfigPath,
98+
], {
99+
cwd: process.cwd(),
100+
})
101+
102+
const output = await new Response(proc.stdout).text()
103+
await proc.exited
104+
105+
expect(output).toContain('Looks good! No changes needed.')
106+
})
107+
})

0 commit comments

Comments
 (0)