@2digits/oxlint-plugin

中文文档
0.5.0发布于 2026/11/06MITTypeScriptES Module构建来源已验证近 30 天下载 5390

Custom Oxlint rules for 2digits projects.

原始使用文档
文档目录11 节
  1. Rules
  2. Alchemy Domain models
  3. Endpoint header schema keys
  4. Tests
  5. Duration constants
  6. Function-valued Config defaults
  7. anti-slop Effect rules
  8. Automatic fixes
  9. Statement spacing (opt-in)
  10. Other fixes
  11. Adding a rule

@2digits/oxlint-plugin

Custom Oxlint JavaScript plugin rules used by @2digits/oxlint-config.

The recommended set is enabled by default by @2digits/oxlint-config. The opt-in prefer-effect-filesystem and prefer-effect-path rules are excluded because the configured @effect/tsgo/nodeBuiltinImport diagnostic already enforces those imports more broadly. To register the plugin directly:

import { defineConfig } from 'oxlint';

import { recommendedRules } from '@2digits/oxlint-plugin';

export default defineConfig({
  jsPlugins: [{ name: '2digits', specifier: import.meta.resolve('@2digits/oxlint-plugin') }],
  rules: recommendedRules,
});

Rules

The package exports all rules as rules, their names as RuleName, and the default error-level configuration as recommendedRules.

  • Copied and adapted Effect practices: avoid-data-tagged-error, ban-error-string, effect-promise-vs-trypromise, no-logging-in-catch, and throw-in-effect-gen.
  • Syntax-safe Effect diagnostics proposed in open Effect-TS/tsgo issues: the remaining cors-*, dual-*, no-*, prefer-*, preserve-*, and require-* Effect rules.
  • Effect v4 API guidance for Array, DateTime, Duration, Encoding, FileSystem, Filter, Headers, Match, Path, and Url, plus interruptible Effect.tryPromise thunks.
  • Effect and Alchemy import policy: namespace imports from PascalCase submodule entrypoints, canonical Effect aliases, and no root-barrel module imports. @effect/vitest and lowercase unstable barrels are intentionally exempt.
  • Alchemy v2 practices: every other alchemy-* rule.
  • Alchemy Domain modeling rules from the @alchemy.run/domain agent skill: the alchemy-domain-* rules. See Alchemy Domain models.
  • Schema compiler hoisting from t3code: no-inline-schema-compile reports Schema.decode*, Schema.encode*, Schema.is, and Schema.asserts compilers that are built and invoked inside a function body when their schema could be hoisted to module scope.
  • Log results: no-effect-as-on-log reports Effect.as, Effect.asSome and Effect.asVoid applied to an Effect.log* or Console.* log effect (.pipe(Effect.as(x)), pipe(log, Effect.as(x)) or data-first). Write Effect.gen that yield*s the log and returns the value instead.
  • Schema chain style: consistent-schema-pipe applies Schema modifiers and data-last steps with .pipe(...) (Schema.String.pipe(Schema.optionalKey)) and calls Schema wrappers such as Schema.Array(...) directly. See ADR 0001.

See each rule's meta.docs.url for its upstream rule, issue, or framework documentation. Copied-code attribution is in NOTICE.

Alchemy Domain models

The alchemy-domain-* rules check the conventions that keep an @alchemy.run/domain model correct. They only look at calls on a @alchemy.run/domain import, so other code is unaffected.

  • alchemy-domain-pure-decide reports time (Date(), Date.now(), new Date(), Clock.*, DateTime.now, Jwt.verify), randomness (Math.random(), crypto.randomUUID(), Random.*, SecureRandom.*, and the effect/crypto and effect/jose APIs that generate keys, nonces or signatures, such as Aead.Aead, Jwk.generateKeyPair and Jws.sign) and I/O (fetch, Effect.promise, Effect.tryPromise, Jwks.Jwks) inside an aggregate's decide handlers. Deterministic crypto (Digest, Mac, Kdf, KeyFormat, Jws.verify) is allowed. Read time from decide's now argument and call external systems from a policy or saga.
  • alchemy-domain-total-handlers reports throw and Option/Result getOrThrow* inside an aggregate's evolve and reply or a view's evolve. Keep any state the event can't follow instead. In decide it reports them too: return a rejection instead.
  • alchemy-domain-require-view-version requires version on every Domain.View, so a change to what the view computes sits next to the number that must be bumped for the deploy to rebuild it. version: 0 keeps the current default.
  • alchemy-domain-block-file-name requires one block per file, named after its kind (Account.aggregate.ts, WithdrawMoney.command.ts, FraudCheck.external.ts, CurrentActor.context.ts). Events and rejections may share the file of the command, external system or stream that uses them; in their own .event.ts or .rejection.ts file, one per file. Test and story files are skipped.
  • alchemy-domain-pure-imports restricts block files (*.aggregate.ts, *.command.ts, *.middleware.ts, ...) that import @alchemy.run/domain to relative imports, effect and @alchemy.run/domain.

