xstate 6.0.0-beta.0 → 6.0.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/bin/xstate.js +10 -4
- package/dist/declarations/src/setup.d.ts +3 -3
- package/dist/declarations/src/types.d.ts +7 -1
- package/dist/xstate.umd.min.js.map +1 -1
- package/docs/agents.md +9 -0
- package/docs/api-stability.json +166 -160
- package/docs/index.md +1 -1
- package/docs/xstate-v5-to-v6.md +162 -5
- package/package.json +1 -1
package/docs/index.md
CHANGED
package/docs/xstate-v5-to-v6.md
CHANGED
|
@@ -294,6 +294,26 @@ on: {
|
|
|
294
294
|
}
|
|
295
295
|
```
|
|
296
296
|
|
|
297
|
+
### Transitions inside parallel regions
|
|
298
|
+
|
|
299
|
+
A transition whose targets stay inside one region of a parallel state exits
|
|
300
|
+
and reenters that region only. In v5 the same transition, when it reentered
|
|
301
|
+
its source or targeted a child state, exited the whole parallel state and
|
|
302
|
+
reset every sibling region to its initial state. v6 leaves the siblings where
|
|
303
|
+
they are. A transition declared on a sibling region for the same event still
|
|
304
|
+
runs, because the two exit sets no longer overlap.
|
|
305
|
+
|
|
306
|
+
```ts
|
|
307
|
+
// Parallel state `p` with regions `a` and `b`.
|
|
308
|
+
// Event `t`: `a` reenters itself; `b.b1` moves to `b.b2`.
|
|
309
|
+
// v5 after `t`: { a: 'a1', b: 'b1' } — `b` was reset when `p` was exited.
|
|
310
|
+
// v6 after `t`: { a: 'a1', b: 'b2' } — only `a` was exited.
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
Reset the sibling regions explicitly if the v5 behavior was intended: target
|
|
314
|
+
the parallel state itself, or list a target in every region.
|
|
315
|
+
`@xstate/scxml` keeps the SCXML exit semantics for SCXML sources.
|
|
316
|
+
|
|
297
317
|
---
|
|
298
318
|
|
|
299
319
|
## 2. Guards
|
|
@@ -552,7 +572,7 @@ These inferred typestates work as follows. See [Typestates](typestates.md) for t
|
|
|
552
572
|
|
|
553
573
|
<!-- setup state contracts from SetupStateSchema in packages/core/src/setup.ts -->
|
|
554
574
|
|
|
555
|
-
`setup()` still exists in v6 and still accepts `actions`, `guards`, `actors`, and `delays` (merged into every machine created from it). What changed: `types` is replaced by `schemas`, and `setup()` gains a `states` key for declaring **state contracts**. A contract can provide state-level schemas plus structural metadata and defaults such as `type`, `initial`, `history`, `target`, `id`, and `route`. This types `createMachine` and `createStateConfig`, including the `initial: { target, input }` form and transitions targeting those states. Setups that declare only schemas
|
|
575
|
+
`setup()` still exists in v6 and still accepts `actions`, `guards`, `actors`, and `delays` (merged into every machine created from it). What changed: `types` is replaced by `schemas`, and `setup()` gains a `states` key for declaring **state contracts**. A contract can provide state-level schemas plus structural metadata and defaults such as `type`, `initial`, `history`, `target`, `id`, and `route`. This types `createMachine` and `createStateConfig`, including the `initial: { target, input }` form and transitions targeting those states. Setups that declare only schemas don't require state contracts, but unknown literal transition targets are still type errors.
|
|
556
576
|
|
|
557
577
|
```ts
|
|
558
578
|
// v6 - setup with root schemas and state-level input schemas
|
|
@@ -1058,6 +1078,11 @@ Transition arrays are not accepted by the authoring APIs. Use a single
|
|
|
1058
1078
|
transition function to select among targets. Serialized transition arrays are
|
|
1059
1079
|
still accepted by `machineFromJSON(...)`.
|
|
1060
1080
|
|
|
1081
|
+
A chain of eventless transitions that never settles ends the actor in v6. The
|
|
1082
|
+
actor processes at most 1000 microsteps per event and then fails with
|
|
1083
|
+
`InfiniteTransitionError`, so `snapshot.status` is `'error'`. v5 ran such a
|
|
1084
|
+
loop until the process ran out of memory.
|
|
1085
|
+
|
|
1061
1086
|
---
|
|
1062
1087
|
|
|
1063
1088
|
## 16. Removed top-level exports
|
|
@@ -1606,13 +1631,143 @@ It runs these transforms in order. `--transform name,name` selects a subset.
|
|
|
1606
1631
|
| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1607
1632
|
| `rename-imports` | Renames `interpret` to `createActor`, `Interpreter` to `Actor`, and `fromCallback`, `fromObservable`, and `fromEventObservable` to their `create*Logic` names, and the executable action types to their executable effect names, including usages. Import aliases are kept. |
|
|
1608
1633
|
| `string-targets` | Wraps bare string transition values under `on`, `after`, `onDone`, `onError`, and `always` in `{ target: '...' }` inside `createMachine` and `createStateConfig` configs. |
|
|
1609
|
-
| `types-to-schemas` | Converts `types: {} as { ... }` to `schemas` with `types<T>()` entries. Events become a map
|
|
1634
|
+
| `types-to-schemas` | Converts `types: {} as { ... }` and the per-key form `types: { context: {} as C, events: {} as E }` to `schemas` with `types<T>()` entries. Events become a map when they are written as a union of object literals with a string-literal `type`, inline or through type aliases declared in the same file. Other event types become `events: types<E>()`, which loads but does not type-check as a v6 event map, so the entry is reported as a manual site to rewrite as a map by hand. A `types` value in any other shape, such as a variable, is reported as a manual site. |
|
|
1635
|
+
| `lower-actions` | Rewrites transition objects that have `actions` or `guard`, and transition arrays, as one transition function `(args, enq) => { ... }`; non-function `entry` and `exit` values as one function; and the action creators and guard combinators inside them (`assign`, `enqueueActions`, `raise`, `sendTo`, `sendParent`, `forwardTo`, `emit`, `log`, `cancel`, `stopChild`, `spawnChild`, `and`, `or`, `not`, `stateIn`) as `enq` calls, context patches and boolean expressions, following named implementations and module-level constants defined in the same file. Named actions and guards keep their v5 signatures; the generated code calls them as `enq(args.actions.name, args, params)` and `args.guards.name(args, params)`. A named `assign` action becomes a function that returns a context patch. Moves an object-literal second `createMachine` argument into the config, and rewrites `fromPromise(fn)` as `createAsyncLogic({ run: fn })` when `fn` does not read `system` or `emit`. Removes the `'xstate'` imports it leaves unused. A transition, `entry`, or `exit` value that contains anything it cannot rewrite, such as an `enqueueActions` callback that passes `enqueue` as a value or returns early, a guard or action defined in another file, or a named `assign` that `provide()` overrides, is left unchanged and reported by the transforms below. |
|
|
1610
1636
|
| `report-removed-apis` | Reports, without rewriting, uses of `assign`, `raise`, `sendTo`, `sendParent`, `forwardTo`, `emit`, `log`, `cancel`, `spawnChild`, `stop`, `stopChild`, `enqueueActions`, `and`, `or`, `not`, `stateIn`, `fromPromise`, `fromTransition`, `toObserver`, `pathToStateValue`, and `SpecialTargets`, with a suggested replacement. |
|
|
1611
1637
|
| `report-id-path-targets` | Reports, without rewriting, `#a.b` targets and `matches('#a.b')` or `stateIn('#a.b')` arguments that are not whole state ids of the machine config that contains them. References outside a `createMachine` or `createStateConfig` config are always reported. v6 resolves `#` targets by exact id only. |
|
|
1638
|
+
| `suffixed-lifecycle-events` | Rewrites `on` keys that use suffixed v5 lifecycle event types to the category type with `matches`: `'xstate.done.actor.job': { target: 'done' }` becomes `'xstate.done.actor': { matches: { actorId: 'job' }, target: 'done' }`, and the same for `xstate.error.actor.<id>`, `xstate.snapshot.<id>` and `xstate.done.state.<id>` (with `stateId`). A function transition moves under `to`. Reports `xstate.after.<delay>.<id>` keys, keys whose category type is already handled in the same `on`, transitions that are not object or function literals, and suffixed type strings elsewhere in the file, such as `event.type === 'xstate.done.actor.job'`. See [Internal lifecycle events](#internal-lifecycle-events). |
|
|
1639
|
+
| `report-inspection-events` | Reports, without rewriting, string literals naming v5 inspection event types: `@xstate.event`, `@xstate.snapshot`, `@xstate.action`, `@xstate.microstep`, and `@xstate.deadletter`. v6 emits only `@xstate.actor` and `@xstate.transition`, so comparisons against the old types never match; TypeScript reports them only when the handler is typed with `InspectionEvent`. Scans every file, including files that do not import `xstate`. |
|
|
1640
|
+
| `report-action-shapes` | Reports, without rewriting, `entry` and `exit` values that are not functions (strings, arrays, objects, identifiers, or calls such as `assign(...)`), and `actions`, `guard`, and `cond` keys on transition objects under `on`, `after`, `always`, `onDone`, `onError`, `onSnapshot`, or an object `initial`, including each element of a transition array, and the transition arrays themselves. v6 rejects these shapes: `entry` and `exit` take a single function `(args, enq) => { ... }`, and transitions queue effects on `enq` and return `{ target }` or `undefined` instead of listing `actions` and `guard`. |
|
|
1641
|
+
| `report-implementations` | Reports, without rewriting, a second argument to `createMachine(config, implementations)` that `lower-actions` did not move into the config, such as a variable. v6 ignores the second argument. |
|
|
1642
|
+
|
|
1643
|
+
`string-targets` wraps string elements inside transition arrays but keeps the arrays. `lower-actions` rewrites a transition array as one function when any element has `actions` or `guard`; `report-action-shapes` reports the arrays that remain. The v6 `createMachine` types reject transition arrays in `on` and `always` (for example, `on: { X: [ … ] }`); replace each with one transition function (§15, §16).
|
|
1644
|
+
|
|
1645
|
+
`lower-actions` converts the action creators and guard combinators it can resolve in the file. Rewrite the reported ones as inline functions by hand (§1, §2). The transforms read and write imports from `'xstate'` only.
|
|
1646
|
+
|
|
1647
|
+
### Exit codes and JSON output
|
|
1648
|
+
|
|
1649
|
+
`migrate` exits with one of these codes:
|
|
1650
|
+
|
|
1651
|
+
| Code | Meaning |
|
|
1652
|
+
| --- | --- |
|
|
1653
|
+
| `0` | No manual migration is needed. |
|
|
1654
|
+
| `1` | Usage error, or the transformed output failed the syntax check and was not written. |
|
|
1655
|
+
| `2` | Manual migration is needed. At least one transform reported a site. |
|
|
1656
|
+
|
|
1657
|
+
`--json` prints one JSON report to stdout instead of the text summary. `manualSites` lists each reported site:
|
|
1658
|
+
|
|
1659
|
+
```json
|
|
1660
|
+
{
|
|
1661
|
+
"filesChanged": 1,
|
|
1662
|
+
"files": [{ "filePath": "...", "changed": true, "transforms": [{ "name": "...", "changed": true, "notes": [] }] }],
|
|
1663
|
+
"manualSites": [{ "file": "...", "line": 12, "column": 7, "api": "assign", "hint": "...", "transform": "report-removed-apis" }]
|
|
1664
|
+
}
|
|
1665
|
+
```
|
|
1666
|
+
|
|
1667
|
+
### Check behavior with `baseline` and `verify`
|
|
1668
|
+
|
|
1669
|
+
<!-- commands, flags and report fields from packages/xstate-codemod/src/verify/cli.ts, run.ts, diff.ts and deltas.ts -->
|
|
1670
|
+
|
|
1671
|
+
`baseline` and `verify` check that a migrated machine behaves like the v5 original. `baseline` loads the v5 module under XState v5, generates event sequences, and records a trace of each one. `verify` loads the migrated module under XState v6, replays the same sequences, and compares the traces step by step. The codemod package includes both XState versions and resolves `'xstate'` imports in the loaded file to the matching one, so the project can have either version installed.
|
|
1672
|
+
|
|
1673
|
+
Record the baseline before `migrate` rewrites the file:
|
|
1674
|
+
|
|
1675
|
+
```bash
|
|
1676
|
+
npx @xstate/codemod baseline src/checkout.ts --out .xstate-migrate/checkout.json
|
|
1677
|
+
npx @xstate/codemod migrate src/checkout.ts
|
|
1678
|
+
# fix the reported manual sites
|
|
1679
|
+
npx @xstate/codemod verify --baseline .xstate-migrate/checkout.json src/checkout.ts
|
|
1680
|
+
```
|
|
1681
|
+
|
|
1682
|
+
When both versions exist as separate files, `verify v5-file v6-file` records the baseline and replays it in one run.
|
|
1683
|
+
|
|
1684
|
+
The loaded module must export the machine. `--export name` selects an export. Otherwise the commands use the `default` export, then the first export that is actor logic. When the module exports more than one machine and `--export` is not passed, the baseline's `notes` and the text output name the exports and the one used.
|
|
1685
|
+
|
|
1686
|
+
#### Sequences
|
|
1687
|
+
|
|
1688
|
+
Sequences come from the v5 machine's shortest paths, its simple paths, and seeded random walks. Random walks also advance a simulated clock to the next pending timer, so they cover delayed transitions. `--walks` (default `25`), `--steps` (default `12`), and `--seed` (default `1`) control the walks. `--max-sequences` (default `200`) caps the total. Simple paths grow quickly with payload cases and parallel states; when `baseline` fails with `CHILD_TIMEOUT`, pass `--no-simple-paths` to skip them. The baseline stores the setting as `simplePaths: false` in `options`.
|
|
1689
|
+
|
|
1690
|
+
While an actor runs, in both versions, `Date.now()` returns `1700000000000` plus the simulated clock's time, `performance.now()` returns the simulated clock's time, and `Math.random()` returns values from a generator seeded by `--seed` and the sequence index. A machine that stores these values in `context` records the same values under v5 and v6. `new Date()` without arguments still reads the real clock.
|
|
1691
|
+
|
|
1692
|
+
Events are sent as `{ type }` unless an events file supplies payloads. Pass `--events events.json`, an object that maps each event type to a list of payloads:
|
|
1693
|
+
|
|
1694
|
+
```json
|
|
1695
|
+
{ "ADD_ITEM": [{ "type": "ADD_ITEM", "sku": "A1" }, { "type": "ADD_ITEM", "sku": "B2" }] }
|
|
1696
|
+
```
|
|
1697
|
+
|
|
1698
|
+
Event types without cases are listed in the report under `eventsWithoutCases`. When a guard throws because an event has no payload, the graph traversal that hit it is dropped and the event is left out of the random walks that reach it; each such case is listed under `generationErrors`. Add payload cases for those events to cover the transitions behind them. A graph traversal that hits its limit, usually because `context` changes on every step, is also listed there; random walks still cover those paths.
|
|
1699
|
+
|
|
1700
|
+
#### Trace
|
|
1701
|
+
|
|
1702
|
+
The baseline records, for the state after start and after each step:
|
|
1703
|
+
|
|
1704
|
+
- `value`, `context`, `status`, `output`, and `error`
|
|
1705
|
+
- `processed`: every event the root actor processed during the step, in order, including raised, delayed, and done events
|
|
1706
|
+
- `children`: the live child actors, by id and source
|
|
1707
|
+
- `sent`: events sent to other actors, with the target id and delay
|
|
1708
|
+
- `emitted`: events emitted with `emit`
|
|
1709
|
+
- `actions`: custom actions run, by type and params
|
|
1710
|
+
|
|
1711
|
+
Before comparison, events are normalized: suffixed v5 lifecycle event types such as `xstate.done.actor.<id>` become the v6 category types with their `actorId`, `stateId`, or `delay` fields, `xstate.init` is dropped, `sessionId` fields are removed, and generated child ids are replaced by `(anon)`. Functions and symbols in `context` and `output` are replaced by markers and counted under `nonSerializable`.
|
|
1712
|
+
|
|
1713
|
+
`verify` compares every field except `actions` by default. `--compare +actions` adds it. Inline v6 actions have no stable type, so `actions` differ after most migrations. When both versions end a step with `status: 'error'`, only `status` and `error` are compared for that step, because a v6 errored snapshot has no `value`. For each sequence, the first mismatching field at a step is reported as one diff. An unclassified diff ends that sequence. A classified diff is recorded and the comparison continues, so an intentional difference cannot hide a later one.
|
|
1714
|
+
|
|
1715
|
+
#### Stubs
|
|
1716
|
+
|
|
1717
|
+
Invoked and spawned actors that are referenced by name are replaced by stubs. A stub defaults to `"pending"`, a promise that never settles. Pass `--stubs stubs.json` to script outcomes and delays:
|
|
1718
|
+
|
|
1719
|
+
```json
|
|
1720
|
+
{
|
|
1721
|
+
"actors": { "fetchUser": { "done": { "id": 1 } }, "saveOrder": { "error": "boom" }, "poll": "pending" },
|
|
1722
|
+
"delays": { "RETRY": 100 }
|
|
1723
|
+
}
|
|
1724
|
+
```
|
|
1725
|
+
|
|
1726
|
+
`done` resolves with the given output, and `error` rejects with the given value. `{}` resolves with `undefined`, and `{ "done": null }` resolves with `null`. `delays` sets named delays on both versions.
|
|
1727
|
+
|
|
1728
|
+
A baseline records one outcome per actor. To cover both the `onDone` and `onError` paths, record a second baseline with a different `--stubs` file.
|
|
1729
|
+
|
|
1730
|
+
#### Input
|
|
1731
|
+
|
|
1732
|
+
Pass `--input input.json` when the machine reads `input`. The file's JSON value is passed as `createActor(logic, { input })` in both versions. `baseline` stores the input in the baseline file, and `verify --baseline` uses it unless `--input` is passed.
|
|
1733
|
+
|
|
1734
|
+
When the machine errors at start, `baseline` and `verify` print `start: machine errored at start: <error>`, and `baseline --json` reports the message as `startError`. A machine that builds `context` from `input` errors this way when no input is passed.
|
|
1735
|
+
|
|
1736
|
+
#### Results
|
|
1737
|
+
|
|
1738
|
+
`verify` exits with one of these codes:
|
|
1739
|
+
|
|
1740
|
+
| Code | Meaning |
|
|
1741
|
+
| --- | --- |
|
|
1742
|
+
| `0` | Equivalent. Every diff, if any, matched an intentional delta. |
|
|
1743
|
+
| `1` | Usage error. |
|
|
1744
|
+
| `3` | Diverged. At least one diff is `unclassified`. |
|
|
1745
|
+
| `4` | A module failed to load or run. `error` has the phase (`baseline` or `replay`), a code such as `EXPORT_NOT_FOUND`, and the message. |
|
|
1746
|
+
|
|
1747
|
+
The default output is a summary and the first five unclassified diffs. `--json` prints the full report. Each entry in `diffs` has the sequence id, the step, the `field`, the v5 value as `expected`, the v6 value as `actual`, a `witness` (the events and clock advances that reproduce the step), and a `classification`. A `status` diff where either side errored also has a `detail` with the error message of each errored side.
|
|
1748
|
+
|
|
1749
|
+
A diff that matches a known v6 change is classified with a rule instead of `unclassified`:
|
|
1750
|
+
|
|
1751
|
+
| Rule | Diff |
|
|
1752
|
+
| --- | --- |
|
|
1753
|
+
| `anonymous-child-id` | Child or send-target ids that differ only by a generated suffix. |
|
|
1754
|
+
| `error-string-shape` | Error strings with the same message after the prefix is removed. |
|
|
1755
|
+
| `context-reference` | `context` that is equal after removing actor-reference keys (`$actor`). |
|
|
1756
|
+
| `done-event-session` | Processed events that are equal after dropping `sessionId`. |
|
|
1757
|
+
| `final-state-output` | `output` that is undefined in v5 and set in v6, because v6 uses a top-level final state's `output` when the root has none (see [Machine output](#machine-output)). |
|
|
1758
|
+
| `root-send-parent` | `status` and `error` where v5 errored with `Unable to send event to actor '#_parent'` and v6 did not, because v6 makes a send to a missing parent a dead letter (see [Sending to a missing target](#sending-to-a-missing-target)). Later fields of that step, such as `value`, can still differ. |
|
|
1759
|
+
|
|
1760
|
+
Fix the v6 code for each unclassified diff. Do not edit the baseline.
|
|
1612
1761
|
|
|
1613
|
-
|
|
1762
|
+
#### Limits
|
|
1614
1763
|
|
|
1615
|
-
|
|
1764
|
+
- Actor logic written inline in `invoke.src` or `spawn` cannot be stubbed. It runs for real in both versions, and its source is listed under `unstubbed`.
|
|
1765
|
+
- Actions run for real. Side effects inside them, such as network requests or writes, happen during `baseline` and `verify`.
|
|
1766
|
+
- `actions` are not compared unless `--compare +actions` is passed. v6 records an `emit` as an action named after the emitted event, so `+actions` reports a diff on every emitting step.
|
|
1767
|
+
- Random walks advance the simulated clock to the next pending timer only. The v6 clock also fires timers scheduled during a jump and the v5 clock does not, so a longer jump would diverge on clock behavior alone.
|
|
1768
|
+
- An equivalent result covers the generated sequences only. Behavior that needs an event payload or actor outcome missing from the events and stubs files is not exercised.
|
|
1769
|
+
- A transition that reenters its own parallel region reports a `value` diff that is not classified. v5 reset the sibling regions and v6 leaves them in place (see "Transitions inside parallel regions"). Confirm that the v6 value is the intended one, or add explicit targets for the siblings.
|
|
1770
|
+
- A machine whose eventless transitions never settle makes `baseline` fail with `CHILD_CRASH` and an out-of-memory message, because v5 has no microstep limit. v6 ends the actor with `InfiniteTransitionError` instead. Fix the loop in the v5 machine before recording a baseline.
|
|
1616
1771
|
|
|
1617
1772
|
### Running v5 and v6 side by side
|
|
1618
1773
|
|
|
@@ -1629,7 +1784,7 @@ import { createMachine as createV5Machine } from 'xstate';
|
|
|
1629
1784
|
import { createMachine } from 'xstate-v6';
|
|
1630
1785
|
```
|
|
1631
1786
|
|
|
1632
|
-
Migrate one file at a time: run the codemod on it, finish the manual changes, then change its `'xstate'` imports to `'xstate-v6'`. When no v5 imports remain, remove the alias and install `xstate@beta` as `xstate`.
|
|
1787
|
+
Migrate one file at a time: run the codemod on it, finish the manual changes, then change its `'xstate'` imports to `'xstate-v6'`. `verify` resolves `'xstate'`, `'xstate/*'`, `'xstate-v6'`, and `'xstate-v6/*'` imports in the migrated file to its own XState v6, so it works before or after the import change. When no v5 imports remain, remove the alias and install `xstate@beta` as `xstate`.
|
|
1633
1788
|
|
|
1634
1789
|
---
|
|
1635
1790
|
|
|
@@ -1825,7 +1980,9 @@ In v5, an undeclared delay name resolved to `undefined`, and the delayed transit
|
|
|
1825
1980
|
|
|
1826
1981
|
## Migration checklist
|
|
1827
1982
|
|
|
1983
|
+
- [ ] Record a behavior baseline for each machine file with `xstate-codemod baseline` (§27)
|
|
1828
1984
|
- [ ] Run `xstate-codemod migrate --dry`, apply it, and migrate the APIs it reports by hand (§27)
|
|
1985
|
+
- [ ] Run `xstate-codemod verify --baseline` on each migrated file and fix every unclassified diff (§27)
|
|
1829
1986
|
- [ ] Replace every `assign({...})` with an inline function returning a shallow `{ context: {...} }` patch
|
|
1830
1987
|
- [ ] Replace every `raise`, `sendTo`, `sendParent`, `forwardTo`, `emit`, `log`, `cancel`, `spawnChild`, `stopChild` with the corresponding `enq.*` call
|
|
1831
1988
|
- [ ] Replace `enqueueActions(...)` with a regular inline `(args, enq) => { ... }` function
|