mirror of
https://github.com/danbulant/oxc
synced 2026-05-24 12:21:58 +00:00
feat(napi/minify): implement napi (#8478)
This commit is contained in:
parent
9d550aacaf
commit
4ad695dcfb
11 changed files with 290 additions and 41 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -1870,7 +1870,9 @@ dependencies = [
|
||||||
"oxc_codegen",
|
"oxc_codegen",
|
||||||
"oxc_minifier",
|
"oxc_minifier",
|
||||||
"oxc_parser",
|
"oxc_parser",
|
||||||
|
"oxc_sourcemap",
|
||||||
"oxc_span",
|
"oxc_span",
|
||||||
|
"oxc_syntax",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ fn minify(
|
||||||
let mut program = ret.program;
|
let mut program = ret.program;
|
||||||
let options = MinifierOptions {
|
let options = MinifierOptions {
|
||||||
mangle: mangle.then(MangleOptions::default),
|
mangle: mangle.then(MangleOptions::default),
|
||||||
compress: CompressOptions::default(),
|
compress: Some(CompressOptions::default()),
|
||||||
};
|
};
|
||||||
let ret = Minifier::new(options).build(allocator, &mut program);
|
let ret = Minifier::new(options).build(allocator, &mut program);
|
||||||
CodeGenerator::new()
|
CodeGenerator::new()
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ mod tester;
|
||||||
use oxc_allocator::Allocator;
|
use oxc_allocator::Allocator;
|
||||||
use oxc_ast::ast::Program;
|
use oxc_ast::ast::Program;
|
||||||
use oxc_mangler::Mangler;
|
use oxc_mangler::Mangler;
|
||||||
use oxc_semantic::SemanticBuilder;
|
use oxc_semantic::{SemanticBuilder, Stats};
|
||||||
|
|
||||||
pub use oxc_mangler::MangleOptions;
|
pub use oxc_mangler::MangleOptions;
|
||||||
|
|
||||||
|
|
@ -21,12 +21,12 @@ pub use crate::{ast_passes::CompressorPass, compressor::Compressor, options::Com
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub struct MinifierOptions {
|
pub struct MinifierOptions {
|
||||||
pub mangle: Option<MangleOptions>,
|
pub mangle: Option<MangleOptions>,
|
||||||
pub compress: CompressOptions,
|
pub compress: Option<CompressOptions>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for MinifierOptions {
|
impl Default for MinifierOptions {
|
||||||
fn default() -> Self {
|
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 {
|
pub fn build<'a>(self, allocator: &'a Allocator, program: &mut Program<'a>) -> MinifierReturn {
|
||||||
|
let stats = if let Some(compress) = self.options.compress {
|
||||||
let semantic = SemanticBuilder::new().build(program).semantic;
|
let semantic = SemanticBuilder::new().build(program).semantic;
|
||||||
let stats = semantic.stats();
|
let stats = semantic.stats();
|
||||||
let (symbols, scopes) = semantic.into_symbol_table_and_scope_tree();
|
let (symbols, scopes) = semantic.into_symbol_table_and_scope_tree();
|
||||||
Compressor::new(allocator, self.options.compress)
|
Compressor::new(allocator, compress)
|
||||||
.build_with_symbols_and_scopes(symbols, scopes, program);
|
.build_with_symbols_and_scopes(symbols, scopes, program);
|
||||||
|
stats
|
||||||
|
} else {
|
||||||
|
Stats::default()
|
||||||
|
};
|
||||||
let mangler = self.options.mangle.map(|options| {
|
let mangler = self.options.mangle.map(|options| {
|
||||||
let semantic = SemanticBuilder::new().with_stats(stats).build(program).semantic;
|
let semantic = SemanticBuilder::new().with_stats(stats).build(program).semantic;
|
||||||
let (symbols, scopes) = semantic.into_symbol_table_and_scope_tree();
|
let (symbols, scopes) = semantic.into_symbol_table_and_scope_tree();
|
||||||
|
|
|
||||||
|
|
@ -271,7 +271,7 @@ impl Oxc {
|
||||||
let compress_options = minifier_options.compress_options.unwrap_or_default();
|
let compress_options = minifier_options.compress_options.unwrap_or_default();
|
||||||
let options = MinifierOptions {
|
let options = MinifierOptions {
|
||||||
mangle: minifier_options.mangle.unwrap_or_default().then(MangleOptions::default),
|
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 {
|
CompressOptions {
|
||||||
drop_console: compress_options.drop_console,
|
drop_console: compress_options.drop_console,
|
||||||
drop_debugger: compress_options.drop_debugger,
|
drop_debugger: compress_options.drop_debugger,
|
||||||
|
|
@ -279,7 +279,7 @@ impl Oxc {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
CompressOptions::all_false()
|
CompressOptions::all_false()
|
||||||
},
|
}),
|
||||||
};
|
};
|
||||||
Minifier::new(options).build(&allocator, &mut program).mangler
|
Minifier::new(options).build(&allocator, &mut program).mangler
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,9 @@ oxc_allocator = { workspace = true }
|
||||||
oxc_codegen = { workspace = true }
|
oxc_codegen = { workspace = true }
|
||||||
oxc_minifier = { workspace = true }
|
oxc_minifier = { workspace = true }
|
||||||
oxc_parser = { workspace = true }
|
oxc_parser = { workspace = true }
|
||||||
|
oxc_sourcemap = { workspace = true, features = ["napi", "rayon"] }
|
||||||
oxc_span = { workspace = true }
|
oxc_span = { workspace = true }
|
||||||
|
oxc_syntax = { workspace = true }
|
||||||
|
|
||||||
napi = { workspace = true }
|
napi = { workspace = true }
|
||||||
napi-derive = { workspace = true }
|
napi-derive = { workspace = true }
|
||||||
|
|
|
||||||
69
napi/minify/index.d.ts
vendored
69
napi/minify/index.d.ts
vendored
|
|
@ -1,4 +1,71 @@
|
||||||
/* auto-generated by NAPI-RS */
|
/* auto-generated by NAPI-RS */
|
||||||
/* eslint-disable */
|
/* 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>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,7 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build-dev": "napi build --platform",
|
"build-dev": "napi build --platform",
|
||||||
"build": "napi build --platform --release",
|
"build": "napi build --platform --release",
|
||||||
"test": "echo 'skip'"
|
"test": "vitest --typecheck run ./test"
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14.*"
|
|
||||||
},
|
},
|
||||||
"napi": {
|
"napi": {
|
||||||
"binaryName": "minify",
|
"binaryName": "minify",
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,59 @@
|
||||||
|
#![allow(clippy::needless_pass_by_value)]
|
||||||
|
|
||||||
|
mod options;
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use napi::Either;
|
||||||
use napi_derive::napi;
|
use napi_derive::napi;
|
||||||
|
|
||||||
use oxc_allocator::Allocator;
|
use oxc_allocator::Allocator;
|
||||||
use oxc_codegen::{Codegen, CodegenOptions};
|
use oxc_codegen::{Codegen, CodegenOptions};
|
||||||
use oxc_minifier::{CompressOptions, MangleOptions, Minifier, MinifierOptions};
|
use oxc_minifier::Minifier;
|
||||||
use oxc_parser::Parser;
|
use oxc_parser::Parser;
|
||||||
use oxc_span::SourceType;
|
use oxc_span::SourceType;
|
||||||
|
|
||||||
#[allow(clippy::needless_pass_by_value)]
|
use crate::options::{MinifyOptions, MinifyResult};
|
||||||
|
|
||||||
|
/// Minify synchronously.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// * Fails to parse the options.
|
||||||
#[napi]
|
#[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 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().with_typescript(true);
|
||||||
|
|
||||||
let mut program = Parser::new(&allocator, &source_text, source_type).parse().program;
|
let mut program = Parser::new(&allocator, &source_text, source_type).parse().program;
|
||||||
|
|
||||||
let mangler = Minifier::new(MinifierOptions {
|
let mangler = Minifier::new(minifier_options).build(&allocator, &mut program).mangler;
|
||||||
mangle: Some(MangleOptions::default()),
|
|
||||||
compress: CompressOptions::default(),
|
|
||||||
})
|
|
||||||
.build(&allocator, &mut program)
|
|
||||||
.mangler;
|
|
||||||
|
|
||||||
Codegen::new()
|
let mut codegen_options = match &options.codegen {
|
||||||
.with_options(CodegenOptions { minify: true, ..CodegenOptions::default() })
|
Some(Either::A(false)) => CodegenOptions { minify: false, ..CodegenOptions::default() },
|
||||||
.with_mangler(mangler)
|
None | Some(Either::A(true)) => {
|
||||||
.build(&program)
|
CodegenOptions { minify: true, ..CodegenOptions::default() }
|
||||||
.code
|
}
|
||||||
|
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
123
napi/minify/src/options.rs
Normal 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>,
|
||||||
|
}
|
||||||
34
napi/minify/test/minify.test.ts
Normal file
34
napi/minify/test/minify.test.ts
Normal 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',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
11
napi/transform/index.d.ts
vendored
11
napi/transform/index.d.ts
vendored
|
|
@ -205,17 +205,6 @@ export declare const enum Severity {
|
||||||
Advice = 'Advice'
|
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.
|
* Transpile a JavaScript or TypeScript into a target ECMAScript version.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue