@astryxdesign/cli 0.4.5-canary.bc34238 → 0.4.5-canary.c2bb31b

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.
@@ -212,4 +212,95 @@ describe('drop-xds-prefix-imports', () => {
212
212
  const output = await applyTransform(input);
213
213
  expect(output).toBe(input);
214
214
  });
215
+
216
+ it('aliases a component wrapper collision in place (XDS -> Astryx), leaving the local fn untouched', async () => {
217
+ const input = [
218
+ `import {XDSLinkProvider} from '@xds/core/Link';`,
219
+ `export default function LinkProvider({children}) {`,
220
+ ` return <XDSLinkProvider component={NextLink}>{children}</XDSLinkProvider>;`,
221
+ `}`,
222
+ ].join('\n');
223
+ const output = await applyTransform(input);
224
+ // Import aliased in place, local wrapper + its name untouched.
225
+ expect(output).toContain('import {LinkProvider as AstryxLinkProvider}');
226
+ expect(output).toContain('function LinkProvider({children})');
227
+ // JSX rewritten to the alias -> no self-recursion.
228
+ expect(output).toContain('<AstryxLinkProvider component={NextLink}>');
229
+ expect(output).not.toContain('XDSLinkProvider');
230
+ // No duplicate bare LinkProvider import.
231
+ expect(output).not.toMatch(/import \{LinkProvider\}/);
232
+ });
233
+
234
+ it('aliases a hook collision as use<Astryx>Name (not <Astryx>use)', async () => {
235
+ const input = [
236
+ `import {useXDSToast} from '@xds/core';`,
237
+ `function useToast() {`,
238
+ ` return useXDSToast();`,
239
+ `}`,
240
+ ].join('\n');
241
+ const output = await applyTransform(input);
242
+ expect(output).toContain('import {useToast as useAstryxToast}');
243
+ expect(output).toContain('function useToast()');
244
+ expect(output).toContain('return useAstryxToast();');
245
+ expect(output).not.toContain('useXDSToast');
246
+ // Must not produce the malformed `Astryxuse...` form.
247
+ expect(output).not.toContain('AstryxuseToast');
248
+ });
249
+
250
+ it('aliases on collision with a local default import binding', async () => {
251
+ const input = [
252
+ `import Link from 'next/link';`,
253
+ `import {XDSLink} from '@xds/core';`,
254
+ `export const a = <XDSLink href="/" />;`,
255
+ `export const b = <Link href="/" />;`,
256
+ ].join('\n');
257
+ const output = await applyTransform(input);
258
+ expect(output).toContain('import {Link as AstryxLink}');
259
+ expect(output).toContain(`import Link from 'next/link';`);
260
+ expect(output).toContain('<AstryxLink href="/" />');
261
+ expect(output).toContain('<Link href="/" />');
262
+ expect(output).not.toContain('XDSLink');
263
+ });
264
+
265
+ it('aliases on collision with a local type alias', async () => {
266
+ const input = [
267
+ `import type {XDSTab} from '@xds/core';`,
268
+ `type Tab = {id: string};`,
269
+ `const active: XDSTab = null as any;`,
270
+ `const local: Tab = {id: '1'};`,
271
+ ].join('\n');
272
+ const output = await applyTransform(input);
273
+ expect(output).toContain('Tab as AstryxTab');
274
+ expect(output).toContain('type Tab = {id: string};');
275
+ expect(output).toContain('const active: AstryxTab');
276
+ expect(output).toContain('const local: Tab');
277
+ expect(output).not.toContain('XDSTab');
278
+ });
279
+
280
+ it('aliases on collision with a local interface', async () => {
281
+ const input = [
282
+ `import type {XDSTheme} from '@xds/core';`,
283
+ `interface Theme {}`,
284
+ `const t: XDSTheme = null as any;`,
285
+ `const local: Theme = {};`,
286
+ ].join('\n');
287
+ const output = await applyTransform(input);
288
+ expect(output).toContain('Theme as AstryxTheme');
289
+ expect(output).toContain('interface Theme {}');
290
+ expect(output).toContain('const t: AstryxTheme');
291
+ expect(output).not.toContain('XDSTheme');
292
+ });
293
+
294
+ it('still bare-renames when there is NO colliding local binding', async () => {
295
+ const input = [
296
+ `import {XDSButton} from '@xds/core';`,
297
+ `export const App = () => <XDSButton label="Hi" />;`,
298
+ ].join('\n');
299
+ const output = await applyTransform(input);
300
+ // No local `Button` binding -> existing blind bare-rename behavior kept.
301
+ expect(output).toContain(`import {Button} from '@xds/core';`);
302
+ expect(output).toContain('<Button label="Hi" />');
303
+ expect(output).not.toContain('AstryxButton');
304
+ expect(output).not.toContain('XDSButton');
305
+ });
215
306
  });
