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