Skip to main content

turbopack_ecmascript/analyzer/well_known/
mod.rs

1use std::{iter, mem::take};
2
3pub mod kinds;
4pub mod require_context;
5
6use anyhow::Result;
7use either::Either;
8use smallvec::SmallVec;
9use turbo_rcstr::rcstr;
10use turbo_tasks::Vc;
11use turbopack_core::{compile_time_info::CompileTimeInfo, environment::Rendering};
12use url::Url;
13
14use super::{
15    ConstantValue, JsValue, JsValueUrlKind, Modified, ModuleValue, WellKnownFunctionKind,
16    WellKnownObjectKind,
17};
18use crate::analyzer::{Bump, BumpVec, RequireContextValue, ThreadLocal};
19
20pub async fn replace_well_known<'a>(
21    arena: &'a ThreadLocal<Bump>,
22    value: JsValue<'a>,
23    compile_time_info: Vc<CompileTimeInfo>,
24    allow_project_root_tracing: bool,
25) -> Result<(JsValue<'a>, Modified)> {
26    Ok(match value {
27        JsValue::Call(_, call) if matches!(call.callee(), JsValue::WellKnownFunction(_)) => {
28            let (callee, args) = call.into_parts();
29            let JsValue::WellKnownFunction(kind) = callee else {
30                unreachable!()
31            };
32            (
33                well_known_function_call(
34                    arena,
35                    kind,
36                    JsValue::unknown_empty(false, rcstr!("this is not analyzed yet")),
37                    args,
38                    compile_time_info,
39                    allow_project_root_tracing,
40                )
41                .await?,
42                Modified::Yes,
43            )
44        }
45        JsValue::Member(_, mut obj, mut prop) if matches!(&*obj, JsValue::WellKnownObject(_)) => {
46            let JsValue::WellKnownObject(kind) = take(&mut *obj) else {
47                unreachable!()
48            };
49            well_known_object_member(arena, kind, take(&mut *prop), compile_time_info).await?
50        }
51        JsValue::Member(_, mut obj, mut prop) if matches!(&*obj, JsValue::WellKnownFunction(_)) => {
52            let JsValue::WellKnownFunction(kind) = take(&mut *obj) else {
53                unreachable!()
54            };
55            well_known_function_member(arena.get_or_default(), kind, take(&mut *prop))
56        }
57        JsValue::Member(_, mut obj, mut prop) if matches!(&*obj, JsValue::Array { .. }) => {
58            match prop.as_str() {
59                Some("filter") => (
60                    JsValue::WellKnownFunction(WellKnownFunctionKind::ArrayFilter),
61                    Modified::Yes,
62                ),
63                Some("forEach") => (
64                    JsValue::WellKnownFunction(WellKnownFunctionKind::ArrayForEach),
65                    Modified::Yes,
66                ),
67                Some("map") => (
68                    JsValue::WellKnownFunction(WellKnownFunctionKind::ArrayMap),
69                    Modified::Yes,
70                ),
71                _ => (
72                    JsValue::member(arena.get_or_default(), take(&mut *obj), take(&mut *prop)),
73                    Modified::No,
74                ),
75            }
76        }
77        // module.hot → WellKnownObject(ModuleHot) (only when HMR is enabled)
78        JsValue::Member(_, obj, prop)
79            if matches!(&*obj, JsValue::FreeVar(name) if &**name == "module")
80                && prop.as_str() == Some("hot")
81                && compile_time_info.await?.hot_module_replacement_enabled =>
82        {
83            (
84                JsValue::WellKnownObject(WellKnownObjectKind::ModuleHot),
85                Modified::Yes,
86            )
87        }
88        _ => (value, Modified::No),
89    })
90}
91
92pub async fn well_known_function_call<'a>(
93    arena: &'a ThreadLocal<Bump>,
94    kind: WellKnownFunctionKind<'a>,
95    _this: JsValue<'a>,
96    args: BumpVec<'a, JsValue<'a>>,
97    compile_time_info: Vc<CompileTimeInfo>,
98    allow_project_root_tracing: bool,
99) -> Result<JsValue<'a>> {
100    Ok(match kind {
101        WellKnownFunctionKind::ObjectAssign => object_assign(arena.get_or_default(), args),
102        WellKnownFunctionKind::PathJoin => path_join(arena.get_or_default(), args),
103        WellKnownFunctionKind::PathDirname => path_dirname(arena.get_or_default(), args),
104        WellKnownFunctionKind::PathResolve(cwd) => path_resolve(
105            arena.get_or_default(),
106            cwd.clone_in(arena.get_or_default()),
107            args,
108        ),
109        WellKnownFunctionKind::Import => import(arena.get_or_default(), args),
110        WellKnownFunctionKind::Require => require(arena.get_or_default(), args),
111        WellKnownFunctionKind::RequireContextRequire(value) => {
112            require_context_require(arena.get_or_default(), value, args)?
113        }
114        WellKnownFunctionKind::RequireContextRequireKeys(value) => {
115            require_context_require_keys(arena.get_or_default(), value, args)?
116        }
117        WellKnownFunctionKind::RequireContextRequireResolve(value) => {
118            require_context_require_resolve(arena.get_or_default(), value, args)?
119        }
120        WellKnownFunctionKind::PathToFileUrl => path_to_file_url(arena.get_or_default(), args),
121        WellKnownFunctionKind::OsArch => compile_time_info
122            .environment()
123            .compile_target()
124            .await?
125            .arch
126            .as_str()
127            .into(),
128        WellKnownFunctionKind::OsPlatform => compile_time_info
129            .environment()
130            .compile_target()
131            .await?
132            .platform
133            .as_str()
134            .into(),
135        WellKnownFunctionKind::ProcessCwd => {
136            if allow_project_root_tracing
137                && let Some(cwd) = &*compile_time_info.environment().cwd().await?
138            {
139                format!("/ROOT/{}", cwd.path).into()
140            } else {
141                JsValue::unknown(
142                    JsValue::call_from_parts(
143                        arena.get_or_default(),
144                        JsValue::WellKnownFunction(kind),
145                        args,
146                    ),
147                    true,
148                    rcstr!("process.cwd is not specified in the environment"),
149                )
150            }
151        }
152        WellKnownFunctionKind::OsEndianness => compile_time_info
153            .environment()
154            .compile_target()
155            .await?
156            .endianness
157            .as_str()
158            .into(),
159        WellKnownFunctionKind::NodeExpress => {
160            JsValue::WellKnownObject(WellKnownObjectKind::NodeExpressApp)
161        }
162        // bypass
163        WellKnownFunctionKind::NodeResolveFrom => {
164            JsValue::WellKnownFunction(WellKnownFunctionKind::NodeResolveFrom)
165        }
166
167        _ => JsValue::unknown(
168            JsValue::call_from_parts(
169                arena.get_or_default(),
170                JsValue::WellKnownFunction(kind),
171                args,
172            ),
173            true,
174            rcstr!("unsupported function"),
175        ),
176    })
177}
178
179fn object_assign<'a>(arena: &'a Bump, args: BumpVec<'a, JsValue<'a>>) -> JsValue<'a> {
180    if args.iter().all(|arg| matches!(arg, JsValue::Object { .. })) {
181        if let Some(mut merged_object) = args.into_iter().reduce(|mut acc, cur| {
182            if let JsValue::Object {
183                parts, mutability, ..
184            } = &mut acc
185                && let JsValue::Object {
186                    parts: next_parts,
187                    mutability: next_mutability,
188                    ..
189                } = &cur
190            {
191                parts.extend(arena, next_parts.iter().map(|p| p.clone_in(arena)));
192                mutability.merge_with(*next_mutability);
193            }
194            acc
195        }) {
196            merged_object.update_total_nodes();
197            merged_object
198        } else {
199            JsValue::unknown(
200                JsValue::call_from_iter(
201                    arena,
202                    JsValue::WellKnownFunction(WellKnownFunctionKind::ObjectAssign),
203                    [],
204                ),
205                true,
206                rcstr!("empty arguments for Object.assign"),
207            )
208        }
209    } else {
210        JsValue::unknown(
211            JsValue::call_from_parts(
212                arena,
213                JsValue::WellKnownFunction(WellKnownFunctionKind::ObjectAssign),
214                args,
215            ),
216            true,
217            rcstr!("only const object assign is supported"),
218        )
219    }
220}
221
222fn path_join<'a>(arena: &'a Bump, args: BumpVec<'a, JsValue<'a>>) -> JsValue<'a> {
223    if args.is_empty() {
224        return rcstr!(".").into();
225    }
226    let mut locked_prefix: SmallVec<[JsValue<'a>; 16]> = SmallVec::new();
227    let mut segments: SmallVec<[JsValue<'a>; 16]> = SmallVec::new();
228    for arg in args {
229        let arg_parts = if let Some(str) = arg.as_str() {
230            let split = str.split('/');
231            Either::Left(split.map(|s| s.into()))
232        } else {
233            Either::Right(iter::once(arg))
234        };
235        for item in arg_parts {
236            if let Some(str) = item.as_str() {
237                match str {
238                    "" | "." => {
239                        if locked_prefix.is_empty() && segments.is_empty() {
240                            locked_prefix.push(item);
241                        }
242                    }
243                    ".." => {
244                        if segments.pop().is_none() {
245                            locked_prefix.push(item);
246                        }
247                    }
248                    _ => segments.push(item),
249                }
250            } else {
251                locked_prefix.append(&mut segments);
252                locked_prefix.push(item);
253            }
254        }
255    }
256    locked_prefix.append(&mut segments);
257    let mut iter = locked_prefix.into_iter();
258    let first = iter.next().unwrap();
259    let mut last_is_str = first.as_str().is_some();
260    // `segments` is now empty; reuse it as the render buffer (`result`) for the
261    // joined parts to avoid allocating a third vec.
262    let mut result = segments;
263    result.push(first);
264    for part in iter {
265        let is_str = part.as_str().is_some();
266        if last_is_str && is_str {
267            result.push(rcstr!("/").into());
268        } else {
269            result.push(JsValue::alternatives(BumpVec::from_iter_in(
270                arena,
271                [rcstr!("/").into(), rcstr!("").into()],
272            )));
273        }
274        result.push(part);
275        last_is_str = is_str;
276    }
277    JsValue::concat(BumpVec::from_iter_in(arena, result))
278}
279
280fn path_resolve<'a>(
281    arena: &'a Bump,
282    cwd: JsValue<'a>,
283    mut args: BumpVec<'a, JsValue<'a>>,
284) -> JsValue<'a> {
285    // If no path segments are passed, `path.resolve()` will return the absolute
286    // path of the current working directory.
287    if args.is_empty() {
288        return JsValue::unknown_empty(false, rcstr!("cwd is not static analyzable"));
289    }
290    if args.len() == 1 {
291        return args.into_iter().next().unwrap();
292    }
293
294    // path.resolve stops at the string starting with `/`
295    for (idx, arg) in args.iter().enumerate().rev() {
296        if idx != 0
297            && let Some(str) = arg.as_str()
298            && str.starts_with('/')
299        {
300            return path_resolve(arena, cwd, args.split_off(arena, idx));
301        }
302    }
303
304    let mut results_final: SmallVec<[JsValue<'a>; 16]> = SmallVec::new();
305    let mut results: SmallVec<[JsValue<'a>; 16]> = SmallVec::new();
306    for arg in args {
307        let arg_parts = if let Some(str) = arg.as_str() {
308            let split = str.split('/');
309            Either::Left(split.map(|s| s.into()))
310        } else {
311            Either::Right(iter::once(arg))
312        };
313        for item in arg_parts {
314            if let Some(str) = item.as_str() {
315                match str {
316                    "" | "." => {
317                        if results_final.is_empty() && results.is_empty() {
318                            results_final.push(item);
319                        }
320                    }
321                    ".." => {
322                        if results.pop().is_none() {
323                            results_final.push(item);
324                        }
325                    }
326                    _ => results.push(item),
327                }
328            } else {
329                results_final.append(&mut results);
330                results_final.push(item);
331            }
332        }
333    }
334    results_final.append(&mut results);
335    let mut iter = results_final.into_iter();
336    let first = iter.next().unwrap();
337
338    let is_already_absolute =
339        first.is_empty_string() == Some(true) || first.starts_with("/") == Some(true);
340
341    let mut last_was_str = first.as_str().is_some();
342
343    if !is_already_absolute {
344        results.push(cwd);
345    }
346
347    results.push(first);
348    for part in iter {
349        let is_str = part.as_str().is_some();
350        if last_was_str && is_str {
351            results.push(rcstr!("/").into());
352        } else {
353            results.push(JsValue::alternatives(BumpVec::from_iter_in(
354                arena,
355                [rcstr!("/").into(), rcstr!("").into()],
356            )));
357        }
358        results.push(part);
359        last_was_str = is_str;
360    }
361
362    JsValue::concat(BumpVec::from_iter_in(arena, results))
363}
364
365fn path_dirname<'a>(arena: &'a Bump, mut args: BumpVec<'a, JsValue<'a>>) -> JsValue<'a> {
366    if let Some(arg) = args.iter_mut().next() {
367        if let Some(str) = arg.as_str() {
368            if let Some(i) = str.rfind('/') {
369                return JsValue::Constant(ConstantValue::Str(str[..i].to_string().into()));
370            } else {
371                return JsValue::Constant(ConstantValue::Str(rcstr!("").into()));
372            }
373        } else if let JsValue::Concat(_, items) = arg
374            && let Some(last) = items.last_mut()
375            && let Some(str) = last.as_str()
376            && let Some(i) = str.rfind('/')
377        {
378            *last = JsValue::Constant(ConstantValue::Str(str[..i].to_string().into()));
379            return take(arg);
380        }
381    }
382    JsValue::unknown(
383        JsValue::call_from_parts(
384            arena,
385            JsValue::WellKnownFunction(WellKnownFunctionKind::PathDirname),
386            args,
387        ),
388        true,
389        rcstr!("path.dirname with unsupported arguments"),
390    )
391}
392
393/// Resolve the contents of an import call, throwing errors
394/// if we come across any unsupported syntax.
395pub fn import<'a>(arena: &'a Bump, args: BumpVec<'a, JsValue<'a>>) -> JsValue<'a> {
396    match &args[..] {
397        [JsValue::Constant(ConstantValue::Str(v))] => JsValue::promise(
398            arena,
399            JsValue::Module(ModuleValue {
400                module: v.as_atom().into_owned().into(),
401                annotations: None,
402                analyze_for_constants: false,
403                reference: None,
404            }),
405        ),
406        _ => JsValue::unknown(
407            JsValue::call_from_parts(
408                arena,
409                JsValue::WellKnownFunction(WellKnownFunctionKind::Import),
410                args,
411            ),
412            true,
413            rcstr!("only a single constant argument is supported"),
414        ),
415    }
416}
417
418/// Resolve the contents of a require call, throwing errors
419/// if we come across any unsupported syntax.
420fn require<'a>(arena: &'a Bump, args: BumpVec<'a, JsValue<'a>>) -> JsValue<'a> {
421    if args.len() == 1 {
422        if let Some(s) = args[0].as_str() {
423            JsValue::Module(ModuleValue {
424                module: s.into(),
425                annotations: None,
426                analyze_for_constants: false,
427                reference: None,
428            })
429        } else {
430            JsValue::unknown(
431                JsValue::call_from_parts(
432                    arena,
433                    JsValue::WellKnownFunction(WellKnownFunctionKind::Require),
434                    args,
435                ),
436                true,
437                rcstr!("only constant argument is supported"),
438            )
439        }
440    } else {
441        JsValue::unknown(
442            JsValue::call_from_parts(
443                arena,
444                JsValue::WellKnownFunction(WellKnownFunctionKind::Require),
445                args,
446            ),
447            true,
448            rcstr!("only a single argument is supported"),
449        )
450    }
451}
452
453/// (try to) statically evaluate `require.context(...)()`
454fn require_context_require<'a>(
455    arena: &'a Bump,
456    val: Box<RequireContextValue>,
457    args: BumpVec<'a, JsValue<'a>>,
458) -> Result<JsValue<'a>> {
459    if args.is_empty() {
460        return Ok(JsValue::unknown(
461            JsValue::call_from_parts(
462                arena,
463                JsValue::WellKnownFunction(WellKnownFunctionKind::RequireContextRequire(val)),
464                args,
465            ),
466            true,
467            rcstr!(
468                "require.context(...).require() requires an argument specifying the module path"
469            ),
470        ));
471    }
472
473    let Some(s) = args[0].as_str() else {
474        return Ok(JsValue::unknown(
475            JsValue::call_from_parts(
476                arena,
477                JsValue::WellKnownFunction(WellKnownFunctionKind::RequireContextRequire(val)),
478                args,
479            ),
480            true,
481            rcstr!(
482                "require.context(...).require() only accepts a single, constant string argument"
483            ),
484        ));
485    };
486
487    let Some(m) = val.0.get(s) else {
488        return Ok(JsValue::unknown(
489            JsValue::call_from_parts(
490                arena,
491                JsValue::WellKnownFunction(WellKnownFunctionKind::RequireContextRequire(val)),
492                args,
493            ),
494            true,
495            rcstr!(
496                "require.context(...).require() can only be called with an argument that's in the \
497                 context"
498            ),
499        ));
500    };
501
502    Ok(JsValue::Module(ModuleValue {
503        module: m.to_string().into(),
504        annotations: None,
505        analyze_for_constants: false,
506        reference: None,
507    }))
508}
509
510/// (try to) statically evaluate `require.context(...).keys()`
511fn require_context_require_keys<'a>(
512    arena: &'a Bump,
513    val: Box<RequireContextValue>,
514    args: BumpVec<'a, JsValue<'a>>,
515) -> Result<JsValue<'a>> {
516    Ok(if args.is_empty() {
517        JsValue::array(BumpVec::from_iter_in(
518            arena,
519            val.0.keys().cloned().map(|k| k.into()),
520        ))
521    } else {
522        JsValue::unknown(
523            JsValue::call_from_parts(
524                arena,
525                JsValue::WellKnownFunction(WellKnownFunctionKind::RequireContextRequireKeys(val)),
526                args,
527            ),
528            true,
529            rcstr!("require.context(...).keys() does not accept arguments"),
530        )
531    })
532}
533
534/// (try to) statically evaluate `require.context(...).resolve()`
535fn require_context_require_resolve<'a>(
536    arena: &'a Bump,
537    val: Box<RequireContextValue>,
538    args: BumpVec<'a, JsValue<'a>>,
539) -> Result<JsValue<'a>> {
540    if args.len() != 1 {
541        return Ok(JsValue::unknown(
542            JsValue::call_from_parts(
543                arena,
544                JsValue::WellKnownFunction(WellKnownFunctionKind::RequireContextRequireResolve(
545                    val,
546                )),
547                args,
548            ),
549            true,
550            rcstr!(
551                "require.context(...).resolve() only accepts a single, constant string argument"
552            ),
553        ));
554    }
555
556    let Some(s) = args[0].as_str() else {
557        return Ok(JsValue::unknown(
558            JsValue::call_from_parts(
559                arena,
560                JsValue::WellKnownFunction(WellKnownFunctionKind::RequireContextRequireResolve(
561                    val,
562                )),
563                args,
564            ),
565            true,
566            rcstr!(
567                "require.context(...).resolve() only accepts a single, constant string argument"
568            ),
569        ));
570    };
571
572    let Some(m) = val.0.get(s) else {
573        return Ok(JsValue::unknown(
574            JsValue::call_from_parts(
575                arena,
576                JsValue::WellKnownFunction(WellKnownFunctionKind::RequireContextRequireResolve(
577                    val,
578                )),
579                args,
580            ),
581            true,
582            rcstr!(
583                "require.context(...).resolve() can only be called with an argument that's in the \
584                 context"
585            ),
586        ));
587    };
588
589    Ok(m.as_str().into())
590}
591
592fn path_to_file_url<'a>(arena: &'a Bump, args: BumpVec<'a, JsValue<'a>>) -> JsValue<'a> {
593    if args.len() == 1 {
594        if let Some(path) = args[0].as_str() {
595            Url::from_file_path(path)
596                .map(|url| JsValue::Url(String::from(url).into(), JsValueUrlKind::Absolute))
597                .unwrap_or_else(|_| {
598                    JsValue::unknown(
599                        JsValue::call_from_parts(
600                            arena,
601                            JsValue::WellKnownFunction(WellKnownFunctionKind::PathToFileUrl),
602                            args,
603                        ),
604                        true,
605                        rcstr!("url not parseable: path is relative or has an invalid prefix"),
606                    )
607                })
608        } else {
609            JsValue::unknown(
610                JsValue::call_from_parts(
611                    arena,
612                    JsValue::WellKnownFunction(WellKnownFunctionKind::PathToFileUrl),
613                    args,
614                ),
615                true,
616                rcstr!("only constant argument is supported"),
617            )
618        }
619    } else {
620        JsValue::unknown(
621            JsValue::call_from_parts(
622                arena,
623                JsValue::WellKnownFunction(WellKnownFunctionKind::PathToFileUrl),
624                args,
625            ),
626            true,
627            rcstr!("only a single argument is supported"),
628        )
629    }
630}
631
632fn well_known_function_member<'a>(
633    arena: &'a Bump,
634    kind: WellKnownFunctionKind<'a>,
635    prop: JsValue<'a>,
636) -> (JsValue<'a>, Modified) {
637    let new_value = match (kind, prop.as_str()) {
638        (WellKnownFunctionKind::Require, Some("resolve")) => {
639            JsValue::WellKnownFunction(WellKnownFunctionKind::RequireResolve)
640        }
641        (WellKnownFunctionKind::Require, Some("cache")) => {
642            JsValue::WellKnownObject(WellKnownObjectKind::RequireCache)
643        }
644        (WellKnownFunctionKind::Require, Some("context")) => {
645            JsValue::WellKnownFunction(WellKnownFunctionKind::RequireContext)
646        }
647        (WellKnownFunctionKind::RequireContextRequire(val), Some("resolve")) => {
648            JsValue::WellKnownFunction(WellKnownFunctionKind::RequireContextRequireResolve(val))
649        }
650        (WellKnownFunctionKind::RequireContextRequire(val), Some("keys")) => {
651            JsValue::WellKnownFunction(WellKnownFunctionKind::RequireContextRequireKeys(val))
652        }
653        (WellKnownFunctionKind::NodeStrongGlobalize, Some("SetRootDir")) => {
654            JsValue::WellKnownFunction(WellKnownFunctionKind::NodeStrongGlobalizeSetRootDir)
655        }
656        (WellKnownFunctionKind::NodeResolveFrom, Some("silent")) => {
657            JsValue::WellKnownFunction(WellKnownFunctionKind::NodeResolveFrom)
658        }
659        (WellKnownFunctionKind::Import, Some("meta")) => {
660            JsValue::WellKnownObject(WellKnownObjectKind::ImportMeta)
661        }
662        #[allow(unreachable_patterns)]
663        (kind, _) => {
664            return (
665                JsValue::member(arena, JsValue::WellKnownFunction(kind), prop),
666                Modified::No,
667            );
668        }
669    };
670    (new_value, Modified::Yes)
671}
672
673async fn well_known_object_member<'a>(
674    arena: &'a ThreadLocal<Bump>,
675    kind: WellKnownObjectKind,
676    prop: JsValue<'a>,
677    compile_time_info: Vc<CompileTimeInfo>,
678) -> Result<(JsValue<'a>, Modified)> {
679    let new_value = match kind {
680        WellKnownObjectKind::GlobalObject => global_object(arena.get_or_default(), prop),
681        WellKnownObjectKind::PathModule | WellKnownObjectKind::PathModuleDefault => {
682            path_module_member(arena, kind, prop, compile_time_info).await?
683        }
684        WellKnownObjectKind::FsModule
685        | WellKnownObjectKind::FsModuleDefault
686        | WellKnownObjectKind::FsModulePromises
687        | WellKnownObjectKind::GracefulFsModule
688        | WellKnownObjectKind::GracefulFsModuleDefault => {
689            fs_module_member(arena.get_or_default(), kind, prop)
690        }
691        WellKnownObjectKind::FsExtraModule | WellKnownObjectKind::FsExtraModuleDefault => {
692            fs_extra_module_member(arena.get_or_default(), kind, prop)
693        }
694        WellKnownObjectKind::ModuleModule | WellKnownObjectKind::ModuleModuleDefault => {
695            module_module_member(arena.get_or_default(), kind, prop)
696        }
697        WellKnownObjectKind::UrlModule | WellKnownObjectKind::UrlModuleDefault => {
698            url_module_member(arena.get_or_default(), kind, prop)
699        }
700        WellKnownObjectKind::WorkerThreadsModule
701        | WellKnownObjectKind::WorkerThreadsModuleDefault => {
702            worker_threads_module_member(arena.get_or_default(), kind, prop)
703        }
704        WellKnownObjectKind::ChildProcessModule
705        | WellKnownObjectKind::ChildProcessModuleDefault => {
706            child_process_module_member(arena.get_or_default(), kind, prop)
707        }
708        WellKnownObjectKind::OsModule | WellKnownObjectKind::OsModuleDefault => {
709            os_module_member(arena.get_or_default(), kind, prop)
710        }
711        WellKnownObjectKind::NodeProcessModule => {
712            node_process_member(arena, prop, compile_time_info).await?
713        }
714        WellKnownObjectKind::NodePreGyp => node_pre_gyp(arena.get_or_default(), prop),
715        WellKnownObjectKind::NodeExpressApp => express(arena.get_or_default(), prop),
716        WellKnownObjectKind::NodeProtobufLoader => protobuf_loader(arena.get_or_default(), prop),
717        WellKnownObjectKind::ImportMeta => match prop.as_str() {
718            Some("env") => JsValue::WellKnownObject(WellKnownObjectKind::ImportMetaEnv),
719            // import.meta.turbopackHot is the ESM equivalent of module.hot for HMR
720            Some("turbopackHot") if compile_time_info.await?.hot_module_replacement_enabled => {
721                JsValue::WellKnownObject(WellKnownObjectKind::ModuleHot)
722            }
723            // import.meta.glob is the Vite-compatible glob import.
724            // Note: import.meta.globEager() (removed in Vite 3) is intentionally
725            // not supported. Users should migrate to import.meta.glob('...', { eager: true }).
726            Some("glob") => JsValue::WellKnownFunction(WellKnownFunctionKind::ImportMetaGlob),
727            _ => {
728                return Ok((
729                    JsValue::member(arena.get_or_default(), JsValue::WellKnownObject(kind), prop),
730                    Modified::No,
731                ));
732            }
733        },
734        WellKnownObjectKind::ImportMetaEnv => {
735            let compile_time_info = compile_time_info.await?;
736            let mode = compile_time_info
737                .defines
738                .read_process_env(rcstr!("NODE_ENV"))
739                .owned()
740                .await?
741                .unwrap_or_else(|| rcstr!("development"));
742            let is_prod = mode == "production";
743
744            match prop.as_str() {
745                Some("MODE") => JsValue::from(mode),
746                Some("PROD") => JsValue::from(ConstantValue::from(is_prod)),
747                Some("DEV") => JsValue::from(ConstantValue::from(!is_prod)),
748                Some("BASE_URL") => {
749                    JsValue::from(compile_time_info.import_meta_env_base_url.clone())
750                }
751                Some("SSR") => JsValue::from(ConstantValue::from(matches!(
752                    *compile_time_info.environment.rendering().await?,
753                    Rendering::Server
754                ))),
755                Some(_) => JsValue::Constant(ConstantValue::Undefined),
756                None => {
757                    return Ok((
758                        JsValue::member(
759                            arena.get_or_default(),
760                            JsValue::WellKnownObject(kind),
761                            prop,
762                        ),
763                        Modified::No,
764                    ));
765                }
766            }
767        }
768        WellKnownObjectKind::ModuleHot => match prop.as_str() {
769            Some("accept") => JsValue::WellKnownFunction(WellKnownFunctionKind::ModuleHotAccept),
770            Some("decline") => JsValue::WellKnownFunction(WellKnownFunctionKind::ModuleHotDecline),
771            _ => {
772                return Ok((
773                    JsValue::unknown(
774                        JsValue::member(
775                            arena.get_or_default(),
776                            JsValue::WellKnownObject(kind),
777                            prop,
778                        ),
779                        true,
780                        rcstr!("unsupported property on module.hot"),
781                    ),
782                    Modified::Yes,
783                ));
784            }
785        },
786        WellKnownObjectKind::Navigator => match prop.as_str() {
787            Some("serviceWorker") => {
788                JsValue::WellKnownObject(WellKnownObjectKind::NavigatorServiceWorker)
789            }
790            _ => {
791                return Ok((
792                    JsValue::member(arena.get_or_default(), JsValue::WellKnownObject(kind), prop),
793                    Modified::No,
794                ));
795            }
796        },
797        WellKnownObjectKind::NavigatorServiceWorker => match prop.as_str() {
798            Some("register") => {
799                JsValue::WellKnownFunction(WellKnownFunctionKind::ServiceWorkerRegister)
800            }
801            _ => {
802                return Ok((
803                    JsValue::member(arena.get_or_default(), JsValue::WellKnownObject(kind), prop),
804                    Modified::No,
805                ));
806            }
807        },
808        #[allow(unreachable_patterns)]
809        _ => {
810            return Ok((
811                JsValue::member(arena.get_or_default(), JsValue::WellKnownObject(kind), prop),
812                Modified::No,
813            ));
814        }
815    };
816    Ok((new_value, Modified::Yes))
817}
818
819fn global_object<'a>(arena: &'a Bump, prop: JsValue<'a>) -> JsValue<'a> {
820    match prop.as_str() {
821        Some("assign") => JsValue::WellKnownFunction(WellKnownFunctionKind::ObjectAssign),
822        _ => JsValue::unknown(
823            JsValue::member(
824                arena,
825                JsValue::WellKnownObject(WellKnownObjectKind::GlobalObject),
826                prop,
827            ),
828            true,
829            rcstr!("unsupported property on global Object"),
830        ),
831    }
832}
833
834async fn path_module_member<'a>(
835    arena: &'a ThreadLocal<Bump>,
836    kind: WellKnownObjectKind,
837    prop: JsValue<'a>,
838    compile_time_info: Vc<CompileTimeInfo>,
839) -> Result<JsValue<'a>> {
840    Ok(match (kind, prop.as_str()) {
841        (.., Some("join")) => JsValue::WellKnownFunction(WellKnownFunctionKind::PathJoin),
842        (.., Some("dirname")) => JsValue::WellKnownFunction(WellKnownFunctionKind::PathDirname),
843        (.., Some("resolve")) => {
844            // cwd is added while resolving in references.rs
845            JsValue::WellKnownFunction(WellKnownFunctionKind::PathResolve(
846                arena.get_or_default().alloc(JsValue::from("")),
847            ))
848        }
849        (.., Some("sep")) => compile_time_info
850            .environment()
851            .compile_target()
852            .await?
853            .platform
854            .path_separator()
855            .into(),
856        (WellKnownObjectKind::PathModule, Some("default")) => {
857            JsValue::WellKnownObject(WellKnownObjectKind::PathModuleDefault)
858        }
859        _ => JsValue::unknown(
860            JsValue::member(
861                arena.get_or_default(),
862                JsValue::WellKnownObject(WellKnownObjectKind::PathModule),
863                prop,
864            ),
865            true,
866            rcstr!("unsupported property on Node.js path module"),
867        ),
868    })
869}
870
871fn fs_module_member<'a>(
872    arena: &'a Bump,
873    kind: WellKnownObjectKind,
874    prop: JsValue<'a>,
875) -> JsValue<'a> {
876    if let Some(word) = prop.as_str() {
877        match (kind, word) {
878            (
879                ..,
880                "realpath" | "realpathSync" | "stat" | "statSync" | "existsSync"
881                | "createReadStream" | "exists" | "open" | "openSync" | "readFile" | "readFileSync",
882            ) => {
883                return JsValue::WellKnownFunction(WellKnownFunctionKind::FsReadMethod(
884                    word.into(),
885                ));
886            }
887            (.., "readdir" | "readdirSync") => {
888                return JsValue::WellKnownFunction(WellKnownFunctionKind::FsReadDir);
889            }
890            (WellKnownObjectKind::FsModule | WellKnownObjectKind::FsModuleDefault, "promises") => {
891                return JsValue::WellKnownObject(WellKnownObjectKind::FsModulePromises);
892            }
893            (WellKnownObjectKind::FsModule, "default") => {
894                return JsValue::WellKnownObject(WellKnownObjectKind::FsModuleDefault);
895            }
896            _ => {}
897        }
898    }
899    JsValue::unknown(
900        JsValue::member(
901            arena,
902            JsValue::WellKnownObject(WellKnownObjectKind::FsModule),
903            prop,
904        ),
905        true,
906        rcstr!("unsupported property on Node.js fs module"),
907    )
908}
909
910fn fs_extra_module_member<'a>(
911    arena: &'a Bump,
912    kind: WellKnownObjectKind,
913    prop: JsValue<'a>,
914) -> JsValue<'a> {
915    if let Some(word) = prop.as_str() {
916        match (kind, word) {
917            // regular fs methods
918            (
919                ..,
920                "realpath" | "realpathSync" | "stat" | "statSync" | "existsSync"
921                | "createReadStream" | "exists" | "open" | "openSync" | "readFile" | "readFileSync",
922            ) => {
923                return JsValue::WellKnownFunction(WellKnownFunctionKind::FsReadMethod(
924                    word.into(),
925                ));
926            }
927            (.., "readdir" | "readdirSync") => {
928                return JsValue::WellKnownFunction(WellKnownFunctionKind::FsReadDir);
929            }
930            // fs-extra specific
931            (
932                ..,
933                "pathExists" | "pathExistsSync" | "readJson" | "readJSON" | "readJsonSync"
934                | "readJSONSync",
935            ) => {
936                return JsValue::WellKnownFunction(WellKnownFunctionKind::FsReadMethod(
937                    word.into(),
938                ));
939            }
940            (WellKnownObjectKind::FsExtraModule, "default") => {
941                return JsValue::WellKnownObject(WellKnownObjectKind::FsExtraModuleDefault);
942            }
943            _ => {}
944        }
945    }
946    JsValue::unknown(
947        JsValue::member(
948            arena,
949            JsValue::WellKnownObject(WellKnownObjectKind::FsExtraModule),
950            prop,
951        ),
952        true,
953        rcstr!("unsupported property on fs-extra module"),
954    )
955}
956
957fn module_module_member<'a>(
958    arena: &'a Bump,
959    kind: WellKnownObjectKind,
960    prop: JsValue<'a>,
961) -> JsValue<'a> {
962    match (kind, prop.as_str()) {
963        (.., Some("createRequire")) => {
964            JsValue::WellKnownFunction(WellKnownFunctionKind::CreateRequire)
965        }
966        (WellKnownObjectKind::ModuleModule, Some("default")) => {
967            JsValue::WellKnownObject(WellKnownObjectKind::ModuleModuleDefault)
968        }
969        _ => JsValue::unknown(
970            JsValue::member(
971                arena,
972                JsValue::WellKnownObject(WellKnownObjectKind::ModuleModule),
973                prop,
974            ),
975            true,
976            rcstr!("unsupported property on Node.js `module` module"),
977        ),
978    }
979}
980
981fn url_module_member<'a>(
982    arena: &'a Bump,
983    kind: WellKnownObjectKind,
984    prop: JsValue<'a>,
985) -> JsValue<'a> {
986    match (kind, prop.as_str()) {
987        (.., Some("pathToFileURL")) => {
988            JsValue::WellKnownFunction(WellKnownFunctionKind::PathToFileUrl)
989        }
990        (WellKnownObjectKind::UrlModule, Some("default")) => {
991            JsValue::WellKnownObject(WellKnownObjectKind::UrlModuleDefault)
992        }
993        _ => JsValue::unknown(
994            JsValue::member(
995                arena,
996                JsValue::WellKnownObject(WellKnownObjectKind::UrlModule),
997                prop,
998            ),
999            true,
1000            rcstr!("unsupported property on Node.js url module"),
1001        ),
1002    }
1003}
1004
1005fn worker_threads_module_member<'a>(
1006    arena: &'a Bump,
1007    kind: WellKnownObjectKind,
1008    prop: JsValue<'a>,
1009) -> JsValue<'a> {
1010    match (kind, prop.as_str()) {
1011        (.., Some("Worker")) => {
1012            JsValue::WellKnownFunction(WellKnownFunctionKind::NodeWorkerConstructor)
1013        }
1014        (WellKnownObjectKind::WorkerThreadsModule, Some("default")) => {
1015            JsValue::WellKnownObject(WellKnownObjectKind::WorkerThreadsModuleDefault)
1016        }
1017        _ => JsValue::unknown(
1018            JsValue::member(
1019                arena,
1020                JsValue::WellKnownObject(WellKnownObjectKind::WorkerThreadsModule),
1021                prop,
1022            ),
1023            true,
1024            rcstr!("unsupported property on Node.js worker_threads module"),
1025        ),
1026    }
1027}
1028
1029fn child_process_module_member<'a>(
1030    arena: &'a Bump,
1031    kind: WellKnownObjectKind,
1032    prop: JsValue<'a>,
1033) -> JsValue<'a> {
1034    let prop_str = prop.as_str();
1035    match (kind, prop_str) {
1036        (.., Some("spawn" | "spawnSync" | "execFile" | "execFileSync")) => {
1037            JsValue::WellKnownFunction(WellKnownFunctionKind::ChildProcessSpawnMethod(
1038                prop_str.unwrap().into(),
1039            ))
1040        }
1041        (.., Some("fork")) => JsValue::WellKnownFunction(WellKnownFunctionKind::ChildProcessFork),
1042        (WellKnownObjectKind::ChildProcessModule, Some("default")) => {
1043            JsValue::WellKnownObject(WellKnownObjectKind::ChildProcessModuleDefault)
1044        }
1045
1046        _ => JsValue::unknown(
1047            JsValue::member(
1048                arena,
1049                JsValue::WellKnownObject(WellKnownObjectKind::ChildProcessModule),
1050                prop,
1051            ),
1052            true,
1053            rcstr!("unsupported property on Node.js child_process module"),
1054        ),
1055    }
1056}
1057
1058fn os_module_member<'a>(
1059    arena: &'a Bump,
1060    kind: WellKnownObjectKind,
1061    prop: JsValue<'a>,
1062) -> JsValue<'a> {
1063    match (kind, prop.as_str()) {
1064        (.., Some("platform")) => JsValue::WellKnownFunction(WellKnownFunctionKind::OsPlatform),
1065        (.., Some("arch")) => JsValue::WellKnownFunction(WellKnownFunctionKind::OsArch),
1066        (.., Some("endianness")) => JsValue::WellKnownFunction(WellKnownFunctionKind::OsEndianness),
1067        (WellKnownObjectKind::OsModule, Some("default")) => {
1068            JsValue::WellKnownObject(WellKnownObjectKind::OsModuleDefault)
1069        }
1070        _ => JsValue::unknown(
1071            JsValue::member(
1072                arena,
1073                JsValue::WellKnownObject(WellKnownObjectKind::OsModule),
1074                prop,
1075            ),
1076            true,
1077            rcstr!("unsupported property on Node.js os module"),
1078        ),
1079    }
1080}
1081
1082async fn node_process_member<'a>(
1083    arena: &'a ThreadLocal<Bump>,
1084    prop: JsValue<'a>,
1085    compile_time_info: Vc<CompileTimeInfo>,
1086) -> Result<JsValue<'a>> {
1087    Ok(match prop.as_str() {
1088        Some("arch") => compile_time_info
1089            .environment()
1090            .compile_target()
1091            .await?
1092            .arch
1093            .as_str()
1094            .into(),
1095        Some("platform") => compile_time_info
1096            .environment()
1097            .compile_target()
1098            .await?
1099            .platform
1100            .as_str()
1101            .into(),
1102        Some("cwd") => JsValue::WellKnownFunction(WellKnownFunctionKind::ProcessCwd),
1103        Some("argv") => JsValue::WellKnownObject(WellKnownObjectKind::NodeProcessArgv),
1104        Some("env") => JsValue::WellKnownObject(WellKnownObjectKind::NodeProcessEnv),
1105        _ => JsValue::unknown(
1106            JsValue::member(
1107                arena.get_or_default(),
1108                JsValue::WellKnownObject(WellKnownObjectKind::NodeProcessModule),
1109                prop,
1110            ),
1111            true,
1112            rcstr!("unsupported property on Node.js process object"),
1113        ),
1114    })
1115}
1116
1117fn node_pre_gyp<'a>(arena: &'a Bump, prop: JsValue<'a>) -> JsValue<'a> {
1118    match prop.as_str() {
1119        Some("find") => JsValue::WellKnownFunction(WellKnownFunctionKind::NodePreGypFind),
1120        _ => JsValue::unknown(
1121            JsValue::member(
1122                arena,
1123                JsValue::WellKnownObject(WellKnownObjectKind::NodePreGyp),
1124                prop,
1125            ),
1126            true,
1127            rcstr!("unsupported property on @mapbox/node-pre-gyp module"),
1128        ),
1129    }
1130}
1131
1132fn express<'a>(arena: &'a Bump, prop: JsValue<'a>) -> JsValue<'a> {
1133    match prop.as_str() {
1134        Some("set") => JsValue::WellKnownFunction(WellKnownFunctionKind::NodeExpressSet),
1135        _ => JsValue::unknown(
1136            JsValue::member(
1137                arena,
1138                JsValue::WellKnownObject(WellKnownObjectKind::NodeExpressApp),
1139                prop,
1140            ),
1141            true,
1142            rcstr!("unsupported property on require('express')() object"),
1143        ),
1144    }
1145}
1146
1147fn protobuf_loader<'a>(arena: &'a Bump, prop: JsValue<'a>) -> JsValue<'a> {
1148    match prop.as_str() {
1149        Some("load") | Some("loadSync") => {
1150            JsValue::WellKnownFunction(WellKnownFunctionKind::NodeProtobufLoad)
1151        }
1152        _ => JsValue::unknown(
1153            JsValue::member(
1154                arena,
1155                JsValue::WellKnownObject(WellKnownObjectKind::NodeProtobufLoader),
1156                prop,
1157            ),
1158            true,
1159            rcstr!("unsupported property on require('@grpc/proto-loader') object"),
1160        ),
1161    }
1162}
1163
1164#[cfg(test)]
1165mod tests {
1166    use bumpalo::Bump;
1167
1168    use super::path_join;
1169    use crate::analyzer::{BumpVec, JsValue};
1170
1171    /// Renders the result of [`path_join`] into a single `String`.
1172    ///
1173    /// `path_join` returns a [`JsValue::Concat`] of the resulting path segments
1174    /// interleaved with `/` separators (or a bare [`JsValue::Constant`] for the
1175    /// empty-args case). When every input is a constant string the entire result
1176    /// is made of constant strings, so we can flatten it back into the joined
1177    /// path by concatenating each leaf. This avoids relying on `normalize`, which
1178    /// would collapse a result of `""` into an empty `Concat` rather than a
1179    /// `Constant`.
1180    ///
1181    /// For non-constant inputs the result also contains [`JsValue::FreeVar`]
1182    /// leaves and `"/"`-or-`""` separator [`JsValue::Alternatives`]; we render a
1183    /// free var as its name and pick the first (`"/"`) option of a separator so
1184    /// the rendering stays deterministic.
1185    fn render(value: &JsValue<'_>) -> String {
1186        match value {
1187            JsValue::Concat(_, parts) => parts.iter().map(render).collect(),
1188            JsValue::Alternatives { values, .. } => render(&values[0]),
1189            JsValue::FreeVar(name) => name.to_string(),
1190            other => other
1191                .as_str()
1192                .expect("path_join over constant strings should yield constant strings")
1193                .to_string(),
1194        }
1195    }
1196
1197    /// Calls `path_join` with the given string segments and returns the joined
1198    /// path as a `String`.
1199    fn join(arena: &Bump, segments: &[&str]) -> String {
1200        let args = BumpVec::from_iter_in(arena, segments.iter().map(|s| JsValue::from(*s)));
1201        render(&path_join(arena, args))
1202    }
1203
1204    /// Cases where `path_join`'s static-analysis result matches the runtime
1205    /// behaviour of Node's `path.posix.join`.
1206    ///
1207    /// Mirrors the `joinTests` table in Node's `test/parallel/test-path-join.js`:
1208    /// <https://github.com/nodejs/node/blob/main/test/parallel/test-path-join.js>
1209    #[test]
1210    fn matches_node_path_posix_join() {
1211        let arena = Bump::new();
1212
1213        assert_eq!(join(&arena, &[]), ".");
1214        assert_eq!(join(&arena, &["/.", "x/b", "..", "/b/c.js"]), "/x/b/c.js");
1215        assert_eq!(join(&arena, &["foo", "../../../bar"]), "../../bar");
1216        assert_eq!(join(&arena, &["foo/", "../../../bar"]), "../../bar");
1217        assert_eq!(join(&arena, &["foo/x", "../../../bar"]), "../bar");
1218        assert_eq!(join(&arena, &["foo/x", "./bar"]), "foo/x/bar");
1219        assert_eq!(join(&arena, &["foo/x/", "./bar"]), "foo/x/bar");
1220        assert_eq!(join(&arena, &["foo/x/", ".", "bar"]), "foo/x/bar");
1221        assert_eq!(join(&arena, &[".", ".", "."]), ".");
1222        assert_eq!(join(&arena, &[".", "./", "."]), ".");
1223        assert_eq!(join(&arena, &[".", "/./", "."]), ".");
1224        assert_eq!(join(&arena, &[".", "/////./", "."]), ".");
1225        assert_eq!(join(&arena, &["."]), ".");
1226        assert_eq!(join(&arena, &["foo", "/bar"]), "foo/bar");
1227        assert_eq!(join(&arena, &["", "/foo"]), "/foo");
1228        assert_eq!(join(&arena, &["", "", "/foo"]), "/foo");
1229        assert_eq!(join(&arena, &["foo", ""]), "foo");
1230        assert_eq!(join(&arena, &["foo", "", "/bar"]), "foo/bar");
1231        assert_eq!(join(&arena, &[" /foo"]), " /foo");
1232        assert_eq!(join(&arena, &[" ", "foo"]), " /foo");
1233        assert_eq!(join(&arena, &[" ", "."]), " ");
1234        assert_eq!(join(&arena, &[" ", ""]), " ");
1235        assert_eq!(join(&arena, &["/", "foo"]), "/foo");
1236        assert_eq!(join(&arena, &["/", "/foo"]), "/foo");
1237        assert_eq!(join(&arena, &["/", "//foo"]), "/foo");
1238        assert_eq!(join(&arena, &["/", "", "/foo"]), "/foo");
1239        assert_eq!(join(&arena, &["", "/", "foo"]), "/foo");
1240        assert_eq!(join(&arena, &["", "/", "/foo"]), "/foo");
1241    }
1242
1243    /// `..` cancels the most recent entry on the poppable `segments` stack.
1244    #[test]
1245    fn dotdot_pops_from_segments() {
1246        let arena = Bump::new();
1247
1248        assert_eq!(join(&arena, &["foo/bar/baz", "../.."]), "foo");
1249        assert_eq!(join(&arena, &["a/b", ".."]), "a");
1250        assert_eq!(join(&arena, &["a/b/c/d", "../../.."]), "a");
1251        // The `..` only pops what is currently on the stack.
1252        assert_eq!(join(&arena, &["a/b", "../../c"]), "c");
1253    }
1254
1255    /// When `segments` is empty there is nothing to pop, so `..` is committed to
1256    /// `locked_prefix` instead. Once there it can no longer be cancelled, which
1257    /// is why `..` is not clamped at the root.
1258    #[test]
1259    fn unpoppable_dotdot_is_locked_into_prefix() {
1260        let arena = Bump::new();
1261
1262        assert_eq!(join(&arena, &["../../foo"]), "../../foo");
1263        // The leading `..` is locked into the prefix; the later `foo/..` cancels
1264        // within `segments`, leaving only the locked `..`.
1265        assert_eq!(join(&arena, &["..", "foo", ".."]), "..");
1266        // `..` past an absolute root accumulates rather than being clamped.
1267        assert_eq!(join(&arena, &["/foo", "../../bar"]), "/../bar");
1268    }
1269
1270    /// A leading `.` (or empty segment) is locked into `locked_prefix`, but only
1271    /// while both stacks are still empty — interior `.`/empty segments are
1272    /// dropped.
1273    #[test]
1274    fn leading_dot_is_locked_but_interior_is_dropped() {
1275        let arena = Bump::new();
1276
1277        assert_eq!(join(&arena, &["./foo", ".", "bar"]), "./foo/bar");
1278        assert_eq!(join(&arena, &["foo/x", ".", "bar"]), "foo/x/bar");
1279        assert_eq!(join(&arena, &[".", ".", "."]), ".");
1280    }
1281
1282    /// Cases where `path_join`'s static-analysis result diverges from Node's
1283    /// `path.posix.join`. These all involve absolute paths (a leading `/`) or
1284    /// empty-string inputs, which the static analysis does not model the same way
1285    /// Node does at runtime.
1286    ///
1287    /// The assertions below are intentionally commented out — they describe the
1288    /// behaviour we would want to match (Node's computed value) but which
1289    /// `path_join` does not currently produce. The trailing comment on each line
1290    /// records what `path_join` returns today.
1291    ///
1292    /// Mirrors additional rows of the `joinTests` table in Node's
1293    /// `test/parallel/test-path-join.js`:
1294    /// <https://github.com/nodejs/node/blob/main/test/parallel/test-path-join.js>
1295    #[test]
1296    fn diverges_from_node_path_posix_join() {
1297        // let arena = Bump::new();
1298
1299        // path_join: "/foo"
1300        // assert_eq!(join(&arena, &["", "foo"]), "foo");
1301        // path_join: "/foo"
1302        // assert_eq!(join(&arena, &["", "", "foo"]), "foo");
1303        // path_join: "/../../foo"
1304        // assert_eq!(join(&arena, &["", "..", "..", "/foo"]), "../../foo");
1305        // path_join: ""
1306        // assert_eq!(join(&arena, &["/"]), "/");
1307        // path_join: ""
1308        // assert_eq!(join(&arena, &["/", "."]), "/");
1309        // path_join: "/../../bar"
1310        // assert_eq!(join(&arena, &["/foo", "../../../bar"]), "/bar");
1311        // path_join: "/.."
1312        // assert_eq!(join(&arena, &["/", ".."]), "/");
1313        // path_join: "/../.."
1314        // assert_eq!(join(&arena, &["/", "..", ".."]), "/");
1315        // path_join: ""
1316        // assert_eq!(join(&arena, &["", "."]), ".");
1317        // path_join: ""
1318        // assert_eq!(join(&arena, &[""]), ".");
1319        // path_join: ""
1320        // assert_eq!(join(&arena, &["", ""]), ".");
1321    }
1322
1323    /// A non-constant (dynamic) segment flushes the working `segments` stack into
1324    /// `locked_prefix` and freezes everything before it. A later `..` cannot pop
1325    /// across that boundary, unlike the all-constant case.
1326    #[test]
1327    fn dynamic_segment_freezes_preceding_segments() {
1328        let arena = Bump::new();
1329
1330        // Baseline: with all-constant segments, `..` pops `x` off `segments`.
1331        assert_eq!(join(&arena, &["foo", "x", ".."]), "foo");
1332
1333        // With a dynamic segment between `foo` and `..`, `foo` is flushed into
1334        // `locked_prefix` and survives — the trailing `..` cannot reach it.
1335        let args = BumpVec::from_iter_in(
1336            &arena,
1337            [
1338                JsValue::from("foo"),
1339                JsValue::FreeVar("dynamic".into()),
1340                JsValue::from(".."),
1341            ],
1342        );
1343        assert_eq!(render(&path_join(&arena, args)), "foo/dynamic/..");
1344    }
1345}