@astryxdesign/cli 0.1.0-canary.eb78210 → 0.1.0-canary.f54a06f

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astryxdesign/cli",
3
- "version": "0.1.0-canary.eb78210",
3
+ "version": "0.1.0-canary.f54a06f",
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",
@@ -54,9 +54,9 @@
54
54
  "jscodeshift": "^17.3.0"
55
55
  },
56
56
  "peerDependencies": {
57
- "@astryxdesign/core": "0.1.0-canary.eb78210",
58
- "@astryxdesign/lab": "0.1.0-canary.eb78210",
59
- "@astryxdesign/theme-neutral": "0.1.0-canary.eb78210"
57
+ "@astryxdesign/core": "0.1.0-canary.f54a06f",
58
+ "@astryxdesign/lab": "0.1.0-canary.f54a06f",
59
+ "@astryxdesign/theme-neutral": "0.1.0-canary.f54a06f"
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.eb78210",
74
- "@astryxdesign/lab": "0.1.0-canary.eb78210",
75
- "@astryxdesign/theme-neutral": "0.1.0-canary.eb78210"
73
+ "@astryxdesign/core": "0.1.0-canary.f54a06f",
74
+ "@astryxdesign/lab": "0.1.0-canary.f54a06f",
75
+ "@astryxdesign/theme-neutral": "0.1.0-canary.f54a06f"
76
76
  },
77
77
  "scripts": {
78
78
  "astryx": "node bin/astryx.mjs",
@@ -16,6 +16,7 @@ describe('registry', () => {
16
16
  '0.0.13',
17
17
  '0.0.14',
18
18
  '0.0.15',
19
+ '0.1.0',
19
20
  ]);
20
21
  });
21
22
  });
