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 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_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; 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 if !args.force_memory_cleanup {
624 forget(tt);
625 }
626
627 Ok(())
628}