@astryxdesign/cli 0.4.6 → 0.4.7-canary.019ae5a

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 (73) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +53 -51
  3. package/api/docs/docs.doc.mjs +2 -2
  4. package/api/index.d.mts +1 -1
  5. package/api/index.mjs +1 -1
  6. package/api/search/search.mjs +50 -4
  7. package/api/search/search.test.mjs +71 -0
  8. package/api/template/template.doc.mjs +2 -2
  9. package/api/theme/build/build.mjs +8 -38
  10. package/api/theme/targets/targets.d.mts +18 -0
  11. package/api/theme/targets/targets.mjs +87 -0
  12. package/api/theme/targets/targets.test.mjs +65 -0
  13. package/api/theme/theme.d.mts +1 -0
  14. package/api/theme/theme.mjs +3 -1
  15. package/api/theme/theme.type.d.mts +23 -0
  16. package/api/theme/theme.type.mjs +21 -1
  17. package/api/theme/themeTargets.doc.d.mts +11 -0
  18. package/api/theme/themeTargets.doc.mjs +58 -0
  19. package/api/theme/themeTemplate.doc.mjs +3 -3
  20. package/assets/codemods/transforms/next/__tests__/next-codemods.test.mjs +127 -0
  21. package/assets/codemods/transforms/next/banner-collapsible-content.mjs +171 -0
  22. package/assets/codemods/transforms/next/index.mjs +11 -1
  23. package/assets/docs/README.md +50 -0
  24. package/assets/docs/cli-integrations.doc.mjs +4 -4
  25. package/assets/docs/theme.doc.dense.mjs +1 -1
  26. package/assets/docs/theme.doc.mjs +1 -1
  27. package/assets/docs/typography.doc.mjs +2 -2
  28. package/assets/templates/blocks/components/Banner/BannerCollapsibleContent.doc.mjs +1 -1
  29. package/assets/templates/blocks/components/Banner/BannerCollapsibleContent.tsx +1 -1
  30. package/assets/templates/blocks/components/Dialog/DialogAdaptivePresentation.doc.mjs +25 -0
  31. package/assets/templates/blocks/components/Dialog/DialogAdaptivePresentation.tsx +167 -0
  32. package/assets/templates/blocks/components/Dialog/DialogScrollingContent.tsx +1 -1
  33. package/assets/templates/blocks/components/Step/StepContent.doc.mjs +14 -0
  34. package/assets/templates/blocks/components/Step/StepContent.tsx +32 -0
  35. package/assets/templates/blocks/components/Step/StepIndicator.doc.mjs +14 -0
  36. package/assets/templates/blocks/components/Step/StepIndicator.tsx +60 -0
  37. package/assets/templates/blocks/components/Step/StepShowcase.doc.mjs +15 -0
  38. package/assets/templates/blocks/components/Step/StepShowcase.tsx +26 -0
  39. package/assets/templates/blocks/components/Step/StepStates.doc.mjs +14 -0
  40. package/assets/templates/blocks/components/Step/StepStates.tsx +46 -0
  41. package/assets/templates/blocks/components/Stepper/StepperCustomContent.doc.mjs +22 -0
  42. package/assets/templates/blocks/components/Stepper/StepperCustomContent.tsx +126 -0
  43. package/assets/templates/blocks/components/Stepper/StepperIndicatorModes.doc.mjs +1 -1
  44. package/assets/templates/blocks/components/Stepper/StepperIndicatorModes.tsx +17 -5
  45. package/assets/templates/blocks/components/Stepper/StepperOnTrackHorizontal.doc.mjs +14 -0
  46. package/assets/templates/blocks/components/Stepper/StepperOnTrackHorizontal.tsx +25 -0
  47. package/assets/templates/blocks/components/Stepper/StepperOnTrackVertical.doc.mjs +2 -2
  48. package/assets/templates/blocks/components/Stepper/StepperOnTrackVertical.tsx +1 -1
  49. package/assets/templates/blocks/components/Stepper/StepperShowcase.doc.mjs +1 -1
  50. package/assets/templates/blocks/components/Stepper/StepperShowcase.tsx +6 -7
  51. package/assets/templates/blocks/components/Stepper/StepperStatus.tsx +1 -1
  52. package/assets/templates/pages/mixed-gallery/page.tsx +12 -3
  53. package/assets/templates/pages/table-grouped/page.tsx +151 -144
  54. package/assets/templates/themes/neutral/neutralTheme.ts +13 -8
  55. package/clients/cli/commands/build-theme.mjs +85 -0
  56. package/clients/cli/commands/dialog-adaptive-template.test.mjs +24 -0
  57. package/clients/cli/commands/theme-targets.behavior.test.mjs +64 -0
  58. package/clients/cli/commands/theme-targets.doc.mjs +38 -0
  59. package/clients/cli/commands/theme-template.doc.mjs +2 -2
  60. package/clients/cli/commands/theme.doc.mjs +4 -2
  61. package/clients/cli/index.mjs +1 -0
  62. package/clients/cli/lib/manifest.mjs +2 -0
  63. package/foundation/discovery/theming-targets.d.mts +86 -0
  64. package/foundation/discovery/theming-targets.mjs +202 -0
  65. package/foundation/discovery/theming-targets.test.mjs +245 -0
  66. package/foundation/response/response-types.doc.mjs +7 -2
  67. package/package.json +9 -9
  68. package/assets/templates/blocks/components/Stepper/StepperHorizontal.doc.mjs +0 -14
  69. package/assets/templates/blocks/components/Stepper/StepperHorizontal.tsx +0 -24
  70. package/assets/templates/blocks/components/Stepper/StepperMultiStepForm.doc.mjs +0 -14
  71. package/assets/templates/blocks/components/Stepper/StepperMultiStepForm.tsx +0 -92
  72. package/assets/templates/blocks/components/Stepper/StepperVerticalOnboarding.doc.mjs +0 -14
  73. package/assets/templates/blocks/components/Stepper/StepperVerticalOnboarding.tsx +0 -40
