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
package/src/sql/sql.css CHANGED
@@ -237,11 +237,13 @@ dfk-sql:not(:defined) {
237
237
 
238
238
  /* --- Icon buttons and their tooltips ------------------------------------- */
239
239
 
240
- /* The same rules (and the `[data-tip]` tooltip below) are declared in
241
- `DfkSql.css`, because the buttons are split across the shadow boundary: the
242
- code-block actions live in the shadow tree, the fullscreen toggle lives in
243
- this light-DOM tab strip. Keep the two copies in sync. */
244
- .dfk-sql-icon-button {
240
+ /* The class names are written by the shared `IconButton` widget
241
+ (`src/IconButton.ts`). The same rules (and the `[data-tip]` tooltip below) are
242
+ declared in `DfkSql.css` and `DfkMermaid.css`, because the buttons are split
243
+ across shadow boundaries: the code-block actions live in `<dfk-sql>`'s tree,
244
+ a diagram's cluster in `<dfk-mermaid>`'s, and the fullscreen toggle in this
245
+ light-DOM tab strip. Keep the copies in sync. */
246
+ .dfk-icon-button {
245
247
  position: relative;
246
248
  display: inline-flex;
247
249
  align-items: center;
@@ -257,27 +259,27 @@ dfk-sql:not(:defined) {
257
259
  transition: background 0.12s ease, color 0.12s ease;
258
260
  }
259
261
 
260
- .dfk-sql-icon-button:hover:not(:disabled),
261
- .dfk-sql-icon-button:focus-visible {
262
+ .dfk-icon-button:hover:not(:disabled),
263
+ .dfk-icon-button:focus-visible {
262
264
  background: var(--ifm-color-emphasis-200, #e6e6e6);
263
265
  color: var(--ifm-color-primary, #14459b);
264
266
  }
265
267
 
266
- .dfk-sql-icon-button:disabled {
268
+ .dfk-icon-button:disabled {
267
269
  opacity: 0.45;
268
270
  cursor: progress;
269
271
  }
270
272
 
271
- .dfk-sql-icon-button[hidden] {
273
+ .dfk-icon-button[hidden] {
272
274
  display: none;
273
275
  }
274
276
 
275
277
  /* A sticky state, e.g. the wrap toggle while wrapping is on. */
276
- .dfk-sql-icon-on {
278
+ .dfk-icon-on {
277
279
  color: var(--ifm-color-primary, #14459b);
278
280
  }
279
281
 
280
- .dfk-sql-icon {
282
+ .dfk-icon {
281
283
  font-size: 1rem;
282
284
  }
283
285
 
package/src/sql/verify.ts CHANGED
@@ -3,10 +3,10 @@
3
3
  * publishes, so a broken example is caught by CI instead of by a reader
4
4
  * clicking **Run**.
5
5
  *
6
- * It is the same environment the site gives a block: DuckDB-Wasm in a worker,
6
+ * It is the same environment the site gives a block: DuckDB-Wasm in a browser,
7
7
  * the site's extension preloaded, one instance per page and the page's blocks
8
- * sharing a connection (see `sql/nodeRunner.ts` for why it is this target and
9
- * not the blocking one).
8
+ * sharing a connection (see `sql/browserRunner.ts` for why this is a real
9
+ * browser and not the Node worker the kit used to run).
10
10
  *
11
11
  * A block may fail *on purpose* — half the guide ends on a statement that
12
12
  * demonstrates an error. Such a block says so in its own metadata
@@ -15,12 +15,11 @@
15
15
  * on has to be data rather than a string match on a comment. Only blocks that
16
16
  * did not behave as declared make the command exit non-zero.
17
17
  */
18
- import {mkdirSync, mkdtempSync, readdirSync, rmSync, statSync, writeFileSync} from 'node:fs';
19
- import {tmpdir} from 'node:os';
18
+ import {readdirSync, statSync, writeFileSync} from 'node:fs';
20
19
  import {join, resolve} from 'node:path';
21
20
 
22
21
  import {collectRunnableSql, expectsError, type RunnableSqlBlock} from './collect';
23
- import {WasmSqlRunner, type WasmPlatform} from './nodeRunner';
22
+ import {BrowserSqlRunner, type WasmPlatform} from './browserRunner';
24
23
 
25
24
  export interface VerifyOptions {
26
25
  /** Docs site root; defaults to the working directory. */
@@ -39,11 +38,10 @@ export interface VerifyOptions {
39
38
  /** Per-block timeout in milliseconds; a hang is reported instead of blocking CI. */
40
39
  timeoutMs?: number;
41
40
  /**
42
- * Directory the blocks run in. Defaults to a fresh temporary directory that
43
- * is removed afterwards: a block may `COPY … TO 'a.csv'`, and on Node
44
- * DuckDB's file system is the real one, relative to the working directory.
41
+ * The browser executable that runs the blocks. Defaults to a detected
42
+ * Chrome/Edge; the `DFK_BROWSER` environment variable is the same override.
45
43
  */
46
- workingDir?: string;
44
+ browser?: string;
47
45
  /** Write the full result list here as JSON. */
48
46
  reportFile?: string;
49
47
  }
@@ -79,8 +77,6 @@ const DEFAULT_TIMEOUT_MS = 30_000;
79
77
 
80
78
  /** Runs every runnable block of the site and returns the outcome of each. */
81
79
  export async function verifySqlDocs(options: VerifyOptions = {}): Promise<VerifyReport> {
82
- // Everything is resolved to absolute paths first: the run changes the working
83
- // directory (see below).
84
80
  const siteDir = resolve(options.siteDir ?? process.cwd());
85
81
  const blocks = collectRunnableSql({
86
82
  siteDir,
@@ -94,26 +90,10 @@ export async function verifySqlDocs(options: VerifyOptions = {}): Promise<Verify
94
90
  : defaultExtension(siteDir);
95
91
  const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
96
92
 
97
- // A block may write files — `COPY (SELECT 1) TO 'a.csv'` — and on Node the
98
- // file system behind DuckDB is the real one, resolved against the working
99
- // directory. Giving the run a scratch directory of its own keeps the docs tree
100
- // clean instead of dropping test residue into it.
101
- const scratch = options.workingDir
102
- ? resolve(options.workingDir)
103
- : mkdtempSync(join(tmpdir(), 'duckfn-sql-verify-'));
104
- mkdirSync(scratch, {recursive: true});
105
- const previousCwd = process.cwd();
106
- process.chdir(scratch);
107
-
108
- let results: BlockResult[];
109
- try {
110
- results = await runPages(blocks, extension, options, timeoutMs);
111
- } finally {
112
- process.chdir(previousCwd);
113
- if (!options.workingDir) {
114
- rmSync(scratch, {recursive: true, force: true});
115
- }
116
- }
93
+ // No working directory: in a browser DuckDB's file system is the instance's
94
+ // own memory, so `COPY … TO` / `dfn_file_write_*` never touch the docs tree —
95
+ // they land in the page and vanish on the next `newPage()`.
96
+ const results = await runPages(blocks, extension, options, timeoutMs);
117
97
 
118
98
  const report: VerifyReport = {
119
99
  blocks: results,
@@ -135,10 +115,11 @@ async function runPages(
135
115
  options: VerifyOptions,
136
116
  timeoutMs: number,
137
117
  ): Promise<BlockResult[]> {
138
- const runner = await WasmSqlRunner.create({
118
+ const runner = await BrowserSqlRunner.create({
139
119
  extension,
140
120
  platform: options.platform,
141
121
  engine: options.engine,
122
+ browser: options.browser,
142
123
  });
143
124
  const results: BlockResult[] = [];
144
125
  try {
@@ -158,7 +139,7 @@ async function runPages(
158
139
 
159
140
  /** Runs one block and judges the result against the block's own declaration. */
160
141
  async function runBlock(
161
- runner: WasmSqlRunner,
142
+ runner: BrowserSqlRunner,
162
143
  block: RunnableSqlBlock,
163
144
  timeoutMs: number,
164
145
  ): Promise<BlockResult> {
@@ -278,9 +259,9 @@ export async function cliMain(argv: readonly string[]): Promise<void> {
278
259
 
279
260
  const USAGE = `Usage: duckfn-sql-verify [options]
280
261
 
281
- Runs every runnable SQL block of a duckfn docs site in DuckDB-Wasm, and checks
282
- that each one behaves as its own metadata declares ("expect": "error" for a
283
- block that demonstrates a failure).
262
+ Runs every runnable SQL block of a duckfn docs site in a headless browser
263
+ (DuckDB-Wasm), and checks that each one behaves as its own metadata declares
264
+ ("expect": "error" for a block that demonstrates a failure).
284
265
 
285
266
  --site <dir> Docs site root (default: the working directory)
286
267
  --content <dir> Content directory, relative to the site root (repeatable;
@@ -291,8 +272,9 @@ block that demonstrates a failure).
291
272
  --platform <eh|mvp> DuckDB-Wasm bundle, which must match the extension build
292
273
  (default: eh)
293
274
  --engine <path> Engine wasm override
275
+ --browser <path> Browser executable (default: a detected Chrome/Edge,
276
+ or the DFK_BROWSER environment variable)
294
277
  --timeout <ms> Per-block timeout (default: 30000)
295
- --working-dir <dir> Directory the blocks run in (default: a temporary one)
296
278
  --report <file> Write the full result list as JSON
297
279
  --quiet Only report unexpected behaviour
298
280
  --help Show this help
@@ -330,12 +312,12 @@ function parseArgs(argv: readonly string[]): ParsedArgs {
330
312
  case '--engine':
331
313
  options.engine = next();
332
314
  break;
315
+ case '--browser':
316
+ options.browser = next();
317
+ break;
333
318
  case '--timeout':
334
319
  options.timeoutMs = Number(next());
335
320
  break;
336
- case '--working-dir':
337
- options.workingDir = next();
338
- break;
339
321
  case '--report':
340
322
  options.reportFile = next();
341
323
  break;
@@ -1,16 +0,0 @@
1
- /**
2
- * The CodeMirror 6 editor that *is* the code view of a runnable SQL block.
3
- *
4
- * Every CodeMirror module arrives through dynamic `import()` inside
5
- * {@link mountSqlEditor}: a page full of SQL examples pays nothing for the
6
- * editor on its critical path, and Docusaurus' Node prerender never evaluates
7
- * any of it.
8
- */
9
- export interface SqlEditor {
10
- getValue(): string;
11
- setValue(value: string): void;
12
- /** Soft-wraps long lines, or stops wrapping them (the block's wrap toggle). */
13
- setWrap(wrapped: boolean): void;
14
- destroy(): void;
15
- }
16
- export declare function mountSqlEditor(container: HTMLElement, value: string, onChange: (value: string) => void): Promise<SqlEditor>;
@@ -1,51 +0,0 @@
1
- export interface DuckdbQueryResult {
2
- numRows: number;
3
- schema: {
4
- fields: {
5
- name: string;
6
- }[];
7
- };
8
- }
9
- export interface DuckdbConnection {
10
- query(sql: string): Promise<DuckdbQueryResult>;
11
- }
12
- export type WasmPlatform = 'eh' | 'mvp';
13
- export interface RunnerOptions {
14
- /**
15
- * The extension to `LOAD`: a local `.duckdb_extension.wasm` path (served to
16
- * the worker over a loopback http server) or an absolute `http(s)` URL.
17
- */
18
- extension: string;
19
- /**
20
- * DuckDB-Wasm platform. Must match how the extension was built — a site
21
- * serving `duckfn-wasm_eh.duckdb_extension.wasm` runs the `eh` bundle.
22
- */
23
- platform?: WasmPlatform;
24
- /** The engine wasm; defaults to the one shipped beside the worker bundle. */
25
- engine?: string;
26
- }
27
- export interface RunResult {
28
- rows: number;
29
- columns: number;
30
- }
31
- /**
32
- * One DuckDB-Wasm instance at a time, driven from Node.
33
- *
34
- * `newPage()` is what a docs site does per page load: a fresh instance, a fresh
35
- * connection, the extension loaded again. Blocks of one page then share state
36
- * (a table created in one block is visible to the next), while pages stay
37
- * isolated — which is why the runner is used one page at a time rather than
38
- * over a single long-lived connection.
39
- */
40
- export declare class WasmSqlRunner {
41
- #private;
42
- private constructor();
43
- static create(options: RunnerOptions): Promise<WasmSqlRunner>;
44
- /** Drops the current instance and starts a fresh page: new instance, new connection. */
45
- newPage(): Promise<void>;
46
- /** Runs one block; the caller decides whether a failure is expected. */
47
- run(sql: string): Promise<RunResult>;
48
- /** Where the fetched extension was staged, for diagnostics. */
49
- get stagingDir(): string | null;
50
- close(): Promise<void>;
51
- }
@@ -1,115 +0,0 @@
1
- import { createRequire as e } from "node:module";
2
- import { basename as t, dirname as n, join as r } from "node:path";
3
- import { mkdirSync as i, readFileSync as a, rmSync as o } from "node:fs";
4
- import { createServer as s } from "node:http";
5
- import { Worker as c } from "node:worker_threads";
6
- import { homedir as l } from "node:os";
7
- //#region src/sql/nodeRunner.ts
8
- var u = e(import.meta.url), d = "@duckdb/duckdb-wasm/dist/duckdb-node.cjs", f = class {
9
- #e;
10
- #t = /* @__PURE__ */ new Map();
11
- onmessage = null;
12
- onerror = null;
13
- onclose = null;
14
- constructor(e) {
15
- this.#e = new c(u.resolve(d), { workerData: {
16
- mod: e,
17
- name: "",
18
- type: ""
19
- } }), this.#e.on("message", (e) => this.#n("message", e)), this.#e.on("error", (e) => this.#n("error", e)), this.#e.on("exit", () => this.#n("close"));
20
- }
21
- #n(e, t) {
22
- let n = {
23
- type: e,
24
- data: t,
25
- target: this,
26
- currentTarget: this
27
- }, r = this[`on${e}`];
28
- typeof r == "function" && r(n);
29
- for (let t of this.#t.get(e) ?? []) t(n);
30
- }
31
- addEventListener(e, t) {
32
- this.#t.set(e, [...this.#t.get(e) ?? [], t]);
33
- }
34
- removeEventListener(e, t) {
35
- this.#t.set(e, (this.#t.get(e) ?? []).filter((e) => e !== t));
36
- }
37
- postMessage(e, t) {
38
- this.#e.postMessage(e, t ?? []);
39
- }
40
- terminate() {
41
- return this.#e.terminate();
42
- }
43
- }, p = class e {
44
- #e = null;
45
- #t = null;
46
- #n = null;
47
- #r = null;
48
- #i;
49
- #a;
50
- #o;
51
- constructor(e, t, n) {
52
- this.#i = e, this.#a = t, this.#o = n;
53
- }
54
- static async create(t) {
55
- let i = t.platform ?? "eh", a = u.resolve(`@duckdb/duckdb-wasm/dist/duckdb-node-${i}.worker.cjs`), o = t.engine ?? r(n(a), `duckdb-${i}.wasm`), s = /^https?:\/\//i.test(t.extension) ? null : await m(t.extension), c = s ? s.url : t.extension, l = new e(c, o, a);
56
- return l.#n = s ? s.server : null, l.#r = s ? h(s.stagingHost, s.stagingSegment) : null, l;
57
- }
58
- async newPage() {
59
- this.#e && (await this.#e.terminate(), this.#e = null, this.#t = null);
60
- let e = await import(
61
- /* @vite-ignore */
62
- d
63
- ), t = new e.AsyncDuckDB(new e.ConsoleLogger(), new f(this.#o));
64
- await t.instantiate(this.#a, null), await t.open({ allowUnsignedExtensions: !0 });
65
- let n = await t.connect();
66
- await n.query(`LOAD '${this.#i}'`), this.#e = t, this.#t = n;
67
- }
68
- async run(e) {
69
- let t = this.#t;
70
- if (!t) throw Error("sql/verify: no page is open — call newPage() first");
71
- let n = await t.query(e);
72
- return {
73
- rows: n.numRows,
74
- columns: n.schema.fields.length
75
- };
76
- }
77
- get stagingDir() {
78
- return this.#r;
79
- }
80
- async close() {
81
- this.#e && (await this.#e.terminate(), this.#e = null, this.#t = null), this.#n &&= (await new Promise((e) => this.#n?.close(() => e())), null);
82
- }
83
- };
84
- async function m(e) {
85
- let n = a(e), r = t(e), i = g(e), o = s((e, t) => {
86
- t.writeHead(200, {
87
- "content-type": "application/octet-stream",
88
- "content-length": n.length
89
- }), t.end(n);
90
- }), c = process.platform === "win32" ? 80 : 0;
91
- await new Promise((e, t) => {
92
- o.once("error", (e) => {
93
- t(/* @__PURE__ */ Error(`sql/verify: cannot serve the extension on port ${c} (${e.code}): a port in the URL would put a colon into DuckDB's staging path, which is not a legal Windows path — free port 80, or load the extension from an http(s) URL with --extension`));
94
- }), o.listen(c, "localhost", e);
95
- });
96
- let l = o.address(), u = typeof l == "object" && l ? l.port : c, d = u === 80 ? "localhost" : `localhost:${u}`;
97
- return {
98
- server: o,
99
- url: `http://${d}/${i}/${r}`,
100
- stagingHost: d,
101
- stagingSegment: i
102
- };
103
- }
104
- function h(e, t) {
105
- let n = r(l(), ".duckdb", "extensions", e, t);
106
- return o(n, {
107
- recursive: !0,
108
- force: !0
109
- }), i(n, { recursive: !0 }), n;
110
- }
111
- function g(e) {
112
- return t(e).split(".")[0] ?? "";
113
- }
114
- //#endregion
115
- export { p as WasmSqlRunner };
package/src/sql/editor.ts DELETED
@@ -1,75 +0,0 @@
1
- /**
2
- * The CodeMirror 6 editor that *is* the code view of a runnable SQL block.
3
- *
4
- * Every CodeMirror module arrives through dynamic `import()` inside
5
- * {@link mountSqlEditor}: a page full of SQL examples pays nothing for the
6
- * editor on its critical path, and Docusaurus' Node prerender never evaluates
7
- * any of it.
8
- */
9
-
10
- export interface SqlEditor {
11
- getValue(): string;
12
- setValue(value: string): void;
13
- /** Soft-wraps long lines, or stops wrapping them (the block's wrap toggle). */
14
- setWrap(wrapped: boolean): void;
15
- destroy(): void;
16
- }
17
-
18
- export async function mountSqlEditor(
19
- container: HTMLElement,
20
- value: string,
21
- onChange: (value: string) => void,
22
- ): Promise<SqlEditor> {
23
- const [
24
- {basicSetup},
25
- {sql},
26
- {EditorView, keymap},
27
- {defaultKeymap, historyKeymap},
28
- {Compartment},
29
- ] = await Promise.all([
30
- import('codemirror'),
31
- import('@codemirror/lang-sql'),
32
- import('@codemirror/view'),
33
- import('@codemirror/commands'),
34
- import('@codemirror/state'),
35
- ]);
36
-
37
- // Wrapping is toggled from the outside, and reconfiguring it must not disturb
38
- // the document or the undo history — that is exactly what a compartment is
39
- // for, so the extension is swapped in place rather than rebuilt.
40
- const wrap = new Compartment();
41
-
42
- const view = new EditorView({
43
- doc: value,
44
- extensions: [
45
- basicSetup,
46
- sql(),
47
- keymap.of([...defaultKeymap, ...historyKeymap]),
48
- wrap.of([]),
49
- EditorView.updateListener.of((update) => {
50
- if (update.docChanged) {
51
- onChange(update.state.doc.toString());
52
- }
53
- }),
54
- ],
55
- parent: container,
56
- // `root` is left to CodeMirror's own `getRoot(container)`. The container sits
57
- // in `<dfk-sql>`'s shadow root, so style-mod mounts the base theme into that
58
- // same shadow root — exactly where the `.cm-*` rules are needed. Pinning it
59
- // to `document` would put them outside the editor's tree instead, where a
60
- // shadow boundary stops them.
61
- });
62
-
63
- return {
64
- getValue: () => view.state.doc.toString(),
65
- setValue: (next: string) =>
66
- view.dispatch({
67
- changes: {from: 0, to: view.state.doc.length, insert: next},
68
- }),
69
- setWrap: (wrapped: boolean) =>
70
- view.dispatch({
71
- effects: wrap.reconfigure(wrapped ? EditorView.lineWrapping : []),
72
- }),
73
- destroy: () => view.destroy(),
74
- };
75
- }