diff --git a/Cargo.lock b/Cargo.lock index 72e13c7f89411..e11b76ff0126c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1872,6 +1872,17 @@ dependencies = [ "smallvec", ] +[[package]] +name = "oxc_linter_napi" +version = "0.1.0" +dependencies = [ + "mimalloc-safe", + "napi", + "napi-build", + "napi-derive", + "oxlint", +] + [[package]] name = "oxc_macros" version = "0.0.0" @@ -2301,6 +2312,7 @@ dependencies = [ "insta", "lazy-regex", "mimalloc-safe", + "napi", "oxc-miette", "oxc_allocator", "oxc_diagnostics", diff --git a/Cargo.toml b/Cargo.toml index 00aa401e293ca..573cccd806c83 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -136,6 +136,7 @@ oxc_linter = { path = "crates/oxc_linter" } oxc_macros = { path = "crates/oxc_macros" } oxc_tasks_common = { path = "tasks/common" } oxc_tasks_transform_checker = { path = "tasks/transform_checker" } +oxlint = { path = "apps/oxlint" } # Relaxed version so the user can decide which version to use. napi = "3.0.0-beta" diff --git a/apps/oxlint/Cargo.toml b/apps/oxlint/Cargo.toml index 0aa48aeca1b99..f0059da0f32d8 100644 --- a/apps/oxlint/Cargo.toml +++ b/apps/oxlint/Cargo.toml @@ -16,6 +16,8 @@ description.workspace = true workspace = true [lib] +crate-type = ["lib"] +path = "src/lib.rs" doctest = false [[bin]] @@ -34,6 +36,7 @@ bpaf = { workspace = true, features = ["autocomplete", "bright-color", "derive"] cow-utils = { workspace = true } ignore = { workspace = true, features = ["simd-accel"] } miette = { workspace = true } +napi = { workspace = true } rayon = { workspace = true } rustc-hash = { workspace = true } serde = { workspace = true } diff --git a/apps/oxlint/src/lib.rs b/apps/oxlint/src/lib.rs index 26dbbe909445f..960583df910cf 100644 --- a/apps/oxlint/src/lib.rs +++ b/apps/oxlint/src/lib.rs @@ -7,6 +7,67 @@ mod tester; mod walk; pub mod cli { - pub use crate::{command::*, lint::LintRunner, result::CliRunResult, runner::Runner}; } + +#[cfg(all(feature = "allocator", not(miri), not(target_family = "wasm")))] +#[global_allocator] +static GLOBAL: mimalloc_safe::MiMalloc = mimalloc_safe::MiMalloc; + +use cli::{CliRunResult, LintRunner, Runner}; +use std::{ffi::OsStr, io::BufWriter}; + +pub fn lint() -> CliRunResult { + init_tracing(); + init_miette(); + + let mut args = std::env::args_os().peekable(); + + let args = match args.peek() { + Some(s) if s == OsStr::new("node") => args.skip(2), + _ => args.skip(1), + }; + let args = args.collect::>(); + + // SAFELY skip first two args (node + script.js) + // let cli_args = std::env::args_os().skip(2); + let cmd = crate::cli::lint_command(); + let command = match cmd.run_inner(&*args) { + Ok(cmd) => cmd, + Err(e) => { + e.print_message(100); + return CliRunResult::InvalidOptionConfig; + } + }; + + command.handle_threads(); + // stdio is blocked by LineWriter, use a BufWriter to reduce syscalls. + // See `https://github.com/rust-lang/rust/issues/60673`. + let mut stdout = BufWriter::new(std::io::stdout()); + + LintRunner::new(command).run(&mut stdout) +} + +// Initialize the data which relies on `is_atty` system calls so they don't block subsequent threads. +fn init_miette() { + miette::set_hook(Box::new(|_| Box::new(miette::MietteHandlerOpts::new().build()))).unwrap(); +} + +/// To debug `oxc_resolver`: +/// `OXC_LOG=oxc_resolver oxlint --import-plugin` +fn init_tracing() { + use tracing_subscriber::{filter::Targets, prelude::*}; + + // Usage without the `regex` feature. + // + tracing_subscriber::registry() + .with(std::env::var("OXC_LOG").map_or_else( + |_| Targets::new(), + |env_var| { + use std::str::FromStr; + Targets::from_str(&env_var).unwrap() + }, + )) + .with(tracing_subscriber::fmt::layer()) + .init(); +} diff --git a/apps/oxlint/src/main.rs b/apps/oxlint/src/main.rs index 4137d2b48d75e..ef4f2fc998ee2 100644 --- a/apps/oxlint/src/main.rs +++ b/apps/oxlint/src/main.rs @@ -1,44 +1,5 @@ -// NOTE: Miri does not support custom allocators -#[cfg(all(feature = "allocator", not(miri), not(target_family = "wasm")))] -#[global_allocator] -static GLOBAL: mimalloc_safe::MiMalloc = mimalloc_safe::MiMalloc; - -use oxlint::cli::{CliRunResult, LintRunner, Runner}; -use std::io::BufWriter; +use oxlint::{cli::CliRunResult, lint}; fn main() -> CliRunResult { - init_tracing(); - init_miette(); - - let command = oxlint::cli::lint_command().run(); - command.handle_threads(); - // stdio is blocked by LineWriter, use a BufWriter to reduce syscalls. - // See `https://github.com/rust-lang/rust/issues/60673`. - let mut stdout = BufWriter::new(std::io::stdout()); - - LintRunner::new(command).run(&mut stdout) -} - -// Initialize the data which relies on `is_atty` system calls so they don't block subsequent threads. -fn init_miette() { - miette::set_hook(Box::new(|_| Box::new(miette::MietteHandlerOpts::new().build()))).unwrap(); -} - -/// To debug `oxc_resolver`: -/// `OXC_LOG=oxc_resolver oxlint --import-plugin` -fn init_tracing() { - use tracing_subscriber::{filter::Targets, prelude::*}; - - // Usage without the `regex` feature. - // - tracing_subscriber::registry() - .with(std::env::var("OXC_LOG").map_or_else( - |_| Targets::new(), - |env_var| { - use std::str::FromStr; - Targets::from_str(&env_var).unwrap() - }, - )) - .with(tracing_subscriber::fmt::layer()) - .init(); + lint() } diff --git a/dprint.json b/dprint.json index 0dc3d7af90928..0c1d536915af9 100644 --- a/dprint.json +++ b/dprint.json @@ -17,6 +17,8 @@ "**/CHANGELOG.md", "pnpm-workspace.yaml", "pnpm-lock.yaml", + "napi/oxlint2/src/bindings.js", + "napi/oxlint2/src/bindings.d.ts", "napi/{transform,minify,playground}/index.js", "napi/{parser,transform,minify,playground}/index.d.ts", "napi/{parser,transform,minify,playground}/*.wasi-browser.js", diff --git a/napi/oxlint2/Cargo.toml b/napi/oxlint2/Cargo.toml new file mode 100644 index 0000000000000..03435e4d3a9b1 --- /dev/null +++ b/napi/oxlint2/Cargo.toml @@ -0,0 +1,43 @@ +[package] +name = "oxc_linter_napi" +version = "0.1.0" +authors.workspace = true +categories.workspace = true +edition.workspace = true +homepage.workspace = true +include = ["/src", "build.rs"] +keywords.workspace = true +license.workspace = true +publish = false +repository.workspace = true +rust-version.workspace = true +description.workspace = true + +[lints] +workspace = true + +[lib] +crate-type = ["cdylib", "lib"] +test = false +doctest = false + +[dependencies] +oxlint = { workspace = true } + +napi = { workspace = true, features = ["async"] } +napi-derive = { workspace = true } + +[target.'cfg(not(any(target_os = "linux", target_os = "freebsd", target_arch = "arm", target_family = "wasm")))'.dependencies] +mimalloc-safe = { workspace = true, optional = true, features = ["skip_collect_on_exit"] } + +[target.'cfg(all(target_os = "linux", not(target_arch = "arm"), not(target_arch = "aarch64")))'.dependencies] +mimalloc-safe = { workspace = true, optional = true, features = ["skip_collect_on_exit", "local_dynamic_tls"] } + +[target.'cfg(all(target_os = "linux", target_arch = "aarch64"))'.dependencies] +mimalloc-safe = { workspace = true, optional = true, features = ["skip_collect_on_exit", "local_dynamic_tls", "no_opt_arch"] } + +[build-dependencies] +napi-build = { workspace = true } + +[features] +default = [] diff --git a/napi/oxlint2/README.md b/napi/oxlint2/README.md new file mode 100644 index 0000000000000..6c1e88e453bfd --- /dev/null +++ b/napi/oxlint2/README.md @@ -0,0 +1 @@ +# Oxlint 2 diff --git a/napi/oxlint2/build.rs b/napi/oxlint2/build.rs new file mode 100644 index 0000000000000..0f1b01002b079 --- /dev/null +++ b/napi/oxlint2/build.rs @@ -0,0 +1,3 @@ +fn main() { + napi_build::setup(); +} diff --git a/napi/oxlint2/package.json b/napi/oxlint2/package.json new file mode 100644 index 0000000000000..54e75939411f9 --- /dev/null +++ b/napi/oxlint2/package.json @@ -0,0 +1,44 @@ +{ + "name": "oxlint2", + "version": "0.1.0", + "main": "src/index.js", + "type": "module", + "scripts": { + "build-dev": "napi build --platform --js ./bindings.js --dts ./bindings.d.ts --output-dir src --no-dts-cache --esm", + "build": "pnpm run build-dev --release", + "test": "echo 'No tests defined yet'" + }, + "engines": { + "node": ">=20.0.0" + }, + "description": "Staging package for oxlint while we integrate custom JS plugins into oxlint", + "author": "Boshen and oxc contributors", + "license": "MIT", + "homepage": "https://oxc.rs", + "bugs": "https://github.com/oxc-project/oxc/issues", + "repository": { + "type": "git", + "url": "https://github.com/oxc-project/oxc.git", + "directory": "napi/oxlint2" + }, + "publishConfig": { + "registry": "https://registry.npmjs.org/", + "access": "public" + }, + "devDependencies": { + "typescript": "catalog:" + }, + "napi": { + "binaryName": "oxlint", + "targets": [ + "win32-x64", + "win32-arm64", + "linux-x64-gnu", + "linux-arm64-gnu", + "linux-x64-musl", + "linux-arm64-musl", + "darwin-x64", + "darwin-arm64" + ] + } +} diff --git a/napi/oxlint2/src/bindings.d.ts b/napi/oxlint2/src/bindings.d.ts new file mode 100644 index 0000000000000..9ad1986c37ca4 --- /dev/null +++ b/napi/oxlint2/src/bindings.d.ts @@ -0,0 +1,3 @@ +/* auto-generated by NAPI-RS */ +/* eslint-disable */ +export declare function lint(): boolean diff --git a/napi/oxlint2/src/bindings.js b/napi/oxlint2/src/bindings.js new file mode 100644 index 0000000000000..fa1e677268dec --- /dev/null +++ b/napi/oxlint2/src/bindings.js @@ -0,0 +1,379 @@ +// prettier-ignore +/* eslint-disable */ +// @ts-nocheck +/* auto-generated by NAPI-RS */ + +import { createRequire } from 'node:module' +const require = createRequire(import.meta.url) +const __dirname = new URL('.', import.meta.url).pathname + +const { readFileSync } = require('node: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 = () => { + let report = null + if (typeof process.report?.getReport === 'function') { + process.report.excludeNetwork = true + report = process.report.getReport() + } + 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.env.NAPI_RS_NATIVE_LIBRARY_PATH) { + try { + nativeBinding = require(process.env.NAPI_RS_NATIVE_LIBRARY_PATH); + } catch (err) { + loadErrors.push(err) + } + } else if (process.platform === 'android') { + if (process.arch === 'arm64') { + try { + return require('./oxlint.android-arm64.node') + } catch (e) { + loadErrors.push(e) + } + try { + return require('oxlint2-android-arm64') + } catch (e) { + loadErrors.push(e) + } + + } else if (process.arch === 'arm') { + try { + return require('./oxlint.android-arm-eabi.node') + } catch (e) { + loadErrors.push(e) + } + try { + return require('oxlint2-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('./oxlint.win32-x64-msvc.node') + } catch (e) { + loadErrors.push(e) + } + try { + return require('oxlint2-win32-x64-msvc') + } catch (e) { + loadErrors.push(e) + } + + } else if (process.arch === 'ia32') { + try { + return require('./oxlint.win32-ia32-msvc.node') + } catch (e) { + loadErrors.push(e) + } + try { + return require('oxlint2-win32-ia32-msvc') + } catch (e) { + loadErrors.push(e) + } + + } else if (process.arch === 'arm64') { + try { + return require('./oxlint.win32-arm64-msvc.node') + } catch (e) { + loadErrors.push(e) + } + try { + return require('oxlint2-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('./oxlint.darwin-universal.node') + } catch (e) { + loadErrors.push(e) + } + try { + return require('oxlint2-darwin-universal') + } catch (e) { + loadErrors.push(e) + } + + if (process.arch === 'x64') { + try { + return require('./oxlint.darwin-x64.node') + } catch (e) { + loadErrors.push(e) + } + try { + return require('oxlint2-darwin-x64') + } catch (e) { + loadErrors.push(e) + } + + } else if (process.arch === 'arm64') { + try { + return require('./oxlint.darwin-arm64.node') + } catch (e) { + loadErrors.push(e) + } + try { + return require('oxlint2-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('./oxlint.freebsd-x64.node') + } catch (e) { + loadErrors.push(e) + } + try { + return require('oxlint2-freebsd-x64') + } catch (e) { + loadErrors.push(e) + } + + } else if (process.arch === 'arm64') { + try { + return require('./oxlint.freebsd-arm64.node') + } catch (e) { + loadErrors.push(e) + } + try { + return require('oxlint2-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('./oxlint.linux-x64-musl.node') + } catch (e) { + loadErrors.push(e) + } + try { + return require('oxlint2-linux-x64-musl') + } catch (e) { + loadErrors.push(e) + } + + } else { + try { + return require('./oxlint.linux-x64-gnu.node') + } catch (e) { + loadErrors.push(e) + } + try { + return require('oxlint2-linux-x64-gnu') + } catch (e) { + loadErrors.push(e) + } + + } + } else if (process.arch === 'arm64') { + if (isMusl()) { + try { + return require('./oxlint.linux-arm64-musl.node') + } catch (e) { + loadErrors.push(e) + } + try { + return require('oxlint2-linux-arm64-musl') + } catch (e) { + loadErrors.push(e) + } + + } else { + try { + return require('./oxlint.linux-arm64-gnu.node') + } catch (e) { + loadErrors.push(e) + } + try { + return require('oxlint2-linux-arm64-gnu') + } catch (e) { + loadErrors.push(e) + } + + } + } else if (process.arch === 'arm') { + if (isMusl()) { + try { + return require('./oxlint.linux-arm-musleabihf.node') + } catch (e) { + loadErrors.push(e) + } + try { + return require('oxlint2-linux-arm-musleabihf') + } catch (e) { + loadErrors.push(e) + } + + } else { + try { + return require('./oxlint.linux-arm-gnueabihf.node') + } catch (e) { + loadErrors.push(e) + } + try { + return require('oxlint2-linux-arm-gnueabihf') + } catch (e) { + loadErrors.push(e) + } + + } + } else if (process.arch === 'riscv64') { + if (isMusl()) { + try { + return require('./oxlint.linux-riscv64-musl.node') + } catch (e) { + loadErrors.push(e) + } + try { + return require('oxlint2-linux-riscv64-musl') + } catch (e) { + loadErrors.push(e) + } + + } else { + try { + return require('./oxlint.linux-riscv64-gnu.node') + } catch (e) { + loadErrors.push(e) + } + try { + return require('oxlint2-linux-riscv64-gnu') + } catch (e) { + loadErrors.push(e) + } + + } + } else if (process.arch === 'ppc64') { + try { + return require('./oxlint.linux-ppc64-gnu.node') + } catch (e) { + loadErrors.push(e) + } + try { + return require('oxlint2-linux-ppc64-gnu') + } catch (e) { + loadErrors.push(e) + } + + } else if (process.arch === 's390x') { + try { + return require('./oxlint.linux-s390x-gnu.node') + } catch (e) { + loadErrors.push(e) + } + try { + return require('oxlint2-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('./oxlint.wasi.cjs') + } catch (err) { + if (process.env.NAPI_RS_FORCE_WASI) { + loadErrors.push(err) + } + } + if (!nativeBinding) { + try { + nativeBinding = require('oxlint2-wasm32-wasi') + } catch (err) { + if (process.env.NAPI_RS_FORCE_WASI) { + loadErrors.push(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`) +} + +const { lint } = nativeBinding +export { lint } diff --git a/napi/oxlint2/src/index.js b/napi/oxlint2/src/index.js new file mode 100644 index 0000000000000..60cd7dc8aedee --- /dev/null +++ b/napi/oxlint2/src/index.js @@ -0,0 +1,19 @@ +import { lint } from './bindings.js'; + +class Linter { + run() { + return lint(); + } +} + +function main() { + const linter = new Linter(); + + const result = linter.run(); + + if (!result) { + process.exit(1); + } +} + +main(); diff --git a/napi/oxlint2/src/lib.rs b/napi/oxlint2/src/lib.rs new file mode 100644 index 0000000000000..7cb251652f915 --- /dev/null +++ b/napi/oxlint2/src/lib.rs @@ -0,0 +1,10 @@ +use std::process::{ExitCode, Termination}; + +use napi_derive::napi; + +use oxlint::lint as oxlint_lint; + +#[napi] +pub fn lint() -> bool { + oxlint_lint().report() == ExitCode::SUCCESS +} diff --git a/napi/oxlint2/tsconfig.json b/napi/oxlint2/tsconfig.json new file mode 100644 index 0000000000000..c878e86a202a1 --- /dev/null +++ b/napi/oxlint2/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "module": "Preserve", + "moduleResolution": "Bundler", + "noEmit": true, + "target": "ESNext" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 71bc0bd27fc84..0e7be074616e5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -82,6 +82,12 @@ importers: specifier: 'catalog:' version: 3.2.4(@types/node@24.0.3)(@vitest/browser@3.2.4) + napi/oxlint2: + devDependencies: + typescript: + specifier: 'catalog:' + version: 5.8.3 + napi/parser: dependencies: '@oxc-project/types': @@ -134,6 +140,8 @@ importers: npm/runtime: {} + npm/wasm-web: {} + tasks/compat_data: devDependencies: degit: