@astryxdesign/cli 0.7.2-canary.caec7ca → 0.7.2-canary.cbb7fa3

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 (50) hide show
  1. package/api/component/_adapter.d.mts +41 -1
  2. package/api/component/_adapter.mjs +93 -7
  3. package/api/component/component.mjs +22 -5
  4. package/api/component/component.test.mjs +80 -0
  5. package/api/component/integration-export-name.test.mjs +644 -0
  6. package/api/component/list/list.mjs +42 -4
  7. package/api/gap-report/gap-report.mjs +67 -4
  8. package/api/integration/add-contribution.component-names.test.mjs +66 -0
  9. package/api/integration/add-contribution.mjs +7 -3
  10. package/api/integration/pack-check.mjs +61 -107
  11. package/api/integration/pack-check.test.mjs +359 -0
  12. package/api/search/search.d.mts +9 -0
  13. package/api/search/search.mjs +108 -16
  14. package/api/search/search.test.mjs +36 -0
  15. package/api/swizzle/copy/copy.mjs +28 -14
  16. package/api/theme/build/build.family.test.mjs +16 -0
  17. package/api/theme/build/build.mjs +77 -18
  18. package/api/theme/themeBuild.doc.mjs +3 -3
  19. package/assets/codemods/transforms/next/__tests__/migrate-list-edge-compensation-to-edge.test.mjs +120 -0
  20. package/assets/codemods/transforms/next/index.mjs +11 -1
  21. package/assets/codemods/transforms/next/migrate-list-edge-compensation-to-edge.mjs +198 -0
  22. package/assets/docs/tree/add-a-component.doc.mjs +10 -0
  23. package/assets/docs/tree/checks.doc.mjs +1 -1
  24. package/assets/docs/tree/component-family.doc.mjs +5 -1
  25. package/assets/docs/tree/component-imports.doc.mjs +8 -0
  26. package/assets/docs/tree/single-component.doc.mjs +1 -1
  27. package/assets/docs/tree/troubleshooting.doc.mjs +9 -1
  28. package/assets/docs/tree/versioning.doc.mjs +1 -0
  29. package/assets/docs/working-with-ai.doc.mjs +43 -4
  30. package/authoring/doctypes/component/component.doc.mjs +2 -2
  31. package/authoring/doctypes/component/type.ts +6 -3
  32. package/clients/cli/commands/build-theme.path-safety.test.mjs +84 -0
  33. package/clients/cli/commands/build-theme.registry.test.mjs +5 -2
  34. package/clients/cli/commands/theme-build.doc.mjs +2 -2
  35. package/foundation/discovery/component-discovery.d.mts +172 -10
  36. package/foundation/discovery/component-discovery.export-names.test.mjs +760 -0
  37. package/foundation/discovery/component-discovery.mjs +863 -55
  38. package/foundation/discovery/component-discovery.test.mjs +96 -0
  39. package/foundation/discovery/component-replacement.mjs +18 -0
  40. package/foundation/discovery/component-replacement.test.mjs +40 -0
  41. package/foundation/discovery/module-exports.d.mts +27 -0
  42. package/foundation/discovery/module-exports.mjs +356 -31
  43. package/foundation/discovery/module-exports.test.mjs +172 -0
  44. package/foundation/doc-compiler/doc-loads.test.mjs +4 -0
  45. package/foundation/doc-compiler/inputs.mjs +23 -0
  46. package/foundation/doc-compiler/inputs.test.mjs +0 -1
  47. package/foundation/integrations/contribution-inventory.mjs +6 -1
  48. package/foundation/xle/registry.mjs +4 -0
  49. package/foundation/xle/registry.test.mjs +28 -0
  50. package/package.json +9 -9
@@ -35,6 +35,14 @@ export const docs = {
35
35
  type: 'prose',
36
36
  text: 'Add writes the entry only when package.json already has an `exports` map, so start every package with `"exports": {}`.',
37
37
  },
38
+ {
39
+ type: 'prose',
40
+ text: 'A file does not have to be named after its component. When `Slides.tsx` exports `AcmeCarousel` and nothing named `Slides`, keep the doc beside it as `Slides.doc.mjs` and set its `name` to `AcmeCarousel`: apps list, search and open the component by that name, and `integration verify` checks that its import exports `AcmeCarousel`. A file that exports its own name keeps that name. A package that names its files in kebab case works the same way: `acme-gallery.doc.mjs` beside `acme-gallery.tsx` sets `name: \'AcmeGallery\'`.',
41
+ },
42
+ {
43
+ type: 'prose',
44
+ text: "The file name still opens the doc. A name that Core or another doc's file already has stays theirs, and so does a Core name your package replaces: with `Badge.doc.mjs` naming `AcmeBadge`, `astryx component Badge` opens Core's `Badge`, and `--package` with your package name reaches yours.",
45
+ },
38
46
  ],
