@astryxdesign/cli 0.4.6 → 0.4.7-canary.019ae5a

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 (73) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +53 -51
  3. package/api/docs/docs.doc.mjs +2 -2
  4. package/api/index.d.mts +1 -1
  5. package/api/index.mjs +1 -1
  6. package/api/search/search.mjs +50 -4
  7. package/api/search/search.test.mjs +71 -0
  8. package/api/template/template.doc.mjs +2 -2
  9. package/api/theme/build/build.mjs +8 -38
  10. package/api/theme/targets/targets.d.mts +18 -0
  11. package/api/theme/targets/targets.mjs +87 -0
  12. package/api/theme/targets/targets.test.mjs +65 -0
  13. package/api/theme/theme.d.mts +1 -0
  14. package/api/theme/theme.mjs +3 -1
  15. package/api/theme/theme.type.d.mts +23 -0
  16. package/api/theme/theme.type.mjs +21 -1
  17. package/api/theme/themeTargets.doc.d.mts +11 -0
  18. package/api/theme/themeTargets.doc.mjs +58 -0
  19. package/api/theme/themeTemplate.doc.mjs +3 -3
  20. package/assets/codemods/transforms/next/__tests__/next-codemods.test.mjs +127 -0
  21. package/assets/codemods/transforms/next/banner-collapsible-content.mjs +171 -0
  22. package/assets/codemods/transforms/next/index.mjs +11 -1
  23. package/assets/docs/README.md +50 -0
  24. package/assets/docs/cli-integrations.doc.mjs +4 -4
  25. package/assets/docs/theme.doc.dense.mjs +1 -1
  26. package/assets/docs/theme.doc.mjs +1 -1
  27. package/assets/docs/typography.doc.mjs +2 -2
  28. package/assets/templates/blocks/components/Banner/BannerCollapsibleContent.doc.mjs +1 -1
  29. package/assets/templates/blocks/components/Banner/BannerCollapsibleContent.tsx +1 -1
  30. package/assets/templates/blocks/components/Dialog/DialogAdaptivePresentation.doc.mjs +25 -0
  31. package/assets/templates/blocks/components/Dialog/DialogAdaptivePresentation.tsx +167 -0
  32. package/assets/templates/blocks/components/Dialog/DialogScrollingContent.tsx +1 -1
  33. package/assets/templates/blocks/components/Step/StepContent.doc.mjs +14 -0
  34. package/assets/templates/blocks/components/Step/StepContent.tsx +32 -0
  35. package/assets/templates/blocks/components/Step/StepIndicator.doc.mjs +14 -0
  36. package/assets/templates/blocks/components/Step/StepIndicator.tsx +60 -0
  37. package/assets/templates/blocks/components/Step/StepShowcase.doc.mjs +15 -0
  38. package/assets/templates/blocks/components/Step/StepShowcase.tsx +26 -0
  39. package/assets/templates/blocks/components/Step/StepStates.doc.mjs +14 -0
  40. package/assets/templates/blocks/components/Step/StepStates.tsx +46 -0
  41. package/assets/templates/blocks/components/Stepper/StepperCustomContent.doc.mjs +22 -0
  42. package/assets/templates/blocks/components/Stepper/StepperCustomContent.tsx +126 -0
  43. package/assets/templates/blocks/components/Stepper/StepperIndicatorModes.doc.mjs +1 -1
  44. package/assets/templates/blocks/components/Stepper/StepperIndicatorModes.tsx +17 -5
  45. package/assets/templates/blocks/components/Stepper/StepperOnTrackHorizontal.doc.mjs +14 -0
  46. package/assets/templates/blocks/components/Stepper/StepperOnTrackHorizontal.tsx +25 -0
  47. package/assets/templates/blocks/components/Stepper/StepperOnTrackVertical.doc.mjs +2 -2
  48. package/assets/templates/blocks/components/Stepper/StepperOnTrackVertical.tsx +1 -1
  49. package/assets/templates/blocks/components/Stepper/StepperShowcase.doc.mjs +1 -1
  50. package/assets/templates/blocks/components/Stepper/StepperShowcase.tsx +6 -7
  51. package/assets/templates/blocks/components/Stepper/StepperStatus.tsx +1 -1
  52. package/assets/templates/pages/mixed-gallery/page.tsx +12 -3
  53. package/assets/templates/pages/table-grouped/page.tsx +151 -144
  54. package/assets/templates/themes/neutral/neutralTheme.ts +13 -8
  55. package/clients/cli/commands/build-theme.mjs +85 -0
  56. package/clients/cli/commands/dialog-adaptive-template.test.mjs +24 -0
  57. package/clients/cli/commands/theme-targets.behavior.test.mjs +64 -0
  58. package/clients/cli/commands/theme-targets.doc.mjs +38 -0
  59. package/clients/cli/commands/theme-template.doc.mjs +2 -2
  60. package/clients/cli/commands/theme.doc.mjs +4 -2
  61. package/clients/cli/index.mjs +1 -0
  62. package/clients/cli/lib/manifest.mjs +2 -0
  63. package/foundation/discovery/theming-targets.d.mts +86 -0
  64. package/foundation/discovery/theming-targets.mjs +202 -0
  65. package/foundation/discovery/theming-targets.test.mjs +245 -0
  66. package/foundation/response/response-types.doc.mjs +7 -2
  67. package/package.json +9 -9
  68. package/assets/templates/blocks/components/Stepper/StepperHorizontal.doc.mjs +0 -14
  69. package/assets/templates/blocks/components/Stepper/StepperHorizontal.tsx +0 -24
  70. package/assets/templates/blocks/components/Stepper/StepperMultiStepForm.doc.mjs +0 -14
  71. package/assets/templates/blocks/components/Stepper/StepperMultiStepForm.tsx +0 -92
  72. package/assets/templates/blocks/components/Stepper/StepperVerticalOnboarding.doc.mjs +0 -14
  73. package/assets/templates/blocks/components/Stepper/StepperVerticalOnboarding.tsx +0 -40
