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 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 if !args.force_memory_cleanup {
546 forget(tt);
547 }
548
549 Ok(())
550}