next_core/
raw_ecmascript_module.rs

1use std::io::Write;
2
3use anyhow::{Result, bail};
4use either::Either;
5use once_cell::sync::Lazy;
6use regex::Regex;
7use tracing::Instrument;
8use turbo_rcstr::rcstr;
9use turbo_tasks::{FxIndexMap, FxIndexSet, ResolvedVc, TryJoinIterExt, ValueToString, Vc};
10use turbo_tasks_fs::{FileContent, rope::Rope};
11use turbopack::{ModuleAssetContext, module_options::CustomModuleType};
12use turbopack_core::{
13    asset::{Asset, AssetContent},
14    chunk::{ChunkItem, ChunkType, ChunkableModule, ChunkingContext},
15    code_builder::CodeBuilder,
16    compile_time_info::{
17        CompileTimeDefineValue, CompileTimeInfo, DefinableNameSegment, FreeVarReference,
18    },
19    context::AssetContext,
20    ident::AssetIdent,
21    module::{Module, ModuleSideEffects},
22    module_graph::ModuleGraph,
23    output::OutputAssetsReference,
24    resolve::ModulePart,
25    source::{OptionSource, Source},
26    source_map::GenerateSourceMap,
27};
28use turbopack_ecmascript::{
29    EcmascriptInputTransforms,
30    chunk::{
31        EcmascriptChunkItem, EcmascriptChunkItemContent, EcmascriptChunkItemOptions,
32        EcmascriptChunkPlaceable, EcmascriptChunkType, EcmascriptExports,
33    },
34    source_map::parse_source_map_comment,
35    utils::StringifyJs,
36};
37
38#[turbo_tasks::value(shared)]
39pub struct RawEcmascriptModuleType {}
40
41#[turbo_tasks::value_impl]
42impl CustomModuleType for RawEcmascriptModuleType {
43    #[turbo_tasks::function]
44    fn create_module(
45        &self,
46        source: Vc<Box<dyn Source>>,
47        module_asset_context: Vc<ModuleAssetContext>,
48        _part: Option<ModulePart>,
49    ) -> Vc<Box<dyn Module>> {
50        Vc::upcast(RawEcmascriptModule::new(
51            source,
52            module_asset_context.compile_time_info(),
53        ))
54    }
55
56    #[turbo_tasks::function]
57    fn extend_ecmascript_transforms(
58        self: Vc<Self>,
59        _preprocess: Vc<EcmascriptInputTransforms>,
60        _main: Vc<EcmascriptInputTransforms>,
61        _postprocess: Vc<EcmascriptInputTransforms>,
62    ) -> Vc<Box<dyn CustomModuleType>> {
63        // Just ignore them
64        Vc::upcast(self)
65    }
66}
67
68#[turbo_tasks::value]
69pub struct RawEcmascriptModule {
70    source: ResolvedVc<Box<dyn Source>>,
71    compile_time_info: ResolvedVc<CompileTimeInfo>,
72}
73
74#[turbo_tasks::value_impl]
75impl RawEcmascriptModule {
76    #[turbo_tasks::function]
77    pub fn new(
78        source: ResolvedVc<Box<dyn Source>>,
79        compile_time_info: ResolvedVc<CompileTimeInfo>,
80    ) -> Vc<Self> {
81        RawEcmascriptModule {
82            source,
83            compile_time_info,
84        }
85        .cell()
86    }
87}
88
89#[turbo_tasks::value_impl]
90impl Module for RawEcmascriptModule {
91    #[turbo_tasks::function]
92    fn ident(&self) -> Vc<AssetIdent> {
93        self.source.ident().with_modifier(rcstr!("raw"))
94    }
95
96    #[turbo_tasks::function]
97    fn source(&self) -> Vc<OptionSource> {
98        Vc::cell(Some(self.source))
99    }
100
101    #[turbo_tasks::function]
102    fn side_effects(self: Vc<Self>) -> Vc<ModuleSideEffects> {
103        ModuleSideEffects::SideEffectful.cell()
104    }
105}
106
107#[turbo_tasks::value_impl]
108impl Asset for RawEcmascriptModule {
109    #[turbo_tasks::function]
110    fn content(&self) -> Vc<AssetContent> {
111        self.source.content()
112    }
113}
114
115#[turbo_tasks::value_impl]
116impl ChunkableModule for RawEcmascriptModule {
117    #[turbo_tasks::function]
118    fn as_chunk_item(
119        self: ResolvedVc<Self>,
120        _module_graph: Vc<ModuleGraph>,
121        chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
122    ) -> Vc<Box<dyn turbopack_core::chunk::ChunkItem>> {
123        Vc::upcast(
124            RawEcmascriptChunkItem {
125                module: self,
126                chunking_context,
127            }
128            .cell(),
129        )
130    }
131}
132
133#[turbo_tasks::value_impl]
134impl EcmascriptChunkPlaceable for RawEcmascriptModule {
135    #[turbo_tasks::function]
136    fn get_exports(&self) -> Vc<EcmascriptExports> {
137        EcmascriptExports::CommonJs.cell()
138    }
139}
140
141#[turbo_tasks::value]
142struct RawEcmascriptChunkItem {
143    module: ResolvedVc<RawEcmascriptModule>,
144    chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
145}
146
147#[turbo_tasks::value_impl]
148impl OutputAssetsReference for RawEcmascriptChunkItem {}
149
150#[turbo_tasks::value_impl]
151impl ChunkItem for RawEcmascriptChunkItem {
152    #[turbo_tasks::function]
153    fn asset_ident(&self) -> Vc<AssetIdent> {
154        self.module.ident()
155    }
156
157    #[turbo_tasks::function]
158    fn chunking_context(&self) -> Vc<Box<dyn ChunkingContext>> {
159        *self.chunking_context
160    }
161
162    #[turbo_tasks::function]
163    async fn ty(&self) -> Result<Vc<Box<dyn ChunkType>>> {
164        Ok(Vc::upcast(
165            Vc::<EcmascriptChunkType>::default().resolve().await?,
166        ))
167    }
168
169    #[turbo_tasks::function]
170    fn module(&self) -> Vc<Box<dyn Module>> {
171        Vc::upcast(*self.module)
172    }
173}
174
175#[turbo_tasks::value_impl]
176impl EcmascriptChunkItem for RawEcmascriptChunkItem {
177    #[turbo_tasks::function]
178    async fn content(&self) -> Result<Vc<EcmascriptChunkItemContent>> {
179        let span = tracing::info_span!(
180            "code generation raw module",
181            name = display(self.module.ident().to_string().await?)
182        );
183
184        async {
185            let module = self.module.await?;
186            let source = module.source;
187            let content = source.content().file_content().await?;
188            let content = match &*content {
189                FileContent::Content(file) => file.content(),
190                FileContent::NotFound => bail!("RawEcmascriptModule content not found"),
191            };
192
193            static ENV_REGEX: Lazy<Regex> =
194                Lazy::new(|| Regex::new(r"process\.env\.([a-zA-Z0-9_]+)").unwrap());
195
196            let content_str = content.to_str()?;
197
198            let mut env_vars = FxIndexSet::default();
199            for (_, [name]) in ENV_REGEX.captures_iter(&content_str).map(|c| c.extract()) {
200                env_vars.insert(name);
201            }
202
203            let mut code = CodeBuilder::default();
204            if !env_vars.is_empty() {
205                let replacements = module
206                    .compile_time_info
207                    .await?
208                    .free_var_references
209                    .individual()
210                    .await?;
211                code += "var process = {env:\n";
212                writeln!(
213                    code,
214                    "{}",
215                    StringifyJs(
216                        &env_vars
217                            .into_iter()
218                            .map(async |name| {
219                                Ok((
220                                    name,
221                                    if let Some(value) =
222                                        replacements.get(&DefinableNameSegment::Name(name.into()))
223                                        && let Some((_, value)) =
224                                            value.0.iter().find(|(path, _)| {
225                                                matches!(
226                                                    path.as_slice(),
227                                                    [
228                                                        DefinableNameSegment::Name(a),
229                                                        DefinableNameSegment::Name(b)
230                                                    ] if a == "process" && b == "env"
231                                                )
232                                            })
233                                    {
234                                        let value = value.await?;
235                                        let value = match &*value {
236                                            FreeVarReference::Value(
237                                                CompileTimeDefineValue::String(value),
238                                            ) => serde_json::Value::String(value.to_string()),
239                                            FreeVarReference::Value(
240                                                CompileTimeDefineValue::Bool(value),
241                                            ) => serde_json::Value::Bool(*value),
242                                            _ => {
243                                                bail!(
244                                                    "Unexpected replacement for \
245                                                     process.env.{name} in RawEcmascriptModule: \
246                                                     {value:?}"
247                                                );
248                                            }
249                                        };
250                                        Some(value)
251                                    } else {
252                                        None
253                                    },
254                                ))
255                            })
256                            .try_join()
257                            .await?
258                            .into_iter()
259                            .collect::<FxIndexMap<_, _>>()
260                    )
261                )?;
262                code += "};\n";
263            }
264
265            code += "(function(){\n";
266            let source_map = if let Some((source_map, _)) = parse_source_map_comment(
267                source,
268                Either::Right(&content_str),
269                &*self.module.ident().path().await?,
270            )
271            .await?
272            {
273                let source_map = source_map.generate_source_map().await?;
274                source_map.as_content().map(|f| f.content().clone())
275            } else {
276                None
277            };
278            code.push_source(content, source_map);
279
280            // Add newline in case the raw code had a comment as the last line and no final newline.
281            code += "\n})();\n";
282
283            let code = code.build();
284            let source_map = if code.has_source_map() {
285                let source_map = code.generate_source_map_ref(None);
286
287                static SECTIONS_REGEX: Lazy<Regex> =
288                    Lazy::new(|| Regex::new(r#"sections"[\s\n]*:"#).unwrap());
289                Some(if !SECTIONS_REGEX.is_match(&source_map.to_str()?) {
290                    // This is definitely not an index source map
291                    source_map
292                } else {
293                    let _span = tracing::span!(
294                        tracing::Level::WARN,
295                        "flattening index source map in RawEcmascriptModule"
296                    )
297                    .entered();
298                    match swc_sourcemap::lazy::decode(&source_map.to_bytes())? {
299                        swc_sourcemap::lazy::DecodedMap::Regular(_) => source_map,
300                        // without flattening the index map, we would get nested index source maps
301                        // in the output chunks, which are apparently not
302                        // supported
303                        swc_sourcemap::lazy::DecodedMap::Index(source_map) => {
304                            let source_map = source_map.flatten()?.into_raw_sourcemap();
305                            let result = serde_json::to_vec(&source_map)?;
306                            Rope::from(result)
307                        }
308                    }
309                })
310            } else {
311                None
312            };
313
314            Ok(EcmascriptChunkItemContent {
315                source_map,
316                inner_code: code.into_source_code(),
317                options: EcmascriptChunkItemOptions {
318                    module_and_exports: true,
319                    ..Default::default()
320                },
321                ..Default::default()
322            }
323            .cell())
324        }
325        .instrument(span)
326        .await
327    }
328}