diff --git a/napi/minify/index.d.ts b/napi/minify/index.d.ts index 1d35248aab25f..c4f97dd2a3847 100644 --- a/napi/minify/index.d.ts +++ b/napi/minify/index.d.ts @@ -70,6 +70,8 @@ export interface CompressOptions { dropLabels?: Array /** Limit the maximum number of iterations for debugging purpose. */ maxIterations?: number + /** Treeshake options. */ + treeshake?: TreeShakeOptions } export interface CompressOptionsKeepNames { @@ -140,6 +142,40 @@ export interface MinifyResult { map?: SourceMap errors: Array } + +export interface 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 + */ + annotations?: boolean + /** + * Whether to treat this function call as pure. + * + * This function is called for normal function calls, new calls, and + * tagged template calls. + */ + manualPureFunctions?: Array + /** + * Whether property read accesses have side effects. + * + * @default 'always' + */ + propertyReadSideEffects?: boolean | 'always' + /** + * 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 + */ + unknownGlobalSideEffects?: boolean +} export interface Comment { type: 'Line' | 'Block' value: string diff --git a/napi/minify/src/options.rs b/napi/minify/src/options.rs index 27993e9fd4f4d..8cd3278c892c1 100644 --- a/napi/minify/src/options.rs +++ b/napi/minify/src/options.rs @@ -2,7 +2,66 @@ use napi::Either; use napi_derive::napi; use oxc_compat::EngineTargets; -use oxc_minifier::TreeShakeOptions; + +#[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, + + /// 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>, + + /// Whether property read accesses have side effects. + /// + /// @default 'always' + #[napi(ts_type = "boolean | 'always'")] + pub property_read_side_effects: Option>, + + /// 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, +} + +impl TryFrom<&TreeShakeOptions> for oxc_minifier::TreeShakeOptions { + type Error = String; + + fn try_from(o: &TreeShakeOptions) -> Result { + 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, + }, + unknown_global_side_effects: o + .unknown_global_side_effects + .unwrap_or(default.unknown_global_side_effects), + }) + } +} #[napi(object)] pub struct CompressOptions { @@ -61,6 +120,9 @@ pub struct CompressOptions { /// Limit the maximum number of iterations for debugging purpose. pub max_iterations: Option, + + /// Treeshake options. + pub treeshake: Option, } impl TryFrom<&CompressOptions> for oxc_minifier::CompressOptions { @@ -87,7 +149,10 @@ impl TryFrom<&CompressOptions> for oxc_minifier::CompressOptions { None => default.unused, }, keep_names: o.keep_names.as_ref().map(Into::into).unwrap_or_default(), - treeshake: TreeShakeOptions::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() diff --git a/napi/minify/test/minify.test.ts b/napi/minify/test/minify.test.ts index dd24b68f9f114..cd0422ccce92a 100644 --- a/napi/minify/test/minify.test.ts +++ b/napi/minify/test/minify.test.ts @@ -46,6 +46,83 @@ describe('simple', () => { }); }); +describe('treeshake options', () => { + it('respects annotations by default', () => { + const code = '/* @__PURE__ */ foo(); bar();'; + const ret = minify('test.js', code, { + compress: {}, + }); + // The @__PURE__ annotated call should be removed + expect(ret.code).toBe('bar();'); + expect(ret.errors.length).toBe(0); + }); + + it('can disable annotations', () => { + const code = '/* @__PURE__ */ foo(); bar();'; + const ret = minify('test.js', code, { + compress: { + treeshake: { + annotations: false, + }, + }, + }); + // With annotations disabled, @__PURE__ is not respected + expect(ret.code).toBe('foo(),bar();'); + expect(ret.errors.length).toBe(0); + }); + + it('supports manual pure functions', () => { + const code = 'foo(); bar(); baz();'; + const ret = minify('test.js', code, { + compress: { + treeshake: { + manualPureFunctions: ['foo', 'baz'], + }, + }, + }); + // foo and baz should be removed as they're marked as pure, bar should remain + expect(ret.code).toBe('bar();'); + expect(ret.errors.length).toBe(0); + }); + + it('supports propertyReadSideEffects as boolean', () => { + const code = 'const x = obj.prop; foo();'; + const ret = minify('test.js', code, { + compress: { + treeshake: { + propertyReadSideEffects: false, + }, + }, + }); + expect(ret.errors.length).toBe(0); + }); + + it('supports propertyReadSideEffects as "always"', () => { + const code = 'const x = obj.prop; foo();'; + const ret = minify('test.js', code, { + compress: { + treeshake: { + propertyReadSideEffects: 'always', + }, + }, + }); + expect(ret.errors.length).toBe(0); + }); + + it('rejects invalid propertyReadSideEffects string value', () => { + const code = 'const x = obj.prop; foo();'; + const ret = minify('test.js', code, { + compress: { + treeshake: { + propertyReadSideEffects: 'invalid' as any, + }, + }, + }); + expect(ret.errors.length).toBe(1); + expect(ret.errors[0].message).toContain('Invalid propertyReadSideEffects value'); + }); +}); + describe('worker', () => { it('should run', async () => { const code = await new Promise((resolve, reject) => {