@llui/vite-plugin 0.0.11 → 0.0.14

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/transform.js CHANGED
@@ -90,6 +90,68 @@ const PROP_KEYS = new Set([
90
90
  'innerHTML',
91
91
  'textContent',
92
92
  ]);
93
+ /**
94
+ * Walk from `use` outward toward the source file root, looking for a
95
+ * `const <name> = <initializer>` (or `let`/`var`) declaration that binds
96
+ * this identifier in an enclosing scope. Returns the initializer if found.
97
+ *
98
+ * This enables reactive-binding detection for hoisted accessors:
99
+ *
100
+ * const cls = (s: State) => isActive(item, s.path) ? 'on' : 'off'
101
+ * return a({ class: cls }, ...)
102
+ *
103
+ * Without resolution, `class: cls` would fall through the static path
104
+ * and emit `__e.className = cls`, coercing the function to its source
105
+ * string. With resolution, we see that `cls` is an arrow and emit a
106
+ * binding exactly as if the arrow had been inlined.
107
+ *
108
+ * Scope rules:
109
+ * - Only single-binding `const`/`let`/`var` declarations with an
110
+ * initializer are considered. No destructuring, no multi-declarator
111
+ * statements (too easy to get wrong without a type checker).
112
+ * - Later reassignments are NOT tracked — if the identifier is `let`
113
+ * and gets reassigned, the resolution is unreliable. We conservatively
114
+ * refuse to resolve `let` bindings for now (arrow-valued accessors
115
+ * are ~always `const` in practice).
116
+ * - The declaration must dominate the use (same block or an enclosing
117
+ * one). TypeScript's block semantics mean walking up parent blocks
118
+ * is sufficient.
119
+ */
120
+ function resolveLocalConstInitializer(use) {
121
+ const name = use.text;
122
+ let node = use;
123
+ while (node.parent) {
124
+ const parent = node.parent;
125
+ // Scan statements of an enclosing block/source-file for a matching declaration
126
+ let statements = null;
127
+ if (ts.isBlock(parent) || ts.isSourceFile(parent) || ts.isModuleBlock(parent)) {
128
+ statements = parent.statements;
129
+ }
130
+ else if (ts.isCaseClause(parent) || ts.isDefaultClause(parent)) {
131
+ statements = parent.statements;
132
+ }
133
+ if (statements) {
134
+ for (const stmt of statements) {
135
+ if (!ts.isVariableStatement(stmt))
136
+ continue;
137
+ // Skip `let` — reassignment would invalidate our resolution
138
+ const flags = stmt.declarationList.flags;
139
+ if (!(flags & ts.NodeFlags.Const))
140
+ continue;
141
+ if (stmt.declarationList.declarations.length !== 1)
142
+ continue;
143
+ const decl = stmt.declarationList.declarations[0];
144
+ if (!ts.isIdentifier(decl.name) || decl.name.text !== name)
145
+ continue;
146
+ if (!decl.initializer)
147
+ continue;
148
+ return decl.initializer;
149
+ }
150
+ }
151
+ node = parent;
152
+ }
153
+ return null;
154
+ }
93
155
  function classifyKind(key) {
94
156
  if (key === 'class' || key === 'className')
95
157
  return 'class';
@@ -328,11 +390,24 @@ __enableDevTools()${relayCall}
328
390
  const replaceCalls = components
329
391
  .map(({ varName, componentName }) => ` __replaceComponent("${componentName}", ${varName})`)
330
392
  .join('\n');
393
+ // HMR auto-connect: when the Vite plugin detects that @llui/mcp's
394
+ // active marker file exists or appears, it sends `llui:mcp-ready`
395
+ // with the MCP bridge port. We forward that to __lluiConnect so the
396
+ // browser connects automatically — no console gymnastics, no retry
397
+ // spam, regardless of whether MCP or Vite started first.
398
+ const mcpHmrHandler = mcpPort !== null
399
+ ? `
400
+ import.meta.hot.on('llui:mcp-ready', (data) => {
401
+ if (typeof globalThis.__lluiConnect === 'function') {
402
+ globalThis.__lluiConnect(data?.port)
403
+ }
404
+ })`
405
+ : '';
331
406
  const bottom = `
332
407
  if (import.meta.hot) {
333
408
  import.meta.hot.accept(() => {
334
409
  ${replaceCalls}
335
- })
410
+ })${mcpHmrHandler}
336
411
  }
337
412
  `.trim();
338
413
  return { top, bottom };
@@ -629,6 +704,17 @@ function tryTransformElementCall(node, helpers, fieldBits, compiled, bailed, f)
629
704
  events.push(f.createArrayLiteralExpression([f.createStringLiteral(eventName), value]));
630
705
  continue;
631
706
  }
