duckfn-docs-kit 0.2.1 → 0.4.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.
Files changed (53) hide show
  1. package/AGENTS.md +294 -224
  2. package/README.md +10 -6
  3. package/bin/sql-verify.mjs +12 -12
  4. package/dist/IconButton.d.ts +22 -0
  5. package/dist/codemirror.d.ts +29 -0
  6. package/dist/index.d.ts +28 -7
  7. package/dist/index.js +2 -2
  8. package/dist/mermaid/DfkMermaid.d.ts +7 -0
  9. package/dist/mermaid/config.d.ts +48 -0
  10. package/dist/mermaid/remark.d.ts +40 -0
  11. package/dist/mermaid/remark.js +32 -0
  12. package/dist/mermaid/render.d.ts +94 -0
  13. package/dist/mermaid/styles.d.ts +6 -0
  14. package/dist/mermaid/title.d.ts +23 -0
  15. package/dist/{register-DKLiYs-F.js → register-Dev_kc3Z.js} +921 -579
  16. package/dist/remark.d.ts +1 -1
  17. package/dist/sql/browserRunner.d.ts +41 -0
  18. package/dist/sql/browserRunner.js +186 -0
  19. package/dist/sql/client.js +1 -1
  20. package/dist/sql/harness.d.ts +59 -0
  21. package/dist/sql/harness.js +8328 -0
  22. package/dist/sql/remark.d.ts +6 -1
  23. package/dist/sql/renderers.d.ts +2 -0
  24. package/dist/sql/runtime.d.ts +20 -0
  25. package/dist/sql/verify.d.ts +4 -5
  26. package/dist/sql/verify.js +36 -49
  27. package/package.json +6 -2
  28. package/src/IconButton.ts +50 -0
  29. package/src/codemirror.ts +88 -0
  30. package/src/index.ts +31 -7
  31. package/src/mermaid/DfkMermaid.css +289 -0
  32. package/src/mermaid/DfkMermaid.ts +557 -0
  33. package/src/mermaid/config.ts +74 -0
  34. package/src/mermaid/remark.ts +98 -0
  35. package/src/mermaid/render.ts +178 -0
  36. package/src/mermaid/styles.ts +24 -0
  37. package/src/mermaid/title.ts +127 -0
  38. package/src/register.ts +3 -0
  39. package/src/remark.ts +1 -1
  40. package/src/sql/DfkSql.css +13 -10
  41. package/src/sql/DfkSql.ts +11 -46
  42. package/src/sql/browserRunner.ts +402 -0
  43. package/src/sql/harness.ts +134 -0
  44. package/src/sql/remark.ts +6 -1
  45. package/src/sql/renderers.ts +24 -3
  46. package/src/sql/runtime.ts +44 -0
  47. package/src/sql/sql.css +13 -11
  48. package/src/sql/verify.ts +23 -41
  49. package/dist/sql/editor.d.ts +0 -16
  50. package/dist/sql/nodeRunner.d.ts +0 -51
  51. package/dist/sql/nodeRunner.js +0 -115
  52. package/src/sql/editor.ts +0 -75
  53. package/src/sql/nodeRunner.ts +0 -298
