@astryxdesign/cli 0.1.4-canary.ec9f8b2 → 0.1.4-canary.efa3ea5

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.
@@ -125,7 +125,7 @@ If you don't know all three, run \`npx astryx init --features agents\` to genera
125
125
  lang: 'json',
126
126
  label: 'package.json',
127
127
  code: `"scripts": {
128
- "astryx": "node node_modules/@astryxdesign/cli/bin/astryx.mjs"
128
+ "xds": "node node_modules/@astryxdesign/cli/bin/astryx.mjs"
129
129
  }`,
130
130
  },
131
131
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astryxdesign/cli",
3
- "version": "0.1.4-canary.ec9f8b2",
3
+ "version": "0.1.4-canary.efa3ea5",
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",
@@ -68,17 +68,17 @@
68
68
  "CHANGELOG.md"
69
69
  ],
70
70
  "dependencies": {
71
- "@clack/prompts": "^1.7.0",
71
+ "@clack/prompts": "^1.5.1",
72
72
  "commander": "^12.1.0",
73
73
  "jiti": "^2.7.0",
74
74
  "jscodeshift": "^17.3.0",
75
75
  "zod": "^4.4.3"
76
76
  },
77
77
  "peerDependencies": {
78
- "@astryxdesign/charts": "0.1.4-canary.ec9f8b2",
79
- "@astryxdesign/core": "0.1.4-canary.ec9f8b2",
80
- "@astryxdesign/lab": "0.1.4-canary.ec9f8b2",
81
- "@astryxdesign/theme-neutral": "0.1.4-canary.ec9f8b2",
78
+ "@astryxdesign/charts": "0.1.4-canary.efa3ea5",
79
+ "@astryxdesign/core": "0.1.4-canary.efa3ea5",
80
+ "@astryxdesign/lab": "0.1.4-canary.efa3ea5",
81
+ "@astryxdesign/theme-neutral": "0.1.4-canary.efa3ea5",
82
82
  "gpt-tokenizer": "^3.4.0"
83
83
  },
84
84
  "peerDependenciesMeta": {
@@ -96,10 +96,10 @@
96
96
  }
97
97
  },
98
98
  "devDependencies": {
99
- "@astryxdesign/charts": "0.1.4-canary.ec9f8b2",
100
- "@astryxdesign/core": "0.1.4-canary.ec9f8b2",
101
- "@astryxdesign/lab": "0.1.4-canary.ec9f8b2",
102
- "@astryxdesign/theme-neutral": "0.1.4-canary.ec9f8b2",
99
+ "@astryxdesign/charts": "0.1.4-canary.efa3ea5",
100
+ "@astryxdesign/core": "0.1.4-canary.efa3ea5",
101
+ "@astryxdesign/lab": "0.1.4-canary.efa3ea5",
102
+ "@astryxdesign/theme-neutral": "0.1.4-canary.efa3ea5",
103
103
  "gpt-tokenizer": "^3.4.0"
104
104
  },
105
105
  "scripts": {
@@ -19,7 +19,6 @@ describe('registry', () => {
19
19
  '0.1.0',
20
20
  '0.1.2',
21
21
  '0.1.3',
22
- '0.1.5',
23
22
  ]);
24
23
  });
25
24
  });
@@ -20,7 +20,6 @@ const registry = new Map([
20
20
  ['0.1.0', () => import('./transforms/v0.1.0/index.mjs')],
21
21
  ['0.1.2', () => import('./transforms/v0.1.2/index.mjs')],
22
22
  ['0.1.3', () => import('./transforms/v0.1.3/index.mjs')],
23
- ['0.1.5', () => import('./transforms/v0.1.5/index.mjs')],
24
23
  ]);
25
24
 
26
25
  // Re-export from the shared utility so registry callers and other consumers
