@astryxdesign/cli 0.1.7-canary.cc83e7a → 0.1.7-canary.d91c851

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 (96) hide show
  1. package/README.md +115 -19
  2. package/docs/cli-integrations.doc.mjs +150 -0
  3. package/docs/getting-started.doc.mjs +9 -9
  4. package/docs/migration.doc.mjs +18 -18
  5. package/docs/principles.doc.dense.mjs +1 -1
  6. package/docs/principles.doc.mjs +6 -6
  7. package/docs/principles.doc.zh.mjs +1 -1
  8. package/docs/styling-libraries.doc.mjs +3 -3
  9. package/docs/styling.doc.mjs +4 -4
  10. package/docs/theme.doc.dense.mjs +2 -2
  11. package/docs/theme.doc.mjs +7 -7
  12. package/docs/theme.doc.zh.mjs +1 -1
  13. package/docs/tokens.doc.mjs +1 -1
  14. package/docs/working-with-ai.doc.mjs +18 -18
  15. package/package.json +11 -10
  16. package/scripts/postinstall.mjs +74 -0
  17. package/src/api/doctor.mjs +3 -3
  18. package/src/codemods/__tests__/registry.test.mjs +1 -0
  19. package/src/codemods/ensure-jscodeshift.mjs +11 -27
  20. package/src/codemods/registry.mjs +1 -0
  21. package/src/codemods/run-codemod.mjs +1 -1
  22. package/src/codemods/runner.mjs +2 -2
  23. package/src/codemods/transforms/v0.1.0/__tests__/drop-xds-prefix-imports.test.mjs +42 -7
  24. package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-module-specifiers.test.mjs +43 -0
  25. package/src/codemods/transforms/v0.1.0/drop-xds-prefix-imports.mjs +102 -3
  26. package/src/codemods/transforms/v0.1.0/migrate-xds-module-specifiers.mjs +72 -6
  27. package/src/codemods/transforms/v0.1.8/__tests__/rename-avatar-size-scale.test.mjs +161 -0
  28. package/src/codemods/transforms/v0.1.8/index.mjs +19 -0
  29. package/src/codemods/transforms/v0.1.8/rename-avatar-size-scale.mjs +234 -0
  30. package/src/commands/agent-docs.mjs +128 -14
  31. package/src/commands/agent-docs.test.mjs +97 -4
  32. package/src/commands/build-theme.mjs +10 -71
  33. package/src/commands/build.mjs +15 -15
  34. package/src/commands/cli-postinstall.test.mjs +42 -0
  35. package/src/commands/component/index.mjs +4 -4
  36. package/src/commands/discover.mjs +7 -5
  37. package/src/commands/docs.mjs +4 -4
  38. package/src/commands/hook/index.mjs +4 -4
  39. package/src/commands/init.mjs +48 -152
  40. package/src/commands/init.next-steps.test.mjs +1 -1
  41. package/src/commands/interactive-guard.test.mjs +19 -22
  42. package/src/commands/json-contract.test.mjs +1 -1
  43. package/src/commands/layout.mjs +1 -1
  44. package/src/commands/search.mjs +4 -4
  45. package/src/commands/setup-nudge.test.mjs +108 -0
  46. package/src/commands/swizzle.mjs +11 -34
  47. package/src/commands/template.mjs +11 -31
  48. package/src/commands/upgrade.mjs +138 -32
  49. package/src/commands/upgrade.test.mjs +155 -1
  50. package/src/index.mjs +40 -6
  51. package/src/lib/component-format.mjs +2 -1
  52. package/src/lib/term-log.mjs +48 -0
  53. package/src/types/upgrade.d.ts +39 -2
  54. package/src/utils/package-manager.mjs +78 -0
  55. package/src/utils/package-manager.test.mjs +108 -1
  56. package/src/utils/path-safety.mjs +0 -18
  57. package/src/utils/update-check.mjs +2 -1
  58. package/templates/blocks/components/Avatar/AvatarFallbackChain.tsx +4 -4
  59. package/templates/blocks/components/Avatar/AvatarGroup.tsx +2 -2
  60. package/templates/blocks/components/Avatar/AvatarInitialsFallback.tsx +1 -1
  61. package/templates/blocks/components/Avatar/AvatarShowcase.tsx +4 -4
  62. package/templates/blocks/components/Avatar/AvatarUserCard.tsx +1 -1
  63. package/templates/blocks/components/Avatar/AvatarWithImage.tsx +4 -4
  64. package/templates/blocks/components/Avatar/AvatarWithStatus.tsx +3 -3
  65. package/templates/blocks/components/AvatarGroup/AvatarGroupShowcase.tsx +2 -2
  66. package/templates/blocks/components/AvatarGroupOverflow/AvatarGroupOverflowCustomText.tsx +1 -1
  67. package/templates/blocks/components/AvatarGroupOverflow/AvatarGroupOverflowDefault.tsx +1 -1
  68. package/templates/blocks/components/AvatarGroupOverflow/AvatarGroupOverflowShowcase.tsx +2 -2
  69. package/templates/blocks/components/AvatarStatusDot/AvatarStatusDotShowcase.tsx +3 -3
  70. package/templates/blocks/components/AvatarStatusDot/AvatarStatusDotVariants.tsx +3 -3
  71. package/templates/blocks/components/Carousel/CarouselSnap.tsx +1 -1
  72. package/templates/blocks/components/ChatMessage/ChatMessageAvatarName.tsx +2 -2
  73. package/templates/blocks/components/ChatMessage/ChatMessageMultiBubble.tsx +1 -1
  74. package/templates/blocks/components/ChatMessageBubble/ChatMessageBubbleGrouping.tsx +1 -1
  75. package/templates/blocks/components/ChatMessageBubble/ChatMessageBubbleMetadata.tsx +1 -1
  76. package/templates/blocks/components/ChatMessageList/ChatMessageListDensity.tsx +5 -9
  77. package/templates/blocks/components/ChatMessageList/ChatMessageListFullFeatured.tsx +1 -1
  78. package/templates/blocks/components/HoverCard/HoverCardShowcase.tsx +1 -1
  79. package/templates/blocks/components/Item/ItemWithMedia.tsx +2 -2
  80. package/templates/blocks/components/ListItem/ListItemWithMedia.tsx +2 -2
  81. package/templates/blocks/components/Pagination/PaginationDotsCarousel.tsx +2 -6
  82. package/templates/blocks/components/Stack/StackFillItem.tsx +2 -6
  83. package/templates/blocks/components/TabList/TabListTabsWithActions.doc.mjs +1 -1
  84. package/templates/blocks/components/TabList/TabListTabsWithActions.tsx +2 -7
  85. package/templates/blocks/components/TypeaheadItem/TypeaheadItemShowcase.tsx +1 -1
  86. package/templates/pages/ai-chat/page.tsx +4 -4
  87. package/templates/pages/dashboard-portfolio/page.tsx +3 -11
  88. package/templates/pages/detail-page/page.tsx +5 -12
  89. package/templates/pages/documentation-design/page.tsx +1 -1
  90. package/templates/pages/messaging-shell/page.tsx +6 -8
  91. package/templates/pages/table-grouped/page.tsx +9 -14
  92. package/templates/pages/table-page/page.tsx +7 -9
  93. package/templates/pages/table-page-heatmap-status/page.tsx +5 -13
  94. package/docs/integration-authoring.md +0 -105
  95. package/src/utils/interactive.mjs +0 -76
  96. package/src/utils/interactive.test.mjs +0 -70
