[79a0317] | 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 | }; |
---|