1use std::collections::BTreeMap;
6use std::path::{Path, PathBuf};
7
8use crate::configuration::{
9 Asset, Builder, CompilerOptions, DEFAULT_OUT_DIR, DEFAULT_SOURCE_ROOT, ProjectConfiguration,
10};
11use crate::utils::get_default_tsconfig_path::get_default_tsconfig_path_in;
12
13pub mod assets_manager;
14pub mod base_compiler;
15pub mod compiler;
16pub mod defaults;
17pub mod helpers;
18pub mod hooks;
19pub mod interfaces;
20pub mod plugins;
21pub mod rust_toolchain_loader;
22pub mod swc;
23pub mod watch_compiler;
24pub mod webpack_compiler;
25
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub enum ConfigValue {
28 Bool(bool),
29 String(String),
30 Object(BTreeMap<String, ConfigValue>),
31}
32
33pub fn get_value_of_path<'a>(
34 object: &'a BTreeMap<String, ConfigValue>,
35 property_path: &str,
36) -> Option<&'a ConfigValue> {
37 let mut current = object;
38 let mut current_value = None;
39 let mut path = String::new();
40 let mut is_concat_in_progress = false;
41
42 for fragment in property_path.split('.') {
43 if fragment.starts_with('"') && fragment.ends_with('"') {
44 path = strip_double_quotes(fragment);
45 } else if fragment.starts_with('"') {
46 path.push_str(&strip_double_quotes(fragment));
47 path.push('.');
48 is_concat_in_progress = true;
49 continue;
50 } else if is_concat_in_progress && !fragment.ends_with('"') {
51 path.push_str(fragment);
52 path.push('.');
53 continue;
54 } else if fragment.ends_with('"') {
55 path.push_str(&strip_double_quotes(fragment));
56 is_concat_in_progress = false;
57 } else {
58 path = fragment.to_string();
59 }
60
61 current_value = current.get(&path);
62 match current_value {
63 Some(ConfigValue::Object(next)) => current = next,
64 Some(_) => {}
65 None => return None,
66 }
67 path.clear();
68 }
69
70 current_value
71}
72
73pub fn get_value_or_default<'a>(
74 object: &'a BTreeMap<String, ConfigValue>,
75 property_path: &str,
76 default: &'a ConfigValue,
77) -> &'a ConfigValue {
78 get_value_of_path(object, property_path).unwrap_or(default)
79}
80
81#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
82pub enum BuilderVariant {
83 #[default]
84 Cargo,
85 Tsc,
86 Swc,
87 Webpack,
88}
89
90impl BuilderVariant {
91 pub const fn as_str(self) -> &'static str {
92 match self {
93 Self::Tsc => "tsc",
94 Self::Cargo => "cargo",
95 Self::Swc => "swc",
96 Self::Webpack => "webpack",
97 }
98 }
99}
100
101#[derive(Clone, Debug, Default, PartialEq, Eq)]
102pub struct CompilerCommandOptions {
103 pub path: Option<String>,
104 pub webpack: Option<bool>,
105 pub webpack_path: Option<String>,
106 pub builder: Option<BuilderVariant>,
107 pub watch: Option<bool>,
108 pub watch_assets: Option<bool>,
109 pub type_check: Option<bool>,
110 pub preserve_watch_output: Option<bool>,
111}
112
113#[derive(Clone, Debug, Default, PartialEq, Eq)]
114pub struct BuildCommand {
115 pub apps: Vec<String>,
116 pub options: CompilerCommandOptions,
117}
118
119#[derive(Clone, Debug, PartialEq, Eq)]
120pub struct BuildPlanRequest {
121 pub cwd: PathBuf,
122 pub command: BuildCommand,
123 pub project: ProjectConfiguration,
124 pub compiler_options: CompilerOptions,
125 pub ts_build_info_file: Option<PathBuf>,
126}
127
128#[derive(Clone, Debug, PartialEq, Eq)]
129pub struct BuildPlan {
130 pub inputs: CompilerInputs,
131 pub watch: Option<WatchOptions>,
132 pub asset_deletes_on_unlink: Vec<AssetDeleteOnUnlink>,
133}
134
135#[derive(Clone, Debug, PartialEq, Eq)]
136pub struct CompilerInputs {
137 pub builder: BuilderVariant,
138 pub cwd: PathBuf,
139 pub apps: Vec<String>,
140 pub ts_config_path: PathBuf,
141 pub webpack_config_path: Option<PathBuf>,
142 pub source_root: PathBuf,
143 pub entry_file: String,
144 pub output_dir: PathBuf,
145 pub type_check: bool,
146 pub assets: Vec<AssetPlan>,
147 pub output_cleanup: Option<OutputCleanup>,
148 pub swc: Option<SwcCompilerPlan>,
149}
150
151#[derive(Clone, Debug, PartialEq, Eq)]
152pub struct SwcCompilerPlan {
153 pub swcrc_path: Option<PathBuf>,
154 pub cli_options: SwcCliOptions,
155 pub type_checker: Option<SwcTypeCheckerPlan>,
156 pub watch: Option<SwcWatchPlan>,
157}
158
159#[derive(Clone, Debug, PartialEq, Eq)]
160pub struct SwcCliOptions {
161 pub out_dir: PathBuf,
162 pub filenames: Vec<PathBuf>,
163 pub sync: bool,
164 pub extensions: Vec<String>,
165 pub copy_files: bool,
166 pub include_dotfiles: bool,
167 pub quiet: bool,
168 pub watch: bool,
169 pub strip_leading_paths: bool,
170}
171
172#[derive(Clone, Debug, PartialEq, Eq)]
173pub enum SwcTypeCheckerPlan {
174 TypeCheckerHost(SwcTypeCheckerHostPlan),
175 ForkedTypeChecker(SwcForkedTypeCheckerPlan),
176}
177
178#[derive(Clone, Debug, PartialEq, Eq)]
179pub struct SwcTypeCheckerHostPlan {
180 pub ts_config_path: PathBuf,
181 pub output_dir: PathBuf,
182 pub watch: bool,
183}
184
185#[derive(Clone, Debug, PartialEq, Eq)]
186pub struct SwcForkedTypeCheckerPlan {
187 pub ts_config_path: PathBuf,
188 pub app_name: Option<String>,
189 pub source_root: PathBuf,
190 pub watch: bool,
191}
192
193#[derive(Clone, Debug, PartialEq, Eq)]
194pub struct SwcWatchPlan {
195 pub watch_files_in_src_dir: SwcWatchFilesInSrcDirPlan,
196 pub watch_files_in_out_dir: SwcWatchFilesInOutDirPlan,
197}
198
199#[derive(Clone, Debug, PartialEq, Eq)]
200pub struct SwcWatchFilesInSrcDirPlan {
201 pub src_dir: Option<PathBuf>,
202 pub extensions: Vec<String>,
203 pub ignore_initial: bool,
204 pub await_write_finish_stability_threshold_ms: u64,
205 pub await_write_finish_poll_interval_ms: u64,
206}
207
208#[derive(Clone, Debug, PartialEq, Eq)]
209pub struct SwcWatchFilesInOutDirPlan {
210 pub out_dir: PathBuf,
211 pub extensions: Vec<String>,
212 pub debounce_ms: u64,
213 pub ignore_initial: bool,
214 pub await_write_finish_stability_threshold_ms: u64,
215 pub await_write_finish_poll_interval_ms: u64,
216}
217
218#[derive(Clone, Debug, PartialEq, Eq)]
219pub struct AssetPlan {
220 pub glob: String,
221 pub include: Option<PathBuf>,
222 pub exclude: Option<String>,
223 pub out_dir: PathBuf,
224 pub flat: bool,
225 pub watch_assets: bool,
226}
227
228#[derive(Clone, Debug, PartialEq, Eq)]
229pub struct OutputCleanup {
230 pub out_dir: PathBuf,
231 pub ts_build_info_file: Option<PathBuf>,
232}
233
234#[derive(Clone, Debug, PartialEq, Eq)]
235pub struct WatchOptions {
236 pub manual_restart: bool,
237 pub preserve_watch_output: bool,
238}
239
240#[derive(Clone, Debug, PartialEq, Eq)]
241pub struct AssetDeleteOnUnlink {
242 pub glob: String,
243 pub out_dir: PathBuf,
244}
245
246pub fn get_builder(
247 options: &CompilerCommandOptions,
248 compiler_options: &CompilerOptions,
249) -> BuilderVariant {
250 if options.webpack == Some(true) {
251 return BuilderVariant::Webpack;
252 }
253
254 if let Some(builder) = options.builder {
255 return builder;
256 }
257
258 if compiler_options.webpack {
259 return BuilderVariant::Webpack;
260 }
261
262 match compiler_options.builder {
263 Builder::Cargo => BuilderVariant::Cargo,
264 Builder::Tsc(_) => BuilderVariant::Tsc,
265 Builder::Swc(_) => BuilderVariant::Swc,
266 Builder::Webpack(_) => BuilderVariant::Webpack,
267 }
268}
269
270pub fn get_tsc_config_path(
271 options: &CompilerCommandOptions,
272 compiler_options: &CompilerOptions,
273) -> PathBuf {
274 get_tsc_config_path_in(Path::new("."), options, compiler_options)
275}
276
277pub fn get_tsc_config_path_in(
278 cwd: &Path,
279 options: &CompilerCommandOptions,
280 compiler_options: &CompilerOptions,
281) -> PathBuf {
282 if let Some(path) = &options.path {
283 return PathBuf::from(path);
284 }
285
286 if let Some(path) = &compiler_options.ts_config_path {
287 return PathBuf::from(path);
288 }
289
290 if let Builder::Tsc(builder_options) = &compiler_options.builder {
291 if let Some(path) = &builder_options.config_path {
292 return PathBuf::from(path);
293 }
294 }
295
296 PathBuf::from(get_default_tsconfig_path_in(cwd))
297}
298
299pub fn get_webpack_config_path(
300 options: &CompilerCommandOptions,
301 compiler_options: &CompilerOptions,
302) -> Option<PathBuf> {
303 if let Some(path) = &options.webpack_path {
304 return Some(PathBuf::from(path));
305 }
306
307 if let Some(path) = &compiler_options.webpack_config_path {
308 return Some(PathBuf::from(path));
309 }
310
311 if let Builder::Webpack(builder_options) = &compiler_options.builder {
312 return builder_options.config_path.as_ref().map(PathBuf::from);
313 }
314
315 None
316}
317
318pub fn create_build_plan(request: BuildPlanRequest) -> BuildPlan {
319 let options = &request.command.options;
320 let builder = get_builder(options, &request.compiler_options);
321 let output_dir = get_output_dir(&request.compiler_options);
322 let source_root = request
323 .project
324 .source_root
325 .as_ref()
326 .map(PathBuf::from)
327 .unwrap_or_else(|| PathBuf::from(DEFAULT_SOURCE_ROOT));
328 let entry_file = request
329 .project
330 .entry_file
331 .clone()
332 .unwrap_or_else(|| "main".to_string());
333 let assets = create_asset_plans(
334 &request.compiler_options.assets,
335 &output_dir,
336 options.watch_assets.unwrap_or(false),
337 );
338 let asset_deletes_on_unlink = assets
339 .iter()
340 .filter(|asset| asset.watch_assets)
341 .map(|asset| AssetDeleteOnUnlink {
342 glob: asset.glob.clone(),
343 out_dir: asset.out_dir.clone(),
344 })
345 .collect();
346 let watch_enabled = options.watch.unwrap_or(false);
347 let type_check = options.type_check.unwrap_or(false);
348 let ts_config_path = get_tsc_config_path_in(&request.cwd, options, &request.compiler_options);
349 let swc = (builder == BuilderVariant::Swc).then(|| {
350 create_swc_compiler_plan(
351 &request.command.apps,
352 &request.compiler_options,
353 &ts_config_path,
354 &source_root,
355 &output_dir,
356 type_check,
357 watch_enabled,
358 )
359 });
360
361 BuildPlan {
362 inputs: CompilerInputs {
363 builder,
364 cwd: request.cwd,
365 apps: request.command.apps,
366 ts_config_path,
367 webpack_config_path: get_webpack_config_path(options, &request.compiler_options),
368 source_root,
369 entry_file,
370 output_dir: output_dir.clone(),
371 type_check,
372 assets,
373 output_cleanup: request
374 .compiler_options
375 .delete_out_dir
376 .unwrap_or(false)
377 .then_some(OutputCleanup {
378 out_dir: output_dir,
379 ts_build_info_file: request.ts_build_info_file,
380 }),
381 swc,
382 },
383 watch: watch_enabled.then_some(WatchOptions {
384 manual_restart: request.compiler_options.manual_restart,
385 preserve_watch_output: options.preserve_watch_output.unwrap_or(false),
386 }),
387 asset_deletes_on_unlink,
388 }
389}
390
391fn create_swc_compiler_plan(
392 apps: &[String],
393 compiler_options: &CompilerOptions,
394 ts_config_path: &PathBuf,
395 source_root: &PathBuf,
396 output_dir: &PathBuf,
397 type_check: bool,
398 watch: bool,
399) -> SwcCompilerPlan {
400 let builder_options = match &compiler_options.builder {
401 Builder::Swc(options) => Some(options),
402 _ => None,
403 };
404 let cli_options = create_swc_cli_options(builder_options, source_root, output_dir, watch);
405
406 SwcCompilerPlan {
407 swcrc_path: builder_options
408 .and_then(|options| options.swcrc_path.as_ref())
409 .map(PathBuf::from),
410 type_checker: type_check.then(|| {
411 if watch {
412 SwcTypeCheckerPlan::ForkedTypeChecker(SwcForkedTypeCheckerPlan {
413 ts_config_path: ts_config_path.clone(),
414 app_name: apps.first().cloned(),
415 source_root: source_root.clone(),
416 watch,
417 })
418 } else {
419 SwcTypeCheckerPlan::TypeCheckerHost(SwcTypeCheckerHostPlan {
420 ts_config_path: ts_config_path.clone(),
421 output_dir: output_dir.clone(),
422 watch,
423 })
424 }
425 }),
426 watch: watch.then(|| create_swc_watch_plan(&cli_options)),
427 cli_options,
428 }
429}
430
431fn create_swc_cli_options(
432 builder_options: Option<&crate::configuration::SwcBuilderOptions>,
433 source_root: &PathBuf,
434 output_dir: &PathBuf,
435 watch: bool,
436) -> SwcCliOptions {
437 let default_filenames = vec![source_root.clone()];
438 let default_extensions = vec![".js".to_string(), ".ts".to_string()];
439
440 SwcCliOptions {
441 out_dir: builder_options
442 .and_then(|options| options.out_dir.as_ref())
443 .map(PathBuf::from)
444 .unwrap_or_else(|| output_dir.clone()),
445 filenames: builder_options
446 .map(|options| path_list_or_default(&options.filenames, default_filenames.clone()))
447 .unwrap_or(default_filenames),
448 sync: builder_options
449 .and_then(|options| options.sync)
450 .unwrap_or(false),
451 extensions: builder_options
452 .map(|options| string_list_or_default(&options.extensions, default_extensions.clone()))
453 .unwrap_or(default_extensions),
454 copy_files: builder_options
455 .and_then(|options| options.copy_files)
456 .unwrap_or(false),
457 include_dotfiles: builder_options
458 .and_then(|options| options.include_dotfiles)
459 .unwrap_or(false),
460 quiet: builder_options
461 .and_then(|options| options.quiet)
462 .unwrap_or(false),
463 watch,
464 strip_leading_paths: true,
465 }
466}
467
468fn create_swc_watch_plan(cli_options: &SwcCliOptions) -> SwcWatchPlan {
469 SwcWatchPlan {
470 watch_files_in_src_dir: SwcWatchFilesInSrcDirPlan {
471 src_dir: cli_options.filenames.first().cloned(),
472 extensions: cli_options.extensions.clone(),
473 ignore_initial: true,
474 await_write_finish_stability_threshold_ms: 50,
475 await_write_finish_poll_interval_ms: 10,
476 },
477 watch_files_in_out_dir: SwcWatchFilesInOutDirPlan {
478 out_dir: cli_options.out_dir.clone(),
479 extensions: vec![".js".to_string(), ".mjs".to_string()],
480 debounce_ms: 150,
481 ignore_initial: true,
482 await_write_finish_stability_threshold_ms: 50,
483 await_write_finish_poll_interval_ms: 10,
484 },
485 }
486}
487
488fn path_list_or_default(values: &[String], default: Vec<PathBuf>) -> Vec<PathBuf> {
489 if values.is_empty() {
490 default
491 } else {
492 values.iter().map(PathBuf::from).collect()
493 }
494}
495
496fn string_list_or_default(values: &[String], default: Vec<String>) -> Vec<String> {
497 if values.is_empty() {
498 default
499 } else {
500 values.to_vec()
501 }
502}
503
504fn get_output_dir(compiler_options: &CompilerOptions) -> PathBuf {
505 if let Builder::Swc(options) = &compiler_options.builder {
506 if let Some(out_dir) = &options.out_dir {
507 return PathBuf::from(out_dir);
508 }
509 }
510
511 PathBuf::from(DEFAULT_OUT_DIR)
512}
513
514fn create_asset_plans(
515 assets: &[Asset],
516 default_out_dir: &PathBuf,
517 default_watch_assets: bool,
518) -> Vec<AssetPlan> {
519 assets
520 .iter()
521 .map(|asset| match asset {
522 Asset::Glob(glob) => AssetPlan {
523 glob: glob.clone(),
524 include: None,
525 exclude: None,
526 out_dir: default_out_dir.clone(),
527 flat: false,
528 watch_assets: default_watch_assets,
529 },
530 Asset::Entry(entry) => AssetPlan {
531 glob: entry.glob.clone(),
532 include: entry.include.as_ref().map(PathBuf::from),
533 exclude: entry.exclude.clone(),
534 out_dir: entry
535 .out_dir
536 .as_ref()
537 .map(PathBuf::from)
538 .unwrap_or_else(|| default_out_dir.clone()),
539 flat: entry.flat.unwrap_or(false),
540 watch_assets: entry.watch_assets.unwrap_or(default_watch_assets),
541 },
542 })
543 .collect()
544}
545
546fn strip_double_quotes(text: &str) -> String {
547 text.replace('"', "")
548}
549
550#[cfg(test)]
551mod tests {
552 use super::*;
553
554 #[test]
555 fn reads_path_with_quoted_fragment_containing_dot() {
556 let mut app = BTreeMap::new();
557 app.insert(
558 "sourceRoot".to_string(),
559 ConfigValue::String("src".to_string()),
560 );
561
562 let mut projects = BTreeMap::new();
563 projects.insert("api.v1".to_string(), ConfigValue::Object(app));
564
565 let mut root = BTreeMap::new();
566 root.insert("projects".to_string(), ConfigValue::Object(projects));
567
568 assert_eq!(
569 get_value_of_path(&root, "projects.\"api.v1\".sourceRoot"),
570 Some(&ConfigValue::String("src".to_string()))
571 );
572 }
573}