@@ -10,7 +10,7 @@ export const docsZh = {
10
10
  { section: 'Theme Props', title: 'Theme 属性', content: [null] },
11
11
  { section: 'Creating a Custom Theme', title: '创建自定义主题', content: [{ type: 'prose', text: '使用 CLI 向导(推荐)或手动 defineTheme。只覆盖与默认值不同的令牌。' }, null] },
12
12
  { section: 'defineTheme', title: 'defineTheme', content: [{ type: 'prose', text: '支持比例配置(typography、radius、motion)+ 显式令牌覆盖 + 组件覆盖。' }, null, null] },
13
- { section: 'Building Themes for Production', title: '生产构建', content: [{ type: 'prose', text: 'npx astryx theme build 将 defineTheme 编译为静态 CSS。输出 .css + .js(__built:true)+ .d.ts。' }, null, null, null, null] },
13
+ { section: 'Building Themes for Production', title: '生产构建', content: [{ type: 'prose', text: 'astryx theme build 将 defineTheme 编译为静态 CSS。输出 .css + .js(__built:true)+ .d.ts。' }, null, null, null, null] },
14
14
  { section: 'Runtime vs Built Themes', title: '运行时 vs 构建', content: [{ type: 'prose', text: '运行时:useInsertionEffect 在客户端注入样式。构建:静态 CSS 在首次渲染时就存在。SSR 应用请使用 /built + theme.css。' }, null, null, null] },
15
15
  { section: 'Light/Dark Mode', title: '亮/暗模式', content: [{ type: 'prose', text: "令牌值使用 [light, dark] 元组实现自动模式切换。Theme 上 mode='system'(默认)跟随系统偏好。" }, null, null] },
16
16
  { section: 'Nesting Themes', title: '嵌套主题', content: [{ type: 'prose', text: '将不同部分包裹在独立的 <Theme> 提供者中。' }, null] },
@@ -1068,7 +1068,7 @@ export const docs = {
1068
1068
  },
1069
1069
  {
1070
1070
  "type": "prose",
1071
- "text": "See `npx astryx docs styling` for how to apply tokens via xstyle, className, and compound component patterns. See `npx astryx docs theme` for overriding tokens with defineTheme."
1071
+ "text": "See `astryx docs styling` for how to apply tokens via xstyle, className, and compound component patterns. See `astryx docs theme` for overriding tokens with defineTheme."
1072
1072
  }
1073
1073
  ]
1074
1074
  }
