From 4ad695dcfbe6dfa281789abb92bcbb4244c1e32f Mon Sep 17 00:00:00 2001 From: Boshen <1430279+Boshen@users.noreply.github.com> Date: Tue, 14 Jan 2025 08:55:55 +0000 Subject: [PATCH] feat(napi/minify): implement napi (#8478) --- Cargo.lock | 2 + crates/oxc_minifier/examples/minifier.rs | 2 +- crates/oxc_minifier/src/lib.rs | 21 ++-- crates/oxc_wasm/src/lib.rs | 4 +- napi/minify/Cargo.toml | 2 + napi/minify/index.d.ts | 69 ++++++++++++- napi/minify/package.json | 5 +- napi/minify/src/lib.rs | 58 ++++++++--- napi/minify/src/options.rs | 123 +++++++++++++++++++++++ napi/minify/test/minify.test.ts | 34 +++++++ napi/transform/index.d.ts | 11 -- 11 files changed, 290 insertions(+), 41 deletions(-) create mode 100644 napi/minify/src/options.rs create mode 100644 napi/minify/test/minify.test.ts diff --git a/Cargo.lock b/Cargo.lock index a4ead9743..7d26d97d0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1870,7 +1870,9 @@ dependencies = [ "oxc_codegen", "oxc_minifier", "oxc_parser", + "oxc_sourcemap", "oxc_span", + "oxc_syntax", ] [[package]] diff --git a/crates/oxc_minifier/examples/minifier.rs b/crates/oxc_minifier/examples/minifier.rs index ab8ad88eb..4d02d16c9 100644 --- a/crates/oxc_minifier/examples/minifier.rs +++ b/crates/oxc_minifier/examples/minifier.rs @@ -50,7 +50,7 @@ fn minify( let mut program = ret.program; let options = MinifierOptions { mangle: mangle.then(MangleOptions::default), - compress: CompressOptions::default(), + compress: Some(CompressOptions::default()), }; let ret = Minifier::new(options).build(allocator, &mut program); CodeGenerator::new() diff --git a/crates/oxc_minifier/src/lib.rs b/crates/oxc_minifier/src/lib.rs index 479a6e513..f94309e22 100644 --- a/crates/oxc_minifier/src/lib.rs +++ b/crates/oxc_minifier/src/lib.rs @@ -12,7 +12,7 @@ mod tester; use oxc_allocator::Allocator; use oxc_ast::ast::Program; use oxc_mangler::Mangler; -use oxc_semantic::SemanticBuilder; +use oxc_semantic::{SemanticBuilder, Stats}; pub use oxc_mangler::MangleOptions; @@ -21,12 +21,12 @@ pub use crate::{ast_passes::CompressorPass, compressor::Compressor, options::Com #[derive(Debug, Clone, Copy)] pub struct MinifierOptions { pub mangle: Option, - pub compress: CompressOptions, + pub compress: Option, } impl Default for MinifierOptions { fn default() -> Self { - Self { mangle: Some(MangleOptions::default()), compress: CompressOptions::default() } + Self { mangle: Some(MangleOptions::default()), compress: Some(CompressOptions::default()) } } } @@ -44,11 +44,16 @@ impl Minifier { } pub fn build<'a>(self, allocator: &'a Allocator, program: &mut Program<'a>) -> MinifierReturn { - let semantic = SemanticBuilder::new().build(program).semantic; - let stats = semantic.stats(); - let (symbols, scopes) = semantic.into_symbol_table_and_scope_tree(); - Compressor::new(allocator, self.options.compress) - .build_with_symbols_and_scopes(symbols, scopes, program); + let stats = if let Some(compress) = self.options.compress { + let semantic = SemanticBuilder::new().build(program).semantic; + let stats = semantic.stats(); + let (symbols, scopes) = semantic.into_symbol_table_and_scope_tree(); + Compressor::new(allocator, compress) + .build_with_symbols_and_scopes(symbols, scopes, program); + stats + } else { + Stats::default() + }; let mangler = self.options.mangle.map(|options| { let semantic = SemanticBuilder::new().with_stats(stats).build(program).semantic; let (symbols, scopes) = semantic.into_symbol_table_and_scope_tree(); diff --git a/crates/oxc_wasm/src/lib.rs b/crates/oxc_wasm/src/lib.rs index 0eb2ef59d..89db47e73 100644 --- a/crates/oxc_wasm/src/lib.rs +++ b/crates/oxc_wasm/src/lib.rs @@ -271,7 +271,7 @@ impl Oxc { let compress_options = minifier_options.compress_options.unwrap_or_default(); let options = MinifierOptions { mangle: minifier_options.mangle.unwrap_or_default().then(MangleOptions::default), - compress: if minifier_options.compress.unwrap_or_default() { + compress: Some(if minifier_options.compress.unwrap_or_default() { CompressOptions { drop_console: compress_options.drop_console, drop_debugger: compress_options.drop_debugger, @@ -279,7 +279,7 @@ impl Oxc { } } else { CompressOptions::all_false() - }, + }), }; Minifier::new(options).build(&allocator, &mut program).mangler } else { diff --git a/napi/minify/Cargo.toml b/napi/minify/Cargo.toml index 5833d97b7..497b4e631 100644 --- a/napi/minify/Cargo.toml +++ b/napi/minify/Cargo.toml @@ -25,7 +25,9 @@ oxc_allocator = { workspace = true } oxc_codegen = { workspace = true } oxc_minifier = { workspace = true } oxc_parser = { workspace = true } +oxc_sourcemap = { workspace = true, features = ["napi", "rayon"] } oxc_span = { workspace = true } +oxc_syntax = { workspace = true } napi = { workspace = true } napi-derive = { workspace = true } diff --git a/napi/minify/index.d.ts b/napi/minify/index.d.ts index e69391abf..3360b5c0e 100644 --- a/napi/minify/index.d.ts +++ b/napi/minify/index.d.ts @@ -1,4 +1,71 @@ /* auto-generated by NAPI-RS */ /* eslint-disable */ -export declare function minify(filename: string, sourceText: string): string +export interface CodegenOptions { + /** + * Remove whitespace. + * + * @default true + */ + whitespace?: boolean +} + +export interface CompressOptions { + /** + * Enables optional catch or nullish-coalescing operator if targeted higher. + * + * @default 'es2015' + */ + target?: string + /** + * Pass true to discard calls to `console.*`. + * + * @default false + */ + dropConsole?: boolean + /** + * Remove `debugger;` statements. + * + * @default true + */ + dropDebugger?: boolean +} + +export interface MangleOptions { + /** Pass true to mangle names declared in the top level scope. */ + toplevel?: boolean + /** Debug mangled names. */ + debug?: boolean +} + +/** + * Minify synchronously. + * + * # Errors + * + * * Fails to parse the options. + */ +export declare function minify(filename: string, sourceText: string, options?: MinifyOptions | undefined | null): MinifyResult + +export interface MinifyOptions { + compress?: boolean | CompressOptions + mangle?: boolean | MangleOptions + codegen?: boolean | CodegenOptions + sourcemap?: boolean +} + +export interface MinifyResult { + code: string + map?: SourceMap +} + +export interface SourceMap { + file?: string + mappings: string + names: Array + sourceRoot?: string + sources: Array + sourcesContent?: Array + version: number + x_google_ignoreList?: Array +} diff --git a/napi/minify/package.json b/napi/minify/package.json index 3693e036d..ddaf3d3f3 100644 --- a/napi/minify/package.json +++ b/napi/minify/package.json @@ -4,10 +4,7 @@ "scripts": { "build-dev": "napi build --platform", "build": "napi build --platform --release", - "test": "echo 'skip'" - }, - "engines": { - "node": ">=14.*" + "test": "vitest --typecheck run ./test" }, "napi": { "binaryName": "minify", diff --git a/napi/minify/src/lib.rs b/napi/minify/src/lib.rs index 6ddeecfa9..ae9c6acbf 100644 --- a/napi/minify/src/lib.rs +++ b/napi/minify/src/lib.rs @@ -1,29 +1,59 @@ +#![allow(clippy::needless_pass_by_value)] + +mod options; + +use std::path::PathBuf; + +use napi::Either; use napi_derive::napi; use oxc_allocator::Allocator; use oxc_codegen::{Codegen, CodegenOptions}; -use oxc_minifier::{CompressOptions, MangleOptions, Minifier, MinifierOptions}; +use oxc_minifier::Minifier; use oxc_parser::Parser; use oxc_span::SourceType; -#[allow(clippy::needless_pass_by_value)] +use crate::options::{MinifyOptions, MinifyResult}; + +/// Minify synchronously. +/// +/// # Errors +/// +/// * Fails to parse the options. #[napi] -pub fn minify(filename: String, source_text: String) -> String { +pub fn minify( + filename: String, + source_text: String, + options: Option, +) -> napi::Result { + let options = options.unwrap_or_default(); + + let minifier_options = match oxc_minifier::MinifierOptions::try_from(&options) { + Ok(options) => options, + Err(error) => return Err(napi::Error::from_reason(&error)), + }; + let allocator = Allocator::default(); + let source_type = SourceType::from_path(&filename).unwrap_or_default().with_typescript(true); let mut program = Parser::new(&allocator, &source_text, source_type).parse().program; - let mangler = Minifier::new(MinifierOptions { - mangle: Some(MangleOptions::default()), - compress: CompressOptions::default(), - }) - .build(&allocator, &mut program) - .mangler; + let mangler = Minifier::new(minifier_options).build(&allocator, &mut program).mangler; - Codegen::new() - .with_options(CodegenOptions { minify: true, ..CodegenOptions::default() }) - .with_mangler(mangler) - .build(&program) - .code + let mut codegen_options = match &options.codegen { + Some(Either::A(false)) => CodegenOptions { minify: false, ..CodegenOptions::default() }, + None | Some(Either::A(true)) => { + CodegenOptions { minify: true, ..CodegenOptions::default() } + } + Some(Either::B(o)) => CodegenOptions::from(o), + }; + + if options.sourcemap == Some(true) { + codegen_options.source_map_path = Some(PathBuf::from(filename)); + } + + let ret = Codegen::new().with_options(codegen_options).with_mangler(mangler).build(&program); + + Ok(MinifyResult { code: ret.code, map: ret.map.map(oxc_sourcemap::napi::SourceMap::from) }) } diff --git a/napi/minify/src/options.rs b/napi/minify/src/options.rs new file mode 100644 index 000000000..918d67093 --- /dev/null +++ b/napi/minify/src/options.rs @@ -0,0 +1,123 @@ +use std::str::FromStr; + +use napi::Either; +use napi_derive::napi; + +use oxc_sourcemap::napi::SourceMap; +use oxc_syntax::es_target::ESTarget; + +#[napi(object)] +pub struct CompressOptions { + /// Enables optional catch or nullish-coalescing operator if targeted higher. + /// + /// @default 'es2015' + pub target: Option, + + /// Pass true to discard calls to `console.*`. + /// + /// @default false + pub drop_console: Option, + + /// Remove `debugger;` statements. + /// + /// @default true + pub drop_debugger: Option, +} + +impl Default for CompressOptions { + fn default() -> Self { + Self { target: None, drop_console: None, drop_debugger: Some(true) } + } +} + +impl TryFrom<&CompressOptions> for oxc_minifier::CompressOptions { + type Error = String; + fn try_from(o: &CompressOptions) -> Result { + 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), + }) + } +} + +#[napi(object)] +#[derive(Default)] +pub struct MangleOptions { + /// Pass true to mangle names declared in the top level scope. + pub toplevel: Option, + + /// Debug mangled names. + pub debug: Option, +} + +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) } + } +} + +#[napi(object)] +pub struct CodegenOptions { + /// Remove whitespace. + /// + /// @default true + pub whitespace: Option, +} + +impl Default for CodegenOptions { + fn default() -> Self { + Self { whitespace: Some(true) } + } +} + +impl From<&CodegenOptions> for oxc_codegen::CodegenOptions { + fn from(o: &CodegenOptions) -> Self { + oxc_codegen::CodegenOptions { + minify: o.whitespace.unwrap_or(true), + ..oxc_codegen::CodegenOptions::default() + } + } +} + +#[napi(object)] +#[derive(Default)] +pub struct MinifyOptions { + pub compress: Option>, + + pub mangle: Option>, + + pub codegen: Option>, + + pub sourcemap: Option, +} + +impl TryFrom<&MinifyOptions> for oxc_minifier::MinifierOptions { + type Error = String; + + fn try_from(o: &MinifyOptions) -> Result { + 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 }) + } +} + +#[napi(object)] +pub struct MinifyResult { + pub code: String, + + pub map: Option, +} diff --git a/napi/minify/test/minify.test.ts b/napi/minify/test/minify.test.ts new file mode 100644 index 000000000..a9a88a1cb --- /dev/null +++ b/napi/minify/test/minify.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from 'vitest'; + +import { minify } from '../index'; + +describe('simple', () => { + const code = 'function foo() { var bar; bar(undefined) } foo();'; + + it('matches output', () => { + const ret = minify('test.js', code, { sourcemap: true }); + expect(ret).toStrictEqual({ + 'code': 'function foo(){var b;b(void 0)}foo();', + 'map': { + 'mappings': 'AAAA,SAAS,KAAM,CAAE,IAAIA,EAAK,SAAc,AAAE,CAAC,KAAK', + 'names': [ + 'bar', + ], + 'sources': [ + 'test.js', + ], + 'sourcesContent': [ + code, + ], + 'version': 3, + }, + }); + }); + + it('can turn off everything', () => { + const ret = minify('test.js', code, { compress: false, mangle: false, codegen: { whitespace: false } }); + expect(ret).toStrictEqual({ + 'code': 'function foo() {\n\tvar bar;\n\tbar(undefined);\n}\nfoo();\n', + }); + }); +}); diff --git a/napi/transform/index.d.ts b/napi/transform/index.d.ts index 492ba4810..85c0f7f32 100644 --- a/napi/transform/index.d.ts +++ b/napi/transform/index.d.ts @@ -205,17 +205,6 @@ export declare const enum Severity { Advice = 'Advice' } -export interface SourceMap { - file?: string - mappings: string - names: Array - sourceRoot?: string - sources: Array - sourcesContent?: Array - version: number - x_google_ignoreList?: Array -} - /** * Transpile a JavaScript or TypeScript into a target ECMAScript version. *