@pyreon/compiler 0.12.5 → 0.12.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.
- package/lib/analysis/index.js.html +1 -1
- package/lib/index.js +161 -11
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/jsx.ts +223 -19
- package/src/tests/jsx.test.ts +119 -0
package/package.json
CHANGED
package/src/jsx.ts
CHANGED
|
@@ -138,7 +138,8 @@ export function transformJSX(code: string, filename = 'input.tsx'): TransformRes
|
|
|
138
138
|
function wrap(expr: ts.Expression): void {
|
|
139
139
|
const start = expr.getStart(sf)
|
|
140
140
|
const end = expr.getEnd()
|
|
141
|
-
|
|
141
|
+
const exprText = inlineVarsInText(code.slice(start, end))
|
|
142
|
+
replacements.push({ start, end, text: `() => ${exprText}` })
|
|
142
143
|
}
|
|
143
144
|
|
|
144
145
|
/** Try to hoist or wrap an expression, pushing a replacement if needed. */
|
|
@@ -255,6 +256,223 @@ export function transformJSX(code: string, filename = 'input.tsx'): TransformRes
|
|
|
255
256
|
ts.forEachChild(expr, walk)
|
|
256
257
|
}
|
|
257
258
|
|
|
259
|
+
// ── Prop-derived variable tracking ─────────────────────────────────────────
|
|
260
|
+
// Pre-pass: find variables derived from props/splitProps results inside
|
|
261
|
+
// component functions. These are inlined at JSX use sites so the compiler's
|
|
262
|
+
// existing wrapping makes them reactive.
|
|
263
|
+
//
|
|
264
|
+
// Example:
|
|
265
|
+
// const align = props.alignX ?? 'left'
|
|
266
|
+
// return <div class={align}> ← inlined to: class={props.alignX ?? 'left'}
|
|
267
|
+
// ← compiler wraps: class={() => props.alignX ?? 'left'}
|
|
268
|
+
|
|
269
|
+
/** Names that refer to the props object or splitProps results. */
|
|
270
|
+
const propsNames = new Set<string>()
|
|
271
|
+
|
|
272
|
+
/** Map of variable name → source text of the original expression. */
|
|
273
|
+
const propDerivedVars = new Map<string, string>()
|
|
274
|
+
|
|
275
|
+
/** Check if an expression reads from a tracked props-like object. */
|
|
276
|
+
function readsFromProps(node: ts.Node): boolean {
|
|
277
|
+
if (ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.expression)) {
|
|
278
|
+
return propsNames.has(node.expression.text)
|
|
279
|
+
}
|
|
280
|
+
if (ts.isElementAccessExpression(node) && ts.isIdentifier(node.expression)) {
|
|
281
|
+
return propsNames.has(node.expression.text)
|
|
282
|
+
}
|
|
283
|
+
// Check children recursively — e.g. props.x ?? 'default'
|
|
284
|
+
let found = false
|
|
285
|
+
ts.forEachChild(node, (child) => {
|
|
286
|
+
if (found) return
|
|
287
|
+
if (readsFromProps(child)) found = true
|
|
288
|
+
})
|
|
289
|
+
return found
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Pre-pass: scan a function body for prop-derived variable declarations.
|
|
293
|
+
* callbackDepth tracks nesting inside callback arguments (map, filter, etc.)
|
|
294
|
+
* to avoid tracking variables declared inside callbacks as prop-derived. */
|
|
295
|
+
let _callbackDepth = 0
|
|
296
|
+
function scanForPropDerivedVars(node: ts.Node): void {
|
|
297
|
+
// Track callback nesting — don't track vars inside callbacks
|
|
298
|
+
if ((ts.isArrowFunction(node) || ts.isFunctionExpression(node))) {
|
|
299
|
+
const parent = node.parent
|
|
300
|
+
if (parent && ts.isCallExpression(parent) && parent.arguments.includes(node as any)) {
|
|
301
|
+
_callbackDepth++
|
|
302
|
+
ts.forEachChild(node, scanForPropDerivedVars)
|
|
303
|
+
_callbackDepth--
|
|
304
|
+
return
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
// Track the function's first parameter as a props name.
|
|
308
|
+
// Only for COMPONENT functions — not callbacks like .map(item => <div>...)
|
|
309
|
+
// Heuristic: component functions are named declarations, const assignments,
|
|
310
|
+
// or export defaults — NOT inline arguments to calls like .map(), .filter().
|
|
311
|
+
if ((ts.isFunctionDeclaration(node) || ts.isArrowFunction(node) || ts.isFunctionExpression(node))
|
|
312
|
+
&& node.parameters.length > 0) {
|
|
313
|
+
|
|
314
|
+
// Skip functions that are arguments to a call (map/filter callbacks)
|
|
315
|
+
const parent = node.parent
|
|
316
|
+
if (parent && ts.isCallExpression(parent) && parent.arguments.includes(node as any)) {
|
|
317
|
+
ts.forEachChild(node, scanForPropDerivedVars)
|
|
318
|
+
return
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const firstParam = node.parameters[0]!
|
|
322
|
+
if (ts.isIdentifier(firstParam.name)) {
|
|
323
|
+
// Check if this function returns JSX (is a component)
|
|
324
|
+
let hasJSX = false
|
|
325
|
+
ts.forEachChild(node, function checkJSX(n) {
|
|
326
|
+
if (hasJSX) return
|
|
327
|
+
if (ts.isJsxElement(n) || ts.isJsxSelfClosingElement(n) || ts.isJsxFragment(n)) {
|
|
328
|
+
hasJSX = true
|
|
329
|
+
return
|
|
330
|
+
}
|
|
331
|
+
ts.forEachChild(n, checkJSX)
|
|
332
|
+
})
|
|
333
|
+
if (hasJSX) propsNames.add(firstParam.name.text)
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Track splitProps results: const [own, rest] = splitProps(props, [...])
|
|
338
|
+
if (ts.isVariableStatement(node)) {
|
|
339
|
+
for (const decl of node.declarationList.declarations) {
|
|
340
|
+
if (ts.isArrayBindingPattern(decl.name) && decl.initializer
|
|
341
|
+
&& ts.isCallExpression(decl.initializer)) {
|
|
342
|
+
const callee = decl.initializer.expression
|
|
343
|
+
if (ts.isIdentifier(callee) && callee.text === 'splitProps') {
|
|
344
|
+
for (const el of decl.name.elements) {
|
|
345
|
+
if (ts.isBindingElement(el) && ts.isIdentifier(el.name)) {
|
|
346
|
+
propsNames.add(el.name.text)
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Track: const x = props.y ?? z OR const x = own.y
|
|
353
|
+
// Skip let/var — mutable variables can be reassigned, unsafe to inline
|
|
354
|
+
// Skip declarations inside callbacks (map, filter, etc.)
|
|
355
|
+
if (!(node.declarationList.flags & ts.NodeFlags.Const)) continue
|
|
356
|
+
if (_callbackDepth > 0) continue
|
|
357
|
+
if (ts.isIdentifier(decl.name) && decl.initializer) {
|
|
358
|
+
if (readsFromProps(decl.initializer)) {
|
|
359
|
+
const varName = decl.name.text
|
|
360
|
+
const exprText = code.slice(decl.initializer.getStart(sf), decl.initializer.getEnd())
|
|
361
|
+
propDerivedVars.set(varName, exprText)
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
ts.forEachChild(node, scanForPropDerivedVars)
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Run pre-pass
|
|
371
|
+
scanForPropDerivedVars(sf)
|
|
372
|
+
|
|
373
|
+
// Transitive resolution: if const b = a + 1 where a is prop-derived,
|
|
374
|
+
// then b is also prop-derived. Inline b → (a + 1) → ((props.x) + 1).
|
|
375
|
+
// Fixed-point iteration until no new variables are added.
|
|
376
|
+
let changed = true
|
|
377
|
+
while (changed) {
|
|
378
|
+
changed = false
|
|
379
|
+
sf.forEachChild(function scanTransitive(node) {
|
|
380
|
+
if (!ts.isVariableStatement(node)) { ts.forEachChild(node, scanTransitive); return }
|
|
381
|
+
for (const decl of node.declarationList.declarations) {
|
|
382
|
+
if (!ts.isIdentifier(decl.name) || !decl.initializer) continue
|
|
383
|
+
const varName = decl.name.text
|
|
384
|
+
if (propDerivedVars.has(varName)) continue // already tracked
|
|
385
|
+
if (node.declarationList.flags & ts.NodeFlags.Let) continue // skip let
|
|
386
|
+
// Check if the initializer references any existing prop-derived var
|
|
387
|
+
let usesPropVar = false
|
|
388
|
+
ts.forEachChild(decl.initializer, function check(n) {
|
|
389
|
+
if (usesPropVar) return
|
|
390
|
+
if (ts.isIdentifier(n) && propDerivedVars.has(n.text)) {
|
|
391
|
+
const parent = n.parent
|
|
392
|
+
if (parent && ts.isPropertyAccessExpression(parent) && parent.name === n) return
|
|
393
|
+
usesPropVar = true
|
|
394
|
+
}
|
|
395
|
+
ts.forEachChild(n, check)
|
|
396
|
+
})
|
|
397
|
+
if (usesPropVar) {
|
|
398
|
+
const exprText = code.slice(decl.initializer.getStart(sf), decl.initializer.getEnd())
|
|
399
|
+
propDerivedVars.set(varName, exprText)
|
|
400
|
+
changed = true
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
})
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Resolve transitive inlining: if b's expr references a, replace a with its expr
|
|
407
|
+
for (const [varName, expr] of propDerivedVars) {
|
|
408
|
+
let resolved = expr
|
|
409
|
+
for (const [depName, depExpr] of propDerivedVars) {
|
|
410
|
+
if (depName === varName) continue
|
|
411
|
+
const re = new RegExp(`(?<![.\\w])${depName}(?![\\w:])`, 'g')
|
|
412
|
+
if (re.test(resolved)) {
|
|
413
|
+
resolved = resolved.replace(re, `(${depExpr})`)
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
if (resolved !== expr) propDerivedVars.set(varName, resolved)
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Enhanced dynamic check — combines containsCall with props awareness.
|
|
421
|
+
* Returns true if an expression is reactive (contains signal calls,
|
|
422
|
+
* accesses props members, or references prop-derived variables).
|
|
423
|
+
*/
|
|
424
|
+
/**
|
|
425
|
+
* Replace prop-derived variable names in a source text with their original expressions.
|
|
426
|
+
* Simple regex-based replacement — safe because variable names are identifiers.
|
|
427
|
+
*/
|
|
428
|
+
function inlineVarsInText(text: string): string {
|
|
429
|
+
if (propDerivedVars.size === 0) return text
|
|
430
|
+
let result = text
|
|
431
|
+
for (const [varName, expr] of propDerivedVars) {
|
|
432
|
+
// Replace standalone variable references only.
|
|
433
|
+
// Must NOT match:
|
|
434
|
+
// - property access: obj.varName (preceded by .)
|
|
435
|
+
// - property name in object: { varName: ... } (followed by :)
|
|
436
|
+
// - part of longer identifier: varNameExtra
|
|
437
|
+
// Lookbehind ensures not preceded by . or alphanumeric
|
|
438
|
+
// Lookahead ensures not followed by alphanumeric or :
|
|
439
|
+
const re = new RegExp(`(?<![.\\w])${varName}(?![\\w:])`, 'g')
|
|
440
|
+
result = result.replace(re, `(${expr})`)
|
|
441
|
+
}
|
|
442
|
+
return result
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function isDynamic(node: ts.Node): boolean {
|
|
446
|
+
if (containsCall(node)) return true
|
|
447
|
+
return accessesProps(node)
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/** Check if an expression accesses a tracked props object or a prop-derived variable. */
|
|
451
|
+
function accessesProps(node: ts.Node): boolean {
|
|
452
|
+
if (ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.expression)) {
|
|
453
|
+
if (propsNames.has(node.expression.text)) return true
|
|
454
|
+
}
|
|
455
|
+
if (ts.isIdentifier(node) && propDerivedVars.has(node.text)) {
|
|
456
|
+
const parent = node.parent
|
|
457
|
+
if (parent && ts.isPropertyAccessExpression(parent) && parent.name === node) return false
|
|
458
|
+
return true
|
|
459
|
+
}
|
|
460
|
+
let found = false
|
|
461
|
+
ts.forEachChild(node, (child) => {
|
|
462
|
+
if (found) return
|
|
463
|
+
if (ts.isArrowFunction(child) || ts.isFunctionExpression(child)) return
|
|
464
|
+
if (accessesProps(child)) found = true
|
|
465
|
+
})
|
|
466
|
+
return found
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function shouldWrap(node: ts.Expression): boolean {
|
|
470
|
+
if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) return false
|
|
471
|
+
if (isStatic(node)) return false
|
|
472
|
+
if (ts.isCallExpression(node) && isPureStaticCall(node)) return false
|
|
473
|
+
return isDynamic(node)
|
|
474
|
+
}
|
|
475
|
+
|
|
258
476
|
function walk(node: ts.Node): void {
|
|
259
477
|
if (ts.isJsxElement(node) && tryTemplateEmit(node)) return
|
|
260
478
|
if (ts.isJsxSelfClosingElement(node) || ts.isJsxElement(node)) checkForWarnings(node)
|
|
@@ -484,7 +702,7 @@ export function transformJSX(code: string, filename = 'input.tsx'): TransformRes
|
|
|
484
702
|
if (ts.isArrowFunction(exprNode) || ts.isFunctionExpression(exprNode)) {
|
|
485
703
|
return { expr: `(${sliceExpr(exprNode)})()`, isReactive: true }
|
|
486
704
|
}
|
|
487
|
-
return { expr: sliceExpr(exprNode), isReactive:
|
|
705
|
+
return { expr: sliceExpr(exprNode), isReactive: isDynamic(exprNode) }
|
|
488
706
|
}
|
|
489
707
|
|
|
490
708
|
/** Build a setter expression for an attribute. */
|
|
@@ -579,7 +797,7 @@ export function transformJSX(code: string, filename = 'input.tsx'): TransformRes
|
|
|
579
797
|
const expr = sliceExpr(attr.expression)
|
|
580
798
|
// Use runtime-dom's applyProps which handles class, style, events, etc.
|
|
581
799
|
needsApplyPropsImport = true
|
|
582
|
-
if (
|
|
800
|
+
if (isDynamic(attr.expression)) {
|
|
583
801
|
reactiveBindExprs.push(`_applyProps(${varName}, ${expr})`)
|
|
584
802
|
} else {
|
|
585
803
|
bindLines.push(`_applyProps(${varName}, ${expr})`)
|
|
@@ -629,7 +847,7 @@ export function transformJSX(code: string, filename = 'input.tsx'): TransformRes
|
|
|
629
847
|
// When r.name() changes, r.email()'s _bind doesn't re-run.
|
|
630
848
|
needsBindImportGlobal = true
|
|
631
849
|
const d = nextDisp()
|
|
632
|
-
bindLines.push(`const ${d} = _bind(() => { ${tVar}.data = ${expr} })`)
|
|
850
|
+
bindLines.push(`const ${d} = _bind(() => { ${tVar}.data = ${inlineVarsInText(expr)} })`)
|
|
633
851
|
}
|
|
634
852
|
return needsPlaceholder ? '<!>' : ''
|
|
635
853
|
}
|
|
@@ -753,7 +971,7 @@ export function transformJSX(code: string, filename = 'input.tsx'): TransformRes
|
|
|
753
971
|
if (reactiveBindExprs.length > 0) {
|
|
754
972
|
needsBindImportGlobal = true
|
|
755
973
|
const combinedName = nextDisp()
|
|
756
|
-
const combinedBody = reactiveBindExprs.join('; ')
|
|
974
|
+
const combinedBody = reactiveBindExprs.map(inlineVarsInText).join('; ')
|
|
757
975
|
bindLines.push(`const ${combinedName} = _bind(() => { ${combinedBody} })`)
|
|
758
976
|
}
|
|
759
977
|
|
|
@@ -999,20 +1217,6 @@ function isPureStaticCall(node: ts.CallExpression): boolean {
|
|
|
999
1217
|
return node.arguments.every((arg) => !ts.isSpreadElement(arg) && isStatic(arg))
|
|
1000
1218
|
}
|
|
1001
1219
|
|
|
1002
|
-
function shouldWrap(node: ts.Expression): boolean {
|
|
1003
|
-
// Already a function — user explicitly wrapped or it's a callback
|
|
1004
|
-
if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) return false
|
|
1005
|
-
// Static literal — no signals involved
|
|
1006
|
-
if (isStatic(node)) return false
|
|
1007
|
-
// Pure call with static args — result is constant
|
|
1008
|
-
if (ts.isCallExpression(node) && isPureStaticCall(node)) return false
|
|
1009
|
-
// Only wrap if the expression tree contains a call — signal reads are always
|
|
1010
|
-
// function calls (e.g. `count()`, `name()`). Plain identifiers, object literals
|
|
1011
|
-
// like `style={{ color: "red" }}`, array literals, and member accesses are
|
|
1012
|
-
// left as-is to avoid unnecessary reactive wrappers.
|
|
1013
|
-
return containsCall(node)
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
1220
|
function containsCall(node: ts.Node): boolean {
|
|
1017
1221
|
if (ts.isCallExpression(node)) {
|
|
1018
1222
|
// Skip pure calls with static args
|
package/src/tests/jsx.test.ts
CHANGED
|
@@ -1246,3 +1246,122 @@ describe('JSX transform — per-text-node bind', () => {
|
|
|
1246
1246
|
expect(result).toContain('_bindText(c,')
|
|
1247
1247
|
})
|
|
1248
1248
|
})
|
|
1249
|
+
|
|
1250
|
+
// ─── Reactive props auto-detection ──────────────────────────────────────────
|
|
1251
|
+
|
|
1252
|
+
describe('JSX transform — reactive props detection', () => {
|
|
1253
|
+
test('props.x in text child is reactive (wrapped in _bind)', () => {
|
|
1254
|
+
const result = t('function Comp(props) { return <div>{props.name}</div> }')
|
|
1255
|
+
expect(result).toContain('_bind(() => {')
|
|
1256
|
+
expect(result).toContain('props.name')
|
|
1257
|
+
})
|
|
1258
|
+
|
|
1259
|
+
test('props.x in attribute is reactive (wrapped in _bind)', () => {
|
|
1260
|
+
const result = t('function Comp(props) { return <div class={props.cls}></div> }')
|
|
1261
|
+
expect(result).toContain('_bind(() => {')
|
|
1262
|
+
expect(result).toContain('props.cls')
|
|
1263
|
+
})
|
|
1264
|
+
|
|
1265
|
+
test('prop-derived variable inlined in text child', () => {
|
|
1266
|
+
const result = t('function Comp(props) { const x = props.name ?? "anon"; return <div>{x}</div> }')
|
|
1267
|
+
expect(result).toContain('_bind(() => {')
|
|
1268
|
+
expect(result).toContain('props.name ?? "anon"')
|
|
1269
|
+
// x should be inlined, not used directly
|
|
1270
|
+
expect(result).not.toMatch(/__t\d+\.data = x\b/)
|
|
1271
|
+
})
|
|
1272
|
+
|
|
1273
|
+
test('prop-derived variable inlined in attribute', () => {
|
|
1274
|
+
const result = t('function Comp(props) { const align = props.alignX ?? "left"; return <div class={align}></div> }')
|
|
1275
|
+
expect(result).toContain('_bind(() => {')
|
|
1276
|
+
expect(result).toContain('props.alignX ?? "left"')
|
|
1277
|
+
})
|
|
1278
|
+
|
|
1279
|
+
test('splitProps results tracked as props-like', () => {
|
|
1280
|
+
const result = t('function Comp(props) { const [own, rest] = splitProps(props, ["x"]); const v = own.x ?? 5; return <div>{v}</div> }')
|
|
1281
|
+
expect(result).toContain('_bind(() => {')
|
|
1282
|
+
expect(result).toContain('own.x ?? 5')
|
|
1283
|
+
})
|
|
1284
|
+
|
|
1285
|
+
test('non-component function NOT tracked (no JSX)', () => {
|
|
1286
|
+
const result = t('function helper(props) { const x = props.y; return x }')
|
|
1287
|
+
expect(result).not.toContain('_bind')
|
|
1288
|
+
expect(result).not.toContain('_tpl')
|
|
1289
|
+
})
|
|
1290
|
+
|
|
1291
|
+
test('static values unchanged by props tracking', () => {
|
|
1292
|
+
const result = t('function Comp(props) { return <div class="static">text</div> }')
|
|
1293
|
+
expect(result).toContain('_tpl("<div class=\\"static\\">text</div>"')
|
|
1294
|
+
expect(result).not.toContain('_bind')
|
|
1295
|
+
})
|
|
1296
|
+
|
|
1297
|
+
test('signal calls still work alongside props detection', () => {
|
|
1298
|
+
const result = t('function Comp(props) { return <div>{count()}</div> }')
|
|
1299
|
+
expect(result).toContain('_bindText(count,')
|
|
1300
|
+
})
|
|
1301
|
+
|
|
1302
|
+
test('arrow function component detected', () => {
|
|
1303
|
+
const result = t('const Comp = (props) => <div>{props.x}</div>')
|
|
1304
|
+
expect(result).toContain('_bind(() => {')
|
|
1305
|
+
expect(result).toContain('props.x')
|
|
1306
|
+
})
|
|
1307
|
+
})
|
|
1308
|
+
|
|
1309
|
+
// ─── Transitive prop derivation ─────────────────────────────────────────────
|
|
1310
|
+
|
|
1311
|
+
describe('JSX transform — transitive prop derivation', () => {
|
|
1312
|
+
test('const b = a + 1 where a is prop-derived', () => {
|
|
1313
|
+
const result = t('function Comp(props) { const a = props.x; const b = a + 1; return <div>{b}</div> }')
|
|
1314
|
+
expect(result).toContain('_bind(() => {')
|
|
1315
|
+
expect(result).toContain('props.x')
|
|
1316
|
+
// b should be inlined transitively
|
|
1317
|
+
expect(result).not.toMatch(/__t\d+\.data = b\b/)
|
|
1318
|
+
})
|
|
1319
|
+
|
|
1320
|
+
test('deep chain: c = b * 2, b = a + 1, a = props.x', () => {
|
|
1321
|
+
const result = t('function Comp(props) { const a = props.x; const b = a + 1; const c = b * 2; return <div>{c}</div> }')
|
|
1322
|
+
expect(result).toContain('props.x')
|
|
1323
|
+
// Full chain inlined
|
|
1324
|
+
expect(result).toContain('_bind')
|
|
1325
|
+
})
|
|
1326
|
+
|
|
1327
|
+
test('non-prop-derived variable NOT inlined', () => {
|
|
1328
|
+
const result = t('function Comp(props) { const x = 42; return <div>{x}</div> }')
|
|
1329
|
+
// x = 42 is static, not prop-derived — should NOT be wrapped
|
|
1330
|
+
expect(result).not.toContain('_bind')
|
|
1331
|
+
})
|
|
1332
|
+
|
|
1333
|
+
test('let variables NOT tracked (mutable — can be reassigned)', () => {
|
|
1334
|
+
const result = t('function Comp(props) { let x = props.y; x = "override"; return <div>{x}</div> }')
|
|
1335
|
+
// let is excluded — x is NOT inlined, set statically
|
|
1336
|
+
expect(result).toContain('textContent = x')
|
|
1337
|
+
expect(result).not.toContain('_bind')
|
|
1338
|
+
})
|
|
1339
|
+
|
|
1340
|
+
test('mixed props and signals in same expression', () => {
|
|
1341
|
+
const result = t('function Comp(props) { return <div class={`${props.base} ${count()}`}></div> }')
|
|
1342
|
+
expect(result).toContain('_bind(() => {')
|
|
1343
|
+
})
|
|
1344
|
+
|
|
1345
|
+
test('prop-derived used in non-JSX stays static', () => {
|
|
1346
|
+
// The variable is still captured — only JSX usage is inlined
|
|
1347
|
+
const result = t('function Comp(props) { const x = props.y; console.log(x); return <div>{x}</div> }')
|
|
1348
|
+
// console.log(x) uses the captured value — compiler doesn't touch it
|
|
1349
|
+
expect(result).toContain('console.log(x)')
|
|
1350
|
+
// JSX usage is inlined
|
|
1351
|
+
expect(result).toContain('props.y')
|
|
1352
|
+
expect(result).toContain('_bind')
|
|
1353
|
+
})
|
|
1354
|
+
|
|
1355
|
+
test('.map() callback params NOT treated as props', () => {
|
|
1356
|
+
const result = t('function App(props) { return <div>{tabs.map((tab) => { const C = tab.component; return <div><C /></div> })}</div> }')
|
|
1357
|
+
// tab is a callback param, not a component's props — should NOT be tracked
|
|
1358
|
+
expect(result).not.toContain('(tab.component)')
|
|
1359
|
+
})
|
|
1360
|
+
|
|
1361
|
+
test('prop read with ?? default used multiple times', () => {
|
|
1362
|
+
const result = t('function Comp(props) { const x = props.a ?? "def"; return <div class={x}>{x}</div> }')
|
|
1363
|
+
// Both uses should be inlined
|
|
1364
|
+
const matches = result.match(/props\.a \?\? "def"/g)
|
|
1365
|
+
expect(matches?.length).toBeGreaterThanOrEqual(2)
|
|
1366
|
+
})
|
|
1367
|
+
})
|