turbopack_cli/dev/
mod.rs

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    /// Attempts to find an open port to bind.
153    fn find_port(&self, host: IpAddr, port: u16, max_attempts: u16) -> Result<DevServerBuilder> {
154        // max_attempts of 1 means we loop 0 times.
155        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                    // Returned error from `listen` is not `std::io::Error` but `anyhow::Error`,
165                    // so we need to access its source to check if it is
166                    // `std::io::ErrorKind::AddrInUse`.
167                    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            // Initialize a ConsoleUi reporter if no custom reporter was provided
216            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
367/// Start a devserver with the given args.
368pub 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            // When using an IPv6 address, we need to surround the IP in brackets to
419            // distinguish it from the port's `:`.
420            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")]
527// When profiling, exits the process when no new updates have been received for
528// a given timeout and there are no more tasks in progress.
529async fn profile_timeout<T>(tt: &TurboTasks<Backend>, future: impl Future<Output = T>) -> T {
530    /// How long to wait in between updates before force-exiting the process
531    /// during profiling.
532    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}