Skip to content
This repository has been archived by the owner on Sep 28, 2020. It is now read-only.

refactor: new cache implementation #320

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
108 changes: 58 additions & 50 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions package.json
Expand Up @@ -41,8 +41,8 @@
"webpack": "^4.0.0 || ^5.0.0"
},
"dependencies": {
"find-cache-dir": "^3.3.1",
"fs-extra": "^9.0.0",
"loader-fs-cache": "^1.0.3",
"loader-utils": "^2.0.0",
"object-hash": "^2.0.3",
"schema-utils": "^2.6.5"
Expand Down Expand Up @@ -70,7 +70,6 @@
"jest": "^25.2.6",
"jest-junit": "^10.0.0",
"lint-staged": "^10.1.1",
"mkdirp": "^1.0.3",
"npm-run-all": "^4.1.5",
"prettier": "^2.0.2",
"standard-version": "^7.1.0",
Expand Down
187 changes: 187 additions & 0 deletions src/cache.js
@@ -0,0 +1,187 @@
/**
* Original Filesystem Cache implementation by babel-loader
* Licensed under the MIT License
*
* @see https://github.com/babel/babel-loader/commits/master/src/fs-cache.js
* @see https://github.com/babel/babel-loader/commits/master/src/cache.js
*/

/**
* Filesystem Cache
*
* Given a file and a transform function, cache the result into files
* or retrieve the previously cached files if the given file is already known.
*
* @see https://github.com/babel/babel-loader/issues/34
* @see https://github.com/babel/babel-loader/pull/41
*/
import fs from 'fs';
import os from 'os';
import { join } from 'path';
import { promisify } from 'util';
import zlib from 'zlib';
import { createHash } from 'crypto';

import findCacheDir from 'find-cache-dir';

// Lazily instantiated when needed
let defaultCacheDirectory = null;

const readFile = promisify(fs.readFile);
const writeFile = promisify(fs.writeFile);
const gunzip = promisify(zlib.gunzip);
const gzip = promisify(zlib.gzip);

/**
* Read the contents from the compressed file.
*
* @async
* @params {String} filename
* @params {Boolean} compress
*/
const read = async (filename, compress) => {
const data = await readFile(filename + (compress ? '.gz' : ''));
const content = compress ? await gunzip(data) : data;

return JSON.parse(content.toString());
};

/**
* Write contents into a compressed file.
*
* @async
* @params {String} filename
* @params {Boolean} compress
* @params {String} result
*/
const write = async (filename, compress, result) => {
const content = JSON.stringify(result);

const data = compress ? await gzip(content) : content;
return writeFile(filename + (compress ? '.gz' : ''), data);
};

/**
* Build the filename for the cached file
*
* @params {String} source File source code
* @params {String} identifier
* @params {Object} options Options used
*
* @return {String}
*/
const filename = (source, identifier, options) => {
const hash = createHash('md4');

const contents = JSON.stringify({ source, options, identifier });

hash.update(contents);

return `${hash.digest('hex')}.json`;
};

/**
* Handle the cache
*
* @params {String} directory
* @params {Object} params
*/
const handleCache = async (directory, params) => {
const {
source,
options = {},
transform,
cacheIdentifier,
cacheDirectory,
cacheCompression,
} = params;

const file = join(directory, filename(source, cacheIdentifier, options));

try {
// No errors mean that the file was previously cached
// we just need to return it
return await read(file, cacheCompression);
// eslint-disable-next-line no-empty
} catch (err) {}

const fallback =
typeof cacheDirectory !== 'string' && directory !== os.tmpdir();

// Make sure the directory exists.
try {
fs.mkdirSync(directory, { recursive: true });
} catch (err) {
if (fallback) {
return handleCache(os.tmpdir(), params);
}

throw err;
}

// Otherwise just transform the file
// return it to the user asap and write it in cache
const result = await transform(source, options);

try {
await write(file, cacheCompression, result);
} catch (err) {
if (fallback) {
// Fallback to tmpdir if node_modules folder not writable
return handleCache(os.tmpdir(), params);
}

throw err;
}

return result;
};

/**
* Retrieve file from cache, or create a new one for future reads
*
* @async
* @param {Object} params
* @param {String} params.cacheDirectory Directory to store cached files
* @param {String} params.cacheIdentifier Unique identifier to bust cache
* @param {Boolean} params.cacheCompression
* @param {String} params.source Original contents of the file to be cached
* @param {Object} params.options Options to be given to the transform fn
* @param {Function} params.transform Function that will transform the
* original file and whose result will be
* cached
*
* @example
*
* cache({
* cacheDirectory: '.tmp/cache',
* cacheIdentifier: 'babel-loader-cachefile',
* cacheCompression: true,
* source: *source code from file*,
* options: {
* experimental: true,
* runtime: true
* },
* transform: function(source, options) {
* var content = *do what you need with the source*
* return content;
* }
* });
*/

module.exports = async (params) => {
let directory;

if (typeof params.cacheDirectory === 'string') {
directory = params.cacheDirectory;
} else {
if (defaultCacheDirectory === null) {
defaultCacheDirectory =
findCacheDir({ name: 'eslint-loader' }) || os.tmpdir();
}

directory = defaultCacheDirectory;
}

return handleCache(directory, params);
};