spark-ssr 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 ADDED
@@ -0,0 +1,117 @@
1
+ # spark-ssr
2
+
3
+ **Zero config. No build. SSR the Spark way.**
4
+
5
+ The framework reads the HTML template and infers everything:
6
+
7
+ ```html
8
+ <!-- index.html -->
9
+ <h1>Tasks</h1>
10
+
11
+ <template await="todos">
12
+ <input bind:value="draft" placeholder="New task">
13
+ <button onclick={add}>Add</button>
14
+ <template each="todo in todos">
15
+ <li>
16
+ <input type="checkbox" bind:checked="todo.done" onchange={patch}>
17
+ {todo.title}
18
+ <button onclick={remove}>✕</button>
19
+ </li>
20
+ </template>
21
+ </template>
22
+
23
+ <spark-ssr table="todos" />
24
+ ```
25
+
26
+ ```bash
27
+ bun spark-ssr
28
+ ```
29
+
30
+ That's it.
31
+
32
+ | The template says | The framework knows |
33
+ | ----------------------------------------------- | -------------------------------------------- |
34
+ | `<template each="todo in todos">` | You need data called `todos` |
35
+ | `table="todos"` | It's backed by the `todos` table |
36
+ | `onclick={add}` (outside the loop) | `POST /api/todos` — insert |
37
+ | `bind:checked="todo.done"` + `onchange={patch}` | `PATCH /api/todos/:id` — update `done` |
38
+ | `onclick={remove}` (inside the loop) | `DELETE /api/todos/:id` — remove the row |
39
+ | `bind:value="draft"` | Local state variable `draft` |
40
+ | `user_id` column in the table | Auth — `WHERE user_id = :session.id` |
41
+
42
+ No `<script>`. No SQL. No ORM. No server file. No build step.
43
+
44
+ ## Config (spark.json)
45
+
46
+ ```json
47
+ {
48
+ "db": "postgres://localhost:5432/myapp",
49
+ "auth": { "table": "users", "identity": "email", "secret": "ENV.SESSION_SECRET" },
50
+ "cors": true
51
+ }
52
+ ```
53
+
54
+ `sqlite://./dev.db` works too (Bun ships both drivers). `ENV.*` values resolve
55
+ from the environment at startup. `cors: true` allows all origins on `/api/*`;
56
+ an array allows specific ones.
57
+
58
+ ## Routing
59
+
60
+ The filesystem is the router. `pages/index.html` → `/`,
61
+ `pages/blog/[slug].html` → `/blog/:slug` (`:slug` binds into queries).
62
+ Without a `pages/` folder, `*.html` at the project root serve the same way.
63
+
64
+ ## Explicit queries
65
+
66
+ ```html
67
+ <!-- pages/search.html -->
68
+ <h1>Results for "{q}"</h1>
69
+ <template each="result in results">
70
+ <p>{result.title}</p>
71
+ </template>
72
+
73
+ <spark-ssr>
74
+ GET /api/search → SELECT * FROM posts WHERE title LIKE '%' || :q || '%' LIMIT 20
75
+ </spark-ssr>
76
+ ```
77
+
78
+ `:` params inject automatically: path params (`:slug`), query string (`:q`),
79
+ JSON body (`:body.title`), session (`:session.id`), headers
80
+ (`:header.x-forwarded-for`), uploads (`:file.url`).
81
+
82
+ ## Custom endpoints — api/
83
+
84
+ `api/stats.html` auto-serves as `GET /api/stats`:
85
+
86
+ ```html
87
+ <spark-ssr>
88
+ GET → SELECT COUNT(*) AS videos FROM videos
89
+ </spark-ssr>
90
+ ```
91
+
92
+ Or drop to a `<script>` (runs server-side; `req`, `res`, `db`, `fetch` in
93
+ scope; the return value becomes the JSON response).
94
+
95
+ ## Everything else
96
+
97
+ - **Components** — `<div import="/components/card" title="{post.title}">`
98
+ inlines at render time; components are pure UI.
99
+ - **Auth** — built-in email/password sessions (`POST /api/users?auth` logs in,
100
+ passwords hash on insert), or a plugin (`auth.plugin` in spark.json).
101
+ - **Middleware** — `middleware.html` runs on every request (`req`, `res`,
102
+ `rateLimit`, `state` in scope; return `{ status, body }` to short-circuit).
103
+ - **Uploads** — multipart bodies stream to `uploads/`; `:file.url` binds the
104
+ stored URL into your INSERT.
105
+ - **Error pages** — `404.html` / `500.html` at the project root.
106
+ - **Static assets** — `public/` plus co-located page assets, served as-is.
107
+ - **Hydration** — interactive pages ship fully-rendered HTML plus a generated
108
+ client component; `mount()` takes over with the same spark-html runtime.
109
+
110
+ ## Deploy
111
+
112
+ ```bash
113
+ bun spark-ssr build # dist/ with a compiled single binary
114
+ bun spark-ssr start # run in production
115
+ ```
116
+
117
+ MIT
package/bin/cli.js ADDED
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * spark-ssr CLI
4
+ *
5
+ * bun spark-ssr serve the current directory (default)
6
+ * bun spark-ssr --port 3000 pick a port
7
+ * bun spark-ssr build assemble dist/ (+ compiled binary)
8
+ * bun spark-ssr start serve dist/ if built, else the project
9
+ *
10
+ * Options:
11
+ * --port <n> Port (default 3000, or PORT env).
12
+ * --root <dir> Project root (default cwd).
13
+ * --no-compile build: skip the single-binary compile, copy only.
14
+ * -h, --help Show this help.
15
+ */
16
+ import { join, resolve } from 'node:path';
17
+ import { cpSync, existsSync, mkdirSync, rmSync, writeFileSync, readdirSync, statSync } from 'node:fs';
18
+ import { serve } from '../src/index.js';
19
+
20
+ function parseArgs(argv) {
21
+ const opts = { cmd: 'serve', compile: true };
22
+ for (let i = 0; i < argv.length; i++) {
23
+ const a = argv[i];
24
+ if (a === '-h' || a === '--help') opts.help = true;
25
+ else if (a === '--port') opts.port = Number(argv[++i]);
26
+ else if (a === '--root') opts.root = argv[++i];
27
+ else if (a === '--no-compile') opts.compile = false;
28
+ else if (a === 'build' || a === 'start' || a === 'serve') opts.cmd = a;
29
+ else if (a.startsWith('--')) { console.error(`Unknown option: ${a}`); process.exit(2); }
30
+ }
31
+ return opts;
32
+ }
33
+
34
+ const HELP = `spark-ssr — zero-config SSR for spark-html on Bun
35
+
36
+ Usage:
37
+ bun spark-ssr [serve] [--port <n>] [--root <dir>]
38
+ bun spark-ssr build [--no-compile]
39
+ bun spark-ssr start
40
+ `;
41
+
42
+ // The project files a deployment needs — pages, components, api, public,
43
+ // error pages, middleware, config. node_modules/dist/uploads stay behind.
44
+ const SHIP_DIRS = ['pages', 'components', 'api', 'public'];
45
+ const SHIP_FILES = ['404.html', '500.html', 'middleware.html', 'spark.json', 'package.json'];
46
+
47
+ async function build(root, compile) {
48
+ const dist = join(root, 'dist');
49
+ rmSync(dist, { recursive: true, force: true });
50
+ mkdirSync(dist, { recursive: true });
51
+ for (const d of SHIP_DIRS) {
52
+ if (existsSync(join(root, d))) cpSync(join(root, d), join(dist, d), { recursive: true });
53
+ }
54
+ for (const f of SHIP_FILES) {
55
+ if (existsSync(join(root, f))) cpSync(join(root, f), join(dist, f));
56
+ }
57
+ // Zero-config single-file projects keep their root pages/assets.
58
+ if (!existsSync(join(root, 'pages'))) {
59
+ for (const f of readdirSync(root)) {
60
+ if (f === 'dist' || f === 'node_modules' || f.startsWith('.')) continue;
61
+ const full = join(root, f);
62
+ if (statSync(full).isFile() && /\.(html|css|js|json|png|jpg|svg|ico|webp)$/.test(f)
63
+ && !SHIP_FILES.includes(f)) {
64
+ cpSync(full, join(dist, f));
65
+ }
66
+ }
67
+ }
68
+ writeFileSync(join(dist, '__server.js'),
69
+ "import { serve } from 'spark-ssr';\n" +
70
+ 'serve({ root: process.cwd(), port: Number(process.env.PORT) || 3000 });\n');
71
+ console.log(`✓ assembled dist/`);
72
+ if (compile) {
73
+ const r = Bun.spawnSync(
74
+ ['bun', 'build', '--compile', join(dist, '__server.js'), '--outfile', join(dist, 'app')],
75
+ { cwd: root, stdout: 'inherit', stderr: 'inherit' },
76
+ );
77
+ if (r.exitCode !== 0) process.exit(r.exitCode);
78
+ console.log('✓ compiled dist/app — run it from dist/ (reads pages/ next to it)');
79
+ }
80
+ }
81
+
82
+ const opts = parseArgs(process.argv.slice(2));
83
+ if (opts.help) { process.stdout.write(HELP); process.exit(0); }
84
+
85
+ const root = resolve(opts.root || process.cwd());
86
+ const port = opts.port ?? (Number(process.env.PORT) || 3000);
87
+
88
+ if (opts.cmd === 'build') {
89
+ await build(root, opts.compile);
90
+ } else if (opts.cmd === 'start') {
91
+ const dist = join(root, 'dist');
92
+ await serve({ root: existsSync(join(dist, '__server.js')) ? dist : root, port });
93
+ } else {
94
+ await serve({ root, port });
95
+ }
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "spark-ssr",
3
+ "version": "0.1.0",
4
+ "description": "Zero-config SSR for spark-html on Bun. The HTML template infers everything: filesystem routing, <spark-ssr> declarative queries, auto CRUD APIs, sessions, uploads, middleware. No build step.",
5
+ "homepage": "https://wilkinnovo.github.io/spark-html",
6
+ "type": "module",
7
+ "main": "./src/index.js",
8
+ "bin": {
9
+ "spark-ssr": "./bin/cli.js"
10
+ },
11
+ "exports": {
12
+ ".": {
13
+ "default": "./src/index.js"
14
+ }
15
+ },
16
+ "files": [
17
+ "src",
18
+ "bin"
19
+ ],
20
+ "engines": {
21
+ "bun": ">=1.2.0"
22
+ },
23
+ "scripts": {
24
+ "test": "bun test/ssr.js"
25
+ },
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/wilkinnovo/spark-html.git",
29
+ "directory": "packages/spark-ssr"
30
+ },
31
+ "dependencies": {
32
+ "linkedom": "^0.18.12",
33
+ "spark-html": "^0.27.0"
34
+ },
35
+ "keywords": [
36
+ "spark-html",
37
+ "ssr",
38
+ "bun",
39
+ "no-build",
40
+ "sql",
41
+ "html",
42
+ "server"
43
+ ],
44
+ "license": "MIT"
45
+ }
package/src/config.js ADDED
@@ -0,0 +1,40 @@
1
+ /**
2
+ * spark.json — the whole config. Values prefixed "ENV." resolve from
3
+ * process.env at load time, so secrets never live in the file.
4
+ *
5
+ * { "db": "sqlite://./dev.db",
6
+ * "auth": { "table": "users", "identity": "email", "secret": "ENV.SESSION_SECRET" },
7
+ * "cors": true }
8
+ */
9
+ import { join } from 'node:path';
10
+ import { existsSync, readFileSync } from 'node:fs';
11
+
12
+ function resolveEnv(v) {
13
+ if (typeof v === 'string' && v.startsWith('ENV.')) {
14
+ const key = v.slice(4);
15
+ const val = process.env[key];
16
+ if (val === undefined) {
17
+ throw new Error(`spark.json references ${v} but ${key} is not set in the environment`);
18
+ }
19
+ return val;
20
+ }
21
+ if (Array.isArray(v)) return v.map(resolveEnv);
22
+ if (v && typeof v === 'object') {
23
+ const out = {};
24
+ for (const [k, val] of Object.entries(v)) out[k] = resolveEnv(val);
25
+ return out;
26
+ }
27
+ return v;
28
+ }
29
+
30
+ export function loadConfig(root) {
31
+ const file = join(root, 'spark.json');
32
+ const raw = existsSync(file) ? JSON.parse(readFileSync(file, 'utf8')) : {};
33
+ const cfg = resolveEnv(raw);
34
+ return {
35
+ db: cfg.db || null,
36
+ auth: cfg.auth || null,
37
+ cors: cfg.cors ?? false,
38
+ uploads: cfg.uploads || 'uploads',
39
+ };
40
+ }
package/src/db.js ADDED
@@ -0,0 +1,72 @@
1
+ /**
2
+ * One tiny adapter per driver, no ORM. SQL arrives with `?` placeholders and
3
+ * an ordered values array (the `:param` → `?` rewrite is quote-aware and
4
+ * happens in parse.js). Bun ships both drivers — nothing to install.
5
+ *
6
+ * sqlite://./dev.db sqlite::memory: → bun:sqlite
7
+ * postgres://… postgresql://… → Bun.SQL
8
+ */
9
+
10
+ // Statements that produce rows (SELECT/WITH, or anything RETURNING) go
11
+ // through .all(); the rest through .run() so we still get change counts.
12
+ const yieldsRows = (sql) => /^\s*(select|with)\b/i.test(sql) || /\breturning\b/i.test(sql);
13
+
14
+ // sqlite binds primitives only — flatten anything else.
15
+ const bindable = (v) =>
16
+ v === undefined ? null
17
+ : v === null || typeof v === 'number' || typeof v === 'string' || typeof v === 'bigint' ? v
18
+ : typeof v === 'boolean' ? (v ? 1 : 0)
19
+ : JSON.stringify(v);
20
+
21
+ export async function connect(url) {
22
+ if (!url) return null;
23
+
24
+ if (url.startsWith('sqlite:')) {
25
+ const { Database } = await import('bun:sqlite');
26
+ let path = url.slice('sqlite:'.length).replace(/^\/\//, '');
27
+ if (path === '' || path === ':memory:') path = ':memory:';
28
+ const db = new Database(path, { create: true });
29
+ return {
30
+ kind: 'sqlite',
31
+ async query(sql, values = []) {
32
+ const vals = values.map(bindable);
33
+ if (yieldsRows(sql)) return db.query(sql).all(...vals);
34
+ const r = db.run(sql, vals);
35
+ return Object.assign([], { changes: r.changes, lastInsertRowid: r.lastInsertRowid });
36
+ },
37
+ async columns(table) {
38
+ if (!/^\w+$/.test(table)) return [];
39
+ try {
40
+ return db.query(`PRAGMA table_info(${table})`).all()
41
+ .map((c) => ({ name: c.name, type: String(c.type || '').toUpperCase() }));
42
+ } catch { return []; }
43
+ },
44
+ close() { db.close(); },
45
+ raw: db,
46
+ };
47
+ }
48
+
49
+ if (/^postgres(ql)?:/.test(url)) {
50
+ const sql = new Bun.SQL(url);
51
+ const positional = (q) => { let i = 0; return q.replace(/\?/g, () => `$${++i}`); };
52
+ return {
53
+ kind: 'postgres',
54
+ async query(q, values = []) {
55
+ return await sql.unsafe(positional(q), values.map(bindable));
56
+ },
57
+ async columns(table) {
58
+ try {
59
+ const rows = await sql.unsafe(
60
+ 'SELECT column_name AS name, data_type AS type FROM information_schema.columns WHERE table_name = $1',
61
+ [table],
62
+ );
63
+ return rows.map((r) => ({ name: r.name, type: String(r.type || '').toUpperCase() }));
64
+ } catch { return []; }
65
+ },
66
+ close() { return sql.close(); },
67
+ raw: sql,
68
+ };
69
+ }
70
+
71
+ throw new Error(`spark-ssr: unsupported db url "${url}" (sqlite:// or postgres:// expected)`);
72
+ }
package/src/hydrate.js ADDED
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Hydration, the Spark way: the browser receives fully-rendered HTML wrapped
3
+ * in a host `<div import="/__spark/page/<key>" data-spark-ssr>`. A client
4
+ * `mount()` re-resolves that import against this module's generated component:
5
+ * the authored page, minus the <spark-ssr> block, plus a synthesized <script>
6
+ * holding local state, the initial data (imported from /__spark/data/<key>.js
7
+ * so no code-shaped strings ever land in a component script), and the CRUD
8
+ * handlers the template implies:
9
+ *
10
+ * bare handler OUTSIDE a loop → insert (POST)
11
+ * bare handler INSIDE a loop, next to a member bind → update (PATCH)
12
+ * bare handler INSIDE a loop, no member bind → delete (DELETE)
13
+ *
14
+ * Handlers inside loops are rewritten to pass their row: onclick={remove}
15
+ * becomes onclick={remove(todo)} — the runtime runs it as an inline statement.
16
+ */
17
+ import { parseHTML } from 'linkedom';
18
+
19
+ // Structural roles from the analysis (names are the author's own).
20
+ export function handlerRoles(analysis) {
21
+ const insert = analysis.handlers.find((h) => !h.inEach) || null;
22
+ const update = analysis.handlers.find((h) => h.inEach && h.withMemberBind) || null;
23
+ const del = analysis.handlers.find((h) => h.inEach && !h.withMemberBind) || null;
24
+ return { insert, update, del };
25
+ }
26
+
27
+ // The column a lone top-level bind maps to when its name isn't a column:
28
+ // the first text-ish, non-bookkeeping column.
29
+ export function primaryColumn(cols) {
30
+ const skip = new Set(['id', 'user_id', 'created', 'created_at', 'updated', 'updated_at']);
31
+ const texty = cols.filter((c) => !skip.has(c.name) && /CHAR|TEXT|CLOB|^$/.test(c.type || ''));
32
+ return (texty[0] || cols.find((c) => !skip.has(c.name)) || {}).name || null;
33
+ }
34
+
35
+ /**
36
+ * Transform the authored page into the client component served at
37
+ * /__spark/page/<key>.html.
38
+ * - <template await="x"> unwraps to its resolved-branch content (state
39
+ * starts from the init module; no promise to wait on client-side).
40
+ * - loop handlers get their row argument.
41
+ * - the synthesized <script> is appended.
42
+ */
43
+ export function clientComponent({ html, analysis, plan, table, cols, key }) {
44
+ const { document } = parseHTML('<!doctype html><html><body>' + html + '</body></html>');
45
+
46
+ // Nested templates may keep their children in .childNodes rather than
47
+ // .content depending on how linkedom parsed them — read both.
48
+ const kids = (node) => [
49
+ ...(node.content ? node.content.childNodes : []),
50
+ ...node.childNodes,
51
+ ];
52
+
53
+ (function transform(node, loopVar) {
54
+ if (node.nodeType !== 1) return;
55
+ const tag = (node.tagName || '').toLowerCase();
56
+ if (tag === 'template') {
57
+ const each = node.getAttribute('each');
58
+ const aw = node.getAttribute('await');
59
+ if (aw) {
60
+ // Prefer an explicit then-branch, else the direct children.
61
+ const content = kids(node);
62
+ const thenTpl = content.find((c) => c.nodeType === 1
63
+ && (c.tagName || '').toLowerCase() === 'template' && c.hasAttribute('then'));
64
+ const keep = thenTpl ? kids(thenTpl) : content.filter((c) =>
65
+ !(c.nodeType === 1 && (c.tagName || '').toLowerCase() === 'template'
66
+ && (c.hasAttribute('then') || c.hasAttribute('catch'))));
67
+ for (const k of keep) node.parentNode.insertBefore(k, node);
68
+ node.remove();
69
+ for (const k of keep) transform(k, loopVar);
70
+ return;
71
+ }
72
+ let inner = loopVar;
73
+ if (each) {
74
+ const em = each.match(/^\s*([\w$]+)/);
75
+ if (em) inner = em[1];
76
+ }
77
+ for (const c of kids(node)) transform(c, inner);
78
+ return;
79
+ }
80
+ if (loopVar && node.attributes) {
81
+ for (const attr of [...node.attributes]) {
82
+ const v = String(attr.value || '').trim();
83
+ if (/^on\w+$/.test(attr.name) && /^\{[a-zA-Z_$][\w$]*\}$/.test(v)) {
84
+ const name = v.slice(1, -1);
85
+ if (analysis.handlers.some((h) => h.name === name && h.inEach)) {
86
+ attr.value = '{' + name + '(' + loopVar + ')}';
87
+ }
88
+ }
89
+ }
90
+ }
91
+ for (const c of [...node.childNodes]) transform(c, loopVar);
92
+ })(document.body, null);
93
+
94
+ return document.body.innerHTML + '\n<script>\n' + clientScript({ analysis, plan, table, cols, key }) + '</script>\n';
95
+ }
96
+
97
+ // The synthesized component script. Plain functions, no template literals,
98
+ // no code-shaped strings (the runtime's script rewriter is not string-aware).
99
+ export function clientScript({ analysis, plan, table, cols, key }) {
100
+ const L = [];
101
+ L.push(`import __init from '/__spark/data/${key}.js';`);
102
+ for (const p of plan) {
103
+ L.push(`let ${p.var} = __init.${p.var};`);
104
+ }
105
+ for (const b of analysis.topBinds) {
106
+ L.push(`let ${b.v} = ${b.kind === 'checked' ? 'false' : "''"};`);
107
+ }
108
+
109
+ if (!table) return L.join('\n') + '\n';
110
+ const api = '/api/' + table;
111
+ const listVar = plan.find((p) => p.source && p.source.kind === 'table' && p.source.table === table)?.var;
112
+ const { insert, update, del } = handlerRoles(analysis);
113
+ const H = { 'content-type': 'application/json' };
114
+
115
+ if (listVar) {
116
+ L.push('async function __refresh() {');
117
+ L.push(` const r = await fetch('${api}');`);
118
+ L.push(` ${listVar} = await r.json();`);
119
+ L.push('}');
120
+ }
121
+ if (insert) {
122
+ L.push(`async function ${insert.name}() {`);
123
+ L.push(' const body = {};');
124
+ const colNames = cols.map((c) => c.name);
125
+ const fallback = primaryColumn(cols);
126
+ for (const b of analysis.topBinds) {
127
+ const col = colNames.includes(b.v) ? b.v : fallback;
128
+ if (col) L.push(` body.${col} = ${b.v};`);
129
+ }
130
+ L.push(` await fetch('${api}', { method: 'POST', headers: ${JSON.stringify(H)}, body: JSON.stringify(body) });`);
131
+ for (const b of analysis.topBinds) L.push(` ${b.v} = ${b.kind === 'checked' ? 'false' : "''"};`);
132
+ if (listVar) L.push(' await __refresh();');
133
+ L.push('}');
134
+ }
135
+ if (update) {
136
+ L.push(`async function ${update.name}(row) {`);
137
+ L.push(' const body = {};');
138
+ for (const f of [...new Set(analysis.rowBinds.map((b) => b.field))]) {
139
+ L.push(` body.${f} = row.${f};`);
140
+ }
141
+ L.push(` await fetch('${api}/' + row.id, { method: 'PATCH', headers: ${JSON.stringify(H)}, body: JSON.stringify(body) });`);
142
+ L.push('}');
143
+ }
144
+ if (del) {
145
+ L.push(`async function ${del.name}(row) {`);
146
+ L.push(` await fetch('${api}/' + row.id, { method: 'DELETE' });`);
147
+ if (listVar) L.push(' await __refresh();');
148
+ L.push('}');
149
+ }
150
+ return L.join('\n') + '\n';
151
+ }
152
+
153
+ // The per-request init module served at /__spark/data/<key>.js — plain data
154
+ // in bundled JS (never inlined into a component <script>), no-store cached.
155
+ export function initModule(data) {
156
+ const json = JSON.stringify(data)
157
+ .replace(/\u2028/g, '\\u2028')
158
+ .replace(/\u2029/g, '\\u2029')
159
+ .replace(/<\//g, '<\\/');
160
+ return 'export default ' + json + ';\n';
161
+ }
package/src/index.js ADDED
@@ -0,0 +1,14 @@
1
+ /**
2
+ * spark-ssr — zero-config SSR for spark-html on Bun.
3
+ *
4
+ * The HTML template infers everything: <template each="todo in todos"> means
5
+ * you need `todos`; <spark-ssr table="todos"> backs it with a table and the
6
+ * REST endpoints the handlers imply; a user_id column means auth scoping.
7
+ * Filesystem routing, sessions, uploads, middleware — no build step.
8
+ */
9
+ export { serve } from './server.js';
10
+ export { loadConfig } from './config.js';
11
+ export { connect } from './db.js';
12
+ export { extractBlocks, rewriteParams, analyze, dataPlan, singleShaped } from './parse.js';
13
+ export { renderFragment, evalExpr } from './render.js';
14
+ export { clientComponent, clientScript, initModule, handlerRoles, primaryColumn } from './hydrate.js';