Skip to main content

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