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