octane 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.
Files changed (59) hide show
  1. package/dist/cjs/compatibility.cjs +35 -0
  2. package/dist/cjs/component-flags.cjs +8 -1
  3. package/dist/cjs/css.cjs +9 -0
  4. package/dist/cjs/dom-tables.cjs +8 -1
  5. package/dist/cjs/error-codes.client.generated.cjs +15 -2
  6. package/dist/cjs/error-codes.server.generated.cjs +5 -0
  7. package/dist/cjs/event-names.cjs +39 -0
  8. package/dist/cjs/index.cjs +13 -0
  9. package/dist/cjs/runtime.cjs +1345 -359
  10. package/dist/cjs/runtime.server.cjs +620 -183
  11. package/dist/cjs/server/index.cjs +26 -0
  12. package/dist/cjs/version.cjs +1 -1
  13. package/dist/compatibility.d.ts +7 -0
  14. package/dist/compatibility.js +10 -0
  15. package/dist/compiler/bundler.js +20 -30
  16. package/dist/compiler/compile-renderer-boundaries.js +1 -0
  17. package/dist/compiler/compile-universal.js +3 -0
  18. package/dist/compiler/compile.js +877 -627
  19. package/dist/compiler/hook-deps.js +100 -3
  20. package/dist/compiler/hook-names.js +1 -0
  21. package/dist/compiler/hydrate-boundaries.js +165 -15
  22. package/dist/compiler/manual-hooks.js +171 -0
  23. package/dist/compiler/parser.node.js +34 -8
  24. package/dist/compiler/plain-hook-memo.js +54 -5
  25. package/dist/compiler/slot-hooks.d.ts +43 -0
  26. package/dist/compiler/slot-hooks.js +154 -35
  27. package/dist/compiler/strong-mode.js +836 -3
  28. package/dist/compiler/style-scopes.js +1134 -0
  29. package/dist/compiler/volar.LICENSES.txt +1 -1
  30. package/dist/compiler/volar.js +4543 -2839
  31. package/dist/component-flags.d.ts +1 -1
  32. package/dist/component-flags.js +8 -1
  33. package/dist/css.d.ts +7 -0
  34. package/dist/css.js +8 -0
  35. package/dist/dom-tables.js +8 -1
  36. package/dist/error-codes.client.generated.d.ts +3 -1
  37. package/dist/error-codes.client.generated.js +15 -2
  38. package/dist/error-codes.server.generated.d.ts +1 -0
  39. package/dist/error-codes.server.generated.js +5 -0
  40. package/dist/event-names.d.ts +1 -0
  41. package/dist/event-names.js +15 -0
  42. package/dist/html-tree-validation.d.ts +17 -34
  43. package/dist/index.d.ts +3 -1
  44. package/dist/index.js +12 -1
  45. package/dist/jsx-runtime.d.ts +6 -1
  46. package/dist/public-types.d.ts +188 -0
  47. package/dist/public-types.js +0 -0
  48. package/dist/runtime.d.ts +128 -69
  49. package/dist/runtime.js +1349 -360
  50. package/dist/runtime.server.d.ts +67 -15
  51. package/dist/runtime.server.js +619 -184
  52. package/dist/server/index.d.ts +4 -2
  53. package/dist/server/index.js +23 -0
  54. package/dist/static/index.d.ts +2 -1
  55. package/dist/static/index.js +3 -1
  56. package/dist/universal-core.d.ts +8 -1
  57. package/dist/universal-core.js +95 -9
  58. package/dist/version.js +1 -1
  59. package/package.json +4 -3
@@ -24,6 +24,7 @@ const STABLE_TUPLE_RESULTS = new Map([
24
24
  ['useReducer', new Set([1, 2])],
25
25
  ['useTransition', new Set([1])],
26
26
  ['useActionState', new Set([1])],
27
+ ['useFormState', new Set([1])],
27
28
  ['useOptimistic', new Set([1])],
28
29
  ]);
29
30
 
