feat: adding JSDoc

This commit is contained in:
2026-05-03 10:12:42 +08:00
parent 81bca6aa80
commit 5fe3ba771e
15 changed files with 389 additions and 10 deletions
+237
View File
@@ -0,0 +1,237 @@
---
name: jsdoc
description: Guidelines for writing minimal, high-quality JSDoc comments in TypeScript.
---
# JSDoc Skill
This skill provides focused guidelines for writing JSDoc comments consistently across the codebase.
## When to Use
- Documenting exported type properties and configuration options
- Adding context that TypeScript types don't convey on their own
- Providing usage examples for non-obvious or multi-variant APIs
- Writing inline documentation for generated docs
## What It Does
- Defines `@example` format conventions (label + inline backtick or fenced block)
- Ensures every exported type and property has meaningful documentation
- Avoids redundant tags that duplicate TypeScript type information
- Ensures consistent documentation style across all packages
---
## `@example` Format
### Short one-liner — label on the `@example` line, code as inline backtick on the next line
```typescript
/**
* @example Required parameter
* `name: Type`
*
* @example Optional parameter
* `name?: Type`
*/
```
### Multi-line — fenced code block immediately after `@example`
````typescript
/**
* @example
* ```ts
* const result = buildParams(node, {
* paramsType: 'inline',
* })
* ```
*/
````
### Multiple variants — use multiple `@example` blocks
```typescript
/**
* @example Object mode
* `{ id, data, params }: { id: string; data: Data; params?: QueryParams }`
*
* @example Inline mode
* `id: string, data: Data, params?: QueryParams`
*/
```
### Rules
| Rule | ✅ Correct | ❌ Incorrect |
| ----------------------- | ----------------------------------- | -------------------------------------------- |
| Label + inline code | `@example Required\n\`name: Type\`` | `@example \`name: Type\`` (code on tag line) |
| Multi-line code | Fenced ` ```ts ``` ` block | Bare code lines without a fence |
| Short examples | Inline backtick | Triple-backtick fence (too heavy) |
| One concern per example | Separate `@example` blocks | One example covering all cases |
---
## Tags
### Use Frequently
| Tag | Purpose | Notes |
| ------------- | ------------------ | ------------------------------------------------------ |
| `@default` | Default value | Only when default is non-obvious; omit for `undefined` |
| `@example` | Usage example | Prefer for complex or multi-variant APIs |
| `@note` | Important caveat | Version info, breaking changes |
| `@deprecated` | Mark as deprecated | Include migration path |
### Use Sparingly
| Tag | Purpose |
| ----------- | ----------------------- |
| `@see` | Reference external docs |
| `@internal` | Internal API |
| `@beta` | Experimental |
### Avoid (TypeScript Provides)
-`@param` — use TypeScript parameter types
-`@returns` — use TypeScript return type
-`@type` — use TypeScript type annotation
-`@typedef` — use `type` or `interface`
-`@default undefined` — optional (`?`) already implies this
---
## Documentation Patterns
### Simple property — always multi-line
```typescript
/**
* Output directory for generated files.
*/
outDir?: string
```
> ❌ Never use single-line `/** description */` — always expand to multi-line.
### Property with non-obvious default
```typescript
/**
* Maximum number of concurrent callbacks during traversal.
* Higher values overlap I/O-bound work; lower values reduce memory pressure.
*
* @default 30
*/
concurrency?: number
```
> Do **not** add `@default false` or `@default undefined` when the TypeScript type already makes the default obvious.
### Enum / union with options
```typescript
/**
* How path parameters are emitted in the function signature.
* - `'object'` groups them as a single destructured parameter
* - `'inline'` spreads them as individual parameters
* - `'inlineSpread'` emits a single rest parameter
*/
pathParamsType: 'object' | 'inline' | 'inlineSpread'
```
### Property with example
```typescript
/**
* Applies a uniform transformation to every resolved type string.
* Use this for framework-level type wrappers.
*
* @example Wrap every type in a reactive container
* `typeWrapper: (t) => \`Reactive<${t}>\``
*/
typeWrapper?: (type: string) => string
```
### Nested properties — every field gets its own multi-line JSDoc
```typescript
names?: {
/**
* Name for the request body parameter.
* @default 'data'
*/
data?: string
/**
* Name for the query parameters group parameter.
* @default 'params'
*/
params?: string
/**
* Name for the header parameters group parameter.
* @default 'headers'
*/
headers?: string
}
```
### Function documentation
Only add JSDoc when it adds value beyond the signature:
```typescript
// ✅ No JSDoc needed — signature is self-explanatory
function camelCase(str: string): string { ... }
// ✅ JSDoc adds value — explains behaviour and non-obvious edge cases
/**
* Returns `true` when the schema resolves to a plain string output.
*
* - `string`, `uuid`, `email`, `url`, `datetime` are always plain strings.
* - `date` and `time` are plain strings when their `representation` is `'string'`.
*
* @example UUID resolves to a plain string
* `isStringType(uuidSchema) // true`
*
* @example Date with date representation is not a plain string
* `isStringType(dateSchema) // false`
*/
function isStringType(node: SchemaNode): boolean { ... }
```
---
## Guidelines
**✅ DO:**
- Document **what** the property does, not its TypeScript type
- Give every exported type, property, and function a JSDoc comment
- Always use multi-line JSDoc blocks — never single-line `/** ... */`
- Use concrete, full-sentence descriptions — not "Enum schema." or "Boolean value."
- Include `@default` only when the default is non-obvious (not `undefined`, not `false`)
- Use multiple `@example` blocks to show different variants or modes
- Keep `@example` labels short and descriptive
**❌ DON'T:**
- Write single-line `/** description */` — always use multi-line
- Write `@default undefined` — optional `?` already implies this
- Put code directly on the `@example` line: `@example \`foo: string\`` → move code to next line
- Use `@param` or `@returns` tags
- Over-document trivial, self-explanatory properties
---
## Tag Order
For consistency, use this order within a JSDoc block:
1. Description (required)
2. Bullet-list of variants or behaviours (if applicable)
3. `@default` (if non-obvious)
4. `@example` (one or more)
5. `@note` (if needed)
6. `@deprecated` (if applicable)
7. `@see` (if providing references)
+8
View File
@@ -113,6 +113,14 @@ packages/result # output
- 不要讓 agent 修改 `prototype/``frontend/``backend/` 原始輸入資料夾;生成與執行 artifact 應限制在 `output/``.ht/`
- 保持 MVP 邊界,避免加泛用抽象或一開始支援所有 monorepo 形態。
## JSDoc 原則
- 依專案的 `jsdoc` skill 撰寫 JSDoc;這裡只補充 HTML Transform 專案特有規則。
- JavaScript 的 JSDoc 優先補在 exported API、stage 入口、跨模組 helper,或描述非顯而易見契約與限制的位置;不為測試檔或區域性小函式刻意補註解。
- JSDoc 使用繁體中文,並把說明重點放在 artifact、pipeline stage、MVP 限制、cache/placeholder 行為、輸入輸出契約等專案脈絡。
- 不新增 `jsdoc` 產生 HTML 文件的設定、script 或 build step,除非使用者明確要求。
- 補 JSDoc 不應順手 refactor、改行為、加抽象或擴大 scope。
## 驗證
完成任何改動後至少執行:
+6 -1
View File
@@ -1,4 +1,9 @@
/**
* 提供 `ht.config.*` 使用的設定包裝器,讓使用者能保留日後型別提示的入口。
*
* @param {object} config 使用者提供的 HTML Transform 設定。
* @returns {object} 原樣回傳的設定物件。
*/
export function defineConfig(config) {
return config
}
+12 -1
View File
@@ -6,10 +6,22 @@ const commands = {
gemini: ['gemini']
}
/**
* 將設定中的 agent 名稱解析成可執行命令。
*
* @param {string} agent `ht.config.*` 中的 agent 名稱。
* @returns {string[]} 命令與預設參數。
*/
export function resolveAgentCommand(agent) {
return commands[agent] ?? [agent]
}
/**
* 檢查命令是否能從目前 shell PATH 找到。
*
* @param {string} command 要檢查的可執行檔名稱。
* @returns {Promise<boolean>} 找得到命令時為 `true`。
*/
export async function commandExists(command) {
return new Promise((resolve) => {
const child = spawn('which', [command], { stdio: 'ignore' })
@@ -17,4 +29,3 @@ export async function commandExists(command) {
child.on('error', () => resolve(false))
})
}
+6 -1
View File
@@ -36,6 +36,12 @@ const defaultConfig = {
}
}
/**
* 從專案根目錄載入 `ht.config.*`,並補齊 CLI pipeline 需要的預設路徑與 stage 設定。
*
* @param {string} cwd 要搜尋設定檔的工作目錄。
* @returns {Promise<object>} 正規化後的設定物件,包含解析後的絕對路徑。
*/
export async function loadConfig(cwd = process.cwd()) {
const candidates = ['ht.config.mjs', 'ht.config.js', 'ht.config.json', 'ht.config.ts']
for (const filename of candidates) {
@@ -88,4 +94,3 @@ function normalizeConfig(config, cwd) {
htDir: resolve(cwd, '.ht')
}
}
+45 -1
View File
@@ -2,24 +2,56 @@ import { mkdir, readFile, readdir, stat, writeFile } from 'node:fs/promises'
import { createHash } from 'node:crypto'
import { dirname, extname, join, relative } from 'node:path'
/**
* 確保資料夾存在,供 stage 在寫入 `.ht/` 與 output artifact 前使用。
*
* @param {string} path 要建立的資料夾路徑。
* @returns {Promise<void>}
*/
export async function ensureDir(path) {
await mkdir(path, { recursive: true })
}
/**
* 以穩定格式寫入 JSON artifact。
*
* @param {string} path 輸出檔案路徑。
* @param {unknown} data 要序列化的資料。
* @returns {Promise<void>}
*/
export async function writeJson(path, data) {
await ensureDir(dirname(path))
await writeFile(path, `${JSON.stringify(data, null, 2)}\n`)
}
/**
* 讀取 JSON artifact。
*
* @param {string} path JSON 檔案路徑。
* @returns {Promise<unknown>} 解析後的 JSON 值。
*/
export async function readJson(path) {
return JSON.parse(await readFile(path, 'utf8'))
}
/**
* 計算檔案內容雜湊,供 prototype cache 判斷是否命中。
*
* @param {string} path 檔案路徑。
* @returns {Promise<string>} 十六進位 SHA-256 digest。
*/
export async function sha256File(path) {
const content = await readFile(path)
return createHash('sha256').update(content).digest('hex')
}
/**
* 遞迴列出資料夾內檔案,缺少資料夾時回傳空陣列以符合 MVP doctor/scan 行為。
*
* @param {string} root 搜尋根目錄。
* @param {string[]} extensions 要保留的副檔名;空陣列代表不過濾。
* @returns {Promise<string[]>} 依字典序排序的檔案路徑。
*/
export async function listFiles(root, extensions = []) {
const output = []
async function walk(dir) {
@@ -39,6 +71,12 @@ export async function listFiles(root, extensions = []) {
return output.sort()
}
/**
* 檢查路徑是否存在。
*
* @param {string} path 要檢查的路徑。
* @returns {Promise<boolean>} 路徑存在時為 `true`。
*/
export async function exists(path) {
try {
await stat(path)
@@ -48,7 +86,13 @@ export async function exists(path) {
}
}
/**
* 將輸入檔路徑轉成 `.ht/` artifact 使用的穩定相對名稱。
*
* @param {string} root 輸入根目錄。
* @param {string} file 輸入檔案路徑。
* @returns {string} 去除 `.html` 副檔名並標準化分隔符的 artifact 名稱。
*/
export function artifactPath(root, file) {
return relative(root, file).replaceAll('\\', '/').replace(/\.html$/i, '')
}
+24
View File
@@ -1,3 +1,9 @@
/**
* 從 HTML source 萃取 deterministic summary,作為 MVP 階段替代 browser snapshot 的 evidence。
*
* @param {string} html HTML source。
* @returns {object} 頁面標題、文字、表單欄位與互動元素摘要。
*/
export function summarizeHtml(html) {
const title = matchText(html, /<title[^>]*>([\s\S]*?)<\/title>/i)
const headings = collect(html, /<h([1-6])[^>]*>([\s\S]*?)<\/h\1>/gi).map((item) => cleanText(item[2]))
@@ -15,6 +21,12 @@ export function summarizeHtml(html) {
return { title, headings, buttons, labels, inputs, textSamples }
}
/**
* 依 semantic landmark 將 HTML source 拆成可追蹤的頁面區塊。
*
* @param {string} html HTML source。
* @returns {Array<object>} 區塊清單;找不到 landmark 時會回傳單一 `main` 區塊。
*/
export function extractRegions(html) {
const landmarks = [
['header', /<header\b[^>]*>([\s\S]*?)<\/header>/gi, 'banner'],
@@ -50,6 +62,12 @@ export function extractRegions(html) {
return regions
}
/**
* 取出 MVP pipeline 可用於比對與生成的可見文字樣本。
*
* @param {string} html HTML source。
* @returns {string[]} 去除 script、style 與標籤後的文字 token。
*/
export function visibleText(html) {
const withoutScripts = html
.replace(/<script\b[\s\S]*?<\/script>/gi, ' ')
@@ -66,6 +84,12 @@ export function visibleText(html) {
.slice(0, 50)
}
/**
* 將 deterministic region evidence 轉成目前 MVP 使用的 Vuetify-oriented RegionSpec。
*
* @param {object} region `extractRegions` 產生的區塊。
* @returns {object} 後續 plan/run stage 使用的 RegionSpec。
*/
export function inferRegionSpec(region) {
const summary = summarizeHtml(region.html)
const hasForm = /<(form|input|select|textarea)\b/i.test(region.html)
+12 -1
View File
@@ -3,6 +3,12 @@ import { join } from 'node:path'
import { exists } from './files.js'
import { commandExists } from './agent.js'
/**
* 依專案規則解析 Playwright CLI,不依賴使用者全域環境作為第一選擇。
*
* @param {string} cwd 專案工作目錄。
* @returns {Promise<object | null>} 可執行命令、參數與來源;找不到時為 `null`。
*/
export async function resolvePlaywrightCli(cwd = process.cwd()) {
const localBin = join(cwd, 'node_modules/.bin/playwright-cli')
if (await exists(localBin)) return { command: localBin, args: [], source: 'local' }
@@ -11,6 +17,12 @@ export async function resolvePlaywrightCli(cwd = process.cwd()) {
return null
}
/**
* 確認解析到的 Playwright CLI 能實際執行。
*
* @param {string} cwd 專案工作目錄。
* @returns {Promise<boolean>} `playwright-cli --version` 成功時為 `true`。
*/
export async function playwrightCliAvailable(cwd = process.cwd()) {
const resolved = await resolvePlaywrightCli(cwd)
if (!resolved) return false
@@ -20,4 +32,3 @@ export async function playwrightCliAvailable(cwd = process.cwd()) {
child.on('error', () => resolve(false))
})
}
+5 -1
View File
@@ -3,6 +3,11 @@ import { join } from 'node:path'
import { loadConfig } from '../lib/config.js'
import { ensureDir, listFiles, readJson } from '../lib/files.js'
/**
* 執行 Stage 4:依現有 LayoutSpec 產生 deterministic placeholder diff report。
*
* @returns {Promise<void>}
*/
export async function diff() {
const config = await loadConfig()
const specs = await listFiles(join(config.htDir, 'spec'), ['.json'])
@@ -44,4 +49,3 @@ function renderDiffReport(config, spec) {
</html>
`
}
+5
View File
@@ -4,6 +4,11 @@ import { commandExists, resolveAgentCommand } from '../lib/agent.js'
import { exists } from '../lib/files.js'
import { playwrightCliAvailable, resolvePlaywrightCli } from '../lib/playwright-cli.js'
/**
* 檢查本機執行 HTML Transform pipeline 所需的資料夾與命令。
*
* @returns {Promise<void>}
*/
export async function doctor() {
const config = await loadConfig()
const agentCommand = resolveAgentCommand(config.agent)[0]
+7 -1
View File
@@ -4,6 +4,13 @@ import { basename, join } from 'node:path'
import { loadConfig } from '../lib/config.js'
import { listFiles, readJson, writeJson } from '../lib/files.js'
/**
* 執行 Stage 2:依 LayoutSpec 建立 tasklist、component mapping 與專案/API contract artifacts。
*
* @param {object} options 執行選項。
* @param {boolean} [options.assumeYes] 跳過互動 review 提示。
* @returns {Promise<void>}
*/
export async function plan(options = {}) {
const config = await loadConfig()
const specs = await listFiles(join(config.htDir, 'spec'), ['.json'])
@@ -117,4 +124,3 @@ async function writeMarkdown(path, content) {
function renderPreview(taskList) {
return taskList.tasks.map((task) => `${task.id} -> ${task.targetFile}`).join('\n')
}
+7 -1
View File
@@ -3,6 +3,13 @@ import { basename, dirname, join } from 'node:path'
import { loadConfig } from '../lib/config.js'
import { ensureDir, exists, listFiles, readJson, writeJson } from '../lib/files.js'
/**
* 執行 Stage 3:根據 TaskList 產生 output 內的 Vue/Vuetify 檔案與 run metadata。
*
* @param {object} options 執行選項。
* @param {boolean} [options.retryFailed] 僅重跑狀態為 `error` 的 task。
* @returns {Promise<void>}
*/
export async function runTasks(options = {}) {
const config = await loadConfig()
const taskListPath = join(config.htDir, 'plan/tasklist.json')
@@ -94,4 +101,3 @@ function topoSort(tasks) {
function escapeHtml(value) {
return String(value).replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('"', '&quot;')
}
+5
View File
@@ -10,6 +10,11 @@ const viewports = [
{ name: 'mobile', width: 375, height: 812 }
]
/**
* 執行 Stage 1:掃描 prototype HTML,產生 deterministic evidence 與 LayoutSpec artifacts。
*
* @returns {Promise<void>}
*/
export async function scan() {
const config = await loadConfig()
const htmlFiles = await listFiles(config.prototypeDir, ['.html'])
+5 -1
View File
@@ -2,6 +2,11 @@ import { join } from 'node:path'
import { loadConfig } from '../lib/config.js'
import { exists, readJson } from '../lib/files.js'
/**
* 顯示目前 TaskList 中各 task 狀態的數量。
*
* @returns {Promise<void>}
*/
export async function status() {
const config = await loadConfig()
const path = join(config.htDir, 'plan/tasklist.json')
@@ -15,4 +20,3 @@ export async function status() {
console.log(`${state}: ${counts[state]?.length ?? 0}`)
}
}
+5 -1
View File
@@ -3,6 +3,11 @@ import { join } from 'node:path'
import { loadConfig } from '../lib/config.js'
import { exists, listFiles, readJson, writeJson } from '../lib/files.js'
/**
* 執行 Stage 5:執行設定中的 quality commands,並寫出 MVP verification report。
*
* @returns {Promise<void>}
*/
export async function verify() {
const config = await loadConfig()
const quality = {}
@@ -43,4 +48,3 @@ async function runDomChecks(config) {
const missing = taskList.tasks.filter((task) => !outputFiles.some((file) => file.endsWith(task.targetFile)))
return { status: missing.length === 0 ? 'passed' : 'failed', missingTargets: missing.map((task) => task.targetFile) }
}