duckfn-docs-kit 0.1.0 → 0.2.1
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 +224 -689
- package/README.md +7 -5
- package/bin/sql-verify.mjs +12 -0
- package/dist/remark.d.ts +1 -1
- package/dist/sql/collect.d.ts +29 -0
- package/dist/sql/collect.js +65 -0
- package/dist/sql/nodeRunner.d.ts +51 -0
- package/dist/sql/nodeRunner.js +115 -0
- package/dist/sql/remark.d.ts +20 -0
- package/dist/sql/remark.js +1 -1
- package/dist/sql/verify.d.ts +61 -0
- package/dist/sql/verify.js +148 -0
- package/package.json +5 -1
- package/src/remark.ts +1 -1
- package/src/sql/DfkSql.ts +2 -2
- package/src/sql/collect.ts +136 -0
- package/src/sql/nodeRunner.ts +298 -0
- package/src/sql/remark.ts +21 -3
- package/src/sql/sql.css +1 -1
- package/src/sql/verify.ts +357 -0
- package/src/toc-toggle/TocToggle.css +28 -0
package/AGENTS.md
CHANGED
|
@@ -1,689 +1,224 @@
|
|
|
1
|
-
# duckfn-docs-kit —
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
`
|
|
67
|
-
- **
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
`
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
-
|
|
224
|
-
|
|
225
|
-
`dropdown_menu_click`(`menuKey = menuItem.menuKey || menuItem.text`,故每项都显式给
|
|
226
|
-
`MENU.*` key 以与本地化文案解耦)。`getCellInfo(col,row).field` 对表体单元格也返回所属
|
|
227
|
-
列 field,故菜单项对表头/表体都能定位到列。
|
|
228
|
-
- `menu`/`tooltip` 的 `parentElement` 默认是 `table.getElement()`(在 `.dfk-sql-table` 内),
|
|
229
|
-
故不与全屏 overlay 抢 z-index。它们的 `renderMode` 默认 `html`,vendor 自己会注入一份
|
|
230
|
-
**写死浅色(#fff/#000、Roboto)**的文档级样式表,所以暗色模式下菜单/提示仍是浅色 ——
|
|
231
|
-
这是 vendor 现状,**不要用 `sql.css` 去改它的配色**;真要隔离就改走 Shadow DOM,但要
|
|
232
|
-
注意 VTable 的样式表注入在 `document.head`,跨不过 shadow 边界(需把那段 CSS 复制进
|
|
233
|
-
shadow root 才能生效)。
|
|
234
|
-
|
|
235
|
-
**扩展加载(`runtime.ts`)**
|
|
236
|
-
|
|
237
|
-
- **wasm 上 `INSTALL` 是空操作**(没有可安装的持久存储),只有 `LOAD` 真的 fetch
|
|
238
|
-
`.duckdb_extension.wasm`、验签、加载;`INSTALL … FROM` 只记录「这个扩展以后从哪
|
|
239
|
-
拉」。所以按名加载 =(可选 `INSTALL <name> FROM <community|'url'>`)+ `LOAD <name>`;
|
|
240
|
-
`{url}` 条目 = 直接 `LOAD '<绝对URL>'`(worker 基于 blob URL,解析不了相对路径,
|
|
241
|
-
站内路径由主线程先拼成绝对 URL)。**不要用 `SET custom_extension_repository`**:
|
|
242
|
-
那是全局状态,会污染之后所有扩展的加载。
|
|
243
|
-
- 扩展名 / 仓库 / URL 是**白名单校验**而非转义——它们直接进 SQL 文本;仓库关键字
|
|
244
|
-
(`community` / `core`)裸拼,URL 加引号。
|
|
245
|
-
- 按 `${repository}\0${name}`(或 `url\0<url>`)记忆化(成功与 in-flight 都记),
|
|
246
|
-
失败时从表里删掉以便重试;**已加载集合全页共享**,与「每个块状态独立」不冲突。
|
|
247
|
-
- **页面进入即预热**:`DfkSql.connectedCallback()` 顺手 `init()`(失败静默——
|
|
248
|
-
`init()` 可重试,最终由点 Run 的块把错误显示出来),所以第一个点 Run 的人不用等
|
|
249
|
-
DuckDB 下载与预加载;没有可运行块的页面(如首页)不会触发。
|
|
250
|
-
- `allowUnsignedExtensions` **只由第一个 `init()` 决定**:配置在 `open()` 时一次性交给
|
|
251
|
-
worker,之后改不了。站点级值(注入配置)与块级值在创建实例前合并,谁先到谁定调。
|
|
252
|
-
- 文件名的契约:文件名**第一个 `.` 之前必须是扩展名**(wasm 用它拼 `<name>_init_c_api`
|
|
253
|
-
入口符号),所以 release 资产 `duckfn-wasm_eh.duckdb_extension.wasm` 落盘时
|
|
254
|
-
必须改名为 `duckfn.duckdb_extension.wasm`;`runtimeConfig` 两侧都做校验兜底。
|
|
255
|
-
|
|
256
|
-
**扩展预加载(`sql/extensions.ts` + `runtimeConfig.ts`)**
|
|
257
|
-
|
|
258
|
-
- 站点在 `docusaurus.config.ts` 用 `dfkExtensions({preload, allowUnsignedExtensions})` 配
|
|
259
|
-
一条**有序**列表;插件规范化后注入每个页面的
|
|
260
|
-
`<script id="dfk-sql-runtime" type="application/json">`,`runtime.ts` 首次 `init()`
|
|
261
|
-
读一次,按序加载完才置 `ready`。同一次 `getClientModules()` 还把 `sql/client`
|
|
262
|
-
(注册 `dfk-*` 元素)注入每个页面——docs 页面从不 import 本包的 React 树,元素
|
|
263
|
-
注册必须从客户端模块来,所以站点不再需要自备 clientModules 文件。TOC 折叠的胶水
|
|
264
|
-
同理,由 `dfkTocToggle()`(`toc-toggle/plugin`)注入 `toc-toggle/client`;两个插件
|
|
265
|
-
都从**站点**(`createRequire(siteDir/package.json)`)解析自己 `dist/` 下的 client
|
|
266
|
-
入口,所以站点打包 config 也不会断。
|
|
267
|
-
- 三种来源:裸扩展名(官方仓库)、`{name, repository}`(`community` / `core` / URL)、
|
|
268
|
-
`{url}`(同源静态文件或绝对 URL)。
|
|
269
|
-
- `{url}` 带 `release` 时,插件在 dev/build 启动时从 GitHub **最新** release 拉取该资产
|
|
270
|
-
到 `static/<url>`:本地缓存(默认 `<siteDir>/.cache/duckfn-docs-kit`)+ sha256
|
|
271
|
-
sidecar,与 release 的 `digest` 相同就跳过下载;网络失败时有缓存则降级为警告
|
|
272
|
-
(CI 每次全新环境、无缓存,会直接失败),资产名对不上时报错并列出可用名。
|
|
273
|
-
- 注入路径用 `context.siteConfig.baseUrl` 拼(多语言构建时它是本地化值;Docusaurus 会把
|
|
274
|
-
`static/` 拷进每个 locale 的 outDir,天然自洽);站内相对路径由 `runtime.ts` 在
|
|
275
|
-
浏览器里解析成绝对 URL。
|
|
276
|
-
- `runtimeConfig.ts` 是两侧唯一共享契约:无任何 import 的纯类型 + 校验函数,Node 插件与
|
|
277
|
-
浏览器 bundle 都不会把对方拖进来;Docusaurus 插件 API 用**结构化类型**,本包不依赖
|
|
278
|
-
`@docusaurus/types`。
|
|
279
|
-
- **版本耦合(改动前先读回)**:wasm 扩展只能由「与 duckdb-wasm 内置 DuckDB 版本 ABI
|
|
280
|
-
兼容」的构建提供,所以 kit 的 `package.json` 把 `@duckdb/duckdb-wasm` 固定成**精确
|
|
281
|
-
版本**(当前 `1.33.1-dev64.0`,内置 v1.5.5,与 CI 的 `TARGET_DUCKDB_VERSION` 一致)。
|
|
282
|
-
实测:`1.32.0`(内置 v1.4.3)拒绝 v1.5.5 构建的扩展(C API slot 数 459 vs 546,
|
|
283
|
-
报 `C extension API layout mismatch`);`1.33.1-dev57.0`(内置 v1.5.4)反而能加载
|
|
284
|
-
——wasm 补丁只校验 C API slot 数(1.5.4/1.5.5 的 unstable 区未变),原生则按版本
|
|
285
|
-
戳严格校验(1.5.4 的原生 duckdb 会拒绝 v1.5.5 构建的扩展)。升级 duckdb-wasm 或
|
|
286
|
-
改 CI 的 `duckdb_version` 时必须成对验证(跑一遍可运行 SQL 页的两个示例块即可)。
|
|
287
|
-
|
|
288
|
-
## 代码风格(硬性要求)
|
|
289
|
-
|
|
290
|
-
这些规则是本包存在的意义所在,评审时逐条对照。
|
|
291
|
-
|
|
292
|
-
### 0. 设计目标:保留模式 UI,不是 mini React / mini Lit
|
|
293
|
-
|
|
294
|
-
**明确禁止** React / Vue / Lit 风格的「状态 → render → 重建 DOM」模型。
|
|
295
|
-
本包的目标是:
|
|
296
|
-
|
|
297
|
-
> 用 TypeScript 对浏览器原生 DOM API 做面向对象封装。
|
|
298
|
-
|
|
299
|
-
组件应该表现得像一个**持有内部 DOM 对象的普通类**,而不是一个「根据 state
|
|
300
|
-
不断重新计算 UI」的函数。冲突时按下面的优先级裁决(上面的赢):
|
|
301
|
-
|
|
302
|
-
1. 保留 DOM 节点 —— 节点长期存在,不随状态重建
|
|
303
|
-
2. 类字段持有 DOM 引用
|
|
304
|
-
3. 直接修改 DOM(property / attribute / text / class / 事件监听)
|
|
305
|
-
4. 局部更新
|
|
306
|
-
5. 必要时才替换**局部集合**
|
|
307
|
-
6. 禁止整组件 re-render
|
|
308
|
-
7. 禁止用响应式 state 驱动 render
|
|
309
|
-
8. 禁止引入 Lit / React / Vue 等 UI runtime 或其设计模式
|
|
310
|
-
|
|
311
|
-
### 1. 禁止拼 HTML 字符串
|
|
312
|
-
|
|
313
|
-
不许 `innerHTML = \`...\``、不许 `insertAdjacentHTML`、不许任何「模板字符串
|
|
314
|
-
生成标记再塞进 DOM」的写法。
|
|
315
|
-
|
|
316
|
-
```ts
|
|
317
|
-
// 禁止
|
|
318
|
-
this.innerHTML = `<button class="x">${label}</button>`;
|
|
319
|
-
```
|
|
320
|
-
|
|
321
|
-
### 2. DOM 用对象操作,字段持有引用
|
|
322
|
-
|
|
323
|
-
`document.createElement` 创建、**类的字段持有引用**、直接对字段调方法。
|
|
324
|
-
|
|
325
|
-
```ts
|
|
326
|
-
readonly #run: HTMLButtonElement;
|
|
327
|
-
readonly #output: HTMLDivElement;
|
|
328
|
-
|
|
329
|
-
constructor() {
|
|
330
|
-
super();
|
|
331
|
-
this.#run = document.createElement('button');
|
|
332
|
-
this.#run.textContent = 'Run';
|
|
333
|
-
this.#run.addEventListener('click', () => this.#onRun());
|
|
334
|
-
this.#output = document.createElement('div');
|
|
335
|
-
}
|
|
336
|
-
```
|
|
337
|
-
|
|
338
|
-
**`querySelector` 不得当作组件内部的状态管理方式**。需要反复访问的节点一律
|
|
339
|
-
存成字段;`querySelector` 只允许出现在「从外部挂载点找目标」(如
|
|
340
|
-
`TocToggle.ts` 里找 `.theme-doc-toc-desktop`)这类不属于组件自身结构的地方。
|
|
341
|
-
|
|
342
|
-
纯结构节点用 `el()`(`src/dom.ts`)建,不必展开成 `createElement` + 逐行赋值。它的
|
|
343
|
-
options 按标签收窄,直接写标签自己的属性;`class` / `text` 是 `className` /
|
|
344
|
-
`textContent` 的简写,`aria-*` / `data-*` 以及没有同名属性的自定义元素属性走 `attrs`
|
|
345
|
-
兜底。属性名拼错、值类型不对、把 `style` / `dataset` / 方法名塞进去,都是编译错误:
|
|
346
|
-
|
|
347
|
-
```ts
|
|
348
|
-
readonly #logo = el('img', {class: 'dfk-logo', alt: '', width: 480, height: 480});
|
|
349
|
-
```
|
|
350
|
-
|
|
351
|
-
第三个参数是可选的 `init` 回调,用来**就地描述不会再被单独引用的结构**,省掉一个
|
|
352
|
-
字段;需要反复访问的节点仍然存成字段:
|
|
353
|
-
|
|
354
|
-
```ts
|
|
355
|
-
this.root.append(
|
|
356
|
-
el('span', {class: 'dfk-next-card-body'}, (body) =>
|
|
357
|
-
body.append(this.#title, this.#details),
|
|
358
|
-
),
|
|
359
|
-
this.#arrow,
|
|
360
|
-
);
|
|
361
|
-
```
|
|
362
|
-
|
|
363
|
-
`init` 里不要读**声明顺序在其后**的 `#字段`(字段初始化器按声明顺序求值,会踩 TDZ):
|
|
364
|
-
在构造函数里传 `init` 最稳妥,构造函数执行时所有字段都已初始化。
|
|
365
|
-
|
|
366
|
-
### 3. 更新是局部、直接、明确的;不得有通用刷新机制
|
|
367
|
-
|
|
368
|
-
状态变化 = 改**相关的那几个**节点,用命名的领域 setter 表达,而不是把整个 UI
|
|
369
|
-
当成 `data` 的函数重新算一遍。
|
|
370
|
-
|
|
371
|
-
```ts
|
|
372
|
-
setResult(value: string): void {
|
|
373
|
-
this.#output.textContent = value;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
setLoading(value: boolean): void {
|
|
377
|
-
this.#run.disabled = value;
|
|
378
|
-
this.#output.hidden = value;
|
|
379
|
-
}
|
|
380
|
-
```
|
|
381
|
-
|
|
382
|
-
改哪一处用**标准 DOM API** 直说,不要绕:`textContent`、`setAttribute` /
|
|
383
|
-
`removeAttribute`、`classList.add/remove/toggle`、`hidden`、`disabled`、`value`,
|
|
384
|
-
以及 `href` / `src` 这类直接赋值的属性。除此之外不要再找第三种写法。
|
|
385
|
-
|
|
386
|
-
**必须避免**的形态(贴出来是为了让评审一眼对上号):
|
|
387
|
-
|
|
388
|
-
```ts
|
|
389
|
-
set data(value) { this.#data = value; this.render(); } // 响应式 state 驱动 render
|
|
390
|
-
update(data) { this.replaceChildren(); this.render(data); } // 用替换子树模拟更新
|
|
391
|
-
render(data) { /* 根据 data 重新构建整个 DOM */ } // 通用刷新机制
|
|
392
|
-
rebuild() { this.replaceChildren(); this.build(); } // 同上,换个名字而已
|
|
393
|
-
```
|
|
394
|
-
|
|
395
|
-
不允许实现 `render()` / `rebuild()` / `rerender()` 之类的**通用**刷新机制;
|
|
396
|
-
不允许因为一个字段变化就重建整个子树;不允许通过「替换子树」模拟响应式更新。
|
|
397
|
-
|
|
398
|
-
### 4. `replaceChildren()` 的分级用法
|
|
399
|
-
|
|
400
|
-
`replaceChildren()` 本身不是禁品,禁的是把它当**整组件的通用 render 机制**:
|
|
401
|
-
|
|
402
|
-
- 允许:**确实需要整体更新(或一次性静态组装)的局部集合**,例如卡片网格
|
|
403
|
-
一次给出全新条目、按钮的「图标 + 文本」这种固定组合。
|
|
404
|
-
- 禁止:拿它清空组件自己的根子树再重建 —— 那是第 3 条里的 `rebuild()`。
|
|
405
|
-
|
|
406
|
-
变长列表能增量就增量:按数据长度**增删条目**、复用已有条目对象
|
|
407
|
-
(`DfkHero.ts` 的 `setBadges()` 就是这么做的),只有条目语义整体失效时才整批替换。
|
|
408
|
-
|
|
409
|
-
### 5. 内容入口:命名的领域 setter(本包已统一)
|
|
410
|
-
|
|
411
|
-
`dfk-*` 元素**不接受整份 data blob,也没有通用的 render/apply 入口**。每个组件
|
|
412
|
-
暴露一组命名的领域 setter,一个方法只碰它负责的那部分节点:
|
|
413
|
-
|
|
414
|
-
```ts
|
|
415
|
-
hero.setTitle(text);
|
|
416
|
-
hero.setTagline(text);
|
|
417
|
-
hero.setPrimaryAction({label, href});
|
|
418
|
-
hero.setBadges(badges);
|
|
419
|
-
|
|
420
|
-
features.setSectionTitle(text);
|
|
421
|
-
features.setFeatures(items);
|
|
422
|
-
```
|
|
423
|
-
|
|
424
|
-
- setter 只做 mutation;变长集合(badge 行、卡片网格)按新长度增删条目、复用
|
|
425
|
-
已有条目对象,不整体重建。
|
|
426
|
-
- setter 只写自己持有的节点,所以**在元素尚未连接、甚至尚未插入文档时调用也安全**。
|
|
427
|
-
消费方因此不必关心 `connectedCallback()` 的时序,可以乱序批量喂数据。
|
|
428
|
-
- **不做 attribute reflection**:组件不读属性、也不把 setter 的值镜像回 attribute
|
|
429
|
-
(内容不是可序列化的标记,唯一例外是自有节点上 `iconify-icon` 的 `icon` 属性)。
|
|
430
|
-
内容入口只有 setter。
|
|
431
|
-
- 新增字段就加一个对应的 setter,**不要**回头引入 `setData()` / `update()` /
|
|
432
|
-
`apply()` 这类通用入口。
|
|
433
|
-
- 改 setter 签名属于公开 API 变更,需同步 `docs/src/pages/index.tsx` 的
|
|
434
|
-
`mount*()` 助手、`src/index.ts` 的类型导出,以及下游消费方。
|
|
435
|
-
- **例外(attribute 种子)**:`<dfk-sql>` 由 `sql/remark` 插件从 Markdown 自动
|
|
436
|
-
生成为 `<dfk-sql config="…" sql="…">`(外加一份预渲染的加载占位,见「加载占位」),
|
|
437
|
-
没有 `mount*()` 助手、也挂不上 ref
|
|
438
|
-
去调 setter —— 内容只能来自 `config` / `sql` 两个字符串属性。因此它在
|
|
439
|
-
`connectedCallback()` 里 `getAttribute` **各读一次**作初始种子。这与「不做
|
|
440
|
-
attribute reflection」不冲突:读一次用于初始化,不是 attribute 变化再驱动
|
|
441
|
-
重渲染,仍是保留模式。新增同类「由构建期插件生成、无 React 挂载点」的元素
|
|
442
|
-
才可套用此例外,手写 JSX 的元素仍走 setter。
|
|
443
|
-
|
|
444
|
-
### 6. 生命周期:构造函数建树 + 挂 shadow root
|
|
445
|
-
|
|
446
|
-
优先在 `constructor()` 里创建静态结构、`attachShadow` 并组装完毕;
|
|
447
|
-
`connectedCallback()` 只做**必要的一次性初始化**(启动监听、注册外部资源)。
|
|
448
|
-
不要把 `connectedCallback() → rebuild() → build()` 当成标准渲染生命周期。
|
|
449
|
-
|
|
450
|
-
**Custom Elements 规范限制(必须遵守)**:构造函数里**不得给 `this`
|
|
451
|
-
加属性或子节点**(`this.append(...)`、`this.setAttribute(...)` 都不行),
|
|
452
|
-
否则 `document.createElement()` / `innerHTML` 解析创建的元素会抛错。
|
|
453
|
-
`attachShadow()` 不在禁止之列,所以正确拆法是:构造时把结构挂进 shadow root,
|
|
454
|
-
light DOM 始终空着。
|
|
455
|
-
|
|
456
|
-
```ts
|
|
457
|
-
constructor() {
|
|
458
|
-
super();
|
|
459
|
-
this.#section = document.createElement('section');
|
|
460
|
-
this.#section.append(this.#title, this.#body); // 组装进自有节点
|
|
461
|
-
const shadow = this.attachShadow({mode: 'open'}); // 构造函数里合法
|
|
462
|
-
shadow.adoptedStyleSheets = [homeStyles()];
|
|
463
|
-
shadow.appendChild(this.#section);
|
|
464
|
-
}
|
|
465
|
-
```
|
|
466
|
-
|
|
467
|
-
组件因此**从诞生那一刻起结构就完整**,setter 在元素连接前调用也安全,
|
|
468
|
-
`connectedCallback()` 不再承担「挂载」职责。
|
|
469
|
-
|
|
470
|
-
**监听器按对象归属决定挂在哪**:
|
|
471
|
-
|
|
472
|
-
- 挂在**自有节点**上(`this.#primary.addEventListener('click', …)`)→ 写在
|
|
473
|
-
`constructor()` 里,节点与元素同生命周期,不需要清理,也不需要重挂。
|
|
474
|
-
- 挂在**外部对象**上(`document` / `window` / `matchMedia`)→ 必须在
|
|
475
|
-
`disconnectedCallback()` 里 `removeEventListener` / `removeListener`,且注册要有
|
|
476
|
-
幂等标志 —— 元素被移动或 React 重挂载会再次触发 `connectedCallback()`,重复注册
|
|
477
|
-
会让回调执行多次。
|
|
478
|
-
|
|
479
|
-
### 7. 尽量类化,但没有「组件基类」
|
|
480
|
-
|
|
481
|
-
一个组件一个 class:web component 直接 `extends HTMLElementBase`(`src/dom.ts`),
|
|
482
|
-
**`home/` 下不存在共享的组件基类**。每个类自带字段、constructor 组装并挂进自己
|
|
483
|
-
的 shadow root、自己的 setter,不做 `create()` / `apply()` 之类的模板
|
|
484
|
-
方法抽象 —— 那种抽象会把「状态驱动刷新」重新引进来,正是第 0、3 条要避免的。
|
|
485
|
-
|
|
486
|
-
非元素的小部件(`TocToggle`、卡片类 `DfkFeatureCard` / `DfkNextStepCard`)同样
|
|
487
|
-
是 class。函数式导出只留给真正的纯工具(`dom.ts` 的 `el()`)。
|
|
488
|
-
|
|
489
|
-
### 8. 图标一律用官方 `<iconify-icon>` web component
|
|
490
|
-
|
|
491
|
-
npm 包 `iconify-icon`,`register.ts` 里 side-effect import 注册。
|
|
492
|
-
`document.createElement('iconify-icon')` + `setAttribute('icon',
|
|
493
|
-
'lucide:sparkles')` 即可,字形按需从 Iconify 公共 API 加载。
|
|
494
|
-
|
|
495
|
-
- **不要**自己封装 SVG(不手写路径、不拼 `data:image/svg+xml`、不做 CSS mask
|
|
496
|
-
助手)—— 官方的实现比自研封装好。
|
|
497
|
-
- 数据契约里图标就是 Iconify 名字字符串(`'lucide:arrow-right'`),不引入
|
|
498
|
-
`@iconify/types`、不装 `@iconify-icons/*` 本地图标集。
|
|
499
|
-
- 尺寸/颜色用 CSS 作用在 `iconify-icon` 元素上(`home.css` 的
|
|
500
|
-
`.dfk-button-icon` 等),字形自带 `currentColor`。
|
|
501
|
-
- **尺寸只认 `font-size`**:组件内部渲染的是 `<svg width="1em" height="1em">`,
|
|
502
|
-
字形大小跟随宿主的 font-size;给宿主设 CSS `width`/`height` 只会撑大空盒子,
|
|
503
|
-
图标本身不变。
|
|
504
|
-
|
|
505
|
-
### 9. 默认用 Shadow DOM
|
|
506
|
-
|
|
507
|
-
`dfk-*` 组件**默认渲染进 shadow root**(`mode: 'open'`),样式用
|
|
508
|
-
`adoptedStyleSheets` 注入(`home/styles.ts` 把 `home.css` 以 `?inline` 内联进
|
|
509
|
-
bundle,全局共享一个 `CSSStyleSheet`)。这样宿主页面的全局 CSS 进不来、组件的
|
|
510
|
-
CSS 也漏不出去,组件边界干净,不依赖「人工命名空间」去避免污染。
|
|
511
|
-
|
|
512
|
-
**主题照样跟随宿主**:CSS 自定义属性会**继承穿过 shadow 边界**,所以组件内部
|
|
513
|
-
用 `var(--duckfn-*)` / `var(--ifm-*)` 即可拿到消费站的 Infima 变量与品牌色,
|
|
514
|
-
`[data-theme]` 切换自动生效 —— 前提是这些变量声明在**文档的** `:root` /
|
|
515
|
-
`[data-theme]` 上(这正是 `tokens.css` 必须留在全局、不能塞进 shadow 的原因)。
|
|
516
|
-
组件内部**不要**写 `[data-theme]` 选择器,也不要依赖宿主的 class。
|
|
517
|
-
|
|
518
|
-
**只有「继承属性」能穿过边界**:`box-sizing` 不是继承属性,宿主 Infima 的
|
|
519
|
-
`* { box-sizing: border-box }` 选不进 shadow tree,组件内所有盒子会退回
|
|
520
|
-
`content-box`,带 padding / max-width 的盒子尺寸随之变化 —— 足以把布局阈值挪位
|
|
521
|
-
(实测:feature grid 在 72rem 容器上限处从 3 列变 4 列)。所以 `home.css` 顶部
|
|
522
|
-
必须在 shadow 作用域里**重新声明一次** box-sizing 重置,别指望宿主的通用规则。
|
|
523
|
-
|
|
524
|
-
**例外(万不得已才退回 light DOM)**:仅当组件必须直接复用消费站 light DOM 的
|
|
525
|
-
CSS 时 —— 例如 `TocToggle` 注入并改写 Docusaurus 自己的 TOC、其规则必须落在
|
|
526
|
-
`@layer docusaurus.theme-classic` 里 —— 才不用 shadow root。这种组件的类名一律
|
|
527
|
-
`dfk-` 前缀(或 `toc-` 这类自有前缀),避免与宿主撞名。新增例外要在评审时说清楚
|
|
528
|
-
「依赖了宿主的哪条规则」。
|
|
529
|
-
|
|
530
|
-
`<dfk-sql>` 是**混合**形态,且 light-DOM 部分只剩一件事:CodeMirror 编辑器与那簇悬浮
|
|
531
|
-
图标按钮**全在 shadow root 里**(编辑器不再 slot,因为 style-mod 会把 `.cm-*` 基础主题
|
|
532
|
-
以 `adoptedStyleSheets` 挂到 `getRoot()` 解析出的根上 —— 编辑器在 shadow 里,解析出的
|
|
533
|
-
就是同一个 shadow root,样式正好落在用它的那棵树里;反过来把编辑器放 light DOM、样式
|
|
534
|
-
却落进 shadow root,就是第一阶段那个「编辑器没样式」的 bug)。只有 **VTable 结果容器**
|
|
535
|
-
在 light DOM(`slot="dfk-result"`),因为 VTable 往**文档级**注入样式表,shadow 边界
|
|
536
|
-
挡得住它。其 light-DOM 样式(`.dfk-sql-result` 一族)走 `sql/sql.css` → `kit.css` 的全局
|
|
537
|
-
通道,同样全部 `dfk-sql-` 前缀。
|
|
538
|
-
|
|
539
|
-
这条也是第 11 条「light DOM 恒为空」的例外之所以安全的原因:编辑器在构造函数里就挂进
|
|
540
|
-
shadow root,light DOM 唯一的节点(结果容器)只在**用户点「执行」之后**才创建,
|
|
541
|
-
hydration 早已完成。
|
|
542
|
-
|
|
543
|
-
### 10. SSR 安全
|
|
544
|
-
|
|
545
|
-
Docusaurus 预渲染在 Node 里 import 本包。
|
|
546
|
-
|
|
547
|
-
- 继承 `HTMLElement` 的类必须 `extends HTMLElementBase`(`src/dom.ts`,Node 下
|
|
548
|
-
回退为空基类),否则模块求值直接崩。
|
|
549
|
-
- **类字段初始化器不要碰 `document`**:Node 下类体只被求值、不实例化,所以
|
|
550
|
-
字段初始化器安全的前提是「服务端永远不会 new 这个类」。目前正是如此,
|
|
551
|
-
浏览器侧由元素 upgrade(即 `constructor()`)触达。
|
|
552
|
-
- 触碰 `window` / `document` / `customElements` 的入口(`registerDfkElements()`、
|
|
553
|
-
`TocToggle.init()`)要么带守卫,要么由消费方在浏览器环境调用。
|
|
554
|
-
- `styles.ts` 的 `CSSStyleSheet` 必须**惰性创建**(`homeStyles()` 在构造函数里
|
|
555
|
-
才调用):模块级 `new CSSStyleSheet()` 会在 Node 预渲染 import 时直接崩。
|
|
556
|
-
`?inline` import 进来的只是字符串,模块级安全。
|
|
557
|
-
- `iconify-icon` 在 Node 里 import 是安全的(官方包已处理)。
|
|
558
|
-
- Node 构建期模块只有 `src/remark.ts`、`src/sql/remark.ts` 与 `src/sql/extensions.ts`
|
|
559
|
-
(连同无依赖的共享契约 `src/sql/runtimeConfig.ts`),它们不得 import 任何浏览器模块。
|
|
560
|
-
|
|
561
|
-
### 11. React 19 自定义元素
|
|
562
|
-
|
|
563
|
-
JSX 类型增强写在 `src/index.ts` 的
|
|
564
|
-
`declare module 'react' { namespace JSX { … } }`,新增元素时同步补上。
|
|
565
|
-
React 19 的 SSR/hydration 不会把对象 prop 设到自定义元素上(对象无法序列化进
|
|
566
|
-
预渲染 HTML),docs 侧用 callback ref 显式喂内容,见
|
|
567
|
-
`docs/src/pages/index.tsx`。
|
|
568
|
-
|
|
569
|
-
三点因此而来的约定:
|
|
570
|
-
|
|
571
|
-
- **`mount*()` 助手必须是纯 setter 调用(幂等)**。React 可能对同一节点多次调用
|
|
572
|
-
ref(StrictMode 双调用、元素移动后重挂载),重复喂同样的数据必须无副作用。
|
|
573
|
-
- **预渲染 HTML 里 `<dfk-*>` 是空壳,这是已知取舍**:结构挂在 shadow root 里,
|
|
574
|
-
light DOM 始终为空,所以外壳有、内容没有,hydration 之前那几块是空的
|
|
575
|
-
(未定义的自定义元素默认 `display: inline`,高度为 0)。这个一次性高度跳变是刻意
|
|
576
|
-
接受的代价。不要为此把结构改回「预渲染时也能拼出来」的写法 —— 那必然退回拼字符串
|
|
577
|
-
或响应式 render。
|
|
578
|
-
- **light DOM 恒为空顺带消除了 hydration mismatch**:以前 `connectedCallback()`
|
|
579
|
-
往 light DOM 挂子树,React hydration 比对子节点数量对不上,会报一次可恢复的
|
|
580
|
-
mismatch(minified #418)。现在服务端 HTML 与客户端元素的 light DOM 都是空的,
|
|
581
|
-
比对一致。若控制台再出现 #418 指向 `dfk-*`,说明有代码把节点挂回了 light DOM,
|
|
582
|
-
那是 bug,要修原因而不是用 `suppressHydrationWarning` 掩盖。
|
|
583
|
-
|
|
584
|
-
## 发版流程(npm)
|
|
585
|
-
|
|
586
|
-
发布的是 `duckfn-docs-kit` 这一个包,流程比 Rust 侧短:**切版本 → 提交并打 tag →
|
|
587
|
-
`npm publish` → 切下一开发版本**,没有远程流水线要等(`docs-kit-v*` 不匹配任何 workflow
|
|
588
|
-
的 tag 过滤器,理由见第 2 步)。
|
|
589
|
-
|
|
590
|
-
命令都在根目录的 `Justfile` 里(`just --list` 可查),实现是
|
|
591
|
-
`scripts/release-docs-kit.sh`。它与 `scripts/release.sh`(两个 crate 的发版)分开:两条流程
|
|
592
|
-
各打各的 tag,也互不触发对方的 CI。
|
|
593
|
-
|
|
594
|
-
版本号形如 `X.Y.Z`(例如 `0.1.0`),tag 形如 `docs-kit-v0.1.0`。只有**正式版本**才打 tag、
|
|
595
|
-
才发 npm;`0.1.1-dev.0` 这类开发版本留在分支上。
|
|
596
|
-
|
|
597
|
-
| 步骤 | 命令 |
|
|
598
|
-
| --- | --- |
|
|
599
|
-
| 0. 前置检查 | `just release_kit_check` |
|
|
600
|
-
| 1. 提升版本号 | `just release_kit_bump 0.1.1` |
|
|
601
|
-
| 2. 提交并打 tag | `git commit …` 后 `just release_kit_tag 0.1.1` |
|
|
602
|
-
| 3. 发布 npm 包 | `just release_kit_publish`(先 `npm login`) |
|
|
603
|
-
| 4. 切开发版本 | `just release_kit_dev 0.1.2-dev.0` |
|
|
604
|
-
|
|
605
|
-
### 0. 前置检查
|
|
606
|
-
|
|
607
|
-
```bash
|
|
608
|
-
just release_kit_check # 构建 + 类型检查 + npm pack --dry-run
|
|
609
|
-
npm whoami # 没登录先 npm login
|
|
610
|
-
```
|
|
611
|
-
|
|
612
|
-
`npm pack --dry-run` 打印 tarball 的文件清单,是发布前最值得看的一项:预期是 `dist/**` +
|
|
613
|
-
`src/**` + `AGENTS.md` + `README.md` + `LICENSE` + `package.json`(当前 64 个文件)。
|
|
614
|
-
多出别的东西时先查 `files` 白名单,不要靠 `.npmignore` 追着排除。
|
|
615
|
-
|
|
616
|
-
### 1. 提升版本号
|
|
617
|
-
|
|
618
|
-
```bash
|
|
619
|
-
just release_kit_bump 0.1.1
|
|
620
|
-
```
|
|
621
|
-
|
|
622
|
-
脚本把版本号从工作区的开发版本(如 `0.1.1-dev.0`)切成正式版本,只改两个文件:
|
|
623
|
-
`duckfn-docs-kit/package.json`,以及根 `package-lock.json` 里该 workspace 的条目;然后打印
|
|
624
|
-
残留的旧版本号(应为空)与 `git diff --stat`。
|
|
625
|
-
|
|
626
|
-
**不要用 `npm version` / `npm install` 改版本号**:它们会顺手 reify 整个 workspace、触发对
|
|
627
|
-
registry 的 fetch(本机被 `EALLOWREMOTE` 拦下),结果是版本号改了、lockfile 没动。脚本直接
|
|
628
|
-
重写这两处 JSON —— 两个文件的既有格式就是 2 空格缩进 + LF,重写是幂等的。
|
|
629
|
-
|
|
630
|
-
### 2. 提交并打 tag
|
|
631
|
-
|
|
632
|
-
```bash
|
|
633
|
-
git add -A
|
|
634
|
-
git commit -m "chore(release-kit): 发布 duckfn-docs-kit v0.1.1"
|
|
635
|
-
just release_kit_tag 0.1.1 # 打 docs-kit-v0.1.1,推送 main 与 tag
|
|
636
|
-
```
|
|
637
|
-
|
|
638
|
-
`docs-kit-v*` **故意**不匹配两个 workflow 的 tag 过滤器(`MainDistributionPipeline.yml` 与
|
|
639
|
-
`DeployDocs.yml` 都只认 `v*.*.*`):推这个 tag 不构建扩展、也不重发文档站,所以 kit 发版不必
|
|
640
|
-
等流水线;文档站仍然只由 crate 的 `v*.*.*` tag 触发。
|
|
641
|
-
|
|
642
|
-
### 3. 发布到 npm
|
|
643
|
-
|
|
644
|
-
```bash
|
|
645
|
-
npm login # 只需一次;启用了 2FA 的话发布时会要 OTP
|
|
646
|
-
just release_kit_publish # 先跑 release_kit_guard,再 npm publish -w duckfn-docs-kit
|
|
647
|
-
```
|
|
648
|
-
|
|
649
|
-
`release_kit_guard` 在上传之前拦四种情况:版本号还是开发版本(npm 会把预发布版本也挂到
|
|
650
|
-
`latest` 上)、工作区不干净、本地没有对应的 `docs-kit-v*` tag、该版本在 npm 上已存在
|
|
651
|
-
(同一版本不能覆盖,只能发新版本)。只想打包不上传,用 `just release_kit_publish_dry`。
|
|
652
|
-
|
|
653
|
-
发布后核对:
|
|
654
|
-
|
|
655
|
-
```bash
|
|
656
|
-
npm view duckfn-docs-kit version
|
|
657
|
-
```
|
|
658
|
-
|
|
659
|
-
### 4. 切到下一开发版本
|
|
660
|
-
|
|
661
|
-
```bash
|
|
662
|
-
just release_kit_dev 0.1.2-dev.0
|
|
663
|
-
```
|
|
664
|
-
|
|
665
|
-
只动同样的两个文件,不打 tag、不发布。开发版本不能被 `npm publish` 直接发出去 ——
|
|
666
|
-
`release_kit_guard` 会拒绝,这正是它存在的意义。
|
|
667
|
-
|
|
668
|
-
### 发布内容
|
|
669
|
-
|
|
670
|
-
包内容 = `files` 白名单 + npm 的固定规则,所以**不需要**在包的结构之外维护清单:
|
|
671
|
-
|
|
672
|
-
- `files: ["dist", "src", "AGENTS.md"]`:`dist` 是构建产物;`src` 必须带上(CSS 子路径
|
|
673
|
-
`duckfn-docs-kit/src/kit.css` 直接指向源文件);`AGENTS.md` 随包发布,下游读者因此能看到
|
|
674
|
-
本包的全部约定。
|
|
675
|
-
- `README.md`、`LICENSE`、`package.json` 由 npm 自动收录:本包目录下有自己的 `LICENSE`
|
|
676
|
-
(根目录那份不会被带进来),README 是 npm 包页的正文。
|
|
677
|
-
- `dist/` 在 `.gitignore` 里,但照样进包 —— `files` 白名单优先于 gitignore。`prepack` 脚本
|
|
678
|
-
会在 `npm pack` / `npm publish` 之前重跑 `npm run build`,所以发布用的产物总是当前源码
|
|
679
|
-
构建出来的,不依赖上一次留下的 `dist/`。
|
|
680
|
-
|
|
681
|
-
## 其它约定
|
|
682
|
-
|
|
683
|
-
- 文本文件一律 LF(仓库根 AGENTS.md 有替换命令)。
|
|
684
|
-
- 注释解释「为什么」,与 docs/ 现有风格一致;本包面向国际下游用户,注释用英文。
|
|
685
|
-
- `exports`/`files` 已按可发布形态维护,**不要改结构**:新增 / 移动模块只改
|
|
686
|
-
`vite.config.ts` 的 `entry`,通配映射自己会跟上。
|
|
687
|
-
- `README.md` 与 `docs/docs/docs-kit/**` 是两份正文,面向的读者不同:前者给在 npm 上直接看
|
|
688
|
-
这个包的人(英文),后者是本仓库文档站的用户指南。改公开 API 时两边都要看。
|
|
689
|
-
- 本包已发布到 npm(`duckfn-docs-kit`),`private` 与 `publishConfig` 的处理见上面的发版流程。
|
|
1
|
+
# duckfn-docs-kit — usage guide for agents and site authors
|
|
2
|
+
|
|
3
|
+
This is the **consumer-facing** companion to [`README.md`](./README.md): the README says what to
|
|
4
|
+
import and how to wire it into `docusaurus.config.ts`, this file states the **contracts and the
|
|
5
|
+
traps** — the things that are easy to get subtly wrong when you write a docs page or review one.
|
|
6
|
+
|
|
7
|
+
Read it before writing runnable SQL blocks or configuring preloads.
|
|
8
|
+
|
|
9
|
+
> Developing the kit itself? Its internal conventions (rendering contract, shadow-DOM rules,
|
|
10
|
+
> release flow) are in `CONVENTIONS.md` in the repository. That file is **not** published; this one
|
|
11
|
+
> is, so it is what a dependency reader sees.
|
|
12
|
+
|
|
13
|
+
## The three things that most often go wrong
|
|
14
|
+
|
|
15
|
+
1. **A runnable block shows only the result of its *last* statement.** Stacking several independent
|
|
16
|
+
examples in one block means the reader sees one result and the others silently vanish. One
|
|
17
|
+
example per block; several statements only for a preamble (`SET`, `CREATE`, …) that the last
|
|
18
|
+
query needs.
|
|
19
|
+
2. **`show: "html"` / `"iframe"` / `"svg"` need to be told which column holds the markup.** With more
|
|
20
|
+
than one result column you **must** name it: `"field": "<column>"`. Without it the renderer has
|
|
21
|
+
nothing to preview. `"tab_name"` names the column that labels each preview tab, and without it
|
|
22
|
+
tabs read `Row 1`, `Row 2`, …
|
|
23
|
+
3. **Only a block whose info string is JSON with `"type":"duckfn"` becomes runnable.** A bare
|
|
24
|
+
```` ```sql ```` block (or any other metastring) stays a plain, un-runnable code block — no Run
|
|
25
|
+
button, nothing executed.
|
|
26
|
+
|
|
27
|
+
## Install and wire it up
|
|
28
|
+
|
|
29
|
+
See [`README.md`](./README.md) — install, the three `docusaurus.config.ts` plugins, and the one CSS
|
|
30
|
+
import. Everything below assumes that is already done.
|
|
31
|
+
|
|
32
|
+
## Runnable SQL blocks
|
|
33
|
+
|
|
34
|
+
### The shape
|
|
35
|
+
|
|
36
|
+
A fenced block whose info string is a JSON config. The block itself becomes a CodeMirror editor
|
|
37
|
+
with a Run button; nothing executes until the reader clicks it.
|
|
38
|
+
|
|
39
|
+
````md
|
|
40
|
+
```sql {"type":"duckfn"}
|
|
41
|
+
SELECT 40 + 2 AS answer;
|
|
42
|
+
```
|
|
43
|
+
````
|
|
44
|
+
|
|
45
|
+
Works in `.md` and `.mdx` alike — the metastring is rewritten during the build, before either format
|
|
46
|
+
is compiled.
|
|
47
|
+
|
|
48
|
+
### Config reference
|
|
49
|
+
|
|
50
|
+
| Field | Meaning |
|
|
51
|
+
| --- | --- |
|
|
52
|
+
| `type` | `"duckfn"`. Required — this is what makes the block runnable. |
|
|
53
|
+
| `show` | `table` (default), `text`, `html`, `iframe`, `svg`. See *Result renderers*. |
|
|
54
|
+
| `field` | The column holding the markup, for `html` / `iframe` / `svg`. Required when the result has more than one column. |
|
|
55
|
+
| `tab_name` | The column whose value labels each preview tab. Defaults to `Row N`. |
|
|
56
|
+
| `option.width` · `option.height` | CSS lengths for the preview box (`"100%"`, `"640px"`). |
|
|
57
|
+
| `option.sandbox` | Sandbox tokens for the iframe, replacing the default `allow-scripts`. Widen deliberately. |
|
|
58
|
+
| `extensions` | Extra extension names to `LOAD` before this block runs, on top of the site's preloads. |
|
|
59
|
+
| `repository` | Where those extensions come from: `community`, `core`, or a repository URL. |
|
|
60
|
+
| `allowUnsignedExtensions` | Accept an unverifiable signature. Site-wide via the preload config, or per block; the first block to initialise the engine settles it. |
|
|
61
|
+
| `expect` | `ok` (default) or `error`. `error` declares "this block must fail" — see *Testing*. |
|
|
62
|
+
|
|
63
|
+
### Behaviour you have to design around
|
|
64
|
+
|
|
65
|
+
- **The result shown is the last statement's.** A block with `SET …; CREATE …; SELECT …` shows the
|
|
66
|
+
`SELECT`. A block with three independent `SELECT`s shows the third one only.
|
|
67
|
+
- **Default `show`:** a single column with a single row renders as `text`; anything else renders as
|
|
68
|
+
a `table`. Set `show` explicitly when the shape matters.
|
|
69
|
+
- **One DuckDB-Wasm instance per page, one connection per page.** Blocks on the same page share
|
|
70
|
+
state — a table or macro created in one block is visible to the next — and pages are isolated
|
|
71
|
+
from each other. Do not write a block that depends on another *page*.
|
|
72
|
+
- **The site's preloaded extensions are already loaded.** Call into them directly; do not add
|
|
73
|
+
`extensions` for the extension the site documents.
|
|
74
|
+
- **Errors are a result, not a broken block.** A failing statement renders its message in the result
|
|
75
|
+
area and keeps whatever the reader typed.
|
|
76
|
+
- **Every result has a tab strip** (even a plain table), and the fullscreen toggle lives at its right
|
|
77
|
+
end. Table results bring sorting, resizable rows/columns, a right-click menu and header drag.
|
|
78
|
+
|
|
79
|
+
### Result renderers
|
|
80
|
+
|
|
81
|
+
| `show` | What it renders | Needs |
|
|
82
|
+
| --- | --- | --- |
|
|
83
|
+
| `table` | The result grid. | — |
|
|
84
|
+
| `text` | A bare scalar, as one line. | A single column/row result. |
|
|
85
|
+
| `html` / `iframe` | One tab per row; the markup goes into a sandboxed `iframe` (`srcdoc`), with a trailing `Table` tab that is always last. | `field` (unless the result has exactly one column). `tab_name` to label tabs. |
|
|
86
|
+
| `svg` | The markup is spliced **into the page** (one tab per row, trailing `Table` tab). | Same as above. |
|
|
87
|
+
|
|
88
|
+
Two facts worth knowing before you pick one:
|
|
89
|
+
|
|
90
|
+
- `html` and `iframe` are the *same* renderer. The frame is sandboxed with `allow-scripts` and
|
|
91
|
+
**without** `allow-same-origin`, so a report's JavaScript runs while the frame keeps an opaque
|
|
92
|
+
origin — that is what makes charts work, and it is also why the parent page cannot read
|
|
93
|
+
`iframe.contentDocument` (it is `null` by design).
|
|
94
|
+
- `svg` shares the page, so anything that could execute or navigate — `script`, `foreignObject`,
|
|
95
|
+
`on*` handlers, `javascript:` links — is stripped before insertion.
|
|
96
|
+
|
|
97
|
+
### Examples
|
|
98
|
+
|
|
99
|
+
A table result, and a scalar that degrades to text:
|
|
100
|
+
|
|
101
|
+
````md
|
|
102
|
+
```sql {"type":"duckfn","show":"table"}
|
|
103
|
+
SELECT * FROM range(10) WHERE range > 5;
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
```sql {"type":"duckfn"}
|
|
107
|
+
SELECT 1;
|
|
108
|
+
```
|
|
109
|
+
````
|
|
110
|
+
|
|
111
|
+
An HTML (or iframe) report — note `field` and `tab_name`:
|
|
112
|
+
|
|
113
|
+
````md
|
|
114
|
+
```sql {"type":"duckfn","show":"iframe","field":"html","tab_name":"label","option":{"height":"170px"}}
|
|
115
|
+
SELECT * FROM (VALUES
|
|
116
|
+
('Bars', '<!doctype html><body><h4>Quarterly revenue</h4><svg viewBox="0 0 240 80">…</svg></body>')
|
|
117
|
+
) AS t(label, html);
|
|
118
|
+
```
|
|
119
|
+
````
|
|
120
|
+
|
|
121
|
+
An inline SVG, and an extension that is not preloaded:
|
|
122
|
+
|
|
123
|
+
````md
|
|
124
|
+
```sql {"type":"duckfn","show":"svg","option":{"height":"140px"}}
|
|
125
|
+
SELECT '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><circle cx="50" cy="50" r="40"/></svg>';
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
```sql {"type":"duckfn","show":"table","extensions":["inet"]}
|
|
129
|
+
SELECT '127.0.0.1'::INET::VARCHAR AS ip;
|
|
130
|
+
```
|
|
131
|
+
````
|
|
132
|
+
|
|
133
|
+
### Mistakes to check for when reviewing a page
|
|
134
|
+
|
|
135
|
+
- Several independent examples stacked in one block — only the last result is visible. **Split
|
|
136
|
+
them into one block per example.**
|
|
137
|
+
- `show: "html"` / `"iframe"` / `"svg"` with a multi-column result and no `field`.
|
|
138
|
+
- No `tab_name`, so the preview tabs read `Row 1`, `Row 2`, … instead of something meaningful.
|
|
139
|
+
- A bare ```` ```sql ```` block where a runnable one was intended (it renders as a plain listing).
|
|
140
|
+
- Declaring `extensions` for the extension the site already preloads.
|
|
141
|
+
- A block that depends on a table created on a *different* page.
|
|
142
|
+
|
|
143
|
+
## Preloading extensions (the `dfkExtensions` plugin)
|
|
144
|
+
|
|
145
|
+
The site declares an ordered preload list; the kit fetches the release assets at dev/build startup,
|
|
146
|
+
injects the list into every page and loads them — in order — while DuckDB initialises. Blocks can
|
|
147
|
+
then call into those extensions without declaring anything.
|
|
148
|
+
|
|
149
|
+
Three source kinds:
|
|
150
|
+
|
|
151
|
+
| Entry | Meaning |
|
|
152
|
+
| --- | --- |
|
|
153
|
+
| `'json'` | A name: `LOAD json` from the official repository. |
|
|
154
|
+
| `{name: 'h3', repository: 'community'}` | `community`, `core` or a repository URL. On wasm `INSTALL` only records *where* a later `LOAD` fetches from. |
|
|
155
|
+
| `{url: 'duckdb-extensions/x.duckdb_extension.wasm', release: {repository, asset}}` | The site serves the file itself; with `release`, the build fetches that asset from the repository's latest release (cached by sha256 in `<siteDir>/.cache/duckfn-docs-kit/`). |
|
|
156
|
+
|
|
157
|
+
Constraints that bite:
|
|
158
|
+
|
|
159
|
+
- **The file name is a contract**: the text before the first dot is the entry symbol DuckDB looks
|
|
160
|
+
up, so a release asset named `duckfn-wasm_eh.duckdb_extension.wasm` must be served as
|
|
161
|
+
`duckfn.duckdb_extension.wasm` (the `url` decides the file name).
|
|
162
|
+
- **Platforms must match**: a `wasm_eh` extension needs a runtime bundle on the `eh` platform. Pin
|
|
163
|
+
`@duckdb/duckdb-wasm` to the exact version whose bundled DuckDB is ABI-compatible with the
|
|
164
|
+
extension build.
|
|
165
|
+
- **Unsigned third-party extensions need `allowUnsignedExtensions: true`** (the WebAssembly
|
|
166
|
+
equivalent of `duckdb -unsigned`). Community extensions are signed and load without it.
|
|
167
|
+
|
|
168
|
+
## Testing the blocks (`duckfn-sql-verify`)
|
|
169
|
+
|
|
170
|
+
The kit ships the same runner the browser uses, so a site can execute every block it publishes:
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
duckfn-sql-verify --site . # or: npx duckfn-sql-verify --site .
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
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 ."`.
|
|
179
|
+
|
|
180
|
+
- **A block that demonstrates a failure must say so**: `{"type":"duckfn","expect":"error"}`. The
|
|
181
|
+
check is two-way — a block that declares `error` and starts succeeding is reported too — and a
|
|
182
|
+
`-- error:` comment in the SQL is *not* read; only the metadata counts.
|
|
183
|
+
- Useful options: `--extension <path|url>`, `--platform eh|mvp`, `--content <dir>` (repeatable),
|
|
184
|
+
`--timeout <ms>`, `--report <file>`, `--working-dir <dir>`, `--quiet`.
|
|
185
|
+
- The default extension is the single file under `static/duckdb-extensions/`.
|
|
186
|
+
|
|
187
|
+
## TOC collapse control (`dfkTocToggle()`)
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
plugins: [dfkTocToggle()],
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Adds a collapse button to the desktop table of contents and remembers the choice in
|
|
194
|
+
`localStorage` (`duckfn:toc-collapsed`). Custom labels come from the plugin's `labels` option,
|
|
195
|
+
keyed by a lower-cased `html-lang` prefix (`{en: {hide, show}, 'zh-hans': {…}}`).
|
|
196
|
+
|
|
197
|
+
## Version placeholder (`remarkVersionPlaceholder`)
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
remarkPlugins: [[remarkVersionPlaceholder, {version: DUCKFN_VERSION}]],
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Replaces `{{DUCKFN_VERSION}}` inside `text`, `inlineCode` and `code` nodes, so a release updates one
|
|
204
|
+
file instead of every page. It only touches that exact placeholder — anything else is left alone.
|
|
205
|
+
|
|
206
|
+
## Home-page components
|
|
207
|
+
|
|
208
|
+
`<dfk-hero>`, `<dfk-features>`, `<dfk-next-steps>` (and `<dfk-sql>`) are registered by the barrel
|
|
209
|
+
import (`duckfn-docs-kit`). The home components are **setter-driven and do not reflect attributes**:
|
|
210
|
+
give them named setters (`setTitle`, `setTagline`, `setFeatures`, …), not markup content. They
|
|
211
|
+
render into shadow roots, inherit `--duckfn-*` / `--ifm-*` CSS variables from the page, and are
|
|
212
|
+
safe to call before the element is connected.
|
|
213
|
+
|
|
214
|
+
## Troubleshooting
|
|
215
|
+
|
|
216
|
+
| Symptom | Likely cause |
|
|
217
|
+
| --- | --- |
|
|
218
|
+
| A code block has no Run button | Its info string is not JSON with `"type":"duckfn"`. |
|
|
219
|
+
| The block runs but nothing appears in the preview | `show: "html"` / `"iframe"` / `"svg"` without `field` on a multi-column result. |
|
|
220
|
+
| The preview tabs are labelled `Row 1`, `Row 2`, … | No `tab_name`. |
|
|
221
|
+
| Clicking Run shows an error like `Table with name … does not exist` | The block depends on something created on another page, or on a statement that is no longer the last one in its block. |
|
|
222
|
+
| `LOAD` fails with a signature error | `allowUnsignedExtensions: true` missing for a third-party asset. |
|
|
223
|
+
| A preloaded extension fails to load | The served file name's pre-dot part does not match the extension's entry symbol, or the platform does not match the runtime bundle. |
|
|
224
|
+
| Only the last of several examples shows a result | That is the contract — split the block. |
|