package/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # @xds/cli
2
2
 
3
+ # 0.4.7
4
+
5
+ ---
6
+
3
7
  # 0.4.6
4
8
 
5
9
  #### New Features
package/README.md CHANGED
@@ -383,51 +383,52 @@ Every response has a `type` discriminant. The full set is below (generated from
383
383
 
384
384
  <!-- BEGIN GENERATED: response-types -->
385
385
 
386
- | Type | What `data` carries |
387
- | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
388
- | `component.list` | The component catalog grouped by category: `detail` (the level: names \| compact \| full) and `components`, the grouped map of names+package, brief entries, or a full ComponentDoc per entry. |
389
- | `component.detail` | One component's authored ComponentDoc plus ownership metadata (owner package, import specifier, and whether source is available). |
390
- | `component.detail.props` | Just one component's props table (ComponentPropDoc[]). |
391
- | `component.detail.source` | One component's source file, as {component, source}. |
392
- | `component.detail.showcase` | One component's showcase example, as {component, aspectRatio, source}. |
393
- | `component.detail.blocks` | One component's example blocks, as {component, showcase, examples, related} of BlockEntry. |
394
- | `docs.list` | All reference-doc topics as DocsListEntry[] ({topic, description}), in discovery order. |
395
- | `docs.detail` | One topic's full ReferenceDoc, with token-ref blocks inlined. |
396
- | `docs.detail.section` | A single ReferenceSection of a topic: the first whose title contains the section query. |
397
- | `blog.list` | The feed URL plus every post parsed from the RSS feed, each with slug, title, description, date, type, authors, link, and plaintext URL. |
398
- | `blog.detail` | One post's metadata plus the feed URL and the post's full plaintext body. |
399
- | `discover.list` | The configured external packages (name, category, components, version, description); when empty it carries meta.configured to tell "nothing configured" from "nothing discovered". |
400
- | `discover.detail` | A single external package entry, for an @scope/name query. |
401
- | `discover.detail.doc` | The validated ComponentDoc for one external component: an @scope/name/Component query, or a free-text term resolving to exactly one component. |
402
- | `discover.search` | The echoed query plus the matching {package, component} pairs, when a free-text term matches several components. |
403
- | `search` | The echoed query plus a ranked SearchResultEntry[] (domain, name, score, reason, description, follow-up command, and import path where relevant). |
404
- | `build.help` | A marker (`playbook: true`) that the renderer expands into the how-to-build-a-page workflow; emitted when no query is given. |
405
- | `build.kit` | The grouped composition kit: echoed query, hasResults/directMatch flags, the closest page templates, drop-in block patterns, idea-specific components/hooks, and the always-on frame + foundation component-name arrays. |
406
- | `swizzle.list` | The names of swizzlable components discoverable from cwd's @astryxdesign/core. |
407
- | `swizzle.copy` | An eject receipt: component name, owning package, output directory, files-copied count, the written file names, whether any file uses StyleX, and an optional maintainer note. |
408
- | `template.list` | Every discovered template (page + block); each entry carries id, name, description, kind, owning package, optional category and componentsUsed, and readiness flags. |
409
- | `template.show` | The resolved template's raw source plus its description, kind, and the component names it composes. |
410
- | `template.skeleton` | A layout skeleton (structural tags with spatial annotations) plus the template's description and the components it composes. |
411
- | `template.copy` | A scaffold receipt: template id, output directory, written file name, and file count. |
412
- | `template.cdn` | A write receipt for the no-build-step CDN starter page: the path (relative to cwd), the Astryx version every CDN URL was pinned to, whether it was written, and the reason it was not `exists` when a file was already there, which is a success. |
413
- | `hook.list` | The hook catalog grouped by category: `detail` (the level: names \| compact \| full) and `components`, the grouped map of hook names, brief entries, or a full HookDoc per entry. |
414
- | `hook.detail` | One hook's full authored HookDoc. |
415
- | `hook.detail.params` | Just one hook's parameters table (HookParamDoc[]). |
416
- | `theme.build` | A theme build receipt: name, token- and component-override counts, output size, the written outputs {css, js, dts, and variantsDts when applicable}, and any validation warnings. |
417
- | `theme.build.check` | The --check receipt: theme name, an upToDate flag, the stale outputs (each {path, reason: missing \| outdated}), and the full list of checked paths. Writes nothing. |
418
- | `theme.build.batch` | Several themes built in one invocation: `count` plus one {file, receipt} per theme in argument order, where receipt is that theme's theme.build (or theme.build.check) envelope, or null when it produced no CSS. |
419
- | `theme.list` | Every bundled theme as a ThemeListEntry[]: each with slug, displayName, description, and a maintained flag. |
420
- | `theme.add` | A scaffold receipt: resolved slug, displayName, maintained flag, outputDir (relative to cwd), the theme entry file, its exportName, and the files written. |
421
- | `theme.template` | A write receipt for the annotated theme template: the path (relative to cwd), whether it was written, and the reason it was not `exists` when a file was already there, which is a success. |
422
- | `upgrade.list` | Every available codemod, oldest→newest, as {name, title, version, optional}; returned for --list without running anything. |
423
- | `upgrade.status` | A short-circuit outcome with no codemods run (up_to_date, no_codemods, or config_fixable), each carrying the agent-docs summary. |
424
- | `upgrade.run` | The run receipt: from/to versions, codemod count, integrations processed, the agent-docs summary, and (apply mode) filesChanged, transformsApplied, and per-codemod errors. |
425
- | `manifest` | The self-describing CLI capability manifest: name, version, apiVersion, global options, the command tree (args, options, json flag, response types, examples), the jsonSupported allowlist, and the flat responseTypes index. |
426
- | `doctor` | The health-check report: `checks` (each with id, label, status: pass \| warn \| fail \| info, a message, and a fix when not passing) plus a `summary` of counts per status. |
427
- | `integration.validate` | The validation result: the package name and version (both null when no local manifest is found) plus issues, an AstryxIntegrationIssue[] of {code, severity: warning \| error, message}. |
428
- | `layout.expand` | The expansion: parsed form, generated TSX code, componentsUsed, states (count of useState hooks scaffolded), todos, blocksReferenced (each {name, mode}), warnings, and written (the output path, or null when nothing was written). |
429
- | `layout.check` | The validation result: a valid flag, the detected form, errors (each with line/col, message, formatted text, and suggestions), warnings, and the expression re-printed in both canonical surfaces (compact and outline). |
430
- | `layout.grammar` | The XLE/XLO grammar cheatsheet: a text field with the full reference plus an aliases map (short name canonical component) generated from this install's registry. |
386
+ | Type | What `data` carries |
387
+ | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
388
+ | `component.list` | The component catalog grouped by category: `detail` (the level: names \| compact \| full) and `components`, the grouped map of names+package, brief entries, or a full ComponentDoc per entry. |
389
+ | `component.detail` | One component's authored ComponentDoc plus ownership metadata (owner package, import specifier, and whether source is available). |
390
+ | `component.detail.props` | Just one component's props table (ComponentPropDoc[]). |
391
+ | `component.detail.source` | One component's source file, as {component, source}. |
392
+ | `component.detail.showcase` | One component's showcase example, as {component, aspectRatio, source}. |
393
+ | `component.detail.blocks` | One component's example blocks, as {component, showcase, examples, related} of BlockEntry. |
394
+ | `docs.list` | All reference-doc topics as DocsListEntry[] ({topic, description}), in discovery order. |
395
+ | `docs.detail` | One topic's full ReferenceDoc, with token-ref blocks inlined. |
396
+ | `docs.detail.section` | A single ReferenceSection of a topic: the first whose title contains the section query. |
397
+ | `blog.list` | The feed URL plus every post parsed from the RSS feed, each with slug, title, description, date, type, authors, link, and plaintext URL. |
398
+ | `blog.detail` | One post's metadata plus the feed URL and the post's full plaintext body. |
399
+ | `discover.list` | The configured external packages (name, category, components, version, description); when empty it carries meta.configured to tell "nothing configured" from "nothing discovered". |
400
+ | `discover.detail` | A single external package entry, for an @scope/name query. |
401
+ | `discover.detail.doc` | The validated ComponentDoc for one external component: an @scope/name/Component query, or a free-text term resolving to exactly one component. |
402
+ | `discover.search` | The echoed query plus the matching {package, component} pairs, when a free-text term matches several components. |
403
+ | `search` | The echoed query plus a ranked SearchResultEntry[] (domain, name, score, reason, description, follow-up command, and import path where relevant). |
404
+ | `build.help` | A marker (`playbook: true`) that the renderer expands into the how-to-build-a-page workflow; emitted when no query is given. |
405
+ | `build.kit` | The grouped composition kit: echoed query, hasResults/directMatch flags, the closest page templates, drop-in block patterns, idea-specific components/hooks, and the always-on frame + foundation component-name arrays. |
406
+ | `swizzle.list` | The names of swizzlable components discoverable from cwd's @astryxdesign/core. |
407
+ | `swizzle.copy` | An eject receipt: component name, owning package, output directory, files-copied count, the written file names, whether any file uses StyleX, and an optional maintainer note. |
408
+ | `template.list` | Every discovered template (page + block); each entry carries id, name, description, kind, owning package, optional category and componentsUsed, and readiness flags. |
409
+ | `template.show` | The resolved template's raw source plus its description, kind, and the component names it composes. |
410
+ | `template.skeleton` | A layout skeleton (structural tags with spatial annotations) plus the template's description and the components it composes. |
411
+ | `template.copy` | A scaffold receipt: template id, output directory, written file name, and file count. |
412
+ | `template.cdn` | A write receipt for the no-build-step CDN starter page: the path (relative to cwd), the Astryx version every CDN URL was pinned to, whether it was written, and the reason it was not. `exists` when a file was already there, which is a success. |
413
+ | `hook.list` | The hook catalog grouped by category: `detail` (the level: names \| compact \| full) and `components`, the grouped map of hook names, brief entries, or a full HookDoc per entry. |
414
+ | `hook.detail` | One hook's full authored HookDoc. |
415
+ | `hook.detail.params` | Just one hook's parameters table (HookParamDoc[]). |
416
+ | `theme.build` | A theme build receipt: name, token- and component-override counts, output size, the written outputs {css, js, dts, and variantsDts when applicable}, and any validation warnings. |
417
+ | `theme.build.check` | The --check receipt: theme name, an upToDate flag, the stale outputs (each {path, reason: missing \| outdated}), and the full list of checked paths. Writes nothing. |
418
+ | `theme.build.batch` | Several themes built in one invocation: `count` plus one {file, receipt} per theme in argument order, where receipt is that theme's theme.build (or theme.build.check) envelope, or null when it produced no CSS. |
419
+ | `theme.list` | Every bundled theme as a ThemeListEntry[]: each with slug, displayName, description, and a maintained flag. |
420
+ | `theme.add` | A scaffold receipt: resolved slug, displayName, maintained flag, outputDir (relative to cwd), the theme entry file, its exportName, and the files written. |
421
+ | `theme.template` | A write receipt for the annotated theme template: the path (relative to cwd), whether it was written, and the reason it was not. `exists` when a file was already there, which is a success. |
422
+ | `theme.targets` | The whole themeable surface: the echoed filter, the component count, and one entry per theming target — {key, className, component, props, states}, where props and states are its legal override keys. |
423
+ | `upgrade.list` | Every available codemod, oldest→newest, as {name, title, version, optional}; returned for --list without running anything. |
424
+ | `upgrade.status` | A short-circuit outcome with no codemods run (up_to_date, no_codemods, or config_fixable), each carrying the agent-docs summary. |
425
+ | `upgrade.run` | The run receipt: from/to versions, codemod count, integrations processed, the agent-docs summary, and (apply mode) filesChanged, transformsApplied, and per-codemod errors. |
426
+ | `manifest` | The self-describing CLI capability manifest: name, version, apiVersion, global options, the command tree (args, options, json flag, response types, examples), the jsonSupported allowlist, and the flat responseTypes index. |
427
+ | `doctor` | The health-check report: `checks` (each with id, label, status: pass \| warn \| fail \| info, a message, and a fix when not passing) plus a `summary` of counts per status. |
428
+ | `integration.validate` | The validation result: the package name and version (both null when no local manifest is found) plus issues, an AstryxIntegrationIssue[] of {code, severity: warning \| error, message}. |
429
+ | `layout.expand` | The expansion: parsed form, generated TSX code, componentsUsed, states (count of useState hooks scaffolded), todos, blocksReferenced (each {name, mode}), warnings, and written (the output path, or null when nothing was written). |
430
+ | `layout.check` | The validation result: a valid flag, the detected form, errors (each with line/col, message, formatted text, and suggestions), warnings, and the expression re-printed in both canonical surfaces (compact and outline). |
431
+ | `layout.grammar` | The XLE/XLO grammar cheatsheet: a text field with the full reference plus an aliases map (short name → canonical component) generated from this install's registry. |
431
432
 
432
433
  <!-- END GENERATED: response-types -->
433
434
  <!-- Generated by scripts/generate-cli-readme.mjs from the response-types EnumDoc. Run `pnpm -F @astryxdesign/cli readme`. -->
@@ -580,12 +581,13 @@ export default {
580
581
  };
581
582
  ```
582
583
 
583
- | Field | Type | Purpose |
584
- | ------------ | -------- | --------------------------------------------------------------------- |
585
- | `components` | `string` | Directory holding the package's components and their `.doc.*` files. |
586
- | `templates` | `string` | Directory holding the package's page/block templates. |
587
- | `codemods` | `string` | Directory holding upgrade codemods run by `astryx upgrade`. |
588
- | `issuesUrl` | `string` | Where "report an issue" links for this package's contributions point. |
584
+ | Field | Type | Purpose |
585
+ | ------------ | -------- | --------------------------------------------------------------------------------- |
586
+ | `components` | `string` | Directory holding the package's components and their `.doc.*` files. |
587
+ | `templates` | `string` | Directory holding the package's page/block templates. |
588
+ | `codemods` | `string` | Directory holding upgrade codemods run by `astryx upgrade`. |
589
+ | `docs` | `string` | Directory of reference docs; each `{topic}.doc.*` becomes a topic the CLI serves. |
590
+ | `issuesUrl` | `string` | Where "report an issue" links for this package's contributions point. |
589
591
 
590
592
  Every field is optional; declare only the roots the package ships. There is no
591
593
  factory: write a plain object, and annotate it with the `AstryxIntegration` type
@@ -19,8 +19,8 @@ export const doc = {
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
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. ' +
22
+ 'ones the project\'s configured integrations contribute, including any ' +
23
+ 'topic an integration replaces or extends, so it depends on the cwd. ' +
24
24
  'Overlay options select localized or dense variants.',
25
25
  importPath: '@astryxdesign/cli/api',
26
26
  signature:
package/api/index.d.mts CHANGED
@@ -31,6 +31,6 @@ export * from "./doctor/doctor.type.mjs";
31
31
  export * from "./layout/layout.type.mjs";
32
32
  export * from "./integration/validate-integration.type.mjs";
33
33
  export type Logger = import("./logger.mjs").Logger;
34
- export { themeBuild, themeAdd, themeList, listThemes } from "./theme/theme.mjs";
34
+ export { themeBuild, themeAdd, themeList, themeTargets, listThemes } from "./theme/theme.mjs";
35
35
  export { layoutExpand, layoutCheck, layoutGrammar } from "./layout/layout.mjs";
36
36
  export { validateIntegration, summarizeIssues } from "./integration/validate-integration.mjs";
package/api/index.mjs CHANGED
@@ -25,7 +25,7 @@ export {docs} from './docs/docs.mjs';
25
25
  export {blog} from './blog/blog.mjs';
26
26
  export {discover} from './discover/discover.mjs';
27
27
  export {template} from './template/template.mjs';
28
- export {themeBuild, themeAdd, themeList, listThemes} from './theme/theme.mjs';
28
+ export {themeBuild, themeAdd, themeList, themeTargets, listThemes} from './theme/theme.mjs';
29
29
  export {hook} from './hook/hook.mjs';
30
30
  export {search} from './search/search.mjs';
31
31
  export {build} from './build/build.mjs';
@@ -34,10 +34,12 @@ import {pathToFileURL} from 'node:url';
34
34
  import {findCoreDir} from '../../foundation/fs/paths.mjs';
35
35
  import {
36
36
  discoverComponents,
37
+ discoverIntegrationComponents,
37
38
  findComponentReadme,
38
39
  resolveImportPath,
39
40
  } from '../../foundation/discovery/component-discovery.mjs';
40
41
  import {discoverHooks, findHookDoc} from '../../foundation/discovery/hook-discovery.mjs';
42
+ import {loadIntegrationsSafely} from '../component/_adapter.mjs';
41
43
  import {levenshteinDistance} from '../../foundation/text/string-utils.mjs';
42
44
  import {discoverTemplates, extractComponents} from '../template/template.mjs';
43
45
  import {loadDocsCatalog, loadTopicDoc} from '../docs/_adapter.mjs';
@@ -350,12 +352,12 @@ async function loadModuleDoc(docPath, exportName = 'docs') {
350
352
  }
351
353
 
352
354
  /**
353
- * Build component candidates: name + keywords + usage/description from the
354
- * component's .doc.mjs.
355
+ * Build component candidates from core's own tree: name + keywords +
356
+ * usage/description from the component's .doc.mjs.
355
357
  * @param {string} coreDir
356
358
  * @returns {Promise<Candidate[]>}
357
359
  */
358
- async function gatherComponents(coreDir) {
360
+ async function gatherCoreComponents(coreDir) {
359
361
  const grouped = discoverComponents(coreDir);
360
362
  const names = Object.values(grouped).flat();
361
363
  /** @type {Candidate[]} */
@@ -383,6 +385,50 @@ async function gatherComponents(coreDir) {
383
385
  return candidates;
384
386
  }
385
387
 
388
+ /**
389
+ * Build component candidates contributed by the project's configured
390
+ * integrations (astryx.config's `integrations`): name + keywords +
391
+ * usage/description from each component's .doc.mjs, same as core. Without
392
+ * this, an integration component is invisible to `search`/`build` even
393
+ * though `component --list`/`component <Name>` already resolve it — the two
394
+ * discovery paths silently disagreed.
395
+ * @param {string} cwd
396
+ * @returns {Promise<Candidate[]>}
397
+ */
398
+ async function gatherIntegrationComponents(cwd) {
399
+ const loadedIntegrations = await loadIntegrationsSafely(cwd);
400
+ /** @type {Candidate[]} */
401
+ const candidates = [];
402
+ for (const integration of loadedIntegrations) {
403
+ for (const rec of discoverIntegrationComponents(integration)) {
404
+ const doc = await loadModuleDoc(rec.docPath);
405
+ candidates.push({
406
+ domain: 'component',
407
+ name: rec.name,
408
+ keywords: doc && Array.isArray(doc.keywords) ? doc.keywords : [],
409
+ description: doc ? doc.usage?.description || doc.description || '' : '',
410
+ _import: rec.package,
411
+ });
412
+ }
413
+ }
414
+ return candidates;
415
+ }
416
+
417
+ /**
418
+ * Build component candidates: core's own tree plus every configured
419
+ * integration's components.
420
+ * @param {string} coreDir
421
+ * @param {string} cwd
422
+ * @returns {Promise<Candidate[]>}
423
+ */
424
+ async function gatherComponents(coreDir, cwd) {
425
+ const [core, integrations] = await Promise.all([
426
+ gatherCoreComponents(coreDir),
427
+ gatherIntegrationComponents(cwd),
428
+ ]);
429
+ return [...core, ...integrations];
430
+ }
431
+
386
432
  /**
387
433
  * Build hook candidates: name + keywords + usage/description from the hook's
388
434
  * .doc.mjs.
@@ -611,7 +657,7 @@ export async function search(query, options = {}) {
611
657
  /** @param {string} d */
612
658
  const wants = d => !type || type === d;
613
659
  const [components, hooks, docTopics, templates] = await Promise.all([
614
- wants('component') ? gatherComponents(coreDir) : [],
660
+ wants('component') ? gatherComponents(coreDir, cwd) : [],
615
661
  wants('hook') ? gatherHooks(coreDir) : [],
616
662
  wants('doc') ? gatherDocs(cwd) : [],
617
663
  wants('template') ? gatherTemplates(cwd) : [],
@@ -10,9 +10,17 @@
10
10
  * `limit`, an empty query, and a bad `--type` all throw AstryxError with the
11
11
  * ERR_INVALID_ARGUMENT code, so a direct `@astryxdesign/cli/api` caller gets the
12
12
  * same contract as `astryx search` on the command line.
13
+ *
14
+ * The last describe block covers integration-contributed components, using the
15
+ * same temp-consumer harness as template-integration.test.mjs. Before this,
16
+ * `search`/`build` only ever scanned @astryxdesign/core — an integration's own
17
+ * components were invisible to both, even though `component --list` and
18
+ * `component <Name>` already resolved them. The two discovery paths silently
19
+ * disagreed.
13
20
  */
14
21
 
15
22
  import {describe, it, expect} from 'vitest';
23
+ import * as fs from 'node:fs';
16
24
  import * as path from 'node:path';
17
25
  import {fileURLToPath} from 'node:url';
18
26
  import {search, SEARCH_DOMAINS} from './search.mjs';
@@ -92,3 +100,66 @@ describe('search leaf — limit validation (API matches the CLI contract)', () =
92
100
  });
93
101
  }, SLOW);
94
102
  });
103
+
104
+ describe('search leaf — integration components', () => {
105
+ /**
106
+ * A minimal consumer project: a stub `@astryxdesign/core` (so `findCoreDir`
107
+ * resolves without needing the real package) plus an installed
108
+ * `@acme/widgets` integration that contributes one component.
109
+ */
110
+ function makeConsumerWithIntegrationComponent() {
111
+ const dir = fs.mkdtempSync(path.join(process.cwd(), '.astryx-search-it-'));
112
+ fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({name: 'consumer'}));
113
+ fs.writeFileSync(
114
+ path.join(dir, 'astryx.config.mjs'),
115
+ `export default { integrations: ['@acme/widgets'] };\n`,
116
+ );
117
+
118
+ // Stub core: just needs to exist with an (empty) src/ so discoverComponents
119
+ // doesn't throw. Its own component list is irrelevant to this test.
120
+ const coreDir = path.join(dir, 'node_modules', '@astryxdesign', 'core');
121
+ fs.mkdirSync(path.join(coreDir, 'src'), {recursive: true});
122
+
123
+ const widgetsDir = path.join(dir, 'node_modules', '@acme', 'widgets');
124
+ fs.mkdirSync(path.join(widgetsDir, 'components'), {recursive: true});
125
+ fs.writeFileSync(
126
+ path.join(widgetsDir, 'package.json'),
127
+ JSON.stringify({name: '@acme/widgets', version: '1.0.0'}),
128
+ );
129
+ fs.writeFileSync(
130
+ path.join(widgetsDir, 'astryx.integration.mjs'),
131
+ `export default { components: './components' };\n`,
132
+ );
133
+ fs.writeFileSync(
134
+ path.join(widgetsDir, 'components', 'FancyGizmo.doc.mjs'),
135
+ `export const docs = {
136
+ name: 'FancyGizmo',
137
+ keywords: ['gizmo', 'widget'],
138
+ usage: {description: 'A fancy gizmo widget.'},
139
+ };\n`,
140
+ );
141
+
142
+ return dir;
143
+ }
144
+
145
+ it('includes a component contributed by a configured integration', async () => {
146
+ const dir = makeConsumerWithIntegrationComponent();
147
+ try {
148
+ const r = await search('gizmo', {cwd: dir, type: 'component'});
149
+ expect(r.data.results.some(x => x.name === 'FancyGizmo')).toBe(true);
150
+ } finally {
151
+ fs.rmSync(dir, {recursive: true, force: true});
152
+ }
153
+ }, SLOW);
154
+
155
+ it('reports the contributing package as the import hint', async () => {
156
+ const dir = makeConsumerWithIntegrationComponent();
157
+ try {
158
+ const r = await search('FancyGizmo', {cwd: dir, type: 'component'});
159
+ const hit = r.data.results.find(x => x.name === 'FancyGizmo');
160
+ expect(hit?.import).toBe('@acme/widgets');
161
+ } finally {
162
+ fs.rmSync(dir, {recursive: true, force: true});
163
+ }
164
+ }, SLOW);
165
+ });
@@ -104,7 +104,7 @@ export const doc = {
104
104
  {
105
105
  type: 'template.skeleton',
106
106
  description:
107
- "A layout skeleton structural tags with spatial annotations plus the template's description and the components it composes.",
107
+ "A layout skeleton (structural tags with spatial annotations) plus the template's description and the components it composes.",
108
108
  },
109
109
  {
110
110
  type: 'template.copy',
@@ -114,7 +114,7 @@ export const doc = {
114
114
  {
115
115
  type: 'template.cdn',
116
116
  description:
117
- 'A write receipt for the CDN starter page: the path (relative to cwd), the Astryx version every CDN URL was pinned to, whether it was written, and the reason it was not `exists` when a file was already there, which is a success.',
117
+ 'A write receipt for the CDN starter page: the path (relative to cwd), the Astryx version every CDN URL was pinned to, whether it was written, and the reason it was not. `exists` when a file was already there, which is a success.',
118
118
  },
119
119
  ],
120
120
  throws: [
@@ -43,6 +43,10 @@ import {ERROR_CODES} from '../../../foundation/response/error-codes.mjs';
43
43
  import {AstryxError} from '../../error.mjs';
44
44
  import {logger} from '../../logger.mjs';
45
45
  import {loadComponentDoc} from '../../../foundation/discovery/component-loader.mjs';
46
+ import {
47
+ collectThemingTargets,
48
+ targetsByKey,
49
+ } from '../../../foundation/discovery/theming-targets.mjs';
46
50
  import {
47
51
  collectUnloadedFonts,
48
52
  formatFontLoadingHelp,
@@ -882,6 +886,9 @@ ${iconType}export declare const ${toIdentifier(themeDef.name)}Theme: DefinedThem
882
886
  * Returns null when docs are unavailable so validation can skip unknown-key
883
887
  * warnings rather than guessing from a second registry.
884
888
  *
889
+ * Shares its enumeration with `theme targets`, so what a theme author can list
890
+ * is exactly what this validator accepts.
891
+ *
885
892
  * @returns {Promise<Record<string, string[]> | null>}
886
893
  */
887
894
  async function loadKnownComponents() {
@@ -889,44 +896,7 @@ async function loadKnownComponents() {
889
896
  const coreSrc = coreRoot ? path.join(coreRoot, 'src') : null;
890
897
  if (!coreSrc || !fs.existsSync(coreSrc)) return null;
891
898
 
892
- /** @type {Record<string, string[]>} */
893
- const targets = {};
894
-
895
- /** @param {string} dir */
896
- async function scan(dir) {
897
- const entries = fs.readdirSync(dir, {withFileTypes: true});
898
- for (const entry of entries) {
899
- const full = path.join(dir, entry.name);
900
- if (entry.isDirectory()) {
901
- if (entry.name === 'node_modules' || entry.name === '__tests__') continue;
902
- await scan(full);
903
- continue;
904
- }
905
- if (!entry.name.endsWith('.doc.mjs')) continue;
906
-
907
- /** @type {any} */
908
- let doc;
909
- try {
910
- doc = await loadComponentDoc(full);
911
- } catch {
912
- continue;
913
- }
914
-
915
- for (const target of doc?.theming?.targets || []) {
916
- const className = target?.className;
917
- if (typeof className !== 'string') continue;
918
- const key = className.replace(/^astryx-/, '');
919
- if (!key) continue;
920
- const props = [target.visualProps, target.states]
921
- .filter(list => Array.isArray(list))
922
- .flat()
923
- .filter((/** @type {unknown} */ p) => typeof p === 'string');
924
- targets[key] = [...new Set([...(targets[key] || []), ...props])];
925
- }
926
- }
927
- }
928
-
929
- await scan(coreSrc);
899
+ const targets = targetsByKey(await collectThemingTargets(coreSrc));
930
900
  return Object.keys(targets).length > 0 ? targets : null;
931
901
  }
932
902
 
@@ -0,0 +1,18 @@
1
+ // @generated by scripts/sync-api-types.mjs from the JSDoc in api/**/*.mjs.
2
+ // DO NOT EDIT — run `pnpm sync:api-types` to regenerate.
3
+
4
+ /**
5
+ * List every component theming target — the `defineTheme` `components` keys,
6
+ * with the props and states each one accepts.
7
+ *
8
+ * A filter naming a component exactly wins over a substring search, so
9
+ * `theme targets Button` is Button's own set (what `astryx component Button`
10
+ * prints) rather than every key that happens to contain "button".
11
+ *
12
+ * @param {string} [filter] - component name, or a substring of a target key
13
+ * @param {{cwd?: string}} [ctx]
14
+ * @returns {Promise<import('../theme.type.mjs').ThemeTargetsResponse>}
15
+ */
16
+ export function themeTargets(filter?: string, { cwd }?: {
17
+ cwd?: string;
18
+ }): Promise<import("../theme.type.mjs").ThemeTargetsResponse>;
@@ -0,0 +1,87 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file `astryx theme targets` leaf — the whole themeable surface, enumerated.
5
+ *
6
+ * @input a cwd (to resolve the project's `@astryxdesign/core`) and an optional
7
+ * component/key filter
8
+ * @output the `theme.targets` envelope: one row per theming target
9
+ * @position api/theme/targets — projection over
10
+ * foundation/discovery/theming-targets.mjs, the same component docs
11
+ * `astryx component <Name>` prints its Theming table from.
12
+ */
13
+
14
+ import * as path from 'node:path';
15
+ import {findCoreDir} from '../../../foundation/fs/paths.mjs';
16
+ import {collectThemingTargets} from '../../../foundation/discovery/theming-targets.mjs';
17
+ import {ERROR_CODES} from '../../../foundation/response/error-codes.mjs';
18
+ import {AstryxError} from '../../error.mjs';
19
+
20
+ /**
21
+ * Whether a target matches the caller's filter loosely: any target whose key,
22
+ * class, or component contains it — so `theme targets thumb` finds the switch
23
+ * thumb without knowing which component owns it.
24
+ * @param {import('../../../foundation/discovery/theming-targets.mjs').ThemingTarget} target
25
+ * @param {string} filter - already lowercased
26
+ * @returns {boolean}
27
+ */
28
+ function matchesLoosely(target, filter) {
29
+ return (
30
+ target.key.toLowerCase().includes(filter) ||
31
+ target.className.toLowerCase().includes(filter) ||
32
+ target.component.toLowerCase().includes(filter)
33
+ );
34
+ }
35
+
36
+ /**
37
+ * List every component theming target — the `defineTheme` `components` keys,
38
+ * with the props and states each one accepts.
39
+ *
40
+ * A filter naming a component exactly wins over a substring search, so
41
+ * `theme targets Button` is Button's own set (what `astryx component Button`
42
+ * prints) rather than every key that happens to contain "button".
43
+ *
44
+ * @param {string} [filter] - component name, or a substring of a target key
45
+ * @param {{cwd?: string}} [ctx]
46
+ * @returns {Promise<import('../theme.type.mjs').ThemeTargetsResponse>}
47
+ */
48
+ export async function themeTargets(filter, {cwd = process.cwd()} = {}) {
49
+ const coreDir = findCoreDir(cwd);
50
+ if (!coreDir) {
51
+ throw new AstryxError(
52
+ 'Could not find @astryxdesign/core package',
53
+ undefined,
54
+ ERROR_CODES.ERR_CORE_NOT_FOUND,
55
+ );
56
+ }
57
+
58
+ const all = await collectThemingTargets(path.join(coreDir, 'src'));
59
+ const needle = filter ? String(filter).toLowerCase() : null;
60
+ let targets = all;
61
+ if (needle) {
62
+ const named = all.filter(t => t.component.toLowerCase() === needle);
63
+ targets = named.length > 0 ? named : all.filter(t => matchesLoosely(t, needle));
64
+ }
65
+
66
+ if (needle && targets.length === 0) {
67
+ const near = [...new Set(all.map(t => t.component))]
68
+ .filter(name => name.toLowerCase().startsWith(needle.slice(0, 3)))
69
+ .sort()
70
+ .slice(0, 5)
71
+ .map(name => ({name, reason: 'has theming targets'}));
72
+ throw new AstryxError(
73
+ `No theming target matches "${filter}". Run \`theme targets\` with no filter for the whole list.`,
74
+ near.length > 0 ? near : undefined,
75
+ ERROR_CODES.ERR_UNKNOWN_COMPONENT,
76
+ );
77
+ }
78
+
79
+ return {
80
+ type: 'theme.targets',
81
+ data: {
82
+ filter: filter ?? null,
83
+ componentCount: new Set(targets.map(t => t.component)).size,
84
+ targets,
85
+ },
86
+ };
87
+ }
@@ -0,0 +1,65 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Direct-API tests for the `theme targets` leaf. Runs against the real
5
+ * core docs, so it doubles as a guard that the themeable surface stays
6
+ * readable and shaped as `theme.targets` promises.
7
+ */
8
+
9
+ import {describe, it, expect} from 'vitest';
10
+ import {themeTargets} from './targets.mjs';
11
+
12
+ describe('themeTargets (api/theme/targets)', () => {
13
+ it('returns a theme.targets envelope covering the whole surface', async () => {
14
+ const result = await themeTargets();
15
+ expect(result.type).toBe('theme.targets');
16
+ expect(result.data.filter).toBeNull();
17
+ expect(result.data.targets.length).toBeGreaterThan(100);
18
+ expect(result.data.componentCount).toBeGreaterThan(50);
19
+ for (const t of result.data.targets) {
20
+ expect(Object.keys(t).sort()).toEqual([
21
+ 'className',
22
+ 'component',
23
+ 'key',
24
+ 'props',
25
+ 'states',
26
+ ]);
27
+ }
28
+ }, 60_000);
29
+
30
+ it('scopes to one component by name', async () => {
31
+ const {data} = await themeTargets('Switch');
32
+ expect(data.filter).toBe('Switch');
33
+ expect(data.componentCount).toBe(1);
34
+ expect(data.targets.map(t => t.key)).toEqual([
35
+ 'switch',
36
+ 'switch-field',
37
+ 'switch-thumb',
38
+ ]);
39
+ }, 60_000);
40
+
41
+ // Half the system's keys contain "button" (chat-send-button, toggle-button,
42
+ // …). A component name has to mean the component, or `theme targets Button`
43
+ // answers a different question than `component Button` and the two views
44
+ // look like they disagree.
45
+ it('prefers an exact component name over a substring match', async () => {
46
+ const {data} = await themeTargets('Button');
47
+ expect(data.targets.map(t => t.key)).toEqual(['button']);
48
+ }, 60_000);
49
+
50
+ // This command answers "which theme slot paints the switch thumb?" — a
51
+ // question you can only ask by the part, not the component, until you
52
+ // already know which component owns it.
53
+ it('searches keys by substring, across components', async () => {
54
+ const {data} = await themeTargets('thumb');
55
+ expect(data.componentCount).toBeGreaterThan(1);
56
+ expect(data.targets.map(t => t.key)).toContain('switch-thumb');
57
+ for (const t of data.targets) expect(t.key).toContain('thumb');
58
+ }, 60_000);
59
+
60
+ it('rejects a filter that matches nothing, with components to try', async () => {
61
+ await expect(themeTargets('nosuchthing')).rejects.toMatchObject({
62
+ code: 'ERR_UNKNOWN_COMPONENT',
63
+ });
64
+ }, 60_000);
65
+ });