-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Expand file tree
/
Copy pathfileSystem.ts
More file actions
239 lines (194 loc) · 6.98 KB
/
fileSystem.ts
File metadata and controls
239 lines (194 loc) · 6.98 KB
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
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
import fsSync from "fs";
import stringify from "json-stable-stringify";
import fsModule, { writeFile } from "fs/promises";
import fs from "node:fs";
import { homedir, tmpdir } from "node:os";
import pathModule from "node:path";
import { parseJSONC, stringifyJSONC, parseTOML, stringifyTOML } from "confbox";
// Creates a file at the given path, if the directory doesn't exist it will be created
export async function createFile(
path: string,
contents: string | NodeJS.ArrayBufferView
): Promise<string> {
await fsModule.mkdir(pathModule.dirname(path), { recursive: true });
await fsModule.writeFile(path, contents);
return path;
}
/**
* Sanitizes a hash to be safe for use as a filename.
* esbuild's hashes are base64-encoded and may contain `/` and `+` characters.
*/
export function sanitizeHashForFilename(hash: string): string {
return hash.replace(/\//g, "_").replace(/\+/g, "-");
}
/**
* Creates a file using a content-addressable store for deduplication.
* Files are stored by their content hash, so identical content is only stored once.
* The build directory gets a hardlink to the stored file.
*
* @param filePath - The destination path for the file
* @param contents - The file contents to write
* @param storeDir - The shared store directory for deduplication
* @param contentHash - The content hash (e.g., from esbuild's outputFile.hash)
* @returns The destination file path
*/
export async function createFileWithStore(
filePath: string,
contents: string | NodeJS.ArrayBufferView,
storeDir: string,
contentHash: string
): Promise<string> {
// Sanitize hash to be filesystem-safe (base64 can contain / and +)
const safeHash = sanitizeHashForFilename(contentHash);
// Store files by their content hash for true content-addressable storage
const storePath = pathModule.join(storeDir, safeHash);
// Ensure build directory exists
await fsModule.mkdir(pathModule.dirname(filePath), { recursive: true });
// Remove existing file at destination if it exists (hardlinks fail on existing files)
if (fsSync.existsSync(filePath)) {
await fsModule.unlink(filePath);
}
// Check if content already exists in store by hash
if (fsSync.existsSync(storePath)) {
// Create hardlink from build path to store path
// Fall back to copy if hardlink fails (e.g., on Windows or cross-device)
try {
await fsModule.link(storePath, filePath);
} catch (linkError) {
try {
await fsModule.copyFile(storePath, filePath);
} catch (copyError) {
throw linkError; // Rethrow original error if copy also fails
}
}
return filePath;
}
// Write to store first (using hash as filename)
await fsModule.writeFile(storePath, contents);
// Create hardlink in build directory (with original filename)
// Fall back to copy if hardlink fails (e.g., on Windows or cross-device)
try {
await fsModule.link(storePath, filePath);
} catch (linkError) {
try {
await fsModule.copyFile(storePath, filePath);
} catch (copyError) {
throw linkError; // Rethrow original error if copy also fails
}
}
return filePath;
}
export function isDirectory(configPath: string) {
try {
return fs.statSync(configPath).isDirectory();
} catch (error) {
// ignore error
return false;
}
}
export async function pathExists(path: string): Promise<boolean> {
return fsSync.existsSync(path);
}
export async function someFileExists(directory: string, filenames: string[]): Promise<boolean> {
for (let index = 0; index < filenames.length; index++) {
const filename = filenames[index];
if (!filename) continue;
const path = pathModule.join(directory, filename);
if (await pathExists(path)) {
return true;
}
}
return false;
}
export async function removeFile(path: string) {
await fsModule.unlink(path);
}
export async function readFile(path: string) {
return await fsModule.readFile(path, "utf8");
}
export function expandTilde(filePath: string) {
if (typeof filePath !== "string") {
throw new TypeError("Path must be a string");
}
if (filePath === "~") {
return homedir();
}
if (filePath.startsWith("~/")) {
return pathModule.resolve(homedir(), filePath.slice(2));
}
return pathModule.resolve(filePath);
}
export async function readJSONFile(path: string) {
const fileContents = await fsModule.readFile(path, "utf8");
return JSON.parse(fileContents);
}
export async function safeReadJSONFile(path: string) {
try {
const fileExists = await pathExists(path);
if (!fileExists) return;
const fileContents = await readFile(path);
return JSON.parse(fileContents);
} catch {
return;
}
}
/**
* Use this for deterministic builds. Uses `json-stable-stringify` to sort keys alphabetically.
* @param path - The path to the file to write
* @param json - The JSON object to write
* @param pretty - Whether to pretty print the JSON
*/
export async function writeJSONFile(path: string, json: any, pretty = false) {
await safeWriteFile(path, stringify(json, pretty ? { space: 2 } : undefined) ?? "");
}
/**
* Use this when you want to preserve the original key ordering (e.g. user's package.json)
* @param path - The path to the file to write
* @param json - The JSON object to write
* @param pretty - Whether to pretty print the JSON
*/
export async function writeJSONFilePreserveOrder(path: string, json: any, pretty = false) {
await safeWriteFile(path, JSON.stringify(json, undefined, pretty ? 2 : undefined));
}
// Will create the directory if it doesn't exist
export async function safeWriteFile(path: string, contents: string) {
await fsModule.mkdir(pathModule.dirname(path), { recursive: true });
await fsModule.writeFile(path, contents);
}
export function readJSONFileSync(path: string) {
const fileContents = fsSync.readFileSync(path, "utf8");
return JSON.parse(fileContents);
}
export function safeDeleteFileSync(path: string) {
try {
fs.unlinkSync(path);
} catch (error) {
// ignore error
}
}
// Create a temporary directory within the OS's temp directory
export async function createTempDir(): Promise<string> {
// Generate a unique temp directory path
const tempDirPath: string = pathModule.join(tmpdir(), "trigger-");
// Create the temp directory synchronously and return the path
const directory = await fsModule.mkdtemp(tempDirPath);
return directory;
}
export async function safeReadTomlFile(path: string) {
const fileExists = await pathExists(path);
if (!fileExists) return;
const fileContents = await readFile(path);
return parseTOML(fileContents.replace(/\r\n/g, "\n"));
}
export async function writeTomlFile(path: string, toml: any) {
await safeWriteFile(path, stringifyTOML(toml));
}
export async function safeReadJSONCFile(path: string) {
const fileExists = await pathExists(path);
if (!fileExists) return;
const fileContents = await readFile(path);
return parseJSONC(fileContents.replace(/\r\n/g, "\n"));
}
export async function writeJSONCFile(path: string, json: any) {
await safeWriteFile(path, stringifyJSONC(json));
}