@astryxdesign/cli 0.4.5-canary.5d8ece1 → 0.4.5-canary.6f36dda

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.
@@ -12,6 +12,7 @@
12
12
  * @property {any[]} [components]
13
13
  * @property {{description?: string}} [usage]
14
14
  * @property {any} [theming]
15
+ * @property {string} [import] set when the doc states its own import specifier
15
16
  */
16
17
  /**
17
18
  * Options object for `loadDocs`, matching its declared parameter shape (used
@@ -29,6 +30,12 @@
29
30
  * @property {string|undefined} issuesUrl
30
31
  * @property {import('../../foundation/integrations/integrations.mjs').LoadedIntegration|null} integration
31
32
  */
33
+ /**
34
+ * What ownership shaping needs from an owner. The core and legacy-external
35
+ * paths synthesize a bare `{package, sourcePath}` rather than resolving a full
36
+ * {@link ComponentOwner}, so everything past those two is optional here.
37
+ * @typedef {Partial<ComponentOwner> & {package: string, sourcePath: string|null}} OwnershipSubject
38
+ */
32
39
  /**
33
40
  * A back-compat external package discovered via `pkg.astryx.docs`.
34
41
  * @typedef {{name: string, category: string, docsDir: string}} ExternalPackageRef
@@ -152,15 +159,12 @@ export function extractProps(docs: LoadedComponentDoc): any[];
152
159
  * swizzleable source file exists for the owner). Existing doc fields (name,
153
160
  * usage, props, …) are preserved.
154
161
  * @param {LoadedComponentDoc} docs
155
- * @param {{package: string, sourcePath: string|null}} owner
162
+ * @param {OwnershipSubject} owner
156
163
  * @param {string} componentName
157
164
  * @param {string} coreDir
158
165
  * @returns {import('./component.type.mjs').ComponentDetailResponse['data']}
159
166
  */
160
- export function withOwnership(docs: LoadedComponentDoc, owner: {
161
- package: string;
162
- sourcePath: string | null;
163
- }, componentName: string, coreDir: string): import("./component.type.mjs").ComponentDetailResponse["data"];
167
+ export function withOwnership(docs: LoadedComponentDoc, owner: OwnershipSubject, componentName: string, coreDir: string): import("./component.type.mjs").ComponentDetailResponse["data"];
164
168
  /**
165
169
  * When the caller asked for "Code" but the resolved doc is for "CodeBlock"
166
170
  * (parent), scope the response to just the matching sub-component. Returns the
@@ -193,6 +197,10 @@ export type LoadedComponentDoc = {
193
197
  description?: string;
194
198
  } | undefined;
195
199
  theming?: any;
200
+ /**
201
+ * set when the doc states its own import specifier
202
+ */
203
+ import?: string | undefined;
196
204
  };
197
205
  /**
198
206
  * Options object for `loadDocs`, matching its declared parameter shape (used
@@ -215,6 +223,15 @@ export type ComponentOwner = {
215
223
  issuesUrl: string | undefined;
216
224
  integration: import("../../foundation/integrations/integrations.mjs").LoadedIntegration | null;
217
225
  };
226
+ /**
227
+ * What ownership shaping needs from an owner. The core and legacy-external
228
+ * paths synthesize a bare `{package, sourcePath}` rather than resolving a full
229
+ * {@link ComponentOwner}, so everything past those two is optional here.
230
+ */
231
+ export type OwnershipSubject = Partial<ComponentOwner> & {
232
+ package: string;
233
+ sourcePath: string | null;
234
+ };
218
235
  /**
219
236
  * A back-compat external package discovered via `pkg.astryx.docs`.
220
237
  */
@@ -18,6 +18,8 @@
18
18
  * deduped, so each leaf stays a thin projection.
19
19
  */
20
20
 
21
+ import * as fs from 'node:fs';
22
+ import * as path from 'node:path';
21
23
  import {ERROR_CODES} from '../../foundation/response/error-codes.mjs';
22
24
  import {findCoreDir, discoverExternalPackages} from '../../foundation/fs/paths.mjs';
23
25
  import {
@@ -48,6 +50,7 @@ export {CORE_PACKAGE};
48
50
  * @property {any[]} [components]
49
51
  * @property {{description?: string}} [usage]
50
52
  * @property {any} [theming]
53
+ * @property {string} [import] set when the doc states its own import specifier
51
54
  */
52
55
 
53
56
  /**
@@ -68,6 +71,13 @@ export {CORE_PACKAGE};
68
71
  * @property {import('../../foundation/integrations/integrations.mjs').LoadedIntegration|null} integration
69
72
  */
70
73
 
74
+ /**
75
+ * What ownership shaping needs from an owner. The core and legacy-external
76
+ * paths synthesize a bare `{package, sourcePath}` rather than resolving a full
77
+ * {@link ComponentOwner}, so everything past those two is optional here.
78
+ * @typedef {Partial<ComponentOwner> & {package: string, sourcePath: string|null}} OwnershipSubject
79
+ */
80
+
71
81
  /**
72
82
  * A back-compat external package discovered via `pkg.astryx.docs`.
73
83
  * @typedef {{name: string, category: string, docsDir: string}} ExternalPackageRef
@@ -357,7 +367,7 @@ export function extractProps(docs) {
357
367
  * swizzleable source file exists for the owner). Existing doc fields (name,
358
368
  * usage, props, …) are preserved.
359
369
  * @param {LoadedComponentDoc} docs
360
- * @param {{package: string, sourcePath: string|null}} owner
370
+ * @param {OwnershipSubject} owner
361
371
  * @param {string} componentName
362
372
  * @param {string} coreDir
363
373
  * @returns {import('./component.type.mjs').ComponentDetailResponse['data']}
@@ -366,15 +376,53 @@ export function withOwnership(docs, owner, componentName, coreDir) {
366
376
  const importSpec =
367
377
  owner.package === CORE_PACKAGE
368
378
  ? resolveImportPath(coreDir, componentName)
369
- : `${owner.package}/${componentName}`;
379
+ : resolveIntegrationImportPath(owner, componentName);
370
380
  return /** @type {any} */ ({
371
381
  ...docs,
372
382
  package: owner.package,
373
- import: importSpec,
383
+ // A doc file may state its own specifier, e.g. when one entry point exports
384
+ // several components. Only fall back to a resolved one when it does not.
385
+ import: docs.import ?? importSpec,
374
386
  sourceAvailable: owner.sourcePath != null,
375
387
  });
376
388
  }
377
389
 
390
+ /**
391
+ * Resolve the specifier an integration component is imported from, against the
392
+ * owning package's `exports` map.
393
+ *
394
+ * A component lives in a directory that need not share its name — several
395
+ * components can be exported from one entry point — so the specifier has to
396
+ * come from the directory the doc file sits in, checked against `exports`,
397
+ * rather than from the component name. Falls back to the package root when the
398
+ * directory is not an exported subpath, matching what a consumer would have to
399
+ * write by hand.
400
+ *
401
+ * @param {OwnershipSubject} owner
402
+ * @param {string} componentName
403
+ * @returns {string}
404
+ */
405
+ function resolveIntegrationImportPath(owner, componentName) {
406
+ const packageDir = owner.integration?.__packageDir;
407
+ const directory = owner.docPath
408
+ ? path.basename(path.dirname(owner.docPath))
409
+ : componentName;
410
+ if (!packageDir) {
411
+ return owner.package;
412
+ }
413
+ try {
414
+ const manifest = JSON.parse(
415
+ fs.readFileSync(path.join(packageDir, 'package.json'), 'utf-8'),
416
+ );
417
+ if (manifest.exports?.[`./${directory}`]) {
418
+ return `${owner.package}/${directory}`;
419
+ }
420
+ } catch {
421
+ // An unreadable or malformed manifest is not worth failing a lookup over.
422
+ }
423
+ return owner.package;
424
+ }
425
+
378
426
  /**
379
427
  * When the caller asked for "Code" but the resolved doc is for "CodeBlock"
380
428
  * (parent), scope the response to just the matching sub-component. Returns the
@@ -47,7 +47,12 @@ const INTEGRATION_ISSUES = 'https://example.com/meta/issues';
47
47
  * Returns the absolute `components` dir so the Project.load mock can hand back a
48
48
  * resolved integration entry.
49
49
  */
