sinwan-compiler 0.2.4 → 0.2.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/docs/api.md CHANGED
@@ -1,25 +1,41 @@
1
1
  # API reference
2
2
 
3
- ## `transformJSX(code, filename, options?)`
3
+ Import from `sinwan-compiler`. Types below match the package exports.
4
4
 
5
- Transforms a single JSX/TSX file.
5
+ ## Transform
6
+
7
+ ### `transformJSX(code, filename, options?)`
8
+
9
+ Rewrite one module. Returns `{ code: string; map?: unknown }`.
6
10
 
7
11
  ```ts
8
12
  import { transformJSX } from "sinwan-compiler";
9
13
 
10
- const result = transformJSX(code, filename, {
14
+ const { code, map } = transformJSX(source, "src/App.tsx", {
11
15
  hoist: true,
16
+ dev: false,
12
17
  explicitBindings: false,
13
18
  analyze: "./.sinwan/reactive-props.json",
14
- analyzeMetadata: new Map(),
19
+ analyzeMetadata: cache.reactiveProps,
20
+ resolveImport: (source, fromFile) => /* absolute path or null */,
15
21
  });
16
22
  ```
17
23
 
18
- Returns `{ code: string; map?: any }`.
24
+ See [Transform](transform.md) for wrapping rules and options.
25
+
26
+ ### `COMPILER_TEMPLATE_SLOT_PROTOCOL`
27
+
28
+ `{ slotPrefix, encodeSlot(index), decodeSlot(data) }`. Markers look like `s:0`. Keep this aligned with the Sinwan runtime protocol.
29
+
30
+ ### `autoWrapComponents(ast, filename?)`
19
31
 
20
- ## `analyze(options)`
32
+ Mutates a Babel AST: wrap exported component-like functions with `cc(...)`. Returns `true` if anything changed. `transformJSX` already runs this.
21
33
 
22
- Production entry point. Analyzes the project and writes metadata to disk.
34
+ ## Analysis
35
+
36
+ ### `analyze(options)`
37
+
38
+ Scan a project and write JSON to `outFile`.
23
39
 
24
40
  ```ts
25
41
  import { analyze } from "sinwan-compiler";
@@ -27,58 +43,93 @@ import { analyze } from "sinwan-compiler";
27
43
  analyze({
28
44
  root: "./src",
29
45
  outFile: "./.sinwan/reactive-props.json",
46
+ extensions: [".tsx", ".ts", ".jsx", ".js"], // default
30
47
  tsConfigPath: "./tsconfig.json",
31
48
  bunfigPath: "./bunfig.toml",
32
49
  workspaces: "../../package.json",
50
+ resolve: (source, fromFile) => null,
33
51
  });
34
52
  ```
35
53
 
36
- ## `analyzeProject(options)`
54
+ Missing `tsconfig.json` / `bunfig.toml` next to `root` are skipped unless you pass the paths explicitly.
55
+
56
+ ### `analyzeProject(options)`
37
57
 
38
- Programmatic analyzer. Returns the full project analysis without writing to disk.
58
+ Same scan, no disk write. Returns `{ modules, reactiveProps }`.
39
59
 
40
60
  ```ts
41
61
  import { analyzeProject } from "sinwan-compiler";
42
62
 
43
63
  const project = analyzeProject({
44
64
  root: "./src",
45
- files: { "/project/A.tsx": "..." },
65
+ files: { "/tmp/A.tsx": "export const A = () => <div/>" }, // optional in-memory map
46
66
  tsConfigPath: "./tsconfig.json",
67
+ workspaces: { file: "../../package.json", include: ["../../packages/ui"] },
47
68
  });
48
69
 
49
- project.reactiveProps.get("/project/A.tsx")?.get("Component");
70
+ project.reactiveProps.get("/tmp/A.tsx")?.get("A"); // Set<string> | undefined
50
71
  ```
51
72
 
