@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
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.67",
|
|
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
|
}
|
package/src/analyze/prune.js
CHANGED
|
@@ -422,18 +422,14 @@ function can_render_dynamic_content(element, check_classes = false) {
|
|
|
422
422
|
function get_possible_element_siblings(node, direction, adjacent_only) {
|
|
423
423
|
/** @type {Map<AST.TSRXJSXElement, boolean>} */
|
|
424
424
|
const siblings = new Map();
|
|
425
|
-
const
|
|
425
|
+
const container = get_sibling_container(node);
|
|
426
426
|
|
|
427
|
-
if (
|
|
427
|
+
if (container === null) {
|
|
428
428
|
return siblings;
|
|
429
429
|
}
|
|
430
430
|
|
|
431
|
-
// Get the container that holds the siblings
|
|
432
|
-
const container = node_children(parent);
|
|
433
431
|
const node_index = container.indexOf(node);
|
|
434
432
|
|
|
435
|
-
if (node_index === -1) return siblings;
|
|
436
|
-
|
|
437
433
|
// Determine which siblings to check based on direction
|
|
438
434
|
let start, end, step;
|
|
439
435
|
if (direction === FORWARD) {
|
|
@@ -532,9 +528,8 @@ function apply_combinator(relative_selector, rest_selectors, rule, node, directi
|
|
|
532
528
|
sibling_matched = true;
|
|
533
529
|
} else {
|
|
534
530
|
// Check if there are any elements after this component that could match the remaining selectors
|
|
535
|
-
const
|
|
536
|
-
if (
|
|
537
|
-
const container = node_children(parent);
|
|
531
|
+
const container = get_sibling_container(node);
|
|
532
|
+
if (container !== null) {
|
|
538
533
|
const component_index = container.indexOf(possible_sibling);
|
|
539
534
|
|
|
540
535
|
// For adjacent combinator, only check immediate next element
|
|
@@ -578,6 +573,38 @@ function apply_combinator(relative_selector, rest_selectors, rule, node, directi
|
|
|
578
573
|
return true;
|
|
579
574
|
}
|
|
580
575
|
}
|
|
576
|
+
/**
|
|
577
|
+
* The children list an element sits in — that of the nearest ancestor on its
|
|
578
|
+
* path whose children include it, which is its parent element, a fragment
|
|
579
|
+
* (an authored `<>…</>`, a control-flow branch's output, or the synthetic
|
|
580
|
+
* root of a style scope), or `null` when the element has no container.
|
|
581
|
+
*
|
|
582
|
+
* Sibling combinators (`+`, `~`) read this list. Ancestor combinators keep
|
|
583
|
+
* using {@link get_element_parent}: a fragment is not an element, and a
|
|
584
|
+
* scope's root fragment stands for the container that a scoped block never
|
|
585
|
+
* styles.
|
|
586
|
+
*
|
|
587
|
+
* @param {AST.TSRXElementNode} node
|
|
588
|
+
* @returns {AST.Node[] | null}
|
|
589
|
+
*/
|
|
590
|
+
function get_sibling_container(node) {
|
|
591
|
+
const path = node.metadata?.path;
|
|
592
|
+
if (!path || !path.length) {
|
|
593
|
+
return null;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
let i = path.length;
|
|
597
|
+
|
|
598
|
+
while (i--) {
|
|
599
|
+
const children = node_children(path[i]);
|
|
600
|
+
if (children.includes(node)) {
|
|
601
|
+
return children;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
return null;
|
|
606
|
+
}
|
|
607
|
+
|
|
581
608
|
/**
|
|
582
609
|
* @param {AST.TSRXElementNode} node
|
|
583
610
|
* @returns {AST.TSRXJSXElement | null}
|