@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.
Files changed (52) hide show
  1. package/README.md +26 -1
  2. package/package.json +9 -5
  3. package/src/analyze/css-analyze.js +44 -6
  4. package/src/analyze/index.js +14 -1
  5. package/src/analyze/style-analyze.js +467 -0
  6. package/src/analyze/validation.js +57 -0
  7. package/src/diagnostics.js +22 -0
  8. package/src/index.js +17 -0
  9. package/src/parse/style.js +97 -7
  10. package/src/plugin.js +145 -47
  11. package/src/scope.js +1 -1
  12. package/src/transform/jsx/index.js +94 -417
  13. package/src/transform/jsx/style-scopes.js +842 -0
  14. package/src/transform/scoping.js +129 -79
  15. package/src/transform/segments.js +16 -4
  16. package/src/transform/style-ref.js +74 -13
  17. package/src/transform/stylesheet.js +2 -1
  18. package/src/utils/is-reference.js +59 -0
  19. package/tests/fixtures/scoped-styles/README.md +71 -0
  20. package/tests/fixtures/scoped-styles/apply-forms.expected.json +18 -0
  21. package/tests/fixtures/scoped-styles/apply-forms.tsrx +69 -0
  22. package/tests/fixtures/scoped-styles/assigned-positions.expected.json +29 -0
  23. package/tests/fixtures/scoped-styles/assigned-positions.tsrx +90 -0
  24. package/tests/fixtures/scoped-styles/class-opt-in.expected.json +13 -0
  25. package/tests/fixtures/scoped-styles/class-opt-in.tsrx +39 -0
  26. package/tests/fixtures/scoped-styles/control-flow-else-if.expected.json +11 -0
  27. package/tests/fixtures/scoped-styles/control-flow-else-if.tsrx +26 -0
  28. package/tests/fixtures/scoped-styles/control-flow.expected.json +17 -0
  29. package/tests/fixtures/scoped-styles/control-flow.tsrx +102 -0
  30. package/tests/fixtures/scoped-styles/cross-module-apply.expected.json +14 -0
  31. package/tests/fixtures/scoped-styles/cross-module-apply.tsrx +48 -0
  32. package/tests/fixtures/scoped-styles/element-rooted-templates.expected.json +12 -0
  33. package/tests/fixtures/scoped-styles/element-rooted-templates.tsrx +33 -0
  34. package/tests/fixtures/scoped-styles/precedence.expected.json +11 -0
  35. package/tests/fixtures/scoped-styles/precedence.tsrx +45 -0
  36. package/tests/fixtures/scoped-styles/rfc-opening-example/panel.expected.json +12 -0
  37. package/tests/fixtures/scoped-styles/rfc-opening-example/panel.tsrx +46 -0
  38. package/tests/fixtures/scoped-styles/rfc-opening-example/theme.expected.json +9 -0
  39. package/tests/fixtures/scoped-styles/rfc-opening-example/theme.tsrx +24 -0
  40. package/tests/fixtures/scoped-styles/search-panel.expected.json +11 -0
  41. package/tests/fixtures/scoped-styles/search-panel.tsrx +48 -0
  42. package/tests/fixtures/scoped-styles/sibling-scope.expected.json +11 -0
  43. package/tests/fixtures/scoped-styles/sibling-scope.tsrx +44 -0
  44. package/tests/fixtures/scoped-styles/sibling-scopes.expected.json +12 -0
  45. package/tests/fixtures/scoped-styles/sibling-scopes.tsrx +51 -0
  46. package/tests/fixtures/scoped-styles/theme-composition.expected.json +14 -0
  47. package/tests/fixtures/scoped-styles/theme-composition.tsrx +45 -0
  48. package/tests/fixtures/scoped-styles/theme-diamond.expected.json +10 -0
  49. package/tests/fixtures/scoped-styles/theme-diamond.tsrx +18 -0
  50. package/tests/shared/scoped-styles-fixtures.js +67 -0
  51. package/tests/utils/fixtures/style-syntax.js +519 -0
  52. package/types/index.d.ts +85 -0
