JavaScript 与 ShellAPI
直接源自代码的原生门面与声明契约。
实验性源码版本。生产安全加固与 SDK 发布仍在进行中。
使用生成的原生门面#
原生方法返回 Promise,并要求匹配 origin 的能力授权。下面的契约从 Framework 已提交的 TypeScript 声明中生成。bigint、不透明句柄和二进制载荷保持声明的编解码语义。
import '/tuffite-shell-api.js';
const windows = await window.tuffite.window.list();window.tuffite 桥接对象#
原生运行时在符合条件的应用和 Explorer 主文档中提供 window.tuffite。NativeBridge 契约包含 invoke()、postMessage()、addEventListener() 和 removeEventListener()。生成模块在同一对象上扩展 tuffite.window 等原生门面。普通浏览器标签页不提供原生桥接对象。
订阅与移除事件监听#
监听使用 EventTarget API。addEventListener(type, listener, options?) 接受 capture 布尔值或 AddEventListenerOptions,包括 once 和 signal。removeEventListener(type, listener, options?) 须使用相同函数和匹配的 capture 值。在 React 中可在 effect 内订阅,并在清理函数中移除监听。
- channel:原生应用事件,detail 为 {channel, data: Uint8Array}。按应用事件契约解码 data,不会自动转换成 JSON。
- message:Explorer 树的 JSON 消息,detail 为 {data, from}。名为 message 的业务 channel 仍通过 channel 事件类型接收。
- navigation、open、popup、newwindow:冒泡的 Explorer 请求,detail 为 {url, disposition, userGesture, from}。dialog:带 accept() 和 dismiss() 的 ExplorerDialogController。传播和归属规则参见 Explorer API。
const onChannel = (event) => {
const { channel, data } = event.detail; // data: Uint8Array
if (channel === 'app:changed') {
console.log(channel, data);
}
};
window.tuffite.addEventListener('channel', onChannel);
// During component cleanup, use the same listener reference.
window.tuffite.removeEventListener('channel', onChannel);发送 Explorer 消息#
postMessage(data, {to}?) 返回 Promise<boolean>。默认目标为 top,也接受 parent 和浏览器签发的表面引用。data 须能序列化为 JSON。浏览器验证路由和归属。true 表示接受并投递或入队,不表示接收方应答。
await window.tuffite.postMessage({ type: 'ready' }, { to: 'parent' });
const onMessage = ({ detail: { data, from } }) => {
console.log(data, from);
};
window.tuffite.addEventListener('message', onMessage);
// Later:
window.tuffite.removeEventListener('message', onMessage);底层二进制调用#
invoke(channel, payload) 返回 Promise<Uint8Array>,并要求对应路由的原生权限。它不经过 Explorer 树,不接受 to 选项。普通应用调用应使用生成的类型化门面,它们处理命令标识、编解码器、schema 指纹和协议验证。
const response = await window.tuffite.invoke(route, binaryPayload);
// binaryPayload and response are Uint8Array.运行时辅助与错误#
tuffite/runtime 导出 TuffiteError、invokeBinary()、createCommandApi()、createTypedCommandApi() 和 createFrameworkApi(),以及 codec 和桥接类型。这些辅助接口服务于生成绑定和工具。TuffiteError 提供 code 以及可选 requestId/cause。原生应用包以 /tuffite-runtime.js 提供运行时模块。
JavaScript 包导出#
下面的完整声明从 JavaScript 包 exports 中的每个 types 入口生成,包含 NativeBridge 事件重载、运行时辅助、Explorer 类型和仅用于开发的 Vite 插件。可按导出路径、类型或成员名称筛选。
tuffite
// Generated from ../../framework/shell_api/common.d.ts, ../../framework/shell_api/dialog.d.ts, ../../framework/shell_api/path.d.ts, ../../framework/shell_api/remoting.d.ts, ../../framework/shell_api/fs.d.ts, ../../framework/shell_api/storage.d.ts, ../../framework/shell_api/log.d.ts, ../../framework/shell_api/network.d.ts, ../../framework/shell_api/utility.d.ts, ../../framework/shell_api/window.d.ts. Do not edit.
declare global {
interface Window {
readonly tuffite: typeof import('./dialog').tuffite & typeof import('./path').tuffite & typeof import('./remoting').tuffite & typeof import('./fs').tuffite & typeof import('./storage').tuffite & typeof import('./log').tuffite & typeof import('./network').tuffite & typeof import('./utility').tuffite & typeof import('./window').tuffite & import('../runtime').NativeBridge;
}
}
export {};
tuffite/runtime
export type Codec = Readonly<Record<string, unknown>>;
export type TuffiteErrorCode =
| "invalid_argument"
| "incompatible_abi"
| "invalid_handle"
| "unavailable"
| "resource_exhausted"
| "internal"
| "transport_unavailable"
| "invalid_request"
| "unknown_command"
| "handler_failed"
| "decode_error"
| "protocol_mismatch"
| "schema_mismatch"
| "command_failed"
| (string & {});
export class TuffiteError extends Error {
readonly code: TuffiteErrorCode;
readonly requestId?: bigint;
readonly cause?: unknown;
constructor(
code: TuffiteErrorCode,
message: string,
options?: Readonly<{ requestId?: bigint; cause?: unknown }>,
);
}
export interface ChannelEvent extends Event {
readonly detail: Readonly<{ channel: string; data: Uint8Array }>;
}
export interface ExplorerMessageEvent<T = unknown> extends Event {
readonly detail: Readonly<{ data: T; from: string }>;
}
export interface ExplorerOpenEvent extends Event {
readonly detail: Readonly<{
url: string;
disposition: string;
userGesture: boolean;
from: string;
}>;
}
export interface ExplorerDialogController {
readonly type: "alert" | "confirm" | "prompt" | "beforeunload";
readonly message: string;
readonly defaultValue: string;
readonly isReload: boolean;
readonly from: string;
accept(promptText?: string): Promise<boolean>;
dismiss(): Promise<boolean>;
}
export interface ExplorerDialogEvent extends Event {
readonly detail: ExplorerDialogController;
}
export type ExplorerSurfaceRef = string & {
readonly __tuffiteExplorerSurfaceRef: unique symbol;
};
export type ExplorerMessageTarget = "parent" | "top" | ExplorerSurfaceRef;
export interface NativeBridge extends EventTarget {
invoke(channel: string, payload: Uint8Array): Promise<Uint8Array>;
postMessage(
data: unknown,
options?: Readonly<{ to?: ExplorerMessageTarget }>,
): Promise<boolean>;
addEventListener(
type: "channel",
listener: (event: ChannelEvent) => void,
options?: boolean | AddEventListenerOptions,
): void;
removeEventListener(
type: "channel",
listener: (event: ChannelEvent) => void,
options?: boolean | EventListenerOptions,
): void;
addEventListener(
type: "message",
listener: (event: ExplorerMessageEvent) => void,
options?: boolean | AddEventListenerOptions,
): void;
removeEventListener(
type: "message",
listener: (event: ExplorerMessageEvent) => void,
options?: boolean | EventListenerOptions,
): void;
addEventListener(
type: "open" | "popup" | "newwindow" | "navigation",
listener: (event: ExplorerOpenEvent) => void,
options?: boolean | AddEventListenerOptions,
): void;
removeEventListener(
type: "open" | "popup" | "newwindow" | "navigation",
listener: (event: ExplorerOpenEvent) => void,
options?: boolean | EventListenerOptions,
): void;
addEventListener(
type: "dialog",
listener: (event: ExplorerDialogEvent) => void,
options?: boolean | AddEventListenerOptions,
): void;
removeEventListener(
type: "dialog",
listener: (event: ExplorerDialogEvent) => void,
options?: boolean | EventListenerOptions,
): void;
}
export type CommandEntry = readonly [
path: string,
id: string,
schemaFingerprint: string,
request: readonly Codec[],
response: Codec,
];
export type ConstantEntry = readonly [path: string, value: unknown];
export function invokeBinary(
channel: string,
commandId: bigint,
schemaFingerprint: bigint,
payload?: Uint8Array,
): Promise<Uint8Array>;
export function createCommandApi(
entries: readonly (readonly [path: string, id: bigint])[],
typed?: boolean,
rootTarget?: object,
channelPrefix?: string,
): object;
export function createTypedCommandApi(
entries: readonly CommandEntry[],
constants?: readonly ConstantEntry[],
namespace?: string,
): object;
export type FrameworkEntry = readonly [
path: string,
request: readonly Codec[],
response: Codec,
];
export function createFrameworkApi(
entries: readonly FrameworkEntry[],
constants?: readonly ConstantEntry[],
): object;
tuffite/explorer
export type ExplorerNavigationPolicy = "emit" | "allow";
export type ExplorerDialogPolicy = "emit" | "suppress";
export type ExplorerJavaScriptWorld = "isolated" | "main";
export type ExplorerPartitionMode = "shared" | "persist" | "memory";
export interface ExplorerNavigationState {
readonly url: string;
readonly title: string;
readonly documentId: string;
readonly canGoBack: boolean;
readonly canGoForward: boolean;
readonly isLoading: boolean;
readonly rendererProcess: Readonly<{
id: number;
ownerId: number;
isolated: boolean;
}>;
readonly storagePartition: Readonly<{
domain: string;
name: string;
inMemory: boolean;
isDefault: boolean;
mode: ExplorerPartitionMode;
shared: boolean;
inherited: boolean;
requested: string;
createdDomain: string;
createdName: string;
createdInMemory: boolean;
createdIsDefault: boolean;
}>;
}
export interface ExplorerLoadErrorState extends ExplorerNavigationState {
readonly errorCode: number;
readonly errorDescription: string;
}
export interface ExplorerOpenDetail {
readonly url: string;
readonly disposition: string;
readonly userGesture: boolean;
readonly from: string;
}
export interface ExplorerDialogController {
readonly type: "alert" | "confirm" | "prompt" | "beforeunload";
readonly message: string;
readonly defaultValue: string;
readonly isReload: boolean;
readonly from: string;
accept(promptText?: string): Promise<boolean>;
dismiss(): Promise<boolean>;
}
export type ExplorerTransferState = "pending" | "attaching" | "transferred" | "closed";
/** Pending resources are owned by the source document, independently of GC. */
export interface ExplorerTransfer extends EventTarget {
readonly token: string;
readonly state: ExplorerTransferState;
close(): Promise<void>;
onstatechange: ((event: Event) => void) | null;
}
export interface ExplorerEventMap {
download: CustomEvent<ExplorerDownload>;
attached: Event;
detached: Event;
loadstart: CustomEvent<ExplorerNavigationState>;
loadstop: CustomEvent<ExplorerNavigationState>;
loadcommit: CustomEvent<ExplorerNavigationState>;
loaderror: CustomEvent<ExplorerLoadErrorState>;
open: CustomEvent<ExplorerOpenDetail>;
popup: CustomEvent<ExplorerOpenDetail>;
newwindow: CustomEvent<ExplorerOpenDetail>;
navigation: CustomEvent<ExplorerOpenDetail>;
dialog: CustomEvent<ExplorerDialogController>;
message: CustomEvent<unknown>;
}
export interface HTMLExplorerElement extends HTMLElement {
src: string;
/** Empty selects the parent's partition; `shared` is not an element value. */
partition: string;
navigation: ExplorerNavigationPolicy;
dialogs: ExplorerDialogPolicy;
contextMenuEnabled: boolean;
deferred: boolean;
detach(): Promise<ExplorerTransfer>;
attach(token: string): Promise<void>;
ondetached: ((event: Event) => void) | null;
onattached: ((event: Event) => void) | null;
/** Starts navigation; completion is reported by load events. */
load(url: string): void;
download(url: string): Promise<boolean>;
getDownloads(): Promise<ExplorerDownload[]>;
cancelDownload(id: number): Promise<boolean>;
/** Base64 bytes of an owned completed download, bounded to 64 MiB. */
readDownload(id: number): Promise<string>;
/** A compositor PNG data URL, or an empty string when unavailable. */
capturePreview(): Promise<string>;
back(): Promise<boolean>;
forward(): Promise<boolean>;
go(offset: number): Promise<boolean>;
reload(options?: Readonly<{ bypassCache?: boolean }>): Promise<boolean>;
stop(): Promise<boolean>;
getNavigationState(): Promise<ExplorerNavigationState>;
/** The caller must validate the JSON-representable result at runtime. */
executeJavaScript(
source: string,
options?: Readonly<{
world?: ExplorerJavaScriptWorld;
expectedDocumentId?: string;
}>,
): Promise<unknown>;
postMessage(data: unknown): Promise<boolean>;
addEventListener<K extends keyof ExplorerEventMap>(
type: K,
listener: (this: HTMLExplorerElement, event: ExplorerEventMap[K]) => void,
options?: boolean | AddEventListenerOptions,
): void;
removeEventListener<K extends keyof ExplorerEventMap>(
type: K,
listener: (this: HTMLExplorerElement, event: ExplorerEventMap[K]) => void,
options?: boolean | EventListenerOptions,
): void;
}
declare global {
interface HTMLElementTagNameMap {
explorer: HTMLExplorerElement;
}
}
export type ExplorerDownload = {
id: number;
url: string;
filename: string;
path: string;
mimeType: string;
state: "in-progress" | "complete" | "cancelled" | "interrupted";
receivedBytes: number;
totalBytes: number;
error: number;
navigation: boolean;
};
tuffite/vite
export interface TuffiteViteOptions {
manifest?: string;
root?: string;
static?: Readonly<Record<string, string>>;
shellApi?: false | Readonly<{ url: string; file: string }>;
}
export function tuffite(options?: TuffiteViteOptions): import("vite").Plugin;
export function findManifest(start: string): string | undefined;
export function contentTypeForPath(path: string): string | undefined;
export function decodeRequestPath(requestUrl: string | undefined): string | undefined;
export function resolveConfinedFile(root: string, path: string): Promise<string | undefined>;
Framework 模块#
展开模块可以查看完整源码声明,包括参数和返回值模型。
tuffite.common0 个方法
/** Filesystem roots accepted by path and fs APIs. */
export enum BaseDirectory {
Resource = "resource",
Data = "data",
Cache = "cache",
Temp = "temp",
}
tuffite.dialog2 个方法
openFile(options?: OpenFileDialogOptions): Promise<string | undefined>;openDirectory(options?: OpenDirectoryDialogOptions): Promise<string | undefined>;export interface OpenFileDialogOptions {
readonly title?: string;
readonly defaultPath?: string;
/** Extension groups without leading dots, for example [["html", "htm"], ["md"]]. */
readonly extensions?: string[][];
}
export interface OpenDirectoryDialogOptions {
readonly title?: string;
}
export declare namespace tuffite {
export namespace dialog {
export function openFile(options?: OpenFileDialogOptions): Promise<string | undefined>;
export function openDirectory(options?: OpenDirectoryDialogOptions): Promise<string | undefined>;
}
}
tuffite.fs5 个方法
readFile(base: BaseDirectory, path: string): Promise<Uint8Array>;writeFile(
base: BaseDirectory,
path: string,
contents: Uint8Array,
): Promise<void>;exists(base: BaseDirectory, path: string): Promise<boolean>;createDir(base: BaseDirectory, path: string): Promise<void>;removeFile(base: BaseDirectory, path: string): Promise<void>;export declare namespace tuffite {
export namespace fs {
export function readFile(base: BaseDirectory, path: string): Promise<Uint8Array>;
export function writeFile(
base: BaseDirectory,
path: string,
contents: Uint8Array,
): Promise<void>;
export function exists(base: BaseDirectory, path: string): Promise<boolean>;
export function createDir(base: BaseDirectory, path: string): Promise<void>;
export function removeFile(base: BaseDirectory, path: string): Promise<void>;
}
}
tuffite.log5 个方法
trace(message: string): Promise<void>;debug(message: string): Promise<void>;info(message: string): Promise<void>;warn(message: string): Promise<void>;error(message: string): Promise<void>;export declare namespace tuffite {
export namespace log {
export function trace(message: string): Promise<void>;
export function debug(message: string): Promise<void>;
export function info(message: string): Promise<void>;
export function warn(message: string): Promise<void>;
export function error(message: string): Promise<void>;
}
}
tuffite.network1 个方法
request(url: string): Promise<NetworkResponse>;export interface NetworkResponse {
readonly status: number;
readonly body: Uint8Array;
}
export declare namespace tuffite {
export namespace net {
export function request(url: string): Promise<NetworkResponse>;
}
}
tuffite.path2 个方法
directories(): Promise<PathDirectories>;resolve(base: BaseDirectory, relative?: string): Promise<string>;export interface PathDirectories {
readonly resource: string;
readonly data: string;
readonly cache: string;
readonly temp: string;
}
export declare namespace tuffite {
export namespace path {
export function directories(): Promise<PathDirectories>;
export function resolve(base: BaseDirectory, relative?: string): Promise<string>;
}
}
tuffite.remoting9 个方法
capabilities(): Promise<RemotingCapabilities>;start(options: RemotingStartOptions): Promise<RemotingSession>;processSignal(id: bigint, signal: RemotingSignal): Promise<void>;takeSignals(id: bigint, afterSequence?: bigint): Promise<RemotingSignal[]>;sendInput(id: bigint, event: RemotingInputEvent): Promise<void>;stop(id: bigint): Promise<void>;stats(id: bigint): Promise<RemotingStats>;capture(screenId?: bigint): Promise<RemotingSnapshot>;injectInput(event: RemotingInputEvent): Promise<void>;export interface RemotingCapabilities {
readonly available: boolean;
readonly protocol: string;
readonly videoCodecs: string[];
readonly input: boolean;
readonly clipboard: boolean;
readonly reconnect: boolean;
readonly snapshot: boolean;
}
export interface RemotingIceServer {
readonly urls: string[];
readonly username?: string;
readonly credential?: string;
}
export interface RemotingStartOptions {
/** Per-session secret supplied by the application's authenticated control plane. */
readonly authKey: string;
readonly iceServers: RemotingIceServer[];
readonly maxBitrateKbps?: number;
readonly targetFramerate?: number;
readonly screenId?: bigint;
}
export interface RemotingSession {
readonly id: bigint;
readonly state: string;
}
export interface RemotingSessionDescription {
readonly type: string;
readonly sdp: string;
}
export interface RemotingIceCandidate {
readonly mid: string;
readonly mLineIndex: number;
readonly candidate: string;
}
export interface RemotingSignal {
readonly sequence: bigint;
readonly description?: RemotingSessionDescription;
readonly candidates: RemotingIceCandidate[];
}
export interface RemotingInputEvent {
readonly kind: string;
readonly pressed?: boolean;
readonly usbKeycode?: number;
readonly text?: string;
readonly x?: number;
readonly y?: number;
readonly button?: string;
readonly buttonDown?: boolean;
readonly wheelDeltaX?: number;
readonly wheelDeltaY?: number;
readonly wheelTicksX?: number;
readonly wheelTicksY?: number;
readonly clipboardMimeType?: string;
readonly clipboardData?: Uint8Array;
}
export interface RemotingStats {
readonly state: string;
readonly route: string;
readonly transport: string;
readonly latencyMs: number;
readonly jitterMs: number;
readonly availableBandwidthKbps: number;
readonly packetLossPercent: number;
}
export interface RemotingSnapshot {
readonly width: number;
readonly height: number;
readonly mimeType: string;
readonly data: Uint8Array;
}
export declare namespace tuffite {
export namespace remoting {
export function capabilities(): Promise<RemotingCapabilities>;
export function start(options: RemotingStartOptions): Promise<RemotingSession>;
export function processSignal(id: bigint, signal: RemotingSignal): Promise<void>;
export function takeSignals(id: bigint, afterSequence?: bigint): Promise<RemotingSignal[]>;
export function sendInput(id: bigint, event: RemotingInputEvent): Promise<void>;
export function stop(id: bigint): Promise<void>;
export function stats(id: bigint): Promise<RemotingStats>;
export function capture(screenId?: bigint): Promise<RemotingSnapshot>;
/** Native application callers only; renderer calls are denied by routing policy. */
export function injectInput(event: RemotingInputEvent): Promise<void>;
}
}
tuffite.storage4 个方法
get(key: string): Promise<Uint8Array | undefined>;set(key: string, value: Uint8Array): Promise<void>;remove(key: string): Promise<void>;clear(): Promise<void>;export declare namespace tuffite {
export namespace storage {
export function get(key: string): Promise<Uint8Array | undefined>;
export function set(key: string, value: Uint8Array): Promise<void>;
export function remove(key: string): Promise<void>;
export function clear(): Promise<void>;
}
}
tuffite.utility2 个方法
invoke(
name: string,
requestId: bigint,
payload?: Uint8Array,
): Promise<Uint8Array>;cancel(name: string, requestId: bigint): Promise<void>;export declare namespace tuffite {
export namespace utility {
export function invoke(
name: string,
requestId: bigint,
payload?: Uint8Array,
): Promise<Uint8Array>;
export function cancel(name: string, requestId: bigint): Promise<void>;
}
}
tuffite.window12 个方法
current(): Promise<WindowInfo>;list(): Promise<WindowInfo[]>;create(options?: WindowOptions): Promise<WindowInfo>;focus(id?: WindowId): Promise<WindowInfo>;minimize(id?: WindowId): Promise<WindowInfo>;maximize(id?: WindowId): Promise<WindowInfo>;restore(id?: WindowId): Promise<WindowInfo>;setFullscreen(fullscreen: boolean, id?: WindowId): Promise<WindowInfo>;close(id?: WindowId): Promise<void>;setTitle(title: string, id?: WindowId): Promise<WindowInfo>;getBounds(id?: WindowId): Promise<WindowBounds>;setBounds(bounds: WindowBounds, id?: WindowId): Promise<WindowInfo>;/** Opaque browser-process identifier. It is intentionally not a native handle. */
export type WindowId = bigint;
export interface WindowBounds {
readonly x: number;
readonly y: number;
readonly width: number;
readonly height: number;
}
export interface WindowInfo {
readonly id: WindowId;
readonly label: string;
readonly title: string;
readonly url: string;
readonly bounds: WindowBounds;
readonly focused: boolean;
readonly minimized: boolean;
readonly maximized: boolean;
readonly fullscreen: boolean;
}
export interface WindowOptions {
readonly label?: string;
readonly url?: string;
readonly title?: string;
readonly bounds?: WindowBounds;
readonly focused?: boolean;
}
export declare namespace tuffite {
export namespace window {
export function current(): Promise<WindowInfo>;
export function list(): Promise<WindowInfo[]>;
export function create(options?: WindowOptions): Promise<WindowInfo>;
export function focus(id?: WindowId): Promise<WindowInfo>;
export function minimize(id?: WindowId): Promise<WindowInfo>;
export function maximize(id?: WindowId): Promise<WindowInfo>;
export function restore(id?: WindowId): Promise<WindowInfo>;
export function setFullscreen(fullscreen: boolean, id?: WindowId): Promise<WindowInfo>;
export function close(id?: WindowId): Promise<void>;
export function setTitle(title: string, id?: WindowId): Promise<WindowInfo>;
export function getBounds(id?: WindowId): Promise<WindowBounds>;
export function setBounds(bounds: WindowBounds, id?: WindowId): Promise<WindowInfo>;
}
}