@astryxdesign/cli 0.4.7 → 0.5.0-canary.009bcb3
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/CHANGELOG.md +64 -0
- package/README.md +53 -51
- package/api/docs/docs.doc.mjs +2 -2
- package/api/index.d.mts +1 -1
- package/api/index.mjs +1 -1
- package/api/search/search.mjs +50 -4
- package/api/search/search.test.mjs +71 -0
- package/api/template/data-token-fallbacks.test.mjs +85 -0
- package/api/template/template.doc.mjs +2 -2
- package/api/theme/build/build.mjs +43 -44
- package/api/theme/targets/targets.d.mts +18 -0
- package/api/theme/targets/targets.mjs +87 -0
- package/api/theme/targets/targets.test.mjs +66 -0
- package/api/theme/theme.d.mts +1 -0
- package/api/theme/theme.mjs +3 -1
- package/api/theme/theme.type.d.mts +23 -0
- package/api/theme/theme.type.mjs +21 -1
- package/api/theme/themeTargets.doc.d.mts +11 -0
- package/api/theme/themeTargets.doc.mjs +58 -0
- package/api/theme/themeTemplate.doc.mjs +3 -3
- package/assets/codemods/__tests__/registry.test.mjs +1 -0
- package/assets/codemods/registry.mjs +1 -0
- package/assets/codemods/transforms/v0.5.0/__tests__/next-codemods.test.mjs +127 -0
- package/assets/codemods/transforms/v0.5.0/banner-collapsible-content.mjs +171 -0
- package/assets/codemods/transforms/v0.5.0/index.mjs +20 -0
- package/assets/docs/README.md +50 -0
- package/assets/docs/cli-integrations.doc.mjs +4 -4
- package/assets/docs/theme.doc.dense.mjs +1 -1
- package/assets/docs/theme.doc.mjs +1 -1
- package/assets/docs/typography.doc.mjs +2 -2
- package/assets/templates/blocks/components/Banner/BannerCollapsibleContent.doc.mjs +1 -1
- package/assets/templates/blocks/components/Banner/BannerCollapsibleContent.tsx +1 -1
- package/assets/templates/blocks/components/DateInput/DateInputClearable.tsx +5 -1
- package/assets/templates/blocks/components/DateInput/DateInputDateRange.tsx +6 -1
- package/assets/templates/blocks/components/DateInput/DateInputFormats.tsx +5 -1
- package/assets/templates/blocks/components/DateInput/DateInputShowcase.tsx +4 -1
- package/assets/templates/blocks/components/DateInput/DateInputWithDescription.tsx +5 -1
- package/assets/templates/blocks/components/DateInput/DateInputWithValidation.tsx +5 -1
- package/assets/templates/blocks/components/Dialog/DialogAdaptivePresentation.doc.mjs +25 -0
- package/assets/templates/blocks/components/Dialog/DialogAdaptivePresentation.tsx +167 -0
- package/assets/templates/blocks/components/Dialog/DialogScrollingContent.tsx +1 -1
- package/assets/templates/blocks/components/HoverCard/HoverCardHookUsage.tsx +4 -1
- package/assets/templates/blocks/components/Step/StepContent.doc.mjs +14 -0
- package/assets/templates/blocks/components/Step/StepContent.tsx +32 -0
- package/assets/templates/blocks/components/Step/StepIndicator.doc.mjs +14 -0
- package/assets/templates/blocks/components/Step/StepIndicator.tsx +60 -0
- package/assets/templates/blocks/components/Step/StepShowcase.doc.mjs +15 -0
- package/assets/templates/blocks/components/Step/StepShowcase.tsx +26 -0
- package/assets/templates/blocks/components/Step/StepStates.doc.mjs +14 -0
- package/assets/templates/blocks/components/Step/StepStates.tsx +46 -0
- package/assets/templates/blocks/components/Stepper/StepperCustomContent.doc.mjs +22 -0
- package/assets/templates/blocks/components/Stepper/StepperCustomContent.tsx +126 -0
- package/assets/templates/blocks/components/Stepper/StepperIndicatorModes.doc.mjs +1 -1
- package/assets/templates/blocks/components/Stepper/StepperIndicatorModes.tsx +17 -5
- package/assets/templates/blocks/components/Stepper/StepperOnTrackHorizontal.doc.mjs +14 -0
- package/assets/templates/blocks/components/Stepper/StepperOnTrackHorizontal.tsx +25 -0
- package/assets/templates/blocks/components/Stepper/StepperOnTrackVertical.doc.mjs +2 -2
- package/assets/templates/blocks/components/Stepper/StepperOnTrackVertical.tsx +1 -1
- package/assets/templates/blocks/components/Stepper/StepperShowcase.doc.mjs +1 -1
- package/assets/templates/blocks/components/Stepper/StepperShowcase.tsx +6 -7
- package/assets/templates/blocks/components/Stepper/StepperStatus.tsx +1 -1
- package/assets/templates/pages/dashboard-cohort-funnel/page.tsx +1 -1
- package/assets/templates/pages/dashboard-data/page.tsx +1 -1
- package/assets/templates/pages/dashboard-portfolio/page.tsx +5 -5
- package/assets/templates/pages/dashboard-service-monitoring/page.tsx +1 -1
- package/assets/templates/pages/mixed-gallery/page.tsx +12 -3
- package/assets/templates/pages/settings-dialog/page.tsx +2543 -757
- package/assets/templates/pages/settings-dialog/template.doc.mjs +1 -2
- package/assets/templates/pages/table-filter/page.tsx +4093 -0
- package/assets/templates/pages/table-filter/template.doc.mjs +12 -0
- package/assets/templates/pages/table-grouped/page.tsx +151 -144
- package/assets/templates/pages/theme-showcase/page.tsx +36 -17
- package/assets/templates/themes/chocolate/chocolateTheme.ts +3 -1
- package/assets/templates/themes/matcha/matchaTheme.ts +3 -1
- package/assets/templates/themes/neutral/neutralTheme.ts +16 -9
- package/assets/templates/themes/stone/stoneTheme.ts +3 -1
- package/clients/cli/commands/build-theme.color-scheme.test.mjs +7 -6
- package/clients/cli/commands/build-theme.data-tokens.test.mjs +144 -0
- package/clients/cli/commands/build-theme.mjs +85 -0
- package/clients/cli/commands/build-theme.variants.test.mjs +3 -0
- package/clients/cli/commands/dialog-adaptive-template.test.mjs +24 -0
- package/clients/cli/commands/theme-targets.behavior.test.mjs +64 -0
- package/clients/cli/commands/theme-targets.doc.mjs +38 -0
- package/clients/cli/commands/theme-template.doc.mjs +2 -2
- package/clients/cli/commands/theme.doc.mjs +4 -2
- package/clients/cli/index.mjs +1 -0
- package/clients/cli/lib/component-format.mjs +2 -2
- package/clients/cli/lib/manifest.mjs +2 -0
- package/foundation/discovery/component-discovery.mjs +10 -9
- package/foundation/discovery/hook-discovery.mjs +2 -1
- package/foundation/discovery/hook-discovery.test.mjs +156 -37
- package/foundation/discovery/theming-targets.d.mts +86 -0
- package/foundation/discovery/theming-targets.mjs +202 -0
- package/foundation/discovery/theming-targets.test.mjs +245 -0
- package/foundation/fs/paths.d.mts +16 -0
- package/foundation/fs/paths.mjs +36 -0
- package/foundation/fs/paths.test.mjs +29 -1
- package/foundation/response/response-types.doc.mjs +7 -2
- package/package.json +9 -9
- package/assets/templates/blocks/components/Stepper/StepperHorizontal.doc.mjs +0 -14
- package/assets/templates/blocks/components/Stepper/StepperHorizontal.tsx +0 -24
- package/assets/templates/blocks/components/Stepper/StepperMultiStepForm.doc.mjs +0 -14
- package/assets/templates/blocks/components/Stepper/StepperMultiStepForm.tsx +0 -92
- package/assets/templates/blocks/components/Stepper/StepperVerticalOnboarding.doc.mjs +0 -14
- package/assets/templates/blocks/components/Stepper/StepperVerticalOnboarding.tsx +0 -40
|
@@ -5,16 +5,25 @@
|
|
|
5
5
|
* @astryxdesign/core source tree plus throwaway fixtures. Pins the
|
|
6
6
|
* category grouping, the missing-src guarding (which differs from
|
|
7
7
|
* component-discovery), and the Levenshtein fuzzy-fallback in findHookDoc.
|
|
8
|
+
* Also gates the index against the hooks barrel: every hook the built barrel
|
|
9
|
+
* exports must be discoverable, so adding a hook without a doc fails here.
|
|
8
10
|
*/
|
|
9
11
|
|
|
10
12
|
import {describe, it, expect, afterAll} from 'vitest';
|
|
11
13
|
import * as fs from 'node:fs';
|
|
12
14
|
import * as os from 'node:os';
|
|
13
15
|
import * as path from 'node:path';
|
|
14
|
-
import {fileURLToPath} from 'node:url';
|
|
15
|
-
import {
|
|
16
|
+
import {fileURLToPath, pathToFileURL} from 'node:url';
|
|
17
|
+
import {
|
|
18
|
+
discoverHooks,
|
|
19
|
+
findHookDoc,
|
|
20
|
+
getAllHookNames,
|
|
21
|
+
} from './hook-discovery.mjs';
|
|
16
22
|
|
|
17
|
-
const REPO = path.resolve(
|
|
23
|
+
const REPO = path.resolve(
|
|
24
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
25
|
+
'../../../..',
|
|
26
|
+
);
|
|
18
27
|
const CORE = path.join(REPO, 'packages', 'core');
|
|
19
28
|
const SLOW = 30_000;
|
|
20
29
|
|
|
@@ -29,35 +38,53 @@ afterAll(() => {
|
|
|
29
38
|
});
|
|
30
39
|
|
|
31
40
|
describe('discoverHooks (real core)', () => {
|
|
32
|
-
it(
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
expect([...
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
41
|
+
it(
|
|
42
|
+
'discovers real hooks grouped by category',
|
|
43
|
+
() => {
|
|
44
|
+
const hooks = discoverHooks(CORE);
|
|
45
|
+
const cats = Object.keys(hooks);
|
|
46
|
+
expect(cats.length).toBeGreaterThan(1);
|
|
47
|
+
if (cats.includes('Other')) expect(cats[cats.length - 1]).toBe('Other');
|
|
48
|
+
const nonOther = cats.filter(c => c !== 'Other');
|
|
49
|
+
expect([...nonOther].sort((a, b) => a.localeCompare(b))).toEqual(
|
|
50
|
+
nonOther,
|
|
51
|
+
);
|
|
52
|
+
for (const members of Object.values(hooks)) {
|
|
53
|
+
expect([...members].sort()).toEqual(members);
|
|
54
|
+
}
|
|
55
|
+
const all = getAllHookNames(CORE);
|
|
56
|
+
expect(all).toContain('useMediaQuery');
|
|
57
|
+
expect(all.length).toBeGreaterThan(10);
|
|
58
|
+
},
|
|
59
|
+
SLOW,
|
|
60
|
+
);
|
|
46
61
|
|
|
47
62
|
it('capitalizes categories and buckets category-less docs into Other', () => {
|
|
48
63
|
const core = mkTmp('as-hd-cat-');
|
|
49
64
|
const hooksDir = path.join(core, 'src', 'hooks');
|
|
50
65
|
fs.mkdirSync(hooksDir, {recursive: true});
|
|
51
|
-
fs.writeFileSync(
|
|
52
|
-
|
|
53
|
-
|
|
66
|
+
fs.writeFileSync(
|
|
67
|
+
path.join(hooksDir, 'useThing.doc.mjs'),
|
|
68
|
+
"export default {\n category: 'layout',\n};\n",
|
|
69
|
+
);
|
|
70
|
+
fs.writeFileSync(
|
|
71
|
+
path.join(hooksDir, 'useOrphan.doc.mjs'),
|
|
72
|
+
'export default {};\n',
|
|
73
|
+
);
|
|
74
|
+
expect(discoverHooks(core)).toEqual({
|
|
75
|
+
Layout: ['useThing'],
|
|
76
|
+
Other: ['useOrphan'],
|
|
77
|
+
});
|
|
54
78
|
});
|
|
55
79
|
|
|
56
80
|
it('discovers use*.doc.mjs colocated in component directories', () => {
|
|
57
81
|
const core = mkTmp('as-hd-colo-');
|
|
58
82
|
const compDir = path.join(core, 'src', 'Resizable');
|
|
59
83
|
fs.mkdirSync(compDir, {recursive: true});
|
|
60
|
-
fs.writeFileSync(
|
|
84
|
+
fs.writeFileSync(
|
|
85
|
+
path.join(compDir, 'useResizable.doc.mjs'),
|
|
86
|
+
"export default {\n category: 'interaction',\n};\n",
|
|
87
|
+
);
|
|
61
88
|
expect(discoverHooks(core)).toEqual({Interaction: ['useResizable']});
|
|
62
89
|
});
|
|
63
90
|
|
|
@@ -69,13 +96,25 @@ describe('discoverHooks (real core)', () => {
|
|
|
69
96
|
});
|
|
70
97
|
|
|
71
98
|
describe('findHookDoc (real core)', () => {
|
|
72
|
-
it(
|
|
73
|
-
|
|
74
|
-
|
|
99
|
+
it(
|
|
100
|
+
'resolves an exact hook name',
|
|
101
|
+
() => {
|
|
102
|
+
expect(findHookDoc(CORE, 'useMediaQuery')).toMatch(
|
|
103
|
+
/useMediaQuery\.doc\.mjs$/,
|
|
104
|
+
);
|
|
105
|
+
},
|
|
106
|
+
SLOW,
|
|
107
|
+
);
|
|
75
108
|
|
|
76
|
-
it(
|
|
77
|
-
|
|
78
|
-
|
|
109
|
+
it(
|
|
110
|
+
'resolves a bare (use-prefix-stripped, case-insensitive) name',
|
|
111
|
+
() => {
|
|
112
|
+
expect(findHookDoc(CORE, 'mediaquery')).toMatch(
|
|
113
|
+
/useMediaQuery\.doc\.mjs$/,
|
|
114
|
+
);
|
|
115
|
+
},
|
|
116
|
+
SLOW,
|
|
117
|
+
);
|
|
79
118
|
|
|
80
119
|
it('returns null when src/ is missing (guarded)', () => {
|
|
81
120
|
const core = mkTmp('as-fhd-nosrc-');
|
|
@@ -84,14 +123,94 @@ describe('findHookDoc (real core)', () => {
|
|
|
84
123
|
});
|
|
85
124
|
|
|
86
125
|
describe('findHookDoc fuzzy Levenshtein fallback (pinned current behavior)', () => {
|
|
87
|
-
it(
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
126
|
+
it(
|
|
127
|
+
'auto-resolves a typo within edit distance 3',
|
|
128
|
+
() => {
|
|
129
|
+
expect(getAllHookNames(CORE)).not.toContain('useLayers');
|
|
130
|
+
expect(getAllHookNames(CORE)).toContain('useLayer');
|
|
131
|
+
expect(findHookDoc(CORE, 'useLayers')).toMatch(/useLayer\.doc\.mjs$/);
|
|
132
|
+
},
|
|
133
|
+
SLOW,
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
it(
|
|
137
|
+
'returns null when the closest hook is farther than distance 3',
|
|
138
|
+
() => {
|
|
139
|
+
expect(findHookDoc(CORE, 'useZzzzzz')).toBeNull();
|
|
140
|
+
expect(findHookDoc(CORE, 'zzzzzzzzzz')).toBeNull();
|
|
141
|
+
},
|
|
142
|
+
SLOW,
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* The set of hooks a barrel actually exports, read from the module itself
|
|
148
|
+
* rather than from its text. Importing is what makes this a derivation: it sees
|
|
149
|
+
* `export * from` re-exports, aliases and any other form the language allows,
|
|
150
|
+
* and type-only exports have already been erased. A regex over the source can
|
|
151
|
+
* only see the spellings someone thought to write a pattern for.
|
|
152
|
+
* @param {string} entry absolute path to a built ESM barrel
|
|
153
|
+
*/
|
|
154
|
+
async function exportedHookNames(entry) {
|
|
155
|
+
const mod = await import(pathToFileURL(entry).href);
|
|
156
|
+
return new Set(Object.keys(mod).filter(name => /^use[A-Z]/.test(name)));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
describe('core hooks barrel coverage (real core)', () => {
|
|
160
|
+
// Discovery is filesystem-driven: a hook is only reachable from `astryx hook`
|
|
161
|
+
// and `astryx search` if it ships a .doc.mjs. Nothing about adding a hook
|
|
162
|
+
// forces one, so the index silently goes stale; seven exported hooks,
|
|
163
|
+
// useAnnounce among them, were invisible until this check existed.
|
|
164
|
+
//
|
|
165
|
+
// Only the standalone hooks in src/hooks/ are in scope. Hooks that belong to
|
|
166
|
+
// a component's API (useDialog, useToast, ...) are documented in that
|
|
167
|
+
// component's directory and deliberately never enter this barrel, so their
|
|
168
|
+
// absence from it is not a defect.
|
|
169
|
+
it(
|
|
170
|
+
'every hook exported from the barrel has a doc and is discoverable',
|
|
171
|
+
async () => {
|
|
172
|
+
// The built barrel, not src/hooks/index.ts: this suite runs in the `node`
|
|
173
|
+
// project, which has no StyleX babel transform, so importing core's TS
|
|
174
|
+
// source throws on the first stylex.defineVars it reaches. dist is present
|
|
175
|
+
// because the project's globalSetup builds core before any worker forks.
|
|
176
|
+
const exported = await exportedHookNames(
|
|
177
|
+
path.join(CORE, 'dist', 'hooks', 'index.js'),
|
|
178
|
+
);
|
|
179
|
+
expect(exported.size).toBeGreaterThan(10);
|
|
180
|
+
|
|
181
|
+
const discovered = new Set(getAllHookNames(CORE));
|
|
182
|
+
const missing = [...exported]
|
|
183
|
+
.filter(name => !discovered.has(name))
|
|
184
|
+
.sort();
|
|
185
|
+
if (missing.length) {
|
|
186
|
+
throw new Error(
|
|
187
|
+
`${missing.length} exported hook(s) are missing from the CLI hook index, so ` +
|
|
188
|
+
`\`astryx hook <name>\` and \`astryx search\` deny they exist. Add a ` +
|
|
189
|
+
`<hook>.doc.mjs next to each hook in packages/core/src/hooks/:\n ${missing.join('\n ')}`,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
expect(missing).toEqual([]);
|
|
193
|
+
},
|
|
194
|
+
SLOW,
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
it('sees hooks re-exported with `export *`, which a regex over `export {…}` misses', async () => {
|
|
198
|
+
const dir = mkTmp('as-hd-star-');
|
|
199
|
+
fs.writeFileSync(
|
|
200
|
+
path.join(dir, 'useStarred.mjs'),
|
|
201
|
+
'export function useStarred() {}\n',
|
|
202
|
+
);
|
|
203
|
+
fs.writeFileSync(
|
|
204
|
+
path.join(dir, 'useNamed.mjs'),
|
|
205
|
+
'export function useNamed() {}\n',
|
|
206
|
+
);
|
|
207
|
+
fs.writeFileSync(
|
|
208
|
+
path.join(dir, 'index.mjs'),
|
|
209
|
+
"export * from './useStarred.mjs';\nexport {useNamed} from './useNamed.mjs';\n",
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
expect(await exportedHookNames(path.join(dir, 'index.mjs'))).toEqual(
|
|
213
|
+
new Set(['useStarred', 'useNamed']),
|
|
214
|
+
);
|
|
215
|
+
});
|
|
97
216
|
});
|
|
@@ -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
|
+
}
|