@@ -0,0 +1,29 @@
1
+ {
2
+ "elements": {
3
+ "{local.local}": [],
4
+ "{inBody.inBody}": [],
5
+ "{inNested.inNested}": [],
6
+ "{helper().inFunction}": []
7
+ },
8
+ "cssOrder": [
9
+ "exported",
10
+ "local",
11
+ "reexported",
12
+ "renamed",
13
+ "inFunction",
14
+ "inBody",
15
+ "inBlock",
16
+ "inNested"
17
+ ],
18
+ "pruned": ["em", "i", "b", "u", "s"],
19
+ "classMaps": {
20
+ "exported": ["exported"],
21
+ "local": ["local"],
22
+ "reexported": ["reexported"],
23
+ "renamed": ["renamed"],
24
+ "inFunction": ["inFunction"],
25
+ "inBody": ["inBody"],
26
+ "inBlock": ["inBlock"],
27
+ "inNested": ["inNested"]
28
+ }
29
+ }
@@ -0,0 +1,90 @@
1
+ // Assigned blocks in every declaration position. Exported (directly or via
2
+ // an export list) blocks keep every selector; local unapplied ones keep only
3
+ // their class selectors.
4
+ export const exported = <style>
5
+ .exported {
6
+ --label: exported;
7
+ }
8
+ div {
9
+ color: red;
10
+ }
11
+ </style>;
12
+
13
+ const local = <style>
14
+ .local {
15
+ --label: local;
16
+ }
17
+ em {
18
+ color: red;
19
+ }
20
+ </style>;
21
+
22
+ const reexported = <style>
23
+ .reexported {
24
+ --label: reexported;
25
+ }
26
+ span {
27
+ color: red;
28
+ }
29
+ </style>;
30
+ export { reexported };
31
+
32
+ const renamed = <style>
33
+ .renamed {
34
+ --label: renamed;
35
+ }
36
+ strong {
37
+ color: red;
38
+ }
39
+ </style>;
40
+ export { renamed as alias };
41
+
42
+ function helper() {
43
+ const inFunction = <style>
44
+ .inFunction {
45
+ --label: inFunction;
46
+ }
47
+ i {
48
+ color: red;
49
+ }
50
+ </style>;
51
+ return inFunction;
52
+ }
53
+
54
+ export function Positions() @{
55
+ const inBody = <style>
56
+ .inBody {
57
+ --label: inBody;
58
+ }
59
+ b {
60
+ color: red;
61
+ }
62
+ </style>;
63
+ {
64
+ const inBlock = <style>
65
+ .inBlock {
66
+ --label: inBlock;
67
+ }
68
+ u {
69
+ color: red;
70
+ }
71
+ </style>;
72
+ console.log(inBlock);
73
+ }
74
+ <>
75
+ <div class={local.local}>{'local'}</div>
76
+ <div class={inBody.inBody}>{'body'}</div>
77
+ @{
78
+ const inNested = <style>
79
+ .inNested {
80
+ --label: inNested;
81
+ }
82
+ s {
83
+ color: red;
84
+ }
85
+ </style>;
86
+ <div class={inNested.inNested}>{'nested'}</div>
87
+ }
88
+ <div class={helper().inFunction}>{'function'}</div>
89
+ </>
90
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "elements": {
3
+ "l1 local": ["local"],
4
+ "{`local ${parentClass}`}": ["local"],
5
+ "{theme.$class}": [],
6
+ "{theme.card}": []
7
+ },
8
+ "cssOrder": ["local", "theme"],
9
+ "pruned": [],
10
+ "classMaps": {
11
+ "theme": ["theme"]
12
+ }
13
+ }
@@ -0,0 +1,39 @@
1
+ // Reading `theme.$class` opts single elements into a theme — every selector
2
+ // kept, like an applied block — without `apply` stamping the whole scope; a
3
+ // child component receives it through a prop and stamps it before its own
4
+ // scope hash. Elements that do not read it stay untouched.
5
+ function Card({ parentClass }: { parentClass: string }) @{
6
+ <>
7
+ <style>
8
+ .local {
9
+ --label: local;
10
+ }
11
+ article {
12
+ padding: 0;
13
+ }
14
+ </style>
15
+ <article class={`local ${parentClass}`}>
16
+ <h2 class="l1 local">{'title'}</h2>
17
+ </article>
18
+ </>
19
+ }
20
+
21
+ export function ClassOptIn() @{
22
+ const theme = <style>
23
+ .theme {
24
+ --label: theme;
25
+ }
26
+ div {
27
+ color: blue;
28
+ }
29
+ .card {
30
+ color: red;
31
+ }
32
+ </style>;
33
+ <>
34
+ <Card parentClass={theme.$class} />
35
+ <div class={theme.$class}>{'opted in'}</div>
36
+ <div class={theme.card}>{'card'}</div>
37
+ <p>{'untouched'}</p>
38
+ </>
39
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "elements": {
3
+ "e0 root": ["root"],
4
+ "e1": ["root"],
5
+ "e2 elif": ["root", "elif"],
6
+ "e3": ["root"]
7
+ },
8
+ "cssOrder": ["root", "elif"],
9
+ "pruned": [],
10
+ "classMaps": {}
11
+ }
@@ -0,0 +1,26 @@
1
+ // An `@else if` body renders one output node like any other branch; a
2
+ // fragment there holds its own block and scope.
3
+ export function ElseIf({ a, b }: { a: boolean; b: boolean }) @{
4
+ <>
5
+ <style>
6
+ .root {
7
+ --label: root;
8
+ }
9
+ </style>
10
+ <div class="e0 root">{'root'}</div>
11
+ @if (a) {
12
+ <b class="e1">{'a'}</b>
13
+ } @else if (b) {
14
+ <>
15
+ <style>
16
+ .elif {
17
+ --label: elif;
18
+ }
19
+ </style>
20
+ <b class="e2 elif">{'b'}</b>
21
+ </>
22
+ } @else {
23
+ <b class="e3">{'neither'}</b>
24
+ }
25
+ </>
26
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "elements": {
3
+ "e0 root": ["root"],
4
+ "e1 yes": ["root", "yes"],
5
+ "e2 no": ["root", "no"],
6
+ "e3 item": ["root", "item"],
7
+ "e4 empty": ["root", "empty"],
8
+ "e5 one": ["root", "one"],
9
+ "e6": ["root"],
10
+ "e7 other": ["root", "other"],
11
+ "e8 ok": ["root", "ok"],
12
+ "e9 err": ["root", "err"]
13
+ },
14
+ "cssOrder": ["root", "yes", "no", "item", "empty", "one", "other", "ok", "err"],
15
+ "pruned": [],
16
+ "classMaps": {}
17
+ }
@@ -0,0 +1,102 @@
1
+ // Every directive branch renders one output node; a fragment there is a
2
+ // children list of its own, so its block is a scope whose CSS ships regardless.
3
+ export function ControlFlow({
4
+ ready,
5
+ items,
6
+ kind,
7
+ }: {
8
+ ready: boolean;
9
+ items: string[];
10
+ kind: number;
11
+ }) @{
12
+ <>
13
+ <style>
14
+ .root {
15
+ --label: root;
16
+ }
17
+ </style>
18
+ <div class="e0 root">{'root'}</div>
19
+ @if (ready) {
20
+ <>
21
+ <style>
22
+ .yes {
23
+ --label: yes;
24
+ }
25
+ </style>
26
+ <p class="e1 yes">{'yes'}</p>
27
+ </>
28
+ } @else {
29
+ <>
30
+ <style>
31
+ .no {
32
+ --label: no;
33
+ }
34
+ </style>
35
+ <p class="e2 no">{'no'}</p>
36
+ </>
37
+ }
38
+ @for (const item of items) {
39
+ <>
40
+ <style>
41
+ .item {
42
+ --label: item;
43
+ }
44
+ </style>
45
+ <li class="e3 item">{item}</li>
46
+ </>
47
+ } @empty {
48
+ <>
49
+ <style>
50
+ .empty {
51
+ --label: empty;
52
+ }
53
+ </style>
54
+ <li class="e4 empty">{'none'}</li>
55
+ </>
56
+ }
57
+ @switch (kind) {
58
+ @case 1: {
59
+ <>
60
+ <style>
61
+ .one {
62
+ --label: one;
63
+ }
64
+ </style>
65
+ <b class="e5 one">{'one'}</b>
66
+ </>
67
+ }
68
+ @case 2: {
69
+ <b class="e6">{'two'}</b>
70
+ }
71
+ @default: {
72
+ <>
73
+ <style>
74
+ .other {
75
+ --label: other;
76
+ }
77
+ </style>
78
+ <i class="e7 other">{'other'}</i>
79
+ </>
80
+ }
81
+ }
82
+ @try {
83
+ <>
84
+ <style>
85
+ .ok {
86
+ --label: ok;
87
+ }
88
+ </style>
89
+ <em class="e8 ok">{'ok'}</em>
90
+ </>
91
+ } @catch (error) {
92
+ <>
93
+ <style>
94
+ .err {
95
+ --label: err;
96
+ }
97
+ </style>
98
+ <s class="e9 err">{'err'}</s>
99
+ </>
100
+ }
101
+ </>
102
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "elements": {
3
+ "x1 scope": ["scope", "import:theme", "import:themes.dark"],
4
+ "{active ? 'on' : 'off'}": ["scope", "import:theme", "import:themes.dark"],
5
+ "x2 nested": ["scope", "nested", "import:theme", "import:themes.dark"]
6
+ },
7
+ "cssOrder": ["localBase", "mixed", "scope", "nested"],
8
+ "pruned": [],
9
+ "classMaps": {
10
+ "localBase": ["localBase"],
11
+ "mixed": ["localBase", "import:theme", "mixed"],
12
+ "bundle": ["import:theme", "import:themes.dark"]
13
+ }
14
+ }
@@ -0,0 +1,48 @@
1
+ // Imported themes stay runtime `$class` reads, after every static hash.
2
+ import { theme } from './theme.tsrx';
3
+ import * as themes from './themes.tsrx';
4
+
5
+ const localBase = <style>
6
+ .localBase {
7
+ --label: localBase;
8
+ }
9
+ div {
10
+ color: red;
11
+ }
12
+ </style>;
13
+
14
+ export const mixed = <style apply={[localBase, theme]}>
15
+ .mixed {
16
+ --label: mixed;
17
+ }
18
+ div {
19
+ color: blue;
20
+ }
21
+ </style>;
22
+
23
+ export const bundle = <style apply={[theme, themes.dark]} />;
24
+
25
+ export function CrossModule({ active }: { active: boolean }) @{
26
+ <>
27
+ <style apply={[theme, themes.dark]} />
28
+ <style>
29
+ .scope {
30
+ --label: scope;
31
+ }
32
+ div {
33
+ color: black;
34
+ }
35
+ </style>
36
+ <div class="x1 scope">{'x'}</div>
37
+ <p class={active ? 'on' : 'off'}>@{
38
+ <>
39
+ <style>
40
+ .nested {
41
+ --label: nested;
42
+ }
43
+ </style>
44
+ <b class="x2 nested">{'n'}</b>
45
+ </>
46
+ }</p>
47
+ </>
48
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "elements": {
3
+ "card-root": [],
4
+ "card-text card": ["card"],
5
+ "chip-root": [],
6
+ "chip-text chip": ["chip"],
7
+ "outside": []
8
+ },
9
+ "cssOrder": ["card", "chip"],
10
+ "pruned": ["div"],
11
+ "classMaps": {}
12
+ }
@@ -0,0 +1,33 @@
1
+ // RFC problem 4: an element assigned to a variable inside a `@{ … }` body keeps
2
+ // its block. The block sits in the element's children list, so it styles the
3
+ // children and never the element that contains it. (Outside a `@{ … }` or
4
+ // control-flow body a bodied standalone block is an error.)
5
+ export function Templates() @{
6
+ const card = <div class="card-root">
7
+ <style>
8
+ .card {
9
+ --label: card;
10
+ }
11
+ div {
12
+ margin: 0;
13
+ }
14
+ </style>
15
+ <p class="card-text card">{'card'}</p>
16
+ </div>;
17
+ const chip = <span class="chip-root">
18
+ <style>
19
+ .chip {
20
+ --label: chip;
21
+ }
22
+ b {
23
+ font-weight: bold;
24
+ }
25
+ </style>
26
+ <b class="chip-text chip">{'chip'}</b>
27
+ </span>;
28
+ <>
29
+ {card}
30
+ {chip}
31
+ <p class="outside">{'outside'}</p>
32
+ </>
33
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "elements": {
3
+ "o1 outer": ["outer", "theme"],
4
+ "i1 inner": ["outer", "inner", "theme"]
5
+ },
6
+ "cssOrder": ["theme", "outer", "outer", "inner"],
7
+ "pruned": [],
8
+ "classMaps": {
9
+ "theme": ["theme"]
10
+ }
11
+ }
@@ -0,0 +1,45 @@
1
+ // Ordering: applied theme, then the outer scope's blocks as one contiguous
2
+ // group (even though one sits after the nested scope), then the inner scope.
3
+ const theme = <style>
4
+ .theme {
5
+ --label: theme;
6
+ }
7
+ div {
8
+ color: green;
9
+ }
10
+ </style>;
11
+
12
+ export function Precedence() @{
13
+ <>
14
+ <style apply={theme}>
15
+ .outer {
16
+ --label: outer;
17
+ }
18
+ div {
19
+ color: black;
20
+ }
21
+ </style>
22
+ <div class="o1 outer">{'outer'}</div>
23
+ @{
24
+ <>
25
+ <style>
26
+ .inner {
27
+ --label: inner;
28
+ }
29
+ div {
30
+ color: blue;
31
+ }
32
+ </style>
33
+ <div class="i1 inner">{'inner'}</div>
34
+ </>
35
+ }
36
+ <style>
37
+ .outer {
38
+ --label: outer;
39
+ }
40
+ div {
41
+ font-style: italic;
42
+ }
43
+ </style>
44
+ </>
45
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "elements": {
3
+ "{theme.dark}": ["scopeA", "import:theme"],
4
+ "d1 scopeA": ["scopeA", "import:theme"],
5
+ "p1": ["scopeA", "import:theme"],
6
+ "d2 scopeB": ["scopeA", "scopeB", "import:theme"],
7
+ "p2": ["scopeA", "scopeB", "import:theme"]
8
+ },
9
+ "cssOrder": ["scopeA", "scopeA", "scopeB"],
10
+ "pruned": [],
11
+ "classMaps": {}
12
+ }
@@ -0,0 +1,46 @@
1
+ // panel.tsrx — the component module of the RFC's opening example.
2
+ import { theme } from './theme.tsrx';
3
+
4
+ export function Panel() @{
5
+ <>
6
+ <style apply={theme}>
7
+ /* scope A; also attaches theme.$class to the scope */
8
+ .scopeA {
9
+ --label: scopeA;
10
+ }
11
+ div {
12
+ color: black;
13
+ }
14
+ </style>
15
+
16
+ <span class={theme.dark}>Purple</span>
17
+ <div class="d1 scopeA">Black system-ui: the local rule beats the theme's green</div>
18
+
19
+ <style>
20
+ /* still scope A, so the same hash */
21
+ .scopeA {
22
+ --label: scopeA;
23
+ }
24
+ p {
25
+ margin: 0;
26
+ }
27
+ </style>
28
+ <p class="p1">No margin</p>
29
+
30
+ @{
31
+ <>
32
+ <style>
33
+ /* scope B, nested inside A */
34
+ .scopeB {
35
+ --label: scopeB;
36
+ }
37
+ div {
38
+ font-weight: bold;
39
+ }
40
+ </style>
41
+ <div class="d2 scopeB">Black and bold: A and B both reach here</div>
42
+ <p class="p2">No margin: A reaches here too</p>
43
+ </>
44
+ }
45
+ </>
46
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "elements": {},
3
+ "cssOrder": ["base", "theme"],
4
+ "pruned": [],
5
+ "classMaps": {
6
+ "base": ["base"],
7
+ "theme": ["base", "theme"]
8
+ }
9
+ }
@@ -0,0 +1,24 @@
1
+ // theme.tsrx — the theme module of the RFC's opening example.
2
+ export const base = <style>
3
+ .base {
4
+ --label: base;
5
+ }
6
+ div {
7
+ font-family: system-ui;
8
+ }
9
+ .muted {
10
+ color: gray;
11
+ }
12
+ </style>;
13
+
14
+ export const theme = <style apply={base}>
15
+ .theme {
16
+ --label: theme;
17
+ }
18
+ div {
19
+ color: green;
20
+ }
21
+ .dark {
22
+ color: purple;
23
+ }
24
+ </style>;
@@ -0,0 +1,11 @@
1
+ {
2
+ "elements": {
3
+ "filters panel": ["panel"],
4
+ "filter": ["panel"],
5
+ "list results": ["panel", "results"],
6
+ "result": ["panel", "results"]
7
+ },
8
+ "cssOrder": ["panel", "results"],
9
+ "pruned": [],
10
+ "classMaps": {}
11
+ }
@@ -0,0 +1,48 @@
1
+ // The RFC's SearchPanel: a nested scope with setup code owns its own block.
2
+ export function SearchPanel({
3
+ filters,
4
+ results,
5
+ }: {
6
+ filters: Array<{ label: string }>;
7
+ results: Array<{ title: string }>;
8
+ }) @{
9
+ <>
10
+ <style>
11
+ .panel {
12
+ --label: panel;
13
+ }
14
+ /* reaches both lists: this scope and the nested one */
15
+ li {
16
+ list-style: none;
17
+ padding: 0.25rem 0.5rem;
18
+ }
19
+ </style>
20
+
21
+ <ul class="filters panel">
22
+ @for (const f of filters) {
23
+ <li class="filter">{f.label}</li>
24
+ }
25
+ </ul>
26
+
27
+ @{
28
+ const top = results.slice(0, 10);
29
+ <>
30
+ <style>
31
+ .results {
32
+ --label: results;
33
+ }
34
+ /* reaches only this scope's list, and wins here */
35
+ li {
36
+ padding: 0.75rem 0;
37
+ border-bottom: 1px solid gray;
38
+ }
39
+ </style>
40
+ <ul class="list results">
41
+ @for (const r of top) {
42
+ <li class="result">{r.title}</li>
43
+ }
44
+ </ul>
45
+ </>
46
+ }
47
+ </>
48
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "elements": {
3
+ "status": ["status"],
4
+ "title": ["status", "title"],
5
+ "ok": ["status", "title", "ok"],
6
+ "wait": ["status", "title", "wait"]
7
+ },
8
+ "cssOrder": ["status", "title", "ok", "wait"],
9
+ "pruned": [".status"],
10
+ "classMaps": {}
11
+ }