52
- ## `AnalyzerCache`
73
+ ### `analyzeModule(code, filePath)`
74
+
75
+ Analyze one file. Used by the cache and tests.
76
+
77
+ ### `loadMetadata(filePath)`
53
78
 
54
- Incremental analyzer for dev/HMR.
79
+ Read analyzer JSON into `Map<file, Map<exportName, Set<prop>>>`. Missing file → empty map.
80
+
81
+ ### `loadWorkspacePackages(workspaces, root)`
82
+
83
+ Resolve workspace directories from a file, a list of globs/paths, or `{ file, include }`.
84
+
85
+ ### `AnalyzerCache`
86
+
87
+ Incremental graph for plugins.
55
88
 
56
89
  ```ts
57
90
  import { AnalyzerCache } from "sinwan-compiler";
58
91
 
59
92
  const cache = new AnalyzerCache({
60
- root: "./src",
93
+ root: process.cwd(),
61
94
  tsConfigPath: "./tsconfig.json",
62
95
  bunfigPath: "./bunfig.toml",
63
96
  workspaces: "../../package.json",
64
97
  cachePath: "./.sinwan/cache.json",
98
+ resolve: (source, fromFile) => null, // optional
65
99
  });
66
100
 
67
- cache.update(filePath, newCode);
68
- const metadata = cache.reactiveProps;
101
+ cache.update(absPath, source);
102
+ cache.remove(absPath); // also recomputes modules this file imported
103
+ cache.save(); // no-op without cachePath
104
+
105
+ cache.reactiveProps; // Map<file, Map<export, Set<prop>>>
106
+ cache.modules;
69
107
  ```
70
108
 
71
- ### Methods
109
+ `restore` runs in the constructor when `cachePath` exists. Stale function spans skip that module instead of throwing.
110
+
111
+ ## CLI helpers
112
+
113
+ ### `runAnalyzeCli(args?)`
114
+
115
+ Parse `analyze [root] [outFile] [--tsconfig] [--bunfig] [--workspaces]` and call `analyze()`. Defaults to `process.argv.slice(2)`. Unknown commands write to stderr and `process.exit(1)`.
116
+
117
+ ## Other exports
72
118
 
73
- - `update(filePath, code)` — re-analyze a file and recompute affected files.
74
- - `remove(filePath)` — remove a file and recompute affected files.
75
- - `save()` — persist the cache to `cachePath`.
76
- - `restore()` — load the cache from `cachePath` (called automatically if `cachePath` exists).
119
+ | Name | Role |
120
+ | ---- | ---- |
121
+ | `collectExportedComponents(code)` | File-local exported component names (no `cc` required) |
122
+ | `wrapReactiveExpressions(ast, options?)` | Reactive wrap without hoisting |
123
+ | `trackReactiveImports(ast)` | Import-name sets for signals, `cc`, `useFetch`, … |
124
+ | `collectComponentFunctions(ast, ccNames)` | `cc(...)` functions, names, and exports |
125
+ | `computeLocalScopes(ast, names)` | Per-function reactive bindings |
126
+ | `collectComponentCallGraph(ast, names)` | Local JSX call sites |
127
+ | `propagateReactiveProps(scopes, graph, fns)` | Fixed-point prop sets |
128
+ | `buildFullScope(scopes, fn, reactiveProps)` | Local bindings plus reactive props |
129
+ | `containsReactiveValue(expr, scope)` | Whether an expression reads a reactive value |
130
+ | `isReactiveValue(node, scope)` | Single-node check |
131
+ | `getAllPropNames(params)` | Prop names from a component parameter (`*` for a bare identifier) |
77
132
 
78
133
  ## Types
79
134
 