@@ -34,7 +34,7 @@ export const docs = {
34
34
  type: 'code',
35
35
  lang: 'text',
36
36
  label: 'Paste this into your AI',
37
- code: 'Install @astryxdesign/cli and run `npx astryx init --features agents` to set up your Astryx context. Read the generated file.',
37
+ code: 'Install @astryxdesign/cli and run `npx @astryxdesign/cli init --features agents` to set up your Astryx context. Read the generated file.',
38
38
  },
39
39
  {
40
40
  type: 'prose',
@@ -48,9 +48,9 @@ export const docs = {
48
48
  type: 'code',
49
49
  lang: 'bash',
50
50
  label: 'Manual options',
51
- code: `npx astryx init --features agents --agent claude # CLAUDE.md
52
- npx astryx init --features agents --agent cursor # .cursorrules
53
- npx astryx init --features agents --agent codex # AGENTS.md (Copilot, Codex, etc.)`,
51
+ code: `npx @astryxdesign/cli init --features agents --agent claude # CLAUDE.md
52
+ npx @astryxdesign/cli init --features agents --agent cursor # .cursorrules
53
+ npx @astryxdesign/cli init --features agents --agent codex # AGENTS.md (Copilot, Codex, etc.)`,
54
54
  },
55
55
  ],
56
56
  },
@@ -65,9 +65,9 @@ npx astryx init --features agents --agent codex # AGENTS.md (Copilot, Codex,
65
65
  type: 'list',
66
66
  style: 'ordered',
67
67
  items: [
68
- '`npx astryx template --list`: find a related page pattern to use as reference',
69
- '`npx astryx template <name> --skeleton`: study the layout structure',
70
- '`npx astryx component <Name>`: read props and examples for every component used',
68
+ '`astryx template --list`: find a related page pattern to use as reference',
69
+ '`astryx template <name> --skeleton`: study the layout structure',
70
+ '`astryx component <Name>`: read props and examples for every component used',
71
71
  ],
72
72
  },
73
73
  {
@@ -88,7 +88,7 @@ npx astryx init --features agents --agent codex # AGENTS.md (Copilot, Codex,
88
88
  lang: 'bash',
89
89
  label: 'Install as a Cursor user rule',
90
90
  code: `mkdir -p ~/.cursor/rules
91
- npx astryx init --features agents --agent-docs-path ~/.cursor/rules/xds.mdc`,
91
+ npx @astryxdesign/cli init --features agents --agent-docs-path ~/.cursor/rules/xds.mdc`,
92
92
  },
93
93
  ],
94
94
  },
@@ -109,12 +109,12 @@ npx astryx init --features agents --agent-docs-path ~/.cursor/rules/xds.mdc`,
109
109
  2. How do you make an Dialog non-dismissible?
110
110
  3. What prop does Selector use for its items?
111
111
 
112
- If you don't know all three, run \`npx astryx init --features agents\` to generate agent docs, then read the generated file.`,
112
+ If you don't know all three, run \`npx @astryxdesign/cli init --features agents\` to generate agent docs, then read the generated file.`,
113
113
  },
114
114
  ],
115
115
  },
116
116
  {
117
- title: 'The npx astryx Pattern',
117
+ title: 'The astryx Pattern',
118
118
  content: [
119
119
  {
120
120
  type: 'prose',
@@ -130,16 +130,16 @@ If you don't know all three, run \`npx astryx init --features agents\` to genera
130
130
  },
131
131
  {
132
132
  type: 'prose',
133
- text: 'With this alias, agents use `npx astryx component --list` instead of guessing the binary path. The `--` separator is standard npm convention for passing flags to scripts.',
133
+ text: 'With this alias, agents use `astryx component --list` instead of guessing the binary path. The `--` separator is standard npm convention for passing flags to scripts.',
134
134
  },
135
135
  {
136
136
  type: 'code',
137
137
  lang: 'bash',
138
138
  label: 'Reliable CLI invocation',
139
- code: `npx astryx component --list
140
- npx astryx component Dialog --dense
141
- npx astryx docs styling --dense
142
- npx astryx docs tokens --dense`,
139
+ code: `astryx component --list
140
+ astryx component Dialog --dense
141
+ astryx docs styling --dense
142
+ astryx docs tokens --dense`,
143
143
  },
144
144
  ],
145
145
  },
