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, glob::Glob, 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,
22    module_graph::ModuleGraph,
23    resolve::ModulePart,
24    source::Source,
25    source_map::GenerateSourceMap,
26};
27use turbopack_ecmascript::{
28    EcmascriptInputTransforms,
29    chunk::{
30        EcmascriptChunkItem, EcmascriptChunkItemContent, EcmascriptChunkItemOptions,
31        EcmascriptChunkPlaceable, EcmascriptChunkType, EcmascriptExports,
32    },
33    source_map::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        _part: Option<ModulePart>,
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    fn ident(&self) -> Vc<AssetIdent> {
92        self.source.ident().with_modifier(rcstr!("raw"))
93    }
94}
95
96#[turbo_tasks::value_impl]
97impl Asset for RawEcmascriptModule {
98    #[turbo_tasks::function]
99    fn content(&self) -> Vc<AssetContent> {
100        self.source.content()
101    }
102}
103
104#[turbo_tasks::value_impl]
105impl ChunkableModule for RawEcmascriptModule {
106    #[turbo_tasks::function]
107    fn as_chunk_item(
108        self: ResolvedVc<Self>,
109        _module_graph: Vc<ModuleGraph>,
110        chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
111    ) -> Vc<Box<dyn turbopack_core::chunk::ChunkItem>> {
112        Vc::upcast(
113            RawEcmascriptChunkItem {
114                module: self,
115                chunking_context,
116            }
117            .cell(),
118        )
119    }
120}
121
122#[turbo_tasks::value_impl]
123impl EcmascriptChunkPlaceable for RawEcmascriptModule {
124    #[turbo_tasks::function]
125    fn get_exports(&self) -> Vc<EcmascriptExports> {
126        EcmascriptExports::CommonJs.cell()
127    }
128
129    #[turbo_tasks::function]
130    fn is_marked_as_side_effect_free(&self, _side_effect_free_packages: Vc<Glob>) -> Vc<bool> {
131        Vc::cell(false)
132    }
133}
134
135#[turbo_tasks::value]
136struct RawEcmascriptChunkItem {
137    module: ResolvedVc<RawEcmascriptModule>,
138    chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
139}
140
141#[turbo_tasks::value_impl]
142impl ChunkItem for RawEcmascriptChunkItem {
143    #[turbo_tasks::function]
144    fn asset_ident(&self) -> Vc<AssetIdent> {
145        self.module.ident()
146    }
147
148    #[turbo_tasks::function]
149    fn chunking_context(&self) -> Vc<Box<dyn ChunkingContext>> {
150        *self.chunking_context
151    }
152
153    #[turbo_tasks::function]
154    async fn ty(&self) -> Result<Vc<Box<dyn ChunkType>>> {
155        Ok(Vc::upcast(
156            Vc::<EcmascriptChunkType>::default().resolve().await?,
157        ))
158    }
159
160    #[turbo_tasks::function]
161    fn module(&self) -> Vc<Box<dyn Module>> {
162        Vc::upcast(*self.module)
163    }
164}
165
166#[turbo_tasks::value_impl]
167impl EcmascriptChunkItem for RawEcmascriptChunkItem {
168    #[turbo_tasks::function]
169    async fn content(&self) -> Result<Vc<EcmascriptChunkItemContent>> {
170        let span = tracing::info_span!(
171            "code generation raw module",
172            name = display(self.module.ident().to_string().await?)
173        );
174
175        async {
176            let module = self.module.await?;
177            let source = module.source;
178            let content = source.content().file_content().await?;
179            let content = match &*content {
180                FileContent::Content(file) => file.content(),
181                FileContent::NotFound => bail!("RawEcmascriptModule content not found"),
182            };
183
184            static ENV_REGEX: Lazy<Regex> =
185                Lazy::new(|| Regex::new(r"process\.env\.([a-zA-Z0-9_]+)").unwrap());
186
187            let content_str = content.to_str()?;
188
189            let mut env_vars = FxIndexSet::default();
190            for (_, [name]) in ENV_REGEX.captures_iter(&content_str).map(|c| c.extract()) {
191                env_vars.insert(name);
192            }
193
194            let mut code = CodeBuilder::default();
195            if !env_vars.is_empty() {
196                let replacements = module
197                    .compile_time_info
198                    .await?
199                    .free_var_references
200                    .individual()
201                    .await?;
202                code += "var process = {env:\n";
203                writeln!(
204                    code,
205                    "{}",
206                    StringifyJs(
207                        &env_vars
208                            .into_iter()
209                            .map(async |name| {
210                                Ok((
211                                    name,
212                                    if let Some(value) =
213                                        replacements.get(&DefinableNameSegment::Name(name.into()))
214                                        && let Some((_, value)) = value.iter().find(|(path, _)| {
215                                            matches!(
216                                                path.as_slice(),
217                                                [
218                                                    DefinableNameSegment::Name(a),
219                                                    DefinableNameSegment::Name(b)
220                                                ] if a == "process" && b == "env"
221                                            )
222                                        })
223                                    {
224                                        let value = value.await?;
225                                        let value = match &*value {
226                                            FreeVarReference::Value(
227                                                CompileTimeDefineValue::String(value),
228                                            ) => serde_json::Value::String(value.to_string()),
229                                            FreeVarReference::Value(
230                                                CompileTimeDefineValue::Bool(value),
231                                            ) => serde_json::Value::Bool(*value),
232                                            _ => {
233                                                bail!(
234                                                    "Unexpected replacement for \
235                                                     process.env.{name} in RawEcmascriptModule: \
236                                                     {value:?}"
237                                                );
238                                            }
239                                        };
240                                        Some(value)
241                                    } else {
242                                        None
243                                    },
244                                ))
245                            })
246                            .try_join()
247                            .await?
248                            .into_iter()
249                            .collect::<FxIndexMap<_, _>>()
250                    )
251                )?;
252                code += "};\n";
253            }
254
255            code += "(function(){\n";
256            let source_map = if let Some((source_map, _)) = parse_source_map_comment(
257                source,
258                Either::Right(&content_str),
259                &*self.module.ident().path().await?,
260            )
261            .await?
262            {
263                source_map.generate_source_map().owned().await?
264            } else {
265                None
266            };
267            code.push_source(content, source_map);
268
269            // Add newline in case the raw code had a comment as the last line and no final newline.
270            code += "\n})();\n";
271
272            let code = code.build();
273            let source_map = if code.has_source_map() {
274                let source_map = code.generate_source_map_ref(None);
275
276                static SECTIONS_REGEX: Lazy<Regex> =
277                    Lazy::new(|| Regex::new(r#"sections"[\s\n]*:"#).unwrap());
278                Some(if !SECTIONS_REGEX.is_match(&source_map.to_str()?) {
279                    // This is definitely not an index source map
280                    source_map
281                } else {
282                    let _span = tracing::span!(
283                        tracing::Level::WARN,
284                        "flattening index source map in RawEcmascriptModule"
285                    )
286                    .entered();
287                    match swc_sourcemap::lazy::decode(&source_map.to_bytes())? {
288                        swc_sourcemap::lazy::DecodedMap::Regular(_) => source_map,
289                        // without flattening the index map, we would get nested index source maps
290                        // in the output chunks, which are apparently not
291                        // supported
292                        swc_sourcemap::lazy::DecodedMap::Index(source_map) => {
293                            let source_map = source_map.flatten()?.into_raw_sourcemap();
294                            let result = serde_json::to_vec(&source_map)?;
295                            Rope::from(result)
296                        }
297                    }
298                })
299            } else {
300                None
301            };
302
303            Ok(EcmascriptChunkItemContent {
304                source_map,
305                inner_code: code.into_source_code(),
306                options: EcmascriptChunkItemOptions {
307                    module_and_exports: true,
308                    ..Default::default()
309                },
310                ..Default::default()
311            }
312            .into())
313        }
314        .instrument(span)
315        .await
316    }
317}