@@ -57,6 +58,7 @@ function declareName(scope, name, details = null) {
57
58
  octaneNamespace: false,
58
59
  hookRuntimeImport: null,
59
60
  hookRuntimeNamespace: false,
61
+ customHookImport: null,
60
62
  };
61
63
  scope.bindings.set(name, binding);
62
64
  }
@@ -66,6 +68,7 @@ function declareName(scope, name, details = null) {
66
68
  if (details?.octaneNamespace) binding.octaneNamespace = true;
67
69
  if (details?.hookRuntimeImport) binding.hookRuntimeImport = details.hookRuntimeImport;
68
70
  if (details?.hookRuntimeNamespace) binding.hookRuntimeNamespace = true;
71
+ if (details?.customHookImport) binding.customHookImport = details.customHookImport;
69
72
  return binding;
70
73
  }
71
74
 
@@ -133,6 +136,13 @@ function predeclareDirect(statements, scope, hookRuntimeModules, bindingsOnly =
133
136
  octaneNamespace: isOctane && specifier.type === 'ImportNamespaceSpecifier',
134
137
  hookRuntimeImport: isHookRuntime ? imported : null,
135
138
  hookRuntimeNamespace: isHookRuntime && specifier.type === 'ImportNamespaceSpecifier',
139
+ customHookImport:
140
+ !isHookRuntime &&
141
+ original.importKind !== 'type' &&
142
+ specifier.importKind !== 'type' &&
143
+ (/^use[A-Z]/.test(imported ?? '') || /^use[A-Z]/.test(specifier.local.name))
144
+ ? (imported ?? specifier.local.name)
145
+ : null,
136
146
  });
137
147
  }
138
148
  continue;
