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.
- package/AGENTS.md +294 -224
- package/README.md +10 -6
- package/bin/sql-verify.mjs +12 -12
- package/dist/IconButton.d.ts +22 -0
- package/dist/codemirror.d.ts +29 -0
- package/dist/index.d.ts +28 -7
- package/dist/index.js +2 -2
- package/dist/mermaid/DfkMermaid.d.ts +7 -0
- package/dist/mermaid/config.d.ts +48 -0
- package/dist/mermaid/remark.d.ts +40 -0
- package/dist/mermaid/remark.js +32 -0
- package/dist/mermaid/render.d.ts +94 -0
- package/dist/mermaid/styles.d.ts +6 -0
- package/dist/mermaid/title.d.ts +23 -0
- package/dist/{register-DKLiYs-F.js → register-Dev_kc3Z.js} +921 -579
- package/dist/remark.d.ts +1 -1
- package/dist/sql/browserRunner.d.ts +41 -0
- package/dist/sql/browserRunner.js +186 -0
- package/dist/sql/client.js +1 -1
- package/dist/sql/harness.d.ts +59 -0
- package/dist/sql/harness.js +8328 -0
- package/dist/sql/remark.d.ts +6 -1
- package/dist/sql/renderers.d.ts +2 -0
- package/dist/sql/runtime.d.ts +20 -0
- package/dist/sql/verify.d.ts +4 -5
- package/dist/sql/verify.js +36 -49
- package/package.json +6 -2
- package/src/IconButton.ts +50 -0
- package/src/codemirror.ts +88 -0
- package/src/index.ts +31 -7
- package/src/mermaid/DfkMermaid.css +289 -0
- package/src/mermaid/DfkMermaid.ts +557 -0
- package/src/mermaid/config.ts +74 -0
- package/src/mermaid/remark.ts +98 -0
- package/src/mermaid/render.ts +178 -0
- package/src/mermaid/styles.ts +24 -0
- package/src/mermaid/title.ts +127 -0
- package/src/register.ts +3 -0
- package/src/remark.ts +1 -1
- package/src/sql/DfkSql.css +13 -10
- package/src/sql/DfkSql.ts +11 -46
- package/src/sql/browserRunner.ts +402 -0
- package/src/sql/harness.ts +134 -0
- package/src/sql/remark.ts +6 -1
- package/src/sql/renderers.ts +24 -3
- package/src/sql/runtime.ts +44 -0
- package/src/sql/sql.css +13 -11
- package/src/sql/verify.ts +23 -41
- package/dist/sql/editor.d.ts +0 -16
- package/dist/sql/nodeRunner.d.ts +0 -51
- package/dist/sql/nodeRunner.js +0 -115
- package/src/sql/editor.ts +0 -75
- package/src/sql/nodeRunner.ts +0 -298
package/src/sql/nodeRunner.ts
DELETED
|
@@ -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
|
-
}
|