sinwan-compiler 0.2.9 → 0.2.11

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.
@@ -231,6 +231,8 @@ function trackLocalScopeBindings(path, names, scope) {
231
231
  const body = fn.body;
232
232
  if (body && containsReactiveRead(body, scope)) {
233
233
  scope.bindings.set(name, { kind: "getter" });
234
+ } else {
235
+ scope.bindings.set(name, { kind: "function" });
234
236
  }
235
237
  }
236
238
  });
@@ -329,6 +331,9 @@ function visitCallCalleeAndArgs(node, visitExpr) {
329
331
  visitExpr(arg, true);
330
332
  }
331
333
  }
334
+ function isSignalValuePath(path) {
335
+ return path[0] === "value" || path.length >= 2 && path[1] === "value";
336
+ }
332
337
  function getMemberExpressionRootAndPath(node) {
333
338
  const path = [];
334
339
  let current = node;
@@ -375,8 +380,9 @@ function isReactiveRead(node, scope) {
375
380
  if (!rootPath)
376
381
  return false;
377
382
  const binding = scope.bindings.get(rootPath.root);
378
- if (!binding)
379
- return false;
383
+ if (!binding) {
384
+ return isSignalValuePath(rootPath.path);
385
+ }
380
386
  if (binding.kind === "prop") {
381
387
  return true;
382
388
  }
@@ -393,10 +399,27 @@ function isReactiveRead(node, scope) {
393
399
  }
394
400
  function isGetterCall(node, scope) {
395
401
  const callee = node.callee;
396
- if (!t.isIdentifier(callee))
397
- return false;
398
- const binding = scope.bindings.get(callee.name);
399
- return binding?.kind === "getter";
402
+ if (t.isIdentifier(callee)) {
403
+ const binding = scope.bindings.get(callee.name);
404
+ if (binding?.kind === "getter")
405
+ return true;
406
+ if (binding)
407
+ return false;
408
+ return node.arguments.length === 0;
409
+ }
410
+ if (isMemberLike(callee) && !callee.computed && t.isIdentifier(callee.property)) {
411
+ if (node.arguments.length !== 0)
412
+ return false;
413
+ const rootPath = getMemberExpressionRootAndPath(callee);
414
+ if (!rootPath)
415
+ return false;
416
+ const binding = scope.bindings.get(rootPath.root);
417
+ if (binding?.kind === "mutable" || binding?.kind === "function" || binding?.kind === "signal" || binding?.kind === "computed" || binding?.kind === "prop" || binding?.kind === "getter" || binding?.kind === "signalObject") {
418
+ return false;
419
+ }
420
+ return true;
421
+ }
422
+ return false;
400
423
  }
401
424
  function isReactiveValue2(node, scope) {
402
425
  if (t.isTSNonNullExpression(node)) {
@@ -414,8 +437,9 @@ function isReactiveValue2(node, scope) {
414
437
  if (!rootPath)
415
438
  return false;
416
439
  const binding = scope.bindings.get(rootPath.root);
417
- if (!binding)
418
- return false;
440
+ if (!binding) {
441
+ return isSignalValuePath(rootPath.path);
442
+ }
419
443
  if (binding.kind === "prop" || binding.kind === "mutable") {
420
444
  return true;
421
445
  }
@@ -440,7 +464,7 @@ function containsReactiveValue2(expr, scope) {
440
464
  return;
441
465
  }
442
466
  }
443
- if (t.isCallExpression(node) && isGetterCall(node, scope)) {
467
+ if (isCallLike(node) && isGetterCall(node, scope)) {
444
468
  found = true;
445
469
  return;
446
470
  }
@@ -489,7 +513,7 @@ function containsReactiveRead(expr, scope) {
489
513
  return;
490
514
  }
491
515
  }
492
- if (t.isCallExpression(node) && isGetterCall(node, scope)) {
516
+ if (isCallLike(node) && isGetterCall(node, scope)) {
493
517
  found = true;
494
518
  return;
495
519
  }
@@ -551,7 +575,7 @@ function getComponentExpressionInfo(exprPath) {
551
575
  }
552
576
  }
553
577
  if (!firstChar || firstChar !== firstChar.toUpperCase() || !/[A-Z]/.test(firstChar)) {
554
- return { isComponent: false, componentName: null, attributeName: null };
578
+ return { isComponent: false, componentName: null, attributeName };
555
579
  }
556
580
  return { isComponent: true, componentName, attributeName };
557
581
  }
@@ -660,6 +684,10 @@ function createBindingDescriptor(expr, exprPath, options) {
660
684
  }
661
685
  const parent = exprPath.parentPath;
662
686
  if (parent && parent.isJSXAttribute && parent.isJSXAttribute()) {
687
+ const compInfo = getComponentExpressionInfo(exprPath);
688
+ if (compInfo.isComponent) {
689
+ return wrapExpression(expr);
690
+ }
663
691
  const attrName = parent.node.name.name;
664
692
  if (attrName === "style") {
665
693
  return t.callExpression(t.identifier("_$bindStyle"), [
@@ -1110,12 +1138,13 @@ function wrapReactiveExpressions2(ast, options = {}) {
1110
1138
  trackPropBindings(scope, path.node.params[0], allProps);
1111
1139
  }
1112
1140
  }
1113
- if (scope.bindings.size === 0)
1114
- return;
1115
1141
  path.traverse({
1116
1142
  JSXExpressionContainer(exprPath) {
1117
1143
  const expr = exprPath.node.expression;
1118
1144
  const compInfo = getComponentExpressionInfo(exprPath);
1145
+ if (compInfo.attributeName?.startsWith("on")) {
1146
+ return;
1147
+ }
1119
1148
  if (compInfo.isComponent && compInfo.attributeName !== null) {
1120
1149
  const knownProp = !!compInfo.componentName && isReactiveComponentProp(compInfo.componentName, compInfo.attributeName, componentNames, reactiveProps, importedReactiveProps);
1121
1150
  if (!knownProp && (!shouldWrap(expr, scope) || isReactiveContainerPassThrough(expr, scope))) {
package/dist/cli.js CHANGED
@@ -3,7 +3,7 @@
3
3
  import {
4
4
  runAnalyzeCli2,
5
5
  runCliIfMain2
6
- } from "./chunk-6dnsvgyf.js";
6
+ } from "./chunk-ad9mh6gz.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-6dnsvgyf.js";
20
+ } from "./chunk-ad9mh6gz.js";
21
21
 
22
22
  // src/transform.ts
23
23
  import { parse } from "@babel/parser";
@@ -9,6 +9,11 @@
9
9
  * - `createMutable` / `createStore` from `sinwan/store`
10
10
  * - `signal` / `computed` from `sinwan/reactivity`
11
11
  * - `useState` from `sinwan/react`
12
+ * - `.value` reads of signals returned from other modules (`useTheme`,
13
+ * `inject`, custom hooks) even when those identifiers were not created
14
+ * by a local `signal()` call
15
+ * - zero-arity getter calls returned from hooks (`counter()`, `api.count()`)
16
+ * the same way local `useState` getters (`count()`) are wrapped
12
17
  *
13
18
  * Example:
14
19
  * <p>{state.name}</p> → <p>{() => state.name}</p>
@@ -40,6 +45,10 @@ export interface ComputedBinding {
40
45
  export interface GetterBinding {
41
46
  kind: "getter";
42
47
  }
48
+ /** Local helper that is not a useState-style getter. `{greet()}` stays eager. */
49
+ export interface FunctionBinding {
50
+ kind: "function";
51
+ }
43
52
  /**
44
53
  * Binding for a variable holding the object returned by `useFetch()` /
45
54
  * `createFetch()(...)`. Every property on that object is a `Signal` or
@@ -54,7 +63,7 @@ export interface SignalObjectBinding {
54
63
  export interface PropBinding {
55
64
  kind: "prop";
56
65
  }
57
- export type Binding = MutableBinding | SignalBinding | ComputedBinding | GetterBinding | SignalObjectBinding | PropBinding;
66
+ export type Binding = MutableBinding | SignalBinding | ComputedBinding | GetterBinding | FunctionBinding | SignalObjectBinding | PropBinding;
58
67
  export interface ReactiveScope {
59
68
  bindings: Map<string, Binding>;
60
69
  }
package/docs/README.md CHANGED
@@ -7,7 +7,7 @@ If you are building a Sinwan app, start with [Plugins](plugins.md). If you are c
7
7
  ## Pipeline
8
8
 
9
9
  1. **Auto-`cc`** — exported functions that look like components (`App`, `Card`, …) are wrapped with `cc(...)` so the rest of the pipeline sees the same components the runtime will.
10
- 2. **Reactive wrap** — JSX expressions that read signals, stores, `useState`, or `useFetch` become `() => …` (or explicit binding helpers when that mode is on).
10
+ 2. **Reactive wrap** — JSX expressions that read signals, stores, `useState`, `useFetch`, `.value` on signals returned from other modules (`useTheme`, `inject`), or zero-arity getters returned from hooks (`counter()`) become `() => …` (or explicit binding helpers when that mode is on).
11
11
  3. **Template hoist** — static native-element trees become `_$createTemplate(...)` calls with slots for dynamic bits (`child`, `attr`, `event`, `ref`).
12
12
  4. **Analyze (optional)** — a project-wide pass marks which *exported* component props are actually reactive, so static strings are not wrapped.
13
13
 
package/docs/transform.md CHANGED
@@ -70,8 +70,10 @@ Reads the compiler wraps:
70
70
  - Store / mutable fields: `state.name`, destructured `const { name } = state`
71
71
  - Signals and computeds: `count.value`, including `count.value?.n` and `count.value!.n`
72
72
  - `useState` getters: `count()`
73
+ - Zero-arity getters **returned from another hook**: `{count()}`, `{counter()}`, `{api.count()}`. Local helpers (`greet()`), calls with arguments (`format("x")`), and event attributes (`onclick={make()}`) stay eager.
73
74
  - `useFetch` signal values: `f.data.value` (not the shell `f` or `f.data`)
74
75
  - Calls to a **local** function whose body reads one of the above
76
+ - `.value` reads of signals that came from **another module** (`useTheme()`, `inject()`, custom hooks) even when the identifier was not created by a local `signal()` call: `{theme.value}`, `{api.theme.value}`. Nested JSX inside `.map` still wraps the inner slots (`variant={theme.value === value ? …}`) rather than remounting the list.
75
77
  - 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).
76
78
 
77
79
  Not wrapped:
@@ -90,7 +92,7 @@ Not wrapped:
90
92
  **User components** (`<Label>`, `<Child title={…} />`):
91
93
 
92
94
  - **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.
95
+ - **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. With `explicitBindings`, component props stay **getters** (`each={() => …}`), never `_$bindAttr` — `For` / `Show` / `Label` call `resolve(prop)`, which does not unwrap bind descriptors.
94
96
 
95
97
  **Built-in control-flow** components wrap specific props, and they wrap reactive expression children (those children render directly):
96
98
 
@@ -141,6 +143,6 @@ transformJSX(code, "src/App.tsx", {
141
143
 
142
144
  - **`analyze` / `analyzeMetadata`** — cross-file reactive props. Invalid JSON is ignored (transform still succeeds).
143
145
  - **`resolveImport`** — maps `./Child` to an absolute file so imported components can use analyzer metadata. Without it, imported call sites are not wrapped at the parent.
144
- - **`explicitBindings`** — wrap with `_$bindText` / `_$bindAttr` / `_$bindStyle` / `_$bindClass` from `sinwan/renderer` instead of a bare `() => …`.
146
+ - **`explicitBindings`** — wrap **native** text/attrs with `_$bindText` / `_$bindAttr` / `_$bindStyle` / `_$bindClass`. Component props and component children stay `() => …` so `resolve(prop)` still sees a getter.
145
147
 
146
148
  Returns `{ code, map }` with a source map.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sinwan-compiler",
3
- "version": "0.2.9",
3
+ "version": "0.2.11",
4
4
  "description": "Shared compiler core for Sinwan plugins",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",