@astryxdesign/cli 0.1.0-canary.7f46cdb → 0.1.0-canary.9745d0a
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/bin/astryx.mjs +22 -7
- package/docs/icons.doc.mjs +1 -1
- package/docs/migration.doc.mjs +2 -2
- package/docs/shape.doc.mjs +1 -1
- package/docs/styling.doc.mjs +2 -2
- package/docs/theme.doc.dense.mjs +2 -2
- package/docs/theme.doc.mjs +14 -0
- package/docs/theme.doc.zh.mjs +2 -2
- package/docs/working-with-ai.doc.mjs +3 -3
- package/package.json +8 -8
- package/src/codemods/__tests__/registry.test.mjs +1 -0
- package/src/codemods/registry.mjs +1 -0
- package/src/codemods/runner.mjs +105 -51
- package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-config-surfaces.test.mjs +116 -0
- package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-module-specifiers.test.mjs +51 -0
- package/src/codemods/transforms/v0.1.0/index.mjs +28 -0
- package/src/codemods/transforms/v0.1.0/migrate-xds-config-surfaces.mjs +230 -0
- package/src/codemods/transforms/v0.1.0/migrate-xds-module-specifiers.mjs +84 -0
- package/src/commands/agent-docs.mjs +89 -70
- package/src/commands/agent-docs.path-safety.test.mjs +1 -1
- package/src/commands/agent-docs.test.mjs +66 -10
- package/src/commands/build-theme.import-path.test.mjs +1 -1
- package/src/commands/build-theme.path-safety.test.mjs +1 -1
- package/src/commands/build-theme.prose.test.mjs +1 -1
- package/src/commands/component-package.test.mjs +1 -1
- package/src/commands/component.test.mjs +1 -1
- package/src/commands/docs.test.mjs +1 -1
- package/src/commands/doctor.test.mjs +1 -1
- package/src/commands/external-showcase.test.mjs +1 -1
- package/src/commands/interactive-guard.test.mjs +1 -1
- package/src/commands/json-contract.test.mjs +1 -1
- package/src/commands/swizzle-gap-safety.test.mjs +1 -1
- package/src/commands/swizzle.path-safety.test.mjs +1 -1
- package/src/commands/template.path-safety.test.mjs +1 -1
- package/src/commands/template.test.mjs +1 -1
- package/src/commands/upgrade.mjs +117 -38
- package/src/commands/upgrade.test.mjs +1 -1
- package/src/lib/config.mjs +12 -0
- package/src/lib/config.test.mjs +42 -0
- package/src/lib/error-codes.mjs +3 -0
- package/src/types/error-codes.d.ts +1 -0
- package/src/utils/interactive.mjs +1 -1
- package/src/utils/interactive.test.mjs +2 -0
- package/src/utils/package-manager.test.mjs +1 -1
- package/src/utils/path-safety.test.mjs +1 -1
- package/src/utils/paths.test.mjs +8 -8
- package/src/utils/update-check.mjs +4 -26
- package/src/utils/update-check.test.mjs +2 -64
package/bin/astryx.mjs
CHANGED
|
@@ -12,10 +12,25 @@
|
|
|
12
12
|
//
|
|
13
13
|
// `node-version.mjs` is intentionally dependency-free so it loads on the
|
|
14
14
|
// unsupported runtimes this guard protects against.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
//
|
|
16
|
+
// Sibling `src/` modules are resolved by this bin's REAL path. When the CLI is
|
|
17
|
+
// installed as a package, `npx astryx` runs this file through the
|
|
18
|
+
// `node_modules/.bin/astryx` symlink, and some Node versions resolve relative
|
|
19
|
+
// specifiers from the symlink's directory (→ `node_modules/src/...`) rather
|
|
20
|
+
// than the real package — breaking bare `../src/...` imports with a cryptic
|
|
21
|
+
// ERR_MODULE_NOT_FOUND. realpath-ing `import.meta.url` makes these imports
|
|
22
|
+
// resolve correctly however the bin is invoked (symlink, copy, or Windows
|
|
23
|
+
// shim). Uses only built-ins, so the version gate below still runs first.
|
|
24
|
+
import {fileURLToPath, pathToFileURL} from 'node:url';
|
|
25
|
+
import {realpathSync} from 'node:fs';
|
|
26
|
+
import {dirname, join} from 'node:path';
|
|
27
|
+
|
|
28
|
+
const binDir = dirname(realpathSync(fileURLToPath(import.meta.url)));
|
|
29
|
+
const importSrc = rel =>
|
|
30
|
+
import(pathToFileURL(join(binDir, '..', 'src', rel)).href);
|
|
31
|
+
|
|
32
|
+
const {isNodeVersionSupported, unsupportedNodeMessage} =
|
|
33
|
+
await importSrc('lib/node-version.mjs');
|
|
19
34
|
|
|
20
35
|
if (!isNodeVersionSupported(process.versions.node)) {
|
|
21
36
|
const msg = unsupportedNodeMessage(process.versions.node);
|
|
@@ -39,9 +54,9 @@ if (!isNodeVersionSupported(process.versions.node)) {
|
|
|
39
54
|
|
|
40
55
|
// Imports that transitively load `styleText` must happen AFTER the gate above,
|
|
41
56
|
// so they are dynamically imported here rather than at the top of the module.
|
|
42
|
-
const {program} = await
|
|
43
|
-
const {isJsonMode, toErrorEnvelope} = await
|
|
44
|
-
const {handleCommanderError} = await
|
|
57
|
+
const {program} = await importSrc('index.mjs');
|
|
58
|
+
const {isJsonMode, toErrorEnvelope} = await importSrc('lib/json.mjs');
|
|
59
|
+
const {handleCommanderError} = await importSrc('lib/json-shim.mjs');
|
|
45
60
|
|
|
46
61
|
/**
|
|
47
62
|
* Top-level error boundary (contract guarantee #4): an uncaught throw must
|
package/docs/icons.doc.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* @file Icons reference doc: semantic icon names available in
|
|
4
|
+
* @file Icons reference doc: semantic icon names available in Astryx
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
/** @type {import('../../core/src/docs-types').ReferenceDoc} */
|
package/docs/migration.doc.mjs
CHANGED
|
@@ -234,7 +234,7 @@ export function AppRoot({children}: {children: React.ReactNode}) {
|
|
|
234
234
|
type: 'code',
|
|
235
235
|
lang: 'text',
|
|
236
236
|
label: 'Paste this into your AI',
|
|
237
|
-
code: `We are migrating this existing Tailwind/shadcn app to
|
|
237
|
+
code: `We are migrating this existing Tailwind/shadcn app to Astryx incrementally.
|
|
238
238
|
|
|
239
239
|
First run:
|
|
240
240
|
- npx astryx docs migration --dense
|
|
@@ -242,7 +242,7 @@ First run:
|
|
|
242
242
|
- npx astryx docs styling --dense
|
|
243
243
|
- npx astryx template AppShellTopNavWithSideNav --skeleton
|
|
244
244
|
|
|
245
|
-
Then migrate one route or shell surface at a time. Keep business logic and routing intact. Replace shadcn/Radix/Tailwind primitives with
|
|
245
|
+
Then migrate one route or shell surface at a time. Keep business logic and routing intact. Replace shadcn/Radix/Tailwind primitives with Astryx components, remove hardcoded colors, verify light and dark mode, and take screenshots before moving to the next surface.`,
|
|
246
246
|
},
|
|
247
247
|
],
|
|
248
248
|
},
|
package/docs/shape.doc.mjs
CHANGED
|
@@ -44,7 +44,7 @@ export const docs = {
|
|
|
44
44
|
type: 'code',
|
|
45
45
|
lang: 'css',
|
|
46
46
|
label: 'Concentric radius formula',
|
|
47
|
-
code: `/* Automatic in
|
|
47
|
+
code: `/* Automatic in Astryx Card */
|
|
48
48
|
--card-concentric-radius: max(0px, calc(var(--_card-radius) - var(--card-padding)));`,
|
|
49
49
|
},
|
|
50
50
|
],
|
package/docs/styling.doc.mjs
CHANGED
|
@@ -231,7 +231,7 @@ const overrides = stylex.create({
|
|
|
231
231
|
content: [
|
|
232
232
|
{
|
|
233
233
|
type: 'prose',
|
|
234
|
-
text: 'When external CSS needs to target an
|
|
234
|
+
text: 'When external CSS needs to target an Astryx component by prop or state, combine the stable component class with reflected data attributes. The component class identifies the component (`.astryx-button`, `.astryx-card`); data attributes identify the axis and value (`data-variant`, `data-size`, `data-level`, etc.). This is the preferred selector surface for new CSS because it is explicit and collision-resistant.',
|
|
235
235
|
},
|
|
236
236
|
{
|
|
237
237
|
type: 'code',
|
|
@@ -273,7 +273,7 @@ const overrides = stylex.create({
|
|
|
273
273
|
content: [
|
|
274
274
|
{
|
|
275
275
|
type: 'prose',
|
|
276
|
-
text: '
|
|
276
|
+
text: 'Astryx still emits legacy bare prop/state classes such as `.primary`, `.sm`, `.level-2`, and `.checked` for compatibility with existing apps and built themes. Do not write new CSS against these bare classes. The stable base component classes (`.astryx-button`, `.astryx-card`, etc.) are not deprecated; only the unprefixed prop/state classes are the legacy surface.',
|
|
277
277
|
},
|
|
278
278
|
{
|
|
279
279
|
type: 'code',
|
package/docs/theme.doc.dense.mjs
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
export const docsDense = {
|
|
6
6
|
description: 'Theme provider, custom themes, light/dark, component overrides',
|
|
7
7
|
sections: [
|
|
8
|
-
{ title: 'Quick Start', content: [null, null, { type: 'prose', text: 'default import = runtime injection. /built import = pre-compiled CSS (pair with theme.css).' }] },
|
|
9
|
-
{ title: 'Themes', content: [null, { type: 'prose', text: 'published: neutral (start here), butter, chocolate, gothic (dark-only), matcha, stone, y2k. @astryxdesign/theme-{name} = source (runtime). @astryxdesign/theme-{name}/built = optimized (+ theme.css).' }] },
|
|
8
|
+
{ title: 'Quick Start', content: [null, null, null, null, { type: 'prose', text: 'default import = runtime injection. /built import = pre-compiled CSS (pair with theme.css).' }] },
|
|
9
|
+
{ title: 'Themes', content: [null, null, { type: 'prose', text: 'published: neutral (start here), butter, chocolate, gothic (dark-only), matcha, stone, y2k. @astryxdesign/theme-{name} = source (runtime). @astryxdesign/theme-{name}/built = optimized (+ theme.css).' }] },
|
|
10
10
|
{ title: 'Props', content: [null] },
|
|
11
11
|
{ title: 'Custom Theme', content: [{ type: 'prose', text: 'CLI wizard or manual defineTheme. only override tokens that differ.' }, null] },
|
|
12
12
|
{ title: 'defineTheme', content: [{ type: 'prose', text: 'scale configs (color, typography, radius, motion) + explicit token overrides + component overrides. color derives full palette from accent hex via HCT.' }, null, null] },
|
package/docs/theme.doc.mjs
CHANGED
|
@@ -14,6 +14,12 @@ export const docs = {
|
|
|
14
14
|
title: 'Quick Start',
|
|
15
15
|
category: 'guide',
|
|
16
16
|
content: [
|
|
17
|
+
{
|
|
18
|
+
type: 'code',
|
|
19
|
+
lang: 'bash',
|
|
20
|
+
label: 'Install a theme package',
|
|
21
|
+
code: 'npm install @astryxdesign/theme-neutral',
|
|
22
|
+
},
|
|
17
23
|
{
|
|
18
24
|
type: 'code',
|
|
19
25
|
lang: 'tsx',
|
|
@@ -45,6 +51,10 @@ function App() {
|
|
|
45
51
|
);
|
|
46
52
|
}`,
|
|
47
53
|
},
|
|
54
|
+
{
|
|
55
|
+
type: 'prose',
|
|
56
|
+
text: 'Each theme ships as its own npm package. Install the one you want, then wrap your app in `<Theme>` — the same pattern works for every theme; just swap the package and import name.',
|
|
57
|
+
},
|
|
48
58
|
{
|
|
49
59
|
type: 'prose',
|
|
50
60
|
text: 'The default import uses runtime style injection, which works everywhere with no build step. The `/built` import skips injection and relies on the pre-compiled CSS file for better performance and SSR support.',
|
|
@@ -55,6 +65,10 @@ function App() {
|
|
|
55
65
|
title: 'Available Themes',
|
|
56
66
|
category: 'guide',
|
|
57
67
|
content: [
|
|
68
|
+
{
|
|
69
|
+
type: 'prose',
|
|
70
|
+
text: 'Install the theme package you want with `npm install @astryxdesign/theme-{name}`, then import its theme object as shown below.',
|
|
71
|
+
},
|
|
58
72
|
{
|
|
59
73
|
type: 'table',
|
|
60
74
|
headers: ['Theme', 'Import', 'Description'],
|
package/docs/theme.doc.zh.mjs
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
export const docsZh = {
|
|
6
6
|
description: 'Theme 提供者、自定义主题、亮/暗模式和组件样式覆盖。',
|
|
7
7
|
sections: [
|
|
8
|
-
{ title: '快速开始', content: [null, null, { type: 'prose', text: '默认导入使用运行时样式注入。/built 导入使用预编译 CSS(需配合 theme.css)。' }] },
|
|
9
|
-
{ title: '可用主题', content: [null, { type: 'prose', text: '已发布主题:neutral(推荐起点)、butter、chocolate、gothic(仅暗色)、matcha、stone、y2k。@astryxdesign/theme-{name} = 源码版(运行时注入)。@astryxdesign/theme-{name}/built = 优化版(配合 theme.css)。' }] },
|
|
8
|
+
{ title: '快速开始', content: [null, null, null, null, { type: 'prose', text: '默认导入使用运行时样式注入。/built 导入使用预编译 CSS(需配合 theme.css)。' }] },
|
|
9
|
+
{ title: '可用主题', content: [null, null, { type: 'prose', text: '已发布主题:neutral(推荐起点)、butter、chocolate、gothic(仅暗色)、matcha、stone、y2k。@astryxdesign/theme-{name} = 源码版(运行时注入)。@astryxdesign/theme-{name}/built = 优化版(配合 theme.css)。' }] },
|
|
10
10
|
{ title: 'Theme 属性', content: [null] },
|
|
11
11
|
{ title: '创建自定义主题', content: [{ type: 'prose', text: '使用 CLI 向导(推荐)或手动 defineTheme。只覆盖与默认值不同的令牌。' }, null] },
|
|
12
12
|
{ title: 'defineTheme', content: [{ type: 'prose', text: '支持比例配置(typography、radius、motion)+ 显式令牌覆盖 + 组件覆盖。' }, null, null] },
|
|
@@ -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 agent-docs` to set up your
|
|
37
|
+
code: 'Install @astryxdesign/cli and run `npx astryx agent-docs` to set up your Astryx context. Read the generated file.',
|
|
38
38
|
},
|
|
39
39
|
{
|
|
40
40
|
type: 'prose',
|
|
@@ -103,7 +103,7 @@ npx astryx agent-docs --agent-docs-path ~/.cursor/rules/xds.mdc`,
|
|
|
103
103
|
type: 'code',
|
|
104
104
|
lang: 'text',
|
|
105
105
|
label: 'Paste this into your AI',
|
|
106
|
-
code: `Before writing any
|
|
106
|
+
code: `Before writing any Astryx code, check your knowledge:
|
|
107
107
|
|
|
108
108
|
1. What is the correct import path for Button?
|
|
109
109
|
2. How do you make an Dialog non-dismissible?
|
|
@@ -165,7 +165,7 @@ npx astryx docs tokens --dense`,
|
|
|
165
165
|
content: [
|
|
166
166
|
{
|
|
167
167
|
type: 'prose',
|
|
168
|
-
text: '
|
|
168
|
+
text: 'Astryx ships a Model Context Protocol (MCP) server that any MCP-compatible AI tool can connect to. Instead of manually pasting CLI output, the AI can query the Astryx design system directly, searching for components, reading full documentation, and pulling code examples on demand.',
|
|
169
169
|
},
|
|
170
170
|
{
|
|
171
171
|
type: 'prose',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astryxdesign/cli",
|
|
3
|
-
"version": "0.1.0-canary.
|
|
3
|
+
"version": "0.1.0-canary.9745d0a",
|
|
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",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"url": "https://github.com/facebook/astryx/issues"
|
|
16
16
|
},
|
|
17
17
|
"keywords": [
|
|
18
|
-
"
|
|
18
|
+
"astryx",
|
|
19
19
|
"cli",
|
|
20
20
|
"design-system",
|
|
21
21
|
"init",
|
|
@@ -54,9 +54,9 @@
|
|
|
54
54
|
"jscodeshift": "^17.3.0"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
|
-
"@astryxdesign/core": "0.1.0-canary.
|
|
58
|
-
"@astryxdesign/lab": "0.1.0-canary.
|
|
59
|
-
"@astryxdesign/theme-neutral": "0.1.0-canary.
|
|
57
|
+
"@astryxdesign/core": "0.1.0-canary.9745d0a",
|
|
58
|
+
"@astryxdesign/lab": "0.1.0-canary.9745d0a",
|
|
59
|
+
"@astryxdesign/theme-neutral": "0.1.0-canary.9745d0a"
|
|
60
60
|
},
|
|
61
61
|
"peerDependenciesMeta": {
|
|
62
62
|
"@astryxdesign/core": {
|
|
@@ -70,9 +70,9 @@
|
|
|
70
70
|
}
|
|
71
71
|
},
|
|
72
72
|
"devDependencies": {
|
|
73
|
-
"@astryxdesign/core": "0.1.0-canary.
|
|
74
|
-
"@astryxdesign/lab": "0.1.0-canary.
|
|
75
|
-
"@astryxdesign/theme-neutral": "0.1.0-canary.
|
|
73
|
+
"@astryxdesign/core": "0.1.0-canary.9745d0a",
|
|
74
|
+
"@astryxdesign/lab": "0.1.0-canary.9745d0a",
|
|
75
|
+
"@astryxdesign/theme-neutral": "0.1.0-canary.9745d0a"
|
|
76
76
|
},
|
|
77
77
|
"scripts": {
|
|
78
78
|
"astryx": "node bin/astryx.mjs",
|
|
@@ -17,6 +17,7 @@ const registry = new Map([
|
|
|
17
17
|
['0.0.13', () => import('./transforms/v0.0.13/index.mjs')],
|
|
18
18
|
['0.0.14', () => import('./transforms/v0.0.14/index.mjs')],
|
|
19
19
|
['0.0.15', () => import('./transforms/v0.0.15/index.mjs')],
|
|
20
|
+
['0.1.0', () => import('./transforms/v0.1.0/index.mjs')],
|
|
20
21
|
]);
|
|
21
22
|
|
|
22
23
|
// Re-export from the shared utility so registry callers and other consumers
|
package/src/codemods/runner.mjs
CHANGED
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
import * as fs from 'node:fs';
|
|
12
12
|
import * as path from 'node:path';
|
|
13
13
|
import * as p from '@clack/prompts';
|
|
14
|
-
import {getRunPrefix} from '../utils/package-manager.mjs';
|
|
15
14
|
import {humanLog} from '../lib/json.mjs';
|
|
16
15
|
|
|
17
16
|
// Known corruption patterns that indicate a broken transform.
|
|
@@ -87,39 +86,6 @@ function findSourceFiles(dir) {
|
|
|
87
86
|
return results.sort();
|
|
88
87
|
}
|
|
89
88
|
|
|
90
|
-
/**
|
|
91
|
-
* Detect the project's formatter and run it on changed files.
|
|
92
|
-
* Tries prettier, then biome. Silently skips if none found.
|
|
93
|
-
*
|
|
94
|
-
* @param {string[]} files - Absolute paths to files that were modified
|
|
95
|
-
* @param {boolean} [silent] - Suppress human-facing output
|
|
96
|
-
*/
|
|
97
|
-
async function formatChangedFiles(files, silent = false) {
|
|
98
|
-
if (files.length === 0) return;
|
|
99
|
-
|
|
100
|
-
const {execSync} = await import('node:child_process');
|
|
101
|
-
const fileArgs = files.map(f => `"${f}"`).join(' ');
|
|
102
|
-
const prefix = getRunPrefix();
|
|
103
|
-
|
|
104
|
-
const formatters = [
|
|
105
|
-
{name: 'prettier', cmd: `${prefix} prettier --write ${fileArgs}`},
|
|
106
|
-
{name: 'biome', cmd: `${prefix} biome format --write ${fileArgs}`},
|
|
107
|
-
];
|
|
108
|
-
|
|
109
|
-
for (const {name, cmd} of formatters) {
|
|
110
|
-
try {
|
|
111
|
-
execSync(cmd, {stdio: 'pipe', timeout: 30000});
|
|
112
|
-
if (!silent)
|
|
113
|
-
p.log.info(
|
|
114
|
-
`Formatted ${files.length} file${files.length === 1 ? '' : 's'} with ${name}`,
|
|
115
|
-
);
|
|
116
|
-
return;
|
|
117
|
-
} catch {
|
|
118
|
-
// Formatter not available or failed — try next
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
89
|
/**
|
|
124
90
|
* Validate transform output before writing to disk.
|
|
125
91
|
*
|
|
@@ -162,6 +128,86 @@ export function validateOutput(result, source, j, {parse = true} = {}) {
|
|
|
162
128
|
return {valid: true};
|
|
163
129
|
}
|
|
164
130
|
|
|
131
|
+
function isConfigCodemod(transformEntry) {
|
|
132
|
+
return transformEntry.meta?.codemodType === 'config';
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const CONFIG_CODEMOD_PATHS = new Set([
|
|
136
|
+
'package.json',
|
|
137
|
+
'astryx.config.mjs',
|
|
138
|
+
'xds.config.mjs',
|
|
139
|
+
]);
|
|
140
|
+
|
|
141
|
+
function resolveConfigPath(relativePath) {
|
|
142
|
+
if (!CONFIG_CODEMOD_PATHS.has(relativePath)) {
|
|
143
|
+
throw new Error(`unsupported config codemod path: ${relativePath}`);
|
|
144
|
+
}
|
|
145
|
+
return path.resolve(process.cwd(), relativePath);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function readOptionalConfigFile(relativePath) {
|
|
149
|
+
const fullPath = resolveConfigPath(relativePath);
|
|
150
|
+
if (!fs.existsSync(fullPath)) return null;
|
|
151
|
+
return {path: relativePath, source: fs.readFileSync(fullPath, 'utf-8')};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function getConfigCodemodContext() {
|
|
155
|
+
return {
|
|
156
|
+
packageJson: readOptionalConfigFile('package.json'),
|
|
157
|
+
astryxConfig: readOptionalConfigFile('astryx.config.mjs'),
|
|
158
|
+
xdsConfig: readOptionalConfigFile('xds.config.mjs'),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function runConfigCodemod(transformEntry, {apply, log}) {
|
|
163
|
+
const {transform} = transformEntry;
|
|
164
|
+
const api = {config: getConfigCodemodContext()};
|
|
165
|
+
const result = await transform({path: process.cwd(), source: ''}, api);
|
|
166
|
+
const errors = result?.errors ?? [];
|
|
167
|
+
if (errors.length > 0) {
|
|
168
|
+
for (const error of errors) {
|
|
169
|
+
log.error(` ✗ ${error.file ?? 'config'} — ${error.error}`);
|
|
170
|
+
}
|
|
171
|
+
return {filesChanged: 0, errors};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const changes = result?.changes ?? [];
|
|
175
|
+
if (changes.length === 0) return {filesChanged: 0, errors: []};
|
|
176
|
+
|
|
177
|
+
for (const change of changes) {
|
|
178
|
+
const fullPath = resolveConfigPath(change.path);
|
|
179
|
+
if (change.delete) {
|
|
180
|
+
if (apply) fs.rmSync(fullPath, {force: true});
|
|
181
|
+
log[apply ? 'success' : 'warn'](
|
|
182
|
+
` ${apply ? '✓' : '~'} ${change.path} (delete${apply ? '' : ', dry run'})`,
|
|
183
|
+
);
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (apply) {
|
|
188
|
+
fs.writeFileSync(fullPath, change.source, 'utf-8');
|
|
189
|
+
}
|
|
190
|
+
log[apply ? 'success' : 'warn'](
|
|
191
|
+
` ${apply ? '✓' : '~'} ${change.path}${apply ? '' : ' (would change)'}`,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (changes.length > 0) {
|
|
196
|
+
const verb = apply ? 'Updated' : 'Would update';
|
|
197
|
+
log.info(
|
|
198
|
+
` ${verb} ${changes.length} config file${changes.length === 1 ? '' : 's'}`,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
filesChanged: changes.length,
|
|
204
|
+
writtenFiles: changes
|
|
205
|
+
.filter(change => !change.delete && path.extname(change.path) !== '.json')
|
|
206
|
+
.map(change => resolveConfigPath(change.path)),
|
|
207
|
+
errors: [],
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
165
211
|
/**
|
|
166
212
|
* Run codemods against source files.
|
|
167
213
|
*
|
|
@@ -197,18 +243,12 @@ export async function runCodemods(
|
|
|
197
243
|
|
|
198
244
|
if (files.length === 0) {
|
|
199
245
|
log.warn('No source files found.');
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
errors: [],
|
|
205
|
-
writtenFiles: [],
|
|
206
|
-
skippedOptional: [],
|
|
207
|
-
};
|
|
246
|
+
} else {
|
|
247
|
+
log.info(
|
|
248
|
+
`Found ${files.length} source file${files.length === 1 ? '' : 's'}`,
|
|
249
|
+
);
|
|
208
250
|
}
|
|
209
251
|
|
|
210
|
-
log.info(`Found ${files.length} source file${files.length === 1 ? '' : 's'}`);
|
|
211
|
-
|
|
212
252
|
// Dynamically import jscodeshift
|
|
213
253
|
const jscodeshift = (await import('jscodeshift')).default;
|
|
214
254
|
|
|
@@ -239,6 +279,24 @@ export async function runCodemods(
|
|
|
239
279
|
|
|
240
280
|
log.info(` ${meta.title}`);
|
|
241
281
|
|
|
282
|
+
if (isConfigCodemod(transformEntry)) {
|
|
283
|
+
const result = await runConfigCodemod(transformEntry, {apply, log});
|
|
284
|
+
if (result.errors.length > 0) {
|
|
285
|
+
for (const error of result.errors) {
|
|
286
|
+
errors.push({
|
|
287
|
+
file: error.file ?? 'config',
|
|
288
|
+
codemod: name,
|
|
289
|
+
error: error.error,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
} else if (result.filesChanged > 0) {
|
|
293
|
+
totalFilesChanged += result.filesChanged;
|
|
294
|
+
totalTransformsApplied += result.filesChanged;
|
|
295
|
+
writtenFiles.push(...(result.writtenFiles ?? []));
|
|
296
|
+
}
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
|
|
242
300
|
let filesChanged = 0;
|
|
243
301
|
|
|
244
302
|
for (const filePath of files) {
|
|
@@ -323,12 +381,6 @@ export async function runCodemods(
|
|
|
323
381
|
}
|
|
324
382
|
}
|
|
325
383
|
|
|
326
|
-
// Post-codemod formatting: run the project's formatter on changed files
|
|
327
|
-
// so codemods don't introduce style drift (jscodeshift may change quotes, etc.)
|
|
328
|
-
if (apply && writtenFiles.length > 0) {
|
|
329
|
-
await formatChangedFiles(writtenFiles, silent);
|
|
330
|
-
}
|
|
331
|
-
|
|
332
384
|
if (totalValidationBlocked > 0) {
|
|
333
385
|
log.warn(
|
|
334
386
|
`${totalValidationBlocked} file${totalValidationBlocked === 1 ? ' was' : 's were'} blocked by validation — no changes written to ${totalValidationBlocked === 1 ? 'that file' : 'those files'}.`,
|
|
@@ -366,7 +418,9 @@ export async function runCodemods(
|
|
|
366
418
|
if (meta.description) {
|
|
367
419
|
log.info(` ${meta.description}`);
|
|
368
420
|
}
|
|
369
|
-
log.info(
|
|
421
|
+
log.info(
|
|
422
|
+
` Run: astryx upgrade --codemod ${name} --path <dir> --apply`,
|
|
423
|
+
);
|
|
370
424
|
}
|
|
371
425
|
}
|
|
372
426
|
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
import {describe, it, expect} from 'vitest';
|
|
4
|
+
|
|
5
|
+
async function applyConfigCodemod(files) {
|
|
6
|
+
const {default: transform} =
|
|
7
|
+
await import('../migrate-xds-config-surfaces.mjs');
|
|
8
|
+
const config = {
|
|
9
|
+
packageJson: files['package.json']
|
|
10
|
+
? {path: 'package.json', source: files['package.json']}
|
|
11
|
+
: null,
|
|
12
|
+
astryxConfig: files['astryx.config.mjs']
|
|
13
|
+
? {path: 'astryx.config.mjs', source: files['astryx.config.mjs']}
|
|
14
|
+
: null,
|
|
15
|
+
xdsConfig: files['xds.config.mjs']
|
|
16
|
+
? {path: 'xds.config.mjs', source: files['xds.config.mjs']}
|
|
17
|
+
: null,
|
|
18
|
+
};
|
|
19
|
+
return transform({path: process.cwd(), source: ''}, {config});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe('migrate-xds-config-surfaces', () => {
|
|
23
|
+
it('extracts package.json xds config into astryx.config.mjs and removes the package key in place', async () => {
|
|
24
|
+
const input = `{
|
|
25
|
+
"dependencies": {"@xds/core":"^0.0.15"},
|
|
26
|
+
"xds" : {"theme":"neutral","docs":"./src"}
|
|
27
|
+
}
|
|
28
|
+
`;
|
|
29
|
+
|
|
30
|
+
const result = await applyConfigCodemod({'package.json': input});
|
|
31
|
+
expect(result.errors ?? []).toEqual([]);
|
|
32
|
+
expect(result.changes).toEqual([
|
|
33
|
+
{
|
|
34
|
+
path: 'package.json',
|
|
35
|
+
source: `{
|
|
36
|
+
"dependencies": {"@xds/core":"^0.0.15"}
|
|
37
|
+
}
|
|
38
|
+
`,
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
path: 'astryx.config.mjs',
|
|
42
|
+
source: `export default {
|
|
43
|
+
"theme": "neutral",
|
|
44
|
+
"docs": "./src"
|
|
45
|
+
};
|
|
46
|
+
`,
|
|
47
|
+
},
|
|
48
|
+
]);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('extracts package.json astryx config into astryx.config.mjs too', async () => {
|
|
52
|
+
const result = await applyConfigCodemod({
|
|
53
|
+
'package.json': `{"astryx":{"theme":"neutral"}}\n`,
|
|
54
|
+
});
|
|
55
|
+
expect(result.changes).toEqual([
|
|
56
|
+
{path: 'package.json', source: `{}\n`},
|
|
57
|
+
{
|
|
58
|
+
path: 'astryx.config.mjs',
|
|
59
|
+
source: `export default {
|
|
60
|
+
"theme": "neutral"
|
|
61
|
+
};
|
|
62
|
+
`,
|
|
63
|
+
},
|
|
64
|
+
]);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('bails when package.json contains both xds and astryx config keys', async () => {
|
|
68
|
+
const result = await applyConfigCodemod({
|
|
69
|
+
'package.json': `{"xds":{"theme":"default"},"astryx":{"theme":"neutral"}}\n`,
|
|
70
|
+
});
|
|
71
|
+
expect(result.errors?.[0]?.error).toMatch(/both xds and astryx/);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('does not rewrite dependency keys in package.json', async () => {
|
|
75
|
+
const input = `{"dependencies":{"@xds/core":"^0.0.15"}}\n`;
|
|
76
|
+
const result = await applyConfigCodemod({'package.json': input});
|
|
77
|
+
expect(result.changes).toEqual([]);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('bails when package config and astryx.config.mjs both exist', async () => {
|
|
81
|
+
const result = await applyConfigCodemod({
|
|
82
|
+
'package.json': `{"xds":{"theme":"neutral"}}\n`,
|
|
83
|
+
'astryx.config.mjs': `export default {};\n`,
|
|
84
|
+
});
|
|
85
|
+
expect(result.errors?.[0]?.error).toMatch(/migrate the config manually/);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('bails when package config and xds.config.mjs both exist', async () => {
|
|
89
|
+
const result = await applyConfigCodemod({
|
|
90
|
+
'package.json': `{"xds":{"theme":"neutral"}}\n`,
|
|
91
|
+
'xds.config.mjs': `export default {};\n`,
|
|
92
|
+
});
|
|
93
|
+
expect(result.errors?.[0]?.error).toMatch(/migrate the config manually/);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('renames xds.config.mjs to astryx.config.mjs when no package config exists', async () => {
|
|
97
|
+
const result = await applyConfigCodemod({
|
|
98
|
+
'xds.config.mjs': "export default { theme: 'neutral' };\n",
|
|
99
|
+
});
|
|
100
|
+
expect(result.changes).toEqual([
|
|
101
|
+
{
|
|
102
|
+
path: 'astryx.config.mjs',
|
|
103
|
+
source: "export default { theme: 'neutral' };\n",
|
|
104
|
+
},
|
|
105
|
+
{path: 'xds.config.mjs', delete: true},
|
|
106
|
+
]);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('bails when xds.config.mjs and astryx.config.mjs both exist', async () => {
|
|
110
|
+
const result = await applyConfigCodemod({
|
|
111
|
+
'xds.config.mjs': `export default {};\n`,
|
|
112
|
+
'astryx.config.mjs': `export default {};\n`,
|
|
113
|
+
});
|
|
114
|
+
expect(result.errors?.[0]?.error).toMatch(/migrate the config manually/);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
import {describe, it, expect} from 'vitest';
|
|
4
|
+
|
|
5
|
+
async function applyTransform(source, filePath = 'test.tsx') {
|
|
6
|
+
const {default: transform} =
|
|
7
|
+
await import('../migrate-xds-module-specifiers.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: filePath};
|
|
12
|
+
const result = transform(file, api);
|
|
13
|
+
return result ?? source;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe('migrate-xds-module-specifiers', () => {
|
|
17
|
+
it('renames import and export source paths including subpaths', async () => {
|
|
18
|
+
const input = [
|
|
19
|
+
"import {Button} from '@xds/core/Button';",
|
|
20
|
+
"import '@xds/theme-default/theme.css';",
|
|
21
|
+
"export {LabThing} from '@xds/lab/Thing';",
|
|
22
|
+
"export * from '@xds/core/theme';",
|
|
23
|
+
].join('\n');
|
|
24
|
+
|
|
25
|
+
const output = await applyTransform(input);
|
|
26
|
+
expect(output).toContain('@astryxdesign/core/Button');
|
|
27
|
+
expect(output).toContain('@astryxdesign/theme-neutral/theme.css');
|
|
28
|
+
expect(output).toContain('@astryxdesign/lab/Thing');
|
|
29
|
+
expect(output).toContain('@astryxdesign/core/theme');
|
|
30
|
+
expect(output).not.toContain('@xds/');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('renames dynamic import and require string literals', async () => {
|
|
34
|
+
const output = await applyTransform(
|
|
35
|
+
[
|
|
36
|
+
"const mod = await import('@xds/core/theme');",
|
|
37
|
+
"const core = require('@xds/core');",
|
|
38
|
+
].join('\n'),
|
|
39
|
+
'test.ts',
|
|
40
|
+
);
|
|
41
|
+
expect(output).toContain('@astryxdesign/core/theme');
|
|
42
|
+
expect(output).toContain('@astryxdesign/core');
|
|
43
|
+
expect(output).not.toContain('@xds/');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('does not rewrite ordinary string literals', async () => {
|
|
47
|
+
const input = "const packageName = '@xds/core';";
|
|
48
|
+
const output = await applyTransform(input, 'test.ts');
|
|
49
|
+
expect(output).toBe(input);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file v0.1.0 transform manifest
|
|
5
|
+
*
|
|
6
|
+
* Lists all codemods for the v0.1.0 release in the order they should run.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import migrateXdsConfigSurfaces, {
|
|
10
|
+
meta as migrateXdsConfigSurfacesMeta,
|
|
11
|
+
} from './migrate-xds-config-surfaces.mjs';
|
|
12
|
+
|
|
13
|
+
import migrateXdsModuleSpecifiers, {
|
|
14
|
+
meta as migrateXdsModuleSpecifiersMeta,
|
|
15
|
+
} from './migrate-xds-module-specifiers.mjs';
|
|
16
|
+
|
|
17
|
+
export default [
|
|
18
|
+
{
|
|
19
|
+
name: 'migrate-xds-config-surfaces',
|
|
20
|
+
transform: migrateXdsConfigSurfaces,
|
|
21
|
+
meta: migrateXdsConfigSurfacesMeta,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: 'migrate-xds-module-specifiers',
|
|
25
|
+
transform: migrateXdsModuleSpecifiers,
|
|
26
|
+
meta: migrateXdsModuleSpecifiersMeta,
|
|
27
|
+
},
|
|
28
|
+
];
|