80
- - `CallSite` — local component call site.
81
- - `ImportedCallSite` — cross-file component call site.
82
- - `ModuleAnalysis` — analysis of a single module.
83
- - `ProjectAnalysis` — analysis of the whole project.
84
- - `WorkspacesConfig` — workspace file path, explicit package paths/globs, or both. Used to enable cross-package analysis in monorepos.
135
+ `AnalyzeOptions`, `WorkspacesConfig`, `ModuleAnalysis`, `ProjectAnalysis`, `ImportInfo`, `ImportedCallSite`, `CallSite`, `ReactiveScope`, `Binding`, `ImportNames`, `ComponentExpressionInfo`, `ExportedComponent`, `TransformOptions`.
@@ -1,75 +1,78 @@
1
1
  # Architecture
2
2
 
3
- ## File layout
3
+ This page is for people changing the compiler. App authors can stay on [Transform](transform.md) and [Plugins](plugins.md).
4
+
5
+ ## Layout
4
6
 
5
7
  ```
6
8
  src/
7
- index.ts Public exports
8
- transform.ts JSX transform and template hoisting
9
- reactive-wrap.ts Reactive source detection, call graph, propagation
10
- analyze.ts Cross-file analyzer, cache, import resolution, CLI
11
- cli.ts CLI entry point
9
+ index.ts Public exports
10
+ auto-cc.ts Exported function → cc(...)
11
+ reactive-wrap.ts Imports, scopes, call graph, wrap
12
+ transform.ts Hoist + transformJSX
13
+ analyze.ts Project scan, resolution, AnalyzerCache
14
+ cli.ts analyze CLI
15
+ exports.ts collectExportedComponents
12
16
  ```
13
17
 
14
18
  ## Data flow
15
19
 
16
- 1. **Transform**: receives source code and optional reactive-prop metadata.
17
- 2. **Analyzer (dev)**: builds `AnalyzerCache` incrementally as files are transformed.
18
- 3. **Analyzer (production)**: `analyzeProject()` scans the whole project and writes metadata.
19
- 4. **Propagation**: `propagateReactiveProps()` runs a fixed-point algorithm over the call graph.
20
+ ```text
21
+ transformJSX
22
+ autoWrapComponents
23
+ wrapReactiveExpressions ← analyze / analyzeMetadata / resolveImport
24
+ hoist JSXElement roots ← _$createTemplate + slots
25
+
26
+ analyze / AnalyzerCache.update
27
+ autoWrapComponents
28
+ analyzeModule ← scopes + local + imported call sites
29
+ buildProjectReactiveProps
30
+ propagateReactiveProps
31
+ ```
20
32
 
21
- ## Key data structures
33
+ Dev plugins call `cache.update` on each file, then pass `cache.reactiveProps` into `transformJSX` as `analyzeMetadata`. Production writes the same map to JSON (`analyze`) and the plugin sets `analyze: path`.
22
34
 
23
- ### `CallSite`
35
+ ## Call graph
24
36
 
25
- ```ts
26
- {
27
- callee: t.Function;
28
- props: { name: string; value: t.Expression }[];
29
- spreads: t.Expression[];
30
- }
31
- ```
37
+ A **local** site is `<Child />` where `Child` is `cc(...)` in the same file.
32
38
 
33
- `spreads` stores the spread expressions. Object-literal spreads are analyzed precisely; unknown spreads are treated conservatively.
39
+ An **imported** site stores `{ caller, source, name, props, spreads }`. The project pass resolves `source` to a file and attaches the site to that file’s export (or `"default"`).
34
40
 
35
- ### `ModuleAnalysis`
41
+ `spreads` are analyzed when the expression is an object literal or a local `const props = { … }` in the caller. Otherwise every known callee prop is marked reactive.
36
42
 
37
- Per-file analysis including:
43
+ ## Propagation
38
44
 
39
- - imports
40
- - exports
41
- - default export
42
- - component functions
43
- - local scopes
44
- - local call graph
45
- - imported call sites
45
+ 1. Seed callee sets from values that are already reactive in the caller’s local scope.
46
+ 2. Worklist over all component functions.
47
+ 3. For each site, add prop names whose values are reactive in the caller’s **full** scope (local bindings + that function’s own reactive props).
48
+ 4. Repeat until sets stop growing.
46
49
 
