ReactiveX/rxjs · error · Error

outputRoot is required when write is enabled.

Error message

outputRoot is required when write is enabled.

What it means

migrateTestFiles is the one-call convenience wrapper: it plans and (when options.write is true) applies the plan. Writing requires a concrete destination, so the very first thing the function checks is that outputRoot is provided whenever write is enabled; without it the migrator would have to write files alongside their sources with no way to control the destination.

Source

Thrown at packages/migrate/src/node.ts:168

    canonicalOutputs.add(canonicalOutputPath);
    await assertWritableOutput(resolvedOutputPath, options.overwrite ?? false);
  }
}

async function assertWritableOutput(outputPath: string, overwrite: boolean): Promise<void> {
  try {
    const outputStats = await lstat(outputPath);
    if (outputStats.isSymbolicLink()) throw new Error(`Refusing to overwrite a symbolic link: ${outputPath}`);
    if (!outputStats.isFile()) throw new Error(`Output path is not a regular file: ${outputPath}`);
    if (!overwrite) throw new Error(`Output path already exists; enable overwrite explicitly: ${outputPath}`);
  } catch (error: unknown) {
    if (!isMissingPathError(error)) throw error;
  }
}

export async function migrateTestFiles(options: MigrateFilesOptions): Promise<readonly MigratedFile[]> {
  if (options.write && !options.outputRoot) {
    throw new Error('outputRoot is required when write is enabled.');
  }
  const plan = await planMigrationFiles(options);
  return options.write ? applyMigrationPlan(plan, { overwrite: options.overwrite }) : plan.files;
}

function safeOutputPath(outputRoot: string, outputName: string): string {
  if (!outputName || outputName === '.' || isAbsolute(outputName)) {
    throw new Error(`Output name must be a non-empty relative path: ${outputName || '<empty>'}`);
  }
  const outputPath = resolve(outputRoot, outputName);
  if (outputPath === outputRoot) throw new Error(`Output name must identify a file below outputRoot: ${outputName}`);
  assertContained(outputRoot, outputPath, `Output path is outside outputRoot: ${outputName}`);
  return outputPath;
}

function assertContained(root: string, candidate: string, message: string): void {
  const localPath = relative(root, candidate);
  if (localPath === '' || (localPath !== '..' && !localPath.startsWith(`..${sep}`) && !isAbsolute(localPath))) return;

View on GitHub (pinned to 54796b38a5)

Solutions

  1. Add outputRoot pointing at the destination directory for the migrated files
  2. If you only wanted a dry run, remove write:true instead of adding outputRoot
  3. Derive outputRoot from sourceRoot explicitly (e.g. path.join(sourceRoot, 'migrated')) if you want a predictable default

Example fix

// before
await migrateTestFiles({ files, sourceRoot, write: true });

// after
await migrateTestFiles({ files, sourceRoot, outputRoot: path.join(sourceRoot, 'migrated'), write: true });
Defensive patterns

Strategy: type-guard

Validate before calling

if (options.write && !options.outputRoot) {
  throw new Error('outputRoot is required when write is enabled.'); // fail fast with your own message
}

Type guard

interface WriteOptions { write?: boolean; outputRoot?: string }
function isWritableConfig(o: WriteOptions): o is WriteOptions & { write: true; outputRoot: string } {
  return o.write === true && typeof o.outputRoot === 'string' && o.outputRoot.length > 0;
}

Prevention

When it happens

Trigger: Calling migrateTestFiles({ write: true }) without an outputRoot property (or with it set to undefined/empty).

Common situations: Adapting a dry-run script (which legitimately omits outputRoot) to actually write by adding write:true; typoing the option name; assuming outputRoot defaults to sourceRoot when writing.

Related errors


AI-assisted analysis of ReactiveX/rxjs@54796b38a5 (2026-08-28). Data as JSON: /api/errors/94e145dd83a9aa47. Report an issue: GitHub.