Ztron's frontend layer is a standard Vite project and is framework-agnostic.
React, Vue, Svelte, Solid, Tailwind CSS, and the rest of the frontend
ecosystem plug in directly. This page uses examples/react-demo
(React 19 + Tailwind CSS v4), examples/vue-demo (Vue 3 + Tailwind CSS
v4), and examples/svelte-demo (Svelte 5 + Tailwind CSS v4) as living
examples to cover the integration recipe and the bundling constraints.
ztron dev / ztron build let the CLI build its own Vite dev server /
build, and the ztronVitePlugin injects the __ZTRON_INTERNALS__ bridge
script into index.html via transformIndexHtml. Vite merges the project's
own frontend/vite.config.ts, so third-party plugins such as react() and
tailwindcss() take effect as soon as you list them. The project config does
not need to repeat base / output.format (the CLI enforces ./ and
iife), and you must not add the __ZTRON_INTERNALS__ bootstrap script
yourself (the CLI injects it).
The only contract between frontend and backend is @zturnlibs/ztron-api: an
ordinary npm package whose invoke, events, Channel, fs/path/window, and
other APIs are plain ESM exports, independent of any framework. Components,
routing, and state management are entirely your choice.
Taking examples/react-demo as the example, the dependencies are plain
frontend dependencies:
Note the vite entry in devDependencies: frontend/vite.config.ts needs
import { defineConfig } from "vite", and under pnpm's strict isolation the
config file itself must be able to resolve it, so the project installs its
own vite (same 6.x major as the CLI, the same instance inside the lockfile).
frontend/vite.config.ts only declares the third-party plugins:
The entry frontend/src/main.tsx is a plain React entry with no Ztron-specific
initialization; the bridge is already in place from the index.html stage:
One point about code layout: the backend TS sources live in src/, outside
the Vite root (frontend/). Both the Vite dev server and build only serve
modules inside the root, so at runtime you cannot reach across the root to
import them. Therefore:
invoke from @zturnlibs/ztron-api
directly, e.g. invoke<string>("react-demo:greet", { name });ztron codegen
(src/ztron-commands.ts) are shown as a code block for their reference
shape, for business projects that point the frontend root at the repo root
and can import them directly:React 19's StrictMode runs effects twice in development
(mount → cleanup → mount), deliberately to surface leaks. Ztron's
subscription-style APIs (listen returns an UnlistenFn; geolocation's
watchPosition pairs with clearWatch) therefore come with one hard rule:
the unlisten/teardown must be returned from cleanup, otherwise a double
mount leaves duplicate listeners and duplicate callbacks behind.
frontend/src/hooks.ts in react-demo provides three hooks you can copy as-is
(extracted into the official package below; the demo now re-exports it):
Typical usage inside components:
Official adapter packages: the React hooks, the Vue composables and the Svelte listener helpers are extracted into the official packages
@zturnlibs/ztron-react,@zturnlibs/ztron-vueand@zturnlibs/ztron-svelte. All three depend only on@zturnlibs/ztron-api, their peer dependencies match the framework (react >= 18,vue ^3.5,svelte ^5), and all three demos consume them via re-export shims. One-line usage for each package:The react package is tested against React 19 (React 18 works too); the vue package tears everything down in
onScopeDispose(fires on component unmount and oneffectScope.stop()alike); the svelte package also ships a standalonesubscribe(returns the cleanup synchronously, usable outside components).
examples/vue-demo replicates the full react-demo showcase (five tabs:
backend calls, events, Channel, theme, system) in Vue 3, proving that the
same pipeline holds for SFC single-file components. The dependencies are
again plain frontend dependencies:
The vite note is the same as for React (the config file itself must
resolve it; same 6.x major as the CLI). @vitejs/plugin-vue uses the
vite-6-compatible 6.x major.
frontend/vite.config.ts just swaps the React plugin for the Vue one:
The entry frontend/src/main.ts is a plain Vue entry; the bridge is already
in place from the index.html stage:
Single-file components and type checking: components are written with
<script setup lang="ts">, and the typecheck script becomes
vue-tsc --noEmit (vue-tsc understands .vue files directly; the tsconfig
needs no jsx setting, and include only has to cover src and
frontend/src). The across-the-root constraint from react-demo applies to
Vue as well: runtime calls go through invoke<string>("vue-demo:greet", { name }) directly, with the codegen bindings shown only as a reference
shape.
The composables cleanup convention: React's hooks cleanup rule translates
to Vue as the unlisten must be torn down in onScopeDispose (fires on
component unmount and on effectScope.stop() alike).
frontend/src/composables.ts in vue-demo provides three composables you
can copy as-is (extracted into @zturnlibs/ztron-vue; the demo re-exports
the package):
defineAsyncComponent and IIFE inlining: Vue's lazy-loading idiom is
defineAsyncComponent(() => import("./LazyPane.vue")). Like React.lazy,
dynamic import() is inlined into the main bundle by the single-file IIFE
artifact of ztron build (vue-demo verifies this with the VUE_LAZY_OK
marker string inside LazyPane), and there is likewise no real code
splitting.
To start from a scaffold, ztron init --template vue-ts generates the same
minimal project (see the CLI Reference).
examples/svelte-demo replicates the full react-demo showcase (five tabs:
backend calls, events, Channel, theme, system) in Svelte 5, proving that the
same pipeline holds for Svelte single-file components. The dependencies are
again plain frontend dependencies:
The vite note is the same as for React and Vue (the config file itself
must resolve it; same 6.x major as the CLI). @sveltejs/vite-plugin-svelte
uses the vite-6-compatible 5.x major (the plugin's 6.x targets vite 7).
frontend/vite.config.ts just swaps the framework plugin for the Svelte
one:
The entry frontend/src/main.ts is a plain Svelte 5 entry using the
mount idiom (which replaces the Svelte 4 new App({...}) constructor
style); the bridge is already in place from the index.html stage:
Components and type checking: components are written with
<script lang="ts"> plus Svelte 5 runes, with UI state declared via
$state ($derived available). The typecheck script is
svelte-check --tsconfig ./tsconfig.json --config ./svelte.config.js, and
the two configs split the work: during dev / build the Svelte 5 compiler
understands erasable lang="ts" syntax natively, so the vite plugin
compiles directly without preprocessing; the root svelte.config.js
(with vitePreprocess) is read by svelte-check and must be pinned with
--config, otherwise svelte-check's upward search from frontend/src
hits frontend/vite.config.ts first and fails with "No Svelte
configuration found". The across-the-root constraint from react/vue
applies to Svelte as well: runtime calls go through
invoke<string>("svelte-demo:greet", { name }) directly, with the codegen
bindings shown only as a reference shape.
The runes cleanup convention: React/Vue's cleanup rule translates to Svelte
as the subscription starts during component initialisation and the
unlisten is torn down in onDestroy. frontend/src/lib/listeners.ts in
svelte-demo provides a listener helper you can copy as-is (extracted into
@zturnlibs/ztron-svelte; the demo re-exports the package; it only uses
lifecycle hooks and no runes, so a plain .ts module is enough):
{#await import} and IIFE inlining: Svelte's lazy-loading idiom is an await block that consumes the dynamic import's module namespace and destructures the component out of it:
Like React.lazy and defineAsyncComponent, dynamic import() is inlined
into the main bundle by the single-file IIFE artifact of ztron build
(svelte-demo verifies this with the SVELTE_LAZY_OK marker string inside
LazyPane), and there is likewise no real code splitting.
To start from a scaffold, ztron init --template svelte generates the same
minimal project (see the CLI Reference).
Tailwind v4 plugs in with a single @tailwindcss/vite line (the
tailwindcss() above); the entry CSS only needs @import "tailwindcss";.
<style>
elements, which the default CSP's style-src 'self' 'unsafe-inline'
already allows; no extra configuration needed.dark: variant follows CSS
prefers-color-scheme by default, and inside the WebView
prefers-color-scheme follows the window's appearance. Calling
getCurrentWebviewWindow().setTheme("dark" | "light" | null) switches the
window appearance and the page palette flips immediately (null means
follow the system). The react-demo "Theme" tab demonstrates this.The ztron build frontend artifact is a single-file IIFE + classic
script: under the file:// opaque origin the WebView refuses to execute
module scripts, so the CLI enforces IIFE output. Three constraints follow:
import() is inlined into the main bundle. React.lazy +
Suspense still work (react-demo's LazyPane does exactly this), but there
is no real code splitting; do not expect on-demand loading to shrink the
initial bundle.<style> (covered by the default CSP).ztron dev runs a Vite dev server that serves
modules normally with HMR; the constraints above only apply to the build
artifact.The default CSP injected into the built index.html is:
ws://localhost:* covers the React dev server and the HMR websocket, and
style-src 'unsafe-inline' covers runtime style injection from Tailwind and
component libraries, so the React development experience works out of the
box. Remote images, fonts, and similar resources need img-src / font-src
extensions in ztron.conf.json's app.security.csp (or devCsp during
development); see the Security Model.
The WebView loads a purely static SPA: there is no Node server and no SSR
pipeline. Meta-frameworks premised on server-side rendering, such as
Next.js, Nuxt, and Remix, do not apply. SPA routing (react-router and
friends) works, but file:// has no server-side path fallback, so a
hash/memory routing mode that does not depend on server paths is the safest
choice.
Solid (vite-plugin-solid) and other frameworks use their official Vite
plugins; list them in the plugins array of frontend/vite.config.ts and
everything else follows the exact same pattern as React, Vue, and Svelte:
the bridge is injected by the CLI, the config is merged by Vite, and
runtime calls go through @zturnlibs/ztron-api. The subscription/request
cleanup convention applies equally; wrap it for each framework's lifecycle
following the react-demo hooks.ts, the vue-demo composables.ts, or the
svelte-demo listeners.ts pattern.
Deep dive: Examples · Calling Backend Commands · CLI Reference
适用版本:ztron 0.3.1