707
+ // If the value is an Identifier that refers to a local const-bound
708
+ // arrow/function, resolve it and treat it as if the arrow had been
709
+ // inlined. Without this, `class: cls` where `const cls = (s) => ...`
710
+ // falls through to the static path and emits `.className = cls`,
711
+ // coercing the function to its source string in the DOM.
712
+ if (ts.isIdentifier(value)) {
713
+ const resolved = resolveLocalConstInitializer(value);
714
+ if (resolved && (ts.isArrowFunction(resolved) || ts.isFunctionExpression(resolved))) {
715
+ value = resolved;
716
+ }
717
+ }
632
718
  // Reactive binding — value is an arrow function or function expression
633
719
  if (ts.isArrowFunction(value) || ts.isFunctionExpression(value)) {
634
720
  const kind = classifyKind(key);
@@ -2373,6 +2459,14 @@ function analyzeSubtree(node, helpers, fieldBits, path) {
2373
2459
  events.push([key.slice(2).toLowerCase(), value]);
2374
2460
  continue;
2375
2461
  }
2462
+ // Resolve identifier → local const arrow initializer (see elSplit
2463
+ // path for the full rationale).
2464
+ if (ts.isIdentifier(value)) {
2465
+ const resolved = resolveLocalConstInitializer(value);
2466
+ if (resolved && (ts.isArrowFunction(resolved) || ts.isFunctionExpression(resolved))) {
2467
+ value = resolved;
2468
+ }
2469
+ }
2376
2470
  // Reactive binding
2377
2471
  if (ts.isArrowFunction(value) || ts.isFunctionExpression(value)) {
2378
2472
  const kind = classifyKind(key);
@@ -2851,9 +2945,12 @@ function isPerItemCall(node) {
2851
2945
  return ts.isArrowFunction(arg) || ts.isFunctionExpression(arg);
2852
2946
  }
2853
2947
  // Matches: item.FIELD — the item-proxy shorthand equivalent of item(t => t.FIELD).
2854
- // Loose heuristic: any `IDENT.IDENT` where the left side is the bare identifier `item`.
2855
- // The runtime detects per-item via accessor.length === 0, so passing the property access
2856
- // directly as a binding accessor works regardless of what the compiler assumes.
2948
+ // Scope-checked: the `item` identifier must resolve to a parameter of an
2949
+ // `each({ render })` callback. Without this check, plain
2950
+ // `arr.map((item) => item.field)` outside each() would be rewritten as a
2951
+ // per-item binding and crash at runtime with "accessor is not a function"
2952
+ // because `item.field` evaluates to a bare value (not a function) when
2953
+ // treated as an accessor.
2857
2954
  function isPerItemFieldAccess(node) {
2858
2955
  if (!ts.isPropertyAccessExpression(node))
2859
2956
  return false;
@@ -2863,6 +2960,58 @@ function isPerItemFieldAccess(node) {
2863
2960
  return false;
2864
2961
  if (!ts.isIdentifier(node.name))
2865
2962
  return false;
2963
+ return isItemBoundToEachRender(node);
2964
+ }
2965
+ /**
2966
+ * Walks up from a node and returns true iff the nearest enclosing function
2967
+ * that binds an `item` parameter is the `render` property of an `each()`
2968
+ * call. Handles both positional (`(item) => …`) and destructured
2969
+ * (`({ item, index }) => …`) parameter bindings.
2970
+ */
2971
+ function isItemBoundToEachRender(node) {
2972
+ let current = node.parent;
2973
+ while (current) {
2974
+ if (ts.isArrowFunction(current) || ts.isFunctionExpression(current)) {
2975
+ if (functionParamsBindItem(current)) {
2976
+ return isEachRenderCallback(current);
2977
+ }
2978
+ }
2979
+ current = current.parent;
2980
+ }
2981
+ return false;
2982
+ }
2983
+ function functionParamsBindItem(fn) {
2984
+ for (const param of fn.parameters) {
2985
+ if (bindingNameBindsItem(param.name))
2986
+ return true;
2987
+ }
2988
+ return false;
2989
+ }
2990
+ function bindingNameBindsItem(name) {
2991
+ if (ts.isIdentifier(name))
2992
+ return name.text === 'item';
2993
+ if (ts.isObjectBindingPattern(name) || ts.isArrayBindingPattern(name)) {
2994
+ for (const el of name.elements) {
2995
+ if (ts.isBindingElement(el) && bindingNameBindsItem(el.name))
2996
+ return true;
2997
+ }
2998
+ }
2999
+ return false;
3000
+ }
3001
+ function isEachRenderCallback(fn) {
3002
+ const parent = fn.parent;
3003
+ if (!parent || !ts.isPropertyAssignment(parent))
3004
+ return false;
3005
+ if (!ts.isIdentifier(parent.name) || parent.name.text !== 'render')
3006
+ return false;
3007
+ const objLit = parent.parent;
3008
+ if (!objLit || !ts.isObjectLiteralExpression(objLit))
3009
+ return false;
3010
+ const call = objLit.parent;
3011
+ if (!call || !ts.isCallExpression(call))
3012
+ return false;
3013
+ if (!ts.isIdentifier(call.expression) || call.expression.text !== 'each')
3014
+ return false;
2866
3015
  return true;
2867
3016
  }
2868
3017
  // Matches the hoisted identifiers produced by tryDeduplicateItemSelectors: __a0, __a1, …