@test328932/test328933 1.0.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/package.json +41 -0
- package/src/component-border.js +839 -0
- package/src/component-values.js +248 -0
- package/src/index.d.ts +10 -0
- package/src/index.js +6 -0
- package/src/llm-explain-inject.js +218 -0
- package/src/runtime/index.d.ts +4 -0
- package/src/runtime/index.js +2 -0
- package/src/runtime/llm-explain-panel.jsx +813 -0
- package/src/runtime/llm-worker.js +7 -0
- package/src/save-file.js +46 -0
- package/src/source-compressor.js +352 -0
- package/src/source-explorer.js +129 -0
|
@@ -0,0 +1,839 @@
|
|
|
1
|
+
import { parse } from '@babel/parser'
|
|
2
|
+
import traverse from '@babel/traverse'
|
|
3
|
+
import generate from '@babel/generator'
|
|
4
|
+
import * as t from '@babel/types'
|
|
5
|
+
|
|
6
|
+
const BORDER_STYLE = '2px solid green'
|
|
7
|
+
const traverseAst = traverse.default ?? traverse
|
|
8
|
+
const generateCode = generate.default ?? generate
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Merges { border: '2px solid green' } into an existing style prop value.
|
|
12
|
+
* Works for: style={{ ... }}, style={someVar} (skipped), no style prop (added).
|
|
13
|
+
*/
|
|
14
|
+
function buildBorderStyleExpression(existingStyleExpr) {
|
|
15
|
+
const borderProp = t.objectProperty(
|
|
16
|
+
t.identifier('border'),
|
|
17
|
+
t.stringLiteral(BORDER_STYLE),
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
if (!existingStyleExpr) {
|
|
21
|
+
return t.objectExpression([borderProp])
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (t.isObjectExpression(existingStyleExpr)) {
|
|
25
|
+
// Clone and prepend the border property (existing values override it if they set border too)
|
|
26
|
+
return t.objectExpression([borderProp, ...existingStyleExpr.properties])
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// For dynamic expressions like style={styles.foo}, use spread: { border, ...expr }
|
|
30
|
+
return t.objectExpression([borderProp, t.spreadElement(existingStyleExpr)])
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Inject border style into the root JSX element of a return statement.
|
|
35
|
+
* Skips fragments (<> or <React.Fragment>) since they can't have styles.
|
|
36
|
+
*/
|
|
37
|
+
function injectBorderIntoJSX(jsxNode) {
|
|
38
|
+
if (!t.isJSXElement(jsxNode)) return // skip fragments, non-JSX
|
|
39
|
+
|
|
40
|
+
const openingEl = jsxNode.openingElement
|
|
41
|
+
const stylePropIndex = openingEl.attributes.findIndex(
|
|
42
|
+
attr => t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name, { name: 'style' }),
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
if (stylePropIndex === -1) {
|
|
46
|
+
// No style prop — add one
|
|
47
|
+
openingEl.attributes.push(
|
|
48
|
+
t.jsxAttribute(
|
|
49
|
+
t.jsxIdentifier('style'),
|
|
50
|
+
t.jsxExpressionContainer(buildBorderStyleExpression(null)),
|
|
51
|
+
),
|
|
52
|
+
)
|
|
53
|
+
} else {
|
|
54
|
+
const existing = openingEl.attributes[stylePropIndex]
|
|
55
|
+
const valueNode = existing.value
|
|
56
|
+
|
|
57
|
+
let innerExpr = null
|
|
58
|
+
if (t.isJSXExpressionContainer(valueNode)) {
|
|
59
|
+
innerExpr = valueNode.expression
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
existing.value = t.jsxExpressionContainer(buildBorderStyleExpression(innerExpr))
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function isComponentName(name) {
|
|
67
|
+
return typeof name === 'string' && /^[A-Z]/.test(name)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function isTopLevelDeclaration(path) {
|
|
71
|
+
const parent = path.parentPath
|
|
72
|
+
if (!parent) return false
|
|
73
|
+
if (parent.isProgram()) return true
|
|
74
|
+
if (parent.isExportNamedDeclaration() || parent.isExportDefaultDeclaration()) {
|
|
75
|
+
return parent.parentPath?.isProgram() ?? false
|
|
76
|
+
}
|
|
77
|
+
return false
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function ensureBlockBody(functionNode) {
|
|
81
|
+
if (t.isBlockStatement(functionNode.body)) return
|
|
82
|
+
functionNode.body = t.blockStatement([t.returnStatement(functionNode.body)])
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function buildCloseButton() {
|
|
86
|
+
return t.jsxElement(
|
|
87
|
+
t.jsxOpeningElement(
|
|
88
|
+
t.jsxIdentifier('button'),
|
|
89
|
+
[
|
|
90
|
+
t.jsxAttribute(t.jsxIdentifier('type'), t.stringLiteral('button')),
|
|
91
|
+
t.jsxAttribute(
|
|
92
|
+
t.jsxIdentifier('onClick'),
|
|
93
|
+
t.jsxExpressionContainer(
|
|
94
|
+
t.arrowFunctionExpression(
|
|
95
|
+
[],
|
|
96
|
+
t.callExpression(t.identifier('setVisible'), [
|
|
97
|
+
t.arrowFunctionExpression(
|
|
98
|
+
[t.identifier('prev')],
|
|
99
|
+
t.unaryExpression('!', t.identifier('prev')),
|
|
100
|
+
),
|
|
101
|
+
]),
|
|
102
|
+
),
|
|
103
|
+
),
|
|
104
|
+
),
|
|
105
|
+
],
|
|
106
|
+
false,
|
|
107
|
+
),
|
|
108
|
+
t.jsxClosingElement(t.jsxIdentifier('button')),
|
|
109
|
+
[t.jsxText('x')],
|
|
110
|
+
false,
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function wrapWithVisibilityUI(jsxNode) {
|
|
115
|
+
return t.jsxFragment(
|
|
116
|
+
t.jsxOpeningFragment(),
|
|
117
|
+
t.jsxClosingFragment(),
|
|
118
|
+
[
|
|
119
|
+
buildCloseButton(),
|
|
120
|
+
t.jsxExpressionContainer(
|
|
121
|
+
t.conditionalExpression(
|
|
122
|
+
t.identifier('visible'),
|
|
123
|
+
jsxNode,
|
|
124
|
+
t.nullLiteral(),
|
|
125
|
+
),
|
|
126
|
+
),
|
|
127
|
+
],
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function wrapWithAppDrawer(jsxNode, todoPanelIdentifier) {
|
|
132
|
+
const layoutStyle = t.objectExpression([
|
|
133
|
+
t.objectProperty(t.identifier('display'), t.stringLiteral('flex')),
|
|
134
|
+
t.objectProperty(t.identifier('width'), t.stringLiteral('100vw')),
|
|
135
|
+
t.objectProperty(t.identifier('maxWidth'), t.stringLiteral('100vw')),
|
|
136
|
+
])
|
|
137
|
+
|
|
138
|
+
const drawerStyle = t.objectExpression([
|
|
139
|
+
t.objectProperty(t.identifier('width'), t.stringLiteral('350px')),
|
|
140
|
+
t.objectProperty(t.identifier('minWidth'), t.stringLiteral('350px')),
|
|
141
|
+
t.objectProperty(t.identifier('padding'), t.stringLiteral('20px 14px')),
|
|
142
|
+
t.objectProperty(t.identifier('background'), t.stringLiteral('#102a43')),
|
|
143
|
+
t.objectProperty(t.identifier('color'), t.stringLiteral('#f0f4f8')),
|
|
144
|
+
t.objectProperty(t.identifier('minHeight'), t.stringLiteral('100vh')),
|
|
145
|
+
t.objectProperty(t.identifier('overflowY'), t.stringLiteral('auto')),
|
|
146
|
+
])
|
|
147
|
+
|
|
148
|
+
const contentStyle = t.objectExpression([
|
|
149
|
+
t.objectProperty(t.identifier('flex'), t.numericLiteral(1)),
|
|
150
|
+
t.objectProperty(t.identifier('paddingLeft'), t.stringLiteral('16px')),
|
|
151
|
+
])
|
|
152
|
+
|
|
153
|
+
const todoPanelJsx = t.jsxElement(
|
|
154
|
+
t.jsxOpeningElement(
|
|
155
|
+
t.jsxIdentifier(todoPanelIdentifier.name),
|
|
156
|
+
[],
|
|
157
|
+
true,
|
|
158
|
+
),
|
|
159
|
+
null,
|
|
160
|
+
[],
|
|
161
|
+
true,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
return t.jsxElement(
|
|
165
|
+
t.jsxOpeningElement(
|
|
166
|
+
t.jsxIdentifier('div'),
|
|
167
|
+
[
|
|
168
|
+
t.jsxAttribute(
|
|
169
|
+
t.jsxIdentifier('style'),
|
|
170
|
+
t.jsxExpressionContainer(layoutStyle),
|
|
171
|
+
),
|
|
172
|
+
],
|
|
173
|
+
false,
|
|
174
|
+
),
|
|
175
|
+
t.jsxClosingElement(t.jsxIdentifier('div')),
|
|
176
|
+
[
|
|
177
|
+
t.jsxElement(
|
|
178
|
+
t.jsxOpeningElement(
|
|
179
|
+
t.jsxIdentifier('aside'),
|
|
180
|
+
[
|
|
181
|
+
t.jsxAttribute(
|
|
182
|
+
t.jsxIdentifier('style'),
|
|
183
|
+
t.jsxExpressionContainer(drawerStyle),
|
|
184
|
+
),
|
|
185
|
+
],
|
|
186
|
+
false,
|
|
187
|
+
),
|
|
188
|
+
t.jsxClosingElement(t.jsxIdentifier('aside')),
|
|
189
|
+
[todoPanelJsx],
|
|
190
|
+
false,
|
|
191
|
+
),
|
|
192
|
+
t.jsxElement(
|
|
193
|
+
t.jsxOpeningElement(
|
|
194
|
+
t.jsxIdentifier('div'),
|
|
195
|
+
[
|
|
196
|
+
t.jsxAttribute(
|
|
197
|
+
t.jsxIdentifier('style'),
|
|
198
|
+
t.jsxExpressionContainer(contentStyle),
|
|
199
|
+
),
|
|
200
|
+
],
|
|
201
|
+
false,
|
|
202
|
+
),
|
|
203
|
+
t.jsxClosingElement(t.jsxIdentifier('div')),
|
|
204
|
+
[jsxNode],
|
|
205
|
+
false,
|
|
206
|
+
),
|
|
207
|
+
],
|
|
208
|
+
false,
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function buildAutoTodoEffect(useEffectCallee, userEventIdentifier, screenIdentifier) {
|
|
213
|
+
return t.expressionStatement(
|
|
214
|
+
t.callExpression(t.cloneNode(useEffectCallee), [
|
|
215
|
+
t.arrowFunctionExpression(
|
|
216
|
+
[],
|
|
217
|
+
t.blockStatement([
|
|
218
|
+
t.variableDeclaration('const', [
|
|
219
|
+
t.variableDeclarator(
|
|
220
|
+
t.identifier('timer'),
|
|
221
|
+
t.callExpression(t.identifier('setTimeout'), [
|
|
222
|
+
t.arrowFunctionExpression(
|
|
223
|
+
[],
|
|
224
|
+
t.blockStatement([
|
|
225
|
+
t.variableDeclaration('const', [
|
|
226
|
+
t.variableDeclarator(
|
|
227
|
+
t.identifier('user'),
|
|
228
|
+
t.callExpression(
|
|
229
|
+
t.memberExpression(
|
|
230
|
+
t.cloneNode(userEventIdentifier),
|
|
231
|
+
t.identifier('setup'),
|
|
232
|
+
),
|
|
233
|
+
[],
|
|
234
|
+
),
|
|
235
|
+
),
|
|
236
|
+
]),
|
|
237
|
+
t.variableDeclaration('const', [
|
|
238
|
+
t.variableDeclarator(
|
|
239
|
+
t.identifier('todoInput'),
|
|
240
|
+
t.callExpression(
|
|
241
|
+
t.memberExpression(
|
|
242
|
+
t.cloneNode(screenIdentifier),
|
|
243
|
+
t.identifier('getByPlaceholderText'),
|
|
244
|
+
),
|
|
245
|
+
[t.stringLiteral('Add a todo...')],
|
|
246
|
+
),
|
|
247
|
+
),
|
|
248
|
+
]),
|
|
249
|
+
t.expressionStatement(
|
|
250
|
+
t.awaitExpression(
|
|
251
|
+
t.callExpression(
|
|
252
|
+
t.memberExpression(t.identifier('user'), t.identifier('type')),
|
|
253
|
+
[t.identifier('todoInput'), t.stringLiteral('Plugin auto todo after 5s')],
|
|
254
|
+
),
|
|
255
|
+
),
|
|
256
|
+
),
|
|
257
|
+
t.variableDeclaration('const', [
|
|
258
|
+
t.variableDeclarator(
|
|
259
|
+
t.identifier('addButton'),
|
|
260
|
+
t.callExpression(
|
|
261
|
+
t.memberExpression(
|
|
262
|
+
t.cloneNode(screenIdentifier),
|
|
263
|
+
t.identifier('getByRole'),
|
|
264
|
+
),
|
|
265
|
+
[
|
|
266
|
+
t.stringLiteral('button'),
|
|
267
|
+
t.objectExpression([
|
|
268
|
+
t.objectProperty(
|
|
269
|
+
t.identifier('name'),
|
|
270
|
+
t.regExpLiteral('add', 'i'),
|
|
271
|
+
),
|
|
272
|
+
]),
|
|
273
|
+
],
|
|
274
|
+
),
|
|
275
|
+
),
|
|
276
|
+
]),
|
|
277
|
+
t.expressionStatement(
|
|
278
|
+
t.awaitExpression(
|
|
279
|
+
t.callExpression(
|
|
280
|
+
t.memberExpression(t.identifier('user'), t.identifier('click')),
|
|
281
|
+
[t.identifier('addButton')],
|
|
282
|
+
),
|
|
283
|
+
),
|
|
284
|
+
),
|
|
285
|
+
]),
|
|
286
|
+
true,
|
|
287
|
+
),
|
|
288
|
+
t.numericLiteral(5000),
|
|
289
|
+
]),
|
|
290
|
+
),
|
|
291
|
+
]),
|
|
292
|
+
t.returnStatement(
|
|
293
|
+
t.arrowFunctionExpression(
|
|
294
|
+
[],
|
|
295
|
+
t.callExpression(t.identifier('clearTimeout'), [t.identifier('timer')]),
|
|
296
|
+
),
|
|
297
|
+
),
|
|
298
|
+
]),
|
|
299
|
+
),
|
|
300
|
+
t.arrayExpression([]),
|
|
301
|
+
]),
|
|
302
|
+
)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function insertImportBeforeNonImport(programPath, importNode) {
|
|
306
|
+
const firstNonImportIndex = programPath.node.body.findIndex(node => !t.isImportDeclaration(node))
|
|
307
|
+
if (firstNonImportIndex === -1) {
|
|
308
|
+
programPath.node.body.push(importNode)
|
|
309
|
+
} else {
|
|
310
|
+
programPath.node.body.splice(firstNonImportIndex, 0, importNode)
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function ensureNamedImport(programPath, moduleName, importedName, preferredLocalName = importedName) {
|
|
315
|
+
let moduleImportPath = null
|
|
316
|
+
let existingLocalName = null
|
|
317
|
+
|
|
318
|
+
programPath.get('body').forEach(statementPath => {
|
|
319
|
+
if (!statementPath.isImportDeclaration()) return
|
|
320
|
+
if (statementPath.node.source.value !== moduleName) return
|
|
321
|
+
|
|
322
|
+
if (!moduleImportPath) moduleImportPath = statementPath
|
|
323
|
+
statementPath.node.specifiers.forEach(specifier => {
|
|
324
|
+
if (
|
|
325
|
+
t.isImportSpecifier(specifier) &&
|
|
326
|
+
t.isIdentifier(specifier.imported, { name: importedName })
|
|
327
|
+
) {
|
|
328
|
+
existingLocalName = specifier.local.name
|
|
329
|
+
}
|
|
330
|
+
})
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
if (existingLocalName) {
|
|
334
|
+
return t.identifier(existingLocalName)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const localName = programPath.scope.hasBinding(preferredLocalName)
|
|
338
|
+
? `__${preferredLocalName}`
|
|
339
|
+
: preferredLocalName
|
|
340
|
+
|
|
341
|
+
if (moduleImportPath && !moduleImportPath.node.specifiers.some(t.isImportNamespaceSpecifier)) {
|
|
342
|
+
moduleImportPath.node.specifiers.push(
|
|
343
|
+
t.importSpecifier(t.identifier(localName), t.identifier(importedName)),
|
|
344
|
+
)
|
|
345
|
+
return t.identifier(localName)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const importNode = t.importDeclaration(
|
|
349
|
+
[t.importSpecifier(t.identifier(localName), t.identifier(importedName))],
|
|
350
|
+
t.stringLiteral(moduleName),
|
|
351
|
+
)
|
|
352
|
+
insertImportBeforeNonImport(programPath, importNode)
|
|
353
|
+
return t.identifier(localName)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function ensureDefaultImport(programPath, moduleName, preferredLocalName) {
|
|
357
|
+
let moduleImportPath = null
|
|
358
|
+
let existingLocalName = null
|
|
359
|
+
|
|
360
|
+
programPath.get('body').forEach(statementPath => {
|
|
361
|
+
if (!statementPath.isImportDeclaration()) return
|
|
362
|
+
if (statementPath.node.source.value !== moduleName) return
|
|
363
|
+
|
|
364
|
+
if (!moduleImportPath) moduleImportPath = statementPath
|
|
365
|
+
statementPath.node.specifiers.forEach(specifier => {
|
|
366
|
+
if (t.isImportDefaultSpecifier(specifier)) {
|
|
367
|
+
existingLocalName = specifier.local.name
|
|
368
|
+
}
|
|
369
|
+
})
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
if (existingLocalName) {
|
|
373
|
+
return t.identifier(existingLocalName)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const localName = programPath.scope.hasBinding(preferredLocalName)
|
|
377
|
+
? `__${preferredLocalName}`
|
|
378
|
+
: preferredLocalName
|
|
379
|
+
|
|
380
|
+
if (moduleImportPath && !moduleImportPath.node.specifiers.some(t.isImportNamespaceSpecifier)) {
|
|
381
|
+
moduleImportPath.node.specifiers.unshift(t.importDefaultSpecifier(t.identifier(localName)))
|
|
382
|
+
return t.identifier(localName)
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const importNode = t.importDeclaration(
|
|
386
|
+
[t.importDefaultSpecifier(t.identifier(localName))],
|
|
387
|
+
t.stringLiteral(moduleName),
|
|
388
|
+
)
|
|
389
|
+
insertImportBeforeNonImport(programPath, importNode)
|
|
390
|
+
return t.identifier(localName)
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function ensureReactHookCallee(programPath, hookName) {
|
|
394
|
+
let reactImportPath = null
|
|
395
|
+
let namedHookLocal = null
|
|
396
|
+
let namespaceImportLocal = null
|
|
397
|
+
let defaultImportLocal = null
|
|
398
|
+
|
|
399
|
+
programPath.get('body').forEach(statementPath => {
|
|
400
|
+
if (!statementPath.isImportDeclaration()) return
|
|
401
|
+
if (statementPath.node.source.value !== 'react') return
|
|
402
|
+
|
|
403
|
+
if (!reactImportPath) reactImportPath = statementPath
|
|
404
|
+
statementPath.node.specifiers.forEach(specifier => {
|
|
405
|
+
if (
|
|
406
|
+
t.isImportSpecifier(specifier) &&
|
|
407
|
+
t.isIdentifier(specifier.imported, { name: hookName })
|
|
408
|
+
) {
|
|
409
|
+
namedHookLocal = specifier.local.name
|
|
410
|
+
}
|
|
411
|
+
if (t.isImportNamespaceSpecifier(specifier)) {
|
|
412
|
+
namespaceImportLocal = specifier.local.name
|
|
413
|
+
}
|
|
414
|
+
if (t.isImportDefaultSpecifier(specifier)) {
|
|
415
|
+
defaultImportLocal = specifier.local.name
|
|
416
|
+
}
|
|
417
|
+
})
|
|
418
|
+
})
|
|
419
|
+
|
|
420
|
+
if (namedHookLocal) {
|
|
421
|
+
return t.identifier(namedHookLocal)
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (namespaceImportLocal) {
|
|
425
|
+
return t.memberExpression(t.identifier(namespaceImportLocal), t.identifier(hookName))
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (defaultImportLocal) {
|
|
429
|
+
return t.memberExpression(t.identifier(defaultImportLocal), t.identifier(hookName))
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const localName = programPath.scope.hasBinding(hookName)
|
|
433
|
+
? `__component${hookName[0].toUpperCase()}${hookName.slice(1)}`
|
|
434
|
+
: hookName
|
|
435
|
+
|
|
436
|
+
if (reactImportPath && !reactImportPath.node.specifiers.some(t.isImportNamespaceSpecifier)) {
|
|
437
|
+
reactImportPath.node.specifiers.push(
|
|
438
|
+
t.importSpecifier(t.identifier(localName), t.identifier(hookName)),
|
|
439
|
+
)
|
|
440
|
+
return t.identifier(localName)
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const hookImport = t.importDeclaration(
|
|
444
|
+
[t.importSpecifier(t.identifier(localName), t.identifier(hookName))],
|
|
445
|
+
t.stringLiteral('react'),
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
insertImportBeforeNonImport(programPath, hookImport)
|
|
449
|
+
|
|
450
|
+
return t.identifier(localName)
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function ensureUseStateCallee(programPath) {
|
|
454
|
+
return ensureReactHookCallee(programPath, 'useState')
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function ensureUseEffectCallee(programPath) {
|
|
458
|
+
return ensureReactHookCallee(programPath, 'useEffect')
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function transformComponentFunction(functionPath, useStateCallee, options = {}) {
|
|
462
|
+
const {
|
|
463
|
+
withAppDrawer = false,
|
|
464
|
+
withAutoTodo = false,
|
|
465
|
+
useEffectCallee = null,
|
|
466
|
+
userEventIdentifier = null,
|
|
467
|
+
screenIdentifier = null,
|
|
468
|
+
todoPanelIdentifier = null,
|
|
469
|
+
} = options
|
|
470
|
+
ensureBlockBody(functionPath.node)
|
|
471
|
+
|
|
472
|
+
if (
|
|
473
|
+
functionPath.scope.hasOwnBinding('visible') ||
|
|
474
|
+
functionPath.scope.hasOwnBinding('setVisible')
|
|
475
|
+
) {
|
|
476
|
+
return false
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const returnPaths = []
|
|
480
|
+
functionPath.get('body').traverse({
|
|
481
|
+
ReturnStatement(returnPath) {
|
|
482
|
+
if (returnPath.getFunctionParent() !== functionPath) return
|
|
483
|
+
const arg = returnPath.node.argument
|
|
484
|
+
if (t.isJSXElement(arg) || t.isJSXFragment(arg)) {
|
|
485
|
+
returnPaths.push(returnPath)
|
|
486
|
+
}
|
|
487
|
+
},
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
if (returnPaths.length === 0) return false
|
|
491
|
+
|
|
492
|
+
functionPath.node.body.body.unshift(
|
|
493
|
+
t.variableDeclaration('const', [
|
|
494
|
+
t.variableDeclarator(
|
|
495
|
+
t.arrayPattern([t.identifier('visible'), t.identifier('setVisible')]),
|
|
496
|
+
t.callExpression(t.cloneNode(useStateCallee), [t.booleanLiteral(true)]),
|
|
497
|
+
),
|
|
498
|
+
]),
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
if (withAutoTodo && useEffectCallee && userEventIdentifier && screenIdentifier) {
|
|
502
|
+
functionPath.node.body.body.splice(
|
|
503
|
+
1,
|
|
504
|
+
0,
|
|
505
|
+
buildAutoTodoEffect(useEffectCallee, userEventIdentifier, screenIdentifier),
|
|
506
|
+
)
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
returnPaths.forEach(returnPath => {
|
|
510
|
+
let argument = returnPath.node.argument
|
|
511
|
+
if (!argument) return
|
|
512
|
+
if (t.isJSXElement(argument)) {
|
|
513
|
+
injectBorderIntoJSX(argument)
|
|
514
|
+
}
|
|
515
|
+
if (withAppDrawer && todoPanelIdentifier) {
|
|
516
|
+
argument = wrapWithAppDrawer(argument, todoPanelIdentifier)
|
|
517
|
+
}
|
|
518
|
+
returnPath.node.argument = wrapWithVisibilityUI(argument)
|
|
519
|
+
})
|
|
520
|
+
|
|
521
|
+
return true
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
export default function componentBorderPlugin() {
|
|
525
|
+
return {
|
|
526
|
+
name: 'vite-plugin-component-border',
|
|
527
|
+
enforce: 'pre',
|
|
528
|
+
transform(code, id) {
|
|
529
|
+
// Only process JSX/TSX files, skip node_modules
|
|
530
|
+
if (!/\.[jt]sx?$/.test(id) || id.includes('node_modules')) return
|
|
531
|
+
if (!code.includes('jsx') && !/<[A-Z]/.test(code) && !/<[a-z]/.test(code)) return
|
|
532
|
+
|
|
533
|
+
let ast
|
|
534
|
+
try {
|
|
535
|
+
ast = parse(code, {
|
|
536
|
+
sourceType: 'module',
|
|
537
|
+
plugins: ['jsx', 'typescript'],
|
|
538
|
+
})
|
|
539
|
+
} catch {
|
|
540
|
+
return // unparseable file — skip
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
let modified = false
|
|
544
|
+
let cachedUseStateCallee = null
|
|
545
|
+
let cachedUseEffectCallee = null
|
|
546
|
+
let cachedUserEventIdentifier = null
|
|
547
|
+
let cachedScreenIdentifier = null
|
|
548
|
+
let todoPanelInjected = false
|
|
549
|
+
|
|
550
|
+
const injectTodoPanelComponent = (programPath) => {
|
|
551
|
+
if (todoPanelInjected) return t.identifier('__TodoPanel')
|
|
552
|
+
todoPanelInjected = true
|
|
553
|
+
|
|
554
|
+
// Build the entire TodoPanel + API Objects + SourceExplorer component as AST and inject it into the file
|
|
555
|
+
const todoPanelCode = `
|
|
556
|
+
function __TodoPanel() {
|
|
557
|
+
const [__todos, __setTodos] = __useState__Panel([]);
|
|
558
|
+
const [__input, __setInput] = __useState__Panel('');
|
|
559
|
+
const [__apiObjects, __setApiObjects] = __useState__Panel([]);
|
|
560
|
+
const [__isLoadingApi, __setIsLoadingApi] = __useState__Panel(false);
|
|
561
|
+
const [__tree, __setTree] = __useState__Panel([]);
|
|
562
|
+
const [__openedFile, __setOpenedFile] = __useState__Panel(null);
|
|
563
|
+
const [__activePath, __setActivePath] = __useState__Panel('');
|
|
564
|
+
|
|
565
|
+
__useEffect__Panel(() => {
|
|
566
|
+
fetch('/__source/tree')
|
|
567
|
+
.then(r => r.ok ? r.json() : [])
|
|
568
|
+
.then(data => Array.isArray(data) ? __setTree(data) : null)
|
|
569
|
+
.catch(() => {});
|
|
570
|
+
}, []);
|
|
571
|
+
|
|
572
|
+
const __addTodo = () => {
|
|
573
|
+
const trimmed = __input.trim();
|
|
574
|
+
if (!trimmed) return;
|
|
575
|
+
__setTodos(prev => [...prev, { id: Date.now(), text: trimmed, done: false }]);
|
|
576
|
+
__setInput('');
|
|
577
|
+
};
|
|
578
|
+
const __toggleTodo = (id) => {
|
|
579
|
+
__setTodos(prev => prev.map(t => t.id === id ? { ...t, done: !t.done } : t));
|
|
580
|
+
};
|
|
581
|
+
const __deleteTodo = (id) => {
|
|
582
|
+
__setTodos(prev => prev.filter(t => t.id !== id));
|
|
583
|
+
};
|
|
584
|
+
const __saveTodos = () => {
|
|
585
|
+
fetch('/__save-list-items', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(__todos) });
|
|
586
|
+
};
|
|
587
|
+
const __fetchApiObjects = () => {
|
|
588
|
+
__setIsLoadingApi(true);
|
|
589
|
+
fetch('https://api.restful-api.dev/objects')
|
|
590
|
+
.then(r => r.ok ? r.json() : [])
|
|
591
|
+
.then(data => { if (Array.isArray(data)) __setApiObjects(data); })
|
|
592
|
+
.catch(() => {})
|
|
593
|
+
.finally(() => __setIsLoadingApi(false));
|
|
594
|
+
};
|
|
595
|
+
const __saveApiObjects = () => {
|
|
596
|
+
fetch('/__save-api-objects', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(__apiObjects) });
|
|
597
|
+
};
|
|
598
|
+
const __openFile = (filePath) => {
|
|
599
|
+
__setActivePath(filePath);
|
|
600
|
+
fetch('/__source/file?path=' + encodeURIComponent(filePath))
|
|
601
|
+
.then(r => r.ok ? r.json() : null)
|
|
602
|
+
.then(data => { if (data && data.content) __setOpenedFile(data); })
|
|
603
|
+
.catch(() => {});
|
|
604
|
+
};
|
|
605
|
+
const __renderTree = (nodes) => {
|
|
606
|
+
return (
|
|
607
|
+
<ul style={{ listStyle: 'none', padding: '0 0 0 12px', margin: 0, fontSize: '11px' }}>
|
|
608
|
+
{nodes.map(node => (
|
|
609
|
+
<li key={node.path}>
|
|
610
|
+
{node.type === 'dir' ? (
|
|
611
|
+
<details>
|
|
612
|
+
<summary style={{ cursor: 'pointer', color: '#b0c4de', padding: '2px 0' }}>{node.name}</summary>
|
|
613
|
+
{node.children && node.children.length > 0 ? __renderTree(node.children) : null}
|
|
614
|
+
</details>
|
|
615
|
+
) : (
|
|
616
|
+
<button onClick={() => __openFile(node.path)} style={{ background: 'none', border: 'none', color: __activePath === node.path ? '#48bb78' : '#90cdf4', cursor: 'pointer', padding: '2px 0', fontSize: '11px', textAlign: 'left', width: '100%' }}>{node.name}</button>
|
|
617
|
+
)}
|
|
618
|
+
</li>
|
|
619
|
+
))}
|
|
620
|
+
</ul>
|
|
621
|
+
);
|
|
622
|
+
};
|
|
623
|
+
const __panelStyle = { color: '#f0f4f8', fontSize: '13px', width: '100%' };
|
|
624
|
+
const __inputStyle = { width: '100%', padding: '6px 8px', border: '1px solid #486581', borderRadius: '4px', background: '#1a3a5c', color: '#f0f4f8', fontSize: '12px', marginBottom: '6px', boxSizing: 'border-box' };
|
|
625
|
+
const __btnStyle = { width: '100%', padding: '5px', border: 'none', borderRadius: '4px', background: '#2d6a4f', color: '#fff', cursor: 'pointer', fontSize: '12px', marginBottom: '4px' };
|
|
626
|
+
const __btnSecondaryStyle = { width: '100%', padding: '5px', border: '1px solid #486581', borderRadius: '4px', background: 'transparent', color: '#90cdf4', cursor: 'pointer', fontSize: '12px', marginBottom: '4px' };
|
|
627
|
+
const __btnRowStyle = { display: 'flex', gap: '4px', marginBottom: '8px' };
|
|
628
|
+
const __listStyle = { listStyle: 'none', padding: 0, margin: 0 };
|
|
629
|
+
const __itemStyle = (done) => ({ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '4px 0', borderBottom: '1px solid #243b53', textDecoration: done ? 'line-through' : 'none', opacity: done ? 0.6 : 1, cursor: 'pointer', fontSize: '12px' });
|
|
630
|
+
const __deleteBtnStyle = { background: 'none', border: 'none', color: '#e63946', cursor: 'pointer', fontSize: '14px', padding: '0 4px' };
|
|
631
|
+
const __dividerStyle = { borderTop: '1px solid #243b53', margin: '12px 0', opacity: 0.5 };
|
|
632
|
+
const __apiItemStyle = { padding: '3px 0', borderBottom: '1px solid #243b53', fontSize: '11px', wordBreak: 'break-word' };
|
|
633
|
+
return (
|
|
634
|
+
<div style={__panelStyle}>
|
|
635
|
+
<h4 style={{ margin: '0 0 8px 0', fontSize: '14px' }}>Todo List</h4>
|
|
636
|
+
<input style={__inputStyle} value={__input} onChange={e => __setInput(e.target.value)} onKeyDown={e => e.key === 'Enter' && __addTodo()} placeholder="Add a todo..." />
|
|
637
|
+
<div style={__btnRowStyle}>
|
|
638
|
+
<button style={__btnStyle} onClick={__addTodo}>Add</button>
|
|
639
|
+
<button style={__btnSecondaryStyle} onClick={__saveTodos}>Save</button>
|
|
640
|
+
</div>
|
|
641
|
+
<ul style={__listStyle}>
|
|
642
|
+
{__todos.map(todo => (
|
|
643
|
+
<li key={todo.id} style={__itemStyle(todo.done)}>
|
|
644
|
+
<span onClick={() => __toggleTodo(todo.id)}>{todo.text}</span>
|
|
645
|
+
<button style={__deleteBtnStyle} onClick={() => __deleteTodo(todo.id)}>\\u00d7</button>
|
|
646
|
+
</li>
|
|
647
|
+
))}
|
|
648
|
+
</ul>
|
|
649
|
+
{__todos.length === 0 && (
|
|
650
|
+
<p style={{ margin: '4px 0', opacity: 0.5, fontSize: '11px' }}>No todos yet.</p>
|
|
651
|
+
)}
|
|
652
|
+
<div style={__dividerStyle} />
|
|
653
|
+
<h4 style={{ margin: '0 0 8px 0', fontSize: '14px' }}>API Objects</h4>
|
|
654
|
+
<div style={__btnRowStyle}>
|
|
655
|
+
<button style={__btnStyle} onClick={__fetchApiObjects} disabled={__isLoadingApi}>{__isLoadingApi ? 'Loading...' : 'Fetch'}</button>
|
|
656
|
+
<button style={__btnSecondaryStyle} onClick={__saveApiObjects} disabled={__apiObjects.length === 0}>Save</button>
|
|
657
|
+
</div>
|
|
658
|
+
<ul style={__listStyle}>
|
|
659
|
+
{__apiObjects.map(obj => (
|
|
660
|
+
<li key={obj.id} style={__apiItemStyle}>{obj.name || obj.id}</li>
|
|
661
|
+
))}
|
|
662
|
+
</ul>
|
|
663
|
+
{__apiObjects.length === 0 && (
|
|
664
|
+
<p style={{ margin: '4px 0', opacity: 0.5, fontSize: '11px' }}>No API objects loaded.</p>
|
|
665
|
+
)}
|
|
666
|
+
<div style={__dividerStyle} />
|
|
667
|
+
<h4 style={{ margin: '0 0 8px 0', fontSize: '14px' }}>Source Explorer</h4>
|
|
668
|
+
{__tree.length > 0 ? __renderTree(__tree) : <p style={{ opacity: 0.5, fontSize: '11px' }}>No source files.</p>}
|
|
669
|
+
{__openedFile && (
|
|
670
|
+
<textarea readOnly value={__openedFile.path + ':\\n' + __openedFile.content} style={{ width: '100%', minHeight: '120px', marginTop: '8px', padding: '6px', fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace', fontSize: '10px', background: '#1a3a5c', color: '#e2e8f0', border: '1px solid #486581', borderRadius: '4px', boxSizing: 'border-box', whiteSpace: 'pre', resize: 'vertical' }} />
|
|
671
|
+
)}
|
|
672
|
+
</div>
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
`
|
|
676
|
+
const todoPanelAst = parse(todoPanelCode, {
|
|
677
|
+
sourceType: 'module',
|
|
678
|
+
plugins: ['jsx'],
|
|
679
|
+
})
|
|
680
|
+
|
|
681
|
+
// Ensure LlmExplainPanel import exists
|
|
682
|
+
const llmPanelId = ensureNamedImport(
|
|
683
|
+
programPath,
|
|
684
|
+
'@test328932/test328933/runtime',
|
|
685
|
+
'LlmExplainPanel',
|
|
686
|
+
'LlmExplainPanel',
|
|
687
|
+
)
|
|
688
|
+
|
|
689
|
+
// Rename __useState__Panel and __useEffect__Panel to the actual hook callees
|
|
690
|
+
const useStateId = ensureReactHookCallee(programPath, 'useState')
|
|
691
|
+
const useEffectId = ensureReactHookCallee(programPath, 'useEffect')
|
|
692
|
+
traverseAst(todoPanelAst, {
|
|
693
|
+
Identifier(idPath) {
|
|
694
|
+
if (idPath.node.name === '__useState__Panel') {
|
|
695
|
+
idPath.node.name = useStateId.name || 'useState'
|
|
696
|
+
}
|
|
697
|
+
if (idPath.node.name === '__useEffect__Panel') {
|
|
698
|
+
idPath.node.name = useEffectId.name || 'useEffect'
|
|
699
|
+
}
|
|
700
|
+
},
|
|
701
|
+
noScope: true,
|
|
702
|
+
})
|
|
703
|
+
|
|
704
|
+
// Inject <LlmExplainPanel sourcePath="App.tsx" /> as last child of the TodoPanel's return div
|
|
705
|
+
const llmPanelElement = t.jsxElement(
|
|
706
|
+
t.jsxOpeningElement(
|
|
707
|
+
t.jsxIdentifier(llmPanelId.name || 'LlmExplainPanel'),
|
|
708
|
+
[
|
|
709
|
+
t.jsxAttribute(
|
|
710
|
+
t.jsxIdentifier('sourcePath'),
|
|
711
|
+
t.stringLiteral('App.tsx'),
|
|
712
|
+
),
|
|
713
|
+
],
|
|
714
|
+
true,
|
|
715
|
+
),
|
|
716
|
+
null,
|
|
717
|
+
[],
|
|
718
|
+
true,
|
|
719
|
+
)
|
|
720
|
+
|
|
721
|
+
// Find the top-level return statement in __TodoPanel and append LlmExplainPanel
|
|
722
|
+
// We need the return whose parent function is __TodoPanel itself, not nested functions
|
|
723
|
+
const todoPanelFunc = todoPanelAst.program.body[0]
|
|
724
|
+
if (t.isFunctionDeclaration(todoPanelFunc) && t.isBlockStatement(todoPanelFunc.body)) {
|
|
725
|
+
const bodyStatements = todoPanelFunc.body.body
|
|
726
|
+
for (let i = bodyStatements.length - 1; i >= 0; i--) {
|
|
727
|
+
const stmt = bodyStatements[i]
|
|
728
|
+
if (t.isReturnStatement(stmt) && t.isJSXElement(stmt.argument)) {
|
|
729
|
+
stmt.argument.children.push(llmPanelElement)
|
|
730
|
+
break
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// Insert the TodoPanel function before the first non-import statement
|
|
736
|
+
const funcDecl = todoPanelAst.program.body[0]
|
|
737
|
+
insertImportBeforeNonImport(programPath, funcDecl)
|
|
738
|
+
|
|
739
|
+
return t.identifier('__TodoPanel')
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
const getTodoPanelIdentifier = path => {
|
|
743
|
+
const programPath = path.findParent(p => p.isProgram())
|
|
744
|
+
return injectTodoPanelComponent(programPath)
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
const getUseStateCallee = path => {
|
|
748
|
+
if (cachedUseStateCallee) return cachedUseStateCallee
|
|
749
|
+
const programPath = path.findParent(p => p.isProgram())
|
|
750
|
+
cachedUseStateCallee = ensureUseStateCallee(programPath)
|
|
751
|
+
return cachedUseStateCallee
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
const getUseEffectCallee = path => {
|
|
755
|
+
if (cachedUseEffectCallee) return cachedUseEffectCallee
|
|
756
|
+
const programPath = path.findParent(p => p.isProgram())
|
|
757
|
+
cachedUseEffectCallee = ensureUseEffectCallee(programPath)
|
|
758
|
+
return cachedUseEffectCallee
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
const getUserEventIdentifier = path => {
|
|
762
|
+
if (cachedUserEventIdentifier) return cachedUserEventIdentifier
|
|
763
|
+
const programPath = path.findParent(p => p.isProgram())
|
|
764
|
+
cachedUserEventIdentifier = ensureDefaultImport(
|
|
765
|
+
programPath,
|
|
766
|
+
'@testing-library/user-event',
|
|
767
|
+
'userEvent',
|
|
768
|
+
)
|
|
769
|
+
return cachedUserEventIdentifier
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
const getScreenIdentifier = path => {
|
|
773
|
+
if (cachedScreenIdentifier) return cachedScreenIdentifier
|
|
774
|
+
const programPath = path.findParent(p => p.isProgram())
|
|
775
|
+
cachedScreenIdentifier = ensureNamedImport(
|
|
776
|
+
programPath,
|
|
777
|
+
'@testing-library/dom',
|
|
778
|
+
'screen',
|
|
779
|
+
'screen',
|
|
780
|
+
)
|
|
781
|
+
return cachedScreenIdentifier
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
traverseAst(ast, {
|
|
785
|
+
// Top-level arrow/function component: const Foo = () => <div />
|
|
786
|
+
VariableDeclarator(path) {
|
|
787
|
+
if (!t.isIdentifier(path.node.id) || !isComponentName(path.node.id.name)) return
|
|
788
|
+
if (!isTopLevelDeclaration(path.parentPath)) return
|
|
789
|
+
|
|
790
|
+
const { init } = path.node
|
|
791
|
+
if (!init) return
|
|
792
|
+
if (!t.isArrowFunctionExpression(init) && !t.isFunctionExpression(init)) return
|
|
793
|
+
|
|
794
|
+
const initPath = path.get('init')
|
|
795
|
+
const useStateCallee = getUseStateCallee(path)
|
|
796
|
+
const isAppComponent = path.node.id.name === 'App'
|
|
797
|
+
if (
|
|
798
|
+
transformComponentFunction(initPath, useStateCallee, {
|
|
799
|
+
withAppDrawer: isAppComponent,
|
|
800
|
+
withAutoTodo: isAppComponent,
|
|
801
|
+
useEffectCallee: isAppComponent ? getUseEffectCallee(path) : null,
|
|
802
|
+
userEventIdentifier: isAppComponent ? getUserEventIdentifier(path) : null,
|
|
803
|
+
screenIdentifier: isAppComponent ? getScreenIdentifier(path) : null,
|
|
804
|
+
todoPanelIdentifier: isAppComponent ? getTodoPanelIdentifier(path) : null,
|
|
805
|
+
})
|
|
806
|
+
) {
|
|
807
|
+
modified = true
|
|
808
|
+
}
|
|
809
|
+
},
|
|
810
|
+
|
|
811
|
+
// Top-level function component: function Foo() { return <div /> }
|
|
812
|
+
FunctionDeclaration(path) {
|
|
813
|
+
if (!path.node.id || !isComponentName(path.node.id.name)) return
|
|
814
|
+
if (!isTopLevelDeclaration(path)) return
|
|
815
|
+
|
|
816
|
+
const useStateCallee = getUseStateCallee(path)
|
|
817
|
+
const isAppComponent = path.node.id.name === 'App'
|
|
818
|
+
if (
|
|
819
|
+
transformComponentFunction(path, useStateCallee, {
|
|
820
|
+
withAppDrawer: isAppComponent,
|
|
821
|
+
withAutoTodo: isAppComponent,
|
|
822
|
+
useEffectCallee: isAppComponent ? getUseEffectCallee(path) : null,
|
|
823
|
+
userEventIdentifier: isAppComponent ? getUserEventIdentifier(path) : null,
|
|
824
|
+
screenIdentifier: isAppComponent ? getScreenIdentifier(path) : null,
|
|
825
|
+
todoPanelIdentifier: isAppComponent ? getTodoPanelIdentifier(path) : null,
|
|
826
|
+
})
|
|
827
|
+
) {
|
|
828
|
+
modified = true
|
|
829
|
+
}
|
|
830
|
+
},
|
|
831
|
+
})
|
|
832
|
+
|
|
833
|
+
if (!modified) return
|
|
834
|
+
|
|
835
|
+
const output = generateCode(ast, { retainLines: true }, code)
|
|
836
|
+
return { code: output.code, map: output.map }
|
|
837
|
+
},
|
|
838
|
+
}
|
|
839
|
+
}
|