@xstate/codemod 0.1.0-beta.0 → 0.1.0-beta.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 CHANGED
@@ -5,5 +5,7 @@ Run `xstate-codemod migrate [globs...] --dry` to review a v5-to-v6 migration. Om
5
5
 
6
6
  The transforms rename imports, wrap string targets, convert inline `types` declarations to schemas, report APIs requiring manual migration, and report `#id.path` targets that v6 no longer resolves. `--transform name,name` selects transforms.
7
7
 
8
+ `--json` prints a single JSON report to stdout, including a `manualSites` list with the file, line, column, API and hint for each manual migration step. The command exits with `0` when no manual migration is needed, `1` on a usage error or invalid migration output, and `2` when manual migration is needed.
9
+
8
10
  <!-- helper binding and output validation from src/util.ts, src/transforms/types-to-schemas.ts and src/runner.ts -->
9
11
  The codemod supports named, namespace and type-only imports. It checks transformed syntax before saving. Run your application's typecheck and tests after migration, and review any reported manual migration steps.
@@ -0,0 +1,38 @@
1
+ // Maps `xstate` and `xstate/*` to the major selected by XSTATE_VERIFY_MAJOR:
2
+ // `xstate5` (npm:xstate@5, bundled with this package) or this package's own
3
+ // `xstate` (6). Under 6, `xstate-v6` (the side-by-side install name from the
4
+ // migration guide) maps to the same package. Synchronous hooks cover both
5
+ // `import` and `require` (tsx transpiles CommonJS user files). Every specifier
6
+ // resolves from this file, so the user's module and the verify runner share
7
+ // one xstate instance.
8
+ import { registerHooks } from 'node:module';
9
+
10
+ const major = process.env.XSTATE_VERIFY_MAJOR;
11
+ if (major !== '5' && major !== '6') {
12
+ throw new Error(`XSTATE_VERIFY_MAJOR must be 5 or 6, got ${major}`);
13
+ }
14
+ const target = major === '5' ? 'xstate5' : 'xstate';
15
+ const aliases = major === '5' ? ['xstate'] : ['xstate', 'xstate-v6'];
16
+ let resolvingTarget = false;
17
+
18
+ function aliasFor(specifier) {
19
+ return aliases.find(
20
+ (alias) => specifier === alias || specifier.startsWith(`${alias}/`)
21
+ );
22
+ }
23
+
24
+ registerHooks({
25
+ resolve(specifier, context, nextResolve) {
26
+ const alias = resolvingTarget ? undefined : aliasFor(specifier);
27
+ if (alias) {
28
+ resolvingTarget = true;
29
+ try {
30
+ const url = import.meta.resolve(target + specifier.slice(alias.length));
31
+ return { url, shortCircuit: true };
32
+ } finally {
33
+ resolvingTarget = false;
34
+ }
35
+ }
36
+ return nextResolve(specifier, context);
37
+ }
38
+ });
package/bin/codemod.js CHANGED
@@ -1,4 +1,12 @@
1
1
  #!/usr/bin/env node
2
- import { main } from '../dist/xstate-codemod.js';
2
+ import { main, verifyCli } from '../dist/xstate-codemod.js';
3
3
 
