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

@swiftbrowser/native

v0.28.10

Published

Run a @swiftbrowser/react app natively: the runtime a JavaScriptCore-hosted app needs (timers, console, fetch and the other web globals over the SwiftBrowser Native bridge), the `run` entry that renders into the native SwiftUI renderer, and the bundler th

Readme

@swiftbrowser/native

Runs a @swiftbrowser/react app natively: inside an iOS app, in JavaScriptCore, drawn by real SwiftUI through the SwiftBrowser Native Swift package. The package is three things:

  • run(<App />), the entry an app calls: a @swiftbrowser/react root whose ops go to the native renderer and whose events come back from it, the native counterpart of mount.
  • The runtime: the web globals bare JavaScriptCore lacks, built on the bridge's primitives (setTimeout and friends, requestAnimationFrame, console, performance, fetch with Headers, Request, Response and AbortController, URL, URLSearchParams, TextEncoder, TextDecoder, atob, btoa, crypto.getRandomValues and randomUUID, localStorage, structuredClone, navigator (with onLine from the host's path monitor, and the online and offline events; with wakeLock over the host's idle timer; with geolocation over the host's location manager), speechSynthesis and SpeechRecognition over the system's voices and recognizer, Image over the host's image loader, for the size of a picture without drawing it, which is how AsyncImage's placeholder and failure show natively). React's scheduler reads some of these when it loads, so the bundle starts with the runtime.
  • The bundler: swiftbrowser-native build writes one app.js for the Xcode target, and swiftbrowser-native dev serves it with rebuild on save for the app's NativeApp.watch(url:).

The design is in docs/native.md. The package is experimental, like @swiftbrowser/react.

The package also carries the Swift side: swift/ is the SwiftBrowser Native Swift package (its Package.swift and Sources, without the tests), so an Xcode project can depend on node_modules/@swiftbrowser/native/swift as a local package and build against the exact version of the renderer this JavaScript was written for. swiftbrowser native init lays a project out that way:

npx @swiftbrowser/cli native init MyApp   # MyApp.xcodeproj (iOS and macOS), apple/, src/, web/
cd MyApp && npm install                   # React, @swiftbrowser/react, @swiftbrowser/web and this package
npm run build                             # apple/app.js, the bundle the app ships
open MyApp.xcodeproj                      # run it on a simulator, an iPhone or this Mac
npm run dev:web                           # the same app as a web page

For a hand-made project:

npm install @swiftbrowser/native @swiftbrowser/react react react-reconciler

An app

// src/index.tsx
import { run } from '@swiftbrowser/native';
import { List, NavigationStack, Section, Text, Toggle } from '@swiftbrowser/react';
import { useState } from 'react';

function App() {
  const [on, setOn] = useState(false);
  return (
    <NavigationStack>
      <List navigationTitle="Settings">
        <Section header="General">
          <Toggle title="Notifications" isOn={[on, setOn]} />
          <Text foregroundStyle="secondary">{on ? 'On' : 'Off'}</Text>
        </Section>
      </List>
    </NavigationStack>
  );
}

run(<App />);
npx swiftbrowser-native build src/index.tsx --out ../apple/app.js   # the bundle Xcode ships
npx swiftbrowser-native dev src/index.tsx --out ../apple/app.js     # http://localhost:5173/, Fast Refresh on save