@@ -154,9 +154,9 @@ npx astryx docs tokens --dense`,
154
154
  type: 'code',
155
155
  lang: 'bash',
156
156
  label: 'Dense output for pasting into AI conversations',
157
- code: `npx astryx component Dialog --dense
158
- npx astryx docs styling --dense
159
- npx astryx docs tokens --dense`,
157
+ code: `astryx component Dialog --dense
158
+ astryx docs styling --dense
159
+ astryx docs tokens --dense`,
160
160
  },
161
161
  ],
162
162
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astryxdesign/cli",
3
- "version": "0.1.7-canary.cc83e7a",
3
+ "version": "0.1.7-canary.d91c851",
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",
@@ -67,22 +67,22 @@
67
67
  "files": [
68
68
  "bin",
69
69
  "src",
70
+ "scripts/postinstall.mjs",
70
71
  "templates",
71
72
  "docs",
72
73
  "CHANGELOG.md"
73
74
  ],
74
75
  "dependencies": {
75
- "@clack/prompts": "^1.7.0",
76
76
  "commander": "^12.1.0",
77
77
  "jiti": "^2.7.0",
78
78
  "jscodeshift": "^17.3.0",
79
79
  "zod": "^4.4.3"
80
80
  },
81
81
  "peerDependencies": {
82
- "@astryxdesign/charts": "0.1.7-canary.cc83e7a",
83
- "@astryxdesign/core": "0.1.7-canary.cc83e7a",
84
- "@astryxdesign/lab": "0.1.7-canary.cc83e7a",
85
- "@astryxdesign/theme-neutral": "0.1.7-canary.cc83e7a",
82
+ "@astryxdesign/charts": "0.1.7-canary.d91c851",
83
+ "@astryxdesign/core": "0.1.7-canary.d91c851",
84
+ "@astryxdesign/lab": "0.1.7-canary.d91c851",
85
+ "@astryxdesign/theme-neutral": "0.1.7-canary.d91c851",
86
86
  "gpt-tokenizer": "^3.4.0"
87
87
  },
88
88
  "peerDependenciesMeta": {
@@ -100,14 +100,15 @@
100
100
  }
101
101
  },
102
102
  "devDependencies": {
103
- "@astryxdesign/charts": "0.1.7-canary.cc83e7a",
104
- "@astryxdesign/core": "0.1.7-canary.cc83e7a",
105
- "@astryxdesign/lab": "0.1.7-canary.cc83e7a",
106
- "@astryxdesign/theme-neutral": "0.1.7-canary.cc83e7a",
103
+ "@astryxdesign/charts": "0.1.7-canary.d91c851",
104
+ "@astryxdesign/core": "0.1.7-canary.d91c851",
105
+ "@astryxdesign/lab": "0.1.7-canary.d91c851",
106
+ "@astryxdesign/theme-neutral": "0.1.7-canary.d91c851",
107
107
  "gpt-tokenizer": "^3.4.0"
108
108
  },
109
109
  "scripts": {
110
110
  "astryx": "node bin/astryx.mjs",
111
+ "postinstall": "node scripts/postinstall.mjs",
111
112
  "typecheck:template-docs": "tsc --project tsconfig.template-docs.json",
112
113
  "typecheck:json-api": "tsc --project tsconfig.json-api.json && tsc --project tsconfig.api-contract.json"
113
114
  }
@@ -0,0 +1,74 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file @astryxdesign/cli postinstall — nudge to run `astryx init`.
5
+ *
6
+ * Enforcement layer 2 of making `astryx init` foolproof: when the CLI is
7
+ * installed as a project dependency and the project hasn't run init yet, print a
8
+ * one-line next-step so agents/humans discover it.
9
+ *
10
+ * Reuses the ONE setup check (isAstryxInitialized from agent-docs.mjs — a
11
+ * dep-free import chain, safe at install time). Non-interactive, never fails the
12
+ * install, and stays quiet:
13
+ * - in the monorepo/source build (not under node_modules),
14
+ * - during npx's transient fetch (npx runs the bin — likely `init` — right
15
+ * after, so nudging here would double up), and
16
+ * - once setup is already done.
17
+ */
18
+
19
+ import {fileURLToPath, pathToFileURL} from 'node:url';
20
+
21
+ const HERE = fileURLToPath(import.meta.url);
22
+
23
+ /**
24
+ * Pure decision: should the postinstall print the setup nudge? Split out so the
25
+ * matrix is unit-testable without an actual npm install.
26
+ *
27
+ * @param {object} opts
28
+ * @param {string} opts.scriptPath - Absolute path of this script (location tells
29
+ * us dependency vs monorepo vs npx-cache).
30
+ * @param {string} [opts.npmCommand] - process.env.npm_command ('install', 'exec', …).
31
+ * @param {boolean} opts.isSetUp - Whether the project already ran init.
32
+ * @returns {boolean}
33
+ */
34
+ export function shouldNudge({scriptPath, npmCommand, isSetUp} = {}) {
35
+ if (!scriptPath || !scriptPath.includes('node_modules')) return false; // monorepo/source build
36
+ if (scriptPath.includes('_npx') || npmCommand === 'exec') return false; // npx transient — init runs next
37
+ if (isSetUp) return false; // already set up — stay quiet
38
+ return true;
39
+ }
40
+
41
+ /** @param {string} root @returns {Promise<boolean>} */
42
+ async function projectIsSetUp(root) {
43
+ try {
44
+ const {isAstryxInitialized} = await import('../src/commands/agent-docs.mjs');
45
+ return isAstryxInitialized(root);
46
+ } catch {
47
+ return false; // best-effort — if the check can't load, fall through and nudge
48
+ }
49
+ }
50
+
51
+ async function main() {
52
+ const root = process.env.INIT_CWD || process.cwd();
53
+ const nudge = shouldNudge({
54
+ scriptPath: HERE,
55
+ npmCommand: process.env.npm_command,
56
+ isSetUp: await projectIsSetUp(root),
57
+ });
58
+ if (nudge) {
59
+ // Scoped package form (`@astryxdesign/cli`) — always resolves to us. Bare
60
+ // `npx astryx` would fetch an unrelated look-alike package (see PR #4151).
61
+ // After that lands, switch this to its getCliInvocation() single source of truth.
62
+ process.stdout.write(
63
+ '\nNext step: run `npx @astryxdesign/cli init` to finish setup and install the Astryx agent prompt.\n\n',
64
+ );
65
+ }
66
+ }
67
+
68
+ // Run only when executed directly (`node scripts/postinstall.mjs`), never when
69
+ // imported by tests.
70
+ if (import.meta.url === pathToFileURL(process.argv[1] || '').href) {
71
+ main()
72
+ .catch(() => {})
73
+ .finally(() => process.exit(0));
74
+ }
@@ -26,7 +26,7 @@ import {createRequire} from 'node:module';
26
26
 
27
27
  import {MIN_NODE_VERSION, isNodeVersionSupported} from '../lib/node-version.mjs';
28
28
  import {CLI_ROOT, findCoreDir} from '../utils/paths.mjs';
29
- import {detectPackageManager} from '../utils/package-manager.mjs';
29
+ import {detectPackageManager, getCliInvocation} from '../utils/package-manager.mjs';
30
30
  import {findConfigPath, Project} from '../lib/project.mjs';
31
31
  import {semverCompare} from '../utils/semver.mjs';
32
32
 
@@ -343,7 +343,7 @@ export function checkAgentDocs(ctx) {
343
343
  label: 'AI agent docs',
344
344
  status: 'info',
345
345
  message: 'No agent docs (CLAUDE.md / AGENTS.md / .cursorrules) found.',
346
- fix: 'Generate agent docs with `astryx init --features agents`.',
346
+ fix: `Generate agent docs with \`${getCliInvocation(ctx.cwd)} init --features agents\`.`,
347
347
  };
