@astryxdesign/cli 0.4.5 → 0.4.6-canary.cea9ecd

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 (72) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/api/component/_adapter.d.mts +22 -5
  3. package/api/component/_adapter.mjs +51 -3
  4. package/api/docs/_adapter.d.mts +37 -8
  5. package/api/docs/_adapter.mjs +70 -41
  6. package/api/docs/detail/detail.d.mts +2 -0
  7. package/api/docs/detail/detail.mjs +12 -10
  8. package/api/docs/detail/section/section.d.mts +2 -0
  9. package/api/docs/detail/section/section.mjs +1 -0
  10. package/api/docs/docs.d.mts +2 -0
  11. package/api/docs/docs.doc.mjs +11 -2
  12. package/api/docs/docs.mjs +2 -1
  13. package/api/docs/docs.type.d.mts +15 -0
  14. package/api/docs/docs.type.mjs +6 -0
  15. package/api/docs/integrationDocs.test.mjs +208 -0
  16. package/api/docs/list/list.d.mts +5 -1
  17. package/api/docs/list/list.mjs +30 -14
  18. package/api/init/run/run.mjs +9 -4
  19. package/api/integration/validate-integration.mjs +2 -1
  20. package/api/integration/validate-integration.type.d.mts +1 -0
  21. package/api/integration/validate-integration.type.mjs +1 -0
  22. package/api/search/search.mjs +26 -15
  23. package/api/upgrade/_adapter.d.mts +2 -2
  24. package/api/upgrade/_adapter.mjs +7 -3
  25. package/api/upgrade/run/run.mjs +1 -1
  26. package/assets/codemods/transforms/v0.1.0/__tests__/drop-xds-prefix-imports.test.mjs +245 -0
  27. package/assets/codemods/transforms/v0.1.0/drop-xds-prefix-imports.mjs +139 -13
  28. package/assets/docs/cli-integrations.doc.mjs +41 -3
  29. package/assets/docs/layout.doc.dense.mjs +275 -30
  30. package/assets/docs/layout.doc.mjs +412 -78
  31. package/assets/docs/theme.doc.mjs +3 -3
  32. package/assets/templates/blocks/components/AvatarGroup/AvatarGroupInteractive.doc.mjs +15 -0
  33. package/assets/templates/blocks/components/AvatarGroup/AvatarGroupInteractive.tsx +47 -0
  34. package/assets/templates/blocks/components/AvatarGroup/AvatarGroupShowcase.doc.mjs +1 -1
  35. package/assets/templates/blocks/components/Breadcrumbs/BreadcrumbsCustomSeparator.tsx +18 -5
  36. package/assets/templates/blocks/components/Selector/SelectorOptionDescriptions.doc.mjs +14 -0
  37. package/assets/templates/blocks/components/Selector/SelectorOptionDescriptions.tsx +76 -0
  38. package/assets/templates/pages/dashboard-cohort-funnel/page.tsx +914 -0
  39. package/assets/templates/pages/dashboard-cohort-funnel/template.doc.mjs +12 -0
  40. package/assets/templates/pages/dashboard-data/page.tsx +894 -0
  41. package/assets/templates/pages/dashboard-data/template.doc.mjs +13 -0
  42. package/assets/templates/pages/dashboard-executive-summary/page.tsx +874 -0
  43. package/assets/templates/pages/dashboard-executive-summary/template.doc.mjs +13 -0
  44. package/assets/templates/pages/dashboard-project-status/page.tsx +1139 -0
  45. package/assets/templates/pages/dashboard-project-status/template.doc.mjs +13 -0
  46. package/assets/templates/pages/dashboard-service-monitoring/page.tsx +1590 -0
  47. package/assets/templates/pages/dashboard-service-monitoring/template.doc.mjs +12 -0
  48. package/authoring/doctypes/_schema.d.mts +2 -0
  49. package/authoring/doctypes/_schema.mjs +5 -0
  50. package/authoring/doctypes/reference/reference.doc.mjs +14 -0
  51. package/authoring/doctypes/reference/type.ts +12 -0
  52. package/authoring/doctypes/template/type.ts +2 -0
  53. package/authoring/integration/integration.doc.mjs +10 -1
  54. package/authoring/integration/parse.d.mts +1 -0
  55. package/authoring/integration/parse.mjs +1 -0
  56. package/authoring/integration/parse.test.mjs +1 -0
  57. package/authoring/integration/type.ts +5 -0
  58. package/clients/cli/commands/component-ownership.test.mjs +63 -3
  59. package/clients/cli/commands/theme-build.doc.mjs +1 -1
  60. package/foundation/agent-docs/agent-docs.d.mts +19 -3
  61. package/foundation/agent-docs/agent-docs.mjs +30 -11
  62. package/foundation/agent-docs/agent-docs.test.mjs +38 -0
  63. package/foundation/config/project.d.mts +16 -0
  64. package/foundation/config/project.mjs +65 -4
  65. package/foundation/config/project.test.mjs +66 -0
  66. package/foundation/discovery/docs-discovery.d.mts +185 -0
  67. package/foundation/discovery/docs-discovery.mjs +544 -0
  68. package/foundation/discovery/docs-discovery.test.mjs +341 -0
  69. package/foundation/integrations/integrations.d.mts +8 -6
  70. package/foundation/integrations/integrations.mjs +6 -4
  71. package/foundation/integrations/validate-contributions.mjs +30 -2
  72. package/package.json +9 -9
