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
|
@@ -0,0 +1,1134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lexically scoped `<style>` blocks, `$class`, and `apply` for the Octane
|
|
3
|
+
* compiler (RFC tsrx-org/RFCs#1). One copy-on-write pre-pass per top-level
|
|
4
|
+
* statement decides which blocks belong to which template scope, emits one
|
|
5
|
+
* `injectStyle(hash, css)` entry per scope in lexical pre-order, and stamps
|
|
6
|
+
* every host element with the hashes of all its enclosing scopes followed by
|
|
7
|
+
* the classes of every applied theme — so the client and server emitters only
|
|
8
|
+
* serialize what is already on the AST and agree by construction.
|
|
9
|
+
*
|
|
10
|
+
* Scope model (shared with `@tsrx/core`'s `transform/jsx/style-scopes.js`;
|
|
11
|
+
* amendment A1 of the RFC):
|
|
12
|
+
*
|
|
13
|
+
* - A standalone block is a child of a native element or a fragment, and
|
|
14
|
+
* that CHILDREN LIST is its scope: the block styles the items beside it and
|
|
15
|
+
* everything below them; it never styles the element that contains it, nor
|
|
16
|
+
* any ancestor. A `@{ … }` body or a directive branch body holds setup
|
|
17
|
+
* statements and exactly one output node, and a block is an output node
|
|
18
|
+
* like any other, so a block beside the output node is the parser's
|
|
19
|
+
* multiple-outputs error and a lone block as the output is the analyzer's
|
|
20
|
+
* `tsrx-style-standalone-needs-fragment`; those lists are only searched for
|
|
21
|
+
* nested templates and assigned blocks. A list holding no block is not a
|
|
22
|
+
* scope and adds no hash.
|
|
23
|
+
* - Every block of one list shares the scope hash (the first bodied block's
|
|
24
|
+
* position-derived hash) and renders into ONE injection — the runtime
|
|
25
|
+
* dedupes by id. Lists nested in a scope that hold blocks are nested scopes
|
|
26
|
+
* with hashes of their own; two blocks in different lists never share one.
|
|
27
|
+
* - A scope's sheets are pruned against the list's other children and their
|
|
28
|
+
* subtrees (function boundaries excluded): a selector that matches none of
|
|
29
|
+
* them survives only as a `(unused)` comment, so a rule aimed at the
|
|
30
|
+
* container is visibly dead. `@tsrx/core`'s `prune_css` matches selectors
|
|
31
|
+
* against elements and records what it found on their metadata, and the
|
|
32
|
+
* adopted AST may be frozen, so the elements it sees are a private clone of
|
|
33
|
+
* the items (`cloneAstNode`, fresh metadata per node) carrying the ancestor
|
|
34
|
+
* paths its combinator matching reads.
|
|
35
|
+
* - Elements carry `authored hashes… applied…`: enclosing scope hashes outer
|
|
36
|
+
* first, then applied theme classes (literals for same-module themes whose
|
|
37
|
+
* class is statically known, `theme.$class` reads otherwise). `apply` on a
|
|
38
|
+
* standalone block reaches the same set of elements as its CSS.
|
|
39
|
+
* - Emission order is lexical pre-order: a scope's sheet sits where its first
|
|
40
|
+
* block is, after the assigned blocks declared before it in the enclosing
|
|
41
|
+
* statement list, before the scopes and assigned blocks nested in it.
|
|
42
|
+
* - Raw CSS in `<style>` is TSRX template syntax: the core analyzer rejects a
|
|
43
|
+
* standalone block outside a `@{ … }` or directive body
|
|
44
|
+
* (`tsrx-style-standalone-outside-template`), so plain-TSX returns never
|
|
45
|
+
* reach this pass with one. `<style>{expr}</style>` parses as an ordinary
|
|
46
|
+
* `JSXElement`: it is not a block, opens no scope, and is never stamped.
|
|
47
|
+
* - Assigned blocks (`const theme = <style>…</style>`) lower anywhere a
|
|
48
|
+
* declaration is legal: their sheet injects at the declaration position and
|
|
49
|
+
* the initializer becomes the class-map object (`$class` first). Exported or
|
|
50
|
+
* applied blocks are themes and keep every selector; other blocks keep only
|
|
51
|
+
* what the class map exposes. The classification comes from the core
|
|
52
|
+
* analyzer (`metadata.styleKind`, `metadata.styleApplies`), which runs on
|
|
53
|
+
* the parser AST before it is adopted, so this pass reads it read-only.
|
|
54
|
+
* - `style(expr)` is Octane's class-string expression (core has no such
|
|
55
|
+
* intrinsic): it resolves to the scope chain plus the value only where TSRX
|
|
56
|
+
* reads a class value — as the expression of a JSX attribute value or of a
|
|
57
|
+
* template expression-container child, directly or nested in the
|
|
58
|
+
* array/conditional/logical/template expressions of that container. The
|
|
59
|
+
* `style` attribute is excluded (its value is CSS, never a class list), and
|
|
60
|
+
* every other `style(...)` — a statement, a declaration initializer, a call
|
|
61
|
+
* argument, a callback body — is an ordinary user call left untouched.
|
|
62
|
+
* - Float style resources (`<style href precedence>`) stay in the tree.
|
|
63
|
+
*
|
|
64
|
+
* The parser AST may be frozen (see `adoptParserAst`): nodes are never
|
|
65
|
+
* written; changed spines are rebuilt and StyleSheet subtrees are cloned
|
|
66
|
+
* before the core render pipeline touches them.
|
|
67
|
+
*/
|
|
68
|
+
|
|
69
|
+
import {
|
|
70
|
+
analyzeCss,
|
|
71
|
+
buildStyleClassMap,
|
|
72
|
+
builders as b,
|
|
73
|
+
clone_ast_node as cloneAstNode,
|
|
74
|
+
createScopeRoot,
|
|
75
|
+
createStyleClassMapFromStylesheet,
|
|
76
|
+
prepareStylesheetForRender,
|
|
77
|
+
pruneCss,
|
|
78
|
+
renderStylesheets,
|
|
79
|
+
} from '@tsrx/core';
|
|
80
|
+
|
|
81
|
+
const SKIP_KEYS = new Set(['loc', 'start', 'end', 'parent', 'metadata', 'css']);
|
|
82
|
+
const DIRECTIVE_TYPES = new Set([
|
|
83
|
+
'JSXIfExpression',
|
|
84
|
+
'JSXForExpression',
|
|
85
|
+
'JSXSwitchExpression',
|
|
86
|
+
'JSXTryExpression',
|
|
87
|
+
]);
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @typedef {{
|
|
91
|
+
* inheritOriginLoc: (root: any, origin: any) => any,
|
|
92
|
+
* markSynthesized: (node: any) => any,
|
|
93
|
+
* markSynthesizedAttr: (attr: any) => any,
|
|
94
|
+
* headResourceKind: (node: any) => string | null,
|
|
95
|
+
* isCompositeJsxTag: (node: any) => boolean,
|
|
96
|
+
* isStyleCall: (node: any) => boolean,
|
|
97
|
+
* }} StyleScopeTools
|
|
98
|
+
* @typedef {{ hash: string | null, applied: Array<string | any> }} ScopeEntry
|
|
99
|
+
* @typedef {{
|
|
100
|
+
* ctx: any,
|
|
101
|
+
* tools: StyleScopeTools,
|
|
102
|
+
* stack: ScopeEntry[],
|
|
103
|
+
* sequence: number,
|
|
104
|
+
* orderBase: number,
|
|
105
|
+
* firstHash: string | null,
|
|
106
|
+
* staticClasses: Map<any, string | null>,
|
|
107
|
+
* appliedParts: Map<any, Array<string | any>>,
|
|
108
|
+
* runtimeApplied: any[],
|
|
109
|
+
* stampedHost: boolean,
|
|
110
|
+
* }} PassState `stampedHost` is set while the attributes of a host element
|
|
111
|
+
* the current chain will be stamped on are walked: a `style(expr)` nested in
|
|
112
|
+
* that element's class value yields its value alone, the stamp adds the chain.
|
|
113
|
+
*/
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Bind the compiler helpers this pass needs (they live in compile.js).
|
|
117
|
+
*
|
|
118
|
+
* @param {StyleScopeTools} tools
|
|
119
|
+
*/
|
|
120
|
+
export function createStyleScopePass(tools) {
|
|
121
|
+
/**
|
|
122
|
+
* Rewrite one top-level statement (or a component function) copy-on-write.
|
|
123
|
+
* Injections are pushed onto `ctx.cssInjections` in emission order with an
|
|
124
|
+
* `order` key that sorts them among the module's other entries.
|
|
125
|
+
*
|
|
126
|
+
* @param {any} root
|
|
127
|
+
* @param {any} ctx
|
|
128
|
+
* @returns {{ node: any, cssHash: string | null, runtimeApplied: any[] }}
|
|
129
|
+
* the rewritten node, the outermost scope hash the root owns (or `null`,
|
|
130
|
+
* kept for callers that key "this component has scoped CSS" on it), and
|
|
131
|
+
* the `theme.$class` targets the root's standalone scopes read at runtime
|
|
132
|
+
* (imported themes), one per distinct target expression
|
|
133
|
+
*/
|
|
134
|
+
function applyStyleScopes(root, ctx) {
|
|
135
|
+
/** @type {PassState} */
|
|
136
|
+
const state = {
|
|
137
|
+
ctx,
|
|
138
|
+
tools,
|
|
139
|
+
stack: [],
|
|
140
|
+
sequence: 0,
|
|
141
|
+
orderBase: typeof root?.start === 'number' ? root.start : 0,
|
|
142
|
+
firstHash: null,
|
|
143
|
+
staticClasses: new Map(),
|
|
144
|
+
appliedParts: new Map(),
|
|
145
|
+
runtimeApplied: [],
|
|
146
|
+
stampedHost: false,
|
|
147
|
+
};
|
|
148
|
+
const node = walk(root, state, 'statement');
|
|
149
|
+
return { node, cssHash: state.firstHash, runtimeApplied: state.runtimeApplied };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return { applyStyleScopes };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// --- traversal ---------------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* @param {any} node
|
|
159
|
+
* @returns {boolean}
|
|
160
|
+
*/
|
|
161
|
+
function isTemplateNode(node) {
|
|
162
|
+
return node?.type === 'JSXElement' || node?.type === 'JSXFragment';
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* @param {any} node
|
|
167
|
+
* @returns {boolean}
|
|
168
|
+
*/
|
|
169
|
+
function isDirective(node) {
|
|
170
|
+
return node != null && DIRECTIVE_TYPES.has(node.type);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* @param {any} node
|
|
175
|
+
* @returns {boolean}
|
|
176
|
+
*/
|
|
177
|
+
function isFunctionNode(node) {
|
|
178
|
+
return (
|
|
179
|
+
node?.type === 'FunctionDeclaration' ||
|
|
180
|
+
node?.type === 'FunctionExpression' ||
|
|
181
|
+
node?.type === 'ArrowFunctionExpression'
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* @param {any} node
|
|
187
|
+
* @param {string} key
|
|
188
|
+
* @returns {boolean}
|
|
189
|
+
*/
|
|
190
|
+
function isStatementListKey(node, key) {
|
|
191
|
+
if (key === 'body') return node.type === 'BlockStatement' || node.type === 'Program';
|
|
192
|
+
if (key === 'consequent') return node.type === 'SwitchCase';
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* @template T
|
|
198
|
+
* @param {T[]} list
|
|
199
|
+
* @param {(item: T) => T | null} map `null` drops the item
|
|
200
|
+
* @returns {T[]}
|
|
201
|
+
*/
|
|
202
|
+
function mapList(list, map) {
|
|
203
|
+
let out = null;
|
|
204
|
+
for (let i = 0; i < list.length; i++) {
|
|
205
|
+
const item = list[i];
|
|
206
|
+
const next = item !== null && typeof item === 'object' ? map(item) : item;
|
|
207
|
+
if (out === null && next !== item) out = list.slice(0, i);
|
|
208
|
+
if (out !== null && next !== null) out.push(next);
|
|
209
|
+
}
|
|
210
|
+
return out ?? list;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Copy-on-write map over a node's child properties.
|
|
215
|
+
*
|
|
216
|
+
* @param {any} node
|
|
217
|
+
* @param {(child: any, key: string) => any} map
|
|
218
|
+
* @returns {any}
|
|
219
|
+
*/
|
|
220
|
+
function rewriteChildren(node, map) {
|
|
221
|
+
let out = null;
|
|
222
|
+
for (const key of Object.keys(node)) {
|
|
223
|
+
if (SKIP_KEYS.has(key)) continue;
|
|
224
|
+
const value = node[key];
|
|
225
|
+
if (value === null || typeof value !== 'object') continue;
|
|
226
|
+
let next;
|
|
227
|
+
if (Array.isArray(value)) {
|
|
228
|
+
next = mapList(value, (child) => map(child, key));
|
|
229
|
+
} else if (typeof value.type === 'string') {
|
|
230
|
+
next = map(value, key);
|
|
231
|
+
} else {
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
if (next !== value) {
|
|
235
|
+
if (out === null) out = { ...node };
|
|
236
|
+
out[key] = next;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return out ?? node;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* The main copy-on-write walk.
|
|
244
|
+
*
|
|
245
|
+
* `mode` says what a node found here is:
|
|
246
|
+
* - `statement`: a statement slot (a bare `<style>` here was already reported
|
|
247
|
+
* by the analyzer),
|
|
248
|
+
* - `expression`: a value slot (a `<style>` here is an assigned block).
|
|
249
|
+
*
|
|
250
|
+
* A native element or fragment is the same thing in either slot: it is
|
|
251
|
+
* stamped with the current chain and its children list may open a scope.
|
|
252
|
+
*
|
|
253
|
+
* @param {any} node
|
|
254
|
+
* @param {PassState} state
|
|
255
|
+
* @param {'statement' | 'expression'} mode
|
|
256
|
+
* @returns {any}
|
|
257
|
+
*/
|
|
258
|
+
function walk(node, state, mode) {
|
|
259
|
+
if (node === null || typeof node !== 'object' || typeof node.type !== 'string') return node;
|
|
260
|
+
|
|
261
|
+
switch (node.type) {
|
|
262
|
+
case 'JSXStyleElement':
|
|
263
|
+
// A value slot is an assigned block. A statement slot (or the render
|
|
264
|
+
// slot of a code block) is the analyzer's `needs-fragment` error; the
|
|
265
|
+
// node is left alone and contributes no CSS.
|
|
266
|
+
if (mode === 'expression') return lowerAssignedStyle(node, state);
|
|
267
|
+
return node;
|
|
268
|
+
case 'JSXCodeBlock':
|
|
269
|
+
return processCodeBlock(node, state);
|
|
270
|
+
case 'JSXElement':
|
|
271
|
+
case 'JSXFragment':
|
|
272
|
+
return walkTemplateNode(node, state);
|
|
273
|
+
case 'JSXExpressionContainer':
|
|
274
|
+
return rewriteChildren(node, (child) => walk(child, state, 'expression'));
|
|
275
|
+
case 'ExpressionStatement':
|
|
276
|
+
return rewriteChildren(node, (child) => walk(child, state, 'statement'));
|
|
277
|
+
case 'JSXAttribute':
|
|
278
|
+
return processAttribute(node, state);
|
|
279
|
+
default:
|
|
280
|
+
if (isDirective(node)) return processDirective(node, state);
|
|
281
|
+
if (isFunctionNode(node)) {
|
|
282
|
+
// A function boundary: its template is not stamped by the enclosing
|
|
283
|
+
// scopes, but still hosts scopes and assigned blocks of its own.
|
|
284
|
+
return withStack(state, [], () =>
|
|
285
|
+
rewriteChildren(node, (child, key) =>
|
|
286
|
+
walk(
|
|
287
|
+
child,
|
|
288
|
+
state,
|
|
289
|
+
key === 'body' && child.type === 'JSXCodeBlock' ? 'statement' : 'expression',
|
|
290
|
+
),
|
|
291
|
+
),
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return rewriteChildren(node, (child, key) =>
|
|
298
|
+
walk(child, state, isStatementListKey(node, key) ? 'statement' : 'expression'),
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* @template T
|
|
304
|
+
* @param {PassState} state
|
|
305
|
+
* @param {ScopeEntry[]} stack
|
|
306
|
+
* @param {() => T} run
|
|
307
|
+
* @returns {T}
|
|
308
|
+
*/
|
|
309
|
+
function withStack(state, stack, run) {
|
|
310
|
+
const previous = state.stack;
|
|
311
|
+
state.stack = stack;
|
|
312
|
+
try {
|
|
313
|
+
return run();
|
|
314
|
+
} finally {
|
|
315
|
+
state.stack = previous;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* A native element or fragment: stamp it with the CURRENT chain (host
|
|
321
|
+
* elements only — the element itself is never inside its own children's
|
|
322
|
+
* scope), walk its attribute values as expressions of that same chain, then
|
|
323
|
+
* walk its children list. When the list holds standalone blocks it is a scope:
|
|
324
|
+
* the blocks are stripped, their sheet prepared, and the other children walked
|
|
325
|
+
* with the scope pushed. A list without blocks keeps the current chain.
|
|
326
|
+
*
|
|
327
|
+
* @param {any} node
|
|
328
|
+
* @param {PassState} state
|
|
329
|
+
* @returns {any}
|
|
330
|
+
*/
|
|
331
|
+
function walkTemplateNode(node, state) {
|
|
332
|
+
// Attribute values first — a `style(expr)` in a class value resolves to the
|
|
333
|
+
// chain before the stamp reads that value — then the stamp itself.
|
|
334
|
+
const stamped = isStampedHost(node, state);
|
|
335
|
+
const previousStampedHost = state.stampedHost;
|
|
336
|
+
state.stampedHost = stamped;
|
|
337
|
+
let out;
|
|
338
|
+
try {
|
|
339
|
+
out = rewriteChildren(node, (child, key) =>
|
|
340
|
+
key === 'children' ? child : walk(child, state, 'expression'),
|
|
341
|
+
);
|
|
342
|
+
} finally {
|
|
343
|
+
state.stampedHost = previousStampedHost;
|
|
344
|
+
}
|
|
345
|
+
if (stamped) out = addScopeClasses(out, state);
|
|
346
|
+
const children = node.children;
|
|
347
|
+
if (!Array.isArray(children)) return out;
|
|
348
|
+
const own = collectOwnBlocks(children);
|
|
349
|
+
const scope =
|
|
350
|
+
own.length > 0
|
|
351
|
+
? prepareScope(
|
|
352
|
+
own,
|
|
353
|
+
state,
|
|
354
|
+
children.filter((child) => !own.includes(child)),
|
|
355
|
+
)
|
|
356
|
+
: null;
|
|
357
|
+
const next = withStack(state, scope === null ? state.stack : [...state.stack, scope], () =>
|
|
358
|
+
mapList(children, (child) => {
|
|
359
|
+
if (child.type === 'JSXStyleElement') return own.includes(child) ? null : child;
|
|
360
|
+
return walkListItem(child, state);
|
|
361
|
+
}),
|
|
362
|
+
);
|
|
363
|
+
return next === children ? out : { ...out, children: next };
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Whether the scope chain is stamped on this element: a host element or a
|
|
368
|
+
* dynamic `<{expr}>` tag. Composite components stop stamping (their host
|
|
369
|
+
* elements belong to their own scopes), and a `style` host element —
|
|
370
|
+
* `<style>{expr}</style>`, an ordinary element after amendment A1 — is never
|
|
371
|
+
* stamped, like a `<style>` block.
|
|
372
|
+
*
|
|
373
|
+
* @param {any} node
|
|
374
|
+
* @param {PassState} state
|
|
375
|
+
* @returns {boolean}
|
|
376
|
+
*/
|
|
377
|
+
function isStampedHost(node, state) {
|
|
378
|
+
if (node.type !== 'JSXElement') return false;
|
|
379
|
+
if (state.tools.isCompositeJsxTag(node) && !node.metadata?.dynamicElement) return false;
|
|
380
|
+
const name = node.openingElement?.name;
|
|
381
|
+
return !(name?.type === 'JSXIdentifier' && name.name === 'style');
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* `@{ … }`: setup statements (searched for assigned blocks and nested
|
|
386
|
+
* templates) and the single output node, walked in the current chain.
|
|
387
|
+
*
|
|
388
|
+
* @param {any} node
|
|
389
|
+
* @param {PassState} state
|
|
390
|
+
* @returns {any}
|
|
391
|
+
*/
|
|
392
|
+
function processCodeBlock(node, state) {
|
|
393
|
+
const body = processList(node.body || [], state);
|
|
394
|
+
const render = node.render ? walkListItem(node.render, state) : node.render;
|
|
395
|
+
if (body === node.body && render === node.render) return node;
|
|
396
|
+
return { ...node, body, render };
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Each branch body of a directive is a scope; `@else if` chains parse as
|
|
401
|
+
* plain `IfStatement` alternates.
|
|
402
|
+
*
|
|
403
|
+
* @param {any} node
|
|
404
|
+
* @param {PassState} state
|
|
405
|
+
* @returns {any}
|
|
406
|
+
*/
|
|
407
|
+
function processDirective(node, state) {
|
|
408
|
+
return rewriteChildren(node, (child, key) => {
|
|
409
|
+
if (child.type === 'BlockStatement') return processBlockBody(child, state);
|
|
410
|
+
if (child.type === 'CatchClause') {
|
|
411
|
+
const body = processBlockBody(child.body, state);
|
|
412
|
+
return body === child.body ? child : { ...child, body };
|
|
413
|
+
}
|
|
414
|
+
if (child.type === 'SwitchCase') {
|
|
415
|
+
const consequent = processList(child.consequent || [], state);
|
|
416
|
+
return consequent === child.consequent ? child : { ...child, consequent };
|
|
417
|
+
}
|
|
418
|
+
if (key === 'alternate' && (isDirective(child) || child.type === 'IfStatement')) {
|
|
419
|
+
return processDirective(child, state);
|
|
420
|
+
}
|
|
421
|
+
return walk(child, state, 'expression');
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* @param {any} block
|
|
427
|
+
* @param {PassState} state
|
|
428
|
+
* @returns {any}
|
|
429
|
+
*/
|
|
430
|
+
function processBlockBody(block, state) {
|
|
431
|
+
const body = processList(block.body || [], state);
|
|
432
|
+
return body === block.body ? block : { ...block, body };
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* A statement list (a `@{ … }` body, a directive branch body, a switch case):
|
|
437
|
+
* not a scope of its own. Its items are searched for nested templates and
|
|
438
|
+
* assigned blocks in the current chain.
|
|
439
|
+
*
|
|
440
|
+
* @param {any[]} nodes
|
|
441
|
+
* @param {PassState} state
|
|
442
|
+
* @returns {any[]}
|
|
443
|
+
*/
|
|
444
|
+
function processList(nodes, state) {
|
|
445
|
+
return mapList(nodes, (item) => walkListItem(item, state));
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* @param {any} item
|
|
450
|
+
* @param {PassState} state
|
|
451
|
+
* @returns {any}
|
|
452
|
+
*/
|
|
453
|
+
function walkListItem(item, state) {
|
|
454
|
+
if (isTemplateNode(item)) return walkTemplateNode(item, state);
|
|
455
|
+
if (item.type === 'JSXStyleElement') return item;
|
|
456
|
+
if (item.type === 'JSXExpressionContainer') {
|
|
457
|
+
// A template child hole is a class-string position for `style(expr)`.
|
|
458
|
+
const expression = walkStyleValue(item.expression, state);
|
|
459
|
+
return expression === item.expression ? item : { ...item, expression };
|
|
460
|
+
}
|
|
461
|
+
return walk(item, state, 'statement');
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* A JSX attribute: its value is a class-string position for `style(expr)`
|
|
466
|
+
* unless the attribute is `style` (CSS, never a class list — a `style(...)`
|
|
467
|
+
* there is a user helper), so that one and every other value walk as ordinary
|
|
468
|
+
* expressions. In the `class` of an element the chain is stamped on, only a
|
|
469
|
+
* whole-value `style(expr)` carries the chain itself; a call nested in an
|
|
470
|
+
* array, conditional, logical, or template there yields its value alone, and
|
|
471
|
+
* the stamp appends the chain once to the composed value — so the chain is
|
|
472
|
+
* present whichever branch runs, and never twice.
|
|
473
|
+
*
|
|
474
|
+
* @param {any} attribute
|
|
475
|
+
* @param {PassState} state
|
|
476
|
+
* @returns {any}
|
|
477
|
+
*/
|
|
478
|
+
function processAttribute(attribute, state) {
|
|
479
|
+
const value = attribute.value;
|
|
480
|
+
if (value?.type !== 'JSXExpressionContainer') {
|
|
481
|
+
return rewriteChildren(attribute, (child) => walk(child, state, 'expression'));
|
|
482
|
+
}
|
|
483
|
+
const name = attribute.name;
|
|
484
|
+
const isStyleAttribute = name?.type === 'JSXIdentifier' && name.name === 'style';
|
|
485
|
+
const isClassAttribute =
|
|
486
|
+
name?.type === 'JSXIdentifier' && (name.name === 'class' || name.name === 'className');
|
|
487
|
+
const expression = isStyleAttribute
|
|
488
|
+
? walk(value.expression, state, 'expression')
|
|
489
|
+
: walkStyleValue(value.expression, state, true, isClassAttribute && state.stampedHost);
|
|
490
|
+
if (expression === value.expression) return attribute;
|
|
491
|
+
return { ...attribute, value: { ...value, expression } };
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* The class-string positions inside an attribute value or a child hole: the
|
|
496
|
+
* expression itself, or the parts of an array, conditional, logical, template,
|
|
497
|
+
* or parenthesized/TS-wrapper expression around it. Anything else is walked as
|
|
498
|
+
* an ordinary expression, so a `style(...)` inside (a call argument, a callback
|
|
499
|
+
* body) stays a user call.
|
|
500
|
+
*
|
|
501
|
+
* @param {any} node
|
|
502
|
+
* @param {PassState} state
|
|
503
|
+
* @param {boolean} [whole] whether `node` is the whole value (wrappers are
|
|
504
|
+
* transparent), as opposed to a part of an array/conditional/logical/template
|
|
505
|
+
* @param {boolean} [stamped] whether the value is the class of an element the
|
|
506
|
+
* chain is stamped on: a `style(expr)` that is only a PART of it then yields
|
|
507
|
+
* its value alone (see processAttribute)
|
|
508
|
+
* @returns {any}
|
|
509
|
+
*/
|
|
510
|
+
function walkStyleValue(node, state, whole = true, stamped = false) {
|
|
511
|
+
if (node === null || typeof node !== 'object' || typeof node.type !== 'string') return node;
|
|
512
|
+
if (state.tools.isStyleCall(node)) {
|
|
513
|
+
if (stamped && !whole) return walk(node.arguments[0], state, 'expression');
|
|
514
|
+
return resolveStyleCall(node, state);
|
|
515
|
+
}
|
|
516
|
+
switch (node.type) {
|
|
517
|
+
case 'ArrayExpression': {
|
|
518
|
+
const elements = mapList(node.elements, (element) =>
|
|
519
|
+
element?.type === 'SpreadElement'
|
|
520
|
+
? walk(element, state, 'expression')
|
|
521
|
+
: walkStyleValue(element, state, false, stamped),
|
|
522
|
+
);
|
|
523
|
+
return elements === node.elements ? node : { ...node, elements };
|
|
524
|
+
}
|
|
525
|
+
case 'ConditionalExpression': {
|
|
526
|
+
const test = walk(node.test, state, 'expression');
|
|
527
|
+
const consequent = walkStyleValue(node.consequent, state, false, stamped);
|
|
528
|
+
const alternate = walkStyleValue(node.alternate, state, false, stamped);
|
|
529
|
+
if (test === node.test && consequent === node.consequent && alternate === node.alternate) {
|
|
530
|
+
return node;
|
|
531
|
+
}
|
|
532
|
+
return { ...node, test, consequent, alternate };
|
|
533
|
+
}
|
|
534
|
+
case 'LogicalExpression': {
|
|
535
|
+
const left = walkStyleValue(node.left, state, false, stamped);
|
|
536
|
+
const right = walkStyleValue(node.right, state, false, stamped);
|
|
537
|
+
return left === node.left && right === node.right ? node : { ...node, left, right };
|
|
538
|
+
}
|
|
539
|
+
case 'TemplateLiteral': {
|
|
540
|
+
const expressions = mapList(node.expressions, (expression) =>
|
|
541
|
+
walkStyleValue(expression, state, false, stamped),
|
|
542
|
+
);
|
|
543
|
+
return expressions === node.expressions ? node : { ...node, expressions };
|
|
544
|
+
}
|
|
545
|
+
case 'ParenthesizedExpression':
|
|
546
|
+
case 'TSAsExpression':
|
|
547
|
+
case 'TSSatisfiesExpression':
|
|
548
|
+
case 'TSNonNullExpression':
|
|
549
|
+
case 'TSTypeAssertion': {
|
|
550
|
+
const expression = walkStyleValue(node.expression, state, whole, stamped);
|
|
551
|
+
return expression === node.expression ? node : { ...node, expression };
|
|
552
|
+
}
|
|
553
|
+
default:
|
|
554
|
+
return walk(node, state, 'expression');
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* The standalone blocks a children list owns: its direct `<style>` items, in
|
|
560
|
+
* source order. Blocks inside a nested element or fragment belong to that
|
|
561
|
+
* node's own children list; Float style resources are not scoped blocks.
|
|
562
|
+
*
|
|
563
|
+
* @param {any[]} nodes
|
|
564
|
+
* @returns {any[]}
|
|
565
|
+
*/
|
|
566
|
+
function collectOwnBlocks(nodes) {
|
|
567
|
+
/** @type {any[]} */
|
|
568
|
+
const blocks = [];
|
|
569
|
+
for (const node of nodes) {
|
|
570
|
+
if (node?.type === 'JSXStyleElement' && !isFloatStyleResource(node)) blocks.push(node);
|
|
571
|
+
}
|
|
572
|
+
return blocks.sort((a, b) => (a.start ?? 0) - (b.start ?? 0));
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* The elements a scope's selectors can match (the core transform's
|
|
577
|
+
* `collect_css_prunable_elements`): every host element of the items and their
|
|
578
|
+
* subtrees, nested scopes included, stopping at function boundaries and
|
|
579
|
+
* skipping `<style>` hosts. Each element gets its ancestor chain as
|
|
580
|
+
* `metadata.path`, which `pruneCss` reads for combinators; the nodes are a
|
|
581
|
+
* private clone, so the writes never reach the (possibly frozen) parser AST.
|
|
582
|
+
*
|
|
583
|
+
* @param {any} value a cloned node or list of cloned nodes
|
|
584
|
+
* @param {any[]} elements
|
|
585
|
+
* @param {any[]} path
|
|
586
|
+
* @returns {any[]}
|
|
587
|
+
*/
|
|
588
|
+
function collectPrunableElements(value, elements, path) {
|
|
589
|
+
if (Array.isArray(value)) {
|
|
590
|
+
for (const item of value) collectPrunableElements(item, elements, path);
|
|
591
|
+
return elements;
|
|
592
|
+
}
|
|
593
|
+
if (value === null || typeof value !== 'object' || typeof value.type !== 'string')
|
|
594
|
+
return elements;
|
|
595
|
+
if (isFunctionNode(value)) return elements;
|
|
596
|
+
if (value.type === 'JSXElement' && value.metadata?.native_tsrx) {
|
|
597
|
+
const name = value.openingElement?.name;
|
|
598
|
+
if (name?.type === 'JSXIdentifier' && name.name === 'style') return elements;
|
|
599
|
+
value.metadata.path = path.slice();
|
|
600
|
+
elements.push(value);
|
|
601
|
+
}
|
|
602
|
+
const childPath = [...path, value];
|
|
603
|
+
for (const key of Object.keys(value)) {
|
|
604
|
+
if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata' || key === 'css') {
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
607
|
+
const child = value[key];
|
|
608
|
+
if (Array.isArray(child)) {
|
|
609
|
+
for (const item of child) {
|
|
610
|
+
if (item !== null && typeof item === 'object' && typeof item.type === 'string') {
|
|
611
|
+
collectPrunableElements(item, elements, childPath);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
} else if (child !== null && typeof child === 'object' && typeof child.type === 'string') {
|
|
615
|
+
collectPrunableElements(child, elements, childPath);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
return elements;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* `<style href precedence>` (React Float): plain CSS by href identity, outside
|
|
623
|
+
* the scope model.
|
|
624
|
+
*
|
|
625
|
+
* @param {any} node
|
|
626
|
+
* @returns {boolean}
|
|
627
|
+
*/
|
|
628
|
+
function isFloatStyleResource(node) {
|
|
629
|
+
let hasHref = false;
|
|
630
|
+
let hasPrecedence = false;
|
|
631
|
+
for (const attr of node.openingElement?.attributes || []) {
|
|
632
|
+
if (attr?.type !== 'JSXAttribute' || attr.name?.type !== 'JSXIdentifier') continue;
|
|
633
|
+
if (attr.name.name === 'href') hasHref = true;
|
|
634
|
+
else if (attr.name.name === 'precedence') hasPrecedence = true;
|
|
635
|
+
}
|
|
636
|
+
return hasHref && hasPrecedence;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// --- sheets ------------------------------------------------------------------
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* @param {PassState} state
|
|
643
|
+
* @returns {number}
|
|
644
|
+
*/
|
|
645
|
+
function nextOrder(state) {
|
|
646
|
+
state.sequence += 1;
|
|
647
|
+
return state.orderBase + state.sequence * 1e-6;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Render a scope's sheets into one injection and compute what its elements
|
|
652
|
+
* carry.
|
|
653
|
+
*
|
|
654
|
+
* @param {any[]} own the list's standalone blocks
|
|
655
|
+
* @param {PassState} state
|
|
656
|
+
* @param {any[]} items the list's other children — what the blocks reach
|
|
657
|
+
* @returns {ScopeEntry}
|
|
658
|
+
*/
|
|
659
|
+
function prepareScope(own, state, items) {
|
|
660
|
+
const { ctx } = state;
|
|
661
|
+
/** @type {Array<{ node: any, sheet: any }>} */
|
|
662
|
+
const sheets = [];
|
|
663
|
+
for (const block of own) {
|
|
664
|
+
const sheet = (block.children || []).find((c) => c && c.type === 'StyleSheet');
|
|
665
|
+
if (sheet) sheets.push({ node: block, sheet });
|
|
666
|
+
}
|
|
667
|
+
let hash = null;
|
|
668
|
+
if (sheets.length > 0) {
|
|
669
|
+
hash = sheets[0].node.metadata?.styleScopeHash || sheets[0].sheet.hash || null;
|
|
670
|
+
const scopeItems = cloneAstNode(items);
|
|
671
|
+
// A fragment exposes root siblings without making the scope's container
|
|
672
|
+
// match ancestor selectors. Keep it on the private pruning tree.
|
|
673
|
+
const elements = collectPrunableElements(scopeItems, [], [createScopeRoot(scopeItems)]);
|
|
674
|
+
const prepared = sheets.map(({ node, sheet }) => {
|
|
675
|
+
const regionHash = node.metadata?.styleScopeHash || sheet.hash;
|
|
676
|
+
const clone = cloneAstNode(sheet);
|
|
677
|
+
clone.hash = hash;
|
|
678
|
+
// `analyzeCss` marks `:global(...)` selectors; pruning then marks the
|
|
679
|
+
// selectors that reach an element as scoped and used, exactly like the
|
|
680
|
+
// core transform — an unmatched rule renders as an `(unused)` comment.
|
|
681
|
+
analyzeCss(clone);
|
|
682
|
+
const styleClasses = new Map();
|
|
683
|
+
const topScopedClasses = new Map();
|
|
684
|
+
for (const element of elements) {
|
|
685
|
+
pruneCss(clone, element, styleClasses, topScopedClasses, regionHash);
|
|
686
|
+
}
|
|
687
|
+
return clone;
|
|
688
|
+
});
|
|
689
|
+
ctx.cssInjections.push({
|
|
690
|
+
hash,
|
|
691
|
+
css: renderStylesheets(prepared),
|
|
692
|
+
order: nextOrder(state),
|
|
693
|
+
// The authored `<style>` element(s) this stylesheet came from, so the
|
|
694
|
+
// emitted `injectStyle` maps back to them.
|
|
695
|
+
origins: sheets.map(({ node }) => node),
|
|
696
|
+
});
|
|
697
|
+
ctx.runtimeNeeded.add('injectStyle');
|
|
698
|
+
if (state.firstHash === null) state.firstHash = hash;
|
|
699
|
+
}
|
|
700
|
+
/** @type {Array<string | any>} */
|
|
701
|
+
const applied = [];
|
|
702
|
+
for (const block of own) {
|
|
703
|
+
for (const part of resolveAppliedParts(block, state)) {
|
|
704
|
+
if (typeof part !== 'string' || !applied.includes(part)) applied.push(part);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
for (const part of applied) {
|
|
708
|
+
if (typeof part !== 'string') recordRuntimeApplied(part.object, state);
|
|
709
|
+
}
|
|
710
|
+
return { hash, applied };
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* The class parts a block's `apply` contributes: one literal per statically
|
|
715
|
+
* known hash of a same-module theme, otherwise a runtime `<target>.$class`
|
|
716
|
+
* read.
|
|
717
|
+
*
|
|
718
|
+
* @param {any} block
|
|
719
|
+
* @param {PassState} state
|
|
720
|
+
* @returns {Array<string | any>}
|
|
721
|
+
*/
|
|
722
|
+
function resolveAppliedParts(block, state) {
|
|
723
|
+
const cached = state.appliedParts.get(block);
|
|
724
|
+
if (cached) return cached;
|
|
725
|
+
/** @type {Array<string | any>} */
|
|
726
|
+
const parts = [];
|
|
727
|
+
for (const resolution of block.metadata?.styleApplies ?? []) {
|
|
728
|
+
const staticClass = resolution.target ? staticStyleClass(resolution.target, state) : null;
|
|
729
|
+
if (staticClass !== null) {
|
|
730
|
+
for (const hash of staticClass.split(' ')) {
|
|
731
|
+
if (hash && !parts.includes(hash)) parts.push(hash);
|
|
732
|
+
}
|
|
733
|
+
continue;
|
|
734
|
+
}
|
|
735
|
+
parts.push(
|
|
736
|
+
state.tools.inheritOriginLoc(
|
|
737
|
+
b.member(cloneAstNode(resolution.expression), b.id('$class')),
|
|
738
|
+
resolution.expression,
|
|
739
|
+
),
|
|
740
|
+
);
|
|
741
|
+
}
|
|
742
|
+
state.appliedParts.set(block, parts);
|
|
743
|
+
return parts;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
/**
|
|
747
|
+
* The `$class` of an assigned block when every applied theme in its chain is
|
|
748
|
+
* a same-module block: applied hashes first (each once), own hash last.
|
|
749
|
+
*
|
|
750
|
+
* @param {any} block
|
|
751
|
+
* @param {PassState} state
|
|
752
|
+
* @returns {string | null}
|
|
753
|
+
*/
|
|
754
|
+
function staticStyleClass(block, state) {
|
|
755
|
+
const cached = state.staticClasses.get(block);
|
|
756
|
+
if (cached !== undefined) return cached;
|
|
757
|
+
/** @type {string[]} */
|
|
758
|
+
const parts = [];
|
|
759
|
+
let result = '';
|
|
760
|
+
for (const resolution of block.metadata?.styleApplies ?? []) {
|
|
761
|
+
const applied = resolution.target ? staticStyleClass(resolution.target, state) : null;
|
|
762
|
+
if (applied === null) {
|
|
763
|
+
result = null;
|
|
764
|
+
break;
|
|
765
|
+
}
|
|
766
|
+
for (const hash of applied.split(' ')) {
|
|
767
|
+
if (hash && !parts.includes(hash)) parts.push(hash);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
if (result !== null) {
|
|
771
|
+
const sheet = (block.children || []).find((c) => c && c.type === 'StyleSheet');
|
|
772
|
+
const own = sheet ? block.metadata?.styleScopeHash || sheet.hash : null;
|
|
773
|
+
if (own && !parts.includes(own)) parts.push(own);
|
|
774
|
+
result = parts.join(' ');
|
|
775
|
+
}
|
|
776
|
+
state.staticClasses.set(block, result);
|
|
777
|
+
return result;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/**
|
|
781
|
+
* `const theme = <style>…</style>` (or a property value / default export):
|
|
782
|
+
* inject the sheet at the declaration position and hand back the class-map
|
|
783
|
+
* object. A body-less `<style apply={…} />` exposes `$class` only.
|
|
784
|
+
*
|
|
785
|
+
* @param {any} styleNode
|
|
786
|
+
* @param {PassState} state
|
|
787
|
+
* @returns {any}
|
|
788
|
+
*/
|
|
789
|
+
function lowerAssignedStyle(styleNode, state) {
|
|
790
|
+
const { ctx, tools } = state;
|
|
791
|
+
const applied = resolveAppliedParts(styleNode, state);
|
|
792
|
+
const sheet = (styleNode.children || []).find((c) => c && c.type === 'StyleSheet');
|
|
793
|
+
if (!sheet) {
|
|
794
|
+
// A body-less bundle has no sheet of its own, but on the server reading
|
|
795
|
+
// it must still inject the themes it applies: it is wrapped like any
|
|
796
|
+
// assigned block, with nothing to inject for itself.
|
|
797
|
+
return wrapServerStyleMap(
|
|
798
|
+
tools.inheritOriginLoc(buildStyleClassMap(new Map(), null, { applied }), styleNode),
|
|
799
|
+
null,
|
|
800
|
+
null,
|
|
801
|
+
styleNode,
|
|
802
|
+
state,
|
|
803
|
+
);
|
|
804
|
+
}
|
|
805
|
+
const hash = styleNode.metadata?.styleScopeHash || sheet.hash;
|
|
806
|
+
const clone = cloneAstNode(sheet);
|
|
807
|
+
clone.hash = hash;
|
|
808
|
+
// `analyzeCss` marks `:global(...)` selectors so the renderer leaves them
|
|
809
|
+
// unscoped; the render mode comes from the core analyzer's classification.
|
|
810
|
+
analyzeCss(clone);
|
|
811
|
+
prepareStylesheetForRender(
|
|
812
|
+
clone,
|
|
813
|
+
styleNode.metadata?.styleKind === 'theme' ? 'theme' : 'class-map',
|
|
814
|
+
);
|
|
815
|
+
const css = renderStylesheets([clone]);
|
|
816
|
+
ctx.cssInjections.push({
|
|
817
|
+
hash,
|
|
818
|
+
css,
|
|
819
|
+
order: nextOrder(state),
|
|
820
|
+
origin: styleNode,
|
|
821
|
+
});
|
|
822
|
+
ctx.runtimeNeeded.add('injectStyle');
|
|
823
|
+
// The class-map object is built loc-less by the core helper; it maps to the
|
|
824
|
+
// authored <style>.
|
|
825
|
+
return wrapServerStyleMap(
|
|
826
|
+
tools.inheritOriginLoc(createStyleClassMapFromStylesheet(clone, { applied }), styleNode),
|
|
827
|
+
hash,
|
|
828
|
+
css,
|
|
829
|
+
styleNode,
|
|
830
|
+
state,
|
|
831
|
+
);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
/**
|
|
835
|
+
* On the server a module's assigned blocks only inject inside the bodies of
|
|
836
|
+
* that module's own components; a theme read from ANOTHER module would never
|
|
837
|
+
* reach the request collector. Wrap the map so property access injects its
|
|
838
|
+
* CSS (after the CSS of the themes it applies) into the active render.
|
|
839
|
+
*
|
|
840
|
+
* Every applied block is a dependency, whether it lives in this module or is
|
|
841
|
+
* imported: the class list inlines a same-module theme's hashes as literals,
|
|
842
|
+
* which says nothing about its CSS, so the wrapper touches the applied maps
|
|
843
|
+
* first (each of them a wrapper touching its own, so a chain injects
|
|
844
|
+
* transitively, applied before applier, each sheet once). A body-less bundle
|
|
845
|
+
* has no sheet (`null` id and css) and only forwards the touch.
|
|
846
|
+
*
|
|
847
|
+
* @param {any} map
|
|
848
|
+
* @param {string | null} hash
|
|
849
|
+
* @param {string | null} css
|
|
850
|
+
* @param {any} styleNode the authored block, whose `styleApplies` name the dependencies
|
|
851
|
+
* @param {PassState} state
|
|
852
|
+
* @returns {any}
|
|
853
|
+
*/
|
|
854
|
+
function wrapServerStyleMap(map, hash, css, styleNode, state) {
|
|
855
|
+
const { ctx, tools } = state;
|
|
856
|
+
if (ctx.mode !== 'server') return map;
|
|
857
|
+
ctx.runtimeNeeded.add('styleMap');
|
|
858
|
+
/** @type {any[]} */
|
|
859
|
+
const dependencies = [];
|
|
860
|
+
/** @type {string[]} */
|
|
861
|
+
const seen = [];
|
|
862
|
+
for (const resolution of styleNode.metadata?.styleApplies ?? []) {
|
|
863
|
+
const key = expressionKey(resolution.expression);
|
|
864
|
+
if (seen.includes(key)) continue;
|
|
865
|
+
seen.push(key);
|
|
866
|
+
dependencies.push(
|
|
867
|
+
tools.inheritOriginLoc(cloneAstNode(resolution.expression), resolution.expression),
|
|
868
|
+
);
|
|
869
|
+
}
|
|
870
|
+
const literal = (value) =>
|
|
871
|
+
value === null ? b.literal(null, 'null') : b.literal(value, JSON.stringify(value));
|
|
872
|
+
return tools.inheritOriginLoc(
|
|
873
|
+
b.call(
|
|
874
|
+
'_$styleMap',
|
|
875
|
+
literal(hash),
|
|
876
|
+
literal(css),
|
|
877
|
+
map,
|
|
878
|
+
...(dependencies.length > 0 ? [b.array(dependencies)] : []),
|
|
879
|
+
),
|
|
880
|
+
map,
|
|
881
|
+
);
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/**
|
|
885
|
+
* @param {any} expression the theme expression read at runtime (`theme` of `theme.$class`)
|
|
886
|
+
* @param {PassState} state
|
|
887
|
+
*/
|
|
888
|
+
function recordRuntimeApplied(expression, state) {
|
|
889
|
+
const key = expressionKey(expression);
|
|
890
|
+
if (state.runtimeApplied.some((existing) => expressionKey(existing) === key)) return;
|
|
891
|
+
state.runtimeApplied.push(expression);
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/**
|
|
895
|
+
* @param {any} node
|
|
896
|
+
* @returns {string}
|
|
897
|
+
*/
|
|
898
|
+
function expressionKey(node) {
|
|
899
|
+
if (node?.type === 'Identifier') return node.name;
|
|
900
|
+
if (node?.type === 'MemberExpression' && !node.computed) {
|
|
901
|
+
return `${expressionKey(node.object)}.${expressionKey(node.property)}`;
|
|
902
|
+
}
|
|
903
|
+
return JSON.stringify(node, (key, value) => (SKIP_KEYS.has(key) ? undefined : value));
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// --- class stamping ----------------------------------------------------------
|
|
907
|
+
|
|
908
|
+
/**
|
|
909
|
+
* @param {PassState} state
|
|
910
|
+
* @returns {{ hashes: string[], applied: Array<string | any> }}
|
|
911
|
+
*/
|
|
912
|
+
function currentChain(state) {
|
|
913
|
+
/** @type {string[]} */
|
|
914
|
+
const hashes = [];
|
|
915
|
+
/** @type {Array<string | any>} */
|
|
916
|
+
const applied = [];
|
|
917
|
+
for (const entry of state.stack) {
|
|
918
|
+
if (entry.hash && !hashes.includes(entry.hash)) hashes.push(entry.hash);
|
|
919
|
+
for (const part of entry.applied) {
|
|
920
|
+
if (typeof part !== 'string' || (!applied.includes(part) && !hashes.includes(part))) {
|
|
921
|
+
applied.push(part);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
return { hashes, applied };
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
/**
|
|
929
|
+
* Fold a sequence of class parts (literals and expressions) into one value: a
|
|
930
|
+
* string literal when everything is static, otherwise one template literal.
|
|
931
|
+
*
|
|
932
|
+
* @param {Array<string | any>} sequence
|
|
933
|
+
* @returns {any}
|
|
934
|
+
*/
|
|
935
|
+
function buildClassValue(sequence) {
|
|
936
|
+
/** @type {any[]} */
|
|
937
|
+
const quasis = [];
|
|
938
|
+
/** @type {any[]} */
|
|
939
|
+
const expressions = [];
|
|
940
|
+
let text = '';
|
|
941
|
+
for (const part of sequence) {
|
|
942
|
+
if (typeof part === 'string') {
|
|
943
|
+
if (part) text = text ? `${text} ${part}` : part;
|
|
944
|
+
continue;
|
|
945
|
+
}
|
|
946
|
+
const between = expressions.length > 0;
|
|
947
|
+
quasis.push(b.quasi(text ? (between ? ` ${text} ` : `${text} `) : between ? ' ' : '', false));
|
|
948
|
+
expressions.push(part);
|
|
949
|
+
text = '';
|
|
950
|
+
}
|
|
951
|
+
if (expressions.length === 0) return b.literal(text, JSON.stringify(text));
|
|
952
|
+
quasis.push(b.quasi(text ? ` ${text}` : '', true));
|
|
953
|
+
return b.template(quasis, expressions);
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
/**
|
|
957
|
+
* Stamp the current scope chain on a host element, copy-on-write.
|
|
958
|
+
*
|
|
959
|
+
* @param {any} element
|
|
960
|
+
* @param {PassState} state
|
|
961
|
+
* @returns {any}
|
|
962
|
+
*/
|
|
963
|
+
function addScopeClasses(element, state) {
|
|
964
|
+
const { hashes, applied } = currentChain(state);
|
|
965
|
+
if (hashes.length === 0 && applied.length === 0) return element;
|
|
966
|
+
const { ctx, tools } = state;
|
|
967
|
+
const openingElement = element.openingElement;
|
|
968
|
+
const attrs = openingElement?.attributes || [];
|
|
969
|
+
const index = attrs.findIndex(
|
|
970
|
+
(attr) =>
|
|
971
|
+
attr?.type === 'JSXAttribute' &&
|
|
972
|
+
attr.name?.type === 'JSXIdentifier' &&
|
|
973
|
+
(attr.name.name === 'class' || attr.name.name === 'className'),
|
|
974
|
+
);
|
|
975
|
+
|
|
976
|
+
/** @type {any[]} */
|
|
977
|
+
const chain = [...hashes, ...applied];
|
|
978
|
+
let newAttrs;
|
|
979
|
+
if (index === -1) {
|
|
980
|
+
// A synthesized class attribute maps to the element's opening tag; it is
|
|
981
|
+
// marked NOT AUTHORED so inspection does not claim the tag as its origin.
|
|
982
|
+
const value = buildClassValue(chain);
|
|
983
|
+
newAttrs = [
|
|
984
|
+
...attrs,
|
|
985
|
+
tools.markSynthesizedAttr(
|
|
986
|
+
tools.inheritOriginLoc(
|
|
987
|
+
b.jsx_attribute(
|
|
988
|
+
b.jsx_id('class'),
|
|
989
|
+
value.type === 'Literal' ? value : b.jsx_expression_container(value),
|
|
990
|
+
),
|
|
991
|
+
openingElement,
|
|
992
|
+
),
|
|
993
|
+
),
|
|
994
|
+
];
|
|
995
|
+
} else {
|
|
996
|
+
const existing = attrs[index];
|
|
997
|
+
const value = existing.value;
|
|
998
|
+
let newAttr;
|
|
999
|
+
if (!value) {
|
|
1000
|
+
// The NAME is authored (`class`), the value is not.
|
|
1001
|
+
const built = buildClassValue(chain);
|
|
1002
|
+
newAttr = {
|
|
1003
|
+
...existing,
|
|
1004
|
+
value: tools.markSynthesized(
|
|
1005
|
+
tools.inheritOriginLoc(
|
|
1006
|
+
built.type === 'Literal' ? built : b.jsx_expression_container(built),
|
|
1007
|
+
existing,
|
|
1008
|
+
),
|
|
1009
|
+
),
|
|
1010
|
+
};
|
|
1011
|
+
} else if (value.type === 'Literal' && typeof value.value === 'string') {
|
|
1012
|
+
const built = buildClassValue([value.value, ...chain]);
|
|
1013
|
+
newAttr = {
|
|
1014
|
+
...existing,
|
|
1015
|
+
value:
|
|
1016
|
+
built.type === 'Literal'
|
|
1017
|
+
? { ...value, value: built.value, raw: built.raw }
|
|
1018
|
+
: tools.inheritOriginLoc(b.jsx_expression_container(built), value),
|
|
1019
|
+
};
|
|
1020
|
+
} else {
|
|
1021
|
+
const expression = value.type === 'JSXExpressionContainer' ? value.expression : value;
|
|
1022
|
+
let base;
|
|
1023
|
+
if (isResolvedStyleValue(expression, tools)) {
|
|
1024
|
+
// `class={style(expr)}` already resolves to the chain plus the value.
|
|
1025
|
+
return element;
|
|
1026
|
+
}
|
|
1027
|
+
if (expression.type === 'Literal' && typeof expression.value === 'string') {
|
|
1028
|
+
base = expression.value;
|
|
1029
|
+
} else {
|
|
1030
|
+
// Normalize a dynamic value (clsx arrays/objects, null) BEFORE the
|
|
1031
|
+
// chain is appended so the template-literal slot is a plain string.
|
|
1032
|
+
ctx.runtimeNeeded.add('normalizeClass');
|
|
1033
|
+
base = tools.inheritOriginLoc(b.call('_$normalizeClass', expression), expression);
|
|
1034
|
+
}
|
|
1035
|
+
newAttr = {
|
|
1036
|
+
...existing,
|
|
1037
|
+
value: tools.inheritOriginLoc(
|
|
1038
|
+
b.jsx_expression_container(buildClassValue([base, ...chain])),
|
|
1039
|
+
value,
|
|
1040
|
+
),
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
newAttrs = attrs.slice();
|
|
1044
|
+
newAttrs[index] = newAttr;
|
|
1045
|
+
}
|
|
1046
|
+
// The core helper mirrors the attribute list onto `element.attributes` in
|
|
1047
|
+
// every branch; keep that alias in sync on the rebuilt copy.
|
|
1048
|
+
const out = { ...element, openingElement: { ...openingElement, attributes: newAttrs } };
|
|
1049
|
+
if ('attributes' in element) out.attributes = newAttrs;
|
|
1050
|
+
return out;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
/**
|
|
1054
|
+
* Whether a class value IS a `style(expr)` — the whole value, through
|
|
1055
|
+
* parenthesized and TS wrapper expressions — and so already carries the chain.
|
|
1056
|
+
* A call that is only a part of the value was lowered to its value alone (see
|
|
1057
|
+
* walkStyleValue) and leaves the stamp to add the chain.
|
|
1058
|
+
*
|
|
1059
|
+
* @param {any} expression
|
|
1060
|
+
* @param {StyleScopeTools} tools
|
|
1061
|
+
* @returns {boolean}
|
|
1062
|
+
*/
|
|
1063
|
+
function isResolvedStyleValue(expression, tools) {
|
|
1064
|
+
let current = expression;
|
|
1065
|
+
while (
|
|
1066
|
+
current?.type === 'ParenthesizedExpression' ||
|
|
1067
|
+
current?.type === 'TSAsExpression' ||
|
|
1068
|
+
current?.type === 'TSSatisfiesExpression' ||
|
|
1069
|
+
current?.type === 'TSNonNullExpression' ||
|
|
1070
|
+
current?.type === 'TSTypeAssertion'
|
|
1071
|
+
) {
|
|
1072
|
+
current = current.expression;
|
|
1073
|
+
}
|
|
1074
|
+
return (
|
|
1075
|
+
current != null &&
|
|
1076
|
+
(tools.isStyleCall(current) || current.metadata?.tsrx_style_resolved === true)
|
|
1077
|
+
);
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
/**
|
|
1081
|
+
* `{style(expr)}` → the scope chain plus the value: literals fold, dynamic
|
|
1082
|
+
* values concatenate at runtime (`'hash ' + expr`, so an absent value still
|
|
1083
|
+
* yields the chain).
|
|
1084
|
+
*
|
|
1085
|
+
* @param {any} node
|
|
1086
|
+
* @param {PassState} state
|
|
1087
|
+
* @returns {any}
|
|
1088
|
+
*/
|
|
1089
|
+
function resolveStyleCall(node, state) {
|
|
1090
|
+
const { hashes, applied } = currentChain(state);
|
|
1091
|
+
const inner = walk(node.arguments[0], state, 'expression');
|
|
1092
|
+
if (hashes.length === 0 && applied.length === 0) return inner;
|
|
1093
|
+
const chain = [...hashes, ...applied];
|
|
1094
|
+
if (inner.type === 'Literal' && typeof inner.value === 'string') {
|
|
1095
|
+
const value = buildClassValue([...chain, inner.value]);
|
|
1096
|
+
return markResolvedStyle(
|
|
1097
|
+
state.tools.inheritOriginLoc(
|
|
1098
|
+
value.type === 'Literal' ? b.literal(value.value, value.raw) : value,
|
|
1099
|
+
node,
|
|
1100
|
+
),
|
|
1101
|
+
);
|
|
1102
|
+
}
|
|
1103
|
+
// `(<chain> + ' ' + (expr))`: the chain must always be present.
|
|
1104
|
+
let prefix = null;
|
|
1105
|
+
let text = '';
|
|
1106
|
+
for (const part of chain) {
|
|
1107
|
+
if (typeof part === 'string') {
|
|
1108
|
+
text = text ? `${text} ${part}` : part;
|
|
1109
|
+
continue;
|
|
1110
|
+
}
|
|
1111
|
+
const literal = b.literal(text ? `${text} ` : ' ', JSON.stringify(text ? `${text} ` : ' '));
|
|
1112
|
+
prefix = prefix
|
|
1113
|
+
? b.binary('+', b.binary('+', prefix, literal), part)
|
|
1114
|
+
: b.binary('+', literal, part);
|
|
1115
|
+
text = '';
|
|
1116
|
+
}
|
|
1117
|
+
const tail = b.literal(text ? ` ${text} ` : ' ', JSON.stringify(text ? ` ${text} ` : ' '));
|
|
1118
|
+
const expression = prefix
|
|
1119
|
+
? b.binary('+', b.binary('+', prefix, tail), inner)
|
|
1120
|
+
: b.binary('+', b.literal(`${text} `, JSON.stringify(`${text} `)), inner);
|
|
1121
|
+
return markResolvedStyle(state.tools.inheritOriginLoc(expression, node));
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
/**
|
|
1125
|
+
* A class value the pre-pass built from `style(expr)` already carries the
|
|
1126
|
+
* chain; the stamp must not append it again. Compiler-built nodes only.
|
|
1127
|
+
*
|
|
1128
|
+
* @param {any} node
|
|
1129
|
+
* @returns {any}
|
|
1130
|
+
*/
|
|
1131
|
+
function markResolvedStyle(node) {
|
|
1132
|
+
node.metadata = { ...(node.metadata ?? {}), tsrx_style_resolved: true };
|
|
1133
|
+
return node;
|
|
1134
|
+
}
|