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 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 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 if !args.force_memory_cleanup {
566 forget(tt);
567 }
568
569 Ok(())
570}