1 | /**
|
---|
2 | * Filesystem Cache
|
---|
3 | *
|
---|
4 | * Given a file and a transform function, cache the result into files
|
---|
5 | * or retrieve the previously cached files if the given file is already known.
|
---|
6 | *
|
---|
7 | * @see https://github.com/babel/babel-loader/issues/34
|
---|
8 | * @see https://github.com/babel/babel-loader/pull/41
|
---|
9 | */
|
---|
10 | const os = require("os");
|
---|
11 | const path = require("path");
|
---|
12 | const zlib = require("zlib");
|
---|
13 | const crypto = require("crypto");
|
---|
14 | const {
|
---|
15 | promisify
|
---|
16 | } = require("util");
|
---|
17 | const {
|
---|
18 | readFile,
|
---|
19 | writeFile,
|
---|
20 | mkdir
|
---|
21 | } = require("fs/promises");
|
---|
22 | const findCacheDirP = import("find-cache-dir");
|
---|
23 | const transform = require("./transform");
|
---|
24 | // Lazily instantiated when needed
|
---|
25 | let defaultCacheDirectory = null;
|
---|
26 | let hashType = "sha256";
|
---|
27 | // use md5 hashing if sha256 is not available
|
---|
28 | try {
|
---|
29 | crypto.createHash(hashType);
|
---|
30 | } catch {
|
---|
31 | hashType = "md5";
|
---|
32 | }
|
---|
33 | const gunzip = promisify(zlib.gunzip);
|
---|
34 | const gzip = promisify(zlib.gzip);
|
---|
35 |
|
---|
36 | /**
|
---|
37 | * Read the contents from the compressed file.
|
---|
38 | *
|
---|
39 | * @async
|
---|
40 | * @params {String} filename
|
---|
41 | * @params {Boolean} compress
|
---|
42 | */
|
---|
43 | const read = async function (filename, compress) {
|
---|
44 | const data = await readFile(filename + (compress ? ".gz" : ""));
|
---|
45 | const content = compress ? await gunzip(data) : data;
|
---|
46 | return JSON.parse(content.toString());
|
---|
47 | };
|
---|
48 |
|
---|
49 | /**
|
---|
50 | * Write contents into a compressed file.
|
---|
51 | *
|
---|
52 | * @async
|
---|
53 | * @params {String} filename
|
---|
54 | * @params {Boolean} compress
|
---|
55 | * @params {String} result
|
---|
56 | */
|
---|
57 | const write = async function (filename, compress, result) {
|
---|
58 | const content = JSON.stringify(result);
|
---|
59 | const data = compress ? await gzip(content) : content;
|
---|
60 | return await writeFile(filename + (compress ? ".gz" : ""), data);
|
---|
61 | };
|
---|
62 |
|
---|
63 | /**
|
---|
64 | * Build the filename for the cached file
|
---|
65 | *
|
---|
66 | * @params {String} source File source code
|
---|
67 | * @params {Object} options Options used
|
---|
68 | *
|
---|
69 | * @return {String}
|
---|
70 | */
|
---|
71 | const filename = function (source, identifier, options) {
|
---|
72 | const hash = crypto.createHash(hashType);
|
---|
73 | const contents = JSON.stringify({
|
---|
74 | source,
|
---|
75 | options,
|
---|
76 | identifier
|
---|
77 | });
|
---|
78 | hash.update(contents);
|
---|
79 | return hash.digest("hex") + ".json";
|
---|
80 | };
|
---|
81 |
|
---|
82 | /**
|
---|
83 | * Handle the cache
|
---|
84 | *
|
---|
85 | * @params {String} directory
|
---|
86 | * @params {Object} params
|
---|
87 | */
|
---|
88 | const handleCache = async function (directory, params) {
|
---|
89 | const {
|
---|
90 | source,
|
---|
91 | options = {},
|
---|
92 | cacheIdentifier,
|
---|
93 | cacheDirectory,
|
---|
94 | cacheCompression,
|
---|
95 | logger
|
---|
96 | } = params;
|
---|
97 | const file = path.join(directory, filename(source, cacheIdentifier, options));
|
---|
98 | try {
|
---|
99 | // No errors mean that the file was previously cached
|
---|
100 | // we just need to return it
|
---|
101 | logger.debug(`reading cache file '${file}'`);
|
---|
102 | return await read(file, cacheCompression);
|
---|
103 | } catch {
|
---|
104 | // conitnue if cache can't be read
|
---|
105 | logger.debug(`discarded cache as it can not be read`);
|
---|
106 | }
|
---|
107 | const fallback = typeof cacheDirectory !== "string" && directory !== os.tmpdir();
|
---|
108 |
|
---|
109 | // Make sure the directory exists.
|
---|
110 | try {
|
---|
111 | // overwrite directory if exists
|
---|
112 | logger.debug(`creating cache folder '${directory}'`);
|
---|
113 | await mkdir(directory, {
|
---|
114 | recursive: true
|
---|
115 | });
|
---|
116 | } catch (err) {
|
---|
117 | if (fallback) {
|
---|
118 | return handleCache(os.tmpdir(), params);
|
---|
119 | }
|
---|
120 | throw err;
|
---|
121 | }
|
---|
122 |
|
---|
123 | // Otherwise just transform the file
|
---|
124 | // return it to the user asap and write it in cache
|
---|
125 | logger.debug(`applying Babel transform`);
|
---|
126 | const result = await transform(source, options);
|
---|
127 |
|
---|
128 | // Do not cache if there are external dependencies,
|
---|
129 | // since they might change and we cannot control it.
|
---|
130 | if (!result.externalDependencies.length) {
|
---|
131 | try {
|
---|
132 | logger.debug(`writing result to cache file '${file}'`);
|
---|
133 | await write(file, cacheCompression, result);
|
---|
134 | } catch (err) {
|
---|
135 | if (fallback) {
|
---|
136 | // Fallback to tmpdir if node_modules folder not writable
|
---|
137 | return handleCache(os.tmpdir(), params);
|
---|
138 | }
|
---|
139 | throw err;
|
---|
140 | }
|
---|
141 | }
|
---|
142 | return result;
|
---|
143 | };
|
---|
144 |
|
---|
145 | /**
|
---|
146 | * Retrieve file from cache, or create a new one for future reads
|
---|
147 | *
|
---|
148 | * @async
|
---|
149 | * @param {Object} params
|
---|
150 | * @param {String} params.cacheDirectory Directory to store cached files
|
---|
151 | * @param {String} params.cacheIdentifier Unique identifier to bust cache
|
---|
152 | * @param {Boolean} params.cacheCompression Whether compressing cached files
|
---|
153 | * @param {String} params.source Original contents of the file to be cached
|
---|
154 | * @param {Object} params.options Options to be given to the transform fn
|
---|
155 | *
|
---|
156 | * @example
|
---|
157 | *
|
---|
158 | * const result = await cache({
|
---|
159 | * cacheDirectory: '.tmp/cache',
|
---|
160 | * cacheIdentifier: 'babel-loader-cachefile',
|
---|
161 | * cacheCompression: false,
|
---|
162 | * source: *source code from file*,
|
---|
163 | * options: {
|
---|
164 | * experimental: true,
|
---|
165 | * runtime: true
|
---|
166 | * },
|
---|
167 | * });
|
---|
168 | */
|
---|
169 |
|
---|
170 | module.exports = async function (params) {
|
---|
171 | let directory;
|
---|
172 | if (typeof params.cacheDirectory === "string") {
|
---|
173 | directory = params.cacheDirectory;
|
---|
174 | } else {
|
---|
175 | if (defaultCacheDirectory === null) {
|
---|
176 | const {
|
---|
177 | default: findCacheDir
|
---|
178 | } = await findCacheDirP;
|
---|
179 | defaultCacheDirectory = findCacheDir({
|
---|
180 | name: "babel-loader"
|
---|
181 | }) || os.tmpdir();
|
---|
182 | }
|
---|
183 | directory = defaultCacheDirectory;
|
---|
184 | }
|
---|
185 | return await handleCache(directory, params);
|
---|
186 | }; |
---|