None of them autofix. The rules are syntax-only, so a helper called from decide or evolve is not followed.

Endpoint header schema keys

no-uppercase-http-api-header reports each static ASCII-uppercase key in a direct Effect v4 endpoint headers fields object. Incoming headers are lowercase, and schema decoding looks up keys exactly:

import * as Schema from 'effect/Schema';
import { HttpApiEndpoint } from 'effect/unstable/httpapi';

// Diagnosed: the required header is missing to the decoder even when it arrived.
HttpApiEndpoint.get('me', '/me', { headers: { 'X-Api-Key': Schema.String } });
// Valid: the schema matches the lowercase incoming key.
HttpApiEndpoint.get('me', '/me', { headers: { 'x-api-key': Schema.String } });

The rule checks options at argument 2 of get, post, put, patch, delete, head, and options. It resolves lexical import bindings from effect/unstable/httpapi and its HttpApiEndpoint submodule, including named aliases and namespace imports, and ignores shadowed bindings. Referenced schemas, computed keys, spreads, duplicate keys, accessors, and ambiguous options objects are outside this initial slice. Curried make(method), v3 setHeaders, native Request/Response/fetch headers, outgoing setters, and native Headers.get/has are not checked. Only ASCII uppercase letters are diagnosed; the rule does not normalize non-ASCII names.

There is no autofix: renaming a header schema key changes typed contracts and may collide with a lowercase key. Review the schema and its consumers together. Runtime controls use the workspace's Effect version with @effect/vitest. This is preventive guidance: the historical agency scan found zero bad sites and seven valid endpoint schemas, not seven defects or a runtime outage.

Tests

Use @effect/vitest for tests exercising Effect code. Run effects with it.effect (or it.live when live runtime services are required), not manual Effect.run* calls. RuleTester and synchronous CLI tests use regular it.

Duration constants

prefer-effect-duration wraps number literals and literal arithmetic passed as Duration.Input to Effect v4 APIs (Effect.sleep, Effect.timeout, Schedule.spaced, Schedule.exponential, Stream.debounce, TestClock.adjust, the duration option of Effect.timeoutOrElse, and others) with Duration.millis.

prefer-effect-duration-constants reports const declarations that hold a duration as a plain number, in files that import Effect:

import * as Effect from 'effect/Effect';

const TIMEOUT_MS = 30_000; // Declare it as Duration.seconds(30)
const CACHE_TTL_SECONDS = 3600; // Declare it as Duration.hours(1)
const ONE_DAY = 24 * 60 * 60 * 1000; // Declare it as Duration.days(1)
const wait = 250; // Declare it as Duration.millis(250)

Effect.sleep(wait);

A constant is reported when its name ends in a plural or abbreviated unit (_MS, Seconds, _MINS, _DAYS) or a duration word (TIMEOUT, DELAY, INTERVAL, TTL, ONE_DAY, maxAge, expiresIn), when it is hand-written time arithmetic, or when it is passed to an Effect duration API. Only the last word of the name counts, so RETRY_DELAY_FACTOR is not a duration. Names containing per, at, epoch, or timestamp (MS_PER_SECOND, REQUESTS_PER_SECOND, CREATED_AT_MS), zero or negative sentinels, and constants annotated with a Duration type are skipped unless they are passed to a duration API. The suggested constructor uses the unit from the name; Effect's millisecond interpretation applies only to constants passed to duration APIs. Without a known unit, the message stays generic.

There is no autofix: the constant changes from number to Duration. Convert at non-Effect boundaries with Duration.toMillis or Duration.toSeconds, read configurable durations with Config.Duration, and use Schema.DurationFromMillis for schema fields.

Function-valued Config defaults

no-function-config-default reports inline zero-parameter functions passed to Effect's eager Config.withDefault API, except when the receiving config is locally proven to hold a function:

import * as Config from 'effect/Config';

Config.withDefault(
  Config.succeed(() => 1),
  () => 2,
);
const config = Config.succeed(() => {
  return 1;
});

config.pipe(Config.withDefault(() => 2));

The proof follows same-file const receiver aliases with no reassignment and recognizes runtime Config imports from effect and effect/Config, including aliases and lexical shadowing. The succeed argument must be a function literal; a nonfunction object containing a function does not qualify. In .pipe(...), only the first operation can use the proof. Imported, mutable, or otherwise composed configs remain unknown and retain the diagnostic. This is syntax proof, not general inference of Config<A>. Parameterized function defaults remain excluded. There is no autofix: eagerly evaluating or unwrapping a thunk could change side effects or an intentional function value.

