sinwan-compiler 0.2.10 → 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.
- package/dist/{chunk-qj8m16x2.js → chunk-ad9mh6gz.js} +38 -13
- package/dist/cli.js +1 -1
- package/dist/index.js +1 -1
- package/dist/reactive-wrap.d.ts +10 -1
- package/docs/README.md +1 -1
- package/docs/transform.md +2 -0
- package/package.json +1 -1
|
@@ -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
|
|
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 (
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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
|
|
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 (
|
|
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 (
|
|
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
|
|
578
|
+
return { isComponent: false, componentName: null, attributeName };
|
|
555
579
|
}
|
|
556
580
|
return { isComponent: true, componentName, attributeName };
|
|
557
581
|
}
|
|
@@ -1114,12 +1138,13 @@ function wrapReactiveExpressions2(ast, options = {}) {
|
|
|
1114
1138
|
trackPropBindings(scope, path.node.params[0], allProps);
|
|
1115
1139
|
}
|
|
1116
1140
|
}
|
|
1117
|
-
if (scope.bindings.size === 0)
|
|
1118
|
-
return;
|
|
1119
1141
|
path.traverse({
|
|
1120
1142
|
JSXExpressionContainer(exprPath) {
|
|
1121
1143
|
const expr = exprPath.node.expression;
|
|
1122
1144
|
const compInfo = getComponentExpressionInfo(exprPath);
|
|
1145
|
+
if (compInfo.attributeName?.startsWith("on")) {
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1123
1148
|
if (compInfo.isComponent && compInfo.attributeName !== null) {
|
|
1124
1149
|
const knownProp = !!compInfo.componentName && isReactiveComponentProp(compInfo.componentName, compInfo.attributeName, componentNames, reactiveProps, importedReactiveProps);
|
|
1125
1150
|
if (!knownProp && (!shouldWrap(expr, scope) || isReactiveContainerPassThrough(expr, scope))) {
|
package/dist/cli.js
CHANGED
package/dist/index.js
CHANGED
package/dist/reactive-wrap.d.ts
CHANGED
|
@@ -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 `
|
|
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:
|