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
package/dist/compiler/compile.js
CHANGED
|
@@ -39,6 +39,8 @@ import {
|
|
|
39
39
|
strongHash,
|
|
40
40
|
} from '@tsrx/core';
|
|
41
41
|
import { parseModule } from '#octane/compiler-parser';
|
|
42
|
+
import { createStyleScopePass } from './style-scopes.js';
|
|
43
|
+
import { decodeHTMLStrict } from 'entities';
|
|
42
44
|
import { print as esrapPrint } from 'esrap';
|
|
43
45
|
import esrapTsx from 'esrap/languages/tsx';
|
|
44
46
|
import { buildFatSegments } from './fat-segments.js';
|
|
@@ -106,7 +108,13 @@ import {
|
|
|
106
108
|
cssStyleValue,
|
|
107
109
|
hyphenateStyleName,
|
|
108
110
|
} from '../dom-tables.js';
|
|
111
|
+
import { isDelegatedEventProp } from '../event-names.js';
|
|
109
112
|
import { sanitizeURLAttribute } from '../sanitize-url.js';
|
|
113
|
+
import {
|
|
114
|
+
invalidHtmlNestingWithAncestor,
|
|
115
|
+
invalidHtmlNestingWithParent,
|
|
116
|
+
invalidHtmlTextNesting,
|
|
117
|
+
} from '../html-tree-validation.js';
|
|
110
118
|
|
|
111
119
|
// --- Parser-AST immutability enforcement ------------------------------------
|
|
112
120
|
//
|
|
@@ -233,6 +241,10 @@ function inheritOriginLoc(root, origin) {
|
|
|
233
241
|
if (origin == null || origin.loc == null) return root;
|
|
234
242
|
const { start, end, loc } = origin;
|
|
235
243
|
mapAst(root, (node) => {
|
|
244
|
+
// A parsed `<style>` body is a CSS AST whose positions are relative to the
|
|
245
|
+
// CSS text, never to the module: stamping module positions onto it would
|
|
246
|
+
// corrupt the scoped-CSS render (and it is parser-owned, so shared).
|
|
247
|
+
if (node.type === 'StyleSheet') return node;
|
|
236
248
|
// Only real AST nodes are stamped — mapAst also visits typeless data
|
|
237
249
|
// objects (TemplateElement.value, Literal.regex, embedded config), which
|
|
238
250
|
// must stay untouched (and may be frozen).
|
|
@@ -310,6 +322,13 @@ function markSynthesizedAttr(attr) {
|
|
|
310
322
|
return attr;
|
|
311
323
|
}
|
|
312
324
|
|
|
325
|
+
/** A compiler-added scope-hash class, not an authored `class`/`className`. */
|
|
326
|
+
function isSynthesizedClassAttr(attr) {
|
|
327
|
+
const raw = jsxAttrRawName(attr);
|
|
328
|
+
if (raw !== 'class' && raw !== 'className') return false;
|
|
329
|
+
return attr.name?.metadata?.tsrx_synthesized_origin === true;
|
|
330
|
+
}
|
|
331
|
+
|
|
313
332
|
// Curated exact origins (`inspect: true`). A generated token whose authored
|
|
314
333
|
// counterpart the compiler KNOWS — the `$$click` slot key of an `onClick`, the
|
|
315
334
|
// block helper an `@if`/`@for` lowers to — but which the module map alone
|
|
@@ -622,7 +641,7 @@ const CONTROLLED_KIND_HELPERS = {
|
|
|
622
641
|
// recorder (recordBakedAttrOrigins) derives the name/value offsets from that
|
|
623
642
|
// shape arithmetically — keep the invariant when adding forms.
|
|
624
643
|
function bakeStaticAttr(attrName, lv, tag, namespace = 'html') {
|
|
625
|
-
if (lv == null) return '';
|
|
644
|
+
if (lv == null || attrName === 'innerText' || attrName === 'textContent') return '';
|
|
626
645
|
const isCustom =
|
|
627
646
|
(namespace === 'html' || namespace === 'opaque') && tag !== undefined && tag.includes('-');
|
|
628
647
|
const lower = attrName.toLowerCase();
|
|
@@ -663,6 +682,8 @@ function bakeStaticAttr(attrName, lv, tag, namespace = 'html') {
|
|
|
663
682
|
return lv ? ` ${lower}=""` : '';
|
|
664
683
|
}
|
|
665
684
|
if (typeof lv === 'string') {
|
|
685
|
+
if (!isCustom && (lower === 'rowspan' || lower === 'start') && Number.isNaN(Number(lv)))
|
|
686
|
+
return '';
|
|
666
687
|
if (!isCustom && POSITIVE_NUMERIC_ATTR_PROPS.has(lower) && !(Number(lv) >= 1)) return '';
|
|
667
688
|
if (
|
|
668
689
|
!isCustom &&
|
|
@@ -1565,12 +1586,13 @@ function inheritSoleCompRoot(bodyNodes, ctx) {
|
|
|
1565
1586
|
// Collect the LOCAL names bound to the octane boundary builtins (see
|
|
1566
1587
|
// inheritSoleCompRoot) from a module's import declarations. Aliased imports
|
|
1567
1588
|
// (`import { Suspense as S }`) are matched by their local binding.
|
|
1568
|
-
function collectOctaneBoundaryNames(astBody) {
|
|
1589
|
+
function collectOctaneBoundaryNames(astBody, onlySuspense = false) {
|
|
1569
1590
|
const names = new Set();
|
|
1570
1591
|
for (const node of astBody) {
|
|
1571
1592
|
if (node.type !== 'ImportDeclaration' || node.source.value !== 'octane') continue;
|
|
1572
1593
|
for (const sp of node.specifiers || []) {
|
|
1573
1594
|
const imported = sp.imported?.name;
|
|
1595
|
+
if (onlySuspense && imported !== 'Suspense') continue;
|
|
1574
1596
|
if (
|
|
1575
1597
|
(imported === 'Suspense' ||
|
|
1576
1598
|
imported === 'ErrorBoundary' ||
|
|
@@ -4713,6 +4735,225 @@ function containsRenderCall(stmts, memoCtx = null) {
|
|
|
4713
4735
|
return found;
|
|
4714
4736
|
}
|
|
4715
4737
|
|
|
4738
|
+
// Built-in hooks whose render-time work cannot schedule an update for the
|
|
4739
|
+
// calling component. Their function arguments run later (commit, event, or
|
|
4740
|
+
// never), so those bodies are deferred like any other closure.
|
|
4741
|
+
const SETUP_PASSIVE_HOOKS = new Set([
|
|
4742
|
+
'useCallback',
|
|
4743
|
+
'useRef',
|
|
4744
|
+
'useEffect',
|
|
4745
|
+
'useLayoutEffect',
|
|
4746
|
+
'useInsertionEffect',
|
|
4747
|
+
'useImperativeHandle',
|
|
4748
|
+
'useEffectEvent',
|
|
4749
|
+
'useId',
|
|
4750
|
+
'useDeferredValue',
|
|
4751
|
+
'useTransition',
|
|
4752
|
+
'useFormStatus',
|
|
4753
|
+
'useActionState',
|
|
4754
|
+
'useFormState',
|
|
4755
|
+
'useContext',
|
|
4756
|
+
'use',
|
|
4757
|
+
]);
|
|
4758
|
+
// Built-in hooks that invoke a function argument synchronously during render
|
|
4759
|
+
// (a memo factory, a lazy initializer, a snapshot reader, an optimistic
|
|
4760
|
+
// reducer, a linked-state reconciler). Those bodies belong to setup.
|
|
4761
|
+
const SETUP_SYNC_FACTORY_HOOKS = new Set([
|
|
4762
|
+
'useMemo',
|
|
4763
|
+
'useState',
|
|
4764
|
+
'useLinkedState',
|
|
4765
|
+
'useReducer',
|
|
4766
|
+
'useSyncExternalStore',
|
|
4767
|
+
'useOptimistic',
|
|
4768
|
+
]);
|
|
4769
|
+
// Standard globals that only project their arguments. A user-defined
|
|
4770
|
+
// `toString`/`valueOf`/`toJSON` that schedules a render violates the hook rules
|
|
4771
|
+
// in React and Octane alike, so these count as call-free setup.
|
|
4772
|
+
const SETUP_PURE_GLOBAL_CALLEES = new Set([
|
|
4773
|
+
'String',
|
|
4774
|
+
'Number',
|
|
4775
|
+
'Boolean',
|
|
4776
|
+
'BigInt',
|
|
4777
|
+
'Symbol',
|
|
4778
|
+
'parseInt',
|
|
4779
|
+
'parseFloat',
|
|
4780
|
+
'isNaN',
|
|
4781
|
+
'isFinite',
|
|
4782
|
+
'encodeURI',
|
|
4783
|
+
'encodeURIComponent',
|
|
4784
|
+
'decodeURI',
|
|
4785
|
+
'decodeURIComponent',
|
|
4786
|
+
]);
|
|
4787
|
+
// Namespace members that never invoke an argument. `Array.from`,
|
|
4788
|
+
// `Object.groupBy`, `Object.fromEntries`, and friends iterate or call user
|
|
4789
|
+
// code, so they stay unclassified. JSON's replacer and reviver positions are
|
|
4790
|
+
// checked like a hook factory (see walkInvokedArgument).
|
|
4791
|
+
const SETUP_PURE_NAMESPACE_MEMBERS = new Map([
|
|
4792
|
+
['Math', null],
|
|
4793
|
+
[
|
|
4794
|
+
'Number',
|
|
4795
|
+
new Set(['isFinite', 'isInteger', 'isNaN', 'isSafeInteger', 'parseFloat', 'parseInt']),
|
|
4796
|
+
],
|
|
4797
|
+
['Object', new Set(['entries', 'freeze', 'hasOwn', 'is', 'isFrozen', 'keys', 'values'])],
|
|
4798
|
+
['Array', new Set(['isArray', 'of'])],
|
|
4799
|
+
['Date', new Set(['now', 'parse', 'UTC'])],
|
|
4800
|
+
['JSON', new Set(['parse', 'stringify'])],
|
|
4801
|
+
]);
|
|
4802
|
+
|
|
4803
|
+
// Every binding name the module declares anywhere. A global namespace shadowed
|
|
4804
|
+
// by any local, parameter, or import is treated as an unknown callee.
|
|
4805
|
+
function collectModuleBoundNames(ast) {
|
|
4806
|
+
const names = new Set();
|
|
4807
|
+
const seen = new WeakSet();
|
|
4808
|
+
function walk(n) {
|
|
4809
|
+
if (!n || typeof n !== 'object') return;
|
|
4810
|
+
if (Array.isArray(n)) {
|
|
4811
|
+
for (const x of n) walk(x);
|
|
4812
|
+
return;
|
|
4813
|
+
}
|
|
4814
|
+
if (seen.has(n)) return;
|
|
4815
|
+
seen.add(n);
|
|
4816
|
+
switch (n.type) {
|
|
4817
|
+
case 'VariableDeclarator':
|
|
4818
|
+
collectBindings(n.id, names);
|
|
4819
|
+
break;
|
|
4820
|
+
case 'FunctionDeclaration':
|
|
4821
|
+
case 'FunctionExpression':
|
|
4822
|
+
case 'ArrowFunctionExpression':
|
|
4823
|
+
if (n.id) names.add(n.id.name);
|
|
4824
|
+
for (const param of n.params || []) collectBindings(param, names);
|
|
4825
|
+
break;
|
|
4826
|
+
case 'ClassDeclaration':
|
|
4827
|
+
case 'ClassExpression':
|
|
4828
|
+
if (n.id) names.add(n.id.name);
|
|
4829
|
+
break;
|
|
4830
|
+
case 'CatchClause':
|
|
4831
|
+
collectBindings(n.param, names);
|
|
4832
|
+
break;
|
|
4833
|
+
case 'ImportSpecifier':
|
|
4834
|
+
case 'ImportDefaultSpecifier':
|
|
4835
|
+
case 'ImportNamespaceSpecifier':
|
|
4836
|
+
if (n.local) names.add(n.local.name);
|
|
4837
|
+
break;
|
|
4838
|
+
}
|
|
4839
|
+
for (const key in n) {
|
|
4840
|
+
if (AST_WALK_SKIP_KEYS.has(key)) continue;
|
|
4841
|
+
walk(n[key]);
|
|
4842
|
+
}
|
|
4843
|
+
}
|
|
4844
|
+
walk(ast);
|
|
4845
|
+
return names;
|
|
4846
|
+
}
|
|
4847
|
+
|
|
4848
|
+
/**
|
|
4849
|
+
* Can this setup schedule a render-phase update for its own component? A
|
|
4850
|
+
* setter, dispatcher, transition start, custom hook, or any other call the
|
|
4851
|
+
* compiler cannot classify may; the built-in hooks above and standard pure
|
|
4852
|
+
* globals cannot. Closures are deferred unless a built-in hook runs them
|
|
4853
|
+
* during render. This admits the setup checkpoint, an optimization over the
|
|
4854
|
+
* runtime's own child-initialization guards, so an unclassified callee fails
|
|
4855
|
+
* closed to emitting the checkpoint.
|
|
4856
|
+
*/
|
|
4857
|
+
function setupCanScheduleSelfUpdate(stmts, ctx) {
|
|
4858
|
+
let found = false;
|
|
4859
|
+
const seen = new WeakSet();
|
|
4860
|
+
const bound = ctx._moduleBoundNames;
|
|
4861
|
+
const isGlobal = (name) => bound !== undefined && !bound.has(name);
|
|
4862
|
+
function walk(n, deep) {
|
|
4863
|
+
if (found || !n) return;
|
|
4864
|
+
if (Array.isArray(n)) {
|
|
4865
|
+
for (const x of n) walk(x, deep);
|
|
4866
|
+
return;
|
|
4867
|
+
}
|
|
4868
|
+
if (typeof n !== 'object') return;
|
|
4869
|
+
const t = n.type;
|
|
4870
|
+
if (!t) return;
|
|
4871
|
+
if (seen.has(n)) return;
|
|
4872
|
+
seen.add(n);
|
|
4873
|
+
if (
|
|
4874
|
+
!deep &&
|
|
4875
|
+
(t === 'ArrowFunctionExpression' || t === 'FunctionExpression' || t === 'FunctionDeclaration')
|
|
4876
|
+
) {
|
|
4877
|
+
return;
|
|
4878
|
+
}
|
|
4879
|
+
if (t === 'CallExpression' || t === 'OptionalCallExpression') {
|
|
4880
|
+
const hook = n._octaneImportedHook;
|
|
4881
|
+
if (hook !== undefined && SETUP_PASSIVE_HOOKS.has(hook)) {
|
|
4882
|
+
walk(n.arguments, deep);
|
|
4883
|
+
return;
|
|
4884
|
+
}
|
|
4885
|
+
if (hook !== undefined && SETUP_SYNC_FACTORY_HOOKS.has(hook)) {
|
|
4886
|
+
for (const argument of n.arguments) walkInvokedArgument(argument);
|
|
4887
|
+
return;
|
|
4888
|
+
}
|
|
4889
|
+
const callee = n.callee;
|
|
4890
|
+
if (
|
|
4891
|
+
callee?.type === 'Identifier' &&
|
|
4892
|
+
n._octaneUnboundCallee === true &&
|
|
4893
|
+
SETUP_PURE_GLOBAL_CALLEES.has(callee.name)
|
|
4894
|
+
) {
|
|
4895
|
+
walk(n.arguments, true);
|
|
4896
|
+
return;
|
|
4897
|
+
}
|
|
4898
|
+
if (
|
|
4899
|
+
callee?.type === 'MemberExpression' &&
|
|
4900
|
+
!callee.computed &&
|
|
4901
|
+
callee.object?.type === 'Identifier' &&
|
|
4902
|
+
callee.property?.type === 'Identifier' &&
|
|
4903
|
+
SETUP_PURE_NAMESPACE_MEMBERS.has(callee.object.name) &&
|
|
4904
|
+
isGlobal(callee.object.name)
|
|
4905
|
+
) {
|
|
4906
|
+
const members = SETUP_PURE_NAMESPACE_MEMBERS.get(callee.object.name);
|
|
4907
|
+
if (members === null || members.has(callee.property.name)) {
|
|
4908
|
+
if (callee.object.name === 'JSON') {
|
|
4909
|
+
walk(n.arguments[0], true);
|
|
4910
|
+
for (let index = 1; index < n.arguments.length; index++)
|
|
4911
|
+
walkInvokedArgument(n.arguments[index]);
|
|
4912
|
+
} else walk(n.arguments, true);
|
|
4913
|
+
return;
|
|
4914
|
+
}
|
|
4915
|
+
}
|
|
4916
|
+
found = true;
|
|
4917
|
+
return;
|
|
4918
|
+
}
|
|
4919
|
+
if (t === 'NewExpression' || t === 'TaggedTemplateExpression') {
|
|
4920
|
+
found = true;
|
|
4921
|
+
return;
|
|
4922
|
+
}
|
|
4923
|
+
for (const key in n) {
|
|
4924
|
+
if (AST_WALK_SKIP_KEYS.has(key)) continue;
|
|
4925
|
+
walk(n[key], deep);
|
|
4926
|
+
}
|
|
4927
|
+
}
|
|
4928
|
+
// A sync-factory hook may invoke any argument whose value can be a function.
|
|
4929
|
+
// Only shapes that visibly cannot be callable, or whose bodies are visible
|
|
4930
|
+
// here, keep the analysis open; a named factory, member, call result, or
|
|
4931
|
+
// conditional fails closed exactly like an unclassified call.
|
|
4932
|
+
function walkInvokedArgument(argument) {
|
|
4933
|
+
if (found || !argument) return;
|
|
4934
|
+
switch (argument.type) {
|
|
4935
|
+
case 'ArrowFunctionExpression':
|
|
4936
|
+
case 'FunctionExpression':
|
|
4937
|
+
case 'Literal':
|
|
4938
|
+
case 'TemplateLiteral':
|
|
4939
|
+
case 'BinaryExpression':
|
|
4940
|
+
case 'UnaryExpression':
|
|
4941
|
+
case 'UpdateExpression':
|
|
4942
|
+
walk(argument, true);
|
|
4943
|
+
return;
|
|
4944
|
+
case 'ArrayExpression':
|
|
4945
|
+
// Dependency and initial-value arrays are never invoked; their
|
|
4946
|
+
// elements are ordinary setup expressions.
|
|
4947
|
+
walk(argument.elements, true);
|
|
4948
|
+
return;
|
|
4949
|
+
default:
|
|
4950
|
+
found = true;
|
|
4951
|
+
}
|
|
4952
|
+
}
|
|
4953
|
+
for (const s of stmts) walk(s, false);
|
|
4954
|
+
return found;
|
|
4955
|
+
}
|
|
4956
|
+
|
|
4716
4957
|
// A hook call is identified by naming convention — the same signal React and
|
|
4717
4958
|
// React Compiler key on — plus the exact `unstable_` and `UNSTABLE_` staging
|
|
4718
4959
|
// prefixes bindings expose (`unstable_useRouterState` in @octanejs/remix-router
|
|
@@ -6485,9 +6726,21 @@ function mapCallbackCapturesLexicalReceiver(root) {
|
|
|
6485
6726
|
return walk(root);
|
|
6486
6727
|
}
|
|
6487
6728
|
|
|
6488
|
-
//
|
|
6489
|
-
//
|
|
6490
|
-
|
|
6729
|
+
// Scoped `<style>` blocks, `$class`, and `apply` (RFC tsrx-org/RFCs#1): one
|
|
6730
|
+
// copy-on-write pre-pass per top-level statement, see ./style-scopes.js.
|
|
6731
|
+
const { applyStyleScopes } = createStyleScopePass({
|
|
6732
|
+
inheritOriginLoc,
|
|
6733
|
+
markSynthesized,
|
|
6734
|
+
markSynthesizedAttr,
|
|
6735
|
+
headResourceKind,
|
|
6736
|
+
isCompositeJsxTag,
|
|
6737
|
+
isStyleCall,
|
|
6738
|
+
});
|
|
6739
|
+
|
|
6740
|
+
// Recognise `style(expr)` — a plain `CallExpression(style, [expr])`, Octane's
|
|
6741
|
+
// class-string expression. The style-scope pre-pass (./style-scopes.js)
|
|
6742
|
+
// resolves it where TSRX reads a class value (attribute values and template
|
|
6743
|
+
// child holes, the `style` attribute excepted); anywhere else it is a user call.
|
|
6491
6744
|
function isStyleCall(node) {
|
|
6492
6745
|
return (
|
|
6493
6746
|
node &&
|
|
@@ -6499,26 +6752,14 @@ function isStyleCall(node) {
|
|
|
6499
6752
|
);
|
|
6500
6753
|
}
|
|
6501
6754
|
|
|
6502
|
-
//
|
|
6503
|
-
//
|
|
6504
|
-
//
|
|
6505
|
-
//
|
|
6506
|
-
//
|
|
6507
|
-
//
|
|
6508
|
-
function resolveStyleExpr(node,
|
|
6509
|
-
|
|
6510
|
-
if (!isStyleCall(node) || !cssHash) return node;
|
|
6511
|
-
const inner = node.arguments[0];
|
|
6512
|
-
if (inner.type === 'Literal' && typeof inner.value === 'string') {
|
|
6513
|
-
const combined = inner.value ? `${cssHash} ${inner.value}` : cssHash;
|
|
6514
|
-
// The resolved class string maps to the authored `{style (…)}` call.
|
|
6515
|
-
return b.literal(combined, JSON.stringify(combined), node);
|
|
6516
|
-
}
|
|
6517
|
-
// Dynamic: emit `(<hash> + ' ' + (expr))` so absent/null produces "<hash> ".
|
|
6518
|
-
return inheritOriginLoc(
|
|
6519
|
-
b.binary('+', b.literal(cssHash + ' ', JSON.stringify(cssHash + ' ')), inner),
|
|
6520
|
-
node,
|
|
6521
|
-
);
|
|
6755
|
+
// The emitters' former `style(expr)` expansion. The pre-pass resolves every
|
|
6756
|
+
// class-string position before the emitters run, and a `style(...)` still on
|
|
6757
|
+
// the AST here is a user call in another position (a `style={style(p)}` value,
|
|
6758
|
+
// a child hole's call argument), which must print as authored — so this is a
|
|
6759
|
+
// pass-through kept at its call sites; `cssHash` is only the "this component
|
|
6760
|
+
// owns scoped CSS" flag they thread along.
|
|
6761
|
+
function resolveStyleExpr(node, _cssHash) {
|
|
6762
|
+
return node;
|
|
6522
6763
|
}
|
|
6523
6764
|
|
|
6524
6765
|
/**
|
|
@@ -6544,6 +6785,11 @@ function isComponentFunction(node) {
|
|
|
6544
6785
|
// hooks slotted (via withSlot) and its returned JSX lowered to a descriptor, and
|
|
6545
6786
|
// whatever USES it (`<Foo/>` / `{expr}`) is what renders it. (Async/generator
|
|
6546
6787
|
// excluded; a `use*` custom hook that returns JSX is still a hook, not this.)
|
|
6788
|
+
//
|
|
6789
|
+
// Owned returns inside `if`/`else`/`switch`/`try`/loops count: a function whose
|
|
6790
|
+
// only JSX exits are nested still needs the return-JSX path so scoped `<style>`
|
|
6791
|
+
// (hash stamp, injectStyle, `ref` class-map writes) runs. Nested function
|
|
6792
|
+
// bodies are a different component/value boundary and are ignored.
|
|
6547
6793
|
function isReturnJsxFunction(node) {
|
|
6548
6794
|
if (!node) return false;
|
|
6549
6795
|
if (node.type !== 'FunctionDeclaration' && node.type !== 'FunctionExpression') return false;
|
|
@@ -6553,9 +6799,58 @@ function isReturnJsxFunction(node) {
|
|
|
6553
6799
|
if (node.id == null) return false;
|
|
6554
6800
|
if (node.async || node.generator) return false;
|
|
6555
6801
|
if (!node.body || node.body.type !== 'BlockStatement') return false;
|
|
6556
|
-
|
|
6557
|
-
|
|
6558
|
-
|
|
6802
|
+
const statements = node.body.body || [];
|
|
6803
|
+
for (let i = 0; i < statements.length; i++) {
|
|
6804
|
+
if (statementHasOwnedJsxReturn(statements[i])) return true;
|
|
6805
|
+
}
|
|
6806
|
+
return false;
|
|
6807
|
+
}
|
|
6808
|
+
|
|
6809
|
+
/** @param {any} stmt @returns {boolean} */
|
|
6810
|
+
function statementHasOwnedJsxReturn(stmt) {
|
|
6811
|
+
if (!stmt) return false;
|
|
6812
|
+
if (stmt.type === 'ReturnStatement') return !!(stmt.argument && isJsxNode(stmt.argument));
|
|
6813
|
+
if (stmt.type === 'BlockStatement') {
|
|
6814
|
+
const list = stmt.body || [];
|
|
6815
|
+
for (let i = 0; i < list.length; i++) {
|
|
6816
|
+
if (statementHasOwnedJsxReturn(list[i])) return true;
|
|
6817
|
+
}
|
|
6818
|
+
return false;
|
|
6819
|
+
}
|
|
6820
|
+
if (stmt.type === 'IfStatement') {
|
|
6821
|
+
return (
|
|
6822
|
+
statementHasOwnedJsxReturn(stmt.consequent) || statementHasOwnedJsxReturn(stmt.alternate)
|
|
6823
|
+
);
|
|
6824
|
+
}
|
|
6825
|
+
if (stmt.type === 'SwitchStatement') {
|
|
6826
|
+
const cases = stmt.cases || [];
|
|
6827
|
+
for (let i = 0; i < cases.length; i++) {
|
|
6828
|
+
const consequent = cases[i].consequent || [];
|
|
6829
|
+
for (let j = 0; j < consequent.length; j++) {
|
|
6830
|
+
if (statementHasOwnedJsxReturn(consequent[j])) return true;
|
|
6831
|
+
}
|
|
6832
|
+
}
|
|
6833
|
+
return false;
|
|
6834
|
+
}
|
|
6835
|
+
if (stmt.type === 'TryStatement') {
|
|
6836
|
+
return (
|
|
6837
|
+
statementHasOwnedJsxReturn(stmt.block) ||
|
|
6838
|
+
(stmt.handler ? statementHasOwnedJsxReturn(stmt.handler.body) : false) ||
|
|
6839
|
+
statementHasOwnedJsxReturn(stmt.finalizer)
|
|
6840
|
+
);
|
|
6841
|
+
}
|
|
6842
|
+
if (
|
|
6843
|
+
stmt.type === 'ForStatement' ||
|
|
6844
|
+
stmt.type === 'ForInStatement' ||
|
|
6845
|
+
stmt.type === 'ForOfStatement' ||
|
|
6846
|
+
stmt.type === 'WhileStatement' ||
|
|
6847
|
+
stmt.type === 'DoWhileStatement' ||
|
|
6848
|
+
stmt.type === 'LabeledStatement' ||
|
|
6849
|
+
stmt.type === 'WithStatement'
|
|
6850
|
+
) {
|
|
6851
|
+
return statementHasOwnedJsxReturn(stmt.body);
|
|
6852
|
+
}
|
|
6853
|
+
return false;
|
|
6559
6854
|
}
|
|
6560
6855
|
|
|
6561
6856
|
/** True when `node` is one plain lowercase host element (never a component/fragment). */
|
|
@@ -8333,7 +8628,9 @@ function compileAuthored(source, filename, options, bundlerMetadata) {
|
|
|
8333
8628
|
throw new Error(`Unknown compile mode "${mode}" — expected 'client' or 'server'.`);
|
|
8334
8629
|
}
|
|
8335
8630
|
const cleanFilename = cleanCompileFilename(filename);
|
|
8336
|
-
const analyzedAst =
|
|
8631
|
+
const analyzedAst = markParserSensitiveHosts(
|
|
8632
|
+
normalizeAuthoredJsxLiterals(parseModule(source, cleanFilename)),
|
|
8633
|
+
);
|
|
8337
8634
|
analyzeTsrx(analyzedAst, cleanFilename);
|
|
8338
8635
|
adoptParserAst(analyzedAst);
|
|
8339
8636
|
assertNativeReadDiagnostics(analyzedAst, source, cleanFilename, options);
|
|
@@ -9023,8 +9320,12 @@ function compileInternal(
|
|
|
9023
9320
|
// that are ever assigned (`foo = bar`) are excluded: their identity is then
|
|
9024
9321
|
// as mutable as a module `let`, which fails closed.
|
|
9025
9322
|
ctx.moduleFunctionDeclarations = collectImmutableModuleFunctions(ast.body);
|
|
9323
|
+
// Global namespaces are only trusted by the setup checkpoint analysis when
|
|
9324
|
+
// no module binding shadows them (see setupCanScheduleSelfUpdate).
|
|
9325
|
+
ctx._moduleBoundNames = collectModuleBoundNames(ast);
|
|
9026
9326
|
// M3 inherit-range exclusion set (see inheritSoleCompRoot).
|
|
9027
9327
|
ctx._octaneBoundaryNames = collectOctaneBoundaryNames(ast.body);
|
|
9328
|
+
ctx._octaneSuspenseNames = collectOctaneBoundaryNames(ast.body, true);
|
|
9028
9329
|
const productionEffects = !ctx.hmr && !ctx.dev && !ctx.profile;
|
|
9029
9330
|
const exportedComponents = ast.body.filter(
|
|
9030
9331
|
(statement) =>
|
|
@@ -9579,16 +9880,11 @@ function compileInternal(
|
|
|
9579
9880
|
// use, custom helpers, etc.) — merged into the single prelude import.
|
|
9580
9881
|
addUserImportSpecifiers(ctx, node);
|
|
9581
9882
|
} else {
|
|
9582
|
-
// Style
|
|
9583
|
-
//
|
|
9584
|
-
//
|
|
9585
|
-
|
|
9586
|
-
|
|
9587
|
-
// in an ExportNamedDeclaration).
|
|
9588
|
-
if (node.type === 'ExportNamedDeclaration' && node.declaration) {
|
|
9589
|
-
const declaration = applyStyleMap(node.declaration, ctx);
|
|
9590
|
-
if (declaration !== node.declaration) node = { ...node, declaration };
|
|
9591
|
-
}
|
|
9883
|
+
// Style blocks anywhere in a non-component statement: assigned blocks
|
|
9884
|
+
// (`const x = <style>…</style>`, exported or nested in a helper) become
|
|
9885
|
+
// class-map objects and their sheets flow through cssInjections; scoped
|
|
9886
|
+
// blocks inside plain functions' templates get their scope hashes.
|
|
9887
|
+
node = applyStyleScopes(node, ctx).node;
|
|
9592
9888
|
// HOOKS EVERYWHERE: a plain function (a custom hook, a helper) can hold
|
|
9593
9889
|
// octane hooks too — slot them the same way components do, so their base
|
|
9594
9890
|
// hooks get a per-call-site symbol (and custom-hook calls inside get one to
|
|
@@ -10143,6 +10439,7 @@ function compileServer(
|
|
|
10143
10439
|
// M3 inherit-range exclusion set — must match the client compile's
|
|
10144
10440
|
// (see inheritSoleCompRoot; both modes read the same import declarations).
|
|
10145
10441
|
ctx._octaneBoundaryNames = collectOctaneBoundaryNames(ast.body);
|
|
10442
|
+
ctx._octaneSuspenseNames = collectOctaneBoundaryNames(ast.body, true);
|
|
10146
10443
|
|
|
10147
10444
|
// Style maps are module values and can be referenced by a component declared
|
|
10148
10445
|
// before the map itself. Lower every top-level map before compiling any
|
|
@@ -10156,11 +10453,18 @@ function compileServer(
|
|
|
10156
10453
|
let newBody = null;
|
|
10157
10454
|
const statements = ast.body;
|
|
10158
10455
|
for (let i = 0; i < statements.length; i++) {
|
|
10159
|
-
|
|
10160
|
-
|
|
10161
|
-
|
|
10162
|
-
|
|
10163
|
-
|
|
10456
|
+
// Components lower their own scopes inside compileServerComponent so
|
|
10457
|
+
// only the CSS they own is emitted per render; every other statement's
|
|
10458
|
+
// blocks are module-level.
|
|
10459
|
+
const statement = statements[i];
|
|
10460
|
+
const declaration =
|
|
10461
|
+
statement.type === 'ExportDefaultDeclaration' || statement.type === 'ExportNamedDeclaration'
|
|
10462
|
+
? statement.declaration
|
|
10463
|
+
: statement;
|
|
10464
|
+
const node =
|
|
10465
|
+
isComponentFunction(declaration) || isReturnJsxFunction(declaration)
|
|
10466
|
+
? statement
|
|
10467
|
+
: applyStyleScopes(statement, ctx).node;
|
|
10164
10468
|
if (newBody === null && node !== statements[i]) newBody = statements.slice(0, i);
|
|
10165
10469
|
if (newBody !== null) newBody.push(node);
|
|
10166
10470
|
}
|
|
@@ -10240,7 +10544,8 @@ function compileServer(
|
|
|
10240
10544
|
},
|
|
10241
10545
|
});
|
|
10242
10546
|
} else {
|
|
10243
|
-
|
|
10547
|
+
const fnName = node.id?.name || node.declaration?.id?.name || 'module';
|
|
10548
|
+
bodyNodes.push(rewriteModuleJsxValues(rewriteHookCalls(node, ctx, fnName), ctx));
|
|
10244
10549
|
}
|
|
10245
10550
|
}
|
|
10246
10551
|
|
|
@@ -10319,11 +10624,11 @@ function compileServerComponent(node, ctx) {
|
|
|
10319
10624
|
const isExported = !!(node.export || node.default);
|
|
10320
10625
|
const isDefault = !!node.default;
|
|
10321
10626
|
|
|
10322
|
-
// Scoped <style>:
|
|
10627
|
+
// Scoped <style>: applyStyleScopes stamps scope classes + registers cssInjections.
|
|
10323
10628
|
// Capture this component's entries to emit injectStyle INSIDE the body (so the
|
|
10324
10629
|
// active server render collects CSS only for components it actually renders).
|
|
10325
10630
|
const beforeCss = ctx.cssInjections.length;
|
|
10326
|
-
const scoping =
|
|
10631
|
+
const scoping = applyStyleScopes(node, ctx);
|
|
10327
10632
|
node = scoping.node;
|
|
10328
10633
|
const cssHash = scoping.cssHash;
|
|
10329
10634
|
const cssEntries = [...ctx.moduleCssInjections, ...ctx.cssInjections.slice(beforeCss)].sort(
|
|
@@ -10351,6 +10656,12 @@ function compileServerComponent(node, ctx) {
|
|
|
10351
10656
|
cssEntries,
|
|
10352
10657
|
'opaque',
|
|
10353
10658
|
node.body?.type === 'JSXCodeBlock',
|
|
10659
|
+
null,
|
|
10660
|
+
false,
|
|
10661
|
+
false,
|
|
10662
|
+
// Imported themes this component applies are touched at the top of
|
|
10663
|
+
// its body (serverThemeTouchStatements) so their CSS precedes its own.
|
|
10664
|
+
scoping.runtimeApplied,
|
|
10354
10665
|
);
|
|
10355
10666
|
} finally {
|
|
10356
10667
|
ctx.currentComponentLocals = prevLocals;
|
|
@@ -10366,7 +10677,10 @@ function compileServerComponent(node, ctx) {
|
|
|
10366
10677
|
const warmStatement =
|
|
10367
10678
|
warmNode === null
|
|
10368
10679
|
? null
|
|
10369
|
-
: inheritOriginLoc(
|
|
10680
|
+
: inheritOriginLoc(
|
|
10681
|
+
b.stmt(b.call(requireRuntimeForContext(ctx, 'markWarm'), b.id(name), warmNode)),
|
|
10682
|
+
node,
|
|
10683
|
+
);
|
|
10370
10684
|
|
|
10371
10685
|
// Mirror the client emission's hoisting rule (see compileComponent): a
|
|
10372
10686
|
// component referenced ABOVE its declaration keeps real function-declaration
|
|
@@ -10382,7 +10696,7 @@ function compileServerComponent(node, ctx) {
|
|
|
10382
10696
|
b.logical(
|
|
10383
10697
|
'&&',
|
|
10384
10698
|
b.binary('===', b.unary('typeof', b.id(name)), b.literal('function', "'function'")),
|
|
10385
|
-
b.
|
|
10699
|
+
b.call(requireRuntimeForContext(ctx, 'markWarm'), b.id(name), warmNode),
|
|
10386
10700
|
),
|
|
10387
10701
|
),
|
|
10388
10702
|
node,
|
|
@@ -10432,6 +10746,8 @@ function ssrCompileBody(
|
|
|
10432
10746
|
componentNs = null,
|
|
10433
10747
|
returnedFragmentTemplate = false,
|
|
10434
10748
|
returnedFragmentRoot = false,
|
|
10749
|
+
themeTouches = [],
|
|
10750
|
+
unboxedHtml = false,
|
|
10435
10751
|
) {
|
|
10436
10752
|
const prevMapTemps = ctx.currentMapTemps;
|
|
10437
10753
|
ctx.currentMapTemps = [];
|
|
@@ -10447,6 +10763,8 @@ function ssrCompileBody(
|
|
|
10447
10763
|
componentNs,
|
|
10448
10764
|
returnedFragmentTemplate,
|
|
10449
10765
|
returnedFragmentRoot,
|
|
10766
|
+
themeTouches,
|
|
10767
|
+
unboxedHtml,
|
|
10450
10768
|
);
|
|
10451
10769
|
} finally {
|
|
10452
10770
|
ctx.currentMapTemps = prevMapTemps;
|
|
@@ -10464,6 +10782,10 @@ function ssrCompileBodyWithMapTemps(
|
|
|
10464
10782
|
componentNs,
|
|
10465
10783
|
returnedFragmentTemplate,
|
|
10466
10784
|
returnedFragmentRoot,
|
|
10785
|
+
// Imported themes the component's scopes apply — only the component body
|
|
10786
|
+
// itself passes them; nested branch/loop helpers (ssrCompileSub) pass none.
|
|
10787
|
+
themeTouches,
|
|
10788
|
+
unboxedHtml,
|
|
10467
10789
|
) {
|
|
10468
10790
|
const returnedOutput = node.body?.type === 'JSXCodeBlock' && hasOwnValueReturn(node);
|
|
10469
10791
|
const mapTemps = ctx.currentMapTemps;
|
|
@@ -10617,6 +10939,7 @@ function ssrCompileBodyWithMapTemps(
|
|
|
10617
10939
|
ctx._valueDirectiveLowering = prevValueDirectiveLowering;
|
|
10618
10940
|
|
|
10619
10941
|
const body = [];
|
|
10942
|
+
body.push(...serverThemeTouchStatements(themeTouches ?? [], ctx, node));
|
|
10620
10943
|
if (cssEntries && cssEntries.length) {
|
|
10621
10944
|
ctx.runtimeNeeded.add('injectStyle');
|
|
10622
10945
|
for (const entry of cssEntries) {
|
|
@@ -10685,7 +11008,12 @@ function ssrCompileBodyWithMapTemps(
|
|
|
10685
11008
|
node.params.length > 0
|
|
10686
11009
|
? [...node.params, b.id('__s'), b.id('__extra')]
|
|
10687
11010
|
: [b.id('__props'), b.id('__s'), b.id('__extra')];
|
|
10688
|
-
|
|
11011
|
+
// Private loop items feed only HTML concatenation, so their serialized tail
|
|
11012
|
+
// needs no carrier. Other bodies can cross a component/value boundary, where
|
|
11013
|
+
// the carrier distinguishes compiled HTML from authored text returns. This
|
|
11014
|
+
// flag belongs to this body only; nested callbacks keep their own contract.
|
|
11015
|
+
if (!unboxedHtml) ctx.runtimeNeeded.add('ssrHtml');
|
|
11016
|
+
body.push(b.return(unboxedHtml ? htmlExpr : ssrCall('ssrHtml', [htmlExpr], node)));
|
|
10689
11017
|
const origin =
|
|
10690
11018
|
node.loc != null
|
|
10691
11019
|
? node
|
|
@@ -10836,6 +11164,23 @@ function ssrCall(helper, args, origin) {
|
|
|
10836
11164
|
);
|
|
10837
11165
|
}
|
|
10838
11166
|
|
|
11167
|
+
/**
|
|
11168
|
+
* `_$touchStyleMap(theme)` for every imported theme a server component's
|
|
11169
|
+
* scopes apply, emitted before the component's own `injectStyle` calls so the
|
|
11170
|
+
* theme's CSS (a lazily injecting `_$styleMap`) lands first in the request
|
|
11171
|
+
* collector and the applying scope's rules win the cascade.
|
|
11172
|
+
*/
|
|
11173
|
+
function serverThemeTouchStatements(runtimeApplied, ctx, origin) {
|
|
11174
|
+
if (!runtimeApplied || runtimeApplied.length === 0) return [];
|
|
11175
|
+
ctx.runtimeNeeded.add('touchStyleMap');
|
|
11176
|
+
return runtimeApplied.map((expression) =>
|
|
11177
|
+
inheritOriginLoc(
|
|
11178
|
+
b.stmt(ssrCall('touchStyleMap', [cloneAstNode(expression)], expression)),
|
|
11179
|
+
origin,
|
|
11180
|
+
),
|
|
11181
|
+
);
|
|
11182
|
+
}
|
|
11183
|
+
|
|
10839
11184
|
function ssrThunk(expression, origin) {
|
|
10840
11185
|
return inheritOriginLoc(b.arrow([], expression), origin);
|
|
10841
11186
|
}
|
|
@@ -10849,17 +11194,17 @@ function ssrSourcePair(present, value, origin) {
|
|
|
10849
11194
|
}
|
|
10850
11195
|
|
|
10851
11196
|
/**
|
|
10852
|
-
* One `[spread, name, value]` source row for a grouped attribute helper.
|
|
10853
|
-
* name literal is the only token in the group that names THIS attribute, so
|
|
10854
|
-
* is what the authored name is claimed against (see registerAttrLoweringOrigin);
|
|
10855
|
-
* the helper itself stays anchored on the element it serializes.
|
|
11197
|
+
* One `[spread, name, value, merge?]` source row for a grouped attribute helper.
|
|
11198
|
+
* The name literal is the only token in the group that names THIS attribute, so
|
|
11199
|
+
* it is what the authored name is claimed against (see registerAttrLoweringOrigin);
|
|
11200
|
+
* the helper itself stays anchored on the element it serializes. A trailing
|
|
11201
|
+
* `true` marks a synthesized scope class that must merge with a spread's class.
|
|
10856
11202
|
*/
|
|
10857
|
-
function ssrDirectSource(ctx, name, value, origin) {
|
|
11203
|
+
function ssrDirectSource(ctx, name, value, origin, merge) {
|
|
10858
11204
|
registerAttrLoweringOrigin(ctx, origin?.name, null, name);
|
|
10859
|
-
|
|
10860
|
-
|
|
10861
|
-
|
|
10862
|
-
);
|
|
11205
|
+
const cells = [b.literal(false, 'false'), b.literal(name, JSON.stringify(name)), value];
|
|
11206
|
+
if (merge) cells.push(b.literal(true, 'true'));
|
|
11207
|
+
return inheritOriginLoc(b.array(cells), origin);
|
|
10863
11208
|
}
|
|
10864
11209
|
|
|
10865
11210
|
/**
|
|
@@ -11022,14 +11367,15 @@ function ssrEmitNode(
|
|
|
11022
11367
|
ctx.runtimeNeeded.add(fn);
|
|
11023
11368
|
return ssrCall(fn, [resolveStyleExpr(rewriteHookCalls(expr, ctx, name), cssHash)], node);
|
|
11024
11369
|
}
|
|
11025
|
-
ctx.runtimeNeeded.add('ssrChild');
|
|
11026
11370
|
// rewriteJsxValues lowers any JSX embedded in the expression (e.g.
|
|
11027
11371
|
// `{cond && <div/>}`, a ternary, a `.map(x => <Row/>)`) to printable
|
|
11028
11372
|
// createElement(...) descriptors — exactly like ssrEmitTsrxExpression and
|
|
11029
11373
|
// the client makeChildCall. Without it the raw JSX leaks into the emitted
|
|
11030
11374
|
// ssrChild(...) call as unparseable source.
|
|
11375
|
+
const childHelper = nlGuard ? 'ssrChildPre' : 'ssrChild';
|
|
11376
|
+
ctx.runtimeNeeded.add(childHelper);
|
|
11031
11377
|
const childExpr = ssrCall(
|
|
11032
|
-
|
|
11378
|
+
childHelper,
|
|
11033
11379
|
[
|
|
11034
11380
|
resolveStyleExpr(rewriteJsxValues(rewriteHookCalls(expr, ctx, name), ctx), cssHash),
|
|
11035
11381
|
b.id('__s'),
|
|
@@ -11617,7 +11963,15 @@ function ssrEmitElement(node, ctx, name, inlinedSubs, parentNs, cssHash, compone
|
|
|
11617
11963
|
);
|
|
11618
11964
|
attrExpr = tsrxExprNode(attrInner, ctx, name, inlinedSubs);
|
|
11619
11965
|
}
|
|
11620
|
-
attrSources.push(
|
|
11966
|
+
attrSources.push(
|
|
11967
|
+
ssrDirectSource(
|
|
11968
|
+
ctx,
|
|
11969
|
+
rawAttrName,
|
|
11970
|
+
bindAttributeEvaluation(attrExpr),
|
|
11971
|
+
attr,
|
|
11972
|
+
isSynthesizedClassAttr(attr),
|
|
11973
|
+
),
|
|
11974
|
+
);
|
|
11621
11975
|
continue;
|
|
11622
11976
|
}
|
|
11623
11977
|
|
|
@@ -11901,9 +12255,11 @@ function ssrEmitElement(node, ctx, name, inlinedSubs, parentNs, cssHash, compone
|
|
|
11901
12255
|
staticTextLiteral(onlyChild0.expression) === null &&
|
|
11902
12256
|
!isKnownStringChildExpression(onlyChild0.expression, ctx.knownStringChildLocals)
|
|
11903
12257
|
) {
|
|
11904
|
-
|
|
12258
|
+
const childHelper =
|
|
12259
|
+
tag === 'pre' || tag === 'textarea' || tag === 'listing' ? 'ssrChildTextPre' : 'ssrChildText';
|
|
12260
|
+
ctx.runtimeNeeded.add(childHelper);
|
|
11905
12261
|
childrenExpr = ssrCall(
|
|
11906
|
-
|
|
12262
|
+
childHelper,
|
|
11907
12263
|
[
|
|
11908
12264
|
resolveStyleExpr(
|
|
11909
12265
|
rewriteJsxValues(rewriteHookCalls(onlyChild0.expression, ctx, name), ctx),
|
|
@@ -12202,8 +12558,7 @@ function ssrEmitComponent(node, ctx, name, inlinedSubs, parentNs, cssHash, compo
|
|
|
12202
12558
|
const children = rewriteOpaqueTitles(sourceChildren, ctx, 'opaque');
|
|
12203
12559
|
const opaqueChildren = !activityDescriptor && !isFragmentLongForm(node, ctx);
|
|
12204
12560
|
const descriptorChildren =
|
|
12205
|
-
(
|
|
12206
|
-
ctx.descriptorChildrenBindings?.has(tagBindingName(node))) ||
|
|
12561
|
+
isDescriptorChildrenTag(node, ctx) ||
|
|
12207
12562
|
ctx._tsxValuePos ||
|
|
12208
12563
|
(returnedFragmentTemplate && !requiresTemplateNormalization(node, parentNs, true, ctx));
|
|
12209
12564
|
if (descriptorChildren) {
|
|
@@ -12394,6 +12749,7 @@ function ssrCompileSub(
|
|
|
12394
12749
|
componentNs,
|
|
12395
12750
|
returnedFragmentTemplate = false,
|
|
12396
12751
|
returnedFragmentRoot = false,
|
|
12752
|
+
unboxedHtml = false,
|
|
12397
12753
|
) {
|
|
12398
12754
|
const fnName = `${baseName}$${ctx.nextHelperId++}`;
|
|
12399
12755
|
const synth = { params: paramNodes || [], body: bodyStmts };
|
|
@@ -12422,6 +12778,9 @@ function ssrCompileSub(
|
|
|
12422
12778
|
componentNs,
|
|
12423
12779
|
returnedFragmentTemplate,
|
|
12424
12780
|
returnedFragmentRoot,
|
|
12781
|
+
// Nested helpers touch no themes; the component body already did.
|
|
12782
|
+
[],
|
|
12783
|
+
unboxedHtml,
|
|
12425
12784
|
);
|
|
12426
12785
|
} finally {
|
|
12427
12786
|
ctx._ssrFoldedExprHoles = prevFoldedExprHoles;
|
|
@@ -12584,6 +12943,9 @@ function ssrEmitFor(node, ctx, name, inlinedSubs, parentNs, cssHash, componentNs
|
|
|
12584
12943
|
cssHash,
|
|
12585
12944
|
parentNs,
|
|
12586
12945
|
componentNs,
|
|
12946
|
+
false,
|
|
12947
|
+
false,
|
|
12948
|
+
true,
|
|
12587
12949
|
);
|
|
12588
12950
|
inlinedSubs.push(itemSub.fn);
|
|
12589
12951
|
let emptyCall = ssrHtmlTemplate([], node, ctx);
|
|
@@ -13048,147 +13410,6 @@ function hasModuleStyleMaps(body) {
|
|
|
13048
13410
|
});
|
|
13049
13411
|
}
|
|
13050
13412
|
|
|
13051
|
-
// Copy-on-write: returns the (possibly rebuilt) statement; the input is never
|
|
13052
|
-
// modified. The core analyze/prepare pipeline mutates the sheet it is given,
|
|
13053
|
-
// so it runs over a clone of the (bounded) StyleSheet subtree.
|
|
13054
|
-
function applyStyleMap(stmt, ctx) {
|
|
13055
|
-
if (stmt.type !== 'VariableDeclaration') return stmt;
|
|
13056
|
-
let newDeclarations = null;
|
|
13057
|
-
const declarations = stmt.declarations;
|
|
13058
|
-
for (let i = 0; i < declarations.length; i++) {
|
|
13059
|
-
const decl = declarations[i];
|
|
13060
|
-
let next = decl;
|
|
13061
|
-
const styleNode = decl.init && decl.init.type === 'JSXStyleElement' ? decl.init : null;
|
|
13062
|
-
const parsedSheet = styleNode
|
|
13063
|
-
? (styleNode.children || []).find((c) => c && c.type === 'StyleSheet')
|
|
13064
|
-
: null;
|
|
13065
|
-
const hash = parsedSheet
|
|
13066
|
-
? styleNode.metadata?.styleScopeHash || parsedSheet.hash || null
|
|
13067
|
-
: null;
|
|
13068
|
-
if (parsedSheet && hash) {
|
|
13069
|
-
const sheet = cloneAstNode(parsedSheet);
|
|
13070
|
-
// `analyzeCss` marks `:global(...)` selectors (is_global / is_global_block
|
|
13071
|
-
// metadata) so the renderer leaves them UNSCOPED. Without this pass
|
|
13072
|
-
// `:global(a)` would be scoped to `.<hash>a`. Mirrors tsrx-ripple, which
|
|
13073
|
-
// runs analyzeCss(stylesheet) before prepareStylesheetForRender.
|
|
13074
|
-
analyzeCss(sheet);
|
|
13075
|
-
prepareStylesheetForRender(sheet, true);
|
|
13076
|
-
const css = renderStylesheets([sheet]);
|
|
13077
|
-
ctx.cssInjections.push({
|
|
13078
|
-
hash,
|
|
13079
|
-
css,
|
|
13080
|
-
order: styleNode.start ?? stmt.start ?? 0,
|
|
13081
|
-
// The authored `<style>` element. `injectStyle` is emitted at module
|
|
13082
|
-
// scope, so without this the whole block maps to the module origin
|
|
13083
|
-
// and neither the tag nor its CSS is reachable from the output.
|
|
13084
|
-
origin: styleNode,
|
|
13085
|
-
});
|
|
13086
|
-
ctx.runtimeNeeded.add('injectStyle');
|
|
13087
|
-
// Replace the JSXStyleElement init with the class-map ObjectExpression
|
|
13088
|
-
// (built loc-less by the core helper — it maps to the authored <style>).
|
|
13089
|
-
next = {
|
|
13090
|
-
...decl,
|
|
13091
|
-
init: inheritOriginLoc(createStyleClassMapFromStylesheet(sheet), styleNode),
|
|
13092
|
-
};
|
|
13093
|
-
}
|
|
13094
|
-
if (newDeclarations === null && next !== decl) newDeclarations = declarations.slice(0, i);
|
|
13095
|
-
if (newDeclarations !== null) newDeclarations.push(next);
|
|
13096
|
-
}
|
|
13097
|
-
return newDeclarations === null ? stmt : { ...stmt, declarations: newDeclarations };
|
|
13098
|
-
}
|
|
13099
|
-
|
|
13100
|
-
// Wrap every DYNAMIC `class` / `className` expression in a `normalizeClass(...)` call
|
|
13101
|
-
// BEFORE the scoped-CSS hash is appended. `@tsrx/core`'s annotate_with_hash bakes the
|
|
13102
|
-
// hash onto a dynamic class via a template literal — `` `${expr} <hash>` `` — which would
|
|
13103
|
-
// stringify an array/object clsx value the wrong way (`['a','b']` → "a,b", `{}` →
|
|
13104
|
-
// "[object Object]"). Normalizing first makes the interpolated slot a plain string, so
|
|
13105
|
-
// the hash concat stays correct AND clsx composition works in scoped components. (It also
|
|
13106
|
-
// turns a bare `class={undefined}` in a scoped component from "undefined <hash>" into just
|
|
13107
|
-
// "<hash>".) Unscoped components need no wrap — the runtime `setClassName` / `ssrAttr`
|
|
13108
|
-
// normalize the raw value directly. String literals are left alone so they keep folding
|
|
13109
|
-
// into the static template. Stops at nested component function boundaries (their class
|
|
13110
|
-
// exprs belong to a different scope), mirroring annotate_with_hash's own traversal.
|
|
13111
|
-
// Copy-on-write: rebuilt spines only where a wrap lands; untouched subtrees
|
|
13112
|
-
// are returned by reference and the input node is never written to. Callers
|
|
13113
|
-
// must use the return value.
|
|
13114
|
-
function wrapScopedClassExprs(node, ctx) {
|
|
13115
|
-
if (!node || typeof node !== 'object') return node;
|
|
13116
|
-
if (Array.isArray(node)) {
|
|
13117
|
-
let out = null;
|
|
13118
|
-
for (let i = 0; i < node.length; i++) {
|
|
13119
|
-
const mapped = wrapScopedClassExprs(node[i], ctx);
|
|
13120
|
-
if (out === null && mapped !== node[i]) out = node.slice(0, i);
|
|
13121
|
-
if (out !== null) out.push(mapped);
|
|
13122
|
-
}
|
|
13123
|
-
return out ?? node;
|
|
13124
|
-
}
|
|
13125
|
-
if (
|
|
13126
|
-
(node.type === 'FunctionDeclaration' ||
|
|
13127
|
-
node.type === 'FunctionExpression' ||
|
|
13128
|
-
node.type === 'ArrowFunctionExpression') &&
|
|
13129
|
-
node.metadata?.tsrx_dynamic_wrapper !== true
|
|
13130
|
-
) {
|
|
13131
|
-
return node;
|
|
13132
|
-
}
|
|
13133
|
-
let working = node;
|
|
13134
|
-
if (node.type === 'JSXElement') {
|
|
13135
|
-
const attrs = node.openingElement?.attributes;
|
|
13136
|
-
if (Array.isArray(attrs)) {
|
|
13137
|
-
let newAttrs = null;
|
|
13138
|
-
for (let i = 0; i < attrs.length; i++) {
|
|
13139
|
-
const attr = attrs[i];
|
|
13140
|
-
let next = attr;
|
|
13141
|
-
if (
|
|
13142
|
-
attr?.type === 'JSXAttribute' &&
|
|
13143
|
-
attr.name?.type === 'JSXIdentifier' &&
|
|
13144
|
-
(attr.name.name === 'class' || attr.name.name === 'className') &&
|
|
13145
|
-
attr.value?.type === 'JSXExpressionContainer'
|
|
13146
|
-
) {
|
|
13147
|
-
const expr = attr.value.expression;
|
|
13148
|
-
// Skip string literals (fold statically) and `{style (…)}` calls
|
|
13149
|
-
// (resolveStyleExpr owns those). Everything else is a runtime value.
|
|
13150
|
-
if (
|
|
13151
|
-
expr &&
|
|
13152
|
-
!(expr.type === 'Literal' && typeof expr.value === 'string') &&
|
|
13153
|
-
!isStyleCall(expr)
|
|
13154
|
-
) {
|
|
13155
|
-
next = {
|
|
13156
|
-
...attr,
|
|
13157
|
-
value: {
|
|
13158
|
-
...attr.value,
|
|
13159
|
-
// The wrapper maps to the class expression it normalizes.
|
|
13160
|
-
expression: inheritOriginLoc(b.call('_$normalizeClass', expr), expr),
|
|
13161
|
-
},
|
|
13162
|
-
};
|
|
13163
|
-
ctx.runtimeNeeded.add('normalizeClass');
|
|
13164
|
-
}
|
|
13165
|
-
}
|
|
13166
|
-
if (newAttrs === null && next !== attr) newAttrs = attrs.slice(0, i);
|
|
13167
|
-
if (newAttrs !== null) newAttrs.push(next);
|
|
13168
|
-
}
|
|
13169
|
-
if (newAttrs !== null) {
|
|
13170
|
-
working = { ...node, openingElement: { ...node.openingElement, attributes: newAttrs } };
|
|
13171
|
-
if ('attributes' in node) working.attributes = newAttrs;
|
|
13172
|
-
}
|
|
13173
|
-
}
|
|
13174
|
-
}
|
|
13175
|
-
// Generic descent AFTER the element-level wrap (mirrors the historical pass
|
|
13176
|
-
// order): nested JSX inside attribute values and children is wrapped too.
|
|
13177
|
-
let out = working === node ? null : working;
|
|
13178
|
-
for (const key of Object.keys(working)) {
|
|
13179
|
-
if (key === 'loc' || key === 'start' || key === 'end' || key === 'parent') continue;
|
|
13180
|
-
if (key === 'metadata' || key === 'css') continue;
|
|
13181
|
-
const v = working[key];
|
|
13182
|
-
if (!v || typeof v !== 'object') continue;
|
|
13183
|
-
const mapped = wrapScopedClassExprs(v, ctx);
|
|
13184
|
-
if (mapped !== v) {
|
|
13185
|
-
if (out === null) out = { ...node };
|
|
13186
|
-
out[key] = mapped;
|
|
13187
|
-
}
|
|
13188
|
-
}
|
|
13189
|
-
return out ?? working;
|
|
13190
|
-
}
|
|
13191
|
-
|
|
13192
13413
|
// Local copy-on-write mirror of @tsrx/core's annotate_with_hash (scoping.js):
|
|
13193
13414
|
// stamps the scoped-CSS hash class onto native (and dynamic-tag) JSX elements
|
|
13194
13415
|
// and drops JSXStyleElement nodes from the rendered tree. The core helper
|
|
@@ -13203,301 +13424,6 @@ function isCompositeJsxTag(node) {
|
|
|
13203
13424
|
return name.type === 'JSXMemberExpression';
|
|
13204
13425
|
}
|
|
13205
13426
|
|
|
13206
|
-
function addHashClassToElement(element, hash, classAttrName) {
|
|
13207
|
-
const openingElement = element.openingElement;
|
|
13208
|
-
const attrs = openingElement?.attributes || [];
|
|
13209
|
-
const index = attrs.findIndex(
|
|
13210
|
-
(attr) =>
|
|
13211
|
-
attr?.type === 'JSXAttribute' &&
|
|
13212
|
-
attr.name?.type === 'JSXIdentifier' &&
|
|
13213
|
-
(attr.name.name === 'class' || attr.name.name === 'className'),
|
|
13214
|
-
);
|
|
13215
|
-
let newAttrs;
|
|
13216
|
-
if (index === -1) {
|
|
13217
|
-
// A synthesized class attribute maps to the element's opening tag — the
|
|
13218
|
-
// loc keeps generated positions inside the element for source maps, but
|
|
13219
|
-
// there is no authored `class` here, so it is marked NOT AUTHORED. Left
|
|
13220
|
-
// unmarked, inspection reports the whole opening tag as the origin of the
|
|
13221
|
-
// scoped class, and a hover on any attribute in that tag (`onClick`, …)
|
|
13222
|
-
// resolves to the class the compiler added.
|
|
13223
|
-
newAttrs = [
|
|
13224
|
-
...attrs,
|
|
13225
|
-
markSynthesizedAttr(
|
|
13226
|
-
inheritOriginLoc(
|
|
13227
|
-
b.jsx_attribute(b.jsx_id(classAttrName), b.literal(hash, JSON.stringify(hash))),
|
|
13228
|
-
openingElement,
|
|
13229
|
-
),
|
|
13230
|
-
),
|
|
13231
|
-
];
|
|
13232
|
-
} else {
|
|
13233
|
-
const existing = attrs[index];
|
|
13234
|
-
const value = existing.value;
|
|
13235
|
-
let newAttr;
|
|
13236
|
-
if (!value) {
|
|
13237
|
-
// The NAME is authored (`class`), the value is not.
|
|
13238
|
-
newAttr = {
|
|
13239
|
-
...existing,
|
|
13240
|
-
value: markSynthesized(inheritOriginLoc(b.literal(hash, JSON.stringify(hash)), existing)),
|
|
13241
|
-
};
|
|
13242
|
-
} else if (value.type === 'Literal' && typeof value.value === 'string') {
|
|
13243
|
-
const merged = `${value.value} ${hash}`;
|
|
13244
|
-
newAttr = { ...existing, value: { ...value, value: merged, raw: JSON.stringify(merged) } };
|
|
13245
|
-
} else {
|
|
13246
|
-
const expression = value.type === 'JSXExpressionContainer' ? value.expression : value;
|
|
13247
|
-
// The template-literal bake maps to the authored class value it wraps.
|
|
13248
|
-
newAttr = {
|
|
13249
|
-
...existing,
|
|
13250
|
-
value: inheritOriginLoc(
|
|
13251
|
-
b.jsx_expression_container(
|
|
13252
|
-
b.template([b.quasi('', false), b.quasi(` ${hash}`, true)], [expression]),
|
|
13253
|
-
),
|
|
13254
|
-
value,
|
|
13255
|
-
),
|
|
13256
|
-
};
|
|
13257
|
-
}
|
|
13258
|
-
newAttrs = attrs.slice();
|
|
13259
|
-
newAttrs[index] = newAttr;
|
|
13260
|
-
}
|
|
13261
|
-
// The core helper mirrors the attribute list onto `element.attributes` in
|
|
13262
|
-
// every branch; keep that alias in sync on the rebuilt copy.
|
|
13263
|
-
return {
|
|
13264
|
-
...element,
|
|
13265
|
-
openingElement: { ...openingElement, attributes: newAttrs },
|
|
13266
|
-
attributes: newAttrs,
|
|
13267
|
-
};
|
|
13268
|
-
}
|
|
13269
|
-
|
|
13270
|
-
function annotateRootWithHash(node, hash, classAttrName) {
|
|
13271
|
-
if (!node || typeof node !== 'object') return node;
|
|
13272
|
-
if (
|
|
13273
|
-
(node.type === 'FunctionDeclaration' ||
|
|
13274
|
-
node.type === 'FunctionExpression' ||
|
|
13275
|
-
node.type === 'ArrowFunctionExpression') &&
|
|
13276
|
-
node.metadata?.tsrx_dynamic_wrapper !== true
|
|
13277
|
-
) {
|
|
13278
|
-
return node;
|
|
13279
|
-
}
|
|
13280
|
-
if (node.type === 'JSXElement') {
|
|
13281
|
-
let working = node;
|
|
13282
|
-
if (!isCompositeJsxTag(node) || node.metadata?.dynamicElement) {
|
|
13283
|
-
working = addHashClassToElement(node, hash, classAttrName);
|
|
13284
|
-
}
|
|
13285
|
-
const children = working.children;
|
|
13286
|
-
if (Array.isArray(children)) {
|
|
13287
|
-
let newChildren = null;
|
|
13288
|
-
for (let i = 0; i < children.length; i++) {
|
|
13289
|
-
const mapped = annotateRootWithHash(children[i], hash, classAttrName);
|
|
13290
|
-
// Dropped style elements are filtered out (core: .filter(Boolean)).
|
|
13291
|
-
if (newChildren === null && (mapped !== children[i] || !mapped)) {
|
|
13292
|
-
newChildren = children.slice(0, i);
|
|
13293
|
-
}
|
|
13294
|
-
if (newChildren !== null && mapped) newChildren.push(mapped);
|
|
13295
|
-
}
|
|
13296
|
-
if (newChildren !== null) {
|
|
13297
|
-
if (working === node) working = { ...node, children: newChildren };
|
|
13298
|
-
else working.children = newChildren;
|
|
13299
|
-
}
|
|
13300
|
-
}
|
|
13301
|
-
return working;
|
|
13302
|
-
}
|
|
13303
|
-
// Scoped-CSS styles leave the render tree here; Float style resources stay —
|
|
13304
|
-
// the HeadHoist partition (normalizeChildren) owns removing them from body DOM.
|
|
13305
|
-
if (node.type === 'JSXStyleElement') return headResourceKind(node) === 'style' ? node : null;
|
|
13306
|
-
let out = null;
|
|
13307
|
-
for (const key of Object.keys(node)) {
|
|
13308
|
-
if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata' || key === 'css') {
|
|
13309
|
-
continue;
|
|
13310
|
-
}
|
|
13311
|
-
const value = node[key];
|
|
13312
|
-
if (!value || typeof value !== 'object') continue;
|
|
13313
|
-
let mapped;
|
|
13314
|
-
if (Array.isArray(value)) {
|
|
13315
|
-
// Core maps generic arrays WITHOUT filtering — a dropped style element
|
|
13316
|
-
// leaves a null slot the downstream child normalization skips.
|
|
13317
|
-
let newArray = null;
|
|
13318
|
-
for (let i = 0; i < value.length; i++) {
|
|
13319
|
-
const item = annotateRootWithHash(value[i], hash, classAttrName);
|
|
13320
|
-
if (newArray === null && item !== value[i]) newArray = value.slice(0, i);
|
|
13321
|
-
if (newArray !== null) newArray.push(item);
|
|
13322
|
-
}
|
|
13323
|
-
mapped = newArray ?? value;
|
|
13324
|
-
} else {
|
|
13325
|
-
mapped = annotateRootWithHash(value, hash, classAttrName);
|
|
13326
|
-
}
|
|
13327
|
-
if (mapped !== value) {
|
|
13328
|
-
if (out === null) out = { ...node };
|
|
13329
|
-
out[key] = mapped;
|
|
13330
|
-
}
|
|
13331
|
-
}
|
|
13332
|
-
return out ?? node;
|
|
13333
|
-
}
|
|
13334
|
-
|
|
13335
|
-
/**
|
|
13336
|
-
* Find the JSX render roots owned by a component. `@{}` components expose one
|
|
13337
|
-
* `JSXCodeBlock.render`; React-style functions can return JSX from any block in
|
|
13338
|
-
* their own body. Nested functions are separate component/value boundaries and
|
|
13339
|
-
* must not donate styles to their enclosing component.
|
|
13340
|
-
*/
|
|
13341
|
-
function componentStyleRoots(componentNode) {
|
|
13342
|
-
const body = componentNode.body;
|
|
13343
|
-
if (!body) return [];
|
|
13344
|
-
if (body.type === 'JSXCodeBlock') {
|
|
13345
|
-
return body.render ? [{ kind: 'render', node: body.render }] : [];
|
|
13346
|
-
}
|
|
13347
|
-
if (body.type !== 'BlockStatement') return [];
|
|
13348
|
-
|
|
13349
|
-
const roots = [];
|
|
13350
|
-
function visit(node) {
|
|
13351
|
-
if (!node || typeof node !== 'object') return;
|
|
13352
|
-
if (Array.isArray(node)) {
|
|
13353
|
-
for (const child of node) visit(child);
|
|
13354
|
-
return;
|
|
13355
|
-
}
|
|
13356
|
-
if (
|
|
13357
|
-
node.type === 'FunctionDeclaration' ||
|
|
13358
|
-
node.type === 'FunctionExpression' ||
|
|
13359
|
-
node.type === 'ArrowFunctionExpression'
|
|
13360
|
-
) {
|
|
13361
|
-
return;
|
|
13362
|
-
}
|
|
13363
|
-
if (node.type === 'ReturnStatement') {
|
|
13364
|
-
if (node.argument && isJsxNode(node.argument)) {
|
|
13365
|
-
roots.push({ kind: 'return', statement: node, node: node.argument });
|
|
13366
|
-
}
|
|
13367
|
-
return;
|
|
13368
|
-
}
|
|
13369
|
-
for (const key of Object.keys(node)) {
|
|
13370
|
-
if (
|
|
13371
|
-
key === 'loc' ||
|
|
13372
|
-
key === 'start' ||
|
|
13373
|
-
key === 'end' ||
|
|
13374
|
-
key === 'parent' ||
|
|
13375
|
-
key === 'metadata' ||
|
|
13376
|
-
key === 'css'
|
|
13377
|
-
)
|
|
13378
|
-
continue;
|
|
13379
|
-
visit(node[key]);
|
|
13380
|
-
}
|
|
13381
|
-
}
|
|
13382
|
-
visit(body);
|
|
13383
|
-
return roots;
|
|
13384
|
-
}
|
|
13385
|
-
|
|
13386
|
-
/**
|
|
13387
|
-
* Walk a component's owned render roots for `JSXStyleElement` nodes. For each
|
|
13388
|
-
* one found:
|
|
13389
|
-
* - Pull the pre-parsed `StyleSheet` AST out of its children.
|
|
13390
|
-
* - Run `prepareStylesheetForRender` (rewrites `.foo` → `.foo.<hash>`) over a
|
|
13391
|
-
* clone of the sheet — the core pipeline mutates what it is given.
|
|
13392
|
-
* - Collect into a list rendered via `renderStylesheets` to a CSS string.
|
|
13393
|
-
* - Register `{hash, css}` on `ctx.cssInjections` so a module-level
|
|
13394
|
-
* `injectStyle(hash, css)` is emitted in the prelude.
|
|
13395
|
-
* - Rebuild the render roots via `annotateRootWithHash` (COW) to stamp the
|
|
13396
|
-
* hash class on every native JSX element AND remove the JSXStyleElement
|
|
13397
|
-
* nodes from the rendered tree (they don't contribute DOM in the new model).
|
|
13398
|
-
*
|
|
13399
|
-
* Returns `{ cssHash, node }` — the hash (or `null` when no `<style>` blocks
|
|
13400
|
-
* are present) and the possibly-rebuilt component node the caller must use.
|
|
13401
|
-
*
|
|
13402
|
-
* The first `JSXStyleElement` contributes the canonical hash for the whole
|
|
13403
|
-
* component. @tsrx/core hashes individual style tags by source position, so
|
|
13404
|
-
* multiple blocks are explicitly rebased onto that canonical component hash
|
|
13405
|
-
* before selector rendering. Every rendered root receives the same hash class.
|
|
13406
|
-
*/
|
|
13407
|
-
function applyCssScoping(componentNode, ctx) {
|
|
13408
|
-
const roots = componentStyleRoots(componentNode);
|
|
13409
|
-
if (roots.length === 0) return { cssHash: null, node: componentNode };
|
|
13410
|
-
let cssHash = null;
|
|
13411
|
-
const styles = [];
|
|
13412
|
-
function collect(node) {
|
|
13413
|
-
if (!node || typeof node !== 'object') return;
|
|
13414
|
-
if (Array.isArray(node)) {
|
|
13415
|
-
for (const i of node) collect(i);
|
|
13416
|
-
return;
|
|
13417
|
-
}
|
|
13418
|
-
if (
|
|
13419
|
-
node.type === 'FunctionDeclaration' ||
|
|
13420
|
-
node.type === 'FunctionExpression' ||
|
|
13421
|
-
node.type === 'ArrowFunctionExpression'
|
|
13422
|
-
) {
|
|
13423
|
-
return;
|
|
13424
|
-
}
|
|
13425
|
-
if (node.type === 'JSXStyleElement') {
|
|
13426
|
-
// A Float style resource ships its plain CSS by href identity — it
|
|
13427
|
-
// neither contributes to the component's scope hash nor gets scoped.
|
|
13428
|
-
if (headResourceKind(node) === 'style') return;
|
|
13429
|
-
const sheet = (node.children || []).find((c) => c && c.type === 'StyleSheet');
|
|
13430
|
-
if (sheet) {
|
|
13431
|
-
styles.push({ node, sheet });
|
|
13432
|
-
if (!cssHash) cssHash = node.metadata?.styleScopeHash || sheet.hash || null;
|
|
13433
|
-
}
|
|
13434
|
-
return;
|
|
13435
|
-
}
|
|
13436
|
-
for (const key of Object.keys(node)) {
|
|
13437
|
-
if (
|
|
13438
|
-
key === 'loc' ||
|
|
13439
|
-
key === 'start' ||
|
|
13440
|
-
key === 'end' ||
|
|
13441
|
-
key === 'parent' ||
|
|
13442
|
-
key === 'metadata' ||
|
|
13443
|
-
key === 'css'
|
|
13444
|
-
)
|
|
13445
|
-
continue;
|
|
13446
|
-
const v = node[key];
|
|
13447
|
-
if (v && typeof v === 'object') collect(v);
|
|
13448
|
-
}
|
|
13449
|
-
}
|
|
13450
|
-
for (const root of roots) collect(root.node);
|
|
13451
|
-
if (!cssHash || styles.length === 0) return { cssHash: null, node: componentNode };
|
|
13452
|
-
// A component has one scope even when its CSS is split for readability.
|
|
13453
|
-
// Rebase before analyze/render: selector and keyframe rewriting read
|
|
13454
|
-
// `sheet.hash`, while DOM annotation below uses `cssHash`. The core
|
|
13455
|
-
// analyze/prepare pipeline mutates the sheet it is given, so it runs over a
|
|
13456
|
-
// clone of each (bounded) StyleSheet subtree — the parser-owned sheet nodes
|
|
13457
|
-
// stay pristine.
|
|
13458
|
-
const preparedSheets = styles.map((style) => {
|
|
13459
|
-
const sheet = cloneAstNode(style.sheet);
|
|
13460
|
-
sheet.hash = cssHash;
|
|
13461
|
-
// Mark `:global(...)` selectors before scoping so they render unscoped.
|
|
13462
|
-
analyzeCss(sheet);
|
|
13463
|
-
prepareStylesheetForRender(sheet);
|
|
13464
|
-
return sheet;
|
|
13465
|
-
});
|
|
13466
|
-
const css = renderStylesheets(preparedSheets);
|
|
13467
|
-
ctx.cssInjections.push({
|
|
13468
|
-
hash: cssHash,
|
|
13469
|
-
css,
|
|
13470
|
-
order: styles[0]?.sheet.start ?? styles[0]?.node.start ?? componentNode.start ?? 0,
|
|
13471
|
-
// The authored `<style>` element(s) this stylesheet came from.
|
|
13472
|
-
// `injectStyle` is emitted at module scope, so without an origin the
|
|
13473
|
-
// block maps to the module and neither the tag nor its CSS is reachable.
|
|
13474
|
-
origins: styles.map((style) => style.node).filter(Boolean),
|
|
13475
|
-
});
|
|
13476
|
-
ctx.runtimeNeeded.add('injectStyle');
|
|
13477
|
-
// Rebuild every owned render root copy-on-write: add the canonical hash
|
|
13478
|
-
// class to native elements and strip JSXStyleElement nodes from DOM output.
|
|
13479
|
-
// The rebuilt roots are grafted back through a spine rebuild of the
|
|
13480
|
-
// component node; the received component tree is never modified.
|
|
13481
|
-
let node = componentNode;
|
|
13482
|
-
const returnReplacements = new Map();
|
|
13483
|
-
for (const root of roots) {
|
|
13484
|
-
// Normalize dynamic class exprs BEFORE the hash is appended (see helper), so
|
|
13485
|
-
// clsx array/object values compose correctly alongside the scope hash.
|
|
13486
|
-
const wrapped = wrapScopedClassExprs(root.node, ctx);
|
|
13487
|
-
const annotated = annotateRootWithHash(wrapped, cssHash, 'class');
|
|
13488
|
-
if (annotated === root.node) continue;
|
|
13489
|
-
if (root.kind === 'render') {
|
|
13490
|
-
node = { ...node, body: { ...node.body, render: annotated } };
|
|
13491
|
-
} else {
|
|
13492
|
-
returnReplacements.set(root.statement, { ...root.statement, argument: annotated });
|
|
13493
|
-
}
|
|
13494
|
-
}
|
|
13495
|
-
if (returnReplacements.size > 0) {
|
|
13496
|
-
node = mapAst(node, (n) => returnReplacements.get(n) ?? null);
|
|
13497
|
-
}
|
|
13498
|
-
return { cssHash, node };
|
|
13499
|
-
}
|
|
13500
|
-
|
|
13501
13427
|
// Strip comments and plain string literals so prose or route paths cannot
|
|
13502
13428
|
// register as a component reference during the early-reference scan below.
|
|
13503
13429
|
// Template literals are deliberately LEFT intact: `${Name}` interpolations are
|
|
@@ -13727,14 +13653,15 @@ function compileComponent(node, ctx, options) {
|
|
|
13727
13653
|
: null;
|
|
13728
13654
|
const firstStyle = ctx.cssInjections.length;
|
|
13729
13655
|
|
|
13730
|
-
// Scoped `<style>`
|
|
13731
|
-
//
|
|
13732
|
-
//
|
|
13733
|
-
//
|
|
13734
|
-
//
|
|
13735
|
-
//
|
|
13736
|
-
//
|
|
13737
|
-
|
|
13656
|
+
// Scoped `<style>` blocks. TSRX surfaces each block as a `JSXStyleElement`
|
|
13657
|
+
// item of the list it is written in (parser pre-computes the content hash
|
|
13658
|
+
// + parses CSS into a StyleSheet AST). The pre-pass gives every list that
|
|
13659
|
+
// holds a block its own hash, rewrites `.foo` → `.foo.<hash>`, stamps the
|
|
13660
|
+
// hash on the list's other items and their descendants (never on the
|
|
13661
|
+
// element that contains the list), emits one `injectStyle(hash, css)` per
|
|
13662
|
+
// scope, and surfaces the first scope's hash as `cssHash` — kept only as a
|
|
13663
|
+
// "this component owns scoped CSS" flag for resolveStyleExpr's fallback.
|
|
13664
|
+
const scoping = applyStyleScopes(node, ctx);
|
|
13738
13665
|
node = scoping.node;
|
|
13739
13666
|
let cssHash = scoping.cssHash;
|
|
13740
13667
|
// Backwards-compat: internal callers (legacy synthetic Component shapes)
|
|
@@ -13855,7 +13782,9 @@ function compileComponent(node, ctx, options) {
|
|
|
13855
13782
|
: fnNode,
|
|
13856
13783
|
];
|
|
13857
13784
|
if (pendingWarm) {
|
|
13858
|
-
statements.push(
|
|
13785
|
+
statements.push(
|
|
13786
|
+
guarded(b.call(requireRuntimeForContext(ctx, 'markWarm'), b.id(name), pendingWarm)),
|
|
13787
|
+
);
|
|
13859
13788
|
}
|
|
13860
13789
|
if (hmrWrap && returnedOutput) {
|
|
13861
13790
|
statements.push(
|
|
@@ -13890,7 +13819,12 @@ function compileComponent(node, ctx, options) {
|
|
|
13890
13819
|
// identity by naming the inner FunctionExpression.
|
|
13891
13820
|
const fnExpr = functionExpressionFromDeclaration(fnNode);
|
|
13892
13821
|
const warmedFn = pendingWarm
|
|
13893
|
-
? markPure(
|
|
13822
|
+
? markPure(
|
|
13823
|
+
inheritOriginLoc(
|
|
13824
|
+
b.call(requireRuntimeForContext(ctx, 'markWarm'), fnExpr, pendingWarm),
|
|
13825
|
+
node,
|
|
13826
|
+
),
|
|
13827
|
+
)
|
|
13894
13828
|
: fnExpr;
|
|
13895
13829
|
const abiFn =
|
|
13896
13830
|
hmrWrap && returnedOutput
|
|
@@ -14370,6 +14304,32 @@ function compileFunctionBody(node, ctx, name, parentNs = 'html', cssHash = null,
|
|
|
14370
14304
|
bodyStatements.push(...mapTemps.map((temp) => inheritOriginLoc(b.let(temp), node)));
|
|
14371
14305
|
}
|
|
14372
14306
|
bodyStatements.push(...rewrittenStatements);
|
|
14307
|
+
// A self-update scheduled by setup discards this output. The owning Block
|
|
14308
|
+
// replays setup before returning to its parent, so children initialize from
|
|
14309
|
+
// the settled state and no template/resource work escapes the first pass.
|
|
14310
|
+
// Setup that only reads built-in hooks and pure globals cannot schedule an
|
|
14311
|
+
// update; keep those bodies unchanged (see setupCanScheduleSelfUpdate).
|
|
14312
|
+
if (
|
|
14313
|
+
ctx.mode !== 'server' &&
|
|
14314
|
+
ctx._universalRuntimeUnit == null &&
|
|
14315
|
+
!returnedOutput &&
|
|
14316
|
+
setupCanScheduleSelfUpdate(statements, ctx)
|
|
14317
|
+
) {
|
|
14318
|
+
bodyStatements.push(
|
|
14319
|
+
inheritOriginLoc(
|
|
14320
|
+
b.if(
|
|
14321
|
+
b.logical(
|
|
14322
|
+
'&&',
|
|
14323
|
+
b.member(b.member(b.id('__s'), 'block'), 'pending'),
|
|
14324
|
+
b.unary('!', b.member(b.member(b.id('__s'), 'block'), 'crossRenderUpdate')),
|
|
14325
|
+
),
|
|
14326
|
+
b.return(null),
|
|
14327
|
+
null,
|
|
14328
|
+
),
|
|
14329
|
+
node,
|
|
14330
|
+
),
|
|
14331
|
+
);
|
|
14332
|
+
}
|
|
14373
14333
|
// GLOBAL resource registrations (stylesheet precedence links, style
|
|
14374
14334
|
// resources, async scripts) run here: after setup, so an href computed from
|
|
14375
14335
|
// a body local is initialized, and BEFORE the constructs below mount any
|
|
@@ -14564,6 +14524,11 @@ function transformUniversalParallelUse(ast, ctx, metadata) {
|
|
|
14564
14524
|
node?.type === 'FunctionExpression' || node?.type === 'ArrowFunctionExpression';
|
|
14565
14525
|
const unwrapAssignedFunction = (node) => {
|
|
14566
14526
|
if (isFunction(node)) return node;
|
|
14527
|
+
if (
|
|
14528
|
+
calleeName(node) === runtimeAliasForContext(ctx, 'markWarm') &&
|
|
14529
|
+
isFunction(node.arguments?.[0])
|
|
14530
|
+
)
|
|
14531
|
+
return node.arguments[0];
|
|
14567
14532
|
if (
|
|
14568
14533
|
node?.type === 'CallExpression' &&
|
|
14569
14534
|
node.callee?.type === 'MemberExpression' &&
|
|
@@ -14696,14 +14661,7 @@ function transformUniversalParallelUse(ast, ctx, metadata) {
|
|
|
14696
14661
|
};
|
|
14697
14662
|
// The warm-plan attach maps to the component function it decorates.
|
|
14698
14663
|
const assignWarmPlan = (fn, warmNode) =>
|
|
14699
|
-
inheritOriginLoc(
|
|
14700
|
-
b.call(
|
|
14701
|
-
b.member(b.id('Object'), 'assign'),
|
|
14702
|
-
fn,
|
|
14703
|
-
b.object([b.prop('init', b.id('__warm'), warmNode)]),
|
|
14704
|
-
),
|
|
14705
|
-
fn,
|
|
14706
|
-
);
|
|
14664
|
+
inheritOriginLoc(b.call(requireRuntimeForContext(ctx, 'markWarm'), fn, warmNode), fn);
|
|
14707
14665
|
|
|
14708
14666
|
const transformFunction = (fn, name, component) => {
|
|
14709
14667
|
if (!isFunction(fn) || transformed.has(fn)) return fn;
|
|
@@ -15764,7 +15722,11 @@ function parallelUseWalkJsx(nodes, ctx, componentName, creations, warmChildren,
|
|
|
15764
15722
|
const nameNode = node.openingElement && node.openingElement.name;
|
|
15765
15723
|
const isComponent =
|
|
15766
15724
|
nameNode && nameNode.type === 'JSXIdentifier' && /^[A-Z]/.test(nameNode.name);
|
|
15767
|
-
|
|
15725
|
+
const transparentBoundary =
|
|
15726
|
+
isComponent &&
|
|
15727
|
+
ctx._octaneSuspenseNames?.has(nameNode.name) &&
|
|
15728
|
+
!ctx.currentComponentLocals?.has(nameNode.name);
|
|
15729
|
+
if (isComponent && !transparentBoundary) {
|
|
15768
15730
|
// Server: creations flowing INTO child props (`<Kid p={make(x)}/>`)
|
|
15769
15731
|
// need the same cross-pass identity as use()-site creations — every
|
|
15770
15732
|
// streaming wave re-runs this body, and a recreated per-request
|
|
@@ -16424,7 +16386,7 @@ function buildWarmArtifacts(node, ctx, componentName, creations, warmChildren) {
|
|
|
16424
16386
|
// The hoisted fetch plan: creations + child calls, params destructured
|
|
16425
16387
|
// from the incoming props object. Single-param components only (the norm).
|
|
16426
16388
|
// Returned as a bare arrow — compileComponent attaches it to the INNER
|
|
16427
|
-
// function object via
|
|
16389
|
+
// function object via markWarm, so the component's own body (where
|
|
16428
16390
|
// the function-expression name shadows the module const) sees it too;
|
|
16429
16391
|
// hmr() forwards it from the wrapped fn onto the HMR wrapper.
|
|
16430
16392
|
let warmNode = null;
|
|
@@ -16644,13 +16606,17 @@ const NUMERIC_HOOK_SLOT_POSITION = {
|
|
|
16644
16606
|
useEffectEvent: 1,
|
|
16645
16607
|
useImperativeHandle: 3,
|
|
16646
16608
|
useActionState: 3,
|
|
16609
|
+
useFormState: 3,
|
|
16647
16610
|
useOptimistic: 2,
|
|
16648
16611
|
useSignal$: 1,
|
|
16649
16612
|
};
|
|
16650
16613
|
|
|
16651
16614
|
function appendHookSlotArgument(name, args, slot, numeric, origin) {
|
|
16652
16615
|
const out = [...args];
|
|
16653
|
-
const position =
|
|
16616
|
+
const position =
|
|
16617
|
+
numeric || name === 'useState' || name === 'useRef'
|
|
16618
|
+
? NUMERIC_HOOK_SLOT_POSITION[name]
|
|
16619
|
+
: undefined;
|
|
16654
16620
|
if (position !== undefined) {
|
|
16655
16621
|
while (out.length < position) out.push(b.id('undefined', origin));
|
|
16656
16622
|
}
|
|
@@ -16658,6 +16624,18 @@ function appendHookSlotArgument(name, args, slot, numeric, origin) {
|
|
|
16658
16624
|
return out;
|
|
16659
16625
|
}
|
|
16660
16626
|
|
|
16627
|
+
function wrapHookCallWithSlot(call, ctx, slot, callee, args) {
|
|
16628
|
+
const fn = call.typeArguments
|
|
16629
|
+
? { type: 'TSInstantiationExpression', expression: callee, typeArguments: call.typeArguments }
|
|
16630
|
+
: callee;
|
|
16631
|
+
return {
|
|
16632
|
+
...call,
|
|
16633
|
+
callee: b.id(requireRuntimeForContext(ctx, 'withSlot')),
|
|
16634
|
+
typeArguments: null,
|
|
16635
|
+
arguments: [typeof slot === 'string' ? b.id(slot) : slot, fn, ...args],
|
|
16636
|
+
};
|
|
16637
|
+
}
|
|
16638
|
+
|
|
16661
16639
|
function arrayPatternObservesStateGetter(pattern) {
|
|
16662
16640
|
const elements = pattern.elements || [];
|
|
16663
16641
|
if (elements[2] != null) return true;
|
|
@@ -17308,20 +17286,20 @@ function rewriteHookCalls(node, ctx, componentName, localRoot = false) {
|
|
|
17308
17286
|
const importedName = n._octaneImportedHook;
|
|
17309
17287
|
const hookRuntimeImportedName = n._octaneHookRuntimeImportedHook;
|
|
17310
17288
|
const name = importedName ?? hookRuntimeImportedName ?? localName;
|
|
17311
|
-
// Three kinds of call
|
|
17289
|
+
// Three kinds of call receive a per-call-site hook identity:
|
|
17312
17290
|
// - a built-in base hook (HOOK_NAMES) — also needs its runtime import;
|
|
17313
17291
|
// - a custom / library hook by React's `use[A-Z]` convention — e.g. a
|
|
17314
17292
|
// `useStore` binding from @octanejs/zustand that WRAPS a base hook and
|
|
17315
|
-
//
|
|
17293
|
+
// resolves the enclosing call path. `useContext` is keyed by context identity (no
|
|
17316
17294
|
// slot) and `use` has no uppercase suffix, so both are excluded here. We
|
|
17317
17295
|
// do NOT import custom hooks — they're user/library imports;
|
|
17318
17296
|
// - a server-mode `use(thenable)` — a stable suspense-cache key.
|
|
17319
17297
|
// Distinct call sites get distinct slots, so `useStore(a)`/`useStore(b)`
|
|
17320
17298
|
// (or the same hook twice) stay independent.
|
|
17321
17299
|
//
|
|
17322
|
-
//
|
|
17323
|
-
//
|
|
17324
|
-
//
|
|
17300
|
+
// A `use*` name is reserved for hooks. The custom wrapper preserves its
|
|
17301
|
+
// authored argument list; inside a plain JS loop the convention is
|
|
17302
|
+
// enforced by rejectHookInJsLoop.
|
|
17325
17303
|
const isBuiltin =
|
|
17326
17304
|
(HOOK_NAMES.has(name) ||
|
|
17327
17305
|
(ctx.nativeReads &&
|
|
@@ -17338,10 +17316,9 @@ function rewriteHookCalls(node, ctx, componentName, localRoot = false) {
|
|
|
17338
17316
|
const inline = lowerLocalMemoExpression(n, name);
|
|
17339
17317
|
if (inline !== null) return inline;
|
|
17340
17318
|
}
|
|
17341
|
-
// Keep a Symbol
|
|
17342
|
-
//
|
|
17343
|
-
//
|
|
17344
|
-
// production ABI; arbitrary callable helpers cannot.
|
|
17319
|
+
// Keep a Symbol on custom-hook call paths so their internal base-hook
|
|
17320
|
+
// identities compose across modules. Proven render-scope base hooks
|
|
17321
|
+
// can use the smaller numeric production ABI.
|
|
17345
17322
|
const hasSpread = n.arguments.some((arg) => arg.type === 'SpreadElement');
|
|
17346
17323
|
const memoRoute = isBuiltin && canLowerClientMemo(n, name);
|
|
17347
17324
|
const memoCandidate = memoRoute ? slotMemoCandidate(n, name) : null;
|
|
@@ -17406,18 +17383,20 @@ function rewriteHookCalls(node, ctx, componentName, localRoot = false) {
|
|
|
17406
17383
|
// separately must not re-promote its nested hooks to render-local.
|
|
17407
17384
|
const argsLocalRoot = localSlotMarks.get(n) === true;
|
|
17408
17385
|
const args = n.arguments.map((a) => rewriteHookCalls(a, ctx, componentName, argsLocalRoot));
|
|
17409
|
-
|
|
17410
|
-
|
|
17411
|
-
|
|
17412
|
-
|
|
17413
|
-
|
|
17414
|
-
|
|
17415
|
-
|
|
17416
|
-
|
|
17417
|
-
|
|
17418
|
-
|
|
17419
|
-
|
|
17420
|
-
|
|
17386
|
+
const callee = nativeMemo
|
|
17387
|
+
? b.id(runtimeAliasForContext(ctx, 'nativePuMemo'))
|
|
17388
|
+
: getterHelper !== null
|
|
17389
|
+
? b.id(runtimeAliasForContext(ctx, getterHelper))
|
|
17390
|
+
: n.callee._octaneGenerated
|
|
17391
|
+
? b.id(runtimeAliasForContext(ctx, name))
|
|
17392
|
+
: n.callee;
|
|
17393
|
+
// The call path supplies identity without adding an argument that
|
|
17394
|
+
// changes a custom hook's defaults, rest values or arguments.length.
|
|
17395
|
+
// State/ref spreads also need this path: an empty spread has no
|
|
17396
|
+
// initializer position into which a trailing slot may safely fall.
|
|
17397
|
+
if (isCustom || (hasSpread && (name === 'useState' || name === 'useRef')))
|
|
17398
|
+
return wrapHookCallWithSlot(n, ctx, slot, callee, args);
|
|
17399
|
+
|
|
17421
17400
|
const hookArgs = explicitMemoSlot
|
|
17422
17401
|
? args
|
|
17423
17402
|
: appendHookSlotArgument(name, args, slot, numericSlot, n);
|
|
@@ -17432,13 +17411,7 @@ function rewriteHookCalls(node, ctx, componentName, localRoot = false) {
|
|
|
17432
17411
|
return {
|
|
17433
17412
|
...n,
|
|
17434
17413
|
...(memoRoute ? { _octaneInlineMemoHook: memoCandidate } : null),
|
|
17435
|
-
callee
|
|
17436
|
-
? b.id(runtimeAliasForContext(ctx, 'nativePuMemo'))
|
|
17437
|
-
: getterHelper !== null
|
|
17438
|
-
? b.id(runtimeAliasForContext(ctx, getterHelper))
|
|
17439
|
-
: n.callee._octaneGenerated
|
|
17440
|
-
? b.id(runtimeAliasForContext(ctx, name))
|
|
17441
|
-
: n.callee,
|
|
17414
|
+
callee,
|
|
17442
17415
|
arguments: hookArgs,
|
|
17443
17416
|
};
|
|
17444
17417
|
}
|
|
@@ -17505,14 +17478,20 @@ function rewriteHookCalls(node, ctx, componentName, localRoot = false) {
|
|
|
17505
17478
|
const args = n.arguments.map((a) =>
|
|
17506
17479
|
rewriteHookCalls(a, ctx, componentName, localSlotMarks.get(n) === true),
|
|
17507
17480
|
);
|
|
17481
|
+
const callee = nativeMemo
|
|
17482
|
+
? b.id(runtimeAliasForContext(ctx, 'nativePuMemo'))
|
|
17483
|
+
: getterHelper !== null
|
|
17484
|
+
? b.id(runtimeAliasForContext(ctx, getterHelper))
|
|
17485
|
+
: n.callee;
|
|
17486
|
+
if (
|
|
17487
|
+
(name === 'useState' || name === 'useRef') &&
|
|
17488
|
+
args.some((arg) => arg.type === 'SpreadElement')
|
|
17489
|
+
)
|
|
17490
|
+
return wrapHookCallWithSlot(n, ctx, slot, callee, args);
|
|
17508
17491
|
return {
|
|
17509
17492
|
...n,
|
|
17510
17493
|
...(memoRoute ? { _octaneInlineMemoHook: memoCandidate } : null),
|
|
17511
|
-
callee
|
|
17512
|
-
? b.id(runtimeAliasForContext(ctx, 'nativePuMemo'))
|
|
17513
|
-
: getterHelper !== null
|
|
17514
|
-
? b.id(runtimeAliasForContext(ctx, getterHelper))
|
|
17515
|
-
: n.callee,
|
|
17494
|
+
callee,
|
|
17516
17495
|
arguments: explicitMemoSlot
|
|
17517
17496
|
? args
|
|
17518
17497
|
: appendHookSlotArgument(name, args, slot, numericSlot, n),
|
|
@@ -17524,9 +17503,8 @@ function rewriteHookCalls(node, ctx, componentName, localRoot = false) {
|
|
|
17524
17503
|
// and the like). Same `use[A-Z]` convention, applied to the PROPERTY name.
|
|
17525
17504
|
// The callee is a member access, so the plain withSlot form would sever
|
|
17526
17505
|
// `this`; instead the WHOLE call is wrapped in a thunk —
|
|
17527
|
-
// `_$withSlot(sym, () => obj.useX(...args
|
|
17528
|
-
//
|
|
17529
|
-
// `sym` still reaches slot-forwarding bindings (splitSlot convention).
|
|
17506
|
+
// `_$withSlot(sym, () => obj.useX(...args))` preserves the receiver and
|
|
17507
|
+
// authored arguments while supplying the inner base hooks' call path.
|
|
17530
17508
|
if (
|
|
17531
17509
|
n.type === 'CallExpression' &&
|
|
17532
17510
|
!n.optional &&
|
|
@@ -17558,7 +17536,7 @@ function rewriteHookCalls(node, ctx, componentName, localRoot = false) {
|
|
|
17558
17536
|
return b.call(
|
|
17559
17537
|
withSlotAlias,
|
|
17560
17538
|
b.id(symVar),
|
|
17561
|
-
b.arrow([],
|
|
17539
|
+
b.arrow([], { ...n, callee: { ...n.callee, object }, arguments: args }),
|
|
17562
17540
|
);
|
|
17563
17541
|
}
|
|
17564
17542
|
return null;
|
|
@@ -17574,7 +17552,7 @@ function compileReturnJsxFunction(node, ctx, options) {
|
|
|
17574
17552
|
const hookMemoOpaqueOwner = ctx.inlineHookMemo && hasInlineMemoOpaqueDirective(node);
|
|
17575
17553
|
recordProfileComponent(ctx, node, name);
|
|
17576
17554
|
const beforeCss = ctx.cssInjections.length;
|
|
17577
|
-
const scoping =
|
|
17555
|
+
const scoping = applyStyleScopes(node, ctx);
|
|
17578
17556
|
node = scoping.node;
|
|
17579
17557
|
const cssHash = scoping.cssHash;
|
|
17580
17558
|
const cssEntries =
|
|
@@ -17696,6 +17674,7 @@ function compileReturnJsxFunction(node, ctx, options) {
|
|
|
17696
17674
|
compInlinedSubs,
|
|
17697
17675
|
cssHash,
|
|
17698
17676
|
cssEntries,
|
|
17677
|
+
scoping.runtimeApplied,
|
|
17699
17678
|
),
|
|
17700
17679
|
};
|
|
17701
17680
|
}
|
|
@@ -17775,6 +17754,10 @@ function compileReturnJsxFunction(node, ctx, options) {
|
|
|
17775
17754
|
}),
|
|
17776
17755
|
);
|
|
17777
17756
|
}
|
|
17757
|
+
if (ctx.mode === 'server' && !ctx.nativeReads) {
|
|
17758
|
+
// Imported themes inject on read; touch them ahead of this body's own CSS.
|
|
17759
|
+
returnBody.unshift(...serverThemeTouchStatements(scoping.runtimeApplied, ctx, node));
|
|
17760
|
+
}
|
|
17778
17761
|
const emittedFunction = b.function_declaration(
|
|
17779
17762
|
node.id,
|
|
17780
17763
|
node.params,
|
|
@@ -17818,7 +17801,15 @@ function compileReturnJsxFunction(node, ctx, options) {
|
|
|
17818
17801
|
return { nodes: [fn] };
|
|
17819
17802
|
}
|
|
17820
17803
|
|
|
17821
|
-
function nativeReturnedJsxValue(
|
|
17804
|
+
function nativeReturnedJsxValue(
|
|
17805
|
+
node,
|
|
17806
|
+
ctx,
|
|
17807
|
+
name,
|
|
17808
|
+
inlinedSubs,
|
|
17809
|
+
cssHash,
|
|
17810
|
+
cssEntries,
|
|
17811
|
+
runtimeApplied = [],
|
|
17812
|
+
) {
|
|
17822
17813
|
let value;
|
|
17823
17814
|
if (requiresTemplateNormalization(node, 'html', true, ctx)) {
|
|
17824
17815
|
// Head resources, directive roots and other compiler-only syntax keep
|
|
@@ -17838,7 +17829,8 @@ function nativeReturnedJsxValue(node, ctx, name, inlinedSubs, cssHash, cssEntrie
|
|
|
17838
17829
|
}
|
|
17839
17830
|
// A stored styled value may be constructed outside an SSR render. Its CSS
|
|
17840
17831
|
// enters the request collector when interpreted, not a global pending queue.
|
|
17841
|
-
|
|
17832
|
+
const themeTouches = serverThemeTouchStatements(runtimeApplied, ctx, node);
|
|
17833
|
+
if (cssHash === null && !ctx.nativeModuleStyles && themeTouches.length === 0) return value;
|
|
17842
17834
|
const scopedElement = runtimeAliasForContext(ctx, 'nativeCreateScopedElement');
|
|
17843
17835
|
const scopedValue = runtimeAliasForContext(ctx, 'nativeCreateScopedValue');
|
|
17844
17836
|
// A static styled fragment needs a reader too, even when it only owns a
|
|
@@ -17859,15 +17851,18 @@ function nativeReturnedJsxValue(node, ctx, name, inlinedSubs, cssHash, cssEntrie
|
|
|
17859
17851
|
node,
|
|
17860
17852
|
);
|
|
17861
17853
|
}
|
|
17862
|
-
if (cssEntries.length === 0) return value;
|
|
17863
|
-
ctx.runtimeNeeded.add('injectStyle');
|
|
17864
|
-
const css =
|
|
17865
|
-
|
|
17866
|
-
|
|
17867
|
-
|
|
17868
|
-
|
|
17869
|
-
|
|
17870
|
-
|
|
17854
|
+
if (cssEntries.length === 0 && themeTouches.length === 0) return value;
|
|
17855
|
+
if (cssEntries.length > 0) ctx.runtimeNeeded.add('injectStyle');
|
|
17856
|
+
const css = [
|
|
17857
|
+
...themeTouches,
|
|
17858
|
+
...cssEntries.map((entry) => {
|
|
17859
|
+
const origin = claimCssOrigins(ctx, entry) ?? node;
|
|
17860
|
+
return inheritOriginLoc(
|
|
17861
|
+
b.stmt(ssrCall('injectStyle', [b.literal(entry.hash), b.literal(entry.css)], origin)),
|
|
17862
|
+
origin,
|
|
17863
|
+
);
|
|
17864
|
+
}),
|
|
17865
|
+
];
|
|
17871
17866
|
const readerIndex =
|
|
17872
17867
|
value.callee?.name === scopedValue ? 0 : value.callee?.name === scopedElement ? 2 : -1;
|
|
17873
17868
|
if (readerIndex === -1) {
|
|
@@ -18012,11 +18007,22 @@ function requiresTemplateNormalization(
|
|
|
18012
18007
|
t === 'FragmentStart' ||
|
|
18013
18008
|
t === 'FragmentEnd' ||
|
|
18014
18009
|
t === 'FoldedDirective' ||
|
|
18015
|
-
t === 'HeadHoist'
|
|
18016
|
-
t === 'JSXCodeBlock'
|
|
18010
|
+
t === 'HeadHoist'
|
|
18017
18011
|
) {
|
|
18018
18012
|
return true;
|
|
18019
18013
|
}
|
|
18014
|
+
if (t === 'JSXCodeBlock') {
|
|
18015
|
+
// A render-only child block is transparent template grouping (the same
|
|
18016
|
+
// rule normalizeChildren and extractFragment apply): only its render
|
|
18017
|
+
// decides. A setup-bearing or code-only block is a render scope of its
|
|
18018
|
+
// own and needs the compiled-fragment path. A styled static fragment
|
|
18019
|
+
// therefore stays a descriptor when its block only groups the fragment
|
|
18020
|
+
// that holds the `<style>` — the form amendment A1 leaves a value factory
|
|
18021
|
+
// written in plain TSX (raw CSS needs a `@{ … }` container).
|
|
18022
|
+
const body = node.body || [];
|
|
18023
|
+
if (body.length > 0 || !node.render) return true;
|
|
18024
|
+
return requiresTemplateNormalization(node.render, parentNs, allowHeadHoists, ctx);
|
|
18025
|
+
}
|
|
18020
18026
|
if (t === 'Fragment' || t === 'JSXFragment' || t === 'Tsx' || t === 'Tsrx') {
|
|
18021
18027
|
return (node.children || []).some((child) =>
|
|
18022
18028
|
requiresTemplateNormalization(child, parentNs, allowHeadHoists, ctx),
|
|
@@ -18319,6 +18325,15 @@ function extractFragment(node, ctx, holeProps, parentNs = 'html') {
|
|
|
18319
18325
|
// `{/* … */}` is a `JSXEmptyExpression` container — a JSX comment, which
|
|
18320
18326
|
// renders to nothing (React drops it). Skip it so it never becomes a hole.
|
|
18321
18327
|
if (!expr || expr.type === 'JSXEmptyExpression') continue;
|
|
18328
|
+
// String literals (`{" "}`, `{"hello"}`) are static text, same as JSXText.
|
|
18329
|
+
// The .tsrx and server paths fold them via staticTextLiteral; hoisting one
|
|
18330
|
+
// as hN here made .tsx client templates grow an extra text hole while the
|
|
18331
|
+
// server baked the string, so hydration consumed the next sibling (duplicate
|
|
18332
|
+
// elements). Leave the container in place so emitElementHtml bakes it.
|
|
18333
|
+
if (staticTextLiteral(expr) !== null) {
|
|
18334
|
+
newChildren.push(child);
|
|
18335
|
+
continue;
|
|
18336
|
+
}
|
|
18322
18337
|
const hn = `h${holeProps.length}`;
|
|
18323
18338
|
if (expr && expr.type === 'TSAsExpression') {
|
|
18324
18339
|
// Preserve the `as T` cast in the renderer (it marks a dynamic TEXT hole).
|
|
@@ -19217,6 +19232,20 @@ function rewriteJsxValues(node, ctx, eagerMapCallbackRoots = false, eagerMapCall
|
|
|
19217
19232
|
if (lower != null) node = lowerSetupValueDirectives(node, lower);
|
|
19218
19233
|
return mapAst(node, (n) => {
|
|
19219
19234
|
const t = n && n.type;
|
|
19235
|
+
if (isFunctionNode(n) && n.body?.type === 'JSXCodeBlock') {
|
|
19236
|
+
const name = n.id?.name ?? allocCompilerName(ctx, '__template');
|
|
19237
|
+
const previousLocals = ctx.currentComponentLocals;
|
|
19238
|
+
ctx.currentComponentLocals = collectComponentLocals(n);
|
|
19239
|
+
try {
|
|
19240
|
+
const compiled =
|
|
19241
|
+
ctx.mode === 'server'
|
|
19242
|
+
? ssrCompileBody(n, ctx, name, null, [], 'opaque')
|
|
19243
|
+
: compileFunctionBody(n, ctx, name, 'opaque');
|
|
19244
|
+
return functionExpressionFromDeclaration({ ...compiled, id: n.id ?? null }, n);
|
|
19245
|
+
} finally {
|
|
19246
|
+
ctx.currentComponentLocals = previousLocals;
|
|
19247
|
+
}
|
|
19248
|
+
}
|
|
19220
19249
|
if (
|
|
19221
19250
|
ctx.nativeReads &&
|
|
19222
19251
|
lower == null &&
|
|
@@ -20067,11 +20096,6 @@ function jsxTagName(n) {
|
|
|
20067
20096
|
: null;
|
|
20068
20097
|
}
|
|
20069
20098
|
|
|
20070
|
-
/** @param {any} n @returns {boolean} */
|
|
20071
|
-
function isHeadElementNode(n) {
|
|
20072
|
-
return Boolean(n && n.type === 'JSXElement' && jsxTagName(n) === 'head');
|
|
20073
|
-
}
|
|
20074
|
-
|
|
20075
20099
|
/** @param {any} n @returns {boolean} */
|
|
20076
20100
|
function isHoistableHeadElementNode(n) {
|
|
20077
20101
|
if (n == null || (n.type !== 'JSXElement' && n.type !== 'Element')) return false;
|
|
@@ -20111,7 +20135,7 @@ function isHoistableHeadElementNode(n) {
|
|
|
20111
20135
|
inner.value === 'stylesheet';
|
|
20112
20136
|
}
|
|
20113
20137
|
}
|
|
20114
|
-
if (tag === 'link' && relStylesheet && hasPrecedence
|
|
20138
|
+
if (tag === 'link' && relStylesheet && (!hasPrecedence || !hasHref || emptyHref)) return false;
|
|
20115
20139
|
return true;
|
|
20116
20140
|
}
|
|
20117
20141
|
|
|
@@ -20329,7 +20353,7 @@ function deferredHeadAttrs(el) {
|
|
|
20329
20353
|
// props.children under HTML, SVG, or MathML. namespaceHead resolves that choice
|
|
20330
20354
|
// from its actual parent at render time while retaining ordinary component-block
|
|
20331
20355
|
// hydration and keyed ownership.
|
|
20332
|
-
function deferredTitleElement(el, ctx) {
|
|
20356
|
+
function deferredTitleElement(el, ctx, tag = 'title') {
|
|
20333
20357
|
ctx.runtimeNeeded.add('namespaceHeadElement');
|
|
20334
20358
|
let authoredKey = null;
|
|
20335
20359
|
for (const attr of el.openingElement.attributes || []) {
|
|
@@ -20345,7 +20369,7 @@ function deferredTitleElement(el, ctx) {
|
|
|
20345
20369
|
}
|
|
20346
20370
|
const args = [
|
|
20347
20371
|
b.literal(headKey(el, 0, ctx)),
|
|
20348
|
-
b.literal(
|
|
20372
|
+
b.literal(tag),
|
|
20349
20373
|
deferredHeadAttrs(el),
|
|
20350
20374
|
headTextExpression(el),
|
|
20351
20375
|
];
|
|
@@ -20658,9 +20682,137 @@ function emitHeadServer(headNodes, ctx) {
|
|
|
20658
20682
|
});
|
|
20659
20683
|
}
|
|
20660
20684
|
|
|
20685
|
+
// JSX indentation is syntax, while same-line spaces are rendered content. Cook
|
|
20686
|
+
// authored literals once, before either emitter sees them, so entities cannot be
|
|
20687
|
+
// decoded twice as a tree passes through fragment and value lowering.
|
|
20688
|
+
function normalizeAuthoredJsxLiterals(ast) {
|
|
20689
|
+
return mapAst(ast, (node) => {
|
|
20690
|
+
if (node.type === 'JSXText') {
|
|
20691
|
+
const lines = node.value.split(/\r\n|\n|\r/);
|
|
20692
|
+
let last = 0;
|
|
20693
|
+
for (let i = 0; i < lines.length; i++) if (/[^ \t]/.test(lines[i])) last = i;
|
|
20694
|
+
let value = '';
|
|
20695
|
+
for (let i = 0; i < lines.length; i++) {
|
|
20696
|
+
let line = lines[i].replace(/\t/g, ' ');
|
|
20697
|
+
if (i !== 0) line = line.replace(/^ +/, '');
|
|
20698
|
+
if (i !== lines.length - 1) line = line.replace(/ +$/, '');
|
|
20699
|
+
if (line !== '') value += line + (i !== last ? ' ' : '');
|
|
20700
|
+
}
|
|
20701
|
+
if (value.includes('&')) value = decodeHTMLStrict(value);
|
|
20702
|
+
return value === node.value ? node : { ...node, value, raw: value };
|
|
20703
|
+
}
|
|
20704
|
+
if (
|
|
20705
|
+
node.type === 'JSXAttribute' &&
|
|
20706
|
+
node.value?.type === 'Literal' &&
|
|
20707
|
+
typeof node.value.value === 'string' &&
|
|
20708
|
+
node.value.value.includes('&')
|
|
20709
|
+
) {
|
|
20710
|
+
const value = decodeHTMLStrict(node.value.value);
|
|
20711
|
+
return { ...node, value: { ...node.value, value, raw: JSON.stringify(value) } };
|
|
20712
|
+
}
|
|
20713
|
+
if (
|
|
20714
|
+
node.type === 'JSXStyleElement' &&
|
|
20715
|
+
(hasJsxAttribute(node, 'dangerouslySetInnerHTML') || hasJsxAttribute(node, 'children'))
|
|
20716
|
+
) {
|
|
20717
|
+
return normalizeAuthoredJsxLiterals(
|
|
20718
|
+
b.jsx_element(node, node.openingElement.attributes || [], []),
|
|
20719
|
+
);
|
|
20720
|
+
}
|
|
20721
|
+
return null;
|
|
20722
|
+
});
|
|
20723
|
+
}
|
|
20724
|
+
|
|
20725
|
+
function requiresImperativeHostTree(root, ctx) {
|
|
20726
|
+
const tag = jsxTagName(root) || elementTagName(root);
|
|
20727
|
+
return (
|
|
20728
|
+
(ctx.mode !== 'server' && root._octaneImperativeHost === true) ||
|
|
20729
|
+
hasJsxAttribute(root, 'key') ||
|
|
20730
|
+
tag === 'noscript' ||
|
|
20731
|
+
tag === 'html' ||
|
|
20732
|
+
tag === 'head' ||
|
|
20733
|
+
tag === 'body'
|
|
20734
|
+
);
|
|
20735
|
+
}
|
|
20736
|
+
|
|
20737
|
+
// Analyze parser repair once per authored tree. Mark only the affected ancestor,
|
|
20738
|
+
// preserving template compilation for unrelated hosts around that subtree.
|
|
20739
|
+
function markParserSensitiveHosts(ast) {
|
|
20740
|
+
const unsafe = new WeakSet();
|
|
20741
|
+
let found = false;
|
|
20742
|
+
const mark = (node) => {
|
|
20743
|
+
unsafe.add(node);
|
|
20744
|
+
found = true;
|
|
20745
|
+
};
|
|
20746
|
+
function visit(node, ancestors = [], namespace = 'html') {
|
|
20747
|
+
if (node === null || typeof node !== 'object') return;
|
|
20748
|
+
if (Array.isArray(node)) {
|
|
20749
|
+
for (const child of node) visit(child, ancestors, namespace);
|
|
20750
|
+
return;
|
|
20751
|
+
}
|
|
20752
|
+
if (node.type === 'JSXText') {
|
|
20753
|
+
if (ancestors.length && invalidHtmlTextNesting(node.value, ancestors[0].tag) !== null)
|
|
20754
|
+
mark(ancestors[0].node);
|
|
20755
|
+
return;
|
|
20756
|
+
}
|
|
20757
|
+
if (node.type === 'JSXFragment') {
|
|
20758
|
+
visit(node.children || [], ancestors, namespace);
|
|
20759
|
+
return;
|
|
20760
|
+
}
|
|
20761
|
+
if (node.type === 'JSXElement') {
|
|
20762
|
+
const tag = jsxTagName(node);
|
|
20763
|
+
if (isComponentTag(node)) {
|
|
20764
|
+
visit(node.children || []);
|
|
20765
|
+
return;
|
|
20766
|
+
}
|
|
20767
|
+
const self = nsForSelf(tag, namespace);
|
|
20768
|
+
if (self === 'html' && ancestors.length) {
|
|
20769
|
+
if (invalidHtmlNestingWithParent(tag, ancestors[0].tag) !== null) mark(ancestors[0].node);
|
|
20770
|
+
const tags = ancestors.map((entry) => entry.tag);
|
|
20771
|
+
for (let i = 1; i < ancestors.length; i++) {
|
|
20772
|
+
if (invalidHtmlNestingWithAncestor(tag, tags.slice(0, i + 1)) !== null)
|
|
20773
|
+
mark(ancestors[i].node);
|
|
20774
|
+
}
|
|
20775
|
+
}
|
|
20776
|
+
const childNamespace = nsForChildren(tag, namespace);
|
|
20777
|
+
visit(
|
|
20778
|
+
node.children || [],
|
|
20779
|
+
childNamespace === 'html' ? [{ node, tag }, ...ancestors] : [],
|
|
20780
|
+
childNamespace,
|
|
20781
|
+
);
|
|
20782
|
+
visit(node.openingElement?.attributes || []);
|
|
20783
|
+
return;
|
|
20784
|
+
}
|
|
20785
|
+
// A value expression or directive owns its insertion range. Its elements
|
|
20786
|
+
// are parsed separately, so lexical host ancestry does not cross it.
|
|
20787
|
+
for (const key in node) {
|
|
20788
|
+
if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') continue;
|
|
20789
|
+
visit(node[key]);
|
|
20790
|
+
}
|
|
20791
|
+
}
|
|
20792
|
+
visit(ast);
|
|
20793
|
+
return found
|
|
20794
|
+
? mapAst(ast, (node) => (unsafe.has(node) ? { ...node, _octaneImperativeHost: true } : null))
|
|
20795
|
+
: ast;
|
|
20796
|
+
}
|
|
20797
|
+
|
|
20798
|
+
function rewriteImperativeHeadElements(node, ctx, namespace = 'html', inNoscript = false) {
|
|
20799
|
+
if (node?.type !== 'JSXElement' && node?.type !== 'JSXFragment') return node;
|
|
20800
|
+
const tag = jsxTagName(node);
|
|
20801
|
+
const nextNamespace = tag ? nsForChildren(tag, namespace) : namespace;
|
|
20802
|
+
if (!inNoscript && namespace === 'html' && isHoistableHeadElementNode(node)) {
|
|
20803
|
+
return deferredTitleElement(node, ctx, tag);
|
|
20804
|
+
}
|
|
20805
|
+
return {
|
|
20806
|
+
...node,
|
|
20807
|
+
children: (node.children || []).map((child) =>
|
|
20808
|
+
rewriteImperativeHeadElements(child, ctx, nextNamespace, inNoscript || tag === 'noscript'),
|
|
20809
|
+
),
|
|
20810
|
+
};
|
|
20811
|
+
}
|
|
20812
|
+
|
|
20661
20813
|
/**
|
|
20662
20814
|
* Normalize a list of JSX child nodes into the shapes the emitters consume:
|
|
20663
|
-
* -
|
|
20815
|
+
* - Indentation-only JSXText / JSX comments (`{…}` empty containers) → dropped
|
|
20664
20816
|
* - JSXText with content → `Text` node wrapping a string Literal
|
|
20665
20817
|
* - `{expr}` container → `Text` hole, or `TSRXExpression` when the
|
|
20666
20818
|
* expression needs the rich dispatcher (portals, JSX ternaries,
|
|
@@ -20670,7 +20822,7 @@ function emitHeadServer(headNodes, ctx) {
|
|
|
20670
20822
|
* - JSXElement → `Element` (with `<Fragment ref>` expanded to a
|
|
20671
20823
|
* FragmentStart/…/FragmentEnd sequence, `<Activity>` lowered to an
|
|
20672
20824
|
* ActivityStatement, eligible `<title>/<meta>/<link>` hoisted as `HeadHoist`,
|
|
20673
|
-
* and
|
|
20825
|
+
* and parser-sensitive host trees lowered as descriptors)
|
|
20674
20826
|
* - Fragments (`<>…</>`, Tsx/Tsrx) → flattened (children inlined)
|
|
20675
20827
|
* - JSXStyleElement → dropped (its CSS is handled by the scoping pipeline)
|
|
20676
20828
|
* - Directive expressions (@if/@for/@try/@switch, `@{…}` child blocks) →
|
|
@@ -20681,13 +20833,19 @@ function emitHeadServer(headNodes, ctx) {
|
|
|
20681
20833
|
// `inSvg`: the children being normalized sit inside an SVG-namespace subtree.
|
|
20682
20834
|
// SVG has its own `<title>` (the accessibility tooltip element) — it must stay
|
|
20683
20835
|
// where it is, NOT hoist to document.head (React 19 makes the same exception).
|
|
20684
|
-
function normalizeChildren(
|
|
20836
|
+
function normalizeChildren(
|
|
20837
|
+
nodes,
|
|
20838
|
+
inSvg = false,
|
|
20839
|
+
ctx = null,
|
|
20840
|
+
inNoscript = false,
|
|
20841
|
+
allowImperative = true,
|
|
20842
|
+
) {
|
|
20685
20843
|
const out = [];
|
|
20686
20844
|
if (!nodes) return out;
|
|
20687
20845
|
for (const n of nodes) {
|
|
20688
20846
|
if (!n) continue;
|
|
20689
20847
|
if (n.type === 'JSXText') {
|
|
20690
|
-
if (/^\s*$/.test(n.value)) continue;
|
|
20848
|
+
if (n.value === '' || (/^\s*$/.test(n.value) && /[\n\r]/.test(n.value))) continue;
|
|
20691
20849
|
// The Text node and its literal map to the authored JSX text.
|
|
20692
20850
|
out.push(
|
|
20693
20851
|
inheritOriginLoc(
|
|
@@ -20788,14 +20946,29 @@ function normalizeChildren(nodes, inSvg = false, ctx = null, inNoscript = false)
|
|
|
20788
20946
|
out.push({ type: 'ActivityStatement', mode, children: n.children || [] });
|
|
20789
20947
|
continue;
|
|
20790
20948
|
}
|
|
20791
|
-
//
|
|
20792
|
-
//
|
|
20793
|
-
|
|
20794
|
-
|
|
20795
|
-
|
|
20796
|
-
|
|
20797
|
-
|
|
20949
|
+
// Keys need a reconciliation boundary. Parser-sensitive host trees need
|
|
20950
|
+
// imperative construction so HTML repair cannot change binding paths.
|
|
20951
|
+
// The shared descriptor path provides both without taxing ordinary templates.
|
|
20952
|
+
if (
|
|
20953
|
+
ctx &&
|
|
20954
|
+
allowImperative &&
|
|
20955
|
+
!isComponentTag(n) &&
|
|
20956
|
+
(hasJsxAttribute(n, 'key') || (!inSvg && requiresImperativeHostTree(n, ctx)))
|
|
20957
|
+
) {
|
|
20958
|
+
out.push(
|
|
20959
|
+
inheritOriginLoc(
|
|
20960
|
+
{
|
|
20961
|
+
type: 'Text',
|
|
20962
|
+
expression: jsxElementToCreateElement(
|
|
20963
|
+
rewriteImperativeHeadElements(n, ctx, inSvg ? 'svg' : 'html'),
|
|
20964
|
+
ctx,
|
|
20965
|
+
),
|
|
20966
|
+
imperativeRoot: n,
|
|
20967
|
+
},
|
|
20968
|
+
n,
|
|
20969
|
+
),
|
|
20798
20970
|
);
|
|
20971
|
+
continue;
|
|
20799
20972
|
}
|
|
20800
20973
|
// `<title>`/`<meta>`/eligible `<link>` → hoist to the document-head channel (NOT
|
|
20801
20974
|
// body DOM). Kept in `out` as a synthetic node so planJsx / ssrCompileBody
|
|
@@ -20843,6 +21016,18 @@ function normalizeChildren(nodes, inSvg = false, ctx = null, inNoscript = false)
|
|
|
20843
21016
|
} else if (n.type === 'Tsx' || n.type === 'Tsrx' || n.type === 'JSXFragment') {
|
|
20844
21017
|
out.push(...normalizeChildren(n.children || [], inSvg, ctx, inNoscript));
|
|
20845
21018
|
} else if (n.type === 'JSXStyleElement') {
|
|
21019
|
+
if (hasJsxAttribute(n, 'dangerouslySetInnerHTML') || hasJsxAttribute(n, 'children')) {
|
|
21020
|
+
out.push(
|
|
21021
|
+
...normalizeChildren(
|
|
21022
|
+
[b.jsx_element(n, n.openingElement.attributes || [], [])],
|
|
21023
|
+
inSvg,
|
|
21024
|
+
ctx,
|
|
21025
|
+
inNoscript,
|
|
21026
|
+
allowImperative,
|
|
21027
|
+
),
|
|
21028
|
+
);
|
|
21029
|
+
continue;
|
|
21030
|
+
}
|
|
20846
21031
|
// `<style href precedence>` is a Float style resource, not scoped CSS —
|
|
20847
21032
|
// partition it to the head channel like precedence links.
|
|
20848
21033
|
if (!inSvg && !inNoscript && headResourceKind(n) === 'style') {
|
|
@@ -20850,7 +21035,7 @@ function normalizeChildren(nodes, inSvg = false, ctx = null, inNoscript = false)
|
|
|
20850
21035
|
continue;
|
|
20851
21036
|
}
|
|
20852
21037
|
// Drop a `<style>` block at child position — its CSS gets registered
|
|
20853
|
-
// via the
|
|
21038
|
+
// via the style-scope pre-pass (applyStyleScopes, ./style-scopes.js);
|
|
20854
21039
|
// it contributes no DOM here.
|
|
20855
21040
|
continue;
|
|
20856
21041
|
} else if (isWrappedJsxDirective(n)) {
|
|
@@ -21852,9 +22037,9 @@ function planJsx(
|
|
|
21852
22037
|
for (let rootI = 0; rootI < jsxNodes.length; rootI++) {
|
|
21853
22038
|
const node = jsxNodes[rootI];
|
|
21854
22039
|
const nodeIsComp = node.type === 'Element' && isComponentTag(node);
|
|
21855
|
-
if (ctx.dev && node.type === 'Element' && !nodeIsComp) {
|
|
22040
|
+
if (ctx.dev && ((node.type === 'Element' && !nodeIsComp) || node.imperativeRoot)) {
|
|
21856
22041
|
collectClientHtmlNestingWarnings(
|
|
21857
|
-
node,
|
|
22042
|
+
node.imperativeRoot ?? node,
|
|
21858
22043
|
ctx,
|
|
21859
22044
|
parentNs === 'opaque' ? 'html' : parentNs,
|
|
21860
22045
|
nestingWarningIdentities,
|
|
@@ -22692,6 +22877,7 @@ function planJsx(
|
|
|
22692
22877
|
(fc.indexIndependent ? 8 : 0) |
|
|
22693
22878
|
(fc.ssrMarkerless ? 16 : 0) |
|
|
22694
22879
|
(fc.requiresScope ? 32 : 0) |
|
|
22880
|
+
(ctx.dev && fc.missingMapKey ? 64 : 0) |
|
|
22695
22881
|
(fc.keyedSelectionIndex >= 0 ? fc.keyedSelectionIndex << 6 : 0);
|
|
22696
22882
|
// Arg layout: forBlock(__s, slot, host, items, keyFn, body, flags?, deps?,
|
|
22697
22883
|
// emptyBody?, anchor?, ownEnd?, selectionBody?). The final argument is
|
|
@@ -23638,7 +23824,7 @@ function cleanupsPush(fnNode) {
|
|
|
23638
23824
|
}
|
|
23639
23825
|
|
|
23640
23826
|
/**
|
|
23641
|
-
* The `[spread, name, value]` rows a commit-phase source collector
|
|
23827
|
+
* The `[spread, name, value, merge?]` rows a commit-phase source collector
|
|
23642
23828
|
* (`setHostPropSources` / `setFormControlSources`) resolves in authored order.
|
|
23643
23829
|
* Shared by the mount and update emits, which differ only in how a source's
|
|
23644
23830
|
* value is read back — a bag local at mount, a bag field on update.
|
|
@@ -23648,20 +23834,26 @@ function cleanupsPush(fnNode) {
|
|
|
23648
23834
|
* split ssrDirectSource makes on the server); the collector itself serializes
|
|
23649
23835
|
* every source of the element and stays anchored there. Nothing else in a row
|
|
23650
23836
|
* reproduces the authored name — the value is the prop's expression — so
|
|
23651
|
-
* without this the whole group answered only for the element.
|
|
23837
|
+
* without this the whole group answered only for the element. A trailing `true`
|
|
23838
|
+
* marks a synthesized scope class that must merge with a spread's class.
|
|
23652
23839
|
*/
|
|
23653
23840
|
function commitSourceRows(sources, valueOf) {
|
|
23654
|
-
|
|
23655
|
-
|
|
23656
|
-
|
|
23657
|
-
|
|
23658
|
-
|
|
23659
|
-
|
|
23660
|
-
|
|
23661
|
-
|
|
23662
|
-
|
|
23663
|
-
|
|
23664
|
-
|
|
23841
|
+
const rows = [];
|
|
23842
|
+
for (let i = 0; i < sources.length; i++) {
|
|
23843
|
+
const source = sources[i];
|
|
23844
|
+
if (source.spread) {
|
|
23845
|
+
rows.push(b.array([b.literal(true), valueOf(source.binding, true)]));
|
|
23846
|
+
continue;
|
|
23847
|
+
}
|
|
23848
|
+
const cells = [
|
|
23849
|
+
b.literal(false),
|
|
23850
|
+
inheritOriginLoc(b.literal(source.name), source.nameOrigin ?? null),
|
|
23851
|
+
valueOf(source.binding, false),
|
|
23852
|
+
];
|
|
23853
|
+
if (source.merge) cells.push(b.literal(true));
|
|
23854
|
+
rows.push(b.array(cells));
|
|
23855
|
+
}
|
|
23856
|
+
return b.array(rows);
|
|
23665
23857
|
}
|
|
23666
23858
|
|
|
23667
23859
|
// Mount for a DEFERRED property-write binding: store the element ref + seed the
|
|
@@ -23710,7 +23902,15 @@ function emitBindingMount(bind, elVar, bag) {
|
|
|
23710
23902
|
// `suppressHydrationWarning`: stamp a JS flag (NOT a DOM attribute) the runtime reads to
|
|
23711
23903
|
// keep the server value + skip the warning on a hydration mismatch for this element.
|
|
23712
23904
|
if (bind.kind === 'suppress') {
|
|
23713
|
-
return st(
|
|
23905
|
+
return st(
|
|
23906
|
+
b.stmt(
|
|
23907
|
+
b.assignment(
|
|
23908
|
+
'=',
|
|
23909
|
+
b.member(el(), '__oct_suppress'),
|
|
23910
|
+
b.binary('===', bind.expr, b.literal(true)),
|
|
23911
|
+
),
|
|
23912
|
+
),
|
|
23913
|
+
);
|
|
23714
23914
|
}
|
|
23715
23915
|
if (bind.kind === 'dangerChild') {
|
|
23716
23916
|
return st(b.stmt(b.call('_$markDangerouslySetInnerHTMLChildren', el())));
|
|
@@ -24850,7 +25050,7 @@ function collectClientHtmlNestingWarnings(root, ctx, inheritedNamespace, identit
|
|
|
24850
25050
|
const visit = (node, namespace, ancestors) => {
|
|
24851
25051
|
if (node?.type !== 'Element' && node?.type !== 'JSXElement') return;
|
|
24852
25052
|
if (isComponentTag(node)) return;
|
|
24853
|
-
const tag = elementTagName(node);
|
|
25053
|
+
const tag = jsxTagName(node) || elementTagName(node);
|
|
24854
25054
|
if (tag === null) return;
|
|
24855
25055
|
const semanticTag = tag.toLowerCase();
|
|
24856
25056
|
const ownNamespace = nsForSelf(tag, namespace);
|
|
@@ -24884,7 +25084,13 @@ function collectClientHtmlNestingWarnings(root, ctx, inheritedNamespace, identit
|
|
|
24884
25084
|
? 'html'
|
|
24885
25085
|
: nsForChildren(tag, namespace);
|
|
24886
25086
|
const nextAncestors = [...ancestors, { tag: semanticTag, namespace: ownNamespace, location }];
|
|
24887
|
-
const children = normalizeChildren(
|
|
25087
|
+
const children = normalizeChildren(
|
|
25088
|
+
node.children || [],
|
|
25089
|
+
childNamespace === 'svg',
|
|
25090
|
+
ctx,
|
|
25091
|
+
false,
|
|
25092
|
+
false,
|
|
25093
|
+
);
|
|
24888
25094
|
for (const child of children) {
|
|
24889
25095
|
if (child.type === 'Text' && ownNamespace === 'html' && restrictedTextParent(semanticTag)) {
|
|
24890
25096
|
const text = staticTextLiteral(child.expression);
|
|
@@ -25138,6 +25344,7 @@ function emitElementHtml(
|
|
|
25138
25344
|
name: rawAttrName,
|
|
25139
25345
|
nameOrigin: attr.name,
|
|
25140
25346
|
binding,
|
|
25347
|
+
merge: isSynthesizedClassAttr(attr),
|
|
25141
25348
|
});
|
|
25142
25349
|
continue;
|
|
25143
25350
|
}
|
|
@@ -25156,12 +25363,22 @@ function emitElementHtml(
|
|
|
25156
25363
|
// `suppressHydrationWarning` (React shallow semantics): NEVER a DOM attribute (not
|
|
25157
25364
|
// serialized client or server) — stamp a JS flag the runtime's hydration-mismatch
|
|
25158
25365
|
// paths read, so a server/client divergence on this element keeps the server value
|
|
25159
|
-
// and skips the warning.
|
|
25366
|
+
// and skips the warning. Only a bare prop or a value equal to true opts in.
|
|
25160
25367
|
if (rawAttrName === 'suppressHydrationWarning') {
|
|
25161
25368
|
const v = attr.value;
|
|
25162
25369
|
const inner = v && v.type === 'JSXExpressionContainer' ? v.expression : v;
|
|
25163
25370
|
const isFalse = inner && inner.type === 'Literal' && inner.value === false;
|
|
25164
|
-
if (!isFalse)
|
|
25371
|
+
if (!isFalse)
|
|
25372
|
+
bindings.push({
|
|
25373
|
+
id: bindings.length,
|
|
25374
|
+
kind: 'suppress',
|
|
25375
|
+
path,
|
|
25376
|
+
origin: attr,
|
|
25377
|
+
expr:
|
|
25378
|
+
inner == null
|
|
25379
|
+
? b.literal(true, 'true', attr)
|
|
25380
|
+
: tsrxExprNode(inner, ctx, componentName, inlinedSubs),
|
|
25381
|
+
});
|
|
25165
25382
|
continue;
|
|
25166
25383
|
}
|
|
25167
25384
|
const attrName = normalizeJsxAttrName(rawAttrName, tag, hostNs);
|
|
@@ -25383,6 +25600,7 @@ function emitElementHtml(
|
|
|
25383
25600
|
// path; valid static attrs still bake with their real boolean semantics.
|
|
25384
25601
|
if (
|
|
25385
25602
|
isEventAttrName(attrName) ||
|
|
25603
|
+
MUST_USE_PROPERTY_PROPS.has(attrName.toLowerCase()) ||
|
|
25386
25604
|
isAfterSpread ||
|
|
25387
25605
|
classBeforeSpread ||
|
|
25388
25606
|
(ctx.dev && needsDevStaticAttrValidation(attrName, true, tag, hostNs))
|
|
@@ -25473,6 +25691,7 @@ function emitElementHtml(
|
|
|
25473
25691
|
if (
|
|
25474
25692
|
inner.type === 'Literal' &&
|
|
25475
25693
|
!isEventAttrName(attrName) &&
|
|
25694
|
+
!MUST_USE_PROPERTY_PROPS.has(attrName.toLowerCase()) &&
|
|
25476
25695
|
!isAfterSpread &&
|
|
25477
25696
|
!classBeforeSpread &&
|
|
25478
25697
|
!(ctx.dev && needsDevStaticAttrValidation(attrName, inner.value, tag, hostNs))
|
|
@@ -25485,7 +25704,7 @@ function emitElementHtml(
|
|
|
25485
25704
|
// Dynamic value — record a binding. (Also reached for literal values that
|
|
25486
25705
|
// come after a spread, since those need to win over the spread at runtime.)
|
|
25487
25706
|
const expr = tsrxExprNode(inner, ctx, componentName, inlinedSubs);
|
|
25488
|
-
if (isEventAttrName(attrName)) {
|
|
25707
|
+
if (isEventAttrName(attrName) && !(tag.includes('-') && !isDelegatedEventProp(attrName))) {
|
|
25489
25708
|
// React-shape: a trailing `Capture` selects the capture phase (fired
|
|
25490
25709
|
// root→target before bubble handlers), stamped under `$$capture:<type>`.
|
|
25491
25710
|
// The real events gotpointercapture / lostpointercapture literally end in
|
|
@@ -25501,7 +25720,14 @@ function emitElementHtml(
|
|
|
25501
25720
|
capture = true;
|
|
25502
25721
|
rest = rest.slice(0, -7);
|
|
25503
25722
|
}
|
|
25504
|
-
const eventName =
|
|
25723
|
+
const eventName =
|
|
25724
|
+
rest === 'DoubleClick'
|
|
25725
|
+
? 'dblclick'
|
|
25726
|
+
: rest === 'Focus'
|
|
25727
|
+
? 'focusin'
|
|
25728
|
+
: rest === 'Blur'
|
|
25729
|
+
? 'focusout'
|
|
25730
|
+
: rest.toLowerCase();
|
|
25505
25731
|
const slotKey = capture ? `$$capture:${eventName}` : `$$${eventName}`;
|
|
25506
25732
|
// `onClick` → the `'$$click'` slot key. Without this the attribute name
|
|
25507
25733
|
// is unreachable from the output: the key's map position resolves to
|
|
@@ -26182,7 +26408,10 @@ function emitElementHtml(
|
|
|
26182
26408
|
};
|
|
26183
26409
|
const ic = makeIfCall(asIf, ctx, inlinedSubs, childNs, cssHash);
|
|
26184
26410
|
ic.hostPath = path;
|
|
26411
|
+
ic.anchorPath = [...path, childIdx];
|
|
26185
26412
|
ifCalls.push(ic);
|
|
26413
|
+
appendTemplatePart(html, '<!>', 'anchor');
|
|
26414
|
+
childIdx++;
|
|
26186
26415
|
} else if (isKnownStringChildExpression(expr, ctx.knownStringChildLocals)) {
|
|
26187
26416
|
bindings.push({
|
|
26188
26417
|
id: bindings.length,
|
|
@@ -26277,7 +26506,7 @@ function makePortalCall(callNode, ctx, componentName, inlinedSubs, parentNs, css
|
|
|
26277
26506
|
bodyArg,
|
|
26278
26507
|
);
|
|
26279
26508
|
} else {
|
|
26280
|
-
bodyExpr = tsrxExprNode(bodyArg, ctx, componentName, inlinedSubs);
|
|
26509
|
+
bodyExpr = tsrxExprNode(rewriteJsxValues(bodyArg, ctx), ctx, componentName, inlinedSubs);
|
|
26281
26510
|
}
|
|
26282
26511
|
return {
|
|
26283
26512
|
id: ctx.nextHelperId++,
|
|
@@ -26707,6 +26936,21 @@ function tagBindingName(node) {
|
|
|
26707
26936
|
return name.type === 'Identifier' || name.type === 'JSXIdentifier' ? name.name : null;
|
|
26708
26937
|
}
|
|
26709
26938
|
|
|
26939
|
+
// A marked module binding can be shadowed by a parameter, block, or directive
|
|
26940
|
+
// item. Keep the name-only fast path for all other tags; a candidate needs the
|
|
26941
|
+
// same authored-scope proof used for Activity, shared by client and SSR emits.
|
|
26942
|
+
function isDescriptorChildrenTag(node, ctx) {
|
|
26943
|
+
const name = node.openingElement?.name || node.id;
|
|
26944
|
+
if (
|
|
26945
|
+
(name?.type !== 'Identifier' && name?.type !== 'JSXIdentifier') ||
|
|
26946
|
+
!ctx.descriptorChildrenBindings?.has(name.name)
|
|
26947
|
+
) {
|
|
26948
|
+
return false;
|
|
26949
|
+
}
|
|
26950
|
+
const binding = activityReferenceBinding(name, node, ctx);
|
|
26951
|
+
return binding?.scope === ctx.activityLexical?.rootScope;
|
|
26952
|
+
}
|
|
26953
|
+
|
|
26710
26954
|
// React-style render-prop detection: if a component's children are exactly one
|
|
26711
26955
|
// `{fn}` expression hole whose expression is a function (arrow or function
|
|
26712
26956
|
// expression) — `<Comp>{(data) => <jsx/>}</Comp>` — return that function node so
|
|
@@ -27059,7 +27303,7 @@ function makeCompCall(
|
|
|
27059
27303
|
const children = rewriteOpaqueTitles(sourceChildren, ctx, 'opaque');
|
|
27060
27304
|
const childrenParentNs =
|
|
27061
27305
|
!activityDescriptor && !isFragmentLongForm(node, ctx) ? 'opaque' : parentNs;
|
|
27062
|
-
if (compName !== null && ctx
|
|
27306
|
+
if (compName !== null && isDescriptorChildrenTag(node, ctx)) {
|
|
27063
27307
|
const kids = children
|
|
27064
27308
|
.map((child) => lowerInspectableJsxChild(child, ctx))
|
|
27065
27309
|
.filter((child) => child != null);
|
|
@@ -27707,16 +27951,20 @@ function makeForCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null)
|
|
|
27707
27951
|
const keyHelper = `_key$${ctx.nextHelperId++}`;
|
|
27708
27952
|
const keyOrigin = node.loc ? node : keyFn.loc ? keyFn : ctx._moduleOrigin;
|
|
27709
27953
|
const keyBody = keyFn.body;
|
|
27954
|
+
const keyCapturesParent = [...collectFreeIdentifiers(keyFn, new Set())].some((name) =>
|
|
27955
|
+
ctx.currentComponentLocals?.has(name),
|
|
27956
|
+
);
|
|
27710
27957
|
const inlineKey =
|
|
27711
|
-
|
|
27712
|
-
|
|
27713
|
-
|
|
27714
|
-
|
|
27715
|
-
|
|
27716
|
-
|
|
27717
|
-
|
|
27718
|
-
|
|
27719
|
-
|
|
27958
|
+
keyCapturesParent ||
|
|
27959
|
+
(node.nativeArrayMap !== undefined &&
|
|
27960
|
+
mapCallbackHasEventClosure(node.nativeArrayMap.callback) &&
|
|
27961
|
+
!isDestructured &&
|
|
27962
|
+
!node.index &&
|
|
27963
|
+
keyBody?.type === 'MemberExpression' &&
|
|
27964
|
+
keyBody.computed !== true &&
|
|
27965
|
+
keyBody.object?.type === 'Identifier' &&
|
|
27966
|
+
keyBody.object.name === itemName &&
|
|
27967
|
+
keyBody.property?.name === 'id');
|
|
27720
27968
|
if (!inlineKey) {
|
|
27721
27969
|
ctx.hoistedHelpers.push(inheritOriginLoc(b.const(keyHelper, keyFn), keyOrigin));
|
|
27722
27970
|
}
|
|
@@ -28129,6 +28377,7 @@ function makeForCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null)
|
|
|
28129
28377
|
);
|
|
28130
28378
|
|
|
28131
28379
|
const mapCall = node.nativeArrayMap || null;
|
|
28380
|
+
if (keyCapturesParent) autoMemoDeps = null;
|
|
28132
28381
|
return {
|
|
28133
28382
|
id: ctx.nextHelperId++,
|
|
28134
28383
|
loc: devLoc(ctx, node),
|
|
@@ -28148,6 +28397,7 @@ function makeForCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null)
|
|
|
28148
28397
|
b.id(mapCall.receiverName),
|
|
28149
28398
|
])
|
|
28150
28399
|
: node.right, // the fold replaces this with a `props.hN` member read
|
|
28400
|
+
missingMapKey: mapCall !== null && node.key == null,
|
|
28151
28401
|
mapMethodExpr: mapCall
|
|
28152
28402
|
? mapCall.methodName === null
|
|
28153
28403
|
? b.member(b.id(mapCall.receiverName), 'map')
|