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