1use std::{
2 env::current_dir,
3 future::{Future, join},
4 io::{Write, stdout},
5 net::{IpAddr, SocketAddr},
6 path::{MAIN_SEPARATOR, PathBuf},
7 sync::Arc,
8 time::{Duration, Instant},
9};
10
11use anyhow::{Context, Result};
12use owo_colors::OwoColorize;
13use rustc_hash::FxHashSet;
14use turbo_rcstr::{RcStr, rcstr};
15use turbo_tasks::{
16 NonLocalValue, OperationVc, ResolvedVc, TransientInstance, TurboTasks, UpdateInfo, Vc,
17 trace::TraceRawVcs,
18 util::{FormatBytes, FormatDuration},
19};
20use turbo_tasks_backend::{
21 BackendOptions, GitVersionInfo, StartupCacheState, StorageMode, TurboTasksBackend,
22 noop_backing_storage, turbo_backing_storage,
23};
24use turbo_tasks_fs::FileSystem;
25use turbo_tasks_malloc::TurboMalloc;
26use turbo_unix_path::join_path;
27use turbopack::evaluate_context::node_build_environment;
28use turbopack_cli_utils::issue::{ConsoleUi, LogOptions};
29use turbopack_core::{
30 issue::{IssueReporter, IssueSeverity},
31 resolve::parse::Request,
32 server_fs::ServerFileSystem,
33};
34use turbopack_dev_server::{
35 DevServer, DevServerBuilder, SourceProvider,
36 introspect::IntrospectionSource,
37 source::{
38 ContentSource, combined::CombinedContentSource, router::PrefixedRouterContentSource,
39 static_assets::StaticAssetsContentSource,
40 },
41};
42use turbopack_ecmascript_runtime::RuntimeType;
43use turbopack_env::dotenv::load_env;
44use turbopack_node::{child_process_backend, execution_context::ExecutionContext};
45use turbopack_nodejs::NodeJsChunkingContext;
46
47use self::web_entry_source::create_web_entry_source;
48use crate::{
49 arguments::DevArguments,
50 contexts::NodeEnv,
51 util::{
52 EntryRequest, NormalizedDirs, normalize_dirs, normalize_entries, output_fs, project_fs,
53 },
54};
55
56pub(crate) mod web_entry_source;
57
58pub struct TurbopackDevServerBuilder {
59 turbo_tasks: Arc<TurboTasks<TurboTasksBackend>>,
60 project_dir: RcStr,
61 root_dir: RcStr,
62 entry_requests: Vec<EntryRequest>,
63 eager_compile: bool,
64 hostname: Option<IpAddr>,
65 issue_reporter: Option<Box<dyn IssueReporterProvider>>,
66 port: Option<u16>,
67 browserslist_query: RcStr,
68 log_level: IssueSeverity,
69 show_all: bool,
70 log_detail: bool,
71 allow_retry: bool,
72}
73
74impl TurbopackDevServerBuilder {
75 pub fn new(
76 turbo_tasks: Arc<TurboTasks<TurboTasksBackend>>,
77 project_dir: RcStr,
78 root_dir: RcStr,
79 ) -> TurbopackDevServerBuilder {
80 TurbopackDevServerBuilder {
81 turbo_tasks,
82 project_dir,
83 root_dir,
84 entry_requests: vec![],
85 eager_compile: false,
86 hostname: None,
87 issue_reporter: None,
88 port: None,
89 browserslist_query: "last 1 Chrome versions, last 1 Firefox versions, last 1 Safari \
90 versions, last 1 Edge versions"
91 .into(),
92 log_level: IssueSeverity::Warning,
93 show_all: false,
94 log_detail: false,
95 allow_retry: false,
96 }
97 }
98
99 pub fn entry_request(mut self, entry_asset_path: EntryRequest) -> TurbopackDevServerBuilder {
100 self.entry_requests.push(entry_asset_path);
101 self
102 }
103
104 pub fn eager_compile(mut self, eager_compile: bool) -> TurbopackDevServerBuilder {
105 self.eager_compile = eager_compile;
106 self
107 }
108
109 pub fn hostname(mut self, hostname: IpAddr) -> TurbopackDevServerBuilder {
110 self.hostname = Some(hostname);
111 self
112 }
113
114 pub fn port(mut self, port: u16) -> TurbopackDevServerBuilder {
115 self.port = Some(port);
116 self
117 }
118
119 pub fn browserslist_query(mut self, browserslist_query: RcStr) -> TurbopackDevServerBuilder {
120 self.browserslist_query = browserslist_query;
121 self
122 }
123
124 pub fn log_level(mut self, log_level: IssueSeverity) -> TurbopackDevServerBuilder {
125 self.log_level = log_level;
126 self
127 }
128
129 pub fn show_all(mut self, show_all: bool) -> TurbopackDevServerBuilder {
130 self.show_all = show_all;
131 self
132 }
133
134 pub fn allow_retry(mut self, allow_retry: bool) -> TurbopackDevServerBuilder {
135 self.allow_retry = allow_retry;
136 self
137 }
138
139 pub fn log_detail(mut self, log_detail: bool) -> TurbopackDevServerBuilder {
140 self.log_detail = log_detail;
141 self
142 }
143
144 pub fn issue_reporter(
145 mut self,
146 issue_reporter: Box<dyn IssueReporterProvider>,
147 ) -> TurbopackDevServerBuilder {
148 self.issue_reporter = Some(issue_reporter);
149 self
150 }
151
152 fn find_port(&self, host: IpAddr, port: u16, max_attempts: u16) -> Result<DevServerBuilder> {
154 let max_attempts = max_attempts - 1;
156 let mut attempts = 0;
157 loop {
158 let current_port = port + attempts;
159 let addr = SocketAddr::new(host, current_port);
160 let listen_result = DevServer::listen(addr);
161
162 if let Err(e) = &listen_result
163 && self.allow_retry
164 && attempts < max_attempts
165 {
166 let should_retry = e
170 .source()
171 .and_then(|e| {
172 e.downcast_ref::<std::io::Error>()
173 .map(|e| e.kind() == std::io::ErrorKind::AddrInUse)
174 })
175 .unwrap_or(false);
176
177 if should_retry {
178 println!(
179 "{} - Port {} is in use, trying {} instead",
180 "warn ".yellow(),
181 current_port,
182 current_port + 1
183 );
184 attempts += 1;
185 continue;
186 }
187 }
188
189 return listen_result;
190 }
191 }
192
193 pub async fn build(self) -> Result<DevServer> {
194 let port = self.port.context("port must be set")?;
195 let host = self.hostname.context("hostname must be set")?;
196
197 let server = self.find_port(host, port, 10)?;
198
199 let turbo_tasks = self.turbo_tasks;
200 let project_dir: RcStr = self.project_dir;
201 let root_dir: RcStr = self.root_dir;
202 let eager_compile = self.eager_compile;
203 let show_all = self.show_all;
204 let log_detail: bool = self.log_detail;
205 let browserslist_query: RcStr = self.browserslist_query;
206 let log_args = TransientInstance::new(LogOptions {
207 current_dir: current_dir().unwrap(),
208 project_dir: PathBuf::from(project_dir.clone()),
209 show_all,
210 log_detail,
211 log_level: self.log_level,
212 });
213 let entry_requests = Arc::new(self.entry_requests);
214 let tasks = turbo_tasks.clone();
215 let issue_provider = self.issue_reporter.unwrap_or_else(|| {
216 Box::new(move || Vc::upcast(ConsoleUi::new(log_args.clone())))
218 });
219
220 #[derive(Clone, TraceRawVcs, NonLocalValue)]
221 struct ServerSourceProvider {
222 root_dir: RcStr,
223 project_dir: RcStr,
224 entry_requests: Arc<Vec<EntryRequest>>,
225 eager_compile: bool,
226 browserslist_query: RcStr,
227 }
228 impl SourceProvider for ServerSourceProvider {
229 fn get_source(&self) -> OperationVc<Box<dyn ContentSource>> {
230 source(
231 self.root_dir.clone(),
232 self.project_dir.clone(),
233 self.entry_requests.clone(),
234 self.eager_compile,
235 self.browserslist_query.clone(),
236 )
237 }
238 }
239 let source = ServerSourceProvider {
240 root_dir,
241 project_dir,
242 entry_requests,
243 eager_compile,
244 browserslist_query,
245 };
246
247 let issue_reporter_arc = Arc::new(move || issue_provider.get_issue_reporter());
248 Ok(server.serve(tasks, source, issue_reporter_arc))
249 }
250}
251
252#[turbo_tasks::function(operation, root)]
253async fn source(
254 root_dir: RcStr,
255 project_dir: RcStr,
256 entry_requests: Arc<Vec<EntryRequest>>,
257 eager_compile: bool,
258 browserslist_query: RcStr,
259) -> Result<Vc<Box<dyn ContentSource>>> {
260 let project_relative = project_dir.strip_prefix(&*root_dir).unwrap();
261 let project_relative: RcStr = project_relative
262 .strip_prefix(MAIN_SEPARATOR)
263 .unwrap_or(project_relative)
264 .replace(MAIN_SEPARATOR, "/")
265 .into();
266
267 let output_fs = output_fs(project_dir);
268 const OUTPUT_DIR: &str = ".turbopack/build";
269 let fs: Vc<Box<dyn FileSystem>> = project_fs(
270 root_dir,
271 true,
272 join_path(project_relative.as_str(), OUTPUT_DIR)
273 .unwrap()
274 .into(),
275 );
276 let root_path = fs.root().owned().await?;
277 let project_path = root_path.join(&project_relative)?;
278
279 let env = load_env(root_path.clone());
280 let build_output_root = output_fs.root().await?.join(OUTPUT_DIR)?;
281
282 let build_output_root_to_root_path = project_path
283 .join(OUTPUT_DIR)?
284 .get_relative_path_to(&root_path)
285 .context("Project path is in root path")?;
286 let build_output_root_to_root_path = build_output_root_to_root_path;
287
288 let build_chunking_context = NodeJsChunkingContext::builder(
289 root_path.clone(),
290 build_output_root.clone(),
291 build_output_root_to_root_path,
292 build_output_root.clone(),
293 build_output_root.join("chunks")?,
294 build_output_root.join("assets")?,
295 node_build_environment().to_resolved().await?,
296 RuntimeType::Development,
297 )
298 .shared_runtime_chunk(true)
301 .build();
302
303 let node_backend = child_process_backend();
304 let execution_context = ExecutionContext::new(
305 root_path.clone(),
306 Vc::upcast(build_chunking_context),
307 env,
308 node_backend,
309 );
310
311 let server_fs = Vc::upcast::<Box<dyn FileSystem>>(ServerFileSystem::new());
312 let server_root = server_fs.root().owned().await?;
313 let entry_requests = entry_requests
314 .iter()
315 .map(|r| match r {
316 EntryRequest::Relative(p) => Request::relative(
317 p.clone().into(),
318 Default::default(),
319 Default::default(),
320 false,
321 ),
322 EntryRequest::Module(m, p) => Request::module(
323 m.clone().into(),
324 p.clone().into(),
325 Default::default(),
326 Default::default(),
327 ),
328 })
329 .collect();
330
331 let web_source: ResolvedVc<Box<dyn ContentSource>> = create_web_entry_source(
332 root_path.clone(),
333 execution_context,
334 entry_requests,
335 server_root,
336 rcstr!("/ROOT"),
337 env,
338 eager_compile,
339 NodeEnv::Development.cell(),
340 Default::default(),
341 browserslist_query,
342 )
343 .to_resolved()
344 .await?;
345 let static_source = ResolvedVc::upcast(
346 StaticAssetsContentSource::new(Default::default(), project_path.join("public")?)
347 .to_resolved()
348 .await?,
349 );
350 let main_source = CombinedContentSource::new(vec![static_source, web_source])
351 .to_resolved()
352 .await?;
353 let introspect = ResolvedVc::upcast(
354 IntrospectionSource {
355 roots: FxHashSet::from_iter([ResolvedVc::upcast(main_source)]),
356 }
357 .resolved_cell(),
358 );
359 let main_source = ResolvedVc::upcast(main_source);
360 Ok(Vc::upcast(PrefixedRouterContentSource::new(
361 Default::default(),
362 vec![(rcstr!("__turbopack__"), introspect)],
363 *main_source,
364 )))
365}
366
367pub async fn start_server(args: &DevArguments) -> Result<()> {
369 let start = Instant::now();
370
371 #[cfg(feature = "tokio_console")]
372 console_subscriber::init();
373
374 let NormalizedDirs {
375 project_dir,
376 root_dir,
377 } = normalize_dirs(&args.common.dir, &args.common.root)?;
378
379 let is_ci = std::env::var("CI").is_ok_and(|v| !v.is_empty());
380 let is_short_session = is_ci;
381
382 let tt = if args.common.persistent_caching {
383 let version_info = GitVersionInfo {
384 describe: env!("VERGEN_GIT_DESCRIBE"),
385 dirty: option_env!("CI").is_none_or(|v| v.is_empty())
386 && env!("VERGEN_GIT_DIRTY") == "true",
387 };
388 let cache_dir = args
389 .common
390 .cache_dir
391 .clone()
392 .unwrap_or_else(|| PathBuf::from(&*project_dir).join(".turbopack/cache"));
393 let (backing_storage, cache_state) =
394 turbo_backing_storage(&cache_dir, &version_info, is_ci, is_short_session, false)?;
395 let storage_mode = if std::env::var("TURBO_ENGINE_READ_ONLY").is_ok() {
396 StorageMode::ReadOnly
397 } else if is_ci || is_short_session {
398 StorageMode::ReadWriteOnShutdown
399 } else {
400 StorageMode::ReadWrite
401 };
402 let tt = TurboTasks::new(TurboTasksBackend::new(
403 BackendOptions {
404 storage_mode: Some(storage_mode),
405 ..Default::default()
406 },
407 backing_storage,
408 ));
409 if let StartupCacheState::Invalidated { reason_code } = cache_state {
410 eprintln!(
411 "{} - Turbopack cache was invalidated{}",
412 "warn ".yellow(),
413 reason_code
414 .as_deref()
415 .map(|r| format!(": {r}"))
416 .unwrap_or_default()
417 );
418 }
419 tt
420 } else {
421 TurboTasks::new(TurboTasksBackend::new(
422 BackendOptions {
423 storage_mode: None,
424 ..Default::default()
425 },
426 noop_backing_storage(),
427 ))
428 };
429
430 let tt_clone = tt.clone();
431
432 let mut server = TurbopackDevServerBuilder::new(tt, project_dir, root_dir)
433 .eager_compile(args.eager_compile)
434 .hostname(args.hostname)
435 .port(args.port)
436 .log_detail(args.common.log_detail)
437 .show_all(args.common.show_all)
438 .log_level(
439 args.common
440 .log_level
441 .map_or_else(|| IssueSeverity::Warning, |l| l.0),
442 );
443
444 for entry in normalize_entries(&args.common.entries) {
445 server = server.entry_request(EntryRequest::Relative(entry))
446 }
447
448 #[cfg(feature = "serializable")]
449 {
450 server = server.allow_retry(args.allow_retry);
451 }
452
453 let server = server.build().await?;
454
455 {
456 let addr = &server.addr;
457 let hostname = if addr.ip().is_loopback() || addr.ip().is_unspecified() {
458 "localhost".to_string()
459 } else if addr.is_ipv6() {
460 format!("[{}]", addr.ip())
463 } else {
464 addr.ip().to_string()
465 };
466 let index_uri = match addr.port() {
467 443 => format!("https://{hostname}"),
468 80 => format!("http://{hostname}"),
469 port => format!("http://{hostname}:{port}"),
470 };
471 println!(
472 "{} - started server on {}, url: {}",
473 "ready".green(),
474 server.addr,
475 index_uri
476 );
477 if !args.no_open {
478 let _ = webbrowser::open(&index_uri);
479 }
480 }
481
482 let stats_future = async move {
483 if args.common.log_detail {
484 println!(
485 "{event_type} - initial compilation {start} ({memory})",
486 event_type = "event".purple(),
487 start = FormatDuration(start.elapsed()),
488 memory = FormatBytes(TurboMalloc::memory_usage())
489 );
490 }
491
492 let mut progress_counter = 0;
493 loop {
494 let update_future = profile_timeout(
495 tt_clone.as_ref(),
496 tt_clone.aggregated_update_info(Duration::from_millis(100), Duration::MAX),
497 );
498
499 if let Some(UpdateInfo {
500 duration,
501 tasks,
502 reasons,
503 ..
504 }) = update_future.await
505 {
506 progress_counter = 0;
507 match (args.common.log_detail, !reasons.is_empty()) {
508 (true, true) => {
509 println!(
510 "\x1b[2K{event_type} - {reasons} {duration} ({tasks} tasks, {memory})",
511 event_type = "event".purple(),
512 duration = FormatDuration(duration),
513 tasks = tasks,
514 memory = FormatBytes(TurboMalloc::memory_usage())
515 );
516 }
517 (true, false) => {
518 println!(
519 "\x1b[2K{event_type} - compilation {duration} ({tasks} tasks, \
520 {memory})",
521 event_type = "event".purple(),
522 duration = FormatDuration(duration),
523 tasks = tasks,
524 memory = FormatBytes(TurboMalloc::memory_usage())
525 );
526 }
527 (false, true) => {
528 println!(
529 "\x1b[2K{event_type} - {reasons} {duration}",
530 event_type = "event".purple(),
531 duration = FormatDuration(duration),
532 );
533 }
534 (false, false) => {
535 if duration > Duration::from_secs(1) {
536 println!(
537 "\x1b[2K{event_type} - compilation {duration}",
538 event_type = "event".purple(),
539 duration = FormatDuration(duration),
540 );
541 }
542 }
543 }
544 } else {
545 progress_counter += 1;
546 if args.common.log_detail {
547 print!(
548 "\x1b[2K{event_type} - updating for {progress_counter}s... ({memory})\r",
549 event_type = "event".purple(),
550 memory = FormatBytes(TurboMalloc::memory_usage())
551 );
552 } else {
553 print!(
554 "\x1b[2K{event_type} - updating for {progress_counter}s...\r",
555 event_type = "event".purple(),
556 );
557 }
558 let _ = stdout().lock().flush();
559 }
560 }
561 };
562
563 join!(stats_future, async { server.future.await.unwrap() }).await;
564
565 Ok(())
566}
567
568#[cfg(feature = "profile")]
569async fn profile_timeout<T>(
572 tt: &TurboTasks<TurboTasksBackend>,
573 future: impl Future<Output = T>,
574) -> T {
575 const PROFILE_EXIT_TIMEOUT: Duration = Duration::from_secs(5);
578
579 futures::pin_mut!(future);
580 loop {
581 match tokio::time::timeout(PROFILE_EXIT_TIMEOUT, &mut future).await {
582 Ok(res) => return res,
583 Err(_) => {
584 if tt.get_in_progress_count() == 0 {
585 std::process::exit(0)
586 }
587 }
588 }
589 }
590}
591
592#[cfg(not(feature = "profile"))]
593fn profile_timeout<T>(
594 _tt: &TurboTasks<TurboTasksBackend>,
595 future: impl Future<Output = T>,
596) -> impl Future<Output = T> {
597 future
598}
599
600pub trait IssueReporterProvider: Send + Sync + 'static {
601 fn get_issue_reporter(&self) -> Vc<Box<dyn IssueReporter>>;
602}
603
604impl<T> IssueReporterProvider for T
605where
606 T: Fn() -> Vc<Box<dyn IssueReporter>> + Send + Sync + Clone + 'static,
607{
608 fn get_issue_reporter(&self) -> Vc<Box<dyn IssueReporter>> {
609 self()
610 }
611}