Skip to main content

turbopack_browser/ecmascript/list/
content.rs

1use std::io::Write;
2
3use anyhow::{Context, Result};
4use bincode::{Decode, Encode};
5use either::Either;
6use indoc::writedoc;
7use serde::{Deserialize, Serialize};
8use turbo_rcstr::RcStr;
9use turbo_tasks::{FxIndexMap, NonLocalValue, ResolvedVc, TryJoinIterExt, Vc, trace::TraceRawVcs};
10use turbo_tasks_fs::{File, FileContent};
11use turbopack_core::{
12    asset::{Asset, AssetContent},
13    chunk::ChunkingContext,
14    code_builder::{Code, CodeBuilder},
15    output::OutputAsset,
16    version::{Update, Version, VersionedContent},
17};
18use turbopack_ecmascript::{
19    chunk_list::{
20        update::update_chunk_list,
21        version::{ChunkListVersion, compute_chunk_list_version},
22    },
23    utils::StringifyJs,
24};
25
26use super::asset::{EcmascriptDevChunkList, EcmascriptDevChunkListSource};
27use crate::chunking_context::{
28    CURRENT_CHUNK_METHOD_DOCUMENT_CURRENT_SCRIPT_EXPR, CurrentChunkMethod,
29};
30
31#[derive(
32    Clone, Debug, Serialize, Deserialize, TraceRawVcs, PartialEq, Eq, NonLocalValue, Encode, Decode,
33)]
34enum CurrentChunkMethodWithData {
35    StringLiteral(RcStr),
36    DocumentCurrentScript,
37}
38
39/// Contents of an [`EcmascriptDevChunkList`].
40#[turbo_tasks::value]
41pub struct EcmascriptDevChunkListContent {
42    current_chunk_method: CurrentChunkMethodWithData,
43    #[bincode(with = "turbo_bincode::indexmap")]
44    pub(super) chunks_contents: FxIndexMap<String, ResolvedVc<Box<dyn VersionedContent>>>,
45    source: EcmascriptDevChunkListSource,
46    /// The global variable name used for chunk loading (derived from chunkLoadingGlobal config).
47    chunk_loading_global: RcStr,
48}
49
50#[turbo_tasks::value_impl]
51impl EcmascriptDevChunkListContent {
52    /// Creates a new [`EcmascriptDevChunkListContent`].
53    #[turbo_tasks::function]
54    pub async fn new(chunk_list: Vc<EcmascriptDevChunkList>) -> Result<Vc<Self>> {
55        let chunk_list_ref = chunk_list.await?;
56        let output_root = chunk_list_ref.chunking_context.output_root().await?;
57        let current_chunk_method = match *chunk_list_ref
58            .chunking_context
59            .current_chunk_method()
60            .await?
61        {
62            CurrentChunkMethod::StringLiteral => {
63                let path = output_root
64                    .get_path_to(&*chunk_list.path().await?)
65                    .context("chunk list path not in output root")?
66                    .into();
67                CurrentChunkMethodWithData::StringLiteral(path)
68            }
69            CurrentChunkMethod::DocumentCurrentScript => {
70                CurrentChunkMethodWithData::DocumentCurrentScript
71            }
72        };
73        let chunk_loading_global = (*chunk_list_ref
74            .chunking_context
75            .chunk_loading_global()
76            .await?)
77            .clone();
78        Ok(EcmascriptDevChunkListContent {
79            current_chunk_method,
80            chunks_contents: chunk_list_ref
81                .chunks
82                .await?
83                .iter()
84                .map(async |chunk| {
85                    Ok((
86                        output_root
87                            .get_path_to(&*chunk.path().await?)
88                            .map(|path| path.to_string()),
89                        chunk.versioned_content().to_resolved().await?,
90                    ))
91                })
92                .try_join()
93                .await?
94                .into_iter()
95                .filter_map(|(path, content)| path.map(|path| (path, content)))
96                .collect(),
97            source: chunk_list_ref.source,
98            chunk_loading_global,
99        }
100        .cell())
101    }
102
103    /// Computes the version of this content.
104    #[turbo_tasks::function]
105    pub async fn version(&self) -> Result<Vc<ChunkListVersion>> {
106        compute_chunk_list_version(&self.chunks_contents).await
107    }
108
109    #[turbo_tasks::function]
110    pub(super) async fn code(self: Vc<Self>) -> Result<Vc<Code>> {
111        let this = self.await?;
112
113        let chunks = this
114            .chunks_contents
115            .keys()
116            .map(|s| s.as_str())
117            .collect::<Vec<_>>();
118
119        let script_or_path = match &this.current_chunk_method {
120            CurrentChunkMethodWithData::StringLiteral(path) => Either::Left(StringifyJs(path)),
121            CurrentChunkMethodWithData::DocumentCurrentScript => {
122                Either::Right(CURRENT_CHUNK_METHOD_DOCUMENT_CURRENT_SCRIPT_EXPR)
123            }
124        };
125
126        let mut code = CodeBuilder::default();
127
128        // When loaded, JS chunks must register themselves with the `TURBOPACK` global
129        // variable. Similarly, we register the chunk list with the
130        // `{chunk_loading_global}_CHUNK_LISTS` global variable.
131        let chunk_lists_global = format!("{}_CHUNK_LISTS", this.chunk_loading_global);
132        writedoc!(
133            code,
134            // `||=` would be better but we need to be es2020 compatible
135            //`x || (x = default)` is better than `x = x || default` simply because we avoid _writing_ the property in the common case.
136            r#"
137                (globalThis[{chunk_lists_global}] || (globalThis[{chunk_lists_global}] = [])).push({{
138                    script: {script_or_path},
139                    chunks: {chunks},
140                    source: {source}
141                }});
142            "#,
143            chunk_lists_global = StringifyJs(&chunk_lists_global),
144            chunks = StringifyJs(&chunks),
145            source = StringifyJs(&this.source),
146        )?;
147
148        Ok(Code::cell(code.build()))
149    }
150}
151
152#[turbo_tasks::value_impl]
153impl VersionedContent for EcmascriptDevChunkListContent {
154    #[turbo_tasks::function]
155    async fn content(self: Vc<Self>) -> Result<Vc<AssetContent>> {
156        let code = self.code().await?;
157        Ok(AssetContent::file(
158            FileContent::Content(File::from(code.source_code().clone())).cell(),
159        ))
160    }
161
162    #[turbo_tasks::function]
163    fn version(self: Vc<Self>) -> Vc<Box<dyn Version>> {
164        Vc::upcast(self.version())
165    }
166
167    #[turbo_tasks::function]
168    async fn update(
169        self: ResolvedVc<Self>,
170        from_version: ResolvedVc<Box<dyn Version>>,
171    ) -> Result<Vc<Update>> {
172        let this = self.await?;
173        update_chunk_list(&this.chunks_contents, self.version(), from_version).await
174    }
175}