@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.
- package/CHANGELOG.md +4 -0
- package/README.md +53 -51
- package/api/docs/docs.doc.mjs +2 -2
- package/api/index.d.mts +1 -1
- package/api/index.mjs +1 -1
- package/api/search/search.mjs +50 -4
- package/api/search/search.test.mjs +71 -0
- package/api/template/template.doc.mjs +2 -2
- package/api/theme/build/build.mjs +8 -38
- package/api/theme/targets/targets.d.mts +18 -0
- package/api/theme/targets/targets.mjs +87 -0
- package/api/theme/targets/targets.test.mjs +65 -0
- package/api/theme/theme.d.mts +1 -0
- package/api/theme/theme.mjs +3 -1
- package/api/theme/theme.type.d.mts +23 -0
- package/api/theme/theme.type.mjs +21 -1
- package/api/theme/themeTargets.doc.d.mts +11 -0
- package/api/theme/themeTargets.doc.mjs +58 -0
- package/api/theme/themeTemplate.doc.mjs +3 -3
- package/assets/codemods/transforms/next/__tests__/next-codemods.test.mjs +127 -0
- package/assets/codemods/transforms/next/banner-collapsible-content.mjs +171 -0
- package/assets/codemods/transforms/next/index.mjs +11 -1
- package/assets/docs/README.md +50 -0
- package/assets/docs/cli-integrations.doc.mjs +4 -4
- package/assets/docs/theme.doc.dense.mjs +1 -1
- package/assets/docs/theme.doc.mjs +1 -1
- package/assets/docs/typography.doc.mjs +2 -2
- package/assets/templates/blocks/components/Banner/BannerCollapsibleContent.doc.mjs +1 -1
- package/assets/templates/blocks/components/Banner/BannerCollapsibleContent.tsx +1 -1
- package/assets/templates/blocks/components/Dialog/DialogAdaptivePresentation.doc.mjs +25 -0
- package/assets/templates/blocks/components/Dialog/DialogAdaptivePresentation.tsx +167 -0
- package/assets/templates/blocks/components/Dialog/DialogScrollingContent.tsx +1 -1
- package/assets/templates/blocks/components/Step/StepContent.doc.mjs +14 -0
- package/assets/templates/blocks/components/Step/StepContent.tsx +32 -0
- package/assets/templates/blocks/components/Step/StepIndicator.doc.mjs +14 -0
- package/assets/templates/blocks/components/Step/StepIndicator.tsx +60 -0
- package/assets/templates/blocks/components/Step/StepShowcase.doc.mjs +15 -0
- package/assets/templates/blocks/components/Step/StepShowcase.tsx +26 -0
- package/assets/templates/blocks/components/Step/StepStates.doc.mjs +14 -0
- package/assets/templates/blocks/components/Step/StepStates.tsx +46 -0
- package/assets/templates/blocks/components/Stepper/StepperCustomContent.doc.mjs +22 -0
- package/assets/templates/blocks/components/Stepper/StepperCustomContent.tsx +126 -0
- package/assets/templates/blocks/components/Stepper/StepperIndicatorModes.doc.mjs +1 -1
- package/assets/templates/blocks/components/Stepper/StepperIndicatorModes.tsx +17 -5
- package/assets/templates/blocks/components/Stepper/StepperOnTrackHorizontal.doc.mjs +14 -0
- package/assets/templates/blocks/components/Stepper/StepperOnTrackHorizontal.tsx +25 -0
- package/assets/templates/blocks/components/Stepper/StepperOnTrackVertical.doc.mjs +2 -2
- package/assets/templates/blocks/components/Stepper/StepperOnTrackVertical.tsx +1 -1
- package/assets/templates/blocks/components/Stepper/StepperShowcase.doc.mjs +1 -1
- package/assets/templates/blocks/components/Stepper/StepperShowcase.tsx +6 -7
- package/assets/templates/blocks/components/Stepper/StepperStatus.tsx +1 -1
- package/assets/templates/pages/mixed-gallery/page.tsx +12 -3
- package/assets/templates/pages/table-grouped/page.tsx +151 -144
- package/assets/templates/themes/neutral/neutralTheme.ts +13 -8
- package/clients/cli/commands/build-theme.mjs +85 -0
- package/clients/cli/commands/dialog-adaptive-template.test.mjs +24 -0
- package/clients/cli/commands/theme-targets.behavior.test.mjs +64 -0
- package/clients/cli/commands/theme-targets.doc.mjs +38 -0
- package/clients/cli/commands/theme-template.doc.mjs +2 -2
- package/clients/cli/commands/theme.doc.mjs +4 -2
- package/clients/cli/index.mjs +1 -0
- package/clients/cli/lib/manifest.mjs +2 -0
- package/foundation/discovery/theming-targets.d.mts +86 -0
- package/foundation/discovery/theming-targets.mjs +202 -0
- package/foundation/discovery/theming-targets.test.mjs +245 -0
- package/foundation/response/response-types.doc.mjs +7 -2
- package/package.json +9 -9
- package/assets/templates/blocks/components/Stepper/StepperHorizontal.doc.mjs +0 -14
- package/assets/templates/blocks/components/Stepper/StepperHorizontal.tsx +0 -24
- package/assets/templates/blocks/components/Stepper/StepperMultiStepForm.doc.mjs +0 -14
- package/assets/templates/blocks/components/Stepper/StepperMultiStepForm.tsx +0 -92
- package/assets/templates/blocks/components/Stepper/StepperVerticalOnboarding.doc.mjs +0 -14
- package/assets/templates/blocks/components/Stepper/StepperVerticalOnboarding.tsx +0 -40
package/CHANGELOG.md
CHANGED
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
|
|
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
|
|
422
|
-
| `
|
|
423
|
-
| `upgrade.
|
|
424
|
-
| `upgrade.
|
|
425
|
-
| `
|
|
426
|
-
| `
|
|
427
|
-
| `
|
|
428
|
-
| `
|
|
429
|
-
| `layout.
|
|
430
|
-
| `layout.
|
|
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
|
-
| `
|
|
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
|
package/api/docs/docs.doc.mjs
CHANGED
|
@@ -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
|
|
23
|
-
'topic an integration replaces or extends
|
|
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';
|
package/api/search/search.mjs
CHANGED
|
@@ -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 +
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
+
});
|