From bda6792d5fa5124d0ff1870ff9aebc7fdf0b6d86 Mon Sep 17 00:00:00 2001 From: Mike Orozco Date: Tue, 21 Jul 2026 09:23:50 -0500 Subject: [PATCH] fix(host): reinject renderer after reload --- README.md | 8 ++++---- docs/extension-development.md | 4 ++-- package.json | 2 +- scripts/check-sdk-package.ts | 2 +- sdk/package.json | 2 +- src/host/bootstrap.ts | 6 +++--- src/host/renderer-loads.ts | 13 +++++++++++++ src/shared/constants.ts | 2 +- tests/fixtures/serve-renderer-harness.ts | 2 +- tests/renderer-loads.test.ts | 20 ++++++++++++++++++++ 10 files changed, 47 insertions(+), 14 deletions(-) create mode 100644 src/host/renderer-loads.ts create mode 100644 tests/renderer-loads.test.ts diff --git a/README.md b/README.md index 44974b4..c8c30ae 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ Release executables are currently unsigned. Windows SmartScreen may warn before 3. Verify the archive: ```powershell - $zip = ".\zcode-extensions-v0.3.2-windows-x64.zip" + $zip = ".\zcode-extensions-v0.3.3-windows-x64.zip" $expected = (Get-Content "$zip.sha256").Split()[0].ToLowerInvariant() $actual = (Get-FileHash $zip -Algorithm SHA256).Hash.ToLowerInvariant() if ($actual -ne $expected) { throw "Checksum mismatch" } @@ -52,7 +52,7 @@ Release executables are currently unsigned. Windows SmartScreen may warn before 4. Extract the archive to a permanent location. The ZIP contains a stable `zcode-extensions` directory: ```powershell - Expand-Archive .\zcode-extensions-v0.3.2-windows-x64.zip -DestinationPath D:\ + Expand-Archive .\zcode-extensions-v0.3.3-windows-x64.zip -DestinationPath D:\ Set-Location D:\zcode-extensions ``` @@ -110,7 +110,7 @@ Queued updates replace only the extension bundle on startup. The previous bundle Close ZCode, download the new release, and extract it over the same parent directory: ```powershell -Expand-Archive .\zcode-extensions-v0.3.2-windows-x64.zip -DestinationPath D:\ -Force +Expand-Archive .\zcode-extensions-v0.3.3-windows-x64.zip -DestinationPath D:\ -Force Set-Location D:\zcode-extensions .\bin\zdp.exe repair .\bin\zdp.exe launch @@ -181,7 +181,7 @@ bun run build:example bun run build bun run build:sdk bun run pack:sdk -bun run release:package -- --tag v0.3.2 +bun run release:package -- --tag v0.3.3 ``` See [Developing extensions](docs/extension-development.md) for the public API and [Hello Extension](examples/hello-extension) for a complete minimal project. diff --git a/docs/extension-development.md b/docs/extension-development.md index f0e7de5..6a1f84a 100644 --- a/docs/extension-development.md +++ b/docs/extension-development.md @@ -1,6 +1,6 @@ # Developing ZCode Desktop Extensions -This guide describes extension API version 1 as implemented by host/SDK 0.3.2. The public package is [`@notmike101/zcode-extension-sdk`](https://www.npmjs.com/package/@notmike101/zcode-extension-sdk), the source contract is [`sdk/index.ts`](../sdk/index.ts), and [Hello Extension](../examples/hello-extension) remains a complete legacy-lifecycle example. +This guide describes extension API version 1 as implemented by host/SDK 0.3.3. The public package is [`@notmike101/zcode-extension-sdk`](https://www.npmjs.com/package/@notmike101/zcode-extension-sdk), the source contract is [`sdk/index.ts`](../sdk/index.ts), and [Hello Extension](../examples/hello-extension) remains a complete legacy-lifecycle example. Extensions are trusted local code. A main entrypoint runs in ZCode's Electron main process with Node.js access, while an optional renderer entrypoint runs inside the ZCode renderer. Declared capabilities control access through the SDK and are shown during installation; they do not sandbox trusted Node or renderer code. @@ -9,7 +9,7 @@ Extensions are trusted local code. A main entrypoint runs in ZCode's Electron ma For a separate Bun or TypeScript project: ```powershell -bun add -d @notmike101/zcode-extension-sdk@0.3.2 +bun add -d @notmike101/zcode-extension-sdk@0.3.3 ``` Import main-only types and helpers from `@notmike101/zcode-extension-sdk/main`, browser-safe renderer types and helpers from `/renderer`, and unstable raw-channel types from `/experimental`. The root export is also browser-safe. A JSON Schema is available at `/manifest.schema.json`, and `validateExtensionManifest` or `assertExtensionManifest` can validate manifests at runtime. diff --git a/package.json b/package.json index 8169a55..c9286fc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zcode-desktop-extensions", - "version": "0.3.2", + "version": "0.3.3", "description": "An update-resistant extension host for the ZCode Electron desktop application.", "private": true, "license": "MIT", diff --git a/scripts/check-sdk-package.ts b/scripts/check-sdk-package.ts index 6aa624b..e51048d 100644 --- a/scripts/check-sdk-package.ts +++ b/scripts/check-sdk-package.ts @@ -11,7 +11,7 @@ const packageJson = JSON.parse(await readFile(path.join(sdk, "package.json"), "u exports: Record; }; -if (packageJson.name !== "@notmike101/zcode-extension-sdk" || packageJson.version !== "0.3.2") { +if (packageJson.name !== "@notmike101/zcode-extension-sdk" || packageJson.version !== "0.3.3") { throw new Error("Unexpected SDK package identity"); } for (const entry of [".", "./main", "./renderer", "./experimental", "./manifest.schema.json"]) { diff --git a/sdk/package.json b/sdk/package.json index 2c2db32..421e5f9 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@notmike101/zcode-extension-sdk", - "version": "0.3.2", + "version": "0.3.3", "description": "Public TypeScript SDK and authoring helpers for ZCode Desktop Extensions.", "license": "MIT", "type": "module", diff --git a/src/host/bootstrap.ts b/src/host/bootstrap.ts index ecd3a66..d447f8a 100644 --- a/src/host/bootstrap.ts +++ b/src/host/bootstrap.ts @@ -12,6 +12,7 @@ import {TaskService} from "../protocol/task-service.ts"; import {ZCodeGateway} from "../protocol/zcode-gateway.ts"; import {ExtensionZCodeService} from "../protocol/extension-service.ts"; import {PluginManager} from "./plugin-manager.ts"; +import {observeRendererLoads} from "./renderer-loads.ts"; protocol.registerSchemesAsPrivileged([{ scheme: "zdp", @@ -179,11 +180,10 @@ app.once("ready", async () => { app.on("web-contents-created", (_event, contents) => { if (contents.getType() !== "window") return; - contents.once("did-finish-load", async () => { + observeRendererLoads(contents, async () => { try { await initialized; renderers.add(contents); - contents.once("destroyed", () => renderers.delete(contents)); const rendererCode = await readFile(path.join(runtimeVersionDir, "renderer", "index.js"), "utf8"); await contents.executeJavaScript(`${rendererCode}\n//# sourceURL=zdp-renderer.js`, true); await writeJsonAtomic(paths.bootState, { @@ -196,7 +196,7 @@ app.on("web-contents-created", (_event, contents) => { } catch (error) { await logger.error("Renderer injection failed", error); } - }); + }, () => renderers.delete(contents)); }); app.on("before-quit", (event) => { diff --git a/src/host/renderer-loads.ts b/src/host/renderer-loads.ts new file mode 100644 index 0000000..3c54092 --- /dev/null +++ b/src/host/renderer-loads.ts @@ -0,0 +1,13 @@ +export type RendererLoadEmitter = { + on(event: "did-finish-load", listener: () => void): unknown; + once(event: "destroyed", listener: () => void): unknown; +}; + +export function observeRendererLoads( + contents: RendererLoadEmitter, + onLoad: () => void | Promise, + onDestroyed: () => void, +): void { + contents.on("did-finish-load", () => { void onLoad(); }); + contents.once("destroyed", onDestroyed); +} diff --git a/src/shared/constants.ts b/src/shared/constants.ts index 9082128..e6f3415 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -1,7 +1,7 @@ import path from "node:path"; export const HOST_NAME = "ZCode Desktop Extensions"; -export const HOST_VERSION = "0.3.2"; +export const HOST_VERSION = "0.3.3"; export const API_VERSION = 1; export const INSTALL_STATE_VERSION = 1; export const DEFAULT_ZCODE_ROOT = path.join( diff --git a/tests/fixtures/serve-renderer-harness.ts b/tests/fixtures/serve-renderer-harness.ts index c04edc1..043d800 100644 --- a/tests/fixtures/serve-renderer-harness.ts +++ b/tests/fixtures/serve-renderer-harness.ts @@ -15,7 +15,7 @@ const server = Bun.serve({ }); } if (url.pathname === "/renderer/index.js") { - return new Response(await readFile(path.join(root, "runtime", "versions", "0.3.2", "renderer", "index.js")), { + return new Response(await readFile(path.join(root, "runtime", "versions", "0.3.3", "renderer", "index.js")), { headers: {"content-type": "text/javascript; charset=utf-8"}, }); } diff --git a/tests/renderer-loads.test.ts b/tests/renderer-loads.test.ts new file mode 100644 index 0000000..9fb7a56 --- /dev/null +++ b/tests/renderer-loads.test.ts @@ -0,0 +1,20 @@ +import {describe, expect, test} from "bun:test"; +import {EventEmitter} from "node:events"; +import {observeRendererLoads} from "../src/host/renderer-loads.ts"; + +describe("renderer load observation", () => { + test("reinjects after every load and disposes once", () => { + const contents = new EventEmitter(); + let loads = 0; + let destroyed = 0; + + observeRendererLoads(contents, () => { loads += 1; }, () => { destroyed += 1; }); + contents.emit("did-finish-load"); + contents.emit("did-finish-load"); + contents.emit("destroyed"); + contents.emit("destroyed"); + + expect(loads).toBe(2); + expect(destroyed).toBe(1); + }); +});