octane 0.2.3 → 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/css.cjs +9 -0
- package/dist/cjs/dom-tables.cjs +6 -1
- package/dist/cjs/runtime.cjs +122 -27
- package/dist/cjs/runtime.server.cjs +258 -63
- package/dist/cjs/server/index.cjs +4 -0
- package/dist/cjs/version.cjs +1 -1
- package/dist/compiler/bundler.js +7 -0
- package/dist/compiler/compile.js +258 -528
- package/dist/compiler/hydrate-boundaries.js +165 -15
- package/dist/compiler/parser.node.js +34 -8
- 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/css.d.ts +7 -0
- package/dist/css.js +8 -0
- package/dist/dom-tables.js +6 -1
- package/dist/runtime.d.ts +19 -3
- package/dist/runtime.js +129 -28
- package/dist/runtime.server.d.ts +28 -1
- package/dist/runtime.server.js +263 -64
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +4 -0
- package/dist/version.js +1 -1
- package/package.json +3 -3
package/dist/compiler/compile.js
CHANGED
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
strongHash,
|
|
40
40
|
} from '@tsrx/core';
|
|
41
41
|
import { parseModule } from '#octane/compiler-parser';
|
|
42
|
+
import { createStyleScopePass } from './style-scopes.js';
|
|
42
43
|
import { decodeHTMLStrict } from 'entities';
|
|
43
44
|
import { print as esrapPrint } from 'esrap';
|
|
44
45
|
import esrapTsx from 'esrap/languages/tsx';
|
|
@@ -240,6 +241,10 @@ function inheritOriginLoc(root, origin) {
|
|
|
240
241
|
if (origin == null || origin.loc == null) return root;
|
|
241
242
|
const { start, end, loc } = origin;
|
|
242
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;
|
|
243
248
|
// Only real AST nodes are stamped — mapAst also visits typeless data
|
|
244
249
|
// objects (TemplateElement.value, Literal.regex, embedded config), which
|
|
245
250
|
// must stay untouched (and may be frozen).
|
|
@@ -317,6 +322,13 @@ function markSynthesizedAttr(attr) {
|
|
|
317
322
|
return attr;
|
|
318
323
|
}
|
|
319
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
|
+
|
|
320
332
|
// Curated exact origins (`inspect: true`). A generated token whose authored
|
|
321
333
|
// counterpart the compiler KNOWS — the `$$click` slot key of an `onClick`, the
|
|
322
334
|
// block helper an `@if`/`@for` lowers to — but which the module map alone
|
|
@@ -6714,9 +6726,21 @@ function mapCallbackCapturesLexicalReceiver(root) {
|
|
|
6714
6726
|
return walk(root);
|
|
6715
6727
|
}
|
|
6716
6728
|
|
|
6717
|
-
//
|
|
6718
|
-
//
|
|
6719
|
-
|
|
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.
|
|
6720
6744
|
function isStyleCall(node) {
|
|
6721
6745
|
return (
|
|
6722
6746
|
node &&
|
|
@@ -6728,26 +6752,14 @@ function isStyleCall(node) {
|
|
|
6728
6752
|
);
|
|
6729
6753
|
}
|
|
6730
6754
|
|
|
6731
|
-
//
|
|
6732
|
-
//
|
|
6733
|
-
//
|
|
6734
|
-
//
|
|
6735
|
-
//
|
|
6736
|
-
//
|
|
6737
|
-
function resolveStyleExpr(node,
|
|
6738
|
-
|
|
6739
|
-
if (!isStyleCall(node) || !cssHash) return node;
|
|
6740
|
-
const inner = node.arguments[0];
|
|
6741
|
-
if (inner.type === 'Literal' && typeof inner.value === 'string') {
|
|
6742
|
-
const combined = inner.value ? `${cssHash} ${inner.value}` : cssHash;
|
|
6743
|
-
// The resolved class string maps to the authored `{style (…)}` call.
|
|
6744
|
-
return b.literal(combined, JSON.stringify(combined), node);
|
|
6745
|
-
}
|
|
6746
|
-
// Dynamic: emit `(<hash> + ' ' + (expr))` so absent/null produces "<hash> ".
|
|
6747
|
-
return inheritOriginLoc(
|
|
6748
|
-
b.binary('+', b.literal(cssHash + ' ', JSON.stringify(cssHash + ' ')), inner),
|
|
6749
|
-
node,
|
|
6750
|
-
);
|
|
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;
|
|
6751
6763
|
}
|
|
6752
6764
|
|
|
6753
6765
|
/**
|
|
@@ -6773,6 +6785,11 @@ function isComponentFunction(node) {
|
|
|
6773
6785
|
// hooks slotted (via withSlot) and its returned JSX lowered to a descriptor, and
|
|
6774
6786
|
// whatever USES it (`<Foo/>` / `{expr}`) is what renders it. (Async/generator
|
|
6775
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.
|
|
6776
6793
|
function isReturnJsxFunction(node) {
|
|
6777
6794
|
if (!node) return false;
|
|
6778
6795
|
if (node.type !== 'FunctionDeclaration' && node.type !== 'FunctionExpression') return false;
|
|
@@ -6782,9 +6799,58 @@ function isReturnJsxFunction(node) {
|
|
|
6782
6799
|
if (node.id == null) return false;
|
|
6783
6800
|
if (node.async || node.generator) return false;
|
|
6784
6801
|
if (!node.body || node.body.type !== 'BlockStatement') return false;
|
|
6785
|
-
|
|
6786
|
-
|
|
6787
|
-
|
|
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;
|
|
6788
6854
|
}
|
|
6789
6855
|
|
|
6790
6856
|
/** True when `node` is one plain lowercase host element (never a component/fragment). */
|
|
@@ -9814,16 +9880,11 @@ function compileInternal(
|
|
|
9814
9880
|
// use, custom helpers, etc.) — merged into the single prelude import.
|
|
9815
9881
|
addUserImportSpecifiers(ctx, node);
|
|
9816
9882
|
} else {
|
|
9817
|
-
// Style
|
|
9818
|
-
//
|
|
9819
|
-
//
|
|
9820
|
-
|
|
9821
|
-
|
|
9822
|
-
// in an ExportNamedDeclaration).
|
|
9823
|
-
if (node.type === 'ExportNamedDeclaration' && node.declaration) {
|
|
9824
|
-
const declaration = applyStyleMap(node.declaration, ctx);
|
|
9825
|
-
if (declaration !== node.declaration) node = { ...node, declaration };
|
|
9826
|
-
}
|
|
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;
|
|
9827
9888
|
// HOOKS EVERYWHERE: a plain function (a custom hook, a helper) can hold
|
|
9828
9889
|
// octane hooks too — slot them the same way components do, so their base
|
|
9829
9890
|
// hooks get a per-call-site symbol (and custom-hook calls inside get one to
|
|
@@ -10392,11 +10453,18 @@ function compileServer(
|
|
|
10392
10453
|
let newBody = null;
|
|
10393
10454
|
const statements = ast.body;
|
|
10394
10455
|
for (let i = 0; i < statements.length; i++) {
|
|
10395
|
-
|
|
10396
|
-
|
|
10397
|
-
|
|
10398
|
-
|
|
10399
|
-
|
|
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;
|
|
10400
10468
|
if (newBody === null && node !== statements[i]) newBody = statements.slice(0, i);
|
|
10401
10469
|
if (newBody !== null) newBody.push(node);
|
|
10402
10470
|
}
|
|
@@ -10556,11 +10624,11 @@ function compileServerComponent(node, ctx) {
|
|
|
10556
10624
|
const isExported = !!(node.export || node.default);
|
|
10557
10625
|
const isDefault = !!node.default;
|
|
10558
10626
|
|
|
10559
|
-
// Scoped <style>:
|
|
10627
|
+
// Scoped <style>: applyStyleScopes stamps scope classes + registers cssInjections.
|
|
10560
10628
|
// Capture this component's entries to emit injectStyle INSIDE the body (so the
|
|
10561
10629
|
// active server render collects CSS only for components it actually renders).
|
|
10562
10630
|
const beforeCss = ctx.cssInjections.length;
|
|
10563
|
-
const scoping =
|
|
10631
|
+
const scoping = applyStyleScopes(node, ctx);
|
|
10564
10632
|
node = scoping.node;
|
|
10565
10633
|
const cssHash = scoping.cssHash;
|
|
10566
10634
|
const cssEntries = [...ctx.moduleCssInjections, ...ctx.cssInjections.slice(beforeCss)].sort(
|
|
@@ -10588,6 +10656,12 @@ function compileServerComponent(node, ctx) {
|
|
|
10588
10656
|
cssEntries,
|
|
10589
10657
|
'opaque',
|
|
10590
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,
|
|
10591
10665
|
);
|
|
10592
10666
|
} finally {
|
|
10593
10667
|
ctx.currentComponentLocals = prevLocals;
|
|
@@ -10672,6 +10746,7 @@ function ssrCompileBody(
|
|
|
10672
10746
|
componentNs = null,
|
|
10673
10747
|
returnedFragmentTemplate = false,
|
|
10674
10748
|
returnedFragmentRoot = false,
|
|
10749
|
+
themeTouches = [],
|
|
10675
10750
|
unboxedHtml = false,
|
|
10676
10751
|
) {
|
|
10677
10752
|
const prevMapTemps = ctx.currentMapTemps;
|
|
@@ -10688,6 +10763,7 @@ function ssrCompileBody(
|
|
|
10688
10763
|
componentNs,
|
|
10689
10764
|
returnedFragmentTemplate,
|
|
10690
10765
|
returnedFragmentRoot,
|
|
10766
|
+
themeTouches,
|
|
10691
10767
|
unboxedHtml,
|
|
10692
10768
|
);
|
|
10693
10769
|
} finally {
|
|
@@ -10706,6 +10782,9 @@ function ssrCompileBodyWithMapTemps(
|
|
|
10706
10782
|
componentNs,
|
|
10707
10783
|
returnedFragmentTemplate,
|
|
10708
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,
|
|
10709
10788
|
unboxedHtml,
|
|
10710
10789
|
) {
|
|
10711
10790
|
const returnedOutput = node.body?.type === 'JSXCodeBlock' && hasOwnValueReturn(node);
|
|
@@ -10860,6 +10939,7 @@ function ssrCompileBodyWithMapTemps(
|
|
|
10860
10939
|
ctx._valueDirectiveLowering = prevValueDirectiveLowering;
|
|
10861
10940
|
|
|
10862
10941
|
const body = [];
|
|
10942
|
+
body.push(...serverThemeTouchStatements(themeTouches ?? [], ctx, node));
|
|
10863
10943
|
if (cssEntries && cssEntries.length) {
|
|
10864
10944
|
ctx.runtimeNeeded.add('injectStyle');
|
|
10865
10945
|
for (const entry of cssEntries) {
|
|
@@ -11084,6 +11164,23 @@ function ssrCall(helper, args, origin) {
|
|
|
11084
11164
|
);
|
|
11085
11165
|
}
|
|
11086
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
|
+
|
|
11087
11184
|
function ssrThunk(expression, origin) {
|
|
11088
11185
|
return inheritOriginLoc(b.arrow([], expression), origin);
|
|
11089
11186
|
}
|
|
@@ -11097,17 +11194,17 @@ function ssrSourcePair(present, value, origin) {
|
|
|
11097
11194
|
}
|
|
11098
11195
|
|
|
11099
11196
|
/**
|
|
11100
|
-
* One `[spread, name, value]` source row for a grouped attribute helper.
|
|
11101
|
-
* name literal is the only token in the group that names THIS attribute, so
|
|
11102
|
-
* is what the authored name is claimed against (see registerAttrLoweringOrigin);
|
|
11103
|
-
* 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.
|
|
11104
11202
|
*/
|
|
11105
|
-
function ssrDirectSource(ctx, name, value, origin) {
|
|
11203
|
+
function ssrDirectSource(ctx, name, value, origin, merge) {
|
|
11106
11204
|
registerAttrLoweringOrigin(ctx, origin?.name, null, name);
|
|
11107
|
-
|
|
11108
|
-
|
|
11109
|
-
|
|
11110
|
-
);
|
|
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);
|
|
11111
11208
|
}
|
|
11112
11209
|
|
|
11113
11210
|
/**
|
|
@@ -11866,7 +11963,15 @@ function ssrEmitElement(node, ctx, name, inlinedSubs, parentNs, cssHash, compone
|
|
|
11866
11963
|
);
|
|
11867
11964
|
attrExpr = tsrxExprNode(attrInner, ctx, name, inlinedSubs);
|
|
11868
11965
|
}
|
|
11869
|
-
attrSources.push(
|
|
11966
|
+
attrSources.push(
|
|
11967
|
+
ssrDirectSource(
|
|
11968
|
+
ctx,
|
|
11969
|
+
rawAttrName,
|
|
11970
|
+
bindAttributeEvaluation(attrExpr),
|
|
11971
|
+
attr,
|
|
11972
|
+
isSynthesizedClassAttr(attr),
|
|
11973
|
+
),
|
|
11974
|
+
);
|
|
11870
11975
|
continue;
|
|
11871
11976
|
}
|
|
11872
11977
|
|
|
@@ -12453,8 +12558,7 @@ function ssrEmitComponent(node, ctx, name, inlinedSubs, parentNs, cssHash, compo
|
|
|
12453
12558
|
const children = rewriteOpaqueTitles(sourceChildren, ctx, 'opaque');
|
|
12454
12559
|
const opaqueChildren = !activityDescriptor && !isFragmentLongForm(node, ctx);
|
|
12455
12560
|
const descriptorChildren =
|
|
12456
|
-
(
|
|
12457
|
-
ctx.descriptorChildrenBindings?.has(tagBindingName(node))) ||
|
|
12561
|
+
isDescriptorChildrenTag(node, ctx) ||
|
|
12458
12562
|
ctx._tsxValuePos ||
|
|
12459
12563
|
(returnedFragmentTemplate && !requiresTemplateNormalization(node, parentNs, true, ctx));
|
|
12460
12564
|
if (descriptorChildren) {
|
|
@@ -12674,6 +12778,8 @@ function ssrCompileSub(
|
|
|
12674
12778
|
componentNs,
|
|
12675
12779
|
returnedFragmentTemplate,
|
|
12676
12780
|
returnedFragmentRoot,
|
|
12781
|
+
// Nested helpers touch no themes; the component body already did.
|
|
12782
|
+
[],
|
|
12677
12783
|
unboxedHtml,
|
|
12678
12784
|
);
|
|
12679
12785
|
} finally {
|
|
@@ -13304,147 +13410,6 @@ function hasModuleStyleMaps(body) {
|
|
|
13304
13410
|
});
|
|
13305
13411
|
}
|
|
13306
13412
|
|
|
13307
|
-
// Copy-on-write: returns the (possibly rebuilt) statement; the input is never
|
|
13308
|
-
// modified. The core analyze/prepare pipeline mutates the sheet it is given,
|
|
13309
|
-
// so it runs over a clone of the (bounded) StyleSheet subtree.
|
|
13310
|
-
function applyStyleMap(stmt, ctx) {
|
|
13311
|
-
if (stmt.type !== 'VariableDeclaration') return stmt;
|
|
13312
|
-
let newDeclarations = null;
|
|
13313
|
-
const declarations = stmt.declarations;
|
|
13314
|
-
for (let i = 0; i < declarations.length; i++) {
|
|
13315
|
-
const decl = declarations[i];
|
|
13316
|
-
let next = decl;
|
|
13317
|
-
const styleNode = decl.init && decl.init.type === 'JSXStyleElement' ? decl.init : null;
|
|
13318
|
-
const parsedSheet = styleNode
|
|
13319
|
-
? (styleNode.children || []).find((c) => c && c.type === 'StyleSheet')
|
|
13320
|
-
: null;
|
|
13321
|
-
const hash = parsedSheet
|
|
13322
|
-
? styleNode.metadata?.styleScopeHash || parsedSheet.hash || null
|
|
13323
|
-
: null;
|
|
13324
|
-
if (parsedSheet && hash) {
|
|
13325
|
-
const sheet = cloneAstNode(parsedSheet);
|
|
13326
|
-
// `analyzeCss` marks `:global(...)` selectors (is_global / is_global_block
|
|
13327
|
-
// metadata) so the renderer leaves them UNSCOPED. Without this pass
|
|
13328
|
-
// `:global(a)` would be scoped to `.<hash>a`. Mirrors tsrx-ripple, which
|
|
13329
|
-
// runs analyzeCss(stylesheet) before prepareStylesheetForRender.
|
|
13330
|
-
analyzeCss(sheet);
|
|
13331
|
-
prepareStylesheetForRender(sheet, true);
|
|
13332
|
-
const css = renderStylesheets([sheet]);
|
|
13333
|
-
ctx.cssInjections.push({
|
|
13334
|
-
hash,
|
|
13335
|
-
css,
|
|
13336
|
-
order: styleNode.start ?? stmt.start ?? 0,
|
|
13337
|
-
// The authored `<style>` element. `injectStyle` is emitted at module
|
|
13338
|
-
// scope, so without this the whole block maps to the module origin
|
|
13339
|
-
// and neither the tag nor its CSS is reachable from the output.
|
|
13340
|
-
origin: styleNode,
|
|
13341
|
-
});
|
|
13342
|
-
ctx.runtimeNeeded.add('injectStyle');
|
|
13343
|
-
// Replace the JSXStyleElement init with the class-map ObjectExpression
|
|
13344
|
-
// (built loc-less by the core helper — it maps to the authored <style>).
|
|
13345
|
-
next = {
|
|
13346
|
-
...decl,
|
|
13347
|
-
init: inheritOriginLoc(createStyleClassMapFromStylesheet(sheet), styleNode),
|
|
13348
|
-
};
|
|
13349
|
-
}
|
|
13350
|
-
if (newDeclarations === null && next !== decl) newDeclarations = declarations.slice(0, i);
|
|
13351
|
-
if (newDeclarations !== null) newDeclarations.push(next);
|
|
13352
|
-
}
|
|
13353
|
-
return newDeclarations === null ? stmt : { ...stmt, declarations: newDeclarations };
|
|
13354
|
-
}
|
|
13355
|
-
|
|
13356
|
-
// Wrap every DYNAMIC `class` / `className` expression in a `normalizeClass(...)` call
|
|
13357
|
-
// BEFORE the scoped-CSS hash is appended. `@tsrx/core`'s annotate_with_hash bakes the
|
|
13358
|
-
// hash onto a dynamic class via a template literal — `` `${expr} <hash>` `` — which would
|
|
13359
|
-
// stringify an array/object clsx value the wrong way (`['a','b']` → "a,b", `{}` →
|
|
13360
|
-
// "[object Object]"). Normalizing first makes the interpolated slot a plain string, so
|
|
13361
|
-
// the hash concat stays correct AND clsx composition works in scoped components. (It also
|
|
13362
|
-
// turns a bare `class={undefined}` in a scoped component from "undefined <hash>" into just
|
|
13363
|
-
// "<hash>".) Unscoped components need no wrap — the runtime `setClassName` / `ssrAttr`
|
|
13364
|
-
// normalize the raw value directly. String literals are left alone so they keep folding
|
|
13365
|
-
// into the static template. Stops at nested component function boundaries (their class
|
|
13366
|
-
// exprs belong to a different scope), mirroring annotate_with_hash's own traversal.
|
|
13367
|
-
// Copy-on-write: rebuilt spines only where a wrap lands; untouched subtrees
|
|
13368
|
-
// are returned by reference and the input node is never written to. Callers
|
|
13369
|
-
// must use the return value.
|
|
13370
|
-
function wrapScopedClassExprs(node, ctx) {
|
|
13371
|
-
if (!node || typeof node !== 'object') return node;
|
|
13372
|
-
if (Array.isArray(node)) {
|
|
13373
|
-
let out = null;
|
|
13374
|
-
for (let i = 0; i < node.length; i++) {
|
|
13375
|
-
const mapped = wrapScopedClassExprs(node[i], ctx);
|
|
13376
|
-
if (out === null && mapped !== node[i]) out = node.slice(0, i);
|
|
13377
|
-
if (out !== null) out.push(mapped);
|
|
13378
|
-
}
|
|
13379
|
-
return out ?? node;
|
|
13380
|
-
}
|
|
13381
|
-
if (
|
|
13382
|
-
(node.type === 'FunctionDeclaration' ||
|
|
13383
|
-
node.type === 'FunctionExpression' ||
|
|
13384
|
-
node.type === 'ArrowFunctionExpression') &&
|
|
13385
|
-
node.metadata?.tsrx_dynamic_wrapper !== true
|
|
13386
|
-
) {
|
|
13387
|
-
return node;
|
|
13388
|
-
}
|
|
13389
|
-
let working = node;
|
|
13390
|
-
if (node.type === 'JSXElement') {
|
|
13391
|
-
const attrs = node.openingElement?.attributes;
|
|
13392
|
-
if (Array.isArray(attrs)) {
|
|
13393
|
-
let newAttrs = null;
|
|
13394
|
-
for (let i = 0; i < attrs.length; i++) {
|
|
13395
|
-
const attr = attrs[i];
|
|
13396
|
-
let next = attr;
|
|
13397
|
-
if (
|
|
13398
|
-
attr?.type === 'JSXAttribute' &&
|
|
13399
|
-
attr.name?.type === 'JSXIdentifier' &&
|
|
13400
|
-
(attr.name.name === 'class' || attr.name.name === 'className') &&
|
|
13401
|
-
attr.value?.type === 'JSXExpressionContainer'
|
|
13402
|
-
) {
|
|
13403
|
-
const expr = attr.value.expression;
|
|
13404
|
-
// Skip string literals (fold statically) and `{style (…)}` calls
|
|
13405
|
-
// (resolveStyleExpr owns those). Everything else is a runtime value.
|
|
13406
|
-
if (
|
|
13407
|
-
expr &&
|
|
13408
|
-
!(expr.type === 'Literal' && typeof expr.value === 'string') &&
|
|
13409
|
-
!isStyleCall(expr)
|
|
13410
|
-
) {
|
|
13411
|
-
next = {
|
|
13412
|
-
...attr,
|
|
13413
|
-
value: {
|
|
13414
|
-
...attr.value,
|
|
13415
|
-
// The wrapper maps to the class expression it normalizes.
|
|
13416
|
-
expression: inheritOriginLoc(b.call('_$normalizeClass', expr), expr),
|
|
13417
|
-
},
|
|
13418
|
-
};
|
|
13419
|
-
ctx.runtimeNeeded.add('normalizeClass');
|
|
13420
|
-
}
|
|
13421
|
-
}
|
|
13422
|
-
if (newAttrs === null && next !== attr) newAttrs = attrs.slice(0, i);
|
|
13423
|
-
if (newAttrs !== null) newAttrs.push(next);
|
|
13424
|
-
}
|
|
13425
|
-
if (newAttrs !== null) {
|
|
13426
|
-
working = { ...node, openingElement: { ...node.openingElement, attributes: newAttrs } };
|
|
13427
|
-
if ('attributes' in node) working.attributes = newAttrs;
|
|
13428
|
-
}
|
|
13429
|
-
}
|
|
13430
|
-
}
|
|
13431
|
-
// Generic descent AFTER the element-level wrap (mirrors the historical pass
|
|
13432
|
-
// order): nested JSX inside attribute values and children is wrapped too.
|
|
13433
|
-
let out = working === node ? null : working;
|
|
13434
|
-
for (const key of Object.keys(working)) {
|
|
13435
|
-
if (key === 'loc' || key === 'start' || key === 'end' || key === 'parent') continue;
|
|
13436
|
-
if (key === 'metadata' || key === 'css') continue;
|
|
13437
|
-
const v = working[key];
|
|
13438
|
-
if (!v || typeof v !== 'object') continue;
|
|
13439
|
-
const mapped = wrapScopedClassExprs(v, ctx);
|
|
13440
|
-
if (mapped !== v) {
|
|
13441
|
-
if (out === null) out = { ...node };
|
|
13442
|
-
out[key] = mapped;
|
|
13443
|
-
}
|
|
13444
|
-
}
|
|
13445
|
-
return out ?? working;
|
|
13446
|
-
}
|
|
13447
|
-
|
|
13448
13413
|
// Local copy-on-write mirror of @tsrx/core's annotate_with_hash (scoping.js):
|
|
13449
13414
|
// stamps the scoped-CSS hash class onto native (and dynamic-tag) JSX elements
|
|
13450
13415
|
// and drops JSXStyleElement nodes from the rendered tree. The core helper
|
|
@@ -13459,301 +13424,6 @@ function isCompositeJsxTag(node) {
|
|
|
13459
13424
|
return name.type === 'JSXMemberExpression';
|
|
13460
13425
|
}
|
|
13461
13426
|
|
|
13462
|
-
function addHashClassToElement(element, hash, classAttrName) {
|
|
13463
|
-
const openingElement = element.openingElement;
|
|
13464
|
-
const attrs = openingElement?.attributes || [];
|
|
13465
|
-
const index = attrs.findIndex(
|
|
13466
|
-
(attr) =>
|
|
13467
|
-
attr?.type === 'JSXAttribute' &&
|
|
13468
|
-
attr.name?.type === 'JSXIdentifier' &&
|
|
13469
|
-
(attr.name.name === 'class' || attr.name.name === 'className'),
|
|
13470
|
-
);
|
|
13471
|
-
let newAttrs;
|
|
13472
|
-
if (index === -1) {
|
|
13473
|
-
// A synthesized class attribute maps to the element's opening tag — the
|
|
13474
|
-
// loc keeps generated positions inside the element for source maps, but
|
|
13475
|
-
// there is no authored `class` here, so it is marked NOT AUTHORED. Left
|
|
13476
|
-
// unmarked, inspection reports the whole opening tag as the origin of the
|
|
13477
|
-
// scoped class, and a hover on any attribute in that tag (`onClick`, …)
|
|
13478
|
-
// resolves to the class the compiler added.
|
|
13479
|
-
newAttrs = [
|
|
13480
|
-
...attrs,
|
|
13481
|
-
markSynthesizedAttr(
|
|
13482
|
-
inheritOriginLoc(
|
|
13483
|
-
b.jsx_attribute(b.jsx_id(classAttrName), b.literal(hash, JSON.stringify(hash))),
|
|
13484
|
-
openingElement,
|
|
13485
|
-
),
|
|
13486
|
-
),
|
|
13487
|
-
];
|
|
13488
|
-
} else {
|
|
13489
|
-
const existing = attrs[index];
|
|
13490
|
-
const value = existing.value;
|
|
13491
|
-
let newAttr;
|
|
13492
|
-
if (!value) {
|
|
13493
|
-
// The NAME is authored (`class`), the value is not.
|
|
13494
|
-
newAttr = {
|
|
13495
|
-
...existing,
|
|
13496
|
-
value: markSynthesized(inheritOriginLoc(b.literal(hash, JSON.stringify(hash)), existing)),
|
|
13497
|
-
};
|
|
13498
|
-
} else if (value.type === 'Literal' && typeof value.value === 'string') {
|
|
13499
|
-
const merged = `${value.value} ${hash}`;
|
|
13500
|
-
newAttr = { ...existing, value: { ...value, value: merged, raw: JSON.stringify(merged) } };
|
|
13501
|
-
} else {
|
|
13502
|
-
const expression = value.type === 'JSXExpressionContainer' ? value.expression : value;
|
|
13503
|
-
// The template-literal bake maps to the authored class value it wraps.
|
|
13504
|
-
newAttr = {
|
|
13505
|
-
...existing,
|
|
13506
|
-
value: inheritOriginLoc(
|
|
13507
|
-
b.jsx_expression_container(
|
|
13508
|
-
b.template([b.quasi('', false), b.quasi(` ${hash}`, true)], [expression]),
|
|
13509
|
-
),
|
|
13510
|
-
value,
|
|
13511
|
-
),
|
|
13512
|
-
};
|
|
13513
|
-
}
|
|
13514
|
-
newAttrs = attrs.slice();
|
|
13515
|
-
newAttrs[index] = newAttr;
|
|
13516
|
-
}
|
|
13517
|
-
// The core helper mirrors the attribute list onto `element.attributes` in
|
|
13518
|
-
// every branch; keep that alias in sync on the rebuilt copy.
|
|
13519
|
-
return {
|
|
13520
|
-
...element,
|
|
13521
|
-
openingElement: { ...openingElement, attributes: newAttrs },
|
|
13522
|
-
attributes: newAttrs,
|
|
13523
|
-
};
|
|
13524
|
-
}
|
|
13525
|
-
|
|
13526
|
-
function annotateRootWithHash(node, hash, classAttrName) {
|
|
13527
|
-
if (!node || typeof node !== 'object') return node;
|
|
13528
|
-
if (
|
|
13529
|
-
(node.type === 'FunctionDeclaration' ||
|
|
13530
|
-
node.type === 'FunctionExpression' ||
|
|
13531
|
-
node.type === 'ArrowFunctionExpression') &&
|
|
13532
|
-
node.metadata?.tsrx_dynamic_wrapper !== true
|
|
13533
|
-
) {
|
|
13534
|
-
return node;
|
|
13535
|
-
}
|
|
13536
|
-
if (node.type === 'JSXElement') {
|
|
13537
|
-
let working = node;
|
|
13538
|
-
if (!isCompositeJsxTag(node) || node.metadata?.dynamicElement) {
|
|
13539
|
-
working = addHashClassToElement(node, hash, classAttrName);
|
|
13540
|
-
}
|
|
13541
|
-
const children = working.children;
|
|
13542
|
-
if (Array.isArray(children)) {
|
|
13543
|
-
let newChildren = null;
|
|
13544
|
-
for (let i = 0; i < children.length; i++) {
|
|
13545
|
-
const mapped = annotateRootWithHash(children[i], hash, classAttrName);
|
|
13546
|
-
// Dropped style elements are filtered out (core: .filter(Boolean)).
|
|
13547
|
-
if (newChildren === null && (mapped !== children[i] || !mapped)) {
|
|
13548
|
-
newChildren = children.slice(0, i);
|
|
13549
|
-
}
|
|
13550
|
-
if (newChildren !== null && mapped) newChildren.push(mapped);
|
|
13551
|
-
}
|
|
13552
|
-
if (newChildren !== null) {
|
|
13553
|
-
if (working === node) working = { ...node, children: newChildren };
|
|
13554
|
-
else working.children = newChildren;
|
|
13555
|
-
}
|
|
13556
|
-
}
|
|
13557
|
-
return working;
|
|
13558
|
-
}
|
|
13559
|
-
// Scoped-CSS styles leave the render tree here; Float style resources stay —
|
|
13560
|
-
// the HeadHoist partition (normalizeChildren) owns removing them from body DOM.
|
|
13561
|
-
if (node.type === 'JSXStyleElement') return headResourceKind(node) === 'style' ? node : null;
|
|
13562
|
-
let out = null;
|
|
13563
|
-
for (const key of Object.keys(node)) {
|
|
13564
|
-
if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata' || key === 'css') {
|
|
13565
|
-
continue;
|
|
13566
|
-
}
|
|
13567
|
-
const value = node[key];
|
|
13568
|
-
if (!value || typeof value !== 'object') continue;
|
|
13569
|
-
let mapped;
|
|
13570
|
-
if (Array.isArray(value)) {
|
|
13571
|
-
// Core maps generic arrays WITHOUT filtering — a dropped style element
|
|
13572
|
-
// leaves a null slot the downstream child normalization skips.
|
|
13573
|
-
let newArray = null;
|
|
13574
|
-
for (let i = 0; i < value.length; i++) {
|
|
13575
|
-
const item = annotateRootWithHash(value[i], hash, classAttrName);
|
|
13576
|
-
if (newArray === null && item !== value[i]) newArray = value.slice(0, i);
|
|
13577
|
-
if (newArray !== null) newArray.push(item);
|
|
13578
|
-
}
|
|
13579
|
-
mapped = newArray ?? value;
|
|
13580
|
-
} else {
|
|
13581
|
-
mapped = annotateRootWithHash(value, hash, classAttrName);
|
|
13582
|
-
}
|
|
13583
|
-
if (mapped !== value) {
|
|
13584
|
-
if (out === null) out = { ...node };
|
|
13585
|
-
out[key] = mapped;
|
|
13586
|
-
}
|
|
13587
|
-
}
|
|
13588
|
-
return out ?? node;
|
|
13589
|
-
}
|
|
13590
|
-
|
|
13591
|
-
/**
|
|
13592
|
-
* Find the JSX render roots owned by a component. `@{}` components expose one
|
|
13593
|
-
* `JSXCodeBlock.render`; React-style functions can return JSX from any block in
|
|
13594
|
-
* their own body. Nested functions are separate component/value boundaries and
|
|
13595
|
-
* must not donate styles to their enclosing component.
|
|
13596
|
-
*/
|
|
13597
|
-
function componentStyleRoots(componentNode) {
|
|
13598
|
-
const body = componentNode.body;
|
|
13599
|
-
if (!body) return [];
|
|
13600
|
-
if (body.type === 'JSXCodeBlock') {
|
|
13601
|
-
return body.render ? [{ kind: 'render', node: body.render }] : [];
|
|
13602
|
-
}
|
|
13603
|
-
if (body.type !== 'BlockStatement') return [];
|
|
13604
|
-
|
|
13605
|
-
const roots = [];
|
|
13606
|
-
function visit(node) {
|
|
13607
|
-
if (!node || typeof node !== 'object') return;
|
|
13608
|
-
if (Array.isArray(node)) {
|
|
13609
|
-
for (const child of node) visit(child);
|
|
13610
|
-
return;
|
|
13611
|
-
}
|
|
13612
|
-
if (
|
|
13613
|
-
node.type === 'FunctionDeclaration' ||
|
|
13614
|
-
node.type === 'FunctionExpression' ||
|
|
13615
|
-
node.type === 'ArrowFunctionExpression'
|
|
13616
|
-
) {
|
|
13617
|
-
return;
|
|
13618
|
-
}
|
|
13619
|
-
if (node.type === 'ReturnStatement') {
|
|
13620
|
-
if (node.argument && isJsxNode(node.argument)) {
|
|
13621
|
-
roots.push({ kind: 'return', statement: node, node: node.argument });
|
|
13622
|
-
}
|
|
13623
|
-
return;
|
|
13624
|
-
}
|
|
13625
|
-
for (const key of Object.keys(node)) {
|
|
13626
|
-
if (
|
|
13627
|
-
key === 'loc' ||
|
|
13628
|
-
key === 'start' ||
|
|
13629
|
-
key === 'end' ||
|
|
13630
|
-
key === 'parent' ||
|
|
13631
|
-
key === 'metadata' ||
|
|
13632
|
-
key === 'css'
|
|
13633
|
-
)
|
|
13634
|
-
continue;
|
|
13635
|
-
visit(node[key]);
|
|
13636
|
-
}
|
|
13637
|
-
}
|
|
13638
|
-
visit(body);
|
|
13639
|
-
return roots;
|
|
13640
|
-
}
|
|
13641
|
-
|
|
13642
|
-
/**
|
|
13643
|
-
* Walk a component's owned render roots for `JSXStyleElement` nodes. For each
|
|
13644
|
-
* one found:
|
|
13645
|
-
* - Pull the pre-parsed `StyleSheet` AST out of its children.
|
|
13646
|
-
* - Run `prepareStylesheetForRender` (rewrites `.foo` → `.foo.<hash>`) over a
|
|
13647
|
-
* clone of the sheet — the core pipeline mutates what it is given.
|
|
13648
|
-
* - Collect into a list rendered via `renderStylesheets` to a CSS string.
|
|
13649
|
-
* - Register `{hash, css}` on `ctx.cssInjections` so a module-level
|
|
13650
|
-
* `injectStyle(hash, css)` is emitted in the prelude.
|
|
13651
|
-
* - Rebuild the render roots via `annotateRootWithHash` (COW) to stamp the
|
|
13652
|
-
* hash class on every native JSX element AND remove the JSXStyleElement
|
|
13653
|
-
* nodes from the rendered tree (they don't contribute DOM in the new model).
|
|
13654
|
-
*
|
|
13655
|
-
* Returns `{ cssHash, node }` — the hash (or `null` when no `<style>` blocks
|
|
13656
|
-
* are present) and the possibly-rebuilt component node the caller must use.
|
|
13657
|
-
*
|
|
13658
|
-
* The first `JSXStyleElement` contributes the canonical hash for the whole
|
|
13659
|
-
* component. @tsrx/core hashes individual style tags by source position, so
|
|
13660
|
-
* multiple blocks are explicitly rebased onto that canonical component hash
|
|
13661
|
-
* before selector rendering. Every rendered root receives the same hash class.
|
|
13662
|
-
*/
|
|
13663
|
-
function applyCssScoping(componentNode, ctx) {
|
|
13664
|
-
const roots = componentStyleRoots(componentNode);
|
|
13665
|
-
if (roots.length === 0) return { cssHash: null, node: componentNode };
|
|
13666
|
-
let cssHash = null;
|
|
13667
|
-
const styles = [];
|
|
13668
|
-
function collect(node) {
|
|
13669
|
-
if (!node || typeof node !== 'object') return;
|
|
13670
|
-
if (Array.isArray(node)) {
|
|
13671
|
-
for (const i of node) collect(i);
|
|
13672
|
-
return;
|
|
13673
|
-
}
|
|
13674
|
-
if (
|
|
13675
|
-
node.type === 'FunctionDeclaration' ||
|
|
13676
|
-
node.type === 'FunctionExpression' ||
|
|
13677
|
-
node.type === 'ArrowFunctionExpression'
|
|
13678
|
-
) {
|
|
13679
|
-
return;
|
|
13680
|
-
}
|
|
13681
|
-
if (node.type === 'JSXStyleElement') {
|
|
13682
|
-
// A Float style resource ships its plain CSS by href identity — it
|
|
13683
|
-
// neither contributes to the component's scope hash nor gets scoped.
|
|
13684
|
-
if (headResourceKind(node) === 'style') return;
|
|
13685
|
-
const sheet = (node.children || []).find((c) => c && c.type === 'StyleSheet');
|
|
13686
|
-
if (sheet) {
|
|
13687
|
-
styles.push({ node, sheet });
|
|
13688
|
-
if (!cssHash) cssHash = node.metadata?.styleScopeHash || sheet.hash || null;
|
|
13689
|
-
}
|
|
13690
|
-
return;
|
|
13691
|
-
}
|
|
13692
|
-
for (const key of Object.keys(node)) {
|
|
13693
|
-
if (
|
|
13694
|
-
key === 'loc' ||
|
|
13695
|
-
key === 'start' ||
|
|
13696
|
-
key === 'end' ||
|
|
13697
|
-
key === 'parent' ||
|
|
13698
|
-
key === 'metadata' ||
|
|
13699
|
-
key === 'css'
|
|
13700
|
-
)
|
|
13701
|
-
continue;
|
|
13702
|
-
const v = node[key];
|
|
13703
|
-
if (v && typeof v === 'object') collect(v);
|
|
13704
|
-
}
|
|
13705
|
-
}
|
|
13706
|
-
for (const root of roots) collect(root.node);
|
|
13707
|
-
if (!cssHash || styles.length === 0) return { cssHash: null, node: componentNode };
|
|
13708
|
-
// A component has one scope even when its CSS is split for readability.
|
|
13709
|
-
// Rebase before analyze/render: selector and keyframe rewriting read
|
|
13710
|
-
// `sheet.hash`, while DOM annotation below uses `cssHash`. The core
|
|
13711
|
-
// analyze/prepare pipeline mutates the sheet it is given, so it runs over a
|
|
13712
|
-
// clone of each (bounded) StyleSheet subtree — the parser-owned sheet nodes
|
|
13713
|
-
// stay pristine.
|
|
13714
|
-
const preparedSheets = styles.map((style) => {
|
|
13715
|
-
const sheet = cloneAstNode(style.sheet);
|
|
13716
|
-
sheet.hash = cssHash;
|
|
13717
|
-
// Mark `:global(...)` selectors before scoping so they render unscoped.
|
|
13718
|
-
analyzeCss(sheet);
|
|
13719
|
-
prepareStylesheetForRender(sheet);
|
|
13720
|
-
return sheet;
|
|
13721
|
-
});
|
|
13722
|
-
const css = renderStylesheets(preparedSheets);
|
|
13723
|
-
ctx.cssInjections.push({
|
|
13724
|
-
hash: cssHash,
|
|
13725
|
-
css,
|
|
13726
|
-
order: styles[0]?.sheet.start ?? styles[0]?.node.start ?? componentNode.start ?? 0,
|
|
13727
|
-
// The authored `<style>` element(s) this stylesheet came from.
|
|
13728
|
-
// `injectStyle` is emitted at module scope, so without an origin the
|
|
13729
|
-
// block maps to the module and neither the tag nor its CSS is reachable.
|
|
13730
|
-
origins: styles.map((style) => style.node).filter(Boolean),
|
|
13731
|
-
});
|
|
13732
|
-
ctx.runtimeNeeded.add('injectStyle');
|
|
13733
|
-
// Rebuild every owned render root copy-on-write: add the canonical hash
|
|
13734
|
-
// class to native elements and strip JSXStyleElement nodes from DOM output.
|
|
13735
|
-
// The rebuilt roots are grafted back through a spine rebuild of the
|
|
13736
|
-
// component node; the received component tree is never modified.
|
|
13737
|
-
let node = componentNode;
|
|
13738
|
-
const returnReplacements = new Map();
|
|
13739
|
-
for (const root of roots) {
|
|
13740
|
-
// Normalize dynamic class exprs BEFORE the hash is appended (see helper), so
|
|
13741
|
-
// clsx array/object values compose correctly alongside the scope hash.
|
|
13742
|
-
const wrapped = wrapScopedClassExprs(root.node, ctx);
|
|
13743
|
-
const annotated = annotateRootWithHash(wrapped, cssHash, 'class');
|
|
13744
|
-
if (annotated === root.node) continue;
|
|
13745
|
-
if (root.kind === 'render') {
|
|
13746
|
-
node = { ...node, body: { ...node.body, render: annotated } };
|
|
13747
|
-
} else {
|
|
13748
|
-
returnReplacements.set(root.statement, { ...root.statement, argument: annotated });
|
|
13749
|
-
}
|
|
13750
|
-
}
|
|
13751
|
-
if (returnReplacements.size > 0) {
|
|
13752
|
-
node = mapAst(node, (n) => returnReplacements.get(n) ?? null);
|
|
13753
|
-
}
|
|
13754
|
-
return { cssHash, node };
|
|
13755
|
-
}
|
|
13756
|
-
|
|
13757
13427
|
// Strip comments and plain string literals so prose or route paths cannot
|
|
13758
13428
|
// register as a component reference during the early-reference scan below.
|
|
13759
13429
|
// Template literals are deliberately LEFT intact: `${Name}` interpolations are
|
|
@@ -13983,14 +13653,15 @@ function compileComponent(node, ctx, options) {
|
|
|
13983
13653
|
: null;
|
|
13984
13654
|
const firstStyle = ctx.cssInjections.length;
|
|
13985
13655
|
|
|
13986
|
-
// Scoped `<style>`
|
|
13987
|
-
//
|
|
13988
|
-
//
|
|
13989
|
-
//
|
|
13990
|
-
//
|
|
13991
|
-
//
|
|
13992
|
-
//
|
|
13993
|
-
|
|
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);
|
|
13994
13665
|
node = scoping.node;
|
|
13995
13666
|
let cssHash = scoping.cssHash;
|
|
13996
13667
|
// Backwards-compat: internal callers (legacy synthetic Component shapes)
|
|
@@ -17881,7 +17552,7 @@ function compileReturnJsxFunction(node, ctx, options) {
|
|
|
17881
17552
|
const hookMemoOpaqueOwner = ctx.inlineHookMemo && hasInlineMemoOpaqueDirective(node);
|
|
17882
17553
|
recordProfileComponent(ctx, node, name);
|
|
17883
17554
|
const beforeCss = ctx.cssInjections.length;
|
|
17884
|
-
const scoping =
|
|
17555
|
+
const scoping = applyStyleScopes(node, ctx);
|
|
17885
17556
|
node = scoping.node;
|
|
17886
17557
|
const cssHash = scoping.cssHash;
|
|
17887
17558
|
const cssEntries =
|
|
@@ -18003,6 +17674,7 @@ function compileReturnJsxFunction(node, ctx, options) {
|
|
|
18003
17674
|
compInlinedSubs,
|
|
18004
17675
|
cssHash,
|
|
18005
17676
|
cssEntries,
|
|
17677
|
+
scoping.runtimeApplied,
|
|
18006
17678
|
),
|
|
18007
17679
|
};
|
|
18008
17680
|
}
|
|
@@ -18082,6 +17754,10 @@ function compileReturnJsxFunction(node, ctx, options) {
|
|
|
18082
17754
|
}),
|
|
18083
17755
|
);
|
|
18084
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
|
+
}
|
|
18085
17761
|
const emittedFunction = b.function_declaration(
|
|
18086
17762
|
node.id,
|
|
18087
17763
|
node.params,
|
|
@@ -18125,7 +17801,15 @@ function compileReturnJsxFunction(node, ctx, options) {
|
|
|
18125
17801
|
return { nodes: [fn] };
|
|
18126
17802
|
}
|
|
18127
17803
|
|
|
18128
|
-
function nativeReturnedJsxValue(
|
|
17804
|
+
function nativeReturnedJsxValue(
|
|
17805
|
+
node,
|
|
17806
|
+
ctx,
|
|
17807
|
+
name,
|
|
17808
|
+
inlinedSubs,
|
|
17809
|
+
cssHash,
|
|
17810
|
+
cssEntries,
|
|
17811
|
+
runtimeApplied = [],
|
|
17812
|
+
) {
|
|
18129
17813
|
let value;
|
|
18130
17814
|
if (requiresTemplateNormalization(node, 'html', true, ctx)) {
|
|
18131
17815
|
// Head resources, directive roots and other compiler-only syntax keep
|
|
@@ -18145,7 +17829,8 @@ function nativeReturnedJsxValue(node, ctx, name, inlinedSubs, cssHash, cssEntrie
|
|
|
18145
17829
|
}
|
|
18146
17830
|
// A stored styled value may be constructed outside an SSR render. Its CSS
|
|
18147
17831
|
// enters the request collector when interpreted, not a global pending queue.
|
|
18148
|
-
|
|
17832
|
+
const themeTouches = serverThemeTouchStatements(runtimeApplied, ctx, node);
|
|
17833
|
+
if (cssHash === null && !ctx.nativeModuleStyles && themeTouches.length === 0) return value;
|
|
18149
17834
|
const scopedElement = runtimeAliasForContext(ctx, 'nativeCreateScopedElement');
|
|
18150
17835
|
const scopedValue = runtimeAliasForContext(ctx, 'nativeCreateScopedValue');
|
|
18151
17836
|
// A static styled fragment needs a reader too, even when it only owns a
|
|
@@ -18166,15 +17851,18 @@ function nativeReturnedJsxValue(node, ctx, name, inlinedSubs, cssHash, cssEntrie
|
|
|
18166
17851
|
node,
|
|
18167
17852
|
);
|
|
18168
17853
|
}
|
|
18169
|
-
if (cssEntries.length === 0) return value;
|
|
18170
|
-
ctx.runtimeNeeded.add('injectStyle');
|
|
18171
|
-
const css =
|
|
18172
|
-
|
|
18173
|
-
|
|
18174
|
-
|
|
18175
|
-
|
|
18176
|
-
|
|
18177
|
-
|
|
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
|
+
];
|
|
18178
17866
|
const readerIndex =
|
|
18179
17867
|
value.callee?.name === scopedValue ? 0 : value.callee?.name === scopedElement ? 2 : -1;
|
|
18180
17868
|
if (readerIndex === -1) {
|
|
@@ -18319,11 +18007,22 @@ function requiresTemplateNormalization(
|
|
|
18319
18007
|
t === 'FragmentStart' ||
|
|
18320
18008
|
t === 'FragmentEnd' ||
|
|
18321
18009
|
t === 'FoldedDirective' ||
|
|
18322
|
-
t === 'HeadHoist'
|
|
18323
|
-
t === 'JSXCodeBlock'
|
|
18010
|
+
t === 'HeadHoist'
|
|
18324
18011
|
) {
|
|
18325
18012
|
return true;
|
|
18326
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
|
+
}
|
|
18327
18026
|
if (t === 'Fragment' || t === 'JSXFragment' || t === 'Tsx' || t === 'Tsrx') {
|
|
18328
18027
|
return (node.children || []).some((child) =>
|
|
18329
18028
|
requiresTemplateNormalization(child, parentNs, allowHeadHoists, ctx),
|
|
@@ -18626,6 +18325,15 @@ function extractFragment(node, ctx, holeProps, parentNs = 'html') {
|
|
|
18626
18325
|
// `{/* … */}` is a `JSXEmptyExpression` container — a JSX comment, which
|
|
18627
18326
|
// renders to nothing (React drops it). Skip it so it never becomes a hole.
|
|
18628
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
|
+
}
|
|
18629
18337
|
const hn = `h${holeProps.length}`;
|
|
18630
18338
|
if (expr && expr.type === 'TSAsExpression') {
|
|
18631
18339
|
// Preserve the `as T` cast in the renderer (it marks a dynamic TEXT hole).
|
|
@@ -21327,7 +21035,7 @@ function normalizeChildren(
|
|
|
21327
21035
|
continue;
|
|
21328
21036
|
}
|
|
21329
21037
|
// Drop a `<style>` block at child position — its CSS gets registered
|
|
21330
|
-
// via the
|
|
21038
|
+
// via the style-scope pre-pass (applyStyleScopes, ./style-scopes.js);
|
|
21331
21039
|
// it contributes no DOM here.
|
|
21332
21040
|
continue;
|
|
21333
21041
|
} else if (isWrappedJsxDirective(n)) {
|
|
@@ -24116,7 +23824,7 @@ function cleanupsPush(fnNode) {
|
|
|
24116
23824
|
}
|
|
24117
23825
|
|
|
24118
23826
|
/**
|
|
24119
|
-
* The `[spread, name, value]` rows a commit-phase source collector
|
|
23827
|
+
* The `[spread, name, value, merge?]` rows a commit-phase source collector
|
|
24120
23828
|
* (`setHostPropSources` / `setFormControlSources`) resolves in authored order.
|
|
24121
23829
|
* Shared by the mount and update emits, which differ only in how a source's
|
|
24122
23830
|
* value is read back — a bag local at mount, a bag field on update.
|
|
@@ -24126,20 +23834,26 @@ function cleanupsPush(fnNode) {
|
|
|
24126
23834
|
* split ssrDirectSource makes on the server); the collector itself serializes
|
|
24127
23835
|
* every source of the element and stays anchored there. Nothing else in a row
|
|
24128
23836
|
* reproduces the authored name — the value is the prop's expression — so
|
|
24129
|
-
* 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.
|
|
24130
23839
|
*/
|
|
24131
23840
|
function commitSourceRows(sources, valueOf) {
|
|
24132
|
-
|
|
24133
|
-
|
|
24134
|
-
|
|
24135
|
-
|
|
24136
|
-
|
|
24137
|
-
|
|
24138
|
-
|
|
24139
|
-
|
|
24140
|
-
|
|
24141
|
-
|
|
24142
|
-
|
|
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);
|
|
24143
23857
|
}
|
|
24144
23858
|
|
|
24145
23859
|
// Mount for a DEFERRED property-write binding: store the element ref + seed the
|
|
@@ -25630,6 +25344,7 @@ function emitElementHtml(
|
|
|
25630
25344
|
name: rawAttrName,
|
|
25631
25345
|
nameOrigin: attr.name,
|
|
25632
25346
|
binding,
|
|
25347
|
+
merge: isSynthesizedClassAttr(attr),
|
|
25633
25348
|
});
|
|
25634
25349
|
continue;
|
|
25635
25350
|
}
|
|
@@ -27221,6 +26936,21 @@ function tagBindingName(node) {
|
|
|
27221
26936
|
return name.type === 'Identifier' || name.type === 'JSXIdentifier' ? name.name : null;
|
|
27222
26937
|
}
|
|
27223
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
|
+
|
|
27224
26954
|
// React-style render-prop detection: if a component's children are exactly one
|
|
27225
26955
|
// `{fn}` expression hole whose expression is a function (arrow or function
|
|
27226
26956
|
// expression) — `<Comp>{(data) => <jsx/>}</Comp>` — return that function node so
|
|
@@ -27573,7 +27303,7 @@ function makeCompCall(
|
|
|
27573
27303
|
const children = rewriteOpaqueTitles(sourceChildren, ctx, 'opaque');
|
|
27574
27304
|
const childrenParentNs =
|
|
27575
27305
|
!activityDescriptor && !isFragmentLongForm(node, ctx) ? 'opaque' : parentNs;
|
|
27576
|
-
if (compName !== null && ctx
|
|
27306
|
+
if (compName !== null && isDescriptorChildrenTag(node, ctx)) {
|
|
27577
27307
|
const kids = children
|
|
27578
27308
|
.map((child) => lowerInspectableJsxChild(child, ctx))
|
|
27579
27309
|
.filter((child) => child != null);
|