@pyreon/compiler 0.11.4 → 0.11.6

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.
@@ -12,37 +12,37 @@
12
12
  * 3. MCP server `migrate_react` / `validate` tools (AI agent integration)
13
13
  */
14
14
 
15
- import ts from "typescript"
15
+ import ts from 'typescript'
16
16
 
17
17
  // ═══════════════════════════════════════════════════════════════════════════════
18
18
  // Types
19
19
  // ═══════════════════════════════════════════════════════════════════════════════
20
20
 
21
21
  export type ReactDiagnosticCode =
22
- | "react-import"
23
- | "react-dom-import"
24
- | "react-router-import"
25
- | "use-state"
26
- | "use-effect-mount"
27
- | "use-effect-deps"
28
- | "use-effect-no-deps"
29
- | "use-memo"
30
- | "use-callback"
31
- | "use-ref-dom"
32
- | "use-ref-box"
33
- | "use-reducer"
34
- | "use-layout-effect"
35
- | "memo-wrapper"
36
- | "forward-ref"
37
- | "class-name-prop"
38
- | "html-for-prop"
39
- | "on-change-input"
40
- | "dangerously-set-inner-html"
41
- | "dot-value-signal"
42
- | "array-map-jsx"
43
- | "key-on-for-child"
44
- | "create-context-import"
45
- | "use-context-import"
22
+ | 'react-import'
23
+ | 'react-dom-import'
24
+ | 'react-router-import'
25
+ | 'use-state'
26
+ | 'use-effect-mount'
27
+ | 'use-effect-deps'
28
+ | 'use-effect-no-deps'
29
+ | 'use-memo'
30
+ | 'use-callback'
31
+ | 'use-ref-dom'
32
+ | 'use-ref-box'
33
+ | 'use-reducer'
34
+ | 'use-layout-effect'
35
+ | 'memo-wrapper'
36
+ | 'forward-ref'
37
+ | 'class-name-prop'
38
+ | 'html-for-prop'
39
+ | 'on-change-input'
40
+ | 'dangerously-set-inner-html'
41
+ | 'dot-value-signal'
42
+ | 'array-map-jsx'
43
+ | 'key-on-for-child'
44
+ | 'create-context-import'
45
+ | 'use-context-import'
46
46
 
