npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

bini-native

v1.0.3

Published

Automatic Tauri native feature detection and wiring

Readme

bini-native

Automatic Tauri native wiring for Bini.js. Import one Vite plugin. Never touch src-tauri/ by hand again.

npm version npm downloads license vite tauri types


What it does

bini-native watches your frontend source, works out which native capabilities your app actually uses, and wires up the Tauri side automatically — Rust plugin registration, Cargo.toml dependencies, capability permissions, Android manifest entries, and iOS/macOS Info.plist usage descriptions. You import a Vite plugin once. That's the entire setup.

No CLI commands to memorize. No manual src-tauri/src/lib.rs edits. No forgetting a permission and getting a silent, unexplained failure. Complexity stays invisible; you write normal web APIs (navigator.geolocation, new Notification(...), navigator.clipboard.writeText(...)) and they work identically in the browser and inside the native shell.

Installation

pnpm add -D bini-native
# or
npm install -D bini-native
# or
yarn add -D bini-native
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { biniNative } from 'bini-native';

export default defineConfig({
  plugins: [react(), biniNative()],
});

That's it. Run tauri dev as usual — see How it works for what that does and doesn't do during tauri build.

How it works

Wiring only ever happens during tauri dev. tauri build is a deliberate no-op: bini-native is registered with Vite's apply: 'serve', so none of its hooks — detection, package installs, Cargo.toml/lib.rs patching, capability/manifest/plist edits, polyfill injection — run during a production build. vite build behaves exactly as if the plugin weren't installed at all.

| Mode | What happens | |---|---| | tauri dev | Detection + wiring runs twice: once synchronously at module load, before Vite's dev server port opens (Tauri's CLI waits for that port before starting cargo build, so wiring always finishes first), then again on Vite's buildStart as a safety net | | tauri build | Nothing. No detection, no installs, no file writes |

This is safe because the wiring is persistent, not build-time: everything bini-native writes — Cargo.toml entries, lib.rs plugin registrations, capabilities/default.json permissions, AndroidManifest.xml, Info.plist — lands as ordinary files inside src-tauri/. Those are the same files cargo and the platform build tooling read no matter which command last touched them. Run tauri dev once, and the wiring is on disk; every later tauri build just compiles against what's already there, the same as if you'd hand-edited those files yourself.

Practical implication: run tauri dev at least once — and again after adding a new native API — before your first tauri build. On a completely fresh checkout where tauri dev has never run, src-tauri/ hasn't been wired yet and tauri build won't wire it for you.

There's no "always wired regardless of use" tier. Every plugin — including the everyday ones like dialog and clipboard — is wired only if your source actually uses it, via a combined regex + AST import scan:

  • showOpenFilePicker(...) / showSaveFilePicker(...) → dialog plugin (which pulls in the filesystem plugin automatically — see below)
  • navigator.clipboard.writeText / readText → clipboard plugin
  • new Notification(...) → notification plugin
  • navigator.geolocation.* → geolocation plugin + Android/iOS/macOS location permissions
  • registerGlobalShortcut(...) → global-shortcut plugin (desktop-only, see Platform notes)
  • setAutoStart(...) → autostart plugin (desktop-only, see Platform notes)
  • window.biniStore.* → persistent key-value store plugin
  • getUserMedia({ video: true }) / { audio: true } → camera/microphone platform permissions (no plugin needed — Tauri's webview supports these natively)
  • Any direct import ... from '@tauri-apps/plugin-*' → that plugin, even without a matching Web API call

One dependency link exists on purpose: the dialog polyfill's synthetic file handle (getFile(), createWritable()) dynamically imports the filesystem plugin internally, even though your own code never touches it directly. So a project that only calls showOpenFilePicker() still gets the filesystem plugin wired — otherwise the picker would compile fine and then fail at runtime the first time someone tries to read the picked file.

One thing stays unconditional regardless of what's detected: Tauri's core capability (core:default — window operations, events, app metadata) is always ensured in capabilities/default.json, even on a project with zero plugin usage, since it's foundational to any Tauri app rather than tied to a specific plugin.

If you add a new API mid-session, bini-native detects it and prints a one-line notice to restart tauri dev — it deliberately does not rewrite src-tauri/ files while cargo might be running, to avoid write races.

Supported plugins

| Feature | Web API you write | Rust crate | Platform | |---|---|---|---| | Opener | @tauri-apps/plugin-opener | tauri-plugin-opener | All | | Dialog | showOpenFilePicker / showSaveFilePicker polyfill | tauri-plugin-dialog | All (auto-pulls in Filesystem) | | Filesystem | @tauri-apps/plugin-fs | tauri-plugin-fs | All | | Clipboard | navigator.clipboard.writeText / readText | tauri-plugin-clipboard-manager | All | | Notifications | new Notification(...) | tauri-plugin-notification | All | | OS info | @tauri-apps/plugin-os | tauri-plugin-os | All | | Geolocation | navigator.geolocation.* | tauri-plugin-geolocation | Android, iOS, macOS (wired if detected, but the permission always fails on Windows/Linux — see Platform notes) | | Global shortcut | registerGlobalShortcut(...) | tauri-plugin-global-shortcut | Desktop only | | Autostart | setAutoStart(...) | tauri-plugin-autostart | Desktop only | | Persistent store | window.biniStore.* | tauri-plugin-store | All | | Camera / microphone | getUserMedia(...) | (none — native webview support) | All | | Bluetooth | navigator.bluetooth | (no first-party plugin exists) | Unsupported — logged, not wired |

"Desktop only" plugins are handled specially, not just wired the same way as everything else: their Cargo dependency is placed under a target-conditional section ([target."cfg(not(any(target_os = "android", target_os = "ios")))".dependencies]) rather than plain [dependencies], and their registration is wrapped in #[cfg(desktop)] inside .setup() rather than chained onto the builder. Their Rust crates genuinely don't expose the relevant APIs when compiled for Android/iOS — chaining them the normal way would fail to compile on mobile builds.

Platform notes

A couple of things are Tauri/OS limitations, not bugs in this plugin:

  • Notifications on Windows dev builds — unpackaged tauri dev builds have no Start Menu shortcut / AUMID, so Windows silently denies the permission with no prompt at all. Run tauri build and install the result to test notifications properly.
  • Geolocation on Windows/Linux desktop — Tauri's official geolocation plugin only supports Android, iOS, and macOS. It's still wired automatically if your source uses it (since the same code path may also target mobile), but the permission request will always fail on Windows/Linux desktop.
  • Autostart and global shortcuts don't exist on mobile — these two plugins are Tauri desktop-only by design (there's no "launch at startup" or "global keyboard shortcut" concept on Android/iOS). If your source uses them, they're still wired, but only inside a #[cfg(desktop)] block and a target-conditional Cargo dependency — so an Android or iOS build compiles cleanly and simply doesn't include that code, rather than failing to compile.