@@ -64,6 +64,22 @@ export const meta = {
64
64
 
65
65
  const XDS_CORE_SOURCE = /^@xds\/core(\/.*)?$/;
66
66
 
67
+ /**
68
+ * Compute a collision alias for an XDS-prefixed identifier by replacing the
69
+ * `XDS` segment IN PLACE with `Astryx` (as opposed to prefixing the bare name).
70
+ * This keeps hook names well-formed:
71
+ *
72
+ * XDSButton -> AstryxButton
73
+ * XDSLinkProvider -> AstryxLinkProvider
74
+ * useXDSToast -> useAstryxToast (NOT AstryxuseToast)
75
+ *
76
+ * Only the first `XDS` occurrence is replaced, matching how `bareName` strips a
77
+ * single leading prefix.
78
+ */
79
+ function aliasName(/** @type {any} */ name) {
80
+ return name.replace('XDS', 'Astryx');
81
+ }
82
+
67
83
  /**
68
84
  * Compute the bare (unprefixed) name for an XDS-prefixed identifier.
69
85
  * Returns null if the name is not XDS-prefixed in a renameable way.
@@ -190,7 +206,12 @@ export default function transformer(file, api) {
190
206
  if (!node) return;
191
207
  if (
192
208
  (node.type === 'FunctionDeclaration' ||
193
- node.type === 'ClassDeclaration') &&
209
+ node.type === 'ClassDeclaration' ||
210
+ // Type-only bindings share the module namespace for our purposes: a
211
+ // `type Tab`/`interface Theme` collides with an un-prefixed `XDSTab`/
212
+ // `XDSTheme` type import and would create a duplicate declaration.
213
+ node.type === 'TSTypeAliasDeclaration' ||
214
+ node.type === 'TSInterfaceDeclaration') &&
194
215
  node.id
195
216
  ) {
196
217
  existingBindings.add(node.id.name);
@@ -239,10 +260,13 @@ export default function transformer(file, api) {
239
260
  if (bare !== importedName && existingBindings.has(bare)) {
240
261
  // COLLISION: the bare name is already a top-level binding in this file
241
262
  // (e.g. a local `export function CodeBlock` alongside imported
242
- // `XDSCodeBlock`). Un-prefixing directly would create a duplicate
243
- // declaration, so alias the import to `Astryx<Name>` and rename the
263
+ // `XDSCodeBlock`, or a `type Tab` alongside `XDSTab`). Un-prefixing
264
+ // directly would create a duplicate declaration (TS2451), so alias the
265
+ // import by replacing `XDS` with `Astryx` in place and rename the
244
266
  // import's references to that alias, leaving the local binding intact.
245
- const alias = `Astryx${bare}`;
267
+ // In-place replacement keeps hooks well-formed: `useXDSToast` becomes
268
+ // `useAstryxToast`, not `AstryxuseToast`.
269
+ const alias = aliasName(importedName);
246
270
  localRenames.set(importedName, alias);
247
271
  existingBindings.add(alias);
248
272
  hasChanges = true;
@@ -0,0 +1,14 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /** @type {import('@astryxdesign/cli/authoring').TemplateDoc} */
4
+ export const doc = {
5
+ type: 'block',
6
+ exampleFor: 'Selector',
7
+ name: 'Selector — Option descriptions',
8
+ displayName: 'Selector — Option descriptions',
9
+ description:
10
+ 'Options carry a description, so the dropdown draws a two-line row. The closed trigger is sized by padding, so it is the size token for a one-line value and exactly one line taller for a two-line one — both on the 4px rhythm. An InputGroup pins the row, so the value folds back onto one line there.',
11
+ isReady: true,
12
+ aspectRatio: 16 / 9,
13
+ componentsUsed: ['Selector', 'SelectorOption', 'InputGroup', 'Button', 'Stack'],
14
+ };
@@ -0,0 +1,76 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ import {useState} from 'react';
6
+ import {Selector, SelectorOption} from '@astryxdesign/core/Selector';
7
+ import {InputGroup} from '@astryxdesign/core/InputGroup';
8
+ import {Button} from '@astryxdesign/core/Button';
9
+ import {Stack} from '@astryxdesign/core/Layout';
10
+ import {LockClosedIcon, GlobeAltIcon} from '@heroicons/react/24/outline';
11
+
12
+ const VISIBILITY = [
13
+ {
14
+ value: 'private',
15
+ label: 'Private',
16
+ icon: LockClosedIcon,
17
+ description: 'Only members can access this space and its content.',
18
+ },
19
+ {
20
+ value: 'public',
21
+ label: 'Public',
22
+ icon: GlobeAltIcon,
23
+ description: 'Anyone at the company can find and join this space.',
24
+ },
25
+ ];
26
+
27
+ export default function SelectorOptionDescriptions() {
28
+ const [condensed, setCondensed] = useState<string | undefined>('private');
29
+ const [full, setFull] = useState<string | undefined>('private');
30
+ const [grouped, setGrouped] = useState<string | undefined>('private');
31
+
32
+ return (
33
+ <Stack direction="vertical" gap={6}>
34
+ <Selector
35
+ style={{width: 320}}
36
+ label="Visibility"
37
+ description="Default trigger: one line at the size token."
38
+ options={VISIBILITY}
39
+ value={condensed}
40
+ onChange={setCondensed}
41
+ />
42
+ <Selector
43
+ style={{width: 320}}
44
+ label="Visibility"
45
+ description="A stacked SelectorOption: the description gets its own line, and the trigger grows by exactly one line."
46
+ options={VISIBILITY}
47
+ value={full}
48
+ onChange={setFull}
49
+ renderValue={option => (
50
+ <SelectorOption
51
+ icon={option.icon}
52
+ label={option.label ?? option.value}
53
+ description={option.description}
54
+ />
55
+ )}
56
+ />
57
+ <InputGroup label="Visibility">
58
+ <Selector
59
+ label="Visibility"
60
+ isLabelHidden
61
+ options={VISIBILITY}
62
+ value={grouped}
63
+ onChange={setGrouped}
64
+ renderValue={option => (
65
+ <SelectorOption
66
+ icon={option.icon}
67
+ label={option.label ?? option.value}
68
+ description={option.description}
69
+ />
70
+ )}
71
+ />
72
+ <Button label="Save" />
73
+ </InputGroup>
74
+ </Stack>
75
+ );
76
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astryxdesign/cli",
3
- "version": "0.4.5-canary.bc34238",
3
+ "version": "0.4.5-canary.c2bb31b",
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.bc34238",
91
- "@astryxdesign/core": "0.4.5-canary.bc34238",
92
- "@astryxdesign/lab": "0.4.5-canary.bc34238",
93
- "@astryxdesign/theme-neutral": "0.4.5-canary.bc34238",
90
+ "@astryxdesign/charts": "0.4.5-canary.c2bb31b",
91
+ "@astryxdesign/core": "0.4.5-canary.c2bb31b",
92
+ "@astryxdesign/lab": "0.4.5-canary.c2bb31b",
93
+ "@astryxdesign/theme-neutral": "0.4.5-canary.c2bb31b",
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.bc34238",
112
- "@astryxdesign/core": "0.4.5-canary.bc34238",
113
- "@astryxdesign/lab": "0.4.5-canary.bc34238",
114
- "@astryxdesign/theme-neutral": "0.4.5-canary.bc34238",
111
+ "@astryxdesign/charts": "0.4.5-canary.c2bb31b",
112
+ "@astryxdesign/core": "0.4.5-canary.c2bb31b",
113
+ "@astryxdesign/lab": "0.4.5-canary.c2bb31b",
114
+ "@astryxdesign/theme-neutral": "0.4.5-canary.c2bb31b",
115
115
  "gpt-tokenizer": "^3.4.0"
116
116
  },
117
117
  "scripts": {