Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ jobs:
- run: bunx tsc --noEmit -p packages/webtau/tsconfig.json
- run: bunx tsc --noEmit -p packages/webtau-vite/tsconfig.json
- run: bunx tsc --noEmit -p packages/create-gametau/tsconfig.json
- run: bunx tsc --noEmit -p examples/electrobun-counter/tsconfig.json
- run: bunx tsc --noEmit -p examples/battlestation/tsconfig.json

lint:
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/).

### Added
- `webtau/adapters/electrobun`: `bootstrapElectrobunFromWindowBridge()`, `createElectrobunWindowBridgeProvider()`, `isElectrobun()`, and `getElectrobunCapabilities()` for first-class Electrobun runtime detection and bridge bootstrap.
- `webtau`: `getRuntimeInfo()` for runtime id and capability introspection across WASM, Tauri, and provider-backed runtimes.
- `create-gametau`: `--desktop-shell electrobun` and `--electrobun-mode hybrid|native|dual`.
- Electrobun counter example BrowserWindow and GPUWindow build lanes.

Expand Down
23 changes: 18 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -332,11 +332,24 @@ if (!isTauri()) {
}
```

#### `isTauri()`

Returns `true` when running inside Tauri (checks `window.__TAURI_INTERNALS__`).

#### `wasm_state!(Type)` (Rust crate)
#### `isTauri()`

Returns `true` when running inside Tauri (checks `window.__TAURI_INTERNALS__`).

#### `getRuntimeInfo()`

Returns the active runtime id plus capability flags for capability-based branching.

```typescript
import { getRuntimeInfo } from "webtau";

const runtime = getRuntimeInfo();
// { id: "wasm" | "tauri" | "electrobun" | string, platform, capabilities }
```

Use this when a template or app needs to distinguish shell/render mode without relying on ad hoc globals.

#### `wasm_state!(Type)` (Rust crate)

Generates thread-local state management for WASM. Replaces Tauri's `State<Mutex<T>>` for the browser target.

Expand Down
7 changes: 3 additions & 4 deletions examples/electrobun-counter/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { configure, isTauri } from "webtau";
import { configure, getRuntimeInfo, isTauri } from "webtau";
import {
getElectrobunCapabilities,
bootstrapElectrobunFromWindowBridge,
} from "webtau/adapters/electrobun";
import { setupElectrobunHybridWgpuWhenReady } from "./hybrid-wgpu";
Expand All @@ -12,9 +11,9 @@ async function main() {
let hybridHandle: Awaited<ReturnType<typeof setupElectrobunHybridWgpuWhenReady>> = null;

if (bootstrapElectrobunFromWindowBridge()) {
const capabilities = getElectrobunCapabilities();
hybridHandle = await setupElectrobunHybridWgpuWhenReady();
const renderMode = hybridHandle?.renderMode ?? capabilities?.renderMode ?? "browser";
const runtime = getRuntimeInfo();
const renderMode = hybridHandle?.renderMode ?? runtime.capabilities.renderMode ?? "browser";
modeEl.textContent = `Electrobun bridge (${renderMode})`;
} else if (!isTauri()) {
modeEl.textContent = "WASM (web)";
Expand Down
1 change: 1 addition & 0 deletions packages/create-gametau/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ Runtime bootstrap is explicit in scaffolded `src/index.ts`:
- Electrobun path: `bootstrapElectrobunFromWindowBridge()` from `webtau/adapters/electrobun`
- Tauri path: `bootstrapTauri()` from `webtau/adapters/tauri`
- Web path: `configure({ loadWasm })` from `webtau`
- Runtime/capability seam: `getRuntimeInfo()` from `webtau`

This keeps the generated project lightweight while giving contributors clear extension points for production features.

Expand Down
8 changes: 7 additions & 1 deletion packages/create-gametau/templates/base/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { configure, isTauri } from "webtau";
import { configure, getRuntimeInfo, isTauri } from "webtau";
import { bootstrapElectrobunFromWindowBridge } from "webtau/adapters/electrobun";
import { bootstrapTauri } from "webtau/adapters/tauri";
import { startGameLoop } from "./game/loop";
Expand Down Expand Up @@ -43,6 +43,12 @@ async function main() {
});
}

const runtime = getRuntimeInfo();
document.body.dataset.runtime = runtime.id;
if (runtime.capabilities.renderMode) {
document.body.dataset.renderMode = runtime.capabilities.renderMode;
}

// Set up the renderer
const app = document.getElementById("app")!;
await initScene(app);
Expand Down
28 changes: 28 additions & 0 deletions packages/webtau/src/adapters/electrobun.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import type {
DialogAdapter,
EventAdapter,
FsAdapter,
RuntimeCapabilities,
WindowAdapter,
} from "../provider.js";
import { setWindowAdapter } from "../window.js";
Expand Down Expand Up @@ -99,6 +100,23 @@ export function getElectrobunCapabilities(): ElectrobunCapabilities | null {
};
}

function getElectrobunRuntimeCapabilities(): RuntimeCapabilities {
const capabilities = getElectrobunCapabilities();

return {
events: true,
fs: true,
dialog: true,
window: true,
task: true,
convertFileSrc: true,
renderMode: capabilities?.renderMode ?? "unknown",
hasGpuWindow: capabilities?.hasGpuWindow ?? false,
hasWgpuView: capabilities?.hasWgpuView ?? false,
hasWebGpu: capabilities?.hasWebGpu ?? false,
};
}

export function createElectrobunWindowBridgeProvider(
bridge: ElectrobunBridge = getElectrobunBridge() as ElectrobunBridge,
): CoreProvider {
Expand All @@ -116,6 +134,11 @@ export function createElectrobunWindowBridgeProvider(
? bridge.convertFileSrc(filePath, protocol)
: `electrobun://asset/${filePath.replace(/^\/+/, "")}`
),
runtimeInfo: {
id: "electrobun",
platform: "desktop",
capabilities: getElectrobunRuntimeCapabilities(),
},
};
}

