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