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