Requirements

  • A Tauri v2 project (src-tauri/ present) — on projects without one, bini-native no-ops entirely, so it's safe to include in web-only builds too
  • A src/main.tsx / main.jsx / main.ts / main.js entry file, where runtime polyfills get injected
  • Vite 8
  • tauri dev run at least once before tauri build on a fresh checkout (see How it works)

CI / production builds

Because wiring is dev-only and persistent, a CI pipeline that only ever runs tauri build on a fresh checkout — never tauri dev — will build against whatever's already committed in src-tauri/, unwired for anything not already there. Two common ways to handle this:

  • Commit src-tauri/ after wiring locally. Run tauri dev once on your machine, confirm the plugin's edits look right, then commit Cargo.toml, lib.rs, capabilities/default.json, and any manifest/plist changes like any other source file. CI's tauri build then just compiles them — no special CI step needed.
  • Run a short-lived tauri dev in CI first, then kill it once wiring finishes, before invoking tauri build. Slower, but useful if you don't want generated Tauri files in version control.

Either way, bini-native never runs code during the build step itself — build is always just cargo compiling whatever's on disk.

Security

bini-native runs during tauri dev and touches your package manager and your src-tauri/ files directly, so it's worth being explicit about how it does that:

  • No shell string interpolation, and no shell: true. Package installs (npm add, pnpm add, etc.) go through cross-spawn instead of Node's built-in child_process with shell: true. This distinction matters: Node's own shell: true option does not escape an args array — it concatenates the entries into a single string and hands that to the shell to reparse, which is exactly the injection surface a naive execSync string would have (this is also why Node emits deprecation warning DEP0190 when you combine shell: true with an args array). cross-spawn avoids that entirely by resolving Windows .cmd/.bat shims and quoting arguments itself, without ever asking a shell to reparse a joined string.
  • Package names are allowlisted. Every package name bini-native might install is checked against a strict pattern (^(@scope/)?name with only alphanumerics, ., _, -) before it's ever passed to the package manager. Anything that doesn't match is skipped with a warning instead of being installed.
  • Writes are contained to your project. Every file bini-native patches (Cargo.toml, lib.rs/main.rs, capabilities/default.json, AndroidManifest.xml, Info.plist) is resolved from a fixed src-tauri/ root via static path joins, with a guard that refuses to write anywhere outside that root.
  • Build is a strictly smaller attack surface than dev. Because tauri build doesn't run any of the plugin's code, none of the above — subprocess spawning, package installs, file patching — happens as part of a production build at all.
  • Dependency footprint is intentionally small. There are two runtime dependencies: oxc-parser (MIT, used for the AST import scan that detects @tauri-apps/plugin-* usage) and cross-spawn (MIT, used for the safe cross-platform process spawning described above). oxc-parser ships native NAPI bindings per platform, which is why you'll see "native code" flagged by supply-chain scanners — that's expected for a Rust-backed parser and not something a pure-JS package can avoid while staying fast. cross-spawn is pure JS with no native code. vite is a peer dependency, not bundled. typescript, tsup, and everything else live in devDependencies and are never published — you can confirm this yourself with npm pack --dry-run or pnpm pack --dry-run, which lists exactly what ships in the tarball.

If your org runs supply-chain scanning as part of CI, point it at the packed tarball rather than the full repo/lockfile where possible — scanning devDependencies will surface findings (license, minification, etc.) that never reach anyone who installs bini-native.

FAQ

Does this touch my code every time I save? No. File watching only triggers a cheap, read-only feature-detection scan (debounced) that logs a restart notice if something new shows up — it never rewrites src-tauri/ mid-session.

Does tauri build re-check or re-wire anything? No — tauri build is a complete no-op for bini-native. It only ever runs during tauri dev. See How it works and CI / production builds.

What if I've already hand-edited lib.rs? bini-native checks the actual registered .plugin(...) call for each crate. If it's already correct, it's left untouched. If it's missing, it's inserted. If it's present but wrong — wrong call form, or a desktop-only plugin chained where it shouldn't be — it's corrected in place rather than left broken.

Does "detected" mean I have to call the Web API directly? No — a direct import from the plugin's npm package counts too, not just the polyfilled Web API. So import { readFile } from '@tauri-apps/plugin-fs' wires the filesystem plugin even without touching a browser API at all.

Can I use this without Bini.js? Yes — it's a plain Vite plugin with no Bini.js-specific dependency. It expects a conventional Vite + Tauri project layout, nothing more.

Contributing

Issues and PRs welcome. Please include your OS, Tauri version, and (if relevant) the exact web API call that didn't get detected or wired correctly.

License

MIT © Bini.js