turbopack_browser/ecmascript/
content.rs

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