diff --git a/crates/rspack_plugin_runtime/src/chunk_prefetch_preload.rs b/crates/rspack_plugin_runtime/src/chunk_prefetch_preload.rs index ba2d55cf6bdf..78ec901ca2a1 100644 --- a/crates/rspack_plugin_runtime/src/chunk_prefetch_preload.rs +++ b/crates/rspack_plugin_runtime/src/chunk_prefetch_preload.rs @@ -1,6 +1,6 @@ use rspack_core::{ - ChunkGroupOrderKey, ChunkUkey, Compilation, CompilationAdditionalTreeRuntimeRequirements, Plugin, - RuntimeGlobals, + ChunkGroupOrderKey, ChunkUkey, Compilation, CompilationAdditionalChunkRuntimeRequirements, + CompilationAdditionalTreeRuntimeRequirements, Plugin, RuntimeGlobals, }; use rspack_error::Result; use rspack_hook::{plugin, plugin_hook}; @@ -14,14 +14,22 @@ use crate::runtime_module::{ #[derive(Debug, Default)] pub struct ChunkPrefetchPreloadPlugin; -#[plugin_hook(CompilationAdditionalTreeRuntimeRequirements for ChunkPrefetchPreloadPlugin)] -async fn additional_tree_runtime_requirements( +#[plugin_hook(CompilationAdditionalChunkRuntimeRequirements for ChunkPrefetchPreloadPlugin)] +async fn additional_chunk_runtime_requirements( &self, compilation: &mut Compilation, chunk_ukey: &ChunkUkey, runtime_requirements: &mut RuntimeGlobals, ) -> Result<()> { let chunk = compilation.chunk_by_ukey.expect_get(chunk_ukey); + if compilation + .chunk_graph + .get_number_of_entry_modules(chunk_ukey) + == 0 + { + return Ok(()); + } + if let Some(startup_child_chunks) = chunk.get_children_of_type_in_order(&ChunkGroupOrderKey::Prefetch, compilation, false) { @@ -33,7 +41,16 @@ async fn additional_tree_runtime_requirements( Box::new(ChunkPrefetchStartupRuntimeModule::new(startup_child_chunks)), )? } + Ok(()) +} +#[plugin_hook(CompilationAdditionalTreeRuntimeRequirements for ChunkPrefetchPreloadPlugin)] +async fn additional_tree_runtime_requirements( + &self, + compilation: &mut Compilation, + chunk_ukey: &ChunkUkey, + runtime_requirements: &mut RuntimeGlobals, +) -> Result<()> { let chunk = compilation.chunk_by_ukey.expect_get(chunk_ukey); let chunk_filter = |_: &ChunkUkey, __: &Compilation| true; let mut chunk_map = chunk.get_child_ids_by_orders_map(false, compilation, &chunk_filter); @@ -67,6 +84,10 @@ impl Plugin for ChunkPrefetchPreloadPlugin { .compilation_hooks .additional_tree_runtime_requirements .tap(additional_tree_runtime_requirements::new(self)); + ctx + .compilation_hooks + .additional_chunk_runtime_requirements + .tap(additional_chunk_runtime_requirements::new(self)); Ok(()) } } diff --git a/tests/rspack-test/configCases/web/prefetch-preload-runtime-chunk/chunk1-a.js b/tests/rspack-test/configCases/web/prefetch-preload-runtime-chunk/chunk1-a.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tests/rspack-test/configCases/web/prefetch-preload-runtime-chunk/chunk1-b.js b/tests/rspack-test/configCases/web/prefetch-preload-runtime-chunk/chunk1-b.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tests/rspack-test/configCases/web/prefetch-preload-runtime-chunk/chunk1-c.js b/tests/rspack-test/configCases/web/prefetch-preload-runtime-chunk/chunk1-c.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tests/rspack-test/configCases/web/prefetch-preload-runtime-chunk/chunk1.js b/tests/rspack-test/configCases/web/prefetch-preload-runtime-chunk/chunk1.js new file mode 100644 index 000000000000..60d6f1685b7d --- /dev/null +++ b/tests/rspack-test/configCases/web/prefetch-preload-runtime-chunk/chunk1.js @@ -0,0 +1,5 @@ +export default function() { + import(/* webpackPrefetch: true, webpackChunkName: "chunk1-a" */ "./chunk1-a"); + import(/* webpackPreload: true, webpackChunkName: "chunk1-b" */ "./chunk1-b"); + import(/* webpackPrefetch: 10, webpackChunkName: "chunk1-c" */ "./chunk1-c"); +} diff --git a/tests/rspack-test/configCases/web/prefetch-preload-runtime-chunk/chunk2.js b/tests/rspack-test/configCases/web/prefetch-preload-runtime-chunk/chunk2.js new file mode 100644 index 000000000000..a225cae317f4 --- /dev/null +++ b/tests/rspack-test/configCases/web/prefetch-preload-runtime-chunk/chunk2.js @@ -0,0 +1,4 @@ +export default function() { + import(/* webpackPrefetch: true, webpackChunkName: "chunk1-a" */ "./chunk1-a"); + import(/* webpackPreload: true, webpackChunkName: "chunk1-b" */ "./chunk1-b"); +} diff --git a/tests/rspack-test/configCases/web/prefetch-preload-runtime-chunk/index.js b/tests/rspack-test/configCases/web/prefetch-preload-runtime-chunk/index.js new file mode 100644 index 000000000000..65636fc9d5c2 --- /dev/null +++ b/tests/rspack-test/configCases/web/prefetch-preload-runtime-chunk/index.js @@ -0,0 +1,89 @@ +// This config need to be set on initial evaluation to be effective +__webpack_nonce__ = "nonce"; +__webpack_public_path__ = "https://example.com/public/path/"; + +it("should prefetch and preload child chunks on chunk load", () => { + let link, script; + + expect(document.head._children).toHaveLength(1); + + // Test prefetch from entry chunk + link = document.head._children[0]; + expect(link._type).toBe("link"); + expect(link.rel).toBe("prefetch"); + expect(link.href).toBe("https://example.com/public/path/chunk1.js"); + + const promise = import( + /* webpackChunkName: "chunk1", webpackPrefetch: true */ "./chunk1" + ); + + expect(document.head._children).toHaveLength(3); + + // Test normal script loading + script = document.head._children[1]; + expect(script._type).toBe("script"); + expect(script.src).toBe("https://example.com/public/path/chunk1.js"); + expect(script.getAttribute("nonce")).toBe("nonce"); + expect(script.crossOrigin).toBe("anonymous"); + expect(script.onload).toBeTypeOf("function"); + + // Test preload of chunk1-b + link = document.head._children[2]; + expect(link._type).toBe("link"); + expect(link.rel).toBe("preload"); + expect(link.as).toBe("script"); + expect(link.href).toBe("https://example.com/public/path/chunk1-b.js"); + expect(link.getAttribute("nonce")).toBe("nonce"); + expect(link.crossOrigin).toBe("anonymous"); + + // Run the script + __non_webpack_require__("./chunk1.js"); + + script.onload(); + + return promise.then(() => { + expect(document.head._children).toHaveLength(4); + + // Test prefetching for chunk1-c and chunk1-a in this order + link = document.head._children[2]; + expect(link._type).toBe("link"); + expect(link.rel).toBe("prefetch"); + expect(link.href).toBe("https://example.com/public/path/chunk1-c.js"); + expect(link.crossOrigin).toBe("anonymous"); + + link = document.head._children[3]; + expect(link._type).toBe("link"); + expect(link.rel).toBe("prefetch"); + expect(link.href).toBe("https://example.com/public/path/chunk1-a.js"); + expect(link.crossOrigin).toBe("anonymous"); + + const promise2 = import( + /* webpackChunkName: "chunk1", webpackPrefetch: true */ "./chunk1" + ); + + // Loading chunk1 again should not trigger prefetch/preload + expect(document.head._children).toHaveLength(4); + + const promise3 = import(/* webpackChunkName: "chunk2" */ "./chunk2"); + + expect(document.head._children).toHaveLength(5); + + // Test normal script loading + script = document.head._children[4]; + expect(script._type).toBe("script"); + expect(script.src).toBe("https://example.com/public/path/chunk2.js"); + expect(script.getAttribute("nonce")).toBe("nonce"); + expect(script.crossOrigin).toBe("anonymous"); + expect(script.onload).toBeTypeOf("function"); + + // Run the script + __non_webpack_require__("./chunk2.js"); + + script.onload(); + + return promise3.then(() => { + // Loading chunk2 again should not trigger prefetch/preload as it's already prefetch/preloaded + expect(document.head._children).toHaveLength(4); + }); + }); +}); diff --git a/tests/rspack-test/configCases/web/prefetch-preload-runtime-chunk/rspack.config.js b/tests/rspack-test/configCases/web/prefetch-preload-runtime-chunk/rspack.config.js new file mode 100644 index 000000000000..8332691f9167 --- /dev/null +++ b/tests/rspack-test/configCases/web/prefetch-preload-runtime-chunk/rspack.config.js @@ -0,0 +1,32 @@ +const fs = require("fs"); +const path = require("path"); + +/** @type {import("@rspack/core").Configuration} */ +module.exports = { + target: "web", + output: { + filename: "[name].js", + chunkFilename: "[name].js", + crossOriginLoading: "anonymous" + }, + performance: { + hints: false + }, + optimization: { + minimize: false, + runtimeChunk: { + name: entrypoint => `runtime~${entrypoint.name}`, + } + }, + plugins: [{ + apply(compiler) { + compiler.hooks.done.tap("DonePlugin", () => { + const output = compiler.options.output.path; + const runtime = fs.readFileSync(path.join(output, "runtime~main.js"), "utf-8"); + expect(runtime).not.toContain("webpack/runtime/chunk_prefetch_startup"); + const main = fs.readFileSync(path.join(output, "main.js"), "utf-8"); + expect(main).toContain("webpack/runtime/chunk_prefetch_startup"); + }); + } + }] +}; diff --git a/tests/rspack-test/configCases/web/prefetch-preload-runtime-chunk/test.config.js b/tests/rspack-test/configCases/web/prefetch-preload-runtime-chunk/test.config.js new file mode 100644 index 000000000000..75066e0f50c4 --- /dev/null +++ b/tests/rspack-test/configCases/web/prefetch-preload-runtime-chunk/test.config.js @@ -0,0 +1,5 @@ +module.exports = { + findBundle() { + return ["runtime~main.js", "main.js"]; + } +}; \ No newline at end of file