Skip to main content

oxc_minify_napi/
options.rs

1use napi::Either;
2use napi_derive::napi;
3
4use oxc_compat::EngineTargets;
5
6#[napi(object)]
7pub struct TreeShakeOptions {
8    /// Whether to respect the pure annotations.
9    ///
10    /// Pure annotations are comments that mark an expression as pure.
11    /// For example: @__PURE__ or #__NO_SIDE_EFFECTS__.
12    ///
13    /// @default true
14    pub annotations: Option<bool>,
15
16    /// Whether to treat this function call as pure.
17    ///
18    /// This function is called for normal function calls, new calls, and
19    /// tagged template calls.
20    pub manual_pure_functions: Option<Vec<String>>,
21
22    /// Whether property read accesses have side effects.
23    ///
24    /// @default 'always'
25    #[napi(ts_type = "boolean | 'always'")]
26    pub property_read_side_effects: Option<Either<bool, String>>,
27
28    /// Whether property write accesses (assignments to member expressions) have side effects.
29    ///
30    /// When false, assignments like `obj.prop = value` are considered side-effect-free
31    /// (assuming the object and value expressions themselves are side-effect-free).
32    ///
33    /// @default true
34    pub property_write_side_effects: Option<bool>,
35
36    /// Whether accessing a global variable has side effects.
37    ///
38    /// Accessing a non-existing global variable will throw an error.
39    /// Global variable may be a getter that has side effects.
40    ///
41    /// @default true
42    pub unknown_global_side_effects: Option<bool>,
43
44    /// Whether invalid import statements have side effects.
45    ///
46    /// Accessing a non-existing import name will throw an error.
47    /// Also import statements that cannot be resolved will throw an error.
48    ///
49    /// @default true
50    pub invalid_import_side_effects: Option<bool>,
51}
52
53impl TryFrom<&TreeShakeOptions> for oxc_minifier::TreeShakeOptions {
54    type Error = String;
55
56    fn try_from(o: &TreeShakeOptions) -> Result<Self, Self::Error> {
57        let default = oxc_minifier::TreeShakeOptions::default();
58        Ok(oxc_minifier::TreeShakeOptions {
59            annotations: o.annotations.unwrap_or(default.annotations),
60            manual_pure_functions: o
61                .manual_pure_functions
62                .clone()
63                .unwrap_or(default.manual_pure_functions),
64            property_read_side_effects: match &o.property_read_side_effects {
65                Some(Either::A(false)) => oxc_minifier::PropertyReadSideEffects::None,
66                Some(Either::A(true)) => oxc_minifier::PropertyReadSideEffects::All,
67                Some(Either::B(s)) if s == "always" => oxc_minifier::PropertyReadSideEffects::All,
68                Some(Either::B(s)) => {
69                    return Err(format!(
70                        "Invalid propertyReadSideEffects value: '{s}'. Expected 'always'."
71                    ));
72                }
73                None => default.property_read_side_effects,
74            },
75            property_write_side_effects: o
76                .property_write_side_effects
77                .unwrap_or(default.property_write_side_effects),
78            unknown_global_side_effects: o
79                .unknown_global_side_effects
80                .unwrap_or(default.unknown_global_side_effects),
81            invalid_import_side_effects: o
82                .invalid_import_side_effects
83                .unwrap_or(default.invalid_import_side_effects),
84        })
85    }
86}
87
88#[napi(object)]
89pub struct CompressOptions {
90    /// Set desired EcmaScript standard version for output.
91    ///
92    /// Set `esnext` to enable all target highering.
93    ///
94    /// Example:
95    ///
96    /// * `'es2015'`
97    /// * `['es2020', 'chrome58', 'edge16', 'firefox57', 'node12', 'safari11']`
98    ///
99    /// @default 'esnext'
100    ///
101    /// @see [oxc#target](https://oxc.rs/docs/guide/usage/transformer/lowering#target)
102    pub target: Option<Either<String, Vec<String>>>,
103
104    /// Pass true to discard calls to `console.*`.
105    ///
106    /// @default false
107    pub drop_console: Option<bool>,
108
109    /// Remove `debugger;` statements.
110    ///
111    /// @default true
112    pub drop_debugger: Option<bool>,
113
114    /// Pass `true` to drop unreferenced functions and variables.
115    ///
116    /// Simple direct variable assignments do not count as references unless set to `keep_assign`.
117    /// @default true
118    #[napi(ts_type = "boolean | 'keep_assign'")]
119    pub unused: Option<Either<bool, String>>,
120
121    /// Keep function / class names.
122    pub keep_names: Option<CompressOptionsKeepNames>,
123
124    /// Join consecutive var, let and const statements.
125    ///
126    /// @default true
127    pub join_vars: Option<bool>,
128
129    /// Join consecutive simple statements using the comma operator.
130    ///
131    /// `a; b` -> `a, b`
132    ///
133    /// @default true
134    pub sequences: Option<bool>,
135
136    /// Set of label names to drop from the code.
137    ///
138    /// Labeled statements matching these names will be removed during minification.
139    ///
140    /// @default []
141    pub drop_labels: Option<Vec<String>>,
142
143    /// Limit the maximum number of iterations for debugging purpose.
144    pub max_iterations: Option<u8>,
145
146    /// Treeshake options.
147    pub treeshake: Option<TreeShakeOptions>,
148}
149
150impl TryFrom<&CompressOptions> for oxc_minifier::CompressOptions {
151    type Error = String;
152    fn try_from(o: &CompressOptions) -> Result<Self, Self::Error> {
153        let default = oxc_minifier::CompressOptions::default();
154        Ok(oxc_minifier::CompressOptions {
155            target: match &o.target {
156                Some(Either::A(s)) => EngineTargets::from_target(s)?,
157                Some(Either::B(list)) => EngineTargets::from_target_list(list)?,
158                _ => default.target,
159            },
160            drop_console: o.drop_console.unwrap_or(default.drop_console),
161            drop_debugger: o.drop_debugger.unwrap_or(default.drop_debugger),
162            join_vars: o.join_vars.unwrap_or(true),
163            sequences: o.sequences.unwrap_or(true),
164            unused: match &o.unused {
165                Some(Either::A(true)) => oxc_minifier::CompressOptionsUnused::Remove,
166                Some(Either::A(false)) => oxc_minifier::CompressOptionsUnused::Keep,
167                Some(Either::B(s)) => match s.as_str() {
168                    "keep_assign" => oxc_minifier::CompressOptionsUnused::KeepAssign,
169                    _ => return Err(format!("Invalid unused option: `{s}`.")),
170                },
171                None => default.unused,
172            },
173            keep_names: o.keep_names.as_ref().map(Into::into).unwrap_or_default(),
174            treeshake: match &o.treeshake {
175                Some(ts) => oxc_minifier::TreeShakeOptions::try_from(ts)?,
176                None => oxc_minifier::TreeShakeOptions::default(),
177            },
178            drop_labels: o
179                .drop_labels
180                .as_ref()
181                .map(|labels| labels.iter().cloned().collect())
182                .unwrap_or_default(),
183            max_iterations: o.max_iterations,
184        })
185    }
186}
187
188#[napi(object)]
189pub struct CompressOptionsKeepNames {
190    /// Keep function names so that `Function.prototype.name` is preserved.
191    ///
192    /// This does not guarantee that the `undefined` name is preserved.
193    ///
194    /// @default false
195    pub function: bool,
196
197    /// Keep class names so that `Class.prototype.name` is preserved.
198    ///
199    /// This does not guarantee that the `undefined` name is preserved.
200    ///
201    /// @default false
202    pub class: bool,
203}
204
205impl From<&CompressOptionsKeepNames> for oxc_minifier::CompressOptionsKeepNames {
206    fn from(o: &CompressOptionsKeepNames) -> Self {
207        oxc_minifier::CompressOptionsKeepNames { function: o.function, class: o.class }
208    }
209}
210
211#[napi(object)]
212#[derive(Default)]
213pub struct MangleOptions {
214    /// Pass `true` to mangle names declared in the top level scope.
215    ///
216    /// @default true for modules and commonjs, otherwise false
217    pub toplevel: Option<bool>,
218
219    /// Preserve `name` property for functions and classes.
220    ///
221    /// @default false
222    pub keep_names: Option<Either<bool, MangleOptionsKeepNames>>,
223
224    /// Debug mangled names.
225    pub debug: Option<bool>,
226}
227
228impl From<&MangleOptions> for oxc_minifier::MangleOptions {
229    fn from(o: &MangleOptions) -> Self {
230        let default = oxc_minifier::MangleOptions::default();
231        Self {
232            top_level: o.toplevel,
233            keep_names: match &o.keep_names {
234                Some(Either::A(false)) => oxc_minifier::MangleOptionsKeepNames::all_false(),
235                Some(Either::A(true)) => oxc_minifier::MangleOptionsKeepNames::all_true(),
236                Some(Either::B(o)) => oxc_minifier::MangleOptionsKeepNames::from(o),
237                None => default.keep_names,
238            },
239            debug: o.debug.unwrap_or(default.debug),
240        }
241    }
242}
243
244#[napi(object)]
245pub struct MangleOptionsKeepNames {
246    /// Preserve `name` property for functions.
247    ///
248    /// @default false
249    pub function: bool,
250
251    /// Preserve `name` property for classes.
252    ///
253    /// @default false
254    pub class: bool,
255}
256
257impl From<&MangleOptionsKeepNames> for oxc_minifier::MangleOptionsKeepNames {
258    fn from(o: &MangleOptionsKeepNames) -> Self {
259        oxc_minifier::MangleOptionsKeepNames { function: o.function, class: o.class }
260    }
261}
262
263#[napi(string_enum = "lowercase")]
264pub enum LegalCommentsMode {
265    /// Do not preserve any legal comments.
266    None,
267    /// Preserve all legal comments inline.
268    Inline,
269    /// Move all legal comments to the end of the file.
270    Eof,
271    /// Extract legal comments without linking.
272    External,
273}
274
275#[napi(object)]
276pub struct LegalCommentsLinked {
277    /// Extract legal comments and write them to the given path, with a link
278    /// comment appended to the generated code.
279    pub linked: String,
280}
281
282#[napi(object)]
283pub struct CodegenOptions {
284    /// Remove whitespace.
285    ///
286    /// @default true
287    pub remove_whitespace: Option<bool>,
288
289    /// How to handle legal comments (comments containing `@license`, `@preserve`, or starting with `//!`/`/*!`).
290    ///
291    /// * `"none"` - Do not preserve any legal comments.
292    /// * `"inline"` - Preserve all legal comments inline.
293    /// * `"eof"` - Move all legal comments to the end of the file.
294    /// * `"external"` - Extract legal comments without linking.
295    /// * `{ linked: "path/to/legal.txt" }` - Extract legal comments and add a link comment to the given path.
296    ///
297    /// @default "none" (when minifying)
298    #[napi(ts_type = "'none' | 'inline' | 'eof' | 'external' | { linked: string }")]
299    pub legal_comments: Option<Either<LegalCommentsMode, LegalCommentsLinked>>,
300}
301
302impl Default for CodegenOptions {
303    fn default() -> Self {
304        Self { remove_whitespace: Some(true), legal_comments: None }
305    }
306}
307
308impl CodegenOptions {
309    /// Convert N-API codegen options into codegen options.
310    ///
311    /// # Errors
312    ///
313    /// Returns an error if the `linked` variant is given an empty path.
314    pub fn to_codegen_options(&self) -> Result<oxc_codegen::CodegenOptions, String> {
315        let mut opts = if self.remove_whitespace.unwrap_or(true) {
316            oxc_codegen::CodegenOptions::minify()
317        } else {
318            // Need to remove all comments.
319            oxc_codegen::CodegenOptions { minify: false, ..oxc_codegen::CodegenOptions::minify() }
320        };
321
322        if let Some(legal) = &self.legal_comments {
323            opts.comments.legal = match legal {
324                Either::A(mode) => match mode {
325                    LegalCommentsMode::None => oxc_codegen::LegalComment::None,
326                    LegalCommentsMode::Inline => oxc_codegen::LegalComment::Inline,
327                    LegalCommentsMode::Eof => oxc_codegen::LegalComment::Eof,
328                    LegalCommentsMode::External => oxc_codegen::LegalComment::External,
329                },
330                Either::B(linked) => {
331                    if linked.linked.is_empty() {
332                        return Err("legalComments.linked must be a non-empty path.".into());
333                    }
334                    oxc_codegen::LegalComment::Linked(linked.linked.clone())
335                }
336            };
337        }
338
339        Ok(opts)
340    }
341}
342
343#[napi(object)]
344#[derive(Default)]
345pub struct MinifyOptions {
346    /// Use when minifying an ES module.
347    pub module: Option<bool>,
348
349    pub compress: Option<Either<bool, CompressOptions>>,
350
351    pub mangle: Option<Either<bool, MangleOptions>>,
352
353    pub codegen: Option<Either<bool, CodegenOptions>>,
354
355    pub sourcemap: Option<bool>,
356}
357
358impl TryFrom<&MinifyOptions> for oxc_minifier::MinifierOptions {
359    type Error = String;
360
361    fn try_from(o: &MinifyOptions) -> Result<Self, Self::Error> {
362        let compress = match &o.compress {
363            Some(Either::A(false)) => None,
364            None | Some(Either::A(true)) => Some(oxc_minifier::CompressOptions::default()),
365            Some(Either::B(o)) => Some(oxc_minifier::CompressOptions::try_from(o)?),
366        };
367        let mangle = match &o.mangle {
368            Some(Either::A(false)) => None,
369            None | Some(Either::A(true)) => Some(oxc_minifier::MangleOptions::default()),
370            Some(Either::B(o)) => Some(oxc_minifier::MangleOptions::from(o)),
371        };
372        Ok(oxc_minifier::MinifierOptions { compress, mangle })
373    }
374}