@@ -0,0 +1,341 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Colocated tests for reference-doc discovery: what an integration's
5
+ * docs root contributes, what the catalog does with `replaces` / `extends`,
6
+ * and which authored mistakes are caught at the load boundary rather than
7
+ * reaching a reader as a blank section.
8
+ *
9
+ * Fixtures are scaffolded under a repo-local temp dir, not /tmp, because Vite
10
+ * refuses to dynamically import a module from outside the project root.
11
+ */
12
+
13
+ import {afterEach, beforeEach, describe, expect, it} from 'vitest';
14
+ import * as fs from 'node:fs';
15
+ import * as path from 'node:path';
16
+ import {
17
+ BUILTIN_DOCS_PACKAGE,
18
+ DocsCatalog,
19
+ discoverBuiltinTopics,
20
+ discoverIntegrationDocs,
21
+ mergeTopic,
22
+ problemsInTopic,
23
+ } from './docs-discovery.mjs';
24
+
25
+ let tmpDir;
26
+
27
+ /** A minimal, valid topic. */
28
+ function topic(fields) {
29
+ return {
30
+ type: 'generic',
31
+ name: 'deploying',
32
+ title: 'Deploying',
33
+ description: 'How to ship it.',
34
+ sections: [{title: 'Overview', content: [{type: 'prose', text: 'Ship it.'}]}],
35
+ ...fields,
36
+ };
37
+ }
38
+
39
+ /** Write a docs root holding one file per topic; returns the integration. */
40
+ function integration(name, topics, {docsDir = 'docs'} = {}) {
41
+ const root = path.join(tmpDir, name.replace(/[^\w]/g, '_'), docsDir);
42
+ fs.mkdirSync(root, {recursive: true});
43
+ for (const [file, value] of Object.entries(topics)) {
44
+ fs.writeFileSync(
45
+ path.join(root, file),
46
+ typeof value === 'string'
47
+ ? value
48
+ : `export const docs = ${JSON.stringify(value, null, 2)};\n`,
49
+ );
50
+ }
51
+ return {name, docs: root};
52
+ }
53
+
54
+ beforeEach(() => {
55
+ tmpDir = fs.mkdtempSync(path.join(process.cwd(), '.astryx-docs-test-'));
56
+ });
57
+
58
+ afterEach(() => {
59
+ fs.rmSync(tmpDir, {recursive: true, force: true});
60
+ });
61
+
62
+ describe('discoverBuiltinTopics', () => {
63
+ it("finds the CLI's own topics and not their localization overlays", () => {
64
+ const topics = discoverBuiltinTopics();
65
+ expect(Object.keys(topics).length).toBeGreaterThan(0);
66
+ expect(topics.tokens).toMatch(/assets[/\\]docs[/\\]tokens\.doc\.mjs$/);
67
+ for (const name of Object.keys(topics)) {
68
+ expect(name).not.toMatch(/\.(zh|dense)$/);
69
+ }
70
+ });
71
+ });
72
+
73
+ describe('discoverIntegrationDocs', () => {
74
+ it('contributes nothing when no docs root is declared or the root is gone', async () => {
75
+ expect(await discoverIntegrationDocs({name: '@acme/widgets'})).toEqual({
76
+ records: [],
77
+ errors: [],
78
+ });
79
+ expect(
80
+ await discoverIntegrationDocs({
81
+ name: '@acme/widgets',
82
+ docs: path.join(tmpDir, 'nope'),
83
+ }),
84
+ ).toEqual({records: [], errors: []});
85
+ });
86
+
87
+ it('reads every topic under the root, with what it declares', async () => {
88
+ const {records, errors} = await discoverIntegrationDocs(
89
+ integration('@acme/widgets', {
90
+ 'deploying.doc.mjs': topic(),
91
+ 'getting-started.doc.mjs': topic({
92
+ name: 'getting-started',
93
+ title: 'Getting started',
94
+ replaces: 'getting-started',
95
+ }),
96
+ 'theme-extra.doc.mjs': topic({name: 'theme-extra', extends: 'theme'}),
97
+ }),
98
+ );
99
+ expect(errors).toEqual([]);
100
+ expect(records.map(r => [r.name, r.package, r.replaces, r.extendsTopic])).toEqual([
101
+ ['deploying', '@acme/widgets', undefined, undefined],
102
+ ['getting-started', '@acme/widgets', 'getting-started', undefined],
103
+ ['theme-extra', '@acme/widgets', undefined, 'theme'],
104
+ ]);
105
+ });
106
+
107
+ it('accepts a stamped default export as well as `export const docs`', async () => {
108
+ const {records, errors} = await discoverIntegrationDocs(
109
+ integration('@acme/default-export', {
110
+ 'deploying.doc.mjs': `export default ${JSON.stringify(topic())};\n`,
111
+ }),
112
+ );
113
+ expect(errors).toEqual([]);
114
+ expect(records.map(r => r.name)).toEqual(['deploying']);
115
+ });
116
+
117
+ it('ignores a localization overlay beside a topic', async () => {
118
+ const {records} = await discoverIntegrationDocs(
119
+ integration('@acme/localized', {
120
+ 'deploying.doc.mjs': topic(),
121
+ 'deploying.doc.zh.mjs': `export const docsZh = {description: '部署', sections: []};\n`,
122
+ }),
123
+ );
124
+ expect(records.map(r => r.name)).toEqual(['deploying']);
125
+ });
126
+
127
+ it('reports a doc that exports nothing, rather than skipping it silently', async () => {
128
+ const {records, errors} = await discoverIntegrationDocs(
129
+ integration('@acme/empty', {'deploying.doc.mjs': 'export const nope = 1;\n'}),
130
+ );
131
+ expect(records).toEqual([]);
132
+ expect(errors).toHaveLength(1);
133
+ expect(errors[0].message).toContain('exports no doc');
134
+ });
135
+
136
+ it('reports two files claiming one topic name', async () => {
137
+ const {records, errors} = await discoverIntegrationDocs(
138
+ integration('@acme/dupes', {
139
+ 'a-deploying.doc.mjs': topic(),
140
+ 'b-deploying.doc.mjs': topic({title: 'Deploying, again'}),
141
+ }),
142
+ );
143
+ expect(records).toHaveLength(1);
144
+ expect(errors[0].message).toContain('both define the topic "deploying"');
145
+ });
146
+
147
+ it('reports a doc that both replaces and extends', async () => {
148
+ const {records, errors} = await discoverIntegrationDocs(
149
+ integration('@acme/both', {
150
+ 'deploying.doc.mjs': topic({replaces: 'theme', extends: 'theme'}),
151
+ }),
152
+ );
153
+ expect(records).toEqual([]);
154
+ expect(errors[0].message).toContain('declares both');
155
+ });
156
+ });
157
+
158
+ describe('problemsInTopic', () => {
159
+ it('passes a well-formed topic', () => {
160
+ expect(problemsInTopic(topic())).toEqual([]);
161
+ });
162
+
163
+ it('catches the mistakes that would render as a blank section', () => {
164
+ expect(problemsInTopic(topic({title: ''}))).toContain(
165
+ 'title: expected a non-empty string',
166
+ );
167
+ expect(problemsInTopic(topic({sections: []}))).toContain(
168
+ 'sections: expected at least one section',
169
+ );
170
+ // A misspelled required field: the block renders nothing at all.
171
+ expect(
172
+ problemsInTopic(
173
+ topic({sections: [{title: 'Overview', content: [{type: 'prose', txt: 'oops'}]}]}),
174
+ ),
175
+ ).toEqual([
176
+ 'sections[0].content[0].text: required for a prose block',
177
+ 'sections[0].content[0].txt: not a field of a prose block',
178
+ ]);
179
+ // A heading level the renderer does not index.
180
+ expect(
181
+ problemsInTopic(
182
+ topic({
183
+ sections: [{title: 'Overview', content: [{type: 'heading', level: 2, text: 'x'}]}],
184
+ }),
185
+ ),
186
+ ).toContain('sections[0].content[0].level: 2 is not one of 3, 4, 5, 6');
187
+ // A short row renders blank cells; a long one drops its tail.
188
+ expect(
189
+ problemsInTopic(
190
+ topic({
191
+ sections: [
192
+ {
193
+ title: 'Overview',
194
+ content: [{type: 'table', headers: ['A', 'B'], rows: [['1']]}],
195
+ },
196
+ ],
197
+ }),
198
+ ),
199
+ ).toContain('sections[0].content[0].rows[0]: has 1 cells but the table has 2 headers');
200
+ });
201
+
202
+ it('rejects a name that is not URL-safe', () => {
203
+ expect(problemsInTopic(topic({name: 'not a topic'})).join('\n')).toContain('URL-safe');
204
+ });
205
+ });
206
+
207
+ describe('DocsCatalog', () => {
208
+ const record = fields => ({
209
+ name: 'deploying',
210
+ package: '@acme/widgets',
211
+ path: '/pkg/docs/deploying.doc.mjs',
212
+ ...fields,
213
+ });
214
+
215
+ it('starts from the built-in topics, owned by the CLI', () => {
216
+ const catalog = DocsCatalog.fromBuiltins({tokens: '/cli/tokens.doc.mjs'});
217
+ expect(catalog.names()).toEqual(['tokens']);
218
+ expect(catalog.resolve('tokens').package).toBe(BUILTIN_DOCS_PACKAGE);
219
+ expect(catalog.resolve('TOKENS').name).toBe('tokens');
220
+ expect(catalog.resolve('nope')).toBeUndefined();
221
+ expect(catalog.resolve(42)).toBeUndefined();
222
+ });
223
+
224
+ it('adds a new topic', () => {
225
+ const catalog = DocsCatalog.fromBuiltins({tokens: '/cli/tokens.doc.mjs'});
226
+ expect(catalog.add(record())).toBeNull();
227
+ expect(catalog.names()).toEqual(['tokens', 'deploying']);
228
+ expect(catalog.resolve('deploying').package).toBe('@acme/widgets');
229
+ });
230
+
231
+ it('refuses to shadow an existing topic by name alone', () => {
232
+ const catalog = DocsCatalog.fromBuiltins({tokens: '/cli/tokens.doc.mjs'});
233
+ const issue = catalog.add(record({name: 'tokens'}));
234
+ expect(issue).toMatchObject({code: 'invalid_doc', severity: 'error'});
235
+ expect(issue.message).toContain("already provided by @astryxdesign/cli");
236
+ expect(issue.message).toContain("replaces: 'tokens'");
237
+ // The built-in topic is untouched.
238
+ expect(catalog.resolve('tokens').package).toBe(BUILTIN_DOCS_PACKAGE);
239
+ });
240
+
241
+ it('replaces a topic in place, keeping its position', () => {
242
+ const catalog = DocsCatalog.fromBuiltins({
243
+ 'getting-started': '/cli/getting-started.doc.mjs',
244
+ tokens: '/cli/tokens.doc.mjs',
245
+ });
246
+ expect(
247
+ catalog.add(record({name: 'getting-started', replaces: 'getting-started'})),
248
+ ).toBeNull();
249
+ expect(catalog.names()).toEqual(['getting-started', 'tokens']);
250
+ const entry = catalog.resolve('getting-started');
251
+ expect(entry.package).toBe('@acme/widgets');
252
+ expect(entry.replaces).toBe('getting-started');
253
+ });
254
+
255
+ it('leaves the old name as an alias when the replacement renames it', () => {
256
+ const catalog = DocsCatalog.fromBuiltins({
257
+ 'getting-started': '/cli/getting-started.doc.mjs',
258
+ });
259
+ catalog.add(record({name: 'setup', replaces: 'getting-started'}));
260
+ expect(catalog.names()).toEqual(['setup']);
261
+ expect(catalog.resolve('getting-started').name).toBe('setup');
262
+ expect(catalog.resolve('setup').name).toBe('setup');
263
+ });
264
+
265
+ it('keeps every name a twice-renamed topic has answered to', () => {
266
+ const catalog = DocsCatalog.fromBuiltins({
267
+ 'getting-started': '/cli/getting-started.doc.mjs',
268
+ });
269
+ catalog.add(record({name: 'setup', replaces: 'getting-started'}));
270
+ catalog.add(
271
+ record({name: 'install', package: '@acme/later', replaces: 'setup'}),
272
+ );
273
+ expect(catalog.resolve('getting-started').name).toBe('install');
274
+ expect(catalog.resolve('setup').name).toBe('install');
275
+ });
276
+
277
+ it('warns, and lets the later package win, when two replace one topic', () => {
278
+ const catalog = DocsCatalog.fromBuiltins({tokens: '/cli/tokens.doc.mjs'});
279
+ expect(catalog.add(record({name: 'tokens', replaces: 'tokens'}))).toBeNull();
280
+ const issue = catalog.add(
281
+ record({name: 'tokens', package: '@acme/later', replaces: 'tokens'}),
282
+ );
283
+ expect(issue).toMatchObject({code: 'duplicate_doc', severity: 'warning'});
284
+ expect(issue.message).toContain('@acme/later is configured later, so it wins');
285
+ expect(catalog.resolve('tokens').package).toBe('@acme/later');
286
+ });
287
+
288
+ it('records an extension against its target rather than adding a topic', () => {
289
+ const catalog = DocsCatalog.fromBuiltins({theme: '/cli/theme.doc.mjs'});
290
+ expect(
291
+ catalog.add(record({name: 'theme-extra', extendsTopic: 'theme'})),
292
+ ).toBeNull();
293
+ expect(catalog.names()).toEqual(['theme']);
294
+ expect(catalog.resolve('theme').extensions).toEqual([
295
+ {package: '@acme/widgets', path: '/pkg/docs/deploying.doc.mjs'},
296
+ ]);
297
+ });
298
+
299
+ it('reports a replace/extend target that no package provides', () => {
300
+ const catalog = DocsCatalog.fromBuiltins({tokens: '/cli/tokens.doc.mjs'});
301
+ expect(catalog.add(record({replaces: 'nope'}))).toMatchObject({
302
+ code: 'invalid_doc',
303
+ severity: 'error',
304
+ });
305
+ expect(catalog.add(record({extendsTopic: 'nope'}))).toMatchObject({
306
+ code: 'invalid_doc',
307
+ severity: 'error',
308
+ });
309
+ expect(catalog.names()).toEqual(['tokens']);
310
+ });
311
+ });
312
+
313
+ describe('mergeTopic', () => {
314
+ const base = {
315
+ title: 'Theme',
316
+ description: 'Theming.',
317
+ sections: [
318
+ {title: 'Install', content: [{type: 'prose', text: 'npm i'}]},
319
+ {title: 'Tokens', content: [{type: 'prose', text: 'use tokens'}]},
320
+ ],
321
+ };
322
+
323
+ it('replaces a section by title, appends a new one, and leaves the rest', () => {
324
+ const merged = mergeTopic(base, {
325
+ sections: [
326
+ {title: 'Install', content: [{type: 'prose', text: 'yarn add'}]},
327
+ {title: 'Internal', content: [{type: 'prose', text: 'the meta way'}]},
328
+ ],
329
+ });
330
+ expect(merged.sections.map(s => s.title)).toEqual(['Install', 'Tokens', 'Internal']);
331
+ expect(merged.sections[0].content[0].text).toBe('yarn add');
332
+ expect(merged.sections[1].content[0].text).toBe('use tokens');
333
+ // The base is untouched.
334
+ expect(base.sections[0].content[0].text).toBe('npm i');
335
+ });
336
+
337
+ it('takes the title and description only when the overlay states them', () => {
338
+ expect(mergeTopic(base, {sections: []}).title).toBe('Theme');
339
+ expect(mergeTopic(base, {title: 'Theming', sections: []}).title).toBe('Theming');
340
+ });
341
+ });
@@ -38,15 +38,16 @@ export function loadIntegrations(specs?: string[], { cwd }?: {
38
38
  }): Promise<LoadedIntegration[]>;
