@doubleloop-io/oxlint-plugin-effect 0.1.0 → 0.1.2
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/CHANGELOG.md +22 -1
- package/README.md +17 -200
- package/dist/rules/no-inline-yield-star.js +63 -14
- package/docs/user-guide/README.md +24 -0
- package/docs/user-guide/alongside-effect-tsgo.md +23 -0
- package/docs/user-guide/common-limits.md +10 -0
- package/docs/user-guide/configuration.md +43 -0
- package/docs/user-guide/default-configuration.md +88 -0
- package/docs/user-guide/installation.md +12 -0
- package/docs/user-guide/rules/adapter-import-boundary.md +27 -0
- package/docs/user-guide/rules/all-errors-handled-at-boundary.md +30 -0
- package/docs/user-guide/rules/anti-slop-no-manual-effect-error-tag.md +26 -0
- package/docs/user-guide/rules/anti-slop-no-service-constructor-imports.md +32 -0
- package/docs/user-guide/rules/anti-slop-prefer-effect-match.md +30 -0
- package/docs/user-guide/rules/exported-schema-identifier.md +26 -0
- package/docs/user-guide/rules/no-asymmetric-match-handlers.md +30 -0
- package/docs/user-guide/rules/no-effect-promise.md +26 -0
- package/docs/user-guide/rules/no-effect-return-type.md +29 -0
- package/docs/user-guide/rules/no-inline-schema-codec.md +26 -0
- package/docs/user-guide/rules/no-inline-yield-star.md +29 -0
- package/docs/user-guide/rules/no-manual-discriminant-comparison.md +36 -0
- package/docs/user-guide/rules/no-manual-discriminant-construction.md +32 -0
- package/docs/user-guide/rules/no-nested-match.md +44 -0
- package/docs/user-guide/rules/no-parameterless-catch-all.md +28 -0
- package/docs/user-guide/rules/no-provide-on-definition.md +27 -0
- package/docs/user-guide/rules/no-throw-or-primitive-failure.md +34 -0
- package/docs/user-guide/rules/no-transport-types-in-generator.md +41 -0
- package/docs/user-guide/rules/no-yielded-match.md +35 -0
- package/docs/user-guide/rules/prefer-effect-fn.md +31 -0
- package/docs/user-guide/rules/prefer-effect-vitest.md +30 -0
- package/docs/user-guide/rules/provide-named-layer.md +27 -0
- package/docs/user-guide/rules/redacted-secret-names.md +31 -0
- package/docs/user-guide/rules/run-only-in-runner.md +32 -0
- package/docs/user-guide/rules/service-built-with-of.md +36 -0
- package/docs/user-guide/rules/service-key-matches-name.md +29 -0
- package/docs/user-guide/rules/service-port-shape-only.md +27 -0
- package/docs/user-guide/rules/single-style-per-function.md +36 -0
- package/docs/user-guide/rules/split-success-path-from-error-handling.md +37 -0
- package/docs/user-guide/rules/try-object-form.md +27 -0
- package/docs/user-guide/rules/untraced-recursion.md +31 -0
- package/docs/user-guide/rules.md +26 -0
- package/docs/user-guide/shared-settings.md +65 -0
- package/docs/user-guide/troubleshooting.md +30 -0
- package/docs/user-guide/versioning.md +8 -0
- package/package.json +14 -9
- package/RULES.md +0 -1056
package/CHANGELOG.md
CHANGED
|
@@ -1,10 +1,31 @@
|
|
|
1
1
|
# @doubleloop-io/oxlint-plugin-effect
|
|
2
2
|
|
|
3
|
+
## 0.1.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#42](https://github.com/doubleloop-io/oxlint-plugin-effect/pull/42) [`00de7c6`](https://github.com/doubleloop-io/oxlint-plugin-effect/commit/00de7c6780be261181bd49e34c183f020733754f) Thanks [@iacoware](https://github.com/iacoware)! - `no-inline-yield-star` adds a hint when the `yield*` sits inside a larger conditional branch, where binding it first would run it unconditionally: extract the effect into a named function and transform its result inside it (`Effect.map`), so the branch is a single `yield*`.
|
|
8
|
+
|
|
9
|
+
- [#41](https://github.com/doubleloop-io/oxlint-plugin-effect/pull/41) [`a71cd0f`](https://github.com/doubleloop-io/oxlint-plugin-effect/commit/a71cd0fbcc82be7e9961550ec4924ae483b64016) Thanks [@iacoware](https://github.com/iacoware)! - `no-inline-yield-star` autofix names a `const` whose name is taken after the object it is read from (`reader.rows` → `readerRows`, `Effect.scope` → `effectScope`) before falling back to a numeric suffix.
|
|
10
|
+
|
|
11
|
+
- [#39](https://github.com/doubleloop-io/oxlint-plugin-effect/pull/39) [`469f03d`](https://github.com/doubleloop-io/oxlint-plugin-effect/commit/469f03d1eb92126a918d8cf456e8483b05c4fdd9) Thanks [@d4v1d82](https://github.com/d4v1d82)! - Prevent `no-inline-yield-star` autofixes from generating duplicate `const` bindings when multiple statements in the same generator extract the same base name.
|
|
12
|
+
|
|
13
|
+
## 0.1.1
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- [`2c67281`](https://github.com/doubleloop-io/oxlint-plugin-effect/commit/2c672814db1d169752d04295374001b935b785f5) Thanks [@iacoware](https://github.com/iacoware)! - The documentation has a website, now the package homepage: https://doubleloop-io.github.io/oxlint-plugin-effect/
|
|
18
|
+
|
|
19
|
+
- The rule catalog moves from `RULES.md`, no longer in the package, to one file per rule in `docs/user-guide/rules/`; the guide ships in the package.
|
|
20
|
+
- The README becomes the essential guide and links to the website.
|
|
21
|
+
- New Troubleshooting page.
|
|
22
|
+
- Improve rules documentation.
|
|
23
|
+
|
|
3
24
|
## 0.1.0
|
|
4
25
|
|
|
5
26
|
First stable release.
|
|
6
27
|
|
|
7
|
-
- 30 rules for Effect code, grouped by topic in [`
|
|
28
|
+
- 30 rules for Effect code, grouped by topic in [`docs/user-guide/rules.md`](docs/user-guide/rules.md): error channel, tagged values, services and Layers, runtime and entry point, design and boundaries, effectful functions, code shape, Schema, config and secrets. Three are absorbed from [`dmmulroy/anti-slop`](https://github.com/dmmulroy/anti-slop).
|
|
8
29
|
- An `all` preset that enables every rule at `warn`, for `oxlint.config.ts` and `.oxlintrc.json`.
|
|
9
30
|
- Shared settings under `settings["dl-effect"]`: `entryFiles`, `testFiles`, `discriminants`.
|
|
10
31
|
- Written for Effect 4; rules also recognize the Effect 3 name of the same API.
|
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# @doubleloop-io/oxlint-plugin-effect
|
|
2
2
|
|
|
3
|
-
An oxlint plugin that enforces a catalog of patterns for Effect 4 code.
|
|
3
|
+
An [oxlint](https://github.com/oxc-project/oxc) plugin that enforces a catalog of patterns for [Effect](https://github.com/Effect-TS/effect/) 4 code.
|
|
4
4
|
|
|
5
|
-
The rules are written and tested against Effect 4. Where an Effect 3 API has the same intent, the rules recognize its v3 name too: each rule's Trigger
|
|
5
|
+
The rules are written and tested against Effect 4. Where an Effect 3 API has the same intent, the rules recognize its v3 name too: each rule's Trigger names them. The [Rules overview](https://doubleloop-io.github.io/oxlint-plugin-effect/rules-overview/) lists every rule.
|
|
6
6
|
|
|
7
|
-
It complements the type-aware rules of [`@effect/tsgo`](
|
|
7
|
+
It complements the type-aware rules of [`@effect/tsgo`](https://doubleloop-io.github.io/oxlint-plugin-effect/alongside-effect-tsgo/): it does not need them, and it checks what they do not.
|
|
8
8
|
|
|
9
9
|
## Installation
|
|
10
10
|
|
|
@@ -12,9 +12,9 @@ It complements the type-aware rules of [`@effect/tsgo`](#alongside-effecttsgo):
|
|
|
12
12
|
pnpm add -D @doubleloop-io/oxlint-plugin-effect oxlint
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
Requires Node `^22.20.0 || >=24.8.0` and oxlint `^1.82.0`.
|
|
15
|
+
Requires Node `^22.20.0 || >=24.8.0` and oxlint `^1.82.0`. What a release may change and which oxlint versions are supported: [Versioning](https://doubleloop-io.github.io/oxlint-plugin-effect/versioning/).
|
|
16
16
|
|
|
17
|
-
##
|
|
17
|
+
## Usage
|
|
18
18
|
|
|
19
19
|
Extend the `all` preset. Prefer `oxlint.config.ts`, where `extends` takes config objects and the preset is imported by package name:
|
|
20
20
|
|
|
@@ -33,203 +33,20 @@ With `.oxlintrc.json`, `extends` takes file paths only, so the preset is reached
|
|
|
33
33
|
}
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
`all` enables every rule at `warn`, so adopting it never breaks a build: look at the warnings first, then raise to `error` the rules you want to enforce.
|
|
36
|
+
`all` enables every rule at `warn`, so adopting it never breaks a build: look at the warnings first, then raise to `error` the rules you want to enforce.
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
**Set `entryFiles` before reading the warnings** on a layout that is not a Node CLI, such as Next.js or a monorepo. Its default, `**/main.ts`, `**/cli.ts`, `**/bin/**`, decides which files may run Effects and import adapters, so on another layout it turns correct code into warnings. How to set it: [Shared settings](https://doubleloop-io.github.io/oxlint-plugin-effect/shared-settings/#setting-a-value).
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
{
|
|
42
|
-
"jsPlugins": ["@doubleloop-io/oxlint-plugin-effect"],
|
|
43
|
-
"rules": {
|
|
44
|
-
"dl-effect/no-effect-promise": "error",
|
|
45
|
-
"dl-effect/try-object-form": "error"
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
### Configuration options
|
|
51
|
-
|
|
52
|
-
Three settings describe your project, not a rule, so they are declared once under `settings["dl-effect"]` and every rule that uses them reads them from there. Their defaults fit a Node CLI; on another layout (Next.js, a monorepo) set them before reading the warnings.
|
|
53
|
-
|
|
54
|
-
- `entryFiles`: the files allowed to run Effects and import adapters. Default `**/main.ts`, `**/cli.ts`, `**/bin/**`.
|
|
55
|
-
- `testFiles`: the test files, exempt from the rules that tests legitimately break and the only files `prefer-effect-vitest` checks. Default `**/*.test.*`, `**/*.spec.*`, `**/__tests__/**`, `**/test/**`, `**/tests/**`.
|
|
56
|
-
- `discriminants`: the fields that discriminate a closed union. Default `_tag`; add the ones your unions use, such as `type` or `kind`.
|
|
57
|
-
|
|
58
|
-
The rules that use each one are listed in [Shared settings](RULES.md#shared-settings). Set one only where its default does not fit: it **replaces** the default instead of extending it. An unknown key or a value that is not an array of strings stops the lint with an error, so a typo is never silently ignored.
|
|
59
|
-
|
|
60
|
-
```ts
|
|
61
|
-
import dlEffectAll from "@doubleloop-io/oxlint-plugin-effect/presets/all"
|
|
62
|
-
import { defineConfig } from "oxlint"
|
|
63
|
-
|
|
64
|
-
export default defineConfig({
|
|
65
|
-
extends: [dlEffectAll],
|
|
66
|
-
settings: {
|
|
67
|
-
"dl-effect": { entryFiles: ["apps/web/src/app/**/route.ts", "apps/web/src/runtime.ts"] },
|
|
68
|
-
},
|
|
69
|
-
})
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
```json
|
|
73
|
-
{
|
|
74
|
-
"extends": ["./node_modules/@doubleloop-io/oxlint-plugin-effect/dist/presets/all.json"],
|
|
75
|
-
"settings": {
|
|
76
|
-
"dl-effect": { "entryFiles": ["apps/web/src/app/**/route.ts", "apps/web/src/runtime.ts"] }
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
The package exports the defaults as `defaultEntryFiles`, `defaultTestFiles` and `defaultDiscriminants`: spread them to extend a default (`testFiles: [...defaultTestFiles, "e2e/**"]`).
|
|
82
|
-
|
|
83
|
-
Settings belong to the config that lints: oxlint does not inherit them from a preset in `extends`, and rejects them in `overrides`. For a value that differs by group of files, use `overrides` on the rules instead.
|
|
84
|
-
|
|
85
|
-
The other options and every default are in [Default configuration](#default-configuration).
|
|
86
|
-
|
|
87
|
-
### Alongside `@effect/tsgo`
|
|
88
|
-
|
|
89
|
-
[`@effect/tsgo`](https://github.com/Effect-TS/tsgo) is the official Effect language service on top of the TypeScript Go compiler; its `effecttsgo` oxlint presets add more than 100 type-aware rules. This plugin does not need it, and its rules target what those presets do not check, so the two complement each other. To use both, follow its [Oxlint Setup guide](https://github.com/Effect-TS/tsgo/blob/main/docs/README.md) and put its preset in `extends` **before** `all`:
|
|
90
|
-
|
|
91
|
-
```ts
|
|
92
|
-
import dlEffectAll from "@doubleloop-io/oxlint-plugin-effect/presets/all"
|
|
93
|
-
import { recommended as effectTsgoRecommended } from "@effect/tsgo/oxlint-presets"
|
|
94
|
-
import { defineConfig } from "oxlint"
|
|
95
|
-
|
|
96
|
-
export default defineConfig({ extends: [effectTsgoRecommended, dlEffectAll] })
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
```json
|
|
100
|
-
{
|
|
101
|
-
"extends": [
|
|
102
|
-
"./node_modules/@effect/tsgo/oxlint-presets/recommended.json",
|
|
103
|
-
"./node_modules/@doubleloop-io/oxlint-plugin-effect/dist/presets/all.json"
|
|
104
|
-
]
|
|
105
|
-
}
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
## Rules
|
|
109
|
-
|
|
110
|
-
What each rule reports and why is in [`RULES.md`](RULES.md). Report false positives and missed cases in the [GitHub issues](https://github.com/doubleloop-io/oxlint-plugin-effect/issues).
|
|
111
|
-
|
|
112
|
-
| Topic | Rules |
|
|
113
|
-
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
114
|
-
| Error channel | [`no-throw-or-primitive-failure`](RULES.md#no-throw-or-primitive-failure), [`try-object-form`](RULES.md#try-object-form), [`no-effect-promise`](RULES.md#no-effect-promise), [`no-parameterless-catch-all`](RULES.md#no-parameterless-catch-all), [`split-success-path-from-error-handling`](RULES.md#split-success-path-from-error-handling), [`anti-slop-no-manual-effect-error-tag`](RULES.md#anti-slop-no-manual-effect-error-tag) |
|
|
115
|
-
| Tagged values | [`no-manual-discriminant-comparison`](RULES.md#no-manual-discriminant-comparison), [`no-manual-discriminant-construction`](RULES.md#no-manual-discriminant-construction) |
|
|
116
|
-
| Services & Layers | [`service-port-shape-only`](RULES.md#service-port-shape-only), [`service-key-matches-name`](RULES.md#service-key-matches-name), [`service-built-with-of`](RULES.md#service-built-with-of), [`provide-named-layer`](RULES.md#provide-named-layer), [`no-provide-on-definition`](RULES.md#no-provide-on-definition), [`anti-slop-no-service-constructor-imports`](RULES.md#anti-slop-no-service-constructor-imports) |
|
|
117
|
-
| Runtime & entrypoint | [`run-only-in-runner`](RULES.md#run-only-in-runner), [`prefer-effect-vitest`](RULES.md#prefer-effect-vitest), [`all-errors-handled-at-boundary`](RULES.md#all-errors-handled-at-boundary) |
|
|
118
|
-
| Design & boundaries | [`adapter-import-boundary`](RULES.md#adapter-import-boundary), [`no-transport-types-in-generator`](RULES.md#no-transport-types-in-generator) |
|
|
119
|
-
| Effectful functions | [`prefer-effect-fn`](RULES.md#prefer-effect-fn), [`untraced-recursion`](RULES.md#untraced-recursion), [`single-style-per-function`](RULES.md#single-style-per-function) |
|
|
120
|
-
| Code shape | [`no-nested-match`](RULES.md#no-nested-match), [`no-yielded-match`](RULES.md#no-yielded-match), [`no-inline-yield-star`](RULES.md#no-inline-yield-star), [`no-effect-return-type`](RULES.md#no-effect-return-type), [`no-asymmetric-match-handlers`](RULES.md#no-asymmetric-match-handlers), [`anti-slop-prefer-effect-match`](RULES.md#anti-slop-prefer-effect-match) |
|
|
121
|
-
| Schema | [`exported-schema-identifier`](RULES.md#exported-schema-identifier), [`no-inline-schema-codec`](RULES.md#no-inline-schema-codec) |
|
|
122
|
-
| Config & secrets | [`redacted-secret-names`](RULES.md#redacted-secret-names) |
|
|
123
|
-
|
|
124
|
-
### Absorbed from anti-slop
|
|
125
|
-
|
|
126
|
-
Some rules come from the Effect rules of [`dmmulroy/anti-slop`](https://github.com/dmmulroy/anti-slop), MIT-licensed: see [`LICENSE.anti-slop`](LICENSE.anti-slop).
|
|
127
|
-
|
|
128
|
-
- Imported: [`anti-slop-no-manual-effect-error-tag`](RULES.md#anti-slop-no-manual-effect-error-tag), [`anti-slop-no-service-constructor-imports`](RULES.md#anti-slop-no-service-constructor-imports), [`anti-slop-prefer-effect-match`](RULES.md#anti-slop-prefer-effect-match).
|
|
129
|
-
- Superseded: `no-manual-tag-comparison` by [`no-manual-discriminant-comparison`](RULES.md#no-manual-discriminant-comparison), `no-manual-tagged-construction` by [`no-manual-discriminant-construction`](RULES.md#no-manual-discriminant-construction).
|
|
130
|
-
|
|
131
|
-
## Default configuration
|
|
132
|
-
|
|
133
|
-
`all` is equivalent to the config below, with every setting and option spelled out at its default. Both **replace** the defaults instead of extending them: see [Shared settings](RULES.md#shared-settings).
|
|
134
|
-
|
|
135
|
-
```ts
|
|
136
|
-
import { defineConfig } from "oxlint"
|
|
137
|
-
|
|
138
|
-
// Constructors that must not appear inside an Effect generator: the conversion to the
|
|
139
|
-
// transport belongs in the second argument of Effect.fn. Add your own transport DTOs.
|
|
140
|
-
const transportTypes = ["Response", "NextResponse"]
|
|
141
|
-
|
|
142
|
-
// Schema fields holding a secret: a field matches an entry or ends with it capitalized
|
|
143
|
-
// (`password` matches `dbPassword`). No generic `token`: `nextPageToken` is not a secret.
|
|
144
|
-
const secretFields = ["password", "secret", "apiKey", "accessToken", "refreshToken", "idToken"]
|
|
145
|
-
|
|
146
|
-
export default defineConfig({
|
|
147
|
-
jsPlugins: ["@doubleloop-io/oxlint-plugin-effect"],
|
|
148
|
-
settings: {
|
|
149
|
-
"dl-effect": {
|
|
150
|
-
// Files allowed to run Effects (Effect.run*, runMain, ManagedRuntime.make) and to
|
|
151
|
-
// import adapters. `index.ts` is left out on purpose: as a barrel it would let
|
|
152
|
-
// adapters leak through re-exports.
|
|
153
|
-
entryFiles: ["**/main.ts", "**/cli.ts", "**/bin/**"],
|
|
154
|
-
// Files exempt from the rules that tests legitimately break. Helper directories such
|
|
155
|
-
// as `src/testing/` are support code, not tests, so they are not matched.
|
|
156
|
-
testFiles: [
|
|
157
|
-
"**/*.test.*",
|
|
158
|
-
"**/*.spec.*",
|
|
159
|
-
"**/__tests__/**",
|
|
160
|
-
"**/test/**",
|
|
161
|
-
"**/tests/**",
|
|
162
|
-
],
|
|
163
|
-
// Fields that discriminate a closed union, compared or switched on only through an
|
|
164
|
-
// exhaustive match and set only by a constructor. Add the ones your unions use, such
|
|
165
|
-
// as `type` or `kind`.
|
|
166
|
-
discriminants: ["_tag"],
|
|
167
|
-
},
|
|
168
|
-
},
|
|
169
|
-
// Every rule at `warn`, to measure noise without blocking.
|
|
170
|
-
rules: {
|
|
171
|
-
// Error channel
|
|
172
|
-
"dl-effect/no-throw-or-primitive-failure": "warn",
|
|
173
|
-
"dl-effect/try-object-form": "warn",
|
|
174
|
-
"dl-effect/no-effect-promise": "warn",
|
|
175
|
-
"dl-effect/no-parameterless-catch-all": "warn",
|
|
176
|
-
"dl-effect/split-success-path-from-error-handling": "warn",
|
|
177
|
-
"dl-effect/anti-slop-no-manual-effect-error-tag": "warn",
|
|
178
|
-
// Tagged values
|
|
179
|
-
"dl-effect/no-manual-discriminant-comparison": "warn",
|
|
180
|
-
"dl-effect/no-manual-discriminant-construction": "warn",
|
|
181
|
-
// Services & Layers
|
|
182
|
-
"dl-effect/service-port-shape-only": "warn",
|
|
183
|
-
"dl-effect/service-key-matches-name": "warn",
|
|
184
|
-
"dl-effect/service-built-with-of": "warn",
|
|
185
|
-
"dl-effect/provide-named-layer": "warn",
|
|
186
|
-
"dl-effect/no-provide-on-definition": "warn",
|
|
187
|
-
"dl-effect/anti-slop-no-service-constructor-imports": "warn",
|
|
188
|
-
// Runtime & entrypoint
|
|
189
|
-
"dl-effect/run-only-in-runner": "warn",
|
|
190
|
-
"dl-effect/prefer-effect-vitest": "warn",
|
|
191
|
-
"dl-effect/all-errors-handled-at-boundary": "warn",
|
|
192
|
-
// Design & boundaries
|
|
193
|
-
"dl-effect/adapter-import-boundary": "warn",
|
|
194
|
-
"dl-effect/no-transport-types-in-generator": ["warn", { transportTypes }],
|
|
195
|
-
// Effectful functions
|
|
196
|
-
"dl-effect/prefer-effect-fn": "warn",
|
|
197
|
-
"dl-effect/untraced-recursion": "warn",
|
|
198
|
-
"dl-effect/single-style-per-function": "warn",
|
|
199
|
-
// Code shape
|
|
200
|
-
"dl-effect/no-nested-match": "warn",
|
|
201
|
-
"dl-effect/no-yielded-match": "warn",
|
|
202
|
-
"dl-effect/no-inline-yield-star": "warn",
|
|
203
|
-
"dl-effect/no-effect-return-type": ["warn", { checkExported: false }],
|
|
204
|
-
"dl-effect/no-asymmetric-match-handlers": "warn",
|
|
205
|
-
"dl-effect/anti-slop-prefer-effect-match": "warn",
|
|
206
|
-
// Schema
|
|
207
|
-
"dl-effect/exported-schema-identifier": "warn",
|
|
208
|
-
"dl-effect/no-inline-schema-codec": "warn",
|
|
209
|
-
// Config & secrets
|
|
210
|
-
"dl-effect/redacted-secret-names": ["warn", { secretFields }],
|
|
211
|
-
},
|
|
212
|
-
})
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
To change a setting, declare it in `settings`; to change a rule option, extend `all` and override only that rule.
|
|
216
|
-
|
|
217
|
-
## Versioning
|
|
218
|
-
|
|
219
|
-
Versions are `0.x`: any minor release may add, remove or change rules, options, settings and messages.
|
|
220
|
-
|
|
221
|
-
Supported oxlint is the `peerDependencies` range, `^1.82.0`; CI tests the tarball against `1.82.0`. oxlint's JS plugin API is still experimental, so a newer oxlint minor may break the plugin before the range is narrowed.
|
|
40
|
+
## Documentation
|
|
222
41
|
|
|
223
|
-
|
|
42
|
+
The guide is on the [website](https://doubleloop-io.github.io/oxlint-plugin-effect/):
|
|
224
43
|
|
|
225
|
-
-
|
|
226
|
-
-
|
|
227
|
-
-
|
|
228
|
-
-
|
|
229
|
-
-
|
|
230
|
-
-
|
|
231
|
-
-
|
|
232
|
-
- `pnpm build:w`: reruns `pnpm build`, preset included, on every change to `index.ts`, `rules/`, `presets/`, `shared/` or `tsconfig.build.json`; a failed build waits for the next change.
|
|
233
|
-
- `pnpm consumer:link <consumer>`: after `pnpm build`, replaces the pnpm symlinks of the plugin in every workspace package of `<consumer>` with links to the checkout, so the consumer's own lint runs the checkout's rules; no tracked file of the consumer changes. `pnpm consumer:unlink <consumer>` restores them with `pnpm install --frozen-lockfile`, as does any `pnpm install` in the consumer.
|
|
44
|
+
- [Configuration](https://doubleloop-io.github.io/oxlint-plugin-effect/configuration/): the `all` preset and picking rules.
|
|
45
|
+
- [Shared settings](https://doubleloop-io.github.io/oxlint-plugin-effect/shared-settings/): the settings that describe your project, their defaults and the rules that use them.
|
|
46
|
+
- [Default configuration](https://doubleloop-io.github.io/oxlint-plugin-effect/default-configuration/): `all` with every setting and option spelled out.
|
|
47
|
+
- [Alongside `@effect/tsgo`](https://doubleloop-io.github.io/oxlint-plugin-effect/alongside-effect-tsgo/): combining its presets with this one.
|
|
48
|
+
- [Rules overview](https://doubleloop-io.github.io/oxlint-plugin-effect/rules-overview/): every rule, grouped by topic.
|
|
49
|
+
- [Versioning](https://doubleloop-io.github.io/oxlint-plugin-effect/versioning/): what a release may change.
|
|
50
|
+
- [Troubleshooting](https://doubleloop-io.github.io/oxlint-plugin-effect/troubleshooting/): the problems most often hit when adopting the plugin.
|
|
234
51
|
|
|
235
|
-
|
|
52
|
+
Coding agents: the same pages ship in the package under `docs/user-guide/`, starting from `docs/user-guide/README.md`.
|
|
@@ -35,6 +35,29 @@ const statementOf = (node) => {
|
|
|
35
35
|
return current;
|
|
36
36
|
};
|
|
37
37
|
const logicalAssignments = new Set(["&&=", "||=", "??="]);
|
|
38
|
+
const isConditionalBranch = (child, parent) => {
|
|
39
|
+
switch (parent.type) {
|
|
40
|
+
case "ConditionalExpression":
|
|
41
|
+
return parent.test !== child;
|
|
42
|
+
case "LogicalExpression":
|
|
43
|
+
return parent.right === child;
|
|
44
|
+
case "AssignmentExpression":
|
|
45
|
+
return parent.right === child && logicalAssignments.has(parent.operator);
|
|
46
|
+
default:
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
// A branch that is the `yield*` itself can be bound whole; a larger branch cannot be bound first
|
|
51
|
+
// without running the effect unconditionally.
|
|
52
|
+
const isNestedInConditionalBranch = (node, statement) => {
|
|
53
|
+
let child = node.parent;
|
|
54
|
+
while (child !== statement && child.parent) {
|
|
55
|
+
if (isConditionalBranch(child, child.parent))
|
|
56
|
+
return true;
|
|
57
|
+
child = child.parent;
|
|
58
|
+
}
|
|
59
|
+
return false;
|
|
60
|
+
};
|
|
38
61
|
// The extracted `const` runs before its statement, once and unconditionally: any position that
|
|
39
62
|
// short-circuits, repeats or belongs to a nested statement would change when the effect runs.
|
|
40
63
|
const runsOnceBeforeStatement = (child, parent, statement) => {
|
|
@@ -89,13 +112,32 @@ const trailingName = (node) => {
|
|
|
89
112
|
return undefined;
|
|
90
113
|
}
|
|
91
114
|
};
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
115
|
+
const memberOwner = (node) => {
|
|
116
|
+
switch (node.type) {
|
|
117
|
+
case "MemberExpression":
|
|
118
|
+
return node.computed ? undefined : node.object;
|
|
119
|
+
case "CallExpression":
|
|
120
|
+
case "NewExpression":
|
|
121
|
+
return memberOwner(node.callee);
|
|
122
|
+
default:
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
const lowerFirst = (name) => name.charAt(0).toLowerCase() + name.slice(1);
|
|
127
|
+
const upperFirst = (name) => name.charAt(0).toUpperCase() + name.slice(1);
|
|
128
|
+
// `yield* readSheet()` becomes `sheet`, `yield* reader.rows` becomes `rows`, or `readerRows`
|
|
129
|
+
// when `rows` is taken: the owner tells the reader where the value comes from, a suffix does not.
|
|
130
|
+
const names = (node) => {
|
|
131
|
+
const { argument } = node;
|
|
132
|
+
const name = argument ? trailingName(argument)?.replace(verbPrefix, "") : undefined;
|
|
133
|
+
if (!argument || name === undefined || name === "")
|
|
134
|
+
return { base: "value" };
|
|
135
|
+
const base = lowerFirst(name);
|
|
136
|
+
if (reservedWords.has(base))
|
|
137
|
+
return { base: "value" };
|
|
138
|
+
const owner = memberOwner(argument);
|
|
139
|
+
const ownerName = owner ? trailingName(owner) : undefined;
|
|
140
|
+
return ownerName ? { base, qualified: lowerFirst(ownerName) + upperFirst(base) } : { base };
|
|
99
141
|
};
|
|
100
142
|
const namesInUse = (scope) => {
|
|
101
143
|
const names = new Set();
|
|
@@ -130,12 +172,14 @@ const replacedRange = (node, sourceCode) => {
|
|
|
130
172
|
}
|
|
131
173
|
return wrappers[wrappers.length - 1 - callParens] ?? node.range;
|
|
132
174
|
};
|
|
133
|
-
const extraction = (statement, hoisted, sourceCode) => {
|
|
134
|
-
const taken = namesInUse(sourceCode.getScope(statement).variableScope);
|
|
175
|
+
const extraction = (statement, hoisted, sourceCode, namesByOwner, owner) => {
|
|
176
|
+
const taken = namesByOwner.get(owner) ?? namesInUse(sourceCode.getScope(statement).variableScope);
|
|
177
|
+
namesByOwner.set(owner, taken);
|
|
135
178
|
const replacements = hoisted.map((node) => {
|
|
136
|
-
|
|
179
|
+
const { base, qualified } = names(node);
|
|
180
|
+
let name = taken.has(base) && qualified !== undefined ? qualified : base;
|
|
137
181
|
for (let suffix = 2; taken.has(name); suffix++)
|
|
138
|
-
name = `${
|
|
182
|
+
name = `${base}${suffix}`;
|
|
139
183
|
taken.add(name);
|
|
140
184
|
return { node, name, range: replacedRange(node, sourceCode) };
|
|
141
185
|
});
|
|
@@ -161,6 +205,7 @@ const extraction = (statement, hoisted, sourceCode) => {
|
|
|
161
205
|
const guarded = startsLine && /^[[(`+\-/]/.test(remainder) ? `;${remainder}` : remainder;
|
|
162
206
|
return [...declarations, guarded].join(startsLine ? `\n${indentation}` : "; ");
|
|
163
207
|
};
|
|
208
|
+
const inlineYieldStarMessage = "This `yield*` runs inside a larger expression. Bind its result first (`const rows = yield* reader.rows`), so each statement runs one effect in reading order.";
|
|
164
209
|
export const noInlineYieldStarRule = defineRule({
|
|
165
210
|
meta: {
|
|
166
211
|
type: "suggestion",
|
|
@@ -169,7 +214,8 @@ export const noInlineYieldStarRule = defineRule({
|
|
|
169
214
|
description: "Bind the result of a `yield*` to a `const` instead of using it inside a larger expression.",
|
|
170
215
|
},
|
|
171
216
|
messages: {
|
|
172
|
-
inlineYieldStar:
|
|
217
|
+
inlineYieldStar: inlineYieldStarMessage,
|
|
218
|
+
conditionalYieldStar: `${inlineYieldStarMessage} When inside a conditional branch, binding it first would run it unconditionally. Instead, extract the effect into a named function and transform its result inside it (\`Effect.map\`), so the branch is a single \`yield*\`.`,
|
|
173
219
|
},
|
|
174
220
|
},
|
|
175
221
|
createOnce(context) {
|
|
@@ -197,6 +243,7 @@ export const noInlineYieldStarRule = defineRule({
|
|
|
197
243
|
VariableDeclarator: collect,
|
|
198
244
|
"Program:exit"() {
|
|
199
245
|
const byStatement = new Map();
|
|
246
|
+
const namesByOwner = new Map();
|
|
200
247
|
for (const node of inline) {
|
|
201
248
|
const statement = statementOf(node);
|
|
202
249
|
byStatement.set(statement, [...(byStatement.get(statement) ?? []), node]);
|
|
@@ -212,12 +259,14 @@ export const noInlineYieldStarRule = defineRule({
|
|
|
212
259
|
hoisted.length > 0 &&
|
|
213
260
|
!reordersEffects(hoisted, evaluatedInStatement);
|
|
214
261
|
const replacement = fixable
|
|
215
|
-
? extraction(statement, hoisted, context.sourceCode)
|
|
262
|
+
? extraction(statement, hoisted, context.sourceCode, namesByOwner, owner)
|
|
216
263
|
: undefined;
|
|
217
264
|
for (const node of yields) {
|
|
218
265
|
context.report({
|
|
219
266
|
node,
|
|
220
|
-
messageId:
|
|
267
|
+
messageId: isNestedInConditionalBranch(node, statement)
|
|
268
|
+
? "conditionalYieldStar"
|
|
269
|
+
: "inlineYieldStar",
|
|
221
270
|
...(replacement !== undefined && hoisted.includes(node)
|
|
222
271
|
? { fix: (fixer) => fixer.replaceText(statement, replacement) }
|
|
223
272
|
: {}),
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "@doubleloop-io/oxlint-plugin-effect"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
An [oxlint](https://github.com/oxc-project/oxc) plugin that enforces a catalog of patterns for [Effect](https://github.com/Effect-TS/effect/) 4 code.
|
|
6
|
+
|
|
7
|
+
The rules are written and tested against Effect 4. Where an Effect 3 API has the same intent, the rules recognize its v3 name too: each rule's Trigger names them.
|
|
8
|
+
|
|
9
|
+
It complements the type-aware rules of [`@effect/tsgo`](alongside-effect-tsgo.md): it does not need them, and it checks what they do not.
|
|
10
|
+
|
|
11
|
+
## Guide
|
|
12
|
+
|
|
13
|
+
- [Installation](installation.md): the package and the Node and oxlint versions it requires.
|
|
14
|
+
- [Configuration](configuration.md): the `all` preset and picking rules.
|
|
15
|
+
- [Shared settings](shared-settings.md): the settings that describe your project, their defaults and the rules that use them.
|
|
16
|
+
- [Alongside `@effect/tsgo`](alongside-effect-tsgo.md): combining its presets with this one.
|
|
17
|
+
- [Default configuration](default-configuration.md): `all` with every setting and option spelled out.
|
|
18
|
+
- [Versioning](versioning.md): what a release may change and which oxlint versions are supported.
|
|
19
|
+
- [Troubleshooting](troubleshooting.md): the problems most often hit when adopting the plugin.
|
|
20
|
+
|
|
21
|
+
## Rules
|
|
22
|
+
|
|
23
|
+
- [Rules overview](rules.md): every rule, grouped by topic.
|
|
24
|
+
- [Common limits](common-limits.md): the limits every rule shares.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Alongside `@effect/tsgo`
|
|
3
|
+
order: 4
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
[`@effect/tsgo`](https://github.com/Effect-TS/tsgo) is the official Effect language service on top of the TypeScript Go compiler; its `effecttsgo` oxlint presets add more than 100 type-aware rules. This plugin does not need it, and its rules target what those presets do not check, so the two complement each other. To use both, follow its [Oxlint Setup guide](https://github.com/Effect-TS/tsgo/blob/main/docs/README.md) and put its preset in `extends` **before** `all`:
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
import dlEffectAll from "@doubleloop-io/oxlint-plugin-effect/presets/all"
|
|
10
|
+
import { recommended as effectTsgoRecommended } from "@effect/tsgo/oxlint-presets"
|
|
11
|
+
import { defineConfig } from "oxlint"
|
|
12
|
+
|
|
13
|
+
export default defineConfig({ extends: [effectTsgoRecommended, dlEffectAll] })
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
```json
|
|
17
|
+
{
|
|
18
|
+
"extends": [
|
|
19
|
+
"./node_modules/@effect/tsgo/oxlint-presets/recommended.json",
|
|
20
|
+
"./node_modules/@doubleloop-io/oxlint-plugin-effect/dist/presets/all.json"
|
|
21
|
+
]
|
|
22
|
+
}
|
|
23
|
+
```
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Common limits
|
|
3
|
+
order: 6
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
The limits every rule shares.
|
|
7
|
+
|
|
8
|
+
- **Syntax only**: no rule uses type information. Only two rules look past the file: [`adapter-import-boundary`](rules/adapter-import-boundary.md), to find which directories are adapters, and [`prefer-effect-vitest`](rules/prefer-effect-vitest.md), to find whether `@effect/vitest` is installed.
|
|
9
|
+
- **Literal names**: `Effect.gen` is recognized, `import { Effect as E }` or `import * as L from "effect/Layer"` slip through. On code that uses aliases, silence is not compliance.
|
|
10
|
+
- **Effect 4**: the rules are written against v4. Where a v3 API has the same intent, they recognize its v3 name too (`catchAll`, `Either`, `Context.Tag`, `Config.redacted`, …), and the Trigger names it.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Configuration
|
|
3
|
+
order: 2
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Extend the `all` preset. Prefer `oxlint.config.ts`, where `extends` takes config objects and the preset is imported by package name:
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
import dlEffectAll from "@doubleloop-io/oxlint-plugin-effect/presets/all"
|
|
10
|
+
import { defineConfig } from "oxlint"
|
|
11
|
+
|
|
12
|
+
export default defineConfig({ extends: [dlEffectAll] })
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
With `.oxlintrc.json`, `extends` takes file paths only, so the preset is reached through `node_modules`:
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
{
|
|
19
|
+
"extends": ["./node_modules/@doubleloop-io/oxlint-plugin-effect/dist/presets/all.json"]
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`all` enables every rule at `warn`, so adopting it never breaks a build: look at the warnings first, then raise to `error` the rules you want to enforce. A `recommended` preset with reasoned severities will come from measurements on real codebases.
|
|
24
|
+
|
|
25
|
+
To pick rules without the preset, load the plugin by package name and list the rules:
|
|
26
|
+
|
|
27
|
+
```json
|
|
28
|
+
{
|
|
29
|
+
"jsPlugins": ["@doubleloop-io/oxlint-plugin-effect"],
|
|
30
|
+
"rules": {
|
|
31
|
+
"dl-effect/no-effect-promise": "error",
|
|
32
|
+
"dl-effect/try-object-form": "error"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Settings
|
|
38
|
+
|
|
39
|
+
The settings that describe your project, such as its entry files and test files, are on [Shared settings](shared-settings.md). Their defaults fit a Node CLI: on another layout (Next.js, a monorepo), set them before reading the warnings.
|
|
40
|
+
|
|
41
|
+
The other options and every default are in [Default configuration](default-configuration.md).
|
|
42
|
+
|
|
43
|
+
To use it with the `@effect/tsgo` presets, see [Alongside `@effect/tsgo`](alongside-effect-tsgo.md).
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Default configuration
|
|
3
|
+
order: 5
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
`all` is equivalent to the config below, with every setting and option spelled out at its default. Both **replace** the defaults instead of extending them: see [Shared settings](shared-settings.md).
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
import { defineConfig } from "oxlint"
|
|
10
|
+
|
|
11
|
+
// Constructors that must not appear inside an Effect generator: the conversion to the
|
|
12
|
+
// transport belongs in the second argument of Effect.fn. Add your own transport DTOs.
|
|
13
|
+
const transportTypes = ["Response", "NextResponse"]
|
|
14
|
+
|
|
15
|
+
// Schema fields holding a secret: a field matches an entry or ends with it capitalized
|
|
16
|
+
// (`password` matches `dbPassword`). No generic `token`: `nextPageToken` is not a secret.
|
|
17
|
+
const secretFields = ["password", "secret", "apiKey", "accessToken", "refreshToken", "idToken"]
|
|
18
|
+
|
|
19
|
+
export default defineConfig({
|
|
20
|
+
jsPlugins: ["@doubleloop-io/oxlint-plugin-effect"],
|
|
21
|
+
settings: {
|
|
22
|
+
"dl-effect": {
|
|
23
|
+
// Files allowed to run Effects (Effect.run*, runMain, ManagedRuntime.make) and to
|
|
24
|
+
// import adapters. `index.ts` is left out on purpose: as a barrel it would let
|
|
25
|
+
// adapters leak through re-exports.
|
|
26
|
+
entryFiles: ["**/main.ts", "**/cli.ts", "**/bin/**"],
|
|
27
|
+
// Files exempt from the rules that tests legitimately break. Helper directories such
|
|
28
|
+
// as `src/testing/` are support code, not tests, so they are not matched.
|
|
29
|
+
testFiles: [
|
|
30
|
+
"**/*.test.*",
|
|
31
|
+
"**/*.spec.*",
|
|
32
|
+
"**/__tests__/**",
|
|
33
|
+
"**/test/**",
|
|
34
|
+
"**/tests/**",
|
|
35
|
+
],
|
|
36
|
+
// Fields that discriminate a closed union, compared or switched on only through an
|
|
37
|
+
// exhaustive match and set only by a constructor. Add the ones your unions use, such
|
|
38
|
+
// as `type` or `kind`.
|
|
39
|
+
discriminants: ["_tag"],
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
// Every rule at `warn`, to measure noise without blocking.
|
|
43
|
+
rules: {
|
|
44
|
+
// Error channel
|
|
45
|
+
"dl-effect/no-throw-or-primitive-failure": "warn",
|
|
46
|
+
"dl-effect/try-object-form": "warn",
|
|
47
|
+
"dl-effect/no-effect-promise": "warn",
|
|
48
|
+
"dl-effect/no-parameterless-catch-all": "warn",
|
|
49
|
+
"dl-effect/split-success-path-from-error-handling": "warn",
|
|
50
|
+
"dl-effect/anti-slop-no-manual-effect-error-tag": "warn",
|
|
51
|
+
// Tagged values
|
|
52
|
+
"dl-effect/no-manual-discriminant-comparison": "warn",
|
|
53
|
+
"dl-effect/no-manual-discriminant-construction": "warn",
|
|
54
|
+
// Services & Layers
|
|
55
|
+
"dl-effect/service-port-shape-only": "warn",
|
|
56
|
+
"dl-effect/service-key-matches-name": "warn",
|
|
57
|
+
"dl-effect/service-built-with-of": "warn",
|
|
58
|
+
"dl-effect/provide-named-layer": "warn",
|
|
59
|
+
"dl-effect/no-provide-on-definition": "warn",
|
|
60
|
+
"dl-effect/anti-slop-no-service-constructor-imports": "warn",
|
|
61
|
+
// Runtime & entrypoint
|
|
62
|
+
"dl-effect/run-only-in-runner": "warn",
|
|
63
|
+
"dl-effect/prefer-effect-vitest": "warn",
|
|
64
|
+
"dl-effect/all-errors-handled-at-boundary": "warn",
|
|
65
|
+
// Design & boundaries
|
|
66
|
+
"dl-effect/adapter-import-boundary": "warn",
|
|
67
|
+
"dl-effect/no-transport-types-in-generator": ["warn", { transportTypes }],
|
|
68
|
+
// Effectful functions
|
|
69
|
+
"dl-effect/prefer-effect-fn": "warn",
|
|
70
|
+
"dl-effect/untraced-recursion": "warn",
|
|
71
|
+
"dl-effect/single-style-per-function": "warn",
|
|
72
|
+
// Code shape
|
|
73
|
+
"dl-effect/no-nested-match": "warn",
|
|
74
|
+
"dl-effect/no-yielded-match": "warn",
|
|
75
|
+
"dl-effect/no-inline-yield-star": "warn",
|
|
76
|
+
"dl-effect/no-effect-return-type": ["warn", { checkExported: false }],
|
|
77
|
+
"dl-effect/no-asymmetric-match-handlers": "warn",
|
|
78
|
+
"dl-effect/anti-slop-prefer-effect-match": "warn",
|
|
79
|
+
// Schema
|
|
80
|
+
"dl-effect/exported-schema-identifier": "warn",
|
|
81
|
+
"dl-effect/no-inline-schema-codec": "warn",
|
|
82
|
+
// Config & secrets
|
|
83
|
+
"dl-effect/redacted-secret-names": ["warn", { secretFields }],
|
|
84
|
+
},
|
|
85
|
+
})
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
To change a setting, declare it in `settings`; to change a rule option, extend `all` and override only that rule.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Installation
|
|
3
|
+
order: 1
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
pnpm add -D @doubleloop-io/oxlint-plugin-effect oxlint
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
Requires Node `^22.20.0 || >=24.8.0` and oxlint `^1.82.0`. What a release may change and which oxlint versions are supported: [Versioning](versioning.md).
|
|
11
|
+
|
|
12
|
+
Then extend the `all` preset: see [Configuration](configuration.md).
|