diff --git a/Cargo.lock b/Cargo.lock index 4dc68b8..6803252 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -695,7 +695,7 @@ checksum = "fb37767f6569cd834a413442455e0f066d0d522de8630436e2a1761d9726ba56" [[package]] name = "oxbuild" -version = "0.1.2" +version = "0.1.3" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index f89ea61..ff5e217 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "oxbuild" -version = "0.1.2" +version = "0.1.3" description = "Ultra-fast typescript compiler" authors = ["Don Isaac "] license = "MIT" @@ -55,7 +55,7 @@ panic = "abort" # The profile that 'cargo dist' will build with [profile.dist] inherits = "release" -lto = "thin" +lto = "thin" # Config for 'cargo dist' [workspace.metadata.dist] @@ -66,7 +66,13 @@ ci = "github" # The installers to generate for each app installers = ["shell", "powershell", "npm"] # Target platforms to build apps for (Rust target-triple syntax) -targets = ["aarch64-apple-darwin", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-unknown-linux-musl", "x86_64-pc-windows-msvc"] +targets = [ + "aarch64-apple-darwin", + "x86_64-apple-darwin", + "x86_64-unknown-linux-gnu", + "x86_64-unknown-linux-musl", + "x86_64-pc-windows-msvc", +] # The archive format to use for windows builds (defaults .zip) windows-archive = ".tar.gz" # The archive format to use for non-windows builds (defaults .tar.xz) diff --git a/src/cli.rs b/src/cli.rs index a200a14..3d15580 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -46,6 +46,7 @@ By default, Oxbuild will look for a tsconfig.json next to the nearest package.js .get_matches() } +#[derive(Debug)] #[non_exhaustive] pub struct CliOptions { pub root: Root, diff --git a/src/cli/root.rs b/src/cli/root.rs index 5b0b37d..2c5068f 100644 --- a/src/cli/root.rs +++ b/src/cli/root.rs @@ -24,6 +24,12 @@ impl Deref for Root { type Target = Path; fn deref(&self) -> &Self::Target { + self.as_ref() + } +} + +impl AsRef for Root { + fn as_ref(&self) -> &Path { self.root.as_ref().unwrap_or(&self.cwd) } } diff --git a/src/compiler/options.rs b/src/compiler/options.rs index 87d2a23..d51146d 100644 --- a/src/compiler/options.rs +++ b/src/compiler/options.rs @@ -3,25 +3,32 @@ use std::{ path::{Path, PathBuf}, }; +use oxc::transformer::TransformOptions; + #[derive(Debug, Clone)] pub struct CompileOptions { root_dir: PathBuf, /// Emit .d.ts files using isolatedDeclarations. d_ts: bool, + transform_options: TransformOptions, } impl Default for CompileOptions { fn default() -> Self { let cwd = env::current_dir().unwrap(); - Self::new(cwd) + Self::new(cwd, TransformOptions::default()) } } impl CompileOptions { - pub fn new(root_dir: PathBuf) -> Self { + pub fn new(root_dir: PathBuf, transform_options: TransformOptions) -> Self { assert!(root_dir.is_dir()); assert!(root_dir.is_absolute()); + + debug_assert_eq!(root_dir, transform_options.cwd); + Self { + transform_options, root_dir, d_ts: false, } diff --git a/src/options.rs b/src/options.rs index 001643a..588a0e5 100644 --- a/src/options.rs +++ b/src/options.rs @@ -1,14 +1,17 @@ -use crate::cli::{CliOptions, Root}; use std::{ + fmt, fs::{self}, num::NonZeroUsize, path::PathBuf, }; use miette::{IntoDiagnostic, Report, Result, WrapErr}; +use oxc::transformer::TransformOptions; // use package_json::{PackageJson, PackageJsonManager}; use serde::Deserialize; +use crate::cli::{CliOptions, Root}; + // use crate::error::AnyError; pub struct OxbuildOptions { @@ -20,8 +23,7 @@ pub struct OxbuildOptions { /// Path to output folder where compiled code will be written. pub dist: PathBuf, pub num_threads: NonZeroUsize, - // package_json: PackageJson, - // tsconfig: Option, // TODO + pub transform_options: TransformOptions, } impl OxbuildOptions { @@ -71,26 +73,40 @@ impl OxbuildOptions { let dist = if let Some(out_dir) = co.and_then(|co| co.out_dir.as_ref()) { root.resolve(out_dir) } else { - let dist = root.join("dist").to_path_buf(); - if !dist.exists() { - fs::create_dir(&dist).into_diagnostic()?; - } - // TODO: clean dist dir? - dist + root.join("dist").to_path_buf() }; - assert!(dist.is_dir()); // FIXME: handle errors + + // TODO: clean dist dir? + if !dist.exists() { + fs::create_dir(&dist).into_diagnostic()?; + } + if !dist.is_dir() { + return Err(Report::msg(format!( + "Invalid output directory: '{}' is not a directory", + dist.display() + ))); + } let isolated_declarations = co .and_then(|co| co.isolated_declarations) // no tsconfig means they're using JavaScript. We can't emit .d.ts files in that case. .unwrap_or(false); + let mut transform_options = tsconfig + .as_ref() + .map(|tsconfig| tsconfig.transform_options()) + .transpose()? + .unwrap_or_default(); + + transform_options.cwd = root.to_path_buf(); + Ok(Self { root, isolated_declarations, src, dist, num_threads, + transform_options, }) } } @@ -106,6 +122,8 @@ impl TsConfig { } } +/// [`compilerOptions`](https://www.typescriptlang.org/tsconfig/#compilerOptions) in a +/// `tsconfig.json` file. #[derive(Debug, Default, Deserialize)] #[serde(rename_all = "camelCase")] struct TsConfigCompilerOptions { @@ -113,12 +131,85 @@ struct TsConfigCompilerOptions { root_dir: Option, out_dir: Option, isolated_declarations: Option, + /// https://www.typescriptlang.org/tsconfig/#target + #[serde(default)] + target: TsConfigTarget, } +/// https://www.typescriptlang.org/tsconfig/#target +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Deserialize)] +#[serde(rename_all = "snake_case")] // just needed for lowercasing values +pub enum TsConfigTarget { + /// Not supported by oxc + ES3, + ES5, + /// Same as es2015 + ES6, + /// Same as es6 + ES2015, + ES2016, + ES2017, + ES2018, + ES2019, + ES2020, + ES2021, + ES2022, + ES2023, + #[default] + ESNext, +} +impl fmt::Display for TsConfigTarget { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::ESNext => "ESNext".fmt(f), + Self::ES2023 => "ES2023".fmt(f), + Self::ES2022 => "ES2022".fmt(f), + Self::ES2021 => "ES2021".fmt(f), + Self::ES2020 => "ES2020".fmt(f), + Self::ES2019 => "ES2019".fmt(f), + Self::ES2018 => "ES2018".fmt(f), + Self::ES2017 => "ES2017".fmt(f), + Self::ES2016 => "ES2016".fmt(f), + Self::ES2015 => "ES2015".fmt(f), + Self::ES6 => "ES6".fmt(f), + Self::ES5 => "ES5".fmt(f), + Self::ES3 => "ES3".fmt(f), + } + } +} +impl TsConfigTarget { + /// Returns [`true`] if this version of ECMAScript is not supported by `oxc_transform` + fn is_unsupported(self) -> bool { + matches!(self, Self::ES3) + } +} + +/// A parsed `tsconfig.json` file. +/// +/// See: [TSConfig Reference](https://www.typescriptlang.org/tsconfig/) impl TsConfig { pub fn parse(mut source_text: String) -> Result { json_strip_comments::strip(&mut source_text).unwrap(); serde_json::from_str(&source_text).into_diagnostic() } + + pub fn transform_options(&self) -> Result { + let co = self.compiler_options(); + let target = co.map(|co| co.target).unwrap_or_default(); + + if target.is_unsupported() { + return Err(Report::msg(format!( + "Oxbuild does not support compiling to {target}. Please use a higher target version.", + ))); + } + let mut options = TransformOptions::default(); + + // TODO: set presets once TransformOptions supports factories that take a target ECMAScript version + if target <= TsConfigTarget::ES2021 { + options.es2021.logical_assignment_operators = true + } + + Ok(options) + } } diff --git a/src/walk.rs b/src/walk.rs index d563248..07fff4c 100644 --- a/src/walk.rs +++ b/src/walk.rs @@ -21,8 +21,12 @@ pub struct WalkerBuilder { impl WalkerBuilder { pub fn new(options: OxbuildOptions, sender: DiagnosticSender) -> Self { - let compile_options = CompileOptions::new(options.root.deref().to_path_buf()) - .with_d_ts(options.isolated_declarations); + let compile_options = CompileOptions::new( + options.root.deref().to_path_buf(), + options.transform_options.clone(), + ) + .with_d_ts(options.isolated_declarations); + Self { compile_options: Arc::new(compile_options), options: Arc::new(options),