ReactiveX/rxjs · error · Error

Output name must identify a file below outputRoot: ${outputN

Error message

Output name must identify a file below outputRoot: ${outputName}

What it means

After resolving outputName against outputRoot, safeOutputPath verifies the result is strictly below the root. A name like 'sub/..' (or any name whose segments cancel out to the root itself) resolves back to outputRoot, which is a directory, not a writable output file, so it is rejected. This is distinct from the outside-root case, which assertContained handles next.

Source

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

  } 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;
  throw new Error(message);
}

async function canonicalDirectory(path: string, label: string): Promise<string> {
  const canonicalPath = await realpath(path);
  const pathStats = await stat(canonicalPath);
  if (!pathStats.isDirectory()) throw new Error(`${label} is not a directory: ${path}`);
  return canonicalPath;
}

async function canonicalFutureDirectory(path: string, label: string): Promise<string> {

View on GitHub (pinned to 54796b38a5)

Solutions

  1. Ensure outputName ends with a concrete filename (with extension) after any segment manipulation
  2. Compute the name from path.relative(sourceRoot, sourcePath) so it always retains the basename
  3. Add a unit assertion that your outputName output never resolves to the outputRoot

Example fix

// before
outputName: ({ sourcePath }) => path.dirname(path.relative(sourceRoot, sourcePath)),

// after
outputName: ({ sourcePath }) => path.relative(sourceRoot, sourcePath).replace(/\.ts$/, '.next.ts'),
Defensive patterns

Strategy: validation

Validate before calling

import { resolve } from 'node:path';
function nameIdentifiesFile(outputRoot: string, name: string): boolean {
  return resolve(outputRoot, name) !== resolve(outputRoot);
}

Type guard

function isFileBelowRoot(outputRoot: string, name: unknown): name is string {
  return typeof name === 'string' && name.length > 0 && name !== '.' && resolve(outputRoot, name) !== resolve(outputRoot);
}

Prevention

When it happens

Trigger: options.outputName returning a path whose resolution equals outputRoot, e.g. 'foo/..' or '.'-equivalent segment chains like 'a/../..'-style collapse when outputRoot is a parent, or the literal name '.' (that specific case is caught one line earlier).

Common situations: A name-deriving callback that strips extensions and segments (e.g. removes everything after the last slash) and ends up with an empty/identity path; naive string manipulation on relative paths.

Related errors


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