@astryxdesign/cli 0.1.1 → 0.1.2-canary.2149fa3
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 +50 -0
- package/docs/theme.doc.mjs +2 -2
- package/docs/working-with-ai.doc.mjs +6 -6
- package/package.json +19 -8
- package/src/api/discover.mjs +78 -26
- package/src/api/index.mjs +1 -0
- package/src/api/layout.mjs +301 -0
- package/src/api/layout.test.mjs +238 -0
- package/src/api/template.mjs +191 -50
- package/src/api/template.test.mjs +2 -0
- package/src/api/theme-add.mjs +182 -0
- package/src/codemods/__tests__/registry.test.mjs +1 -0
- package/src/codemods/registry.mjs +1 -0
- package/src/codemods/transforms/v0.1.2/__tests__/rename-text-color-active-to-accent.test.mjs +120 -0
- package/src/codemods/transforms/v0.1.2/index.mjs +19 -0
- package/src/codemods/transforms/v0.1.2/rename-text-color-active-to-accent.mjs +136 -0
- package/src/commands/build-theme.import-path.test.mjs +22 -2
- package/src/commands/build-theme.mjs +185 -16
- package/src/commands/gap-report.mjs +17 -9
- package/src/commands/gap-report.test.mjs +21 -16
- package/src/commands/init.mjs +34 -8
- package/src/commands/init.next-steps.test.mjs +46 -0
- package/src/commands/layout.mjs +139 -0
- package/src/commands/swizzle.mjs +51 -23
- package/src/commands/upgrade.mjs +2 -71
- package/src/config.mjs +31 -0
- package/src/config.test.mjs +24 -0
- package/src/index.mjs +6 -0
- package/src/lib/config-schema.mjs +119 -0
- package/src/lib/config.mjs +34 -7
- package/src/lib/config.test.mjs +51 -2
- package/src/lib/error-codes.mjs +11 -0
- package/src/lib/integrations.mjs +155 -0
- package/src/lib/integrations.test.mjs +154 -0
- package/src/lib/levenshtein.mjs +29 -0
- package/src/lib/manifest.mjs +10 -0
- package/src/lib/package-scanner.mjs +31 -7
- package/src/lib/string-utils.mjs +5 -14
- package/src/lib/xle/browser.d.ts +100 -0
- package/src/lib/xle/browser.mjs +120 -0
- package/src/lib/xle/expand.mjs +622 -0
- package/src/lib/xle/parse.mjs +581 -0
- package/src/lib/xle/print.mjs +174 -0
- package/src/lib/xle/registry-core.mjs +170 -0
- package/src/lib/xle/registry.mjs +237 -0
- package/src/lib/xle/splice.mjs +137 -0
- package/src/lib/xle/validate.mjs +356 -0
- package/src/lib/xle/xle.test.mjs +333 -0
- package/src/types/config.d.ts +99 -0
- package/src/utils/github.mjs +12 -27
- package/templates/blocks/components/Card/ClickableCardWithNestedButton.doc.mjs +1 -1
- package/templates/blocks/components/Card/SelectableCardMulti.doc.mjs +1 -1
- package/templates/blocks/components/ChatComposerDrawer/ChatComposerDrawerWithProgress.tsx +1 -1
- package/templates/blocks/components/ChatDictationButton/ChatDictationButtonShowcase.tsx +3 -3
- package/templates/blocks/components/CommandPaletteEmpty/CommandPaletteEmptyShowcase.doc.mjs +15 -0
- package/templates/blocks/components/CommandPaletteEmpty/CommandPaletteEmptyShowcase.tsx +26 -0
- package/templates/blocks/components/DateInput/DateInputDateRange.doc.mjs +2 -2
- package/templates/blocks/components/FileInput/FileInputShowcase.tsx +4 -2
- package/templates/blocks/components/LinkProvider/LinkProviderCustomLink.doc.mjs +14 -0
- package/templates/blocks/components/LinkProvider/LinkProviderCustomLink.tsx +43 -0
- package/templates/blocks/components/MultiSelector/MultiSelectorShowcase.tsx +6 -2
- package/templates/blocks/components/NumberInput/NumberInputShowcase.tsx +6 -2
- package/templates/blocks/components/Outline/OutlineShowcase.doc.mjs +15 -0
- package/templates/blocks/components/Outline/OutlineShowcase.tsx +22 -0
- package/templates/blocks/components/RadioList/RadioListShowcase.tsx +8 -1
- package/templates/blocks/components/SegmentedControl/SegmentedControlShowcase.tsx +3 -1
- package/templates/blocks/components/Selector/SelectorShowcase.tsx +4 -1
- package/templates/blocks/components/Slider/SliderShowcase.tsx +10 -1
- package/templates/blocks/components/Tab/TabShowcase.tsx +3 -1
- package/templates/blocks/components/TabList/TabListShowcase.tsx +3 -1
- package/templates/blocks/components/TabMenu/TabMenuShowcase.tsx +3 -1
- package/templates/blocks/components/Table/ColumnResizeHookUsage.doc.mjs +14 -0
- package/templates/blocks/components/Table/ColumnResizeHookUsage.tsx +59 -0
- package/templates/blocks/components/Table/StickyColumnsHookUsage.doc.mjs +14 -0
- package/templates/blocks/components/Table/StickyColumnsHookUsage.tsx +104 -0
- package/templates/blocks/components/Text/TextColors.tsx +20 -5
- package/templates/blocks/components/TextArea/TextAreaShowcase.tsx +4 -2
- package/templates/blocks/components/TextInput/TextInputShowcase.tsx +4 -2
- package/templates/blocks/components/Thumbnail/ThumbnailDisabled.tsx +11 -6
- package/templates/blocks/components/Thumbnail/ThumbnailGallery.tsx +43 -7
- package/templates/blocks/components/Thumbnail/ThumbnailRemovable.tsx +40 -3
- package/templates/blocks/components/Thumbnail/ThumbnailShowcase.tsx +6 -5
- package/templates/blocks/components/Thumbnail/ThumbnailStates.tsx +11 -6
- package/templates/blocks/components/Timestamp/TimestampColors.tsx +2 -2
- package/templates/blocks/components/ToggleButton/ToggleButtonGroup.doc.mjs +1 -1
- package/templates/blocks/components/Tokenizer/TokenizerShowcase.tsx +8 -6
- package/templates/blocks/components/Toolbar/ToolbarTableFilter.doc.mjs +2 -2
- package/templates/blocks/components/Toolbar/ToolbarTableFilter.tsx +23 -14
- package/templates/blocks/components/Typeahead/TypeaheadShowcase.tsx +6 -2
- package/templates/pages/shell-nav/page.tsx +321 -0
- package/templates/pages/shell-nav/template.doc.mjs +12 -0
- package/templates/pages/shell-side-nav/page.tsx +242 -0
- package/templates/pages/shell-side-nav/template.doc.mjs +12 -0
- package/templates/pages/shell-top-nav/page.tsx +224 -0
- package/templates/pages/shell-top-nav/template.doc.mjs +12 -0
- package/templates/pages/theme-showcase/page.tsx +8 -19
- package/templates/themes/butter/butterTheme.ts +916 -0
- package/templates/themes/butter/icons.tsx +77 -0
- package/templates/themes/chocolate/chocolateTheme.ts +230 -0
- package/templates/themes/chocolate/icons.tsx +77 -0
- package/templates/themes/gothic/gothicTheme.ts +657 -0
- package/templates/themes/gothic/icons.tsx +77 -0
- package/templates/themes/manifest.json +90 -0
- package/templates/themes/matcha/icons.tsx +67 -0
- package/templates/themes/matcha/matchaTheme.ts +247 -0
- package/templates/themes/neutral/icons.tsx +77 -0
- package/templates/themes/neutral/neutralTheme.ts +603 -0
- package/templates/themes/stone/icons.tsx +77 -0
- package/templates/themes/stone/stoneTheme.ts +652 -0
- package/templates/themes/y2k/icons.tsx +67 -0
- package/templates/themes/y2k/y2kTheme.ts +617 -0
- package/templates/blocks/components/MoreMenu/MoreMenuInToolbar.doc.mjs +0 -14
- package/templates/blocks/components/MoreMenu/MoreMenuInToolbar.tsx +0 -57
package/src/commands/swizzle.mjs
CHANGED
|
@@ -15,7 +15,11 @@ import * as fs from 'node:fs';
|
|
|
15
15
|
import * as path from 'node:path';
|
|
16
16
|
import * as p from '@clack/prompts';
|
|
17
17
|
import {findCoreDir, listComponents} from '../utils/paths.mjs';
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
assertWithin,
|
|
20
|
+
PathSafetyError,
|
|
21
|
+
isNonInteractive,
|
|
22
|
+
} from '../utils/path-safety.mjs';
|
|
19
23
|
import {isInteractive} from '../utils/interactive.mjs';
|
|
20
24
|
import {jsonOut, humanLog} from '../lib/json.mjs';
|
|
21
25
|
import {cliError} from '../lib/cli-error.mjs';
|
|
@@ -104,7 +108,9 @@ export function registerSwizzle(program) {
|
|
|
104
108
|
}
|
|
105
109
|
humanLog(`\nUsage: astryx swizzle <component>\n`);
|
|
106
110
|
humanLog('Example: astryx swizzle Button');
|
|
107
|
-
humanLog(
|
|
111
|
+
humanLog(
|
|
112
|
+
' astryx swizzle XDSButton (XDS prefix also works)\n',
|
|
113
|
+
);
|
|
108
114
|
return;
|
|
109
115
|
}
|
|
110
116
|
|
|
@@ -112,10 +118,10 @@ export function registerSwizzle(program) {
|
|
|
112
118
|
const componentDir = path.join(coreDir, 'src', dirName);
|
|
113
119
|
|
|
114
120
|
if (!fs.existsSync(componentDir)) {
|
|
115
|
-
cliError(
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
);
|
|
121
|
+
cliError(`Component "${component}" not found.`, {
|
|
122
|
+
suggestions: components.slice(0, 10).map(n => ({name: n})),
|
|
123
|
+
code: ERROR_CODES.ERR_UNKNOWN_COMPONENT,
|
|
124
|
+
});
|
|
119
125
|
return;
|
|
120
126
|
}
|
|
121
127
|
|
|
@@ -196,11 +202,16 @@ export function registerSwizzle(program) {
|
|
|
196
202
|
}
|
|
197
203
|
|
|
198
204
|
const relOutput = path.relative(process.cwd(), outputDir);
|
|
199
|
-
const copiedFiles = files.filter(
|
|
205
|
+
const copiedFiles = files.filter(
|
|
206
|
+
f =>
|
|
207
|
+
!f.includes('.test.') &&
|
|
208
|
+
f !== 'README.md' &&
|
|
209
|
+
fs.statSync(path.join(componentDir, f)).isFile(),
|
|
210
|
+
);
|
|
200
211
|
|
|
201
212
|
// --- Gap reporting ---
|
|
202
213
|
|
|
203
|
-
const gapConfig = loadGapReportConfig();
|
|
214
|
+
const gapConfig = await loadGapReportConfig();
|
|
204
215
|
let gapReportUrl = null;
|
|
205
216
|
let gapDryRunPreview = null;
|
|
206
217
|
|
|
@@ -224,7 +235,7 @@ export function registerSwizzle(program) {
|
|
|
224
235
|
json,
|
|
225
236
|
});
|
|
226
237
|
|
|
227
|
-
const preview = buildGapReportPreview(previewArgs);
|
|
238
|
+
const preview = await buildGapReportPreview(previewArgs);
|
|
228
239
|
|
|
229
240
|
if (!willFile) {
|
|
230
241
|
// Dry-run: do NOT call gh. Surface what would have been filed.
|
|
@@ -239,10 +250,12 @@ export function registerSwizzle(program) {
|
|
|
239
250
|
};
|
|
240
251
|
} else if (gapConfig.command || checkGhCli()) {
|
|
241
252
|
try {
|
|
242
|
-
gapReportUrl = createGapReport(previewArgs);
|
|
253
|
+
gapReportUrl = await createGapReport(previewArgs);
|
|
243
254
|
} catch (err) {
|
|
244
255
|
if (!json)
|
|
245
|
-
console.error(
|
|
256
|
+
console.error(
|
|
257
|
+
`Warning: Could not file gap report: ${err.message}`,
|
|
258
|
+
);
|
|
246
259
|
}
|
|
247
260
|
}
|
|
248
261
|
}
|
|
@@ -259,17 +272,23 @@ export function registerSwizzle(program) {
|
|
|
259
272
|
gapReportSuppressed: reportingSuppressed || !gapConfig.enabled,
|
|
260
273
|
});
|
|
261
274
|
humanLog(`\n✓ Copied ${copied} files to ${relOutput}/\n`);
|
|
262
|
-
humanLog(
|
|
275
|
+
humanLog(
|
|
276
|
+
'Relative imports have been rewritten to use @astryxdesign/core.',
|
|
277
|
+
);
|
|
263
278
|
humanLog('You can now customize the component source freely.\n');
|
|
264
279
|
if (gapReportUrl) {
|
|
265
280
|
humanLog(`✓ Gap report filed: ${gapReportUrl}\n`);
|
|
266
281
|
} else if (gapDryRunPreview) {
|
|
267
|
-
humanLog(
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
282
|
+
humanLog(
|
|
283
|
+
formatPreview(
|
|
284
|
+
await buildGapReportPreview({
|
|
285
|
+
component: dirName,
|
|
286
|
+
category: options.gapCategory || 'other',
|
|
287
|
+
intention: options.gap,
|
|
288
|
+
source: 'llm-auto',
|
|
289
|
+
}),
|
|
290
|
+
),
|
|
291
|
+
);
|
|
273
292
|
humanLog(
|
|
274
293
|
'\n[dry-run] No gap report was filed. Re-run with --commit to file.',
|
|
275
294
|
);
|
|
@@ -283,10 +302,18 @@ export function registerSwizzle(program) {
|
|
|
283
302
|
return;
|
|
284
303
|
}
|
|
285
304
|
|
|
286
|
-
if (json)
|
|
305
|
+
if (json)
|
|
306
|
+
return jsonOut('swizzle.copy', {
|
|
307
|
+
component: dirName,
|
|
308
|
+
outputDir: relOutput,
|
|
309
|
+
filesCopied: copied,
|
|
310
|
+
files: copiedFiles.map(f => f),
|
|
311
|
+
});
|
|
287
312
|
|
|
288
313
|
humanLog(`\n✓ Copied ${copied} files to ${relOutput}/\n`);
|
|
289
|
-
humanLog(
|
|
314
|
+
humanLog(
|
|
315
|
+
'Relative imports have been rewritten to use @astryxdesign/core.',
|
|
316
|
+
);
|
|
290
317
|
humanLog('You can now customize the component source freely.\n');
|
|
291
318
|
|
|
292
319
|
if (reportingSuppressed || !gapConfig.enabled) {
|
|
@@ -330,7 +357,8 @@ export function registerSwizzle(program) {
|
|
|
330
357
|
placeholder:
|
|
331
358
|
'e.g. "Need a compact variant for use in dense data tables"',
|
|
332
359
|
validate: val => {
|
|
333
|
-
if (!val.trim())
|
|
360
|
+
if (!val.trim())
|
|
361
|
+
return 'Please describe what you were trying to do';
|
|
334
362
|
},
|
|
335
363
|
}),
|
|
336
364
|
);
|
|
@@ -350,7 +378,7 @@ export function registerSwizzle(program) {
|
|
|
350
378
|
source: 'interactive',
|
|
351
379
|
};
|
|
352
380
|
|
|
353
|
-
const preview = buildGapReportPreview(previewArgs);
|
|
381
|
+
const preview = await buildGapReportPreview(previewArgs);
|
|
354
382
|
|
|
355
383
|
p.note(
|
|
356
384
|
`${preview.mode === 'github' ? `Repo: ${preview.repo}` : `Custom command: ${preview.command}`}\n\n` +
|
|
@@ -378,7 +406,7 @@ export function registerSwizzle(program) {
|
|
|
378
406
|
s.start('Filing gap report');
|
|
379
407
|
|
|
380
408
|
try {
|
|
381
|
-
const url = createGapReport(previewArgs);
|
|
409
|
+
const url = await createGapReport(previewArgs);
|
|
382
410
|
s.stop('Gap report filed');
|
|
383
411
|
humanLog(`✓ ${url}\n`);
|
|
384
412
|
} catch (err) {
|
package/src/commands/upgrade.mjs
CHANGED
|
@@ -26,7 +26,6 @@
|
|
|
26
26
|
|
|
27
27
|
import * as fs from 'node:fs';
|
|
28
28
|
import * as path from 'node:path';
|
|
29
|
-
import {pathToFileURL} from 'node:url';
|
|
30
29
|
import {execFile} from 'node:child_process';
|
|
31
30
|
import {promisify} from 'node:util';
|
|
32
31
|
import * as p from '@clack/prompts';
|
|
@@ -38,6 +37,7 @@ import {getRunPrefix} from '../utils/package-manager.mjs';
|
|
|
38
37
|
import {isValidSemver, semverGte, semverGt} from '../utils/semver.mjs';
|
|
39
38
|
import {jsonOut, jsonError} from '../lib/json.mjs';
|
|
40
39
|
import {loadConfig} from '../lib/config.mjs';
|
|
40
|
+
import {loadIntegrations} from '../lib/integrations.mjs';
|
|
41
41
|
import {ERROR_CODES} from '../lib/error-codes.mjs';
|
|
42
42
|
|
|
43
43
|
const execFileAsync = promisify(execFile);
|
|
@@ -64,75 +64,6 @@ function detectInstalledTargetVersion() {
|
|
|
64
64
|
return null;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
function isPathSpec(spec) {
|
|
68
|
-
return (
|
|
69
|
-
spec.startsWith('.') ||
|
|
70
|
-
spec.startsWith('/') ||
|
|
71
|
-
spec.endsWith('.mjs') ||
|
|
72
|
-
spec.endsWith('.js')
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function resolvePackageDir(packageName) {
|
|
77
|
-
const parts = packageName.split('/');
|
|
78
|
-
return path.resolve(process.cwd(), 'node_modules', ...parts);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function resolveIntegrationFile(spec) {
|
|
82
|
-
if (isPathSpec(spec)) {
|
|
83
|
-
return path.resolve(process.cwd(), spec);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const packageDir = resolvePackageDir(spec);
|
|
87
|
-
const pkgPath = path.join(packageDir, 'package.json');
|
|
88
|
-
let pkg;
|
|
89
|
-
try {
|
|
90
|
-
pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
91
|
-
} catch {
|
|
92
|
-
throw new Error(
|
|
93
|
-
`Could not find installed integration package "${spec}" at ${pkgPath}. Install it first or pass a direct integration file path.`,
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const manifestPath = pkg.astryx?.integration ?? pkg.xds?.integration;
|
|
98
|
-
if (!manifestPath) {
|
|
99
|
-
throw new Error(
|
|
100
|
-
`Package "${spec}" does not declare astryx.integration (or legacy xds.integration) in package.json.`,
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
return path.resolve(packageDir, manifestPath);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
async function loadIntegrations(specs) {
|
|
107
|
-
const integrations = [];
|
|
108
|
-
for (const spec of specs) {
|
|
109
|
-
const file = resolveIntegrationFile(spec);
|
|
110
|
-
const mod = await import(pathToFileURL(file).href);
|
|
111
|
-
const integration = mod.default ?? mod.integration ?? mod;
|
|
112
|
-
if (!integration || typeof integration !== 'object') {
|
|
113
|
-
throw new Error(`Integration ${spec} did not export an object.`);
|
|
114
|
-
}
|
|
115
|
-
const integrationDir = path.dirname(file);
|
|
116
|
-
if (Array.isArray(integration.codemods)) {
|
|
117
|
-
for (const codemod of integration.codemods) {
|
|
118
|
-
if (typeof codemod.transform === 'string') {
|
|
119
|
-
const transformPath = path.resolve(integrationDir, codemod.transform);
|
|
120
|
-
const transformMod = await import(pathToFileURL(transformPath).href);
|
|
121
|
-
codemod.transform =
|
|
122
|
-
transformMod.default ?? transformMod.transform ?? transformMod;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
integrations.push({
|
|
127
|
-
...integration,
|
|
128
|
-
__file: file,
|
|
129
|
-
__dir: integrationDir,
|
|
130
|
-
__spec: spec,
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
return integrations;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
67
|
function normalizeIntegrationTransforms(integration, from, to) {
|
|
137
68
|
const transforms = [];
|
|
138
69
|
for (const entry of integration.codemods ?? []) {
|
|
@@ -507,7 +438,7 @@ export function registerUpgrade(program) {
|
|
|
507
438
|
} catch {
|
|
508
439
|
if (!json) {
|
|
509
440
|
p.log.warn(
|
|
510
|
-
`Could not update agent docs. Run \`${getRunPrefix()} astryx
|
|
441
|
+
`Could not update agent docs. Run \`${getRunPrefix()} astryx init --features agents\` to update manually.`,
|
|
511
442
|
);
|
|
512
443
|
}
|
|
513
444
|
}
|
package/src/config.mjs
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
import {validateConfig, validateIntegration} from './lib/config-schema.mjs';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Type-preserving helpers for Astryx config and integration manifests.
|
|
7
|
+
*
|
|
8
|
+
* These are intentionally tiny runtime identity functions. Their value is the
|
|
9
|
+
* exported TypeScript surface from `@astryxdesign/cli/config`, so config files
|
|
10
|
+
* can get editor/type feedback without coupling to CLI internals.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @template {import('./types/config').AstryxConfig} T
|
|
15
|
+
* @param {T} config
|
|
16
|
+
* @returns {T}
|
|
17
|
+
*/
|
|
18
|
+
export function createConfig(config) {
|
|
19
|
+
validateConfig(config);
|
|
20
|
+
return config;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @template {import('./types/config').AstryxIntegration} T
|
|
25
|
+
* @param {T} integration
|
|
26
|
+
* @returns {T}
|
|
27
|
+
*/
|
|
28
|
+
export function createIntegration(integration) {
|
|
29
|
+
validateIntegration(integration);
|
|
30
|
+
return integration;
|
|
31
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
import {describe, expect, it} from 'vitest';
|
|
4
|
+
import {createConfig, createIntegration} from './config.mjs';
|
|
5
|
+
|
|
6
|
+
describe('config helpers', () => {
|
|
7
|
+
it('return config and integration objects unchanged', () => {
|
|
8
|
+
const config = {integrations: ['@acme/widgets']};
|
|
9
|
+
const integration = {name: '@acme/widgets', docs: './docs'};
|
|
10
|
+
expect(createConfig(config)).toBe(config);
|
|
11
|
+
expect(createIntegration(integration)).toBe(integration);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('validates config and integration shapes', () => {
|
|
15
|
+
expect(() => createConfig({integrations: [42]})).toThrow(/integrations/);
|
|
16
|
+
expect(() => createIntegration({docs: './docs'})).toThrow(/name/);
|
|
17
|
+
expect(() =>
|
|
18
|
+
createIntegration({
|
|
19
|
+
name: '@acme/widgets',
|
|
20
|
+
postCodemod: [{name: 'empty'}],
|
|
21
|
+
}),
|
|
22
|
+
).toThrow(/postCodemod/);
|
|
23
|
+
});
|
|
24
|
+
});
|
package/src/index.mjs
CHANGED
|
@@ -59,10 +59,15 @@ export const JSON_SUPPORTED = new Set([
|
|
|
59
59
|
'template',
|
|
60
60
|
'hook',
|
|
61
61
|
'theme build',
|
|
62
|
+
'theme list',
|
|
63
|
+
'theme add',
|
|
62
64
|
'gap-report',
|
|
63
65
|
'upgrade',
|
|
64
66
|
'manifest',
|
|
65
67
|
'doctor',
|
|
68
|
+
'layout expand',
|
|
69
|
+
'layout check',
|
|
70
|
+
'layout grammar',
|
|
66
71
|
]);
|
|
67
72
|
|
|
68
73
|
program
|
|
@@ -243,6 +248,7 @@ const commands = [
|
|
|
243
248
|
{name: 'swizzle', path: './commands/swizzle.mjs', register: 'registerSwizzle'},
|
|
244
249
|
// agent-docs folded into init — functions still importable from agent-docs.mjs
|
|
245
250
|
{name: 'template', path: './commands/template.mjs', register: 'registerTemplate'},
|
|
251
|
+
{name: 'layout', path: './commands/layout.mjs', register: 'registerLayout'},
|
|
246
252
|
{name: 'gap-report', path: './commands/gap-report.mjs', register: 'registerGapReport'},
|
|
247
253
|
{name: 'upgrade', path: './commands/upgrade.mjs', register: 'registerUpgrade'},
|
|
248
254
|
{name: 'theme', path: './commands/build-theme.mjs', register: 'registerTheme'},
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/** Runtime schemas for Astryx config and integration manifests. */
|
|
4
|
+
|
|
5
|
+
import {z} from 'zod';
|
|
6
|
+
|
|
7
|
+
const Fn = z.custom(value => typeof value === 'function', {
|
|
8
|
+
message: 'Expected function',
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const StringOrStringArray = z.union([z.string(), z.array(z.string())]);
|
|
12
|
+
const GapReportSchema = z.union([
|
|
13
|
+
z.literal(false),
|
|
14
|
+
z.object({command: z.string()}).passthrough(),
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
export const AstryxConfigSchema = z
|
|
18
|
+
.object({
|
|
19
|
+
packages: StringOrStringArray.optional(),
|
|
20
|
+
integrations: StringOrStringArray.optional(),
|
|
21
|
+
gapReport: GapReportSchema.optional(),
|
|
22
|
+
template: z
|
|
23
|
+
.object({
|
|
24
|
+
get: Fn.optional(),
|
|
25
|
+
})
|
|
26
|
+
.passthrough()
|
|
27
|
+
.optional(),
|
|
28
|
+
})
|
|
29
|
+
.passthrough();
|
|
30
|
+
|
|
31
|
+
export const AstryxIntegrationCodemodSchema = z
|
|
32
|
+
.object({
|
|
33
|
+
name: z.string(),
|
|
34
|
+
from: z.string().optional(),
|
|
35
|
+
to: z.string().optional(),
|
|
36
|
+
title: z.string().optional(),
|
|
37
|
+
description: z.string().optional(),
|
|
38
|
+
pr: z.string().optional(),
|
|
39
|
+
optional: z.boolean().optional(),
|
|
40
|
+
fileExtensions: z.array(z.string()).optional(),
|
|
41
|
+
transform: z.union([z.string(), Fn]),
|
|
42
|
+
})
|
|
43
|
+
.passthrough();
|
|
44
|
+
|
|
45
|
+
export const AstryxPostCodemodHookSchema = z
|
|
46
|
+
.object({
|
|
47
|
+
name: z.string().optional(),
|
|
48
|
+
run: Fn.optional(),
|
|
49
|
+
command: Fn.optional(),
|
|
50
|
+
})
|
|
51
|
+
.passthrough()
|
|
52
|
+
.refine(value => value.run || value.command, {
|
|
53
|
+
message: 'postCodemod hook must define run() or command()',
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
export const AstryxIntegrationSchema = z
|
|
57
|
+
.object({
|
|
58
|
+
name: z.string(),
|
|
59
|
+
version: z.string().optional(),
|
|
60
|
+
displayName: z.string().optional(),
|
|
61
|
+
description: z.string().optional(),
|
|
62
|
+
docs: z.string().optional(),
|
|
63
|
+
category: z.string().optional(),
|
|
64
|
+
blocks: z.string().optional(),
|
|
65
|
+
gapReport: GapReportSchema.optional(),
|
|
66
|
+
template: z
|
|
67
|
+
.object({
|
|
68
|
+
get: z.union([z.string(), Fn]).optional(),
|
|
69
|
+
})
|
|
70
|
+
.passthrough()
|
|
71
|
+
.optional(),
|
|
72
|
+
codemods: z.array(AstryxIntegrationCodemodSchema).optional(),
|
|
73
|
+
postCodemod: z.array(AstryxPostCodemodHookSchema).optional(),
|
|
74
|
+
})
|
|
75
|
+
.passthrough();
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @param {string} label
|
|
79
|
+
* @param {import('zod').ZodError} error
|
|
80
|
+
*/
|
|
81
|
+
function formatZodError(label, error) {
|
|
82
|
+
const issues = error.issues
|
|
83
|
+
.map(issue => {
|
|
84
|
+
const path = issue.path.length ? issue.path.join('.') : '(root)';
|
|
85
|
+
return `${path}: ${issue.message}`;
|
|
86
|
+
})
|
|
87
|
+
.join('; ');
|
|
88
|
+
return `${label} is invalid: ${issues}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* @param {unknown} config
|
|
93
|
+
* @returns {import('../types/config').AstryxConfig}
|
|
94
|
+
*/
|
|
95
|
+
export function validateConfig(config) {
|
|
96
|
+
const result = AstryxConfigSchema.safeParse(config);
|
|
97
|
+
if (!result.success) {
|
|
98
|
+
throw new Error(
|
|
99
|
+
formatZodError('astryx.config.mjs default export', result.error),
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
return result.data;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* @param {unknown} integration
|
|
107
|
+
* @param {string} [label]
|
|
108
|
+
* @returns {import('../types/config').AstryxIntegration}
|
|
109
|
+
*/
|
|
110
|
+
export function validateIntegration(
|
|
111
|
+
integration,
|
|
112
|
+
label = 'integration manifest',
|
|
113
|
+
) {
|
|
114
|
+
const result = AstryxIntegrationSchema.safeParse(integration);
|
|
115
|
+
if (!result.success) {
|
|
116
|
+
throw new Error(formatZodError(label, result.error));
|
|
117
|
+
}
|
|
118
|
+
return result.data;
|
|
119
|
+
}
|
package/src/lib/config.mjs
CHANGED
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
import * as fs from 'node:fs';
|
|
11
11
|
import * as path from 'node:path';
|
|
12
12
|
import {pathToFileURL} from 'node:url';
|
|
13
|
+
import {loadIntegrations} from './integrations.mjs';
|
|
14
|
+
import {validateConfig} from './config-schema.mjs';
|
|
13
15
|
|
|
14
16
|
const DEFAULTS = {
|
|
15
17
|
packages: [],
|
|
@@ -40,18 +42,30 @@ export async function loadConfig(startDir = process.cwd()) {
|
|
|
40
42
|
const configPath = findConfigPath(startDir);
|
|
41
43
|
if (!configPath) return {...DEFAULTS};
|
|
42
44
|
|
|
45
|
+
let rawConfig;
|
|
43
46
|
try {
|
|
44
47
|
const mod = await import(pathToFileURL(configPath).href);
|
|
45
|
-
|
|
46
|
-
return {
|
|
47
|
-
...DEFAULTS,
|
|
48
|
-
...config,
|
|
49
|
-
packages: normalizePackages(config.packages, path.dirname(configPath)),
|
|
50
|
-
integrations: normalizeIntegrations(config.integrations),
|
|
51
|
-
};
|
|
48
|
+
rawConfig = mod.default || {};
|
|
52
49
|
} catch {
|
|
53
50
|
return {...DEFAULTS};
|
|
54
51
|
}
|
|
52
|
+
|
|
53
|
+
const config = validateConfig(rawConfig);
|
|
54
|
+
const configDir = path.dirname(configPath);
|
|
55
|
+
const integrationSpecs = normalizeIntegrations(config.integrations);
|
|
56
|
+
const loadedIntegrations = await loadIntegrations(integrationSpecs, {
|
|
57
|
+
cwd: configDir,
|
|
58
|
+
});
|
|
59
|
+
return {
|
|
60
|
+
...DEFAULTS,
|
|
61
|
+
...config,
|
|
62
|
+
packages: normalizePackages(config.packages, configDir),
|
|
63
|
+
integrations: integrationSpecs,
|
|
64
|
+
loadedIntegrations,
|
|
65
|
+
gapReport:
|
|
66
|
+
config.gapReport ?? firstDefined(loadedIntegrations, 'gapReport'),
|
|
67
|
+
template: mergeTemplateConfig(config.template, loadedIntegrations),
|
|
68
|
+
};
|
|
55
69
|
}
|
|
56
70
|
|
|
57
71
|
/**
|
|
@@ -84,3 +98,16 @@ function normalizeIntegrations(integrations) {
|
|
|
84
98
|
const arr = Array.isArray(integrations) ? integrations : [integrations];
|
|
85
99
|
return arr.filter(value => typeof value === 'string' && value !== '');
|
|
86
100
|
}
|
|
101
|
+
|
|
102
|
+
function firstDefined(integrations, key) {
|
|
103
|
+
for (const integration of integrations) {
|
|
104
|
+
if (integration[key] !== undefined) return integration[key];
|
|
105
|
+
}
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function mergeTemplateConfig(template, integrations) {
|
|
110
|
+
if (template?.get) return template;
|
|
111
|
+
const integrationTemplate = firstDefined(integrations, 'template');
|
|
112
|
+
return integrationTemplate ?? template;
|
|
113
|
+
}
|
package/src/lib/config.test.mjs
CHANGED
|
@@ -15,10 +15,25 @@ afterEach(() => {
|
|
|
15
15
|
fs.rmSync(tmpDir, {recursive: true, force: true});
|
|
16
16
|
});
|
|
17
17
|
|
|
18
|
+
function installIntegrationPackage(dir, name = '@nest/xds-meta') {
|
|
19
|
+
const packageDir = path.join(dir, 'node_modules', ...name.split('/'));
|
|
20
|
+
fs.mkdirSync(packageDir, {recursive: true});
|
|
21
|
+
fs.writeFileSync(
|
|
22
|
+
path.join(packageDir, 'package.json'),
|
|
23
|
+
JSON.stringify({name, astryx: {integration: './astryx.integration.mjs'}}),
|
|
24
|
+
);
|
|
25
|
+
fs.writeFileSync(
|
|
26
|
+
path.join(packageDir, 'astryx.integration.mjs'),
|
|
27
|
+
`export default { name: '${name}' };
|
|
28
|
+
`,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
18
32
|
describe('loadConfig', () => {
|
|
19
33
|
it('normalizes integrations from astryx.config.mjs', async () => {
|
|
20
34
|
const dir = path.join(tmpDir, 'one');
|
|
21
35
|
fs.mkdirSync(dir);
|
|
36
|
+
installIntegrationPackage(dir);
|
|
22
37
|
fs.writeFileSync(
|
|
23
38
|
path.join(dir, 'astryx.config.mjs'),
|
|
24
39
|
`export default { integrations: '@nest/xds-meta' };\n`,
|
|
@@ -28,15 +43,49 @@ describe('loadConfig', () => {
|
|
|
28
43
|
});
|
|
29
44
|
});
|
|
30
45
|
|
|
31
|
-
it('
|
|
46
|
+
it('rejects invalid integration package manifests', async () => {
|
|
47
|
+
const dir = path.join(tmpDir, 'bad');
|
|
48
|
+
fs.mkdirSync(dir);
|
|
49
|
+
const packageDir = path.join(dir, 'node_modules', '@bad', 'widgets');
|
|
50
|
+
fs.mkdirSync(packageDir, {recursive: true});
|
|
51
|
+
fs.writeFileSync(
|
|
52
|
+
path.join(dir, 'astryx.config.mjs'),
|
|
53
|
+
`export default { integrations: '@bad/widgets' };\n`,
|
|
54
|
+
);
|
|
55
|
+
fs.writeFileSync(
|
|
56
|
+
path.join(packageDir, 'package.json'),
|
|
57
|
+
JSON.stringify({
|
|
58
|
+
name: '@bad/widgets',
|
|
59
|
+
astryx: {integration: './astryx.integration.mjs'},
|
|
60
|
+
}),
|
|
61
|
+
);
|
|
62
|
+
fs.writeFileSync(
|
|
63
|
+
path.join(packageDir, 'astryx.integration.mjs'),
|
|
64
|
+
`export default { docs: './docs' };\n`,
|
|
65
|
+
);
|
|
66
|
+
await expect(loadConfig(dir)).rejects.toThrow(/name/);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('normalizes integration arrays', async () => {
|
|
32
70
|
const dir = path.join(tmpDir, 'two');
|
|
33
71
|
fs.mkdirSync(dir);
|
|
72
|
+
installIntegrationPackage(dir);
|
|
34
73
|
fs.writeFileSync(
|
|
35
74
|
path.join(dir, 'astryx.config.mjs'),
|
|
36
|
-
`export default { integrations: ['@nest/xds-meta', ''
|
|
75
|
+
`export default { integrations: ['@nest/xds-meta', ''] };\n`,
|
|
37
76
|
);
|
|
38
77
|
await expect(loadConfig(dir)).resolves.toMatchObject({
|
|
39
78
|
integrations: ['@nest/xds-meta'],
|
|
40
79
|
});
|
|
41
80
|
});
|
|
81
|
+
|
|
82
|
+
it('rejects invalid config shapes', async () => {
|
|
83
|
+
const dir = path.join(tmpDir, 'invalid');
|
|
84
|
+
fs.mkdirSync(dir);
|
|
85
|
+
fs.writeFileSync(
|
|
86
|
+
path.join(dir, 'astryx.config.mjs'),
|
|
87
|
+
`export default { integrations: [42] };\n`,
|
|
88
|
+
);
|
|
89
|
+
await expect(loadConfig(dir)).rejects.toThrow(/integrations/);
|
|
90
|
+
});
|
|
42
91
|
});
|
package/src/lib/error-codes.mjs
CHANGED
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
* | 'ERR_UNKNOWN_SECTION'
|
|
53
53
|
* | 'ERR_UNKNOWN_CATEGORY'
|
|
54
54
|
* | 'ERR_UNKNOWN_TEMPLATE'
|
|
55
|
+
* | 'ERR_UNKNOWN_THEME'
|
|
55
56
|
* | 'ERR_UNKNOWN_PACKAGE'
|
|
56
57
|
* | 'ERR_UNKNOWN_AGENT'
|
|
57
58
|
* | 'ERR_UNKNOWN_FEATURE'
|
|
@@ -75,6 +76,8 @@
|
|
|
75
76
|
* | 'ERR_DEP_MISSING'
|
|
76
77
|
* | 'ERR_GH_CLI'
|
|
77
78
|
* | 'ERR_GAP_REPORT_FAILED'
|
|
79
|
+
* | 'ERR_LAYOUT_PARSE'
|
|
80
|
+
* | 'ERR_LAYOUT_INVALID'
|
|
78
81
|
* )} ErrorCode
|
|
79
82
|
*/
|
|
80
83
|
|
|
@@ -122,6 +125,8 @@ export const ERROR_CODES = Object.freeze({
|
|
|
122
125
|
ERR_UNKNOWN_CATEGORY: 'ERR_UNKNOWN_CATEGORY',
|
|
123
126
|
/** No template matched the requested name. */
|
|
124
127
|
ERR_UNKNOWN_TEMPLATE: 'ERR_UNKNOWN_TEMPLATE',
|
|
128
|
+
/** No theme matched the requested slug (theme add). */
|
|
129
|
+
ERR_UNKNOWN_THEME: 'ERR_UNKNOWN_THEME',
|
|
125
130
|
/** No package matched the requested name (discover). */
|
|
126
131
|
ERR_UNKNOWN_PACKAGE: 'ERR_UNKNOWN_PACKAGE',
|
|
127
132
|
/** An unrecognized `--agent` value was passed to agent-docs/init. */
|
|
@@ -180,6 +185,12 @@ export const ERROR_CODES = Object.freeze({
|
|
|
180
185
|
ERR_GH_CLI: 'ERR_GH_CLI',
|
|
181
186
|
/** Filing a gap report failed at the command/integration boundary. */
|
|
182
187
|
ERR_GAP_REPORT_FAILED: 'ERR_GAP_REPORT_FAILED',
|
|
188
|
+
|
|
189
|
+
// ── Layout expressions (XLE/XLO) ─────────────────────────────────
|
|
190
|
+
/** A layout expression failed to parse (syntax error, with line/col). */
|
|
191
|
+
ERR_LAYOUT_PARSE: 'ERR_LAYOUT_PARSE',
|
|
192
|
+
/** A layout expression parsed but failed validation (unknown component/prop/enum/block). */
|
|
193
|
+
ERR_LAYOUT_INVALID: 'ERR_LAYOUT_INVALID',
|
|
183
194
|
});
|
|
184
195
|
|
|
185
196
|
/**
|