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