forked from parcel-bundler/parcel
-
Notifications
You must be signed in to change notification settings - Fork 0
/
FSCache.js
134 lines (112 loc) · 3.41 KB
/
FSCache.js
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
const fs = require('./utils/fs');
const path = require('path');
const md5 = require('./utils/md5');
const objectHash = require('./utils/objectHash');
const pkg = require('../package.json');
const logger = require('./Logger');
const glob = require('fast-glob');
const isGlob = require('is-glob');
// These keys can affect the output, so if they differ, the cache should not match
const OPTION_KEYS = ['publicURL', 'minify', 'hmr', 'target', 'scopeHoist'];
class FSCache {
constructor(options) {
this.dir = path.resolve(options.cacheDir || '.cache');
this.dirExists = false;
this.invalidated = new Set();
this.optionsHash = objectHash(
OPTION_KEYS.reduce((p, k) => ((p[k] = options[k]), p), {
version: pkg.version
})
);
}
async ensureDirExists() {
if (this.dirExists) {
return;
}
await fs.mkdirp(this.dir);
// Create sub-directories for every possible hex value
// This speeds up large caches on many file systems since there are fewer files in a single directory.
for (let i = 0; i < 256; i++) {
await fs.mkdirp(path.join(this.dir, ('00' + i.toString(16)).slice(-2)));
}
this.dirExists = true;
}
getCacheFile(filename) {
let hash = md5(this.optionsHash + filename);
return path.join(this.dir, hash.slice(0, 2), hash.slice(2) + '.json');
}
async getLastModified(filename) {
if (isGlob(filename)) {
let files = await glob(filename, {
onlyFiles: true
});
return (await Promise.all(
files.map(file => fs.stat(file).then(({mtime}) => mtime.getTime()))
)).reduce((a, b) => Math.max(a, b), 0);
}
return (await fs.stat(filename)).mtime.getTime();
}
async writeDepMtimes(data) {
// Write mtimes for each dependent file that is already compiled into this asset
for (let dep of data.dependencies) {
if (dep.includedInParent) {
dep.mtime = await this.getLastModified(dep.name);
}
}
}
async write(filename, data) {
try {
await this.ensureDirExists();
await this.writeDepMtimes(data);
await fs.writeFile(this.getCacheFile(filename), JSON.stringify(data));
this.invalidated.delete(filename);
} catch (err) {
logger.error(`Error writing to cache: ${err.message}`);
}
}
async checkDepMtimes(data) {
// Check mtimes for files that are already compiled into this asset
// If any of them changed, invalidate.
for (let dep of data.dependencies) {
if (dep.includedInParent) {
if ((await this.getLastModified(dep.name)) > dep.mtime) {
return false;
}
}
}
return true;
}
async read(filename) {
if (this.invalidated.has(filename)) {
return null;
}
let cacheFile = this.getCacheFile(filename);
try {
let stats = await fs.stat(filename);
let cacheStats = await fs.stat(cacheFile);
if (stats.mtime > cacheStats.mtime) {
return null;
}
let json = await fs.readFile(cacheFile);
let data = JSON.parse(json);
if (!(await this.checkDepMtimes(data))) {
return null;
}
return data;
} catch (err) {
return null;
}
}
invalidate(filename) {
this.invalidated.add(filename);
}
async delete(filename) {
try {
await fs.unlink(this.getCacheFile(filename));
this.invalidated.delete(filename);
} catch (err) {
// Fail silently
}
}
}
module.exports = FSCache;