46 lines
1.8 KiB
JavaScript
46 lines
1.8 KiB
JavaScript
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);
|
|
}
|