@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,467 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module-level analysis of `<style>` blocks: which blocks are standalone
|
|
3
|
+
* (scoped to the template scope they sit in) and which are assigned
|
|
4
|
+
* (`const theme = <style>…</style>`), what every `apply` attribute resolves to,
|
|
5
|
+
* and whether an assigned block is a theme (exported or applied, D5) or a
|
|
6
|
+
* class map. Results are stamped on the style nodes' `metadata` so the target
|
|
7
|
+
* transforms — which clone nodes but share metadata — read one shape, and are
|
|
8
|
+
* summarized on `program.metadata.styles` for consumer compilers.
|
|
9
|
+
*
|
|
10
|
+
* Declared-before-use (D13 layer 1) is enforced here by source position and
|
|
11
|
+
* lexical visibility: same-module CSS is emitted in lexical order, so a theme
|
|
12
|
+
* declared after its applier would win the cascade instead of losing it.
|
|
13
|
+
*
|
|
14
|
+
* @import * as AST from 'estree'
|
|
15
|
+
* @import * as ESTreeJSX from 'estree-jsx'
|
|
16
|
+
* @import { Binding, ScopeInterface, StyleApplyResolution, StyleAnalysis, TSRXAnalysisState, Visitors } from '../../types/index'
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { walk } from 'zimmerframe';
|
|
20
|
+
import { DIAGNOSTIC_CODES } from '../diagnostics.js';
|
|
21
|
+
import { get_style_class_map_names, get_style_element_stylesheet } from '../transform/style-ref.js';
|
|
22
|
+
import { is_function_node, is_template_directive } from '../utils/ast.js';
|
|
23
|
+
import {
|
|
24
|
+
TSRX_STYLE_APPLY_DUPLICATE_ERROR,
|
|
25
|
+
TSRX_STYLE_APPLY_UNSUPPORTED_HOST_ERROR,
|
|
26
|
+
TSRX_STYLE_APPLY_VALUE_ERROR,
|
|
27
|
+
TSRX_STYLE_RESERVED_CLASS_KEY_ERROR,
|
|
28
|
+
TSRX_STYLE_STANDALONE_AT_MODULE_SCOPE_ERROR,
|
|
29
|
+
TSRX_STYLE_STANDALONE_NEEDS_FRAGMENT_ERROR,
|
|
30
|
+
TSRX_STYLE_STANDALONE_OUTSIDE_TEMPLATE_ERROR,
|
|
31
|
+
tsrx_style_apply_before_declaration_error,
|
|
32
|
+
tsrx_style_apply_target_error,
|
|
33
|
+
tsrx_style_unknown_attribute_error,
|
|
34
|
+
validate_style,
|
|
35
|
+
} from './validation.js';
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* `container_depth` counts the enclosing TSRX containers — `@{ … }` bodies
|
|
39
|
+
* and control-flow directives — where raw CSS in a standalone block is
|
|
40
|
+
* template syntax; native elements only bump `template_depth`.
|
|
41
|
+
*
|
|
42
|
+
* @typedef {{ function_depth: number, template_depth: number, container_depth: number }} StyleWalkState
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* `apply` sites resolve against ordinary JavaScript scoping, so the analyzer
|
|
47
|
+
* looks the target up from the nearest enclosing scope of the style block.
|
|
48
|
+
*
|
|
49
|
+
* @param {AST.Node[]} path
|
|
50
|
+
* @param {Map<AST.Node, ScopeInterface>} scopes
|
|
51
|
+
* @returns {ScopeInterface | null}
|
|
52
|
+
*/
|
|
53
|
+
function nearest_scope(path, scopes) {
|
|
54
|
+
for (let i = path.length - 1; i >= 0; i -= 1) {
|
|
55
|
+
const scope = scopes.get(path[i]);
|
|
56
|
+
if (scope) return scope;
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Names exported through `export { a, b as c }` and `export default a`.
|
|
63
|
+
*
|
|
64
|
+
* @param {AST.Program} ast
|
|
65
|
+
* @returns {Set<string>}
|
|
66
|
+
*/
|
|
67
|
+
function collect_exported_names(ast) {
|
|
68
|
+
/** @type {Set<string>} */
|
|
69
|
+
const names = new Set();
|
|
70
|
+
for (const statement of ast.body) {
|
|
71
|
+
if (statement.type === 'ExportNamedDeclaration' && !statement.declaration) {
|
|
72
|
+
for (const specifier of statement.specifiers) {
|
|
73
|
+
if (specifier.local.type === 'Identifier') names.add(specifier.local.name);
|
|
74
|
+
}
|
|
75
|
+
} else if (
|
|
76
|
+
statement.type === 'ExportDefaultDeclaration' &&
|
|
77
|
+
statement.declaration.type === 'Identifier'
|
|
78
|
+
) {
|
|
79
|
+
names.add(statement.declaration.name);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return names;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* A style block is standalone when it is template content rather than a value:
|
|
87
|
+
* a child of a native element/fragment, the render output of a `@{ … }` body
|
|
88
|
+
* or a control-flow body, or a bare statement. Only the first placement is
|
|
89
|
+
* valid; the others are reported (5.1).
|
|
90
|
+
*
|
|
91
|
+
* @param {AST.Node[]} path
|
|
92
|
+
* @returns {boolean}
|
|
93
|
+
*/
|
|
94
|
+
export function is_standalone_style_position(path) {
|
|
95
|
+
const parent = path.at(-1);
|
|
96
|
+
if (!parent) return true;
|
|
97
|
+
switch (parent.type) {
|
|
98
|
+
case 'JSXElement':
|
|
99
|
+
case 'JSXFragment':
|
|
100
|
+
return !!parent.metadata?.native_tsrx;
|
|
101
|
+
case 'JSXCodeBlock':
|
|
102
|
+
case 'BlockStatement':
|
|
103
|
+
case 'SwitchCase':
|
|
104
|
+
case 'Program':
|
|
105
|
+
case 'ExpressionStatement':
|
|
106
|
+
return true;
|
|
107
|
+
default:
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* @param {ESTreeJSX.JSXAttributeNode} attr
|
|
114
|
+
* @param {string} name
|
|
115
|
+
* @returns {boolean}
|
|
116
|
+
*/
|
|
117
|
+
function is_named_attribute(attr, name) {
|
|
118
|
+
return (
|
|
119
|
+
attr.type === 'JSXAttribute' && attr.name.type === 'JSXIdentifier' && attr.name.name === name
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* @param {ESTreeJSX.JSXAttribute} attr
|
|
125
|
+
* @returns {AST.Expression | null}
|
|
126
|
+
*/
|
|
127
|
+
function attribute_expression(attr) {
|
|
128
|
+
const value = attr.value;
|
|
129
|
+
if (!value || value.type !== 'JSXExpressionContainer') return null;
|
|
130
|
+
return value.expression.type === 'JSXEmptyExpression' ? null : value.expression;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* @param {AST.Expression} expression
|
|
135
|
+
* @returns {string}
|
|
136
|
+
*/
|
|
137
|
+
function describe_target(expression) {
|
|
138
|
+
if (expression.type === 'Identifier') return expression.name;
|
|
139
|
+
if (
|
|
140
|
+
expression.type === 'MemberExpression' &&
|
|
141
|
+
!expression.computed &&
|
|
142
|
+
expression.property.type === 'Identifier'
|
|
143
|
+
) {
|
|
144
|
+
return `${describe_target(/** @type {AST.Expression} */ (expression.object))}.${expression.property.name}`;
|
|
145
|
+
}
|
|
146
|
+
return 'apply target';
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* @param {AST.Expression} expression
|
|
151
|
+
* @returns {AST.Identifier | null}
|
|
152
|
+
*/
|
|
153
|
+
function member_root(expression) {
|
|
154
|
+
let current = expression;
|
|
155
|
+
while (current.type === 'MemberExpression' && !current.computed) {
|
|
156
|
+
current = /** @type {AST.Expression} */ (current.object);
|
|
157
|
+
}
|
|
158
|
+
return current.type === 'Identifier' ? current : null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* @param {Binding} binding
|
|
163
|
+
* @param {AST.Expression} expression the `apply` entry for the member chain
|
|
164
|
+
* @returns {AST.JSXStyleElement | null | undefined} `undefined` when the
|
|
165
|
+
* member does not name a style block of a module-local object
|
|
166
|
+
*/
|
|
167
|
+
function resolve_local_member(binding, expression) {
|
|
168
|
+
if (expression.type !== 'MemberExpression' || expression.computed) return undefined;
|
|
169
|
+
if (expression.object.type !== 'Identifier' || expression.property.type !== 'Identifier') {
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
const object = binding.initial;
|
|
173
|
+
if (object?.type !== 'ObjectExpression') return undefined;
|
|
174
|
+
const name = expression.property.name;
|
|
175
|
+
for (const property of object.properties) {
|
|
176
|
+
if (
|
|
177
|
+
property.type === 'Property' &&
|
|
178
|
+
!property.computed &&
|
|
179
|
+
((property.key.type === 'Identifier' && property.key.name === name) ||
|
|
180
|
+
(property.key.type === 'Literal' && property.key.value === name)) &&
|
|
181
|
+
property.value.type === 'JSXStyleElement'
|
|
182
|
+
) {
|
|
183
|
+
return property.value;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Whether any reference to the binding reads its `$class` property.
|
|
191
|
+
*
|
|
192
|
+
* @param {Binding | null} binding
|
|
193
|
+
* @returns {boolean}
|
|
194
|
+
*/
|
|
195
|
+
function is_class_read(binding) {
|
|
196
|
+
if (!binding) return false;
|
|
197
|
+
return binding.references.some(({ node, path }) => {
|
|
198
|
+
const parent = path.at(-1);
|
|
199
|
+
if (parent?.type !== 'MemberExpression' || parent.object !== node) return false;
|
|
200
|
+
if (parent.computed) {
|
|
201
|
+
return parent.property.type === 'Literal' && parent.property.value === '$class';
|
|
202
|
+
}
|
|
203
|
+
return parent.property.type === 'Identifier' && parent.property.name === '$class';
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Run the module-level style analysis. `scopes` comes from `create_scopes`
|
|
209
|
+
* over the same program so target resolution uses real bindings.
|
|
210
|
+
*
|
|
211
|
+
* @param {AST.Program} ast
|
|
212
|
+
* @param {Map<AST.Node, ScopeInterface>} scopes
|
|
213
|
+
* @param {TSRXAnalysisState} state
|
|
214
|
+
* @returns {StyleAnalysis}
|
|
215
|
+
*/
|
|
216
|
+
export function analyze_styles(ast, scopes, state) {
|
|
217
|
+
const exported_names = collect_exported_names(ast);
|
|
218
|
+
const errors = state.collect ? state.errors : undefined;
|
|
219
|
+
/** @type {AST.JSXStyleElement[]} */
|
|
220
|
+
const assigned = [];
|
|
221
|
+
/** @type {AST.JSXStyleElement[]} */
|
|
222
|
+
const standalone = [];
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* @param {string} message
|
|
226
|
+
* @param {string} code
|
|
227
|
+
* @param {AST.Node} node
|
|
228
|
+
*/
|
|
229
|
+
const report = (message, code, node) => {
|
|
230
|
+
validate_style(message, code, node, state.filename, errors, state.comments);
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* @param {AST.Expression} expression
|
|
235
|
+
* @param {ScopeInterface | null} scope
|
|
236
|
+
* @param {StyleApplyResolution[]} resolutions
|
|
237
|
+
*/
|
|
238
|
+
const resolve_apply_entry = (expression, scope, resolutions) => {
|
|
239
|
+
if (expression.type === 'ArrayExpression') {
|
|
240
|
+
for (const element of expression.elements) {
|
|
241
|
+
if (!element || element.type === 'SpreadElement') {
|
|
242
|
+
report(
|
|
243
|
+
tsrx_style_apply_target_error('apply entry'),
|
|
244
|
+
DIAGNOSTIC_CODES.STYLE_APPLY_TARGET,
|
|
245
|
+
element ?? expression,
|
|
246
|
+
);
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
resolve_apply_entry(element, scope, resolutions);
|
|
250
|
+
}
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const root = member_root(expression);
|
|
255
|
+
const name = describe_target(expression);
|
|
256
|
+
const binding = root && scope ? scope.get(root.name) : null;
|
|
257
|
+
if (!root || !binding) {
|
|
258
|
+
report(tsrx_style_apply_target_error(name), DIAGNOSTIC_CODES.STYLE_APPLY_TARGET, expression);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (binding.declaration_kind === 'import') {
|
|
263
|
+
resolutions.push({ expression, target: null, kind: 'import' });
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** @type {AST.JSXStyleElement | null | undefined} */
|
|
268
|
+
let target;
|
|
269
|
+
if (expression.type === 'Identifier') {
|
|
270
|
+
target = binding.initial?.type === 'JSXStyleElement' ? binding.initial : undefined;
|
|
271
|
+
} else {
|
|
272
|
+
target = resolve_local_member(binding, expression);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (!target) {
|
|
276
|
+
report(tsrx_style_apply_target_error(name), DIAGNOSTIC_CODES.STYLE_APPLY_TARGET, expression);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (/** @type {number} */ (binding.node.start) > /** @type {number} */ (root.start)) {
|
|
281
|
+
report(
|
|
282
|
+
tsrx_style_apply_before_declaration_error(name),
|
|
283
|
+
DIAGNOSTIC_CODES.STYLE_APPLY_BEFORE_DECLARATION,
|
|
284
|
+
root,
|
|
285
|
+
);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
target.metadata.styleApplied = true;
|
|
290
|
+
resolutions.push({ expression, target, kind: 'local' });
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
walk(
|
|
294
|
+
/** @type {AST.Node} */ (ast),
|
|
295
|
+
/** @type {StyleWalkState} */ ({ function_depth: 0, template_depth: 0, container_depth: 0 }),
|
|
296
|
+
/** @type {Visitors<AST.Node, StyleWalkState>} */ ({
|
|
297
|
+
_(node, { state: walk_state, next }) {
|
|
298
|
+
if (is_function_node(node)) {
|
|
299
|
+
next({ ...walk_state, function_depth: walk_state.function_depth + 1 });
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
if (node.type === 'JSXCodeBlock' || is_template_directive(node)) {
|
|
303
|
+
// `@else if` chains and `@catch` clauses are children of the
|
|
304
|
+
// directive node, so one bump covers every branch body.
|
|
305
|
+
next({
|
|
306
|
+
...walk_state,
|
|
307
|
+
template_depth: walk_state.template_depth + 1,
|
|
308
|
+
container_depth: walk_state.container_depth + 1,
|
|
309
|
+
});
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
if (
|
|
313
|
+
(node.type === 'JSXElement' || node.type === 'JSXFragment') &&
|
|
314
|
+
node.metadata?.native_tsrx
|
|
315
|
+
) {
|
|
316
|
+
next({ ...walk_state, template_depth: walk_state.template_depth + 1 });
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
next();
|
|
320
|
+
},
|
|
321
|
+
|
|
322
|
+
JSXStyleElement(node, { path, state: walk_state, next }) {
|
|
323
|
+
const is_standalone = is_standalone_style_position(path);
|
|
324
|
+
const inside_head = path.some(
|
|
325
|
+
(ancestor) =>
|
|
326
|
+
ancestor.type === 'JSXElement' &&
|
|
327
|
+
ancestor.openingElement.name.type === 'JSXIdentifier' &&
|
|
328
|
+
ancestor.openingElement.name.name === 'head',
|
|
329
|
+
);
|
|
330
|
+
const attributes = node.openingElement.attributes;
|
|
331
|
+
const is_resource = attributes.some((attr) => is_named_attribute(attr, 'href'));
|
|
332
|
+
|
|
333
|
+
/** @type {ESTreeJSX.JSXAttribute | null} */
|
|
334
|
+
let apply_attr = null;
|
|
335
|
+
for (const attr of attributes) {
|
|
336
|
+
if (attr.type !== 'JSXAttribute') continue;
|
|
337
|
+
if (is_named_attribute(attr, 'apply')) {
|
|
338
|
+
if (apply_attr) {
|
|
339
|
+
report(
|
|
340
|
+
TSRX_STYLE_APPLY_DUPLICATE_ERROR,
|
|
341
|
+
DIAGNOSTIC_CODES.STYLE_APPLY_DUPLICATE,
|
|
342
|
+
attr,
|
|
343
|
+
);
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
apply_attr = attr;
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
if (is_named_attribute(attr, 'ref') || inside_head || is_resource) continue;
|
|
350
|
+
const attr_name =
|
|
351
|
+
attr.name.type === 'JSXIdentifier'
|
|
352
|
+
? attr.name.name
|
|
353
|
+
: `${attr.name.namespace.name}:${attr.name.name.name}`;
|
|
354
|
+
report(
|
|
355
|
+
tsrx_style_unknown_attribute_error(attr_name),
|
|
356
|
+
DIAGNOSTIC_CODES.STYLE_UNKNOWN_ATTRIBUTE,
|
|
357
|
+
attr,
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/** @type {StyleApplyResolution[]} */
|
|
362
|
+
const resolutions = [];
|
|
363
|
+
if (apply_attr) {
|
|
364
|
+
const expression = attribute_expression(apply_attr);
|
|
365
|
+
if (!expression) {
|
|
366
|
+
report(TSRX_STYLE_APPLY_VALUE_ERROR, DIAGNOSTIC_CODES.STYLE_APPLY_VALUE, apply_attr);
|
|
367
|
+
} else if (inside_head || is_resource) {
|
|
368
|
+
report(
|
|
369
|
+
TSRX_STYLE_APPLY_UNSUPPORTED_HOST_ERROR,
|
|
370
|
+
DIAGNOSTIC_CODES.STYLE_APPLY_UNSUPPORTED_HOST,
|
|
371
|
+
apply_attr,
|
|
372
|
+
);
|
|
373
|
+
} else {
|
|
374
|
+
resolve_apply_entry(expression, nearest_scope(path, scopes), resolutions);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
node.metadata.styleApplies = resolutions;
|
|
378
|
+
|
|
379
|
+
if (is_standalone) {
|
|
380
|
+
if (!inside_head && !is_resource) {
|
|
381
|
+
const parent = path.at(-1);
|
|
382
|
+
const in_children_list =
|
|
383
|
+
parent?.type === 'JSXElement' || parent?.type === 'JSXFragment';
|
|
384
|
+
if (walk_state.function_depth === 0 && walk_state.template_depth === 0) {
|
|
385
|
+
report(
|
|
386
|
+
TSRX_STYLE_STANDALONE_AT_MODULE_SCOPE_ERROR,
|
|
387
|
+
DIAGNOSTIC_CODES.STYLE_STANDALONE_AT_MODULE_SCOPE,
|
|
388
|
+
node,
|
|
389
|
+
);
|
|
390
|
+
} else if (!in_children_list) {
|
|
391
|
+
// A block is an output node: as the lone output of a `@{ … }` or
|
|
392
|
+
// control-flow body, or as a statement, it styles nothing. Beside
|
|
393
|
+
// another output it is already the parser's multiple-outputs error.
|
|
394
|
+
report(
|
|
395
|
+
TSRX_STYLE_STANDALONE_NEEDS_FRAGMENT_ERROR,
|
|
396
|
+
DIAGNOSTIC_CODES.STYLE_STANDALONE_NEEDS_FRAGMENT,
|
|
397
|
+
node,
|
|
398
|
+
);
|
|
399
|
+
} else if (walk_state.container_depth === 0 && !node.openingElement.selfClosing) {
|
|
400
|
+
// Raw CSS in `<style>` is TSRX template syntax: outside every
|
|
401
|
+
// `@{ … }` and control-flow body the file is plain TSX, where a
|
|
402
|
+
// `<style>` takes an expression child instead. A self-closing
|
|
403
|
+
// `<style apply={…} />` holds no CSS text and is ordinary JSX.
|
|
404
|
+
report(
|
|
405
|
+
TSRX_STYLE_STANDALONE_OUTSIDE_TEMPLATE_ERROR,
|
|
406
|
+
DIAGNOSTIC_CODES.STYLE_STANDALONE_OUTSIDE_TEMPLATE,
|
|
407
|
+
node,
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
standalone.push(node);
|
|
411
|
+
}
|
|
412
|
+
} else {
|
|
413
|
+
assigned.push(node);
|
|
414
|
+
const parent = path.at(-1);
|
|
415
|
+
const grandparent = path.at(-2);
|
|
416
|
+
/** @type {AST.VariableDeclarator | null} */
|
|
417
|
+
let declarator = null;
|
|
418
|
+
if (parent?.type === 'VariableDeclarator') {
|
|
419
|
+
declarator = parent;
|
|
420
|
+
} else if (parent?.type === 'Property' && grandparent?.type === 'ObjectExpression') {
|
|
421
|
+
const holder = path.at(-3);
|
|
422
|
+
if (holder?.type === 'VariableDeclarator') declarator = holder;
|
|
423
|
+
}
|
|
424
|
+
const declared_name = declarator?.id.type === 'Identifier' ? declarator.id.name : null;
|
|
425
|
+
const declaration_index = declarator ? path.indexOf(declarator) : -1;
|
|
426
|
+
const export_parent = declaration_index > 0 ? path[declaration_index - 2] : null;
|
|
427
|
+
node.metadata.styleExported =
|
|
428
|
+
parent?.type === 'ExportDefaultDeclaration' ||
|
|
429
|
+
export_parent?.type === 'ExportNamedDeclaration' ||
|
|
430
|
+
(declared_name !== null && exported_names.has(declared_name));
|
|
431
|
+
// Reading `theme.$class` opts an element (or a child component's
|
|
432
|
+
// element, through a prop) into the block's whole stylesheet, so
|
|
433
|
+
// such a block is a theme: every selector stays, like `apply`.
|
|
434
|
+
node.metadata.styleClassRead =
|
|
435
|
+
declared_name !== null &&
|
|
436
|
+
is_class_read(nearest_scope(path, scopes)?.get(declared_name) ?? null);
|
|
437
|
+
|
|
438
|
+
const stylesheet = get_style_element_stylesheet(node);
|
|
439
|
+
if (stylesheet && get_style_class_map_names(stylesheet).includes('$class')) {
|
|
440
|
+
report(
|
|
441
|
+
TSRX_STYLE_RESERVED_CLASS_KEY_ERROR,
|
|
442
|
+
DIAGNOSTIC_CODES.STYLE_RESERVED_CLASS_KEY,
|
|
443
|
+
node,
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
next();
|
|
449
|
+
},
|
|
450
|
+
}),
|
|
451
|
+
);
|
|
452
|
+
|
|
453
|
+
for (const node of assigned) {
|
|
454
|
+
node.metadata.styleKind =
|
|
455
|
+
node.metadata.styleExported || node.metadata.styleApplied || node.metadata.styleClassRead
|
|
456
|
+
? 'theme'
|
|
457
|
+
: 'class-map';
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/** @type {StyleAnalysis} */
|
|
461
|
+
const styles = { assigned, standalone };
|
|
462
|
+
/** @type {{ metadata?: { styles?: StyleAnalysis } }} */ (ast).metadata = {
|
|
463
|
+
.../** @type {{ metadata?: object }} */ (ast).metadata,
|
|
464
|
+
styles,
|
|
465
|
+
};
|
|
466
|
+
return styles;
|
|
467
|
+
}
|
|
@@ -32,6 +32,48 @@ export const TSRX_FORGOTTEN_STATEMENT_CONTAINER_ERROR =
|
|
|
32
32
|
"This TSRX template output is unused. Return it, assign it to a value that is rendered, or make it part of the rendered output of a function '@{...}' body.";
|
|
33
33
|
export const TSRX_UNSUPPORTED_LAZY_ASSIGNMENT_POSITION_ERROR =
|
|
34
34
|
'Lazy destructuring assignments require a directly lazy target as a standalone statement inside a program, block, TSRX code block, or switch case.';
|
|
35
|
+
export const TSRX_STYLE_APPLY_VALUE_ERROR =
|
|
36
|
+
"The 'apply' attribute of a <style> block requires an expression value: apply={theme} or apply={[a, b]}.";
|
|
37
|
+
export const TSRX_STYLE_APPLY_DUPLICATE_ERROR =
|
|
38
|
+
"A <style> block accepts a single 'apply' attribute; pass several themes as an array: apply={[a, b]}.";
|
|
39
|
+
export const TSRX_STYLE_APPLY_UNSUPPORTED_HOST_ERROR =
|
|
40
|
+
"The 'apply' attribute is only supported on scoped <style> blocks, not on <head> styles or resource styles.";
|
|
41
|
+
export const TSRX_STYLE_RESERVED_CLASS_KEY_ERROR =
|
|
42
|
+
"'$class' is reserved on assigned <style> blocks for the block's scope hash; rename the '.$class' selector.";
|
|
43
|
+
export const TSRX_STYLE_STANDALONE_AT_MODULE_SCOPE_ERROR =
|
|
44
|
+
'A standalone <style> block is only allowed inside a template scope. At module scope assign it: const theme = <style>…</style>.';
|
|
45
|
+
export const TSRX_STYLE_STANDALONE_NEEDS_FRAGMENT_ERROR =
|
|
46
|
+
'A standalone <style> block must be a child of an element or a fragment. Wrap it with the output it styles in a fragment: <><style>…</style><div>…</div></>.';
|
|
47
|
+
export const TSRX_STYLE_STANDALONE_OUTSIDE_TEMPLATE_ERROR =
|
|
48
|
+
'A standalone <style> block with CSS text is TSRX template syntax and needs an enclosing @{ … } body or an @if/@for/@switch/@try body. In plain TSX give <style> an expression child instead: <style>{css}</style>. To declare a reusable block here, assign it: const theme = <style>…</style>.';
|
|
49
|
+
export const TSRX_CSS_GLOBAL_NESTED_IN_PSEUDOCLASS_ERROR =
|
|
50
|
+
'A :global selector cannot be inside a pseudoclass.';
|
|
51
|
+
export const TSRX_CSS_GLOBAL_MIDDLE_PLACEMENT_ERROR =
|
|
52
|
+
':global(...) can be at the start or end of a selector sequence, but not in the middle.';
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param {string} name
|
|
56
|
+
* @returns {string}
|
|
57
|
+
*/
|
|
58
|
+
export function tsrx_style_apply_target_error(name) {
|
|
59
|
+
return `'${name}' is not a style block. An 'apply' target must be a variable, import, or member holding an assigned <style> block.`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @param {string} name
|
|
64
|
+
* @returns {string}
|
|
65
|
+
*/
|
|
66
|
+
export function tsrx_style_apply_before_declaration_error(name) {
|
|
67
|
+
return `'${name}' is applied before its declaration. Declare the style block before the block that applies it.`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @param {string} name
|
|
72
|
+
* @returns {string}
|
|
73
|
+
*/
|
|
74
|
+
export function tsrx_style_unknown_attribute_error(name) {
|
|
75
|
+
return `Unknown <style> attribute '${name}'. Scoped style blocks accept 'ref' and 'apply'.`;
|
|
76
|
+
}
|
|
35
77
|
|
|
36
78
|
const invalid_nestings = {
|
|
37
79
|
// <p> cannot contain block-level elements
|
|
@@ -434,3 +476,18 @@ export function validate_nesting(element, context, errors) {
|
|
|
434
476
|
}
|
|
435
477
|
}
|
|
436
478
|
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Report a style diagnostic through the shared error channel so editors get
|
|
482
|
+
* positions and `@tsrx-ignore` applies.
|
|
483
|
+
*
|
|
484
|
+
* @param {string} message
|
|
485
|
+
* @param {string} code
|
|
486
|
+
* @param {AST.Node} node
|
|
487
|
+
* @param {string | null} filename
|
|
488
|
+
* @param {CompileError[] | undefined} errors
|
|
489
|
+
* @param {AST.CommentWithLocation[] | undefined} comments
|
|
490
|
+
*/
|
|
491
|
+
export function validate_style(message, code, node, filename, errors, comments) {
|
|
492
|
+
error(message, filename, node, errors, comments, code);
|
|
493
|
+
}
|
package/src/diagnostics.js
CHANGED
|
@@ -6,4 +6,26 @@ export const DIAGNOSTIC_CODES = {
|
|
|
6
6
|
TEMPLATE_RETURN_STATEMENT: 'tsrx-template-return-statement',
|
|
7
7
|
FORGOTTEN_STATEMENT_CONTAINER: 'tsrx-forgotten-statement-container',
|
|
8
8
|
UNSUPPORTED_LAZY_ASSIGNMENT_POSITION: 'tsrx-unsupported-lazy-assignment-position',
|
|
9
|
+
/** `<style apply>` carries no expression value. */
|
|
10
|
+
STYLE_APPLY_VALUE: 'tsrx-style-apply-value',
|
|
11
|
+
/** An `apply` entry is not an identifier, member, or array of those, or does not resolve to a style block. */
|
|
12
|
+
STYLE_APPLY_TARGET: 'tsrx-style-apply-target',
|
|
13
|
+
/** An `apply` target is declared after the applying block in source order. */
|
|
14
|
+
STYLE_APPLY_BEFORE_DECLARATION: 'tsrx-style-apply-before-declaration',
|
|
15
|
+
/** Two `apply` attributes on one `<style>` block. */
|
|
16
|
+
STYLE_APPLY_DUPLICATE: 'tsrx-style-apply-duplicate',
|
|
17
|
+
/** `apply` on a `<head>` style or a resource (`href`) style. */
|
|
18
|
+
STYLE_APPLY_UNSUPPORTED_HOST: 'tsrx-style-apply-unsupported-host',
|
|
19
|
+
/** An assigned style block authors a `.$class` class selector. */
|
|
20
|
+
STYLE_RESERVED_CLASS_KEY: 'tsrx-style-reserved-class-key',
|
|
21
|
+
/** A standalone `<style>` block at module scope. */
|
|
22
|
+
STYLE_STANDALONE_AT_MODULE_SCOPE: 'tsrx-style-standalone-at-module-scope',
|
|
23
|
+
/** A standalone `<style>` block with CSS text outside any `@{ … }` or control-flow body. */
|
|
24
|
+
STYLE_STANDALONE_OUTSIDE_TEMPLATE: 'tsrx-style-standalone-outside-template',
|
|
25
|
+
/** A standalone `<style>` block in a statement slot: the lone output of a `@{ … }` or control-flow body, or a statement. */
|
|
26
|
+
STYLE_STANDALONE_NEEDS_FRAGMENT: 'tsrx-style-standalone-needs-fragment',
|
|
27
|
+
/** A `<style>` attribute other than `ref` and `apply`. */
|
|
28
|
+
STYLE_UNKNOWN_ATTRIBUTE: 'tsrx-style-unknown-attribute',
|
|
29
|
+
/** `:global` used where the scoping rules do not allow it. */
|
|
30
|
+
CSS_GLOBAL_PLACEMENT: 'tsrx-css-global-placement',
|
|
9
31
|
};
|
package/src/index.js
CHANGED
|
@@ -186,6 +186,7 @@ export {
|
|
|
186
186
|
collect_style_ref_attributes as collectStyleRefAttributes,
|
|
187
187
|
create_style_class_map as createStyleClassMap,
|
|
188
188
|
create_style_class_map_from_stylesheet as createStyleClassMapFromStylesheet,
|
|
189
|
+
build_style_class_map as buildStyleClassMap,
|
|
189
190
|
create_style_ref_setup_statements as createStyleRefSetupStatements,
|
|
190
191
|
get_style_element_stylesheet as getStyleElementStylesheet,
|
|
191
192
|
} from './transform/style-ref.js';
|
|
@@ -251,6 +252,7 @@ export {
|
|
|
251
252
|
// Analyze
|
|
252
253
|
export { analyze_css as analyzeCss } from './analyze/css-analyze.js';
|
|
253
254
|
export { prune_css as pruneCss } from './analyze/prune.js';
|
|
255
|
+
export { create_scope_root as createScopeRoot } from './transform/jsx/style-scopes.js';
|
|
254
256
|
export {
|
|
255
257
|
TSRX_DO_WHILE_STATEMENT_ERROR,
|
|
256
258
|
TSRX_FORGOTTEN_STATEMENT_CONTAINER_ERROR,
|
|
@@ -279,4 +281,20 @@ export {
|
|
|
279
281
|
validate_forgotten_statement_container as validateForgottenStatementContainer,
|
|
280
282
|
validate_nesting as validateNesting,
|
|
281
283
|
is_template_value_position as isTemplateValuePosition,
|
|
284
|
+
TSRX_STYLE_APPLY_VALUE_ERROR,
|
|
285
|
+
TSRX_STYLE_APPLY_DUPLICATE_ERROR,
|
|
286
|
+
TSRX_STYLE_APPLY_UNSUPPORTED_HOST_ERROR,
|
|
287
|
+
TSRX_STYLE_RESERVED_CLASS_KEY_ERROR,
|
|
288
|
+
TSRX_STYLE_STANDALONE_AT_MODULE_SCOPE_ERROR,
|
|
289
|
+
TSRX_STYLE_STANDALONE_OUTSIDE_TEMPLATE_ERROR,
|
|
290
|
+
TSRX_STYLE_STANDALONE_NEEDS_FRAGMENT_ERROR,
|
|
291
|
+
TSRX_CSS_GLOBAL_NESTED_IN_PSEUDOCLASS_ERROR,
|
|
292
|
+
TSRX_CSS_GLOBAL_MIDDLE_PLACEMENT_ERROR,
|
|
293
|
+
tsrx_style_apply_target_error as tsrxStyleApplyTargetError,
|
|
294
|
+
tsrx_style_apply_before_declaration_error as tsrxStyleApplyBeforeDeclarationError,
|
|
295
|
+
tsrx_style_unknown_attribute_error as tsrxStyleUnknownAttributeError,
|
|
282
296
|
} from './analyze/validation.js';
|
|
297
|
+
export {
|
|
298
|
+
analyze_styles as analyzeStyles,
|
|
299
|
+
is_standalone_style_position as isStandaloneStylePosition,
|
|
300
|
+
} from './analyze/style-analyze.js';
|