sinwan-compiler 0.2.2 → 0.2.4
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/auto-cc.d.ts +34 -0
- package/dist/{chunk-3bqqn28f.js → chunk-w580fdrf.js} +599 -85
- package/dist/cli.js +1 -1
- package/dist/exports.d.ts +27 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +154 -33
- package/dist/reactive-wrap.d.ts +16 -3
- package/dist/transform.d.ts +22 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SinwanJS Compiler — Exported component detection.
|
|
3
|
+
*
|
|
4
|
+
* Shared utility for detecting top-level exported component bindings in a
|
|
5
|
+
* module. Used by the Vite and Bun plugins for HMR boundary injection.
|
|
6
|
+
*
|
|
7
|
+
* Detection is intentionally conservative: only top-level exports whose name
|
|
8
|
+
* starts with an uppercase letter (the component naming convention) are
|
|
9
|
+
* returned. The runtime safely ignores anything that is not used as a
|
|
10
|
+
* component, so over-matching is harmless.
|
|
11
|
+
*/
|
|
12
|
+
export interface ExportedComponent {
|
|
13
|
+
/** Local binding name in the module (referenced in the accept callback). */
|
|
14
|
+
local: string;
|
|
15
|
+
/** Export key on the module namespace ("default" for the default export). */
|
|
16
|
+
key: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Collect top-level exported component bindings from a module source string.
|
|
20
|
+
*
|
|
21
|
+
* Parses the source with Babel, then walks top-level export declarations to
|
|
22
|
+
* find functions and variables whose name starts with an uppercase letter.
|
|
23
|
+
*
|
|
24
|
+
* Returns an array of `{ local, key }` pairs. Returns an empty array if the
|
|
25
|
+
* source cannot be parsed.
|
|
26
|
+
*/
|
|
27
|
+
export declare function collectExportedComponents(code: string, filename: string): ExportedComponent[];
|
package/dist/index.d.ts
CHANGED
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
* used by the Bun and Vite plugins.
|
|
6
6
|
*/
|
|
7
7
|
export { transformJSX, type TransformOptions } from "./transform";
|
|
8
|
+
export { COMPILER_TEMPLATE_SLOT_PROTOCOL } from "./transform";
|
|
9
|
+
export { autoWrapComponents } from "./auto-cc";
|
|
8
10
|
export { analyze, analyzeProject, analyzeModule, loadMetadata, AnalyzerCache, type AnalyzeOptions, type ModuleAnalysis, type ProjectAnalysis, type ImportedCallSite, type ImportInfo, type WorkspacesConfig, } from "./analyze";
|
|
11
|
+
export { collectExportedComponents, type ExportedComponent } from "./exports";
|
|
9
12
|
export { runAnalyzeCli } from "./cli";
|
|
10
13
|
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
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
analyze,
|
|
5
5
|
analyzeModule,
|
|
6
6
|
analyzeProject,
|
|
7
|
+
autoWrapComponents,
|
|
7
8
|
buildFullScope,
|
|
8
9
|
collectComponentCallGraph,
|
|
9
10
|
collectComponentFunctions,
|
|
@@ -16,7 +17,7 @@ import {
|
|
|
16
17
|
runAnalyzeCli,
|
|
17
18
|
trackReactiveImports,
|
|
18
19
|
wrapReactiveExpressions
|
|
19
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-w580fdrf.js";
|
|
20
21
|
|
|
21
22
|
// src/transform.ts
|
|
22
23
|
import { parse } from "@babel/parser";
|
|
@@ -25,10 +26,21 @@ import * as t from "@babel/types";
|
|
|
25
26
|
import _traverse from "@babel/traverse";
|
|
26
27
|
var generate = typeof _generate === "function" ? _generate : _generate.default ?? _generate;
|
|
27
28
|
var traverse = typeof _traverse === "function" ? _traverse : _traverse.default ?? _traverse;
|
|
28
|
-
var
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
var COMPILER_TEMPLATE_SLOT_PROTOCOL = {
|
|
30
|
+
slotPrefix: "s",
|
|
31
|
+
encodeSlot(index) {
|
|
32
|
+
return `s:${index}`;
|
|
33
|
+
},
|
|
34
|
+
decodeSlot(data) {
|
|
35
|
+
if (data.startsWith("s:")) {
|
|
36
|
+
const idx = parseInt(data.slice(2), 10);
|
|
37
|
+
return Number.isNaN(idx) ? null : idx;
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
function nextSlotId(counter) {
|
|
43
|
+
return `<!--${COMPILER_TEMPLATE_SLOT_PROTOCOL.encodeSlot(counter.value++)}-->`;
|
|
32
44
|
}
|
|
33
45
|
var VOID_ELEMENTS = new Set([
|
|
34
46
|
"area",
|
|
@@ -81,9 +93,9 @@ function cleanJSXText(value) {
|
|
|
81
93
|
return str;
|
|
82
94
|
}
|
|
83
95
|
function extractTemplate(node, filename) {
|
|
84
|
-
|
|
96
|
+
const counter = { value: 0 };
|
|
85
97
|
const slots = [];
|
|
86
|
-
const html = elementToHtml(node, slots, [], filename);
|
|
98
|
+
const html = elementToHtml(node, slots, [], filename, counter);
|
|
87
99
|
return { html, slots };
|
|
88
100
|
}
|
|
89
101
|
function warnSuspectedStyleInterpolation(attrValue, filename, line) {
|
|
@@ -153,7 +165,7 @@ function serializeStyleValue(expr) {
|
|
|
153
165
|
}
|
|
154
166
|
return parts.join(";");
|
|
155
167
|
}
|
|
156
|
-
function elementToHtml(node, slots, path, filename) {
|
|
168
|
+
function elementToHtml(node, slots, path, filename, counter) {
|
|
157
169
|
const tagName = jsxNameToString(node.openingElement.name);
|
|
158
170
|
if (tagName === "")
|
|
159
171
|
return "";
|
|
@@ -171,7 +183,18 @@ function elementToHtml(node, slots, path, filename) {
|
|
|
171
183
|
if (attrName === "children")
|
|
172
184
|
continue;
|
|
173
185
|
if (attrName === "ref") {
|
|
174
|
-
|
|
186
|
+
if (attr.value?.type === "JSXExpressionContainer") {
|
|
187
|
+
const expr = attr.value.expression;
|
|
188
|
+
if (expr.type !== "JSXEmptyExpression") {
|
|
189
|
+
slots.push({
|
|
190
|
+
path: [...path],
|
|
191
|
+
type: "ref",
|
|
192
|
+
name: "ref",
|
|
193
|
+
expr
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
continue;
|
|
175
198
|
}
|
|
176
199
|
if (attr.value?.type === "JSXExpressionContainer") {
|
|
177
200
|
const expr = attr.value.expression;
|
|
@@ -204,8 +227,15 @@ function elementToHtml(node, slots, path, filename) {
|
|
|
204
227
|
html += isVoid ? " />" : ">";
|
|
205
228
|
if (isVoid)
|
|
206
229
|
return html;
|
|
207
|
-
|
|
208
|
-
|
|
230
|
+
const childResult = childrenToHtml(node.children, slots, path, filename, 0, counter);
|
|
231
|
+
html += childResult.html;
|
|
232
|
+
html += `</${tagName}>`;
|
|
233
|
+
return html;
|
|
234
|
+
}
|
|
235
|
+
function childrenToHtml(children, slots, path, filename, startIndex, counter) {
|
|
236
|
+
let html = "";
|
|
237
|
+
let childIndex = startIndex;
|
|
238
|
+
for (const child of children) {
|
|
209
239
|
if (child.type === "JSXText") {
|
|
210
240
|
const text = cleanJSXText(child.value ?? "");
|
|
211
241
|
if (text) {
|
|
@@ -215,7 +245,7 @@ function elementToHtml(node, slots, path, filename) {
|
|
|
215
245
|
} else if (child.type === "JSXExpressionContainer") {
|
|
216
246
|
if (child.expression.type === "JSXEmptyExpression")
|
|
217
247
|
continue;
|
|
218
|
-
const slot = nextSlotId();
|
|
248
|
+
const slot = nextSlotId(counter);
|
|
219
249
|
slots.push({
|
|
220
250
|
path: [...path, childIndex],
|
|
221
251
|
type: "child",
|
|
@@ -224,12 +254,29 @@ function elementToHtml(node, slots, path, filename) {
|
|
|
224
254
|
html += slot;
|
|
225
255
|
childIndex++;
|
|
226
256
|
} else if (child.type === "JSXElement") {
|
|
227
|
-
|
|
228
|
-
|
|
257
|
+
const childName = child.openingElement.name;
|
|
258
|
+
const childTag = childName.type === "JSXIdentifier" ? childName.name : "";
|
|
259
|
+
const isFirstCharUpper = childTag && childTag.charAt(0) === childTag.charAt(0).toUpperCase();
|
|
260
|
+
if (isFirstCharUpper) {
|
|
261
|
+
const slot = nextSlotId(counter);
|
|
262
|
+
slots.push({
|
|
263
|
+
path: [...path, childIndex],
|
|
264
|
+
type: "child",
|
|
265
|
+
expr: child
|
|
266
|
+
});
|
|
267
|
+
html += slot;
|
|
268
|
+
childIndex++;
|
|
269
|
+
} else {
|
|
270
|
+
html += elementToHtml(child, slots, [...path, childIndex], filename, counter);
|
|
271
|
+
childIndex++;
|
|
272
|
+
}
|
|
273
|
+
} else if (child.type === "JSXFragment") {
|
|
274
|
+
const fragResult = childrenToHtml(child.children, slots, path, filename, childIndex, counter);
|
|
275
|
+
html += fragResult.html;
|
|
276
|
+
childIndex = fragResult.childIndex;
|
|
229
277
|
}
|
|
230
278
|
}
|
|
231
|
-
html
|
|
232
|
-
return html;
|
|
279
|
+
return { html, childIndex };
|
|
233
280
|
}
|
|
234
281
|
function jsxNameToString(name) {
|
|
235
282
|
if (name.type === "JSXIdentifier")
|
|
@@ -245,10 +292,12 @@ function transformJSX(code, filename, options = {}) {
|
|
|
245
292
|
plugins: ["jsx", "typescript"],
|
|
246
293
|
sourceFilename: filename
|
|
247
294
|
});
|
|
295
|
+
autoWrapComponents(ast, filename);
|
|
248
296
|
wrapReactiveExpressions(ast, {
|
|
249
297
|
explicitBindings: options.explicitBindings,
|
|
250
298
|
analyze: options.analyze,
|
|
251
299
|
analyzeMetadata: options.analyzeMetadata,
|
|
300
|
+
resolveImport: options.resolveImport,
|
|
252
301
|
filename
|
|
253
302
|
});
|
|
254
303
|
const hoist = options.hoist !== false;
|
|
@@ -275,10 +324,23 @@ function transformJSX(code, filename, options = {}) {
|
|
|
275
324
|
const templateExpr = t.callExpression(t.identifier("_$createTemplate"), [t.identifier(tmplId), t.arrayExpression(dynamicExprs)]);
|
|
276
325
|
const parentIsJSX = path.parentPath?.isJSXElement() || path.parentPath?.isJSXFragment();
|
|
277
326
|
path.replaceWith(parentIsJSX ? t.jsxExpressionContainer(templateExpr) : templateExpr);
|
|
278
|
-
} catch {
|
|
327
|
+
} catch (err) {
|
|
328
|
+
if (options.dev) {
|
|
329
|
+
console.warn(`[Sinwan] template hoisting skipped in ${filename}: ${err.message}`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
279
332
|
}
|
|
280
333
|
});
|
|
281
334
|
if (templates.length === 0) {
|
|
335
|
+
if (options.explicitBindings) {
|
|
336
|
+
const bindingImport = t.importDeclaration([
|
|
337
|
+
t.importSpecifier(t.identifier("_$bindText"), t.identifier("_$bindText")),
|
|
338
|
+
t.importSpecifier(t.identifier("_$bindAttr"), t.identifier("_$bindAttr")),
|
|
339
|
+
t.importSpecifier(t.identifier("_$bindStyle"), t.identifier("_$bindStyle")),
|
|
340
|
+
t.importSpecifier(t.identifier("_$bindClass"), t.identifier("_$bindClass"))
|
|
341
|
+
], t.stringLiteral("sinwan/renderer"));
|
|
342
|
+
ast.program.body.unshift(bindingImport);
|
|
343
|
+
}
|
|
282
344
|
const result2 = generate(ast, {
|
|
283
345
|
sourceMaps: true,
|
|
284
346
|
sourceFileName: filename
|
|
@@ -308,22 +370,81 @@ function transformJSX(code, filename, options = {}) {
|
|
|
308
370
|
const result = generate(ast, { sourceMaps: true, sourceFileName: filename });
|
|
309
371
|
return { code: result.code, map: result.map };
|
|
310
372
|
}
|
|
373
|
+
// src/exports.ts
|
|
374
|
+
import { parse as parse2 } from "@babel/parser";
|
|
375
|
+
function isComponentName(name) {
|
|
376
|
+
return !!name && name[0] === name[0].toUpperCase() && /^[A-Z]/.test(name);
|
|
377
|
+
}
|
|
378
|
+
function collectExportedComponents(code, filename) {
|
|
379
|
+
let ast;
|
|
380
|
+
try {
|
|
381
|
+
ast = parse2(code, {
|
|
382
|
+
sourceType: "module",
|
|
383
|
+
plugins: ["jsx", "typescript"],
|
|
384
|
+
sourceFilename: filename
|
|
385
|
+
});
|
|
386
|
+
} catch {
|
|
387
|
+
return [];
|
|
388
|
+
}
|
|
389
|
+
const found = [];
|
|
390
|
+
const seen = new Set;
|
|
391
|
+
const add = (local, key) => {
|
|
392
|
+
const sig = `${local}::${key}`;
|
|
393
|
+
if (seen.has(sig))
|
|
394
|
+
return;
|
|
395
|
+
seen.add(sig);
|
|
396
|
+
found.push({ local, key });
|
|
397
|
+
};
|
|
398
|
+
for (const node of ast.program.body) {
|
|
399
|
+
if (node.type === "ExportNamedDeclaration") {
|
|
400
|
+
if (node.declaration) {
|
|
401
|
+
const decl = node.declaration;
|
|
402
|
+
if (decl.type === "FunctionDeclaration" && decl.id && isComponentName(decl.id.name)) {
|
|
403
|
+
add(decl.id.name, decl.id.name);
|
|
404
|
+
} else if (decl.type === "VariableDeclaration") {
|
|
405
|
+
for (const d of decl.declarations) {
|
|
406
|
+
if (d.id?.type === "Identifier" && isComponentName(d.id.name)) {
|
|
407
|
+
add(d.id.name, d.id.name);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
} else if (node.specifiers && !node.source) {
|
|
412
|
+
for (const spec of node.specifiers) {
|
|
413
|
+
if (spec.type === "ExportSpecifier" && spec.local?.type === "Identifier" && spec.exported?.type === "Identifier" && isComponentName(spec.exported.name)) {
|
|
414
|
+
add(spec.local.name, spec.exported.name);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
} else if (node.type === "ExportDefaultDeclaration") {
|
|
419
|
+
const decl = node.declaration;
|
|
420
|
+
if (decl.type === "Identifier") {
|
|
421
|
+
add(decl.name, "default");
|
|
422
|
+
} else if (decl.type === "FunctionDeclaration" && decl.id) {
|
|
423
|
+
add(decl.id.name, "default");
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return found;
|
|
428
|
+
}
|
|
311
429
|
export {
|
|
312
|
-
|
|
313
|
-
|
|
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,
|
|
430
|
+
AnalyzerCache,
|
|
431
|
+
COMPILER_TEMPLATE_SLOT_PROTOCOL,
|
|
327
432
|
analyze,
|
|
328
|
-
|
|
433
|
+
analyzeModule,
|
|
434
|
+
analyzeProject,
|
|
435
|
+
autoWrapComponents,
|
|
436
|
+
buildFullScope,
|
|
437
|
+
collectComponentCallGraph,
|
|
438
|
+
collectComponentFunctions,
|
|
439
|
+
collectExportedComponents,
|
|
440
|
+
computeLocalScopes,
|
|
441
|
+
containsReactiveValue,
|
|
442
|
+
getAllPropNames,
|
|
443
|
+
isReactiveValue,
|
|
444
|
+
loadMetadata,
|
|
445
|
+
propagateReactiveProps,
|
|
446
|
+
runAnalyzeCli,
|
|
447
|
+
trackReactiveImports,
|
|
448
|
+
transformJSX,
|
|
449
|
+
wrapReactiveExpressions
|
|
329
450
|
};
|
package/dist/reactive-wrap.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* Supported reactive sources:
|
|
9
9
|
* - `createMutable` / `createStore` from `sinwan/store`
|
|
10
10
|
* - `signal` / `computed` from `sinwan/reactivity`
|
|
11
|
-
* - `useState` from `sinwan/react
|
|
11
|
+
* - `useState` from `sinwan/react`
|
|
12
12
|
*
|
|
13
13
|
* Example:
|
|
14
14
|
* <p>{state.name}</p> → <p>{() => state.name}</p>
|
|
@@ -22,6 +22,7 @@ export interface ImportNames {
|
|
|
22
22
|
signal: Set<string>;
|
|
23
23
|
computed: Set<string>;
|
|
24
24
|
useState: Set<string>;
|
|
25
|
+
useFetch: Set<string>;
|
|
25
26
|
cc: Set<string>;
|
|
26
27
|
}
|
|
27
28
|
export declare function trackReactiveImports(ast: t.Node): ImportNames;
|
|
@@ -39,15 +40,26 @@ export interface ComputedBinding {
|
|
|
39
40
|
export interface GetterBinding {
|
|
40
41
|
kind: "getter";
|
|
41
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Binding for a variable holding the object returned by `useFetch()` /
|
|
45
|
+
* `createFetch()(...)`. Every property on that object is a `Signal` or
|
|
46
|
+
* `Computed`, so a read like `f.data.value` (path `["data", "value"]`) is
|
|
47
|
+
* reactive. The shell object itself (`f`) and a bare property (`f.data`, the
|
|
48
|
+
* signal object) are NOT reactive reads — the runtime `resolve()` unwraps them
|
|
49
|
+
* inside effects when forwarded to control-flow props such as `<Show when>`.
|
|
50
|
+
*/
|
|
51
|
+
export interface SignalObjectBinding {
|
|
52
|
+
kind: "signalObject";
|
|
53
|
+
}
|
|
42
54
|
export interface PropBinding {
|
|
43
55
|
kind: "prop";
|
|
44
56
|
}
|
|
45
|
-
export type Binding = MutableBinding | SignalBinding | ComputedBinding | GetterBinding | PropBinding;
|
|
57
|
+
export type Binding = MutableBinding | SignalBinding | ComputedBinding | GetterBinding | SignalObjectBinding | PropBinding;
|
|
46
58
|
export interface ReactiveScope {
|
|
47
59
|
bindings: Map<string, Binding>;
|
|
48
60
|
}
|
|
49
61
|
export declare function getAllPropNames(params: t.Node | undefined): Set<string>;
|
|
50
|
-
export declare function isReactiveValue(node: t.
|
|
62
|
+
export declare function isReactiveValue(node: t.Node, scope: ReactiveScope): boolean;
|
|
51
63
|
export declare function containsReactiveValue(expr: t.Expression, scope: ReactiveScope): boolean;
|
|
52
64
|
export interface ComponentExpressionInfo {
|
|
53
65
|
isComponent: boolean;
|
|
@@ -76,5 +88,6 @@ export declare function wrapReactiveExpressions(ast: t.Node, options?: {
|
|
|
76
88
|
explicitBindings?: boolean;
|
|
77
89
|
analyze?: string;
|
|
78
90
|
analyzeMetadata?: Map<string, Map<string, Set<string>>>;
|
|
91
|
+
resolveImport?: (source: string, fromFile: string) => string | null;
|
|
79
92
|
filename?: string;
|
|
80
93
|
}): void;
|
package/dist/transform.d.ts
CHANGED
|
@@ -16,7 +16,29 @@ export interface TransformOptions {
|
|
|
16
16
|
analyze?: string;
|
|
17
17
|
/** In-memory reactive-props metadata (used by plugin-level caches). */
|
|
18
18
|
analyzeMetadata?: Map<string, Map<string, Set<string>>>;
|
|
19
|
+
/**
|
|
20
|
+
* Resolves an import source (e.g. "./Child") to an absolute file path,
|
|
21
|
+
* relative to the file being transformed. Used to look up reactive-prop
|
|
22
|
+
* metadata for components imported from other modules so reactive values
|
|
23
|
+
* passed to them are wrapped at the call site. When omitted, cross-module
|
|
24
|
+
* component call sites fall back to the conservative (non-wrapping) behavior.
|
|
25
|
+
*/
|
|
26
|
+
resolveImport?: (source: string, fromFile: string) => string | null;
|
|
19
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Template slot marker encoding.
|
|
30
|
+
*
|
|
31
|
+
* This mirrors the runtime's `DEFAULT_TEMPLATE_SLOT_PROTOCOL` in
|
|
32
|
+
* `sinwan/src/renderer/template-protocol.ts`. The compiler cannot import the
|
|
33
|
+
* runtime (separate packages), so the format is duplicated here. A drift test
|
|
34
|
+
* (`sinwan/__tests__/compiler-protocol-drift.test.ts`) asserts the two stay
|
|
35
|
+
* in sync.
|
|
36
|
+
*/
|
|
37
|
+
export declare const COMPILER_TEMPLATE_SLOT_PROTOCOL: {
|
|
38
|
+
slotPrefix: string;
|
|
39
|
+
encodeSlot(index: number): string;
|
|
40
|
+
decodeSlot(data: string): number | null;
|
|
41
|
+
};
|
|
20
42
|
export declare function transformJSX(code: string, filename: string, options?: TransformOptions): {
|
|
21
43
|
code: string;
|
|
22
44
|
map?: any;
|