@@ -0,0 +1,24 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ import {describe, expect, it} from 'vitest';
4
+ import {component} from '../../../api/component/component.mjs';
5
+
6
+ const CWD = {cwd: '.'};
7
+
8
+ // Resolves built-in block templates by walking the source tree.
9
+ const SCAN_TIMEOUT = 30_000;
10
+
11
+ describe(
12
+ 'Dialog adaptive presentation template',
13
+ () => {
14
+ it('is listed as a Dialog example block', async () => {
15
+ const result = await component('Dialog', {
16
+ ...CWD,
17
+ blocks: true,
18
+ });
19
+ const exampleNames = result.data.examples.map(b => b.name);
20
+ expect(exampleNames).toContain('DialogAdaptivePresentation');
21
+ });
22
+ },
23
+ SCAN_TIMEOUT,
24
+ );
@@ -0,0 +1,64 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file CLI behavior for `astryx theme targets`.
5
+ *
6
+ * The API leaf is covered by api/theme/targets/targets.test.mjs; what is only
7
+ * reachable here is the terminal binding — the table a human reads, the JSON
8
+ * envelope a lint script reads, and the route into component overrides from
9
+ * `theme --help`, which is where the question gets asked.
10
+ */
11
+
12
+ import {describe, it, expect} from 'vitest';
13
+ import {runCli} from '../../../test-utils/run-cli.mjs';
14
+
15
+ describe('astryx theme targets', () => {
16
+ it('prints one greppable line per target, with props and states', async () => {
17
+ const {status, stdout} = await runCli(['theme', 'targets', 'Switch']);
18
+
19
+ expect(status).toBe(0);
20
+ expect(stdout).toMatch(/^switch\s+Switch\s+size\s+checked, disabled$/m);
21
+ expect(stdout).toMatch(/^switch-thumb\s+Switch\s+size\s+checked$/m);
22
+ expect(stdout).toMatch(/3 across 1 component/);
23
+ });
24
+
25
+ it('lists the whole surface when unfiltered', async () => {
26
+ const {status, stdout} = await runCli(['theme', 'targets']);
27
+
28
+ expect(status).toBe(0);
29
+ const rows = stdout.split('\n').filter(l => /^[a-z][a-z0-9-]*\s{2,}/.test(l));
30
+ expect(rows.length).toBeGreaterThan(100);
31
+ expect(stdout).toMatch(/^button\s/m);
32
+ expect(stdout).toMatch(/^switch-thumb\s/m);
33
+ }, 30_000);
34
+
35
+ it('returns a theme.targets envelope under --json', async () => {
36
+ const {status, stdout} = await runCli(['--json', 'theme', 'targets', 'Switch']);
37
+
38
+ expect(status).toBe(0);
39
+ const payload = JSON.parse(stdout);
40
+ expect(payload.type).toBe('theme.targets');
41
+ expect(payload.data.targets).toContainEqual({
42
+ key: 'switch-thumb',
43
+ className: 'astryx-switch-thumb',
44
+ component: 'Switch',
45
+ props: ['size'],
46
+ states: ['checked'],
47
+ });
48
+ });
49
+
50
+ it('fails a filter that matches nothing', async () => {
51
+ const {status, stderr} = await runCli(['theme', 'targets', 'nosuchthing']);
52
+
53
+ expect(status).toBe(1);
54
+ expect(stderr).toMatch(/No theming target matches "nosuchthing"/);
55
+ });
56
+
57
+ it('routes a theming question from `theme --help` to component overrides', async () => {
58
+ const {stdout} = await runCli(['theme', '--help']);
59
+
60
+ expect(stdout).toMatch(/Component style overrides:/);
61
+ expect(stdout).toMatch(/theme targets/);
62
+ expect(stdout).toMatch(/component <Name>/);
63
+ });
64
+ });
@@ -0,0 +1,38 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file CommandDoc for `astryx theme targets`. The terminal binding of the
5
+ * `themeTargets()` function (referenced via `fn`); its args map to that
6
+ * function's params so a converter can build Commander config + --help from one
7
+ * source of truth.
8
+ * @position packages/cli/clients/cli/commands — command documentation
9
+ */
10
+
11
+ /** @type {import('@astryxdesign/cli/authoring').CommandDoc} */
12
+ export const doc = {
13
+ type: 'command',
14
+ name: 'theme targets',
15
+ displayName: 'astryx theme targets',
16
+ namespace: 'cli',
17
+ summary: 'List the component theming targets a theme can override',
18
+ description:
19
+ 'Prints every `defineTheme` components key across the system: the stable class it paints, ' +
20
+ 'the component that declares it, and the props and states that are legal override keys ' +
21
+ 'under it. This is the whole themeable surface in one command — what auditing a theme, or ' +
22
+ 'answering "which key paints this pixel?", used to need one `astryx component <Name>` per ' +
23
+ 'component to assemble. Pass a component name to scope it; pass any substring to search ' +
24
+ 'keys. `--json` for a list a repo can lint its own theme against.',
25
+ fn: 'themeTargets',
26
+ args: [{name: 'filter', param: 'filter', required: false}],
27
+ examples: [
28
+ {label: 'The whole themeable surface', cli: 'astryx theme targets'},
29
+ {label: "One component's targets", cli: 'astryx theme targets Switch'},
30
+ {label: 'Search keys', cli: 'astryx theme targets thumb'},
31
+ {label: 'For a lint or an audit script', cli: 'astryx --json theme targets'},
32
+ ],
33
+ exitCodes: [
34
+ {code: 0, when: 'success'},
35
+ {code: 1, when: 'a filter matches no target, or core cannot be resolved'},
36
+ ],
37
+ related: ['component', 'theme build', 'theme template'],
38
+ };
@@ -16,9 +16,9 @@ export const doc = {
16
16
  namespace: 'cli',
17
17
  summary: 'Write the annotated theme template into your project',
18
18
  description:
19
- 'Writes theme.template.ts: the annotated reference for the whole theme surface every ' +
19
+ 'Writes theme.template.ts: the annotated reference for the whole theme surface, covering every ' +
20
20
  'defineTheme field, the token families, the component override syntax, and how a theme is ' +
21
- 'consumed naming the CLI command that prints the authoritative reference for each. Read ' +
21
+ 'consumed, naming the CLI command that prints the authoritative reference for each. Read ' +
22
22
  'it, copy what you need into your own theme file, delete it. Use `theme add <slug>` instead ' +
23
23
  'to start from a theme we ship. Leaves an existing file untouched unless --overwrite.',
24
24
  fn: 'themeTemplate',
@@ -17,11 +17,13 @@ export const doc = {
17
17
  description:
18
18
  'The theme command group. Running astryx theme with no subcommand prints the ' +
19
19
  'subcommand list; the work happens in the subcommands: compile a theme (build), ' +
20
- 'scaffold one into your project (add), start a custom one from the annotated template (template), or list the bundled themes (list).',
21
- subcommands: ['build', 'add', 'list', 'template'],
20
+ 'scaffold one into your project (add), start a custom one from the annotated template (template), ' +
21
+ 'list the bundled themes (list), or list the component theming targets a theme can override (targets).',
22
+ subcommands: ['build', 'add', 'list', 'template', 'targets'],
22
23
  examples: [
23
24
  {label: 'List bundled themes', cli: 'astryx theme list'},
24
25
  {label: 'Scaffold a theme', cli: 'astryx theme add matcha'},
26
+ {label: 'See what a theme can override', cli: 'astryx theme targets'},
25
27
  ],
26
28
  exitCodes: [
27
29
  {code: 0, when: 'success (help shown, or a subcommand succeeded)'},
@@ -70,6 +70,7 @@ export const JSON_SUPPORTED = new Set([
70
70
  'theme list',
71
71
  'theme add',
72
72
  'theme template',
73
+ 'theme targets',
73
74
  'upgrade',
74
75
  'manifest',
75
76
  'doctor',
@@ -77,6 +77,7 @@ export const RESPONSE_TYPES = {
77
77
  'theme list': ['theme.list'],
78
78
  'theme add': ['theme.list', 'theme.add'],
79
79
  'theme template': ['theme.template'],
80
+ 'theme targets': ['theme.targets'],
80
81
  upgrade: ['upgrade.list', 'upgrade.status', 'upgrade.run'],
81
82
  manifest: ['manifest'],
82
83
  doctor: ['doctor'],
@@ -116,6 +117,7 @@ const EXAMPLES = {
116
117
  'astryx theme add matcha ./src/themes/matcha',
117
118
  ],
118
119
  'theme template': ['astryx theme template', 'astryx theme template --json'],
120
+ 'theme targets': ['astryx theme targets Switch', 'astryx --json theme targets'],
119
121
  upgrade: ['astryx upgrade --json'],
120
122
  manifest: ['astryx manifest --json', 'astryx --json'],
121
123
  doctor: ['astryx doctor', 'astryx doctor --json'],
@@ -0,0 +1,86 @@
1
+ // @generated by scripts/sync-api-types.mjs from the JSDoc in foundation/**/*.mjs.
2
+ // DO NOT EDIT — run `pnpm sync:api-types` to regenerate.
3
+
4
+ /**
5
+ * Every theming target declared under a core `src` directory, sorted by key
6
+ * then component. A key can appear more than once: a shared sub-element (the
7
+ * radio indicator, say) is documented by every component that renders it.
8
+ *
9
+ * Unreadable docs are skipped rather than fatal — a single malformed doc must
10
+ * not take out theme validation or the listing.
11
+ *
12
+ * @param {string} coreSrc - absolute path to `<core>/src`
13
+ * @returns {Promise<ThemingTarget[]>}
14
+ */
15
+ export function collectThemingTargets(coreSrc: string): Promise<ThemingTarget[]>;
16
+ /**
17
+ * One public custom property a theme may set on a component's target.
18
+ * @typedef {object} ThemingVar
19
+ * @property {string} name - the custom property, e.g. `--tree-list-indent`
20
+ * @property {string} component - the component whose doc declares it
21
+ * @property {string} dir - absolute path to the directory the doc lives in
22
+ * @property {string} default - the documented default value
23
+ */
24
+ /**
25
+ * Every PUBLIC theming var declared under a core `src` directory, sorted by
26
+ * name. Private `--_*` vars are a component's own plumbing, not a theme's to
27
+ * set, so they are not enumerated here.
28
+ *
29
+ * @param {string} coreSrc - absolute path to `<core>/src`
30
+ * @returns {Promise<ThemingVar[]>}
31
+ */
32
+ export function collectThemingVars(coreSrc: string): Promise<ThemingVar[]>;
33
+ /**
34
+ * Collapse the enumeration into the `{key: [props and states]}` map theme
35
+ * validation checks override keys against — both are legal override keys, so
36
+ * they share one list.
37
+ * @param {ThemingTarget[]} targets
38
+ * @returns {Record<string, string[]>}
39
+ */
40
+ export function targetsByKey(targets: ThemingTarget[]): Record<string, string[]>;
41
+ /**
42
+ * One theming target, as a theme author has to write it.
43
+ */
44
+ export type ThemingTarget = {
45
+ /**
46
+ * - the `defineTheme` `components` key (class minus the `astryx-` prefix)
47
+ */
48
+ key: string;
49
+ /**
50
+ * - the stable class the component renders
51
+ */
52
+ className: string;
53
+ /**
54
+ * - the component whose doc declares it
55
+ */
56
+ component: string;
57
+ /**
58
+ * - visual props the target reflects (`variant:value` keys)
59
+ */
60
+ props: string[];
61
+ /**
62
+ * - runtime states the target reflects (bare-name keys)
63
+ */
64
+ states: string[];
65
+ };
66
+ /**
67
+ * One public custom property a theme may set on a component's target.
68
+ */
69
+ export type ThemingVar = {
70
+ /**
71
+ * - the custom property, e.g. `--tree-list-indent`
72
+ */
73
+ name: string;
74
+ /**
75
+ * - the component whose doc declares it
76
+ */
77
+ component: string;
78
+ /**
79
+ * - absolute path to the directory the doc lives in
80
+ */
81
+ dir: string;
82
+ /**
83
+ * - the documented default value
84
+ */
85
+ default: string;
86
+ };
@@ -0,0 +1,202 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file The one enumeration of component theming targets.
5
+ *
6
+ * @input a core `src` directory
7
+ * @output every `theming.targets` entry authored in a component `.doc.mjs`,
8
+ * flattened into the `defineTheme` component key a theme author writes
9
+ * @position packages/cli/foundation/discovery — shared by `theme targets` (the
10
+ * listing) and `theme build` (override validation). Both read the
11
+ * component docs, which are the source of truth `astryx component
12
+ * <Name>` prints; nothing here is a second registry, so the list a
13
+ * theme author can enumerate and the set the compiler accepts cannot
14
+ * drift from the components or from each other.
15
+ */
16
+
17
+ import * as fs from 'node:fs';
18
+ import * as path from 'node:path';
19
+ import {loadComponentDoc} from './component-loader.mjs';
20
+
21
+ const SKIP_DIRS = new Set(['node_modules', '__tests__']);
22
+
23
+ /**
24
+ * One theming target, as a theme author has to write it.
25
+ * @typedef {object} ThemingTarget
26
+ * @property {string} key - the `defineTheme` `components` key (class minus the `astryx-` prefix)
27
+ * @property {string} className - the stable class the component renders
28
+ * @property {string} component - the component whose doc declares it
29
+ * @property {string[]} props - visual props the target reflects (`variant:value` keys)
30
+ * @property {string[]} states - runtime states the target reflects (bare-name keys)
31
+ */
32
+
33
+ /**
34
+ * Strip the namespace prefix to get the `defineTheme` key for a class name.
35
+ *
36
+ * Keep the `astryx-` literal in sync with packages/core/src/naming.ts
37
+ * (NAMESPACE / classPrefix), the same way component-format.mjs does.
38
+ * <!-- SYNC: packages/core/src/naming.ts (namespace prefix source of truth) -->
39
+ * @param {string} className
40
+ * @returns {string}
41
+ */
42
+ function targetKey(className) {
43
+ return className.replace(/^astryx-/, '');
44
+ }
45
+
46
+ /**
47
+ * Every theming target declared under a core `src` directory, sorted by key
48
+ * then component. A key can appear more than once: a shared sub-element (the
49
+ * radio indicator, say) is documented by every component that renders it.
50
+ *
51
+ * Unreadable docs are skipped rather than fatal — a single malformed doc must
52
+ * not take out theme validation or the listing.
53
+ *
54
+ * @param {string} coreSrc - absolute path to `<core>/src`
55
+ * @returns {Promise<ThemingTarget[]>}
56
+ */
57
+ export async function collectThemingTargets(coreSrc) {
58
+ if (!coreSrc || !fs.existsSync(coreSrc)) return [];
59
+
60
+ /** @type {ThemingTarget[]} */
61
+ const targets = [];
62
+
63
+ /** @param {string} dir */
64
+ async function scan(dir) {
65
+ for (const entry of fs.readdirSync(dir, {withFileTypes: true})) {
66
+ const full = path.join(dir, entry.name);
67
+ if (entry.isDirectory()) {
68
+ if (SKIP_DIRS.has(entry.name)) continue;
69
+ await scan(full);
70
+ continue;
71
+ }
72
+ if (!entry.name.endsWith('.doc.mjs')) continue;
73
+
74
+ /** @type {any} */
75
+ let doc;
76
+ try {
77
+ doc = await loadComponentDoc(full);
78
+ } catch {
79
+ continue;
80
+ }
81
+
82
+ const component =
83
+ typeof doc?.name === 'string' && doc.name
84
+ ? doc.name
85
+ : path.basename(path.dirname(full));
86
+
87
+ for (const target of doc?.theming?.targets || []) {
88
+ const className = target?.className;
89
+ if (typeof className !== 'string') continue;
90
+ const key = targetKey(className);
91
+ if (!key) continue;
92
+ targets.push({
93
+ key,
94
+ className,
95
+ component,
96
+ props: stringList(target.visualProps),
97
+ states: stringList(target.states),
98
+ });
99
+ }
100
+ }
101
+ }
102
+
103
+ await scan(coreSrc);
104
+
105
+ targets.sort(
106
+ (a, b) => a.key.localeCompare(b.key) || a.component.localeCompare(b.component),
107
+ );
108
+ return targets;
109
+ }
110
+
111
+ /**
112
+ * One public custom property a theme may set on a component's target.
113
+ * @typedef {object} ThemingVar
114
+ * @property {string} name - the custom property, e.g. `--tree-list-indent`
115
+ * @property {string} component - the component whose doc declares it
116
+ * @property {string} dir - absolute path to the directory the doc lives in
117
+ * @property {string} default - the documented default value
118
+ */
119
+
120
+ /**
121
+ * Every PUBLIC theming var declared under a core `src` directory, sorted by
122
+ * name. Private `--_*` vars are a component's own plumbing, not a theme's to
123
+ * set, so they are not enumerated here.
124
+ *
125
+ * @param {string} coreSrc - absolute path to `<core>/src`
126
+ * @returns {Promise<ThemingVar[]>}
127
+ */
128
+ export async function collectThemingVars(coreSrc) {
129
+ if (!coreSrc || !fs.existsSync(coreSrc)) return [];
130
+
131
+ /** @type {Map<string, ThemingVar>} */
132
+ const vars = new Map();
133
+
134
+ /** @param {string} dir */
135
+ async function scan(dir) {
136
+ for (const entry of fs.readdirSync(dir, {withFileTypes: true})) {
137
+ const full = path.join(dir, entry.name);
138
+ if (entry.isDirectory()) {
139
+ if (SKIP_DIRS.has(entry.name)) continue;
140
+ await scan(full);
141
+ continue;
142
+ }
143
+ if (!entry.name.endsWith('.doc.mjs')) continue;
144
+
145
+ /** @type {any} */
146
+ let doc;
147
+ try {
148
+ doc = await loadComponentDoc(full);
149
+ } catch {
150
+ continue;
151
+ }
152
+
153
+ const component =
154
+ typeof doc?.name === 'string' && doc.name
155
+ ? doc.name
156
+ : path.basename(path.dirname(full));
157
+
158
+ for (const entryVar of doc?.theming?.vars || []) {
159
+ const name = entryVar?.name;
160
+ if (typeof name !== 'string') continue;
161
+ if (entryVar.private === true || name.startsWith('--_')) continue;
162
+ if (vars.has(name)) continue;
163
+ vars.set(name, {
164
+ name,
165
+ component,
166
+ dir: path.dirname(full),
167
+ default: typeof entryVar.default === 'string' ? entryVar.default : '',
168
+ });
169
+ }
170
+ }
171
+ }
172
+
173
+ await scan(coreSrc);
174
+
175
+ return [...vars.values()].sort((a, b) => a.name.localeCompare(b.name));
176
+ }
177
+
178
+ /**
179
+ * Collapse the enumeration into the `{key: [props and states]}` map theme
180
+ * validation checks override keys against — both are legal override keys, so
181
+ * they share one list.
182
+ * @param {ThemingTarget[]} targets
183
+ * @returns {Record<string, string[]>}
184
+ */
185
+ export function targetsByKey(targets) {
186
+ /** @type {Record<string, string[]>} */
187
+ const byKey = {};
188
+ for (const t of targets) {
189
+ byKey[t.key] = [...new Set([...(byKey[t.key] || []), ...t.props, ...t.states])];
190
+ }
191
+ return byKey;
192
+ }
193
+
194
+ /**
195
+ * @param {unknown} value
196
+ * @returns {string[]}
197
+ */
198
+ function stringList(value) {
199
+ return Array.isArray(value)
200
+ ? value.filter((/** @type {unknown} */ v) => typeof v === 'string')
201
+ : [];
202
+ }