beast-devtools 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/.claude/launch.json +11 -0
- package/CHANGELOG.md +37 -0
- package/README.md +87 -0
- package/devtools/CHANGELOG.md +26 -0
- package/devtools/LICENSE +15 -0
- package/devtools/README.md +164 -0
- package/devtools/client/BeastDevtools.btsx +185 -0
- package/devtools/client/CodeView.btsx +61 -0
- package/devtools/client/ComponentsPanel.btsx +212 -0
- package/devtools/client/FileList.btsx +35 -0
- package/devtools/client/InspectorPanel.btsx +131 -0
- package/devtools/client/RefactorPanel.btsx +304 -0
- package/devtools/client/api.ts +69 -0
- package/devtools/client/compile.test.ts +22 -0
- package/devtools/client/devtools.css +1279 -0
- package/devtools/client/highlight.ts +210 -0
- package/devtools/client/mount.ts +16 -0
- package/devtools/client/runtime.ts +168 -0
- package/devtools/client/util.ts +136 -0
- package/devtools/package.json +73 -0
- package/devtools/server/analyze.test.ts +148 -0
- package/devtools/server/analyze.ts +737 -0
- package/devtools/server/diff.ts +82 -0
- package/devtools/server/line-map.ts +63 -0
- package/devtools/server/octane-bundler.d.ts +17 -0
- package/devtools/server/project.ts +369 -0
- package/devtools/server/refactor.test.ts +224 -0
- package/devtools/server/refactor.ts +224 -0
- package/devtools/server/source-scan.ts +377 -0
- package/devtools/shared/types.ts +208 -0
- package/devtools/test/fixtures/App.btsx +193 -0
- package/devtools/tsconfig.build.json +17 -0
- package/devtools/vite.ts +159 -0
- package/favicon.ico +0 -0
- package/index.html +14 -0
- package/package.json +34 -0
- package/public/beast.svg +1 -0
- package/src/App.btsx +144 -0
- package/src/AppHeader.btsx +24 -0
- package/src/LeftArticle.btsx +22 -0
- package/src/RightArticle.btsx +19 -0
- package/src/env.d.ts +8 -0
- package/src/lib/utils.ts +6 -0
- package/src/main.ts +8 -0
- package/src/style.css +44 -0
- package/tsconfig.json +39 -0
- package/vite.config.ts +19 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { readFileSync } from 'node:fs'
|
|
3
|
+
import { compileBeastResult } from 'beast-tsrx'
|
|
4
|
+
import { createOctaneCompiler } from 'octane/compiler/bundler'
|
|
5
|
+
import type { AnalyzerSettings, FileAnalysis, RefactorSuggestion } from '../shared/types.ts'
|
|
6
|
+
import { analyzeDocument } from './analyze.ts'
|
|
7
|
+
import { buildLineMap } from './line-map.ts'
|
|
8
|
+
import { hookCall, identifiersIn, parsePropsParameter, patternNames, topLevelDeclarations } from './source-scan.ts'
|
|
9
|
+
|
|
10
|
+
const APP = readFileSync(new URL('../test/fixtures/App.btsx', import.meta.url), 'utf8')
|
|
11
|
+
|
|
12
|
+
function analyze(source: string, settings: Partial<AnalyzerSettings> = {}, name = 'Fixture'): FileAnalysis {
|
|
13
|
+
const { ast } = compileBeastResult(source, { filename: `${name}.btsx`, componentName: name })
|
|
14
|
+
return analyzeDocument(ast, source, name, { depthLimit: 5, minLines: 8, fileLines: 30, ...settings })
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Apply a suggestion the way a developer would: insert the component, replace the section. */
|
|
18
|
+
function apply(source: string, suggestion: RefactorSuggestion): string {
|
|
19
|
+
const lines = source.split('\n')
|
|
20
|
+
const replaced = [
|
|
21
|
+
...lines.slice(0, suggestion.startLine - 1),
|
|
22
|
+
suggestion.usage,
|
|
23
|
+
...lines.slice(suggestion.endLine),
|
|
24
|
+
]
|
|
25
|
+
const at = suggestion.insertBeforeLine - 1
|
|
26
|
+
return [...replaced.slice(0, at), ...suggestion.snippet.split('\n'), ...replaced.slice(at)].join('\n')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function compilesThroughOctane(source: string, name: string): void {
|
|
30
|
+
const { code } = compileBeastResult(source, { filename: `${name}.btsx`, componentName: name })
|
|
31
|
+
const octane = createOctaneCompiler({ root: process.cwd(), environment: 'client', hmr: false, dev: false })
|
|
32
|
+
expect(octane.transform(code, `/virtual/${name}.tsrx`, { environment: 'client' })).not.toBeNull()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('source scanning', () => {
|
|
36
|
+
test('identifiersIn skips property names, keywords, and string contents', () => {
|
|
37
|
+
const names = identifiersIn("copyNote('left', active.left.note) ?? `tab-${panel.id}` + typeof x")
|
|
38
|
+
expect([...names].sort()).toEqual(['active', 'copyNote', 'panel', 'x'])
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('identifiersIn keeps spread targets and optional-chain roots', () => {
|
|
42
|
+
expect([...identifiersIn('{...cardProps} ref?.current')].sort()).toEqual(['cardProps', 'ref'])
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('patternNames handles nested, renamed, defaulted, and rest bindings', () => {
|
|
46
|
+
expect(patternNames('{ a, b: renamed, c = 1, d: [e, ...f], ...rest }')).toEqual(['a', 'renamed', 'c', 'e', 'f', 'rest'])
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('topLevelDeclarations ignores declarations nested in function bodies', () => {
|
|
50
|
+
const code = [
|
|
51
|
+
"const [count, setCount] = useState<number>(0);",
|
|
52
|
+
'const save = async () => { const inner = 1; await send(inner); };',
|
|
53
|
+
'function helper() { let hidden = 2 }',
|
|
54
|
+
].join('\n')
|
|
55
|
+
expect(topLevelDeclarations(code).map((d) => d.names)).toEqual([['count', 'setCount'], ['save'], ['helper']])
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('parsePropsParameter splits names from the type annotation', () => {
|
|
59
|
+
expect(parsePropsParameter('{ user, compact = false }: { user: User; compact?: boolean }')).toEqual({
|
|
60
|
+
names: ['user', 'compact'],
|
|
61
|
+
type: '{ user: User; compact?: boolean }',
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('hookCall reads generic type arguments', () => {
|
|
66
|
+
expect(hookCall("useState<PanelSide | null>(null)")).toEqual({ hook: 'useState', typeArgument: 'PanelSide | null', argument: 'null' })
|
|
67
|
+
expect(hookCall('useRef<Map<string, number>>(new Map())')?.typeArgument).toBe('Map<string, number>')
|
|
68
|
+
expect(hookCall('panels.find(Boolean)')).toBeNull()
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
describe('line map', () => {
|
|
73
|
+
test('maps the App component header both ways', () => {
|
|
74
|
+
const { code, map } = compileBeastResult(APP, { filename: 'App.btsx', componentName: 'App' })
|
|
75
|
+
const generated = code.split('\n')
|
|
76
|
+
const { tsrxToBtsx, btsxToTsrx } = buildLineMap(map, generated.length, APP.split('\n').length)
|
|
77
|
+
const mainLine = APP.split('\n').findIndex((line) => line.startsWith('main(')) + 1
|
|
78
|
+
const targets = btsxToTsrx[mainLine - 1]!
|
|
79
|
+
expect(targets.length).toBeGreaterThan(0)
|
|
80
|
+
expect(generated[targets[0]! - 1]).toContain('<main')
|
|
81
|
+
expect(tsrxToBtsx[targets[0]! - 1]).toBe(mainLine)
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
describe('refactor analyzer', () => {
|
|
86
|
+
const analysis = analyze(APP, {}, 'App')
|
|
87
|
+
|
|
88
|
+
test('measures structural depth per line, including continuation lines', () => {
|
|
89
|
+
const lines = APP.split('\n')
|
|
90
|
+
const depthOf = (prefix: string) => analysis.lineDepths[lines.findIndex((line) => line.trimStart().startsWith(prefix))]
|
|
91
|
+
expect(depthOf('main(')).toBe(0)
|
|
92
|
+
expect(depthOf('~ aria-labelledby="showcase-title"')).toBe(1)
|
|
93
|
+
expect(depthOf('li(')).toBe(7)
|
|
94
|
+
expect(depthOf('setup')).toBeNull()
|
|
95
|
+
expect(analysis.maxDepth).toBe(7)
|
|
96
|
+
expect(analysis.indentUnit).toBe(2)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test('suggests extracting the deep sections with inferred, typed props', () => {
|
|
100
|
+
const extracts = analysis.suggestions.filter((s) => s.kind === 'extract')
|
|
101
|
+
expect(extracts.map((s) => s.name)).toEqual(['AppHeader', 'LeftArticle', 'RightArticle'])
|
|
102
|
+
const header = extracts[0]!
|
|
103
|
+
expect(header.props).toEqual([
|
|
104
|
+
{ name: 'activeId', type: 'PanelId' },
|
|
105
|
+
{ name: 'setActiveId', type: '(value: PanelId) => void' },
|
|
106
|
+
{ name: 'setCopiedSide', type: '(value: PanelSide | null) => void' },
|
|
107
|
+
])
|
|
108
|
+
expect(header.snippet.split('\n')[0]).toBe('component AppHeader')
|
|
109
|
+
expect(header.usage.trim()).toBe('AppHeader(activeId={activeId} setActiveId={setActiveId} setCopiedSide={setCopiedSide})')
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
test('finds the structurally identical copy-note blocks', () => {
|
|
113
|
+
const duplicate = analysis.suggestions.find((s) => s.kind === 'duplicate')
|
|
114
|
+
expect(duplicate?.name).toBe('CopyNote')
|
|
115
|
+
expect(duplicate?.occurrences).toHaveLength(2)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
test('applied extraction suggestions still compile through Beast and Octane', () => {
|
|
119
|
+
for (const suggestion of analysis.suggestions.filter((s) => s.kind === 'extract')) {
|
|
120
|
+
compilesThroughOctane(apply(APP, suggestion), 'App')
|
|
121
|
+
}
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
test('keeps a hoisted loop key at the call site', () => {
|
|
125
|
+
const source = [
|
|
126
|
+
'props { groups }: { groups: { id: string; items: string[] }[] }',
|
|
127
|
+
'.list',
|
|
128
|
+
' each group in groups',
|
|
129
|
+
' .group(key={group.id})',
|
|
130
|
+
' h2 Title',
|
|
131
|
+
' ul',
|
|
132
|
+
' each item in group.items',
|
|
133
|
+
' li #{item}',
|
|
134
|
+
'',
|
|
135
|
+
].join('\n')
|
|
136
|
+
const [suggestion] = analyze(source, { depthLimit: 3, minLines: 4 }).suggestions
|
|
137
|
+
expect(suggestion?.name).toBe('Group')
|
|
138
|
+
expect(suggestion?.usage.trim()).toBe('Group(key={group.id} group={group})')
|
|
139
|
+
expect(suggestion?.snippet.split('\n')[2]).toBe(' .group')
|
|
140
|
+
compilesThroughOctane(apply(source, suggestion!), 'Fixture')
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
test('stays quiet about shallow components', () => {
|
|
144
|
+
const shallow = analyze('section\n h1 Title\n p Body\n')
|
|
145
|
+
expect(shallow.suggestions).toEqual([])
|
|
146
|
+
expect(shallow.maxDepth).toBe(1)
|
|
147
|
+
})
|
|
148
|
+
})
|