Skip to main content

turbopack_cli/build/
mod.rs

1use std::{
2    env::current_dir,
3    mem::forget,
4    path::{MAIN_SEPARATOR, PathBuf},
5    sync::Arc,
6};
7
8use anyhow::{Context, Result, bail};
9use either::Either;
10use rustc_hash::FxHashSet;
11use tracing::Instrument;
12use turbo_rcstr::RcStr;
13use turbo_tasks::{
14    Effects, OperationVc, ResolvedVc, TransientInstance, TryJoinIterExt, TurboTasks, Vc,
15    take_effects,
16};
17use turbo_tasks_backend::{
18    BackendOptions, GitVersionInfo, NoopBackingStorage, StartupCacheState, StorageMode,
19    TurboBackingStorage, TurboTasksBackend, noop_backing_storage, turbo_backing_storage,
20};
21use turbo_tasks_fs::FileSystem;
22use turbo_unix_path::join_path;
23use turbopack::global_module_ids::get_global_module_id_strategy;
24use turbopack_browser::{BrowserChunkingContext, CurrentChunkMethod};
25use turbopack_cli_utils::issue::{ConsoleUi, LogOptions};
26use turbopack_core::{
27    asset::Asset,
28    chunk::{
29        ChunkingConfig, ChunkingContext, ChunkingContextExt, ContentHashing, EvaluatableAsset,
30        MangleType, MinifyType, SourceMapsType, availability_info::AvailabilityInfo,
31    },
32    environment::{BrowserEnvironment, Environment, ExecutionEnvironment, NodeJsEnvironment},
33    ident::AssetIdent,
34    issue::{IssueReporter, IssueSeverity, handle_issues},
35    module::Module,
36    module_graph::{
37        ModuleGraph, SingleModuleGraph,
38        binding_usage_info::compute_binding_usage_info,
39        chunk_group_info::{ChunkGroup, ChunkGroupEntry},
40    },
41    output::{OutputAsset, OutputAssets, OutputAssetsWithReferenced},
42    reference_type::{EntryReferenceSubType, ReferenceType},
43    resolve::{
44        origin::{PlainResolveOrigin, ResolveOrigin, ResolveOriginExt},
45        parse::Request,
46    },
47};
48use turbopack_css::chunk::CssChunkType;
49use turbopack_ecmascript::chunk::EcmascriptChunkType;
50use turbopack_ecmascript_runtime::RuntimeType;
51use turbopack_env::dotenv::load_env;
52use turbopack_node::{child_process_backend, execution_context::ExecutionContext};
53use turbopack_nodejs::NodeJsChunkingContext;
54
55use crate::{
56    arguments::{BuildArguments, Target},
57    contexts::{NodeEnv, get_client_asset_context, get_client_compile_time_info},
58    util::{
59        EntryRequest, NormalizedDirs, normalize_dirs, normalize_entries, output_fs, project_fs,
60    },
61};
62
63type Backend = TurboTasksBackend<Either<TurboBackingStorage, NoopBackingStorage>>;
64
65pub struct TurbopackBuildBuilder {
66    turbo_tasks: Arc<TurboTasks<Backend>>,
67    project_dir: RcStr,
68    root_dir: RcStr,
69    entry_requests: Vec<EntryRequest>,
70    browserslist_query: RcStr,
71    log_level: IssueSeverity,
72    show_all: bool,
73    log_detail: bool,
74    source_maps_type: SourceMapsType,
75    minify_type: MinifyType,
76    target: Target,
77    scope_hoist: bool,
78}
79
80impl TurbopackBuildBuilder {
81    pub fn new(turbo_tasks: Arc<TurboTasks<Backend>>, project_dir: RcStr, root_dir: RcStr) -> Self {
82        TurbopackBuildBuilder {
83            turbo_tasks,
84            project_dir,
85            root_dir,
86            entry_requests: vec![],
87            browserslist_query: "last 1 Chrome versions, last 1 Firefox versions, last 1 Safari \
88                                 versions, last 1 Edge versions"
89                .into(),
90            log_level: IssueSeverity::Warning,
91            show_all: false,
92            log_detail: false,
93            source_maps_type: SourceMapsType::Full,
94            minify_type: MinifyType::Minify {
95                mangle: Some(MangleType::OptimalSize),
96            },
97            target: Target::Node,
98            scope_hoist: true,
99        }
100    }
101
102    pub fn entry_request(mut self, entry_asset_path: EntryRequest) -> Self {
103        self.entry_requests.push(entry_asset_path);
104        self
105    }
106
107    pub fn browserslist_query(mut self, browserslist_query: RcStr) -> Self {
108        self.browserslist_query = browserslist_query;
109        self
110    }
111
112    pub fn log_level(mut self, log_level: IssueSeverity) -> Self {
113        self.log_level = log_level;
114        self
115    }
116
117    pub fn show_all(mut self, show_all: bool) -> Self {
118        self.show_all = show_all;
119        self
120    }
121
122    pub fn log_detail(mut self, log_detail: bool) -> Self {
123        self.log_detail = log_detail;
124        self
125    }
126
127    pub fn source_maps_type(mut self, source_maps_type: SourceMapsType) -> Self {
128        self.source_maps_type = source_maps_type;
129        self
130    }
131
132    pub fn minify_type(mut self, minify_type: MinifyType) -> Self {
133        self.minify_type = minify_type;
134        self
135    }
136
137    pub fn scope_hoist(mut self, scope_hoist: bool) -> Self {
138        self.scope_hoist = scope_hoist;
139        self
140    }
141
142    pub fn target(mut self, target: Target) -> Self {
143        self.target = target;
144        self
145    }
146
147    pub async fn build(self) -> Result<()> {
148        self.turbo_tasks
149            .run_once(async move {
150                let wrapper_op = extract_effects_operation(build_internal(
151                    self.project_dir.clone(),
152                    self.root_dir,
153                    self.entry_requests.clone(),
154                    self.browserslist_query,
155                    self.source_maps_type,
156                    self.minify_type,
157                    self.target,
158                    self.scope_hoist,
159                ));
160
161                // Await the result to propagate any errors and capture effects.
162                let effects = wrapper_op.read_strongly_consistent().await?;
163
164                effects.apply().await?;
165
166                let issue_reporter: Vc<Box<dyn IssueReporter>> =
167                    Vc::upcast(ConsoleUi::new(TransientInstance::new(LogOptions {
168                        project_dir: PathBuf::from(self.project_dir),
169                        current_dir: current_dir().unwrap(),
170                        show_all: self.show_all,
171                        log_detail: self.log_detail,
172                        log_level: self.log_level,
173                    })));
174
175                handle_issues(wrapper_op, issue_reporter, IssueSeverity::Error, None, None).await?;
176
177                Ok(())
178            })
179            .await
180    }
181}
182
183#[turbo_tasks::function(operation)]
184async fn extract_effects_operation(op: OperationVc<()>) -> Result<Vc<Effects>> {
185    let _ = op.resolve().strongly_consistent().await?;
186    Ok(take_effects(op).await?.cell())
187}
188
189#[turbo_tasks::function(operation)]
190async fn build_internal(
191    project_dir: RcStr,
192    root_dir: RcStr,
193    entry_requests: Vec<EntryRequest>,
194    browserslist_query: RcStr,
195    source_maps_type: SourceMapsType,
196    minify_type: MinifyType,
197    target: Target,
198    scope_hoist: bool,
199) -> Result<()> {
200    let output_fs = output_fs(project_dir.clone());
201    const OUTPUT_DIR: &str = "dist";
202    let project_relative = project_dir.strip_prefix(&*root_dir).unwrap();
203    let project_relative: RcStr = project_relative
204        .strip_prefix(MAIN_SEPARATOR)
205        .unwrap_or(project_relative)
206        .replace(MAIN_SEPARATOR, "/")
207        .into();
208    let project_fs = project_fs(
209        root_dir.clone(),
210        /* watch= */ false,
211        join_path(project_relative.as_str(), OUTPUT_DIR)
212            .unwrap()
213            .into(),
214    );
215    let root_path = project_fs.root().owned().await?;
216    let project_path = root_path.join(&project_relative)?;
217    let build_output_root = output_fs.root().await?.join(OUTPUT_DIR)?;
218
219    let node_env = NodeEnv::Production.cell();
220
221    let build_output_root_to_root_path = project_path
222        .join(OUTPUT_DIR)?
223        .get_relative_path_to(&root_path)
224        .context("Project path is in root path")?;
225
226    let runtime_type = match *node_env.await? {
227        NodeEnv::Development => RuntimeType::Development,
228        NodeEnv::Production => RuntimeType::Production,
229    };
230
231    let compile_time_info =
232        get_client_compile_time_info(browserslist_query.clone(), node_env, false);
233    let node_backend = child_process_backend();
234    let execution_context = ExecutionContext::new(
235        root_path.clone(),
236        Vc::upcast(
237            NodeJsChunkingContext::builder(
238                project_path.clone(),
239                build_output_root.clone(),
240                build_output_root_to_root_path.clone(),
241                build_output_root.clone(),
242                build_output_root.clone(),
243                build_output_root.clone(),
244                Environment::new(ExecutionEnvironment::NodeJsLambda(
245                    NodeJsEnvironment::default().resolved_cell(),
246                ))
247                .to_resolved()
248                .await?,
249                runtime_type,
250            )
251            .build(),
252        ),
253        load_env(root_path.clone()),
254        node_backend,
255    );
256
257    let asset_context = get_client_asset_context(
258        project_path.clone(),
259        execution_context,
260        compile_time_info,
261        node_env,
262        source_maps_type,
263    );
264
265    let entry_requests = (*entry_requests
266        .into_iter()
267        .map(|r| async move {
268            Ok(match r {
269                EntryRequest::Relative(p) => Request::relative(
270                    p.clone().into(),
271                    Default::default(),
272                    Default::default(),
273                    false,
274                ),
275                EntryRequest::Module(m, p) => Request::module(
276                    m.clone().into(),
277                    p.clone().into(),
278                    Default::default(),
279                    Default::default(),
280                ),
281            })
282        })
283        .try_join()
284        .await?)
285        .to_vec();
286
287    let origin = PlainResolveOrigin::new(asset_context, project_fs.root().await?.join("_")?);
288    let project_dir = &project_dir;
289    let entries = async move {
290        entry_requests
291            .into_iter()
292            .map(|request_vc| async move {
293                let ty = ReferenceType::Entry(EntryReferenceSubType::Undefined);
294                let request = request_vc.await?;
295                origin
296                    .resolve_asset(request_vc, origin.resolve_options(), ty)
297                    .await?
298                    .first_module()
299                    .await?
300                    .with_context(|| {
301                        format!(
302                            "Unable to resolve entry {} from directory {}.",
303                            request.request().unwrap(),
304                            project_dir
305                        )
306                    })
307            })
308            .try_join()
309            .await
310    }
311    .instrument(tracing::info_span!("resolve entries"))
312    .await?;
313
314    let single_graph = SingleModuleGraph::new_with_entries(
315        ResolvedVc::cell(vec![ChunkGroupEntry::Entry(entries.clone())]),
316        false,
317        true,
318    );
319    let mut module_graph = ModuleGraph::from_graphs(vec![single_graph], None);
320    let binding_usage = compute_binding_usage_info(module_graph, true);
321    let unused_references = binding_usage
322        .connect()
323        .unused_references()
324        .to_resolved()
325        .await?;
326    module_graph = ModuleGraph::from_graphs(vec![single_graph], Some(binding_usage));
327    let module_graph = module_graph.connect();
328    let module_id_strategy = get_global_module_id_strategy(module_graph)
329        .to_resolved()
330        .await?;
331
332    let chunking_context: Vc<Box<dyn ChunkingContext>> = match target {
333        Target::Browser => {
334            let mut builder = BrowserChunkingContext::builder(
335                project_path,
336                build_output_root.clone(),
337                build_output_root_to_root_path,
338                build_output_root.clone(),
339                build_output_root.clone(),
340                build_output_root.clone(),
341                Environment::new(ExecutionEnvironment::Browser(
342                    BrowserEnvironment {
343                        dom: true,
344                        web_worker: false,
345                        service_worker: false,
346                        browserslist_query: browserslist_query.clone(),
347                    }
348                    .resolved_cell(),
349                ))
350                .to_resolved()
351                .await?,
352                runtime_type,
353            )
354            .source_maps(source_maps_type)
355            .module_id_strategy(module_id_strategy)
356            .export_usage(Some(binding_usage.connect().to_resolved().await?))
357            .unused_references(unused_references)
358            .current_chunk_method(CurrentChunkMethod::DocumentCurrentScript)
359            .minify_type(minify_type);
360
361            match *node_env.await? {
362                NodeEnv::Development => {}
363                NodeEnv::Production => {
364                    builder = builder
365                        .chunking_config(
366                            Vc::<EcmascriptChunkType>::default().to_resolved().await?,
367                            ChunkingConfig {
368                                min_chunk_size: 50_000,
369                                max_chunk_count_per_group: 40,
370                                max_merge_chunk_size: 200_000,
371                                ..Default::default()
372                            },
373                        )
374                        .chunking_config(
375                            Vc::<CssChunkType>::default().to_resolved().await?,
376                            ChunkingConfig {
377                                max_merge_chunk_size: 100_000,
378                                ..Default::default()
379                            },
380                        )
381                        .chunk_content_hashing(ContentHashing::Direct { length: 13 })
382                        .asset_content_hashing(ContentHashing::Direct { length: 13 })
383                        .nested_async_availability(true)
384                        .module_merging(scope_hoist);
385                }
386            }
387
388            Vc::upcast(builder.build())
389        }
390        Target::Node => {
391            let mut builder = NodeJsChunkingContext::builder(
392                project_path,
393                build_output_root.clone(),
394                build_output_root_to_root_path,
395                build_output_root.clone(),
396                build_output_root.clone(),
397                build_output_root.clone(),
398                Environment::new(ExecutionEnvironment::NodeJsLambda(
399                    NodeJsEnvironment::default().resolved_cell(),
400                ))
401                .to_resolved()
402                .await?,
403                runtime_type,
404            )
405            .source_maps(source_maps_type)
406            .module_id_strategy(module_id_strategy)
407            .export_usage(Some(binding_usage.connect().to_resolved().await?))
408            .unused_references(unused_references)
409            .minify_type(minify_type);
410
411            match *node_env.await? {
412                NodeEnv::Development => {}
413                NodeEnv::Production => {
414                    builder = builder
415                        .chunking_config(
416                            Vc::<EcmascriptChunkType>::default().to_resolved().await?,
417                            ChunkingConfig {
418                                min_chunk_size: 20_000,
419                                max_chunk_count_per_group: 100,
420                                max_merge_chunk_size: 100_000,
421                                ..Default::default()
422                            },
423                        )
424                        .chunking_config(
425                            Vc::<CssChunkType>::default().to_resolved().await?,
426                            ChunkingConfig {
427                                max_merge_chunk_size: 100_000,
428                                ..Default::default()
429                            },
430                        )
431                        .module_merging(scope_hoist);
432                }
433            }
434
435            Vc::upcast(builder.build())
436        }
437    };
438
439    let entry_chunk_groups = entries
440        .into_iter()
441        .map(|entry_module| {
442            let build_output_root = build_output_root.clone();
443
444            async move {
445                Ok(
446                    if let Some(ecmascript) =
447                        ResolvedVc::try_sidecast::<Box<dyn EvaluatableAsset>>(entry_module)
448                    {
449                        match target {
450                            Target::Browser => chunking_context.evaluated_chunk_group_assets(
451                                AssetIdent::from_path(
452                                    build_output_root
453                                        .join(
454                                            ecmascript.ident().path().await?.file_stem().unwrap(),
455                                        )?
456                                        .with_extension("entry.js"),
457                                ),
458                                ChunkGroup::Entry(
459                                    [ResolvedVc::upcast(ecmascript)].into_iter().collect(),
460                                ),
461                                module_graph,
462                                AvailabilityInfo::root(),
463                            ),
464                            Target::Node => OutputAssetsWithReferenced {
465                                assets: ResolvedVc::cell(vec![
466                                    chunking_context
467                                        .entry_chunk_group(
468                                            build_output_root
469                                                .join(
470                                                    ecmascript
471                                                        .ident()
472                                                        .path()
473                                                        .await?
474                                                        .file_stem()
475                                                        .unwrap(),
476                                                )?
477                                                .with_extension("entry.js"),
478                                            ChunkGroup::Entry(vec![ResolvedVc::upcast(ecmascript)]),
479                                            module_graph,
480                                            OutputAssets::empty(),
481                                            OutputAssets::empty(),
482                                            AvailabilityInfo::root(),
483                                        )
484                                        .await?
485                                        .asset,
486                                ]),
487                                referenced_assets: ResolvedVc::cell(vec![]),
488                                references: ResolvedVc::cell(vec![]),
489                            }
490                            .cell(),
491                        }
492                    } else {
493                        bail!(
494                            "Entry module is not chunkable, so it can't be used to bootstrap the \
495                             application"
496                        )
497                    },
498                )
499            }
500        })
501        .try_join()
502        .await?;
503
504    let all_assets = async move {
505        let mut all_assets: FxHashSet<ResolvedVc<Box<dyn OutputAsset>>> = FxHashSet::default();
506        for group in entry_chunk_groups {
507            all_assets.extend(group.expand_all_assets().await?);
508        }
509        anyhow::Ok(all_assets)
510    }
511    .instrument(tracing::info_span!("list chunks"))
512    .await?;
513
514    all_assets
515        .iter()
516        .map(|c| async move { c.content().write(c.path().owned().await?).await })
517        .try_join()
518        .await?;
519
520    Ok(())
521}
522
523pub async fn build(args: &BuildArguments) -> Result<()> {
524    let NormalizedDirs {
525        project_dir,
526        root_dir,
527    } = normalize_dirs(&args.common.dir, &args.common.root)?;
528
529    let is_ci = std::env::var("CI").is_ok_and(|v| !v.is_empty());
530    let is_short_session = true; // build sessions are always short
531
532    let tt = if args.common.persistent_caching {
533        let version_info = GitVersionInfo {
534            describe: env!("VERGEN_GIT_DESCRIBE"),
535            dirty: option_env!("CI").is_none_or(|v| v.is_empty())
536                && env!("VERGEN_GIT_DIRTY") == "true",
537        };
538        let cache_dir = args
539            .common
540            .cache_dir
541            .clone()
542            .unwrap_or_else(|| PathBuf::from(&*project_dir).join(".turbopack/cache"));
543        let (backing_storage, cache_state) =
544            turbo_backing_storage(&cache_dir, &version_info, is_ci, is_short_session, false)?;
545        let storage_mode = if std::env::var("TURBO_ENGINE_READ_ONLY").is_ok() {
546            StorageMode::ReadOnly
547        } else if is_ci || is_short_session {
548            StorageMode::ReadWriteOnShutdown
549        } else {
550            StorageMode::ReadWrite
551        };
552        let tt = TurboTasks::new(TurboTasksBackend::new(
553            BackendOptions {
554                dependency_tracking: false,
555                storage_mode: Some(storage_mode),
556                ..Default::default()
557            },
558            Either::Left(backing_storage),
559        ));
560        if let StartupCacheState::Invalidated { reason_code } = cache_state {
561            eprintln!(
562                "warn  - Turbopack cache was invalidated{}",
563                reason_code
564                    .as_deref()
565                    .map(|r| format!(": {r}"))
566                    .unwrap_or_default()
567            );
568        }
569        tt
570    } else {
571        TurboTasks::new(TurboTasksBackend::new(
572            BackendOptions {
573                dependency_tracking: false,
574                storage_mode: None,
575                ..Default::default()
576            },
577            Either::Right(noop_backing_storage()),
578        ))
579    };
580
581    let mut builder = TurbopackBuildBuilder::new(tt.clone(), project_dir, root_dir)
582        .log_detail(args.common.log_detail)
583        .log_level(
584            args.common
585                .log_level
586                .map_or_else(|| IssueSeverity::Warning, |l| l.0),
587        )
588        .source_maps_type(if args.no_sourcemap {
589            SourceMapsType::None
590        } else {
591            SourceMapsType::Full
592        })
593        .minify_type(if args.no_minify {
594            MinifyType::NoMinify
595        } else {
596            MinifyType::Minify {
597                mangle: Some(MangleType::OptimalSize),
598            }
599        })
600        .scope_hoist(!args.no_scope_hoist)
601        .target(args.common.target.unwrap_or(Target::Node))
602        .show_all(args.common.show_all);
603
604    for entry in normalize_entries(&args.common.entries) {
605        builder = builder.entry_request(EntryRequest::Relative(entry));
606    }
607
608    builder.build().await?;
609
610    // Intentionally leak this `Arc`. Otherwise we'll waste time during process exit performing a
611    // ton of drop calls.
612    if !args.force_memory_cleanup {
613        forget(tt);
614    }
615
616    Ok(())
617}