Skip to content

Commit

Permalink
breaking: use webpack builtin cache
Browse files Browse the repository at this point in the history
  • Loading branch information
JLHwung committed Sep 4, 2024
1 parent a0c450d commit c73b722
Show file tree
Hide file tree
Showing 4 changed files with 165 additions and 29 deletions.
99 changes: 99 additions & 0 deletions src/cacheHandler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
const serialize = require("./serialize");
const transform = require("./transform");
const { promisify } = require("node:util");

/** @typedef {import("webpack").Compilation} Compilation */
/** @typedef {import("webpack").LoaderContext<{}>} LoaderContext */
/** @typedef {ReturnType<Compilation["getLogger"]>} WebpackLogger */
/** @typedef {ReturnType<Compilation["getCache"]>} CacheFacade */

const addTimestamps = async function (externalDependencies, getFileTimestamp) {
for (const depAndEmptyTimestamp of externalDependencies) {
try {
const [dep] = depAndEmptyTimestamp;
const { timestamp } = await getFileTimestamp(dep);
depAndEmptyTimestamp.push(timestamp);
} catch {
// ignore errors if timestamp is not available
}
}
};

const areExternalDependenciesModified = async function (
externalDepsWithTimestamp,
getFileTimestamp,
) {
for (const depAndTimestamp of externalDepsWithTimestamp) {
const [dep, timestamp] = depAndTimestamp;
let newTimestamp;
try {
newTimestamp = (await getFileTimestamp(dep)).timestamp;
} catch {
return true;
}
if (timestamp !== newTimestamp) {
return true;
}
}
return false;
};

/**
* @this {LoaderContext}
* @param {string} filename The input resource path
* @param {string} source The input source
* @param {object} options The Babel transform options
* @param {CacheFacade} cacheFacade The webpack cache facade instance
* @param {string} cacheIdentifier The extra cache identifier
* @param {WebpackLogger} logger
*/
async function handleCache(
filename,
source,
options = {},
cacheFacade,
cacheIdentifier,
logger,
) {
const getFileTimestamp = promisify((path, cb) => {
this._compilation.fileSystemInfo.getFileTimestamp(path, cb);
});
const hash = this.utils.createHash(
this._compilation.outputOptions.hashFunction,
);
const cacheKey = hash
.update(serialize([options, source, cacheIdentifier]))
.digest("hex");
logger.debug(`getting cache for '${filename}', cachekey '${cacheKey}'`);

const itemCache = cacheFacade.getItemCache(cacheKey, null);

let result = await itemCache.getPromise();
logger.debug(
result ? `found cache for '${filename}'` : `missed cache for '${filename}'`,
);
if (result) {
if (
await areExternalDependenciesModified(
result.externalDependencies,
getFileTimestamp,
)
) {
logger.debug(
`discarded cache for '${filename}' due to changes in external dependencies`,
);
result = null;
}
}

if (!result) {
logger.debug("applying Babel transform");
result = await transform(source, options);
await addTimestamps(result.externalDependencies, getFileTimestamp);
logger.debug(`caching result for '${filename}'`);
await itemCache.storePromise(result);
logger.debug(`cached result for '${filename}'`);
}
}

module.exports = handleCache;
43 changes: 25 additions & 18 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,12 @@ if (/^6\./.test(babel.version)) {
}

const { version } = require("../package.json");
const cache = require("./cache");
const cacheHandler = require("./cacheHandler");
const transform = require("./transform");
const injectCaller = require("./injectCaller");
const schema = require("./schema");

const { isAbsolute } = require("path");
const { promisify } = require("util");

function subscribe(subscriber, metadata, context) {
if (context[subscriber]) {
Expand Down Expand Up @@ -112,6 +111,23 @@ async function loader(source, inputSourceMap, overrides) {
),
);
}
if (typeof loaderOptions.cacheDirectory === "string") {
this.emitWarning(
new Error(
"babel-loader does not support customizing the cacheDirectory since it now uses the webpack builtin cache. You can use cacheDirectory: true and specify the webpack cache location instead via the webpack option `cache.cacheDirectory`.",
),
);
}
if ("cacheCompression" in loaderOptions) {
this.emitWarning(
new Error(
"The option `cacheCompression` has been removed since the babel-loader now uses the webpack builtin cache." +
loaderOptions.cacheCompression
? " You can specify the webpack option `cache.compression` to 'gzip' or 'brotli'."
: "",
),
);
}

