@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.
@@ -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
+ }