oxc_minify_napi 0.135.0

A collection of JavaScript tools written in Rust.
Documentation
use napi::Either;
use napi_derive::napi;

use oxc_compat::EngineTargets;

#[napi(object)]
pub struct TreeShakeOptions {
    /// Whether to respect the pure annotations.
    ///
    /// Pure annotations are comments that mark an expression as pure.
    /// For example: @__PURE__ or #__NO_SIDE_EFFECTS__.
    ///
    /// @default true
    pub annotations: Option<bool>,

    /// Whether to treat this function call as pure.
    ///
    /// This function is called for normal function calls, new calls, and
    /// tagged template calls.
    pub manual_pure_functions: Option<Vec<String>>,

    /// Whether property read accesses have side effects.
    ///
    /// @default 'always'
    #[napi(ts_type = "boolean | 'always'")]
    pub property_read_side_effects: Option<Either<bool, String>>,

    /// Whether property write accesses (assignments to member expressions) have side effects.
    ///
    /// When false, assignments like `obj.prop = value` are considered side-effect-free
    /// (assuming the object and value expressions themselves are side-effect-free).
    ///
    /// @default true
    pub property_write_side_effects: Option<bool>,

    /// Whether accessing a global variable has side effects.
    ///
    /// Accessing a non-existing global variable will throw an error.
    /// Global variable may be a getter that has side effects.
    ///
    /// @default true
    pub unknown_global_side_effects: Option<bool>,

    /// Whether invalid import statements have side effects.
    ///
    /// Accessing a non-existing import name will throw an error.
    /// Also import statements that cannot be resolved will throw an error.
    ///
    /// @default true
    pub invalid_import_side_effects: Option<bool>,
}

impl TryFrom<&TreeShakeOptions> for oxc_minifier::TreeShakeOptions {
    type Error = String;

    fn try_from(o: &TreeShakeOptions) -> Result<Self, Self::Error> {
        let default = oxc_minifier::TreeShakeOptions::default();
        Ok(oxc_minifier::TreeShakeOptions {
            annotations: o.annotations.unwrap_or(default.annotations),
            manual_pure_functions: o
                .manual_pure_functions
                .clone()
                .unwrap_or(default.manual_pure_functions),
            property_read_side_effects: match &o.property_read_side_effects {
                Some(Either::A(false)) => oxc_minifier::PropertyReadSideEffects::None,
                Some(Either::A(true)) => oxc_minifier::PropertyReadSideEffects::All,
                Some(Either::B(s)) if s == "always" => oxc_minifier::PropertyReadSideEffects::All,
                Some(Either::B(s)) => {
                    return Err(format!(
                        "Invalid propertyReadSideEffects value: '{s}'. Expected 'always'."
                    ));
                }
                None => default.property_read_side_effects,
            },
            property_write_side_effects: o
                .property_write_side_effects
                .unwrap_or(default.property_write_side_effects),
            unknown_global_side_effects: o
                .unknown_global_side_effects
                .unwrap_or(default.unknown_global_side_effects),
            invalid_import_side_effects: o
                .invalid_import_side_effects
                .unwrap_or(default.invalid_import_side_effects),
        })
    }
}

#[napi(object)]
pub struct CompressOptions {
    /// Set desired EcmaScript standard version for output.
    ///
    /// Set `esnext` to enable all target highering.
    ///
    /// Example:
    ///
    /// * `'es2015'`
    /// * `['es2020', 'chrome58', 'edge16', 'firefox57', 'node12', 'safari11']`
    ///
    /// @default 'esnext'
    ///
    /// @see [oxc#target](https://oxc.rs/docs/guide/usage/transformer/lowering#target)
    pub target: Option<Either<String, Vec<String>>>,

    /// Pass true to discard calls to `console.*`.
    ///
    /// @default false
    pub drop_console: Option<bool>,

    /// Remove `debugger;` statements.
    ///
    /// @default true
    pub drop_debugger: Option<bool>,

    /// Pass `true` to drop unreferenced functions and variables.
    ///
    /// Simple direct variable assignments do not count as references unless set to `keep_assign`.
    /// @default true
    #[napi(ts_type = "boolean | 'keep_assign'")]
    pub unused: Option<Either<bool, String>>,

    /// Keep function / class names.
    pub keep_names: Option<CompressOptionsKeepNames>,

    /// Join consecutive var, let and const statements.
    ///
    /// @default true
    pub join_vars: Option<bool>,

