Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Bugfix] Ensure stability of filename cache-keys #909

Merged
merged 5 commits into from
Jul 12, 2024
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 1 addition & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,7 @@ This loader also supports the following loader-specific option:

* `cacheDirectory`: Default `false`. When set, the given directory will be used to cache the results of the loader. Future webpack builds will attempt to read from the cache to avoid needing to run the potentially expensive Babel recompilation process on each run. If the value is set to `true` in options (`{cacheDirectory: true}`), the loader will use the default cache directory in `node_modules/.cache/babel-loader` or fallback to the default OS temporary file directory if no `node_modules` folder could be found in any root directory.

* `cacheIdentifier`: Default is a string composed by
- the `@babel/core`'s version and the `babel-loader`'s version
- the [merged](https://babeljs.io/docs/configuration#how-babel-merges-config-items) [Babel config](https://babeljs.io/docs/config-files), including options passed to `babel-loader` and the contents of `babel.config.js` or `.babelrc` file if they exist
- the value of the environment variable `BABEL_ENV` with a fallback to the `NODE_ENV` environment variable.
This can be set to a custom value to force cache busting if the identifier changes.
* `cacheIdentifier`: Default is a string composed by the `@babel/core`'s version and the `babel-loader`'s version. The final cache id will be determined by the input file path, the [merged](https://babeljs.io/docs/configuration#how-babel-merges-config-items) Babel config via `Babel.loadPartialConfigAsync` and the `cacheIdentifier`. The merged Babel config will be determined by the `babel.config.js` or `.babelrc` file if they exist, or the value of the environment variable `BABEL_ENV` and `NODE_ENV`. `cacheIdentifier` can be set to a custom value to force cache busting if the identifier changes.

* `cacheCompression`: Default `true`. When set, each Babel transform output will be compressed with Gzip. If you want to opt-out of cache compression, set it to `false` -- your project may benefit from this if it transpiles thousands of files.

Expand Down
7 changes: 3 additions & 4 deletions src/cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ const zlib = require("zlib");
const crypto = require("crypto");
const { promisify } = require("util");
const { readFile, writeFile, mkdir } = require("fs/promises");
// Lazily instantiated when needed
const findCacheDirP = import("find-cache-dir");

const transform = require("./transform");
// Lazily instantiated when needed
const serialize = require("./serialize");
let defaultCacheDirectory = null;

let hashType = "sha256";
Expand Down Expand Up @@ -70,9 +71,7 @@ const write = async function (filename, compress, result) {
const filename = function (source, identifier, options) {
const hash = crypto.createHash(hashType);

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

hash.update(contents);
hash.update(serialize([options, source, identifier]));

return hash.digest("hex") + ".json";
};
Expand Down
6 changes: 1 addition & 5 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -174,11 +174,7 @@ async function loader(source, inputSourceMap, overrides) {

const {
cacheDirectory = null,
cacheIdentifier = JSON.stringify({
options,
"@babel/core": transform.version,
"@babel/loader": version,
}),
cacheIdentifier = "core" + transform.version + "," + "loader" + version,
cacheCompression = true,
metadataSubscribers = [],
} = loaderOptions;
Expand Down
83 changes: 83 additions & 0 deletions src/serialize.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
var objToString = Object.prototype.toString;
var objKeys = Object.getOwnPropertyNames;

/**
* A custom Babel options serializer
*
* Intentional deviation from JSON.stringify:
* 1. Object properties are sorted before seralizing
* 2. The output is NOT a valid JSON: e.g.
* The output does not enquote strings, which means a JSON-like string '{"a":1}'
* will share the same result with an JS object { a: 1 }. This is not an issue
* for Babel options, but it can not be used for general serialization purpose
* 3. Only 20% slower than the native JSON.stringify on V8
*
* This function is a fork from https://github.com/nickyout/fast-stable-stringify
* @param {*} val Babel options
* @param {*} isArrayProp
* @returns serialized Babel options
*/
function serialize(val, isArrayProp) {
var i, max, str, keys, key, propVal, toStr;
if (val === true) {
return "!0";
}
if (val === false) {
return "!1";
}
switch (typeof val) {
case "object":
if (val === null) {
return null;
} else if (val.toJSON && typeof val.toJSON === "function") {
return serialize(val.toJSON(), isArrayProp);
} else {
toStr = objToString.call(val);
if (toStr === "[object Array]") {
str = "[";
max = val.length - 1;
for (i = 0; i < max; i++) {
str += serialize(val[i], true) + ",";
}
if (max > -1) {
str += serialize(val[i], true);
}
return str + "]";
} else if (toStr === "[object Object]") {
// only object is left
keys = objKeys(val).sort();
max = keys.length;
str = "{";
i = 0;
while (i < max) {
key = keys[i];
propVal = serialize(val[key], false);
if (propVal !== undefined) {
if (str) {
str += ",";
}
str += '"' + key + '":' + propVal;
}
i++;
}
return str + "}";
} else {
return JSON.stringify(val);
}
}
case "function":
case "undefined":
return isArrayProp ? null : undefined;
case "string":
return val;
default:
return isFinite(val) ? val : null;
}
}

module.exports = function (val) {
var returnVal = serialize(val, false);
if (returnVal !== undefined) {
return "" + returnVal;
}
};
4 changes: 3 additions & 1 deletion test/cache.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -323,5 +323,7 @@ test("should allow to specify the .babelrc file", async t => {
t.deepEqual(multiStats.stats[1].compilation.warnings, []);

const files = fs.readdirSync(t.context.cacheDirectory);
t.true(files.length === 2);
// The two configs resolved to same Babel config because "fixtures/babelrc"
// is { "presets": ["@babel/preset-env"] }
t.true(files.length === 1);
});
Loading