1use std::{
2 borrow::Cow,
3 fs::{canonicalize, create_dir_all},
4 io::Write,
5 path::{Path, PathBuf},
6 sync::{Arc, LazyLock},
7 thread,
8 time::Duration,
9};
10
11use anyhow::{Context, Result, anyhow, bail};
12use bincode::{Decode, Encode};
13use flate2::write::GzEncoder;
14use futures_util::TryFutureExt;
15use napi::{
16 Env, JsFunction, JsObject, Status,
17 bindgen_prelude::{External, within_runtime_if_available},
18 threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode},
19};
20use napi_derive::napi;
21use next_api::{
22 entrypoints::Entrypoints,
23 next_server_nft::next_server_nft_assets,
24 operation::{
25 EntrypointsOperation, InstrumentationOperation, MiddlewareOperation, OptionEndpoint,
26 RouteOperation,
27 },
28 project::{
29 DebugBuildPaths, DefineEnv, DraftModeOptions, HmrTarget, PartialProjectOptions, Project,
30 ProjectContainer, ProjectOptions, WatchOptions,
31 },
32 project_asset_hashes_manifest::immutable_hashes_manifest_asset_if_enabled,
33 route::{Endpoint, EndpointGroupKey, Route},
34 routes_hashes_manifest::routes_hashes_manifest_asset_if_enabled,
35};
36use next_core::{
37 app_structure::find_app_dir,
38 next_config::DIST_PROFILES_DIR_NAME,
39 next_telemetry::ProjectFeatureUsageSummary,
40 tracing_presets::{
41 TRACING_NEXT_OVERVIEW_TARGETS, TRACING_NEXT_TARGETS, TRACING_NEXT_TURBO_TASKS_TARGETS,
42 TRACING_NEXT_TURBOPACK_TARGETS,
43 },
44};
45use rand::RngExt;
46use serde::Serialize;
47use tokio::{io::AsyncWriteExt, runtime::Handle, time::Instant};
48use tracing::Instrument;
49use tracing_subscriber::{Registry, layer::SubscriberExt, util::SubscriberInitExt};
50use turbo_rcstr::{RcStr, rcstr};
51use turbo_tasks::{
52 Effects, FxIndexSet, OperationValue, OperationVc, PrettyPrintError, ReadRef, ResolvedVc,
53 TransientInstance, TryJoinIterExt, TurboTasksApi, TurboTasksCallApi, UpdateInfo, Vc,
54 mark_top_level_task,
55 message_queue::{CompilationEvent, Severity},
56 read_strongly_consistent_and_apply_effects, take_effects,
57 trace::TraceRawVcs,
58 unmark_top_level_task_may_leak_eventually_consistent_state,
59};
60use turbo_tasks_backend::db_invalidation::invalidation_reasons;
61use turbo_tasks_fs::{
62 DiskFileSystem, FileContent, FileSystem, FileSystemPath, canonicalize_to_rcstr, invalidation,
63 to_verbatim_with_case_folded_disk, util::uri_from_file,
64};
65use turbo_unix_path::{get_relative_path_to, unix_to_sys};
66use turbopack_core::{
67 PROJECT_FILESYSTEM_NAME, SOURCE_URL_PROTOCOL,
68 issue::PlainIssue,
69 output::{OutputAsset, OutputAssets},
70 source_map::{SourceMap, Token},
71 version::{PartialUpdate, TotalUpdate, Update, VersionState},
72};
73use turbopack_ecmascript_hmr_protocol::{ClientUpdateInstruction, Issue, ResourceIdentifier};
74use turbopack_trace_utils::{
75 exit::{ExitHandler, ExitReceiver},
76 filter_layer::FilterLayer,
77 raw_trace::RawTraceLayer,
78 trace_writer::TraceWriter,
79};
80use url::Url;
81
82use crate::{
83 next_api::{
84 analyze::{WriteAnalyzeResult, write_analyze_data_with_issues_operation},
85 endpoint::ExternalEndpoint,
86 turbopack_ctx::{
87 MemoryEvictionMode, NapiNextTurbopackCallbacks, NapiNextTurbopackCallbacksJsObject,
88 NextTurboTasks, NextTurbopackContext, create_turbo_tasks,
89 },
90 utils::{
91 DetachedVc, NapiIssue, NapiUsedFeature, RootTask, TurbopackResult, get_issues,
92 strongly_consistent_catch_collectables, subscribe,
93 },
94 },
95 util::DhatProfilerGuard,
96};
97
98const SLOW_FILESYSTEM_THRESHOLD: Duration = Duration::from_millis(200);
101static SOURCE_MAP_PREFIX: LazyLock<String> = LazyLock::new(|| format!("{SOURCE_URL_PROTOCOL}///"));
102static SOURCE_MAP_PREFIX_PROJECT: LazyLock<String> =
103 LazyLock::new(|| format!("{SOURCE_URL_PROTOCOL}///[{PROJECT_FILESYSTEM_NAME}]/"));
104
105#[napi(object)]
106#[derive(Clone, Debug)]
107pub struct NapiEnvVar {
108 pub name: RcStr,
109 pub value: RcStr,
110}
111
112#[napi(object)]
113#[derive(Clone, Debug)]
114pub struct NapiOptionEnvVar {
115 pub name: RcStr,
116 pub value: Option<RcStr>,
117}
118
119#[napi(object)]
120pub struct NapiDraftModeOptions {
121 pub preview_mode_id: RcStr,
122 pub preview_mode_encryption_key: RcStr,
123 pub preview_mode_signing_key: RcStr,
124}
125
126impl From<NapiDraftModeOptions> for DraftModeOptions {
127 fn from(val: NapiDraftModeOptions) -> Self {
128 DraftModeOptions {
129 preview_mode_id: val.preview_mode_id,
130 preview_mode_encryption_key: val.preview_mode_encryption_key,
131 preview_mode_signing_key: val.preview_mode_signing_key,
132 }
133 }
134}
135
136#[napi(object)]
137pub struct NapiWatchOptions {
138 pub enable: bool,
140
141 pub poll_interval_ms: Option<f64>,
144}
145
146#[napi(object)]
147pub struct NapiProjectOptions {
148 pub root_path: RcStr,
152
153 pub project_path: RcStr,
156
157 pub dist_dir: RcStr,
161
162 pub watch: NapiWatchOptions,
164
165 pub next_config: RcStr,
167
168 pub env: Vec<NapiEnvVar>,
170
171 pub define_env: NapiDefineEnv,
174
175 pub dev: bool,
177
178 pub encryption_key: RcStr,
180
181 pub build_id: RcStr,
183
184 pub preview_props: NapiDraftModeOptions,
186
187 pub browserslist_query: RcStr,
189
190 pub no_mangling: bool,
194
195 pub write_routes_hashes_manifest: bool,
197
198 pub current_node_js_version: RcStr,
200
201 pub debug_build_paths: Option<NapiDebugBuildPaths>,
204
205 pub deferred_entries: Option<Vec<RcStr>>,
207
208 pub is_persistent_caching_enabled: bool,
210
211 pub next_version: RcStr,
213
214 pub server_hmr: Option<bool>,
216}
217
218#[napi(object)]
220pub struct NapiPartialProjectOptions {
221 pub root_path: Option<RcStr>,
225
226 pub project_path: Option<RcStr>,
230
231 pub watch: Option<NapiWatchOptions>,
233
234 pub next_config: Option<RcStr>,
236
237 pub env: Option<Vec<NapiEnvVar>>,
239
240 pub define_env: Option<NapiDefineEnv>,
243
244 pub dev: Option<bool>,
246
247 pub encryption_key: Option<RcStr>,
249
250 pub build_id: Option<RcStr>,
252
253 pub preview_props: Option<NapiDraftModeOptions>,
255
256 pub browserslist_query: Option<RcStr>,
258
259 pub write_routes_hashes_manifest: Option<bool>,
261
262 pub no_mangling: Option<bool>,
266}
267
268#[napi(object)]
269#[derive(Clone, Debug)]
270pub struct NapiDefineEnv {
271 pub client: Vec<NapiOptionEnvVar>,
272 pub edge: Vec<NapiOptionEnvVar>,
273 pub nodejs: Vec<NapiOptionEnvVar>,
274}
275
276#[napi(object)]
277pub struct NapiTurboEngineOptions {
278 pub dependency_tracking: Option<bool>,
280 pub is_ci: Option<bool>,
282 pub is_short_session: Option<bool>,
284 pub skip_compaction: Option<bool>,
286 pub turbopack_memory_eviction: MemoryEvictionMode,
288}
289
290impl From<NapiWatchOptions> for WatchOptions {
291 fn from(val: NapiWatchOptions) -> Self {
292 WatchOptions {
293 enable: val.enable,
294 poll_interval: val
295 .poll_interval_ms
296 .filter(|interval| !interval.is_nan() && interval.is_finite() && *interval > 0.0)
297 .map(|interval| Duration::from_secs_f64(interval / 1000.0)),
298 }
299 }
300}
301
302impl From<NapiProjectOptions> for ProjectOptions {
303 fn from(val: NapiProjectOptions) -> Self {
304 let NapiProjectOptions {
305 root_path,
306 project_path,
307 dist_dir: _,
309 watch,
310 next_config,
311 env,
312 define_env,
313 dev,
314 encryption_key,
315 build_id,
316 preview_props,
317 browserslist_query,
318 no_mangling,
319 write_routes_hashes_manifest,
320 current_node_js_version,
321 debug_build_paths,
322 deferred_entries,
323 is_persistent_caching_enabled,
324 next_version,
325 server_hmr,
326 } = val;
327 ProjectOptions {
328 root_path,
329 project_path,
330 watch: watch.into(),
331 next_config,
332 env: env.into_iter().map(|var| (var.name, var.value)).collect(),
333 define_env: define_env.into(),
334 dev,
335 encryption_key,
336 build_id,
337 preview_props: preview_props.into(),
338 browserslist_query,
339 no_mangling,
340 write_routes_hashes_manifest,
341 current_node_js_version,
342 debug_build_paths: debug_build_paths.map(|p| DebugBuildPaths {
343 app: p.app,
344 pages: p.pages,
345 }),
346 deferred_entries,
347 is_persistent_caching_enabled,
348 next_version,
349 server_hmr: server_hmr.unwrap_or(false),
350 }
351 }
352}
353
354impl From<NapiPartialProjectOptions> for PartialProjectOptions {
355 fn from(val: NapiPartialProjectOptions) -> Self {
356 let NapiPartialProjectOptions {
357 root_path,
358 project_path,
359 watch,
360 next_config,
361 env,
362 define_env,
363 dev,
364 encryption_key,
365 build_id,
366 preview_props,
367 browserslist_query,
368 no_mangling,
369 write_routes_hashes_manifest,
370 } = val;
371 PartialProjectOptions {
372 root_path,
373 project_path,
374 watch: watch.map(From::from),
375 next_config,
376 env: env.map(|env| env.into_iter().map(|var| (var.name, var.value)).collect()),
377 define_env: define_env.map(|env| env.into()),
378 dev,
379 encryption_key,
380 build_id,
381 preview_props: preview_props.map(|props| props.into()),
382 browserslist_query,
383 no_mangling,
384 write_routes_hashes_manifest,
385 debug_build_paths: None,
386 }
387 }
388}
389
390impl From<NapiDefineEnv> for DefineEnv {
391 fn from(val: NapiDefineEnv) -> Self {
392 DefineEnv {
393 client: val
394 .client
395 .into_iter()
396 .map(|var| (var.name, var.value))
397 .collect(),
398 edge: val
399 .edge
400 .into_iter()
401 .map(|var| (var.name, var.value))
402 .collect(),
403 nodejs: val
404 .nodejs
405 .into_iter()
406 .map(|var| (var.name, var.value))
407 .collect(),
408 }
409 }
410}
411
412pub struct ProjectInstance {
413 turbopack_ctx: NextTurbopackContext,
414 container: ResolvedVc<ProjectContainer>,
415 exit_receiver: tokio::sync::Mutex<Option<ExitReceiver>>,
416}
417
418#[napi(ts_return_type = "Promise<{ __napiType: \"Project\" }>")]
419pub fn project_new(
420 env: Env,
421 mut options: NapiProjectOptions,
422 turbo_engine_options: NapiTurboEngineOptions,
423 napi_callbacks: NapiNextTurbopackCallbacksJsObject,
424) -> napi::Result<JsObject> {
425 let napi_callbacks = NapiNextTurbopackCallbacks::from_js(&env, napi_callbacks)?;
426 let (exit, exit_receiver) = ExitHandler::new_receiver();
427
428 options.root_path = canonicalize_to_rcstr(Path::new(&*options.root_path)).map_err(|e| {
431 napi::Error::from_reason(PrettyPrintError(&anyhow::Error::from(e)).to_string())
432 })?;
433 create_dir_all(Path::new(&*options.dist_dir))
435 .with_context(|| format!("failed to create dist directory {:?}", options.dist_dir))
436 .map_err(|e| napi::Error::from_reason(PrettyPrintError(&e).to_string()))?;
437 options.dist_dir = canonicalize_to_rcstr(Path::new(&*options.dist_dir)).map_err(|e| {
438 napi::Error::from_reason(PrettyPrintError(&anyhow::Error::from(e)).to_string())
439 })?;
440
441 if let Some(dhat_profiler) = DhatProfilerGuard::try_init() {
442 exit.on_exit(async move {
443 tokio::task::spawn_blocking(move || drop(dhat_profiler))
444 .await
445 .unwrap()
446 });
447 }
448
449 let mut trace = std::env::var("NEXT_TURBOPACK_TRACING")
450 .ok()
451 .filter(|v| !v.is_empty());
452
453 if cfg!(feature = "tokio-console") && trace.is_none() {
454 trace = Some("overview".to_owned());
458 }
459
460 enum Compression {
461 None,
462 GzipFast,
463 GzipBest,
464 }
465 let mut compress = Compression::None;
466 if let Some(mut trace) = trace {
467 let trace_path_override = std::env::var_os("NEXT_TURBOPACK_TRACING_PATH")
468 .filter(|v| !v.is_empty())
469 .map(PathBuf::from);
470 let trace_file = if let Some(path) = trace_path_override {
471 if path.is_absolute() {
472 path
473 } else {
474 std::env::current_dir()
475 .context("Unable to read current working directory")
476 .unwrap()
477 .join(path)
478 }
479 } else {
480 Path::new(&options.root_path)
481 .join(&*unix_to_sys(&options.project_path))
482 .join(DIST_PROFILES_DIR_NAME)
483 .join("trace-turbopack.bin")
485 };
486 let trace_dir = trace_file
487 .parent()
488 .expect("Trace file path must have a parent directory");
489
490 println!("Turbopack tracing enabled with targets: {trace}");
491 println!(" Note that this might have a small performance impact.");
492 println!(" Trace output will be written to {}", trace_file.display());
493
494 trace = trace
495 .split(",")
496 .filter_map(|item| {
497 Some(match item {
499 "overview" | "1" => Cow::Owned(TRACING_NEXT_OVERVIEW_TARGETS.join(",")),
500 "next" => Cow::Owned(TRACING_NEXT_TARGETS.join(",")),
501 "turbopack" => Cow::Owned(TRACING_NEXT_TURBOPACK_TARGETS.join(",")),
502 "turbo-tasks" => Cow::Owned(TRACING_NEXT_TURBO_TASKS_TARGETS.join(",")),
503 "gz" => {
504 compress = Compression::GzipFast;
505 return None;
506 }
507 "gz-best" => {
508 compress = Compression::GzipBest;
509 return None;
510 }
511 _ => Cow::Borrowed(item),
512 })
513 })
514 .intersperse_with(|| Cow::Borrowed(","))
515 .collect::<String>();
516
517 let subscriber = Registry::default();
518
519 if cfg!(feature = "tokio-console") {
520 trace = format!("{trace},tokio=trace,runtime=trace");
521 }
522 #[cfg(feature = "tokio-console")]
523 let subscriber = subscriber.with(console_subscriber::spawn());
524
525 let subscriber = subscriber.with(FilterLayer::try_new(&trace).unwrap());
526
527 create_dir_all(trace_dir)
531 .with_context(|| {
532 format!(
533 "Unable to create trace output directory {}",
534 trace_dir.display()
535 )
536 })
537 .unwrap();
538 let (trace_writer, trace_writer_guard) = match compress {
539 Compression::None => {
540 let trace_writer = std::fs::File::create(trace_file.clone()).unwrap();
541 TraceWriter::new(trace_writer)
542 }
543 Compression::GzipFast => {
544 let trace_writer = std::fs::File::create(trace_file.clone()).unwrap();
545 let trace_writer = GzEncoder::new(trace_writer, flate2::Compression::fast());
546 TraceWriter::new(trace_writer)
547 }
548 Compression::GzipBest => {
549 let trace_writer = std::fs::File::create(trace_file.clone()).unwrap();
550 let trace_writer = GzEncoder::new(trace_writer, flate2::Compression::best());
551 TraceWriter::new(trace_writer)
552 }
553 };
554 let subscriber = subscriber.with(RawTraceLayer::new(trace_writer));
555
556 exit.on_exit(async move {
557 tokio::task::spawn_blocking(move || drop(trace_writer_guard))
558 .await
559 .unwrap();
560 });
561
562 let trace_server = std::env::var("NEXT_TURBOPACK_TRACE_SERVER").ok();
563 if trace_server.is_some() {
564 thread::spawn(move || {
565 turbopack_trace_server::start_turbopack_trace_server(trace_file, None);
566 });
567 println!("Turbopack trace server started. View trace at https://trace.nextjs.org");
568 }
569
570 subscriber.init();
571 }
572
573 env.spawn_future(
574 async move {
575 let dependency_tracking = turbo_engine_options.dependency_tracking.unwrap_or(true);
576 let is_ci = turbo_engine_options.is_ci.unwrap_or(false);
577 let is_short_session = turbo_engine_options.is_short_session.unwrap_or(false);
578 let skip_compaction = turbo_engine_options.skip_compaction.unwrap_or(false);
579 let turbopack_memory_eviction = turbo_engine_options.turbopack_memory_eviction;
580 let turbo_tasks = create_turbo_tasks(
581 PathBuf::from(&options.dist_dir),
582 &options.next_version,
583 options.is_persistent_caching_enabled,
584 dependency_tracking,
585 is_ci,
586 is_short_session,
587 skip_compaction,
588 turbopack_memory_eviction,
589 )?;
590 let turbopack_ctx = NextTurbopackContext::new(turbo_tasks.clone(), napi_callbacks);
591
592 if let Some(stats_path) = std::env::var_os("NEXT_TURBOPACK_TASK_STATISTICS") {
593 let task_stats = turbo_tasks.task_statistics().enable().clone();
594 exit.on_exit(async move {
595 tokio::task::spawn_blocking(move || {
596 let mut file = std::fs::File::create(&stats_path)
597 .with_context(|| format!("failed to create or open {stats_path:?}"))?;
598 serde_json::to_writer(&file, &task_stats)
599 .context("failed to serialize or write task statistics")?;
600 file.flush().context("failed to flush file")
601 })
602 .await
603 .unwrap()
604 .unwrap();
605 });
606 }
607
608 let options = ProjectOptions::from(options);
609 let is_dev = options.dev;
610 let root_path = options.root_path.clone();
611 let container = turbo_tasks
612 .run(async move {
613 let container_op = ProjectContainer::new_operation(rcstr!("next.js"), is_dev);
614 ProjectContainer::initialize(container_op, options).await?;
615 container_op.resolve().strongly_consistent().await
616 })
617 .or_else(|e| turbopack_ctx.throw_turbopack_internal_result(&e.into()))
618 .await?;
619
620 if is_dev {
621 Handle::current().spawn({
622 let tt = turbo_tasks.clone();
623 let root_path = root_path.clone();
624 async move {
625 let result = tt
626 .clone()
627 .run(async move {
628 #[turbo_tasks::function(operation, root)]
629 fn project_node_root_path_operation(
630 container: ResolvedVc<ProjectContainer>,
631 ) -> Vc<FileSystemPath> {
632 container.project().node_root()
633 }
634
635 let mut absolute_benchmark_dir = PathBuf::from(root_path);
636 absolute_benchmark_dir.push(
637 &project_node_root_path_operation(container)
638 .read_strongly_consistent()
639 .await?
640 .path,
641 );
642 benchmark_file_io(&tt, &absolute_benchmark_dir).await
643 })
644 .await;
645 if let Err(err) = result {
646 println!("Failed to benchmark file I/O: {err}");
649 }
650 }
651 .instrument(tracing::info_span!("benchmark file I/O"))
652 });
653 }
654
655 Ok(External::new(ProjectInstance {
656 turbopack_ctx,
657 container,
658 exit_receiver: tokio::sync::Mutex::new(Some(exit_receiver)),
659 }))
660 }
661 .instrument(tracing::info_span!("create project")),
662 )
663}
664
665#[derive(Debug, Clone, Serialize)]
666struct SlowFilesystemEvent {
667 directory: String,
668 duration_ms: u128,
669}
670
671impl CompilationEvent for SlowFilesystemEvent {
672 fn type_name(&self) -> &'static str {
673 "SlowFilesystemEvent"
674 }
675
676 fn severity(&self) -> Severity {
677 Severity::Warning
678 }
679
680 fn message(&self) -> String {
681 format!(
682 "Slow filesystem detected. The benchmark took {}ms. If {} is a network drive, \
683 consider moving it to a local folder.\n\
684 See more: https://nextjs.org/docs/app/guides/local-development",
685 self.duration_ms, self.directory
686 )
687 }
688
689 fn to_json(&self) -> String {
690 serde_json::to_string(self).unwrap()
691 }
692}
693
694async fn benchmark_file_io(turbo_tasks: &NextTurboTasks, dir: &Path) -> Result<()> {
702 let temp_path = dir.join(format!(
703 "tmp_file_io_benchmark_{:x}",
704 rand::random::<u128>()
705 ));
706
707 let mut random_buffer = [0u8; 512];
708 rand::rng().fill(&mut random_buffer[..]);
709
710 let start = Instant::now();
714 async {
715 for _ in 0..3 {
716 let mut file = tokio::fs::File::create(&temp_path).await?;
718 file.write_all(&random_buffer).await?;
719 file.sync_all().await?;
720 drop(file);
721
722 tokio::fs::remove_file(&temp_path).await?;
724 }
725 anyhow::Ok(())
726 }
727 .instrument(tracing::info_span!("benchmark file IO (measurement)", path = %temp_path.display()))
728 .await?;
729
730 let duration = Instant::now().duration_since(start);
731 if duration > SLOW_FILESYSTEM_THRESHOLD {
732 turbo_tasks.send_compilation_event(Arc::new(SlowFilesystemEvent {
733 directory: dir.to_string_lossy().into(),
734 duration_ms: duration.as_millis(),
735 }));
736 }
737
738 Ok(())
739}
740
741#[tracing::instrument(level = "info", name = "update project", skip_all)]
742#[napi]
743pub async fn project_update(
744 #[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External<ProjectInstance>,
745 options: NapiPartialProjectOptions,
746) -> napi::Result<()> {
747 let ctx = &project.turbopack_ctx;
748 let options = options.into();
749 let container = project.container;
750
751 ctx.turbo_tasks()
752 .run(async move { container.update(options).await })
753 .or_else(|e| ctx.throw_turbopack_internal_result(&e.into()))
754 .await
755}
756
757#[napi]
760pub async fn project_invalidate_file_system_cache(
761 #[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External<ProjectInstance>,
762) -> napi::Result<()> {
763 tokio::task::spawn_blocking(move || {
764 project
767 .turbopack_ctx
768 .turbo_tasks()
769 .backend()
770 .invalidate_storage(invalidation_reasons::USER_REQUEST)
771 })
772 .await
773 .context("panicked while invalidating filesystem cache")??;
774 Ok(())
775}
776
777#[napi]
782pub async fn project_on_exit(
783 #[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External<ProjectInstance>,
784) {
785 project_on_exit_internal(&project).await
786}
787
788async fn project_on_exit_internal(project: &ProjectInstance) {
789 let exit_receiver = project.exit_receiver.lock().await.take();
790 exit_receiver
791 .expect("`project.onExitSync` must only be called once")
792 .run_exit_handler()
793 .await;
794}
795
796#[tracing::instrument(level = "info", name = "shutdown project", skip_all)]
802#[napi]
803pub async fn project_shutdown(
804 #[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External<ProjectInstance>,
805) {
806 project.turbopack_ctx.turbo_tasks().stop_and_wait().await;
807 project_on_exit_internal(&project).await;
808}
809
810#[napi(object)]
811#[derive(Default)]
812pub struct AppPageNapiRoute {
813 pub original_name: Option<RcStr>,
815
816 pub html_endpoint: Option<External<ExternalEndpoint>>,
817 pub rsc_hmr_endpoint: Option<External<ExternalEndpoint>>,
818}
819
820#[napi(object)]
821#[derive(Default)]
822pub struct NapiRoute {
823 pub pathname: RcStr,
825 pub original_name: Option<RcStr>,
827
828 pub r#type: &'static str,
830
831 pub pages: Option<Vec<AppPageNapiRoute>>,
832
833 pub endpoint: Option<External<ExternalEndpoint>>,
835 pub html_endpoint: Option<External<ExternalEndpoint>>,
836 pub rsc_hmr_endpoint: Option<External<ExternalEndpoint>>,
837 pub data_endpoint: Option<External<ExternalEndpoint>>,
838}
839
840impl NapiRoute {
841 fn from_route(
842 pathname: RcStr,
843 value: RouteOperation,
844 turbopack_ctx: &NextTurbopackContext,
845 ) -> Self {
846 let convert_endpoint = |endpoint: OperationVc<OptionEndpoint>| {
847 Some(External::new(ExternalEndpoint(DetachedVc::new(
848 turbopack_ctx.clone(),
849 endpoint,
850 ))))
851 };
852 match value {
853 RouteOperation::Page {
854 html_endpoint,
855 data_endpoint,
856 } => NapiRoute {
857 pathname,
858 r#type: "page",
859 html_endpoint: convert_endpoint(html_endpoint),
860 data_endpoint: convert_endpoint(data_endpoint),
861 ..Default::default()
862 },
863 RouteOperation::PageApi { endpoint } => NapiRoute {
864 pathname,
865 r#type: "page-api",
866 endpoint: convert_endpoint(endpoint),
867 ..Default::default()
868 },
869 RouteOperation::AppPage(pages) => NapiRoute {
870 pathname,
871 r#type: "app-page",
872 pages: Some(
873 pages
874 .into_iter()
875 .map(|page_route| AppPageNapiRoute {
876 original_name: Some(page_route.original_name),
877 html_endpoint: convert_endpoint(page_route.html_endpoint),
878 rsc_hmr_endpoint: convert_endpoint(page_route.rsc_hmr_endpoint),
879 })
880 .collect(),
881 ),
882 ..Default::default()
883 },
884 RouteOperation::AppRoute {
885 original_name,
886 endpoint,
887 } => NapiRoute {
888 pathname,
889 original_name: Some(original_name),
890 r#type: "app-route",
891 endpoint: convert_endpoint(endpoint),
892 ..Default::default()
893 },
894 RouteOperation::Conflict => NapiRoute {
895 pathname,
896 r#type: "conflict",
897 ..Default::default()
898 },
899 }
900 }
901}
902
903#[napi(object)]
904pub struct NapiMiddleware {
905 pub endpoint: External<ExternalEndpoint>,
906 pub is_proxy: bool,
907}
908
909impl NapiMiddleware {
910 fn from_middleware(
911 value: &MiddlewareOperation,
912 turbopack_ctx: &NextTurbopackContext,
913 ) -> Result<Self> {
914 Ok(NapiMiddleware {
915 endpoint: External::new(ExternalEndpoint(DetachedVc::new(
916 turbopack_ctx.clone(),
917 value.endpoint,
918 ))),
919 is_proxy: value.is_proxy,
920 })
921 }
922}
923
924#[napi(object)]
925pub struct NapiInstrumentation {
926 pub node_js: External<ExternalEndpoint>,
927 pub edge: External<ExternalEndpoint>,
928}
929
930impl NapiInstrumentation {
931 fn from_instrumentation(
932 value: &InstrumentationOperation,
933 turbopack_ctx: &NextTurbopackContext,
934 ) -> Result<Self> {
935 Ok(NapiInstrumentation {
936 node_js: External::new(ExternalEndpoint(DetachedVc::new(
937 turbopack_ctx.clone(),
938 value.node_js,
939 ))),
940 edge: External::new(ExternalEndpoint(DetachedVc::new(
941 turbopack_ctx.clone(),
942 value.edge,
943 ))),
944 })
945 }
946}
947
948#[napi(object)]
949pub struct NapiEntrypoints {
950 pub routes: Vec<NapiRoute>,
951 pub middleware: Option<NapiMiddleware>,
952 pub instrumentation: Option<NapiInstrumentation>,
953 pub pages_document_endpoint: External<ExternalEndpoint>,
954 pub pages_app_endpoint: External<ExternalEndpoint>,
955 pub pages_error_endpoint: External<ExternalEndpoint>,
956}
957
958impl NapiEntrypoints {
959 fn from_entrypoints_op(
960 entrypoints: &EntrypointsOperation,
961 turbopack_ctx: &NextTurbopackContext,
962 ) -> Result<Self> {
963 let routes = entrypoints
964 .routes
965 .iter()
966 .map(|(k, v)| NapiRoute::from_route(k.clone(), v.clone(), turbopack_ctx))
967 .collect();
968 let middleware = entrypoints
969 .middleware
970 .as_ref()
971 .map(|m| NapiMiddleware::from_middleware(m, turbopack_ctx))
972 .transpose()?;
973 let instrumentation = entrypoints
974 .instrumentation
975 .as_ref()
976 .map(|i| NapiInstrumentation::from_instrumentation(i, turbopack_ctx))
977 .transpose()?;
978 let pages_document_endpoint = External::new(ExternalEndpoint(DetachedVc::new(
979 turbopack_ctx.clone(),
980 entrypoints.pages_document_endpoint,
981 )));
982 let pages_app_endpoint = External::new(ExternalEndpoint(DetachedVc::new(
983 turbopack_ctx.clone(),
984 entrypoints.pages_app_endpoint,
985 )));
986 let pages_error_endpoint = External::new(ExternalEndpoint(DetachedVc::new(
987 turbopack_ctx.clone(),
988 entrypoints.pages_error_endpoint,
989 )));
990 Ok(NapiEntrypoints {
991 routes,
992 middleware,
993 instrumentation,
994 pages_document_endpoint,
995 pages_app_endpoint,
996 pages_error_endpoint,
997 })
998 }
999}
1000
1001#[turbo_tasks::value(serialization = "skip")]
1002struct EntrypointsWithIssues {
1003 entrypoints: Option<ReadRef<EntrypointsOperation>>,
1004 issues: Arc<Vec<ReadRef<PlainIssue>>>,
1005 effects: Arc<Effects>,
1006}
1007
1008#[turbo_tasks::function(operation, root)]
1009async fn get_entrypoints_with_issues_operation(
1010 container: ResolvedVc<ProjectContainer>,
1011) -> Result<Vc<EntrypointsWithIssues>> {
1012 let entrypoints_operation =
1013 EntrypointsOperation::new(project_container_entrypoints_operation(container));
1014 let filter = container.project().issue_filter().await?;
1015 let (entrypoints, issues, effects) =
1016 strongly_consistent_catch_collectables(entrypoints_operation, &filter).await?;
1017 Ok(EntrypointsWithIssues {
1018 entrypoints,
1019 issues,
1020 effects,
1021 }
1022 .cell())
1023}
1024
1025#[turbo_tasks::function(operation, root)]
1026fn project_container_entrypoints_operation(
1027 container: ResolvedVc<ProjectContainer>,
1030) -> Vc<Entrypoints> {
1031 container.entrypoints()
1032}
1033
1034#[turbo_tasks::value(serialization = "skip")]
1035struct OperationResult {
1036 issues: Arc<Vec<ReadRef<PlainIssue>>>,
1037 effects: Arc<Effects>,
1038}
1039
1040#[turbo_tasks::value(serialization = "skip")]
1041struct AllWrittenEntrypointsWithIssues {
1042 entrypoints: Option<ReadRef<EntrypointsOperation>>,
1043 issues: Arc<Vec<ReadRef<PlainIssue>>>,
1044 effects: Arc<Effects>,
1045}
1046
1047#[napi(object)]
1048#[derive(Clone, Debug)]
1049pub struct NapiDebugBuildPaths {
1050 pub app: Vec<RcStr>,
1051 pub pages: Vec<RcStr>,
1052}
1053
1054#[turbo_tasks::task_input]
1055#[derive(Clone, Copy, Debug, Eq, Hash, OperationValue, PartialEq, TraceRawVcs, Encode, Decode)]
1056enum EntrypointsWritePhase {
1057 All,
1058 NonDeferred,
1059 Deferred,
1060}
1061
1062fn normalize_deferred_route(route: &str) -> String {
1063 let with_leading_slash = if route.starts_with('/') {
1064 route.to_owned()
1065 } else {
1066 format!("/{route}")
1067 };
1068
1069 if with_leading_slash.len() > 1 && with_leading_slash.ends_with('/') {
1070 with_leading_slash
1071 .strip_suffix('/')
1072 .unwrap_or_default()
1073 .to_owned()
1074 } else {
1075 with_leading_slash
1076 }
1077}
1078
1079fn is_deferred_app_route(route: &str, deferred_entries: &[RcStr]) -> bool {
1080 let normalized_route = normalize_deferred_route(route);
1081
1082 deferred_entries.iter().any(|entry| {
1083 let normalized_entry = normalize_deferred_route(entry);
1084 normalized_route == normalized_entry
1085 || normalized_route.starts_with(&format!("{normalized_entry}/"))
1086 })
1087}
1088
1089#[derive(Clone, Debug, TraceRawVcs)]
1090struct DeferredPhaseBuildPaths {
1091 non_deferred: DebugBuildPaths,
1092 all: DebugBuildPaths,
1093 deferred_invalidation_dirs: Vec<RcStr>,
1094}
1095
1096fn to_app_debug_path(route: &str, leaf: &'static str) -> RcStr {
1097 let with_leading_slash = if route.starts_with('/') {
1098 route.to_owned()
1099 } else {
1100 format!("/{route}")
1101 };
1102
1103 let normalized_route = if with_leading_slash.len() > 1 && with_leading_slash.ends_with('/') {
1104 with_leading_slash.trim_end_matches('/').to_owned()
1105 } else {
1106 with_leading_slash
1107 };
1108
1109 if normalized_route == "/" {
1110 format!("/{leaf}").into()
1111 } else {
1112 format!("{normalized_route}/{leaf}").into()
1113 }
1114}
1115
1116fn app_entry_source_dir_from_original_name(original_name: &str) -> RcStr {
1117 let normalized_name = normalize_deferred_route(original_name);
1118 let mut segments = normalized_name
1119 .trim_start_matches('/')
1120 .split('/')
1121 .filter(|segment| !segment.is_empty())
1122 .collect::<Vec<_>>();
1123
1124 if !segments.is_empty() {
1125 segments.pop();
1126 }
1127
1128 if segments.is_empty() {
1129 rcstr!("/")
1130 } else {
1131 format!("/{}", segments.join("/")).into()
1132 }
1133}
1134
1135fn compute_deferred_phase_build_paths(
1136 entrypoints: &Entrypoints,
1137 deferred_entries: &[RcStr],
1138) -> DeferredPhaseBuildPaths {
1139 let mut non_deferred_app = FxIndexSet::default();
1140 let mut deferred_app = FxIndexSet::default();
1141 let mut deferred_invalidation_dirs = FxIndexSet::default();
1142 let mut pages = FxIndexSet::default();
1143
1144 for (route_key, route) in entrypoints.routes.iter() {
1145 match route {
1146 Route::Page { .. } | Route::PageApi { .. } => {
1147 pages.insert(route_key.clone());
1148 }
1149 Route::AppPage(app_page_routes) => {
1150 let app_debug_path = to_app_debug_path(route_key.as_str(), "page");
1151 if is_deferred_app_route(route_key.as_str(), deferred_entries) {
1152 deferred_app.insert(app_debug_path);
1153 deferred_invalidation_dirs.extend(app_page_routes.iter().map(|route| {
1154 app_entry_source_dir_from_original_name(route.original_name.as_str())
1155 }));
1156 } else {
1157 non_deferred_app.insert(app_debug_path);
1158 }
1159 }
1160 Route::AppRoute { original_name, .. } => {
1161 let app_debug_path = to_app_debug_path(route_key.as_str(), "route");
1162 if is_deferred_app_route(route_key.as_str(), deferred_entries) {
1163 deferred_app.insert(app_debug_path);
1164 deferred_invalidation_dirs.insert(app_entry_source_dir_from_original_name(
1165 original_name.as_str(),
1166 ));
1167 } else {
1168 non_deferred_app.insert(app_debug_path);
1169 }
1170 }
1171 Route::Conflict => {}
1172 }
1173 }
1174
1175 let pages_vec = pages.into_iter().collect::<Vec<_>>();
1176 let all_app_vec = non_deferred_app
1177 .iter()
1178 .chain(deferred_app.iter())
1179 .cloned()
1180 .collect::<FxIndexSet<_>>()
1181 .into_iter()
1182 .collect::<Vec<_>>();
1183
1184 DeferredPhaseBuildPaths {
1185 non_deferred: DebugBuildPaths {
1186 app: non_deferred_app.into_iter().collect::<Vec<_>>(),
1187 pages: pages_vec.clone(),
1188 },
1189 all: DebugBuildPaths {
1190 app: all_app_vec,
1191 pages: pages_vec,
1192 },
1193 deferred_invalidation_dirs: deferred_invalidation_dirs.into_iter().collect::<Vec<_>>(),
1194 }
1195}
1196
1197async fn invalidate_deferred_entry_source_dirs_after_callback(
1198 container: ResolvedVc<ProjectContainer>,
1199 deferred_invalidation_dirs: Vec<RcStr>,
1200) -> Result<()> {
1201 if deferred_invalidation_dirs.is_empty() {
1202 return Ok(());
1203 }
1204
1205 #[turbo_tasks::value(cell = "new", eq = "manual")]
1206 struct ProjectInfo(Option<FileSystemPath>, DiskFileSystem);
1207
1208 #[turbo_tasks::function(operation, root)]
1209 async fn project_info_operation(
1210 container: ResolvedVc<ProjectContainer>,
1211 ) -> Result<Vc<ProjectInfo>> {
1212 let project = container.project();
1213 let app_dir = find_app_dir(project.project_path().owned().await?)
1214 .owned()
1215 .await?;
1216 let project_fs = project.project_fs().owned().await?;
1217 Ok(ProjectInfo(app_dir, project_fs).cell())
1218 }
1219 let ProjectInfo(app_dir, project_fs) = &*project_info_operation(container)
1220 .read_strongly_consistent()
1221 .await?;
1222
1223 let Some(app_dir) = app_dir else {
1224 return Ok(());
1225 };
1226 let app_dir_sys_path = project_fs.to_sys_path_raw(app_dir);
1230 let paths_to_invalidate = deferred_invalidation_dirs
1231 .into_iter()
1232 .map(|dir| {
1233 let normalized_dir = normalize_deferred_route(dir.as_str());
1234 let relative_dir = normalized_dir.trim_start_matches('/');
1235 if relative_dir.is_empty() {
1236 app_dir_sys_path.clone()
1237 } else {
1238 app_dir_sys_path.join(unix_to_sys(relative_dir).as_ref())
1239 }
1240 })
1241 .collect::<FxIndexSet<_>>()
1242 .into_iter()
1243 .collect::<Vec<_>>();
1244
1245 if paths_to_invalidate.is_empty() {
1246 project_fs.invalidate_with_reason(|path| invalidation::Initialize {
1248 path: RcStr::from(path.to_string_lossy()),
1249 });
1250 } else {
1251 project_fs.invalidate_path_and_children_with_reason(paths_to_invalidate, |path| {
1252 invalidation::Initialize {
1253 path: RcStr::from(path.to_string_lossy()),
1254 }
1255 });
1256 }
1257
1258 Ok(())
1259}
1260
1261fn is_deferred_endpoint_group(key: &EndpointGroupKey, deferred_entries: &[RcStr]) -> bool {
1262 if deferred_entries.is_empty() {
1263 return false;
1264 }
1265
1266 let EndpointGroupKey::Route(route_key) = key else {
1267 return false;
1268 };
1269
1270 is_deferred_app_route(route_key.as_str(), deferred_entries)
1271}
1272
1273fn should_include_endpoint_group(
1274 write_phase: EntrypointsWritePhase,
1275 key: &EndpointGroupKey,
1276 deferred_entries: &[RcStr],
1277) -> bool {
1278 let is_deferred = is_deferred_endpoint_group(key, deferred_entries);
1279
1280 match write_phase {
1281 EntrypointsWritePhase::All => true,
1282 EntrypointsWritePhase::NonDeferred => !is_deferred,
1283 EntrypointsWritePhase::Deferred => is_deferred,
1284 }
1285}
1286
1287async fn app_route_filter_for_write_phase(
1288 project: Vc<Project>,
1289 write_phase: EntrypointsWritePhase,
1290 deferred_entries: &[RcStr],
1291) -> Result<Option<Vec<RcStr>>> {
1292 if matches!(write_phase, EntrypointsWritePhase::All) || deferred_entries.is_empty() {
1293 return Ok(None);
1294 }
1295
1296 let include_deferred = write_phase == EntrypointsWritePhase::Deferred;
1297 let app_project = project.app_project().await?;
1298 let app_route_keys = if let Some(app_project) = &*app_project {
1299 app_project
1300 .route_keys()
1301 .await?
1302 .iter()
1303 .cloned()
1304 .collect::<Vec<_>>()
1305 } else {
1306 Vec::new()
1307 };
1308
1309 Ok(Some(
1310 app_route_keys
1311 .iter()
1312 .filter(|route| {
1313 is_deferred_app_route(route.as_str(), deferred_entries) == include_deferred
1314 })
1315 .cloned()
1316 .collect::<Vec<_>>(),
1317 ))
1318}
1319
1320#[tracing::instrument(level = "info", name = "write all entrypoints to disk", skip_all)]
1321#[napi]
1322pub async fn project_write_all_entrypoints_to_disk(
1323 #[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External<ProjectInstance>,
1324 app_dir_only: bool,
1325) -> napi::Result<TurbopackResult<Option<NapiEntrypoints>>> {
1326 let ctx = &project.turbopack_ctx;
1327 let container = project.container;
1328 let tt = ctx.turbo_tasks();
1329
1330 #[turbo_tasks::function(operation, root)]
1331 async fn has_deferred_entrypoints_operation(
1332 container: ResolvedVc<ProjectContainer>,
1333 ) -> Result<Vc<bool>> {
1334 let project = container.project();
1335 let deferred_entries = project.deferred_entries().owned().await?;
1336
1337 if deferred_entries.is_empty() {
1338 return Ok(Vc::cell(false));
1339 }
1340
1341 let app_project = project.app_project().await?;
1342 let has_deferred = if let Some(app_project) = &*app_project {
1343 app_project
1344 .route_keys()
1345 .await?
1346 .iter()
1347 .any(|route_key| is_deferred_app_route(route_key.as_str(), &deferred_entries))
1348 } else {
1349 false
1350 };
1351
1352 Ok(Vc::cell(has_deferred))
1353 }
1354
1355 let has_deferred_entrypoints = tt
1356 .run(async move {
1357 Ok(*has_deferred_entrypoints_operation(container)
1358 .read_strongly_consistent()
1359 .await?)
1360 })
1361 .or_else(|e| ctx.throw_turbopack_internal_result(&e.into()))
1362 .await?;
1363
1364 let phase_build_paths = if has_deferred_entrypoints {
1365 Some(
1366 tt.run(async move {
1367 #[turbo_tasks::value(serialization = "skip")]
1368 struct DeferredEntrypointInfo(ReadRef<Entrypoints>, ReadRef<Vec<RcStr>>);
1369
1370 #[turbo_tasks::function(operation, root)]
1371 async fn deferred_entrypoint_info_operation(
1372 container: ResolvedVc<ProjectContainer>,
1373 ) -> Result<Vc<DeferredEntrypointInfo>> {
1374 let project = container.project();
1375 Ok(DeferredEntrypointInfo(
1376 project.entrypoints().await?,
1377 project.deferred_entries().await?,
1378 )
1379 .cell())
1380 }
1381
1382 let DeferredEntrypointInfo(entrypoints, deferred_entries) =
1383 &*deferred_entrypoint_info_operation(container)
1384 .read_strongly_consistent()
1385 .await?;
1386
1387 Ok(compute_deferred_phase_build_paths(
1388 entrypoints,
1389 deferred_entries,
1390 ))
1391 })
1392 .or_else(|e| ctx.throw_turbopack_internal_result(&e.into()))
1393 .await?,
1394 )
1395 } else {
1396 None
1397 };
1398
1399 if let Some(phase_build_paths) = phase_build_paths.as_ref() {
1400 let non_deferred_build_paths = phase_build_paths.non_deferred.clone();
1401 tt.run(async move {
1402 container
1403 .update(PartialProjectOptions {
1404 debug_build_paths: Some(non_deferred_build_paths),
1405 ..Default::default()
1406 })
1407 .await?;
1408 Ok(())
1409 })
1410 .or_else(|e| ctx.throw_turbopack_internal_result(&e.into()))
1411 .await?;
1412 }
1413
1414 let first_phase = if has_deferred_entrypoints {
1415 EntrypointsWritePhase::NonDeferred
1416 } else {
1417 EntrypointsWritePhase::All
1418 };
1419
1420 let (mut entrypoints, mut issues) = tt
1421 .run(async move {
1422 let entrypoints_with_issues_op = get_all_written_entrypoints_with_issues_operation(
1423 container,
1424 app_dir_only,
1425 first_phase,
1426 );
1427
1428 let read =
1429 read_strongly_consistent_and_apply_effects(entrypoints_with_issues_op, |v| {
1430 &v.effects
1431 })
1432 .await?;
1433 let AllWrittenEntrypointsWithIssues {
1434 entrypoints,
1435 issues,
1436 ..
1437 } = &*read;
1438
1439 Ok((
1440 entrypoints.clone(),
1441 issues.iter().cloned().collect::<Vec<_>>(),
1442 ))
1443 })
1444 .or_else(|e| ctx.throw_turbopack_internal_result(&e.into()))
1445 .await?;
1446
1447 if has_deferred_entrypoints {
1448 ctx.on_before_deferred_entries().await?;
1449
1450 let deferred_invalidation_dirs = phase_build_paths
1454 .as_ref()
1455 .map(|paths| paths.deferred_invalidation_dirs.clone())
1456 .unwrap_or_default();
1457
1458 tt.run(async move {
1459 invalidate_deferred_entry_source_dirs_after_callback(
1460 container,
1461 deferred_invalidation_dirs,
1462 )
1463 .await?;
1464 Ok(())
1465 })
1466 .or_else(|e| ctx.throw_turbopack_internal_result(&e.into()))
1467 .await?;
1468
1469 if let Some(phase_build_paths) = phase_build_paths.as_ref() {
1470 let all_build_paths = phase_build_paths.all.clone();
1471 tt.run(async move {
1472 container
1473 .update(PartialProjectOptions {
1474 debug_build_paths: Some(all_build_paths),
1475 ..Default::default()
1476 })
1477 .await?;
1478 Ok(())
1479 })
1480 .or_else(|e| ctx.throw_turbopack_internal_result(&e.into()))
1481 .await?;
1482 }
1483
1484 let (deferred_entrypoints, deferred_issues) = tt
1485 .run(async move {
1486 let entrypoints_with_issues_op = get_all_written_entrypoints_with_issues_operation(
1487 container,
1488 app_dir_only,
1489 EntrypointsWritePhase::Deferred,
1490 );
1491
1492 let read =
1493 read_strongly_consistent_and_apply_effects(entrypoints_with_issues_op, |v| {
1494 &v.effects
1495 })
1496 .await?;
1497 let AllWrittenEntrypointsWithIssues {
1498 entrypoints,
1499 issues,
1500 ..
1501 } = &*read;
1502
1503 Ok((
1504 entrypoints.clone(),
1505 issues.iter().cloned().collect::<Vec<_>>(),
1506 ))
1507 })
1508 .or_else(|e| ctx.throw_turbopack_internal_result(&e.into()))
1509 .await?;
1510
1511 if deferred_entrypoints.is_some() {
1512 entrypoints = deferred_entrypoints;
1513 }
1514 issues.extend(deferred_issues);
1515 }
1516
1517 let emit_issues = tt
1518 .run(async move {
1519 let emit_result_op = emit_all_output_assets_once_with_issues_operation(
1520 container,
1521 app_dir_only,
1522 has_deferred_entrypoints,
1523 );
1524 let read =
1525 read_strongly_consistent_and_apply_effects(emit_result_op, |v| &v.effects).await?;
1526 let OperationResult { issues, .. } = &*read;
1527
1528 Ok(issues.clone())
1529 })
1530 .or_else(|e| ctx.throw_turbopack_internal_result(&e.into()))
1531 .await?;
1532
1533 issues.extend(emit_issues.iter().cloned());
1534
1535 Ok(TurbopackResult {
1536 result: if let Some(entrypoints) = entrypoints {
1537 Some(NapiEntrypoints::from_entrypoints_op(
1538 &entrypoints,
1539 &project.turbopack_ctx,
1540 )?)
1541 } else {
1542 None
1543 },
1544 issues: issues.iter().map(|i| NapiIssue::from(&**i)).collect(),
1545 })
1546}
1547
1548#[turbo_tasks::function(operation, root)]
1549async fn get_all_written_entrypoints_with_issues_operation(
1550 container: ResolvedVc<ProjectContainer>,
1551 app_dir_only: bool,
1552 write_phase: EntrypointsWritePhase,
1553) -> Result<Vc<AllWrittenEntrypointsWithIssues>> {
1554 let entrypoints_operation = EntrypointsOperation::new(all_entrypoints_write_to_disk_operation(
1555 container,
1556 app_dir_only,
1557 write_phase,
1558 ));
1559 let filter = container.project().issue_filter().await?;
1560 let (entrypoints, issues, effects) =
1561 strongly_consistent_catch_collectables(entrypoints_operation, &filter).await?;
1562 Ok(AllWrittenEntrypointsWithIssues {
1563 entrypoints,
1564 issues,
1565 effects,
1566 }
1567 .cell())
1568}
1569
1570#[turbo_tasks::function(operation, root)]
1571pub async fn all_entrypoints_write_to_disk_operation(
1572 project: ResolvedVc<ProjectContainer>,
1573 app_dir_only: bool,
1574 write_phase: EntrypointsWritePhase,
1575) -> Result<Vc<Entrypoints>> {
1576 let output_assets_operation = output_assets_operation(project, app_dir_only, write_phase);
1578 let _ = output_assets_operation.connect().await?;
1579
1580 Ok(project.entrypoints())
1581}
1582
1583#[turbo_tasks::function(operation)]
1584async fn output_assets_for_single_emit_operation(
1585 container: ResolvedVc<ProjectContainer>,
1586 app_dir_only: bool,
1587 has_deferred_entrypoints: bool,
1588) -> Result<Vc<OutputAssets>> {
1589 if !has_deferred_entrypoints {
1590 return Ok(
1591 output_assets_operation(container, app_dir_only, EntrypointsWritePhase::All).connect(),
1592 );
1593 }
1594
1595 let non_deferred_output_assets =
1596 output_assets_operation(container, app_dir_only, EntrypointsWritePhase::NonDeferred)
1597 .connect()
1598 .await?;
1599 let deferred_output_assets =
1600 output_assets_operation(container, app_dir_only, EntrypointsWritePhase::Deferred)
1601 .connect()
1602 .await?;
1603
1604 let merged_output_assets: FxIndexSet<ResolvedVc<Box<dyn OutputAsset>>> =
1605 non_deferred_output_assets
1606 .iter()
1607 .chain(deferred_output_assets.iter())
1608 .copied()
1609 .collect();
1610
1611 Ok(Vc::cell(merged_output_assets.into_iter().collect()))
1612}
1613
1614#[turbo_tasks::function(operation, root)]
1615async fn emit_all_output_assets_once_operation(
1616 container: ResolvedVc<ProjectContainer>,
1617 app_dir_only: bool,
1618 has_deferred_entrypoints: bool,
1619) -> Result<Vc<Entrypoints>> {
1620 let output_assets_operation =
1621 output_assets_for_single_emit_operation(container, app_dir_only, has_deferred_entrypoints);
1622 container
1623 .project()
1624 .emit_all_output_assets(output_assets_operation)
1625 .as_side_effect()
1626 .await?;
1627
1628 Ok(container.entrypoints())
1629}
1630
1631#[turbo_tasks::function(operation, root)]
1632async fn emit_all_output_assets_once_with_issues_operation(
1633 container: ResolvedVc<ProjectContainer>,
1634 app_dir_only: bool,
1635 has_deferred_entrypoints: bool,
1636) -> Result<Vc<OperationResult>> {
1637 let entrypoints_operation = EntrypointsOperation::new(emit_all_output_assets_once_operation(
1638 container,
1639 app_dir_only,
1640 has_deferred_entrypoints,
1641 ));
1642 let filter = container.project().issue_filter().await?;
1643 let (_, issues, effects) =
1644 strongly_consistent_catch_collectables(entrypoints_operation, &filter).await?;
1645
1646 Ok(OperationResult { issues, effects }.cell())
1647}
1648
1649#[turbo_tasks::function(operation)]
1650async fn output_assets_operation(
1651 container: ResolvedVc<ProjectContainer>,
1652 app_dir_only: bool,
1653 write_phase: EntrypointsWritePhase,
1654) -> Result<Vc<OutputAssets>> {
1655 let project = container.project();
1656 let deferred_entries = project.deferred_entries().owned().await?;
1657 let app_route_filter =
1658 app_route_filter_for_write_phase(project, write_phase, &deferred_entries).await?;
1659
1660 let endpoint_groups = project
1661 .get_all_endpoint_groups_with_app_route_filter(app_dir_only, app_route_filter)
1662 .await?;
1663
1664 let endpoints = endpoint_groups
1665 .iter()
1666 .filter(|(key, _)| should_include_endpoint_group(write_phase, key, &deferred_entries))
1667 .flat_map(|(_, group)| {
1668 group
1669 .primary
1670 .iter()
1671 .chain(group.additional.iter())
1672 .map(|entry| entry.endpoint)
1673 })
1674 .collect::<Vec<_>>();
1675
1676 let endpoint_assets = endpoints
1677 .iter()
1678 .map(|endpoint| async move { endpoint.output().await?.output_assets.await })
1679 .try_join()
1680 .await?;
1681
1682 let output_assets: FxIndexSet<ResolvedVc<Box<dyn OutputAsset>>> = endpoint_assets
1683 .iter()
1684 .flat_map(|assets| assets.iter().copied())
1685 .collect();
1686
1687 if write_phase == EntrypointsWritePhase::NonDeferred {
1688 return Ok(Vc::cell(output_assets.into_iter().collect()));
1689 }
1690
1691 let whole_app_module_graphs = project.whole_app_module_graphs();
1692 whole_app_module_graphs.as_side_effect().await?;
1694
1695 let nft = next_server_nft_assets(project).await?;
1696 let routes_hashes_manifest = routes_hashes_manifest_asset_if_enabled(project).await?;
1697 let immutable_hashes_manifest_asset =
1698 immutable_hashes_manifest_asset_if_enabled(project).await?;
1699
1700 Ok(Vc::cell(
1701 output_assets
1702 .into_iter()
1703 .chain(nft.iter().copied())
1704 .chain(routes_hashes_manifest.iter().copied())
1705 .chain(immutable_hashes_manifest_asset.iter().copied())
1706 .collect(),
1707 ))
1708}
1709
1710#[tracing::instrument(level = "info", name = "get entrypoints", skip_all)]
1711#[napi]
1712pub async fn project_entrypoints(
1713 #[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External<ProjectInstance>,
1714) -> napi::Result<TurbopackResult<Option<NapiEntrypoints>>> {
1715 let container = project.container;
1716
1717 let (entrypoints, issues) = project
1718 .turbopack_ctx
1719 .turbo_tasks()
1720 .run_once(async move {
1721 let entrypoints_with_issues_op = get_entrypoints_with_issues_operation(container);
1722
1723 let EntrypointsWithIssues {
1725 entrypoints,
1726 issues,
1727 effects: _,
1728 } = &*entrypoints_with_issues_op
1729 .read_strongly_consistent()
1730 .await?;
1731
1732 Ok((entrypoints.clone(), issues.clone()))
1733 })
1734 .await
1735 .map_err(|e| napi::Error::from_reason(PrettyPrintError(&e).to_string()))?;
1736
1737 let result = match entrypoints {
1738 Some(entrypoints) => Some(NapiEntrypoints::from_entrypoints_op(
1739 &entrypoints,
1740 &project.turbopack_ctx,
1741 )?),
1742 None => None,
1743 };
1744
1745 Ok(TurbopackResult {
1746 result,
1747 issues: issues.iter().map(|i| NapiIssue::from(&**i)).collect(),
1748 })
1749}
1750
1751#[tracing::instrument(level = "info", name = "subscribe to entrypoints", skip_all)]
1752#[napi(ts_return_type = "{ __napiType: \"RootTask\" }")]
1753pub fn project_entrypoints_subscribe(
1754 #[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External<ProjectInstance>,
1755 func: JsFunction,
1756) -> napi::Result<External<RootTask>> {
1757 let turbopack_ctx = project.turbopack_ctx.clone();
1758 let container = project.container;
1759 subscribe(
1760 turbopack_ctx.clone(),
1761 func,
1762 move || {
1763 async move {
1764 let entrypoints_with_issues_op = get_entrypoints_with_issues_operation(container);
1765 let read =
1766 read_strongly_consistent_and_apply_effects(entrypoints_with_issues_op, |v| {
1767 &v.effects
1768 })
1769 .await?;
1770 let EntrypointsWithIssues {
1771 entrypoints,
1772 issues,
1773 ..
1774 } = &*read;
1775 Ok((entrypoints.clone(), issues.clone()))
1776 }
1777 .instrument(tracing::info_span!("entrypoints subscription"))
1778 },
1779 move |ctx| {
1780 let (entrypoints, issues) = ctx.value;
1781 let result = match entrypoints {
1782 Some(entrypoints) => Some(NapiEntrypoints::from_entrypoints_op(
1783 &entrypoints,
1784 &turbopack_ctx,
1785 )?),
1786 None => None,
1787 };
1788
1789 Ok(vec![TurbopackResult {
1790 result,
1791 issues: issues
1792 .iter()
1793 .map(|issue| NapiIssue::from(&**issue))
1794 .collect(),
1795 }])
1796 },
1797 )
1798}
1799
1800#[turbo_tasks::value(serialization = "skip")]
1801struct HmrUpdateWithIssues {
1802 update: ReadRef<Update>,
1803 issues: Arc<Vec<ReadRef<PlainIssue>>>,
1804 effects: Arc<Effects>,
1805}
1806
1807#[turbo_tasks::function(operation, root)]
1808fn project_hmr_update_operation(
1809 project: ResolvedVc<Project>,
1810 chunk_name: RcStr,
1811 target: HmrTarget,
1812 state: ResolvedVc<VersionState>,
1813) -> Vc<Update> {
1814 project.hmr_update(chunk_name, target, *state)
1815}
1816
1817#[tracing::instrument(
1818 level = "info",
1819 name = "hmr subscription",
1820 skip_all,
1821 fields(chunk_name = %chunk_name, target = %target),
1822)]
1823#[turbo_tasks::function(operation, root)]
1824async fn hmr_update_with_issues_operation(
1825 project: ResolvedVc<Project>,
1826 chunk_name: RcStr,
1827 state: ResolvedVc<VersionState>,
1828 target: HmrTarget,
1829) -> Result<Vc<HmrUpdateWithIssues>> {
1830 tracing::info!(chunk_name = %chunk_name, target = %target, "hmr subscription");
1831 let update_op = project_hmr_update_operation(project, chunk_name, target, state);
1832 let update = update_op.read_strongly_consistent().await?;
1837 let filter = project.issue_filter().await?;
1838 let issues = get_issues(update_op, &filter).await?;
1839 let effects = Arc::new(take_effects(update_op).await?);
1840 Ok(HmrUpdateWithIssues {
1841 update,
1842 issues,
1843 effects,
1844 }
1845 .cell())
1846}
1847
1848#[tracing::instrument(level = "info", name = "get HMR events", skip(project, func), fields(target = %target, chunk_name = %chunk_name))]
1849#[napi(ts_return_type = "{ __napiType: \"RootTask\" }")]
1850pub fn project_hmr_events(
1851 #[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External<ProjectInstance>,
1852 chunk_name: RcStr,
1853 target: String,
1854 func: JsFunction,
1855) -> napi::Result<External<RootTask>> {
1856 let hmr_target = target
1857 .parse::<HmrTarget>()
1858 .map_err(napi::Error::from_reason)?;
1859
1860 let container = project.container;
1861 let session = TransientInstance::new(());
1862 subscribe(
1863 project.turbopack_ctx.clone(),
1864 func,
1865 {
1866 let outer_chunk_name = chunk_name.clone();
1867 let session = session.clone();
1868 move || {
1869 let chunk_name: RcStr = outer_chunk_name.clone();
1870 let session = session.clone();
1871 async move {
1872 unmark_top_level_task_may_leak_eventually_consistent_state();
1874 let project = container.project().to_resolved().await?;
1875 let state = project
1876 .hmr_version_state(chunk_name.clone(), hmr_target, session)
1877 .to_resolved()
1878 .await?;
1879
1880 let update_op = hmr_update_with_issues_operation(
1881 project,
1882 chunk_name.clone(),
1883 state,
1884 hmr_target,
1885 );
1886 mark_top_level_task();
1888 let read =
1889 read_strongly_consistent_and_apply_effects(update_op, |v| &v.effects)
1890 .await?;
1891 unmark_top_level_task_may_leak_eventually_consistent_state();
1893 let HmrUpdateWithIssues { update, issues, .. } = &*read;
1894 match &**update {
1895 Update::Missing | Update::None => {}
1896 Update::Total(TotalUpdate { to }) => {
1897 state.set(to.clone()).await?;
1898 }
1899 Update::Partial(PartialUpdate { to, .. }) => {
1900 state.set(to.clone()).await?;
1901 }
1902 }
1903 Ok((Some(update.clone()), issues.clone()))
1904 }
1905 }
1906 },
1907 move |ctx| {
1908 let (update, issues) = ctx.value;
1909
1910 let napi_issues = issues
1911 .iter()
1912 .map(|issue| NapiIssue::from(&**issue))
1913 .collect();
1914 let update_issues = issues
1915 .iter()
1916 .map(|issue| Issue::from(&**issue))
1917 .collect::<Vec<_>>();
1918
1919 let identifier = ResourceIdentifier {
1920 path: chunk_name.clone(),
1921 headers: None,
1922 };
1923 let update = match update.as_deref() {
1924 None | Some(Update::Missing) | Some(Update::Total(_)) => {
1925 ClientUpdateInstruction::restart(&identifier, &update_issues)
1926 }
1927 Some(Update::Partial(update)) => ClientUpdateInstruction::partial(
1928 &identifier,
1929 &update.instruction,
1930 &update_issues,
1931 ),
1932 Some(Update::None) => ClientUpdateInstruction::issues(&identifier, &update_issues),
1933 };
1934
1935 Ok(vec![TurbopackResult {
1936 result: ctx.env.to_js_value(&update)?,
1937 issues: napi_issues,
1938 }])
1939 },
1940 )
1941}
1942
1943#[napi(object)]
1944struct HmrChunkNames {
1945 pub chunk_names: Vec<RcStr>,
1946}
1947
1948#[turbo_tasks::value(serialization = "skip")]
1949struct HmrChunkNamesWithIssues {
1950 chunk_names: ReadRef<Vec<RcStr>>,
1951 issues: Arc<Vec<ReadRef<PlainIssue>>>,
1952 effects: Arc<Effects>,
1953}
1954
1955#[turbo_tasks::function(operation, root)]
1956fn project_hmr_chunk_names_operation(
1957 container: ResolvedVc<ProjectContainer>,
1958 target: HmrTarget,
1959) -> Vc<Vec<RcStr>> {
1960 container.hmr_chunk_names(target)
1961}
1962
1963#[turbo_tasks::function(operation, root)]
1964async fn get_hmr_chunk_names_with_issues_operation(
1965 container: ResolvedVc<ProjectContainer>,
1966 target: HmrTarget,
1967) -> Result<Vc<HmrChunkNamesWithIssues>> {
1968 let hmr_chunk_names_op = project_hmr_chunk_names_operation(container, target);
1969 let hmr_chunk_names = hmr_chunk_names_op.read_strongly_consistent().await?;
1976 let filter = container.project().issue_filter().await?;
1977 let issues = get_issues(hmr_chunk_names_op, &filter).await?;
1978 let effects = Arc::new(take_effects(hmr_chunk_names_op).await?);
1979 Ok(HmrChunkNamesWithIssues {
1980 chunk_names: hmr_chunk_names,
1981 issues,
1982 effects,
1983 }
1984 .cell())
1985}
1986
1987#[tracing::instrument(level = "info", name = "get HMR chunk names", skip(project, func), fields(target = %target))]
1988#[napi(ts_return_type = "{ __napiType: \"RootTask\" }")]
1989pub fn project_hmr_chunk_names_subscribe(
1990 #[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External<ProjectInstance>,
1991 target: String,
1992 func: JsFunction,
1993) -> napi::Result<External<RootTask>> {
1994 let hmr_target = target
1995 .parse::<HmrTarget>()
1996 .map_err(napi::Error::from_reason)?;
1997
1998 let container = project.container;
1999 subscribe(
2000 project.turbopack_ctx.clone(),
2001 func,
2002 move || async move {
2003 let hmr_chunk_names_with_issues_op =
2004 get_hmr_chunk_names_with_issues_operation(container, hmr_target);
2005 let read =
2006 read_strongly_consistent_and_apply_effects(hmr_chunk_names_with_issues_op, |v| {
2007 &v.effects
2008 })
2009 .await?;
2010 let HmrChunkNamesWithIssues {
2011 chunk_names,
2012 issues,
2013 ..
2014 } = &*read;
2015
2016 Ok((chunk_names.clone(), issues.clone()))
2017 },
2018 move |ctx| {
2019 let (chunk_names, issues) = ctx.value;
2020
2021 Ok(vec![TurbopackResult {
2022 result: HmrChunkNames {
2023 chunk_names: ReadRef::into_owned(chunk_names),
2024 },
2025 issues: issues
2026 .iter()
2027 .map(|issue| NapiIssue::from(&**issue))
2028 .collect(),
2029 }])
2030 },
2031 )
2032}
2033
2034pub enum UpdateMessage {
2035 Start,
2036 End(UpdateInfo),
2037}
2038
2039#[napi(object)]
2040struct NapiUpdateMessage {
2041 pub update_type: &'static str,
2042 pub value: Option<NapiUpdateInfo>,
2043}
2044
2045impl From<UpdateMessage> for NapiUpdateMessage {
2046 fn from(update_message: UpdateMessage) -> Self {
2047 match update_message {
2048 UpdateMessage::Start => NapiUpdateMessage {
2049 update_type: "start",
2050 value: None,
2051 },
2052 UpdateMessage::End(info) => NapiUpdateMessage {
2053 update_type: "end",
2054 value: Some(info.into()),
2055 },
2056 }
2057 }
2058}
2059
2060#[napi(object)]
2061struct NapiUpdateInfo {
2062 pub duration: u32,
2063 pub tasks: u32,
2064}
2065
2066impl From<UpdateInfo> for NapiUpdateInfo {
2067 fn from(update_info: UpdateInfo) -> Self {
2068 Self {
2069 duration: update_info.duration.as_millis() as u32,
2070 tasks: update_info.tasks as u32,
2071 }
2072 }
2073}
2074
2075#[napi]
2087pub fn project_update_info_subscribe(
2088 #[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External<ProjectInstance>,
2089 aggregation_ms: u32,
2090 func: JsFunction,
2091) -> napi::Result<()> {
2092 let func: ThreadsafeFunction<UpdateMessage> = func.create_threadsafe_function(0, |ctx| {
2093 let message = ctx.value;
2094 Ok(vec![NapiUpdateMessage::from(message)])
2095 })?;
2096 tokio::spawn(async move {
2097 let tt = project.turbopack_ctx.turbo_tasks();
2098 loop {
2099 let update_info = tt
2100 .aggregated_update_info(Duration::ZERO, Duration::ZERO)
2101 .await;
2102
2103 func.call(
2104 Ok(UpdateMessage::Start),
2105 ThreadsafeFunctionCallMode::NonBlocking,
2106 );
2107
2108 let update_info = match update_info {
2109 Some(update_info) => update_info,
2110 None => {
2111 tt.get_or_wait_aggregated_update_info(Duration::from_millis(
2112 aggregation_ms.into(),
2113 ))
2114 .await
2115 }
2116 };
2117
2118 let status = func.call(
2119 Ok(UpdateMessage::End(update_info)),
2120 ThreadsafeFunctionCallMode::NonBlocking,
2121 );
2122
2123 if !matches!(status, Status::Ok) {
2124 let error = anyhow!("Error calling JS function: {}", status);
2125 eprintln!("{error}");
2126 break;
2127 }
2128 }
2129 });
2130 Ok(())
2131}
2132
2133#[napi]
2135pub fn project_compilation_events_subscribe(
2136 #[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External<ProjectInstance>,
2137 func: JsFunction,
2138 event_types: Option<Vec<String>>,
2139) -> napi::Result<()> {
2140 let tsfn: ThreadsafeFunction<Arc<dyn CompilationEvent>> =
2141 func.create_threadsafe_function(0, |ctx| {
2142 let event: Arc<dyn CompilationEvent> = ctx.value;
2143
2144 let env = ctx.env;
2145 let mut obj = env.create_object()?;
2146 obj.set_named_property("typeName", event.type_name())?;
2147 obj.set_named_property("severity", event.severity().to_string())?;
2148 obj.set_named_property("message", event.message())?;
2149 obj.set_named_property("eventJson", event.to_json())?;
2150
2151 let external = env.create_external(event, None);
2152 obj.set_named_property("eventData", external)?;
2153
2154 Ok(vec![obj])
2155 })?;
2156
2157 tokio::spawn(async move {
2158 let tt = project.turbopack_ctx.turbo_tasks();
2159 let mut receiver = tt.subscribe_to_compilation_events(event_types);
2160 while let Some(msg) = receiver.recv().await {
2161 let status = tsfn.call(Ok(msg), ThreadsafeFunctionCallMode::Blocking);
2162
2163 if status != Status::Ok {
2164 break;
2165 }
2166 }
2167 let _ = tsfn.call(
2171 Err(napi::Error::new(
2172 Status::Cancelled,
2173 "compilation events subscription closed",
2174 )),
2175 ThreadsafeFunctionCallMode::Blocking,
2176 );
2177 });
2178
2179 Ok(())
2180}
2181
2182#[napi(object)]
2183#[turbo_tasks::task_input]
2184#[derive(Clone, Debug, Eq, Hash, OperationValue, PartialEq, TraceRawVcs, Encode, Decode)]
2185pub struct StackFrame {
2186 pub is_server: bool,
2187 pub is_ignored: Option<bool>,
2188 pub original_file: Option<RcStr>,
2189 pub file: RcStr,
2190 pub line: Option<u32>,
2192 pub column: Option<u32>,
2194 pub method_name: Option<RcStr>,
2195}
2196
2197#[turbo_tasks::value(transparent)]
2198#[derive(Clone)]
2199pub struct OptionStackFrame(Option<StackFrame>);
2200
2201fn parse_and_canonicalize_source_url(source_url: &str) -> Result<(RcStr, Option<RcStr>)> {
2212 let (path, module) = match Url::parse(source_url) {
2213 Ok(url) => match url.scheme() {
2214 "file" => {
2215 let Ok(path) = url.to_file_path() else {
2216 bail!("Failed to convert file URL to file path: {url}");
2217 };
2218 let module = url.query_pairs().find(|(k, _)| k == "id");
2219 (
2220 path,
2221 match module {
2222 Some(module) => Some(urlencoding::decode(&module.1)?.into_owned().into()),
2223 None => None,
2224 },
2225 )
2226 }
2227 _ => bail!("Unknown url scheme '{}'", url.scheme()),
2228 },
2229 Err(_) => (PathBuf::from(source_url), None),
2230 };
2231
2232 let path = match canonicalize(&path) {
2235 Ok(canonical) => canonical,
2236 Err(_) => {
2237 if cfg!(windows) {
2240 to_verbatim_with_case_folded_disk(&path).unwrap_or(path)
2241 } else {
2242 path
2243 }
2244 }
2245 };
2246
2247 let path = path
2248 .into_string()
2249 .map(RcStr::from)
2250 .map_err(|p| anyhow!("path {p:?} is not valid unicode"))?;
2251 Ok((path, module))
2252}
2253
2254#[turbo_tasks::function]
2258async fn get_source_map_rope(
2259 container: Vc<ProjectContainer>,
2260 sys_path: RcStr,
2261 module: Option<RcStr>,
2262) -> Result<Vc<FileContent>> {
2263 let sys_path = Path::new(&*sys_path);
2264
2265 let project = container.project();
2266 let output_fs = project.output_fs().to_resolved().await?;
2267 let Some(fs_path) = output_fs
2268 .await?
2269 .try_from_sys_path(output_fs, sys_path, None)
2270 else {
2271 return Ok(FileContent::NotFound.cell());
2273 };
2274
2275 let Some(chunk_base_unix) = project.node_root().await?.get_path_to(&fs_path) else {
2276 return Ok(FileContent::NotFound.cell());
2278 };
2279
2280 let client_path = project
2281 .client_relative_path()
2282 .await?
2283 .join(chunk_base_unix)?;
2284
2285 let mut map = container.get_source_map(fs_path, module.clone());
2288
2289 if !map.await?.is_content() {
2290 map = container.get_source_map(client_path, module.clone());
2295 if !map.await?.is_content() {
2296 bail!("chunk/module {sys_path:?} (module: {module:?}) is missing a sourcemap");
2297 }
2298 }
2299
2300 Ok(map)
2301}
2302
2303#[turbo_tasks::function(operation, root)]
2306fn get_source_map_rope_operation(
2307 container: ResolvedVc<ProjectContainer>,
2308 file_path_sys: RcStr,
2309 module: Option<RcStr>,
2310) -> Vc<FileContent> {
2311 get_source_map_rope(*container, file_path_sys, module)
2312}
2313
2314#[turbo_tasks::function(operation, root)]
2318async fn project_trace_source_operation(
2319 container: ResolvedVc<ProjectContainer>,
2320 frame: StackFrame,
2321 frame_file_path_sys: RcStr,
2322 frame_module: Option<RcStr>,
2323 current_directory_file_url: RcStr,
2324) -> Result<Vc<OptionStackFrame>> {
2325 let Some(map) = &*SourceMap::new_from_rope_cached(get_source_map_rope(
2326 *container,
2327 frame_file_path_sys,
2328 frame_module,
2329 ))
2330 .await?
2331 else {
2332 return Ok(Vc::cell(None));
2333 };
2334
2335 let Some(line) = frame.line else {
2336 return Ok(Vc::cell(None));
2337 };
2338
2339 let token = map.lookup_token(
2340 line.saturating_sub(1),
2341 frame.column.unwrap_or(1).saturating_sub(1),
2342 );
2343
2344 let (original_file, line, column, method_name, is_ignored) = match token {
2345 Token::Original(token) => (
2346 token.original_file,
2348 Some(token.original_line + 1),
2350 Some(token.original_column + 1),
2351 token.name,
2352 token.is_ignored,
2353 ),
2354 Token::Synthetic(token) => {
2355 let Some(original_file) = token.guessed_original_file else {
2356 return Ok(Vc::cell(None));
2357 };
2358 (original_file, None, None, None, false)
2359 }
2360 };
2361
2362 fn decode_uri_fragment(value: &str) -> Result<RcStr> {
2364 Ok(match urlencoding::decode(value)? {
2365 Cow::Borrowed(borrowed) => RcStr::from(borrowed),
2366 Cow::Owned(owned) => RcStr::from(owned),
2367 })
2368 }
2369
2370 let project_root_uri =
2371 uri_from_file(container.project().project_root_path().owned().await?, None).await? + "/";
2372 let current_directory_path = decode_uri_fragment(¤t_directory_file_url)?;
2375 let (file, original_file) =
2376 if let Some(source_file) = original_file.strip_prefix(&project_root_uri) {
2377 (
2379 RcStr::from(
2380 get_relative_path_to(
2381 ¤t_directory_path,
2382 &decode_uri_fragment(&original_file)?,
2383 )
2384 .trim_start_matches("./"),
2386 ),
2387 Some(decode_uri_fragment(source_file)?),
2388 )
2389 } else if let Some(source_file) = original_file.strip_prefix(&*SOURCE_MAP_PREFIX_PROJECT) {
2390 let source_file = decode_uri_fragment(source_file)?;
2393 (
2394 RcStr::from(
2395 get_relative_path_to(
2396 ¤t_directory_path,
2397 &format!("{}{}", decode_uri_fragment(&project_root_uri)?, source_file),
2398 )
2399 .trim_start_matches("./"),
2401 ),
2402 Some(source_file),
2403 )
2404 } else if let Some(source_file) = original_file.strip_prefix(&*SOURCE_MAP_PREFIX) {
2405 (decode_uri_fragment(source_file)?, None)
2407 } else {
2408 bail!(
2409 "Original file ({}) outside project ({})",
2410 original_file,
2411 project_root_uri
2412 )
2413 };
2414
2415 Ok(Vc::cell(Some(StackFrame {
2416 file,
2417 original_file,
2418 method_name,
2419 line,
2420 column,
2421 is_server: frame.is_server,
2422 is_ignored: Some(is_ignored),
2423 })))
2424}
2425
2426#[tracing::instrument(level = "info", name = "apply SourceMap to stack frame", skip_all)]
2427#[napi]
2428pub async fn project_trace_source(
2429 #[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External<ProjectInstance>,
2430 frame: StackFrame,
2431 current_directory_file_url: String,
2432) -> napi::Result<Option<StackFrame>> {
2433 let container = project.container;
2434 let ctx = &project.turbopack_ctx;
2435 let (frame_file_path_sys, frame_module) = parse_and_canonicalize_source_url(&frame.file)
2438 .map_err(|e| napi::Error::from_reason(PrettyPrintError(&e).to_string()))?;
2439 ctx.turbo_tasks()
2440 .run(async move {
2441 let traced_frame = project_trace_source_operation(
2442 container,
2443 frame,
2444 frame_file_path_sys,
2445 frame_module,
2446 RcStr::from(current_directory_file_url),
2447 )
2448 .read_strongly_consistent()
2449 .await?;
2450 Ok(ReadRef::into_owned(traced_frame))
2451 })
2452 .await
2456 .map_err(|e| napi::Error::from_reason(PrettyPrintError(&e.into()).to_string()))
2457}
2458
2459#[tracing::instrument(level = "info", name = "get source content for asset", skip_all)]
2460#[napi]
2461pub async fn project_get_source_for_asset(
2462 #[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External<ProjectInstance>,
2463 file_path: RcStr,
2464) -> napi::Result<Option<String>> {
2465 let container = project.container;
2466 let ctx = &project.turbopack_ctx;
2467 ctx.turbo_tasks()
2468 .run(async move {
2469 #[turbo_tasks::function(operation, root)]
2470 async fn source_content_operation(
2471 container: ResolvedVc<ProjectContainer>,
2472 file_path: RcStr,
2473 ) -> Result<Vc<FileContent>> {
2474 let project_path = container.project().project_path().await?;
2475 Ok(project_path.fs().root().await?.join(&file_path)?.read())
2476 }
2477
2478 let source_content = &*source_content_operation(container, file_path.clone())
2479 .read_strongly_consistent()
2480 .await?;
2481
2482 let FileContent::Content(source_content) = source_content else {
2483 bail!("Cannot find source for asset {}", file_path);
2484 };
2485
2486 Ok(Some(source_content.content().to_str()?.into_owned()))
2487 })
2488 .await
2492 .map_err(|e| napi::Error::from_reason(PrettyPrintError(&e.into()).to_string()))
2493}
2494
2495#[tracing::instrument(level = "info", name = "get SourceMap for asset", skip_all)]
2496#[napi]
2497pub async fn project_get_source_map(
2498 #[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External<ProjectInstance>,
2499 source_map_url: RcStr,
2500) -> napi::Result<Option<String>> {
2501 let container = project.container;
2502 let ctx = &project.turbopack_ctx;
2503 let (file_path_sys, module) = parse_and_canonicalize_source_url(&source_map_url)
2506 .map_err(|e| napi::Error::from_reason(PrettyPrintError(&e).to_string()))?;
2507 ctx.turbo_tasks()
2508 .run(async move {
2509 let source_map = get_source_map_rope_operation(container, file_path_sys, module)
2510 .read_strongly_consistent()
2511 .await?;
2512 let Some(map) = source_map.as_content() else {
2513 return Ok(None);
2514 };
2515 Ok(Some(map.content().to_str()?.to_string()))
2516 })
2517 .await
2521 .map_err(|e| napi::Error::from_reason(PrettyPrintError(&e.into()).to_string()))
2522}
2523
2524#[napi]
2525pub fn project_get_source_map_sync(
2526 #[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External<ProjectInstance>,
2527 file_path: RcStr,
2528) -> napi::Result<Option<String>> {
2529 within_runtime_if_available(|| {
2530 tokio::runtime::Handle::current().block_on(project_get_source_map(project, file_path))
2531 })
2532}
2533
2534#[napi]
2535pub async fn project_write_analyze_data(
2536 #[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External<ProjectInstance>,
2537 app_dir_only: bool,
2538) -> napi::Result<TurbopackResult<()>> {
2539 let container = project.container;
2540 let issues = project
2541 .turbopack_ctx
2542 .turbo_tasks()
2543 .run_once(async move {
2544 let analyze_data_op = write_analyze_data_with_issues_operation(container, app_dir_only);
2545 let read =
2547 read_strongly_consistent_and_apply_effects(analyze_data_op, |v| &v.effects).await?;
2548 let WriteAnalyzeResult { issues, .. } = &*read;
2549 Ok(issues.clone())
2550 })
2551 .await
2552 .map_err(|e| napi::Error::from_reason(PrettyPrintError(&e).to_string()))?;
2553
2554 Ok(TurbopackResult {
2555 result: (),
2556 issues: issues.iter().map(|i| NapiIssue::from(&**i)).collect(),
2557 })
2558}
2559
2560#[turbo_tasks::function(operation, root)]
2561async fn get_all_compilation_issues_inner_operation(
2562 container: ResolvedVc<ProjectContainer>,
2563) -> Result<Vc<()>> {
2564 let project = container.project();
2565 project
2571 .whole_app_module_graphs_without_dropping_issues()
2572 .as_side_effect()
2573 .await?;
2574 Ok(Vc::cell(()))
2575}
2576
2577#[turbo_tasks::function(operation, root)]
2578async fn get_all_compilation_issues_operation(
2579 container: ResolvedVc<ProjectContainer>,
2580) -> Result<Vc<OperationResult>> {
2581 let inner_op = get_all_compilation_issues_inner_operation(container);
2582 let filter = container.project().issue_filter().await?;
2583 let (_, issues, effects) = strongly_consistent_catch_collectables(inner_op, &filter).await?;
2584 Ok(OperationResult { issues, effects }.cell())
2585}
2586
2587#[tracing::instrument(level = "info", name = "get project feature usage", skip_all)]
2594#[napi]
2595pub async fn project_feature_usage(
2596 #[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External<ProjectInstance>,
2597) -> napi::Result<Vec<NapiUsedFeature>> {
2598 let container = project.container;
2599 let summary = project
2600 .turbopack_ctx
2601 .turbo_tasks()
2602 .run_once(async move {
2603 #[turbo_tasks::function(operation, root)]
2604 async fn project_feature_usage_operation(
2605 container: ResolvedVc<ProjectContainer>,
2606 ) -> Result<Vc<ProjectFeatureUsageSummary>> {
2607 Ok(container.project().project_feature_usage())
2608 }
2609 project_feature_usage_operation(container)
2610 .read_strongly_consistent()
2611 .await
2612 })
2613 .await
2614 .map_err(|e| napi::Error::from_reason(PrettyPrintError(&e).to_string()))?;
2615
2616 Ok(summary
2617 .features
2618 .iter()
2619 .map(|(name, count)| NapiUsedFeature::new(name.clone(), *count))
2620 .collect())
2621}
2622
2623#[tracing::instrument(level = "info", name = "get all compilation issues", skip_all)]
2624#[napi]
2625pub async fn project_get_all_compilation_issues(
2626 #[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External<ProjectInstance>,
2627) -> napi::Result<TurbopackResult<()>> {
2628 let container = project.container;
2629 let issues = project
2630 .turbopack_ctx
2631 .turbo_tasks()
2632 .run_once(async move {
2633 let op = get_all_compilation_issues_operation(container);
2634 let OperationResult { issues, effects: _ } = &*op.read_strongly_consistent().await?;
2635 Ok(issues.clone())
2636 })
2637 .await
2638 .map_err(|e| napi::Error::from_reason(PrettyPrintError(&e).to_string()))?;
2639
2640 Ok(TurbopackResult {
2641 result: (),
2642 issues: issues.iter().map(|i| NapiIssue::from(&**i)).collect(),
2643 })
2644}
2645
2646#[napi]
2650pub async fn turbopack_database_compact(path: String, next_version: String) -> napi::Result<()> {
2651 let describe = crate::next_api::turbopack_ctx::cache_describe(&next_version);
2652 let version_info = crate::next_api::turbopack_ctx::git_version_info(&describe);
2653 let is_ci = std::env::var("CI").is_ok_and(|v| !v.is_empty());
2654 turbo_tasks_backend::compact_database(&PathBuf::from(path), &version_info, is_ci)
2655 .map_err(|e| napi::Error::from_reason(format!("Database compaction failed: {e}")))?;
2656 Ok(())
2657}