@@ -579,10 +589,20 @@ function buildScopes(ast, onlyImported, hookRuntimeModules, bindingsOnly = false
579
589
  const name = canonicalHookName(node, scope, onlyImported);
580
590
  const config = DEPENDENCY_HOOKS.get(name);
581
591
  const hookRuntimeImportedName = canonicalHookName(node, scope, true);
582
- if (octaneImportedName !== null || unboundCallee || hookRuntimeImportedName !== null) {
592
+ const customHookImport =
593
+ node.optional !== true && callee?.type === 'Identifier'
594
+ ? resolveBinding(scope, callee.name)?.customHookImport
595
+ : null;
596
+ if (
597
+ octaneImportedName !== null ||
598
+ unboundCallee ||
599
+ hookRuntimeImportedName !== null ||
600
+ customHookImport
601
+ ) {
583
602
  const props = {};
584
603
  if (octaneImportedName !== null) props._octaneImportedHook = octaneImportedName;
585
604
  if (unboundCallee) props._octaneUnboundCallee = true;
605
+ if (customHookImport) props._octaneCustomHookCall = customHookImport;
586
606
  if (octaneImportedName === null && hookRuntimeImportedName !== null) {
587
607
  props._octaneHookRuntimeImportedHook = hookRuntimeImportedName;
588
608
  }
@@ -665,8 +685,9 @@ function buildScopes(ast, onlyImported, hookRuntimeModules, bindingsOnly = false
665
685
  declarationBindings,
666
686
  };
667
687
  if (bindingsOnly) return analysis;
668
- // The surgical plain-TS pass slots base hooks only; without a custom-hook
669
- // withSlot boundary, two local wrapper calls would share their inner slots.
688
+ if (onlyImported) annotateLocalHookAliases(analysis);
689
+ // The plain-TS pass slots base hooks and imported/aliased hook values. Local
690
+ // custom-hook functions still own their authored slot boundaries.
670
691
  // Restrict custom-call inference to the full TSRX/TSX compiler, which emits
671
692
  // that boundary for every plain-identifier custom hook call.
672
693
  const customHooks = onlyImported ? new Map() : discoverCustomDependencyHooks(analysis);
@@ -687,6 +708,82 @@ function buildScopes(ast, onlyImported, hookRuntimeModules, bindingsOnly = false
687
708
  return analysis;
688
709
  }
689
710
 
711
+ // An immutable alias may select a base hook directly, or a zero-argument local
712
+ // factory may return that selection (the usual isomorphic-effect pattern).
713
+ // Follow only value aliases, never the body of an ordinary custom hook. This
714
+ // proof needs the completed lexical graph so declaration order and shadowing
715
+ // cannot change a call's identity.
716
+ function annotateLocalHookAliases(analysis) {
717
+ // Ordinary modules with only direct imported hooks need no alias tables.
718
+ if (
719
+ !analysis.calls.some(({ call, scope }) => {
720
+ const binding = directCallBinding(call, scope);
721
+ return binding !== null && !binding.imported;
722
+ })
723
+ )
724
+ return;
725
+ const values = new Map();
726
+ const factories = new Map();
727
+ for (const { decl, bindings, kind } of analysis.declarators) {
728
+ if (kind !== 'const' || decl.id.type !== 'Identifier') continue;
729
+ const binding = bindings[0]?.binding;
730
+ if (binding && !binding.reassigned) values.set(binding, unwrapValue(decl.init));
731
+ }
732
+ if (values.size === 0) return;
733
+ for (const record of analysis.functions) {
734
+ if (record.binding && record.stableDefinition && !record.binding.reassigned) {
735
+ factories.set(record.binding, record.node);
736
+ }
737
+ }
738
+ function referencesHook(node, seen) {
739
+ const value = unwrapValue(node);
740
+ if (!value) return false;
741
+ if (value.type === 'Identifier') {
742
+ const binding = resolveBinding(analysis.nodeScopes.get(value), value.name);
743
+ if (!binding || binding.reassigned || seen.has(binding)) return false;
744
+ if (/^use[A-Z]/.test(binding.hookRuntimeImport ?? '') || binding.customHookImport)
745
+ return true;
746
+ seen.add(binding);
747
+ return referencesHook(values.get(binding), seen);
748
+ }
749
+ if (value.type === 'ConditionalExpression') {
750
+ return referencesHook(value.consequent, seen) || referencesHook(value.alternate, seen);
751
+ }
752
+ if (value.type === 'LogicalExpression') {
753
+ return referencesHook(value.left, seen) || referencesHook(value.right, seen);
754
+ }
755
+ if (
756
+ value.type === 'CallExpression' &&
757
+ value.optional !== true &&
758
+ value.arguments.length === 0
759
+ ) {
760
+ const binding = directCallBinding(value, analysis.nodeScopes.get(value));
761
+ const factory = factories.get(binding);
762
+ if (!factory || factory.params.length !== 0 || seen.has(binding)) return false;
763
+ seen.add(binding);
764
+ const body = factory.body;
765
+ return referencesHook(
766
+ body.type === 'BlockStatement'
767
+ ? body.body.length === 1 && body.body[0].type === 'ReturnStatement'
768
+ ? body.body[0].argument
769
+ : null
770
+ : body,
771
+ seen,
772
+ );
773
+ }
774
+ return false;
775
+ }
776
+ for (const { call, scope } of analysis.calls) {
777
+ if (call.optional === true || call.callee.type !== 'Identifier') continue;
778
+ const binding = directCallBinding(call, scope);
779
+ if (!values.has(binding) || !referencesHook(values.get(binding), new Set([binding]))) continue;
780
+ analysis.callAnnotations.set(call, {
781
+ ...analysis.callAnnotations.get(call),
782
+ _octaneCustomHookCall: binding.name,
783
+ });
784
+ }
785
+ }
786
+
690
787
  function collectPatternBindings(pattern, scope, into) {
691
788
  if (!pattern) return;
692
789
  if (pattern.type === 'Identifier') {
@@ -17,6 +17,7 @@ export const HOOK_NAMES = new Set([
17
17
  'useSyncExternalStore',
18
18
  // React 19 Actions bundle.
19
19
  'useActionState',
20
+ 'useFormState',
20
21
  'useFormStatus',
21
22
  'useOptimistic',
22
23
  ]);
@@ -30,7 +30,14 @@ function inheritGeneratedOrigin(root, origin) {
30
30
  for (const item of value) visit(item);
31
31
  return;
32
32
  }
33
- if (typeof value.type === 'string' && value.loc == null && origin?.loc != null) {
33
+ // Adopted parser nodes (including StyleSheet subtrees) may be frozen
34
+ // and already carry CSS-relative positions; only stamp generated nodes.
35
+ if (
36
+ typeof value.type === 'string' &&
37
+ value.loc == null &&
38
+ origin?.loc != null &&
39
+ !Object.isFrozen(value)
40
+ ) {
34
41
  value.start = origin.start;
35
42
  value.end = origin.end;
36
43
  value.loc = origin.loc;
@@ -440,6 +447,161 @@ function isHookCall(node, hookNames) {
440
447
  );
441
448
  }
442
449
 
450
+ function collectSubtreeNodes(value, output) {
451
+ if (!value || typeof value !== 'object') return;
452
+ if (Array.isArray(value)) {
453
+ for (const child of value) collectSubtreeNodes(child, output);
454
+ return;
455
+ }
456
+ if (output.has(value)) return;
457
+ output.add(value);
458
+ for (const [key, child] of Object.entries(value)) {
459
+ if (!SKIP_KEYS.has(key)) collectSubtreeNodes(child, output);
460
+ }
461
+ }
462
+
463
+ function subtreeContainsNode(root, target) {
464
+ if (root === target) return true;
465
+ const seen = new WeakSet();
466
+ const visit = (value) => {
467
+ if (!value || typeof value !== 'object') return false;
468
+ if (Array.isArray(value)) {
469
+ for (const child of value) {
470
+ if (visit(child)) return true;
471
+ }
472
+ return false;
473
+ }
474
+ if (seen.has(value)) return false;
475
+ seen.add(value);
476
+ if (value === target) return true;
477
+ for (const [key, child] of Object.entries(value)) {
478
+ if (!SKIP_KEYS.has(key) && visit(child)) return true;
479
+ }
480
+ return false;
481
+ };
482
+ return visit(root);
483
+ }
484
+
485
+ function collectOwnStyleBlocks(nodes) {
486
+ const blocks = [];
487
+ for (const node of nodes ?? []) {
488
+ if (node?.type === 'JSXStyleElement' && !isFloatStyleResource(node)) blocks.push(node);
489
+ }
490
+ return blocks;
491
+ }
492
+
493
+ function isFloatStyleResource(node) {
494
+ let hasHref = false;
495
+ let hasPrecedence = false;
496
+ for (const attribute of node.openingElement?.attributes ?? []) {
497
+ if (attribute?.type !== 'JSXAttribute' && attribute?.type !== 'Attribute') continue;
498
+ const name = jsxAttributeName(attribute);
499
+ if (name === 'href') hasHref = true;
500
+ else if (name === 'precedence') hasPrecedence = true;
501
+ }
502
+ return hasHref && hasPrecedence;
503
+ }
504
+
505
+ function isStampableHost(node) {
506
+ if (node?.type !== 'JSXElement' && node?.type !== 'Element') return false;
507
+ if (node.metadata?.dynamicElement) return true;
508
+ const name = node.openingElement?.name ?? node.name;
509
+ if (!name) return false;
510
+ if (name.type === 'JSXIdentifier' || name.type === 'Identifier') {
511
+ return name.name !== 'style' && !/^[A-Z]/.test(name.name);
512
+ }
513
+ // Namespaced hosts (`svg:rect`) are stamped by the style-scope pass;
514
+ // member expressions are composites and are not.
515
+ return name.type === 'JSXNamespacedName' || name.type === 'NamespacedName';
516
+ }
517
+
518
+ function walkStampableHosts(node, visitHost) {
519
+ if (!node || typeof node !== 'object') return;
520
+ if (Array.isArray(node)) {
521
+ for (const child of node) walkStampableHosts(child, visitHost);
522
+ return;
523
+ }
524
+ if (isFunction(node)) return;
525
+ if (isStampableHost(node)) visitHost(node);
526
+ for (const [key, child] of Object.entries(node)) {
527
+ if (!SKIP_KEYS.has(key)) walkStampableHosts(child, visitHost);
528
+ }
529
+ }
530
+
531
+ /**
532
+ * A children list that holds standalone `<style>` blocks is one lexical style
533
+ * scope (plan S8.5 / RFC sibling scopes). Extraction is safe when that whole
534
+ * scope — its blocks and the host elements they stamp — sits inside the split
535
+ * boundary, because both compiles keep the authored-position hash. A scope
536
+ * that has blocks or stamped hosts on both sides of the boundary would emit
537
+ * disagreeing class lists; that is still `OCTANE_HYDRATE_SPLIT_STYLE`. Styles
538
+ * nested in functions never joined the enclosing scopes and are not checked.
539
+ */
540
+ function styleScopeStraddlesBoundary(list, own, inside) {
541
+ let hasInside = false;
542
+ let hasOutside = false;
543
+ const mark = (node) => {
544
+ if (inside.has(node)) hasInside = true;
545
+ else hasOutside = true;
546
+ };
547
+ for (const block of own) mark(block);
548
+ for (const item of list) {
549
+ if (own.includes(item)) continue;
550
+ walkStampableHosts(item, mark);
551
+ }
552
+ return hasInside && hasOutside;
553
+ }
554
+
555
+ function assertHydrateStyleScopes(boundary, filename) {
556
+ const inside = new WeakSet();
557
+ collectSubtreeNodes(boundary.node.children, inside);
558
+ const seenLists = new WeakSet();
559
+ const seen = new WeakSet();
560
+ const visit = (node, inFunction) => {
561
+ if (!node || typeof node !== 'object') return;
562
+ if (Array.isArray(node)) {
563
+ for (const child of node) visit(child, inFunction);
564
+ return;
565
+ }
566
+ if (seen.has(node)) return;
567
+ seen.add(node);
568
+ if (TRANSPARENT_TS_EXPRESSIONS.has(node.type)) {
569
+ visit(node.expression, inFunction);
570
+ return;
571
+ }
572
+ if (!inFunction && isFunction(node)) {
573
+ const nested = !subtreeContainsNode(node, boundary.node);
574
+ for (const [key, child] of Object.entries(node)) {
575
+ if (!SKIP_KEYS.has(key)) visit(child, nested);
576
+ }
577
+ return;
578
+ }
579
+ if (
580
+ !inFunction &&
581
+ (node.type === 'JSXElement' || node.type === 'JSXFragment' || node.type === 'Element') &&
582
+ Array.isArray(node.children)
583
+ ) {
584
+ const own = collectOwnStyleBlocks(node.children);
585
+ if (own.length > 0 && !seenLists.has(node.children)) {
586
+ seenLists.add(node.children);
587
+ if (styleScopeStraddlesBoundary(node.children, own, inside)) {
588
+ const reported = own.find((block) => inside.has(block)) ?? own[0];
589
+ throw extractionError(
590
+ 'OCTANE_HYDRATE_SPLIT_STYLE',
591
+ filename,
592
+ reported,
593
+ 'a scoped <style> cannot straddle a split Hydrate boundary — its style scope has blocks or stamped elements on both sides. Keep the whole scope inside the boundary, move it entirely outside, extract a child component, or set `split={false}`',
594
+ );
595
+ }
596
+ }
597
+ }
598
+ for (const [key, child] of Object.entries(node)) {
599
+ if (!SKIP_KEYS.has(key)) visit(child, inFunction);
600
+ }
601
+ };
602
+ visit(boundary.ast ?? boundary.node, false);
603
+ }
604
+
443
605
  function validateBoundary(boundary, filename, hookNames) {
444
606
  for (const child of boundary.node.children ?? []) {
445
607
  if (child?.type === 'JSXExpressionContainer' && isFunction(child.expression)) {
@@ -489,20 +651,6 @@ function validateBoundary(boundary, filename, hookNames) {
489
651
  'direct hook calls cannot move into a split child. Call the hook in a child component or set `split={false}`',
490
652
  );
491
653
  }
492
- // `directHooks` also marks the owning component's direct render scope: a
493
- // scoped <style> there belongs to that component's single style scope,
494
- // which extraction would tear in half (the server annotates the whole
495
- // component with one scope hash; the split chunk would compile the sheet
496
- // under another). Styles nested inside functions never joined the
497
- // component scope, so they move freely.
498
- if (directHooks && node.type === 'JSXStyleElement') {
499
- throw extractionError(
500
- 'OCTANE_HYDRATE_SPLIT_STYLE',
501
- filename,
502
- node,
503
- 'a scoped <style> cannot move into a split child — its rules belong to the owning component’s style scope. Move the <style> outside the boundary, into a child component, or set `split={false}`',
504
- );
505
- }
506
654
  if (isFunction(node)) {
507
655
  // A nested function keeps hook calls in its own invocation. Ordinary
508
656
  // functions also bind their own receiver; arrows still capture `this`
@@ -530,6 +678,7 @@ function validateBoundary(boundary, filename, hookNames) {
530
678
  }
531
679
  };
532
680
  visit(boundary.node.children);
681
+ assertHydrateStyleScopes(boundary, filename);
533
682
  }
534
683
 
535
684
  /** Resolve Hydrate boundaries and assign source-order paths under their nearest boundary. */
@@ -1607,6 +1756,7 @@ export function prepareHydrateBoundaries(source, filename, boundaryPath = null,
1607
1756
  for (const boundary of analysis.boundaries) {
1608
1757
  boundary.filename = filename;
1609
1758
  boundary.hookNames = analysis.imports.hookNames;
1759
+ boundary.ast = analysis.ast;
1610
1760
  }
1611
1761
  const request = sameSourceRequest(filename);
1612
1762
  const moduleMovePlan = createModuleMovePlanAst(analysis, request);
@@ -0,0 +1,171 @@
1
+ // Manual hook packages own their explicit sub-slot layout. Adapt exported or
2
+ // escaping hook definitions at their owner, so bound/forwarded imports retain
3
+ // that protocol without changing ordinary custom-hook argument lists.
4
+ import { builders as b } from '@tsrx/core';
5
+
6
+ const META = new Set([
7
+ 'loc',
8
+ 'start',
9
+ 'end',
10
+ 'range',
11
+ 'metadata',
12
+ 'parent',
13
+ 'comments',
14
+ 'leadingComments',
15
+ 'trailingComments',
16
+ 'innerComments',
17
+ ]);
18
+ const isHook = (name) =>
19
+ typeof name === 'string' && /^(?:(?:experimental|unstable|UNSTABLE)_)?use[A-Z]/.test(name);
20
+ const isFunction = (node) =>
21
+ node?.type === 'FunctionDeclaration' ||
22
+ node?.type === 'FunctionExpression' ||
23
+ node?.type === 'ArrowFunctionExpression';
24
+
25
+ function children(node, visit) {
26
+ for (const key in node) {
27
+ if (META.has(key) || key.startsWith('_octane')) continue;
28
+ const value = node[key];
29
+ if (Array.isArray(value)) for (const child of value) visit(child, node, key);
30
+ else if (value && typeof value === 'object') visit(value, node, key);
31
+ }
32
+ }
33
+
34
+ export function findManualHookProviders(ast) {
35
+ const escaping = new Set();
36
+ const definitions = new Map();
37
+ function scan(node, parent, key) {
38
+ if (!node || typeof node !== 'object') return;
39
+ if (isFunction(node) && node.body) {
40
+ const bindingName = parent?.type === 'VariableDeclarator' ? parent.id?.name : undefined;
41
+ const name = isHook(bindingName) ? bindingName : node.id?.name;
42
+ if (isHook(name)) {
43
+ // A named expression can have a private implementation name while its
44
+ // exported binding establishes the hook contract. Keep its public
45
+ // Function.name when installing the provider boundary.
46
+ definitions.set(node, { name, displayName: node.id?.name ?? name });
47
+ if (
48
+ parent?.type === 'ExportNamedDeclaration' ||
49
+ parent?.type === 'ExportDefaultDeclaration'
50
+ )
51
+ escaping.add(name);
52
+ if (parent?.type === 'ReturnStatement') escaping.add(name);
53
+ }
54
+ }
55
+ if (node.type === 'Identifier' && isHook(node.name)) {
56
+ const binding =
57
+ (isFunction(parent) && (key === 'id' || key === 'params')) ||
58
+ (parent?.type === 'VariableDeclarator' && key === 'id') ||
59
+ parent?.type === 'ImportSpecifier' ||
60
+ parent?.type === 'ImportDefaultSpecifier' ||
61
+ parent?.type === 'ImportNamespaceSpecifier';
62
+ const directCall = parent?.type === 'CallExpression' && key === 'callee';
63
+ const property =
64
+ (parent?.type === 'MemberExpression' && key === 'property' && !parent.computed) ||
65
+ (parent?.type === 'Property' && key === 'key' && !parent.computed);
66
+ if (!binding && !directCall && !property) escaping.add(node.name);
67
+ }
68
+ if (
69
+ node.type === 'ExportNamedDeclaration' &&
70
+ node.declaration?.type === 'VariableDeclaration'
71
+ ) {
72
+ for (const declaration of node.declaration.declarations) {
73
+ if (isHook(declaration.id?.name)) escaping.add(declaration.id.name);
74
+ }
75
+ }
76
+ children(node, scan);
77
+ }
78
+ scan(ast, null, null);
79
+ return new Map(
80
+ [...definitions]
81
+ .filter(([, { name }]) => escaping.has(name))
82
+ .map(([node, { displayName }]) => [node, displayName]),
83
+ );
84
+ }
85
+
86
+ function declarationOf(statement) {
87
+ return statement?.type === 'ExportNamedDeclaration' ||
88
+ statement?.type === 'ExportDefaultDeclaration'
89
+ ? statement.declaration
90
+ : statement;
91
+ }
92
+
93
+ // Only the parameters before the first default/rest contribute to Function.length.
94
+ // The public wrapper must not evaluate authored defaults or destructuring; the
95
+ // private implementation receives the original arguments after slot adaptation.
96
+ export function manualHookWrapperParameters(node) {
97
+ const parameters = [];
98
+ for (const param of node.params) {
99
+ if (param.type === 'Identifier' && param.name === 'this') continue;
100
+ if (param.type === 'AssignmentPattern' || param.type === 'RestElement') break;
101
+ parameters.push(b.id(`_$arg${parameters.length}`));
102
+ }
103
+ return parameters;
104
+ }
105
+
106
+ export function adaptManualHookProviders(ast, requireHelper, allocateName) {
107
+ const providers = findManualHookProviders(ast);
108
+ if (!providers.size) return ast;
109
+ function visit(node) {
110
+ if (!node || typeof node !== 'object') return node;
111
+ if (Array.isArray(node)) {
112
+ let changed = false;
113
+ const result = node.map((item) => {
114
+ const next = visit(item);
115
+ changed ||= next !== item;
116
+ return next;
117
+ });
118
+ return changed ? result : node;
119
+ }
120
+ let result = node;
121
+ for (const key in node) {
122
+ if (META.has(key) || key.startsWith('_octane')) continue;
123
+ const value = node[key];
124
+ if (!value || typeof value !== 'object') continue;
125
+ const next = visit(value);
126
+ if (next !== value) {
127
+ if (result === node) result = { ...node };
128
+ result[key] = next;
129
+ }
130
+ }
131
+ if (node.type === 'Program' || node.type === 'BlockStatement') {
132
+ let changed = false;
133
+ const body = result.body.flatMap((statement, index) => {
134
+ const original = declarationOf(node.body[index]);
135
+ if (original?.type !== 'FunctionDeclaration' || !providers.has(original))
136
+ return [statement];
137
+ changed = true;
138
+ const declaration = declarationOf(statement);
139
+ const implementation = allocateName(`_$manual_${original.id.name}`);
140
+ const wrapper = b.function_declaration(
141
+ original.id,
142
+ manualHookWrapperParameters(original),
143
+ b.block([
144
+ b.return(
145
+ b.call(
146
+ requireHelper('invokeManualHook'),
147
+ b.id(implementation),
148
+ b.this,
149
+ b.id('arguments'),
150
+ ),
151
+ ),
152
+ ]),
153
+ );
154
+ // Both declarations hoist. Keep the exported binding stable even for
155
+ // cyclic imports and aliases captured before the textual declaration.
156
+ return [
157
+ statement === declaration ? wrapper : { ...statement, declaration: wrapper },
158
+ { ...declaration, id: b.id(implementation) },
159
+ ];
160
+ });
161
+ if (changed) result = { ...result, body };
162
+ }
163
+ return providers.has(node) && node.type !== 'FunctionDeclaration'
164
+ ? {
165
+ ...b.call(requireHelper('manualHook'), result, b.literal(providers.get(node))),
166
+ __octanePure: true,
167
+ }
168
+ : result;
169
+ }
170
+ return visit(ast);
171
+ }
@@ -40,9 +40,41 @@ function publishOutput(options, selected) {
40
40
  }
41
41
  }
42
42
 
43
+ /**
44
+ * `<style>` with an expression child — `<style>{css}</style>`, an ordinary
45
+ * element rather than a scoped block. The compat facade reads every `<style>`
46
+ * element's raw text as CSS after its own error translation, so this shape
47
+ * surfaces as a bare `Error` from that CSS reader instead of a `SyntaxError`.
48
+ */
49
+ const STYLE_EXPRESSION_CHILD = /<style\b[^>]*>\s*\{/;
50
+
51
+ /**
52
+ * A native rejection the JavaScript parser may still accept: a `SyntaxError`
53
+ * the facade translated, or the bare `Error` its CSS reader raises for a
54
+ * `<style>` expression child. Operational failures keep their own name or
55
+ * code and stay visible, and so does any other bare `Error`: without the
56
+ * `<style …>{` shape in the source there is nothing the CSS reader could have
57
+ * rejected.
58
+ * @param {unknown} error
59
+ * @param {string} source
60
+ * @returns {boolean}
61
+ */
62
+ function isNativeSyntaxRejection(error, source) {
63
+ if (!(error instanceof Error)) return false;
64
+ if (error.name === 'ParserOperationalError') return false;
65
+ if ('code' in error && typeof error.code === 'string' && error.code.startsWith('ERR_TSRX_')) {
66
+ return false;
67
+ }
68
+ if (error instanceof SyntaxError) return true;
69
+ return (
70
+ error.name === 'Error' && error.constructor === Error && STYLE_EXPRESSION_CHILD.test(source)
71
+ );
72
+ }
73
+
43
74
  /**
44
75
  * Keep native parsing first while accepting authored syntax already supported
45
- * by the JavaScript parser, such as literal less-than text in TSRX children.
76
+ * by the JavaScript parser, such as literal less-than text in TSRX children
77
+ * or a `<style>` element with an expression child.
46
78
  * @param {string} source
47
79
  * @param {string} [filename]
48
80
  * @param {ParseOptions} [options]
@@ -56,13 +88,7 @@ export function parseModule(source, filename = 'module.tsrx', options) {
56
88
  program = parseNativeModule(source, resolvedFilename, nativeOptions);
57
89
  } catch (nativeError) {
58
90
  // Operational failures must stay visible, even if given a syntax subclass.
59
- if (
60
- !(nativeError instanceof SyntaxError) ||
61
- nativeError.name === 'ParserOperationalError' ||
62
- ('code' in nativeError &&
63
- typeof nativeError.code === 'string' &&
64
- nativeError.code.startsWith('ERR_TSRX_'))
65
- ) {
91
+ if (!isNativeSyntaxRejection(nativeError, source)) {
66
92
  publishOutput(options, nativeOptions);
67
93
  throw nativeError;
68
94
  }