-
-
Notifications
You must be signed in to change notification settings - Fork 933
/
importIndexedDir.ts
143 lines (135 loc) · 4.9 KB
/
importIndexedDir.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
import { promises as fs } from 'fs'
import path from 'path'
import { globalWarn, logger } from '@pnpm/logger'
import rimraf from '@zkochan/rimraf'
import sanitizeFilename from 'sanitize-filename'
import makeEmptyDir from 'make-empty-dir'
import pathTemp from 'path-temp'
import renameOverwrite from 'rename-overwrite'
const filenameConflictsLogger = logger('_filename-conflicts')
export type ImportFile = (src: string, dest: string) => Promise<void>
export async function importIndexedDir (
importFile: ImportFile,
newDir: string,
filenames: Record<string, string>,
opts: {
keepModulesDir?: boolean
}
) {
const stage = pathTemp(path.dirname(newDir))
try {
await tryImportIndexedDir(importFile, stage, filenames)
if (opts.keepModulesDir) {
// Keeping node_modules is needed only when the hoisted node linker is used.
await moveOrMergeModulesDirs(path.join(newDir, 'node_modules'), path.join(stage, 'node_modules'))
}
await renameOverwrite(stage, newDir)
} catch (err: any) { // eslint-disable-line
try {
await rimraf(stage)
} catch (err) {} // eslint-disable-line:no-empty
if (err['code'] === 'EEXIST') {
const { uniqueFileMap, conflictingFileNames } = getUniqueFileMap(filenames)
if (Object.keys(conflictingFileNames).length === 0) throw err
filenameConflictsLogger.debug({
conflicts: conflictingFileNames,
writingTo: newDir,
})
globalWarn(
`Not all files were linked to "${path.relative(process.cwd(), newDir)}". ` +
'Some of the files have equal names in different case, ' +
'which is an issue on case-insensitive filesystems. ' +
`The conflicting file names are: ${JSON.stringify(conflictingFileNames)}`
)
await importIndexedDir(importFile, newDir, uniqueFileMap, opts)
return
}
if (err['code'] === 'ENOENT') {
const { sanitizedFilenames, invalidFilenames } = sanitizeFilenames(filenames)
if (invalidFilenames.length === 0) throw err
globalWarn(`\
The package linked to "${path.relative(process.cwd(), newDir)}" had \
files with invalid names: ${invalidFilenames.join(', ')}. \
They were renamed.`)
await importIndexedDir(importFile, newDir, sanitizedFilenames, opts)
return
}
throw err
}
}
function sanitizeFilenames (filenames: Record<string, string>) {
const sanitizedFilenames: Record<string, string> = {}
const invalidFilenames: string[] = []
for (const [filename, src] of Object.entries(filenames)) {
const sanitizedFilename = filename.split('/').map((f) => sanitizeFilename(f)).join('/')
if (sanitizedFilename !== filename) {
invalidFilenames.push(filename)
}
sanitizedFilenames[sanitizedFilename] = src
}
return { sanitizedFilenames, invalidFilenames }
}
async function tryImportIndexedDir (importFile: ImportFile, newDir: string, filenames: Record<string, string>) {
await makeEmptyDir(newDir, { recursive: true })
const alldirs = new Set<string>()
Object.keys(filenames)
.forEach((f) => {
const dir = path.dirname(f)
if (dir === '.') return
alldirs.add(dir)
})
await Promise.all(
Array.from(alldirs)
.sort((d1, d2) => d1.length - d2.length) // from shortest to longest
.map(async (dir) => fs.mkdir(path.join(newDir, dir), { recursive: true }))
)
await Promise.all(
Object.entries(filenames)
.map(async ([f, src]: [string, string]) => {
const dest = path.join(newDir, f)
await importFile(src, dest)
})
)
}
function getUniqueFileMap (fileMap: Record<string, string>) {
const lowercaseFiles = new Map<string, string>()
const conflictingFileNames = {}
const uniqueFileMap = {}
for (const filename of Object.keys(fileMap).sort()) {
const lowercaseFilename = filename.toLowerCase()
if (lowercaseFiles.has(lowercaseFilename)) {
conflictingFileNames[filename] = lowercaseFiles.get(lowercaseFilename)
continue
}
lowercaseFiles.set(lowercaseFilename, filename)
uniqueFileMap[filename] = fileMap[filename]
}
return {
conflictingFileNames,
uniqueFileMap,
}
}
async function moveOrMergeModulesDirs (src: string, dest: string) {
try {
await fs.rename(src, dest)
} catch (err: any) { // eslint-disable-line
switch (err.code) {
case 'ENOENT':
// If src directory doesn't exist, there is nothing to do
return
case 'ENOTEMPTY':
case 'EPERM': // This error code is thrown on Windows
// The newly added dependency might have node_modules if it has bundled dependencies.
await mergeModulesDirs(src, dest)
return
default:
throw err
}
}
}
async function mergeModulesDirs (src: string, dest: string) {
const srcFiles = await fs.readdir(src)
const destFiles = new Set(await fs.readdir(dest))
const filesToMove = srcFiles.filter((file) => !destFiles.has(file))
await Promise.all(filesToMove.map((file) => fs.rename(path.join(src, file), path.join(dest, file))))
}