@astryxdesign/cli 0.7.2-canary.db3a2e7 → 0.7.2-canary.ebcc404

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 (63) hide show
  1. package/api/component/_adapter.d.mts +27 -1
  2. package/api/component/_adapter.mjs +45 -7
  3. package/api/component/component.mjs +12 -3
  4. package/api/component/component.test.mjs +420 -167
  5. package/api/component/integration-export-name.test.mjs +419 -0
  6. package/api/component/list/list.mjs +42 -4
  7. package/api/gap-report/gap-report.mjs +66 -4
  8. package/api/integration/pack-check.mjs +13 -104
  9. package/api/integration/pack-check.test.mjs +112 -0
  10. package/api/search/search.d.mts +9 -0
  11. package/api/search/search.mjs +90 -10
  12. package/api/search/search.test.mjs +36 -0
  13. package/api/swizzle/copy/copy.mjs +28 -14
  14. package/api/template/template.test.mjs +120 -3
  15. package/api/theme/build/build.family.test.mjs +16 -0
  16. package/api/theme/build/build.mjs +77 -18
  17. package/api/theme/themeBuild.doc.mjs +3 -3
  18. package/assets/codemods/transforms/next/__tests__/migrate-list-edge-compensation-to-edge.test.mjs +120 -0
  19. package/assets/codemods/transforms/next/index.mjs +11 -1
  20. package/assets/codemods/transforms/next/migrate-list-edge-compensation-to-edge.mjs +198 -0
  21. package/assets/docs/tree/component-imports.doc.mjs +8 -0
  22. package/assets/docs/working-with-ai.doc.mjs +43 -4
  23. package/assets/templates/blocks/components/Drawer/DrawerForm.doc.mjs +24 -0
  24. package/assets/templates/blocks/components/Drawer/DrawerForm.tsx +62 -0
  25. package/assets/templates/blocks/components/Drawer/DrawerScrim.doc.mjs +22 -0
  26. package/assets/templates/blocks/components/Drawer/DrawerScrim.tsx +68 -0
  27. package/assets/templates/blocks/components/Drawer/DrawerShowcase.doc.mjs +22 -0
  28. package/assets/templates/blocks/components/Drawer/DrawerShowcase.tsx +41 -0
  29. package/assets/templates/blocks/components/Drawer/DrawerStart.doc.mjs +21 -0
  30. package/assets/templates/blocks/components/Drawer/DrawerStart.tsx +41 -0
  31. package/assets/templates/blocks/components/Drawer/DrawerWidths.doc.mjs +22 -0
  32. package/assets/templates/blocks/components/Drawer/DrawerWidths.tsx +54 -0
  33. package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderShowcase.doc.mjs +15 -0
  34. package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderShowcase.tsx +73 -0
  35. package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderSubtitleEndContent.doc.mjs +22 -0
  36. package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderSubtitleEndContent.tsx +49 -0
  37. package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderWithCloseButton.doc.mjs +21 -0
  38. package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderWithCloseButton.tsx +42 -0
  39. package/assets/templates/pages/detail-page/page.tsx +3 -3
  40. package/assets/templates/pages/table-inbox/page.tsx +10 -8
  41. package/authoring/doctypes/base/type.ts +6 -0
  42. package/authoring/doctypes/component/component.doc.mjs +3 -3
  43. package/authoring/doctypes/component/type.ts +6 -3
  44. package/clients/cli/commands/build-theme.path-safety.test.mjs +84 -0
  45. package/clients/cli/commands/build-theme.registry.test.mjs +5 -2
  46. package/clients/cli/commands/theme-build.doc.mjs +2 -2
  47. package/clients/cli/lib/component-format.mjs +5 -1
  48. package/foundation/discovery/component-discovery.d.mts +95 -6
  49. package/foundation/discovery/component-discovery.export-names.test.mjs +293 -0
  50. package/foundation/discovery/component-discovery.mjs +524 -49
  51. package/foundation/discovery/component-discovery.test.mjs +96 -0
  52. package/foundation/discovery/component-replacement.mjs +18 -0
  53. package/foundation/discovery/component-replacement.test.mjs +20 -0
  54. package/foundation/discovery/module-exports.d.mts +21 -0
  55. package/foundation/discovery/module-exports.mjs +343 -31
  56. package/foundation/discovery/module-exports.test.mjs +172 -0
  57. package/foundation/discovery/template-adapter.mjs +74 -5
  58. package/foundation/doc-compiler/doc-loads.test.mjs +5 -1
  59. package/foundation/doc-compiler/inputs.mjs +23 -0
  60. package/foundation/doc-compiler/inputs.test.mjs +0 -1
  61. package/foundation/xle/registry.mjs +4 -0
  62. package/foundation/xle/registry.test.mjs +28 -0
  63. package/package.json +10 -10