47
47
  export interface ReactDiagnostic {
48
48
  /** Machine-readable code for filtering and programmatic handling */
@@ -62,7 +62,7 @@ export interface ReactDiagnostic {
62
62
  }
63
63
 
64
64
  export interface MigrationChange {
65
- type: "replace" | "remove" | "add"
65
+ type: 'replace' | 'remove' | 'add'
66
66
  line: number
67
67
  description: string
68
68
  }
@@ -89,87 +89,87 @@ interface HookMapping {
89
89
 
90
90
  const _REACT_HOOK_MAP: Record<string, HookMapping> = {
91
91
  useState: {
92
- pyreonFn: "signal",
93
- pyreonImport: "@pyreon/reactivity",
94
- description: "Signals are callable functions — read: count(), write: count.set(5)",
92
+ pyreonFn: 'signal',
93
+ pyreonImport: '@pyreon/reactivity',
94
+ description: 'Signals are callable functions — read: count(), write: count.set(5)',
95
95
  example:
96
- "const count = signal(0)\n// Read: count() Write: count.set(5) Update: count.update(n => n + 1)",
96
+ 'const count = signal(0)\n// Read: count() Write: count.set(5) Update: count.update(n => n + 1)',
97
97
  },
98
98
  useEffect: {
99
- pyreonFn: "effect",
100
- pyreonImport: "@pyreon/reactivity",
101
- description: "Effects auto-track signal dependencies — no dependency array needed",
102
- example: "effect(() => {\n console.log(count()) // auto-subscribes to count\n})",
99
+ pyreonFn: 'effect',
100
+ pyreonImport: '@pyreon/reactivity',
101
+ description: 'Effects auto-track signal dependencies — no dependency array needed',
102
+ example: 'effect(() => {\n console.log(count()) // auto-subscribes to count\n})',
103
103
  },
104
104
  useLayoutEffect: {
105
- pyreonFn: "effect",
106
- pyreonImport: "@pyreon/reactivity",
107
- description: "Pyreon effects run synchronously after signal updates",
108
- example: "effect(() => {\n // runs sync after signal changes\n})",
105
+ pyreonFn: 'effect',
106
+ pyreonImport: '@pyreon/reactivity',
107
+ description: 'Pyreon effects run synchronously after signal updates',
108
+ example: 'effect(() => {\n // runs sync after signal changes\n})',
109
109
  },
110
110
  useMemo: {
111
- pyreonFn: "computed",
112
- pyreonImport: "@pyreon/reactivity",
113
- description: "Computed values auto-track dependencies and memoize",
114
- example: "const doubled = computed(() => count() * 2)",
111
+ pyreonFn: 'computed',
112
+ pyreonImport: '@pyreon/reactivity',
113
+ description: 'Computed values auto-track dependencies and memoize',
114
+ example: 'const doubled = computed(() => count() * 2)',
115
115
  },
116
116
  useCallback: {
117
- pyreonFn: "(plain function)",
118
- pyreonImport: "",
117
+ pyreonFn: '(plain function)',
118
+ pyreonImport: '',
119
119
  description:
120
- "Not needed — Pyreon components run once, so closures never go stale. Use a plain function",
121
- example: "const handleClick = () => doSomething(count())",
120
+ 'Not needed — Pyreon components run once, so closures never go stale. Use a plain function',
121
+ example: 'const handleClick = () => doSomething(count())',
122
122
  },
123
123
  useReducer: {
124
- pyreonFn: "signal",
125
- pyreonImport: "@pyreon/reactivity",
126
- description: "Use signal with update() for reducer-like patterns",
124
+ pyreonFn: 'signal',
125
+ pyreonImport: '@pyreon/reactivity',
126
+ description: 'Use signal with update() for reducer-like patterns',
127
127
  example:
128
- "const state = signal(initialState)\nconst dispatch = (action) => state.update(s => reducer(s, action))",
128
+ 'const state = signal(initialState)\nconst dispatch = (action) => state.update(s => reducer(s, action))',
129
129
  },
130
130
  }
131
131
 
132
132
  /** React import sources → Pyreon equivalents */
133
133
  const IMPORT_REWRITES: Record<string, string | null> = {
134
- react: "@pyreon/core",
135
- "react-dom": "@pyreon/runtime-dom",
136
- "react-dom/client": "@pyreon/runtime-dom",
137
- "react-dom/server": "@pyreon/runtime-server",
138
- "react-router": "@pyreon/router",
139
- "react-router-dom": "@pyreon/router",
134
+ react: '@pyreon/core',
135
+ 'react-dom': '@pyreon/runtime-dom',
136
+ 'react-dom/client': '@pyreon/runtime-dom',
137
+ 'react-dom/server': '@pyreon/runtime-server',
138
+ 'react-router': '@pyreon/router',
139
+ 'react-router-dom': '@pyreon/router',
140
140
  }
141
141
 
142
142
  /** React specifiers that map to specific Pyreon imports */
143
143
  const SPECIFIER_REWRITES: Record<string, { name: string; from: string }> = {
144
- useState: { name: "signal", from: "@pyreon/reactivity" },
145
- useEffect: { name: "effect", from: "@pyreon/reactivity" },
146
- useLayoutEffect: { name: "effect", from: "@pyreon/reactivity" },
147
- useMemo: { name: "computed", from: "@pyreon/reactivity" },
148
- useReducer: { name: "signal", from: "@pyreon/reactivity" },
149
- useRef: { name: "signal", from: "@pyreon/reactivity" },
150
- createContext: { name: "createContext", from: "@pyreon/core" },
151
- useContext: { name: "useContext", from: "@pyreon/core" },
152
- Fragment: { name: "Fragment", from: "@pyreon/core" },
153
- Suspense: { name: "Suspense", from: "@pyreon/core" },
154
- lazy: { name: "lazy", from: "@pyreon/core" },
155
- memo: { name: "", from: "" }, // removed, not needed
156
- forwardRef: { name: "", from: "" }, // removed, not needed
157
- createRoot: { name: "mount", from: "@pyreon/runtime-dom" },
158
- hydrateRoot: { name: "hydrateRoot", from: "@pyreon/runtime-dom" },
144
+ useState: { name: 'signal', from: '@pyreon/reactivity' },
145
+ useEffect: { name: 'effect', from: '@pyreon/reactivity' },
146
+ useLayoutEffect: { name: 'effect', from: '@pyreon/reactivity' },
147
+ useMemo: { name: 'computed', from: '@pyreon/reactivity' },
148
+ useReducer: { name: 'signal', from: '@pyreon/reactivity' },
149
+ useRef: { name: 'signal', from: '@pyreon/reactivity' },
150
+ createContext: { name: 'createContext', from: '@pyreon/core' },
151
+ useContext: { name: 'useContext', from: '@pyreon/core' },
152
+ Fragment: { name: 'Fragment', from: '@pyreon/core' },
153
+ Suspense: { name: 'Suspense', from: '@pyreon/core' },
154
+ lazy: { name: 'lazy', from: '@pyreon/core' },
155
+ memo: { name: '', from: '' }, // removed, not needed
156
+ forwardRef: { name: '', from: '' }, // removed, not needed
157
+ createRoot: { name: 'mount', from: '@pyreon/runtime-dom' },
158
+ hydrateRoot: { name: 'hydrateRoot', from: '@pyreon/runtime-dom' },
159
159
  // React Router
160
- useNavigate: { name: "useRouter", from: "@pyreon/router" },
161
- useParams: { name: "useRoute", from: "@pyreon/router" },
162
- useLocation: { name: "useRoute", from: "@pyreon/router" },
163
- Link: { name: "RouterLink", from: "@pyreon/router" },
164
- NavLink: { name: "RouterLink", from: "@pyreon/router" },
165
- Outlet: { name: "RouterView", from: "@pyreon/router" },
166
- useSearchParams: { name: "useSearchParams", from: "@pyreon/router" },
160
+ useNavigate: { name: 'useRouter', from: '@pyreon/router' },
161
+ useParams: { name: 'useRoute', from: '@pyreon/router' },
162
+ useLocation: { name: 'useRoute', from: '@pyreon/router' },
163
+ Link: { name: 'RouterLink', from: '@pyreon/router' },
164
+ NavLink: { name: 'RouterLink', from: '@pyreon/router' },
165
+ Outlet: { name: 'RouterView', from: '@pyreon/router' },
166
+ useSearchParams: { name: 'useSearchParams', from: '@pyreon/router' },
167
167
  }
168
168
 
169
169
  /** JSX attribute rewrites (React → standard HTML) */
170
170
  const JSX_ATTR_REWRITES: Record<string, string> = {
171
- className: "class",
172
- htmlFor: "for",
171
+ className: 'class',
172
+ htmlFor: 'for',
173
173
  }
174
174
 
175
175
  // ═══════════════════════════════════════════════════════════════════════════════
@@ -220,11 +220,11 @@ function detectImportDeclaration(ctx: DetectContext, node: ts.ImportDeclaration)
220
220
  }
221
221
  }
222
222
 
223
- const diagCode = source.startsWith("react-router")
224
- ? "react-router-import"
225
- : source.startsWith("react-dom")
226
- ? "react-dom-import"
227
- : "react-import"
223
+ const diagCode = source.startsWith('react-router')
224
+ ? 'react-router-import'
225
+ : source.startsWith('react-dom')
226
+ ? 'react-dom-import'
227
+ : 'react-import'
228
228
 
229
229
  detectDiag(
230
230
  ctx,
@@ -234,7 +234,7 @@ function detectImportDeclaration(ctx: DetectContext, node: ts.ImportDeclaration)
234
234
  detectGetNodeText(ctx, node),
235
235
  pyreonSource
236
236
  ? `import { ... } from "${pyreonSource}"`
237
- : "Remove this import — not needed in Pyreon",
237
+ : 'Remove this import — not needed in Pyreon',
238
238
  true,
239
239
  )
240
240
  }