@@ -939,9 +939,15 @@ export function registerTheme(program) {
939
939
  if (component.length > 0) {
940
940
  const componentInner = component.join('\n\n');
941
941
  const componentScope = `@scope (${scopeSelector}) to (${scopeTo}) {\n${componentInner}\n}`;
942
- // #3658: also emit attribute-specific rules so <Theme mode> can override color-scheme
942
+ // light-dark() needs a color-scheme declaration in this bundle, but a
943
+ // bare `:root { color-scheme: light dark }` outranks reset.css's
944
+ // `html[data-theme]` mapping (astryx-theme layer comes after reset),
945
+ // silently defeating `<Theme mode>` forcing on <html>. Mirror the
946
+ // attribute-keyed mapping so the declaration follows the active mode.
943
947
  const colorSchemeDecl = componentScope.includes('light-dark(')
944
- ? ' :root { color-scheme: light dark; }\n html[data-theme="light"] { color-scheme: light; }\n html[data-theme="dark"] { color-scheme: dark; }\n\n'
948
+ ? ' :root { color-scheme: light dark; }\n' +
949
+ ' html[data-theme="light"] { color-scheme: light; }\n' +
950
+ ' html[data-theme="dark"] { color-scheme: dark; }\n\n'
945
951
  : '';
946
952
  cssParts.push(
947
953
  `@layer astryx-theme {\n${colorSchemeDecl}${componentScope}\n}`,
@@ -28,10 +28,20 @@ import * as fs from 'node:fs';
28
28
  import * as path from 'node:path';
29
29
  import * as os from 'node:os';
30
30
  import {fileURLToPath} from 'node:url';
31
- import {ensureCoreBuilt} from './ensure-core-built.mjs';
32
31
 
33
32
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
34
33
  const CLI_BIN = path.resolve(__dirname, '../../bin/astryx.mjs');
34
+ const REPO_ROOT = path.resolve(__dirname, '../../../..');
35
+ const CORE_THEME_ENTRY = path.join(
36
+ REPO_ROOT,
37
+ 'packages/core/dist/theme/index.js',
38
+ );
39
+ // The fix reads core's shipped component declarations to decide whether an
40
+ // interface is augmentable; those .d.ts files come from the same core build.
41
+ const CORE_BUTTON_DTS = path.join(
42
+ REPO_ROOT,
43
+ 'packages/core/dist/Button/index.d.ts',
44
+ );
35
45
 
36
46
  function runCli(args, cwd) {
37
47
  try {
@@ -58,13 +68,14 @@ function writeTheme(dir, contents) {
58
68
  return file;
59
69
  }
60
70
 
61
- // Build core through the shared lock helper — this suite previously ran its
62
- // own unguarded `if (!exists) pnpm -F core build`, and when Vitest scheduled
63
- // it alongside the other build-theme suites on a fresh checkout, the
64
- // concurrent builds collided on packages/core/dist (core's build starts by
65
- // wiping dist), nondeterministically breaking whichever suite was mid-read.
66
71
  beforeAll(() => {
67
- ensureCoreBuilt();
72
+ if (!fs.existsSync(CORE_THEME_ENTRY) || !fs.existsSync(CORE_BUTTON_DTS)) {
73
+ execFileSync('pnpm', ['-F', '@astryxdesign/core', 'build'], {
74
+ cwd: REPO_ROOT,
75
+ stdio: 'pipe',
76
+ timeout: 180_000,
77
+ });
78
+ }
68
79
  }, 200_000);
69
80
 
70
81
  let tmpDir;
@@ -8,10 +8,15 @@ import {Center} from '@astryxdesign/core/Center';
8
8
  export default function AspectRatioCircleImage() {
9
9
  return (
10
10
  <Center width={300}>
11
- <AspectRatio ratio={1} shape="ellipse" fit="cover">
11
+ <AspectRatio ratio={1} shape="ellipse">
12
12
  <img
13
13
  src="https://lookaside.facebook.com/assets/astryx/light-home-square-1.png"
14
14
  alt="Circular image"
15
+ style={{
16
+ width: '100%',
17
+ height: '100%',
18
+ objectFit: 'cover',
19
+ }}
15
20
  />
16
21
  </AspectRatio>
17
22
  </Center>
@@ -25,11 +25,16 @@ export default function AspectRatioImageGallery() {
25
25
  <Center width={600}>
26
26
  <Grid columns={3} gap={4} width="100%">
27
27
  {images.map(({id, alt}) => (
28
- <AspectRatio key={id} ratio={4 / 3} fit="cover">
28
+ <AspectRatio key={id} ratio={4 / 3}>
29
29
  <img
30
30
  src="https://lookaside.facebook.com/assets/astryx/illustrative-horizontal-1.png"
31
31
  alt={alt}
32
- style={{borderRadius: 8}}
32
+ style={{
33
+ width: '100%',
34
+ height: '100%',
35
+ objectFit: 'cover',
36
+ borderRadius: 8,
37
+ }}
33
38
  />
34
39
  </AspectRatio>
35
40
  ))}
@@ -34,13 +34,16 @@ export default function AspectRatioShowcase() {
34
34
  <VStack key={label} gap={2} hAlign="center">
35
35
  <AspectRatio
36
36
  ratio={ratio}
37
- fit="cover"
38
37
  style={{
39
38
  height: 120,
40
39
  width: 'auto',
41
40
  borderRadius: 'var(--radius-container)',
42
41
  }}>
43
- <img src={src} alt={alt} />
42
+ <img
43
+ src={src}
44
+ alt={alt}
45
+ style={{width: '100%', height: '100%', objectFit: 'cover'}}
46
+ />
44
47
  </AspectRatio>
45
48
  <Text type="supporting" color="secondary">
46
49
  {label}
@@ -8,10 +8,15 @@ import {Center} from '@astryxdesign/core/Center';
8
8
  export default function AspectRatioSquareImage() {
9
9
  return (
10
10
  <Center width={300}>
11
- <AspectRatio ratio={1} fit="cover">
11
+ <AspectRatio ratio={1}>
12
12
  <img
13
13
  src="https://lookaside.facebook.com/assets/astryx/light-home-square-1.png"
14
14
  alt="1:1 square"
15
+ style={{
16
+ width: '100%',
17
+ height: '100%',
18
+ objectFit: 'cover',
19
+ }}
15
20
  />
16
21
  </AspectRatio>
17
22
  </Center>
@@ -8,10 +8,15 @@ import {Center} from '@astryxdesign/core/Center';
8
8
  export default function AspectRatioWidescreen() {
9
9
  return (
10
10
  <Center width={600}>
11
- <AspectRatio ratio={16 / 9} fit="cover">
11
+ <AspectRatio ratio={16 / 9}>
12
12
  <img
13
13
  src="https://lookaside.facebook.com/assets/astryx/light-scene-horizontal-1.png"
14
14
  alt="16:9 widescreen"
15
+ style={{
16
+ width: '100%',
17
+ height: '100%',
18
+ objectFit: 'cover',
19
+ }}
15
20
  />
16
21
  </AspectRatio>
17
22
  </Center>
@@ -6,7 +6,7 @@ export const doc = {
6
6
  exampleFor: 'Collapsible',
7
7
  name: 'Collapsible — Divided Accordion',
8
8
  displayName: 'Collapsible — Divided Accordion',
9
- description: 'FAQ-style accordion using the hasDividers prop on CollapsibleGroup: built-in row hairlines and density padding with zero custom CSS. Use for FAQs, settings lists, and nav sections.',
9
+ description: 'FAQ-style accordion using the dividers prop on CollapsibleGroup: built-in row hairlines and density padding with zero custom CSS. Use for FAQs, settings lists, and nav sections.',
10
10
  isReady: true,
11
11
  aspectRatio: 4 / 3,
12
12
  componentsUsed: ['Collapsible', 'CollapsibleGroup', 'Text'],
@@ -8,7 +8,7 @@ import {Text} from '@astryxdesign/core/Text';
8
8
  export default function CollapsibleDividedAccordion() {
9
9
  return (
10
10
  <div style={{width: '100%', maxWidth: 400}}>
11
- <CollapsibleGroup type="single" hasDividers defaultValue="reset">
11
+ <CollapsibleGroup type="single" dividers="between" defaultValue="reset">
12
12
  <Collapsible trigger="How do I reset my password?" value="reset">
13
13
  <Text type="body">
14
14
  Go to Settings → Security → Change Password. You'll receive a
@@ -439,11 +439,8 @@ export default function KanbanBoardTemplate() {
439
439
  let cb = columnRefCbs.current.get(id);
440
440
  if (!cb) {
441
441
  cb = el => {
442
- if (el) {
443
- columnEls.current.set(id, el);
444
- } else {
445
- columnEls.current.delete(id);
446
- }
442
+ if (el) {columnEls.current.set(id, el);}
443
+ else {columnEls.current.delete(id);}
447
444
  };
448
445
  columnRefCbs.current.set(id, cb);
449
446
  }
@@ -454,11 +451,8 @@ export default function KanbanBoardTemplate() {
454
451
  let cb = cardRefCbs.current.get(id);
455
452
  if (!cb) {
456
453
  cb = el => {
457
- if (el) {
458
- cardEls.current.set(id, el);
459
- } else {
460
- cardEls.current.delete(id);
461
- }
454
+ if (el) {cardEls.current.set(id, el);}
455
+ else {cardEls.current.delete(id);}
462
456
  };
463
457
  cardRefCbs.current.set(id, cb);
464
458
  }
@@ -493,9 +487,7 @@ export default function KanbanBoardTemplate() {
493
487
  ): DropTarget | null => {
494
488
  for (const [colId, el] of Array.from(columnEls.current.entries())) {
495
489
  const r = el.getBoundingClientRect();
496
- if (px < r.left || px > r.right || py < r.top || py > r.bottom) {
497
- continue;
498
- }
490
+ if (px < r.left || px > r.right || py < r.top || py > r.bottom) {continue;}
499
491
 
500
492
  const ids = itemsByColumn[colId]
501
493
  .filter(it => it.id !== draggedId)
@@ -504,9 +496,7 @@ export default function KanbanBoardTemplate() {
504
496
  let index = ids.length;
505
497
  for (let i = 0; i < ids.length; i++) {
506
498
  const cardEl = cardEls.current.get(ids[i]);
507
- if (!cardEl) {
508
- continue;
509
- }
499
+ if (!cardEl) {continue;}
510
500
  const cr = cardEl.getBoundingClientRect();
511
501
  if (py < cr.top + cr.height / 2) {
512
502
  index = i;
@@ -523,27 +513,21 @@ export default function KanbanBoardTemplate() {
523
513
  const commitDrag = (id: string, target: DropTarget) => {
524
514
  setItems(prev => {
525
515
  const moved = prev.find(it => it.id === id);
526
- if (!moved) {
527
- return prev;
528
- }
516
+ if (!moved) {return prev;}
529
517
 
530
518
  const rest = prev.filter(it => it.id !== id);
531
519
  const updated: WorkItem = {...moved, column: target.column};
532
520
  const colItems = rest.filter(it => it.column === target.column);
533
521
  const anchor = colItems[target.index];
534
522
 
535
- if (!anchor) {
536
- return [...rest, updated];
537
- }
523
+ if (!anchor) {return [...rest, updated];}
538
524
  const at = rest.indexOf(anchor);
539
525
  return [...rest.slice(0, at), updated, ...rest.slice(at)];
540
526
  });
541
527
  };
542
528
 
543
529
  const onCardPointerDown = (e: ReactPointerEvent, id: string) => {
544
- if (e.button !== 0) {
545
- return;
546
- }
530
+ if (e.button !== 0) {return;}
547
531
  // Let the card's own controls (the actions menu) handle the press.
548
532
  if (
549
533
  (e.target as HTMLElement).closest(
@@ -554,9 +538,7 @@ export default function KanbanBoardTemplate() {
554
538
  }
555
539
 
556
540
  const el = cardEls.current.get(id);
557
- if (!el) {
558
- return;
559
- }
541
+ if (!el) {return;}
560
542
 
561
543
  const rect = el.getBoundingClientRect();
562
544
  const startX = e.clientX;
@@ -592,9 +574,7 @@ export default function KanbanBoardTemplate() {
592
574
 
593
575
  const onUp = () => {
594
576
  teardownRef.current?.();
595
- if (started && target) {
596
- commitDrag(id, target);
597
- }
577
+ if (started && target) {commitDrag(id, target);}
598
578
  setDrag(null);
599
579
  };
600
580
 
@@ -614,9 +594,7 @@ export default function KanbanBoardTemplate() {
614
594
 
615
595
  // Suppress selection while dragging and detach listeners on unmount.
616
596
  useEffect(() => {
617
- if (!isDragging) {
618
- return;
619
- }
597
+ if (!isDragging) {return;}
620
598
  const previous = document.body.style.userSelect;
621
599
  document.body.style.userSelect = 'none';
622
600
  return () => {
@@ -634,9 +612,7 @@ export default function KanbanBoardTemplate() {
634
612
  const ghostTarget =
635
613
  drag && drag.target && drag.target.column === colId ? drag : null;
636
614
 
637
- if (visible.length === 0 && !ghostTarget) {
638
- return null;
639
- }
615
+ if (visible.length === 0 && !ghostTarget) {return null;}
640
616
 
641
617
  const nodes: ReactNode[] = visible.map(it => (
642
618
  <BoardCard
@@ -1,93 +0,0 @@
1
- // Copyright (c) Meta Platforms, Inc. and affiliates.
2
-
3
- import {describe, it, expect} from 'vitest';
4
-
5
- async function applyTransform(source) {
6
- const {default: transform} =
7
- await import('../rename-switch-label-spacing-default-to-hug.mjs');
8
- const jscodeshift = (await import('jscodeshift')).default;
9
- const j = jscodeshift.withParser('tsx');
10
- const api = {jscodeshift: j, stats: () => {}, report: () => {}};
11
- const file = {source, path: 'test.tsx'};
12
- const result = transform(file, api);
13
- return result ?? source;
14
- }
15
-
16
- describe('rename-switch-label-spacing-default-to-hug', () => {
17
- it('renames Switch labelSpacing="default" to labelSpacing="hug"', async () => {
18
- const input = `<Switch label="Notify" value={on} labelSpacing="default" />`;
19
- const output = await applyTransform(input);
20
- expect(output).toContain("'hug'");
21
- expect(output).not.toContain('default');
22
- });
23
-
24
- it("renames expression-container labelSpacing={'default'}", async () => {
25
- const input = `<Switch label="Notify" value={on} labelSpacing={'default'} />`;
26
- const output = await applyTransform(input);
27
- expect(output).toContain("'hug'");
28
- expect(output).not.toContain("'default'");
29
- });
30
-
31
- it('renames default inside a ternary labelSpacing expression', async () => {
32
- const input = `<Switch label="Notify" value={on} labelSpacing={isRow ? 'spread' : 'default'} />`;
33
- const output = await applyTransform(input);
34
- expect(output).toContain("'hug'");
35
- expect(output).toContain("'spread'");
36
- expect(output).not.toContain("'default'");
37
- });
38
-
39
- it('does not change labelSpacing="spread"', async () => {
40
- const input = `<Switch label="Notify" value={on} labelSpacing="spread" />`;
41
- const output = await applyTransform(input);
42
- expect(output).toBe(input);
43
- });
44
-
45
- it('does not touch labelSpacing="default" on non-Switch components', async () => {
46
- const input = `<LegacyToggle labelSpacing="default" />`;
47
- const output = await applyTransform(input);
48
- expect(output).toBe(input);
49
- });
50
-
51
- it('does not touch other props with a "default" value on Switch', async () => {
52
- const input = `<Switch label="Notify" value={on} data-variant="default" />`;
53
- const output = await applyTransform(input);
54
- expect(output).toBe(input);
55
- });
56
-
57
- it('does not touch unrelated "default" string literals', async () => {
58
- const input = `const theme = {mode: 'default', label: 'Default Mode'};`;
59
- const output = await applyTransform(input);
60
- expect(output).toBe(input);
61
- });
62
-
63
- it('renames object labelSpacing prop in files importing Switch', async () => {
64
- const input = `import {Switch} from '@astryxdesign/core/Switch';
65
- const cfg = {labelSpacing: 'default'};`;
66
- const output = await applyTransform(input);
67
- expect(output).toContain("labelSpacing: 'hug'");
68
- expect(output).not.toContain("'default'");
69
- });
70
-
71
- it("renames labelSpacing: 'default' as const in files importing Switch", async () => {
72
- const input = `import {Switch} from '@astryxdesign/core/Switch';
73
- const ROWS = [{labelSpacing: 'default' as const, label: 'Adjacent'}];`;
74
- const output = await applyTransform(input);
75
- expect(output).toContain("'hug' as const");
76
- expect(output).not.toContain("'default'");
77
- });
78
-
79
- it('does not rename object labelSpacing prop when Switch is not imported', async () => {
80
- const input = `const cfg = {labelSpacing: 'default'};`;
81
- const output = await applyTransform(input);
82
- expect(output).toBe(input);
83
- });
84
-
85
- it('renames Storybook argTypes options in files importing Switch', async () => {
86
- const input = `import {Switch} from '@astryxdesign/core/Switch';
87
- const meta = {argTypes: {labelSpacing: {control: 'select', options: ['default', 'spread']}}};`;
88
- const output = await applyTransform(input);
89
- expect(output).toContain("'hug'");
90
- expect(output).toContain("'spread'");
91
- expect(output).not.toContain("'default'");
92
- });
93
- });
@@ -1,19 +0,0 @@
1
- // Copyright (c) Meta Platforms, Inc. and affiliates.
2
-
3
- /**
4
- * @file v0.1.5 transform manifest
5
- *
6
- * Lists all codemods for the v0.1.5 release in the order they should run.
7
- */
8
-
9
- import renameSwitchLabelSpacingDefaultToHug, {
10
- meta as renameSwitchLabelSpacingDefaultToHugMeta,
11
- } from './rename-switch-label-spacing-default-to-hug.mjs';
12
-
13
- export default [
14
- {
15
- name: 'rename-switch-label-spacing-default-to-hug',
16
- transform: renameSwitchLabelSpacingDefaultToHug,
17
- meta: renameSwitchLabelSpacingDefaultToHugMeta,
18
- },
19
- ];
@@ -1,140 +0,0 @@
1
- // Copyright (c) Meta Platforms, Inc. and affiliates.
2
-
3
- /**
4
- * @file Codemod: Rename Switch labelSpacing "default" to "hug"
5
- * @see https://github.com/facebook/astryx/issues/2889
6
- *
7
- * The `labelSpacing="default"` value is renamed to `labelSpacing="hug"` so
8
- * the name describes the behavior (label and switch hug each other), matching
9
- * the behavioral register of the sibling `spread` value and the existing
10
- * `hug`/`fill` layout vocabulary on TabList and SegmentedControl. The old
11
- * `default` value keeps working as a deprecated alias, so this codemod is a
12
- * cleanup, not a build fix.
13
- *
14
- * Transforms (scoped to Switch):
15
- * - <Switch labelSpacing="default" /> → <Switch labelSpacing="hug" />
16
- * - <Switch labelSpacing={'default'} /> → <Switch labelSpacing={'hug'} />
17
- * - <Switch labelSpacing={cond ? 'default' : x}> → <Switch labelSpacing={cond ? 'hug' : x}>
18
- * - Storybook argTypes: labelSpacing: { options: [..., 'default', ...] }
19
- * → replaces 'default' with 'hug' (files importing Switch)
20
- * - Object properties: { labelSpacing: 'default' } / { labelSpacing: 'default' as const }
21
- * → { labelSpacing: 'hug' } (files importing Switch)
22
- *
23
- * The transform deliberately does NOT touch bare `'default'` strings that are
24
- * not a `labelSpacing` value (an extremely common string otherwise), so it is
25
- * safe to run across an entire codebase.
26
- */
27
-
28
- export const meta = {
29
- title: 'Rename Switch labelSpacing "default" to "hug"',
30
- description:
31
- 'Renames the `labelSpacing="default"` value to `labelSpacing="hug"` on ' +
32
- 'Switch. The old value keeps working as a deprecated alias. Also updates ' +
33
- 'Storybook labelSpacing argTypes options and object-literal `labelSpacing` ' +
34
- 'props in files that import Switch.',
35
- pr: '#2889',
36
- };
37
-
38
- const OLD_VALUE = 'default';
39
- const NEW_VALUE = 'hug';
40
-
41
- /** Prop whose value is being renamed. */
42
- const TARGET_PROP = 'labelSpacing';
43
-
44
- /** Components whose `labelSpacing` prop accepts a SwitchLabelSpacing value. */
45
- const TARGET_COMPONENTS = new Set(['Switch']);
46
-
47
- export default function transformer(file, api) {
48
- const j = api.jscodeshift;
49
- const root = j(file.source);
50
- let hasChanges = false;
51
-
52
- /** Rewrite a string-literal node when it equals the old value. */
53
- function renameStringLiteral(node) {
54
- if (!node) return false;
55
- if (
56
- (node.type === 'StringLiteral' || node.type === 'Literal') &&
57
- node.value === OLD_VALUE
58
- ) {
59
- node.value = NEW_VALUE;
60
- if (node.raw) node.raw = `'${NEW_VALUE}'`;
61
- return true;
62
- }
63
- return false;
64
- }
65
-
66
- /** Recursively rewrite the old value inside ternary/logical expressions. */
67
- function renameInExpression(node) {
68
- if (!node) return false;
69
- let changed = false;
70
- if (node.type === 'StringLiteral' || node.type === 'Literal') {
71
- changed = renameStringLiteral(node) || changed;
72
- } else if (node.type === 'ConditionalExpression') {
73
- changed = renameInExpression(node.consequent) || changed;
74
- changed = renameInExpression(node.alternate) || changed;
75
- } else if (node.type === 'LogicalExpression') {
76
- changed = renameInExpression(node.left) || changed;
77
- changed = renameInExpression(node.right) || changed;
78
- } else if (node.type === 'TSAsExpression') {
79
- // `'default' as const` → rewrite the inner expression
80
- changed = renameInExpression(node.expression) || changed;
81
- }
82
- return changed;
83
- }
84
-
85
- // --- 1. JSX attribute: labelSpacing="default" / labelSpacing={'default'} on Switch ---
86
- root.find(j.JSXOpeningElement).forEach(path => {
87
- const name = path.node.name;
88
- const componentName = name.type === 'JSXIdentifier' ? name.name : null;
89
- if (!componentName || !TARGET_COMPONENTS.has(componentName)) return;
90
-
91
- path.node.attributes.forEach(attr => {
92
- if (attr.type !== 'JSXAttribute') return;
93
- if (!attr.name || attr.name.name !== TARGET_PROP) return;
94
-
95
- const value = attr.value;
96
- if (!value) return;
97
-
98
- if (value.type === 'StringLiteral' || value.type === 'Literal') {
99
- if (renameStringLiteral(value)) hasChanges = true;
100
- } else if (value.type === 'JSXExpressionContainer') {
101
- if (renameInExpression(value.expression)) hasChanges = true;
102
- }
103
- });
104
- });
105
-
106
- // Object-property / argTypes transforms only run in files that use a target
107
- // component — keeps unrelated `{ labelSpacing: 'default' }` strings safe.
108
- const importsTarget =
109
- root
110
- .find(j.ImportSpecifier)
111
- .filter(p => TARGET_COMPONENTS.has(p.node.imported?.name))
112
- .size() > 0;
113
-
114
- if (importsTarget) {
115
- const PropertyType = j.ObjectProperty ?? j.Property;
116
-
117
- // --- 2. Object property: { labelSpacing: 'default' } / 'default' as const ---
118
- root.find(PropertyType, {key: {name: TARGET_PROP}}).forEach(path => {
119
- if (renameInExpression(path.node.value)) hasChanges = true;
120
- });
121
-
122
- // --- 3. Storybook argTypes: labelSpacing: { options: [..., 'default', ...] } ---
123
- root.find(PropertyType, {key: {name: TARGET_PROP}}).forEach(path => {
124
- const value = path.node.value;
125
- if (!value || value.type !== 'ObjectExpression') return;
126
-
127
- const optionsProp = value.properties.find(
128
- p => p.key && (p.key.name === 'options' || p.key.value === 'options'),
129
- );
130
- if (optionsProp && optionsProp.value.type === 'ArrayExpression') {
131
- optionsProp.value.elements.forEach(el => {
132
- if (renameStringLiteral(el)) hasChanges = true;
133
- });
134
- }
135
- });
136
- }
137
-
138
- if (!hasChanges) return undefined;
139
- return root.toSource({quote: 'single'});
140
- }