@astryxdesign/cli 0.1.0 → 0.1.1-canary.080d887
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 +68 -0
- package/README.md +117 -75
- package/bin/astryx.mjs +22 -7
- package/docs/getting-started.doc.mjs +11 -11
- package/docs/icons.doc.mjs +1 -1
- package/docs/migration.doc.mjs +2 -2
- package/docs/shape.doc.mjs +1 -1
- package/docs/styling.doc.mjs +3 -4
- package/docs/theme.doc.dense.mjs +2 -2
- package/docs/theme.doc.mjs +14 -0
- package/docs/theme.doc.zh.mjs +2 -2
- package/docs/working-with-ai.doc.mjs +4 -4
- package/package.json +20 -9
- package/src/api/discover.mjs +78 -26
- package/src/api/doctor.mjs +3 -3
- package/src/api/layout.mjs +301 -0
- package/src/api/layout.test.mjs +238 -0
- package/src/api/search.mjs +207 -13
- package/src/api/template.mjs +193 -51
- package/src/api/template.test.mjs +2 -0
- package/src/codemods/__tests__/registry.test.mjs +1 -0
- package/src/codemods/registry.mjs +1 -0
- package/src/codemods/runner.mjs +105 -51
- package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-config-surfaces.test.mjs +116 -0
- package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-module-specifiers.test.mjs +51 -0
- package/src/codemods/transforms/v0.1.0/index.mjs +28 -0
- package/src/codemods/transforms/v0.1.0/migrate-xds-config-surfaces.mjs +230 -0
- package/src/codemods/transforms/v0.1.0/migrate-xds-module-specifiers.mjs +84 -0
- package/src/commands/agent-docs.mjs +119 -66
- package/src/commands/agent-docs.path-safety.test.mjs +1 -1
- package/src/commands/agent-docs.test.mjs +87 -31
- package/src/commands/build-theme.import-path.test.mjs +1 -1
- package/src/commands/build-theme.path-safety.test.mjs +1 -1
- package/src/commands/build-theme.prose.test.mjs +1 -1
- package/src/commands/build.mjs +196 -0
- package/src/commands/component-package.test.mjs +1 -1
- package/src/commands/component.test.mjs +1 -1
- package/src/commands/docs.test.mjs +1 -1
- package/src/commands/doctor.test.mjs +1 -1
- package/src/commands/external-showcase.test.mjs +1 -1
- package/src/commands/gap-report.mjs +17 -9
- package/src/commands/gap-report.test.mjs +21 -16
- package/src/commands/init.mjs +43 -9
- package/src/commands/init.next-steps.test.mjs +46 -0
- package/src/commands/interactive-guard.test.mjs +1 -1
- package/src/commands/json-contract.test.mjs +10 -3
- package/src/commands/layout.mjs +139 -0
- package/src/commands/swizzle-gap-safety.test.mjs +1 -1
- package/src/commands/swizzle.mjs +51 -23
- package/src/commands/swizzle.path-safety.test.mjs +1 -1
- package/src/commands/template.path-safety.test.mjs +1 -1
- package/src/commands/template.test.mjs +1 -1
- package/src/commands/upgrade.mjs +292 -177
- package/src/commands/upgrade.test.mjs +41 -27
- package/src/config.mjs +31 -0
- package/src/config.test.mjs +24 -0
- package/src/index.mjs +5 -0
- package/src/lib/config-schema.mjs +119 -0
- package/src/lib/config.mjs +45 -6
- package/src/lib/config.test.mjs +91 -0
- 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 +6 -0
- package/src/lib/package-scanner.mjs +31 -7
- package/src/lib/string-utils.mjs +5 -14
- package/src/lib/xle/browser.d.ts +91 -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/types/error-codes.d.ts +1 -0
- package/src/utils/github.mjs +12 -27
- package/src/utils/interactive.mjs +1 -1
- package/src/utils/interactive.test.mjs +2 -0
- package/src/utils/package-manager.mjs +1 -1
- package/src/utils/package-manager.test.mjs +1 -1
- package/src/utils/path-safety.test.mjs +1 -1
- package/src/utils/paths.test.mjs +8 -8
- package/src/utils/update-check.mjs +4 -26
- package/src/utils/update-check.test.mjs +2 -64
- package/templates/blocks/components/AppShell/AppShellContentOnly.tsx +1 -9
- package/templates/blocks/components/AppShell/AppShellShowcase.tsx +1 -10
- package/templates/blocks/components/AppShell/AppShellSideNavOnly.tsx +1 -9
- package/templates/blocks/components/AppShell/AppShellTopNavOnly.tsx +1 -9
- package/templates/blocks/components/AppShell/AppShellTopNavWithSideNav.tsx +1 -9
- package/templates/blocks/components/AppShell/AppShellWithBanner.tsx +1 -9
- package/templates/blocks/components/AspectRatio/AspectRatioShowcase.tsx +12 -19
- package/templates/blocks/components/Banner/BannerShowcase.tsx +1 -8
- package/templates/blocks/components/Blockquote/BlockquoteShowcase.tsx +1 -8
- package/templates/blocks/components/Carousel/CarouselShowcase.tsx +2 -12
- package/templates/blocks/components/ChatComposerDrawer/ChatComposerDrawerShowcase.tsx +6 -9
- package/templates/blocks/components/ChatLayout/ChatLayoutPanelChat.tsx +10 -12
- package/templates/blocks/components/ChatMessageList/ChatMessageListDensity.tsx +1 -9
- package/templates/blocks/components/ChatMessageList/ChatMessageListFullFeatured.tsx +1 -9
- package/templates/blocks/components/ChatMessageList/ChatMessageListShowcase.tsx +1 -9
- package/templates/blocks/components/ChatMessageMetadata/ChatMessageMetadataShowcase.tsx +1 -8
- package/templates/blocks/components/ChatSendButton/ChatSendButtonInComposer.tsx +1 -8
- package/templates/blocks/components/Citation/CitationInlineText.tsx +4 -4
- package/templates/blocks/components/Code/CodeInlineInParagraph.tsx +1 -8
- package/templates/blocks/components/CodeBlock/CodeBlockBashCommand.tsx +1 -1
- package/templates/blocks/components/CodeBlock/CodeBlockJSONConfig.tsx +1 -1
- package/templates/blocks/components/CommandPaletteEmpty/CommandPaletteEmptyShowcase.doc.mjs +15 -0
- package/templates/blocks/components/CommandPaletteEmpty/CommandPaletteEmptyShowcase.tsx +26 -0
- package/templates/blocks/components/CommandPaletteItem/CommandPaletteItemShowcase.tsx +9 -12
- package/templates/blocks/components/ContextMenu/ContextMenuShowcase.tsx +13 -15
- package/templates/blocks/components/DateInput/DateInputDateRange.doc.mjs +2 -2
- package/templates/blocks/components/Divider/DividerShowcase.tsx +1 -8
- package/templates/blocks/components/Divider/DividerVertical.tsx +7 -9
- package/templates/blocks/components/Field/FieldShowcase.tsx +1 -8
- package/templates/blocks/components/FormLayout/FormLayoutHorizontal.tsx +1 -6
- package/templates/blocks/components/Grid/GridResponsiveAutoFit.tsx +1 -9
- package/templates/blocks/components/HoverCard/HoverCardInlineTextHoverCard.tsx +4 -6
- package/templates/blocks/components/HoverCard/HoverCardInteractiveContent.tsx +1 -6
- package/templates/blocks/components/HoverCard/HoverCardProfileHoverCard.tsx +2 -8
- package/templates/blocks/components/HoverCard/HoverCardShowcase.tsx +1 -8
- package/templates/blocks/components/OverflowList/OverflowListOverflowBadges.tsx +8 -11
- package/templates/blocks/components/OverflowList/OverflowListOverflowDropdownActions.tsx +9 -12
- package/templates/blocks/components/Overlay/OverlayBottomStrip.tsx +4 -17
- package/templates/blocks/components/Overlay/OverlayHoverReveal.tsx +15 -16
- package/templates/blocks/components/Overlay/OverlayShowcase.tsx +5 -21
- package/templates/blocks/components/Pagination/PaginationDotsCarousel.tsx +2 -14
- package/templates/blocks/components/Pagination/PaginationPageSize.tsx +12 -14
- package/templates/blocks/components/Pagination/PaginationVariants.tsx +1 -8
- package/templates/blocks/components/Pagination/PaginationWithTable.tsx +2 -14
- package/templates/blocks/components/Slider/SliderShowcase.tsx +10 -1
- package/templates/blocks/components/ToggleButton/ToggleButtonGroup.doc.mjs +1 -1
- package/templates/blocks/components/Tokenizer/TokenizerClear.tsx +1 -6
- package/templates/blocks/components/Tokenizer/TokenizerCreatable.tsx +2 -7
- package/templates/blocks/components/Tokenizer/TokenizerEndContent.tsx +1 -6
- package/templates/blocks/components/Tokenizer/TokenizerIcon.tsx +1 -6
- package/templates/blocks/components/Tokenizer/TokenizerMaxEntries.tsx +1 -6
- package/templates/blocks/components/Tokenizer/TokenizerOverflow.tsx +2 -7
- package/templates/blocks/components/Tokenizer/TokenizerShowcase.tsx +1 -6
- package/templates/blocks/components/Tokenizer/TokenizerStates.tsx +4 -9
- package/templates/blocks/components/Toolbar/ToolbarCardHeader.tsx +1 -10
- package/templates/blocks/components/Toolbar/ToolbarSizes.tsx +1 -8
- package/templates/blocks/components/Toolbar/ToolbarTableFilter.tsx +1 -8
- package/templates/blocks/components/Toolbar/ToolbarThreeSlot.tsx +1 -10
- package/templates/blocks/components/Toolbar/ToolbarWithTabs.tsx +8 -11
- package/templates/pages/ai-chat/page.tsx +71 -64
- package/templates/pages/ai-chat-landing/page.tsx +8 -12
- package/templates/pages/centered-hero/page.tsx +13 -15
- package/templates/pages/classic-gallery/page.tsx +27 -34
- package/templates/pages/detail-page/page.tsx +18 -18
- package/templates/pages/documentation/page.tsx +42 -58
- package/templates/pages/documentation-design/page.tsx +82 -60
- package/templates/pages/documentation-technical/page.tsx +101 -60
- package/templates/pages/editor/page.tsx +42 -54
- package/templates/pages/file-explorer/page.tsx +13 -16
- package/templates/pages/form-two-column/page.tsx +13 -17
- package/templates/pages/gallery-hero/page.tsx +13 -15
- package/templates/pages/ide/page.tsx +188 -264
- package/templates/pages/library/page.tsx +16 -23
- package/templates/pages/login/page.tsx +14 -18
- package/templates/pages/login-card/page.tsx +14 -18
- package/templates/pages/login-split/page.tsx +50 -48
- package/templates/pages/login-sso/page.tsx +9 -13
- package/templates/pages/mixed-gallery/page.tsx +51 -45
- package/templates/pages/payment-form/page.tsx +56 -70
- package/templates/pages/product-detail/page.tsx +27 -33
- package/templates/pages/product-gallery/page.tsx +7 -13
- package/templates/pages/settings-dialog/page.tsx +35 -43
- package/templates/pages/settings-sidebar/page.tsx +39 -47
- package/templates/pages/side-gallery/page.tsx +6 -9
- package/templates/pages/table-grouped/page.tsx +11 -15
- package/templates/pages/theme-showcase/page.tsx +33 -37
- package/templates/blocks/components/MoreMenu/MoreMenuInToolbar.doc.mjs +0 -14
- package/templates/blocks/components/MoreMenu/MoreMenuInToolbar.tsx +0 -67
package/src/api/template.mjs
CHANGED
|
@@ -7,7 +7,11 @@
|
|
|
7
7
|
import * as fs from 'node:fs';
|
|
8
8
|
import * as path from 'node:path';
|
|
9
9
|
import {CLI_ROOT, discoverExternalPackages} from '../utils/paths.mjs';
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
assertWithin,
|
|
12
|
+
isFilePathArg,
|
|
13
|
+
PathSafetyError,
|
|
14
|
+
} from '../utils/path-safety.mjs';
|
|
11
15
|
import {AstryxError} from './error.mjs';
|
|
12
16
|
import {ERROR_CODES} from '../lib/error-codes.mjs';
|
|
13
17
|
import {loadConfig} from '../lib/config.mjs';
|
|
@@ -23,7 +27,7 @@ const BLOCKS_DIR = path.join(TEMPLATES_DIR, 'blocks');
|
|
|
23
27
|
* or not. Mirrors apps/docsite/public/template-assets/placeholder.svg.
|
|
24
28
|
*/
|
|
25
29
|
const PLACEHOLDER_IMAGE =
|
|
26
|
-
|
|
30
|
+
'data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20400%20300%22%20preserveAspectRatio%3D%22xMidYMid%20slice%22%3E%3Crect%20width%3D%22400%22%20height%3D%22300%22%20fill%3D%22%23f5f6f8%22%2F%3E%3Cg%20transform%3D%22translate%28200%20150%29%22%20fill%3D%22none%22%20stroke%3D%22%23c2cad6%22%20stroke-width%3D%225%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Crect%20x%3D%22-44%22%20y%3D%22-44%22%20width%3D%2288%22%20height%3D%2288%22%20rx%3D%2216%22%2F%3E%3Ccircle%20cx%3D%2218%22%20cy%3D%22-18%22%20r%3D%222.5%22%20fill%3D%22%23c2cad6%22%20stroke%3D%22none%22%2F%3E%3Cpath%20d%3D%22M-34%2030%20L-8%200%20L10%2018%20L20%208%20L34%2024%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E';
|
|
27
31
|
|
|
28
32
|
/**
|
|
29
33
|
* Demo-image sources to strip from scaffolded projects — Meta's lookaside CDN
|
|
@@ -60,9 +64,12 @@ export {discoverAll as discoverTemplates};
|
|
|
60
64
|
export function listTemplates() {
|
|
61
65
|
const all = [];
|
|
62
66
|
if (fs.existsSync(PAGES_DIR)) {
|
|
63
|
-
all.push(
|
|
64
|
-
|
|
65
|
-
|
|
67
|
+
all.push(
|
|
68
|
+
...fs
|
|
69
|
+
.readdirSync(PAGES_DIR, {withFileTypes: true})
|
|
70
|
+
.filter(e => e.isDirectory())
|
|
71
|
+
.map(e => e.name),
|
|
72
|
+
);
|
|
66
73
|
}
|
|
67
74
|
return all.sort();
|
|
68
75
|
}
|
|
@@ -83,7 +90,8 @@ function findDocFiles(dir, pattern) {
|
|
|
83
90
|
|
|
84
91
|
async function discoverPages() {
|
|
85
92
|
if (!fs.existsSync(PAGES_DIR)) return [];
|
|
86
|
-
const dirs = fs
|
|
93
|
+
const dirs = fs
|
|
94
|
+
.readdirSync(PAGES_DIR, {withFileTypes: true})
|
|
87
95
|
.filter(e => e.isDirectory());
|
|
88
96
|
|
|
89
97
|
const templates = [];
|
|
@@ -95,6 +103,7 @@ async function discoverPages() {
|
|
|
95
103
|
dirName: dir.name,
|
|
96
104
|
name: doc?.name || dir.name,
|
|
97
105
|
description: doc?.description || '',
|
|
106
|
+
category: doc?.category || '',
|
|
98
107
|
isReady: doc?.isReady ?? true,
|
|
99
108
|
scaffold: doc?.scaffold ?? false,
|
|
100
109
|
filePath: path.join(dirPath, 'page.tsx'),
|
|
@@ -137,7 +146,11 @@ async function discoverBlocks() {
|
|
|
137
146
|
* @param {string} [cwd]
|
|
138
147
|
*/
|
|
139
148
|
async function discoverExternalBlocks(cwd = process.cwd()) {
|
|
140
|
-
const
|
|
149
|
+
const config = await loadConfig(cwd);
|
|
150
|
+
const integrationPackages = (config.loadedIntegrations ?? [])
|
|
151
|
+
.map(integration => integration.package)
|
|
152
|
+
.filter(Boolean);
|
|
153
|
+
const externals = [...discoverExternalPackages(cwd), ...integrationPackages];
|
|
141
154
|
const blocks = [];
|
|
142
155
|
|
|
143
156
|
for (const ext of externals) {
|
|
@@ -192,9 +205,7 @@ async function discoverAll() {
|
|
|
192
205
|
export async function findRelatedBlocks(componentName, cwd) {
|
|
193
206
|
const blocks = await discoverAllBlocks(cwd);
|
|
194
207
|
return blocks.filter(b =>
|
|
195
|
-
b.componentsUsed.some(c =>
|
|
196
|
-
c.toLowerCase() === componentName.toLowerCase(),
|
|
197
|
-
),
|
|
208
|
+
b.componentsUsed.some(c => c.toLowerCase() === componentName.toLowerCase()),
|
|
198
209
|
);
|
|
199
210
|
}
|
|
200
211
|
|
|
@@ -217,7 +228,7 @@ export async function findShowcase(componentName, cwd, options) {
|
|
|
217
228
|
return true;
|
|
218
229
|
});
|
|
219
230
|
|
|
220
|
-
const toResult =
|
|
231
|
+
const toResult = b => ({
|
|
221
232
|
name: b.name,
|
|
222
233
|
aspectRatio: b.aspectRatio,
|
|
223
234
|
filePath: b.filePath,
|
|
@@ -241,11 +252,17 @@ export async function findShowcase(componentName, cwd, options) {
|
|
|
241
252
|
}
|
|
242
253
|
|
|
243
254
|
const UBIQUITOUS = new Set([
|
|
244
|
-
'Text',
|
|
245
|
-
'
|
|
255
|
+
'Text',
|
|
256
|
+
'Heading',
|
|
257
|
+
'Button',
|
|
258
|
+
'HStack',
|
|
259
|
+
'VStack',
|
|
260
|
+
'Link',
|
|
261
|
+
'StackItem',
|
|
262
|
+
'Icon',
|
|
246
263
|
]);
|
|
247
264
|
|
|
248
|
-
function extractComponents(pagePath) {
|
|
265
|
+
export function extractComponents(pagePath) {
|
|
249
266
|
const src = fs.readFileSync(pagePath, 'utf-8');
|
|
250
267
|
// Match JSX opening tags, e.g. `<Section` or the legacy `<XDSSection`.
|
|
251
268
|
// Templates author bare component names post un-prefix migration
|
|
@@ -258,22 +275,115 @@ function extractComponents(pagePath) {
|
|
|
258
275
|
while ((m = tagRegex.exec(src)) !== null) {
|
|
259
276
|
matches.push(m[2]);
|
|
260
277
|
}
|
|
261
|
-
return [
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
278
|
+
return [
|
|
279
|
+
...new Set(
|
|
280
|
+
matches
|
|
281
|
+
.filter(n => !['Theme', 'ThemeProvider'].includes(n))
|
|
282
|
+
.filter(n => !UBIQUITOUS.has(n))
|
|
283
|
+
.map(n =>
|
|
284
|
+
n.replace(
|
|
285
|
+
/(Item|Section|Header|Content|Footer|Panel|Heading|CollapseButton|Column|Sortable|Selection|Group|Source)$/,
|
|
286
|
+
'',
|
|
287
|
+
),
|
|
288
|
+
)
|
|
289
|
+
.filter(Boolean),
|
|
290
|
+
),
|
|
291
|
+
].sort();
|
|
268
292
|
}
|
|
269
293
|
|
|
270
294
|
const STRUCTURAL = new Set([
|
|
271
|
-
'AppShell',
|
|
272
|
-
'
|
|
273
|
-
'
|
|
274
|
-
'
|
|
295
|
+
'AppShell',
|
|
296
|
+
'Layout',
|
|
297
|
+
'LayoutHeader',
|
|
298
|
+
'LayoutContent',
|
|
299
|
+
'LayoutPanel',
|
|
300
|
+
'LayoutFooter',
|
|
301
|
+
'Card',
|
|
302
|
+
'Section',
|
|
303
|
+
'Grid',
|
|
304
|
+
'GridSpan',
|
|
305
|
+
'List',
|
|
306
|
+
'Table',
|
|
307
|
+
'TabList',
|
|
308
|
+
'Toolbar',
|
|
309
|
+
'SideNav',
|
|
310
|
+
'TopNav',
|
|
311
|
+
'Dialog',
|
|
312
|
+
'FormLayout',
|
|
313
|
+
'Center',
|
|
275
314
|
]);
|
|
276
315
|
|
|
316
|
+
const SPATIAL_PROPS = [
|
|
317
|
+
'padding',
|
|
318
|
+
'contentPadding',
|
|
319
|
+
'gap',
|
|
320
|
+
'rowGap',
|
|
321
|
+
'columnGap',
|
|
322
|
+
'columns',
|
|
323
|
+
'minChildWidth',
|
|
324
|
+
'hasDivider',
|
|
325
|
+
'defaultHasDividers',
|
|
326
|
+
'variant',
|
|
327
|
+
'density',
|
|
328
|
+
'role',
|
|
329
|
+
'height',
|
|
330
|
+
'width',
|
|
331
|
+
'maxWidth',
|
|
332
|
+
];
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Copy allowlisted layout props verbatim from a JSX opening-tag fragment.
|
|
336
|
+
* Uses quote/brace matching so object literals and spaced strings stay intact.
|
|
337
|
+
*
|
|
338
|
+
* @param {string} tagText
|
|
339
|
+
* @returns {string[]}
|
|
340
|
+
*/
|
|
341
|
+
function extractSpatialAttrs(tagText) {
|
|
342
|
+
const attrs = [];
|
|
343
|
+
for (const name of SPATIAL_PROPS) {
|
|
344
|
+
const eqMatch = tagText.match(new RegExp(`\\b${name}\\s*=\\s*`));
|
|
345
|
+
if (eqMatch) {
|
|
346
|
+
const start = eqMatch.index;
|
|
347
|
+
let i = eqMatch.index + eqMatch[0].length;
|
|
348
|
+
const rest = tagText.slice(i);
|
|
349
|
+
|
|
350
|
+
if (rest[0] === '"' || rest[0] === "'") {
|
|
351
|
+
const q = rest[0];
|
|
352
|
+
i += 1;
|
|
353
|
+
while (i < tagText.length && tagText[i] !== q) {
|
|
354
|
+
if (tagText[i] === '\\') i += 1;
|
|
355
|
+
i += 1;
|
|
356
|
+
}
|
|
357
|
+
if (i < tagText.length) i += 1;
|
|
358
|
+
} else if (rest[0] === '{') {
|
|
359
|
+
let depth = 0;
|
|
360
|
+
while (i < tagText.length) {
|
|
361
|
+
if (tagText[i] === '{') depth += 1;
|
|
362
|
+
else if (tagText[i] === '}') {
|
|
363
|
+
depth -= 1;
|
|
364
|
+
if (depth === 0) {
|
|
365
|
+
i += 1;
|
|
366
|
+
break;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
i += 1;
|
|
370
|
+
}
|
|
371
|
+
} else {
|
|
372
|
+
const bare = rest.match(/^[^\s/>]+/);
|
|
373
|
+
i += bare ? bare[0].length : 0;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
attrs.push(tagText.slice(start, i).trim());
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (new RegExp(`\\b${name}(?=[\\s/>])`).test(tagText)) {
|
|
381
|
+
attrs.push(name);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return attrs;
|
|
385
|
+
}
|
|
386
|
+
|
|
277
387
|
function extractSkeleton(source) {
|
|
278
388
|
const lines = source.split('\n');
|
|
279
389
|
const out = [];
|
|
@@ -286,11 +396,18 @@ function extractSkeleton(source) {
|
|
|
286
396
|
for (let i = 0; i < lines.length; i++) {
|
|
287
397
|
const t = lines[i].trim();
|
|
288
398
|
|
|
289
|
-
if (t.match(/^export\s+default\s+function/)) {
|
|
290
|
-
|
|
399
|
+
if (t.match(/^export\s+default\s+function/)) {
|
|
400
|
+
inDefaultExport = true;
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
if (inDefaultExport && t.match(/^return\s*\(/)) {
|
|
404
|
+
capturing = true;
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
291
407
|
if (!capturing) continue;
|
|
292
408
|
if (out.length >= MAX_LINES) {
|
|
293
|
-
if (!out[out.length - 1]?.includes('...'))
|
|
409
|
+
if (!out[out.length - 1]?.includes('...'))
|
|
410
|
+
out.push(' '.repeat(depth) + '...');
|
|
294
411
|
continue;
|
|
295
412
|
}
|
|
296
413
|
|
|
@@ -308,20 +425,13 @@ function extractSkeleton(source) {
|
|
|
308
425
|
if (lines[j].includes('>')) break;
|
|
309
426
|
}
|
|
310
427
|
|
|
311
|
-
const props =
|
|
312
|
-
const propRegex = /\b(padding|contentPadding|gap|rowGap|columnGap|columns|minChildWidth|hasDivider|defaultHasDividers|variant|density|role|height|width|maxWidth)\s*[=]\s*\{?\s*['"]?([^}'"\s,/>]+)/g;
|
|
313
|
-
let m;
|
|
314
|
-
while ((m = propRegex.exec(tagText)) !== null) {
|
|
315
|
-
const val = m[2];
|
|
316
|
-
if (val === 'true') props.push(m[1]);
|
|
317
|
-
else if (/^\d+$/.test(val)) props.push(`${m[1]}={${val}}`);
|
|
318
|
-
else props.push(`${m[1]}="${val}"`);
|
|
319
|
-
}
|
|
320
|
-
|
|
428
|
+
const props = extractSpatialAttrs(tagText);
|
|
321
429
|
const hasSpatialProps = props.length > 0;
|
|
322
430
|
const propStr = hasSpatialProps ? ' ' + props.join(' ') : '';
|
|
323
431
|
const isVStack = comp === 'VStack' || comp === 'HStack';
|
|
324
|
-
const isSelfClosing = tagText.match(
|
|
432
|
+
const isSelfClosing = tagText.match(
|
|
433
|
+
new RegExp('<' + tagName + '[^>]*/>', 's'),
|
|
434
|
+
);
|
|
325
435
|
|
|
326
436
|
if (isVStack && !hasSpatialProps) continue;
|
|
327
437
|
|
|
@@ -348,13 +458,18 @@ function extractSkeleton(source) {
|
|
|
348
458
|
continue;
|
|
349
459
|
}
|
|
350
460
|
|
|
351
|
-
const slotMatch = t.match(
|
|
461
|
+
const slotMatch = t.match(
|
|
462
|
+
/^(header|content|footer|start|end|sideNav|topNav)\s*=\s*\{/,
|
|
463
|
+
);
|
|
352
464
|
if (slotMatch) {
|
|
353
465
|
out.push(' '.repeat(depth) + `/* ${slotMatch[1]}: */`);
|
|
354
466
|
continue;
|
|
355
467
|
}
|
|
356
468
|
|
|
357
|
-
if (
|
|
469
|
+
if (
|
|
470
|
+
t.startsWith('<div') &&
|
|
471
|
+
(t.includes('padding') || t.includes('maxWidth') || t.includes('gap:'))
|
|
472
|
+
) {
|
|
358
473
|
const styleProps = [];
|
|
359
474
|
const divText = lines.slice(i, Math.min(i + 5, lines.length)).join(' ');
|
|
360
475
|
const pp = divText.match(/padding[^:]*:\s*['"]?([^'"},)]+)/);
|
|
@@ -394,7 +509,7 @@ export async function getTemplateById(id, options = {}) {
|
|
|
394
509
|
'Add a template.get function to astryx.config.mjs:\n\n' +
|
|
395
510
|
' export default {\n' +
|
|
396
511
|
' template: {\n' +
|
|
397
|
-
|
|
512
|
+
' get: async (id) => { /* return template source string */ },\n' +
|
|
398
513
|
' },\n' +
|
|
399
514
|
' };',
|
|
400
515
|
undefined,
|
|
@@ -407,7 +522,11 @@ export async function getTemplateById(id, options = {}) {
|
|
|
407
522
|
source = await getter(id);
|
|
408
523
|
} catch (err) {
|
|
409
524
|
const detail = err instanceof Error ? err.message : String(err);
|
|
410
|
-
throw new AstryxError(
|
|
525
|
+
throw new AstryxError(
|
|
526
|
+
`template.get("${id}") threw an error: ${detail}`,
|
|
527
|
+
undefined,
|
|
528
|
+
ERROR_CODES.ERR_TEMPLATE_GET,
|
|
529
|
+
);
|
|
411
530
|
}
|
|
412
531
|
|
|
413
532
|
if (source == null) {
|
|
@@ -449,7 +568,14 @@ export async function getTemplateById(id, options = {}) {
|
|
|
449
568
|
* @returns {Promise<{type: string, data: unknown}>}
|
|
450
569
|
*/
|
|
451
570
|
export async function template(name, options = {}) {
|
|
452
|
-
const {
|
|
571
|
+
const {
|
|
572
|
+
list = false,
|
|
573
|
+
skeleton = false,
|
|
574
|
+
show = false,
|
|
575
|
+
targetPath,
|
|
576
|
+
type,
|
|
577
|
+
cwd = process.cwd(),
|
|
578
|
+
} = options;
|
|
453
579
|
const templates = await discoverAll();
|
|
454
580
|
|
|
455
581
|
if (list || (!name && !skeleton)) {
|
|
@@ -486,7 +612,11 @@ export async function template(name, options = {}) {
|
|
|
486
612
|
);
|
|
487
613
|
}
|
|
488
614
|
if (!fs.existsSync(match.filePath)) {
|
|
489
|
-
throw new AstryxError(
|
|
615
|
+
throw new AstryxError(
|
|
616
|
+
`No source file found for template "${name}"`,
|
|
617
|
+
undefined,
|
|
618
|
+
ERROR_CODES.ERR_NO_SOURCE,
|
|
619
|
+
);
|
|
490
620
|
}
|
|
491
621
|
const src = fs.readFileSync(match.filePath, 'utf-8');
|
|
492
622
|
return {
|
|
@@ -501,7 +631,11 @@ export async function template(name, options = {}) {
|
|
|
501
631
|
}
|
|
502
632
|
|
|
503
633
|
if (!fs.existsSync(match.filePath)) {
|
|
504
|
-
throw new AstryxError(
|
|
634
|
+
throw new AstryxError(
|
|
635
|
+
`No source file found for template "${name}"`,
|
|
636
|
+
undefined,
|
|
637
|
+
ERROR_CODES.ERR_NO_SOURCE,
|
|
638
|
+
);
|
|
505
639
|
}
|
|
506
640
|
|
|
507
641
|
if (show || !targetPath) {
|
|
@@ -528,7 +662,11 @@ export async function template(name, options = {}) {
|
|
|
528
662
|
});
|
|
529
663
|
} catch (err) {
|
|
530
664
|
if (err instanceof PathSafetyError) {
|
|
531
|
-
throw new AstryxError(
|
|
665
|
+
throw new AstryxError(
|
|
666
|
+
err.message,
|
|
667
|
+
undefined,
|
|
668
|
+
ERROR_CODES.ERR_PATH_TRAVERSAL,
|
|
669
|
+
);
|
|
532
670
|
}
|
|
533
671
|
throw err;
|
|
534
672
|
}
|
|
@@ -545,9 +683,8 @@ export async function template(name, options = {}) {
|
|
|
545
683
|
outputFilePath = resolvedTarget;
|
|
546
684
|
} else {
|
|
547
685
|
outputDir = resolvedTarget;
|
|
548
|
-
outputFileName =
|
|
549
|
-
? path.basename(match.filePath)
|
|
550
|
-
: 'page.tsx';
|
|
686
|
+
outputFileName =
|
|
687
|
+
match.type === 'block' ? path.basename(match.filePath) : 'page.tsx';
|
|
551
688
|
outputFilePath = path.join(outputDir, outputFileName);
|
|
552
689
|
}
|
|
553
690
|
|
|
@@ -562,6 +699,11 @@ export async function template(name, options = {}) {
|
|
|
562
699
|
const relOutput = path.relative(cwd, outputDir) || '.';
|
|
563
700
|
return {
|
|
564
701
|
type: 'template.copy',
|
|
565
|
-
data: {
|
|
702
|
+
data: {
|
|
703
|
+
template: name,
|
|
704
|
+
outputDir: relOutput,
|
|
705
|
+
fileName: outputFileName,
|
|
706
|
+
filesCopied: 1,
|
|
707
|
+
},
|
|
566
708
|
};
|
|
567
709
|
}
|
|
@@ -74,5 +74,7 @@ describe('template --skeleton component extraction (prefix-agnostic)', () => {
|
|
|
74
74
|
expect(result.data.skeleton.trim().length).toBeGreaterThan(0);
|
|
75
75
|
expect(result.data.skeleton).toMatch(/<[A-Z]\w+/);
|
|
76
76
|
expect(result.data.skeleton).not.toContain('<XDS');
|
|
77
|
+
|
|
78
|
+
expect(result.data.skeleton).toContain('columns={{minWidth: 200}}');
|
|
77
79
|
});
|
|
78
80
|
});
|
|
@@ -17,6 +17,7 @@ const registry = new Map([
|
|
|
17
17
|
['0.0.13', () => import('./transforms/v0.0.13/index.mjs')],
|
|
18
18
|
['0.0.14', () => import('./transforms/v0.0.14/index.mjs')],
|
|
19
19
|
['0.0.15', () => import('./transforms/v0.0.15/index.mjs')],
|
|
20
|
+
['0.1.0', () => import('./transforms/v0.1.0/index.mjs')],
|
|
20
21
|
]);
|
|
21
22
|
|
|
22
23
|
// Re-export from the shared utility so registry callers and other consumers
|
package/src/codemods/runner.mjs
CHANGED
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
import * as fs from 'node:fs';
|
|
12
12
|
import * as path from 'node:path';
|
|
13
13
|
import * as p from '@clack/prompts';
|
|
14
|
-
import {getRunPrefix} from '../utils/package-manager.mjs';
|
|
15
14
|
import {humanLog} from '../lib/json.mjs';
|
|
16
15
|
|
|
17
16
|
// Known corruption patterns that indicate a broken transform.
|
|
@@ -87,39 +86,6 @@ function findSourceFiles(dir) {
|
|
|
87
86
|
return results.sort();
|
|
88
87
|
}
|
|
89
88
|
|
|
90
|
-
/**
|
|
91
|
-
* Detect the project's formatter and run it on changed files.
|
|
92
|
-
* Tries prettier, then biome. Silently skips if none found.
|
|
93
|
-
*
|
|
94
|
-
* @param {string[]} files - Absolute paths to files that were modified
|
|
95
|
-
* @param {boolean} [silent] - Suppress human-facing output
|
|
96
|
-
*/
|
|
97
|
-
async function formatChangedFiles(files, silent = false) {
|
|
98
|
-
if (files.length === 0) return;
|
|
99
|
-
|
|
100
|
-
const {execSync} = await import('node:child_process');
|
|
101
|
-
const fileArgs = files.map(f => `"${f}"`).join(' ');
|
|
102
|
-
const prefix = getRunPrefix();
|
|
103
|
-
|
|
104
|
-
const formatters = [
|
|
105
|
-
{name: 'prettier', cmd: `${prefix} prettier --write ${fileArgs}`},
|
|
106
|
-
{name: 'biome', cmd: `${prefix} biome format --write ${fileArgs}`},
|
|
107
|
-
];
|
|
108
|
-
|
|
109
|
-
for (const {name, cmd} of formatters) {
|
|
110
|
-
try {
|
|
111
|
-
execSync(cmd, {stdio: 'pipe', timeout: 30000});
|
|
112
|
-
if (!silent)
|
|
113
|
-
p.log.info(
|
|
114
|
-
`Formatted ${files.length} file${files.length === 1 ? '' : 's'} with ${name}`,
|
|
115
|
-
);
|
|
116
|
-
return;
|
|
117
|
-
} catch {
|
|
118
|
-
// Formatter not available or failed — try next
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
89
|
/**
|
|
124
90
|
* Validate transform output before writing to disk.
|
|
125
91
|
*
|
|
@@ -162,6 +128,86 @@ export function validateOutput(result, source, j, {parse = true} = {}) {
|
|
|
162
128
|
return {valid: true};
|
|
163
129
|
}
|
|
164
130
|
|
|
131
|
+
function isConfigCodemod(transformEntry) {
|
|
132
|
+
return transformEntry.meta?.codemodType === 'config';
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const CONFIG_CODEMOD_PATHS = new Set([
|
|
136
|
+
'package.json',
|
|
137
|
+
'astryx.config.mjs',
|
|
138
|
+
'xds.config.mjs',
|
|
139
|
+
]);
|
|
140
|
+
|
|
141
|
+
function resolveConfigPath(relativePath) {
|
|
142
|
+
if (!CONFIG_CODEMOD_PATHS.has(relativePath)) {
|
|
143
|
+
throw new Error(`unsupported config codemod path: ${relativePath}`);
|
|
144
|
+
}
|
|
145
|
+
return path.resolve(process.cwd(), relativePath);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function readOptionalConfigFile(relativePath) {
|
|
149
|
+
const fullPath = resolveConfigPath(relativePath);
|
|
150
|
+
if (!fs.existsSync(fullPath)) return null;
|
|
151
|
+
return {path: relativePath, source: fs.readFileSync(fullPath, 'utf-8')};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function getConfigCodemodContext() {
|
|
155
|
+
return {
|
|
156
|
+
packageJson: readOptionalConfigFile('package.json'),
|
|
157
|
+
astryxConfig: readOptionalConfigFile('astryx.config.mjs'),
|
|
158
|
+
xdsConfig: readOptionalConfigFile('xds.config.mjs'),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function runConfigCodemod(transformEntry, {apply, log}) {
|
|
163
|
+
const {transform} = transformEntry;
|
|
164
|
+
const api = {config: getConfigCodemodContext()};
|
|
165
|
+
const result = await transform({path: process.cwd(), source: ''}, api);
|
|
166
|
+
const errors = result?.errors ?? [];
|
|
167
|
+
if (errors.length > 0) {
|
|
168
|
+
for (const error of errors) {
|
|
169
|
+
log.error(` ✗ ${error.file ?? 'config'} — ${error.error}`);
|
|
170
|
+
}
|
|
171
|
+
return {filesChanged: 0, errors};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const changes = result?.changes ?? [];
|
|
175
|
+
if (changes.length === 0) return {filesChanged: 0, errors: []};
|
|
176
|
+
|
|
177
|
+
for (const change of changes) {
|
|
178
|
+
const fullPath = resolveConfigPath(change.path);
|
|
179
|
+
if (change.delete) {
|
|
180
|
+
if (apply) fs.rmSync(fullPath, {force: true});
|
|
181
|
+
log[apply ? 'success' : 'warn'](
|
|
182
|
+
` ${apply ? '✓' : '~'} ${change.path} (delete${apply ? '' : ', dry run'})`,
|
|
183
|
+
);
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (apply) {
|
|
188
|
+
fs.writeFileSync(fullPath, change.source, 'utf-8');
|
|
189
|
+
}
|
|
190
|
+
log[apply ? 'success' : 'warn'](
|
|
191
|
+
` ${apply ? '✓' : '~'} ${change.path}${apply ? '' : ' (would change)'}`,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (changes.length > 0) {
|
|
196
|
+
const verb = apply ? 'Updated' : 'Would update';
|
|
197
|
+
log.info(
|
|
198
|
+
` ${verb} ${changes.length} config file${changes.length === 1 ? '' : 's'}`,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
filesChanged: changes.length,
|
|
204
|
+
writtenFiles: changes
|
|
205
|
+
.filter(change => !change.delete && path.extname(change.path) !== '.json')
|
|
206
|
+
.map(change => resolveConfigPath(change.path)),
|
|
207
|
+
errors: [],
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
165
211
|
/**
|
|
166
212
|
* Run codemods against source files.
|
|
167
213
|
*
|
|
@@ -197,18 +243,12 @@ export async function runCodemods(
|
|
|
197
243
|
|
|
198
244
|
if (files.length === 0) {
|
|
199
245
|
log.warn('No source files found.');
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
errors: [],
|
|
205
|
-
writtenFiles: [],
|
|
206
|
-
skippedOptional: [],
|
|
207
|
-
};
|
|
246
|
+
} else {
|
|
247
|
+
log.info(
|
|
248
|
+
`Found ${files.length} source file${files.length === 1 ? '' : 's'}`,
|
|
249
|
+
);
|
|
208
250
|
}
|
|
209
251
|
|
|
210
|
-
log.info(`Found ${files.length} source file${files.length === 1 ? '' : 's'}`);
|
|
211
|
-
|
|
212
252
|
// Dynamically import jscodeshift
|
|
213
253
|
const jscodeshift = (await import('jscodeshift')).default;
|
|
214
254
|
|
|
@@ -239,6 +279,24 @@ export async function runCodemods(
|
|
|
239
279
|
|
|
240
280
|
log.info(` ${meta.title}`);
|
|
241
281
|
|
|
282
|
+
if (isConfigCodemod(transformEntry)) {
|
|
283
|
+
const result = await runConfigCodemod(transformEntry, {apply, log});
|
|
284
|
+
if (result.errors.length > 0) {
|
|
285
|
+
for (const error of result.errors) {
|
|
286
|
+
errors.push({
|
|
287
|
+
file: error.file ?? 'config',
|
|
288
|
+
codemod: name,
|
|
289
|
+
error: error.error,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
} else if (result.filesChanged > 0) {
|
|
293
|
+
totalFilesChanged += result.filesChanged;
|
|
294
|
+
totalTransformsApplied += result.filesChanged;
|
|
295
|
+
writtenFiles.push(...(result.writtenFiles ?? []));
|
|
296
|
+
}
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
|
|
242
300
|
let filesChanged = 0;
|
|
243
301
|
|
|
244
302
|
for (const filePath of files) {
|
|
@@ -323,12 +381,6 @@ export async function runCodemods(
|
|
|
323
381
|
}
|
|
324
382
|
}
|
|
325
383
|
|
|
326
|
-
// Post-codemod formatting: run the project's formatter on changed files
|
|
327
|
-
// so codemods don't introduce style drift (jscodeshift may change quotes, etc.)
|
|
328
|
-
if (apply && writtenFiles.length > 0) {
|
|
329
|
-
await formatChangedFiles(writtenFiles, silent);
|
|
330
|
-
}
|
|
331
|
-
|
|
332
384
|
if (totalValidationBlocked > 0) {
|
|
333
385
|
log.warn(
|
|
334
386
|
`${totalValidationBlocked} file${totalValidationBlocked === 1 ? ' was' : 's were'} blocked by validation — no changes written to ${totalValidationBlocked === 1 ? 'that file' : 'those files'}.`,
|
|
@@ -366,7 +418,9 @@ export async function runCodemods(
|
|
|
366
418
|
if (meta.description) {
|
|
367
419
|
log.info(` ${meta.description}`);
|
|
368
420
|
}
|
|
369
|
-
log.info(
|
|
421
|
+
log.info(
|
|
422
|
+
` Run: astryx upgrade --codemod ${name} --path <dir> --apply`,
|
|
423
|
+
);
|
|
370
424
|
}
|
|
371
425
|
}
|
|
372
426
|
|