@mwguerra/hull 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/LICENSE +21 -0
- package/README.md +631 -0
- package/assets/hull-logo.png +0 -0
- package/assets/hull-logo.svg +5 -0
- package/bin/hull.js +4 -0
- package/devtools/dist/index.html +29 -0
- package/host/CMakeLists.txt +101 -0
- package/host/README.md +94 -0
- package/host/linux.Dockerfile +26 -0
- package/host/src/bindings/credentials.hpp +35 -0
- package/host/src/bindings/database.hpp +51 -0
- package/host/src/bindings/files.hpp +58 -0
- package/host/src/bindings/http.hpp +84 -0
- package/host/src/bindings/printer.hpp +281 -0
- package/host/src/bindings/storage.hpp +71 -0
- package/host/src/db_core.hpp +198 -0
- package/host/src/dispatcher.hpp +81 -0
- package/host/src/file_store.hpp +91 -0
- package/host/src/keychain.hpp +157 -0
- package/host/src/main.cpp +386 -0
- package/host/src/paths.hpp +62 -0
- package/host/src/secure.hpp +124 -0
- package/host/src/serve.hpp +113 -0
- package/host/test/db_test.cpp +80 -0
- package/host/test/secure_files_test.cpp +68 -0
- package/host/third_party/sqlite/sqlite3.c +269376 -0
- package/host/third_party/sqlite/sqlite3.h +14347 -0
- package/package.json +58 -0
- package/src/bridge/bridge-core.js +92 -0
- package/src/bridge/index.js +139 -0
- package/src/bridge/native-store.js +34 -0
- package/src/cli/build.js +122 -0
- package/src/cli/config.js +102 -0
- package/src/cli/dev.js +158 -0
- package/src/cli/eject.js +39 -0
- package/src/cli/host.js +61 -0
- package/src/cli/index.js +54 -0
- package/src/cli/installer.js +265 -0
- package/src/cli/release.js +178 -0
- package/src/cli/start.js +45 -0
- package/src/cli/timing.js +22 -0
- package/src/cli/vite.js +16 -0
- package/src/react/index.js +30 -0
- package/src/vue/index.js +31 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Marcelo Guerra
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,631 @@
|
|
|
1
|
+
# @mwguerra/hull
|
|
2
|
+
|
|
3
|
+
> Tiny native desktop apps from your Vanilla-JS / React / Vue UI — a prebuilt C++
|
|
4
|
+
> web-view host you drive with npm scripts. No compiler, no Electron, no bundled
|
|
5
|
+
> browser engine.
|
|
6
|
+
|
|
7
|
+
Hull ships a small prebuilt native binary that renders your existing Vite app in the
|
|
8
|
+
operating system's web view (WebView2 / WebKit / WebKitGTK) and exposes a JSON bridge
|
|
9
|
+
to a C++ backend with batteries included: **TLS HTTP, encrypted storage, OS keychain,
|
|
10
|
+
SQLite, files, and printing**. Your app stays plain JS/React/Vue.
|
|
11
|
+
|
|
12
|
+
This README is the full reference for now.
|
|
13
|
+
|
|
14
|
+
## Contents
|
|
15
|
+
|
|
16
|
+
- [Quick start](#quick-start)
|
|
17
|
+
- [Integrate your project](#integrate-your-project) — **Vanilla JS · React · Vue**
|
|
18
|
+
- [Try it from a blank project](#try-it-from-a-blank-project)
|
|
19
|
+
- [Talking to the backend](#talking-to-the-backend)
|
|
20
|
+
- [Bridge API reference](#bridge-api-reference)
|
|
21
|
+
- [CLI commands](#cli-commands)
|
|
22
|
+
- [Configuration (`.hullrc`)](#configuration-hullrc)
|
|
23
|
+
- [Develop in the browser (no recompile)](#develop-in-the-browser-no-recompile)
|
|
24
|
+
- [Versioned releases](#versioned-releases)
|
|
25
|
+
- [Security (at-rest crypto is a build option)](#security-at-rest-crypto-is-a-build-option)
|
|
26
|
+
- [Custom native code (eject)](#custom-native-code)
|
|
27
|
+
- [Platform support](#platform-support)
|
|
28
|
+
- [How it works](#how-it-works)
|
|
29
|
+
|
|
30
|
+
## Quick start
|
|
31
|
+
|
|
32
|
+
In any existing Vite app (Vanilla JS, React, or Vue):
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm i -D @mwguerra/hull
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Add scripts to `package.json`:
|
|
39
|
+
|
|
40
|
+
```jsonc
|
|
41
|
+
{
|
|
42
|
+
"scripts": {
|
|
43
|
+
"dev": "hull dev", // Vite dev server in a native window (HMR)
|
|
44
|
+
"build": "hull build", // single-file UI, packaged with the host -> ./release
|
|
45
|
+
"start": "hull start" // run the packaged build
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`npm run dev` opens your app as a desktop window. **Zero config** — the window title
|
|
51
|
+
and a per-app storage namespace are derived from `package.json`, and the window ships
|
|
52
|
+
with the Hull logo as its icon until you set your own. Installing `@mwguerra/hull`
|
|
53
|
+
also pulls the prebuilt host for your OS/CPU automatically (an os/cpu-gated optional
|
|
54
|
+
dependency, e.g. `@mwguerra/hull-win32-x64`).
|
|
55
|
+
|
|
56
|
+
Starting a **brand-new** project? Copy one of the recipes below.
|
|
57
|
+
|
|
58
|
+
## Integrate your project
|
|
59
|
+
|
|
60
|
+
The C++ backend and the JSON bridge (`@mwguerra/hull/bridge`) are **identical across
|
|
61
|
+
frameworks** — only the UI layer and the optional state hook differ. Each recipe below
|
|
62
|
+
is the exact shape of a runnable example in the repo (`examples/vanilla-js`,
|
|
63
|
+
`examples/react`, `examples/vue`); every example exercises **all** features (bridge,
|
|
64
|
+
settings + C++→UI events, credentials, HTTP, printing, SQLite, files, single-image
|
|
65
|
+
upload). Copy the one you want and trim.
|
|
66
|
+
|
|
67
|
+
Every Hull project, regardless of framework, has:
|
|
68
|
+
|
|
69
|
+
- the `@mwguerra/hull` dev dependency + the npm scripts,
|
|
70
|
+
- a normal `vite.config.js` (Hull injects the single-file plugin only at build time),
|
|
71
|
+
- an `index.html` Vite entry,
|
|
72
|
+
- an optional [`.hullrc`](#configuration-hullrc),
|
|
73
|
+
- your UI code, which imports from `@mwguerra/hull/bridge`.
|
|
74
|
+
|
|
75
|
+
Project layout (same for all three):
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
my-app/
|
|
79
|
+
├── package.json
|
|
80
|
+
├── vite.config.js
|
|
81
|
+
├── .hullrc # optional
|
|
82
|
+
├── index.html
|
|
83
|
+
└── src/
|
|
84
|
+
├── main.js|.jsx # Vite entry
|
|
85
|
+
├── App.vue|.jsx # (React/Vue) your root component
|
|
86
|
+
└── style.css
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Vanilla JS
|
|
90
|
+
|
|
91
|
+
`package.json`:
|
|
92
|
+
|
|
93
|
+
```jsonc
|
|
94
|
+
{
|
|
95
|
+
"name": "my-app",
|
|
96
|
+
"private": true,
|
|
97
|
+
"type": "module",
|
|
98
|
+
"scripts": {
|
|
99
|
+
"dev": "hull dev",
|
|
100
|
+
"dev:browser": "hull dev --browser",
|
|
101
|
+
"build": "hull build",
|
|
102
|
+
"start": "hull start",
|
|
103
|
+
"web": "vite"
|
|
104
|
+
},
|
|
105
|
+
"devDependencies": {
|
|
106
|
+
"@mwguerra/hull": "^0.1.0",
|
|
107
|
+
"vite": "^6.0.0"
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
`vite.config.js`:
|
|
113
|
+
|
|
114
|
+
```js
|
|
115
|
+
import { defineConfig } from "vite";
|
|
116
|
+
// Plain Vite — no framework plugin. `hull build` adds the single-file plugin.
|
|
117
|
+
export default defineConfig({});
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
`index.html`:
|
|
121
|
+
|
|
122
|
+
```html
|
|
123
|
+
<!doctype html>
|
|
124
|
+
<html lang="en">
|
|
125
|
+
<head><meta charset="UTF-8" /><title>My App</title></head>
|
|
126
|
+
<body>
|
|
127
|
+
<button id="ping">Send to C++</button>
|
|
128
|
+
<pre id="out"></pre>
|
|
129
|
+
<script type="module" src="/src/main.js"></script>
|
|
130
|
+
</body>
|
|
131
|
+
</html>
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
`src/main.js` — the bridge with no framework (use `nativeSetting` for two-way state):
|
|
135
|
+
|
|
136
|
+
```js
|
|
137
|
+
import { ping, db, nativeSetting, hasBridge } from "@mwguerra/hull/bridge";
|
|
138
|
+
|
|
139
|
+
// 1) call C++ and show the result
|
|
140
|
+
document.querySelector("#ping").addEventListener("click", async () => {
|
|
141
|
+
const res = await ping("hello"); // -> { ok: true, echo: "hello" }
|
|
142
|
+
document.querySelector("#out").textContent = JSON.stringify(res);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// 2) a two-way persisted setting (C++ stores it; C++ pushes changes back)
|
|
146
|
+
const theme = nativeSetting("theme");
|
|
147
|
+
theme.subscribe((v) => document.documentElement.classList.toggle("dark", v === "dark"));
|
|
148
|
+
theme.load(); // initial pull (no-op in a plain browser)
|
|
149
|
+
// theme.set("dark") persists and notifies subscribers
|
|
150
|
+
|
|
151
|
+
// 3) SQLite — works in the native host or browser dev mode
|
|
152
|
+
if (hasBridge()) {
|
|
153
|
+
db.migrate(["CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT NOT NULL)"])
|
|
154
|
+
.then(() => db.query("SELECT * FROM notes ORDER BY id DESC"))
|
|
155
|
+
.then((notes) => console.log(notes))
|
|
156
|
+
.catch(console.error);
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### React
|
|
161
|
+
|
|
162
|
+
`package.json`:
|
|
163
|
+
|
|
164
|
+
```jsonc
|
|
165
|
+
{
|
|
166
|
+
"name": "my-app",
|
|
167
|
+
"private": true,
|
|
168
|
+
"type": "module",
|
|
169
|
+
"scripts": {
|
|
170
|
+
"dev": "hull dev",
|
|
171
|
+
"dev:browser": "hull dev --browser",
|
|
172
|
+
"build": "hull build",
|
|
173
|
+
"start": "hull start",
|
|
174
|
+
"web": "vite"
|
|
175
|
+
},
|
|
176
|
+
"dependencies": { "react": "^18.3.0", "react-dom": "^18.3.0" },
|
|
177
|
+
"devDependencies": {
|
|
178
|
+
"@mwguerra/hull": "^0.1.0",
|
|
179
|
+
"@vitejs/plugin-react": "^4.3.0",
|
|
180
|
+
"vite": "^6.0.0"
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
`vite.config.js`:
|
|
186
|
+
|
|
187
|
+
```js
|
|
188
|
+
import { defineConfig } from "vite";
|
|
189
|
+
import react from "@vitejs/plugin-react";
|
|
190
|
+
export default defineConfig({ plugins: [react()] });
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
`index.html`:
|
|
194
|
+
|
|
195
|
+
```html
|
|
196
|
+
<!doctype html>
|
|
197
|
+
<html lang="en">
|
|
198
|
+
<head><meta charset="UTF-8" /><title>My App</title></head>
|
|
199
|
+
<body>
|
|
200
|
+
<div id="root"></div>
|
|
201
|
+
<script type="module" src="/src/main.jsx"></script>
|
|
202
|
+
</body>
|
|
203
|
+
</html>
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
`src/main.jsx`:
|
|
207
|
+
|
|
208
|
+
```jsx
|
|
209
|
+
import { StrictMode } from "react";
|
|
210
|
+
import { createRoot } from "react-dom/client";
|
|
211
|
+
import App from "./App.jsx";
|
|
212
|
+
import "./style.css";
|
|
213
|
+
|
|
214
|
+
createRoot(document.getElementById("root")).render(
|
|
215
|
+
<StrictMode><App /></StrictMode>
|
|
216
|
+
);
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
`src/App.jsx` — bridge + the `useNativeState` hook:
|
|
220
|
+
|
|
221
|
+
```jsx
|
|
222
|
+
import { useEffect, useState } from "react";
|
|
223
|
+
import { ping, db, hasBridge } from "@mwguerra/hull/bridge";
|
|
224
|
+
import { useNativeState } from "@mwguerra/hull/react";
|
|
225
|
+
|
|
226
|
+
export default function App() {
|
|
227
|
+
const [out, setOut] = useState(null);
|
|
228
|
+
const [theme, setTheme] = useNativeState("theme"); // like useState, persisted in C++
|
|
229
|
+
const [notes, setNotes] = useState([]);
|
|
230
|
+
|
|
231
|
+
useEffect(() => {
|
|
232
|
+
document.documentElement.classList.toggle("dark", theme === "dark");
|
|
233
|
+
}, [theme]);
|
|
234
|
+
|
|
235
|
+
useEffect(() => {
|
|
236
|
+
if (!hasBridge()) return; // native host or browser dev mode
|
|
237
|
+
(async () => {
|
|
238
|
+
await db.migrate(["CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT NOT NULL)"]);
|
|
239
|
+
setNotes(await db.query("SELECT * FROM notes ORDER BY id DESC"));
|
|
240
|
+
})();
|
|
241
|
+
}, []);
|
|
242
|
+
|
|
243
|
+
return (
|
|
244
|
+
<>
|
|
245
|
+
<button onClick={async () => setOut(await ping("hello"))}>Send to C++</button>
|
|
246
|
+
{out && <pre>{JSON.stringify(out)}</pre>}
|
|
247
|
+
<select value={theme ?? ""} onChange={(e) => setTheme(e.target.value)}>
|
|
248
|
+
<option value="light">Light</option>
|
|
249
|
+
<option value="dark">Dark</option>
|
|
250
|
+
</select>
|
|
251
|
+
<ul>{notes.map((n) => <li key={n.id}>{n.body}</li>)}</ul>
|
|
252
|
+
</>
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Vue
|
|
258
|
+
|
|
259
|
+
`package.json`:
|
|
260
|
+
|
|
261
|
+
```jsonc
|
|
262
|
+
{
|
|
263
|
+
"name": "my-app",
|
|
264
|
+
"private": true,
|
|
265
|
+
"type": "module",
|
|
266
|
+
"scripts": {
|
|
267
|
+
"dev": "hull dev",
|
|
268
|
+
"dev:browser": "hull dev --browser",
|
|
269
|
+
"build": "hull build",
|
|
270
|
+
"start": "hull start",
|
|
271
|
+
"web": "vite"
|
|
272
|
+
},
|
|
273
|
+
"dependencies": { "vue": "^3.5.0" },
|
|
274
|
+
"devDependencies": {
|
|
275
|
+
"@mwguerra/hull": "^0.1.0",
|
|
276
|
+
"@vitejs/plugin-vue": "^5.2.0",
|
|
277
|
+
"vite": "^6.0.0"
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
`vite.config.js`:
|
|
283
|
+
|
|
284
|
+
```js
|
|
285
|
+
import { defineConfig } from "vite";
|
|
286
|
+
import vue from "@vitejs/plugin-vue";
|
|
287
|
+
export default defineConfig({ plugins: [vue()] });
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
`index.html`:
|
|
291
|
+
|
|
292
|
+
```html
|
|
293
|
+
<!doctype html>
|
|
294
|
+
<html lang="en">
|
|
295
|
+
<head><meta charset="UTF-8" /><title>My App</title></head>
|
|
296
|
+
<body>
|
|
297
|
+
<div id="app"></div>
|
|
298
|
+
<script type="module" src="/src/main.js"></script>
|
|
299
|
+
</body>
|
|
300
|
+
</html>
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
`src/main.js`:
|
|
304
|
+
|
|
305
|
+
```js
|
|
306
|
+
import { createApp } from "vue";
|
|
307
|
+
import App from "./App.vue";
|
|
308
|
+
import "./style.css";
|
|
309
|
+
|
|
310
|
+
createApp(App).mount("#app");
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
`src/App.vue` — bridge + the `useNativeState` hook:
|
|
314
|
+
|
|
315
|
+
```vue
|
|
316
|
+
<script setup>
|
|
317
|
+
import { ref, watch, onMounted } from "vue";
|
|
318
|
+
import { ping, db, hasBridge } from "@mwguerra/hull/bridge";
|
|
319
|
+
import { useNativeState } from "@mwguerra/hull/vue";
|
|
320
|
+
|
|
321
|
+
const out = ref(null);
|
|
322
|
+
async function send() { out.value = await ping("hello"); }
|
|
323
|
+
|
|
324
|
+
const theme = useNativeState("theme"); // a ref; edits persist in C++, C++ pushes back
|
|
325
|
+
watch(theme, (v) => document.documentElement.classList.toggle("dark", v === "dark"),
|
|
326
|
+
{ immediate: true });
|
|
327
|
+
|
|
328
|
+
const notes = ref([]);
|
|
329
|
+
onMounted(async () => {
|
|
330
|
+
if (!hasBridge()) return; // native host or browser dev mode
|
|
331
|
+
await db.migrate(["CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT NOT NULL)"]);
|
|
332
|
+
notes.value = await db.query("SELECT * FROM notes ORDER BY id DESC");
|
|
333
|
+
});
|
|
334
|
+
</script>
|
|
335
|
+
|
|
336
|
+
<template>
|
|
337
|
+
<button @click="send">Send to C++</button>
|
|
338
|
+
<pre v-if="out">{{ out }}</pre>
|
|
339
|
+
<select v-model="theme">
|
|
340
|
+
<option value="light">Light</option>
|
|
341
|
+
<option value="dark">Dark</option>
|
|
342
|
+
</select>
|
|
343
|
+
<ul><li v-for="n in notes" :key="n.id">{{ n.body }}</li></ul>
|
|
344
|
+
</template>
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
### Shared `.hullrc` + run (all three)
|
|
348
|
+
|
|
349
|
+
`.hullrc` (optional — see [Configuration](#configuration-hullrc)):
|
|
350
|
+
|
|
351
|
+
```json
|
|
352
|
+
{
|
|
353
|
+
"appId": "com.you.my-app",
|
|
354
|
+
"window": { "title": "My App", "width": 1100, "height": 760 }
|
|
355
|
+
}
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
Then:
|
|
359
|
+
|
|
360
|
+
```bash
|
|
361
|
+
npm install
|
|
362
|
+
npm run dev # native window with HMR (+ a dev inspector tab)
|
|
363
|
+
npm run dev:browser # run the UI in your browser with the full bridge, no recompile
|
|
364
|
+
npm run build # single-file the UI + package with the host -> ./release
|
|
365
|
+
npm run start # run the packaged app
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
> The three recipes differ **only** in the UI layer. `@mwguerra/hull/bridge` (ping, db,
|
|
369
|
+
> files, settings, credentials, http, printers) is the same in all of them;
|
|
370
|
+
> `@mwguerra/hull/vue` and `@mwguerra/hull/react` add the `useNativeState` hook (Vanilla
|
|
371
|
+
> JS uses `nativeSetting` directly). For the complete, feature-by-feature versions, see
|
|
372
|
+
> the `examples/` apps in the repo.
|
|
373
|
+
|
|
374
|
+
## Try it from a blank project
|
|
375
|
+
|
|
376
|
+
Scaffold a fresh Vite app, add Hull, package it, and open the desktop window. The same
|
|
377
|
+
commands work on **Windows, macOS, and Linux** — `@mwguerra/hull` pulls the prebuilt
|
|
378
|
+
host for your OS/CPU automatically, and `hull start` opens the packaged app.
|
|
379
|
+
|
|
380
|
+
**Vue:**
|
|
381
|
+
|
|
382
|
+
```bash
|
|
383
|
+
npm create vite@latest my-hull-app -- --template vue
|
|
384
|
+
cd my-hull-app
|
|
385
|
+
npm install
|
|
386
|
+
npm i -D @mwguerra/hull
|
|
387
|
+
npx hull build # bundle the UI + package it with the native host
|
|
388
|
+
npx hull start # open the desktop app
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
**React:**
|
|
392
|
+
|
|
393
|
+
```bash
|
|
394
|
+
npm create vite@latest my-hull-app -- --template react
|
|
395
|
+
cd my-hull-app
|
|
396
|
+
npm install
|
|
397
|
+
npm i -D @mwguerra/hull
|
|
398
|
+
npx hull build
|
|
399
|
+
npx hull start
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
> Want a live-reload window without packaging first? Run `npx hull dev` instead of
|
|
403
|
+
> `build` + `start`. Plain JS works too — use `--template vanilla`. To wire `hull` into
|
|
404
|
+
> your npm scripts, see the [recipes above](#integrate-your-project).
|
|
405
|
+
|
|
406
|
+
## Talking to the backend
|
|
407
|
+
|
|
408
|
+
Every call goes UI → C++ and returns a Promise; all the real work happens in the
|
|
409
|
+
native host.
|
|
410
|
+
|
|
411
|
+
```js
|
|
412
|
+
import { ping, httpPost, saveCredential, isNative } from "@mwguerra/hull/bridge";
|
|
413
|
+
|
|
414
|
+
await ping("hello"); // -> { ok: true, echo: "hello" }
|
|
415
|
+
const res = await httpPost("https://api.example.com/x", { a: 1 }); // TLS, in C++
|
|
416
|
+
await saveCredential("api.example.com", "default", token); // -> OS keychain
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
Structured persistence with embedded SQLite (parameterized, stored per-user):
|
|
420
|
+
|
|
421
|
+
```js
|
|
422
|
+
import { db } from "@mwguerra/hull/bridge";
|
|
423
|
+
await db.migrate(["CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT NOT NULL)"]);
|
|
424
|
+
await db.exec("INSERT INTO notes (body) VALUES (?)", ["hello"]);
|
|
425
|
+
const notes = await db.query("SELECT * FROM notes ORDER BY id DESC");
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
Files / uploads (e.g. show an uploaded image — the pattern the examples use):
|
|
429
|
+
|
|
430
|
+
```js
|
|
431
|
+
import { files } from "@mwguerra/hull/bridge";
|
|
432
|
+
await files.write(file.name, file); // string | Uint8Array | ArrayBuffer | Blob
|
|
433
|
+
const bytes = await files.read(file.name); // Uint8Array
|
|
434
|
+
const url = URL.createObjectURL(new Blob([bytes], { type: "image/png" }));
|
|
435
|
+
imgEl.src = url; // preview; URL.revokeObjectURL(url) later
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
Two-way persisted state (plaintext by default; encrypted at rest in the secure build):
|
|
439
|
+
|
|
440
|
+
```js
|
|
441
|
+
// Vue
|
|
442
|
+
import { useNativeState } from "@mwguerra/hull/vue";
|
|
443
|
+
const theme = useNativeState("theme"); // a ref; edits persist, C++ pushes sync back
|
|
444
|
+
|
|
445
|
+
// React
|
|
446
|
+
import { useNativeState } from "@mwguerra/hull/react";
|
|
447
|
+
const [theme, setTheme] = useNativeState("theme");
|
|
448
|
+
|
|
449
|
+
// Vanilla JS
|
|
450
|
+
import { nativeSetting } from "@mwguerra/hull/bridge";
|
|
451
|
+
const theme = nativeSetting("theme"); // .get() / .set(v) / .subscribe(fn) / .load()
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
## Bridge API reference
|
|
455
|
+
|
|
456
|
+
All from `@mwguerra/hull/bridge`:
|
|
457
|
+
|
|
458
|
+
| Function | Backend |
|
|
459
|
+
|----------|---------|
|
|
460
|
+
| `ping(text)` | sync echo (diagnostics) |
|
|
461
|
+
| `httpPost(url, body)` / `httpGet(url)` | cpp-httplib + OpenSSL, on a worker thread; injects a `Bearer` token from the keychain |
|
|
462
|
+
| `saveSetting` / `loadSetting` / `loadAllSettings` | per-user store (plaintext by default; AES in the secure build) |
|
|
463
|
+
| `nativeSetting(key)` | two-way setting store: `.get()` / `.set(v)` / `.subscribe(fn)` / `.load()` |
|
|
464
|
+
| `saveCredential` / `credentialExists` / `eraseCredential` | OS keychain; **write-only** — secrets never return to JS |
|
|
465
|
+
| `listPrinters` | discover printers (Winspool / CUPS) |
|
|
466
|
+
| `printMessage(printer, text)` | print a **text document** — works with any printer (Print to PDF, OneNote, laser) |
|
|
467
|
+
| `printReceipt(printer, text)` / `printNetwork(host, port, text)` | raw **ESC/POS** for thermal receipt printers (spooler / TCP port-9100) |
|
|
468
|
+
| `db.query` / `db.get` / `db.exec` / `db.batch` / `db.migrate` | embedded SQLite, parameterized, per-user storage |
|
|
469
|
+
| `files.write` / `read` / `readText` / `list` / `remove` | file/upload storage in the per-user dir (through the secure layer) |
|
|
470
|
+
| `appInfo()` | `{ ok, appId, secure }` — `secure` true on a crypto build |
|
|
471
|
+
| `bridge.on(event, fn)` | subscribe to C++ → UI push events (e.g. `settings:changed`); returns an unsubscribe fn |
|
|
472
|
+
| `hasBridge()` / `isNative()` / `bridgeMode()` | `hasBridge` = reachable (native or browser dev); `isNative` = native web view; `bridgeMode` = `"native"`/`"http"`/`"none"` |
|
|
473
|
+
|
|
474
|
+
Framework hooks: `useNativeState(key)` from `@mwguerra/hull/vue` (returns a ref) and
|
|
475
|
+
`@mwguerra/hull/react` (returns `[value, setValue]`).
|
|
476
|
+
|
|
477
|
+
## CLI commands
|
|
478
|
+
|
|
479
|
+
| Command | What it does |
|
|
480
|
+
|---------|--------------|
|
|
481
|
+
| `hull dev` | Vite dev server rendered in a native window (HMR) + a dev inspector tab |
|
|
482
|
+
| `hull dev --browser` | run the UI in your browser with the full bridge over HTTP/SSE (no recompile) |
|
|
483
|
+
| `hull build [vX.Y.Z]` | single-file the UI and package it with the host into `release/<version\|development>/<platform>/` + an archive |
|
|
484
|
+
| `hull build … --platform <key\|all>` | also package other platforms whose host binary is present; `--format zip\|tar.gz` |
|
|
485
|
+
| `hull start [vX.Y.Z]` | run a packaged build |
|
|
486
|
+
| `hull installer [vX.Y.Z]` | wrap the build into a native installer — `.dmg` (macOS), `.deb` (Linux), `.exe` (Windows) |
|
|
487
|
+
| `hull eject` | copy the C++ host project into `./desktop` to add native bindings |
|
|
488
|
+
|
|
489
|
+
Add `-v` / `--verbose` to any command for per-step timings (every command prints its
|
|
490
|
+
total time). The version argument must match `vX.Y.Z` (optionally `-suffix`); with no
|
|
491
|
+
version, output goes to a `development/` folder.
|
|
492
|
+
|
|
493
|
+
> Pass **flags** like `--platform` via `npx hull …` (or the binary directly). `npm run`
|
|
494
|
+
> swallows unknown flags, so `npm run build -- v1.2.3` works for the version but
|
|
495
|
+
> `--platform` won't reach Hull through it.
|
|
496
|
+
|
|
497
|
+
## Configuration (`.hullrc`)
|
|
498
|
+
|
|
499
|
+
Drop a `.hullrc` (JSON) in your project root — only the keys you set override the
|
|
500
|
+
package defaults. Lookup order: `.hullrc` → `.hullrc.json` → `hull.config.json`.
|
|
501
|
+
|
|
502
|
+
```json
|
|
503
|
+
{
|
|
504
|
+
"appId": "com.you.notes",
|
|
505
|
+
"secure": false,
|
|
506
|
+
"window": { "title": "Notes", "width": 1200, "height": 800, "icon": "build/icon.png" }
|
|
507
|
+
}
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
| Key | Default | Meaning |
|
|
511
|
+
|-----|---------|---------|
|
|
512
|
+
| `appId` | `com.hull.<pkg name>` | namespaces the store, DB, files, and keychain entries so multiple Hull apps never collide |
|
|
513
|
+
| `window.title` | pkg `productName`/`name` | native window title |
|
|
514
|
+
| `window.width` / `window.height` | `1100` / `760` | window size |
|
|
515
|
+
| `window.icon` (or top-level `icon`) | bundled Hull logo | PNG/ICO for the window/app icon; set at runtime on Windows (GDI+), via the app bundle on macOS/Linux; SVG is not a valid native icon |
|
|
516
|
+
| `secure` | `false` | run the crypto host build (`hull-host-secure`): AES files/settings + SQLCipher DB |
|
|
517
|
+
| `debug` | `false` | open the web-view dev tools |
|
|
518
|
+
| `outDir` | `dist` | Vite UI build dir |
|
|
519
|
+
| `releaseDir` | `release` | packaged-app output dir |
|
|
520
|
+
|
|
521
|
+
## Develop in the browser (no recompile)
|
|
522
|
+
|
|
523
|
+
```bash
|
|
524
|
+
npm run dev -- --browser # or: npx hull dev --browser
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
Runs the UI in your **browser** with full Vite HMR while bridge calls still reach the
|
|
528
|
+
real native backend over HTTP/SSE — change a label, hit reload, no recompile. Both
|
|
529
|
+
`hull dev` and `--browser` also open a dev-only **inspector** (live bridge calls,
|
|
530
|
+
events, DB/file ops, timings) that is **stripped from production builds**
|
|
531
|
+
(`import.meta.env.DEV` dead-code elimination).
|
|
532
|
+
|
|
533
|
+
## Versioned releases
|
|
534
|
+
|
|
535
|
+
```bash
|
|
536
|
+
npm run build # -> release/development/<platform>/ + archive
|
|
537
|
+
npm run build -- v1.2.3 # -> release/v1.2.3/<platform>/ + archive
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
Each build emits a self-contained, versioned bundle and a ready-to-ship archive (`.zip`
|
|
541
|
+
on Windows, `.tar.gz` on macOS/Linux) with the minimal runnable set — the host binary,
|
|
542
|
+
the libraries it needs, your inlined `app.html`, a double-click launcher, and `icon.png`
|
|
543
|
+
if you configured one. Unpack on the target and run. `--platform all` also packages
|
|
544
|
+
other platforms whose host binary is installed (realistically produced via CI, one
|
|
545
|
+
runner per OS). With `secure: true`, bundle dirs and archives get a `-secure` suffix.
|
|
546
|
+
|
|
547
|
+
### Native installers
|
|
548
|
+
|
|
549
|
+
After a build, wrap it into a native installer for the **current** OS:
|
|
550
|
+
|
|
551
|
+
```bash
|
|
552
|
+
npm run build && npx hull installer # -> release/<version>/<App>-<version>-<key>.<dmg|deb|exe>
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
| OS | Output | Tooling |
|
|
556
|
+
|----|--------|---------|
|
|
557
|
+
| macOS | `.dmg` (the `.app` + an Applications drop-link) | `hdiutil` (built in) |
|
|
558
|
+
| Linux | `.deb` (installs to `/opt`, registers the `.desktop` + icon, deps via `dpkg-shlibdeps`) | `dpkg-deb` (built in) |
|
|
559
|
+
| Windows | `.exe` (per-user install, Start-Menu/Desktop shortcuts, uninstaller) | [Inno Setup](https://jrsoftware.org/isinfo.php) — `winget install JRSoftware.InnoSetup` |
|
|
560
|
+
|
|
561
|
+
Each is built on its own OS (the tools are OS-native), like the host. Install with:
|
|
562
|
+
double-click the `.dmg` and drag to Applications; `sudo apt install ./<app>.deb`; run the
|
|
563
|
+
`.exe`. Unsigned for now — for distribution to other machines, add code-signing
|
|
564
|
+
(macOS notarization / Windows Authenticode) as a later step.
|
|
565
|
+
|
|
566
|
+
## Security (at-rest crypto is a build option)
|
|
567
|
+
|
|
568
|
+
Default build = **no crypto, everything fast** (plaintext at rest; secrets still in the
|
|
569
|
+
keychain). For encryption at rest, use the **secure build**:
|
|
570
|
+
|
|
571
|
+
```bash
|
|
572
|
+
npm run build:host:secure # AES for files/settings + SQLCipher for the DB
|
|
573
|
+
# then in .hullrc: { "secure": true }
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
Files and the DB go through one crypto **layer** — nothing calls cryptography directly.
|
|
577
|
+
SQLite is also hardened in all builds: `PRAGMA trusted_schema=OFF` on every connection,
|
|
578
|
+
and the default build compiles with `SQLITE_OMIT_LOAD_EXTENSION` and `SQLITE_DQS=0`.
|
|
579
|
+
Queries are always parameterized (bound in C++), and `exec`/`query`/`get` run one
|
|
580
|
+
statement each.
|
|
581
|
+
|
|
582
|
+
## Custom native code
|
|
583
|
+
|
|
584
|
+
Need your own C++ binding? Run `hull eject` to copy the host project into `./desktop`,
|
|
585
|
+
add a binding (`d.on("myThing", (args, reply) => reply({ ok: true }))`), and build it
|
|
586
|
+
with CMake. The standard bindings (HTTP / storage / keychain / printing / DB / files)
|
|
587
|
+
are already there to extend. See `desktop/README.md`.
|
|
588
|
+
|
|
589
|
+
## Platform support
|
|
590
|
+
|
|
591
|
+
| | Windows | macOS | Linux |
|
|
592
|
+
|---|---------|-------|-------|
|
|
593
|
+
| Web view | WebView2 (Edge) | WebKit | WebKitGTK 6 |
|
|
594
|
+
| Credentials | Credential Manager | Keychain | libsecret |
|
|
595
|
+
| Printing | Winspool | CUPS | CUPS |
|
|
596
|
+
| Window icon | runtime (GDI+) | `.app` bundle (built by `hull build`) | auto `.desktop` + icon-theme install |
|
|
597
|
+
| `hull build` output | folder + `.cmd` launcher (zip) | **`.app` bundle** (tar.gz) | folder + `.sh` launcher (tar.gz) |
|
|
598
|
+
| Build the host on | Windows | macOS | any OS via Docker, or native Linux |
|
|
599
|
+
|
|
600
|
+
End users only need the OS web-view runtime (preinstalled on Windows 11 and macOS;
|
|
601
|
+
`libwebkitgtk-6.0` on Linux). A host must be built on its own OS — true cross-compile
|
|
602
|
+
isn't realistic for WebView2/WebKit — except Linux, which builds from any OS via Docker.
|
|
603
|
+
|
|
604
|
+
**Linux sandbox note:** WebKitGTK sandboxes its subprocesses with bubblewrap, which
|
|
605
|
+
needs unprivileged user namespaces. They're blocked on Ubuntu 24.04 (AppArmor default)
|
|
606
|
+
and in many containers, which otherwise crashes the app with
|
|
607
|
+
`bwrap: setting up uid map: Permission denied`. Hull's host **auto-detects** this and
|
|
608
|
+
disables the sandbox so the app still runs (with a notice). Override with
|
|
609
|
+
`hull start --no-sandbox`, `.hullrc` `{ "linux": { "sandbox": false } }`, or keep it by
|
|
610
|
+
enabling userns (`sudo sysctl kernel.apparmor_restrict_unprivileged_userns=0`). See
|
|
611
|
+
[platforms.md](https://github.com/mwguerra/hull/blob/main/docs/platforms.md#troubleshooting).
|
|
612
|
+
|
|
613
|
+
**Linux icon note:** GTK4 has no runtime "set icon from a PNG", so on `hull dev`/`start`
|
|
614
|
+
the host auto-installs desktop integration — it writes `~/.local/share/applications/<appId>.desktop`
|
|
615
|
+
and the icon into the user icon theme, and sets the window's app-id so the compositor
|
|
616
|
+
shows it (Wayland + X11). A new icon may need a moment or a re-login for the shell to
|
|
617
|
+
pick it up. See [configuration.md](https://github.com/mwguerra/hull/blob/main/docs/configuration.md#the-windowicon-key).
|
|
618
|
+
|
|
619
|
+
## How it works
|
|
620
|
+
|
|
621
|
+
- Prebuilt host binaries are delivered as platform-gated optional dependencies
|
|
622
|
+
(`@mwguerra/hull-win32-x64`, …) — npm installs only the one for your machine.
|
|
623
|
+
- `hull build` uses your project's Vite plus `vite-plugin-singlefile` to inline the
|
|
624
|
+
whole UI into one HTML file, then bundles it with the host.
|
|
625
|
+
- The host loads that file at runtime (`--app`) in production, or your dev server
|
|
626
|
+
(`--url`) during development. The bridge is exposed over the web view natively, or
|
|
627
|
+
over HTTP/SSE in browser dev mode.
|
|
628
|
+
|
|
629
|
+
## License
|
|
630
|
+
|
|
631
|
+
MIT
|
|
Binary file
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32" fill="none">
|
|
2
|
+
<rect width="32" height="32" rx="8" fill="#10b981" fill-opacity="0.15"></rect>
|
|
3
|
+
<path d="M7 13H25" stroke="#10b981" stroke-width="2.5" stroke-linecap="round"></path>
|
|
4
|
+
<path d="M8 13Q16 26 24 13" stroke="#10b981" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"></path>
|
|
5
|
+
</svg>
|
package/bin/hull.js
ADDED