turbopack_browser/ecmascript/
content.rs

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