@pyreon/compiler 0.12.4 → 0.12.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.
@@ -5386,7 +5386,7 @@ var drawChart = (function (exports) {
5386
5386
  </script>
5387
5387
  <script>
5388
5388
  /*<!--*/
5389
- const data = {"version":2,"tree":{"name":"root","children":[{"name":"index.js","children":[{"name":"src","children":[{"uid":"b14201ae-1","name":"jsx.ts"},{"uid":"b14201ae-3","name":"project-scanner.ts"},{"uid":"b14201ae-5","name":"react-intercept.ts"},{"uid":"b14201ae-7","name":"index.ts"}]}]}],"isRoot":true},"nodeParts":{"b14201ae-1":{"renderedLength":26107,"gzipLength":7273,"brotliLength":0,"metaUid":"b14201ae-0"},"b14201ae-3":{"renderedLength":4762,"gzipLength":1730,"brotliLength":0,"metaUid":"b14201ae-2"},"b14201ae-5":{"renderedLength":27692,"gzipLength":6920,"brotliLength":0,"metaUid":"b14201ae-4"},"b14201ae-7":{"renderedLength":0,"gzipLength":0,"brotliLength":0,"metaUid":"b14201ae-6"}},"nodeMetas":{"b14201ae-0":{"id":"/src/jsx.ts","moduleParts":{"index.js":"b14201ae-1"},"imported":[{"uid":"b14201ae-8"}],"importedBy":[{"uid":"b14201ae-6"}]},"b14201ae-2":{"id":"/src/project-scanner.ts","moduleParts":{"index.js":"b14201ae-3"},"imported":[{"uid":"b14201ae-9"},{"uid":"b14201ae-10"}],"importedBy":[{"uid":"b14201ae-6"}]},"b14201ae-4":{"id":"/src/react-intercept.ts","moduleParts":{"index.js":"b14201ae-5"},"imported":[{"uid":"b14201ae-8"}],"importedBy":[{"uid":"b14201ae-6"}]},"b14201ae-6":{"id":"/src/index.ts","moduleParts":{"index.js":"b14201ae-7"},"imported":[{"uid":"b14201ae-0"},{"uid":"b14201ae-2"},{"uid":"b14201ae-4"}],"importedBy":[],"isEntry":true},"b14201ae-8":{"id":"typescript","moduleParts":{},"imported":[],"importedBy":[{"uid":"b14201ae-0"},{"uid":"b14201ae-4"}]},"b14201ae-9":{"id":"node:fs","moduleParts":{},"imported":[],"importedBy":[{"uid":"b14201ae-2"}]},"b14201ae-10":{"id":"node:path","moduleParts":{},"imported":[],"importedBy":[{"uid":"b14201ae-2"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
5389
+ const data = {"version":2,"tree":{"name":"root","children":[{"name":"index.js","children":[{"name":"src","children":[{"uid":"4ebf1530-1","name":"jsx.ts"},{"uid":"4ebf1530-3","name":"project-scanner.ts"},{"uid":"4ebf1530-5","name":"react-intercept.ts"},{"uid":"4ebf1530-7","name":"index.ts"}]}]}],"isRoot":true},"nodeParts":{"4ebf1530-1":{"renderedLength":34297,"gzipLength":9205,"brotliLength":0,"metaUid":"4ebf1530-0"},"4ebf1530-3":{"renderedLength":4762,"gzipLength":1730,"brotliLength":0,"metaUid":"4ebf1530-2"},"4ebf1530-5":{"renderedLength":27692,"gzipLength":6920,"brotliLength":0,"metaUid":"4ebf1530-4"},"4ebf1530-7":{"renderedLength":0,"gzipLength":0,"brotliLength":0,"metaUid":"4ebf1530-6"}},"nodeMetas":{"4ebf1530-0":{"id":"/src/jsx.ts","moduleParts":{"index.js":"4ebf1530-1"},"imported":[{"uid":"4ebf1530-8"}],"importedBy":[{"uid":"4ebf1530-6"}]},"4ebf1530-2":{"id":"/src/project-scanner.ts","moduleParts":{"index.js":"4ebf1530-3"},"imported":[{"uid":"4ebf1530-9"},{"uid":"4ebf1530-10"}],"importedBy":[{"uid":"4ebf1530-6"}]},"4ebf1530-4":{"id":"/src/react-intercept.ts","moduleParts":{"index.js":"4ebf1530-5"},"imported":[{"uid":"4ebf1530-8"}],"importedBy":[{"uid":"4ebf1530-6"}]},"4ebf1530-6":{"id":"/src/index.ts","moduleParts":{"index.js":"4ebf1530-7"},"imported":[{"uid":"4ebf1530-0"},{"uid":"4ebf1530-2"},{"uid":"4ebf1530-4"}],"importedBy":[],"isEntry":true},"4ebf1530-8":{"id":"typescript","moduleParts":{},"imported":[],"importedBy":[{"uid":"4ebf1530-0"},{"uid":"4ebf1530-4"}]},"4ebf1530-9":{"id":"node:fs","moduleParts":{},"imported":[],"importedBy":[{"uid":"4ebf1530-2"}]},"4ebf1530-10":{"id":"node:path","moduleParts":{},"imported":[],"importedBy":[{"uid":"4ebf1530-2"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
5390
5390
 
5391
5391
  const run = () => {
5392
5392
  const width = window.innerWidth;
package/lib/index.js CHANGED
@@ -85,6 +85,7 @@ function transformJSX(code, filename = "input.tsx") {
85
85
  let needsBindTextImportGlobal = false;
86
86
  let needsBindDirectImportGlobal = false;
87
87
  let needsBindImportGlobal = false;
88
+ let needsApplyPropsImportGlobal = false;
88
89
  /**
89
90
  * If `node` is a fully-static JSX element/fragment, register a module-scope
90
91
  * hoist for it and return the generated variable name. Otherwise return null.
@@ -104,10 +105,11 @@ function transformJSX(code, filename = "input.tsx") {
104
105
  function wrap(expr) {
105
106
  const start = expr.getStart(sf);
106
107
  const end = expr.getEnd();
108
+ const exprText = inlineVarsInText(code.slice(start, end));
107
109
  replacements.push({
108
110
  start,
109
111
  end,
110
- text: `() => ${code.slice(start, end)}`
112
+ text: `() => ${exprText}`
111
113
  });
112
114
  }
113
115
  /** Try to hoist or wrap an expression, pushing a replacement if needed. */
@@ -122,7 +124,7 @@ function transformJSX(code, filename = "input.tsx") {
122
124
  }
123
125
  /** Try to emit a template for a JsxElement. Returns true if handled. */
124
126
  function tryTemplateEmit(node) {
125
- if (templateElementCount(node) < 1) return false;
127
+ if (templateElementCount(node, true) < 1) return false;
126
128
  const tplCall = buildTemplateCall(node);
127
129
  if (!tplCall) return false;
128
130
  const start = node.getStart(sf);
@@ -203,6 +205,161 @@ function transformJSX(code, filename = "input.tsx") {
203
205
  }
204
206
  ts.forEachChild(expr, walk);
205
207
  }
208
+ /** Names that refer to the props object or splitProps results. */
209
+ const propsNames = /* @__PURE__ */ new Set();
210
+ /** Map of variable name → source text of the original expression. */
211
+ const propDerivedVars = /* @__PURE__ */ new Map();
212
+ /** Check if an expression reads from a tracked props-like object. */
213
+ function readsFromProps(node) {
214
+ if (ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.expression)) return propsNames.has(node.expression.text);
215
+ if (ts.isElementAccessExpression(node) && ts.isIdentifier(node.expression)) return propsNames.has(node.expression.text);
216
+ let found = false;
217
+ ts.forEachChild(node, (child) => {
218
+ if (found) return;
219
+ if (readsFromProps(child)) found = true;
220
+ });
221
+ return found;
222
+ }
223
+ /** Pre-pass: scan a function body for prop-derived variable declarations.
224
+ * callbackDepth tracks nesting inside callback arguments (map, filter, etc.)
225
+ * to avoid tracking variables declared inside callbacks as prop-derived. */
226
+ let _callbackDepth = 0;
227
+ function scanForPropDerivedVars(node) {
228
+ if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) {
229
+ const parent = node.parent;
230
+ if (parent && ts.isCallExpression(parent) && parent.arguments.includes(node)) {
231
+ _callbackDepth++;
232
+ ts.forEachChild(node, scanForPropDerivedVars);
233
+ _callbackDepth--;
234
+ return;
235
+ }
236
+ }
237
+ if ((ts.isFunctionDeclaration(node) || ts.isArrowFunction(node) || ts.isFunctionExpression(node)) && node.parameters.length > 0) {
238
+ const parent = node.parent;
239
+ if (parent && ts.isCallExpression(parent) && parent.arguments.includes(node)) {
240
+ ts.forEachChild(node, scanForPropDerivedVars);
241
+ return;
242
+ }
243
+ const firstParam = node.parameters[0];
244
+ if (ts.isIdentifier(firstParam.name)) {
245
+ let hasJSX = false;
246
+ ts.forEachChild(node, function checkJSX(n) {
247
+ if (hasJSX) return;
248
+ if (ts.isJsxElement(n) || ts.isJsxSelfClosingElement(n) || ts.isJsxFragment(n)) {
249
+ hasJSX = true;
250
+ return;
251
+ }
252
+ ts.forEachChild(n, checkJSX);
253
+ });
254
+ if (hasJSX) propsNames.add(firstParam.name.text);
255
+ }
256
+ }
257
+ if (ts.isVariableStatement(node)) for (const decl of node.declarationList.declarations) {
258
+ if (ts.isArrayBindingPattern(decl.name) && decl.initializer && ts.isCallExpression(decl.initializer)) {
259
+ const callee = decl.initializer.expression;
260
+ if (ts.isIdentifier(callee) && callee.text === "splitProps") {
261
+ for (const el of decl.name.elements) if (ts.isBindingElement(el) && ts.isIdentifier(el.name)) propsNames.add(el.name.text);
262
+ }
263
+ }
264
+ if (!(node.declarationList.flags & ts.NodeFlags.Const)) continue;
265
+ if (_callbackDepth > 0) continue;
266
+ if (ts.isIdentifier(decl.name) && decl.initializer) {
267
+ if (readsFromProps(decl.initializer)) {
268
+ const varName = decl.name.text;
269
+ const exprText = code.slice(decl.initializer.getStart(sf), decl.initializer.getEnd());
270
+ propDerivedVars.set(varName, exprText);
271
+ }
272
+ }
273
+ }
274
+ ts.forEachChild(node, scanForPropDerivedVars);
275
+ }
276
+ scanForPropDerivedVars(sf);
277
+ let changed = true;
278
+ while (changed) {
279
+ changed = false;
280
+ sf.forEachChild(function scanTransitive(node) {
281
+ if (!ts.isVariableStatement(node)) {
282
+ ts.forEachChild(node, scanTransitive);
283
+ return;
284
+ }
285
+ for (const decl of node.declarationList.declarations) {
286
+ if (!ts.isIdentifier(decl.name) || !decl.initializer) continue;
287
+ const varName = decl.name.text;
288
+ if (propDerivedVars.has(varName)) continue;
289
+ if (node.declarationList.flags & ts.NodeFlags.Let) continue;
290
+ let usesPropVar = false;
291
+ ts.forEachChild(decl.initializer, function check(n) {
292
+ if (usesPropVar) return;
293
+ if (ts.isIdentifier(n) && propDerivedVars.has(n.text)) {
294
+ const parent = n.parent;
295
+ if (parent && ts.isPropertyAccessExpression(parent) && parent.name === n) return;
296
+ usesPropVar = true;
297
+ }
298
+ ts.forEachChild(n, check);
299
+ });
300
+ if (usesPropVar) {
301
+ const exprText = code.slice(decl.initializer.getStart(sf), decl.initializer.getEnd());
302
+ propDerivedVars.set(varName, exprText);
303
+ changed = true;
304
+ }
305
+ }
306
+ });
307
+ }
308
+ for (const [varName, expr] of propDerivedVars) {
309
+ let resolved = expr;
310
+ for (const [depName, depExpr] of propDerivedVars) {
311
+ if (depName === varName) continue;
312
+ const re = new RegExp(`(?<![.\\w])${depName}(?![\\w:])`, "g");
313
+ if (re.test(resolved)) resolved = resolved.replace(re, `(${depExpr})`);
314
+ }
315
+ if (resolved !== expr) propDerivedVars.set(varName, resolved);
316
+ }
317
+ /**
318
+ * Enhanced dynamic check — combines containsCall with props awareness.
319
+ * Returns true if an expression is reactive (contains signal calls,
320
+ * accesses props members, or references prop-derived variables).
321
+ */
322
+ /**
323
+ * Replace prop-derived variable names in a source text with their original expressions.
324
+ * Simple regex-based replacement — safe because variable names are identifiers.
325
+ */
326
+ function inlineVarsInText(text) {
327
+ if (propDerivedVars.size === 0) return text;
328
+ let result = text;
329
+ for (const [varName, expr] of propDerivedVars) {
330
+ const re = new RegExp(`(?<![.\\w])${varName}(?![\\w:])`, "g");
331
+ result = result.replace(re, `(${expr})`);
332
+ }
333
+ return result;
334
+ }
335
+ function isDynamic(node) {
336
+ if (containsCall(node)) return true;
337
+ return accessesProps(node);
338
+ }
339
+ /** Check if an expression accesses a tracked props object or a prop-derived variable. */
340
+ function accessesProps(node) {
341
+ if (ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.expression)) {
342
+ if (propsNames.has(node.expression.text)) return true;
343
+ }
344
+ if (ts.isIdentifier(node) && propDerivedVars.has(node.text)) {
345
+ const parent = node.parent;
346
+ if (parent && ts.isPropertyAccessExpression(parent) && parent.name === node) return false;
347
+ return true;
348
+ }
349
+ let found = false;
350
+ ts.forEachChild(node, (child) => {
351
+ if (found) return;
352
+ if (ts.isArrowFunction(child) || ts.isFunctionExpression(child)) return;
353
+ if (accessesProps(child)) found = true;
354
+ });
355
+ return found;
356
+ }
357
+ function shouldWrap(node) {
358
+ if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) return false;
359
+ if (isStatic(node)) return false;
360
+ if (ts.isCallExpression(node) && isPureStaticCall(node)) return false;
361
+ return isDynamic(node);
362
+ }
206
363
  function walk(node) {
207
364
  if (ts.isJsxElement(node) && tryTemplateEmit(node)) return;
208
365
  if (ts.isJsxSelfClosingElement(node) || ts.isJsxElement(node)) checkForWarnings(node);
@@ -236,6 +393,7 @@ function transformJSX(code, filename = "input.tsx") {
236
393
  const runtimeDomImports = ["_tpl"];
237
394
  if (needsBindDirectImportGlobal) runtimeDomImports.push("_bindDirect");
238
395
  if (needsBindTextImportGlobal) runtimeDomImports.push("_bindText");
396
+ if (needsApplyPropsImportGlobal) runtimeDomImports.push("_applyProps");
239
397
  const reactivityImports = needsBindImportGlobal ? `\nimport { _bind } from "@pyreon/reactivity";` : "";
240
398
  result = `import { ${runtimeDomImports.join(", ")} } from "@pyreon/runtime-dom";${reactivityImports}\n` + result;
241
399
  }
@@ -245,10 +403,18 @@ function transformJSX(code, filename = "input.tsx") {
245
403
  usesTemplates: needsTplImport,
246
404
  warnings
247
405
  };
248
- /** Check if a single attribute would prevent template emission. */
249
- function hasBailAttr(node) {
406
+ /**
407
+ * Check if attributes prevent template emission.
408
+ * - `key` always bails (VNode reconciliation prop)
409
+ * - Spread on inner elements bails (too complex to merge in _bind)
410
+ * - Spread on root element is allowed — applied via applyProps in _bind
411
+ */
412
+ function hasBailAttr(node, isRoot = false) {
250
413
  for (const attr of jsxAttrs(node)) {
251
- if (ts.isJsxSpreadAttribute(attr)) return true;
414
+ if (ts.isJsxSpreadAttribute(attr)) {
415
+ if (isRoot) continue;
416
+ return true;
417
+ }
252
418
  if (ts.isJsxAttribute(attr) && ts.isIdentifier(attr.name) && attr.name.text === "key") return true;
253
419
  }
254
420
  return false;
@@ -271,10 +437,10 @@ function transformJSX(code, filename = "input.tsx") {
271
437
  * Count DOM elements in a JSX subtree. Returns -1 if the tree is not
272
438
  * eligible for template emission.
273
439
  */
274
- function templateElementCount(node) {
440
+ function templateElementCount(node, isRoot = false) {
275
441
  const tag = jsxTagName(node);
276
442
  if (!tag || !isLowerCase(tag)) return -1;
277
- if (hasBailAttr(node)) return -1;
443
+ if (hasBailAttr(node, isRoot)) return -1;
278
444
  if (!ts.isJsxElement(node)) return 1;
279
445
  let count = 1;
280
446
  for (const child of node.children) {
@@ -306,6 +472,7 @@ function transformJSX(code, filename = "input.tsx") {
306
472
  const reactiveBindExprs = [];
307
473
  let needsBindTextImport = false;
308
474
  let needsBindDirectImport = false;
475
+ let needsApplyPropsImport = false;
309
476
  function nextVar() {
310
477
  return `__e${varIdx++}`;
311
478
  }
@@ -376,7 +543,7 @@ function transformJSX(code, filename = "input.tsx") {
376
543
  };
377
544
  return {
378
545
  expr: sliceExpr(exprNode),
379
- isReactive: containsCall(exprNode)
546
+ isReactive: isDynamic(exprNode)
380
547
  };
381
548
  }
382
549
  /** Build a setter expression for an attribute. */
@@ -434,6 +601,13 @@ function transformJSX(code, filename = "input.tsx") {
434
601
  }
435
602
  /** Process a single attribute, returning HTML to append. */
436
603
  function processOneAttr(attr, varName) {
604
+ if (ts.isJsxSpreadAttribute(attr)) {
605
+ const expr = sliceExpr(attr.expression);
606
+ needsApplyPropsImport = true;
607
+ if (isDynamic(attr.expression)) reactiveBindExprs.push(`_applyProps(${varName}, ${expr})`);
608
+ else bindLines.push(`_applyProps(${varName}, ${expr})`);
609
+ return "";
610
+ }
437
611
  if (!ts.isJsxAttribute(attr)) return "";
438
612
  const attrName = ts.isIdentifier(attr.name) ? attr.name.text : "";
439
613
  if (attrName === "key") return "";
@@ -457,7 +631,11 @@ function transformJSX(code, filename = "input.tsx") {
457
631
  needsBindTextImport = true;
458
632
  const d = nextDisp();
459
633
  bindLines.push(`const ${d} = _bindText(${directRef}, ${tVar})`);
460
- } else reactiveBindExprs.push(`${tVar}.data = ${expr}`);
634
+ } else {
635
+ needsBindImportGlobal = true;
636
+ const d = nextDisp();
637
+ bindLines.push(`const ${d} = _bind(() => { ${tVar}.data = ${inlineVarsInText(expr)} })`);
638
+ }
461
639
  return needsPlaceholder ? "<!>" : "";
462
640
  }
463
641
  /** Emit bind lines for a static text expression child. */
@@ -516,11 +694,12 @@ function transformJSX(code, filename = "input.tsx") {
516
694
  if (html === null) return null;
517
695
  if (needsBindTextImport) needsBindTextImportGlobal = true;
518
696
  if (needsBindDirectImport) needsBindDirectImportGlobal = true;
697
+ if (needsApplyPropsImport) needsApplyPropsImportGlobal = true;
519
698
  const escaped = html.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
520
699
  if (reactiveBindExprs.length > 0) {
521
700
  needsBindImportGlobal = true;
522
701
  const combinedName = nextDisp();
523
- const combinedBody = reactiveBindExprs.join("; ");
702
+ const combinedBody = reactiveBindExprs.map(inlineVarsInText).join("; ");
524
703
  bindLines.push(`const ${combinedName} = _bind(() => { ${combinedBody} })`);
525
704
  }
526
705
  if (bindLines.length === 0 && disposerNames.length === 0) return `_tpl("${escaped}", () => null)`;
@@ -668,13 +847,60 @@ function isStaticChild(child) {
668
847
  function isStatic(node) {
669
848
  return ts.isStringLiteral(node) || ts.isNumericLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node) || node.kind === ts.SyntaxKind.TrueKeyword || node.kind === ts.SyntaxKind.FalseKeyword || node.kind === ts.SyntaxKind.NullKeyword || node.kind === ts.SyntaxKind.UndefinedKeyword;
670
849
  }
671
- function shouldWrap(node) {
672
- if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) return false;
673
- if (isStatic(node)) return false;
674
- return containsCall(node);
850
+ /** Known pure global functions that don't read signals. */
851
+ const PURE_CALLS = new Set([
852
+ "Math.max",
853
+ "Math.min",
854
+ "Math.abs",
855
+ "Math.floor",
856
+ "Math.ceil",
857
+ "Math.round",
858
+ "Math.pow",
859
+ "Math.sqrt",
860
+ "Math.random",
861
+ "Math.trunc",
862
+ "Math.sign",
863
+ "Number.parseInt",
864
+ "Number.parseFloat",
865
+ "Number.isNaN",
866
+ "Number.isFinite",
867
+ "parseInt",
868
+ "parseFloat",
869
+ "isNaN",
870
+ "isFinite",
871
+ "String.fromCharCode",
872
+ "String.fromCodePoint",
873
+ "Object.keys",
874
+ "Object.values",
875
+ "Object.entries",
876
+ "Object.assign",
877
+ "Object.freeze",
878
+ "Object.create",
879
+ "Array.from",
880
+ "Array.isArray",
881
+ "Array.of",
882
+ "JSON.stringify",
883
+ "JSON.parse",
884
+ "encodeURIComponent",
885
+ "decodeURIComponent",
886
+ "encodeURI",
887
+ "decodeURI",
888
+ "Date.now"
889
+ ]);
890
+ /** Check if a call expression calls a known pure function with static args. */
891
+ function isPureStaticCall(node) {
892
+ const callee = node.expression;
893
+ let name = "";
894
+ if (ts.isIdentifier(callee)) name = callee.text;
895
+ else if (ts.isPropertyAccessExpression(callee) && ts.isIdentifier(callee.expression)) name = `${callee.expression.text}.${callee.name.text}`;
896
+ if (!PURE_CALLS.has(name)) return false;
897
+ return node.arguments.every((arg) => !ts.isSpreadElement(arg) && isStatic(arg));
675
898
  }
676
899
  function containsCall(node) {
677
- if (ts.isCallExpression(node)) return true;
900
+ if (ts.isCallExpression(node)) {
901
+ if (isPureStaticCall(node)) return false;
902
+ return true;
903
+ }
678
904
  if (ts.isTaggedTemplateExpression(node)) return true;
679
905
  if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) return false;
680
906
  return ts.forEachChild(node, containsCall) ?? false;