47
- ### `AnalyzerCache`
50
+ Cycles terminate because sets only grow.
48
51
 
49
- - Stores `ModuleAnalysis` objects keyed by absolute file path.
50
- - Maintains `reactiveProps: Map<filePath, Map<exportName, Set<propName>>>`.
51
- - Maintains a reverse importer index for targeted incremental propagation.
52
- - Serializes to JSON for persistence across process restarts.
52
+ ## Import resolution
53
53
 
54
- ## Propagation algorithm
54
+ `createResolver` order:
55
55
 
56
- 1. Initialize every exported component with an empty reactive-prop set.
57
- 2. For each component function, build a full scope that includes its own bindings and the reactive props of its parent.
58
- 3. For each call site, check every named prop and object-literal spread key. If the value contains a reactive read, add the prop name to the callee's reactive set.
59
- 4. For unknown spreads, add all known callee prop names.
60
- 5. If a callee's reactive set grows, add the callee to the worklist and repeat until stable.
56
+ 1. `tsconfig` `paths`
57
+ 2. `bunfig.toml` aliases
58
+ 3. Workspace package name + subpath
59
+ 4. Relative path + extensions / `index.*`
61
60
 
62
- ## Import resolution
61
+ `AnalyzerCache` wraps that with an in-memory lookup so HMR can resolve `./Child` to a module that exists only in the cache.
62
+
63
+ ## Cache persistence
64
+
65
+ Serialized modules store function **start/end** offsets. Restore re-parses the file and remaps those spans. If a span is gone (the file was edited), `restoreModule` returns `null` and that entry is dropped. The next `update` fills it in.
63
66
 
64
- The resolver combines multiple sources:
67
+ `remove(path)`:
65
68
 
66
- - `tsconfig.json` `paths`
67
- - `bunfig.toml` aliases
68
- - Relative imports
69
- - Custom resolver
69
+ 1. Collect importers of `path` and modules `path` imported
70
+ 2. Delete the module and importer index entries
71
+ 3. `recomputeFor` that set
72
+ 4. `save()` if `cachePath` is set
70
73
 
71
- Resolution order: tsconfig → bunfig → relative → custom.
74
+ That second set matters: deleting a parent must let the child’s props become static again.
72
75
 
73
- ## Serialization
76
+ ## Template protocol
74
77
 
75
- The cache serializes modules using function start/end positions as stable identifiers. When restoring, the source file is re-parsed and functions are matched by position. This allows the cache to survive source edits as long as the analyzed functions remain at the same positions.
78
+ `COMPILER_TEMPLATE_SLOT_PROTOCOL` in `transform.ts` is duplicated from the runtime (`sinwan` template protocol). A drift test in the `sinwan` package keeps the two in sync. Slot types: `child`, `attr`, `event`, `ref`.
package/docs/cli.md CHANGED
@@ -1,46 +1,53 @@
1
1
  # CLI
2
2
 
3
- The `sinwan-compiler` package ships a small CLI for production analysis.
3
+ Production analysis is a single command. Dev servers do not need it; plugins keep a live cache.
4
4
 
5
- ## Command
5
+ The published binary name is `sinwan` (see `package.json` `"bin"`). After install you can also run the package name:
6
6
 
7
7
  ```bash
8
8
  bunx sinwan-compiler analyze [root] [outFile] [options]
9
+ # same after a local install:
10
+ sinwan analyze [root] [outFile] [options]
9
11
  ```
10
12
 
11
- - `root` — project root to scan (default: current working directory).
12
- - `outFile` — path for the output JSON (default: `<root>/.sinwan/reactive-props.json`).
13
+ | Argument | Default |
14
+ | -------- | ------- |
15
+ | `root` | Current working directory |
16
+ | `outFile` | `<root>/.sinwan/reactive-props.json` |
13
17
 
