next_swc_napi/next_api/
project.rs

1use std::{borrow::Cow, io::Write, path::PathBuf, sync::Arc, thread, time::Duration};
2
3use anyhow::{Context, Result, anyhow, bail};
4use bincode::{Decode, Encode};
5use flate2::write::GzEncoder;
6use futures_util::TryFutureExt;
7use napi::{
8    Env, JsFunction, JsObject, Status,
9    bindgen_prelude::{External, within_runtime_if_available},
10    threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode},
11};
12use napi_derive::napi;
13use next_api::{
14    entrypoints::Entrypoints,
15    next_server_nft::next_server_nft_assets,
16    operation::{
17        EntrypointsOperation, InstrumentationOperation, MiddlewareOperation, OptionEndpoint,
18        RouteOperation,
19    },
20    project::{
21        DefineEnv, DraftModeOptions, PartialProjectOptions, Project, ProjectContainer,
22        ProjectOptions, WatchOptions,
23    },
24    route::Endpoint,
25    routes_hashes_manifest::routes_hashes_manifest_asset_if_enabled,
26};
27use next_core::tracing_presets::{
28    TRACING_NEXT_OVERVIEW_TARGETS, TRACING_NEXT_TARGETS, TRACING_NEXT_TURBO_TASKS_TARGETS,
29    TRACING_NEXT_TURBOPACK_TARGETS,
30};
31use once_cell::sync::Lazy;
32use rand::Rng;
33use serde::Serialize;
34use tokio::{io::AsyncWriteExt, runtime::Handle, time::Instant};
35use tracing::Instrument;
36use tracing_subscriber::{Registry, layer::SubscriberExt, util::SubscriberInitExt};
37use turbo_rcstr::{RcStr, rcstr};
38use turbo_tasks::{
39    Effects, FxIndexSet, NonLocalValue, OperationValue, OperationVc, ReadRef, ResolvedVc,
40    TaskInput, TransientInstance, TryJoinIterExt, TurboTasksApi, UpdateInfo, Vc, get_effects,
41    message_queue::{CompilationEvent, Severity},
42    trace::TraceRawVcs,
43};
44use turbo_tasks_backend::{BackingStorage, db_invalidation::invalidation_reasons};
45use turbo_tasks_fs::{
46    DiskFileSystem, FileContent, FileSystem, FileSystemPath, util::uri_from_file,
47};
48use turbo_unix_path::{get_relative_path_to, sys_to_unix};
49use turbopack_core::{
50    PROJECT_FILESYSTEM_NAME, SOURCE_URL_PROTOCOL,
51    diagnostics::PlainDiagnostic,
52    error::PrettyPrintError,
53    issue::PlainIssue,
54    output::{OutputAsset, OutputAssets},
55    source_map::{SourceMap, Token},
56    version::{PartialUpdate, TotalUpdate, Update, VersionState},
57};
58use turbopack_ecmascript_hmr_protocol::{ClientUpdateInstruction, Issue, ResourceIdentifier};
59use turbopack_trace_utils::{
60    exit::{ExitHandler, ExitReceiver},
61    filter_layer::FilterLayer,
62    raw_trace::RawTraceLayer,
63    trace_writer::TraceWriter,
64};
65use url::Url;
66
67use crate::{
68    next_api::{
69        analyze::{WriteAnalyzeResult, write_analyze_data_with_issues_operation},
70        endpoint::ExternalEndpoint,
71        turbopack_ctx::{
72            NapiNextTurbopackCallbacks, NapiNextTurbopackCallbacksJsObject, NextTurboTasks,
73            NextTurbopackContext, create_turbo_tasks,
74        },
75        utils::{
76            DetachedVc, NapiDiagnostic, NapiIssue, RootTask, TurbopackResult, get_diagnostics,
77            get_issues, strongly_consistent_catch_collectables, subscribe,
78        },
79    },
80    util::DhatProfilerGuard,
81};
82
83/// Used by [`benchmark_file_io`]. This is a noisy benchmark, so set the
84/// threshold high.
85const SLOW_FILESYSTEM_THRESHOLD: Duration = Duration::from_millis(100);
86static SOURCE_MAP_PREFIX: Lazy<String> = Lazy::new(|| format!("{SOURCE_URL_PROTOCOL}///"));
87static SOURCE_MAP_PREFIX_PROJECT: Lazy<String> =
88    Lazy::new(|| format!("{SOURCE_URL_PROTOCOL}///[{PROJECT_FILESYSTEM_NAME}]/"));
89
90#[napi(object)]
91#[derive(Clone, Debug)]
92pub struct NapiEnvVar {
93    pub name: RcStr,
94    pub value: RcStr,
95}
96
97#[napi(object)]
98#[derive(Clone, Debug)]
99pub struct NapiOptionEnvVar {
100    pub name: RcStr,
101    pub value: Option<RcStr>,
102}
103
104#[napi(object)]
105pub struct NapiDraftModeOptions {
106    pub preview_mode_id: RcStr,
107    pub preview_mode_encryption_key: RcStr,
108    pub preview_mode_signing_key: RcStr,
109}
110
111impl From<NapiDraftModeOptions> for DraftModeOptions {
112    fn from(val: NapiDraftModeOptions) -> Self {
113        DraftModeOptions {
114            preview_mode_id: val.preview_mode_id,
115            preview_mode_encryption_key: val.preview_mode_encryption_key,
116            preview_mode_signing_key: val.preview_mode_signing_key,
117        }
118    }
119}
120
121#[napi(object)]
122pub struct NapiWatchOptions {
123    /// Whether to watch the filesystem for file changes.
124    pub enable: bool,
125
126    /// Enable polling at a certain interval if the native file watching doesn't work (e.g.
127    /// docker).
128    pub poll_interval_ms: Option<f64>,
129}
130
131#[napi(object)]
132pub struct NapiProjectOptions {
133    /// An absolute root path (Unix or Windows path) from which all files must be nested under.
134    /// Trying to access a file outside this root will fail, so think of this as a chroot.
135    /// E.g. `/home/user/projects/my-repo`.
136    pub root_path: RcStr,
137
138    /// A path which contains the app/pages directories, relative to [`Project::root_path`], always
139    /// Unix path. E.g. `apps/my-app`
140    pub project_path: RcStr,
141
142    /// A path where tracing output will be written to and/or cache is read/written.
143    /// Usually equal to the `distDir` in next.config.js.
144    /// E.g. `.next`
145    pub dist_dir: RcStr,
146
147    /// Filesystem watcher options.
148    pub watch: NapiWatchOptions,
149
150    /// The contents of next.config.js, serialized to JSON.
151    pub next_config: RcStr,
152
153    /// A map of environment variables to use when compiling code.
154    pub env: Vec<NapiEnvVar>,
155
156    /// A map of environment variables which should get injected at compile
157    /// time.
158    pub define_env: NapiDefineEnv,
159
160    /// The mode in which Next.js is running.
161    pub dev: bool,
162
163    /// The server actions encryption key.
164    pub encryption_key: RcStr,
165
166    /// The build id.
167    pub build_id: RcStr,
168
169    /// Options for draft mode.
170    pub preview_props: NapiDraftModeOptions,
171
172    /// The browserslist query to use for targeting browsers.
173    pub browserslist_query: RcStr,
174
175    /// When the code is minified, this opts out of the default mangling of
176    /// local names for variables, functions etc., which can be useful for
177    /// debugging/profiling purposes.
178    pub no_mangling: bool,
179
180    /// Whether to write the route hashes manifest.
181    pub write_routes_hashes_manifest: bool,
182
183    /// The version of Node.js that is available/currently running.
184    pub current_node_js_version: RcStr,
185}
186
187/// [NapiProjectOptions] with all fields optional.
188#[napi(object)]
189pub struct NapiPartialProjectOptions {
190    /// An absolute root path  (Unix or Windows path) from which all files must be nested under.
191    /// Trying to access a file outside this root will fail, so think of this as a chroot.
192    /// E.g. `/home/user/projects/my-repo`.
193    pub root_path: Option<RcStr>,
194
195    /// A path which contains the app/pages directories, relative to [`Project::root_path`], always
196    /// a Unix path.
197    /// E.g. `apps/my-app`
198    pub project_path: Option<RcStr>,
199
200    /// Filesystem watcher options.
201    pub watch: Option<NapiWatchOptions>,
202
203    /// The contents of next.config.js, serialized to JSON.
204    pub next_config: Option<RcStr>,
205
206    /// A map of environment variables to use when compiling code.
207    pub env: Option<Vec<NapiEnvVar>>,
208
209    /// A map of environment variables which should get injected at compile
210    /// time.
211    pub define_env: Option<NapiDefineEnv>,
212
213    /// The mode in which Next.js is running.
214    pub dev: Option<bool>,
215
216    /// The server actions encryption key.
217    pub encryption_key: Option<RcStr>,
218
219    /// The build id.
220    pub build_id: Option<RcStr>,
221
222    /// Options for draft mode.
223    pub preview_props: Option<NapiDraftModeOptions>,
224
225    /// The browserslist query to use for targeting browsers.
226    pub browserslist_query: Option<RcStr>,
227
228    /// Whether to write the route hashes manifest.
229    pub write_routes_hashes_manifest: Option<bool>,
230
231    /// When the code is minified, this opts out of the default mangling of
232    /// local names for variables, functions etc., which can be useful for
233    /// debugging/profiling purposes.
234    pub no_mangling: Option<bool>,
235}
236
237#[napi(object)]
238#[derive(Clone, Debug)]
239pub struct NapiDefineEnv {
240    pub client: Vec<NapiOptionEnvVar>,
241    pub edge: Vec<NapiOptionEnvVar>,
242    pub nodejs: Vec<NapiOptionEnvVar>,
243}
244
245#[napi(object)]
246pub struct NapiTurboEngineOptions {
247    /// Use the new backend with filesystem cache enabled.
248    pub persistent_caching: Option<bool>,
249    /// An upper bound of memory that turbopack will attempt to stay under.
250    pub memory_limit: Option<f64>,
251    /// Track dependencies between tasks. If false, any change during build will error.
252    pub dependency_tracking: Option<bool>,
253    /// Whether the project is running in a CI environment.
254    pub is_ci: Option<bool>,
255    /// Whether the project is running in a short session.
256    pub is_short_session: Option<bool>,
257}
258
259impl From<NapiWatchOptions> for WatchOptions {
260    fn from(val: NapiWatchOptions) -> Self {
261        WatchOptions {
262            enable: val.enable,
263            poll_interval: val
264                .poll_interval_ms
265                .filter(|interval| !interval.is_nan() && interval.is_finite() && *interval > 0.0)
266                .map(|interval| Duration::from_secs_f64(interval / 1000.0)),
267        }
268    }
269}
270
271impl From<NapiProjectOptions> for ProjectOptions {
272    fn from(val: NapiProjectOptions) -> Self {
273        let NapiProjectOptions {
274            root_path,
275            project_path,
276            // Only used for initializing cache and tracing
277            dist_dir: _,
278            watch,
279            next_config,
280            env,
281            define_env,
282            dev,
283            encryption_key,
284            build_id,
285            preview_props,
286            browserslist_query,
287            no_mangling,
288            write_routes_hashes_manifest,
289            current_node_js_version,
290        } = val;
291        ProjectOptions {
292            root_path,
293            project_path,
294            watch: watch.into(),
295            next_config,
296            env: env.into_iter().map(|var| (var.name, var.value)).collect(),
297            define_env: define_env.into(),
298            dev,
299            encryption_key,
300            build_id,
301            preview_props: preview_props.into(),
302            browserslist_query,
303            no_mangling,
304            write_routes_hashes_manifest,
305            current_node_js_version,
306        }
307    }
308}
309
310impl From<NapiPartialProjectOptions> for PartialProjectOptions {
311    fn from(val: NapiPartialProjectOptions) -> Self {
312        let NapiPartialProjectOptions {
313            root_path,
314            project_path,
315            watch,
316            next_config,
317            env,
318            define_env,
319            dev,
320            encryption_key,
321            build_id,
322            preview_props,
323            browserslist_query,
324            no_mangling,
325            write_routes_hashes_manifest,
326        } = val;
327        PartialProjectOptions {
328            root_path,
329            project_path,
330            watch: watch.map(From::from),
331            next_config,
332            env: env.map(|env| env.into_iter().map(|var| (var.name, var.value)).collect()),
333            define_env: define_env.map(|env| env.into()),
334            dev,
335            encryption_key,
336            build_id,
337            preview_props: preview_props.map(|props| props.into()),
338            browserslist_query,
339            no_mangling,
340            write_routes_hashes_manifest,
341        }
342    }
343}
344
345impl From<NapiDefineEnv> for DefineEnv {
346    fn from(val: NapiDefineEnv) -> Self {
347        DefineEnv {
348            client: val
349                .client
350                .into_iter()
351                .map(|var| (var.name, var.value))
352                .collect(),
353            edge: val
354                .edge
355                .into_iter()
356                .map(|var| (var.name, var.value))
357                .collect(),
358            nodejs: val
359                .nodejs
360                .into_iter()
361                .map(|var| (var.name, var.value))
362                .collect(),
363        }
364    }
365}
366
367pub struct ProjectInstance {
368    turbopack_ctx: NextTurbopackContext,
369    container: ResolvedVc<ProjectContainer>,
370    exit_receiver: tokio::sync::Mutex<Option<ExitReceiver>>,
371}
372
373#[napi(ts_return_type = "Promise<{ __napiType: \"Project\" }>")]
374pub fn project_new(
375    env: Env,
376    options: NapiProjectOptions,
377    turbo_engine_options: NapiTurboEngineOptions,
378    napi_callbacks: NapiNextTurbopackCallbacksJsObject,
379) -> napi::Result<JsObject> {
380    let napi_callbacks = NapiNextTurbopackCallbacks::from_js(napi_callbacks)?;
381    let (exit, exit_receiver) = ExitHandler::new_receiver();
382
383    if let Some(dhat_profiler) = DhatProfilerGuard::try_init() {
384        exit.on_exit(async move {
385            tokio::task::spawn_blocking(move || drop(dhat_profiler))
386                .await
387                .unwrap()
388        });
389    }
390
391    let mut trace = std::env::var("NEXT_TURBOPACK_TRACING")
392        .ok()
393        .filter(|v| !v.is_empty());
394
395    if cfg!(feature = "tokio-console") && trace.is_none() {
396        // ensure `trace` is set to *something* so that the `tokio-console` feature works,
397        // otherwise you just get empty output from `tokio-console`, which can be
398        // confusing.
399        trace = Some("overview".to_owned());
400    }
401
402    enum Compression {
403        None,
404        GzipFast,
405        GzipBest,
406    }
407    let mut compress = Compression::None;
408    if let Some(mut trace) = trace {
409        let internal_dir = PathBuf::from(&options.root_path)
410            .join(&options.project_path)
411            .join(&options.dist_dir);
412        let trace_file = internal_dir.join("trace-turbopack");
413
414        println!("Turbopack tracing enabled with targets: {trace}");
415        println!("  Note that this might have a small performance impact.");
416        println!("  Trace output will be written to {}", trace_file.display());
417
418        trace = trace
419            .split(",")
420            .filter_map(|item| {
421                // Trace presets
422                Some(match item {
423                    "overview" | "1" => Cow::Owned(TRACING_NEXT_OVERVIEW_TARGETS.join(",")),
424                    "next" => Cow::Owned(TRACING_NEXT_TARGETS.join(",")),
425                    "turbopack" => Cow::Owned(TRACING_NEXT_TURBOPACK_TARGETS.join(",")),
426                    "turbo-tasks" => Cow::Owned(TRACING_NEXT_TURBO_TASKS_TARGETS.join(",")),
427                    "gz" => {
428                        compress = Compression::GzipFast;
429                        return None;
430                    }
431                    "gz-best" => {
432                        compress = Compression::GzipBest;
433                        return None;
434                    }
435                    _ => Cow::Borrowed(item),
436                })
437            })
438            .intersperse_with(|| Cow::Borrowed(","))
439            .collect::<String>();
440
441        let subscriber = Registry::default();
442
443        if cfg!(feature = "tokio-console") {
444            trace = format!("{trace},tokio=trace,runtime=trace");
445        }
446        #[cfg(feature = "tokio-console")]
447        let subscriber = subscriber.with(console_subscriber::spawn());
448
449        let subscriber = subscriber.with(FilterLayer::try_new(&trace).unwrap());
450
451        std::fs::create_dir_all(&internal_dir)
452            .context("Unable to create .next directory")
453            .unwrap();
454        let (trace_writer, trace_writer_guard) = match compress {
455            Compression::None => {
456                let trace_writer = std::fs::File::create(trace_file.clone()).unwrap();
457                TraceWriter::new(trace_writer)
458            }
459            Compression::GzipFast => {
460                let trace_writer = std::fs::File::create(trace_file.clone()).unwrap();
461                let trace_writer = GzEncoder::new(trace_writer, flate2::Compression::fast());
462                TraceWriter::new(trace_writer)
463            }
464            Compression::GzipBest => {
465                let trace_writer = std::fs::File::create(trace_file.clone()).unwrap();
466                let trace_writer = GzEncoder::new(trace_writer, flate2::Compression::best());
467                TraceWriter::new(trace_writer)
468            }
469        };
470        let subscriber = subscriber.with(RawTraceLayer::new(trace_writer));
471
472        exit.on_exit(async move {
473            tokio::task::spawn_blocking(move || drop(trace_writer_guard))
474                .await
475                .unwrap();
476        });
477
478        let trace_server = std::env::var("NEXT_TURBOPACK_TRACE_SERVER").ok();
479        if trace_server.is_some() {
480            thread::spawn(move || {
481                turbopack_trace_server::start_turbopack_trace_server(trace_file, None);
482            });
483            println!("Turbopack trace server started. View trace at https://trace.nextjs.org");
484        }
485
486        subscriber.init();
487    }
488
489    env.spawn_future(
490        async move {
491            let memory_limit = turbo_engine_options
492                .memory_limit
493                .map(|m| m as usize)
494                .unwrap_or(usize::MAX);
495            let persistent_caching = turbo_engine_options.persistent_caching.unwrap_or_default();
496            let dependency_tracking = turbo_engine_options.dependency_tracking.unwrap_or(true);
497            let is_ci = turbo_engine_options.is_ci.unwrap_or(false);
498            let is_short_session = turbo_engine_options.is_short_session.unwrap_or(false);
499            let turbo_tasks = create_turbo_tasks(
500                PathBuf::from(&options.dist_dir),
501                persistent_caching,
502                memory_limit,
503                dependency_tracking,
504                is_ci,
505                is_short_session,
506            )?;
507            let turbopack_ctx = NextTurbopackContext::new(turbo_tasks.clone(), napi_callbacks);
508
509            if let Some(stats_path) = std::env::var_os("NEXT_TURBOPACK_TASK_STATISTICS") {
510                let task_stats = turbo_tasks.task_statistics().enable().clone();
511                exit.on_exit(async move {
512                    tokio::task::spawn_blocking(move || {
513                        let mut file = std::fs::File::create(&stats_path)
514                            .with_context(|| format!("failed to create or open {stats_path:?}"))?;
515                        serde_json::to_writer(&file, &task_stats)
516                            .context("failed to serialize or write task statistics")?;
517                        file.flush().context("failed to flush file")
518                    })
519                    .await
520                    .unwrap()
521                    .unwrap();
522                });
523            }
524
525            let options: ProjectOptions = options.into();
526            let is_dev = options.dev;
527            let container = turbo_tasks
528                .run(async move {
529                    let project = ProjectContainer::new(rcstr!("next.js"), is_dev);
530                    let project = project.to_resolved().await?;
531                    project.initialize(options).await?;
532                    Ok(project)
533                })
534                .or_else(|e| turbopack_ctx.throw_turbopack_internal_result(&e.into()))
535                .await?;
536
537            if is_dev {
538                Handle::current().spawn({
539                    let tt = turbo_tasks.clone();
540                    async move {
541                        let result = tt
542                            .clone()
543                            .run(async move {
544                                benchmark_file_io(
545                                    tt,
546                                    container.project().node_root().owned().await?,
547                                )
548                                .await
549                            })
550                            .await;
551                        if let Err(err) = result {
552                            // TODO Not ideal to print directly to stdout.
553                            // We should use a compilation event instead to report async errors.
554                            println!("Failed to benchmark file I/O: {err}");
555                        }
556                    }
557                    .instrument(tracing::info_span!("benchmark file I/O"))
558                });
559            }
560
561            Ok(External::new(ProjectInstance {
562                turbopack_ctx,
563                container,
564                exit_receiver: tokio::sync::Mutex::new(Some(exit_receiver)),
565            }))
566        }
567        .instrument(tracing::info_span!("create project")),
568    )
569}
570
571#[derive(Debug, Clone, Serialize)]
572struct SlowFilesystemEvent {
573    directory: String,
574    duration_ms: u128,
575}
576
577impl CompilationEvent for SlowFilesystemEvent {
578    fn type_name(&self) -> &'static str {
579        "SlowFilesystemEvent"
580    }
581
582    fn severity(&self) -> Severity {
583        Severity::Warning
584    }
585
586    fn message(&self) -> String {
587        format!(
588            "Slow filesystem detected. The benchmark took {}ms. If {} is a network drive, \
589             consider moving it to a local folder. If you have an antivirus enabled, consider \
590             excluding your project directory.",
591            self.duration_ms, self.directory
592        )
593    }
594
595    fn to_json(&self) -> String {
596        serde_json::to_string(self).unwrap()
597    }
598}
599
600/// A very simple and low-overhead, but potentially noisy benchmark to detect
601/// very slow disk IO. Warns the user (via `println!`) if the benchmark takes
602/// more than `SLOW_FILESYSTEM_THRESHOLD`.
603///
604/// This idea is copied from Bun:
605/// - https://x.com/jarredsumner/status/1637549427677364224
606/// - https://github.com/oven-sh/bun/blob/06a9aa80c38b08b3148bfeabe560/src/install/install.zig#L3038
607async fn benchmark_file_io(turbo_tasks: NextTurboTasks, directory: FileSystemPath) -> Result<()> {
608    // try to get the real file path on disk so that we can use it with tokio
609    let fs = ResolvedVc::try_downcast_type::<DiskFileSystem>(directory.fs)
610        .context(anyhow!(
611            "expected node_root to be a DiskFileSystem, cannot benchmark"
612        ))?
613        .await?;
614
615    let directory = fs.to_sys_path(&directory);
616    let temp_path = directory.join(format!(
617        "tmp_file_io_benchmark_{:x}",
618        rand::random::<u128>()
619    ));
620
621    let mut random_buffer = [0u8; 512];
622    rand::rng().fill(&mut random_buffer[..]);
623
624    // perform IO directly with tokio (skipping `tokio_tasks_fs`) to avoid the
625    // additional noise/overhead of tasks caching, invalidation, file locks,
626    // etc.
627    let start = Instant::now();
628    async {
629        for _ in 0..3 {
630            // create a new empty file
631            let mut file = tokio::fs::File::create(&temp_path).await?;
632            file.write_all(&random_buffer).await?;
633            file.sync_all().await?;
634            drop(file);
635
636            // remove the file
637            tokio::fs::remove_file(&temp_path).await?;
638        }
639        anyhow::Ok(())
640    }
641    .instrument(tracing::info_span!("benchmark file IO (measurement)", path = %temp_path.display()))
642    .await?;
643
644    let duration = Instant::now().duration_since(start);
645    if duration > SLOW_FILESYSTEM_THRESHOLD {
646        turbo_tasks.send_compilation_event(Arc::new(SlowFilesystemEvent {
647            directory: directory.to_string_lossy().into(),
648            duration_ms: duration.as_millis(),
649        }));
650    }
651
652    Ok(())
653}
654
655#[tracing::instrument(level = "info", name = "update project", skip_all)]
656#[napi]
657pub async fn project_update(
658    #[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External<ProjectInstance>,
659    options: NapiPartialProjectOptions,
660) -> napi::Result<()> {
661    let ctx = &project.turbopack_ctx;
662    let options = options.into();
663    let container = project.container;
664    ctx.turbo_tasks()
665        .run(async move {
666            container.update(options).await?;
667            Ok(())
668        })
669        .or_else(|e| ctx.throw_turbopack_internal_result(&e.into()))
670        .await
671}
672
673/// Invalidates the filesystem cache so that it will be deleted next time that a turbopack project
674/// is created with filesystem cache enabled.
675#[napi]
676pub async fn project_invalidate_file_system_cache(
677    #[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External<ProjectInstance>,
678) -> napi::Result<()> {
679    tokio::task::spawn_blocking(move || {
680        // TODO: Let the JS caller specify a reason? We need to limit the reasons to ones we know
681        // how to generate a message for on the Rust side of the FFI.
682        project
683            .turbopack_ctx
684            .turbo_tasks()
685            .backend()
686            .backing_storage()
687            .invalidate(invalidation_reasons::USER_REQUEST)
688    })
689    .await
690    .context("panicked while invalidating filesystem cache")??;
691    Ok(())
692}
693
694/// Runs exit handlers for the project registered using the [`ExitHandler`] API.
695///
696/// This is called by `project_shutdown`, so if you're calling that API, you shouldn't call this
697/// one.
698#[napi]
699pub async fn project_on_exit(
700    #[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External<ProjectInstance>,
701) {
702    project_on_exit_internal(&project).await
703}
704
705async fn project_on_exit_internal(project: &ProjectInstance) {
706    let exit_receiver = project.exit_receiver.lock().await.take();
707    exit_receiver
708        .expect("`project.onExitSync` must only be called once")
709        .run_exit_handler()
710        .await;
711}
712
713/// Runs `project_on_exit`, and then waits for turbo_tasks to gracefully shut down.
714///
715/// This is used in builds where it's important that we completely persist turbo-tasks to disk, but
716/// it's skipped in the development server (`project_on_exit` is used instead with a short timeout),
717/// where we prioritize fast exit and user responsiveness over all else.
718#[tracing::instrument(level = "info", name = "shutdown project", skip_all)]
719#[napi]
720pub async fn project_shutdown(
721    #[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External<ProjectInstance>,
722) {
723    project.turbopack_ctx.turbo_tasks().stop_and_wait().await;
724    project_on_exit_internal(&project).await;
725}
726
727#[napi(object)]
728#[derive(Default)]
729pub struct AppPageNapiRoute {
730    /// The relative path from project_path to the route file
731    pub original_name: Option<RcStr>,
732
733    pub html_endpoint: Option<External<ExternalEndpoint>>,
734    pub rsc_endpoint: Option<External<ExternalEndpoint>>,
735}
736
737#[napi(object)]
738#[derive(Default)]
739pub struct NapiRoute {
740    /// The router path
741    pub pathname: String,
742    /// The relative path from project_path to the route file
743    pub original_name: Option<RcStr>,
744
745    /// The type of route, eg a Page or App
746    pub r#type: &'static str,
747
748    pub pages: Option<Vec<AppPageNapiRoute>>,
749
750    // Different representations of the endpoint
751    pub endpoint: Option<External<ExternalEndpoint>>,
752    pub html_endpoint: Option<External<ExternalEndpoint>>,
753    pub rsc_endpoint: Option<External<ExternalEndpoint>>,
754    pub data_endpoint: Option<External<ExternalEndpoint>>,
755}
756
757impl NapiRoute {
758    fn from_route(
759        pathname: String,
760        value: RouteOperation,
761        turbopack_ctx: &NextTurbopackContext,
762    ) -> Self {
763        let convert_endpoint = |endpoint: OperationVc<OptionEndpoint>| {
764            Some(External::new(ExternalEndpoint(DetachedVc::new(
765                turbopack_ctx.clone(),
766                endpoint,
767            ))))
768        };
769        match value {
770            RouteOperation::Page {
771                html_endpoint,
772                data_endpoint,
773            } => NapiRoute {
774                pathname,
775                r#type: "page",
776                html_endpoint: convert_endpoint(html_endpoint),
777                data_endpoint: convert_endpoint(data_endpoint),
778                ..Default::default()
779            },
780            RouteOperation::PageApi { endpoint } => NapiRoute {
781                pathname,
782                r#type: "page-api",
783                endpoint: convert_endpoint(endpoint),
784                ..Default::default()
785            },
786            RouteOperation::AppPage(pages) => NapiRoute {
787                pathname,
788                r#type: "app-page",
789                pages: Some(
790                    pages
791                        .into_iter()
792                        .map(|page_route| AppPageNapiRoute {
793                            original_name: Some(page_route.original_name),
794                            html_endpoint: convert_endpoint(page_route.html_endpoint),
795                            rsc_endpoint: convert_endpoint(page_route.rsc_endpoint),
796                        })
797                        .collect(),
798                ),
799                ..Default::default()
800            },
801            RouteOperation::AppRoute {
802                original_name,
803                endpoint,
804            } => NapiRoute {
805                pathname,
806                original_name: Some(original_name),
807                r#type: "app-route",
808                endpoint: convert_endpoint(endpoint),
809                ..Default::default()
810            },
811            RouteOperation::Conflict => NapiRoute {
812                pathname,
813                r#type: "conflict",
814                ..Default::default()
815            },
816        }
817    }
818}
819
820#[napi(object)]
821pub struct NapiMiddleware {
822    pub endpoint: External<ExternalEndpoint>,
823    pub is_proxy: bool,
824}
825
826impl NapiMiddleware {
827    fn from_middleware(
828        value: &MiddlewareOperation,
829        turbopack_ctx: &NextTurbopackContext,
830    ) -> Result<Self> {
831        Ok(NapiMiddleware {
832            endpoint: External::new(ExternalEndpoint(DetachedVc::new(
833                turbopack_ctx.clone(),
834                value.endpoint,
835            ))),
836            is_proxy: value.is_proxy,
837        })
838    }
839}
840
841#[napi(object)]
842pub struct NapiInstrumentation {
843    pub node_js: External<ExternalEndpoint>,
844    pub edge: External<ExternalEndpoint>,
845}
846
847impl NapiInstrumentation {
848    fn from_instrumentation(
849        value: &InstrumentationOperation,
850        turbopack_ctx: &NextTurbopackContext,
851    ) -> Result<Self> {
852        Ok(NapiInstrumentation {
853            node_js: External::new(ExternalEndpoint(DetachedVc::new(
854                turbopack_ctx.clone(),
855                value.node_js,
856            ))),
857            edge: External::new(ExternalEndpoint(DetachedVc::new(
858                turbopack_ctx.clone(),
859                value.edge,
860            ))),
861        })
862    }
863}
864
865#[napi(object)]
866pub struct NapiEntrypoints {
867    pub routes: Vec<NapiRoute>,
868    pub middleware: Option<NapiMiddleware>,
869    pub instrumentation: Option<NapiInstrumentation>,
870    pub pages_document_endpoint: External<ExternalEndpoint>,
871    pub pages_app_endpoint: External<ExternalEndpoint>,
872    pub pages_error_endpoint: External<ExternalEndpoint>,
873}
874
875impl NapiEntrypoints {
876    fn from_entrypoints_op(
877        entrypoints: &EntrypointsOperation,
878        turbopack_ctx: &NextTurbopackContext,
879    ) -> Result<Self> {
880        let routes = entrypoints
881            .routes
882            .iter()
883            .map(|(k, v)| NapiRoute::from_route(k.to_string(), v.clone(), turbopack_ctx))
884            .collect();
885        let middleware = entrypoints
886            .middleware
887            .as_ref()
888            .map(|m| NapiMiddleware::from_middleware(m, turbopack_ctx))
889            .transpose()?;
890        let instrumentation = entrypoints
891            .instrumentation
892            .as_ref()
893            .map(|i| NapiInstrumentation::from_instrumentation(i, turbopack_ctx))
894            .transpose()?;
895        let pages_document_endpoint = External::new(ExternalEndpoint(DetachedVc::new(
896            turbopack_ctx.clone(),
897            entrypoints.pages_document_endpoint,
898        )));
899        let pages_app_endpoint = External::new(ExternalEndpoint(DetachedVc::new(
900            turbopack_ctx.clone(),
901            entrypoints.pages_app_endpoint,
902        )));
903        let pages_error_endpoint = External::new(ExternalEndpoint(DetachedVc::new(
904            turbopack_ctx.clone(),
905            entrypoints.pages_error_endpoint,
906        )));
907        Ok(NapiEntrypoints {
908            routes,
909            middleware,
910            instrumentation,
911            pages_document_endpoint,
912            pages_app_endpoint,
913            pages_error_endpoint,
914        })
915    }
916}
917
918#[turbo_tasks::value(serialization = "none")]
919struct EntrypointsWithIssues {
920    entrypoints: Option<ReadRef<EntrypointsOperation>>,
921    issues: Arc<Vec<ReadRef<PlainIssue>>>,
922    diagnostics: Arc<Vec<ReadRef<PlainDiagnostic>>>,
923    effects: Arc<Effects>,
924}
925
926#[turbo_tasks::function(operation)]
927async fn get_entrypoints_with_issues_operation(
928    container: ResolvedVc<ProjectContainer>,
929) -> Result<Vc<EntrypointsWithIssues>> {
930    let entrypoints_operation =
931        EntrypointsOperation::new(project_container_entrypoints_operation(container));
932    let (entrypoints, issues, diagnostics, effects) =
933        strongly_consistent_catch_collectables(entrypoints_operation).await?;
934    Ok(EntrypointsWithIssues {
935        entrypoints,
936        issues,
937        diagnostics,
938        effects,
939    }
940    .cell())
941}
942
943#[turbo_tasks::function(operation)]
944fn project_container_entrypoints_operation(
945    // the container is a long-lived object with internally mutable state, there's no risk of it
946    // becoming stale
947    container: ResolvedVc<ProjectContainer>,
948) -> Vc<Entrypoints> {
949    container.entrypoints()
950}
951
952#[turbo_tasks::value(serialization = "none")]
953struct OperationResult {
954    issues: Arc<Vec<ReadRef<PlainIssue>>>,
955    diagnostics: Arc<Vec<ReadRef<PlainDiagnostic>>>,
956    effects: Arc<Effects>,
957}
958
959#[turbo_tasks::value(serialization = "none")]
960struct AllWrittenEntrypointsWithIssues {
961    entrypoints: Option<ReadRef<EntrypointsOperation>>,
962    issues: Arc<Vec<ReadRef<PlainIssue>>>,
963    diagnostics: Arc<Vec<ReadRef<PlainDiagnostic>>>,
964    effects: Arc<Effects>,
965}
966
967#[tracing::instrument(level = "info", name = "write all entrypoints to disk", skip_all)]
968#[napi]
969pub async fn project_write_all_entrypoints_to_disk(
970    #[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External<ProjectInstance>,
971    app_dir_only: bool,
972) -> napi::Result<TurbopackResult<Option<NapiEntrypoints>>> {
973    let ctx = &project.turbopack_ctx;
974    let container = project.container;
975    let tt = ctx.turbo_tasks();
976
977    let (entrypoints, issues, diags) = tt
978        .run(async move {
979            let entrypoints_with_issues_op =
980                get_all_written_entrypoints_with_issues_operation(container, app_dir_only);
981
982            // Read and compile the files
983            let AllWrittenEntrypointsWithIssues {
984                entrypoints,
985                issues,
986                diagnostics,
987                effects,
988            } = &*entrypoints_with_issues_op
989                .read_strongly_consistent()
990                .await?;
991
992            // Write the files to disk
993            effects.apply().await?;
994
995            Ok((entrypoints.clone(), issues.clone(), diagnostics.clone()))
996        })
997        .or_else(|e| ctx.throw_turbopack_internal_result(&e.into()))
998        .await?;
999
1000    Ok(TurbopackResult {
1001        result: if let Some(entrypoints) = entrypoints {
1002            Some(NapiEntrypoints::from_entrypoints_op(
1003                &entrypoints,
1004                &project.turbopack_ctx,
1005            )?)
1006        } else {
1007            None
1008        },
1009        issues: issues.iter().map(|i| NapiIssue::from(&**i)).collect(),
1010        diagnostics: diags.iter().map(|d| NapiDiagnostic::from(d)).collect(),
1011    })
1012}
1013
1014#[turbo_tasks::function(operation)]
1015async fn get_all_written_entrypoints_with_issues_operation(
1016    container: ResolvedVc<ProjectContainer>,
1017    app_dir_only: bool,
1018) -> Result<Vc<AllWrittenEntrypointsWithIssues>> {
1019    let entrypoints_operation = EntrypointsOperation::new(all_entrypoints_write_to_disk_operation(
1020        container,
1021        app_dir_only,
1022    ));
1023    let (entrypoints, issues, diagnostics, effects) =
1024        strongly_consistent_catch_collectables(entrypoints_operation).await?;
1025    Ok(AllWrittenEntrypointsWithIssues {
1026        entrypoints,
1027        issues,
1028        diagnostics,
1029        effects,
1030    }
1031    .cell())
1032}
1033
1034#[turbo_tasks::function(operation)]
1035pub async fn all_entrypoints_write_to_disk_operation(
1036    project: ResolvedVc<ProjectContainer>,
1037    app_dir_only: bool,
1038) -> Result<Vc<Entrypoints>> {
1039    let output_assets_operation = output_assets_operation(project, app_dir_only);
1040    project
1041        .project()
1042        .emit_all_output_assets(output_assets_operation)
1043        .as_side_effect()
1044        .await?;
1045
1046    Ok(project.entrypoints())
1047}
1048
1049#[turbo_tasks::function(operation)]
1050async fn output_assets_operation(
1051    container: ResolvedVc<ProjectContainer>,
1052    app_dir_only: bool,
1053) -> Result<Vc<OutputAssets>> {
1054    let project = container.project();
1055    let whole_app_module_graphs = project.whole_app_module_graphs();
1056    let endpoint_assets = project
1057        .get_all_endpoints(app_dir_only)
1058        .await?
1059        .iter()
1060        .map(|endpoint| async move { endpoint.output().await?.output_assets.await })
1061        .try_join()
1062        .await?;
1063
1064    let output_assets: FxIndexSet<ResolvedVc<Box<dyn OutputAsset>>> = endpoint_assets
1065        .iter()
1066        .flat_map(|assets| assets.iter().copied())
1067        .collect();
1068
1069    let nft = next_server_nft_assets(project).await?;
1070
1071    let routes_hashes_manifest = routes_hashes_manifest_asset_if_enabled(project).await?;
1072
1073    whole_app_module_graphs.as_side_effect().await?;
1074
1075    Ok(Vc::cell(
1076        output_assets
1077            .into_iter()
1078            .chain(nft.iter().copied())
1079            .chain(routes_hashes_manifest.iter().copied())
1080            .collect(),
1081    ))
1082}
1083
1084#[tracing::instrument(level = "info", name = "get entrypoints", skip_all)]
1085#[napi]
1086pub async fn project_entrypoints(
1087    #[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External<ProjectInstance>,
1088) -> napi::Result<TurbopackResult<Option<NapiEntrypoints>>> {
1089    let container = project.container;
1090
1091    let (entrypoints, issues, diags) = project
1092        .turbopack_ctx
1093        .turbo_tasks()
1094        .run_once(async move {
1095            let entrypoints_with_issues_op = get_entrypoints_with_issues_operation(container);
1096
1097            // Read and compile the files
1098            let EntrypointsWithIssues {
1099                entrypoints,
1100                issues,
1101                diagnostics,
1102                effects: _,
1103            } = &*entrypoints_with_issues_op
1104                .read_strongly_consistent()
1105                .await?;
1106
1107            Ok((entrypoints.clone(), issues.clone(), diagnostics.clone()))
1108        })
1109        .await
1110        .map_err(|e| napi::Error::from_reason(PrettyPrintError(&e).to_string()))?;
1111
1112    let result = match entrypoints {
1113        Some(entrypoints) => Some(NapiEntrypoints::from_entrypoints_op(
1114            &entrypoints,
1115            &project.turbopack_ctx,
1116        )?),
1117        None => None,
1118    };
1119
1120    Ok(TurbopackResult {
1121        result,
1122        issues: issues.iter().map(|i| NapiIssue::from(&**i)).collect(),
1123        diagnostics: diags.iter().map(|d| NapiDiagnostic::from(d)).collect(),
1124    })
1125}
1126
1127#[tracing::instrument(level = "info", name = "subscribe to entrypoints", skip_all)]
1128#[napi(ts_return_type = "{ __napiType: \"RootTask\" }")]
1129pub fn project_entrypoints_subscribe(
1130    #[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External<ProjectInstance>,
1131    func: JsFunction,
1132) -> napi::Result<External<RootTask>> {
1133    let turbopack_ctx = project.turbopack_ctx.clone();
1134    let container = project.container;
1135    subscribe(
1136        turbopack_ctx.clone(),
1137        func,
1138        move || {
1139            async move {
1140                let entrypoints_with_issues_op = get_entrypoints_with_issues_operation(container);
1141                let EntrypointsWithIssues {
1142                    entrypoints,
1143                    issues,
1144                    diagnostics,
1145                    effects,
1146                } = &*entrypoints_with_issues_op
1147                    .read_strongly_consistent()
1148                    .await?;
1149
1150                effects.apply().await?;
1151                Ok((entrypoints.clone(), issues.clone(), diagnostics.clone()))
1152            }
1153            .instrument(tracing::info_span!("entrypoints subscription"))
1154        },
1155        move |ctx| {
1156            let (entrypoints, issues, diags) = ctx.value;
1157            let result = match entrypoints {
1158                Some(entrypoints) => Some(NapiEntrypoints::from_entrypoints_op(
1159                    &entrypoints,
1160                    &turbopack_ctx,
1161                )?),
1162                None => None,
1163            };
1164
1165            Ok(vec![TurbopackResult {
1166                result,
1167                issues: issues
1168                    .iter()
1169                    .map(|issue| NapiIssue::from(&**issue))
1170                    .collect(),
1171                diagnostics: diags.iter().map(|d| NapiDiagnostic::from(d)).collect(),
1172            }])
1173        },
1174    )
1175}
1176
1177#[turbo_tasks::value(serialization = "none")]
1178struct HmrUpdateWithIssues {
1179    update: ReadRef<Update>,
1180    issues: Arc<Vec<ReadRef<PlainIssue>>>,
1181    diagnostics: Arc<Vec<ReadRef<PlainDiagnostic>>>,
1182    effects: Arc<Effects>,
1183}
1184
1185#[turbo_tasks::function(operation)]
1186async fn hmr_update_with_issues_operation(
1187    project: ResolvedVc<Project>,
1188    identifier: RcStr,
1189    state: ResolvedVc<VersionState>,
1190) -> Result<Vc<HmrUpdateWithIssues>> {
1191    let update_op = project_hmr_update_operation(project, identifier, state);
1192    let update = update_op.read_strongly_consistent().await?;
1193    let issues = get_issues(update_op).await?;
1194    let diagnostics = get_diagnostics(update_op).await?;
1195    let effects = Arc::new(get_effects(update_op).await?);
1196    Ok(HmrUpdateWithIssues {
1197        update,
1198        issues,
1199        diagnostics,
1200        effects,
1201    }
1202    .cell())
1203}
1204
1205#[turbo_tasks::function(operation)]
1206fn project_hmr_update_operation(
1207    project: ResolvedVc<Project>,
1208    identifier: RcStr,
1209    state: ResolvedVc<VersionState>,
1210) -> Vc<Update> {
1211    project.hmr_update(identifier, *state)
1212}
1213
1214#[tracing::instrument(level = "info", name = "get HMR events", skip(project, func))]
1215#[napi(ts_return_type = "{ __napiType: \"RootTask\" }")]
1216pub fn project_hmr_events(
1217    #[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External<ProjectInstance>,
1218    identifier: RcStr,
1219    func: JsFunction,
1220) -> napi::Result<External<RootTask>> {
1221    let container = project.container;
1222    let session = TransientInstance::new(());
1223    subscribe(
1224        project.turbopack_ctx.clone(),
1225        func,
1226        {
1227            let outer_identifier = identifier.clone();
1228            let session = session.clone();
1229            move || {
1230                let identifier: RcStr = outer_identifier.clone();
1231                let session = session.clone();
1232                async move {
1233                    let project = container.project().to_resolved().await?;
1234                    let state = project
1235                        .hmr_version_state(identifier.clone(), session)
1236                        .to_resolved()
1237                        .await?;
1238
1239                    let update_op =
1240                        hmr_update_with_issues_operation(project, identifier.clone(), state);
1241                    let update = update_op.read_strongly_consistent().await?;
1242                    let HmrUpdateWithIssues {
1243                        update,
1244                        issues,
1245                        diagnostics,
1246                        effects,
1247                    } = &*update;
1248                    effects.apply().await?;
1249                    match &**update {
1250                        Update::Missing | Update::None => {}
1251                        Update::Total(TotalUpdate { to }) => {
1252                            state.set(to.clone()).await?;
1253                        }
1254                        Update::Partial(PartialUpdate { to, .. }) => {
1255                            state.set(to.clone()).await?;
1256                        }
1257                    }
1258                    Ok((Some(update.clone()), issues.clone(), diagnostics.clone()))
1259                }
1260            }
1261        },
1262        move |ctx| {
1263            let (update, issues, diags) = ctx.value;
1264
1265            let napi_issues = issues
1266                .iter()
1267                .map(|issue| NapiIssue::from(&**issue))
1268                .collect();
1269            let update_issues = issues
1270                .iter()
1271                .map(|issue| Issue::from(&**issue))
1272                .collect::<Vec<_>>();
1273
1274            let identifier = ResourceIdentifier {
1275                path: identifier.clone(),
1276                headers: None,
1277            };
1278            let update = match update.as_deref() {
1279                None | Some(Update::Missing) | Some(Update::Total(_)) => {
1280                    ClientUpdateInstruction::restart(&identifier, &update_issues)
1281                }
1282                Some(Update::Partial(update)) => ClientUpdateInstruction::partial(
1283                    &identifier,
1284                    &update.instruction,
1285                    &update_issues,
1286                ),
1287                Some(Update::None) => ClientUpdateInstruction::issues(&identifier, &update_issues),
1288            };
1289
1290            Ok(vec![TurbopackResult {
1291                result: ctx.env.to_js_value(&update)?,
1292                issues: napi_issues,
1293                diagnostics: diags.iter().map(|d| NapiDiagnostic::from(d)).collect(),
1294            }])
1295        },
1296    )
1297}
1298
1299#[napi(object)]
1300struct HmrIdentifiers {
1301    pub identifiers: Vec<RcStr>,
1302}
1303
1304#[turbo_tasks::value(serialization = "none")]
1305struct HmrIdentifiersWithIssues {
1306    identifiers: ReadRef<Vec<RcStr>>,
1307    issues: Arc<Vec<ReadRef<PlainIssue>>>,
1308    diagnostics: Arc<Vec<ReadRef<PlainDiagnostic>>>,
1309    effects: Arc<Effects>,
1310}
1311
1312#[turbo_tasks::function(operation)]
1313async fn get_hmr_identifiers_with_issues_operation(
1314    container: ResolvedVc<ProjectContainer>,
1315) -> Result<Vc<HmrIdentifiersWithIssues>> {
1316    let hmr_identifiers_op = project_container_hmr_identifiers_operation(container);
1317    let hmr_identifiers = hmr_identifiers_op.read_strongly_consistent().await?;
1318    let issues = get_issues(hmr_identifiers_op).await?;
1319    let diagnostics = get_diagnostics(hmr_identifiers_op).await?;
1320    let effects = Arc::new(get_effects(hmr_identifiers_op).await?);
1321    Ok(HmrIdentifiersWithIssues {
1322        identifiers: hmr_identifiers,
1323        issues,
1324        diagnostics,
1325        effects,
1326    }
1327    .cell())
1328}
1329
1330#[turbo_tasks::function(operation)]
1331fn project_container_hmr_identifiers_operation(
1332    container: ResolvedVc<ProjectContainer>,
1333) -> Vc<Vec<RcStr>> {
1334    container.hmr_identifiers()
1335}
1336
1337#[tracing::instrument(level = "info", name = "get HMR identifiers", skip_all)]
1338#[napi(ts_return_type = "{ __napiType: \"RootTask\" }")]
1339pub fn project_hmr_identifiers_subscribe(
1340    #[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External<ProjectInstance>,
1341    func: JsFunction,
1342) -> napi::Result<External<RootTask>> {
1343    let container = project.container;
1344    subscribe(
1345        project.turbopack_ctx.clone(),
1346        func,
1347        move || async move {
1348            let hmr_identifiers_with_issues_op =
1349                get_hmr_identifiers_with_issues_operation(container);
1350            let HmrIdentifiersWithIssues {
1351                identifiers,
1352                issues,
1353                diagnostics,
1354                effects,
1355            } = &*hmr_identifiers_with_issues_op
1356                .read_strongly_consistent()
1357                .await?;
1358            effects.apply().await?;
1359
1360            Ok((identifiers.clone(), issues.clone(), diagnostics.clone()))
1361        },
1362        move |ctx| {
1363            let (identifiers, issues, diagnostics) = ctx.value;
1364
1365            Ok(vec![TurbopackResult {
1366                result: HmrIdentifiers {
1367                    identifiers: ReadRef::into_owned(identifiers),
1368                },
1369                issues: issues
1370                    .iter()
1371                    .map(|issue| NapiIssue::from(&**issue))
1372                    .collect(),
1373                diagnostics: diagnostics
1374                    .iter()
1375                    .map(|d| NapiDiagnostic::from(d))
1376                    .collect(),
1377            }])
1378        },
1379    )
1380}
1381
1382pub enum UpdateMessage {
1383    Start,
1384    End(UpdateInfo),
1385}
1386
1387#[napi(object)]
1388struct NapiUpdateMessage {
1389    pub update_type: &'static str,
1390    pub value: Option<NapiUpdateInfo>,
1391}
1392
1393impl From<UpdateMessage> for NapiUpdateMessage {
1394    fn from(update_message: UpdateMessage) -> Self {
1395        match update_message {
1396            UpdateMessage::Start => NapiUpdateMessage {
1397                update_type: "start",
1398                value: None,
1399            },
1400            UpdateMessage::End(info) => NapiUpdateMessage {
1401                update_type: "end",
1402                value: Some(info.into()),
1403            },
1404        }
1405    }
1406}
1407
1408#[napi(object)]
1409struct NapiUpdateInfo {
1410    pub duration: u32,
1411    pub tasks: u32,
1412}
1413
1414impl From<UpdateInfo> for NapiUpdateInfo {
1415    fn from(update_info: UpdateInfo) -> Self {
1416        Self {
1417            duration: update_info.duration.as_millis() as u32,
1418            tasks: update_info.tasks as u32,
1419        }
1420    }
1421}
1422
1423/// Subscribes to lifecycle events of the compilation.
1424///
1425/// Emits an [UpdateMessage::Start] event when any computation starts.
1426/// Emits an [UpdateMessage::End] event when there was no computation for the
1427/// specified time (`aggregation_ms`). The [UpdateMessage::End] event contains
1428/// information about the computations that happened since the
1429/// [UpdateMessage::Start] event. It contains the duration of the computation
1430/// (excluding the idle time that was spend waiting for `aggregation_ms`), and
1431/// the number of tasks that were executed.
1432///
1433/// The signature of the `func` is `(update_message: UpdateMessage) => void`.
1434#[napi]
1435pub fn project_update_info_subscribe(
1436    #[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External<ProjectInstance>,
1437    aggregation_ms: u32,
1438    func: JsFunction,
1439) -> napi::Result<()> {
1440    let func: ThreadsafeFunction<UpdateMessage> = func.create_threadsafe_function(0, |ctx| {
1441        let message = ctx.value;
1442        Ok(vec![NapiUpdateMessage::from(message)])
1443    })?;
1444    tokio::spawn(async move {
1445        let tt = project.turbopack_ctx.turbo_tasks();
1446        loop {
1447            let update_info = tt
1448                .aggregated_update_info(Duration::ZERO, Duration::ZERO)
1449                .await;
1450
1451            func.call(
1452                Ok(UpdateMessage::Start),
1453                ThreadsafeFunctionCallMode::NonBlocking,
1454            );
1455
1456            let update_info = match update_info {
1457                Some(update_info) => update_info,
1458                None => {
1459                    tt.get_or_wait_aggregated_update_info(Duration::from_millis(
1460                        aggregation_ms.into(),
1461                    ))
1462                    .await
1463                }
1464            };
1465
1466            let status = func.call(
1467                Ok(UpdateMessage::End(update_info)),
1468                ThreadsafeFunctionCallMode::NonBlocking,
1469            );
1470
1471            if !matches!(status, Status::Ok) {
1472                let error = anyhow!("Error calling JS function: {}", status);
1473                eprintln!("{error}");
1474                break;
1475            }
1476        }
1477    });
1478    Ok(())
1479}
1480
1481/// Subscribes to all compilation events that are not cached like timing and progress information.
1482#[napi]
1483pub fn project_compilation_events_subscribe(
1484    #[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External<ProjectInstance>,
1485    func: JsFunction,
1486    event_types: Option<Vec<String>>,
1487) -> napi::Result<()> {
1488    let tsfn: ThreadsafeFunction<Arc<dyn CompilationEvent>> =
1489        func.create_threadsafe_function(0, |ctx| {
1490            let event: Arc<dyn CompilationEvent> = ctx.value;
1491
1492            let env = ctx.env;
1493            let mut obj = env.create_object()?;
1494            obj.set_named_property("typeName", event.type_name())?;
1495            obj.set_named_property("severity", event.severity().to_string())?;
1496            obj.set_named_property("message", event.message())?;
1497
1498            let external = env.create_external(event, None);
1499            obj.set_named_property("eventData", external)?;
1500
1501            Ok(vec![obj])
1502        })?;
1503
1504    tokio::spawn(async move {
1505        let tt = project.turbopack_ctx.turbo_tasks();
1506        let mut receiver = tt.subscribe_to_compilation_events(event_types);
1507        while let Some(msg) = receiver.recv().await {
1508            let status = tsfn.call(Ok(msg), ThreadsafeFunctionCallMode::Blocking);
1509
1510            if status != Status::Ok {
1511                break;
1512            }
1513        }
1514    });
1515
1516    Ok(())
1517}
1518
1519#[napi(object)]
1520#[derive(
1521    Clone,
1522    Debug,
1523    Eq,
1524    Hash,
1525    NonLocalValue,
1526    OperationValue,
1527    PartialEq,
1528    TaskInput,
1529    TraceRawVcs,
1530    Encode,
1531    Decode,
1532)]
1533pub struct StackFrame {
1534    pub is_server: bool,
1535    pub is_internal: Option<bool>,
1536    pub original_file: Option<RcStr>,
1537    pub file: RcStr,
1538    /// 1-indexed, unlike source map tokens
1539    pub line: Option<u32>,
1540    /// 1-indexed, unlike source map tokens
1541    pub column: Option<u32>,
1542    pub method_name: Option<RcStr>,
1543}
1544
1545#[turbo_tasks::value(transparent)]
1546#[derive(Clone)]
1547pub struct OptionStackFrame(Option<StackFrame>);
1548
1549#[turbo_tasks::function]
1550pub async fn get_source_map_rope(
1551    container: Vc<ProjectContainer>,
1552    source_url: RcStr,
1553) -> Result<Vc<FileContent>> {
1554    let (file_path_sys, module) = match Url::parse(&source_url) {
1555        Ok(url) => match url.scheme() {
1556            "file" => {
1557                let path = match url.to_file_path() {
1558                    Ok(path) => path.to_string_lossy().into(),
1559                    Err(_) => {
1560                        bail!("Failed to convert file URL to file path: {url}");
1561                    }
1562                };
1563                let module = url.query_pairs().find(|(k, _)| k == "id");
1564                (
1565                    path,
1566                    match module {
1567                        Some(module) => Some(urlencoding::decode(&module.1)?.into_owned().into()),
1568                        None => None,
1569                    },
1570                )
1571            }
1572            _ => bail!("Unknown url scheme '{}'", url.scheme()),
1573        },
1574        Err(_) => (source_url.to_string(), None),
1575    };
1576
1577    let chunk_base_unix =
1578        match file_path_sys.strip_prefix(container.project().dist_dir_absolute().await?.as_str()) {
1579            Some(relative_path) => sys_to_unix(relative_path),
1580            None => {
1581                // File doesn't exist within the dist dir
1582                return Ok(FileContent::NotFound.cell());
1583            }
1584        };
1585
1586    let server_path = container
1587        .project()
1588        .node_root()
1589        .await?
1590        .join(&chunk_base_unix)?;
1591
1592    let client_path = container
1593        .project()
1594        .client_relative_path()
1595        .await?
1596        .join(&chunk_base_unix)?;
1597
1598    let mut map = container.get_source_map(server_path, module.clone());
1599
1600    if !map.await?.is_content() {
1601        // If the chunk doesn't exist as a server chunk, try a client chunk.
1602        // TODO: Properly tag all server chunks and use the `isServer` query param.
1603        // Currently, this is inaccurate as it does not cover RSC server
1604        // chunks.
1605        map = container.get_source_map(client_path, module);
1606        if !map.await?.is_content() {
1607            bail!("chunk/module '{}' is missing a sourcemap", source_url);
1608        }
1609    }
1610
1611    Ok(map)
1612}
1613
1614#[turbo_tasks::function(operation)]
1615pub fn get_source_map_rope_operation(
1616    container: ResolvedVc<ProjectContainer>,
1617    file_path: RcStr,
1618) -> Vc<FileContent> {
1619    get_source_map_rope(*container, file_path)
1620}
1621
1622#[turbo_tasks::function(operation)]
1623pub async fn project_trace_source_operation(
1624    container: ResolvedVc<ProjectContainer>,
1625    frame: StackFrame,
1626    current_directory_file_url: RcStr,
1627) -> Result<Vc<OptionStackFrame>> {
1628    let Some(map) =
1629        &*SourceMap::new_from_rope_cached(get_source_map_rope(*container, frame.file)).await?
1630    else {
1631        return Ok(Vc::cell(None));
1632    };
1633
1634    let Some(line) = frame.line else {
1635        return Ok(Vc::cell(None));
1636    };
1637
1638    let token = map.lookup_token(
1639        line.saturating_sub(1),
1640        frame.column.unwrap_or(1).saturating_sub(1),
1641    );
1642
1643    let (original_file, line, column, method_name) = match token {
1644        Token::Original(token) => (
1645            match urlencoding::decode(&token.original_file)? {
1646                Cow::Borrowed(_) => token.original_file,
1647                Cow::Owned(original_file) => RcStr::from(original_file),
1648            },
1649            // JS stack frames are 1-indexed, source map tokens are 0-indexed
1650            Some(token.original_line + 1),
1651            Some(token.original_column + 1),
1652            token.name,
1653        ),
1654        Token::Synthetic(token) => {
1655            let Some(original_file) = token.guessed_original_file else {
1656                return Ok(Vc::cell(None));
1657            };
1658            (original_file, None, None, None)
1659        }
1660    };
1661
1662    let project_root_uri =
1663        uri_from_file(container.project().project_root_path().owned().await?, None).await? + "/";
1664    let (file, original_file, is_internal) =
1665        if let Some(source_file) = original_file.strip_prefix(&project_root_uri) {
1666            // Client code uses file://
1667            (
1668                RcStr::from(
1669                    get_relative_path_to(&current_directory_file_url, &original_file)
1670                        // TODO(sokra) remove this to include a ./ here to make it a relative path
1671                        .trim_start_matches("./"),
1672                ),
1673                Some(RcStr::from(source_file)),
1674                false,
1675            )
1676        } else if let Some(source_file) = original_file.strip_prefix(&*SOURCE_MAP_PREFIX_PROJECT) {
1677            // Server code uses turbopack:///[project]
1678            // TODO should this also be file://?
1679            (
1680                RcStr::from(
1681                    get_relative_path_to(
1682                        &current_directory_file_url,
1683                        &format!("{project_root_uri}{source_file}"),
1684                    )
1685                    // TODO(sokra) remove this to include a ./ here to make it a relative path
1686                    .trim_start_matches("./"),
1687                ),
1688                Some(RcStr::from(source_file)),
1689                false,
1690            )
1691        } else if let Some(source_file) = original_file.strip_prefix(&*SOURCE_MAP_PREFIX) {
1692            // All other code like turbopack:///[turbopack] is internal code
1693            // TODO(veil): Should the protocol be preserved?
1694            (RcStr::from(source_file), None, true)
1695        } else {
1696            bail!(
1697                "Original file ({}) outside project ({})",
1698                original_file,
1699                project_root_uri
1700            )
1701        };
1702
1703    Ok(Vc::cell(Some(StackFrame {
1704        file,
1705        original_file,
1706        method_name,
1707        line,
1708        column,
1709        is_server: frame.is_server,
1710        is_internal: Some(is_internal),
1711    })))
1712}
1713
1714#[tracing::instrument(level = "info", name = "apply SourceMap to stack frame", skip_all)]
1715#[napi]
1716pub async fn project_trace_source(
1717    #[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External<ProjectInstance>,
1718    frame: StackFrame,
1719    current_directory_file_url: String,
1720) -> napi::Result<Option<StackFrame>> {
1721    let container = project.container;
1722    let ctx = &project.turbopack_ctx;
1723    ctx.turbo_tasks()
1724        .run(async move {
1725            let traced_frame = project_trace_source_operation(
1726                container,
1727                frame,
1728                RcStr::from(current_directory_file_url),
1729            )
1730            .read_strongly_consistent()
1731            .await?;
1732            Ok(ReadRef::into_owned(traced_frame))
1733        })
1734        // HACK: Don't use `TurbopackInternalError`, this function is race-condition prone (the
1735        // source files may have changed or been deleted), so these probably aren't internal errors?
1736        // Ideally we should differentiate.
1737        .await
1738        .map_err(|e| napi::Error::from_reason(PrettyPrintError(&e.into()).to_string()))
1739}
1740
1741#[tracing::instrument(level = "info", name = "get source content for asset", skip_all)]
1742#[napi]
1743pub async fn project_get_source_for_asset(
1744    #[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External<ProjectInstance>,
1745    file_path: RcStr,
1746) -> napi::Result<Option<String>> {
1747    let container = project.container;
1748    let ctx = &project.turbopack_ctx;
1749    ctx.turbo_tasks()
1750        .run(async move {
1751            let source_content = &*container
1752                .project()
1753                .project_path()
1754                .await?
1755                .fs()
1756                .root()
1757                .await?
1758                .join(&file_path)?
1759                .read()
1760                .await?;
1761
1762            let FileContent::Content(source_content) = source_content else {
1763                bail!("Cannot find source for asset {}", file_path);
1764            };
1765
1766            Ok(Some(source_content.content().to_str()?.into_owned()))
1767        })
1768        // HACK: Don't use `TurbopackInternalError`, this function is race-condition prone (the
1769        // source files may have changed or been deleted), so these probably aren't internal errors?
1770        // Ideally we should differentiate.
1771        .await
1772        .map_err(|e| napi::Error::from_reason(PrettyPrintError(&e.into()).to_string()))
1773}
1774
1775#[tracing::instrument(level = "info", name = "get SourceMap for asset", skip_all)]
1776#[napi]
1777pub async fn project_get_source_map(
1778    #[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External<ProjectInstance>,
1779    file_path: RcStr,
1780) -> napi::Result<Option<String>> {
1781    let container = project.container;
1782    let ctx = &project.turbopack_ctx;
1783    ctx.turbo_tasks()
1784        .run(async move {
1785            let source_map = get_source_map_rope_operation(container, file_path)
1786                .read_strongly_consistent()
1787                .await?;
1788            let Some(map) = source_map.as_content() else {
1789                return Ok(None);
1790            };
1791            Ok(Some(map.content().to_str()?.to_string()))
1792        })
1793        // HACK: Don't use `TurbopackInternalError`, this function is race-condition prone (the
1794        // source files may have changed or been deleted), so these probably aren't internal errors?
1795        // Ideally we should differentiate.
1796        .await
1797        .map_err(|e| napi::Error::from_reason(PrettyPrintError(&e.into()).to_string()))
1798}
1799
1800#[napi]
1801pub fn project_get_source_map_sync(
1802    #[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External<ProjectInstance>,
1803    file_path: RcStr,
1804) -> napi::Result<Option<String>> {
1805    within_runtime_if_available(|| {
1806        tokio::runtime::Handle::current().block_on(project_get_source_map(project, file_path))
1807    })
1808}
1809
1810#[napi]
1811pub async fn project_write_analyze_data(
1812    #[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External<ProjectInstance>,
1813    app_dir_only: bool,
1814) -> napi::Result<TurbopackResult<()>> {
1815    let container = project.container;
1816    let (issues, diagnostics) = project
1817        .turbopack_ctx
1818        .turbo_tasks()
1819        .run_once(async move {
1820            let analyze_data_op = write_analyze_data_with_issues_operation(container, app_dir_only);
1821            let WriteAnalyzeResult {
1822                issues,
1823                diagnostics,
1824                effects,
1825            } = &*analyze_data_op.read_strongly_consistent().await?;
1826
1827            // Write the files to disk
1828            effects.apply().await?;
1829            Ok((issues.clone(), diagnostics.clone()))
1830        })
1831        .await
1832        .map_err(|e| napi::Error::from_reason(PrettyPrintError(&e).to_string()))?;
1833
1834    Ok(TurbopackResult {
1835        result: (),
1836        issues: issues.iter().map(|i| NapiIssue::from(&**i)).collect(),
1837        diagnostics: diagnostics
1838            .iter()
1839            .map(|d| NapiDiagnostic::from(d))
1840            .collect(),
1841    })
1842}