facebook/flow · error · InvalidReplacementError

import/export cannot be replaced into a

Error message

import/export cannot be replaced into a ${replacementParent.parent.type}.

What it means

InvalidReplacementError thrown by flow-transform's replaceStatementWithMany when the replacement nodes include an import/export (ModuleDeclaration) but the statement being replaced does not live where module declarations are legal. The same `isValidModuleDeclarationParent` check as insertStatement applies: only `Program` (or a BlockStatement inside `DeclareModule`) may receive module declarations.

Solutions

  1. Split the operation: replace the nested statement with non-module statements, and insert the import separately relative to a top-level Program.body statement.
  2. Only replace-with-imports when `stmt.parent.type === 'Program'`.
  3. Collect imports during traversal and emit them once at Program level.

Example fix

// before
mutate.replaceStatementWithMany(nestedStmt, [buildImport(), buildStatement()]); // throws

// after
mutate.replaceStatementWithMany(nestedStmt, [buildStatement()]);
mutate.insertStatementBefore(firstTopLevelStmt, buildImport());
Defensive patterns

Strategy: type-guard

Validate before calling

function canReplaceWithModuleDeclarations(anchor) {
  const p = anchor.parent;
  return p.type === 'Program' ||
    (p.type === 'BlockStatement' && p.parent?.type === 'DeclareModule');
}

if (newNodes.some(isModuleDeclaration) && !canReplaceWithModuleDeclarations(anchor)) {
  // split: hoist module declarations to Program level
  const stmts = newNodes.filter(n => !isModuleDeclaration(n));
  mutate.replaceStatementWithMany(anchor, stmts);
  mutate.insertStatementBefore(firstTopLevelStatement, moduleNodes[0]);
} else {
  mutate.replaceStatementWithMany(anchor, newNodes);
}

Type guard

function isModuleDeclaration(n): boolean {
  return ['ImportDeclaration', 'ExportNamedDeclaration',
    'ExportDefaultDeclaration', 'ExportAllDeclaration'].includes(n.type);
}

Prevention

When it happens

Trigger: `mutate.replaceStatementWithMany(nestedStmt, [detachedImport, ...])` where nestedStmt.parent is a function BlockStatement, SwitchCase consequent, or IfStatement branch - i.e. replacing a non-top-level statement with an import/export.

Common situations: Codemods converting a nested statement to a module import (e.g. replacing an inner require() call's statement with an import); forgetting to hoist when replacing a statement that sits inside a function body.

Related errors


AI-assisted analysis of facebook/flow@f88ac94bcf (2026-08-20). Data as JSON: /api/errors/e9ffe57b4eabdcd6. Report an issue: GitHub.

Appendix: source

Thrown at packages/flow-transform/src/transform/mutations/ReplaceStatementWithMany.js:63

    nodesToReplaceWith,
    keepComments: options?.keepComments ?? false,
  };
}

export function performReplaceStatementWithManyMutation(
  mutationContext: MutationContext,
  mutation: ReplaceStatementWithManyMutation,
): ESNode {
  const replacementParent = getStatementParent(mutation.target);

  // enforce that if we are replacing with module declarations - they are being inserted in a valid location
  if (
    !isValidModuleDeclarationParent(
      replacementParent.parent,
      mutation.nodesToReplaceWith,
    )
  ) {
    throw new InvalidReplacementError(
      `import/export cannot be replaced into a ${replacementParent.parent.type}.`,
    );
  }

  mutationContext.markDeletion(mutation.target);
  mutationContext.markMutation(replacementParent.parent, replacementParent.key);

  if (mutation.keepComments) {
    // attach comments to the very first replacement node
    moveCommentsToNewNode(mutation.target, mutation.nodesToReplaceWith[0]);
  }

  if (replacementParent.type === 'array') {
    const parent: interface {
      [string]: ReadonlyArray<DetachedNode<Statement | ModuleDeclaration>>,
    } = replacementParent.parent;
    parent[replacementParent.key] = astArrayMutationHelpers.replaceInArray(
      parent[replacementParent.key],

View on GitHub (pinned to f88ac94bcf)