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 rustc_hash::FxHashSet;
10use tracing::Instrument;
11use turbo_rcstr::RcStr;
12use turbo_tasks::{
13    Effects, OperationVc, ResolvedVc, TransientInstance, TryJoinIterExt, TurboTasks, Vc,
14    read_strongly_consistent_and_apply_effects, take_effects,
15};
16use turbo_tasks_backend::{
17    BackendOptions, GitVersionInfo, StartupCacheState, StorageMode, TurboTasksBackend,
18    noop_backing_storage, turbo_backing_storage,
19};
20use turbo_tasks_fs::FileSystem;
21use turbo_unix_path::join_path;
22use turbopack::global_module_ids::get_global_module_id_strategy;
23use turbopack_browser::{BrowserChunkingContext, CurrentChunkMethod};
24use turbopack_cli_utils::issue::{ConsoleUi, LogOptions};
25use turbopack_core::{
26    asset::Asset,
27    chunk::{
28        ChunkingConfig, ChunkingContext, ChunkingContextExt, ContentHashing, EvaluatableAsset,
29        MangleType, MinifyType, SourceMapsType, availability_info::AvailabilityInfo,
30    },
31    context::AssetContext,
32    environment::{BrowserEnvironment, Environment, ExecutionEnvironment, NodeJsEnvironment},
33    ident::AssetIdent,
34    issue::{IssueReporter, IssueSeverity, handle_issues},
35    module::Module,
36    module_graph::{
37        GraphEntries, ModuleGraph, SingleModuleGraph,
38        binding_usage_info::compute_binding_usage_info,
39        chunk_group_info::{ChunkGroup, ChunkGroupEntry, EntryHeuristics},
40    },
41    output::{OutputAsset, OutputAssets, OutputAssetsWithReferenced},
42    reference_type::{EntryReferenceSubType, ReferenceType},
43    resolve::{
44        origin::{PlainResolveOrigin, ResolveOrigin},
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;
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                read_strongly_consistent_and_apply_effects(wrapper_op, |e| e).await?;
162
163                let issue_reporter: Vc<Box<dyn IssueReporter>> =
164                    Vc::upcast(ConsoleUi::new(TransientInstance::new(LogOptions {
165                        project_dir: PathBuf::from(self.project_dir),
166                        current_dir: current_dir().unwrap(),
167                        show_all: self.show_all,
168                        log_detail: self.log_detail,
169                        log_level: self.log_level,
170                    })));
171
172                handle_issues(wrapper_op, issue_reporter, IssueSeverity::Error, None, None).await?;
173
174                Ok(())
175            })
176            .await
177    }
178}
179
180#[turbo_tasks::function(operation, root)]
181async fn extract_effects_operation(op: OperationVc<()>) -> Result<Vc<Effects>> {
182    let _ = op.resolve().strongly_consistent().await?;
183    Ok(take_effects(op).await?.cell())
184}
185
186#[turbo_tasks::function(operation, root)]
187async fn build_internal(
188    project_dir: RcStr,
189    root_dir: RcStr,
190    entry_requests: Vec<EntryRequest>,
191    browserslist_query: RcStr,
192    source_maps_type: SourceMapsType,
193    minify_type: MinifyType,
194    target: Target,
195    scope_hoist: bool,
196) -> Result<()> {
197    let output_fs = output_fs(project_dir.clone());
198    const OUTPUT_DIR: &str = "dist";
199    let project_relative = project_dir.strip_prefix(&*root_dir).unwrap();
200    let project_relative: RcStr = project_relative
201        .strip_prefix(MAIN_SEPARATOR)
202        .unwrap_or(project_relative)
203        .replace(MAIN_SEPARATOR, "/")
204        .into();
205    let project_fs = project_fs(
206        root_dir.clone(),
207        /* watch= */ false,
208        join_path(project_relative.as_str(), OUTPUT_DIR)
209            .unwrap()
210            .into(),
211    );
212    let root_path = project_fs.root().owned().await?;
213    let project_path = root_path.join(&project_relative)?;
214    let build_output_root = output_fs.root().await?.join(OUTPUT_DIR)?;
215
216    let node_env = NodeEnv::Production.cell();
217
218    let build_output_root_to_root_path = project_path
219        .join(OUTPUT_DIR)?
220        .get_relative_path_to(&root_path)
221        .context("Project path is in root path")?;
222
223    let runtime_type = match *node_env.await? {
224        NodeEnv::Development => RuntimeType::Development,
225        NodeEnv::Production => RuntimeType::Production,
226    };
227
228    let compile_time_info =
229        get_client_compile_time_info(browserslist_query.clone(), node_env, false);
230    let node_backend = child_process_backend();
231    let execution_context = ExecutionContext::new(
232        root_path.clone(),
233        Vc::upcast(
234            NodeJsChunkingContext::builder(
235                project_path.clone(),
236                build_output_root.clone(),
237                build_output_root_to_root_path.clone(),
238                build_output_root.clone(),
239                build_output_root.clone(),
240                build_output_root.clone(),
241                Environment::new(ExecutionEnvironment::NodeJsLambda(
242                    NodeJsEnvironment::default().resolved_cell(),
243                ))
244                .to_resolved()
245                .await?,
246                runtime_type,
247            )
248            // Shared by every build-time JS evaluation, each with its own module graph but all
249            // emitting the same runtime chunk.
250            .shared_runtime_chunk(true)
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 =
288        PlainResolveOrigin::new(asset_context, project_fs.root().await?.join("_")?).await?;
289    let resolve_options = origin.resolve_options();
290    let asset_context = origin.asset_context();
291    let origin_path = origin.origin_path();
292    let project_dir = &project_dir;
293    let entries = async move {
294        entry_requests
295            .into_iter()
296            .map(|request_vc| {
297                let origin_path = origin_path.clone();
298                async move {
299                    let ty = ReferenceType::Entry(EntryReferenceSubType::Undefined);
300                    let request = request_vc.await?;
301                    asset_context
302                        .resolve_asset(origin_path, request_vc, resolve_options, ty)
303                        .await?
304                        .first_module()
305                        .await?
306                        .with_context(|| {
307                            format!(
308                                "Unable to resolve entry {} from directory {}.",
309                                request.request().unwrap(),
310                                project_dir
311                            )
312                        })
313                }
314            })
315            .try_join()
316            .await
317    }
318    .instrument(tracing::info_span!("resolve entries"))
319    .await?;
320
321    let single_graph = SingleModuleGraph::new_with_entries(
322        GraphEntries::from_chunk_groups(vec![ChunkGroupEntry::Entry {
323            modules: entries.clone(),
324            heuristics: EntryHeuristics::default(),
325        }])
326        .resolved_cell(),
327        false,
328        true,
329    );
330    let mut module_graph = ModuleGraph::from_graphs(vec![single_graph], None);
331    let binding_usage = compute_binding_usage_info(module_graph, true);
332    let unused_references = binding_usage
333        .connect()
334        .unused_references()
335        .to_resolved()
336        .await?;
337    module_graph = ModuleGraph::from_graphs(vec![single_graph], Some(binding_usage));
338    let module_graph = module_graph.connect();
339    let module_id_strategy = get_global_module_id_strategy(module_graph)
340        .to_resolved()
341        .await?;
342
343    let chunking_context: Vc<Box<dyn ChunkingContext>> = match target {
344        Target::Browser => {
345            let mut builder = BrowserChunkingContext::builder(
346                project_path,
347                build_output_root.clone(),
348                build_output_root_to_root_path,
349                build_output_root.clone(),
350                build_output_root.clone(),
351                build_output_root.clone(),
352                Environment::new(ExecutionEnvironment::Browser(
353                    BrowserEnvironment {
354                        dom: true,
355                        web_worker: false,
356                        service_worker: false,
357                        browserslist_query: browserslist_query.clone(),
358                    }
359                    .resolved_cell(),
360                ))
361                .to_resolved()
362                .await?,
363                runtime_type,
364            )
365            .source_maps(source_maps_type)
366            .module_id_strategy(module_id_strategy)
367            .export_usage(Some(binding_usage.connect().to_resolved().await?))
368            .unused_references(unused_references)
369            .current_chunk_method(CurrentChunkMethod::DocumentCurrentScript)
370            .minify_type(minify_type);
371
372            match *node_env.await? {
373                NodeEnv::Development => {}
374                NodeEnv::Production => {
375                    builder = builder
376                        .chunking_config(
377                            Vc::<EcmascriptChunkType>::default().to_resolved().await?,
378                            ChunkingConfig {
379                                min_chunk_size: 50_000,
380                                max_chunk_count_per_group: 40,
381                                max_merge_chunk_size: 200_000,
382                                ..Default::default()
383                            },
384                        )
385                        .chunking_config(
386                            Vc::<CssChunkType>::default().to_resolved().await?,
387                            ChunkingConfig {
388                                max_merge_chunk_size: 100_000,
389                                ..Default::default()
390                            },
391                        )
392                        .chunk_content_hashing(ContentHashing::Direct { length: 13 })
393                        .asset_content_hashing(ContentHashing::Direct { length: 13 })
394                        .nested_async_availability(true)
395                        .module_merging(scope_hoist);
396                }
397            }
398
399            Vc::upcast(builder.build())
400        }
401        Target::Node => {
402            let mut builder = NodeJsChunkingContext::builder(
403                project_path,
404                build_output_root.clone(),
405                build_output_root_to_root_path,
406                build_output_root.clone(),
407                build_output_root.clone(),
408                build_output_root.clone(),
409                Environment::new(ExecutionEnvironment::NodeJsLambda(
410                    NodeJsEnvironment::default().resolved_cell(),
411                ))
412                .to_resolved()
413                .await?,
414                runtime_type,
415            )
416            .source_maps(source_maps_type)
417            .module_id_strategy(module_id_strategy)
418            .export_usage(Some(binding_usage.connect().to_resolved().await?))
419            .unused_references(unused_references)
420            .minify_type(minify_type);
421
422            match *node_env.await? {
423                NodeEnv::Development => {}
424                NodeEnv::Production => {
425                    builder = builder
426                        .chunking_config(
427                            Vc::<EcmascriptChunkType>::default().to_resolved().await?,
428                            ChunkingConfig {
429                                min_chunk_size: 20_000,
430                                max_chunk_count_per_group: 100,
431                                max_merge_chunk_size: 100_000,
432                                ..Default::default()
433                            },
434                        )
435                        .chunking_config(
436                            Vc::<CssChunkType>::default().to_resolved().await?,
437                            ChunkingConfig {
438                                max_merge_chunk_size: 100_000,
439                                ..Default::default()
440                            },
441                        )
442                        .module_merging(scope_hoist);
443                }
444            }
445
446            Vc::upcast(builder.build())
447        }
448    };
449
450    let entry_chunk_groups = entries
451        .into_iter()
452        .map(|entry_module| {
453            let build_output_root = build_output_root.clone();
454
455            async move {
456                Ok(
457                    if let Some(ecmascript) =
458                        ResolvedVc::try_sidecast::<Box<dyn EvaluatableAsset>>(entry_module)
459                    {
460                        match target {
461                            Target::Browser => chunking_context.evaluated_chunk_group_assets(
462                                AssetIdent::from_path(
463                                    build_output_root
464                                        .join(ecmascript.ident().await?.path.file_stem().unwrap())?
465                                        .with_extension("entry.js"),
466                                )
467                                .into_vc(),
468                                ChunkGroup::Entry(
469                                    [ResolvedVc::upcast(ecmascript)].into_iter().collect(),
470                                ),
471                                module_graph,
472                                OutputAssets::empty(),
473                                AvailabilityInfo::root(),
474                            ),
475                            Target::Node => OutputAssetsWithReferenced {
476                                assets: ResolvedVc::cell(vec![
477                                    chunking_context
478                                        .entry_chunk_group(
479                                            build_output_root
480                                                .join(
481                                                    ecmascript
482                                                        .ident()
483                                                        .await?
484                                                        .path
485                                                        .file_stem()
486                                                        .unwrap(),
487                                                )?
488                                                .with_extension("entry.js"),
489                                            ChunkGroup::Entry(vec![ResolvedVc::upcast(ecmascript)]),
490                                            module_graph,
491                                            OutputAssets::empty(),
492                                            OutputAssets::empty(),
493                                            AvailabilityInfo::root(),
494                                        )
495                                        .await?
496                                        .asset,
497                                ]),
498                                referenced_assets: ResolvedVc::cell(vec![]),
499                                references: ResolvedVc::cell(vec![]),
500                            }
501                            .cell(),
502                        }
503                    } else {
504                        bail!(
505                            "Entry module is not chunkable, so it can't be used to bootstrap the \
506                             application"
507                        )
508                    },
509                )
510            }
511        })
512        .try_join()
513        .await?;
514
515    let all_assets = async move {
516        let mut all_assets: FxHashSet<ResolvedVc<Box<dyn OutputAsset>>> = FxHashSet::default();
517        for group in entry_chunk_groups {
518            all_assets.extend(group.expand_all_assets().await?);
519        }
520        anyhow::Ok(all_assets)
521    }
522    .instrument(tracing::info_span!("list chunks"))
523    .await?;
524
525    all_assets
526        .iter()
527        .map(|c| async move { c.content().write(c.path().owned().await?).await })
528        .try_join()
529        .await?;
530
531    Ok(())
532}
533
534pub async fn build(args: &BuildArguments) -> Result<()> {
535    let NormalizedDirs {
536        project_dir,
537        root_dir,
538    } = normalize_dirs(&args.common.dir, &args.common.root)?;
539
540    let is_ci = std::env::var("CI").is_ok_and(|v| !v.is_empty());
541    let is_short_session = true; // build sessions are always short
542
543    let tt = if args.common.persistent_caching {
544        let version_info = GitVersionInfo {
545            describe: env!("VERGEN_GIT_DESCRIBE"),
546            dirty: option_env!("CI").is_none_or(|v| v.is_empty())
547                && env!("VERGEN_GIT_DIRTY") == "true",
548        };
549        let cache_dir = args
550            .common
551            .cache_dir
552            .clone()
553            .unwrap_or_else(|| PathBuf::from(&*project_dir).join(".turbopack/cache"));
554        let (backing_storage, cache_state) =
555            turbo_backing_storage(&cache_dir, &version_info, is_ci, is_short_session, false)?;
556        let storage_mode = if std::env::var("TURBO_ENGINE_READ_ONLY").is_ok() {
557            StorageMode::ReadOnly
558        } else if is_ci || is_short_session {
559            StorageMode::ReadWriteOnShutdown
560        } else {
561            StorageMode::ReadWrite
562        };
563        let tt = TurboTasks::new(TurboTasksBackend::new(
564            BackendOptions {
565                dependency_tracking: false,
566                storage_mode: Some(storage_mode),
567                ..Default::default()
568            },
569            backing_storage,
570        ));
571        if let StartupCacheState::Invalidated { reason_code } = cache_state {
572            eprintln!(
573                "warn  - Turbopack cache was invalidated{}",
574                reason_code
575                    .as_deref()
576                    .map(|r| format!(": {r}"))
577                    .unwrap_or_default()
578            );
579        }
580        tt
581    } else {
582        TurboTasks::new(TurboTasksBackend::new(
583            BackendOptions {
584                dependency_tracking: false,
585                storage_mode: None,
586                ..Default::default()
587            },
588            noop_backing_storage(),
589        ))
590    };
591
592    let mut builder = TurbopackBuildBuilder::new(tt.clone(), project_dir, root_dir)
593        .log_detail(args.common.log_detail)
594        .log_level(
595            args.common
596                .log_level
597                .map_or_else(|| IssueSeverity::Warning, |l| l.0),
598        )
599        .source_maps_type(if args.no_sourcemap {
600            SourceMapsType::None
601        } else {
602            SourceMapsType::Full
603        })
604        .minify_type(if args.no_minify {
605            MinifyType::NoMinify
606        } else {
607            MinifyType::Minify {
608                mangle: Some(MangleType::OptimalSize),
609            }
610        })
611        .scope_hoist(!args.no_scope_hoist)
612        .target(args.common.target.unwrap_or(Target::Node))
613        .show_all(args.common.show_all);
614
615    for entry in normalize_entries(&args.common.entries) {
616        builder = builder.entry_request(EntryRequest::Relative(entry));
617    }
618
619    builder.build().await?;
620
621    // Intentionally leak this `Arc`. Otherwise we'll waste time during process exit performing a
622    // ton of drop calls.
623    if !args.force_memory_cleanup {
624        forget(tt);
625    }
626
627    Ok(())
628}