4
- process.exit(main(process.argv.slice(2)));
4
+ const argv = process.argv.slice(2);
5
+ const [command] = argv;
6
+
7
+ // Set the exit code instead of calling process.exit() so a large JSON report
8
+ // written to a pipe is flushed before the process ends.
9
+ process.exitCode =
10
+ command === 'baseline' || command === 'verify'
11
+ ? await verifyCli(argv)
12
+ : main(argv);
@@ -0,0 +1,3 @@
1
+ import { childMain } from '../dist/xstate-codemod.js';
2
+
3
+ await childMain();
@@ -1,7 +1,8 @@
1
- export interface ParsedArgs {
1
+ interface ParsedArgs {
2
2
  command: string | undefined;
3
3
  globs: string[];
4
4
  dry: boolean;
5
+ json: boolean;
5
6
  transformNames: string[] | undefined;
6
7
  help: boolean;
7
8
  }
@@ -14,3 +15,4 @@ export declare function parseArgs(argv: string[]): ParsedArgs;
14
15
  export declare function resolveFiles(globs: string[]): string[];
15
16
  /** CLI entry point. Returns a process exit code. */
16
17
  export declare function main(argv: string[]): number;
18
+ export {};
@@ -1,4 +1,5 @@
1
1
  export type { Transform, TransformResult } from "./types.js";
2
- export { transforms, transformsByName, renameImports, stringTargets, typesToSchemas, reportRemovedApis, reportIdPathTargets } from "./transforms/index.js";
2
+ export { transforms, transformsByName, renameImports, stringTargets, typesToSchemas, lowerActions, reportRemovedApis, reportIdPathTargets } from "./transforms/index.js";
3
3
  export { run, applyToSourceFile, selectTransforms, type RunReport, type FileTransformReport } from "./runner.js";
4
4
  export { main, parseArgs, resolveFiles } from "./cli.js";
5
+ export { verifyCli, childMain, runBaseline, runVerify, VerifyError, type BaselineOptions, type VerifyOptions, type Baseline, type VerifyReport } from "./verify/index.js";
@@ -1,11 +1,12 @@
1
1
  import { SourceFile } from 'ts-morph';
2
- import type { Transform } from "./types.js";
2
+ import type { ManualSite, Transform } from "./types.js";
3
3
  export interface FileTransformReport {
4
4
  filePath: string;
5
5
  perTransform: Array<{
6
6
  transform: string;
7
7
  changed: boolean;
8
8
  notes: string[];
9
+ sites: ManualSite[];
9
10
  }>;
10
11
  changed: boolean;
11
12
  newText?: string;
@@ -15,6 +16,10 @@ export interface RunReport {
15
16
  filesChanged: number;
16
17
  /** Notes emitted by the report-only transforms (manual-migration section). */
17
18
  manualMigrationNotes: string[];
19
+ /** Structured locations that need manual migration, from every transform. */
20
+ manualSites: Array<ManualSite & {
21
+ transform: string;
22
+ }>;
18
23
  }
19
24
  /** Resolve a list of transform names to Transform objects. */
20
25
  export declare function selectTransforms(names?: string[]): Transform[];
@@ -2,9 +2,10 @@ import type { Transform } from "../types.js";
2
2
  import { renameImports } from "./rename-imports.js";
3
3
  import { stringTargets } from "./string-targets.js";
4
4
  import { typesToSchemas } from "./types-to-schemas.js";
5
+ import { lowerActions } from "./lower-actions.js";
5
6
  import { reportRemovedApis } from "./report-removed-apis.js";
6
7
  import { reportIdPathTargets } from "./report-id-path-targets.js";
7
8
  /** All transforms, in recommended application order. */
8
9
  export declare const transforms: Transform[];
9
10
  export declare const transformsByName: Record<string, Transform>;
10
- export { renameImports, stringTargets, typesToSchemas, reportRemovedApis, reportIdPathTargets };
11
+ export { renameImports, stringTargets, typesToSchemas, lowerActions, reportRemovedApis, reportIdPathTargets };
@@ -0,0 +1,7 @@
1
+ import type { Transform } from "../types.js";
2
+ /**
3
+ * Lowers v5 action creators, guards and transition objects to v6 transition
4
+ * and entry/exit functions. Anything it cannot lower is left untouched for the
5
+ * report transforms.
6
+ */
7
+ export declare const lowerActions: Transform;
@@ -1,6 +1,7 @@
1
1
  import type { Transform } from "../types.js";
2
2
  /**
3
- * Converts `types: {} as { context; events; input; ... }` inside machine config
4
- * objects into `schemas: { context: types<...>(), ... }`.
3
+ * Converts `types: {} as { context; events; input; ... }` (or the per-key
4
+ * `types: { context: {} as C, ... }` form) inside machine config objects into
5
+ * `schemas: { context: types<...>(), ... }`.
5
6
  */
6
7
  export declare const typesToSchemas: Transform;
@@ -1,7 +1,16 @@
1
1
  import type { SourceFile } from 'ts-morph';
2
+ /** A source location that needs manual migration. */
3
+ export interface ManualSite {
4
+ file: string;
5
+ line: number;
6
+ column: number;
7
+ api: string;
8
+ hint: string;
9
+ }
2
10
  export interface TransformResult {
3
11
  changed: boolean;
4
12
  notes: string[];
13
+ sites?: ManualSite[];
5
14
  }
6
15
  export interface Transform {
7
16
  name: string;
@@ -0,0 +1,3 @@
1
+ /** Marks the start of the JSON result on stdout; user code may print before it. */
2
+ export declare const RESULT_MARKER = "\n@@xstate-verify-result@@\n";
3
+ export declare function childMain(): Promise<void>;
@@ -0,0 +1,2 @@
1
+ /** `baseline` and `verify` commands; `argv[0]` is the command name. */
2
+ export declare function verifyCli(argv: string[]): Promise<number>;
@@ -0,0 +1,4 @@
1
+ export { verifyCli } from "./cli.js";
2
+ export { childMain } from "./child.js";
3
+ export { runBaseline, runVerify, startError, VerifyError, type BaselineOptions, type VerifyOptions } from "./run.js";
4
+ export type * from "./types.js";
@@ -0,0 +1,37 @@
1
+ import type { Baseline, EventCases, GenerateOptions, Stubs, VerifyReport } from "./types.js";
2
+ export declare class VerifyError extends Error {
3
+ phase: 'baseline' | 'replay';
4
+ code: string;
5
+ stderr: string;
6
+ childStack: string | undefined;
7
+ constructor(phase: 'baseline' | 'replay', code: string, message: string, stderr: string, childStack?: string);
8
+ }
9
+ export interface BaselineOptions extends Partial<GenerateOptions> {
10
+ file: string;
11
+ export?: string;
12
+ events?: EventCases;
13
+ stubs?: Stubs;
14
+ input?: unknown;
15
+ timeout?: number;
16
+ }
17
+ /** The error of the first trace's start step, when the machine errored there. */
18
+ export declare function startError(baseline: Baseline): string | undefined;
19
+ /** Records a golden trace of a v5 module. Throws {@link VerifyError}. */
20
+ export declare function runBaseline(options: BaselineOptions): Promise<Baseline>;
21
+ export interface VerifyOptions extends Partial<GenerateOptions> {
22
+ /** The v6 module to replay. */
23
+ file: string;
24
+ /** A v5 module to baseline first; ignored when `baseline` is given. */
25
+ v5?: string;
26
+ baseline?: Baseline;
27
+ export?: string;
28
+ /** Override the baseline's recorded cases/stubs/input. */
29
+ events?: EventCases;
30
+ stubs?: Stubs;
31
+ input?: unknown;
32
+ /** Trace fields to compare; defaults to all but `actions`. */
33
+ compare?: string[];
34
+ timeout?: number;
35
+ }
36
+ /** Replays the baseline's sequences on a v6 module and diffs the traces. */
37
+ export declare function runVerify(options: VerifyOptions): Promise<VerifyReport>;
@@ -0,0 +1,177 @@
1
+ export type EventObject = {
2
+ type: string;
3
+ [key: string]: unknown;
4
+ };
5
+ export type NormalizedEvent = EventObject;
6
+ export type SequenceStep = {
7
+ event: EventObject;
8
+ } | {
9
+ advance: number;
10
+ };
11
+ export type Origin = 'shortest' | 'simple' | 'random';
12
+ export interface Sequence {
13
+ id: string;
14
+ origin: Origin;
15
+ steps: SequenceStep[];
16
+ }
17
+ export interface TraceStep {
18
+ input: SequenceStep | {
19
+ start: true;
20
+ };
21
+ value: unknown;
22
+ context: unknown;
23
+ status: 'active' | 'done' | 'error' | 'stopped';
24
+ output?: unknown;
25
+ error?: string;
26
+ processed: NormalizedEvent[];
27
+ children: Array<{
28
+ id: string;
29
+ src: string;
30
+ }>;
31
+ sent: Array<{
32
+ to: string;
33
+ event: NormalizedEvent;
34
+ delay?: number;
35
+ }>;
36
+ emitted: NormalizedEvent[];
37
+ actions: Array<{
38
+ type: string;
39
+ params: unknown;
40
+ }>;
41
+ }
42
+ export interface Trace {
43
+ sequenceId: string;
44
+ steps: TraceStep[];
45
+ }
46
+ export type StubSpec = 'pending' | {
47
+ done: unknown;
48
+ } | {
49
+ error: string;
50
+ };
51
+ export interface Stubs {
52
+ actors?: Record<string, StubSpec>;
53
+ delays?: Record<string, number>;
54
+ }
55
+ /** `{ TYPE: [payload, …] }`; each payload is merged into `{ type: TYPE }`. */
56
+ export type EventCases = Record<string, Array<Record<string, unknown>>>;
57
+ export interface GenerateOptions {
58
+ seed: number;
59
+ walks: number;
60
+ steps: number;
61
+ maxSequences: number;
62
+ /** `false` skips the v5 graph's simple paths (`--no-simple-paths`). */
63
+ simplePaths?: boolean;
64
+ }
65
+ export interface Baseline {
66
+ version: 1;
67
+ file: string;
68
+ export: string;
69
+ xstateVersion: string;
70
+ options: GenerateOptions;
71
+ events: EventCases;
72
+ stubs: Stubs;
73
+ /** Passed as `createActor(logic, { input })`. */
74
+ input?: unknown;
75
+ sequences: Sequence[];
76
+ traces: Trace[];
77
+ unstubbed: string[];
78
+ eventsWithoutCases: string[];
79
+ /** Graph origins or events dropped because a guard threw while generating. */
80
+ generationErrors: string[];
81
+ nonSerializable: number;
82
+ /** Loader notes, such as which of several machine exports was used. */
83
+ notes?: string[];
84
+ }
85
+ export interface ChildJob {
86
+ mode: 'baseline' | 'replay';
87
+ file: string;
88
+ export?: string;
89
+ events: EventCases;
90
+ stubs: Stubs;
91
+ input?: unknown;
92
+ options: GenerateOptions;
93
+ /** Replay only. */
94
+ sequences?: Sequence[];
95
+ }
96
+ export type ChildResult = {
97
+ ok: true;
98
+ export: string;
99
+ xstateVersion: string;
100
+ sequences: Sequence[];
101
+ traces: Trace[];
102
+ unstubbed: string[];
103
+ eventsWithoutCases: string[];
104
+ generationErrors?: string[];
105
+ nonSerializable: number;
106
+ notes?: string[];
107
+ } | {
108
+ ok: false;
109
+ code: string;
110
+ message: string;
111
+ stack?: string;
112
+ };
113
+ export type Classification = 'unclassified' | {
114
+ rule: string;
115
+ section: string;
116
+ note: string;
117
+ };
118
+ export interface Diff {
119
+ sequenceId: string;
120
+ origin: Origin;
121
+ step: number;
122
+ witness: SequenceStep[];
123
+ field: string;
124
+ expected: unknown;
125
+ actual: unknown;
126
+ /** Error message(s) behind a `status` diff where either side errored. */
127
+ detail?: string;
128
+ classification: Classification;
129
+ }
130
+ export interface VerifyReport {
131
+ status: 'equivalent' | 'diverged' | 'error';
132
+ v5: {
133
+ file: string;
134
+ export: string;
135
+ xstateVersion: string;
136
+ };
137
+ v6: {
138
+ file: string;
139
+ export: string;
140
+ xstateVersion: string;
141
+ };
142
+ sequences: number;
143
+ stepsCompared: number;
144
+ diffs: Diff[];
145
+ intentional: number;
146
+ unstubbed: string[];
147
+ eventsWithoutCases: string[];
148
+ generationErrors: string[];
149
+ nonSerializable: number;
150
+ /** The baseline's error at start, if the v5 machine errored there. */
151
+ startError?: string;
152
+ notes: string[];
153
+ error?: {
154
+ phase: 'baseline' | 'replay';
155
+ code: string;
156
+ message: string;
157
+ stack?: string;
158
+ stderr: string;
159
+ };
160
+ }
161
+ /** Per-step buckets a recorder fills from inspection events. */
162
+ export interface StepRecord {
163
+ processed: NormalizedEvent[];
164
+ sent: TraceStep['sent'];
165
+ emitted: NormalizedEvent[];
166
+ actions: TraceStep['actions'];
167
+ }
168
+ export interface Recorder {
169
+ setRoot(root: unknown): void;
170
+ inspect(event: any): void;
171
+ emit(event: any): void;
172
+ srcOf(ref: unknown): string;
173
+ take(): StepRecord;
174
+ }
175
+ export interface Stats {
176
+ nonSerializable: number;
177
+ }