use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use crate::configuration::{
Asset, Builder, CompilerOptions, DEFAULT_OUT_DIR, DEFAULT_SOURCE_ROOT, ProjectConfiguration,
};
use crate::utils::get_default_tsconfig_path::get_default_tsconfig_path_in;
pub mod assets_manager;
pub mod base_compiler;
pub mod compiler;
pub mod defaults;
pub mod helpers;
pub mod hooks;
pub mod interfaces;
pub mod plugins;
pub mod rust_toolchain_loader;
pub mod swc;
pub mod watch_compiler;
pub mod webpack_compiler;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ConfigValue {
Bool(bool),
String(String),
Object(BTreeMap<String, ConfigValue>),
}
pub fn get_value_of_path<'a>(
object: &'a BTreeMap<String, ConfigValue>,
property_path: &str,
) -> Option<&'a ConfigValue> {
let mut current = object;
let mut current_value = None;
let mut path = String::new();
let mut is_concat_in_progress = false;
for fragment in property_path.split('.') {
if fragment.starts_with('"') && fragment.ends_with('"') {
path = strip_double_quotes(fragment);
} else if fragment.starts_with('"') {
path.push_str(&strip_double_quotes(fragment));
path.push('.');
is_concat_in_progress = true;
continue;
} else if is_concat_in_progress && !fragment.ends_with('"') {
path.push_str(fragment);
path.push('.');
continue;
} else if fragment.ends_with('"') {
path.push_str(&strip_double_quotes(fragment));
is_concat_in_progress = false;
} else {
path = fragment.to_string();
}
current_value = current.get(&path);
match current_value {
Some(ConfigValue::Object(next)) => current = next,
Some(_) => {}
None => return None,
}
path.clear();
}
current_value
}
pub fn get_value_or_default<'a>(
object: &'a BTreeMap<String, ConfigValue>,
property_path: &str,
default: &'a ConfigValue,
) -> &'a ConfigValue {
get_value_of_path(object, property_path).unwrap_or(default)
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum BuilderVariant {
#[default]
Cargo,
Tsc,
Swc,
Webpack,
}
impl BuilderVariant {
pub const fn as_str(self) -> &'static str {
match self {
Self::Tsc => "tsc",
Self::Cargo => "cargo",
Self::Swc => "swc",
Self::Webpack => "webpack",
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct CompilerCommandOptions {
pub path: Option<String>,
pub webpack: Option<bool>,
pub webpack_path: Option<String>,
pub builder: Option<BuilderVariant>,
pub watch: Option<bool>,
pub watch_assets: Option<bool>,
pub type_check: Option<bool>,
pub preserve_watch_output: Option<bool>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct BuildCommand {
pub apps: Vec<String>,
pub options: CompilerCommandOptions,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BuildPlanRequest {
pub cwd: PathBuf,
pub command: BuildCommand,
pub project: ProjectConfiguration,
pub compiler_options: CompilerOptions,
pub ts_build_info_file: Option<PathBuf>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BuildPlan {
pub inputs: CompilerInputs,
pub watch: Option<WatchOptions>,
pub asset_deletes_on_unlink: Vec<AssetDeleteOnUnlink>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CompilerInputs {
pub builder: BuilderVariant,
pub cwd: PathBuf,
pub apps: Vec<String>,
pub ts_config_path: PathBuf,
pub webpack_config_path: Option<PathBuf>,
pub source_root: PathBuf,
pub entry_file: String,
pub output_dir: PathBuf,
pub type_check: bool,
pub assets: Vec<AssetPlan>,
pub output_cleanup: Option<OutputCleanup>,
pub swc: Option<SwcCompilerPlan>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SwcCompilerPlan {
pub swcrc_path: Option<PathBuf>,
pub cli_options: SwcCliOptions,
pub type_checker: Option<SwcTypeCheckerPlan>,
pub watch: Option<SwcWatchPlan>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SwcCliOptions {
pub out_dir: PathBuf,
pub filenames: Vec<PathBuf>,
pub sync: bool,
pub extensions: Vec<String>,
pub copy_files: bool,
pub include_dotfiles: bool,
pub quiet: bool,
pub watch: bool,
pub strip_leading_paths: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SwcTypeCheckerPlan {
TypeCheckerHost(SwcTypeCheckerHostPlan),
ForkedTypeChecker(SwcForkedTypeCheckerPlan),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SwcTypeCheckerHostPlan {
pub ts_config_path: PathBuf,
pub output_dir: PathBuf,
pub watch: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SwcForkedTypeCheckerPlan {
pub ts_config_path: PathBuf,
pub app_name: Option<String>,
pub source_root: PathBuf,
pub watch: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SwcWatchPlan {
pub watch_files_in_src_dir: SwcWatchFilesInSrcDirPlan,
pub watch_files_in_out_dir: SwcWatchFilesInOutDirPlan,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SwcWatchFilesInSrcDirPlan {
pub src_dir: Option<PathBuf>,
pub extensions: Vec<String>,
pub ignore_initial: bool,
pub await_write_finish_stability_threshold_ms: u64,
pub await_write_finish_poll_interval_ms: u64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SwcWatchFilesInOutDirPlan {
pub out_dir: PathBuf,
pub extensions: Vec<String>,
pub debounce_ms: u64,
pub ignore_initial: bool,
pub await_write_finish_stability_threshold_ms: u64,
pub await_write_finish_poll_interval_ms: u64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AssetPlan {
pub glob: String,
pub include: Option<PathBuf>,
pub exclude: Option<String>,
pub out_dir: PathBuf,
pub flat: bool,
pub watch_assets: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct OutputCleanup {
pub out_dir: PathBuf,
pub ts_build_info_file: Option<PathBuf>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct WatchOptions {
pub manual_restart: bool,
pub preserve_watch_output: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AssetDeleteOnUnlink {
pub glob: String,
pub out_dir: PathBuf,
}
pub fn get_builder(
options: &CompilerCommandOptions,
compiler_options: &CompilerOptions,
) -> BuilderVariant {
if options.webpack == Some(true) {
return BuilderVariant::Webpack;
}
if let Some(builder) = options.builder {
return builder;
}
if compiler_options.webpack {
return BuilderVariant::Webpack;
}
match compiler_options.builder {
Builder::Cargo => BuilderVariant::Cargo,
Builder::Tsc(_) => BuilderVariant::Tsc,
Builder::Swc(_) => BuilderVariant::Swc,
Builder::Webpack(_) => BuilderVariant::Webpack,
}
}
pub fn get_tsc_config_path(
options: &CompilerCommandOptions,
compiler_options: &CompilerOptions,
) -> PathBuf {
get_tsc_config_path_in(Path::new("."), options, compiler_options)
}
pub fn get_tsc_config_path_in(
cwd: &Path,
options: &CompilerCommandOptions,
compiler_options: &CompilerOptions,
) -> PathBuf {
if let Some(path) = &options.path {
return PathBuf::from(path);
}
if let Some(path) = &compiler_options.ts_config_path {
return PathBuf::from(path);
}
if let Builder::Tsc(builder_options) = &compiler_options.builder {
if let Some(path) = &builder_options.config_path {
return PathBuf::from(path);
}
}
PathBuf::from(get_default_tsconfig_path_in(cwd))
}
pub fn get_webpack_config_path(
options: &CompilerCommandOptions,
compiler_options: &CompilerOptions,
) -> Option<PathBuf> {
if let Some(path) = &options.webpack_path {
return Some(PathBuf::from(path));
}
if let Some(path) = &compiler_options.webpack_config_path {
return Some(PathBuf::from(path));
}
if let Builder::Webpack(builder_options) = &compiler_options.builder {
return builder_options.config_path.as_ref().map(PathBuf::from);
}
None
}
pub fn create_build_plan(request: BuildPlanRequest) -> BuildPlan {
let options = &request.command.options;
let builder = get_builder(options, &request.compiler_options);
let output_dir = get_output_dir(&request.compiler_options);
let source_root = request
.project
.source_root
.as_ref()
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from(DEFAULT_SOURCE_ROOT));
let entry_file = request
.project
.entry_file
.clone()
.unwrap_or_else(|| "main".to_string());
let assets = create_asset_plans(
&request.compiler_options.assets,
&output_dir,
options.watch_assets.unwrap_or(false),
);
let asset_deletes_on_unlink = assets
.iter()
.filter(|asset| asset.watch_assets)
.map(|asset| AssetDeleteOnUnlink {
glob: asset.glob.clone(),
out_dir: asset.out_dir.clone(),
})
.collect();
let watch_enabled = options.watch.unwrap_or(false);
let type_check = options.type_check.unwrap_or(false);
let ts_config_path = get_tsc_config_path_in(&request.cwd, options, &request.compiler_options);
let swc = (builder == BuilderVariant::Swc).then(|| {
create_swc_compiler_plan(
&request.command.apps,
&request.compiler_options,
&ts_config_path,
&source_root,
&output_dir,
type_check,
watch_enabled,
)
});
BuildPlan {
inputs: CompilerInputs {
builder,
cwd: request.cwd,
apps: request.command.apps,
ts_config_path,
webpack_config_path: get_webpack_config_path(options, &request.compiler_options),
source_root,
entry_file,
output_dir: output_dir.clone(),
type_check,
assets,
output_cleanup: request
.compiler_options
.delete_out_dir
.unwrap_or(false)
.then_some(OutputCleanup {
out_dir: output_dir,
ts_build_info_file: request.ts_build_info_file,
}),
swc,
},
watch: watch_enabled.then_some(WatchOptions {
manual_restart: request.compiler_options.manual_restart,
preserve_watch_output: options.preserve_watch_output.unwrap_or(false),
}),
asset_deletes_on_unlink,
}
}
fn create_swc_compiler_plan(
apps: &[String],
compiler_options: &CompilerOptions,
ts_config_path: &PathBuf,
source_root: &PathBuf,
output_dir: &PathBuf,
type_check: bool,
watch: bool,
) -> SwcCompilerPlan {
let builder_options = match &compiler_options.builder {
Builder::Swc(options) => Some(options),
_ => None,
};
let cli_options = create_swc_cli_options(builder_options, source_root, output_dir, watch);
SwcCompilerPlan {
swcrc_path: builder_options
.and_then(|options| options.swcrc_path.as_ref())
.map(PathBuf::from),
type_checker: type_check.then(|| {
if watch {
SwcTypeCheckerPlan::ForkedTypeChecker(SwcForkedTypeCheckerPlan {
ts_config_path: ts_config_path.clone(),
app_name: apps.first().cloned(),
source_root: source_root.clone(),
watch,
})
} else {
SwcTypeCheckerPlan::TypeCheckerHost(SwcTypeCheckerHostPlan {
ts_config_path: ts_config_path.clone(),
output_dir: output_dir.clone(),
watch,
})
}
}),
watch: watch.then(|| create_swc_watch_plan(&cli_options)),
cli_options,
}
}
fn create_swc_cli_options(
builder_options: Option<&crate::configuration::SwcBuilderOptions>,
source_root: &PathBuf,
output_dir: &PathBuf,
watch: bool,
) -> SwcCliOptions {
let default_filenames = vec![source_root.clone()];
let default_extensions = vec![".js".to_string(), ".ts".to_string()];
SwcCliOptions {
out_dir: builder_options
.and_then(|options| options.out_dir.as_ref())
.map(PathBuf::from)
.unwrap_or_else(|| output_dir.clone()),
filenames: builder_options
.map(|options| path_list_or_default(&options.filenames, default_filenames.clone()))
.unwrap_or(default_filenames),
sync: builder_options
.and_then(|options| options.sync)
.unwrap_or(false),
extensions: builder_options
.map(|options| string_list_or_default(&options.extensions, default_extensions.clone()))
.unwrap_or(default_extensions),
copy_files: builder_options
.and_then(|options| options.copy_files)
.unwrap_or(false),
include_dotfiles: builder_options
.and_then(|options| options.include_dotfiles)
.unwrap_or(false),
quiet: builder_options
.and_then(|options| options.quiet)
.unwrap_or(false),
watch,
strip_leading_paths: true,
}
}
fn create_swc_watch_plan(cli_options: &SwcCliOptions) -> SwcWatchPlan {
SwcWatchPlan {
watch_files_in_src_dir: SwcWatchFilesInSrcDirPlan {
src_dir: cli_options.filenames.first().cloned(),
extensions: cli_options.extensions.clone(),
ignore_initial: true,
await_write_finish_stability_threshold_ms: 50,
await_write_finish_poll_interval_ms: 10,
},
watch_files_in_out_dir: SwcWatchFilesInOutDirPlan {
out_dir: cli_options.out_dir.clone(),
extensions: vec![".js".to_string(), ".mjs".to_string()],
debounce_ms: 150,
ignore_initial: true,
await_write_finish_stability_threshold_ms: 50,
await_write_finish_poll_interval_ms: 10,
},
}
}
fn path_list_or_default(values: &[String], default: Vec<PathBuf>) -> Vec<PathBuf> {
if values.is_empty() {
default
} else {
values.iter().map(PathBuf::from).collect()
}
}
fn string_list_or_default(values: &[String], default: Vec<String>) -> Vec<String> {
if values.is_empty() {
default
} else {
values.to_vec()
}
}
fn get_output_dir(compiler_options: &CompilerOptions) -> PathBuf {
if let Builder::Swc(options) = &compiler_options.builder {
if let Some(out_dir) = &options.out_dir {
return PathBuf::from(out_dir);
}
}
PathBuf::from(DEFAULT_OUT_DIR)
}
fn create_asset_plans(
assets: &[Asset],
default_out_dir: &PathBuf,
default_watch_assets: bool,
) -> Vec<AssetPlan> {
assets
.iter()
.map(|asset| match asset {
Asset::Glob(glob) => AssetPlan {
glob: glob.clone(),
include: None,
exclude: None,
out_dir: default_out_dir.clone(),
flat: false,
watch_assets: default_watch_assets,
},
Asset::Entry(entry) => AssetPlan {
glob: entry.glob.clone(),
include: entry.include.as_ref().map(PathBuf::from),
exclude: entry.exclude.clone(),
out_dir: entry
.out_dir
.as_ref()
.map(PathBuf::from)
.unwrap_or_else(|| default_out_dir.clone()),
flat: entry.flat.unwrap_or(false),
watch_assets: entry.watch_assets.unwrap_or(default_watch_assets),
},
})
.collect()
}
fn strip_double_quotes(text: &str) -> String {
text.replace('"', "")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reads_path_with_quoted_fragment_containing_dot() {
let mut app = BTreeMap::new();
app.insert(
"sourceRoot".to_string(),
ConfigValue::String("src".to_string()),
);
let mut projects = BTreeMap::new();
projects.insert("api.v1".to_string(), ConfigValue::Object(app));
let mut root = BTreeMap::new();
root.insert("projects".to_string(), ConfigValue::Object(projects));
assert_eq!(
get_value_of_path(&root, "projects.\"api.v1\".sourceRoot"),
Some(&ConfigValue::String("src".to_string()))
);
}
}