Authoring Extensions
Extensions can provide ordinary Solid components, retained native components, or asynchronous native services. Editor, Markdown, and Terminal are optional component packages; Updater is an optional service. Third-party packages use the same SDK without changing quickgui-host.
#Choose an extension type
Use an ordinary TypeScript package for reusable Solid UI. Use a native component when its state or rendering logic belongs in Rust, or a native service for asynchronous operations. Native libraries run in the application's existing process and use its renderer; they do not bundle another host.
#Create an extension project
quickgui init-extension my-components --type go
quickgui init-extension my-service --type rustTypeScript apps consume the same native packages as Go. --npm-package sets the artifact package name.
#Share a component
import { Button, type JSX } from "@quickgui/solid";
export function PrimaryButton(props: JSX.NativeProps) {
return (
<Button
{...props}
style={[props.style, { bg: "#2563eb", color: "#ffffff", roundedLg: true, p3: true }]}
/>
);
}Publish the package and add it as an application dependency.
#Lay out a native extension
A combined extension can be both a Go module and an npm package. Put Rust in src/, TypeScript in js/, and built libraries in lib/<platform>-<arch>/. The root manifest names your package, independent version, ABI, and library. See the Go authoring guide and the first-party extensions/ packages.
Cargo.toml
go.mod
badge.go
package.json
quickgui.extension.json
src/
lib.rs
js/
index.ts
lib/
darwin-arm64/libquickgui_acme_badge.dylib#Declare dependencies
bun add @quickgui/extension-terminalimport { defineConfig } from "@quickgui/cli";
export default defineConfig({
language: "typescript",
name: "Terminal App",
identifier: "com.example.terminal",
extensions: ["@quickgui/extension-terminal"],
});Custom extensions may use a scoped name such as @acme/extension-echo.
#Implement the native contract
Native components implement create, update, render, event, and destroy. Rust authors use Component, ComponentFactory, and ComponentRuntime from quickgui-extension-sdk, which has no renderer dependency. Render stable keyed primitives, request only visible virtual rows, and wake only the owning instance when background work completes. Release state and wake handles when it is destroyed.
A TypeScript binding mounts the registered component and supplies reactive properties:
import { ExtensionComponent } from "@quickgui/solid";
export function Badge(props: { label: string }) {
return ExtensionComponent({
package: "acme-badge",
component: "badge",
get properties() { return { label: props.label }; },
});
}onEvent(kind, value, event) receives the component's output; the binding owns its typed public API. The host never interprets package-specific properties.
Native services instead implement invoke and shutdown. Copy borrowed input before returning, keep events scoped to their sessions, and release workers when a session stops.
#Automatic registration
The CLI bundles declared extensions. The app loads them at startup, so components can call them after app.whenReady().
#Build and package artifacts
Rebuild the native library when its implementation changes, then rebuild the app. Resources listed in the manifest are copied next to the library. Export js/index.ts from the npm package. Keep @quickgui/* and Solid in peerDependencies, with development copies in devDependencies; do not bundle another QuickGUI runtime. Extension READMEs contain build instructions only; the user guide belongs on the website.
#Test and distribute
import { ExtensionSession, invokeExtension } from "@quickgui/native";
const result = await invokeExtension("acme-service", "lookup", { key: "example" });
const session = new ExtensionSession("acme-service", {}, (event) => console.log(event));
await session.ready;
await session.request("refresh", null);
await session.close();Test against a packed extension in a fresh project. Confirm that an app without the package does not bundle the library.