Expand Down Expand Up @@ -370,6 +393,11 @@ export function createElectrobunCoreProvider(): CoreProvider {
convertFileSrc: (filePath: string): string => {
return `electrobun://asset/${filePath.replace(/^\/+/, "")}`;
},
runtimeInfo: {
id: "electrobun",
platform: "desktop",
capabilities: getElectrobunRuntimeCapabilities(),
},
};
}

Expand Down
46 changes: 29 additions & 17 deletions packages/webtau/src/adapters/tauri.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@
* ```
*/

import { registerProvider } from "../core.js";
import type { EventCallback, UnlistenFn } from "../event.js";
import { setEventAdapter } from "../event.js";
import type { CoreProvider, EventAdapter } from "../provider.js";
import { registerProvider } from "../core.js";
import type { EventCallback, UnlistenFn } from "../event.js";
import { setEventAdapter } from "../event.js";
import type { CoreProvider, EventAdapter } from "../provider.js";

// ── Typed shapes for dynamically imported Tauri modules ─────────────────────
// @tauri-apps/api is an optional peer dependency. These interfaces describe
Expand Down Expand Up @@ -64,19 +64,31 @@ export function createTauriEventAdapter(): EventAdapter {
// Prefer auto-detection (isTauri() path in invoke()) over explicit registration
// unless you need to force Tauri mode or test with a mock provider.

export function createTauriCoreProvider(): CoreProvider {
return {
id: "tauri",
invoke: async <T = unknown>(command: string, args?: Record<string, unknown>): Promise<T> => {
const mod = await import("@tauri-apps/api/core" as string) as TauriCoreModule;
return mod.invoke<T>(command, args);
},
convertFileSrc: (filePath: string, protocol?: string): string => {
const proto = protocol ?? "asset";
return `${proto}://localhost${filePath.startsWith("/") ? filePath : `/${filePath}`}`;
},
};
}
export function createTauriCoreProvider(): CoreProvider {
return {
id: "tauri",
invoke: async <T = unknown>(command: string, args?: Record<string, unknown>): Promise<T> => {
const mod = await import("@tauri-apps/api/core" as string) as TauriCoreModule;
return mod.invoke<T>(command, args);
},
convertFileSrc: (filePath: string, protocol?: string): string => {
const proto = protocol ?? "asset";
return `${proto}://localhost${filePath.startsWith("/") ? filePath : `/${filePath}`}`;
},
runtimeInfo: {
id: "tauri",
platform: "desktop",
capabilities: {
events: true,
fs: true,
dialog: true,
window: true,
task: true,
convertFileSrc: true,
},
},
};
}

