@@ -0,0 +1,45 @@
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user