ReactiveX/rxjs · error · Error

Output path must identify a file below outputRoot: ${outputP

Error message

Output path must identify a file below outputRoot: ${outputPath}

What it means

Before writing, preflightPlanOutputs resolves each plan entry's outputPath and verifies it is strictly below outputRoot. If the resolved output path equals outputRoot itself, the migrator would be trying to write a file onto the directory root, so it throws. This protects the destination tree from being clobbered by a degenerate path.

Source

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

  plan: MigrationFilePlan,
  options: ApplyMigrationPlanOptions = {}
): Promise<readonly MigratedFile[]> {
  await preflightPlanOutputs(plan, options);
  for (const { outputPath, result } of plan.files) {
    await mkdir(dirname(outputPath), { recursive: true });
    await writeFile(outputPath, result.code, 'utf8');
  }
  return plan.files;
}

async function preflightPlanOutputs(plan: MigrationFilePlan, options: ApplyMigrationPlanOptions): Promise<void> {
  const outputRoot = resolve(plan.outputRoot);
  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;
  }

View on GitHub (pinned to 54796b38a5)

Solutions

  1. Build plans with planMigrationFiles rather than constructing them by hand, so safeOutputPath enforces file-below-root
  2. Fix the offending entry so outputPath points at a concrete file beneath outputRoot
  3. If post-processing plan paths, assert each is relative and non-empty before applying

Example fix

// before
plan.files[0].outputPath = plan.outputRoot;

// after
plan.files[0].outputPath = path.join(plan.outputRoot, ' migrated.spec.ts');
Defensive patterns

Strategy: validation

Validate before calling

const root = resolve(plan.outputRoot);
const ok = plan.files.every(f => resolve(f.outputPath) !== root);
if (ok) await applyMigrationPlan(plan);

Type guard

function hasFileOutputsBelowRoot(plan: { outputRoot: string; files: { outputPath: string }[] }): boolean {
  const root = resolve(plan.outputRoot);
  return plan.files.every(f => resolve(f.outputPath) !== root);
}

Prevention

When it happens

Trigger: Calling applyMigrationPlan with a hand-built or mutated MigrationFilePlan whose files[].outputPath resolves exactly to plan.outputRoot.

Common situations: Constructing a plan manually (instead of via planMigrationFiles) with outputPath set to '.' or to the outputRoot string; or code that rewrites plan paths and accidentally collapses one onto the root.

Related errors


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