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 either::Either;
13use owo_colors::OwoColorize;
14use rustc_hash::FxHashSet;
15use turbo_rcstr::{RcStr, rcstr};
16use turbo_tasks::{
17 NonLocalValue, OperationVc, ResolvedVc, TransientInstance, TurboTasks, UpdateInfo, Vc,
18 trace::TraceRawVcs,
19 util::{FormatBytes, FormatDuration},
20};
21use turbo_tasks_backend::{
22 BackendOptions, GitVersionInfo, NoopBackingStorage, StartupCacheState, StorageMode,
23 TurboBackingStorage, TurboTasksBackend, noop_backing_storage, turbo_backing_storage,
24};
25use turbo_tasks_fs::FileSystem;
26use turbo_tasks_malloc::TurboMalloc;
27use turbo_unix_path::join_path;
28use turbopack::evaluate_context::node_build_environment;
29use turbopack_cli_utils::issue::{ConsoleUi, LogOptions};
30use turbopack_core::{
31 issue::{IssueReporter, IssueSeverity},
32 resolve::parse::Request,
33 server_fs::ServerFileSystem,
34};
35use turbopack_dev_server::{
36 DevServer, DevServerBuilder, SourceProvider,
37 introspect::IntrospectionSource,
38 source::{
39 ContentSource, combined::CombinedContentSource, router::PrefixedRouterContentSource,
40 static_assets::StaticAssetsContentSource,
41 },
42};
43use turbopack_ecmascript_runtime::RuntimeType;
44use turbopack_env::dotenv::load_env;
45use turbopack_node::{child_process_backend, execution_context::ExecutionContext};
46use turbopack_nodejs::NodeJsChunkingContext;
47
48use self::web_entry_source::create_web_entry_source;
49use crate::{
50 arguments::DevArguments,
51 contexts::NodeEnv,
52 util::{
53 EntryRequest, NormalizedDirs, normalize_dirs, normalize_entries, output_fs, project_fs,
54 },
55};
56
57pub(crate) mod web_entry_source;
58
59type Backend = TurboTasksBackend<Either<TurboBackingStorage, NoopBackingStorage>>;
60
61pub struct TurbopackDevServerBuilder {
62 turbo_tasks: Arc<TurboTasks<Backend>>,
63 project_dir: RcStr,
64 root_dir: RcStr,
65 entry_requests: Vec<EntryRequest>,
66 eager_compile: bool,
67 hostname: Option<IpAddr>,
68 issue_reporter: Option<Box<dyn IssueReporterProvider>>,
69 port: Option<u16>,
70 browserslist_query: RcStr,
71 log_level: IssueSeverity,
72 show_all: bool,
73 log_detail: bool,
74 allow_retry: bool,
75}
76
77impl TurbopackDevServerBuilder {
78 pub fn new(
79 turbo_tasks: Arc<TurboTasks<Backend>>,
80 project_dir: RcStr,
81 root_dir: RcStr,
82 ) -> TurbopackDevServerBuilder {
83 TurbopackDevServerBuilder {
84 turbo_tasks,
85 project_dir,
86 root_dir,
87 entry_requests: vec![],
88 eager_compile: false,
89 hostname: None,
90 issue_reporter: None,
91 port: None,
92 browserslist_query: "last 1 Chrome versions, last 1 Firefox versions, last 1 Safari \
93 versions, last 1 Edge versions"
94 .into(),
95 log_level: IssueSeverity::Warning,
96 show_all: false,
97 log_detail: false,
98 allow_retry: false,
99 }
100 }
101
102 pub fn entry_request(mut self, entry_asset_path: EntryRequest) -> TurbopackDevServerBuilder {
103 self.entry_requests.push(entry_asset_path);
104 self
105 }
106
107 pub fn eager_compile(mut self, eager_compile: bool) -> TurbopackDevServerBuilder {
108 self.eager_compile = eager_compile;
109 self
110 }
111
112 pub fn hostname(mut self, hostname: IpAddr) -> TurbopackDevServerBuilder {
113 self.hostname = Some(hostname);
114 self
115 }
116
117 pub fn port(mut self, port: u16) -> TurbopackDevServerBuilder {
118 self.port = Some(port);
119 self
120 }
121
122 pub fn browserslist_query(mut self, browserslist_query: RcStr) -> TurbopackDevServerBuilder {
123 self.browserslist_query = browserslist_query;
124 self
125 }
126
127 pub fn log_level(mut self, log_level: IssueSeverity) -> TurbopackDevServerBuilder {
128 self.log_level = log_level;
129 self
130 }
131
132 pub fn show_all(mut self, show_all: bool) -> TurbopackDevServerBuilder {
133 self.show_all = show_all;
134 self
135 }
136
137 pub fn allow_retry(mut self, allow_retry: bool) -> TurbopackDevServerBuilder {
138 self.allow_retry = allow_retry;
139 self
140 }
141
142 pub fn log_detail(mut self, log_detail: bool) -> TurbopackDevServerBuilder {
143 self.log_detail = log_detail;
144 self
145 }
146
147 pub fn issue_reporter(
148 mut self,
149 issue_reporter: Box<dyn IssueReporterProvider>,
150 ) -> TurbopackDevServerBuilder {
151 self.issue_reporter = Some(issue_reporter);
152 self
153 }
154
155 fn find_port(&self, host: IpAddr, port: u16, max_attempts: u16) -> Result<DevServerBuilder> {
157 let max_attempts = max_attempts - 1;
159 let mut attempts = 0;
160 loop {
161 let current_port = port + attempts;
162 let addr = SocketAddr::new(host, current_port);
163 let listen_result = DevServer::listen(addr);
164
165 if let Err(e) = &listen_result
166 && self.allow_retry
167 && attempts < max_attempts
168 {
169 let should_retry = e
173 .source()
174 .and_then(|e| {
175 e.downcast_ref::<std::io::Error>()
176 .map(|e| e.kind() == std::io::ErrorKind::AddrInUse)
177 })
178 .unwrap_or(false);
179
180 if should_retry {
181 println!(
182 "{} - Port {} is in use, trying {} instead",
183 "warn ".yellow(),
184 current_port,
185 current_port + 1
186 );
187 attempts += 1;
188 continue;
189 }
190 }
191
192 return listen_result;
193 }
194 }
195
196 pub async fn build(self) -> Result<DevServer> {
197 let port = self.port.context("port must be set")?;
198 let host = self.hostname.context("hostname must be set")?;
199
200 let server = self.find_port(host, port, 10)?;
201
202 let turbo_tasks = self.turbo_tasks;
203 let project_dir: RcStr = self.project_dir;
204 let root_dir: RcStr = self.root_dir;
205 let eager_compile = self.eager_compile;
206 let show_all = self.show_all;
207 let log_detail: bool = self.log_detail;
208 let browserslist_query: RcStr = self.browserslist_query;
209 let log_args = TransientInstance::new(LogOptions {
210 current_dir: current_dir().unwrap(),
211 project_dir: PathBuf::from(project_dir.clone()),
212 show_all,
213 log_detail,
214 log_level: self.log_level,
215 });
216 let entry_requests = Arc::new(self.entry_requests);
217 let tasks = turbo_tasks.clone();
218 let issue_provider = self.issue_reporter.unwrap_or_else(|| {
219 Box::new(move || Vc::upcast(ConsoleUi::new(log_args.clone())))
221 });
222
223 #[derive(Clone, TraceRawVcs, NonLocalValue)]
224 struct ServerSourceProvider {
225 root_dir: RcStr,
226 project_dir: RcStr,
227 entry_requests: Arc<Vec<EntryRequest>>,
228 eager_compile: bool,
229 browserslist_query: RcStr,
230 }
231 impl SourceProvider for ServerSourceProvider {
232 fn get_source(&self) -> OperationVc<Box<dyn ContentSource>> {
233 source(
234 self.root_dir.clone(),
235 self.project_dir.clone(),
236 self.entry_requests.clone(),
237 self.eager_compile,
238 self.browserslist_query.clone(),
239 )
240 }
241 }
242 let source = ServerSourceProvider {
243 root_dir,
244 project_dir,
245 entry_requests,
246 eager_compile,
247 browserslist_query,
248 };
249
250 let issue_reporter_arc = Arc::new(move || issue_provider.get_issue_reporter());
251 Ok(server.serve(tasks, source, issue_reporter_arc))
252 }
253}
254
255#[turbo_tasks::function(operation)]
256async fn source(
257 root_dir: RcStr,
258 project_dir: RcStr,
259 entry_requests: Arc<Vec<EntryRequest>>,
260 eager_compile: bool,
261 browserslist_query: RcStr,
262) -> Result<Vc<Box<dyn ContentSource>>> {
263 let project_relative = project_dir.strip_prefix(&*root_dir).unwrap();
264 let project_relative: RcStr = project_relative
265 .strip_prefix(MAIN_SEPARATOR)
266 .unwrap_or(project_relative)
267 .replace(MAIN_SEPARATOR, "/")
268 .into();
269
270 let output_fs = output_fs(project_dir);
271 const OUTPUT_DIR: &str = ".turbopack/build";
272 let fs: Vc<Box<dyn FileSystem>> = project_fs(
273 root_dir,
274 true,
275 join_path(project_relative.as_str(), OUTPUT_DIR)
276 .unwrap()
277 .into(),
278 );
279 let root_path = fs.root().owned().await?;
280 let project_path = root_path.join(&project_relative)?;
281
282 let env = load_env(root_path.clone());
283 let build_output_root = output_fs.root().await?.join(OUTPUT_DIR)?;
284
285 let build_output_root_to_root_path = project_path
286 .join(OUTPUT_DIR)?
287 .get_relative_path_to(&root_path)
288 .context("Project path is in root path")?;
289 let build_output_root_to_root_path = build_output_root_to_root_path;
290
291 let build_chunking_context = NodeJsChunkingContext::builder(
292 root_path.clone(),
293 build_output_root.clone(),
294 build_output_root_to_root_path,
295 build_output_root.clone(),
296 build_output_root.join("chunks")?,
297 build_output_root.join("assets")?,
298 node_build_environment().to_resolved().await?,
299 RuntimeType::Development,
300 )
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)?;
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 Either::Left(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 Either::Right(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>(tt: &TurboTasks<Backend>, future: impl Future<Output = T>) -> 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<Backend>,
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}