Skip to main content

turbopack_browser/ecmascript/
content.rs

1use std::io::Write;
2
3use anyhow::Result;
4use either::Either;
5use turbo_rcstr::RcStr;
6use turbo_tasks::{ResolvedVc, Vc, turbobail};
7use turbo_tasks_fs::{File, FileContent};
8use turbopack_core::{
9    asset::AssetContent,
10    chunk::{ChunkingContext, MinifyType, ModuleId},
11    code_builder::{Code, CodeBuilder},
12    output::OutputAsset,
13    source_map::{GenerateSourceMap, SourceMapAsset},
14    version::{MergeableVersionedContent, Version, VersionedContent, VersionedContentMerger},
15};
16use turbopack_ecmascript::{
17    chunk::{EcmascriptChunkContent, EcmascriptChunkContentEntries},
18    hmr::{
19        EcmascriptHmrChunkContent, merger::EcmascriptChunkContentMerger,
20        version::EcmascriptChunkVersion,
21    },
22    minify::minify,
23    utils::StringifyJs,
24};
25
26use super::chunk::EcmascriptBrowserChunk;
27use crate::{
28    BrowserChunkingContext,
29    chunking_context::{CURRENT_CHUNK_METHOD_DOCUMENT_CURRENT_SCRIPT_EXPR, CurrentChunkMethod},
30};
31
32#[turbo_tasks::value(serialization = "skip")]
33pub struct EcmascriptBrowserChunkContent {
34    pub(super) chunking_context: ResolvedVc<BrowserChunkingContext>,
35    pub(super) chunk: ResolvedVc<EcmascriptBrowserChunk>,
36    pub(super) content: ResolvedVc<EcmascriptChunkContent>,
37    pub(super) source_map: ResolvedVc<SourceMapAsset>,
38}
39
40#[turbo_tasks::value_impl]
41impl EcmascriptBrowserChunkContent {
42    #[turbo_tasks::function]
43    pub(crate) fn new(
44        chunking_context: ResolvedVc<BrowserChunkingContext>,
45        chunk: ResolvedVc<EcmascriptBrowserChunk>,
46        content: ResolvedVc<EcmascriptChunkContent>,
47        source_map: ResolvedVc<SourceMapAsset>,
48    ) -> Result<Vc<Self>> {
49        Ok(EcmascriptBrowserChunkContent {
50            chunking_context,
51            chunk,
52            content,
53            source_map,
54        }
55        .cell())
56    }
57
58    #[turbo_tasks::function]
59    pub(crate) async fn code(self: Vc<Self>) -> Result<Vc<Code>> {
60        let this = self.await?;
61        let source_maps = *this
62            .chunking_context
63            .reference_chunk_source_maps(*ResolvedVc::upcast(this.chunk))
64            .await?;
65        // Lifetime hack to pull out the var into this scope
66        let chunk_path;
67        let script_or_path = match *this.chunking_context.current_chunk_method().await? {
68            CurrentChunkMethod::StringLiteral => {
69                let output_root = this.chunking_context.output_root().await?;
70                let chunk_path_vc = this.chunk.path();
71                chunk_path = chunk_path_vc.await?;
72                let chunk_server_path = if let Some(path) = output_root.get_path_to(&chunk_path) {
73                    path
74                } else {
75                    turbobail!("chunk path {chunk_path} is not in output root {output_root}");
76                };
77                Either::Left(StringifyJs(chunk_server_path))
78            }
79            CurrentChunkMethod::DocumentCurrentScript => {
80                Either::Right(CURRENT_CHUNK_METHOD_DOCUMENT_CURRENT_SCRIPT_EXPR)
81            }
82        };
83        let mut code = CodeBuilder::new(
84            source_maps,
85            *this.chunking_context.debug_ids_enabled().await?,
86        );
87
88        // When a chunk is executed, it will either register itself with the current
89        // instance of the runtime, or it will push itself onto the list of pending
90        // chunks (using the configured chunk loading global variable).
91        //
92        // When the runtime executes (see the `evaluate` module), it will pick up and
93        // register all pending chunks, and replace the list of pending chunks
94        // with itself so later chunks can register directly with it.
95        let chunk_loading_global = this.chunking_context.chunk_loading_global().await?;
96        write!(
97            code,
98            // `||=` would be better but we need to be es2020 compatible
99            //`x || (x = default)` is better than `x = x || default` simply because we avoid _writing_ the property in the common case.
100            r#"(globalThis[{chunk_loading_global}] || (globalThis[{chunk_loading_global}] = [])).push([{script_or_path},"#,
101            chunk_loading_global = StringifyJs(&chunk_loading_global),
102        )?;
103
104        let content = this.content.await?;
105        let mut chunk_items = content.chunk_item_code_module_ids_and_paths().await?;
106        // Sort items by their module path so that similar modules stay
107        // together so that the chunks gzips better.
108        chunk_items.sort_by(|a, b| {
109            a.first()
110                .map(|(id, _, path)| (path, id))
111                .cmp(&b.first().map(|(id, _, path)| (path, id)))
112        });
113        for item in &chunk_items {
114            for (id, item_code, _) in &**item {
115                write!(code, "\n{}, ", StringifyJs(id))?;
116                code.push_code(item_code);
117                write!(code, ",")?;
118            }
119        }
120
121        write!(code, "\n]);")?;
122
123        let mut code = code.build();
124
125        if let MinifyType::Minify { mangle } = *this.chunking_context.minify_type().await? {
126            code = minify(code, source_maps, mangle)?;
127        }
128
129        Ok(code.cell())
130    }
131}
132
133#[turbo_tasks::value_impl]
134impl VersionedContent for EcmascriptBrowserChunkContent {
135    #[turbo_tasks::function]
136    async fn content(self: Vc<Self>) -> Result<Vc<AssetContent>> {
137        let this = self.await?;
138
139        Ok(AssetContent::file(
140            FileContent::Content(File::from(
141                self.code()
142                    .to_rope_with_magic_comments(|| *this.source_map)
143                    .await?,
144            ))
145            .cell(),
146        ))
147    }
148
149    #[turbo_tasks::function]
150    fn version(self: Vc<Self>) -> Vc<Box<dyn Version>> {
151        Vc::upcast(self.ecmascript_chunk_version())
152    }
153}
154
155#[turbo_tasks::value_impl]
156impl EcmascriptHmrChunkContent for EcmascriptBrowserChunkContent {
157    #[turbo_tasks::function]
158    fn entries(&self) -> Vc<EcmascriptChunkContentEntries> {
159        EcmascriptChunkContentEntries::new(*self.content)
160    }
161
162    #[turbo_tasks::function]
163    async fn ecmascript_chunk_version(&self) -> Result<Vc<EcmascriptChunkVersion>> {
164        Ok(EcmascriptChunkVersion::new(
165            self.chunking_context.output_root().owned().await?,
166            self.chunk.path().owned().await?,
167            *self.content,
168            *self.chunking_context.minify_type().await?,
169        ))
170    }
171}
172
173#[turbo_tasks::value_impl]
174impl MergeableVersionedContent for EcmascriptBrowserChunkContent {
175    #[turbo_tasks::function]
176    fn get_merger(&self) -> Vc<Box<dyn VersionedContentMerger>> {
177        Vc::upcast(EcmascriptChunkContentMerger::new())
178    }
179}
180
181#[turbo_tasks::value_impl]
182impl GenerateSourceMap for EcmascriptBrowserChunkContent {
183    #[turbo_tasks::function]
184    fn generate_source_map(self: Vc<Self>) -> Vc<FileContent> {
185        self.code().generate_source_map()
186    }
187
188    #[turbo_tasks::function]
189    async fn by_section(self: Vc<Self>, section: RcStr) -> Result<Vc<FileContent>> {
190        // Weirdly, the ContentSource will have already URL decoded the ModuleId, and we
191        // can't reparse that via serde.
192        if let Ok(id) = ModuleId::parse(&section) {
193            let entries = self.entries().await?;
194            for (entry_id, entry) in entries.iter() {
195                if id == *entry_id {
196                    let sm = entry.code.generate_source_map();
197                    return Ok(sm);
198                }
199            }
200        }
201
202        Ok(FileContent::NotFound.cell())
203    }
204}