Skip to main content

next_napi_bindings/next_api/
project.rs

1use std::{
2    borrow::Cow,
3    fs::{canonicalize, create_dir_all},
4    io::Write,
5    path::{Path, PathBuf},
6    sync::{Arc, LazyLock},
7    thread,
8    time::Duration,
9};
10
11use anyhow::{Context, Result, anyhow, bail};
12use bincode::{Decode, Encode};
13use flate2::write::GzEncoder;
14use futures_util::TryFutureExt;
15use napi::{
16    Env, JsFunction, JsObject, Status,
17    bindgen_prelude::{External, within_runtime_if_available},
18    threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode},
19};
20use napi_derive::napi;
21use next_api::{
22    entrypoints::Entrypoints,
23    next_server_nft::next_server_nft_assets,
24    operation::{
25        EntrypointsOperation, InstrumentationOperation, MiddlewareOperation, OptionEndpoint,
26        RouteOperation,
27    },
28    project::{
29        DebugBuildPaths, DefineEnv, DraftModeOptions, HmrTarget, PartialProjectOptions, Project,
30        ProjectContainer, ProjectOptions, WatchOptions,
31    },
32    project_asset_hashes_manifest::immutable_hashes_manifest_asset_if_enabled,
33    route::{Endpoint, EndpointGroupKey, Route},
34    routes_hashes_manifest::routes_hashes_manifest_asset_if_enabled,
35};
36use next_core::{
37    app_structure::find_app_dir,
38    next_config::DIST_PROFILES_DIR_NAME,
39    next_telemetry::ProjectFeatureUsageSummary,
40    tracing_presets::{
41        TRACING_NEXT_OVERVIEW_TARGETS, TRACING_NEXT_TARGETS, TRACING_NEXT_TURBO_TASKS_TARGETS,
42        TRACING_NEXT_TURBOPACK_TARGETS,
43    },
44};
45use rand::RngExt;
46use serde::Serialize;
47use tokio::{io::AsyncWriteExt, runtime::Handle, time::Instant};
48use tracing::Instrument;
49use tracing_subscriber::{Registry, layer::SubscriberExt, util::SubscriberInitExt};
50use turbo_rcstr::{RcStr, rcstr};
51use turbo_tasks::{
52    Effects, FxIndexSet, OperationValue, OperationVc, PrettyPrintError, ReadRef, ResolvedVc,
53    TransientInstance, TryJoinIterExt, TurboTasksApi, TurboTasksCallApi, UpdateInfo, Vc,
54    mark_top_level_task,
55    message_queue::{CompilationEvent, Severity},
56    read_strongly_consistent_and_apply_effects, take_effects,
57    trace::TraceRawVcs,
58    unmark_top_level_task_may_leak_eventually_consistent_state,
59};
60use turbo_tasks_backend::db_invalidation::invalidation_reasons;
61use turbo_tasks_fs::{
62    DiskFileSystem, FileContent, FileSystem, FileSystemPath, canonicalize_to_rcstr, invalidation,
63    to_verbatim_with_case_folded_disk, util::uri_from_file,
64};
65use turbo_unix_path::{get_relative_path_to, unix_to_sys};
66use turbopack_core::{
67    PROJECT_FILESYSTEM_NAME, SOURCE_URL_PROTOCOL,
68    issue::PlainIssue,
69    output::{OutputAsset, OutputAssets},
70    source_map::{SourceMap, Token},
71    version::{PartialUpdate, TotalUpdate, Update, VersionState},
72};
73use turbopack_ecmascript_hmr_protocol::{ClientUpdateInstruction, Issue, ResourceIdentifier};
74use turbopack_trace_utils::{
75    exit::{ExitHandler, ExitReceiver},
76    filter_layer::FilterLayer,
77    raw_trace::RawTraceLayer,
78    trace_writer::TraceWriter,
79};
80use url::Url;
81
82use crate::{
83    next_api::{
84        analyze::{WriteAnalyzeResult, write_analyze_data_with_issues_operation},
85        endpoint::ExternalEndpoint,
86        turbopack_ctx::{
87            MemoryEvictionMode, NapiNextTurbopackCallbacks, NapiNextTurbopackCallbacksJsObject,
88            NextTurboTasks, NextTurbopackContext, create_turbo_tasks,
89        },
90        utils::{
91            DetachedVc, NapiIssue, NapiUsedFeature, RootTask, TurbopackResult, get_issues,
92            strongly_consistent_catch_collectables, subscribe,
93        },
94    },
95    util::DhatProfilerGuard,
96};
97
98/// Used by [`benchmark_file_io`]. This is a noisy benchmark, so set the
99/// threshold high.
100const SLOW_FILESYSTEM_THRESHOLD: Duration = Duration::from_millis(200);
101static SOURCE_MAP_PREFIX: LazyLock<String> = LazyLock::new(|| format!("{SOURCE_URL_PROTOCOL}///"));
102static SOURCE_MAP_PREFIX_PROJECT: LazyLock<String> =
103    LazyLock::new(|| format!("{SOURCE_URL_PROTOCOL}///[{PROJECT_FILESYSTEM_NAME}]/"));
104
105#[napi(object)]
106#[derive(Clone, Debug)]
107pub struct NapiEnvVar {
108    pub name: RcStr,
109    pub value: RcStr,
110}
111
112#[napi(object)]
113#[derive(Clone, Debug)]
114pub struct NapiOptionEnvVar {
115    pub name: RcStr,
116    pub value: Option<RcStr>,
117}
118
119#[napi(object)]
120pub struct NapiDraftModeOptions {
121    pub preview_mode_id: RcStr,
122    pub preview_mode_encryption_key: RcStr,
123    pub preview_mode_signing_key: RcStr,
124}
125
126impl From<NapiDraftModeOptions> for DraftModeOptions {
127    fn from(val: NapiDraftModeOptions) -> Self {
128        DraftModeOptions {
129            preview_mode_id: val.preview_mode_id,
130            preview_mode_encryption_key: val.preview_mode_encryption_key,
131            preview_mode_signing_key: val.preview_mode_signing_key,
132        }
133    }
134}
135
136#[napi(object)]
137pub struct NapiWatchOptions {
138    /// Whether to watch the filesystem for file changes.
139    pub enable: bool,
140
141    /// Enable polling at a certain interval if the native file watching doesn't work (e.g.
142    /// docker).
143    pub poll_interval_ms: Option<f64>,
144}
145
146#[napi(object)]
147pub struct NapiProjectOptions {
148    /// An absolute root path (Unix or Windows path) from which all files must be nested under.
149    /// Trying to access a file outside this root will fail, so think of this as a chroot.
150    /// E.g. `/home/user/projects/my-repo`.
151    pub root_path: RcStr,
152
153    /// A path which contains the app/pages directories, relative to [`Project::root_path`], always
154    /// Unix path. E.g. `apps/my-app`
155    pub project_path: RcStr,
156
157    /// A path where tracing output will be written to and/or cache is read/written.
158    /// Usually equal to the `distDir` in next.config.js.
159    /// E.g. `.next`
160    pub dist_dir: RcStr,
161
162    /// Filesystem watcher options.
163    pub watch: NapiWatchOptions,
164
165    /// The contents of next.config.js, serialized to JSON.
166    pub next_config: RcStr,
167
168    /// A map of environment variables to use when compiling code.
169    pub env: Vec<NapiEnvVar>,
170
171    /// A map of environment variables which should get injected at compile
172    /// time.
173    pub define_env: NapiDefineEnv,
174
175    /// The mode in which Next.js is running.
176    pub dev: bool,
177
178    /// The server actions encryption key.
179    pub encryption_key: RcStr,
180
181    /// The build id.
182    pub build_id: RcStr,
183
184    /// Options for draft mode.
185    pub preview_props: NapiDraftModeOptions,
186
187    /// The browserslist query to use for targeting browsers.
188    pub browserslist_query: RcStr,
189
190    /// When the code is minified, this opts out of the default mangling of
191    /// local names for variables, functions etc., which can be useful for
192    /// debugging/profiling purposes.
193    pub no_mangling: bool,
194
195    /// Whether to write the route hashes manifest.
196    pub write_routes_hashes_manifest: bool,
197
198    /// The version of Node.js that is available/currently running.
199    pub current_node_js_version: RcStr,
200
201    /// Debug build paths for selective builds.
202    /// When set, only routes matching these paths will be included in the build.
203    pub debug_build_paths: Option<NapiDebugBuildPaths>,
204
205    /// App-router page routes that should be built after non-deferred routes.
206    pub deferred_entries: Option<Vec<RcStr>>,
207
208    // Whether persistent caching is enabled
209    pub is_persistent_caching_enabled: bool,
210
211    /// The version of Next.js that is running.
212    pub next_version: RcStr,
213
214    /// Whether server-side HMR is enabled (disabled with --no-server-fast-refresh).
215    pub server_hmr: Option<bool>,
216}
217
218/// [NapiProjectOptions] with all fields optional.
219#[napi(object)]
220pub struct NapiPartialProjectOptions {
221    /// An absolute root path  (Unix or Windows path) from which all files must be nested under.
222    /// Trying to access a file outside this root will fail, so think of this as a chroot.
223    /// E.g. `/home/user/projects/my-repo`.
224    pub root_path: Option<RcStr>,
225
226    /// A path which contains the app/pages directories, relative to [`Project::root_path`], always
227    /// a Unix path.
228    /// E.g. `apps/my-app`
229    pub project_path: Option<RcStr>,
230
231    /// Filesystem watcher options.
232    pub watch: Option<NapiWatchOptions>,
233
234    /// The contents of next.config.js, serialized to JSON.
235    pub next_config: Option<RcStr>,
236
237    /// A map of environment variables to use when compiling code.
238    pub env: Option<Vec<NapiEnvVar>>,
239
240    /// A map of environment variables which should get injected at compile
241    /// time.
242    pub define_env: Option<NapiDefineEnv>,
243
244    /// The mode in which Next.js is running.
245    pub dev: Option<bool>,
246
247    /// The server actions encryption key.
248    pub encryption_key: Option<RcStr>,
249
250    /// The build id.
251    pub build_id: Option<RcStr>,
252
253    /// Options for draft mode.
254    pub preview_props: Option<NapiDraftModeOptions>,
255
256    /// The browserslist query to use for targeting browsers.
257    pub browserslist_query: Option<RcStr>,
258
259    /// Whether to write the route hashes manifest.
260    pub write_routes_hashes_manifest: Option<bool>,
261
262    /// When the code is minified, this opts out of the default mangling of
263    /// local names for variables, functions etc., which can be useful for
264    /// debugging/profiling purposes.
265    pub no_mangling: Option<bool>,
266}
267
268#[napi(object)]
269#[derive(Clone, Debug)]
270pub struct NapiDefineEnv {
271    pub client: Vec<NapiOptionEnvVar>,
272    pub edge: Vec<NapiOptionEnvVar>,
273    pub nodejs: Vec<NapiOptionEnvVar>,
274}
275
276#[napi(object)]
277pub struct NapiTurboEngineOptions {
278    /// Track dependencies between tasks. If false, any change during build will error.
279    pub dependency_tracking: Option<bool>,
280    /// Whether the project is running in a CI environment.
281    pub is_ci: Option<bool>,
282    /// Whether the project is running in a short session.
283    pub is_short_session: Option<bool>,
284    /// Whether to skip database compaction during shutdown.
285    pub skip_compaction: Option<bool>,
286    /// Turbopack memory eviction mode for the persistent cache.
287    pub turbopack_memory_eviction: MemoryEvictionMode,
288}
289
290impl From<NapiWatchOptions> for WatchOptions {
291    fn from(val: NapiWatchOptions) -> Self {
292        WatchOptions {
293            enable: val.enable,
294            poll_interval: val
295                .poll_interval_ms
296                .filter(|interval| !interval.is_nan() && interval.is_finite() && *interval > 0.0)
297                .map(|interval| Duration::from_secs_f64(interval / 1000.0)),
298        }
299    }
300}
301
302impl From<NapiProjectOptions> for ProjectOptions {
303    fn from(val: NapiProjectOptions) -> Self {
304        let NapiProjectOptions {
305            root_path,
306            project_path,
307            // Only used for initializing cache and tracing
308            dist_dir: _,
309            watch,
310            next_config,
311            env,
312            define_env,
313            dev,
314            encryption_key,
315            build_id,
316            preview_props,
317            browserslist_query,
318            no_mangling,
319            write_routes_hashes_manifest,
320            current_node_js_version,
321            debug_build_paths,
322            deferred_entries,
323            is_persistent_caching_enabled,
324            next_version,
325            server_hmr,
326        } = val;
327        ProjectOptions {
328            root_path,
329            project_path,
330            watch: watch.into(),
331            next_config,
332            env: env.into_iter().map(|var| (var.name, var.value)).collect(),
333            define_env: define_env.into(),
334            dev,
335            encryption_key,
336            build_id,
337            preview_props: preview_props.into(),
338            browserslist_query,
339            no_mangling,
340            write_routes_hashes_manifest,
341            current_node_js_version,
342            debug_build_paths: debug_build_paths.map(|p| DebugBuildPaths {
343                app: p.app,
344                pages: p.pages,
345            }),
346            deferred_entries,
347            is_persistent_caching_enabled,
348            next_version,
349            server_hmr: server_hmr.unwrap_or(false),
350        }
351    }
352}
353
354impl From<NapiPartialProjectOptions> for PartialProjectOptions {
355    fn from(val: NapiPartialProjectOptions) -> Self {
356        let NapiPartialProjectOptions {
357            root_path,
358            project_path,
359            watch,
360            next_config,
361            env,
362            define_env,
363            dev,
364            encryption_key,
365            build_id,
366            preview_props,
367            browserslist_query,
368            no_mangling,
369            write_routes_hashes_manifest,
370        } = val;
371        PartialProjectOptions {
372            root_path,
373            project_path,
374            watch: watch.map(From::from),
375            next_config,
376            env: env.map(|env| env.into_iter().map(|var| (var.name, var.value)).collect()),
377            define_env: define_env.map(|env| env.into()),
378            dev,
379            encryption_key,
380            build_id,
381            preview_props: preview_props.map(|props| props.into()),
382            browserslist_query,
383            no_mangling,
384            write_routes_hashes_manifest,
385            debug_build_paths: None,
386        }
387    }
388}
389
390impl From<NapiDefineEnv> for DefineEnv {
391    fn from(val: NapiDefineEnv) -> Self {
392        DefineEnv {
393            client: val
394                .client
395                .into_iter()
396                .map(|var| (var.name, var.value))
397                .collect(),
398            edge: val
399                .edge
400                .into_iter()
401                .map(|var| (var.name, var.value))
402                .collect(),
403            nodejs: val
404                .nodejs
405                .into_iter()
406                .map(|var| (var.name, var.value))
407                .collect(),
408        }
409    }
410}
411
412pub struct ProjectInstance {
413    turbopack_ctx: NextTurbopackContext,
414    container: ResolvedVc<ProjectContainer>,
415    exit_receiver: tokio::sync::Mutex<Option<ExitReceiver>>,
416}
417
418#[napi(ts_return_type = "Promise<{ __napiType: \"Project\" }>")]
419pub fn project_new(
420    env: Env,
421    mut options: NapiProjectOptions,
422    turbo_engine_options: NapiTurboEngineOptions,
423    napi_callbacks: NapiNextTurbopackCallbacksJsObject,
424) -> napi::Result<JsObject> {
425    let napi_callbacks = NapiNextTurbopackCallbacks::from_js(&env, napi_callbacks)?;
426    let (exit, exit_receiver) = ExitHandler::new_receiver();
427
428    // The root path must be canonicalized before DiskFileSystem is constructed, but do it early,
429    // before we use it for creating a trace file or anything else. The root path always exists.
430    options.root_path = canonicalize_to_rcstr(Path::new(&*options.root_path)).map_err(|e| {
431        napi::Error::from_reason(PrettyPrintError(&anyhow::Error::from(e)).to_string())
432    })?;
433    // The dist dir (e.g. `.next`) may not exist yet, and `canonicalize` requires an existing path.
434    create_dir_all(Path::new(&*options.dist_dir))
435        .with_context(|| format!("failed to create dist directory {:?}", options.dist_dir))
436        .map_err(|e| napi::Error::from_reason(PrettyPrintError(&e).to_string()))?;
437    options.dist_dir = canonicalize_to_rcstr(Path::new(&*options.dist_dir)).map_err(|e| {
438        napi::Error::from_reason(PrettyPrintError(&anyhow::Error::from(e)).to_string())
439    })?;
440
441    if let Some(dhat_profiler) = DhatProfilerGuard::try_init() {
442        exit.on_exit(async move {
443            tokio::task::spawn_blocking(move || drop(dhat_profiler))
444                .await
445                .unwrap()
446        });
447    }
448
449    let mut trace = std::env::var("NEXT_TURBOPACK_TRACING")
450        .ok()
451        .filter(|v| !v.is_empty());
452
453    if cfg!(feature = "tokio-console") && trace.is_none() {
454        // ensure `trace` is set to *something* so that the `tokio-console` feature works,
455        // otherwise you just get empty output from `tokio-console`, which can be
456        // confusing.
457        trace = Some("overview".to_owned());
458    }
459
460    enum Compression {
461        None,
462        GzipFast,
463        GzipBest,
464    }
465    let mut compress = Compression::None;
466    if let Some(mut trace) = trace {
467        let trace_path_override = std::env::var_os("NEXT_TURBOPACK_TRACING_PATH")
468            .filter(|v| !v.is_empty())
469            .map(PathBuf::from);
470        let trace_file = if let Some(path) = trace_path_override {
471            if path.is_absolute() {
472                path
473            } else {
474                std::env::current_dir()
475                    .context("Unable to read current working directory")
476                    .unwrap()
477                    .join(path)
478            }
479        } else {
480            Path::new(&options.root_path)
481                .join(&*unix_to_sys(&options.project_path))
482                .join(DIST_PROFILES_DIR_NAME)
483                // use a generic binary extension to hint to random tools not to read it.
484                .join("trace-turbopack.bin")
485        };
486        let trace_dir = trace_file
487            .parent()
488            .expect("Trace file path must have a parent directory");
489
490        println!("Turbopack tracing enabled with targets: {trace}");
491        println!("  Note that this might have a small performance impact.");
492        println!("  Trace output will be written to {}", trace_file.display());
493
494        trace = trace
495            .split(",")
496            .filter_map(|item| {
497                // Trace presets
498                Some(match item {
499                    "overview" | "1" => Cow::Owned(TRACING_NEXT_OVERVIEW_TARGETS.join(",")),
500                    "next" => Cow::Owned(TRACING_NEXT_TARGETS.join(",")),
501                    "turbopack" => Cow::Owned(TRACING_NEXT_TURBOPACK_TARGETS.join(",")),
502                    "turbo-tasks" => Cow::Owned(TRACING_NEXT_TURBO_TASKS_TARGETS.join(",")),
503                    "gz" => {
504                        compress = Compression::GzipFast;
505                        return None;
506                    }
507                    "gz-best" => {
508                        compress = Compression::GzipBest;
509                        return None;
510                    }
511                    _ => Cow::Borrowed(item),
512                })
513            })
514            .intersperse_with(|| Cow::Borrowed(","))
515            .collect::<String>();
516
517        let subscriber = Registry::default();
518
519        if cfg!(feature = "tokio-console") {
520            trace = format!("{trace},tokio=trace,runtime=trace");
521        }
522        #[cfg(feature = "tokio-console")]
523        let subscriber = subscriber.with(console_subscriber::spawn());
524
525        let subscriber = subscriber.with(FilterLayer::try_new(&trace).unwrap());
526
527        // For the default `.next-profiles` location the JS CLI already created
528        // this directory (with its `.gitignore`) before invoking the binding; this
529        // is a safety net and also covers a `NEXT_TURBOPACK_TRACING_PATH` override.
530        create_dir_all(trace_dir)
531            .with_context(|| {
532                format!(
533                    "Unable to create trace output directory {}",
534                    trace_dir.display()
535                )
536            })
537            .unwrap();
538        let (trace_writer, trace_writer_guard) = match compress {
539            Compression::None => {
540                let trace_writer = std::fs::File::create(trace_file.clone()).unwrap();
541                TraceWriter::new(trace_writer)
542            }
543            Compression::GzipFast => {
544                let trace_writer = std::fs::File::create(trace_file.clone()).unwrap();
545                let trace_writer = GzEncoder::new(trace_writer, flate2::Compression::fast());
546                TraceWriter::new(trace_writer)
547            }
548            Compression::GzipBest => {
549                let trace_writer = std::fs::File::create(trace_file.clone()).unwrap();
550                let trace_writer = GzEncoder::new(trace_writer, flate2::Compression::best());
551                TraceWriter::new(trace_writer)
552            }
553        };
554        let subscriber = subscriber.with(RawTraceLayer::new(trace_writer));
555
556        exit.on_exit(async move {
557            tokio::task::spawn_blocking(move || drop(trace_writer_guard))
558                .await
559                .unwrap();
560        });
561
562        let trace_server = std::env::var("NEXT_TURBOPACK_TRACE_SERVER").ok();
563        if trace_server.is_some() {
564            thread::spawn(move || {
565                turbopack_trace_server::start_turbopack_trace_server(trace_file, None);
566            });
567            println!("Turbopack trace server started. View trace at https://trace.nextjs.org");
568        }
569
570        subscriber.init();
571    }
572
573    env.spawn_future(
574        async move {
575            let dependency_tracking = turbo_engine_options.dependency_tracking.unwrap_or(true);
576            let is_ci = turbo_engine_options.is_ci.unwrap_or(false);
577            let is_short_session = turbo_engine_options.is_short_session.unwrap_or(false);
578            let skip_compaction = turbo_engine_options.skip_compaction.unwrap_or(false);
579            let turbopack_memory_eviction = turbo_engine_options.turbopack_memory_eviction;
580            let turbo_tasks = create_turbo_tasks(
581                PathBuf::from(&options.dist_dir),
582                &options.next_version,
583                options.is_persistent_caching_enabled,
584                dependency_tracking,
585                is_ci,
586                is_short_session,
587                skip_compaction,
588                turbopack_memory_eviction,
589            )?;
590            let turbopack_ctx = NextTurbopackContext::new(turbo_tasks.clone(), napi_callbacks);
591
592            if let Some(stats_path) = std::env::var_os("NEXT_TURBOPACK_TASK_STATISTICS") {
593                let task_stats = turbo_tasks.task_statistics().enable().clone();
594                exit.on_exit(async move {
595                    tokio::task::spawn_blocking(move || {
596                        let mut file = std::fs::File::create(&stats_path)
597                            .with_context(|| format!("failed to create or open {stats_path:?}"))?;
598                        serde_json::to_writer(&file, &task_stats)
599                            .context("failed to serialize or write task statistics")?;
600                        file.flush().context("failed to flush file")
601                    })
602                    .await
603                    .unwrap()
604                    .unwrap();
605                });
606            }
607
608            let options = ProjectOptions::from(options);
609            let is_dev = options.dev;
610            let root_path = options.root_path.clone();
611            let container = turbo_tasks
612                .run(async move {
613                    let container_op = ProjectContainer::new_operation(rcstr!("next.js"), is_dev);
614                    ProjectContainer::initialize(container_op, options).await?;
615                    container_op.resolve().strongly_consistent().await
616                })
617                .or_else(|e| turbopack_ctx.throw_turbopack_internal_result(&e.into()))
618                .await?;
619
620            if is_dev {
621                Handle::current().spawn({
622                    let tt = turbo_tasks.clone();
623                    let root_path = root_path.clone();
624                    async move {
625                        let result = tt
626                            .clone()
627                            .run(async move {
628                                #[turbo_tasks::function(operation, root)]
629                                fn project_node_root_path_operation(
630                                    container: ResolvedVc<ProjectContainer>,
631                                ) -> Vc<FileSystemPath> {
632                                    container.project().node_root()
633                                }
634
635                                let mut absolute_benchmark_dir = PathBuf::from(root_path);
636                                absolute_benchmark_dir.push(
637                                    &project_node_root_path_operation(container)
638                                        .read_strongly_consistent()
639                                        .await?
640                                        .path,
641                                );
642                                benchmark_file_io(&tt, &absolute_benchmark_dir).await
643                            })
644                            .await;
645                        if let Err(err) = result {
646                            // TODO Not ideal to print directly to stdout.
647                            // We should use a compilation event instead to report async errors.
648                            println!("Failed to benchmark file I/O: {err}");
649                        }
650                    }
651                    .instrument(tracing::info_span!("benchmark file I/O"))
652                });
653            }
654
655            Ok(External::new(ProjectInstance {
656                turbopack_ctx,
657                container,
658                exit_receiver: tokio::sync::Mutex::new(Some(exit_receiver)),
659            }))
660        }
661        .instrument(tracing::info_span!("create project")),
662    )
663}
664
665#[derive(Debug, Clone, Serialize)]
666struct SlowFilesystemEvent {
667    directory: String,
668    duration_ms: u128,
669}
670
671impl CompilationEvent for SlowFilesystemEvent {
672    fn type_name(&self) -> &'static str {
673        "SlowFilesystemEvent"
674    }
675
676    fn severity(&self) -> Severity {
677        Severity::Warning
678    }
679
680    fn message(&self) -> String {
681        format!(
682            "Slow filesystem detected. The benchmark took {}ms. If {} is a network drive, \
683             consider moving it to a local folder.\n\
684            See more: https://nextjs.org/docs/app/guides/local-development",
685            self.duration_ms, self.directory
686        )
687    }
688
689    fn to_json(&self) -> String {
690        serde_json::to_string(self).unwrap()
691    }
692}
693
694/// A very simple and low-overhead, but potentially noisy benchmark to detect
695/// very slow disk IO. Warns the user (via `println!`) if the benchmark takes
696/// more than `SLOW_FILESYSTEM_THRESHOLD`.
697///
698/// This idea is copied from Bun:
699/// - https://x.com/jarredsumner/status/1637549427677364224
700/// - https://github.com/oven-sh/bun/blob/06a9aa80c38b08b3148bfeabe560/src/install/install.zig#L3038
701async fn benchmark_file_io(turbo_tasks: &NextTurboTasks, dir: &Path) -> Result<()> {
702    let temp_path = dir.join(format!(
703        "tmp_file_io_benchmark_{:x}",
704        rand::random::<u128>()
705    ));
706
707    let mut random_buffer = [0u8; 512];
708    rand::rng().fill(&mut random_buffer[..]);
709
710    // perform IO directly with tokio (skipping `tokio_tasks_fs`) to avoid the
711    // additional noise/overhead of tasks caching, invalidation, file locks,
712    // etc.
713    let start = Instant::now();
714    async {
715        for _ in 0..3 {
716            // create a new empty file
717            let mut file = tokio::fs::File::create(&temp_path).await?;
718            file.write_all(&random_buffer).await?;
719            file.sync_all().await?;
720            drop(file);
721
722            // remove the file
723            tokio::fs::remove_file(&temp_path).await?;
724        }
725        anyhow::Ok(())
726    }
727    .instrument(tracing::info_span!("benchmark file IO (measurement)", path = %temp_path.display()))
728    .await?;
729
730    let duration = Instant::now().duration_since(start);
731    if duration > SLOW_FILESYSTEM_THRESHOLD {
732        turbo_tasks.send_compilation_event(Arc::new(SlowFilesystemEvent {
733            directory: dir.to_string_lossy().into(),
734            duration_ms: duration.as_millis(),
735        }));
736    }
737
738    Ok(())
739}
740
741#[tracing::instrument(level = "info", name = "update project", skip_all)]
742#[napi]
743pub async fn project_update(
744    #[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External<ProjectInstance>,
745    options: NapiPartialProjectOptions,
746) -> napi::Result<()> {
747    let ctx = &project.turbopack_ctx;
748    let options = options.into();
749    let container = project.container;
750
751    ctx.turbo_tasks()
752        .run(async move { container.update(options).await })
753        .or_else(|e| ctx.throw_turbopack_internal_result(&e.into()))
754        .await
755}
756
757/// Invalidates the filesystem cache so that it will be deleted next time that a turbopack project
758/// is created with filesystem cache enabled.
759#[napi]
760pub async fn project_invalidate_file_system_cache(
761    #[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External<ProjectInstance>,
762) -> napi::Result<()> {
763    tokio::task::spawn_blocking(move || {
764        // TODO: Let the JS caller specify a reason? We need to limit the reasons to ones we know
765        // how to generate a message for on the Rust side of the FFI.
766        project
767            .turbopack_ctx
768            .turbo_tasks()
769            .backend()
770            .invalidate_storage(invalidation_reasons::USER_REQUEST)
771    })
772    .await
773    .context("panicked while invalidating filesystem cache")??;
774    Ok(())
775}
776
777/// Runs exit handlers for the project registered using the [`ExitHandler`] API.
778///
779/// This is called by `project_shutdown`, so if you're calling that API, you shouldn't call this
780/// one.
781#[napi]
782pub async fn project_on_exit(
783    #[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External<ProjectInstance>,
784) {
785    project_on_exit_internal(&project).await
786}
787
788async fn project_on_exit_internal(project: &ProjectInstance) {
789    let exit_receiver = project.exit_receiver.lock().await.take();
790    exit_receiver
791        .expect("`project.onExitSync` must only be called once")
792        .run_exit_handler()
793        .await;
794}
795
796/// Runs `project_on_exit`, and then waits for turbo_tasks to gracefully shut down.
797///
798/// This is used in builds where it's important that we completely persist turbo-tasks to disk, but
799/// it's skipped in the development server (`project_on_exit` is used instead with a short timeout),
800/// where we prioritize fast exit and user responsiveness over all else.
801#[tracing::instrument(level = "info", name = "shutdown project", skip_all)]
802#[napi]
803pub async fn project_shutdown(
804    #[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External<ProjectInstance>,
805) {
806    project.turbopack_ctx.turbo_tasks().stop_and_wait().await;
807    project_on_exit_internal(&project).await;
808}
809
810#[napi(object)]
811#[derive(Default)]
812pub struct AppPageNapiRoute {
813    /// The relative path from project_path to the route file
814    pub original_name: Option<RcStr>,
815
816    pub html_endpoint: Option<External<ExternalEndpoint>>,
817    pub rsc_hmr_endpoint: Option<External<ExternalEndpoint>>,
818}
819
820#[napi(object)]
821#[derive(Default)]
822pub struct NapiRoute {
823    /// The router path
824    pub pathname: RcStr,
825    /// The relative path from project_path to the route file
826    pub original_name: Option<RcStr>,
827
828    /// The type of route, eg a Page or App
829    pub r#type: &'static str,
830
831    pub pages: Option<Vec<AppPageNapiRoute>>,
832
833    // Different representations of the endpoint
834    pub endpoint: Option<External<ExternalEndpoint>>,
835    pub html_endpoint: Option<External<ExternalEndpoint>>,
836    pub rsc_hmr_endpoint: Option<External<ExternalEndpoint>>,
837    pub data_endpoint: Option<External<ExternalEndpoint>>,
838}
839
840impl NapiRoute {
841    fn from_route(
842        pathname: RcStr,
843        value: RouteOperation,
844        turbopack_ctx: &NextTurbopackContext,
845    ) -> Self {
846        let convert_endpoint = |endpoint: OperationVc<OptionEndpoint>| {
847            Some(External::new(ExternalEndpoint(DetachedVc::new(
848                turbopack_ctx.clone(),
849                endpoint,
850            ))))
851        };
852        match value {
853            RouteOperation::Page {
854                html_endpoint,
855                data_endpoint,
856            } => NapiRoute {
857                pathname,
858                r#type: "page",
859                html_endpoint: convert_endpoint(html_endpoint),
860                data_endpoint: convert_endpoint(data_endpoint),
861                ..Default::default()
862            },
863            RouteOperation::PageApi { endpoint } => NapiRoute {
864                pathname,
865                r#type: "page-api",
866                endpoint: convert_endpoint(endpoint),
867                ..Default::default()
868            },
869            RouteOperation::AppPage(pages) => NapiRoute {
870                pathname,
871                r#type: "app-page",
872                pages: Some(
873                    pages
874                        .into_iter()
875                        .map(|page_route| AppPageNapiRoute {
876                            original_name: Some(page_route.original_name),
877                            html_endpoint: convert_endpoint(page_route.html_endpoint),
878                            rsc_hmr_endpoint: convert_endpoint(page_route.rsc_hmr_endpoint),
879                        })
880                        .collect(),
881                ),
882                ..Default::default()
883            },
884            RouteOperation::AppRoute {
885                original_name,
886                endpoint,
887            } => NapiRoute {
888                pathname,
889                original_name: Some(original_name),
890                r#type: "app-route",
891                endpoint: convert_endpoint(endpoint),
892                ..Default::default()
893            },
894            RouteOperation::Conflict => NapiRoute {
895                pathname,
896                r#type: "conflict",
897                ..Default::default()
898            },
899        }
900    }
901}
902
903#[napi(object)]
904pub struct NapiMiddleware {
905    pub endpoint: External<ExternalEndpoint>,
906    pub is_proxy: bool,
907}
908
909impl NapiMiddleware {
910    fn from_middleware(
911        value: &MiddlewareOperation,
912        turbopack_ctx: &NextTurbopackContext,
913    ) -> Result<Self> {
914        Ok(NapiMiddleware {
915            endpoint: External::new(ExternalEndpoint(DetachedVc::new(
916                turbopack_ctx.clone(),
917                value.endpoint,
918            ))),
919            is_proxy: value.is_proxy,
920        })
921    }
922}
923
924#[napi(object)]
925pub struct NapiInstrumentation {
926    pub node_js: External<ExternalEndpoint>,
927    pub edge: External<ExternalEndpoint>,
928}
929
930impl NapiInstrumentation {
931    fn from_instrumentation(
932        value: &InstrumentationOperation,
933        turbopack_ctx: &NextTurbopackContext,
934    ) -> Result<Self> {
935        Ok(NapiInstrumentation {
936            node_js: External::new(ExternalEndpoint(DetachedVc::new(
937                turbopack_ctx.clone(),
938                value.node_js,
939            ))),
940            edge: External::new(ExternalEndpoint(DetachedVc::new(
941                turbopack_ctx.clone(),
942                value.edge,
943            ))),
944        })
945    }
946}
947
948#[napi(object)]
949pub struct NapiEntrypoints {
950    pub routes: Vec<NapiRoute>,
951    pub middleware: Option<NapiMiddleware>,
952    pub instrumentation: Option<NapiInstrumentation>,
953    pub pages_document_endpoint: External<ExternalEndpoint>,
954    pub pages_app_endpoint: External<ExternalEndpoint>,
955    pub pages_error_endpoint: External<ExternalEndpoint>,
956}
957
958impl NapiEntrypoints {
959    fn from_entrypoints_op(
960        entrypoints: &EntrypointsOperation,
961        turbopack_ctx: &NextTurbopackContext,
962    ) -> Result<Self> {
963        let routes = entrypoints
964            .routes
965            .iter()
966            .map(|(k, v)| NapiRoute::from_route(k.clone(), v.clone(), turbopack_ctx))
967            .collect();
968        let middleware = entrypoints
969            .middleware
970            .as_ref()
971            .map(|m| NapiMiddleware::from_middleware(m, turbopack_ctx))
972            .transpose()?;
973        let instrumentation = entrypoints
974            .instrumentation
975            .as_ref()
976            .map(|i| NapiInstrumentation::from_instrumentation(i, turbopack_ctx))
977            .transpose()?;
978        let pages_document_endpoint = External::new(ExternalEndpoint(DetachedVc::new(
979            turbopack_ctx.clone(),
980            entrypoints.pages_document_endpoint,
981        )));
982        let pages_app_endpoint = External::new(ExternalEndpoint(DetachedVc::new(
983            turbopack_ctx.clone(),
984            entrypoints.pages_app_endpoint,
985        )));
986        let pages_error_endpoint = External::new(ExternalEndpoint(DetachedVc::new(
987            turbopack_ctx.clone(),
988            entrypoints.pages_error_endpoint,
989        )));
990        Ok(NapiEntrypoints {
991            routes,
992            middleware,
993            instrumentation,
994            pages_document_endpoint,
995            pages_app_endpoint,
996            pages_error_endpoint,
997        })
998    }
999}
1000
1001#[turbo_tasks::value(serialization = "skip")]
1002struct EntrypointsWithIssues {
1003    entrypoints: Option<ReadRef<EntrypointsOperation>>,
1004    issues: Arc<Vec<ReadRef<PlainIssue>>>,
1005    effects: Arc<Effects>,
1006}
1007
1008#[turbo_tasks::function(operation, root)]
1009async fn get_entrypoints_with_issues_operation(
1010    container: ResolvedVc<ProjectContainer>,
1011) -> Result<Vc<EntrypointsWithIssues>> {
1012    let entrypoints_operation =
1013        EntrypointsOperation::new(project_container_entrypoints_operation(container));
1014    let filter = container.project().issue_filter().await?;
1015    let (entrypoints, issues, effects) =
1016        strongly_consistent_catch_collectables(entrypoints_operation, &filter).await?;
1017    Ok(EntrypointsWithIssues {
1018        entrypoints,
1019        issues,
1020        effects,
1021    }
1022    .cell())
1023}
1024
1025#[turbo_tasks::function(operation, root)]
1026fn project_container_entrypoints_operation(
1027    // the container is a long-lived object with internally mutable state, there's no risk of it
1028    // becoming stale
1029    container: ResolvedVc<ProjectContainer>,
1030) -> Vc<Entrypoints> {
1031    container.entrypoints()
1032}
1033
1034#[turbo_tasks::value(serialization = "skip")]
1035struct OperationResult {
1036    issues: Arc<Vec<ReadRef<PlainIssue>>>,
1037    effects: Arc<Effects>,
1038}
1039
1040#[turbo_tasks::value(serialization = "skip")]
1041struct AllWrittenEntrypointsWithIssues {
1042    entrypoints: Option<ReadRef<EntrypointsOperation>>,
1043    issues: Arc<Vec<ReadRef<PlainIssue>>>,
1044    effects: Arc<Effects>,
1045}
1046
1047#[napi(object)]
1048#[derive(Clone, Debug)]
1049pub struct NapiDebugBuildPaths {
1050    pub app: Vec<RcStr>,
1051    pub pages: Vec<RcStr>,
1052}
1053
1054#[turbo_tasks::task_input]
1055#[derive(Clone, Copy, Debug, Eq, Hash, OperationValue, PartialEq, TraceRawVcs, Encode, Decode)]
1056enum EntrypointsWritePhase {
1057    All,
1058    NonDeferred,
1059    Deferred,
1060}
1061
1062fn normalize_deferred_route(route: &str) -> String {
1063    let with_leading_slash = if route.starts_with('/') {
1064        route.to_owned()
1065    } else {
1066        format!("/{route}")
1067    };
1068
1069    if with_leading_slash.len() > 1 && with_leading_slash.ends_with('/') {
1070        with_leading_slash
1071            .strip_suffix('/')
1072            .unwrap_or_default()
1073            .to_owned()
1074    } else {
1075        with_leading_slash
1076    }
1077}
1078
1079fn is_deferred_app_route(route: &str, deferred_entries: &[RcStr]) -> bool {
1080    let normalized_route = normalize_deferred_route(route);
1081
1082    deferred_entries.iter().any(|entry| {
1083        let normalized_entry = normalize_deferred_route(entry);
1084        normalized_route == normalized_entry
1085            || normalized_route.starts_with(&format!("{normalized_entry}/"))
1086    })
1087}
1088
1089#[derive(Clone, Debug, TraceRawVcs)]
1090struct DeferredPhaseBuildPaths {
1091    non_deferred: DebugBuildPaths,
1092    all: DebugBuildPaths,
1093    deferred_invalidation_dirs: Vec<RcStr>,
1094}
1095
1096fn to_app_debug_path(route: &str, leaf: &'static str) -> RcStr {
1097    let with_leading_slash = if route.starts_with('/') {
1098        route.to_owned()
1099    } else {
1100        format!("/{route}")
1101    };
1102
1103    let normalized_route = if with_leading_slash.len() > 1 && with_leading_slash.ends_with('/') {
1104        with_leading_slash.trim_end_matches('/').to_owned()
1105    } else {
1106        with_leading_slash
1107    };
1108
1109    if normalized_route == "/" {
1110        format!("/{leaf}").into()
1111    } else {
1112        format!("{normalized_route}/{leaf}").into()
1113    }
1114}
1115
1116fn app_entry_source_dir_from_original_name(original_name: &str) -> RcStr {
1117    let normalized_name = normalize_deferred_route(original_name);
1118    let mut segments = normalized_name
1119        .trim_start_matches('/')
1120        .split('/')
1121        .filter(|segment| !segment.is_empty())
1122        .collect::<Vec<_>>();
1123
1124    if !segments.is_empty() {
1125        segments.pop();
1126    }
1127
1128    if segments.is_empty() {
1129        rcstr!("/")
1130    } else {
1131        format!("/{}", segments.join("/")).into()
1132    }
1133}
1134
1135fn compute_deferred_phase_build_paths(
1136    entrypoints: &Entrypoints,
1137    deferred_entries: &[RcStr],
1138) -> DeferredPhaseBuildPaths {
1139    let mut non_deferred_app = FxIndexSet::default();
1140    let mut deferred_app = FxIndexSet::default();
1141    let mut deferred_invalidation_dirs = FxIndexSet::default();
1142    let mut pages = FxIndexSet::default();
1143
1144    for (route_key, route) in entrypoints.routes.iter() {
1145        match route {
1146            Route::Page { .. } | Route::PageApi { .. } => {
1147                pages.insert(route_key.clone());
1148            }
1149            Route::AppPage(app_page_routes) => {
1150                let app_debug_path = to_app_debug_path(route_key.as_str(), "page");
1151                if is_deferred_app_route(route_key.as_str(), deferred_entries) {
1152                    deferred_app.insert(app_debug_path);
1153                    deferred_invalidation_dirs.extend(app_page_routes.iter().map(|route| {
1154                        app_entry_source_dir_from_original_name(route.original_name.as_str())
1155                    }));
1156                } else {
1157                    non_deferred_app.insert(app_debug_path);
1158                }
1159            }
1160            Route::AppRoute { original_name, .. } => {
1161                let app_debug_path = to_app_debug_path(route_key.as_str(), "route");
1162                if is_deferred_app_route(route_key.as_str(), deferred_entries) {
1163                    deferred_app.insert(app_debug_path);
1164                    deferred_invalidation_dirs.insert(app_entry_source_dir_from_original_name(
1165                        original_name.as_str(),
1166                    ));
1167                } else {
1168                    non_deferred_app.insert(app_debug_path);
1169                }
1170            }
1171            Route::Conflict => {}
1172        }
1173    }
1174
1175    let pages_vec = pages.into_iter().collect::<Vec<_>>();
1176    let all_app_vec = non_deferred_app
1177        .iter()
1178        .chain(deferred_app.iter())
1179        .cloned()
1180        .collect::<FxIndexSet<_>>()
1181        .into_iter()
1182        .collect::<Vec<_>>();
1183
1184    DeferredPhaseBuildPaths {
1185        non_deferred: DebugBuildPaths {
1186            app: non_deferred_app.into_iter().collect::<Vec<_>>(),
1187            pages: pages_vec.clone(),
1188        },
1189        all: DebugBuildPaths {
1190            app: all_app_vec,
1191            pages: pages_vec,
1192        },
1193        deferred_invalidation_dirs: deferred_invalidation_dirs.into_iter().collect::<Vec<_>>(),
1194    }
1195}
1196
1197async fn invalidate_deferred_entry_source_dirs_after_callback(
1198    container: ResolvedVc<ProjectContainer>,
1199    deferred_invalidation_dirs: Vec<RcStr>,
1200) -> Result<()> {
1201    if deferred_invalidation_dirs.is_empty() {
1202        return Ok(());
1203    }
1204
1205    #[turbo_tasks::value(cell = "new", eq = "manual")]
1206    struct ProjectInfo(Option<FileSystemPath>, DiskFileSystem);
1207
1208    #[turbo_tasks::function(operation, root)]
1209    async fn project_info_operation(
1210        container: ResolvedVc<ProjectContainer>,
1211    ) -> Result<Vc<ProjectInfo>> {
1212        let project = container.project();
1213        let app_dir = find_app_dir(project.project_path().owned().await?)
1214            .owned()
1215            .await?;
1216        let project_fs = project.project_fs().owned().await?;
1217        Ok(ProjectInfo(app_dir, project_fs).cell())
1218    }
1219    let ProjectInfo(app_dir, project_fs) = &*project_info_operation(container)
1220        .read_strongly_consistent()
1221        .await?;
1222
1223    let Some(app_dir) = app_dir else {
1224        return Ok(());
1225    };
1226    // Use `to_sys_path_raw` (not `to_sys_path`): these paths are compared against the invalidator
1227    // map keys inside `invalidate_path_and_children_with_reason`, which use the internal (verbatim
1228    // on Windows) representation.
1229    let app_dir_sys_path = project_fs.to_sys_path_raw(app_dir);
1230    let paths_to_invalidate = deferred_invalidation_dirs
1231        .into_iter()
1232        .map(|dir| {
1233            let normalized_dir = normalize_deferred_route(dir.as_str());
1234            let relative_dir = normalized_dir.trim_start_matches('/');
1235            if relative_dir.is_empty() {
1236                app_dir_sys_path.clone()
1237            } else {
1238                app_dir_sys_path.join(unix_to_sys(relative_dir).as_ref())
1239            }
1240        })
1241        .collect::<FxIndexSet<_>>()
1242        .into_iter()
1243        .collect::<Vec<_>>();
1244
1245    if paths_to_invalidate.is_empty() {
1246        // Fallback to full invalidation when app dir paths are unavailable.
1247        project_fs.invalidate_with_reason(|path| invalidation::Initialize {
1248            path: RcStr::from(path.to_string_lossy()),
1249        });
1250    } else {
1251        project_fs.invalidate_path_and_children_with_reason(paths_to_invalidate, |path| {
1252            invalidation::Initialize {
1253                path: RcStr::from(path.to_string_lossy()),
1254            }
1255        });
1256    }
1257
1258    Ok(())
1259}
1260
1261fn is_deferred_endpoint_group(key: &EndpointGroupKey, deferred_entries: &[RcStr]) -> bool {
1262    if deferred_entries.is_empty() {
1263        return false;
1264    }
1265
1266    let EndpointGroupKey::Route(route_key) = key else {
1267        return false;
1268    };
1269
1270    is_deferred_app_route(route_key.as_str(), deferred_entries)
1271}
1272
1273fn should_include_endpoint_group(
1274    write_phase: EntrypointsWritePhase,
1275    key: &EndpointGroupKey,
1276    deferred_entries: &[RcStr],
1277) -> bool {
1278    let is_deferred = is_deferred_endpoint_group(key, deferred_entries);
1279
1280    match write_phase {
1281        EntrypointsWritePhase::All => true,
1282        EntrypointsWritePhase::NonDeferred => !is_deferred,
1283        EntrypointsWritePhase::Deferred => is_deferred,
1284    }
1285}
1286
1287async fn app_route_filter_for_write_phase(
1288    project: Vc<Project>,
1289    write_phase: EntrypointsWritePhase,
1290    deferred_entries: &[RcStr],
1291) -> Result<Option<Vec<RcStr>>> {
1292    if matches!(write_phase, EntrypointsWritePhase::All) || deferred_entries.is_empty() {
1293        return Ok(None);
1294    }
1295
1296    let include_deferred = write_phase == EntrypointsWritePhase::Deferred;
1297    let app_project = project.app_project().await?;
1298    let app_route_keys = if let Some(app_project) = &*app_project {
1299        app_project
1300            .route_keys()
1301            .await?
1302            .iter()
1303            .cloned()
1304            .collect::<Vec<_>>()
1305    } else {
1306        Vec::new()
1307    };
1308
1309    Ok(Some(
1310        app_route_keys
1311            .iter()
1312            .filter(|route| {
1313                is_deferred_app_route(route.as_str(), deferred_entries) == include_deferred
1314            })
1315            .cloned()
1316            .collect::<Vec<_>>(),
1317    ))
1318}
1319
1320#[tracing::instrument(level = "info", name = "write all entrypoints to disk", skip_all)]
1321#[napi]
1322pub async fn project_write_all_entrypoints_to_disk(
1323    #[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External<ProjectInstance>,
1324    app_dir_only: bool,
1325) -> napi::Result<TurbopackResult<Option<NapiEntrypoints>>> {
1326    let ctx = &project.turbopack_ctx;
1327    let container = project.container;
1328    let tt = ctx.turbo_tasks();
1329
1330    #[turbo_tasks::function(operation, root)]
1331    async fn has_deferred_entrypoints_operation(
1332        container: ResolvedVc<ProjectContainer>,
1333    ) -> Result<Vc<bool>> {
1334        let project = container.project();
1335        let deferred_entries = project.deferred_entries().owned().await?;
1336
1337        if deferred_entries.is_empty() {
1338            return Ok(Vc::cell(false));
1339        }
1340
1341        let app_project = project.app_project().await?;
1342        let has_deferred = if let Some(app_project) = &*app_project {
1343            app_project
1344                .route_keys()
1345                .await?
1346                .iter()
1347                .any(|route_key| is_deferred_app_route(route_key.as_str(), &deferred_entries))
1348        } else {
1349            false
1350        };
1351
1352        Ok(Vc::cell(has_deferred))
1353    }
1354
1355    let has_deferred_entrypoints = tt
1356        .run(async move {
1357            Ok(*has_deferred_entrypoints_operation(container)
1358                .read_strongly_consistent()
1359                .await?)
1360        })
1361        .or_else(|e| ctx.throw_turbopack_internal_result(&e.into()))
1362        .await?;
1363
1364    let phase_build_paths = if has_deferred_entrypoints {
1365        Some(
1366            tt.run(async move {
1367                #[turbo_tasks::value(serialization = "skip")]
1368                struct DeferredEntrypointInfo(ReadRef<Entrypoints>, ReadRef<Vec<RcStr>>);
1369
1370                #[turbo_tasks::function(operation, root)]
1371                async fn deferred_entrypoint_info_operation(
1372                    container: ResolvedVc<ProjectContainer>,
1373                ) -> Result<Vc<DeferredEntrypointInfo>> {
1374                    let project = container.project();
1375                    Ok(DeferredEntrypointInfo(
1376                        project.entrypoints().await?,
1377                        project.deferred_entries().await?,
1378                    )
1379                    .cell())
1380                }
1381
1382                let DeferredEntrypointInfo(entrypoints, deferred_entries) =
1383                    &*deferred_entrypoint_info_operation(container)
1384                        .read_strongly_consistent()
1385                        .await?;
1386
1387                Ok(compute_deferred_phase_build_paths(
1388                    entrypoints,
1389                    deferred_entries,
1390                ))
1391            })
1392            .or_else(|e| ctx.throw_turbopack_internal_result(&e.into()))
1393            .await?,
1394        )
1395    } else {
1396        None
1397    };
1398
1399    if let Some(phase_build_paths) = phase_build_paths.as_ref() {
1400        let non_deferred_build_paths = phase_build_paths.non_deferred.clone();
1401        tt.run(async move {
1402            container
1403                .update(PartialProjectOptions {
1404                    debug_build_paths: Some(non_deferred_build_paths),
1405                    ..Default::default()
1406                })
1407                .await?;
1408            Ok(())
1409        })
1410        .or_else(|e| ctx.throw_turbopack_internal_result(&e.into()))
1411        .await?;
1412    }
1413
1414    let first_phase = if has_deferred_entrypoints {
1415        EntrypointsWritePhase::NonDeferred
1416    } else {
1417        EntrypointsWritePhase::All
1418    };
1419
1420    let (mut entrypoints, mut issues) = tt
1421        .run(async move {
1422            let entrypoints_with_issues_op = get_all_written_entrypoints_with_issues_operation(
1423                container,
1424                app_dir_only,
1425                first_phase,
1426            );
1427
1428            let read =
1429                read_strongly_consistent_and_apply_effects(entrypoints_with_issues_op, |v| {
1430                    &v.effects
1431                })
1432                .await?;
1433            let AllWrittenEntrypointsWithIssues {
1434                entrypoints,
1435                issues,
1436                ..
1437            } = &*read;
1438
1439            Ok((
1440                entrypoints.clone(),
1441                issues.iter().cloned().collect::<Vec<_>>(),
1442            ))
1443        })
1444        .or_else(|e| ctx.throw_turbopack_internal_result(&e.into()))
1445        .await?;
1446
1447    if has_deferred_entrypoints {
1448        ctx.on_before_deferred_entries().await?;
1449
1450        // onBeforeDeferredEntries can materialize deferred route source files on disk.
1451        // Build mode does not run a filesystem watcher, so force invalidation for the
1452        // deferred source subtrees before compiling deferred entrypoints.
1453        let deferred_invalidation_dirs = phase_build_paths
1454            .as_ref()
1455            .map(|paths| paths.deferred_invalidation_dirs.clone())
1456            .unwrap_or_default();
1457
1458        tt.run(async move {
1459            invalidate_deferred_entry_source_dirs_after_callback(
1460                container,
1461                deferred_invalidation_dirs,
1462            )
1463            .await?;
1464            Ok(())
1465        })
1466        .or_else(|e| ctx.throw_turbopack_internal_result(&e.into()))
1467        .await?;
1468
1469        if let Some(phase_build_paths) = phase_build_paths.as_ref() {
1470            let all_build_paths = phase_build_paths.all.clone();
1471            tt.run(async move {
1472                container
1473                    .update(PartialProjectOptions {
1474                        debug_build_paths: Some(all_build_paths),
1475                        ..Default::default()
1476                    })
1477                    .await?;
1478                Ok(())
1479            })
1480            .or_else(|e| ctx.throw_turbopack_internal_result(&e.into()))
1481            .await?;
1482        }
1483
1484        let (deferred_entrypoints, deferred_issues) = tt
1485            .run(async move {
1486                let entrypoints_with_issues_op = get_all_written_entrypoints_with_issues_operation(
1487                    container,
1488                    app_dir_only,
1489                    EntrypointsWritePhase::Deferred,
1490                );
1491
1492                let read =
1493                    read_strongly_consistent_and_apply_effects(entrypoints_with_issues_op, |v| {
1494                        &v.effects
1495                    })
1496                    .await?;
1497                let AllWrittenEntrypointsWithIssues {
1498                    entrypoints,
1499                    issues,
1500                    ..
1501                } = &*read;
1502
1503                Ok((
1504                    entrypoints.clone(),
1505                    issues.iter().cloned().collect::<Vec<_>>(),
1506                ))
1507            })
1508            .or_else(|e| ctx.throw_turbopack_internal_result(&e.into()))
1509            .await?;
1510
1511        if deferred_entrypoints.is_some() {
1512            entrypoints = deferred_entrypoints;
1513        }
1514        issues.extend(deferred_issues);
1515    }
1516
1517    let emit_issues = tt
1518        .run(async move {
1519            let emit_result_op = emit_all_output_assets_once_with_issues_operation(
1520                container,
1521                app_dir_only,
1522                has_deferred_entrypoints,
1523            );
1524            let read =
1525                read_strongly_consistent_and_apply_effects(emit_result_op, |v| &v.effects).await?;
1526            let OperationResult { issues, .. } = &*read;
1527
1528            Ok(issues.clone())
1529        })
1530        .or_else(|e| ctx.throw_turbopack_internal_result(&e.into()))
1531        .await?;
1532
1533    issues.extend(emit_issues.iter().cloned());
1534
1535    Ok(TurbopackResult {
1536        result: if let Some(entrypoints) = entrypoints {
1537            Some(NapiEntrypoints::from_entrypoints_op(
1538                &entrypoints,
1539                &project.turbopack_ctx,
1540            )?)
1541        } else {
1542            None
1543        },
1544        issues: issues.iter().map(|i| NapiIssue::from(&**i)).collect(),
1545    })
1546}
1547
1548#[turbo_tasks::function(operation, root)]
1549async fn get_all_written_entrypoints_with_issues_operation(
1550    container: ResolvedVc<ProjectContainer>,
1551    app_dir_only: bool,
1552    write_phase: EntrypointsWritePhase,
1553) -> Result<Vc<AllWrittenEntrypointsWithIssues>> {
1554    let entrypoints_operation = EntrypointsOperation::new(all_entrypoints_write_to_disk_operation(
1555        container,
1556        app_dir_only,
1557        write_phase,
1558    ));
1559    let filter = container.project().issue_filter().await?;
1560    let (entrypoints, issues, effects) =
1561        strongly_consistent_catch_collectables(entrypoints_operation, &filter).await?;
1562    Ok(AllWrittenEntrypointsWithIssues {
1563        entrypoints,
1564        issues,
1565        effects,
1566    }
1567    .cell())
1568}
1569
1570#[turbo_tasks::function(operation, root)]
1571pub async fn all_entrypoints_write_to_disk_operation(
1572    project: ResolvedVc<ProjectContainer>,
1573    app_dir_only: bool,
1574    write_phase: EntrypointsWritePhase,
1575) -> Result<Vc<Entrypoints>> {
1576    // Compute all outputs for this phase but do not emit to disk yet.
1577    let output_assets_operation = output_assets_operation(project, app_dir_only, write_phase);
1578    let _ = output_assets_operation.connect().await?;
1579
1580    Ok(project.entrypoints())
1581}
1582
1583#[turbo_tasks::function(operation)]
1584async fn output_assets_for_single_emit_operation(
1585    container: ResolvedVc<ProjectContainer>,
1586    app_dir_only: bool,
1587    has_deferred_entrypoints: bool,
1588) -> Result<Vc<OutputAssets>> {
1589    if !has_deferred_entrypoints {
1590        return Ok(
1591            output_assets_operation(container, app_dir_only, EntrypointsWritePhase::All).connect(),
1592        );
1593    }
1594
1595    let non_deferred_output_assets =
1596        output_assets_operation(container, app_dir_only, EntrypointsWritePhase::NonDeferred)
1597            .connect()
1598            .await?;
1599    let deferred_output_assets =
1600        output_assets_operation(container, app_dir_only, EntrypointsWritePhase::Deferred)
1601            .connect()
1602            .await?;
1603
1604    let merged_output_assets: FxIndexSet<ResolvedVc<Box<dyn OutputAsset>>> =
1605        non_deferred_output_assets
1606            .iter()
1607            .chain(deferred_output_assets.iter())
1608            .copied()
1609            .collect();
1610
1611    Ok(Vc::cell(merged_output_assets.into_iter().collect()))
1612}
1613
1614#[turbo_tasks::function(operation, root)]
1615async fn emit_all_output_assets_once_operation(
1616    container: ResolvedVc<ProjectContainer>,
1617    app_dir_only: bool,
1618    has_deferred_entrypoints: bool,
1619) -> Result<Vc<Entrypoints>> {
1620    let output_assets_operation =
1621        output_assets_for_single_emit_operation(container, app_dir_only, has_deferred_entrypoints);
1622    container
1623        .project()
1624        .emit_all_output_assets(output_assets_operation)
1625        .as_side_effect()
1626        .await?;
1627
1628    Ok(container.entrypoints())
1629}
1630
1631#[turbo_tasks::function(operation, root)]
1632async fn emit_all_output_assets_once_with_issues_operation(
1633    container: ResolvedVc<ProjectContainer>,
1634    app_dir_only: bool,
1635    has_deferred_entrypoints: bool,
1636) -> Result<Vc<OperationResult>> {
1637    let entrypoints_operation = EntrypointsOperation::new(emit_all_output_assets_once_operation(
1638        container,
1639        app_dir_only,
1640        has_deferred_entrypoints,
1641    ));
1642    let filter = container.project().issue_filter().await?;
1643    let (_, issues, effects) =
1644        strongly_consistent_catch_collectables(entrypoints_operation, &filter).await?;
1645
1646    Ok(OperationResult { issues, effects }.cell())
1647}
1648
1649#[turbo_tasks::function(operation)]
1650async fn output_assets_operation(
1651    container: ResolvedVc<ProjectContainer>,
1652    app_dir_only: bool,
1653    write_phase: EntrypointsWritePhase,
1654) -> Result<Vc<OutputAssets>> {
1655    let project = container.project();
1656    let deferred_entries = project.deferred_entries().owned().await?;
1657    let app_route_filter =
1658        app_route_filter_for_write_phase(project, write_phase, &deferred_entries).await?;
1659
1660    let endpoint_groups = project
1661        .get_all_endpoint_groups_with_app_route_filter(app_dir_only, app_route_filter)
1662        .await?;
1663
1664    let endpoints = endpoint_groups
1665        .iter()
1666        .filter(|(key, _)| should_include_endpoint_group(write_phase, key, &deferred_entries))
1667        .flat_map(|(_, group)| {
1668            group
1669                .primary
1670                .iter()
1671                .chain(group.additional.iter())
1672                .map(|entry| entry.endpoint)
1673        })
1674        .collect::<Vec<_>>();
1675
1676    let endpoint_assets = endpoints
1677        .iter()
1678        .map(|endpoint| async move { endpoint.output().await?.output_assets.await })
1679        .try_join()
1680        .await?;
1681
1682    let output_assets: FxIndexSet<ResolvedVc<Box<dyn OutputAsset>>> = endpoint_assets
1683        .iter()
1684        .flat_map(|assets| assets.iter().copied())
1685        .collect();
1686
1687    if write_phase == EntrypointsWritePhase::NonDeferred {
1688        return Ok(Vc::cell(output_assets.into_iter().collect()));
1689    }
1690
1691    let whole_app_module_graphs = project.whole_app_module_graphs();
1692    // This makes the trace file nicer to look at
1693    whole_app_module_graphs.as_side_effect().await?;
1694
1695    let nft = next_server_nft_assets(project).await?;
1696    let routes_hashes_manifest = routes_hashes_manifest_asset_if_enabled(project).await?;
1697    let immutable_hashes_manifest_asset =
1698        immutable_hashes_manifest_asset_if_enabled(project).await?;
1699
1700    Ok(Vc::cell(
1701        output_assets
1702            .into_iter()
1703            .chain(nft.iter().copied())
1704            .chain(routes_hashes_manifest.iter().copied())
1705            .chain(immutable_hashes_manifest_asset.iter().copied())
1706            .collect(),
1707    ))
1708}
1709
1710#[tracing::instrument(level = "info", name = "get entrypoints", skip_all)]
1711#[napi]
1712pub async fn project_entrypoints(
1713    #[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External<ProjectInstance>,
1714) -> napi::Result<TurbopackResult<Option<NapiEntrypoints>>> {
1715    let container = project.container;
1716
1717    let (entrypoints, issues) = project
1718        .turbopack_ctx
1719        .turbo_tasks()
1720        .run_once(async move {
1721            let entrypoints_with_issues_op = get_entrypoints_with_issues_operation(container);
1722
1723            // Read and compile the files
1724            let EntrypointsWithIssues {
1725                entrypoints,
1726                issues,
1727                effects: _,
1728            } = &*entrypoints_with_issues_op
1729                .read_strongly_consistent()
1730                .await?;
1731
1732            Ok((entrypoints.clone(), issues.clone()))
1733        })
1734        .await
1735        .map_err(|e| napi::Error::from_reason(PrettyPrintError(&e).to_string()))?;
1736
1737    let result = match entrypoints {
1738        Some(entrypoints) => Some(NapiEntrypoints::from_entrypoints_op(
1739            &entrypoints,
1740            &project.turbopack_ctx,
1741        )?),
1742        None => None,
1743    };
1744
1745    Ok(TurbopackResult {
1746        result,
1747        issues: issues.iter().map(|i| NapiIssue::from(&**i)).collect(),
1748    })
1749}
1750
1751#[tracing::instrument(level = "info", name = "subscribe to entrypoints", skip_all)]
1752#[napi(ts_return_type = "{ __napiType: \"RootTask\" }")]
1753pub fn project_entrypoints_subscribe(
1754    #[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External<ProjectInstance>,
1755    func: JsFunction,
1756) -> napi::Result<External<RootTask>> {
1757    let turbopack_ctx = project.turbopack_ctx.clone();
1758    let container = project.container;
1759    subscribe(
1760        turbopack_ctx.clone(),
1761        func,
1762        move || {
1763            async move {
1764                let entrypoints_with_issues_op = get_entrypoints_with_issues_operation(container);
1765                let read =
1766                    read_strongly_consistent_and_apply_effects(entrypoints_with_issues_op, |v| {
1767                        &v.effects
1768                    })
1769                    .await?;
1770                let EntrypointsWithIssues {
1771                    entrypoints,
1772                    issues,
1773                    ..
1774                } = &*read;
1775                Ok((entrypoints.clone(), issues.clone()))
1776            }
1777            .instrument(tracing::info_span!("entrypoints subscription"))
1778        },
1779        move |ctx| {
1780            let (entrypoints, issues) = ctx.value;
1781            let result = match entrypoints {
1782                Some(entrypoints) => Some(NapiEntrypoints::from_entrypoints_op(
1783                    &entrypoints,
1784                    &turbopack_ctx,
1785                )?),
1786                None => None,
1787            };
1788
1789            Ok(vec![TurbopackResult {
1790                result,
1791                issues: issues
1792                    .iter()
1793                    .map(|issue| NapiIssue::from(&**issue))
1794                    .collect(),
1795            }])
1796        },
1797    )
1798}
1799
1800#[turbo_tasks::value(serialization = "skip")]
1801struct HmrUpdateWithIssues {
1802    update: ReadRef<Update>,
1803    issues: Arc<Vec<ReadRef<PlainIssue>>>,
1804    effects: Arc<Effects>,
1805}
1806
1807#[turbo_tasks::function(operation, root)]
1808fn project_hmr_update_operation(
1809    project: ResolvedVc<Project>,
1810    chunk_name: RcStr,
1811    target: HmrTarget,
1812    state: ResolvedVc<VersionState>,
1813) -> Vc<Update> {
1814    project.hmr_update(chunk_name, target, *state)
1815}
1816
1817#[tracing::instrument(
1818    level = "info",
1819    name = "hmr subscription",
1820    skip_all,
1821    fields(chunk_name = %chunk_name, target = %target),
1822)]
1823#[turbo_tasks::function(operation, root)]
1824async fn hmr_update_with_issues_operation(
1825    project: ResolvedVc<Project>,
1826    chunk_name: RcStr,
1827    state: ResolvedVc<VersionState>,
1828    target: HmrTarget,
1829) -> Result<Vc<HmrUpdateWithIssues>> {
1830    tracing::info!(chunk_name = %chunk_name, target = %target, "hmr subscription");
1831    let update_op = project_hmr_update_operation(project, chunk_name, target, state);
1832    // NOTE: we do not use `strongly_consistent_catch_collectables` here. The JS HMR
1833    // consumers in `hot-reloader-turbopack.ts` (`subscribeToServerHmr` and
1834    // `subscribeToClientHmrEvents`) rely on this read *throwing* on build-graph
1835    // failures to trigger their recovery paths
1836    let update = update_op.read_strongly_consistent().await?;
1837    let filter = project.issue_filter().await?;
1838    let issues = get_issues(update_op, &filter).await?;
1839    let effects = Arc::new(take_effects(update_op).await?);
1840    Ok(HmrUpdateWithIssues {
1841        update,
1842        issues,
1843        effects,
1844    }
1845    .cell())
1846}
1847
1848#[tracing::instrument(level = "info", name = "get HMR events", skip(project, func), fields(target = %target, chunk_name = %chunk_name))]
1849#[napi(ts_return_type = "{ __napiType: \"RootTask\" }")]
1850pub fn project_hmr_events(
1851    #[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External<ProjectInstance>,
1852    chunk_name: RcStr,
1853    target: String,
1854    func: JsFunction,
1855) -> napi::Result<External<RootTask>> {
1856    let hmr_target = target
1857        .parse::<HmrTarget>()
1858        .map_err(napi::Error::from_reason)?;
1859
1860    let container = project.container;
1861    let session = TransientInstance::new(());
1862    subscribe(
1863        project.turbopack_ctx.clone(),
1864        func,
1865        {
1866            let outer_chunk_name = chunk_name.clone();
1867            let session = session.clone();
1868            move || {
1869                let chunk_name: RcStr = outer_chunk_name.clone();
1870                let session = session.clone();
1871                async move {
1872                    // HACK(bgw): Remove this unmark call
1873                    unmark_top_level_task_may_leak_eventually_consistent_state();
1874                    let project = container.project().to_resolved().await?;
1875                    let state = project
1876                        .hmr_version_state(chunk_name.clone(), hmr_target, session)
1877                        .to_resolved()
1878                        .await?;
1879
1880                    let update_op = hmr_update_with_issues_operation(
1881                        project,
1882                        chunk_name.clone(),
1883                        state,
1884                        hmr_target,
1885                    );
1886                    // HACK(bgw): Remove this mark call
1887                    mark_top_level_task();
1888                    let read =
1889                        read_strongly_consistent_and_apply_effects(update_op, |v| &v.effects)
1890                            .await?;
1891                    // HACK(bgw): Remove this unmark call
1892                    unmark_top_level_task_may_leak_eventually_consistent_state();
1893                    let HmrUpdateWithIssues { update, issues, .. } = &*read;
1894                    match &**update {
1895                        Update::Missing | Update::None => {}
1896                        Update::Total(TotalUpdate { to }) => {
1897                            state.set(to.clone()).await?;
1898                        }
1899                        Update::Partial(PartialUpdate { to, .. }) => {
1900                            state.set(to.clone()).await?;
1901                        }
1902                    }
1903                    Ok((Some(update.clone()), issues.clone()))
1904                }
1905            }
1906        },
1907        move |ctx| {
1908            let (update, issues) = ctx.value;
1909
1910            let napi_issues = issues
1911                .iter()
1912                .map(|issue| NapiIssue::from(&**issue))
1913                .collect();
1914            let update_issues = issues
1915                .iter()
1916                .map(|issue| Issue::from(&**issue))
1917                .collect::<Vec<_>>();
1918
1919            let identifier = ResourceIdentifier {
1920                path: chunk_name.clone(),
1921                headers: None,
1922            };
1923            let update = match update.as_deref() {
1924                None | Some(Update::Missing) | Some(Update::Total(_)) => {
1925                    ClientUpdateInstruction::restart(&identifier, &update_issues)
1926                }
1927                Some(Update::Partial(update)) => ClientUpdateInstruction::partial(
1928                    &identifier,
1929                    &update.instruction,
1930                    &update_issues,
1931                ),
1932                Some(Update::None) => ClientUpdateInstruction::issues(&identifier, &update_issues),
1933            };
1934
1935            Ok(vec![TurbopackResult {
1936                result: ctx.env.to_js_value(&update)?,
1937                issues: napi_issues,
1938            }])
1939        },
1940    )
1941}
1942
1943#[napi(object)]
1944struct HmrChunkNames {
1945    pub chunk_names: Vec<RcStr>,
1946}
1947
1948#[turbo_tasks::value(serialization = "skip")]
1949struct HmrChunkNamesWithIssues {
1950    chunk_names: ReadRef<Vec<RcStr>>,
1951    issues: Arc<Vec<ReadRef<PlainIssue>>>,
1952    effects: Arc<Effects>,
1953}
1954
1955#[turbo_tasks::function(operation, root)]
1956fn project_hmr_chunk_names_operation(
1957    container: ResolvedVc<ProjectContainer>,
1958    target: HmrTarget,
1959) -> Vc<Vec<RcStr>> {
1960    container.hmr_chunk_names(target)
1961}
1962
1963#[turbo_tasks::function(operation, root)]
1964async fn get_hmr_chunk_names_with_issues_operation(
1965    container: ResolvedVc<ProjectContainer>,
1966    target: HmrTarget,
1967) -> Result<Vc<HmrChunkNamesWithIssues>> {
1968    let hmr_chunk_names_op = project_hmr_chunk_names_operation(container, target);
1969    // Do NOT switch this to `strongly_consistent_catch_collectables`. The JS HMR
1970    // chunk-names consumer in `hot-reloader-turbopack.ts` relies on this read
1971    // *throwing* on build-graph failures so its outer `try` block exits the
1972    // subscription loop. Swallowing the error and emitting an empty chunk-name
1973    // list keeps the loop running but with stale state, and obscures the real
1974    // failure from the dev server log.
1975    let hmr_chunk_names = hmr_chunk_names_op.read_strongly_consistent().await?;
1976    let filter = container.project().issue_filter().await?;
1977    let issues = get_issues(hmr_chunk_names_op, &filter).await?;
1978    let effects = Arc::new(take_effects(hmr_chunk_names_op).await?);
1979    Ok(HmrChunkNamesWithIssues {
1980        chunk_names: hmr_chunk_names,
1981        issues,
1982        effects,
1983    }
1984    .cell())
1985}
1986
1987#[tracing::instrument(level = "info", name = "get HMR chunk names", skip(project, func), fields(target = %target))]
1988#[napi(ts_return_type = "{ __napiType: \"RootTask\" }")]
1989pub fn project_hmr_chunk_names_subscribe(
1990    #[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External<ProjectInstance>,
1991    target: String,
1992    func: JsFunction,
1993) -> napi::Result<External<RootTask>> {
1994    let hmr_target = target
1995        .parse::<HmrTarget>()
1996        .map_err(napi::Error::from_reason)?;
1997
1998    let container = project.container;
1999    subscribe(
2000        project.turbopack_ctx.clone(),
2001        func,
2002        move || async move {
2003            let hmr_chunk_names_with_issues_op =
2004                get_hmr_chunk_names_with_issues_operation(container, hmr_target);
2005            let read =
2006                read_strongly_consistent_and_apply_effects(hmr_chunk_names_with_issues_op, |v| {
2007                    &v.effects
2008                })
2009                .await?;
2010            let HmrChunkNamesWithIssues {
2011                chunk_names,
2012                issues,
2013                ..
2014            } = &*read;
2015
2016            Ok((chunk_names.clone(), issues.clone()))
2017        },
2018        move |ctx| {
2019            let (chunk_names, issues) = ctx.value;
2020
2021            Ok(vec![TurbopackResult {
2022                result: HmrChunkNames {
2023                    chunk_names: ReadRef::into_owned(chunk_names),
2024                },
2025                issues: issues
2026                    .iter()
2027                    .map(|issue| NapiIssue::from(&**issue))
2028                    .collect(),
2029            }])
2030        },
2031    )
2032}
2033
2034pub enum UpdateMessage {
2035    Start,
2036    End(UpdateInfo),
2037}
2038
2039#[napi(object)]
2040struct NapiUpdateMessage {
2041    pub update_type: &'static str,
2042    pub value: Option<NapiUpdateInfo>,
2043}
2044
2045impl From<UpdateMessage> for NapiUpdateMessage {
2046    fn from(update_message: UpdateMessage) -> Self {
2047        match update_message {
2048            UpdateMessage::Start => NapiUpdateMessage {
2049                update_type: "start",
2050                value: None,
2051            },
2052            UpdateMessage::End(info) => NapiUpdateMessage {
2053                update_type: "end",
2054                value: Some(info.into()),
2055            },
2056        }
2057    }
2058}
2059
2060#[napi(object)]
2061struct NapiUpdateInfo {
2062    pub duration: u32,
2063    pub tasks: u32,
2064}
2065
2066impl From<UpdateInfo> for NapiUpdateInfo {
2067    fn from(update_info: UpdateInfo) -> Self {
2068        Self {
2069            duration: update_info.duration.as_millis() as u32,
2070            tasks: update_info.tasks as u32,
2071        }
2072    }
2073}
2074
2075/// Subscribes to lifecycle events of the compilation.
2076///
2077/// Emits an [UpdateMessage::Start] event when any computation starts.
2078/// Emits an [UpdateMessage::End] event when there was no computation for the
2079/// specified time (`aggregation_ms`). The [UpdateMessage::End] event contains
2080/// information about the computations that happened since the
2081/// [UpdateMessage::Start] event. It contains the duration of the computation
2082/// (excluding the idle time that was spend waiting for `aggregation_ms`), and
2083/// the number of tasks that were executed.
2084///
2085/// The signature of the `func` is `(update_message: UpdateMessage) => void`.
2086#[napi]
2087pub fn project_update_info_subscribe(
2088    #[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External<ProjectInstance>,
2089    aggregation_ms: u32,
2090    func: JsFunction,
2091) -> napi::Result<()> {
2092    let func: ThreadsafeFunction<UpdateMessage> = func.create_threadsafe_function(0, |ctx| {
2093        let message = ctx.value;
2094        Ok(vec![NapiUpdateMessage::from(message)])
2095    })?;
2096    tokio::spawn(async move {
2097        let tt = project.turbopack_ctx.turbo_tasks();
2098        loop {
2099            let update_info = tt
2100                .aggregated_update_info(Duration::ZERO, Duration::ZERO)
2101                .await;
2102
2103            func.call(
2104                Ok(UpdateMessage::Start),
2105                ThreadsafeFunctionCallMode::NonBlocking,
2106            );
2107
2108            let update_info = match update_info {
2109                Some(update_info) => update_info,
2110                None => {
2111                    tt.get_or_wait_aggregated_update_info(Duration::from_millis(
2112                        aggregation_ms.into(),
2113                    ))
2114                    .await
2115                }
2116            };
2117
2118            let status = func.call(
2119                Ok(UpdateMessage::End(update_info)),
2120                ThreadsafeFunctionCallMode::NonBlocking,
2121            );
2122
2123            if !matches!(status, Status::Ok) {
2124                let error = anyhow!("Error calling JS function: {}", status);
2125                eprintln!("{error}");
2126                break;
2127            }
2128        }
2129    });
2130    Ok(())
2131}
2132
2133/// Subscribes to all compilation events that are not cached like timing and progress information.
2134#[napi]
2135pub fn project_compilation_events_subscribe(
2136    #[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External<ProjectInstance>,
2137    func: JsFunction,
2138    event_types: Option<Vec<String>>,
2139) -> napi::Result<()> {
2140    let tsfn: ThreadsafeFunction<Arc<dyn CompilationEvent>> =
2141        func.create_threadsafe_function(0, |ctx| {
2142            let event: Arc<dyn CompilationEvent> = ctx.value;
2143
2144            let env = ctx.env;
2145            let mut obj = env.create_object()?;
2146            obj.set_named_property("typeName", event.type_name())?;
2147            obj.set_named_property("severity", event.severity().to_string())?;
2148            obj.set_named_property("message", event.message())?;
2149            obj.set_named_property("eventJson", event.to_json())?;
2150
2151            let external = env.create_external(event, None);
2152            obj.set_named_property("eventData", external)?;
2153
2154            Ok(vec![obj])
2155        })?;
2156
2157    tokio::spawn(async move {
2158        let tt = project.turbopack_ctx.turbo_tasks();
2159        let mut receiver = tt.subscribe_to_compilation_events(event_types);
2160        while let Some(msg) = receiver.recv().await {
2161            let status = tsfn.call(Ok(msg), ThreadsafeFunctionCallMode::Blocking);
2162
2163            if status != Status::Ok {
2164                break;
2165            }
2166        }
2167        // Signal the JS side that the subscription has ended (e.g. after
2168        // project shutdown drops all senders).  This allows the async
2169        // iterator to exit promptly instead of hanging forever.
2170        let _ = tsfn.call(
2171            Err(napi::Error::new(
2172                Status::Cancelled,
2173                "compilation events subscription closed",
2174            )),
2175            ThreadsafeFunctionCallMode::Blocking,
2176        );
2177    });
2178
2179    Ok(())
2180}
2181
2182#[napi(object)]
2183#[turbo_tasks::task_input]
2184#[derive(Clone, Debug, Eq, Hash, OperationValue, PartialEq, TraceRawVcs, Encode, Decode)]
2185pub struct StackFrame {
2186    pub is_server: bool,
2187    pub is_ignored: Option<bool>,
2188    pub original_file: Option<RcStr>,
2189    pub file: RcStr,
2190    /// 1-indexed, unlike source map tokens
2191    pub line: Option<u32>,
2192    /// 1-indexed, unlike source map tokens
2193    pub column: Option<u32>,
2194    pub method_name: Option<RcStr>,
2195}
2196
2197#[turbo_tasks::value(transparent)]
2198#[derive(Clone)]
2199pub struct OptionStackFrame(Option<StackFrame>);
2200
2201/// Parses a stack-frame source URL — either a `file://` URL (with an optional `id` query parameter
2202/// identifying the module) or a raw system path — into a system path and an optional module id,
2203/// for use with [`get_source_map_rope`].
2204///
2205/// The returned path is canonicalized (with a lexical fallback if the file no longer exists).
2206/// Canonicalization (not just lexical normalization) is needed because the path is later compared
2207/// against paths derived from the canonicalized project root, and two spellings of a path only
2208/// compare equal once both are reduced to the same canonical form. Canonicalization performs
2209/// untracked filesystem reads, so this must be called from the napi layer, outside of any (cached)
2210/// turbo-tasks function.
2211fn parse_and_canonicalize_source_url(source_url: &str) -> Result<(RcStr, Option<RcStr>)> {
2212    let (path, module) = match Url::parse(source_url) {
2213        Ok(url) => match url.scheme() {
2214            "file" => {
2215                let Ok(path) = url.to_file_path() else {
2216                    bail!("Failed to convert file URL to file path: {url}");
2217                };
2218                let module = url.query_pairs().find(|(k, _)| k == "id");
2219                (
2220                    path,
2221                    match module {
2222                        Some(module) => Some(urlencoding::decode(&module.1)?.into_owned().into()),
2223                        None => None,
2224                    },
2225                )
2226            }
2227            _ => bail!("Unknown url scheme '{}'", url.scheme()),
2228        },
2229        Err(_) => (PathBuf::from(source_url), None),
2230    };
2231
2232    // Canonicalization resolves symlinks and (on Windows) yields a verbatim path with a
2233    // case-folded drive letter and on-disk casing, matching the canonicalized project root.
2234    let path = match canonicalize(&path) {
2235        Ok(canonical) => canonical,
2236        Err(_) => {
2237            // The file may not exist (e.g. a stale stack frame). Fall back to a purely lexical
2238            // normalization that approximates the canonical format.
2239            if cfg!(windows) {
2240                to_verbatim_with_case_folded_disk(&path).unwrap_or(path)
2241            } else {
2242                path
2243            }
2244        }
2245    };
2246
2247    let path = path
2248        .into_string()
2249        .map(RcStr::from)
2250        .map_err(|p| anyhow!("path {p:?} is not valid unicode"))?;
2251    Ok((path, module))
2252}
2253
2254/// `file_path_sys` and `module` must be produced by [`parse_and_canonicalize_source_url`], which
2255/// canonicalizes the path. That must happen in the napi layer: this is a cached turbo-tasks
2256/// function, so it cannot perform untracked reads like canonicalization itself.
2257#[turbo_tasks::function]
2258async fn get_source_map_rope(
2259    container: Vc<ProjectContainer>,
2260    sys_path: RcStr,
2261    module: Option<RcStr>,
2262) -> Result<Vc<FileContent>> {
2263    let sys_path = Path::new(&*sys_path);
2264
2265    let project = container.project();
2266    let output_fs = project.output_fs().to_resolved().await?;
2267    let Some(fs_path) = output_fs
2268        .await?
2269        .try_from_sys_path(output_fs, sys_path, None)
2270    else {
2271        // The path is outside of the filesystem root
2272        return Ok(FileContent::NotFound.cell());
2273    };
2274
2275    let Some(chunk_base_unix) = project.node_root().await?.get_path_to(&fs_path) else {
2276        // The path is not within the dist dir
2277        return Ok(FileContent::NotFound.cell());
2278    };
2279
2280    let client_path = project
2281        .client_relative_path()
2282        .await?
2283        .join(chunk_base_unix)?;
2284
2285    // `fs_path` is the server path: it's inside `node_root`, and `output_fs` is the filesystem
2286    // that `node_root` uses.
2287    let mut map = container.get_source_map(fs_path, module.clone());
2288
2289    if !map.await?.is_content() {
2290        // If the chunk doesn't exist as a server chunk, try a client chunk.
2291        // TODO: Properly tag all server chunks and use the `isServer` query param.
2292        // Currently, this is inaccurate as it does not cover RSC server
2293        // chunks.
2294        map = container.get_source_map(client_path, module.clone());
2295        if !map.await?.is_content() {
2296            bail!("chunk/module {sys_path:?} (module: {module:?}) is missing a sourcemap");
2297        }
2298    }
2299
2300    Ok(map)
2301}
2302
2303/// See [`get_source_map_rope`]: `file_path_sys` and `module` must be produced by
2304/// [`parse_and_canonicalize_source_url`] in the napi layer.
2305#[turbo_tasks::function(operation, root)]
2306fn get_source_map_rope_operation(
2307    container: ResolvedVc<ProjectContainer>,
2308    file_path_sys: RcStr,
2309    module: Option<RcStr>,
2310) -> Vc<FileContent> {
2311    get_source_map_rope(*container, file_path_sys, module)
2312}
2313
2314/// `frame_file_path_sys` and `frame_module` are the normalized form of `frame.file`, and must be
2315/// produced by [`parse_and_canonicalize_source_url`] in the napi layer (see
2316/// [`get_source_map_rope`]).
2317#[turbo_tasks::function(operation, root)]
2318async fn project_trace_source_operation(
2319    container: ResolvedVc<ProjectContainer>,
2320    frame: StackFrame,
2321    frame_file_path_sys: RcStr,
2322    frame_module: Option<RcStr>,
2323    current_directory_file_url: RcStr,
2324) -> Result<Vc<OptionStackFrame>> {
2325    let Some(map) = &*SourceMap::new_from_rope_cached(get_source_map_rope(
2326        *container,
2327        frame_file_path_sys,
2328        frame_module,
2329    ))
2330    .await?
2331    else {
2332        return Ok(Vc::cell(None));
2333    };
2334
2335    let Some(line) = frame.line else {
2336        return Ok(Vc::cell(None));
2337    };
2338
2339    let token = map.lookup_token(
2340        line.saturating_sub(1),
2341        frame.column.unwrap_or(1).saturating_sub(1),
2342    );
2343
2344    let (original_file, line, column, method_name, is_ignored) = match token {
2345        Token::Original(token) => (
2346            // Still percent-encoded, like the URIs it's compared against.
2347            token.original_file,
2348            // JS stack frames are 1-indexed, source map tokens are 0-indexed
2349            Some(token.original_line + 1),
2350            Some(token.original_column + 1),
2351            token.name,
2352            token.is_ignored,
2353        ),
2354        Token::Synthetic(token) => {
2355            let Some(original_file) = token.guessed_original_file else {
2356                return Ok(Vc::cell(None));
2357            };
2358            (original_file, None, None, None, false)
2359        }
2360    };
2361
2362    // Turns a percent-encoded URI fragment back into a path for output.
2363    fn decode_uri_fragment(value: &str) -> Result<RcStr> {
2364        Ok(match urlencoding::decode(value)? {
2365            Cow::Borrowed(borrowed) => RcStr::from(borrowed),
2366            Cow::Owned(owned) => RcStr::from(owned),
2367        })
2368    }
2369
2370    let project_root_uri =
2371        uri_from_file(container.project().project_root_path().owned().await?, None).await? + "/";
2372    // Relative paths are computed on decoded inputs: they come from
2373    // different encoders that disagree on characters like `[` vs `%5B`.
2374    let current_directory_path = decode_uri_fragment(&current_directory_file_url)?;
2375    let (file, original_file) =
2376        if let Some(source_file) = original_file.strip_prefix(&project_root_uri) {
2377            // Client code uses file://
2378            (
2379                RcStr::from(
2380                    get_relative_path_to(
2381                        &current_directory_path,
2382                        &decode_uri_fragment(&original_file)?,
2383                    )
2384                    // TODO(sokra) remove this to include a ./ here to make it a relative path
2385                    .trim_start_matches("./"),
2386                ),
2387                Some(decode_uri_fragment(source_file)?),
2388            )
2389        } else if let Some(source_file) = original_file.strip_prefix(&*SOURCE_MAP_PREFIX_PROJECT) {
2390            // Server code uses turbopack:///[project]
2391            // TODO should this also be file://?
2392            let source_file = decode_uri_fragment(source_file)?;
2393            (
2394                RcStr::from(
2395                    get_relative_path_to(
2396                        &current_directory_path,
2397                        &format!("{}{}", decode_uri_fragment(&project_root_uri)?, source_file),
2398                    )
2399                    // TODO(sokra) remove this to include a ./ here to make it a relative path
2400                    .trim_start_matches("./"),
2401                ),
2402                Some(source_file),
2403            )
2404        } else if let Some(source_file) = original_file.strip_prefix(&*SOURCE_MAP_PREFIX) {
2405            // TODO(veil): Should the protocol be preserved?
2406            (decode_uri_fragment(source_file)?, None)
2407        } else {
2408            bail!(
2409                "Original file ({}) outside project ({})",
2410                original_file,
2411                project_root_uri
2412            )
2413        };
2414
2415    Ok(Vc::cell(Some(StackFrame {
2416        file,
2417        original_file,
2418        method_name,
2419        line,
2420        column,
2421        is_server: frame.is_server,
2422        is_ignored: Some(is_ignored),
2423    })))
2424}
2425
2426#[tracing::instrument(level = "info", name = "apply SourceMap to stack frame", skip_all)]
2427#[napi]
2428pub async fn project_trace_source(
2429    #[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External<ProjectInstance>,
2430    frame: StackFrame,
2431    current_directory_file_url: String,
2432) -> napi::Result<Option<StackFrame>> {
2433    let container = project.container;
2434    let ctx = &project.turbopack_ctx;
2435    // Normalization canonicalizes (an untracked read), so it must happen here, outside of the
2436    // cached turbo-tasks functions below.
2437    let (frame_file_path_sys, frame_module) = parse_and_canonicalize_source_url(&frame.file)
2438        .map_err(|e| napi::Error::from_reason(PrettyPrintError(&e).to_string()))?;
2439    ctx.turbo_tasks()
2440        .run(async move {
2441            let traced_frame = project_trace_source_operation(
2442                container,
2443                frame,
2444                frame_file_path_sys,
2445                frame_module,
2446                RcStr::from(current_directory_file_url),
2447            )
2448            .read_strongly_consistent()
2449            .await?;
2450            Ok(ReadRef::into_owned(traced_frame))
2451        })
2452        // HACK: Don't use `TurbopackInternalError`, this function is race-condition prone (the
2453        // source files may have changed or been deleted), so these probably aren't internal errors?
2454        // Ideally we should differentiate.
2455        .await
2456        .map_err(|e| napi::Error::from_reason(PrettyPrintError(&e.into()).to_string()))
2457}
2458
2459#[tracing::instrument(level = "info", name = "get source content for asset", skip_all)]
2460#[napi]
2461pub async fn project_get_source_for_asset(
2462    #[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External<ProjectInstance>,
2463    file_path: RcStr,
2464) -> napi::Result<Option<String>> {
2465    let container = project.container;
2466    let ctx = &project.turbopack_ctx;
2467    ctx.turbo_tasks()
2468        .run(async move {
2469            #[turbo_tasks::function(operation, root)]
2470            async fn source_content_operation(
2471                container: ResolvedVc<ProjectContainer>,
2472                file_path: RcStr,
2473            ) -> Result<Vc<FileContent>> {
2474                let project_path = container.project().project_path().await?;
2475                Ok(project_path.fs().root().await?.join(&file_path)?.read())
2476            }
2477
2478            let source_content = &*source_content_operation(container, file_path.clone())
2479                .read_strongly_consistent()
2480                .await?;
2481
2482            let FileContent::Content(source_content) = source_content else {
2483                bail!("Cannot find source for asset {}", file_path);
2484            };
2485
2486            Ok(Some(source_content.content().to_str()?.into_owned()))
2487        })
2488        // HACK: Don't use `TurbopackInternalError`, this function is race-condition prone (the
2489        // source files may have changed or been deleted), so these probably aren't internal errors?
2490        // Ideally we should differentiate.
2491        .await
2492        .map_err(|e| napi::Error::from_reason(PrettyPrintError(&e.into()).to_string()))
2493}
2494
2495#[tracing::instrument(level = "info", name = "get SourceMap for asset", skip_all)]
2496#[napi]
2497pub async fn project_get_source_map(
2498    #[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External<ProjectInstance>,
2499    source_map_url: RcStr,
2500) -> napi::Result<Option<String>> {
2501    let container = project.container;
2502    let ctx = &project.turbopack_ctx;
2503    // Normalization canonicalizes (an untracked read), so it must happen here, outside of the
2504    // cached turbo-tasks functions below.
2505    let (file_path_sys, module) = parse_and_canonicalize_source_url(&source_map_url)
2506        .map_err(|e| napi::Error::from_reason(PrettyPrintError(&e).to_string()))?;
2507    ctx.turbo_tasks()
2508        .run(async move {
2509            let source_map = get_source_map_rope_operation(container, file_path_sys, module)
2510                .read_strongly_consistent()
2511                .await?;
2512            let Some(map) = source_map.as_content() else {
2513                return Ok(None);
2514            };
2515            Ok(Some(map.content().to_str()?.to_string()))
2516        })
2517        // HACK: Don't use `TurbopackInternalError`, this function is race-condition prone (the
2518        // source files may have changed or been deleted), so these probably aren't internal errors?
2519        // Ideally we should differentiate.
2520        .await
2521        .map_err(|e| napi::Error::from_reason(PrettyPrintError(&e.into()).to_string()))
2522}
2523
2524#[napi]
2525pub fn project_get_source_map_sync(
2526    #[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External<ProjectInstance>,
2527    file_path: RcStr,
2528) -> napi::Result<Option<String>> {
2529    within_runtime_if_available(|| {
2530        tokio::runtime::Handle::current().block_on(project_get_source_map(project, file_path))
2531    })
2532}
2533
2534#[napi]
2535pub async fn project_write_analyze_data(
2536    #[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External<ProjectInstance>,
2537    app_dir_only: bool,
2538) -> napi::Result<TurbopackResult<()>> {
2539    let container = project.container;
2540    let issues = project
2541        .turbopack_ctx
2542        .turbo_tasks()
2543        .run_once(async move {
2544            let analyze_data_op = write_analyze_data_with_issues_operation(container, app_dir_only);
2545            // Write the files to disk
2546            let read =
2547                read_strongly_consistent_and_apply_effects(analyze_data_op, |v| &v.effects).await?;
2548            let WriteAnalyzeResult { issues, .. } = &*read;
2549            Ok(issues.clone())
2550        })
2551        .await
2552        .map_err(|e| napi::Error::from_reason(PrettyPrintError(&e).to_string()))?;
2553
2554    Ok(TurbopackResult {
2555        result: (),
2556        issues: issues.iter().map(|i| NapiIssue::from(&**i)).collect(),
2557    })
2558}
2559
2560#[turbo_tasks::function(operation, root)]
2561async fn get_all_compilation_issues_inner_operation(
2562    container: ResolvedVc<ProjectContainer>,
2563) -> Result<Vc<()>> {
2564    let project = container.project();
2565    // Build the whole app module graph without chunking, code gen, or disk emission.
2566    // We use whole_app_module_graphs_without_dropping_issues() instead of
2567    // whole_app_module_graphs() because the latter drops issues in development mode
2568    // (to avoid duplicate per-route HMR noise). The non-dropping variant ensures issues
2569    // like missing modules and transform errors are properly collected as collectables here.
2570    project
2571        .whole_app_module_graphs_without_dropping_issues()
2572        .as_side_effect()
2573        .await?;
2574    Ok(Vc::cell(()))
2575}
2576
2577#[turbo_tasks::function(operation, root)]
2578async fn get_all_compilation_issues_operation(
2579    container: ResolvedVc<ProjectContainer>,
2580) -> Result<Vc<OperationResult>> {
2581    let inner_op = get_all_compilation_issues_inner_operation(container);
2582    let filter = container.project().issue_filter().await?;
2583    let (_, issues, effects) = strongly_consistent_catch_collectables(inner_op, &filter).await?;
2584    Ok(OperationResult { issues, effects }.cell())
2585}
2586
2587/// Returns the build-feature-usage telemetry summary for this project — the set of
2588/// `(featureName, invocationCount)` pairs reported to the Next.js telemetry service.
2589///
2590/// Intended to be called once at the end of a build, after `writeAllEntrypointsToDisk`. The
2591/// summary is computed by walking the whole-app module graph and is cached by turbo-tasks, so the
2592/// call is cheap when the graph is already materialized.
2593#[tracing::instrument(level = "info", name = "get project feature usage", skip_all)]
2594#[napi]
2595pub async fn project_feature_usage(
2596    #[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External<ProjectInstance>,
2597) -> napi::Result<Vec<NapiUsedFeature>> {
2598    let container = project.container;
2599    let summary = project
2600        .turbopack_ctx
2601        .turbo_tasks()
2602        .run_once(async move {
2603            #[turbo_tasks::function(operation, root)]
2604            async fn project_feature_usage_operation(
2605                container: ResolvedVc<ProjectContainer>,
2606            ) -> Result<Vc<ProjectFeatureUsageSummary>> {
2607                Ok(container.project().project_feature_usage())
2608            }
2609            project_feature_usage_operation(container)
2610                .read_strongly_consistent()
2611                .await
2612        })
2613        .await
2614        .map_err(|e| napi::Error::from_reason(PrettyPrintError(&e).to_string()))?;
2615
2616    Ok(summary
2617        .features
2618        .iter()
2619        .map(|(name, count)| NapiUsedFeature::new(name.clone(), *count))
2620        .collect())
2621}
2622
2623#[tracing::instrument(level = "info", name = "get all compilation issues", skip_all)]
2624#[napi]
2625pub async fn project_get_all_compilation_issues(
2626    #[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External<ProjectInstance>,
2627) -> napi::Result<TurbopackResult<()>> {
2628    let container = project.container;
2629    let issues = project
2630        .turbopack_ctx
2631        .turbo_tasks()
2632        .run_once(async move {
2633            let op = get_all_compilation_issues_operation(container);
2634            let OperationResult { issues, effects: _ } = &*op.read_strongly_consistent().await?;
2635            Ok(issues.clone())
2636        })
2637        .await
2638        .map_err(|e| napi::Error::from_reason(PrettyPrintError(&e).to_string()))?;
2639
2640    Ok(TurbopackResult {
2641        result: (),
2642        issues: issues.iter().map(|i| NapiIssue::from(&**i)).collect(),
2643    })
2644}
2645
2646/// Opens the Turbopack persistent cache database at the given path and performs a full compaction.
2647///
2648/// The `path` should point to the `<distDir>/cache/turbopack` directory.
2649#[napi]
2650pub async fn turbopack_database_compact(path: String, next_version: String) -> napi::Result<()> {
2651    let describe = crate::next_api::turbopack_ctx::cache_describe(&next_version);
2652    let version_info = crate::next_api::turbopack_ctx::git_version_info(&describe);
2653    let is_ci = std::env::var("CI").is_ok_and(|v| !v.is_empty());
2654    turbo_tasks_backend::compact_database(&PathBuf::from(path), &version_info, is_ci)
2655        .map_err(|e| napi::Error::from_reason(format!("Database compaction failed: {e}")))?;
2656    Ok(())
2657}