sinwan-compiler 0.0.0 → 0.2.0
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/LICENSE +21 -0
- package/README.md +27 -0
- package/dist/analyze.d.ts +103 -0
- package/dist/chunk-ba0546ea.js +1848 -0
- package/dist/cli.d.ts +10 -0
- package/dist/cli.js +8 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +329 -0
- package/dist/reactive-wrap.d.ts +80 -0
- package/dist/transform.d.ts +23 -0
- package/docs/README.md +26 -0
- package/docs/analyzer.md +97 -0
- package/docs/api.md +84 -0
- package/docs/architecture.md +75 -0
- package/docs/cli.md +46 -0
- package/docs/plugins.md +60 -0
- package/docs/transform.md +64 -0
- package/package.json +44 -2
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* SinwanJS CLI — project-wide reactive prop analyzer.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* bunx sinwan-compiler analyze [root] [outFile]
|
|
7
|
+
*
|
|
8
|
+
* Writes `.sinwan/reactive-props.json` by default.
|
|
9
|
+
*/
|
|
10
|
+
export declare function runAnalyzeCli(args?: string[]): void;
|
package/dist/cli.js
ADDED
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SinwanJS Compiler — shared core.
|
|
3
|
+
*
|
|
4
|
+
* Exports the JSX transform, reactive prop analysis, and metadata helpers
|
|
5
|
+
* used by the Bun and Vite plugins.
|
|
6
|
+
*/
|
|
7
|
+
export { transformJSX, type TransformOptions } from "./transform";
|
|
8
|
+
export { analyze, analyzeProject, analyzeModule, loadMetadata, AnalyzerCache, type AnalyzeOptions, type ModuleAnalysis, type ProjectAnalysis, type ImportedCallSite, type ImportInfo, type WorkspacesConfig, } from "./analyze";
|
|
9
|
+
export { runAnalyzeCli } from "./cli";
|
|
10
|
+
export { wrapReactiveExpressions, trackReactiveImports, collectComponentFunctions, computeLocalScopes, collectComponentCallGraph, propagateReactiveProps, buildFullScope, containsReactiveValue, getAllPropNames, isReactiveValue, type ReactiveScope, type Binding, type CallSite, type ImportNames, type ComponentExpressionInfo, } from "./reactive-wrap";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
AnalyzerCache,
|
|
4
|
+
analyze,
|
|
5
|
+
analyzeModule,
|
|
6
|
+
analyzeProject,
|
|
7
|
+
buildFullScope,
|
|
8
|
+
collectComponentCallGraph,
|
|
9
|
+
collectComponentFunctions,
|
|
10
|
+
computeLocalScopes,
|
|
11
|
+
containsReactiveValue,
|
|
12
|
+
getAllPropNames,
|
|
13
|
+
isReactiveValue,
|
|
14
|
+
loadMetadata,
|
|
15
|
+
propagateReactiveProps,
|
|
16
|
+
runAnalyzeCli,
|
|
17
|
+
trackReactiveImports,
|
|
18
|
+
wrapReactiveExpressions
|
|
19
|
+
} from "./chunk-ba0546ea.js";
|
|
20
|
+
|
|
21
|
+
// src/transform.ts
|
|
22
|
+
import { parse } from "@babel/parser";
|
|
23
|
+
import _generate from "@babel/generator";
|
|
24
|
+
import * as t from "@babel/types";
|
|
25
|
+
import _traverse from "@babel/traverse";
|
|
26
|
+
var generate = typeof _generate === "function" ? _generate : _generate.default ?? _generate;
|
|
27
|
+
var traverse = typeof _traverse === "function" ? _traverse : _traverse.default ?? _traverse;
|
|
28
|
+
var SLOT_PREFIX = "s";
|
|
29
|
+
var slotId = 0;
|
|
30
|
+
function nextSlotId() {
|
|
31
|
+
return `<!--${SLOT_PREFIX}:${slotId++}-->`;
|
|
32
|
+
}
|
|
33
|
+
var VOID_ELEMENTS = new Set([
|
|
34
|
+
"area",
|
|
35
|
+
"base",
|
|
36
|
+
"br",
|
|
37
|
+
"col",
|
|
38
|
+
"embed",
|
|
39
|
+
"hr",
|
|
40
|
+
"img",
|
|
41
|
+
"input",
|
|
42
|
+
"link",
|
|
43
|
+
"meta",
|
|
44
|
+
"param",
|
|
45
|
+
"source",
|
|
46
|
+
"track",
|
|
47
|
+
"wbr"
|
|
48
|
+
]);
|
|
49
|
+
function escapeHtml(str) {
|
|
50
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
51
|
+
}
|
|
52
|
+
function cleanJSXText(value) {
|
|
53
|
+
const lines = value.split(/\r\n|\n|\r/);
|
|
54
|
+
let lastNonEmptyLine = 0;
|
|
55
|
+
for (let i = 0;i < lines.length; i++) {
|
|
56
|
+
const line = lines[i] ?? "";
|
|
57
|
+
if (/[^ \t]/.exec(line)) {
|
|
58
|
+
lastNonEmptyLine = i;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
let str = "";
|
|
62
|
+
for (let i = 0;i < lines.length; i++) {
|
|
63
|
+
const line = lines[i] ?? "";
|
|
64
|
+
const isFirstLine = i === 0;
|
|
65
|
+
const isLastLine = i === lines.length - 1;
|
|
66
|
+
const isLastNonEmptyLine = i === lastNonEmptyLine;
|
|
67
|
+
let trimmedLine = line.replace(/\t/g, " ");
|
|
68
|
+
if (!isFirstLine) {
|
|
69
|
+
trimmedLine = trimmedLine.replace(/^ +/, "");
|
|
70
|
+
}
|
|
71
|
+
if (!isLastLine) {
|
|
72
|
+
trimmedLine = trimmedLine.replace(/ +$/, "");
|
|
73
|
+
}
|
|
74
|
+
if (trimmedLine) {
|
|
75
|
+
if (!isLastNonEmptyLine) {
|
|
76
|
+
trimmedLine += " ";
|
|
77
|
+
}
|
|
78
|
+
str += trimmedLine;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return str;
|
|
82
|
+
}
|
|
83
|
+
function extractTemplate(node, filename) {
|
|
84
|
+
slotId = 0;
|
|
85
|
+
const slots = [];
|
|
86
|
+
const html = elementToHtml(node, slots, [], filename);
|
|
87
|
+
return { html, slots };
|
|
88
|
+
}
|
|
89
|
+
function warnSuspectedStyleInterpolation(attrValue, filename, line) {
|
|
90
|
+
if (!attrValue.includes("${"))
|
|
91
|
+
return;
|
|
92
|
+
const location = line ? `:${line}` : "";
|
|
93
|
+
console.warn('[Sinwan] JSX string literal style attribute contains "${...}" which will not be interpolated. ' + "Use style={`...${...}`} instead. (" + filename + location + ")");
|
|
94
|
+
}
|
|
95
|
+
function isLiteralStyleValue(expr) {
|
|
96
|
+
return t.isStringLiteral(expr) || t.isNumericLiteral(expr) || t.isBooleanLiteral(expr) || t.isNullLiteral(expr);
|
|
97
|
+
}
|
|
98
|
+
function isStaticStyleValue(expr) {
|
|
99
|
+
if (t.isStringLiteral(expr))
|
|
100
|
+
return true;
|
|
101
|
+
if (t.isTemplateLiteral(expr) && expr.expressions.length === 0)
|
|
102
|
+
return true;
|
|
103
|
+
if (!t.isObjectExpression(expr))
|
|
104
|
+
return false;
|
|
105
|
+
for (const prop of expr.properties) {
|
|
106
|
+
if (!t.isObjectProperty(prop))
|
|
107
|
+
return false;
|
|
108
|
+
if (prop.computed)
|
|
109
|
+
return false;
|
|
110
|
+
if (!isLiteralStyleValue(prop.value))
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
function toKebabCase(value) {
|
|
116
|
+
return value.includes("-") ? value : value.replace(/[A-Z]/g, (char) => `-${char.toLowerCase()}`);
|
|
117
|
+
}
|
|
118
|
+
function serializeStyleValue(expr) {
|
|
119
|
+
if (t.isStringLiteral(expr))
|
|
120
|
+
return expr.value;
|
|
121
|
+
if (t.isTemplateLiteral(expr) && expr.expressions.length === 0) {
|
|
122
|
+
return expr.quasis[0]?.value?.raw ?? "";
|
|
123
|
+
}
|
|
124
|
+
if (!t.isObjectExpression(expr))
|
|
125
|
+
return "";
|
|
126
|
+
const parts = [];
|
|
127
|
+
for (const prop of expr.properties) {
|
|
128
|
+
if (!t.isObjectProperty(prop))
|
|
129
|
+
continue;
|
|
130
|
+
const key = prop.key;
|
|
131
|
+
let name = null;
|
|
132
|
+
if (t.isIdentifier(key))
|
|
133
|
+
name = key.name;
|
|
134
|
+
else if (t.isStringLiteral(key))
|
|
135
|
+
name = key.value;
|
|
136
|
+
if (!name)
|
|
137
|
+
continue;
|
|
138
|
+
const val = prop.value;
|
|
139
|
+
if (t.isNullLiteral(val))
|
|
140
|
+
continue;
|
|
141
|
+
if (t.isBooleanLiteral(val) && !val.value)
|
|
142
|
+
continue;
|
|
143
|
+
let valueStr = "";
|
|
144
|
+
if (t.isStringLiteral(val))
|
|
145
|
+
valueStr = val.value;
|
|
146
|
+
else if (t.isNumericLiteral(val))
|
|
147
|
+
valueStr = String(val.value);
|
|
148
|
+
else if (t.isBooleanLiteral(val))
|
|
149
|
+
valueStr = String(val.value);
|
|
150
|
+
if (valueStr === "")
|
|
151
|
+
continue;
|
|
152
|
+
parts.push(`${toKebabCase(name)}:${valueStr}`);
|
|
153
|
+
}
|
|
154
|
+
return parts.join(";");
|
|
155
|
+
}
|
|
156
|
+
function elementToHtml(node, slots, path, filename) {
|
|
157
|
+
const tagName = jsxNameToString(node.openingElement.name);
|
|
158
|
+
if (tagName === "")
|
|
159
|
+
return "";
|
|
160
|
+
const firstChar = tagName.charAt(0);
|
|
161
|
+
if (firstChar && firstChar === firstChar.toUpperCase()) {
|
|
162
|
+
throw new Error("Cannot hoist element containing component calls");
|
|
163
|
+
}
|
|
164
|
+
const isVoid = VOID_ELEMENTS.has(tagName);
|
|
165
|
+
let html = `<${tagName}`;
|
|
166
|
+
for (const attr of node.openingElement.attributes) {
|
|
167
|
+
if (attr.type === "JSXSpreadAttribute") {
|
|
168
|
+
throw new Error("Cannot hoist element with spread attributes");
|
|
169
|
+
}
|
|
170
|
+
const attrName = attr.name.name;
|
|
171
|
+
if (attrName === "children")
|
|
172
|
+
continue;
|
|
173
|
+
if (attrName === "ref") {
|
|
174
|
+
throw new Error("Cannot hoist element with ref");
|
|
175
|
+
}
|
|
176
|
+
if (attr.value?.type === "JSXExpressionContainer") {
|
|
177
|
+
const expr = attr.value.expression;
|
|
178
|
+
if (expr.type === "JSXEmptyExpression")
|
|
179
|
+
continue;
|
|
180
|
+
if (attrName === "style" && t.isExpression(expr) && isStaticStyleValue(expr)) {
|
|
181
|
+
const serialized = serializeStyleValue(expr);
|
|
182
|
+
html += ` ${attrName}="${escapeHtml(serialized)}"`;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
slots.push({
|
|
186
|
+
path: [...path],
|
|
187
|
+
type: attrName.startsWith("on") ? "event" : "attr",
|
|
188
|
+
name: attrName,
|
|
189
|
+
expr
|
|
190
|
+
});
|
|
191
|
+
html += ` ${attrName}=""`;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (attr.value?.type === "JSXText" || attr.value?.type === "StringLiteral") {
|
|
195
|
+
const rawValue = attr.value.value;
|
|
196
|
+
if (attrName === "style") {
|
|
197
|
+
warnSuspectedStyleInterpolation(rawValue, filename, attr.value.loc?.start?.line);
|
|
198
|
+
}
|
|
199
|
+
html += ` ${attrName}="${escapeHtml(rawValue)}"`;
|
|
200
|
+
} else if (!attr.value) {
|
|
201
|
+
html += ` ${attrName}`;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
html += isVoid ? " />" : ">";
|
|
205
|
+
if (isVoid)
|
|
206
|
+
return html;
|
|
207
|
+
let childIndex = 0;
|
|
208
|
+
for (const child of node.children) {
|
|
209
|
+
if (child.type === "JSXText") {
|
|
210
|
+
const text = cleanJSXText(child.value ?? "");
|
|
211
|
+
if (text) {
|
|
212
|
+
html += escapeHtml(text);
|
|
213
|
+
childIndex++;
|
|
214
|
+
}
|
|
215
|
+
} else if (child.type === "JSXExpressionContainer") {
|
|
216
|
+
if (child.expression.type === "JSXEmptyExpression")
|
|
217
|
+
continue;
|
|
218
|
+
const slot = nextSlotId();
|
|
219
|
+
slots.push({
|
|
220
|
+
path: [...path, childIndex],
|
|
221
|
+
type: "child",
|
|
222
|
+
expr: child.expression
|
|
223
|
+
});
|
|
224
|
+
html += slot;
|
|
225
|
+
childIndex++;
|
|
226
|
+
} else if (child.type === "JSXElement") {
|
|
227
|
+
html += elementToHtml(child, slots, [...path, childIndex], filename);
|
|
228
|
+
childIndex++;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
html += `</${tagName}>`;
|
|
232
|
+
return html;
|
|
233
|
+
}
|
|
234
|
+
function jsxNameToString(name) {
|
|
235
|
+
if (name.type === "JSXIdentifier")
|
|
236
|
+
return name.name;
|
|
237
|
+
if (name.type === "JSXMemberExpression") {
|
|
238
|
+
return jsxNameToString(name.object) + "." + jsxNameToString(name.property);
|
|
239
|
+
}
|
|
240
|
+
return "";
|
|
241
|
+
}
|
|
242
|
+
function transformJSX(code, filename, options = {}) {
|
|
243
|
+
const ast = parse(code, {
|
|
244
|
+
sourceType: "module",
|
|
245
|
+
plugins: ["jsx", "typescript"],
|
|
246
|
+
sourceFilename: filename
|
|
247
|
+
});
|
|
248
|
+
wrapReactiveExpressions(ast, {
|
|
249
|
+
explicitBindings: options.explicitBindings,
|
|
250
|
+
analyze: options.analyze,
|
|
251
|
+
analyzeMetadata: options.analyzeMetadata,
|
|
252
|
+
filename
|
|
253
|
+
});
|
|
254
|
+
const hoist = options.hoist !== false;
|
|
255
|
+
const templates = [];
|
|
256
|
+
let templateCounter = 0;
|
|
257
|
+
traverse(ast, {
|
|
258
|
+
JSXElement(path) {
|
|
259
|
+
if (!hoist)
|
|
260
|
+
return;
|
|
261
|
+
if (path.findParent((p) => p.isJSXElement()))
|
|
262
|
+
return;
|
|
263
|
+
const opening = path.node.openingElement;
|
|
264
|
+
const tagName = opening.name;
|
|
265
|
+
if (tagName.type !== "JSXIdentifier")
|
|
266
|
+
return;
|
|
267
|
+
const name = tagName.name;
|
|
268
|
+
if (!name || name[0] !== name[0].toLowerCase())
|
|
269
|
+
return;
|
|
270
|
+
try {
|
|
271
|
+
const extracted = extractTemplate(path.node, filename);
|
|
272
|
+
const tmplId = `_$tmpl_${templateCounter++}`;
|
|
273
|
+
templates.push({ id: tmplId, template: extracted });
|
|
274
|
+
const dynamicExprs = extracted.slots.filter((s) => s.expr).map((s) => s.expr);
|
|
275
|
+
const templateExpr = t.callExpression(t.identifier("_$createTemplate"), [t.identifier(tmplId), t.arrayExpression(dynamicExprs)]);
|
|
276
|
+
const parentIsJSX = path.parentPath?.isJSXElement() || path.parentPath?.isJSXFragment();
|
|
277
|
+
path.replaceWith(parentIsJSX ? t.jsxExpressionContainer(templateExpr) : templateExpr);
|
|
278
|
+
} catch {}
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
if (templates.length === 0) {
|
|
282
|
+
const result2 = generate(ast, {
|
|
283
|
+
sourceMaps: true,
|
|
284
|
+
sourceFileName: filename
|
|
285
|
+
});
|
|
286
|
+
return { code: result2.code, map: result2.map };
|
|
287
|
+
}
|
|
288
|
+
const templateDecls = templates.map(({ id, template }) => t.variableDeclaration("const", [
|
|
289
|
+
t.variableDeclarator(t.identifier(id), t.objectExpression([
|
|
290
|
+
t.objectProperty(t.identifier("html"), t.stringLiteral(template.html)),
|
|
291
|
+
t.objectProperty(t.identifier("slots"), t.arrayExpression(template.slots.map((slot) => t.objectExpression([
|
|
292
|
+
t.objectProperty(t.identifier("path"), t.arrayExpression(slot.path.map((i) => t.numericLiteral(i)))),
|
|
293
|
+
t.objectProperty(t.identifier("type"), t.stringLiteral(slot.type)),
|
|
294
|
+
...slot.name ? [
|
|
295
|
+
t.objectProperty(t.identifier("name"), t.stringLiteral(slot.name))
|
|
296
|
+
] : []
|
|
297
|
+
]))))
|
|
298
|
+
]))
|
|
299
|
+
]));
|
|
300
|
+
const importSpecifiers = [
|
|
301
|
+
t.importSpecifier(t.identifier("_$createTemplate"), t.identifier("_$createTemplate"))
|
|
302
|
+
];
|
|
303
|
+
if (options.explicitBindings) {
|
|
304
|
+
importSpecifiers.push(t.importSpecifier(t.identifier("_$bindText"), t.identifier("_$bindText")), t.importSpecifier(t.identifier("_$bindAttr"), t.identifier("_$bindAttr")), t.importSpecifier(t.identifier("_$bindStyle"), t.identifier("_$bindStyle")), t.importSpecifier(t.identifier("_$bindClass"), t.identifier("_$bindClass")));
|
|
305
|
+
}
|
|
306
|
+
const importDecl = t.importDeclaration(importSpecifiers, t.stringLiteral("sinwan/renderer"));
|
|
307
|
+
ast.program.body.unshift(importDecl, ...templateDecls);
|
|
308
|
+
const result = generate(ast, { sourceMaps: true, sourceFileName: filename });
|
|
309
|
+
return { code: result.code, map: result.map };
|
|
310
|
+
}
|
|
311
|
+
export {
|
|
312
|
+
wrapReactiveExpressions,
|
|
313
|
+
transformJSX,
|
|
314
|
+
trackReactiveImports,
|
|
315
|
+
runAnalyzeCli,
|
|
316
|
+
propagateReactiveProps,
|
|
317
|
+
loadMetadata,
|
|
318
|
+
isReactiveValue,
|
|
319
|
+
getAllPropNames,
|
|
320
|
+
containsReactiveValue,
|
|
321
|
+
computeLocalScopes,
|
|
322
|
+
collectComponentFunctions,
|
|
323
|
+
collectComponentCallGraph,
|
|
324
|
+
buildFullScope,
|
|
325
|
+
analyzeProject,
|
|
326
|
+
analyzeModule,
|
|
327
|
+
analyze,
|
|
328
|
+
AnalyzerCache
|
|
329
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SinwanJS Compiler — Reactive JSX Expression Wrapping
|
|
3
|
+
*
|
|
4
|
+
* Wraps JSX expressions that read reactive values in zero-arity arrow
|
|
5
|
+
* functions so the runtime renderer can create an effect and update the DOM
|
|
6
|
+
* when the value changes.
|
|
7
|
+
*
|
|
8
|
+
* Supported reactive sources:
|
|
9
|
+
* - `createMutable` / `createStore` from `sinwan/store`
|
|
10
|
+
* - `signal` / `computed` from `sinwan/reactivity`
|
|
11
|
+
* - `useState` from `sinwan/react-client`
|
|
12
|
+
*
|
|
13
|
+
* Example:
|
|
14
|
+
* <p>{state.name}</p> → <p>{() => state.name}</p>
|
|
15
|
+
* <p>{count.value}</p> → <p>{() => count.value}</p>
|
|
16
|
+
* <p>{getCount()}</p> → <p>{() => getCount()}</p>
|
|
17
|
+
*/
|
|
18
|
+
import * as t from "@babel/types";
|
|
19
|
+
export interface ImportNames {
|
|
20
|
+
createMutable: Set<string>;
|
|
21
|
+
createStore: Set<string>;
|
|
22
|
+
signal: Set<string>;
|
|
23
|
+
computed: Set<string>;
|
|
24
|
+
useState: Set<string>;
|
|
25
|
+
cc: Set<string>;
|
|
26
|
+
}
|
|
27
|
+
export declare function trackReactiveImports(ast: t.Node): ImportNames;
|
|
28
|
+
export interface MutableBinding {
|
|
29
|
+
kind: "mutable";
|
|
30
|
+
root: string;
|
|
31
|
+
path: string[];
|
|
32
|
+
}
|
|
33
|
+
export interface SignalBinding {
|
|
34
|
+
kind: "signal";
|
|
35
|
+
}
|
|
36
|
+
export interface ComputedBinding {
|
|
37
|
+
kind: "computed";
|
|
38
|
+
}
|
|
39
|
+
export interface GetterBinding {
|
|
40
|
+
kind: "getter";
|
|
41
|
+
}
|
|
42
|
+
export interface PropBinding {
|
|
43
|
+
kind: "prop";
|
|
44
|
+
}
|
|
45
|
+
export type Binding = MutableBinding | SignalBinding | ComputedBinding | GetterBinding | PropBinding;
|
|
46
|
+
export interface ReactiveScope {
|
|
47
|
+
bindings: Map<string, Binding>;
|
|
48
|
+
}
|
|
49
|
+
export declare function getAllPropNames(params: t.Node | undefined): Set<string>;
|
|
50
|
+
export declare function isReactiveValue(node: t.Identifier | t.MemberExpression, scope: ReactiveScope): boolean;
|
|
51
|
+
export declare function containsReactiveValue(expr: t.Expression, scope: ReactiveScope): boolean;
|
|
52
|
+
export interface ComponentExpressionInfo {
|
|
53
|
+
isComponent: boolean;
|
|
54
|
+
componentName: string | null;
|
|
55
|
+
attributeName: string | null;
|
|
56
|
+
}
|
|
57
|
+
export declare function collectComponentFunctions(ast: t.Node, ccNames: Set<string>): {
|
|
58
|
+
functions: Set<t.Function>;
|
|
59
|
+
names: Map<string, t.Function>;
|
|
60
|
+
exported: Set<t.Function>;
|
|
61
|
+
};
|
|
62
|
+
export declare function computeLocalScopes(ast: t.Node, names: ImportNames): Map<t.Function, ReactiveScope>;
|
|
63
|
+
export type CallSite = {
|
|
64
|
+
callee: t.Function;
|
|
65
|
+
props: {
|
|
66
|
+
name: string;
|
|
67
|
+
value: t.Expression;
|
|
68
|
+
}[];
|
|
69
|
+
/** Spread expressions forwarded to the child. */
|
|
70
|
+
spreads: t.Expression[];
|
|
71
|
+
};
|
|
72
|
+
export declare function collectComponentCallGraph(ast: t.Node, componentNames: Map<string, t.Function>): Map<t.Function, CallSite[]>;
|
|
73
|
+
export declare function buildFullScope(localScopes: Map<t.Function, ReactiveScope>, fn: t.Function, reactiveProps: Set<string>): ReactiveScope | null;
|
|
74
|
+
export declare function propagateReactiveProps(localScopes: Map<t.Function, ReactiveScope>, callGraph: Map<t.Function, CallSite[]>, componentFunctions: Set<t.Function>): Map<t.Function, Set<string>>;
|
|
75
|
+
export declare function wrapReactiveExpressions(ast: t.Node, options?: {
|
|
76
|
+
explicitBindings?: boolean;
|
|
77
|
+
analyze?: string;
|
|
78
|
+
analyzeMetadata?: Map<string, Map<string, Set<string>>>;
|
|
79
|
+
filename?: string;
|
|
80
|
+
}): void;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SinwanJS Compiler — JSX Transform
|
|
3
|
+
*
|
|
4
|
+
* Transforms JSX AST to use template hoisting.
|
|
5
|
+
* Static JSX elements are extracted to module-level template strings
|
|
6
|
+
* and replaced with optimized template creation calls.
|
|
7
|
+
*/
|
|
8
|
+
export interface TransformOptions {
|
|
9
|
+
/** Enable template hoisting (default: true) */
|
|
10
|
+
hoist?: boolean;
|
|
11
|
+
/** Enable dev mode (source maps, etc.) */
|
|
12
|
+
dev?: boolean;
|
|
13
|
+
/** Emit explicit compiler-driven binding descriptors (Phase 2). */
|
|
14
|
+
explicitBindings?: boolean;
|
|
15
|
+
/** Path to the reactive-props metadata file produced by `sinwan analyze`. */
|
|
16
|
+
analyze?: string;
|
|
17
|
+
/** In-memory reactive-props metadata (used by plugin-level caches). */
|
|
18
|
+
analyzeMetadata?: Map<string, Map<string, Set<string>>>;
|
|
19
|
+
}
|
|
20
|
+
export declare function transformJSX(code: string, filename: string, options?: TransformOptions): {
|
|
21
|
+
code: string;
|
|
22
|
+
map?: any;
|
|
23
|
+
};
|
package/docs/README.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# sinwan-compiler
|
|
2
|
+
|
|
3
|
+
The shared compiler package for SinwanJS. It provides the JSX transform, the project-wide reactive-prop analyzer, and the dev/HMR cache used by both the Bun and Vite plugins.
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
7
|
+
- **JSX transform**: Converts reactive JSX expressions into lazy zero-arity functions so the Sinwan runtime can update the DOM efficiently.
|
|
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.
|
|
10
|
+
|
|
11
|
+
## Packages
|
|
12
|
+
|
|
13
|
+
- `src/transform.ts` — JSX transform and reactive expression wrapping.
|
|
14
|
+
- `src/analyze.ts` — `analyze`, `analyzeProject`, `AnalyzerCache`, and import resolution.
|
|
15
|
+
- `src/reactive-wrap.ts` — Helpers for detecting reactive sources, collecting component call graphs, and propagating reactivity.
|
|
16
|
+
- `src/cli.ts` — `sinwan-compiler analyze` CLI.
|
|
17
|
+
- `src/index.ts` — Public package exports.
|
|
18
|
+
|
|
19
|
+
## Documentation
|
|
20
|
+
|
|
21
|
+
- [`analyzer.md`](analyzer.md) — Cross-file reactive-prop analyzer and cache.
|
|
22
|
+
- [`transform.md`](transform.md) — JSX transform and wrapping rules.
|
|
23
|
+
- [`cli.md`](cli.md) — Command-line usage.
|
|
24
|
+
- [`plugins.md`](plugins.md) — Using the compiler with Bun and Vite.
|
|
25
|
+
- [`api.md`](api.md) — Public API reference.
|
|
26
|
+
- [`architecture.md`](architecture.md) — Internal design and data flow.
|
package/docs/analyzer.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Cross-file reactive-prop analyzer
|
|
2
|
+
|
|
3
|
+
The analyzer determines which props of an exported component are reactive. It runs over the whole project (or a cached subset) and produces a map of `filePath → exportName → Set<propName>`.
|
|
4
|
+
|
|
5
|
+
## How it works
|
|
6
|
+
|
|
7
|
+
1. **Parse** every `.tsx/.ts/.jsx/.js` file into a Babel AST.
|
|
8
|
+
2. **Track imports** of reactive sources (`createMutable`, `createStore`, `signal`, `computed`, `useState`) and the component factory `cc`.
|
|
9
|
+
3. **Collect component functions** defined with `cc(...)`.
|
|
10
|
+
4. **Build a call graph** of local and imported JSX calls (`<Child ... />`).
|
|
11
|
+
5. **Propagate reactivity** bottom-up through the call graph using a fixed-point algorithm.
|
|
12
|
+
|
|
13
|
+
A prop is reactive for a given component if any of its callers passes a value that reads a reactive source.
|
|
14
|
+
|
|
15
|
+
## Example
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
// Child.tsx
|
|
19
|
+
import { cc } from "sinwan/component";
|
|
20
|
+
export const Child = cc(({ title }) => <h1>{title}</h1>);
|
|
21
|
+
|
|
22
|
+
// Parent.tsx
|
|
23
|
+
import { cc } from "sinwan/component";
|
|
24
|
+
import { signal } from "sinwan/reactivity";
|
|
25
|
+
import { Child } from "./Child";
|
|
26
|
+
const Parent = cc(() => <Child title={signal.value} />);
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The analyzer produces:
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"/project/Child.tsx": { "Child": ["title"] }
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Workspace packages
|
|
38
|
+
|
|
39
|
+
Cross-file analysis works for local monorepo packages. The analyzer discovers workspace packages from a `package.json` `workspaces` field, a `pnpm-workspace.yaml`, or explicit package paths. Only packages that are actually imported by the project are analyzed.
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
analyzeProject({
|
|
43
|
+
root: "./apps/web",
|
|
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
|
+
```
|
|
57
|
+
|
|
58
|
+
Package imports are resolved to files inside the declared package, typically under the package `src` directory (or the directory of the `source` field in `package.json`). Subpath imports such as `@sinwan/ui/Button` map to `packages/sinwan-ui/src/Button.tsx`.
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
## Import resolution
|
|
63
|
+
|
|
64
|
+
The analyzer resolves imports in this order:
|
|
65
|
+
|
|
66
|
+
1. `tsconfig.json` `paths` (if configured).
|
|
67
|
+
2. `bunfig.toml` aliases (if configured or auto-detected).
|
|
68
|
+
3. Workspace packages (if configured).
|
|
69
|
+
4. Relative imports with the configured extensions.
|
|
70
|
+
|
|
71
|
+
You can also provide a custom resolver:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
analyzeProject({
|
|
75
|
+
root: "/project",
|
|
76
|
+
resolve: (source, fromFile) => {
|
|
77
|
+
// return absolute path or null
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Spread props
|
|
83
|
+
|
|
84
|
+
Spread props are handled precisely when possible:
|
|
85
|
+
|
|
86
|
+
- `<Child {...{ title: "Hello", count: signal.value }} />` is treated as two named props; only `count` becomes reactive.
|
|
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.
|
|
93
|
+
|
|
94
|
+
## Limitations
|
|
95
|
+
|
|
96
|
+
- Unknown runtime objects (`{...props}` where `props` is not a local object literal) are conservatively treated as reactive.
|
|
97
|
+
- Named slot props and `children` are regular props; they are reactive only when the passed value is reactive.
|
package/docs/api.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# API reference
|
|
2
|
+
|
|
3
|
+
## `transformJSX(code, filename, options?)`
|
|
4
|
+
|
|
5
|
+
Transforms a single JSX/TSX file.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { transformJSX } from "sinwan-compiler";
|
|
9
|
+
|
|
10
|
+
const result = transformJSX(code, filename, {
|
|
11
|
+
hoist: true,
|
|
12
|
+
explicitBindings: false,
|
|
13
|
+
analyze: "./.sinwan/reactive-props.json",
|
|
14
|
+
analyzeMetadata: new Map(),
|
|
15
|
+
});
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Returns `{ code: string; map?: any }`.
|
|
19
|
+
|
|
20
|
+
## `analyze(options)`
|
|
21
|
+
|
|
22
|
+
Production entry point. Analyzes the project and writes metadata to disk.
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { analyze } from "sinwan-compiler";
|
|
26
|
+
|
|
27
|
+
analyze({
|
|
28
|
+
root: "./src",
|
|
29
|
+
outFile: "./.sinwan/reactive-props.json",
|
|
30
|
+
tsConfigPath: "./tsconfig.json",
|
|
31
|
+
bunfigPath: "./bunfig.toml",
|
|
32
|
+
workspaces: "../../package.json",
|
|
33
|
+
});
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## `analyzeProject(options)`
|
|
37
|
+
|
|
38
|
+
Programmatic analyzer. Returns the full project analysis without writing to disk.
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import { analyzeProject } from "sinwan-compiler";
|
|
42
|
+
|
|
43
|
+
const project = analyzeProject({
|
|
44
|
+
root: "./src",
|
|
45
|
+
files: { "/project/A.tsx": "..." },
|
|
46
|
+
tsConfigPath: "./tsconfig.json",
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
project.reactiveProps.get("/project/A.tsx")?.get("Component");
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## `AnalyzerCache`
|
|
53
|
+
|
|
54
|
+
Incremental analyzer for dev/HMR.
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import { AnalyzerCache } from "sinwan-compiler";
|
|
58
|
+
|
|
59
|
+
const cache = new AnalyzerCache({
|
|
60
|
+
root: "./src",
|
|
61
|
+
tsConfigPath: "./tsconfig.json",
|
|
62
|
+
bunfigPath: "./bunfig.toml",
|
|
63
|
+
workspaces: "../../package.json",
|
|
64
|
+
cachePath: "./.sinwan/cache.json",
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
cache.update(filePath, newCode);
|
|
68
|
+
const metadata = cache.reactiveProps;
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Methods
|
|
72
|
+
|
|
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).
|
|
77
|
+
|
|
78
|
+
## Types
|
|
79
|
+
|
|
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.
|