logger.debug("normalizing loader options");
// Standardize on 'sourceMaps' as the key passed through to Webpack, so that
Expand Down Expand Up @@ -147,7 +163,6 @@ async function loader(source, inputSourceMap, overrides) {
delete programmaticOptions.customize;
delete programmaticOptions.cacheDirectory;
delete programmaticOptions.cacheIdentifier;
delete programmaticOptions.cacheCompression;
delete programmaticOptions.metadataSubscribers;

logger.debug("resolving Babel configs");
Expand Down Expand Up @@ -176,32 +191,24 @@ async function loader(source, inputSourceMap, overrides) {
}

const {
cacheDirectory = null,
cacheDirectory,
cacheIdentifier = "core" + transform.version + "," + "loader" + version,
cacheCompression = true,
metadataSubscribers = [],
} = loaderOptions;

let result;
if (cacheDirectory) {
logger.debug("cache is enabled");
const getFileTimestamp = promisify((path, cb) => {
this._compilation.fileSystemInfo.getFileTimestamp(path, cb);
});
const hash = this.utils.createHash(
this._compilation.outputOptions.hashFunction,
);
result = await cache({
const cacheFacade = this._compilation.getCache("babel-loader");
result = await cacheHandler.call(
this,
filename,
source,
options,
transform,
cacheDirectory,
cacheFacade,
cacheIdentifier,
cacheCompression,
hash,
getFileTimestamp,
logger,
});
);
} else {
logger.debug("cache is disabled, applying Babel transform");
result = await transform(source, options);
Expand Down
48 changes: 37 additions & 11 deletions test/cache.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import test from "node:test";
import fs from "node:fs";
import path from "node:path";
import assert from "node:assert/strict";
import webpack from "webpack";
import { webpackAsync } from "./helpers/webpackAsync.js";
import createTestDirectory from "./helpers/createTestDirectory.js";
import { fileURLToPath } from "node:url";
Expand Down Expand Up @@ -403,19 +404,30 @@ test("should cache result when there are external dependencies", async () => {

test("should output debug logs when stats.loggingDebug includes babel-loader", async () => {
const config = Object.assign({}, globalConfig, {
cache: {
type: "memory",
},
output: {
path: context.directory,
},
module: {
rules: [
{
test: /\.jsx?/,
loader: babelLoader,

exclude: /node_modules/,
options: {
cacheDirectory: true,
presets: ["@babel/preset-env"],
},
use: [
{
loader: babelLoader,
options: {
cacheDirectory: true,
presets: ["@babel/preset-env"],
},
},
{
loader: "./test/fixtures/uncacheable-passthrough-loader.cjs",
},
],
},
],
},
Expand All @@ -424,10 +436,24 @@ test("should output debug logs when stats.loggingDebug includes babel-loader", a
},
});

const stats = await webpackAsync(config);

assert.match(
stats.toString(config.stats),
/normalizing loader options\n\s+resolving Babel configs\n\s+cache is enabled\n\s+reading cache file.+\n\s+discarded cache as it can not be read\n\s+creating cache folder.+\n\s+applying Babel transform\n\s+writing result to cache file.+\n\s+added '.+babel.config.json' to webpack dependencies/,
);
return new Promise((resolve, reject) => {
const compiler = webpack(config);
compiler.run((err, stats) => {
if (err) reject(err);
assert.match(
stats.toString(config.stats),
/normalizing loader options\n\s+resolving Babel configs\n\s+cache is enabled\n\s+getting cache for.+\n\s+missed cache for.+\n\s+applying Babel transform\n\s+caching result for.+\n\s+cached result for.+\n\s+added '.+babel.config.json' to webpack dependencies/,
"The first run stat does not match the snapshot regex",
);
compiler.run((err, newStats) => {
if (err) reject(err);
assert.match(
newStats.toString(config.stats),
/normalizing loader options\n\s+resolving Babel configs\n\s+cache is enabled\n\s+getting cache for.+\n\s+found cache for.+\n\s+added '.+babel.config.json' to webpack dependencies/,
"The second run stat does not match the snapshot regex",
);
resolve();
});
});
});
});
4 changes: 4 additions & 0 deletions test/fixtures/uncacheable-passthrough-loader.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module.exports = function UncacheablePassthroughLoader(source) {
this.cacheable(false);
return source;
};

0 comments on commit c73b722

Please sign in to comment.