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