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