Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions napi/minify/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ export interface CompressOptions {
dropLabels?: Array<string>
/** Limit the maximum number of iterations for debugging purpose. */
maxIterations?: number
/** Treeshake options. */
treeshake?: TreeShakeOptions
}

export interface CompressOptionsKeepNames {
Expand Down Expand Up @@ -140,6 +142,40 @@ export interface MinifyResult {
map?: SourceMap
errors: Array<OxcError>
}

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<string>
/**
* 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
Expand Down
69 changes: 67 additions & 2 deletions napi/minify/src/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<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 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>,
}

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,
},
unknown_global_side_effects: o
.unknown_global_side_effects
.unwrap_or(default.unknown_global_side_effects),
})
}
}

#[napi(object)]
pub struct CompressOptions {
Expand Down Expand Up @@ -61,6 +120,9 @@ pub struct CompressOptions {

/// 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 {
Expand All @@ -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()
Expand Down
77 changes: 77 additions & 0 deletions napi/minify/test/minify.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
Loading