octane 0.1.30 → 0.1.32

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.
@@ -1811,6 +1811,62 @@ function collectComponentNames(ast) {
1811
1811
  return names;
1812
1812
  }
1813
1813
 
1814
+ function collectOwnerFreeThreeHostComponents(ast, state, development) {
1815
+ if (
1816
+ development ||
1817
+ state.hmr ||
1818
+ state.profile ||
1819
+ state.renderer.id !== 'three' ||
1820
+ state.renderer.module !== '@octanejs/three/renderer'
1821
+ ) {
1822
+ return null;
1823
+ }
1824
+
1825
+ const constructors = new Set();
1826
+ for (const statement of ast.body ?? []) {
1827
+ if (
1828
+ statement.type !== 'ImportDeclaration' ||
1829
+ statement.importKind === 'type' ||
1830
+ statement.source?.value !== '@octanejs/three'
1831
+ ) {
1832
+ continue;
1833
+ }
1834
+ for (const specifier of statement.specifiers ?? []) {
1835
+ if (
1836
+ specifier.type === 'ImportSpecifier' &&
1837
+ specifier.importKind !== 'type' &&
1838
+ (specifier.imported?.name ?? specifier.imported?.value) === 'extend' &&
1839
+ typeof specifier.local?.name === 'string'
1840
+ ) {
1841
+ constructors.add(specifier.local.name);
1842
+ }
1843
+ }
1844
+ }
1845
+ if (constructors.size === 0) return null;
1846
+
1847
+ const names = new Set();
1848
+ for (const statement of ast.body ?? []) {
1849
+ const declaration =
1850
+ statement.type === 'ExportNamedDeclaration' ? statement.declaration : statement;
1851
+ if (declaration?.type !== 'VariableDeclaration' || declaration.kind !== 'const') continue;
1852
+ for (const binding of declaration.declarations ?? []) {
1853
+ const call = binding.init;
1854
+ if (
1855
+ binding.id?.type === 'Identifier' &&
1856
+ call?.type === 'CallExpression' &&
1857
+ call.optional !== true &&
1858
+ call.callee?.type === 'Identifier' &&
1859
+ constructors.has(call.callee.name) &&
1860
+ call.arguments?.length === 1 &&
1861
+ call.arguments[0]?.type !== 'SpreadElement'
1862
+ ) {
1863
+ names.add(binding.id.name);
1864
+ }
1865
+ }
1866
+ }
1867
+ return names.size === 0 ? null : { names, lexical: createLexicalAnalysis(ast) };
1868
+ }
1869
+
1814
1870
  function addPatternNames(pattern, names) {
1815
1871
  if (!pattern) return;
1816
1872
  if (pattern.type === 'Identifier') {
@@ -1966,32 +2022,52 @@ function isOwnerFreeForAttribute(attribute) {
1966
2022
  );
1967
2023
  }
1968
2024
 
1969
- function ownerFreeForHost(node) {
2025
+ function ownerFreeForLeaf(node) {
1970
2026
  if (node.empty != null) return null;
1971
2027
  if (!isOwnerFreeForExpression(node.right) || !isOwnerFreeForExpression(node.key)) return null;
1972
2028
  const body = (node.body?.body ?? []).filter(
1973
2029
  (statement) => statement.type !== 'JSXText' || normalizeJsxText(statement.value ?? '') !== '',
1974
2030
  );
1975
2031
  if (body.length !== 1) return null;
1976
- const host = body[0];
2032
+ const leaf = body[0];
2033
+ if (leaf.type !== 'JSXElement' && leaf.type !== 'Element') return null;
1977
2034
  if (
1978
- (host.type !== 'JSXElement' && host.type !== 'Element') ||
1979
- isComponentElement(host) ||
1980
- jsxName(host) === 'Activity'
1981
- ) {
1982
- return null;
1983
- }
1984
- const type = jsxName(host);
1985
- if (type === null || !/^[a-z]/.test(type)) return null;
1986
- if (
1987
- (host.children ?? []).some(
2035
+ (leaf.children ?? []).some(
1988
2036
  (child) => child.type !== 'JSXText' || normalizeJsxText(child.value ?? '') !== '',
1989
2037
  )
1990
2038
  ) {
1991
2039
  return null;
1992
2040
  }
1993
- const attributes = host.openingElement?.attributes ?? host.attributes ?? [];
1994
- return attributes.every(isOwnerFreeForAttribute) ? host : null;
2041
+ const attributes = leaf.openingElement?.attributes ?? leaf.attributes ?? [];
2042
+ return attributes.every(isOwnerFreeForAttribute) ? leaf : null;
2043
+ }
2044
+
2045
+ function ownerFreeForHost(node) {
2046
+ const host = ownerFreeForLeaf(node);
2047
+ if (host === null || isComponentElement(host) || jsxName(host) === 'Activity') return null;
2048
+ const type = jsxName(host);
2049
+ return type !== null && /^[a-z]/.test(type) ? host : null;
2050
+ }
2051
+
2052
+ function ownerFreeForThreeHostComponent(node, state) {
2053
+ const trusted = state.ownerFreeThreeHostComponents;
2054
+ if (trusted == null) return null;
2055
+ const component = ownerFreeForLeaf(node);
2056
+ if (component === null || !isComponentElement(component)) return null;
2057
+ const attributes = component.openingElement?.attributes ?? component.attributes ?? [];
2058
+ const attributesSeen = new Set();
2059
+ for (const attribute of attributes) {
2060
+ const attributeKey = attributeName(attribute);
2061
+ if (attributeKey === '__proto__' || attributesSeen.has(attributeKey)) return null;
2062
+ attributesSeen.add(attributeKey);
2063
+ }
2064
+ const name = component.openingElement?.name ?? component.name;
2065
+ if (name?.type !== 'JSXIdentifier' || !trusted.names.has(name.name)) return null;
2066
+ const binding = trusted.lexical.resolveBinding(
2067
+ trusted.lexical.nodeScopes.get(name) ?? trusted.lexical.rootScope,
2068
+ name.name,
2069
+ );
2070
+ return binding?.scope === trusted.lexical.rootScope ? component : null;
1995
2071
  }
1996
2072
 
1997
2073
  function allocPlan(state, root, origin = null) {
@@ -2407,6 +2483,85 @@ function compilePlainPropsObjectAst(attributes, state, origin) {
2407
2483
  return inheritGeneratedOrigin(b.object(entries), origin);
2408
2484
  }
2409
2485
 
2486
+ /**
2487
+ * Lower a `class={[…]}` array literal to its clsx-composed string when every
2488
+ * element is statically a string or falsy.
2489
+ *
2490
+ * The runtime composes class arrays clsx-style, so `['row', on && 'danger']`
2491
+ * is only ever observed as `'row'` or `'row danger'` — but as a slot value the
2492
+ * array is rebuilt on every render, and on a transported root each rebuild is
2493
+ * re-encoded, making the hottest per-row prop a fresh allocation per render.
2494
+ * Emitting the string-building expression instead makes the slot value a
2495
+ * primitive: identity-comparable, allocation-free, and byte-identical in the
2496
+ * background and main-thread programs (so first-screen adoption still sees
2497
+ * the same tree). An all-literal array folds further, into a static plan prop
2498
+ * with no slot at all. Anything not statically string-or-falsy (spreads,
2499
+ * nested arrays, objects, expressions with non-literal truthy arms) keeps the
2500
+ * authored array slot and the runtime's general composition.
2501
+ *
2502
+ * Returns `{ staticValue }` for a fully static class, `{ expression }` for a
2503
+ * string-building lowering, or `null` to keep the authored value.
2504
+ */
2505
+ function loweredHostClassAst(expression, state) {
2506
+ if (expression.type !== 'ArrayExpression') return null;
2507
+ const elements = expression.elements ?? [];
2508
+ if (elements.length === 0) return { staticValue: '' };
2509
+ /** @type {({ static: string } | { test: any, value: string })[]} */
2510
+ const parts = [];
2511
+ for (const element of elements) {
2512
+ if (element == null || element.type === 'SpreadElement') return null;
2513
+ if (element.type === 'Literal' && typeof element.value === 'string') {
2514
+ if (element.value !== '') parts.push({ static: element.value });
2515
+ continue;
2516
+ }
2517
+ if (
2518
+ element.type === 'LogicalExpression' &&
2519
+ element.operator === '&&' &&
2520
+ element.right.type === 'Literal' &&
2521
+ typeof element.right.value === 'string' &&
2522
+ element.right.value !== ''
2523
+ ) {
2524
+ parts.push({ test: element.left, value: element.right.value });
2525
+ continue;
2526
+ }
2527
+ return null;
2528
+ }
2529
+ // The first part must be a literal so every later piece can join with an
2530
+ // unconditional leading space.
2531
+ if (parts.length !== 0 && parts[0].static === undefined) return null;
2532
+ let composed = null;
2533
+ let pendingStatic = '';
2534
+ const flushStatic = () => {
2535
+ if (pendingStatic === '') return;
2536
+ const literal = b.literal(pendingStatic, JSON.stringify(pendingStatic));
2537
+ composed = composed === null ? literal : b.binary('+', composed, literal);
2538
+ pendingStatic = '';
2539
+ };
2540
+ for (const part of parts) {
2541
+ if (part.static !== undefined) {
2542
+ pendingStatic += pendingStatic === '' && composed === null ? part.static : ` ${part.static}`;
2543
+ continue;
2544
+ }
2545
+ flushStatic();
2546
+ const spaced = ` ${part.value}`;
2547
+ composed = b.binary(
2548
+ '+',
2549
+ composed,
2550
+ inheritGeneratedOrigin(
2551
+ b.conditional(
2552
+ dynamicExpressionAst(part.test, state),
2553
+ b.literal(spaced, JSON.stringify(spaced)),
2554
+ b.literal('', '""'),
2555
+ ),
2556
+ part.test,
2557
+ ),
2558
+ );
2559
+ }
2560
+ if (composed === null) return { staticValue: pendingStatic };
2561
+ flushStatic();
2562
+ return { expression: inheritGeneratedOrigin(composed, expression) };
2563
+ }
2564
+
2410
2565
  function compileAttributeAst(attribute, context, state, canonicalizeHostClass) {
2411
2566
  if (attribute.type === 'JSXSpreadAttribute' || attribute.type === 'SpreadAttribute') {
2412
2567
  throw universalError(
@@ -2428,6 +2583,18 @@ function compileAttributeAst(attribute, context, state, canonicalizeHostClass) {
2428
2583
  if (value.type === 'Literal') return { name, staticValue: value.value };
2429
2584
  if (value.type === 'JSXExpressionContainer') {
2430
2585
  if (!value.expression || value.expression.type === 'JSXEmptyExpression') return null;
2586
+ if (name === 'class') {
2587
+ const lowered = loweredHostClassAst(value.expression, state);
2588
+ if (lowered !== null) {
2589
+ if (lowered.expression === undefined) return { name, staticValue: lowered.staticValue };
2590
+ const slot = context.values.length;
2591
+ context.values.push(lowered.expression);
2592
+ return { name, slot };
2593
+ }
2594
+ }
2595
+ if (value.expression.type === 'Literal' && isStaticPropLiteral(value.expression.value)) {
2596
+ return { name, staticValue: value.expression.value };
2597
+ }
2431
2598
  const slot = context.values.length;
2432
2599
  context.values.push(mainThreadHostValueAst(name, value.expression, state));
2433
2600
  return { name, slot };
@@ -2435,6 +2602,15 @@ function compileAttributeAst(attribute, context, state, canonicalizeHostClass) {
2435
2602
  throw universalError(state.filename, attribute, `unsupported value for host attribute ${name}.`);
2436
2603
  }
2437
2604
 
2605
+ /**
2606
+ * Literal values that can ride the frozen plan instead of a per-render slot.
2607
+ * `null` stays a slot: it is a legal "no handler" value for event props, whose
2608
+ * erasure in main-thread first-screen programs happens at the slot site.
2609
+ */
2610
+ function isStaticPropLiteral(value) {
2611
+ return typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean';
2612
+ }
2613
+
2438
2614
  function addDynamicAst(context, expression) {
2439
2615
  const slot = context.values.length;
2440
2616
  context.values.push(expression);
@@ -2710,6 +2886,55 @@ function rewriteSetupStatementAst(statement, state) {
2710
2886
  return rewritten === null ? [] : [rewritten];
2711
2887
  }
2712
2888
 
2889
+ function compileOwnerFreeForHostAst(host, state, itemBinding, indexBinding) {
2890
+ const context = { values: [] };
2891
+ const plan = allocPlan(state, compileHostElementAst(host, context, state), host);
2892
+ return {
2893
+ plan: generatedIdentifier(plan, host),
2894
+ render: generatedArrow(
2895
+ [itemBinding, indexBinding],
2896
+ inheritGeneratedOrigin(b.array(context.values), host),
2897
+ host,
2898
+ ),
2899
+ };
2900
+ }
2901
+
2902
+ function compileOwnerFreeThreeHostComponentAst(component, state, itemBinding, indexBinding) {
2903
+ const attributes = component.openingElement?.attributes ?? component.attributes ?? [];
2904
+ const names = [];
2905
+ const values = [];
2906
+ for (const attribute of attributes) {
2907
+ const name = attributeName(attribute);
2908
+ const value = attribute.value;
2909
+ const expression =
2910
+ value === null
2911
+ ? inheritGeneratedOrigin(b.literal(true), attribute)
2912
+ : value.type === 'Literal'
2913
+ ? inheritGeneratedOrigin(b.literal(value.value), value)
2914
+ : mainThreadHostValueAst(name, value.expression, state);
2915
+ names.push(name);
2916
+ values.push(expression);
2917
+ }
2918
+ const helper = (state.helpers.hostComponentLeafPlan ??= allocName(
2919
+ state,
2920
+ '__octaneUniversalHostComponentLeafPlan',
2921
+ ));
2922
+ const signature = JSON.stringify(names);
2923
+ return {
2924
+ plan: generatedCall(
2925
+ helper,
2926
+ [b.literal(state.renderer.id), jsxNameExpressionAst(component, state), b.literal(signature)],
2927
+ component,
2928
+ ),
2929
+ render: generatedArrow(
2930
+ [itemBinding, indexBinding],
2931
+ inheritGeneratedOrigin(b.array(values), component),
2932
+ component,
2933
+ ),
2934
+ signature: inheritGeneratedOrigin(b.literal(signature), component),
2935
+ };
2936
+ }
2937
+
2713
2938
  function compileForAst(node, context, state) {
2714
2939
  if (node.await) {
2715
2940
  throw universalError(
@@ -2730,18 +2955,43 @@ function compileForAst(node, context, state) {
2730
2955
  node.index ?? generatedIdentifier(allocName(state, '__octaneUniversalIndex'), node);
2731
2956
  assertNoResidualTemplate(node.right, state, '@for source');
2732
2957
  assertNoResidualTemplate(node.key, state, '@for key');
2958
+ const host = !state.hmr ? ownerFreeForHost(node) : null;
2959
+ const component = host === null ? ownerFreeForThreeHostComponent(node, state) : null;
2960
+ const compactHost =
2961
+ host === null ? null : compileOwnerFreeForHostAst(host, state, itemBinding, indexBinding);
2962
+ const compactComponent =
2963
+ component === null
2964
+ ? null
2965
+ : compileOwnerFreeThreeHostComponentAst(component, state, itemBinding, indexBinding);
2733
2966
  const args = [
2734
2967
  rewriteSourceAst(node.right, state),
2735
2968
  generatedArrow([itemBinding, indexBinding], rewriteSourceAst(node.key, state), node.key),
2736
- compileBlockValueAst(
2737
- node.body?.body ?? [],
2738
- state,
2739
- [itemBinding, indexBinding],
2740
- node.body ?? node,
2741
- ),
2969
+ compactHost?.render ??
2970
+ compactComponent?.render ??
2971
+ compileBlockValueAst(
2972
+ node.body?.body ?? [],
2973
+ state,
2974
+ [itemBinding, indexBinding],
2975
+ node.body ?? node,
2976
+ ),
2742
2977
  ];
2743
- if (!state.hmr && ownerFreeForHost(node) !== null) {
2744
- args.push(b.literal(null, 'null'), b.literal(true), b.literal(true));
2978
+ if (host !== null) {
2979
+ args.push(
2980
+ b.literal(null, 'null'),
2981
+ b.literal(true),
2982
+ b.literal(true),
2983
+ inheritGeneratedOrigin(b.unary('void', b.literal(0)), host),
2984
+ compactHost.plan,
2985
+ );
2986
+ } else if (component !== null) {
2987
+ args.push(
2988
+ b.literal(null, 'null'),
2989
+ b.literal(true),
2990
+ b.literal(true),
2991
+ jsxNameExpressionAst(component, state),
2992
+ compactComponent.plan,
2993
+ compactComponent.signature,
2994
+ );
2745
2995
  } else if (node.empty) {
2746
2996
  args.push(compileBlockValueAst(node.empty?.body ?? [], state, [], node.empty));
2747
2997
  }
@@ -2828,6 +3078,16 @@ function compileChildAst(node, context, state) {
2828
3078
  }
2829
3079
  if (node.type === 'JSXExpressionContainer') {
2830
3080
  if (!node.expression || node.expression.type === 'JSXEmptyExpression') return [];
3081
+ // A string-literal child is authored text with braces around it: fold it
3082
+ // into the plan like JSXText, so the constant stops riding every render's
3083
+ // slot array. Renderers without host text keep the renderable-hole slot.
3084
+ if (
3085
+ node.expression.type === 'Literal' &&
3086
+ typeof node.expression.value === 'string' &&
3087
+ state.renderer.text === 'host'
3088
+ ) {
3089
+ return [withPlanOrigin({ kind: 'text', value: node.expression.value }, node)];
3090
+ }
2831
3091
  return [addDynamicAst(context, dynamicExpressionAst(node.expression, state))];
2832
3092
  }
2833
3093
  if (node.type === 'JSXElement' || node.type === 'Element') {
@@ -3048,6 +3308,9 @@ function universalHelperImportAst(state, extraPairs = [], origin = null) {
3048
3308
  ['universalPlan', state.helpers.plan],
3049
3309
  ['universalValue', state.helpers.value],
3050
3310
  ['universalComponent', state.helpers.nestedComponent],
3311
+ ...(state.helpers.hostComponentLeafPlan === undefined
3312
+ ? []
3313
+ : [['universalHostComponentLeafPlan', state.helpers.hostComponentLeafPlan]]),
3051
3314
  ['universalProps', state.helpers.props],
3052
3315
  ['universalIf', state.helpers.if],
3053
3316
  ['universalSwitch', state.helpers.switch],
@@ -3696,6 +3959,11 @@ export function compileUniversal(
3696
3959
  componentNames: collectComponentNames(ast),
3697
3960
  runtimeImports: new Map(),
3698
3961
  };
3962
+ state.ownerFreeThreeHostComponents = collectOwnerFreeThreeHostComponents(
3963
+ ast,
3964
+ state,
3965
+ options.dev === true,
3966
+ );
3699
3967
  state.helpers.component = allocName(state, '__octaneDefineUniversalComponent');
3700
3968
  state.helpers.plan = allocName(state, '__octaneUniversalPlan');
3701
3969
  state.helpers.value = allocName(state, '__octaneUniversalValue');