@fitzzero/quickdraw-lint

中文文档
5.1.4发布于 2026/10/26MITES Module零依赖构建来源已验证近 30 天下载 2668

oxlint plugin, base config and baseline command for quickdraw apps

原始使用文档
文档目录5 节
  1. Rules
  2. Which files a rule checks
  3. Baselines
  4. From the 4.1 rules
  5. Developing

@fitzzero/quickdraw-lint

The quickdraw oxlint plugin and the configs every quickdraw 5.0 app extends (design: docs/rfcs/0003-v5.md, section 14). The rules catch the mistakes that matter with the 5.0 API: writes subscribers never see, frames sent by hand, reads that grow with the table, bypassing the typed client, and 4.x calls that no longer exist. Every rule is tested, and every rule can be adopted without fixing old code first (baselines, below).

bun add -d @fitzzero/quickdraw-lint oxlint
// .oxlintrc.json in your app
{
  "extends": [
    "./node_modules/@fitzzero/quickdraw-lint/oxlint.base.jsonc",
    // or, in an app built from the quickdraw template, the template config
    // instead: it extends the base and adds the design-system rules
    // "./node_modules/@fitzzero/quickdraw-lint/oxlint.template.jsonc",
  ],
  // plugins, ignorePatterns, env, globals and settings are not inherited
  "plugins": ["typescript", "import", "react", "nextjs", "jsx_a11y"],
  "ignorePatterns": ["**/dist/**", "**/node_modules/**"],
}

