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