@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.
Files changed (176) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/README.md +117 -75
  3. package/bin/astryx.mjs +22 -7
  4. package/docs/getting-started.doc.mjs +11 -11
  5. package/docs/icons.doc.mjs +1 -1
  6. package/docs/migration.doc.mjs +2 -2
  7. package/docs/shape.doc.mjs +1 -1
  8. package/docs/styling.doc.mjs +3 -4
  9. package/docs/theme.doc.dense.mjs +2 -2
  10. package/docs/theme.doc.mjs +14 -0
  11. package/docs/theme.doc.zh.mjs +2 -2
  12. package/docs/working-with-ai.doc.mjs +4 -4
  13. package/package.json +20 -9
  14. package/src/api/discover.mjs +78 -26
  15. package/src/api/doctor.mjs +3 -3
  16. package/src/api/layout.mjs +301 -0
  17. package/src/api/layout.test.mjs +238 -0
  18. package/src/api/search.mjs +207 -13
  19. package/src/api/template.mjs +193 -51
  20. package/src/api/template.test.mjs +2 -0
  21. package/src/codemods/__tests__/registry.test.mjs +1 -0
  22. package/src/codemods/registry.mjs +1 -0
  23. package/src/codemods/runner.mjs +105 -51
  24. package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-config-surfaces.test.mjs +116 -0
  25. package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-module-specifiers.test.mjs +51 -0
  26. package/src/codemods/transforms/v0.1.0/index.mjs +28 -0
  27. package/src/codemods/transforms/v0.1.0/migrate-xds-config-surfaces.mjs +230 -0
  28. package/src/codemods/transforms/v0.1.0/migrate-xds-module-specifiers.mjs +84 -0
  29. package/src/commands/agent-docs.mjs +119 -66
  30. package/src/commands/agent-docs.path-safety.test.mjs +1 -1
  31. package/src/commands/agent-docs.test.mjs +87 -31
  32. package/src/commands/build-theme.import-path.test.mjs +1 -1
  33. package/src/commands/build-theme.path-safety.test.mjs +1 -1
  34. package/src/commands/build-theme.prose.test.mjs +1 -1
  35. package/src/commands/build.mjs +196 -0
  36. package/src/commands/component-package.test.mjs +1 -1
  37. package/src/commands/component.test.mjs +1 -1
  38. package/src/commands/docs.test.mjs +1 -1
  39. package/src/commands/doctor.test.mjs +1 -1
  40. package/src/commands/external-showcase.test.mjs +1 -1
  41. package/src/commands/gap-report.mjs +17 -9
  42. package/src/commands/gap-report.test.mjs +21 -16
  43. package/src/commands/init.mjs +43 -9
  44. package/src/commands/init.next-steps.test.mjs +46 -0
  45. package/src/commands/interactive-guard.test.mjs +1 -1
  46. package/src/commands/json-contract.test.mjs +10 -3
  47. package/src/commands/layout.mjs +139 -0
  48. package/src/commands/swizzle-gap-safety.test.mjs +1 -1
  49. package/src/commands/swizzle.mjs +51 -23
  50. package/src/commands/swizzle.path-safety.test.mjs +1 -1
  51. package/src/commands/template.path-safety.test.mjs +1 -1
  52. package/src/commands/template.test.mjs +1 -1
  53. package/src/commands/upgrade.mjs +292 -177
  54. package/src/commands/upgrade.test.mjs +41 -27
  55. package/src/config.mjs +31 -0
  56. package/src/config.test.mjs +24 -0
  57. package/src/index.mjs +5 -0
  58. package/src/lib/config-schema.mjs +119 -0
  59. package/src/lib/config.mjs +45 -6
  60. package/src/lib/config.test.mjs +91 -0
  61. package/src/lib/error-codes.mjs +11 -0
  62. package/src/lib/integrations.mjs +155 -0
  63. package/src/lib/integrations.test.mjs +154 -0
  64. package/src/lib/levenshtein.mjs +29 -0
  65. package/src/lib/manifest.mjs +6 -0
  66. package/src/lib/package-scanner.mjs +31 -7
  67. package/src/lib/string-utils.mjs +5 -14
  68. package/src/lib/xle/browser.d.ts +91 -0
  69. package/src/lib/xle/browser.mjs +120 -0
  70. package/src/lib/xle/expand.mjs +622 -0
  71. package/src/lib/xle/parse.mjs +581 -0
  72. package/src/lib/xle/print.mjs +174 -0
  73. package/src/lib/xle/registry-core.mjs +170 -0
  74. package/src/lib/xle/registry.mjs +237 -0
  75. package/src/lib/xle/splice.mjs +137 -0
  76. package/src/lib/xle/validate.mjs +356 -0
  77. package/src/lib/xle/xle.test.mjs +333 -0
  78. package/src/types/config.d.ts +99 -0
  79. package/src/types/error-codes.d.ts +1 -0
  80. package/src/utils/github.mjs +12 -27
  81. package/src/utils/interactive.mjs +1 -1
  82. package/src/utils/interactive.test.mjs +2 -0
  83. package/src/utils/package-manager.mjs +1 -1
  84. package/src/utils/package-manager.test.mjs +1 -1
  85. package/src/utils/path-safety.test.mjs +1 -1
  86. package/src/utils/paths.test.mjs +8 -8
  87. package/src/utils/update-check.mjs +4 -26
  88. package/src/utils/update-check.test.mjs +2 -64
  89. package/templates/blocks/components/AppShell/AppShellContentOnly.tsx +1 -9
  90. package/templates/blocks/components/AppShell/AppShellShowcase.tsx +1 -10
  91. package/templates/blocks/components/AppShell/AppShellSideNavOnly.tsx +1 -9
  92. package/templates/blocks/components/AppShell/AppShellTopNavOnly.tsx +1 -9
  93. package/templates/blocks/components/AppShell/AppShellTopNavWithSideNav.tsx +1 -9
  94. package/templates/blocks/components/AppShell/AppShellWithBanner.tsx +1 -9
  95. package/templates/blocks/components/AspectRatio/AspectRatioShowcase.tsx +12 -19
  96. package/templates/blocks/components/Banner/BannerShowcase.tsx +1 -8
  97. package/templates/blocks/components/Blockquote/BlockquoteShowcase.tsx +1 -8
  98. package/templates/blocks/components/Carousel/CarouselShowcase.tsx +2 -12
  99. package/templates/blocks/components/ChatComposerDrawer/ChatComposerDrawerShowcase.tsx +6 -9
  100. package/templates/blocks/components/ChatLayout/ChatLayoutPanelChat.tsx +10 -12
  101. package/templates/blocks/components/ChatMessageList/ChatMessageListDensity.tsx +1 -9
  102. package/templates/blocks/components/ChatMessageList/ChatMessageListFullFeatured.tsx +1 -9
  103. package/templates/blocks/components/ChatMessageList/ChatMessageListShowcase.tsx +1 -9
  104. package/templates/blocks/components/ChatMessageMetadata/ChatMessageMetadataShowcase.tsx +1 -8
  105. package/templates/blocks/components/ChatSendButton/ChatSendButtonInComposer.tsx +1 -8
  106. package/templates/blocks/components/Citation/CitationInlineText.tsx +4 -4
  107. package/templates/blocks/components/Code/CodeInlineInParagraph.tsx +1 -8
  108. package/templates/blocks/components/CodeBlock/CodeBlockBashCommand.tsx +1 -1
  109. package/templates/blocks/components/CodeBlock/CodeBlockJSONConfig.tsx +1 -1
  110. package/templates/blocks/components/CommandPaletteEmpty/CommandPaletteEmptyShowcase.doc.mjs +15 -0
  111. package/templates/blocks/components/CommandPaletteEmpty/CommandPaletteEmptyShowcase.tsx +26 -0
  112. package/templates/blocks/components/CommandPaletteItem/CommandPaletteItemShowcase.tsx +9 -12
  113. package/templates/blocks/components/ContextMenu/ContextMenuShowcase.tsx +13 -15
  114. package/templates/blocks/components/DateInput/DateInputDateRange.doc.mjs +2 -2
  115. package/templates/blocks/components/Divider/DividerShowcase.tsx +1 -8
  116. package/templates/blocks/components/Divider/DividerVertical.tsx +7 -9
  117. package/templates/blocks/components/Field/FieldShowcase.tsx +1 -8
  118. package/templates/blocks/components/FormLayout/FormLayoutHorizontal.tsx +1 -6
  119. package/templates/blocks/components/Grid/GridResponsiveAutoFit.tsx +1 -9
  120. package/templates/blocks/components/HoverCard/HoverCardInlineTextHoverCard.tsx +4 -6
  121. package/templates/blocks/components/HoverCard/HoverCardInteractiveContent.tsx +1 -6
  122. package/templates/blocks/components/HoverCard/HoverCardProfileHoverCard.tsx +2 -8
  123. package/templates/blocks/components/HoverCard/HoverCardShowcase.tsx +1 -8
  124. package/templates/blocks/components/OverflowList/OverflowListOverflowBadges.tsx +8 -11
  125. package/templates/blocks/components/OverflowList/OverflowListOverflowDropdownActions.tsx +9 -12
  126. package/templates/blocks/components/Overlay/OverlayBottomStrip.tsx +4 -17
  127. package/templates/blocks/components/Overlay/OverlayHoverReveal.tsx +15 -16
  128. package/templates/blocks/components/Overlay/OverlayShowcase.tsx +5 -21
  129. package/templates/blocks/components/Pagination/PaginationDotsCarousel.tsx +2 -14
  130. package/templates/blocks/components/Pagination/PaginationPageSize.tsx +12 -14
  131. package/templates/blocks/components/Pagination/PaginationVariants.tsx +1 -8
  132. package/templates/blocks/components/Pagination/PaginationWithTable.tsx +2 -14
  133. package/templates/blocks/components/Slider/SliderShowcase.tsx +10 -1
  134. package/templates/blocks/components/ToggleButton/ToggleButtonGroup.doc.mjs +1 -1
  135. package/templates/blocks/components/Tokenizer/TokenizerClear.tsx +1 -6
  136. package/templates/blocks/components/Tokenizer/TokenizerCreatable.tsx +2 -7
  137. package/templates/blocks/components/Tokenizer/TokenizerEndContent.tsx +1 -6
  138. package/templates/blocks/components/Tokenizer/TokenizerIcon.tsx +1 -6
  139. package/templates/blocks/components/Tokenizer/TokenizerMaxEntries.tsx +1 -6
  140. package/templates/blocks/components/Tokenizer/TokenizerOverflow.tsx +2 -7
  141. package/templates/blocks/components/Tokenizer/TokenizerShowcase.tsx +1 -6
  142. package/templates/blocks/components/Tokenizer/TokenizerStates.tsx +4 -9
  143. package/templates/blocks/components/Toolbar/ToolbarCardHeader.tsx +1 -10
  144. package/templates/blocks/components/Toolbar/ToolbarSizes.tsx +1 -8
  145. package/templates/blocks/components/Toolbar/ToolbarTableFilter.tsx +1 -8
  146. package/templates/blocks/components/Toolbar/ToolbarThreeSlot.tsx +1 -10
  147. package/templates/blocks/components/Toolbar/ToolbarWithTabs.tsx +8 -11
  148. package/templates/pages/ai-chat/page.tsx +71 -64
  149. package/templates/pages/ai-chat-landing/page.tsx +8 -12
  150. package/templates/pages/centered-hero/page.tsx +13 -15
  151. package/templates/pages/classic-gallery/page.tsx +27 -34
  152. package/templates/pages/detail-page/page.tsx +18 -18
  153. package/templates/pages/documentation/page.tsx +42 -58
  154. package/templates/pages/documentation-design/page.tsx +82 -60
  155. package/templates/pages/documentation-technical/page.tsx +101 -60
  156. package/templates/pages/editor/page.tsx +42 -54
  157. package/templates/pages/file-explorer/page.tsx +13 -16
  158. package/templates/pages/form-two-column/page.tsx +13 -17
  159. package/templates/pages/gallery-hero/page.tsx +13 -15
  160. package/templates/pages/ide/page.tsx +188 -264
  161. package/templates/pages/library/page.tsx +16 -23
  162. package/templates/pages/login/page.tsx +14 -18
  163. package/templates/pages/login-card/page.tsx +14 -18
  164. package/templates/pages/login-split/page.tsx +50 -48
  165. package/templates/pages/login-sso/page.tsx +9 -13
  166. package/templates/pages/mixed-gallery/page.tsx +51 -45
  167. package/templates/pages/payment-form/page.tsx +56 -70
  168. package/templates/pages/product-detail/page.tsx +27 -33
  169. package/templates/pages/product-gallery/page.tsx +7 -13
  170. package/templates/pages/settings-dialog/page.tsx +35 -43
  171. package/templates/pages/settings-sidebar/page.tsx +39 -47
  172. package/templates/pages/side-gallery/page.tsx +6 -9
  173. package/templates/pages/table-grouped/page.tsx +11 -15
  174. package/templates/pages/theme-showcase/page.tsx +33 -37
  175. package/templates/blocks/components/MoreMenu/MoreMenuInToolbar.doc.mjs +0 -14
  176. 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
+ }
@@ -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 { pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')); } catch { continue; }
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 { entries = fs.readdirSync(dir, {withFileTypes: true}); } catch { return; }
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')) components.push(entry.name.replace('.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 { entries = fs.readdirSync(dir, {withFileTypes: true}); } catch { return null; }
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()) { const f = walk(full); if (f) return f; }
89
- else if (entry.name === target) return full;
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
  }
@@ -4,20 +4,11 @@
4
4
  * @file String utilities — fuzzy matching and semantic search for component names
5
5
  */
6
6
 
7
- export function levenshteinDistance(a, b) {
8
- const m = a.length, n = b.length;
9
- const dp = Array.from({length: m + 1}, () => Array(n + 1).fill(0));
10
- for (let i = 0; i <= m; i++) dp[i][0] = i;
11
- for (let j = 0; j <= n; j++) dp[0][j] = j;
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
+ }