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
75const 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 pub enable: bool,
117
118 pub poll_interval_ms: Option<f64>,
121}
122
123#[napi(object)]
124pub struct NapiProjectOptions {
125 pub root_path: RcStr,
128
129 pub project_path: RcStr,
131
132 pub dist_dir: RcStr,
135
136 pub watch: NapiWatchOptions,
138
139 pub next_config: RcStr,
141
142 pub js_config: RcStr,
144
145 pub env: Vec<NapiEnvVar>,
147
148 pub define_env: NapiDefineEnv,
151
152 pub dev: bool,
154
155 pub encryption_key: RcStr,
157
158 pub build_id: RcStr,
160
161 pub preview_props: NapiDraftModeOptions,
163
164 pub browserslist_query: RcStr,
166
167 pub no_mangling: bool,
171
172 pub current_node_js_version: RcStr,
174}
175
176#[napi(object)]
178pub struct NapiPartialProjectOptions {
179 pub root_path: Option<RcStr>,
182
183 pub project_path: Option<RcStr>,
185
186 pub dist_dir: Option<Option<RcStr>>,
189
190 pub watch: Option<NapiWatchOptions>,
192
193 pub next_config: Option<RcStr>,
195
196 pub js_config: Option<RcStr>,
198
199 pub env: Option<Vec<NapiEnvVar>>,
201
202 pub define_env: Option<NapiDefineEnv>,
205
206 pub dev: Option<bool>,
208
209 pub encryption_key: Option<RcStr>,
211
212 pub build_id: Option<RcStr>,
214
215 pub preview_props: Option<NapiDraftModeOptions>,
217
218 pub browserslist_query: Option<RcStr>,
220
221 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 pub persistent_caching: Option<bool>,
239 pub memory_limit: Option<f64>,
241 pub dependency_tracking: Option<bool>,
243 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 trace = Some("overview".to_owned());
356 }
357
358 if let Some(mut trace) = trace {
359 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#[tracing::instrument(skip(turbo_tasks))]
509async fn benchmark_file_io(
510 turbo_tasks: NextTurboTasks,
511 directory: FileSystemPath,
512) -> Result<Vc<Completion>> {
513 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 let start = Instant::now();
534 async move {
535 for _ in 0..3 {
536 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 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#[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 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#[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#[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 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 pub pathname: String,
656 pub original_name: Option<RcStr>,
658
659 pub r#type: &'static str,
661
662 pub pages: Option<Vec<AppPageNapiRoute>>,
663
664 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 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 let EntrypointsWithIssues {
891 entrypoints,
892 issues,
893 diagnostics,
894 effects,
895 } = &*entrypoints_with_issues_op
896 .read_strongly_consistent()
897 .await?;
898
899 let now = Instant::now();
901
902 effects.apply().await?;
904
905 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#[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#[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 pub line: Option<u32>,
1387 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 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 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 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 (
1514 RcStr::from(
1515 get_relative_path_to(¤t_directory_file_url, &original_file)
1516 .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 (
1526 RcStr::from(
1527 get_relative_path_to(
1528 ¤t_directory_file_url,
1529 &format!("{project_root_uri}{source_file}"),
1530 )
1531 .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 (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}