Skip to content

Commit

Permalink
feat: add chunk prefetch and preload functionality with EJS templates
Browse files Browse the repository at this point in the history
  • Loading branch information
GiveMe-A-Name committed Jan 23, 2025
1 parent 7075fd2 commit 3a406fa
Show file tree
Hide file tree
Showing 32 changed files with 606 additions and 586 deletions.
65 changes: 24 additions & 41 deletions crates/rspack_plugin_runtime/src/runtime_module/auto_public_path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use rspack_core::{
get_js_chunk_filename_template, impl_runtime_module,
rspack_sources::{BoxSource, RawStringSource, SourceExt},
ChunkUkey, Compilation, OutputOptions, PathData, RuntimeGlobals, RuntimeModule,
RuntimeModuleStage, SourceType,
RuntimeModuleStage, RuntimeTemplate, SourceType,
};

use super::utils::get_undo_path;
Expand Down Expand Up @@ -34,6 +34,13 @@ impl RuntimeModule for AutoPublicPathRuntimeModule {
RuntimeModuleStage::Attach
}

fn template(&self) -> Vec<(String, String)> {
vec![(
self.id.to_string(),
include_str!("runtime/auto_public_path.ejs").to_string(),
)]
}

fn generate(&self, compilation: &Compilation) -> rspack_error::Result<BoxSource> {
let chunk = self.chunk.expect("The chunk should be attached");
let chunk = compilation.chunk_by_ukey.expect_get(&chunk);
Expand Down Expand Up @@ -63,15 +70,22 @@ impl RuntimeModule for AutoPublicPathRuntimeModule {
)?;
Ok(
RawStringSource::from(auto_public_path_template(
&compilation.runtime_template,
&self.id,
&filename,
&compilation.options.output,
))
)?)
.boxed(),
)
}
}

