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/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // @bun
3
3
  import {
4
4
  runAnalyzeCli
5
- } from "./chunk-3bqqn28f.js";
5
+ } from "./chunk-w580fdrf.js";
6
6
  export {
7
7
  runAnalyzeCli
8
8
  };
@@ -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-3bqqn28f.js";
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 SLOT_PREFIX = "s";
29
- var slotId = 0;
30
- function nextSlotId() {
31
- return `<!--${SLOT_PREFIX}:${slotId++}-->`;
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
- slotId = 0;
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
- throw new Error("Cannot hoist element with ref");
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
- let childIndex = 0;
208
- for (const child of node.children) {
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
- html += elementToHtml(child, slots, [...path, childIndex], filename);
228
- childIndex++;
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 += `</${tagName}>`;
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
- 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,
430
+ AnalyzerCache,
431
+ COMPILER_TEMPLATE_SLOT_PROTOCOL,
327
432
  analyze,
328
- AnalyzerCache
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
  };
@@ -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-client`
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.Identifier | t.MemberExpression, scope: ReactiveScope): boolean;
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;
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sinwan-compiler",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "Shared compiler core for Sinwan plugins",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",