Skip to main content

turbopack_ecmascript/
utils.rs

1use std::ops::Deref;
2
3use bincode::{Decode, Encode};
4use serde::Serialize;
5use smallvec::SmallVec;
6use swc_core::{
7    common::{DUMMY_SP, SyntaxContext},
8    ecma::{
9        ast::{ComputedPropName, Expr, Lit, MemberProp, ObjectPatProp, Pat, PropName, Str},
10        visit::AstParentKind,
11    },
12};
13use turbo_rcstr::{RcStr, rcstr};
14use turbo_tasks::{NonLocalValue, TaskInput, trace::TraceRawVcs};
15use turbopack_core::{chunk::ModuleId, resolve::pattern::Pattern};
16
17use crate::analyzer::{
18    ConstantNumber, ConstantValue, JsValue, JsValueUrlKind, ModuleValue, WellKnownFunctionKind,
19    WellKnownObjectKind,
20};
21
22pub fn unparen(expr: &Expr) -> &Expr {
23    if let Some(expr) = expr.as_paren() {
24        return unparen(&expr.expr);
25    }
26    if let Expr::Seq(seq) = expr {
27        return unparen(seq.exprs.last().unwrap());
28    }
29    expr
30}
31
32/// The statically-known export name read by a member property (`obj.foo` /
33/// `obj["foo"]`), or `None` for a dynamic/computed access.
34pub(crate) fn extract_name_from_member_prop(prop: &MemberProp) -> Option<SmallVec<[RcStr; 1]>> {
35    match prop {
36        MemberProp::Ident(ident) => Some(SmallVec::from_buf([ident.sym.as_str().into()])),
37        MemberProp::Computed(ComputedPropName {
38            expr: box Expr::Lit(Lit::Str(s)),
39            ..
40        }) => s.value.as_str().map(|v| SmallVec::from_buf([v.into()])),
41        _ => None,
42    }
43}
44
45/// The statically-known export names bound by an object destructuring pattern
46/// (`const { a, b } = …`), or `None` for a rest element or computed key.
47pub(crate) fn extract_names_from_object_pat(pat: &Pat) -> Option<SmallVec<[RcStr; 1]>> {
48    let Pat::Object(obj_pat) = pat else {
49        return None;
50    };
51    let mut names = SmallVec::new();
52    for prop in &obj_pat.props {
53        match prop {
54            ObjectPatProp::KeyValue(kv) => match &kv.key {
55                PropName::Ident(ident) => names.push(ident.sym.as_str().into()),
56                PropName::Str(s) => names.push(s.value.as_str()?.into()),
57                _ => return None, // computed key, can't determine statically
58            },
59            ObjectPatProp::Assign(assign) => {
60                names.push(assign.key.sym.as_str().into());
61            }
62            ObjectPatProp::Rest(_) => return None, // rest pattern means all exports needed
63        }
64    }
65    Some(names)
66}
67
68/// Converts a js-value into a Pattern for matching resources.
69pub fn js_value_to_pattern(value: &JsValue<'_>) -> Pattern {
70    match value {
71        JsValue::Constant(v) => Pattern::Constant(match v {
72            ConstantValue::Str(str) => {
73                // Normalize windows file paths when constructing the pattern.
74                // See PACK-3279
75                if str.as_str().contains("\\") {
76                    RcStr::from(str.to_string().replace('\\', "/"))
77                } else {
78                    str.as_rcstr()
79                }
80            }
81            ConstantValue::True => rcstr!("true"),
82            ConstantValue::False => rcstr!("false"),
83            ConstantValue::Null => rcstr!("null"),
84            ConstantValue::Num(ConstantNumber(n)) => n.to_string().into(),
85            ConstantValue::BigInt(n) => n.to_string().into(),
86            ConstantValue::Regex(box (exp, flags)) => format!("/{exp}/{flags}").into(),
87            ConstantValue::Undefined => rcstr!("undefined"),
88        }),
89        JsValue::Url(v, JsValueUrlKind::Relative) => Pattern::Constant(v.as_rcstr()),
90        JsValue::Alternatives {
91            total_nodes: _,
92            values,
93            logical_property: _,
94        } => {
95            let mut alts = Pattern::Alternatives(values.iter().map(js_value_to_pattern).collect());
96            alts.normalize();
97            alts
98        }
99        JsValue::Concat(_, parts) => {
100            let mut concats =
101                Pattern::Concatenation(parts.iter().map(js_value_to_pattern).collect());
102            concats.normalize();
103            concats
104        }
105        JsValue::Add(..) => {
106            // TODO do we need to handle that here
107            // or is that already covered by normalization of JsValue
108            Pattern::Dynamic
109        }
110        _ => Pattern::Dynamic,
111    }
112}
113
114const JS_MAX_SAFE_INTEGER: u64 = (1u64 << 53) - 1;
115
116pub fn module_id_to_lit(module_id: &ModuleId) -> Expr {
117    Expr::Lit(match module_id {
118        ModuleId::Number(n) => {
119            if *n <= JS_MAX_SAFE_INTEGER {
120                Lit::Num((*n as f64).into())
121            } else {
122                Lit::Str(Str {
123                    span: DUMMY_SP,
124                    value: n.to_string().into(),
125                    raw: None,
126                })
127            }
128        }
129        ModuleId::String(s) => Lit::Str(Str {
130            span: DUMMY_SP,
131            value: (s as &str).into(),
132            raw: None,
133        }),
134    })
135}
136
137pub struct StringifyModuleId<'a>(pub &'a ModuleId);
138
139impl std::fmt::Display for StringifyModuleId<'_> {
140    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
141        match self.0 {
142            ModuleId::Number(n) => {
143                if *n <= JS_MAX_SAFE_INTEGER {
144                    n.fmt(f)
145                } else {
146                    write!(f, "\"{n}\"")
147                }
148            }
149            ModuleId::String(s) => StringifyJs(s).fmt(f),
150        }
151    }
152}
153
154pub struct StringifyJs<'a, T>(pub &'a T)
155where
156    T: ?Sized;
157
158impl<T> std::fmt::Display for StringifyJs<'_, T>
159where
160    T: Serialize + ?Sized,
161{
162    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
163        /// [`std::fmt::Formatter`] does not implement [`std::io::Write`],
164        /// so we need to wrap it in a struct that does.
165        struct DisplayWriter<'a, 'b> {
166            f: &'a mut std::fmt::Formatter<'b>,
167        }
168
169        impl std::io::Write for DisplayWriter<'_, '_> {
170            fn write(&mut self, bytes: &[u8]) -> std::result::Result<usize, std::io::Error> {
171                self.f
172                    .write_str(std::str::from_utf8(bytes).map_err(std::io::Error::other)?)
173                    .map_err(std::io::Error::other)?;
174                Ok(bytes.len())
175            }
176
177            fn flush(&mut self) -> std::result::Result<(), std::io::Error> {
178                unreachable!()
179            }
180        }
181
182        let to_writer = match f.alternate() {
183            true => serde_json::to_writer_pretty,
184            false => serde_json::to_writer,
185        };
186
187        to_writer(DisplayWriter { f }, self.0).map_err(|_err| std::fmt::Error)
188    }
189}
190
191pub struct FormatIter<T: Iterator, F: Fn() -> T>(pub F);
192
193macro_rules! format_iter {
194    ($trait:path) => {
195        impl<T: Iterator, F: Fn() -> T> $trait for FormatIter<T, F>
196        where
197            T::Item: $trait,
198        {
199            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
200                for item in self.0() {
201                    item.fmt(f)?;
202                }
203                Ok(())
204            }
205        }
206    };
207}
208
209format_iter!(std::fmt::Binary);
210format_iter!(std::fmt::Debug);
211format_iter!(std::fmt::Display);
212format_iter!(std::fmt::LowerExp);
213format_iter!(std::fmt::LowerHex);
214format_iter!(std::fmt::Octal);
215format_iter!(std::fmt::Pointer);
216format_iter!(std::fmt::UpperExp);
217format_iter!(std::fmt::UpperHex);
218
219#[derive(Clone, PartialEq, Eq, TraceRawVcs, Debug, NonLocalValue, Hash, Encode, Decode)]
220pub enum AstPathRange {
221    /// The ast path to the block or expression.
222    Exact(
223        #[bincode(with_serde)]
224        #[turbo_tasks(trace_ignore)]
225        Vec<AstParentKind>,
226    ),
227    /// The ast path to a expression just before the range in the parent of the
228    /// specific ast path.
229    StartAfter(
230        #[bincode(with_serde)]
231        #[turbo_tasks(trace_ignore)]
232        Vec<AstParentKind>,
233    ),
234}
235
236/// Converts a module value (ie an import) to a well known object,
237/// which we specifically handle.
238pub fn module_value_to_well_known_object<'a>(module_value: &ModuleValue) -> Option<JsValue<'a>> {
239    Some(match module_value.module.as_bytes() {
240        b"node:path" | b"path" => JsValue::WellKnownObject(WellKnownObjectKind::PathModule),
241        b"node:fs/promises" | b"fs/promises" => {
242            JsValue::WellKnownObject(WellKnownObjectKind::FsModule)
243        }
244        b"node:fs" | b"fs" => JsValue::WellKnownObject(WellKnownObjectKind::FsModule),
245        b"node:child_process" | b"child_process" => {
246            JsValue::WellKnownObject(WellKnownObjectKind::ChildProcessModule)
247        }
248        b"node:os" | b"os" => JsValue::WellKnownObject(WellKnownObjectKind::OsModule),
249        b"node:process" | b"process" => {
250            JsValue::WellKnownObject(WellKnownObjectKind::NodeProcessModule)
251        }
252        b"node:url" | b"url" => JsValue::WellKnownObject(WellKnownObjectKind::UrlModule),
253        b"node:module" | b"module" => JsValue::WellKnownObject(WellKnownObjectKind::ModuleModule),
254        b"node:worker_threads" | b"worker_threads" => {
255            JsValue::WellKnownObject(WellKnownObjectKind::WorkerThreadsModule)
256        }
257        b"node-pre-gyp" | b"@mapbox/node-pre-gyp" => {
258            JsValue::WellKnownObject(WellKnownObjectKind::NodePreGyp)
259        }
260        b"node-gyp-build" => JsValue::WellKnownFunction(WellKnownFunctionKind::NodeGypBuild),
261        b"node:bindings" | b"bindings" => {
262            JsValue::WellKnownFunction(WellKnownFunctionKind::NodeBindings)
263        }
264        b"express" => JsValue::WellKnownFunction(WellKnownFunctionKind::NodeExpress),
265        b"strong-globalize" => {
266            JsValue::WellKnownFunction(WellKnownFunctionKind::NodeStrongGlobalize)
267        }
268        b"resolve-from" => JsValue::WellKnownFunction(WellKnownFunctionKind::NodeResolveFrom),
269        b"@grpc/proto-loader" => JsValue::WellKnownObject(WellKnownObjectKind::NodeProtobufLoader),
270        b"fs-extra" => JsValue::WellKnownObject(WellKnownObjectKind::FsExtraModule),
271        _ => return None,
272    })
273}
274
275#[derive(Hash, Debug, Clone, Copy, Eq, PartialEq, TraceRawVcs, Encode, Decode)]
276pub struct AstSyntaxContext(
277    #[turbo_tasks(trace_ignore)]
278    #[bincode(with_serde)]
279    SyntaxContext,
280);
281
282impl TaskInput for AstSyntaxContext {
283    fn is_transient(&self) -> bool {
284        false
285    }
286}
287unsafe impl NonLocalValue for AstSyntaxContext {}
288
289impl Deref for AstSyntaxContext {
290    type Target = SyntaxContext;
291
292    fn deref(&self) -> &Self::Target {
293        &self.0
294    }
295}
296
297impl From<SyntaxContext> for AstSyntaxContext {
298    fn from(v: SyntaxContext) -> Self {
299        Self(v)
300    }
301}
302
303/// Generates an inline source map comment that maps back to the original file in a trivial way
304///
305/// This is useful for source transforms that convert non-JS files (like text or binary files)
306/// into ES modules. The inline source map ensures that debuggers and error stacks show
307/// the original file path rather than the generated code.
308///
309/// # Arguments
310/// * `original_path` - The path to the original file (used in source map's "sources" field)
311/// * `original_content` - The original file content (used in source map's "sourcesContent" field)
312///
313/// # Returns
314/// An inline source map comment (e.g., `//# sourceMappingURL=data:application/json;base64,...`)
315pub fn inline_source_map_comment(original_path: &str, original_content: &str) -> String {
316    let source_map = serde_json::json!({
317        "version": 3,
318        "sources": [format!("turbopack:///{}", original_path)],
319        "sourcesContent": [original_content],
320        "names": [],
321        // Maps 0:0 in the output code to 0:0 in the original. Sufficient for
322        // bundle analyzers to attribute the bytes in the output chunks
323        "mappings": "AAAA",
324    });
325
326    let source_map_base64 = data_encoding::BASE64.encode(source_map.to_string().as_bytes());
327
328    format!(
329        "//# sourceMappingURL=data:application/json;base64,{}",
330        source_map_base64
331    )
332}
333
334#[cfg(test)]
335mod tests {
336    use turbo_rcstr::rcstr;
337    use turbopack_core::resolve::pattern::Pattern;
338
339    use crate::{
340        analyzer::{BumpVec, ConstantString, ConstantValue, JsValue, ThreadLocal},
341        utils::js_value_to_pattern,
342    };
343
344    #[test]
345    fn test_path_normalization_in_pattern() {
346        let arena = ThreadLocal::new();
347        assert_eq!(
348            Pattern::Constant(rcstr!("hello/world")),
349            js_value_to_pattern(&JsValue::Constant(ConstantValue::Str(
350                ConstantString::RcStr(rcstr!("hello\\world"))
351            )))
352        );
353
354        assert_eq!(
355            Pattern::Constant(rcstr!("hello/world")),
356            js_value_to_pattern(&JsValue::Concat(
357                1,
358                BumpVec::from_iter_in(
359                    arena.get_or_default(),
360                    [
361                        rcstr!("hello").into(),
362                        rcstr!("\\").into(),
363                        rcstr!("world").into()
364                    ]
365                )
366            ))
367        );
368    }
369
370    #[test]
371    fn test_inline_source_map_comment() {
372        use super::inline_source_map_comment;
373
374        let comment = inline_source_map_comment("test.txt", "hello");
375
376        assert!(comment.starts_with("//# sourceMappingURL=data:application/json;base64,"));
377
378        // Verify the source map is valid JSON when decoded
379        let source_map_part = comment
380            .split("base64,")
381            .nth(1)
382            .expect("should have base64 part");
383        let decoded = data_encoding::BASE64
384            .decode(source_map_part.as_bytes())
385            .expect("should decode");
386        let json: serde_json::Value =
387            serde_json::from_slice(&decoded).expect("should be valid JSON");
388
389        assert_eq!(json["version"], 3);
390        assert_eq!(json["sources"][0], "turbopack:///test.txt");
391        assert_eq!(json["sourcesContent"][0], "hello");
392        assert_eq!(json["mappings"], "AAAA");
393    }
394}