turbopack_nft/
nft.rs

1use std::{collections::HashSet, env::current_dir, path::PathBuf};
2
3use anyhow::Result;
4use turbo_rcstr::{RcStr, rcstr};
5use turbo_tasks::{ResolvedVc, TransientInstance, TryJoinIterExt, ValueToString, Vc};
6use turbo_tasks_fs::{DiskFileSystem, FileSystem};
7use turbopack::{
8    ModuleAssetContext,
9    module_options::{
10        CssOptionsContext, EcmascriptOptionsContext, ModuleOptionsContext,
11        TypescriptTransformOptions,
12    },
13};
14use turbopack_cli_utils::issue::{ConsoleUi, LogOptions};
15use turbopack_core::{
16    compile_time_info::CompileTimeInfo,
17    context::AssetContext,
18    environment::{Environment, ExecutionEnvironment, NodeJsEnvironment},
19    file_source::FileSource,
20    ident::Layer,
21    issue::{IssueReporter, IssueSeverity, handle_issues},
22    output::{OutputAsset, OutputAssetsReference},
23    reference::all_assets_from_entries,
24    reference_type::ReferenceType,
25    traced_asset::TracedAsset,
26};
27use turbopack_ecmascript::AnalyzeMode;
28use turbopack_resolve::resolve_options_context::ResolveOptionsContext;
29
30pub async fn node_file_trace(
31    project_root: RcStr,
32    input: RcStr,
33    graph: bool,
34    show_issues: bool,
35    max_depth: Option<usize>,
36) -> Result<()> {
37    let op = node_file_trace_operation(project_root.clone(), input.clone(), graph, max_depth);
38    let result = op.resolve_strongly_consistent().await?;
39
40    if show_issues {
41        let issue_reporter: Vc<Box<dyn IssueReporter>> =
42            Vc::upcast(ConsoleUi::new(TransientInstance::new(LogOptions {
43                project_dir: PathBuf::from(project_root),
44                current_dir: current_dir().unwrap(),
45                show_all: true,
46                log_detail: false,
47                log_level: IssueSeverity::Hint,
48            })));
49
50        handle_issues(op, issue_reporter, IssueSeverity::Error, None, None).await?;
51    }
52
53    println!("FILELIST:");
54    for a in result.await? {
55        println!("{a}");
56    }
57
58    Ok(())
59}
60
61#[turbo_tasks::function(operation)]
62async fn node_file_trace_operation(
63    project_root: RcStr,
64    input: RcStr,
65    graph: bool,
66    max_depth: Option<usize>,
67) -> Result<Vc<Vec<RcStr>>> {
68    let workspace_fs: Vc<Box<dyn FileSystem>> = Vc::upcast(DiskFileSystem::new(
69        rcstr!("workspace"),
70        project_root.clone(),
71    ));
72    let input_dir = workspace_fs.root().owned().await?;
73    let input = input_dir.join(&format!("{input}"))?;
74
75    let source = FileSource::new(input);
76    let environment = Environment::new(ExecutionEnvironment::NodeJsLambda(
77        NodeJsEnvironment::default().resolved_cell(),
78    ));
79    let module_asset_context = ModuleAssetContext::new_without_replace_externals(
80        Default::default(),
81        // This config should be kept in sync with
82        // turbopack/crates/turbopack-tracing/tests/node-file-trace.rs and
83        // turbopack/crates/turbopack-tracing/tests/unit.rs and
84        // turbopack/crates/turbopack/src/lib.rs and
85        // turbopack/crates/turbopack-nft/src/nft.rs
86        CompileTimeInfo::new(environment),
87        ModuleOptionsContext {
88            ecmascript: EcmascriptOptionsContext {
89                enable_typescript_transform: Some(
90                    TypescriptTransformOptions::default().resolved_cell(),
91                ),
92                ..Default::default()
93            },
94            css: CssOptionsContext {
95                enable_raw_css: true,
96                ..Default::default()
97            },
98            // Environment is not passed in order to avoid downleveling JS / CSS for
99            // node-file-trace.
100            environment: None,
101            analyze_mode: AnalyzeMode::Tracing,
102            // Disable tree shaking. Even side-effect-free imports need to be traced, as they will
103            // execute at runtime.
104            tree_shaking_mode: None,
105            ..Default::default()
106        }
107        .cell(),
108        ResolveOptionsContext {
109            enable_node_native_modules: true,
110            enable_node_modules: Some(input_dir),
111            custom_conditions: vec![rcstr!("node")],
112            enable_node_externals: true,
113            loose_errors: true,
114            collect_affecting_sources: true,
115            ..Default::default()
116        }
117        .cell(),
118        Layer::new(rcstr!("externals-tracing")),
119    );
120    let module = module_asset_context
121        .process(Vc::upcast(source), ReferenceType::Undefined)
122        .module();
123
124    let asset = TracedAsset::new(module).to_resolved().await?;
125
126    Ok(Vc::cell(if graph {
127        to_graph(ResolvedVc::upcast(asset), max_depth.unwrap_or(usize::MAX)).await?
128    } else {
129        to_list(ResolvedVc::upcast(asset)).await?
130    }))
131}
132
133async fn to_list(asset: ResolvedVc<Box<dyn OutputAsset>>) -> Result<Vec<RcStr>> {
134    let mut assets = all_assets_from_entries(Vc::cell(vec![asset]))
135        .await?
136        .iter()
137        .map(async |a| Ok(a.path().await?.path.clone()))
138        .try_join()
139        .await?;
140    assets.sort();
141    assets.dedup();
142
143    Ok(assets)
144}
145
146async fn to_graph(asset: ResolvedVc<Box<dyn OutputAsset>>, max_depth: usize) -> Result<Vec<RcStr>> {
147    let mut visited = HashSet::new();
148    let mut queue = Vec::new();
149    queue.push((0, asset));
150
151    let mut result = vec![];
152    while let Some((depth, asset)) = queue.pop() {
153        let references = asset.references().all_assets().await?;
154        let mut indent = String::new();
155        for _ in 0..depth {
156            indent.push_str("  ");
157        }
158        if visited.insert(asset) {
159            if depth < max_depth {
160                for &asset in references.iter().rev() {
161                    queue.push((depth + 1, asset));
162                }
163            }
164            result.push(format!("{}{}", indent, asset.path().to_string().await?).into());
165        } else if references.is_empty() {
166            result.push(format!("{}{} *", indent, asset.path().to_string().await?).into());
167        } else {
168            result.push(format!("{}{} *...", indent, asset.path().to_string().await?).into());
169        }
170    }
171    result.push("".into());
172    result.push("*    : revisited and no references".into());
173    result.push("*... : revisited and references were already printed".into());
174    Ok(result)
175}