14
- ## Options
18
+ | Flag | Meaning |
19
+ | ---- | ------- |
20
+ | `--tsconfig`, `-t` | `tsconfig.json` for `paths` |
21
+ | `--bunfig`, `-b` | `bunfig.toml` for Bun aliases |
22
+ | `--workspaces`, `-w` | `package.json`, `pnpm-workspace.yaml`, or a package directory |
15
23
 
16
- | Flag | Description |
17
- | -------------------- | --------------------------------------------------------------------------------- |
18
- | `--tsconfig`, `-t` | Path to `tsconfig.json` for path alias resolution. |
19
- | `--bunfig`, `-b` | Path to `bunfig.toml` for Bun alias resolution. |
20
- | `--workspaces`, `-w` | Path to a `package.json` or `pnpm-workspace.yaml` to discover workspace packages. |
24
+ Anything other than `analyze` prints a short usage line and exits with code `1`.
21
25
 
22
26
  ## Example
23
27
 
24
28
  ```bash
25
- bunx sinwan-compiler analyze ./src ./.sinwan/reactive-props.json --tsconfig ./tsconfig.json --workspaces ../../package.json
29
+ bunx sinwan-compiler analyze ./src ./.sinwan/reactive-props.json \
30
+ --tsconfig ./tsconfig.json \
31
+ --workspaces ../../package.json
26
32
  ```
27
33
 
28
- ## Output format
34
+ ## Output
29
35
 
30
- The CLI writes a JSON file mapping each source file to its exported component names and their reactive props:
36
+ JSON: absolute file path → export name → reactive prop names.
31
37
 
32
38
  ```json
33
39
  {
34
40
  "/project/src/Child.tsx": {
35
- "Child": ["title"]
41
+ "Child": ["title"],
42
+ "default": ["label"]
36
43
  }
37
44
  }
38
45
  ```
39
46
 
40
- ## Use in plugins
41
-
42
- Pass the generated JSON to the Bun or Vite plugin via the `analyze` option:
47
+ Pass that file to a plugin:
43
48
 
44
49
  ```ts
45
50
  sinwan({ analyze: "./.sinwan/reactive-props.json" });
46
51
  ```
52
+
53
+ Programmatic equivalent: `analyze({ root, outFile, tsConfigPath, bunfigPath, workspaces })` from [API](api.md).
package/docs/plugins.md CHANGED
@@ -1,60 +1,65 @@
1
- # Bun and Vite plugins
1
+ # Using the compiler from Bun and Vite
2
2
 
3
- Both plugins use the shared compiler package. The cache is optional and enabled by passing the `cache` option.
3
+ App projects should depend on **`bun-plugin-sinwan`** or **`vite-plugin-sinwan`**. Those packages import `sinwan-compiler` at runtime (`packages: "external"` in their builds) and pass the right options. You do not add `sinwan-compiler` unless you customize the pipeline.
4
4
 
5
- ## Bun plugin
5
+ The plugins accept any compiler `>=0.2.5 <1.0.0`. A new compiler release does not need a matching plugin publish. Existing apps update with `bun update sinwan-compiler`.
6
+
7
+ ## Bun
6
8
 
7
9
  ```ts
8
10
  import { sinwan } from "bun-plugin-sinwan";
9
11
 
10
- Bun.build({
11
- entrypoints: ["./src/index.tsx"],
12
- plugins: [
13
- sinwan({
14
- hoist: true,
15
- cache: {
16
- root: "./src",
17
- tsConfigPath: "./tsconfig.json",
18
- bunfigPath: "./bunfig.toml",
19
- workspaces: "../../package.json",
20
- cachePath: "./.sinwan/cache.json",
21
- },
22
- }),
23
- ],
12
+ export default sinwan({
13
+ hoist: true,
14
+ cache: {
15
+ root: process.cwd(),
16
+ tsConfigPath: "./tsconfig.json",
17
+ bunfigPath: "./bunfig.toml",
18
+ workspaces: "../../package.json",
19
+ cachePath: "./.sinwan/cache.json",
20
+ },
24
21
  });
25
22
  ```
26
23
 
