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
79const 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 pub enable: bool,
121
122 pub poll_interval_ms: Option<f64>,
125}
126
127#[napi(object)]
128pub struct NapiProjectOptions {
129 pub root_path: RcStr,
133
134 pub project_path: RcStr,
137
138 pub dist_dir: RcStr,
142
143 pub watch: NapiWatchOptions,
145
146 pub next_config: RcStr,
148
149 pub js_config: RcStr,
151
152 pub env: Vec<NapiEnvVar>,
154
155 pub define_env: NapiDefineEnv,
158
159 pub dev: bool,
161
162 pub encryption_key: RcStr,
164
165 pub build_id: RcStr,
167
168 pub preview_props: NapiDraftModeOptions,
170
171 pub browserslist_query: RcStr,
173
174 pub no_mangling: bool,
178
179 pub current_node_js_version: RcStr,
181}
182
183#[napi(object)]
185pub struct NapiPartialProjectOptions {
186 pub root_path: Option<RcStr>,
190
191 pub project_path: Option<RcStr>,
195
196 pub dist_dir: Option<Option<RcStr>>,
200
201 pub watch: Option<NapiWatchOptions>,
203
204 pub next_config: Option<RcStr>,
206
207 pub js_config: Option<RcStr>,
209
210 pub env: Option<Vec<NapiEnvVar>>,
212
213 pub define_env: Option<NapiDefineEnv>,
216
217 pub dev: Option<bool>,
219
220 pub encryption_key: Option<RcStr>,
222
223 pub build_id: Option<RcStr>,
225
226 pub preview_props: Option<NapiDraftModeOptions>,
228
229 pub browserslist_query: Option<RcStr>,
231
232 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 pub persistent_caching: Option<bool>,
250 pub memory_limit: Option<f64>,
252 pub dependency_tracking: Option<bool>,
254 pub is_ci: Option<bool>,
256 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 trace = Some("overview".to_owned());
374 }
375
376 if let Some(mut trace) = trace {
377 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#[tracing::instrument(skip(turbo_tasks))]
532async fn benchmark_file_io(
533 turbo_tasks: NextTurboTasks,
534 directory: FileSystemPath,
535) -> Result<Vc<Completion>> {
536 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 let start = Instant::now();
557 async move {
558 for _ in 0..3 {
559 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 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#[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 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#[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#[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 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 pub pathname: String,
669 pub original_name: Option<RcStr>,
671
672 pub r#type: &'static str,
674
675 pub pages: Option<Vec<AppPageNapiRoute>>,
676
677 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 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 let EntrypointsWithIssues {
904 entrypoints,
905 issues,
906 diagnostics,
907 effects,
908 } = &*entrypoints_with_issues_op
909 .read_strongly_consistent()
910 .await?;
911
912 let now = Instant::now();
914
915 effects.apply().await?;
917
918 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#[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#[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 pub line: Option<u32>,
1400 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 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 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 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 (
1530 RcStr::from(
1531 get_relative_path_to(¤t_directory_file_url, &original_file)
1532 .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 (
1542 RcStr::from(
1543 get_relative_path_to(
1544 ¤t_directory_file_url,
1545 &format!("{project_root_uri}{source_file}"),
1546 )
1547 .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 (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 .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 .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 .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}