duckfn-docs-kit 0.2.0 → 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/README.md CHANGED
@@ -24,7 +24,7 @@ npm install duckfn-docs-kit
24
24
  | --- | --- | --- |
25
25
  | `duckfn-docs-kit` | browser | Home-page custom elements (`<dfk-hero>`, `<dfk-features>`, `<dfk-next-steps>`, `<dfk-sql>`), `registerDfkElements()` and the value types their setters accept |
26
26
  | `duckfn-docs-kit/remark` | Node (build) | `remarkVersionPlaceholder`: replaces `{{DUCKFN_VERSION}}` inside `text` / `inlineCode` / `code` nodes |
27
- | `duckfn-docs-kit/sql/remark` | Node (build) | `remarkRunnableSql`: turns fenced `sql run` blocks into `<dfk-sql>` elements |
27
+ | `duckfn-docs-kit/sql/remark` | Node (build) | `remarkRunnableSql`: turns fenced `sql {"type":"duckfn",…}` blocks into `<dfk-sql>` elements |
28
28
  | `duckfn-docs-kit/sql/extensions` | Node (build) | `dfkExtensions()` Docusaurus plugin: preloads a site's DuckDB extensions before the first block runs |
29
29
  | `duckfn-docs-kit/toc-toggle/plugin` | Node (build) | `dfkTocToggle()` Docusaurus plugin: adds the TOC collapse control |
30
30
  | `duckfn-docs-kit/toc-toggle/TocToggle` | browser | The TOC collapse class, for a site that drives it itself |
@@ -97,10 +97,12 @@ styles inside the JS bundle, so they need nothing here.
97
97
 
98
98
  ## Documentation
99
99
 
100
- The user guide lives at <https://shijianjs.github.io/duckfn/docs/docs-kit>. The
101
- conventions this package is written to — retained-mode components, shadow DOM,
102
- SSR safety, the SQL rendering contract — are documented in
103
- [`AGENTS.md`](./AGENTS.md), which ships inside the package.
100
+ The user guide lives at <https://shijianjs.github.io/duckfn/docs/docs-kit>. If you —
101
+ or an AI agent — are *using* this package, read [`AGENTS.md`](./AGENTS.md): it ships
102
+ inside the package and states the contracts (the block metastring, the config
103
+ fields, the traps). The conventions this package is *written to* — retained-mode
104
+ components, shadow DOM, SSR safety — live in `CONVENTIONS.md` in the repository and
105
+ are not published.
104
106
 
105
107
  ## License
106
108
 
@@ -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.13`. */
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
+ }