27
- ## Vite plugin
24
+ `bunfig.toml` `[serve.static] plugins` can only load a **ready-made** plugin object. Scaffolded apps keep a small `sinwan-plugin.ts` that calls `sinwan({ cache: … })` and export that object.
25
+
26
+ The Bun plugin pins `sinwan` / `sinwan/*` to the **app** `node_modules` so a `file:` or `link:` package (for example `sinwan-router`) does not load a second runtime.
27
+
28
+ ## Vite
28
29
 
29
30
  ```ts
30
31
  import { sinwan } from "vite-plugin-sinwan";
32
+ import { defineConfig } from "vite";
31
33
 
32
- export default {
34
+ export default defineConfig({
33
35
  plugins: [
34
36
  sinwan({
35
37
  hoist: true,
36
38
  cache: {
37
39
  tsConfigPath: "./tsconfig.json",
38
- bunfigPath: "./bunfig.toml",
39
- workspaces: "../../package.json",
40
40
  cachePath: "./.sinwan/cache.json",
41
41
  },
42
42
  }),
43
43
  ],
44
- };
44
+ });
45
45
  ```
46
46
 
47
- ## Cache options
47
+ The Vite plugin sets `resolve.dedupe: ["sinwan"]` for the same duplicate-runtime problem.
48
+
49
+ ## Shared plugin options
48
50
 
49
- | Option | Description |
50
- | -------------- | ----------------------------------------------------------------------------------------------------- |
51
- | `root` | Project root for the analyzer. |
52
- | `tsConfigPath` | Path to `tsconfig.json`. |
53
- | `bunfigPath` | Path to `bunfig.toml`. If omitted, the plugin tries to auto-detect `bunfig.toml` in the project root. |
54
- | `workspaces` | Path to a workspace file (`package.json` or `pnpm-workspace.yaml`) or explicit package paths/globs. |
55
- | `cachePath` | Path to a JSON file for persistent cache. |
51
+ | Option | Default | Role |
52
+ | ------ | ------- | ---- |
53
+ | `hoist` | `true` | Template hoisting |
54
+ | `explicitBindings` | `false` | `_$bind*` helpers instead of bare `() =>` |
55
+ | `analyze` | unset | Path to CLI JSON (production) |
56
+ | `cache` | `true` | Incremental analyzer in dev. `false` turns it off. An object sets `root`, `tsConfigPath`, `bunfigPath`, `workspaces`, `cachePath` |
57
+ | `fastRefresh` | `true` (Vite) | In-place component HMR in `vite serve` only |
56
58
 
57
59
  ## Dev vs production
58
60
 
59
- - In **dev**, enable the `cache` option for incremental HMR analysis.
60
- - In **production**, run the CLI ahead of time and pass the generated JSON via the `analyze` option for deterministic builds.
61
+ **Dev** — leave `cache` on (the default). Each transformed file calls `AnalyzerCache.update`. No CLI step.
62
+
63
+ **Production** — run [the CLI](cli.md) (or `analyze()`) and set `analyze` to that JSON so every CI machine uses the same metadata.
64
+
65
+ If a file is deleted in HMR, the plugin should call `cache.remove(path)` so child components drop reactive props that only that caller needed. The compiler’s `remove` updates both importers and imported modules.
package/docs/transform.md CHANGED
@@ -1,64 +1,139 @@
1
1
  # JSX transform
2
2
 
3
- The transform rewrites JSX so the Sinwan runtime can wrap reactive reads in effects at render time.
3
+ `transformJSX(code, filename, options?)` rewrites one module so the Sinwan runtime can track reactive reads and reuse static markup.
4
4
 
5
- ## Reactive wrapping
5
+ You normally get this through a plugin. Call it yourself only in tests or custom tooling.
6
6
 
7
- The compiler wraps expressions that read reactive values in zero-arity arrow functions. For example:
7
+ ## Quick example
8
8
 
