|
1 | 1 | const fs = require("node:fs"); |
2 | 2 | const path = require("node:path"); |
3 | | -const { CopyRspackPlugin, Stats, rspack } = require("@rspack/core"); |
| 3 | +const { |
| 4 | + CopyRspackPlugin, |
| 5 | + Stats, |
| 6 | + lazyCompilationMiddleware, |
| 7 | + rspack |
| 8 | +} = require("@rspack/core"); |
4 | 9 |
|
5 | 10 | const fixtureRoot = path.resolve(__dirname, "js/copy-plugin-cache"); |
6 | 11 |
|
@@ -112,6 +117,9 @@ function watch(compiler) { |
112 | 117 | return new Promise((resolve, reject) => { |
113 | 118 | watching.close(error => (error ? reject(error) : resolve())); |
114 | 119 | }); |
| 120 | + }, |
| 121 | + invalidate() { |
| 122 | + watching.invalidate(); |
115 | 123 | } |
116 | 124 | }; |
117 | 125 | } |
@@ -384,6 +392,142 @@ describe("CopyRspackPlugin pattern cache", () => { |
384 | 392 | } |
385 | 393 | }); |
386 | 394 |
|
| 395 | + it("reuses a 2000-file copy pattern during an empty lazy watch rebuild", async () => { |
| 396 | + const fileCount = 2000; |
| 397 | + const root = path.join(fixtureRoot, "lazy-watch-cache-hit"); |
| 398 | + const beforeWatch = new Date(Date.now() - 3000); |
| 399 | + fs.rmSync(root, { recursive: true, force: true }); |
| 400 | + const entry = write(root, "src/index.js", "export const value = 'lazy';\n"); |
| 401 | + for (let index = 0; index < fileCount; index += 1) { |
| 402 | + const asset = write(root, `assets/${index}.txt`, `${index}\n`); |
| 403 | + fs.utimesSync(asset, beforeWatch, beforeWatch); |
| 404 | + } |
| 405 | + for (const filename of [ |
| 406 | + path.join(root, "src"), |
| 407 | + path.join(root, "assets"), |
| 408 | + entry |
| 409 | + ]) { |
| 410 | + fs.utimesSync(filename, beforeWatch, beforeWatch); |
| 411 | + } |
| 412 | + |
| 413 | + const compiler = rspack({ |
| 414 | + context: root, |
| 415 | + mode: "development", |
| 416 | + target: "web", |
| 417 | + devtool: false, |
| 418 | + cache: true, |
| 419 | + incremental: true, |
| 420 | + entry: "./src/index.js", |
| 421 | + lazyCompilation: { entries: true, imports: false }, |
| 422 | + output: { path: path.join(root, "dist"), filename: "main.js" }, |
| 423 | + plugins: [ |
| 424 | + new CopyRspackPlugin({ patterns: [{ from: "assets", to: "copied" }] }) |
| 425 | + ] |
| 426 | + }); |
| 427 | + const middleware = lazyCompilationMiddleware(compiler); |
| 428 | + const watching = watch(compiler); |
| 429 | + |
| 430 | + try { |
| 431 | + const initial = await watching.next(); |
| 432 | + expect(initial.error).toBeNull(); |
| 433 | + expect(initial.stats.hasErrors()).toBe(false); |
| 434 | + expect( |
| 435 | + initial.stats.compilation |
| 436 | + .getAssets() |
| 437 | + .filter(({ name }) => name.startsWith("copied/")) |
| 438 | + ).toHaveLength(fileCount); |
| 439 | + await new Promise(resolve => setTimeout(resolve, 200)); |
| 440 | + |
| 441 | + const bundle = fs.readFileSync(path.join(root, "dist/main.js"), "utf8"); |
| 442 | + const encoded = bundle.match(/var data = ("(?:[^"\\]|\\.)*")/)?.[1]; |
| 443 | + expect(encoded).toBeDefined(); |
| 444 | + const moduleId = JSON.parse(encoded); |
| 445 | + |
| 446 | + await new Promise((resolve, reject) => { |
| 447 | + Promise.resolve( |
| 448 | + middleware( |
| 449 | + { |
| 450 | + body: [moduleId], |
| 451 | + method: "POST", |
| 452 | + url: "/_rspack/lazy/trigger" |
| 453 | + }, |
| 454 | + { |
| 455 | + end: resolve, |
| 456 | + write() {}, |
| 457 | + writeHead(status) { |
| 458 | + expect(status).toBe(200); |
| 459 | + } |
| 460 | + }, |
| 461 | + reject |
| 462 | + ) |
| 463 | + ).catch(reject); |
| 464 | + }); |
| 465 | + const updated = await watching.next(); |
| 466 | + |
| 467 | + expect(updated.error).toBeNull(); |
| 468 | + expect(updated.stats.hasErrors()).toBe(false); |
| 469 | + expect(updated.stats.compilation.watchInvalidationKind).toBe("lazy"); |
| 470 | + expect(compiler.modifiedFiles).toEqual(new Set()); |
| 471 | + expect(compiler.removedFiles).toEqual(new Set()); |
| 472 | + expect(reusedPatterns(updated.stats.compilation)).toBe(1); |
| 473 | + expect( |
| 474 | + updated.stats.compilation |
| 475 | + .getAssets() |
| 476 | + .filter(({ name }) => name.startsWith("copied/")) |
| 477 | + ).toHaveLength(fileCount); |
| 478 | + } finally { |
| 479 | + await watching.close(); |
| 480 | + } |
| 481 | + }); |
| 482 | + |
| 483 | + it("does not reuse a copy pattern for an empty normal watch invalidation", async () => { |
| 484 | + const { root, compiler } = createCompiler("normal-watch-invalidation", [ |
| 485 | + { from: "assets/source", to: "copied" } |
| 486 | + ]); |
| 487 | + const source = write(root, "assets/source/one.txt", "before\n"); |
| 488 | + const beforeWatch = new Date(Date.now() - 3000); |
| 489 | + for (const filename of [ |
| 490 | + path.join(root, "src"), |
| 491 | + path.join(root, "src/index.js"), |
| 492 | + path.join(root, "assets"), |
| 493 | + path.join(root, "assets/source"), |
| 494 | + source |
| 495 | + ]) { |
| 496 | + fs.utimesSync(filename, beforeWatch, beforeWatch); |
| 497 | + } |
| 498 | + let mutateDuringWatchRun = false; |
| 499 | + compiler.hooks.watchRun.tap("copy-plugin-cache-test", () => { |
| 500 | + if (!mutateDuringWatchRun) return; |
| 501 | + mutateDuringWatchRun = false; |
| 502 | + const { atime, mtime } = fs.statSync(source); |
| 503 | + write(root, "assets/source/one.txt", "after\n"); |
| 504 | + fs.utimesSync(source, atime, mtime); |
| 505 | + }); |
| 506 | + const watching = watch(compiler); |
| 507 | + |
| 508 | + try { |
| 509 | + const initial = await watching.next(); |
| 510 | + expect(initial.error).toBeNull(); |
| 511 | + expect(initial.stats.hasErrors()).toBe(false); |
| 512 | + expect(asset(initial.stats.compilation, "copied/one.txt")).toBe("before\n"); |
| 513 | + await new Promise(resolve => setTimeout(resolve, 200)); |
| 514 | + |
| 515 | + mutateDuringWatchRun = true; |
| 516 | + watching.invalidate(); |
| 517 | + const updated = await watching.next(); |
| 518 | + |
| 519 | + expect(updated.error).toBeNull(); |
| 520 | + expect(updated.stats.hasErrors()).toBe(false); |
| 521 | + expect(updated.stats.compilation.watchInvalidationKind).toBe("normal"); |
| 522 | + expect(compiler.modifiedFiles).toEqual(new Set()); |
| 523 | + expect(compiler.removedFiles).toEqual(new Set()); |
| 524 | + expect(asset(updated.stats.compilation, "copied/one.txt")).toBe("after\n"); |
| 525 | + expect(reusedPatterns(updated.stats.compilation)).toBe(0); |
| 526 | + } finally { |
| 527 | + await watching.close(); |
| 528 | + } |
| 529 | + }); |
| 530 | + |
387 | 531 | it("preserves equal-priority copy order with interleaved cache hits and misses", async () => { |
388 | 532 | const patternCount = 64; |
389 | 533 | const patterns = Array.from({ length: patternCount }, (_, index) => ({ |
|
0 commit comments