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