@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.
- package/CHANGELOG.md +26 -0
- package/api/component/_adapter.d.mts +22 -5
- package/api/component/_adapter.mjs +51 -3
- package/api/docs/_adapter.d.mts +37 -8
- package/api/docs/_adapter.mjs +70 -41
- package/api/docs/detail/detail.d.mts +2 -0
- package/api/docs/detail/detail.mjs +12 -10
- package/api/docs/detail/section/section.d.mts +2 -0
- package/api/docs/detail/section/section.mjs +1 -0
- package/api/docs/docs.d.mts +2 -0
- package/api/docs/docs.doc.mjs +11 -2
- package/api/docs/docs.mjs +2 -1
- package/api/docs/docs.type.d.mts +15 -0
- package/api/docs/docs.type.mjs +6 -0
- package/api/docs/integrationDocs.test.mjs +208 -0
- package/api/docs/list/list.d.mts +5 -1
- package/api/docs/list/list.mjs +30 -14
- package/api/init/run/run.mjs +9 -4
- package/api/integration/validate-integration.mjs +2 -1
- package/api/integration/validate-integration.type.d.mts +1 -0
- package/api/integration/validate-integration.type.mjs +1 -0
- package/api/search/search.mjs +26 -15
- package/api/upgrade/_adapter.d.mts +2 -2
- package/api/upgrade/_adapter.mjs +7 -3
- package/api/upgrade/run/run.mjs +1 -1
- package/assets/codemods/transforms/v0.1.0/__tests__/drop-xds-prefix-imports.test.mjs +245 -0
- package/assets/codemods/transforms/v0.1.0/drop-xds-prefix-imports.mjs +139 -13
- package/assets/docs/cli-integrations.doc.mjs +41 -3
- package/assets/docs/layout.doc.dense.mjs +275 -30
- package/assets/docs/layout.doc.mjs +412 -78
- package/assets/docs/theme.doc.mjs +3 -3
- package/assets/templates/blocks/components/AvatarGroup/AvatarGroupInteractive.doc.mjs +15 -0
- package/assets/templates/blocks/components/AvatarGroup/AvatarGroupInteractive.tsx +47 -0
- package/assets/templates/blocks/components/AvatarGroup/AvatarGroupShowcase.doc.mjs +1 -1
- package/assets/templates/blocks/components/Breadcrumbs/BreadcrumbsCustomSeparator.tsx +18 -5
- package/assets/templates/blocks/components/Selector/SelectorOptionDescriptions.doc.mjs +14 -0
- package/assets/templates/blocks/components/Selector/SelectorOptionDescriptions.tsx +76 -0
- package/assets/templates/pages/dashboard-cohort-funnel/page.tsx +914 -0
- package/assets/templates/pages/dashboard-cohort-funnel/template.doc.mjs +12 -0
- package/assets/templates/pages/dashboard-data/page.tsx +894 -0
- package/assets/templates/pages/dashboard-data/template.doc.mjs +13 -0
- package/assets/templates/pages/dashboard-executive-summary/page.tsx +874 -0
- package/assets/templates/pages/dashboard-executive-summary/template.doc.mjs +13 -0
- package/assets/templates/pages/dashboard-project-status/page.tsx +1139 -0
- package/assets/templates/pages/dashboard-project-status/template.doc.mjs +13 -0
- package/assets/templates/pages/dashboard-service-monitoring/page.tsx +1590 -0
- package/assets/templates/pages/dashboard-service-monitoring/template.doc.mjs +12 -0
- package/authoring/doctypes/_schema.d.mts +2 -0
- package/authoring/doctypes/_schema.mjs +5 -0
- package/authoring/doctypes/reference/reference.doc.mjs +14 -0
- package/authoring/doctypes/reference/type.ts +12 -0
- package/authoring/doctypes/template/type.ts +2 -0
- package/authoring/integration/integration.doc.mjs +10 -1
- package/authoring/integration/parse.d.mts +1 -0
- package/authoring/integration/parse.mjs +1 -0
- package/authoring/integration/parse.test.mjs +1 -0
- package/authoring/integration/type.ts +5 -0
- package/clients/cli/commands/component-ownership.test.mjs +63 -3
- package/clients/cli/commands/theme-build.doc.mjs +1 -1
- package/foundation/agent-docs/agent-docs.d.mts +19 -3
- package/foundation/agent-docs/agent-docs.mjs +30 -11
- package/foundation/agent-docs/agent-docs.test.mjs +38 -0
- package/foundation/config/project.d.mts +16 -0
- package/foundation/config/project.mjs +65 -4
- package/foundation/config/project.test.mjs +66 -0
- package/foundation/discovery/docs-discovery.d.mts +185 -0
- package/foundation/discovery/docs-discovery.mjs +544 -0
- package/foundation/discovery/docs-discovery.test.mjs +341 -0
- package/foundation/integrations/integrations.d.mts +8 -6
- package/foundation/integrations/integrations.mjs +6 -4
- package/foundation/integrations/validate-contributions.mjs +30 -2
- 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`
|
|
42
|
-
* absolute paths resolved from the manifest. The `__`-prefixed fields
|
|
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`
|
|
62
|
-
* absolute paths resolved from the manifest. The `__`-prefixed fields
|
|
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`
|
|
22
|
-
* absolute paths resolved from the manifest. The `__`-prefixed fields
|
|
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.
|
|
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": {
|