@dormer/cli 0.0.0-stage → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.md +201 -2
  2. package/bin/dormer.mjs +26 -0
  3. package/dist/cli.js +1301 -0
  4. package/dist/index.js +676 -0
  5. package/package.json +57 -4
  6. package/src/args.ts +68 -0
  7. package/src/backend.ts +142 -0
  8. package/src/cli.ts +45 -0
  9. package/src/commands/build.ts +105 -0
  10. package/src/commands/create.ts +132 -0
  11. package/src/commands/dev.ts +128 -0
  12. package/src/commands/doctor.ts +141 -0
  13. package/src/commands/inspect.ts +60 -0
  14. package/src/commands/package.ts +159 -0
  15. package/src/config.ts +151 -0
  16. package/src/host.ts +85 -0
  17. package/src/index.ts +8 -0
  18. package/src/resolve.ts +75 -0
  19. package/src/ui.ts +57 -0
  20. package/templates/react/README.md +34 -0
  21. package/templates/react/_gitignore +5 -0
  22. package/templates/react/dormer.config.json +16 -0
  23. package/templates/react/package.json +29 -0
  24. package/templates/react/renderer/index.html +12 -0
  25. package/templates/react/renderer/src/App.tsx +34 -0
  26. package/templates/react/renderer/src/main.tsx +6 -0
  27. package/templates/react/renderer/src/style.css +8 -0
  28. package/templates/react/renderer/src/vite-env.d.ts +2 -0
  29. package/templates/react/renderer/tsconfig.json +15 -0
  30. package/templates/react/renderer/vite.config.ts +7 -0
  31. package/templates/react/src/main.ts +34 -0
  32. package/templates/react/src/shared.ts +10 -0
  33. package/templates/react/tsconfig.json +15 -0
  34. package/templates/svelte/README.md +34 -0
  35. package/templates/svelte/_gitignore +5 -0
  36. package/templates/svelte/dormer.config.json +16 -0
  37. package/templates/svelte/package.json +27 -0
  38. package/templates/svelte/renderer/index.html +12 -0
  39. package/templates/svelte/renderer/src/App.svelte +32 -0
  40. package/templates/svelte/renderer/src/main.ts +5 -0
  41. package/templates/svelte/renderer/src/style.css +8 -0
  42. package/templates/svelte/renderer/src/vite-env.d.ts +2 -0
  43. package/templates/svelte/renderer/tsconfig.json +15 -0
  44. package/templates/svelte/renderer/vite.config.ts +7 -0
  45. package/templates/svelte/src/main.ts +34 -0
  46. package/templates/svelte/src/shared.ts +10 -0
  47. package/templates/svelte/tsconfig.json +15 -0
  48. package/templates/vanilla/README.md +34 -0
  49. package/templates/vanilla/_gitignore +5 -0
  50. package/templates/vanilla/dormer.config.json +16 -0
  51. package/templates/vanilla/package.json +24 -0
  52. package/templates/vanilla/renderer/index.html +12 -0
  53. package/templates/vanilla/renderer/src/main.ts +31 -0
  54. package/templates/vanilla/renderer/src/style.css +8 -0
  55. package/templates/vanilla/renderer/src/vite-env.d.ts +2 -0
  56. package/templates/vanilla/renderer/tsconfig.json +14 -0
  57. package/templates/vanilla/renderer/vite.config.ts +6 -0
  58. package/templates/vanilla/src/main.ts +34 -0
  59. package/templates/vanilla/src/shared.ts +10 -0
  60. package/templates/vanilla/tsconfig.json +15 -0
  61. package/templates/vue/README.md +34 -0
  62. package/templates/vue/_gitignore +5 -0
  63. package/templates/vue/dormer.config.json +16 -0
  64. package/templates/vue/package.json +27 -0
  65. package/templates/vue/renderer/index.html +12 -0
  66. package/templates/vue/renderer/src/App.vue +36 -0
  67. package/templates/vue/renderer/src/main.ts +5 -0
  68. package/templates/vue/renderer/src/style.css +8 -0
  69. package/templates/vue/renderer/src/vite-env.d.ts +8 -0
  70. package/templates/vue/renderer/tsconfig.json +15 -0
  71. package/templates/vue/renderer/vite.config.ts +7 -0
  72. package/templates/vue/src/main.ts +34 -0
  73. package/templates/vue/src/shared.ts +10 -0
  74. package/templates/vue/tsconfig.json +15 -0
