rosefc 0.2.0 → 0.2.1
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 +1 -1
- package/deploy/Dockerfile.node +2 -2
- package/deploy/Dockerfile.static +5 -5
- package/deploy/cloudflare/functions/[[path]].ts +2 -2
- package/deploy/cloudflare/wrangler.toml +2 -2
- package/deploy/fly.toml +4 -4
- package/deploy/k8s/{rosefd.yaml → rosefc.yaml} +22 -22
- package/deploy/native/{RosefdApp.xcscheme → RosefcApp.xcscheme} +16 -16
- package/deploy/native/android.yml +3 -3
- package/deploy/native/flutter.yml +8 -8
- package/deploy/native/ios.yml +8 -8
- package/deploy/netlify/functions/{rosefd.ts → rosefc.ts} +5 -5
- package/deploy/netlify.toml +1 -1
- package/deploy/vercel.json +2 -2
- package/dist-cli/compiler.mjs +149 -149
- package/dist-cli/edge.mjs +14 -14
- package/dist-cli/export.mjs +546 -546
- package/dist-cli/{rosefd.mjs → rosefc.mjs} +398 -398
- package/dist-cli/runtime.mjs +2 -2
- package/dist-cli/test.mjs +159 -159
- package/package.json +7 -7
- package/src/FRAMEWORK_VERSION +1 -1
- package/src/cli/content.ts +1 -1
- package/src/cli/edge.ts +3 -3
- package/src/cli/prod-server.ts +12 -12
- package/src/cli/shell.ts +17 -17
- package/src/cli/shutdown.ts +3 -3
- package/src/runtime/db.ts +3 -3
- package/src/runtime/index.ts +60 -60
- package/src/types/{rosefd.d.ts → rosefc.d.ts} +12 -12
- /package/deploy/api/{rosefd.ts → rosefc.ts} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rosefc",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "The next-century web framework: zero-hydration, compile-time reactivity, single binary deploy",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -22,10 +22,10 @@
|
|
|
22
22
|
"test:go": "tsx src/cli/index.ts build example && node tests/test-go.mjs",
|
|
23
23
|
"test:pack": "node tests/test-pack.mjs",
|
|
24
24
|
"test:resilience": "tsx tests/test-resilience.ts",
|
|
25
|
-
"build:go": "node -e \"const fs=require('fs');if(!fs.existsSync('dist')){console.error('build:go: dist/ missing - run npm run build first');process.exit(1)}\" && go build -o
|
|
25
|
+
"build:go": "node -e \"const fs=require('fs');if(!fs.existsSync('dist')){console.error('build:go: dist/ missing - run npm run build first');process.exit(1)}\" && go build -o rosefc-server ."
|
|
26
26
|
},
|
|
27
27
|
"bin": {
|
|
28
|
-
"
|
|
28
|
+
"rosefc": "dist-cli/rosefc.mjs"
|
|
29
29
|
},
|
|
30
30
|
"exports": {
|
|
31
31
|
"./edge": "./dist-cli/edge.mjs",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"files": [
|
|
38
38
|
"dist-cli",
|
|
39
39
|
"src/FRAMEWORK_VERSION",
|
|
40
|
-
"src/types/
|
|
40
|
+
"src/types/rosefc.d.ts",
|
|
41
41
|
"src/cli/content.ts",
|
|
42
42
|
"src/cli/edge.ts",
|
|
43
43
|
"src/cli/hot-vary.ts",
|
|
@@ -71,11 +71,11 @@
|
|
|
71
71
|
},
|
|
72
72
|
"repository": {
|
|
73
73
|
"type": "git",
|
|
74
|
-
"url": "git+https://github.com/okdunli/
|
|
74
|
+
"url": "git+https://github.com/okdunli/rosefc.git"
|
|
75
75
|
},
|
|
76
|
-
"homepage": "https://github.com/okdunli/
|
|
76
|
+
"homepage": "https://github.com/okdunli/rosefc#readme",
|
|
77
77
|
"bugs": {
|
|
78
|
-
"url": "https://github.com/okdunli/
|
|
78
|
+
"url": "https://github.com/okdunli/rosefc/issues"
|
|
79
79
|
},
|
|
80
80
|
"keywords": [
|
|
81
81
|
"framework",
|
package/src/FRAMEWORK_VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.2.
|
|
1
|
+
0.2.1
|
package/src/cli/content.ts
CHANGED
|
@@ -162,7 +162,7 @@ export function byTag(
|
|
|
162
162
|
return out;
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
globalThis.
|
|
165
|
+
globalThis.__rosefcContentMk = renderMarkdown;
|
|
166
166
|
|
|
167
167
|
/** RSS 2.0 feed from a collection - wire to /feed.xml or /api/feed. */
|
|
168
168
|
export function buildRss(
|
package/src/cli/edge.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
//
|
|
2
|
+
// Rosefc Edge adapter - Web-standard fetch handler.
|
|
3
3
|
import { pathToFileURL } from 'url';
|
|
4
4
|
import { join } from 'path';
|
|
5
5
|
import { buildShell, shellOpen, clientScriptTag, securityHeaders, mintNonce, getClientSource, setBundleMode } from './shell.js';
|
|
@@ -92,7 +92,7 @@ const withEdgeQueue = <T>(fn: () => Promise<T>): Promise<T> => {
|
|
|
92
92
|
};
|
|
93
93
|
|
|
94
94
|
/**
|
|
95
|
-
* Create a fetch handler from a built
|
|
95
|
+
* Create a fetch handler from a built Rosefc project.
|
|
96
96
|
* @param outDir directory containing server.js and client.js
|
|
97
97
|
* @param preloaded for runtimes with NO filesystem (Cloudflare Workers /
|
|
98
98
|
* Pages): the caller statically imports the server bundle (so the deploy
|
|
@@ -187,7 +187,7 @@ export async function createEdgeHandler(
|
|
|
187
187
|
// execute the render, so it asks for the JSON half of one - the state the
|
|
188
188
|
// page's $data produced. Middleware already ran above (a wall short-
|
|
189
189
|
// circuited before this point), so a gated route never leaks through it.
|
|
190
|
-
if (pathname === '/
|
|
190
|
+
if (pathname === '/__rosefc/data' && request.method === 'GET') {
|
|
191
191
|
const target = searchParams.get('path') || '/';
|
|
192
192
|
try {
|
|
193
193
|
// The gate is per-page: judge the target path, not the channel URL.
|
package/src/cli/prod-server.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Must run before any request can reach bootstrap(): see shell.ts.
|
|
2
|
-
(globalThis as any).
|
|
2
|
+
(globalThis as any).__ROSEFC_SKIP_MINIFY = true;
|
|
3
3
|
|
|
4
4
|
import http from 'node:http';
|
|
5
5
|
import { Readable } from 'node:stream';
|
|
@@ -123,13 +123,13 @@ if (shouldWarm) ready.then(async (h) => {
|
|
|
123
123
|
const res = await runIsolated(() => h(new Request('http://localhost' + r)));
|
|
124
124
|
const buf = await res.arrayBuffer();
|
|
125
125
|
const suspect = res.status === 200 && res.headers.get('content-type')?.includes('text/html') && buf.byteLength < 2048;
|
|
126
|
-
console.log('[
|
|
126
|
+
console.log('[rosefc] warm', r, 'status=' + res.status, 'bytes=' + buf.byteLength, suspect ? 'SUSPECT-EMPTY' : '');
|
|
127
127
|
} catch (e) {
|
|
128
|
-
console.error('[
|
|
128
|
+
console.error('[rosefc] warm', r, 'FAILED:', (e as Error)?.stack ?? e);
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
131
|
}));
|
|
132
|
-
}).catch((e) => console.error('[
|
|
132
|
+
}).catch((e) => console.error('[rosefc] warm loop aborted:', (e as Error)?.stack ?? e));
|
|
133
133
|
|
|
134
134
|
const hotCache = new Map<string, { raw: Buffer; br?: Buffer; gzip?: Buffer; headers: Record<string, string | string[]>; etag: string; fetchedAt: number; status?: number; refreshing?: boolean }>();
|
|
135
135
|
const HOT_CACHE_MAX = 300;
|
|
@@ -163,7 +163,7 @@ function invalidateHot(): void {
|
|
|
163
163
|
// invalidation on, so it calls this at its commit point instead. The request
|
|
164
164
|
// path needs no hook: the handler schedules its own invalidation on the
|
|
165
165
|
// response's finish (below).
|
|
166
|
-
(globalThis as unknown as {
|
|
166
|
+
(globalThis as unknown as { __rosefcInvalidateHot?: () => void }).__rosefcInvalidateHot = invalidateHot;
|
|
167
167
|
|
|
168
168
|
const gzipFallback: zlib.ZlibOptions = { level: 6 };
|
|
169
169
|
|
|
@@ -332,7 +332,7 @@ function refusesStore(response: Response, carried: string[] = []): boolean {
|
|
|
332
332
|
|
|
333
333
|
async function refetchHot(pathname: string, cookieHeader: string, dims: string[], names: string[]): Promise<void> {
|
|
334
334
|
const gen = hotGen;
|
|
335
|
-
const headers: Record<string, string> = { 'user-agent': '
|
|
335
|
+
const headers: Record<string, string> = { 'user-agent': 'rosefc-hot' };
|
|
336
336
|
if (cookieHeader) headers.cookie = cookieHeader;
|
|
337
337
|
// The warm render must negotiate the SAME dimensions the entry it fills is
|
|
338
338
|
// keyed by: a header-less warm render stored under a language (or a declared
|
|
@@ -515,13 +515,13 @@ try {
|
|
|
515
515
|
if (!(k in process.env)) process.env[k] = v;
|
|
516
516
|
}
|
|
517
517
|
} catch { /* no .env: fine */ }
|
|
518
|
-
const REVALIDATE_TOKEN = process.env.
|
|
518
|
+
const REVALIDATE_TOKEN = process.env.ROSEFC_REVALIDATE_TOKEN || '';
|
|
519
519
|
let revalidateMod: any = null;
|
|
520
520
|
|
|
521
521
|
function handleRevalidate(req: http.IncomingMessage, res: http.ServerResponse): boolean {
|
|
522
|
-
if (req.method !== 'POST' || !req.url!.startsWith('/
|
|
522
|
+
if (req.method !== 'POST' || !req.url!.startsWith('/__rosefc/revalidate')) return false;
|
|
523
523
|
if (!REVALIDATE_TOKEN) { res.statusCode = 404; res.end(); return true; }
|
|
524
|
-
if (req.headers['x-
|
|
524
|
+
if (req.headers['x-rosefc-token'] !== REVALIDATE_TOKEN) {
|
|
525
525
|
res.statusCode = 403; res.setHeader('content-type', 'application/json');
|
|
526
526
|
res.end(JSON.stringify({ error: 'invalid token' })); return true;
|
|
527
527
|
}
|
|
@@ -714,7 +714,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
714
714
|
response = await runIsolated(() => handle!(new Request(url, init)));
|
|
715
715
|
} catch (err) {
|
|
716
716
|
if (!replayable) throw err;
|
|
717
|
-
console.warn('[
|
|
717
|
+
console.warn('[rosefc] render failed, retrying once:', (err as Error)?.message ?? err);
|
|
718
718
|
await new Promise((r) => setTimeout(r, 200));
|
|
719
719
|
response = await runIsolated(() => handle!(new Request(url, init)));
|
|
720
720
|
}
|
|
@@ -777,7 +777,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
777
777
|
writeAccessLine(req, res, 0);
|
|
778
778
|
}
|
|
779
779
|
} catch (err) {
|
|
780
|
-
console.error('[
|
|
780
|
+
console.error('[rosefc] request failed:', (err as Error)?.stack ?? err);
|
|
781
781
|
if (!res.headersSent) {
|
|
782
782
|
res.statusCode = 500;
|
|
783
783
|
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
@@ -789,7 +789,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
789
789
|
|
|
790
790
|
if (!process.env.VERCEL) {
|
|
791
791
|
server.listen(port, () => {
|
|
792
|
-
console.log(`🌹
|
|
792
|
+
console.log(`🌹 Rosefc prod server on http://localhost:${port}`);
|
|
793
793
|
});
|
|
794
794
|
|
|
795
795
|
let stopping = false;
|
package/src/cli/shell.ts
CHANGED
|
@@ -23,7 +23,7 @@ export function setBundleMode(mode: string): void {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
|
-
* Extra connect-src sources from the app's
|
|
26
|
+
* Extra connect-src sources from the app's rosefc.config.js
|
|
27
27
|
* (`export const csp = { connectSrc: 'https:' }`). The framework default is
|
|
28
28
|
* 'self' - a closed network. Apps whose client talks to an API host (a cloud
|
|
29
29
|
* gateway, a user-configured LLM endpoint) widen it here; the value is
|
|
@@ -156,7 +156,7 @@ if (!__app) {
|
|
|
156
156
|
function __saveScroll() {
|
|
157
157
|
try {
|
|
158
158
|
const s = history.state || {};
|
|
159
|
-
history.replaceState(Object.assign({}, s, {
|
|
159
|
+
history.replaceState(Object.assign({}, s, { __rosefcScroll: window.scrollY }), '');
|
|
160
160
|
} catch (e) { /* history state unavailable (file:// edge): best effort */ }
|
|
161
161
|
}
|
|
162
162
|
function __navigate(pathname, restore) {
|
|
@@ -165,7 +165,7 @@ function __navigate(pathname, restore) {
|
|
|
165
165
|
if (restore) {
|
|
166
166
|
// back/forward: restore the entry's scroll (top when it has none)
|
|
167
167
|
const s = history.state || {};
|
|
168
|
-
const y = typeof s.
|
|
168
|
+
const y = typeof s.__rosefcScroll === 'number' ? s.__rosefcScroll : 0;
|
|
169
169
|
return Promise.resolve(p).then(() => { window.scrollTo(0, y); }, () => { window.scrollTo(0, y); });
|
|
170
170
|
}
|
|
171
171
|
// a fresh navigation starts at the top, like a full page load
|
|
@@ -189,7 +189,7 @@ document.addEventListener('click', (e) => {
|
|
|
189
189
|
});
|
|
190
190
|
// Prefetch the target route's full render (including $data) on hover, keyboard
|
|
191
191
|
// focus, or touch - the click that follows paints from cache: no await, no
|
|
192
|
-
// request. Other frameworks prefetch data only;
|
|
192
|
+
// request. Other frameworks prefetch data only; Rosefc prefetches the render
|
|
193
193
|
// because the inlined bundle already contains every route.
|
|
194
194
|
function __prefetchHandler(e) {
|
|
195
195
|
const a = e.target.closest && e.target.closest('a[href^="/"]');
|
|
@@ -239,20 +239,20 @@ document.addEventListener('submit', (e) => {
|
|
|
239
239
|
// running page. Both are DIRECT references (no wrapper arrow): the names take
|
|
240
240
|
// no arguments, so the call is identical and every document that carries the
|
|
241
241
|
// bundle pays eleven bytes for the pair instead of forty.
|
|
242
|
-
window.
|
|
242
|
+
window.__rosefc = { state: serializeState, prefetch: prefetchStats };
|
|
243
243
|
// initial=true: DOM is SSR output, resume state + wire markers (zero hydration).
|
|
244
244
|
// If the document was rendered for a DIFFERENT route (a static-file server's
|
|
245
245
|
// SPA fallback serves index.html for unknown paths), adopt nothing - let the
|
|
246
246
|
// client router render the requested route from scratch.
|
|
247
|
-
const __st = document.getElementById('
|
|
247
|
+
const __st = document.getElementById('__rosefc_state');
|
|
248
248
|
let __state = __st ? JSON.parse(__st.textContent || '{}') : {};
|
|
249
249
|
// The dev server's reload bridge hands the pre-reload signal graph back inside
|
|
250
|
-
// the next document (window.
|
|
250
|
+
// the next document (window.__rosefc_resume). The resumed values win over the
|
|
251
251
|
// fresh render's, but the ROUTE stays the one the server just rendered - and
|
|
252
252
|
// the route renders client-side from the bundle rather than adopting the SSR
|
|
253
253
|
// DOM, because a resumed list can be longer than the document's rows and
|
|
254
254
|
// adoption could not add them. Absent in every non-dev document.
|
|
255
|
-
const __resumed = window.
|
|
255
|
+
const __resumed = window.__rosefc_resume ? JSON.parse(window.__rosefc_resume) : null;
|
|
256
256
|
if (__resumed) {
|
|
257
257
|
const __route = __state.__route;
|
|
258
258
|
__state = Object.assign(__state, __resumed);
|
|
@@ -272,7 +272,7 @@ if (__state.__notFound) {
|
|
|
272
272
|
// layout): defer the initial wire() until the browser is idle so the SSR
|
|
273
273
|
// paint is interactive-to-paint without blocking on the event wiring. Falls
|
|
274
274
|
// back to eager wiring where requestIdleCallback is unavailable.
|
|
275
|
-
if (window.
|
|
275
|
+
if (window.__rosefcLazy && typeof requestIdleCallback === 'function') {
|
|
276
276
|
requestIdleCallback(() => start(__app, window.location.pathname, true));
|
|
277
277
|
} else {
|
|
278
278
|
start(__app, window.location.pathname, true);
|
|
@@ -286,7 +286,7 @@ let BOOTSTRAP_CACHE: string | null = null;
|
|
|
286
286
|
function bootstrap(): string {
|
|
287
287
|
if (BOOTSTRAP_CACHE === null) {
|
|
288
288
|
// The bundled prod server (dist/prod.mjs) sets the skip flag: esbuild's transformSync spawns a worker thread whose entry is the bundle itself.
|
|
289
|
-
if ((globalThis as any).
|
|
289
|
+
if ((globalThis as any).__ROSEFC_SKIP_MINIFY) {
|
|
290
290
|
BOOTSTRAP_CACHE = BOOTSTRAP_SOURCE;
|
|
291
291
|
} else {
|
|
292
292
|
try {
|
|
@@ -366,8 +366,8 @@ export function mintNonce(): string {
|
|
|
366
366
|
* The document's CSP. Two shapes, chosen per response:
|
|
367
367
|
*
|
|
368
368
|
* - no nonce (the default): `script-src 'sha256-…'` over the exact bytes the
|
|
369
|
-
* tag will carry. Nothing else may run - the strongest policy
|
|
370
|
-
* and the reason a
|
|
369
|
+
* tag will carry. Nothing else may run - the strongest policy Rosefc has,
|
|
370
|
+
* and the reason a Rosefc document needs no 'unsafe-inline'.
|
|
371
371
|
* - a nonce (a route exporting `csp = { nonce: true }`):
|
|
372
372
|
* `script-src 'nonce-…' 'strict-dynamic'`. A hash dies the moment anything
|
|
373
373
|
* between the server and the browser touches the document - a CDN, a WAF,
|
|
@@ -492,7 +492,7 @@ function mergeHeadBlocks(blocks: string[]): string {
|
|
|
492
492
|
// The key is stamped into a double-quoted attribute value, so a quote
|
|
493
493
|
// inside a (pathological) href must not break the markup.
|
|
494
494
|
key = key.replace(/"/g, '');
|
|
495
|
-
const marked = m[0].replace(/^<\w+/, (t) => `${t} data-
|
|
495
|
+
const marked = m[0].replace(/^<\w+/, (t) => `${t} data-rosefc-head="${key}"`);
|
|
496
496
|
put(key, marked);
|
|
497
497
|
}
|
|
498
498
|
}
|
|
@@ -554,7 +554,7 @@ export function shellOpen(styles: string = getStyles(), lang = 'en', dir = '', s
|
|
|
554
554
|
<head>
|
|
555
555
|
<meta charset="UTF-8">
|
|
556
556
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
557
|
-
<title>
|
|
557
|
+
<title>Rosefc</title>${demoStyles}${styleTag}
|
|
558
558
|
${FAVICON}
|
|
559
559
|
</head>
|
|
560
560
|
<body>${appOpen}`;
|
|
@@ -570,20 +570,20 @@ export function buildShell(ssrHtml: string, stateJson: string, client: string |
|
|
|
570
570
|
let bodyAttrs = '';
|
|
571
571
|
const headClean = headStamped.replace(/<(html|body)\b([^>]*)>/gi, (_m, tag: string, attrs: string) => {
|
|
572
572
|
// mergeHeadBlocks stamps every passthrough element with its key; the
|
|
573
|
-
const clean = attrs.replace(/\s*data-
|
|
573
|
+
const clean = attrs.replace(/\s*data-rosefc-head="[^"]*"/, '');
|
|
574
574
|
if (tag === 'html') htmlAttrs = clean;
|
|
575
575
|
else bodyAttrs = clean;
|
|
576
576
|
return '';
|
|
577
577
|
});
|
|
578
578
|
|
|
579
|
-
const titleTag = /<title>/i.test(mergedHead) ? '' : '<title>
|
|
579
|
+
const titleTag = /<title>/i.test(mergedHead) ? '' : '<title>Rosefc</title>';
|
|
580
580
|
|
|
581
581
|
const iconTag = /rel=["']?icon/i.test(mergedHead) ? '' : `\n ${FAVICON}`;
|
|
582
582
|
const headTags = headClean ? `\n ${headClean}` : '';
|
|
583
583
|
const styleTag = styles ? `\n <style>${styles}</style>` : '';
|
|
584
584
|
const stateSafe = stateJson.replace(/</g, '\\u003c');
|
|
585
585
|
const jsTail = js
|
|
586
|
-
? `\n <script type="application/json" id="
|
|
586
|
+
? `\n <script type="application/json" id="__rosefc_state">${stateSafe}</script>\n ${clientScriptTag(client, nonce)}`
|
|
587
587
|
: '';
|
|
588
588
|
const open = js && shell ? '<div id="app">' : '';
|
|
589
589
|
const close = js && shell ? '</div>' : '';
|
package/src/cli/shutdown.ts
CHANGED
|
@@ -13,9 +13,9 @@ const shutdownHooks: ShutdownHook[] = [];
|
|
|
13
13
|
let draining: Promise<HookResult[]> | null = null;
|
|
14
14
|
|
|
15
15
|
// Exposed on globalThis because esbuild bundles app code with no external allowlist: a static import from the app would be inlined as a SECOND copy.
|
|
16
|
-
(globalThis as unknown as {
|
|
16
|
+
(globalThis as unknown as { __rosefcOnShutdown?: typeof onShutdown }).__rosefcOnShutdown =
|
|
17
17
|
onShutdown;
|
|
18
|
-
(globalThis as unknown as {
|
|
18
|
+
(globalThis as unknown as { __rosefcRunShutdownHooks?: typeof runShutdownHooks }).__rosefcRunShutdownHooks =
|
|
19
19
|
runShutdownHooks;
|
|
20
20
|
|
|
21
21
|
/** Returns an unregister function - a hot-swapped client must drop its old hook. */
|
|
@@ -41,7 +41,7 @@ export function runShutdownHooks(reason: string): Promise<HookResult[]> {
|
|
|
41
41
|
} catch (err) {
|
|
42
42
|
const message = err instanceof Error ? err.message : String(err);
|
|
43
43
|
results.push({ name: hook.name, ok: false, error: message });
|
|
44
|
-
console.error(`[
|
|
44
|
+
console.error(`[rosefc] shutdown hook ${hook.name} failed (${reason}):`, message);
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
return results;
|
package/src/runtime/db.ts
CHANGED
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
// carry their own copy of this module) - a module-level variable would be
|
|
51
51
|
// per-copy, so a route would see "no driver registered" from a different
|
|
52
52
|
// bundle's registration. Same reason the rest of the server runtime publishes
|
|
53
|
-
// itself through
|
|
53
|
+
// itself through __rosefcAuth / __rosefcKvApi / __rosefcExtras.
|
|
54
54
|
//
|
|
55
55
|
// Zero dependencies: no import statement in this file at all.
|
|
56
56
|
|
|
@@ -84,7 +84,7 @@ export interface Db extends Queries {
|
|
|
84
84
|
tx<T>(fn: (t: Queries) => Promise<T> | T): Promise<T>;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
const KEY = '
|
|
87
|
+
const KEY = '__rosefcDb';
|
|
88
88
|
|
|
89
89
|
const slot = () => globalThis as unknown as Record<string, Driver | undefined>;
|
|
90
90
|
|
|
@@ -100,7 +100,7 @@ function requireDriver(): Driver {
|
|
|
100
100
|
const driver = slot()[KEY];
|
|
101
101
|
if (!driver) {
|
|
102
102
|
throw new Error(
|
|
103
|
-
'
|
|
103
|
+
'rosefc/db: no database driver registered - call registerDb({ query }) once at boot ' +
|
|
104
104
|
'(src/pages/_middleware.rose, or a module it imports) before any handler queries',
|
|
105
105
|
);
|
|
106
106
|
}
|