|
1 | 1 | import { cp, mkdir, rm, symlink } from "node:fs/promises"; |
2 | 2 | import path from "node:path"; |
3 | 3 |
|
| 4 | +type TargetDeps = { |
| 5 | + cp: typeof cp; |
| 6 | + mkdir: typeof mkdir; |
| 7 | + rm: typeof rm; |
| 8 | + symlink: typeof symlink; |
| 9 | + stderr: NodeJS.WritableStream; |
| 10 | +}; |
| 11 | + |
4 | 12 | type TargetParams = { |
5 | 13 | sourceDir: string; |
6 | 14 | targetDir: string; |
7 | 15 | mode?: "symlink" | "copy"; |
| 16 | + explicitTargetMode?: boolean; |
| 17 | + deps?: TargetDeps; |
8 | 18 | }; |
9 | 19 |
|
10 | | -const removeTarget = async (targetDir: string) => { |
11 | | - await rm(targetDir, { recursive: true, force: true }); |
| 20 | +const removeTarget = async (targetDir: string, deps: TargetDeps) => { |
| 21 | + await deps.rm(targetDir, { recursive: true, force: true }); |
12 | 22 | }; |
13 | 23 |
|
14 | 24 | export const applyTargetDir = async (params: TargetParams) => { |
| 25 | + const deps = params.deps ?? { |
| 26 | + cp, |
| 27 | + mkdir, |
| 28 | + rm, |
| 29 | + symlink, |
| 30 | + stderr: process.stderr, |
| 31 | + }; |
15 | 32 | const parentDir = path.dirname(params.targetDir); |
16 | | - await mkdir(parentDir, { recursive: true }); |
17 | | - await removeTarget(params.targetDir); |
| 33 | + await deps.mkdir(parentDir, { recursive: true }); |
| 34 | + await removeTarget(params.targetDir, deps); |
18 | 35 |
|
19 | 36 | const defaultMode = process.platform === "win32" ? "copy" : "symlink"; |
20 | 37 | const mode = params.mode ?? defaultMode; |
21 | 38 | if (mode === "copy") { |
22 | | - await cp(params.sourceDir, params.targetDir, { recursive: true }); |
| 39 | + await deps.cp(params.sourceDir, params.targetDir, { recursive: true }); |
23 | 40 | return; |
24 | 41 | } |
25 | 42 |
|
26 | 43 | const type = process.platform === "win32" ? "junction" : "dir"; |
27 | | - await symlink(params.sourceDir, params.targetDir, type); |
| 44 | + try { |
| 45 | + await deps.symlink(params.sourceDir, params.targetDir, type); |
| 46 | + } catch (error) { |
| 47 | + const code = (error as NodeJS.ErrnoException).code; |
| 48 | + const fallbackCodes = new Set(["EPERM", "EACCES", "ENOTSUP", "EINVAL"]); |
| 49 | + if (code && fallbackCodes.has(code)) { |
| 50 | + if (params.explicitTargetMode) { |
| 51 | + const message = error instanceof Error ? error.message : String(error); |
| 52 | + deps.stderr.write( |
| 53 | + `Warning: Failed to create symlink at ${params.targetDir}. Falling back to copy. ${message}\n`, |
| 54 | + ); |
| 55 | + } |
| 56 | + await deps.cp(params.sourceDir, params.targetDir, { recursive: true }); |
| 57 | + return; |
| 58 | + } |
| 59 | + throw error; |
| 60 | + } |
28 | 61 | }; |
0 commit comments