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 CHANGED
@@ -167,22 +167,32 @@ Constraints that bite:
167
167
 
168
168
  ## Testing the blocks (`duckfn-sql-verify`)
169
169
 
170
- The kit ships the same runner the browser uses, so a site can execute every block it publishes:
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 with the site's extension loaded, and fails the process when a block does not behave
178
- as it declares. Wire it into `package.json` as `"test": "duckfn-sql-verify --site ."`.
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
- `--timeout <ms>`, `--report <file>`, `--working-dir <dir>`, `--quiet`.
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
 
@@ -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-DKLiYs-F.js";
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 = /* @__PURE__ */ new Map();
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.#c();
182
- return t.allowUnsignedExtensions && (this.#a = !0), this.#e = "loading", this.#n = this.#l(t.preload).catch((e) => {
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
- #c() {
187
- return this.#o ??= re(), this.#o;
187
+ #l() {
188
+ return this.#s ??= re(), this.#s;
188
189
  }
189
- async #l(e) {
190
- let t = await import("@duckdb/duckdb-wasm"), n = await t.selectBundle(t.getJsDelivrBundles());
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.#u(t);
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.#u({
231
+ return await this.init(), this.#d({
223
232
  name: e,
224
233
  repository: t.repository
225
234
  });
226
235
  }
227
- #u(e) {
236
+ #d(e) {
228
237
  let t = o(e);
229
- if (typeof t == "string") return this.#d(`\u0000${t}`, () => this.#f(t));
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.#d(`${n ?? ""}\u0000${e}`, () => this.#f(e, n));
241
+ return this.#f(`${n ?? ""}\u0000${e}`, () => this.#p(e, n));
233
242
  }
234
243
  let { url: n } = t;
235
- return this.#d(`url\u0000${n}`, () => this.#p(n));
244
+ return this.#f(`url\u0000${n}`, () => this.#m(n));
236
245
  }
237
- #d(e, t) {
238
- let n = this.#s.get(e);
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.#s.delete(e), t;
250
+ throw this.#c.delete(e), t;
242
251
  });
243
- return this.#s.set(e, r), r;
252
+ return this.#c.set(e, r), r;
244
253
  }
245
- async #f(e, t) {
254
+ async #p(e, t) {
246
255
  if (!r.test(e)) throw Error(`Not a valid extension name: ${e}`);
247
- let n = this.#m();
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 #p(e) {
251
- await this.#m().query(`LOAD '${v(e)}'`);
259
+ async #m(e) {
260
+ await this.#h().query(`LOAD '${v(e)}'`);
252
261
  }
253
- #m() {
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.14`. */
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 };
@@ -1,4 +1,4 @@
1
- import { t as e } from "../register-DKLiYs-F.js";
1
+ import { t as e } from "../register-CALCwFBv.js";
2
2
  //#region src/sql/client.ts
3
3
  typeof window < "u" && e();
4
4
  //#endregion
@@ -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
+ }