@@ -250,13 +250,13 @@ function detectUseState(ctx: DetectContext, node: ts.CallExpression): void {
250
250
  ) {
251
251
  const firstEl = parent.name.elements[0]
252
252
  const valueName =
253
- firstEl && ts.isBindingElement(firstEl) ? (firstEl.name as ts.Identifier).text : "value"
254
- const initArg = node.arguments[0] ? detectGetNodeText(ctx, node.arguments[0]) : "undefined"
253
+ firstEl && ts.isBindingElement(firstEl) ? (firstEl.name as ts.Identifier).text : 'value'
254
+ const initArg = node.arguments[0] ? detectGetNodeText(ctx, node.arguments[0]) : 'undefined'
255
255
 
256
256
  detectDiag(
257
257
  ctx,
258
258
  node,
259
- "use-state",
259
+ 'use-state',
260
260
  `useState is a React API. In Pyreon, use signal(). Read: ${valueName}(), Write: ${valueName}.set(x)`,
261
261
  detectGetNodeText(ctx, parent),
262
262
  `${valueName} = signal(${initArg})`,
@@ -266,10 +266,10 @@ function detectUseState(ctx: DetectContext, node: ts.CallExpression): void {
266
266
  detectDiag(
267
267
  ctx,
268
268
  node,
269
- "use-state",
270
- "useState is a React API. In Pyreon, use signal().",
269
+ 'use-state',
270
+ 'useState is a React API. In Pyreon, use signal().',
271
271
  detectGetNodeText(ctx, node),
272
- "signal(initialValue)",
272
+ 'signal(initialValue)',
273
273
  true,
274
274
  )
275
275
  }
@@ -296,32 +296,32 @@ function detectUseEffect(ctx: DetectContext, node: ts.CallExpression): void {
296
296
  detectDiag(
297
297
  ctx,
298
298
  node,
299
- "use-effect-mount",
299
+ 'use-effect-mount',
300
300
  `${hookName} with empty deps [] means "run once on mount". Use onMount() in Pyreon.`,
301
301
  detectGetNodeText(ctx, node),
302
302
  hasCleanup
303
- ? "onMount(() => {\n // setup...\n return () => { /* cleanup */ }\n})"
304
- : "onMount(() => {\n // setup...\n})",
303
+ ? 'onMount(() => {\n // setup...\n return () => { /* cleanup */ }\n})'
304
+ : 'onMount(() => {\n // setup...\n})',
305
305
  true,
306
306
  )
307
307
  } else if (depsArg && ts.isArrayLiteralExpression(depsArg)) {
308
308
  detectDiag(
309
309
  ctx,
310
310
  node,
311
- "use-effect-deps",
311
+ 'use-effect-deps',
312
312
  `${hookName} with dependency array. In Pyreon, effect() auto-tracks dependencies — no array needed.`,
313
313
  detectGetNodeText(ctx, node),
314
- "effect(() => {\n // reads are auto-tracked\n})",
314
+ 'effect(() => {\n // reads are auto-tracked\n})',
315
315
  true,
316
316
  )
317
317
  } else if (!depsArg) {
318
318
  detectDiag(
319
319
  ctx,
320
320
  node,
321
- "use-effect-no-deps",
321
+ 'use-effect-no-deps',
322
322
  `${hookName} with no dependency array. In Pyreon, use effect() — it auto-tracks signal reads.`,
323
323
  detectGetNodeText(ctx, node),
324
- "effect(() => {\n // runs when accessed signals change\n})",
324
+ 'effect(() => {\n // runs when accessed signals change\n})',
325
325
  true,
326
326
  )
327
327
  }
@@ -329,13 +329,13 @@ function detectUseEffect(ctx: DetectContext, node: ts.CallExpression): void {
329
329
 
330
330
  function detectUseMemo(ctx: DetectContext, node: ts.CallExpression): void {
331
331
  const computeFn = node.arguments[0]
332
- const computeText = computeFn ? detectGetNodeText(ctx, computeFn) : "() => value"
332
+ const computeText = computeFn ? detectGetNodeText(ctx, computeFn) : '() => value'
333
333
 
334
334
  detectDiag(
335
335
  ctx,
336
336
  node,
337
- "use-memo",
338
- "useMemo is a React API. In Pyreon, use computed() — dependencies auto-tracked.",
337
+ 'use-memo',
338
+ 'useMemo is a React API. In Pyreon, use computed() — dependencies auto-tracked.',
339
339
  detectGetNodeText(ctx, node),
340
340
  `computed(${computeText})`,
341
341
  true,
@@ -344,13 +344,13 @@ function detectUseMemo(ctx: DetectContext, node: ts.CallExpression): void {
344
344
 
345
345
  function detectUseCallback(ctx: DetectContext, node: ts.CallExpression): void {
346
346
  const callbackFn = node.arguments[0]
347
- const callbackText = callbackFn ? detectGetNodeText(ctx, callbackFn) : "() => {}"
347
+ const callbackText = callbackFn ? detectGetNodeText(ctx, callbackFn) : '() => {}'
348
348
 
349
349
  detectDiag(
350
350
  ctx,
351
351
  node,
352
- "use-callback",
353
- "useCallback is not needed in Pyreon. Components run once, so closures never go stale. Use a plain function.",
352
+ 'use-callback',
353
+ 'useCallback is not needed in Pyreon. Components run once, so closures never go stale. Use a plain function.',
354
354
  detectGetNodeText(ctx, node),
355
355
  callbackText,
356
356
  true,
@@ -361,25 +361,25 @@ function detectUseRef(ctx: DetectContext, node: ts.CallExpression): void {
361
361
  const arg = node.arguments[0]
362
362
  const isNullInit =
363
363
  arg &&
364
- (arg.kind === ts.SyntaxKind.NullKeyword || (ts.isIdentifier(arg) && arg.text === "undefined"))
364
+ (arg.kind === ts.SyntaxKind.NullKeyword || (ts.isIdentifier(arg) && arg.text === 'undefined'))
365
365
 
366
366
  if (isNullInit) {
367
367
  detectDiag(
368
368
  ctx,
369
369
  node,
370
- "use-ref-dom",
371
- "useRef(null) for DOM refs. In Pyreon, use createRef() from @pyreon/core.",
370
+ 'use-ref-dom',
371
+ 'useRef(null) for DOM refs. In Pyreon, use createRef() from @pyreon/core.',
372
372
  detectGetNodeText(ctx, node),
373
- "createRef()",
373
+ 'createRef()',
374
374
  true,
375
375
  )
376
376
  } else {
377
- const initText = arg ? detectGetNodeText(ctx, arg) : "undefined"
377
+ const initText = arg ? detectGetNodeText(ctx, arg) : 'undefined'
378
378
  detectDiag(
379
379
  ctx,
380
380
  node,
381
- "use-ref-box",
382
- "useRef for mutable values. In Pyreon, use signal() — it works the same way but is reactive.",
381
+ 'use-ref-box',
382
+ 'useRef for mutable values. In Pyreon, use signal() — it works the same way but is reactive.',
383
383
  detectGetNodeText(ctx, node),
384
384
  `signal(${initText})`,
385
385
  true,
@@ -391,10 +391,10 @@ function detectUseReducer(ctx: DetectContext, node: ts.CallExpression): void {
391
391
  detectDiag(
392
392
  ctx,
393
393
  node,
394
- "use-reducer",
395
- "useReducer is a React API. In Pyreon, use signal() with update() for reducer patterns.",
394
+ 'use-reducer',
395
+ 'useReducer is a React API. In Pyreon, use signal() with update() for reducer patterns.',
396
396
  detectGetNodeText(ctx, node),
397
- "const state = signal(initialState)\nconst dispatch = (action) => state.update(s => reducer(s, action))",
397
+ 'const state = signal(initialState)\nconst dispatch = (action) => state.update(s => reducer(s, action))',
398
398
  false,
399
399
  )
400
400
  }
@@ -403,7 +403,7 @@ function isCallToReactDot(callee: ts.Expression, methodName: string): boolean {
403
403
  return (
404
404
  ts.isPropertyAccessExpression(callee) &&
405
405
  ts.isIdentifier(callee.expression) &&
406
- callee.expression.text === "React" &&
406
+ callee.expression.text === 'React' &&
407
407
  callee.name.text === methodName
408
408
  )
409
409
  }
@@ -411,17 +411,17 @@ function isCallToReactDot(callee: ts.Expression, methodName: string): boolean {
411
411
  function detectMemoWrapper(ctx: DetectContext, node: ts.CallExpression): void {
412
412
  const callee = node.expression
413
413
  const isMemo =
414
- (ts.isIdentifier(callee) && callee.text === "memo") || isCallToReactDot(callee, "memo")
414
+ (ts.isIdentifier(callee) && callee.text === 'memo') || isCallToReactDot(callee, 'memo')
415
415
 
416
416
  if (isMemo) {
417
417
  const inner = node.arguments[0]
418
- const innerText = inner ? detectGetNodeText(ctx, inner) : "Component"
418
+ const innerText = inner ? detectGetNodeText(ctx, inner) : 'Component'
419
419
 
420
420
  detectDiag(
421
421
  ctx,
422
422
  node,
423
- "memo-wrapper",
424
- "memo() is not needed in Pyreon. Components run once — only signals trigger updates, not re-renders.",
423
+ 'memo-wrapper',
424
+ 'memo() is not needed in Pyreon. Components run once — only signals trigger updates, not re-renders.',
425
425
  detectGetNodeText(ctx, node),
426
426
  innerText,
427
427
  true,
@@ -432,17 +432,17 @@ function detectMemoWrapper(ctx: DetectContext, node: ts.CallExpression): void {
432
432
  function detectForwardRef(ctx: DetectContext, node: ts.CallExpression): void {
433
433
  const callee = node.expression
434
434
  const isForwardRef =
435
- (ts.isIdentifier(callee) && callee.text === "forwardRef") ||
436
- isCallToReactDot(callee, "forwardRef")
435
+ (ts.isIdentifier(callee) && callee.text === 'forwardRef') ||
436
+ isCallToReactDot(callee, 'forwardRef')
437
437
 
438
438
  if (isForwardRef) {
439
439
  detectDiag(
440
440
  ctx,
441
441
  node,
442
- "forward-ref",
443
- "forwardRef is not needed in Pyreon. Pass ref as a regular prop.",
442
+ 'forward-ref',
443
+ 'forwardRef is not needed in Pyreon. Pass ref as a regular prop.',
444
444
  detectGetNodeText(ctx, node),
445
- "// Just pass ref as a prop:\nconst MyInput = (props) => <input ref={props.ref} />",
445
+ '// Just pass ref as a prop:\nconst MyInput = (props) => <input ref={props.ref} />',
446
446
  true,
447
447
  )
448
448
  }
@@ -456,7 +456,7 @@ function detectJsxAttributes(ctx: DetectContext, node: ts.JsxAttribute): void {
456
456
  detectDiag(
457
457
  ctx,
458
458
  node,
459
- attrName === "className" ? "class-name-prop" : "html-for-prop",
459
+ attrName === 'className' ? 'class-name-prop' : 'html-for-prop',
460
460
  `'${attrName}' is a React JSX attribute. Use '${htmlAttr}' in Pyreon (standard HTML).`,
461
461
  detectGetNodeText(ctx, node),
462
462
  detectGetNodeText(ctx, node).replace(attrName, htmlAttr),
@@ -464,32 +464,32 @@ function detectJsxAttributes(ctx: DetectContext, node: ts.JsxAttribute): void {
464
464
  )
465
465
  }
466
466
 
467
- if (attrName === "onChange") {
467
+ if (attrName === 'onChange') {
468
468
  const jsxElement = findParentJsxElement(node)
469
469
  if (jsxElement) {
470
470
  const tagName = getJsxTagName(jsxElement)
471
- if (tagName === "input" || tagName === "textarea" || tagName === "select") {
471
+ if (tagName === 'input' || tagName === 'textarea' || tagName === 'select') {
472
472
  detectDiag(
473
473
  ctx,
474
474
  node,
475
- "on-change-input",
475
+ 'on-change-input',
476
476
  `onChange on <${tagName}> fires on blur in Pyreon (native DOM behavior). For keypress-by-keypress updates, use onInput.`,
477
477
  detectGetNodeText(ctx, node),
478
- detectGetNodeText(ctx, node).replace("onChange", "onInput"),
478
+ detectGetNodeText(ctx, node).replace('onChange', 'onInput'),
479
479
  true,
480
480
  )
481
481
  }
482
482
  }
483
483
  }
484
484
 
485
- if (attrName === "dangerouslySetInnerHTML") {
485
+ if (attrName === 'dangerouslySetInnerHTML') {
486
486
  detectDiag(
487
487
  ctx,
488
488
  node,
489
- "dangerously-set-inner-html",
490
- "dangerouslySetInnerHTML is React-specific. Use innerHTML prop in Pyreon.",
489
+ 'dangerously-set-inner-html',
490
+ 'dangerouslySetInnerHTML is React-specific. Use innerHTML prop in Pyreon.',
491
491
  detectGetNodeText(ctx, node),
492
- "innerHTML={htmlString}",
492
+ 'innerHTML={htmlString}',
493
493
  true,
494
494
  )
495
495
  }
@@ -502,7 +502,7 @@ function detectDotValueSignal(ctx: DetectContext, node: ts.PropertyAccessExpress
502
502
  detectDiag(
503
503
  ctx,
504
504
  node,
505
- "dot-value-signal",
505
+ 'dot-value-signal',
506
506
  `'${varName}.value' looks like a Vue ref pattern. Pyreon signals are callable functions. Use ${varName}.set(x) to write.`,
507
507
  detectGetNodeText(ctx, parent),
508
508
  `${varName}.set(${detectGetNodeText(ctx, parent.right)})`,
@@ -521,13 +521,13 @@ function detectArrayMapJsx(ctx: DetectContext, node: ts.CallExpression): void {
521
521
  const mapCallback = node.arguments[0]
522
522
  const mapCallbackText = mapCallback
523
523
  ? detectGetNodeText(ctx, mapCallback)
524
- : "item => <li>{item}</li>"
524
+ : 'item => <li>{item}</li>'
525
525
 
526
526
  detectDiag(
527
527
  ctx,
528
528
  node,
529
- "array-map-jsx",
530
- "Array.map() in JSX is not reactive in Pyreon. Use <For> for efficient keyed list rendering.",
529
+ 'array-map-jsx',
530
+ 'Array.map() in JSX is not reactive in Pyreon. Use <For> for efficient keyed list rendering.',
531
531
  detectGetNodeText(ctx, node),
532
532
  `<For each={${arrayExpr}} by={item => item.id}>\n {${mapCallbackText}}\n</For>`,
533
533
  false,
@@ -547,7 +547,7 @@ function isCallToEffectHook(node: ts.Node): node is ts.CallExpression {
547
547
  return (
548
548
  ts.isCallExpression(node) &&
549
549
  ts.isIdentifier(node.expression) &&
550
- (node.expression.text === "useEffect" || node.expression.text === "useLayoutEffect")
550
+ (node.expression.text === 'useEffect' || node.expression.text === 'useLayoutEffect')
551
551
  )
552
552
  }
553
553
 
@@ -556,7 +556,7 @@ function isMapCallExpression(node: ts.Node): node is ts.CallExpression {
556
556
  ts.isCallExpression(node) &&
557
557
  ts.isPropertyAccessExpression(node.expression) &&
558
558
  ts.isIdentifier(node.expression.name) &&
559
- node.expression.name.text === "map"
559
+ node.expression.name.text === 'map'
560
560
  )
561
561
  }
562
562
 
@@ -564,19 +564,19 @@ function isDotValueAccess(node: ts.Node): node is ts.PropertyAccessExpression {
564
564
  return (
565
565
  ts.isPropertyAccessExpression(node) &&
566
566
  ts.isIdentifier(node.name) &&
567
- node.name.text === "value" &&
567
+ node.name.text === 'value' &&
568
568
  ts.isIdentifier(node.expression)
569
569
  )
570
570
  }
571
571
 
572
572
  function detectVisitNode(ctx: DetectContext, node: ts.Node): void {
573
573
  if (ts.isImportDeclaration(node)) detectImportDeclaration(ctx, node)
574
- if (isCallToHook(node, "useState")) detectUseState(ctx, node)
574
+ if (isCallToHook(node, 'useState')) detectUseState(ctx, node)
575
575
  if (isCallToEffectHook(node)) detectUseEffect(ctx, node)
576
- if (isCallToHook(node, "useMemo")) detectUseMemo(ctx, node)
577
- if (isCallToHook(node, "useCallback")) detectUseCallback(ctx, node)
578
- if (isCallToHook(node, "useRef")) detectUseRef(ctx, node)
579
- if (isCallToHook(node, "useReducer")) detectUseReducer(ctx, node)
576
+ if (isCallToHook(node, 'useMemo')) detectUseMemo(ctx, node)
577
+ if (isCallToHook(node, 'useCallback')) detectUseCallback(ctx, node)
578
+ if (isCallToHook(node, 'useRef')) detectUseRef(ctx, node)
579
+ if (isCallToHook(node, 'useReducer')) detectUseReducer(ctx, node)
580
580
  if (ts.isCallExpression(node)) detectMemoWrapper(ctx, node)
581
581
  if (ts.isCallExpression(node)) detectForwardRef(ctx, node)
582
582
  if (ts.isJsxAttribute(node) && ts.isIdentifier(node.name)) detectJsxAttributes(ctx, node)
@@ -591,7 +591,7 @@ function detectVisit(ctx: DetectContext, node: ts.Node): void {
591
591
  })
592
592
  }
593
593
 
594
- export function detectReactPatterns(code: string, filename = "input.tsx"): ReactDiagnostic[] {
594
+ export function detectReactPatterns(code: string, filename = 'input.tsx'): ReactDiagnostic[] {
595
595
  const sf = ts.createSourceFile(filename, code, ts.ScriptTarget.ESNext, true, ts.ScriptKind.TSX)
596
596
  const ctx: DetectContext = {
597
597
  sf,
@@ -672,8 +672,8 @@ function migrateUseState(ctx: MigrateContext, node: ts.CallExpression): void {
672
672
  ) {
673
673
  const firstEl = parent.name.elements[0]
674
674
  const valueName =
675
- firstEl && ts.isBindingElement(firstEl) ? (firstEl.name as ts.Identifier).text : "value"
676
- const initArg = node.arguments[0] ? migrateGetNodeText(ctx, node.arguments[0]) : "undefined"
675
+ firstEl && ts.isBindingElement(firstEl) ? (firstEl.name as ts.Identifier).text : 'value'
676
+ const initArg = node.arguments[0] ? migrateGetNodeText(ctx, node.arguments[0]) : 'undefined'
677
677
 
678
678
  const declStart = parent.getStart(ctx.sf)
679
679
  const declEnd = parent.getEnd()
@@ -682,9 +682,9 @@ function migrateUseState(ctx: MigrateContext, node: ts.CallExpression): void {
682
682
  end: declEnd,
683
683
  text: `${valueName} = signal(${initArg})`,
684
684
  })
685
- migrateAddImport(ctx, "@pyreon/reactivity", "signal")
685
+ migrateAddImport(ctx, '@pyreon/reactivity', 'signal')
686
686
  ctx.changes.push({
687
- type: "replace",
687
+ type: 'replace',
688
688
  line: migrateGetLine(ctx, node),
689
689
  description: `useState → signal: ${valueName}`,
690
690
  })
@@ -704,18 +704,18 @@ function migrateUseEffect(ctx: MigrateContext, node: ts.CallExpression): void {
704
704
  ) {
705
705
  const callbackText = migrateGetNodeText(ctx, callbackArg)
706
706
  migrateReplace(ctx, node, `onMount(${callbackText})`)
707
- migrateAddImport(ctx, "@pyreon/core", "onMount")
707
+ migrateAddImport(ctx, '@pyreon/core', 'onMount')
708
708
  ctx.changes.push({
709
- type: "replace",
709
+ type: 'replace',
710
710
  line: migrateGetLine(ctx, node),
711
711
  description: `${hookName}(fn, []) → onMount(fn)`,
712
712
  })
713
713
  } else if (callbackArg) {
714
714
  const callbackText = migrateGetNodeText(ctx, callbackArg)
715
715
  migrateReplace(ctx, node, `effect(${callbackText})`)
716
- migrateAddImport(ctx, "@pyreon/reactivity", "effect")
716
+ migrateAddImport(ctx, '@pyreon/reactivity', 'effect')
717
717
  ctx.changes.push({
718
- type: "replace",
718
+ type: 'replace',
719
719
  line: migrateGetLine(ctx, node),
720
720
  description: `${hookName} → effect (auto-tracks deps)`,
721
721
  })
@@ -726,11 +726,11 @@ function migrateUseMemo(ctx: MigrateContext, node: ts.CallExpression): void {
726
726
  const computeFn = node.arguments[0]
727
727
  if (computeFn) {
728
728
  migrateReplace(ctx, node, `computed(${migrateGetNodeText(ctx, computeFn)})`)
729
- migrateAddImport(ctx, "@pyreon/reactivity", "computed")
729
+ migrateAddImport(ctx, '@pyreon/reactivity', 'computed')
730
730
  ctx.changes.push({
731
- type: "replace",
731
+ type: 'replace',
732
732
  line: migrateGetLine(ctx, node),
733
- description: "useMemo → computed (auto-tracks deps)",
733
+ description: 'useMemo → computed (auto-tracks deps)',
734
734
  })
735
735
  }
736
736
  }
@@ -740,9 +740,9 @@ function migrateUseCallback(ctx: MigrateContext, node: ts.CallExpression): void
740
740
  if (callbackFn) {
741
741
  migrateReplace(ctx, node, migrateGetNodeText(ctx, callbackFn))
742
742
  ctx.changes.push({
743
- type: "replace",
743
+ type: 'replace',
744
744
  line: migrateGetLine(ctx, node),
745
- description: "useCallback → plain function (not needed in Pyreon)",
745
+ description: 'useCallback → plain function (not needed in Pyreon)',
746
746
  })
747
747
  }
748
748
  }
@@ -751,23 +751,23 @@ function migrateUseRef(ctx: MigrateContext, node: ts.CallExpression): void {
751
751
  const arg = node.arguments[0]
752
752
  const isNullInit =
753
753
  arg &&
754
- (arg.kind === ts.SyntaxKind.NullKeyword || (ts.isIdentifier(arg) && arg.text === "undefined"))
754
+ (arg.kind === ts.SyntaxKind.NullKeyword || (ts.isIdentifier(arg) && arg.text === 'undefined'))
755
755
 
756
756
  if (isNullInit || !arg) {
757
- migrateReplace(ctx, node, "createRef()")
758
- migrateAddImport(ctx, "@pyreon/core", "createRef")
757
+ migrateReplace(ctx, node, 'createRef()')
758
+ migrateAddImport(ctx, '@pyreon/core', 'createRef')
759
759
  ctx.changes.push({
760
- type: "replace",
760
+ type: 'replace',
761
761
  line: migrateGetLine(ctx, node),
762
- description: "useRef(null) → createRef()",
762
+ description: 'useRef(null) → createRef()',
763
763
  })
764
764
  } else {
765
765
  migrateReplace(ctx, node, `signal(${migrateGetNodeText(ctx, arg)})`)
766
- migrateAddImport(ctx, "@pyreon/reactivity", "signal")
766
+ migrateAddImport(ctx, '@pyreon/reactivity', 'signal')
767
767
  ctx.changes.push({
768
- type: "replace",
768
+ type: 'replace',
769
769
  line: migrateGetLine(ctx, node),
770
- description: "useRef(value) → signal(value)",
770
+ description: 'useRef(value) → signal(value)',
771
771
  })
772
772
  }
773
773
  }
@@ -775,14 +775,14 @@ function migrateUseRef(ctx: MigrateContext, node: ts.CallExpression): void {
775
775
  function migrateMemoWrapper(ctx: MigrateContext, node: ts.CallExpression): void {
776
776
  const callee = node.expression
777
777
  const isMemo =
778
- (ts.isIdentifier(callee) && callee.text === "memo") || isCallToReactDot(callee, "memo")
778
+ (ts.isIdentifier(callee) && callee.text === 'memo') || isCallToReactDot(callee, 'memo')
779
779
 
780
780
  if (isMemo && node.arguments[0]) {
781
781
  migrateReplace(ctx, node, migrateGetNodeText(ctx, node.arguments[0]))
782
782
  ctx.changes.push({
783
- type: "remove",
783
+ type: 'remove',
784
784
  line: migrateGetLine(ctx, node),
785
- description: "Removed memo() wrapper (not needed in Pyreon)",
785
+ description: 'Removed memo() wrapper (not needed in Pyreon)',
786
786
  })
787
787
  }
788
788
  }
@@ -790,15 +790,15 @@ function migrateMemoWrapper(ctx: MigrateContext, node: ts.CallExpression): void
790
790
  function migrateForwardRef(ctx: MigrateContext, node: ts.CallExpression): void {
791
791
  const callee = node.expression
792
792
  const isForwardRef =
793
- (ts.isIdentifier(callee) && callee.text === "forwardRef") ||
794
- isCallToReactDot(callee, "forwardRef")
793
+ (ts.isIdentifier(callee) && callee.text === 'forwardRef') ||
794
+ isCallToReactDot(callee, 'forwardRef')
795
795
 
796
796
  if (isForwardRef && node.arguments[0]) {
797
797
  migrateReplace(ctx, node, migrateGetNodeText(ctx, node.arguments[0]))
798
798
  ctx.changes.push({
799
- type: "remove",
799
+ type: 'remove',
800
800
  line: migrateGetLine(ctx, node),
801
- description: "Removed forwardRef wrapper (pass ref as normal prop in Pyreon)",
801
+ description: 'Removed forwardRef wrapper (pass ref as normal prop in Pyreon)',
802
802
  })
803
803
  }
804
804
  }
@@ -814,24 +814,24 @@ function migrateJsxAttributes(ctx: MigrateContext, node: ts.JsxAttribute): void
814
814
  text: htmlAttr,
815
815
  })
816
816
  ctx.changes.push({
817
- type: "replace",
817
+ type: 'replace',
818
818
  line: migrateGetLine(ctx, node),
819
819
  description: `${attrName} → ${htmlAttr}`,
820
820
  })
821
821
  }
822
822
 
823
- if (attrName === "onChange") {
823
+ if (attrName === 'onChange') {
824
824
  const jsxElement = findParentJsxElement(node)
825
825
  if (jsxElement) {
826
826
  const tagName = getJsxTagName(jsxElement)
827
- if (tagName === "input" || tagName === "textarea" || tagName === "select") {
827
+ if (tagName === 'input' || tagName === 'textarea' || tagName === 'select') {
828
828
  ctx.replacements.push({
829
829
  start: node.name.getStart(ctx.sf),
830
830
  end: node.name.getEnd(),
831
- text: "onInput",
831
+ text: 'onInput',
832
832
  })
833
833
  ctx.changes.push({
834
- type: "replace",
834
+ type: 'replace',
835
835
  line: migrateGetLine(ctx, node),
836
836
  description: `onChange on <${tagName}> → onInput (native DOM events)`,
837
837
  })
@@ -839,7 +839,7 @@ function migrateJsxAttributes(ctx: MigrateContext, node: ts.JsxAttribute): void
839
839
  }
840
840
  }
841
841
 
842
- if (attrName === "dangerouslySetInnerHTML") {
842
+ if (attrName === 'dangerouslySetInnerHTML') {
843
843
  migrateDangerouslySetInnerHTML(ctx, node)
844
844
  }
845
845
  }
@@ -852,16 +852,16 @@ function migrateDangerouslySetInnerHTML(ctx: MigrateContext, node: ts.JsxAttribu
852
852
  if (!ts.isObjectLiteralExpression(expr)) return
853
853
 
854
854
  const htmlProp = expr.properties.find(
855
- (p) => ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && p.name.text === "__html",
855
+ (p) => ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && p.name.text === '__html',
856
856
  ) as ts.PropertyAssignment | undefined
857
857
 
858
858
  if (htmlProp) {
859
859
  const valueText = migrateGetNodeText(ctx, htmlProp.initializer)
860
860
  migrateReplace(ctx, node, `innerHTML={${valueText}}`)
861
861
  ctx.changes.push({
862
- type: "replace",
862
+ type: 'replace',
863
863
  line: migrateGetLine(ctx, node),
864
- description: "dangerouslySetInnerHTML → innerHTML",
864
+ description: 'dangerouslySetInnerHTML → innerHTML',
865
865
  })
866
866
  }
867
867
  }
@@ -869,11 +869,11 @@ function migrateDangerouslySetInnerHTML(ctx: MigrateContext, node: ts.JsxAttribu
869
869
  function applyReplacements(code: string, ctx: MigrateContext): string {
870
870
  // Remove React import declarations
871
871
  for (const imp of ctx.importsToRemove) {
872
- ctx.replacements.push({ start: imp.getStart(ctx.sf), end: imp.getEnd(), text: "" })
872
+ ctx.replacements.push({ start: imp.getStart(ctx.sf), end: imp.getEnd(), text: '' })
873
873
  ctx.changes.push({
874
- type: "remove",
874
+ type: 'remove',
875
875
  line: ctx.sf.getLineAndCharacterOfPosition(imp.getStart(ctx.sf)).line + 1,
876
- description: "Removed React import",
876
+ description: 'Removed React import',
877
877
  })
878
878
  }
879
879
 
@@ -908,7 +908,7 @@ function applyReplacements(code: string, ctx: MigrateContext): string {
908
908
  lastPos = r.end
909
909
  }
910
910
  parts.push(code.slice(lastPos))
911
- return parts.join("")
911
+ return parts.join('')
912
912
  }
913
913
 
914
914
  function insertPyreonImports(code: string, pyreonImports: Map<string, Set<string>>): string {
@@ -917,10 +917,10 @@ function insertPyreonImports(code: string, pyreonImports: Map<string, Set<string
917
917
  const importLines: string[] = []
918
918
  const sorted = [...pyreonImports.entries()].sort(([a], [b]) => a.localeCompare(b))
919
919
  for (const [source, specs] of sorted) {
920
- const specList = [...specs].sort().join(", ")
920
+ const specList = [...specs].sort().join(', ')
921
921
  importLines.push(`import { ${specList} } from "${source}"`)
922
922
  }
923
- const importBlock = importLines.join("\n")
923
+ const importBlock = importLines.join('\n')
924
924
 
925
925
  const lastImportEnd = findLastImportEnd(code)
926
926
  if (lastImportEnd > 0) {
@@ -931,11 +931,11 @@ function insertPyreonImports(code: string, pyreonImports: Map<string, Set<string
931
931
 
932
932
  function migrateVisitNode(ctx: MigrateContext, node: ts.Node): void {
933
933
  if (ts.isImportDeclaration(node)) migrateImportDeclaration(ctx, node)
934
- if (isCallToHook(node, "useState")) migrateUseState(ctx, node)
934
+ if (isCallToHook(node, 'useState')) migrateUseState(ctx, node)
935
935
  if (isCallToEffectHook(node)) migrateUseEffect(ctx, node)
936
- if (isCallToHook(node, "useMemo")) migrateUseMemo(ctx, node)
937
- if (isCallToHook(node, "useCallback")) migrateUseCallback(ctx, node)
938
- if (isCallToHook(node, "useRef")) migrateUseRef(ctx, node)
936
+ if (isCallToHook(node, 'useMemo')) migrateUseMemo(ctx, node)
937
+ if (isCallToHook(node, 'useCallback')) migrateUseCallback(ctx, node)
938
+ if (isCallToHook(node, 'useRef')) migrateUseRef(ctx, node)
939
939
  if (ts.isCallExpression(node)) migrateMemoWrapper(ctx, node)
940
940
  if (ts.isCallExpression(node)) migrateForwardRef(ctx, node)
941
941
  if (ts.isJsxAttribute(node) && ts.isIdentifier(node.name)) migrateJsxAttributes(ctx, node)
@@ -948,7 +948,7 @@ function migrateVisit(ctx: MigrateContext, node: ts.Node): void {
948
948
  })
949
949
  }
950
950
 
951
- export function migrateReactCode(code: string, filename = "input.tsx"): MigrationResult {
951
+ export function migrateReactCode(code: string, filename = 'input.tsx'): MigrationResult {
952
952
  const sf = ts.createSourceFile(filename, code, ts.ScriptTarget.ESNext, true, ts.ScriptKind.TSX)
953
953
  const diagnostics = detectReactPatterns(code, filename)
954
954
 
@@ -968,7 +968,7 @@ export function migrateReactCode(code: string, filename = "input.tsx"): Migratio
968
968
  result = insertPyreonImports(result, ctx.pyreonImports)
969
969
 
970
970
  // Clean up empty lines from removed imports
971
- result = result.replace(/\n{3,}/g, "\n\n")
971
+ result = result.replace(/\n{3,}/g, '\n\n')
972
972
 
973
973
  return { code: result, diagnostics, changes: ctx.changes }
974
974
  }
@@ -1002,7 +1002,7 @@ function getJsxTagName(node: ts.JsxOpeningElement | ts.JsxSelfClosingElement): s
1002
1002
  if (ts.isIdentifier(tagName)) {
1003
1003
  return tagName.text
1004
1004
  }
1005
- return ""
1005
+ return ''
1006
1006
  }
1007
1007
 
1008
1008
  function findLastImportEnd(code: string): number {
@@ -1062,7 +1062,7 @@ const ERROR_PATTERNS: ErrorPattern[] = [
1062
1062
  pattern: /Cannot read properties of undefined \(reading '(set|update|peek|subscribe)'\)/,
1063
1063
  diagnose: (m) => ({
1064
1064
  cause: `Calling .${m[1]}() on undefined. The signal variable is likely out of scope, misspelled, or not yet initialized.`,
1065
- fix: "Check that the signal is defined and in scope. Signals must be created with signal() before use.",
1065
+ fix: 'Check that the signal is defined and in scope. Signals must be created with signal() before use.',
1066
1066
  fixCode: `const mySignal = signal(initialValue)\nmySignal.${m[1]}(newValue)`,
1067
1067
  }),
1068
1068
  },
@@ -1070,7 +1070,7 @@ const ERROR_PATTERNS: ErrorPattern[] = [
1070
1070
  pattern: /(\w+) is not a function/,
1071
1071
  diagnose: (m) => ({
1072
1072
  cause: `'${m[1]}' is not callable. If this is a signal, you need to call it: ${m[1]}()`,
1073
- fix: "Pyreon signals are callable functions. Read: signal(), Write: signal.set(value)",
1073
+ fix: 'Pyreon signals are callable functions. Read: signal(), Write: signal.set(value)',
1074
1074
  fixCode: `// Read value:\nconst value = ${m[1]}()\n// Set value:\n${m[1]}.set(newValue)`,
1075
1075
  }),
1076
1076
  },
@@ -1086,7 +1086,7 @@ const ERROR_PATTERNS: ErrorPattern[] = [
1086
1086
  pattern: /Cannot find module 'react'/,
1087
1087
  diagnose: () => ({
1088
1088
  cause: "Importing from 'react' in a Pyreon project.",
1089
- fix: "Replace React imports with Pyreon equivalents.",
1089
+ fix: 'Replace React imports with Pyreon equivalents.',
1090
1090
  fixCode:
1091
1091
  '// Instead of:\nimport { useState } from "react"\n// Use:\nimport { signal } from "@pyreon/reactivity"',
1092
1092
  }),
@@ -1096,51 +1096,51 @@ const ERROR_PATTERNS: ErrorPattern[] = [
1096
1096
  diagnose: (m) => ({
1097
1097
  cause: `Accessing .${m[1]} on a signal. Pyreon signals don't have a .${m[1]} property.`,
1098
1098
  fix:
1099
- m[1] === "value"
1100
- ? "Pyreon signals are callable functions, not .value getters. Call signal() to read, signal.set() to write."
1099
+ m[1] === 'value'
1100
+ ? 'Pyreon signals are callable functions, not .value getters. Call signal() to read, signal.set() to write.'
1101
1101
  : `Signals have these methods: .set(), .update(), .peek(), .subscribe(). '${m[1]}' is not one of them.`,
1102
1102
  fixCode:
1103
- m[1] === "value" ? "// Read: mySignal()\n// Write: mySignal.set(newValue)" : undefined,
1103
+ m[1] === 'value' ? '// Read: mySignal()\n// Write: mySignal.set(newValue)' : undefined,
1104
1104
  }),
1105
1105
  },
1106
1106
  {
1107
1107
  pattern: /Type '(\w+)' is not assignable to type 'VNode'/,
1108
1108
  diagnose: (m) => ({
1109
1109
  cause: `Component returned ${m[1]} instead of VNode. Components must return JSX, null, or a string.`,
1110
- fix: "Make sure your component returns a JSX element, null, or a string.",
1111
- fixCode: "const MyComponent = (props) => {\n return <div>{props.children}</div>\n}",
1110
+ fix: 'Make sure your component returns a JSX element, null, or a string.',
1111
+ fixCode: 'const MyComponent = (props) => {\n return <div>{props.children}</div>\n}',
1112
1112
  }),
1113
1113
  },
1114
1114
  {
1115
1115
  pattern: /onMount callback must return/,
1116
1116
  diagnose: () => ({
1117
- cause: "onMount expects a callback that optionally returns a CleanupFn.",
1118
- fix: "Return a cleanup function, or return nothing.",
1119
- fixCode: "onMount(() => {\n // setup code\n})",
1117
+ cause: 'onMount expects a callback that optionally returns a CleanupFn.',
1118
+ fix: 'Return a cleanup function, or return nothing.',
1119
+ fixCode: 'onMount(() => {\n // setup code\n})',
1120
1120
  }),
1121
1121
  },
1122
1122
  {
1123
1123
  pattern: /Expected 'by' prop on <For>/,
1124
1124
  diagnose: () => ({
1125
1125
  cause: "<For> requires a 'by' prop for efficient keyed reconciliation.",
1126
- fix: "Add a by prop that returns a unique key for each item.",
1126
+ fix: 'Add a by prop that returns a unique key for each item.',
1127
1127
  fixCode:
1128
- "<For each={items()} by={item => item.id}>\n {item => <li>{item.name}</li>}\n</For>",
1128
+ '<For each={items()} by={item => item.id}>\n {item => <li>{item.name}</li>}\n</For>',
1129
1129
  }),
1130
1130
  },
1131
1131
  {
1132
1132
  pattern: /useHook.*outside.*component/i,
1133
1133
  diagnose: () => ({
1134
1134
  cause:
1135
- "Hook called outside a component function. Pyreon hooks must be called during component setup.",
1136
- fix: "Move the hook call inside a component function body.",
1135
+ 'Hook called outside a component function. Pyreon hooks must be called during component setup.',
1136
+ fix: 'Move the hook call inside a component function body.',
1137
1137
  }),
1138
1138
  },
1139
1139
  {
1140
1140
  pattern: /Hydration mismatch/,
1141
1141
  diagnose: () => ({
1142
1142
  cause: "Server-rendered HTML doesn't match client-rendered output.",
1143
- fix: "Ensure SSR and client render the same initial content. Check for browser-only APIs (window, document) in SSR code.",
1143
+ fix: 'Ensure SSR and client render the same initial content. Check for browser-only APIs (window, document) in SSR code.',
1144
1144
  related: "Use typeof window !== 'undefined' checks or onMount() for client-only code.",
1145
1145
  }),
1146
1146
  },