ReactiveX/rxjs · error · Error

Refusing to overwrite a symbolic link: ${outputPath}

Error message

Refusing to overwrite a symbolic link: ${outputPath}

What it means

assertWritableOutput uses lstat, which inspects the path itself rather than its target, and refuses to overwrite when the existing output path is a symbolic link — even with overwrite enabled. Overwriting a symlink would replace the link with a regular file and could damage setups where outputs are deliberately linked into another tree, so the migrator fail-closes.

Source

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

  const canonicalOutputRoot = await canonicalFutureDirectory(outputRoot, 'outputRoot');
  const canonicalOutputs = new Set<string>();
  for (const { sourcePath, outputPath, result } of plan.files) {
    if (result.status === 'refused') throw new Error(`Migration result was refused for source: ${sourcePath}`);
    const resolvedOutputPath = resolve(outputPath);
    if (resolvedOutputPath === outputRoot) throw new Error(`Output path must identify a file below outputRoot: ${outputPath}`);
    assertContained(outputRoot, resolvedOutputPath, `Output path is outside outputRoot: ${outputPath}`);
    const canonicalOutputPath = await canonicalFuturePath(resolvedOutputPath);
    assertContained(canonicalOutputRoot, canonicalOutputPath, `Output path resolves outside outputRoot: ${outputPath}`);
    if (canonicalOutputs.has(canonicalOutputPath)) throw new Error(`Duplicate output path: ${outputPath}`);
    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>'}`);

View on GitHub (pinned to 54796b38a5)

Solutions

  1. Manually remove the offending symlink (rm <path>) so the migrator creates a regular file
  2. Retarget the plan so outputPath points at the real file location instead of the link
  3. Change your outputRoot to a directory that is not symlink-managed

Example fix

# before
applyMigrationPlan(plan) // throws: Refusing to overwrite a symbolic link

# after
rm packages/migrated/old-link.spec.ts
applyMigrationPlan(plan)
Defensive patterns

Strategy: validation

Validate before calling

import { lstat } from 'node:fs/promises';
async function noSymlinkOutputs(plan: { files: { outputPath: string }[] }): Promise<boolean> {
  for (const f of plan.files) {
    try { if ((await lstat(f.outputPath)).isSymbolicLink()) return false; }
    catch (e) { if ((e as NodeJS.ErrnoException).code !== 'ENOENT') throw e; }
  }
  return true;
}

Try / catch

try {
  await applyMigrationPlan(plan);
} catch (e) {
  if (e instanceof Error && e.message.startsWith('Refusing to overwrite a symbolic link')) {
    // remove the symlink manually, then retry
  } else throw e;
}

Prevention

When it happens

Trigger: Calling applyMigrationPlan (or migrateTestFiles with write:true) when a planned outputPath exists and lstat reports it as a symlink. Passing overwrite:true does NOT bypass this.

Common situations: Output directories managed with symlinks (dotfile-style layouts, monorepo package linking, containers with linked volumes), or a previous partial migration whose outputs were symlinked elsewhere.

Related errors


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