feat(napi/minify): implement napi (#8478)

This commit is contained in:
Boshen 2025-01-14 08:55:55 +00:00
parent 9d550aacaf
commit 4ad695dcfb
11 changed files with 290 additions and 41 deletions

2
Cargo.lock generated
View file

@ -1870,7 +1870,9 @@ dependencies = [
"oxc_codegen",
"oxc_minifier",
"oxc_parser",
"oxc_sourcemap",
"oxc_span",
"oxc_syntax",
]
[[package]]

View file

@ -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()

View file

@ -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<MangleOptions>,
pub compress: CompressOptions,
pub compress: Option<CompressOptions>,
}
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();

View file

@ -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 {

View file

@ -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 }

View file

@ -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<string>
sourceRoot?: string
sources: Array<string>
sourcesContent?: Array<string>
version: number
x_google_ignoreList?: Array<number>
}

View file

@ -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",

View file

@ -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<MinifyOptions>,
) -> napi::Result<MinifyResult> {
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) })
}

123
napi/minify/src/options.rs Normal file
View file

@ -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<String>,
/// Pass true to discard calls to `console.*`.
///
/// @default false
pub drop_console: Option<bool>,
/// Remove `debugger;` statements.
///
/// @default true
pub drop_debugger: Option<bool>,
}
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<Self, Self::Error> {
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<bool>,
/// Debug mangled names.
pub debug: Option<bool>,
}
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<bool>,
}
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<Either<bool, CompressOptions>>,
pub mangle: Option<Either<bool, MangleOptions>>,
pub codegen: Option<Either<bool, CodegenOptions>>,
pub sourcemap: Option<bool>,
}
impl TryFrom<&MinifyOptions> for oxc_minifier::MinifierOptions {
type Error = String;
fn try_from(o: &MinifyOptions) -> Result<Self, Self::Error> {
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<SourceMap>,
}

View file

@ -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',
});
});
});

View file

@ -205,17 +205,6 @@ export declare const enum Severity {
Advice = 'Advice'
}
export interface SourceMap {
file?: string
mappings: string
names: Array<string>
sourceRoot?: string
sources: Array<string>
sourcesContent?: Array<string>
version: number
x_google_ignoreList?: Array<number>
}
/**
* Transpile a JavaScript or TypeScript into a target ECMAScript version.
*