ReactiveX/rxjs · error · Error

Output name must be a non-empty relative path: ${outputName

Error message

Output name must be a non-empty relative path: ${outputName || '<empty>'}

What it means

safeOutputPath builds each output path by resolving a relative name against outputRoot, and rejects names that are empty, '.', or absolute. Absolute or degenerate names would either escape the outputRoot contract or fail to identify a file, so they are treated as programming errors in the outputName configuration.

Source

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

    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;
  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;

View on GitHub (pinned to 54796b38a5)

Solutions

  1. Make outputName always return a non-empty relative path with a file extension
  2. If the callback computes absolute paths, strip the outputRoot prefix first (path.relative(outputRoot, p))
  3. Guard the callback's fallback branch so it never returns undefined for unmatched files

Example fix

// before
outputName: ({ sourcePath }) => absoluteNames.get(sourcePath),

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

Strategy: validation

Validate before calling

import { isAbsolute } from 'node:path';
function validOutputName(name: string): boolean {
  return Boolean(name) && name !== '.' && !isAbsolute(name);
}

Type guard

function isRelativeOutputName(name: unknown): name is string {
  return typeof name === 'string' && name.length > 0 && name !== '.' && !isAbsolute(name);
}

Prevention

When it happens

Trigger: A custom options.outputName function returning '', '.', or an absolute path like '/tmp/out.spec.ts' (or a value starting with '/' or a Windows drive letter); also triggered when a string outputName is empty.

Common situations: A name-mapping callback that returns undefined/null coerced to string on edge cases, or one that resolves names against the wrong root and returns an absolute path.

Related errors


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