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;
15use turbo_tasks::{
16 NonLocalValue, OperationVc, ResolvedVc, TransientInstance, TurboTasks, UpdateInfo, Value, 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 if self.allow_retry && attempts < max_attempts {
164 let should_retry = e
168 .source()
169 .and_then(|e| {
170 e.downcast_ref::<std::io::Error>()
171 .map(|e| e.kind() == std::io::ErrorKind::AddrInUse)
172 })
173 .unwrap_or(false);
174
175 if should_retry {
176 println!(
177 "{} - Port {} is in use, trying {} instead",
178 "warn ".yellow(),
179 current_port,
180 current_port + 1
181 );
182 attempts += 1;
183 continue;
184 }
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, TraceRawVcs, 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)]
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 let fs: Vc<Box<dyn FileSystem>> = project_fs(root_dir);
268 let root_path = fs.root().to_resolved().await?;
269 let project_path = root_path.join(project_relative).to_resolved().await?;
270
271 let env = load_env(*root_path);
272 let build_output_root = output_fs
273 .root()
274 .join(".turbopack/build".into())
275 .to_resolved()
276 .await?;
277
278 let build_output_root_to_root_path = project_path
279 .join(".turbopack/build".into())
280 .await?
281 .get_relative_path_to(&*root_path.await?)
282 .context("Project path is in root path")?;
283 let build_output_root_to_root_path = ResolvedVc::cell(build_output_root_to_root_path);
284
285 let build_chunking_context = NodeJsChunkingContext::builder(
286 root_path,
287 build_output_root,
288 build_output_root_to_root_path,
289 build_output_root,
290 build_output_root
291 .join("chunks".into())
292 .to_resolved()
293 .await?,
294 build_output_root
295 .join("assets".into())
296 .to_resolved()
297 .await?,
298 node_build_environment().to_resolved().await?,
299 RuntimeType::Development,
300 )
301 .build();
302
303 let execution_context =
304 ExecutionContext::new(*root_path, Vc::upcast(build_chunking_context), env);
305
306 let server_fs = Vc::upcast::<Box<dyn FileSystem>>(ServerFileSystem::new());
307 let server_root = server_fs.root();
308 let entry_requests = entry_requests
309 .iter()
310 .map(|r| match r {
311 EntryRequest::Relative(p) => Request::relative(
312 Value::new(p.clone().into()),
313 Default::default(),
314 Default::default(),
315 false,
316 ),
317 EntryRequest::Module(m, p) => Request::module(
318 m.clone(),
319 Value::new(p.clone().into()),
320 Default::default(),
321 Default::default(),
322 ),
323 })
324 .collect();
325
326 let web_source: ResolvedVc<Box<dyn ContentSource>> = create_web_entry_source(
327 *root_path,
328 execution_context,
329 entry_requests,
330 server_root,
331 Vc::cell("/ROOT".into()),
332 env,
333 eager_compile,
334 NodeEnv::Development.cell(),
335 Default::default(),
336 browserslist_query,
337 )
338 .to_resolved()
339 .await?;
340 let static_source = ResolvedVc::upcast(
341 StaticAssetsContentSource::new(Default::default(), project_path.join("public".into()))
342 .to_resolved()
343 .await?,
344 );
345 let main_source = CombinedContentSource::new(vec![static_source, web_source])
346 .to_resolved()
347 .await?;
348 let introspect = ResolvedVc::upcast(
349 IntrospectionSource {
350 roots: FxHashSet::from_iter([ResolvedVc::upcast(main_source)]),
351 }
352 .resolved_cell(),
353 );
354 let main_source = ResolvedVc::upcast(main_source);
355 Ok(Vc::upcast(PrefixedRouterContentSource::new(
356 Default::default(),
357 vec![("__turbopack__".into(), introspect)],
358 *main_source,
359 )))
360}
361
362pub fn register() {
363 turbopack::register();
364 include!(concat!(env!("OUT_DIR"), "/register.rs"));
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 register();
374
375 let NormalizedDirs {
376 project_dir,
377 root_dir,
378 } = normalize_dirs(&args.common.dir, &args.common.root)?;
379
380 let tt = TurboTasks::new(TurboTasksBackend::new(
381 BackendOptions {
382 storage_mode: None,
383 ..Default::default()
384 },
385 noop_backing_storage(),
386 ));
387
388 let tt_clone = tt.clone();
389
390 let mut server = TurbopackDevServerBuilder::new(tt, project_dir, root_dir)
391 .eager_compile(args.eager_compile)
392 .hostname(args.hostname)
393 .port(args.port)
394 .log_detail(args.common.log_detail)
395 .show_all(args.common.show_all)
396 .log_level(
397 args.common
398 .log_level
399 .map_or_else(|| IssueSeverity::Warning, |l| l.0),
400 );
401
402 for entry in normalize_entries(&args.common.entries) {
403 server = server.entry_request(EntryRequest::Relative(entry))
404 }
405
406 #[cfg(feature = "serializable")]
407 {
408 server = server.allow_retry(args.allow_retry);
409 }
410
411 let server = server.build().await?;
412
413 {
414 let addr = &server.addr;
415 let hostname = if addr.ip().is_loopback() || addr.ip().is_unspecified() {
416 "localhost".to_string()
417 } else if addr.is_ipv6() {
418 format!("[{}]", addr.ip())
421 } else {
422 addr.ip().to_string()
423 };
424 let index_uri = match addr.port() {
425 443 => format!("https://{hostname}"),
426 80 => format!("http://{hostname}"),
427 port => format!("http://{hostname}:{port}"),
428 };
429 println!(
430 "{} - started server on {}, url: {}",
431 "ready".green(),
432 server.addr,
433 index_uri
434 );
435 if !args.no_open {
436 let _ = webbrowser::open(&index_uri);
437 }
438 }
439
440 let stats_future = async move {
441 if args.common.log_detail {
442 println!(
443 "{event_type} - initial compilation {start} ({memory})",
444 event_type = "event".purple(),
445 start = FormatDuration(start.elapsed()),
446 memory = FormatBytes(TurboMalloc::memory_usage())
447 );
448 }
449
450 let mut progress_counter = 0;
451 loop {
452 let update_future = profile_timeout(
453 tt_clone.as_ref(),
454 tt_clone.aggregated_update_info(Duration::from_millis(100), Duration::MAX),
455 );
456
457 if let Some(UpdateInfo {
458 duration,
459 tasks,
460 reasons,
461 ..
462 }) = update_future.await
463 {
464 progress_counter = 0;
465 match (args.common.log_detail, !reasons.is_empty()) {
466 (true, true) => {
467 println!(
468 "\x1b[2K{event_type} - {reasons} {duration} ({tasks} tasks, {memory})",
469 event_type = "event".purple(),
470 duration = FormatDuration(duration),
471 tasks = tasks,
472 memory = FormatBytes(TurboMalloc::memory_usage())
473 );
474 }
475 (true, false) => {
476 println!(
477 "\x1b[2K{event_type} - compilation {duration} ({tasks} tasks, \
478 {memory})",
479 event_type = "event".purple(),
480 duration = FormatDuration(duration),
481 tasks = tasks,
482 memory = FormatBytes(TurboMalloc::memory_usage())
483 );
484 }
485 (false, true) => {
486 println!(
487 "\x1b[2K{event_type} - {reasons} {duration}",
488 event_type = "event".purple(),
489 duration = FormatDuration(duration),
490 );
491 }
492 (false, false) => {
493 if duration > Duration::from_secs(1) {
494 println!(
495 "\x1b[2K{event_type} - compilation {duration}",
496 event_type = "event".purple(),
497 duration = FormatDuration(duration),
498 );
499 }
500 }
501 }
502 } else {
503 progress_counter += 1;
504 if args.common.log_detail {
505 print!(
506 "\x1b[2K{event_type} - updating for {progress_counter}s... ({memory})\r",
507 event_type = "event".purple(),
508 memory = FormatBytes(TurboMalloc::memory_usage())
509 );
510 } else {
511 print!(
512 "\x1b[2K{event_type} - updating for {progress_counter}s...\r",
513 event_type = "event".purple(),
514 );
515 }
516 let _ = stdout().lock().flush();
517 }
518 }
519 };
520
521 join!(stats_future, async { server.future.await.unwrap() }).await;
522
523 Ok(())
524}
525
526#[cfg(feature = "profile")]
527async fn profile_timeout<T>(tt: &TurboTasks<Backend>, future: impl Future<Output = T>) -> T {
530 const PROFILE_EXIT_TIMEOUT: Duration = Duration::from_secs(5);
533
534 futures::pin_mut!(future);
535 loop {
536 match tokio::time::timeout(PROFILE_EXIT_TIMEOUT, &mut future).await {
537 Ok(res) => return res,
538 Err(_) => {
539 if tt.get_in_progress_count() == 0 {
540 std::process::exit(0)
541 }
542 }
543 }
544 }
545}
546
547#[cfg(not(feature = "profile"))]
548fn profile_timeout<T>(
549 _tt: &TurboTasks<Backend>,
550 future: impl Future<Output = T>,
551) -> impl Future<Output = T> {
552 future
553}
554
555pub trait IssueReporterProvider: Send + Sync + 'static {
556 fn get_issue_reporter(&self) -> Vc<Box<dyn IssueReporter>>;
557}
558
559impl<T> IssueReporterProvider for T
560where
561 T: Fn() -> Vc<Box<dyn IssueReporter>> + Send + Sync + Clone + 'static,
562{
563 fn get_issue_reporter(&self) -> Vc<Box<dyn IssueReporter>> {
564 self()
565 }
566}