9
9
  ```tsx
10
- <p>{state.name}</p>
10
+ import { cc } from "sinwan/component";
11
+ import { signal } from "sinwan/reactivity";
12
+
13
+ export const Counter = cc(() => {
14
+ const count = signal(0);
15
+ return <p>{count.value}</p>;
16
+ });
11
17
  ```
12
18
 
13
- becomes:
19
+ The compiler turns the child into a lazy getter:
14
20
 
15
21
  ```tsx
16
- <p>{() => state.name}</p>
22
+ <p>{() => count.value}</p>
17
23
  ```
18
24
 
19
- The runtime calls these functions inside an effect so the DOM updates when the value changes.
25
+ The runtime runs that function inside an effect and updates the text when `count` changes.
20
26
 
21
- ## What is reactive
27
+ ## Automatic `cc()` wrapping
22
28
 
23
- The compiler recognizes values from:
29
+ Exported functions that look like components are wrapped for you. You can write React-style components and still get a Sinwan instance and `displayName`.
24
30
 
25
- - `createMutable` / `createStore` from `sinwan/store`
26
- - `signal` / `computed` from `sinwan/reactivity`
27
- - `useState` from `sinwan/react-client`
31
+ A candidate must be:
28
32
 
29
- Reactive reads include:
33
+ - **Exported** (named or default)
34
+ - **Uppercase** (or an anonymous `export default function ()`)
35
+ - **0 or 1 parameter**
36
+ - **Returning JSX** in that function (not only in a nested helper)
37
+ - **Not already** `cc(...)` (or a local alias of `cc`)
30
38
 
31
- - Member expressions: `state.name`, `signal.value`
32
- - Function calls: `getCount()`
33
- - Derived expressions: `count() + 1`
39
+ ```tsx
40
+ export function App() {
41
+ return <h1>Hello</h1>;
42
+ }
43
+
44
+ // becomes
45
+ import { cc } from "sinwan/component";
46
+ export const App = cc(function App() {
47
+ return <h1>Hello</h1>;
48
+ });
49
+ ```
34
50
 
35
- ## What is NOT wrapped
51
+ Not wrapped: lowercase helpers, generators, multi-argument functions, or functions that never return JSX. If `cc` is not imported yet, the compiler adds `import { cc } from "sinwan/component"`.
36
52
 
37
- - Plain literals: `"Hello"`, `123`
38
- - Event handlers: `onClick={handleClick}`
39
- - Static bindings that the analyzer proved are non-reactive
53
+ `cc` may be imported from `sinwan` or `sinwan/component`.
40
54
 
41
- ## Cross-file optimization
55
+ ## What counts as reactive
42
56
 
43
- When the analyzer is enabled, the transform can skip wrapping a prop if the analyzer determined that all callers pass a static value. This reduces runtime overhead.
57
+ Tracked imports:
44
58
 
