Skip to main content

cli/lib/compiler/
mod.rs

1//! Build and watch orchestration for Rust-native nestrs projects.
2//!
3//! Upstream source: `../nest-cli/lib/compiler`.
4
5use 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}