Skip to main content

turbopack_ecmascript/analyzer/
mod.rs

1use swc_core::{
2    common::Mark,
3    ecma::ast::{Id, Ident},
4};
5
6pub(crate) use self::imports::ImportMap;
7
8pub mod builtin;
9pub mod bump_vec;
10pub mod graph;
11pub mod imports;
12pub mod linker;
13pub mod side_effects;
14pub mod top_level_await;
15pub mod well_known;
16
17mod jsvalue;
18pub use bump_vec::BumpVec;
19pub use bumpalo::Bump;
20pub use jsvalue::*;
21pub use thread_local::ThreadLocal;
22pub use well_known::{kinds::*, require_context::*};
23
24fn is_unresolved(i: &Ident, unresolved_mark: Mark) -> bool {
25    i.ctxt.outer() == unresolved_mark
26}
27
28fn is_unresolved_id(i: &Id, unresolved_mark: Mark) -> bool {
29    i.1.outer() == unresolved_mark
30}
31
32/// Whether a visitor — or one of the builtin / well-known rewrite helpers —
33/// changed the `JsValue` it was given. Returned alongside the (possibly
34/// rewritten) value. [`Modified::Yes`] makes the linker re-enter the value for
35/// further processing; [`Modified::No`] means it is final.
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum Modified {
38    Yes,
39    No,
40}
41
42impl Modified {
43    /// `true` if the value was modified.
44    pub fn is_modified(self) -> bool {
45        matches!(self, Modified::Yes)
46    }
47}
48
49impl From<bool> for Modified {
50    fn from(modified: bool) -> Self {
51        if modified {
52            Modified::Yes
53        } else {
54            Modified::No
55        }
56    }
57}
58
59#[doc(hidden)]
60pub mod test_utils {
61    use anyhow::Result;
62    use turbo_rcstr::rcstr;
63    use turbo_tasks::{FxIndexMap, PrettyPrintError, Vc};
64    use turbopack_core::compile_time_info::CompileTimeInfo;
65
66    use super::{
67        ConstantValue, JsValue, JsValueUrlKind, Modified, ModuleValue, WellKnownFunctionKind,
68        WellKnownObjectKind, builtin::early_replace_builtin, well_known::replace_well_known,
69    };
70    use crate::{
71        analyzer::{
72            Bump, RequireContextValue, ThreadLocal, builtin::replace_builtin,
73            imports::ImportAttributes, parse_require_context,
74        },
75        utils::module_value_to_well_known_object,
76    };
77
78    pub async fn early_visitor<'a>(
79        _arena: &'a ThreadLocal<Bump>,
80        mut v: JsValue<'a>,
81    ) -> Result<(JsValue<'a>, Modified)> {
82        let m = early_replace_builtin(&mut v);
83        Ok((v, m))
84    }
85
86    /// Visitor that replaces well known functions and objects with their
87    /// corresponding values. Returns the new value and whether it was modified.
88    pub async fn visitor<'a>(
89        arena: &'a ThreadLocal<Bump>,
90        v: JsValue<'a>,
91        compile_time_info: Vc<CompileTimeInfo>,
92        attributes: &ImportAttributes,
93    ) -> Result<(JsValue<'a>, Modified)> {
94        let ImportAttributes { ignore, .. } = *attributes;
95        let mut new_value = match v {
96            JsValue::Call(_, ref call)
97                if matches!(
98                    call.callee(),
99                    JsValue::WellKnownFunction(WellKnownFunctionKind::Import)
100                ) =>
101            {
102                match &call.args()[0] {
103                    JsValue::Constant(ConstantValue::Str(v)) => JsValue::promise(
104                        arena.get_or_default(),
105                        JsValue::Module(ModuleValue {
106                            module: v.as_atom().into_owned().into(),
107                            annotations: None,
108                        }),
109                    ),
110                    _ => v.into_unknown(true, rcstr!("import() non constant")),
111                }
112            }
113            JsValue::Call(_, ref call)
114                if matches!(
115                    call.callee(),
116                    JsValue::WellKnownFunction(WellKnownFunctionKind::CreateRequire)
117                ) =>
118            {
119                if let [JsValue::Member(_, obj, prop)] = call.args()
120                    && matches!(
121                        &**obj,
122                        JsValue::WellKnownObject(WellKnownObjectKind::ImportMeta)
123                    )
124                    && let JsValue::Constant(ConstantValue::Str(prop)) = &**prop
125                    && prop.as_str() == "url"
126                {
127                    JsValue::WellKnownFunction(WellKnownFunctionKind::Require)
128                } else {
129                    v.into_unknown(true, rcstr!("createRequire() non constant"))
130                }
131            }
132            JsValue::Call(_, ref call)
133                if matches!(
134                    call.callee(),
135                    JsValue::WellKnownFunction(WellKnownFunctionKind::RequireResolve)
136                ) =>
137            {
138                match &call.args()[0] {
139                    JsValue::Constant(v) => (v.to_string() + "/resolved/lib/index.js").into(),
140                    _ => v.into_unknown(true, rcstr!("require.resolve non constant")),
141                }
142            }
143            JsValue::Call(_, ref call)
144                if matches!(
145                    call.callee(),
146                    JsValue::WellKnownFunction(WellKnownFunctionKind::ImportMetaGlob)
147                ) =>
148            {
149                v.into_unknown(false, rcstr!("import.meta.glob()"))
150            }
151            JsValue::Call(_, ref call)
152                if matches!(
153                    call.callee(),
154                    JsValue::WellKnownFunction(WellKnownFunctionKind::RequireContext)
155                ) =>
156            {
157                match parse_require_context(call.args()) {
158                    Ok(options) => {
159                        let mut map = FxIndexMap::default();
160
161                        map.insert(
162                            rcstr!("./a"),
163                            format!("[context: {}]/a", options.dir).into(),
164                        );
165                        map.insert(
166                            rcstr!("./b"),
167                            format!("[context: {}]/b", options.dir).into(),
168                        );
169                        map.insert(
170                            rcstr!("./c"),
171                            format!("[context: {}]/c", options.dir).into(),
172                        );
173
174                        JsValue::WellKnownFunction(WellKnownFunctionKind::RequireContextRequire(
175                            Box::new(RequireContextValue(map)),
176                        ))
177                    }
178                    Err(err) => v.into_unknown(true, PrettyPrintError(&err).to_string().into()),
179                }
180            }
181            JsValue::New(_, ref call)
182                if matches!(
183                    call.callee(),
184                    JsValue::WellKnownFunction(WellKnownFunctionKind::URLConstructor)
185                ) =>
186            {
187                if let [
188                    JsValue::Constant(ConstantValue::Str(url)),
189                    JsValue::Member(_, obj, prop),
190                ] = call.args()
191                    && matches!(
192                        &**obj,
193                        JsValue::WellKnownObject(WellKnownObjectKind::ImportMeta)
194                    )
195                    && let JsValue::Constant(ConstantValue::Str(prop)) = &**prop
196                {
197                    if prop.as_str() == "url" {
198                        // TODO avoid clone
199                        JsValue::Url(url.clone(), JsValueUrlKind::Relative)
200                    } else {
201                        v.into_unknown(true, rcstr!("new non constant"))
202                    }
203                } else {
204                    v.into_unknown(true, rcstr!("new non constant"))
205                }
206            }
207            JsValue::FreeVar(ref var) => match &**var {
208                "__dirname" => rcstr!("__dirname").into(),
209                "__filename" => rcstr!("__filename").into(),
210
211                "require" => JsValue::unknown_if(
212                    ignore,
213                    JsValue::WellKnownFunction(WellKnownFunctionKind::Require),
214                    true,
215                    rcstr!("ignored require"),
216                ),
217                "import" => JsValue::unknown_if(
218                    ignore,
219                    JsValue::WellKnownFunction(WellKnownFunctionKind::Import),
220                    true,
221                    rcstr!("ignored import"),
222                ),
223                "Worker" => JsValue::unknown_if(
224                    ignore,
225                    JsValue::WellKnownFunction(WellKnownFunctionKind::WorkerConstructor),
226                    true,
227                    rcstr!("ignored Worker constructor"),
228                ),
229                "define" => JsValue::WellKnownFunction(WellKnownFunctionKind::Define),
230                "URL" => JsValue::WellKnownFunction(WellKnownFunctionKind::URLConstructor),
231                "process" => JsValue::WellKnownObject(WellKnownObjectKind::NodeProcessModule),
232                "Object" => JsValue::WellKnownObject(WellKnownObjectKind::GlobalObject),
233                "Buffer" => JsValue::WellKnownObject(WellKnownObjectKind::NodeBuffer),
234                _ => v.into_unknown(true, rcstr!("unknown global")),
235            },
236            JsValue::Module(ref mv) => {
237                if let Some(wko) = module_value_to_well_known_object(mv) {
238                    wko
239                } else {
240                    return Ok((v, Modified::No));
241                }
242            }
243            _ => {
244                let (mut v, m1) = replace_well_known(arena, v, compile_time_info, true).await?;
245                let m2 = replace_builtin(arena.get_or_default(), &mut v);
246                let m = if m1.is_modified() || m2.is_modified() {
247                    Modified::Yes
248                } else {
249                    Modified::from(v.make_nested_operations_unknown())
250                };
251                return Ok((v, m));
252            }
253        };
254        new_value.normalize_shallow(arena.get_or_default());
255        Ok((new_value, Modified::Yes))
256    }
257}
258
259#[cfg(test)]
260mod tests {
261    use std::{mem::take, path::PathBuf, sync::Arc, time::Instant};
262
263    use bumpalo::boxed::Box as BumpBox;
264    use parking_lot::Mutex;
265    use rustc_hash::FxHashMap;
266    use swc_core::{
267        common::{
268            FilePathMapping, GLOBALS, Globals, Mark, SourceMap, comments::SingleThreadedComments,
269        },
270        ecma::{
271            ast::{EsVersion, Id},
272            parser::parse_file_as_program,
273            transforms::base::resolver,
274            visit::VisitMutWith,
275        },
276        testing::{NormalizedOutput, fixture},
277    };
278    use turbo_rcstr::{RcStr, rcstr};
279    use turbo_tasks::{ResolvedVc, TurboTasks, util::FormatDuration};
280    use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage};
281    use turbopack_core::{
282        compile_time_info::CompileTimeInfo,
283        environment::{Environment, ExecutionEnvironment, NodeJsEnvironment, NodeJsVersion},
284        target::{Arch, CompileTarget, Endianness, Libc, Platform},
285    };
286
287    use super::{
288        BumpVec, JsValue,
289        graph::{ConditionalKind, Effect, EffectArg, EvalContext, VarGraph, create_graph},
290        linker::link,
291    };
292    use crate::{
293        AnalyzeMode,
294        analyzer::{Bump, ThreadLocal, graph::AssignmentScopes, imports::ImportAttributes},
295    };
296
297    #[fixture("tests/analyzer/graph/**/input.js")]
298    fn fixture(input: PathBuf) {
299        let input = RcStr::from(input.to_str().unwrap());
300        let rt = tokio::runtime::Builder::new_multi_thread()
301            .worker_threads(2)
302            .enable_all()
303            .build()
304            .unwrap();
305        rt.block_on(async move {
306            let tt = TurboTasks::new(TurboTasksBackend::new(
307                BackendOptions::default(),
308                noop_backing_storage(),
309            ));
310            tt.run_once(async move {
311                fixture_op(input).read_strongly_consistent().await?;
312                anyhow::Ok(())
313            })
314            .await
315            .unwrap();
316        });
317    }
318
319    #[turbo_tasks::function(operation, root)]
320    async fn fixture_op(input: RcStr) -> anyhow::Result<()> {
321        let input = PathBuf::from(input.as_str());
322        let graph_snapshot_path = input.with_file_name("graph.snapshot");
323        let graph_explained_snapshot_path = input.with_file_name("graph-explained.snapshot");
324        let graph_effects_snapshot_path = input.with_file_name("graph-effects.snapshot");
325        let resolved_explained_snapshot_path = input.with_file_name("resolved-explained.snapshot");
326        let resolved_effects_snapshot_path = input.with_file_name("resolved-effects.snapshot");
327        let large_marker = input.with_file_name("large");
328
329        let cm: Arc<SourceMap> = Arc::new(SourceMap::new(FilePathMapping::empty()));
330        let globals = Arc::new(Globals::new());
331        let arena = ThreadLocal::new();
332
333        // Keep all non-`Send` SWC types (`SingleThreadedComments`, `Lrc<SourceFile>`)
334        // confined to this synchronous block so they don't have to cross an `.await`
335        // and break the `Send` bound on `tt.run_once`'s future.
336        let (eval_context, mut var_graph) = GLOBALS.set(&globals, || {
337            let fm = cm.load_file(&input).unwrap();
338            let comments = SingleThreadedComments::default();
339            let mut m = parse_file_as_program(
340                &fm,
341                Default::default(),
342                EsVersion::latest(),
343                Some(&comments),
344                &mut vec![],
345            )
346            .map_err(|err| anyhow::anyhow!("parse error: {err:?}"))?;
347
348            let unresolved_mark = Mark::new();
349            let top_level_mark = Mark::new();
350            m.visit_mut_with(&mut resolver(unresolved_mark, top_level_mark, false));
351
352            let eval_context = EvalContext::new(
353                Some(&m),
354                unresolved_mark,
355                top_level_mark,
356                Default::default(),
357                Some(&comments),
358            );
359
360            let var_graph = create_graph(
361                arena.get_or_default(),
362                &m,
363                &eval_context,
364                AnalyzeMode::CodeGenerationAndTracing,
365                true,
366            );
367            anyhow::Ok((eval_context, var_graph))
368        })?;
369        let var_cache = Default::default();
370
371        let mut named_values = var_graph
372            .values
373            .iter()
374            .map(|((id, ctx), value)| {
375                let unique = var_graph.values.keys().filter(|(i, _)| id == i).count() == 1;
376                let value = value.clone_in(arena.get_or_default());
377                if unique {
378                    (id.to_string(), ((id.clone(), *ctx), value))
379                } else {
380                    (format!("{id}{ctx:?}"), ((id.clone(), *ctx), value))
381                }
382            })
383            .collect::<Vec<_>>();
384        named_values.sort_by(|a, b| a.0.cmp(&b.0));
385
386        fn explain_all<'x, 'a: 'x>(
387            values: impl IntoIterator<Item = (&'x String, &'x JsValue<'a>, Option<AssignmentScopes>)>,
388        ) -> String {
389            values
390                .into_iter()
391                .map(|(id, value, assignment_scopes)| {
392                    let non_root_assignments = match assignment_scopes {
393                        Some(AssignmentScopes::AllInModuleEvalScope) => " (const after eval)",
394                        _ => "",
395                    };
396                    let (explainer, hints) = value.explain(10, 5);
397                    format!("{id}{non_root_assignments} = {explainer}{hints}")
398                })
399                .collect::<Vec<_>>()
400                .join("\n\n")
401        }
402
403        {
404            // Dump snapshot of graph
405
406            let large = large_marker.exists();
407
408            if !large {
409                NormalizedOutput::from(format!(
410                    "{:#?}",
411                    named_values
412                        .iter()
413                        .map(|(name, (_, value))| (name, value))
414                        .collect::<Vec<_>>()
415                ))
416                .compare_to_file(&graph_snapshot_path)
417                .unwrap();
418            }
419            NormalizedOutput::from(explain_all(named_values.iter().map(
420                |(name, (id, value))| {
421                    (
422                        name,
423                        value,
424                        eval_context.imports.assignment_scopes.get(id).copied(),
425                    )
426                },
427            )))
428            .compare_to_file(&graph_explained_snapshot_path)
429            .unwrap();
430            if !large {
431                NormalizedOutput::from(format!("{:#?}", var_graph.effects))
432                    .compare_to_file(&graph_effects_snapshot_path)
433                    .unwrap();
434            }
435        }
436
437        {
438            // Dump snapshot of resolved
439
440            let start = Instant::now();
441            let mut resolved = Vec::new();
442            for (name, id) in named_values.iter().map(|(name, (id, _))| (name, id)) {
443                let start = Instant::now();
444                // Ideally this would use eval_context.imports.get_attributes(span), but the
445                // span isn't available here
446                let (res, steps) = resolve(
447                    &arena,
448                    &var_graph,
449                    JsValue::Variable(id.clone()),
450                    ImportAttributes::empty_ref(),
451                    &var_cache,
452                )
453                .await;
454                let time = start.elapsed();
455                if time.as_millis() > 1 {
456                    println!(
457                        "linking {} {name} took {} in {} steps",
458                        input.display(),
459                        FormatDuration(time),
460                        steps
461                    );
462                }
463
464                resolved.push((name.clone(), res));
465            }
466            let time = start.elapsed();
467            if time.as_millis() > 1 {
468                println!("linking {} took {}", input.display(), FormatDuration(time));
469            }
470
471            let start = Instant::now();
472            let explainer = explain_all(resolved.iter().map(|(name, value)| (name, value, None)));
473            let time = start.elapsed();
474            if time.as_millis() > 1 {
475                println!(
476                    "explaining {} took {}",
477                    input.display(),
478                    FormatDuration(time)
479                );
480            }
481
482            NormalizedOutput::from(explainer)
483                .compare_to_file(&resolved_explained_snapshot_path)
484                .unwrap();
485        }
486
487        {
488            // Dump snapshot of resolved effects
489
490            let start = Instant::now();
491            let mut resolved = Vec::new();
492            let mut queue = take(&mut var_graph.effects)
493                .into_iter()
494                .map(|effect| (0, effect))
495                .rev()
496                .collect::<Vec<_>>();
497            let mut i = 0;
498            while let Some((parent, effect)) = queue.pop() {
499                i += 1;
500                let start = Instant::now();
501                async fn handle_args<'a>(
502                    arena: &'a ThreadLocal<Bump>,
503                    args: BumpVec<'a, EffectArg<'a>>,
504                    queue: &mut Vec<(usize, Effect<'a>)>,
505                    var_graph: &VarGraph<'a>,
506                    var_cache: &Mutex<FxHashMap<Id, JsValue<'a>>>,
507                    i: usize,
508                ) -> Vec<JsValue<'a>> {
509                    let mut new_args = Vec::with_capacity(args.len());
510                    for arg in args {
511                        match arg {
512                            EffectArg::Value(v) => {
513                                new_args.push(
514                                    resolve(
515                                        arena,
516                                        var_graph,
517                                        v,
518                                        ImportAttributes::empty_ref(),
519                                        var_cache,
520                                    )
521                                    .await
522                                    .0,
523                                );
524                            }
525                            EffectArg::Closure(v, effects) => {
526                                new_args.push(
527                                    resolve(
528                                        arena,
529                                        var_graph,
530                                        v,
531                                        ImportAttributes::empty_ref(),
532                                        var_cache,
533                                    )
534                                    .await
535                                    .0,
536                                );
537                                queue.extend(
538                                    BumpVec::from(BumpBox::into_inner(effects).effects)
539                                        .into_iter()
540                                        .rev()
541                                        .map(|e| (i, e)),
542                                );
543                            }
544                            EffectArg::Spread => {
545                                new_args.push(JsValue::unknown_empty(true, rcstr!("spread")));
546                            }
547                        }
548                    }
549                    new_args
550                }
551                let steps = match effect {
552                    Effect::Conditional {
553                        mut condition,
554                        kind,
555                        ..
556                    } => {
557                        let (condition, steps) = resolve(
558                            &arena,
559                            &var_graph,
560                            take(&mut *condition),
561                            ImportAttributes::empty_ref(),
562                            &var_cache,
563                        )
564                        .await;
565                        resolved.push((format!("{parent} -> {i} conditional"), condition));
566                        match BumpBox::into_inner(kind) {
567                            ConditionalKind::If { then } => {
568                                queue.extend(
569                                    BumpVec::from(then.effects)
570                                        .into_iter()
571                                        .rev()
572                                        .map(|e| (i, e)),
573                                );
574                            }
575                            ConditionalKind::Else { r#else } => {
576                                queue.extend(
577                                    BumpVec::from(r#else.effects)
578                                        .into_iter()
579                                        .rev()
580                                        .map(|e| (i, e)),
581                                );
582                            }
583                            ConditionalKind::IfElse { then, r#else }
584                            | ConditionalKind::Ternary { then, r#else } => {
585                                queue.extend(
586                                    BumpVec::from(r#else.effects)
587                                        .into_iter()
588                                        .rev()
589                                        .map(|e| (i, e)),
590                                );
591                                queue.extend(
592                                    BumpVec::from(then.effects)
593                                        .into_iter()
594                                        .rev()
595                                        .map(|e| (i, e)),
596                                );
597                            }
598                            ConditionalKind::IfElseMultiple { then, r#else } => {
599                                for then in BumpVec::from(then) {
600                                    queue.extend(
601                                        BumpVec::from(then.effects)
602                                            .into_iter()
603                                            .rev()
604                                            .map(|e| (i, e)),
605                                    );
606                                }
607                                for r#else in BumpVec::from(r#else) {
608                                    queue.extend(
609                                        BumpVec::from(r#else.effects)
610                                            .into_iter()
611                                            .rev()
612                                            .map(|e| (i, e)),
613                                    );
614                                }
615                            }
616                            ConditionalKind::And { expr }
617                            | ConditionalKind::Or { expr }
618                            | ConditionalKind::NullishCoalescing { expr }
619                            | ConditionalKind::Labeled { body: expr } => {
620                                queue.extend(
621                                    BumpVec::from(expr.effects)
622                                        .into_iter()
623                                        .rev()
624                                        .map(|e| (i, e)),
625                                );
626                            }
627                        };
628                        steps
629                    }
630                    Effect::Call {
631                        mut func,
632                        args,
633                        new,
634                        span,
635                        ..
636                    } => {
637                        let (func, steps) = resolve(
638                            &arena,
639                            &var_graph,
640                            take(&mut *func),
641                            eval_context.imports.get_attributes(span),
642                            &var_cache,
643                        )
644                        .await;
645                        let new_args =
646                            handle_args(&arena, args, &mut queue, &var_graph, &var_cache, i).await;
647                        resolved.push((
648                            format!("{parent} -> {i} call"),
649                            if new {
650                                JsValue::new_from_iter(arena.get_or_default(), func, new_args)
651                            } else {
652                                JsValue::call_from_iter(arena.get_or_default(), func, new_args)
653                            },
654                        ));
655                        steps
656                    }
657                    Effect::FreeVar { var, .. } => {
658                        resolved.push((format!("{parent} -> {i} free var"), JsValue::FreeVar(var)));
659                        0
660                    }
661                    Effect::TypeOf { mut arg, .. } => {
662                        let (arg, steps) = resolve(
663                            &arena,
664                            &var_graph,
665                            take(&mut *arg),
666                            ImportAttributes::empty_ref(),
667                            &var_cache,
668                        )
669                        .await;
670                        resolved.push((
671                            format!("{parent} -> {i} typeof"),
672                            JsValue::type_of(arena.get_or_default(), arg),
673                        ));
674                        steps
675                    }
676                    Effect::MemberCall {
677                        mut obj,
678                        mut prop,
679                        args,
680                        ..
681                    } => {
682                        let (obj, obj_steps) = resolve(
683                            &arena,
684                            &var_graph,
685                            take(&mut *obj),
686                            ImportAttributes::empty_ref(),
687                            &var_cache,
688                        )
689                        .await;
690                        let (prop, prop_steps) = resolve(
691                            &arena,
692                            &var_graph,
693                            take(&mut *prop),
694                            ImportAttributes::empty_ref(),
695                            &var_cache,
696                        )
697                        .await;
698                        let new_args =
699                            handle_args(&arena, args, &mut queue, &var_graph, &var_cache, i).await;
700                        resolved.push((
701                            format!("{parent} -> {i} member call"),
702                            JsValue::member_call_from_iter(
703                                arena.get_or_default(),
704                                obj,
705                                prop,
706                                new_args,
707                            ),
708                        ));
709                        obj_steps + prop_steps
710                    }
711                    Effect::DynamicImport { args, .. } => {
712                        let new_args =
713                            handle_args(&arena, args, &mut queue, &var_graph, &var_cache, i).await;
714                        resolved.push((
715                            format!("{parent} -> {i} dynamic import"),
716                            JsValue::call_from_iter(
717                                arena.get_or_default(),
718                                JsValue::FreeVar("import".into()),
719                                new_args,
720                            ),
721                        ));
722                        0
723                    }
724                    Effect::Unreachable { .. } => {
725                        resolved.push((
726                            format!("{parent} -> {i} unreachable"),
727                            JsValue::unknown_empty(true, rcstr!("unreachable")),
728                        ));
729                        0
730                    }
731                    Effect::ImportMeta { .. }
732                    | Effect::ImportedBinding { .. }
733                    | Effect::Member { .. }
734                    | Effect::In { .. } => 0,
735                };
736                let time = start.elapsed();
737                if time.as_millis() > 1 {
738                    println!(
739                        "linking effect {} took {} in {} steps",
740                        input.display(),
741                        FormatDuration(time),
742                        steps
743                    );
744                }
745            }
746            let time = start.elapsed();
747            if time.as_millis() > 1 {
748                println!(
749                    "linking effects {} took {}",
750                    input.display(),
751                    FormatDuration(time)
752                );
753            }
754
755            let start = Instant::now();
756            let explainer = explain_all(resolved.iter().map(|(name, value)| (name, value, None)));
757            let time = start.elapsed();
758            if time.as_millis() > 1 {
759                println!(
760                    "explaining effects {} took {}",
761                    input.display(),
762                    FormatDuration(time)
763                );
764            }
765
766            NormalizedOutput::from(explainer)
767                .compare_to_file(&resolved_effects_snapshot_path)
768                .unwrap();
769        }
770
771        Ok(())
772    }
773
774    async fn resolve<'a>(
775        arena: &'a ThreadLocal<Bump>,
776        var_graph: &VarGraph<'a>,
777        val: JsValue<'a>,
778        attributes: &ImportAttributes,
779        var_cache: &Mutex<FxHashMap<Id, JsValue<'a>>>,
780    ) -> (JsValue<'a>, u32) {
781        // The caller (`fixture`) runs us inside `tt.run_once`, so a real
782        // turbo-tasks task context is already established here.
783        async {
784            let compile_time_info = CompileTimeInfo::builder(
785                Environment::new(ExecutionEnvironment::NodeJsLambda(
786                    NodeJsEnvironment {
787                        compile_target: CompileTarget {
788                            arch: Arch::X64,
789                            platform: Platform::Linux,
790                            endianness: Endianness::Little,
791                            libc: Libc::Glibc,
792                        }
793                        .resolved_cell(),
794                        node_version: NodeJsVersion::default().resolved_cell(),
795                        cwd: ResolvedVc::cell(None),
796                    }
797                    .resolved_cell(),
798                ))
799                .to_resolved()
800                .await?,
801            )
802            .cell()
803            .await?;
804            link(
805                arena,
806                var_graph,
807                val,
808                &(|val| Box::pin(super::test_utils::early_visitor(arena, val))),
809                &(|val| {
810                    Box::pin(super::test_utils::visitor(
811                        arena,
812                        val,
813                        compile_time_info,
814                        attributes,
815                    ))
816                }),
817                &Default::default(),
818                var_cache,
819            )
820            .await
821        }
822        .await
823        .unwrap()
824    }
825}