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, NoopBackingStorage, TurboTasksBackend, noop_backing_storage,
22};
23use turbo_tasks_fs::FileSystem;
24use turbo_tasks_malloc::TurboMalloc;
25use turbopack::evaluate_context::node_build_environment;
26use turbopack_cli_utils::issue::{ConsoleUi, LogOptions};
27use turbopack_core::{
28 issue::{IssueReporter, IssueSeverity},
29 resolve::parse::Request,
30 server_fs::ServerFileSystem,
31};
32use turbopack_dev_server::{
33 DevServer, DevServerBuilder, SourceProvider,
34 introspect::IntrospectionSource,
35 source::{
36 ContentSource, combined::CombinedContentSource, router::PrefixedRouterContentSource,
37 static_assets::StaticAssetsContentSource,
38 },
39};
40use turbopack_ecmascript_runtime::RuntimeType;
41use turbopack_env::dotenv::load_env;
42use turbopack_node::execution_context::ExecutionContext;
43use turbopack_nodejs::NodeJsChunkingContext;
44
45use self::web_entry_source::create_web_entry_source;
46use crate::{
47 arguments::DevArguments,
48 contexts::NodeEnv,
49 util::{
50 EntryRequest, NormalizedDirs, normalize_dirs, normalize_entries, output_fs, project_fs,
51 },
52};
53
54pub(crate) mod web_entry_source;
55
56type Backend = TurboTasksBackend<NoopBackingStorage>;
57
58pub struct TurbopackDevServerBuilder {
59 turbo_tasks: Arc<TurboTasks<Backend>>,
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<Backend>>,
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)]
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 let fs: Vc<Box<dyn FileSystem>> = project_fs(root_dir, true);
269 let root_path = fs.root().await?.clone_value();
270 let project_path = root_path.join(&project_relative)?;
271
272 let env = load_env(root_path.clone());
273 let build_output_root = output_fs.root().await?.join(".turbopack/build")?;
274
275 let build_output_root_to_root_path = project_path
276 .join(".turbopack/build")?
277 .get_relative_path_to(&root_path)
278 .context("Project path is in root path")?;
279 let build_output_root_to_root_path = build_output_root_to_root_path;
280
281 let build_chunking_context = NodeJsChunkingContext::builder(
282 root_path.clone(),
283 build_output_root.clone(),
284 build_output_root_to_root_path,
285 build_output_root.clone(),
286 build_output_root.join("chunks")?,
287 build_output_root.join("assets")?,
288 node_build_environment().to_resolved().await?,
289 RuntimeType::Development,
290 )
291 .build();
292
293 let execution_context =
294 ExecutionContext::new(root_path.clone(), Vc::upcast(build_chunking_context), env);
295
296 let server_fs = Vc::upcast::<Box<dyn FileSystem>>(ServerFileSystem::new());
297 let server_root = server_fs.root().await?.clone_value();
298 let entry_requests = entry_requests
299 .iter()
300 .map(|r| match r {
301 EntryRequest::Relative(p) => Request::relative(
302 p.clone().into(),
303 Default::default(),
304 Default::default(),
305 false,
306 ),
307 EntryRequest::Module(m, p) => Request::module(
308 m.clone(),
309 p.clone().into(),
310 Default::default(),
311 Default::default(),
312 ),
313 })
314 .collect();
315
316 let web_source: ResolvedVc<Box<dyn ContentSource>> = create_web_entry_source(
317 root_path.clone(),
318 execution_context,
319 entry_requests,
320 server_root,
321 rcstr!("/ROOT"),
322 env,
323 eager_compile,
324 NodeEnv::Development.cell(),
325 Default::default(),
326 browserslist_query,
327 )
328 .to_resolved()
329 .await?;
330 let static_source = ResolvedVc::upcast(
331 StaticAssetsContentSource::new(Default::default(), project_path.join("public")?)
332 .to_resolved()
333 .await?,
334 );
335 let main_source = CombinedContentSource::new(vec![static_source, web_source])
336 .to_resolved()
337 .await?;
338 let introspect = ResolvedVc::upcast(
339 IntrospectionSource {
340 roots: FxHashSet::from_iter([ResolvedVc::upcast(main_source)]),
341 }
342 .resolved_cell(),
343 );
344 let main_source = ResolvedVc::upcast(main_source);
345 Ok(Vc::upcast(PrefixedRouterContentSource::new(
346 Default::default(),
347 vec![(rcstr!("__turbopack__"), introspect)],
348 *main_source,
349 )))
350}
351
352pub async fn start_server(args: &DevArguments) -> Result<()> {
354 let start = Instant::now();
355
356 #[cfg(feature = "tokio_console")]
357 console_subscriber::init();
358 crate::register();
359
360 let NormalizedDirs {
361 project_dir,
362 root_dir,
363 } = normalize_dirs(&args.common.dir, &args.common.root)?;
364
365 let tt = TurboTasks::new(TurboTasksBackend::new(
366 BackendOptions {
367 storage_mode: None,
368 ..Default::default()
369 },
370 noop_backing_storage(),
371 ));
372
373 let tt_clone = tt.clone();
374
375 let mut server = TurbopackDevServerBuilder::new(tt, project_dir, root_dir)
376 .eager_compile(args.eager_compile)
377 .hostname(args.hostname)
378 .port(args.port)
379 .log_detail(args.common.log_detail)
380 .show_all(args.common.show_all)
381 .log_level(
382 args.common
383 .log_level
384 .map_or_else(|| IssueSeverity::Warning, |l| l.0),
385 );
386
387 for entry in normalize_entries(&args.common.entries) {
388 server = server.entry_request(EntryRequest::Relative(entry))
389 }
390
391 #[cfg(feature = "serializable")]
392 {
393 server = server.allow_retry(args.allow_retry);
394 }
395
396 let server = server.build().await?;
397
398 {
399 let addr = &server.addr;
400 let hostname = if addr.ip().is_loopback() || addr.ip().is_unspecified() {
401 "localhost".to_string()
402 } else if addr.is_ipv6() {
403 format!("[{}]", addr.ip())
406 } else {
407 addr.ip().to_string()
408 };
409 let index_uri = match addr.port() {
410 443 => format!("https://{hostname}"),
411 80 => format!("http://{hostname}"),
412 port => format!("http://{hostname}:{port}"),
413 };
414 println!(
415 "{} - started server on {}, url: {}",
416 "ready".green(),
417 server.addr,
418 index_uri
419 );
420 if !args.no_open {
421 let _ = webbrowser::open(&index_uri);
422 }
423 }
424
425 let stats_future = async move {
426 if args.common.log_detail {
427 println!(
428 "{event_type} - initial compilation {start} ({memory})",
429 event_type = "event".purple(),
430 start = FormatDuration(start.elapsed()),
431 memory = FormatBytes(TurboMalloc::memory_usage())
432 );
433 }
434
435 let mut progress_counter = 0;
436 loop {
437 let update_future = profile_timeout(
438 tt_clone.as_ref(),
439 tt_clone.aggregated_update_info(Duration::from_millis(100), Duration::MAX),
440 );
441
442 if let Some(UpdateInfo {
443 duration,
444 tasks,
445 reasons,
446 ..
447 }) = update_future.await
448 {
449 progress_counter = 0;
450 match (args.common.log_detail, !reasons.is_empty()) {
451 (true, true) => {
452 println!(
453 "\x1b[2K{event_type} - {reasons} {duration} ({tasks} tasks, {memory})",
454 event_type = "event".purple(),
455 duration = FormatDuration(duration),
456 tasks = tasks,
457 memory = FormatBytes(TurboMalloc::memory_usage())
458 );
459 }
460 (true, false) => {
461 println!(
462 "\x1b[2K{event_type} - compilation {duration} ({tasks} tasks, \
463 {memory})",
464 event_type = "event".purple(),
465 duration = FormatDuration(duration),
466 tasks = tasks,
467 memory = FormatBytes(TurboMalloc::memory_usage())
468 );
469 }
470 (false, true) => {
471 println!(
472 "\x1b[2K{event_type} - {reasons} {duration}",
473 event_type = "event".purple(),
474 duration = FormatDuration(duration),
475 );
476 }
477 (false, false) => {
478 if duration > Duration::from_secs(1) {
479 println!(
480 "\x1b[2K{event_type} - compilation {duration}",
481 event_type = "event".purple(),
482 duration = FormatDuration(duration),
483 );
484 }
485 }
486 }
487 } else {
488 progress_counter += 1;
489 if args.common.log_detail {
490 print!(
491 "\x1b[2K{event_type} - updating for {progress_counter}s... ({memory})\r",
492 event_type = "event".purple(),
493 memory = FormatBytes(TurboMalloc::memory_usage())
494 );
495 } else {
496 print!(
497 "\x1b[2K{event_type} - updating for {progress_counter}s...\r",
498 event_type = "event".purple(),
499 );
500 }
501 let _ = stdout().lock().flush();
502 }
503 }
504 };
505
506 join!(stats_future, async { server.future.await.unwrap() }).await;
507
508 Ok(())
509}
510
511#[cfg(feature = "profile")]
512async fn profile_timeout<T>(tt: &TurboTasks<Backend>, future: impl Future<Output = T>) -> T {
515 const PROFILE_EXIT_TIMEOUT: Duration = Duration::from_secs(5);
518
519 futures::pin_mut!(future);
520 loop {
521 match tokio::time::timeout(PROFILE_EXIT_TIMEOUT, &mut future).await {
522 Ok(res) => return res,
523 Err(_) => {
524 if tt.get_in_progress_count() == 0 {
525 std::process::exit(0)
526 }
527 }
528 }
529 }
530}
531
532#[cfg(not(feature = "profile"))]
533fn profile_timeout<T>(
534 _tt: &TurboTasks<Backend>,
535 future: impl Future<Output = T>,
536) -> impl Future<Output = T> {
537 future
538}
539
540pub trait IssueReporterProvider: Send + Sync + 'static {
541 fn get_issue_reporter(&self) -> Vc<Box<dyn IssueReporter>>;
542}
543
544impl<T> IssueReporterProvider for T
545where
546 T: Fn() -> Vc<Box<dyn IssueReporter>> + Send + Sync + Clone + 'static,
547{
548 fn get_issue_reporter(&self) -> Vc<Box<dyn IssueReporter>> {
549 self()
550 }
551}