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/README.md +39 -14
- package/dist/analyze.d.ts +0 -1
- package/dist/{chunk-w580fdrf.js → chunk-m4rk009c.js} +169 -219
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +5 -3
- package/dist/index.js +110 -44
- package/dist/reactive-wrap.d.ts +4 -0
- package/docs/README.md +38 -19
- package/docs/analyzer.md +63 -52
- package/docs/api.md +77 -26
- package/docs/architecture.md +52 -49
- package/docs/cli.md +24 -17
- package/docs/plugins.md +37 -32
- package/docs/transform.md +109 -34
- package/package.json +2 -2
package/dist/cli.d.ts
CHANGED
|
@@ -8,3 +8,5 @@
|
|
|
8
8
|
* Writes `.sinwan/reactive-props.json` by default.
|
|
9
9
|
*/
|
|
10
10
|
export declare function runAnalyzeCli(args?: string[]): void;
|
|
11
|
+
/** Runs the CLI only when this module is the process entry point. */
|
|
12
|
+
export declare function runCliIfMain(isMain?: boolean, args?: string[]): void;
|
package/dist/cli.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
} from "./chunk-
|
|
3
|
+
autoWrapComponents2,
|
|
4
|
+
analyzeModule2,
|
|
5
|
+
analyzeProject2,
|
|
6
|
+
AnalyzerCache2,
|
|
7
|
+
analyze2,
|
|
8
|
+
loadMetadata2,
|
|
9
|
+
trackReactiveImports2,
|
|
10
|
+
getAllPropNames2,
|
|
11
|
+
isReactiveValue2,
|
|
12
|
+
containsReactiveValue2,
|
|
13
|
+
collectComponentFunctions2,
|
|
14
|
+
computeLocalScopes2,
|
|
15
|
+
collectComponentCallGraph2,
|
|
16
|
+
buildFullScope2,
|
|
17
|
+
propagateReactiveProps2,
|
|
18
|
+
wrapReactiveExpressions2,
|
|
19
|
+
runAnalyzeCli2
|
|
20
|
+
} from "./chunk-m4rk009c.js";
|
|
21
21
|
|
|
22
22
|
// src/transform.ts
|
|
23
23
|
import { parse } from "@babel/parser";
|
|
@@ -92,6 +92,63 @@ function cleanJSXText(value) {
|
|
|
92
92
|
}
|
|
93
93
|
return str;
|
|
94
94
|
}
|
|
95
|
+
function htmlAttributeName(attrName) {
|
|
96
|
+
if (attrName === "defaultValue")
|
|
97
|
+
return "value";
|
|
98
|
+
if (attrName === "defaultChecked")
|
|
99
|
+
return "checked";
|
|
100
|
+
return attrName;
|
|
101
|
+
}
|
|
102
|
+
function jsxAttrName(attr) {
|
|
103
|
+
if (attr.type !== "JSXAttribute" || attr.name.type !== "JSXIdentifier") {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
return attr.name.name;
|
|
107
|
+
}
|
|
108
|
+
function hasAttr(attributes, name) {
|
|
109
|
+
for (const attr of attributes) {
|
|
110
|
+
if (jsxAttrName(attr) === name)
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
function attrIsExpression(attributes, name) {
|
|
116
|
+
for (const attr of attributes) {
|
|
117
|
+
if (attr.type !== "JSXAttribute" || jsxAttrName(attr) !== name)
|
|
118
|
+
continue;
|
|
119
|
+
return attr.value?.type === "JSXExpressionContainer";
|
|
120
|
+
}
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
function elementNeedsJsxEnhancer(node) {
|
|
124
|
+
const tagName = jsxNameToString(node.openingElement.name);
|
|
125
|
+
const attrs = node.openingElement.attributes;
|
|
126
|
+
switch (tagName) {
|
|
127
|
+
case "select":
|
|
128
|
+
return hasAttr(attrs, "defaultValue") || hasAttr(attrs, "value");
|
|
129
|
+
case "textarea":
|
|
130
|
+
return hasAttr(attrs, "value");
|
|
131
|
+
case "option":
|
|
132
|
+
return hasAttr(attrs, "selected");
|
|
133
|
+
case "form":
|
|
134
|
+
return attrIsExpression(attrs, "action");
|
|
135
|
+
case "input":
|
|
136
|
+
case "button":
|
|
137
|
+
return attrIsExpression(attrs, "formAction");
|
|
138
|
+
case "progress":
|
|
139
|
+
return attrIsExpression(attrs, "value");
|
|
140
|
+
case "style":
|
|
141
|
+
return hasAttr(attrs, "href") && hasAttr(attrs, "precedence");
|
|
142
|
+
case "script":
|
|
143
|
+
return hasAttr(attrs, "src") && hasAttr(attrs, "async");
|
|
144
|
+
case "link":
|
|
145
|
+
case "meta":
|
|
146
|
+
case "title":
|
|
147
|
+
return !hasAttr(attrs, "itemProp");
|
|
148
|
+
default:
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
95
152
|
function extractTemplate(node, filename) {
|
|
96
153
|
const counter = { value: 0 };
|
|
97
154
|
const slots = [];
|
|
@@ -175,6 +232,7 @@ function elementToHtml(node, slots, path, filename, counter) {
|
|
|
175
232
|
}
|
|
176
233
|
const isVoid = VOID_ELEMENTS.has(tagName);
|
|
177
234
|
let html = `<${tagName}`;
|
|
235
|
+
let textareaDefault = "";
|
|
178
236
|
for (const attr of node.openingElement.attributes) {
|
|
179
237
|
if (attr.type === "JSXSpreadAttribute") {
|
|
180
238
|
throw new Error("Cannot hoist element with spread attributes");
|
|
@@ -196,13 +254,14 @@ function elementToHtml(node, slots, path, filename, counter) {
|
|
|
196
254
|
}
|
|
197
255
|
continue;
|
|
198
256
|
}
|
|
257
|
+
const htmlName = htmlAttributeName(attrName);
|
|
199
258
|
if (attr.value?.type === "JSXExpressionContainer") {
|
|
200
259
|
const expr = attr.value.expression;
|
|
201
260
|
if (expr.type === "JSXEmptyExpression")
|
|
202
261
|
continue;
|
|
203
262
|
if (attrName === "style" && t.isExpression(expr) && isStaticStyleValue(expr)) {
|
|
204
263
|
const serialized = serializeStyleValue(expr);
|
|
205
|
-
html += ` ${
|
|
264
|
+
html += ` ${htmlName}="${escapeHtml(serialized)}"`;
|
|
206
265
|
continue;
|
|
207
266
|
}
|
|
208
267
|
slots.push({
|
|
@@ -211,7 +270,7 @@ function elementToHtml(node, slots, path, filename, counter) {
|
|
|
211
270
|
name: attrName,
|
|
212
271
|
expr
|
|
213
272
|
});
|
|
214
|
-
html += ` ${
|
|
273
|
+
html += ` ${htmlName}=""`;
|
|
215
274
|
continue;
|
|
216
275
|
}
|
|
217
276
|
if (attr.value?.type === "JSXText" || attr.value?.type === "StringLiteral") {
|
|
@@ -219,9 +278,13 @@ function elementToHtml(node, slots, path, filename, counter) {
|
|
|
219
278
|
if (attrName === "style") {
|
|
220
279
|
warnSuspectedStyleInterpolation(rawValue, filename, attr.value.loc?.start?.line);
|
|
221
280
|
}
|
|
222
|
-
|
|
281
|
+
if (tagName === "textarea" && attrName === "defaultValue") {
|
|
282
|
+
textareaDefault = escapeHtml(rawValue);
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
html += ` ${htmlName}="${escapeHtml(rawValue)}"`;
|
|
223
286
|
} else if (!attr.value) {
|
|
224
|
-
html += ` ${
|
|
287
|
+
html += ` ${htmlName}`;
|
|
225
288
|
}
|
|
226
289
|
}
|
|
227
290
|
html += isVoid ? " />" : ">";
|
|
@@ -229,6 +292,7 @@ function elementToHtml(node, slots, path, filename, counter) {
|
|
|
229
292
|
return html;
|
|
230
293
|
const childResult = childrenToHtml(node.children, slots, path, filename, 0, counter);
|
|
231
294
|
html += childResult.html;
|
|
295
|
+
html += textareaDefault;
|
|
232
296
|
html += `</${tagName}>`;
|
|
233
297
|
return html;
|
|
234
298
|
}
|
|
@@ -257,7 +321,7 @@ function childrenToHtml(children, slots, path, filename, startIndex, counter) {
|
|
|
257
321
|
const childName = child.openingElement.name;
|
|
258
322
|
const childTag = childName.type === "JSXIdentifier" ? childName.name : "";
|
|
259
323
|
const isFirstCharUpper = childTag && childTag.charAt(0) === childTag.charAt(0).toUpperCase();
|
|
260
|
-
if (isFirstCharUpper) {
|
|
324
|
+
if (isFirstCharUpper || elementNeedsJsxEnhancer(child)) {
|
|
261
325
|
const slot = nextSlotId(counter);
|
|
262
326
|
slots.push({
|
|
263
327
|
path: [...path, childIndex],
|
|
@@ -292,8 +356,8 @@ function transformJSX(code, filename, options = {}) {
|
|
|
292
356
|
plugins: ["jsx", "typescript"],
|
|
293
357
|
sourceFilename: filename
|
|
294
358
|
});
|
|
295
|
-
|
|
296
|
-
|
|
359
|
+
autoWrapComponents2(ast, filename);
|
|
360
|
+
wrapReactiveExpressions2(ast, {
|
|
297
361
|
explicitBindings: options.explicitBindings,
|
|
298
362
|
analyze: options.analyze,
|
|
299
363
|
analyzeMetadata: options.analyzeMetadata,
|
|
@@ -316,6 +380,8 @@ function transformJSX(code, filename, options = {}) {
|
|
|
316
380
|
const name = tagName.name;
|
|
317
381
|
if (!name || name[0] !== name[0].toLowerCase())
|
|
318
382
|
return;
|
|
383
|
+
if (elementNeedsJsxEnhancer(path.node))
|
|
384
|
+
return;
|
|
319
385
|
try {
|
|
320
386
|
const extracted = extractTemplate(path.node, filename);
|
|
321
387
|
const tmplId = `_$tmpl_${templateCounter++}`;
|
|
@@ -341,11 +407,11 @@ function transformJSX(code, filename, options = {}) {
|
|
|
341
407
|
], t.stringLiteral("sinwan/renderer"));
|
|
342
408
|
ast.program.body.unshift(bindingImport);
|
|
343
409
|
}
|
|
344
|
-
const
|
|
410
|
+
const result = generate(ast, {
|
|
345
411
|
sourceMaps: true,
|
|
346
412
|
sourceFileName: filename
|
|
347
413
|
});
|
|
348
|
-
return { code:
|
|
414
|
+
return { code: result.code, map: result.map };
|
|
349
415
|
}
|
|
350
416
|
const templateDecls = templates.map(({ id, template }) => t.variableDeclaration("const", [
|
|
351
417
|
t.variableDeclarator(t.identifier(id), t.objectExpression([
|
|
@@ -427,24 +493,24 @@ function collectExportedComponents(code, filename) {
|
|
|
427
493
|
return found;
|
|
428
494
|
}
|
|
429
495
|
export {
|
|
430
|
-
AnalyzerCache,
|
|
496
|
+
AnalyzerCache2 as AnalyzerCache,
|
|
431
497
|
COMPILER_TEMPLATE_SLOT_PROTOCOL,
|
|
432
|
-
analyze,
|
|
433
|
-
analyzeModule,
|
|
434
|
-
analyzeProject,
|
|
435
|
-
autoWrapComponents,
|
|
436
|
-
buildFullScope,
|
|
437
|
-
collectComponentCallGraph,
|
|
438
|
-
collectComponentFunctions,
|
|
498
|
+
analyze2 as analyze,
|
|
499
|
+
analyzeModule2 as analyzeModule,
|
|
500
|
+
analyzeProject2 as analyzeProject,
|
|
501
|
+
autoWrapComponents2 as autoWrapComponents,
|
|
502
|
+
buildFullScope2 as buildFullScope,
|
|
503
|
+
collectComponentCallGraph2 as collectComponentCallGraph,
|
|
504
|
+
collectComponentFunctions2 as collectComponentFunctions,
|
|
439
505
|
collectExportedComponents,
|
|
440
|
-
computeLocalScopes,
|
|
441
|
-
containsReactiveValue,
|
|
442
|
-
getAllPropNames,
|
|
443
|
-
isReactiveValue,
|
|
444
|
-
loadMetadata,
|
|
445
|
-
propagateReactiveProps,
|
|
446
|
-
runAnalyzeCli,
|
|
447
|
-
trackReactiveImports,
|
|
506
|
+
computeLocalScopes2 as computeLocalScopes,
|
|
507
|
+
containsReactiveValue2 as containsReactiveValue,
|
|
508
|
+
getAllPropNames2 as getAllPropNames,
|
|
509
|
+
isReactiveValue2 as isReactiveValue,
|
|
510
|
+
loadMetadata2 as loadMetadata,
|
|
511
|
+
propagateReactiveProps2 as propagateReactiveProps,
|
|
512
|
+
runAnalyzeCli2 as runAnalyzeCli,
|
|
513
|
+
trackReactiveImports2 as trackReactiveImports,
|
|
448
514
|
transformJSX,
|
|
449
|
-
wrapReactiveExpressions
|
|
515
|
+
wrapReactiveExpressions2 as wrapReactiveExpressions
|
|
450
516
|
};
|
package/dist/reactive-wrap.d.ts
CHANGED
|
@@ -59,6 +59,7 @@ export interface ReactiveScope {
|
|
|
59
59
|
bindings: Map<string, Binding>;
|
|
60
60
|
}
|
|
61
61
|
export declare function getAllPropNames(params: t.Node | undefined): Set<string>;
|
|
62
|
+
export declare function resolveLocalObjectLiteral(fn: t.Function, name: string): t.ObjectExpression | null;
|
|
62
63
|
export declare function isReactiveValue(node: t.Node, scope: ReactiveScope): boolean;
|
|
63
64
|
export declare function containsReactiveValue(expr: t.Expression, scope: ReactiveScope): boolean;
|
|
64
65
|
export interface ComponentExpressionInfo {
|
|
@@ -66,6 +67,9 @@ export interface ComponentExpressionInfo {
|
|
|
66
67
|
componentName: string | null;
|
|
67
68
|
attributeName: string | null;
|
|
68
69
|
}
|
|
70
|
+
export declare function getComponentExpressionInfo(exprPath: {
|
|
71
|
+
parentPath?: unknown;
|
|
72
|
+
}): ComponentExpressionInfo;
|
|
69
73
|
export declare function collectComponentFunctions(ast: t.Node, ccNames: Set<string>): {
|
|
70
74
|
functions: Set<t.Function>;
|
|
71
75
|
names: Map<string, t.Function>;
|
package/docs/README.md
CHANGED
|
@@ -1,26 +1,45 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Compiler overview
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`sinwan-compiler` is the shared engine behind the Bun and Vite plugins. It has three jobs: rewrite JSX, decide which props are reactive, and keep that decision cheap in dev.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
If you are building a Sinwan app, start with [Plugins](plugins.md). If you are changing the compiler, start with [Architecture](architecture.md).
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
- **Cross-file analysis**: Determines which component props are reactive across the whole project, allowing the transform to avoid wrapping static props in effects.
|
|
9
|
-
- **Dev cache**: Maintains an incremental, persistent cache for Bun and Vite so dev startup and HMR are fast.
|
|
7
|
+
## Pipeline
|
|
10
8
|
|
|
11
|
-
|
|
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).
|
|
11
|
+
3. **Template hoist** — static native-element trees become `_$createTemplate(...)` calls with slots for dynamic bits (`child`, `attr`, `event`, `ref`).
|
|
12
|
+
4. **Analyze (optional)** — a project-wide pass marks which *exported* component props are actually reactive, so static strings are not wrapped.
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
```text
|
|
15
|
+
source.tsx
|
|
16
|
+
│
|
|
17
|
+
▼
|
|
18
|
+
autoWrapComponents() plain `export function App()` → cc(App)
|
|
19
|
+
│
|
|
20
|
+
▼
|
|
21
|
+
wrapReactiveExpressions() {count.value} → {() => count.value}
|
|
22
|
+
│
|
|
23
|
+
▼
|
|
24
|
+
hoist templates <div class="card">…</div> → _$tmpl_N
|
|
25
|
+
│
|
|
26
|
+
▼
|
|
27
|
+
generated module
|
|
28
|
+
```
|
|
18
29
|
|
|
19
|
-
##
|
|
30
|
+
## When analysis runs
|
|
20
31
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
32
|
+
| Mode | Who runs it | What the transform receives |
|
|
33
|
+
| ---- | ----------- | --------------------------- |
|
|
34
|
+
| Dev | Plugin `AnalyzerCache` | In-memory `analyzeMetadata` after each file update |
|
|
35
|
+
| Production | CLI / `analyze()` | JSON file via `analyze: "./.sinwan/reactive-props.json"` |
|
|
36
|
+
| Off | Nobody | Conservative wrapping (exported props treated as reactive) |
|
|
37
|
+
|
|
38
|
+
## Guides
|
|
39
|
+
|
|
40
|
+
- [Transform](transform.md) — what gets wrapped, auto-`cc`, hoisting, built-in components
|
|
41
|
+
- [Analyzer](analyzer.md) — call graph, spreads, workspaces, cache
|
|
42
|
+
- [CLI](cli.md) — `sinwan analyze`
|
|
43
|
+
- [Plugins](plugins.md) — wiring Bun and Vite
|
|
44
|
+
- [API](api.md) — exported functions and types
|
|
45
|
+
- [Architecture](architecture.md) — data structures and propagation
|
package/docs/analyzer.md
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
|
-
# Cross-file
|
|
1
|
+
# Cross-file analyzer
|
|
2
2
|
|
|
3
|
-
The analyzer
|
|
3
|
+
The analyzer answers: **for each exported component, which props actually change?** The transform uses that map so a static `title="Hello"` does not become an effect.
|
|
4
|
+
|
|
5
|
+
Result shape: `filePath → exportName → Set<propName>`.
|
|
4
6
|
|
|
5
7
|
## How it works
|
|
6
8
|
|
|
7
|
-
1.
|
|
8
|
-
2.
|
|
9
|
-
3.
|
|
10
|
-
4.
|
|
11
|
-
5.
|
|
9
|
+
1. Scan `.tsx` / `.ts` / `.jsx` / `.js` (or the files you pass in).
|
|
10
|
+
2. Auto-wrap the same exported components the transform would wrap.
|
|
11
|
+
3. Track reactive imports and `cc(...)`.
|
|
12
|
+
4. Build a call graph: local `<Child />` and imported `<Child />`.
|
|
13
|
+
5. Propagate reactivity until the sets stop growing.
|
|
12
14
|
|
|
13
|
-
A prop is reactive
|
|
15
|
+
A prop is reactive if **any** caller passes a value that reads a reactive source (including through another component).
|
|
14
16
|
|
|
15
17
|
## Example
|
|
16
18
|
|
|
@@ -23,10 +25,12 @@ export const Child = cc(({ title }) => <h1>{title}</h1>);
|
|
|
23
25
|
import { cc } from "sinwan/component";
|
|
24
26
|
import { signal } from "sinwan/reactivity";
|
|
25
27
|
import { Child } from "./Child";
|
|
26
|
-
const Parent = cc(() => <Child title={signal.value} />);
|
|
27
|
-
```
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
const Parent = cc(() => {
|
|
30
|
+
const title = signal("Hello");
|
|
31
|
+
return <Child title={title.value} />;
|
|
32
|
+
});
|
|
33
|
+
```
|
|
30
34
|
|
|
31
35
|
```json
|
|
32
36
|
{
|
|
@@ -34,64 +38,71 @@ The analyzer produces:
|
|
|
34
38
|
}
|
|
35
39
|
```
|
|
36
40
|
|
|
37
|
-
|
|
41
|
+
If every caller passed `title="Hello"`, `Child` would be `[]` and the transform would not wrap `title` inside `Child`.
|
|
38
42
|
|
|
39
|
-
|
|
43
|
+
Default exports are stored under the name `"default"`.
|
|
40
44
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
// From a workspace file
|
|
45
|
-
workspaces: "../../package.json",
|
|
46
|
-
// From a pnpm workspace
|
|
47
|
-
workspaces: "../../pnpm-workspace.yaml",
|
|
48
|
-
// Explicit package paths or globs
|
|
49
|
-
workspaces: ["../../packages/sinwan-ui", "../../packages/shared"],
|
|
50
|
-
// Or combine both
|
|
51
|
-
workspaces: {
|
|
52
|
-
file: "../../package.json",
|
|
53
|
-
include: ["../../packages/sinwan-ui"],
|
|
54
|
-
},
|
|
55
|
-
});
|
|
56
|
-
```
|
|
45
|
+
## Spreads
|
|
46
|
+
|
|
47
|
+
The analyzer is precise when it can see the object:
|
|
57
48
|
|
|
58
|
-
|
|
49
|
+
```tsx
|
|
50
|
+
<Child {...{ title: "Hello", count: s.value }} />
|
|
51
|
+
// only `count` is reactive
|
|
59
52
|
|
|
53
|
+
const props = { title: "Hello", count: s.value };
|
|
54
|
+
<Child {...props} />
|
|
55
|
+
// same, if `props` is that object literal in the same function
|
|
56
|
+
```
|
|
60
57
|
|
|
58
|
+
String keys work (`{ "title": s.value }`). If the spread is an unknown value (`{...rest}` from parameters), **every known prop of the callee**, including `children`, is marked reactive.
|
|
61
59
|
|
|
62
60
|
## Import resolution
|
|
63
61
|
|
|
64
|
-
|
|
62
|
+
Lookups run in this order:
|
|
63
|
+
|
|
64
|
+
1. `tsconfig.json` `paths` (when a config is passed or `tsconfig.json` exists next to `root`)
|
|
65
|
+
2. `bunfig.toml` aliases (`[install] alias = { … }` or `[install.alias]`)
|
|
66
|
+
3. Workspace packages (see below)
|
|
67
|
+
4. Relative imports (`./Child`, `./ui` → `ui.tsx` or `ui/index.tsx`)
|
|
68
|
+
|
|
69
|
+
You can replace that with `resolve(source, fromFile)`.
|
|
70
|
+
|
|
71
|
+
`AnalyzerCache` also resolves **in-memory** modules first (HMR can update a file before it hits disk), including `./ui` → `ui/index.tsx` already in the cache.
|
|
65
72
|
|
|
66
|
-
|
|
67
|
-
2. `bunfig.toml` aliases (if configured or auto-detected).
|
|
68
|
-
3. Workspace packages (if configured).
|
|
69
|
-
4. Relative imports with the configured extensions.
|
|
73
|
+
## Workspaces
|
|
70
74
|
|
|
71
|
-
|
|
75
|
+
Only packages that the app actually imports are pulled in.
|
|
72
76
|
|
|
73
77
|
```ts
|
|
74
78
|
analyzeProject({
|
|
75
|
-
root: "/
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
+
root: "./apps/web",
|
|
80
|
+
workspaces: "../../package.json", // npm/bun `workspaces`
|
|
81
|
+
// or "../../pnpm-workspace.yaml"
|
|
82
|
+
// or ["../../packages/ui"]
|
|
83
|
+
// or { file: "../../package.json", include: ["../../packages/ui"] }
|
|
79
84
|
});
|
|
80
85
|
```
|
|
81
86
|
|
|
82
|
-
|
|
87
|
+
`@scope/ui/Button` maps to `packages/ui/src/Button.tsx` (or the directory of `package.json` `"source"`).
|
|
88
|
+
|
|
89
|
+
## Cache (`AnalyzerCache`)
|
|
90
|
+
|
|
91
|
+
Dev plugins call `update(file, code)` on each transform. The cache:
|
|
92
|
+
|
|
93
|
+
- Re-analyzes that file
|
|
94
|
+
- Walks importers **and** imported modules
|
|
95
|
+
- Writes `cachePath` when you set one
|
|
96
|
+
|
|
97
|
+
`remove(file)` is for deletes / HMR unlinks. It drops the module and **recomputes both**:
|
|
83
98
|
|
|
84
|
-
|
|
99
|
+
- files that imported it (parents)
|
|
100
|
+
- files it imported (children — their props may become static again)
|
|
85
101
|
|
|
86
|
-
|
|
87
|
-
- `<Child {...props} />` is also resolved precisely if `props` is a local variable initialized with an object literal in the same function:
|
|
88
|
-
```tsx
|
|
89
|
-
const props = { title: "Hello", count: signal.value };
|
|
90
|
-
return <Child {...props} />;
|
|
91
|
-
```
|
|
92
|
-
- Spreads of function parameters or other unknown runtime objects fall back to the conservative rule: all known props of the callee are marked reactive.
|
|
102
|
+
If `cachePath` exists, the constructor restores it. Function identities are start/end offsets. If a file changed so those spans no longer match, that module is skipped (no throw); the next `update` rebuilds it.
|
|
93
103
|
|
|
94
|
-
##
|
|
104
|
+
## Conservative rules
|
|
95
105
|
|
|
96
|
-
- Unknown
|
|
97
|
-
-
|
|
106
|
+
- Unknown spreads are conservative (all callee props).
|
|
107
|
+
- `children` is a normal prop.
|
|
108
|
+
- Without metadata, the transform treats **exported** component props as reactive so a parent in another file cannot silently go stale.
|