@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
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
import {afterEach, beforeEach, describe, expect, it} from 'vitest';
|
|
4
|
+
import * as fs from 'node:fs';
|
|
5
|
+
import * as path from 'node:path';
|
|
6
|
+
import {loadConfig} from './config.mjs';
|
|
7
|
+
import {discover} from '../api/discover.mjs';
|
|
8
|
+
import {loadGapReportConfig} from '../utils/github.mjs';
|
|
9
|
+
import {getTemplateById} from '../api/template.mjs';
|
|
10
|
+
|
|
11
|
+
let tmpDir;
|
|
12
|
+
let originalCwd;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
originalCwd = process.cwd();
|
|
16
|
+
tmpDir = fs.mkdtempSync(
|
|
17
|
+
path.join(process.cwd(), '.astryx-integration-test-'),
|
|
18
|
+
);
|
|
19
|
+
fs.mkdirSync(path.join(tmpDir, 'node_modules', '@acme', 'widgets'), {
|
|
20
|
+
recursive: true,
|
|
21
|
+
});
|
|
22
|
+
fs.writeFileSync(
|
|
23
|
+
path.join(tmpDir, 'astryx.config.mjs'),
|
|
24
|
+
`export default { integrations: '@acme/widgets' };\n`,
|
|
25
|
+
);
|
|
26
|
+
fs.writeFileSync(
|
|
27
|
+
path.join(tmpDir, 'node_modules', '@acme', 'widgets', 'package.json'),
|
|
28
|
+
JSON.stringify({
|
|
29
|
+
name: '@acme/widgets',
|
|
30
|
+
version: '1.2.3',
|
|
31
|
+
displayName: 'Acme Widgets',
|
|
32
|
+
astryx: {integration: './astryx.integration.mjs'},
|
|
33
|
+
}),
|
|
34
|
+
);
|
|
35
|
+
fs.writeFileSync(
|
|
36
|
+
path.join(
|
|
37
|
+
tmpDir,
|
|
38
|
+
'node_modules',
|
|
39
|
+
'@acme',
|
|
40
|
+
'widgets',
|
|
41
|
+
'astryx.integration.mjs',
|
|
42
|
+
),
|
|
43
|
+
`export default {
|
|
44
|
+
name: '@acme/widgets',
|
|
45
|
+
version: '1.2.3',
|
|
46
|
+
displayName: 'Acme Widgets',
|
|
47
|
+
docs: './docs',
|
|
48
|
+
category: 'Acme',
|
|
49
|
+
gapReport: {command: './scripts/report-gap.sh'},
|
|
50
|
+
template: {get: './template.mjs#getTemplate'},
|
|
51
|
+
codemods: [{name: 'noop', from: '0.0.0', to: '9.0.0', transform: './codemod.mjs'}],
|
|
52
|
+
};\n`,
|
|
53
|
+
);
|
|
54
|
+
fs.mkdirSync(path.join(tmpDir, 'node_modules', '@acme', 'widgets', 'docs'));
|
|
55
|
+
fs.writeFileSync(
|
|
56
|
+
path.join(
|
|
57
|
+
tmpDir,
|
|
58
|
+
'node_modules',
|
|
59
|
+
'@acme',
|
|
60
|
+
'widgets',
|
|
61
|
+
'docs',
|
|
62
|
+
'Widget.doc.mjs',
|
|
63
|
+
),
|
|
64
|
+
`export const doc = {
|
|
65
|
+
name: 'Widget',
|
|
66
|
+
usage: {description: 'Acme widget'},
|
|
67
|
+
props: [],
|
|
68
|
+
};\n`,
|
|
69
|
+
);
|
|
70
|
+
fs.mkdirSync(
|
|
71
|
+
path.join(tmpDir, 'node_modules', '@acme', 'widgets', 'scripts'),
|
|
72
|
+
);
|
|
73
|
+
fs.writeFileSync(
|
|
74
|
+
path.join(
|
|
75
|
+
tmpDir,
|
|
76
|
+
'node_modules',
|
|
77
|
+
'@acme',
|
|
78
|
+
'widgets',
|
|
79
|
+
'scripts',
|
|
80
|
+
'report-gap.sh',
|
|
81
|
+
),
|
|
82
|
+
'#!/bin/sh\ncat >/dev/null\necho ok\n',
|
|
83
|
+
);
|
|
84
|
+
fs.writeFileSync(
|
|
85
|
+
path.join(tmpDir, 'node_modules', '@acme', 'widgets', 'template.mjs'),
|
|
86
|
+
`export async function getTemplate(id) { return 'template:' + id; }\n`,
|
|
87
|
+
);
|
|
88
|
+
fs.writeFileSync(
|
|
89
|
+
path.join(tmpDir, 'node_modules', '@acme', 'widgets', 'codemod.mjs'),
|
|
90
|
+
`export default function transform() { return undefined; }\n`,
|
|
91
|
+
);
|
|
92
|
+
process.chdir(tmpDir);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
afterEach(() => {
|
|
96
|
+
process.chdir(originalCwd);
|
|
97
|
+
fs.rmSync(tmpDir, {recursive: true, force: true});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe('configured integrations', () => {
|
|
101
|
+
it('load docs/gap/template/codemod metadata from integration manifests', async () => {
|
|
102
|
+
const config = await loadConfig(tmpDir);
|
|
103
|
+
expect(config.integrations).toEqual(['@acme/widgets']);
|
|
104
|
+
expect(config.loadedIntegrations[0].name).toBe('@acme/widgets');
|
|
105
|
+
expect(config.loadedIntegrations[0].package).toMatchObject({
|
|
106
|
+
name: '@acme/widgets',
|
|
107
|
+
category: 'Acme',
|
|
108
|
+
});
|
|
109
|
+
expect(config.loadedIntegrations[0].codemods[0].transform).toEqual(
|
|
110
|
+
expect.any(Function),
|
|
111
|
+
);
|
|
112
|
+
expect(config.gapReport.command).toBe(
|
|
113
|
+
path.join(
|
|
114
|
+
tmpDir,
|
|
115
|
+
'node_modules',
|
|
116
|
+
'@acme',
|
|
117
|
+
'widgets',
|
|
118
|
+
'scripts',
|
|
119
|
+
'report-gap.sh',
|
|
120
|
+
),
|
|
121
|
+
);
|
|
122
|
+
await expect(config.template.get('P123')).resolves.toBe('template:P123');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('makes integration docs discoverable without packages config', async () => {
|
|
126
|
+
const result = await discover(undefined, {});
|
|
127
|
+
expect(result.type).toBe('discover.list');
|
|
128
|
+
expect(result.data).toEqual([
|
|
129
|
+
expect.objectContaining({
|
|
130
|
+
name: '@acme/widgets',
|
|
131
|
+
category: 'Acme',
|
|
132
|
+
components: ['Widget'],
|
|
133
|
+
}),
|
|
134
|
+
]);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('uses integration gap-report and template hooks', async () => {
|
|
138
|
+
await expect(loadGapReportConfig()).resolves.toMatchObject({
|
|
139
|
+
enabled: true,
|
|
140
|
+
command: path.join(
|
|
141
|
+
tmpDir,
|
|
142
|
+
'node_modules',
|
|
143
|
+
'@acme',
|
|
144
|
+
'widgets',
|
|
145
|
+
'scripts',
|
|
146
|
+
'report-gap.sh',
|
|
147
|
+
),
|
|
148
|
+
});
|
|
149
|
+
await expect(getTemplateById('P123', {cwd: tmpDir})).resolves.toEqual({
|
|
150
|
+
type: 'template.get',
|
|
151
|
+
data: {id: 'P123', source: 'template:P123'},
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Levenshtein edit distance — pure, dependency-free.
|
|
5
|
+
*
|
|
6
|
+
* Lives apart from string-utils.mjs (which dynamically imports node:fs/path
|
|
7
|
+
* for component search) so browser-bundled code — the XLE/XLO layout
|
|
8
|
+
* language — can use fuzzy matching without dragging node: schemes into the
|
|
9
|
+
* webpack graph.
|
|
10
|
+
*
|
|
11
|
+
* @input two strings
|
|
12
|
+
* @output edit distance (number)
|
|
13
|
+
* @position lib — shared by string-utils.mjs and lib/xle/validate.mjs
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export function levenshteinDistance(a, b) {
|
|
17
|
+
const m = a.length, n = b.length;
|
|
18
|
+
const dp = Array.from({length: m + 1}, () => Array(n + 1).fill(0));
|
|
19
|
+
for (let i = 0; i <= m; i++) dp[i][0] = i;
|
|
20
|
+
for (let j = 0; j <= n; j++) dp[0][j] = j;
|
|
21
|
+
for (let i = 1; i <= m; i++) {
|
|
22
|
+
for (let j = 1; j <= n; j++) {
|
|
23
|
+
dp[i][j] = a[i-1] === b[j-1]
|
|
24
|
+
? dp[i-1][j-1]
|
|
25
|
+
: 1 + Math.min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return dp[m][n];
|
|
29
|
+
}
|
package/src/lib/manifest.mjs
CHANGED
|
@@ -64,6 +64,9 @@ export const RESPONSE_TYPES = {
|
|
|
64
64
|
'gap-report': ['gap-report.categories', 'gap-report.dryRun', 'gap-report.file'],
|
|
65
65
|
manifest: ['manifest'],
|
|
66
66
|
doctor: ['doctor'],
|
|
67
|
+
'layout expand': ['layout.expand'],
|
|
68
|
+
'layout check': ['layout.check'],
|
|
69
|
+
'layout grammar': ['layout.grammar'],
|
|
67
70
|
};
|
|
68
71
|
|
|
69
72
|
/**
|
|
@@ -83,6 +86,9 @@ const EXAMPLES = {
|
|
|
83
86
|
manifest: ['astryx manifest --json', 'astryx --json'],
|
|
84
87
|
doctor: ['astryx doctor', 'astryx doctor --json'],
|
|
85
88
|
init: ['astryx init'],
|
|
89
|
+
'layout expand': [`astryx layout expand 'V[g6] > C{card-callout}*4' ./src/Page.tsx`],
|
|
90
|
+
'layout check': [`astryx layout check 'A[cp6] > L > LC > S[p6]' --json`],
|
|
91
|
+
'layout grammar': ['astryx layout grammar'],
|
|
86
92
|
};
|
|
87
93
|
|
|
88
94
|
/**
|
|
@@ -15,7 +15,11 @@ export function scanDirectory(scanDir) {
|
|
|
15
15
|
const pkgPath = path.join(scanDir, entry.name, 'package.json');
|
|
16
16
|
if (!fs.existsSync(pkgPath)) continue;
|
|
17
17
|
let pkg;
|
|
18
|
-
try {
|
|
18
|
+
try {
|
|
19
|
+
pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
20
|
+
} catch {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
19
23
|
if (!pkg.astryx || !pkg.astryx.docs) continue;
|
|
20
24
|
const pkgDir = path.join(scanDir, entry.name);
|
|
21
25
|
const docsDir = path.resolve(pkgDir, pkg.astryx.docs);
|
|
@@ -36,9 +40,18 @@ export function scanDirectory(scanDir) {
|
|
|
36
40
|
return packages;
|
|
37
41
|
}
|
|
38
42
|
|
|
39
|
-
export function scanAllPackages(packageDirs) {
|
|
43
|
+
export function scanAllPackages(packageDirs, explicitPackages = []) {
|
|
40
44
|
const all = [];
|
|
41
45
|
const seen = new Set();
|
|
46
|
+
|
|
47
|
+
for (const pkg of explicitPackages) {
|
|
48
|
+
if (!pkg || seen.has(pkg.name)) continue;
|
|
49
|
+
const components = discoverDocComponents(pkg.docsDir);
|
|
50
|
+
if (components.length === 0) continue;
|
|
51
|
+
seen.add(pkg.name);
|
|
52
|
+
all.push({...pkg, components});
|
|
53
|
+
}
|
|
54
|
+
|
|
42
55
|
for (const dir of packageDirs) {
|
|
43
56
|
for (const pkg of scanDirectory(dir)) {
|
|
44
57
|
if (seen.has(pkg.name)) continue;
|
|
@@ -54,12 +67,17 @@ function discoverDocComponents(docsDir) {
|
|
|
54
67
|
const components = [];
|
|
55
68
|
function walk(dir) {
|
|
56
69
|
let entries;
|
|
57
|
-
try {
|
|
70
|
+
try {
|
|
71
|
+
entries = fs.readdirSync(dir, {withFileTypes: true});
|
|
72
|
+
} catch {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
58
75
|
for (const entry of entries) {
|
|
59
76
|
if (entry.name === 'node_modules' || entry.name === '__tests__') continue;
|
|
60
77
|
const full = path.join(dir, entry.name);
|
|
61
78
|
if (entry.isDirectory()) walk(full);
|
|
62
|
-
else if (entry.name.endsWith('.doc.mjs'))
|
|
79
|
+
else if (entry.name.endsWith('.doc.mjs'))
|
|
80
|
+
components.push(entry.name.replace('.doc.mjs', ''));
|
|
63
81
|
}
|
|
64
82
|
}
|
|
65
83
|
walk(docsDir);
|
|
@@ -81,12 +99,18 @@ function findDocFile(docsDir, name) {
|
|
|
81
99
|
const target = name + '.doc.mjs';
|
|
82
100
|
function walk(dir) {
|
|
83
101
|
let entries;
|
|
84
|
-
try {
|
|
102
|
+
try {
|
|
103
|
+
entries = fs.readdirSync(dir, {withFileTypes: true});
|
|
104
|
+
} catch {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
85
107
|
for (const entry of entries) {
|
|
86
108
|
if (entry.name === 'node_modules' || entry.name === '__tests__') continue;
|
|
87
109
|
const full = path.join(dir, entry.name);
|
|
88
|
-
if (entry.isDirectory()) {
|
|
89
|
-
|
|
110
|
+
if (entry.isDirectory()) {
|
|
111
|
+
const f = walk(full);
|
|
112
|
+
if (f) return f;
|
|
113
|
+
} else if (entry.name === target) return full;
|
|
90
114
|
}
|
|
91
115
|
return null;
|
|
92
116
|
}
|
package/src/lib/string-utils.mjs
CHANGED
|
@@ -4,20 +4,11 @@
|
|
|
4
4
|
* @file String utilities — fuzzy matching and semantic search for component names
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
for (let i = 1; i <= m; i++) {
|
|
13
|
-
for (let j = 1; j <= n; j++) {
|
|
14
|
-
dp[i][j] = a[i-1] === b[j-1]
|
|
15
|
-
? dp[i-1][j-1]
|
|
16
|
-
: 1 + Math.min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]);
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
return dp[m][n];
|
|
20
|
-
}
|
|
7
|
+
// levenshteinDistance lives in its own pure module so browser-bundled code
|
|
8
|
+
// (the XLE/XLO language) can use it without this file's dynamic node:fs/path
|
|
9
|
+
// imports landing in the webpack graph. Re-exported for existing consumers.
|
|
10
|
+
import {levenshteinDistance} from './levenshtein.mjs';
|
|
11
|
+
export {levenshteinDistance} from './levenshtein.mjs';
|
|
21
12
|
|
|
22
13
|
/**
|
|
23
14
|
* Find the closest component names to a given (possibly misspelled) name.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/** Type surface for `@astryxdesign/cli/xle` — the browser-safe layout language. */
|
|
4
|
+
|
|
5
|
+
export interface XLEIssue {
|
|
6
|
+
message: string;
|
|
7
|
+
line?: number;
|
|
8
|
+
col?: number;
|
|
9
|
+
formatted: string;
|
|
10
|
+
suggestions?: string[];
|
|
11
|
+
kind?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface SerializedRegistry {
|
|
15
|
+
components: Array<{
|
|
16
|
+
name: string;
|
|
17
|
+
exportName: string;
|
|
18
|
+
dirName: string;
|
|
19
|
+
importPath: string;
|
|
20
|
+
undocumented?: boolean;
|
|
21
|
+
props: Array<Record<string, unknown>>;
|
|
22
|
+
}>;
|
|
23
|
+
aliases: Array<[string, string]>;
|
|
24
|
+
componentNames: string[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface XLEBlock {
|
|
28
|
+
dirName: string;
|
|
29
|
+
description?: string;
|
|
30
|
+
category?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface CheckOptions {
|
|
34
|
+
blocks?: XLEBlock[];
|
|
35
|
+
form?: 'auto' | 'compact' | 'outline';
|
|
36
|
+
loose?: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface CheckResult {
|
|
40
|
+
ok: boolean;
|
|
41
|
+
valid?: boolean;
|
|
42
|
+
form?: 'compact' | 'outline';
|
|
43
|
+
errors: XLEIssue[];
|
|
44
|
+
warnings: string[];
|
|
45
|
+
compact?: string;
|
|
46
|
+
outline?: string;
|
|
47
|
+
parseError?: {message: string; line?: number; col?: number};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface ExpandOptions extends CheckOptions {
|
|
51
|
+
name?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type ExpandResult =
|
|
55
|
+
| {ok: false; errors: XLEIssue[]}
|
|
56
|
+
| {
|
|
57
|
+
ok: true;
|
|
58
|
+
form: 'compact' | 'outline';
|
|
59
|
+
code: string;
|
|
60
|
+
componentsUsed: string[];
|
|
61
|
+
states: number;
|
|
62
|
+
todos: string[];
|
|
63
|
+
warnings: string[];
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export function checkExpression(
|
|
67
|
+
expression: string,
|
|
68
|
+
registry: SerializedRegistry | object,
|
|
69
|
+
opts?: CheckOptions,
|
|
70
|
+
): CheckResult;
|
|
71
|
+
|
|
72
|
+
export function expandExpression(
|
|
73
|
+
expression: string,
|
|
74
|
+
registry: SerializedRegistry | object,
|
|
75
|
+
opts?: ExpandOptions,
|
|
76
|
+
): ExpandResult;
|
|
77
|
+
|
|
78
|
+
export function hydrateRegistry(json: SerializedRegistry): object;
|
|
79
|
+
export function serializeRegistry(registry: object): SerializedRegistry;
|
|
80
|
+
export function parse(source: string, opts?: {form?: string}): unknown;
|
|
81
|
+
export function detectForm(source: string): 'compact' | 'outline';
|
|
82
|
+
export function validate(doc: unknown, registry: object, blocks?: XLEBlock[], opts?: {loose?: boolean}): {errors: XLEIssue[]; warnings: XLEIssue[]};
|
|
83
|
+
export function expand(doc: unknown, registry: object, opts?: {componentName?: string}): {code: string; componentsUsed: string[]; states: number; todos: string[]};
|
|
84
|
+
export function toCompact(doc: unknown): string;
|
|
85
|
+
export function toOutline(doc: unknown): string;
|
|
86
|
+
export const ALIAS_TABLE: Record<string, string>;
|
|
87
|
+
export const SPACING_STEPS: number[];
|
|
88
|
+
export class XLEParseError extends Error {
|
|
89
|
+
line: number;
|
|
90
|
+
col: number;
|
|
91
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file XLE/XLO browser barrel — the layout language without Node deps.
|
|
5
|
+
*
|
|
6
|
+
* The CLI builds its registry by reading .doc.mjs from disk (registry.mjs,
|
|
7
|
+
* Node-only). The browser playground instead receives a *serialized*
|
|
8
|
+
* registry (built at deploy time) and hydrates it here, then runs the same
|
|
9
|
+
* pure parse → validate → expand → print pipeline. Importing this module
|
|
10
|
+
* never pulls in node:fs, so it bundles cleanly for the web.
|
|
11
|
+
*
|
|
12
|
+
* @input expression string + a serialized-or-hydrated registry (+ blocks)
|
|
13
|
+
* @output checkExpression / expandExpression + the raw pipeline pieces
|
|
14
|
+
* @position lib/xle — exported as `@astryxdesign/cli/xle`
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import {parse, detectForm, XLEParseError} from './parse.mjs';
|
|
18
|
+
import {validate} from './validate.mjs';
|
|
19
|
+
import {expand} from './expand.mjs';
|
|
20
|
+
import {toCompact, toOutline} from './print.mjs';
|
|
21
|
+
import {hydrateRegistry} from './registry-core.mjs';
|
|
22
|
+
|
|
23
|
+
export {parse, detectForm, XLEParseError, validate, expand, toCompact, toOutline};
|
|
24
|
+
export {hydrateRegistry, serializeRegistry, ALIAS_TABLE, resolveComponent, SPACING_STEPS} from './registry-core.mjs';
|
|
25
|
+
|
|
26
|
+
/** Accept either a hydrated registry (Map fields) or serialized JSON. */
|
|
27
|
+
function asRegistry(registry) {
|
|
28
|
+
return registry && registry.components instanceof Map ? registry : hydrateRegistry(registry);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function formatIssue(issue) {
|
|
32
|
+
const where = issue.line != null ? `line ${issue.line}: ` : '';
|
|
33
|
+
return `${where}${issue.message}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Validate an expression and return both canonical surfaces — the browser
|
|
38
|
+
* twin of `layout check`. Never throws on invalid input; returns the errors.
|
|
39
|
+
*
|
|
40
|
+
* @param {string} expression
|
|
41
|
+
* @param {object} registry - serialized or hydrated
|
|
42
|
+
* @param {object} [opts]
|
|
43
|
+
* @param {Array} [opts.blocks] - block list for {hint} resolution (default [])
|
|
44
|
+
* @param {'compact'|'outline'|'auto'} [opts.form]
|
|
45
|
+
* @param {boolean} [opts.loose]
|
|
46
|
+
* @returns {{ok: boolean, valid?: boolean, form?: string, errors: Array, warnings: string[], compact?: string, outline?: string, parseError?: object}}
|
|
47
|
+
*/
|
|
48
|
+
export function checkExpression(expression, registry, opts = {}) {
|
|
49
|
+
const {blocks = [], form = 'auto', loose = false} = opts;
|
|
50
|
+
let doc;
|
|
51
|
+
try {
|
|
52
|
+
doc = parse(expression, {form});
|
|
53
|
+
} catch (e) {
|
|
54
|
+
if (e instanceof XLEParseError) {
|
|
55
|
+
return {
|
|
56
|
+
ok: false,
|
|
57
|
+
valid: false,
|
|
58
|
+
errors: [{message: e.message, line: e.line, col: e.col, formatted: formatIssue(e), kind: 'parse'}],
|
|
59
|
+
warnings: [],
|
|
60
|
+
parseError: {message: e.message, line: e.line, col: e.col},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
throw e;
|
|
64
|
+
}
|
|
65
|
+
const {errors, warnings} = validate(doc, asRegistry(registry), blocks, {loose});
|
|
66
|
+
return {
|
|
67
|
+
ok: true,
|
|
68
|
+
valid: errors.length === 0,
|
|
69
|
+
form: doc.form,
|
|
70
|
+
errors: errors.map(e => ({...e, formatted: formatIssue(e)})),
|
|
71
|
+
warnings: warnings.map(formatIssue),
|
|
72
|
+
compact: toCompact(doc),
|
|
73
|
+
outline: toOutline(doc),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Validate + expand to TSX — the browser twin of `layout expand`.
|
|
79
|
+
* Returns either {code, ...} or {errors} (never writes files).
|
|
80
|
+
*
|
|
81
|
+
* @param {string} expression
|
|
82
|
+
* @param {object} registry
|
|
83
|
+
* @param {object} [opts]
|
|
84
|
+
* @param {Array} [opts.blocks]
|
|
85
|
+
* @param {'compact'|'outline'|'auto'} [opts.form]
|
|
86
|
+
* @param {boolean} [opts.loose]
|
|
87
|
+
* @param {string} [opts.name] - generated component name (PascalCase)
|
|
88
|
+
* @param {Map} [opts.blockModules] - prepared block sources/imports for splicing;
|
|
89
|
+
* omit in the browser (hints stay as TODO markers without sources)
|
|
90
|
+
*/
|
|
91
|
+
export function expandExpression(expression, registry, opts = {}) {
|
|
92
|
+
const {blocks = [], form = 'auto', loose = false, name = 'GeneratedLayout', blockModules} = opts;
|
|
93
|
+
const reg = asRegistry(registry);
|
|
94
|
+
let doc;
|
|
95
|
+
try {
|
|
96
|
+
doc = parse(expression, {form});
|
|
97
|
+
} catch (e) {
|
|
98
|
+
if (e instanceof XLEParseError) {
|
|
99
|
+
return {ok: false, errors: [{message: e.message, line: e.line, col: e.col, formatted: formatIssue(e), kind: 'parse'}]};
|
|
100
|
+
}
|
|
101
|
+
throw e;
|
|
102
|
+
}
|
|
103
|
+
const {errors, warnings} = validate(doc, reg, blocks, {loose});
|
|
104
|
+
if (errors.length > 0) {
|
|
105
|
+
return {ok: false, errors: errors.map(e => ({...e, formatted: formatIssue(e)}))};
|
|
106
|
+
}
|
|
107
|
+
if (!/^[A-Z][A-Za-z0-9]*$/.test(name)) {
|
|
108
|
+
return {ok: false, errors: [{message: `Component name must be PascalCase, got '${name}'`, formatted: `Component name must be PascalCase, got '${name}'`}]};
|
|
109
|
+
}
|
|
110
|
+
const result = expand(doc, reg, {componentName: name, blockModules});
|
|
111
|
+
return {
|
|
112
|
+
ok: true,
|
|
113
|
+
form: form === 'auto' ? detectForm(expression) : form,
|
|
114
|
+
code: result.code,
|
|
115
|
+
componentsUsed: result.componentsUsed,
|
|
116
|
+
states: result.states,
|
|
117
|
+
todos: result.todos,
|
|
118
|
+
warnings: warnings.map(formatIssue),
|
|
119
|
+
};
|
|
120
|
+
}
|