first commit

This commit is contained in:
2026-07-12 15:55:18 +05:30
commit ee98026cc5
404 changed files with 44522 additions and 0 deletions
+17
View File
@@ -0,0 +1,17 @@
# @wrnexus/native
Cross-platform capabilities for browsers, Capacitor WebViews, and compiled native apps.
```ts
import { native } from "@wrnexus/native";
if (native.supports("share")) await native.run("share", { title: "WrNexus", url: location.href });
```
Built-ins include `camera`, `clipboard.write`, `share`, `geolocation`, `network`,
`haptics`, storage, filesystem, notifications, and device information. Browser
capabilities use Web APIs; mobile capabilities use installed Capacitor plugins.
`platform()` returns `server` during SSR, `browser` on the web, and the Capacitor
platform in a native WebView. Unsupported operations reject with
`NativeUnavailableError`; use `supports()` before presenting optional UI.
+11
View File
@@ -0,0 +1,11 @@
{
"name": "@wrnexus/native",
"version": "0.2.12",
"type": "module",
"main": "src/index.ts",
"exports": {
".": "./src/index.ts",
"./browser": "./src/browser.ts",
"./mobile": "./src/mobile.ts"
}
}
+121
View File
@@ -0,0 +1,121 @@
import type { NativeCapability } from "./types.ts";
type ShareOptions = { title?: string; text?: string; url?: string };
type PositionOptions = globalThis.PositionOptions;
export const browserCapabilities: Record<string, NativeCapability> = {
"clipboard.write": {
browser: {
supported: () => typeof navigator !== "undefined" && !!navigator.clipboard?.writeText,
run: (options) =>
navigator.clipboard.writeText(String((options as { text?: unknown })?.text ?? "")),
},
},
share: {
browser: {
supported: () => typeof navigator !== "undefined" && typeof navigator.share === "function",
run: (options) => navigator.share(options as ShareOptions),
},
},
geolocation: {
browser: {
supported: () => typeof navigator !== "undefined" && !!navigator.geolocation,
run: (options) =>
new Promise((resolve, reject) =>
navigator.geolocation.getCurrentPosition(resolve, reject, options as PositionOptions),
),
},
},
network: {
browser: {
supported: () => typeof navigator !== "undefined",
run: () => ({ connected: navigator.onLine }),
},
},
camera: {
browser: {
supported: () => typeof navigator !== "undefined" && !!navigator.mediaDevices?.getUserMedia,
run: (options) =>
navigator.mediaDevices.getUserMedia({ video: options ?? true, audio: false }),
},
},
"storage.get": {
browser: {
supported: () => typeof localStorage !== "undefined",
run: (options) => ({
value: localStorage.getItem(String((options as { key?: unknown })?.key ?? "")),
}),
},
},
"storage.set": {
browser: {
supported: () => typeof localStorage !== "undefined",
run: (options) => {
const value = options as { key?: unknown; value?: unknown };
localStorage.setItem(String(value?.key ?? ""), String(value?.value ?? ""));
},
},
},
"notifications.schedule": {
browser: {
supported: () => typeof Notification !== "undefined",
run: async (options) => {
if (Notification.permission === "default") await Notification.requestPermission();
if (Notification.permission !== "granted")
throw new Error("Notification permission was not granted");
const value = options as { title?: string; body?: string };
return new Notification(value?.title ?? "Notification", { body: value?.body });
},
},
},
"device.info": {
browser: {
supported: () => typeof navigator !== "undefined",
run: () => ({
platform: "web",
userAgent: navigator.userAgent,
language: navigator.language,
}),
},
},
haptics: {
browser: {
supported: () => typeof navigator !== "undefined" && typeof navigator.vibrate === "function",
run: (options) => navigator.vibrate((options as { duration?: number })?.duration ?? 20),
},
},
"filesystem.read": {
browser: {
supported: () => typeof window !== "undefined" && "showOpenFilePicker" in window,
run: async () => {
const picker = (
window as unknown as Window & {
showOpenFilePicker(): Promise<Array<{ getFile(): Promise<File> }>>;
}
).showOpenFilePicker;
const [handle] = await picker.call(window);
return handle?.getFile();
},
},
},
"filesystem.write": {
browser: {
supported: () => typeof window !== "undefined" && "showSaveFilePicker" in window,
run: async (options) => {
const picker = (
window as unknown as Window & {
showSaveFilePicker(): Promise<{
createWritable(): Promise<{
write(value: unknown): Promise<void>;
close(): Promise<void>;
}>;
}>;
}
).showSaveFilePicker;
const writable = await (await picker.call(window)).createWritable();
await writable.write((options as { data?: unknown })?.data ?? "");
await writable.close();
},
},
},
};
+24
View File
@@ -0,0 +1,24 @@
import { browserCapabilities } from "./browser.ts";
import { mobileCapabilities } from "./mobile.ts";
import { isMobile, platform, register, registered, run, supports } from "./registry.ts";
const names = new Set([...Object.keys(browserCapabilities), ...Object.keys(mobileCapabilities)]);
for (const name of names) {
register(name, { ...browserCapabilities[name], ...mobileCapabilities[name] });
}
export * from "./types.ts";
export {
NativeUnavailableError,
clearRegistry,
isMobile,
platform,
register,
registered,
run,
supports,
} from "./registry.ts";
export { browserCapabilities } from "./browser.ts";
export { mobileCapabilities } from "./mobile.ts";
export const native = { isMobile, platform, register, registered, run, supports };
+69
View File
@@ -0,0 +1,69 @@
import type { NativeCapability } from "./types.ts";
type Plugin = Record<string, (...args: unknown[]) => unknown>;
function plugin(name: string): Plugin | undefined {
const root = globalThis as typeof globalThis & {
Capacitor?: { Plugins?: Record<string, Plugin> };
};
return root.Capacitor?.Plugins?.[name];
}
function capacitorAdapter(pluginName: string, method: string): NativeCapability["mobile"] {
return {
supported: () => typeof plugin(pluginName)?.[method] === "function",
run: (options) => plugin(pluginName)![method]!(options),
};
}
function transformedCapacitorAdapter(
pluginName: string,
method: string,
transform: (options: unknown) => unknown,
): NativeCapability["mobile"] {
return {
supported: () => typeof plugin(pluginName)?.[method] === "function",
run: (options) => plugin(pluginName)![method]!(transform(options)),
};
}
export const mobileCapabilities: Record<string, NativeCapability> = {
camera: { mobile: capacitorAdapter("Camera", "getPhoto") },
"clipboard.write": {
mobile: transformedCapacitorAdapter("Clipboard", "write", (options) => {
const value = options as { text?: unknown; string?: unknown };
return { string: String(value?.text ?? value?.string ?? "") };
}),
},
share: { mobile: capacitorAdapter("Share", "share") },
geolocation: { mobile: capacitorAdapter("Geolocation", "getCurrentPosition") },
network: { mobile: capacitorAdapter("Network", "getStatus") },
haptics: { mobile: capacitorAdapter("Haptics", "impact") },
"storage.get": { mobile: capacitorAdapter("Preferences", "get") },
"storage.set": { mobile: capacitorAdapter("Preferences", "set") },
"filesystem.read": { mobile: capacitorAdapter("Filesystem", "readFile") },
"filesystem.write": { mobile: capacitorAdapter("Filesystem", "writeFile") },
"notifications.schedule": {
mobile: transformedCapacitorAdapter("LocalNotifications", "schedule", (options) => {
const value = options as {
notifications?: unknown[];
id?: number;
title?: string;
body?: string;
schedule?: unknown;
};
if (value?.notifications) return value;
return {
notifications: [
{
id: value?.id ?? Date.now() % 2_147_483_647,
title: value?.title ?? "Notification",
body: value?.body ?? "",
schedule: value?.schedule,
},
],
};
}),
},
"device.info": { mobile: capacitorAdapter("Device", "getInfo") },
};
+106
View File
@@ -0,0 +1,106 @@
import type {
NativeAdapter,
NativeCapability,
NativePlatform,
NativeRunOptions,
NativeTarget,
NativeBrowserRuntime,
} from "./types.ts";
export class NativeUnavailableError extends Error {
constructor(message: string) {
super(message);
this.name = "NativeUnavailableError";
}
}
function capacitor():
| {
isNativePlatform?: () => boolean;
getPlatform?: () => string;
}
| undefined {
if (typeof globalThis === "undefined") return undefined;
return (globalThis as typeof globalThis & { Capacitor?: object }).Capacitor;
}
export function isMobile(): boolean {
const bridge = capacitor();
return (
bridge?.isNativePlatform?.() === true ||
["ios", "android"].includes(bridge?.getPlatform?.() ?? "")
);
}
export function platform(): NativePlatform {
const bridge = capacitor();
if (isMobile()) return bridge?.getPlatform?.() ?? "webview";
return typeof window === "undefined" ? "server" : "browser";
}
const capabilities = new Map<string, NativeCapability>();
function browserRuntime(): NativeBrowserRuntime | undefined {
if (typeof globalThis === "undefined") return undefined;
return (globalThis as typeof globalThis & { WrNexusNative?: NativeBrowserRuntime }).WrNexusNative;
}
function pendingRegistrations(): Map<string, NativeCapability> | undefined {
if (typeof window === "undefined") return undefined;
const root = globalThis as typeof globalThis & {
__WrNexusNativePending?: Map<string, NativeCapability>;
};
return (root.__WrNexusNativePending ??= new Map());
}
export function register<TOptions = unknown, TResult = unknown>(
name: string,
capability: NativeCapability<TOptions, TResult>,
): () => void {
if (!/^[a-z][a-z0-9.-]*$/i.test(name))
throw new TypeError(`Invalid native capability name: ${name}`);
const value = capability as NativeCapability;
capabilities.set(name, value);
const runtime = browserRuntime();
if (runtime) runtime.register(name, value);
else pendingRegistrations()?.set(name, value);
return () => {
capabilities.delete(name);
const activeRuntime = browserRuntime();
if (activeRuntime) activeRuntime.unregister(name);
else pendingRegistrations()?.delete(name);
};
}
export function registered(): string[] {
return [...capabilities.keys()].sort();
}
function adapter(name: string, target: NativeTarget): NativeAdapter | undefined {
return capabilities.get(name)?.[target];
}
export function supports(
name: string,
target: NativeTarget = isMobile() ? "mobile" : "browser",
): boolean {
const value = adapter(name, target);
return !!value && (value.supported?.() ?? true);
}
export async function run<TResult = unknown>(
name: string,
options?: unknown,
runOptions: NativeRunOptions = {},
): Promise<TResult> {
const target = runOptions.target ?? (isMobile() ? "mobile" : "browser");
const value = adapter(name, target);
if (!value || !(value.supported?.() ?? true)) {
throw new NativeUnavailableError(`Native capability "${name}" is unavailable on ${target}`);
}
return (await value.run(options)) as TResult;
}
export function clearRegistry(): void {
capabilities.clear();
}
+25
View File
@@ -0,0 +1,25 @@
export type NativePlatform = "server" | "browser" | "webview" | "ios" | "android" | "expo" | string;
export type NativeTarget = "browser" | "mobile";
export interface NativeAdapter<TOptions = unknown, TResult = unknown> {
supported?: () => boolean;
run(options?: TOptions): TResult | Promise<TResult>;
}
export interface NativeCapability<TOptions = unknown, TResult = unknown> {
browser?: NativeAdapter<TOptions, TResult>;
mobile?: NativeAdapter<TOptions, TResult>;
}
export interface NativeRunOptions {
target?: NativeTarget;
}
export interface NativeBrowserRuntime {
target: NativeTarget;
isMobile: boolean;
register(name: string, capability: NativeCapability): void;
unregister(name: string): void;
supports(name: string): boolean;
run<TResult = unknown>(name: string, options?: unknown): Promise<TResult>;
}
+88
View File
@@ -0,0 +1,88 @@
import { afterEach, expect, test } from "bun:test";
import {
clearRegistry,
NativeUnavailableError,
platform,
register,
run,
supports,
} from "../src/registry.ts";
import { mobileCapabilities } from "../src/mobile.ts";
afterEach(clearRegistry);
test("registers and runs a browser capability", async () => {
register("example.echo", { browser: { run: (options) => options } });
expect(supports("example.echo", "browser")).toBe(true);
expect(await run<{ value: number }>("example.echo", { value: 3 }, { target: "browser" })).toEqual(
{ value: 3 },
);
});
test("reports unsupported adapters without invoking them", async () => {
register("example.mobile", { mobile: { supported: () => false, run: () => "no" } });
expect(supports("example.mobile", "mobile")).toBe(false);
expect(run("example.mobile", undefined, { target: "mobile" })).rejects.toBeInstanceOf(
NativeUnavailableError,
);
});
test("unregister callback removes a custom capability", () => {
const unregister = register("example.temp", { browser: { run: () => true } });
expect(supports("example.temp", "browser")).toBe(true);
unregister();
expect(supports("example.temp", "browser")).toBe(false);
});
test("reports server rather than browser during SSR", () => {
const root = globalThis as typeof globalThis & { window?: Window };
const existing = Object.getOwnPropertyDescriptor(root, "window");
Reflect.deleteProperty(root, "window");
expect(platform()).toBe("server");
if (existing) Object.defineProperty(root, "window", existing);
});
test("mirrors custom registration into the declarative browser runtime", () => {
const calls: string[] = [];
const root = globalThis as typeof globalThis & { WrNexusNative?: object };
root.WrNexusNative = {
target: "browser",
isMobile: false,
register: (name: string) => calls.push(`add:${name}`),
unregister: (name: string) => calls.push(`remove:${name}`),
supports: () => true,
run: async () => undefined,
};
const unregister = register("example.mirror", { browser: { run: () => true } });
unregister();
delete root.WrNexusNative;
expect(calls).toEqual(["add:example.mirror", "remove:example.mirror"]);
});
test("normalizes unified clipboard options for Capacitor", async () => {
let received: unknown;
const root = globalThis as typeof globalThis & { Capacitor?: object };
root.Capacitor = {
Plugins: { Clipboard: { write: (options: unknown) => (received = options) } },
};
await mobileCapabilities["clipboard.write"]!.mobile!.run({ text: "hello" });
delete root.Capacitor;
expect(received).toEqual({ string: "hello" });
});
test("normalizes a simple notification into Capacitor's notification list", async () => {
let received: unknown;
const root = globalThis as typeof globalThis & { Capacitor?: object };
root.Capacitor = {
Plugins: { LocalNotifications: { schedule: (options: unknown) => (received = options) } },
};
await mobileCapabilities["notifications.schedule"]!.mobile!.run({
id: 7,
title: "Hi",
body: "There",
});
delete root.Capacitor;
expect(received).toEqual({
notifications: [{ id: 7, title: "Hi", body: "There", schedule: undefined }],
});
});