use anyhow::Result;
use bincode::{Decode, Encode};
use mime_guess::mime::TEXT_HTML_UTF_8;
use turbo_rcstr::RcStr;
use turbo_tasks::{
NonLocalValue, ReadRef, ResolvedVc, TaskInput, TryJoinIterExt, Vc, trace::TraceRawVcs,
};
use turbo_tasks_fs::{File, FileContent, FileSystemPath};
use turbo_tasks_hash::{Xxh3Hash64Hasher, encode_base64};
use turbopack_core::{
asset::{Asset, AssetContent},
chunk::{
ChunkableModule, ChunkingContext, ChunkingContextExt, EvaluatableAssets,
availability_info::AvailabilityInfo,
},
module::Module,
module_graph::{ModuleGraph, chunk_group_info::ChunkGroup},
output::{OutputAsset, OutputAssetsReference, OutputAssetsWithReferenced},
version::{Version, VersionedContent},
};
#[derive(
Clone, Debug, Eq, Hash, NonLocalValue, PartialEq, TaskInput, TraceRawVcs, Encode, Decode,
)]
pub struct DevHtmlEntry {
pub chunkable_module: ResolvedVc<Box<dyn ChunkableModule>>,
pub module_graph: ResolvedVc<ModuleGraph>,
pub chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
pub runtime_entries: Option<ResolvedVc<EvaluatableAssets>>,
}
/// The HTML entry point of the dev server.
///
/// Generates an HTML page that includes the ES and CSS chunks.
#[turbo_tasks::value(shared)]
#[derive(Clone)]
pub struct DevHtmlAsset {
path: FileSystemPath,
entries: Vec<DevHtmlEntry>,
body: Option<RcStr>,
}
#[turbo_tasks::value_impl]
impl OutputAssetsReference for DevHtmlAsset {
#[turbo_tasks::function]
fn references(self: Vc<Self>) -> Vc<OutputAssetsWithReferenced> {
self.chunk_group()
}
}
#[turbo_tasks::value_impl]
impl OutputAsset for DevHtmlAsset {
#[turbo_tasks::function]
fn path(&self) -> Vc<FileSystemPath> {
self.path.clone().cell()
}
}
#[turbo_tasks::value_impl]
impl Asset for DevHtmlAsset {
#[turbo_tasks::function]
fn content(self: Vc<Self>) -> Vc<AssetContent> {
self.html_content().content()
}
#[turbo_tasks::function]
fn versioned_content(self: Vc<Self>) -> Vc<Box<dyn VersionedContent>> {
Vc::upcast(self.html_content())
}
}
impl DevHtmlAsset {
/// Create a new dev HTML asset.
pub fn new(path: FileSystemPath, entries: Vec<DevHtmlEntry>) -> Vc<Self> {
DevHtmlAsset {
path,
entries,
body: None,
}
.cell()
}
/// Create a new dev HTML asset.
pub fn new_with_body(
path: FileSystemPath,
entries: Vec<DevHtmlEntry>,
body: RcStr,
) -> Vc<Self> {
DevHtmlAsset {
path,
entries,
body: Some(body),
}
.cell()
}
}
#[turbo_tasks::value_impl]
impl DevHtmlAsset {
#[turbo_tasks::function]
pub async fn with_path(self: Vc<Self>, path: FileSystemPath) -> Result<Vc<Self>> {
let mut html: DevHtmlAsset = self.owned().await?;
html.path = path;
Ok(html.cell())
}
#[turbo_tasks::function]
pub async fn with_body(self: Vc<Self>, body: RcStr) -> Result<Vc<Self>> {
let mut html: DevHtmlAsset = self.owned().await?;
html.body = Some(body);
Ok(html.cell())
}
}
#[turbo_tasks::value_impl]
impl DevHtmlAsset {
#[turbo_tasks::function]
async fn html_content(self: Vc<Self>) -> Result<Vc<DevHtmlAssetContent>> {
let this = self.await?;
let context_path = this.path.parent();
let mut chunk_paths = vec![];
for chunk in &*self.chunk_group().await?.assets.await? {
let chunk_path = &*chunk.path().await?;
if let Some(relative_path) = context_path.get_path_to(chunk_path) {
chunk_paths.push(format!("/{relative_path}").into());
}
}
Ok(DevHtmlAssetContent::new(chunk_paths, this.body.clone()))
}
#[turbo_tasks::function]
async fn chunk_group(&self) -> Result<Vc<OutputAssetsWithReferenced>> {
let all_chunk_groups = self
.entries
.iter()
.map(|entry| async move {
let &DevHtmlEntry {
chunkable_module,
chunking_context,
module_graph,
runtime_entries,
} = entry;
let asset_with_referenced = if let Some(runtime_entries) = runtime_entries {
let runtime_entries =
if let Some(evaluatable) = ResolvedVc::try_downcast(chunkable_module) {
runtime_entries
.with_entry(*evaluatable)
.to_resolved()
.await?
} else {
runtime_entries
};
chunking_context
.evaluated_chunk_group_assets(
chunkable_module.ident(),
ChunkGroup::Entry(
runtime_entries
.await?
.iter()
.map(|v| ResolvedVc::upcast(*v))
.collect(),
),
*module_graph,
AvailabilityInfo::root(),
)
.await?
} else {
chunking_context
.root_chunk_group_assets(
chunkable_module.ident(),
ChunkGroup::Entry(vec![ResolvedVc::upcast(chunkable_module)]),
*module_graph,
)
.await?
};
Ok((
asset_with_referenced.assets.await?,
asset_with_referenced.referenced_assets.await?,
asset_with_referenced.references.await?,
))
})
.try_join()
.await?;
let mut all_assets = Vec::new();
let mut all_referenced_assets = Vec::new();
let mut all_references = Vec::new();
for (asset, referenced_asset, reference) in all_chunk_groups {
all_assets.extend(asset);
all_referenced_assets.extend(referenced_asset);
all_references.extend(reference);
}
Ok(OutputAssetsWithReferenced {
assets: ResolvedVc::cell(all_assets),
referenced_assets: ResolvedVc::cell(all_referenced_assets),
references: ResolvedVc::cell(all_references),
}
.cell())
}
}
#[turbo_tasks::value(operation)]
struct DevHtmlAssetContent {
chunk_paths: Vec<RcStr>,
body: Option<RcStr>,
}
impl DevHtmlAssetContent {
fn new(chunk_paths: Vec<RcStr>, body: Option<RcStr>) -> Vc<Self> {
DevHtmlAssetContent { chunk_paths, body }.cell()
}
}
#[turbo_tasks::value_impl]
impl DevHtmlAssetContent {
#[turbo_tasks::function]
fn content(&self) -> Result<Vc<AssetContent>> {
let mut scripts = Vec::new();
let mut stylesheets = Vec::new();
for relative_path in &*self.chunk_paths {
if relative_path.ends_with(".js") {
scripts.push(format!("<script src=\"{relative_path}\"></script>"));
} else if relative_path.ends_with(".css") {
stylesheets.push(format!(
"<link data-turbopack rel=\"stylesheet\" href=\"{relative_path}\">"
));
} else {
anyhow::bail!("chunk with unknown asset type: {}", relative_path)
}
}
let body = match &self.body {
Some(body) => body.as_str(),
None => "",
};
let html: RcStr = format!(
"<!DOCTYPE html>\n<html>\n<head>\n{}\n</head>\n<body>\n{}\n{}\n</body>\n</html>",
stylesheets.join("\n"),
body,
scripts.join("\n"),
)
.into();
Ok(AssetContent::file(
FileContent::Content(File::from(html).with_content_type(TEXT_HTML_UTF_8)).cell(),
))
}
#[turbo_tasks::function]
async fn version(self: Vc<Self>) -> Result<Vc<DevHtmlAssetVersion>> {
let this = self.await?;
Ok(DevHtmlAssetVersion { content: this }.cell())
}
}
#[turbo_tasks::value_impl]
impl VersionedContent for DevHtmlAssetContent {
#[turbo_tasks::function]
fn content(self: Vc<Self>) -> Vc<AssetContent> {
self.content()
}
#[turbo_tasks::function]
fn version(self: Vc<Self>) -> Vc<Box<dyn Version>> {
Vc::upcast(self.version())
}
}
#[turbo_tasks::value(operation)]
struct DevHtmlAssetVersion {
content: ReadRef<DevHtmlAssetContent>,
}
#[turbo_tasks::value_impl]
impl Version for DevHtmlAssetVersion {
#[turbo_tasks::function]
fn id(&self) -> Vc<RcStr> {
let mut hasher = Xxh3Hash64Hasher::new();
for relative_path in &*self.content.chunk_paths {
hasher.write_ref(relative_path);
}
if let Some(body) = &self.content.body {
hasher.write_ref(body);
}
let hash = hasher.finish();
let hash = encode_base64(hash);
Vc::cell(hash.into())
}
}