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