The configs load the plugin through a path relative to themselves, so an app needs no jsPlugins entry of its own. The comment at the top of oxlint.base.jsonc lists what an extending config inherits. Its path overrides (explicit types in packages/shared and packages/db, the web app's relaxed budgets) are **/-prefixed, so they apply whether lint runs from the app's root or from each package directory. The plugin runs on oxlint 1.52 or later.

Rules

Every rule is syntactic: oxlint's JS plugins see one file's syntax and no types. Where a rule has to guess, it prefers missing a case to reporting correct code; each rule's file says what it leaves alone.

RuleReportsChecks by default
no-untracked-writeimporting the untracked Prisma client (prisma, PrismaClient from the app's db package, @prisma/client or the generated client) and writing through a client named prisma (a parameter, variable, destructured binding or class member typed Tracked<...> or TrackedTransaction<...>, imported from @fitzzero/quickdraw-core or @fitzzero/quickdraw-core/prisma under any name, is the tracked client whatever its name; an app's own Tracked is not)services, jobs, routes
no-foreign-writea write to a model that the enclosing defineService neither owns (model) nor lists in writeseverywhere
no-nested-writea nested relation write (data: { labels: { create } }) through the tracked client (db, tx, or a binding or class member typed with quickdraw's Tracked<...>): only the parent row is trackedeverywhere
no-raw-sql-write$executeRaw*, and $queryRaw* running a write, in a function that does not record the rows with ctx.touch(...) (or qd.collections.reset(...))services, jobs, routes
no-manual-emit.emit() on the Socket.IO server or a socket (io.to(room).emit), and qd: strings, the framework's event and room namesservices, jobs, routes
no-inline-auth-guardif (!ctx.principal) throw ... and its variants inside a handler (declare access instead), and a kind check there that throws, if (ctx.principal.kind !== "user") throw ... (declare kinds instead)everywhere
no-plain-error-refusala throw of a built-in error (new Error(...), TypeError(...), with or without new) in a function given as auth.authenticate to createServer or createHttpRouter, as loadPrincipal to socketAuth, or as principal to createMcpRegistry (written there, or a function or const of the same file), which the server takes for its own failure, so the client retries the credentials for ever (an MCP agent is never told its token is refused); not one inside a nested function, a try with a catch or a catch clauseeverywhere but tests
no-unbounded-readfindMany without take (a read by ids, id equal, { in } or { equals }, is bounded; { not }, { notIn } and { gt } are not)services
no-db-call-in-loopa database call per item of a for/for...of/for...in loop or .map callback, awaited or sent to Promise.all (not while, createMany, in: its own set, tx writes)services, jobs, routes
no-emit-in-loopctx.rooms.emit, emitToUser or a stream's push once per item to the same room, user or stream scope (fan-out is fine; batch with pushMany)services, jobs, routes
no-load-then-filterfindMany whose rows are only .filtered or .find-ed by a condition a where could expressservices, jobs, routes
no-prisma-in-routesmodel calls on the Prisma client in route handlersroutes
no-cross-service-internal-importsimporting another service directory's files other than its indexservices
no-await-void-mutateawait mutation.mutate(...): mutate returns nothing; use mutateAsyncclient code
no-untyped-clienta TanStack Query hook whose queryFn/mutationFn calls quickdraw by hand, or whose queryKey is a quickdraw key (hooks the client lacks may use a member's key and call)client code
no-manual-refetcha quickdraw query's refetch() right after (or in the callbacks of) a quickdraw mutation, and invalidateQueries/refetchQueries/resetQueries on a quickdraw keyclient code
no-raw-socketsocket.emit, socket.on, socket.off and the other raw Socket.IO calls: on a socket (or x.socket), on whatever socket.io-client's io(), connect() or Manager made under any name (followed through variables and this fields), and with a qd: event name on any receiverclient code
no-v4-apievery 4.x API 5.0 removed or moved; each message names the replacementeverywhere
prefer-kita method written by hand that a kit implements (get, list, create, search, share, adminList, or getTask, listTasks, createTask, updateTask, deleteTask for model "task"; remove only on a membership model or beside another sharing method) in a defineService whose model is a string (or a const of one) and whose methods spread no kit (a spread variable or call counts as one), or that duplicates what a kit spread beside it serves (crud.handlers' access literal names, search for search.handlers, every admin method for admin.handlers); a // quickdraw: hand-written because <reason> comment above it keeps it quieteverywhere but tests
no-todo-schematodoSchema(), the placeholder schema the 4.x migration (@fitzzero/quickdraw-codemod) leaves where a method had none: it validates nothing; as a query() or mutation() input without keys, it also hides the input's keys from defineService's id-reach (rowless) check, and the message says soeverywhere
require-describea contract member without a describe: the contract (defineContract), each query and mutation, collection, stream, channel and event, written as an object literal (a spread or a definition built elsewhere is left alone), and a static describe under minWords words (default 3); the MCP bridge describes a method's tool with it and quickdraw-docs leads each section with iteverywhere but tests
no-unused-baselinean allowance in the baseline file that no violation uses any more (below)baselined files
no-raw-button-strings, no-raw-tooltip-strings, no-raw-typography-stringsraw strings in MUI Button, Typography and Tooltip titles (oxlint.template.jsonc)*.tsx, *.jsx

The base config turns on all but the last three, at error, except no-unused-baseline, no-todo-schema, prefer-kit, require-describe and no-plain-error-refusal, which warn. A warning never fails an upgrade's lint; once an app has no reports left from one, it can set the rule to "error" in its own config ("quickdraw/require-describe": "error", say) so new code cannot add any. oxlint.template.jsonc extends the base and adds the last three.

Which files a rule checks

A rule about one layer checks only that layer's files. The defaults follow the template's layout. The rules scoped to services, jobs and routes, no-raw-socket, prefer-kit and require-describe skip tests (__tests__/, test/, tests/, *.test.*, *.spec.*), which seed, inspect and probe on purpose; the other rules check tests too.

LayerGlobs
services**/services/**
jobs**/jobs/**
routes**/routes/**, **/routes.*
client code**/*.tsx, **/*.jsx, **/apps/web/**

Client code is also any file that imports @tanstack/react-query, socket.io-client or @fitzzero/quickdraw-core/client, so a hook in a .ts file is checked wherever oxlint runs from, the app root or apps/web alike; for the client rules the globs add files on top of those.

Every file-scoped rule takes files and ignore globs, matched against the path relative to where oxlint runs (keep them **/-prefixed):

{
  "rules": {
    "quickdraw/no-unbounded-read": ["error", { "files": ["**/server/services/**"] }],
  },
}

Other options: clients (the names the database client goes by: ["db", "tx"] for the write rules, plus prisma for the read rules) on the rules that look at database calls; modules (specifiers of the untracked client's modules) on no-untracked-write; emitters on no-manual-emit; sockets, allowedEvents and allowedPrefixes on no-raw-socket; clients (the typed client's names, ["qd"]) on no-untyped-client and no-manual-refetch; minWords (the fewest words a static describe may have, default 3) on require-describe; shared and allow ({ "<service>": ["<service>/<file>"] }) on no-cross-service-internal-imports. Each rule's options are in its schema, so a misspelled option fails oxlint at startup.

The write rules (no-untracked-write, no-nested-write, no-foreign-write) also take a client declared with quickdraw's Tracked<...> or TrackedTransaction<...> as the tracked client, whatever its name: a parameter or variable, a destructured binding whose pattern's type gives it one (a type literal, or this file's interface or type alias: { db }: { db: Tracked<Db> }), or this.<member> its class declares so (a property, or a constructor's parameter property). The type counts when the file imports it from @fitzzero/quickdraw-core or @fitzzero/quickdraw-core/prisma, under any name or through a namespace import, or through this file's type alias of it. A type of the app's own named Tracked, or one imported from another module (a re-export included), is not read: a rule sees one file, and only quickdraw's type holds the client to the tracked one.

Baselines

An app adopts the base config without fixing every existing violation first, quickdraw's rules and oxlint's own alike:

bunx quickdraw-lint baseline -c .oxlintrc.json   # writes .quickdraw-lint-baseline.json
// .oxlintrc.json: every rule reads the file
{
  "settings": { "quickdraw": { "baseline": ".quickdraw-lint-baseline.json" } },
}
// package.json: lint through quickdraw-lint check (per package: -c ../../.oxlintrc.json src)
{ "scripts": { "lint": "quickdraw-lint check -c .oxlintrc.json ." } }

quickdraw-lint baseline runs oxlint with your config (-c to pick one; paths after the options, the current directory by default) and records a fingerprint for every violation it reports: the rule, the file, and a hash of the violating line's trimmed text. A violation is then reported only when its fingerprint is not recorded, or occurs more often than recorded. So fixing an old violation and adding a new one in the same file reports the new one, at its line, while edits elsewhere in the file (which move lines without changing them) disturb nothing. A violation that a disable comment covers is not recorded and uses no allowance, and a file oxlint cannot parse is never recorded: no baseline holds a syntax error. Commit the file.

The quickdraw rules apply the file themselves, so a plain oxlint run (an editor's too) leaves out their recorded violations. oxlint's own rules (no-unused-vars, no-shadow, ...) run natively, where a JS plugin cannot reach them: quickdraw-lint check runs oxlint and leaves out their recorded violations as well, so it is the command an app lints with. It passes every option it does not know (-c, --fix, paths) to oxlint, prints what is left as file:line:column: message [Severity/rule] (--format json for oxlint's JSON report, filtered), takes --quiet, --deny-warnings and --max-warnings <n> as oxlint does, and exits 1 when an error is left.

When a recorded violation is fixed, its allowance is left unused, and no-unused-baseline warns at the top of the file (quickdraw-lint check reports oxlint's own rules' unused allowances the same way); run the baseline command again so the file shrinks and nothing new can take the allowance's place. A single rule takes the file as an option instead of the setting: ["error", { "baseline": ".quickdraw-lint-baseline.json" }]. A relative path is looked up from each linted file's directory upwards, so one file at the repository root also serves lint runs started from package directories. A quickdraw rule is keyed by its name, any other rule by the code oxlint reports it under:

{
  "version": 2,
  "files": {
    "apps/api/src/services/task.ts": {
      "eslint(no-unused-vars)": { "9a0364b9e99bb480": 1 },
      "no-unbounded-read": { "5d41402abc4b2a76": 1, "7d793037a0760186": 2 }
    }
  }
}

A version 1 file (counts per file, from an earlier 5.0 prerelease) is refused with a message saying to write it again with the command.

From the 4.1 rules

The 4.1 rules were moved here unchanged before 5.0; 5.0 replaces them. A config that still names a removed rule fails to load, so drop these names from your overrides:

4.1 rule5.0
no-cross-service-mutationsno-foreign-write: owners come from defineService's model and writes, so there is no allowlist to keep
no-direct-prisma-mutationsno-untracked-write (there is no this.create to prefer: write through db)
no-manual-collection-eventsno-manual-emit (collections derive their deltas from tracked writes)
no-raw-socket-emit, no-raw-socket-onno-raw-socket, with no app-specific default exemptions
no-raw-service-room-stringremoved: entity and collection rooms belong to the framework (no-manual-emit reports qd: names)
require-zod-schemaremoved: every contract method declares input and output
no-service-method-record, no-unsafe-payload-castremoved: there are no service classes or defineMethod generics; inputs are typed by the contract
no-raw-button-strings, no-raw-tooltip-strings, no-raw-typography-stringsunchanged, moved to oxlint.template.jsonc

Developing

bun run --filter @fitzzero/quickdraw-lint test runs every rule's cases under oxlint's own RuleTester (oxlint/plugins-dev: oxlint's parser and plugin runtime, in vitest), and plugin/test/oxlint.test.mjs runs the oxlint CLI with the shipped configs: every rule must report its example there (with the template config alone too), the core package's fixture apps must pass the rules that judge service definitions, and the baseline command must round-trip. plugin/test/check.test.mjs runs the configs' path overrides from the app root and from package directories, and quickdraw-lint check. The plugin's .mjs files and bin/ ship as they are; there is no build.

版本与发布记录

@fitzzero/quickdraw-lint 共发布 15 个版本,首次发布于 2026/10/05;过去 12 个月发布了 15 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
5.1.4latest2026/10/2619 天前
5.1.32026/10/2322 天前
5.1.22026/10/121 个月前
5.1.12026/10/111 个月前
5.1.02026/10/111 个月前
5.0.12026/10/101 个月前

其他发布通道:next → 5.0.0-rc.7。

关于 @fitzzero/quickdraw-lint 的常见问题

@fitzzero/quickdraw-lint 是什么?

oxlint plugin, base config and baseline command for quickdraw apps

如何安装 @fitzzero/quickdraw-lint?

在项目目录运行 npm install @fitzzero/quickdraw-lint,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:oxlint(^1.52.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。它还提供命令行工具 quickdraw-lint,可以用 npx quickdraw-lint 直接运行。

@fitzzero/quickdraw-lint 支持 TypeScript 吗?

该包的 package.json 没有声明类型入口。需要类型提示时,可以在 npm 上查找社区维护的 @types/fitzzero__quickdraw-lint 是否存在。

@fitzzero/quickdraw-lint 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@fitzzero/quickdraw-lint 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=24。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@fitzzero/quickdraw-lint 可以商用吗?

@fitzzero/quickdraw-lint 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@fitzzero/quickdraw-lint 的下载量有多少?

根据 npm 官方下载统计,@fitzzero/quickdraw-lint 在 2026/10/15 – 2026/11/13 这 30 天内被下载 2,668 次,日均约 89 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@fitzzero/quickdraw-lint 还在维护吗?

最新版本 5.1.4 发布于 2026/10/26(19 天前);至今共发布 15 个版本,其中过去 12 个月发布了 15 个;首次发布于 2026/10/05。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。