@astryxdesign/cli 0.1.2-canary.ac17c7f → 0.1.2-canary.b6820d0
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/README.md +7 -1
- package/package.json +7 -19
- package/src/api/component.mjs +12 -253
- package/src/api/discover.mjs +7 -14
- package/src/api/doctor.mjs +22 -7
- package/src/api/layout.mjs +10 -11
- package/src/api/layout.test.mjs +1 -4
- package/src/api/template.mjs +84 -242
- package/src/codemods/__tests__/registry.test.mjs +0 -1
- package/src/codemods/registry.mjs +0 -1
- package/src/codemods/runner.mjs +96 -73
- package/src/codemods/transforms/{v0.1.0 → v0.0.15}/drop-xds-prefix-imports.mjs +4 -15
- package/src/codemods/transforms/v0.0.15/index.mjs +13 -0
- package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-config-surfaces.test.mjs +116 -0
- package/src/codemods/transforms/v0.1.0/index.mjs +6 -15
- package/src/codemods/transforms/v0.1.0/migrate-xds-config-surfaces.mjs +230 -0
- package/src/commands/agent-docs.mjs +1 -1
- package/src/commands/component/index.mjs +10 -44
- package/src/commands/discover.mjs +16 -5
- package/src/commands/doctor.test.mjs +3 -3
- package/src/commands/gap-report.mjs +464 -0
- package/src/commands/gap-report.test.mjs +168 -0
- package/src/commands/json-contract.test.mjs +32 -0
- package/src/commands/swizzle-gap-safety.test.mjs +273 -0
- package/src/commands/swizzle.mjs +235 -224
- package/src/commands/swizzle.path-safety.test.mjs +3 -3
- package/src/commands/template.mjs +37 -33
- package/src/commands/upgrade.mjs +170 -349
- package/src/config.mjs +20 -7
- package/src/config.test.mjs +13 -89
- package/src/index.mjs +3 -7
- package/src/lib/component-discovery.mjs +0 -177
- package/src/lib/config-schema.mjs +55 -41
- package/src/lib/config.mjs +113 -0
- package/src/lib/config.test.mjs +91 -0
- package/src/lib/error-codes.mjs +12 -7
- package/src/lib/integrations.mjs +115 -92
- package/src/lib/integrations.test.mjs +107 -88
- package/src/lib/manifest.mjs +2 -5
- package/src/types/api.d.ts +6 -2
- package/src/types/base.d.ts +9 -3
- package/src/types/component.d.ts +2 -27
- package/src/types/config.d.ts +85 -56
- package/src/types/error-codes.d.ts +3 -5
- package/src/types/gap-report.d.ts +29 -0
- package/src/types/index.d.ts +1 -0
- package/src/types/swizzle.d.ts +2 -9
- package/src/types/template.d.ts +7 -10
- package/src/types/upgrade.d.ts +0 -29
- package/src/utils/github.mjs +237 -0
- package/src/utils/interactive.mjs +2 -2
- package/templates/blocks/components/HoverCard/HoverCardInteractiveContent.doc.mjs +1 -1
- package/templates/blocks/components/HoverCard/HoverCardInteractiveContent.tsx +4 -9
- package/src/api/template-integration.test.mjs +0 -225
- package/src/api/validate-integration.mjs +0 -370
- package/src/api/validate-integration.test.mjs +0 -222
- package/src/codemod.mjs +0 -93
- package/src/codemod.test.mjs +0 -134
- package/src/codemods/__tests__/runner.test.mjs +0 -103
- package/src/codemods/integration-discovery.mjs +0 -168
- package/src/codemods/integration-discovery.test.mjs +0 -234
- package/src/codemods/integration-runner.mjs +0 -109
- package/src/codemods/run-codemod.mjs +0 -207
- package/src/codemods/transforms/v0.1.0/__tests__/v0.1.0-ordering.test.mjs +0 -81
- package/src/codemods/transforms/v0.1.3/__tests__/migrate-layout-components-to-experimental.test.mjs +0 -360
- package/src/codemods/transforms/v0.1.3/index.mjs +0 -19
- package/src/codemods/transforms/v0.1.3/migrate-layout-components-to-experimental.mjs +0 -260
- package/src/commands/component-ownership.test.mjs +0 -227
- package/src/commands/swizzle.routing.test.mjs +0 -279
- package/src/commands/upgrade.config-ordering.test.mjs +0 -273
- package/src/commands/upgrade.integration-policy.test.mjs +0 -204
- package/src/commands/validate-integration.mjs +0 -110
- package/src/commands/validate-integration.test.mjs +0 -124
- package/src/integration.mjs +0 -19
- package/src/lib/config-cache.mjs +0 -91
- package/src/lib/config-cache.test.mjs +0 -83
- package/src/lib/integration-warnings.mjs +0 -62
- package/src/lib/integration-warnings.test.mjs +0 -102
- package/src/lib/module-loader.mjs +0 -80
- package/src/lib/module-loader.test.mjs +0 -106
- package/src/lib/project.mjs +0 -502
- package/src/lib/project.test.mjs +0 -308
- package/src/template.mjs +0 -73
- package/src/template.test.mjs +0 -127
- package/src/types/codemod.d.ts +0 -81
- package/src/types/integration.d.ts +0 -29
- package/src/types/template-api.d.ts +0 -54
- package/src/types/validate-integration.d.ts +0 -24
- package/templates/blocks/components/DateRangeInput/DateRangeInputWithPresets.doc.mjs +0 -14
- package/templates/blocks/components/DateRangeInput/DateRangeInputWithPresets.tsx +0 -46
- package/templates/blocks/components/DateRangeInput/DateRangeInputWithValidation.doc.mjs +0 -14
- package/templates/blocks/components/DateRangeInput/DateRangeInputWithValidation.tsx +0 -52
- package/templates/blocks/components/DateTimeInput/DateTimeInputWithValidation.doc.mjs +0 -14
- package/templates/blocks/components/DateTimeInput/DateTimeInputWithValidation.tsx +0 -43
- package/templates/blocks/components/Tab/TabWithSelectedIcon.doc.mjs +0 -13
- package/templates/blocks/components/Tab/TabWithSelectedIcon.tsx +0 -39
- /package/src/codemods/transforms/{v0.1.0 → v0.0.15}/__tests__/drop-xds-prefix-imports.test.mjs +0 -0
package/README.md
CHANGED
|
@@ -62,6 +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
|
+
| `gap-report` | Report a gap when a component doesn't meet your needs |
|
|
65
66
|
| `doctor` | Diagnose your XDS setup and report problems with fixes (CI-friendly via exit code) |
|
|
66
67
|
|
|
67
68
|
### Global options
|
|
@@ -160,6 +161,7 @@ if (isError(result)) {
|
|
|
160
161
|
| `ERR_INVALID_VERSION` | A `--from`/`--to` value was not a valid semver string. |
|
|
161
162
|
| `ERR_DEP_MISSING` | A required external dependency (e.g. jscodeshift) is missing. |
|
|
162
163
|
| `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). |
|
|
163
165
|
|
|
164
166
|
## Capability manifest (agent discovery)
|
|
165
167
|
|
|
@@ -356,6 +358,8 @@ Every response has a `type` string that uniquely identifies it:
|
|
|
356
358
|
| `xds --json theme build <file>` | `theme.build` | `ThemeBuildResponse` |
|
|
357
359
|
| `xds --json upgrade --list` | `upgrade.list` | `UpgradeListResponse` |
|
|
358
360
|
| `xds --json upgrade [--apply]` | `upgrade.run` | `UpgradeRunResponse` |
|
|
361
|
+
| `xds --json gap-report --list-categories` | `gap-report.categories` | `GapReportCategoriesResponse` |
|
|
362
|
+
| `xds --json gap-report --component X ...` | `gap-report.file` | `GapReportFileResponse` |
|
|
359
363
|
| `xds --json doctor` | `doctor` | `DoctorResponse` |
|
|
360
364
|
| any error | — | `CLIError` |
|
|
361
365
|
| unsupported command | — | `CLIUnsupportedError` |
|
|
@@ -430,6 +434,8 @@ export default {
|
|
|
430
434
|
templates: {
|
|
431
435
|
get: async id => fetchTemplateFromAPI(id),
|
|
432
436
|
},
|
|
433
|
-
|
|
437
|
+
gapReport: {
|
|
438
|
+
url: 'https://your-api.com/gaps',
|
|
439
|
+
},
|
|
434
440
|
};
|
|
435
441
|
```
|
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.b6820d0",
|
|
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,18 +43,6 @@
|
|
|
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
|
-
},
|
|
58
46
|
"./xle": {
|
|
59
47
|
"types": "./src/lib/xle/browser.d.ts",
|
|
60
48
|
"import": "./src/lib/xle/browser.mjs"
|
|
@@ -75,9 +63,9 @@
|
|
|
75
63
|
"zod": "^4.4.3"
|
|
76
64
|
},
|
|
77
65
|
"peerDependencies": {
|
|
78
|
-
"@astryxdesign/core": "0.1.2-canary.
|
|
79
|
-
"@astryxdesign/lab": "0.1.2-canary.
|
|
80
|
-
"@astryxdesign/theme-neutral": "0.1.2-canary.
|
|
66
|
+
"@astryxdesign/core": "0.1.2-canary.b6820d0",
|
|
67
|
+
"@astryxdesign/lab": "0.1.2-canary.b6820d0",
|
|
68
|
+
"@astryxdesign/theme-neutral": "0.1.2-canary.b6820d0",
|
|
81
69
|
"gpt-tokenizer": "^2.0.0"
|
|
82
70
|
},
|
|
83
71
|
"peerDependenciesMeta": {
|
|
@@ -92,9 +80,9 @@
|
|
|
92
80
|
}
|
|
93
81
|
},
|
|
94
82
|
"devDependencies": {
|
|
95
|
-
"@astryxdesign/core": "0.1.2-canary.
|
|
96
|
-
"@astryxdesign/lab": "0.1.2-canary.
|
|
97
|
-
"@astryxdesign/theme-neutral": "0.1.2-canary.
|
|
83
|
+
"@astryxdesign/core": "0.1.2-canary.b6820d0",
|
|
84
|
+
"@astryxdesign/lab": "0.1.2-canary.b6820d0",
|
|
85
|
+
"@astryxdesign/theme-neutral": "0.1.2-canary.b6820d0",
|
|
98
86
|
"gpt-tokenizer": "^2.0.0"
|
|
99
87
|
},
|
|
100
88
|
"scripts": {
|
package/src/api/component.mjs
CHANGED
|
@@ -11,48 +11,18 @@ 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,
|
|
15
14
|
discoverComponents,
|
|
16
15
|
discoverExternalComponentsGrouped,
|
|
17
|
-
discoverIntegrationComponents,
|
|
18
16
|
findComponentReadme,
|
|
19
17
|
findComponentSource,
|
|
20
18
|
findExternalComponentDoc,
|
|
21
|
-
findIntegrationComponentDoc,
|
|
22
|
-
findIntegrationComponentSource,
|
|
23
19
|
resolveImportPath,
|
|
24
20
|
} from '../lib/component-discovery.mjs';
|
|
25
|
-
import {Project} from '../lib/project.mjs';
|
|
26
21
|
import {loadDocs} from '../lib/component-loader.mjs';
|
|
27
22
|
import {searchComponents} from '../lib/string-utils.mjs';
|
|
28
23
|
import {AstryxError} from './error.mjs';
|
|
29
24
|
import {findShowcase, findRelatedBlocks} from './template.mjs';
|
|
30
25
|
|
|
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
|
-
|
|
56
26
|
/**
|
|
57
27
|
* Resolve an external package by name from the discovered externals list.
|
|
58
28
|
* @param {string} packageName - e.g. '@acme/xds-widgets'
|
|
@@ -161,13 +131,8 @@ export async function component(name, options = {}) {
|
|
|
161
131
|
return {type: 'component.full', data: {[match[0]]: entries}};
|
|
162
132
|
}
|
|
163
133
|
|
|
164
|
-
// Default: brief —
|
|
165
|
-
|
|
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
|
-
};
|
|
134
|
+
// Default: brief — names only
|
|
135
|
+
return {type: 'component.list', data: {[match[0]]: match[1]}};
|
|
171
136
|
}
|
|
172
137
|
|
|
173
138
|
// All components — merge core + external packages with grouped subcategories
|
|
@@ -214,189 +179,43 @@ export async function component(name, options = {}) {
|
|
|
214
179
|
return {type: 'component.full', data: result};
|
|
215
180
|
}
|
|
216
181
|
|
|
217
|
-
// Default: brief —
|
|
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.
|
|
182
|
+
// Default: brief — names only (with externals merged in)
|
|
251
183
|
const externals = discoverExternalPackages(cwd);
|
|
252
184
|
for (const ext of externals) {
|
|
253
|
-
if (seenIntegration.has(ext.name)) continue;
|
|
254
185
|
const grouped = discoverExternalComponentsGrouped(ext.docsDir);
|
|
255
186
|
const groupKeys = Object.keys(grouped);
|
|
256
187
|
if (groupKeys.length === 0) continue;
|
|
257
188
|
|
|
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.
|
|
258
191
|
const hasGroups = groupKeys.some(
|
|
259
192
|
k => grouped[k].length > 1 || grouped[k][0] !== k,
|
|
260
193
|
);
|
|
261
194
|
|
|
262
195
|
if (hasGroups) {
|
|
263
196
|
for (const [group, members] of Object.entries(grouped)) {
|
|
264
|
-
|
|
265
|
-
name: n,
|
|
266
|
-
package: ext.name,
|
|
267
|
-
}));
|
|
197
|
+
components[`${group} (${ext.name})`] = members;
|
|
268
198
|
}
|
|
269
199
|
} else {
|
|
200
|
+
// All ungrouped — single flat list under the package category
|
|
270
201
|
const allComps = Object.values(grouped).flat().sort();
|
|
271
202
|
if (allComps.length > 0) {
|
|
272
|
-
|
|
273
|
-
name: n,
|
|
274
|
-
package: ext.name,
|
|
275
|
-
}));
|
|
203
|
+
components[`${ext.category} (${ext.name})`] = allComps;
|
|
276
204
|
}
|
|
277
205
|
}
|
|
278
206
|
}
|
|
279
|
-
return {type: 'component.list', data:
|
|
207
|
+
return {type: 'component.list', data: components};
|
|
280
208
|
}
|
|
281
209
|
|
|
282
210
|
// ── Single component ───────────────────────────────────────────
|
|
283
211
|
|
|
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
|
-
|
|
292
212
|
const dirName = name.replace(/^XDS/, '');
|
|
293
213
|
|
|
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
|
-
|
|
353
214
|
// When scoped to a specific package, search that package first.
|
|
354
215
|
// This is critical for components that exist in both core and an external
|
|
355
216
|
// package (e.g. AppShell, Button, SideNav) — the package scope ensures
|
|
356
217
|
// the external package's docs are returned, not core's.
|
|
357
218
|
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.
|
|
400
219
|
const ext = resolveExternalPackage(packageScope, cwd);
|
|
401
220
|
if (!ext) {
|
|
402
221
|
throw new AstryxError(`External package "${packageScope}" not found`, undefined, ERROR_CODES.ERR_UNKNOWN_PACKAGE);
|
|
@@ -425,49 +244,11 @@ export async function component(name, options = {}) {
|
|
|
425
244
|
const p = docs.props || (docs.components ? docs.components.flatMap(c => c.props || []) : []);
|
|
426
245
|
return {type: 'component.detail.props', data: p};
|
|
427
246
|
}
|
|
428
|
-
return {
|
|
429
|
-
type: 'component.detail',
|
|
430
|
-
data: withOwnership(docs, {package: ext.name, sourcePath: null}, dirName),
|
|
431
|
-
};
|
|
247
|
+
return {type: 'component.detail', data: docs};
|
|
432
248
|
}
|
|
433
249
|
throw new AstryxError(`No component "${name}" in package "${packageScope}"`, undefined, ERROR_CODES.ERR_UNKNOWN_COMPONENT);
|
|
434
250
|
}
|
|
435
251
|
|
|
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
|
-
|
|
471
252
|
if (source) {
|
|
472
253
|
const sourcePath = findComponentSource(coreDir, dirName);
|
|
473
254
|
if (!sourcePath) {
|
|
@@ -493,10 +274,6 @@ export async function component(name, options = {}) {
|
|
|
493
274
|
|
|
494
275
|
let readmePath = findComponentReadme(coreDir, dirName);
|
|
495
276
|
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;
|
|
500
277
|
|
|
501
278
|
if (!readmePath) {
|
|
502
279
|
const externals = discoverExternalPackages(cwd);
|
|
@@ -504,8 +281,6 @@ export async function component(name, options = {}) {
|
|
|
504
281
|
const extDocPath = findExternalComponentDoc(ext.docsDir, dirName);
|
|
505
282
|
if (extDocPath) {
|
|
506
283
|
readmePath = extDocPath;
|
|
507
|
-
resolvedOwnerPackage = ext.name;
|
|
508
|
-
resolvedSourcePath = null;
|
|
509
284
|
break;
|
|
510
285
|
}
|
|
511
286
|
}
|
|
@@ -524,8 +299,6 @@ export async function component(name, options = {}) {
|
|
|
524
299
|
if (topScore >= 90 && topTied.length === 1 && gap >= 20) {
|
|
525
300
|
resolvedName = topTied[0].name;
|
|
526
301
|
readmePath = findComponentReadme(coreDir, resolvedName);
|
|
527
|
-
resolvedOwnerPackage = CORE_PACKAGE;
|
|
528
|
-
resolvedSourcePath = findComponentSource(coreDir, resolvedName);
|
|
529
302
|
} else {
|
|
530
303
|
const threshold = Math.max(topScore - 20, 1);
|
|
531
304
|
const candidates = results.filter(r => r.score >= threshold).slice(0, 5);
|
|
@@ -609,14 +382,7 @@ export async function component(name, options = {}) {
|
|
|
609
382
|
if (props) {
|
|
610
383
|
return {type: 'component.detail.props', data: matchingComponent.props || []};
|
|
611
384
|
}
|
|
612
|
-
return {
|
|
613
|
-
type: 'component.detail',
|
|
614
|
-
data: withOwnership(
|
|
615
|
-
scoped,
|
|
616
|
-
{package: resolvedOwnerPackage, sourcePath: resolvedSourcePath},
|
|
617
|
-
dirName,
|
|
618
|
-
),
|
|
619
|
-
};
|
|
385
|
+
return {type: 'component.detail', data: scoped};
|
|
620
386
|
}
|
|
621
387
|
|
|
622
388
|
if (props) {
|
|
@@ -624,12 +390,5 @@ export async function component(name, options = {}) {
|
|
|
624
390
|
return {type: 'component.detail.props', data: p};
|
|
625
391
|
}
|
|
626
392
|
|
|
627
|
-
return {
|
|
628
|
-
type: 'component.detail',
|
|
629
|
-
data: withOwnership(
|
|
630
|
-
docs,
|
|
631
|
-
{package: resolvedOwnerPackage, sourcePath: resolvedSourcePath},
|
|
632
|
-
resolvedName,
|
|
633
|
-
),
|
|
634
|
-
};
|
|
393
|
+
return {type: 'component.detail', data: docs};
|
|
635
394
|
}
|
package/src/api/discover.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* @file Programmatic API for the discover command.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import {loadConfig} from '../lib/config.mjs';
|
|
8
8
|
import {
|
|
9
9
|
scanAllPackages,
|
|
10
10
|
findComponentInPackages,
|
|
@@ -40,7 +40,7 @@ function validateDocs(docs) {
|
|
|
40
40
|
*/
|
|
41
41
|
export async function discover(query, options = {}) {
|
|
42
42
|
const {lang = null, zh = false} = options;
|
|
43
|
-
const
|
|
43
|
+
const config = await loadConfig();
|
|
44
44
|
const toEntry = pkg => ({
|
|
45
45
|
name: pkg.name,
|
|
46
46
|
category: pkg.category,
|
|
@@ -50,21 +50,14 @@ export async function discover(query, options = {}) {
|
|
|
50
50
|
displayName: pkg.displayName,
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
.map(integration => ({
|
|
58
|
-
name: integration.name,
|
|
59
|
-
version: integration.version,
|
|
60
|
-
category: integration.name,
|
|
61
|
-
docsDir: integration.components,
|
|
62
|
-
}));
|
|
63
|
-
if (explicitPackages.length === 0) {
|
|
53
|
+
const explicitPackages = (config.loadedIntegrations ?? [])
|
|
54
|
+
.map(integration => integration.package)
|
|
55
|
+
.filter(Boolean);
|
|
56
|
+
if (config.packages.length === 0 && explicitPackages.length === 0) {
|
|
64
57
|
return {type: 'discover.list', data: [], meta: {configured: false}};
|
|
65
58
|
}
|
|
66
59
|
|
|
67
|
-
const packages = scanAllPackages(
|
|
60
|
+
const packages = scanAllPackages(config.packages, explicitPackages);
|
|
68
61
|
|
|
69
62
|
if (packages.length === 0) {
|
|
70
63
|
return {type: 'discover.list', data: [], meta: {configured: true}};
|
package/src/api/doctor.mjs
CHANGED
|
@@ -27,7 +27,7 @@ import {createRequire} from 'node:module';
|
|
|
27
27
|
import {MIN_NODE_VERSION, isNodeVersionSupported} from '../lib/node-version.mjs';
|
|
28
28
|
import {CLI_ROOT, findCoreDir} from '../utils/paths.mjs';
|
|
29
29
|
import {detectPackageManager} from '../utils/package-manager.mjs';
|
|
30
|
-
import {findConfigPath,
|
|
30
|
+
import {findConfigPath, loadConfig} from '../lib/config.mjs';
|
|
31
31
|
import {semverCompare} from '../utils/semver.mjs';
|
|
32
32
|
|
|
33
33
|
const _require = createRequire(import.meta.url);
|
|
@@ -279,9 +279,8 @@ export async function checkConfig(ctx) {
|
|
|
279
279
|
};
|
|
280
280
|
}
|
|
281
281
|
|
|
282
|
-
//
|
|
283
|
-
//
|
|
284
|
-
// re-import directly to surface a genuine load failure as a FAIL.
|
|
282
|
+
// loadConfig swallows errors and returns defaults, so re-import directly to
|
|
283
|
+
// surface a genuine load failure as a FAIL.
|
|
285
284
|
try {
|
|
286
285
|
const {pathToFileURL} = await import('node:url');
|
|
287
286
|
const mod = await import(pathToFileURL(ctx.configPath).href);
|
|
@@ -292,9 +291,25 @@ export async function checkConfig(ctx) {
|
|
|
292
291
|
label: 'astryx.config.mjs',
|
|
293
292
|
status: 'fail',
|
|
294
293
|
message: `astryx.config.mjs default export is not an object (got ${typeof config}).`,
|
|
295
|
-
fix: 'Export a default object from astryx.config.mjs, e.g. `export default {
|
|
294
|
+
fix: 'Export a default object from astryx.config.mjs, e.g. `export default { theme: "default" };`.',
|
|
296
295
|
};
|
|
297
296
|
}
|
|
297
|
+
// Validate that `packages`, if present, is a string or array of strings.
|
|
298
|
+
if (config?.packages !== undefined) {
|
|
299
|
+
const arr = Array.isArray(config.packages)
|
|
300
|
+
? config.packages
|
|
301
|
+
: [config.packages];
|
|
302
|
+
const bad = arr.some(p => typeof p !== 'string');
|
|
303
|
+
if (bad) {
|
|
304
|
+
return {
|
|
305
|
+
id: 'config',
|
|
306
|
+
label: 'astryx.config.mjs',
|
|
307
|
+
status: 'fail',
|
|
308
|
+
message: 'astryx.config.mjs `packages` must be a string or array of strings.',
|
|
309
|
+
fix: 'Set `packages` to a path string or array of path strings.',
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
}
|
|
298
313
|
return {
|
|
299
314
|
id: 'config',
|
|
300
315
|
label: 'astryx.config.mjs',
|
|
@@ -478,8 +493,8 @@ export async function runChecks(options = {}) {
|
|
|
478
493
|
// Resolve a possible theme key from config (best-effort; never throws).
|
|
479
494
|
let configTheme = null;
|
|
480
495
|
try {
|
|
481
|
-
const
|
|
482
|
-
configTheme =
|
|
496
|
+
const loaded = await loadConfig(cwd);
|
|
497
|
+
configTheme = loaded?.theme ?? null;
|
|
483
498
|
} catch {
|
|
484
499
|
// Best-effort: a missing/invalid config leaves configTheme null.
|
|
485
500
|
}
|
package/src/api/layout.mjs
CHANGED
|
@@ -25,14 +25,13 @@ import {expand} from '../lib/xle/expand.mjs';
|
|
|
25
25
|
import {toCompact, toOutline} from '../lib/xle/print.mjs';
|
|
26
26
|
import {buildRegistry, ALIAS_TABLE} from '../lib/xle/registry.mjs';
|
|
27
27
|
import {discoverTemplates, stripTemplateAssetRefs} from './template.mjs';
|
|
28
|
-
import {
|
|
28
|
+
import {loadConfig} from '../lib/config.mjs';
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* The catalog a `{hint}` can resolve to: template blocks (spliced inline) plus
|
|
32
|
-
* any app-registered local components from astryx.config.mjs
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* @astryxdesign/core registry can't see.
|
|
32
|
+
* any app-registered local components from astryx.config.mjs `layout.components`
|
|
33
|
+
* (imported by name). App components are how XLE reaches domain pieces — the
|
|
34
|
+
* KpiCard/chart/drawer set that the @astryxdesign/core registry can't see.
|
|
36
35
|
*/
|
|
37
36
|
async function loadBlocks(cwd) {
|
|
38
37
|
const blocks = [];
|
|
@@ -43,20 +42,20 @@ async function loadBlocks(cwd) {
|
|
|
43
42
|
// discovery is best-effort
|
|
44
43
|
}
|
|
45
44
|
try {
|
|
46
|
-
const
|
|
47
|
-
const components =
|
|
45
|
+
const config = await loadConfig(cwd);
|
|
46
|
+
const components = config.layout?.components || {};
|
|
48
47
|
for (const [name, spec] of Object.entries(components)) {
|
|
49
|
-
const importPath = spec.from;
|
|
48
|
+
const importPath = typeof spec === 'string' ? spec : spec.from;
|
|
50
49
|
if (!importPath) continue;
|
|
51
50
|
blocks.push({
|
|
52
51
|
type: 'block',
|
|
53
52
|
kind: 'component',
|
|
54
53
|
dirName: name,
|
|
55
54
|
name,
|
|
56
|
-
description: spec.description
|
|
55
|
+
description: typeof spec === 'object' ? spec.description || '' : '',
|
|
57
56
|
category: 'app',
|
|
58
57
|
importPath,
|
|
59
|
-
isDefault: Boolean(spec.default),
|
|
58
|
+
isDefault: typeof spec === 'object' ? Boolean(spec.default) : false,
|
|
60
59
|
});
|
|
61
60
|
}
|
|
62
61
|
} catch {
|
|
@@ -282,7 +281,7 @@ TEMPLATE REFERENCING ({hint} pulls in real content — this is how XLE reaches
|
|
|
282
281
|
{kpi-card} standalone reference (no wrapper element) — place a component directly
|
|
283
282
|
{kpi-card}*4 repeat a reference; the definition/import is emitted once
|
|
284
283
|
app components register local ones in astryx.config.mjs to import them by name:
|
|
285
|
-
export default {
|
|
284
|
+
export default {layout: {components: {KpiCard: '@/components/KpiCard'}}}
|
|
286
285
|
then {kpi-card} → import {KpiCard} + <KpiCard /> (kebab ↔ Pascal)
|
|
287
286
|
|
|
288
287
|
STRUCTURE THE EXPANDER HANDLES
|
package/src/api/layout.test.mjs
CHANGED
|
@@ -196,14 +196,11 @@ describe('template referencing', () => {
|
|
|
196
196
|
|
|
197
197
|
it('imports app-registered local components (the local-component bridge)', async () => {
|
|
198
198
|
// Inside the workspace so @astryxdesign/core resolves; cleaned up after.
|
|
199
|
-
// A package.json beside the config makes Project.load resolve it as the
|
|
200
|
-
// sibling-of-nearest-package.json (the standard config resolution).
|
|
201
199
|
const cwd = mkdtempSync(join(process.cwd(), '.xle-imp-test-'));
|
|
202
200
|
try {
|
|
203
|
-
writeFileSync(join(cwd, 'package.json'), '{"name": "xle-imp-fixture"}\n');
|
|
204
201
|
writeFileSync(
|
|
205
202
|
join(cwd, 'astryx.config.mjs'),
|
|
206
|
-
`export default {
|
|
203
|
+
`export default {layout: {components: {KpiCard: '@/components/KpiCard', TimeRangePicker: {from: '@/components/TimeRangePicker'}}}};\n`,
|
|
207
204
|
);
|
|
208
205
|
const result = await layoutExpand('S[p6] > (G[c4 g4] > {kpi-card}*4) + {time-range-picker}', {
|
|
209
206
|
name: 'Demo',
|