@astryxdesign/cli 0.4.5 → 0.4.6-canary.f1d4034

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
package/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # @xds/cli
2
2
 
3
+ # 0.4.6
4
+
5
+ #### New Features
6
+
7
+ - An integration can contribute reference-doc topics: point `docs` at a root in `astryx.integration.*` and every `{topic}.doc.{ts,mjs,js}` under it is served by `astryx docs`, indexed by `astryx search`, and named in the agent-docs block, beside the built-in topics. A topic may also declare `replaces: '<topic>'` to take over an existing one (renaming it leaves the old name resolving as an alias) or `extends: '<topic>'` to merge onto one section by section. A name that collides without declaring either is an `invalid_doc` issue rather than a silent override, and `validate-integration` reports it. (#5311)
8
+ Also fixes the agent-docs block's topic list, which scanned for `\w+` and so silently dropped every hyphenated topic — `getting-started`, `cli-integrations`, `browser-support`, `styling-libraries` and `working-with-ai` were missing from every block ever written, and an agent cannot ask for a topic it was never told about.
9
+ - Five dashboard page templates: `dashboard-cohort-funnel`, `dashboard-data`, `dashboard-executive-summary`, `dashboard-project-status` and `dashboard-service-monitoring`. Each is a complete page — layout, realistic sample data, and the component choices that go with the shape of the data — so `astryx template <name>` gives you something to edit rather than a blank frame (#5245).
10
+
11
+ #### Fixes
12
+
13
+ - `component` built the import specifier for an integration component by joining the package name and the component name, which assumes every component is exported from a subpath named after itself. Components are commonly grouped behind a single entry point named after the concept, so the suggested import pointed at a subpath the package does not export and did not resolve (#4810).
14
+ The specifier is now resolved against the owning package's `exports` map, keyed on the directory the component's doc file sits in, and falls back to the package root when that directory is not an exported subpath. A specifier a doc file states for itself is also no longer overwritten.
15
+ - The upgrade codemod no longer collapses significant JSX whitespace when it renames an element tag. Renaming `<OldName>` next to text and a `{expression}` (e.g. `hello {name} world`) previously dropped the adjacent space (`hello {name}world`); element-tag renames are now spliced into the output so the surrounding JSX is left untouched (#5149).
16
+ - The XDS-prefix codemod no longer produces a file that will not compile. Dropping the prefix renames `XDSButton` to `Button`, but if the file already had a local binding called `Button` the rewrite collided with it and shadowed one of the two. The import is now aliased instead, so both survive and the file still typechecks (#5225).
17
+
18
+ #### Contributors
19
+
20
+ Thanks to everyone who contributed to this release:
21
+
22
+ - @ejhammond
23
+ - @josephfarina
24
+ - @kentonquatman
25
+ - @rubyycheung
26
+
27
+ ---
28
+
3
29
  # 0.4.5
4
30
 
5
31
  ---
@@ -12,6 +12,7 @@
12
12
  * @property {any[]} [components]
13
13
  * @property {{description?: string}} [usage]
14
14
  * @property {any} [theming]
15
+ * @property {string} [import] set when the doc states its own import specifier
15
16
  */
16
17
  /**
17
18
  * Options object for `loadDocs`, matching its declared parameter shape (used
@@ -29,6 +30,12 @@
29
30
  * @property {string|undefined} issuesUrl
30
31
  * @property {import('../../foundation/integrations/integrations.mjs').LoadedIntegration|null} integration
31
32
  */
33
+ /**
34
+ * What ownership shaping needs from an owner. The core and legacy-external
35
+ * paths synthesize a bare `{package, sourcePath}` rather than resolving a full
36
+ * {@link ComponentOwner}, so everything past those two is optional here.
37
+ * @typedef {Partial<ComponentOwner> & {package: string, sourcePath: string|null}} OwnershipSubject
38
+ */
32
39
  /**
33
40
  * A back-compat external package discovered via `pkg.astryx.docs`.
34
41
  * @typedef {{name: string, category: string, docsDir: string}} ExternalPackageRef
@@ -152,15 +159,12 @@ export function extractProps(docs: LoadedComponentDoc): any[];
152
159
  * swizzleable source file exists for the owner). Existing doc fields (name,
153
160
  * usage, props, …) are preserved.
154
161
  * @param {LoadedComponentDoc} docs
155
- * @param {{package: string, sourcePath: string|null}} owner
162
+ * @param {OwnershipSubject} owner
156
163
  * @param {string} componentName
157
164
  * @param {string} coreDir
158
165
  * @returns {import('./component.type.mjs').ComponentDetailResponse['data']}
159
166
  */
160
- export function withOwnership(docs: LoadedComponentDoc, owner: {
161
- package: string;
162
- sourcePath: string | null;
163
- }, componentName: string, coreDir: string): import("./component.type.mjs").ComponentDetailResponse["data"];
167
+ export function withOwnership(docs: LoadedComponentDoc, owner: OwnershipSubject, componentName: string, coreDir: string): import("./component.type.mjs").ComponentDetailResponse["data"];
164
168
  /**
165
169
  * When the caller asked for "Code" but the resolved doc is for "CodeBlock"
166
170
  * (parent), scope the response to just the matching sub-component. Returns the
@@ -193,6 +197,10 @@ export type LoadedComponentDoc = {
193
197
  description?: string;
194
198
  } | undefined;
195
199
  theming?: any;
200
+ /**
201
+ * set when the doc states its own import specifier
202
+ */
203
+ import?: string | undefined;
196
204
  };
197
205
  /**
198
206
  * Options object for `loadDocs`, matching its declared parameter shape (used
@@ -215,6 +223,15 @@ export type ComponentOwner = {
215
223
  issuesUrl: string | undefined;
216
224
  integration: import("../../foundation/integrations/integrations.mjs").LoadedIntegration | null;
217
225
  };
226
+ /**
227
+ * What ownership shaping needs from an owner. The core and legacy-external
228
+ * paths synthesize a bare `{package, sourcePath}` rather than resolving a full
229
+ * {@link ComponentOwner}, so everything past those two is optional here.
230
+ */
231
+ export type OwnershipSubject = Partial<ComponentOwner> & {
232
+ package: string;
233
+ sourcePath: string | null;
234
+ };
218
235
  /**
219
236
  * A back-compat external package discovered via `pkg.astryx.docs`.
220
237
  */
@@ -18,6 +18,8 @@
18
18
  * deduped, so each leaf stays a thin projection.
19
19
  */
20
20
 
21
+ import * as fs from 'node:fs';
22
+ import * as path from 'node:path';
21
23
  import {ERROR_CODES} from '../../foundation/response/error-codes.mjs';
22
24
  import {findCoreDir, discoverExternalPackages} from '../../foundation/fs/paths.mjs';
23
25
  import {
@@ -48,6 +50,7 @@ export {CORE_PACKAGE};
48
50
  * @property {any[]} [components]
49
51
  * @property {{description?: string}} [usage]
50
52
  * @property {any} [theming]
53
+ * @property {string} [import] set when the doc states its own import specifier
51
54
  */
52
55
 
53
56
  /**
@@ -68,6 +71,13 @@ export {CORE_PACKAGE};
68
71
  * @property {import('../../foundation/integrations/integrations.mjs').LoadedIntegration|null} integration
69
72
  */
70
73
 
74
+ /**
75
+ * What ownership shaping needs from an owner. The core and legacy-external
76
+ * paths synthesize a bare `{package, sourcePath}` rather than resolving a full
77
+ * {@link ComponentOwner}, so everything past those two is optional here.
78
+ * @typedef {Partial<ComponentOwner> & {package: string, sourcePath: string|null}} OwnershipSubject
79
+ */
80
+
71
81
  /**
72
82
  * A back-compat external package discovered via `pkg.astryx.docs`.
73
83
  * @typedef {{name: string, category: string, docsDir: string}} ExternalPackageRef
@@ -357,7 +367,7 @@ export function extractProps(docs) {
357
367
  * swizzleable source file exists for the owner). Existing doc fields (name,
358
368
  * usage, props, …) are preserved.
359
369
  * @param {LoadedComponentDoc} docs
360
- * @param {{package: string, sourcePath: string|null}} owner
370
+ * @param {OwnershipSubject} owner
361
371
  * @param {string} componentName
362
372
  * @param {string} coreDir
363
373
  * @returns {import('./component.type.mjs').ComponentDetailResponse['data']}
@@ -366,15 +376,53 @@ export function withOwnership(docs, owner, componentName, coreDir) {
366
376
  const importSpec =
367
377
  owner.package === CORE_PACKAGE
368
378
  ? resolveImportPath(coreDir, componentName)
369
- : `${owner.package}/${componentName}`;
379
+ : resolveIntegrationImportPath(owner, componentName);
370
380
  return /** @type {any} */ ({
371
381
  ...docs,
372
382
  package: owner.package,
373
- import: importSpec,
383
+ // A doc file may state its own specifier, e.g. when one entry point exports
384
+ // several components. Only fall back to a resolved one when it does not.
385
+ import: docs.import ?? importSpec,
374
386
  sourceAvailable: owner.sourcePath != null,
375
387
  });
376
388
  }
377
389
 
390
+ /**
391
+ * Resolve the specifier an integration component is imported from, against the
392
+ * owning package's `exports` map.
393
+ *
394
+ * A component lives in a directory that need not share its name — several
395
+ * components can be exported from one entry point — so the specifier has to
396
+ * come from the directory the doc file sits in, checked against `exports`,
397
+ * rather than from the component name. Falls back to the package root when the
398
+ * directory is not an exported subpath, matching what a consumer would have to
399
+ * write by hand.
400
+ *
401
+ * @param {OwnershipSubject} owner
402
+ * @param {string} componentName
403
+ * @returns {string}
404
+ */
405
+ function resolveIntegrationImportPath(owner, componentName) {
406
+ const packageDir = owner.integration?.__packageDir;
407
+ const directory = owner.docPath
408
+ ? path.basename(path.dirname(owner.docPath))
409
+ : componentName;
410
+ if (!packageDir) {
411
+ return owner.package;
412
+ }
413
+ try {
414
+ const manifest = JSON.parse(
415
+ fs.readFileSync(path.join(packageDir, 'package.json'), 'utf-8'),
416
+ );
417
+ if (manifest.exports?.[`./${directory}`]) {
418
+ return `${owner.package}/${directory}`;
419
+ }
420
+ } catch {
421
+ // An unreadable or malformed manifest is not worth failing a lookup over.
422
+ }
423
+ return owner.package;
424
+ }
425
+
378
426
  /**
379
427
  * When the caller asked for "Code" but the resolved doc is for "CodeBlock"
380
428
  * (parent), scope the response to just the matching sub-component. Returns the
@@ -2,9 +2,19 @@
2
2
  // DO NOT EDIT — run `pnpm sync:api-types` to regenerate.
3
3
 
4
4
  /**
5
- * @returns {Record<string, string>}
5
+ * The project's topics: the built-in ones plus whatever the configured
6
+ * integrations contribute.
7
+ *
8
+ * A docs read must not depend on a healthy project config. `astryx docs
9
+ * tokens` answered without loading anything before integrations could
10
+ * contribute topics, and it still answers when the config is unreadable — the
11
+ * built-in topics are the floor, and the integration issues surface on the
12
+ * commands that own them.
13
+ *
14
+ * @param {string} [cwd]
15
+ * @returns {Promise<DocsCatalog>}
6
16
  */
7
- export function discoverTopics(): Record<string, string>;
17
+ export function loadDocsCatalog(cwd?: string): Promise<DocsCatalog>;
8
18
  /**
9
19
  * @param {string} docPath
10
20
  * @param {{lang?: string|null}} [opts]
@@ -14,18 +24,35 @@ export function loadReferenceDocs(docPath: string, { lang }?: {
14
24
  lang?: string | null;
15
25
  }): Promise<import("./docs.type.mjs").DocsDetailResponse["data"]>;
16
26
  /**
17
- * Discover topics, resolve `topic` to its doc file (throwing `ERR_UNKNOWN_TOPIC`
18
- * when unmatched), and load that topic's reference doc with any --dense/--zh
19
- * overlay applied. Shared by the detail and section leaves so topic normalization
20
- * and unknown-topic handling live in exactly one place.
27
+ * Load one catalog entry: its own doc, plus any extension an integration
28
+ * merged onto it, in configuration order.
29
+ *
30
+ * A localization overlay applies to each file before the extensions are
31
+ * merged, so an extension written in the base language stays readable under
32
+ * `--dense`/`--zh` (it replaces its own sections and leaves the rest
33
+ * translated) rather than being dropped.
34
+ *
35
+ * @param {import('../../foundation/discovery/docs-discovery.mjs').DocsTopicEntry} entry
36
+ * @param {{lang?: string|null}} [opts]
37
+ * @returns {Promise<import('./docs.type.mjs').DocsDetailResponse['data']>}
38
+ */
39
+ export function loadTopicDoc(entry: import("../../foundation/discovery/docs-discovery.mjs").DocsTopicEntry, { lang }?: {
40
+ lang?: string | null;
41
+ }): Promise<import("./docs.type.mjs").DocsDetailResponse["data"]>;
42
+ /**
43
+ * Resolve `topic` against the project's catalog (throwing `ERR_UNKNOWN_TOPIC`
44
+ * when unmatched), and load it with any --dense/--zh overlay and any
45
+ * integration extension applied. Shared by the detail and section leaves so
46
+ * topic normalization and unknown-topic handling live in exactly one place.
21
47
  *
22
48
  * @param {string} topic
23
49
  * @param {object} [options]
24
50
  * @param {string} [options.lang]
25
51
  * @param {boolean} [options.zh]
26
52
  * @param {boolean} [options.dense]
53
+ * @param {string} [options.cwd]
27
54
  * @returns {Promise<{
28
- * topics: Record<string, string>,
55
+ * catalog: DocsCatalog,
29
56
  * docsData: import('./docs.type.mjs').DocsDetailResponse['data'],
30
57
  * }>}
31
58
  */
@@ -33,7 +60,9 @@ export function resolveTopicDocs(topic: string, options?: {
33
60
  lang?: string | undefined;
34
61
  zh?: boolean | undefined;
35
62
  dense?: boolean | undefined;
63
+ cwd?: string | undefined;
36
64
  }): Promise<{
37
- topics: Record<string, string>;
65
+ catalog: DocsCatalog;
38
66
  docsData: import("./docs.type.mjs").DocsDetailResponse["data"];
39
67
  }>;
68
+ import { DocsCatalog } from '../../foundation/discovery/docs-discovery.mjs';
@@ -3,35 +3,50 @@
3
3
  /**
4
4
  * @file Shared doc-loading and topic-resolution helpers for the docs leaves.
5
5
  *
6
- * @input packages/cli/assets/docs/{topic}.doc.mjs and, when a --dense/--zh overlay is
7
- * requested, the sibling {topic}.doc.dense.mjs / {topic}.doc.zh.mjs.
8
- * @output Topic discovery map, overlay-merged reference-doc data, and a combined
9
- * resolve step ({topics, docsData}) that the detail and section leaves share.
6
+ * @input The project's doc catalog the CLI's own
7
+ * packages/cli/assets/docs/{topic}.doc.mjs plus every topic the configured
8
+ * integrations contribute and, when a --dense/--zh overlay is requested,
9
+ * the sibling {topic}.doc.dense.mjs / {topic}.doc.zh.mjs.
10
+ * @output Catalog access, overlay- and extension-merged reference-doc data,
11
+ * and a combined resolve step ({catalog, docsData}) that the detail and
12
+ * section leaves share.
10
13
  * @position Sits beside docs.mjs (api/docs/). Owns everything ≥2 leaves need so
11
- * no leaf re-implements discovery, overlay merging, or unknown-topic handling.
14
+ * no leaf re-implements resolution, overlay merging, or unknown-topic
15
+ * handling. Discovery itself lives in foundation/discovery/docs-discovery,
16
+ * which api/search and the agent-docs block read through the same catalog.
12
17
  */
13
18
 
14
19
  import * as fs from 'node:fs';
15
20
  import * as path from 'node:path';
16
21
  import {pathToFileURL} from 'node:url';
17
- import {CLI_ROOT} from '../../foundation/fs/paths.mjs';
22
+ import {Project} from '../../foundation/config/project.mjs';
23
+ import {
24
+ DocsCatalog,
25
+ mergeTopic,
26
+ } from '../../foundation/discovery/docs-discovery.mjs';
18
27
  import {AstryxError} from '../error.mjs';
19
28
  import {ERROR_CODES} from '../../foundation/response/error-codes.mjs';
20
29
 
21
- const DOCS_DIR = path.join(CLI_ROOT, 'assets', 'docs');
22
-
23
30
  /**
24
- * @returns {Record<string, string>}
31
+ * The project's topics: the built-in ones plus whatever the configured
32
+ * integrations contribute.
33
+ *
34
+ * A docs read must not depend on a healthy project config. `astryx docs
35
+ * tokens` answered without loading anything before integrations could
36
+ * contribute topics, and it still answers when the config is unreadable — the
37
+ * built-in topics are the floor, and the integration issues surface on the
38
+ * commands that own them.
39
+ *
40
+ * @param {string} [cwd]
41
+ * @returns {Promise<DocsCatalog>}
25
42
  */
26
- export function discoverTopics() {
27
- /** @type {Record<string, string>} */
28
- const topics = Object.create(null);
29
- if (!fs.existsSync(DOCS_DIR)) return topics;
30
- for (const file of fs.readdirSync(DOCS_DIR)) {
31
- const match = file.match(/^([\w-]+)\.doc\.mjs$/);
32
- if (match) topics[match[1]] = path.join(DOCS_DIR, file);
43
+ export async function loadDocsCatalog(cwd = process.cwd()) {
44
+ try {
45
+ const project = await Project.load(cwd);
46
+ return await project.docs();
47
+ } catch {
48
+ return DocsCatalog.fromBuiltins();
33
49
  }
34
- return topics;
35
50
  }
36
51
 
37
52
  /**
@@ -41,7 +56,7 @@ export function discoverTopics() {
41
56
  */
42
57
  export async function loadReferenceDocs(docPath, {lang} = {}) {
43
58
  const mod = await import(pathToFileURL(docPath).href);
44
- const docs = mod.docs;
59
+ const docs = mod.docs ?? mod.default;
45
60
  if (!lang || lang === 'en') return docs;
46
61
 
47
62
  const dir = path.dirname(docPath);
@@ -96,46 +111,60 @@ export async function loadReferenceDocs(docPath, {lang} = {}) {
96
111
  }
97
112
 
98
113
  /**
99
- * Discover topics, resolve `topic` to its doc file (throwing `ERR_UNKNOWN_TOPIC`
100
- * when unmatched), and load that topic's reference doc with any --dense/--zh
101
- * overlay applied. Shared by the detail and section leaves so topic normalization
102
- * and unknown-topic handling live in exactly one place.
114
+ * Load one catalog entry: its own doc, plus any extension an integration
115
+ * merged onto it, in configuration order.
116
+ *
117
+ * A localization overlay applies to each file before the extensions are
118
+ * merged, so an extension written in the base language stays readable under
119
+ * `--dense`/`--zh` (it replaces its own sections and leaves the rest
120
+ * translated) rather than being dropped.
121
+ *
122
+ * @param {import('../../foundation/discovery/docs-discovery.mjs').DocsTopicEntry} entry
123
+ * @param {{lang?: string|null}} [opts]
124
+ * @returns {Promise<import('./docs.type.mjs').DocsDetailResponse['data']>}
125
+ */
126
+ export async function loadTopicDoc(entry, {lang} = {}) {
127
+ let doc = await loadReferenceDocs(entry.path, {lang});
128
+ for (const extension of entry.extensions) {
129
+ doc = mergeTopic(doc, await loadReferenceDocs(extension.path, {lang}));
130
+ }
131
+ return doc;
132
+ }
133
+
134
+ /**
135
+ * Resolve `topic` against the project's catalog (throwing `ERR_UNKNOWN_TOPIC`
136
+ * when unmatched), and load it with any --dense/--zh overlay and any
137
+ * integration extension applied. Shared by the detail and section leaves so
138
+ * topic normalization and unknown-topic handling live in exactly one place.
103
139
  *
104
140
  * @param {string} topic
105
141
  * @param {object} [options]
106
142
  * @param {string} [options.lang]
107
143
  * @param {boolean} [options.zh]
108
144
  * @param {boolean} [options.dense]
145
+ * @param {string} [options.cwd]
109
146
  * @returns {Promise<{
110
- * topics: Record<string, string>,
147
+ * catalog: DocsCatalog,
111
148
  * docsData: import('./docs.type.mjs').DocsDetailResponse['data'],
112
149
  * }>}
113
150
  */
114
151
  export async function resolveTopicDocs(topic, options = {}) {
115
- const {lang = null, zh = false, dense = false} = options;
152
+ const {lang = null, zh = false, dense = false, cwd} = options;
116
153
  const effectiveLang = lang || (dense ? 'dense' : zh ? 'zh' : null);
117
- const topics = discoverTopics();
154
+ const catalog = await loadDocsCatalog(cwd);
118
155
 
119
- // A public API caller could pass a non-string topic; `.toLowerCase()` below
120
- // would throw a raw TypeError (no ERR_* code downgrades to ERR_UNKNOWN).
121
- // Surface the same stable code the unknown-topic path uses.
122
- if (typeof topic !== 'string') {
156
+ // A public API caller could pass a non-string topic; `resolve` answers
157
+ // undefined for one, which lands on the same stable code as an unknown name
158
+ // rather than a raw TypeError (which downgrades to ERR_UNKNOWN).
159
+ const entry = catalog.resolve(topic);
160
+ if (!entry) {
123
161
  throw new AstryxError(
124
162
  `Unknown topic "${String(topic)}"`,
125
- Object.keys(topics).map(t => ({name: t, reason: 'available topic'})),
126
- ERROR_CODES.ERR_UNKNOWN_TOPIC,
127
- );
128
- }
129
-
130
- const normalized = topic.toLowerCase();
131
- if (!topics[normalized]) {
132
- throw new AstryxError(
133
- `Unknown topic "${topic}"`,
134
- Object.keys(topics).map(t => ({name: t, reason: 'available topic'})),
163
+ catalog.names().map(t => ({name: t, reason: 'available topic'})),
135
164
  ERROR_CODES.ERR_UNKNOWN_TOPIC,
136
165
  );
137
166
  }
138
167
 
139
- const docsData = await loadReferenceDocs(topics[normalized], {lang: effectiveLang});
140
- return {topics, docsData};
168
+ const docsData = await loadTopicDoc(entry, {lang: effectiveLang});
169
+ return {catalog, docsData};
141
170
  }
@@ -7,10 +7,12 @@
7
7
  * @param {string} [options.lang]
8
8
  * @param {boolean} [options.zh]
9
9
  * @param {boolean} [options.dense]
10
+ * @param {string} [options.cwd]
10
11
  * @returns {Promise<import('../docs.type.mjs').DocsDetailResponse>}
11
12
  */
12
13
  export function detail(topic: string, options?: {
13
14
  lang?: string | undefined;
14
15
  zh?: boolean | undefined;
15
16
  dense?: boolean | undefined;
17
+ cwd?: string | undefined;
16
18
  }): Promise<import("../docs.type.mjs").DocsDetailResponse>;
@@ -11,17 +11,19 @@
11
11
  * discovery/loading/topic-resolution with the section leaf via _adapter.mjs.
12
12
  */
13
13
 
14
- import {pathToFileURL} from 'node:url';
15
- import {resolveTopicDocs} from '../_adapter.mjs';
14
+ import {loadTopicDoc, resolveTopicDocs} from '../_adapter.mjs';
16
15
 
17
16
  /**
18
17
  * Resolve token-ref blocks by inlining the referenced section's table.
19
18
  * This allows section docs to reference token tables without duplicating data.
19
+ *
20
+ * The reference is resolved through the catalog, so a topic may point at one
21
+ * an integration contributed (or replaced) rather than only at a built-in.
20
22
  * @param {import('../docs.type.mjs').DocsDetailResponse['data']} docsData
21
- * @param {Record<string, string>} topics
23
+ * @param {import('../../../foundation/discovery/docs-discovery.mjs').DocsCatalog} catalog
22
24
  * @returns {Promise<import('../docs.type.mjs').DocsDetailResponse['data']>}
23
25
  */
24
- async function resolveTokenRefs(docsData, topics) {
26
+ async function resolveTokenRefs(docsData, catalog) {
25
27
  const resolved = {...docsData, sections: [...docsData.sections]};
26
28
  for (let si = 0; si < resolved.sections.length; si++) {
27
29
  const section = resolved.sections[si];
@@ -29,13 +31,12 @@ async function resolveTokenRefs(docsData, topics) {
29
31
  const newContent = [];
30
32
  for (const block of section.content) {
31
33
  if (block.type === 'token-ref') {
32
- const refPath = topics[block.topic];
33
- if (!refPath) {
34
+ const refEntry = catalog.resolve(block.topic);
35
+ if (!refEntry) {
34
36
  newContent.push({type: 'prose', text: `[token-ref: unknown topic "${block.topic}"]`});
35
37
  continue;
36
38
  }
37
- const refMod = await import(pathToFileURL(refPath).href);
38
- const refDocs = refMod.docs;
39
+ const refDocs = await loadTopicDoc(refEntry);
39
40
  const refSection = refDocs.sections.find(
40
41
  (/** @type {import('@astryxdesign/cli/authoring').ReferenceSection} */ s) =>
41
42
  s.title.toLowerCase() === block.section.toLowerCase(),
@@ -72,10 +73,11 @@ async function resolveTokenRefs(docsData, topics) {
72
73
  * @param {string} [options.lang]
73
74
  * @param {boolean} [options.zh]
74
75
  * @param {boolean} [options.dense]
76
+ * @param {string} [options.cwd]
75
77
  * @returns {Promise<import('../docs.type.mjs').DocsDetailResponse>}
76
78
  */
77
79
  export async function detail(topic, options = {}) {
78
- const {topics, docsData} = await resolveTopicDocs(topic, options);
79
- const resolved = await resolveTokenRefs(docsData, topics);
80
+ const {catalog, docsData} = await resolveTopicDocs(topic, options);
81
+ const resolved = await resolveTokenRefs(docsData, catalog);
80
82
  return {type: 'docs.detail', data: resolved};
81
83
  }
@@ -8,10 +8,12 @@
8
8
  * @param {string} [options.lang]
9
9
  * @param {boolean} [options.zh]
10
10
  * @param {boolean} [options.dense]
11
+ * @param {string} [options.cwd]
11
12
  * @returns {Promise<import('../../docs.type.mjs').DocsDetailSectionResponse>}
12
13
  */
13
14
  export function section(topic: string, sectionName: string, options?: {
14
15
  lang?: string | undefined;
15
16
  zh?: boolean | undefined;
16
17
  dense?: boolean | undefined;
18
+ cwd?: string | undefined;
17
19
  }): Promise<import("../../docs.type.mjs").DocsDetailSectionResponse>;
@@ -24,6 +24,7 @@ import {resolveTopicDocs} from '../../_adapter.mjs';
24
24
  * @param {string} [options.lang]
25
25
  * @param {boolean} [options.zh]
26
26
  * @param {boolean} [options.dense]
27
+ * @param {string} [options.cwd]
27
28
  * @returns {Promise<import('../../docs.type.mjs').DocsDetailSectionResponse>}
28
29
  */
29
30
  export async function section(topic, sectionName, options = {}) {
@@ -8,6 +8,7 @@
8
8
  * @param {string} [options.lang]
9
9
  * @param {boolean} [options.zh]
10
10
  * @param {boolean} [options.dense]
11
+ * @param {string} [options.cwd]
11
12
  * @returns {Promise<
12
13
  * import('./docs.type.mjs').DocsListResponse |
13
14
  * import('./docs.type.mjs').DocsDetailResponse |
@@ -18,6 +19,7 @@ export function docs(topic?: string, section?: string, options?: {
18
19
  lang?: string | undefined;
19
20
  zh?: boolean | undefined;
20
21
  dense?: boolean | undefined;
22
+ cwd?: string | undefined;
21
23
  }): Promise<import("./docs.type.mjs").DocsListResponse | import("./docs.type.mjs").DocsDetailResponse | import("./docs.type.mjs").DocsDetailSectionResponse>;
22
24
  import { list } from './list/list.mjs';
23
25
  import { detail } from './detail/detail.mjs';
@@ -18,7 +18,10 @@ export const doc = {
18
18
  'Routes on its arguments: no topic lists every reference-doc topic; a topic ' +
19
19
  'returns that full ReferenceDoc (with token-ref blocks inlined); a topic ' +
20
20
  'plus a section returns the first section whose title contains the ' +
21
- '(case-insensitive) query. Overlay options select localized or dense variants.',
21
+ '(case-insensitive) query. The topic set is the CLI\'s own docs plus the ' +
22
+ 'ones the project\'s configured integrations contribute — including any ' +
23
+ 'topic an integration replaces or extends — so it depends on the cwd. ' +
24
+ 'Overlay options select localized or dense variants.',
22
25
  importPath: '@astryxdesign/cli/api',
23
26
  signature:
24
27
  'docs(topic?: string, section?: string, options?: DocsOptions): Promise<DocsListResponse | DocsDetailResponse | DocsDetailSectionResponse>',
@@ -60,12 +63,18 @@ export const doc = {
60
63
  type: 'boolean',
61
64
  description: 'Return the token-efficient dense doc variant.',
62
65
  },
66
+ {
67
+ name: 'options.cwd',
68
+ type: 'string',
69
+ description:
70
+ "Project directory whose configured integrations contribute topics. Defaults to process.cwd(); an unreadable config falls back to the CLI's own topics.",
71
+ },
63
72
  ],
64
73
  returns: [
65
74
  {
66
75
  type: 'docs.list',
67
76
  description:
68
- 'All available reference-doc topics as DocsListEntry[] ({topic, description}), in discovery order.',
77
+ 'All available reference-doc topics as DocsListEntry[] ({topic, description, package, replaces?}), in read order.',
69
78
  },
70
79
  {
71
80
  type: 'docs.detail',
package/api/docs/docs.mjs CHANGED
@@ -29,6 +29,7 @@ export {list, detail, sectionLeaf as section};
29
29
  * @param {string} [options.lang]
30
30
  * @param {boolean} [options.zh]
31
31
  * @param {boolean} [options.dense]
32
+ * @param {string} [options.cwd]
32
33
  * @returns {Promise<
33
34
  * import('./docs.type.mjs').DocsListResponse |
34
35
  * import('./docs.type.mjs').DocsDetailResponse |
@@ -36,7 +37,7 @@ export {list, detail, sectionLeaf as section};
36
37
  * >}
37
38
  */
38
39
  export async function docs(topic, section, options = {}) {
39
- if (!topic) return list();
40
+ if (!topic) return list(options);
40
41
  if (section) return sectionLeaf(topic, section, options);
41
42
  return detail(topic, options);
42
43
  }
@@ -11,6 +11,16 @@ export type DocsListResponse = {
11
11
  export type DocsListEntry = {
12
12
  topic: string;
13
13
  description: string;
14
+ /**
15
+ * the package that owns this topic —
16
+ * '@astryxdesign/cli' for a built-in one, else the contributing integration
17
+ */
18
+ package: string;
19
+ /**
20
+ * the topic this one took the place of, when it
21
+ * was contributed as a replacement
22
+ */
23
+ replaces?: string | undefined;
14
24
  };
15
25
  /**
16
26
  * xds --json docs <topic>
@@ -33,4 +43,9 @@ export type DocsOptions = {
33
43
  lang?: string | undefined;
34
44
  zh?: boolean | undefined;
35
45
  dense?: boolean | undefined;
46
+ /**
47
+ * project directory whose configured integrations
48
+ * contribute topics; defaults to process.cwd()
49
+ */
50
+ cwd?: string | undefined;
36
51
  };
@@ -23,6 +23,10 @@
23
23
  * @typedef {object} DocsListEntry
24
24
  * @property {string} topic
25
25
  * @property {string} description
26
+ * @property {string} package the package that owns this topic —
27
+ * '@astryxdesign/cli' for a built-in one, else the contributing integration
28
+ * @property {string} [replaces] the topic this one took the place of, when it
29
+ * was contributed as a replacement
26
30
  */
27
31
 
28
32
  /**
@@ -45,6 +49,8 @@
45
49
  * @property {string} [lang]
46
50
  * @property {boolean} [zh]
47
51
  * @property {boolean} [dense]
52
+ * @property {string} [cwd] project directory whose configured integrations
53
+ * contribute topics; defaults to process.cwd()
48
54
  */
49
55
 
50
56
  export {};