Skip to main content

turbopack_nft/
nft.rs

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