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