// ── Bootstrap ─────────────────────────────────────────────────────────────────

Expand Down
157 changes: 157 additions & 0 deletions packages/webtau/src/core.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
configure,
convertFileSrc,
getProvider,
getRuntimeInfo,
invoke,
isTauri,
registerProvider,
Expand All @@ -21,6 +22,162 @@ describe("isTauri", () => {
});
});

describe("getRuntimeInfo", () => {
const globalObj = globalThis as Record<string, unknown>;
let hadWindow = false;
let previousWindow: unknown;

beforeEach(() => {
hadWindow = Object.hasOwn(globalObj, "window");
previousWindow = globalObj.window;
resetProvider();
});

afterEach(() => {
resetProvider();
if (hadWindow) {
globalObj.window = previousWindow;
} else {
delete globalObj.window;
}
});

test("returns wasm runtime info by default", () => {
expect(getRuntimeInfo()).toEqual({
id: "wasm",
platform: "web",
capabilities: {
events: true,
fs: true,
dialog: true,
window: true,
task: true,
convertFileSrc: true,
},
});
});

test("returns tauri runtime info from environment auto-detection", () => {
globalObj.window = { __TAURI_INTERNALS__: {} };

expect(getRuntimeInfo()).toEqual({
id: "tauri",
platform: "desktop",
capabilities: {
events: true,
fs: true,
dialog: true,
window: true,
task: true,
convertFileSrc: true,
},
});
});

test("returns explicit provider runtime info when supplied", () => {
registerProvider({
id: "custom",
invoke: async () => null,
convertFileSrc: (path) => path,
runtimeInfo: {
id: "custom",
platform: "desktop",
capabilities: {
events: false,
fs: false,
dialog: false,
window: false,
task: true,
convertFileSrc: true,
},
},
});

expect(getRuntimeInfo()).toEqual({
id: "custom",
platform: "desktop",
capabilities: {
events: false,
fs: false,
dialog: false,
window: false,
task: true,
convertFileSrc: true,
},
});
});

test("resolves function-form runtimeInfo lazily", () => {
registerProvider({
id: "custom-lazy",
invoke: async () => null,
convertFileSrc: (path) => path,
runtimeInfo: () => ({
id: "custom-lazy",
platform: "desktop",
capabilities: {
events: true,
fs: false,
dialog: false,
window: false,
task: true,
convertFileSrc: true,
},
}),
});

expect(getRuntimeInfo()).toEqual({
id: "custom-lazy",
platform: "desktop",
capabilities: {
events: true,
fs: false,
dialog: false,
window: false,
task: true,
convertFileSrc: true,
},
});
});

test("derives Electrobun render-mode info from the exposed bridge", () => {
globalObj.window = {
__ELECTROBUN__: {
invoke: async () => null,
renderMode: "hybrid",
capabilities: {
hasGpuWindow: false,
hasWgpuView: true,
hasWebGpu: true,
},
},
};

registerProvider({
id: "electrobun",
invoke: async () => null,
convertFileSrc: (path) => path,
});

expect(getRuntimeInfo()).toEqual({
id: "electrobun",
platform: "desktop",
capabilities: {
events: true,
fs: true,
dialog: true,
window: true,
task: true,
convertFileSrc: true,
renderMode: "hybrid",
hasGpuWindow: false,
hasWgpuView: true,
hasWebGpu: true,
},
});
});
});

// ---------------------------------------------------------------------------
// invoke — web/WASM mode
// ---------------------------------------------------------------------------
Expand Down
Loading