    /// Join consecutive simple statements using the comma operator.
    ///
    /// `a; b` -> `a, b`
    ///
    /// @default true
    pub sequences: Option<bool>,

    /// Set of label names to drop from the code.
    ///
    /// Labeled statements matching these names will be removed during minification.
    ///
    /// @default []
    pub drop_labels: Option<Vec<String>>,

    /// Limit the maximum number of iterations for debugging purpose.
    pub max_iterations: Option<u8>,

    /// Treeshake options.
    pub treeshake: Option<TreeShakeOptions>,
}

impl TryFrom<&CompressOptions> for oxc_minifier::CompressOptions {
    type Error = String;
    fn try_from(o: &CompressOptions) -> Result<Self, Self::Error> {
        let default = oxc_minifier::CompressOptions::default();
        Ok(oxc_minifier::CompressOptions {
            target: match &o.target {
                Some(Either::A(s)) => EngineTargets::from_target(s)?,
                Some(Either::B(list)) => EngineTargets::from_target_list(list)?,
                _ => default.target,
            },
            drop_console: o.drop_console.unwrap_or(default.drop_console),
            drop_debugger: o.drop_debugger.unwrap_or(default.drop_debugger),
            join_vars: o.join_vars.unwrap_or(true),
            sequences: o.sequences.unwrap_or(true),
            unused: match &o.unused {
                Some(Either::A(true)) => oxc_minifier::CompressOptionsUnused::Remove,
                Some(Either::A(false)) => oxc_minifier::CompressOptionsUnused::Keep,
                Some(Either::B(s)) => match s.as_str() {
                    "keep_assign" => oxc_minifier::CompressOptionsUnused::KeepAssign,
                    _ => return Err(format!("Invalid unused option: `{s}`.")),
                },
                None => default.unused,
            },
            keep_names: o.keep_names.as_ref().map(Into::into).unwrap_or_default(),
            treeshake: match &o.treeshake {
                Some(ts) => oxc_minifier::TreeShakeOptions::try_from(ts)?,
                None => oxc_minifier::TreeShakeOptions::default(),
            },
            drop_labels: o
                .drop_labels
                .as_ref()
                .map(|labels| labels.iter().cloned().collect())
                .unwrap_or_default(),
            max_iterations: o.max_iterations,
        })
    }
}

#[napi(object)]
pub struct CompressOptionsKeepNames {
    /// Keep function names so that `Function.prototype.name` is preserved.
    ///
    /// This does not guarantee that the `undefined` name is preserved.
    ///
    /// @default false
    pub function: bool,

    /// Keep class names so that `Class.prototype.name` is preserved.
    ///
    /// This does not guarantee that the `undefined` name is preserved.
    ///
    /// @default false
    pub class: bool,
}

impl From<&CompressOptionsKeepNames> for oxc_minifier::CompressOptionsKeepNames {
    fn from(o: &CompressOptionsKeepNames) -> Self {
        oxc_minifier::CompressOptionsKeepNames { function: o.function, class: o.class }
    }
}

#[napi(object)]
#[derive(Default)]
pub struct MangleOptions {
    /// Pass `true` to mangle names declared in the top level scope.
    ///
    /// @default true for modules and commonjs, otherwise false
    pub toplevel: Option<bool>,

    /// Preserve `name` property for functions and classes.
    ///
    /// @default false
    pub keep_names: Option<Either<bool, MangleOptionsKeepNames>>,

    /// Debug mangled names.
    pub debug: Option<bool>,
}

impl From<&MangleOptions> for oxc_minifier::MangleOptions {
    fn from(o: &MangleOptions) -> Self {
        let default = oxc_minifier::MangleOptions::default();
        Self {
            top_level: o.toplevel,
            keep_names: match &o.keep_names {
                Some(Either::A(false)) => oxc_minifier::MangleOptionsKeepNames::all_false(),
                Some(Either::A(true)) => oxc_minifier::MangleOptionsKeepNames::all_true(),
                Some(Either::B(o)) => oxc_minifier::MangleOptionsKeepNames::from(o),
                None => default.keep_names,
            },
            debug: o.debug.unwrap_or(default.debug),
        }
    }
}

#[napi(object)]
pub struct MangleOptionsKeepNames {
    /// Preserve `name` property for functions.
    ///
    /// @default false
    pub function: bool,

