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 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 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 if !args.force_memory_cleanup {
573 forget(tt);
574 }
575
576 Ok(())
577}