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 .build();
299
300 let node_backend = child_process_backend();
301 let execution_context = ExecutionContext::new(
302 root_path.clone(),
303 Vc::upcast(build_chunking_context),
304 env,
305 node_backend,
306 );
307
308 let server_fs = Vc::upcast::<Box<dyn FileSystem>>(ServerFileSystem::new());
309 let server_root = server_fs.root().owned().await?;
310 let entry_requests = entry_requests
311 .iter()
312 .map(|r| match r {
313 EntryRequest::Relative(p) => Request::relative(
314 p.clone().into(),
315 Default::default(),
316 Default::default(),
317 false,
318 ),
319 EntryRequest::Module(m, p) => Request::module(
320 m.clone().into(),
321 p.clone().into(),
322 Default::default(),
323 Default::default(),
324 ),
325 })
326 .collect();
327
328 let web_source: ResolvedVc<Box<dyn ContentSource>> = create_web_entry_source(
329 root_path.clone(),
330 execution_context,
331 entry_requests,
332 server_root,
333 rcstr!("/ROOT"),
334 env,
335 eager_compile,
336 NodeEnv::Development.cell(),
337 Default::default(),
338 browserslist_query,
339 )
340 .to_resolved()
341 .await?;
342 let static_source = ResolvedVc::upcast(
343 StaticAssetsContentSource::new(Default::default(), project_path.join("public")?)
344 .to_resolved()
345 .await?,
346 );
347 let main_source = CombinedContentSource::new(vec![static_source, web_source])
348 .to_resolved()
349 .await?;
350 let introspect = ResolvedVc::upcast(
351 IntrospectionSource {
352 roots: FxHashSet::from_iter([ResolvedVc::upcast(main_source)]),
353 }
354 .resolved_cell(),
355 );
356 let main_source = ResolvedVc::upcast(main_source);
357 Ok(Vc::upcast(PrefixedRouterContentSource::new(
358 Default::default(),
359 vec![(rcstr!("__turbopack__"), introspect)],
360 *main_source,
361 )))
362}
363
364pub async fn start_server(args: &DevArguments) -> Result<()> {
366 let start = Instant::now();
367
368 #[cfg(feature = "tokio_console")]
369 console_subscriber::init();
370
371 let NormalizedDirs {
372 project_dir,
373 root_dir,
374 } = normalize_dirs(&args.common.dir, &args.common.root)?;
375
376 let is_ci = std::env::var("CI").is_ok_and(|v| !v.is_empty());
377 let is_short_session = is_ci;
378
379 let tt = if args.common.persistent_caching {
380 let version_info = GitVersionInfo {
381 describe: env!("VERGEN_GIT_DESCRIBE"),
382 dirty: option_env!("CI").is_none_or(|v| v.is_empty())
383 && env!("VERGEN_GIT_DIRTY") == "true",
384 };
385 let cache_dir = args
386 .common
387 .cache_dir
388 .clone()
389 .unwrap_or_else(|| PathBuf::from(&*project_dir).join(".turbopack/cache"));
390 let (backing_storage, cache_state) =
391 turbo_backing_storage(&cache_dir, &version_info, is_ci, is_short_session, false)?;
392 let storage_mode = if std::env::var("TURBO_ENGINE_READ_ONLY").is_ok() {
393 StorageMode::ReadOnly
394 } else if is_ci || is_short_session {
395 StorageMode::ReadWriteOnShutdown
396 } else {
397 StorageMode::ReadWrite
398 };
399 let tt = TurboTasks::new(TurboTasksBackend::new(
400 BackendOptions {
401 storage_mode: Some(storage_mode),
402 ..Default::default()
403 },
404 backing_storage,
405 ));
406 if let StartupCacheState::Invalidated { reason_code } = cache_state {
407 eprintln!(
408 "{} - Turbopack cache was invalidated{}",
409 "warn ".yellow(),
410 reason_code
411 .as_deref()
412 .map(|r| format!(": {r}"))
413 .unwrap_or_default()
414 );
415 }
416 tt
417 } else {
418 TurboTasks::new(TurboTasksBackend::new(
419 BackendOptions {
420 storage_mode: None,
421 ..Default::default()
422 },
423 noop_backing_storage(),
424 ))
425 };
426
427 let tt_clone = tt.clone();
428
429 let mut server = TurbopackDevServerBuilder::new(tt, project_dir, root_dir)
430 .eager_compile(args.eager_compile)
431 .hostname(args.hostname)
432 .port(args.port)
433 .log_detail(args.common.log_detail)
434 .show_all(args.common.show_all)
435 .log_level(
436 args.common
437 .log_level
438 .map_or_else(|| IssueSeverity::Warning, |l| l.0),
439 );
440
441 for entry in normalize_entries(&args.common.entries) {
442 server = server.entry_request(EntryRequest::Relative(entry))
443 }
444
445 #[cfg(feature = "serializable")]
446 {
447 server = server.allow_retry(args.allow_retry);
448 }
449
450 let server = server.build().await?;
451
452 {
453 let addr = &server.addr;
454 let hostname = if addr.ip().is_loopback() || addr.ip().is_unspecified() {
455 "localhost".to_string()
456 } else if addr.is_ipv6() {
457 format!("[{}]", addr.ip())
460 } else {
461 addr.ip().to_string()
462 };
463 let index_uri = match addr.port() {
464 443 => format!("https://{hostname}"),
465 80 => format!("http://{hostname}"),
466 port => format!("http://{hostname}:{port}"),
467 };
468 println!(
469 "{} - started server on {}, url: {}",
470 "ready".green(),
471 server.addr,
472 index_uri
473 );
474 if !args.no_open {
475 let _ = webbrowser::open(&index_uri);
476 }
477 }
478
479 let stats_future = async move {
480 if args.common.log_detail {
481 println!(
482 "{event_type} - initial compilation {start} ({memory})",
483 event_type = "event".purple(),
484 start = FormatDuration(start.elapsed()),
485 memory = FormatBytes(TurboMalloc::memory_usage())
486 );
487 }
488
489 let mut progress_counter = 0;
490 loop {
491 let update_future = profile_timeout(
492 tt_clone.as_ref(),
493 tt_clone.aggregated_update_info(Duration::from_millis(100), Duration::MAX),
494 );
495
496 if let Some(UpdateInfo {
497 duration,
498 tasks,
499 reasons,
500 ..
501 }) = update_future.await
502 {
503 progress_counter = 0;
504 match (args.common.log_detail, !reasons.is_empty()) {
505 (true, true) => {
506 println!(
507 "\x1b[2K{event_type} - {reasons} {duration} ({tasks} tasks, {memory})",
508 event_type = "event".purple(),
509 duration = FormatDuration(duration),
510 tasks = tasks,
511 memory = FormatBytes(TurboMalloc::memory_usage())
512 );
513 }
514 (true, false) => {
515 println!(
516 "\x1b[2K{event_type} - compilation {duration} ({tasks} tasks, \
517 {memory})",
518 event_type = "event".purple(),
519 duration = FormatDuration(duration),
520 tasks = tasks,
521 memory = FormatBytes(TurboMalloc::memory_usage())
522 );
523 }
524 (false, true) => {
525 println!(
526 "\x1b[2K{event_type} - {reasons} {duration}",
527 event_type = "event".purple(),
528 duration = FormatDuration(duration),
529 );
530 }
531 (false, false) => {
532 if duration > Duration::from_secs(1) {
533 println!(
534 "\x1b[2K{event_type} - compilation {duration}",
535 event_type = "event".purple(),
536 duration = FormatDuration(duration),
537 );
538 }
539 }
540 }
541 } else {
542 progress_counter += 1;
543 if args.common.log_detail {
544 print!(
545 "\x1b[2K{event_type} - updating for {progress_counter}s... ({memory})\r",
546 event_type = "event".purple(),
547 memory = FormatBytes(TurboMalloc::memory_usage())
548 );
549 } else {
550 print!(
551 "\x1b[2K{event_type} - updating for {progress_counter}s...\r",
552 event_type = "event".purple(),
553 );
554 }
555 let _ = stdout().lock().flush();
556 }
557 }
558 };
559
560 join!(stats_future, async { server.future.await.unwrap() }).await;
561
562 Ok(())
563}
564
565#[cfg(feature = "profile")]
566async fn profile_timeout<T>(
569 tt: &TurboTasks<TurboTasksBackend>,
570 future: impl Future<Output = T>,
571) -> T {
572 const PROFILE_EXIT_TIMEOUT: Duration = Duration::from_secs(5);
575
576 futures::pin_mut!(future);
577 loop {
578 match tokio::time::timeout(PROFILE_EXIT_TIMEOUT, &mut future).await {
579 Ok(res) => return res,
580 Err(_) => {
581 if tt.get_in_progress_count() == 0 {
582 std::process::exit(0)
583 }
584 }
585 }
586 }
587}
588
589#[cfg(not(feature = "profile"))]
590fn profile_timeout<T>(
591 _tt: &TurboTasks<TurboTasksBackend>,
592 future: impl Future<Output = T>,
593) -> impl Future<Output = T> {
594 future
595}
596
597pub trait IssueReporterProvider: Send + Sync + 'static {
598 fn get_issue_reporter(&self) -> Vc<Box<dyn IssueReporter>>;
599}
600
601impl<T> IssueReporterProvider for T
602where
603 T: Fn() -> Vc<Box<dyn IssueReporter>> + Send + Sync + Clone + 'static,
604{
605 fn get_issue_reporter(&self) -> Vc<Box<dyn IssueReporter>> {
606 self()
607 }
608}