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