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