@astryxdesign/cli 0.7.2-canary.caec7ca → 0.7.2-canary.cbb7fa3

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 (50) hide show
  1. package/api/component/_adapter.d.mts +41 -1
  2. package/api/component/_adapter.mjs +93 -7
  3. package/api/component/component.mjs +22 -5
  4. package/api/component/component.test.mjs +80 -0
  5. package/api/component/integration-export-name.test.mjs +644 -0
  6. package/api/component/list/list.mjs +42 -4
  7. package/api/gap-report/gap-report.mjs +67 -4
  8. package/api/integration/add-contribution.component-names.test.mjs +66 -0
  9. package/api/integration/add-contribution.mjs +7 -3
  10. package/api/integration/pack-check.mjs +61 -107
  11. package/api/integration/pack-check.test.mjs +359 -0
  12. package/api/search/search.d.mts +9 -0
  13. package/api/search/search.mjs +108 -16
  14. package/api/search/search.test.mjs +36 -0
  15. package/api/swizzle/copy/copy.mjs +28 -14
  16. package/api/theme/build/build.family.test.mjs +16 -0
  17. package/api/theme/build/build.mjs +77 -18
  18. package/api/theme/themeBuild.doc.mjs +3 -3
  19. package/assets/codemods/transforms/next/__tests__/migrate-list-edge-compensation-to-edge.test.mjs +120 -0
  20. package/assets/codemods/transforms/next/index.mjs +11 -1
  21. package/assets/codemods/transforms/next/migrate-list-edge-compensation-to-edge.mjs +198 -0
  22. package/assets/docs/tree/add-a-component.doc.mjs +10 -0
  23. package/assets/docs/tree/checks.doc.mjs +1 -1
  24. package/assets/docs/tree/component-family.doc.mjs +5 -1
  25. package/assets/docs/tree/component-imports.doc.mjs +8 -0
  26. package/assets/docs/tree/single-component.doc.mjs +1 -1
  27. package/assets/docs/tree/troubleshooting.doc.mjs +9 -1
  28. package/assets/docs/tree/versioning.doc.mjs +1 -0
  29. package/assets/docs/working-with-ai.doc.mjs +43 -4
  30. package/authoring/doctypes/component/component.doc.mjs +2 -2
  31. package/authoring/doctypes/component/type.ts +6 -3
  32. package/clients/cli/commands/build-theme.path-safety.test.mjs +84 -0
  33. package/clients/cli/commands/build-theme.registry.test.mjs +5 -2
  34. package/clients/cli/commands/theme-build.doc.mjs +2 -2
  35. package/foundation/discovery/component-discovery.d.mts +172 -10
  36. package/foundation/discovery/component-discovery.export-names.test.mjs +760 -0
  37. package/foundation/discovery/component-discovery.mjs +863 -55
  38. package/foundation/discovery/component-discovery.test.mjs +96 -0
  39. package/foundation/discovery/component-replacement.mjs +18 -0
  40. package/foundation/discovery/component-replacement.test.mjs +40 -0
  41. package/foundation/discovery/module-exports.d.mts +27 -0
  42. package/foundation/discovery/module-exports.mjs +356 -31
  43. package/foundation/discovery/module-exports.test.mjs +172 -0
  44. package/foundation/doc-compiler/doc-loads.test.mjs +4 -0
  45. package/foundation/doc-compiler/inputs.mjs +23 -0
  46. package/foundation/doc-compiler/inputs.test.mjs +0 -1
  47. package/foundation/integrations/contribution-inventory.mjs +6 -1
  48. package/foundation/xle/registry.mjs +4 -0
  49. package/foundation/xle/registry.test.mjs +28 -0
  50. package/package.json +9 -9
