@opetope/lint 0.11.0 → 0.12.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/CHANGELOG.md +667 -4
- package/README.md +69 -596
- package/dist/ast.d.ts +3 -1
- package/dist/ast.js +1 -1
- package/dist/ast.js.map +1 -1
- package/dist/command-hooks.d.ts +2 -2
- package/dist/command-hooks.js +1 -1
- package/dist/command-hooks.js.map +1 -1
- package/dist/context-members.d.ts +22 -0
- package/dist/context-members.js +2 -0
- package/dist/context-members.js.map +1 -0
- package/dist/declaration-ingress.d.ts +10 -2
- package/dist/declaration-ingress.js +1 -1
- package/dist/declaration-ingress.js.map +1 -1
- package/dist/effect-declarations.d.ts +20 -0
- package/dist/effect-declarations.js +2 -0
- package/dist/effect-declarations.js.map +1 -0
- package/dist/index.d.ts +11 -3
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/retired-vocabulary.d.ts +31 -0
- package/dist/retired-vocabulary.js +2 -0
- package/dist/retired-vocabulary.js.map +1 -0
- package/dist/rules/capture-command-cleanup.js +1 -1
- package/dist/rules/capture-command-cleanup.js.map +1 -1
- package/dist/rules/define-feature-property-order.js +1 -1
- package/dist/rules/define-feature-property-order.js.map +1 -1
- package/dist/rules/enabled-predicate.d.ts +6 -0
- package/dist/rules/enabled-predicate.js +2 -0
- package/dist/rules/enabled-predicate.js.map +1 -0
- package/dist/rules/no-command-in-deps.js +1 -1
- package/dist/rules/no-command-in-deps.js.map +1 -1
- package/dist/rules/no-internal-imports.js +1 -1
- package/dist/rules/no-internal-imports.js.map +1 -1
- package/dist/rules/no-retired-vocabulary.d.ts +5 -0
- package/dist/rules/no-retired-vocabulary.js +2 -0
- package/dist/rules/no-retired-vocabulary.js.map +1 -0
- package/dist/rules/no-write-after-source-write.d.ts +6 -0
- package/dist/rules/no-write-after-source-write.js +2 -0
- package/dist/rules/no-write-after-source-write.js.map +1 -0
- package/dist/rules/prefer-effect-current.js +1 -1
- package/dist/rules/prefer-effect-current.js.map +1 -1
- package/oxlintrc.json +3 -1
- package/package.json +1 -2
- package/README.ru.md +0 -648
- package/dist/rules/when-predicate.d.ts +0 -6
- package/dist/rules/when-predicate.js +0 -2
- package/dist/rules/when-predicate.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,26 +1,20 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `@opetope/lint`
|
|
2
2
|
|
|
3
|
-
ESLint rules for the
|
|
4
|
-
declaration: shapes that compile and run, yet say something other than what their author meant. What the type
|
|
5
|
-
checker, the runtime and dead-code analysis already answer stays with them (see [`../docs/decisions.md`](https://www.npmjs.com/package/@opetope/runtime), D227).
|
|
6
|
-
|
|
7
|
-
The package depends on no other `@opetope/*` package and reads no types, so a host can lint sources it has not
|
|
8
|
-
built yet.
|
|
3
|
+
ESLint rules for the Opetope authoring and layer boundaries. The rules guide model writes, feature declarations, hook dependencies and imports; they do not replace type checking or runtime validation.
|
|
9
4
|
|
|
10
5
|
## Installation
|
|
11
6
|
|
|
12
7
|
```sh
|
|
13
|
-
npm install --save-dev @opetope/lint
|
|
8
|
+
npm install --save-exact --save-dev @opetope/lint
|
|
14
9
|
```
|
|
15
10
|
|
|
16
|
-
Use matching Opetope versions
|
|
17
|
-
|
|
11
|
+
Use matching exact Opetope versions; for release candidates, install every Opetope package from `@next`.
|
|
12
|
+
Packages are ESM-only and support Node 20.19+. React integrations support React and React DOM 19.
|
|
18
13
|
|
|
19
|
-
|
|
20
|
-
`node_modules/@opetope/runtime/docs/spec.md` or `spec.ru.md`; recipes are in `cookbook.md` and `cookbook.ru.md`.
|
|
21
|
-
No GitHub access is needed to read those installed guides.
|
|
14
|
+
## Example
|
|
22
15
|
|
|
23
|
-
|
|
16
|
+
This focused example shows the package boundary. [Start](../runtime/docs/start.md) includes a complete
|
|
17
|
+
feature, host, React entry and cleanup in one visible module.
|
|
24
18
|
|
|
25
19
|
```js
|
|
26
20
|
// eslint.config.mjs
|
|
@@ -34,614 +28,93 @@ export default [
|
|
|
34
28
|
];
|
|
35
29
|
```
|
|
36
30
|
|
|
37
|
-
|
|
38
|
-
namespace `opetope`, so a rule is named `opetope/when-predicate`. Parsing requires `@typescript-eslint/parser`;
|
|
39
|
-
both it and `eslint` are peer dependencies.
|
|
40
|
-
|
|
41
|
-
## Configs
|
|
42
|
-
|
|
43
|
-
### `recommended`
|
|
44
|
-
|
|
45
|
-
Every rule that holds wherever Opetope is written: `when-predicate`, `define-feature-property-order`,
|
|
46
|
-
`require-literal-id`, `id-naming`, `no-internal-imports`, `no-snapshot-read-in-render`, `no-snapshot-in-update`,
|
|
47
|
-
`no-redundant-const-tuple`, `no-command-in-deps`, `prefer-effect-current`, `capture-command-cleanup` and `require-declared-models` as errors. A rule that needs to know where a host keeps its files is off here and arrives
|
|
48
|
-
through `layers`; `prefer-model-selection` is off because the number of hooks a component may keep is a taste a
|
|
49
|
-
project settles for itself.
|
|
50
|
-
|
|
51
|
-
`configs.recommended.rules` in full, which is also what `@opetope/lint/oxlintrc.json` ships:
|
|
52
|
-
|
|
53
|
-
| Rule | `recommended` | Turned on by |
|
|
54
|
-
| ------------------------------- | ------------- | ------------------------------------------- |
|
|
55
|
-
| `capture-command-cleanup` | `error` | |
|
|
56
|
-
| `define-feature-property-order` | `error` | |
|
|
57
|
-
| `id-naming` | `error` | |
|
|
58
|
-
| `layer-placement` | `off` | `layers({ integration, models, ui })` |
|
|
59
|
-
| `no-command-in-deps` | `error` | |
|
|
60
|
-
| `no-internal-imports` | `error` | also scoped by `internalImports({ allow })` |
|
|
61
|
-
| `no-redundant-const-tuple` | `error` | |
|
|
62
|
-
| `no-snapshot-in-update` | `error` | |
|
|
63
|
-
| `no-snapshot-read-in-render` | `error` | |
|
|
64
|
-
| `no-subscribe-outside-models` | `off` | `layers({ models })` |
|
|
65
|
-
| `prefer-effect-current` | `error` | |
|
|
66
|
-
| `prefer-model-selection` | `off` | the project, with its own `threshold` |
|
|
67
|
-
| `require-declared-models` | `error` | |
|
|
68
|
-
| `require-literal-id` | `error` | |
|
|
69
|
-
| `when-predicate` | `error` | |
|
|
70
|
-
|
|
71
|
-
The three `off` rules are the three that need something only the project knows: which directories are which layer,
|
|
72
|
-
and how many hooks one component may keep.
|
|
73
|
-
|
|
74
|
-
### `layers`
|
|
75
|
-
|
|
76
|
-
`opetope.configs.layers({ integration, models, ui, tests })` takes the globs of a project's own layers and returns
|
|
77
|
-
the configuration each of them earns. A layer without globs is a layer this project does not have, and it produces
|
|
78
|
-
nothing; a file no glob claims is placed by nobody.
|
|
79
|
-
|
|
80
|
-
```js
|
|
81
|
-
...opetope.configs.layers({
|
|
82
|
-
integration: ['src/features/*/integration/**/*.{ts,tsx}'],
|
|
83
|
-
models: ['src/features/*/models/**/*.ts'],
|
|
84
|
-
ui: ['src/features/*/ui/**/*.{ts,tsx}'],
|
|
85
|
-
}),
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
`integration`, `models` and `ui` each turn on `layer-placement` for their files. `models` also turns on
|
|
89
|
-
`no-subscribe-outside-models` for every source file and steps aside in that layer and in `tests`, which defaults to
|
|
90
|
-
`['**/__tests__/**', '**/*.spec.ts', '**/*.spec.tsx']`. A subscription is covered even in a file no layer claims,
|
|
91
|
-
because forgetting one is how a subscription outlives its reader.
|
|
92
|
-
|
|
93
|
-
### `internalImports`
|
|
94
|
-
|
|
95
|
-
`opetope.configs.internalImports({ allow, files })` configures `opetope/no-internal-imports` across JavaScript and
|
|
96
|
-
TypeScript, including `.mjs` model tests. The rule is already enabled by `recommended`; this helper is useful for
|
|
97
|
-
scoping it or naming trusted library implementation and host integration files. `allow` explicitly switches off
|
|
98
|
-
only this rule for those files. Place the helper's blocks **after** `recommended`: a later `recommended` block
|
|
99
|
-
re-enables the rule, including for the allowed files. Other project restrictions stay intact.
|
|
100
|
-
|
|
101
|
-
```js
|
|
102
|
-
export default [
|
|
103
|
-
opetope.configs.recommended,
|
|
104
|
-
...opetope.configs.internalImports({ allow: ['src/bootstrap/**/devtools.ts'] }),
|
|
105
|
-
];
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
The dedicated rule never changes `no-restricted-imports`. The former `internalImportPattern` export is removed:
|
|
109
|
-
enable this rule or use the helper instead of merging an Opetope pattern into the host's restrictions (D261).
|
|
110
|
-
|
|
111
|
-
## Rules
|
|
112
|
-
|
|
113
|
-
### `no-internal-imports`
|
|
114
|
-
|
|
115
|
-
Application code and tests import public Opetope entries. The rule rejects `@opetope/*/internal` and its deeper
|
|
116
|
-
paths in imports, re-exports, literal `import()` calls, unshadowed `require()` calls, TypeScript import types and
|
|
117
|
-
`import = require`. It includes type-only imports; an internal type is still coupled to the private ABI.
|
|
118
|
-
Use `runCall` from `@opetope/core/testing` to run a real model Call, `openModel` from `@opetope/runtime/testing` to open one without a feature, and `command` from `@opetope/react/testing` to construct a fixture Call.
|
|
119
|
-
Tests do not receive an automatic exception. Trusted library implementation or host integration can use an explicit
|
|
120
|
-
file override; the rule has no autofix because the correct public replacement depends on the imported operation.
|
|
121
|
-
|
|
122
|
-
**The boundary.** This is a syntactic check of literal module paths and templates without substitutions. It does
|
|
123
|
-
not resolve aliases, computed import paths, re-export graphs or custom loaders. A locally shadowed `require` is not
|
|
124
|
-
treated as Node's loader. Apply the config to every JavaScript/TypeScript source and test glob the project uses.
|
|
125
|
-
|
|
126
|
-
### `when-predicate`
|
|
127
|
-
|
|
128
|
-
A contribution's `when` answers the visibility fact of this instance, not the source that carries it (D220). The
|
|
129
|
-
rule reads the `when` of a `slot`, `pipe` or `register` contribution, and any `when` whose function destructures the
|
|
130
|
-
evaluation context `{ exports, imports, own, read }`, and reports three shapes:
|
|
131
|
-
|
|
132
|
-
| Written | Reported |
|
|
133
|
-
| ------------------------------------------ | ----------------------------------------------------------------------------- |
|
|
134
|
-
| `when: ({ imports }) => imports.x.allowed` | returns a source; fixed to `({ imports, read }) => read(imports.x.allowed)` |
|
|
135
|
-
| `when: async ({ read }) => read(x)` | a predicate answers synchronously; no fix |
|
|
136
|
-
| `when: () => allowed` | a `Readable<boolean>` is passed as `when` directly, without a wrapper; no fix |
|
|
137
|
-
|
|
138
|
-
The fix wraps the returned member expression in `read(...)` and adds `read` to the destructured context when it is
|
|
139
|
-
absent. A named context is read through itself: `context => context.imports.x.allowed` becomes
|
|
140
|
-
`context => context.read(context.imports.x.allowed)`.
|
|
141
|
-
|
|
142
|
-
**The boundary.** The rule reads shapes, not types. `({ read }) => read(counter)` over a non-boolean source stays a
|
|
143
|
-
type error, and so does a `read` of something that is not a `Readable`. Where a member access of the evaluation
|
|
144
|
-
context is a plain value rather than a source, the predicate cannot change its answer, and the rule reports it as
|
|
145
|
-
one written for a source; hoist that decision out of `when`, or silence the line. `when` outside a contribution —
|
|
146
|
-
the positional `(current, previous)` of `effect`, or the source value of `scope.while` — is left alone.
|
|
147
|
-
|
|
148
|
-
### `define-feature-property-order`
|
|
149
|
-
|
|
150
|
-
A feature declares its sections in one order: `imports`, `requires`, `own`, `exports`, `provides`, `when`, and the
|
|
151
|
-
`body` loader that replaces the last three stages (spec §2.1, D186). The identity is not among them — a feature
|
|
152
|
-
names itself with its first argument (D280). The order is the reading order of the declaration — the edges, the
|
|
153
|
-
stages, then the lifetime and the loader — so a reader finds a section by position, and a key sorter can be
|
|
154
|
-
configured to hold the same order instead of a second one.
|
|
155
|
-
|
|
156
|
-
The fix reorders the properties. It stands down when a comment sits between two sections, because such a comment
|
|
157
|
-
belongs to neither and reordering would move it away from the line it explains; a comment inside a section travels
|
|
158
|
-
with it. An object with a key that is not a section, or with a spread, is left to the type checker.
|
|
159
|
-
|
|
160
|
-
### `no-redundant-const-tuple`
|
|
161
|
-
|
|
162
|
-
`derive` takes its tuple of sources as a `const` type parameter, so `[left, right]` infers its own tuple and the
|
|
163
|
-
selector already sees exact values. The `as const` written beside it states what the signature already states
|
|
164
|
-
(D279, D309):
|
|
165
|
-
|
|
166
|
-
```ts
|
|
167
|
-
const summary = derive([total, label], (value, suffix) => `${value} ${suffix}`);
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
The rule reads the first argument of a `derive` resolved to `@opetope/core`, and only where that argument is an
|
|
171
|
-
array literal carrying `as const`. The fix removes the assertion and nothing else. It stands down when a comment
|
|
172
|
-
sits between the tuple and the assertion, because such a comment belongs to neither and the removal would take it
|
|
173
|
-
along.
|
|
174
|
-
|
|
175
|
-
**The boundary.** An assertion written anywhere else is left alone: on one element of the tuple, where it says
|
|
176
|
-
something about that element; on the result of the selector; on the options record; and on the single-source form,
|
|
177
|
-
whose argument is not a tuple at all. `as Sources` is a different assertion and says something else. The import is
|
|
178
|
-
resolved within one module — a local alias, a `const` that holds the import and a namespace binding all reach the
|
|
179
|
-
same export — and, unlike `no-command-in-deps`, this rule recognizes the import from `@opetope/core` and nothing
|
|
180
|
-
else: a re-export through a host's own barrel it does not see. That is the deliberate price of a safe fix. This
|
|
181
|
-
rule rewrites code, and a host's own `derive` has no `const` type parameter to make the assertion redundant, so
|
|
182
|
-
removing it there would change the inferred types in silence.
|
|
183
|
-
|
|
184
|
-
### `require-declared-models`
|
|
185
|
-
|
|
186
|
-
A component that reads a per-mount UI model declares that model with `requiresModels`. Owner models remain
|
|
187
|
-
available to the contribution subtree without this declaration (D158, D250).
|
|
188
|
-
|
|
189
|
-
```tsx
|
|
190
|
-
import { defineFeature } from '@opetope/runtime';
|
|
191
|
-
import { requiresModels, useModel } from '@opetope/react';
|
|
192
|
-
|
|
193
|
-
const Form = () => {
|
|
194
|
-
const actions = useModel(FormActions);
|
|
195
|
-
return null;
|
|
196
|
-
};
|
|
197
|
-
const DeclaredForm = requiresModels([FormActions])(Form);
|
|
198
|
-
|
|
199
|
-
defineFeature('example.form', {
|
|
200
|
-
provides: ({ slot }) => ({
|
|
201
|
-
form: slot(FormSlot, ({ model }) => ({
|
|
202
|
-
Component: DeclaredForm,
|
|
203
|
-
models: [model(FormActions, createActions)],
|
|
204
|
-
})),
|
|
205
|
-
}),
|
|
206
|
-
});
|
|
207
|
-
```
|
|
208
|
-
|
|
209
|
-
The rule checks contributions passed to the `slot` capability of a visible `defineFeature` or
|
|
210
|
-
`defineFeature.body` `provides` callback. A local component receiving UI models must declare its requirements;
|
|
211
|
-
each visible component or hook that reads one of those models declares its own list. Wrapping a parent does not
|
|
212
|
-
declare the requirements of a nested function.
|
|
213
|
-
|
|
214
|
-
Named and namespace imports, import aliases, local aliases, a named `provides` context, and both inline and named
|
|
215
|
-
components are recognized. Bindings are compared within their lexical scopes; an unrelated local `useModel`,
|
|
216
|
-
`requiresModels` or `slot` does not become an Opetope API by sharing its name. Named APIs from relative re-exports
|
|
217
|
-
are recognized by their exported names, without reading the other file.
|
|
218
|
-
|
|
219
|
-
**The boundary.** Analysis stays within one module and follows visible local declarations and factory returns.
|
|
220
|
-
Arbitrary objects with `Component` and `models` are not contribution sites. Imported component implementations,
|
|
221
|
-
dynamic model lists and opaque helpers remain unverified: silence is not proof that their model requirements are
|
|
222
|
-
complete. Model references must be locally visible identifiers or aliases; the rule does not inspect types or walk
|
|
223
|
-
the rendered React tree. It has no autofix, because choosing which model a component should read is an authoring
|
|
224
|
-
choice. Types and runtime authority checks continue to apply.
|
|
225
|
-
|
|
226
|
-
### `require-literal-id`
|
|
227
|
-
|
|
228
|
-
A declaration names itself with a string that is written, not built where the declaration stands. The rule reads
|
|
229
|
-
every declaration of the public vocabulary that names itself — `defineFeature`, `defineApplication`,
|
|
230
|
-
`defineCondition`, `defineHostContract`, `defineModel`, `definePort`, `defineSlot`, `defineSwitchSlot`,
|
|
231
|
-
`definePipe` and `defineRegistry`, and the four of the optional Navigation package: `defineScreen`,
|
|
232
|
-
`defineSurface`, `defineLink` and `defineLinkHandlers`. D280 gives them one form: the id is the first argument of
|
|
233
|
-
every one of them.
|
|
234
|
-
|
|
235
|
-
Written means a string literal, a template with no expressions, a name that resolves to an import or to a `const`
|
|
236
|
-
of the same module, and a property read from such a name. Built means anything the call site assembles: a template
|
|
237
|
-
with an expression, a concatenation, a call, or a name that resolves to a parameter.
|
|
238
|
-
|
|
239
|
-
A project that generates a set of declarations from a name says so once, by naming that factory:
|
|
240
|
-
|
|
241
|
-
```js
|
|
242
|
-
'opetope/require-literal-id': ['error', { allowInCallees: ['createSlots'] }],
|
|
243
|
-
```
|
|
244
|
-
|
|
245
|
-
The name is matched against the function the declaration is written in and against the call it is passed to, so
|
|
246
|
-
both shapes of a factory are covered.
|
|
31
|
+
## Documentation
|
|
247
32
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
`
|
|
33
|
+
The [reference](../runtime/docs/reference/lint.md) owns the detailed contract, options and failure semantics.
|
|
34
|
+
[Guides](../runtime/docs/guides/index.md) show individual tasks. Documentation is shipped with
|
|
35
|
+
`@opetope/runtime` in `docs/`, so an installed application can read it without access to this repository.
|
|
251
36
|
|
|
252
|
-
|
|
37
|
+
For a contributor checkout, use `npm run ci:type --workspace @opetope/lint` and
|
|
38
|
+
`npm run ci:test --workspace @opetope/lint` where provided. The root `npm run check` performs full acceptance;
|
|
39
|
+
[contributor commands](../runtime/docs/maintainers/contributing.md) describe the build and package checks.
|
|
253
40
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
the moment the declaration runs, and this rule says so before the code runs. A screen, a surface and a link handler
|
|
257
|
-
registry pass their id through the same `declarationId`, so this rule reads them too; `defineLink` is left out of it
|
|
258
|
-
because a link id is a canonical lower-case path segment of a URL, which the package checks itself and which no
|
|
259
|
-
reserved prefix applies to (D313).
|
|
41
|
+
<a id="opetopelint"></a>
|
|
42
|
+
[See @opetope/lint](../runtime/docs/reference/lint.md).
|
|
260
43
|
|
|
261
|
-
|
|
44
|
+
<a id="usage"></a>
|
|
45
|
+
[See Usage](../runtime/docs/reference/lint.md#lint-usage).
|
|
262
46
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
```
|
|
47
|
+
<a id="configs"></a>
|
|
48
|
+
[See Configs](../runtime/docs/reference/lint.md#configs).
|
|
266
49
|
|
|
267
|
-
|
|
268
|
-
|
|
50
|
+
<a id="recommended"></a>
|
|
51
|
+
[See `recommended`](../runtime/docs/reference/lint.md#recommended).
|
|
269
52
|
|
|
270
|
-
|
|
53
|
+
<a id="layers"></a>
|
|
54
|
+
[See `layers`](../runtime/docs/reference/lint.md#layers).
|
|
271
55
|
|
|
272
|
-
|
|
273
|
-
|
|
56
|
+
<a id="internalimports"></a>
|
|
57
|
+
[See `internalImports`](../runtime/docs/reference/lint.md#internalimports).
|
|
274
58
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
| `defineFeature` | integration — a feature composes the other two layers |
|
|
278
|
-
| `defineModel` | models, or the UI contracts that declare a mount model |
|
|
279
|
-
| `defineSlot`, `defineSwitchSlot`, `defineSurface`, `definePipe`, `defineRegistry`, `definePort`, `defineCondition`, `defineHostContract` | integration, or the UI contracts beside the component |
|
|
280
|
-
| `defineApplication` | none of them: the host bootstrap owns it |
|
|
59
|
+
<a id="rules"></a>
|
|
60
|
+
[See Rules](../runtime/docs/reference/lint.md#rules).
|
|
281
61
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
project's choice, and this rule holds whatever choice it declared.
|
|
62
|
+
<a id="no-internal-imports"></a>
|
|
63
|
+
[See `no-internal-imports`](../runtime/docs/reference/lint.md#no-internal-imports).
|
|
285
64
|
|
|
286
|
-
|
|
65
|
+
<a id="enabled-predicate"></a>
|
|
66
|
+
[See `enabled-predicate`](../runtime/docs/reference/lint.md#enabled-predicate).
|
|
287
67
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
`useModel(Declaration, (model, { read }) => ...)` (D205, D214).
|
|
68
|
+
<a id="define-feature-property-order"></a>
|
|
69
|
+
[See `define-feature-property-order`](../runtime/docs/reference/lint.md#define-feature-property-order).
|
|
291
70
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
stays: those run after render, and the value of that moment is the one they want.
|
|
71
|
+
<a id="no-redundant-const-tuple"></a>
|
|
72
|
+
[See `no-redundant-const-tuple`](../runtime/docs/reference/lint.md#no-redundant-const-tuple).
|
|
295
73
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
reads — including a contribution's model factory, which reads the props of its own mount.
|
|
74
|
+
<a id="require-declared-models"></a>
|
|
75
|
+
[See `require-declared-models`](../runtime/docs/reference/lint.md#require-declared-models).
|
|
299
76
|
|
|
300
|
-
|
|
77
|
+
<a id="require-literal-id"></a>
|
|
78
|
+
[See `require-literal-id`](../runtime/docs/reference/lint.md#require-literal-id).
|
|
301
79
|
|
|
302
|
-
|
|
303
|
-
`
|
|
304
|
-
`update(total, total.getSnapshot() + amount)`, alike. The read runs where the argument is written — before the
|
|
305
|
-
write, and on a state that may already have stopped, where `getSnapshot()` answers a value nothing will move again
|
|
306
|
-
— and the value it computed then lands on a state that may have moved since. The updater form has neither problem: it reads at write
|
|
307
|
-
time, and a write dropped because its context was aborted never calls the updater at all (D282, D288, D295).
|
|
80
|
+
<a id="id-naming"></a>
|
|
81
|
+
[See `id-naming`](../runtime/docs/reference/lint.md#id-naming).
|
|
308
82
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
update(total, previous => previous + amount);
|
|
312
|
-
```
|
|
83
|
+
<a id="layer-placement"></a>
|
|
84
|
+
[See `layer-placement`](../runtime/docs/reference/lint.md#layer-placement).
|
|
313
85
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
itself, or through a name of the same function that holds the read — `const current = s.getSnapshot()`,
|
|
317
|
-
`const { items } = s.getSnapshot()`, or a `let` nothing writes again. A `getSnapshot()` of another readable inside
|
|
318
|
-
the value is a read of another state and stays; so do a value that is already an updater, a read written anywhere
|
|
319
|
-
but in that value, and a second argument that is a spread, whose arguments are assembled somewhere else.
|
|
320
|
-
|
|
321
|
-
The fix writes the updater: the value becomes `previous => ...` with every read of that state replaced by the
|
|
322
|
-
parameter — a destructured field by the field of it, `previous.items` — and the `const` that held the read goes with
|
|
323
|
-
it, comment and all, where this value was its only reader. The parameter is named `previous`, or `snapshot` where an
|
|
324
|
-
enclosing scope already holds `previous`. Where the parameter would shadow a name the file has, where the named read
|
|
325
|
-
has another reader that stays behind, and wherever the read was taken apart or opened with a `let`, the same rewrite
|
|
326
|
-
arrives as a suggestion instead, because those are the rewrites a reader should look at. A value that cannot move
|
|
327
|
-
into a function body at all — one that writes an `await` or a `yield` there, assigns, increments or `delete`s —
|
|
328
|
-
gets the report and nothing else, and so does a read this value keeps for a later call.
|
|
329
|
-
|
|
330
|
-
**The boundary.** `update(S, ...S.getSnapshot()...)` is specific enough on its own, so the rule resolves no import
|
|
331
|
-
and reads no type: a host's own `update` of the same shape is reported too, and the fix is right for it only if it
|
|
332
|
-
accepts an updater as its second argument. A name is the binding it resolves to, but a member expression is the
|
|
333
|
-
path it spells, so two different objects written `this.state` are one state here. The rewrite moves the whole value
|
|
334
|
-
into a function body, so everything else the value called — `Date.now()`, a helper, a formatter — is computed at
|
|
335
|
-
write time instead of where it was written, and is not computed at all for a write the runtime drops; that is what
|
|
336
|
-
the updater form means, and it is worth reading once before the rewrite lands. A read the value reaches by any other
|
|
337
|
-
route — a helper that takes the state and reads it, a name declared in another function, a name the code writes
|
|
338
|
-
again, a value that is already an updater — is not seen, and silence is not proof that a value computed itself from
|
|
339
|
-
the previous one.
|
|
340
|
-
|
|
341
|
-
### `no-command-in-deps`
|
|
342
|
-
|
|
343
|
-
A command hook is a snapshot of the render it was read in: the object is rebuilt on every `inFlight` and `outcome`
|
|
344
|
-
change, while `run` keeps one identity for the life of the binding (D290). A dependency array that names the object
|
|
345
|
-
therefore fires on each of those changes: an effect re-runs, a memo is voided, and an effect that calls `run` from
|
|
346
|
-
it never settles — it starts the command, the status flips, the dependency changes, the cleanup runs, and the
|
|
347
|
-
effect runs again.
|
|
348
|
-
|
|
349
|
-
```tsx
|
|
350
|
-
const load = useCommand(Screen.load);
|
|
351
|
-
const unload = useCommand(Screen.unload);
|
|
352
|
-
|
|
353
|
-
useEffect(() => {
|
|
354
|
-
load.run();
|
|
355
|
-
return () => void unload.run();
|
|
356
|
-
}, [load.run, unload.run]);
|
|
357
|
-
```
|
|
86
|
+
<a id="no-snapshot-read-in-render"></a>
|
|
87
|
+
[See `no-snapshot-read-in-render`](../runtime/docs/reference/lint.md#no-snapshot-read-in-render).
|
|
358
88
|
|
|
359
|
-
|
|
360
|
-
`
|
|
361
|
-
variable of a `useCommand`, a key or a destructured name of `useCommands`, and a field of a `useModel` selection
|
|
362
|
-
the same code reads `run` on. `x.run` and the statuses `x.inFlight` and `x.outcome` are members of that object rather
|
|
363
|
-
than the object, and they stay.
|
|
364
|
-
|
|
365
|
-
The record itself is the worse shape and carries its own report: `useCommands` builds a new object on every render,
|
|
366
|
-
and so does `useModel` with a selection, so `[hooks]` and `[order]` change on every render rather than on every
|
|
367
|
-
status. The answer is the field the callback reads — `hooks.save.run`.
|
|
368
|
-
|
|
369
|
-
The fix is written where the answer is one: a callback that reaches the binding through a single `run` gets that
|
|
370
|
-
path in place of the element, whatever the element was written past — an `as`, a `satisfies` or a `!` is replaced
|
|
371
|
-
along with it, and an array that already names the path loses the element instead of doubling it. Where the callback
|
|
372
|
-
reads more than one path, or reads a status, the rule offers suggestions instead — one per path, statuses first —
|
|
373
|
-
because which of them the dependency means is the author's to say. A callback this rule cannot read through — one
|
|
374
|
-
written elsewhere, one that keeps the object itself, takes it apart, or reads a member outside those four — gets the
|
|
375
|
-
report and no suggestion at all.
|
|
376
|
-
|
|
377
|
-
**The boundary.** Names are resolved by import within one module, not by spelling: `useCommand`, `useCommands` and
|
|
378
|
-
`useModel` are the imports of `@opetope/react`, the six hooks are React's own, and a local alias of either —
|
|
379
|
-
`import { useEffect as effect }` — is the same import. React is the module named `react` exactly; an `@opetope/*`
|
|
380
|
-
name is also taken from a relative import, which is how a package re-exports its own entry through a barrel, so a
|
|
381
|
-
neighbouring `./scheduling` is React's no more than `@host/scheduling` is. React's hooks are read through a
|
|
382
|
-
namespace or a default binding as well (`React.useEffect`); an `@opetope/*` entry has no default export, and only a
|
|
383
|
-
namespace reaches it. Only a `const` holds the hook it was opened with, so a `let` is left alone. A selected
|
|
384
|
-
`useModel` field is a hook only where the code proves it by reading `run` on it — a status name proves nothing,
|
|
385
|
-
since plain data carries one as easily — and a field read as data is of unknown kind and is left alone, as is
|
|
386
|
-
`useModel(Declaration)` without a selection, which keeps returning the granted model. A dependency array of a call
|
|
387
|
-
that is not one of the six hooks belongs to whoever declared it.
|
|
388
|
-
|
|
389
|
-
### `no-subscribe-outside-models`
|
|
390
|
-
|
|
391
|
-
A subscription written by hand owns a cleanup that nothing around it can see. The rule reports `x.subscribe(...)`
|
|
392
|
-
and stays off until `configs.layers` names the model layer that is allowed to hold one; in a component the reader
|
|
393
|
-
is `useReadable` or a model selection, and in a feature or a model it is `effect`, `event` or the `connect` of
|
|
394
|
-
`resource.live`.
|
|
395
|
-
|
|
396
|
-
Handing the function over without calling it is not a subscription written here:
|
|
397
|
-
`useSyncExternalStore(source.subscribe, source.getSnapshot)` passes a reference, and the reader owns what it starts.
|
|
398
|
-
|
|
399
|
-
Neither is a subscription written where a declared node opens its work and owns the release of it. The disposer
|
|
400
|
-
such a callback returns belongs to that node and is drained with the generation that opened it, so the rule stays
|
|
401
|
-
silent there — this is the shape its own message asks for (D299):
|
|
402
|
-
|
|
403
|
-
| Callback | Where | Ingress |
|
|
404
|
-
| ---------------------------------------------- | --------------------------- | ------- |
|
|
405
|
-
| `resource.live({ connect })` | the `connect` member | yes |
|
|
406
|
-
| `event(from, subscribe, run)` | second argument | yes |
|
|
407
|
-
| `scope.while` / `scope.switch` / `scope.keyed` | `open`, second argument | yes |
|
|
408
|
-
| `attach(source, { open })` | the `open` member | yes |
|
|
409
|
-
| `attach(source, { open, close })` | the `close` member | no |
|
|
410
|
-
| `apply`, `load`, `run`, `when`, `key` | modifiers of the same calls | no |
|
|
411
|
-
|
|
412
|
-
`resource.load` opens no subscription and has no ingress at all, so a `subscribe` written in its `load` is reported
|
|
413
|
-
like any other hand-written one (D349).
|
|
414
|
-
|
|
415
|
-
```ts
|
|
416
|
-
own: ({ imports, resource }) => ({
|
|
417
|
-
book: resource.live({
|
|
418
|
-
apply: { change: (_current: ResourceData<Quote>, change: Quote) => change },
|
|
419
|
-
connect: ({ emit, signal, source }) => source.repository.subscribe('BTCUSD', emit, signal),
|
|
420
|
-
lifetime: 'owner',
|
|
421
|
-
source: imports.exchange,
|
|
422
|
-
}),
|
|
423
|
-
});
|
|
424
|
-
```
|
|
89
|
+
<a id="no-snapshot-in-update"></a>
|
|
90
|
+
[See `no-snapshot-in-update`](../runtime/docs/reference/lint.md#no-snapshot-in-update).
|
|
425
91
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
right where a `defineFeature`/`defineFeature.body` **taken from `@opetope/runtime` itself** receives it, and a model
|
|
429
|
-
factory — the second argument of `model(Declaration, factory)` on that same `own`, or a function whose first
|
|
430
|
-
parameter is annotated `ModelContext` from `@opetope/core`. Unlike `no-command-in-deps`, these two anchors take the
|
|
431
|
-
package name and nothing else: a relative import here would let a host's own `./my-own-context` silence the rule.
|
|
432
|
-
`own.resource.live(...)` and a destructured `resource.live(...)` read alike, an alias answers with the key it was
|
|
433
|
-
taken from (`{ resource: materialize }` is still `resource`), and `own.scope.while(...)` reads like the destructured
|
|
434
|
-
`scope.while(...)`.
|
|
92
|
+
<a id="no-command-in-deps"></a>
|
|
93
|
+
[See `no-command-in-deps`](../runtime/docs/reference/lint.md#no-command-in-deps).
|
|
435
94
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
elsewhere and passed in by name, which is not lexically inside; an `own` section hoisted to its own binding
|
|
439
|
-
(`const own = ({ resource }) => …; defineFeature(id, { own })`); and `model(Decl, createOrderModel)` where the factory
|
|
440
|
-
is a named function without the `ModelContext` annotation.
|
|
95
|
+
<a id="no-subscribe-outside-models"></a>
|
|
96
|
+
[See `no-subscribe-outside-models`](../runtime/docs/reference/lint.md#no-subscribe-outside-models).
|
|
441
97
|
|
|
442
|
-
|
|
98
|
+
<a id="prefer-effect-current"></a>
|
|
99
|
+
[See `prefer-effect-current`](../runtime/docs/reference/lint.md#prefer-effect-current).
|
|
443
100
|
|
|
444
|
-
|
|
445
|
-
`source.
|
|
446
|
-
rewrites it to `execution.current` (D296, D323):
|
|
101
|
+
<a id="no-write-after-source-write"></a>
|
|
102
|
+
[See `no-write-after-source-write`](../runtime/docs/reference/lint.md#no-write-after-source-write).
|
|
447
103
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
ctx.effect(quotes, execution => publish(quotes.getSnapshot()));
|
|
451
|
-
// after
|
|
452
|
-
ctx.effect(quotes, execution => publish(execution.current));
|
|
453
|
-
```
|
|
104
|
+
<a id="capture-command-cleanup"></a>
|
|
105
|
+
[See `capture-command-cleanup`](../runtime/docs/reference/lint.md#capture-command-cleanup).
|
|
454
106
|
|
|
455
|
-
|
|
456
|
-
`
|
|
457
|
-
report stands alone.
|
|
458
|
-
|
|
459
|
-
**The two cases it reports without rewriting.** Past the first `await` of the run, `current` is the value this run
|
|
460
|
-
started with and the snapshot is the value of the moment; a change of the source normally restarts the run, so the
|
|
461
|
-
two normally agree — and «normally» is not a licence to rewrite. Under `when` they legitimately differ: the filter
|
|
462
|
-
admits some values and skips others, so the source moves without restarting the run (D168, D296). A modifier record
|
|
463
|
-
this rule cannot read — one assembled elsewhere — is treated as if it carried the predicate.
|
|
464
|
-
|
|
465
|
-
A read inside a nested callback of the run — a `timers.delay`, a subscription handler — is left alone: that code
|
|
466
|
-
runs later than the run does, and the value of that moment is the one it wants. Like every rule that rewrites code,
|
|
467
|
-
this one resolves its context exactly: the `effect` it acts on is the one declared on a `ModelContext` parameter or
|
|
468
|
-
on the context of a `model(Declaration, factory)` written inside a `defineFeature` of `@opetope/runtime` (D309).
|
|
469
|
-
|
|
470
|
-
### `capture-command-cleanup`
|
|
471
|
-
|
|
472
|
-
The writer of a command lives as long as its caller (D288). A write in the `finally` or the `catch` of a `try` that
|
|
473
|
-
awaits runs after the `await`, and once the call is cancelled it is dropped without a record — so the cleanup that
|
|
474
|
-
was supposed to clear a busy flag or a pending mark is exactly the write that never lands. The rule reports that write
|
|
475
|
-
and names the word that does land, a commit taken before the `await` (D319, D330):
|
|
476
|
-
|
|
477
|
-
```ts
|
|
478
|
-
// reported: once the caller is cancelled, `busy` stays `true`
|
|
479
|
-
ctx.call(async (input, { signal, update }) => {
|
|
480
|
-
update(busy, true);
|
|
481
|
-
try {
|
|
482
|
-
await host.send(input, signal);
|
|
483
|
-
} finally {
|
|
484
|
-
update(busy, false);
|
|
485
|
-
}
|
|
486
|
-
});
|
|
487
|
-
|
|
488
|
-
// the cleanup lands while the model lives
|
|
489
|
-
ctx.call(async (input, { capture, signal, update }) => {
|
|
490
|
-
update(busy, true);
|
|
491
|
-
const done = capture(busy);
|
|
492
|
-
try {
|
|
493
|
-
await host.send(input, signal);
|
|
494
|
-
} finally {
|
|
495
|
-
done.update(() => false);
|
|
496
|
-
}
|
|
497
|
-
});
|
|
498
|
-
```
|
|
107
|
+
<a id="prefer-model-selection"></a>
|
|
108
|
+
[See `prefer-model-selection`](../runtime/docs/reference/lint.md#prefer-model-selection).
|
|
499
109
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
both ways out. A cleanup that must run for a cancelled call goes through `capture(state)` taken before the `await`; an
|
|
503
|
-
answer that must not land after cancellation — a failure, a result — belongs in a `catch` after
|
|
504
|
-
`rethrowIfCancelled(cause)`. Where a caller cancels one run to start the next — a search its consumer issues again, a
|
|
505
|
-
Call an effect run invokes and a newer value cancels — the older run's write is exactly the one that should not land,
|
|
506
|
-
and a disable comment says so.
|
|
507
|
-
|
|
508
|
-
Under `policy: 'parallel'` the report does not advise a commit. Calls of such a command overlap, so a cancelled call's
|
|
509
|
-
captured cleanup would clear a flag another call still holds — a flag shared by parallel calls is racy either way —
|
|
510
|
-
and the report says to keep that state per call, or to leave progress to the consumer's `inFlight`. The rule sees the
|
|
511
|
-
policy only when `{ policy: 'parallel' }` is written on the `call` itself; a modifier record assembled elsewhere reads
|
|
512
|
-
as the default queue, where the lane waits for the physical body and a captured cleanup lands in order.
|
|
513
|
-
|
|
514
|
-
The rule is a heuristic over syntax, and it proves nothing about a write it does not report. It reads the writer of a
|
|
515
|
-
command body under these names: `execution.update(…)`; `({ update })` or `({ update: write })` in the parameter list;
|
|
516
|
-
`const { update } = execution` inside the body; and a `const` that gives one of those a second name —
|
|
517
|
-
`const write = update` or `const write = execution.update`. It follows a body passed as a local function. It misses a
|
|
518
|
-
closure that holds the writer (`const clear = () => update(busy, false)` called in `finally`), a writer stored anywhere
|
|
519
|
-
but a `const`, and a context handed to a helper. A `try` counts when its protected block awaits in the body's own
|
|
520
|
-
function: an `await` or a `for await` inside a nested callback suspends that callback, not the command.
|
|
521
|
-
|
|
522
|
-
In a `catch`, a write that follows `rethrowIfCancelled(cause)`, `signal.throwIfAborted()` or an `if (signal.aborted)`
|
|
523
|
-
that throws or returns — in the block that holds the write or in any block around it — is not reported. Such a guard
|
|
524
|
-
is the author's statement that what follows must not run for a cancelled call, and the rule takes it at its word: in
|
|
525
|
-
`catch (cause) { rethrowIfCancelled(cause); update(busy, false); throw cause }` the flag stays `true` on cancellation
|
|
526
|
-
and nothing is reported. A flag that must clear on cancellation is therefore written in `finally` through
|
|
527
|
-
`capture(state)`. A guard in a `finally` saves nothing and is still reported, and so is a check of `signal.aborted`
|
|
528
|
-
that neither throws nor returns. `effect`, `event` and the other reactions are left alone: a newer value is what
|
|
529
|
-
cancelled their run, and dropping its write is the point (D288). The context is resolved exactly: the `call` of a
|
|
530
|
-
`ModelContext` parameter or of the context of `model(Declaration, factory)` inside a `defineFeature` of
|
|
531
|
-
`@opetope/runtime` (D309); a feature's own `call` hands its body no writer.
|
|
532
|
-
|
|
533
|
-
### `prefer-model-selection`
|
|
534
|
-
|
|
535
|
-
A component that takes one granted model and reads its fields through a hook each repeats the same wiring. From
|
|
536
|
-
three fields upwards the rule points at one selection instead (D205, D214):
|
|
110
|
+
<a id="no-retired-vocabulary"></a>
|
|
111
|
+
[See `no-retired-vocabulary`](../runtime/docs/reference/lint.md#no-retired-vocabulary).
|
|
537
112
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
```
|
|
541
|
-
|
|
542
|
-
It counts `useReadable(model.field)` and `useCommand(model.field)` over the variable of a `useModel(Declaration)`
|
|
543
|
-
without a selection, and it counts each granted model separately: two models in one component are two grants, and
|
|
544
|
-
the same variable name in two components is two counts. There is no fix — the shape of the selection is the
|
|
545
|
-
author's to write.
|
|
546
|
-
|
|
547
|
-
## Coexistence with key sorting
|
|
548
|
-
|
|
549
|
-
A host that sorts object keys alphabetically will disagree with `define-feature-property-order`, because the
|
|
550
|
-
sections of a feature are ordered by meaning and not by name. `recommended` touches no sorting rule: reconciling
|
|
551
|
-
them belongs to the host, and there are two ways.
|
|
552
|
-
|
|
553
|
-
**`perfectionist/sort-objects`** takes a named order for exactly the two callees that declare sections, and keeps
|
|
554
|
-
its plain configuration for every other object:
|
|
555
|
-
|
|
556
|
-
```js
|
|
557
|
-
'perfectionist/sort-objects': [
|
|
558
|
-
'error',
|
|
559
|
-
{
|
|
560
|
-
customGroups: [
|
|
561
|
-
{ elementNamePattern: '^id$', groupName: 'feature-id' },
|
|
562
|
-
{ elementNamePattern: '^imports$', groupName: 'feature-imports' },
|
|
563
|
-
{ elementNamePattern: '^requires$', groupName: 'feature-requires' },
|
|
564
|
-
{ elementNamePattern: '^own$', groupName: 'feature-own' },
|
|
565
|
-
{ elementNamePattern: '^exports$', groupName: 'feature-exports' },
|
|
566
|
-
{ elementNamePattern: '^provides$', groupName: 'feature-provides' },
|
|
567
|
-
{ elementNamePattern: '^when$', groupName: 'feature-when' },
|
|
568
|
-
{ elementNamePattern: '^body$', groupName: 'feature-body' },
|
|
569
|
-
],
|
|
570
|
-
groups: [
|
|
571
|
-
'feature-id',
|
|
572
|
-
'feature-imports',
|
|
573
|
-
'feature-requires',
|
|
574
|
-
'feature-own',
|
|
575
|
-
'feature-exports',
|
|
576
|
-
'feature-provides',
|
|
577
|
-
'feature-when',
|
|
578
|
-
'feature-body',
|
|
579
|
-
'unknown',
|
|
580
|
-
],
|
|
581
|
-
order: 'asc',
|
|
582
|
-
type: 'natural',
|
|
583
|
-
useConfigurationIf: { callingFunctionNamePattern: '^(?:defineFeature|defineFeature\\.body)$' },
|
|
584
|
-
},
|
|
585
|
-
{ order: 'asc', type: 'natural' },
|
|
586
|
-
],
|
|
587
|
-
```
|
|
588
|
-
|
|
589
|
-
**ESLint core `sort-keys`, and the rule of the same id in Oxlint**, has no per-callee configuration and no
|
|
590
|
-
per-declaration exception: it reports each key that follows a larger one, wherever the object is. Turn it off for
|
|
591
|
-
the files that declare features, or wrap a declaration in `/* eslint-disable sort-keys */` and
|
|
592
|
-
`/* eslint-enable sort-keys */` — a `// eslint-disable-next-line sort-keys` covers one line, so it silences one
|
|
593
|
-
section, not a multi-line declaration. Oxlint reads the same directives under its own prefix,
|
|
594
|
-
`// oxlint-disable-next-line sort-keys`.
|
|
113
|
+
<a id="coexistence-with-key-sorting"></a>
|
|
114
|
+
[See Coexistence with key sorting](../runtime/docs/reference/lint.md#coexistence-with-key-sorting).
|
|
595
115
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
## Running the rules under Oxlint
|
|
600
|
-
|
|
601
|
-
Oxlint reads ESLint plugins through `jsPlugins`: it imports the module by path and takes its default export. The
|
|
602
|
-
rules stay on the plain rule API — `meta`, `create(context)` with visitors, `context.report` with a `fix`, and
|
|
603
|
-
`getText`/`getCommentsInside` on the source code — with no typed services and no ESLint-only capability, so both
|
|
604
|
-
linters run them, autofix included:
|
|
605
|
-
|
|
606
|
-
```json
|
|
607
|
-
{
|
|
608
|
-
"extends": ["./node_modules/@opetope/lint/oxlintrc.json"],
|
|
609
|
-
"jsPlugins": [{ "name": "opetope", "specifier": "./node_modules/@opetope/lint/dist/index.js" }]
|
|
610
|
-
}
|
|
611
|
-
```
|
|
116
|
+
<a id="running-the-rules-under-oxlint"></a>
|
|
117
|
+
[See Running the rules under Oxlint](../runtime/docs/reference/lint.md#running-the-rules-under-oxlint).
|
|
612
118
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
of being missed by a hand-written severity map. `extends` in `.oxlintrc.json` takes a **file path**, resolved
|
|
616
|
-
relative to the config that names it — not a package specifier — which is why the fragment sits at the root of the
|
|
617
|
-
package and the path a host writes is the same string as its export name. Configurations merge first to last, so a
|
|
618
|
-
rule the project wants stricter goes in its own `rules` after `extends`: the three `off` entries above are exactly
|
|
619
|
-
the ones a project turns on by its own directories.
|
|
620
|
-
|
|
621
|
-
The fragment carries rules and nothing else. Oxlint does merge a `jsPlugins` entry through `extends`, but a host
|
|
622
|
-
that declares the plugin itself — under a wrapper package, as an isolated peer install needs — would then register
|
|
623
|
-
the name `opetope` twice, and the second registration fails the whole configuration. Where the plugin comes from is
|
|
624
|
-
the host's deployment; the severities are this package's contract.
|
|
625
|
-
|
|
626
|
-
The alias `name` fixes the namespace, so a rule keeps the same id under either linter. `jsPlugins` is alpha and
|
|
627
|
-
outside semver: `npm run ci:test` runs the built plugin under Oxlint on a valid and an invalid fixture, through a
|
|
628
|
-
config that extends the shipped fragment, and checks the fix it writes, so a break in that bridge fails here
|
|
629
|
-
instead of in a host.
|
|
630
|
-
|
|
631
|
-
`--fix-suggestions` applies the first suggestion of a report without asking, so a suggestion here is only ever a
|
|
632
|
-
rewrite the reported code already justifies. `no-command-in-deps` offers the paths the callback itself reads, the
|
|
633
|
-
status it watches before the `run` it calls; where it cannot read the use through — the object passed on, taken
|
|
634
|
-
apart, or read past the members the rule knows — it reports and offers nothing, and the dependency stays as written
|
|
635
|
-
until its author changes it.
|
|
636
|
-
|
|
637
|
-
`no-snapshot-in-update` holds the same line from the other side: its suggestion is the fix it would not write
|
|
638
|
-
unasked — a parameter that shadows a name the file already has, or a named read that stays behind for its other
|
|
639
|
-
reader — and both rewrites leave the code doing what it did. Where the value cannot become the body of an updater
|
|
640
|
-
at all, there is no rewrite to offer and the report stands alone.
|
|
641
|
-
|
|
642
|
-
## Checks
|
|
643
|
-
|
|
644
|
-
From this package: `npm run ci:test`, `npm run ci:type`, `npm run ci:eslint`, `npm run build`. The Oxlint bridge
|
|
645
|
-
test reads `dist`, so `npm run build` comes first. `ci:test` also checks that the committed `oxlintrc.json` still
|
|
646
|
-
equals `configs.recommended.rules`; after changing that config run `npm run oxlintrc:generate` and commit the
|
|
647
|
-
result, which is why the build never writes it.
|
|
119
|
+
<a id="checks"></a>
|
|
120
|
+
[See contributor checks](../runtime/docs/maintainers/contributing.md).
|