{"version":3,"file":"snapshot_path_limits.mjs","names":[],"sources":["../../src/server/snapshot_path_limits.ts"],"sourcesContent":["import { createHash } from 'node:crypto'\nimport { basename, dirname, extname, join, resolve } from 'pathe'\n\n/** Classic Win32 `MAX_PATH` includes NUL; tools often treat ~259 as usable. */\nexport const WIN_MAX_PATH = 260\n\n/**\n * Budget under {@link WIN_MAX_PATH} to leave margin for tooling and long `projectRoot` prefixes.\n */\nexport const WIN_SNAPSHOT_PATH_BUDGET = 250\n\n/** NTFS maximum length of a single path component (directory or file name). */\nexport const NTFS_MAX_FILENAME = 255\n\n/** Max length of the readable prefix taken from the original basename (tersify-style). */\nconst SNAPSHOT_SUBPATH_PREFIX_MAX_LEN = 24\n\n/**\n * Minimal probe file under the snapshot folder — detects when the directory prefix alone is too long.\n */\nconst SNAPSHOT_SUBPATH_PROBE_MIN_BASENAME = '0.png'\n\nexport function absoluteSnapshotFilePath(projectRoot: string, baselineDir: string, snapshotFilename: string): string {\n\treturn resolve(projectRoot, join(baselineDir, snapshotFilename))\n}\n\n/**\n * @returns true when the legacy absolute path should be shortened for Windows-safe tooling.\n */\nexport function legacySnapshotPathExceedsLimit(absolutePath: string): boolean {\n\tif (absolutePath.length >= WIN_SNAPSHOT_PATH_BUDGET) return true\n\tif (basename(absolutePath).length > NTFS_MAX_FILENAME) return true\n\treturn false\n}\n\n/**\n * First readable token of a snapshot basename stem (before the first `.`), similar in spirit to\n * [tersify](https://github.com/unional/tersify) terse labels — keeps a short human hint, not the whole name.\n */\nfunction snapshotBasenameTersifyPrefix(stem: string): string {\n\tif (!stem) return 'snap'\n\tconst dot = stem.indexOf('.')\n\tconst first = dot === -1 ? stem : stem.slice(0, dot)\n\tconst cleaned = first.replace(/[^\\w-]/g, '')\n\treturn (cleaned || 'snap').slice(0, SNAPSHOT_SUBPATH_PREFIX_MAX_LEN)\n}\n\n/**\n * Keeps all directory segments of {@link rawSnapshotSubpath}; only the final filename is replaced with\n * `{prefix}-{hash12}{ext}` where `prefix` comes from the first segment of the original stem (e.g. `Button.stories.ts` → `Button`).\n */\nexport function formatCondensedSnapshotSubpath(rawSnapshotSubpath: string): string {\n\tconst norm = rawSnapshotSubpath.replace(/\\\\/g, '/')\n\tconst dir = dirname(norm)\n\tconst file = basename(norm)\n\tconst ext = extname(file)\n\tconst stem = ext ? file.slice(0, -ext.length) : file\n\tconst prefix = snapshotBasenameTersifyPrefix(stem)\n\tconst h = createHash('sha256').update(norm).digest('hex').slice(0, 12)\n\tconst shortFile = `${prefix}-${h}${ext}`\n\tif (dir === '.' || dir === '') return shortFile\n\treturn join(dir, shortFile)\n}\n\n/**\n * `snapshotBaselineRootAbs` is the absolute `__baselines__` root; with `rawSnapshotSubpath` this yields the\n * test-module snapshot directory. Only checks whether that layout can host a tiny baseline file; long\n * `taskId-key.png` paths are handled with {@link resolveLegacySnapshotFileRelativePath} using real names.\n */\nfunction rawSnapshotSubpathExceedsSafePathBudget(snapshotBaselineRootAbs: string, rawSnapshotSubpath: string): boolean {\n\tconst dirAbs = resolve(snapshotBaselineRootAbs, rawSnapshotSubpath)\n\n\tif (legacySnapshotPathExceedsLimit(resolve(dirAbs, SNAPSHOT_SUBPATH_PROBE_MIN_BASENAME))) return true\n\n\tfor (const segment of rawSnapshotSubpath.split(/[/\\\\]/)) {\n\t\tif (segment.length > NTFS_MAX_FILENAME) return true\n\t}\n\n\treturn false\n}\n\n/**\n * Returns {@link rawSnapshotSubpath} or a tersified final segment when `shortenLongSnapshotPaths` is on and\n * `snapshotBaselineRootAbs` + subpath + a minimal `0.png` would exceed {@link legacySnapshotPathExceedsLimit},\n * or a subpath segment exceeds NTFS length. Long snapshot **filenames** (`taskId-key.png`) are shortened\n * separately via {@link resolveLegacySnapshotFileRelativePath}. Baseline, result, and diff trees share the\n * same resolved subpath via {@link getTaskSubpath}.\n */\nexport function resolveSnapshotSubpathWithinLimits(params: {\n\tsnapshotBaselineRootAbs: string\n\trawSnapshotSubpath: string\n\tshortenLongSnapshotPaths: boolean\n}): string {\n\tconst { snapshotBaselineRootAbs, rawSnapshotSubpath, shortenLongSnapshotPaths } = params\n\tif (!shortenLongSnapshotPaths) return rawSnapshotSubpath\n\n\tif (!rawSnapshotSubpathExceedsSafePathBudget(snapshotBaselineRootAbs, rawSnapshotSubpath)) {\n\t\treturn rawSnapshotSubpath\n\t}\n\n\treturn formatCondensedSnapshotSubpath(rawSnapshotSubpath)\n}\n\n/**\n * When `shortenLongSnapshotPaths` is on and the legacy `{@link getLegacySnapshotFilename}` path exceeds\n * {@link legacySnapshotPathExceedsLimit}, rewrites only the final path segment (after the last `/`) like\n * {@link formatCondensedSnapshotSubpath}, so nested `taskId` directories stay intact and `…-*.png` globs keep working.\n */\nexport function resolveLegacySnapshotFileRelativePath(params: {\n\tprojectRoot: string\n\tbaselineDir: string\n\tlegacySnapshotFilename: string\n\tshortenLongSnapshotPaths: boolean\n}): string {\n\tconst { projectRoot, baselineDir, legacySnapshotFilename, shortenLongSnapshotPaths } = params\n\tif (!shortenLongSnapshotPaths) return legacySnapshotFilename\n\n\tconst norm = legacySnapshotFilename.replace(/\\\\/g, '/')\n\tconst abs = absoluteSnapshotFilePath(projectRoot, baselineDir, norm)\n\tif (!legacySnapshotPathExceedsLimit(abs)) return legacySnapshotFilename\n\n\treturn formatCondensedSnapshotSubpath(norm)\n}\n\nexport function getLegacySnapshotFilename(params: {\n\ttaskId: string\n\texplicitSnapshotKey: string | undefined\n\tdefaultSnapshotKey: string | undefined\n\ttaskCount: number\n}): string {\n\tconst key =\n\t\tparams.explicitSnapshotKey ??\n\t\t(typeof params.defaultSnapshotKey === 'string' ? params.defaultSnapshotKey : String(params.taskCount))\n\treturn `${params.taskId}-${key}.png`\n}\n"],"mappings":";;;;;;;AASA,MAAa,2BAA2B;;AAGxC,MAAa,oBAAoB;;AAGjC,MAAM,kCAAkC;;;;AAKxC,MAAM,sCAAsC;AAE5C,SAAgB,yBAAyB,aAAqB,aAAqB,kBAAkC;AACpH,QAAO,QAAQ,aAAa,KAAK,aAAa,iBAAiB,CAAC;;;;;AAMjE,SAAgB,+BAA+B,cAA+B;AAC7E,KAAI,aAAa,UAAU,yBAA0B,QAAO;AAC5D,KAAI,SAAS,aAAa,CAAC,SAAS,kBAAmB,QAAO;AAC9D,QAAO;;;;;;AAOR,SAAS,8BAA8B,MAAsB;AAC5D,KAAI,CAAC,KAAM,QAAO;CAClB,MAAM,MAAM,KAAK,QAAQ,IAAI;AAG7B,UAFc,QAAQ,KAAK,OAAO,KAAK,MAAM,GAAG,IAAI,EAC9B,QAAQ,WAAW,GAAG,IACzB,QAAQ,MAAM,GAAG,gCAAgC;;;;;;AAOrE,SAAgB,+BAA+B,oBAAoC;CAClF,MAAM,OAAO,mBAAmB,QAAQ,OAAO,IAAI;CACnD,MAAM,MAAM,QAAQ,KAAK;CACzB,MAAM,OAAO,SAAS,KAAK;CAC3B,MAAM,MAAM,QAAQ,KAAK;CAIzB,MAAM,YAAY,GAFH,8BADF,MAAM,KAAK,MAAM,GAAG,CAAC,IAAI,OAAO,GAAG,KACE,CAEtB,GADlB,WAAW,SAAS,CAAC,OAAO,KAAK,CAAC,OAAO,MAAM,CAAC,MAAM,GAAG,GAAG,GACnC;AACnC,KAAI,QAAQ,OAAO,QAAQ,GAAI,QAAO;AACtC,QAAO,KAAK,KAAK,UAAU;;;;;;;AAQ5B,SAAS,wCAAwC,yBAAiC,oBAAqC;AAGtH,KAAI,+BAA+B,QAFpB,QAAQ,yBAAyB,mBAAmB,EAEhB,oCAAoC,CAAC,CAAE,QAAO;AAEjG,MAAK,MAAM,WAAW,mBAAmB,MAAM,QAAQ,CACtD,KAAI,QAAQ,SAAS,kBAAmB,QAAO;AAGhD,QAAO;;;;;;;;;AAUR,SAAgB,mCAAmC,QAIxC;CACV,MAAM,EAAE,yBAAyB,oBAAoB,6BAA6B;AAClF,KAAI,CAAC,yBAA0B,QAAO;AAEtC,KAAI,CAAC,wCAAwC,yBAAyB,mBAAmB,CACxF,QAAO;AAGR,QAAO,+BAA+B,mBAAmB;;;;;;;AAQ1D,SAAgB,sCAAsC,QAK3C;CACV,MAAM,EAAE,aAAa,aAAa,wBAAwB,6BAA6B;AACvF,KAAI,CAAC,yBAA0B,QAAO;CAEtC,MAAM,OAAO,uBAAuB,QAAQ,OAAO,IAAI;AAEvD,KAAI,CAAC,+BADO,yBAAyB,aAAa,aAAa,KAAK,CAC5B,CAAE,QAAO;AAEjD,QAAO,+BAA+B,KAAK;;AAG5C,SAAgB,0BAA0B,QAK/B;CACV,MAAM,MACL,OAAO,wBACN,OAAO,OAAO,uBAAuB,WAAW,OAAO,qBAAqB,OAAO,OAAO,UAAU;AACtG,QAAO,GAAG,OAAO,OAAO,GAAG,IAAI"}