duckfn-docs-kit 0.2.1 → 0.3.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 +14 -4
- package/bin/sql-verify.mjs +12 -12
- package/dist/index.js +1 -1
- package/dist/{register-DKLiYs-F.js → register-CALCwFBv.js} +32 -23
- 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/runtime.d.ts +20 -0
- package/dist/sql/verify.d.ts +4 -5
- package/dist/sql/verify.js +36 -49
- package/package.json +3 -2
- package/src/remark.ts +1 -1
- package/src/sql/browserRunner.ts +402 -0
- package/src/sql/harness.ts +134 -0
- package/src/sql/runtime.ts +44 -0
- package/src/sql/verify.ts +23 -41
- package/dist/sql/nodeRunner.d.ts +0 -51
- package/dist/sql/nodeRunner.js +0 -115
- package/src/sql/nodeRunner.ts +0 -298
package/AGENTS.md
CHANGED
|
@@ -167,22 +167,32 @@ Constraints that bite:
|
|
|
167
167
|
|
|
168
168
|
## Testing the blocks (`duckfn-sql-verify`)
|
|
169
169
|
|
|
170
|
-
The kit
|
|
170
|
+
The kit runs every block in a **real browser**, on the same runtime the page uses, so what CI
|
|
171
|
+
checks is what a reader gets:
|
|
171
172
|
|
|
172
173
|
```bash
|
|
173
174
|
duckfn-sql-verify --site . # or: npx duckfn-sql-verify --site .
|
|
174
175
|
```
|
|
175
176
|
|
|
176
177
|
It collects every runnable block (`docs/` plus each `i18n/<locale>/…/current/` by default), runs it
|
|
177
|
-
in DuckDB-Wasm
|
|
178
|
-
|
|
178
|
+
in DuckDB-Wasm **in a headless browser** (driven with `playwright-core`, launching your system
|
|
179
|
+
Chrome/Edge via `executablePath` — so no browser download) with the site's extension loaded, and
|
|
180
|
+
fails the process when a block does not behave as it declares. It is fully offline: the engine and
|
|
181
|
+
the extension are served from local files. Wire it into `package.json` as
|
|
182
|
+
`"test": "duckfn-sql-verify --site ."`.
|
|
179
183
|
|
|
180
184
|
- **A block that demonstrates a failure must say so**: `{"type":"duckfn","expect":"error"}`. The
|
|
181
185
|
check is two-way — a block that declares `error` and starts succeeding is reported too — and a
|
|
182
186
|
`-- error:` comment in the SQL is *not* read; only the metadata counts.
|
|
183
187
|
- Useful options: `--extension <path|url>`, `--platform eh|mvp`, `--content <dir>` (repeatable),
|
|
184
|
-
`--
|
|
188
|
+
`--browser <path>` (the Chrome/Edge executable; otherwise a detected one or `DFK_BROWSER`),
|
|
189
|
+
`--timeout <ms>`, `--report <file>`, `--quiet`. It needs `playwright-core`, which the kit lists
|
|
190
|
+
as a dependency; unlike `playwright` it never downloads a browser.
|
|
185
191
|
- The default extension is the single file under `static/duckdb-extensions/`.
|
|
192
|
+
- **File-system examples stay plain code blocks.** Under DuckDB-Wasm the raw file system is not
|
|
193
|
+
POSIX-faithful: `dfn_file_exists` reads a missing file as "opened" (always `true`), writes/append
|
|
194
|
+
byte order is wrong, and there is no existence primitive in DuckDB's C API to correct it. So keep
|
|
195
|
+
any `COPY … TO` / `output_dir` / file-write demo as a non-runnable block and note the reason.
|
|
186
196
|
|
|
187
197
|
## TOC collapse control (`dfkTocToggle()`)
|
|
188
198
|
|
package/bin/sql-verify.mjs
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* The `duckfn-sql-verify` executable.
|
|
4
|
-
*
|
|
5
|
-
* A hand-written wrapper rather than a built entry: Vite's library build emits
|
|
6
|
-
* ESM and does not preserve a shebang, and npm only needs one file with one and
|
|
7
|
-
* an executable bit. `dist/` is built by `prepack`, so the import below always
|
|
8
|
-
* resolves in a published tarball.
|
|
9
|
-
*/
|
|
10
|
-
import {cliMain} from '../dist/sql/verify.js';
|
|
11
|
-
|
|
12
|
-
await cliMain(process.argv.slice(2));
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* The `duckfn-sql-verify` executable.
|
|
4
|
+
*
|
|
5
|
+
* A hand-written wrapper rather than a built entry: Vite's library build emits
|
|
6
|
+
* ESM and does not preserve a shebang, and npm only needs one file with one and
|
|
7
|
+
* an executable bit. `dist/` is built by `prepack`, so the import below always
|
|
8
|
+
* resolves in a published tarball.
|
|
9
|
+
*/
|
|
10
|
+
import {cliMain} from '../dist/sql/verify.js';
|
|
11
|
+
|
|
12
|
+
await cliMain(process.argv.slice(2));
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as e, i as t, n, r, t as i } from "./register-
|
|
1
|
+
import { a as e, i as t, n, r, t as i } from "./register-CALCwFBv.js";
|
|
2
2
|
export { e as DfkFeatures, t as DfkHero, r as DfkNextSteps, n as DfkSql, i as registerDfkElements };
|
|
@@ -168,7 +168,8 @@ var p = class extends c {
|
|
|
168
168
|
#i = null;
|
|
169
169
|
#a = !1;
|
|
170
170
|
#o = null;
|
|
171
|
-
#s =
|
|
171
|
+
#s = null;
|
|
172
|
+
#c = /* @__PURE__ */ new Map();
|
|
172
173
|
get state() {
|
|
173
174
|
return this.#e;
|
|
174
175
|
}
|
|
@@ -176,24 +177,32 @@ var p = class extends c {
|
|
|
176
177
|
return this.#t;
|
|
177
178
|
}
|
|
178
179
|
init(e = {}) {
|
|
179
|
-
if (e.allowUnsignedExtensions && (this.#a = !0), this.#e === "ready") return Promise.resolve();
|
|
180
|
+
if (e.allowUnsignedExtensions && (this.#a = !0), e.bundle && (this.#o = e.bundle), this.#e === "ready") return Promise.resolve();
|
|
180
181
|
if (this.#n) return this.#n;
|
|
181
|
-
let t = this.#
|
|
182
|
-
return t.allowUnsignedExtensions && (this.#a = !0), this.#e = "loading", this.#n = this.#
|
|
182
|
+
let t = this.#l();
|
|
183
|
+
return t.allowUnsignedExtensions && (this.#a = !0), this.#e = "loading", this.#n = this.#u(t.preload).catch((e) => {
|
|
183
184
|
throw this.#e = "error", this.#t = y(e), this.#n = null, e;
|
|
184
185
|
}), this.#n;
|
|
185
186
|
}
|
|
186
|
-
#
|
|
187
|
-
return this.#
|
|
187
|
+
#l() {
|
|
188
|
+
return this.#s ??= re(), this.#s;
|
|
188
189
|
}
|
|
189
|
-
async #
|
|
190
|
-
let t = await import("@duckdb/duckdb-wasm")
|
|
190
|
+
async #u(e) {
|
|
191
|
+
let t = await import("@duckdb/duckdb-wasm");
|
|
192
|
+
if (this.#o) {
|
|
193
|
+
let n = new Worker(this.#o.mainWorker);
|
|
194
|
+
this.#r = new t.AsyncDuckDB(new t.ConsoleLogger(), n), await this.#r.instantiate(this.#o.mainModule, this.#o.pthreadWorker ?? null), await this.#r.open({ allowUnsignedExtensions: this.#a }), this.#i = await this.#r.connect();
|
|
195
|
+
for (let t of e) await this.#d(t);
|
|
196
|
+
this.#e = "ready", this.#t = "";
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
let n = await t.selectBundle(t.getJsDelivrBundles());
|
|
191
200
|
if (!n.mainWorker) throw Error("The selected DuckDB-Wasm bundle has no worker script");
|
|
192
201
|
let r = URL.createObjectURL(new Blob([`importScripts("${n.mainWorker}");`], { type: "text/javascript" })), i = new Worker(r);
|
|
193
202
|
URL.revokeObjectURL(r);
|
|
194
203
|
let a = new t.ConsoleLogger();
|
|
195
204
|
this.#r = new t.AsyncDuckDB(a, i), await this.#r.instantiate(n.mainModule, n.pthreadWorker), await this.#r.open({ allowUnsignedExtensions: this.#a }), this.#i = await this.#r.connect();
|
|
196
|
-
for (let t of e) await this.#
|
|
205
|
+
for (let t of e) await this.#d(t);
|
|
197
206
|
this.#e = "ready", this.#t = "";
|
|
198
207
|
}
|
|
199
208
|
async execute(e) {
|
|
@@ -219,38 +228,38 @@ var p = class extends c {
|
|
|
219
228
|
}
|
|
220
229
|
}
|
|
221
230
|
async loadExtension(e, t = {}) {
|
|
222
|
-
return await this.init(), this.#
|
|
231
|
+
return await this.init(), this.#d({
|
|
223
232
|
name: e,
|
|
224
233
|
repository: t.repository
|
|
225
234
|
});
|
|
226
235
|
}
|
|
227
|
-
#
|
|
236
|
+
#d(e) {
|
|
228
237
|
let t = o(e);
|
|
229
|
-
if (typeof t == "string") return this.#
|
|
238
|
+
if (typeof t == "string") return this.#f(`\u0000${t}`, () => this.#p(t));
|
|
230
239
|
if ("name" in t) {
|
|
231
240
|
let { name: e, repository: n } = t;
|
|
232
|
-
return this.#
|
|
241
|
+
return this.#f(`${n ?? ""}\u0000${e}`, () => this.#p(e, n));
|
|
233
242
|
}
|
|
234
243
|
let { url: n } = t;
|
|
235
|
-
return this.#
|
|
244
|
+
return this.#f(`url\u0000${n}`, () => this.#m(n));
|
|
236
245
|
}
|
|
237
|
-
#
|
|
238
|
-
let n = this.#
|
|
246
|
+
#f(e, t) {
|
|
247
|
+
let n = this.#c.get(e);
|
|
239
248
|
if (n) return n;
|
|
240
249
|
let r = t().catch((t) => {
|
|
241
|
-
throw this.#
|
|
250
|
+
throw this.#c.delete(e), t;
|
|
242
251
|
});
|
|
243
|
-
return this.#
|
|
252
|
+
return this.#c.set(e, r), r;
|
|
244
253
|
}
|
|
245
|
-
async #
|
|
254
|
+
async #p(e, t) {
|
|
246
255
|
if (!r.test(e)) throw Error(`Not a valid extension name: ${e}`);
|
|
247
|
-
let n = this.#
|
|
256
|
+
let n = this.#h();
|
|
248
257
|
t !== void 0 && await n.query(`INSTALL ${e} FROM ${ne(t)}`), await n.query(`LOAD ${e}`);
|
|
249
258
|
}
|
|
250
|
-
async #
|
|
251
|
-
await this.#
|
|
259
|
+
async #m(e) {
|
|
260
|
+
await this.#h().query(`LOAD '${v(e)}'`);
|
|
252
261
|
}
|
|
253
|
-
#
|
|
262
|
+
#h() {
|
|
254
263
|
let e = this.#i;
|
|
255
264
|
if (!e) throw Error(this.#t || "DuckDB unavailable");
|
|
256
265
|
return e;
|
package/dist/remark.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ import type { Plugin } from 'unified';
|
|
|
13
13
|
*/
|
|
14
14
|
export declare const DEFAULT_VERSION_PLACEHOLDER = "{{DUCKFN_VERSION}}";
|
|
15
15
|
export interface VersionPlaceholderOptions {
|
|
16
|
-
/** The real version string to substitute in, e.g. `0.0.
|
|
16
|
+
/** The real version string to substitute in, e.g. `0.0.15`. */
|
|
17
17
|
version: string;
|
|
18
18
|
/** Override the token if a site uses a different one. */
|
|
19
19
|
placeholder?: string;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** The extension to `LOAD`: a local `.duckdb_extension.wasm` path or an http(s) URL. */
|
|
2
|
+
export interface RunnerOptions {
|
|
3
|
+
extension: string;
|
|
4
|
+
/** DuckDB-Wasm platform bundle; must match how the extension was built. */
|
|
5
|
+
platform?: WasmPlatform;
|
|
6
|
+
/** Engine wasm override; defaults to the platform bundle inside duckdb-wasm. */
|
|
7
|
+
engine?: string;
|
|
8
|
+
/** Browser executable; defaults to a detected Chrome/Edge (`DFK_BROWSER` wins). */
|
|
9
|
+
browser?: string;
|
|
10
|
+
}
|
|
11
|
+
export type WasmPlatform = 'eh' | 'mvp';
|
|
12
|
+
export interface RunResult {
|
|
13
|
+
rows: number;
|
|
14
|
+
columns: number;
|
|
15
|
+
}
|
|
16
|
+
/** The harness's `__dfkQuery` return shape (see `harness.ts`). */
|
|
17
|
+
export interface QueryResult {
|
|
18
|
+
columns: string[];
|
|
19
|
+
rows: Record<string, unknown>[];
|
|
20
|
+
error?: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* One DuckDB-Wasm instance at a time, driven through a headless browser.
|
|
24
|
+
* Mirrors the surface `verify.ts` used from `WasmSqlRunner`.
|
|
25
|
+
*/
|
|
26
|
+
export declare class BrowserSqlRunner {
|
|
27
|
+
#private;
|
|
28
|
+
private constructor();
|
|
29
|
+
static create(options: RunnerOptions): Promise<BrowserSqlRunner>;
|
|
30
|
+
/** Drops the current page state and opens a fresh one: new instance, new connection. */
|
|
31
|
+
newPage(): Promise<void>;
|
|
32
|
+
/** Runs one block; a SQL failure is thrown so the caller can weigh it. */
|
|
33
|
+
run(sql: string): Promise<RunResult>;
|
|
34
|
+
/**
|
|
35
|
+
* Runs one statement and returns its actual rows (not just a count). Used by
|
|
36
|
+
* the vfs browser probe to read each value; `run()` is the verifier's path.
|
|
37
|
+
* Unlike `run()`, a failure is returned as `{error}` rather than thrown.
|
|
38
|
+
*/
|
|
39
|
+
query(sql: string): Promise<QueryResult>;
|
|
40
|
+
close(): Promise<void>;
|
|
41
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { createRequire as e } from "node:module";
|
|
2
|
+
import { basename as t, dirname as n, join as r } from "node:path";
|
|
3
|
+
import { existsSync as i, readFileSync as a } from "node:fs";
|
|
4
|
+
import { createServer as o } from "node:http";
|
|
5
|
+
import { chromium as s } from "playwright-core";
|
|
6
|
+
//#region src/sql/browserRunner.ts
|
|
7
|
+
var c = e(import.meta.url), l = {
|
|
8
|
+
eh: {
|
|
9
|
+
wasm: "duckdb-eh.wasm",
|
|
10
|
+
worker: "duckdb-browser-eh.worker.js"
|
|
11
|
+
},
|
|
12
|
+
mvp: {
|
|
13
|
+
wasm: "duckdb-mvp.wasm",
|
|
14
|
+
worker: "duckdb-browser-mvp.worker.js"
|
|
15
|
+
}
|
|
16
|
+
}, u = class e {
|
|
17
|
+
#e = null;
|
|
18
|
+
#t = null;
|
|
19
|
+
#n = null;
|
|
20
|
+
#r = "";
|
|
21
|
+
#i = null;
|
|
22
|
+
#a;
|
|
23
|
+
#o;
|
|
24
|
+
#s;
|
|
25
|
+
#c;
|
|
26
|
+
#l;
|
|
27
|
+
constructor(e) {
|
|
28
|
+
this.#a = e.extensionUrl, this.#o = e.enginePath, this.#s = e.workerPath, this.#c = e.browserExecutable, this.#l = e.allowUnsigned;
|
|
29
|
+
}
|
|
30
|
+
static async create(t) {
|
|
31
|
+
let i = l[t.platform ?? "eh"], a = n(c.resolve("@duckdb/duckdb-wasm/dist/duckdb-browser.mjs")), o = t.engine ? d(t.engine) : r(a, i.wasm), s = r(a, i.worker), u = /^https?:\/\//i.test(t.extension) ? t.extension : d(t.extension);
|
|
32
|
+
return new e({
|
|
33
|
+
extensionUrl: u,
|
|
34
|
+
enginePath: o,
|
|
35
|
+
workerPath: s,
|
|
36
|
+
browserExecutable: m(t.browser),
|
|
37
|
+
allowUnsigned: !0
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
async #u() {
|
|
41
|
+
if (this.#n) return;
|
|
42
|
+
this.#e = await f(this.#d());
|
|
43
|
+
let e = this.#e.address(), t = typeof e == "object" && e ? e.port : 0;
|
|
44
|
+
this.#r = `http://127.0.0.1:${t}/harness.html`, this.#t = await s.launch({
|
|
45
|
+
executablePath: this.#c,
|
|
46
|
+
headless: !0
|
|
47
|
+
}), this.#n = await this.#t.newPage();
|
|
48
|
+
}
|
|
49
|
+
#d() {
|
|
50
|
+
let e = /^https?:\/\//i.test(this.#a) ? { remoteUrl: this.#a } : {
|
|
51
|
+
localFile: this.#a,
|
|
52
|
+
baseName: t(this.#a)
|
|
53
|
+
};
|
|
54
|
+
return {
|
|
55
|
+
harnessScript: c.resolve("duckfn-docs-kit/sql/harness"),
|
|
56
|
+
engine: {
|
|
57
|
+
file: this.#o,
|
|
58
|
+
name: t(this.#o)
|
|
59
|
+
},
|
|
60
|
+
worker: {
|
|
61
|
+
file: this.#s,
|
|
62
|
+
name: t(this.#s)
|
|
63
|
+
},
|
|
64
|
+
extension: e,
|
|
65
|
+
allowUnsigned: this.#l
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
async newPage() {
|
|
69
|
+
await this.#u();
|
|
70
|
+
let e = this.#n;
|
|
71
|
+
if (!e) throw Error("sql/browserRunner: browser is not running");
|
|
72
|
+
this.#i = null, await e.goto(this.#r, { waitUntil: "load" });
|
|
73
|
+
try {
|
|
74
|
+
await e.evaluate(() => window.__dfkReady);
|
|
75
|
+
} catch (e) {
|
|
76
|
+
this.#i = h(e);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async run(e) {
|
|
80
|
+
if (this.#i) throw Error(this.#i);
|
|
81
|
+
let t = this.#n;
|
|
82
|
+
if (!t) throw Error("sql/browserRunner: no page is open — call newPage() first");
|
|
83
|
+
let n = await t.evaluate((e) => window.__dfkRun(e), e);
|
|
84
|
+
if (n?.error) throw Error(n.error);
|
|
85
|
+
return {
|
|
86
|
+
rows: n.rows,
|
|
87
|
+
columns: n.columns
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
async query(e) {
|
|
91
|
+
if (this.#i) return {
|
|
92
|
+
columns: [],
|
|
93
|
+
rows: [],
|
|
94
|
+
error: this.#i
|
|
95
|
+
};
|
|
96
|
+
let t = this.#n;
|
|
97
|
+
if (!t) throw Error("sql/browserRunner: no page is open — call newPage() first");
|
|
98
|
+
return await t.evaluate((e) => window.__dfkQuery(e), e) ?? {
|
|
99
|
+
columns: [],
|
|
100
|
+
rows: [],
|
|
101
|
+
error: "no result"
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
async close() {
|
|
105
|
+
try {
|
|
106
|
+
await this.#n?.close();
|
|
107
|
+
} catch {}
|
|
108
|
+
this.#n = null;
|
|
109
|
+
try {
|
|
110
|
+
await this.#t?.close();
|
|
111
|
+
} catch {}
|
|
112
|
+
this.#t = null, this.#e &&= (await new Promise((e) => this.#e?.close(() => e())), null);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
function d(e) {
|
|
116
|
+
return /^file:\/\//i.test(e) ? new URL(e).pathname.replace(/^\/(\w:)/i, "$1") : e;
|
|
117
|
+
}
|
|
118
|
+
async function f(e) {
|
|
119
|
+
let t = a(e.harnessScript), n = a(e.engine.file), r = a(e.worker.file), i = "localFile" in e.extension ? a(e.extension.localFile) : null, s = "baseName" in e.extension ? e.extension.baseName : "", c = "remoteUrl" in e.extension ? e.extension.remoteUrl : `ext/${e.extension.baseName}`, l = `<!doctype html>
|
|
120
|
+
<html lang="en">
|
|
121
|
+
<head>
|
|
122
|
+
<meta charset="utf-8" />
|
|
123
|
+
<title>duckfn sql harness</title>
|
|
124
|
+
<script>
|
|
125
|
+
window.DFK_HARNESS_BUNDLE = {
|
|
126
|
+
mainModule: '/vendor/${e.engine.name}',
|
|
127
|
+
mainWorker: '/vendor/${e.worker.name}',
|
|
128
|
+
};
|
|
129
|
+
<\/script>
|
|
130
|
+
<script id="dfk-sql-runtime" type="application/json">
|
|
131
|
+
{"allowUnsignedExtensions":${e.allowUnsigned},"preload":[{"url":"${c}"}]}
|
|
132
|
+
<\/script>
|
|
133
|
+
</head>
|
|
134
|
+
<body>
|
|
135
|
+
<script type="module" src="/harness.js"><\/script>
|
|
136
|
+
</body>
|
|
137
|
+
</html>`, u = o((a, o) => {
|
|
138
|
+
let c = new URL(a.url ?? "/", "http://localhost").pathname;
|
|
139
|
+
if (c === "/harness.html") {
|
|
140
|
+
p(o, "text/html; charset=utf-8", Buffer.from(l, "utf8"));
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (c === "/harness.js") {
|
|
144
|
+
p(o, "text/javascript; charset=utf-8", t);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (c === `/vendor/${e.engine.name}`) {
|
|
148
|
+
p(o, "application/wasm", n);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (c === `/vendor/${e.worker.name}`) {
|
|
152
|
+
p(o, "text/javascript; charset=utf-8", r);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (i && c === `/ext/${s}`) {
|
|
156
|
+
p(o, "application/octet-stream", i);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
o.writeHead(404, { "content-type": "text/plain" }), o.end("not found");
|
|
160
|
+
});
|
|
161
|
+
return await new Promise((e, t) => {
|
|
162
|
+
u.once("error", t), u.listen(0, "127.0.0.1", e);
|
|
163
|
+
}), u;
|
|
164
|
+
}
|
|
165
|
+
function p(e, t, n) {
|
|
166
|
+
e.writeHead(200, {
|
|
167
|
+
"content-type": t,
|
|
168
|
+
"content-length": n.length
|
|
169
|
+
}), e.end(n);
|
|
170
|
+
}
|
|
171
|
+
function m(e) {
|
|
172
|
+
let t = e ?? process.env.DFK_BROWSER;
|
|
173
|
+
if (t) return t;
|
|
174
|
+
let n = [];
|
|
175
|
+
if (process.platform === "win32") {
|
|
176
|
+
let e = process.env.PROGRAMFILES ?? "C:\\Program Files", t = process.env["PROGRAMFILES(X86)"] ?? "C:\\Program Files (x86)", i = process.env.LOCALAPPDATA ?? "";
|
|
177
|
+
n.push(r(e, "Google", "Chrome", "Application", "chrome.exe"), r(t, "Google", "Chrome", "Application", "chrome.exe"), r(t, "Microsoft", "Edge", "Application", "msedge.exe"), r(e, "Microsoft", "Edge", "Application", "msedge.exe"), i ? r(i, "Google", "Chrome", "Application", "chrome.exe") : "");
|
|
178
|
+
} else process.platform === "darwin" ? n.push("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge") : n.push("/usr/bin/google-chrome", "/usr/bin/google-chrome-stable", "/usr/bin/microsoft-edge", "/usr/bin/chromium", "/usr/bin/chromium-browser");
|
|
179
|
+
for (let e of n) if (e && i(e)) return e;
|
|
180
|
+
throw Error("sql/browserRunner: no Chrome/Edge found — pass --browser <path> or set DFK_BROWSER");
|
|
181
|
+
}
|
|
182
|
+
function h(e) {
|
|
183
|
+
return e instanceof Error ? e.message : String(e);
|
|
184
|
+
}
|
|
185
|
+
//#endregion
|
|
186
|
+
export { u as BrowserSqlRunner };
|
package/dist/sql/client.js
CHANGED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `duckfn-docs-kit/sql/harness` — the browser page the offline SQL verifier
|
|
3
|
+
* (`sql/browserRunner`) drives with Playwright.
|
|
4
|
+
*
|
|
5
|
+
* It is deliberately thin: it reuses the site's own runtime (`sql/runtime`) so
|
|
6
|
+
* a verified block runs through exactly the code path a reader's **Run** click
|
|
7
|
+
* does — same `AsyncDuckDB` wiring, same ordered extension preload, same
|
|
8
|
+
* "multi-statement returns the last result" `execute()`. The only difference is
|
|
9
|
+
* where the engine comes from: instead of the jsDelivr CDN, the runner serves
|
|
10
|
+
* `duckdb-*.wasm` and its worker script same-origin and hands those URLs in
|
|
11
|
+
* through {@link window.DFK_HARNESS_BUNDLE}, which `runtime.init` accepts as a
|
|
12
|
+
* {@link LocalBundle}.
|
|
13
|
+
*
|
|
14
|
+
* The two contracts the page reads are injected by the served `harness.html`:
|
|
15
|
+
*
|
|
16
|
+
* - `window.DFK_HARNESS_BUNDLE` — `{mainModule, mainWorker, pthreadWorker?}`,
|
|
17
|
+
* the locally-served engine bundle (an absolute same-origin URL each).
|
|
18
|
+
* - the `<script id="dfk-sql-runtime">` JSON tag — the preload list and
|
|
19
|
+
* `allowUnsignedExtensions`, in the very shape the site plugin injects
|
|
20
|
+
* (see `sql/runtimeConfig`), so nothing here re-implements extension loading.
|
|
21
|
+
*
|
|
22
|
+
* The runner communicates through `window.__dfkReady`, `window.__dfkRun` and
|
|
23
|
+
* `window.__dfkQuery`, each reached with a Playwright `page.evaluate` (which
|
|
24
|
+
* awaits the returned promise and marshals the value back to Node). Page reload
|
|
25
|
+
* per content file is what gives "one instance per page": a fresh document
|
|
26
|
+
* resets the module-level singleton, so pages never share a DuckDB instance
|
|
27
|
+
* while the blocks of one page keep sharing its connection (via
|
|
28
|
+
* `DuckDBRuntime.execute`).
|
|
29
|
+
*/
|
|
30
|
+
import { type LocalBundle } from './runtime';
|
|
31
|
+
declare global {
|
|
32
|
+
interface Window {
|
|
33
|
+
/** The locally-served engine bundle, injected by the runner's harness.html. */
|
|
34
|
+
DFK_HARNESS_BUNDLE?: LocalBundle;
|
|
35
|
+
/** Resolves once the instance is up and the site preloads have loaded. */
|
|
36
|
+
__dfkReady?: Promise<void>;
|
|
37
|
+
/** Runs one block and reports rows/columns, or a captured error message. */
|
|
38
|
+
__dfkRun?: (sql: string) => Promise<HarnessRunResult>;
|
|
39
|
+
/**
|
|
40
|
+
* Runs one statement and returns its actual result — the rows, not just a
|
|
41
|
+
* count. Used by the vfs browser probe (`browserRunner.query`) to read the
|
|
42
|
+
* value each statement produced; the docs verifier never needs it.
|
|
43
|
+
*/
|
|
44
|
+
__dfkQuery?: (sql: string) => Promise<HarnessQueryResult>;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** What {@link window.__dfkRun} resolves with; mirrors the runner's `RunResult`. */
|
|
48
|
+
export interface HarnessRunResult {
|
|
49
|
+
rows: number;
|
|
50
|
+
columns: number;
|
|
51
|
+
/** Set when the block failed; `rows`/`columns` are then 0. */
|
|
52
|
+
error?: string;
|
|
53
|
+
}
|
|
54
|
+
/** What {@link window.__dfkQuery} resolves with: the marshalled result rows. */
|
|
55
|
+
export interface HarnessQueryResult {
|
|
56
|
+
columns: string[];
|
|
57
|
+
rows: Record<string, unknown>[];
|
|
58
|
+
error?: string;
|
|
59
|
+
}
|