knobkit 0.0.1 → 0.0.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.
- package/README.md +153 -0
- package/dist/cli/config.js +2 -0
- package/dist/cli/index.js +31 -14
- package/dist/lib/favicon.d.ts +1 -0
- package/dist/lib/favicon.js +15 -0
- package/dist/server/serve.js +2 -1
- package/package.json +25 -2
- package/src/cli/config.ts +2 -0
- package/src/cli/index.ts +35 -16
- package/src/lib/favicon.ts +16 -0
- package/src/server/serve.ts +2 -1
package/README.md
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# <img src="https://raw.githubusercontent.com/knobkit/knobkit/main/design/logo.svg" height="28" alt="" /> knobkit
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/knobkit) [](https://github.com/knobkit/knobkit/blob/main/LICENSE)
|
|
4
|
+
|
|
5
|
+
**Your AI app, live as you type.** Declare widgets, write event handlers — done. The same file runs
|
|
6
|
+
entirely in the browser — no server at all — or with handlers on a stateless Node server. Swap one line.
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
import { knobkit, chat } from "knobkit";
|
|
10
|
+
|
|
11
|
+
const convo = chat({ placeholder: "Say something…" });
|
|
12
|
+
|
|
13
|
+
const app = knobkit({
|
|
14
|
+
title: "Echo bot",
|
|
15
|
+
description: "Whatever you send comes back reversed.",
|
|
16
|
+
widgets: [convo],
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
app.on(convo.sent, async ({ text }) => {
|
|
20
|
+
convo.say({ role: "user", content: text });
|
|
21
|
+
convo.say({ role: "assistant", content: [...text].reverse().join("") });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
app.serve();
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
That's a complete app. `app.serve()` runs the handler on Node; change that one line to
|
|
28
|
+
`app.mount("#root")` and the identical file runs fully client-side. For a real model-backed chatbot
|
|
29
|
+
(streaming, history), see [`examples/`](https://github.com/knobkit/knobkit/tree/main/examples).
|
|
30
|
+
|
|
31
|
+
## Why not Gradio or Streamlit?
|
|
32
|
+
|
|
33
|
+
Same authoring feel — declare inputs, write a function, get an app — different architecture:
|
|
34
|
+
|
|
35
|
+
- **The browser owns all state.** A knobkit server keeps nothing. A handler pulls only the
|
|
36
|
+
attributes it actually reads and writes back structured edits. No sessions, no sticky state —
|
|
37
|
+
restart, redeploy, or scale the server and no one's app breaks.
|
|
38
|
+
- **One file, two tiers.** Prototype entirely in the browser (`mount` apps build to static files —
|
|
39
|
+
host them anywhere, run WebGPU models client-side), then move handlers to a server when you need
|
|
40
|
+
secrets, large models, or native deps — by changing the last line.
|
|
41
|
+
- **Events, not reruns.** Handlers are plain `on(event, async fn)` functions. No full-script
|
|
42
|
+
re-execution on every interaction, no reactive graph to fight, no widget-key gymnastics.
|
|
43
|
+
- **Data stays where it lives.** A request crossing the wire is just `{ type, payload }` — never a
|
|
44
|
+
copy of state. An attribute a handler never reads (say, generated audio) never leaves the browser.
|
|
45
|
+
|
|
46
|
+
## Quick start
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npm create knobkit@latest my-app # choose mount (browser) or serve (node)
|
|
50
|
+
cd my-app
|
|
51
|
+
npm install
|
|
52
|
+
npm run dev
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Skip the prompts with `npm create knobkit@latest my-app -- --mount` (or `--serve`). Already have a
|
|
56
|
+
project? `npm install knobkit`. Requires Node ≥ 22.
|
|
57
|
+
|
|
58
|
+
## CLI
|
|
59
|
+
|
|
60
|
+
The `knobkit` package installs a `knobkit` command:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
knobkit dev # dev server — auto-detects mount vs serve
|
|
64
|
+
knobkit build # build a browser (mount) app to dist/
|
|
65
|
+
knobkit serve # run a server (serve) app
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The entry file is the `"main"` field of your package.json — like `vite`, the manifest names the
|
|
69
|
+
entry. Pass a file to run something else: `knobkit dev other.tsx`. Flags: `--mount` / `--serve`
|
|
70
|
+
force a mode, `--port <n>` sets the dev-server port. Otherwise `knobkit dev` detects the tier from
|
|
71
|
+
whether your entry file ends in `mount()` or `serve()`.
|
|
72
|
+
|
|
73
|
+
## Concepts
|
|
74
|
+
|
|
75
|
+
- **Widgets** hold structured state and render themselves. Handlers interact through widget methods:
|
|
76
|
+
- **read** with async getters — `await convo.history()`, `await box.value()` — pulled from the browser.
|
|
77
|
+
- **write** with structured edits — `convo.say(m)`, `convo.append(token)`, `out.set(v)`, `log.push(line)`.
|
|
78
|
+
- **produce** by `return`ing an event from a handler (it's re-emitted, like a user action).
|
|
79
|
+
- **Events** are plain serializable `{ type, payload }`. `widget.sent("hi")` builds one.
|
|
80
|
+
- **`on(event, handler)`** registers a handler against a widget's event (`convo.sent`, `go.clicked`).
|
|
81
|
+
- **`setup(fn)`** runs once per session — in the browser on `mount`, per connection on `serve` — with a
|
|
82
|
+
live context, so async startup (load model weights, fetch a user's data) happens here. The page
|
|
83
|
+
renders first; setup is non-blocking.
|
|
84
|
+
- **`busy`** marks a transient working span on a widget — `widget.busy(handler)` wraps an async handler,
|
|
85
|
+
or `widget.busyStart()`/`busyEnd()` bracket one by hand (e.g. a `setup()` load). The widget shows a
|
|
86
|
+
thin indeterminate bar and drops its input events while busy. `disable()`/`enable()` is the persistent
|
|
87
|
+
version (dimmed).
|
|
88
|
+
- **`mount` vs `serve`** is the only thing that changes between in-browser and client/server — the
|
|
89
|
+
widgets, handlers, and methods are identical.
|
|
90
|
+
|
|
91
|
+
## The two runtimes
|
|
92
|
+
|
|
93
|
+
| | `mount("#root")` | `serve()` |
|
|
94
|
+
|---|---|---|
|
|
95
|
+
| State | in the browser | in the browser |
|
|
96
|
+
| `on(...)` handlers | run in the browser | run on a stateless Node server |
|
|
97
|
+
| Transport | local function call | socket.io |
|
|
98
|
+
| Use when | everything fits client-side (incl. WebGPU models) | the handler needs the server (large models, secrets, native deps) |
|
|
99
|
+
|
|
100
|
+
## Widgets
|
|
101
|
+
|
|
102
|
+
Inputs: `text`, `number`, `slider`, `dropdown`, `checkbox`, `checkboxGroup`, `radio`, `upload`, `mic`,
|
|
103
|
+
`webcam`, `chat`, `button`.
|
|
104
|
+
Outputs: `output` (plain text or `format: "markdown"`), `json`, `log`, `label`, `html`, `image`,
|
|
105
|
+
`gallery`, `annotatedImage` (boxes/labels over an image), `highlightedText` (spans over text), `audio`,
|
|
106
|
+
`video`, `file` (download), `progress`, `chart` (bar/line/area), `frame` (embed a URL or a sandboxed
|
|
107
|
+
HTML document in an isolated iframe).
|
|
108
|
+
Both (editable or read-only): `code` (syntax-highlighted editor/viewer), `table` (data grid).
|
|
109
|
+
Custom: `widget({ state, … })`.
|
|
110
|
+
|
|
111
|
+
## Layout
|
|
112
|
+
|
|
113
|
+
`widgets` is the widget tree. An array is a vertical stack; `row`, `col`, and `grid` are containers
|
|
114
|
+
that nest other widgets — composed with the widget objects themselves, no keys or strings:
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
import { knobkit, upload, dropdown, button, output, row, col } from "knobkit";
|
|
118
|
+
|
|
119
|
+
knobkit({ widgets: col(photo, row(size, go), caption) });
|
|
120
|
+
knobkit({ widgets: grid([a, b, c, d], { cols: 2 }) });
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`tabs` and `accordion` are containers too — `tabs([{ label, content }, …])`,
|
|
124
|
+
`accordion({ label, open }, …children)`. Containers are themselves widgets whose state is their
|
|
125
|
+
arrangement, so a handler can restructure the UI at runtime — `panel.add(chart)`, `panel.remove(sidebar)`.
|
|
126
|
+
|
|
127
|
+
## Examples
|
|
128
|
+
|
|
129
|
+
In [`examples/`](https://github.com/knobkit/knobkit/tree/main/examples) — each is a single
|
|
130
|
+
`demo.tsx`. Run one with `pnpm -F knobkit-example-<name> dev`.
|
|
131
|
+
|
|
132
|
+
[`examples/playground`](https://github.com/knobkit/knobkit/tree/main/examples/playground) is an
|
|
133
|
+
in-browser playground: a `code` editor on the right runs live as a mounted knobkit app on the left,
|
|
134
|
+
with a `dropdown` of built-in examples — itself a knobkit app built from `code`/`dropdown`/layout.
|
|
135
|
+
Run it with `pnpm -F knobkit-example-playground dev`.
|
|
136
|
+
|
|
137
|
+
## Develop
|
|
138
|
+
|
|
139
|
+
Requires Node ≥ 22 and pnpm.
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
pnpm install
|
|
143
|
+
pnpm -F knobkit build # build the library + browser client bundle
|
|
144
|
+
pnpm -F knobkit test # vitest
|
|
145
|
+
pnpm typecheck # all packages
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
See [CLAUDE.md](https://github.com/knobkit/knobkit/blob/main/CLAUDE.md) for the architecture and how
|
|
149
|
+
to add a widget.
|
|
150
|
+
|
|
151
|
+
## License
|
|
152
|
+
|
|
153
|
+
[MIT](https://github.com/knobkit/knobkit/blob/main/LICENSE)
|
package/dist/cli/config.js
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { dirname, relative, resolve } from "node:path";
|
|
4
4
|
import { searchForWorkspaceRoot } from "vite";
|
|
5
|
+
import { FAVICON_TAG } from "../lib/favicon.js";
|
|
5
6
|
export function ensureTsconfig(root) {
|
|
6
7
|
const path = resolve(root, "tsconfig.json");
|
|
7
8
|
if (existsSync(path))
|
|
@@ -41,6 +42,7 @@ export function indexHtml(entry) {
|
|
|
41
42
|
<meta charset="utf-8" />
|
|
42
43
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
43
44
|
<title>knobkit</title>
|
|
45
|
+
${FAVICON_TAG}
|
|
44
46
|
</head>
|
|
45
47
|
<body>
|
|
46
48
|
<div id="root"></div>
|
package/dist/cli/index.js
CHANGED
|
@@ -7,11 +7,11 @@ import { runServe } from "./serve.js";
|
|
|
7
7
|
const HELP = `knobkit — build a web app from widgets and event handlers
|
|
8
8
|
|
|
9
9
|
Usage:
|
|
10
|
-
knobkit dev
|
|
11
|
-
knobkit build
|
|
12
|
-
knobkit serve
|
|
10
|
+
knobkit dev Start a dev server (auto-detects mount vs serve)
|
|
11
|
+
knobkit build Build a browser (mount) app to dist/
|
|
12
|
+
knobkit serve Run a server (serve) app (same as: knobkit dev --serve)
|
|
13
13
|
|
|
14
|
-
file
|
|
14
|
+
The entry file is "main" in package.json; pass a file to override (knobkit dev other.tsx)
|
|
15
15
|
|
|
16
16
|
Flags:
|
|
17
17
|
--mount Force browser (mount) mode
|
|
@@ -19,8 +19,7 @@ Flags:
|
|
|
19
19
|
--port <n> Dev server port (mount)
|
|
20
20
|
`;
|
|
21
21
|
function parse(argv) {
|
|
22
|
-
const out = {
|
|
23
|
-
let sawFile = false;
|
|
22
|
+
const out = { mount: false, serve: false };
|
|
24
23
|
for (let i = 0; i < argv.length; i++) {
|
|
25
24
|
const a = argv[i];
|
|
26
25
|
if (a === "--mount")
|
|
@@ -31,13 +30,35 @@ function parse(argv) {
|
|
|
31
30
|
out.port = Number(argv[++i]);
|
|
32
31
|
else if (a.startsWith("--port="))
|
|
33
32
|
out.port = Number(a.slice("--port=".length));
|
|
34
|
-
else if (!a.startsWith("-") &&
|
|
33
|
+
else if (!a.startsWith("-") && out.file === undefined)
|
|
35
34
|
out.file = a;
|
|
36
|
-
sawFile = true;
|
|
37
|
-
}
|
|
38
35
|
}
|
|
39
36
|
return out;
|
|
40
37
|
}
|
|
38
|
+
function entry(root, args) {
|
|
39
|
+
if (args.file) {
|
|
40
|
+
const file = resolve(root, args.file);
|
|
41
|
+
if (!existsSync(file)) {
|
|
42
|
+
process.stderr.write(`knobkit: no such file: ${args.file}\n`);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
return file;
|
|
46
|
+
}
|
|
47
|
+
const pkgPath = resolve(root, "package.json");
|
|
48
|
+
const main = existsSync(pkgPath)
|
|
49
|
+
? JSON.parse(readFileSync(pkgPath, "utf8")).main
|
|
50
|
+
: undefined;
|
|
51
|
+
if (!main) {
|
|
52
|
+
process.stderr.write(`knobkit: no entry file — pass one (knobkit dev app.tsx) or set "main" in package.json\n`);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
const file = resolve(root, main);
|
|
56
|
+
if (!existsSync(file)) {
|
|
57
|
+
process.stderr.write(`knobkit: package.json "main" points to a missing file: ${main}\n`);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
return file;
|
|
61
|
+
}
|
|
41
62
|
function mode(file, args) {
|
|
42
63
|
if (args.serve)
|
|
43
64
|
return "serve";
|
|
@@ -57,11 +78,7 @@ async function main() {
|
|
|
57
78
|
}
|
|
58
79
|
const args = parse(rest);
|
|
59
80
|
const root = process.cwd();
|
|
60
|
-
const file =
|
|
61
|
-
if (!existsSync(file)) {
|
|
62
|
-
process.stderr.write(`knobkit: no such file: ${args.file}\n`);
|
|
63
|
-
process.exit(1);
|
|
64
|
-
}
|
|
81
|
+
const file = entry(root, args);
|
|
65
82
|
ensureTsconfig(root);
|
|
66
83
|
if (cmd === "build")
|
|
67
84
|
return buildMount(root, file);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const FAVICON_TAG: string;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// The K-Tile logo (design/logo.svg) as a data URI, so served and mounted apps get a favicon
|
|
2
|
+
// without serving an extra file.
|
|
3
|
+
const SVG = [
|
|
4
|
+
"%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E",
|
|
5
|
+
"%3Crect x='4' y='4' width='56' height='56' rx='14' fill='%232563eb'/%3E",
|
|
6
|
+
"%3Cg stroke='%23fff' stroke-width='7' stroke-linecap='round'%3E",
|
|
7
|
+
"%3Cline x1='23' y1='16' x2='23' y2='48'/%3E",
|
|
8
|
+
"%3Cline x1='23' y1='33' x2='42' y2='18'/%3E",
|
|
9
|
+
"%3Cline x1='23' y1='33' x2='42' y2='46'/%3E",
|
|
10
|
+
"%3C/g%3E",
|
|
11
|
+
"%3Ccircle cx='23' cy='33' r='6.5' fill='%23fff'/%3E",
|
|
12
|
+
"%3Ccircle cx='23' cy='33' r='2.8' fill='%231d4ed8'/%3E",
|
|
13
|
+
"%3C/svg%3E",
|
|
14
|
+
].join("");
|
|
15
|
+
export const FAVICON_TAG = `<link rel="icon" href="data:image/svg+xml,${SVG}" />`;
|
package/dist/server/serve.js
CHANGED
|
@@ -5,6 +5,7 @@ import { readFile } from "node:fs/promises";
|
|
|
5
5
|
import { Server } from "socket.io";
|
|
6
6
|
import { declare } from "../lib/declare.js";
|
|
7
7
|
import { makeBound } from "../lib/ctx.js";
|
|
8
|
+
import { FAVICON_TAG } from "../lib/favicon.js";
|
|
8
9
|
import { run } from "./context.js";
|
|
9
10
|
const CLIENT_JS = fileURLToPath(new URL("../../dist/client.js", import.meta.url));
|
|
10
11
|
const CLIENT_CSS = fileURLToPath(new URL("../../dist/client.css", import.meta.url));
|
|
@@ -13,7 +14,7 @@ const isEvent = (x) => x != null && typeof x.type === "string" && "payload" in x
|
|
|
13
14
|
function html(decl, loading) {
|
|
14
15
|
return `<!doctype html><html lang="en"><head><meta charset="utf-8" />
|
|
15
16
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
16
|
-
<title>${decl.title ?? "knobkit"}</title
|
|
17
|
+
<title>${decl.title ?? "knobkit"}</title>${FAVICON_TAG}<link rel="stylesheet" href="/client.css" /></head>
|
|
17
18
|
<body><div id="root">${loading}</div><script type="module" src="/client.js"></script></body></html>`;
|
|
18
19
|
}
|
|
19
20
|
// node-only. The browser owns state and renders; the server is stateless. It serves the decl + client
|
package/package.json
CHANGED
|
@@ -1,9 +1,32 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "knobkit",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.0.3",
|
|
4
|
+
"description": "Your AI app, live as you type — declare widgets, write event handlers, done. The same file runs in the browser or on a stateless Node server.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"widgets",
|
|
7
|
+
"ui",
|
|
8
|
+
"framework",
|
|
9
|
+
"declarative",
|
|
10
|
+
"web-app",
|
|
11
|
+
"react",
|
|
12
|
+
"events",
|
|
13
|
+
"isomorphic",
|
|
14
|
+
"rapid-prototyping"
|
|
15
|
+
],
|
|
5
16
|
"license": "MIT",
|
|
6
17
|
"author": "Alain Brown",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/knobkit/knobkit.git",
|
|
21
|
+
"directory": "packages/knobkit"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://github.com/knobkit/knobkit#readme",
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/knobkit/knobkit/issues"
|
|
26
|
+
},
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=22"
|
|
29
|
+
},
|
|
7
30
|
"type": "module",
|
|
8
31
|
"types": "./dist/lib/index.d.ts",
|
|
9
32
|
"exports": {
|
package/src/cli/config.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { dirname, relative, resolve } from "node:path";
|
|
4
4
|
import { searchForWorkspaceRoot, type InlineConfig, type Plugin } from "vite";
|
|
5
|
+
import { FAVICON_TAG } from "../lib/favicon.js";
|
|
5
6
|
|
|
6
7
|
export function ensureTsconfig(root: string): void {
|
|
7
8
|
const path = resolve(root, "tsconfig.json");
|
|
@@ -41,6 +42,7 @@ export function indexHtml(entry: string): string {
|
|
|
41
42
|
<meta charset="utf-8" />
|
|
42
43
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
43
44
|
<title>knobkit</title>
|
|
45
|
+
${FAVICON_TAG}
|
|
44
46
|
</head>
|
|
45
47
|
<body>
|
|
46
48
|
<div id="root"></div>
|
package/src/cli/index.ts
CHANGED
|
@@ -8,11 +8,11 @@ import { runServe } from "./serve.js";
|
|
|
8
8
|
const HELP = `knobkit — build a web app from widgets and event handlers
|
|
9
9
|
|
|
10
10
|
Usage:
|
|
11
|
-
knobkit dev
|
|
12
|
-
knobkit build
|
|
13
|
-
knobkit serve
|
|
11
|
+
knobkit dev Start a dev server (auto-detects mount vs serve)
|
|
12
|
+
knobkit build Build a browser (mount) app to dist/
|
|
13
|
+
knobkit serve Run a server (serve) app (same as: knobkit dev --serve)
|
|
14
14
|
|
|
15
|
-
file
|
|
15
|
+
The entry file is "main" in package.json; pass a file to override (knobkit dev other.tsx)
|
|
16
16
|
|
|
17
17
|
Flags:
|
|
18
18
|
--mount Force browser (mount) mode
|
|
@@ -21,29 +21,52 @@ Flags:
|
|
|
21
21
|
`;
|
|
22
22
|
|
|
23
23
|
interface Args {
|
|
24
|
-
file
|
|
24
|
+
file?: string;
|
|
25
25
|
mount: boolean;
|
|
26
26
|
serve: boolean;
|
|
27
27
|
port?: number;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
function parse(argv: string[]): Args {
|
|
31
|
-
const out: Args = {
|
|
32
|
-
let sawFile = false;
|
|
31
|
+
const out: Args = { mount: false, serve: false };
|
|
33
32
|
for (let i = 0; i < argv.length; i++) {
|
|
34
33
|
const a = argv[i];
|
|
35
34
|
if (a === "--mount") out.mount = true;
|
|
36
35
|
else if (a === "--serve") out.serve = true;
|
|
37
36
|
else if (a === "--port") out.port = Number(argv[++i]);
|
|
38
37
|
else if (a.startsWith("--port=")) out.port = Number(a.slice("--port=".length));
|
|
39
|
-
else if (!a.startsWith("-") &&
|
|
40
|
-
out.file = a;
|
|
41
|
-
sawFile = true;
|
|
42
|
-
}
|
|
38
|
+
else if (!a.startsWith("-") && out.file === undefined) out.file = a;
|
|
43
39
|
}
|
|
44
40
|
return out;
|
|
45
41
|
}
|
|
46
42
|
|
|
43
|
+
function entry(root: string, args: Args): string {
|
|
44
|
+
if (args.file) {
|
|
45
|
+
const file = resolve(root, args.file);
|
|
46
|
+
if (!existsSync(file)) {
|
|
47
|
+
process.stderr.write(`knobkit: no such file: ${args.file}\n`);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
return file;
|
|
51
|
+
}
|
|
52
|
+
const pkgPath = resolve(root, "package.json");
|
|
53
|
+
const main = existsSync(pkgPath)
|
|
54
|
+
? (JSON.parse(readFileSync(pkgPath, "utf8")) as { main?: string }).main
|
|
55
|
+
: undefined;
|
|
56
|
+
if (!main) {
|
|
57
|
+
process.stderr.write(
|
|
58
|
+
`knobkit: no entry file — pass one (knobkit dev app.tsx) or set "main" in package.json\n`,
|
|
59
|
+
);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
const file = resolve(root, main);
|
|
63
|
+
if (!existsSync(file)) {
|
|
64
|
+
process.stderr.write(`knobkit: package.json "main" points to a missing file: ${main}\n`);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
return file;
|
|
68
|
+
}
|
|
69
|
+
|
|
47
70
|
function mode(file: string, args: Args): "mount" | "serve" {
|
|
48
71
|
if (args.serve) return "serve";
|
|
49
72
|
if (args.mount) return "mount";
|
|
@@ -63,11 +86,7 @@ async function main(): Promise<void> {
|
|
|
63
86
|
|
|
64
87
|
const args = parse(rest);
|
|
65
88
|
const root = process.cwd();
|
|
66
|
-
const file =
|
|
67
|
-
if (!existsSync(file)) {
|
|
68
|
-
process.stderr.write(`knobkit: no such file: ${args.file}\n`);
|
|
69
|
-
process.exit(1);
|
|
70
|
-
}
|
|
89
|
+
const file = entry(root, args);
|
|
71
90
|
ensureTsconfig(root);
|
|
72
91
|
|
|
73
92
|
if (cmd === "build") return buildMount(root, file);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// The K-Tile logo (design/logo.svg) as a data URI, so served and mounted apps get a favicon
|
|
2
|
+
// without serving an extra file.
|
|
3
|
+
const SVG = [
|
|
4
|
+
"%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E",
|
|
5
|
+
"%3Crect x='4' y='4' width='56' height='56' rx='14' fill='%232563eb'/%3E",
|
|
6
|
+
"%3Cg stroke='%23fff' stroke-width='7' stroke-linecap='round'%3E",
|
|
7
|
+
"%3Cline x1='23' y1='16' x2='23' y2='48'/%3E",
|
|
8
|
+
"%3Cline x1='23' y1='33' x2='42' y2='18'/%3E",
|
|
9
|
+
"%3Cline x1='23' y1='33' x2='42' y2='46'/%3E",
|
|
10
|
+
"%3C/g%3E",
|
|
11
|
+
"%3Ccircle cx='23' cy='33' r='6.5' fill='%23fff'/%3E",
|
|
12
|
+
"%3Ccircle cx='23' cy='33' r='2.8' fill='%231d4ed8'/%3E",
|
|
13
|
+
"%3C/svg%3E",
|
|
14
|
+
].join("");
|
|
15
|
+
|
|
16
|
+
export const FAVICON_TAG = `<link rel="icon" href="data:image/svg+xml,${SVG}" />`;
|
package/src/server/serve.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type { KnobkitServer, Event } from "../lib/types.js";
|
|
|
7
7
|
import type { Knobkit } from "../lib/knobkit.js";
|
|
8
8
|
import { declare, type AppDecl } from "../lib/declare.js";
|
|
9
9
|
import { makeBound } from "../lib/ctx.js";
|
|
10
|
+
import { FAVICON_TAG } from "../lib/favicon.js";
|
|
10
11
|
import { run } from "./context.js";
|
|
11
12
|
|
|
12
13
|
const CLIENT_JS = fileURLToPath(new URL("../../dist/client.js", import.meta.url));
|
|
@@ -18,7 +19,7 @@ const isEvent = (x: any): x is Event => x != null && typeof x.type === "string"
|
|
|
18
19
|
function html(decl: AppDecl, loading: string): string {
|
|
19
20
|
return `<!doctype html><html lang="en"><head><meta charset="utf-8" />
|
|
20
21
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
21
|
-
<title>${decl.title ?? "knobkit"}</title
|
|
22
|
+
<title>${decl.title ?? "knobkit"}</title>${FAVICON_TAG}<link rel="stylesheet" href="/client.css" /></head>
|
|
22
23
|
<body><div id="root">${loading}</div><script type="module" src="/client.js"></script></body></html>`;
|
|
23
24
|
}
|
|
24
25
|
|