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