@@ -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
@@ -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
- return {
201
- ok: true,
202
- totalFilesChanged: 0,
203
- totalTransformsApplied: 0,
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(` Run: astryx upgrade --codemod ${name} --path <dir> --apply`);
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
+ ];
@@ -0,0 +1,230 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Codemod: migrate XDS config surfaces to Astryx v0.1.0
5
+ *
6
+ * The v0.1.0 CLI reads astryx.config.mjs. This config codemod migrates
7
+ * legacy xds.config.mjs and package.json's top-level `xds` config without
8
+ * changing dependency names or versions.
9
+ */
10
+
11
+ export const meta = {
12
+ title: 'Migrate xds config surfaces to astryx',
13
+ description:
14
+ 'Moves legacy package.json xds config into astryx.config.mjs, or renames ' +
15
+ 'xds.config.mjs when no package.json config exists. Bails out if both exist.',
16
+ pr: '#3092',
17
+ codemodType: 'config',
18
+ };
19
+
20
+ function findTopLevelKey(source, targetKey) {
21
+ let depth = 0;
22
+ let inString = false;
23
+ let escaped = false;
24
+ let stringStart = -1;
25
+
26
+ for (let i = 0; i < source.length; i++) {
27
+ const ch = source[i];
28
+
29
+ if (inString) {
30
+ if (escaped) {
31
+ escaped = false;
32
+ continue;
33
+ }
34
+ if (ch === '\\') {
35
+ escaped = true;
36
+ continue;
37
+ }
38
+ if (ch !== '"') continue;
39
+
40
+ inString = false;
41
+ const rawKey = source.slice(stringStart + 1, i);
42
+ if (depth !== 1 || rawKey !== targetKey) continue;
43
+
44
+ let j = i + 1;
45
+ while (/\s/.test(source[j] ?? '')) j++;
46
+ if (source[j] !== ':') continue;
47
+
48
+ const valueStart = j + 1;
49
+ const value = readJsonValue(source, valueStart);
50
+ if (!value) return null;
51
+ return {keyStart: stringStart, keyEnd: i + 1, colon: j, ...value};
52
+ }
53
+
54
+ if (ch === '"') {
55
+ inString = true;
56
+ stringStart = i;
57
+ continue;
58
+ }
59
+ if (ch === '{' || ch === '[') depth++;
60
+ else if (ch === '}' || ch === ']') depth--;
61
+ }
62
+ return null;
63
+ }
64
+
65
+ function readJsonValue(source, start) {
66
+ let i = start;
67
+ while (/\s/.test(source[i] ?? '')) i++;
68
+ const valueStart = i;
69
+ let inString = false;
70
+ let escaped = false;
71
+ let depth = 0;
72
+
73
+ for (; i < source.length; i++) {
74
+ const ch = source[i];
75
+ if (inString) {
76
+ if (escaped) {
77
+ escaped = false;
78
+ continue;
79
+ }
80
+ if (ch === '\\') {
81
+ escaped = true;
82
+ continue;
83
+ }
84
+ if (ch === '"') inString = false;
85
+ continue;
86
+ }
87
+ if (ch === '"') {
88
+ inString = true;
89
+ continue;
90
+ }
91
+ if (ch === '{' || ch === '[') {
92
+ depth++;
93
+ continue;
94
+ }
95
+ if (ch === '}' || ch === ']') {
96
+ if (depth === 0) break;
97
+ depth--;
98
+ continue;
99
+ }
100
+ if (depth === 0 && ch === ',') break;
101
+ }
102
+
103
+ const valueEnd = i;
104
+ const rawValue = source.slice(valueStart, valueEnd).trim();
105
+ try {
106
+ JSON.parse(rawValue);
107
+ } catch {
108
+ return null;
109
+ }
110
+ return {valueStart, valueEnd, rawValue, terminator: source[i]};
111
+ }
112
+
113
+ function removeTopLevelProperty(source, property) {
114
+ let removeStart = property.keyStart;
115
+ let removeEnd = property.valueEnd;
116
+
117
+ if (source[removeEnd] === ',') {
118
+ removeEnd++;
119
+ if (source[removeEnd] === '\n') removeEnd++;
120
+ return source.slice(0, removeStart) + source.slice(removeEnd);
121
+ }
122
+
123
+ let i = removeStart - 1;
124
+ while (/\s/.test(source[i] ?? '')) i--;
125
+ if (source[i] === ',') {
126
+ const whitespaceBetweenCommaAndKey = source.slice(i + 1, removeStart);
127
+ const preservedWhitespace = whitespaceBetweenCommaAndKey.includes('\n')
128
+ ? '\n'
129
+ : '';
130
+ return source.slice(0, i) + preservedWhitespace + source.slice(removeEnd);
131
+ }
132
+
133
+ return source.slice(0, removeStart) + source.slice(removeEnd);
134
+ }
135
+
136
+ function formatConfigSource(rawConfig) {
137
+ const parsed = JSON.parse(rawConfig);
138
+ return `export default ${JSON.stringify(parsed, null, 2)};\n`;
139
+ }
140
+
141
+ function migratePackageJsonConfig(config) {
142
+ if (!config.packageJson) return {changes: []};
143
+
144
+ const packageJson = config.packageJson.source;
145
+ const xdsConfig = findTopLevelKey(packageJson, 'xds');
146
+ const astryxConfig = findTopLevelKey(packageJson, 'astryx');
147
+ if (!xdsConfig && !astryxConfig) return {changes: []};
148
+
149
+ if (config.astryxConfig) {
150
+ return {
151
+ errors: [
152
+ {
153
+ file: 'package.json',
154
+ error:
155
+ 'package.json contains xds/astryx config and astryx.config.mjs already exists; migrate the config manually.',
156
+ },
157
+ ],
158
+ };
159
+ }
160
+
161
+ if (xdsConfig && astryxConfig) {
162
+ return {
163
+ errors: [
164
+ {
165
+ file: 'package.json',
166
+ error:
167
+ 'package.json contains both xds and astryx config keys; migrate the config manually.',
168
+ },
169
+ ],
170
+ };
171
+ }
172
+
173
+ const configProperty = astryxConfig ?? xdsConfig;
174
+ const packageJsonWithoutConfig = removeTopLevelProperty(
175
+ packageJson,
176
+ configProperty,
177
+ );
178
+ return {
179
+ changes: [
180
+ {path: 'package.json', source: packageJsonWithoutConfig},
181
+ {
182
+ path: 'astryx.config.mjs',
183
+ source: formatConfigSource(configProperty.rawValue),
184
+ },
185
+ ],
186
+ };
187
+ }
188
+
189
+ function migrateXdsConfigFile(config) {
190
+ if (!config.xdsConfig) return {changes: []};
191
+ if (config.astryxConfig) {
192
+ return {
193
+ errors: [
194
+ {
195
+ file: 'xds.config.mjs',
196
+ error:
197
+ 'xds.config.mjs and astryx.config.mjs both exist; migrate the config manually.',
198
+ },
199
+ ],
200
+ };
201
+ }
202
+ return {
203
+ changes: [
204
+ {path: 'astryx.config.mjs', source: config.xdsConfig.source},
205
+ {path: 'xds.config.mjs', delete: true},
206
+ ],
207
+ };
208
+ }
209
+
210
+ export default function transformer(_file, api) {
211
+ const config = api.config ?? {};
212
+ const packageResult = migratePackageJsonConfig(config);
213
+ if (packageResult.errors?.length) return packageResult;
214
+ if (packageResult.changes?.length) {
215
+ if (config.xdsConfig) {
216
+ return {
217
+ errors: [
218
+ {
219
+ file: 'package.json',
220
+ error:
221
+ 'package.json contains xds/astryx config and xds.config.mjs exists; migrate the config manually.',
222
+ },
223
+ ],
224
+ };
225
+ }
226
+ return packageResult;
227
+ }
228
+
229
+ return migrateXdsConfigFile(config);
230
+ }