@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,12 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/** @type {import('@astryxdesign/cli/authoring').TemplateDoc} */
|
|
4
|
+
export const doc = {
|
|
5
|
+
type: 'page',
|
|
6
|
+
name: 'Service Monitoring Dashboard',
|
|
7
|
+
displayName: 'Service Monitoring Dashboard',
|
|
8
|
+
description:
|
|
9
|
+
'Live-ops service-health dashboard: traffic-light KPI tiles with sparklines, and multi-line latency and request-volume charts with a 1h/1d/7d window, beside a triage rail split into two independently scrolling sections: active alerts over a worst-first per-service breakdown with inline status coloring. Global environment, region, and auto-refresh controls; the rail folds into the content column as two cards below 1024px.',
|
|
10
|
+
isReady: false,
|
|
11
|
+
category: 'Dashboard - Monitoring',
|
|
12
|
+
};
|
|
@@ -57,6 +57,8 @@ export const FunctionDocKindSchema: z.ZodObject<{
|
|
|
57
57
|
/** New-format stamped generic reference/topic doc (`type: 'generic'`). */
|
|
58
58
|
export const GenericDocKindSchema: z.ZodObject<{
|
|
59
59
|
type: z.ZodLiteral<"generic">;
|
|
60
|
+
replaces: z.ZodOptional<z.ZodString>;
|
|
61
|
+
extends: z.ZodOptional<z.ZodString>;
|
|
60
62
|
name: z.ZodString;
|
|
61
63
|
displayName: z.ZodOptional<z.ZodString>;
|
|
62
64
|
description: z.ZodOptional<z.ZodString>;
|
|
@@ -94,6 +94,11 @@ export const GenericDocKindSchema = z
|
|
|
94
94
|
.object({
|
|
95
95
|
...BaseDocFields,
|
|
96
96
|
type: z.literal('generic'),
|
|
97
|
+
// Declared rather than left to the passthrough: these two are read by
|
|
98
|
+
// docs discovery to resolve one topic against another, so a non-string
|
|
99
|
+
// should fail at the load boundary, not halfway through resolution.
|
|
100
|
+
replaces: z.string().optional(),
|
|
101
|
+
extends: z.string().optional(),
|
|
97
102
|
})
|
|
98
103
|
.passthrough();
|
|
99
104
|
|
|
@@ -51,6 +51,20 @@ export const doc = {
|
|
|
51
51
|
type: 'string',
|
|
52
52
|
description: "Navigation category: 'guide' or 'foundations'.",
|
|
53
53
|
},
|
|
54
|
+
{
|
|
55
|
+
name: 'replaces',
|
|
56
|
+
type: 'string',
|
|
57
|
+
description:
|
|
58
|
+
"Name of an existing topic this doc takes the place of. Authored by an integration that serves its own guide instead of the built-in one: on a doc of the same name it swaps the content, and on a doc of another name it also leaves the old name as an alias so `astryx docs <old>` still resolves. Exclusive with `extends`.",
|
|
59
|
+
example: "'getting-started'",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: 'extends',
|
|
63
|
+
type: 'string',
|
|
64
|
+
description:
|
|
65
|
+
'Name of an existing topic this doc merges onto, section by section: a section whose title matches one in the base replaces it, a section the base does not have is appended. For correcting or adding to a topic rather than owning it. Exclusive with `replaces`.',
|
|
66
|
+
example: "'theme'",
|
|
67
|
+
},
|
|
54
68
|
{
|
|
55
69
|
name: 'sections',
|
|
56
70
|
type: 'ReferenceSection[]',
|
|
@@ -68,6 +68,18 @@ export interface ReferenceDoc {
|
|
|
68
68
|
description: string;
|
|
69
69
|
/** Navigation category: 'guide' or 'foundations'. */
|
|
70
70
|
category?: string;
|
|
71
|
+
/** Name of an existing topic this doc takes the place of. Authored by an
|
|
72
|
+
* integration whose guide should be served instead of the built-in one —
|
|
73
|
+
* `replaces: 'getting-started'` on a doc named `getting-started` swaps the
|
|
74
|
+
* content, and on a doc named something else swaps it and leaves the old
|
|
75
|
+
* name as an alias, so `astryx docs getting-started` still resolves.
|
|
76
|
+
* Ignored on a built-in topic (there is nothing above it to replace). */
|
|
77
|
+
replaces?: string;
|
|
78
|
+
/** Name of an existing topic this doc merges onto, section by section: a
|
|
79
|
+
* section whose title matches one in the base replaces it, and a section
|
|
80
|
+
* the base does not have is appended. For correcting or adding to a topic
|
|
81
|
+
* rather than owning it — `replaces` and `extends` are exclusive. */
|
|
82
|
+
extends?: string;
|
|
71
83
|
/** Ordered sections that make up the doc. */
|
|
72
84
|
sections: ReferenceSection[];
|
|
73
85
|
/** Token category for foundational docs that map to a token section.
|
|
@@ -15,7 +15,8 @@ export const doc = {
|
|
|
15
15
|
description:
|
|
16
16
|
'The astryx.integration.* manifest that sits beside an integration ' +
|
|
17
17
|
"package's package.json. Points the CLI at the package's components, " +
|
|
18
|
-
'templates, and
|
|
18
|
+
'templates, codemods, and doc topics, and where to file issues. Every ' +
|
|
19
|
+
'field is optional.',
|
|
19
20
|
appliesTo: 'astryx.integration.{ts,mjs,js}',
|
|
20
21
|
fields: [
|
|
21
22
|
{
|
|
@@ -38,6 +39,13 @@ export const doc = {
|
|
|
38
39
|
description: 'Relative path to the codemods root (resolved to absolute).',
|
|
39
40
|
example: "'./codemods'",
|
|
40
41
|
},
|
|
42
|
+
{
|
|
43
|
+
name: 'docs',
|
|
44
|
+
type: 'string',
|
|
45
|
+
description:
|
|
46
|
+
'Relative path to the reference-docs (topics) root (resolved to absolute). Every {topic}.doc.{ts,mjs,js} under it is served by `astryx docs` beside the built-in topics; a topic may also declare `replaces` or `extends` to take the place of a built-in one or merge onto it.',
|
|
47
|
+
example: "'./docs'",
|
|
48
|
+
},
|
|
41
49
|
{
|
|
42
50
|
name: 'issuesUrl',
|
|
43
51
|
type: 'string',
|
|
@@ -52,6 +60,7 @@ export const doc = {
|
|
|
52
60
|
components: './src/components',
|
|
53
61
|
templates: './src/templates',
|
|
54
62
|
codemods: './codemods',
|
|
63
|
+
docs: './docs',
|
|
55
64
|
issuesUrl: 'https://github.com/acme/widgets/issues',
|
|
56
65
|
};`,
|
|
57
66
|
},
|
|
@@ -27,6 +27,7 @@ declare const integrationSchema: z.ZodObject<{
|
|
|
27
27
|
components: z.ZodOptional<z.ZodString>;
|
|
28
28
|
templates: z.ZodOptional<z.ZodString>;
|
|
29
29
|
codemods: z.ZodOptional<z.ZodString>;
|
|
30
|
+
docs: z.ZodOptional<z.ZodString>;
|
|
30
31
|
issuesUrl: z.ZodOptional<z.ZodString>;
|
|
31
32
|
}, z.core.$strict>;
|
|
32
33
|
export {};
|
|
@@ -26,6 +26,7 @@ describe('parseIntegration (load boundary)', () => {
|
|
|
26
26
|
expect(parseIntegration({components: './src'})).toEqual({
|
|
27
27
|
components: './src',
|
|
28
28
|
});
|
|
29
|
+
expect(parseIntegration({docs: './docs'})).toEqual({docs: './docs'});
|
|
29
30
|
expect(() =>
|
|
30
31
|
parseIntegration({components: './c', issuesUrl: 'https://example.com/i'}),
|
|
31
32
|
).not.toThrow();
|
|
@@ -14,6 +14,11 @@ export interface AstryxIntegration {
|
|
|
14
14
|
templates?: string;
|
|
15
15
|
/** Relative path to the codemods root (resolved to absolute). */
|
|
16
16
|
codemods?: string;
|
|
17
|
+
/** Relative path to the reference-docs (topics) root (resolved to
|
|
18
|
+
* absolute). Every `{topic}.doc.{ts,mjs,js}` under it is a topic the CLI
|
|
19
|
+
* serves from `astryx docs`, alongside the built-in ones. A topic may also
|
|
20
|
+
* `replace` or `extend` a built-in topic; see the ReferenceDoc type. */
|
|
21
|
+
docs?: string;
|
|
17
22
|
/** Where to file issues/feedback for this integration. */
|
|
18
23
|
issuesUrl?: string;
|
|
19
24
|
}
|
|
@@ -47,7 +47,12 @@ const INTEGRATION_ISSUES = 'https://example.com/meta/issues';
|
|
|
47
47
|
* Returns the absolute `components` dir so the Project.load mock can hand back a
|
|
48
48
|
* resolved integration entry.
|
|
49
49
|
*/
|
|
50
|
-
function createFixture({
|
|
50
|
+
function createFixture({
|
|
51
|
+
withSource = true,
|
|
52
|
+
extraComponent = null,
|
|
53
|
+
packageExports = null,
|
|
54
|
+
entryPoint = null,
|
|
55
|
+
} = {}) {
|
|
51
56
|
const realCoreDir = path.resolve(import.meta.dirname, '..', '..', '..', '..', 'core');
|
|
52
57
|
const coreDir = path.join(tmpDir, 'packages', 'core');
|
|
53
58
|
fs.mkdirSync(path.dirname(coreDir), {recursive: true});
|
|
@@ -58,7 +63,11 @@ function createFixture({withSource = true, extraComponent = null} = {}) {
|
|
|
58
63
|
fs.mkdirSync(compDir, {recursive: true});
|
|
59
64
|
fs.writeFileSync(
|
|
60
65
|
path.join(intDir, 'package.json'),
|
|
61
|
-
JSON.stringify({
|
|
66
|
+
JSON.stringify({
|
|
67
|
+
name: INTEGRATION_NAME,
|
|
68
|
+
version: '1.2.3',
|
|
69
|
+
...(packageExports ? {exports: packageExports} : {}),
|
|
70
|
+
}),
|
|
62
71
|
);
|
|
63
72
|
fs.writeFileSync(
|
|
64
73
|
path.join(compDir, 'MetaAppShell.doc.mjs'),
|
|
@@ -81,6 +90,20 @@ function createFixture({withSource = true, extraComponent = null} = {}) {
|
|
|
81
90
|
);
|
|
82
91
|
}
|
|
83
92
|
|
|
93
|
+
// A component whose directory is an entry point exporting several
|
|
94
|
+
// components, so the directory name and the component name differ.
|
|
95
|
+
if (entryPoint) {
|
|
96
|
+
const entryDir = path.join(compDir, entryPoint.directory);
|
|
97
|
+
fs.mkdirSync(entryDir, {recursive: true});
|
|
98
|
+
const ownSpecifier = entryPoint.importSpec
|
|
99
|
+
? `\n import: '${entryPoint.importSpec}',`
|
|
100
|
+
: '';
|
|
101
|
+
fs.writeFileSync(
|
|
102
|
+
path.join(entryDir, `${entryPoint.component}.doc.mjs`),
|
|
103
|
+
`export const docs = {\n name: '${entryPoint.component}',${ownSpecifier}\n usage: { description: '${entryPoint.component} from an entry point.' },\n};\n`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
84
107
|
const integration = {
|
|
85
108
|
name: INTEGRATION_NAME,
|
|
86
109
|
version: '1.2.3',
|
|
@@ -88,6 +111,7 @@ function createFixture({withSource = true, extraComponent = null} = {}) {
|
|
|
88
111
|
templates: undefined,
|
|
89
112
|
codemods: undefined,
|
|
90
113
|
issuesUrl: INTEGRATION_ISSUES,
|
|
114
|
+
__packageDir: intDir,
|
|
91
115
|
};
|
|
92
116
|
projectLoadMock.mockResolvedValue({
|
|
93
117
|
integrations: [INTEGRATION_NAME],
|
|
@@ -162,7 +186,9 @@ describe('component() — integration ownership via config', () => {
|
|
|
162
186
|
expect(result.data.name).toBe('MetaAppShell');
|
|
163
187
|
expect(result.data.package).toBe(INTEGRATION_NAME);
|
|
164
188
|
expect(result.data.sourceAvailable).toBe(true);
|
|
165
|
-
|
|
189
|
+
// This fixture declares no `exports`, so there is no subpath to import
|
|
190
|
+
// from and the specifier is the package root.
|
|
191
|
+
expect(result.data.import).toBe(INTEGRATION_NAME);
|
|
166
192
|
});
|
|
167
193
|
|
|
168
194
|
it('--package resolves the integration component', async () => {
|
|
@@ -217,6 +243,40 @@ describe('component() — integration ownership via config', () => {
|
|
|
217
243
|
expect(result.data.package).toBe(INTEGRATION_NAME);
|
|
218
244
|
});
|
|
219
245
|
|
|
246
|
+
it('resolves the import specifier against the package exports map', async () => {
|
|
247
|
+
// The doc sits in a `Toolbar` directory but the component is
|
|
248
|
+
// `ToolbarSearch`, so a specifier built from the component name would
|
|
249
|
+
// point at a subpath the package does not export.
|
|
250
|
+
createFixture({
|
|
251
|
+
packageExports: {'.': './index.js', './Toolbar': './components/Toolbar/index.js'},
|
|
252
|
+
entryPoint: {directory: 'Toolbar', component: 'ToolbarSearch'},
|
|
253
|
+
});
|
|
254
|
+
const result = await component('ToolbarSearch', {cwd: tmpDir});
|
|
255
|
+
expect(result.data.import).toBe(`${INTEGRATION_NAME}/Toolbar`);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('falls back to the package root when the directory is not an exported subpath', async () => {
|
|
259
|
+
createFixture({
|
|
260
|
+
packageExports: {'.': './index.js'},
|
|
261
|
+
entryPoint: {directory: 'Toolbar', component: 'ToolbarSearch'},
|
|
262
|
+
});
|
|
263
|
+
const result = await component('ToolbarSearch', {cwd: tmpDir});
|
|
264
|
+
expect(result.data.import).toBe(INTEGRATION_NAME);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('keeps a specifier the doc file states for itself', async () => {
|
|
268
|
+
createFixture({
|
|
269
|
+
packageExports: {'.': './index.js', './Toolbar': './components/Toolbar/index.js'},
|
|
270
|
+
entryPoint: {
|
|
271
|
+
directory: 'Toolbar',
|
|
272
|
+
component: 'ToolbarSearch',
|
|
273
|
+
importSpec: `${INTEGRATION_NAME}/Toolbar/Search`,
|
|
274
|
+
},
|
|
275
|
+
});
|
|
276
|
+
const result = await component('ToolbarSearch', {cwd: tmpDir});
|
|
277
|
+
expect(result.data.import).toBe(`${INTEGRATION_NAME}/Toolbar/Search`);
|
|
278
|
+
});
|
|
279
|
+
|
|
220
280
|
it('JSON list includes integration components as {name, package} objects', async () => {
|
|
221
281
|
createFixture();
|
|
222
282
|
const result = await component(undefined, {cwd: tmpDir, list: true});
|
|
@@ -19,7 +19,7 @@ export const doc = {
|
|
|
19
19
|
'Compiles a file that calls defineTheme() into a scoped CSS file, a JS module, and ' +
|
|
20
20
|
'type declarations: the exact CSS the <Theme> runtime emits. Takes any number of theme ' +
|
|
21
21
|
'files and compiles them in one process, in argument order, stopping at the first ' +
|
|
22
|
-
'failure
|
|
22
|
+
'failure; an app with several themes does not need a shell loop. With --check it writes ' +
|
|
23
23
|
'nothing and instead reports whether the committed outputs have drifted from source. ' +
|
|
24
24
|
'When a separate build step emits the icon registry, --icons-specifier declares the ' +
|
|
25
25
|
'fully specified module path that the generated JS should import.',
|
|
@@ -102,16 +102,28 @@ export function detectStylingSystem(targetDir: string): "stylex" | "tailwind" |
|
|
|
102
102
|
* configured (see {@link detectStylingSystem}) so the agent never reaches for a
|
|
103
103
|
* styling path that isn't compiled here.
|
|
104
104
|
*
|
|
105
|
+
* `topics` is the project's doc-topic list. Passing it is what puts an
|
|
106
|
+
* integration's topics — including one it contributed in place of a built-in —
|
|
107
|
+
* in front of the agent by name; without it the block falls back to the CLI's
|
|
108
|
+
* own topics, because resolving a project's catalog is async and this is not.
|
|
109
|
+
*
|
|
110
|
+
* Either way the list now includes the hyphenated topics. The fallback scan
|
|
111
|
+
* matched `\w+`, which does not match `-`, so five real topics were missing
|
|
112
|
+
* from every block ever written — `getting-started` and `cli-integrations`
|
|
113
|
+
* among them. An agent cannot ask for a topic it was never told about, and
|
|
114
|
+
* `getting-started` is the one it should reach for first.
|
|
115
|
+
*
|
|
105
116
|
* @param {string} version
|
|
106
|
-
* @param {{coreDir?: string|null, invocation?: string, stylingSystem?: 'stylex'|'tailwind'|'css', zh?: boolean, lang?: string}} [options]
|
|
117
|
+
* @param {{coreDir?: string|null, invocation?: string, stylingSystem?: 'stylex'|'tailwind'|'css', zh?: boolean, lang?: string, topics?: string[]}} [options]
|
|
107
118
|
* @returns {string}
|
|
108
119
|
*/
|
|
109
|
-
export function generateCompressedIndex(version: string, { coreDir, invocation, stylingSystem }?: {
|
|
120
|
+
export function generateCompressedIndex(version: string, { coreDir, invocation, stylingSystem, topics }?: {
|
|
110
121
|
coreDir?: string | null;
|
|
111
122
|
invocation?: string;
|
|
112
123
|
stylingSystem?: "stylex" | "tailwind" | "css";
|
|
113
124
|
zh?: boolean;
|
|
114
125
|
lang?: string;
|
|
126
|
+
topics?: string[];
|
|
115
127
|
}): string;
|
|
116
128
|
/**
|
|
117
129
|
* Get Astryx version from core package.
|
|
@@ -186,12 +198,16 @@ export function removeAgentDocs(targetDir: string): void;
|
|
|
186
198
|
* @param {string} [options.agent] - Tool preset: 'claude', 'cursor', 'codex', 'hermes', 'all'
|
|
187
199
|
* @param {string[]} [options.paths] - Explicit paths (overrides agent/auto-detect)
|
|
188
200
|
* @param {boolean} [options.onlyReplace] - Only update files that already have Astryx markers (for upgrades)
|
|
201
|
+
* @param {string[]} [options.topics] - Doc topics to list in the block; defaults
|
|
202
|
+
* to the CLI's own. Pass the project's catalog (`(await project.docs()).names()`)
|
|
203
|
+
* so an integration's topics reach the agent.
|
|
189
204
|
* @returns {string[]} List of files written
|
|
190
205
|
*/
|
|
191
|
-
export function installAgentDocs(targetDir: string, { zh, lang, agent, paths, onlyReplace }?: {
|
|
206
|
+
export function installAgentDocs(targetDir: string, { zh, lang, agent, paths, onlyReplace, topics }?: {
|
|
192
207
|
zh?: boolean | undefined;
|
|
193
208
|
lang?: string | undefined;
|
|
194
209
|
agent?: string | undefined;
|
|
195
210
|
paths?: string[] | undefined;
|
|
196
211
|
onlyReplace?: boolean | undefined;
|
|
212
|
+
topics?: string[] | undefined;
|
|
197
213
|
}): string[];
|
|
@@ -299,11 +299,22 @@ export function detectStylingSystem(targetDir) {
|
|
|
299
299
|
* configured (see {@link detectStylingSystem}) so the agent never reaches for a
|
|
300
300
|
* styling path that isn't compiled here.
|
|
301
301
|
*
|
|
302
|
+
* `topics` is the project's doc-topic list. Passing it is what puts an
|
|
303
|
+
* integration's topics — including one it contributed in place of a built-in —
|
|
304
|
+
* in front of the agent by name; without it the block falls back to the CLI's
|
|
305
|
+
* own topics, because resolving a project's catalog is async and this is not.
|
|
306
|
+
*
|
|
307
|
+
* Either way the list now includes the hyphenated topics. The fallback scan
|
|
308
|
+
* matched `\w+`, which does not match `-`, so five real topics were missing
|
|
309
|
+
* from every block ever written — `getting-started` and `cli-integrations`
|
|
310
|
+
* among them. An agent cannot ask for a topic it was never told about, and
|
|
311
|
+
* `getting-started` is the one it should reach for first.
|
|
312
|
+
*
|
|
302
313
|
* @param {string} version
|
|
303
|
-
* @param {{coreDir?: string|null, invocation?: string, stylingSystem?: 'stylex'|'tailwind'|'css', zh?: boolean, lang?: string}} [options]
|
|
314
|
+
* @param {{coreDir?: string|null, invocation?: string, stylingSystem?: 'stylex'|'tailwind'|'css', zh?: boolean, lang?: string, topics?: string[]}} [options]
|
|
304
315
|
* @returns {string}
|
|
305
316
|
*/
|
|
306
|
-
export function generateCompressedIndex(version, {coreDir, invocation = getCliInvocation(), stylingSystem = 'css'} = {}) {
|
|
317
|
+
export function generateCompressedIndex(version, {coreDir, invocation = getCliInvocation(), stylingSystem = 'css', topics} = {}) {
|
|
307
318
|
const run = invocation;
|
|
308
319
|
const lines = [MARKER_START];
|
|
309
320
|
|
|
@@ -377,13 +388,18 @@ export function generateCompressedIndex(version, {coreDir, invocation = getCliIn
|
|
|
377
388
|
lines.push(` component --list ${componentCount} components by category`);
|
|
378
389
|
lines.push(' template --list page + block recipes');
|
|
379
390
|
const docsDir = path.join(CLI_ROOT, 'assets', 'docs');
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
391
|
+
const resolvedTopics =
|
|
392
|
+
topics ??
|
|
393
|
+
(fs.existsSync(docsDir)
|
|
394
|
+
? fs
|
|
395
|
+
.readdirSync(docsDir)
|
|
396
|
+
.map(f => f.match(/^([\w-]+)\.doc\.mjs$/))
|
|
397
|
+
.filter(/** @returns {m is RegExpMatchArray} */ (m) => m != null)
|
|
398
|
+
.map(m => m[1])
|
|
399
|
+
.sort()
|
|
400
|
+
: []);
|
|
401
|
+
if (resolvedTopics.length > 0) {
|
|
402
|
+
lines.push(` docs <topic> ${resolvedTopics.join(', ')}`);
|
|
387
403
|
}
|
|
388
404
|
lines.push(' swizzle <Name> eject component source for deep customization');
|
|
389
405
|
lines.push(' upgrade --apply run after any @astryxdesign/core bump');
|
|
@@ -566,14 +582,17 @@ export function removeAgentDocs(targetDir) {
|
|
|
566
582
|
* @param {string} [options.agent] - Tool preset: 'claude', 'cursor', 'codex', 'hermes', 'all'
|
|
567
583
|
* @param {string[]} [options.paths] - Explicit paths (overrides agent/auto-detect)
|
|
568
584
|
* @param {boolean} [options.onlyReplace] - Only update files that already have Astryx markers (for upgrades)
|
|
585
|
+
* @param {string[]} [options.topics] - Doc topics to list in the block; defaults
|
|
586
|
+
* to the CLI's own. Pass the project's catalog (`(await project.docs()).names()`)
|
|
587
|
+
* so an integration's topics reach the agent.
|
|
569
588
|
* @returns {string[]} List of files written
|
|
570
589
|
*/
|
|
571
|
-
export function installAgentDocs(targetDir, {zh = false, lang, agent, paths, onlyReplace = false} = {}) {
|
|
590
|
+
export function installAgentDocs(targetDir, {zh = false, lang, agent, paths, onlyReplace = false, topics} = {}) {
|
|
572
591
|
const coreDir = findCoreDir(targetDir);
|
|
573
592
|
const version = getXdsVersion(coreDir);
|
|
574
593
|
const invocation = getCliInvocation(targetDir);
|
|
575
594
|
const stylingSystem = detectStylingSystem(targetDir);
|
|
576
|
-
const compressedIndex = generateCompressedIndex(version, {coreDir, zh, lang, invocation, stylingSystem});
|
|
595
|
+
const compressedIndex = generateCompressedIndex(version, {coreDir, zh, lang, invocation, stylingSystem, topics});
|
|
577
596
|
/** @type {string[]} */
|
|
578
597
|
const written = [];
|
|
579
598
|
|
|
@@ -117,6 +117,44 @@ describe('generateCompressedIndex', () => {
|
|
|
117
117
|
// The header defines the mapping; the bare "run every command as `npx astryx`" footgun must be absent.
|
|
118
118
|
expect(result).not.toContain('npx astryx <cmd>');
|
|
119
119
|
});
|
|
120
|
+
|
|
121
|
+
/** The `docs <topic> a, b, c` line of the block. */
|
|
122
|
+
function topicLine(block) {
|
|
123
|
+
return (
|
|
124
|
+
block.split('\n').find(line => line.trimStart().startsWith('docs <topic>')) ?? ''
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
it('names the hyphenated topics, which the scan used to drop', () => {
|
|
129
|
+
// The fallback scan matched `\w+`, which does not match `-`, so five real
|
|
130
|
+
// topics were missing from every block ever written. An agent cannot ask
|
|
131
|
+
// for a topic it was never told about, and `getting-started` — the one it
|
|
132
|
+
// should reach for first — was one of them.
|
|
133
|
+
const line = topicLine(generateCompressedIndex('1.0.0'));
|
|
134
|
+
for (const topic of [
|
|
135
|
+
'getting-started',
|
|
136
|
+
'cli-integrations',
|
|
137
|
+
'browser-support',
|
|
138
|
+
'styling-libraries',
|
|
139
|
+
'working-with-ai',
|
|
140
|
+
]) {
|
|
141
|
+
expect(line).toContain(topic);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('lists the topics it is given, so an integration’s reach the agent', () => {
|
|
146
|
+
const line = topicLine(
|
|
147
|
+
generateCompressedIndex('1.0.0', {topics: ['tokens', 'deploying']}),
|
|
148
|
+
);
|
|
149
|
+
expect(line).toContain('tokens, deploying');
|
|
150
|
+
// The project's catalog replaces the built-in scan rather than adding to it:
|
|
151
|
+
// a topic an integration replaced must not also be listed under its old name.
|
|
152
|
+
expect(line).not.toContain('typography');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('omits the line entirely when the project has no topics', () => {
|
|
156
|
+
expect(topicLine(generateCompressedIndex('1.0.0', {topics: []}))).toBe('');
|
|
157
|
+
});
|
|
120
158
|
});
|
|
121
159
|
|
|
122
160
|
describe('detectStylingSystem', () => {
|
|
@@ -99,6 +99,21 @@ export class Project {
|
|
|
99
99
|
* @returns {Promise<Array<object>>}
|
|
100
100
|
*/
|
|
101
101
|
templates(): Promise<Array<object>>;
|
|
102
|
+
/**
|
|
103
|
+
* The CLI's own doc topics plus the ones the configured integrations
|
|
104
|
+
* contribute, resolved into one catalog: additions, replacements (with the
|
|
105
|
+
* replaced name left as an alias), and extensions merged in configuration
|
|
106
|
+
* order. Same skip+warn policy as its siblings — a broken integration
|
|
107
|
+
* contributes no topics and its issues are collected. Memoized per instance.
|
|
108
|
+
*
|
|
109
|
+
* Two problems can only be seen with every integration in hand, so they are
|
|
110
|
+
* raised here rather than in the per-integration validators: a topic that
|
|
111
|
+
* collides with one another package already provides, and a `replaces` /
|
|
112
|
+
* `extends` that names a topic nothing provides.
|
|
113
|
+
*
|
|
114
|
+
* @returns {Promise<DocsCatalog>}
|
|
115
|
+
*/
|
|
116
|
+
docs(): Promise<DocsCatalog>;
|
|
102
117
|
/**
|
|
103
118
|
* Core registry transforms + integration codemods for an upgrade range.
|
|
104
119
|
* Wraps getTransformsBetween (core) and discoverIntegrationCodemods /
|
|
@@ -152,3 +167,4 @@ export class Project {
|
|
|
152
167
|
export type ProjectIntegrationIssue = import("../integrations/issue").AstryxIntegrationIssue & {
|
|
153
168
|
package: string;
|
|
154
169
|
};
|
|
170
|
+
import { DocsCatalog } from '../discovery/docs-discovery.mjs';
|
|
@@ -6,16 +6,16 @@
|
|
|
6
6
|
* `Project` is the one entry point a command uses to read everything it needs
|
|
7
7
|
* about a consumer's project: the validated config surface, the configured
|
|
8
8
|
* integrations, and the resolved discovery sets (components, templates,
|
|
9
|
-
* codemods) — plus issue routing (issuesUrl) and the accumulated
|
|
10
|
-
* issues. It replaces the old `loadConfig(cwd)` plain-object loader
|
|
11
|
-
* per-command fan-out into the various discovery helpers.
|
|
9
|
+
* codemods, docs) — plus issue routing (issuesUrl) and the accumulated
|
|
10
|
+
* integration issues. It replaces the old `loadConfig(cwd)` plain-object loader
|
|
11
|
+
* and the per-command fan-out into the various discovery helpers.
|
|
12
12
|
*
|
|
13
13
|
* Design:
|
|
14
14
|
* - `Project.load(cwd, {cache})` is the async factory (constructors can't be
|
|
15
15
|
* async). It does what loadConfig did — find the config sibling-of
|
|
16
16
|
* package.json, import + validate it, load the configured integrations —
|
|
17
17
|
* and nothing more. Discovery is LAZY.
|
|
18
|
-
* - Discovery methods (components/templates/codemods) are MEMOIZED per
|
|
18
|
+
* - Discovery methods (components/templates/codemods/docs) are MEMOIZED per
|
|
19
19
|
* instance (via the pluggable cache) and orchestrate the EXISTING discovery
|
|
20
20
|
* functions — Project never reimplements discovery.
|
|
21
21
|
* - SKIP + WARN policy: as a discovery method runs, per-integration work is
|
|
@@ -45,6 +45,10 @@ import {
|
|
|
45
45
|
discoverAll as discoverTemplates,
|
|
46
46
|
discoverIntegrationTemplatesForOne,
|
|
47
47
|
} from '../discovery/template-adapter.mjs';
|
|
48
|
+
import {
|
|
49
|
+
DocsCatalog,
|
|
50
|
+
discoverIntegrationDocs,
|
|
51
|
+
} from '../discovery/docs-discovery.mjs';
|
|
48
52
|
import {getTransformsBetween} from '../../assets/codemods/registry.mjs';
|
|
49
53
|
import {
|
|
50
54
|
discoverIntegrationCodemods,
|
|
@@ -454,6 +458,63 @@ export class Project {
|
|
|
454
458
|
});
|
|
455
459
|
}
|
|
456
460
|
|
|
461
|
+
/**
|
|
462
|
+
* The CLI's own doc topics plus the ones the configured integrations
|
|
463
|
+
* contribute, resolved into one catalog: additions, replacements (with the
|
|
464
|
+
* replaced name left as an alias), and extensions merged in configuration
|
|
465
|
+
* order. Same skip+warn policy as its siblings — a broken integration
|
|
466
|
+
* contributes no topics and its issues are collected. Memoized per instance.
|
|
467
|
+
*
|
|
468
|
+
* Two problems can only be seen with every integration in hand, so they are
|
|
469
|
+
* raised here rather than in the per-integration validators: a topic that
|
|
470
|
+
* collides with one another package already provides, and a `replaces` /
|
|
471
|
+
* `extends` that names a topic nothing provides.
|
|
472
|
+
*
|
|
473
|
+
* @returns {Promise<DocsCatalog>}
|
|
474
|
+
*/
|
|
475
|
+
async docs() {
|
|
476
|
+
return this.#memo('docs', async () => {
|
|
477
|
+
const catalog = DocsCatalog.fromBuiltins();
|
|
478
|
+
|
|
479
|
+
for (const integration of this.#loadedIntegrations) {
|
|
480
|
+
await this.#collectIssues(integration);
|
|
481
|
+
const pkg = this.#pkgLabel(integration);
|
|
482
|
+
const hadError = this.#issues.some(
|
|
483
|
+
i => i.package === pkg && i.severity === 'error',
|
|
484
|
+
);
|
|
485
|
+
if (hadError) continue;
|
|
486
|
+
if (!integration?.docs) continue;
|
|
487
|
+
try {
|
|
488
|
+
const {records, errors} = await discoverIntegrationDocs(integration);
|
|
489
|
+
for (const e of errors) {
|
|
490
|
+
this.#pushIssue(pkg, {
|
|
491
|
+
code: 'invalid_doc',
|
|
492
|
+
severity: 'error',
|
|
493
|
+
message: e.message,
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
// Any unusable doc withdraws the whole package's topics, matching
|
|
497
|
+
// how a bad template withdraws its package's templates: a partial
|
|
498
|
+
// docs contribution is the state where a `replaces` silently does
|
|
499
|
+
// nothing and a reader gets the topic it was meant to replace.
|
|
500
|
+
if (errors.length > 0) continue;
|
|
501
|
+
for (const record of records) {
|
|
502
|
+
const issue = catalog.add(record);
|
|
503
|
+
if (issue) this.#pushIssue(pkg, issue);
|
|
504
|
+
}
|
|
505
|
+
} catch (err) {
|
|
506
|
+
this.#pushIssue(pkg, {
|
|
507
|
+
code: 'invalid_doc',
|
|
508
|
+
severity: 'error',
|
|
509
|
+
message: errorMessage(err),
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
return catalog;
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
|
|
457
518
|
/**
|
|
458
519
|
* Core registry transforms + integration codemods for an upgrade range.
|
|
459
520
|
* Wraps getTransformsBetween (core) and discoverIntegrationCodemods /
|