import {cp, copyFile, lstat, mkdir, readdir, realpath, symlink} from 'node:fs/promises'; import {dirname, join, relative, sep} from 'node:path'; export const under=(path,base)=>path===base||path.startsWith(base+sep); /** * Copy a release tree for npm or pnpm layouts. * In-tree symlinks are rewritten as relative links inside `to` so the package is relocatable. * Symlinks that escape `from` but stay under `root` are materialized; anything else is rejected. */ export async function copyReleaseTree(from,to,{root=from,filter=async()=>true}={}) { from=await realpath(from); root=await realpath(root); async function walk(srcDir,destDir) { await mkdir(destDir,{recursive:true}); for(const entry of await readdir(srcDir,{withFileTypes:true})) { const srcPath=join(srcDir,entry.name); if(!await filter(srcPath))continue; const destPath=join(destDir,entry.name); if(entry.isSymbolicLink()){ const target=await realpath(srcPath).catch(()=>null); if(!target||(!under(target,from)&&!under(target,root))) throw new Error(`Release source contains a symbolic link: ${srcPath}`); if(under(target,from)){ const mapped=join(to,relative(from,target)); await symlink(relative(dirname(destPath),mapped),destPath); continue; } await cp(target,destPath,{recursive:true,dereference:true,filter}); continue; } if(entry.isDirectory()){ await walk(srcPath,destPath); continue; } if(entry.isFile()){ await copyFile(srcPath,destPath); continue; } // Rare non-file entries (sockets/devices) should never ship. throw new Error(`Release source contains an unsupported file type: ${srcPath}`); } } await walk(from,to); }