diff --git a/crates/oxc_mangler/src/lib.rs b/crates/oxc_mangler/src/lib.rs index c93b79b1acb80..d43bea1b8cbf6 100644 --- a/crates/oxc_mangler/src/lib.rs +++ b/crates/oxc_mangler/src/lib.rs @@ -16,8 +16,11 @@ pub(crate) mod base54; #[derive(Default, Debug, Clone, Copy)] pub struct MangleOptions { - /// Also mangle exported variables. + /// Pass true to mangle names declared in the top level scope. + /// + /// Default: `false` pub top_level: bool, + /// Use more readable mangled names /// (e.g. `slot_0`, `slot_1`, `slot_2`, ...) for debugging. /// diff --git a/crates/oxc_minifier/README.md b/crates/oxc_minifier/README.md index d41ecadff097f..6fd8c63331269 100644 --- a/crates/oxc_minifier/README.md +++ b/crates/oxc_minifier/README.md @@ -2,9 +2,14 @@ A JavaScript minifier has three components: -1. printer +1. compressor 2. mangler -3. compressor +3. printer + +## Compressor + +The compressor is responsible for rewriting statements and expressions for minimal text output. +[Terser](https://github.com/terser/terser) is a good place to start for learning the fundamentals. ## Mangler @@ -13,10 +18,9 @@ It is responsible for shortening variables. Its algorithm should be gzip friendl The printer is also responsible for printing out the shortened variable names. -## Compressor +## Printer -The compressor is responsible for rewriting statements and expressions for minimal text output. -[Terser](https://github.com/terser/terser) is a good place to start for learning the fundamentals. +The printer is responsible for removing whitespace from the source text. ### Assumptions diff --git a/crates/oxc_minifier/src/options.rs b/crates/oxc_minifier/src/options.rs index ccf7d19a871b6..a08926455cf88 100644 --- a/crates/oxc_minifier/src/options.rs +++ b/crates/oxc_minifier/src/options.rs @@ -2,11 +2,14 @@ use oxc_syntax::es_target::ESTarget; #[derive(Debug, Clone, Copy)] pub struct CompressOptions { - /// Enable features that are targeted above. + /// Set desired EcmaScript standard version for output. /// /// e.g. /// /// * catch optional binding when >= es2019 + /// * `??` operator >= es2020 + /// + /// Default `ESTarget::ESNext` pub target: ESTarget, /// Remove `debugger;` statements. diff --git a/napi/minify/README.md b/napi/minify/README.md index d61f2071b08e8..57e74793c336a 100644 --- a/napi/minify/README.md +++ b/napi/minify/README.md @@ -1,3 +1,49 @@ # Oxc Minify This is alpha software and may yield incorrect results, feel free to [submit a bug report](https://github.com/oxc-project/oxc/issues/new?assignees=&labels=C-bug&projects=&template=bug_report.md). + +### Performance and Compression Size + +See [minification-benchmarks](https://github.com/privatenumber/minification-benchmarks) for details. + +The current version already outperforms `esbuild`, +but it still lacks a few key minification techniques +such as constant inlining and dead code removal, +which we plan to implement next. + +## Caveats + +To maximize performance, `oxc-minify` assumes the input code is semantically correct. +It uses `oxc-parser`'s fast mode to parse the input code, +which does not check for semantic errors related to symbols and scopes. + +## API + +```javascript +import { minify } from 'oxc-minify'; + +const filename = 'test.js'; +const code = "const x = 'a' + 'b'; console.log(x);"; +const options = { + compress: { + target: 'esnext', + }, + mangle: { + toplevel: false, + }, + codegen: { + removeWhitespace: true, + }, + sourcemap: true, +}; +const result = minify(filename, code, options); + +console.log(result.code); +console.log(result.map); +``` + +## Assumptions + +`oxc-minify` makes some assumptions about the source code. + +See https://github.com/oxc-project/oxc/blob/main/crates/oxc_minifier/README.md#assumptions for details. diff --git a/napi/minify/index.d.ts b/napi/minify/index.d.ts index a6cd19906cf86..16b90c18cb776 100644 --- a/napi/minify/index.d.ts +++ b/napi/minify/index.d.ts @@ -6,16 +6,23 @@ export interface CodegenOptions { * * @default true */ - whitespace?: boolean + removeWhitespace?: boolean } export interface CompressOptions { /** - * Enables optional catch or nullish-coalescing operator if targeted higher. + * Set desired EcmaScript standard version for output. * - * @default 'es2015' + * Set `esnext` to enable all target highering. + * + * e.g. + * + * * catch optional binding when >= es2019 + * * `??` operator >= es2020 + * + * @default 'esnext' */ - target?: string + target?: 'esnext' | 'es2015' | 'es2016' | 'es2017' | 'es2018' | 'es2019' | 'es2020' | 'es2021' | 'es2022' | 'es2023' | 'es2024' /** * Pass true to discard calls to `console.*`. * @@ -31,19 +38,17 @@ export interface CompressOptions { } export interface MangleOptions { - /** Pass true to mangle names declared in the top level scope. */ + /** + * Pass `true` to mangle names declared in the top level scope. + * + * @default false + */ toplevel?: boolean /** Debug mangled names. */ debug?: boolean } -/** - * Minify synchronously. - * - * # Errors - * - * * Fails to parse the options. - */ +/** Minify synchronously. */ export declare function minify(filename: string, sourceText: string, options?: MinifyOptions | undefined | null): MinifyResult export interface MinifyOptions { diff --git a/napi/minify/src/lib.rs b/napi/minify/src/lib.rs index dd86d016fe362..e2c78b4357208 100644 --- a/napi/minify/src/lib.rs +++ b/napi/minify/src/lib.rs @@ -1,4 +1,4 @@ -#![expect(clippy::needless_pass_by_value)] +#![expect(clippy::needless_pass_by_value, clippy::missing_errors_doc)] mod options; @@ -16,10 +16,6 @@ use oxc_span::SourceType; use crate::options::{MinifyOptions, MinifyResult}; /// Minify synchronously. -/// -/// # Errors -/// -/// * Fails to parse the options. #[napi] pub fn minify( filename: String, @@ -35,7 +31,7 @@ pub fn minify( let allocator = Allocator::default(); - let source_type = SourceType::from_path(&filename).unwrap_or_default().with_typescript(true); + let source_type = SourceType::from_path(&filename).unwrap_or_default(); let mut program = Parser::new(&allocator, &source_text, source_type).parse().program; diff --git a/napi/minify/src/options.rs b/napi/minify/src/options.rs index 918d670936231..e26b4b0c6965d 100644 --- a/napi/minify/src/options.rs +++ b/napi/minify/src/options.rs @@ -8,9 +8,19 @@ use oxc_syntax::es_target::ESTarget; #[napi(object)] pub struct CompressOptions { - /// Enables optional catch or nullish-coalescing operator if targeted higher. + /// Set desired EcmaScript standard version for output. /// - /// @default 'es2015' + /// Set `esnext` to enable all target highering. + /// + /// e.g. + /// + /// * catch optional binding when >= es2019 + /// * `??` operator >= es2020 + /// + /// @default 'esnext' + #[napi( + ts_type = "'esnext' | 'es2015' | 'es2016' | 'es2017' | 'es2018' | 'es2019' | 'es2020' | 'es2021' | 'es2022' | 'es2023' | 'es2024'" + )] pub target: Option, /// Pass true to discard calls to `console.*`. @@ -33,15 +43,16 @@ impl Default for CompressOptions { impl TryFrom<&CompressOptions> for oxc_minifier::CompressOptions { type Error = String; fn try_from(o: &CompressOptions) -> Result { + let default = oxc_minifier::CompressOptions::default(); Ok(oxc_minifier::CompressOptions { target: o .target .as_ref() .map(|s| ESTarget::from_str(s)) .transpose()? - .unwrap_or(ESTarget::ES2015), - drop_debugger: o.drop_debugger.unwrap_or(false), - drop_console: o.drop_console.unwrap_or(true), + .unwrap_or(default.target), + drop_console: o.drop_console.unwrap_or(default.drop_console), + drop_debugger: o.drop_debugger.unwrap_or(default.drop_debugger), }) } } @@ -49,7 +60,9 @@ impl TryFrom<&CompressOptions> for oxc_minifier::CompressOptions { #[napi(object)] #[derive(Default)] pub struct MangleOptions { - /// Pass true to mangle names declared in the top level scope. + /// Pass `true` to mangle names declared in the top level scope. + /// + /// @default false pub toplevel: Option, /// Debug mangled names. @@ -58,7 +71,11 @@ pub struct MangleOptions { impl From<&MangleOptions> for oxc_minifier::MangleOptions { fn from(o: &MangleOptions) -> Self { - Self { top_level: o.toplevel.unwrap_or(false), debug: o.debug.unwrap_or(false) } + let default = oxc_minifier::MangleOptions::default(); + Self { + top_level: o.toplevel.unwrap_or(default.top_level), + debug: o.debug.unwrap_or(default.debug), + } } } @@ -67,20 +84,21 @@ pub struct CodegenOptions { /// Remove whitespace. /// /// @default true - pub whitespace: Option, + pub remove_whitespace: Option, } impl Default for CodegenOptions { fn default() -> Self { - Self { whitespace: Some(true) } + Self { remove_whitespace: Some(true) } } } impl From<&CodegenOptions> for oxc_codegen::CodegenOptions { fn from(o: &CodegenOptions) -> Self { + let default = oxc_codegen::CodegenOptions::default(); oxc_codegen::CodegenOptions { - minify: o.whitespace.unwrap_or(true), - ..oxc_codegen::CodegenOptions::default() + minify: o.remove_whitespace.unwrap_or(default.minify), + ..default } } } diff --git a/napi/minify/test/minify.test.ts b/napi/minify/test/minify.test.ts index 56aaa4469274e..1aa7de9ed6be5 100644 --- a/napi/minify/test/minify.test.ts +++ b/napi/minify/test/minify.test.ts @@ -27,11 +27,17 @@ describe('simple', () => { }); it('can turn off everything', () => { - const ret = minify('test.js', code, { compress: false, mangle: false, codegen: { whitespace: false } }); + const ret = minify('test.js', code, { compress: false, mangle: false, codegen: { removeWhitespace: false } }); expect(ret).toStrictEqual({ 'code': 'function foo() {\n\tvar bar;\n\tbar(undefined);\n}\nfoo();\n', }); }); + + it('defaults to esnext', () => { + const code = 'try { foo } catch (e) {}'; + const ret = minify('test.js', code); + expect(ret.code).toBe('try{foo}catch{}'); + }); }); describe('worker', () => { diff --git a/napi/parser/README.md b/napi/parser/README.md index b7ad375f74a06..ebfe7df94a106 100644 --- a/napi/parser/README.md +++ b/napi/parser/README.md @@ -4,12 +4,15 @@ ### ESTree -The returned JavaScript AST follows the [ESTree](https://github.com/estree/estree) specification. +When parsing `.js` or `.jsx` files, the AST returned is fully conformant with the +[ESTree standard](https://github.com/estree/estree). It is fully aligned with Acorn's AST, and any deviation would be considered a bug. The returned TypeScript AST will conform to [@typescript-eslint/typescript-estree](https://www.npmjs.com/package/@typescript-eslint/typescript-estree) in the near future. +If you need all ASTs in the same with-TS-properties format, use the `astType: 'ts'` option. + ### AST Types [@oxc-project/types](https://www.npmjs.com/package/@oxc-project/types) can be used. For example: @@ -41,17 +44,6 @@ Fast mode is best suited for parser plugins, where other parts of your build pip Please note that turning off fast mode ​incurs​ a small performance overhead. -### ESTree compatibility - -When parsing JS or JSX files, the AST returned is fully conformant with the -[ESTree standard](https://github.com/estree/estree). - -When parsing TS or TSX files, the AST has additional properties related to TypeScript syntax. -These extra properties are broadly (but not entirely) in line with -[TypeScript ESLint](https://typescript-eslint.io/packages/parser/)'s AST. - -If you need all ASTs in the same with-TS-properties format, use the `astType: 'ts'` option. - ### Returns ESM information. It is likely that you are writing a parser plugin that requires ESM information.