@tsrx/core 0.1.64 → 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
package/README.md
CHANGED
|
@@ -64,7 +64,32 @@ here and keeps package docs focused on the core parser API.
|
|
|
64
64
|
(`import`, `prop`, `let`, `const`, `function`, `for_pattern`, …).
|
|
65
65
|
- **AST utilities** — pattern walkers, identifier extraction, builders, location
|
|
66
66
|
helpers, obfuscation helpers.
|
|
67
|
-
- **CSS support** — `parseStyle`, `analyzeCss`, `renderStylesheets`.
|
|
67
|
+
- **CSS support** — `parseStyle`, `analyzeCss`, `renderStylesheets`. CSS node
|
|
68
|
+
offsets are relative to the style body; a sheet parsed with a `body` origin
|
|
69
|
+
(every sheet `parseModule` produces) records `sourceStart` and a file-relative
|
|
70
|
+
`loc`, and `analyzeCss` anchors its `:global` placement diagnostics on the
|
|
71
|
+
selector with file-relative positions. Pass `{ errors, comments }` to collect
|
|
72
|
+
them instead of throwing.
|
|
73
|
+
- **Scoped styles** — `analyzeTsrx` resolves every `<style>` block. A standalone
|
|
74
|
+
block is a child of an element or fragment and is scoped to its siblings: it
|
|
75
|
+
styles the items beside it and everything below them, never the element that
|
|
76
|
+
contains it, and the compiler adds a hash class to those elements so the block's
|
|
77
|
+
selectors match only there. A block is an output node, so a block that is the
|
|
78
|
+
lone output of a `@{ … }` or control-flow body is
|
|
79
|
+
`STYLE_STANDALONE_NEEDS_FRAGMENT`. Raw CSS is TSRX template syntax, so a block
|
|
80
|
+
with CSS in it outside every `@{ … }`/control-flow body is
|
|
81
|
+
`STYLE_STANDALONE_OUTSIDE_TEMPLATE`; plain-TSX `<style>{css}</style>` is an
|
|
82
|
+
ordinary element. Assigned blocks (`const theme = <style>…</style>`) are
|
|
83
|
+
classified as `theme` (exported, applied, or `$class` read) or `class-map`, and
|
|
84
|
+
`apply` targets are resolved through real bindings, declared before use. Results
|
|
85
|
+
ride on each block's `metadata` (`styleKind`, `styleApplies`, `styleApplied`,
|
|
86
|
+
`styleExported`) and on `program.metadata.styles`, and the analysis result
|
|
87
|
+
exposes `scopes`. Target compilers use `prepareStylesheetForRender(sheet, mode)`
|
|
88
|
+
with `mode: 'scope' | 'class-map' | 'theme'` (a boolean still means
|
|
89
|
+
`class-map`/`scope`) and `createStyleClassMapFromStylesheet(sheet, options)`,
|
|
90
|
+
whose object starts with `$class` and accepts `{ applied }` for composed themes.
|
|
91
|
+
Style diagnostics use the `STYLE_*` and `CSS_GLOBAL_PLACEMENT` codes in
|
|
92
|
+
`DIAGNOSTIC_CODES`.
|
|
68
93
|
- **HTML helpers** — `isVoidElement`, `isBooleanAttribute`, `isDomProperty`,
|
|
69
94
|
`validateNesting`.
|
|
70
95
|
- **Event helpers** — delegated-event utilities, event-name normalization.
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"description": "Core compiler infrastructure for TSRX syntax",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Dominic Gannaway",
|
|
6
|
-
"version": "0.1.
|
|
6
|
+
"version": "0.1.66",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
@@ -66,7 +66,9 @@
|
|
|
66
66
|
"./test-harness/dep-scan": "./tests/shared/dep-scan.js",
|
|
67
67
|
"./test-harness/runtime/*": "./tests/shared/runtime/*.js",
|
|
68
68
|
"./test-harness/runtime/*.js": "./tests/shared/runtime/*.js",
|
|
69
|
-
"./test-harness/runtime/*.tsrx": "./tests/shared/runtime/*.tsrx"
|
|
69
|
+
"./test-harness/runtime/*.tsrx": "./tests/shared/runtime/*.tsrx",
|
|
70
|
+
"./test-harness/scoped-styles-fixtures": "./tests/shared/scoped-styles-fixtures.js",
|
|
71
|
+
"./test-harness/style-syntax": "./tests/utils/fixtures/style-syntax.js"
|
|
70
72
|
},
|
|
71
73
|
"dependencies": {
|
|
72
74
|
"@jridgewell/sourcemap-codec": "^1.5.5",
|
|
@@ -76,10 +78,9 @@
|
|
|
76
78
|
"@types/estree": "^1.0.8",
|
|
77
79
|
"acorn": "^8.17.0",
|
|
78
80
|
"esrap": "^2.3.2",
|
|
79
|
-
"is-reference": "^3.0.3",
|
|
80
81
|
"magic-string": "^0.30.18",
|
|
81
82
|
"zimmerframe": "^1.1.2",
|
|
82
|
-
"@tsrx/runtime": "0.1.
|
|
83
|
+
"@tsrx/runtime": "0.1.5"
|
|
83
84
|
},
|
|
84
85
|
"devDependencies": {
|
|
85
86
|
"@types/node": "^24.3.0",
|
|
@@ -92,6 +93,9 @@
|
|
|
92
93
|
},
|
|
93
94
|
"files": [
|
|
94
95
|
"src",
|
|
95
|
-
"types"
|
|
96
|
+
"types",
|
|
97
|
+
"tests/fixtures/scoped-styles",
|
|
98
|
+
"tests/shared/scoped-styles-fixtures.js",
|
|
99
|
+
"tests/utils/fixtures/style-syntax.js"
|
|
96
100
|
]
|
|
97
101
|
}
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
/** @import * as AST from 'estree' */
|
|
2
|
+
/** @import { CompileError } from '../../types/index' */
|
|
2
3
|
|
|
3
4
|
import { walk } from 'zimmerframe';
|
|
5
|
+
import { error } from '../errors.js';
|
|
6
|
+
import { DIAGNOSTIC_CODES } from '../diagnostics.js';
|
|
7
|
+
import { css_node_source_position } from '../parse/style.js';
|
|
8
|
+
import {
|
|
9
|
+
TSRX_CSS_GLOBAL_MIDDLE_PLACEMENT_ERROR,
|
|
10
|
+
TSRX_CSS_GLOBAL_NESTED_IN_PSEUDOCLASS_ERROR,
|
|
11
|
+
} from './validation.js';
|
|
4
12
|
|
|
5
13
|
/**
|
|
6
14
|
* True if is `:global` without arguments
|
|
@@ -35,10 +43,41 @@ function is_global(relative_selector) {
|
|
|
35
43
|
}
|
|
36
44
|
|
|
37
45
|
/**
|
|
38
|
-
* Analyze CSS and set metadata for global selectors
|
|
46
|
+
* Analyze CSS and set metadata for global selectors.
|
|
47
|
+
*
|
|
48
|
+
* `:global` placement problems are reported through `error()` with the
|
|
49
|
+
* `CSS_GLOBAL_PLACEMENT` code: pushed onto `errors` when given (and skipped
|
|
50
|
+
* over a suppressing comment when `comments` is given too), thrown otherwise.
|
|
51
|
+
* When `css` is a sheet parsed with a `body` origin (every sheet `parseModule`
|
|
52
|
+
* produces), the diagnostic carries file-relative `pos` / `end` / `loc`; the
|
|
53
|
+
* `fileName` defaults to the sheet's.
|
|
54
|
+
*
|
|
39
55
|
* @param {AST.CSS.Node} css - The CSS AST
|
|
56
|
+
* @param {{
|
|
57
|
+
* filename?: string | null,
|
|
58
|
+
* errors?: CompileError[],
|
|
59
|
+
* comments?: AST.CommentWithLocation[],
|
|
60
|
+
* }} [options]
|
|
40
61
|
*/
|
|
41
|
-
export function analyze_css(css) {
|
|
62
|
+
export function analyze_css(css, options = {}) {
|
|
63
|
+
const sheet = css.type === 'StyleSheet' ? css : null;
|
|
64
|
+
const filename = options.filename ?? sheet?.filename ?? null;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @param {string} message
|
|
68
|
+
* @param {AST.CSS.Node} node
|
|
69
|
+
*/
|
|
70
|
+
function report(message, node) {
|
|
71
|
+
error(
|
|
72
|
+
message,
|
|
73
|
+
filename,
|
|
74
|
+
css_node_source_position(sheet, node),
|
|
75
|
+
options.errors,
|
|
76
|
+
options.comments,
|
|
77
|
+
DIAGNOSTIC_CODES.CSS_GLOBAL_PLACEMENT,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
42
81
|
walk(css, /** @type {{ rule: AST.CSS.Rule | null }} */ ({ rule: null }), {
|
|
43
82
|
Rule(node, context) {
|
|
44
83
|
node.metadata.parent_rule = context.state.rule;
|
|
@@ -98,7 +137,7 @@ export function analyze_css(css) {
|
|
|
98
137
|
is_nested &&
|
|
99
138
|
!(/** @type {AST.CSS.PseudoClassSelector} */ (global.selectors[0]).args)
|
|
100
139
|
) {
|
|
101
|
-
|
|
140
|
+
report(TSRX_CSS_GLOBAL_NESTED_IN_PSEUDOCLASS_ERROR, global.selectors[0]);
|
|
102
141
|
}
|
|
103
142
|
|
|
104
143
|
const idx = node.children.indexOf(global);
|
|
@@ -107,9 +146,8 @@ export function analyze_css(css) {
|
|
|
107
146
|
// ensure `:global(...)` is not used in the middle of a selector (but multiple `global(...)` in sequence are ok)
|
|
108
147
|
for (let i = idx + 1; i < node.children.length; i++) {
|
|
109
148
|
if (!is_global(node.children[i])) {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
);
|
|
149
|
+
report(TSRX_CSS_GLOBAL_MIDDLE_PLACEMENT_ERROR, first);
|
|
150
|
+
break;
|
|
113
151
|
}
|
|
114
152
|
}
|
|
115
153
|
}
|
package/src/analyze/index.js
CHANGED
|
@@ -15,6 +15,8 @@ import {
|
|
|
15
15
|
validate_forgotten_statement_container,
|
|
16
16
|
validate_unsupported_lazy_assignment_position,
|
|
17
17
|
} from './validation.js';
|
|
18
|
+
import { create_scopes, ScopeRoot } from '../scope.js';
|
|
19
|
+
import { analyze_styles } from './style-analyze.js';
|
|
18
20
|
|
|
19
21
|
/**
|
|
20
22
|
* Find the first authored lazy pattern along an assignment target's binding
|
|
@@ -220,5 +222,16 @@ export function analyze_tsrx(ast, filename, options = {}) {
|
|
|
220
222
|
|
|
221
223
|
walk(ast, state, visitors);
|
|
222
224
|
|
|
223
|
-
|
|
225
|
+
// Style `apply` targets resolve through real bindings. Scope diagnostics
|
|
226
|
+
// (duplicate declarations, reserved names) stay with the compilers that
|
|
227
|
+
// already report them, so this run collects into a private list.
|
|
228
|
+
const { scope, scopes } = create_scopes(ast, new ScopeRoot(), null, {
|
|
229
|
+
filename: /** @type {string} */ (filename ?? null),
|
|
230
|
+
collect: true,
|
|
231
|
+
errors: [],
|
|
232
|
+
comments,
|
|
233
|
+
});
|
|
234
|
+
const styles = analyze_styles(ast, scopes, state);
|
|
235
|
+
|
|
236
|
+
return { ast, errors, comments, scope, scopes, styles };
|
|
224
237
|
}
|
|
@@ -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
|
+
}
|