@tsrx/core 0.1.65 → 0.1.67
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/README.md +26 -1
- package/package.json +9 -5
- package/src/analyze/css-analyze.js +44 -6
- package/src/analyze/index.js +14 -1
- package/src/analyze/prune.js +36 -9
- package/src/analyze/style-analyze.js +467 -0
- package/src/analyze/validation.js +57 -0
- package/src/diagnostics.js +22 -0
- package/src/index.js +18 -0
- package/src/parse/style.js +97 -7
- package/src/plugin.js +145 -47
- package/src/scope.js +1 -1
- package/src/transform/jsx/index.js +94 -417
- package/src/transform/jsx/style-scopes.js +866 -0
- package/src/transform/scoping.js +129 -79
- package/src/transform/segments.js +16 -4
- package/src/transform/style-ref.js +74 -13
- package/src/transform/stylesheet.js +2 -1
- package/src/utils/is-reference.js +59 -0
- package/tests/fixtures/scoped-styles/README.md +71 -0
- package/tests/fixtures/scoped-styles/apply-forms.expected.json +18 -0
- package/tests/fixtures/scoped-styles/apply-forms.tsrx +69 -0
- package/tests/fixtures/scoped-styles/assigned-positions.expected.json +29 -0
- package/tests/fixtures/scoped-styles/assigned-positions.tsrx +90 -0
- package/tests/fixtures/scoped-styles/class-opt-in.expected.json +13 -0
- package/tests/fixtures/scoped-styles/class-opt-in.tsrx +39 -0
- package/tests/fixtures/scoped-styles/control-flow-else-if.expected.json +11 -0
- package/tests/fixtures/scoped-styles/control-flow-else-if.tsrx +26 -0
- package/tests/fixtures/scoped-styles/control-flow.expected.json +17 -0
- package/tests/fixtures/scoped-styles/control-flow.tsrx +102 -0
- package/tests/fixtures/scoped-styles/cross-module-apply.expected.json +14 -0
- package/tests/fixtures/scoped-styles/cross-module-apply.tsrx +48 -0
- package/tests/fixtures/scoped-styles/element-rooted-templates.expected.json +12 -0
- package/tests/fixtures/scoped-styles/element-rooted-templates.tsrx +33 -0
- package/tests/fixtures/scoped-styles/precedence.expected.json +11 -0
- package/tests/fixtures/scoped-styles/precedence.tsrx +45 -0
- package/tests/fixtures/scoped-styles/rfc-opening-example/panel.expected.json +12 -0
- package/tests/fixtures/scoped-styles/rfc-opening-example/panel.tsrx +46 -0
- package/tests/fixtures/scoped-styles/rfc-opening-example/theme.expected.json +9 -0
- package/tests/fixtures/scoped-styles/rfc-opening-example/theme.tsrx +24 -0
- package/tests/fixtures/scoped-styles/search-panel.expected.json +11 -0
- package/tests/fixtures/scoped-styles/search-panel.tsrx +48 -0
- package/tests/fixtures/scoped-styles/sibling-combinators.expected.json +16 -0
- package/tests/fixtures/scoped-styles/sibling-combinators.tsrx +42 -0
- package/tests/fixtures/scoped-styles/sibling-scope.expected.json +11 -0
- package/tests/fixtures/scoped-styles/sibling-scope.tsrx +44 -0
- package/tests/fixtures/scoped-styles/sibling-scopes.expected.json +12 -0
- package/tests/fixtures/scoped-styles/sibling-scopes.tsrx +51 -0
- package/tests/fixtures/scoped-styles/theme-composition.expected.json +14 -0
- package/tests/fixtures/scoped-styles/theme-composition.tsrx +45 -0
- package/tests/fixtures/scoped-styles/theme-diamond.expected.json +10 -0
- package/tests/fixtures/scoped-styles/theme-diamond.tsrx +18 -0
- package/tests/shared/scoped-styles-fixtures.js +67 -0
- package/tests/utils/fixtures/style-syntax.js +519 -0
- package/types/index.d.ts +85 -0
|
@@ -0,0 +1,866 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Style scope pre-pass: the one place that decides which `<style>` blocks
|
|
3
|
+
* belong to which scope, in what order their CSS is emitted, and which
|
|
4
|
+
* classes every element carries.
|
|
5
|
+
*
|
|
6
|
+
* A scope is the children list of a native element or fragment that holds at
|
|
7
|
+
* least one standalone block. A block styles the other children of its list
|
|
8
|
+
* and everything below them; it never styles the element that contains it,
|
|
9
|
+
* nor any ancestor. Every block of a scope shares the scope hash (the first
|
|
10
|
+
* bodied block's position-derived hash), and every element carries the hashes
|
|
11
|
+
* of all its enclosing scopes, outer first, followed by the classes of every
|
|
12
|
+
* applied theme. Children lists nested in a scope that hold blocks are nested
|
|
13
|
+
* scopes. Statement lists — `@{ … }` bodies and control-flow bodies — hold no
|
|
14
|
+
* standalone blocks (a block there is an output node, and the analyzer reports
|
|
15
|
+
* it); they are only searched for nested templates and assigned blocks.
|
|
16
|
+
*
|
|
17
|
+
* CSS is emitted in lexical pre-order: a scope's sheets form one contiguous
|
|
18
|
+
* group placed where its first block sits, before the sheets of the scopes
|
|
19
|
+
* and assigned blocks nested in it, after the assigned blocks declared before
|
|
20
|
+
* it. Running once, before the target walker, makes this order an invariant
|
|
21
|
+
* rather than a property of the walker's traversal.
|
|
22
|
+
*
|
|
23
|
+
* The pass is copy-on-write over the parsed AST: nodes it does not change are
|
|
24
|
+
* returned as-is, and node metadata is shared with the parser nodes so later
|
|
25
|
+
* passes (and the editor mappings) see the same annotations.
|
|
26
|
+
*
|
|
27
|
+
* @import * as AST from 'estree'
|
|
28
|
+
* @import * as ESTreeJSX from 'estree-jsx'
|
|
29
|
+
* @import { CompileError, JsxTransformContext as TransformContext } from '../../../types/index'
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { analyze_css } from '../../analyze/css-analyze.js';
|
|
33
|
+
import { prune_css } from '../../analyze/prune.js';
|
|
34
|
+
import { DIAGNOSTIC_CODES } from '../../diagnostics.js';
|
|
35
|
+
import { error } from '../../errors.js';
|
|
36
|
+
import * as b from '../../utils/builders.js';
|
|
37
|
+
import {
|
|
38
|
+
child_nodes,
|
|
39
|
+
has_location,
|
|
40
|
+
is_ast_node,
|
|
41
|
+
is_function_node,
|
|
42
|
+
is_style_element,
|
|
43
|
+
is_template_directive,
|
|
44
|
+
node_children,
|
|
45
|
+
} from '../../utils/ast.js';
|
|
46
|
+
import {
|
|
47
|
+
add_scope_classes,
|
|
48
|
+
is_composite_jsx_element,
|
|
49
|
+
prepare_stylesheet_for_render,
|
|
50
|
+
} from '../scoping.js';
|
|
51
|
+
import {
|
|
52
|
+
collect_style_ref_attributes,
|
|
53
|
+
create_style_class_map,
|
|
54
|
+
create_style_ref_setup_statements,
|
|
55
|
+
get_style_element_stylesheet,
|
|
56
|
+
} from '../style-ref.js';
|
|
57
|
+
import { clone_ast_node, create_generated_identifier } from './ast-builders.js';
|
|
58
|
+
import { set_node_path_metadata } from './helpers.js';
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @typedef {'statement' | 'expression'} DescendMode
|
|
62
|
+
* @typedef {{
|
|
63
|
+
* ctx: TransformContext,
|
|
64
|
+
* class_attr_name: 'class' | 'className',
|
|
65
|
+
* static_classes: Map<AST.JSXStyleElement, string | null>,
|
|
66
|
+
* ref_sink: AST.Statement[],
|
|
67
|
+
* }} StyleScopeState `ref_sink` collects the `<style ref>` setup statements
|
|
68
|
+
* of children-list scopes until the nearest statement slot (the statement
|
|
69
|
+
* list holding the template, or an expression-position root) places them.
|
|
70
|
+
* @typedef {{ hash: string | null, applied: Array<string | AST.Expression>, ref_statements: AST.Statement[] }} ScopeStyles
|
|
71
|
+
*/
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Run the pre-pass over a program. Returns the rewritten program (the same
|
|
75
|
+
* object when nothing changed) and fills `ctx.stylesheets` in emission order.
|
|
76
|
+
*
|
|
77
|
+
* @param {AST.Program} program
|
|
78
|
+
* @param {TransformContext} ctx
|
|
79
|
+
* @returns {AST.Program}
|
|
80
|
+
*/
|
|
81
|
+
export function prepare_style_scopes(program, ctx) {
|
|
82
|
+
/** @type {StyleScopeState} */
|
|
83
|
+
const state = {
|
|
84
|
+
ctx,
|
|
85
|
+
class_attr_name:
|
|
86
|
+
ctx.platform.jsx.classAttrName ?? (ctx.platform.jsx.rewriteClassAttr ? 'className' : 'class'),
|
|
87
|
+
static_classes: new Map(),
|
|
88
|
+
ref_sink: [],
|
|
89
|
+
};
|
|
90
|
+
// Module scope is not a template scope: a standalone block here is an
|
|
91
|
+
// analyzer error, and its statements are only searched for scopes.
|
|
92
|
+
const body = map_list(program.body, (statement) => descend(statement, state, 'statement'));
|
|
93
|
+
return body === program.body ? program : { ...program, body };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* @template {AST.Node} T
|
|
98
|
+
* @param {T[]} list
|
|
99
|
+
* @param {(item: T) => T} map
|
|
100
|
+
* @returns {T[]}
|
|
101
|
+
*/
|
|
102
|
+
function map_list(list, map) {
|
|
103
|
+
let out = list;
|
|
104
|
+
for (let i = 0; i < list.length; i += 1) {
|
|
105
|
+
const item = list[i];
|
|
106
|
+
const next = is_ast_node(item) ? map(item) : item;
|
|
107
|
+
if (next !== item) {
|
|
108
|
+
if (out === list) out = list.slice();
|
|
109
|
+
out[i] = next;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* @param {AST.Node | null | undefined} node
|
|
117
|
+
* @returns {node is AST.TSRXJSXElement | AST.TSRXJSXFragment}
|
|
118
|
+
*/
|
|
119
|
+
function is_native_template_node(node) {
|
|
120
|
+
return (
|
|
121
|
+
(node?.type === 'JSXElement' || node?.type === 'JSXFragment') && !!node.metadata?.native_tsrx
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* A statement-list item that renders a template and may host nested scopes.
|
|
127
|
+
* Setup statements do not.
|
|
128
|
+
*
|
|
129
|
+
* @param {AST.Node} node
|
|
130
|
+
* @returns {boolean}
|
|
131
|
+
*/
|
|
132
|
+
function is_render_item(node) {
|
|
133
|
+
return (
|
|
134
|
+
is_native_template_node(node) || node.type === 'JSXCodeBlock' || is_template_directive(node)
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Copy-on-write search for nested scopes and assigned blocks below a node
|
|
140
|
+
* that is not itself template content of the current scope.
|
|
141
|
+
*
|
|
142
|
+
* @template {AST.Node} T
|
|
143
|
+
* @param {T} node
|
|
144
|
+
* @param {StyleScopeState} state
|
|
145
|
+
* @param {DescendMode} mode whether `node` sits in a statement slot or holds a value
|
|
146
|
+
* @returns {T}
|
|
147
|
+
*/
|
|
148
|
+
function descend(node, state, mode) {
|
|
149
|
+
if (!is_ast_node(node)) return node;
|
|
150
|
+
|
|
151
|
+
if (is_style_element(node)) {
|
|
152
|
+
// A block holding a value is an assigned block. A block in a statement
|
|
153
|
+
// slot outside any template scope was reported by the analyzer; it is
|
|
154
|
+
// left alone so editor output stays analyzable.
|
|
155
|
+
if (mode === 'expression') prepare_assigned_style(node, state);
|
|
156
|
+
return node;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (node.type === 'JSXCodeBlock') {
|
|
160
|
+
return /** @type {T} */ (process_code_block(node, state));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (is_template_directive(node)) {
|
|
164
|
+
return /** @type {T} */ (process_directive(node, state));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (is_native_template_node(node)) {
|
|
168
|
+
// A native template in expression or statement position roots a scope of
|
|
169
|
+
// its own (an assigned template, a returned fragment, a branch value).
|
|
170
|
+
return /** @type {T} */ (process_root(node, state));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (node.type === 'ExpressionStatement') {
|
|
174
|
+
const expression = descend(node.expression, state, 'statement');
|
|
175
|
+
return expression === node.expression ? node : { ...node, expression };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return rewrite_children(node, state, (child, key) =>
|
|
179
|
+
descend(child, state, is_statement_list_key(node, key) ? 'statement' : 'expression'),
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* @param {AST.Node} node
|
|
185
|
+
* @param {string} key
|
|
186
|
+
* @returns {boolean}
|
|
187
|
+
*/
|
|
188
|
+
function is_statement_list_key(node, key) {
|
|
189
|
+
if (key === 'body') return node.type === 'BlockStatement' || node.type === 'Program';
|
|
190
|
+
if (key === 'consequent') return node.type === 'SwitchCase';
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Copy-on-write map over a node's child properties.
|
|
196
|
+
*
|
|
197
|
+
* @template {AST.Node} T
|
|
198
|
+
* @param {T} node
|
|
199
|
+
* @param {StyleScopeState} state
|
|
200
|
+
* @param {(child: AST.Node, key: string) => AST.Node} map
|
|
201
|
+
* @returns {T}
|
|
202
|
+
*/
|
|
203
|
+
function rewrite_children(node, state, map) {
|
|
204
|
+
/** @type {Record<string, unknown>} */
|
|
205
|
+
const source = /** @type {Record<string, unknown>} */ (node);
|
|
206
|
+
let out = source;
|
|
207
|
+
for (const key of Object.keys(source)) {
|
|
208
|
+
if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata' || key === 'css') {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
const value = source[key];
|
|
212
|
+
if (Array.isArray(value)) {
|
|
213
|
+
const next = map_list(/** @type {AST.Node[]} */ (value), (child) => map(child, key));
|
|
214
|
+
if (next !== value) {
|
|
215
|
+
if (out === source) out = { ...source };
|
|
216
|
+
out[key] = next;
|
|
217
|
+
}
|
|
218
|
+
} else if (is_ast_node(value)) {
|
|
219
|
+
const next = map(value, key);
|
|
220
|
+
if (next !== value) {
|
|
221
|
+
if (out === source) out = { ...source };
|
|
222
|
+
out[key] = next;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return /** @type {T} */ (out);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* A native element or fragment that already sits inside a scope. Its own
|
|
231
|
+
* children list is a scope when it holds standalone blocks: the blocks style
|
|
232
|
+
* the other children and their descendants — never the node itself — and are
|
|
233
|
+
* stripped (type-only output keeps emptied stand-ins so the editor can map
|
|
234
|
+
* the tags). Expression containers and attribute values inside are roots of
|
|
235
|
+
* their own.
|
|
236
|
+
*
|
|
237
|
+
* @template {AST.TSRXJSXElement | AST.TSRXJSXFragment} T
|
|
238
|
+
* @param {T} node
|
|
239
|
+
* @param {StyleScopeState} state
|
|
240
|
+
* @returns {T}
|
|
241
|
+
*/
|
|
242
|
+
function descend_template_children(node, state) {
|
|
243
|
+
const children = node_children(node);
|
|
244
|
+
const own = collect_own_blocks(children);
|
|
245
|
+
const scope = own.length > 0 ? prepare_scope(own, children, own[0], state) : null;
|
|
246
|
+
if (scope && scope.ref_statements.length > 0) {
|
|
247
|
+
state.ref_sink.push(...scope.ref_statements);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
let out = rewrite_children(node, state, (child, key) =>
|
|
251
|
+
key === 'children' ? child : descend(child, state, 'expression'),
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
/** @type {AST.Node[]} */
|
|
255
|
+
const next_children = [];
|
|
256
|
+
let changed = false;
|
|
257
|
+
for (const child of /** @type {AST.Node[]} */ (node.children)) {
|
|
258
|
+
if (!is_ast_node(child)) {
|
|
259
|
+
next_children.push(child);
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (scope && is_style_element(child) && own.includes(child)) {
|
|
263
|
+
changed = true;
|
|
264
|
+
if (state.ctx.typeOnly) next_children.push(type_only_style(child));
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
const stamped = scope ? stamp(child, scope, state) : child;
|
|
268
|
+
const next = is_native_template_node(stamped)
|
|
269
|
+
? descend_template_children(stamped, state)
|
|
270
|
+
: is_style_element(stamped)
|
|
271
|
+
? stamped
|
|
272
|
+
: descend(stamped, state, 'statement');
|
|
273
|
+
if (next !== child) changed = true;
|
|
274
|
+
next_children.push(next);
|
|
275
|
+
}
|
|
276
|
+
if (changed) out = { ...out, children: next_children };
|
|
277
|
+
return out;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* `@{ … }`: the setup statements are searched for nested templates and
|
|
282
|
+
* assigned blocks; the render slot holds the template. `ref` maps of the
|
|
283
|
+
* scopes inside the template are appended to the body, after the setup that
|
|
284
|
+
* declares their targets.
|
|
285
|
+
*
|
|
286
|
+
* @param {AST.JSXCodeBlock} node
|
|
287
|
+
* @param {StyleScopeState} state
|
|
288
|
+
* @returns {AST.JSXCodeBlock}
|
|
289
|
+
*/
|
|
290
|
+
function process_code_block(node, state) {
|
|
291
|
+
const body = map_list(node.body, (item) => descend(item, state, 'statement'));
|
|
292
|
+
if (!node.render) return body === node.body ? node : { ...node, body };
|
|
293
|
+
const { statements, result: render } = with_ref_sink(state, () =>
|
|
294
|
+
descend_list_item(/** @type {AST.Node} */ (node.render), state),
|
|
295
|
+
);
|
|
296
|
+
if (body === node.body && render === node.render && statements.length === 0) return node;
|
|
297
|
+
return {
|
|
298
|
+
...node,
|
|
299
|
+
body: /** @type {AST.Statement[]} */ ([...body, ...statements]),
|
|
300
|
+
render,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Each branch body of a directive is a statement list holding one output node.
|
|
306
|
+
*
|
|
307
|
+
* @param {AST.JSXTemplateDirective} node
|
|
308
|
+
* @param {StyleScopeState} state
|
|
309
|
+
* @returns {AST.JSXTemplateDirective}
|
|
310
|
+
*/
|
|
311
|
+
function process_directive(node, state) {
|
|
312
|
+
return rewrite_children(node, state, (child, key) => {
|
|
313
|
+
if (child.type === 'BlockStatement') return process_block_body(child, state);
|
|
314
|
+
if (child.type === 'CatchClause') {
|
|
315
|
+
const body = process_block_body(child.body, state);
|
|
316
|
+
return body === child.body ? child : { ...child, body };
|
|
317
|
+
}
|
|
318
|
+
if (child.type === 'SwitchCase') {
|
|
319
|
+
const consequent = process_statements(child.consequent, state);
|
|
320
|
+
return consequent === child.consequent
|
|
321
|
+
? child
|
|
322
|
+
: { ...child, consequent: /** @type {AST.Statement[]} */ (consequent) };
|
|
323
|
+
}
|
|
324
|
+
// `@else if (…) { … }` parses as a plain `IfStatement` alternate; its
|
|
325
|
+
// bodies are branch bodies like the directive's own.
|
|
326
|
+
if (key === 'alternate' && (is_template_directive(child) || child.type === 'IfStatement')) {
|
|
327
|
+
return process_directive(/** @type {AST.JSXTemplateDirective} */ (child), state);
|
|
328
|
+
}
|
|
329
|
+
return descend(child, state, 'expression');
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* @param {AST.BlockStatement} block
|
|
335
|
+
* @param {StyleScopeState} state
|
|
336
|
+
* @returns {AST.BlockStatement}
|
|
337
|
+
*/
|
|
338
|
+
function process_block_body(block, state) {
|
|
339
|
+
const body = process_statements(block.body, state);
|
|
340
|
+
return body === block.body ? block : { ...block, body: /** @type {AST.Statement[]} */ (body) };
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* A native element/fragment in expression or statement position: its own
|
|
345
|
+
* children list is treated like any other, and the `ref` maps of the scopes
|
|
346
|
+
* inside it are left on its metadata because it has no statement slot of its
|
|
347
|
+
* own — the lowering that turns the root into statements picks them up.
|
|
348
|
+
*
|
|
349
|
+
* @template {AST.TSRXJSXElement | AST.TSRXJSXFragment} T
|
|
350
|
+
* @param {T} node
|
|
351
|
+
* @param {StyleScopeState} state
|
|
352
|
+
* @returns {T}
|
|
353
|
+
*/
|
|
354
|
+
function process_root(node, state) {
|
|
355
|
+
const { statements, result: out } = with_ref_sink(state, () =>
|
|
356
|
+
descend_template_children(node, state),
|
|
357
|
+
);
|
|
358
|
+
if (statements.length > 0) {
|
|
359
|
+
const previous = out.metadata.tsrx_style_ref_statements ?? [];
|
|
360
|
+
out.metadata.tsrx_style_ref_statements = [...previous, ...statements];
|
|
361
|
+
}
|
|
362
|
+
return out;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Run `fn` with a fresh `ref` sink and return what it collected.
|
|
367
|
+
*
|
|
368
|
+
* @template T
|
|
369
|
+
* @param {StyleScopeState} state
|
|
370
|
+
* @param {() => T} fn
|
|
371
|
+
* @returns {{ statements: AST.Statement[], result: T }}
|
|
372
|
+
*/
|
|
373
|
+
function with_ref_sink(state, fn) {
|
|
374
|
+
const previous = state.ref_sink;
|
|
375
|
+
/** @type {AST.Statement[]} */
|
|
376
|
+
const statements = [];
|
|
377
|
+
state.ref_sink = statements;
|
|
378
|
+
try {
|
|
379
|
+
return { statements, result: fn() };
|
|
380
|
+
} finally {
|
|
381
|
+
state.ref_sink = previous;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* A statement list holding setup statements and at most one output node:
|
|
387
|
+
* the `ref` maps of the scopes inside the output node surface in the slot
|
|
388
|
+
* before it.
|
|
389
|
+
*
|
|
390
|
+
* @param {AST.Node[]} nodes
|
|
391
|
+
* @param {StyleScopeState} state
|
|
392
|
+
* @returns {AST.Node[]}
|
|
393
|
+
*/
|
|
394
|
+
function process_statements(nodes, state) {
|
|
395
|
+
/** @type {AST.Node[][]} */
|
|
396
|
+
const out = nodes.map((item) => {
|
|
397
|
+
if (!is_render_item(item)) return [descend(item, state, 'statement')];
|
|
398
|
+
const { statements, result } = with_ref_sink(state, () => descend_list_item(item, state));
|
|
399
|
+
return [...statements, result];
|
|
400
|
+
});
|
|
401
|
+
const changed = out.some((parts, i) => parts.length !== 1 || parts[0] !== nodes[i]);
|
|
402
|
+
return changed ? out.flat() : nodes;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* @param {AST.Node} item
|
|
407
|
+
* @param {StyleScopeState} state
|
|
408
|
+
* @returns {AST.Node}
|
|
409
|
+
*/
|
|
410
|
+
function descend_list_item(item, state) {
|
|
411
|
+
if (is_native_template_node(item)) return descend_template_children(item, state);
|
|
412
|
+
if (is_style_element(item)) return item;
|
|
413
|
+
return descend(item, state, 'statement');
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* The standalone blocks a children list owns: its own style items, in source
|
|
418
|
+
* order. Blocks inside the items' subtrees belong to the lists they sit in.
|
|
419
|
+
*
|
|
420
|
+
* @param {AST.Node[]} nodes
|
|
421
|
+
* @returns {AST.JSXStyleElement[]}
|
|
422
|
+
*/
|
|
423
|
+
export function collect_own_blocks(nodes) {
|
|
424
|
+
return nodes
|
|
425
|
+
.filter(is_style_element)
|
|
426
|
+
.sort((a, b) => /** @type {number} */ (a.start) - /** @type {number} */ (b.start));
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Render a scope's sheets and compute what its elements carry.
|
|
431
|
+
*
|
|
432
|
+
* @param {AST.JSXStyleElement[]} own
|
|
433
|
+
* @param {AST.Node[]} render_items the list's items (and, through them, their subtrees)
|
|
434
|
+
* @param {AST.Node} holder the node whose metadata accumulates the scope's class
|
|
435
|
+
* map: the scope's first block, so two scopes never share a map
|
|
436
|
+
* @param {StyleScopeState} state
|
|
437
|
+
* @returns {ScopeStyles}
|
|
438
|
+
*/
|
|
439
|
+
function prepare_scope(own, render_items, holder, state) {
|
|
440
|
+
const { ctx } = state;
|
|
441
|
+
/** @type {Array<[AST.JSXStyleElement, AST.CSS.StyleSheet]>} */
|
|
442
|
+
const sheets = [];
|
|
443
|
+
for (const block of own) {
|
|
444
|
+
const sheet = get_style_element_stylesheet(block);
|
|
445
|
+
if (sheet) sheets.push([block, sheet]);
|
|
446
|
+
}
|
|
447
|
+
const hash = sheets.length > 0 ? sheets[0][1].hash : null;
|
|
448
|
+
const refs = collect_style_ref_attributes(own);
|
|
449
|
+
const elements = collect_css_prunable_elements(render_items, [], ctx, [
|
|
450
|
+
create_scope_root(render_items),
|
|
451
|
+
]);
|
|
452
|
+
|
|
453
|
+
/** @type {AST.CSS.StyleSheet | null} */
|
|
454
|
+
let first_sheet = null;
|
|
455
|
+
for (const [block, sheet] of sheets) {
|
|
456
|
+
const region_hash = sheet.hash;
|
|
457
|
+
sheet.hash = /** @type {string} */ (hash);
|
|
458
|
+
if (!analyze_scope_css(block, sheet, state)) continue;
|
|
459
|
+
apply_css_definition_metadata(holder, sheet, elements, refs.length > 0, region_hash);
|
|
460
|
+
ctx.stylesheets.push(sheet);
|
|
461
|
+
first_sheet ??= sheet;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const applied = applies_of(own, state);
|
|
465
|
+
|
|
466
|
+
/** @type {AST.Statement[]} */
|
|
467
|
+
let ref_statements = [];
|
|
468
|
+
if (refs.length > 0) {
|
|
469
|
+
ref_statements = create_style_ref_setup_statements(
|
|
470
|
+
refs,
|
|
471
|
+
create_style_class_map(holder, first_sheet, { applied, hash }),
|
|
472
|
+
{
|
|
473
|
+
allowMutableRefTarget: ctx.platform.jsx.multiRefStrategy === 'array',
|
|
474
|
+
createTempIdentifier: () => create_generated_identifier(create_style_ref_temp_name(ctx)),
|
|
475
|
+
},
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return { hash, applied, ref_statements };
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* @param {AST.JSXStyleElement[]} blocks
|
|
484
|
+
* @param {StyleScopeState} state
|
|
485
|
+
* @returns {Array<string | AST.Expression>}
|
|
486
|
+
*/
|
|
487
|
+
function applies_of(blocks, state) {
|
|
488
|
+
return blocks.flatMap((block) => resolve_style_applies(block, state));
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* @param {TransformContext} ctx
|
|
493
|
+
* @returns {string}
|
|
494
|
+
*/
|
|
495
|
+
function create_style_ref_temp_name(ctx) {
|
|
496
|
+
ctx.local_statement_component_index += 1;
|
|
497
|
+
return `_tsrx_style_ref_${ctx.local_statement_component_index}`;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* `analyze_css` reports `:global` placement through a coded fatal error with
|
|
502
|
+
* CSS-relative positions; re-anchor it on the block so editors can place it,
|
|
503
|
+
* and keep going in collect mode.
|
|
504
|
+
*
|
|
505
|
+
* @param {AST.JSXStyleElement} block
|
|
506
|
+
* @param {AST.CSS.StyleSheet} sheet
|
|
507
|
+
* @param {StyleScopeState} state
|
|
508
|
+
* @returns {boolean} whether the sheet is usable
|
|
509
|
+
*/
|
|
510
|
+
function analyze_scope_css(block, sheet, state) {
|
|
511
|
+
try {
|
|
512
|
+
analyze_css(sheet);
|
|
513
|
+
return true;
|
|
514
|
+
} catch (thrown) {
|
|
515
|
+
const compile_error = /** @type {CompileError} */ (thrown);
|
|
516
|
+
if (compile_error?.code !== DIAGNOSTIC_CODES.CSS_GLOBAL_PLACEMENT) throw thrown;
|
|
517
|
+
error(
|
|
518
|
+
compile_error.message,
|
|
519
|
+
state.ctx.filename,
|
|
520
|
+
block,
|
|
521
|
+
state.ctx.collect ? state.ctx.errors : undefined,
|
|
522
|
+
state.ctx.comments,
|
|
523
|
+
compile_error.code,
|
|
524
|
+
);
|
|
525
|
+
return false;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Prune the sheet against the scope's elements and record the class map the
|
|
531
|
+
* scope exposes (through `ref`) on the holder's metadata.
|
|
532
|
+
*
|
|
533
|
+
* @param {AST.Node} holder
|
|
534
|
+
* @param {AST.CSS.StyleSheet} css
|
|
535
|
+
* @param {AST.TSRXJSXElement[]} elements
|
|
536
|
+
* @param {boolean} export_top_scoped_classes
|
|
537
|
+
* @param {string} region_hash
|
|
538
|
+
* @returns {void}
|
|
539
|
+
*/
|
|
540
|
+
export function apply_css_definition_metadata(
|
|
541
|
+
holder,
|
|
542
|
+
css,
|
|
543
|
+
elements,
|
|
544
|
+
export_top_scoped_classes,
|
|
545
|
+
region_hash = css.hash,
|
|
546
|
+
) {
|
|
547
|
+
const metadata = holder.metadata || (holder.metadata = { path: [] });
|
|
548
|
+
const style_classes = metadata.styleClasses || (metadata.styleClasses = new Map());
|
|
549
|
+
const top_scoped_classes = metadata.topScopedClasses || new Map();
|
|
550
|
+
|
|
551
|
+
const prune = () => {
|
|
552
|
+
for (const element of elements) {
|
|
553
|
+
prune_css(css, element, style_classes, top_scoped_classes, region_hash);
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
prune();
|
|
558
|
+
|
|
559
|
+
if (export_top_scoped_classes) {
|
|
560
|
+
for (const [class_name, class_info] of top_scoped_classes) {
|
|
561
|
+
style_classes.set(class_name, class_info.selector ?? class_info);
|
|
562
|
+
}
|
|
563
|
+
prune();
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if (top_scoped_classes.size > 0) {
|
|
567
|
+
metadata.topScopedClasses = top_scoped_classes;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* The root of a scope's ancestor paths: a fragment holding the list's items,
|
|
573
|
+
* so `prune_css` can find the siblings of a top-level item for `+` and `~`
|
|
574
|
+
* combinators. It is not an element, so ancestor combinators never match it —
|
|
575
|
+
* the container of a scope is exactly what a scoped block does not style.
|
|
576
|
+
*
|
|
577
|
+
* @param {AST.Node[]} items
|
|
578
|
+
* @returns {AST.Node}
|
|
579
|
+
*/
|
|
580
|
+
export function create_scope_root(items) {
|
|
581
|
+
return /** @type {AST.Node} */ (
|
|
582
|
+
/** @type {unknown} */ ({
|
|
583
|
+
type: 'JSXFragment',
|
|
584
|
+
openingFragment: { type: 'JSXOpeningFragment' },
|
|
585
|
+
closingFragment: { type: 'JSXClosingFragment' },
|
|
586
|
+
children: items,
|
|
587
|
+
metadata: { path: [] },
|
|
588
|
+
})
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Pruning runs before the walker stamps paths onto template nodes, so each
|
|
594
|
+
* collected element gets its ancestor chain (`metadata.path`) here —
|
|
595
|
+
* descendant/sibling selector matching in `prune_css` reads it. The scope's
|
|
596
|
+
* own elements and those of nested scopes are collected; a component
|
|
597
|
+
* boundary stops the walk. Callers that collect a scope's items themselves
|
|
598
|
+
* should seed `path` with {@link create_scope_root} for the same reason.
|
|
599
|
+
*
|
|
600
|
+
* @param {AST.Node | AST.Node[]} value
|
|
601
|
+
* @param {AST.TSRXJSXElement[]} [elements]
|
|
602
|
+
* @param {TransformContext | null} [transform_context]
|
|
603
|
+
* @param {AST.Node[]} [path]
|
|
604
|
+
* @returns {AST.TSRXJSXElement[]}
|
|
605
|
+
*/
|
|
606
|
+
export function collect_css_prunable_elements(
|
|
607
|
+
value,
|
|
608
|
+
elements = [],
|
|
609
|
+
transform_context = null,
|
|
610
|
+
path = [],
|
|
611
|
+
) {
|
|
612
|
+
if (Array.isArray(value)) {
|
|
613
|
+
for (const child of value) {
|
|
614
|
+
collect_css_prunable_elements(child, elements, transform_context, path);
|
|
615
|
+
}
|
|
616
|
+
return elements;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
if (is_function_node(value) && value.metadata?.tsrx_dynamic_wrapper !== true) {
|
|
620
|
+
return elements;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
if (is_style_host_element(value)) return elements;
|
|
624
|
+
|
|
625
|
+
if (value.type === 'JSXElement' && value.metadata?.native_tsrx) {
|
|
626
|
+
if (!is_style_element(value)) {
|
|
627
|
+
set_node_path_metadata(value, path);
|
|
628
|
+
elements.push(value);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
const child_path = [...path, value];
|
|
633
|
+
|
|
634
|
+
for (const child of child_nodes(value, 'css')) {
|
|
635
|
+
collect_css_prunable_elements(child, elements, transform_context, child_path);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
return elements;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* The class parts a block's `apply` contributes: a literal for a same-module
|
|
643
|
+
* theme whose class is statically known, otherwise a runtime `<target>.$class`
|
|
644
|
+
* read. Memoized on the block's metadata for the assigned-block lowering.
|
|
645
|
+
*
|
|
646
|
+
* @param {AST.JSXStyleElement} block
|
|
647
|
+
* @param {StyleScopeState} state
|
|
648
|
+
* @returns {Array<string | AST.Expression>}
|
|
649
|
+
*/
|
|
650
|
+
export function resolve_style_applies(block, state) {
|
|
651
|
+
if (block.metadata.tsrx_style_class_parts) return block.metadata.tsrx_style_class_parts;
|
|
652
|
+
/** @type {Array<string | AST.Expression>} */
|
|
653
|
+
const parts = [];
|
|
654
|
+
for (const resolution of block.metadata.styleApplies ?? []) {
|
|
655
|
+
const static_class = resolution.target ? static_style_class(resolution.target, state) : null;
|
|
656
|
+
if (static_class !== null) {
|
|
657
|
+
// One entry per hash so a diamond (`a` applied directly and through
|
|
658
|
+
// `b`) stamps each hash once (D12, static dedupe).
|
|
659
|
+
for (const hash of static_class.split(' ')) {
|
|
660
|
+
if (hash && !parts.includes(hash)) parts.push(hash);
|
|
661
|
+
}
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
parts.push(
|
|
665
|
+
b.member(clone_ast_node(resolution.expression, !state.ctx.typeOnly), b.id('$class')),
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
block.metadata.tsrx_style_class_parts = parts;
|
|
669
|
+
return parts;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* The `$class` value of an assigned block when every applied theme in its
|
|
674
|
+
* chain is a same-module block: applied classes first, own hash last (D6).
|
|
675
|
+
*
|
|
676
|
+
* @param {AST.JSXStyleElement} block
|
|
677
|
+
* @param {StyleScopeState} state
|
|
678
|
+
* @returns {string | null}
|
|
679
|
+
*/
|
|
680
|
+
export function static_style_class(block, state) {
|
|
681
|
+
const cached = state.static_classes.get(block);
|
|
682
|
+
if (cached !== undefined) return cached;
|
|
683
|
+
/** @type {string[]} */
|
|
684
|
+
const parts = [];
|
|
685
|
+
/** @type {string | null} */
|
|
686
|
+
let result = '';
|
|
687
|
+
for (const resolution of block.metadata.styleApplies ?? []) {
|
|
688
|
+
const applied = resolution.target ? static_style_class(resolution.target, state) : null;
|
|
689
|
+
if (applied === null) {
|
|
690
|
+
result = null;
|
|
691
|
+
break;
|
|
692
|
+
}
|
|
693
|
+
for (const hash of applied.split(' ')) {
|
|
694
|
+
if (hash && !parts.includes(hash)) parts.push(hash);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
if (result !== null) {
|
|
698
|
+
const sheet = get_style_element_stylesheet(block);
|
|
699
|
+
if (sheet && !parts.includes(sheet.hash)) parts.push(sheet.hash);
|
|
700
|
+
result = parts.join(' ');
|
|
701
|
+
}
|
|
702
|
+
state.static_classes.set(block, result);
|
|
703
|
+
return result;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* Render an assigned block's sheet at its declaration position so it lands
|
|
708
|
+
* in lexical order with the scopes around it; the walker's visitor builds the
|
|
709
|
+
* class map object later from the same sheet.
|
|
710
|
+
*
|
|
711
|
+
* @param {AST.JSXStyleElement} node
|
|
712
|
+
* @param {StyleScopeState} state
|
|
713
|
+
* @returns {void}
|
|
714
|
+
*/
|
|
715
|
+
function prepare_assigned_style(node, state) {
|
|
716
|
+
if (node.metadata.tsrx_style_prepared) return;
|
|
717
|
+
node.metadata.tsrx_style_prepared = true;
|
|
718
|
+
resolve_style_applies(node, state);
|
|
719
|
+
const sheet = get_style_element_stylesheet(node);
|
|
720
|
+
if (!sheet) return;
|
|
721
|
+
if (!analyze_scope_css(node, sheet, state)) return;
|
|
722
|
+
state.ctx.stylesheets.push(
|
|
723
|
+
prepare_stylesheet_for_render(
|
|
724
|
+
sheet,
|
|
725
|
+
node.metadata.styleKind === 'theme' ? 'theme' : 'class-map',
|
|
726
|
+
),
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* Stamp the scope's classes on every element in the subtree, through nested
|
|
732
|
+
* scopes (they append their own later) but not through function boundaries.
|
|
733
|
+
*
|
|
734
|
+
* @template {AST.Node} T
|
|
735
|
+
* @param {T} node
|
|
736
|
+
* @param {ScopeStyles} scope
|
|
737
|
+
* @param {StyleScopeState} state
|
|
738
|
+
* @returns {T}
|
|
739
|
+
*/
|
|
740
|
+
function stamp(node, scope, state) {
|
|
741
|
+
if (scope.hash === null && scope.applied.length === 0) return node;
|
|
742
|
+
return /** @type {T} */ (stamp_node(node, scope, state));
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/**
|
|
746
|
+
* @param {AST.Node} node
|
|
747
|
+
* @param {ScopeStyles} scope
|
|
748
|
+
* @param {StyleScopeState} state
|
|
749
|
+
* @returns {AST.Node}
|
|
750
|
+
*/
|
|
751
|
+
function stamp_node(node, scope, state) {
|
|
752
|
+
if (!is_ast_node(node)) return node;
|
|
753
|
+
if (is_function_node(node) && node.metadata?.tsrx_dynamic_wrapper !== true) return node;
|
|
754
|
+
if (is_style_element(node) || is_style_host_element(node)) return node;
|
|
755
|
+
|
|
756
|
+
// Composite components get no hash (their host elements belong to their own
|
|
757
|
+
// scope); parser-native dynamic tags (`<{expr}>`) render host elements.
|
|
758
|
+
/** @type {AST.Node} */
|
|
759
|
+
const out =
|
|
760
|
+
node.type === 'JSXElement' && (!is_composite_jsx_element(node) || node.metadata?.dynamicElement)
|
|
761
|
+
? add_scope_classes(
|
|
762
|
+
/** @type {AST.TSRXJSXElement} */ (node),
|
|
763
|
+
scope.hash ? [scope.hash] : [],
|
|
764
|
+
scope.applied,
|
|
765
|
+
state.class_attr_name,
|
|
766
|
+
)
|
|
767
|
+
: node;
|
|
768
|
+
return rewrite_children(out, state, (child) => stamp_node(child, scope, state));
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/**
|
|
772
|
+
* `<style>{css}</style>` is an ordinary host element (Rule C): it holds no
|
|
773
|
+
* scoped CSS, so it is neither stamped nor matched by selectors, like a
|
|
774
|
+
* `<style>` block.
|
|
775
|
+
*
|
|
776
|
+
* @param {AST.Node} node
|
|
777
|
+
* @returns {boolean}
|
|
778
|
+
*/
|
|
779
|
+
function is_style_host_element(node) {
|
|
780
|
+
return (
|
|
781
|
+
node.type === 'JSXElement' &&
|
|
782
|
+
!!node.metadata?.native_tsrx &&
|
|
783
|
+
node.openingElement.name.type === 'JSXIdentifier' &&
|
|
784
|
+
node.openingElement.name.name === 'style'
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* The synthesized `$class` read of a type-only `apply` target borrows the
|
|
790
|
+
* target's position for verification only, so a TypeScript error on it (the
|
|
791
|
+
* target is not a style object) lands on the authored identifier rather than
|
|
792
|
+
* on the closing brace, while hover and navigation on that identifier stay
|
|
793
|
+
* the target's own.
|
|
794
|
+
*
|
|
795
|
+
* @param {AST.Node} target
|
|
796
|
+
* @returns {AST.Identifier}
|
|
797
|
+
*/
|
|
798
|
+
function type_only_class_read(target) {
|
|
799
|
+
const id = b.id('$class', has_location(target) ? target : undefined);
|
|
800
|
+
if (has_location(target)) {
|
|
801
|
+
id.metadata = {
|
|
802
|
+
...id.metadata,
|
|
803
|
+
verify_only: true,
|
|
804
|
+
source_length: target.end - target.start,
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
return id;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* The type-only stand-in for a scoped block: no CSS body, and `apply`
|
|
812
|
+
* rewritten to a `data-` attribute reading `$class` of each target so
|
|
813
|
+
* TypeScript checks the target is a style object while the tag keeps its
|
|
814
|
+
* source position.
|
|
815
|
+
*
|
|
816
|
+
* @param {AST.JSXStyleElement} block
|
|
817
|
+
* @returns {AST.JSXStyleElement}
|
|
818
|
+
*/
|
|
819
|
+
export function type_only_style(block) {
|
|
820
|
+
const attributes = block.openingElement.attributes.map((attr) => {
|
|
821
|
+
if (
|
|
822
|
+
attr.type !== 'JSXAttribute' ||
|
|
823
|
+
attr.name.type !== 'JSXIdentifier' ||
|
|
824
|
+
attr.name.name !== 'apply' ||
|
|
825
|
+
attr.value?.type !== 'JSXExpressionContainer' ||
|
|
826
|
+
attr.value.expression.type === 'JSXEmptyExpression'
|
|
827
|
+
) {
|
|
828
|
+
return attr;
|
|
829
|
+
}
|
|
830
|
+
const expression = attr.value.expression;
|
|
831
|
+
const value =
|
|
832
|
+
expression.type === 'ArrayExpression'
|
|
833
|
+
? b.array(
|
|
834
|
+
expression.elements.map((element) =>
|
|
835
|
+
element && element.type !== 'SpreadElement'
|
|
836
|
+
? b.member(clone_ast_node(element), type_only_class_read(element))
|
|
837
|
+
: element,
|
|
838
|
+
),
|
|
839
|
+
)
|
|
840
|
+
: b.member(clone_ast_node(expression), type_only_class_read(expression));
|
|
841
|
+
// The renamed attribute is synthesized text: leave it unmapped so the
|
|
842
|
+
// authored `apply` token serves no misleading hover.
|
|
843
|
+
return {
|
|
844
|
+
...attr,
|
|
845
|
+
name: b.jsx_id('data-tsrx-apply'),
|
|
846
|
+
value: { ...attr.value, expression: value },
|
|
847
|
+
};
|
|
848
|
+
});
|
|
849
|
+
// An unclosed `<style>` has no closing element. The stand-in still has to
|
|
850
|
+
// be valid TSX or the virtual file loses every mapping after the tag.
|
|
851
|
+
// Self-closing apply tags already print as `<style … />` — leave those.
|
|
852
|
+
const closingElement =
|
|
853
|
+
block.closingElement ??
|
|
854
|
+
(block.openingElement.selfClosing
|
|
855
|
+
? null
|
|
856
|
+
: b.jsx_closing_element(
|
|
857
|
+
clone_ast_node(block.openingElement.name),
|
|
858
|
+
has_location(block.openingElement) ? block.openingElement : undefined,
|
|
859
|
+
));
|
|
860
|
+
return {
|
|
861
|
+
...block,
|
|
862
|
+
children: [],
|
|
863
|
+
openingElement: { ...block.openingElement, attributes },
|
|
864
|
+
closingElement,
|
|
865
|
+
};
|
|
866
|
+
}
|