Skip to content

Commit d4181b8

Browse files
Support cache with external dependencies (#1033)
* Add tests from #984 # Conflicts: # .yarnrc.yml # src/index.js * migrate test to node test styles * feat: enable cache where there are external deps * chore: fix dead links in comments * fix lint errors * save dep and timestamp as tuple * simplify handleExternalDependencies interface * chore: create getFileTimestamp only when cache is enabled --------- Co-authored-by: liuxingbaoyu <[email protected]>
1 parent 7fcb533 commit d4181b8

File tree

5 files changed

+133
-19
lines changed

5 files changed

+133
-19
lines changed

.yarnrc.yml

+1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1+
enableGlobalCache: true
12
nodeLinker: node-modules
23
yarnPath: .yarn/releases/yarn-3.6.4.cjs

src/cache.js

+50-13
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,37 @@ const filename = function (source, identifier, options, hash) {
6464
return hash.digest("hex") + ".json";
6565
};
6666

67+
const addTimestamps = async function (externalDependencies, getFileTimestamp) {
68+
for (const depAndEmptyTimestamp of externalDependencies) {
69+
try {
70+
const [dep] = depAndEmptyTimestamp;
71+
const { timestamp } = await getFileTimestamp(dep);
72+
depAndEmptyTimestamp.push(timestamp);
73+
} catch {
74+
// ignore errors if timestamp is not available
75+
}
76+
}
77+
};
78+
79+
const areExternalDependenciesModified = async function (
80+
externalDepsWithTimestamp,
81+
getFileTimestamp,
82+
) {
83+
for (const depAndTimestamp of externalDepsWithTimestamp) {
84+
const [dep, timestamp] = depAndTimestamp;
85+
let newTimestamp;
86+
try {
87+
newTimestamp = (await getFileTimestamp(dep)).timestamp;
88+
} catch {
89+
return true;
90+
}
91+
if (timestamp !== newTimestamp) {
92+
return true;
93+
}
94+
}
95+
return false;
96+
};
97+
6798
/**
6899
* Handle the cache
69100
*
@@ -78,6 +109,7 @@ const handleCache = async function (directory, params) {
78109
cacheDirectory,
79110
cacheCompression,
80111
hash,
112+
getFileTimestamp,
81113
} = params;
82114

83115
const file = path.join(
@@ -88,7 +120,15 @@ const handleCache = async function (directory, params) {
88120
try {
89121
// No errors mean that the file was previously cached
90122
// we just need to return it
91-
return await read(file, cacheCompression);
123+
const result = await read(file, cacheCompression);
124+
if (
125+
!(await areExternalDependenciesModified(
126+
result.externalDependencies,
127+
getFileTimestamp,
128+
))
129+
) {
130+
return result;
131+
}
92132
} catch {
93133
// conitnue if cache can't be read
94134
}
@@ -111,20 +151,17 @@ const handleCache = async function (directory, params) {
111151
// Otherwise just transform the file
112152
// return it to the user asap and write it in cache
113153
const result = await transform(source, options);
154+
await addTimestamps(result.externalDependencies, getFileTimestamp);
114155

115-
// Do not cache if there are external dependencies,
116-
// since they might change and we cannot control it.
117-
if (!result.externalDependencies.length) {
118-
try {
119-
await write(file, cacheCompression, result);
120-
} catch (err) {
121-
if (fallback) {
122-
// Fallback to tmpdir if node_modules folder not writable
123-
return handleCache(os.tmpdir(), params);
124-
}
125-
126-
throw err;
156+
try {
157+
await write(file, cacheCompression, result);
158+
} catch (err) {
159+
if (fallback) {
160+
// Fallback to tmpdir if node_modules folder not writable
161+
return handleCache(os.tmpdir(), params);
127162
}
163+
164+
throw err;
128165
}
129166

130167
return result;

src/index.js

+8-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const injectCaller = require("./injectCaller");
2626
const schema = require("./schema");
2727

2828
const { isAbsolute } = require("path");
29+
const { promisify } = require("util");
2930

3031
function subscribe(subscriber, metadata, context) {
3132
if (context[subscriber]) {
@@ -176,6 +177,9 @@ async function loader(source, inputSourceMap, overrides) {
176177

177178
let result;
178179
if (cacheDirectory) {
180+
const getFileTimestamp = promisify((path, cb) => {
181+
this._compilation.fileSystemInfo.getFileTimestamp(path, cb);
182+
});
179183
const hash = this.utils.createHash(
180184
this._compilation.outputOptions.hashFunction,
181185
);
@@ -187,6 +191,7 @@ async function loader(source, inputSourceMap, overrides) {
187191
cacheIdentifier,
188192
cacheCompression,
189193
hash,
194+
getFileTimestamp,
190195
});
191196
} else {
192197
result = await transform(source, options);
@@ -207,7 +212,9 @@ async function loader(source, inputSourceMap, overrides) {
207212

208213
const { code, map, metadata, externalDependencies } = result;
209214

210-
externalDependencies?.forEach(dep => this.addDependency(dep));
215+
externalDependencies?.forEach(([dep]) => {
216+
this.addDependency(dep);
217+
});
211218
metadataSubscribers.forEach(subscriber => {
212219
subscribe(subscriber, metadata, this);
213220
});

src/transform.js

+4-2
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ module.exports = async function (source, options) {
1616

1717
// We don't return the full result here because some entries are not
1818
// really serializable. For a full list of properties see here:
19-
// https://github.com/babel/babel/blob/main/packages/babel-core/src/transformation/index.js
19+
// https://github.com/babel/babel/blob/main/packages/babel-core/src/transformation/index.ts
2020
// For discussion on this topic see here:
2121
// https://github.com/babel/babel-loader/pull/629
2222
const { ast, code, map, metadata, sourceType, externalDependencies } = result;
@@ -32,7 +32,9 @@ module.exports = async function (source, options) {
3232
metadata,
3333
sourceType,
3434
// Convert it from a Set to an Array to make it JSON-serializable.
35-
externalDependencies: Array.from(externalDependencies || []),
35+
externalDependencies: Array.from(externalDependencies || [], dep => [
36+
dep,
37+
]).sort(),
3638
};
3739
};
3840

test/cache.test.js

+70-3
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ test("should have one file per module", async () => {
222222
assert.deepEqual(stats.compilation.warnings, []);
223223

224224
const files = fs.readdirSync(context.cacheDirectory);
225-
assert.ok(files.length === 3);
225+
assert.strictEqual(files.length, 3);
226226
});
227227

228228
test("should generate a new file if the identifier changes", async () => {
@@ -276,7 +276,7 @@ test("should generate a new file if the identifier changes", async () => {
276276
);
277277

278278
const files = fs.readdirSync(context.cacheDirectory);
279-
assert.ok(files.length === 6);
279+
assert.strictEqual(files.length, 6);
280280
});
281281

282282
test("should allow to specify the .babelrc file", async () => {
@@ -331,5 +331,72 @@ test("should allow to specify the .babelrc file", async () => {
331331
const files = fs.readdirSync(context.cacheDirectory);
332332
// The two configs resolved to same Babel config because "fixtures/babelrc"
333333
// is { "presets": ["@babel/preset-env"] }
334-
assert.ok(files.length === 1);
334+
assert.strictEqual(files.length, 1);
335+
});
336+
337+
test("should cache result when there are external dependencies", async () => {
338+
const dep = path.join(cacheDir, "externalDependency.txt");
339+
340+
fs.writeFileSync(dep, "first update");
341+
342+
let counter = 0;
343+
344+
const config = Object.assign({}, globalConfig, {
345+
entry: path.join(__dirname, "fixtures/constant.js"),
346+
output: {
347+
path: context.directory,
348+
},
349+
module: {
350+
rules: [
351+
{
352+
test: /\.js$/,
353+
loader: babelLoader,
354+
options: {
355+
babelrc: false,
356+
configFile: false,
357+
cacheDirectory: context.cacheDirectory,
358+
plugins: [
359+
api => {
360+
api.cache.never();
361+
api.addExternalDependency(dep);
362+
return {
363+
visitor: {
364+
BooleanLiteral(path) {
365+
counter++;
366+
path.replaceWith(
367+
api.types.stringLiteral(fs.readFileSync(dep, "utf8")),
368+
);
369+
path.stop();
370+
},
371+
},
372+
};
373+
},
374+
],
375+
},
376+
},
377+
],
378+
},
379+
});
380+
381+
let stats = await webpackAsync(config);
382+
assert.deepEqual(stats.compilation.warnings, []);
383+
assert.deepEqual(stats.compilation.errors, []);
384+
385+
assert.ok(stats.compilation.fileDependencies.has(dep));
386+
assert.strictEqual(counter, 1);
387+
388+
stats = await webpackAsync(config);
389+
assert.deepEqual(stats.compilation.warnings, []);
390+
assert.deepEqual(stats.compilation.errors, []);
391+
392+
assert.ok(stats.compilation.fileDependencies.has(dep));
393+
assert.strictEqual(counter, 1);
394+
395+
fs.writeFileSync(dep, "second update");
396+
stats = await webpackAsync(config);
397+
assert.deepEqual(stats.compilation.warnings, []);
398+
assert.deepEqual(stats.compilation.errors, []);
399+
400+
assert.ok(stats.compilation.fileDependencies.has(dep));
401+
assert.strictEqual(counter, 2);
335402
});

0 commit comments

Comments
 (0)