{"record":{"id":"f2fa025a8caaa850","repo":"ReactiveX/rxjs","slug":"output-path-must-identify-a-file-below-outputroot","errorCode":null,"errorMessage":"Output path must identify a file below outputRoot: ${outputPath}","messagePattern":"Output path must identify a file below outputRoot: (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/migrate/src/node.ts","lineNumber":145,"sourceCode":"  plan: MigrationFilePlan,\n  options: ApplyMigrationPlanOptions = {}\n): Promise<readonly MigratedFile[]> {\n  await preflightPlanOutputs(plan, options);\n  for (const { outputPath, result } of plan.files) {\n    await mkdir(dirname(outputPath), { recursive: true });\n    await writeFile(outputPath, result.code, 'utf8');\n  }\n  return plan.files;\n}\n\nasync function preflightPlanOutputs(plan: MigrationFilePlan, options: ApplyMigrationPlanOptions): Promise<void> {\n  const outputRoot = resolve(plan.outputRoot);\n  const canonicalOutputRoot = await canonicalFutureDirectory(outputRoot, 'outputRoot');\n  const canonicalOutputs = new Set<string>();\n  for (const { sourcePath, outputPath, result } of plan.files) {\n    if (result.status === 'refused') throw new Error(`Migration result was refused for source: ${sourcePath}`);\n    const resolvedOutputPath = resolve(outputPath);\n    if (resolvedOutputPath === outputRoot) throw new Error(`Output path must identify a file below outputRoot: ${outputPath}`);\n    assertContained(outputRoot, resolvedOutputPath, `Output path is outside outputRoot: ${outputPath}`);\n    const canonicalOutputPath = await canonicalFuturePath(resolvedOutputPath);\n    assertContained(canonicalOutputRoot, canonicalOutputPath, `Output path resolves outside outputRoot: ${outputPath}`);\n    if (canonicalOutputs.has(canonicalOutputPath)) throw new Error(`Duplicate output path: ${outputPath}`);\n    canonicalOutputs.add(canonicalOutputPath);\n    await assertWritableOutput(resolvedOutputPath, options.overwrite ?? false);\n  }\n}\n\nasync function assertWritableOutput(outputPath: string, overwrite: boolean): Promise<void> {\n  try {\n    const outputStats = await lstat(outputPath);\n    if (outputStats.isSymbolicLink()) throw new Error(`Refusing to overwrite a symbolic link: ${outputPath}`);\n    if (!outputStats.isFile()) throw new Error(`Output path is not a regular file: ${outputPath}`);\n    if (!overwrite) throw new Error(`Output path already exists; enable overwrite explicitly: ${outputPath}`);\n  } catch (error: unknown) {\n    if (!isMissingPathError(error)) throw error;\n  }","sourceCodeStart":127,"sourceCodeEnd":163,"githubUrl":"https://github.com/ReactiveX/rxjs/blob/54796b38a57e6309f9861e174737479bb3f63f61/packages/migrate/src/node.ts#L127-L163","documentation":"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.","triggerScenarios":"Calling applyMigrationPlan with a hand-built or mutated MigrationFilePlan whose files[].outputPath resolves exactly to plan.outputRoot.","commonSituations":"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.","solutions":["Build plans with planMigrationFiles rather than constructing them by hand, so safeOutputPath enforces file-below-root","Fix the offending entry so outputPath points at a concrete file beneath outputRoot","If post-processing plan paths, assert each is relative and non-empty before applying"],"exampleFix":"// before\nplan.files[0].outputPath = plan.outputRoot;\n\n// after\nplan.files[0].outputPath = path.join(plan.outputRoot, ' migrated.spec.ts');","handlingStrategy":"validation","validationCode":"const root = resolve(plan.outputRoot);\nconst ok = plan.files.every(f => resolve(f.outputPath) !== root);\nif (ok) await applyMigrationPlan(plan);","typeGuard":"function hasFileOutputsBelowRoot(plan: { outputRoot: string; files: { outputPath: string }[] }): boolean {\n  const root = resolve(plan.outputRoot);\n  return plan.files.every(f => resolve(f.outputPath) !== root);\n}","tryCatchPattern":null,"preventionTips":["Never hand-construct MigrationFilePlan; use planMigrationFiles","When rewriting plan paths, keep them relative to outputRoot and non-empty"],"tags":["migrate","filesystem","path-validation"],"backgroundTag":"output-path-validation-failed","analyzedSha":"54796b38a57e6309f9861e174737479bb3f63f61","analyzedAt":"2026-08-28T10:21:27.410Z","schemaVersion":2},"datasetVersion":"2026-08-28T11:17:15.048Z"}