Skip to main content

next_api/
project.rs

1use std::{
2    iter,
3    path::{Path, PathBuf},
4    sync::Arc,
5    time::{Duration, Instant, SystemTime},
6};
7
8use anyhow::{Context, Result, bail};
9use async_trait::async_trait;
10use bincode::{Decode, Encode};
11use indexmap::map::Entry;
12use next_core::{
13    app_structure::find_app_dir,
14    emit_assets, get_edge_chunking_context, get_edge_chunking_context_with_client_assets,
15    get_edge_compile_time_info, get_edge_resolve_options_context,
16    instrumentation::instrumentation_files,
17    middleware::middleware_files,
18    mode::NextMode,
19    next_app::{AppPage, AppPath},
20    next_client::{
21        ClientChunkingContextOptions, ClientContextType, ServiceWorkerChunkingContextOptions,
22        get_client_chunking_context, get_client_compile_time_info,
23        get_client_module_options_context, get_client_resolve_options_context,
24        get_service_worker_chunking_context,
25    },
26    next_config::{
27        DIST_PROFILES_DIR_NAME, ModuleIds as ModuleIdStrategyConfig, NextConfig, OutputType,
28        TurbopackPluginRuntimeStrategy,
29    },
30    next_edge::context::EdgeChunkingContextOptions,
31    next_server::{
32        ServerChunkingContextOptions, ServerContextType, get_server_chunking_context,
33        get_server_chunking_context_with_client_assets, get_server_compile_time_info,
34        get_server_module_options_context, get_server_resolve_options_context,
35        get_tracing_compile_time_info,
36    },
37    next_telemetry::ProjectFeatureUsageSummary,
38    parse_segment_config_from_source,
39    segment_config::ParseSegmentMode,
40    util::{NextRuntime, OptionEnvMap},
41};
42use rustc_hash::{FxHashMap, FxHashSet};
43use serde::{Deserialize, Serialize};
44use tracing::{Instrument, field::Empty};
45use turbo_rcstr::{RcStr, rcstr};
46use turbo_tasks::{
47    Completion, Completions, FxIndexMap, InvalidationReason, NonLocalValue, OperationValue,
48    OperationVc, ReadRef, ResolvedVc, State, TransientInstance, TryFlatJoinIterExt, TryJoinIterExt,
49    Vc, debug::ValueDebugFormat, fxindexmap, message_queue::TraceEvent, turbo_tasks,
50};
51use turbo_tasks_env::{EnvMap, ProcessEnv};
52use turbo_tasks_fs::{
53    DiskFileSystem, DiskFileSystemMap, DiskWatcherConfig, FileContent, FileSystem, FileSystemPath,
54    VirtualFileSystem, invalidation,
55};
56use turbo_unix_path::join_path;
57use turbopack::{
58    ModuleAssetContext, evaluate_context::node_build_environment, externals_tracing_module_context,
59    global_module_ids::get_global_module_id_strategy, transition::TransitionOptions,
60};
61use turbopack_core::{
62    PROJECT_FILESYSTEM_NAME,
63    changed::content_changed,
64    chunk::{
65        ChunkingContext, EvaluatableAssets, UnusedReferences,
66        chunk_id_strategy::{ModuleIdFallback, ModuleIdStrategy},
67    },
68    compile_time_info::CompileTimeInfo,
69    context::AssetContext,
70    environment::NodeJsVersion,
71    file_source::FileSource,
72    ident::Layer,
73    issue::{
74        CollectibleIssuesExt, Issue, IssueExt, IssueFilter, IssueSeverity, IssueStage, PlainIssue,
75        StyledString,
76    },
77    module::{Module, Modules},
78    module_graph::{
79        GraphEntries, ModuleGraph, SingleModuleGraph, VisitedModules,
80        binding_usage_info::{
81            BindingUsageInfo, OptionBindingUsageInfo, compute_binding_usage_info,
82        },
83        chunk_group_info::{ChunkGroupEntry, EntryHeuristics},
84    },
85    output::{
86        ExpandOutputAssetsInput, ExpandedOutputAssets, OutputAsset, OutputAssets,
87        expand_output_assets,
88    },
89    reference::all_assets_from_entries,
90    reference_type::{CommonJsReferenceSubType, ReferenceType},
91    resolve::{FindContextFileResult, find_context_file},
92    version::{
93        NotFoundVersion, OptionVersionedContent, Update, Version, VersionState, VersionedContent,
94    },
95};
96use turbopack_ecmascript::async_chunk::proxy::{
97    activation_key_from_chunk_path, lazy_compilation_state,
98};
99#[cfg(all(feature = "process_pool", not(target_family = "wasm")))]
100use turbopack_node::child_process_backend;
101use turbopack_node::execution_context::ExecutionContext;
102#[cfg(feature = "worker_pool")]
103use turbopack_node::worker_threads_backend;
104use turbopack_nodejs::{NodeJsChunkingContext, fs::NodeModulesPathMatcher};
105
106pub use crate::additional_roots::AdditionalRootConfig;
107use crate::{
108    additional_roots::{AdditionalDiskFileSystem, create_additional_root_file_systems},
109    aggregate_hmr::ServerHmrChunkLists,
110    app::{AppProject, OptionAppProject},
111    empty::EmptyEndpoint,
112    entrypoints::Entrypoints,
113    instrumentation::InstrumentationEndpoint,
114    middleware::MiddlewareEndpoint,
115    next_server_nft::{pages_renderer_modules, require_hook_modules},
116    pages::PagesProject,
117    path_utils::convention_file_base_name,
118    route::{
119        Endpoint, EndpointGroup, EndpointGroupEntry, EndpointGroupKey, EndpointGroups, Endpoints,
120        Route,
121    },
122    versioned_content_map::VersionedContentMap,
123};
124
125#[turbo_tasks::task_input]
126#[derive(
127    Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash, OperationValue, Encode, Decode,
128)]
129#[serde(rename_all = "camelCase")]
130pub struct DraftModeOptions {
131    pub preview_mode_id: RcStr,
132    pub preview_mode_encryption_key: RcStr,
133    pub preview_mode_signing_key: RcStr,
134}
135
136/// A cell holding [`DraftModeOptions`], some of these change in every session.
137#[turbo_tasks::value(transparent)]
138struct DraftModeOptionsValue(DraftModeOptions);
139
140#[turbo_tasks::task_input]
141#[derive(
142    Debug,
143    Default,
144    Serialize,
145    Deserialize,
146    Copy,
147    Clone,
148    PartialEq,
149    Eq,
150    Hash,
151    OperationValue,
152    Encode,
153    Decode,
154)]
155#[serde(rename_all = "camelCase")]
156pub struct WatchOptions {
157    /// Whether to watch the filesystem for file changes.
158    pub enable: bool,
159
160    /// Enable polling at a certain interval if the native file watching doesn't work (e.g.
161    /// docker).
162    pub poll_interval: Option<Duration>,
163}
164
165#[turbo_tasks::task_input]
166#[derive(
167    Debug,
168    Default,
169    Serialize,
170    Deserialize,
171    Clone,
172    PartialEq,
173    Eq,
174    Hash,
175    OperationValue,
176    Encode,
177    Decode,
178)]
179#[serde(rename_all = "camelCase")]
180pub struct DebugBuildPaths {
181    pub app: Vec<RcStr>,
182    pub pages: Vec<RcStr>,
183}
184
185/// Pre-converted route keys from debug build paths for O(1) lookups.
186struct DebugBuildPathsRouteKeys {
187    app: FxHashSet<RcStr>,
188    pages: FxHashSet<RcStr>,
189}
190
191impl DebugBuildPathsRouteKeys {
192    fn app_route_key_from_debug_path(path: &str) -> Result<RcStr> {
193        let mut segments = path
194            .trim_start_matches('/')
195            .split('/')
196            .filter(|segment| !segment.is_empty())
197            .collect::<Vec<_>>();
198
199        if let Some(last_segment) = segments.last()
200            && (*last_segment == "page"
201                || last_segment.starts_with("page.")
202                || *last_segment == "route"
203                || last_segment.starts_with("route."))
204        {
205            segments.pop();
206        }
207
208        let normalized_path = segments.join("/");
209        Ok(AppPath::from(AppPage::parse(&normalized_path)?)
210            .to_string()
211            .into())
212    }
213
214    fn pages_route_key_from_debug_path(path: &RcStr) -> Result<RcStr> {
215        // Strip extension: "/foo.tsx" -> "/foo"
216        // Catch-all routes like "/foo/[...slug]" contain dots in the segment name;
217        // only treat the suffix as an extension when it is a plain alphanumeric token.
218        let file_name = path.rsplit('/').next().unwrap_or(path);
219        let result = if let Some(dot_idx) = file_name.rfind('.') {
220            let ext = &file_name[dot_idx + 1..];
221            if !ext.is_empty() && ext.chars().all(|c| c.is_ascii_alphanumeric()) {
222                let trimmed_len = path.len() - (file_name.len() - dot_idx);
223                path[..trimmed_len].into()
224            } else {
225                path.clone()
226            }
227        } else {
228            path.clone()
229        };
230
231        // Strip index suffix: "/foo/index.tsx" -> "/foo"
232        Ok(if let Some(stripped) = result.strip_suffix("/index") {
233            if stripped.is_empty() {
234                "/".into()
235            } else {
236                stripped.into()
237            }
238        } else {
239            result
240        })
241    }
242
243    fn from_debug_build_paths(paths: &DebugBuildPaths) -> Result<Self> {
244        Ok(Self {
245            app: paths
246                .app
247                .iter()
248                .map(|path| Self::app_route_key_from_debug_path(path))
249                .collect::<Result<_>>()?,
250            pages: paths
251                .pages
252                .iter()
253                .map(Self::pages_route_key_from_debug_path)
254                .collect::<Result<_>>()?,
255        })
256    }
257
258    fn should_include_app_route(&self, route_key: &RcStr) -> bool {
259        // Special app router framework routes
260        if matches!(route_key.as_str(), "/_not-found" | "/_global-error") {
261            return true;
262        }
263        self.app.contains(route_key)
264    }
265
266    fn should_include_pages_route(&self, route_key: &RcStr) -> bool {
267        // Special pages router framework routes
268        if matches!(
269            route_key.as_str(),
270            "/_error" | "/_document" | "/_app" | "/404" | "/500"
271        ) {
272            return self.pages.iter().any(|page| {
273                let page = page.as_str();
274                page != "/api" && !page.starts_with("/api/")
275            });
276        }
277        self.pages.contains(route_key)
278    }
279}
280
281#[derive(
282    Debug,
283    Serialize,
284    Deserialize,
285    Clone,
286    PartialEq,
287    Eq,
288    NonLocalValue,
289    OperationValue,
290    Encode,
291    Decode,
292)]
293#[serde(rename_all = "camelCase")]
294pub struct ProjectOptions {
295    /// An [canonicalized][std::fs::canonicalize] root path (Unix or Windows path) from which all
296    /// files must be nested under. Trying to access a file outside this root will fail, so think
297    /// of this as a weak chroot. E.g. `/home/user/projects/my-repo`.
298    ///
299    /// This serves two purposes:
300    /// - It gives us a root to configure the file system watcher with.
301    /// - It ensures the cache is portable when the root path is moved, since every other path is
302    ///   relative to it.
303    ///
304    /// Symlinks outside of the `root_path` can still be resolved if the targets exist in
305    /// [`ProjectOptions::additional_roots`].
306    pub root_path: RcStr,
307
308    /// A path which contains the app/pages directories, relative to [`Project::project_path`].
309    /// Always a Unix-style (`/`-separated) path. E.g. `apps/my-app`.
310    pub project_path: RcStr,
311
312    /// The contents of next.config.js, serialized to JSON.
313    pub next_config: RcStr,
314
315    /// Additional filesystem roots. Canonicalized during initialization.
316    pub additional_roots: Vec<AdditionalRootConfig>,
317
318    /// A map of environment variables to use when compiling code.
319    pub env: Vec<(RcStr, RcStr)>,
320
321    /// A map of environment variables which should get injected at compile time.
322    pub define_env: DefineEnv,
323
324    /// Filesystem watcher options.
325    pub watch: WatchOptions,
326
327    /// The mode in which Next.js is running.
328    pub dev: bool,
329
330    /// The server actions encryption key.
331    pub encryption_key: RcStr,
332
333    /// The build id.
334    pub build_id: RcStr,
335
336    /// Options for draft mode.
337    pub preview_props: DraftModeOptions,
338
339    /// The browserslist query to use for targeting browsers.
340    pub browserslist_query: RcStr,
341
342    /// When the code is minified, this opts out of the default mangling of local names for
343    /// variables, functions etc., which can be useful for debugging/profiling purposes.
344    pub no_mangling: bool,
345
346    /// Whether to write the route hashes manifest.
347    pub write_routes_hashes_manifest: bool,
348
349    /// The version of Node.js that is available/currently running.
350    pub current_node_js_version: RcStr,
351
352    /// Debug build paths for selective builds. When set, only routes matching these paths will be
353    /// included in the build.
354    pub debug_build_paths: Option<DebugBuildPaths>,
355
356    /// App-router page routes that should be built after non-deferred routes.
357    pub deferred_entries: Option<Vec<RcStr>>,
358
359    /// Whether to enable persistent caching
360    pub is_persistent_caching_enabled: bool,
361
362    /// The version of Next.js that is running.
363    pub next_version: RcStr,
364
365    /// Whether server-side HMR is enabled (disabled with `--no-server-fast-refresh`).
366    pub server_hmr: bool,
367}
368
369/// The subset of [`ProjectOptions`] that may change without restarting the process. Used by
370/// [`ProjectContainer::update`].
371///
372/// Refer to [`ProjectOptions`] for documentation on this struct's fields.
373#[derive(Default)]
374pub struct PartialProjectOptions {
375    pub next_config: Option<RcStr>,
376
377    pub env: Option<Vec<(RcStr, RcStr)>>,
378
379    pub define_env: Option<DefineEnv>,
380
381    pub dev: Option<bool>,
382
383    pub encryption_key: Option<RcStr>,
384
385    pub build_id: Option<RcStr>,
386
387    pub preview_props: Option<DraftModeOptions>,
388
389    pub browserslist_query: Option<RcStr>,
390
391    pub no_mangling: Option<bool>,
392
393    pub write_routes_hashes_manifest: Option<bool>,
394
395    pub debug_build_paths: Option<DebugBuildPaths>,
396}
397
398#[turbo_tasks::task_input]
399#[derive(
400    Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash, OperationValue, Encode, Decode,
401)]
402#[serde(rename_all = "camelCase")]
403pub struct DefineEnv {
404    pub client: Vec<(RcStr, Option<RcStr>)>,
405    pub edge: Vec<(RcStr, Option<RcStr>)>,
406    pub nodejs: Vec<(RcStr, Option<RcStr>)>,
407}
408
409#[derive(PartialEq, Eq, ValueDebugFormat, NonLocalValue, Encode, Decode)]
410pub struct Middleware {
411    pub endpoint: ResolvedVc<Box<dyn Endpoint>>,
412    pub is_proxy: bool,
413}
414
415#[derive(PartialEq, Eq, ValueDebugFormat, NonLocalValue, Encode, Decode)]
416pub struct Instrumentation {
417    pub node_js: ResolvedVc<Box<dyn Endpoint>>,
418    pub edge: ResolvedVc<Box<dyn Endpoint>>,
419}
420
421#[derive(Clone, Debug, PartialEq, Eq, NonLocalValue, OperationValue, Encode, Decode)]
422struct ProjectFileSystemState {
423    project_file_system: OperationVc<DiskFileSystem>,
424    output_file_system: OperationVc<DiskFileSystem>,
425}
426
427#[turbo_tasks::value(evict = "never", eq = "manual", cell = "new")]
428pub struct ProjectContainer {
429    name: RcStr,
430    options_state: State<Option<ProjectOptions>>,
431    file_systems_state: State<Option<ProjectFileSystemState>>,
432    additional_roots_state: State<Vec<(RcStr, AdditionalDiskFileSystem)>>,
433    #[turbo_tasks(debug_ignore, unsafe_ignore)]
434    #[bincode(skip)]
435    fs_map_init_lock: tokio::sync::Mutex<()>,
436    versioned_content_map: Option<ResolvedVc<VersionedContentMap>>,
437}
438
439#[turbo_tasks::value_impl]
440impl ProjectContainer {
441    #[turbo_tasks::function(operation, root)]
442    pub fn new_operation(name: RcStr, dev: bool) -> Result<Vc<Self>> {
443        Ok(ProjectContainer {
444            name,
445            // we only need to enable versioning in dev mode, since build
446            // is assumed to be operating over a static snapshot
447            versioned_content_map: if dev {
448                Some(VersionedContentMap::new())
449            } else {
450                None
451            },
452            options_state: State::new(None),
453            file_systems_state: State::new(None),
454            additional_roots_state: State::new(Vec::new()),
455            fs_map_init_lock: tokio::sync::Mutex::new(()),
456        }
457        .cell())
458    }
459}
460
461/// Constructs and activates the initial project container state, including its filesystem
462/// watchers. Called by [`ProjectContainer::initialize`].
463async fn prepare_project_container_state(
464    container_vc: ResolvedVc<ProjectContainer>,
465    options: ProjectOptions,
466) -> Result<Vec<ReadRef<PlainIssue>>> {
467    let container = container_vc.await?;
468    // Operations created during initialization may begin running immediately. Keep operations that
469    // require the complete filesystem map blocked until both filesystem states are populated.
470    let fs_map_init_guard = container.fs_map_init_lock.lock().await;
471
472    let map = disk_file_system_map_operation(container_vc);
473    let config_json: serde_json::Value = serde_json::from_str(&options.next_config)?;
474
475    let dist_dir_root = config_json
476        .get("distDirRoot")
477        .and_then(|value| value.as_str())
478        .unwrap_or(".next");
479
480    let watcher_config = DiskWatcherConfig {
481        poll_interval: options.watch.poll_interval,
482        report_invalidation_reason: true,
483        ..Default::default()
484    };
485
486    let denied_paths = vec![
487        RcStr::from(
488            join_path(&options.project_path, dist_dir_root)
489                .context("distDirRoot must stay inside the project root")?,
490        ),
491        // CPU profiles are written to `.next-profiles/` at the project root (see `--cpu-prof`).
492        // Deny access to it so the bundler doesn't traverse into the profiling output directory.
493        RcStr::from(join_path(&options.project_path, DIST_PROFILES_DIR_NAME).unwrap()),
494    ];
495
496    let enable_watch = options.watch.enable;
497    let configured_additional_roots = options.additional_roots.clone();
498    let project_root = options.root_path.clone();
499    let project_path = options.project_path.clone();
500
501    // `project_root_path_operation` reads `options_state`, so publish it first. This operation
502    // cannot depend on `fs_map_init_lock` because we must eagerly resolve `project_fs_op` to
503    // create `config_path`.
504    container.options_state.set(Some(options));
505
506    // Wrap `options.root_path` in an `OperationVc`
507    // Note: It's important that the identity of this operation is stable, so that we don't end up
508    // changing the identity of every `FileSystemPath` that depends on its output cell.
509    let root_path_op = project_root_path_operation(container_vc);
510
511    let project_fs_op = disk_file_system_operation(
512        PROJECT_FILESYSTEM_NAME,
513        root_path_op,
514        denied_paths,
515        watcher_config,
516        map,
517    );
518
519    let output_fs_op = disk_file_system_operation(
520        rcstr!("output"),
521        root_path_op,
522        Vec::new(),
523        DiskWatcherConfig::default(),
524        DiskFileSystemMap::empty(),
525    );
526
527    // The filesystem only stores (and does not resolve) the filesystem map.
528    container
529        .file_systems_state
530        .set(Some(ProjectFileSystemState {
531            project_file_system: project_fs_op,
532            output_file_system: output_fs_op,
533        }));
534    let project_fs_vc = project_fs_op.resolve().strongly_consistent().await?;
535    let project_fs = project_fs_op.read_strongly_consistent().await?;
536
537    let config_file_name = config_json
538        .get("configFileName")
539        .and_then(|value| value.as_str())
540        .unwrap_or("next.config.js");
541    let config_path = FileSystemPath::new_normalized_unchecked(
542        ResolvedVc::upcast(project_fs_vc),
543        RcStr::default(),
544    )
545    .join(&project_path)?
546    .join(config_file_name)?;
547    let additional_roots = create_additional_root_file_systems(
548        container_vc,
549        configured_additional_roots,
550        &project_root,
551        watcher_config,
552        map,
553        config_path,
554    )
555    .await?;
556
557    let additional_file_systems = additional_roots
558        .roots_by_name
559        .values()
560        .map(|root| root.file_system)
561        .collect::<Vec<_>>();
562
563    // This state must be populated before the lazy additional filesystem operations or the
564    // filesystem map are first resolved.
565    container
566        .additional_roots_state
567        .set(additional_roots.roots_by_name.into_iter().collect());
568    drop(fs_map_init_guard);
569
570    // perform complete invalidations of all paths and watcher setup after finalizing the `map`
571    fn invalidation_reason(path: &Path) -> impl InvalidationReason + Clone + use<> {
572        invalidation::Initialize {
573            path: RcStr::from(path.to_string_lossy()),
574        }
575    }
576    if enable_watch {
577        project_fs.start_watching().await?;
578        for op in &additional_file_systems {
579            let fs = op.read_strongly_consistent().await?;
580            fs.start_watching().await?;
581        }
582    } else {
583        project_fs.invalidate_with_reason(invalidation_reason);
584        for op in &additional_file_systems {
585            op.read_strongly_consistent()
586                .await?
587                .invalidate_with_reason(invalidation_reason);
588        }
589    }
590
591    // we never watch `output_file_system`, but we do invalidate it across restarts, in case some
592    // other process modified or deleted files.
593    output_fs_op
594        .read_strongly_consistent()
595        .await?
596        .invalidate_with_reason(invalidation_reason);
597
598    Ok(additional_roots.issues)
599}
600
601/// Activates the lazy dynamic import that `chunk_path` names, returning whether it named one. The
602/// caller has to rebuild the owning entrypoint before serving the request.
603#[turbo_tasks::function(operation, root)]
604pub async fn activate_lazy_chunk_operation(chunk_path: RcStr) -> Result<Vc<bool>> {
605    let Some(key) = activation_key_from_chunk_path(&chunk_path) else {
606        return Ok(Vc::cell(false));
607    };
608    lazy_compilation_state(key).await?.activate();
609    Ok(Vc::cell(true))
610}
611
612#[turbo_tasks::function(operation, root)]
613pub(crate) fn disk_file_system_operation(
614    name: RcStr,
615    canonical_root: OperationVc<RcStr>,
616    denied_paths: Vec<RcStr>,
617    mut watcher_config: DiskWatcherConfig,
618    map: OperationVc<DiskFileSystemMap>,
619) -> Vc<DiskFileSystem> {
620    watcher_config.extended_batch_delay_matcher =
621        Some(ResolvedVc::upcast(NodeModulesPathMatcher.resolved_cell()));
622    DiskFileSystem::new_with_options(
623        name,
624        canonical_root.connect(),
625        denied_paths,
626        watcher_config,
627        map,
628    )
629}
630
631#[turbo_tasks::function(operation, root)]
632async fn project_root_path_operation(container: ResolvedVc<ProjectContainer>) -> Result<Vc<RcStr>> {
633    let container = container.await?;
634    let root_path = container
635        .options_state
636        .get()
637        .as_ref()
638        .context("Unexpected: ProjectContainer is uninitialized")?
639        .root_path
640        .clone();
641    Ok(Vc::cell(root_path))
642}
643
644#[turbo_tasks::function(operation, root)]
645pub(crate) async fn additional_root_path_operation(
646    container: ResolvedVc<ProjectContainer>,
647    key: RcStr,
648) -> Result<Vc<RcStr>> {
649    let container = container.await?;
650    let _guard = container.fs_map_init_lock.lock().await;
651    let roots = container.additional_roots_state.get();
652    let root = roots
653        .iter()
654        .find_map(|(name, root)| (name == &key).then_some(root))
655        .with_context(|| format!("Unexpected: additional root {key} is missing"))?;
656    Ok(Vc::cell(root.canonical_path.clone()))
657}
658
659#[turbo_tasks::function(operation, root)]
660async fn disk_file_system_map_operation(
661    container: ResolvedVc<ProjectContainer>,
662) -> Result<Vc<DiskFileSystemMap>> {
663    let (project_file_system, additional_file_systems) = {
664        let container = container.await?;
665        let _guard = container.fs_map_init_lock.lock().await;
666        let file_systems = container.file_systems_state.get();
667        let file_systems = file_systems
668            .as_ref()
669            .context("Unexpected: ProjectContainer is uninitialized")?;
670        (
671            file_systems.project_file_system,
672            container
673                .additional_roots_state
674                .get()
675                .iter()
676                .map(|(_, root)| root.file_system)
677                .collect::<Vec<_>>(),
678        )
679    };
680    let filesystems = iter::once(project_file_system)
681        .chain(additional_file_systems)
682        .map(async |operation| {
683            let fs = operation.connect().to_resolved().await?;
684            Ok((PathBuf::from(fs.await?.root()), fs))
685        })
686        .try_join()
687        .await?;
688    Ok(filesystems
689        .into_iter()
690        .collect::<DiskFileSystemMap>()
691        .cell())
692}
693
694enum EnvDiffType {
695    Added,
696    Removed,
697    Modified,
698}
699
700fn env_diff(
701    old: &[(RcStr, Option<RcStr>)],
702    new: &[(RcStr, Option<RcStr>)],
703) -> Vec<(RcStr, EnvDiffType)> {
704    let mut diffs = Vec::new();
705    let mut old_map: FxHashMap<_, _> = old.iter().cloned().collect();
706
707    for (key, new_value) in new.iter() {
708        match old_map.remove(key) {
709            Some(old_value) => {
710                if &old_value != new_value {
711                    diffs.push((key.clone(), EnvDiffType::Modified));
712                }
713            }
714            None => {
715                diffs.push((key.clone(), EnvDiffType::Added));
716            }
717        }
718    }
719
720    for (key, _) in old.iter() {
721        if old_map.contains_key(key) {
722            diffs.push((key.clone(), EnvDiffType::Removed));
723        }
724    }
725
726    diffs
727}
728
729fn env_diff_report(old: &[(RcStr, Option<RcStr>)], new: &[(RcStr, Option<RcStr>)]) -> String {
730    use std::fmt::Write;
731
732    let diff = env_diff(old, new);
733
734    let mut report = String::new();
735    for (key, diff_type) in diff {
736        let symbol = match diff_type {
737            EnvDiffType::Added => "+",
738            EnvDiffType::Removed => "-",
739            EnvDiffType::Modified => "*",
740        };
741        if !report.is_empty() {
742            report.push_str(", ");
743        }
744        write!(report, "{}{}", symbol, key).unwrap();
745    }
746    report
747}
748
749fn define_env_diff_report(old: &DefineEnv, new: &DefineEnv) -> String {
750    use std::fmt::Write;
751
752    let mut report = String::new();
753    for (name, old, new) in [
754        ("client", &old.client, &new.client),
755        ("edge", &old.edge, &new.edge),
756        ("nodejs", &old.nodejs, &new.nodejs),
757    ] {
758        let diff = env_diff_report(old, new);
759        if !diff.is_empty() {
760            if !report.is_empty() {
761                report.push_str(", ");
762            }
763            write!(report, "{name}: {{ {diff} }}").unwrap();
764        }
765    }
766    report
767}
768
769impl ProjectContainer {
770    /// Set up filesystems, watchers, and construct the [`Project`] instance inside the container.
771    ///
772    /// This function is intended to be called inside of [`turbo_tasks::TurboTasks::run`], but not
773    /// part of a [`turbo_tasks::function`]. We don't want it to be possibly re-executed.
774    ///
775    /// This is an associated function instead of a method because we don't currently implement
776    /// [`std::ops::Receiver`] on [`OperationVc`].
777    pub async fn initialize(
778        this_op: OperationVc<Self>,
779        options: ProjectOptions,
780    ) -> Result<Vec<ReadRef<PlainIssue>>> {
781        let this = this_op.read_strongly_consistent().await?;
782        let span = tracing::info_span!(
783            "initialize project",
784            project_name = %this.name,
785            version = options.next_version.as_str(),
786            node_version = options.current_node_js_version.as_str(),
787            os = std::env::consts::OS,
788            arch = std::env::consts::ARCH,
789            turbo_tasks_available_parallelism =
790                turbo_tasks::parallel::available_parallelism().map(|n| n.get()).unwrap_or(0),
791            std_thread_available_parallelism =
792                std::thread::available_parallelism().map(|n| n.get()).unwrap_or(0),
793            dev = options.dev,
794            env_diff = Empty
795        );
796        let span_clone = span.clone();
797        async move {
798            let container = this_op.resolve().strongly_consistent().await?;
799            prepare_project_container_state(container, options).await
800        }
801        .instrument(span_clone)
802        .await
803    }
804
805    pub async fn update(self: ResolvedVc<Self>, options: PartialProjectOptions) -> Result<()> {
806        let span = tracing::info_span!(
807            "update project options",
808            project_name = %self.await?.name,
809            env_diff = Empty
810        );
811        let span_clone = span.clone();
812        async move {
813            // HACK: `update` is called from a top-level function. Top-level functions are not
814            // allowed to perform eventually consistent reads. Create a stub operation
815            // to upgrade the `ResolvedVc` to an `OperationVc`. This is mostly okay
816            // because we can assume the `ProjectContainer` was originally resolved with
817            // strong consistency, and is rarely updated.
818            #[turbo_tasks::function(operation, root)]
819            fn project_container_operation_hack(
820                container: ResolvedVc<ProjectContainer>,
821            ) -> Vc<ProjectContainer> {
822                *container
823            }
824            let this = project_container_operation_hack(self)
825                .read_strongly_consistent()
826                .await?;
827            let PartialProjectOptions {
828                next_config,
829                env,
830                define_env,
831                dev,
832                encryption_key,
833                build_id,
834                preview_props,
835                browserslist_query,
836                no_mangling,
837                write_routes_hashes_manifest,
838                debug_build_paths,
839            } = options;
840
841            // Filesystem roots and watcher options are initialization-only. Changing them requires
842            // restarting the process so their process-local watchers can be recreated safely.
843            let mut new_options = this
844                .options_state
845                .get_untracked()
846                .clone()
847                .context("ProjectContainer need to be initialized with initialize()")?;
848            let old_define_env = new_options.define_env.clone();
849
850            if let Some(next_config) = next_config {
851                new_options.next_config = next_config;
852            }
853            if let Some(env) = env {
854                new_options.env = env;
855            }
856            if let Some(define_env) = define_env {
857                new_options.define_env = define_env;
858            }
859            if let Some(dev) = dev {
860                new_options.dev = dev;
861            }
862            if let Some(encryption_key) = encryption_key {
863                new_options.encryption_key = encryption_key;
864            }
865            if let Some(build_id) = build_id {
866                new_options.build_id = build_id;
867            }
868            if let Some(preview_props) = preview_props {
869                new_options.preview_props = preview_props;
870            }
871            if let Some(browserslist_query) = browserslist_query {
872                new_options.browserslist_query = browserslist_query;
873            }
874            if let Some(no_mangling) = no_mangling {
875                new_options.no_mangling = no_mangling;
876            }
877            if let Some(write_routes_hashes_manifest) = write_routes_hashes_manifest {
878                new_options.write_routes_hashes_manifest = write_routes_hashes_manifest;
879            }
880            if let Some(debug_build_paths) = debug_build_paths {
881                new_options.debug_build_paths = Some(debug_build_paths);
882            }
883
884            span.record(
885                "env_diff",
886                define_env_diff_report(&old_define_env, &new_options.define_env).as_str(),
887            );
888            this.options_state.set(Some(new_options));
889
890            Ok(())
891        }
892        .instrument(span_clone)
893        .await
894    }
895}
896
897#[turbo_tasks::value_impl]
898impl ProjectContainer {
899    #[turbo_tasks::function]
900    pub async fn project(&self) -> Result<Vc<Project>> {
901        let env_map: Vc<EnvMap>;
902        let next_config;
903        let define_env;
904        let root_path_str: RcStr;
905        let project_path;
906        let watch;
907        let dev;
908        let encryption_key;
909        let build_id;
910        let preview_props;
911        let browserslist_query;
912        let no_mangling;
913        let write_routes_hashes_manifest;
914        let current_node_js_version;
915        let debug_build_paths;
916        let deferred_entries;
917        let is_persistent_caching_enabled;
918        let server_hmr;
919        let project_file_system;
920        let output_file_system;
921        let additional_roots;
922        {
923            let options = self.options_state.get();
924            let options = options
925                .as_ref()
926                .context("ProjectContainer need to be initialized with initialize()")?;
927            let file_systems = self.file_systems_state.get();
928            let file_systems = file_systems
929                .as_ref()
930                .context("ProjectContainer need to be initialized with initialize()")?;
931            env_map = Vc::cell(options.env.iter().cloned().collect());
932            define_env = ProjectDefineEnv {
933                client: ResolvedVc::cell(options.define_env.client.iter().cloned().collect()),
934                edge: ResolvedVc::cell(options.define_env.edge.iter().cloned().collect()),
935                nodejs: ResolvedVc::cell(options.define_env.nodejs.iter().cloned().collect()),
936            }
937            .cell();
938            next_config = NextConfig::from_string(Vc::cell(options.next_config.clone()));
939            root_path_str = options.root_path.clone();
940            project_path = options.project_path.clone();
941            watch = options.watch;
942            dev = options.dev;
943            encryption_key = options.encryption_key.clone();
944            build_id = options.build_id.clone();
945            preview_props = options.preview_props.clone();
946            browserslist_query = options.browserslist_query.clone();
947            no_mangling = options.no_mangling;
948            write_routes_hashes_manifest = options.write_routes_hashes_manifest;
949            current_node_js_version = options.current_node_js_version.clone();
950            debug_build_paths = options.debug_build_paths.clone();
951            deferred_entries = options.deferred_entries.clone().unwrap_or_default();
952            is_persistent_caching_enabled = options.is_persistent_caching_enabled;
953            server_hmr = options.server_hmr;
954            project_file_system = file_systems.project_file_system;
955            output_file_system = file_systems.output_file_system;
956            additional_roots = self.additional_roots_state.get().iter().cloned().collect();
957        }
958
959        let root_path = ResolvedVc::cell(root_path_str);
960        let dist_dir = next_config.dist_dir().owned().await?;
961        let dist_dir_root = next_config.dist_dir_root().owned().await?;
962        Ok(Project {
963            root_path,
964            project_path,
965            watch,
966            next_config: next_config.to_resolved().await?,
967            dist_dir,
968            dist_dir_root,
969            env: ResolvedVc::upcast(env_map.to_resolved().await?),
970            define_env: define_env.to_resolved().await?,
971            browserslist_query,
972            mode: if dev {
973                NextMode::Development.resolved_cell()
974            } else {
975                NextMode::Build.resolved_cell()
976            },
977            versioned_content_map: self.versioned_content_map,
978            build_id,
979            encryption_key,
980            preview_props: ResolvedVc::cell(preview_props),
981            no_mangling,
982            write_routes_hashes_manifest,
983            current_node_js_version,
984            debug_build_paths,
985            deferred_entries,
986            is_persistent_caching_enabled,
987            server_hmr,
988            project_file_system,
989            output_file_system,
990            additional_roots,
991        }
992        .cell())
993    }
994
995    /// See [Project::entrypoints].
996    #[turbo_tasks::function]
997    pub fn entrypoints(self: Vc<Self>) -> Vc<Entrypoints> {
998        self.project().entrypoints()
999    }
1000
1001    /// See [`Project::hmr_chunk_names`].
1002    #[turbo_tasks::function]
1003    pub fn hmr_chunk_names(self: Vc<Self>) -> Vc<Vec<RcStr>> {
1004        self.project().hmr_chunk_names()
1005    }
1006
1007    /// Gets a source map for a particular `file_path`. If `dev` mode is disabled, this will always
1008    /// return [`FileContent::NotFound`].
1009    #[turbo_tasks::function]
1010    pub fn get_source_map(
1011        &self,
1012        file_path: FileSystemPath,
1013        section: Option<RcStr>,
1014    ) -> Vc<FileContent> {
1015        if let Some(map) = self.versioned_content_map {
1016            map.get_source_map(file_path, section)
1017        } else {
1018            FileContent::NotFound.cell()
1019        }
1020    }
1021}
1022
1023#[derive(Clone)]
1024#[turbo_tasks::value]
1025pub struct Project {
1026    /// An absolute root path (Windows or Unix path) from which all files must be nested under.
1027    /// Trying to access a file outside this root will fail, so think of this as a chroot.
1028    /// E.g. `/home/user/projects/my-repo`.
1029    root_path: ResolvedVc<RcStr>,
1030
1031    /// A path which contains the app/pages directories, relative to [`Project::root_path`], always
1032    /// a Unix path.
1033    /// E.g. `apps/my-app`
1034    project_path: RcStr,
1035
1036    /// A path where to emit the build outputs, relative to [`Project::project_path`], always a
1037    /// Unix path. Corresponds to next.config.js's `distDir`.
1038    /// E.g. `.next`
1039    dist_dir: RcStr,
1040
1041    /// The root directory of the distDir. In development mode, this is the parent directory of
1042    /// `distDir` since development builds use `{distDir}/dev`. This is used to ensure that the
1043    /// bundler doesn't traverse into the output directory.
1044    dist_dir_root: RcStr,
1045
1046    /// Filesystem watcher options.
1047    watch: WatchOptions,
1048
1049    /// Next config.
1050    next_config: ResolvedVc<NextConfig>,
1051
1052    /// A map of environment variables to use when compiling code.
1053    env: ResolvedVc<Box<dyn ProcessEnv>>,
1054
1055    /// A map of environment variables which should get injected at compile
1056    /// time.
1057    define_env: ResolvedVc<ProjectDefineEnv>,
1058
1059    /// The browserslist query to use for targeting browsers.
1060    browserslist_query: RcStr,
1061
1062    mode: ResolvedVc<NextMode>,
1063
1064    versioned_content_map: Option<ResolvedVc<VersionedContentMap>>,
1065
1066    build_id: RcStr,
1067
1068    encryption_key: RcStr,
1069
1070    preview_props: ResolvedVc<DraftModeOptionsValue>,
1071
1072    /// When the code is minified, this opts out of the default mangling of
1073    /// local names for variables, functions etc., which can be useful for
1074    /// debugging/profiling purposes.
1075    no_mangling: bool,
1076
1077    /// Whether to write the route hashes manifest.
1078    write_routes_hashes_manifest: bool,
1079
1080    current_node_js_version: RcStr,
1081
1082    /// Debug build paths for selective builds.
1083    /// When set, only routes matching these paths will be included in the build.
1084    debug_build_paths: Option<DebugBuildPaths>,
1085
1086    /// App-router page routes that should be built after non-deferred routes.
1087    deferred_entries: Vec<RcStr>,
1088
1089    /// Whether to enable persistent caching
1090    is_persistent_caching_enabled: bool,
1091
1092    /// Whether server-side HMR is enabled (disabled with --no-server-fast-refresh).
1093    server_hmr: bool,
1094
1095    project_file_system: OperationVc<DiskFileSystem>,
1096    output_file_system: OperationVc<DiskFileSystem>,
1097    #[bincode(with = "turbo_bincode::indexmap")]
1098    pub(crate) additional_roots: FxIndexMap<RcStr, AdditionalDiskFileSystem>,
1099}
1100
1101#[turbo_tasks::value]
1102pub struct ProjectDefineEnv {
1103    client: ResolvedVc<OptionEnvMap>,
1104    edge: ResolvedVc<OptionEnvMap>,
1105    nodejs: ResolvedVc<OptionEnvMap>,
1106}
1107
1108async fn import_meta_env_base_url(next_config: ResolvedVc<NextConfig>) -> Result<RcStr> {
1109    Ok(match &*next_config.base_path().await? {
1110        Some(base_path) => format!("{base_path}/").into(),
1111        None => rcstr!("/"),
1112    })
1113}
1114
1115#[turbo_tasks::value_impl]
1116impl ProjectDefineEnv {
1117    #[turbo_tasks::function]
1118    pub fn client(&self) -> Vc<OptionEnvMap> {
1119        *self.client
1120    }
1121
1122    #[turbo_tasks::function]
1123    pub fn edge(&self) -> Vc<OptionEnvMap> {
1124        *self.edge
1125    }
1126
1127    #[turbo_tasks::function]
1128    pub fn nodejs(&self) -> Vc<OptionEnvMap> {
1129        *self.nodejs
1130    }
1131}
1132
1133#[turbo_tasks::value(shared)]
1134struct ConflictIssue {
1135    path: FileSystemPath,
1136    title: ResolvedVc<StyledString>,
1137    description: ResolvedVc<StyledString>,
1138    severity: IssueSeverity,
1139}
1140
1141#[async_trait]
1142#[turbo_tasks::value_impl]
1143impl Issue for ConflictIssue {
1144    fn stage(&self) -> IssueStage {
1145        IssueStage::AppStructure
1146    }
1147
1148    fn severity(&self) -> IssueSeverity {
1149        self.severity
1150    }
1151
1152    async fn file_path(&self) -> Result<FileSystemPath> {
1153        Ok(self.path.clone())
1154    }
1155
1156    async fn title(&self) -> Result<StyledString> {
1157        self.title.owned().await
1158    }
1159
1160    async fn description(&self) -> Result<Option<StyledString>> {
1161        Ok(Some(self.description.owned().await?))
1162    }
1163}
1164
1165#[turbo_tasks::value_impl]
1166impl Project {
1167    #[turbo_tasks::function]
1168    pub async fn app_project(self: Vc<Self>) -> Result<Vc<OptionAppProject>> {
1169        let app_dir = find_app_dir(self.project_path().owned().await?).await?;
1170
1171        Ok(match &*app_dir {
1172            Some(app_dir) => Vc::cell(Some(
1173                AppProject::new(self, app_dir.clone()).to_resolved().await?,
1174            )),
1175            None => Vc::cell(None),
1176        })
1177    }
1178
1179    #[turbo_tasks::function]
1180    pub fn pages_project(self: Vc<Self>) -> Vc<PagesProject> {
1181        PagesProject::new(self)
1182    }
1183
1184    #[turbo_tasks::function]
1185    pub fn project_fs(&self) -> Result<Vc<DiskFileSystem>> {
1186        Ok(self.project_file_system.connect())
1187    }
1188
1189    #[turbo_tasks::function]
1190    pub fn client_fs(self: Vc<Self>) -> Vc<Box<dyn FileSystem>> {
1191        let virtual_fs = VirtualFileSystem::new_with_name(rcstr!("client-fs"));
1192        Vc::upcast(virtual_fs)
1193    }
1194
1195    #[turbo_tasks::function]
1196    pub fn output_fs(&self) -> Vc<DiskFileSystem> {
1197        self.output_file_system.connect()
1198    }
1199
1200    #[turbo_tasks::function]
1201    pub async fn node_root(self: Vc<Self>) -> Result<Vc<FileSystemPath>> {
1202        let this = self.await?;
1203        Ok(self
1204            .output_fs()
1205            .root()
1206            .await?
1207            .join(&this.project_path)?
1208            .join(&this.dist_dir)?
1209            .cell())
1210    }
1211
1212    #[turbo_tasks::function]
1213    pub fn client_root(self: Vc<Self>) -> Vc<FileSystemPath> {
1214        self.client_fs().root()
1215    }
1216
1217    #[turbo_tasks::function]
1218    pub fn project_root_path(self: Vc<Self>) -> Vc<FileSystemPath> {
1219        self.project_fs().root()
1220    }
1221
1222    #[turbo_tasks::function]
1223    pub async fn client_relative_path(self: Vc<Self>) -> Result<Vc<FileSystemPath>> {
1224        let next_config = self.next_config();
1225        Ok(self
1226            .client_root()
1227            .await?
1228            .join(&format!(
1229                "{}/_next",
1230                next_config
1231                    .base_path()
1232                    .await?
1233                    .as_deref()
1234                    .unwrap_or_default(),
1235            ))?
1236            .cell())
1237    }
1238
1239    /// Returns the relative path from the node root to the output root.
1240    /// E.g. from `[project]/test/e2e/app-dir/non-root-project-monorepo/apps/web/app/
1241    /// import-meta-url-ssr/page.tsx` to `[project]/`.
1242    #[turbo_tasks::function]
1243    pub async fn node_root_to_root_path(self: Vc<Self>) -> Result<Vc<RcStr>> {
1244        Ok(Vc::cell(
1245            self.node_root()
1246                .await?
1247                .get_relative_path_to(&*self.output_fs().root().await?)
1248                .context("Expected node root to be inside of output fs")?,
1249        ))
1250    }
1251
1252    #[turbo_tasks::function]
1253    pub async fn project_path(self: Vc<Self>) -> Result<Vc<FileSystemPath>> {
1254        let this = self.await?;
1255        let root = self.project_root_path().await?;
1256        Ok(root.join(&this.project_path)?.cell())
1257    }
1258
1259    #[turbo_tasks::function]
1260    pub(super) fn env(&self) -> Vc<Box<dyn ProcessEnv>> {
1261        *self.env
1262    }
1263
1264    #[turbo_tasks::function]
1265    pub async fn ci_has_next_support(&self) -> Result<Vc<bool>> {
1266        Ok(Vc::cell(
1267            self.env.read(rcstr!("NOW_BUILDER")).await?.is_some(),
1268        ))
1269    }
1270
1271    #[turbo_tasks::function]
1272    pub(super) fn current_node_js_version(&self) -> Vc<NodeJsVersion> {
1273        NodeJsVersion::Static(ResolvedVc::cell(self.current_node_js_version.clone())).cell()
1274    }
1275
1276    #[turbo_tasks::function]
1277    pub fn next_config(&self) -> Vc<NextConfig> {
1278        *self.next_config
1279    }
1280
1281    /// Build the `IssueFilter` for this project, incorporating any
1282    /// `turbopack.ignoreIssue` rules from the Next.js config.
1283    #[turbo_tasks::function]
1284    pub async fn issue_filter(self: Vc<Self>) -> Result<Vc<IssueFilter>> {
1285        let ignore_rules = self.next_config().turbopack_ignore_issue_rules().await?;
1286        Ok(IssueFilter::warnings_and_foreign_errors()
1287            .with_ignore_rules(ReadRef::into_owned(ignore_rules))
1288            .cell())
1289    }
1290
1291    #[turbo_tasks::function]
1292    pub(super) fn is_persistent_caching_enabled(&self) -> Vc<bool> {
1293        Vc::cell(self.is_persistent_caching_enabled)
1294    }
1295
1296    #[turbo_tasks::function]
1297    pub(super) fn next_mode(&self) -> Vc<NextMode> {
1298        *self.mode
1299    }
1300
1301    #[turbo_tasks::function]
1302    pub(super) fn is_watch_enabled(&self) -> Result<Vc<bool>> {
1303        Ok(Vc::cell(self.watch.enable))
1304    }
1305
1306    #[turbo_tasks::function]
1307    pub(super) fn should_write_routes_hashes_manifest(&self) -> Result<Vc<bool>> {
1308        Ok(Vc::cell(self.write_routes_hashes_manifest))
1309    }
1310
1311    #[turbo_tasks::function]
1312    pub(super) async fn should_write_nft_manifests(&self) -> Result<Vc<bool>> {
1313        Ok(Vc::cell(
1314            self.mode.await?.is_production()
1315                && *self.next_config.output().await? != Some(OutputType::Export),
1316        ))
1317    }
1318
1319    #[turbo_tasks::function]
1320    pub fn deferred_entries(&self) -> Vc<Vec<RcStr>> {
1321        Vc::cell(self.deferred_entries.clone())
1322    }
1323
1324    #[turbo_tasks::function]
1325    pub(super) async fn per_page_module_graph(&self) -> Result<Vc<bool>> {
1326        Ok(Vc::cell(*self.mode.await? == NextMode::Development))
1327    }
1328
1329    #[turbo_tasks::function]
1330    pub(super) fn encryption_key(&self) -> Vc<RcStr> {
1331        Vc::cell(self.encryption_key.clone())
1332    }
1333
1334    #[turbo_tasks::function]
1335    pub(super) fn no_mangling(&self) -> Vc<bool> {
1336        Vc::cell(self.no_mangling)
1337    }
1338
1339    #[turbo_tasks::function]
1340    pub(super) async fn execution_context(self: Vc<Self>) -> Result<Vc<ExecutionContext>> {
1341        let node_root = self.node_root().owned().await?;
1342        let next_mode = self.next_mode().await?;
1343        let strategy = *self
1344            .next_config()
1345            .turbopack_plugin_runtime_strategy()
1346            .await?;
1347        let node_backend = match strategy {
1348            #[cfg(feature = "worker_pool")]
1349            TurbopackPluginRuntimeStrategy::WorkerThreads => worker_threads_backend(),
1350            #[cfg(all(feature = "process_pool", not(target_family = "wasm")))]
1351            TurbopackPluginRuntimeStrategy::ChildProcesses => child_process_backend(),
1352        };
1353
1354        let node_execution_chunking_context = Vc::upcast(
1355            NodeJsChunkingContext::builder(
1356                self.project_root_path().owned().await?,
1357                node_root.join("build")?,
1358                self.node_root_to_root_path().owned().await?,
1359                node_root.join("build")?,
1360                node_root.join("build/chunks")?,
1361                node_root.join("build/assets")?,
1362                node_build_environment().to_resolved().await?,
1363                next_mode.runtime_type(),
1364            )
1365            .source_maps(*self.next_config().server_source_maps().await?)
1366            // This context is shared by every node-side transform that needs to evaluate JS at
1367            // build time (postcss configs, webpack loaders, next/font/google, ...). Each of those
1368            // builds its own module graph but they all emit the same `[turbopack]_runtime.js`, so
1369            // no single graph can decide which optional runtime features to drop.
1370            .shared_runtime_chunk(true)
1371            .build(),
1372        );
1373
1374        Ok(ExecutionContext::new(
1375            self.project_path().owned().await?,
1376            node_execution_chunking_context,
1377            self.env(),
1378            node_backend,
1379        ))
1380    }
1381
1382    #[turbo_tasks::function]
1383    pub(super) async fn client_compile_time_info(&self) -> Result<Vc<CompileTimeInfo>> {
1384        let next_mode = self.mode.await?;
1385        Ok(get_client_compile_time_info(
1386            self.browserslist_query.clone(),
1387            self.define_env.client(),
1388            self.next_config.report_system_env_inlining(),
1389            next_mode.is_development(),
1390            import_meta_env_base_url(self.next_config).await?,
1391        ))
1392    }
1393
1394    #[turbo_tasks::function]
1395    pub async fn get_all_endpoint_groups(
1396        self: Vc<Self>,
1397        app_dir_only: bool,
1398    ) -> Result<Vc<EndpointGroups>> {
1399        Ok(self.get_all_endpoint_groups_with_app_route_filter(app_dir_only, None))
1400    }
1401
1402    #[turbo_tasks::function]
1403    pub async fn get_all_endpoint_groups_with_app_route_filter(
1404        self: Vc<Self>,
1405        app_dir_only: bool,
1406        app_route_filter: Option<Vec<RcStr>>,
1407    ) -> Result<Vc<EndpointGroups>> {
1408        let mut endpoint_groups = Vec::new();
1409
1410        let entrypoints = self
1411            .entrypoints_with_app_route_filter(app_route_filter)
1412            .await?;
1413        let mut add_pages_entries = false;
1414
1415        if let Some(middleware) = &entrypoints.middleware {
1416            endpoint_groups.push((
1417                EndpointGroupKey::Middleware,
1418                EndpointGroup::from(middleware.endpoint),
1419            ));
1420        }
1421
1422        if let Some(instrumentation) = &entrypoints.instrumentation {
1423            endpoint_groups.push((
1424                EndpointGroupKey::Instrumentation,
1425                EndpointGroup::from(instrumentation.node_js),
1426            ));
1427            endpoint_groups.push((
1428                EndpointGroupKey::InstrumentationEdge,
1429                EndpointGroup::from(instrumentation.edge),
1430            ));
1431        }
1432
1433        for (key, route) in entrypoints.routes.iter() {
1434            match route {
1435                Route::Page {
1436                    html_endpoint,
1437                    data_endpoint,
1438                } => {
1439                    if !app_dir_only {
1440                        endpoint_groups.push((
1441                            EndpointGroupKey::Route(key.clone()),
1442                            EndpointGroup {
1443                                primary: vec![EndpointGroupEntry {
1444                                    endpoint: *html_endpoint,
1445                                    sub_name: None,
1446                                }],
1447                                // This only exists in development mode for HMR
1448                                additional: data_endpoint
1449                                    .iter()
1450                                    .map(|endpoint| EndpointGroupEntry {
1451                                        endpoint: *endpoint,
1452                                        sub_name: None,
1453                                    })
1454                                    .collect(),
1455                            },
1456                        ));
1457                        add_pages_entries = true;
1458                    }
1459                }
1460                Route::PageApi { endpoint } => {
1461                    if !app_dir_only {
1462                        endpoint_groups.push((
1463                            EndpointGroupKey::Route(key.clone()),
1464                            EndpointGroup::from(*endpoint),
1465                        ));
1466                        add_pages_entries = true;
1467                    }
1468                }
1469                Route::AppPage(page_routes) => {
1470                    endpoint_groups.push((
1471                        EndpointGroupKey::Route(key.clone()),
1472                        EndpointGroup {
1473                            primary: page_routes
1474                                .iter()
1475                                .map(|r| EndpointGroupEntry {
1476                                    endpoint: r.html_endpoint,
1477                                    sub_name: Some(r.original_name.clone()),
1478                                })
1479                                .collect(),
1480                            additional: Vec::new(),
1481                        },
1482                    ));
1483                }
1484                Route::AppRoute {
1485                    original_name: _,
1486                    endpoint,
1487                    ..
1488                } => {
1489                    endpoint_groups.push((
1490                        EndpointGroupKey::Route(key.clone()),
1491                        EndpointGroup::from(*endpoint),
1492                    ));
1493                }
1494                Route::Conflict => {
1495                    tracing::info!("WARN: conflict");
1496                }
1497            }
1498        }
1499
1500        if add_pages_entries {
1501            endpoint_groups.push((
1502                EndpointGroupKey::PagesError,
1503                EndpointGroup::from(entrypoints.pages_error_endpoint),
1504            ));
1505            endpoint_groups.push((
1506                EndpointGroupKey::PagesApp,
1507                EndpointGroup::from(entrypoints.pages_app_endpoint),
1508            ));
1509            endpoint_groups.push((
1510                EndpointGroupKey::PagesDocument,
1511                EndpointGroup::from(entrypoints.pages_document_endpoint),
1512            ));
1513        }
1514
1515        Ok(Vc::cell(endpoint_groups))
1516    }
1517
1518    #[turbo_tasks::function]
1519    pub async fn get_all_endpoints(self: Vc<Self>, app_dir_only: bool) -> Result<Vc<Endpoints>> {
1520        let mut endpoints = Vec::new();
1521        for (_key, group) in self.get_all_endpoint_groups(app_dir_only).await?.iter() {
1522            for entry in group.primary.iter() {
1523                endpoints.push(entry.endpoint);
1524            }
1525            for entry in group.additional.iter() {
1526                endpoints.push(entry.endpoint);
1527            }
1528        }
1529
1530        Ok(Vc::cell(endpoints))
1531    }
1532
1533    #[turbo_tasks::function]
1534    pub async fn get_all_entries(self: Vc<Self>) -> Result<Vc<GraphEntries>> {
1535        let endpoint_entries = self
1536            .get_all_endpoints(false)
1537            .await?
1538            .iter()
1539            .map(|endpoint| endpoint.entries().owned())
1540            .try_join()
1541            .await?;
1542
1543        let result = GraphEntries::concatenate(
1544            endpoint_entries
1545                .into_iter()
1546                .chain(iter::once(self.client_main_modules().owned().await?))
1547                .chain(iter::once(GraphEntries::new(
1548                    vec![],
1549                    // The superset of what any endpoint traces, so that these modules and their
1550                    // references are part of the graph. Which endpoint actually traces them is
1551                    // decided by what is passed to `trace_endpoint`.
1552                    self.pages_traced_modules().owned().await?,
1553                ))),
1554        );
1555
1556        Ok(result.cell())
1557    }
1558
1559    #[turbo_tasks::function]
1560    pub async fn get_all_additional_entries(
1561        self: Vc<Self>,
1562        graphs: Vc<ModuleGraph>,
1563    ) -> Result<Vc<GraphEntries>> {
1564        let result = GraphEntries::concatenate(
1565            self.get_all_endpoints(false)
1566                .await?
1567                .iter()
1568                .map(|endpoint| endpoint.additional_entries(graphs).owned())
1569                .try_join()
1570                .await?,
1571        );
1572        Ok(result.cell())
1573    }
1574
1575    #[turbo_tasks::function]
1576    pub async fn module_graph(
1577        self: Vc<Self>,
1578        entry: ResolvedVc<Box<dyn Module>>,
1579    ) -> Result<Vc<ModuleGraph>> {
1580        Ok(if *self.per_page_module_graph().await? {
1581            ModuleGraph::from_graphs(
1582                vec![SingleModuleGraph::new_with_entry(
1583                    ChunkGroupEntry::Entry {
1584                        modules: vec![entry],
1585                        heuristics: EntryHeuristics::default(),
1586                    },
1587                    /* include_traced */ *self.should_write_nft_manifests().await?,
1588                    /* include_binding_usage */ self.next_mode().await?.is_production(),
1589                )],
1590                None,
1591            )
1592            .connect()
1593        } else {
1594            *self.whole_app_module_graphs().await?.full
1595        })
1596    }
1597
1598    #[turbo_tasks::function]
1599    pub async fn module_graph_for_modules(
1600        self: Vc<Self>,
1601        evaluatable_assets: Vc<EvaluatableAssets>,
1602    ) -> Result<Vc<ModuleGraph>> {
1603        Ok(if *self.per_page_module_graph().await? {
1604            let entries = evaluatable_assets
1605                .await?
1606                .iter()
1607                .copied()
1608                .map(ResolvedVc::upcast)
1609                .collect();
1610            ModuleGraph::from_graphs(
1611                vec![SingleModuleGraph::new_with_entries(
1612                    GraphEntries::from_chunk_groups(vec![ChunkGroupEntry::Entry {
1613                        modules: entries,
1614                        heuristics: EntryHeuristics::default(),
1615                    }])
1616                    .resolved_cell(),
1617                    /* include_traced */ *self.should_write_nft_manifests().await?,
1618                    /* include_binding_usage */ self.next_mode().await?.is_production(),
1619                )],
1620                None,
1621            )
1622            .connect()
1623        } else {
1624            *self.whole_app_module_graphs().await?.full
1625        })
1626    }
1627
1628    /// Computes the whole app module graph without dropping issues.
1629    ///
1630    /// Use this instead of [Self::whole_app_module_graphs] when you need to collect issues from
1631    /// the computation (e.g. for the `get_compilation_issues` MCP tool).
1632    #[turbo_tasks::function]
1633    pub async fn whole_app_module_graphs_without_dropping_issues(
1634        self: ResolvedVc<Self>,
1635    ) -> Result<Vc<BaseAndFullModuleGraph>> {
1636        let module_graphs_op = whole_app_module_graph_operation(self);
1637        let module_graphs_vc = module_graphs_op.connect();
1638        scale_down_node_pool(self).await?;
1639        Ok(module_graphs_vc)
1640    }
1641
1642    /// Computes the whole app module graph, dropping issues in development mode so that
1643    /// individual routes don't each report every issue from the shared graph.
1644    #[turbo_tasks::function(root)]
1645    pub async fn whole_app_module_graphs(
1646        self: ResolvedVc<Self>,
1647    ) -> Result<Vc<BaseAndFullModuleGraph>> {
1648        let module_graphs_op = whole_app_module_graph_operation(self);
1649        let module_graphs_vc = if self.next_mode().await?.is_production() {
1650            module_graphs_op.connect()
1651        } else {
1652            let vc = module_graphs_op.resolve().strongly_consistent().await?;
1653            module_graphs_op.drop_issues();
1654            *vc
1655        };
1656        scale_down_node_pool(self).await?;
1657        Ok(module_graphs_vc)
1658    }
1659
1660    #[turbo_tasks::function]
1661    pub(super) async fn server_compile_time_info(self: Vc<Self>) -> Result<Vc<CompileTimeInfo>> {
1662        let this = self.await?;
1663        Ok(get_server_compile_time_info(
1664            // `/ROOT` corresponds to `[project]/`, so we need exactly the `path` part.
1665            self.project_path(),
1666            this.define_env.nodejs(),
1667            self.current_node_js_version(),
1668            this.next_config.report_system_env_inlining(),
1669            this.server_hmr,
1670            import_meta_env_base_url(this.next_config).await?,
1671        ))
1672    }
1673
1674    #[turbo_tasks::function]
1675    pub(super) async fn edge_compile_time_info(self: Vc<Self>) -> Result<Vc<CompileTimeInfo>> {
1676        let this = self.await?;
1677        Ok(get_edge_compile_time_info(
1678            self.project_path().owned().await?,
1679            this.define_env.edge(),
1680            self.current_node_js_version(),
1681            this.next_config.report_system_env_inlining(),
1682            import_meta_env_base_url(this.next_config).await?,
1683        ))
1684    }
1685
1686    #[turbo_tasks::function]
1687    pub(super) async fn edge_env(&self) -> Result<Vc<EnvMap>> {
1688        let preview_props = self.preview_props.await?;
1689        let edge_env = fxindexmap! {
1690            rcstr!("__NEXT_BUILD_ID") => self.build_id.clone(),
1691            rcstr!("NEXT_SERVER_ACTIONS_ENCRYPTION_KEY") => self.encryption_key.clone(),
1692            rcstr!("__NEXT_PREVIEW_MODE_ID") => preview_props.preview_mode_id.clone(),
1693            rcstr!("__NEXT_PREVIEW_MODE_ENCRYPTION_KEY") => preview_props.preview_mode_encryption_key.clone(),
1694            rcstr!("__NEXT_PREVIEW_MODE_SIGNING_KEY") => preview_props.preview_mode_signing_key.clone(),
1695        };
1696        Ok(Vc::cell(edge_env))
1697    }
1698
1699    #[turbo_tasks::function]
1700    pub(super) async fn client_chunking_context(
1701        self: Vc<Self>,
1702    ) -> Result<Vc<Box<dyn ChunkingContext>>> {
1703        let css_url_suffix = self.next_config().asset_suffix_path();
1704        let turbopack_chunking = self.next_config().turbopack_chunking().await?;
1705        Ok(get_client_chunking_context(ClientChunkingContextOptions {
1706            mode: self.next_mode(),
1707            root_path: self.project_root_path().owned().await?,
1708            client_root: self.client_relative_path().owned().await?,
1709            client_root_to_root_path: rcstr!("/ROOT"),
1710            client_static_folder_name: self
1711                .next_config()
1712                .client_static_folder_name()
1713                .owned()
1714                .await?,
1715            asset_prefix: self.next_config().computed_asset_prefix(),
1716            service_worker_scope_base_path: self.next_config().base_path(),
1717            environment: self.client_compile_time_info().environment(),
1718            module_id_strategy: self.module_ids(),
1719            export_usage: self.export_usage(),
1720            unused_references: self.unused_references(),
1721            minify: self.next_config().turbo_client_minify(self.next_mode()),
1722            source_maps: self.next_config().client_source_maps(self.next_mode()),
1723            no_mangling: self.no_mangling(),
1724            scope_hoisting: self.next_config().turbo_scope_hoisting(self.next_mode()),
1725            nested_async_chunking: self
1726                .next_config()
1727                .turbo_nested_async_chunking(self.next_mode(), true),
1728            shared_runtime: self.next_config().turbo_shared_runtime(self.next_mode()),
1729            per_page_module_graph: self.per_page_module_graph(),
1730            lazy_dynamic_imports: self
1731                .next_config()
1732                .turbopack_lazy_dynamic_imports(*self.next_mode().await?),
1733            debug_ids: self.next_config().turbopack_debug_ids(),
1734            worker_asset_prefix: self.next_config().turbopack_worker_asset_prefix(),
1735            should_use_absolute_url_references: self.next_config().inline_css(),
1736            css_url_suffix,
1737            hash_salt: self.next_config().output_hash_salt().to_resolved().await?,
1738            cross_origin: self.next_config().cross_origin(),
1739            chunk_loading_global: self.next_config().turbopack_chunk_loading_global(),
1740            style_groups_algorithm: self.next_config().css_chunking().owned().await?,
1741            chunking_first_page_load_priority: turbopack_chunking.first_page_load_priority,
1742            chunking_priority_boost_percent: turbopack_chunking.priority_boost_percent,
1743            chunking_request_cost: turbopack_chunking.request_cost,
1744            chunking_min_chunk_size: turbopack_chunking.min_chunk_size,
1745            chunking_max_chunk_count_per_group: turbopack_chunking.max_chunk_count_per_group,
1746            chunking_max_merge_chunk_size: turbopack_chunking.max_merge_chunk_size,
1747            chunking_min_component_chunk_size: turbopack_chunking.min_component_chunk_size,
1748            generate_component_chunks: self.next_config().turbopack_generate_component_chunks(),
1749        }))
1750    }
1751
1752    #[turbo_tasks::function]
1753    pub(super) async fn service_worker_chunking_context(
1754        self: Vc<Self>,
1755    ) -> Result<Vc<Box<dyn ChunkingContext>>> {
1756        Ok(get_service_worker_chunking_context(
1757            ServiceWorkerChunkingContextOptions {
1758                mode: self.next_mode(),
1759                root_path: self.project_root_path().owned().await?,
1760                output_root: self.node_root().owned().await?,
1761                output_root_to_root_path: self.node_root_to_root_path().owned().await?,
1762                environment: self.client_compile_time_info().environment(),
1763                minify: self.next_config().turbo_client_minify(self.next_mode()),
1764                source_maps: self.next_config().client_source_maps(self.next_mode()),
1765                no_mangling: self.no_mangling(),
1766                hash_salt: self.next_config().output_hash_salt().to_resolved().await?,
1767            },
1768        ))
1769    }
1770
1771    #[turbo_tasks::function]
1772    pub(super) async fn service_worker_asset_context(
1773        self: Vc<Self>,
1774    ) -> Result<Vc<Box<dyn AssetContext>>> {
1775        Ok(Vc::upcast(ModuleAssetContext::new(
1776            TransitionOptions::default().cell(),
1777            self.client_compile_time_info(),
1778            get_client_module_options_context(
1779                self.project_path().owned().await?,
1780                self.execution_context(),
1781                self.client_compile_time_info().environment(),
1782                ClientContextType::Other,
1783                self.next_mode(),
1784                self.next_config(),
1785                self.encryption_key(),
1786            ),
1787            get_client_resolve_options_context(
1788                self.project_path().owned().await?,
1789                ClientContextType::Other,
1790                self.next_mode(),
1791                self.next_config(),
1792                self.execution_context(),
1793            ),
1794            Layer::new_with_user_friendly_name(rcstr!("service-worker"), rcstr!("Service Worker")),
1795        )))
1796    }
1797
1798    // Proxy and instrumentation stay eager: the lazy activation hook rebuilds route entries,
1799    // not these startup entries.
1800    #[turbo_tasks::function]
1801    pub(super) async fn server_chunking_context(
1802        self: Vc<Self>,
1803        client_assets: bool,
1804        supports_lazy_dynamic_imports: bool,
1805    ) -> Result<Vc<NodeJsChunkingContext>> {
1806        let css_url_suffix = self.next_config().asset_suffix_path();
1807        let options = ServerChunkingContextOptions {
1808            mode: self.next_mode(),
1809            root_path: self.project_root_path().owned().await?,
1810            node_root: self.node_root().owned().await?,
1811            node_root_to_root_path: self.node_root_to_root_path().owned().await?,
1812            environment: self.server_compile_time_info().environment(),
1813            module_id_strategy: self.module_ids(),
1814            export_usage: self.export_usage(),
1815            unused_references: self.unused_references(),
1816            minify: self.next_config().turbo_server_minify(self.next_mode()),
1817            source_maps: self.next_config().server_source_maps(),
1818            no_mangling: self.no_mangling(),
1819            scope_hoisting: self.next_config().turbo_scope_hoisting(self.next_mode()),
1820            nested_async_chunking: self
1821                .next_config()
1822                .turbo_nested_async_chunking(self.next_mode(), false),
1823            debug_ids: self.next_config().turbopack_debug_ids(),
1824            client_root: self.client_relative_path().owned().await?,
1825            client_static_folder_name: self
1826                .next_config()
1827                .client_static_folder_name()
1828                .owned()
1829                .await?,
1830            asset_prefix: self.next_config().computed_asset_prefix().owned().await?,
1831            css_url_suffix,
1832            hash_salt: self.next_config().output_hash_salt().to_resolved().await?,
1833            style_groups_algorithm: self.next_config().css_chunking().owned().await?,
1834            per_page_module_graph: self.per_page_module_graph(),
1835            lazy_dynamic_imports: supports_lazy_dynamic_imports
1836                && *self
1837                    .next_config()
1838                    .turbopack_lazy_dynamic_imports_ssr(*self.next_mode().await?)
1839                    .await?,
1840        };
1841        Ok(if client_assets {
1842            get_server_chunking_context_with_client_assets(options)
1843        } else {
1844            get_server_chunking_context(options)
1845        })
1846    }
1847
1848    #[turbo_tasks::function]
1849    pub(super) async fn edge_chunking_context(
1850        self: Vc<Self>,
1851        client_assets: bool,
1852    ) -> Result<Vc<Box<dyn ChunkingContext>>> {
1853        let css_url_suffix = self.next_config().asset_suffix_path();
1854        let options = EdgeChunkingContextOptions {
1855            mode: self.next_mode(),
1856            root_path: self.project_root_path().owned().await?,
1857            node_root: self.node_root().owned().await?,
1858            output_root_to_root_path: self.node_root_to_root_path(),
1859            environment: self.edge_compile_time_info().environment(),
1860            module_id_strategy: self.module_ids(),
1861            export_usage: self.export_usage(),
1862            unused_references: self.unused_references(),
1863            turbo_minify: self.next_config().turbo_edge_minify(self.next_mode()),
1864            turbo_source_maps: self.next_config().server_source_maps(),
1865            no_mangling: self.no_mangling(),
1866            scope_hoisting: self.next_config().turbo_scope_hoisting(self.next_mode()),
1867            nested_async_chunking: self
1868                .next_config()
1869                .turbo_nested_async_chunking(self.next_mode(), false),
1870            client_root: self.client_relative_path().owned().await?,
1871            client_static_folder_name: self
1872                .next_config()
1873                .client_static_folder_name()
1874                .owned()
1875                .await?,
1876            asset_prefix: self.next_config().computed_asset_prefix().owned().await?,
1877            css_url_suffix,
1878            hash_salt: self.next_config().output_hash_salt().to_resolved().await?,
1879            cross_origin: self.next_config().cross_origin(),
1880            style_groups_algorithm: self.next_config().css_chunking().owned().await?,
1881        };
1882        Ok(if client_assets {
1883            get_edge_chunking_context_with_client_assets(options)
1884        } else {
1885            get_edge_chunking_context(options)
1886        })
1887    }
1888
1889    #[turbo_tasks::function]
1890    pub(super) fn runtime_chunking_context(
1891        self: Vc<Self>,
1892        client_assets: bool,
1893        runtime: NextRuntime,
1894    ) -> Vc<Box<dyn ChunkingContext>> {
1895        match runtime {
1896            NextRuntime::Edge => self.edge_chunking_context(client_assets),
1897            NextRuntime::NodeJs => Vc::upcast(self.server_chunking_context(
1898                client_assets,
1899                true, // supports_lazy_dynamic_imports
1900            )),
1901        }
1902    }
1903
1904    /// Computes the project's feature-usage telemetry summary.
1905    ///
1906    /// Includes:
1907    /// - The SWC target triple (`swc/target/...`, always on).
1908    /// - Boolean config and compiler-option flags, mirroring the webpack [`TelemetryPlugin`](https://github.com/vercel/next.js/blob/9da305fe320b89ee2f8c3cfb7ecbf48856368913/packages/next/src/build/webpack-config.ts#L2516)
1909    ///   shape.
1910    /// - Per-feature-module import counts (e.g. `next/image`, `next/font/google`) computed by
1911    ///   walking the whole-app module graph and counting **unique importing modules** per feature.
1912    ///   This replaces an earlier `before_resolve` plugin that emitted telemetry per resolve;
1913    ///   because Turbopack caches resolves, the earlier approach under-counted to at most one per
1914    ///   feature.
1915    ///
1916    /// Returns `bail!` if the project is not in build mode — `whole_app_module_graphs` drops
1917    /// issues in development and the graph may not reflect the full project, so reporting
1918    /// telemetry from dev would produce misleading counts.
1919    ///
1920    /// The returned summary is sorted by feature name for determinism.
1921    #[turbo_tasks::function]
1922    pub async fn project_feature_usage(
1923        self: ResolvedVc<Self>,
1924    ) -> Result<Vc<ProjectFeatureUsageSummary>> {
1925        if !self.next_mode().await?.is_production() {
1926            bail!("project_feature_usage() may only be called during `next build`");
1927        }
1928
1929        // (public feature specifier, path suffix) pairs. The suffix identifies the resolved
1930        // feature module; we match via `module.ident().path.path.ends_with(suffix)`. Mirrors
1931        // the webpack `FEATURE_MODULE_MAP` + `FEATURE_MODULE_REGEXP_MAP` in
1932        // `packages/next/src/build/webpack/plugins/telemetry-plugin/telemetry-plugin.ts`.
1933        //
1934        // Font specifiers (`next/font/*`, `@next/font/*`) are matched against the synthesized
1935        // `target.css` virtual module produced by the Next.js font loader transform
1936        // (`crates/next-custom-transforms/src/transforms/fonts`). That transform rewrites
1937        // `import { Inter } from 'next/font/google'` into
1938        // `import inter from 'next/font/google/target.css?{...}'` — the original specifier never
1939        // appears in the module graph, but the synthesized `target.css` module's path suffix does.
1940        // `ident.path.path` does not include the query string (that lives on `ident.query`), so
1941        // `ends_with` is the correct matcher here.
1942        static FEATURE_MODULE_PATH_SUFFIXES: &[(&str, &str)] = &[
1943            ("next/image", "/next/image.js"),
1944            ("next/image", "/next/dist/api/image.js"),
1945            ("next/image", "/next/dist/esm/api/image.js"),
1946            // next/dist/api/image.js re-exports this module. When Turbopack can prove that
1947            // the re-export has no side effects, it may flatten the re-export
1948            // chain and leave this as the feature module in the graph.
1949            ("next/image", "/next/dist/client/image-component.js"),
1950            ("next/image", "/next/dist/esm/client/image-component.js"),
1951            (
1952                "next/image",
1953                "/next/dist/shared/lib/image-external-get-image-props.js",
1954            ),
1955            (
1956                "next/image",
1957                "/next/dist/esm/shared/lib/image-external-get-image-props.js",
1958            ),
1959            ("next/future/image", "/next/future/image.js"),
1960            ("next/legacy/image", "/next/legacy/image.js"),
1961            ("next/script", "/next/script.js"),
1962            ("next/dynamic", "/next/dynamic.js"),
1963            ("next/font/google", "/next/font/google/target.css"),
1964            ("next/font/local", "/next/font/local/target.css"),
1965            ("@next/font/google", "/@next/font/google/target.css"),
1966            ("@next/font/local", "/@next/font/local/target.css"),
1967        ];
1968
1969        // TODO: useSwcLoader is not being reported as it is not directly corresponds (it checks
1970        // babel config existence) — need to confirm what we'll do with turbopack.
1971        let config = self.next_config();
1972        let compiler_options = config.compiler().await?;
1973        let mut features: Vec<(RcStr, u32)> = vec![
1974            // SWC target triple is prefixed with `swc/target/` to match the webpack
1975            // `swc/target/${SWC_TARGET_TRIPLE}` variant in `EventBuildFeatureUsage`.
1976            (
1977                format!("swc/target/{}", env!("VERGEN_CARGO_TARGET_TRIPLE")).into(),
1978                1,
1979            ),
1980            (
1981                rcstr!("skipProxyUrlNormalize"),
1982                (*config.skip_proxy_url_normalize().await?) as u32,
1983            ),
1984            (
1985                rcstr!("skipTrailingSlashRedirect"),
1986                (*config.skip_trailing_slash_redirect().await?) as u32,
1987            ),
1988            (
1989                rcstr!("modularizeImports"),
1990                !config.modularize_imports().await?.is_empty() as u32,
1991            ),
1992            (
1993                rcstr!("transpilePackages"),
1994                !config.transpile_packages().await?.is_empty() as u32,
1995            ),
1996            (rcstr!("swcRelay"), compiler_options.relay.is_some() as u32),
1997            (
1998                rcstr!("swcStyledComponents"),
1999                compiler_options
2000                    .styled_components
2001                    .as_ref()
2002                    .is_some_and(|sc| sc.is_enabled()) as u32,
2003            ),
2004            (
2005                rcstr!("swcReactRemoveProperties"),
2006                compiler_options
2007                    .react_remove_properties
2008                    .as_ref()
2009                    .is_some_and(|rc| rc.is_enabled()) as u32,
2010            ),
2011            (
2012                rcstr!("swcRemoveConsole"),
2013                compiler_options
2014                    .remove_console
2015                    .as_ref()
2016                    .is_some_and(|rc| rc.is_enabled()) as u32,
2017            ),
2018            (
2019                rcstr!("swcEmotion"),
2020                compiler_options
2021                    .emotion
2022                    .as_ref()
2023                    .is_some_and(|e| e.is_enabled()) as u32,
2024            ),
2025        ];
2026
2027        // Module-usage counts: two passes over the module graph.
2028        //  1. Iterate all nodes, classify each in parallel, keep only feature-module matches.
2029        //  2. Walk edges, for each edge whose target is a classified feature module, add the parent
2030        //     to that feature's unique-importer set.
2031        let module_graph = self.whole_app_module_graphs().await?.full.await?;
2032
2033        let matching: FxHashMap<ResolvedVc<Box<dyn Module>>, &'static str> = module_graph
2034            .iter_nodes()
2035            .map(async |node| {
2036                let ident = node.ident().await?;
2037                let path = &ident.path.path;
2038                for &(feature, suffix) in FEATURE_MODULE_PATH_SUFFIXES {
2039                    if path.ends_with(suffix) {
2040                        return Ok(Some((node, feature)));
2041                    }
2042                }
2043                Ok(None)
2044            })
2045            .try_flat_join()
2046            .await?
2047            .into_iter()
2048            .collect();
2049
2050        // Collect (feature, parent) pairs for every edge whose target is a feature module.
2051        //
2052        // We count every such edge regardless of whether the import is eventually tree-shaken.
2053        // This matches webpack's `TelemetryPlugin`, which hooks `finishModules` (before DCE).
2054        // We could filter via `BindingUsageInfo` to only count edges that survive tree-shaking,
2055        // but staying parallel to webpack lets dashboards compare counts across the two bundlers
2056        // directly.
2057        let mut pairs: FxHashSet<(&'static str, ResolvedVc<Box<dyn Module>>)> =
2058            FxHashSet::default();
2059        module_graph.traverse_edges_unordered(|parent, node| {
2060            if let Some((parent_node, _)) = parent
2061                && let Some(&feature) = matching.get(&node)
2062                // A feature's public entry point may re-export another matched module. Only
2063                // count the external importer, not the internal re-export edge.
2064                && matching.get(&parent_node) != Some(&feature)
2065            {
2066                pairs.insert((feature, parent_node));
2067            }
2068            Ok(())
2069        })?;
2070
2071        // Dedupe parents by their source location (path + query + fragment), ignoring
2072        // `ident().layer` and other modifiers. In Turbopack the same user file often appears as
2073        // separate modules per layer (e.g. SSR, client, edge), but webpack counts one "importer"
2074        // per source file — this matches that semantics.
2075        let parent_source_keys = pairs
2076            .into_iter()
2077            .map(async |(feature, parent)| {
2078                let ident = parent.ident().await?;
2079                let key = (
2080                    ident.path.path.clone(),
2081                    ident.query.clone(),
2082                    ident.fragment.clone(),
2083                );
2084                Ok((feature, key))
2085            })
2086            .try_join()
2087            .await?;
2088
2089        let mut importers: FxHashMap<&'static str, FxHashSet<(RcStr, RcStr, RcStr)>> =
2090            FxHashMap::default();
2091        for (feature, key) in parent_source_keys {
2092            importers.entry(feature).or_default().insert(key);
2093        }
2094        for (feature, unique_sources) in importers {
2095            features.push((RcStr::from(feature), unique_sources.len() as u32));
2096        }
2097
2098        features.sort_by(|a, b| a.0.cmp(&b.0));
2099        Ok(ProjectFeatureUsageSummary { features }.cell())
2100    }
2101
2102    /// Scans the app/pages directories for entry points files (matching the
2103    /// provided page_extensions).
2104    #[turbo_tasks::function]
2105    pub async fn entrypoints(self: Vc<Self>) -> Result<Vc<Entrypoints>> {
2106        Ok(self.entrypoints_with_app_route_filter(None))
2107    }
2108
2109    #[turbo_tasks::function]
2110    pub async fn entrypoints_with_app_route_filter(
2111        self: Vc<Self>,
2112        app_route_filter: Option<Vec<RcStr>>,
2113    ) -> Result<Vc<Entrypoints>> {
2114        let this = self.await?;
2115        let mut routes = FxIndexMap::default();
2116        let app_project = self.app_project();
2117        let pages_project = self.pages_project();
2118
2119        // Convert debug build paths to route keys once for O(1) lookups
2120        let debug_build_paths_route_keys = this
2121            .debug_build_paths
2122            .as_ref()
2123            .map(DebugBuildPathsRouteKeys::from_debug_build_paths)
2124            .transpose()?;
2125
2126        if let Some(app_project) = &*app_project.await? {
2127            let app_routes = app_project.routes_with_filter(app_route_filter);
2128            routes.extend(
2129                app_routes
2130                    .await?
2131                    .iter()
2132                    .filter(|(k, _)| {
2133                        debug_build_paths_route_keys
2134                            .as_ref()
2135                            .is_none_or(|keys| keys.should_include_app_route(k))
2136                    })
2137                    .map(|(k, v)| (k.clone(), v.clone())),
2138            );
2139        }
2140
2141        for (pathname, page_route) in &pages_project.routes().await? {
2142            if debug_build_paths_route_keys
2143                .as_ref()
2144                .is_some_and(|keys| !keys.should_include_pages_route(pathname))
2145            {
2146                continue;
2147            }
2148
2149            match routes.entry(pathname.clone()) {
2150                Entry::Occupied(mut entry) => {
2151                    ConflictIssue {
2152                        path: self.project_path().owned().await?,
2153                        title: StyledString::Text(
2154                            format!("App Router and Pages Router both match path: {pathname}")
2155                                .into(),
2156                        )
2157                        .resolved_cell(),
2158                        description: StyledString::Text(
2159                            "Next.js does not support having both App Router and Pages Router \
2160                             routes matching the same path. Please remove one of the conflicting \
2161                             routes."
2162                                .into(),
2163                        )
2164                        .resolved_cell(),
2165                        severity: IssueSeverity::Error,
2166                    }
2167                    .resolved_cell()
2168                    .emit();
2169                    *entry.get_mut() = Route::Conflict;
2170                }
2171                Entry::Vacant(entry) => {
2172                    entry.insert(page_route.clone());
2173                }
2174            }
2175        }
2176
2177        let pages_document_endpoint = self
2178            .pages_project()
2179            .document_endpoint()
2180            .to_resolved()
2181            .await?;
2182        let pages_app_endpoint = self.pages_project().app_endpoint().to_resolved().await?;
2183        let pages_error_endpoint = self.pages_project().error_endpoint().to_resolved().await?;
2184
2185        let middleware = self.find_middleware();
2186        let middleware = if let FindContextFileResult::Found(fs_path, _) = &*middleware.await? {
2187            let is_proxy = convention_file_base_name(fs_path.file_name()) == "proxy";
2188            Some(Middleware {
2189                endpoint: self.middleware_endpoint().to_resolved().await?,
2190                is_proxy,
2191            })
2192        } else {
2193            None
2194        };
2195
2196        let instrumentation = self.find_instrumentation();
2197        let instrumentation = if let FindContextFileResult::Found(..) = *instrumentation.await? {
2198            Some(Instrumentation {
2199                node_js: self.instrumentation_endpoint(false).to_resolved().await?,
2200                edge: self.instrumentation_endpoint(true).to_resolved().await?,
2201            })
2202        } else {
2203            None
2204        };
2205
2206        Ok(Entrypoints {
2207            routes,
2208            middleware,
2209            instrumentation,
2210            pages_document_endpoint,
2211            pages_app_endpoint,
2212            pages_error_endpoint,
2213        }
2214        .cell())
2215    }
2216
2217    #[turbo_tasks::function]
2218    async fn edge_middleware_context(self: Vc<Self>) -> Result<Vc<Box<dyn AssetContext>>> {
2219        let mut transitions = vec![];
2220
2221        let app_dir = find_app_dir(self.project_path().owned().await?)
2222            .owned()
2223            .await?;
2224        let app_project = *self.app_project().await?;
2225
2226        let ecmascript_client_reference_transition_name =
2227            app_project.map(|_| AppProject::client_transition_name());
2228
2229        if let Some(app_project) = app_project {
2230            transitions.push((
2231                AppProject::client_transition_name(),
2232                app_project
2233                    .edge_ecmascript_client_reference_transition()
2234                    .to_resolved()
2235                    .await?,
2236            ));
2237        }
2238
2239        Ok(Vc::upcast(ModuleAssetContext::new(
2240            TransitionOptions {
2241                named_transitions: transitions.clone().into_iter().collect(),
2242                ..Default::default()
2243            }
2244            .cell(),
2245            self.edge_compile_time_info(),
2246            get_server_module_options_context(
2247                self.project_path().owned().await?,
2248                self.execution_context(),
2249                ServerContextType::Middleware {
2250                    app_dir: app_dir.clone(),
2251                    ecmascript_client_reference_transition_name:
2252                        ecmascript_client_reference_transition_name.clone(),
2253                },
2254                self.next_mode(),
2255                self.next_config(),
2256                NextRuntime::Edge,
2257                self.encryption_key(),
2258                self.edge_compile_time_info().environment(),
2259                self.client_compile_time_info().environment(),
2260                // There is no NFT on edge
2261                false,
2262            ),
2263            get_edge_resolve_options_context(
2264                self.project_path().owned().await?,
2265                ServerContextType::Middleware {
2266                    app_dir: app_dir.clone(),
2267                    ecmascript_client_reference_transition_name:
2268                        ecmascript_client_reference_transition_name.clone(),
2269                },
2270                self.next_mode(),
2271                self.next_config(),
2272                self.execution_context(),
2273                None, // root params can't be used in middleware
2274            ),
2275            Layer::new_with_user_friendly_name(
2276                rcstr!("middleware-edge"),
2277                rcstr!("Edge Middleware"),
2278            ),
2279        )))
2280    }
2281
2282    #[turbo_tasks::function]
2283    async fn node_middleware_context(self: Vc<Self>) -> Result<Vc<Box<dyn AssetContext>>> {
2284        let mut transitions = vec![];
2285
2286        let app_dir = find_app_dir(self.project_path().owned().await?)
2287            .owned()
2288            .await?;
2289        let app_project = *self.app_project().await?;
2290
2291        let ecmascript_client_reference_transition_name =
2292            app_project.map(|_| AppProject::client_transition_name());
2293
2294        if let Some(app_project) = app_project {
2295            transitions.push((
2296                AppProject::client_transition_name(),
2297                app_project
2298                    .edge_ecmascript_client_reference_transition()
2299                    .to_resolved()
2300                    .await?,
2301            ));
2302        }
2303
2304        Ok(Vc::upcast(ModuleAssetContext::new(
2305            TransitionOptions {
2306                named_transitions: transitions.clone().into_iter().collect(),
2307                ..Default::default()
2308            }
2309            .cell(),
2310            self.server_compile_time_info(),
2311            get_server_module_options_context(
2312                self.project_path().owned().await?,
2313                self.execution_context(),
2314                ServerContextType::Middleware {
2315                    app_dir: app_dir.clone(),
2316                    ecmascript_client_reference_transition_name:
2317                        ecmascript_client_reference_transition_name.clone(),
2318                },
2319                self.next_mode(),
2320                self.next_config(),
2321                NextRuntime::NodeJs,
2322                self.encryption_key(),
2323                self.server_compile_time_info().environment(),
2324                self.client_compile_time_info().environment(),
2325                *self.should_write_nft_manifests().await?,
2326            ),
2327            get_server_resolve_options_context(
2328                self.project_path().owned().await?,
2329                ServerContextType::Middleware {
2330                    app_dir: app_dir.clone(),
2331                    ecmascript_client_reference_transition_name,
2332                },
2333                self.next_mode(),
2334                self.next_config(),
2335                self.execution_context(),
2336                None, // root params can't be used in middleware
2337            ),
2338            Layer::new_with_user_friendly_name(rcstr!("middleware"), rcstr!("Middleware")),
2339        )))
2340    }
2341
2342    #[turbo_tasks::function]
2343    async fn find_middleware(self: Vc<Self>) -> Result<Vc<FindContextFileResult>> {
2344        Ok(find_context_file(
2345            self.project_path().owned().await?,
2346            middleware_files(self.next_config().page_extensions()),
2347            // our callers do not care about affecting sources
2348            false,
2349        ))
2350    }
2351
2352    #[turbo_tasks::function]
2353    async fn middleware_endpoint(self: Vc<Self>) -> Result<Vc<Box<dyn Endpoint>>> {
2354        let middleware = self.find_middleware();
2355        let FindContextFileResult::Found(fs_path, _) = &*middleware.await? else {
2356            return Ok(Vc::upcast(EmptyEndpoint::new(self)));
2357        };
2358        let source = Vc::upcast(FileSource::new(fs_path.clone()));
2359        let app_dir = find_app_dir(self.project_path().owned().await?)
2360            .owned()
2361            .await?;
2362        let ecmascript_client_reference_transition_name = (*self.app_project().await?)
2363            .as_ref()
2364            .map(|_| AppProject::client_transition_name());
2365
2366        let is_proxy = convention_file_base_name(fs_path.file_name()) == "proxy";
2367        let config = parse_segment_config_from_source(
2368            source,
2369            if is_proxy {
2370                ParseSegmentMode::Proxy
2371            } else {
2372                ParseSegmentMode::Base
2373            },
2374        );
2375        let runtime = config.await?.runtime.unwrap_or(if is_proxy {
2376            NextRuntime::NodeJs
2377        } else {
2378            NextRuntime::Edge
2379        });
2380
2381        let middleware_asset_context = match runtime {
2382            NextRuntime::NodeJs => self.node_middleware_context(),
2383            NextRuntime::Edge => self.edge_middleware_context(),
2384        };
2385
2386        Ok(Vc::upcast(MiddlewareEndpoint::new(
2387            self,
2388            middleware_asset_context,
2389            source,
2390            app_dir.clone(),
2391            ecmascript_client_reference_transition_name,
2392            config,
2393            runtime,
2394        )))
2395    }
2396
2397    #[turbo_tasks::function]
2398    async fn node_instrumentation_context(self: Vc<Self>) -> Result<Vc<Box<dyn AssetContext>>> {
2399        let mut transitions = vec![];
2400
2401        let app_dir = find_app_dir(self.project_path().owned().await?)
2402            .owned()
2403            .await?;
2404        let app_project = &*self.app_project().await?;
2405
2406        let ecmascript_client_reference_transition_name = app_project
2407            .as_ref()
2408            .map(|_| AppProject::client_transition_name());
2409
2410        if let Some(app_project) = app_project {
2411            transitions.push((
2412                AppProject::client_transition_name(),
2413                app_project
2414                    .ecmascript_client_reference_transition()
2415                    .to_resolved()
2416                    .await?,
2417            ));
2418        }
2419
2420        Ok(Vc::upcast(ModuleAssetContext::new(
2421            TransitionOptions {
2422                named_transitions: transitions.into_iter().collect(),
2423                ..Default::default()
2424            }
2425            .cell(),
2426            self.server_compile_time_info(),
2427            get_server_module_options_context(
2428                self.project_path().owned().await?,
2429                self.execution_context(),
2430                ServerContextType::Instrumentation {
2431                    app_dir: app_dir.clone(),
2432                    ecmascript_client_reference_transition_name:
2433                        ecmascript_client_reference_transition_name.clone(),
2434                },
2435                self.next_mode(),
2436                self.next_config(),
2437                NextRuntime::NodeJs,
2438                self.encryption_key(),
2439                self.server_compile_time_info().environment(),
2440                self.client_compile_time_info().environment(),
2441                *self.should_write_nft_manifests().await?,
2442            ),
2443            get_server_resolve_options_context(
2444                self.project_path().owned().await?,
2445                ServerContextType::Instrumentation {
2446                    app_dir: app_dir.clone(),
2447                    ecmascript_client_reference_transition_name,
2448                },
2449                self.next_mode(),
2450                self.next_config(),
2451                self.execution_context(),
2452                None, // root params can't be used in instrumentation
2453            ),
2454            Layer::new_with_user_friendly_name(
2455                rcstr!("instrumentation"),
2456                rcstr!("Instrumentation"),
2457            ),
2458        )))
2459    }
2460
2461    #[turbo_tasks::function]
2462    async fn edge_instrumentation_context(self: Vc<Self>) -> Result<Vc<Box<dyn AssetContext>>> {
2463        let mut transitions = vec![];
2464
2465        let app_dir = find_app_dir(self.project_path().owned().await?)
2466            .owned()
2467            .await?;
2468        let app_project = &*self.app_project().await?;
2469
2470        let ecmascript_client_reference_transition_name = app_project
2471            .as_ref()
2472            .map(|_| AppProject::client_transition_name());
2473
2474        if let Some(app_project) = app_project {
2475            transitions.push((
2476                AppProject::client_transition_name(),
2477                app_project
2478                    .edge_ecmascript_client_reference_transition()
2479                    .to_resolved()
2480                    .await?,
2481            ));
2482        }
2483
2484        Ok(Vc::upcast(ModuleAssetContext::new(
2485            TransitionOptions {
2486                named_transitions: transitions.into_iter().collect(),
2487                ..Default::default()
2488            }
2489            .cell(),
2490            self.edge_compile_time_info(),
2491            get_server_module_options_context(
2492                self.project_path().owned().await?,
2493                self.execution_context(),
2494                ServerContextType::Instrumentation {
2495                    app_dir: app_dir.clone(),
2496                    ecmascript_client_reference_transition_name:
2497                        ecmascript_client_reference_transition_name.clone(),
2498                },
2499                self.next_mode(),
2500                self.next_config(),
2501                NextRuntime::Edge,
2502                self.encryption_key(),
2503                self.edge_compile_time_info().environment(),
2504                self.client_compile_time_info().environment(),
2505                // There is no NFT on edge
2506                false,
2507            ),
2508            get_edge_resolve_options_context(
2509                self.project_path().owned().await?,
2510                ServerContextType::Instrumentation {
2511                    app_dir: app_dir.clone(),
2512                    ecmascript_client_reference_transition_name,
2513                },
2514                self.next_mode(),
2515                self.next_config(),
2516                self.execution_context(),
2517                None, // root params can't be used in instrumentation
2518            ),
2519            Layer::new_with_user_friendly_name(
2520                rcstr!("instrumentation-edge"),
2521                rcstr!("Edge Instrumentation"),
2522            ),
2523        )))
2524    }
2525
2526    #[turbo_tasks::function]
2527    async fn find_instrumentation(self: Vc<Self>) -> Result<Vc<FindContextFileResult>> {
2528        Ok(find_context_file(
2529            self.project_path().owned().await?,
2530            instrumentation_files(self.next_config().page_extensions()),
2531            // our callers do not care about affecting sources
2532            false,
2533        ))
2534    }
2535
2536    #[turbo_tasks::function]
2537    async fn instrumentation_endpoint(
2538        self: Vc<Self>,
2539        is_edge: bool,
2540    ) -> Result<Vc<Box<dyn Endpoint>>> {
2541        let instrumentation = self.find_instrumentation();
2542        let FindContextFileResult::Found(fs_path, _) = &*instrumentation.await? else {
2543            return Ok(Vc::upcast(EmptyEndpoint::new(self)));
2544        };
2545        let source = Vc::upcast(FileSource::new(fs_path.clone()));
2546        let app_dir = find_app_dir(self.project_path().owned().await?)
2547            .owned()
2548            .await?;
2549        let ecmascript_client_reference_transition_name = (*self.app_project().await?)
2550            .as_ref()
2551            .map(|_| AppProject::client_transition_name());
2552
2553        let instrumentation_asset_context = if is_edge {
2554            self.edge_instrumentation_context()
2555        } else {
2556            self.node_instrumentation_context()
2557        };
2558
2559        Ok(Vc::upcast(InstrumentationEndpoint::new(
2560            self,
2561            instrumentation_asset_context,
2562            source,
2563            is_edge,
2564            app_dir.clone(),
2565            ecmascript_client_reference_transition_name,
2566        )))
2567    }
2568
2569    #[turbo_tasks::function]
2570    pub async fn emit_all_output_assets(
2571        self: Vc<Self>,
2572        output_assets: OperationVc<OutputAssets>,
2573    ) -> Result<()> {
2574        let span = tracing::info_span!("emitting");
2575        async move {
2576            let all_output_assets = all_assets_from_entries_operation(output_assets);
2577
2578            let client_relative_path = self.client_relative_path().owned().await?;
2579            let node_root = self.node_root().owned().await?;
2580
2581            if let Some(map) = self.await?.versioned_content_map {
2582                map.insert_output_assets(
2583                    all_output_assets,
2584                    node_root.clone(),
2585                    client_relative_path.clone(),
2586                    node_root.clone(),
2587                )
2588                .as_side_effect()
2589                .await?;
2590
2591                Ok(())
2592            } else {
2593                emit_assets(
2594                    all_output_assets.connect(),
2595                    node_root.clone(),
2596                    client_relative_path.clone(),
2597                    node_root.clone(),
2598                )
2599                .as_side_effect()
2600                .await?;
2601
2602                Ok(())
2603            }
2604        }
2605        .instrument(span)
2606        .await
2607    }
2608
2609    #[turbo_tasks::function]
2610    async fn server_hmr_root_path(self: Vc<Self>) -> Result<Vc<FileSystemPath>> {
2611        Ok(self.node_root().await?.join("server/app")?.cell())
2612    }
2613
2614    /// Get client HMR content by chunk_name.
2615    #[turbo_tasks::function]
2616    async fn hmr_content(self: Vc<Self>, chunk_name: RcStr) -> Result<Vc<OptionVersionedContent>> {
2617        if let Some(map) = self.await?.versioned_content_map {
2618            let content = map.get(self.client_relative_path().await?.join(&chunk_name)?);
2619            Ok(content)
2620        } else {
2621            bail!("must be in dev mode to hmr")
2622        }
2623    }
2624
2625    /// Get the version state for an HMR session. Initialized with the first seen
2626    /// version in that session.
2627    #[turbo_tasks::function]
2628    pub async fn hmr_version_state(
2629        self: ResolvedVc<Self>,
2630        chunk_name: RcStr,
2631        session: TransientInstance<()>,
2632    ) -> Result<Vc<VersionState>> {
2633        // The session argument is important to avoid caching this function between
2634        // sessions.
2635        let _ = session;
2636
2637        #[tracing::instrument(
2638            level = "info",
2639            name = "get HMR version",
2640            skip_all,
2641            fields(chunk_name = %chunk_name),
2642        )]
2643        #[turbo_tasks::function(operation, root)]
2644        async fn hmr_version_operation(
2645            this: ResolvedVc<Project>,
2646            chunk_name: RcStr,
2647        ) -> Result<Vc<Box<dyn Version>>> {
2648            tracing::info!(chunk_name = %chunk_name, "hmr subscription");
2649            let content = this.hmr_content(chunk_name).await?;
2650            if let Some(content) = &*content {
2651                Ok(content.version())
2652            } else {
2653                Ok(Vc::upcast(NotFoundVersion::new()))
2654            }
2655        }
2656        let version_op = hmr_version_operation(self, chunk_name);
2657
2658        // INVALIDATION: This is intentionally untracked to avoid invalidating this
2659        // function completely. We want to initialize the VersionState with the
2660        // first seen version of the session.
2661        let state = VersionState::new(
2662            version_op
2663                .read_trait_strongly_consistent()
2664                .untracked()
2665                .await?,
2666        )
2667        .await?;
2668        Ok(state)
2669    }
2670
2671    /// Emits opaque HMR events whenever a change is detected in the chunk group
2672    /// internally known as `chunk_name`.
2673    #[turbo_tasks::function]
2674    pub async fn hmr_update(
2675        self: Vc<Self>,
2676        chunk_name: RcStr,
2677        from: Vc<VersionState>,
2678    ) -> Result<Vc<Update>> {
2679        let from = from.get();
2680        let content = self.hmr_content(chunk_name).await?;
2681        if let Some(content) = *content {
2682            Ok(content.update(from))
2683        } else {
2684            Ok(Update::Missing.cell())
2685        }
2686    }
2687
2688    /// Server entry chunks shared by all pull baselines.
2689    #[turbo_tasks::function]
2690    pub async fn server_hmr_chunks(self: Vc<Self>) -> Result<Vc<ServerHmrChunkLists>> {
2691        let Some(map) = self.await?.versioned_content_map else {
2692            bail!("must be in dev mode to hmr")
2693        };
2694        let root = self.server_hmr_root_path().owned().await?;
2695        Ok(map.server_hmr_chunks_in_path(root))
2696    }
2697
2698    #[turbo_tasks::function]
2699    pub async fn server_hmr_chunks_for_entries(
2700        self: Vc<Self>,
2701        entry_paths: Vec<RcStr>,
2702    ) -> Result<Vc<ServerHmrChunkLists>> {
2703        let mut chunk_lists =
2704            ServerHmrChunkLists::new(self.server_hmr_chunks().await?.as_slice().to_vec());
2705        chunk_lists.retain_entry_paths(&entry_paths.into_iter().collect());
2706        Ok(chunk_lists.cell())
2707    }
2708
2709    /// Gets a list of all client HMR chunk names that can be subscribed to.
2710    #[turbo_tasks::function]
2711    pub async fn hmr_chunk_names(self: Vc<Self>) -> Result<Vc<Vec<RcStr>>> {
2712        if let Some(map) = self.await?.versioned_content_map {
2713            Ok(map.keys_in_path(self.client_relative_path().owned().await?))
2714        } else {
2715            bail!("must be in dev mode to hmr")
2716        }
2717    }
2718
2719    /// Completion when server side changes are detected in output assets
2720    /// referenced from the roots
2721    #[turbo_tasks::function]
2722    pub async fn server_changed(self: Vc<Self>, roots: Vc<OutputAssets>) -> Result<Vc<Completion>> {
2723        let path = self.node_root().owned().await?;
2724        Ok(any_output_changed(roots, path, true))
2725    }
2726
2727    /// Completion when client side changes are detected in output assets
2728    /// referenced from the roots
2729    #[turbo_tasks::function]
2730    pub async fn client_changed(self: Vc<Self>, roots: Vc<OutputAssets>) -> Result<Vc<Completion>> {
2731        let path = self.client_root().owned().await?;
2732        Ok(any_output_changed(roots, path, false))
2733    }
2734
2735    #[turbo_tasks::function]
2736    pub async fn client_main_modules(self: Vc<Self>) -> Result<Vc<GraphEntries>> {
2737        let pages_project = self.pages_project();
2738        let mut chunk_groups = vec![ChunkGroupEntry::Entry {
2739            modules: vec![pages_project.client_main_module().to_resolved().await?],
2740            heuristics: EntryHeuristics::high_priority(),
2741        }];
2742
2743        if let Some(app_project) = *self.app_project().await? {
2744            chunk_groups.push(ChunkGroupEntry::Entry {
2745                modules: vec![app_project.client_main_module().to_resolved().await?],
2746                heuristics: EntryHeuristics::high_priority(),
2747            });
2748        }
2749
2750        Ok(GraphEntries::from_chunk_groups(chunk_groups).cell())
2751    }
2752
2753    /// Gets the module id strategy for the project.
2754    #[turbo_tasks::function]
2755    pub async fn module_ids(self: Vc<Self>) -> Result<Vc<ModuleIdStrategy>> {
2756        let module_id_strategy = *self.next_config().module_ids(self.next_mode()).await?;
2757        match module_id_strategy {
2758            ModuleIdStrategyConfig::Named => Ok(ModuleIdStrategy {
2759                module_id_map: None,
2760                fallback: ModuleIdFallback::Ident,
2761            }
2762            .cell()),
2763            ModuleIdStrategyConfig::Deterministic => {
2764                let module_graphs = self.whole_app_module_graphs().await?;
2765                Ok(get_global_module_id_strategy(*module_graphs.full))
2766            }
2767        }
2768    }
2769
2770    /// Compute the used exports and unused imports for each module.
2771    #[turbo_tasks::function]
2772    async fn binding_usage_info(self: Vc<Self>) -> Result<Vc<BindingUsageInfo>> {
2773        let module_graphs = self.whole_app_module_graphs().await?;
2774        Ok(module_graphs
2775            .binding_usage_info
2776            .context("No binding usage info")?
2777            .connect())
2778    }
2779
2780    /// Compute the used exports for each module.
2781    #[turbo_tasks::function]
2782    pub async fn export_usage(self: Vc<Self>) -> Result<Vc<OptionBindingUsageInfo>> {
2783        if *self
2784            .next_config()
2785            .turbopack_remove_unused_exports(self.next_mode())
2786            .await?
2787        {
2788            Ok(Vc::cell(Some(
2789                self.binding_usage_info().to_resolved().await?,
2790            )))
2791        } else {
2792            Ok(Vc::cell(None))
2793        }
2794    }
2795
2796    /// Compute the unused references that were removed (inner graph tree shaking).
2797    #[turbo_tasks::function]
2798    pub async fn unused_references(self: Vc<Self>) -> Result<Vc<UnusedReferences>> {
2799        if *self
2800            .next_config()
2801            .turbopack_remove_unused_imports(self.next_mode())
2802            .await?
2803        {
2804            Ok(self.binding_usage_info().unused_references())
2805        } else {
2806            Ok(Vc::cell(Default::default()))
2807        }
2808    }
2809
2810    #[turbo_tasks::function]
2811    pub async fn with_next_config(&self, next_config: Vc<NextConfig>) -> Result<Vc<Self>> {
2812        Ok(Self {
2813            next_config: next_config.to_resolved().await?,
2814            ..(*self).clone()
2815        }
2816        .cell())
2817    }
2818
2819    /// Returns any modules specified as `nextConfig.cacheHandler` and/or `nextConfig.cacheHandlers`
2820    #[turbo_tasks::function]
2821    pub async fn additional_traced_modules(self: Vc<Self>) -> Result<Vc<Modules>> {
2822        let project_path = self.project_path().owned().await?;
2823        let cache_handler = self
2824            .next_config()
2825            .cache_handler(project_path.clone())
2826            .await?;
2827        let cache_handlers = self
2828            .next_config()
2829            .cache_handlers(project_path.clone())
2830            .await?;
2831
2832        let asset_context = externals_tracing_module_context(
2833            get_tracing_compile_time_info(),
2834            /* resolve_typescript */ false,
2835            /* prune */ None,
2836            /* trace_file_references */ true,
2837        );
2838
2839        Ok(Vc::cell(
2840            cache_handler
2841                .iter()
2842                .chain(cache_handlers.iter())
2843                .map(|f| {
2844                    asset_context
2845                        .process(
2846                            Vc::upcast(FileSource::new(f.clone())),
2847                            ReferenceType::CommonJs(CommonJsReferenceSubType::Undefined),
2848                        )
2849                        .module()
2850                })
2851                .map(|m| m.to_resolved())
2852                .try_join()
2853                .await?,
2854        ))
2855    }
2856
2857    /// [`Project::additional_traced_modules`] plus the modules the Pages Router resolves only at
2858    /// runtime: the targets of `next/dist/server/require-hook` and the production Pages renderer.
2859    /// Other endpoints use [`Project::additional_traced_modules`].
2860    #[turbo_tasks::function]
2861    pub async fn pages_traced_modules(self: Vc<Self>) -> Result<Vc<Modules>> {
2862        let asset_context = Vc::upcast(externals_tracing_module_context(
2863            get_tracing_compile_time_info(),
2864            /* resolve_typescript */ false,
2865            /* prune */ None,
2866            /* trace_file_references */ true,
2867        ));
2868        let hook_modules = require_hook_modules(self.project_path().owned().await?, asset_context)
2869            .owned()
2870            .await?;
2871        let renderer_modules =
2872            pages_renderer_modules(self.project_path().owned().await?, asset_context)
2873                .owned()
2874                .await?;
2875
2876        Ok(Vc::cell(
2877            self.additional_traced_modules()
2878                .owned()
2879                .await?
2880                .into_iter()
2881                .chain(hook_modules)
2882                .chain(renderer_modules)
2883                .collect(),
2884        ))
2885    }
2886}
2887
2888/// Scales down or shuts down the Node.js process pool after module graph computation.
2889async fn scale_down_node_pool(project: ResolvedVc<Project>) -> Result<()> {
2890    let execution_context = project.execution_context().await?;
2891    let node_backend = execution_context.node_backend.into_trait_ref().await?;
2892    if *project.is_watch_enabled().await? {
2893        node_backend.scale_down()?;
2894    } else {
2895        node_backend.scale_zero()?;
2896    }
2897    Ok(())
2898}
2899
2900// This is a performance optimization. This function is a root aggregation function that
2901// aggregates over the whole subgraph.
2902#[turbo_tasks::function(operation, root)]
2903async fn whole_app_module_graph_operation(
2904    project: ResolvedVc<Project>,
2905) -> Result<Vc<BaseAndFullModuleGraph>> {
2906    let start = Instant::now();
2907    let wall_start = SystemTime::now();
2908    let span = tracing::info_span!("whole app module graph", modules = Empty, edges = Empty);
2909    let span_clone = span.clone();
2910    let result = async move {
2911        let next_mode = project.next_mode();
2912        let should_trace = *project.should_write_nft_manifests().await?;
2913        let should_read_binding_usage = next_mode.await?.is_production();
2914        let base_single_module_graph = SingleModuleGraph::new_with_entries(
2915            project.get_all_entries().to_resolved().await?,
2916            should_trace,
2917            should_read_binding_usage,
2918        );
2919        let base_visited_modules = VisitedModules::from_graph(base_single_module_graph);
2920
2921        let base = ModuleGraph::from_graphs(vec![base_single_module_graph], None);
2922
2923        let turbopack_remove_unused_imports = *project
2924            .next_config()
2925            .turbopack_remove_unused_imports(next_mode)
2926            .await?;
2927
2928        let base = if turbopack_remove_unused_imports {
2929            // TODO suboptimal that we do compute_binding_usage_info twice (once for the base
2930            // graph and later for the full graph)
2931            let binding_usage_info = compute_binding_usage_info(base, true);
2932            ModuleGraph::from_graphs(vec![base_single_module_graph], Some(binding_usage_info))
2933        } else {
2934            base
2935        };
2936
2937        let additional_entries = project
2938            .get_all_additional_entries(base.connect())
2939            .to_resolved()
2940            .await?;
2941
2942        let additional_module_graph = SingleModuleGraph::new_with_entries_visited(
2943            additional_entries,
2944            base_visited_modules,
2945            should_trace,
2946            should_read_binding_usage,
2947        );
2948
2949        if !span.is_disabled() {
2950            let base_module_count = base_single_module_graph
2951                .connect()
2952                .module_count()
2953                .untracked()
2954                .await?;
2955            let additional_module_count = additional_module_graph
2956                .connect()
2957                .module_count()
2958                .untracked()
2959                .await?;
2960            span.record("modules", *base_module_count + *additional_module_count);
2961            let base_edge_count = base_single_module_graph
2962                .connect()
2963                .edge_count()
2964                .untracked()
2965                .await?;
2966            let additional_edge_count = additional_module_graph
2967                .connect()
2968                .edge_count()
2969                .untracked()
2970                .await?;
2971            span.record("edges", *base_edge_count + *additional_edge_count);
2972        }
2973
2974        let graphs = vec![base_single_module_graph, additional_module_graph];
2975
2976        let (full, binding_usage_info) = if turbopack_remove_unused_imports {
2977            let full_with_unused_references = ModuleGraph::from_graphs(graphs.clone(), None);
2978            let binding_usage_info = compute_binding_usage_info(full_with_unused_references, true);
2979            (
2980                ModuleGraph::from_graphs(graphs, Some(binding_usage_info)),
2981                Some(binding_usage_info),
2982            )
2983        } else {
2984            (ModuleGraph::from_graphs(graphs, None), None)
2985        };
2986
2987        Ok(BaseAndFullModuleGraph {
2988            base: base.connect().to_resolved().await?,
2989            full: full.connect().to_resolved().await?,
2990            binding_usage_info,
2991        }
2992        .cell())
2993    }
2994    .instrument(span_clone)
2995    .await;
2996    turbo_tasks().send_compilation_event(Arc::new(TraceEvent::new_with_duration(
2997        "turbopack-module-graph",
2998        wall_start,
2999        start.elapsed(),
3000        serde_json::json!([]),
3001    )));
3002    result
3003}
3004
3005#[turbo_tasks::value(shared)]
3006pub struct BaseAndFullModuleGraph {
3007    /// The base module graph generated from the entry points.
3008    pub base: ResolvedVc<ModuleGraph>,
3009    /// `full_with_unused_references` but with unused references removed.
3010    pub full: ResolvedVc<ModuleGraph>,
3011    /// Information about binding usage in the module graph.
3012    pub binding_usage_info: Option<OperationVc<BindingUsageInfo>>,
3013}
3014
3015#[turbo_tasks::function]
3016async fn any_output_changed(
3017    roots: Vc<OutputAssets>,
3018    path: FileSystemPath,
3019    server: bool,
3020) -> Result<Vc<Completion>> {
3021    let all_assets = expand_output_assets(
3022        roots.await?.into_iter().map(ExpandOutputAssetsInput::Asset),
3023        true,
3024    )
3025    .await?;
3026    let completions = all_assets
3027        .into_iter()
3028        .map(|m| {
3029            let path = path.clone();
3030
3031            async move {
3032                let asset_path = m.path().await?;
3033                if !asset_path.path.ends_with(".map")
3034                    && (!server || !asset_path.path.ends_with(".css"))
3035                    && asset_path.is_inside_ref(&path)
3036                {
3037                    anyhow::Ok(Some(
3038                        content_changed(*ResolvedVc::upcast(m))
3039                            .to_resolved()
3040                            .await?,
3041                    ))
3042                } else {
3043                    Ok(None)
3044                }
3045            }
3046        })
3047        .try_flat_join()
3048        .await?;
3049
3050    Ok(Vc::<Completions>::cell(completions).completed())
3051}
3052
3053#[turbo_tasks::function(operation, root)]
3054fn all_assets_from_entries_operation(
3055    operation: OperationVc<OutputAssets>,
3056) -> Result<Vc<ExpandedOutputAssets>> {
3057    let assets = operation.connect();
3058    Ok(all_assets_from_entries(assets))
3059}