@tsrx/core 0.1.65 → 0.1.66
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/style-analyze.js +467 -0
- package/src/analyze/validation.js +57 -0
- package/src/diagnostics.js +22 -0
- package/src/index.js +17 -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 +842 -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-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,519 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Table-driven parser spec for the RFC "sibling-scoped `<style>` blocks,
|
|
3
|
+
* `$class`, `apply`" syntax. It is replayed by `tests/utils/parser.test.js`
|
|
4
|
+
* against the acorn parser and doubles as the porting spec for the Rust parser
|
|
5
|
+
* (`oxc-tsrx`), so it is deliberately dependency-free: every case is plain
|
|
6
|
+
* data plus a tiny `locate(ast)` function that walks from the `Program` to the
|
|
7
|
+
* node whose shape is described.
|
|
8
|
+
*
|
|
9
|
+
* Shape vocabulary (all plain objects, matched structurally by the test):
|
|
10
|
+
*
|
|
11
|
+
* - `{ type: 'JSXStyleElement', selfClosing, attributes, apply?, children, css,
|
|
12
|
+
* hasScopeHash, closingElement }`
|
|
13
|
+
* - `attributes`: attribute names on `openingElement.attributes`, in order.
|
|
14
|
+
* - `apply`: the node type of the `apply={…}` expression, when present.
|
|
15
|
+
* - `children`: `[]` for a self-closed block, `['StyleSheet']` for a body.
|
|
16
|
+
* - `css`: the raw CSS text (`''` when self-closed).
|
|
17
|
+
* - `hasScopeHash`: whether `metadata.styleScopeHash` is set.
|
|
18
|
+
* - `closingElement`: whether `closingElement` is present.
|
|
19
|
+
* - `{ type: 'JSXElement', name, children? }` / `{ type: 'JSXFragment', children }`
|
|
20
|
+
* - `children`, when given, lists the element's children in order. A
|
|
21
|
+
* `<style>` whose first non-whitespace child character is `{` is an
|
|
22
|
+
* ordinary `JSXElement` named `style` with expression-container children
|
|
23
|
+
* (plain-TSX `<style>{css}</style>`), never a `JSXStyleElement`.
|
|
24
|
+
* - `{ type: 'JSXCodeBlock', body, render }` where `body` lists the setup
|
|
25
|
+
* statements in source order and `render` is the single output node or
|
|
26
|
+
* `null`. A `<style>` block is an output node: beside another output it is
|
|
27
|
+
* the multiple-outputs error, and the valid placement is inside a fragment
|
|
28
|
+
* or element.
|
|
29
|
+
* - `{ type: 'JSXIfExpression', consequent, alternate }`,
|
|
30
|
+
* `{ type: 'JSXForExpression', body, empty }`,
|
|
31
|
+
* `{ type: 'JSXSwitchExpression', cases: [{ test, consequent }] }`,
|
|
32
|
+
* `{ type: 'JSXTryExpression', block, pending, handler }`
|
|
33
|
+
* where each clause is the statement list of its block (`null` when absent).
|
|
34
|
+
* - Any other `{ type }` matches on `type` alone (e.g. a setup statement or a
|
|
35
|
+
* `JSXExpressionContainer` child).
|
|
36
|
+
*
|
|
37
|
+
* Negative cases carry `error: { message, start, end }` instead of `expected`
|
|
38
|
+
* (or alongside it, when the recovered tree is also specified): `start`/`end`
|
|
39
|
+
* are the offsets of the reported range in `source`.
|
|
40
|
+
*
|
|
41
|
+
* @typedef {{
|
|
42
|
+
* type: 'JSXStyleElement',
|
|
43
|
+
* selfClosing: boolean,
|
|
44
|
+
* attributes: string[],
|
|
45
|
+
* apply?: string,
|
|
46
|
+
* children: string[],
|
|
47
|
+
* css: string,
|
|
48
|
+
* hasScopeHash: boolean,
|
|
49
|
+
* closingElement: boolean,
|
|
50
|
+
* }} StyleShape
|
|
51
|
+
* @typedef {{ type: 'JSXElement', name: string, children?: Shape[] }} ElementShape
|
|
52
|
+
* @typedef {{ type: 'JSXFragment', children: Shape[] }} FragmentShape
|
|
53
|
+
* @typedef {{ type: 'JSXCodeBlock', body: Shape[], render: Shape | null }} CodeBlockShape
|
|
54
|
+
* @typedef {{ type: 'JSXIfExpression', consequent: Shape[], alternate: Shape[] | null }} IfShape
|
|
55
|
+
* @typedef {{ type: 'JSXForExpression', body: Shape[], empty: Shape[] | null }} ForShape
|
|
56
|
+
* @typedef {{ test: string | null, consequent: Shape[] }} SwitchCaseShape
|
|
57
|
+
* @typedef {{ type: 'JSXSwitchExpression', cases: SwitchCaseShape[] }} SwitchShape
|
|
58
|
+
* @typedef {{
|
|
59
|
+
* type: 'JSXTryExpression',
|
|
60
|
+
* block: Shape[],
|
|
61
|
+
* pending: Shape[] | null,
|
|
62
|
+
* handler: Shape[] | null,
|
|
63
|
+
* }} TryShape
|
|
64
|
+
* @typedef {{ type: 'VariableDeclaration' | 'ExpressionStatement' | 'JSXExpressionContainer' }} StatementShape
|
|
65
|
+
* @typedef {StyleShape
|
|
66
|
+
* | ElementShape
|
|
67
|
+
* | FragmentShape
|
|
68
|
+
* | CodeBlockShape
|
|
69
|
+
* | IfShape
|
|
70
|
+
* | ForShape
|
|
71
|
+
* | SwitchShape
|
|
72
|
+
* | TryShape
|
|
73
|
+
* | StatementShape} Shape
|
|
74
|
+
*
|
|
75
|
+
* The parsed `Program` is walked untyped on purpose: the table must not depend
|
|
76
|
+
* on the JS parser's AST typings so it can be lifted verbatim into an issue.
|
|
77
|
+
* @typedef {(ast: any) => unknown} Locate
|
|
78
|
+
* @typedef {{ message: string, start?: number, end?: number }} ExpectedError
|
|
79
|
+
* @typedef {{ name: string, source: string, locate: Locate, expected: Shape, error?: undefined }} PositiveCase
|
|
80
|
+
* @typedef {{ name: string, source: string, locate: Locate, error: ExpectedError, expected?: Shape }} NegativeCase
|
|
81
|
+
* @typedef {PositiveCase | NegativeCase} StyleSyntaxCase
|
|
82
|
+
*/
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* A self-closed `<style … />`: no CSS body, no scope hash.
|
|
86
|
+
*
|
|
87
|
+
* @param {string[]} [attributes]
|
|
88
|
+
* @param {string} [apply]
|
|
89
|
+
* @returns {StyleShape}
|
|
90
|
+
*/
|
|
91
|
+
const style_self = (attributes = [], apply = undefined) => ({
|
|
92
|
+
type: 'JSXStyleElement',
|
|
93
|
+
selfClosing: true,
|
|
94
|
+
attributes,
|
|
95
|
+
...(apply ? { apply } : {}),
|
|
96
|
+
children: [],
|
|
97
|
+
css: '',
|
|
98
|
+
hasScopeHash: false,
|
|
99
|
+
closingElement: false,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* A bodied `<style>…</style>`: parsed sheet, raw CSS, scope hash.
|
|
104
|
+
*
|
|
105
|
+
* @param {string} css
|
|
106
|
+
* @param {string[]} [attributes]
|
|
107
|
+
* @param {string} [apply]
|
|
108
|
+
* @returns {StyleShape}
|
|
109
|
+
*/
|
|
110
|
+
const style_body = (css, attributes = [], apply = undefined) => ({
|
|
111
|
+
type: 'JSXStyleElement',
|
|
112
|
+
selfClosing: false,
|
|
113
|
+
attributes,
|
|
114
|
+
...(apply ? { apply } : {}),
|
|
115
|
+
children: ['StyleSheet'],
|
|
116
|
+
css,
|
|
117
|
+
hasScopeHash: true,
|
|
118
|
+
closingElement: true,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* @param {string} name
|
|
123
|
+
* @param {Shape[]} [children]
|
|
124
|
+
* @returns {ElementShape}
|
|
125
|
+
*/
|
|
126
|
+
const element = (name, children = undefined) => ({
|
|
127
|
+
type: 'JSXElement',
|
|
128
|
+
name,
|
|
129
|
+
...(children ? { children } : {}),
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* The ordinary TSX `<style>{css}</style>`: a `JSXElement` named `style` whose
|
|
134
|
+
* children are the listed expression containers.
|
|
135
|
+
*
|
|
136
|
+
* @param {number} [containers]
|
|
137
|
+
* @returns {ElementShape}
|
|
138
|
+
*/
|
|
139
|
+
const style_host = (containers = 1) =>
|
|
140
|
+
element(
|
|
141
|
+
'style',
|
|
142
|
+
Array.from({ length: containers }, () => ({ type: 'JSXExpressionContainer' })),
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* @param {Shape[]} body
|
|
147
|
+
* @param {Shape | null} render
|
|
148
|
+
* @returns {CodeBlockShape}
|
|
149
|
+
*/
|
|
150
|
+
const code_block = (body, render) => ({ type: 'JSXCodeBlock', body, render });
|
|
151
|
+
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
// Locators: walk from the parsed `Program` to the node under test.
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The `JSXCodeBlock` body of `function App() @{ … }` (the first statement).
|
|
158
|
+
* @type {(ast: any) => any}
|
|
159
|
+
*/
|
|
160
|
+
const component_block = (ast) => ast.body[0].body;
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* The argument of `return` in `function App() { return …; }`.
|
|
164
|
+
* @type {(ast: any) => any}
|
|
165
|
+
*/
|
|
166
|
+
const returned = (ast) => ast.body[0].body.body[0].argument;
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* The initializer of `export const x = …;` (the first statement).
|
|
170
|
+
* @type {Locate}
|
|
171
|
+
*/
|
|
172
|
+
const exported_init = (ast) => ast.body[0].declaration.declarations[0].init;
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* The program's first statement.
|
|
176
|
+
* @type {Locate}
|
|
177
|
+
*/
|
|
178
|
+
const first_statement = (ast) => ast.body[0];
|
|
179
|
+
|
|
180
|
+
const CSS = '.a { color: red; }';
|
|
181
|
+
|
|
182
|
+
/** @type {StyleSyntaxCase[]} */
|
|
183
|
+
export const STYLE_SYNTAX_CASES = [
|
|
184
|
+
// -- self-closing forms ---------------------------------------------------
|
|
185
|
+
{
|
|
186
|
+
name: 'self-closing <style /> without apply',
|
|
187
|
+
source: `function App() @{ <><style /><div /></> }`,
|
|
188
|
+
locate: (ast) => component_block(ast).render.children[0],
|
|
189
|
+
expected: style_self(),
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
name: 'self-closing <style apply={theme} />',
|
|
193
|
+
source: `function App() @{ <><style apply={theme} /><div /></> }`,
|
|
194
|
+
locate: (ast) => component_block(ast).render.children[0],
|
|
195
|
+
expected: style_self(['apply'], 'Identifier'),
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
name: 'self-closing <style apply={[a, b]} />',
|
|
199
|
+
source: `function App() @{ <><style apply={[a, b]} /><div /></> }`,
|
|
200
|
+
locate: (ast) => component_block(ast).render.children[0],
|
|
201
|
+
expected: style_self(['apply'], 'ArrayExpression'),
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
name: 'self-closing <style apply={ns.dark} />',
|
|
205
|
+
source: `function App() @{ <><style apply={ns.dark} /><div /></> }`,
|
|
206
|
+
locate: (ast) => component_block(ast).render.children[0],
|
|
207
|
+
expected: style_self(['apply'], 'MemberExpression'),
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
name: 'self-closing <style ref={r} apply={theme} /> keeps every attribute in order',
|
|
211
|
+
source: `function App() @{ <><style ref={r} apply={theme} /><div /></> }`,
|
|
212
|
+
locate: (ast) => component_block(ast).render.children[0],
|
|
213
|
+
expected: style_self(['ref', 'apply'], 'Identifier'),
|
|
214
|
+
},
|
|
215
|
+
|
|
216
|
+
// -- bodied forms -----------------------------------------------------------
|
|
217
|
+
{
|
|
218
|
+
name: 'bodied <style apply={t}>…</style> keeps its sheet, css, scope hash and apply',
|
|
219
|
+
source: `function App() @{ <><style apply={t}>${CSS}</style><div /></> }`,
|
|
220
|
+
locate: (ast) => component_block(ast).render.children[0],
|
|
221
|
+
expected: style_body(CSS, ['apply'], 'Identifier'),
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
name: 'bodied <style>…</style> without attributes',
|
|
225
|
+
source: `function App() { return <style>${CSS}</style>; }`,
|
|
226
|
+
locate: returned,
|
|
227
|
+
expected: style_body(CSS),
|
|
228
|
+
},
|
|
229
|
+
|
|
230
|
+
// -- `@{ … }` code-block bodies ---------------------------------------------
|
|
231
|
+
// A `@{ … }` body holds setup statements and exactly ONE output node, and a
|
|
232
|
+
// `<style>` block is an output node like any other: beside another output
|
|
233
|
+
// it is the ordinary multiple-outputs error (reported on the later node),
|
|
234
|
+
// and as the lone output it parses but the analyzer reports
|
|
235
|
+
// `tsrx-style-standalone-needs-fragment`. The valid placement is inside a
|
|
236
|
+
// fragment or element (see the fragment forms below).
|
|
237
|
+
{
|
|
238
|
+
name: 'style before the output node in a @{} body is the multiple-outputs error',
|
|
239
|
+
// 0 1 2 3 4 5
|
|
240
|
+
// 0123456789012345678901234567890123456789012345678901234567
|
|
241
|
+
source: `function App() @{ const x = 1; <style apply={a} /> <div /> }`,
|
|
242
|
+
locate: component_block,
|
|
243
|
+
error: {
|
|
244
|
+
message:
|
|
245
|
+
"A code block renders a single node; wrap multiple nodes or text in a fragment '<>…</>'.",
|
|
246
|
+
start: 51,
|
|
247
|
+
end: 58,
|
|
248
|
+
},
|
|
249
|
+
// Recovery: the last output node becomes `render`; the block stays in
|
|
250
|
+
// `body` in source order.
|
|
251
|
+
expected: code_block(
|
|
252
|
+
[{ type: 'VariableDeclaration' }, style_self(['apply'], 'Identifier')],
|
|
253
|
+
element('div'),
|
|
254
|
+
),
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
name: 'style after the output node in a @{} body is the multiple-outputs error',
|
|
258
|
+
// 0 1 2 3 4 5
|
|
259
|
+
// 0123456789012345678901234567890123456789012345678901234567890
|
|
260
|
+
source: `function App() @{ <div /> <style>${CSS}</style> }`,
|
|
261
|
+
locate: component_block,
|
|
262
|
+
error: {
|
|
263
|
+
message:
|
|
264
|
+
"A code block renders a single node; wrap multiple nodes or text in a fragment '<>…</>'.",
|
|
265
|
+
start: 26,
|
|
266
|
+
end: 59,
|
|
267
|
+
},
|
|
268
|
+
expected: code_block([element('div')], style_body(CSS)),
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
name: 'only a style and no output node in a @{} body parses (the analyzer rejects it)',
|
|
272
|
+
source: `function App() @{ <style>${CSS}</style> }`,
|
|
273
|
+
locate: component_block,
|
|
274
|
+
expected: code_block([], style_body(CSS)),
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
name: 'a fragment holding the style and the output node is the valid @{} form',
|
|
278
|
+
source: `function App() @{ const x = 1; <><style apply={a} /><div /></> }`,
|
|
279
|
+
locate: component_block,
|
|
280
|
+
expected: code_block([{ type: 'VariableDeclaration' }], {
|
|
281
|
+
type: 'JSXFragment',
|
|
282
|
+
children: [style_self(['apply'], 'Identifier'), element('div')],
|
|
283
|
+
}),
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
name: 'nested @{} with its own style inside a fragment',
|
|
287
|
+
source: `function App() @{ <><style apply={a} /><div>@{ <><style apply={b} /><span /></> }</div></> }`,
|
|
288
|
+
locate: (ast) => component_block(ast).render.children[1].children[0],
|
|
289
|
+
expected: code_block([], {
|
|
290
|
+
type: 'JSXFragment',
|
|
291
|
+
children: [style_self(['apply'], 'Identifier'), element('span')],
|
|
292
|
+
}),
|
|
293
|
+
},
|
|
294
|
+
|
|
295
|
+
{
|
|
296
|
+
name: 'assigned @{} block: a theme in its setup and an applying fragment output',
|
|
297
|
+
source: `const something = @{
|
|
298
|
+
const theme = <style>${CSS}</style>;
|
|
299
|
+
<>
|
|
300
|
+
<style apply={theme}>${CSS}</style>
|
|
301
|
+
<div />
|
|
302
|
+
</>
|
|
303
|
+
};`,
|
|
304
|
+
locate: (ast) => ast.body[0].declarations[0].init,
|
|
305
|
+
expected: code_block([{ type: 'VariableDeclaration' }], {
|
|
306
|
+
type: 'JSXFragment',
|
|
307
|
+
children: [style_body(CSS, ['apply'], 'Identifier'), element('div')],
|
|
308
|
+
}),
|
|
309
|
+
},
|
|
310
|
+
|
|
311
|
+
// -- directive bodies -------------------------------------------------------
|
|
312
|
+
// Every control-flow body renders one output node too: a block beside it
|
|
313
|
+
// is the multiple-outputs error, and the valid form wraps both in a
|
|
314
|
+
// fragment. (`@switch` case bodies do not report multiple outputs today,
|
|
315
|
+
// so only their fragment form is listed.)
|
|
316
|
+
{
|
|
317
|
+
name: 'style beside the output node in an @if consequent is the multiple-outputs error',
|
|
318
|
+
// 0 1 2 3 4 5
|
|
319
|
+
// 012345678901234567890123456789012345678901234567890123456
|
|
320
|
+
source: `function App() @{ @if (ok) { <style apply={a} /> <b /> } }`,
|
|
321
|
+
locate: (ast) => component_block(ast).render,
|
|
322
|
+
error: {
|
|
323
|
+
message:
|
|
324
|
+
"A code block renders a single node; wrap multiple nodes or text in a fragment '<>…</>'.",
|
|
325
|
+
start: 49,
|
|
326
|
+
end: 54,
|
|
327
|
+
},
|
|
328
|
+
expected: {
|
|
329
|
+
type: 'JSXIfExpression',
|
|
330
|
+
consequent: [style_self(['apply'], 'Identifier'), element('b')],
|
|
331
|
+
alternate: null,
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
name: 'only a style inside an @if consequent parses (the analyzer rejects it)',
|
|
336
|
+
source: `function App() @{ @if (ok) { <style apply={a} /> } }`,
|
|
337
|
+
locate: (ast) => component_block(ast).render,
|
|
338
|
+
expected: {
|
|
339
|
+
type: 'JSXIfExpression',
|
|
340
|
+
consequent: [style_self(['apply'], 'Identifier')],
|
|
341
|
+
alternate: null,
|
|
342
|
+
},
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
name: 'fragments holding style and output inside @if consequent and @else',
|
|
346
|
+
source: `function App() @{ @if (ok) { <><style apply={a} /><b /></> } @else { <><i /><style apply={c} /></> } }`,
|
|
347
|
+
locate: (ast) => component_block(ast).render,
|
|
348
|
+
expected: {
|
|
349
|
+
type: 'JSXIfExpression',
|
|
350
|
+
consequent: [
|
|
351
|
+
{ type: 'JSXFragment', children: [style_self(['apply'], 'Identifier'), element('b')] },
|
|
352
|
+
],
|
|
353
|
+
alternate: [
|
|
354
|
+
{ type: 'JSXFragment', children: [element('i'), style_self(['apply'], 'Identifier')] },
|
|
355
|
+
],
|
|
356
|
+
},
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
name: 'style beside the output node in a @for body is the multiple-outputs error',
|
|
360
|
+
// 0 1 2 3 4 5 6 7
|
|
361
|
+
// 012345678901234567890123456789012345678901234567890123456789012345678901
|
|
362
|
+
source: `function App() @{ @for (const x of xs) { <style apply={a} /> <b>{x}</b> } }`,
|
|
363
|
+
locate: (ast) => component_block(ast).render,
|
|
364
|
+
error: {
|
|
365
|
+
message:
|
|
366
|
+
"A code block renders a single node; wrap multiple nodes or text in a fragment '<>…</>'.",
|
|
367
|
+
start: 61,
|
|
368
|
+
end: 71,
|
|
369
|
+
},
|
|
370
|
+
expected: {
|
|
371
|
+
type: 'JSXForExpression',
|
|
372
|
+
body: [style_self(['apply'], 'Identifier'), element('b')],
|
|
373
|
+
empty: null,
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
name: 'fragments holding style and output inside @for body and @empty',
|
|
378
|
+
source: `function App() @{ @for (const x of xs) { <><style apply={a} /><b>{x}</b></> } @empty { <><i /><style apply={c} /></> } }`,
|
|
379
|
+
locate: (ast) => component_block(ast).render,
|
|
380
|
+
expected: {
|
|
381
|
+
type: 'JSXForExpression',
|
|
382
|
+
body: [
|
|
383
|
+
{ type: 'JSXFragment', children: [style_self(['apply'], 'Identifier'), element('b')] },
|
|
384
|
+
],
|
|
385
|
+
empty: [
|
|
386
|
+
{ type: 'JSXFragment', children: [element('i'), style_self(['apply'], 'Identifier')] },
|
|
387
|
+
],
|
|
388
|
+
},
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
name: 'fragments holding style and output inside @switch case and default',
|
|
392
|
+
source: `function App() @{ @switch (k) { @case 1: { <><style apply={a} /><b /></> } @default: { <><i /><style apply={c} /></> } } }`,
|
|
393
|
+
locate: (ast) => component_block(ast).render,
|
|
394
|
+
expected: {
|
|
395
|
+
type: 'JSXSwitchExpression',
|
|
396
|
+
cases: [
|
|
397
|
+
{
|
|
398
|
+
test: 'Literal',
|
|
399
|
+
consequent: [
|
|
400
|
+
{
|
|
401
|
+
type: 'JSXFragment',
|
|
402
|
+
children: [style_self(['apply'], 'Identifier'), element('b')],
|
|
403
|
+
},
|
|
404
|
+
],
|
|
405
|
+
},
|
|
406
|
+
{
|
|
407
|
+
test: null,
|
|
408
|
+
consequent: [
|
|
409
|
+
{
|
|
410
|
+
type: 'JSXFragment',
|
|
411
|
+
children: [element('i'), style_self(['apply'], 'Identifier')],
|
|
412
|
+
},
|
|
413
|
+
],
|
|
414
|
+
},
|
|
415
|
+
],
|
|
416
|
+
},
|
|
417
|
+
},
|
|
418
|
+
{
|
|
419
|
+
name: 'style beside the output node in a @try block is the multiple-outputs error',
|
|
420
|
+
// 0 1 2 3 4 5
|
|
421
|
+
// 0123456789012345678901234567890123456789012345678901
|
|
422
|
+
source: `function App() @{ @try { <style apply={a} /> <b /> } @catch (e) { <i /> } }`,
|
|
423
|
+
locate: (ast) => component_block(ast).render,
|
|
424
|
+
error: {
|
|
425
|
+
message:
|
|
426
|
+
"A code block renders a single node; wrap multiple nodes or text in a fragment '<>…</>'.",
|
|
427
|
+
start: 45,
|
|
428
|
+
end: 50,
|
|
429
|
+
},
|
|
430
|
+
expected: {
|
|
431
|
+
type: 'JSXTryExpression',
|
|
432
|
+
block: [style_self(['apply'], 'Identifier'), element('b')],
|
|
433
|
+
pending: null,
|
|
434
|
+
handler: [element('i')],
|
|
435
|
+
},
|
|
436
|
+
},
|
|
437
|
+
{
|
|
438
|
+
name: 'fragments holding style and output inside @try, @pending and @catch',
|
|
439
|
+
source: `function App() @{ @try { <><style apply={a} /><b /></> } @pending { <><style apply={p} /><u /></> } @catch (e) { <><i /><style apply={c} /></> } }`,
|
|
440
|
+
locate: (ast) => component_block(ast).render,
|
|
441
|
+
expected: {
|
|
442
|
+
type: 'JSXTryExpression',
|
|
443
|
+
block: [
|
|
444
|
+
{ type: 'JSXFragment', children: [style_self(['apply'], 'Identifier'), element('b')] },
|
|
445
|
+
],
|
|
446
|
+
pending: [
|
|
447
|
+
{ type: 'JSXFragment', children: [style_self(['apply'], 'Identifier'), element('u')] },
|
|
448
|
+
],
|
|
449
|
+
handler: [
|
|
450
|
+
{ type: 'JSXFragment', children: [element('i'), style_self(['apply'], 'Identifier')] },
|
|
451
|
+
],
|
|
452
|
+
},
|
|
453
|
+
},
|
|
454
|
+
|
|
455
|
+
// -- fragments --------------------------------------------------------------
|
|
456
|
+
{
|
|
457
|
+
name: 'multiple sibling style blocks inside one fragment',
|
|
458
|
+
source: `function App() { return <><style apply={a} /><style>${CSS}</style><div /></>; }`,
|
|
459
|
+
locate: returned,
|
|
460
|
+
expected: {
|
|
461
|
+
type: 'JSXFragment',
|
|
462
|
+
children: [style_self(['apply'], 'Identifier'), style_body(CSS), element('div')],
|
|
463
|
+
},
|
|
464
|
+
},
|
|
465
|
+
|
|
466
|
+
// -- expression-child <style> (plain TSX) -----------------------------------
|
|
467
|
+
{
|
|
468
|
+
name: '<style>{css}</style> is an ordinary JSXElement in a plain-TSX return',
|
|
469
|
+
source: `function App() { return <style>{css}</style>; }`,
|
|
470
|
+
locate: returned,
|
|
471
|
+
expected: style_host(),
|
|
472
|
+
},
|
|
473
|
+
{
|
|
474
|
+
name: '<style>{css}</style> stays an ordinary JSXElement inside a @{} body',
|
|
475
|
+
source: `function App() @{ <section><style>{css}</style><div /></section> }`,
|
|
476
|
+
locate: (ast) => component_block(ast).render,
|
|
477
|
+
expected: element('section', [style_host(), element('div')]),
|
|
478
|
+
},
|
|
479
|
+
{
|
|
480
|
+
name: '<style> with whitespace before its first { child is an ordinary JSXElement',
|
|
481
|
+
source: `function App() { return <section><style>\n\t{css}\n</style></section>; }`,
|
|
482
|
+
locate: (ast) => returned(ast).children[0],
|
|
483
|
+
expected: style_host(),
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
name: '<style> with two expression children is an ordinary JSXElement',
|
|
487
|
+
source: `function App() { return <style>{reset}{theme}</style>; }`,
|
|
488
|
+
locate: returned,
|
|
489
|
+
expected: style_host(2),
|
|
490
|
+
},
|
|
491
|
+
{
|
|
492
|
+
name: 'a { inside CSS text after other text still parses as a bodied block',
|
|
493
|
+
source: `function App() @{ <><style>.a{color:red}</style><div /></> }`,
|
|
494
|
+
locate: (ast) => component_block(ast).render.children[0],
|
|
495
|
+
expected: style_body('.a{color:red}'),
|
|
496
|
+
},
|
|
497
|
+
|
|
498
|
+
// -- module scope -----------------------------------------------------------
|
|
499
|
+
{
|
|
500
|
+
name: 'module-scope assigned self-closed block',
|
|
501
|
+
source: `export const theme = <style apply={[a, b]} />;`,
|
|
502
|
+
locate: exported_init,
|
|
503
|
+
expected: style_self(['apply'], 'ArrayExpression'),
|
|
504
|
+
},
|
|
505
|
+
{
|
|
506
|
+
name: 'module-scope bare <style> still parses without a parser error',
|
|
507
|
+
source: `<style>${CSS}</style>;\nexport function App() { return <div />; }`,
|
|
508
|
+
locate: first_statement,
|
|
509
|
+
expected: style_body(CSS),
|
|
510
|
+
},
|
|
511
|
+
|
|
512
|
+
// -- <head> -----------------------------------------------------------------
|
|
513
|
+
{
|
|
514
|
+
name: 'head <style> has no scope hash',
|
|
515
|
+
source: `function App() { return <head><style>body { margin: 0; }</style></head>; }`,
|
|
516
|
+
locate: (ast) => returned(ast).children[0],
|
|
517
|
+
expected: { ...style_body('body { margin: 0; }'), hasScopeHash: false },
|
|
518
|
+
},
|
|
519
|
+
];
|
package/types/index.d.ts
CHANGED
|
@@ -154,6 +154,24 @@ export interface BaseNodeMetaData {
|
|
|
154
154
|
delegated?: boolean;
|
|
155
155
|
returned_tsrx_return?: AST.ReturnStatement;
|
|
156
156
|
styleScopeHash?: string;
|
|
157
|
+
/** Resolved `apply` entries of a `<style>` block (set by the style analyzer). */
|
|
158
|
+
styleApplies?: StyleApplyResolution[];
|
|
159
|
+
/** An assigned block is the target of some `apply` in its module. */
|
|
160
|
+
styleApplied?: boolean;
|
|
161
|
+
/** An assigned block is exported from its module. */
|
|
162
|
+
styleExported?: boolean;
|
|
163
|
+
/** An assigned block's `$class` is read somewhere in its module (an element or a child prop opts into the theme). */
|
|
164
|
+
styleClassRead?: boolean;
|
|
165
|
+
/** How an assigned block renders: `theme` keeps every selector, `class-map` prunes (D4/D5). */
|
|
166
|
+
styleKind?: 'theme' | 'class-map';
|
|
167
|
+
/** The transform's style pre-pass already rendered this assigned block's sheet. */
|
|
168
|
+
tsrx_style_prepared?: boolean;
|
|
169
|
+
/** Resolved `$class` parts of a block's applied themes (literals or `theme.$class` reads). */
|
|
170
|
+
tsrx_style_class_parts?: Array<string | AST.Expression>;
|
|
171
|
+
/** Style `ref` setup statements for a scope whose root is this native fragment/element. */
|
|
172
|
+
tsrx_style_ref_statements?: AST.Statement[];
|
|
173
|
+
/** Accumulated scope classes of an element (see transform/scoping.js). */
|
|
174
|
+
tsrx_scope_class?: ScopeClassParts;
|
|
157
175
|
css?: {
|
|
158
176
|
scopedClasses: TopScopedClasses;
|
|
159
177
|
hash: string;
|
|
@@ -173,6 +191,8 @@ export interface BaseNodeMetaData {
|
|
|
173
191
|
/** The current var scope contains a lazy `var` binding in a JavaScript loop header. */
|
|
174
192
|
has_lazy_var_loop_descendants?: boolean;
|
|
175
193
|
disable_verification?: boolean;
|
|
194
|
+
/** Map this synthesized identifier's borrowed source span for diagnostics only (no hover/navigation). */
|
|
195
|
+
verify_only?: boolean;
|
|
176
196
|
/** Identifiers whose source ranges also map to this generated identifier. */
|
|
177
197
|
extra_source_mappings?: Array<(AST.Identifier | AST.PrivateIdentifier) & AST.NodeWithLocation>;
|
|
178
198
|
generated_setup_declarations?: AST.Statement[];
|
|
@@ -452,6 +472,7 @@ declare module 'estree' {
|
|
|
452
472
|
|
|
453
473
|
interface JSXCodeBlock extends AST.BaseExpression {
|
|
454
474
|
type: 'JSXCodeBlock';
|
|
475
|
+
/** Setup statements plus any `<style>` siblings of the output node (D3), in source order. */
|
|
455
476
|
body: AST.Statement[];
|
|
456
477
|
render: AST.Node | null;
|
|
457
478
|
metadata: BaseNodeMetaData;
|
|
@@ -460,6 +481,10 @@ declare module 'estree' {
|
|
|
460
481
|
|
|
461
482
|
interface JSXStyleElement extends Omit<AST.TSRXJSXElement, 'type' | 'children'> {
|
|
462
483
|
type: 'JSXStyleElement';
|
|
484
|
+
/**
|
|
485
|
+
* The parsed body, or empty for a self-closed `<style apply={…} />`
|
|
486
|
+
* (`openingElement.selfClosing`), which has no CSS and no scope hash.
|
|
487
|
+
*/
|
|
463
488
|
children: AST.CSS.StyleSheet[];
|
|
464
489
|
css?: string;
|
|
465
490
|
unclosed?: boolean;
|
|
@@ -755,6 +780,7 @@ declare module 'estree' {
|
|
|
755
780
|
|
|
756
781
|
export namespace CSS {
|
|
757
782
|
export interface BaseNode extends AST.NodeWithMaybeComments {
|
|
783
|
+
/** Offset into the style body (`StyleSheet.source`), not the file. */
|
|
758
784
|
start: number;
|
|
759
785
|
end: number;
|
|
760
786
|
loc?: AST.SourceLocation;
|
|
@@ -765,6 +791,14 @@ declare module 'estree' {
|
|
|
765
791
|
children: Array<Atrule | Rule>;
|
|
766
792
|
source: string;
|
|
767
793
|
hash: string;
|
|
794
|
+
/** The file the `<style>` body was parsed from. */
|
|
795
|
+
filename?: string;
|
|
796
|
+
/**
|
|
797
|
+
* File offset of `source[0]`, set when the sheet was parsed with a body
|
|
798
|
+
* origin (every sheet `parseModule` produces). `start` / `end` stay
|
|
799
|
+
* body-relative; `loc` is then the body's file-relative location.
|
|
800
|
+
*/
|
|
801
|
+
sourceStart?: number;
|
|
768
802
|
}
|
|
769
803
|
|
|
770
804
|
export interface Atrule extends BaseNode {
|
|
@@ -1537,6 +1571,57 @@ export interface TSRXAnalysisResult {
|
|
|
1537
1571
|
ast: AST.Program;
|
|
1538
1572
|
errors: CompileError[];
|
|
1539
1573
|
comments: AST.CommentWithLocation[];
|
|
1574
|
+
/** Module scope built by `createScopes` for the same program. */
|
|
1575
|
+
scope: ScopeInterface;
|
|
1576
|
+
scopes: Map<AST.Node, ScopeInterface>;
|
|
1577
|
+
styles: StyleAnalysis;
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
/**
|
|
1581
|
+
* One resolved entry of a `<style apply={…}>` attribute. Holds AST nodes only
|
|
1582
|
+
* (no bindings, whose reference paths point back up the tree) so the analyzed
|
|
1583
|
+
* program stays acyclic for consumers that clone or serialize it.
|
|
1584
|
+
*/
|
|
1585
|
+
export interface StyleApplyResolution {
|
|
1586
|
+
/** The authored entry (an identifier or member expression). */
|
|
1587
|
+
expression: AST.Expression;
|
|
1588
|
+
/** The same-module assigned block the entry names, or `null` for an import (runtime `$class`). */
|
|
1589
|
+
target: AST.JSXStyleElement | null;
|
|
1590
|
+
/** Whether the entry resolves to a same-module block or to an import. */
|
|
1591
|
+
kind: 'local' | 'import';
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
/** Module-level summary produced by the style analyzer (`program.metadata.styles`). */
|
|
1595
|
+
export interface StyleAnalysis {
|
|
1596
|
+
/** `const theme = <style>…</style>` blocks, in source order. */
|
|
1597
|
+
assigned: AST.JSXStyleElement[];
|
|
1598
|
+
/** Blocks that scope the template they sit in, in source order. */
|
|
1599
|
+
standalone: AST.JSXStyleElement[];
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
/** Options for the class map object built for an assigned or `ref`-exposed style block. */
|
|
1603
|
+
export interface StyleClassMapOptions {
|
|
1604
|
+
/** `$class` parts of applied themes, in order: literals for static classes, expressions for runtime reads. */
|
|
1605
|
+
applied?: Array<string | AST.Expression>;
|
|
1606
|
+
/** Override the own hash (`null` for a body-less `<style apply />`). */
|
|
1607
|
+
hash?: string | null;
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
/** How `prepareStylesheetForRender` treats a sheet's selectors (D4). */
|
|
1611
|
+
export type StyleRenderMode = 'scope' | 'class-map' | 'theme';
|
|
1612
|
+
|
|
1613
|
+
/**
|
|
1614
|
+
* The classes a scope pre-pass stamped on one element, kept apart from the
|
|
1615
|
+
* authored value so nested scopes append to one attribute value instead of
|
|
1616
|
+
* nesting template literals: `base statics… applies…`.
|
|
1617
|
+
*/
|
|
1618
|
+
export interface ScopeClassParts {
|
|
1619
|
+
/** The authored class value, if any. */
|
|
1620
|
+
base: AST.Expression | null;
|
|
1621
|
+
/** Scope hashes, outermost first. */
|
|
1622
|
+
hashes: string[];
|
|
1623
|
+
/** Applied theme classes: literals when statically known, else `theme.$class` reads. */
|
|
1624
|
+
applied: Array<string | AST.Expression>;
|
|
1540
1625
|
}
|
|
1541
1626
|
|
|
1542
1627
|
/**
|