@xstate/codemod 0.1.0-beta.0 → 0.1.0-beta.2

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.
@@ -1,5 +1,11 @@
1
1
  import { Node, SyntaxKind, SymbolFlags, ts, Project } from 'ts-morph';
2
- import process from 'node:process';
2
+ import process$1 from 'node:process';
3
+ import { mkdirSync, writeFileSync, readFileSync } from 'node:fs';
4
+ import { dirname, resolve, join } from 'node:path';
5
+ import { parseArgs as parseArgs$1 } from 'node:util';
6
+ import { spawn } from 'node:child_process';
7
+ import { createRequire } from 'node:module';
8
+ import { pathToFileURL } from 'node:url';
3
9
 
4
10
  /** Map of v5 xstate exports → v6 replacement names. */
5
11
  const RENAMES = {
@@ -73,7 +79,7 @@ const renameImports = {
73
79
  * Handles both `createMachine(...)` and `setup(...).createMachine(...)` by
74
80
  * returning the trailing property/identifier name (e.g. `createMachine`).
75
81
  */
76
- function getCallName(call) {
82
+ function getCallName$1(call) {
77
83
  const expr = call.getExpression();
78
84
  if (Node.isIdentifier(expr)) {
79
85
  return expr.getText();
@@ -96,7 +102,7 @@ function getCallName(call) {
96
102
  function findMachineConfigObjects(sourceFile, callNames = ['createMachine', 'createStateConfig']) {
97
103
  const result = [];
98
104
  for (const call of sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {
99
- const name = getCallName(call);
105
+ const name = getCallName$1(call);
100
106
  if (!name || !callNames.includes(name)) {
101
107
  continue;
102
108
  }
@@ -283,52 +289,89 @@ const stringTargets = {
283
289
  };
284
290
 
285
291
  /**
286
- * Extracts the `T` type node from a `{} as T` (AsExpression) initializer of a
287
- * `types:` property. Returns undefined if the shape isn't recognized.
292
+ * Flattens an events type into `[eventType, payloadTypeText]` entries. Accepts
293
+ * inline object literal members with a string-literal `type`, and references
294
+ * to non-generic type aliases declared in the same file. Returns undefined for
295
+ * any other member (imported types, interfaces, mapped types, ...).
288
296
  */
289
- function getTypesAsTypeNode(init) {
290
- if (!Node.isAsExpression(init)) {
291
- return undefined;
297
+ function eventsToEventMap(typeNode, seen = new Set()) {
298
+ if (Node.isParenthesizedTypeNode(typeNode)) {
299
+ return eventsToEventMap(typeNode.getTypeNode(), seen);
292
300
  }
293
- return init.getTypeNode();
294
- }
295
-
296
- /**
297
- * Given a union type node written as an inline literal union of object types,
298
- * each with a `type: '...'` discriminant, returns a map entry list of
299
- * `[eventType, remainingObjectTypeText]`. Returns undefined when the union is
300
- * not a literal inline union of object types with string-literal `type` props.
301
- */
302
- function unionToEventMap(typeNode) {
303
- const members = Node.isUnionTypeNode(typeNode) ? typeNode.getTypeNodes() : [typeNode];
304
- const entries = [];
305
- for (const member of members) {
306
- if (!Node.isTypeLiteral(member)) {
307
- return undefined; // e.g. a type reference — bail out entirely
301
+ if (Node.isUnionTypeNode(typeNode)) {
302
+ const entries = [];
303
+ for (const member of typeNode.getTypeNodes()) {
304
+ const memberEntries = eventsToEventMap(member, seen);
305
+ if (!memberEntries) return undefined;
306
+ entries.push(...memberEntries);
308
307
  }
309
- const lit = member;
310
- const props = lit.getProperties();
311
- const typeProp = props.find(p => p.getName() === 'type');
312
- if (!typeProp) {
308
+ return entries;
309
+ }
310
+ if (Node.isTypeReference(typeNode)) {
311
+ const name = typeNode.getTypeName();
312
+ if (!Node.isIdentifier(name) || typeNode.getTypeArguments().length) {
313
313
  return undefined;
314
314
  }
315
- const typePropTypeNode = typeProp.getTypeNode();
316
- if (!typePropTypeNode || typePropTypeNode.getKind() !== SyntaxKind.LiteralType) {
315
+ const alias = typeNode.getSourceFile().getTypeAlias(name.getText());
316
+ const aliased = alias?.getTypeNode();
317
+ if (!alias || !aliased || alias.getTypeParameters().length) {
317
318
  return undefined;
318
319
  }
319
- const literal = typePropTypeNode.getFirstChild();
320
- if (!literal || !Node.isStringLiteral(literal)) return undefined;
321
- const eventType = literal.getLiteralValue();
320
+ if (seen.has(alias.getName())) return undefined;
321
+ return eventsToEventMap(aliased, new Set(seen).add(alias.getName()));
322
+ }
323
+ if (!Node.isTypeLiteral(typeNode)) return undefined;
324
+ const props = typeNode.getProperties();
325
+ if (props.length !== typeNode.getMembers().length) return undefined;
326
+ const typePropTypeNode = props.find(p => p.getName() === 'type')?.getTypeNode();
327
+ if (!typePropTypeNode || !Node.isLiteralTypeNode(typePropTypeNode)) {
328
+ return undefined;
329
+ }
330
+ const literal = typePropTypeNode.getLiteral();
331
+ if (!Node.isStringLiteral(literal)) return undefined;
332
+
333
+ // Build payload object type text from the remaining props.
334
+ const payloadProps = props.filter(p => p.getName() !== 'type').map(p => p.getText().replace(/[;,]\s*$/, ''));
335
+ const payload = payloadProps.length ? `{ ${payloadProps.join('; ')} }` : `{}`;
336
+ return [{
337
+ eventType: literal.getLiteralValue(),
338
+ payload
339
+ }];
340
+ }
322
341
 
323
- // Build payload object type text from the remaining props.
324
- const payloadProps = props.filter(p => p.getName() !== 'type').map(p => p.getText().replace(/[;,]\s*$/, ''));
325
- const payload = payloadProps.length ? `{ ${payloadProps.join('; ')} }` : `{}`;
326
- entries.push({
327
- eventType,
328
- payload
342
+ /**
343
+ * Reads the members of a `types` initializer, written either as
344
+ * `{} as { context: C; events: E }` or per key as
345
+ * `{ context: {} as C, events: {} as E }`. Returns `'opaque'` for `{} as T`
346
+ * where `T` is not an inline object type, and undefined for any other shape.
347
+ */
348
+ function getTypesMembers(init) {
349
+ if (Node.isAsExpression(init)) {
350
+ const typeNode = init.getTypeNode();
351
+ if (!typeNode) return undefined;
352
+ if (!Node.isTypeLiteral(typeNode)) return 'opaque';
353
+ return typeNode.getProperties().flatMap(member => {
354
+ const memberType = member.getTypeNode();
355
+ return memberType ? [{
356
+ key: member.getName(),
357
+ typeNode: memberType
358
+ }] : [];
359
+ });
360
+ }
361
+ if (!Node.isObjectLiteralExpression(init)) return undefined;
362
+ const members = [];
363
+ for (const prop of init.getProperties()) {
364
+ if (!Node.isPropertyAssignment(prop)) return undefined;
365
+ const key = getKey(prop);
366
+ const value = prop.getInitializer();
367
+ const typeNode = value && Node.isAsExpression(value) ? value.getTypeNode() : undefined;
368
+ if (!key || !typeNode) return undefined;
369
+ members.push({
370
+ key,
371
+ typeNode
329
372
  });
330
373
  }
331
- return entries;
374
+ return members;
332
375
  }
333
376
  function getKey(prop) {
334
377
  const nameNode = prop.getNameNode();
@@ -340,91 +383,109 @@ function getKey(prop) {
340
383
  }
341
384
  return undefined;
342
385
  }
386
+ const LEFTOVER_TYPES_HINT = '`types` was replaced by `schemas`; declare events as a map `schemas: { events: { TYPE: types<Payload>() } }`, or use `types<EventUnion>()` when the union is not inline.';
387
+ const EVENTS_FALLBACK_HINT = '`events: types<E>()` loads but does not type-check as a v6 event map; expand `E` into `events: { TYPE: types<Payload>(), ... }`.';
388
+
389
+ /** Records a manual site for a `types` property left at `pos`. */
390
+ function pushLeftoverSite(sourceFile, pos, sites) {
391
+ const {
392
+ line,
393
+ column
394
+ } = sourceFile.getLineAndColumnAtPos(pos);
395
+ sites.push({
396
+ file: sourceFile.getFilePath(),
397
+ line,
398
+ column,
399
+ api: 'types',
400
+ hint: LEFTOVER_TYPES_HINT
401
+ });
402
+ }
343
403
 
344
404
  /**
345
- * Converts a single `types: {} as { ... }` property into a `schemas: { ... }`
346
- * property. Mutates the containing config object literal.
405
+ * Converts a single `types` property into a `schemas: { ... }` property.
406
+ * Mutates the containing config object literal.
347
407
  */
348
- function convertTypesProperty(configObj, typesProp, notes) {
349
- const init = typesProp.getInitializerOrThrow();
350
- const typeNode = getTypesAsTypeNode(init);
351
- if (!typeNode) {
408
+ function convertTypesProperty(configObj, typesProp, notes, sites, getHelper) {
409
+ const sourceFile = configObj.getSourceFile();
410
+ const start = typesProp.getStart();
411
+ const members = getTypesMembers(typesProp.getInitializerOrThrow());
412
+ if (!members) {
413
+ pushLeftoverSite(sourceFile, start, sites);
352
414
  return false;
353
415
  }
354
416
 
355
417
  // `types: {} as any` (or non-object) — cannot infer a shape; leave a note.
356
- if (!Node.isTypeLiteral(typeNode)) {
357
- notes.push(`left 'types' unchanged: initializer is not an inline object type ({} as ${typeNode.getText()})`);
418
+ if (members === 'opaque') {
419
+ notes.push(`left 'types' unchanged: initializer is not an inline object type (${typesProp.getInitializerOrThrow().getText()})`);
420
+ pushLeftoverSite(sourceFile, start, sites);
358
421
  typesProp.replaceWithText(`${typesProp.getText()} /* TODO(xstate-codemod): could not migrate 'types' to 'schemas' automatically */`);
359
422
  return false;
360
423
  }
361
- const lit = typeNode;
362
- const schemaEntries = [];
363
- let usedTypes = false;
364
- let eventsLeftBehind = false;
365
- for (const member of lit.getProperties()) {
366
- const key = member.getName();
367
- const valueTypeNode = member.getTypeNode();
368
- if (!valueTypeNode) {
369
- continue;
370
- }
371
- if (key === 'events') {
372
- const map = unionToEventMap(valueTypeNode);
373
- if (!map) {
374
- // Not a literal inline union — leave events in `types`, add a TODO.
375
- eventsLeftBehind = true;
376
- notes.push(`could not migrate 'events' to schemas.events map (not a literal inline union); left in place`);
377
- continue;
378
- }
379
- schemaEntries.push(helper => {
380
- const mapEntries = map.map(e => `${JSON.stringify(e.eventType)}: ${helper}<${e.payload}>()`);
381
- return `events: {\n ${mapEntries.join(',\n ')}\n }`;
382
- });
383
- usedTypes = true;
384
- } else {
385
- // context, input, output, emitted, tags, meta, ... → types<T>()
386
- const valueType = valueTypeNode.getText();
387
- schemaEntries.push(helper => `${key}: ${helper}<${valueType}>()`);
388
- usedTypes = true;
389
- }
390
- }
391
- if (schemaEntries.length === 0) {
392
- return false;
393
- }
394
- const helper = usedTypes ? ensureNamedImport(configObj.getSourceFile(), 'types', configObj) : undefined;
424
+ if (members.length === 0) return false;
425
+ const helper = getHelper(configObj);
395
426
  if (!helper) {
396
427
  notes.push("left 'types' unchanged: no xstate runtime import available");
428
+ pushLeftoverSite(sourceFile, start, sites);
397
429
  return false;
398
430
  }
399
- const schemasText = `schemas: {\n ${schemaEntries.map(entry => entry(helper)).join(',\n ')}\n}`;
400
- if (eventsLeftBehind) {
401
- // Keep the original `types` property (holding only the un-migratable
402
- // events) alongside the new `schemas` property.
403
- // Rebuild `types` with only the `events` member preserved.
404
- const eventsMember = lit.getProperties().find(p => p.getName() === 'events');
405
- const preservedTypes = eventsMember ? `types: {} as { ${eventsMember.getText()} } /* TODO(xstate-codemod): migrate events to schemas.events map */` : typesProp.getText();
406
- typesProp.replaceWithText(`${schemasText},\n ${preservedTypes}`);
407
- } else {
408
- typesProp.replaceWithText(schemasText);
431
+ let eventsFallback = false;
432
+ const schemaEntries = members.map(({
433
+ key,
434
+ typeNode
435
+ }) => {
436
+ if (key === 'events') {
437
+ const map = eventsToEventMap(typeNode);
438
+ if (map) {
439
+ const mapEntries = map.map(e => `${JSON.stringify(e.eventType)}: ${helper}<${e.payload}>()`);
440
+ return `events: {\n ${mapEntries.join(',\n ')}\n }`;
441
+ }
442
+ notes.push(`'events' is not an inline literal union or a same-file alias of one; wrote it as a single ${helper}<...>() schema`);
443
+ eventsFallback = true;
444
+ }
445
+ // context, input, output, emitted, tags, meta, ... → types<T>()
446
+ return `${key}: ${helper}<${typeNode.getText()}>()`;
447
+ });
448
+ typesProp.replaceWithText(`schemas: {\n ${schemaEntries.join(',\n ')}\n}`);
449
+ if (eventsFallback) {
450
+ // `events: types<E>()` loads but is not a valid v6 event map type.
451
+ const schemas = configObj.getProperty('schemas');
452
+ const events = schemas && Node.isPropertyAssignment(schemas) ? schemas.getInitializerIfKind(SyntaxKind.ObjectLiteralExpression)?.getProperty('events') : undefined;
453
+ const {
454
+ line,
455
+ column
456
+ } = sourceFile.getLineAndColumnAtPos((events ?? schemas ?? configObj).getStart());
457
+ sites.push({
458
+ file: sourceFile.getFilePath(),
459
+ line,
460
+ column,
461
+ api: 'types',
462
+ hint: EVENTS_FALLBACK_HINT
463
+ });
409
464
  }
410
- notes.push(`converted 'types' → 'schemas' (${schemaEntries.length} key(s))${eventsLeftBehind ? "; 'events' left as types (manual)" : ''}`);
465
+ notes.push(`converted 'types' → 'schemas' (${schemaEntries.length} key(s))`);
411
466
  return true;
412
467
  }
413
468
 
414
469
  /**
415
- * Converts `types: {} as { context; events; input; ... }` inside machine config
416
- * objects into `schemas: { context: types<...>(), ... }`.
470
+ * Converts `types: {} as { context; events; input; ... }` (or the per-key
471
+ * `types: { context: {} as C, ... }` form) inside machine config objects into
472
+ * `schemas: { context: types<...>(), ... }`.
417
473
  */
418
474
  const typesToSchemas = {
419
475
  name: 'types-to-schemas',
420
- 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.',
476
+ description: 'Convert `types: {} as {...}` and `types: { context: {} as C, ... }` into `schemas: { context: types<C>(), events: { ... }, input: types<I>() }`. Events become a map when written as a literal union, inline or in a same-file type alias; otherwise `events: types<E>()`.',
421
477
  apply(sourceFile) {
422
478
  const notes = [];
479
+ const sites = [];
423
480
  let changed = false;
481
+ // `ensureNamedImport` reads stale scope after adding an import, so resolve
482
+ // the helper binding once per file.
483
+ let helper;
484
+ const getHelper = location => helper ??= ensureNamedImport(sourceFile, 'types', location);
424
485
  for (const configObj of findMachineConfigObjects(sourceFile, ['createMachine', 'createStateConfig', 'setup'])) {
425
486
  for (const prop of configObj.getProperties()) {
426
487
  if (Node.isPropertyAssignment(prop) && getKey(prop) === 'types') {
427
- if (convertTypesProperty(configObj, prop, notes)) {
488
+ if (convertTypesProperty(configObj, prop, notes, sites, getHelper)) {
428
489
  changed = true;
429
490
  }
430
491
  }
@@ -432,206 +493,2313 @@ const typesToSchemas = {
432
493
  }
433
494
  return {
434
495
  changed,
435
- notes
496
+ notes,
497
+ sites
436
498
  };
437
499
  }
438
500
  };
439
501
 
440
- /**
441
- * v5 `'xstate'` exports that require structural rewrites in v6 and are too
442
- * risky to automate. Value is a one-line hint for the v6 replacement.
443
- */
444
- const REMOVED_APIS = {
445
- assign: 'return a `{ context }` patch from the transition/action function',
446
- raise: 'use `enq.raise(...)` inside an action',
447
- sendTo: 'use `enq.sendTo(...)` inside an action',
448
- sendParent: 'use `enq.sendTo(...)` with the parent ref inside an action',
449
- forwardTo: 'use `enq.sendTo(target, event)` inside an action',
450
- emit: 'use `enq.emit(...)` inside an action',
451
- log: 'use `enq.log(...)` inside an action',
452
- cancel: 'use `enq.cancel(...)` inside an action',
453
- spawnChild: 'use `enq.spawn(...)` inside an action',
454
- stop: 'use `enq.stop(...)` inside an action',
455
- stopChild: 'use `enq.stop(...)` inside an action',
456
- enqueueActions: 'action functions now receive an `enq` enqueue object directly',
457
- and: 'inline the guard as `(...) => a(...) && b(...)`',
458
- or: 'inline the guard as `(...) => a(...) || b(...)`',
459
- not: 'inline the guard as `(...) => !a(...)`',
460
- stateIn: 'use `self.getSnapshot().matches(...)`',
461
- fromPromise: 'use `createAsyncLogic(...)`',
462
- fromTransition: 'use `createStepLogic({ context, run })`',
463
- toObserver: 'pass an observer object or callback directly',
464
- pathToStateValue: 'build the state value object directly',
465
- SpecialTargets: 'use `enq.sendTo(self, event)` for internal sends; parent sends use `enq.sendTo(...)` with the parent ref'
502
+ /** Thrown to abandon lowering of one node; the report transforms then flag it. */
503
+ class Skip extends Error {}
504
+ const skip = reason => {
505
+ throw new Skip(reason);
466
506
  };
467
507
 
468
- /**
469
- * Detects (does NOT transform) usages of v5 `'xstate'` APIs that were removed
470
- * or restructured in v6. Reports file:line + API + a replacement hint via the
471
- * returned notes.
472
- */
473
- const reportRemovedApis = {
474
- name: 'report-removed-apis',
475
- description: 'Detect (report only) usages of removed/restructured v5 xstate APIs (assign, raise, sendTo, emit, fromPromise, and/or/not, toObserver, SpecialTargets, etc.) that need manual migration.',
476
- apply(sourceFile) {
477
- const notes = [];
478
- const xstateImports = sourceFile.getImportDeclarations().filter(imp => imp.getModuleSpecifierValue() === 'xstate');
479
-
480
- // Map local binding name → canonical API name (respecting aliases).
481
- const tracked = new Map();
482
- for (const imp of xstateImports) {
483
- for (const spec of imp.getNamedImports()) {
484
- const original = spec.getNameNode().getText();
485
- if (REMOVED_APIS[original]) {
486
- const local = (spec.getAliasNode() ?? spec.getNameNode()).getText();
487
- tracked.set(local, original);
488
- }
489
- }
508
+ /** Local name → imported name, for value imports from `'xstate'`. */
509
+ function getXstateImports(sourceFile) {
510
+ const map = new Map();
511
+ for (const imp of sourceFile.getImportDeclarations()) {
512
+ if (imp.getModuleSpecifierValue() !== 'xstate' || imp.isTypeOnly()) continue;
513
+ for (const spec of imp.getNamedImports()) {
514
+ if (spec.isTypeOnly()) continue;
515
+ const local = (spec.getAliasNode() ?? spec.getNameNode()).getText();
516
+ map.set(local, spec.getName());
490
517
  }
491
- if (tracked.size === 0) {
492
- return {
493
- changed: false,
494
- notes
518
+ }
519
+ return map;
520
+ }
521
+
522
+ /** The `'xstate'` export a call invokes, e.g. `assign` for `assign(...)`. */
523
+ function xstateCallee(node, xs) {
524
+ if (!Node.isCallExpression(node)) return undefined;
525
+ const expr = node.getExpression();
526
+ return Node.isIdentifier(expr) ? xs.get(expr.getText()) : undefined;
527
+ }
528
+ function getKeyText$2(node) {
529
+ if (!Node.isPropertyAssignment(node) && !Node.isMethodDeclaration(node) && !Node.isShorthandPropertyAssignment(node)) return undefined;
530
+ const name = node.getNameNode();
531
+ if (Node.isStringLiteral(name) || Node.isNoSubstitutionTemplateLiteral(name)) return name.getLiteralText();
532
+ if (Node.isIdentifier(name) || Node.isNumericLiteral(name)) return name.getText();
533
+ return undefined;
534
+ }
535
+ function getProperty$1(object, key) {
536
+ if (!Node.isObjectLiteralExpression(object)) return undefined;
537
+ const property = object.getProperties().find(candidate => getKeyText$2(candidate) === key);
538
+ return Node.isPropertyAssignment(property) ? property.getInitializer() : undefined;
539
+ }
540
+ function isFunctionLike(node) {
541
+ return !!node && (Node.isArrowFunction(node) || Node.isFunctionExpression(node));
542
+ }
543
+
544
+ /** `.name` or `['na me']`. */
545
+ function access(name) {
546
+ return /^[A-Za-z_$][\w$]*$/.test(name) ? `.${name}` : `[${JSON.stringify(name)}]`;
547
+ }
548
+ /** Guard combinators a named guard may be defined as, inlined at call sites. */
549
+ const INLINE_GUARDS = new Set(['and', 'or', 'not', 'stateIn']);
550
+ function classify$1(property, xs, group) {
551
+ if (Node.isMethodDeclaration(property)) return {
552
+ kind: 'fn',
553
+ property
554
+ };
555
+ if (Node.isShorthandPropertyAssignment(property)) {
556
+ if (group === 'actions') {
557
+ const constAssign = implConstAssign(property.getNameNode(), xs);
558
+ if (constAssign) return {
559
+ kind: 'assign',
560
+ property,
561
+ ...constAssign
495
562
  };
496
563
  }
497
- const filePath = sourceFile.getFilePath();
498
- for (const ident of sourceFile.getDescendantsOfKind(SyntaxKind.Identifier)) {
499
- const local = ident.getText();
500
- const api = tracked.get(local);
501
- if (!api) {
502
- continue;
503
- }
504
- // Skip the import specifier declarations themselves.
505
- const parentKind = ident.getParent()?.getKind();
506
- if (parentKind === SyntaxKind.ImportSpecifier || parentKind === SyntaxKind.ImportClause) {
507
- continue;
508
- }
509
- const {
510
- line,
511
- column
512
- } = sourceFile.getLineAndColumnAtPos(ident.getPos());
513
- notes.push(`${filePath}:${line}:${column} — '${api}' → ${REMOVED_APIS[api]}`);
514
- }
515
-
516
- // Never mutates the file.
517
564
  return {
518
- changed: false,
519
- notes
565
+ kind: resolveIdentifier(property.getNameNode()) === 'fn' ? 'fn' : 'other',
566
+ property
520
567
  };
521
568
  }
522
- };
523
-
524
- /** Keys whose string values (or string array elements) are state targets. */
525
- const TARGET_KEYS = new Set(['target', 'initial', 'on', 'after', 'always', 'onDone', 'onError']);
569
+ if (!Node.isPropertyAssignment(property)) return {
570
+ kind: 'other',
571
+ property
572
+ };
573
+ const value = unwrap(property.getInitializer());
574
+ if (isFunctionLike(value)) return {
575
+ kind: 'fn',
576
+ property
577
+ };
578
+ if (xstateCallee(value, xs) === 'assign') {
579
+ const arg = value.getArguments()[0];
580
+ if (arg) return {
581
+ kind: 'assign',
582
+ property,
583
+ assignArg: arg
584
+ };
585
+ }
586
+ if (Node.isIdentifier(value) && resolveIdentifier(value) === 'fn') {
587
+ return {
588
+ kind: 'fn',
589
+ property
590
+ };
591
+ }
592
+ if (group === 'actions' && Node.isIdentifier(value)) {
593
+ const constAssign = implConstAssign(value, xs);
594
+ if (constAssign) return {
595
+ kind: 'assign',
596
+ property,
597
+ ...constAssign
598
+ };
599
+ }
600
+ const callee = xstateCallee(value, xs);
601
+ if (callee && (group === 'guards' ? INLINE_GUARDS.has(callee) : ACTION_CREATORS.has(callee) || callee === 'enqueueActions')) {
602
+ return {
603
+ kind: 'inline',
604
+ property,
605
+ expr: value
606
+ };
607
+ }
608
+ return {
609
+ kind: 'other',
610
+ property
611
+ };
612
+ }
526
613
 
527
- /** Calls whose first argument is a state value or `#id` reference. */
528
- const MATCH_CALLS = new Set(['matches', 'stateIn']);
529
- function getPropertyName(node) {
530
- if (Node.isPropertyAssignment(node)) {
531
- const name = node.getNameNode();
532
- if (Node.isIdentifier(name) || Node.isStringLiteral(name)) {
533
- return Node.isStringLiteral(name) ? name.getLiteralText() : name.getText();
614
+ /** Strips `as` / `satisfies` / parentheses. */
615
+ function unwrap(node) {
616
+ let current = node;
617
+ while (Node.isAsExpression(current) || Node.isSatisfiesExpression(current) || Node.isParenthesizedExpression(current)) {
618
+ current = current.getExpression();
619
+ }
620
+ return current;
621
+ }
622
+ function collectFrom(impls, object, xs) {
623
+ if (!object || !Node.isObjectLiteralExpression(object)) return;
624
+ for (const group of ['actions', 'guards']) {
625
+ const map = getProperty$1(object, group);
626
+ if (!map || !Node.isObjectLiteralExpression(map)) continue;
627
+ for (const property of map.getProperties()) {
628
+ const key = implKey(property);
629
+ if (key === undefined || impls[group].has(key)) continue;
630
+ impls[group].set(key, classify$1(property, xs, group));
534
631
  }
535
632
  }
536
- return undefined;
537
633
  }
538
- function getStringProperty(object, name) {
539
- const property = object.getProperty(name);
540
- if (!property || !Node.isPropertyAssignment(property)) return undefined;
541
- const initializer = property.getInitializer();
542
- return initializer && (Node.isStringLiteral(initializer) || Node.isNoSubstitutionTemplateLiteral(initializer)) ? initializer.getLiteralText() : undefined;
634
+
635
+ /** Finds the `setup({...})` argument a `createMachine`/`createStateConfig` call is chained to. */
636
+ function findSetupArgument(config) {
637
+ const call = config.getParent();
638
+ if (!Node.isCallExpression(call)) return undefined;
639
+ const expr = call.getExpression();
640
+ if (!Node.isPropertyAccessExpression(expr)) return undefined;
641
+ let receiver = expr.getExpression();
642
+ if (Node.isIdentifier(receiver)) {
643
+ const decl = receiver.getSymbol()?.getDeclarations().find(Node.isVariableDeclaration);
644
+ const init = decl?.getInitializer();
645
+ if (!init) return undefined;
646
+ receiver = init;
647
+ }
648
+ if (!Node.isCallExpression(receiver)) return undefined;
649
+ const callee = receiver.getExpression();
650
+ if (!Node.isIdentifier(callee) || callee.getText() !== 'setup') return undefined;
651
+ return receiver.getArguments()[0];
652
+ }
653
+
654
+ /** Named implementations visible to a machine config: its own top-level keys, then `setup`. */
655
+ function collectImplementations(config, xs) {
656
+ const impls = {
657
+ actions: new Map(),
658
+ guards: new Map()
659
+ };
660
+ collectFrom(impls, config, xs);
661
+ collectFrom(impls, findSetupArgument(config), xs);
662
+ return impls;
663
+ }
664
+
665
+ /** What an identifier's in-file declaration is: a function, a plain value, or unknown. */
666
+ function resolveIdentifier(id) {
667
+ const parent = id.getParent();
668
+ const symbol = Node.isShorthandPropertyAssignment(parent) ? parent.getValueSymbol() : id.getSymbol();
669
+ const decls = symbol?.getDeclarations() ?? [];
670
+ if (decls.length !== 1) return 'unknown';
671
+ const decl = decls[0];
672
+ if (Node.isFunctionDeclaration(decl)) return 'fn';
673
+ if (!Node.isVariableDeclaration(decl)) return 'unknown';
674
+ const list = decl.getParent();
675
+ if (Node.isVariableDeclarationList(list) && !(list.getFlags() & 2) /* NodeFlags.Const */) return 'unknown';
676
+ const init = decl.getInitializer();
677
+ if (!init) return 'unknown';
678
+ const value = unwrap(init);
679
+ if (isFunctionLike(value)) return 'fn';
680
+ if (Node.isObjectLiteralExpression(value) || Node.isArrayLiteralExpression(value) || Node.isStringLiteral(value) || Node.isNumericLiteral(value) || Node.isNoSubstitutionTemplateLiteral(value) || Node.isTemplateExpression(value)) return 'value';
681
+ return 'unknown';
682
+ }
683
+
684
+ /** Initializer of an identifier's single `const` declaration in this file. */
685
+ function resolveConstInitializer(id) {
686
+ const decls = id.getSymbol()?.getDeclarations() ?? [];
687
+ if (decls.length !== 1 || !Node.isVariableDeclaration(decls[0])) return undefined;
688
+ const init = decls[0].getInitializer();
689
+ return init && unwrap(init);
543
690
  }
691
+ function isReferenceIdentifier(id) {
692
+ const parent = id.getParent();
693
+ if (!parent) return false;
694
+ if ((Node.isPropertyAssignment(parent) || Node.isPropertySignature(parent) || Node.isMethodDeclaration(parent) || Node.isPropertyDeclaration(parent)) && parent.getNameNode() === id) return false;
695
+ if (Node.isPropertyAccessExpression(parent) && parent.getNameNode() === id) return false;
696
+ if (Node.isBindingElement(parent) && parent.getPropertyNameNode() === id) return false;
697
+ if (Node.isQualifiedName(parent) && parent.getRight() === id) return false;
698
+ if (Node.isTypeReference(parent)) return false;
699
+ return true;
700
+ }
701
+
544
702
  /**
545
- * Collects the state ids that one machine config literal declares: explicit
546
- * state `id`s and, for `createMachine` configs, implicit ids (the path from
547
- * the root, prefixed by the machine id when one is set). Invoke and actor ids
548
- * are not state ids and are not collected.
703
+ * Names in `names` that `node` references while declared outside it (or not
704
+ * at all, such as globals). Generated bindings with these names would capture
705
+ * those references.
549
706
  */
550
- function collectStateIds(config, withImplicitIds) {
551
- const ids = new Set();
552
- function walk(stateConfig, implicitId) {
553
- const explicitId = getStringProperty(stateConfig, 'id');
554
- if (explicitId !== undefined) ids.add(explicitId);else if (withImplicitIds) ids.add(implicitId);
555
- const states = stateConfig.getProperty('states');
556
- if (!states || !Node.isPropertyAssignment(states)) return;
557
- const statesObject = states.getInitializer();
558
- if (!statesObject || !Node.isObjectLiteralExpression(statesObject)) return;
559
- for (const state of statesObject.getProperties()) {
560
- const key = getPropertyName(state);
561
- if (key === undefined || !Node.isPropertyAssignment(state)) continue;
562
- const child = state.getInitializer();
563
- if (!child || !Node.isObjectLiteralExpression(child)) continue;
564
- walk(child, implicitId ? `${implicitId}.${key}` : key);
707
+ function outerReferences(node, names) {
708
+ const found = new Set();
709
+ const start = node.getPos();
710
+ const end = node.getEnd();
711
+ for (const id of node.getDescendantsOfKind(SyntaxKind.Identifier)) {
712
+ const text = id.getText();
713
+ if (!names.has(text) || !isReferenceIdentifier(id)) continue;
714
+ const parent = id.getParent();
715
+ const symbol = Node.isShorthandPropertyAssignment(parent) ? id.getProject().getTypeChecker().compilerObject.getShorthandAssignmentValueSymbol(parent.compilerNode) : id.getSymbol()?.compilerSymbol;
716
+ const decls = symbol?.declarations ?? [];
717
+ if (!decls.length || decls.some(decl => decl.pos < start || decl.end > end)) {
718
+ found.add(text);
565
719
  }
566
720
  }
567
- walk(config, getStringProperty(config, 'id') ?? '');
568
- return ids;
721
+ return found;
722
+ }
723
+
724
+ /** Key of an implementation map entry, resolving `[Enum.X]` computed keys. */
725
+ function implKey(property) {
726
+ if ((Node.isPropertyAssignment(property) || Node.isMethodDeclaration(property)) && Node.isComputedPropertyName(property.getNameNode())) {
727
+ const name = property.getNameNode();
728
+ return Node.isComputedPropertyName(name) ? resolveStringConstant(name.getExpression()) : undefined;
729
+ }
730
+ return getKeyText$2(property);
569
731
  }
570
732
 
571
733
  /**
572
- * Finds the `createMachine(...)` or `createStateConfig(...)` config literal
573
- * that contains `node`, if any.
734
+ * The string an expression always evaluates to: a string literal, a `const`
735
+ * bound to one, a member of a string enum, or a property of a `const` object
736
+ * literal declared in this file.
574
737
  */
575
- function findEnclosingConfig(node, machineConfigs, stateConfigs) {
576
- for (const ancestor of node.getAncestors()) {
577
- if (!Node.isObjectLiteralExpression(ancestor)) continue;
578
- if (machineConfigs.has(ancestor)) {
579
- return {
580
- config: ancestor,
581
- withImplicitIds: true
582
- };
583
- }
584
- if (stateConfigs.has(ancestor)) {
585
- return {
586
- config: ancestor,
587
- withImplicitIds: false
588
- };
589
- }
738
+ function resolveStringConstant(node) {
739
+ const value = unwrap(node);
740
+ if (Node.isStringLiteral(value) || Node.isNoSubstitutionTemplateLiteral(value)) return value.getLiteralText();
741
+ if (Node.isIdentifier(value)) {
742
+ const decls = value.getSymbol()?.getDeclarations() ?? [];
743
+ if (decls.length !== 1 || !Node.isVariableDeclaration(decls[0])) return;
744
+ if (!isConst(decls[0])) return;
745
+ const init = decls[0].getInitializer();
746
+ const literal = init && unwrap(init);
747
+ return literal && Node.isStringLiteral(literal) ? literal.getLiteralText() : undefined;
590
748
  }
591
- return undefined;
749
+ if (!Node.isPropertyAccessExpression(value)) return undefined;
750
+ const object = value.getExpression();
751
+ if (!Node.isIdentifier(object)) return undefined;
752
+ const member = value.getName();
753
+ const decls = object.getSymbol()?.getDeclarations() ?? [];
754
+ if (decls.length !== 1) return undefined;
755
+ const decl = decls[0];
756
+ if (Node.isEnumDeclaration(decl)) {
757
+ const m = decl.getMember(member);
758
+ const init = m?.getInitializer();
759
+ return init && Node.isStringLiteral(init) ? init.getLiteralText() : undefined;
760
+ }
761
+ if (!Node.isVariableDeclaration(decl) || !isConst(decl)) return undefined;
762
+ const init = decl.getInitializer();
763
+ const literal = init && unwrap(init);
764
+ if (!literal || !Node.isObjectLiteralExpression(literal)) return undefined;
765
+ const property = literal.getProperties().find(candidate => getKeyText$2(candidate) === member);
766
+ if (!Node.isPropertyAssignment(property)) return undefined;
767
+ const v = unwrap(property.getInitializer());
768
+ return Node.isStringLiteral(v) || Node.isNoSubstitutionTemplateLiteral(v) ? v.getLiteralText() : undefined;
592
769
  }
593
- function isMatchArgument(literal) {
594
- const call = literal.getParent();
595
- if (!call || !Node.isCallExpression(call)) return false;
596
- if (call.getArguments()[0] !== literal) return false;
597
- const expression = call.getExpression();
598
- const name = Node.isIdentifier(expression) ? expression.getText() : Node.isPropertyAccessExpression(expression) ? expression.getName() : undefined;
599
- return !!name && MATCH_CALLS.has(name);
770
+ function isConst(decl) {
771
+ const list = decl.getParent();
772
+ return Node.isVariableDeclarationList(list) && !!(list.getFlags() & 2) /* NodeFlags.Const */;
600
773
  }
601
- function isTargetValue(literal) {
602
- let node = literal;
603
- let parent = node.getParent();
604
- if (parent && Node.isArrayLiteralExpression(parent)) {
605
- node = parent;
606
- parent = node.getParent();
774
+
775
+ /**
776
+ * Free identifiers in `node` that are declared inside a function or block
777
+ * other than the module (so inlining the node elsewhere could not see them).
778
+ */
779
+ function nonModuleReferences(node) {
780
+ const found = new Set();
781
+ const start = node.getPos();
782
+ const end = node.getEnd();
783
+ for (const id of node.getDescendantsOfKind(SyntaxKind.Identifier)) {
784
+ if (!isReferenceIdentifier(id)) continue;
785
+ const parent = id.getParent();
786
+ const symbol = Node.isShorthandPropertyAssignment(parent) ? id.getProject().getTypeChecker().compilerObject.getShorthandAssignmentValueSymbol(parent.compilerNode) : id.getSymbol()?.compilerSymbol;
787
+ for (const decl of symbol?.declarations ?? []) {
788
+ if (decl.pos >= start && decl.end <= end) continue;
789
+ if (decl.getSourceFile() !== node.getSourceFile().compilerNode) continue;
790
+ let scope = decl.parent;
791
+ while (scope && !ts.isSourceFile(scope)) {
792
+ if (ts.isFunctionLike(scope) || ts.isBlock(scope) || ts.isClassLike(scope) || ts.isModuleBlock(scope)) {
793
+ found.add(id.getText());
794
+ break;
795
+ }
796
+ scope = scope.parent;
797
+ }
798
+ }
607
799
  }
608
- if (!parent || !Node.isPropertyAssignment(parent)) return false;
609
- const name = getPropertyName(parent);
610
- if (name && TARGET_KEYS.has(name)) return true;
611
- // A bare string transition inside `on: { EVENT: '#a.b' }` or
612
- // `after: { 1000: '#a.b' }`.
613
- const container = parent.getParent();
614
- const owner = container?.getParent();
615
- const ownerName = owner ? getPropertyName(owner) : undefined;
616
- return ownerName === 'on' || ownerName === 'after';
800
+ return [...found];
617
801
  }
618
802
 
803
+ /** Action creators (besides `assign`) that lower to `enq` statements. */
804
+ const ACTION_CREATORS = new Set(['raise', 'sendTo', 'sendParent', 'forwardTo', 'emit', 'log', 'cancel', 'stopChild', 'stop', 'spawnChild']);
619
805
  /**
620
- * Reports (does NOT transform) `#a.b` targets and `matches('#a.b')`/
621
- * `stateIn('#a.b')` references that the codemod cannot verify as whole state
622
- * ids. In v6, the text after `#` is an exact id and is never split into an
623
- * id followed by a path.
806
+ * `const x = assign(...)` (or another action creator) declared at module
807
+ * level and not exported, for an identifier used as an action.
624
808
  */
625
- const reportIdPathTargets = {
626
- name: 'report-id-path-targets',
627
- description: "Detect (report only) '#id.path' targets and matches('#id.path') references that may combine an id with a relative path, which v6 no longer resolves.",
628
- apply(sourceFile) {
629
- const notes = [];
630
- const filePath = sourceFile.getFilePath();
631
- const machineConfigs = new Set(findMachineConfigObjects(sourceFile, ['createMachine']));
632
- const stateConfigs = new Set(findMachineConfigObjects(sourceFile, ['createStateConfig']));
633
- const idsByConfig = new Map();
634
- const literals = [...sourceFile.getDescendantsOfKind(SyntaxKind.StringLiteral), ...sourceFile.getDescendantsOfKind(SyntaxKind.NoSubstitutionTemplateLiteral)].sort((a, b) => a.getStart() - b.getStart());
809
+ function resolveConstAction(id, xs) {
810
+ const decls = id.getSymbol()?.getDeclarations() ?? [];
811
+ if (decls.length !== 1) return undefined;
812
+ const decl = decls[0];
813
+ if (!Node.isVariableDeclaration(decl) || !isConst(decl)) return undefined;
814
+ const statement = decl.getVariableStatement();
815
+ if (!statement || !Node.isSourceFile(statement.getParent()) || statement.isExported() || statement.getDeclarations().length !== 1) return undefined;
816
+ const init = decl.getInitializer();
817
+ const call = init && unwrap(init);
818
+ const callee = call && xstateCallee(call, xs);
819
+ if (!callee || !(callee === 'assign' || callee === 'enqueueActions' || ACTION_CREATORS.has(callee))) return undefined;
820
+ return {
821
+ decl,
822
+ call: call,
823
+ callee
824
+ };
825
+ }
826
+
827
+ /** Whether a config value is a reference rather than written inline. */
828
+ function isIndirect(node) {
829
+ const value = unwrap(node);
830
+ return Node.isIdentifier(value) && value.getText() !== 'undefined' || Node.isPropertyAccessExpression(value) || Node.isElementAccessExpression(value) || Node.isShorthandPropertyAssignment(value) || Node.isSpreadAssignment(value) || Node.isSpreadElement(value);
831
+ }
832
+ /**
833
+ * Resolves a config reference (identifier, shorthand property or spread) to
834
+ * the object/array literal or function of a module-level `const` in this file.
835
+ */
836
+ function derefConfig(node) {
837
+ let value = unwrap(node);
838
+ let symbol;
839
+ if (Node.isShorthandPropertyAssignment(value)) {
840
+ symbol = value.getValueSymbol();
841
+ } else {
842
+ if (Node.isSpreadAssignment(value) || Node.isSpreadElement(value)) {
843
+ value = unwrap(value.getExpression());
844
+ }
845
+ if (!Node.isIdentifier(value)) return undefined;
846
+ symbol = value.getSymbol();
847
+ }
848
+ let decl;
849
+ for (let depth = 0; depth < 5; depth++) {
850
+ const decls = symbol?.getDeclarations() ?? [];
851
+ if (decls.length !== 1) return undefined;
852
+ const d = decls[0];
853
+ if (Node.isFunctionDeclaration(d)) return {
854
+ node: d,
855
+ decl: decl ?? d
856
+ };
857
+ if (!Node.isVariableDeclaration(d) || !isConst(d)) return undefined;
858
+ const statement = d.getVariableStatement();
859
+ if (!statement || !Node.isSourceFile(statement.getParent())) return undefined;
860
+ const init = d.getInitializer();
861
+ if (!init) return undefined;
862
+ decl ??= d;
863
+ value = unwrap(init);
864
+ if (!Node.isIdentifier(value)) break;
865
+ symbol = value.getSymbol();
866
+ }
867
+ if (Node.isObjectLiteralExpression(value) || Node.isArrayLiteralExpression(value) || isFunctionLike(value)) return {
868
+ node: value,
869
+ decl: decl
870
+ };
871
+ return undefined;
872
+ }
873
+
874
+ /**
875
+ * Whether every reference to a module const sits where a config value or a
876
+ * spread of one goes, so rewriting its literal in place is safe.
877
+ */
878
+ function onlyConfigReferences(decl) {
879
+ if (!Node.isVariableDeclaration(decl)) return false;
880
+ const name = decl.getNameNode();
881
+ const symbol = name.getSymbol();
882
+ return decl.getSourceFile().getDescendantsOfKind(SyntaxKind.Identifier).filter(id => {
883
+ if (id === name || id.getText() !== name.getText()) return false;
884
+ const parent = id.getParent();
885
+ return (Node.isShorthandPropertyAssignment(parent) ? parent.getValueSymbol() : id.getSymbol()) === symbol;
886
+ }).every(id => {
887
+ const parent = id.getParent();
888
+ return Node.isShorthandPropertyAssignment(parent) || Node.isSpreadAssignment(parent) || Node.isPropertyAssignment(parent) && parent.getInitializer() === id || Node.isAsExpression(parent) || Node.isSatisfiesExpression(parent);
889
+ });
890
+ }
891
+
892
+ /**
893
+ * `name` in an actions map bound to a module `const name = assign(...)` whose
894
+ * only references are entries of implementation maps.
895
+ */
896
+ function implConstAssign(id, xs) {
897
+ const parent = id.getParent();
898
+ const symbol = Node.isShorthandPropertyAssignment(parent) ? parent.getValueSymbol() : id.getSymbol();
899
+ const decls = symbol?.getDeclarations() ?? [];
900
+ if (decls.length !== 1) return undefined;
901
+ const nameId = Node.isVariableDeclaration(decls[0]) ? decls[0].getNameNode() : undefined;
902
+ if (!nameId) return undefined;
903
+ const found = resolveConstAction(nameId, xs);
904
+ if (!found || found.callee !== 'assign') return undefined;
905
+ const [arg, ...rest] = found.call.getArguments();
906
+ if (!arg || rest.length) return undefined;
907
+ const refsOk = id.getSourceFile().getDescendantsOfKind(SyntaxKind.Identifier).filter(r => r !== nameId && r.getText() === nameId.getText()).every(r => {
908
+ const p = r.getParent();
909
+ const s = Node.isShorthandPropertyAssignment(p) ? p.getValueSymbol() : r.getSymbol();
910
+ if (s !== symbol) return true;
911
+ let value = r;
912
+ while (Node.isAsExpression(value.getParent()) || Node.isSatisfiesExpression(value.getParent()) || Node.isParenthesizedExpression(value.getParent())) value = value.getParent();
913
+ const outer = value.getParent();
914
+ const owner = Node.isShorthandPropertyAssignment(p) ? p : Node.isPropertyAssignment(outer) && outer.getInitializer() === value ? outer : undefined;
915
+ const map = owner?.getParent()?.getParent();
916
+ return !!owner && Node.isPropertyAssignment(map) && getKeyText$2(map) === 'actions';
917
+ });
918
+ return refsOk ? {
919
+ assignArg: arg,
920
+ constDecl: found.decl
921
+ } : undefined;
922
+ }
923
+
924
+ /** The `'xstate'` export an identifier is a value import of, if any. */
925
+ function xstateImportOf(id) {
926
+ if (!Node.isIdentifier(id)) return undefined;
927
+ for (const decl of id.getSymbol()?.getDeclarations() ?? []) {
928
+ if (!Node.isImportSpecifier(decl) || decl.isTypeOnly()) continue;
929
+ const imp = decl.getImportDeclaration();
930
+ if (imp.getModuleSpecifierValue() === 'xstate' && !imp.isTypeOnly()) {
931
+ return decl.getName();
932
+ }
933
+ }
934
+ return undefined;
935
+ }
936
+ function isXstateNamespace(id) {
937
+ if (!Node.isIdentifier(id)) return false;
938
+ return (id.getSymbol()?.getDeclarations() ?? []).some(decl => Node.isNamespaceImport(decl) && decl.getFirstAncestorByKind(SyntaxKind.ImportDeclaration)?.getModuleSpecifierValue() === 'xstate');
939
+ }
940
+
941
+ /** `xstate`'s `name` called as `name(...)` or `ns.name(...)`. */
942
+ function callsXstate(call, names) {
943
+ if (!Node.isCallExpression(call)) return false;
944
+ const callee = call.getExpression();
945
+ if (Node.isIdentifier(callee)) {
946
+ return names.includes(xstateImportOf(callee) ?? '');
947
+ }
948
+ return Node.isPropertyAccessExpression(callee) && names.includes(callee.getName()) && isXstateNamespace(callee.getExpression());
949
+ }
950
+
951
+ /**
952
+ * Whether a config object is the first argument of xstate's `createMachine` /
953
+ * `createStateConfig`, directly or chained on xstate's `setup(...)`.
954
+ */
955
+ function isXstateMachineCall(call) {
956
+ if (!Node.isCallExpression(call)) return false;
957
+ if (callsXstate(call, ['createMachine', 'createStateConfig'])) return true;
958
+ const callee = call.getExpression();
959
+ if (!Node.isPropertyAccessExpression(callee)) return false;
960
+ if (!['createMachine', 'createStateConfig'].includes(callee.getName())) return false;
961
+ let receiver = unwrap(callee.getExpression());
962
+ if (Node.isIdentifier(receiver)) {
963
+ const decl = receiver.getSymbol()?.getDeclarations().find(Node.isVariableDeclaration);
964
+ const init = decl?.getInitializer();
965
+ if (!init) return false;
966
+ receiver = unwrap(init);
967
+ }
968
+ return callsXstate(receiver, ['setup']);
969
+ }
970
+
971
+ /** Replaced by the local name of `matchesState` once the import is ensured. */
972
+ const MATCHES_STATE = '__xstate_codemod_matchesState__';
973
+
974
+ /** What v5's `enqueueActions` callback may destructure. */
975
+ const ENQUEUE_ARGS = new Set(['enqueue', 'check', 'context', 'event', 'self', 'system']);
976
+ const ENQUEUE_METHODS = new Set(['assign', 'raise', 'sendTo', 'sendParent', 'emit', 'log', 'cancel', 'stopChild', 'spawnChild']);
977
+
978
+ /** Indentation of the line a node starts on. */
979
+ function indentOf(node) {
980
+ const text = node.getSourceFile().getFullText();
981
+ const lineStart = text.lastIndexOf('\n', node.getStart() - 1) + 1;
982
+ return /^[ \t]*/.exec(text.slice(lineStart))[0];
983
+ }
984
+
985
+ /** The symbol a (possibly shorthand-property) identifier refers to. */
986
+ function valueSymbol(id) {
987
+ const parent = id.getParent();
988
+ if (Node.isShorthandPropertyAssignment(parent)) {
989
+ return id.getProject().getTypeChecker().compilerObject.getShorthandAssignmentValueSymbol(parent.compilerNode);
990
+ }
991
+ return id.getSymbol()?.compilerSymbol;
992
+ }
993
+
994
+ /** `node`'s text with non-overlapping descendant ranges replaced. */
995
+ function spliceText(node, replacements) {
996
+ const start = node.getStart();
997
+ let text = node.getText();
998
+ const inside = replacements.filter(r => r.node.getStart() >= start && r.node.getEnd() <= node.getEnd()).filter((r, _, all) => !all.some(o => o !== r && o.node.getStart() <= r.node.getStart() && o.node.getEnd() >= r.node.getEnd() && o.node !== r.node)).sort((a, b) => b.node.getStart() - a.node.getStart());
999
+ for (const r of inside) {
1000
+ text = text.slice(0, r.node.getStart() - start) + r.text + text.slice(r.node.getEnd() - start);
1001
+ }
1002
+ return text;
1003
+ }
1004
+
1005
+ /** Names v5 passed to actions and guards that the v6 `args` object also has. */
1006
+ const HOISTABLE = new Set(['context', 'event', 'self', 'system']);
1007
+ const RESERVED = new Set(['args', 'enq', 'context', 'patch']);
1008
+ const TRANSITION_KEYS = new Set(['target', 'actions', 'guard', 'reenter', 'description', 'meta']);
1009
+ /** `({ a, b }) => expr` (or `() => expr`) whose body can be substituted in place. */
1010
+ function inlineable(fn) {
1011
+ if (!Node.isArrowFunction(fn) || fn.isAsync()) return undefined;
1012
+ const body = fn.getBody();
1013
+ if (Node.isBlock(body)) return undefined;
1014
+ const params = fn.getParameters();
1015
+ if (params.length === 0) return {
1016
+ names: [],
1017
+ body
1018
+ };
1019
+ if (params.length !== 1) return undefined;
1020
+ const param = params[0];
1021
+ if (param.getInitializer() || param.isRestParameter()) return undefined;
1022
+ const pattern = param.getNameNode();
1023
+ if (!Node.isObjectBindingPattern(pattern)) return undefined;
1024
+ const names = [];
1025
+ for (const element of pattern.getElements()) {
1026
+ const name = element.getNameNode();
1027
+ if (element.getPropertyNameNode() || element.getInitializer() || element.getDotDotDotToken() || !Node.isIdentifier(name) || !HOISTABLE.has(name.getText())) return undefined;
1028
+ names.push(name.getText());
1029
+ }
1030
+ return {
1031
+ names,
1032
+ body
1033
+ };
1034
+ }
1035
+ function isAtomic(node) {
1036
+ return Node.isIdentifier(node) || Node.isPropertyAccessExpression(node) || Node.isElementAccessExpression(node) || Node.isCallExpression(node) || Node.isParenthesizedExpression(node) || Node.isStringLiteral(node) || Node.isNumericLiteral(node) || Node.isNoSubstitutionTemplateLiteral(node) || Node.isTrueLiteral(node) || Node.isFalseLiteral(node) || Node.isPrefixUnaryExpression(node);
1037
+ }
1038
+ /** A context patch: object-literal properties, or an expression evaluating to one. */
1039
+
1040
+ class Branch {
1041
+ constructor() {
1042
+ this.entries = [];
1043
+ this.assigned = false;
1044
+ this.enqueued = false;
1045
+ /** Patches written from nested control flow: always accumulate in `patch`. */
1046
+ this.forcePatch = false;
1047
+ /** Literal child ids spawned so far → index of the spawning entry. */
1048
+ this.spawns = new Map();
1049
+ /** An assigner received `spawn`, so child ids are unknown statically. */
1050
+ this.spawnAssign = false;
1051
+ /** A `stopChild('id')` was lowered to `args.children[id]`. */
1052
+ this.stringStop = false;
1053
+ /** Arguments text for guards evaluated against an earlier context. */
1054
+ this.argsOverride = void 0;
1055
+ }
1056
+ argsObj() {
1057
+ if (this.argsOverride) return this.argsOverride;
1058
+ return this.assigned ? '{ ...args, context }' : 'args';
1059
+ }
1060
+ push(line, enqueues = true) {
1061
+ this.entries.push(line);
1062
+ if (enqueues) this.enqueued = true;
1063
+ }
1064
+ pushPatch(patch) {
1065
+ this.entries.push(patch);
1066
+ this.assigned = true;
1067
+ }
1068
+ /**
1069
+ * Statement lines plus the `context` return field. Only the patch is
1070
+ * returned, never the whole threaded context: v6 evaluates the transitions
1071
+ * of parallel regions against the same context and merges their patches,
1072
+ * so returning every key would undo a sibling region's update.
1073
+ */
1074
+ finish(fb) {
1075
+ const patches = this.entries.filter(e => typeof e !== 'string');
1076
+ if (!patches.length && !this.forcePatch) {
1077
+ return {
1078
+ lines: this.entries,
1079
+ threaded: false
1080
+ };
1081
+ }
1082
+ if (!this.forcePatch) {
1083
+ // Nothing after the first assign reads `context`: the patches can be
1084
+ // merged straight into the returned object (effects are queued anyway).
1085
+ const first = this.entries.findIndex(e => typeof e !== 'string');
1086
+ const reads = this.entries.slice(first + 1).some(e => /\bcontext\b/.test(typeof e === 'string' ? e : 'props' in e ? e.props : e.expr));
1087
+ if (!reads) {
1088
+ const merged = patches.length === 1 ? 'props' in patches[0] ? `{ ${patches[0].props} }` : patches[0].expr : `{ ${patches.map(p => 'props' in p ? p.props : `...${p.expr}`).join(', ')} }`;
1089
+ return {
1090
+ lines: this.entries.filter(e => typeof e === 'string'),
1091
+ context: `context: ${merged}`,
1092
+ threaded: false
1093
+ };
1094
+ }
1095
+ }
1096
+ fb.threaded = true;
1097
+ return {
1098
+ lines: ['let context = args.context;', 'let patch = {};', ...threadedLines(this.entries)],
1099
+ context: 'context: patch',
1100
+ threaded: true
1101
+ };
1102
+ }
1103
+ }
1104
+ function threadedLines(entries) {
1105
+ return entries.flatMap(e => typeof e === 'string' ? [e] : ['props' in e ? `patch = { ...patch, ${e.props} };` : `patch = { ...patch, ...${e.expr} };`, 'context = { ...args.context, ...patch };']);
1106
+ }
1107
+
1108
+ /** Builds one generated `(args, enq) => { ... }` function. */
1109
+ class FunctionBuilder {
1110
+ /** Source text of `node`, with active renames applied. */
1111
+ t(node) {
1112
+ return this.renames.length ? spliceText(node, this.renames) : node.getText();
1113
+ }
1114
+ paren(node) {
1115
+ return isAtomic(node) ? this.t(node) : `(${this.t(node)})`;
1116
+ }
1117
+
1118
+ /** `params` of the named action being inlined, passed to its dynamic values. */
1119
+
1120
+ constructor(ctx, node) {
1121
+ this.hoist = new Set();
1122
+ /** Named definitions inlined into this function. */
1123
+ this.inlinedDefs = new Set();
1124
+ /** Module-level action consts called here, with the referencing identifiers. */
1125
+ this.constRefs = new Map();
1126
+ /** True when some branch keeps a running `context` and `patch`. */
1127
+ this.threaded = false;
1128
+ this.inline = void 0;
1129
+ /** Identifier renames applied to source text this builder copies. */
1130
+ this.renames = [];
1131
+ this.paramsText = void 0;
1132
+ this.ctx = void 0;
1133
+ this.ctx = ctx;
1134
+ const reserved = outerReferences(node, RESERVED);
1135
+ if (reserved.size) {
1136
+ skip(`references outer '${[...reserved].join("', '")}', which the generated function would shadow`);
1137
+ }
1138
+ this.inline = outerReferences(node, new Set(['event', 'self', 'system'])).size === 0;
1139
+ }
1140
+ inlined(fn) {
1141
+ if (!this.inline) return undefined;
1142
+ const result = inlineable(fn);
1143
+ if (result) result.names.forEach(name => this.hoist.add(name));
1144
+ return result;
1145
+ }
1146
+
1147
+ /** A value v5 accepted either directly or as a function of the action args. */
1148
+ dynamic(node, br, argsText = br.argsObj()) {
1149
+ const value = unwrap(node);
1150
+ if (isFunctionLike(value)) {
1151
+ const inline = this.inlined(value);
1152
+ const p = this.paramsText ? `, ${this.paramsText}` : '';
1153
+ if (inline) return this.t(inline.body);
1154
+ return `(${this.t(value)})(${argsText}${p})`;
1155
+ }
1156
+ if (Node.isIdentifier(value)) {
1157
+ if (value.getText() === 'undefined') return 'undefined';
1158
+ const kind = resolveIdentifier(value);
1159
+ const p = this.paramsText ? `, ${this.paramsText}` : '';
1160
+ if (kind === 'fn') return `${value.getText()}(${argsText}${p})`;
1161
+ if (kind === 'value') return this.t(node);
1162
+ return skip(`cannot tell whether '${value.getText()}' is a function`);
1163
+ }
1164
+ if (Node.isCallExpression(value) || Node.isNewExpression(value)) {
1165
+ return skip(`'${value.getText()}' may evaluate to a function`);
1166
+ }
1167
+ return this.t(node);
1168
+ }
1169
+ options(node, br, keys) {
1170
+ if (!node) return '';
1171
+ const value = unwrap(node);
1172
+ if (!Node.isObjectLiteralExpression(value)) {
1173
+ return skip('options are not an object literal');
1174
+ }
1175
+ const parts = value.getProperties().map(property => {
1176
+ const key = getKeyText$2(property);
1177
+ if (!key || !keys.includes(key) || !Node.isPropertyAssignment(property)) {
1178
+ return skip(`unsupported option '${property.getText()}'`);
1179
+ }
1180
+ return `${key}: ${this.dynamic(property.getInitializer(), br)}`;
1181
+ });
1182
+ return parts.length ? `, { ${parts.join(', ')} }` : '';
1183
+ }
1184
+
1185
+ // ---- guards ---------------------------------------------------------------
1186
+
1187
+ guard(node, br) {
1188
+ const value = unwrap(node);
1189
+ const ref = Node.isIdentifier(value) ? undefined : nameRef(value);
1190
+ if (ref) return this.namedGuard(ref, undefined, br);
1191
+ if (Node.isObjectLiteralExpression(value)) {
1192
+ const {
1193
+ ref,
1194
+ params
1195
+ } = typeAndParams(value, 'guard');
1196
+ return this.namedGuard(ref, params, br);
1197
+ }
1198
+ if (isFunctionLike(value)) {
1199
+ const inline = this.inlined(value);
1200
+ if (inline) {
1201
+ return {
1202
+ text: this.t(inline.body),
1203
+ atomic: isAtomic(inline.body)
1204
+ };
1205
+ }
1206
+ return {
1207
+ text: `(${this.t(value)})(${br.argsObj()})`,
1208
+ atomic: true
1209
+ };
1210
+ }
1211
+ if (Node.isIdentifier(value)) {
1212
+ if (resolveIdentifier(value) === 'fn') {
1213
+ return {
1214
+ text: `${value.getText()}(${br.argsObj()})`,
1215
+ atomic: true
1216
+ };
1217
+ }
1218
+ const named = resolveStringConstant(value);
1219
+ if (named !== undefined) {
1220
+ return this.namedGuard({
1221
+ name: named,
1222
+ access: access(named)
1223
+ }, undefined, br);
1224
+ }
1225
+ return skip(`unresolved guard '${value.getText()}'`);
1226
+ }
1227
+ const callee = xstateCallee(value, this.ctx.xs);
1228
+ if (callee && Node.isCallExpression(value)) {
1229
+ const [arg, ...rest] = value.getArguments();
1230
+ if (!arg || rest.length) return skip(`unsupported ${callee}(...) call`);
1231
+ switch (callee) {
1232
+ case 'and':
1233
+ case 'or':
1234
+ {
1235
+ const list = unwrap(arg);
1236
+ if (!Node.isArrayLiteralExpression(list)) {
1237
+ return skip(`${callee}(...) argument is not an array literal`);
1238
+ }
1239
+ const guards = list.getElements().map(element => this.guard(element, br));
1240
+ if (!guards.length) {
1241
+ return {
1242
+ text: callee === 'and' ? 'true' : 'false',
1243
+ atomic: true
1244
+ };
1245
+ }
1246
+ if (guards.length === 1) return guards[0];
1247
+ return {
1248
+ text: guards.map(g => g.atomic ? g.text : `(${g.text})`).join(callee === 'and' ? ' && ' : ' || '),
1249
+ atomic: false
1250
+ };
1251
+ }
1252
+ case 'not':
1253
+ {
1254
+ const inner = this.guard(arg, br);
1255
+ return {
1256
+ text: `!${inner.atomic ? inner.text : `(${inner.text})`}`,
1257
+ atomic: true
1258
+ };
1259
+ }
1260
+ case 'stateIn':
1261
+ {
1262
+ const state = unwrap(arg);
1263
+ if (!(Node.isStringLiteral(state) && !state.getLiteralText().startsWith('#') || Node.isObjectLiteralExpression(state))) {
1264
+ return skip('stateIn(...) argument is not a state value literal');
1265
+ }
1266
+ return {
1267
+ text: `${MATCHES_STATE}(${state.getText()}, args.value)`,
1268
+ atomic: true
1269
+ };
1270
+ }
1271
+ }
1272
+ }
1273
+ return skip(`unsupported guard '${value.getText()}'`);
1274
+ }
1275
+
1276
+ /** Throws unless a named definition can be inlined at a call site. */
1277
+ inlineCheck(def, name) {
1278
+ const expr = def.expr;
1279
+ const local = nonModuleReferences(expr);
1280
+ if (local.length) {
1281
+ skip(`'${name}' references '${local.join("', '")}', which are not module-scope`);
1282
+ }
1283
+ const shadowed = outerReferences(expr, new Set([...RESERVED, ...HOISTABLE, 'params']));
1284
+ if (shadowed.size) {
1285
+ skip(`'${name}' references outer '${[...shadowed].join("', '")}', which the generated function would shadow`);
1286
+ }
1287
+ }
1288
+ namedGuard(ref, params, br) {
1289
+ const {
1290
+ name
1291
+ } = ref;
1292
+ const def = this.ctx.impls.guards.get(name);
1293
+ if (def?.kind === 'inline') {
1294
+ if (params) return skip(`guard '${name}' is a combinator given params`);
1295
+ this.inlineCheck(def, name);
1296
+ this.inlinedDefs.add(def);
1297
+ return this.guard(def.expr, br);
1298
+ }
1299
+ if (!def || def.kind !== 'fn') {
1300
+ return skip(`guard '${name}' is not a function defined in this file`);
1301
+ }
1302
+ const p = params ? `, ${this.dynamic(params, br)}` : '';
1303
+ return {
1304
+ text: `args.guards${ref.access}(${br.argsObj()}${p})`,
1305
+ atomic: true
1306
+ };
1307
+ }
1308
+
1309
+ // ---- actions --------------------------------------------------------------
1310
+
1311
+ /** True when the item writes context (an `assign`, inline or named). */
1312
+ isAssign(node) {
1313
+ const value = unwrap(node);
1314
+ if (xstateCallee(value, this.ctx.xs) === 'assign') return true;
1315
+ if (Node.isIdentifier(value)) {
1316
+ const constAction = resolveConstAction(value, this.ctx.xs);
1317
+ if (constAction?.callee === 'assign') {
1318
+ return !this.ctx.disabledConsts.has(constAction.decl);
1319
+ }
1320
+ }
1321
+ const name = Node.isObjectLiteralExpression(value) ? nameRef(getInit(value, 'type'))?.name : Node.isIdentifier(value) ? undefined : nameRef(value)?.name;
1322
+ return !!name && this.ctx.impls.actions.get(name)?.kind === 'assign';
1323
+ }
1324
+ actions(node, br) {
1325
+ if (!node) return;
1326
+ const value = unwrap(node);
1327
+ const items = Node.isArrayLiteralExpression(value) ? value.getElements() : [value];
1328
+ let lastAssign = -1;
1329
+ items.forEach((item, i) => {
1330
+ if (this.isAssign(item)) lastAssign = i;
1331
+ });
1332
+ items.forEach((item, i) => {
1333
+ for (const comment of leadingComments(item)) br.push(comment, false);
1334
+ this.action(item, br, i > lastAssign);
1335
+ });
1336
+ }
1337
+ action(node, br, contextIsFinal) {
1338
+ const value = unwrap(node);
1339
+ const ref = Node.isIdentifier(value) ? undefined : nameRef(value);
1340
+ if (ref) return this.namedAction(ref, undefined, br);
1341
+ if (Node.isObjectLiteralExpression(value)) {
1342
+ const {
1343
+ ref,
1344
+ params
1345
+ } = typeAndParams(value, 'action');
1346
+ return this.namedAction(ref, params, br);
1347
+ }
1348
+ if (Node.isIdentifier(value)) {
1349
+ const named = resolveStringConstant(value);
1350
+ if (named !== undefined) {
1351
+ return this.namedAction({
1352
+ name: named,
1353
+ access: access(named)
1354
+ }, undefined, br);
1355
+ }
1356
+ const constAction = resolveConstAction(value, this.ctx.xs);
1357
+ if (constAction && !this.ctx.disabledConsts.has(constAction.decl)) {
1358
+ const refs = this.constRefs.get(constAction.decl) ?? [];
1359
+ refs.push(value);
1360
+ this.constRefs.set(constAction.decl, refs);
1361
+ if (constAction.callee === 'assign') {
1362
+ const argsText = mentionsSpawn(constAction.call) ? spawnArgs(br) : br.argsObj();
1363
+ return br.pushPatch({
1364
+ expr: `${value.getText()}(${argsText})`
1365
+ });
1366
+ }
1367
+ return br.push(`${value.getText()}(${br.argsObj()}, enq);`);
1368
+ }
1369
+ if (resolveIdentifier(value) !== 'fn') {
1370
+ return skip(`unresolved action '${value.getText()}'`);
1371
+ }
1372
+ return br.push(`enq(${value.getText()}, ${br.argsObj()});`);
1373
+ }
1374
+ if (isFunctionLike(value)) {
1375
+ const candidate = inlineable(value);
1376
+ if (candidate && (contextIsFinal || !candidate.names.includes('context'))) {
1377
+ const inline = this.inlined(value);
1378
+ if (inline) return br.push(`enq(() => ${this.t(inline.body)});`);
1379
+ }
1380
+ return br.push(`enq(${this.t(value)}, ${br.argsObj()});`);
1381
+ }
1382
+ const callee = xstateCallee(value, this.ctx.xs);
1383
+ if (!callee || !Node.isCallExpression(value)) {
1384
+ return skip(`unsupported action '${value.getText()}'`);
1385
+ }
1386
+ return this.creator(callee, value.getArguments(), br);
1387
+ }
1388
+
1389
+ /** Rule C for `callee(...args)`, an action creator call. */
1390
+ creator(callee, args, br) {
1391
+ const arity = (min, max) => {
1392
+ if (args.length < min || args.length > max) {
1393
+ skip(`unsupported ${callee}(...) arity`);
1394
+ }
1395
+ };
1396
+ switch (callee) {
1397
+ case 'assign':
1398
+ arity(1, 1);
1399
+ return this.assign(args[0], br);
1400
+ case 'raise':
1401
+ arity(1, 2);
1402
+ return br.push(`enq.raise(${this.dynamic(args[0], br)}${this.options(args[1], br, ['delay', 'id'])});`);
1403
+ case 'sendTo':
1404
+ arity(2, 3);
1405
+ return br.push(`enq.sendTo(${this.target(args[0], br)}, ${this.dynamic(args[1], br)}${this.options(args[2], br, ['delay', 'id'])});`);
1406
+ case 'sendParent':
1407
+ arity(1, 2);
1408
+ return br.push(`enq.sendTo(args.parent, ${this.dynamic(args[0], br)}${this.options(args[1], br, ['delay', 'id'])});`);
1409
+ case 'forwardTo':
1410
+ arity(1, 2);
1411
+ return br.push(`enq.sendTo(${this.target(args[0], br)}, args.event${this.options(args[1], br, ['delay', 'id'])});`);
1412
+ case 'emit':
1413
+ arity(1, 1);
1414
+ return br.push(`enq.emit(${this.dynamic(args[0], br)});`);
1415
+ case 'log':
1416
+ {
1417
+ arity(0, 2);
1418
+ const logged = args[0] ? this.dynamic(args[0], br) : `{ context: ${br.assigned ? 'context' : 'args.context'}, event: args.event }`;
1419
+ const label = args[1];
1420
+ if (label && !Node.isStringLiteral(unwrap(label))) {
1421
+ return skip('log label is not a string literal');
1422
+ }
1423
+ return br.push(label ? `enq.log(${label.getText()}, ${logged});` : `enq.log(${logged});`);
1424
+ }
1425
+ case 'cancel':
1426
+ arity(1, 1);
1427
+ return br.push(`enq.cancel(${this.dynamic(args[0], br)});`);
1428
+ case 'stop':
1429
+ case 'stopChild':
1430
+ {
1431
+ arity(1, 1);
1432
+ const target = unwrap(args[0]);
1433
+ if (!Node.isStringLiteral(target)) {
1434
+ return br.push(`enq.stop(${this.dynamic(args[0], br)});`);
1435
+ }
1436
+ if (br.spawnAssign) {
1437
+ return skip('stopChild by id after an assign that spawns: the child may not exist in args.children yet');
1438
+ }
1439
+ const at = br.spawns.get(target.getLiteralText());
1440
+ if (at !== undefined) {
1441
+ // v5 adds spawned children immediately; v6 queues the spawn, so
1442
+ // stop the ref it returns instead of reading args.children.
1443
+ const line = br.entries[at];
1444
+ const name = `child${at}`;
1445
+ if (line.startsWith('enq.spawn(')) {
1446
+ br.entries[at] = `const ${name} = ${line}`;
1447
+ }
1448
+ return br.push(`enq.stop(${name});`);
1449
+ }
1450
+ br.stringStop = true;
1451
+ return br.push(`enq.stop(args.children[${target.getText()}]);`);
1452
+ }
1453
+ case 'spawnChild':
1454
+ {
1455
+ arity(1, 2);
1456
+ const src = unwrap(args[0]);
1457
+ if (!Node.isStringLiteral(src) && !Node.isIdentifier(src)) {
1458
+ return skip('spawnChild source is not a name or identifier');
1459
+ }
1460
+ br.push(`enq.spawn(${args[0].getText()}${this.options(args[1], br, ['id', 'input'])});`);
1461
+ const options = args[1] && unwrap(args[1]);
1462
+ const id = options && Node.isObjectLiteralExpression(options) && getInit(options, 'id');
1463
+ const idValue = id && unwrap(id);
1464
+ if (idValue && Node.isStringLiteral(idValue)) {
1465
+ br.spawns.set(idValue.getLiteralText(), br.entries.length - 1);
1466
+ }
1467
+ return;
1468
+ }
1469
+ case 'enqueueActions':
1470
+ arity(1, 1);
1471
+ return this.enqueueActions(args[0], br);
1472
+ }
1473
+ return skip(`'${callee}(...)' cannot be lowered`);
1474
+ }
1475
+
1476
+ /**
1477
+ * `enqueueActions(({ enqueue, check, ... }, params) => BODY)`: BODY is kept
1478
+ * with every `enqueue...(...)` call replaced by its rule-C statements and
1479
+ * every `check(g)` by its rule-B expression. v5's `context` and `check` see
1480
+ * the context from before the callback, even after `enqueue.assign`.
1481
+ */
1482
+ enqueueActions(node, br) {
1483
+ const fn = unwrap(node);
1484
+ if (!Node.isArrowFunction(fn) && !Node.isFunctionExpression(fn)) {
1485
+ return skip('enqueueActions argument is not an inline function');
1486
+ }
1487
+ if (fn.isAsync()) return skip('enqueueActions callback is async');
1488
+ const [first, second, ...more] = fn.getParameters();
1489
+ if (more.length) return skip('enqueueActions callback has extra params');
1490
+ const bound = new Map();
1491
+ if (first) {
1492
+ const pattern = first.getNameNode();
1493
+ if (!Node.isObjectBindingPattern(pattern) || first.getInitializer()) {
1494
+ return skip('enqueueActions callback does not destructure its argument');
1495
+ }
1496
+ for (const element of pattern.getElements()) {
1497
+ const name = element.getNameNode();
1498
+ const key = (element.getPropertyNameNode() ?? name).getText();
1499
+ if (element.getPropertyNameNode() || element.getInitializer() || element.getDotDotDotToken() || !Node.isIdentifier(name) || !ENQUEUE_ARGS.has(key)) {
1500
+ return skip(`unsupported enqueueActions binding '${element.getText()}'`);
1501
+ }
1502
+ bound.set(key, name);
1503
+ }
1504
+ }
1505
+ let paramsName;
1506
+ if (second) {
1507
+ const name = second.getNameNode();
1508
+ if (!Node.isIdentifier(name) || second.getInitializer()) {
1509
+ return skip('enqueueActions params is not a plain identifier');
1510
+ }
1511
+ paramsName = name.getText();
1512
+ }
1513
+ const refsTo = key => {
1514
+ const id = bound.get(key);
1515
+ if (!id) return [];
1516
+ const symbol = valueSymbol(id);
1517
+ return fn.getDescendantsOfKind(SyntaxKind.Identifier).filter(r => r !== id && valueSymbol(r) === symbol);
1518
+ };
1519
+ // Calls to replace: enqueue(...), enqueue.x(...), check(...).
1520
+ const calls = [];
1521
+ for (const ref of refsTo('enqueue')) {
1522
+ const parent = ref.getParentOrThrow();
1523
+ if (Node.isCallExpression(parent) && parent.getExpression() === ref) {
1524
+ calls.push({
1525
+ call: parent,
1526
+ kind: 'enqueue',
1527
+ args: parent.getArguments()
1528
+ });
1529
+ continue;
1530
+ }
1531
+ const call = parent.getParent();
1532
+ if (Node.isPropertyAccessExpression(parent) && Node.isCallExpression(call) && call.getExpression() === parent && ENQUEUE_METHODS.has(parent.getName())) {
1533
+ calls.push({
1534
+ call,
1535
+ kind: parent.getName(),
1536
+ args: call.getArguments()
1537
+ });
1538
+ continue;
1539
+ }
1540
+ return skip('enqueueActions uses `enqueue` other than by calling it');
1541
+ }
1542
+ for (const ref of refsTo('check')) {
1543
+ const parent = ref.getParentOrThrow();
1544
+ if (!Node.isCallExpression(parent) || parent.getExpression() !== ref || parent.getArguments().length !== 1) {
1545
+ return skip('enqueueActions uses `check` other than as check(guard)');
1546
+ }
1547
+ calls.push({
1548
+ call: parent,
1549
+ kind: 'check',
1550
+ args: parent.getArguments()
1551
+ });
1552
+ }
1553
+ const within = (inner, outer) => inner !== outer && inner.getStart() >= outer.getStart() && inner.getEnd() <= outer.getEnd();
1554
+ if (calls.some(a => calls.some(b => within(a.call, b.call)))) {
1555
+ return skip('enqueueActions nests enqueue/check calls');
1556
+ }
1557
+ const body = fn.getBody();
1558
+ if (Node.isBlock(body) && body.getDescendantsOfKind(SyntaxKind.ReturnStatement).some(r => r.getFirstAncestor(a => Node.isArrowFunction(a) || Node.isFunctionExpression(a) || Node.isFunctionDeclaration(a) || Node.isMethodDeclaration(a)) === fn)) {
1559
+ return skip('enqueueActions callback returns early');
1560
+ }
1561
+ const hoistable = ['event', 'self', 'system'].filter(k => refsTo(k).length);
1562
+ if (hoistable.length && !this.inline) {
1563
+ return skip('enqueueActions reads event/self/system shadowed outside');
1564
+ }
1565
+ const assigns = calls.some(c => c.kind === 'assign' || c.kind === 'enqueue' && c.args[0] && this.isAssign(c.args[0]));
1566
+ const contextRefs = refsTo('context');
1567
+ const needSnapshot = contextRefs.length > 0 && (assigns || br.assigned);
1568
+ for (const k of hoistable) this.hoist.add(k);
1569
+ if (contextRefs.length && !needSnapshot) {
1570
+ if (!this.inline) {
1571
+ return skip('enqueueActions reads `context` shadowed outside');
1572
+ }
1573
+ this.hoist.add('context');
1574
+ }
1575
+ const fnText = fn.getText();
1576
+ let snapshot = 'contextBefore';
1577
+ for (let i = 2; new RegExp(`\\b${snapshot}\\b`).test(fnText); i++) {
1578
+ snapshot = `contextBefore${i}`;
1579
+ }
1580
+ const snapshotValue = br.assigned ? 'context' : 'args.context';
1581
+ if (assigns) {
1582
+ br.assigned = true;
1583
+ br.forcePatch = true;
1584
+ this.threaded = true;
1585
+ }
1586
+ const replacements = [];
1587
+ if (needSnapshot) {
1588
+ for (const r of contextRefs) {
1589
+ replacements.push({
1590
+ node: r,
1591
+ text: Node.isShorthandPropertyAssignment(r.getParent()) ? `context: ${snapshot}` : snapshot
1592
+ });
1593
+ }
1594
+ }
1595
+ // Source text copied into enqueue/check lowering sees the renames too.
1596
+ const savedRenames = this.renames;
1597
+ this.renames = [...savedRenames, ...replacements];
1598
+ for (const {
1599
+ call,
1600
+ kind,
1601
+ args
1602
+ } of calls) {
1603
+ if (kind === 'check') {
1604
+ const cb = new Branch();
1605
+ cb.argsOverride = needSnapshot ? `{ ...args, context: ${snapshot} }` : snapshotValue === 'context' ? '{ ...args, context }' : 'args';
1606
+ const saved = this.inline;
1607
+ if (needSnapshot) this.inline = false;
1608
+ try {
1609
+ const g = this.guard(args[0], cb);
1610
+ replacements.push({
1611
+ node: call,
1612
+ text: g.atomic ? g.text : `(${g.text})`
1613
+ });
1614
+ } finally {
1615
+ this.inline = saved;
1616
+ }
1617
+ continue;
1618
+ }
1619
+ const sub = new Branch();
1620
+ sub.assigned = br.assigned;
1621
+ if (kind === 'enqueue') {
1622
+ if (args.length !== 1) return skip('enqueue(...) takes one action');
1623
+ this.action(args[0], sub, false);
1624
+ } else {
1625
+ this.creator(kind, args, sub);
1626
+ }
1627
+ if (sub.enqueued) br.enqueued = true;
1628
+ const lines = threadedLines(sub.entries);
1629
+ const statement = call.getParent();
1630
+ if (Node.isExpressionStatement(statement) || statement === fn) {
1631
+ const target = statement === fn ? call : statement;
1632
+ const indent = indentOf(target);
1633
+ replacements.push({
1634
+ node: target,
1635
+ text: lines.join(`\n${indent}`)
1636
+ });
1637
+ } else if (lines.length === 1 && lines[0].endsWith(';') && typeof sub.entries[0] === 'string') {
1638
+ replacements.push({
1639
+ node: call,
1640
+ text: lines[0].slice(0, -1)
1641
+ });
1642
+ } else {
1643
+ return skip('enqueue call is not a statement');
1644
+ }
1645
+ }
1646
+ this.renames = savedRenames;
1647
+ const pre = [];
1648
+ if (needSnapshot) pre.push(`const ${snapshot} = ${snapshotValue};`);
1649
+ if (paramsName && fn.getDescendantsOfKind(SyntaxKind.Identifier).some(i => i.getText() === paramsName && i !== second.getNameNode())) {
1650
+ pre.push(`const ${paramsName} = ${this.paramsText ?? 'undefined'};`);
1651
+ }
1652
+ const statements = Node.isBlock(body) ? body.getStatements() : [body];
1653
+ const texts = statements.map(st => {
1654
+ const text = spliceText(st, replacements);
1655
+ const indent = indentOf(st);
1656
+ const lines = text.split('\n').map(l => l.startsWith(indent) ? l.slice(indent.length) : l);
1657
+ return Node.isBlock(body) || replacements.some(r => r.node === body) ? lines : [`${lines.join('\n')};`];
1658
+ });
1659
+ const declares = statements.some(st => Node.isVariableStatement(st) || Node.isFunctionDeclaration(st) || Node.isClassDeclaration(st));
1660
+ const flat = texts.flatMap(t => t.flatMap(l => l.split('\n')));
1661
+ if (pre.length || declares) {
1662
+ br.push('{', false);
1663
+ for (const line of [...pre, ...flat]) br.push(` ${line}`, false);
1664
+ br.push('}', false);
1665
+ } else {
1666
+ for (const line of flat) br.push(line, false);
1667
+ }
1668
+ }
1669
+ target(node, br) {
1670
+ const value = unwrap(node);
1671
+ if (Node.isStringLiteral(value)) {
1672
+ if (value.getLiteralText().startsWith('#')) {
1673
+ return skip(`send target '${value.getLiteralText()}' is a system id`);
1674
+ }
1675
+ return value.getText();
1676
+ }
1677
+ return this.dynamic(node, br);
1678
+ }
1679
+ namedAction(ref, params, br) {
1680
+ const {
1681
+ name
1682
+ } = ref;
1683
+ const def = this.ctx.impls.actions.get(name);
1684
+ if (!def) return skip(`action '${name}' is not defined in this file`);
1685
+ if (def.kind === 'inline') {
1686
+ this.inlineCheck(def, name);
1687
+ this.inlinedDefs.add(def);
1688
+ const saved = this.paramsText;
1689
+ this.paramsText = params ? this.dynamic(params, br) : undefined;
1690
+ try {
1691
+ return this.action(def.expr, br, false);
1692
+ } finally {
1693
+ this.paramsText = saved;
1694
+ }
1695
+ }
1696
+ if (def.kind === 'fn') {
1697
+ const p = params ? `, ${this.dynamic(params, br)}` : '';
1698
+ return br.push(`enq(args.actions${ref.access}, ${br.argsObj()}${p});`);
1699
+ }
1700
+ if (def.kind === 'assign') {
1701
+ const argsText = mentionsSpawn(def.assignArg) ? spawnArgs(br) : br.argsObj();
1702
+ const p = params ? `, ${this.dynamic(params, br, argsText)}` : '';
1703
+ this.ctx.assignDefs.add(def);
1704
+ return br.pushPatch({
1705
+ expr: `args.actions${ref.access}(${argsText}${p})`
1706
+ });
1707
+ }
1708
+ return skip(`action '${name}' is not a plain function or assign`);
1709
+ }
1710
+ assign(node, br) {
1711
+ const value = unwrap(node);
1712
+ const argsText = mentionsSpawn(node) ? spawnArgs(br) : br.argsObj();
1713
+ if (Node.isObjectLiteralExpression(value)) {
1714
+ const props = value.getProperties().map(property => {
1715
+ if (Node.isShorthandPropertyAssignment(property)) {
1716
+ return this.t(property);
1717
+ }
1718
+ const key = getKeyText$2(property);
1719
+ if (!Node.isPropertyAssignment(property) || key === undefined) {
1720
+ return skip(`unsupported assign property '${property.getText()}'`);
1721
+ }
1722
+ return `${property.getNameNode().getText()}: ${this.dynamic(property.getInitializer(), br, argsText)}`;
1723
+ });
1724
+ return br.pushPatch({
1725
+ props: props.join(', ')
1726
+ });
1727
+ }
1728
+ if (isFunctionLike(value)) {
1729
+ const inline = this.inlined(value);
1730
+ const body = inline && unwrap(inline.body);
1731
+ if (body && Node.isObjectLiteralExpression(body)) {
1732
+ return br.pushPatch({
1733
+ props: body.getProperties().map(p => this.t(p)).join(', ')
1734
+ });
1735
+ }
1736
+ if (inline) return br.pushPatch({
1737
+ expr: this.paren(inline.body)
1738
+ });
1739
+ return br.pushPatch({
1740
+ expr: `(${this.t(value)})(${argsText})`
1741
+ });
1742
+ }
1743
+ if (Node.isIdentifier(value) && resolveIdentifier(value) === 'fn') {
1744
+ return br.pushPatch({
1745
+ expr: `${value.getText()}(${argsText})`
1746
+ });
1747
+ }
1748
+ if (Node.isIdentifier(value) && isPlainObjectConst(value)) {
1749
+ return br.pushPatch({
1750
+ expr: value.getText()
1751
+ });
1752
+ }
1753
+ return skip(`unsupported assign argument '${value.getText()}'`);
1754
+ }
1755
+
1756
+ // ---- assembly -------------------------------------------------------------
1757
+
1758
+ /**
1759
+ * Header plus body. Hoisted names used once as `name.x` are written as
1760
+ * `args.name.x` instead. `guard` goes first when it needs no hoisted name.
1761
+ * `dropContext`: a function-scope `let context` exists, so `context` is not
1762
+ * destructured.
1763
+ */
1764
+ assemble(body, dropContext, guard) {
1765
+ const all = guard ? [guard, ...body] : body;
1766
+ const names = [];
1767
+ let lines = all;
1768
+ for (const name of [...this.hoist].sort()) {
1769
+ if (dropContext && name === 'context') continue;
1770
+ // Property keys such as the returned `context:` are not references.
1771
+ const re = new RegExp(`(?<![\\w$])(?<!(?:^|[^.])\\.)(?<![{,]\\s*)${name}(?![\\w$])|(?<![\\w$])(?<!(?:^|[^.])\\.)${name}(?![\\w$])(?!\\s*:)`, 'g');
1772
+ const count = lines.reduce((n, l) => n + (l.match(re)?.length ?? 0), 0);
1773
+ if (count === 0) continue;
1774
+ const single = new RegExp(`(?<![\\w$])(?<!(?:^|[^.])\\.)${name}(?=\\.)`);
1775
+ if (count === 1 && lines.some(l => single.test(l))) {
1776
+ lines = lines.map(l => l.replace(single, `args.${name}`));
1777
+ continue;
1778
+ }
1779
+ names.push(name);
1780
+ }
1781
+ const header = names.length ? [`const { ${names.join(', ')} } = args;`] : [];
1782
+ if (!guard) return [...header, ...lines];
1783
+ const [g, ...rest] = lines;
1784
+ const usesHoisted = names.some(n => new RegExp(`(?<![\\w$])(?<!(?:^|[^.])\\.)${n}(?![\\w$])`).test(g));
1785
+ return usesHoisted ? [...header, g, ...rest] : [g, ...header, ...rest];
1786
+ }
1787
+ }
1788
+
1789
+ /** A reference to a named implementation: its name and how to access it. */
1790
+
1791
+ function nameRef(node) {
1792
+ if (!node) return undefined;
1793
+ const value = unwrap(node);
1794
+ const name = resolveStringConstant(value);
1795
+ if (name === undefined) return undefined;
1796
+ return Node.isStringLiteral(value) || Node.isNoSubstitutionTemplateLiteral(value) ? {
1797
+ name,
1798
+ access: access(name)
1799
+ } : {
1800
+ name,
1801
+ access: `[${value.getText()}]`
1802
+ };
1803
+ }
1804
+ function getInit(object, key) {
1805
+ const property = object.getProperties().find(p => getKeyText$2(p) === key);
1806
+ return Node.isPropertyAssignment(property) ? property.getInitializer() : undefined;
1807
+ }
1808
+ function typeAndParams(object, what) {
1809
+ for (const property of object.getProperties()) {
1810
+ const key = getKeyText$2(property);
1811
+ if (!Node.isPropertyAssignment(property) || key !== 'type' && key !== 'params') {
1812
+ return skip(`unsupported ${what} object key '${property.getText()}'`);
1813
+ }
1814
+ }
1815
+ const ref = nameRef(getInit(object, 'type'));
1816
+ if (!ref) return skip(`${what} object has no string 'type'`);
1817
+ return {
1818
+ ref,
1819
+ params: getInit(object, 'params')
1820
+ };
1821
+ }
1822
+ function mentionsSpawn(node) {
1823
+ return /\bspawn\b/.test(node.getText());
1824
+ }
1825
+ function spawnArgs(br) {
1826
+ if (br.stringStop) {
1827
+ skip('an assign spawns after stopChild by id; child ids are not static');
1828
+ }
1829
+ br.spawnAssign = true;
1830
+ return br.assigned ? '{ ...args, context, spawn: enq.spawn }' : '{ ...args, spawn: enq.spawn }';
1831
+ }
1832
+
1833
+ /** `//` and `/* *\/` comments directly before a node, as written. */
1834
+ function leadingComments(node) {
1835
+ const text = node.getSourceFile().getFullText();
1836
+ return (ts.getLeadingCommentRanges(text, node.getPos()) ?? []).map(range => text.slice(range.pos, range.end));
1837
+ }
1838
+
1839
+ // ---- transitions ------------------------------------------------------------
1840
+
1841
+ function hasLowerableKeys(object) {
1842
+ return Node.isObjectLiteralExpression(object) && object.getProperties().some(property => {
1843
+ const key = getKeyText$2(property);
1844
+ return key === 'actions' || key === 'guard' || key === 'cond';
1845
+ });
1846
+ }
1847
+ function lowerBranch(fb, object) {
1848
+ const comments = [];
1849
+ let description;
1850
+ for (const property of object.getProperties()) {
1851
+ const key = getKeyText$2(property);
1852
+ if (key === 'cond') {
1853
+ return skip('`cond` is a v4 key that v5 rejects in development and ignores in production');
1854
+ }
1855
+ if (!key || !TRANSITION_KEYS.has(key) || !Node.isPropertyAssignment(property)) {
1856
+ return skip(`unsupported transition key '${property.getText()}'`);
1857
+ }
1858
+ comments.push(...leadingComments(property));
1859
+ if (key === 'description') {
1860
+ const value = unwrap(property.getInitializer());
1861
+ description = Node.isStringLiteral(value) || Node.isNoSubstitutionTemplateLiteral(value) ? value.getLiteralText() : value.getText();
1862
+ }
1863
+ }
1864
+ const br = new Branch();
1865
+ const guardNode = getInit(object, 'guard');
1866
+ const guard = guardNode ? fb.guard(guardNode, br) : undefined;
1867
+ fb.actions(getInit(object, 'actions'), br);
1868
+ const returnFields = [];
1869
+ for (const key of ['target', 'reenter', 'meta']) {
1870
+ const value = getInit(object, key);
1871
+ if (value) returnFields.push(`${key}: ${value.getText()}`);
1872
+ }
1873
+ const {
1874
+ lines,
1875
+ context,
1876
+ threaded
1877
+ } = br.finish(fb);
1878
+ if (context) returnFields.push(context);
1879
+ return {
1880
+ guard,
1881
+ description,
1882
+ comments,
1883
+ lines,
1884
+ enqueued: br.enqueued,
1885
+ returnFields,
1886
+ threaded
1887
+ };
1888
+ }
1889
+ const descriptionComments = description => description === undefined ? [] : description.split(/\r?\n/).map(line => `// ${line}`.trimEnd());
1890
+ const returnStatement = fields => fields.length ? `return { ${fields.join(', ')} };` : 'return {};';
1891
+ const DESCRIPTION_NOTE = 'dropped a transition `description` (kept as a comment); v6 transition functions carry no description';
1892
+ /** Rewrites `const x = raise(...)` (or another creator) as `(args, enq) => {...}`. */
1893
+ function lowerConstCreator(ctx, call) {
1894
+ try {
1895
+ const fb = new FunctionBuilder(ctx, call);
1896
+ const br = new Branch();
1897
+ fb.actions(call, br);
1898
+ const {
1899
+ lines,
1900
+ context
1901
+ } = br.finish(fb);
1902
+ if (context) return undefined;
1903
+ return ['(args, enq) => {', ...fb.assemble(lines, false).map(l => ` ${l}`), '}'].join('\n');
1904
+ } catch (error) {
1905
+ if (error instanceof Skip) return undefined;
1906
+ throw error;
1907
+ }
1908
+ }
1909
+
1910
+ /** Lowers a transition object or array to a function body. */
1911
+ function lowerTransition(ctx, value) {
1912
+ const fb = new FunctionBuilder(ctx, value);
1913
+ const notes = [];
1914
+ if (Node.isObjectLiteralExpression(value)) {
1915
+ const b = lowerBranch(fb, value);
1916
+ if (b.description !== undefined) notes.push(DESCRIPTION_NOTE);
1917
+ const guardLine = b.guard ? `if (!${b.guard.atomic ? b.guard.text : `(${b.guard.text})`}) return;` : undefined;
1918
+ const body = [...b.lines];
1919
+ if (b.returnFields.length || !b.enqueued) {
1920
+ body.push(returnStatement(b.returnFields));
1921
+ }
1922
+ return {
1923
+ body: [...b.comments, ...fb.assemble(body, b.threaded, guardLine)],
1924
+ above: descriptionComments(b.description),
1925
+ notes,
1926
+ inlined: fb.inlinedDefs,
1927
+ constRefs: fb.constRefs
1928
+ };
1929
+ }
1930
+ if (!Node.isArrayLiteralExpression(value)) {
1931
+ return skip('transition is not an object or array');
1932
+ }
1933
+ const body = [];
1934
+ const elements = value.getElements();
1935
+ for (let i = 0; i < elements.length; i++) {
1936
+ const element = unwrap(elements[i]);
1937
+ if (!Node.isObjectLiteralExpression(element)) {
1938
+ return skip('transition array element is not an object literal');
1939
+ }
1940
+ const b = lowerBranch(fb, element);
1941
+ if (b.description !== undefined) notes.push(DESCRIPTION_NOTE);
1942
+ const inner = [...leadingComments(elements[i]), ...descriptionComments(b.description), ...b.comments, ...b.lines, returnStatement(b.returnFields)];
1943
+ if (!b.guard) {
1944
+ if (b.threaded) {
1945
+ body.push('{', ...inner.map(line => ` ${line}`), '}');
1946
+ } else {
1947
+ body.push(...inner);
1948
+ }
1949
+ if (i < elements.length - 1) {
1950
+ notes.push(`dropped ${elements.length - 1 - i} unreachable transition candidate(s) after an unguarded one`);
1951
+ }
1952
+ break;
1953
+ }
1954
+ body.push(`if (${b.guard.text}) {`, ...inner.map(line => ` ${line}`), '}');
1955
+ }
1956
+ return {
1957
+ body: fb.assemble(body, false),
1958
+ above: [],
1959
+ notes,
1960
+ inlined: fb.inlinedDefs,
1961
+ constRefs: fb.constRefs
1962
+ };
1963
+ }
1964
+
1965
+ /** Lowers an `entry`/`exit` value to a function body. */
1966
+ function lowerEntryExit(ctx, value) {
1967
+ const fb = new FunctionBuilder(ctx, value);
1968
+ const br = new Branch();
1969
+ fb.actions(value, br);
1970
+ const {
1971
+ lines,
1972
+ context,
1973
+ threaded
1974
+ } = br.finish(fb);
1975
+ const body = [...lines];
1976
+ if (context) body.push(`return { ${context} };`);
1977
+ return {
1978
+ body: fb.assemble(body, threaded),
1979
+ above: [],
1980
+ notes: [],
1981
+ inlined: fb.inlinedDefs,
1982
+ constRefs: fb.constRefs
1983
+ };
1984
+ }
1985
+
1986
+ /** Rewrites a named `assign(...)` definition as a patch-returning function. */
1987
+ function lowerAssignDefinition(def) {
1988
+ const arg = unwrap(def.assignArg);
1989
+ if (Node.isIdentifier(arg) && isPlainObjectConst(arg)) {
1990
+ return `() => ${arg.getText()}`;
1991
+ }
1992
+ if (isFunctionLike(arg)) return def.assignArg.getText();
1993
+ if (Node.isIdentifier(arg) && resolveIdentifier(arg) === 'fn') {
1994
+ return arg.getText();
1995
+ }
1996
+ if (!Node.isObjectLiteralExpression(arg)) return undefined;
1997
+ const props = arg.getProperties();
1998
+ if (props.some(p => !Node.isPropertyAssignment(p) && !Node.isShorthandPropertyAssignment(p))) return undefined;
1999
+ const inlineOk = outerReferences(arg, new Set([...HOISTABLE, 'args', 'params'])).size === 0;
2000
+ const names = new Set();
2001
+ const inlined = props.map(p => {
2002
+ if (!Node.isPropertyAssignment(p)) return p.getText();
2003
+ const value = unwrap(p.getInitializer());
2004
+ if (!isFunctionLike(value)) return p.getText();
2005
+ const inline = inlineOk ? inlineable(value) : undefined;
2006
+ if (!inline) return undefined;
2007
+ inline.names.forEach(n => names.add(n));
2008
+ return `${p.getNameNode().getText()}: ${inline.body.getText()}`;
2009
+ });
2010
+ if (inlined.every(p => p !== undefined)) {
2011
+ const param = names.size ? `{ ${[...names].sort().join(', ')} }` : '';
2012
+ return `(${param}) => ({ ${inlined.join(', ')} })`;
2013
+ }
2014
+ if (outerReferences(arg, new Set(['args', 'params'])).size) return undefined;
2015
+ const generic = props.map(p => {
2016
+ if (!Node.isPropertyAssignment(p)) return p.getText();
2017
+ const value = unwrap(p.getInitializer());
2018
+ if (!isFunctionLike(value)) return p.getText();
2019
+ return `${p.getNameNode().getText()}: (${value.getText()})(args, params)`;
2020
+ });
2021
+ return `(args, params) => ({ ${generic.join(', ')} })`;
2022
+ }
2023
+
2024
+ /**
2025
+ * A module-level `const` object literal with no function, spread or method
2026
+ * members: `assign(x)` then merges exactly `x`.
2027
+ */
2028
+ function isPlainObjectConst(id) {
2029
+ const decls = id.getSymbol()?.getDeclarations() ?? [];
2030
+ if (decls.length !== 1 || !Node.isVariableDeclaration(decls[0])) return false;
2031
+ const decl = decls[0];
2032
+ const list = decl.getParent();
2033
+ const statement = decl.getVariableStatement();
2034
+ if (!Node.isVariableDeclarationList(list) || !(list.getFlags() & 2) || !statement || !Node.isSourceFile(statement.getParent())) return false;
2035
+ const init = decl.getInitializer();
2036
+ const value = init && unwrap(init);
2037
+ return !!value && Node.isObjectLiteralExpression(value) && value.getProperties().every(p => Node.isPropertyAssignment(p) && !isFunctionLike(unwrap(p.getInitializer())));
2038
+ }
2039
+
2040
+ const IMPLEMENTATION_KEYS = new Set(['actions', 'guards', 'actors', 'delays']);
2041
+
2042
+ /** Action creators and helpers this transform can leave unreferenced. */
2043
+ const REMOVABLE_IMPORTS = new Set(['assign', 'raise', 'sendTo', 'sendParent', 'forwardTo', 'emit', 'log', 'cancel', 'stopChild', 'stop', 'spawnChild', 'enqueueActions', 'and', 'or', 'not', 'stateIn', 'fromPromise']);
2044
+ function applyEdits(sourceFile, edits) {
2045
+ if (!edits.length) return false;
2046
+ // Keep outermost edits only; nested ones are part of a replaced range.
2047
+ const sorted = [...edits].sort((a, b) => a.start - b.start || b.end - a.end);
2048
+ const kept = [];
2049
+ for (const edit of sorted) {
2050
+ const last = kept[kept.length - 1];
2051
+ if (last && edit.start < last.end) continue;
2052
+ kept.push(edit);
2053
+ }
2054
+ let text = sourceFile.getFullText();
2055
+ for (const edit of kept.reverse()) {
2056
+ text = text.slice(0, edit.start) + edit.text + text.slice(edit.end);
2057
+ }
2058
+ sourceFile.replaceWithText(text);
2059
+ return true;
2060
+ }
2061
+ function indentAt(sourceFile, pos) {
2062
+ const text = sourceFile.getFullText();
2063
+ const lineStart = text.lastIndexOf('\n', pos - 1) + 1;
2064
+ return /^[ \t]*/.exec(text.slice(lineStart))[0];
2065
+ }
2066
+ function where(sourceFile, node) {
2067
+ const {
2068
+ line,
2069
+ column
2070
+ } = sourceFile.getLineAndColumnAtPos(node.getStart());
2071
+ return `${sourceFile.getFilePath()}:${line}:${column}`;
2072
+ }
2073
+
2074
+ /** Appends property texts to an object literal, keeping its layout. */
2075
+ function appendProperties(sourceFile, object, texts) {
2076
+ const last = object.getProperties().at(-1);
2077
+ if (!last) {
2078
+ const indent = indentAt(sourceFile, object.getStart());
2079
+ return {
2080
+ start: object.getStart() + 1,
2081
+ end: object.getEnd() - 1,
2082
+ text: `\n${texts.map(t => `${indent} ${t}`).join(',\n')}\n${indent}`
2083
+ };
2084
+ }
2085
+ const indent = indentAt(sourceFile, last.getStart());
2086
+ return {
2087
+ start: last.getEnd(),
2088
+ end: last.getEnd(),
2089
+ text: texts.map(t => `,\n${indent}${t}`).join('')
2090
+ };
2091
+ }
2092
+
2093
+ // ---- F: move createMachine's second argument into the config -----------------
2094
+
2095
+ function moveImplementations(sourceFile, notes) {
2096
+ const edits = [];
2097
+ for (const call of sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {
2098
+ const expr = call.getExpression();
2099
+ const name = Node.isIdentifier(expr) ? expr.getText() : Node.isPropertyAccessExpression(expr) ? expr.getName() : undefined;
2100
+ const args = call.getArguments();
2101
+ if (name !== 'createMachine' || args.length !== 2 || !isXstateMachineCall(call)) continue;
2102
+ const [config, impls] = args.map(unwrap);
2103
+ if (!Node.isObjectLiteralExpression(config) || !Node.isObjectLiteralExpression(impls)) {
2104
+ notes.push(`${where(sourceFile, args[1])} — left the second createMachine argument: not an object literal`);
2105
+ continue;
2106
+ }
2107
+ const plan = [];
2108
+ const newKeys = [];
2109
+ let ok = true;
2110
+ for (const property of impls.getProperties()) {
2111
+ const key = getKeyText$2(property);
2112
+ if (!key || !IMPLEMENTATION_KEYS.has(key) || !(Node.isPropertyAssignment(property) || Node.isShorthandPropertyAssignment(property))) {
2113
+ ok = false;
2114
+ break;
2115
+ }
2116
+ const existing = getProperty$1(config, key);
2117
+ if (!existing) {
2118
+ newKeys.push(property.getText());
2119
+ continue;
2120
+ }
2121
+ const value = Node.isPropertyAssignment(property) ? unwrap(property.getInitializer()) : undefined;
2122
+ const target = unwrap(existing);
2123
+ if (!value || !Node.isObjectLiteralExpression(value) || !Node.isObjectLiteralExpression(target)) {
2124
+ ok = false;
2125
+ break;
2126
+ }
2127
+ const texts = value.getProperties().map(p => p.getText());
2128
+ if (texts.length) plan.push(appendProperties(sourceFile, target, texts));
2129
+ }
2130
+ if (!ok) {
2131
+ notes.push(`${where(sourceFile, args[1])} — left the second createMachine argument: keys other than actions/guards/actors/delays, or a non-literal map`);
2132
+ continue;
2133
+ }
2134
+ if (newKeys.length) plan.push(appendProperties(sourceFile, config, newKeys));
2135
+ plan.push({
2136
+ start: args[0].getEnd(),
2137
+ end: args[1].getEnd(),
2138
+ text: ''
2139
+ });
2140
+ edits.push(...plan);
2141
+ notes.push(`${where(sourceFile, args[1])} — moved the second createMachine argument into the config`);
2142
+ }
2143
+ return applyEdits(sourceFile, edits);
2144
+ }
2145
+
2146
+ // ---- A–E: transitions and entry/exit --------------------------------------------
2147
+
2148
+ function render(sourceFile, property, value, lowered) {
2149
+ const indent = indentAt(sourceFile, property.getStart());
2150
+ const text = lowered.body.join('\n');
2151
+ const usesEnq = /\benq\b/.test(text);
2152
+ const params = usesEnq ? '(args, enq)' : /\bargs\b/.test(text) ? '(args)' : '()';
2153
+ const fn = [`${params} => {`, ...lowered.body.map(line => `${indent} ${line}`), `${indent}}`].join('\n');
2154
+ if (!lowered.above.length || !Node.isPropertyAssignment(property)) {
2155
+ return {
2156
+ start: value.getStart(),
2157
+ end: value.getEnd(),
2158
+ text: fn
2159
+ };
2160
+ }
2161
+ return {
2162
+ start: property.getStart(),
2163
+ end: value.getEnd(),
2164
+ text: `${lowered.above.join(`\n${indent}`)}\n${indent}${property.getNameNode().getText()}: ${fn}`
2165
+ };
2166
+ }
2167
+ function lowerMachines(sourceFile, notes) {
2168
+ // A module-level action const is rewritten only when every reference to it
2169
+ // is a call site that got lowered; otherwise plan again without it.
2170
+ const disabledConsts = new Set();
2171
+ for (;;) {
2172
+ const plan = planMachines(sourceFile, disabledConsts);
2173
+ if (plan.edits) {
2174
+ notes.push(...plan.notes);
2175
+ return applyEdits(sourceFile, plan.edits);
2176
+ }
2177
+ }
2178
+ }
2179
+ function planMachines(sourceFile, disabledConsts) {
2180
+ const notes = [];
2181
+ const xs = getXstateImports(sourceFile);
2182
+ const edits = [];
2183
+ const assignDefs = new Set();
2184
+ const constRefs = new Map();
2185
+ const inlined = new Map();
2186
+ const attempt = (property, value, lower) => {
2187
+ try {
2188
+ const lowered = lower();
2189
+ edits.push(render(sourceFile, property, value, lowered));
2190
+ for (const def of lowered.inlined) {
2191
+ inlined.set(def.property, implName(def));
2192
+ }
2193
+ for (const [decl, ids] of lowered.constRefs) {
2194
+ const set = constRefs.get(decl) ?? new Set();
2195
+ ids.forEach(id => set.add(id));
2196
+ constRefs.set(decl, set);
2197
+ }
2198
+ for (const note of lowered.notes) {
2199
+ notes.push(`${where(sourceFile, value)} — ${note}`);
2200
+ }
2201
+ } catch (error) {
2202
+ if (!(error instanceof Skip)) throw error;
2203
+ notes.push(`${where(sourceFile, value)} — not lowered: ${error.message}`);
2204
+ }
2205
+ };
2206
+ const provided = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression).filter(call => {
2207
+ const callee = call.getExpression();
2208
+ return Node.isIdentifier(callee) && callee.getText() === 'provide' || Node.isPropertyAccessExpression(callee) && callee.getName() === 'provide';
2209
+ }).map(call => call.getArguments().map(a => a.getText()).join(','));
2210
+ const mentions = (text, name) => new RegExp(`(^|[^\\w$])${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}($|[^\\w$])`).test(text);
2211
+ for (const config of findMachineConfigObjects(sourceFile)) {
2212
+ if (!isXstateMachineCall(config.getParentOrThrow())) {
2213
+ notes.push(`${where(sourceFile, config)} — not lowered: the machine call does not resolve to an 'xstate' import`);
2214
+ continue;
2215
+ }
2216
+ const impls = collectImplementations(config, xs);
2217
+ const rewrites = new Map();
2218
+ for (const [name, def] of impls.actions) {
2219
+ if (def.kind !== 'assign') continue;
2220
+ if (provided.some(text => mentions(text, name))) {
2221
+ // `.provide({ actions: { name } })` elsewhere would still pass a v5
2222
+ // assign where call sites expect a patch function.
2223
+ def.kind = 'other';
2224
+ notes.push(`${where(sourceFile, def.property)} — not lowered: '${name}' is overridden through provide(...)`);
2225
+ continue;
2226
+ }
2227
+ const text = lowerAssignDefinition(def);
2228
+ if (text === undefined) def.kind = 'other';else rewrites.set(def, text);
2229
+ }
2230
+ const ctx = {
2231
+ xs,
2232
+ impls,
2233
+ assignDefs,
2234
+ disabledConsts
2235
+ };
2236
+
2237
+ /** Literal behind an in-file const reference, when safe to edit in place. */
2238
+ const resolve = value => {
2239
+ if (!isIndirect(value)) return value;
2240
+ const target = derefConfig(value);
2241
+ if (!target) return value;
2242
+ if (!onlyConfigReferences(target.decl)) return undefined;
2243
+ return target.node;
2244
+ };
2245
+ const transition = (property, value) => {
2246
+ if (!value) return;
2247
+ if (isIndirect(value)) {
2248
+ const target = resolve(value);
2249
+ if (!target || target === value || isFunctionLike(target)) return;
2250
+ // Lowered in place inside the const: replace the literal only.
2251
+ return transition(target, target);
2252
+ }
2253
+ const v = unwrap(value);
2254
+ const candidates = Node.isArrayLiteralExpression(v) ? v.getElements().map(unwrap) : [v];
2255
+ if (!candidates.some(hasLowerableKeys)) {
2256
+ if (Node.isArrayLiteralExpression(v)) collapseArray(v, candidates);
2257
+ return;
2258
+ }
2259
+ attempt(property, value, () => lowerTransition(ctx, v));
2260
+ };
2261
+
2262
+ // v5 takes the first enabled candidate; with no guards that is always the
2263
+ // first one, so the rest are unreachable.
2264
+ const collapseArray = (array, candidates) => {
2265
+ const [first] = candidates;
2266
+ if (!first || !candidates.every(c => Node.isObjectLiteralExpression(c) || Node.isStringLiteral(c) || Node.isNoSubstitutionTemplateLiteral(c))) return;
2267
+ edits.push({
2268
+ start: array.getStart(),
2269
+ end: array.getEnd(),
2270
+ text: first.getText()
2271
+ });
2272
+ if (candidates.length > 1) {
2273
+ notes.push(`${where(sourceFile, array)} — collapsed an unguarded transition array to its first candidate; v5 never reaches the other ${candidates.length - 1}`);
2274
+ }
2275
+ };
2276
+ const transitionMap = map => {
2277
+ const value = map && resolve(map);
2278
+ if (!value || !Node.isObjectLiteralExpression(value)) return;
2279
+ for (const property of value.getProperties()) {
2280
+ if (Node.isPropertyAssignment(property)) {
2281
+ transition(property, property.getInitializer());
2282
+ } else if (Node.isSpreadAssignment(property)) {
2283
+ transitionMap(property);
2284
+ }
2285
+ }
2286
+ };
2287
+ const visitState = node => {
2288
+ const state = node && resolve(node);
2289
+ if (!state || !Node.isObjectLiteralExpression(state)) return;
2290
+ for (const property of state.getProperties()) {
2291
+ if (Node.isSpreadAssignment(property)) {
2292
+ visitState(property);
2293
+ continue;
2294
+ }
2295
+ const key = getKeyText$2(property);
2296
+ if (!key || !Node.isPropertyAssignment(property)) continue;
2297
+ const value = property.getInitializer();
2298
+ if (!value) continue;
2299
+ switch (key) {
2300
+ case 'entry':
2301
+ case 'exit':
2302
+ {
2303
+ const target = resolve(value);
2304
+ if (target && !isFunctionLike(unwrap(target))) {
2305
+ const at = target === value ? property : target;
2306
+ attempt(at, target, () => lowerEntryExit(ctx, target));
2307
+ }
2308
+ break;
2309
+ }
2310
+ case 'on':
2311
+ case 'after':
2312
+ transitionMap(value);
2313
+ break;
2314
+ case 'always':
2315
+ case 'onDone':
2316
+ case 'onError':
2317
+ case 'onSnapshot':
2318
+ transition(property, value);
2319
+ break;
2320
+ case 'invoke':
2321
+ {
2322
+ const list = resolve(value);
2323
+ const invokes = !list ? [] : Node.isArrayLiteralExpression(list) ? list.getElements().map(resolve) : [list];
2324
+ for (const invoke of invokes) {
2325
+ if (!invoke || !Node.isObjectLiteralExpression(invoke)) continue;
2326
+ for (const p of invoke.getProperties()) {
2327
+ const k = getKeyText$2(p);
2328
+ if (Node.isPropertyAssignment(p) && (k === 'onDone' || k === 'onError' || k === 'onSnapshot')) {
2329
+ transition(p, p.getInitializer());
2330
+ }
2331
+ }
2332
+ }
2333
+ break;
2334
+ }
2335
+ case 'states':
2336
+ visitStates(value);
2337
+ break;
2338
+ }
2339
+ }
2340
+ };
2341
+ const visitStates = node => {
2342
+ const states = resolve(node);
2343
+ if (!states || !Node.isObjectLiteralExpression(states)) return;
2344
+ for (const child of states.getProperties()) {
2345
+ if (Node.isPropertyAssignment(child)) {
2346
+ visitState(child.getInitializer());
2347
+ } else if (Node.isShorthandPropertyAssignment(child)) {
2348
+ visitState(child);
2349
+ } else if (Node.isSpreadAssignment(child)) {
2350
+ visitStates(child);
2351
+ }
2352
+ }
2353
+ };
2354
+ visitState(config);
2355
+ for (const [def, text] of rewrites) {
2356
+ if (def.constDecl && Node.isVariableDeclaration(def.constDecl)) {
2357
+ const init = def.constDecl.getInitializer();
2358
+ edits.push({
2359
+ start: init.getStart(),
2360
+ end: init.getEnd(),
2361
+ text
2362
+ });
2363
+ continue;
2364
+ }
2365
+ if (!Node.isPropertyAssignment(def.property)) continue;
2366
+ const init = def.property.getInitializer();
2367
+ edits.push({
2368
+ start: init.getStart(),
2369
+ end: init.getEnd(),
2370
+ text
2371
+ });
2372
+ }
2373
+ }
2374
+ let retry = false;
2375
+ for (const [decl, ids] of constRefs) {
2376
+ const declaration = decl;
2377
+ const nameNode = declaration.getNameNode();
2378
+ const symbol = nameNode.getSymbol();
2379
+ const unlowered = sourceFile.getDescendantsOfKind(SyntaxKind.Identifier).some(id => id !== nameNode && id.getText() === nameNode.getText() && id.getSymbol() === symbol && !ids.has(id));
2380
+ const call = unwrap(declaration.getInitializer());
2381
+ const text = unlowered ? undefined : xstateCallee(call, xs) === 'assign' ? call.getArguments().length !== 1 ? undefined : lowerAssignDefinition({
2382
+ kind: 'assign',
2383
+ property: decl,
2384
+ assignArg: call.getArguments()[0]
2385
+ }) : lowerConstCreator({
2386
+ xs,
2387
+ impls: {
2388
+ actions: new Map(),
2389
+ guards: new Map()
2390
+ },
2391
+ assignDefs,
2392
+ disabledConsts
2393
+ }, call);
2394
+ if (text === undefined) {
2395
+ disabledConsts.add(decl);
2396
+ retry = true;
2397
+ continue;
2398
+ }
2399
+ const init = declaration.getInitializer();
2400
+ edits.push({
2401
+ start: init.getStart(),
2402
+ end: init.getEnd(),
2403
+ text
2404
+ });
2405
+ notes.push(`${where(sourceFile, decl)} — rewrote '${nameNode.getText()}' as a function its call sites invoke`);
2406
+ }
2407
+ if (retry) return {
2408
+ notes
2409
+ };
2410
+
2411
+ // Definitions only referenced from other removed definitions go too.
2412
+ const removed = [];
2413
+ for (let grew = true; grew;) {
2414
+ grew = false;
2415
+ const ranges = [...edits, ...removed.map(p => ({
2416
+ start: p.getStart(),
2417
+ end: p.getEnd()
2418
+ }))];
2419
+ for (const [property, name] of inlined) {
2420
+ if (removed.includes(property)) continue;
2421
+ if (!hasOtherReferences(sourceFile, name, property, ranges)) {
2422
+ removed.push(property);
2423
+ grew = true;
2424
+ }
2425
+ }
2426
+ }
2427
+ for (const [property, name] of inlined) {
2428
+ if (!removed.includes(property)) {
2429
+ notes.push(`${where(sourceFile, property)} — kept '${name}': referenced where it was not inlined`);
2430
+ }
2431
+ }
2432
+ edits.push(...removeProperties(removed));
2433
+ return {
2434
+ edits,
2435
+ notes
2436
+ };
2437
+ }
2438
+ function implName(def) {
2439
+ const name = def.property.getNameNode();
2440
+ return Node.isComputedPropertyName(name) ? resolveStringConstant(name.getExpression()) ?? '' : getKeyText$2(def.property) ?? '';
2441
+ }
2442
+
2443
+ /** Whether `name` still appears outside `def` and the replaced ranges. */
2444
+ function hasOtherReferences(sourceFile, name, def, edits) {
2445
+ const replaced = node => node.getStart() >= def.getStart() && node.getEnd() <= def.getEnd() || edits.some(e => node.getStart() >= e.start && node.getEnd() <= e.end);
2446
+ return sourceFile.forEachDescendant(node => {
2447
+ const matches = (Node.isStringLiteral(node) || Node.isNoSubstitutionTemplateLiteral(node)) && node.getLiteralText() === name || Node.isIdentifier(node) && node.getText() === name || Node.isPropertyAccessExpression(node) && resolveStringConstant(node) === name;
2448
+ return matches && !replaced(node) ? true : undefined;
2449
+ }) ?? false;
2450
+ }
2451
+
2452
+ /** Deletes object literal properties with their separating commas. */
2453
+ function removeProperties(properties) {
2454
+ const edits = [];
2455
+ const objects = new Set(properties.map(p => p.getParentOrThrow()));
2456
+ for (const object of objects) {
2457
+ if (!Node.isObjectLiteralExpression(object)) continue;
2458
+ const props = object.getProperties();
2459
+ const gone = props.map(p => properties.includes(p));
2460
+ if (gone.every(Boolean)) {
2461
+ edits.push({
2462
+ start: object.getStart() + 1,
2463
+ end: object.getEnd() - 1,
2464
+ text: ''
2465
+ });
2466
+ continue;
2467
+ }
2468
+ const lastKept = gone.lastIndexOf(false);
2469
+ props.forEach((p, i) => {
2470
+ if (gone[i] && i < lastKept) {
2471
+ edits.push({
2472
+ start: p.getFullStart(),
2473
+ end: props[i + 1].getFullStart(),
2474
+ text: ''
2475
+ });
2476
+ }
2477
+ });
2478
+ if (lastKept < props.length - 1) {
2479
+ edits.push({
2480
+ start: props[lastKept].getEnd(),
2481
+ end: props.at(-1).getEnd(),
2482
+ text: ''
2483
+ });
2484
+ }
2485
+ }
2486
+ return edits;
2487
+ }
2488
+
2489
+ // ---- F: fromPromise → createAsyncLogic ---------------------------------------
2490
+
2491
+ function promiseFunction(arg) {
2492
+ const value = unwrap(arg);
2493
+ if (isFunctionLike(value)) return value;
2494
+ if (Node.isIdentifier(value)) {
2495
+ const init = resolveConstInitializer(value);
2496
+ if (init && isFunctionLike(init)) return init;
2497
+ const decl = value.getSymbol()?.getDeclarations().find(Node.isFunctionDeclaration);
2498
+ return decl;
2499
+ }
2500
+ return undefined;
2501
+ }
2502
+ function usesSystemOrEmit(fn) {
2503
+ if (!Node.isArrowFunction(fn) && !Node.isFunctionExpression(fn) && !Node.isFunctionDeclaration(fn)) return true;
2504
+ const [param, ...rest] = fn.getParameters();
2505
+ if (rest.length) return true;
2506
+ if (!param) return false;
2507
+ const name = param.getNameNode();
2508
+ if (Node.isObjectBindingPattern(name)) {
2509
+ return name.getElements().some(element => {
2510
+ const key = (element.getPropertyNameNode() ?? element.getNameNode()).getText();
2511
+ return !!element.getDotDotDotToken() || key === 'system' || key === 'emit';
2512
+ });
2513
+ }
2514
+ return /\b(system|emit)\b/.test(fn.getText());
2515
+ }
2516
+ function lowerFromPromise(sourceFile, notes) {
2517
+ const find = () => {
2518
+ const xs = getXstateImports(sourceFile);
2519
+ return sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression).filter(call => {
2520
+ if (xstateCallee(call, xs) !== 'fromPromise') return false;
2521
+ const args = call.getArguments();
2522
+ const fn = args.length === 1 && promiseFunction(args[0]);
2523
+ if (!fn || usesSystemOrEmit(fn)) {
2524
+ notes.push(`${where(sourceFile, call)} — not lowered: fromPromise logic reads \`system\` or \`emit\`, or is not a local function`);
2525
+ return false;
2526
+ }
2527
+ return true;
2528
+ });
2529
+ };
2530
+ if (!find().length) return false;
2531
+ const local = ensureNamedImport(sourceFile, 'createAsyncLogic');
2532
+ if (!local) return false;
2533
+ const calls = find().reverse();
2534
+ for (const call of calls) {
2535
+ call.replaceWithText(`${local}({ run: ${call.getArguments()[0].getText()} })`);
2536
+ }
2537
+ return true;
2538
+ }
2539
+
2540
+ // ---- G: imports -----------------------------------------------------------------
2541
+
2542
+ function cleanupImports(sourceFile) {
2543
+ const references = new Map();
2544
+ for (const id of sourceFile.getDescendantsOfKind(SyntaxKind.Identifier)) {
2545
+ if (id.getFirstAncestorByKind(SyntaxKind.ImportDeclaration)) continue;
2546
+ const parent = id.getParent();
2547
+ if ((Node.isPropertyAssignment(parent) || Node.isPropertyAccessExpression(parent) || Node.isMethodDeclaration(parent) || Node.isPropertySignature(parent)) && parent.getNameNode() === id) continue;
2548
+ references.set(id.getText(), (references.get(id.getText()) ?? 0) + 1);
2549
+ }
2550
+ for (const imp of sourceFile.getImportDeclarations()) {
2551
+ if (imp.getModuleSpecifierValue() !== 'xstate') continue;
2552
+ for (const spec of imp.getNamedImports()) {
2553
+ const local = (spec.getAliasNode() ?? spec.getNameNode()).getText();
2554
+ if (REMOVABLE_IMPORTS.has(spec.getName()) && !references.get(local)) {
2555
+ spec.remove();
2556
+ }
2557
+ }
2558
+ if (!imp.getNamedImports().length && !imp.getDefaultImport() && !imp.getNamespaceImport()) {
2559
+ imp.remove();
2560
+ }
2561
+ }
2562
+ }
2563
+
2564
+ /**
2565
+ * Lowers v5 action creators, guards and transition objects to v6 transition
2566
+ * and entry/exit functions. Anything it cannot lower is left untouched for the
2567
+ * report transforms.
2568
+ */
2569
+ const lowerActions = {
2570
+ name: 'lower-actions',
2571
+ description: 'Rewrite v5 transition `actions`/`guard`, `entry`/`exit` lists, action creators, guard combinators, named `assign` actions, the second `createMachine` argument and `fromPromise` as v6 functions.',
2572
+ apply(sourceFile) {
2573
+ const before = sourceFile.getFullText();
2574
+ const notes = [];
2575
+ let changed = moveImplementations(sourceFile, notes);
2576
+ changed = lowerMachines(sourceFile, notes) || changed;
2577
+ changed = lowerFromPromise(sourceFile, notes) || changed;
2578
+ if (sourceFile.getFullText().includes(MATCHES_STATE)) {
2579
+ const local = ensureNamedImport(sourceFile, 'matchesState') ?? 'matchesState';
2580
+ sourceFile.replaceWithText(sourceFile.getFullText().split(MATCHES_STATE).join(local));
2581
+ }
2582
+ if (changed) cleanupImports(sourceFile);
2583
+ return {
2584
+ changed: sourceFile.getFullText() !== before,
2585
+ notes
2586
+ };
2587
+ }
2588
+ };
2589
+
2590
+ /**
2591
+ * v5 `'xstate'` exports that require structural rewrites in v6 and are too
2592
+ * risky to automate. Value is a one-line hint for the v6 replacement.
2593
+ */
2594
+ const REMOVED_APIS = {
2595
+ assign: 'return a `{ context }` patch from the inline transition function. A named `assign` in `setup({ actions })` becomes a plain function returning the patch; call it from the transition and return its result (`enq(actions.x)` ignores return values).',
2596
+ raise: 'use `enq.raise(...)` inside an action',
2597
+ sendTo: 'use `enq.sendTo(...)` inside an action',
2598
+ sendParent: 'use `enq.sendTo(...)` with the parent ref inside an action',
2599
+ forwardTo: 'use `enq.sendTo(target, event)` inside an action',
2600
+ emit: 'use `enq.emit(...)` inside an action',
2601
+ log: 'use `enq.log(...)` inside an action',
2602
+ cancel: 'use `enq.cancel(...)` inside an action',
2603
+ spawnChild: 'use `enq.spawn(...)` inside an action',
2604
+ stop: 'use `enq.stop(...)` inside an action',
2605
+ stopChild: 'use `enq.stop(...)` inside an action',
2606
+ enqueueActions: 'action functions now receive an `enq` enqueue object directly',
2607
+ and: 'inline the guard as `(...) => a(...) && b(...)`',
2608
+ or: 'inline the guard as `(...) => a(...) || b(...)`',
2609
+ not: 'inline the guard as `(...) => !a(...)`',
2610
+ stateIn: 'use `self.getSnapshot().matches(...)`',
2611
+ fromPromise: 'use `createAsyncLogic(...)`',
2612
+ fromTransition: 'use `createStepLogic({ context, run })`',
2613
+ toObserver: 'pass an observer object or callback directly',
2614
+ pathToStateValue: 'build the state value object directly',
2615
+ SpecialTargets: 'use `enq.sendTo(self, event)` for internal sends; parent sends use `enq.sendTo(...)` with the parent ref'
2616
+ };
2617
+
2618
+ /**
2619
+ * Detects (does NOT transform) usages of v5 `'xstate'` APIs that were removed
2620
+ * or restructured in v6. Reports file:line + API + a replacement hint via the
2621
+ * returned notes.
2622
+ */
2623
+ const reportRemovedApis = {
2624
+ name: 'report-removed-apis',
2625
+ description: 'Detect (report only) usages of removed/restructured v5 xstate APIs (assign, raise, sendTo, emit, fromPromise, and/or/not, toObserver, SpecialTargets, etc.) that need manual migration.',
2626
+ apply(sourceFile) {
2627
+ const notes = [];
2628
+ const sites = [];
2629
+ const xstateImports = sourceFile.getImportDeclarations().filter(imp => imp.getModuleSpecifierValue() === 'xstate');
2630
+
2631
+ // Map local binding name → canonical API name (respecting aliases).
2632
+ const tracked = new Map();
2633
+ for (const imp of xstateImports) {
2634
+ for (const spec of imp.getNamedImports()) {
2635
+ const original = spec.getNameNode().getText();
2636
+ if (REMOVED_APIS[original]) {
2637
+ const local = (spec.getAliasNode() ?? spec.getNameNode()).getText();
2638
+ tracked.set(local, original);
2639
+ }
2640
+ }
2641
+ }
2642
+ if (tracked.size === 0) {
2643
+ return {
2644
+ changed: false,
2645
+ notes,
2646
+ sites
2647
+ };
2648
+ }
2649
+ const filePath = sourceFile.getFilePath();
2650
+ for (const ident of sourceFile.getDescendantsOfKind(SyntaxKind.Identifier)) {
2651
+ const local = ident.getText();
2652
+ const api = tracked.get(local);
2653
+ if (!api) {
2654
+ continue;
2655
+ }
2656
+ // Skip member names such as `enq.emit`; they are not the import.
2657
+ const parent = ident.getParent();
2658
+ if ((Node.isPropertyAccessExpression(parent) || Node.isPropertyAssignment(parent)) && parent.getNameNode() === ident) {
2659
+ continue;
2660
+ }
2661
+ // Skip the import specifier declarations themselves.
2662
+ const parentKind = parent?.getKind();
2663
+ if (parentKind === SyntaxKind.ImportSpecifier || parentKind === SyntaxKind.ImportClause) {
2664
+ continue;
2665
+ }
2666
+ const {
2667
+ line,
2668
+ column
2669
+ } = sourceFile.getLineAndColumnAtPos(ident.getPos());
2670
+ const hint = REMOVED_APIS[api];
2671
+ notes.push(`${filePath}:${line}:${column} — '${api}' → ${hint}`);
2672
+ sites.push({
2673
+ file: filePath,
2674
+ line,
2675
+ column,
2676
+ api,
2677
+ hint
2678
+ });
2679
+ }
2680
+
2681
+ // Never mutates the file.
2682
+ return {
2683
+ changed: false,
2684
+ notes,
2685
+ sites
2686
+ };
2687
+ }
2688
+ };
2689
+
2690
+ /** Keys whose string values (or string array elements) are state targets. */
2691
+ const TARGET_KEYS = new Set(['target', 'initial', 'on', 'after', 'always', 'onDone', 'onError']);
2692
+
2693
+ /** Calls whose first argument is a state value or `#id` reference. */
2694
+ const MATCH_CALLS = new Set(['matches', 'stateIn']);
2695
+ function getPropertyName(node) {
2696
+ if (Node.isPropertyAssignment(node)) {
2697
+ const name = node.getNameNode();
2698
+ if (Node.isIdentifier(name) || Node.isStringLiteral(name)) {
2699
+ return Node.isStringLiteral(name) ? name.getLiteralText() : name.getText();
2700
+ }
2701
+ }
2702
+ return undefined;
2703
+ }
2704
+ function getStringProperty(object, name) {
2705
+ const property = object.getProperty(name);
2706
+ if (!property || !Node.isPropertyAssignment(property)) return undefined;
2707
+ const initializer = property.getInitializer();
2708
+ return initializer && (Node.isStringLiteral(initializer) || Node.isNoSubstitutionTemplateLiteral(initializer)) ? initializer.getLiteralText() : undefined;
2709
+ }
2710
+ const ID_PATH_HINT = "'#…' is an exact state id in v6 and is never split into an id and a path; use the target state's own id (its explicit `id` or its full implicit id)";
2711
+ /**
2712
+ * Collects the state ids that one machine config literal declares: explicit
2713
+ * state `id`s and, for `createMachine` configs, implicit ids (the path from
2714
+ * the root, prefixed by the machine id when one is set). Invoke and actor ids
2715
+ * are not state ids and are not collected.
2716
+ */
2717
+ function collectStateIds(config, withImplicitIds) {
2718
+ const ids = new Set();
2719
+ function walk(stateConfig, implicitId) {
2720
+ const explicitId = getStringProperty(stateConfig, 'id');
2721
+ if (explicitId !== undefined) ids.add(explicitId);else if (withImplicitIds) ids.add(implicitId);
2722
+ const states = stateConfig.getProperty('states');
2723
+ if (!states || !Node.isPropertyAssignment(states)) return;
2724
+ const statesObject = states.getInitializer();
2725
+ if (!statesObject || !Node.isObjectLiteralExpression(statesObject)) return;
2726
+ for (const state of statesObject.getProperties()) {
2727
+ const key = getPropertyName(state);
2728
+ if (key === undefined || !Node.isPropertyAssignment(state)) continue;
2729
+ const child = state.getInitializer();
2730
+ if (!child || !Node.isObjectLiteralExpression(child)) continue;
2731
+ walk(child, implicitId ? `${implicitId}.${key}` : key);
2732
+ }
2733
+ }
2734
+ walk(config, getStringProperty(config, 'id') ?? '');
2735
+ return ids;
2736
+ }
2737
+
2738
+ /**
2739
+ * Finds the `createMachine(...)` or `createStateConfig(...)` config literal
2740
+ * that contains `node`, if any.
2741
+ */
2742
+ function findEnclosingConfig(node, machineConfigs, stateConfigs) {
2743
+ for (const ancestor of node.getAncestors()) {
2744
+ if (!Node.isObjectLiteralExpression(ancestor)) continue;
2745
+ if (machineConfigs.has(ancestor)) {
2746
+ return {
2747
+ config: ancestor,
2748
+ withImplicitIds: true
2749
+ };
2750
+ }
2751
+ if (stateConfigs.has(ancestor)) {
2752
+ return {
2753
+ config: ancestor,
2754
+ withImplicitIds: false
2755
+ };
2756
+ }
2757
+ }
2758
+ return undefined;
2759
+ }
2760
+ function isMatchArgument(literal) {
2761
+ const call = literal.getParent();
2762
+ if (!call || !Node.isCallExpression(call)) return false;
2763
+ if (call.getArguments()[0] !== literal) return false;
2764
+ const expression = call.getExpression();
2765
+ const name = Node.isIdentifier(expression) ? expression.getText() : Node.isPropertyAccessExpression(expression) ? expression.getName() : undefined;
2766
+ return !!name && MATCH_CALLS.has(name);
2767
+ }
2768
+ function isTargetValue(literal) {
2769
+ let node = literal;
2770
+ let parent = node.getParent();
2771
+ if (parent && Node.isArrayLiteralExpression(parent)) {
2772
+ node = parent;
2773
+ parent = node.getParent();
2774
+ }
2775
+ if (!parent || !Node.isPropertyAssignment(parent)) return false;
2776
+ const name = getPropertyName(parent);
2777
+ if (name && TARGET_KEYS.has(name)) return true;
2778
+ // A bare string transition inside `on: { EVENT: '#a.b' }` or
2779
+ // `after: { 1000: '#a.b' }`.
2780
+ const container = parent.getParent();
2781
+ const owner = container?.getParent();
2782
+ const ownerName = owner ? getPropertyName(owner) : undefined;
2783
+ return ownerName === 'on' || ownerName === 'after';
2784
+ }
2785
+
2786
+ /**
2787
+ * Reports (does NOT transform) `#a.b` targets and `matches('#a.b')`/
2788
+ * `stateIn('#a.b')` references that the codemod cannot verify as whole state
2789
+ * ids. In v6, the text after `#` is an exact id and is never split into an
2790
+ * id followed by a path.
2791
+ */
2792
+ const reportIdPathTargets = {
2793
+ name: 'report-id-path-targets',
2794
+ description: "Detect (report only) '#id.path' targets and matches('#id.path') references that may combine an id with a relative path, which v6 no longer resolves.",
2795
+ apply(sourceFile) {
2796
+ const notes = [];
2797
+ const sites = [];
2798
+ const filePath = sourceFile.getFilePath();
2799
+ const machineConfigs = new Set(findMachineConfigObjects(sourceFile, ['createMachine']));
2800
+ const stateConfigs = new Set(findMachineConfigObjects(sourceFile, ['createStateConfig']));
2801
+ const idsByConfig = new Map();
2802
+ const literals = [...sourceFile.getDescendantsOfKind(SyntaxKind.StringLiteral), ...sourceFile.getDescendantsOfKind(SyntaxKind.NoSubstitutionTemplateLiteral)].sort((a, b) => a.getStart() - b.getStart());
635
2803
  for (const literal of literals) {
636
2804
  const text = literal.getLiteralText();
637
2805
  if (!text.startsWith('#') || !text.includes('.')) continue;
@@ -651,23 +2819,458 @@ const reportIdPathTargets = {
651
2819
  line,
652
2820
  column
653
2821
  } = sourceFile.getLineAndColumnAtPos(literal.getStart());
654
- notes.push(`${filePath}:${line}:${column} — '${text}' → '#…' is an exact state id in v6 and is never split into an id and a path; use the target state's own id (its explicit \`id\` or its full implicit id)`);
2822
+ notes.push(`${filePath}:${line}:${column} — '${text}' → ${ID_PATH_HINT}`);
2823
+ sites.push({
2824
+ file: filePath,
2825
+ line,
2826
+ column,
2827
+ api: text,
2828
+ hint: ID_PATH_HINT
2829
+ });
2830
+ }
2831
+
2832
+ // Never mutates the file.
2833
+ return {
2834
+ changed: false,
2835
+ notes,
2836
+ sites
2837
+ };
2838
+ }
2839
+ };
2840
+
2841
+ const SUFFIXED_LIFECYCLE = /^xstate\.(done\.state|done\.actor|error\.actor|snapshot|after)\.(.+)$/;
2842
+ // v6 category type and the payload field that carries the v5 suffix.
2843
+ const REWRITES = {
2844
+ 'done.actor': {
2845
+ type: 'xstate.done.actor',
2846
+ field: 'actorId'
2847
+ },
2848
+ 'error.actor': {
2849
+ type: 'xstate.error.actor',
2850
+ field: 'actorId'
2851
+ },
2852
+ snapshot: {
2853
+ type: 'xstate.snapshot.actor',
2854
+ field: 'actorId'
2855
+ },
2856
+ 'done.state': {
2857
+ type: 'xstate.done.state',
2858
+ field: 'stateId'
2859
+ }
2860
+ };
2861
+ const AFTER_HINT = 'v6 raises `xstate.after` with `delay` and `stateId`; suffixed keys never match. Use `after: { <delay>: ... }` on the state.';
2862
+ function keyHint(category, suffix, reason) {
2863
+ const rewrite = REWRITES[category];
2864
+ if (!rewrite) return AFTER_HINT;
2865
+ return `${reason} Key on '${rewrite.type}' and select the payload with \`matches: { ${rewrite.field}: '${suffix}' }\`.`;
2866
+ }
2867
+ const LITERAL_HINT = 'v6 lifecycle event types carry the id in a payload field (`actorId`, `stateId`, `delay`), so this string never equals `event.type`. Compare the category type and the payload field instead.';
2868
+
2869
+ /** Parses a suffixed lifecycle type; wildcards and `xstate.snapshot.actor` are v6-valid. */
2870
+ function parseSuffixed(text) {
2871
+ const match = SUFFIXED_LIFECYCLE.exec(text);
2872
+ if (!match) return undefined;
2873
+ const [, category, suffix] = match;
2874
+ if (suffix === '*' || suffix.endsWith('.*')) return undefined;
2875
+ if (category === 'snapshot' && suffix === 'actor') return undefined;
2876
+ return {
2877
+ category,
2878
+ suffix
2879
+ };
2880
+ }
2881
+ function getKeyText$1(node) {
2882
+ if (!Node.isPropertyAssignment(node)) return undefined;
2883
+ const name = node.getNameNode();
2884
+ if (Node.isStringLiteral(name)) return name.getLiteralText();
2885
+ if (Node.isIdentifier(name)) return name.getText();
2886
+ return undefined;
2887
+ }
2888
+
2889
+ /** v5 `done.state` suffixes are state ids, which used the `(machine)` placeholder. */
2890
+ function v6StateId(suffix) {
2891
+ return suffix.startsWith('(machine).') ? suffix.slice('(machine).'.length) : suffix;
2892
+ }
2893
+
2894
+ /**
2895
+ * Builds the v6 transition for a suffixed key, or explains why it cannot.
2896
+ * Object literals get `matches` prepended; functions move under `to`.
2897
+ */
2898
+ function rewriteInitializer(entry, field, value) {
2899
+ const init = entry.getInitializerOrThrow();
2900
+ const matches = `matches: { ${field}: ${JSON.stringify(value).replace(/"/g, "'")} }`;
2901
+ if (Node.isObjectLiteralExpression(init)) {
2902
+ if (init.getProperty('matches')) return '';
2903
+ const props = init.getProperties().map(p => p.getText());
2904
+ return `{ ${[matches, ...props].join(', ')} }`;
2905
+ }
2906
+ if (Node.isArrowFunction(init) || Node.isFunctionExpression(init)) {
2907
+ return `{ ${matches}, to: ${init.getText()} }`;
2908
+ }
2909
+ return '';
2910
+ }
2911
+
2912
+ /**
2913
+ * Rewrites `on` keys that use v5 suffixed lifecycle event types, such as
2914
+ * `'xstate.done.actor.job'`, to the v6 category type with `matches`.
2915
+ * Reports what it cannot rewrite: `xstate.after.<delay>.<id>` keys, keys
2916
+ * whose category type is already present or used twice in the same `on`,
2917
+ * and suffixed type strings outside `on` keys (for example
2918
+ * `event.type === 'xstate.done.actor.job'`).
2919
+ */
2920
+ const suffixedLifecycleEvents = {
2921
+ name: 'suffixed-lifecycle-events',
2922
+ description: "Rewrite `on` keys such as 'xstate.done.actor.<id>' to the v6 category type with `matches`; report suffixed types it cannot rewrite.",
2923
+ apply(sourceFile) {
2924
+ const notes = [];
2925
+ const sites = [];
2926
+ const filePath = sourceFile.getFilePath();
2927
+ const handled = new Set();
2928
+ let changed = false;
2929
+ const report = (node, api, hint) => {
2930
+ const {
2931
+ line,
2932
+ column
2933
+ } = sourceFile.getLineAndColumnAtPos(node.getStart());
2934
+ notes.push(`${filePath}:${line}:${column} — '${api}' → ${hint}`);
2935
+ sites.push({
2936
+ file: filePath,
2937
+ line,
2938
+ column,
2939
+ api,
2940
+ hint
2941
+ });
2942
+ };
2943
+ const onObjects = [];
2944
+ for (const config of findMachineConfigObjects(sourceFile)) {
2945
+ for (const property of config.getDescendantsOfKind(SyntaxKind.PropertyAssignment)) {
2946
+ if (getKeyText$1(property) !== 'on') continue;
2947
+ const on = property.getInitializer();
2948
+ if (on && Node.isObjectLiteralExpression(on) && !onObjects.includes(on)) {
2949
+ onObjects.push(on);
2950
+ }
2951
+ }
2952
+ }
2953
+ for (const on of onObjects) {
2954
+ const entries = on.getProperties().flatMap(entry => {
2955
+ const key = getKeyText$1(entry);
2956
+ const parsed = key ? parseSuffixed(key) : undefined;
2957
+ return parsed && Node.isPropertyAssignment(entry) ? [{
2958
+ entry,
2959
+ key: key,
2960
+ ...parsed
2961
+ }] : [];
2962
+ });
2963
+ const existingKeys = new Set(on.getProperties().map(getKeyText$1).filter(Boolean));
2964
+ for (const item of entries) {
2965
+ handled.add(item.entry.getNameNode());
2966
+ const rewrite = REWRITES[item.category];
2967
+ if (!rewrite) {
2968
+ report(item.entry, item.key, AFTER_HINT);
2969
+ continue;
2970
+ }
2971
+ const sameCategory = entries.filter(other => REWRITES[other.category]?.type === rewrite.type);
2972
+ if (existingKeys.has(rewrite.type) || sameCategory.length > 1) {
2973
+ report(item.entry, item.key, keyHint(item.category, item.suffix, `\`on\` already handles '${rewrite.type}' or several suffixed keys share it; merge them into one transition by hand.`));
2974
+ continue;
2975
+ }
2976
+ const value = item.category === 'done.state' ? v6StateId(item.suffix) : item.suffix;
2977
+ const replacement = rewriteInitializer(item.entry, rewrite.field, value);
2978
+ if (!replacement) {
2979
+ report(item.entry, item.key, keyHint(item.category, item.suffix, 'The transition is not an object or function literal, or already has `matches`.'));
2980
+ continue;
2981
+ }
2982
+ item.entry.replaceWithText(`'${rewrite.type}': ${replacement}`);
2983
+ changed = true;
2984
+ }
2985
+ }
2986
+ const importsXState = sourceFile.getImportDeclarations().some(d => /^xstate(\/|$)/.test(d.getModuleSpecifierValue()));
2987
+ if (importsXState) {
2988
+ for (const literal of [...sourceFile.getDescendantsOfKind(SyntaxKind.StringLiteral), ...sourceFile.getDescendantsOfKind(SyntaxKind.NoSubstitutionTemplateLiteral)]) {
2989
+ if (literal.wasForgotten() || handled.has(literal)) continue;
2990
+ if (Node.isImportDeclaration(literal.getParent())) continue;
2991
+ const text = literal.getLiteralText();
2992
+ if (parseSuffixed(text)) report(literal, text, LITERAL_HINT);
2993
+ }
2994
+ }
2995
+ return {
2996
+ changed,
2997
+ notes,
2998
+ sites
2999
+ };
3000
+ }
3001
+ };
3002
+
3003
+ const ENTRY_EXIT_HINT = 'v6 `entry`/`exit` is a single function `(args, enq) => { ... }`; call named actions with `enq(actions.name, params)`.';
3004
+ const TRANSITION_ACTIONS_HINT = 'transition `actions` was removed; use an inline transition function `(args, enq) => { ... }` and queue effects on `enq`; call named actions with `enq(actions.name, params)`.';
3005
+ const TRANSITION_GUARD_HINT = 'transition `guard` was removed; return `{ target }` from an inline transition function when the condition holds, or `undefined` to reject the event; named guards are plain predicates called as `guards.name(params)`.';
3006
+ const TRANSITION_ARRAY_HINT = 'v6 takes one transition function; combine the candidates into an if-chain: `(args, enq) => { if (cond) return { target }; return { target: other }; }`.';
3007
+ const INDIRECT_CONFIG_HINT = 'v6 rejects v5 transition objects, `actions`/`guard` and action lists; this value is not an inline literal or an in-file `const`, so the codemod cannot see into it. Inline it or migrate it by hand.';
3008
+ function getKeyText(node) {
3009
+ if (!Node.isPropertyAssignment(node)) return undefined;
3010
+ const name = node.getNameNode();
3011
+ if (Node.isStringLiteral(name)) return name.getLiteralText();
3012
+ if (Node.isIdentifier(name)) return name.getText();
3013
+ return undefined;
3014
+ }
3015
+ function getProperty(object, key) {
3016
+ if (!Node.isObjectLiteralExpression(object)) return undefined;
3017
+ const property = object.getProperties().find(candidate => getKeyText(candidate) === key);
3018
+ return Node.isPropertyAssignment(property) ? property.getInitializer() : undefined;
3019
+ }
3020
+
3021
+ /**
3022
+ * Reports (does NOT transform) v5 action shapes that v6 rejects: non-function
3023
+ * `entry` / `exit` values, transition arrays, and `actions`, `guard` or `cond`
3024
+ * on transition objects.
3025
+ */
3026
+ const reportActionShapes = {
3027
+ name: 'report-action-shapes',
3028
+ description: 'Detect (report only) non-function `entry`/`exit` values, transition arrays and transition `actions`/`guard`/`cond`, which v6 rejects.',
3029
+ apply(sourceFile) {
3030
+ const notes = [];
3031
+ const sites = [];
3032
+ const filePath = sourceFile.getFilePath();
3033
+ const seen = new Set();
3034
+ const report = (node, api, hint) => {
3035
+ if (seen.has(node)) return;
3036
+ seen.add(node);
3037
+ const {
3038
+ line,
3039
+ column
3040
+ } = sourceFile.getLineAndColumnAtPos(node.getStart());
3041
+ notes.push(`${filePath}:${line}:${column} — '${api}' → ${hint}`);
3042
+ sites.push({
3043
+ file: filePath,
3044
+ line,
3045
+ column,
3046
+ api,
3047
+ hint
3048
+ });
3049
+ };
3050
+
3051
+ /** The literal behind a reference, or undefined (reported) when unknown. */
3052
+ const look = node => {
3053
+ if (!node || !isIndirect(node)) return node;
3054
+ const target = derefConfig(node);
3055
+ if (!target) {
3056
+ report(node, 'indirect-config', INDIRECT_CONFIG_HINT);
3057
+ return undefined;
3058
+ }
3059
+ return target.node;
3060
+ };
3061
+ const visitTransition = node => {
3062
+ const value = look(node);
3063
+ if (!value) return;
3064
+ if (Node.isArrayLiteralExpression(value)) {
3065
+ report(value, 'transition-array', TRANSITION_ARRAY_HINT);
3066
+ value.getElements().forEach(visitTransition);
3067
+ return;
3068
+ }
3069
+ if (!Node.isObjectLiteralExpression(value)) return;
3070
+ for (const property of value.getProperties()) {
3071
+ const key = getKeyText(property);
3072
+ if (key === 'actions') {
3073
+ report(property, 'actions', TRANSITION_ACTIONS_HINT);
3074
+ } else if (key === 'guard' || key === 'cond') {
3075
+ report(property, key, TRANSITION_GUARD_HINT);
3076
+ }
3077
+ }
3078
+ };
3079
+ const visitTransitionMap = node => {
3080
+ const value = look(node);
3081
+ if (!value || !Node.isObjectLiteralExpression(value)) return;
3082
+ for (const property of value.getProperties()) {
3083
+ if (Node.isPropertyAssignment(property)) {
3084
+ visitTransition(property.getInitializer());
3085
+ } else if (Node.isSpreadAssignment(property)) {
3086
+ visitTransitionMap(property);
3087
+ }
3088
+ }
3089
+ };
3090
+ const visitStates = node => {
3091
+ const value = look(node);
3092
+ if (!value || !Node.isObjectLiteralExpression(value)) return;
3093
+ for (const child of value.getProperties()) {
3094
+ if (Node.isPropertyAssignment(child)) {
3095
+ visitState(child.getInitializer());
3096
+ } else if (Node.isShorthandPropertyAssignment(child)) {
3097
+ visitState(child);
3098
+ } else if (Node.isSpreadAssignment(child)) {
3099
+ visitStates(child);
3100
+ }
3101
+ }
3102
+ };
3103
+ const visitState = node => {
3104
+ const state = look(node);
3105
+ if (!state || !Node.isObjectLiteralExpression(state)) return;
3106
+ for (const property of state.getProperties()) {
3107
+ if (Node.isSpreadAssignment(property)) {
3108
+ visitState(property);
3109
+ continue;
3110
+ }
3111
+ const key = getKeyText(property);
3112
+ if (!key || !Node.isPropertyAssignment(property)) continue;
3113
+ const raw = property.getInitializer();
3114
+ const value = raw && ['entry', 'exit', 'invoke'].includes(key) ? look(raw) : raw;
3115
+ switch (key) {
3116
+ case 'entry':
3117
+ case 'exit':
3118
+ if (value && !Node.isArrowFunction(value) && !Node.isFunctionExpression(value) && !Node.isFunctionDeclaration(value)) {
3119
+ report(property, key, ENTRY_EXIT_HINT);
3120
+ }
3121
+ break;
3122
+ case 'on':
3123
+ case 'after':
3124
+ visitTransitionMap(value);
3125
+ break;
3126
+ case 'always':
3127
+ case 'onDone':
3128
+ case 'onError':
3129
+ case 'onSnapshot':
3130
+ visitTransition(value);
3131
+ break;
3132
+ case 'initial':
3133
+ if (value && Node.isObjectLiteralExpression(value)) {
3134
+ visitTransition(value);
3135
+ }
3136
+ break;
3137
+ case 'invoke':
3138
+ {
3139
+ const invokes = value && Node.isArrayLiteralExpression(value) ? value.getElements() : [value];
3140
+ for (const element of invokes) {
3141
+ const invoke = look(element);
3142
+ for (const event of ['onDone', 'onError', 'onSnapshot']) {
3143
+ visitTransition(invoke && getProperty(invoke, event));
3144
+ }
3145
+ }
3146
+ break;
3147
+ }
3148
+ case 'states':
3149
+ visitStates(value);
3150
+ break;
3151
+ }
3152
+ }
3153
+ };
3154
+ for (const config of findMachineConfigObjects(sourceFile)) {
3155
+ visitState(config);
3156
+ }
3157
+
3158
+ // Never mutates the file.
3159
+ return {
3160
+ changed: false,
3161
+ notes,
3162
+ sites
3163
+ };
3164
+ }
3165
+ };
3166
+
3167
+ const SECOND_ARGUMENT_HINT = 'v6 `createMachine` takes only a config; the second argument is ignored. Move actions, guards, actors and delays into `setup({ ... }).createMachine(...)`.';
3168
+ function getCallName(call) {
3169
+ const expr = call.getExpression();
3170
+ if (Node.isIdentifier(expr)) return expr.getText();
3171
+ if (Node.isPropertyAccessExpression(expr)) return expr.getName();
3172
+ return undefined;
3173
+ }
3174
+
3175
+ /**
3176
+ * Reports (does NOT transform) a second `createMachine` argument, which v6
3177
+ * ignores. `lower-actions` moves object-literal ones into the config.
3178
+ */
3179
+ const reportImplementations = {
3180
+ name: 'report-implementations',
3181
+ description: 'Detect (report only) a second `createMachine` argument, which v6 ignores.',
3182
+ apply(sourceFile) {
3183
+ const notes = [];
3184
+ const sites = [];
3185
+ const filePath = sourceFile.getFilePath();
3186
+ const report = (node, api, hint) => {
3187
+ const {
3188
+ line,
3189
+ column
3190
+ } = sourceFile.getLineAndColumnAtPos(node.getStart());
3191
+ notes.push(`${filePath}:${line}:${column} — '${api}' → ${hint}`);
3192
+ sites.push({
3193
+ file: filePath,
3194
+ line,
3195
+ column,
3196
+ api,
3197
+ hint
3198
+ });
3199
+ };
3200
+ for (const call of sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {
3201
+ const name = getCallName(call);
3202
+ const args = call.getArguments();
3203
+ if (name === 'createMachine' && args[1]) {
3204
+ report(args[1], 'implementations', SECOND_ARGUMENT_HINT);
3205
+ }
3206
+ }
3207
+
3208
+ // Never mutates the file.
3209
+ return {
3210
+ changed: false,
3211
+ notes,
3212
+ sites
3213
+ };
3214
+ }
3215
+ };
3216
+
3217
+ const TRANSITION_HINT = 'v6 emits only `@xstate.actor` and `@xstate.transition`; read the event, snapshot, executed `actions` and `microsteps` from `@xstate.transition`.';
3218
+ const HINTS = {
3219
+ '@xstate.event': TRANSITION_HINT,
3220
+ '@xstate.snapshot': TRANSITION_HINT,
3221
+ '@xstate.action': TRANSITION_HINT,
3222
+ '@xstate.microstep': TRANSITION_HINT,
3223
+ '@xstate.deadletter': 'v6 has no dead-letter inspection event; observe undelivered events with the `onRejectedEvent` option of `createActor` or `system.onRejectedEvent(listener)`.'
3224
+ };
3225
+
3226
+ /**
3227
+ * Reports (does NOT transform) string literals naming v5 inspection event
3228
+ * types. Inspection code often types events loosely or lives in plain
3229
+ * JavaScript, where comparing against a removed type compiles and never
3230
+ * matches. Scans every file, with or without an `xstate` import.
3231
+ */
3232
+ const reportInspectionEvents = {
3233
+ name: 'report-inspection-events',
3234
+ description: "Detect (report only) v5 inspection event types such as '@xstate.snapshot', which v6 never emits.",
3235
+ apply(sourceFile) {
3236
+ const notes = [];
3237
+ const sites = [];
3238
+ const filePath = sourceFile.getFilePath();
3239
+ const literals = [...sourceFile.getDescendantsOfKind(SyntaxKind.StringLiteral), ...sourceFile.getDescendantsOfKind(SyntaxKind.NoSubstitutionTemplateLiteral)].sort((a, b) => a.getStart() - b.getStart());
3240
+ for (const literal of literals) {
3241
+ if (Node.isImportDeclaration(literal.getParent())) continue;
3242
+ const api = literal.getLiteralText();
3243
+ const hint = HINTS[api];
3244
+ if (!hint) continue;
3245
+ const {
3246
+ line,
3247
+ column
3248
+ } = sourceFile.getLineAndColumnAtPos(literal.getStart());
3249
+ notes.push(`${filePath}:${line}:${column} — '${api}' → ${hint}`);
3250
+ sites.push({
3251
+ file: filePath,
3252
+ line,
3253
+ column,
3254
+ api,
3255
+ hint
3256
+ });
655
3257
  }
656
3258
 
657
3259
  // Never mutates the file.
658
3260
  return {
659
3261
  changed: false,
660
- notes
3262
+ notes,
3263
+ sites
661
3264
  };
662
3265
  }
663
3266
  };
664
3267
 
665
3268
  /** All transforms, in recommended application order. */
666
- const transforms = [renameImports, stringTargets, typesToSchemas, reportRemovedApis, reportIdPathTargets];
3269
+ const transforms = [renameImports, stringTargets, typesToSchemas, lowerActions, reportRemovedApis, reportIdPathTargets, suffixedLifecycleEvents, reportActionShapes, reportImplementations, reportInspectionEvents];
667
3270
  const transformsByName = Object.fromEntries(transforms.map(t => [t.name, t]));
668
3271
 
669
- /** Report-only transforms whose notes list manual migration steps. */
670
- const REPORT_TRANSFORMS = new Set(['report-removed-apis', 'report-id-path-targets']);
3272
+ /** Report-only transforms never rewrite; their notes list manual migration steps. */
3273
+ const isReportTransform = name => name.startsWith('report-');
671
3274
  /** Resolve a list of transform names to Transform objects. */
672
3275
  function selectTransforms(names) {
673
3276
  if (!names || names.length === 0) {
@@ -693,7 +3296,8 @@ function applyToSourceFile(sourceFile, transforms) {
693
3296
  perTransform.push({
694
3297
  transform: transform.name,
695
3298
  changed: result.changed,
696
- notes: result.notes
3299
+ notes: result.notes,
3300
+ sites: result.sites ?? []
697
3301
  });
698
3302
  if (result.changed) {
699
3303
  changed = true;
@@ -732,14 +3336,22 @@ function run(filePaths, transforms, options = {
732
3336
  });
733
3337
  const files = [];
734
3338
  const manualMigrationNotes = [];
3339
+ const manualSites = [];
735
3340
  let filesChanged = 0;
736
3341
  for (const filePath of filePaths) {
737
3342
  const sourceFile = project.addSourceFileAtPath(filePath);
738
3343
  const report = applyToSourceFile(sourceFile, transforms);
739
3344
  for (const pt of report.perTransform) {
740
- if (REPORT_TRANSFORMS.has(pt.transform)) {
3345
+ if (isReportTransform(pt.transform)) {
741
3346
  manualMigrationNotes.push(...pt.notes);
742
3347
  }
3348
+ // Any transform may leave code it could not migrate.
3349
+ for (const site of pt.sites) {
3350
+ manualSites.push({
3351
+ ...site,
3352
+ transform: pt.transform
3353
+ });
3354
+ }
743
3355
  }
744
3356
  if (report.changed) {
745
3357
  filesChanged++;
@@ -752,7 +3364,8 @@ function run(filePaths, transforms, options = {
752
3364
  return {
753
3365
  files,
754
3366
  filesChanged,
755
- manualMigrationNotes
3367
+ manualMigrationNotes,
3368
+ manualSites
756
3369
  };
757
3370
  }
758
3371
 
@@ -765,6 +3378,7 @@ function parseArgs(argv) {
765
3378
  command: undefined,
766
3379
  globs: [],
767
3380
  dry: false,
3381
+ json: false,
768
3382
  transformNames: undefined,
769
3383
  help: false
770
3384
  };
@@ -774,6 +3388,8 @@ function parseArgs(argv) {
774
3388
  parsed.help = true;
775
3389
  } else if (arg === '--dry') {
776
3390
  parsed.dry = true;
3391
+ } else if (arg === '--json') {
3392
+ parsed.json = true;
777
3393
  } else if (arg === '--transform') {
778
3394
  const value = argv[++i];
779
3395
  if (value) {
@@ -791,7 +3407,7 @@ function parseArgs(argv) {
791
3407
  }
792
3408
  function printHelp() {
793
3409
  const names = transforms.map(t => ` ${t.name} — ${t.description}`);
794
- 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'));
3410
+ process$1.stdout.write(['xstate-codemod — migrate XState source (v5 → v6)', '', 'Usage:', ' xstate-codemod migrate [globs...] [--dry] [--json] [--transform <name,name>]', ' xstate-codemod baseline <v5-file> [--out <path>]', ' xstate-codemod verify --baseline <path> <v6-file>', ' xstate-codemod verify <v5-file> <v6-file>', '', 'Run `xstate-codemod verify --help` for the baseline/verify options.', '', 'Options:', ' --dry Print a summary of changes without writing files', ' --json Print one JSON report to stdout instead of text', ' --transform <names> Comma-separated transform names (default: all)', ' -h, --help Show this help', '', `Default glob: ${DEFAULT_GLOB} (excludes node_modules and dist)`, '', 'Exit codes:', ' 0 No manual migration needed', ' 1 Usage error or invalid migration output', ' 2 Manual migration needed', '', 'Transforms:', ...names, ''].join('\n'));
795
3411
  }
796
3412
 
797
3413
  /**
@@ -818,24 +3434,67 @@ function main(argv) {
818
3434
  return args.command ? 0 : args.help ? 0 : 1;
819
3435
  }
820
3436
  if (args.command !== 'migrate') {
821
- process.stderr.write(`Unknown command '${args.command}'. Did you mean 'migrate'?\n`);
3437
+ process$1.stderr.write(`Unknown command '${args.command}'. Expected 'migrate', 'baseline' or 'verify'.\n`);
822
3438
  return 1;
823
3439
  }
824
3440
  let transforms;
825
3441
  try {
826
3442
  transforms = selectTransforms(args.transformNames);
827
3443
  } catch (err) {
828
- process.stderr.write(`${err.message}\n`);
3444
+ process$1.stderr.write(`${err.message}\n`);
829
3445
  return 1;
830
3446
  }
831
3447
  const files = resolveFiles(args.globs);
832
- if (files.length === 0) {
833
- process.stdout.write('No files matched.\n');
3448
+ if (files.length === 0 && !args.json) {
3449
+ process$1.stdout.write('No files matched.\n');
834
3450
  return 0;
835
3451
  }
836
- const report = run(files, transforms, {
837
- write: !args.dry
838
- });
3452
+ let report;
3453
+ try {
3454
+ report = run(files, transforms, {
3455
+ write: !args.dry
3456
+ });
3457
+ } catch (err) {
3458
+ const message = err.message;
3459
+ if (!message.startsWith('Refusing to write invalid migration output')) {
3460
+ throw err;
3461
+ }
3462
+ if (args.json) {
3463
+ process$1.stdout.write(`${JSON.stringify({
3464
+ error: {
3465
+ code: 'INVALID_OUTPUT',
3466
+ message
3467
+ }
3468
+ })}\n`);
3469
+ } else {
3470
+ process$1.stderr.write(`${message}\n`);
3471
+ }
3472
+ return 1;
3473
+ }
3474
+ const exitCode = report.manualSites.length > 0 ? 2 : 0;
3475
+ if (args.json) {
3476
+ process$1.stdout.write(`${JSON.stringify({
3477
+ filesChanged: report.filesChanged,
3478
+ files: report.files.map(file => ({
3479
+ filePath: file.filePath,
3480
+ changed: file.changed,
3481
+ transforms: file.perTransform.map(pt => ({
3482
+ name: pt.transform,
3483
+ changed: pt.changed,
3484
+ notes: pt.notes
3485
+ }))
3486
+ })),
3487
+ manualSites: report.manualSites.map(site => ({
3488
+ file: site.file,
3489
+ line: site.line,
3490
+ column: site.column,
3491
+ api: site.api,
3492
+ hint: site.hint,
3493
+ transform: site.transform
3494
+ }))
3495
+ }, null, 2)}\n`);
3496
+ return exitCode;
3497
+ }
839
3498
 
840
3499
  // Per-file, per-transform summary.
841
3500
  for (const file of report.files) {
@@ -843,27 +3502,1236 @@ function main(argv) {
843
3502
  if (touched.length === 0) {
844
3503
  continue;
845
3504
  }
846
- process.stdout.write(`\n${file.filePath}${file.changed ? '' : ' (no writes)'}\n`);
3505
+ process$1.stdout.write(`\n${file.filePath}${file.changed ? '' : ' (no writes)'}\n`);
847
3506
  for (const pt of touched) {
848
3507
  const mark = pt.changed ? '✔' : '·';
849
- process.stdout.write(` ${mark} ${pt.transform}\n`);
3508
+ process$1.stdout.write(` ${mark} ${pt.transform}\n`);
850
3509
  for (const note of pt.notes) {
851
- process.stdout.write(` - ${note}\n`);
3510
+ process$1.stdout.write(` - ${note}\n`);
852
3511
  }
853
3512
  }
854
3513
  }
855
3514
 
856
3515
  // Manual-migration section.
857
3516
  if (report.manualMigrationNotes.length) {
858
- process.stdout.write('\n=== Manual migration needed ===\n');
3517
+ process$1.stdout.write('\n=== Manual migration needed ===\n');
859
3518
  for (const note of report.manualMigrationNotes) {
860
- process.stdout.write(` ${note}\n`);
3519
+ process$1.stdout.write(` ${note}\n`);
861
3520
  }
862
3521
  }
863
3522
 
864
3523
  // Final report.
865
- process.stdout.write(`\n${args.dry ? '[dry run] ' : ''}${report.filesChanged} of ${files.length} file(s) ${args.dry ? 'would change' : 'changed'}.\n`);
866
- return 0;
3524
+ process$1.stdout.write(`\n${args.dry ? '[dry run] ' : ''}${report.filesChanged} of ${files.length} file(s) ${args.dry ? 'would change' : 'changed'}.\n`);
3525
+ return exitCode;
3526
+ }
3527
+
3528
+ // v5 spawn (`x:3`), v5 invoke (`0.root.a`), v6 invoke/spawn (`root.a:0`, `child:0`).
3529
+ const ANON_ID = /^(x:\d+|\d+\..+|.+:\d+)$/;
3530
+ function normalizeId(id) {
3531
+ return ANON_ID.test(id) ? '(anon)' : id;
3532
+ }
3533
+
3534
+ /**
3535
+ * v5 gives an id-less machine the root id `(machine)` and prefixes every
3536
+ * implicit state id with it; v6 leaves the prefix out.
3537
+ */
3538
+ function normalizeStateId(id) {
3539
+ if (id === '(machine)') return '';
3540
+ return id.startsWith('(machine).') ? id.slice('(machine).'.length) : id;
3541
+ }
3542
+ function isActorRef(value) {
3543
+ return typeof value.send === 'function' && typeof value.getSnapshot === 'function' && typeof value.id === 'string';
3544
+ }
3545
+
3546
+ /**
3547
+ * JSON round-trip that marks functions/symbols and replaces actor refs.
3548
+ * Objects nested deeper than `maxDepth` become `'[deep]'`.
3549
+ */
3550
+ function toJson(value, stats, seen = new Set(), maxDepth = Infinity) {
3551
+ switch (typeof value) {
3552
+ case 'undefined':
3553
+ case 'string':
3554
+ case 'boolean':
3555
+ return value;
3556
+ case 'number':
3557
+ return Number.isFinite(value) ? value : null;
3558
+ case 'bigint':
3559
+ return String(value);
3560
+ case 'function':
3561
+ stats.nonSerializable++;
3562
+ return '[fn]';
3563
+ case 'symbol':
3564
+ stats.nonSerializable++;
3565
+ return '[symbol]';
3566
+ }
3567
+ if (value === null) return null;
3568
+ const obj = value;
3569
+ if (isActorRef(obj)) return {
3570
+ '@actor': normalizeId(obj.id)
3571
+ };
3572
+ if (obj instanceof Error) return String(obj);
3573
+ if (typeof obj.toJSON === 'function') return toJson(obj.toJSON(), stats, seen, maxDepth);
3574
+ if (seen.has(obj)) return '[circular]';
3575
+ if (seen.size >= maxDepth) return '[deep]';
3576
+ seen.add(obj);
3577
+ try {
3578
+ if (Array.isArray(obj)) {
3579
+ return obj.map(item => toJson(item, stats, seen, maxDepth) ?? null);
3580
+ }
3581
+ const out = {};
3582
+ for (const key of Object.keys(obj)) {
3583
+ const item = toJson(obj[key], stats, seen, maxDepth);
3584
+ if (item !== undefined) out[key] = item;
3585
+ }
3586
+ return out;
3587
+ } finally {
3588
+ seen.delete(obj);
3589
+ }
3590
+ }
3591
+ function withOutput(event, output, stats) {
3592
+ const json = toJson(output, stats);
3593
+ return json === undefined ? event : {
3594
+ ...event,
3595
+ output: json
3596
+ };
3597
+ }
3598
+
3599
+ /** Maps v5 suffixed and v6 payload-identified lifecycle events to one shape. */
3600
+ function normalizeEvent(event, stats) {
3601
+ const {
3602
+ type,
3603
+ sessionId: _sessionId,
3604
+ ...rest
3605
+ } = event;
3606
+ let match;
3607
+ if (type === 'xstate.done.actor' || (match = /^xstate\.done\.actor\.(.+)$/.exec(type))) {
3608
+ const actorId = normalizeId(rest.actorId ?? match[1]);
3609
+ return withOutput({
3610
+ type: 'xstate.done.actor',
3611
+ actorId
3612
+ }, rest.output, stats);
3613
+ }
3614
+ if (type === 'xstate.error.actor' || (match = /^xstate\.error\.actor\.(.+)$/.exec(type))) {
3615
+ return {
3616
+ type: 'xstate.error.actor',
3617
+ actorId: normalizeId(rest.actorId ?? match[1]),
3618
+ error: String(rest.error)
3619
+ };
3620
+ }
3621
+ if (type === 'xstate.done.state' || (match = /^xstate\.done\.state\.(.+)$/.exec(type))) {
3622
+ const stateId = normalizeStateId(rest.stateId ?? match[1]);
3623
+ return withOutput({
3624
+ type: 'xstate.done.state',
3625
+ stateId
3626
+ }, rest.output, stats);
3627
+ }
3628
+ if (type === 'xstate.after') {
3629
+ return {
3630
+ type,
3631
+ delay: rest.delay,
3632
+ stateId: normalizeStateId(rest.stateId)
3633
+ };
3634
+ }
3635
+ if (match = /^xstate\.after\.([^.]+)\.(.+)$/.exec(type)) {
3636
+ const delay = Number(match[1]);
3637
+ return {
3638
+ type: 'xstate.after',
3639
+ delay: Number.isFinite(delay) ? delay : match[1],
3640
+ stateId: normalizeStateId(match[2])
3641
+ };
3642
+ }
3643
+ return {
3644
+ type,
3645
+ ...toJson(rest, stats)
3646
+ };
3647
+ }
3648
+
3649
+ /** JSON with sorted object keys, for order-insensitive equality. */
3650
+ function canonical(value) {
3651
+ return JSON.stringify(value, (_key, item) => item && typeof item === 'object' && !Array.isArray(item) ? Object.fromEntries(Object.keys(item).sort().map(key => [key, item[key]])) : item);
3652
+ }
3653
+ function isEqual(a, b) {
3654
+ return canonical(a) === canonical(b);
3655
+ }
3656
+
3657
+ function mapDeep(value, fn) {
3658
+ if (Array.isArray(value)) return value.map(item => mapDeep(item, fn));
3659
+ if (value && typeof value === 'object') {
3660
+ const out = {};
3661
+ for (const [key, item] of Object.entries(value)) {
3662
+ const mapped = fn(key, item);
3663
+ if (mapped !== undefined) out[key] = mapDeep(mapped, fn);
3664
+ }
3665
+ return out;
3666
+ }
3667
+ return value;
3668
+ }
3669
+ const stripAnonSuffix = value => mapDeep(value, (key, item) => (key === 'id' || key === 'to') && typeof item === 'string' ? item.replace(/(:\d+|\[\d+\])$/, '').replace(/^(x:\d+|\d+\.)/, '') : item);
3670
+ const isActorMarker = item => !!item && typeof item === 'object' && '@actor' in item;
3671
+ const stripErrorPrefix = message => message.replace(/^(\w*Error:\s*)+/, '').trim();
3672
+ const INTENTIONAL_DELTAS = [{
3673
+ rule: 'anonymous-child-id',
3674
+ section: '§12',
3675
+ note: 'anonymous child ids are generated differently',
3676
+ matches: (field, expected, actual) => (field === 'children' || field === 'sent') && isEqual(stripAnonSuffix(expected), stripAnonSuffix(actual))
3677
+ }, {
3678
+ rule: 'error-string-shape',
3679
+ section: '§16',
3680
+ note: 'same error message, different error class or prefix',
3681
+ matches: (field, expected, actual) => field === 'error' && typeof expected === 'string' && typeof actual === 'string' && expected !== '' && actual !== '' && stripErrorPrefix(expected) === stripErrorPrefix(actual)
3682
+ }, {
3683
+ rule: 'context-reference',
3684
+ section: '§20',
3685
+ note: 'context differs only in stored actor references',
3686
+ matches: (field, expected, actual) => field === 'context' && isEqual(mapDeep(expected, (_key, item) => isActorMarker(item) ? undefined : item), mapDeep(actual, (_key, item) => isActorMarker(item) ? undefined : item))
3687
+ }, {
3688
+ rule: 'done-event-session',
3689
+ section: '§12',
3690
+ note: 'done events differ only by sessionId',
3691
+ matches: (field, expected, actual) => field === 'processed' && isEqual(mapDeep(expected, (key, item) => key === 'sessionId' ? undefined : item), mapDeep(actual, (key, item) => key === 'sessionId' ? undefined : item))
3692
+ }, {
3693
+ rule: 'final-state-output',
3694
+ section: '§3',
3695
+ note: "v6 promotes a top-level final state's output to snapshot.output when the root has none; v5 left it undefined",
3696
+ matches: (field, expected, actual) => field === 'output' && expected === undefined && actual !== undefined
3697
+ }, {
3698
+ rule: 'root-send-parent',
3699
+ section: '§26',
3700
+ note: "v5 errors when a root actor sends to its parent; v6 dead-letters the event with reason 'missingTarget'",
3701
+ matches: (field, _expected, _actual, _step, steps) => field === 'status' && steps.expected.status === 'error' && steps.actual.status !== 'error' && /#_parent|Unable to send event to actor/.test(steps.expected.error ?? '')
3702
+ }];
3703
+
3704
+ const FIELD_ORDER = ['status', 'value', 'context', 'output', 'error', 'processed', 'children', 'sent', 'emitted', 'actions'];
3705
+ const DEFAULT_FIELDS = FIELD_ORDER.filter(f => f !== 'actions');
3706
+ function classify(field, expected, actual, step, steps) {
3707
+ const rule = INTENTIONAL_DELTAS.find(delta => delta.matches(field, expected, actual, step, steps));
3708
+ return rule ? {
3709
+ rule: rule.rule,
3710
+ section: rule.section,
3711
+ note: rule.note
3712
+ } : 'unclassified';
3713
+ }
3714
+ function errorDetail(expected, actual) {
3715
+ const parts = [];
3716
+ if (expected.status === 'error') parts.push(`v5 error: ${expected.error}`);
3717
+ if (actual.status === 'error') parts.push(`v6 error: ${actual.error}`);
3718
+ return parts.join('; ') || undefined;
3719
+ }
3720
+
3721
+ /**
3722
+ * Compares traces step by step. An unclassified mismatch ends its sequence;
3723
+ * classified (intentional) mismatches are recorded and comparison continues so
3724
+ * they cannot mask a later real divergence.
3725
+ */
3726
+ function diffTraces(sequences, expected, actual, fields = DEFAULT_FIELDS) {
3727
+ const diffs = [];
3728
+ let stepsCompared = 0;
3729
+ const compared = FIELD_ORDER.filter(field => fields.includes(field));
3730
+ for (const sequence of sequences) {
3731
+ const exp = expected.find(t => t.sequenceId === sequence.id);
3732
+ const act = actual.find(t => t.sequenceId === sequence.id);
3733
+ if (!exp || !act) continue;
3734
+ const length = Math.min(exp.steps.length, act.steps.length);
3735
+ outer: for (let step = 0; step < length; step++) {
3736
+ stepsCompared++;
3737
+ const expStep = exp.steps[step];
3738
+ const actStep = act.steps[step];
3739
+ // v6 errored snapshots carry no value (v5 keeps the last one), so when
3740
+ // both sides errored only the status and error are meaningful.
3741
+ const bothErrored = expStep.status === 'error' && actStep.status === 'error';
3742
+ for (const field of compared) {
3743
+ if (bothErrored && field !== 'status' && field !== 'error') continue;
3744
+ const e = expStep[field];
3745
+ const a = actStep[field];
3746
+ if (isEqual(e, a)) continue;
3747
+ const classification = classify(field, e, a, step, {
3748
+ expected: expStep,
3749
+ actual: actStep
3750
+ });
3751
+ const detail = field === 'status' ? errorDetail(expStep, actStep) : undefined;
3752
+ diffs.push({
3753
+ sequenceId: sequence.id,
3754
+ origin: sequence.origin,
3755
+ step,
3756
+ witness: sequence.steps.slice(0, step),
3757
+ field,
3758
+ expected: e,
3759
+ actual: a,
3760
+ ...(detail && {
3761
+ detail
3762
+ }),
3763
+ classification
3764
+ });
3765
+ if (classification === 'unclassified') break outer;
3766
+ // An intentional status divergence (one side errored, the other kept
3767
+ // running) makes every later field and step incomparable.
3768
+ if (field === 'status') break outer;
3769
+ }
3770
+ }
3771
+ }
3772
+ diffs.sort((a, b) => Number(a.classification !== 'unclassified') - Number(b.classification !== 'unclassified'));
3773
+ return {
3774
+ diffs,
3775
+ stepsCompared
3776
+ };
3777
+ }
3778
+
3779
+ /** Generation note for a graph traversal that never closed. */
3780
+ const TRAVERSAL_LIMIT_NOTE = 'graph traversal hit its limit (context that changes on every step prevents the graph from closing); random walks still cover these paths';
3781
+ function mulberry32(seed) {
3782
+ let a = seed >>> 0;
3783
+ return () => {
3784
+ a = a + 0x6d2b79f5 >>> 0;
3785
+ let t = a;
3786
+ t = Math.imul(t ^ t >>> 15, t | 1);
3787
+ t ^= t + Math.imul(t ^ t >>> 7, t | 61);
3788
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
3789
+ };
3790
+ }
3791
+ function eventCandidates(machine, cases) {
3792
+ const events = [];
3793
+ const withoutCases = [];
3794
+ const types = [...new Set(machine.events)].filter(type => !type.startsWith('xstate.') && !type.includes('*'));
3795
+ for (const type of types) {
3796
+ const payloads = cases[type];
3797
+ if (payloads?.length) {
3798
+ for (const payload of payloads) events.push({
3799
+ ...payload,
3800
+ type
3801
+ });
3802
+ } else {
3803
+ withoutCases.push(type);
3804
+ events.push({
3805
+ type
3806
+ });
3807
+ }
3808
+ }
3809
+ return {
3810
+ events,
3811
+ withoutCases
3812
+ };
3813
+ }
3814
+
3815
+ /**
3816
+ * Advancing exactly to the next deadline fires one timer per step: v5's
3817
+ * SimulatedClock does not fire timers scheduled during an increment while v6's
3818
+ * does, so a longer jump would diverge on clock semantics alone.
3819
+ */
3820
+ function untilNextTimer(clock) {
3821
+ // SimulatedClock keeps pending timeouts in a private map.
3822
+ let next = Infinity;
3823
+ for (const {
3824
+ start,
3825
+ timeout
3826
+ } of clock.timeouts.values()) next = Math.min(next, start + timeout);
3827
+ return next === Infinity ? undefined : next - clock.now();
3828
+ }
3829
+ const flush$1 = async () => {
3830
+ for (let i = 0; i < 3; i++) await new Promise(resolve => setImmediate(resolve));
3831
+ };
3832
+
3833
+ /** Sequences from the v5 graph (shortest, simple) and seeded random walks. */
3834
+ async function generateSequences(xs, graph, machine, events, options, input) {
3835
+ const raw = [];
3836
+ const generationErrors = [];
3837
+ const fromPaths = async (origin, paths) => {
3838
+ let result;
3839
+ try {
3840
+ // The wall clock stays at the epoch during traversal so context that
3841
+ // stores `Date.now()` does not keep the graph from closing.
3842
+ result = await withDeterminism({
3843
+ now: () => 0
3844
+ }, options.seed, paths);
3845
+ } catch (err) {
3846
+ // Unbounded context makes the state space infinite, and a guard that
3847
+ // reads a payload the event cases do not supply throws. Either way the
3848
+ // origin is dropped and reported; random walks still run.
3849
+ const message = err.message ?? String(err);
3850
+ generationErrors.push(`${origin} paths skipped: ${message === 'Traversal limit exceeded' ? TRAVERSAL_LIMIT_NOTE : message}`);
3851
+ return;
3852
+ }
3853
+ for (const path of result) {
3854
+ raw.push({
3855
+ origin,
3856
+ steps: path.steps.filter(step => step.event.type !== 'xstate.init').map(step => ({
3857
+ event: step.event
3858
+ }))
3859
+ });
3860
+ }
3861
+ };
3862
+ const traversal = {
3863
+ events,
3864
+ limit: 1000,
3865
+ input
3866
+ };
3867
+ await fromPaths('shortest', () => graph.getShortestPaths(machine, traversal));
3868
+ if (options.simplePaths === false) generationErrors.push('simple paths skipped (--no-simple-paths)');else await fromPaths('simple', () => graph.getSimplePaths(machine, traversal));
3869
+ const random = mulberry32(options.seed);
3870
+ const randomWalk = async clock => {
3871
+ const actor = xs.createActor(machine, {
3872
+ clock,
3873
+ input
3874
+ });
3875
+ actor.subscribe({
3876
+ error: () => {}
3877
+ });
3878
+ actor.start();
3879
+ await flush$1();
3880
+ const steps = [];
3881
+ while (steps.length < options.steps && actor.getSnapshot().status === 'active') {
3882
+ const snapshot = actor.getSnapshot();
3883
+ const candidates = events.filter(event => {
3884
+ try {
3885
+ return snapshot.can(event);
3886
+ } catch (err) {
3887
+ // A guard threw on this payload; drop the event for this walk.
3888
+ const note = `event '${event.type}' skipped: guard threw: ${err.message ?? String(err)}`;
3889
+ if (!generationErrors.includes(note)) generationErrors.push(note);
3890
+ return false;
3891
+ }
3892
+ }).map(event => ({
3893
+ event
3894
+ }));
3895
+ const ms = untilNextTimer(clock);
3896
+ if (ms !== undefined) candidates.push({
3897
+ advance: ms
3898
+ });
3899
+ if (!candidates.length) break;
3900
+ const next = candidates[Math.floor(random() * candidates.length)];
3901
+ steps.push(next);
3902
+ if ('event' in next) actor.send(next.event);else clock.increment(next.advance);
3903
+ await flush$1();
3904
+ }
3905
+ actor.stop();
3906
+ return steps;
3907
+ };
3908
+ for (let walk = 0; walk < options.walks; walk++) {
3909
+ const clock = new xs.SimulatedClock();
3910
+ const steps = await withDeterminism(clock, options.seed + walk, () => randomWalk(clock));
3911
+ raw.push({
3912
+ origin: 'random',
3913
+ steps
3914
+ });
3915
+ }
3916
+ const seen = new Set();
3917
+ const counts = {
3918
+ shortest: 0,
3919
+ simple: 0,
3920
+ random: 0
3921
+ };
3922
+ const sequences = [];
3923
+ for (const {
3924
+ origin,
3925
+ steps
3926
+ } of raw) {
3927
+ const key = JSON.stringify(steps);
3928
+ if (seen.has(key)) continue;
3929
+ seen.add(key);
3930
+ if (sequences.length >= options.maxSequences) break;
3931
+ sequences.push({
3932
+ id: `${origin}-${counts[origin]++}`,
3933
+ origin,
3934
+ steps
3935
+ });
3936
+ }
3937
+ return {
3938
+ sequences,
3939
+ generationErrors
3940
+ };
3941
+ }
3942
+
3943
+ /** `Date.now()` at simulated time 0. */
3944
+ const EPOCH = 1_700_000_000_000;
3945
+
3946
+ /**
3947
+ * Runs `fn` with `Date.now`, `performance.now`, and `Math.random` driven by
3948
+ * the simulated clock and a seeded generator, so both majors read identical
3949
+ * values. The originals are restored afterwards.
3950
+ */
3951
+ async function withDeterminism(clock, seed, fn) {
3952
+ const {
3953
+ now
3954
+ } = Date;
3955
+ const perfNow = performance.now;
3956
+ const {
3957
+ random
3958
+ } = Math;
3959
+ Date.now = () => EPOCH + clock.now();
3960
+ performance.now = () => clock.now();
3961
+ Math.random = mulberry32(seed);
3962
+ try {
3963
+ return await fn();
3964
+ } finally {
3965
+ Date.now = now;
3966
+ performance.now = perfNow;
3967
+ Math.random = random;
3968
+ }
3969
+ }
3970
+
3971
+ /** Action params can hold whole `args` objects; record only their top levels. */
3972
+ const ACTION_PARAMS_DEPTH$1 = 4;
3973
+ const empty$1 = () => ({
3974
+ processed: [],
3975
+ sent: [],
3976
+ emitted: [],
3977
+ actions: []
3978
+ });
3979
+
3980
+ /**
3981
+ * v5 inspection: `@xstate.snapshot` (one per processed event), `@xstate.event`
3982
+ * (relays; `sourceRef` → `actorRef`), `@xstate.actor`, `@xstate.action`.
3983
+ */
3984
+ function createRecorder5(stats) {
3985
+ let root;
3986
+ let bucket = empty$1();
3987
+ const registry = new Map();
3988
+ return {
3989
+ setRoot: ref => {
3990
+ root = ref;
3991
+ },
3992
+ inspect(e) {
3993
+ switch (e.type) {
3994
+ case '@xstate.actor':
3995
+ registry.set(e.actorRef, typeof e.actorRef.src === 'string' ? e.actorRef.src : '(inline)');
3996
+ break;
3997
+ case '@xstate.snapshot':
3998
+ if (e.actorRef === root && e.event.type !== 'xstate.init') {
3999
+ bucket.processed.push(normalizeEvent(e.event, stats));
4000
+ }
4001
+ break;
4002
+ case '@xstate.event':
4003
+ if (e.sourceRef === root && e.actorRef !== root && e.event.type !== 'xstate.init' && e.event.type !== 'xstate.stop') {
4004
+ bucket.sent.push({
4005
+ to: normalizeId(e.actorRef.id),
4006
+ event: normalizeEvent(e.event, stats)
4007
+ });
4008
+ }
4009
+ break;
4010
+ case '@xstate.action':
4011
+ if (e.actorRef === root && !e.action.type.startsWith('xstate.')) {
4012
+ bucket.actions.push({
4013
+ type: e.action.type,
4014
+ params: toJson(e.action.params, stats, undefined, ACTION_PARAMS_DEPTH$1) ?? null
4015
+ });
4016
+ }
4017
+ break;
4018
+ }
4019
+ },
4020
+ emit(event) {
4021
+ bucket.emitted.push(normalizeEvent(event, stats));
4022
+ },
4023
+ srcOf: ref => registry.get(ref) ?? '(inline)',
4024
+ take() {
4025
+ const taken = bucket;
4026
+ bucket = empty$1();
4027
+ return taken;
4028
+ }
4029
+ };
4030
+ }
4031
+
4032
+ /** Action params can hold whole `args` objects; record only their top levels. */
4033
+ const ACTION_PARAMS_DEPTH = 4;
4034
+ const empty = () => ({
4035
+ processed: [],
4036
+ sent: [],
4037
+ emitted: [],
4038
+ actions: []
4039
+ });
4040
+
4041
+ /**
4042
+ * v6 inspection: `@xstate.actor` (`id`, `src`, `parentRef`) and
4043
+ * `@xstate.transition` (`event`, `snapshot`, `actions[]`, `sent[]`).
4044
+ */
4045
+ function createRecorder6(stats) {
4046
+ let root;
4047
+ let rootSnapshot;
4048
+ let bucket = empty();
4049
+ const registry = new Map();
4050
+
4051
+ // Delayed raises arrive as `xstate.timer` keyed by `{ event }`, `{ seq }` or
4052
+ // `{ id }`; resolve them against the timers of the snapshot that scheduled
4053
+ // them so the trace shows the delivered event, as in v5. A delayed send to
4054
+ // another actor shows up in that firing transition's `sent` instead.
4055
+ function onTimer(event) {
4056
+ if (event.event) {
4057
+ bucket.processed.push(normalizeEvent(event.event, stats));
4058
+ return;
4059
+ }
4060
+ const timer = rootSnapshot?.timers?.find(t => event.id !== undefined ? t.id === event.id : t.seq === event.seq);
4061
+ if (timer && (timer.target === 'self' || timer.target === root)) {
4062
+ bucket.processed.push(normalizeEvent(timer.event, stats));
4063
+ }
4064
+ }
4065
+ return {
4066
+ setRoot: ref => {
4067
+ root = ref;
4068
+ },
4069
+ inspect(e) {
4070
+ if (e.type === '@xstate.actor') {
4071
+ registry.set(e.actorRef, typeof e.src === 'string' ? e.src : '(inline)');
4072
+ return;
4073
+ }
4074
+ if (e.type !== '@xstate.transition' || e.actorRef !== root) return;
4075
+ const type = e.event.type;
4076
+ if (type === 'xstate.timer') onTimer(e.event);else if (type !== 'xstate.init') bucket.processed.push(normalizeEvent(e.event, stats));
4077
+ for (const action of e.actions) {
4078
+ if (action.type.startsWith('xstate.') || action.type.startsWith('@xstate.')) continue;
4079
+ bucket.actions.push({
4080
+ type: action.type,
4081
+ params: toJson(action.params, stats, undefined, ACTION_PARAMS_DEPTH) ?? null
4082
+ });
4083
+ }
4084
+ for (const sent of e.sent) {
4085
+ // v5 reports a delayed send when it is delivered; so does the timer's
4086
+ // firing transition here.
4087
+ if (sent.targetRef === root || sent.delay !== undefined) continue;
4088
+ bucket.sent.push({
4089
+ to: normalizeId(sent.targetId),
4090
+ event: normalizeEvent(sent.event, stats)
4091
+ });
4092
+ }
4093
+ rootSnapshot = e.snapshot;
4094
+ },
4095
+ emit(event) {
4096
+ bucket.emitted.push(normalizeEvent(event, stats));
4097
+ },
4098
+ srcOf: ref => registry.get(ref) ?? '(inline)',
4099
+ take() {
4100
+ const taken = bucket;
4101
+ bucket = empty();
4102
+ return taken;
4103
+ }
4104
+ };
4105
+ }
4106
+
4107
+ function outcome(spec) {
4108
+ if (spec === 'pending') return () => new Promise(() => {});
4109
+ if ('error' in spec) return () => Promise.reject(new Error(spec.error));
4110
+ return () => Promise.resolve(spec.done);
4111
+ }
4112
+
4113
+ /**
4114
+ * Replaces keyed actors with scripted promise logic. Keyed state machines stay
4115
+ * real: they are deterministic and their behavior is part of the trace.
4116
+ * Everything else defaults to `'pending'` so no real side effect runs.
4117
+ */
4118
+ function applyStubs(xs, major, machine, stubs) {
4119
+ const keyed = (major === 5 ? machine.implementations?.actors : machine.sources?.actors) ?? {};
4120
+ const actors = {};
4121
+ for (const [key, logic] of Object.entries(keyed)) {
4122
+ const spec = stubs.actors?.[key];
4123
+ if (!spec && logic instanceof xs.StateMachine) continue;
4124
+ const run = outcome(spec ?? 'pending');
4125
+ actors[key] = major === 5 ? xs.fromPromise(run) : xs.createAsyncLogic({
4126
+ run
4127
+ });
4128
+ }
4129
+ return machine.provide({
4130
+ actors,
4131
+ ...(stubs.delays && {
4132
+ delays: stubs.delays
4133
+ })
4134
+ });
4135
+ }
4136
+
4137
+ /** Marks the start of the JSON result on stdout; user code may print before it. */
4138
+ const RESULT_MARKER = '\n@@xstate-verify-result@@\n';
4139
+ class ChildError extends Error {
4140
+ constructor(code, message) {
4141
+ super(message);
4142
+ this.code = void 0;
4143
+ this.code = code;
4144
+ }
4145
+ }
4146
+ const isLogic = value => !!value && typeof value.transition === 'function' && typeof value.getInitialSnapshot === 'function';
4147
+ async function loadLogic(file, name) {
4148
+ let mod;
4149
+ try {
4150
+ mod = await import(pathToFileURL(file).href);
4151
+ } catch (err) {
4152
+ const error = err;
4153
+ throw Object.assign(new ChildError('LOAD_ERROR', error.message), {
4154
+ stack: error.stack
4155
+ });
4156
+ }
4157
+ const names = Object.keys(mod);
4158
+ const found = name ? isLogic(mod[name]) && name : isLogic(mod.default) && 'default' || names.find(key => isLogic(mod[key]));
4159
+ if (!found) {
4160
+ throw new ChildError('EXPORT_NOT_FOUND', `${name ? `export "${name}" is not actor logic` : 'no actor logic export'} in ${file}; exports: ${names.join(', ') || '(none)'}`);
4161
+ }
4162
+ const notes = [];
4163
+ const logicNames = names.filter(key => isLogic(mod[key]));
4164
+ if (!name && new Set(logicNames.map(key => mod[key])).size > 1) notes.push(`module exports ${logicNames.length} machines: ${logicNames.join(', ')}; using ${found} (pass --export to choose)`);
4165
+ return [found, mod[found], notes];
4166
+ }
4167
+ const flush = async () => {
4168
+ for (let i = 0; i < 3; i++) await new Promise(resolve => setImmediate(resolve));
4169
+ };
4170
+ async function recordTrace(xs, logic, sequence, recorder, stats, input, seed) {
4171
+ const clock = new xs.SimulatedClock();
4172
+ return withDeterminism(clock, seed, async () => {
4173
+ const actor = xs.createActor(logic, {
4174
+ clock,
4175
+ inspect: recorder.inspect,
4176
+ input
4177
+ });
4178
+ recorder.setRoot(actor);
4179
+ actor.subscribe({
4180
+ error: () => {}
4181
+ });
4182
+ actor.on('*', recorder.emit);
4183
+ const capture = input => {
4184
+ const snapshot = actor.getSnapshot();
4185
+ const children = Object.values(snapshot.children ?? {}).filter(Boolean).map(ref => ({
4186
+ id: normalizeId(ref.id),
4187
+ src: recorder.srcOf(ref)
4188
+ })).sort((a, b) => (a.id + a.src).localeCompare(b.id + b.src));
4189
+ const output = toJson(snapshot.output, stats);
4190
+ return {
4191
+ input,
4192
+ value: toJson(snapshot.value, stats),
4193
+ context: toJson(snapshot.context, stats) ?? null,
4194
+ status: snapshot.status,
4195
+ ...(output !== undefined && {
4196
+ output
4197
+ }),
4198
+ ...(snapshot.error !== undefined && {
4199
+ error: String(snapshot.error)
4200
+ }),
4201
+ ...recorder.take(),
4202
+ children
4203
+ };
4204
+ };
4205
+ actor.start();
4206
+ await flush();
4207
+ const steps = [capture({
4208
+ start: true
4209
+ })];
4210
+ for (const step of sequence.steps) {
4211
+ if ('event' in step) actor.send(step.event);else clock.increment(step.advance);
4212
+ await flush();
4213
+ steps.push(capture(step));
4214
+ }
4215
+ actor.stop();
4216
+ return {
4217
+ sequenceId: sequence.id,
4218
+ steps
4219
+ };
4220
+ });
4221
+ }
4222
+ async function runJob(job) {
4223
+ const major = process.env.XSTATE_VERIFY_MAJOR === '5' ? 5 : 6;
4224
+ // The alias hook maps `xstate` to the selected major for these imports too.
4225
+ const xs = await import('xstate');
4226
+ const xstateVersion = createRequire(import.meta.url)('xstate/package.json').version;
4227
+ const [exportName, logic, notes] = await loadLogic(job.file, job.export);
4228
+ const machine = applyStubs(xs, major, logic, job.stubs);
4229
+ const stats = {
4230
+ nonSerializable: 0
4231
+ };
4232
+ let sequences = job.sequences ?? [];
4233
+ let eventsWithoutCases = [];
4234
+ let generationErrors = [];
4235
+ if (job.mode === 'baseline') {
4236
+ const graph = await import('xstate/graph');
4237
+ const candidates = eventCandidates(machine, job.events);
4238
+ eventsWithoutCases = candidates.withoutCases;
4239
+ const generated = await generateSequences(xs, graph, machine, candidates.events, job.options, job.input);
4240
+ sequences = generated.sequences;
4241
+ generationErrors = generated.generationErrors;
4242
+ }
4243
+ const traces = [];
4244
+ const unstubbed = new Set();
4245
+ for (const [index, sequence] of sequences.entries()) {
4246
+ const recorder = major === 5 ? createRecorder5(stats) : createRecorder6(stats);
4247
+ const trace = await recordTrace(xs, machine, sequence, recorder, stats, job.input, job.options.seed + index);
4248
+ for (const step of trace.steps) {
4249
+ for (const child of step.children) if (child.src === '(inline)') unstubbed.add(child.id);
4250
+ }
4251
+ traces.push(trace);
4252
+ }
4253
+ return {
4254
+ ok: true,
4255
+ export: exportName,
4256
+ xstateVersion,
4257
+ sequences,
4258
+ traces,
4259
+ unstubbed: [...unstubbed].sort(),
4260
+ eventsWithoutCases,
4261
+ generationErrors,
4262
+ nonSerializable: stats.nonSerializable,
4263
+ notes
4264
+ };
4265
+ }
4266
+ async function readStdin() {
4267
+ let data = '';
4268
+ for await (const chunk of process.stdin) data += chunk;
4269
+ return data;
4270
+ }
4271
+ async function childMain() {
4272
+ let result;
4273
+ try {
4274
+ result = await runJob(JSON.parse(await readStdin()));
4275
+ } catch (err) {
4276
+ result = {
4277
+ ok: false,
4278
+ code: err instanceof ChildError ? err.code : 'RUN_ERROR',
4279
+ message: String(err?.message ?? err),
4280
+ stack: err?.stack
4281
+ };
4282
+ }
4283
+ process.stdout.write(RESULT_MARKER + JSON.stringify(result), () => process.exit(0));
4284
+ }
4285
+
4286
+ const require = createRequire(import.meta.url);
4287
+ // Resolved from this module, not cwd: the same lookup works from src and dist.
4288
+ const packageRoot = dirname(require.resolve('@xstate/codemod/package.json'));
4289
+ const DEFAULT_OPTIONS = {
4290
+ seed: 1,
4291
+ walks: 25,
4292
+ steps: 12,
4293
+ maxSequences: 200
4294
+ };
4295
+ const DEFAULT_TIMEOUT = 60_000;
4296
+ class VerifyError extends Error {
4297
+ constructor(phase, code, message, stderr, childStack) {
4298
+ super(message);
4299
+ this.phase = void 0;
4300
+ this.code = void 0;
4301
+ this.stderr = void 0;
4302
+ this.childStack = void 0;
4303
+ this.phase = phase;
4304
+ this.code = code;
4305
+ this.stderr = stderr;
4306
+ this.childStack = childStack;
4307
+ }
4308
+ }
4309
+ async function runChild(major, job, timeout = DEFAULT_TIMEOUT) {
4310
+ const args = [
4311
+ // Select xstate's `development` export condition so v6 config diagnostics
4312
+ // throw at load with a clear message instead of failing later at runtime.
4313
+ '--conditions=development', '--import', pathToFileURL(require.resolve('tsx')).href, '--import', pathToFileURL(join(packageRoot, 'bin/alias-hook.mjs')).href, join(packageRoot, 'bin/verify-child.mjs')];
4314
+ return new Promise(done => {
4315
+ const child = spawn(process.execPath, args, {
4316
+ cwd: dirname(job.file),
4317
+ env: {
4318
+ ...process.env,
4319
+ XSTATE_VERIFY_MAJOR: String(major)
4320
+ },
4321
+ stdio: ['pipe', 'pipe', 'pipe']
4322
+ });
4323
+ let stdout = '';
4324
+ let stderr = '';
4325
+ let timedOut = false;
4326
+ const timer = setTimeout(() => {
4327
+ timedOut = true;
4328
+ child.kill('SIGKILL');
4329
+ }, timeout);
4330
+ child.stdout.setEncoding('utf8').on('data', chunk => stdout += chunk);
4331
+ child.stderr.setEncoding('utf8').on('data', chunk => stderr += chunk);
4332
+ child.on('close', code => {
4333
+ clearTimeout(timer);
4334
+ const at = stdout.lastIndexOf(RESULT_MARKER);
4335
+ let result;
4336
+ if (timedOut) {
4337
+ result = {
4338
+ ok: false,
4339
+ code: 'CHILD_TIMEOUT',
4340
+ message: `child timed out after ${timeout}ms`
4341
+ };
4342
+ } else if (at === -1) {
4343
+ // Node prints the fatal reason (e.g. heap out of memory) before the
4344
+ // native stack, so the first such line is the useful one.
4345
+ const fatal = stderr.split('\n').find(line => /FATAL ERROR|^\w*Error:/.test(line));
4346
+ result = {
4347
+ ok: false,
4348
+ code: 'CHILD_CRASH',
4349
+ message: `child exited with code ${code} without a result${fatal ? `: ${fatal.trim()}` : ''}`
4350
+ };
4351
+ } else {
4352
+ result = JSON.parse(stdout.slice(at + RESULT_MARKER.length));
4353
+ }
4354
+ done({
4355
+ result,
4356
+ stderr
4357
+ });
4358
+ });
4359
+ child.stdin.end(JSON.stringify(job));
4360
+ });
4361
+ }
4362
+ /** The error of the first trace's start step, when the machine errored there. */
4363
+ function startError(baseline) {
4364
+ const start = baseline.traces[0]?.steps[0];
4365
+ return start?.status === 'error' ? start.error ?? 'unknown error' : undefined;
4366
+ }
4367
+
4368
+ /** Records a golden trace of a v5 module. Throws {@link VerifyError}. */
4369
+ async function runBaseline(options) {
4370
+ const file = resolve(options.file);
4371
+ const generate = {
4372
+ seed: options.seed ?? DEFAULT_OPTIONS.seed,
4373
+ walks: options.walks ?? DEFAULT_OPTIONS.walks,
4374
+ steps: options.steps ?? DEFAULT_OPTIONS.steps,
4375
+ maxSequences: options.maxSequences ?? DEFAULT_OPTIONS.maxSequences,
4376
+ ...(options.simplePaths === false && {
4377
+ simplePaths: false
4378
+ })
4379
+ };
4380
+ const events = options.events ?? {};
4381
+ const stubs = options.stubs ?? {};
4382
+ const {
4383
+ result,
4384
+ stderr
4385
+ } = await runChild(5, {
4386
+ mode: 'baseline',
4387
+ file,
4388
+ export: options.export,
4389
+ events,
4390
+ stubs,
4391
+ input: options.input,
4392
+ options: generate
4393
+ }, options.timeout);
4394
+ if (!result.ok) throw new VerifyError('baseline', result.code, result.code === 'CHILD_TIMEOUT' ? `${result.message}; ${generate.simplePaths === false ? '' : 'pass --no-simple-paths or '}pass a smaller --events set` : result.message, stderr, result.stack);
4395
+ return {
4396
+ version: 1,
4397
+ file,
4398
+ export: result.export,
4399
+ xstateVersion: result.xstateVersion,
4400
+ options: generate,
4401
+ events,
4402
+ stubs,
4403
+ input: options.input,
4404
+ sequences: result.sequences,
4405
+ traces: result.traces,
4406
+ unstubbed: result.unstubbed,
4407
+ eventsWithoutCases: result.eventsWithoutCases,
4408
+ generationErrors: result.generationErrors ?? [],
4409
+ nonSerializable: result.nonSerializable,
4410
+ notes: result.notes ?? []
4411
+ };
4412
+ }
4413
+ /** Replays the baseline's sequences on a v6 module and diffs the traces. */
4414
+ async function runVerify(options) {
4415
+ const file = resolve(options.file);
4416
+ const report = {
4417
+ status: 'error',
4418
+ v5: {
4419
+ file: options.v5 ? resolve(options.v5) : '',
4420
+ export: '',
4421
+ xstateVersion: ''
4422
+ },
4423
+ v6: {
4424
+ file,
4425
+ export: '',
4426
+ xstateVersion: ''
4427
+ },
4428
+ sequences: 0,
4429
+ stepsCompared: 0,
4430
+ diffs: [],
4431
+ intentional: 0,
4432
+ unstubbed: [],
4433
+ eventsWithoutCases: [],
4434
+ generationErrors: [],
4435
+ nonSerializable: 0,
4436
+ notes: []
4437
+ };
4438
+ let baseline = options.baseline;
4439
+ if (!baseline) {
4440
+ if (!options.v5) throw new TypeError('runVerify needs `v5` or `baseline`');
4441
+ try {
4442
+ baseline = await runBaseline({
4443
+ ...options,
4444
+ file: options.v5
4445
+ });
4446
+ } catch (err) {
4447
+ if (!(err instanceof VerifyError)) throw err;
4448
+ report.error = {
4449
+ phase: err.phase,
4450
+ code: err.code,
4451
+ message: err.message,
4452
+ stack: err.childStack,
4453
+ stderr: err.stderr
4454
+ };
4455
+ return report;
4456
+ }
4457
+ } else {
4458
+ if (options.events) report.notes.push('--events overrides the cases recorded in the baseline');
4459
+ if (options.stubs) report.notes.push('--stubs overrides the stubs recorded in the baseline');
4460
+ if (options.input !== undefined) report.notes.push('--input overrides the input recorded in the baseline');
4461
+ }
4462
+ report.v5 = {
4463
+ file: baseline.file,
4464
+ export: baseline.export,
4465
+ xstateVersion: baseline.xstateVersion
4466
+ };
4467
+ report.sequences = baseline.sequences.length;
4468
+ report.eventsWithoutCases = baseline.eventsWithoutCases;
4469
+ report.generationErrors = baseline.generationErrors ?? [];
4470
+ report.startError = startError(baseline);
4471
+ const {
4472
+ result,
4473
+ stderr
4474
+ } = await runChild(6, {
4475
+ mode: 'replay',
4476
+ file,
4477
+ export: options.export,
4478
+ events: options.events ?? baseline.events,
4479
+ stubs: options.stubs ?? baseline.stubs,
4480
+ input: options.input ?? baseline.input,
4481
+ options: baseline.options,
4482
+ sequences: baseline.sequences
4483
+ }, options.timeout);
4484
+ if (!result.ok) {
4485
+ report.error = {
4486
+ phase: 'replay',
4487
+ code: result.code,
4488
+ message: result.message,
4489
+ stack: result.stack,
4490
+ stderr
4491
+ };
4492
+ return report;
4493
+ }
4494
+ report.v6 = {
4495
+ file,
4496
+ export: result.export,
4497
+ xstateVersion: result.xstateVersion
4498
+ };
4499
+ report.unstubbed = [...new Set([...baseline.unstubbed, ...result.unstubbed])].sort();
4500
+ report.nonSerializable = baseline.nonSerializable + result.nonSerializable;
4501
+ for (const note of [...(baseline.notes ?? []), ...(result.notes ?? [])]) if (!report.notes.includes(note)) report.notes.push(note);
4502
+ const {
4503
+ diffs,
4504
+ stepsCompared
4505
+ } = diffTraces(baseline.sequences, baseline.traces, result.traces, options.compare ?? DEFAULT_FIELDS);
4506
+ report.diffs = diffs;
4507
+ report.stepsCompared = stepsCompared;
4508
+ report.intentional = diffs.filter(d => d.classification !== 'unclassified').length;
4509
+ report.status = diffs.length > report.intentional ? 'diverged' : 'equivalent';
4510
+ return report;
4511
+ }
4512
+
4513
+ const USAGE = `Usage:
4514
+ xstate-codemod baseline <v5-file> [--out <path>] [options]
4515
+ xstate-codemod verify <v5-file> <v6-file> [options]
4516
+ xstate-codemod verify --baseline <path> <v6-file> [options]
4517
+
4518
+ Options:
4519
+ --export <name> export to load (default: default, else first actor logic)
4520
+ --events <file> event payload cases: { "TYPE": [payload, ...] }
4521
+ --stubs <file> actor/delay stubs: { "actors": {...}, "delays": {...} }
4522
+ --input <file> JSON passed as createActor(logic, { input })
4523
+ --seed <n> random walk seed (default 1)
4524
+ --walks <n> random walks (default 25)
4525
+ --steps <n> steps per walk (default 12)
4526
+ --max-sequences <n> sequence cap (default 200)
4527
+ --no-simple-paths skip the graph's simple paths (use when baseline times out)
4528
+ --compare <fields> fields to compare, or +field to add (e.g. +actions)
4529
+ --timeout <ms> per child process (default 60000)
4530
+ --json print the JSON report only
4531
+ `;
4532
+ const startLine = (error, hasInput) => ` start: machine errored at start: ${error} ${hasInput ? '(the baseline exercised nothing past start)' : '(pass --input if the machine reads input)'}`;
4533
+ const EVENTS_ADVICE = ' (add payload cases with --events)';
4534
+
4535
+ /** The --events advice applies only to a guard that threw on an event sent without a payload. */
4536
+ function generationLine(error, eventsWithoutCases) {
4537
+ const type = /^event '(.*)' skipped: guard threw:/.exec(error)?.[1];
4538
+ const advice = type === undefined ? /paths skipped: (?!graph traversal hit its limit)/.test(error) && eventsWithoutCases.length > 0 : eventsWithoutCases.includes(type);
4539
+ return ` generation: ${error}${advice ? EVENTS_ADVICE : ''}`;
4540
+ }
4541
+ const readJson = file => JSON.parse(readFileSync(file, 'utf8'));
4542
+ const show = value => JSON.stringify(value) ?? 'undefined';
4543
+ const showStep = step => 'event' in step ? `send ${show(step.event)}` : `advance ${step.advance}ms`;
4544
+ function compareFields(value) {
4545
+ if (!value) return undefined;
4546
+ const fields = value.split(',').map(f => f.trim()).filter(Boolean);
4547
+ const added = fields.filter(f => f.startsWith('+')).map(f => f.slice(1));
4548
+ const set = fields.filter(f => !f.startsWith('+'));
4549
+ return [...(set.length ? set : DEFAULT_FIELDS), ...added];
4550
+ }
4551
+ function formatReport(report, hasInput) {
4552
+ const lines = [`verify: ${report.status}`];
4553
+ lines.push(` v5: ${report.v5.file} (export ${report.v5.export || '?'}, xstate ${report.v5.xstateVersion || '?'})`);
4554
+ lines.push(` v6: ${report.v6.file} (export ${report.v6.export || '?'}, xstate ${report.v6.xstateVersion || '?'})`);
4555
+ if (report.error) {
4556
+ lines.push(` error (${report.error.phase}): ${report.error.code}: ${report.error.message}`);
4557
+ if (report.error.stderr.trim()) lines.push(` stderr:\n${report.error.stderr.trimEnd()}`);
4558
+ return lines.join('\n') + '\n';
4559
+ }
4560
+ const unclassified = report.diffs.filter(d => d.classification === 'unclassified');
4561
+ lines.push(` sequences: ${report.sequences}, steps compared: ${report.stepsCompared}, unclassified diffs: ${unclassified.length}, intentional: ${report.intentional}`);
4562
+ if (report.startError) lines.push(startLine(report.startError, hasInput));
4563
+ if (report.unstubbed.length) lines.push(` unstubbed inline actors: ${report.unstubbed.join(', ')}`);
4564
+ if (report.eventsWithoutCases.length) lines.push(` events sent without payload (add cases with --events if guards or actions read the payload): ${report.eventsWithoutCases.join(', ')}`);
4565
+ for (const error of report.generationErrors) lines.push(generationLine(error, report.eventsWithoutCases));
4566
+ if (report.nonSerializable) lines.push(` non-serializable values: ${report.nonSerializable}`);
4567
+ for (const note of report.notes) lines.push(` note: ${note}`);
4568
+ unclassified.slice(0, 5).forEach((diff, i) => {
4569
+ lines.push('', `Diff ${i + 1}: ${diff.sequenceId} (${diff.origin}) step ${diff.step}, field ${diff.field}`);
4570
+ lines.push(' witness:');
4571
+ if (!diff.witness.length) lines.push(' (start)');
4572
+ diff.witness.forEach((step, j) => lines.push(` ${j + 1}. ${showStep(step)}`));
4573
+ lines.push(` expected (v5): ${show(diff.expected)}`);
4574
+ lines.push(` actual (v6): ${show(diff.actual)}`);
4575
+ if (diff.detail) lines.push(` detail: ${diff.detail}`);
4576
+ });
4577
+ return lines.join('\n') + '\n';
4578
+ }
4579
+
4580
+ /** `baseline` and `verify` commands; `argv[0]` is the command name. */
4581
+ async function verifyCli(argv) {
4582
+ const [command, ...rest] = argv;
4583
+ let parsed;
4584
+ try {
4585
+ parsed = parseArgs$1({
4586
+ args: rest,
4587
+ allowPositionals: true,
4588
+ options: {
4589
+ out: {
4590
+ type: 'string'
4591
+ },
4592
+ baseline: {
4593
+ type: 'string'
4594
+ },
4595
+ export: {
4596
+ type: 'string'
4597
+ },
4598
+ events: {
4599
+ type: 'string'
4600
+ },
4601
+ stubs: {
4602
+ type: 'string'
4603
+ },
4604
+ input: {
4605
+ type: 'string'
4606
+ },
4607
+ seed: {
4608
+ type: 'string'
4609
+ },
4610
+ walks: {
4611
+ type: 'string'
4612
+ },
4613
+ steps: {
4614
+ type: 'string'
4615
+ },
4616
+ 'max-sequences': {
4617
+ type: 'string'
4618
+ },
4619
+ 'no-simple-paths': {
4620
+ type: 'boolean'
4621
+ },
4622
+ compare: {
4623
+ type: 'string'
4624
+ },
4625
+ timeout: {
4626
+ type: 'string'
4627
+ },
4628
+ json: {
4629
+ type: 'boolean'
4630
+ },
4631
+ help: {
4632
+ type: 'boolean',
4633
+ short: 'h'
4634
+ }
4635
+ }
4636
+ });
4637
+ } catch (err) {
4638
+ process.stderr.write(`${err.message}\n${USAGE}`);
4639
+ return 1;
4640
+ }
4641
+ if (parsed.values.help) {
4642
+ process.stdout.write(USAGE);
4643
+ return 0;
4644
+ }
4645
+ const {
4646
+ values,
4647
+ positionals
4648
+ } = parsed;
4649
+ const int = value => value === undefined ? undefined : Number.parseInt(value, 10);
4650
+ const common = {
4651
+ export: values.export,
4652
+ seed: int(values.seed),
4653
+ walks: int(values.walks),
4654
+ steps: int(values.steps),
4655
+ maxSequences: int(values['max-sequences']),
4656
+ ...(values['no-simple-paths'] && {
4657
+ simplePaths: false
4658
+ }),
4659
+ timeout: int(values.timeout)
4660
+ };
4661
+ const numbers = [common.seed, common.walks, common.steps, common.maxSequences, common.timeout];
4662
+ if (numbers.some(n => n !== undefined && !Number.isFinite(n))) {
4663
+ process.stderr.write(`Numeric options must be integers\n${USAGE}`);
4664
+ return 1;
4665
+ }
4666
+ let events, stubs, input;
4667
+ try {
4668
+ events = values.events ? readJson(values.events) : undefined;
4669
+ stubs = values.stubs ? readJson(values.stubs) : undefined;
4670
+ input = values.input ? readJson(values.input) : undefined;
4671
+ } catch (err) {
4672
+ process.stderr.write(`${err.message}\n`);
4673
+ return 1;
4674
+ }
4675
+ if (command === 'baseline') {
4676
+ if (positionals.length !== 1) {
4677
+ process.stderr.write(USAGE);
4678
+ return 1;
4679
+ }
4680
+ let baseline;
4681
+ try {
4682
+ baseline = await runBaseline({
4683
+ ...common,
4684
+ file: positionals[0],
4685
+ events,
4686
+ stubs,
4687
+ input
4688
+ });
4689
+ } catch (err) {
4690
+ if (!(err instanceof VerifyError)) throw err;
4691
+ process.stderr.write(`baseline: ${err.code}: ${err.message}\n${err.stderr}`);
4692
+ return 4;
4693
+ }
4694
+ const out = values.out ?? join('.xstate-migrate', `${baseline.export}.baseline.json`);
4695
+ mkdirSync(dirname(out), {
4696
+ recursive: true
4697
+ });
4698
+ writeFileSync(out, JSON.stringify(baseline, null, 2) + '\n');
4699
+ const error = startError(baseline);
4700
+ if (values.json) process.stdout.write(JSON.stringify({
4701
+ out,
4702
+ sequences: baseline.sequences.length,
4703
+ startError: error
4704
+ }) + '\n');else process.stdout.write(`baseline: ${baseline.sequences.length} sequences from ${baseline.file} (export ${baseline.export}) → ${out}\n` + (error ? startLine(error, input !== undefined) + '\n' : '') + baseline.generationErrors.map(e => generationLine(e, baseline.eventsWithoutCases) + '\n').join('') + (baseline.notes ?? []).map(note => ` note: ${note}\n`).join(''));
4705
+ return 0;
4706
+ }
4707
+ if (command === 'verify') {
4708
+ const expected = values.baseline ? 1 : 2;
4709
+ if (positionals.length !== expected) {
4710
+ process.stderr.write(USAGE);
4711
+ return 1;
4712
+ }
4713
+ let baseline;
4714
+ try {
4715
+ baseline = values.baseline ? readJson(values.baseline) : undefined;
4716
+ } catch (err) {
4717
+ process.stderr.write(`${err.message}\n`);
4718
+ return 1;
4719
+ }
4720
+ const report = await runVerify({
4721
+ ...common,
4722
+ baseline,
4723
+ v5: baseline ? undefined : positionals[0],
4724
+ file: positionals[expected - 1],
4725
+ events,
4726
+ stubs,
4727
+ input,
4728
+ compare: compareFields(values.compare)
4729
+ });
4730
+ process.stdout.write(values.json ? JSON.stringify(report, null, 2) + '\n' : formatReport(report, (input ?? baseline?.input) !== undefined));
4731
+ return report.status === 'equivalent' ? 0 : report.status === 'diverged' ? 3 : 4;
4732
+ }
4733
+ process.stderr.write(USAGE);
4734
+ return 1;
867
4735
  }
868
4736
 
869
- export { applyToSourceFile, main, parseArgs, renameImports, reportIdPathTargets, reportRemovedApis, resolveFiles, run, selectTransforms, stringTargets, transforms, transformsByName, typesToSchemas };
4737
+ export { VerifyError, applyToSourceFile, childMain, lowerActions, main, parseArgs, renameImports, reportIdPathTargets, reportRemovedApis, resolveFiles, run, runBaseline, runVerify, selectTransforms, stringTargets, transforms, transformsByName, typesToSchemas, verifyCli };