UNPKG

3.5 kBJavaScriptView Raw
1const fs = require('@parcel/fs');
2const path = require('path');
3const md5 = require('./utils/md5');
4const objectHash = require('./utils/objectHash');
5const pkg = require('../package.json');
6const logger = require('@parcel/logger');
7const {isGlob, glob} = require('./utils/glob');
8
9// These keys can affect the output, so if they differ, the cache should not match
10const OPTION_KEYS = [
11 'publicURL',
12 'minify',
13 'hmr',
14 'target',
15 'scopeHoist',
16 'sourceMaps'
17];
18
19class FSCache {
20 constructor(options) {
21 this.dir = path.resolve(options.cacheDir || '.cache');
22 this.dirExists = false;
23 this.invalidated = new Set();
24 this.optionsHash = objectHash(
25 OPTION_KEYS.reduce((p, k) => ((p[k] = options[k]), p), {
26 version: pkg.version
27 })
28 );
29 }
30
31 async ensureDirExists() {
32 if (this.dirExists) {
33 return;
34 }
35
36 await fs.mkdirp(this.dir);
37
38 // Create sub-directories for every possible hex value
39 // This speeds up large caches on many file systems since there are fewer files in a single directory.
40 for (let i = 0; i < 256; i++) {
41 await fs.mkdirp(path.join(this.dir, ('00' + i.toString(16)).slice(-2)));
42 }
43
44 this.dirExists = true;
45 }
46
47 getCacheFile(filename) {
48 let hash = md5(this.optionsHash + filename);
49 return path.join(this.dir, hash.slice(0, 2), hash.slice(2) + '.json');
50 }
51
52 async getLastModified(filename) {
53 if (isGlob(filename)) {
54 let files = await glob(filename, {
55 onlyFiles: true
56 });
57
58 return (await Promise.all(
59 files.map(file => fs.stat(file).then(({mtime}) => mtime.getTime()))
60 )).reduce((a, b) => Math.max(a, b), 0);
61 }
62 return (await fs.stat(filename)).mtime.getTime();
63 }
64
65 async writeDepMtimes(data) {
66 // Write mtimes for each dependent file that is already compiled into this asset
67 for (let dep of data.dependencies) {
68 if (dep.includedInParent) {
69 dep.mtime = await this.getLastModified(dep.name);
70 }
71 }
72 }
73
74 async write(filename, data) {
75 try {
76 await this.ensureDirExists();
77 await this.writeDepMtimes(data);
78 await fs.writeFile(this.getCacheFile(filename), JSON.stringify(data));
79 this.invalidated.delete(filename);
80 } catch (err) {
81 logger.error(`Error writing to cache: ${err.message}`);
82 }
83 }
84
85 async checkDepMtimes(data) {
86 // Check mtimes for files that are already compiled into this asset
87 // If any of them changed, invalidate.
88 for (let dep of data.dependencies) {
89 if (dep.includedInParent) {
90 if ((await this.getLastModified(dep.name)) > dep.mtime) {
91 return false;
92 }
93 }
94 }
95
96 return true;
97 }
98
99 async read(filename) {
100 if (this.invalidated.has(filename)) {
101 return null;
102 }
103
104 let cacheFile = this.getCacheFile(filename);
105
106 try {
107 let stats = await fs.stat(filename);
108 let cacheStats = await fs.stat(cacheFile);
109
110 if (stats.mtime > cacheStats.mtime) {
111 return null;
112 }
113
114 let json = await fs.readFile(cacheFile);
115 let data = JSON.parse(json);
116 if (!(await this.checkDepMtimes(data))) {
117 return null;
118 }
119
120 return data;
121 } catch (err) {
122 return null;
123 }
124 }
125
126 invalidate(filename) {
127 this.invalidated.add(filename);
128 }
129
130 async delete(filename) {
131 try {
132 await fs.unlink(this.getCacheFile(filename));
133 this.invalidated.delete(filename);
134 } catch (err) {
135 // Fail silently
136 }
137 }
138}
139
140module.exports = FSCache;