@dormer/cli 0.0.0-stage → 0.1.0
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.
- package/README.md +194 -2
- package/bin/dormer.mjs +26 -0
- package/dist/cli.js +1301 -0
- package/dist/index.js +676 -0
- package/package.json +59 -4
- package/src/args.ts +68 -0
- package/src/backend.ts +142 -0
- package/src/cli.ts +45 -0
- package/src/commands/build.ts +105 -0
- package/src/commands/create.ts +132 -0
- package/src/commands/dev.ts +128 -0
- package/src/commands/doctor.ts +141 -0
- package/src/commands/inspect.ts +60 -0
- package/src/commands/package.ts +159 -0
- package/src/config.ts +151 -0
- package/src/host.ts +85 -0
- package/src/index.ts +8 -0
- package/src/resolve.ts +75 -0
- package/src/ui.ts +57 -0
- package/templates/react/README.md +34 -0
- package/templates/react/_gitignore +5 -0
- package/templates/react/dormer.config.json +16 -0
- package/templates/react/package.json +29 -0
- package/templates/react/renderer/index.html +12 -0
- package/templates/react/renderer/src/App.tsx +34 -0
- package/templates/react/renderer/src/main.tsx +6 -0
- package/templates/react/renderer/src/style.css +8 -0
- package/templates/react/renderer/src/vite-env.d.ts +2 -0
- package/templates/react/renderer/tsconfig.json +15 -0
- package/templates/react/renderer/vite.config.ts +7 -0
- package/templates/react/src/main.ts +34 -0
- package/templates/react/src/shared.ts +10 -0
- package/templates/react/tsconfig.json +15 -0
- package/templates/svelte/README.md +34 -0
- package/templates/svelte/_gitignore +5 -0
- package/templates/svelte/dormer.config.json +16 -0
- package/templates/svelte/package.json +27 -0
- package/templates/svelte/renderer/index.html +12 -0
- package/templates/svelte/renderer/src/App.svelte +32 -0
- package/templates/svelte/renderer/src/main.ts +5 -0
- package/templates/svelte/renderer/src/style.css +8 -0
- package/templates/svelte/renderer/src/vite-env.d.ts +2 -0
- package/templates/svelte/renderer/tsconfig.json +15 -0
- package/templates/svelte/renderer/vite.config.ts +7 -0
- package/templates/svelte/src/main.ts +34 -0
- package/templates/svelte/src/shared.ts +10 -0
- package/templates/svelte/tsconfig.json +15 -0
- package/templates/vanilla/README.md +34 -0
- package/templates/vanilla/_gitignore +5 -0
- package/templates/vanilla/dormer.config.json +16 -0
- package/templates/vanilla/package.json +24 -0
- package/templates/vanilla/renderer/index.html +12 -0
- package/templates/vanilla/renderer/src/main.ts +31 -0
- package/templates/vanilla/renderer/src/style.css +8 -0
- package/templates/vanilla/renderer/src/vite-env.d.ts +2 -0
- package/templates/vanilla/renderer/tsconfig.json +14 -0
- package/templates/vanilla/renderer/vite.config.ts +6 -0
- package/templates/vanilla/src/main.ts +34 -0
- package/templates/vanilla/src/shared.ts +10 -0
- package/templates/vanilla/tsconfig.json +15 -0
- package/templates/vue/README.md +34 -0
- package/templates/vue/_gitignore +5 -0
- package/templates/vue/dormer.config.json +16 -0
- package/templates/vue/package.json +27 -0
- package/templates/vue/renderer/index.html +12 -0
- package/templates/vue/renderer/src/App.vue +36 -0
- package/templates/vue/renderer/src/main.ts +5 -0
- package/templates/vue/renderer/src/style.css +8 -0
- package/templates/vue/renderer/src/vite-env.d.ts +8 -0
- package/templates/vue/renderer/tsconfig.json +15 -0
- package/templates/vue/renderer/vite.config.ts +7 -0
- package/templates/vue/src/main.ts +34 -0
- package/templates/vue/src/shared.ts +10 -0
- package/templates/vue/tsconfig.json +15 -0
|
@@ -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,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.0",
|
|
15
|
+
"@dormer/renderer": "0.1.0"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@dormer/cli": "0.1.0",
|
|
19
|
+
"@dormer/vite-plugin": "0.1.0",
|
|
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,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,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,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.0",
|
|
15
|
+
"@dormer/renderer": "0.1.0",
|
|
16
|
+
"vue": "^3.5.0"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@dormer/cli": "0.1.0",
|
|
20
|
+
"@dormer/vite-plugin": "0.1.0",
|
|
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,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,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
|
+
}
|