    /// Preserve `name` property for classes.
    ///
    /// @default false
    pub class: bool,
}

impl From<&MangleOptionsKeepNames> for oxc_minifier::MangleOptionsKeepNames {
    fn from(o: &MangleOptionsKeepNames) -> Self {
        oxc_minifier::MangleOptionsKeepNames { function: o.function, class: o.class }
    }
}

#[napi(string_enum = "lowercase")]
pub enum LegalCommentsMode {
    /// Do not preserve any legal comments.
    None,
    /// Preserve all legal comments inline.
    Inline,
    /// Move all legal comments to the end of the file.
    Eof,
    /// Extract legal comments without linking.
    External,
}

#[napi(object)]
pub struct LegalCommentsLinked {
    /// Extract legal comments and write them to the given path, with a link
    /// comment appended to the generated code.
    pub linked: String,
}

#[napi(object)]
pub struct CodegenOptions {
    /// Remove whitespace.
    ///
    /// @default true
    pub remove_whitespace: Option<bool>,

    /// How to handle legal comments (comments containing `@license`, `@preserve`, or starting with `//!`/`/*!`).
    ///
    /// * `"none"` - Do not preserve any legal comments.
    /// * `"inline"` - Preserve all legal comments inline.
    /// * `"eof"` - Move all legal comments to the end of the file.
    /// * `"external"` - Extract legal comments without linking.
    /// * `{ linked: "path/to/legal.txt" }` - Extract legal comments and add a link comment to the given path.
    ///
    /// @default "none" (when minifying)
    #[napi(ts_type = "'none' | 'inline' | 'eof' | 'external' | { linked: string }")]
    pub legal_comments: Option<Either<LegalCommentsMode, LegalCommentsLinked>>,
}

impl Default for CodegenOptions {
    fn default() -> Self {
        Self { remove_whitespace: Some(true), legal_comments: None }
    }
}

impl CodegenOptions {
    /// Convert N-API codegen options into codegen options.
    ///
    /// # Errors
    ///
    /// Returns an error if the `linked` variant is given an empty path.
    pub fn to_codegen_options(&self) -> Result<oxc_codegen::CodegenOptions, String> {
        let mut opts = if self.remove_whitespace.unwrap_or(true) {
            oxc_codegen::CodegenOptions::minify()
        } else {
            // Need to remove all comments.
            oxc_codegen::CodegenOptions { minify: false, ..oxc_codegen::CodegenOptions::minify() }
        };

        if let Some(legal) = &self.legal_comments {
            opts.comments.legal = match legal {
                Either::A(mode) => match mode {
                    LegalCommentsMode::None => oxc_codegen::LegalComment::None,
                    LegalCommentsMode::Inline => oxc_codegen::LegalComment::Inline,
                    LegalCommentsMode::Eof => oxc_codegen::LegalComment::Eof,
                    LegalCommentsMode::External => oxc_codegen::LegalComment::External,
                },
                Either::B(linked) => {
                    if linked.linked.is_empty() {
                        return Err("legalComments.linked must be a non-empty path.".into());
                    }
                    oxc_codegen::LegalComment::Linked(linked.linked.clone())
                }
            };
        }

        Ok(opts)
    }
}

#[napi(object)]
#[derive(Default)]
pub struct MinifyOptions {
    /// Use when minifying an ES module.
    pub module: Option<bool>,

    pub compress: Option<Either<bool, CompressOptions>>,

    pub mangle: Option<Either<bool, MangleOptions>>,

    pub codegen: Option<Either<bool, CodegenOptions>>,

    pub sourcemap: Option<bool>,
}

impl TryFrom<&MinifyOptions> for oxc_minifier::MinifierOptions {
    type Error = String;

    fn try_from(o: &MinifyOptions) -> Result<Self, Self::Error> {
        let compress = match &o.compress {
            Some(Either::A(false)) => None,
            None | Some(Either::A(true)) => Some(oxc_minifier::CompressOptions::default()),
            Some(Either::B(o)) => Some(oxc_minifier::CompressOptions::try_from(o)?),
        };
        let mangle = match &o.mangle {
            Some(Either::A(false)) => None,
            None | Some(Either::A(true)) => Some(oxc_minifier::MangleOptions::default()),
            Some(Either::B(o)) => Some(oxc_minifier::MangleOptions::from(o)),
        };
        Ok(oxc_minifier::MinifierOptions { compress, mangle })
    }
}