348
348
  }
349
349
 
@@ -365,7 +365,7 @@ export function checkAgentDocs(ctx) {
365
365
  label: 'AI agent docs',
366
366
  status: 'warn',
367
367
  message: `Agent docs present (${present.join(', ')}) but no Astryx section markers found.`,
368
- fix: 'Add the Astryx section to your agent docs with `astryx init --features agents`.',
368
+ fix: `Add the Astryx section to your agent docs with \`${getCliInvocation(ctx.cwd)} init --features agents\`.`,
369
369
  };
370
370
  }
371
371
 
@@ -21,6 +21,7 @@ describe('registry', () => {
21
21
  '0.1.3',
22
22
  '0.1.5',
23
23
  '0.1.7',
24
+ '0.1.8',
24
25
  ]);
25
26
  });
26
27
  });
@@ -3,16 +3,15 @@
3
3
  /**
4
4
  * @file Lazy jscodeshift installer
5
5
  *
6
- * Checks if jscodeshift is available and offers to install it on-demand.
6
+ * Checks if jscodeshift is available and installs it on-demand.
7
7
  * Keeps the CLI lean — jscodeshift is only needed for codemods.
8
8
  *
9
- * In non-interactive environments (CI, LLM agents), the interactive prompt
10
- * is skipped. Pass `installDeps: true` to auto-install without prompting,
11
- * or the command will fail with a helpful error message.
9
+ * Non-interactive by default (no prompts): pass `installDeps: true` to
10
+ * auto-install, otherwise the command fails fast with a helpful error.
12
11
  */
13
12
 
14
13
  import {execSync} from 'node:child_process';
15
- import * as p from '@clack/prompts';
14
+ import * as p from '../lib/term-log.mjs';
16
15
  import {detectPackageManager} from '../utils/package-manager.mjs';
17
16
 