On the Swift side, NativeApp().load() runs the bundled app.js and NativeRootView(app:) draws it (see the Swift package's README). The examples under Native/Examples are complete apps laid out this way.

What runs where

Every call across the bridge is synchronous. A tap is one call chain: SwiftUI's button action calls __sbDispatch(json), the handler runs, React flushes the pass, the sink calls __sbNative.applyOps(json), the Swift tree mutates, and everything returns. A controlled TextField therefore never shows a stale frame: when its binding's setter returns, the app's state already holds the new text.

There is no window and no document. Code that checks for them takes its headless path, which is right here; @swiftbrowser/react's Host (React DOM inside the tree) renders nothing, as react-dom is aliased to a stub.

fetch sends the request over the bridge and URLSession performs it. A body may be a Blob or a FormData (sent as multipart: a file upload works as written for the page), and one holding picked or dropped files still on disk (the files themselves, their slices, a Blob built from slices of several, a form with them) is streamed from disk: the body goes to the host as parts, bytes beside the request and ranges of files, and an InputStream reads them in order as the request sends, so a 2 GB upload holds nothing in memory; a host before 0.20 reads the files first. Responses are read whole (response.body is null; arrayBuffer(), text(), json(), blob()). An AbortController's abort cancels the task (fetchCancel). URL is a small parser, enough for absolute and relative URLs and query strings, not the whole WHATWG algorithm; URL.createObjectURL gives a data: URL. WebSocket runs on URLSessionWebSocketTask, crypto.subtle digests (SHA-1 to SHA-512) and signs HMACs on CryptoKit, matchMedia answers the prefers-color-scheme queries from the host's environment, navigator.vibrate plays a haptic, and navigator.onLine is the network path's status from NWPathMonitor, with the online and offline events on the global (addEventListener('offline', …), as there is no window; ononline and onoffline handlers too). navigator.wakeLock.request('screen') keeps the screen awake while its sentinel is held (UIApplication.isIdleTimerDisabled; a native sentinel stays held in the background, where the system owns the screen, and holds again on return). signInWithApple and SignInWithAppleButton show the system's Sign in with Apple sheet (ASAuthorizationController, with the app's com.apple.developer.applesignin entitlement) and answer the credential; appleCredentialState asks the provider. fileManager reaches the app's sandbox through files requests (a range read is FileHandle's seek and read, so a large file goes a piece at a time; a picked or dropped file written is copied on disk, importItem, never through the bridge), and its iCloud Drive container (iCloudDirectory()) where the app has the entitlement: the container's files are read and written under file coordination, downloaded first when another device wrote them, and their changes come to __sbFilesChanged for fileManager.watch, as the app's own writes do. navigator.geolocation asks CLLocationManager: getCurrentPosition for one fix, watchPosition for every fix until clearWatch, with the API's GeolocationPositionError codes (the first ask prompts the viewer; the app's Info.plist names NSLocationWhenInUseUsageDescription). speechSynthesis.speak(new SpeechSynthesisUtterance(text)) reads through AVSpeechSynthesizer (the utterance's lang, rate, pitch and volume, its start, boundary, pause, resume, end and error events; cancel, pause and resume on the queue), and new SpeechRecognition() listens through SFSpeechRecognizer (lang, continuous, interimResults; start, stop, abort; result events with results[i][0].transcript and isFinal, error and end; the app's Info.plist names NSSpeechRecognitionUsageDescription and NSMicrophoneUsageDescription). A page's useScenePhase() is the app's scene phase here (active, inactive, background), as NativeRootView sends it.

Workers

npx swiftbrowser-native build src/index.tsx --out ../apple/app.js --worker render=src/render.worker.ts
import { createWorker } from '@swiftbrowser/native';
const worker = createWorker('render'); // render.worker.js, beside app.js
worker.onmessage = (event) => show(event.data);
worker.postMessage({ html, width, height });

A page keeps heavy work off its UI thread in a Web Worker; a JavaScriptCore app has the same through the host. --worker <name>=<entry> (repeatable; build, dev and site take it) bundles the entry as <name>.worker.js with the runtime first, as app.js is built, and createWorker(name) runs it in a second JSContext in a JSVirtualMachine of its own on a thread of its own, with a run loop: the worker's timers and fetch answers come through it, and so does the engine's deferred work, so an emscripten module's WebAssembly.instantiate settles there as it does in a browser. The worker's scope has self, postMessage, onmessage, addEventListener and the runtime's globals, but no tree: it computes and answers. Messages cross as text, since values cannot be shared between virtual machines: JSON, with typed arrays and ArrayBuffers carried as bytes and Errors as their name and message (encodeMessage and decodeMessage are exported); Maps, functions and the rest JSON drops do not cross, and a Date arrives as its ISO string. terminate() ends the worker. Drawn in a page (site), createWorker returns the page's own Worker over the same bundle, published with the site, so the app's code is the same in all three places. The iOS app's WebAssembly is the engine's interpreter (a third-party app has no JIT), and SIMD in it needs iOS 26.2.

const worker = createWorker('satoru', { engine: 'webview' });

engine: 'webview' runs the same bundle in a hidden WKWebView instead, whose web content process does compile JavaScript and WebAssembly with its JIT: much faster for heavy WebAssembly, for a process and a web view's memory. Its fetch still goes through the app's URLSession, with the app's cookies; a page's own Worker ignores the option.

URLs the app is opened with

import { onOpenURL } from '@swiftbrowser/native';
useEffect(() => onOpenURL((url) => take(new URL(url).searchParams.get('token'))), []);

A universal link or a custom scheme opens the app through SwiftUI's onOpenURL, which the App file hands to NativeApp.open(url:) (the project native init writes does); onOpenURL here delivers it to the app's listeners. A URL that launched the app waits for the bundle and then for the first listener, so nothing is lost on a cold start.

Now playing

navigator.mediaSession.metadata = new MediaMetadata({ title, artist, album, artwork: [{ src }] });
navigator.mediaSession.playbackState = 'playing';
navigator.mediaSession.setPositionState({ duration, position, playbackRate: 1 });
navigator.mediaSession.setActionHandler('nexttrack', () => player.advanceToNextItem());

The browser's Media Session API, as an audio app writes it for the page, works in the native app too: the runtime's navigator.mediaSession and MediaMetadata hand what is set to the host, which puts it on the lock screen and the system's player controls through MediaPlayer, and the buttons there (and the media keys) run the handlers setActionHandler registered: play, pause, stop, nexttrack, previoustrack, seekto (with seekTime) and seekforward / seekbackward (with seekOffset, 15 seconds when the system names none). Setting playbackState to playing activates a playback audio session, which the lock screen needs; for the audio to go on with the screen locked, the app's Xcode target also needs the audio background mode (Signing & Capabilities → Background Modes → Audio, AirPlay, and Picture in Picture).

Notifications

import { onNotification, registerForPush } from '@swiftbrowser/native';

if ((await Notification.requestPermission()) === 'granted') {
  const token = await registerForPush();
  await api.post('/devices', { token });
}
useEffect(() => onNotification((n) => { if (n.event === 'opened') openPost(n.data.postId); }), []);
new Notification('Saved', { body: 'The draft is on the server', tag: 'draft', data: { draftId } });
navigator.setAppBadge(unread);

The browser's Notification works in the native app over UserNotifications: Notification.permission (read at launch and after each requestPermission), requestPermission(), new Notification(title, { body, tag, data, silent, badge }) shown at once (tag is the notification's identifier; a later one with the same tag replaces it), close(), and onclick when the viewer opens it. navigator.setAppBadge and clearAppBadge set the app badge.

registerForPush() registers with APNs and resolves with the device token as hex, or rejects with the system's reason (the Xcode target needs the Push Notifications capability, and the App file the delegate adaptor the project native init writes has: @UIApplicationDelegateAdaptor(NativeAppDelegate.self); an app with a delegate of its own calls NativeApp.didRegisterForRemoteNotifications(deviceToken:), didFailToRegisterForRemoteNotifications(error:) and didReceiveRemoteNotification(userInfo:) from it). onNotification delivers what happens: { event: 'opened' | 'received', remote, id, title, body, data, action? }, with the push payload or the Notification's data as data. opened is the viewer tapping a notification (one that launched the app waits for the first listener, like a URL); received is one that arrived while the app is open (shown as it would be in the background) or a silent push the app delegate was handed.

In the page (the round trip, or any browser) the same calls work with Web Push: Notification is the browser's, registerForPush({ applicationServerKey }) subscribes through the site's service worker with the server's VAPID public key and resolves with the subscription's JSON as the token, and the worker's pushes and clicks become onNotification events. A server tells the two tokens apart by their shape and sends one payload to both: APNs' (aps.alert, aps.badge), which the worker reads too (docs/ops-protocol.md, "Push notifications").

Sharing, picking, the clipboard and signing in

ShareLink, fileImporter, fileExporter, the clipboard and the color chooser from @swiftbrowser/react ask the browser for a share sheet, a file chooser, a save dialog, the pasteboard and a color picker; in the native app the host answers instead, with UIActivityViewController, the photo picker (for pictures and videos) or the document picker (which also saves an export to Files), UIPasteboard and UIColorPickerViewController. Nothing to write: run fills the web renderer's environment with what the page has nothing for, so a screen written for the page shares and picks as it did. A picked file is a LazyFile: a File whose name, type and size are known and whose bytes stay on disk (a document in the app's tmp, a photo copied there, a folder opened in place with its security scope kept) until arrayBuffer(), text() or a slice reads them by range through the files bridge; fileManager.write copies it, a fetch body or FormData streams it from disk, ShareLink and fileExporter copy it. Files dropped on a dropDestination from another app arrive the same way, a dropped folder's named by their paths in it. The app keeps them readable until it asks the host to forget them (files release, or the next folder pick).

import { webAuthenticate, secureStorage } from '@swiftbrowser/native';

const callback = await webAuthenticate('https://x.example/oauth/authorize?…', { callbackScheme: 'myapp' });
secureStorage.setItem('session', new URL(callback).searchParams.get('token')!);

webAuthenticate(url, { callbackScheme, ephemeral }) is the system's sign-in sheet (ASWebAuthenticationSession): it resolves with the callback URL the sign-in ends with, and rejects with an AbortError when the viewer closes it. In the page it runs on the shim's session (the popup flow of docs/ops-protocol.md, "Signing in"). secureStorage has localStorage's shape over the Keychain, for what must not sit in plain text; in the page it is localStorage under a prefix.

Undo

useUndoManager() from @swiftbrowser/react keeps its stack in JavaScript; in the native app the window's UndoManager mirrors it (the host's undoState, Host/UndoBridge.swift: one proxy action per side the app can use, named as the app names it), so the shake, the three-finger swipe, ⌘Z from a keyboard and the Edit menu undo and redo the app's changes, as they do in a SwiftUI app. Nothing to write: run puts the host in the web renderer's environment where the page's keyboard would be.

The dev loop and Fast Refresh

swiftbrowser-native dev serves the bundle in two halves and a manifest:

| path | what | |---|---| | /manifest.json | { "vendor": <hash>, "app": <hash> }, which NativeApp.watch(url:) polls every second | | /vendor.js | the runtime, React Fast Refresh's runtime, React, @swiftbrowser/react and this package: evaluated once | | /app.js | the app's own modules, each run through react-refresh/babel, importing React and the packages through shims that read __sbVendor: evaluated again on every save |

On a save the app half is evaluated in the running engine: its modules register their components with the refresh runtime, and run, called again at its end, performs the refresh and renders the new tree into the same root. A component whose hooks are unchanged keeps its state; one whose hooks changed remounts, as React Fast Refresh decides from the signatures the transform records. A new vendor half (a dependency changed) starts the app over. No module loader or WebSocket is involved, which bare JavaScriptCore lacks. test/fastRefresh.test.ts runs the loop in Node.

launchArguments() reads the process's arguments (-screen list from an Xcode scheme or simctl launch), as the Fidelity example does to open one screen.

In CI

npx swiftbrowser-native ios-profile com.example.MyApp --profile "MyApp Ad Hoc" --name MyApp
npx swiftbrowser-native ios-install-page build/MyApp.ipa --out site --url https://ios.my-app.pages.dev \
  --bundle-id com.example.MyApp --version 1.0.42 [--web https://my-app.pages.dev/] [--commit $GITHUB_SHA]

ios-profile makes an ad hoc provisioning profile again through the App Store Connect API (ASC_KEY_ID, ASC_ISSUER_ID and ASC_KEY_P8 from the environment, a key with the Admin or App Manager role) with every enabled iOS device and valid distribution certificate of the team, registering the bundle ID if the team has none, and installs it where Xcode looks. ios-install-page writes the over-the-air install page around an exported IPA: the IPA, its manifest.plist, an index.html whose button installs it, and the _headers Cloudflare Pages serves them with. SwiftBrowser's reusable workflow sb-ios-adhoc.yml runs both (docs/native.md, "Building and deploying from GitHub Actions").

In the browser

npx swiftbrowser-native site src/index.tsx --out dist          # the app as a static site
npx swiftbrowser-native site src/index.tsx --out ../web/dist/native --base /native/

The same bundle, drawn by the native views in a browser: the package ships the SwiftBrowser Native Swift package compiled for WebAssembly against SwiftBrowser's SwiftUI shim (wasm/SwiftBrowserNative.wasm, the round trip of docs/native.md), and site writes it, the bundle and the web renderer's page as one static directory, the layout swiftbrowser build writes for a Swift app. The page runs the module, the module evaluates the bundle at the page's global scope (so fetch, timers and localStorage are the browser's own, same origin and cookies included), the native views draw the app's ops with the shim's SwiftUI, and the web renderer draws what the shim emits. One app, three renderers: the browser's DOM through @swiftbrowser/react directly, real SwiftUI in the iOS app, and this: the iOS app's own views, in a page. --base /path/ when the site is served under a path of another site, --module <file> for a module built from a checkout (swift build --swift-sdk <wasm sdk> -c release --product NativeRoundTrip), --name <App> for the site's app name (the project's folder by default), --catalog <dir.xcassets> (repeatable) for the app's named images and colors (the Xcode target's catalog, or one kept for the site: a .symbolset holding any SVG draws as a symbol there), and --worker <name>=<entry> (repeatable) for the app's workers, published beside the bundle for the page's Worker.