45
- ```tsx
46
- // Without analyzer: onClick={handleClick} is safe to skip wrapping
47
- // With analyzer: title="Hello" is also known static, so it is skipped
48
- <Child title="Hello" onClick={handleClick} />
49
- ```
59
+ | Module | Names |
60
+ | ------ | ----- |
61
+ | `sinwan/store` | `createMutable`, `createStore` |
62
+ | `sinwan/reactivity` | `signal`, `computed` |
63
+ | `sinwan/react` | `useState` |
64
+ | `sinwan/hook` | `useFetch` |
65
+
66
+ Reads the compiler wraps:
67
+
68
+ - Store / mutable fields: `state.name`, destructured `const { name } = state`
69
+ - Signals and computeds: `count.value`, including `count.value?.n` and `count.value!.n`
70
+ - `useState` getters: `count()`
71
+ - `useFetch` signal values: `f.data.value` (not the shell `f` or `f.data`)
72
+ - Calls to a **local** function whose body reads one of the above
73
+
74
+ Not wrapped:
75
+
76
+ - Literals and plain identifiers
77
+ - Event-handler functions (`onClick={fn}` or `onClick={() => …}`)
78
+ - Nested function *values* passed as props (`title={() => s.value}` stays a render prop)
79
+ - `useFetch` methods such as `abort` / `execute`
80
+ - Static values the [analyzer](analyzer.md) proved never change
81
+
82
+ ## DOM vs component JSX
83
+
84
+ **Native elements** (`div`, `p`, `button`, …): wrap reactive children and attributes.
85
+
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.
87
+
88
+ **Built-in control-flow** components wrap specific props, and they *do* wrap reactive expression children (those children render directly):
89
+
90
+ | Component | Reactive props |
91
+ | --------- | -------------- |
92
+ | `For`, `Index`, `Virtual` | `each` |
93
+ | `Show`, `Switch`, `Match`, `Key` | `when` |
94
+ | `Dynamic` | `component` |
95
+ | `Visible` | `when`, `style` |
96
+ | `Portal` | `mount` |
97
+ | `Activity` | `mode` |
98
+
99
+ `Suspense` is not in this list; its props are left alone.
100
+
101
+ ## Template hoisting
102
+
103
+ Static native trees become a module-level template plus `_$createTemplate(tmpl, [dynamic…])`.
104
+
105
+ Slots:
106
+
107
+ | Type | When |
108
+ | ---- | ---- |
109
+ | `child` | `{expr}`, or a capitalized child component |
110
+ | `attr` | Dynamic attribute (not `on*`) |
111
+ | `event` | `onClick={…}` and other `on*` handlers |
112
+ | `ref` | `ref={fn}` or `ref={object}` |
113
+
114
+ Static `style` objects and strings are written into the HTML (`backgroundColor` → `background-color`). Quoted style strings that contain `${…}` log a warning in the compiler: use a template literal, `style={\`…${x}\`}`.
115
+
116
+ Hoisting is skipped (the JSX is left as JSX) when the root is a component, the tag is a member expression (`Icons.Star` as the root), a spread is on a native element (`<div {...props}>`), or the element needs a JSX enhancer (`<select defaultValue>`, function `action`/`formAction`, head tags without `itemProp`, and the other cases in `enhanced-elements.ts`). Nested enhanced tags become child slots so the native shell can still hoist. In `dev: true`, skipped hoists from extract errors log a warning.
117
+
118
+ Boolean attributes without a value are emitted as HTML flags: `<button disabled>`.
119
+
120
+ The slot marker format is `COMPILER_TEMPLATE_SLOT_PROTOCOL` (`s:0`, `s:1`, …). It must stay aligned with the runtime protocol in `sinwan`.
50
121
 
51
122
  ## Options
52
123
 
53
124
  ```ts
54
- transformJSX(code, filename, {
55
- hoist: true, // template hoisting
56
- explicitBindings: false, // emit compiler-driven binding descriptors
57
- analyze: "path/to/reactive-props.json", // production metadata
58
- analyzeMetadata: cache?.reactiveProps, // dev cache metadata
125
+ transformJSX(code, "src/App.tsx", {
126
+ hoist: true, // default true
127
+ dev: false, // warn when hoisting is skipped
128
+ explicitBindings: false, // emit _$bindText / _$bindAttr / …
129
+ analyze: "./.sinwan/reactive-props.json",
130
+ analyzeMetadata: cache.reactiveProps,
131
+ resolveImport: (source, fromFile) => absolutePathOrNull,
59
132
  });
60
133
  ```
61
134
 
62
- ## Template hoisting
135
+ - **`analyze` / `analyzeMetadata`** — cross-file reactive props. Invalid JSON is ignored (transform still succeeds).
136
+ - **`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.
137
+ - **`explicitBindings`** — wrap with `_$bindText` / `_$bindAttr` / `_$bindStyle` / `_$bindClass` from `sinwan/renderer` instead of a bare `() => …`.
63
138
 
64
- Static JSX subtrees are hoisted into a top-level template factory so they are not recreated on every render.
139
+ Returns `{ code, map }` with a source map.