@@ -1,298 +0,0 @@
1
- /**
2
- * Runs runnable SQL blocks in DuckDB-Wasm from Node, on the same architecture
3
- * the site itself uses: the official Node **worker** target, one instance per
4
- * page, one connection per page.
5
- *
6
- * Three platform facts shape this file, all of them measured rather than
7
- * guessed (duckfn's AGENTS.md records the investigation):
8
- *
9
- * 1. The official Node *blocking* target (`duckdb-node-blocking.cjs`) deadlocks
10
- * when it loads an extension that opens its own connection while registering
11
- * — which is exactly what a docs site's extension may do. The worker target
12
- * is the only Node target that works.
13
- * 2. A bare file name in `LOAD` is not usable on wasm: it hangs, without an
14
- * error, even with the file registered through `registerFileBuffer`. The
15
- * extension has to be fetched over http(s), like the site does it.
16
- * 3. DuckDB stages that URL under
17
- * `~/.duckdb/extensions/<host>[:<port>]/<first path segment>/` — which is
18
- * why the served URL keeps a path segment, and why the directory is
19
- * pre-created: the loader's own `mkdir` is non-recursive. A colon is illegal
20
- * in a Windows path, so the port plan is per platform: Windows takes the
21
- * default port 80 (the URL then carries no port at all), POSIX takes any
22
- * free port. The directory's previous content is removed so a stale copy can
23
- * never be what runs.
24
- */
25
- import {createServer, type Server} from 'node:http';
26
- import {Worker as WorkerThread} from 'node:worker_threads';
27
- import {createRequire} from 'node:module';
28
- import {mkdirSync, readFileSync, rmSync} from 'node:fs';
29
- import {homedir} from 'node:os';
30
- import {basename, dirname, join} from 'node:path';
31
-
32
- /** The slice of the Node target's surface this runner uses (it ships no types). */
33
- interface DuckdbNodeTarget {
34
- AsyncDuckDB: new (logger: unknown, worker: unknown) => DuckdbDatabase;
35
- ConsoleLogger: new () => unknown;
36
- }
37
-
38
- export interface DuckdbQueryResult {
39
- numRows: number;
40
- schema: {fields: {name: string}[]};
41
- }
42
-
43
- export interface DuckdbConnection {
44
- query(sql: string): Promise<DuckdbQueryResult>;
45
- }
46
-
47
- interface DuckdbDatabase {
48
- instantiate(mainModule: string, pthreadWorker: string | null): Promise<void>;
49
- open(config: {allowUnsignedExtensions?: boolean}): Promise<void>;
50
- connect(): Promise<DuckdbConnection>;
51
- terminate(): Promise<void>;
52
- }
53
-
54
- const require = createRequire(import.meta.url);
55
-
56
- /** Loaded through a variable so a bundler leaves the dynamic import alone. */
57
- const NODE_TARGET = '@duckdb/duckdb-wasm/dist/duckdb-node.cjs';
58
-
59
- export type WasmPlatform = 'eh' | 'mvp';
60
-
61
- export interface RunnerOptions {
62
- /**
63
- * The extension to `LOAD`: a local `.duckdb_extension.wasm` path (served to
64
- * the worker over a loopback http server) or an absolute `http(s)` URL.
65
- */
66
- extension: string;
67
- /**
68
- * DuckDB-Wasm platform. Must match how the extension was built — a site
69
- * serving `duckfn-wasm_eh.duckdb_extension.wasm` runs the `eh` bundle.
70
- */
71
- platform?: WasmPlatform;
72
- /** The engine wasm; defaults to the one shipped beside the worker bundle. */
73
- engine?: string;
74
- }
75
-
76
- export interface RunResult {
77
- rows: number;
78
- columns: number;
79
- }
80
-
81
- /**
82
- * The WebWorker surface `AsyncDuckDB` expects, backed by a worker thread.
83
- *
84
- * The official `createWorker()` cannot be reused here: it fetches the worker
85
- * script, wraps it in a blob URL and resolves it as a file path, which is the
86
- * browser's module story. The contract it relies on internally is small —
87
- * `worker_threads` entry with `workerData.mod` — and that is what this
88
- * reproduces.
89
- */
90
- class WorkerShim {
91
- #thread: WorkerThread;
92
- #listeners = new Map<string, ((event: unknown) => void)[]>();
93
- onmessage: ((event: unknown) => void) | null = null;
94
- onerror: ((event: unknown) => void) | null = null;
95
- onclose: ((event: unknown) => void) | null = null;
96
-
97
- constructor(mod: string) {
98
- this.#thread = new WorkerThread(require.resolve(NODE_TARGET), {
99
- workerData: {mod, name: '', type: ''},
100
- });
101
- this.#thread.on('message', (data) => this.#emit('message', data));
102
- this.#thread.on('error', (error) => this.#emit('error', error));
103
- this.#thread.on('exit', () => this.#emit('close'));
104
- }
105
-
106
- #emit(type: string, data?: unknown): void {
107
- const event = {type, data, target: this, currentTarget: this};
108
- const handler = (this as Record<string, unknown>)[`on${type}`];
109
- if (typeof handler === 'function') {
110
- (handler as (event: unknown) => void)(event);
111
- }
112
- for (const listener of this.#listeners.get(type) ?? []) {
113
- listener(event);
114
- }
115
- }
116
-
117
- addEventListener(type: string, listener: (event: unknown) => void): void {
118
- this.#listeners.set(type, [...(this.#listeners.get(type) ?? []), listener]);
119
- }
120
-
121
- removeEventListener(type: string, listener: (event: unknown) => void): void {
122
- this.#listeners.set(
123
- type,
124
- (this.#listeners.get(type) ?? []).filter((candidate) => candidate !== listener),
125
- );
126
- }
127
-
128
- postMessage(data: unknown, transfer?: unknown): void {
129
- // The worker_threads signature always wants the second argument.
130
- this.#thread.postMessage(data, (transfer ?? []) as never);
131
- }
132
-
133
- terminate(): Promise<number> | void {
134
- return this.#thread.terminate();
135
- }
136
- }
137
-
138
- /**
139
- * One DuckDB-Wasm instance at a time, driven from Node.
140
- *
141
- * `newPage()` is what a docs site does per page load: a fresh instance, a fresh
142
- * connection, the extension loaded again. Blocks of one page then share state
143
- * (a table created in one block is visible to the next), while pages stay
144
- * isolated — which is why the runner is used one page at a time rather than
145
- * over a single long-lived connection.
146
- */
147
- export class WasmSqlRunner {
148
- #db: DuckdbDatabase | null = null;
149
- #conn: DuckdbConnection | null = null;
150
- #server: Server | null = null;
151
- #stagingDir: string | null = null;
152
- readonly #extensionUrl: string;
153
- readonly #engine: string;
154
- readonly #workerBundle: string;
155
-
156
- private constructor(extensionUrl: string, engine: string, workerBundle: string) {
157
- this.#extensionUrl = extensionUrl;
158
- this.#engine = engine;
159
- this.#workerBundle = workerBundle;
160
- }
161
-
162
- static async create(options: RunnerOptions): Promise<WasmSqlRunner> {
163
- const platform = options.platform ?? 'eh';
164
- const workerBundle = require.resolve(
165
- `@duckdb/duckdb-wasm/dist/duckdb-node-${platform}.worker.cjs`,
166
- );
167
- const engine = options.engine ?? join(dirname(workerBundle), `duckdb-${platform}.wasm`);
168
-
169
- const remote = /^https?:\/\//i.test(options.extension);
170
- const served = remote ? null : await serveExtension(options.extension);
171
- const url = served ? served.url : options.extension;
172
-
173
- const runner = new WasmSqlRunner(url, engine, workerBundle);
174
- runner.#server = served ? served.server : null;
175
- runner.#stagingDir = served
176
- ? prepareStagingDir(served.stagingHost, served.stagingSegment)
177
- : null;
178
- return runner;
179
- }
180
-
181
- /** Drops the current instance and starts a fresh page: new instance, new connection. */
182
- async newPage(): Promise<void> {
183
- if (this.#db) {
184
- await this.#db.terminate();
185
- this.#db = null;
186
- this.#conn = null;
187
- }
188
- const duckdb = (await import(/* @vite-ignore */ NODE_TARGET)) as unknown as DuckdbNodeTarget;
189
- const db = new duckdb.AsyncDuckDB(new duckdb.ConsoleLogger(), new WorkerShim(this.#workerBundle));
190
- await db.instantiate(this.#engine, null);
191
- await db.open({allowUnsignedExtensions: true});
192
- const conn = await db.connect();
193
- await conn.query(`LOAD '${this.#extensionUrl}'`);
194
- this.#db = db;
195
- this.#conn = conn;
196
- }
197
-
198
- /** Runs one block; the caller decides whether a failure is expected. */
199
- async run(sql: string): Promise<RunResult> {
200
- const conn = this.#conn;
201
- if (!conn) {
202
- throw new Error('sql/verify: no page is open — call newPage() first');
203
- }
204
- const table = await conn.query(sql);
205
- return {rows: table.numRows, columns: table.schema.fields.length};
206
- }
207
-
208
- /** Where the fetched extension was staged, for diagnostics. */
209
- get stagingDir(): string | null {
210
- return this.#stagingDir;
211
- }
212
-
213
- async close(): Promise<void> {
214
- if (this.#db) {
215
- await this.#db.terminate();
216
- this.#db = null;
217
- this.#conn = null;
218
- }
219
- if (this.#server) {
220
- await new Promise<void>((resolve) => this.#server?.close(() => resolve()));
221
- this.#server = null;
222
- }
223
- }
224
- }
225
-
226
- /**
227
- * Serves one extension file over loopback http, because wasm `LOAD` only works
228
- * with a URL (see the file header). The server answers any path with that one
229
- * file: it is a local, short-lived convenience, not a web server.
230
- *
231
- * The port choice is the platform split from the file header: the default port
232
- * where the platform lets a non-root process take it (so the URL stays
233
- * port-less, which is what Windows needs), any free port otherwise.
234
- */
235
- async function serveExtension(file: string): Promise<{
236
- server: Server;
237
- url: string;
238
- /** The directory DuckDB will stage the download under, and its parent. */
239
- stagingHost: string;
240
- stagingSegment: string;
241
- }> {
242
- const body = readFileSync(file);
243
- const name = basename(file);
244
- // DuckDB stages a fetched extension under
245
- // `~/.duckdb/extensions/<host>/<first path segment>/`, so the URL needs one
246
- // path segment — the site serves its extension from `duckdb-extensions/`, and
247
- // this mirrors that shape with the extension's own name.
248
- const segment = extensionName(file);
249
- const server = createServer((_request, response) => {
250
- response.writeHead(200, {
251
- 'content-type': 'application/octet-stream',
252
- 'content-length': body.length,
253
- });
254
- response.end(body);
255
- });
256
- const wanted = process.platform === 'win32' ? 80 : 0;
257
- await new Promise<void>((resolve, reject) => {
258
- server.once('error', (error: NodeJS.ErrnoException) => {
259
- reject(
260
- new Error(
261
- `sql/verify: cannot serve the extension on port ${wanted} (${error.code}): a port in ` +
262
- `the URL would put a colon into DuckDB's staging path, which is not a legal Windows ` +
263
- `path — free port 80, or load the extension from an http(s) URL with --extension`,
264
- ),
265
- );
266
- });
267
- server.listen(wanted, 'localhost', resolve);
268
- });
269
- const address = server.address();
270
- const bound = typeof address === 'object' && address ? address.port : wanted;
271
- const host = bound === 80 ? 'localhost' : `localhost:${bound}`;
272
- return {
273
- server,
274
- url: `http://${host}/${segment}/${name}`,
275
- stagingHost: host,
276
- stagingSegment: segment,
277
- };
278
- }
279
-
280
- /**
281
- * Pre-creates DuckDB's staging directory and empties it.
282
- *
283
- * The directory has to exist beforehand (the loader's own `mkdir` is
284
- * non-recursive), and anything left in it from an earlier run would be reused
285
- * instead of the extension being fetched again — a stale binary silently
286
- * testing the wrong build.
287
- */
288
- function prepareStagingDir(host: string, segment: string): string {
289
- const dir = join(homedir(), '.duckdb', 'extensions', host, segment);
290
- rmSync(dir, {recursive: true, force: true});
291
- mkdirSync(dir, {recursive: true});
292
- return dir;
293
- }
294
-
295
- /** The text before the first dot of the file name — the entry symbol DuckDB looks up. */
296
- function extensionName(file: string): string {
297
- return basename(file).split('.')[0] ?? '';
298
- }