fn auto_public_path_template(filename: &str, output: &OutputOptions) -> String {
fn auto_public_path_template(
runtime_template: &RuntimeTemplate,
id: &str,
filename: &str,
output: &OutputOptions,
) -> rspack_error::Result<String> {
let output_path = output.path.as_str().to_string();
let undo_path = get_undo_path(filename, output_path, false);
let assign = if undo_path.is_empty() {
Expand All @@ -82,45 +96,14 @@ fn auto_public_path_template(filename: &str, output: &OutputOptions) -> String {
RuntimeGlobals::PUBLIC_PATH
)
};
let global = RuntimeGlobals::GLOBAL.name();
let import_meta_name = output.import_meta_name.clone();

let script_url_template = if output.script_type.eq("module") {
format!(
r#"var scriptUrl;
if (typeof {import_meta_name}.url === "string") scriptUrl = {import_meta_name}.url
"#
)
.to_string()
} else {
format!(
r#"var scriptUrl;
if ({global}.importScripts) scriptUrl = {global}.location + "";
var document = {global}.document;
if (!scriptUrl && document) {{
// Technically we could use `document.currentScript instanceof window.HTMLScriptElement`,
// but an attacker could try to inject `<script>HTMLScriptElement = HTMLImageElement</script>`
// and use `<img name="currentScript" src="https://attacker.controlled.server/"></img>`
if (document.currentScript && document.currentScript.tagName.toUpperCase() === 'SCRIPT') scriptUrl = document.currentScript.src;
if (!scriptUrl) {{
var scripts = document.getElementsByTagName("script");
if (scripts.length) {{
var i = scripts.length - 1;
while (i > -1 && (!scriptUrl || !/^http(s?):/.test(scriptUrl))) scriptUrl = scripts[i--].src;
}}
}}
}}
"#
)
};
format!(
r#"
{script_url_template}
// When supporting browsers where an automatic publicPath is not supported you must specify an output.publicPath manually via configuration",
// or pass an empty string ("") and set the __webpack_public_path__ variable from your code to use your own logic.',
if (!scriptUrl) throw new Error("Automatic publicPath is not supported in this browser");
scriptUrl = scriptUrl.replace(/#.*$/, "").replace(/\?.*$/, "").replace(/\/[^\/]+$/, "/");
{assign}
"#
runtime_template.render(
id,
Some(serde_json::json!({
"script_type": output.script_type,
"IMPORT_META_NAME": import_meta_name,
"ASSIGN": assign
})),
)
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
use cow_utils::CowUtils;
use rspack_collections::Identifier;
use rspack_core::{
impl_runtime_module,
Expand Down Expand Up @@ -36,15 +35,22 @@ impl RuntimeModule for ChunkPrefetchPreloadFunctionRuntimeModule {
self.id
}

fn generate(&self, _: &Compilation) -> rspack_error::Result<BoxSource> {
Ok(
RawStringSource::from(
include_str!("runtime/chunk_prefetch_preload_function.js")
.cow_replace("$RUNTIME_FUNCTION$", &self.runtime_function.to_string())
.cow_replace("$RUNTIME_HANDLERS$", &self.runtime_handlers.to_string())
.into_owned(),
)
.boxed(),
)
fn template(&self) -> Vec<(String, String)> {
vec![(
self.id.to_string(),
include_str!("runtime/chunk_prefetch_preload_function.ejs").to_string(),
)]
}

fn generate(&self, compilation: &Compilation) -> rspack_error::Result<BoxSource> {
let source = compilation.runtime_template.render(
&self.id,
Some(serde_json::json!({
"RUNTIME_HANDLERS": &self.runtime_handlers.to_string(),
"RUNTIME_FUNCTION": &self.runtime_function.to_string(),
})),
)?;

Ok(RawStringSource::from(source).boxed())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,17 @@ impl RuntimeModule for ChunkPrefetchStartupRuntimeModule {
self.chunk = Some(chunk);
}

fn template(&self) -> Vec<(String, String)> {
vec![(
self.id.to_string(),
include_str!("runtime/chunk_prefetch_startup.ejs").to_string(),
)]
}

fn generate(&self, compilation: &Compilation) -> rspack_error::Result<BoxSource> {
let chunk_ukey = self.chunk.expect("chunk do not attached");
Ok(
RawStringSource::from(
self

let source = self
.startup_chunks
.iter()
.map(|(group_chunks, child_chunks)| {
Expand Down Expand Up @@ -85,21 +91,15 @@ impl RuntimeModule for ChunkPrefetchStartupRuntimeModule {
}
};

format!(
r#"
{}(0, {}, function() {{
{}
}}, 5);
"#,
RuntimeGlobals::ON_CHUNKS_LOADED,
serde_json::to_string(&group_chunk_ids).expect("invalid json tostring"),
body
)
})
.join("\n"),
)
.boxed(),
)
let source = compilation.runtime_template.render(&self.id, Some(serde_json::json!({
"GROUP_CHUNK_IDS": serde_json::to_string(&group_chunk_ids).expect("invalid json tostring"),
"BODY": body,
})))?;

Ok(source)
}).collect::<rspack_error::Result<Vec<String>>>()?.join("\n");

Ok(RawStringSource::from(source).boxed())
}

fn stage(&self) -> RuntimeModuleStage {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
use std::hash::BuildHasherDefault;

use cow_utils::CowUtils;
use indexmap::IndexMap;
use rspack_cacheable::with::AsMap;
use rspack_collections::Identifier;
Expand Down Expand Up @@ -34,18 +33,21 @@ impl RuntimeModule for ChunkPrefetchTriggerRuntimeModule {
self.id
}

fn generate(&self, _: &Compilation) -> rspack_error::Result<BoxSource> {
Ok(
RawStringSource::from(
include_str!("runtime/chunk_prefetch_trigger.js")
.cow_replace(
"$CHUNK_MAP$",
&serde_json::to_string(&self.chunk_map).expect("invalid json tostring"),
)
.into_owned(),
)
.boxed(),
)
fn template(&self) -> Vec<(String, String)> {
vec![(
self.id.to_string(),
include_str!("runtime/chunk_prefetch_trigger.ejs").to_string(),
)]
}

fn generate(&self, compilation: &Compilation) -> rspack_error::Result<BoxSource> {
let source = compilation.runtime_template.render(
&self.id,
Some(serde_json::json!({
"CHUNK_MAP": &self.chunk_map,
})),
)?;
Ok(RawStringSource::from(source).boxed())
}

fn stage(&self) -> RuntimeModuleStage {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
use std::hash::BuildHasherDefault;

use cow_utils::CowUtils;
use indexmap::IndexMap;
use rspack_cacheable::with::AsMap;
use rspack_collections::Identifier;
Expand Down Expand Up @@ -34,18 +33,22 @@ impl RuntimeModule for ChunkPreloadTriggerRuntimeModule {
self.id
}

fn generate(&self, _: &Compilation) -> rspack_error::Result<BoxSource> {
Ok(
RawStringSource::from(
include_str!("runtime/chunk_preload_trigger.js")
.cow_replace(
"$CHUNK_MAP$",
&serde_json::to_string(&self.chunk_map).expect("invalid json tostring"),
)
.into_owned(),
)
.boxed(),
)
fn template(&self) -> Vec<(String, String)> {
vec![(
self.id.to_string(),
include_str!("runtime/chunk_preload_trigger.ejs").to_string(),
)]
}

fn generate(&self, compilation: &Compilation) -> rspack_error::Result<BoxSource> {
let source = compilation.runtime_template.render(
&self.id,
Some(serde_json::json!({
"CHUNK_MAP": &self.chunk_map,
})),
)?;

Ok(RawStringSource::from(source).boxed())
}

fn stage(&self) -> RuntimeModuleStage {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,16 @@ impl RuntimeModule for CreateFakeNamespaceObjectRuntimeModule {
self.id
}

fn generate(&self, _compilation: &Compilation) -> rspack_error::Result<BoxSource> {
Ok(
RawStringSource::from_static(include_str!("runtime/create_fake_namespace_object.js")).boxed(),
)
fn template(&self) -> Vec<(String, String)> {
vec![(
self.id.to_string(),
include_str!("runtime/create_fake_namespace_object.ejs").to_string(),
)]
}

fn generate(&self, compilation: &Compilation) -> rspack_error::Result<BoxSource> {
let source = compilation.runtime_template.render(&self.id, None)?;

Ok(RawStringSource::from(source).boxed())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
var scriptUrl;
<% if (script_type == "module") { %>
if (typeof <%- IMPORT_META_NAME %>.url === "string") scriptUrl = <%- IMPORT_META_NAME %>.url
<% } else { %>
if (<%- GLOBAL %>.importScripts) scriptUrl = <%- GLOBAL %>.location + "";
var document = <%- GLOBAL %>.document;
if (!scriptUrl && document) {
// Technically we could use `document.currentScript instanceof window.HTMLScriptElement`,
// but an attacker could try to inject `<script>HTMLScriptElement = HTMLImageElement</script>`
// and use `<img name="currentScript" src="https://attacker.controlled.server/"></img>`
if (document.currentScript && document.currentScript.tagName.toUpperCase() === 'SCRIPT') scriptUrl = document.currentScript.src;
if (!scriptUrl) {
var scripts = document.getElementsByTagName("script");
if (scripts.length) {
var i = scripts.length - 1;
while (i > -1 && (!scriptUrl || !/^http(s?):/.test(scriptUrl))) scriptUrl = scripts[i--].src;
}
}
}
<% } %>
// When supporting browsers where an automatic publicPath is not supported you must specify an output.publicPath manually via configuration",
// or pass an empty string ("") and set the __webpack_public_path__ variable from your code to use your own logic.',
if (!scriptUrl) throw new Error("Automatic publicPath is not supported in this browser");
scriptUrl = scriptUrl.replace(/#.*$/, "").replace(/\?.*$/, "").replace(/\/[^\/]+$/, "/");
<%- ASSIGN %>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<%- RUNTIME_HANDLERS %> = {};
<%- RUNTIME_FUNCTION %> = <%- basicFunction("chunkId") %> {
Object.keys(<%- RUNTIME_HANDLERS %>).map(function (key) {
<%- RUNTIME_HANDLERS %>[key](chunkId);
});
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<%- ON_CHUNKS_LOADED %>(0, <%- GROUP_CHUNK_IDS %>, <%- basicFunction("") %> {
<%- BODY %>
}, 5);
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
var chunkToChildrenMap = <%- CHUNK_MAP %>;
<%- ENSURE_CHUNK_HANDLERS %>.prefetch = <%- basicFunction("chunkId, promises") %> {
Promise.all(promises).then(<%- basicFunction("") %> {
var chunks = chunkToChildrenMap[chunkId];
Array.isArray(chunks) && chunks.map(<%- PREFETCH_CHUNK %>);
});
};

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
var chunkToChildrenMap = <%- CHUNK_MAP %>;
<%- ENSURE_CHUNK_HANDLERS %>.preload = <%- basicFunction("chunkId") %> {
var chunks = chunkToChildrenMap[chunkId];
Array.isArray(chunks) && chunks.map(<%- PRELOAD_CHUNK %>);
};

This file was deleted.

Loading

0 comments on commit 3a406fa

Please sign in to comment.