@xstate/codemod 0.1.0-alpha.0 → 0.1.0-alpha.1
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 +9 -0
- package/dist/declarations/src/cli.d.ts +16 -0
- package/dist/declarations/src/index.d.ts +4 -0
- package/dist/declarations/src/runner.d.ts +32 -0
- package/dist/declarations/src/transforms/index.d.ts +9 -0
- package/dist/declarations/src/transforms/rename-imports.d.ts +7 -0
- package/dist/declarations/src/transforms/report-removed-apis.d.ts +7 -0
- package/dist/declarations/src/transforms/string-targets.d.ts +9 -0
- package/dist/declarations/src/transforms/types-to-schemas.d.ts +6 -0
- package/dist/declarations/src/types.d.ts +10 -0
- package/dist/xstate-codemod.d.ts +2 -2
- package/dist/xstate-codemod.js +712 -0
- package/package.json +2 -2
package/README.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# @xstate/codemod
|
|
2
|
+
|
|
3
|
+
<!-- CLI commands and transforms from src/cli.ts and src/transforms/index.ts -->
|
|
4
|
+
Run `xstate-codemod migrate [globs...] --dry` to review a v5-to-v6 migration. Omit `--dry` to save changes. The default scan includes TypeScript and JavaScript files, excluding `node_modules` and `dist`.
|
|
5
|
+
|
|
6
|
+
The transforms rename imports, wrap string targets, convert inline `types` declarations to schemas, and report APIs requiring manual migration. `--transform name,name` selects transforms.
|
|
7
|
+
|
|
8
|
+
<!-- helper binding and output validation from src/util.ts, src/transforms/types-to-schemas.ts and src/runner.ts -->
|
|
9
|
+
Schema helpers use value imports, preserve usable aliases, and avoid conflicting local names. Namespace and type-only imports are supported. Event payload separators and escaped event names retain their meaning. The runner checks transformed syntax before saving; generated code still needs your application's typecheck and tests, and reported manual migrations still need review.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface ParsedArgs {
|
|
2
|
+
command: string | undefined;
|
|
3
|
+
globs: string[];
|
|
4
|
+
dry: boolean;
|
|
5
|
+
transformNames: string[] | undefined;
|
|
6
|
+
help: boolean;
|
|
7
|
+
}
|
|
8
|
+
/** Hand-written arg parser (no yargs). */
|
|
9
|
+
export declare function parseArgs(argv: string[]): ParsedArgs;
|
|
10
|
+
/**
|
|
11
|
+
* Resolves glob patterns into a de-duplicated list of file paths using
|
|
12
|
+
* ts-morph's built-in glob support (no extra glob dependency).
|
|
13
|
+
*/
|
|
14
|
+
export declare function resolveFiles(globs: string[]): string[];
|
|
15
|
+
/** CLI entry point. Returns a process exit code. */
|
|
16
|
+
export declare function main(argv: string[]): number;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export type { Transform, TransformResult } from "./types.js";
|
|
2
|
+
export { transforms, transformsByName, renameImports, stringTargets, typesToSchemas, reportRemovedApis } from "./transforms/index.js";
|
|
3
|
+
export { run, applyToSourceFile, selectTransforms, type RunReport, type FileTransformReport } from "./runner.js";
|
|
4
|
+
export { main, parseArgs, resolveFiles } from "./cli.js";
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { SourceFile } from 'ts-morph';
|
|
2
|
+
import type { Transform } from "./types.js";
|
|
3
|
+
export interface FileTransformReport {
|
|
4
|
+
filePath: string;
|
|
5
|
+
perTransform: Array<{
|
|
6
|
+
transform: string;
|
|
7
|
+
changed: boolean;
|
|
8
|
+
notes: string[];
|
|
9
|
+
}>;
|
|
10
|
+
changed: boolean;
|
|
11
|
+
newText?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface RunReport {
|
|
14
|
+
files: FileTransformReport[];
|
|
15
|
+
filesChanged: number;
|
|
16
|
+
/** Notes emitted by `report-removed-apis` (manual-migration section). */
|
|
17
|
+
manualMigrationNotes: string[];
|
|
18
|
+
}
|
|
19
|
+
/** Resolve a list of transform names to Transform objects. */
|
|
20
|
+
export declare function selectTransforms(names?: string[]): Transform[];
|
|
21
|
+
/** Applies the given transforms to a single source file (in-memory). */
|
|
22
|
+
export declare function applyToSourceFile(sourceFile: SourceFile, transforms: Transform[]): FileTransformReport;
|
|
23
|
+
/**
|
|
24
|
+
* Runs the selected transforms over the given files.
|
|
25
|
+
*
|
|
26
|
+
* @param filePaths absolute or cwd-relative file paths to process
|
|
27
|
+
* @param transforms the transforms to apply
|
|
28
|
+
* @param options.write when true, saves modified files to disk
|
|
29
|
+
*/
|
|
30
|
+
export declare function run(filePaths: string[], transforms: Transform[], options?: {
|
|
31
|
+
write: boolean;
|
|
32
|
+
}): RunReport;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Transform } from "../types.js";
|
|
2
|
+
import { renameImports } from "./rename-imports.js";
|
|
3
|
+
import { stringTargets } from "./string-targets.js";
|
|
4
|
+
import { typesToSchemas } from "./types-to-schemas.js";
|
|
5
|
+
import { reportRemovedApis } from "./report-removed-apis.js";
|
|
6
|
+
/** All transforms, in recommended application order. */
|
|
7
|
+
export declare const transforms: Transform[];
|
|
8
|
+
export declare const transformsByName: Record<string, Transform>;
|
|
9
|
+
export { renameImports, stringTargets, typesToSchemas, reportRemovedApis };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Transform } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Renames imports (and their usages) from `'xstate'` to their v6 equivalents.
|
|
4
|
+
* Preserves aliases: `import { interpret as x }` keeps `x` but renames the
|
|
5
|
+
* imported binding to `createActor`.
|
|
6
|
+
*/
|
|
7
|
+
export declare const renameImports: Transform;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Transform } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Detects (does NOT transform) usages of v5 `'xstate'` APIs that were removed
|
|
4
|
+
* or restructured in v6. Reports file:line + API + a replacement hint via the
|
|
5
|
+
* returned notes.
|
|
6
|
+
*/
|
|
7
|
+
export declare const reportRemovedApis: Transform;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Transform } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Converts bare string transition values inside `createMachine`/`setup().
|
|
4
|
+
* createMachine`/`createStateConfig` config objects into `{ target: '...' }`.
|
|
5
|
+
*
|
|
6
|
+
* Does NOT touch `initial:` (still a string in v6) or existing `target:`
|
|
7
|
+
* values.
|
|
8
|
+
*/
|
|
9
|
+
export declare const stringTargets: Transform;
|
package/dist/xstate-codemod.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export * from "
|
|
2
|
-
//# sourceMappingURL=data:application/json;charset=utf-8;base64,
|
|
1
|
+
export * from "./declarations/src/index.js";
|
|
2
|
+
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoieHN0YXRlLWNvZGVtb2QuZC50cyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4vZGVjbGFyYXRpb25zL3NyYy9pbmRleC5kLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBIn0=
|
|
@@ -0,0 +1,712 @@
|
|
|
1
|
+
import { Node, SyntaxKind, SymbolFlags, ts, Project } from 'ts-morph';
|
|
2
|
+
import process from 'node:process';
|
|
3
|
+
|
|
4
|
+
/** Map of v5 xstate exports → v6 replacement names. */
|
|
5
|
+
const RENAMES = {
|
|
6
|
+
interpret: 'createActor',
|
|
7
|
+
Interpreter: 'Actor',
|
|
8
|
+
fromCallback: 'createCallbackLogic',
|
|
9
|
+
fromObservable: 'createObservableLogic',
|
|
10
|
+
fromEventObservable: 'createEventObservableLogic'
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Renames imports (and their usages) from `'xstate'` to their v6 equivalents.
|
|
15
|
+
* Preserves aliases: `import { interpret as x }` keeps `x` but renames the
|
|
16
|
+
* imported binding to `createActor`.
|
|
17
|
+
*/
|
|
18
|
+
const renameImports = {
|
|
19
|
+
name: 'rename-imports',
|
|
20
|
+
description: "Rename renamed 'xstate' imports (interpret→createActor, Interpreter→Actor, fromCallback/fromObservable/fromEventObservable→create*Logic) and their usages.",
|
|
21
|
+
apply(sourceFile) {
|
|
22
|
+
const notes = [];
|
|
23
|
+
let changed = false;
|
|
24
|
+
const xstateImports = sourceFile.getImportDeclarations().filter(imp => imp.getModuleSpecifierValue() === 'xstate');
|
|
25
|
+
for (const imp of xstateImports) {
|
|
26
|
+
for (const spec of imp.getNamedImports()) {
|
|
27
|
+
const importedName = spec.getNameNode().getText();
|
|
28
|
+
const replacement = RENAMES[importedName];
|
|
29
|
+
if (!replacement) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const aliasNode = spec.getAliasNode();
|
|
33
|
+
if (aliasNode) {
|
|
34
|
+
// Aliased: rename only the imported binding, keep the alias.
|
|
35
|
+
// `interpret as x` → `createActor as x`
|
|
36
|
+
spec.getNameNode().replaceWithText(replacement);
|
|
37
|
+
notes.push(`renamed import '${importedName}' → '${replacement}' (alias '${aliasNode.getText()}' preserved)`);
|
|
38
|
+
} else {
|
|
39
|
+
// Not aliased: rename the binding and every reference to it.
|
|
40
|
+
const nameNode = spec.getNameNode();
|
|
41
|
+
const refs = Node.isIdentifier(nameNode) ? nameNode.findReferencesAsNodes().filter(n => n !== nameNode) : [];
|
|
42
|
+
for (const ref of refs) {
|
|
43
|
+
if (ref.getKind() === SyntaxKind.Identifier) {
|
|
44
|
+
ref.replaceWithText(replacement);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
nameNode.replaceWithText(replacement);
|
|
48
|
+
notes.push(`renamed '${importedName}' → '${replacement}'${refs.length ? ` and ${refs.length} usage(s)` : ''}`);
|
|
49
|
+
}
|
|
50
|
+
changed = true;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
changed,
|
|
55
|
+
notes
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Returns the simple identifier name of the function being called, if any.
|
|
62
|
+
* Handles both `createMachine(...)` and `setup(...).createMachine(...)` by
|
|
63
|
+
* returning the trailing property/identifier name (e.g. `createMachine`).
|
|
64
|
+
*/
|
|
65
|
+
function getCallName(call) {
|
|
66
|
+
const expr = call.getExpression();
|
|
67
|
+
if (Node.isIdentifier(expr)) {
|
|
68
|
+
return expr.getText();
|
|
69
|
+
}
|
|
70
|
+
if (Node.isPropertyAccessExpression(expr)) {
|
|
71
|
+
return expr.getNameNode().getText();
|
|
72
|
+
}
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Finds the config object literals passed as the first argument to
|
|
78
|
+
* `createMachine(...)` or `createStateConfig(...)` (including the
|
|
79
|
+
* `setup(...).createMachine(...)` form).
|
|
80
|
+
*
|
|
81
|
+
* This is a best-effort match by call-expression name, not by resolving the
|
|
82
|
+
* actual import — that is sufficient for a migration codemod and avoids fragile
|
|
83
|
+
* type resolution.
|
|
84
|
+
*/
|
|
85
|
+
function findMachineConfigObjects(sourceFile, callNames = ['createMachine', 'createStateConfig']) {
|
|
86
|
+
const result = [];
|
|
87
|
+
for (const call of sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
88
|
+
const name = getCallName(call);
|
|
89
|
+
if (!name || !callNames.includes(name)) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const firstArg = call.getArguments()[0];
|
|
93
|
+
if (firstArg && Node.isObjectLiteralExpression(firstArg)) {
|
|
94
|
+
result.push(firstArg);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Finds or adds a runtime helper import and returns its binding at the use site. */
|
|
101
|
+
function ensureNamedImport(sourceFile, name, location = sourceFile) {
|
|
102
|
+
const imports = sourceFile.getImportDeclarations().filter(imp => imp.getModuleSpecifierValue() === 'xstate');
|
|
103
|
+
if (!imports.length) return undefined;
|
|
104
|
+
const visible = location.getSymbolsInScope(SymbolFlags.Value);
|
|
105
|
+
for (const imp of imports) {
|
|
106
|
+
if (imp.isTypeOnly()) continue;
|
|
107
|
+
for (const spec of imp.getNamedImports()) {
|
|
108
|
+
if (spec.isTypeOnly() || spec.getName() !== name) continue;
|
|
109
|
+
const local = (spec.getAliasNode() ?? spec.getNameNode()).getText();
|
|
110
|
+
if (visible.find(symbol => symbol.getName() === local)?.getDeclarations().includes(spec)) {
|
|
111
|
+
return local;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Reserve every identifier, including nested bindings, so the new import is
|
|
117
|
+
// usable throughout the file without shadowing an existing local value.
|
|
118
|
+
const identifiers = new Set(sourceFile.getDescendantsOfKind(SyntaxKind.Identifier).filter(node => {
|
|
119
|
+
const parent = node.getParent();
|
|
120
|
+
return !((Node.isPropertyAssignment(parent) || Node.isPropertySignature(parent)) && parent.getNameNode() === node);
|
|
121
|
+
}).map(node => node.getText()));
|
|
122
|
+
let local = name;
|
|
123
|
+
let suffix = 1;
|
|
124
|
+
while (identifiers.has(local)) local = `${name}${suffix++}`;
|
|
125
|
+
const specifier = {
|
|
126
|
+
name,
|
|
127
|
+
...(local === name ? {} : {
|
|
128
|
+
alias: local
|
|
129
|
+
})
|
|
130
|
+
};
|
|
131
|
+
const target = imports.find(imp => !imp.isTypeOnly() && !imp.getNamespaceImport());
|
|
132
|
+
if (target) target.addNamedImport(specifier);else sourceFile.addImportDeclaration({
|
|
133
|
+
moduleSpecifier: 'xstate',
|
|
134
|
+
namedImports: [specifier]
|
|
135
|
+
});
|
|
136
|
+
return local;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Property keys whose value is a transition (or map/array of transitions) that,
|
|
141
|
+
* in v6, must be an object `{ target: '...' }` rather than a bare string.
|
|
142
|
+
*/
|
|
143
|
+
const EVENT_LIKE_KEYS = new Set(['on', 'after']);
|
|
144
|
+
const DIRECT_TRANSITION_KEYS = new Set(['onDone', 'onError', 'always']);
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Wraps a bare string transition value into `{ target: '<str>' }`.
|
|
148
|
+
* Returns true if a change was made.
|
|
149
|
+
*/
|
|
150
|
+
function wrapValue(node) {
|
|
151
|
+
if (Node.isStringLiteral(node)) {
|
|
152
|
+
const raw = node.getText(); // includes quotes
|
|
153
|
+
node.replaceWithText(`{ target: ${raw} }`);
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
if (Node.isArrayLiteralExpression(node)) {
|
|
157
|
+
let changed = false;
|
|
158
|
+
// Iterate over a snapshot; replacing elements is index-stable here since
|
|
159
|
+
// we replace text in place without adding/removing elements.
|
|
160
|
+
for (const el of node.getElements()) {
|
|
161
|
+
if (Node.isStringLiteral(el)) {
|
|
162
|
+
el.replaceWithText(`{ target: ${el.getText()} }`);
|
|
163
|
+
changed = true;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return changed;
|
|
167
|
+
}
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Given the value of an `on:` or `after:` property (an object literal mapping
|
|
173
|
+
* event/delay → transition), wrap each string transition target.
|
|
174
|
+
*/
|
|
175
|
+
function wrapTransitionMap(value) {
|
|
176
|
+
if (!Node.isObjectLiteralExpression(value)) {
|
|
177
|
+
return 0;
|
|
178
|
+
}
|
|
179
|
+
let count = 0;
|
|
180
|
+
for (const prop of value.getProperties()) {
|
|
181
|
+
if (Node.isPropertyAssignment(prop)) {
|
|
182
|
+
const init = prop.getInitializerOrThrow();
|
|
183
|
+
if (wrapValue(init)) {
|
|
184
|
+
count++;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return count;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Recursively walk a state config object literal wrapping string transitions.
|
|
193
|
+
* We recurse into `states`, `on`/`after`/`onDone`/`onError`/`always`, and
|
|
194
|
+
* nested state nodes.
|
|
195
|
+
*/
|
|
196
|
+
function walkStateNode(obj, notes) {
|
|
197
|
+
let count = 0;
|
|
198
|
+
for (const prop of obj.getProperties()) {
|
|
199
|
+
if (!Node.isPropertyAssignment(prop)) {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
const key = getKey$1(prop);
|
|
203
|
+
if (key === undefined) {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
const init = prop.getInitializer();
|
|
207
|
+
if (!init) {
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
if (EVENT_LIKE_KEYS.has(key)) {
|
|
211
|
+
const n = wrapTransitionMap(init);
|
|
212
|
+
count += n;
|
|
213
|
+
if (n) {
|
|
214
|
+
notes.push(`wrapped ${n} string transition(s) under '${key}'`);
|
|
215
|
+
}
|
|
216
|
+
} else if (DIRECT_TRANSITION_KEYS.has(key)) {
|
|
217
|
+
if (wrapValue(init)) {
|
|
218
|
+
count++;
|
|
219
|
+
notes.push(`wrapped string transition under '${key}'`);
|
|
220
|
+
}
|
|
221
|
+
} else if (key === 'states' && Node.isObjectLiteralExpression(init)) {
|
|
222
|
+
for (const stateProp of init.getProperties()) {
|
|
223
|
+
if (Node.isPropertyAssignment(stateProp) && Node.isObjectLiteralExpression(stateProp.getInitializerOrThrow())) {
|
|
224
|
+
count += walkStateNode(stateProp.getInitializerOrThrow(), notes);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
} else if (key === 'invoke') {
|
|
228
|
+
// invoke configs carry their own onDone/onError transitions
|
|
229
|
+
const invokeObjects = Node.isArrayLiteralExpression(init) ? init.getElements().filter(Node.isObjectLiteralExpression) : Node.isObjectLiteralExpression(init) ? [init] : [];
|
|
230
|
+
for (const invokeObj of invokeObjects) {
|
|
231
|
+
count += walkStateNode(invokeObj, notes);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return count;
|
|
236
|
+
}
|
|
237
|
+
function getKey$1(prop) {
|
|
238
|
+
const nameNode = prop.getNameNode();
|
|
239
|
+
if (Node.isIdentifier(nameNode)) {
|
|
240
|
+
return nameNode.getText();
|
|
241
|
+
}
|
|
242
|
+
if (Node.isStringLiteral(nameNode)) {
|
|
243
|
+
return nameNode.getLiteralText();
|
|
244
|
+
}
|
|
245
|
+
if (nameNode.getKind() === SyntaxKind.NumericLiteral) {
|
|
246
|
+
return nameNode.getText();
|
|
247
|
+
}
|
|
248
|
+
return undefined;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Converts bare string transition values inside `createMachine`/`setup().
|
|
253
|
+
* createMachine`/`createStateConfig` config objects into `{ target: '...' }`.
|
|
254
|
+
*
|
|
255
|
+
* Does NOT touch `initial:` (still a string in v6) or existing `target:`
|
|
256
|
+
* values.
|
|
257
|
+
*/
|
|
258
|
+
const stringTargets = {
|
|
259
|
+
name: 'string-targets',
|
|
260
|
+
description: "Wrap bare string transition values (on/after/onDone/onError/always) into { target: '...' } inside machine config objects. Best-effort: matches createMachine/createStateConfig by call name.",
|
|
261
|
+
apply(sourceFile) {
|
|
262
|
+
const notes = [];
|
|
263
|
+
let total = 0;
|
|
264
|
+
for (const configObj of findMachineConfigObjects(sourceFile)) {
|
|
265
|
+
total += walkStateNode(configObj, notes);
|
|
266
|
+
}
|
|
267
|
+
return {
|
|
268
|
+
changed: total > 0,
|
|
269
|
+
notes
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Extracts the `T` type node from a `{} as T` (AsExpression) initializer of a
|
|
276
|
+
* `types:` property. Returns undefined if the shape isn't recognized.
|
|
277
|
+
*/
|
|
278
|
+
function getTypesAsTypeNode(init) {
|
|
279
|
+
if (!Node.isAsExpression(init)) {
|
|
280
|
+
return undefined;
|
|
281
|
+
}
|
|
282
|
+
return init.getTypeNode();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Given a union type node written as an inline literal union of object types,
|
|
287
|
+
* each with a `type: '...'` discriminant, returns a map entry list of
|
|
288
|
+
* `[eventType, remainingObjectTypeText]`. Returns undefined when the union is
|
|
289
|
+
* not a literal inline union of object types with string-literal `type` props.
|
|
290
|
+
*/
|
|
291
|
+
function unionToEventMap(typeNode) {
|
|
292
|
+
const members = Node.isUnionTypeNode(typeNode) ? typeNode.getTypeNodes() : [typeNode];
|
|
293
|
+
const entries = [];
|
|
294
|
+
for (const member of members) {
|
|
295
|
+
if (!Node.isTypeLiteral(member)) {
|
|
296
|
+
return undefined; // e.g. a type reference — bail out entirely
|
|
297
|
+
}
|
|
298
|
+
const lit = member;
|
|
299
|
+
const props = lit.getProperties();
|
|
300
|
+
const typeProp = props.find(p => p.getName() === 'type');
|
|
301
|
+
if (!typeProp) {
|
|
302
|
+
return undefined;
|
|
303
|
+
}
|
|
304
|
+
const typePropTypeNode = typeProp.getTypeNode();
|
|
305
|
+
if (!typePropTypeNode || typePropTypeNode.getKind() !== SyntaxKind.LiteralType) {
|
|
306
|
+
return undefined;
|
|
307
|
+
}
|
|
308
|
+
const literal = typePropTypeNode.getFirstChild();
|
|
309
|
+
if (!literal || !Node.isStringLiteral(literal)) return undefined;
|
|
310
|
+
const eventType = literal.getLiteralValue();
|
|
311
|
+
|
|
312
|
+
// Build payload object type text from the remaining props.
|
|
313
|
+
const payloadProps = props.filter(p => p.getName() !== 'type').map(p => p.getText().replace(/[;,]\s*$/, ''));
|
|
314
|
+
const payload = payloadProps.length ? `{ ${payloadProps.join('; ')} }` : `{}`;
|
|
315
|
+
entries.push({
|
|
316
|
+
eventType,
|
|
317
|
+
payload
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
return entries;
|
|
321
|
+
}
|
|
322
|
+
function getKey(prop) {
|
|
323
|
+
const nameNode = prop.getNameNode();
|
|
324
|
+
if (Node.isIdentifier(nameNode)) {
|
|
325
|
+
return nameNode.getText();
|
|
326
|
+
}
|
|
327
|
+
if (Node.isStringLiteral(nameNode)) {
|
|
328
|
+
return nameNode.getLiteralText();
|
|
329
|
+
}
|
|
330
|
+
return undefined;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Converts a single `types: {} as { ... }` property into a `schemas: { ... }`
|
|
335
|
+
* property. Mutates the containing config object literal.
|
|
336
|
+
*/
|
|
337
|
+
function convertTypesProperty(configObj, typesProp, notes) {
|
|
338
|
+
const init = typesProp.getInitializerOrThrow();
|
|
339
|
+
const typeNode = getTypesAsTypeNode(init);
|
|
340
|
+
if (!typeNode) {
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// `types: {} as any` (or non-object) — cannot infer a shape; leave a note.
|
|
345
|
+
if (!Node.isTypeLiteral(typeNode)) {
|
|
346
|
+
notes.push(`left 'types' unchanged: initializer is not an inline object type ({} as ${typeNode.getText()})`);
|
|
347
|
+
typesProp.replaceWithText(`${typesProp.getText()} /* TODO(xstate-codemod): could not migrate 'types' to 'schemas' automatically */`);
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
350
|
+
const lit = typeNode;
|
|
351
|
+
const schemaEntries = [];
|
|
352
|
+
let usedTypes = false;
|
|
353
|
+
let eventsLeftBehind = false;
|
|
354
|
+
for (const member of lit.getProperties()) {
|
|
355
|
+
const key = member.getName();
|
|
356
|
+
const valueTypeNode = member.getTypeNode();
|
|
357
|
+
if (!valueTypeNode) {
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
if (key === 'events') {
|
|
361
|
+
const map = unionToEventMap(valueTypeNode);
|
|
362
|
+
if (!map) {
|
|
363
|
+
// Not a literal inline union — leave events in `types`, add a TODO.
|
|
364
|
+
eventsLeftBehind = true;
|
|
365
|
+
notes.push(`could not migrate 'events' to schemas.events map (not a literal inline union); left in place`);
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
schemaEntries.push(helper => {
|
|
369
|
+
const mapEntries = map.map(e => `${JSON.stringify(e.eventType)}: ${helper}<${e.payload}>()`);
|
|
370
|
+
return `events: {\n ${mapEntries.join(',\n ')}\n }`;
|
|
371
|
+
});
|
|
372
|
+
usedTypes = true;
|
|
373
|
+
} else {
|
|
374
|
+
// context, input, output, emitted, tags, meta, ... → types<T>()
|
|
375
|
+
const valueType = valueTypeNode.getText();
|
|
376
|
+
schemaEntries.push(helper => `${key}: ${helper}<${valueType}>()`);
|
|
377
|
+
usedTypes = true;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
if (schemaEntries.length === 0) {
|
|
381
|
+
return false;
|
|
382
|
+
}
|
|
383
|
+
const helper = usedTypes ? ensureNamedImport(configObj.getSourceFile(), 'types', configObj) : undefined;
|
|
384
|
+
if (!helper) {
|
|
385
|
+
notes.push("left 'types' unchanged: no xstate runtime import available");
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
388
|
+
const schemasText = `schemas: {\n ${schemaEntries.map(entry => entry(helper)).join(',\n ')}\n}`;
|
|
389
|
+
if (eventsLeftBehind) {
|
|
390
|
+
// Keep the original `types` property (holding only the un-migratable
|
|
391
|
+
// events) alongside the new `schemas` property.
|
|
392
|
+
// Rebuild `types` with only the `events` member preserved.
|
|
393
|
+
const eventsMember = lit.getProperties().find(p => p.getName() === 'events');
|
|
394
|
+
const preservedTypes = eventsMember ? `types: {} as { ${eventsMember.getText()} } /* TODO(xstate-codemod): migrate events to schemas.events map */` : typesProp.getText();
|
|
395
|
+
typesProp.replaceWithText(`${schemasText},\n ${preservedTypes}`);
|
|
396
|
+
} else {
|
|
397
|
+
typesProp.replaceWithText(schemasText);
|
|
398
|
+
}
|
|
399
|
+
notes.push(`converted 'types' → 'schemas' (${schemaEntries.length} key(s))${eventsLeftBehind ? "; 'events' left as types (manual)" : ''}`);
|
|
400
|
+
return true;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Converts `types: {} as { context; events; input; ... }` inside machine config
|
|
405
|
+
* objects into `schemas: { context: types<...>(), ... }`.
|
|
406
|
+
*/
|
|
407
|
+
const typesToSchemas = {
|
|
408
|
+
name: 'types-to-schemas',
|
|
409
|
+
description: 'Convert `types: {} as {...}` into `schemas: { context: types<C>(), events: { ... }, input: types<I>() }`. Events are converted to a map only when written as a literal inline union.',
|
|
410
|
+
apply(sourceFile) {
|
|
411
|
+
const notes = [];
|
|
412
|
+
let changed = false;
|
|
413
|
+
for (const configObj of findMachineConfigObjects(sourceFile, ['createMachine', 'createStateConfig', 'setup'])) {
|
|
414
|
+
for (const prop of configObj.getProperties()) {
|
|
415
|
+
if (Node.isPropertyAssignment(prop) && getKey(prop) === 'types') {
|
|
416
|
+
if (convertTypesProperty(configObj, prop, notes)) {
|
|
417
|
+
changed = true;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return {
|
|
423
|
+
changed,
|
|
424
|
+
notes
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* v5 `'xstate'` exports that require structural rewrites in v6 and are too
|
|
431
|
+
* risky to automate. Value is a one-line hint for the v6 replacement.
|
|
432
|
+
*/
|
|
433
|
+
const REMOVED_APIS = {
|
|
434
|
+
assign: 'return a `{ context }` patch from the transition/action function',
|
|
435
|
+
raise: 'use `enq.raise(...)` inside an action',
|
|
436
|
+
sendTo: 'use `enq.sendTo(...)` inside an action',
|
|
437
|
+
sendParent: 'use `enq.sendTo(...)` with the parent ref inside an action',
|
|
438
|
+
forwardTo: 'use `enq.sendTo(target, event)` inside an action',
|
|
439
|
+
emit: 'use `enq.emit(...)` inside an action',
|
|
440
|
+
log: 'use `enq.log(...)` inside an action',
|
|
441
|
+
cancel: 'use `enq.cancel(...)` inside an action',
|
|
442
|
+
spawnChild: 'use `enq.spawn(...)` inside an action',
|
|
443
|
+
stop: 'use `enq.stop(...)` inside an action',
|
|
444
|
+
stopChild: 'use `enq.stop(...)` inside an action',
|
|
445
|
+
enqueueActions: 'action functions now receive an `enq` enqueue object directly',
|
|
446
|
+
and: 'inline the guard as `(...) => a(...) && b(...)`',
|
|
447
|
+
or: 'inline the guard as `(...) => a(...) || b(...)`',
|
|
448
|
+
not: 'inline the guard as `(...) => !a(...)`',
|
|
449
|
+
stateIn: 'use `self.getSnapshot().matches(...)` or `checkStateIn(snapshot, "#id")`',
|
|
450
|
+
fromPromise: 'use `createAsyncLogic(...)`',
|
|
451
|
+
fromTransition: 'use `createLogic({ context, run })`'
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Detects (does NOT transform) usages of v5 `'xstate'` APIs that were removed
|
|
456
|
+
* or restructured in v6. Reports file:line + API + a replacement hint via the
|
|
457
|
+
* returned notes.
|
|
458
|
+
*/
|
|
459
|
+
const reportRemovedApis = {
|
|
460
|
+
name: 'report-removed-apis',
|
|
461
|
+
description: 'Detect (report only) usages of removed/restructured v5 xstate APIs (assign, raise, sendTo, emit, fromPromise, and/or/not, etc.) that need manual migration.',
|
|
462
|
+
apply(sourceFile) {
|
|
463
|
+
const notes = [];
|
|
464
|
+
const xstateImports = sourceFile.getImportDeclarations().filter(imp => imp.getModuleSpecifierValue() === 'xstate');
|
|
465
|
+
|
|
466
|
+
// Map local binding name → canonical API name (respecting aliases).
|
|
467
|
+
const tracked = new Map();
|
|
468
|
+
for (const imp of xstateImports) {
|
|
469
|
+
for (const spec of imp.getNamedImports()) {
|
|
470
|
+
const original = spec.getNameNode().getText();
|
|
471
|
+
if (REMOVED_APIS[original]) {
|
|
472
|
+
const local = (spec.getAliasNode() ?? spec.getNameNode()).getText();
|
|
473
|
+
tracked.set(local, original);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
if (tracked.size === 0) {
|
|
478
|
+
return {
|
|
479
|
+
changed: false,
|
|
480
|
+
notes
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
const filePath = sourceFile.getFilePath();
|
|
484
|
+
for (const ident of sourceFile.getDescendantsOfKind(SyntaxKind.Identifier)) {
|
|
485
|
+
const local = ident.getText();
|
|
486
|
+
const api = tracked.get(local);
|
|
487
|
+
if (!api) {
|
|
488
|
+
continue;
|
|
489
|
+
}
|
|
490
|
+
// Skip the import specifier declarations themselves.
|
|
491
|
+
const parentKind = ident.getParent()?.getKind();
|
|
492
|
+
if (parentKind === SyntaxKind.ImportSpecifier || parentKind === SyntaxKind.ImportClause) {
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
const {
|
|
496
|
+
line,
|
|
497
|
+
column
|
|
498
|
+
} = sourceFile.getLineAndColumnAtPos(ident.getPos());
|
|
499
|
+
notes.push(`${filePath}:${line}:${column} — '${api}' → ${REMOVED_APIS[api]}`);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Never mutates the file.
|
|
503
|
+
return {
|
|
504
|
+
changed: false,
|
|
505
|
+
notes
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
/** All transforms, in recommended application order. */
|
|
511
|
+
const transforms = [renameImports, stringTargets, typesToSchemas, reportRemovedApis];
|
|
512
|
+
const transformsByName = Object.fromEntries(transforms.map(t => [t.name, t]));
|
|
513
|
+
|
|
514
|
+
/** Resolve a list of transform names to Transform objects. */
|
|
515
|
+
function selectTransforms(names) {
|
|
516
|
+
if (!names || names.length === 0) {
|
|
517
|
+
return transforms;
|
|
518
|
+
}
|
|
519
|
+
const selected = [];
|
|
520
|
+
for (const name of names) {
|
|
521
|
+
const t = transformsByName[name];
|
|
522
|
+
if (!t) {
|
|
523
|
+
throw new Error(`Unknown transform '${name}'. Available: ${Object.keys(transformsByName).join(', ')}`);
|
|
524
|
+
}
|
|
525
|
+
selected.push(t);
|
|
526
|
+
}
|
|
527
|
+
return selected;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/** Applies the given transforms to a single source file (in-memory). */
|
|
531
|
+
function applyToSourceFile(sourceFile, transforms) {
|
|
532
|
+
const perTransform = [];
|
|
533
|
+
let changed = false;
|
|
534
|
+
for (const transform of transforms) {
|
|
535
|
+
const result = transform.apply(sourceFile);
|
|
536
|
+
perTransform.push({
|
|
537
|
+
transform: transform.name,
|
|
538
|
+
changed: result.changed,
|
|
539
|
+
notes: result.notes
|
|
540
|
+
});
|
|
541
|
+
if (result.changed) {
|
|
542
|
+
changed = true;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
if (changed) {
|
|
546
|
+
const diagnostics = sourceFile.getProject().getProgram().compilerObject.getSyntacticDiagnostics(sourceFile.compilerNode);
|
|
547
|
+
if (diagnostics.length) {
|
|
548
|
+
throw new Error(`Refusing to write invalid migration output for ${sourceFile.getFilePath()}: ${diagnostics.map(diagnostic => ts.flattenDiagnosticMessageText(diagnostic.messageText, ' ')).join('; ')}`);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
return {
|
|
552
|
+
filePath: sourceFile.getFilePath(),
|
|
553
|
+
perTransform,
|
|
554
|
+
changed,
|
|
555
|
+
newText: changed ? sourceFile.getFullText() : undefined
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Runs the selected transforms over the given files.
|
|
561
|
+
*
|
|
562
|
+
* @param filePaths absolute or cwd-relative file paths to process
|
|
563
|
+
* @param transforms the transforms to apply
|
|
564
|
+
* @param options.write when true, saves modified files to disk
|
|
565
|
+
*/
|
|
566
|
+
function run(filePaths, transforms, options = {
|
|
567
|
+
write: false
|
|
568
|
+
}) {
|
|
569
|
+
const project = new Project({
|
|
570
|
+
useInMemoryFileSystem: false,
|
|
571
|
+
skipAddingFilesFromTsConfig: true,
|
|
572
|
+
compilerOptions: {
|
|
573
|
+
allowJs: true
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
const files = [];
|
|
577
|
+
const manualMigrationNotes = [];
|
|
578
|
+
let filesChanged = 0;
|
|
579
|
+
for (const filePath of filePaths) {
|
|
580
|
+
const sourceFile = project.addSourceFileAtPath(filePath);
|
|
581
|
+
const report = applyToSourceFile(sourceFile, transforms);
|
|
582
|
+
for (const pt of report.perTransform) {
|
|
583
|
+
if (pt.transform === 'report-removed-apis') {
|
|
584
|
+
manualMigrationNotes.push(...pt.notes);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
if (report.changed) {
|
|
588
|
+
filesChanged++;
|
|
589
|
+
if (options.write) {
|
|
590
|
+
sourceFile.saveSync();
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
files.push(report);
|
|
594
|
+
}
|
|
595
|
+
return {
|
|
596
|
+
files,
|
|
597
|
+
filesChanged,
|
|
598
|
+
manualMigrationNotes
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const DEFAULT_GLOB = '**/*.{ts,tsx,js,jsx}';
|
|
603
|
+
const IGNORE = ['**/node_modules/**', '**/dist/**'];
|
|
604
|
+
|
|
605
|
+
/** Hand-written arg parser (no yargs). */
|
|
606
|
+
function parseArgs(argv) {
|
|
607
|
+
const parsed = {
|
|
608
|
+
command: undefined,
|
|
609
|
+
globs: [],
|
|
610
|
+
dry: false,
|
|
611
|
+
transformNames: undefined,
|
|
612
|
+
help: false
|
|
613
|
+
};
|
|
614
|
+
for (let i = 0; i < argv.length; i++) {
|
|
615
|
+
const arg = argv[i];
|
|
616
|
+
if (arg === '--help' || arg === '-h') {
|
|
617
|
+
parsed.help = true;
|
|
618
|
+
} else if (arg === '--dry') {
|
|
619
|
+
parsed.dry = true;
|
|
620
|
+
} else if (arg === '--transform') {
|
|
621
|
+
const value = argv[++i];
|
|
622
|
+
if (value) {
|
|
623
|
+
parsed.transformNames = value.split(',').map(s => s.trim()).filter(Boolean);
|
|
624
|
+
}
|
|
625
|
+
} else if (arg.startsWith('--transform=')) {
|
|
626
|
+
parsed.transformNames = arg.slice('--transform='.length).split(',').map(s => s.trim()).filter(Boolean);
|
|
627
|
+
} else if (!parsed.command) {
|
|
628
|
+
parsed.command = arg;
|
|
629
|
+
} else {
|
|
630
|
+
parsed.globs.push(arg);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
return parsed;
|
|
634
|
+
}
|
|
635
|
+
function printHelp() {
|
|
636
|
+
const names = transforms.map(t => ` ${t.name} — ${t.description}`);
|
|
637
|
+
process.stdout.write(['xstate-codemod — migrate XState source (v5 → v6)', '', 'Usage:', ' xstate-codemod migrate [globs...] [--dry] [--transform <name,name>]', '', 'Options:', ' --dry Print a summary of changes without writing files', ' --transform <names> Comma-separated transform names (default: all)', ' -h, --help Show this help', '', `Default glob: ${DEFAULT_GLOB} (excludes node_modules and dist)`, '', 'Transforms:', ...names, ''].join('\n'));
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Resolves glob patterns into a de-duplicated list of file paths using
|
|
642
|
+
* ts-morph's built-in glob support (no extra glob dependency).
|
|
643
|
+
*/
|
|
644
|
+
function resolveFiles(globs) {
|
|
645
|
+
const patterns = (globs.length ? globs : [DEFAULT_GLOB]).concat(IGNORE.map(p => `!${p}`));
|
|
646
|
+
const project = new Project({
|
|
647
|
+
skipAddingFilesFromTsConfig: true
|
|
648
|
+
});
|
|
649
|
+
const set = new Set();
|
|
650
|
+
for (const sf of project.addSourceFilesAtPaths(patterns)) {
|
|
651
|
+
set.add(sf.getFilePath());
|
|
652
|
+
}
|
|
653
|
+
return [...set].sort();
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/** CLI entry point. Returns a process exit code. */
|
|
657
|
+
function main(argv) {
|
|
658
|
+
const args = parseArgs(argv);
|
|
659
|
+
if (args.help || !args.command) {
|
|
660
|
+
printHelp();
|
|
661
|
+
return args.command ? 0 : args.help ? 0 : 1;
|
|
662
|
+
}
|
|
663
|
+
if (args.command !== 'migrate') {
|
|
664
|
+
process.stderr.write(`Unknown command '${args.command}'. Did you mean 'migrate'?\n`);
|
|
665
|
+
return 1;
|
|
666
|
+
}
|
|
667
|
+
let transforms;
|
|
668
|
+
try {
|
|
669
|
+
transforms = selectTransforms(args.transformNames);
|
|
670
|
+
} catch (err) {
|
|
671
|
+
process.stderr.write(`${err.message}\n`);
|
|
672
|
+
return 1;
|
|
673
|
+
}
|
|
674
|
+
const files = resolveFiles(args.globs);
|
|
675
|
+
if (files.length === 0) {
|
|
676
|
+
process.stdout.write('No files matched.\n');
|
|
677
|
+
return 0;
|
|
678
|
+
}
|
|
679
|
+
const report = run(files, transforms, {
|
|
680
|
+
write: !args.dry
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
// Per-file, per-transform summary.
|
|
684
|
+
for (const file of report.files) {
|
|
685
|
+
const touched = file.perTransform.filter(pt => pt.changed || pt.notes.length);
|
|
686
|
+
if (touched.length === 0) {
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
process.stdout.write(`\n${file.filePath}${file.changed ? '' : ' (no writes)'}\n`);
|
|
690
|
+
for (const pt of touched) {
|
|
691
|
+
const mark = pt.changed ? '✔' : '·';
|
|
692
|
+
process.stdout.write(` ${mark} ${pt.transform}\n`);
|
|
693
|
+
for (const note of pt.notes) {
|
|
694
|
+
process.stdout.write(` - ${note}\n`);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// Manual-migration section.
|
|
700
|
+
if (report.manualMigrationNotes.length) {
|
|
701
|
+
process.stdout.write('\n=== Manual migration needed ===\n');
|
|
702
|
+
for (const note of report.manualMigrationNotes) {
|
|
703
|
+
process.stdout.write(` ${note}\n`);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// Final report.
|
|
708
|
+
process.stdout.write(`\n${args.dry ? '[dry run] ' : ''}${report.filesChanged} of ${files.length} file(s) ${args.dry ? 'would change' : 'changed'}.\n`);
|
|
709
|
+
return 0;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
export { applyToSourceFile, main, parseArgs, renameImports, reportRemovedApis, resolveFiles, run, selectTransforms, stringTargets, transforms, transformsByName, typesToSchemas };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xstate/codemod",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.1",
|
|
4
4
|
"description": "Codemods for migrating XState source between versions (v5 → v6)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"xstate",
|
|
@@ -42,4 +42,4 @@
|
|
|
42
42
|
"./index.ts"
|
|
43
43
|
]
|
|
44
44
|
}
|
|
45
|
-
}
|
|
45
|
+
}
|