@n6k.io/build 0.0.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.
@@ -0,0 +1,93 @@
1
+ import { useEffect, useState, type ReactNode } from "react";
2
+ import type { Decorator } from "@storybook/react";
3
+ import { useAttach, useDuckDB } from "@n6k.io/db/react";
4
+ import type { within } from "storybook/test";
5
+ import { seedDemoData } from "./seed-demo-data";
6
+ import { StoryShell } from "./story-shell";
7
+
8
+ export const DUCKDB_READY_TESTID = "duckdb-ready";
9
+
10
+ // Wait for the SeededGate's positive ready sentinel. DuckDB-WASM cold-boots and
11
+ // then seeds the demo data, which is variable under a contended headless test
12
+ // run, so this single await needs a generous timeout (kept under the story
13
+ // project's testTimeout); after it resolves, callers can use default `findBy*`.
14
+ export async function waitForDuckDbReady(
15
+ canvas: ReturnType<typeof within>,
16
+ timeout = 25_000,
17
+ ): Promise<void> {
18
+ await canvas.findByTestId(DUCKDB_READY_TESTID, undefined, { timeout });
19
+ }
20
+
21
+ type SeedStatus = "pending" | "ready" | "error";
22
+
23
+ const STORY_CATALOG = "page_db";
24
+
25
+ function SeededGate({ children }: { children: ReactNode }) {
26
+ // Register the catalog with the provider so `useQuery({ catalogs: [...] })`
27
+ // gates pass. TYPE DUCKDB attaches an in-memory database without going
28
+ // through the n6k WebSocket driver (which requires a real fetch URL).
29
+ useAttach(STORY_CATALOG, ":memory:", { TYPE: "DUCKDB" });
30
+
31
+ const { conn, status, attached, error } = useDuckDB();
32
+ const [seedStatus, setSeedStatus] = useState<SeedStatus>("pending");
33
+ const [seedError, setSeedError] = useState<string | null>(null);
34
+
35
+ const catalogAttached = attached[STORY_CATALOG] !== undefined;
36
+
37
+ useEffect(() => {
38
+ if (status !== "ready" || !conn || !catalogAttached) return;
39
+ let cancelled = false;
40
+ seedDemoData(conn)
41
+ .then(() => {
42
+ if (!cancelled) setSeedStatus("ready");
43
+ })
44
+ .catch((e: Error) => {
45
+ if (cancelled) return;
46
+ setSeedError(e.message);
47
+ setSeedStatus("error");
48
+ });
49
+ return () => {
50
+ cancelled = true;
51
+ };
52
+ }, [conn, status, catalogAttached]);
53
+
54
+ if (error) {
55
+ return (
56
+ <div className="p-4 text-sm text-destructive">DuckDB error: {error}</div>
57
+ );
58
+ }
59
+ if (seedStatus === "error") {
60
+ return (
61
+ <div className="p-4 text-sm text-destructive">
62
+ Seed error: {seedError}
63
+ </div>
64
+ );
65
+ }
66
+ if (status !== "ready" || !catalogAttached || seedStatus !== "ready") {
67
+ return (
68
+ <div className="p-4 text-sm text-muted-foreground">
69
+ Loading DuckDB & seeding demo data…
70
+ </div>
71
+ );
72
+ }
73
+ return (
74
+ <>
75
+ <span data-testid={DUCKDB_READY_TESTID} hidden />
76
+ {children}
77
+ </>
78
+ );
79
+ }
80
+
81
+ export const withSeededDuckDb: Decorator = (Story) => (
82
+ <StoryShell>
83
+ <SeededGate>
84
+ <Story />
85
+ </SeededGate>
86
+ </StoryShell>
87
+ );
88
+
89
+ export const withDuckDb: Decorator = (Story) => (
90
+ <StoryShell>
91
+ <Story />
92
+ </StoryShell>
93
+ );
@@ -0,0 +1,13 @@
1
+ // Shared Storybook scaffolding for n6k packages — the DuckDB provider stack,
2
+ // decorators, demo-data seeder, and preview parameters. Consumers wire the Vite
3
+ // plugins (workers/wasm/cross-origin) from `@n6k.io/build/vite` in their
4
+ // `.storybook/main.ts`, and import the runtime pieces below from here.
5
+ export {
6
+ withDuckDb,
7
+ withSeededDuckDb,
8
+ waitForDuckDbReady,
9
+ DUCKDB_READY_TESTID,
10
+ } from "./decorators";
11
+ export { StoryShell } from "./story-shell";
12
+ export { seedDemoData } from "./seed-demo-data";
13
+ export { previewParameters } from "./preview";
@@ -0,0 +1,17 @@
1
+ import type { Preview } from "@storybook/react";
2
+
3
+ /**
4
+ * Shared Storybook preview parameters for n6k packages. Each package's own
5
+ * `.storybook/preview.tsx` spreads these and adds its own theme CSS import.
6
+ */
7
+ export const previewParameters: NonNullable<Preview["parameters"]> = {
8
+ controls: {
9
+ matchers: {
10
+ color: /(background|color)$/i,
11
+ date: /Date$/i,
12
+ },
13
+ },
14
+ a11y: {
15
+ test: "todo",
16
+ },
17
+ };
@@ -0,0 +1,206 @@
1
+ // Seeds an in-memory DuckDB catalog named `page_db` with three demo tables.
2
+ // Used by Storybook stories so DataBlockSourcePicker finds tables under the
3
+ // same catalog name the production code uses.
4
+ //
5
+ // Tables created in page_db.main:
6
+ // - users (25 rows)
7
+ // - orders (80 rows)
8
+ // - events (150 rows)
9
+ // - notes (2 rows; sequence-default PK + a defaulted `label`)
10
+ //
11
+ // Idempotent: safe to call multiple times in the same connection.
12
+
13
+ type Conn = {
14
+ query: (sql: string) => Promise<unknown>;
15
+ };
16
+
17
+ const USER_NAMES: string[] = [
18
+ "Alice Johnson",
19
+ "Bob Smith",
20
+ "Charlie Brown",
21
+ "Diana Prince",
22
+ "Eve Davis",
23
+ "Frank Miller",
24
+ "Grace Lee",
25
+ "Hank Wilson",
26
+ "Ivy Chen",
27
+ "Jack Taylor",
28
+ "Kate Adams",
29
+ "Liam Garcia",
30
+ "Maya Patel",
31
+ "Noah Kim",
32
+ "Olivia Wong",
33
+ "Peter Singh",
34
+ "Quinn Murphy",
35
+ "Rita Lopez",
36
+ "Sam Cohen",
37
+ "Tara Reyes",
38
+ "Uma Khan",
39
+ "Victor Park",
40
+ "Wendy Cruz",
41
+ "Xander Yu",
42
+ "Yara Hassan",
43
+ ];
44
+
45
+ const COUNTRIES = ["US", "UK", "CA", "DE", "FR"];
46
+ const ORDER_STATUSES = ["pending", "paid", "shipped", "cancelled"];
47
+ const EVENT_NAMES = [
48
+ "page_view",
49
+ "click",
50
+ "signup",
51
+ "purchase",
52
+ "logout",
53
+ "search",
54
+ ];
55
+
56
+ function sqlString(s: string): string {
57
+ return `'${s.replaceAll("'", "''")}'`;
58
+ }
59
+
60
+ function emailFor(name: string): string {
61
+ return name.toLowerCase().replaceAll(/\s+/g, ".") + "@example.com";
62
+ }
63
+
64
+ // Deterministic LCG so generated data is stable across runs.
65
+ function rng(seed: number): () => number {
66
+ let state = seed >>> 0;
67
+ return () => {
68
+ state = (state * 1_664_525 + 1_013_904_223) >>> 0;
69
+ return state / 2 ** 32;
70
+ };
71
+ }
72
+
73
+ function pad2(n: number): string {
74
+ return n < 10 ? `0${n}` : String(n);
75
+ }
76
+
77
+ function timestampAt(baseMs: number, offsetHours: number): string {
78
+ const d = new Date(baseMs + offsetHours * 3_600_000);
79
+ const yyyy = d.getUTCFullYear();
80
+ const mm = pad2(d.getUTCMonth() + 1);
81
+ const dd = pad2(d.getUTCDate());
82
+ const hh = pad2(d.getUTCHours());
83
+ const mi = pad2(d.getUTCMinutes());
84
+ const ss = pad2(d.getUTCSeconds());
85
+ return `${yyyy}-${mm}-${dd} ${hh}:${mi}:${ss}`;
86
+ }
87
+
88
+ function buildUsersInsert(): string {
89
+ const base = Date.UTC(2026, 0, 1, 9, 0, 0);
90
+ const rows = USER_NAMES.map((name, i) => {
91
+ const id = i + 1;
92
+ const email = emailFor(name);
93
+ const active = i % 3 !== 0;
94
+ const createdAt = timestampAt(base, i * 13);
95
+ return `(${id}, ${sqlString(name)}, ${sqlString(email)}, ${active ? "TRUE" : "FALSE"}, TIMESTAMP ${sqlString(createdAt)})`;
96
+ });
97
+ return `INSERT INTO page_db.main.users VALUES\n ${rows.join(",\n ")}`;
98
+ }
99
+
100
+ function buildOrdersInsert(): string {
101
+ const base = Date.UTC(2026, 0, 1, 0, 0, 0);
102
+ const r = rng(42);
103
+ const rows: string[] = [];
104
+ for (let i = 0; i < 80; i++) {
105
+ const id = i + 1;
106
+ const userId = Math.floor(r() * USER_NAMES.length) + 1;
107
+ const amount = (50 + Math.floor(r() * 95_000) / 100).toFixed(2);
108
+ const country = COUNTRIES[Math.floor(r() * COUNTRIES.length)];
109
+ const status = ORDER_STATUSES[Math.floor(r() * ORDER_STATUSES.length)];
110
+ const createdAt = timestampAt(base, i * 7);
111
+ rows.push(
112
+ `(${id}, ${userId}, ${amount}, ${sqlString(country!)}, ${sqlString(status!)}, TIMESTAMP ${sqlString(createdAt)})`,
113
+ );
114
+ }
115
+ return `INSERT INTO page_db.main.orders VALUES\n ${rows.join(",\n ")}`;
116
+ }
117
+
118
+ function buildEventsInsert(): string {
119
+ const base = Date.UTC(2026, 3, 1, 0, 0, 0);
120
+ const r = rng(7);
121
+ const rows: string[] = [];
122
+ for (let i = 0; i < 150; i++) {
123
+ const id = i + 1;
124
+ const userId = Math.floor(r() * USER_NAMES.length) + 1;
125
+ const name = EVENT_NAMES[Math.floor(r() * EVENT_NAMES.length)];
126
+ const ts = timestampAt(base, i * 3);
127
+ rows.push(
128
+ `(${id}, ${userId}, ${sqlString(name!)}, TIMESTAMP ${sqlString(ts)})`,
129
+ );
130
+ }
131
+ return `INSERT INTO page_db.main.events VALUES\n ${rows.join(",\n ")}`;
132
+ }
133
+
134
+ export async function seedDemoData(conn: Conn): Promise<void> {
135
+ // Catalog `page_db` is attached by the storybook decorator via useAttach so
136
+ // `useQuery({ catalogs: ['page_db'] })` proceeds. Don't re-attach here.
137
+
138
+ await conn.query(`DROP TABLE IF EXISTS page_db.main.notes`);
139
+ await conn.query(`DROP SEQUENCE IF EXISTS page_db.main.seq_notes_id`);
140
+ await conn.query(`DROP TABLE IF EXISTS page_db.main.events`);
141
+ await conn.query(`DROP TABLE IF EXISTS page_db.main.orders`);
142
+ await conn.query(`DROP TABLE IF EXISTS page_db.main.users`);
143
+ // Dropped after the tables that reference it.
144
+ await conn.query(`DROP TYPE IF EXISTS page_db.main.order_priority`);
145
+
146
+ await conn.query(`
147
+ CREATE TABLE page_db.main.users (
148
+ id INTEGER PRIMARY KEY,
149
+ name VARCHAR,
150
+ email VARCHAR,
151
+ active BOOLEAN,
152
+ created_at TIMESTAMP
153
+ )
154
+ `);
155
+ await conn.query(buildUsersInsert());
156
+
157
+ await conn.query(`
158
+ CREATE TABLE page_db.main.orders (
159
+ id INTEGER PRIMARY KEY,
160
+ user_id INTEGER,
161
+ amount DECIMAL(10,2),
162
+ country VARCHAR,
163
+ status VARCHAR,
164
+ created_at TIMESTAMP
165
+ )
166
+ `);
167
+ await conn.query(buildOrdersInsert());
168
+
169
+ // A Select-style column: an ENUM domain surfaced via meta.enumValues so the
170
+ // grid's enum cell editor (and the Select property type) have real data.
171
+ // Added after the INSERT so the value-less INSERT above stays valid.
172
+ await conn.query(
173
+ `CREATE TYPE page_db.main.order_priority AS ENUM ('low', 'med', 'high')`,
174
+ );
175
+ await conn.query(
176
+ `ALTER TABLE page_db.main.orders ADD COLUMN priority page_db.main.order_priority`,
177
+ );
178
+ await conn.query(
179
+ `UPDATE page_db.main.orders SET priority = 'high' WHERE id % 3 = 0`,
180
+ );
181
+
182
+ await conn.query(`
183
+ CREATE TABLE page_db.main.events (
184
+ id INTEGER PRIMARY KEY,
185
+ user_id INTEGER,
186
+ name VARCHAR,
187
+ ts TIMESTAMP
188
+ )
189
+ `);
190
+ await conn.query(buildEventsInsert());
191
+
192
+ // `notes` demonstrates database defaults: a sequence-backed primary key and a
193
+ // defaulted `label`, so a draft row committed with those cells left blank
194
+ // (omitted from the INSERT) is auto-filled by the catalog.
195
+ await conn.query(`CREATE SEQUENCE page_db.main.seq_notes_id START 1`);
196
+ await conn.query(`
197
+ CREATE TABLE page_db.main.notes (
198
+ id INTEGER PRIMARY KEY DEFAULT nextval('page_db.main.seq_notes_id'),
199
+ label VARCHAR DEFAULT 'Untitled',
200
+ done BOOLEAN DEFAULT false
201
+ )
202
+ `);
203
+ await conn.query(
204
+ `INSERT INTO page_db.main.notes (label, done) VALUES ('Buy milk', false), ('Ship release', true)`,
205
+ );
206
+ }
@@ -0,0 +1,21 @@
1
+ import { useMemo, type ReactNode } from "react";
2
+ import { DuckDBProvider } from "@n6k.io/db/react";
3
+ import { N6K_DEFAULT_WORKER_URLS } from "@n6k.io/db/workers";
4
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
5
+
6
+ /**
7
+ * The provider stack every n6k story needs: a fresh react-query client and a
8
+ * DuckDB-WASM context. Worker URLs come straight from the driver's canonical
9
+ * manifest (`N6K_DEFAULT_WORKER_URLS`) rather than being hardcoded, so they can
10
+ * never drift from what the `bundleN6kWorkers` Vite middleware actually serves.
11
+ */
12
+ export function StoryShell({ children }: { children: ReactNode }) {
13
+ const queryClient = useMemo(() => new QueryClient(), []);
14
+ return (
15
+ <QueryClientProvider client={queryClient}>
16
+ <DuckDBProvider duckdbOptions={N6K_DEFAULT_WORKER_URLS}>
17
+ {children}
18
+ </DuckDBProvider>
19
+ </QueryClientProvider>
20
+ );
21
+ }
@@ -0,0 +1,9 @@
1
+ {{{wrapperImports}}}
2
+ {{{layoutImports}}}
3
+ import Content from '{{{mdxPath}}}'; export default function App() { return (
4
+ {{{wrapperOpen}}}
5
+ {{{layoutOpen}}}
6
+ <Content />
7
+ {{{layoutClose}}}
8
+ {{{wrapperClose}}}
9
+ ); }
@@ -0,0 +1,4 @@
1
+ import { createRoot, hydrateRoot } from 'react-dom/client'; import App from '{{{appModule}}}';
2
+ const root = document.getElementById('root'); if (root.children.length > 0) {
3
+ hydrateRoot(root,
4
+ <App />); } else { createRoot(root).render(<App />); }
@@ -0,0 +1,18 @@
1
+ <html lang="en">
2
+ <head>
3
+ <meta charset="utf-8" />
4
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
5
+ {{#if title}}<title>{{title}}</title>{{/if}}
6
+ {{#if description}}<meta
7
+ name="description"
8
+ content="{{description}}"
9
+ />{{/if}}
10
+ <link rel="stylesheet" href="{{assetPrefix}}/styles.css" />
11
+ {{{headTags}}}
12
+ </head>
13
+ <body>
14
+ <div id="root">{{{content}}}</div>
15
+ <script type="module" src="{{assetPrefix}}/{{slug}}.js"></script>
16
+ {{{scripts}}}
17
+ </body>
18
+ </html>
@@ -0,0 +1,42 @@
1
+ import pino from "pino";
2
+
3
+ let logger = pino({ level: "silent" });
4
+
5
+ export function initLogger(debug: boolean) {
6
+ if (debug) {
7
+ logger = pino({
8
+ level: "debug",
9
+ transport: { target: "pino-pretty" },
10
+ });
11
+ }
12
+ }
13
+
14
+ // pino does not expose its underlying stream in its public types, so narrow to
15
+ // the one method we need (`end`) via a runtime type guard rather than a cast.
16
+ function hasFlushableStream(
17
+ l: unknown,
18
+ ): l is { stream: { end: (cb: () => void) => void } } {
19
+ return (
20
+ typeof l === "object" &&
21
+ l !== null &&
22
+ "stream" in l &&
23
+ typeof l.stream === "object" &&
24
+ l.stream !== null &&
25
+ "end" in l.stream &&
26
+ typeof l.stream.end === "function"
27
+ );
28
+ }
29
+
30
+ export function flushLogger(): Promise<void> {
31
+ return new Promise((resolve) => {
32
+ logger.flush(() => {
33
+ if (hasFlushableStream(logger)) {
34
+ logger.stream.end(() => resolve());
35
+ } else {
36
+ resolve();
37
+ }
38
+ });
39
+ });
40
+ }
41
+
42
+ export { logger };
@@ -0,0 +1,19 @@
1
+ import type { Plugin } from "vite";
2
+
3
+ /**
4
+ * Vite plugin: set COOP/COEP response headers in dev so the page is
5
+ * cross-origin isolated — required for SharedArrayBuffer, which the n6k duckdb
6
+ * workers use to synchronize with the WASM instance.
7
+ */
8
+ export function crossOriginIsolation(): Plugin {
9
+ return {
10
+ name: "cross-origin-isolation",
11
+ configureServer(server) {
12
+ server.middlewares.use((_req, res, next) => {
13
+ res.setHeader("Cross-Origin-Opener-Policy", "same-origin");
14
+ res.setHeader("Cross-Origin-Embedder-Policy", "require-corp");
15
+ next();
16
+ });
17
+ },
18
+ };
19
+ }
@@ -0,0 +1,6 @@
1
+ // Vite-specific n6k build glue. Framework-agnostic cores live in
2
+ // `../workers/manifest` and `../wasm/extensions`; these plugins wire them into
3
+ // a Vite dev server / build.
4
+ export { bundleN6kWorkers } from "./workers.ts";
5
+ export { n6kWasmExtensions } from "./wasm.ts";
6
+ export { crossOriginIsolation } from "./cross-origin.ts";
@@ -0,0 +1,67 @@
1
+ import path from "node:path";
2
+ import fs from "node:fs";
3
+ import type { Plugin } from "vite";
4
+ import { wasmDir } from "@n6k.io/db/wasm-dir";
5
+ import { collectN6kWasmFiles, N6K_WASM_MIME } from "../wasm/extensions.ts";
6
+
7
+ /**
8
+ * Vite plugin: serve n6k duckdb WASM extensions in dev AND emit them into the
9
+ * client bundle for prod. Without prod-emit, paths like
10
+ * `/v1.5.1/wasm_eh/n6k.duckdb_extension.wasm` 404 at runtime.
11
+ */
12
+ export function n6kWasmExtensions(): Plugin {
13
+ const files = collectN6kWasmFiles(wasmDir);
14
+ if (files.length === 0) {
15
+ throw new Error(
16
+ [
17
+ "",
18
+ `[n6k] WASM extensions not found at ${wasmDir}`,
19
+ "[n6k] vite cannot start/build without them — DuckDB would fail at runtime with a cryptic 404 on /vX.Y.Z/wasm_*/n6k.duckdb_extension.wasm.",
20
+ "[n6k] Fix: build & copy the WASM extensions into the linked driver package:",
21
+ "[n6k] cd ../duckdb-driver && make wasm_mvp wasm_threads wasm_eh npm_db_wasm",
22
+ "",
23
+ ].join("\n"),
24
+ );
25
+ }
26
+ const byUrl = new Map(files.map((f) => ["/" + f.rel, f.abs]));
27
+
28
+ return {
29
+ name: "n6k-wasm-extensions",
30
+ configureServer(server) {
31
+ server.middlewares.use((req, res, next) => {
32
+ const abs = byUrl.get(req.url ?? "");
33
+ if (!abs) {
34
+ if (
35
+ req.url &&
36
+ /^\/v[\d.]+\/wasm_(mvp|eh|threads)\/.+\.wasm$/.test(req.url)
37
+ ) {
38
+ console.error(
39
+ `[n6k] 404 wasm extension: ${req.url} — run \`make npm_db_wasm\` in duckdb-driver`,
40
+ );
41
+ }
42
+ next();
43
+ return;
44
+ }
45
+ const ext = path.extname(abs);
46
+ res.setHeader(
47
+ "Content-Type",
48
+ N6K_WASM_MIME[ext] ?? "application/octet-stream",
49
+ );
50
+ res.setHeader("Access-Control-Allow-Origin", "*");
51
+ res.setHeader("Cross-Origin-Resource-Policy", "cross-origin");
52
+ fs.createReadStream(abs).pipe(res);
53
+ });
54
+ },
55
+ async generateBundle() {
56
+ // Only emit into the client bundle (large binaries; server bundle doesn't need them)
57
+ if (this.environment.name !== "client") return;
58
+ for (const f of files) {
59
+ this.emitFile({
60
+ type: "asset",
61
+ fileName: f.rel,
62
+ source: await fs.promises.readFile(f.abs),
63
+ });
64
+ }
65
+ },
66
+ };
67
+ }
@@ -0,0 +1,91 @@
1
+ import type { Plugin } from "vite";
2
+ import {
3
+ N6K_WORKER_ENTRIES,
4
+ N6K_WORKER_DEV_URLS,
5
+ N6K_WORKERS_VIRTUAL_ID,
6
+ N6K_WORKERS_RESOLVED_ID,
7
+ resolveN6kWorkerSource,
8
+ bundleN6kWorker,
9
+ } from "../workers/manifest.ts";
10
+
11
+ /**
12
+ * Vite plugin: serve the n6k workers from a single source of truth.
13
+ *
14
+ * The app imports its worker URLs from `virtual:n6k-workers` rather than
15
+ * hardcoding them, so this plugin owns the logical-name → real-URL mapping and
16
+ * returns the right value per mode:
17
+ * - dev: stable `/_n6k/workers/*.js` paths, re-bundled per request (no-store)
18
+ * - prod client build: content-hashed assets via Rollup's file-URL placeholder
19
+ * - SSR build: the stable paths (workers never run there)
20
+ */
21
+ export function bundleN6kWorkers(): Plugin {
22
+ let isBuild = false;
23
+ return {
24
+ name: "bundle-n6k-workers",
25
+ configResolved(c) {
26
+ isBuild = c.command === "build";
27
+ },
28
+ resolveId(id) {
29
+ if (id === N6K_WORKERS_VIRTUAL_ID) return N6K_WORKERS_RESOLVED_ID;
30
+ },
31
+ async load(id) {
32
+ if (id !== N6K_WORKERS_RESOLVED_ID) return;
33
+ // Prod client build: emit each worker as a content-hashed asset and point
34
+ // the export at Rollup's file-URL placeholder. Rollup rewrites it to the
35
+ // final hashed, base-prefixed URL — so the browser auto-busts when the
36
+ // worker source changes and can otherwise cache it immutably.
37
+ if (isBuild && this.environment?.name === "client") {
38
+ const lines: string[] = [];
39
+ for (const e of N6K_WORKER_ENTRIES) {
40
+ const code = await bundleN6kWorker(resolveN6kWorkerSource(e.src));
41
+ const ref = this.emitFile({
42
+ type: "asset",
43
+ name: e.file,
44
+ source: code,
45
+ });
46
+ lines.push(
47
+ `export const ${e.exportName} = import.meta.ROLLUP_FILE_URL_${ref};`,
48
+ );
49
+ }
50
+ return lines.join("\n");
51
+ }
52
+ // Dev (re-bundled per request by the middleware below) and the SSR build
53
+ // (workers never run there): the stable `/_n6k/workers/*.js` paths.
54
+ return N6K_WORKER_ENTRIES.map(
55
+ (e) =>
56
+ `export const ${e.exportName} = ${JSON.stringify(
57
+ `/_n6k/workers/${e.file}`,
58
+ )};`,
59
+ ).join("\n");
60
+ },
61
+ configureServer(server) {
62
+ server.middlewares.use((req, res, next) => {
63
+ const pathname = (req.url ?? "").split("?", 1)[0];
64
+ const rel = N6K_WORKER_DEV_URLS[pathname];
65
+ if (!rel) {
66
+ next();
67
+ return;
68
+ }
69
+ const src = resolveN6kWorkerSource(rel);
70
+ bundleN6kWorker(src)
71
+ .then((code) => {
72
+ res.setHeader("Content-Type", "text/javascript");
73
+ // Re-bundled from source on every request with no content hash in
74
+ // the URL. `no-cache` (store-but-revalidate) is unreliable without a
75
+ // validator (no ETag is sent), so the browser can serve a stale
76
+ // worker after the source changes. `no-store` forbids storing it at
77
+ // all, so `new Worker(url)` always fetches the freshly-bundled code.
78
+ res.setHeader("Cache-Control", "no-store");
79
+ res.setHeader("Cross-Origin-Resource-Policy", "same-origin");
80
+ res.setHeader("Cross-Origin-Embedder-Policy", "require-corp");
81
+ res.end(code);
82
+ })
83
+ .catch((err: Error) => {
84
+ console.error(`[n6k] failed to bundle ${rel}:`, err);
85
+ res.statusCode = 500;
86
+ res.end(`/* n6k worker bundle error: ${err.message} */`);
87
+ });
88
+ });
89
+ },
90
+ };
91
+ }
@@ -0,0 +1,32 @@
1
+ import path from "node:path";
2
+ import fs from "node:fs";
3
+
4
+ export const N6K_WASM_MIME: Record<string, string> = {
5
+ ".wasm": "application/wasm",
6
+ ".json": "application/json",
7
+ ".js": "text/javascript",
8
+ };
9
+
10
+ export interface N6kWasmFile {
11
+ /** Path relative to the wasm root, e.g. `v1.5.1/wasm_eh/n6k.duckdb_extension.wasm`. */
12
+ rel: string;
13
+ /** Absolute path on disk. */
14
+ abs: string;
15
+ }
16
+
17
+ /** Recursively collect every file under a wasm-extensions root directory. */
18
+ export function collectN6kWasmFiles(root: string): N6kWasmFile[] {
19
+ if (!fs.existsSync(root) || !fs.statSync(root).isDirectory()) return [];
20
+ const out: N6kWasmFile[] = [];
21
+ function walk(dir: string, prefix: string) {
22
+ for (const name of fs.readdirSync(dir)) {
23
+ const abs = path.join(dir, name);
24
+ const rel = prefix ? `${prefix}/${name}` : name;
25
+ const stat = fs.statSync(abs);
26
+ if (stat.isDirectory()) walk(abs, rel);
27
+ else if (stat.isFile()) out.push({ rel, abs });
28
+ }
29
+ }
30
+ walk(root, "");
31
+ return out;
32
+ }