Registered views

import { RegisteredView } from '@swiftbrowser/react';

<RegisteredView name="MapView" props={{ region }} onEvent={(value) => console.log('map said', value)}>
  <Text>Children are SwiftUI views inside the native view</Text>
</RegisteredView>

The Swift app registers the view once:

ViewRegistry.register("MapView") { context in
    MapView(region: context.props["region"]) { context.send(["tapped": true]) }
}

and a page can register its own element under the same name (ViewRegistry.register('MapView', (context) => ({ element, update, dispose })), also from @swiftbrowser/react), so the same element draws on both.

Swift that is not a view registers as a function, before the bundle loads (swiftbrowser native init writes an apple/NativeFunctions.swift for them):

NativeFunctions.register("purchase") { call in            // async
    let product = try await Product.products(for: [call["id"].string ?? ""]).first
    ...
    return ["state": "purchased"]
}
NativeFunctions.registerSync("deviceName") { _ in .string(UIDevice.current.name) }
NativeFunctions.send("transaction", ["id": "pro"])         // Swift reporting on its own
import { callNative, callNativeSync, onNativeEvent, hasNativeFunction, nativeFunctions } from '@swiftbrowser/native';

const { state } = await callNative<{ state: string }>('purchase', { id: 'pro' });
const name = callNativeSync<string>('deviceName');
const stop = onNativeEvent('transaction', (value) => console.log('bought', value));

Params and results cross as JSON (call["id"] reads a member, call.decode(MyParams.self) the whole object). An error a function throws rejects the promise with its description; NativeFunctionCancelled rejects with an AbortError, as the system pickers' cancellations do. callNative is a host request (app:<name>), so it needs nothing the other requests do not; callNativeSync runs inline, for a cheap lookup used during render. On the web build of the same app callNative rejects unsupported:, nativeFunctions() is empty and hasNativeFunction(name) is false, so the app can fall back to the web's way. Everything runs on the main actor.

Testing without a device

installRuntime(native, target) installs the runtime on any object over any bridge, and run(element, { native }) renders through one; the tests in test/ drive both with a fake bridge. test/bundle.test.ts builds the example apps and runs the bundles in a bare vm context with only __sbNative, as JavaScriptCore runs them, and replays the Todos example's snapshot script against the tree the ops build.