@opetope/lint 0.1.0
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 +5 -0
- package/LICENSE +21 -0
- package/README.md +335 -0
- package/README.ru.md +335 -0
- package/dist/ast.d.ts +18 -0
- package/dist/ast.js +2 -0
- package/dist/ast.js.map +1 -0
- package/dist/declarations.d.ts +8 -0
- package/dist/declarations.js +2 -0
- package/dist/declarations.js.map +1 -0
- package/dist/index.d.ts +103 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/model-bindings.d.ts +14 -0
- package/dist/model-bindings.js +2 -0
- package/dist/model-bindings.js.map +1 -0
- package/dist/rule.d.ts +9 -0
- package/dist/rule.js +2 -0
- package/dist/rule.js.map +1 -0
- package/dist/rules/define-feature-property-order.d.ts +5 -0
- package/dist/rules/define-feature-property-order.js +2 -0
- package/dist/rules/define-feature-property-order.js.map +1 -0
- package/dist/rules/id-naming.d.ts +8 -0
- package/dist/rules/id-naming.js +2 -0
- package/dist/rules/id-naming.js.map +1 -0
- package/dist/rules/layer-placement.d.ts +8 -0
- package/dist/rules/layer-placement.js +2 -0
- package/dist/rules/layer-placement.js.map +1 -0
- package/dist/rules/no-snapshot-read-in-render.d.ts +4 -0
- package/dist/rules/no-snapshot-read-in-render.js +2 -0
- package/dist/rules/no-snapshot-read-in-render.js.map +1 -0
- package/dist/rules/no-subscribe-outside-models.d.ts +4 -0
- package/dist/rules/no-subscribe-outside-models.js +2 -0
- package/dist/rules/no-subscribe-outside-models.js.map +1 -0
- package/dist/rules/prefer-model-selection.d.ts +7 -0
- package/dist/rules/prefer-model-selection.js +2 -0
- package/dist/rules/prefer-model-selection.js.map +1 -0
- package/dist/rules/require-declared-models.d.ts +5 -0
- package/dist/rules/require-declared-models.js +2 -0
- package/dist/rules/require-declared-models.js.map +1 -0
- package/dist/rules/require-literal-id.d.ts +7 -0
- package/dist/rules/require-literal-id.js +2 -0
- package/dist/rules/require-literal-id.js.map +1 -0
- package/dist/rules/when-predicate.d.ts +6 -0
- package/dist/rules/when-predicate.js +2 -0
- package/dist/rules/when-predicate.js.map +1 -0
- package/dist/static-value.d.ts +15 -0
- package/dist/static-value.js +2 -0
- package/dist/static-value.js.map +1 -0
- package/package.json +54 -0
package/CHANGELOG.md
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Aleksei Berezin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
# @opetope/lint
|
|
2
|
+
|
|
3
|
+
ESLint rules for the code an Opetope author writes. The plugin holds only what a syntactic check can prove about a
|
|
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.
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
npm install --save-dev @opetope/lint 'eslint@^9' '@typescript-eslint/parser@^8'
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Use matching Opetope versions. For release candidates, append `@next` to every `@opetope/*` package in the command.
|
|
17
|
+
The API is ESM-only; Node 20.19+ is required. Development check commands below apply to a contributor checkout.
|
|
18
|
+
|
|
19
|
+
The normative EN/RU guides are shipped in `@opetope/runtime`: after installing it, open
|
|
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.
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
```js
|
|
26
|
+
// eslint.config.mjs
|
|
27
|
+
import opetope from '@opetope/lint';
|
|
28
|
+
|
|
29
|
+
export default [
|
|
30
|
+
{
|
|
31
|
+
files: ['src/**/*.{ts,tsx}'],
|
|
32
|
+
...opetope.configs.recommended,
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The plugin is both the default export and the named `opetopeLint`. The configuration registers it under the
|
|
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-snapshot-read-in-render` and `require-declared-models` as errors. A rule that needs to know where a
|
|
47
|
+
host keeps its files is off here and arrives through `layers`; `prefer-model-selection` is off because the number
|
|
48
|
+
of hooks a component may keep is a taste a project settles for itself.
|
|
49
|
+
|
|
50
|
+
### `layers`
|
|
51
|
+
|
|
52
|
+
`opetope.configs.layers({ integration, models, ui, tests })` takes the globs of a project's own layers and returns
|
|
53
|
+
the configuration each of them earns. A layer without globs is a layer this project does not have, and it produces
|
|
54
|
+
nothing; a file no glob claims is placed by nobody.
|
|
55
|
+
|
|
56
|
+
```js
|
|
57
|
+
...opetope.configs.layers({
|
|
58
|
+
integration: ['src/features/*/integration/**/*.{ts,tsx}'],
|
|
59
|
+
models: ['src/features/*/models/**/*.ts'],
|
|
60
|
+
ui: ['src/features/*/ui/**/*.{ts,tsx}'],
|
|
61
|
+
}),
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`integration`, `models` and `ui` each turn on `layer-placement` for their files. `models` also turns on
|
|
65
|
+
`no-subscribe-outside-models` for every source file and steps aside in that layer and in `tests`, which defaults to
|
|
66
|
+
`['**/__tests__/**', '**/*.spec.ts', '**/*.spec.tsx']`. A subscription is covered even in a file no layer claims,
|
|
67
|
+
because forgetting one is how a subscription outlives its reader.
|
|
68
|
+
|
|
69
|
+
### `internalImports`
|
|
70
|
+
|
|
71
|
+
`opetope.configs.internalImports({ allow, files })` keeps `@opetope/*/internal` out of the code that is neither the
|
|
72
|
+
runtime nor its host integration. `allow` names the files that do that work — the restriction does not reach them,
|
|
73
|
+
so whatever else the project restricts there stays in force — and `files` is what the restriction covers, by
|
|
74
|
+
default every `.ts` and `.tsx`.
|
|
75
|
+
|
|
76
|
+
```js
|
|
77
|
+
...opetope.configs.internalImports({ allow: ['src/bootstrap/**/devtools.ts'] }),
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`no-restricted-imports` is a core rule with one value per file, and in a flat config the last entry that names it
|
|
81
|
+
wins. A project that already configures that rule takes `opetope.internalImportPattern` into its own entry instead
|
|
82
|
+
of spreading this configuration, which would replace that entry for every file it covers:
|
|
83
|
+
|
|
84
|
+
```js
|
|
85
|
+
'no-restricted-imports': ['error', { paths: myPaths, patterns: [...myPatterns, opetope.internalImportPattern] }],
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The files that integrate the host then get the same entry without that one pattern, so they keep every other
|
|
89
|
+
restriction the project makes.
|
|
90
|
+
|
|
91
|
+
## Rules
|
|
92
|
+
|
|
93
|
+
### `when-predicate`
|
|
94
|
+
|
|
95
|
+
A contribution's `when` answers the visibility fact of this instance, not the source that carries it (D220). The
|
|
96
|
+
rule reads the `when` of a `slot`, `pipe` or `register` contribution, and any `when` whose function destructures the
|
|
97
|
+
evaluation context `{ exports, imports, own, read }`, and reports three shapes:
|
|
98
|
+
|
|
99
|
+
| Written | Reported |
|
|
100
|
+
| ------------------------------------------ | ----------------------------------------------------------------------------- |
|
|
101
|
+
| `when: ({ imports }) => imports.x.allowed` | returns a source; fixed to `({ imports, read }) => read(imports.x.allowed)` |
|
|
102
|
+
| `when: async ({ read }) => read(x)` | a predicate answers synchronously; no fix |
|
|
103
|
+
| `when: () => allowed` | a `Readable<boolean>` is passed as `when` directly, without a wrapper; no fix |
|
|
104
|
+
|
|
105
|
+
The fix wraps the returned member expression in `read(...)` and adds `read` to the destructured context when it is
|
|
106
|
+
absent. A named context is read through itself: `context => context.imports.x.allowed` becomes
|
|
107
|
+
`context => context.read(context.imports.x.allowed)`.
|
|
108
|
+
|
|
109
|
+
**The boundary.** The rule reads shapes, not types. `({ read }) => read(counter)` over a non-boolean source stays a
|
|
110
|
+
type error, and so does a `read` of something that is not a `Readable`. Where a member access of the evaluation
|
|
111
|
+
context is a plain value rather than a source, the predicate cannot change its answer, and the rule reports it as
|
|
112
|
+
one written for a source; hoist that decision out of `when`, or silence the line. `when` outside a contribution —
|
|
113
|
+
the positional `(current, previous)` of `effect`, or the source value of `scope.while` — is left alone.
|
|
114
|
+
|
|
115
|
+
### `define-feature-property-order`
|
|
116
|
+
|
|
117
|
+
A feature declares its sections in one order: `id`, `imports`, `requires`, `own`, `exports`, `provides`, `when`,
|
|
118
|
+
and the `body` loader that replaces the last three stages (spec §2.1, D186). The order is the reading order of the
|
|
119
|
+
declaration — identity, the edges, the stages, then the lifetime and the loader — so a reader finds a section by
|
|
120
|
+
position, and a key sorter can be configured to hold the same order instead of a second one.
|
|
121
|
+
|
|
122
|
+
The fix reorders the properties. It stands down when a comment sits between two sections, because such a comment
|
|
123
|
+
belongs to neither and reordering would move it away from the line it explains; a comment inside a section travels
|
|
124
|
+
with it. An object with a key that is not a section, or with a spread, is left to the type checker.
|
|
125
|
+
|
|
126
|
+
### `require-declared-models`
|
|
127
|
+
|
|
128
|
+
A component that reads a per-mount UI model declares that model with `requiresModels`. Owner models remain
|
|
129
|
+
available to the contribution subtree without this declaration (D158, D250).
|
|
130
|
+
|
|
131
|
+
```tsx
|
|
132
|
+
import { defineFeature } from '@opetope/runtime';
|
|
133
|
+
import { requiresModels, useModel } from '@opetope/react';
|
|
134
|
+
|
|
135
|
+
const Form = () => {
|
|
136
|
+
const actions = useModel(FormActions);
|
|
137
|
+
return null;
|
|
138
|
+
};
|
|
139
|
+
const DeclaredForm = requiresModels([FormActions])(Form);
|
|
140
|
+
|
|
141
|
+
defineFeature({
|
|
142
|
+
id: 'example.form',
|
|
143
|
+
provides: ({ slot }) => ({
|
|
144
|
+
form: slot(FormSlot, ({ model }) => ({
|
|
145
|
+
Component: DeclaredForm,
|
|
146
|
+
models: [model(FormActions, createActions)],
|
|
147
|
+
})),
|
|
148
|
+
}),
|
|
149
|
+
});
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
The rule checks contributions passed to the `slot` capability of a visible `defineFeature` or
|
|
153
|
+
`defineFeature.body` `provides` callback. A local component receiving UI models must declare its requirements;
|
|
154
|
+
each visible component or hook that reads one of those models declares its own list. Wrapping a parent does not
|
|
155
|
+
declare the requirements of a nested function.
|
|
156
|
+
|
|
157
|
+
Named and namespace imports, import aliases, local aliases, a named `provides` context, and both inline and named
|
|
158
|
+
components are recognized. Bindings are compared within their lexical scopes; an unrelated local `useModel`,
|
|
159
|
+
`requiresModels` or `slot` does not become an Opetope API by sharing its name. Named APIs from relative re-exports
|
|
160
|
+
are recognized by their exported names, without reading the other file.
|
|
161
|
+
|
|
162
|
+
**The boundary.** Analysis stays within one module and follows visible local declarations and factory returns.
|
|
163
|
+
Arbitrary objects with `Component` and `models` are not contribution sites. Imported component implementations,
|
|
164
|
+
dynamic model lists and opaque helpers remain unverified: silence is not proof that their model requirements are
|
|
165
|
+
complete. Model references must be locally visible identifiers or aliases; the rule does not inspect types or walk
|
|
166
|
+
the rendered React tree. It has no autofix, because choosing which model a component should read is an authoring
|
|
167
|
+
choice. Types and runtime authority checks continue to apply.
|
|
168
|
+
|
|
169
|
+
### `require-literal-id`
|
|
170
|
+
|
|
171
|
+
A declaration names itself with a string that is written, not built where the declaration stands. The rule reads
|
|
172
|
+
every declaration of the public vocabulary that names itself — `defineFeature`, `defineApplication`,
|
|
173
|
+
`defineCondition`, `defineHostContract`, `defineModel`, `definePort`, `defineSlot`, `defineSwitchSlot`,
|
|
174
|
+
`definePipe` and `defineRegistry`. `defineHostContract`, `defineModel` and `definePort` take the id as their first
|
|
175
|
+
argument; the rest take the `id` key of their options.
|
|
176
|
+
|
|
177
|
+
Written means a string literal, a template with no expressions, a name that resolves to an import or to a `const`
|
|
178
|
+
of the same module, and a property read from such a name. Built means anything the call site assembles: a template
|
|
179
|
+
with an expression, a concatenation, a call, or a name that resolves to a parameter.
|
|
180
|
+
|
|
181
|
+
A project that generates a set of declarations from a name says so once, by naming that factory:
|
|
182
|
+
|
|
183
|
+
```js
|
|
184
|
+
'opetope/require-literal-id': ['error', { allowInCallees: ['createSlots'] }],
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
The name is matched against the function the declaration is written in and against the call it is passed to, so
|
|
188
|
+
both shapes of a factory are covered.
|
|
189
|
+
|
|
190
|
+
**The boundary.** The rule follows names inside one module only: an id imported from another file is written
|
|
191
|
+
there, and that is where its own text is checked. The kernel's `defineModule`, `defineCallTarget` and
|
|
192
|
+
`defineCallLane` are not read, because an author never writes them.
|
|
193
|
+
|
|
194
|
+
### `id-naming`
|
|
195
|
+
|
|
196
|
+
An id starts with a letter and joins segments of letters and digits with one of `.`, `/`, `:` or `-`, within 160
|
|
197
|
+
characters. That is the grammar `declarationId` accepts in `@opetope/core`; an id outside it is a `DeclarationError`
|
|
198
|
+
the moment the declaration runs, and this rule says so before the code runs.
|
|
199
|
+
|
|
200
|
+
A project that reserves a first segment names it, and every declaration of those files must open with it:
|
|
201
|
+
|
|
202
|
+
```js
|
|
203
|
+
'opetope/id-naming': ['error', { prefix: 'workspace' }],
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
**The boundary.** The rule checks the ids whose text it can see in the file — a literal, or a name that resolves to
|
|
207
|
+
one in the same module. An id that arrives from another module is checked where it is written.
|
|
208
|
+
|
|
209
|
+
### `layer-placement`
|
|
210
|
+
|
|
211
|
+
Each declaration is written in the layer that owns it. The rule reports nothing until a project names its layers
|
|
212
|
+
through `configs.layers`, because the names of directories are not a law of the framework:
|
|
213
|
+
|
|
214
|
+
| Declaration | Layer |
|
|
215
|
+
| ----------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
|
|
216
|
+
| `defineFeature` | integration — a feature composes the other two layers |
|
|
217
|
+
| `defineModel` | models, or the UI contracts that declare a mount model |
|
|
218
|
+
| `defineSlot`, `defineSwitchSlot`, `definePipe`, `defineRegistry`, `definePort`, `defineCondition`, `defineHostContract` | integration, or the UI contracts beside the component |
|
|
219
|
+
| `defineApplication` | none of them: the host bootstrap owns it |
|
|
220
|
+
|
|
221
|
+
**This is a convention of the project, not a law of the library.** The framework says a feature's UI imports only
|
|
222
|
+
that feature's own contracts, and that a model knows nothing about the feature; where those files live is the
|
|
223
|
+
project's choice, and this rule holds whatever choice it declared.
|
|
224
|
+
|
|
225
|
+
### `no-snapshot-read-in-render`
|
|
226
|
+
|
|
227
|
+
`getSnapshot()` called while a component or a hook renders reads the value once and never hears the next one. Read
|
|
228
|
+
it with `useReadable`, or select it together with the commands of the same model:
|
|
229
|
+
`useModel(Declaration, (model, { read }) => ...)` (D205, D214).
|
|
230
|
+
|
|
231
|
+
A render scope is a function named `use…`, or a capitalized function that returns elements. The rule looks at the
|
|
232
|
+
function the call is written in, so a snapshot read in an event handler, an effect or any other nested callback
|
|
233
|
+
stays: those run after render, and the value of that moment is the one they want.
|
|
234
|
+
|
|
235
|
+
**The boundary.** The rule reads the name of the receiver, not its type: every `getSnapshot()` in a render scope is
|
|
236
|
+
reported, whichever object it belongs to. A capitalized function that returns no element is a factory and keeps its
|
|
237
|
+
reads — including a contribution's model factory, which reads the props of its own mount.
|
|
238
|
+
|
|
239
|
+
### `no-subscribe-outside-models`
|
|
240
|
+
|
|
241
|
+
A subscription written by hand owns a cleanup that nothing around it can see. The rule reports `x.subscribe(...)`
|
|
242
|
+
and stays off until `configs.layers` names the model layer that is allowed to hold one; in a component the reader
|
|
243
|
+
is `useReadable` or a model selection, and in a feature or a model it is `effect`, `event` or `stream`.
|
|
244
|
+
|
|
245
|
+
Handing the function over without calling it is not a subscription written here:
|
|
246
|
+
`useSyncExternalStore(source.subscribe, source.getSnapshot)` passes a reference, and the reader owns what it starts.
|
|
247
|
+
|
|
248
|
+
### `prefer-model-selection`
|
|
249
|
+
|
|
250
|
+
A component that takes one granted model and reads its fields through a hook each repeats the same wiring. From
|
|
251
|
+
three fields upwards the rule points at one selection instead (D205, D214):
|
|
252
|
+
|
|
253
|
+
```js
|
|
254
|
+
'opetope/prefer-model-selection': ['error', { threshold: 3 }],
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
It counts `useReadable(model.field)` and `useCommand(model.field)` over the variable of a `useModel(Declaration)`
|
|
258
|
+
without a selection, and it counts each granted model separately: two models in one component are two grants, and
|
|
259
|
+
the same variable name in two components is two counts. There is no fix — the shape of the selection is the
|
|
260
|
+
author's to write.
|
|
261
|
+
|
|
262
|
+
## Coexistence with key sorting
|
|
263
|
+
|
|
264
|
+
A host that sorts object keys alphabetically will disagree with `define-feature-property-order`, because the
|
|
265
|
+
sections of a feature are ordered by meaning and not by name. `recommended` touches no sorting rule: reconciling
|
|
266
|
+
them belongs to the host, and there are two ways.
|
|
267
|
+
|
|
268
|
+
**`perfectionist/sort-objects`** takes a named order for exactly the two callees that declare sections, and keeps
|
|
269
|
+
its plain configuration for every other object:
|
|
270
|
+
|
|
271
|
+
```js
|
|
272
|
+
'perfectionist/sort-objects': [
|
|
273
|
+
'error',
|
|
274
|
+
{
|
|
275
|
+
customGroups: [
|
|
276
|
+
{ elementNamePattern: '^id$', groupName: 'feature-id' },
|
|
277
|
+
{ elementNamePattern: '^imports$', groupName: 'feature-imports' },
|
|
278
|
+
{ elementNamePattern: '^requires$', groupName: 'feature-requires' },
|
|
279
|
+
{ elementNamePattern: '^own$', groupName: 'feature-own' },
|
|
280
|
+
{ elementNamePattern: '^exports$', groupName: 'feature-exports' },
|
|
281
|
+
{ elementNamePattern: '^provides$', groupName: 'feature-provides' },
|
|
282
|
+
{ elementNamePattern: '^when$', groupName: 'feature-when' },
|
|
283
|
+
{ elementNamePattern: '^body$', groupName: 'feature-body' },
|
|
284
|
+
],
|
|
285
|
+
groups: [
|
|
286
|
+
'feature-id',
|
|
287
|
+
'feature-imports',
|
|
288
|
+
'feature-requires',
|
|
289
|
+
'feature-own',
|
|
290
|
+
'feature-exports',
|
|
291
|
+
'feature-provides',
|
|
292
|
+
'feature-when',
|
|
293
|
+
'feature-body',
|
|
294
|
+
'unknown',
|
|
295
|
+
],
|
|
296
|
+
order: 'asc',
|
|
297
|
+
type: 'natural',
|
|
298
|
+
useConfigurationIf: { callingFunctionNamePattern: '^(?:defineFeature|defineFeature\\.body)$' },
|
|
299
|
+
},
|
|
300
|
+
{ order: 'asc', type: 'natural' },
|
|
301
|
+
],
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
**ESLint core `sort-keys`, and the rule of the same id in Oxlint**, has no per-callee configuration and no
|
|
305
|
+
per-declaration exception: it reports each key that follows a larger one, wherever the object is. Turn it off for
|
|
306
|
+
the files that declare features, or wrap a declaration in `/* eslint-disable sort-keys */` and
|
|
307
|
+
`/* eslint-enable sort-keys */` — a `// eslint-disable-next-line sort-keys` covers one line, so it silences one
|
|
308
|
+
section, not a multi-line declaration. Oxlint reads the same directives under its own prefix,
|
|
309
|
+
`// oxlint-disable-next-line sort-keys`.
|
|
310
|
+
|
|
311
|
+
The fix of this rule moves keys only inside the object of a `defineFeature` call, so it never reorders anything a
|
|
312
|
+
sorting rule owns elsewhere in the file.
|
|
313
|
+
|
|
314
|
+
## Running the rules under Oxlint
|
|
315
|
+
|
|
316
|
+
Oxlint reads ESLint plugins through `jsPlugins`: it imports the module by path and takes its default export. The
|
|
317
|
+
rules stay on the plain rule API — `meta`, `create(context)` with visitors, `context.report` with a `fix`, and
|
|
318
|
+
`getText`/`getCommentsInside` on the source code — with no typed services and no ESLint-only capability, so both
|
|
319
|
+
linters run them, autofix included:
|
|
320
|
+
|
|
321
|
+
```json
|
|
322
|
+
{
|
|
323
|
+
"jsPlugins": [{ "name": "opetope", "specifier": "./node_modules/@opetope/lint/dist/index.js" }],
|
|
324
|
+
"rules": { "opetope/when-predicate": "error" }
|
|
325
|
+
}
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
The alias `name` fixes the namespace, so a rule keeps the same id under either linter. `jsPlugins` is alpha and
|
|
329
|
+
outside semver: `npm run ci:test` runs the built plugin under Oxlint on a valid and an invalid fixture and checks
|
|
330
|
+
the fix it writes, so a break in that bridge fails here instead of in a host.
|
|
331
|
+
|
|
332
|
+
## Checks
|
|
333
|
+
|
|
334
|
+
From this package: `npm run ci:test`, `npm run ci:type`, `npm run ci:eslint`, `npm run build`. The Oxlint bridge
|
|
335
|
+
test reads `dist`, so `npm run build` comes first.
|