11import fsp from "node:fs/promises" ;
2- import { relative , dirname } from "pathe" ;
2+ import { relative , dirname , join } from "pathe" ;
33import consola from "consola" ;
44import { colors } from "consola/utils" ;
55
@@ -23,3 +23,45 @@ export async function isDirectory(path: string) {
2323 return false ;
2424 }
2525}
26+
27+ /**
28+ * Recursively recreate `src` at `dest` using hard links for files.
29+ *
30+ * Hard links share inodes with the source, so deployment targets that
31+ * read files into per-function bundles (e.g. Vercel Lambda packaging)
32+ * see them as regular files while local disk usage stays flat.
33+ *
34+ * Directories cannot be hard-linked, so the tree is recreated and each
35+ * file is linked individually. Symlinks are preserved as symlinks.
36+ * Falls back to `copyFile` when crossing filesystems (`EXDEV`) or when
37+ * the platform refuses the link operation (`EPERM`).
38+ *
39+ * Entries listed in `skip` are ignored at the top level only.
40+ */
41+ export async function hardLinkDir ( src : string , dest : string , options : { skip ?: Set < string > } = { } ) {
42+ await fsp . mkdir ( dest , { recursive : true } ) ;
43+ const entries = await fsp . readdir ( src , { withFileTypes : true } ) ;
44+ for ( const entry of entries ) {
45+ if ( options . skip ?. has ( entry . name ) ) {
46+ continue ;
47+ }
48+ const srcPath = join ( src , entry . name ) ;
49+ const destPath = join ( dest , entry . name ) ;
50+ if ( entry . isDirectory ( ) ) {
51+ await hardLinkDir ( srcPath , destPath ) ;
52+ } else if ( entry . isSymbolicLink ( ) ) {
53+ const target = await fsp . readlink ( srcPath ) ;
54+ await fsp . symlink ( target , destPath ) ;
55+ } else {
56+ try {
57+ await fsp . link ( srcPath , destPath ) ;
58+ } catch ( err : any ) {
59+ if ( err ?. code === "EXDEV" || err ?. code === "EPERM" ) {
60+ await fsp . copyFile ( srcPath , destPath ) ;
61+ } else {
62+ throw err ;
63+ }
64+ }
65+ }
66+ }
67+ }
0 commit comments