Skip to main content

next_core/
raw_ecmascript_module.rs

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