39
47
  },
40
48
  {
@@ -56,7 +56,7 @@ export default {
56
56
  style: 'unordered',
57
57
  items: [
58
58
  'Keep `props` on the top-level doc. Private implementation helpers do not need entries.',
59
- 'Keep the doc beside the source and change both in the same pull request.',
59
+ 'Name the doc after the source file beside it, such as `AcmeCarousel.doc.mjs` beside `AcmeCarousel.tsx`, and change both in the same pull request.',
60
60
  'If the module later exposes several related public exports, adapt this doc with {@link generic:component-family}.',
61
61
  ],
62
62
  },
@@ -67,7 +67,7 @@ export const docs = {
67
67
  rows: [
68
68
  [
69
69
  '`component_export_missing`: `Component "AcmeCarousel" advertises import "@acme/astryx-widgets/components/AcmeCarousel", but that packed module does not export "AcmeCarousel".`',
70
- "package.json has no `exports` map, or the module does not export the name. Keep `export function AcmeCarousel`, and add the entry: `npm pkg set 'exports[./components/AcmeCarousel]=./components/AcmeCarousel.tsx'`.",
70
+ "package.json has no `exports` map, or the module does not export the name. Keep `export function AcmeCarousel`, and add the entry: `npm pkg set 'exports[./components/AcmeCarousel]=./components/AcmeCarousel.tsx'`. When the quoted name is a file name, such as `acme-carousel`, set the doc's `name` to the component the file exports. A type-only export does not count.",
71
71
  ],
72
72
  [
73
73
  '`template_export_missing`: `Template "acme-dashboard" public import "@acme/astryx-widgets/templates/acme-dashboard" does not have a default export.`',
@@ -125,6 +125,14 @@ export const docs = {
125
125
  type: 'table',
126
126
  headers: ['Symptom', 'Fix'],
127
127
  rows: [
128
+ [
129
+ '`Error: No component named "AcmeCarousel"` for a component your package documents.',
130
+ "Its doc still names its file. Set `name` to the export, such as `name: 'AcmeCarousel'` in `acme-carousel.doc.mjs`. A family member is listed only when the module its family doc imports exports it ({@link generic:component-family}).",
131
+ ],
132
+ [
133
+ '`npx astryx component Dialog` opens Core\'s `Dialog`, not yours.',
134
+ "A name that Core, a legacy docs package, or another package's file already has stays theirs. Run `npx astryx component Dialog --package @acme/astryx-widgets`, or give your component a name of its own.",
135
+ ],
128
136
  [
129
137
  '`Error: Template "dashboard" is ambiguous — narrow it with --type and/or --package.`',
130
138
  "Your template id matches a Core id. Rename it, such as `acme-dashboard`, or set `replaces: 'dashboard'` to take its place. `npx astryx doctor integration templates` warns about this before you publish.",
@@ -140,6 +140,7 @@ export const docs = {
140
140
  'Published 0.6.3 and earlier reject a template that sets `replaces` or `keywords`, drop that template, and hide every doc topic your package ships. Published 0.6.4 and 0.6.5 still drop a template that sets `keywords`.',
141
141
  'A stable CLI before 0.6.4 cannot read a docs section, a section `id`, or a theme folder that `integration add theme` writes. It can then hide every doc topic your package ships.',
142
142
  'Stable 0.6.3 still loads your components, but 0.6.0 cannot read the component docs that `integration add component` writes: `component AcmeCarousel` fails there.',
143
+ 'An app on an older CLI lists a component whose doc names another export of its file under the file name, and lists no family members, so `component AcmeCarousel` fails there.',
143
144
  ],
144
145
  },
145
146
  {
@@ -44,7 +44,7 @@ export const docs = {
44
44
  },
45
45
  {
46
46
  type: 'prose',
47
- text: 'By default this creates `AGENTS.md` (the tool-agnostic standard most agents read). To target a specific tool\'s file instead:',
47
+ text: "By default this creates `AGENTS.md` (the tool-agnostic standard most agents read). To target a specific tool's file instead:",
48
48
  },
49
49
  {
50
50
  type: 'code',
@@ -182,21 +182,60 @@ astryx docs principles --dense`,
182
182
  },
183
183
  {
184
184
  type: 'prose',
185
- text: 'Add the server to your MCP config file. This works with any MCP-compatible tool: Claude Desktop (claude_desktop_config.json), Cursor (.cursor/mcp.json), Windsurf (.windsurf/mcp.json), Cline, and others.',
185
+ text: 'Add the server to your MCP config file. The entry is a remote HTTP server, and the exact shape differs per client, so use the snippet for the tool you run. An entry with an unrecognized transport is skipped with a warning rather than an error, which reads as a server that connects but exposes no tools.',
186
186
  },
187
187
  {
188
188
  type: 'code',
189
189
  lang: 'json',
190
- label: 'MCP config (same for all tools)',
190
+ label: 'Claude Code (.mcp.json)',
191
191
  code: `{
192
192
  "mcpServers": {
193
193
  "astryx": {
194
- "type": "url",
194
+ "type": "http",
195
195
  "url": "https://astryx.atmeta.com/mcp"
196
196
  }
197
197
  }
198
198
  }`,
199
199
  },
200
+ {
201
+ type: 'prose',
202
+ text: 'Claude Code can also write that entry from the terminal:',
203
+ },
204
+ {
205
+ type: 'code',
206
+ lang: 'bash',
207
+ code: `claude mcp add --transport http astryx https://astryx.atmeta.com/mcp`,
208
+ },
209
+ {
210
+ type: 'code',
211
+ lang: 'json',
212
+ label: 'Cline (cline_mcp_settings.json) - type is streamableHttp',
213
+ code: `{
214
+ "mcpServers": {
215
+ "astryx": {
216
+ "type": "streamableHttp",
217
+ "url": "https://astryx.atmeta.com/mcp"
218
+ }
219
+ }
220
+ }`,
221
+ },
222
+ {
223
+ type: 'code',
224
+ lang: 'json',
225
+ label:
226
+ 'Cursor (.cursor/mcp.json) - remote servers take no type field',
227
+ code: `{
228
+ "mcpServers": {
229
+ "astryx": {
230
+ "url": "https://astryx.atmeta.com/mcp"
231
+ }
232
+ }
233
+ }`,
234
+ },
235
+ {
236
+ type: 'prose',
237
+ text: 'Claude Desktop is the exception: claude_desktop_config.json holds local stdio servers, so a remote server is added through Settings, then Connectors, then Add custom connector, pasting the URL above.',
238
+ },
200
239
  {
201
240
  type: 'prose',
202
241
  text: 'Once connected, your AI tool can search for components by natural language (e.g. "dropdown menu", "success message") and retrieve full documentation without any manual CLI invocation. The server uses the same keyword index from component docs, so search quality improves automatically as component documentation is updated.',
@@ -30,7 +30,7 @@ export const doc = {
30
30
  name: 'name',
31
31
  type: 'string',
32
32
  description:
33
- "Stable machine identity and directory name without the Astryx prefix, PascalCase. e.g. 'Button', 'TextInput', 'AppShell'. Change `displayName`, not `name`, to edit the visible label; registry URLs derive from this identity by default.",
33
+ "Stable machine identity and directory name without the Astryx prefix, PascalCase. e.g. 'Button', 'TextInput', 'AppShell'. For an integration component it is the export apps import: the doc sits beside the file that exports it, and when that file exports nothing under its own name, `name` may name the export it does have. Change `displayName`, not `name`, to edit the visible label; registry URLs derive from this identity by default.",
34
34
  required: true,
35
35
  },
36
36
  {
@@ -187,7 +187,7 @@ export const doc = {
187
187
  name: 'components',
188
188
  type: '(ComponentEntry | ComponentRef)[]',
189
189
  description:
190
- 'MultiComponentDoc variant (required there): one entry per public component/hook exported from the directory. Each entry is a full ComponentEntry (inline: name, displayName, description, props | params+returns) or a name-only ComponentRef pointing at a sibling {Name}.doc.mjs.',
190
+ 'MultiComponentDoc variant (required there): one entry per public component/hook exported from the directory. Each entry is a full ComponentEntry (inline: name, displayName, description, props | params+returns) or a name-only ComponentRef pointing at a sibling {Name}.doc.mjs. In an integration, each full entry the module named by `import` exports is a component under its own name.',
191
191
  },
192
192
  {
193
193
  name: 'subComponentOf',
@@ -33,9 +33,12 @@ export interface ComponentBaseDoc extends AuthoredDocGraphFields {
33
33
  type?: 'component';
34
34
  /**
35
35
  * Stable machine identity and directory name without the Astryx prefix,
36
- * PascalCase (for example `Button`, `Table`, or `AppShell`). Do not change
37
- * this to edit the visible label; use `displayName` for that. Registry URLs
38
- * derive from this value by default.
36
+ * PascalCase (for example `Button`, `Table`, or `AppShell`). For an
37
+ * integration component it is the export apps import: the doc sits beside
38
+ * the file that exports it, and when that file exports nothing under its own
39
+ * name, `name` may name the export it does have. Do not change this to edit
40
+ * the visible label; use `displayName` for that. Registry URLs derive from
41
+ * this value by default.
39
42
  */
40
43
  name: string;
41
44
  /** Human-readable display name with spaces between words, used by the
@@ -9,6 +9,9 @@
9
9
  * - Multi-file write is atomic: if one file fails, none are left behind.
10
10
  * - An `--out` escape, or an output directory that cannot be created, fails
11
11
  * with a registered error code.
12
+ * - Every output stays inside the working directory: an absolute `--out`
13
+ * path or a theme file outside it fails before anything is written, while
14
+ * absolute paths inside it build, also through a symlinked spelling.
12
15
  *
13
16
  * Runs the CLI in-process (shared runCli harness) against a tiny synthetic theme file.
14
17
  */
@@ -129,6 +132,68 @@ describe('theme build output-path failures carry registered codes', () => {
129
132
  expect(fs.existsSync(path.join(tmpDir, 'escaped.css'))).toBe(false);
130
133
  });
131
134
 
135
+ it('reports an absolute --out path outside the project as ERR_PATH_TRAVERSAL, with --check too', async () => {
136
+ const project = path.join(tmpDir, 'project');
137
+ const outside = path.join(tmpDir, 'outside');
138
+ fs.mkdirSync(outside, {recursive: true});
139
+ const themeFile = writeTheme(path.join(project, 'themes'), 'ocean');
140
+ const out = path.join(outside, 'ocean.css');
141
+
142
+ for (const extra of [[], ['--check']]) {
143
+ const {result, envelope} = await buildJson(
144
+ [path.relative(project, themeFile), '--out', out, ...extra],
145
+ project,
146
+ );
147
+
148
+ expect(result.code).toBe(1);
149
+ expect(envelope.code).toBe(ERROR_CODES.ERR_PATH_TRAVERSAL);
150
+ }
151
+ expect(fs.readdirSync(outside)).toEqual([]);
152
+ });
153
+
154
+ it('reports a theme file outside the project as ERR_PATH_TRAVERSAL, because its outputs go beside it', async () => {
155
+ const project = path.join(tmpDir, 'project');
156
+ fs.mkdirSync(project, {recursive: true});
157
+ const shared = path.join(tmpDir, 'shared');
158
+ const themeFile = writeTheme(shared, 'ocean');
159
+
160
+ for (const file of [themeFile, path.relative(project, themeFile)]) {
161
+ const {result, envelope} = await buildJson([file], project);
162
+
163
+ expect(result.code).toBe(1);
164
+ expect(envelope.code).toBe(ERROR_CODES.ERR_PATH_TRAVERSAL);
165
+ }
166
+ expect(fs.readdirSync(shared)).toEqual(['theme.mjs']);
167
+ });
168
+
169
+ it('builds absolute paths inside the project, also when they reach it through a symlink', async () => {
170
+ const project = path.join(tmpDir, 'project');
171
+ const themeFile = writeTheme(path.join(project, 'themes'), 'ocean');
172
+ const alias = path.join(tmpDir, 'alias');
173
+ fs.symlinkSync(project, alias, 'dir');
174
+
175
+ for (const [args, written] of [
176
+ [
177
+ [themeFile, '--out', path.join(project, 'dist', 'direct.css')],
178
+ path.join(project, 'dist', 'direct.css'),
179
+ ],
180
+ [
181
+ [themeFile, '--out', path.join(alias, 'dist', 'aliased.css')],
182
+ path.join(project, 'dist', 'aliased.css'),
183
+ ],
184
+ [
185
+ [path.join(alias, 'themes', 'theme.mjs')],
186
+ path.join(project, 'themes', 'ocean.css'),
187
+ ],
188
+ ]) {
189
+ const {result, envelope} = await buildJson(args, project);
190
+
191
+ expect(result.code).toBe(0);
192
+ expect(envelope.type).toBe('theme.build');
193
+ expect(fs.existsSync(written)).toBe(true);
194
+ }
195
+ });
196
+
132
197
  it('reports an --out path that escapes through a symlink as ERR_PATH_TRAVERSAL', async () => {
133
198
  const project = path.join(tmpDir, 'project');
134
199
  const outside = path.join(tmpDir, 'outside');
@@ -177,4 +242,23 @@ describe('theme build output-path failures carry registered codes', () => {
177
242
  code: ERROR_CODES.ERR_PATH_TRAVERSAL,
178
243
  });
179
244
  });
245
+
246
+ it('themeBuild() rejects an output outside cwd: an absolute out path, or the default beside a theme file outside cwd', async () => {
247
+ const project = path.join(tmpDir, 'project');
248
+ const inside = writeTheme(path.join(project, 'themes'), 'ocean');
249
+ const shared = path.join(tmpDir, 'shared');
250
+ const outsideTheme = writeTheme(shared, 'ocean');
251
+ const outDir = path.join(tmpDir, 'out');
252
+
253
+ for (const [file, options] of [
254
+ [path.relative(project, inside), {out: path.join(outDir, 'ocean.css')}],
255
+ [outsideTheme, {}],
256
+ ]) {
257
+ await expect(
258
+ themeBuild(file, options, {cwd: project}),
259
+ ).rejects.toMatchObject({code: ERROR_CODES.ERR_PATH_TRAVERSAL});
260
+ }
261
+ expect(fs.existsSync(outDir)).toBe(false);
262
+ expect(fs.readdirSync(shared)).toEqual(['theme.mjs']);
263
+ });
180
264
  });
@@ -126,7 +126,10 @@ describe('theme build emits a live TextInput selector (#4109)', () => {
126
126
  `};\n`,
127
127
  );
128
128
 
129
- const result = await runCli(['theme', 'build', themeFile, '-o', outFile]);
129
+ const result = await runCli(
130
+ ['theme', 'build', themeFile, '-o', outFile],
131
+ tmpDir,
132
+ );
130
133
  const css = fs.readFileSync(outFile, 'utf8');
131
134
 
132
135
  expect(result.status).toBe(0);
@@ -148,7 +151,7 @@ describe('theme build emits a live TextInput selector (#4109)', () => {
148
151
  `};\n`,
149
152
  );
150
153
 
151
- await runCli(['theme', 'build', themeFile, '-o', outFile]);
154
+ await runCli(['theme', 'build', themeFile, '-o', outFile], tmpDir);
152
155
  const css = fs.readFileSync(outFile, 'utf8');
153
156
 
154
157
  expect(css).toContain('.astryx-text-input');
@@ -30,7 +30,7 @@ export const doc = {
30
30
  {
31
31
  flag: '--family',
32
32
  description:
33
- 'Build the selected extension family into one keyed CSS, JS, and declaration set. Requires --family-key; refused with --out or --watch (ERR_THEME_INVALID)',
33
+ 'Build the selected extension family into one keyed CSS, JS, and declaration set, written beside the family root theme file, which must be inside the working directory (ERR_PATH_TRAVERSAL). Requires --family-key; refused with --out or --watch (ERR_THEME_INVALID)',
34
34
  },
35
35
  {
36
36
  flag: '--family-key <key>',
@@ -41,7 +41,7 @@ export const doc = {
41
41
  flag: '-o, --out <path>',
42
42
  param: 'options.out',
43
43
  description:
44
- 'Output CSS file path; the .js and .d.ts are written beside it (default: <theme name>.css next to the source). A relative path must stay inside the working directory. Single theme only: refused with more than one file or with --family (ERR_THEME_INVALID)',
44
+ 'Output CSS file path, relative or absolute; the .js and .d.ts are written beside it (default: <theme name>.css next to the source). Every output must resolve inside the working directory, the defaults beside the source too (ERR_PATH_TRAVERSAL). Single theme only: refused with more than one file or with --family (ERR_THEME_INVALID)',
45
45
  },
46
46
  {
47
47
  flag: '--icons-specifier <specifier>',
@@ -1,8 +1,20 @@
1
1
  // @generated by scripts/sync-api-types.mjs from the JSDoc in foundation/**/*.mjs.
2
2
  // DO NOT EDIT — run `pnpm sync:api-types` to regenerate.
3
3
 
4
+ /**
5
+ * The entry a family doc gives `name` in its `components`, when the doc is not
6
+ * the component's own (Stack's doc for VStack); null otherwise.
7
+ * @param {any} docs - a loaded component doc
8
+ * @param {string} name - bare component name
9
+ * @returns {any}
10
+ */
11
+ export function familyMember(docs: any, name: string): any;
4
12
  /**
5
13
  * Auto-discover components by scanning for Astryx*.tsx files in core/src/.
14
+ * A component is listed when a doc documents it: a doc in its folder, or, for a
15
+ * component folder with no doc of its own, the doc `findComponentReadme` finds
16
+ * elsewhere (Heading's doc in the Text folder; HStack and VStack as members of
17
+ * Stack's doc).
6
18
  *
7
19
  * Returns an ordered Record where:
8
20
  * - Grouped components use the group name as key: `'Buttons': ['Button', 'IconButton']`
@@ -103,19 +115,81 @@ export function discoverExternalComponentsGrouped(docsDir: string): Record<strin
103
115
  * @returns {string | null}
104
116
  */
105
117
  export function findExternalComponentDoc(docsDir: string, name: string): string | null;
118
+ /** @param {string} docPath */
119
+ export function docStem(docPath: string): string;
120
+ /**
121
+ * Each family doc under an integration's components root that names an
122
+ * import, with the file the import resolves to: null when it resolves to none,
123
+ * such as build output that does not exist yet.
124
+ *
125
+ * @param {{name: string, components?: string, issuesUrl?: string, __packageDir?: string, __packageExports?: Record<string, unknown>|null}} integration
126
+ * @returns {Array<{docPath: string, module: string | null}>}
127
+ */
128
+ export function familyImports(integration: {
129
+ name: string;
130
+ components?: string;
131
+ issuesUrl?: string;
132
+ __packageDir?: string;
133
+ __packageExports?: Record<string, unknown> | null;
134
+ }): Array<{
135
+ docPath: string;
136
+ module: string | null;
137
+ }>;
138
+ /**
139
+ * Whether a discovered integration record is a member its family doc lists,
140
+ * decided from the files, not from whether the doc loads.
141
+ *
142
+ * @param {{name: string, docPath: string|null}} record
143
+ * @returns {boolean}
144
+ */
145
+ export function isIntegrationMember(record: {
146
+ name: string;
147
+ docPath: string | null;
148
+ }): boolean;
149
+ /**
150
+ * The entry a family doc gives a member that discovery lists under its own
151
+ * name, or null when the record is the doc's own component or the loaded doc
152
+ * has no entry for it.
153
+ *
154
+ * @param {any} doc the doc loaded from `record.docPath`
155
+ * @param {{name: string, docPath: string|null}} record
156
+ * @returns {any}
157
+ */
158
+ export function integrationMemberEntry(doc: any, record: {
159
+ name: string;
160
+ docPath: string | null;
161
+ }): any;
162
+ /**
163
+ * Visit every component doc under an integration's components root, hidden
164
+ * ones included, in discovery order, with its stem.
165
+ * @param {string | undefined} componentsDir
166
+ * @param {(docPath: string, stem: string) => void} visit
167
+ */
168
+ export function forEachIntegrationDoc(componentsDir: string | undefined, visit: (docPath: string, stem: string) => void): void;
106
169
  /**
107
170
  * Discover ownership records for the components contributed by a single loaded
108
171
  * integration. Scans the integration's resolved `components` dir for same-stem
109
- * doc files and records each with owner package + issuesUrl + sourcePath.
172
+ * doc files and records each with owner package + issuesUrl + sourcePath. A doc
173
+ * that names a differently named export of its file is recorded under that
174
+ * export, and each family member it lists, under the member's name, unless the
175
+ * name is taken: every doc's file name (a hidden doc's too), a Core name a doc
176
+ * in the package replaces, and a name an earlier doc records.
110
177
  *
111
- * @param {{name: string, components?: string, issuesUrl?: string}} integration
178
+ * @param {{name: string, components?: string, issuesUrl?: string, __packageDir?: string, __packageExports?: Record<string, unknown>|null}} integration
112
179
  * a single entry from `loadedIntegrations` (lib/integrations.mjs)
180
+ * @param {{documentedNames?: boolean}} [options] `documentedNames: false`
181
+ * records each doc under its file name alone, without reading the names
182
+ * docs document: for a caller that needs only what each doc says
113
183
  * @returns {Array<{name: string, package: string, docPath: string, sourcePath: string|null, issuesUrl: string|undefined, group: string|null}>}
114
184
  */
115
185
  export function discoverIntegrationComponents(integration: {
116
186
  name: string;
117
187
  components?: string;
118
188
  issuesUrl?: string;
189
+ __packageDir?: string;
190
+ __packageExports?: Record<string, unknown> | null;
191
+ }, { documentedNames }?: {
192
+ documentedNames?: boolean;
119
193
  }): Array<{
120
194
  name: string;
121
195
  package: string;
@@ -129,11 +203,16 @@ export function discoverIntegrationComponents(integration: {
129
203
  * Invalid metadata or a missing same-stem source removes only that component;
130
204
  * callers can report the returned errors while retaining valid siblings. A
131
205
  * component whose doc sets `replaces` carries the value as written; the
132
- * component replacement resolver decides what it means.
206
+ * component replacement resolver decides what it means. A family member is
207
+ * read through its family's doc: it never carries the family's `replaces`, and
208
+ * a family doc that fails to load is reported once. A doc that fails to load
209
+ * is reported under its file name: what it says cannot be trusted.
133
210
  *
134
211
  * @param {{name: string, components?: string, issuesUrl?: string}} integration
212
+ * @param {{documentedNames?: boolean}} [options] as for
213
+ * {@link discoverIntegrationComponents}
135
214
  * @returns {Promise<{
136
- * components: Array<{name: string, package: string, docPath: string, sourcePath: string, issuesUrl: string|undefined, group: string|null, replaces?: unknown}>,
215
+ * components: Array<{name: string, package: string, docPath: string, sourcePath: string|null, issuesUrl: string|undefined, group: string|null, replaces?: unknown}>,
137
216
  * discovered: Array<{name: string, package: string, docPath: string, sourcePath: string|null, issuesUrl: string|undefined, group: string|null}>,
138
217
  * errors: Array<{name: string, message: string}>,
139
218
  * }>}
@@ -142,12 +221,14 @@ export function discoverValidIntegrationComponents(integration: {
142
221
  name: string;
143
222
  components?: string;
144
223
  issuesUrl?: string;
224
+ }, options?: {
225
+ documentedNames?: boolean;
145
226
  }): Promise<{
146
227
  components: Array<{
147
228
  name: string;
148
229
  package: string;
149
230
  docPath: string;
150
- sourcePath: string;
231
+ sourcePath: string | null;
151
232
  issuesUrl: string | undefined;
152
233
  group: string | null;
153
234
  replaces?: unknown;
@@ -165,30 +246,103 @@ export function discoverValidIntegrationComponents(integration: {
165
246
  message: string;
166
247
  }>;
167
248
  }>;
249
+ /**
250
+ * How an owner holds a component name, which the owner rule
251
+ * ({@link unscopedComponentOwners}) reads: `core` when Core documents it;
252
+ * `file` when an integration doc named after its file is recorded under it;
253
+ * `doc` when only a doc records it: an export name, a member, or the file name
254
+ * of a doc recorded under another export.
255
+ * @typedef {'core' | 'file' | 'doc'} NameHold
256
+ */
257
+ /**
258
+ * How a discovered integration record holds its name.
259
+ * @param {{name: string, docPath: string|null}} record
260
+ * @returns {'file' | 'doc'}
261
+ */
262
+ export function integrationRecordHold(record: {
263
+ name: string;
264
+ docPath: string | null;
265
+ }): "file" | "doc";
266
+ /**
267
+ * The integration component a name finds, as discovery records it, and its
268
+ * hold: the same-stem doc first, then a doc that records the name as an export
269
+ * or a member.
270
+ *
271
+ * @param {{name?: string, components?: string, __packageDir?: string, __packageExports?: Record<string, unknown>|null}} integration
272
+ * @param {string} name
273
+ * @returns {{docPath: string, sourcePath: string|null, hold: 'file' | 'doc'} | null}
274
+ */
275
+ export function findIntegrationComponent(integration: {
276
+ name?: string;
277
+ components?: string;
278
+ __packageDir?: string;
279
+ __packageExports?: Record<string, unknown> | null;
280
+ }, name: string): {
281
+ docPath: string;
282
+ sourcePath: string | null;
283
+ hold: "file" | "doc";
284
+ } | null;
285
+ /**
286
+ * The name discovery records a component doc under: the export its `name`
287
+ * documents when the doc is recorded under it, else the doc's file name.
288
+ *
289
+ * @param {{name?: string, components?: string, __packageDir?: string, __packageExports?: Record<string, unknown>|null}} integration
290
+ * @param {string} docPath
291
+ * @returns {string}
292
+ */
293
+ export function recordedDocName(integration: {
294
+ name?: string;
295
+ components?: string;
296
+ __packageDir?: string;
297
+ __packageExports?: Record<string, unknown> | null;
298
+ }, docPath: string): string;
168
299
  /**
169
300
  * Find an integration component's doc file by name within a loaded
170
301
  * integration's resolved `components` dir. Honors the same-stem convention
171
- * (`{Name}.doc.{ts,mjs,js}`), preferring `.ts` → `.mjs` → `.js`.
302
+ * (`{Name}.doc.{ts,mjs,js}`), preferring `.ts` → `.mjs` → `.js`, and finds a
303
+ * doc that records the component under another name: a differently named
304
+ * export of its file, or a member its family doc lists.
172
305
  *
173
- * @param {{components?: string}} integration
306
+ * @param {{name?: string, components?: string, __packageDir?: string, __packageExports?: Record<string, unknown>|null}} integration
174
307
  * @param {string} name bare component name (no `XDS`/`Astryx` prefix)
175
308
  * @returns {string|null}
176
309
  */
177
310
  export function findIntegrationComponentDoc(integration: {
311
+ name?: string;
178
312
  components?: string;
313
+ __packageDir?: string;
314
+ __packageExports?: Record<string, unknown> | null;
179
315
  }, name: string): string | null;
180
316
  /**
181
- * Find an integration component's swizzleable source file (`{Name}.tsx`) by
182
- * name within a loaded integration's resolved `components` dir. Returns null
317
+ * Find an integration component's swizzleable source file by name within a
318
+ * loaded integration's resolved `components` dir: the same-stem `{Name}.tsx`
319
+ * of its doc, or, for a family member, the file that declares it. Returns null
183
320
  * when the integration ships docs without source.
184
321
  *
185
- * @param {{components?: string}} integration
322
+ * @param {{name?: string, components?: string, __packageDir?: string, __packageExports?: Record<string, unknown>|null}} integration
186
323
  * @param {string} name bare component name
187
324
  * @returns {string|null}
188
325
  */
189
326
  export function findIntegrationComponentSource(integration: {
327
+ name?: string;
190
328
  components?: string;
329
+ __packageDir?: string;
330
+ __packageExports?: Record<string, unknown> | null;
191
331
  }, name: string): string | null;
332
+ /**
333
+ * The owner rule: an owner that holds a name only because a doc records it
334
+ * (`hold: 'doc'`) yields to Core and to every file-named owner; `--package`
335
+ * still reaches it. Surfaces resolve a replacement first, then this rule, then
336
+ * which docs load.
337
+ *
338
+ * @template {{hold?: string}} T
339
+ * @param {T[]} owners each with its {@link NameHold}; an owner without one
340
+ * counts as Core's or file-named
341
+ * @returns {T[]}
342
+ */
343
+ export function unscopedComponentOwners<T extends {
344
+ hold?: string;
345
+ }>(owners: T[]): T[];
192
346
  /**
193
347
  * Build a flat list of ownership records for ALL discoverable components —
194
348
  * core (built-in) plus every loaded integration. This is the authoritative
@@ -217,3 +371,11 @@ export function discoverOwnedComponents(coreDir: string, loadedIntegrations?: Ar
217
371
  }>;
218
372
  /** The owner package name for built-in (core) components. */
219
373
  export const CORE_PACKAGE: import("../../authoring/index.js").ProviderId;
374
+ /**
375
+ * How an owner holds a component name, which the owner rule
376
+ * ({@link unscopedComponentOwners}) reads: `core` when Core documents it;
377
+ * `file` when an integration doc named after its file is recorded under it;
378
+ * `doc` when only a doc records it: an export name, a member, or the file name
379
+ * of a doc recorded under another export.
380
+ */
381
+ export type NameHold = "core" | "file" | "doc";