39
39
  /**
40
40
  * A fully-resolved, loaded integration. Identity (`name`, `version`) comes from
41
- * the package's package.json; the `components`/`templates`/`codemods` roots are
42
- * absolute paths resolved from the manifest. The `__`-prefixed fields are
43
- * internal bookkeeping used by validate-integration and Project.
41
+ * the package's package.json; the `components`/`templates`/`codemods`/`docs`
42
+ * roots are absolute paths resolved from the manifest. The `__`-prefixed fields
43
+ * are internal bookkeeping used by validate-integration and Project.
44
44
  * @typedef {object} LoadedIntegration
45
45
  * @property {string} name
46
46
  * @property {string} [version]
47
47
  * @property {string} [components]
48
48
  * @property {string} [templates]
49
49
  * @property {string} [codemods]
50
+ * @property {string} [docs]
50
51
  * @property {string} [issuesUrl]
51
52
  * @property {string} __spec
52
53
  * @property {string} __packageDir
@@ -58,9 +59,9 @@ export function loadIntegrations(specs?: string[], { cwd }?: {
58
59
  export const MANIFEST_BASENAMES: string[];
59
60
  /**
60
61
  * A fully-resolved, loaded integration. Identity (`name`, `version`) comes from
61
- * the package's package.json; the `components`/`templates`/`codemods` roots are
62
- * absolute paths resolved from the manifest. The `__`-prefixed fields are
63
- * internal bookkeeping used by validate-integration and Project.
62
+ * the package's package.json; the `components`/`templates`/`codemods`/`docs`
63
+ * roots are absolute paths resolved from the manifest. The `__`-prefixed fields
64
+ * are internal bookkeeping used by validate-integration and Project.
64
65
  */