@@ -0,0 +1,7 @@
1
+ import { defineConfig } from "vite";
2
+ import { svelte, vitePreprocess } from "@sveltejs/vite-plugin-svelte";
3
+ import { dormer } from "@dormer/vite-plugin";
4
+
5
+ export default defineConfig({
6
+ plugins: [svelte({ preprocess: vitePreprocess() }), dormer()],
7
+ });
@@ -0,0 +1,34 @@
1
+ // {{name}} — backend. Runs in the Node.js sidecar; the native host owns windows and WebViews.
2
+ import { app, BrowserWindow, ipcMain } from "@dormer/api";
3
+ import type { PingReply } from "./shared.ts";
4
+
5
+ const selftest = process.env.DORMER_SELFTEST === "1";
6
+ const log = (kind: string, payload: Record<string, unknown>) => console.log(JSON.stringify({ kind, ...payload }));
7
+
8
+ // Renderer → backend request/response: `await invoke("ping")` in the page resolves with this.
9
+ ipcMain.handle("ping", (): PingReply => ({ reply: "pong", node: process.version, platform: process.platform, pid: process.pid, at: Date.now(), selftest }));
10
+
11
+ // Fire-and-forget message from the renderer (`send("selftest:done", reply)`); used by the
12
+ // automated template test (packages/cli/test/e2e-linux.sh) when DORMER_SELFTEST=1.
13
+ ipcMain.on("selftest:done", (_event, result: PingReply) => {
14
+ if (!selftest) return;
15
+ const ok = result?.reply === "pong";
16
+ log("template-selftest", { ok, node: result?.node });
17
+ if (ok) app.quit(); else app.exit(1);
18
+ });
19
+
20
+ async function createWindow(): Promise<void> {
21
+ const win = new BrowserWindow({ width: 960, height: 640, title: "{{name}}", show: false, capabilities: ["app:default"] });
22
+ win.once("ready-to-show", () => win.show());
23
+ // `dormer dev` serves the renderer from Vite and passes its URL; production loads dormer://app/.
24
+ const devUrl = process.env.DORMER_DEV_URL;
25
+ if (devUrl) await win.loadURL(devUrl);
26
+ else await win.loadApp("index.html");
27
+ // Backend → renderer events: received in the page with `on("backend:tick", ...)`.
28
+ const tick = setInterval(() => win.webContents.send("backend:tick", { uptime: Math.round(process.uptime()) }), 1000);
29
+ win.on("closed", () => clearInterval(tick));
30
+ }
31
+
32
+ app.on("window-all-closed", () => { if (process.platform !== "darwin") app.quit(); });
33
+ app.on("activate", () => { if (BrowserWindow.getAllWindows().length === 0) void createWindow(); });
34
+ app.whenReady().then(createWindow);
@@ -0,0 +1,10 @@
1
+ // Types shared by the backend and the renderer (no Node.js imports here: the renderer's tsconfig
2
+ // type-checks this file too, which is what gives the page autocompletion for backend replies).
3
+ export interface PingReply {
4
+ reply: "pong";
5
+ node: string;
6
+ platform: string;
7
+ pid: number;
8
+ at: number;
9
+ selftest: boolean;
10
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "strict": true,
7
+ "noEmit": true,
8
+ "allowImportingTsExtensions": true,
9
+ "erasableSyntaxOnly": true,
10
+ "verbatimModuleSyntax": true,
11
+ "skipLibCheck": true,
12
+ "types": ["node"]
13
+ },
14
+ "include": ["src"]
15
+ }
@@ -0,0 +1,34 @@
1
+ # {{name}}
2
+
3
+ A [Dormer](https://github.com/dormer) desktop app: a Node.js backend (`src/main.ts`) and a
4
+ vanilla TypeScript renderer (`renderer/`) shown in the operating system's WebView.
5
+
6
+ ```sh
7
+ npm install
8
+ npm run dev # Vite dev server + native window; the backend is rebuilt on change
9
+ npm run build # dist/renderer, dist/main.js, dist/dormer-app.json
10
+ npm run package # installers (needs the dormer-bundle tool)
11
+ npx dormer doctor # checks Node, the host binary and platform prerequisites
12
+ ```
13
+
14
+ ## Layout
15
+
16
+ ```
17
+ dormer.config.json name, identifier, version, per-window capabilities, Node version pin
18
+ src/main.ts backend: one window, ipcMain.handle("ping"), window-all-closed → quit
19
+ renderer/ Vite project: index.html, vite.config.ts (uses @dormer/vite-plugin), src/
20
+ ```
21
+
22
+ ## How the pieces talk
23
+
24
+ * `invoke("ping")` in the renderer resolves with whatever `ipcMain.handle("ping", ...)` returns.
25
+ * `on("backend:tick", fn)` receives events the backend pushes with `win.webContents.send(...)`.
26
+ * `send("selftest:done", ...)` is a one-way message handled by `ipcMain.on(...)`.
27
+
28
+ The renderer never has Node access; every call goes through the native host's capability gate.
29
+
30
+ ## Self-test
31
+
32
+ With `DORMER_SELFTEST=1` the backend quits after the renderer reports the ping result and logs
33
+ `{"kind":"template-selftest","ok":true}`. The Dormer repository's `packages/cli/test/e2e-linux.sh`
34
+ uses this to verify the template headless.
@@ -0,0 +1,5 @@
1
+ node_modules/
2
+ dist/
3
+ .dormer/
4
+ *.log
5
+ .DS_Store
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "{{name}}",
3
+ "identifier": "{{identifier}}",
4
+ "version": "0.1.0",
5
+ "description": "A Dormer desktop app (vanilla template)",
6
+ "entry": "src/main.ts",
7
+ "renderer": "renderer",
8
+ "firstWindow": { "label": "main", "width": 960, "height": 640, "title": "{{name}}", "backgroundColor": "auto" },
9
+ "windows": {
10
+ "main": { "capabilities": ["app:default"] }
11
+ },
12
+ "capabilities": {
13
+ "app:default": { "description": "Everything the backend exposes, for the app's own windows only", "allow": ["ipc:*", "send:*"], "origins": ["local"] }
14
+ },
15
+ "node": { "version": "24" }
16
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "{{name}}",
3
+ "private": true,
4
+ "version": "0.1.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "dormer dev",
8
+ "build": "dormer build",
9
+ "package": "dormer package",
10
+ "doctor": "dormer doctor",
11
+ "typecheck": "tsc -p . && tsc -p renderer"
12
+ },
13
+ "dependencies": {
14
+ "@dormer/api": "0.1.3",
15
+ "@dormer/renderer": "0.1.3"
16
+ },
17
+ "devDependencies": {
18
+ "@dormer/cli": "0.1.3",
19
+ "@dormer/vite-plugin": "0.1.3",
20
+ "@types/node": "^24.0.0",
21
+ "typescript": "^5.9.0",
22
+ "vite": "^8.3.0"
23
+ }
24
+ }
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>{{name}}</title>
7
+ </head>
8
+ <body>
9
+ <div id="app"></div>
10
+ <script type="module" src="/src/main.ts"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,31 @@
1
+ import "./style.css";
2
+ import { invoke, on, send, isDormer } from "@dormer/renderer";
3
+ import type { PingReply } from "../../src/shared.ts";
4
+
5
+ const root = document.querySelector<HTMLDivElement>("#app")!;
6
+ root.innerHTML = `
7
+ <main>
8
+ <h1>{{name}}</h1>
9
+ <p class="muted">Vanilla TypeScript · Node.js backend · system WebView${import.meta.env.DORMER ? " · built with @dormer/vite-plugin" : ""}</p>
10
+ <button id="ping">Ping the backend</button>
11
+ <pre id="result">…</pre>
12
+ <p id="tick" class="muted"></p>
13
+ </main>`;
14
+ const result = document.querySelector<HTMLPreElement>("#result")!;
15
+ const tick = document.querySelector<HTMLParagraphElement>("#tick")!;
16
+
17
+ async function ping(): Promise<void> {
18
+ // Request/response: page → host (capability check) → Node `ipcMain.handle("ping")` → back.
19
+ const reply = await invoke<PingReply>("ping");
20
+ result.textContent = JSON.stringify(reply, null, 2);
21
+ if (reply.selftest) send("selftest:done", reply); // one-way message, used by the automated test
22
+ }
23
+
24
+ if (isDormer()) {
25
+ document.querySelector("#ping")!.addEventListener("click", () => void ping());
26
+ // Events pushed by the backend with `win.webContents.send("backend:tick", ...)`.
27
+ on("backend:tick", (t: { uptime: number }) => { tick.textContent = `backend uptime: ${t.uptime} s`; });
28
+ void ping();
29
+ } else {
30
+ result.textContent = "Not running inside Dormer. Start the app with `npm run dev`.";
31
+ }
@@ -0,0 +1,8 @@
1
+ :root { color-scheme: light dark; font-family: system-ui, sans-serif; line-height: 1.5; }
2
+ body { margin: 0; }
3
+ main { max-width: 720px; margin: 0 auto; padding: 32px 24px; }
4
+ h1 { margin: 0 0 4px; font-size: 1.6rem; }
5
+ .muted { opacity: 0.7; margin: 0 0 16px; }
6
+ button { font: inherit; padding: 8px 14px; border-radius: 8px; border: 1px solid color-mix(in srgb, currentColor 30%, transparent); background: color-mix(in srgb, currentColor 8%, transparent); cursor: pointer; }
7
+ button:hover { background: color-mix(in srgb, currentColor 16%, transparent); }
8
+ pre { background: color-mix(in srgb, currentColor 8%, transparent); padding: 12px; border-radius: 8px; overflow: auto; min-height: 7em; font-size: 0.85rem; }
@@ -0,0 +1,2 @@
1
+ /// <reference types="vite/client" />
2
+ /// <reference types="@dormer/vite-plugin/client" />
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
5
+ "module": "ESNext",
6
+ "moduleResolution": "bundler",
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "isolatedModules": true,
10
+ "allowImportingTsExtensions": true,
11
+ "skipLibCheck": true
12
+ },
13
+ "include": ["src"]
14
+ }
@@ -0,0 +1,6 @@
1
+ import { defineConfig } from "vite";
2
+ import { dormer } from "@dormer/vite-plugin";
3
+
4
+ export default defineConfig({
5
+ plugins: [dormer()],
6
+ });
@@ -0,0 +1,34 @@
1
+ // {{name}} — backend. Runs in the Node.js sidecar; the native host owns windows and WebViews.
2
+ import { app, BrowserWindow, ipcMain } from "@dormer/api";
3
+ import type { PingReply } from "./shared.ts";
4
+
5
+ const selftest = process.env.DORMER_SELFTEST === "1";
6
+ const log = (kind: string, payload: Record<string, unknown>) => console.log(JSON.stringify({ kind, ...payload }));
7
+
8
+ // Renderer → backend request/response: `await invoke("ping")` in the page resolves with this.
9
+ ipcMain.handle("ping", (): PingReply => ({ reply: "pong", node: process.version, platform: process.platform, pid: process.pid, at: Date.now(), selftest }));
10
+
11
+ // Fire-and-forget message from the renderer (`send("selftest:done", reply)`); used by the
12
+ // automated template test (packages/cli/test/e2e-linux.sh) when DORMER_SELFTEST=1.
13
+ ipcMain.on("selftest:done", (_event, result: PingReply) => {
14
+ if (!selftest) return;
15
+ const ok = result?.reply === "pong";
16
+ log("template-selftest", { ok, node: result?.node });
17
+ if (ok) app.quit(); else app.exit(1);
18
+ });
19
+
20
+ async function createWindow(): Promise<void> {
21
+ const win = new BrowserWindow({ width: 960, height: 640, title: "{{name}}", show: false, capabilities: ["app:default"] });
22
+ win.once("ready-to-show", () => win.show());
23
+ // `dormer dev` serves the renderer from Vite and passes its URL; production loads dormer://app/.
24
+ const devUrl = process.env.DORMER_DEV_URL;
25
+ if (devUrl) await win.loadURL(devUrl);
26
+ else await win.loadApp("index.html");
27
+ // Backend → renderer events: received in the page with `on("backend:tick", ...)`.
28
+ const tick = setInterval(() => win.webContents.send("backend:tick", { uptime: Math.round(process.uptime()) }), 1000);
29
+ win.on("closed", () => clearInterval(tick));
30
+ }
31
+
32
+ app.on("window-all-closed", () => { if (process.platform !== "darwin") app.quit(); });
33
+ app.on("activate", () => { if (BrowserWindow.getAllWindows().length === 0) void createWindow(); });
34
+ app.whenReady().then(createWindow);
@@ -0,0 +1,10 @@
1
+ // Types shared by the backend and the renderer (no Node.js imports here: the renderer's tsconfig
2
+ // type-checks this file too, which is what gives the page autocompletion for backend replies).
3
+ export interface PingReply {
4
+ reply: "pong";
5
+ node: string;
6
+ platform: string;
7
+ pid: number;
8
+ at: number;
9
+ selftest: boolean;
10
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "strict": true,
7
+ "noEmit": true,
8
+ "allowImportingTsExtensions": true,
9
+ "erasableSyntaxOnly": true,
10
+ "verbatimModuleSyntax": true,
11
+ "skipLibCheck": true,
12
+ "types": ["node"]
13
+ },
14
+ "include": ["src"]
15
+ }
@@ -0,0 +1,34 @@
1
+ # {{name}}
2
+
3
+ A [Dormer](https://github.com/dormer) desktop app: a Node.js backend (`src/main.ts`) and a
4
+ Vue renderer (`renderer/`) shown in the operating system's WebView.
5
+
6
+ ```sh
7
+ npm install
8
+ npm run dev # Vite dev server + native window; the backend is rebuilt on change
9
+ npm run build # dist/renderer, dist/main.js, dist/dormer-app.json
10
+ npm run package # installers (needs the dormer-bundle tool)
11
+ npx dormer doctor # checks Node, the host binary and platform prerequisites
12
+ ```
13
+
14
+ ## Layout
15
+
16
+ ```
17
+ dormer.config.json name, identifier, version, per-window capabilities, Node version pin
18
+ src/main.ts backend: one window, ipcMain.handle("ping"), window-all-closed → quit
19
+ renderer/ Vite project: index.html, vite.config.ts (@vitejs/plugin-vue + @dormer/vite-plugin), src/App.vue
20
+ ```
21
+
22
+ ## How the pieces talk
23
+
24
+ * `invoke("ping")` in the renderer resolves with whatever `ipcMain.handle("ping", ...)` returns.
25
+ * `on("backend:tick", fn)` receives events the backend pushes with `win.webContents.send(...)`.
26
+ * `send("selftest:done", ...)` is a one-way message handled by `ipcMain.on(...)`.
27
+
28
+ The renderer never has Node access; every call goes through the native host's capability gate.
29
+
30
+ ## Self-test
31
+
32
+ With `DORMER_SELFTEST=1` the backend quits after the renderer reports the ping result and logs
33
+ `{"kind":"template-selftest","ok":true}`. The Dormer repository's `packages/cli/test/e2e-linux.sh`
34
+ uses this to verify the template headless.
@@ -0,0 +1,5 @@
1
+ node_modules/
2
+ dist/
3
+ .dormer/
4
+ *.log
5
+ .DS_Store
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "{{name}}",
3
+ "identifier": "{{identifier}}",
4
+ "version": "0.1.0",
5
+ "description": "A Dormer desktop app (vue template)",
6
+ "entry": "src/main.ts",
7
+ "renderer": "renderer",
8
+ "firstWindow": { "label": "main", "width": 960, "height": 640, "title": "{{name}}", "backgroundColor": "auto" },
9
+ "windows": {
10
+ "main": { "capabilities": ["app:default"] }
11
+ },
12
+ "capabilities": {
13
+ "app:default": { "description": "Everything the backend exposes, for the app's own windows only", "allow": ["ipc:*", "send:*"], "origins": ["local"] }
14
+ },
15
+ "node": { "version": "24" }
16
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "{{name}}",
3
+ "private": true,
4
+ "version": "0.1.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "dormer dev",
8
+ "build": "dormer build",
9
+ "package": "dormer package",
10
+ "doctor": "dormer doctor",
11
+ "typecheck": "tsc -p . && vue-tsc -p renderer"
12
+ },
13
+ "dependencies": {
14
+ "@dormer/api": "0.1.3",
15
+ "@dormer/renderer": "0.1.3",
16
+ "vue": "^3.5.0"
17
+ },
18
+ "devDependencies": {
19
+ "@dormer/cli": "0.1.3",
20
+ "@dormer/vite-plugin": "0.1.3",
21
+ "@types/node": "^24.0.0",
22
+ "@vitejs/plugin-vue": "^6.0.0",
23
+ "typescript": "^5.9.0",
24
+ "vite": "^8.3.0",
25
+ "vue-tsc": "^3.0.0"
26
+ }
27
+ }
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>{{name}}</title>
7
+ </head>
8
+ <body>
9
+ <div id="app"></div>
10
+ <script type="module" src="/src/main.ts"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,36 @@
1
+ <script setup lang="ts">
2
+ import { onMounted, onUnmounted, ref } from "vue";
3
+ import { invoke, on, send, isDormer } from "@dormer/renderer";
4
+ import type { PingReply } from "../../src/shared.ts";
5
+
6
+ const inside = isDormer();
7
+ const builtWithPlugin = import.meta.env.DORMER;
8
+ const reply = ref<PingReply | null>(null);
9
+ const uptime = ref<number | null>(null);
10
+ let off: (() => void) | undefined;
11
+
12
+ async function ping() {
13
+ // Request/response: page → host (capability check) → Node `ipcMain.handle("ping")` → back.
14
+ const r = await invoke<PingReply>("ping");
15
+ reply.value = r;
16
+ if (r.selftest) send("selftest:done", r); // one-way message, used by the automated test
17
+ }
18
+
19
+ onMounted(() => {
20
+ if (!inside) return;
21
+ // Events pushed by the backend with `win.webContents.send("backend:tick", ...)`; `on` returns an unsubscribe.
22
+ off = on("backend:tick", (t: { uptime: number }) => { uptime.value = t.uptime; });
23
+ void ping();
24
+ });
25
+ onUnmounted(() => off?.());
26
+ </script>
27
+
28
+ <template>
29
+ <main>
30
+ <h1>{{name}}</h1>
31
+ <p class="muted">Vue · Node.js backend · system WebView<span v-if="builtWithPlugin"> · built with @dormer/vite-plugin</span></p>
32
+ <button :disabled="!inside" @click="ping()">Ping the backend</button>
33
+ <pre>{{ inside ? (reply ? JSON.stringify(reply, null, 2) : "…") : "Not running inside Dormer. Start the app with `npm run dev`." }}</pre>
34
+ <p class="muted">{{ uptime !== null ? `backend uptime: ${uptime} s` : "" }}</p>
35
+ </main>
36
+ </template>
@@ -0,0 +1,5 @@
1
+ import { createApp } from "vue";
2
+ import App from "./App.vue";
3
+ import "./style.css";
4
+
5
+ createApp(App).mount("#app");
@@ -0,0 +1,8 @@
1
+ :root { color-scheme: light dark; font-family: system-ui, sans-serif; line-height: 1.5; }
2
+ body { margin: 0; }
3
+ main { max-width: 720px; margin: 0 auto; padding: 32px 24px; }
4
+ h1 { margin: 0 0 4px; font-size: 1.6rem; }
5
+ .muted { opacity: 0.7; margin: 0 0 16px; }
6
+ button { font: inherit; padding: 8px 14px; border-radius: 8px; border: 1px solid color-mix(in srgb, currentColor 30%, transparent); background: color-mix(in srgb, currentColor 8%, transparent); cursor: pointer; }
7
+ button:hover { background: color-mix(in srgb, currentColor 16%, transparent); }
8
+ pre { background: color-mix(in srgb, currentColor 8%, transparent); padding: 12px; border-radius: 8px; overflow: auto; min-height: 7em; font-size: 0.85rem; }
@@ -0,0 +1,8 @@
1
+ /// <reference types="vite/client" />
2
+ /// <reference types="@dormer/vite-plugin/client" />
3
+
4
+ declare module "*.vue" {
5
+ import type { DefineComponent } from "vue";
6
+ const component: DefineComponent<Record<string, never>, Record<string, never>, unknown>;
7
+ export default component;
8
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
5
+ "module": "ESNext",
6
+ "moduleResolution": "bundler",
7
+ "jsx": "preserve",
8
+ "strict": true,
9
+ "noEmit": true,
10
+ "isolatedModules": true,
11
+ "allowImportingTsExtensions": true,
12
+ "skipLibCheck": true
13
+ },
14
+ "include": ["src"]
15
+ }
@@ -0,0 +1,7 @@
1
+ import { defineConfig } from "vite";
2
+ import vue from "@vitejs/plugin-vue";
3
+ import { dormer } from "@dormer/vite-plugin";
4
+
5
+ export default defineConfig({
6
+ plugins: [vue(), dormer()],
7
+ });
@@ -0,0 +1,34 @@
1
+ // {{name}} — backend. Runs in the Node.js sidecar; the native host owns windows and WebViews.
2
+ import { app, BrowserWindow, ipcMain } from "@dormer/api";
3
+ import type { PingReply } from "./shared.ts";
4
+
5
+ const selftest = process.env.DORMER_SELFTEST === "1";
6
+ const log = (kind: string, payload: Record<string, unknown>) => console.log(JSON.stringify({ kind, ...payload }));
7
+
8
+ // Renderer → backend request/response: `await invoke("ping")` in the page resolves with this.
9
+ ipcMain.handle("ping", (): PingReply => ({ reply: "pong", node: process.version, platform: process.platform, pid: process.pid, at: Date.now(), selftest }));
10
+
11
+ // Fire-and-forget message from the renderer (`send("selftest:done", reply)`); used by the
12
+ // automated template test (packages/cli/test/e2e-linux.sh) when DORMER_SELFTEST=1.
13
+ ipcMain.on("selftest:done", (_event, result: PingReply) => {
14
+ if (!selftest) return;
15
+ const ok = result?.reply === "pong";
16
+ log("template-selftest", { ok, node: result?.node });
17
+ if (ok) app.quit(); else app.exit(1);
18
+ });
19
+
20
+ async function createWindow(): Promise<void> {
21
+ const win = new BrowserWindow({ width: 960, height: 640, title: "{{name}}", show: false, capabilities: ["app:default"] });
22
+ win.once("ready-to-show", () => win.show());
23
+ // `dormer dev` serves the renderer from Vite and passes its URL; production loads dormer://app/.
24
+ const devUrl = process.env.DORMER_DEV_URL;
25
+ if (devUrl) await win.loadURL(devUrl);
26
+ else await win.loadApp("index.html");
27
+ // Backend → renderer events: received in the page with `on("backend:tick", ...)`.
28
+ const tick = setInterval(() => win.webContents.send("backend:tick", { uptime: Math.round(process.uptime()) }), 1000);
29
+ win.on("closed", () => clearInterval(tick));
30
+ }
31
+
32
+ app.on("window-all-closed", () => { if (process.platform !== "darwin") app.quit(); });
33
+ app.on("activate", () => { if (BrowserWindow.getAllWindows().length === 0) void createWindow(); });
34
+ app.whenReady().then(createWindow);
@@ -0,0 +1,10 @@
1
+ // Types shared by the backend and the renderer (no Node.js imports here: the renderer's tsconfig
2
+ // type-checks this file too, which is what gives the page autocompletion for backend replies).
3
+ export interface PingReply {
4
+ reply: "pong";
5
+ node: string;
6
+ platform: string;
7
+ pid: number;
8
+ at: number;
9
+ selftest: boolean;
10
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "strict": true,
7
+ "noEmit": true,
8
+ "allowImportingTsExtensions": true,
9
+ "erasableSyntaxOnly": true,
10
+ "verbatimModuleSyntax": true,
11
+ "skipLibCheck": true,
12
+ "types": ["node"]
13
+ },
14
+ "include": ["src"]
15
+ }