@astryxdesign/cli 0.1.2-canary.b712653 → 0.1.2-canary.bbf77ea
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 +0 -1
- package/README.md +49 -55
- package/docs/theme.doc.mjs +2 -2
- package/package.json +19 -7
- package/src/api/component.mjs +253 -12
- package/src/api/discover.mjs +14 -7
- package/src/api/doctor.mjs +10 -25
- package/src/api/layout.mjs +11 -10
- package/src/api/layout.test.mjs +4 -1
- package/src/api/template-integration.test.mjs +225 -0
- package/src/api/template.mjs +242 -84
- package/src/api/validate-integration.mjs +370 -0
- package/src/api/validate-integration.test.mjs +222 -0
- package/src/codemod.mjs +93 -0
- package/src/codemod.test.mjs +134 -0
- package/src/codemods/__tests__/registry.test.mjs +1 -0
- package/src/codemods/__tests__/runner.test.mjs +103 -0
- package/src/codemods/integration-discovery.mjs +168 -0
- package/src/codemods/integration-discovery.test.mjs +234 -0
- package/src/codemods/integration-runner.mjs +109 -0
- package/src/codemods/registry.mjs +1 -0
- package/src/codemods/run-codemod.mjs +207 -0
- package/src/codemods/runner.mjs +73 -96
- package/src/codemods/transforms/v0.0.15/index.mjs +0 -13
- package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-css-surfaces.test.mjs +67 -0
- package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-declare-module.test.mjs +61 -0
- package/src/codemods/transforms/v0.1.0/__tests__/v0.1.0-ordering.test.mjs +104 -0
- package/src/codemods/transforms/{v0.0.15 → v0.1.0}/drop-xds-prefix-imports.mjs +15 -4
- package/src/codemods/transforms/v0.1.0/index.mjs +43 -6
- package/src/codemods/transforms/v0.1.0/migrate-xds-css-surfaces.mjs +77 -0
- package/src/codemods/transforms/v0.1.0/migrate-xds-declare-module.mjs +78 -0
- package/src/codemods/transforms/v0.1.3/__tests__/migrate-layout-components-to-experimental.test.mjs +360 -0
- package/src/codemods/transforms/v0.1.3/index.mjs +19 -0
- package/src/codemods/transforms/v0.1.3/migrate-layout-components-to-experimental.mjs +260 -0
- package/src/commands/agent-docs.mjs +1 -1
- package/src/commands/component/index.mjs +44 -10
- package/src/commands/component-ownership.test.mjs +227 -0
- package/src/commands/discover.mjs +5 -16
- package/src/commands/doctor.test.mjs +3 -3
- package/src/commands/json-contract.test.mjs +0 -32
- package/src/commands/swizzle.mjs +224 -235
- package/src/commands/swizzle.path-safety.test.mjs +3 -3
- package/src/commands/swizzle.routing.test.mjs +279 -0
- package/src/commands/template.mjs +33 -37
- package/src/commands/upgrade.config-ordering.test.mjs +273 -0
- package/src/commands/upgrade.integration-policy.test.mjs +204 -0
- package/src/commands/upgrade.mjs +349 -170
- package/src/commands/validate-integration.mjs +110 -0
- package/src/commands/validate-integration.test.mjs +124 -0
- package/src/config.mjs +7 -20
- package/src/config.test.mjs +89 -13
- package/src/index.mjs +7 -3
- package/src/integration.mjs +19 -0
- package/src/lib/component-discovery.mjs +177 -0
- package/src/lib/config-cache.mjs +91 -0
- package/src/lib/config-cache.test.mjs +83 -0
- package/src/lib/config-schema.mjs +41 -55
- package/src/lib/error-codes.mjs +7 -12
- package/src/lib/integration-warnings.mjs +62 -0
- package/src/lib/integration-warnings.test.mjs +102 -0
- package/src/lib/integrations.mjs +92 -115
- package/src/lib/integrations.test.mjs +88 -107
- package/src/lib/manifest.mjs +5 -2
- package/src/lib/module-loader.mjs +80 -0
- package/src/lib/module-loader.test.mjs +106 -0
- package/src/lib/project.mjs +502 -0
- package/src/lib/project.test.mjs +308 -0
- package/src/template.mjs +73 -0
- package/src/template.test.mjs +127 -0
- package/src/types/api.d.ts +2 -6
- package/src/types/base.d.ts +3 -9
- package/src/types/codemod.d.ts +81 -0
- package/src/types/component.d.ts +27 -2
- package/src/types/config.d.ts +56 -85
- package/src/types/error-codes.d.ts +5 -3
- package/src/types/index.d.ts +0 -1
- package/src/types/integration.d.ts +29 -0
- package/src/types/swizzle.d.ts +9 -2
- package/src/types/template-api.d.ts +54 -0
- package/src/types/template.d.ts +10 -7
- package/src/types/upgrade.d.ts +29 -0
- package/src/types/validate-integration.d.ts +24 -0
- package/src/utils/github.mjs +0 -237
- package/src/utils/interactive.mjs +2 -2
- package/templates/blocks/components/DateRangeInput/DateRangeInputWithPresets.doc.mjs +14 -0
- package/templates/blocks/components/DateRangeInput/DateRangeInputWithPresets.tsx +46 -0
- package/templates/blocks/components/DateRangeInput/DateRangeInputWithValidation.doc.mjs +14 -0
- package/templates/blocks/components/DateRangeInput/DateRangeInputWithValidation.tsx +52 -0
- package/templates/blocks/components/DateTimeInput/DateTimeInputWithValidation.doc.mjs +14 -0
- package/templates/blocks/components/DateTimeInput/DateTimeInputWithValidation.tsx +43 -0
- package/templates/blocks/components/DropdownMenu/DropdownMenuShowcase.tsx +0 -1
- package/templates/blocks/components/DropdownMenuItem/DropdownMenuItemShowcase.tsx +0 -1
- package/templates/blocks/components/HoverCard/HoverCardInteractiveContent.doc.mjs +1 -1
- package/templates/blocks/components/HoverCard/HoverCardInteractiveContent.tsx +9 -4
- package/templates/blocks/components/LinkProvider/LinkProviderCustomLink.doc.mjs +1 -1
- package/templates/blocks/components/MoreMenu/MoreMenuShowcase.tsx +0 -1
- package/templates/blocks/components/Tab/TabWithSelectedIcon.doc.mjs +13 -0
- package/templates/blocks/components/Tab/TabWithSelectedIcon.tsx +39 -0
- package/templates/blocks/components/Table/StickyColumnsHookUsage.doc.mjs +1 -1
- package/templates/pages/shell-side-nav/page.tsx +0 -1
- package/templates/pages/shell-side-nav/template.doc.mjs +1 -1
- package/templates/pages/shell-top-nav/template.doc.mjs +1 -1
- package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-config-surfaces.test.mjs +0 -116
- package/src/codemods/transforms/v0.1.0/migrate-xds-config-surfaces.mjs +0 -230
- package/src/commands/gap-report.mjs +0 -464
- package/src/commands/gap-report.test.mjs +0 -168
- package/src/commands/swizzle-gap-safety.test.mjs +0 -273
- package/src/lib/config.mjs +0 -113
- package/src/lib/config.test.mjs +0 -91
- package/src/types/gap-report.d.ts +0 -29
- /package/src/codemods/transforms/{v0.0.15 → v0.1.0}/__tests__/drop-xds-prefix-imports.test.mjs +0 -0
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -62,8 +62,7 @@ Options:
|
|
|
62
62
|
| `upgrade` | Run codemods to migrate between versions |
|
|
63
63
|
| `theme build` | Compile a defineTheme file to production CSS and JS |
|
|
64
64
|
| `discover` | Discover external packages and components |
|
|
65
|
-
| `
|
|
66
|
-
| `doctor` | Diagnose your XDS setup and report problems with fixes (CI-friendly via exit code) |
|
|
65
|
+
| `doctor` | Diagnose your Astryx setup and report problems with fixes (CI-friendly via exit code) |
|
|
67
66
|
|
|
68
67
|
### Global options
|
|
69
68
|
|
|
@@ -161,7 +160,6 @@ if (isError(result)) {
|
|
|
161
160
|
| `ERR_INVALID_VERSION` | A `--from`/`--to` value was not a valid semver string. |
|
|
162
161
|
| `ERR_DEP_MISSING` | A required external dependency (e.g. jscodeshift) is missing. |
|
|
163
162
|
| `ERR_GH_CLI` | GitHub CLI (`gh`) is not installed or not authenticated. |
|
|
164
|
-
| `ERR_GAP_REPORT_FAILED` | Filing a gap report failed (disabled, or the integration errored). |
|
|
165
163
|
|
|
166
164
|
## Capability manifest (agent discovery)
|
|
167
165
|
|
|
@@ -172,7 +170,7 @@ discriminators each command can emit. Think of it as an OpenAPI spec for the CLI
|
|
|
172
170
|
|
|
173
171
|
```bash
|
|
174
172
|
astryx manifest --json # dedicated surface — type: "manifest"
|
|
175
|
-
|
|
173
|
+
astryx --json # bare invocation — embeds the same payload under data.manifest
|
|
176
174
|
```
|
|
177
175
|
|
|
178
176
|
Shape:
|
|
@@ -182,7 +180,7 @@ Shape:
|
|
|
182
180
|
"apiVersion": 1,
|
|
183
181
|
"type": "manifest",
|
|
184
182
|
"data": {
|
|
185
|
-
"name": "
|
|
183
|
+
"name": "astryx",
|
|
186
184
|
"version": "0.0.14",
|
|
187
185
|
"description": "Design system CLI — components, themes, and tooling",
|
|
188
186
|
"globalOptions": [
|
|
@@ -249,14 +247,14 @@ the `JSON_SUPPORTED` allowlist and a small declarative `RESPONSE_TYPES` map in
|
|
|
249
247
|
`src/lib/manifest.mjs`, guarded by a drift test (`manifest.test.mjs`) so adding a
|
|
250
248
|
command without describing it fails CI.
|
|
251
249
|
|
|
252
|
-
**Backwards-compat:** the bare `
|
|
250
|
+
**Backwards-compat:** the bare `astryx --json` envelope keeps `type: "help"` and its
|
|
253
251
|
original shallow fields (`name`, `version`, `commands` as a `string[]` of names,
|
|
254
252
|
`jsonSupported`); the full structured manifest is additive under `data.manifest`.
|
|
255
253
|
For the standalone manifest envelope (`type: "manifest"`), use `astryx manifest --json`.
|
|
256
254
|
|
|
257
255
|
## Programmatic API
|
|
258
256
|
|
|
259
|
-
The same logic that powers `
|
|
257
|
+
The same logic that powers `astryx --json` is available as importable, type-safe functions:
|
|
260
258
|
|
|
261
259
|
```typescript
|
|
262
260
|
import {
|
|
@@ -269,20 +267,20 @@ import {
|
|
|
269
267
|
AstryxError,
|
|
270
268
|
} from '@astryxdesign/cli/api';
|
|
271
269
|
|
|
272
|
-
// Same result as:
|
|
270
|
+
// Same result as: astryx --json component Button
|
|
273
271
|
const btn = await component('Button');
|
|
274
272
|
btn.type; // 'component.detail'
|
|
275
273
|
btn.data.name; // 'Button' (typed as ComponentDoc)
|
|
276
274
|
|
|
277
|
-
// Same result as:
|
|
275
|
+
// Same result as: astryx --json component --list
|
|
278
276
|
const list = await component(undefined, {list: true});
|
|
279
277
|
list.data; // Record<string, string[]>
|
|
280
278
|
|
|
281
|
-
// Same result as:
|
|
279
|
+
// Same result as: astryx --json docs principles
|
|
282
280
|
const principles = await docs('principles');
|
|
283
|
-
principles.data.title; // '
|
|
281
|
+
principles.data.title; // 'Principles'
|
|
284
282
|
|
|
285
|
-
// Same result as:
|
|
283
|
+
// Same result as: astryx --json hook useMediaQuery
|
|
286
284
|
const useMediaQuery = await hook('useMediaQuery');
|
|
287
285
|
useMediaQuery.data.params; // typed as HookParamDoc[]
|
|
288
286
|
|
|
@@ -296,7 +294,7 @@ try {
|
|
|
296
294
|
}
|
|
297
295
|
```
|
|
298
296
|
|
|
299
|
-
The CLI command handlers are thin wrappers around these functions: they parse args, call the API, then format the output (JSON or text). This guarantees that `@astryxdesign/cli/api` and `
|
|
297
|
+
The CLI command handlers are thin wrappers around these functions: they parse args, call the API, then format the output (JSON or text). This guarantees that `@astryxdesign/cli/api` and `astryx --json` always return identical data.
|
|
300
298
|
|
|
301
299
|
### Consumer utilities
|
|
302
300
|
|
|
@@ -326,43 +324,41 @@ detail.data.name; // already narrowed
|
|
|
326
324
|
|
|
327
325
|
Every response has a `type` string that uniquely identifies it:
|
|
328
326
|
|
|
329
|
-
| Command
|
|
330
|
-
|
|
|
331
|
-
| `
|
|
332
|
-
| `
|
|
333
|
-
| `
|
|
334
|
-
| `
|
|
335
|
-
| `
|
|
336
|
-
| `
|
|
337
|
-
| `
|
|
338
|
-
| `
|
|
339
|
-
| `
|
|
340
|
-
| `
|
|
341
|
-
| `
|
|
342
|
-
| `
|
|
343
|
-
| `
|
|
344
|
-
| `
|
|
345
|
-
| `
|
|
346
|
-
| `
|
|
347
|
-
| `
|
|
348
|
-
| `
|
|
349
|
-
| `
|
|
350
|
-
| `
|
|
351
|
-
| `
|
|
352
|
-
| `
|
|
353
|
-
| `
|
|
354
|
-
| `
|
|
355
|
-
| `
|
|
356
|
-
| `
|
|
357
|
-
| `
|
|
358
|
-
| `
|
|
359
|
-
| `
|
|
360
|
-
| `
|
|
361
|
-
| `
|
|
362
|
-
|
|
|
363
|
-
|
|
|
364
|
-
| any error | — | `CLIError` |
|
|
365
|
-
| unsupported command | — | `CLIUnsupportedError` |
|
|
327
|
+
| Command | Type | Response |
|
|
328
|
+
| ------------------------------------------------- | --------------------------- | --------------------------------- |
|
|
329
|
+
| `astryx --json component [--list]` | `component.list` | `ComponentListResponse` |
|
|
330
|
+
| `astryx --json component --list --detail compact` | `component.brief` | `ComponentBriefResponse` |
|
|
331
|
+
| `astryx --json component --list --detail full` | `component.full` | `ComponentFullResponse` |
|
|
332
|
+
| `astryx --json component <name>` | `component.detail` | `ComponentDetailResponse` |
|
|
333
|
+
| `astryx --json component <name> --props` | `component.detail.props` | `ComponentDetailPropsResponse` |
|
|
334
|
+
| `astryx --json component <name> --source` | `component.detail.source` | `ComponentDetailSourceResponse` |
|
|
335
|
+
| `astryx --json component <name> --showcase` | `component.detail.showcase` | `ComponentDetailShowcaseResponse` |
|
|
336
|
+
| `astryx --json component <name> --blocks` | `component.detail.blocks` | `ComponentDetailBlocksResponse` |
|
|
337
|
+
| `astryx --json discover` | `discover.list` | `DiscoverListResponse` |
|
|
338
|
+
| `astryx --json discover @scope/name` | `discover.detail` | `DiscoverDetailResponse` |
|
|
339
|
+
| `astryx --json discover @scope/name/Comp` | `discover.detail.doc` | `DiscoverDetailDocResponse` |
|
|
340
|
+
| `astryx --json discover <search>` | `discover.search` | `DiscoverSearchResponse` |
|
|
341
|
+
| `astryx --json docs` | `docs.list` | `DocsListResponse` |
|
|
342
|
+
| `astryx --json docs <topic>` | `docs.detail` | `DocsDetailResponse` |
|
|
343
|
+
| `astryx --json docs <topic> <section>` | `docs.detail.section` | `DocsDetailSectionResponse` |
|
|
344
|
+
| `astryx --json template [--list]` | `template.list` | `TemplateListResponse` |
|
|
345
|
+
| `astryx --json template <name>` | `template.show` | `TemplateShowResponse` |
|
|
346
|
+
| `astryx --json template <name> --skeleton` | `template.skeleton` | `TemplateSkeletonResponse` |
|
|
347
|
+
| `astryx --json template <name> [path]` | `template.copy` | `TemplateCopyResponse` |
|
|
348
|
+
| `astryx --json hook [--list]` | `hook.list` | `HookListResponse` |
|
|
349
|
+
| `astryx --json hook --list --detail compact` | `hook.brief` | `HookBriefResponse` |
|
|
350
|
+
| `astryx --json hook --list --detail full` | `hook.full` | `HookFullResponse` |
|
|
351
|
+
| `astryx --json hook <name>` | `hook.detail` | `HookDetailResponse` |
|
|
352
|
+
| `astryx --json hook <name> --params` | `hook.detail.params` | `HookDetailParamsResponse` |
|
|
353
|
+
| `astryx --json search <query>` | `search` | `SearchResponse` |
|
|
354
|
+
| `astryx --json swizzle [--list]` | `swizzle.list` | `SwizzleListResponse` |
|
|
355
|
+
| `astryx --json swizzle <component>` | `swizzle.copy` | `SwizzleCopyResponse` |
|
|
356
|
+
| `astryx --json theme build <file>` | `theme.build` | `ThemeBuildResponse` |
|
|
357
|
+
| `astryx --json upgrade --list` | `upgrade.list` | `UpgradeListResponse` |
|
|
358
|
+
| `astryx --json upgrade [--apply]` | `upgrade.run` | `UpgradeRunResponse` |
|
|
359
|
+
| `astryx --json doctor` | `doctor` | `DoctorResponse` |
|
|
360
|
+
| any error | — | `CLIError` |
|
|
361
|
+
| unsupported command | — | `CLIUnsupportedError` |
|
|
366
362
|
|
|
367
363
|
## Doctor
|
|
368
364
|
|
|
@@ -383,7 +379,7 @@ astryx doctor — diagnosing your setup
|
|
|
383
379
|
@astryxdesign/core v0.0.14 is in step with @astryxdesign/cli v0.0.14.
|
|
384
380
|
⚠ Theme packages
|
|
385
381
|
No @astryxdesign/theme-* packages are installed.
|
|
386
|
-
→ fix: Install a theme, e.g. `npm install @astryxdesign/theme-neutral`, then import its CSS or set
|
|
382
|
+
→ fix: Install a theme, e.g. `npm install @astryxdesign/theme-neutral`, then import its CSS or set astryx.theme.
|
|
387
383
|
ℹ astryx.config.mjs
|
|
388
384
|
No astryx.config.mjs found — using defaults.
|
|
389
385
|
ℹ AI agent docs
|
|
@@ -408,7 +404,7 @@ No failures — but review the ⚠ warnings above when you can.
|
|
|
408
404
|
| Version alignment | pass / warn / info | Installed `@astryxdesign/core` is in step with `@astryxdesign/cli` |
|
|
409
405
|
| Theme packages | pass / warn | An `@astryxdesign/theme-*` package is installed and a theme is wired |
|
|
410
406
|
| astryx.config.mjs | pass / fail / info | Config (if present) loads cleanly with a valid shape |
|
|
411
|
-
| AI agent docs | pass / warn / info | Agent docs exist and contain the
|
|
407
|
+
| AI agent docs | pass / warn / info | Agent docs exist and contain the Astryx section markers |
|
|
412
408
|
| Peer dependencies | pass / warn / info | `@astryxdesign/core`'s peer deps (react, …) are installed |
|
|
413
409
|
| Package manager | info | Reports the detected package manager |
|
|
414
410
|
|
|
@@ -434,8 +430,6 @@ export default {
|
|
|
434
430
|
templates: {
|
|
435
431
|
get: async id => fetchTemplateFromAPI(id),
|
|
436
432
|
},
|
|
437
|
-
|
|
438
|
-
url: 'https://your-api.com/gaps',
|
|
439
|
-
},
|
|
433
|
+
issuesUrl: 'https://github.com/your-org/your-repo/issues',
|
|
440
434
|
};
|
|
441
435
|
```
|
package/docs/theme.doc.mjs
CHANGED
|
@@ -53,7 +53,7 @@ function App() {
|
|
|
53
53
|
},
|
|
54
54
|
{
|
|
55
55
|
type: 'prose',
|
|
56
|
-
text: 'Each theme ships as its own npm package. Install the one you want, then wrap your app in `<Theme
|
|
56
|
+
text: 'Each theme ships as its own npm package. Install the one you want, then wrap your app in `<Theme>`. The same pattern works for every theme; just swap the package and import name.',
|
|
57
57
|
},
|
|
58
58
|
{
|
|
59
59
|
type: 'prose',
|
|
@@ -261,7 +261,7 @@ const brandTheme = defineTheme({
|
|
|
261
261
|
content: [
|
|
262
262
|
{
|
|
263
263
|
type: 'prose',
|
|
264
|
-
text: 'The `components` field in defineTheme uses semantic component keys and style keys
|
|
264
|
+
text: 'The `components` field in defineTheme uses semantic component keys and style keys, not raw CSS selectors. Use `base` for all instances, `variant:value` or `stateName` for specific props/states, and let the theme pipeline choose the underlying selector. For raw external CSS escape hatches, prefer the data-attribute selector surface documented in `astryx docs styling`.',
|
|
265
265
|
},
|
|
266
266
|
{
|
|
267
267
|
type: 'code',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astryxdesign/cli",
|
|
3
|
-
"version": "0.1.2-canary.
|
|
3
|
+
"version": "0.1.2-canary.bbf77ea",
|
|
4
4
|
"displayName": "CLI",
|
|
5
5
|
"description": "Scaffold projects, browse templates, generate themes, and get agent-ready docs from the command line.",
|
|
6
6
|
"author": "Meta Open Source",
|
|
@@ -43,6 +43,18 @@
|
|
|
43
43
|
"types": "./src/types/config.d.ts",
|
|
44
44
|
"import": "./src/config.mjs"
|
|
45
45
|
},
|
|
46
|
+
"./integration": {
|
|
47
|
+
"types": "./src/types/integration.d.ts",
|
|
48
|
+
"import": "./src/integration.mjs"
|
|
49
|
+
},
|
|
50
|
+
"./template": {
|
|
51
|
+
"types": "./src/types/template-api.d.ts",
|
|
52
|
+
"import": "./src/template.mjs"
|
|
53
|
+
},
|
|
54
|
+
"./codemod": {
|
|
55
|
+
"types": "./src/types/codemod.d.ts",
|
|
56
|
+
"import": "./src/codemod.mjs"
|
|
57
|
+
},
|
|
46
58
|
"./xle": {
|
|
47
59
|
"types": "./src/lib/xle/browser.d.ts",
|
|
48
60
|
"import": "./src/lib/xle/browser.mjs"
|
|
@@ -63,9 +75,9 @@
|
|
|
63
75
|
"zod": "^4.4.3"
|
|
64
76
|
},
|
|
65
77
|
"peerDependencies": {
|
|
66
|
-
"@astryxdesign/core": "0.1.2-canary.
|
|
67
|
-
"@astryxdesign/lab": "0.1.2-canary.
|
|
68
|
-
"@astryxdesign/theme-neutral": "0.1.2-canary.
|
|
78
|
+
"@astryxdesign/core": "0.1.2-canary.bbf77ea",
|
|
79
|
+
"@astryxdesign/lab": "0.1.2-canary.bbf77ea",
|
|
80
|
+
"@astryxdesign/theme-neutral": "0.1.2-canary.bbf77ea",
|
|
69
81
|
"gpt-tokenizer": "^2.0.0"
|
|
70
82
|
},
|
|
71
83
|
"peerDependenciesMeta": {
|
|
@@ -80,9 +92,9 @@
|
|
|
80
92
|
}
|
|
81
93
|
},
|
|
82
94
|
"devDependencies": {
|
|
83
|
-
"@astryxdesign/core": "0.1.2-canary.
|
|
84
|
-
"@astryxdesign/lab": "0.1.2-canary.
|
|
85
|
-
"@astryxdesign/theme-neutral": "0.1.2-canary.
|
|
95
|
+
"@astryxdesign/core": "0.1.2-canary.bbf77ea",
|
|
96
|
+
"@astryxdesign/lab": "0.1.2-canary.bbf77ea",
|
|
97
|
+
"@astryxdesign/theme-neutral": "0.1.2-canary.bbf77ea",
|
|
86
98
|
"gpt-tokenizer": "^2.0.0"
|
|
87
99
|
},
|
|
88
100
|
"scripts": {
|
package/src/api/component.mjs
CHANGED
|
@@ -11,18 +11,48 @@ import * as fs from 'node:fs';
|
|
|
11
11
|
import {ERROR_CODES} from '../lib/error-codes.mjs';
|
|
12
12
|
import {findCoreDir, discoverExternalPackages} from '../utils/paths.mjs';
|
|
13
13
|
import {
|
|
14
|
+
CORE_PACKAGE,
|
|
14
15
|
discoverComponents,
|
|
15
16
|
discoverExternalComponentsGrouped,
|
|
17
|
+
discoverIntegrationComponents,
|
|
16
18
|
findComponentReadme,
|
|
17
19
|
findComponentSource,
|
|
18
20
|
findExternalComponentDoc,
|
|
21
|
+
findIntegrationComponentDoc,
|
|
22
|
+
findIntegrationComponentSource,
|
|
19
23
|
resolveImportPath,
|
|
20
24
|
} from '../lib/component-discovery.mjs';
|
|
25
|
+
import {Project} from '../lib/project.mjs';
|
|
21
26
|
import {loadDocs} from '../lib/component-loader.mjs';
|
|
22
27
|
import {searchComponents} from '../lib/string-utils.mjs';
|
|
23
28
|
import {AstryxError} from './error.mjs';
|
|
24
29
|
import {findShowcase, findRelatedBlocks} from './template.mjs';
|
|
25
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Load the configured integrations for `cwd`, swallowing any config errors so
|
|
33
|
+
* component discovery never hard-fails on a malformed/absent integration. An
|
|
34
|
+
* empty list means "core only".
|
|
35
|
+
* @param {string} cwd
|
|
36
|
+
* @returns {Promise<Array<{name: string, components?: string, issuesUrl?: string}>>}
|
|
37
|
+
*/
|
|
38
|
+
async function loadIntegrationsSafely(cwd) {
|
|
39
|
+
try {
|
|
40
|
+
const project = await Project.load(cwd);
|
|
41
|
+
return project.loadedIntegrations;
|
|
42
|
+
} catch {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Resolve a loaded integration by package name.
|
|
49
|
+
* @param {Array<{name: string}>} loadedIntegrations
|
|
50
|
+
* @param {string} packageName
|
|
51
|
+
*/
|
|
52
|
+
function findLoadedIntegration(loadedIntegrations, packageName) {
|
|
53
|
+
return loadedIntegrations.find(i => i.name === packageName) ?? null;
|
|
54
|
+
}
|
|
55
|
+
|
|
26
56
|
/**
|
|
27
57
|
* Resolve an external package by name from the discovered externals list.
|
|
28
58
|
* @param {string} packageName - e.g. '@acme/xds-widgets'
|
|
@@ -131,8 +161,13 @@ export async function component(name, options = {}) {
|
|
|
131
161
|
return {type: 'component.full', data: {[match[0]]: entries}};
|
|
132
162
|
}
|
|
133
163
|
|
|
134
|
-
// Default: brief —
|
|
135
|
-
|
|
164
|
+
// Default: brief — package-qualified object list for the category.
|
|
165
|
+
// Pre-1.0 JSON contract: members are {name, package} objects, not bare
|
|
166
|
+
// strings, so consumers can disambiguate ownership.
|
|
167
|
+
return {
|
|
168
|
+
type: 'component.list',
|
|
169
|
+
data: {[match[0]]: match[1].map(n => ({name: n, package: CORE_PACKAGE}))},
|
|
170
|
+
};
|
|
136
171
|
}
|
|
137
172
|
|
|
138
173
|
// All components — merge core + external packages with grouped subcategories
|
|
@@ -179,43 +214,189 @@ export async function component(name, options = {}) {
|
|
|
179
214
|
return {type: 'component.full', data: result};
|
|
180
215
|
}
|
|
181
216
|
|
|
182
|
-
// Default: brief —
|
|
217
|
+
// Default: brief — package-qualified object list (core + integrations).
|
|
218
|
+
// Pre-1.0 JSON contract: each group's members are {name, package} objects.
|
|
219
|
+
/** @type {Record<string, Array<{name: string, package: string}>>} */
|
|
220
|
+
const listData = {};
|
|
221
|
+
for (const [cat, comps] of Object.entries(components)) {
|
|
222
|
+
listData[cat] = comps.map(n => ({name: n, package: CORE_PACKAGE}));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Integration components (authoritative source: loadedIntegrations).
|
|
226
|
+
const loadedIntegrations = await loadIntegrationsSafely(cwd);
|
|
227
|
+
const seenIntegration = new Set();
|
|
228
|
+
for (const integration of loadedIntegrations) {
|
|
229
|
+
seenIntegration.add(integration.name);
|
|
230
|
+
const owned = discoverIntegrationComponents(integration);
|
|
231
|
+
// Group integration components by their doc `group`, falling back to the
|
|
232
|
+
// package name. Keys are package-qualified so they never collide with
|
|
233
|
+
// core groups or each other.
|
|
234
|
+
/** @type {Map<string, Array<{name: string, package: string}>>} */
|
|
235
|
+
const byGroup = new Map();
|
|
236
|
+
for (const rec of owned) {
|
|
237
|
+
const groupLabel = rec.group ?? integration.name;
|
|
238
|
+
const key = `${groupLabel} (${integration.name})`;
|
|
239
|
+
if (!byGroup.has(key)) byGroup.set(key, []);
|
|
240
|
+
byGroup.get(key).push({name: rec.name, package: integration.name});
|
|
241
|
+
}
|
|
242
|
+
for (const [key, members] of byGroup) {
|
|
243
|
+
members.sort((a, b) => a.name.localeCompare(b.name));
|
|
244
|
+
listData[key] = members;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Back-compat: node_modules-scanned external packages (pkg.astryx.docs)
|
|
249
|
+
// that are NOT configured integrations. Preserves existing discovery for
|
|
250
|
+
// consumers that haven't adopted the config-integration flow.
|
|
183
251
|
const externals = discoverExternalPackages(cwd);
|
|
184
252
|
for (const ext of externals) {
|
|
253
|
+
if (seenIntegration.has(ext.name)) continue;
|
|
185
254
|
const grouped = discoverExternalComponentsGrouped(ext.docsDir);
|
|
186
255
|
const groupKeys = Object.keys(grouped);
|
|
187
256
|
if (groupKeys.length === 0) continue;
|
|
188
257
|
|
|
189
|
-
// If the package has subcategories (groups), emit each as a separate key.
|
|
190
|
-
// If no groups exist, fall back to the flat list under one key.
|
|
191
258
|
const hasGroups = groupKeys.some(
|
|
192
259
|
k => grouped[k].length > 1 || grouped[k][0] !== k,
|
|
193
260
|
);
|
|
194
261
|
|
|
195
262
|
if (hasGroups) {
|
|
196
263
|
for (const [group, members] of Object.entries(grouped)) {
|
|
197
|
-
|
|
264
|
+
listData[`${group} (${ext.name})`] = members.map(n => ({
|
|
265
|
+
name: n,
|
|
266
|
+
package: ext.name,
|
|
267
|
+
}));
|
|
198
268
|
}
|
|
199
269
|
} else {
|
|
200
|
-
// All ungrouped — single flat list under the package category
|
|
201
270
|
const allComps = Object.values(grouped).flat().sort();
|
|
202
271
|
if (allComps.length > 0) {
|
|
203
|
-
|
|
272
|
+
listData[`${ext.category} (${ext.name})`] = allComps.map(n => ({
|
|
273
|
+
name: n,
|
|
274
|
+
package: ext.name,
|
|
275
|
+
}));
|
|
204
276
|
}
|
|
205
277
|
}
|
|
206
278
|
}
|
|
207
|
-
return {type: 'component.list', data:
|
|
279
|
+
return {type: 'component.list', data: listData};
|
|
208
280
|
}
|
|
209
281
|
|
|
210
282
|
// ── Single component ───────────────────────────────────────────
|
|
211
283
|
|
|
284
|
+
if (typeof name !== 'string') {
|
|
285
|
+
throw new AstryxError(
|
|
286
|
+
`No component named "${String(name)}"`,
|
|
287
|
+
undefined,
|
|
288
|
+
ERROR_CODES.ERR_UNKNOWN_COMPONENT,
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
212
292
|
const dirName = name.replace(/^XDS/, '');
|
|
213
293
|
|
|
294
|
+
// Ownership-aware resolution. Build the set of OWNER packages that provide a
|
|
295
|
+
// component with this name across core + every loaded integration. This is
|
|
296
|
+
// what lets the CLI disambiguate by package and expose the owner's source +
|
|
297
|
+
// issuesUrl (the inputs the future integration-component swizzle needs).
|
|
298
|
+
const loadedIntegrations = await loadIntegrationsSafely(cwd);
|
|
299
|
+
const coreDocPath = findComponentReadme(coreDir, dirName);
|
|
300
|
+
/**
|
|
301
|
+
* @type {Array<{
|
|
302
|
+
* package: string,
|
|
303
|
+
* docPath: string,
|
|
304
|
+
* sourcePath: string|null,
|
|
305
|
+
* issuesUrl: string|undefined,
|
|
306
|
+
* integration: object|null,
|
|
307
|
+
* }>}
|
|
308
|
+
*/
|
|
309
|
+
const owners = [];
|
|
310
|
+
if (coreDocPath) {
|
|
311
|
+
owners.push({
|
|
312
|
+
package: CORE_PACKAGE,
|
|
313
|
+
docPath: coreDocPath,
|
|
314
|
+
sourcePath: findComponentSource(coreDir, dirName),
|
|
315
|
+
issuesUrl: undefined,
|
|
316
|
+
integration: null,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
for (const integration of loadedIntegrations) {
|
|
320
|
+
const docPath = findIntegrationComponentDoc(integration, dirName);
|
|
321
|
+
if (!docPath) continue;
|
|
322
|
+
owners.push({
|
|
323
|
+
package: integration.name,
|
|
324
|
+
docPath,
|
|
325
|
+
sourcePath: findIntegrationComponentSource(integration, dirName),
|
|
326
|
+
issuesUrl: integration.issuesUrl,
|
|
327
|
+
integration,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Augment a loaded `component.detail` doc with ownership metadata. Adds
|
|
333
|
+
* `package`, the resolved `import` specifier, and `sourceAvailable` (whether
|
|
334
|
+
* a swizzleable source file exists for the owner). Existing doc fields
|
|
335
|
+
* (name, usage, props, …) are preserved.
|
|
336
|
+
* @param {object} docs
|
|
337
|
+
* @param {{package: string, sourcePath: string|null}} owner
|
|
338
|
+
* @param {string} componentName
|
|
339
|
+
*/
|
|
340
|
+
function withOwnership(docs, owner, componentName) {
|
|
341
|
+
const importSpec =
|
|
342
|
+
owner.package === CORE_PACKAGE
|
|
343
|
+
? resolveImportPath(coreDir, componentName)
|
|
344
|
+
: `${owner.package}/${componentName}`;
|
|
345
|
+
return {
|
|
346
|
+
...docs,
|
|
347
|
+
package: owner.package,
|
|
348
|
+
import: importSpec,
|
|
349
|
+
sourceAvailable: owner.sourcePath != null,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
214
353
|
// When scoped to a specific package, search that package first.
|
|
215
354
|
// This is critical for components that exist in both core and an external
|
|
216
355
|
// package (e.g. AppShell, Button, SideNav) — the package scope ensures
|
|
217
356
|
// the external package's docs are returned, not core's.
|
|
218
357
|
if (packageScope) {
|
|
358
|
+
// Core scope: resolve from core directly.
|
|
359
|
+
if (packageScope === CORE_PACKAGE) {
|
|
360
|
+
const owner = owners.find(o => o.package === CORE_PACKAGE);
|
|
361
|
+
if (!owner) {
|
|
362
|
+
throw new AstryxError(`No component "${name}" in package "${packageScope}"`, undefined, ERROR_CODES.ERR_UNKNOWN_COMPONENT);
|
|
363
|
+
}
|
|
364
|
+
if (source) {
|
|
365
|
+
if (!owner.sourcePath) {
|
|
366
|
+
throw new AstryxError(`Source for "${name}" not found`, undefined, ERROR_CODES.ERR_NO_SOURCE);
|
|
367
|
+
}
|
|
368
|
+
return {type: 'component.detail.source', data: {component: dirName, source: fs.readFileSync(owner.sourcePath, 'utf-8')}};
|
|
369
|
+
}
|
|
370
|
+
const docs = await loadDocs(owner.docPath, {zh, dense, lang});
|
|
371
|
+
if (props) {
|
|
372
|
+
const p = docs.props || (docs.components ? docs.components.flatMap(c => c.props || []) : []);
|
|
373
|
+
return {type: 'component.detail.props', data: p};
|
|
374
|
+
}
|
|
375
|
+
return {type: 'component.detail', data: withOwnership(docs, owner, dirName)};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Integration scope (authoritative): resolve from the loaded integration.
|
|
379
|
+
const integration = findLoadedIntegration(loadedIntegrations, packageScope);
|
|
380
|
+
if (integration) {
|
|
381
|
+
const owner = owners.find(o => o.package === packageScope);
|
|
382
|
+
if (!owner) {
|
|
383
|
+
throw new AstryxError(`No component "${name}" in package "${packageScope}"`, undefined, ERROR_CODES.ERR_UNKNOWN_COMPONENT);
|
|
384
|
+
}
|
|
385
|
+
if (source) {
|
|
386
|
+
if (!owner.sourcePath) {
|
|
387
|
+
throw new AstryxError(`Source for "${name}" not found in package "${packageScope}"`, undefined, ERROR_CODES.ERR_NO_SOURCE);
|
|
388
|
+
}
|
|
389
|
+
return {type: 'component.detail.source', data: {component: dirName, source: fs.readFileSync(owner.sourcePath, 'utf-8')}};
|
|
390
|
+
}
|
|
391
|
+
const docs = await loadDocs(owner.docPath, {zh, dense, lang});
|
|
392
|
+
if (props) {
|
|
393
|
+
const p = docs.props || (docs.components ? docs.components.flatMap(c => c.props || []) : []);
|
|
394
|
+
return {type: 'component.detail.props', data: p};
|
|
395
|
+
}
|
|
396
|
+
return {type: 'component.detail', data: withOwnership(docs, owner, dirName)};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Legacy fallback: node_modules `pkg.astryx.docs` external package.
|
|
219
400
|
const ext = resolveExternalPackage(packageScope, cwd);
|
|
220
401
|
if (!ext) {
|
|
221
402
|
throw new AstryxError(`External package "${packageScope}" not found`, undefined, ERROR_CODES.ERR_UNKNOWN_PACKAGE);
|
|
@@ -244,11 +425,49 @@ export async function component(name, options = {}) {
|
|
|
244
425
|
const p = docs.props || (docs.components ? docs.components.flatMap(c => c.props || []) : []);
|
|
245
426
|
return {type: 'component.detail.props', data: p};
|
|
246
427
|
}
|
|
247
|
-
return {
|
|
428
|
+
return {
|
|
429
|
+
type: 'component.detail',
|
|
430
|
+
data: withOwnership(docs, {package: ext.name, sourcePath: null}, dirName),
|
|
431
|
+
};
|
|
248
432
|
}
|
|
249
433
|
throw new AstryxError(`No component "${name}" in package "${packageScope}"`, undefined, ERROR_CODES.ERR_UNKNOWN_COMPONENT);
|
|
250
434
|
}
|
|
251
435
|
|
|
436
|
+
// Ambiguity: when the name is owned by MORE THAN ONE package (core and/or
|
|
437
|
+
// integrations) and the caller did not scope with --package, refuse to guess.
|
|
438
|
+
// NOTE: legacy `pkg.astryx.docs` externals are intentionally NOT part of this
|
|
439
|
+
// ambiguity set — they retain their historical core-first fallback below so
|
|
440
|
+
// existing consumers (and tests) keep working. Only config-driven integration
|
|
441
|
+
// ownership participates here.
|
|
442
|
+
if (owners.length > 1) {
|
|
443
|
+
throw new AstryxError(
|
|
444
|
+
`Component "${dirName}" is provided by multiple packages. Re-run with --package <pkg> to choose one.`,
|
|
445
|
+
owners.map(o => ({name: o.package, reason: 'provides this component'})),
|
|
446
|
+
ERROR_CODES.ERR_UNKNOWN_COMPONENT,
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Single non-core owner (an integration provides it, core does not) — resolve
|
|
451
|
+
// from that integration so the integration component is authoritative.
|
|
452
|
+
if (owners.length === 1 && owners[0].package !== CORE_PACKAGE) {
|
|
453
|
+
const owner = owners[0];
|
|
454
|
+
if (source) {
|
|
455
|
+
if (!owner.sourcePath) {
|
|
456
|
+
throw new AstryxError(`Source for "${name}" not found`, undefined, ERROR_CODES.ERR_NO_SOURCE);
|
|
457
|
+
}
|
|
458
|
+
return {type: 'component.detail.source', data: {component: dirName, source: fs.readFileSync(owner.sourcePath, 'utf-8')}};
|
|
459
|
+
}
|
|
460
|
+
if (showcase) {
|
|
461
|
+
throw new AstryxError(`No showcase found for "${name}"`, undefined, ERROR_CODES.ERR_NO_SHOWCASE);
|
|
462
|
+
}
|
|
463
|
+
const docs = await loadDocs(owner.docPath, {zh, dense, lang});
|
|
464
|
+
if (props) {
|
|
465
|
+
const p = docs.props || (docs.components ? docs.components.flatMap(c => c.props || []) : []);
|
|
466
|
+
return {type: 'component.detail.props', data: p};
|
|
467
|
+
}
|
|
468
|
+
return {type: 'component.detail', data: withOwnership(docs, owner, dirName)};
|
|
469
|
+
}
|
|
470
|
+
|
|
252
471
|
if (source) {
|
|
253
472
|
const sourcePath = findComponentSource(coreDir, dirName);
|
|
254
473
|
if (!sourcePath) {
|
|
@@ -274,6 +493,10 @@ export async function component(name, options = {}) {
|
|
|
274
493
|
|
|
275
494
|
let readmePath = findComponentReadme(coreDir, dirName);
|
|
276
495
|
let resolvedName = dirName;
|
|
496
|
+
// Track the resolving owner so the detail payload can carry ownership info.
|
|
497
|
+
// Defaults to core; the legacy-external fallback below may reassign it.
|
|
498
|
+
let resolvedOwnerPackage = CORE_PACKAGE;
|
|
499
|
+
let resolvedSourcePath = readmePath ? findComponentSource(coreDir, dirName) : null;
|
|
277
500
|
|
|
278
501
|
if (!readmePath) {
|
|
279
502
|
const externals = discoverExternalPackages(cwd);
|
|
@@ -281,6 +504,8 @@ export async function component(name, options = {}) {
|
|
|
281
504
|
const extDocPath = findExternalComponentDoc(ext.docsDir, dirName);
|
|
282
505
|
if (extDocPath) {
|
|
283
506
|
readmePath = extDocPath;
|
|
507
|
+
resolvedOwnerPackage = ext.name;
|
|
508
|
+
resolvedSourcePath = null;
|
|
284
509
|
break;
|
|
285
510
|
}
|
|
286
511
|
}
|
|
@@ -299,6 +524,8 @@ export async function component(name, options = {}) {
|
|
|
299
524
|
if (topScore >= 90 && topTied.length === 1 && gap >= 20) {
|
|
300
525
|
resolvedName = topTied[0].name;
|
|
301
526
|
readmePath = findComponentReadme(coreDir, resolvedName);
|
|
527
|
+
resolvedOwnerPackage = CORE_PACKAGE;
|
|
528
|
+
resolvedSourcePath = findComponentSource(coreDir, resolvedName);
|
|
302
529
|
} else {
|
|
303
530
|
const threshold = Math.max(topScore - 20, 1);
|
|
304
531
|
const candidates = results.filter(r => r.score >= threshold).slice(0, 5);
|
|
@@ -382,7 +609,14 @@ export async function component(name, options = {}) {
|
|
|
382
609
|
if (props) {
|
|
383
610
|
return {type: 'component.detail.props', data: matchingComponent.props || []};
|
|
384
611
|
}
|
|
385
|
-
return {
|
|
612
|
+
return {
|
|
613
|
+
type: 'component.detail',
|
|
614
|
+
data: withOwnership(
|
|
615
|
+
scoped,
|
|
616
|
+
{package: resolvedOwnerPackage, sourcePath: resolvedSourcePath},
|
|
617
|
+
dirName,
|
|
618
|
+
),
|
|
619
|
+
};
|
|
386
620
|
}
|
|
387
621
|
|
|
388
622
|
if (props) {
|
|
@@ -390,5 +624,12 @@ export async function component(name, options = {}) {
|
|
|
390
624
|
return {type: 'component.detail.props', data: p};
|
|
391
625
|
}
|
|
392
626
|
|
|
393
|
-
return {
|
|
627
|
+
return {
|
|
628
|
+
type: 'component.detail',
|
|
629
|
+
data: withOwnership(
|
|
630
|
+
docs,
|
|
631
|
+
{package: resolvedOwnerPackage, sourcePath: resolvedSourcePath},
|
|
632
|
+
resolvedName,
|
|
633
|
+
),
|
|
634
|
+
};
|
|
394
635
|
}
|