{"record":{"id":"006281b9caaa64e2","repo":"ReactiveX/rxjs","slug":"output-name-must-identify-a-file-below-outputroot","errorCode":null,"errorMessage":"Output name must identify a file below outputRoot: ${outputName}","messagePattern":"Output name must identify a file below outputRoot: (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/migrate/src/node.ts","lineNumber":179,"sourceCode":"  } catch (error: unknown) {\n    if (!isMissingPathError(error)) throw error;\n  }\n}\n\nexport async function migrateTestFiles(options: MigrateFilesOptions): Promise<readonly MigratedFile[]> {\n  if (options.write && !options.outputRoot) {\n    throw new Error('outputRoot is required when write is enabled.');\n  }\n  const plan = await planMigrationFiles(options);\n  return options.write ? applyMigrationPlan(plan, { overwrite: options.overwrite }) : plan.files;\n}\n\nfunction safeOutputPath(outputRoot: string, outputName: string): string {\n  if (!outputName || outputName === '.' || isAbsolute(outputName)) {\n    throw new Error(`Output name must be a non-empty relative path: ${outputName || '<empty>'}`);\n  }\n  const outputPath = resolve(outputRoot, outputName);\n  if (outputPath === outputRoot) throw new Error(`Output name must identify a file below outputRoot: ${outputName}`);\n  assertContained(outputRoot, outputPath, `Output path is outside outputRoot: ${outputName}`);\n  return outputPath;\n}\n\nfunction assertContained(root: string, candidate: string, message: string): void {\n  const localPath = relative(root, candidate);\n  if (localPath === '' || (localPath !== '..' && !localPath.startsWith(`..${sep}`) && !isAbsolute(localPath))) return;\n  throw new Error(message);\n}\n\nasync function canonicalDirectory(path: string, label: string): Promise<string> {\n  const canonicalPath = await realpath(path);\n  const pathStats = await stat(canonicalPath);\n  if (!pathStats.isDirectory()) throw new Error(`${label} is not a directory: ${path}`);\n  return canonicalPath;\n}\n\nasync function canonicalFutureDirectory(path: string, label: string): Promise<string> {","sourceCodeStart":161,"sourceCodeEnd":197,"githubUrl":"https://github.com/ReactiveX/rxjs/blob/54796b38a57e6309f9861e174737479bb3f63f61/packages/migrate/src/node.ts#L161-L197","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","solutions":["Ensure outputName ends with a concrete filename (with extension) after any segment manipulation","Compute the name from path.relative(sourceRoot, sourcePath) so it always retains the basename","Add a unit assertion that your outputName output never resolves to the outputRoot"],"exampleFix":"// before\noutputName: ({ sourcePath }) => path.dirname(path.relative(sourceRoot, sourcePath)),\n\n// after\noutputName: ({ sourcePath }) => path.relative(sourceRoot, sourcePath).replace(/\\.ts$/, '.next.ts'),","handlingStrategy":"validation","validationCode":"import { resolve } from 'node:path';\nfunction nameIdentifiesFile(outputRoot: string, name: string): boolean {\n  return resolve(outputRoot, name) !== resolve(outputRoot);\n}","typeGuard":"function isFileBelowRoot(outputRoot: string, name: unknown): name is string {\n  return typeof name === 'string' && name.length > 0 && name !== '.' && resolve(outputRoot, name) !== resolve(outputRoot);\n}","tryCatchPattern":null,"preventionTips":["Always include the source basename (with extension) in generated output names","Avoid segment-cancelling names like 'foo/..' in outputName logic"],"tags":["migrate","filesystem","path-validation","configuration"],"backgroundTag":"invalid-relative-path","analyzedSha":"54796b38a57e6309f9861e174737479bb3f63f61","analyzedAt":"2026-08-28T10:21:27.410Z","schemaVersion":2},"datasetVersion":"2026-08-28T11:17:15.048Z"}