@@ -0,0 +1,172 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Static reads of a module's exports, without running it: the file that
5
+ * declares an export, and an exported object literal as plain data.
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 {moduleExportSource, staticExportObject} from './module-exports.mjs';
13
+
14
+ /** @type {string} */
15
+ let dir;
16
+
17
+ beforeEach(() => {
18
+ dir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-export-field-'));
19
+ });
20
+
21
+ afterEach(() => {
22
+ fs.rmSync(dir, {recursive: true, force: true});
23
+ });
24
+
25
+ /**
26
+ * @param {string} file
27
+ * @param {string} source
28
+ * @returns {string}
29
+ */
30
+ function write(file, source) {
31
+ const filePath = path.join(dir, file);
32
+ fs.writeFileSync(filePath, source);
33
+ return filePath;
34
+ }
35
+
36
+ const DOC_EXPORTS = ['default', 'docs'];
37
+
38
+ describe('staticExportObject', () => {
39
+ it('reads a default-exported object literal as plain data', () => {
40
+ const file = write(
41
+ 'A.doc.mjs',
42
+ "export default {type: 'component', name: 'AcmeCarousel', order: 2, hidden: false, extra: null};\n",
43
+ );
44
+ expect(staticExportObject(file, DOC_EXPORTS)).toEqual({
45
+ type: 'component',
46
+ name: 'AcmeCarousel',
47
+ order: 2,
48
+ hidden: false,
49
+ extra: null,
50
+ });
51
+ });
52
+
53
+ it('reads a named export, an export list, and the first export it looks for', () => {
54
+ const named = write(
55
+ 'B.doc.mjs',
56
+ "/** @type {object} */\nexport const docs = {\n name: 'AcmeCarousel',\n};\n",
57
+ );
58
+ const both = write(
59
+ 'C.doc.mjs',
60
+ "export const docs = {name: 'Legacy'};\nexport default {name: 'Stamped'};\n",
61
+ );
62
+ const list = write(
63
+ 'E.doc.mjs',
64
+ "const local = {name: 'AcmeCarousel'};\nexport {local as docs};\n",
65
+ );
66
+ expect(staticExportObject(named, DOC_EXPORTS)?.name).toBe('AcmeCarousel');
67
+ expect(staticExportObject(both, DOC_EXPORTS)?.name).toBe('Stamped');
68
+ expect(staticExportObject(list, DOC_EXPORTS)?.name).toBe('AcmeCarousel');
69
+ });
70
+
71
+ it('follows top-level bindings through TypeScript wrappers, at any depth', () => {
72
+ const file = write(
73
+ 'D.doc.ts',
74
+ [
75
+ "const slide = {name: 'AcmeSlide', description: 'One slide.'} as const;",
76
+ "const doc = {name: 'AcmeCarousel', components: [slide, {name: 'AcmeDots', description: 'Dots.'}]} satisfies {name: string};",
77
+ 'export default doc;',
78
+ '',
79
+ ].join('\n'),
80
+ );
81
+ expect(staticExportObject(file, DOC_EXPORTS)).toEqual({
82
+ name: 'AcmeCarousel',
83
+ components: [
84
+ {name: 'AcmeSlide', description: 'One slide.'},
85
+ {name: 'AcmeDots', description: 'Dots.'},
86
+ ],
87
+ });
88
+ });
89
+
90
+ it('leaves out what it cannot read without running the module', () => {
91
+ const file = write(
92
+ 'F.doc.mjs',
93
+ [
94
+ "const prefix = 'Acme';",
95
+ 'const shared = {group: "Media"};',
96
+ 'export default {',
97
+ ' // A computed string, a call and a spread: none is read.',
98
+ ' name: `${prefix}Carousel`,',
99
+ ' usage: makeUsage(),',
100
+ " group: 'Overridden',",
101
+ ' ...shared,',
102
+ " category: 'Media',",
103
+ " components: [...more, {name: 'AcmeSlide'}],",
104
+ '};',
105
+ '',
106
+ ].join('\n'),
107
+ );
108
+ expect(staticExportObject(file, DOC_EXPORTS)).toEqual({
109
+ category: 'Media',
110
+ components: [{name: 'AcmeSlide'}],
111
+ });
112
+ });
113
+
114
+ it('answers null when there is no exported object', () => {
115
+ const files = [
116
+ write(
117
+ 'G.doc.mjs',
118
+ "export const docs = {name: 'Legacy'};\nexport default makeDoc();\n",
119
+ ),
120
+ write('H.doc.mjs', 'export default {name: '),
121
+ write('I.doc.mjs', "export const other = {name: 'AcmeCarousel'};\n"),
122
+ write('J.doc.mjs', "export default ['AcmeCarousel'];\n"),
123
+ ];
124
+ for (const file of files) {
125
+ expect(staticExportObject(file, DOC_EXPORTS)).toBeNull();
126
+ }
127
+ });
128
+ });
129
+
130
+ describe('moduleExportSource', () => {
131
+ it('names the file that declares an export, following local re-exports', () => {
132
+ const impl = write(
133
+ 'impl.tsx',
134
+ 'export function AcmeSlide() { return null; }\n',
135
+ );
136
+ const barrel = write(
137
+ 'index.ts',
138
+ "export {AcmeSlide} from './impl';\nexport * from './dots';\nexport function AcmeCarousel() { return null; }\n",
139
+ );
140
+ const dots = write('dots.tsx', 'export const AcmeDots = () => null;\n');
141
+ expect(moduleExportSource(barrel, 'AcmeSlide')).toBe(impl);
142
+ expect(moduleExportSource(barrel, 'AcmeDots')).toBe(dots);
143
+ expect(moduleExportSource(barrel, 'AcmeCarousel')).toBe(barrel);
144
+ expect(moduleExportSource(barrel, 'AcmeMissing')).toBeNull();
145
+ });
146
+
147
+ it('does not follow a re-export written in a comment', () => {
148
+ write('legacy.tsx', 'export function AcmeTab() { return null; }\n');
149
+ const barrel = write(
150
+ 'tabs.ts',
151
+ "// export * from './legacy';\nexport function AcmeTabs() { return null; }\n",
152
+ );
153
+ expect(moduleExportSource(barrel, 'AcmeTab')).toBeNull();
154
+ });
155
+
156
+ it('does not count a type-only export', () => {
157
+ const file = write('types.ts', 'export type AcmeShape = {id: string};\n');
158
+ expect(moduleExportSource(file, 'AcmeShape')).toBeNull();
159
+ });
160
+
161
+ it('sees a file npm replaced with one of the same size and mtime', () => {
162
+ // npm stamps every installed file with one mtime and writes new files.
163
+ const npmTime = new Date('1985-10-26T08:15:00Z');
164
+ const file = write('Panel.tsx', 'export function AcmePanel() {}\n');
165
+ fs.utimesSync(file, npmTime, npmTime);
166
+ expect(moduleExportSource(file, 'AcmePanel')).toBe(file);
167
+ const next = write('Panel.next.tsx', 'export function AcmePanol() {}\n');
168
+ fs.utimesSync(next, npmTime, npmTime);
169
+ fs.renameSync(next, file);
170
+ expect(moduleExportSource(file, 'AcmePanel')).toBeNull();
171
+ });
172
+ });
@@ -246,6 +246,10 @@ const RUNNERS = {
246
246
  runs: 'astryx.config, through the config parser',
247
247
  sites: ['loadModuleWithParser(..., parseConfig)'],
248
248
  },
