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

breaking: use webpack builtin cache #1040

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
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
14 changes: 11 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,20 @@ The `options` passed here will be [merged](https://babeljs.io/docs/configuration

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.
* `cacheDirectory`: Default `false`. When set to `true`, Babel loader will use the [webpack builtin cache](https://webpack.js.org/configuration/cache/) to store the transformed code.
```js
// webpack.config.js
module.exports = {
...
cache: 'filesystem' // or 'memory'
}
```
Since webpack already caches loader results, it is recommended that you enable the webpack builtin cache and disable the babel-loader cache. In rare circumstances, such as when there is an uncacheable loader applied after babel-loader, the babel-loader cache can improve the build performance.

If you want to implement your own webpack cache backend, such as redis or lmdb, see [`./test/loader.test.js`](./test/loader.test.js) and search `custom webpack cache plugin` for an example.

* `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.

* `customize`: Default `null`. The path of a module that exports a `custom` callback [like the one that you'd pass to `.custom()`](#customized-loader). Since you already have to make a new file to use this, it is recommended that you instead use `.custom` to create a wrapper loader. Only use this if you _must_ continue using `babel-loader` directly, but still want to customize.

* `metadataSubscribers`: Default `[]`. Takes an array of context function names. E.g. if you passed ['myMetadataPlugin'], you'd assign a subscriber function to `context.myMetadataPlugin` within your webpack plugin's hooks & that function will be called with `metadata`. See [`./test/metadata.test.js`](./test/metadata.test.js) for an example.
Expand Down
5 changes: 1 addition & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,6 @@
"engines": {
"node": "^18.20.0 || ^20.10.0 || >=22.0.0"
},
"dependencies": {
"find-up": "^5.0.0"
},
"peerDependencies": {
"@babel/core": "^7.12.0",
"webpack": ">=5.61.0"
Expand Down Expand Up @@ -98,4 +95,4 @@
]
},
"packageManager": "[email protected]"
}
}
227 changes: 0 additions & 227 deletions src/cache.js

This file was deleted.

101 changes: 101 additions & 0 deletions src/cacheHandler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
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}'`);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cache log is updated from "writing result to cache file" to current wordings to reflect the fact that the loader is unaware of the cache implementation: it may be written to the filesystem, it may be written to a database or it is retained in the memory.

}

return result;
}

module.exports = handleCache;
Loading
Loading