107 lines
3.1 KiB
TypeScript
107 lines
3.1 KiB
TypeScript
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();
|
|
}
|