@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.
- package/README.md +115 -19
- package/docs/cli-integrations.doc.mjs +150 -0
- package/docs/getting-started.doc.mjs +9 -9
- package/docs/migration.doc.mjs +18 -18
- package/docs/principles.doc.dense.mjs +1 -1
- package/docs/principles.doc.mjs +6 -6
- package/docs/principles.doc.zh.mjs +1 -1
- package/docs/styling-libraries.doc.mjs +3 -3
- package/docs/styling.doc.mjs +4 -4
- package/docs/theme.doc.dense.mjs +2 -2
- package/docs/theme.doc.mjs +7 -7
- package/docs/theme.doc.zh.mjs +1 -1
- package/docs/tokens.doc.mjs +1 -1
- package/docs/working-with-ai.doc.mjs +18 -18
- package/package.json +11 -10
- package/scripts/postinstall.mjs +74 -0
- package/src/api/doctor.mjs +3 -3
- package/src/codemods/__tests__/registry.test.mjs +1 -0
- package/src/codemods/ensure-jscodeshift.mjs +11 -27
- package/src/codemods/registry.mjs +1 -0
- package/src/codemods/run-codemod.mjs +1 -1
- package/src/codemods/runner.mjs +2 -2
- package/src/codemods/transforms/v0.1.0/__tests__/drop-xds-prefix-imports.test.mjs +42 -7
- package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-module-specifiers.test.mjs +43 -0
- package/src/codemods/transforms/v0.1.0/drop-xds-prefix-imports.mjs +102 -3
- package/src/codemods/transforms/v0.1.0/migrate-xds-module-specifiers.mjs +72 -6
- package/src/codemods/transforms/v0.1.8/__tests__/rename-avatar-size-scale.test.mjs +161 -0
- package/src/codemods/transforms/v0.1.8/index.mjs +19 -0
- package/src/codemods/transforms/v0.1.8/rename-avatar-size-scale.mjs +234 -0
- package/src/commands/agent-docs.mjs +128 -14
- package/src/commands/agent-docs.test.mjs +97 -4
- package/src/commands/build-theme.mjs +10 -71
- package/src/commands/build.mjs +15 -15
- package/src/commands/cli-postinstall.test.mjs +42 -0
- package/src/commands/component/index.mjs +4 -4
- package/src/commands/discover.mjs +7 -5
- package/src/commands/docs.mjs +4 -4
- package/src/commands/hook/index.mjs +4 -4
- package/src/commands/init.mjs +48 -152
- package/src/commands/init.next-steps.test.mjs +1 -1
- package/src/commands/interactive-guard.test.mjs +19 -22
- package/src/commands/json-contract.test.mjs +1 -1
- package/src/commands/layout.mjs +1 -1
- package/src/commands/search.mjs +4 -4
- package/src/commands/setup-nudge.test.mjs +108 -0
- package/src/commands/swizzle.mjs +11 -34
- package/src/commands/template.mjs +11 -31
- package/src/commands/upgrade.mjs +138 -32
- package/src/commands/upgrade.test.mjs +155 -1
- package/src/index.mjs +40 -6
- package/src/lib/component-format.mjs +2 -1
- package/src/lib/term-log.mjs +48 -0
- package/src/types/upgrade.d.ts +39 -2
- package/src/utils/package-manager.mjs +78 -0
- package/src/utils/package-manager.test.mjs +108 -1
- package/src/utils/path-safety.mjs +0 -18
- package/src/utils/update-check.mjs +2 -1
- package/templates/blocks/components/Avatar/AvatarFallbackChain.tsx +4 -4
- package/templates/blocks/components/Avatar/AvatarGroup.tsx +2 -2
- package/templates/blocks/components/Avatar/AvatarInitialsFallback.tsx +1 -1
- package/templates/blocks/components/Avatar/AvatarShowcase.tsx +4 -4
- package/templates/blocks/components/Avatar/AvatarUserCard.tsx +1 -1
- package/templates/blocks/components/Avatar/AvatarWithImage.tsx +4 -4
- package/templates/blocks/components/Avatar/AvatarWithStatus.tsx +3 -3
- package/templates/blocks/components/AvatarGroup/AvatarGroupShowcase.tsx +2 -2
- package/templates/blocks/components/AvatarGroupOverflow/AvatarGroupOverflowCustomText.tsx +1 -1
- package/templates/blocks/components/AvatarGroupOverflow/AvatarGroupOverflowDefault.tsx +1 -1
- package/templates/blocks/components/AvatarGroupOverflow/AvatarGroupOverflowShowcase.tsx +2 -2
- package/templates/blocks/components/AvatarStatusDot/AvatarStatusDotShowcase.tsx +3 -3
- package/templates/blocks/components/AvatarStatusDot/AvatarStatusDotVariants.tsx +3 -3
- package/templates/blocks/components/Carousel/CarouselSnap.tsx +1 -1
- package/templates/blocks/components/ChatMessage/ChatMessageAvatarName.tsx +2 -2
- package/templates/blocks/components/ChatMessage/ChatMessageMultiBubble.tsx +1 -1
- package/templates/blocks/components/ChatMessageBubble/ChatMessageBubbleGrouping.tsx +1 -1
- package/templates/blocks/components/ChatMessageBubble/ChatMessageBubbleMetadata.tsx +1 -1
- package/templates/blocks/components/ChatMessageList/ChatMessageListDensity.tsx +5 -9
- package/templates/blocks/components/ChatMessageList/ChatMessageListFullFeatured.tsx +1 -1
- package/templates/blocks/components/HoverCard/HoverCardShowcase.tsx +1 -1
- package/templates/blocks/components/Item/ItemWithMedia.tsx +2 -2
- package/templates/blocks/components/ListItem/ListItemWithMedia.tsx +2 -2
- package/templates/blocks/components/Pagination/PaginationDotsCarousel.tsx +2 -6
- package/templates/blocks/components/Stack/StackFillItem.tsx +2 -6
- package/templates/blocks/components/TabList/TabListTabsWithActions.doc.mjs +1 -1
- package/templates/blocks/components/TabList/TabListTabsWithActions.tsx +2 -7
- package/templates/blocks/components/TypeaheadItem/TypeaheadItemShowcase.tsx +1 -1
- package/templates/pages/ai-chat/page.tsx +4 -4
- package/templates/pages/dashboard-portfolio/page.tsx +3 -11
- package/templates/pages/detail-page/page.tsx +5 -12
- package/templates/pages/documentation-design/page.tsx +1 -1
- package/templates/pages/messaging-shell/page.tsx +6 -8
- package/templates/pages/table-grouped/page.tsx +9 -14
- package/templates/pages/table-page/page.tsx +7 -9
- package/templates/pages/table-page-heatmap-status/page.tsx +5 -13
- package/docs/integration-authoring.md +0 -105
- package/src/utils/interactive.mjs +0 -76
- package/src/utils/interactive.test.mjs +0 -70
package/docs/theme.doc.zh.mjs
CHANGED
|
@@ -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: '
|
|
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] },
|
package/docs/tokens.doc.mjs
CHANGED
|
@@ -1068,7 +1068,7 @@ export const docs = {
|
|
|
1068
1068
|
},
|
|
1069
1069
|
{
|
|
1070
1070
|
"type": "prose",
|
|
1071
|
-
"text": "See `
|
|
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
|
|
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
|
|
52
|
-
npx
|
|
53
|
-
npx
|
|
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
|
-
'`
|
|
69
|
-
'`
|
|
70
|
-
'`
|
|
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
|
|
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
|
|
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
|
|
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 `
|
|
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: `
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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: `
|
|
158
|
-
|
|
159
|
-
|
|
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.
|
|
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.
|
|
83
|
-
"@astryxdesign/core": "0.1.7-canary.
|
|
84
|
-
"@astryxdesign/lab": "0.1.7-canary.
|
|
85
|
-
"@astryxdesign/theme-neutral": "0.1.7-canary.
|
|
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.
|
|
104
|
-
"@astryxdesign/core": "0.1.7-canary.
|
|
105
|
-
"@astryxdesign/lab": "0.1.7-canary.
|
|
106
|
-
"@astryxdesign/theme-neutral": "0.1.7-canary.
|
|
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
|
+
}
|
package/src/api/doctor.mjs
CHANGED
|
@@ -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:
|
|
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:
|
|
368
|
+
fix: `Add the Astryx section to your agent docs with \`${getCliInvocation(ctx.cwd)} init --features agents\`.`,
|
|
369
369
|
};
|
|
370
370
|
}
|
|
371
371
|
|
|
@@ -3,16 +3,15 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* @file Lazy jscodeshift installer
|
|
5
5
|
*
|
|
6
|
-
* Checks if jscodeshift is available and
|
|
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
|
-
*
|
|
10
|
-
*
|
|
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 '
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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 '
|
|
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
|
|
package/src/codemods/runner.mjs
CHANGED
|
@@ -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 '
|
|
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
|
|
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
|
|
169
|
+
it('un-prefixes override keys inside an @xds/core mock factory', async () => {
|
|
170
170
|
const input = [
|
|
171
|
-
`
|
|
172
|
-
`
|
|
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('
|
|
176
|
-
expect(output).toContain('
|
|
177
|
-
|
|
178
|
-
|
|
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)
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|