18
17
  /**
@@ -30,32 +29,17 @@ export async function ensureJscodeshift({installDeps = false, silent = false} =
30
29
  } catch {
31
30
  log.warn('jscodeshift is required for codemods but not installed.');
32
31
 
33
- const isInteractive = process.stdout.isTTY && !process.env.CI;
34
-
35
32
  if (installDeps) {
36
- // Explicit opt-in — install without prompting
33
+ // Explicit opt-in — install without prompting.
37
34
  return installJscodeshift(silent);
38
35
  }
39
36
 
40
- if (!isInteractive || silent) {
41
- // Non-interactive environment (or --json) — fail fast with a helpful message
42
- log.error(
43
- 'Cannot run codemods without jscodeshift. ' +
44
- 'Use --install-deps to auto-install in non-interactive environments.',
45
- );
46
- return false;
47
- }
48
-
49
- // Interactive TTY — prompt as before
50
- const shouldInstall = await p.confirm({
51
- message: 'Install jscodeshift now?',
52
- initialValue: true,
53
- });
54
- if (p.isCancel(shouldInstall) || !shouldInstall) {
55
- p.log.error('Cannot run codemods without jscodeshift.');
56
- return false;
57
- }
58
- return installJscodeshift(silent);
37
+ // Non-interactive by default: fail fast with a helpful message instead of
38
+ // prompting (the CLI never blocks on a TTY).
39
+ log.error(
40
+ 'Cannot run codemods without jscodeshift. Use --install-deps to auto-install.',
41
+ );
42
+ return false;
59
43
  }
60
44
  }
61
45
 
@@ -22,6 +22,7 @@ const registry = new Map([
22
22
  ['0.1.3', () => import('./transforms/v0.1.3/index.mjs')],
23
23
  ['0.1.5', () => import('./transforms/v0.1.5/index.mjs')],
24
24
  ['0.1.7', () => import('./transforms/v0.1.7/index.mjs')],
25
+ ['0.1.8', () => import('./transforms/v0.1.8/index.mjs')],
25
26
  ]);
26
27
 
27
28
  // Re-export from the shared utility so registry callers and other consumers
@@ -29,7 +29,7 @@
29
29
 
30
30
  import * as fs from 'node:fs';
31
31
  import * as path from 'node:path';
32
- import * as p from '@clack/prompts';
32
+ import * as p from '../lib/term-log.mjs';
33
33
  import {findConfigPath} from '../lib/project.mjs';
34
34
  import {fixDirectiveCorruption, validateOutput} from './runner.mjs';
35
35
 
@@ -10,7 +10,7 @@
10
10
 
11
11
  import * as fs from 'node:fs';
12
12
  import * as path from 'node:path';
13
- import * as p from '@clack/prompts';
13
+ import * as p from '../lib/term-log.mjs';
14
14
  import {humanLog} from '../lib/json.mjs';
15
15
  import {runConfigCodemod} from './run-codemod.mjs';
16
16
 
@@ -184,7 +184,7 @@ export async function runCodemods(
184
184
  versionManifests,
185
185
  {apply, path: srcPath, codemod, skipCodemods, silent = false},
186
186
  ) {
187
- // No-op stub object so silent mode skips clack stdout entirely without
187
+ // No-op stub object so silent mode skips log output entirely without
188
188
  // littering the body with `if (!silent)` guards.
189
189
  const log = silent
190
190
  ? {step() {}, info() {}, success() {}, warn() {}, error() {}, message() {}}
@@ -166,15 +166,50 @@ describe('drop-xds-prefix-imports', () => {
166
166
  expect(output).toContain('export const other = Card;');
167
167
  });
168
168
 
169
- it('un-prefixes normally when there is NO local collision', async () => {
169
+ it('un-prefixes override keys inside an @xds/core mock factory', async () => {
170
170
  const input = [
171
- `import {XDSCodeBlock} from '@xds/core/CodeBlock';`,
172
- `export const App = () => <XDSCodeBlock code="x" />;`,
171
+ `vi.mock('@xds/core/Text', async orig => ({`,
172
+ ` ...(await orig<typeof import('@xds/core/Text')>()),`,
173
+ ` useXDSTruncation: () => ({ref: vi.fn(), isTruncated: true, fullText: ''}),`,
174
+ `}));`,
173
175
  ].join('\n');
174
176
  const output = await applyTransform(input);
175
- expect(output).toContain('{CodeBlock}');
176
- expect(output).toContain('@xds/core/CodeBlock');
177
- expect(output).toContain('<CodeBlock code="x" />');
178
- expect(output).not.toContain('AstryxCodeBlock');
177
+ expect(output).toContain('useTruncation:');
178
+ expect(output).not.toContain('useXDSTruncation');
179
+ // This codemod does not touch the module-path string (that is the
180
+ // module-specifiers codemod's job); the @xds/core/Text path stays here.
181
+ expect(output).toContain(`vi.mock('@xds/core/Text'`);
182
+ });
183
+
184
+ it('un-prefixes component override keys inside a bare @xds/core jest.mock factory', async () => {
185
+ const input = [
186
+ `jest.mock('@xds/core', () => ({`,
187
+ ` XDSButton: () => null,`,
188
+ ` useXDSToast: () => ({}),`,
189
+ `}));`,
190
+ ].join('\n');
191
+ const output = await applyTransform(input);
192
+ expect(output).toContain('Button:');
193
+ expect(output).toContain('useToast:');
194
+ expect(output).not.toContain('XDSButton');
195
+ expect(output).not.toContain('useXDSToast');
196
+ });
197
+
198
+ it('does NOT touch mock factory keys for a non-@xds package', async () => {
199
+ const input = [
200
+ `vi.mock('some-other-pkg', () => ({`,
201
+ ` useXDSFoo: () => 1,`,
202
+ `}));`,
203
+ ].join('\n');
204
+ const output = await applyTransform(input);
205
+ expect(output).toBe(input);
206
+ });
207
+
208
+ it('does NOT touch useXDS keys in an unrelated object literal (not a mock factory)', async () => {
209
+ const input = [`const config = {`, ` useXDSWhatever: true,`, `};`].join(
210
+ '\n',
211
+ );
212
+ const output = await applyTransform(input);
213
+ expect(output).toBe(input);
179
214
  });
180
215
  });
@@ -89,4 +89,47 @@ describe('migrate-xds-module-specifiers', () => {
89
89
  expect(output).not.toContain('defaultTheme');
90
90
  expect(output).not.toContain('neutralTheme as');
91
91
  });
92
+
93
+ it('rewrites the mocked module path in vi.mock (including the inline import() type arg)', async () => {
94
+ const input = [
95
+ "vi.mock('@xds/core/Text', async orig => ({",
96
+ " ...(await orig<typeof import('@xds/core/Text')>()),",
97
+ '}));',
98
+ ].join('\n');
99
+ const output = await applyTransform(input);
100
+ expect(output).toMatch(/vi\.mock\(['"]@astryxdesign\/core\/Text['"]/);
101
+ // The inline dynamic import() type argument is rewritten too.
102
+ expect(output).toMatch(/import\(['"]@astryxdesign\/core\/Text['"]\)/);
103
+ expect(output).not.toContain('@xds/');
104
+ });
105
+
106
+ it('rewrites the mocked module path in jest.mock (bare @xds/core)', async () => {
107
+ const output = await applyTransform(
108
+ "jest.mock('@xds/core', () => ({}));",
109
+ 'test.ts',
110
+ );
111
+ expect(output).toMatch(/jest\.mock\(['"]@astryxdesign\/core['"]/);
112
+ expect(output).not.toContain('@xds/');
113
+ });
114
+
115
+ it('rewrites vi.doMock / jest.doMock and a bare mock() call', async () => {
116
+ const output = await applyTransform(
117
+ [
118
+ "vi.doMock('@xds/lab/Thing', () => ({}));",
119
+ "jest.doMock('@xds/core/Text', () => ({}));",
120
+ "mock('@xds/core', () => ({}));",
121
+ ].join('\n'),
122
+ 'test.ts',
123
+ );
124
+ expect(output).toMatch(/vi\.doMock\(['"]@astryxdesign\/lab\/Thing['"]/);
125
+ expect(output).toMatch(/jest\.doMock\(['"]@astryxdesign\/core\/Text['"]/);
126
+ expect(output).toMatch(/[^.]mock\(['"]@astryxdesign\/core['"]/);
127
+ expect(output).not.toContain('@xds/');
128
+ });
129
+
130
+ it('does NOT rewrite the mock path for a non-@xds package', async () => {
131
+ const input = "vi.mock('some-other-pkg', () => ({useXDSFoo: () => 1}));";
132
+ const output = await applyTransform(input);
133
+ expect(output).toBe(input);
134
+ });
92
135
  });
@@ -35,11 +35,19 @@
35
35
  * including type references in generic type-argument positions such as
36
36
  * `useMemo<XDSTableColumn<Issue>[]>(...)`
37
37
  * 3. Re-exports from @xds/core: export {XDSButton} from '@xds/core/Button'
38
+ * 4. Object-property keys inside a test-framework mock factory for an
39
+ * `@xds/core` module: the partial-mock override in
40
+ * `vi.mock('@xds/core/Text', async orig => ({...await orig(),
41
+ * useXDSTruncation: () => ...}))` must be un-prefixed to `useTruncation`,
42
+ * otherwise the key targets a name the renamed module no longer exports and
43
+ * the mock silently overrides nothing.
38
44
  *
39
45
  * Does NOT touch:
40
46
  * - import source paths (`@xds/core/Button` stays -- subpath dirs are unchanged
41
47
  * by the prefix migration)
42
48
  * - identifiers not bound to an @xds/core import
49
+ * - object-property keys outside an `@xds/core` mock factory (an unrelated
50
+ * `{useXDSFoo: ...}` literal, or a mock of some other package, is untouched)
43
51
  * - the `XDSBaseProps`-style names only when they are NOT imported from core
44
52
  */