50
- function createFixture({withSource = true, extraComponent = null} = {}) {
50
+ function createFixture({
51
+ withSource = true,
52
+ extraComponent = null,
53
+ packageExports = null,
54
+ entryPoint = null,
55
+ } = {}) {
51
56
  const realCoreDir = path.resolve(import.meta.dirname, '..', '..', '..', '..', 'core');
52
57
  const coreDir = path.join(tmpDir, 'packages', 'core');
53
58
  fs.mkdirSync(path.dirname(coreDir), {recursive: true});
@@ -58,7 +63,11 @@ function createFixture({withSource = true, extraComponent = null} = {}) {
58
63
  fs.mkdirSync(compDir, {recursive: true});
59
64
  fs.writeFileSync(
60
65
  path.join(intDir, 'package.json'),
61
- JSON.stringify({name: INTEGRATION_NAME, version: '1.2.3'}),
66
+ JSON.stringify({
67
+ name: INTEGRATION_NAME,
68
+ version: '1.2.3',
69
+ ...(packageExports ? {exports: packageExports} : {}),
70
+ }),
62
71
  );
63
72
  fs.writeFileSync(
64
73
  path.join(compDir, 'MetaAppShell.doc.mjs'),
@@ -81,6 +90,20 @@ function createFixture({withSource = true, extraComponent = null} = {}) {
81
90
  );
82
91
  }
83
92
 
93
+ // A component whose directory is an entry point exporting several
94
+ // components, so the directory name and the component name differ.
95
+ if (entryPoint) {
96
+ const entryDir = path.join(compDir, entryPoint.directory);
97
+ fs.mkdirSync(entryDir, {recursive: true});
98
+ const ownSpecifier = entryPoint.importSpec
99
+ ? `\n import: '${entryPoint.importSpec}',`
100
+ : '';
101
+ fs.writeFileSync(
102
+ path.join(entryDir, `${entryPoint.component}.doc.mjs`),
103
+ `export const docs = {\n name: '${entryPoint.component}',${ownSpecifier}\n usage: { description: '${entryPoint.component} from an entry point.' },\n};\n`,
104
+ );
105
+ }
106
+
84
107
  const integration = {
85
108
  name: INTEGRATION_NAME,
86
109
  version: '1.2.3',
@@ -88,6 +111,7 @@ function createFixture({withSource = true, extraComponent = null} = {}) {
88
111
  templates: undefined,
89
112
  codemods: undefined,
90
113
  issuesUrl: INTEGRATION_ISSUES,
114
+ __packageDir: intDir,
91
115
  };
92
116
  projectLoadMock.mockResolvedValue({
93
117
  integrations: [INTEGRATION_NAME],
@@ -162,7 +186,9 @@ describe('component() — integration ownership via config', () => {
162
186
  expect(result.data.name).toBe('MetaAppShell');
163
187
  expect(result.data.package).toBe(INTEGRATION_NAME);
164
188
  expect(result.data.sourceAvailable).toBe(true);
165
- expect(result.data.import).toBe(`${INTEGRATION_NAME}/MetaAppShell`);
189
+ // This fixture declares no `exports`, so there is no subpath to import
190
+ // from and the specifier is the package root.
191
+ expect(result.data.import).toBe(INTEGRATION_NAME);
166
192
  });
167
193
 
168
194
  it('--package resolves the integration component', async () => {
@@ -217,6 +243,40 @@ describe('component() — integration ownership via config', () => {
217
243
  expect(result.data.package).toBe(INTEGRATION_NAME);
218
244
  });
219
245
 
246
+ it('resolves the import specifier against the package exports map', async () => {
247
+ // The doc sits in a `Toolbar` directory but the component is
248
+ // `ToolbarSearch`, so a specifier built from the component name would
249
+ // point at a subpath the package does not export.
250
+ createFixture({
251
+ packageExports: {'.': './index.js', './Toolbar': './components/Toolbar/index.js'},
252
+ entryPoint: {directory: 'Toolbar', component: 'ToolbarSearch'},
253
+ });
254
+ const result = await component('ToolbarSearch', {cwd: tmpDir});
255
+ expect(result.data.import).toBe(`${INTEGRATION_NAME}/Toolbar`);
256
+ });
257
+
258
+ it('falls back to the package root when the directory is not an exported subpath', async () => {
259
+ createFixture({
260
+ packageExports: {'.': './index.js'},
261
+ entryPoint: {directory: 'Toolbar', component: 'ToolbarSearch'},
262
+ });
263
+ const result = await component('ToolbarSearch', {cwd: tmpDir});
264
+ expect(result.data.import).toBe(INTEGRATION_NAME);
265
+ });
266
+
267
+ it('keeps a specifier the doc file states for itself', async () => {
268
+ createFixture({
269
+ packageExports: {'.': './index.js', './Toolbar': './components/Toolbar/index.js'},
270
+ entryPoint: {
271
+ directory: 'Toolbar',
272
+ component: 'ToolbarSearch',
273
+ importSpec: `${INTEGRATION_NAME}/Toolbar/Search`,
274
+ },
275
+ });
276
+ const result = await component('ToolbarSearch', {cwd: tmpDir});
277
+ expect(result.data.import).toBe(`${INTEGRATION_NAME}/Toolbar/Search`);
278
+ });
279
+
220
280
  it('JSON list includes integration components as {name, package} objects', async () => {
221
281
  createFixture();
222
282
  const result = await component(undefined, {cwd: tmpDir, list: true});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astryxdesign/cli",
3
- "version": "0.4.5-canary.5d8ece1",
3
+ "version": "0.4.5-canary.6f36dda",
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",
@@ -87,10 +87,10 @@
87
87
  "zod": "^4.4.3"
88
88
  },
89
89
  "peerDependencies": {
90
- "@astryxdesign/charts": "0.4.5-canary.5d8ece1",
91
- "@astryxdesign/core": "0.4.5-canary.5d8ece1",
92
- "@astryxdesign/lab": "0.4.5-canary.5d8ece1",
93
- "@astryxdesign/theme-neutral": "0.4.5-canary.5d8ece1",
90
+ "@astryxdesign/charts": "0.4.5-canary.6f36dda",
91
+ "@astryxdesign/core": "0.4.5-canary.6f36dda",
92
+ "@astryxdesign/lab": "0.4.5-canary.6f36dda",
93
+ "@astryxdesign/theme-neutral": "0.4.5-canary.6f36dda",
94
94
  "gpt-tokenizer": "^3.4.0"
95
95
  },
96
96
  "peerDependenciesMeta": {
@@ -108,10 +108,10 @@
108
108
  }
109
109
  },
110
110
  "devDependencies": {
111
- "@astryxdesign/charts": "0.4.5-canary.5d8ece1",
112
- "@astryxdesign/core": "0.4.5-canary.5d8ece1",
113
- "@astryxdesign/lab": "0.4.5-canary.5d8ece1",
114
- "@astryxdesign/theme-neutral": "0.4.5-canary.5d8ece1",
111
+ "@astryxdesign/charts": "0.4.5-canary.6f36dda",
112
+ "@astryxdesign/core": "0.4.5-canary.6f36dda",
113
+ "@astryxdesign/lab": "0.4.5-canary.6f36dda",
114
+ "@astryxdesign/theme-neutral": "0.4.5-canary.6f36dda",
115
115
  "gpt-tokenizer": "^3.4.0"
116
116
  },
117
117
  "scripts": {