anti-slop Effect rules

Adapted from dmmulroy/anti-slop:

  • no-manual-effect-error-tag: prefer tagged handlers for _tag comparisons/switches in broad Effect catches.
  • no-manual-tag-comparison: prefer Match/Predicate for literal _tag comparisons and switches outside those catches.
  • no-manual-tagged-construction: prefer existing constructors over literal string _tag objects; direct Match.when/Match.not pattern objects are exempt.
  • no-service-constructor-imports: reject relative named make[A-Z] imports outside .test/.spec files.
  • prefer-effect-match: also reports chained literal ternaries comparing the same source expression, while retaining the existing Effect-file switch diagnostic.

All are included in recommendedRules, without autofixes. Like upstream, these are syntactic policies: they do not require Effect imports, infer tagged types, or prove that constructors exist. Catch and pattern exemptions recognize the literal Effect/Match identifiers, not aliases or shadowing. Constructor imports are matched by imported name (including aliases), not return type; package/absolute imports are excluded. The switch rules can overlap with the existing prefer-effect-match diagnostic.

Automatic fixes

Statement spacing (opt-in)

padding-line-between-statements adapts anti-slop's vendored ESLint Stylistic rule. It is excluded from recommendedRules: enable it with an explicit policy. Later matching entries override earlier entries.

export default {
  rules: {
    '2digits/padding-line-between-statements': [
      'error',
      { blankLine: 'always', prev: '*', next: 'return' },
      { blankLine: 'always', prev: ['const', 'let'], next: '*' },
      { blankLine: 'any', prev: ['const', 'let'], next: ['const', 'let'] },
    ],
  },
};

Options accept always, never, or any, statement names (including TypeScript declarations and singleline-*/multiline-* variants), arrays, or { selector: 'ExpressionStatement', lineMode: 'multiline' } AST selectors. Selectors must match statement nodes (for example ExpressionStatement[expression.type="CallExpression"]), not their nested expressions. See the upstream statement names. Autofixes preserve trailing comments and avoid collapsing multiple blank-line gaps separated by comments. The exported PaddingLineOption type describes each configuration entry. No options means no diagnostics.

Other fixes

Run vpr lint:oxlint:fix from the repository root to apply fixes from consistent-schema-pipe, prefer-effect-duration, no-empty-effect-callback, no-effect-alchemy-barrel-imports, prefer-effect-alchemy-namespace-imports, alchemy-no-v1-worker-properties, and alchemy-no-deprecated-docker-constraints. Fixes skip ambiguous bindings, conflicting properties, and unsupported import references.

Adding a rule

Oxlint's JavaScript plugin API is currently alpha and does not expose type information. Keep rules syntax-safe and leave semantic Effect checks to @effect/tsgo / @effect/language-service.

Oxfmt formats imports but does not enforce package-specific import architecture. The Effect language service's namespaceImportPackages and importAliases settings guide generated auto-imports only, so the plugin enforces the same policy for handwritten imports.

  1. Add one rule file under src/rules/alchemy or src/rules/effect with defineRule through defineSyntaxRule or defineEffectRule.
  2. Register it in src/rules/index.ts. src/index.ts automatically includes it in recommendedRules.
  3. Add a matching test file under test/rules with valid and invalid RuleTester cases.
  4. Run vpr test and vpr build in this package, then vpr check from the repository root.

defineEffectRule resolves namespace and named imports from effect, @effect/*, and alchemy/* entrypoints, so rules should match canonical API paths instead of hard-coding local import names.

版本与发布记录

@2digits/oxlint-plugin 共发布 27 个版本,首次发布于 2026/09/03;过去 12 个月发布了 27 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.5.0latest2026/11/06今天
0.4.52026/11/041 天前
0.4.42026/11/023 天前
0.4.32026/11/015 天前
0.4.22026/10/288 天前
0.4.12026/10/279 天前

其他发布通道:temp → 0.0.0-tegami-trusted-publish-setup。

关于 @2digits/oxlint-plugin 的常见问题

@2digits/oxlint-plugin 是什么?

Custom Oxlint rules for 2digits projects.

如何安装 @2digits/oxlint-plugin?

在项目目录运行 npm install @2digits/oxlint-plugin,或使用 pnpm add / yarn add / bun add。

@2digits/oxlint-plugin 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@2digits/oxlint-plugin 支持 ESM 还是 CommonJS?

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

@2digits/oxlint-plugin 可以商用吗?

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

@2digits/oxlint-plugin 的下载量有多少?

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

@2digits/oxlint-plugin 还在维护吗?

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