@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.ru.md
DELETED
|
@@ -1,648 +0,0 @@
|
|
|
1
|
-
# @opetope/lint
|
|
2
|
-
|
|
3
|
-
Правила ESLint для кода, который пишет автор на Opetope. Плагин держит только то, что синтаксическая проверка может
|
|
4
|
-
доказать об объявлении: формы, которые компилируются и работают, но говорят не то, что имел в виду автор. То, на что
|
|
5
|
-
уже отвечают проверка типов, рантайм и анализ мёртвого кода, остаётся за ними (см. [`../docs/decisions.md`](https://www.npmjs.com/package/@opetope/runtime), D227).
|
|
6
|
-
|
|
7
|
-
Пакет не зависит ни от одного другого пакета `@opetope/*` и не читает типы, поэтому хост может линтовать исходники,
|
|
8
|
-
которые ещё не собирал.
|
|
9
|
-
|
|
10
|
-
## Установка
|
|
11
|
-
|
|
12
|
-
```sh
|
|
13
|
-
npm install --save-dev @opetope/lint 'eslint@^9' '@typescript-eslint/parser@^8'
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
Используйте согласованные версии Opetope. Для release candidate добавьте `@next` каждому пакету `@opetope/*` в команде.
|
|
17
|
-
API поставляется только в ESM; требуется Node 20.19+. Команды разработки ниже относятся к contributor checkout.
|
|
18
|
-
|
|
19
|
-
Нормативные руководства EN/RU поставляются в `@opetope/runtime`: после его установки откройте
|
|
20
|
-
`node_modules/@opetope/runtime/docs/spec.md` или `spec.ru.md`; рецепты находятся в `cookbook.md` и `cookbook.ru.md`.
|
|
21
|
-
Для чтения установленных руководств доступ к GitHub не нужен.
|
|
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
|
-
Плагин — это и default export, и именованный `opetopeLint`. Конфигурация регистрирует его в namespace `opetope`,
|
|
38
|
-
поэтому правило называется `opetope/when-predicate`. Для разбора нужен `@typescript-eslint/parser`; он и `eslint`
|
|
39
|
-
объявлены peer dependencies.
|
|
40
|
-
|
|
41
|
-
## Configs
|
|
42
|
-
|
|
43
|
-
### `recommended`
|
|
44
|
-
|
|
45
|
-
Все правила, которые действуют везде, где пишут на Opetope: `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` и `require-declared-models` как error. Правило, которому нужно знать, где хост держит свои файлы, здесь выключено и
|
|
48
|
-
приходит через `layers`; `prefer-model-selection` выключено потому, что число hooks, которое компонент вправе
|
|
49
|
-
держать, каждый проект решает для себя.
|
|
50
|
-
|
|
51
|
-
`configs.recommended.rules` целиком — и ровно это поставляется как `@opetope/lint/oxlintrc.json`:
|
|
52
|
-
|
|
53
|
-
| Правило | `recommended` | Включает |
|
|
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` | плюс сужение через `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` | проект, со своим `threshold` |
|
|
67
|
-
| `require-declared-models` | `error` | |
|
|
68
|
-
| `require-literal-id` | `error` | |
|
|
69
|
-
| `when-predicate` | `error` | |
|
|
70
|
-
|
|
71
|
-
Три `off` — это ровно те три, которым нужно знание, которое есть только у проекта: какие каталоги каким слоем
|
|
72
|
-
являются и сколько hooks вправе держать один компонент.
|
|
73
|
-
|
|
74
|
-
### `layers`
|
|
75
|
-
|
|
76
|
-
`opetope.configs.layers({ integration, models, ui, tests })` принимает глобы собственных слоёв проекта и
|
|
77
|
-
возвращает ту конфигурацию, которую каждый из них заслужил. Слой без глобов — это слой, которого у проекта нет, и
|
|
78
|
-
он не даёт ничего; файл, который не заявил ни один глоб, не размещает никто.
|
|
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` и `ui` включают `layer-placement` для своих файлов. `models` вместе с этим включает
|
|
89
|
-
`no-subscribe-outside-models` для всех исходников и отступает в самом слое моделей и в `tests`, у которых значение
|
|
90
|
-
по умолчанию `['**/__tests__/**', '**/*.spec.ts', '**/*.spec.tsx']`. Подписка покрыта и в файле, который не заявил
|
|
91
|
-
ни один слой: забытая подписка — это ровно то, как она переживает своего читателя.
|
|
92
|
-
|
|
93
|
-
### `internalImports`
|
|
94
|
-
|
|
95
|
-
`opetope.configs.internalImports({ allow, files })` настраивает `opetope/no-internal-imports` для JavaScript и
|
|
96
|
-
TypeScript, включая `.mjs`-тесты моделей. Правило уже включено в `recommended`; helper полезен для ограничения
|
|
97
|
-
области проверки или явного перечисления доверенных файлов реализации библиотеки и интеграции хоста.
|
|
98
|
-
`allow` выключает только это правило в указанных файлах. Размещайте блоки helper **после** `recommended`:
|
|
99
|
-
последующий блок `recommended` снова включает правило, в том числе для разрешённых файлов. Остальные ограничения
|
|
100
|
-
проекта продолжают действовать.
|
|
101
|
-
|
|
102
|
-
```js
|
|
103
|
-
export default [
|
|
104
|
-
opetope.configs.recommended,
|
|
105
|
-
...opetope.configs.internalImports({ allow: ['src/bootstrap/**/devtools.ts'] }),
|
|
106
|
-
];
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
Отдельное правило не изменяет `no-restricted-imports`. Прежний экспорт `internalImportPattern` удалён:
|
|
110
|
-
включайте это правило или helper вместо добавления паттерна Opetope в ограничения хоста (D261).
|
|
111
|
-
|
|
112
|
-
## Rules
|
|
113
|
-
|
|
114
|
-
### `no-internal-imports`
|
|
115
|
-
|
|
116
|
-
Код приложения и тесты импортируют публичные входы Opetope. Правило запрещает `@opetope/*/internal` и вложенные
|
|
117
|
-
пути в imports, re-exports, литеральных `import()`, незатенённых `require()`, TypeScript import types и
|
|
118
|
-
`import = require`. Type-only импорты тоже проверяются: внутренний тип связывает потребителя с приватным ABI.
|
|
119
|
-
Для запуска настоящего Call модели есть `runCall` из `@opetope/core/testing`, для открытия модели без фичи — `openModel` из `@opetope/runtime/testing`, для создания фикстуры — `command` из `@opetope/react/testing`.
|
|
120
|
-
Автоматического исключения для тестов нет. Доверенные файлы реализации библиотеки или интеграции хоста можно
|
|
121
|
-
разрешить явным override; autofix отсутствует, потому что публичная замена зависит от импортированной операции.
|
|
122
|
-
|
|
123
|
-
**Граница.** Проверка синтаксическая: строковые пути модулей и шаблоны без подстановок. Она не разрешает алиасы,
|
|
124
|
-
вычисляемые пути, граф re-exports или пользовательские загрузчики. Локальный `require` не считается загрузчиком
|
|
125
|
-
Node. Применяйте конфигурацию ко всем используемым JavaScript/TypeScript исходникам и тестам.
|
|
126
|
-
|
|
127
|
-
### `when-predicate`
|
|
128
|
-
|
|
129
|
-
`when` вклада отвечает фактом видимости этого экземпляра, а не источником, который этот факт несёт (D220). Правило
|
|
130
|
-
читает `when` у вкладов `slot`, `pipe` и `register`, а также любой `when`, чья функция деструктурирует контекст
|
|
131
|
-
вычисления `{ exports, imports, own, read }`, и сообщает о трёх формах:
|
|
132
|
-
|
|
133
|
-
| Написано | О чём сообщает |
|
|
134
|
-
| ------------------------------------------ | ------------------------------------------------------------------------------------- |
|
|
135
|
-
| `when: ({ imports }) => imports.x.allowed` | возвращает источник; фикс приводит к `({ imports, read }) => read(imports.x.allowed)` |
|
|
136
|
-
| `when: async ({ read }) => read(x)` | предикат отвечает синхронно; без фикса |
|
|
137
|
-
| `when: () => allowed` | `Readable<boolean>` передаётся в `when` как есть, без обёртки; без фикса |
|
|
138
|
-
|
|
139
|
-
Фикс оборачивает возвращённое обращение к полю в `read(...)` и добавляет `read` в деструктуризацию контекста, если
|
|
140
|
-
его там нет. Именованный контекст читается через себя: `context => context.imports.x.allowed` становится
|
|
141
|
-
`context => context.read(context.imports.x.allowed)`.
|
|
142
|
-
|
|
143
|
-
**Граница.** Правило читает формы, а не типы. `({ read }) => read(counter)` над не-boolean источником остаётся
|
|
144
|
-
ошибкой типов, как и `read` над тем, что не является `Readable`. Если обращение к полю контекста вычисления — это
|
|
145
|
-
обычное значение, а не источник, предикат не может изменить свой ответ, и правило сообщает о нём как о написанном
|
|
146
|
-
для источника; вынеси такое решение из `when` или заглуши строку. `when` вне вклада — позиционный
|
|
147
|
-
`(current, previous)` у `effect` или значение источника у `scope.while` — правило не трогает.
|
|
148
|
-
|
|
149
|
-
### `define-feature-property-order`
|
|
150
|
-
|
|
151
|
-
Фича объявляет свои секции в одном порядке: `imports`, `requires`, `own`, `exports`, `provides`, `when` и
|
|
152
|
-
загрузчик `body`, который заменяет три последние стадии (spec §2.1, D186). Идентичности среди них нет — фича
|
|
153
|
-
называет себя первым аргументом (D280). Порядок совпадает с порядком чтения объявления — рёбра, стадии, затем
|
|
154
|
-
время жизни и загрузчик, — поэтому читатель находит секцию по её месту, а сортировщик ключей можно настроить на
|
|
155
|
-
тот же порядок вместо второго.
|
|
156
|
-
|
|
157
|
-
Фикс переставляет свойства. Он отступает, если между двумя секциями стоит комментарий: такой комментарий не
|
|
158
|
-
принадлежит ни одной из них, и перестановка увела бы его от строки, которую он объясняет; комментарий внутри секции
|
|
159
|
-
переезжает вместе с ней. Объект с ключом, который не является секцией, или со spread остаётся проверке типов.
|
|
160
|
-
|
|
161
|
-
### `no-redundant-const-tuple`
|
|
162
|
-
|
|
163
|
-
`derive` принимает кортеж источников `const`-параметром типа, поэтому `[left, right]` выводит собственный кортеж, и
|
|
164
|
-
селектор уже видит точные значения. Написанный рядом `as const` утверждает то, что и так утверждает сигнатура
|
|
165
|
-
(D279, D309):
|
|
166
|
-
|
|
167
|
-
```ts
|
|
168
|
-
const summary = derive([total, label], (value, suffix) => `${value} ${suffix}`);
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
Правило читает первый аргумент `derive`, разрешённого к `@opetope/core`, и только там, где этот аргумент —
|
|
172
|
-
литерал массива с `as const`. Фикс убирает утверждение и ничего больше. Он отступает, если между кортежем и
|
|
173
|
-
утверждением стоит комментарий: такой комментарий не принадлежит ни одному из них, и удаление увело бы его вместе
|
|
174
|
-
с диапазоном.
|
|
175
|
-
|
|
176
|
-
**Граница.** Утверждение, написанное в другом месте, остаётся нетронутым: на одном элементе кортежа, где оно
|
|
177
|
-
говорит об этом элементе; на результате селектора; на записи опций; и на форме с одним источником, аргумент
|
|
178
|
-
которой вообще не кортеж. `as Sources` — другое утверждение и говорит другое. Импорт разрешается в пределах одного
|
|
179
|
-
модуля: локальный alias, `const`, который держит импорт, и namespace-привязка достигают одного и того же экспорта,
|
|
180
|
-
— а в отличие от `no-command-in-deps` правило узнаёт только импорт из `@opetope/core` и ничего больше: реэкспорт
|
|
181
|
-
через собственный barrel хоста оно не видит. Это осознанная цена за безопасный фикс. Правило правит код, а у
|
|
182
|
-
собственного `derive` хоста нет `const`-параметра типа, который делал бы утверждение лишним, — снятие `as const`
|
|
183
|
-
там молча изменило бы выведенные типы.
|
|
184
|
-
|
|
185
|
-
### `require-declared-models`
|
|
186
|
-
|
|
187
|
-
Компонент, читающий UI-модель отдельного mount, объявляет её через `requiresModels`. Owner-модели остаются
|
|
188
|
-
доступны всему subtree вклада без такого объявления (D158, D250).
|
|
189
|
-
|
|
190
|
-
```tsx
|
|
191
|
-
import { defineFeature } from '@opetope/runtime';
|
|
192
|
-
import { requiresModels, useModel } from '@opetope/react';
|
|
193
|
-
|
|
194
|
-
const Form = () => {
|
|
195
|
-
const actions = useModel(FormActions);
|
|
196
|
-
return null;
|
|
197
|
-
};
|
|
198
|
-
const DeclaredForm = requiresModels([FormActions])(Form);
|
|
199
|
-
|
|
200
|
-
defineFeature('example.form', {
|
|
201
|
-
provides: ({ slot }) => ({
|
|
202
|
-
form: slot(FormSlot, ({ model }) => ({
|
|
203
|
-
Component: DeclaredForm,
|
|
204
|
-
models: [model(FormActions, createActions)],
|
|
205
|
-
})),
|
|
206
|
-
}),
|
|
207
|
-
});
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
Правило проверяет вклады, переданные capability `slot` из видимого callback `provides` в `defineFeature` или
|
|
211
|
-
`defineFeature.body`. Локальный компонент, получающий UI-модели, объявляет свои требования; каждый видимый
|
|
212
|
-
компонент или hook, читающий одну из этих моделей, объявляет собственный список. Обёртка родителя не объявляет
|
|
213
|
-
требования вложенной функции.
|
|
214
|
-
|
|
215
|
-
Поддерживаются named и namespace imports, aliases импортов и локальных значений, именованный контекст `provides`,
|
|
216
|
-
inline и именованные компоненты. Bindings сравниваются в своих лексических scope: посторонняя локальная функция
|
|
217
|
-
`useModel`, `requiresModels` или `slot` не становится API Opetope из-за совпадения имени. Named API из относительных
|
|
218
|
-
реэкспортов распознаётся по экспортированным именам без чтения другого файла.
|
|
219
|
-
|
|
220
|
-
**Граница.** Анализ ограничен одним модулем и следует видимым локальным объявлениям и возвратам фабрик.
|
|
221
|
-
Произвольные объекты с `Component` и `models` не считаются вкладом. Реализации импортированных компонентов,
|
|
222
|
-
динамические списки моделей и непрозрачные helpers остаются непроверенными: отсутствие диагностики не доказывает
|
|
223
|
-
полноту их требований. Ссылки на модели должны быть видимыми локальными идентификаторами или aliases; правило не
|
|
224
|
-
читает типы и не обходит отрендеренное React-дерево. Autofix отсутствует: выбор модели для чтения принадлежит
|
|
225
|
-
автору. Проверки типов и runtime authority продолжают действовать.
|
|
226
|
-
|
|
227
|
-
### `require-literal-id`
|
|
228
|
-
|
|
229
|
-
Объявление называет себя строкой, которая написана, а не собрана там, где стоит объявление. Правило читает каждое
|
|
230
|
-
объявление публичного словаря, которое называет себя: `defineFeature`, `defineApplication`, `defineCondition`,
|
|
231
|
-
`defineHostContract`, `defineModel`, `definePort`, `defineSlot`, `defineSwitchSlot`, `definePipe` и
|
|
232
|
-
`defineRegistry`, — и четыре из необязательного пакета Navigation: `defineScreen`, `defineSurface`, `defineLink` и
|
|
233
|
-
`defineLinkHandlers`. D280 даёт им одну форму: id — первый аргумент у каждого.
|
|
234
|
-
|
|
235
|
-
Написана — это строковый литерал, шаблон без выражений, имя, которое разрешается в импорт или в `const` того же
|
|
236
|
-
модуля, и чтение поля у такого имени. Собрана — всё, что складывает место вызова: шаблон с выражением, конкатенация,
|
|
237
|
-
вызов функции или имя, которое разрешается в параметр.
|
|
238
|
-
|
|
239
|
-
Проект, который порождает набор объявлений по имени, говорит об этом один раз — называя такую фабрику:
|
|
240
|
-
|
|
241
|
-
```js
|
|
242
|
-
'opetope/require-literal-id': ['error', { allowInCallees: ['createSlots'] }],
|
|
243
|
-
```
|
|
244
|
-
|
|
245
|
-
Имя сверяется и с функцией, внутри которой написано объявление, и с вызовом, в который оно передано, поэтому
|
|
246
|
-
покрыты обе формы фабрики.
|
|
247
|
-
|
|
248
|
-
**Граница.** Имена правило отслеживает только внутри одного модуля: id, импортированный из другого файла, написан
|
|
249
|
-
там, и там же проверяется его собственный текст. `defineModule`, `defineCallTarget` и `defineCallLane` из ядра оно
|
|
250
|
-
не читает: их автор не пишет.
|
|
251
|
-
|
|
252
|
-
### `id-naming`
|
|
253
|
-
|
|
254
|
-
Id начинается с буквы и соединяет сегменты из букв и цифр одним из `.`, `/`, `:` или `-`, не длиннее 160 символов.
|
|
255
|
-
Это грамматика, которую принимает `declarationId` в `@opetope/core`; id вне неё — это `DeclarationError` в момент
|
|
256
|
-
выполнения объявления, а правило говорит об этом до запуска кода. Экран, поверхность и registry обработчиков ссылок
|
|
257
|
-
проходят через тот же `declarationId`, поэтому правило читает и их; `defineLink` из него исключён: id ссылки — это
|
|
258
|
-
канонический сегмент пути URL в нижнем регистре, который пакет проверяет сам и к которому зарезервированный префикс
|
|
259
|
-
неприменим (D313).
|
|
260
|
-
|
|
261
|
-
Проект, который зарезервировал первый сегмент, называет его, и каждое объявление таких файлов обязано с него
|
|
262
|
-
начинаться:
|
|
263
|
-
|
|
264
|
-
```js
|
|
265
|
-
'opetope/id-naming': ['error', { prefix: 'workspace' }],
|
|
266
|
-
```
|
|
267
|
-
|
|
268
|
-
**Граница.** Правило проверяет те id, чей текст видно в файле: литерал или имя, которое разрешается в литерал того
|
|
269
|
-
же модуля. Id, пришедший из другого модуля, проверяется там, где он написан.
|
|
270
|
-
|
|
271
|
-
### `layer-placement`
|
|
272
|
-
|
|
273
|
-
Каждое объявление написано в слое, который им владеет. Правило не сообщает ничего, пока проект не назвал свои слои
|
|
274
|
-
через `configs.layers`: имена каталогов не являются законом фреймворка.
|
|
275
|
-
|
|
276
|
-
| Объявление | Слой |
|
|
277
|
-
| ---------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
|
|
278
|
-
| `defineFeature` | integration — фича сочленяет два других слоя |
|
|
279
|
-
| `defineModel` | models или контракты UI, которые объявляют модель монтирования |
|
|
280
|
-
| `defineSlot`, `defineSwitchSlot`, `defineSurface`, `definePipe`, `defineRegistry`, `definePort`, `defineCondition`, `defineHostContract` | integration или контракты UI рядом с компонентом |
|
|
281
|
-
| `defineApplication` | ни один из них: им владеет bootstrap хоста |
|
|
282
|
-
|
|
283
|
-
**Это конвенция проекта, а не закон библиотеки.** Фреймворк говорит, что UI фичи импортирует только её собственные
|
|
284
|
-
контракты, а модель не знает о фиче; где лежат эти файлы — выбор проекта, и правило держит тот выбор, который он
|
|
285
|
-
объявил.
|
|
286
|
-
|
|
287
|
-
### `no-snapshot-read-in-render`
|
|
288
|
-
|
|
289
|
-
`getSnapshot()`, вызванный во время рендера компонента или хука, читает значение один раз и никогда не услышит
|
|
290
|
-
следующее. Читайте через `useReadable` или выбирайте вместе с командами той же модели:
|
|
291
|
-
`useModel(Declaration, (model, { read }) => ...)` (D205, D214).
|
|
292
|
-
|
|
293
|
-
Область рендера — это функция с именем `use…` либо функция с заглавной буквы, возвращающая элементы. Правило
|
|
294
|
-
смотрит на ту функцию, внутри которой написан вызов, поэтому снимок, прочитанный в обработчике события, эффекте
|
|
295
|
-
или любом другом вложенном колбэке, остаётся: они выполняются после рендера, и им нужно значение того момента.
|
|
296
|
-
|
|
297
|
-
**Граница.** Правило читает имя получателя, а не его тип: сообщается о каждом `getSnapshot()` в области рендера,
|
|
298
|
-
какому бы объекту он ни принадлежал. Функция с заглавной буквы, не возвращающая элементов, — это фабрика, и её
|
|
299
|
-
чтения остаются, включая фабрику модели вклада, которая читает props своего монтирования.
|
|
300
|
-
|
|
301
|
-
### `no-snapshot-in-update`
|
|
302
|
-
|
|
303
|
-
Значение, переданное в `update`, читает то состояние, которое собирается заменить, — и составное,
|
|
304
|
-
`update(sessions, { items: sessions.getSnapshot().items, kind: 'loading' })`, и скалярное,
|
|
305
|
-
`update(total, total.getSnapshot() + amount)`. Чтение выполняется там, где написан аргумент, — до записи и на
|
|
306
|
-
состоянии, которое могло уже остановиться, где `getSnapshot()` отвечает значением, которое больше никто не
|
|
307
|
-
сдвинет, — а вычисленное значение ложится на состояние, которое с тех пор могло уйти вперёд. У формы с апдейтером нет ни одной из этих бед: она
|
|
308
|
-
читает в момент записи, а у записи, отброшенной вместе с отменённым контекстом, апдейтер не вызывается вовсе
|
|
309
|
-
(D282, D288, D295).
|
|
310
|
-
|
|
311
|
-
```ts
|
|
312
|
-
update(sessions, previous => ({ ...previous, kind: 'loading' }));
|
|
313
|
-
update(total, previous => previous + amount);
|
|
314
|
-
```
|
|
315
|
-
|
|
316
|
-
Правило сообщает о вызове `update` — по этому имени или через контекст, `ctx.update`, — ровно с двумя аргументами,
|
|
317
|
-
у которого первым аргументом назван state, а значение читает `getSnapshot()` на нём же: написанный прямо в
|
|
318
|
-
значении или через имя той же функции, которое держит это чтение, — `const current = s.getSnapshot()`,
|
|
319
|
-
`const { items } = s.getSnapshot()` или `let`, который больше никто не переписывает. `getSnapshot()` другого
|
|
320
|
-
readable внутри значения — чтение другого состояния, и оно остаётся; остаются и значение, которое уже является
|
|
321
|
-
апдейтером, и чтение, написанное где угодно, кроме этого значения, и второй аргумент-spread, чьи аргументы
|
|
322
|
-
собраны в другом месте.
|
|
323
|
-
|
|
324
|
-
Фикс пишет апдейтер: значение становится `previous => ...`, где каждое чтение этого состояния заменено параметром —
|
|
325
|
-
а разобранное поле тем же полем параметра, `previous.items`, — и `const`, державший чтение, уходит вместе с ним, с
|
|
326
|
-
комментарием на той же строке, там, где это значение было его единственным читателем. Параметр называется
|
|
327
|
-
`previous`, а где `previous` уже занят объемлющей областью — `snapshot`. Там, где параметр затенил бы имя, которое
|
|
328
|
-
в файле есть, там, где у именованного чтения остаётся другой читатель, и везде, где чтение разобрано или открыто
|
|
329
|
-
через `let`, та же правка приходит suggestion'ом: это ровно те правки, на которые стоит посмотреть. Значение,
|
|
330
|
-
которое вообще нельзя перенести в тело функции — написанные в нём `await` или `yield`, присваивание, инкремент,
|
|
331
|
-
`delete`, — получает сообщение и ничего больше, и так же остаётся чтение, отложенное до более позднего вызова.
|
|
332
|
-
|
|
333
|
-
**Граница.** Форма `update(S, ...S.getSnapshot()...)` достаточно определённа сама по себе, поэтому правило не
|
|
334
|
-
разрешает импортов и не читает типов: собственный `update` хоста той же формы тоже получает сообщение, и фикс
|
|
335
|
-
верен для него только тогда, когда вторым аргументом он принимает апдейтер. Имя — это привязка, к которой оно
|
|
336
|
-
разрешается, а member expression — путь, который он пишет, поэтому два разных объекта, записанных как `this.state`,
|
|
337
|
-
здесь одно состояние. Правка переносит всё значение в тело функции, поэтому и всё остальное, что значение вызывало
|
|
338
|
-
— `Date.now()`, помощник, форматтер, — вычисляется в момент записи, а не там, где написано, и не вычисляется вовсе
|
|
339
|
-
для записи, которую рантайм отбросил; это и значит форма с апдейтером, и это стоит прочитать один раз до того, как
|
|
340
|
-
правка ляжет. Чтение, до которого значение дотягивается любым другим путём — помощник, который принимает state и
|
|
341
|
-
читает его сам, имя, объявленное в другой функции, имя, которое код переписывает, значение, уже являющееся
|
|
342
|
-
апдейтером, — не видно, и молчание правила не доказывает, что значение вычислено из предыдущего.
|
|
343
|
-
|
|
344
|
-
### `no-command-in-deps`
|
|
345
|
-
|
|
346
|
-
Хук команды — это снимок того рендера, в котором его прочитали: объект пересобирается на каждой смене `inFlight` и
|
|
347
|
-
`outcome`, а `run` держит одну идентичность всё время жизни привязки (D290). Массив зависимостей, который называет
|
|
348
|
-
этот объект, срабатывает поэтому на каждой такой смене: эффект перезапускается, memo обнуляется, а эффект, который
|
|
349
|
-
вызывает из него `run`, не останавливается никогда — он запускает команду, статус переключается, зависимость
|
|
350
|
-
меняется, выполняется уборка, и эффект запускается снова.
|
|
351
|
-
|
|
352
|
-
```tsx
|
|
353
|
-
const load = useCommand(Screen.load);
|
|
354
|
-
const unload = useCommand(Screen.unload);
|
|
355
|
-
|
|
356
|
-
useEffect(() => {
|
|
357
|
-
load.run();
|
|
358
|
-
return () => void unload.run();
|
|
359
|
-
}, [load.run, unload.run]);
|
|
360
|
-
```
|
|
361
|
-
|
|
362
|
-
Правило читает массив зависимостей `useEffect`, `useLayoutEffect`, `useInsertionEffect`, `useCallback`, `useMemo` и
|
|
363
|
-
`useImperativeHandle` и сообщает об элементе, который целиком является привязкой хука команды: о переменной
|
|
364
|
-
`useCommand`, о ключе или деструктурированном имени `useCommands` и о поле выбора `useModel`, на котором тот же код
|
|
365
|
-
читает `run`. `x.run` и статусы `x.inFlight` и `x.outcome` — это члены объекта, а не сам объект, и они остаются.
|
|
366
|
-
|
|
367
|
-
Сама запись — форма ещё хуже, и у неё собственное сообщение: `useCommands` строит новый объект на каждом рендере, и
|
|
368
|
-
то же делает `useModel` с выбором, поэтому `[hooks]` и `[order]` меняются на каждом рендере, а не на каждом
|
|
369
|
-
статусе. Ответ здесь — то поле, которое читает колбэк: `hooks.save.run`.
|
|
370
|
-
|
|
371
|
-
Фикс пишется там, где ответ один: колбэк, который дотягивается до привязки единственным путём через `run`, получает
|
|
372
|
-
на месте элемента этот путь — вместе со всем, через что элемент был написан: `as`, `satisfies` или `!` заменяются
|
|
373
|
-
вместе с ним, а массив, в котором этот путь уже есть, теряет элемент вместо того, чтобы удвоить его. Там, где
|
|
374
|
-
колбэк читает больше одного пути или читает статус, правило предлагает suggestions — по одной на путь, статусы
|
|
375
|
-
первыми, — потому что какой из них имеет в виду зависимость, говорит автор. Колбэк, сквозь который правило не
|
|
376
|
-
видит, — написанный в другом месте, держащий сам объект, разбирающий его или читающий член вне этих четырёх —
|
|
377
|
-
получает сообщение и ни одной suggestion.
|
|
378
|
-
|
|
379
|
-
**Граница.** Имена разрешаются импортом в пределах одного модуля, а не написанием: `useCommand`, `useCommands` и
|
|
380
|
-
`useModel` — это импорты `@opetope/react`, шесть хуков — собственные хуки React, а локальный алиас любого из них —
|
|
381
|
-
`import { useEffect as effect }` — это тот же импорт. React — это модуль, названный ровно `react`; имя `@opetope/*`
|
|
382
|
-
берётся ещё и из относительного импорта, потому что так пакет реэкспортирует собственный вход через barrel, и
|
|
383
|
-
соседний `./scheduling` — React не больше, чем `@host/scheduling`. Хуки React читаются и через namespace, и через
|
|
384
|
-
default-привязку (`React.useEffect`); у входа `@opetope/*` default-экспорта нет, и до него дотягивается только
|
|
385
|
-
namespace. Только `const` держит тот хук, которым его открыли, поэтому `let` остаётся в стороне. Выбранное поле
|
|
386
|
-
`useModel` — хук только там, где код доказал это чтением `run` на нём: имя статуса не доказывает ничего, такое имя
|
|
387
|
-
столь же легко носят обычные данные. Поле, прочитанное как данные, остаётся полем неизвестного рода, и так же
|
|
388
|
-
остаётся `useModel(Declaration)` без выбора, который по-прежнему возвращает выданную модель. Массив зависимостей
|
|
389
|
-
вызова, который не является одним из шести хуков, принадлежит тому, кто его объявил.
|
|
390
|
-
|
|
391
|
-
### `no-subscribe-outside-models`
|
|
392
|
-
|
|
393
|
-
Подписка, написанная руками, владеет уборкой, которой не видит ничто вокруг. Правило сообщает о `x.subscribe(...)`
|
|
394
|
-
и молчит, пока `configs.layers` не назвал слой моделей, которому такая подписка разрешена; в компоненте читатель —
|
|
395
|
-
это `useReadable` или выбор модели, а в фиче и в модели — `effect`, `event` или `connect` у `resource.live`.
|
|
396
|
-
|
|
397
|
-
Передача функции без вызова подпиской здесь не является:
|
|
398
|
-
`useSyncExternalStore(source.subscribe, source.getSnapshot)` передаёт ссылку, и читатель владеет тем, что начал.
|
|
399
|
-
|
|
400
|
-
Не является ею и подписка, написанная там, где объявленный узел открывает свою работу и сам её освобождает.
|
|
401
|
-
Disposer, который возвращает такой колбэк, принадлежит этому узлу и сливается вместе с поколением, которое его
|
|
402
|
-
открыло, — поэтому правило там молчит: это ровно та форма, которую предписывает его собственное сообщение (D299):
|
|
403
|
-
|
|
404
|
-
| Колбэк | Где | Ингресс |
|
|
405
|
-
| ---------------------------------------------- | --------------------------- | ------- |
|
|
406
|
-
| `resource.live({ connect })` | член `connect` | да |
|
|
407
|
-
| `event(from, subscribe, run)` | второй аргумент | да |
|
|
408
|
-
| `scope.while` / `scope.switch` / `scope.keyed` | `open`, второй аргумент | да |
|
|
409
|
-
| `attach(source, { open })` | член `open` | да |
|
|
410
|
-
| `attach(source, { open, close })` | член `close` | нет |
|
|
411
|
-
| `apply`, `load`, `run`, `when`, `key` | модификаторы тех же вызовов | нет |
|
|
412
|
-
|
|
413
|
-
`resource.load` не открывает подписки и ингресса не имеет вовсе, поэтому `subscribe`, написанный в его `load`,
|
|
414
|
-
сообщается как любая другая подписка руками (D349).
|
|
415
|
-
|
|
416
|
-
```ts
|
|
417
|
-
own: ({ imports, resource }) => ({
|
|
418
|
-
book: resource.live({
|
|
419
|
-
apply: { change: (_current: ResourceData<Quote>, change: Quote) => change },
|
|
420
|
-
connect: ({ emit, signal, source }) => source.repository.subscribe('BTCUSD', emit, signal),
|
|
421
|
-
lifetime: 'owner',
|
|
422
|
-
source: imports.exchange,
|
|
423
|
-
}),
|
|
424
|
-
});
|
|
425
|
-
```
|
|
426
|
-
|
|
427
|
-
«Внутри» считается до самого низа: вложенная стрелка, тело `function`, `await` на самой подписке, disposer,
|
|
428
|
-
которому сначала дали имя. Контекст разрешается импортом, а не написанием: секция `own`, написанная прямо там, где
|
|
429
|
-
её принимает `defineFeature`/`defineFeature.body`, взятый **из самого `@opetope/runtime`**, и фабрика модели —
|
|
430
|
-
второй аргумент `model(Declaration, factory)` на том же `own` либо функция, первый параметр которой аннотирован
|
|
431
|
-
`ModelContext` из `@opetope/core`. В отличие от `no-command-in-deps`, эти два якоря принимают только имя пакета:
|
|
432
|
-
относительный импорт здесь позволил бы собственному `./my-own-context` хоста заглушить правило.
|
|
433
|
-
`own.resource.live(...)` и деструктурированный `resource.live(...)` читаются одинаково, алиас отвечает тем ключом,
|
|
434
|
-
из которого его достали (`{ resource: materialize }` — это по-прежнему `resource`), а `own.scope.while(...)`
|
|
435
|
-
читается как деструктурированный `scope.while(...)`.
|
|
436
|
-
|
|
437
|
-
Поэтому сообщение остаётся в четырёх формах, и каждая — граница того, что доказывает синтаксис: `resource.live`,
|
|
438
|
-
который правило не проследило ни до одного якоря (импортированный откуда-то ещё, неразрешимый или принадлежащий чужому
|
|
439
|
-
`defineFeature`); колбэк, написанный в другом месте и переданный сюда именем, — он не стоит лексически внутри;
|
|
440
|
-
секция `own`, вынесенная в собственную привязку (`const own = ({ resource }) => …; defineFeature(id, { own })`); и
|
|
441
|
-
`model(Decl, createOrderModel)`, где фабрика — именованная функция без аннотации `ModelContext`.
|
|
442
|
-
|
|
443
|
-
### `prefer-effect-current`
|
|
444
|
-
|
|
445
|
-
`run` эффекта уже получил то значение, ради которого его запустили. Чтение того же источника через
|
|
446
|
-
`source.getSnapshot()` внутри этого прогона читает его второй раз и ничего не говорит о том, какой это прогон,
|
|
447
|
-
поэтому правило переписывает его в `execution.current` (D296, D323):
|
|
448
|
-
|
|
449
|
-
```js
|
|
450
|
-
// было
|
|
451
|
-
ctx.effect(quotes, execution => publish(quotes.getSnapshot()));
|
|
452
|
-
// стало
|
|
453
|
-
ctx.effect(quotes, execution => publish(execution.current));
|
|
454
|
-
```
|
|
455
|
-
|
|
456
|
-
Фикс пишет то слово, которое у прогона уже есть: имя контекста для `execution => …` и локальное имя, под которым
|
|
457
|
-
взят `current`, для `({ current }) => …`. Прогон, не назвавший ни того ни другого, писать нечем, и тогда остаётся
|
|
458
|
-
только сообщение.
|
|
459
|
-
|
|
460
|
-
**Два случая, о которых правило сообщает, но не переписывает.** После первого `await` в прогоне `current` — это
|
|
461
|
-
значение, с которым прогон начался, а снимок — значение текущего момента; смена источника обычно перезапускает
|
|
462
|
-
прогон, поэтому обычно они совпадают, — а «обычно» не даёт права на правку. Под `when` они расходятся законно:
|
|
463
|
-
фильтр допускает одни значения и пропускает другие, поэтому источник меняется без перезапуска прогона (D168, D296).
|
|
464
|
-
Запись модификаторов, которую правило прочитать не может — собранную в другом месте, — оно считает несущей предикат.
|
|
465
|
-
|
|
466
|
-
Чтение внутри вложенного колбэка прогона — в `timers.delay`, в обработчике подписки — остаётся как есть: этот код
|
|
467
|
-
выполняется позже прогона, и ему нужно значение того момента. Как и всякое правило, которое правит код, это
|
|
468
|
-
разрешает свой контекст точно: `effect`, о котором идёт речь, объявлен на параметре типа `ModelContext` либо на
|
|
469
|
-
контексте `model(Declaration, factory)`, написанном внутри `defineFeature` из `@opetope/runtime` (D309).
|
|
470
|
-
|
|
471
|
-
### `capture-command-cleanup`
|
|
472
|
-
|
|
473
|
-
Писатель команды живёт столько же, сколько её вызывающий (D288). Запись в `finally` или `catch` у `try`, который
|
|
474
|
-
ждёт `await`, выполняется после этого `await`, и после отмены вызова она отбрасывается без записи в reporter — так что
|
|
475
|
-
уборка, которая должна была снять флаг занятости или метку ожидания, и есть та запись, которая не ляжет никогда.
|
|
476
|
-
Правило сообщает о такой записи и называет слово, которое ляжет, — commit, взятый до `await` (D319, D330):
|
|
477
|
-
|
|
478
|
-
```ts
|
|
479
|
-
// сообщается: после отмены вызывающего `busy` остаётся `true`
|
|
480
|
-
ctx.call(async (input, { signal, update }) => {
|
|
481
|
-
update(busy, true);
|
|
482
|
-
try {
|
|
483
|
-
await host.send(input, signal);
|
|
484
|
-
} finally {
|
|
485
|
-
update(busy, false);
|
|
486
|
-
}
|
|
487
|
-
});
|
|
488
|
-
|
|
489
|
-
// уборка ложится, пока жива модель
|
|
490
|
-
ctx.call(async (input, { capture, signal, update }) => {
|
|
491
|
-
update(busy, true);
|
|
492
|
-
const done = capture(busy);
|
|
493
|
-
try {
|
|
494
|
-
await host.send(input, signal);
|
|
495
|
-
} finally {
|
|
496
|
-
done.update(() => false);
|
|
497
|
-
}
|
|
498
|
-
});
|
|
499
|
-
```
|
|
500
|
-
|
|
501
|
-
Фикса нет: должна ли уборка лечь после ухода вызывающего — решение автора, а сброс — закон не без причины: поздняя
|
|
502
|
-
запись отменённого вызова не должна затирать то, что написал более новый вызов. Поэтому сообщение называет оба выхода.
|
|
503
|
-
Уборка, которая обязана выполниться для отменённого вызова, идёт через `capture(state)`, взятый до `await`; ответ,
|
|
504
|
-
который не должен лечь после отмены, — отказ, результат, — стоит в `catch` после `rethrowIfCancelled(cause)`. Там, где
|
|
505
|
-
вызывающий отменяет один прогон, чтобы начать следующий, — поиск, который потребитель запускает заново, Call,
|
|
506
|
-
вызванный прогоном эффекта и отменённый более новым значением, — запись старого прогона и есть та, что лечь не должна,
|
|
507
|
-
и об этом говорит комментарий-отключение.
|
|
508
|
-
|
|
509
|
-
При `policy: 'parallel'` сообщение commit не советует. Вызовы такой команды перекрываются, поэтому захваченная уборка
|
|
510
|
-
отменённого вызова сняла бы флаг, который ещё держит другой вызов, — флаг, общий для параллельных вызовов, гонится в
|
|
511
|
-
любом случае, — и сообщение говорит держать такое состояние на вызов или оставить ход потребителю через `inFlight`.
|
|
512
|
-
Правило видит политику, только когда `{ policy: 'parallel' }` написан на самом `call`; запись модификаторов,
|
|
513
|
-
собранная в другом месте, читается как очередь по умолчанию, где lane ждёт физического тела и захваченная уборка
|
|
514
|
-
ложится по порядку.
|
|
515
|
-
|
|
516
|
-
Правило — эвристика над синтаксисом и ничего не доказывает о записи, о которой молчит. Писателя тела команды оно
|
|
517
|
-
читает под такими именами: `execution.update(…)`; `({ update })` или `({ update: write })` в списке параметров;
|
|
518
|
-
`const { update } = execution` внутри тела; и `const`, дающий одному из них второе имя, — `const write = update` или
|
|
519
|
-
`const write = execution.update`. Тело, переданное локальной функцией, правило находит. Оно пропускает замыкание,
|
|
520
|
-
держащее писателя (`const clear = () => update(busy, false)`, вызванное в `finally`), писателя, сохранённого не в
|
|
521
|
-
`const`, и контекст, переданный helper-у. `try` считается, когда его защищённый блок ждёт в собственной функции тела:
|
|
522
|
-
`await` или `for await` внутри вложенного колбэка приостанавливает этот колбэк, а не команду.
|
|
523
|
-
|
|
524
|
-
В `catch` запись после `rethrowIfCancelled(cause)`, `signal.throwIfAborted()` или `if (signal.aborted)`, который
|
|
525
|
-
бросает или возвращает, — в блоке, где стоит запись, или в любом охватывающем его блоке, — не сообщается. Такая
|
|
526
|
-
проверка — заявление автора, что следующее за ней не должно выполняться для отменённого вызова, и правило верит ему на
|
|
527
|
-
слово: в `catch (cause) { rethrowIfCancelled(cause); update(busy, false); throw cause }` флаг после отмены остаётся
|
|
528
|
-
`true`, и сообщения нет. Поэтому флаг, который обязан сняться при отмене, пишется в `finally` через `capture(state)`.
|
|
529
|
-
Проверка в `finally` ничего не спасает и сообщается по-прежнему, как и проверка `signal.aborted`, которая не бросает и
|
|
530
|
-
не возвращает. `effect`, `event` и остальные реакции правило не трогает: их прогон отменило более новое значение, и
|
|
531
|
-
сброс его записи — ровно то, что нужно (D288). Контекст разрешается точно: `call` параметра типа `ModelContext` либо
|
|
532
|
-
контекста `model(Declaration, factory)` внутри `defineFeature` из `@opetope/runtime` (D309); собственный `call` фичи
|
|
533
|
-
писателя телу не даёт.
|
|
534
|
-
|
|
535
|
-
### `prefer-model-selection`
|
|
536
|
-
|
|
537
|
-
Компонент, который берёт одну выданную модель и читает её поля по хуку на поле, повторяет одно и то же
|
|
538
|
-
подключение. От трёх полей и выше правило показывает на один выбор вместо них (D205, D214):
|
|
539
|
-
|
|
540
|
-
```js
|
|
541
|
-
'opetope/prefer-model-selection': ['error', { threshold: 3 }],
|
|
542
|
-
```
|
|
543
|
-
|
|
544
|
-
Оно считает `useReadable(model.field)` и `useCommand(model.field)` над переменной из `useModel(Declaration)` без
|
|
545
|
-
выбора и считает каждую выданную модель отдельно: две модели в одном компоненте — это два гранта, а одно и то же
|
|
546
|
-
имя переменной в двух компонентах — два счёта. Фикса нет: форму выбора пишет автор.
|
|
547
|
-
|
|
548
|
-
## Coexistence with key sorting
|
|
549
|
-
|
|
550
|
-
Хост, который сортирует ключи объектов по алфавиту, разойдётся с `define-feature-property-order`: секции фичи
|
|
551
|
-
упорядочены по смыслу, а не по имени. `recommended` не трогает ни одного правила сортировки — примирять их дело
|
|
552
|
-
хоста, и способа два.
|
|
553
|
-
|
|
554
|
-
**`perfectionist/sort-objects`** принимает именованный порядок ровно для двух callee, которые объявляют секции, и
|
|
555
|
-
сохраняет обычную конфигурацию для всех остальных объектов:
|
|
556
|
-
|
|
557
|
-
```js
|
|
558
|
-
'perfectionist/sort-objects': [
|
|
559
|
-
'error',
|
|
560
|
-
{
|
|
561
|
-
customGroups: [
|
|
562
|
-
{ elementNamePattern: '^id$', groupName: 'feature-id' },
|
|
563
|
-
{ elementNamePattern: '^imports$', groupName: 'feature-imports' },
|
|
564
|
-
{ elementNamePattern: '^requires$', groupName: 'feature-requires' },
|
|
565
|
-
{ elementNamePattern: '^own$', groupName: 'feature-own' },
|
|
566
|
-
{ elementNamePattern: '^exports$', groupName: 'feature-exports' },
|
|
567
|
-
{ elementNamePattern: '^provides$', groupName: 'feature-provides' },
|
|
568
|
-
{ elementNamePattern: '^when$', groupName: 'feature-when' },
|
|
569
|
-
{ elementNamePattern: '^body$', groupName: 'feature-body' },
|
|
570
|
-
],
|
|
571
|
-
groups: [
|
|
572
|
-
'feature-id',
|
|
573
|
-
'feature-imports',
|
|
574
|
-
'feature-requires',
|
|
575
|
-
'feature-own',
|
|
576
|
-
'feature-exports',
|
|
577
|
-
'feature-provides',
|
|
578
|
-
'feature-when',
|
|
579
|
-
'feature-body',
|
|
580
|
-
'unknown',
|
|
581
|
-
],
|
|
582
|
-
order: 'asc',
|
|
583
|
-
type: 'natural',
|
|
584
|
-
useConfigurationIf: { callingFunctionNamePattern: '^(?:defineFeature|defineFeature\\.body)$' },
|
|
585
|
-
},
|
|
586
|
-
{ order: 'asc', type: 'natural' },
|
|
587
|
-
],
|
|
588
|
-
```
|
|
589
|
-
|
|
590
|
-
**ESLint core `sort-keys` и правило с тем же id в Oxlint** не настраиваются по callee и не знают исключения на
|
|
591
|
-
объявление: они сообщают о каждом ключе, который идёт после большего, в любом объекте. Выключите правило для
|
|
592
|
-
файлов, объявляющих фичи, или оберните объявление в `/* eslint-disable sort-keys */` и
|
|
593
|
-
`/* eslint-enable sort-keys */` — `// eslint-disable-next-line sort-keys` покрывает одну строку, то есть глушит
|
|
594
|
-
одну секцию, а не многострочное объявление. Oxlint читает те же директивы под своим префиксом,
|
|
595
|
-
`// oxlint-disable-next-line sort-keys`.
|
|
596
|
-
|
|
597
|
-
Фикс этого правила переставляет ключи только внутри объекта вызова `defineFeature`, поэтому он никогда не трогает
|
|
598
|
-
то, чем правило сортировки владеет в остальном файле.
|
|
599
|
-
|
|
600
|
-
## Running the rules under Oxlint
|
|
601
|
-
|
|
602
|
-
Oxlint читает ESLint-плагины через `jsPlugins`: импортирует модуль по пути и берёт его default export. Правила
|
|
603
|
-
остаются на чистом rule API — `meta`, `create(context)` с visitors, `context.report` с `fix`, `getText` и
|
|
604
|
-
`getCommentsInside` у source code — без typed services и без ESLint-only возможностей, поэтому их выполняют оба
|
|
605
|
-
линтера, вместе с автофиксом:
|
|
606
|
-
|
|
607
|
-
```json
|
|
608
|
-
{
|
|
609
|
-
"extends": ["./node_modules/@opetope/lint/oxlintrc.json"],
|
|
610
|
-
"jsPlugins": [{ "name": "opetope", "specifier": "./node_modules/@opetope/lint/dist/index.js" }]
|
|
611
|
-
}
|
|
612
|
-
```
|
|
613
|
-
|
|
614
|
-
Это две строки, и ни одна из них не транскрипция. `@opetope/lint/oxlintrc.json` сгенерирован из
|
|
615
|
-
`configs.recommended.rules` и поставляется в пакете, поэтому правило, которое пакет добавил, приходит вместе с
|
|
616
|
-
обновлением, а не теряется в написанной руками severity-карте. `extends` в `.oxlintrc.json` принимает **путь
|
|
617
|
-
файла**, разрешаемый относительно конфига, который его назвал, — не specifier пакета; поэтому фрагмент лежит в корне
|
|
618
|
-
пакета, и путь, который пишет хост, совпадает с именем его экспорта. Конфигурации сливаются от первой к последней,
|
|
619
|
-
поэтому правило, которое проект хочет строже, он пишет в собственных `rules` после `extends`: три `off` выше — ровно
|
|
620
|
-
те, которые проект включает по своим каталогам.
|
|
621
|
-
|
|
622
|
-
Фрагмент несёт правила и ничего больше. Oxlint действительно сливает через `extends` и запись `jsPlugins`, но хост,
|
|
623
|
-
который объявляет плагин сам — под пакетом-обёрткой, как того требует изолированная установка peer-ов, — объявит имя
|
|
624
|
-
`opetope` второй раз, и это отвергнет всю конфигурацию. Откуда берётся плагин — деплой хоста; severities — контракт
|
|
625
|
-
этого пакета.
|
|
626
|
-
|
|
627
|
-
Алиас `name` фиксирует namespace, поэтому id правила одинаков в обоих линтерах. `jsPlugins` находится в alpha и вне
|
|
628
|
-
semver: `npm run ci:test` запускает собранный плагин под Oxlint на валидной и невалидной фикстуре — через конфиг,
|
|
629
|
-
который `extends`-ит поставляемый фрагмент, — и проверяет фикс, который тот пишет: поломка этого моста падает здесь,
|
|
630
|
-
а не у хоста.
|
|
631
|
-
|
|
632
|
-
`--fix-suggestions` применяет первую suggestion сообщения, ничего не спрашивая, поэтому suggestion здесь — только
|
|
633
|
-
такая правка, которую оправдывает сам прочитанный код. `no-command-in-deps` предлагает те пути, которые читает сам
|
|
634
|
-
колбэк: статус, за которым он следит, раньше `run`, который он вызывает. Там, где правило не видит использование
|
|
635
|
-
насквозь — объект передан дальше, разобран или прочитан за пределами известных ему членов, — оно сообщает и не
|
|
636
|
-
предлагает ничего, и зависимость остаётся написанной так, как её написали, пока её не изменит автор.
|
|
637
|
-
|
|
638
|
-
`no-snapshot-in-update` держит ту же линию с другой стороны: его suggestion — это тот самый фикс, который правило
|
|
639
|
-
не стало писать без спроса: параметр, затеняющий имя, которое в файле уже есть, или именованное чтение, которое
|
|
640
|
-
остаётся ради другого читателя. Обе правки оставляют код делать то же, что он делал. Там, где значение вообще не
|
|
641
|
-
может стать телом апдейтера, предлагать нечего, и сообщение стоит одно.
|
|
642
|
-
|
|
643
|
-
## Checks
|
|
644
|
-
|
|
645
|
-
Из этого пакета: `npm run ci:test`, `npm run ci:type`, `npm run ci:eslint`, `npm run build`. Тест моста Oxlint
|
|
646
|
-
читает `dist`, поэтому `npm run build` идёт первым. `ci:test` заодно проверяет, что закоммиченный `oxlintrc.json`
|
|
647
|
-
всё ещё равен `configs.recommended.rules`; после изменения этого конфига выполните `npm run oxlintrc:generate` и
|
|
648
|
-
закоммитьте результат — именно поэтому файл не пишет сборка.
|