{"record":{"id":"e0afd4fb6b8c3e73","repo":"JuliusBrussee/caveman","slug":"caveman-build-package-artifact-symlink-is-not-loc","errorCode":null,"errorMessage":"caveman build: package artifact symlink is not lockable: ${JSON.stringify(path)}","messagePattern":"caveman build: package artifact symlink is not lockable: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/agent/src/source-graph.ts","lineNumber":203,"sourceCode":"      if (!required.has(dependency)) continue;\n      throw new Error(`caveman build: unresolved package dependency ${JSON.stringify(dependency)}`, {\n        cause: error,\n      });\n    }\n  }\n}\n\nasync function collectPackageFiles(directory: string, files: Set<string>): Promise<void> {\n  const entries = await opendir(directory);\n  for await (const entry of entries) {\n    if (entry.name === \"node_modules\" || entry.name === \".git\") continue;\n    const path = resolve(directory, entry.name);\n    if (entry.isDirectory()) {\n      await collectPackageFiles(path, files);\n    } else if (entry.isFile()) {\n      files.add(path);\n    } else if (entry.isSymbolicLink()) {\n      throw new Error(`caveman build: package artifact symlink is not lockable: ${JSON.stringify(path)}`);\n    }\n  }\n}\n\nfunction barePackageName(specifier: string): string {\n  const parts = specifier.split(\"/\");\n  if (specifier.startsWith(\"@\")) {\n    if (parts.length < 2 || parts[1] === \"\") throw new Error(\"invalid scoped package\");\n    return `${parts[0]}/${parts[1]}`;\n  }\n  if (parts[0] === \"\") throw new Error(\"invalid package\");\n  return parts[0]!;\n}\n\nfunction resolvePackageExport(exportsValue: unknown, subpath: string): string | undefined {\n  if (typeof exportsValue === \"string\" || Array.isArray(exportsValue)) {\n    return subpath === \".\" ? resolveConditionalExport(exportsValue) : undefined;\n  }","sourceCodeStart":185,"sourceCodeEnd":221,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/agent/src/source-graph.ts#L185-L221","documentation":"Thrown by collectPackageFiles when it encounters a symbolic link while enumerating a package's files. Because this build produces a locked/artifact-accurate file set, symlinks are considered non-lockable: the library refuses rather than following links that could duplicate or escape the package.","triggerScenarios":"collectPackageClosure collects files for a package; readdir-withFileTypes yields an entry that is neither file, directory, nor regular file — specifically entry.isSymbolicLink() is true (e.g. node_modules/bin links or pnpm-style symlinks inside the package).","commonSituations":"pnpm/yarn PnP installs that symlink within packages; a package that ships symlinks (e.g. bin or vendored assets); developer-created symlinks inside node_modules pointing to a local checkout.","solutions":["Replace the symlink inside the package with the real file or a copy.","Reinstall with a package manager/linker that materializes real files (npm instead of pnpm symlinks for this package).","Exclude or restructure symlinked directories before building.","If it's your own package, stop shipping symlinks in published artifacts (use files field to publish real files)."],"exampleFix":"// before\nln -s ../shared/asset.js node_modules/foo/asset.js\n\n// after\ncp ../shared/asset.js node_modules/foo/asset.js","handlingStrategy":"validation","validationCode":"import { readdirSync, lstatSync } from \"node:fs\";\nimport { join } from \"node:path\";\nfunction hasSymlinks(dir) {\n  for (const e of readdirSync(dir, { withFileTypes: true })) {\n    const p = join(dir, e.name);\n    if (e.isSymbolicLink() || lstatSync(p).isSymbolicLink()) return p;\n    if (e.isDirectory() && hasSymlinks(p)) return hasSymlinks(p);\n  }\n  return null;\n}","typeGuard":"null","tryCatchPattern":"try {\n  await graph.expand();\n} catch (e) {\n  if (e instanceof Error && e.message.includes(\"symlink is not lockable\")) {\n    console.error(\"Replace the reported symlink with a real file before building.\");\n  }\n  throw e;\n}","preventionTips":["Don't create symlinks inside node_modules or published packages.","Use a linker that materializes files (npm) for projects fed into this build.","Scan packages for symlinks in CI before graph expansion."],"tags":["filesystem","symlink","build"],"backgroundTag":"unsupported-operation","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}