249
+ 'foundation/discovery/component-discovery.mjs': {
250
+ runs: '@babel/parser, on first use, to read the members a family doc lists in its components without running the doc',
251
+ sites: ['require(@babel/parser)'],
252
+ },
249
253
  'foundation/discovery/docs-discovery.mjs': {
250
254
  runs: 'a contributed topic, for its catalog fields: name, title, description, category, and relationships',
251
255
  sites: ['importDocModule'],
@@ -81,8 +81,31 @@ const NON_DESCRIPTOR_DIRS = new Set([
81
81
  export async function collectDocInputs(project) {
82
82
  const list = new InputList();
83
83
 
84
+ // A component documented as a member of its family's doc (HStack and VStack
85
+ // in Stack's doc) is read through the family's own entry: one descriptor,
86
+ // one reader. Own docs go first so the family keeps its name.
87
+ /** @type {Array<{name: string, package: string, docPath: string}>} */
88
+ const ownDocs = [];
89
+ /** @type {Array<{name: string, package: string, docPath: string}>} */
90
+ const familyDocs = [];
84
91
  for (const record of await project.components()) {
85
92
  if (record.docPath == null) continue;
93
+ const stem = path
94
+ .basename(record.docPath)
95
+ .replace(/\.doc\.[cm]?[jt]s$/u, '')
96
+ .replace(/^XDS/u, '');
97
+ const entry = {
98
+ name: record.name,
99
+ package: record.package,
100
+ docPath: record.docPath,
101
+ };
102
+ (stem === record.name ? ownDocs : familyDocs).push(entry);
103
+ }
104
+ for (const record of ownDocs) {
105
+ list.add('components', record.name, record.package, record.docPath);
106
+ }
107
+ for (const record of familyDocs) {
108
+ if (list.claims(record.docPath)) continue;
86
109
  list.add('components', record.name, record.package, record.docPath);
87
110
  }
88
111
 
@@ -91,7 +91,6 @@ describe('collectDocInputs over what this repo ships', () => {
91
91
  'Chat',
92
92
  'ChatComposerTokenElement',
93
93
  'ContextMenuItem',
94
- 'Heading',
95
94
  'Indicator',
96
95
  'Layer',
97
96
  'NavMenu',
@@ -248,11 +248,16 @@ export async function collectIdentities(loaded) {
248
248
  if (loaded.components && fs.existsSync(loaded.components)) {
249
249
  try {
250
250
  const records = discoverIntegrationComponents(loaded) ?? [];
251
+ // A family doc backs several records: load it, and report it, once.
252
+ const loads = new Map();
251
253
  for (const record of records) {
254
+ const first = !loads.has(record.docPath);
255
+ if (first) loads.set(record.docPath, loadComponentDoc(record.docPath));
252
256
  try {
253
- await loadComponentDoc(record.docPath);
257
+ await loads.get(record.docPath);
254
258
  identities.components.push(record.name);
255
259
  } catch (err) {
260
+ if (!first) continue;
256
261
  errors.push({
257
262
  kind: 'components',
258
263
  message: `${path.relative(loaded.components, record.docPath)}: ${
@@ -20,6 +20,7 @@ import path from 'node:path';
20
20
  import {findCoreDir} from '../fs/paths.mjs';
21
21
  import {
22
22
  discoverComponents,
23
+ familyMember,
23
24
  findComponentReadme,
24
25
  resolveImportPath,
25
26
  } from '../discovery/component-discovery.mjs';
@@ -126,6 +127,9 @@ export async function buildRegistry({cwd = process.cwd()} = {}) {
126
127
  } catch {
127
128
  continue; // a malformed doc must not take down the whole language
128
129
  }
130
+ // A member documented in its family's doc (VStack in Stack's doc) is
131
+ // registered by the family's own folder, with every other member.
132
+ if (familyMember(docs, dirName)) continue;
129
133
  const importPath = resolveImportPath(coreDir, dirName);
130
134
 
131
135
  const register = (/** @type {string} */ rawName, /** @type {import('./xle-ast').DocProp[]} */ props) => {
@@ -0,0 +1,28 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file The layout-language registry over the real @astryxdesign/core tree:
5
+ * the import path it emits for each component.
6
+ */
7
+
8
+ import {describe, it, expect} from 'vitest';
9
+ import * as path from 'node:path';
10
+ import {fileURLToPath} from 'node:url';
11
+ import {buildRegistry} from './registry.mjs';
12
+
13
+ const REPO = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../../..');
14
+
15
+ describe('buildRegistry import paths', () => {
16
+ it('keeps the Stack family on the paths it registers from Stack\'s folder', async () => {
17
+ // HStack and VStack are listed components whose docs live in Stack's doc.
18
+ // Listing them must not let their folders re-register the family under
19
+ // their own paths: a member's folder registers nothing, Stack's does.
20
+ const {components} = await buildRegistry({cwd: REPO});
21
+ const importOf = (/** @type {string} */ name) => components.get(name)?.importPath;
22
+ for (const name of ['Stack', 'StackItem', 'HStack', 'VStack']) {
23
+ expect(importOf(name), name).toBe('@astryxdesign/core/Stack');
24
+ }
25
+ expect(importOf('Heading')).toBe('@astryxdesign/core/Heading');
26
+ expect(importOf('Text')).toBe('@astryxdesign/core/Text');
27
+ }, 120_000);
28
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astryxdesign/cli",
3
- "version": "0.7.2-canary.caec7ca",
3
+ "version": "0.7.2-canary.cbb7fa3",
4
4
  "displayName": "CLI",
5
5
  "description": "Scaffold projects, browse templates, generate themes, and get agent-ready docs from the command line.",
6
6
  "author": "Meta Open Source",
@@ -101,10 +101,10 @@
101
101
  "zod": "^4.4.3"
102
102
  },
103
103
  "peerDependencies": {
104
- "@astryxdesign/charts": "0.7.2-canary.caec7ca",
105
- "@astryxdesign/core": "0.7.2-canary.caec7ca",
106
- "@astryxdesign/lab": "0.7.2-canary.caec7ca",
107
- "@astryxdesign/theme-neutral": "0.7.2-canary.caec7ca"
104
+ "@astryxdesign/charts": "0.7.2-canary.cbb7fa3",
105
+ "@astryxdesign/core": "0.7.2-canary.cbb7fa3",
106
+ "@astryxdesign/lab": "0.7.2-canary.cbb7fa3",
107
+ "@astryxdesign/theme-neutral": "0.7.2-canary.cbb7fa3"
108
108
  },
109
109
  "peerDependenciesMeta": {
110
110
  "@astryxdesign/charts": {
@@ -121,10 +121,10 @@
121
121
  }
122
122
  },
123
123
  "devDependencies": {
124
- "@astryxdesign/charts": "0.7.2-canary.caec7ca",
125
- "@astryxdesign/core": "0.7.2-canary.caec7ca",
126
- "@astryxdesign/lab": "0.7.2-canary.caec7ca",
127
- "@astryxdesign/theme-neutral": "0.7.2-canary.caec7ca",
124
+ "@astryxdesign/charts": "0.7.2-canary.cbb7fa3",
125
+ "@astryxdesign/core": "0.7.2-canary.cbb7fa3",
126
+ "@astryxdesign/lab": "0.7.2-canary.cbb7fa3",
127
+ "@astryxdesign/theme-neutral": "0.7.2-canary.cbb7fa3",
128
128
  "@heroicons/react": "^2.2.0",
129
129
  "@stylexjs/stylex": "^0.19.0",
130
130
  "@types/babel__core": "^7.20.5",