65
66
  export type LoadedIntegration = {
66
67
  name: string;
@@ -68,6 +69,7 @@ export type LoadedIntegration = {
68
69
  components?: string | undefined;
69
70
  templates?: string | undefined;
70
71
  codemods?: string | undefined;
72
+ docs?: string | undefined;
71
73
  issuesUrl?: string | undefined;
72
74
  __spec: string;
73
75
  __packageDir: string;
@@ -6,7 +6,7 @@
6
6
  * Integrations are PACKAGE NAMES listed in astryx.config.{ts,mjs,js}. Each
7
7
  * package declares a single conventional root manifest sibling to its
8
8
  * package.json — astryx.integration.{ts,mjs,js} — which contributes
9
- * components/templates/codemods roots and an optional issuesUrl. Identity
9
+ * components/templates/codemods/docs roots and an optional issuesUrl. Identity
10
10
  * (name, version) comes from the package's package.json, not the manifest.
11
11
  */
12
12
 
@@ -18,15 +18,16 @@ import {loadModuleWithParser, findPresentFiles} from '../fs/module-loader.mjs';
18
18
 
19
19
  /**
20
20
  * A fully-resolved, loaded integration. Identity (`name`, `version`) comes from
21
- * the package's package.json; the `components`/`templates`/`codemods` roots are
22
- * absolute paths resolved from the manifest. The `__`-prefixed fields are
23
- * internal bookkeeping used by validate-integration and Project.
21
+ * the package's package.json; the `components`/`templates`/`codemods`/`docs`
22
+ * roots are absolute paths resolved from the manifest. The `__`-prefixed fields
23
+ * are internal bookkeeping used by validate-integration and Project.
24
24
  * @typedef {object} LoadedIntegration
25
25
  * @property {string} name
26
26
  * @property {string} [version]
27
27
  * @property {string} [components]
28
28
  * @property {string} [templates]
29
29
  * @property {string} [codemods]
30
+ * @property {string} [docs]
30
31
  * @property {string} [issuesUrl]
31
32
  * @property {string} __spec
32
33
  * @property {string} __packageDir
@@ -187,6 +188,7 @@ export async function loadIntegrations(specs = [], {cwd = process.cwd()} = {}) {
187
188
  components: resolveRoot(manifest.components),
188
189
  templates: resolveRoot(manifest.templates),
189
190
  codemods: resolveRoot(manifest.codemods),
191
+ docs: resolveRoot(manifest.docs),
190
192
  issuesUrl: manifest.issuesUrl,
191
193
  __spec: spec,
192
194
  __packageDir: packageDir,
@@ -23,6 +23,7 @@ import * as fs from 'node:fs';
23
23
  import {discoverIntegrationCodemods} from '../../assets/codemods/integration-discovery.mjs';
24
24
  import {discoverIntegrationTemplatesForOne} from '../discovery/template-adapter.mjs';
25
25
  import * as componentDiscovery from '../discovery/component-discovery.mjs';
26
+ import {discoverIntegrationDocs} from '../discovery/docs-discovery.mjs';
26
27
 
27
28
  /**
28
29
  * @typedef {import('./issue').AstryxIntegrationIssue} Issue
@@ -37,12 +38,12 @@ export function issueError(code, message) {
37
38
  /**
38
39
  * Verify each declared contribution root exists on disk. A declared-but-missing
39
40
  * root is a `missing_root` error.
40
- * @param {{components?: string, templates?: string, codemods?: string}} resolved
41
+ * @param {{components?: string, templates?: string, codemods?: string, docs?: string}} resolved
41
42
  * absolute resolved roots (undefined when not declared)
42
43
  * @param {Issue[]} issues
43
44
  */
44
45
  function checkRoots(resolved, issues) {
45
- const kinds = /** @type {const} */ (['components', 'templates', 'codemods']);
46
+ const kinds = /** @type {const} */ (['components', 'templates', 'codemods', 'docs']);
46
47
  for (const kind of kinds) {
47
48
  const root = resolved[kind];
48
49
  if (root == null) continue;
@@ -124,6 +125,31 @@ async function checkComponents(integration, issues) {
124
125
  }
125
126
  }
126
127
 
128
+ /**
129
+ * Validate the integration's doc topics via the landed discovery. A doc that
130
+ * cannot be loaded, is not a usable topic, or collides with a sibling is
131
+ * reported as an `invalid_doc` error.
132
+ *
133
+ * Only per-file problems are visible here: a topic that collides with another
134
+ * PACKAGE's, or names a `replaces`/`extends` target that does not exist, can
135
+ * only be judged once every integration is resolved together, so those are
136
+ * raised by `Project.docs()` instead.
137
+ *
138
+ * @param {LoadedIntegration} integration loaded-integration-shaped object
139
+ * @param {Issue[]} issues
140
+ */
141
+ async function checkDocs(integration, issues) {
142
+ if (!integration.docs || !fs.existsSync(integration.docs)) return;
143
+ try {
144
+ const {errors} = await discoverIntegrationDocs(integration);
145
+ for (const e of errors) {
146
+ issues.push(issueError('invalid_doc', e.message));
147
+ }
148
+ } catch (err) {
149
+ issues.push(issueError('invalid_doc', /** @type {any} */ (err).message));
150
+ }
151
+ }
152
+
127
153
  /**
128
154
  * Run every contribution validator against a loaded-integration-shaped object.
129
155
  * @param {LoadedIntegration} integration
@@ -133,6 +159,7 @@ async function runContributionChecks(integration, issues) {
133
159
  await checkCodemods(integration, issues);
134
160
  await checkTemplates(integration, issues);
135
161
  await checkComponents(integration, issues);
162
+ await checkDocs(integration, issues);
136
163
  }
137
164
 
138
165
  /**
@@ -161,6 +188,7 @@ export async function validateLoadedIntegration(loaded) {
161
188
  components: loaded.components,
162
189
  templates: loaded.templates,
163
190
  codemods: loaded.codemods,
191
+ docs: loaded.docs,
164
192
  },
165
193
  issues,
166
194
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astryxdesign/cli",
3
- "version": "0.4.5",
3
+ "version": "0.4.6-canary.cea9ecd",
4
4
  "displayName": "CLI",
5
5
  "description": "Scaffold projects, browse templates, generate themes, and get agent-ready docs from the command line.",
6
6
  "author": "Meta Open Source",
@@ -87,10 +87,10 @@
87
87
  "zod": "^4.4.3"
88
88
  },
89
89
  "peerDependencies": {
90
- "@astryxdesign/charts": "*",
91
- "@astryxdesign/core": "*",
92
- "@astryxdesign/lab": "*",
93
- "@astryxdesign/theme-neutral": "*",
90
+ "@astryxdesign/charts": "0.4.6-canary.cea9ecd",
91
+ "@astryxdesign/core": "0.4.6-canary.cea9ecd",
92
+ "@astryxdesign/lab": "0.4.6-canary.cea9ecd",
93
+ "@astryxdesign/theme-neutral": "0.4.6-canary.cea9ecd",
94
94
  "gpt-tokenizer": "^3.4.0"
95
95
  },
96
96
  "peerDependenciesMeta": {
@@ -108,10 +108,10 @@
108
108
  }
109
109
  },
110
110
  "devDependencies": {
111
- "@astryxdesign/charts": "*",
112
- "@astryxdesign/core": "*",
113
- "@astryxdesign/lab": "*",
114
- "@astryxdesign/theme-neutral": "*",
111
+ "@astryxdesign/charts": "0.4.6-canary.cea9ecd",
112
+ "@astryxdesign/core": "0.4.6-canary.cea9ecd",
113
+ "@astryxdesign/lab": "0.4.6-canary.cea9ecd",
114
+ "@astryxdesign/theme-neutral": "0.4.6-canary.cea9ecd",
115
115
  "gpt-tokenizer": "^3.4.0"
116
116
  },
117
117
  "scripts": {