diff --git a/Cargo.lock b/Cargo.lock index 1853dc02a..bd69ff79c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1715,6 +1715,20 @@ dependencies = [ "pico-args", ] +[[package]] +name = "oxc_minify_napi" +version = "0.30.3" +dependencies = [ + "napi", + "napi-build", + "napi-derive", + "oxc_allocator", + "oxc_codegen", + "oxc_minifier", + "oxc_parser", + "oxc_span", +] + [[package]] name = "oxc_minsize" version = "0.0.0" diff --git a/napi/minify/Cargo.toml b/napi/minify/Cargo.toml new file mode 100644 index 000000000..fa541681c --- /dev/null +++ b/napi/minify/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "oxc_minify_napi" +version = "0.30.3" +authors.workspace = true +categories.workspace = true +edition.workspace = true +homepage.workspace = true +keywords.workspace = true +license.workspace = true +publish = true +repository.workspace = true +rust-version.workspace = true +description.workspace = true + +[lints] +workspace = true + +[lib] +crate-type = ["cdylib", "lib"] +test = false +doctest = false + +[dependencies] +oxc_allocator = { workspace = true } +oxc_codegen = { workspace = true } +oxc_minifier = { workspace = true } +oxc_parser = { workspace = true } +oxc_span = { workspace = true } + +napi = { workspace = true } +napi-derive = { workspace = true } + +[package.metadata.cargo-shear] +ignored = ["napi"] + +[build-dependencies] +napi-build = { workspace = true } diff --git a/napi/minify/build.rs b/napi/minify/build.rs new file mode 100644 index 000000000..0f1b01002 --- /dev/null +++ b/napi/minify/build.rs @@ -0,0 +1,3 @@ +fn main() { + napi_build::setup(); +} diff --git a/napi/minify/index.d.ts b/napi/minify/index.d.ts new file mode 100644 index 000000000..7928e4f59 --- /dev/null +++ b/napi/minify/index.d.ts @@ -0,0 +1,3 @@ +/* auto-generated by NAPI-RS */ +/* eslint-disable */ +export declare function minify(filename: string, sourceText: string): string; diff --git a/napi/minify/index.js b/napi/minify/index.js new file mode 100644 index 000000000..6b860f19a --- /dev/null +++ b/napi/minify/index.js @@ -0,0 +1,345 @@ +// prettier-ignore +/* eslint-disable */ +/* auto-generated by NAPI-RS */ + +const { readFileSync } = require('fs'); + +let nativeBinding = null; +const loadErrors = []; + +const isMusl = () => { + let musl = false; + if (process.platform === 'linux') { + musl = isMuslFromFilesystem(); + if (musl === null) { + musl = isMuslFromReport(); + } + if (musl === null) { + musl = isMuslFromChildProcess(); + } + } + return musl; +}; + +const isFileMusl = (f) => f.includes('libc.musl-') || f.includes('ld-musl-'); + +const isMuslFromFilesystem = () => { + try { + return readFileSync('/usr/bin/ldd', 'utf-8').includes('musl'); + } catch { + return null; + } +}; + +const isMuslFromReport = () => { + const report = typeof process.report.getReport === 'function' ? process.report.getReport() : null; + if (!report) { + return null; + } + if (report.header && report.header.glibcVersionRuntime) { + return false; + } + if (Array.isArray(report.sharedObjects)) { + if (report.sharedObjects.some(isFileMusl)) { + return true; + } + } + return false; +}; + +const isMuslFromChildProcess = () => { + try { + return require('child_process').execSync('ldd --version', { encoding: 'utf8' }).includes('musl'); + } catch (e) { + // If we reach this case, we don't know if the system is musl or not, so is better to just fallback to false + return false; + } +}; + +function requireNative() { + if (process.platform === 'android') { + if (process.arch === 'arm64') { + try { + return require('./minify.android-arm64.node'); + } catch (e) { + loadErrors.push(e); + } + try { + return require('@oxc-minify/binding-android-arm64'); + } catch (e) { + loadErrors.push(e); + } + } else if (process.arch === 'arm') { + try { + return require('./minify.android-arm-eabi.node'); + } catch (e) { + loadErrors.push(e); + } + try { + return require('@oxc-minify/binding-android-arm-eabi'); + } catch (e) { + loadErrors.push(e); + } + } else { + loadErrors.push(new Error(`Unsupported architecture on Android ${process.arch}`)); + } + } else if (process.platform === 'win32') { + if (process.arch === 'x64') { + try { + return require('./minify.win32-x64-msvc.node'); + } catch (e) { + loadErrors.push(e); + } + try { + return require('@oxc-minify/binding-win32-x64-msvc'); + } catch (e) { + loadErrors.push(e); + } + } else if (process.arch === 'ia32') { + try { + return require('./minify.win32-ia32-msvc.node'); + } catch (e) { + loadErrors.push(e); + } + try { + return require('@oxc-minify/binding-win32-ia32-msvc'); + } catch (e) { + loadErrors.push(e); + } + } else if (process.arch === 'arm64') { + try { + return require('./minify.win32-arm64-msvc.node'); + } catch (e) { + loadErrors.push(e); + } + try { + return require('@oxc-minify/binding-win32-arm64-msvc'); + } catch (e) { + loadErrors.push(e); + } + } else { + loadErrors.push(new Error(`Unsupported architecture on Windows: ${process.arch}`)); + } + } else if (process.platform === 'darwin') { + try { + return require('./minify.darwin-universal.node'); + } catch (e) { + loadErrors.push(e); + } + try { + return require('@oxc-minify/binding-darwin-universal'); + } catch (e) { + loadErrors.push(e); + } + + if (process.arch === 'x64') { + try { + return require('./minify.darwin-x64.node'); + } catch (e) { + loadErrors.push(e); + } + try { + return require('@oxc-minify/binding-darwin-x64'); + } catch (e) { + loadErrors.push(e); + } + } else if (process.arch === 'arm64') { + try { + return require('./minify.darwin-arm64.node'); + } catch (e) { + loadErrors.push(e); + } + try { + return require('@oxc-minify/binding-darwin-arm64'); + } catch (e) { + loadErrors.push(e); + } + } else { + loadErrors.push(new Error(`Unsupported architecture on macOS: ${process.arch}`)); + } + } else if (process.platform === 'freebsd') { + if (process.arch === 'x64') { + try { + return require('./minify.freebsd-x64.node'); + } catch (e) { + loadErrors.push(e); + } + try { + return require('@oxc-minify/binding-freebsd-x64'); + } catch (e) { + loadErrors.push(e); + } + } else if (process.arch === 'arm64') { + try { + return require('./minify.freebsd-arm64.node'); + } catch (e) { + loadErrors.push(e); + } + try { + return require('@oxc-minify/binding-freebsd-arm64'); + } catch (e) { + loadErrors.push(e); + } + } else { + loadErrors.push(new Error(`Unsupported architecture on FreeBSD: ${process.arch}`)); + } + } else if (process.platform === 'linux') { + if (process.arch === 'x64') { + if (isMusl()) { + try { + return require('./minify.linux-x64-musl.node'); + } catch (e) { + loadErrors.push(e); + } + try { + return require('@oxc-minify/binding-linux-x64-musl'); + } catch (e) { + loadErrors.push(e); + } + } else { + try { + return require('./minify.linux-x64-gnu.node'); + } catch (e) { + loadErrors.push(e); + } + try { + return require('@oxc-minify/binding-linux-x64-gnu'); + } catch (e) { + loadErrors.push(e); + } + } + } else if (process.arch === 'arm64') { + if (isMusl()) { + try { + return require('./minify.linux-arm64-musl.node'); + } catch (e) { + loadErrors.push(e); + } + try { + return require('@oxc-minify/binding-linux-arm64-musl'); + } catch (e) { + loadErrors.push(e); + } + } else { + try { + return require('./minify.linux-arm64-gnu.node'); + } catch (e) { + loadErrors.push(e); + } + try { + return require('@oxc-minify/binding-linux-arm64-gnu'); + } catch (e) { + loadErrors.push(e); + } + } + } else if (process.arch === 'arm') { + if (isMusl()) { + try { + return require('./minify.linux-arm-musleabihf.node'); + } catch (e) { + loadErrors.push(e); + } + try { + return require('@oxc-minify/binding-linux-arm-musleabihf'); + } catch (e) { + loadErrors.push(e); + } + } else { + try { + return require('./minify.linux-arm-gnueabihf.node'); + } catch (e) { + loadErrors.push(e); + } + try { + return require('@oxc-minify/binding-linux-arm-gnueabihf'); + } catch (e) { + loadErrors.push(e); + } + } + } else if (process.arch === 'riscv64') { + if (isMusl()) { + try { + return require('./minify.linux-riscv64-musl.node'); + } catch (e) { + loadErrors.push(e); + } + try { + return require('@oxc-minify/binding-linux-riscv64-musl'); + } catch (e) { + loadErrors.push(e); + } + } else { + try { + return require('./minify.linux-riscv64-gnu.node'); + } catch (e) { + loadErrors.push(e); + } + try { + return require('@oxc-minify/binding-linux-riscv64-gnu'); + } catch (e) { + loadErrors.push(e); + } + } + } else if (process.arch === 'ppc64') { + try { + return require('./minify.linux-ppc64-gnu.node'); + } catch (e) { + loadErrors.push(e); + } + try { + return require('@oxc-minify/binding-linux-ppc64-gnu'); + } catch (e) { + loadErrors.push(e); + } + } else if (process.arch === 's390x') { + try { + return require('./minify.linux-s390x-gnu.node'); + } catch (e) { + loadErrors.push(e); + } + try { + return require('@oxc-minify/binding-linux-s390x-gnu'); + } catch (e) { + loadErrors.push(e); + } + } else { + loadErrors.push(new Error(`Unsupported architecture on Linux: ${process.arch}`)); + } + } else { + loadErrors.push(new Error(`Unsupported OS: ${process.platform}, architecture: ${process.arch}`)); + } +} + +nativeBinding = requireNative(); + +if (!nativeBinding || process.env.NAPI_RS_FORCE_WASI) { + try { + nativeBinding = require('./minify.wasi.cjs'); + } catch (err) { + if (process.env.NAPI_RS_FORCE_WASI) { + console.error(err); + } + } + if (!nativeBinding) { + try { + nativeBinding = require('@oxc-minify/binding-wasm32-wasi'); + } catch (err) { + if (process.env.NAPI_RS_FORCE_WASI) { + console.error(err); + } + } + } +} + +if (!nativeBinding) { + if (loadErrors.length > 0) { + // TODO Link to documentation with potential fixes + // - The package owner could build/publish bindings for this arch + // - The user may need to bundle the correct files + // - The user may need to re-install node_modules to get new packages + throw new Error('Failed to load native binding', { cause: loadErrors }); + } + throw new Error(`Failed to load native binding`); +} + +module.exports.minify = nativeBinding.minify; diff --git a/napi/minify/package.json b/napi/minify/package.json new file mode 100644 index 000000000..3be6e5b0a --- /dev/null +++ b/napi/minify/package.json @@ -0,0 +1,24 @@ +{ + "name": "@oxc-minify/binding", + "private": true, + "scripts": { + "build": "napi build --platform --release", + "test": "node test.mjs" + }, + "engines": { + "node": ">=14.*" + }, + "napi": { + "binaryName": "minify", + "targets": [ + "x86_64-pc-windows-msvc", + "aarch64-pc-windows-msvc", + "x86_64-unknown-linux-gnu", + "aarch64-unknown-linux-gnu", + "x86_64-unknown-linux-musl", + "aarch64-unknown-linux-musl", + "x86_64-apple-darwin", + "aarch64-apple-darwin" + ] + } +} diff --git a/napi/minify/src/lib.rs b/napi/minify/src/lib.rs new file mode 100644 index 000000000..bed6d9a65 --- /dev/null +++ b/napi/minify/src/lib.rs @@ -0,0 +1,28 @@ +use napi_derive::napi; + +use oxc_allocator::Allocator; +use oxc_codegen::{Codegen, CodegenOptions}; +use oxc_minifier::{CompressOptions, Minifier, MinifierOptions}; +use oxc_parser::Parser; +use oxc_span::SourceType; + +#[allow(clippy::needless_pass_by_value)] +#[napi] +pub fn minify(filename: String, source_text: String) -> String { + 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: true, compress: CompressOptions::default() }) + .build(&allocator, &mut program) + .mangler; + + Codegen::new() + .with_options(CodegenOptions { minify: true, ..CodegenOptions::default() }) + .with_mangler(mangler) + .with_capacity(source_text.len()) + .build(&program) + .source_text +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0538045ff..67b277e73 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -49,6 +49,8 @@ importers: specifier: ^5.4.5 version: 5.6.2 + napi/minify: {} + napi/parser: devDependencies: '@napi-rs/cli':