45
53
 
@@ -48,7 +56,8 @@ export const meta = {
48
56
  description:
49
57
  'Rewrites @xds/core imports and their usages from the prefixed names ' +
50
58
  '(XDSButton, useXDSTheme, XDSIconRegistry, XDSButtonProps) to their bare ' +
51
- 'names (Button, useTheme, IconRegistry, ButtonProps). Only renames ' +
59
+ 'names (Button, useTheme, IconRegistry, ButtonProps), including override ' +
60
+ 'keys in a vi.mock/jest.mock factory for an @xds/core module. Only renames ' +
52
61
  'bindings imported from @xds/core, leaving unrelated identifiers untouched.',
53
62
  pr: '#2880',
54
63
  };
@@ -74,6 +83,81 @@ function bareName(name) {
74
83
  return null;
75
84
  }
76
85
 
86
+ // A test-framework mock of an `@xds/core` module (`vi.mock('@xds/core/Text',
87
+ // factory)`). We only rewrite override keys inside such a factory, so scope the
88
+ // detection tightly: callee must be `vi.mock`/`vi.doMock`/`jest.mock`/
89
+ // `jest.doMock` (or a bare `mock`) AND the first argument must be a string
90
+ // literal that resolves to `@xds/core` (bare or subpath).
91
+ const MOCK_METHOD_NAMES = new Set(['mock', 'doMock']);
92
+ const MOCK_OBJECT_NAMES = new Set(['vi', 'jest']);
93
+
94
+ function isMockCallee(callee) {
95
+ if (
96
+ callee.type === 'MemberExpression' &&
97
+ !callee.computed &&
98
+ callee.object.type === 'Identifier' &&
99
+ MOCK_OBJECT_NAMES.has(callee.object.name) &&
100
+ callee.property.type === 'Identifier' &&
101
+ MOCK_METHOD_NAMES.has(callee.property.name)
102
+ ) {
103
+ return true;
104
+ }
105
+ return callee.type === 'Identifier' && callee.name === 'mock';
106
+ }
107
+
108
+ function isXdsCoreMockCall(node) {
109
+ if (node.type !== 'CallExpression' || !isMockCallee(node.callee))
110
+ return false;
111
+ const [arg] = node.arguments;
112
+ const value =
113
+ arg && (arg.type === 'StringLiteral' || arg.type === 'Literal')
114
+ ? arg.value
115
+ : null;
116
+ return typeof value === 'string' && XDS_CORE_SOURCE.test(value);
117
+ }
118
+
119
+ // Rename XDS-prefixed object-property KEYS (not values, not nested objects)
120
+ // within the given AST subtree. Used only on a recognized @xds/core mock
121
+ // factory, so the scope guard lives at the call site.
122
+ function renameMockFactoryKeys(node, seen, onChange) {
123
+ if (!node || typeof node !== 'object') return;
124
+ if (Array.isArray(node)) {
125
+ for (const child of node) renameMockFactoryKeys(child, seen, onChange);
126
+ return;
127
+ }
128
+ if (seen.has(node)) return;
129
+ seen.add(node);
130
+
131
+ if (
132
+ (node.type === 'ObjectProperty' || node.type === 'Property') &&
133
+ !node.computed &&
134
+ node.key &&
135
+ node.key.type === 'Identifier'
136
+ ) {
137
+ const bare = bareName(node.key.name);
138
+ if (bare && bare !== node.key.name) {
139
+ node.key.name = bare;
140
+ onChange();
141
+ }
142
+ }
143
+
144
+ for (const key of Object.keys(node)) {
145
+ if (
146
+ key === 'loc' ||
147
+ key === 'start' ||
148
+ key === 'end' ||
149
+ key === 'range' ||
150
+ key === 'comments' ||
151
+ key === 'leadingComments' ||
152
+ key === 'trailingComments' ||
153
+ key === 'tokens'
154
+ ) {
155
+ continue;
156
+ }
157
+ renameMockFactoryKeys(node[key], seen, onChange);
158
+ }
159
+ }
160
+
77
161
  export default function transformer(file, api) {
78
162
  const j = api.jscodeshift;
79
163
  const root = j(file.source);
@@ -190,11 +274,26 @@ export default function transformer(file, api) {
190
274
  }
191
275
  });
192
276
 
277
+ // 3. Un-prefix override keys inside @xds/core mock factories. Independent of
278
+ // the import bindings above: the override key is a factory object property,
279
+ // not a reference to an imported binding, so it must match the renamed export
280
+ // name of the mocked module regardless of what the file imports.
281
+ const seenMockNodes = new Set();
282
+ root.find(j.CallExpression).forEach(path => {
283
+ if (!isXdsCoreMockCall(path.node)) return;
284
+ // Only walk the factory argument(s), never the module-path string.
285
+ for (const arg of path.node.arguments.slice(1)) {
286
+ renameMockFactoryKeys(arg, seenMockNodes, () => {
287
+ hasChanges = true;
288
+ });
289
+ }
290
+ });
291
+
193
292
  if (localRenames.size === 0) {
194
293
  return hasChanges ? root.toSource() : undefined;
195
294
  }
196
295
 
197
- // 3. Rename references to renamed local bindings
296
+ // 4. Rename references to renamed local bindings
198
297
  // Identifiers (type refs, value refs, etc.)
199
298
  root.find(j.Identifier).forEach(path => {
200
299
  const newName = localRenames.get(path.node.name);
@@ -231,7 +330,7 @@ export default function transformer(file, api) {
231
330
  }
232
331
  });
233
332
 
234
- // 4. Rename TS type references in generic type-argument positions.
333
+ // 5. Rename TS type references in generic type-argument positions.
235
334
  //
236
335
  // `j.find(j.Identifier)` (and `j.find(j.TSTypeReference)`) do NOT visit
237
336
  // identifiers nested inside generic type arguments with the tsx/babel parser