1 | const fs = require('@parcel/fs');
|
2 | const path = require('path');
|
3 | const md5 = require('./utils/md5');
|
4 | const objectHash = require('./utils/objectHash');
|
5 | const pkg = require('../package.json');
|
6 | const logger = require('@parcel/logger');
|
7 | const {isGlob, glob} = require('./utils/glob');
|
8 |
|
9 |
|
10 | const OPTION_KEYS = [
|
11 | 'publicURL',
|
12 | 'minify',
|
13 | 'hmr',
|
14 | 'target',
|
15 | 'scopeHoist',
|
16 | 'sourceMaps'
|
17 | ];
|
18 |
|
19 | class 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 |
|
39 |
|
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 |
|
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 |
|
87 |
|
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 |
|
136 | }
|
137 | }
|
138 | }
|
139 |
|
140 | module.exports = FSCache;
|