diff --git a/.yarnrc.yml b/.yarnrc.yml index ef12e5ef..b041d68e 100644 --- a/.yarnrc.yml +++ b/.yarnrc.yml @@ -1 +1,4 @@ yarnPath: .yarn/releases/yarn-3.2.3.cjs +enableGlobalCache: true +nodeLinker: node-modules + diff --git a/src/cache.js b/src/cache.js index dd26c5ec..7c04ba73 100644 --- a/src/cache.js +++ b/src/cache.js @@ -90,9 +90,16 @@ const handleCache = async function (directory, params) { cacheIdentifier, cacheDirectory, cacheCompression, + cachedDepMtimes, } = params; - const file = path.join(directory, filename(source, cacheIdentifier, options)); + const file = path.join( + directory, + filename(source, cacheIdentifier, { + options, + __cachedDepMtimes: cachedDepMtimes, + }), + ); try { // No errors mean that the file was previously cached @@ -119,19 +126,15 @@ const handleCache = async function (directory, params) { // return it to the user asap and write it in cache const result = await transform(source, options); - // Do not cache if there are external dependencies, - // since they might change and we cannot control it. - if (!result.externalDependencies.length) { - 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; + 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; diff --git a/src/index.js b/src/index.js index 5ba55e3e..18962f89 100644 --- a/src/index.js +++ b/src/index.js @@ -26,6 +26,7 @@ const injectCaller = require("./injectCaller"); const schema = require("./schema"); const { isAbsolute } = require("path"); +const fs = require("fs"); const validateOptions = require("schema-utils").validate; function subscribe(subscriber, metadata, context) { @@ -39,19 +40,22 @@ module.exports.custom = makeLoader; function makeLoader(callback) { const overrides = callback ? callback(babel) : undefined; + const data = { + cachedDepMtimes: { __proto__: null }, + }; return function (source, inputSourceMap) { // Make the loader async const callback = this.async(); - loader.call(this, source, inputSourceMap, overrides).then( + loader.call(this, source, inputSourceMap, overrides, data).then( args => callback(null, ...args), err => callback(err), ); }; } -async function loader(source, inputSourceMap, overrides) { +async function loader(source, inputSourceMap, overrides, data) { const filename = this.resourcePath; let loaderOptions = this.getOptions(); @@ -185,6 +189,16 @@ async function loader(source, inputSourceMap, overrides) { let result; if (cacheDirectory) { + const cachedDepMtimes = []; + for (const dep of Object.keys(data.cachedDepMtimes)) { + let mtime = 0; + try { + mtime = fs.statSync(dep).mtimeMs; + } catch (error) {} + cachedDepMtimes.push(dep + mtime); + } + cachedDepMtimes.sort(); + result = await cache({ source, options, @@ -192,6 +206,7 @@ async function loader(source, inputSourceMap, overrides) { cacheDirectory, cacheIdentifier, cacheCompression, + cachedDepMtimes, }); } else { result = await transform(source, options); @@ -212,7 +227,17 @@ async function loader(source, inputSourceMap, overrides) { const { code, map, metadata, externalDependencies } = result; - externalDependencies?.forEach(dep => this.addDependency(dep)); + externalDependencies?.forEach(dep => { + if (data.cachedDepMtimes[dep] == null) { + let mtime = 0; + try { + mtime = fs.statSync(dep).mtimeMs; + } catch (error) {} + data.cachedDepMtimes[dep] = mtime; + } + this.addDependency(dep); + }); + metadataSubscribers.forEach(subscriber => { subscribe(subscriber, metadata, this); }); diff --git a/test/cache.test.js b/test/cache.test.js index 8fa0c3e2..7e940966 100644 --- a/test/cache.test.js +++ b/test/cache.test.js @@ -389,3 +389,90 @@ test.cb("should allow to specify the .babelrc file", t => { }); }); }); + +test.cb("should cache external dependencies", t => { + const dep = path.join(cacheDir, "externalDependency.txt"); + + fs.writeFileSync(dep, "123"); + + let counter = 0; + + const config = Object.assign({}, globalConfig, { + entry: path.join(__dirname, "fixtures/constant.js"), + output: { + path: t.context.directory, + }, + module: { + rules: [ + { + test: /\.js$/, + loader: babelLoader, + options: { + babelrc: false, + configFile: false, + cacheDirectory: t.context.cacheDirectory, + plugins: [ + api => { + api.cache.never(); + api.addExternalDependency(dep); + return { + visitor: { + BooleanLiteral(path) { + counter++; + path.replaceWith( + api.types.stringLiteral(fs.readFileSync(dep, "utf8")), + ); + path.stop(); + }, + }, + }; + }, + ], + }, + }, + ], + }, + }); + + webpack(config, (err, stats) => { + t.deepEqual(stats.compilation.warnings, []); + t.deepEqual(stats.compilation.errors, []); + + t.true(stats.compilation.fileDependencies.has(dep)); + + t.is(counter, 1); + + webpack(config, (err, stats) => { + t.deepEqual(stats.compilation.warnings, []); + t.deepEqual(stats.compilation.errors, []); + + t.true(stats.compilation.fileDependencies.has(dep)); + + t.is(counter, 2); + + webpack(config, (err, stats) => { + t.deepEqual(stats.compilation.warnings, []); + t.deepEqual(stats.compilation.errors, []); + + t.true(stats.compilation.fileDependencies.has(dep)); + + t.is(counter, 2); + + fs.writeFileSync(dep, "456"); + + setTimeout(() => { + webpack(config, (err, stats) => { + t.deepEqual(stats.compilation.warnings, []); + t.deepEqual(stats.compilation.errors, []); + + t.true(stats.compilation.fileDependencies.has(dep)); + + t.is(counter, 3); + + t.end(); + }); + }, 1000); + }); + }); + }); +});