@@ -0,0 +1,293 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Integration components are found by the names their docs document;
5
+ * every doc that works today reads exactly as before.
6
+ */
7
+
8
+ import {afterEach, beforeEach, describe, expect, it} from 'vitest';
9
+ import * as fs from 'node:fs';
10
+ import * as os from 'node:os';
11
+ import * as path from 'node:path';
12
+ import {
13
+ discoverIntegrationComponents,
14
+ discoverValidIntegrationComponents,
15
+ findIntegrationComponent,
16
+ findIntegrationComponentDoc,
17
+ findIntegrationComponentSource,
18
+ unscopedComponentOwners,
19
+ } from './component-discovery.mjs';
20
+
21
+ /** @type {string} the integration's components root */
22
+ let root;
23
+
24
+ beforeEach(() => {
25
+ root = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-export-names-'));
26
+ });
27
+
28
+ afterEach(() => {
29
+ fs.rmSync(root, {recursive: true, force: true});
30
+ });
31
+
32
+ /**
33
+ * Write a component doc, and its same-stem source when given.
34
+ * @param {string} file path under the root, without extension
35
+ * @param {{name: string, source?: string, form?: 'default' | 'docs', suffix?: string, extra?: string}} options
36
+ * `name` is written as a JavaScript expression
37
+ */
38
+ function writeComponent(
39
+ file,
40
+ {name, source, form = 'default', suffix = '.doc.mjs', extra = ''},
41
+ ) {
42
+ const target = path.join(root, file);
43
+ fs.mkdirSync(path.dirname(target), {recursive: true});
44
+ const doc = `{\n type: 'component',\n name: ${name},\n usage: {description: 'A component.'},\n props: [],${extra}\n}`;
45
+ fs.writeFileSync(
46
+ `${target}${suffix}`,
47
+ form === 'default'
48
+ ? `export default ${doc};\n`
49
+ : `export const docs = ${doc};\n`,
50
+ );
51
+ if (source != null) fs.writeFileSync(`${target}.tsx`, source);
52
+ }
53
+
54
+ const integration = () => ({name: '@acme/widgets', components: root});
55
+
56
+ /** @returns {string[]} */
57
+ const names = () =>
58
+ discoverIntegrationComponents(integration())
59
+ .map(record => record.name)
60
+ .sort();
61
+
62
+ describe('integration components whose doc names another export of its file', () => {
63
+ it('records a doc beside a kebab-case file under the export its doc names', async () => {
64
+ writeComponent('artifact-viewer', {
65
+ name: "'AcmeArtifactViewer'",
66
+ source: 'export function AcmeArtifactViewer() { return null; }\n',
67
+ });
68
+
69
+ const [record] = discoverIntegrationComponents(integration());
70
+ expect(record).toMatchObject({
71
+ name: 'AcmeArtifactViewer',
72
+ package: '@acme/widgets',
73
+ docPath: path.join(root, 'artifact-viewer.doc.mjs'),
74
+ sourcePath: path.join(root, 'artifact-viewer.tsx'),
75
+ });
76
+ expect(
77
+ (await discoverValidIntegrationComponents(integration())).components,
78
+ ).toEqual([expect.objectContaining({name: 'AcmeArtifactViewer'})]);
79
+ expect(
80
+ findIntegrationComponentDoc(integration(), 'AcmeArtifactViewer'),
81
+ ).toBe(record.docPath);
82
+ expect(findIntegrationComponentDoc(integration(), 'artifact-viewer')).toBe(
83
+ record.docPath,
84
+ );
85
+ expect(
86
+ findIntegrationComponentSource(integration(), 'AcmeArtifactViewer'),
87
+ ).toBe(record.sourcePath);
88
+ });
89
+
90
+ it('leaves a name to Core or a file-named component over one a doc records', () => {
91
+ const core = {package: '@astryxdesign/core', hold: 'core'};
92
+ const fileNamed = {package: '@beta/kit', hold: 'file'};
93
+ const documented = {package: '@acme/widgets', hold: 'doc'};
94
+ expect(unscopedComponentOwners([documented, fileNamed])).toEqual([
95
+ fileNamed,
96
+ ]);
97
+ expect(unscopedComponentOwners([core, documented])).toEqual([core]);
98
+ expect(unscopedComponentOwners([documented])).toEqual([documented]);
99
+
100
+ // Badge.doc.mjs records AcmeBadge, so the doc holds both names only
101
+ // because it documents them; Panel.doc.mjs names an export another file
102
+ // has, so it keeps its file name.
103
+ writeComponent('Badge', {
104
+ name: "'AcmeBadge'",
105
+ source: 'export function AcmeBadge() { return null; }\n',
106
+ });
107
+ writeComponent('Panel', {
108
+ name: "'AcmeChip'",
109
+ source: 'export function AcmeChip() { return null; }\n',
110
+ });
111
+ writeComponent('AcmeChip', {
112
+ name: "'AcmeChip'",
113
+ source: 'export function AcmeChip() { return null; }\n',
114
+ });
115
+ const hold = (/** @type {string} */ name) =>
116
+ findIntegrationComponent(integration(), name)?.hold;
117
+ expect(hold('AcmeBadge')).toBe('doc');
118
+ expect(hold('Badge')).toBe('doc');
119
+ expect(hold('Panel')).toBe('file');
120
+ expect(hold('AcmeChip')).toBe('file');
121
+ });
122
+
123
+ it('takes no name a hidden doc or a replacement in the package claims', () => {
124
+ // A hidden doc is still the file named AcmeCarousel: its name stays its own.
125
+ writeComponent('AcmeCarousel', {
126
+ name: "'AcmeCarousel'",
127
+ extra: '\n hidden: true,',
128
+ source: 'export function AcmeCarousel() { return null; }\n',
129
+ });
130
+ writeComponent('gallery/Slides', {
131
+ name: "'AcmeCarousel'",
132
+ source: 'export function AcmeCarousel() { return null; }\n',
133
+ });
134
+ // A doc in the package replaces Carousel, so that name answers with it.
135
+ writeComponent('Pager', {
136
+ name: "'AcmePager'",
137
+ extra: "\n replaces: 'Carousel',",
138
+ source: 'export function AcmePager() { return null; }\n',
139
+ });
140
+ writeComponent('Kit', {
141
+ name: "'Carousel'",
142
+ source: 'export function Carousel() { return null; }\n',
143
+ });
144
+
145
+ expect(names()).toEqual(['AcmePager', 'Kit', 'Slides']);
146
+ expect(findIntegrationComponentDoc(integration(), 'AcmeCarousel')).toBe(
147
+ path.join(root, 'AcmeCarousel.doc.mjs'),
148
+ );
149
+ expect(findIntegrationComponentDoc(integration(), 'Carousel')).toBeNull();
150
+ });
151
+
152
+ it('settles a name two docs document by path, the same in every locale', () => {
153
+ // By UTF-16 code unit `B/` sorts before `a/`; a locale's collation puts
154
+ // `a/` first.
155
+ for (const [file, stem] of [
156
+ ['a/Alpha', 'Alpha'],
157
+ ['B/Beta', 'Beta'],
158
+ ]) {
159
+ writeComponent(file, {
160
+ name: "'Twin'",
161
+ source: `export function Twin() { return null; }\n// ${stem}\n`,
162
+ });
163
+ }
164
+
165
+ expect(names()).toEqual(['Alpha', 'Twin']);
166
+ expect(findIntegrationComponentDoc(integration(), 'Twin')).toBe(
167
+ path.join(root, 'B', 'Beta.doc.mjs'),
168
+ );
169
+ });
170
+
171
+ it('reads a name a spread can override, and reports an unreadable doc under its file name', async () => {
172
+ fs.writeFileSync(
173
+ path.join(root, 'Based.doc.mjs'),
174
+ "const base = {\n name: 'Based',\n type: 'component',\n usage: {description: 'x'},\n props: [],\n};\nexport default {...base, name: 'AcmeBased'};\n",
175
+ );
176
+ fs.writeFileSync(
177
+ path.join(root, 'Based.tsx'),
178
+ 'export function AcmeBased() { return null; }\n',
179
+ );
180
+ writeComponent('Throws', {
181
+ name: "'AcmeThrows'",
182
+ source: 'export function AcmeThrows() { return null; }\n',
183
+ });
184
+ const throws = path.join(root, 'Throws.doc.mjs');
185
+ fs.writeFileSync(
186
+ throws,
187
+ `throw new Error('boom');\n${fs.readFileSync(throws, 'utf-8')}`,
188
+ );
189
+
190
+ expect(names()).toEqual(['AcmeBased', 'AcmeThrows']);
191
+ const {errors} = await discoverValidIntegrationComponents(integration());
192
+ expect(errors).toEqual([
193
+ {
194
+ name: 'Throws',
195
+ message: 'Component "Throws" has invalid metadata: boom',
196
+ },
197
+ ]);
198
+ });
199
+
200
+ it('reads a legacy export, a .doc.ts doc and a re-exported component the same way', () => {
201
+ writeComponent('Slides', {
202
+ name: "'AcmeCarousel'",
203
+ form: 'docs',
204
+ source: "export {AcmeCarousel} from './carousel/impl';\n",
205
+ });
206
+ fs.mkdirSync(path.join(root, 'carousel'));
207
+ fs.writeFileSync(
208
+ path.join(root, 'carousel', 'impl.tsx'),
209
+ 'export function AcmeCarousel() { return null; }\n',
210
+ );
211
+ writeComponent('Pager', {
212
+ name: "'AcmePager'",
213
+ suffix: '.doc.ts',
214
+ source: 'export const AcmePager = () => null;\n',
215
+ });
216
+
217
+ expect(names()).toEqual(['AcmeCarousel', 'AcmePager']);
218
+ });
219
+
220
+ it('keeps the file name for every doc that reads as before', async () => {
221
+ // Named after its file.
222
+ writeComponent('AcmeBadge', {
223
+ name: "'AcmeBadge'",
224
+ source: 'export function AcmeBadge() { return null; }\n',
225
+ });
226
+ // The file exports its own name, so that name stays.
227
+ writeComponent('Chip', {
228
+ name: "'AcmeChip'",
229
+ source:
230
+ 'export function Chip() { return null; }\nexport function AcmeChip() { return null; }\n',
231
+ });
232
+ // The doc names an export the file does not have.
233
+ writeComponent('Menu', {
234
+ name: "'AcmeMenu'",
235
+ source: 'export function Dropdown() { return null; }\n',
236
+ });
237
+ // A type is not a runtime export.
238
+ writeComponent('Shape', {
239
+ name: "'AcmeShape'",
240
+ source: 'export type AcmeShape = {id: string};\n',
241
+ });
242
+ // A name the doc computes is not read.
243
+ writeComponent('Tile', {
244
+ name: "`${'Acme'}Tile`",
245
+ source: 'export function AcmeTile() { return null; }\n',
246
+ });
247
+ // Another file already has the name.
248
+ writeComponent('Panel', {
249
+ name: "'AcmeBadge'",
250
+ source: 'export function AcmeBadge() { return null; }\n',
251
+ });
252
+ // No source beside it.
253
+ writeComponent('Orphan', {name: "'AcmeOrphan'"});
254
+
255
+ expect(names()).toEqual([
256
+ 'AcmeBadge',
257
+ 'Chip',
258
+ 'Menu',
259
+ 'Orphan',
260
+ 'Panel',
261
+ 'Shape',
262
+ 'Tile',
263
+ ]);
264
+ for (const name of [
265
+ 'AcmeChip',
266
+ 'AcmeMenu',
267
+ 'AcmeShape',
268
+ 'AcmeTile',
269
+ 'AcmeOrphan',
270
+ ]) {
271
+ expect(findIntegrationComponentDoc(integration(), name)).toBeNull();
272
+ }
273
+ expect(
274
+ (await discoverValidIntegrationComponents(integration())).errors,
275
+ ).toEqual([
276
+ {
277
+ name: 'Orphan',
278
+ message:
279
+ 'Component "Orphan" is missing its same-stem source file Orphan.tsx.',
280
+ },
281
+ ]);
282
+ });
283
+
284
+ it('still leaves a hidden doc out', () => {
285
+ writeComponent('Slides', {
286
+ name: "'AcmeCarousel'",
287
+ extra: '\n hidden: true,',
288
+ source: 'export function AcmeCarousel() { return null; }\n',
289
+ });
290
+
291
+ expect(names()).toEqual([]);
292
+ });
293
+ });