-
-
Notifications
You must be signed in to change notification settings - Fork 452
Expand file tree
/
Copy pathcache.js
More file actions
287 lines (262 loc) · 8.42 KB
/
cache.js
File metadata and controls
287 lines (262 loc) · 8.42 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
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
// @ts-check
/**
* Filesystem Cache
*
* Given a file and a transform function, cache the result into files
* or retrieve the previously cached files if the given file is already known.
*
* @see https://github.com/babel/babel-loader/issues/34
* @see https://github.com/babel/babel-loader/pull/41
*/
const os = require("os");
const path = require("path");
const zlib = require("zlib");
const { promisify } = require("util");
const { readFile, writeFile, mkdir } = require("fs/promises");
const { sync: findUpSync } = require("find-up");
const { env } = process;
const transform = require("./transform");
const serialize = require("./serialize");
/**
* @typedef {object} FileSystemInfoEntry
* @property {number} safeTime
* @property {number} timestamp
*/
/**
* @typedef {object} WebpackLogger
* @property {function(string): void} debug
* @property {function(string): void} info
* @property {function(string): void} warn
* @property {function(string): void} error
*/
/**
* @typedef {object} WebpackHash
* @property {(data: string | Buffer, inputEncoding?: string) => WebpackHash} update
* @property {(encoding?: string) => string | Buffer} digest
*/
/**
* @type {string | null}
*/
let defaultCacheDirectory = null;
const gunzip = promisify(zlib.gunzip);
const gzip = promisify(zlib.gzip);
/**
* Read the contents from the compressed file.
*
* @async
* @param {string} filename
* @param {boolean} compress
*/
const read = async function (filename, compress) {
const data = await readFile(filename + (compress ? ".gz" : ""));
const content = compress ? await gunzip(data) : data;
return JSON.parse(content.toString());
};
/**
* Write contents into a compressed file.
* @async
* @param {string} filename
* @param {boolean} compress
* @param {any} result
*/
const write = async function (filename, compress, result) {
const content = JSON.stringify(result);
const data = compress ? await gzip(content) : content;
return await writeFile(filename + (compress ? ".gz" : ""), data);
};
/**
* Build the filename for the cached file
* @param {string} source File source code
* @param {string} identifier Unique identifier to bust cache
* @param {Object} options Options used
* @param {WebpackHash} hash Hash function returned by `LoaderContext.utils.createHash`
* @return {string}
*/
const filename = function (source, identifier, options, hash) {
hash.update(serialize([options, source, identifier]));
return hash.digest("hex") + ".json";
};
/**
* Add timestamps to external dependencies.
* @async
* @param {import("./transform").TransformResult["externalDependencies"]} externalDependencies
* @param {(filename: string) => Promise<FileSystemInfoEntry>} getFileTimestamp
*/
const addTimestamps = async function (externalDependencies, getFileTimestamp) {
for (const depAndEmptyTimestamp of externalDependencies) {
try {
const [dep] = depAndEmptyTimestamp;
const { timestamp } = await getFileTimestamp(dep);
depAndEmptyTimestamp.push(timestamp);
} catch {
// ignore errors if timestamp is not available
}
}
};
/**
* Check if any external dependencies have been modified.
* @async
* @param {import("./transform").TransformResult["externalDependencies"]} externalDepsWithTimestamp
* @param {(filename: string) => Promise<FileSystemInfoEntry>} getFileTimestamp
* @returns {Promise<boolean>}
*/
const areExternalDependenciesModified = async function (
externalDepsWithTimestamp,
getFileTimestamp,
) {
for (const depAndTimestamp of externalDepsWithTimestamp) {
const [dep, timestamp] = depAndTimestamp;
let newTimestamp;
try {
newTimestamp = (await getFileTimestamp(dep)).timestamp;
} catch {
return true;
}
if (timestamp !== newTimestamp) {
return true;
}
}
return false;
};
/**
* Handle the cache
* @async
* @param {string} directory
* @param {Object} params
* @param {string} params.source The source code to transform.
* @param {import(".").NormalizedOptions} [params.options] Options used for transformation.
* @param {string} params.cacheIdentifier Unique identifier to bust cache.
* @param {string} [params.cacheDirectory] Directory to store cached files.
* @param {boolean} [params.cacheCompression] Whether to compress cached files.
* @param {WebpackHash} params.hash Hash function to use for the cache filename.
* @param {(filename: string) => Promise<FileSystemInfoEntry>} params.getFileTimestamp - Function to get file timestamps.
* @param {WebpackLogger} params.logger
* @returns {Promise<null | import("./transform").TransformResult>}
*/
const handleCache = async function (directory, params) {
const {
source,
options = {},
cacheIdentifier,
cacheDirectory,
cacheCompression,
hash,
getFileTimestamp,
logger,
} = params;
const file = path.join(
directory,
filename(source, cacheIdentifier, options, hash),
);
try {
// No errors mean that the file was previously cached
// we just need to return it
logger.debug(`reading cache file '${file}'`);
const result = await read(file, cacheCompression);
if (
!(await areExternalDependenciesModified(
result.externalDependencies,
getFileTimestamp,
))
) {
logger.debug(`validated cache file '${file}'`);
return result;
}
logger.debug(
`discarded cache file '${file}' due to changes in external dependencies`,
);
} catch {
// continue if cache can't be read
logger.debug(`discarded cache as it can not be read`);
}
const fallback =
typeof cacheDirectory !== "string" && directory !== os.tmpdir();
// Make sure the directory exists.
try {
// overwrite directory if exists
logger.debug(`creating cache folder '${directory}'`);
await mkdir(directory, { recursive: true });
} catch (err) {
if (fallback) {
return handleCache(os.tmpdir(), params);
}
throw err;
}
// Otherwise just transform the file
// return it to the user asap and write it in cache
logger.debug(`applying Babel transform`);
const result = await transform(source, options);
if (!result) {
logger.debug(`no result from Babel transform, skipping cache write`);
return null;
}
await addTimestamps(result.externalDependencies, getFileTimestamp);
try {
logger.debug(`writing result to cache file '${file}'`);
await write(file, cacheCompression, result);
} catch (err) {
if (fallback) {
// Fallback to tmpdir if node_modules folder not writable
return handleCache(os.tmpdir(), params);
}
throw err;
}
return result;
};
/**
* Retrieve file from cache, or create a new one for future reads
* @async
* @param {object} params
* @param {string} params.cacheDirectory Directory to store cached files.
* @param {string} params.cacheIdentifier Unique identifier to bust cache.
* @param {boolean} params.cacheCompression Whether compressing cached files.
* @param {string} params.source Original contents of the file to be cached.
* @param {import(".").NormalizedOptions} params.options Options to be given to the transform function.
* @param {function} params.transform Transform function to apply to the file.
* @param {WebpackHash} params.hash Hash function to use for the cache filename.
* @param {function(string): Promise<FileSystemInfoEntry>} params.getFileTimestamp Function to get file timestamps.
* @param {WebpackLogger} params.logger Logger instance.
*
* @example
*
* const result = await cache({
* cacheDirectory: '.tmp/cache',
* cacheIdentifier: 'babel-loader-cachefile',
* cacheCompression: false,
* source: *source code from file*,
* options: {
* experimental: true,
* runtime: true
* },
* });
*/
module.exports = async function cache(params) {
let directory;
if (typeof params.cacheDirectory === "string") {
directory = params.cacheDirectory;
} else {
defaultCacheDirectory ??= findCacheDir("babel-loader");
directory = defaultCacheDirectory;
}
return await handleCache(directory, params);
};
/**
* Find the cache directory for babel-loader.
* @param {string} name "babel-loader"
* @returns {string}
*/
function findCacheDir(name) {
if (env.CACHE_DIR && !["true", "false", "1", "0"].includes(env.CACHE_DIR)) {
return path.join(env.CACHE_DIR, name);
}
const rootPkgJSONPath = findUpSync("package.json");
if (rootPkgJSONPath) {
return path.join(
path.dirname(rootPkgJSONPath),
"node_modules",
".cache",
name,
);
}
return os.tmpdir();
}