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.
- package/dist/cjs/compatibility.cjs +35 -0
- package/dist/cjs/component-flags.cjs +8 -1
- package/dist/cjs/css.cjs +9 -0
- package/dist/cjs/dom-tables.cjs +8 -1
- package/dist/cjs/error-codes.client.generated.cjs +15 -2
- package/dist/cjs/error-codes.server.generated.cjs +5 -0
- package/dist/cjs/event-names.cjs +39 -0
- package/dist/cjs/index.cjs +13 -0
- package/dist/cjs/runtime.cjs +1345 -359
- package/dist/cjs/runtime.server.cjs +620 -183
- package/dist/cjs/server/index.cjs +26 -0
- package/dist/cjs/version.cjs +1 -1
- package/dist/compatibility.d.ts +7 -0
- package/dist/compatibility.js +10 -0
- package/dist/compiler/bundler.js +20 -30
- package/dist/compiler/compile-renderer-boundaries.js +1 -0
- package/dist/compiler/compile-universal.js +3 -0
- package/dist/compiler/compile.js +877 -627
- package/dist/compiler/hook-deps.js +100 -3
- package/dist/compiler/hook-names.js +1 -0
- package/dist/compiler/hydrate-boundaries.js +165 -15
- package/dist/compiler/manual-hooks.js +171 -0
- package/dist/compiler/parser.node.js +34 -8
- package/dist/compiler/plain-hook-memo.js +54 -5
- package/dist/compiler/slot-hooks.d.ts +43 -0
- package/dist/compiler/slot-hooks.js +154 -35
- package/dist/compiler/strong-mode.js +836 -3
- package/dist/compiler/style-scopes.js +1134 -0
- package/dist/compiler/volar.LICENSES.txt +1 -1
- package/dist/compiler/volar.js +4543 -2839
- package/dist/component-flags.d.ts +1 -1
- package/dist/component-flags.js +8 -1
- package/dist/css.d.ts +7 -0
- package/dist/css.js +8 -0
- package/dist/dom-tables.js +8 -1
- package/dist/error-codes.client.generated.d.ts +3 -1
- package/dist/error-codes.client.generated.js +15 -2
- package/dist/error-codes.server.generated.d.ts +1 -0
- package/dist/error-codes.server.generated.js +5 -0
- package/dist/event-names.d.ts +1 -0
- package/dist/event-names.js +15 -0
- package/dist/html-tree-validation.d.ts +17 -34
- package/dist/index.d.ts +3 -1
- package/dist/index.js +12 -1
- package/dist/jsx-runtime.d.ts +6 -1
- package/dist/public-types.d.ts +188 -0
- package/dist/public-types.js +0 -0
- package/dist/runtime.d.ts +128 -69
- package/dist/runtime.js +1349 -360
- package/dist/runtime.server.d.ts +67 -15
- package/dist/runtime.server.js +619 -184
- package/dist/server/index.d.ts +4 -2
- package/dist/server/index.js +23 -0
- package/dist/static/index.d.ts +2 -1
- package/dist/static/index.js +3 -1
- package/dist/universal-core.d.ts +8 -1
- package/dist/universal-core.js +95 -9
- package/dist/version.js +1 -1
- 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
|
-
|
|
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
|
-
|
|
669
|
-
//
|
|
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') {
|
|
@@ -30,7 +30,14 @@ function inheritGeneratedOrigin(root, origin) {
|
|
|
30
30
|
for (const item of value) visit(item);
|
|
31
31
|
return;
|
|
32
32
|
}
|
|
33
|
-
|
|
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
|
}
|