sinwan-compiler 0.2.7 → 0.2.9

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.
@@ -43,8 +43,17 @@ function isBuiltinReactiveProp(componentName, attrName) {
43
43
  return false;
44
44
  return entry.reactiveProps.has(attrName);
45
45
  }
46
- function isBuiltinControlFlowComponent(componentName) {
47
- return !!componentName && BUILTIN_REACTIVE_PROPS.has(componentName);
46
+ function isReactiveContainerPassThrough(expr, scope) {
47
+ let node = expr;
48
+ while (t.isTSNonNullExpression(node)) {
49
+ node = node.expression;
50
+ }
51
+ if (!t.isIdentifier(node))
52
+ return false;
53
+ const binding = scope.bindings.get(node.name);
54
+ if (!binding)
55
+ return false;
56
+ return binding.kind === "mutable" || binding.kind === "prop";
48
57
  }
49
58
  function trackReactiveImports2(ast) {
50
59
  const names = {
@@ -291,6 +300,35 @@ function analyzeObjectLiteralSpread(spread, fullScope, calleeReactive) {
291
300
  function isMemberLike(node) {
292
301
  return t.isMemberExpression(node) || t.isOptionalMemberExpression(node);
293
302
  }
303
+ function isFunctionLike(node) {
304
+ return t.isArrowFunctionExpression(node) || t.isFunctionExpression(node);
305
+ }
306
+ function isCallLike(node) {
307
+ return t.isCallExpression(node) || t.isOptionalCallExpression(node);
308
+ }
309
+ var COMPILER_BINDING_CALLEES = new Set([
310
+ "_$bindText",
311
+ "_$bindAttr",
312
+ "_$bindStyle",
313
+ "_$bindClass",
314
+ "_$unwrap"
315
+ ]);
316
+ function isCompilerBindingCall(node) {
317
+ const callee = node.callee;
318
+ return t.isIdentifier(callee) && COMPILER_BINDING_CALLEES.has(callee.name);
319
+ }
320
+ function visitCallCalleeAndArgs(node, visitExpr) {
321
+ if (isCompilerBindingCall(node))
322
+ return;
323
+ visitExpr(node.callee, true);
324
+ for (const arg of node.arguments) {
325
+ if (t.isSpreadElement(arg)) {
326
+ visitExpr(arg.argument, true);
327
+ continue;
328
+ }
329
+ visitExpr(arg, true);
330
+ }
331
+ }
294
332
  function getMemberExpressionRootAndPath(node) {
295
333
  const path = [];
296
334
  let current = node;
@@ -428,11 +466,23 @@ function containsReactiveValue2(expr, scope) {
428
466
  }
429
467
  function containsReactiveRead(expr, scope) {
430
468
  let found = false;
431
- function visit(node) {
469
+ function visit(node, enterFunction) {
432
470
  if (found)
433
471
  return;
434
472
  if (!node || typeof node !== "object")
435
473
  return;
474
+ if (isFunctionLike(node)) {
475
+ if (!enterFunction)
476
+ return;
477
+ for (const param of node.params) {
478
+ visit(param, false);
479
+ }
480
+ visit(node.body, false);
481
+ return;
482
+ }
483
+ if (t.isJSXElement(node) || t.isJSXFragment(node)) {
484
+ return;
485
+ }
436
486
  if (isMemberLike(node) || t.isTSNonNullExpression(node) || t.isIdentifier(node)) {
437
487
  if (isReactiveRead(node, scope)) {
438
488
  found = true;
@@ -443,7 +493,8 @@ function containsReactiveRead(expr, scope) {
443
493
  found = true;
444
494
  return;
445
495
  }
446
- if (t.isArrowFunctionExpression(node) || t.isFunctionExpression(node)) {
496
+ if (isCallLike(node)) {
497
+ visitCallCalleeAndArgs(node, visit);
447
498
  return;
448
499
  }
449
500
  for (const key of Object.keys(node)) {
@@ -453,14 +504,14 @@ function containsReactiveRead(expr, scope) {
453
504
  if (Array.isArray(value)) {
454
505
  for (const item of value) {
455
506
  if (item && typeof item === "object")
456
- visit(item);
507
+ visit(item, false);
457
508
  }
458
509
  } else if (value && typeof value === "object") {
459
- visit(value);
510
+ visit(value, false);
460
511
  }
461
512
  }
462
513
  }
463
- visit(expr);
514
+ visit(expr, false);
464
515
  return found;
465
516
  }
466
517
  function getComponentExpressionInfo(exprPath) {
@@ -552,11 +603,21 @@ function transformPropMemberAccess(expr, scope) {
552
603
  parent.object = resolveRoot(rootName);
553
604
  }
554
605
  }
555
- function visit(node) {
606
+ function visit(node, enterFunction) {
556
607
  if (!node || typeof node !== "object")
557
608
  return;
558
- if (t.isArrowFunctionExpression(node) || t.isFunctionExpression(node))
609
+ if (isFunctionLike(node)) {
610
+ if (!enterFunction)
611
+ return;
612
+ for (const param of node.params) {
613
+ visit(param, false);
614
+ }
615
+ visit(node.body, false);
559
616
  return;
617
+ }
618
+ if (t.isJSXElement(node) || t.isJSXFragment(node)) {
619
+ return;
620
+ }
560
621
  if (isMemberLike(node)) {
561
622
  const rootPath = getMemberExpressionRootAndPath(node);
562
623
  if (rootPath) {
@@ -569,6 +630,10 @@ function transformPropMemberAccess(expr, scope) {
569
630
  }
570
631
  return;
571
632
  }
633
+ if (isCallLike(node)) {
634
+ visitCallCalleeAndArgs(node, visit);
635
+ return;
636
+ }
572
637
  for (const key of Object.keys(node)) {
573
638
  if (key === "loc" || key === "start" || key === "end" || key === "leadingComments" || key === "trailingComments")
574
639
  continue;
@@ -576,14 +641,14 @@ function transformPropMemberAccess(expr, scope) {
576
641
  if (Array.isArray(value)) {
577
642
  for (const item of value) {
578
643
  if (item && typeof item === "object")
579
- visit(item);
644
+ visit(item, false);
580
645
  }
581
646
  } else if (value && typeof value === "object") {
582
- visit(value);
647
+ visit(value, false);
583
648
  }
584
649
  }
585
650
  }
586
- visit(expr);
651
+ visit(expr, false);
587
652
  return changed;
588
653
  }
589
654
  function wrapExpression(expr) {
@@ -611,7 +676,8 @@ function createBindingDescriptor(expr, exprPath, options) {
611
676
  wrapExpression(expr)
612
677
  ]);
613
678
  }
614
- if (t.isIdentifier(expr, { name: "children" })) {
679
+ const compInfo = getComponentExpressionInfo(exprPath);
680
+ if (t.isIdentifier(expr, { name: "children" }) || compInfo.isComponent && compInfo.attributeName === null) {
615
681
  return wrapExpression(expr);
616
682
  }
617
683
  return t.callExpression(t.identifier("_$bindText"), [wrapExpression(expr)]);
@@ -1050,15 +1116,10 @@ function wrapReactiveExpressions2(ast, options = {}) {
1050
1116
  JSXExpressionContainer(exprPath) {
1051
1117
  const expr = exprPath.node.expression;
1052
1118
  const compInfo = getComponentExpressionInfo(exprPath);
1053
- if (compInfo.isComponent) {
1054
- if (compInfo.attributeName !== null) {
1055
- if (!compInfo.componentName || !isReactiveComponentProp(compInfo.componentName, compInfo.attributeName, componentNames, reactiveProps, importedReactiveProps)) {
1056
- return;
1057
- }
1058
- } else {
1059
- if (!isBuiltinControlFlowComponent(compInfo.componentName)) {
1060
- return;
1061
- }
1119
+ if (compInfo.isComponent && compInfo.attributeName !== null) {
1120
+ const knownProp = !!compInfo.componentName && isReactiveComponentProp(compInfo.componentName, compInfo.attributeName, componentNames, reactiveProps, importedReactiveProps);
1121
+ if (!knownProp && (!shouldWrap(expr, scope) || isReactiveContainerPassThrough(expr, scope))) {
1122
+ return;
1062
1123
  }
1063
1124
  }
1064
1125
  if (shouldWrap(expr, scope)) {
package/dist/cli.js CHANGED
@@ -3,7 +3,7 @@
3
3
  import {
4
4
  runAnalyzeCli2,
5
5
  runCliIfMain2
6
- } from "./chunk-edjqytm9.js";
6
+ } from "./chunk-6dnsvgyf.js";
7
7
  export {
8
8
  runAnalyzeCli2 as runAnalyzeCli,
9
9
  runCliIfMain2 as runCliIfMain
package/dist/index.js CHANGED
@@ -17,7 +17,7 @@ import {
17
17
  propagateReactiveProps2,
18
18
  wrapReactiveExpressions2,
19
19
  runAnalyzeCli2
20
- } from "./chunk-edjqytm9.js";
20
+ } from "./chunk-6dnsvgyf.js";
21
21
 
22
22
  // src/transform.ts
23
23
  import { parse } from "@babel/parser";
@@ -19,9 +19,9 @@ export interface TransformOptions {
19
19
  /**
20
20
  * Resolves an import source (e.g. "./Child") to an absolute file path,
21
21
  * relative to the file being transformed. Used to look up reactive-prop
22
- * metadata for components imported from other modules so reactive values
23
- * passed to them are wrapped at the call site. When omitted, cross-module
24
- * component call sites fall back to the conservative (non-wrapping) behavior.
22
+ * metadata for components imported from other modules. Derived reads
23
+ * (`title={n.value}`) wrap even without metadata; bare signal/store
24
+ * identifiers stay unwrapped.
25
25
  */
26
26
  resolveImport?: (source: string, fromFile: string) => string | null;
27
27
  }
package/docs/README.md CHANGED
@@ -19,6 +19,8 @@ autoWrapComponents() plain `export function App()` → cc(App)
19
19
  │
20
20
  ▼
21
21
  wrapReactiveExpressions() {count.value} → {() => count.value}
22
+ {arr.map(x => n.value)} → {() => arr.map(...)}
23
+ also wraps those reads as children/attrs of <Label>
22
24
  │
23
25
  ▼
24
26
  hoist templates <div class="card">…</div> → _$tmpl_N
package/docs/plugins.md CHANGED
@@ -51,6 +51,7 @@ The Vite plugin sets `resolve.dedupe: ["sinwan"]` for the same duplicate-runtime
51
51
  | Option | Default | Role |
52
52
  | ------ | ------- | ---- |
53
53
  | `hoist` | `true` | Template hoisting |
54
+ | `dev` | Bun: `NODE_ENV !== "production"` at `sinwan()`; Vite: `config.mode !== "production"` | Warn when hoisting is skipped. Pass `false` for library/`Bun.build` production bundles. |
54
55
  | `explicitBindings` | `false` | `_$bind*` helpers instead of bare `() =>` |
55
56
  | `analyze` | unset | Path to CLI JSON (production) |
56
57
  | `cache` | `true` | Incremental analyzer in dev. `false` turns it off. An object sets `root`, `tsConfigPath`, `bunfigPath`, `workspaces`, `cachePath` |
package/docs/transform.md CHANGED
@@ -22,6 +22,8 @@ The compiler turns the child into a lazy getter:
22
22
  <p>{() => count.value}</p>
23
23
  ```
24
24
 
25
+ The same wrap applies to **user-component children**. `<Label>{checked.value ? "On" : "Off"}</Label>` becomes `{() => checked.value ? "On" : "Off"}` so a one-shot `cc()` parent does not snapshot the string.
26
+
25
27
  The runtime runs that function inside an effect and updates the text when `count` changes.
26
28
 
27
29
  ## Automatic `cc()` wrapping
@@ -70,12 +72,14 @@ Reads the compiler wraps:
70
72
  - `useState` getters: `count()`
71
73
  - `useFetch` signal values: `f.data.value` (not the shell `f` or `f.data`)
72
74
  - Calls to a **local** function whose body reads one of the above
75
+ - Reads **inside call callbacks** that run while the JSX expression is evaluated: `.map` / `.filter` / `Array.from` callbacks, IIFEs, and default parameters of those callbacks. `<For each={keys.map((k) => groups[query.value])}>` becomes `each={() => keys.map(...)}`. Nested JSX inside those callbacks is a separate wrap site (`items.map((i) => <span>{count.value}</span>)` wraps the inner text, not the whole map).
73
76
 
74
77
  Not wrapped:
75
78
 
76
79
  - Literals and plain identifiers
77
80
  - Event-handler functions (`onClick={fn}` or `onClick={() => …}`)
78
81
  - Nested function *values* passed as props (`title={() => s.value}` stays a render prop)
82
+ - Functions stored as object/array values (`{ onClick: () => s.value }`) or returned from a factory (`const make = () => () => s.value`)
79
83
  - `useFetch` methods such as `abort` / `execute`
80
84
  - Static values the [analyzer](analyzer.md) proved never change
81
85
 
@@ -83,9 +87,12 @@ Not wrapped:
83
87
 
84
88
  **Native elements** (`div`, `p`, `button`, …): wrap reactive children and attributes.
85
89
 
86
- **User components** (`<Child title={…} />`): wrap a prop only if that prop is reactive for `Child` (local call graph, analyzer metadata, or a conservative fallback for exported components). Children forwarded to a user component stay unwrapped so the child can read `children` itself.
90
+ **User components** (`<Label>`, `<Child title={…} />`):
91
+
92
+ - **Children:** wrap reactive reads the same way as DOM text (`{count.value}`, `{checked.value ? "On" : "Off"}`) so `cc()` parents do not snapshot the value. Use a getter (`() => …`), never `_$bindText`, because the child may render nodes as well as strings. Render-prop functions (`{(v) => …}`) stay untouched.
93
+ - **Props:** wrap a prop if it is known-reactive for that component (built-in registry, local call graph, analyzer metadata). For *unknown* imported components, still wrap **derived reads** (`htmlFor={id.value}`, `disabled={off.value}`) and leave **container pass-through** alone (`checked={checked}`, `user={user}`) so the child receives the signal or store proxy.
87
94
 
88
- **Built-in control-flow** components wrap specific props, and they *do* wrap reactive expression children (those children render directly):
95
+ **Built-in control-flow** components wrap specific props, and they wrap reactive expression children (those children render directly):
89
96
 
90
97
  | Component | Reactive props |
91
98
  | --------- | -------------- |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sinwan-compiler",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
4
4
  "description": "Shared compiler core for Sinwan plugins",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",