@triggery/eslint-plugin 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 +9 -0
- package/LICENSE +21 -0
- package/README.md +96 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.js +530 -0
- package/dist/index.js.map +1 -0
- package/package.json +80 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# @triggery/eslint-plugin
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
First public preview release.
|
|
6
|
+
|
|
7
|
+
ESLint plugin for Triggery — catches mis-use of createTrigger / useEvent / useCondition / useAction, suggests named hooks, enforces handler size budgets.
|
|
8
|
+
|
|
9
|
+
See the [repository-level CHANGELOG](../../CHANGELOG.md#010--2026-05-16) for the full set of packages and the umbrella feature list. Future entries on this file are appended automatically by changesets.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Aleksey Skhomenko
|
|
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,96 @@
|
|
|
1
|
+
# @triggery/eslint-plugin
|
|
2
|
+
|
|
3
|
+
ESLint plugin for Triggery: catches mis-use of `createTrigger` and the hook-API, enforces conventions, and keeps trigger files readable as specs.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pnpm add -D @triggery/eslint-plugin
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Rules
|
|
10
|
+
|
|
11
|
+
| Rule | Recommended | Strict | Auto-fix |
|
|
12
|
+
|---------------------------|:-----------:|:-------------:|:--------:|
|
|
13
|
+
| `no-dynamic-id` | error | error | — |
|
|
14
|
+
| `no-event-cascade` | error | error | — |
|
|
15
|
+
| `hook-rules` | error | error | — |
|
|
16
|
+
| `exhaustive-conditions` | warn | error | — |
|
|
17
|
+
| `exhaustive-required` | warn | error | — |
|
|
18
|
+
| `max-handler-size` | warn (≤50) | error (≤30) | — |
|
|
19
|
+
| `max-ports-per-trigger` | warn (≤12) | error (≤8) | — |
|
|
20
|
+
| `prefer-named-hook` | off | warn (≥3) | — |
|
|
21
|
+
|
|
22
|
+
## Flat config (ESLint 9.x)
|
|
23
|
+
|
|
24
|
+
```js
|
|
25
|
+
// eslint.config.js
|
|
26
|
+
import triggery from '@triggery/eslint-plugin';
|
|
27
|
+
|
|
28
|
+
export default [
|
|
29
|
+
triggery.configs.recommended,
|
|
30
|
+
// Or, if you want everything dialled up:
|
|
31
|
+
// triggery.configs.strict,
|
|
32
|
+
];
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Cherry-picking individual rules
|
|
36
|
+
|
|
37
|
+
```js
|
|
38
|
+
import triggery from '@triggery/eslint-plugin';
|
|
39
|
+
|
|
40
|
+
export default [
|
|
41
|
+
{
|
|
42
|
+
plugins: { '@triggery': triggery },
|
|
43
|
+
rules: {
|
|
44
|
+
'@triggery/no-dynamic-id': 'error',
|
|
45
|
+
'@triggery/no-event-cascade': 'error',
|
|
46
|
+
'@triggery/max-handler-size': ['warn', { max: 40 }],
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Per-rule
|
|
53
|
+
|
|
54
|
+
### `no-dynamic-id`
|
|
55
|
+
|
|
56
|
+
`createTrigger({ id })` must be a string literal. Trigger ids are runtime registry keys, devtools action labels and graph-output anchors — they must be deterministic.
|
|
57
|
+
|
|
58
|
+
### `no-event-cascade`
|
|
59
|
+
|
|
60
|
+
Disallows calling `useEvent(...)` inside a `useAction(...)` handler. Cascades are allowed at runtime (up to `maxCascadeDepth`), but writing them inline hides cross-trigger control flow.
|
|
61
|
+
|
|
62
|
+
### `hook-rules`
|
|
63
|
+
|
|
64
|
+
Framework-neutral rules-of-hooks: `useEvent` / `useCondition` / `useAction` / `useInlineTrigger` must be called from a component (function whose name starts with an uppercase letter) or a custom hook (function whose name starts with `use[A-Z]`).
|
|
65
|
+
|
|
66
|
+
### `exhaustive-conditions`
|
|
67
|
+
|
|
68
|
+
If a trigger declares `required: ['user','settings']`, the same file must contain at least one `useCondition(<trigger>, 'user', ...)` and one `useCondition(<trigger>, 'settings', ...)`. Cross-file checks are intentionally out of scope to keep the rule fast and predictable.
|
|
69
|
+
|
|
70
|
+
### `exhaustive-required`
|
|
71
|
+
|
|
72
|
+
Every `createTrigger({...})` call must include an explicit `required:` key (use `required: []` if no conditions are required). Catches the common mistake of "I forgot the gate, the handler will run on every fire even when the world isn't ready".
|
|
73
|
+
|
|
74
|
+
### `max-handler-size` (configurable, default 50)
|
|
75
|
+
|
|
76
|
+
```js
|
|
77
|
+
'@triggery/max-handler-size': ['warn', { max: 50 }]
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Counts top-level statements in the handler body. If you hit the limit, consider the `extract-trigger` codemod from `@triggery/codemod`.
|
|
81
|
+
|
|
82
|
+
### `max-ports-per-trigger` (configurable)
|
|
83
|
+
|
|
84
|
+
```js
|
|
85
|
+
'@triggery/max-ports-per-trigger': ['warn', { maxEvents: 8, maxConditions: 8, maxTotal: 12 }]
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Caps the per-trigger port count to keep scenarios spec-like.
|
|
89
|
+
|
|
90
|
+
### `prefer-named-hook` (configurable, off by default)
|
|
91
|
+
|
|
92
|
+
```js
|
|
93
|
+
'@triggery/prefer-named-hook': ['warn', { threshold: 4 }]
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Once a file has `threshold` or more port calls, suggests switching from `useEvent(trigger, 'new-message')` to the named hook `useNewMessageEvent` (available via `trigger.namedHooks()`).
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import * as _typescript_eslint_utils_ts_eslint from '@typescript-eslint/utils/ts-eslint';
|
|
2
|
+
import { ESLint, Linter } from 'eslint';
|
|
3
|
+
|
|
4
|
+
declare const rules: {
|
|
5
|
+
'exhaustive-conditions': _typescript_eslint_utils_ts_eslint.RuleModule<"missing", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
|
|
6
|
+
name: string;
|
|
7
|
+
};
|
|
8
|
+
'exhaustive-required': _typescript_eslint_utils_ts_eslint.RuleModule<"missing", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
|
|
9
|
+
name: string;
|
|
10
|
+
};
|
|
11
|
+
'hook-rules': _typescript_eslint_utils_ts_eslint.RuleModule<"outside", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
|
|
12
|
+
name: string;
|
|
13
|
+
};
|
|
14
|
+
'max-handler-size': _typescript_eslint_utils_ts_eslint.RuleModule<"tooLarge", [{
|
|
15
|
+
max?: number;
|
|
16
|
+
}], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
|
|
17
|
+
name: string;
|
|
18
|
+
};
|
|
19
|
+
'max-ports-per-trigger': _typescript_eslint_utils_ts_eslint.RuleModule<"tooManyTotal" | "tooManyEvents" | "tooManyConditions", [{
|
|
20
|
+
maxEvents?: number;
|
|
21
|
+
maxConditions?: number;
|
|
22
|
+
maxTotal?: number;
|
|
23
|
+
}], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
|
|
24
|
+
name: string;
|
|
25
|
+
};
|
|
26
|
+
'no-dynamic-id': _typescript_eslint_utils_ts_eslint.RuleModule<"dynamic", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
|
|
27
|
+
name: string;
|
|
28
|
+
};
|
|
29
|
+
'no-event-cascade': _typescript_eslint_utils_ts_eslint.RuleModule<"cascade", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
|
|
30
|
+
name: string;
|
|
31
|
+
};
|
|
32
|
+
'prefer-named-hook': _typescript_eslint_utils_ts_eslint.RuleModule<"prefer", [{
|
|
33
|
+
threshold?: number;
|
|
34
|
+
}], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
|
|
35
|
+
name: string;
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
declare const recommended: Linter.Config;
|
|
39
|
+
declare const strict: Linter.Config;
|
|
40
|
+
declare const finalPlugin: ESLint.Plugin;
|
|
41
|
+
|
|
42
|
+
export { finalPlugin as default, recommended, rules, strict };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
import { ESLintUtils, AST_NODE_TYPES } from '@typescript-eslint/utils';
|
|
2
|
+
|
|
3
|
+
// src/rules/exhaustive-conditions.ts
|
|
4
|
+
var TRIGGERY_HOOK_NAMES = /* @__PURE__ */ new Set([
|
|
5
|
+
"useEvent",
|
|
6
|
+
"useCondition",
|
|
7
|
+
"useAction",
|
|
8
|
+
"useInlineTrigger"
|
|
9
|
+
]);
|
|
10
|
+
function isCreateTriggerCall(node) {
|
|
11
|
+
const callee = node.callee;
|
|
12
|
+
if (callee.type === AST_NODE_TYPES.Identifier && callee.name === "createTrigger") return true;
|
|
13
|
+
if (callee.type === AST_NODE_TYPES.MemberExpression && callee.property.type === AST_NODE_TYPES.Identifier && callee.property.name === "createTrigger") {
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
function getStringLiteralValue(node) {
|
|
19
|
+
if (!node) return null;
|
|
20
|
+
if (node.type === AST_NODE_TYPES.Literal && typeof node.value === "string") {
|
|
21
|
+
return node.value;
|
|
22
|
+
}
|
|
23
|
+
if (node.type === AST_NODE_TYPES.TemplateLiteral && node.expressions.length === 0 && node.quasis.length === 1 && node.quasis[0]) {
|
|
24
|
+
return node.quasis[0].value.cooked;
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
function toPascalCase(name) {
|
|
29
|
+
return name.split(/[-_\s]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
30
|
+
}
|
|
31
|
+
function findEnclosingFunction(node) {
|
|
32
|
+
let current = node.parent;
|
|
33
|
+
while (current) {
|
|
34
|
+
if (current.type === AST_NODE_TYPES.FunctionDeclaration || current.type === AST_NODE_TYPES.FunctionExpression || current.type === AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
35
|
+
return current;
|
|
36
|
+
}
|
|
37
|
+
current = current.parent;
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
function isComponentOrHookFunction(fn) {
|
|
42
|
+
const name = getFunctionName(fn);
|
|
43
|
+
if (!name) return false;
|
|
44
|
+
return /^[A-Z]/.test(name) || /^use[A-Z]/.test(name);
|
|
45
|
+
}
|
|
46
|
+
function getFunctionName(fn) {
|
|
47
|
+
if (fn.type === AST_NODE_TYPES.FunctionDeclaration && fn.id) return fn.id.name;
|
|
48
|
+
if (fn.type === AST_NODE_TYPES.FunctionExpression && fn.id) return fn.id.name;
|
|
49
|
+
const parent = fn.parent;
|
|
50
|
+
if (parent && parent.type === AST_NODE_TYPES.VariableDeclarator) {
|
|
51
|
+
const id = parent.id;
|
|
52
|
+
if (id.type === AST_NODE_TYPES.Identifier) return id.name;
|
|
53
|
+
}
|
|
54
|
+
if (parent && parent.type === AST_NODE_TYPES.Property) {
|
|
55
|
+
const key = parent.key;
|
|
56
|
+
if (key.type === AST_NODE_TYPES.Identifier) return key.name;
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/rules/exhaustive-conditions.ts
|
|
62
|
+
var createRule = ESLintUtils.RuleCreator(
|
|
63
|
+
(name) => `https://github.com/triggeryjs/triggery/blob/main/packages/eslint-plugin/docs/rules/${name}.md`
|
|
64
|
+
);
|
|
65
|
+
var exhaustiveConditions = createRule({
|
|
66
|
+
name: "exhaustive-conditions",
|
|
67
|
+
meta: {
|
|
68
|
+
type: "problem",
|
|
69
|
+
docs: {
|
|
70
|
+
description: "Ensure every `required` condition of a trigger has at least one `useCondition` registration in the same file."
|
|
71
|
+
},
|
|
72
|
+
messages: {
|
|
73
|
+
missing: "Trigger `{{trigger}}` declares `required: [..., '{{name}}', ...]`, but this file has no `useCondition({{trigger}}, '{{name}}', ...)`. The handler will always skip with `missing-required`."
|
|
74
|
+
},
|
|
75
|
+
schema: []
|
|
76
|
+
},
|
|
77
|
+
defaultOptions: [],
|
|
78
|
+
create(context) {
|
|
79
|
+
const triggers = [];
|
|
80
|
+
const provided = /* @__PURE__ */ new Map();
|
|
81
|
+
function record(triggerVar, conditionName) {
|
|
82
|
+
let set = provided.get(triggerVar);
|
|
83
|
+
if (!set) {
|
|
84
|
+
set = /* @__PURE__ */ new Set();
|
|
85
|
+
provided.set(triggerVar, set);
|
|
86
|
+
}
|
|
87
|
+
set.add(conditionName);
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
CallExpression(node) {
|
|
91
|
+
if (isCreateTriggerCall(node)) {
|
|
92
|
+
const parent = node.parent;
|
|
93
|
+
if (!parent || parent.type !== AST_NODE_TYPES.VariableDeclarator) return;
|
|
94
|
+
if (parent.id.type !== AST_NODE_TYPES.Identifier) return;
|
|
95
|
+
const varName = parent.id.name;
|
|
96
|
+
const config = node.arguments[0];
|
|
97
|
+
if (!config || config.type !== AST_NODE_TYPES.ObjectExpression) return;
|
|
98
|
+
const requiredProp = config.properties.find(
|
|
99
|
+
(p) => p.type === AST_NODE_TYPES.Property && p.key.type === AST_NODE_TYPES.Identifier && p.key.name === "required"
|
|
100
|
+
);
|
|
101
|
+
if (!requiredProp) return;
|
|
102
|
+
if (requiredProp.value.type !== AST_NODE_TYPES.ArrayExpression) return;
|
|
103
|
+
const required = [];
|
|
104
|
+
for (const el of requiredProp.value.elements) {
|
|
105
|
+
const name = getStringLiteralValue(el ?? void 0);
|
|
106
|
+
if (name) required.push(name);
|
|
107
|
+
}
|
|
108
|
+
if (required.length > 0) triggers.push({ varName, required, node: requiredProp.value });
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (node.callee.type === AST_NODE_TYPES.Identifier && node.callee.name === "useCondition" && node.arguments[0] && node.arguments[0].type === AST_NODE_TYPES.Identifier && node.arguments[1]) {
|
|
112
|
+
const triggerVar = node.arguments[0].name;
|
|
113
|
+
const conditionName = getStringLiteralValue(node.arguments[1]);
|
|
114
|
+
if (conditionName) record(triggerVar, conditionName);
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
"Program:exit"() {
|
|
118
|
+
for (const trigger of triggers) {
|
|
119
|
+
const set = provided.get(trigger.varName) ?? /* @__PURE__ */ new Set();
|
|
120
|
+
for (const name of trigger.required) {
|
|
121
|
+
if (!set.has(name)) {
|
|
122
|
+
context.report({
|
|
123
|
+
node: trigger.node,
|
|
124
|
+
messageId: "missing",
|
|
125
|
+
data: { trigger: trigger.varName, name }
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
var createRule2 = ESLintUtils.RuleCreator(
|
|
135
|
+
(name) => `https://github.com/triggeryjs/triggery/blob/main/packages/eslint-plugin/docs/rules/${name}.md`
|
|
136
|
+
);
|
|
137
|
+
var exhaustiveRequired = createRule2({
|
|
138
|
+
name: "exhaustive-required",
|
|
139
|
+
meta: {
|
|
140
|
+
type: "suggestion",
|
|
141
|
+
docs: {
|
|
142
|
+
description: "Require an explicit `required` array on every createTrigger call (use `required: []` to opt out)."
|
|
143
|
+
},
|
|
144
|
+
messages: {
|
|
145
|
+
missing: "Trigger config is missing `required: [...]`. Add it explicitly so the runtime gate is unambiguous (use `required: []` if no conditions are required)."
|
|
146
|
+
},
|
|
147
|
+
schema: []
|
|
148
|
+
},
|
|
149
|
+
defaultOptions: [],
|
|
150
|
+
create(context) {
|
|
151
|
+
return {
|
|
152
|
+
CallExpression(node) {
|
|
153
|
+
if (!isCreateTriggerCall(node)) return;
|
|
154
|
+
const config = node.arguments[0];
|
|
155
|
+
if (!config || config.type !== AST_NODE_TYPES.ObjectExpression) return;
|
|
156
|
+
const hasRequired = config.properties.some(
|
|
157
|
+
(p) => p.type === AST_NODE_TYPES.Property && p.key.type === AST_NODE_TYPES.Identifier && p.key.name === "required"
|
|
158
|
+
);
|
|
159
|
+
if (!hasRequired) context.report({ node: config, messageId: "missing" });
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
var createRule3 = ESLintUtils.RuleCreator(
|
|
165
|
+
(name) => `https://github.com/triggeryjs/triggery/blob/main/packages/eslint-plugin/docs/rules/${name}.md`
|
|
166
|
+
);
|
|
167
|
+
var hookRules = createRule3({
|
|
168
|
+
name: "hook-rules",
|
|
169
|
+
meta: {
|
|
170
|
+
type: "problem",
|
|
171
|
+
docs: {
|
|
172
|
+
description: "Allow Triggery hooks (useEvent/useCondition/useAction) only inside components or other hooks."
|
|
173
|
+
},
|
|
174
|
+
messages: {
|
|
175
|
+
outside: "`{{hook}}` may only be called from a component or a custom hook (function name starting with an uppercase letter or with `use[A-Z]`)."
|
|
176
|
+
},
|
|
177
|
+
schema: []
|
|
178
|
+
},
|
|
179
|
+
defaultOptions: [],
|
|
180
|
+
create(context) {
|
|
181
|
+
return {
|
|
182
|
+
CallExpression(node) {
|
|
183
|
+
if (node.callee.type !== AST_NODE_TYPES.Identifier) return;
|
|
184
|
+
const hook = node.callee.name;
|
|
185
|
+
if (!TRIGGERY_HOOK_NAMES.has(hook)) return;
|
|
186
|
+
const fn = findEnclosingFunction(node);
|
|
187
|
+
if (!fn || !isComponentOrHookFunction(fn)) {
|
|
188
|
+
context.report({ node, messageId: "outside", data: { hook } });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
var createRule4 = ESLintUtils.RuleCreator(
|
|
195
|
+
(name) => `https://github.com/triggeryjs/triggery/blob/main/packages/eslint-plugin/docs/rules/${name}.md`
|
|
196
|
+
);
|
|
197
|
+
var maxHandlerSize = createRule4({
|
|
198
|
+
name: "max-handler-size",
|
|
199
|
+
meta: {
|
|
200
|
+
type: "suggestion",
|
|
201
|
+
docs: {
|
|
202
|
+
description: "Limit the size of a trigger handler body (default 50 statements)."
|
|
203
|
+
},
|
|
204
|
+
messages: {
|
|
205
|
+
tooLarge: "Trigger handler has {{count}} top-level statements, which exceeds the limit of {{max}}. Consider splitting it (see the `extract-trigger` codemod) or moving logic into smaller actions."
|
|
206
|
+
},
|
|
207
|
+
schema: [
|
|
208
|
+
{
|
|
209
|
+
type: "object",
|
|
210
|
+
additionalProperties: false,
|
|
211
|
+
properties: {
|
|
212
|
+
max: { type: "integer", minimum: 1 }
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
]
|
|
216
|
+
},
|
|
217
|
+
defaultOptions: [{ max: 50 }],
|
|
218
|
+
create(context, [options]) {
|
|
219
|
+
const max = options.max ?? 50;
|
|
220
|
+
return {
|
|
221
|
+
CallExpression(node) {
|
|
222
|
+
if (!isCreateTriggerCall(node)) return;
|
|
223
|
+
const config = node.arguments[0];
|
|
224
|
+
if (!config || config.type !== AST_NODE_TYPES.ObjectExpression) return;
|
|
225
|
+
for (const prop of config.properties) {
|
|
226
|
+
if (prop.type !== AST_NODE_TYPES.Property) continue;
|
|
227
|
+
if (prop.key.type !== AST_NODE_TYPES.Identifier || prop.key.name !== "handler") continue;
|
|
228
|
+
const handler = prop.value;
|
|
229
|
+
let body = null;
|
|
230
|
+
if (handler.type === AST_NODE_TYPES.FunctionExpression || handler.type === AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
231
|
+
if (handler.body.type === AST_NODE_TYPES.BlockStatement) body = handler.body;
|
|
232
|
+
}
|
|
233
|
+
if (!body) return;
|
|
234
|
+
const count = body.body.length;
|
|
235
|
+
if (count > max) {
|
|
236
|
+
context.report({
|
|
237
|
+
node: handler,
|
|
238
|
+
messageId: "tooLarge",
|
|
239
|
+
data: { count, max }
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
var createRule5 = ESLintUtils.RuleCreator(
|
|
249
|
+
(name) => `https://github.com/triggeryjs/triggery/blob/main/packages/eslint-plugin/docs/rules/${name}.md`
|
|
250
|
+
);
|
|
251
|
+
var maxPortsPerTrigger = createRule5({
|
|
252
|
+
name: "max-ports-per-trigger",
|
|
253
|
+
meta: {
|
|
254
|
+
type: "suggestion",
|
|
255
|
+
docs: {
|
|
256
|
+
description: "Cap the number of events / required-conditions per trigger to keep scenarios spec-like."
|
|
257
|
+
},
|
|
258
|
+
messages: {
|
|
259
|
+
tooManyTotal: "Trigger has {{count}} ports declared in its config (limit: {{max}}). Consider splitting it.",
|
|
260
|
+
tooManyEvents: "Trigger lists {{count}} events (limit: {{max}}). Split scenarios that fire on different events.",
|
|
261
|
+
tooManyConditions: "Trigger requires {{count}} conditions (limit: {{max}}). Consider whether all are truly required, or move some to opt-in checks via `check.is()`."
|
|
262
|
+
},
|
|
263
|
+
schema: [
|
|
264
|
+
{
|
|
265
|
+
type: "object",
|
|
266
|
+
additionalProperties: false,
|
|
267
|
+
properties: {
|
|
268
|
+
maxEvents: { type: "integer", minimum: 1 },
|
|
269
|
+
maxConditions: { type: "integer", minimum: 1 },
|
|
270
|
+
maxTotal: { type: "integer", minimum: 1 }
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
]
|
|
274
|
+
},
|
|
275
|
+
defaultOptions: [{ maxEvents: 8, maxConditions: 8, maxTotal: 12 }],
|
|
276
|
+
create(context, [options]) {
|
|
277
|
+
const maxEvents = options.maxEvents ?? 8;
|
|
278
|
+
const maxConditions = options.maxConditions ?? 8;
|
|
279
|
+
const maxTotal = options.maxTotal ?? 12;
|
|
280
|
+
function countStringArrayProp(config, propName) {
|
|
281
|
+
const prop = config.properties.find(
|
|
282
|
+
(p) => p.type === AST_NODE_TYPES.Property && p.key.type === AST_NODE_TYPES.Identifier && p.key.name === propName
|
|
283
|
+
);
|
|
284
|
+
if (!prop) return 0;
|
|
285
|
+
if (prop.value.type !== AST_NODE_TYPES.ArrayExpression) return 0;
|
|
286
|
+
return prop.value.elements.length;
|
|
287
|
+
}
|
|
288
|
+
return {
|
|
289
|
+
CallExpression(node) {
|
|
290
|
+
if (!isCreateTriggerCall(node)) return;
|
|
291
|
+
const config = node.arguments[0];
|
|
292
|
+
if (!config || config.type !== AST_NODE_TYPES.ObjectExpression) return;
|
|
293
|
+
const events = countStringArrayProp(config, "events");
|
|
294
|
+
const required = countStringArrayProp(config, "required");
|
|
295
|
+
const total = events + required;
|
|
296
|
+
if (events > maxEvents) {
|
|
297
|
+
context.report({
|
|
298
|
+
node: config,
|
|
299
|
+
messageId: "tooManyEvents",
|
|
300
|
+
data: { count: events, max: maxEvents }
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
if (required > maxConditions) {
|
|
304
|
+
context.report({
|
|
305
|
+
node: config,
|
|
306
|
+
messageId: "tooManyConditions",
|
|
307
|
+
data: { count: required, max: maxConditions }
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
if (total > maxTotal) {
|
|
311
|
+
context.report({
|
|
312
|
+
node: config,
|
|
313
|
+
messageId: "tooManyTotal",
|
|
314
|
+
data: { count: total, max: maxTotal }
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
var createRule6 = ESLintUtils.RuleCreator(
|
|
322
|
+
(name) => `https://github.com/triggeryjs/triggery/blob/main/packages/eslint-plugin/docs/rules/${name}.md`
|
|
323
|
+
);
|
|
324
|
+
var noDynamicId = createRule6({
|
|
325
|
+
name: "no-dynamic-id",
|
|
326
|
+
meta: {
|
|
327
|
+
type: "problem",
|
|
328
|
+
docs: {
|
|
329
|
+
description: "Require createTrigger({ id }) to be a string literal."
|
|
330
|
+
},
|
|
331
|
+
messages: {
|
|
332
|
+
dynamic: "`createTrigger` id must be a string literal, not a dynamic expression. The id is used as a registry key, in devtools and in graph(), and must be deterministic."
|
|
333
|
+
},
|
|
334
|
+
schema: []
|
|
335
|
+
},
|
|
336
|
+
defaultOptions: [],
|
|
337
|
+
create(context) {
|
|
338
|
+
return {
|
|
339
|
+
CallExpression(node) {
|
|
340
|
+
if (!isCreateTriggerCall(node)) return;
|
|
341
|
+
const config = node.arguments[0];
|
|
342
|
+
if (!config || config.type !== AST_NODE_TYPES.ObjectExpression) return;
|
|
343
|
+
for (const prop of config.properties) {
|
|
344
|
+
if (prop.type !== AST_NODE_TYPES.Property) continue;
|
|
345
|
+
if (prop.key.type !== AST_NODE_TYPES.Identifier || prop.key.name !== "id") continue;
|
|
346
|
+
if (getStringLiteralValue(prop.value) === null) {
|
|
347
|
+
context.report({ node: prop.value, messageId: "dynamic" });
|
|
348
|
+
}
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
var createRule7 = ESLintUtils.RuleCreator(
|
|
356
|
+
(name) => `https://github.com/triggeryjs/triggery/blob/main/packages/eslint-plugin/docs/rules/${name}.md`
|
|
357
|
+
);
|
|
358
|
+
var noEventCascade = createRule7({
|
|
359
|
+
name: "no-event-cascade",
|
|
360
|
+
meta: {
|
|
361
|
+
type: "problem",
|
|
362
|
+
docs: {
|
|
363
|
+
description: "Disallow calling useEvent inside a useAction handler (implicit cascade)."
|
|
364
|
+
},
|
|
365
|
+
messages: {
|
|
366
|
+
cascade: "Avoid calling `{{callee}}` inside a `useAction` handler \u2014 this creates an implicit cascade. Compose triggers explicitly instead."
|
|
367
|
+
},
|
|
368
|
+
schema: []
|
|
369
|
+
},
|
|
370
|
+
defaultOptions: [],
|
|
371
|
+
create(context) {
|
|
372
|
+
const useActionHandlers = [];
|
|
373
|
+
function isUseActionHandlerArgument(node) {
|
|
374
|
+
const parent = node.parent;
|
|
375
|
+
if (!parent || parent.type !== AST_NODE_TYPES.CallExpression) return false;
|
|
376
|
+
if (parent.arguments[2] !== node) return false;
|
|
377
|
+
const callee = parent.callee;
|
|
378
|
+
return callee.type === AST_NODE_TYPES.Identifier && callee.name === "useAction";
|
|
379
|
+
}
|
|
380
|
+
function enterFn(node) {
|
|
381
|
+
if (isUseActionHandlerArgument(node)) useActionHandlers.push(node);
|
|
382
|
+
}
|
|
383
|
+
function exitFn(node) {
|
|
384
|
+
if (useActionHandlers[useActionHandlers.length - 1] === node) useActionHandlers.pop();
|
|
385
|
+
}
|
|
386
|
+
return {
|
|
387
|
+
FunctionExpression: enterFn,
|
|
388
|
+
ArrowFunctionExpression: enterFn,
|
|
389
|
+
FunctionDeclaration: enterFn,
|
|
390
|
+
"FunctionExpression:exit": exitFn,
|
|
391
|
+
"ArrowFunctionExpression:exit": exitFn,
|
|
392
|
+
"FunctionDeclaration:exit": exitFn,
|
|
393
|
+
CallExpression(node) {
|
|
394
|
+
if (useActionHandlers.length === 0) return;
|
|
395
|
+
if (node.callee.type !== AST_NODE_TYPES.Identifier) return;
|
|
396
|
+
if (node.callee.name !== "useEvent") return;
|
|
397
|
+
context.report({
|
|
398
|
+
node,
|
|
399
|
+
messageId: "cascade",
|
|
400
|
+
data: { callee: node.callee.name }
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
var createRule8 = ESLintUtils.RuleCreator(
|
|
407
|
+
(name) => `https://github.com/triggeryjs/triggery/blob/main/packages/eslint-plugin/docs/rules/${name}.md`
|
|
408
|
+
);
|
|
409
|
+
var preferNamedHook = createRule8({
|
|
410
|
+
name: "prefer-named-hook",
|
|
411
|
+
meta: {
|
|
412
|
+
type: "suggestion",
|
|
413
|
+
docs: {
|
|
414
|
+
description: 'In files with many port calls, prefer named hooks (useFooEvent) over generic useEvent("foo").'
|
|
415
|
+
},
|
|
416
|
+
messages: {
|
|
417
|
+
prefer: "Consider using `{{suggestion}}` instead of `{{generic}}` once a file has many ({{count}} \u2265 {{threshold}}) port calls."
|
|
418
|
+
},
|
|
419
|
+
schema: [
|
|
420
|
+
{
|
|
421
|
+
type: "object",
|
|
422
|
+
additionalProperties: false,
|
|
423
|
+
properties: {
|
|
424
|
+
threshold: { type: "integer", minimum: 1 }
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
]
|
|
428
|
+
},
|
|
429
|
+
defaultOptions: [{ threshold: 4 }],
|
|
430
|
+
create(context, [options]) {
|
|
431
|
+
const threshold = options.threshold ?? 4;
|
|
432
|
+
const portCalls = [];
|
|
433
|
+
function suffixFor(kind) {
|
|
434
|
+
switch (kind) {
|
|
435
|
+
case "useEvent":
|
|
436
|
+
return "Event";
|
|
437
|
+
case "useCondition":
|
|
438
|
+
return "Condition";
|
|
439
|
+
case "useAction":
|
|
440
|
+
return "Action";
|
|
441
|
+
default:
|
|
442
|
+
return "";
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
return {
|
|
446
|
+
CallExpression(node) {
|
|
447
|
+
if (node.callee.type !== AST_NODE_TYPES.Identifier) return;
|
|
448
|
+
const name = node.callee.name;
|
|
449
|
+
if (!["useEvent", "useCondition", "useAction"].includes(name)) return;
|
|
450
|
+
const portArg = node.arguments[1];
|
|
451
|
+
const portName = getStringLiteralValue(portArg);
|
|
452
|
+
if (!portName) return;
|
|
453
|
+
portCalls.push({ node, kind: name, name: portName });
|
|
454
|
+
},
|
|
455
|
+
"Program:exit"() {
|
|
456
|
+
if (portCalls.length < threshold) return;
|
|
457
|
+
for (const call of portCalls) {
|
|
458
|
+
const suggestion = `use${toPascalCase(call.name)}${suffixFor(call.kind)}`;
|
|
459
|
+
context.report({
|
|
460
|
+
node: call.node,
|
|
461
|
+
messageId: "prefer",
|
|
462
|
+
data: {
|
|
463
|
+
suggestion,
|
|
464
|
+
generic: `${call.kind}(\u2026, '${call.name}', \u2026)`,
|
|
465
|
+
count: portCalls.length,
|
|
466
|
+
threshold
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
// src/index.ts
|
|
476
|
+
var rules = {
|
|
477
|
+
"exhaustive-conditions": exhaustiveConditions,
|
|
478
|
+
"exhaustive-required": exhaustiveRequired,
|
|
479
|
+
"hook-rules": hookRules,
|
|
480
|
+
"max-handler-size": maxHandlerSize,
|
|
481
|
+
"max-ports-per-trigger": maxPortsPerTrigger,
|
|
482
|
+
"no-dynamic-id": noDynamicId,
|
|
483
|
+
"no-event-cascade": noEventCascade,
|
|
484
|
+
"prefer-named-hook": preferNamedHook
|
|
485
|
+
};
|
|
486
|
+
var meta = {
|
|
487
|
+
name: "@triggery/eslint-plugin",
|
|
488
|
+
version: "0.0.0"
|
|
489
|
+
};
|
|
490
|
+
var plugin = {
|
|
491
|
+
meta,
|
|
492
|
+
rules
|
|
493
|
+
};
|
|
494
|
+
var recommended = {
|
|
495
|
+
plugins: { "@triggery": plugin },
|
|
496
|
+
rules: {
|
|
497
|
+
"@triggery/no-dynamic-id": "error",
|
|
498
|
+
"@triggery/no-event-cascade": "error",
|
|
499
|
+
"@triggery/hook-rules": "error",
|
|
500
|
+
"@triggery/exhaustive-conditions": "warn",
|
|
501
|
+
"@triggery/exhaustive-required": "warn",
|
|
502
|
+
"@triggery/max-handler-size": "warn",
|
|
503
|
+
"@triggery/max-ports-per-trigger": "warn"
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
var strict = {
|
|
507
|
+
plugins: { "@triggery": plugin },
|
|
508
|
+
rules: {
|
|
509
|
+
"@triggery/no-dynamic-id": "error",
|
|
510
|
+
"@triggery/no-event-cascade": "error",
|
|
511
|
+
"@triggery/hook-rules": "error",
|
|
512
|
+
"@triggery/exhaustive-conditions": "error",
|
|
513
|
+
"@triggery/exhaustive-required": "error",
|
|
514
|
+
"@triggery/max-handler-size": ["error", { max: 30 }],
|
|
515
|
+
"@triggery/max-ports-per-trigger": ["error", { maxEvents: 5, maxConditions: 5, maxTotal: 8 }],
|
|
516
|
+
"@triggery/prefer-named-hook": ["warn", { threshold: 3 }]
|
|
517
|
+
}
|
|
518
|
+
};
|
|
519
|
+
var finalPlugin = {
|
|
520
|
+
...plugin,
|
|
521
|
+
configs: {
|
|
522
|
+
recommended,
|
|
523
|
+
strict
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
var index_default = finalPlugin;
|
|
527
|
+
|
|
528
|
+
export { index_default as default, recommended, rules, strict };
|
|
529
|
+
//# sourceMappingURL=index.js.map
|
|
530
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/utils/triggery-call.ts","../src/rules/exhaustive-conditions.ts","../src/rules/exhaustive-required.ts","../src/rules/hook-rules.ts","../src/rules/max-handler-size.ts","../src/rules/max-ports-per-trigger.ts","../src/rules/no-dynamic-id.ts","../src/rules/no-event-cascade.ts","../src/rules/prefer-named-hook.ts","../src/index.ts"],"names":["AST_NODE_TYPES","createRule","ESLintUtils"],"mappings":";;;AAYO,IAAM,mBAAA,uBAA0B,GAAA,CAAI;AAAA,EACzC,UAAA;AAAA,EACA,cAAA;AAAA,EACA,WAAA;AAAA,EACA;AACF,CAAC,CAAA;AAQM,SAAS,oBAAoB,IAAA,EAAwC;AAC1E,EAAA,MAAM,SAAS,IAAA,CAAK,MAAA;AACpB,EAAA,IAAI,OAAO,IAAA,KAAS,cAAA,CAAe,cAAc,MAAA,CAAO,IAAA,KAAS,iBAAiB,OAAO,IAAA;AAEzF,EAAA,IACE,MAAA,CAAO,IAAA,KAAS,cAAA,CAAe,gBAAA,IAC/B,MAAA,CAAO,QAAA,CAAS,IAAA,KAAS,cAAA,CAAe,UAAA,IACxC,MAAA,CAAO,QAAA,CAAS,IAAA,KAAS,eAAA,EACzB;AACA,IAAA,OAAO,IAAA;AAAA,EACT;AACA,EAAA,OAAO,KAAA;AACT;AAEO,SAAS,sBAAsB,IAAA,EAAgD;AACpF,EAAA,IAAI,CAAC,MAAM,OAAO,IAAA;AAClB,EAAA,IAAI,KAAK,IAAA,KAAS,cAAA,CAAe,WAAW,OAAO,IAAA,CAAK,UAAU,QAAA,EAAU;AAC1E,IAAA,OAAO,IAAA,CAAK,KAAA;AAAA,EACd;AACA,EAAA,IACE,IAAA,CAAK,IAAA,KAAS,cAAA,CAAe,eAAA,IAC7B,KAAK,WAAA,CAAY,MAAA,KAAW,CAAA,IAC5B,IAAA,CAAK,OAAO,MAAA,KAAW,CAAA,IACvB,IAAA,CAAK,MAAA,CAAO,CAAC,CAAA,EACb;AACA,IAAA,OAAO,IAAA,CAAK,MAAA,CAAO,CAAC,CAAA,CAAE,KAAA,CAAM,MAAA;AAAA,EAC9B;AACA,EAAA,OAAO,IAAA;AACT;AAMO,SAAS,aAAa,IAAA,EAAsB;AACjD,EAAA,OAAO,IAAA,CACJ,MAAM,SAAS,CAAA,CACf,OAAO,OAAO,CAAA,CACd,GAAA,CAAI,CAAC,IAAA,KAAS,IAAA,CAAK,OAAO,CAAC,CAAA,CAAE,aAAY,GAAI,IAAA,CAAK,MAAM,CAAC,CAAC,CAAA,CAC1D,IAAA,CAAK,EAAE,CAAA;AACZ;AAOO,SAAS,sBACd,IAAA,EAKO;AACP,EAAA,IAAI,UAAqC,IAAA,CAAK,MAAA;AAC9C,EAAA,OAAO,OAAA,EAAS;AACd,IAAA,IACE,OAAA,CAAQ,IAAA,KAAS,cAAA,CAAe,mBAAA,IAChC,OAAA,CAAQ,IAAA,KAAS,cAAA,CAAe,kBAAA,IAChC,OAAA,CAAQ,IAAA,KAAS,cAAA,CAAe,uBAAA,EAChC;AACA,MAAA,OAAO,OAAA;AAAA,IACT;AACA,IAAA,OAAA,GAAU,OAAA,CAAQ,MAAA;AAAA,EACpB;AACA,EAAA,OAAO,IAAA;AACT;AAQO,SAAS,0BACd,EAAA,EACS;AACT,EAAA,MAAM,IAAA,GAAO,gBAAgB,EAAE,CAAA;AAC/B,EAAA,IAAI,CAAC,MAAM,OAAO,KAAA;AAClB,EAAA,OAAO,SAAS,IAAA,CAAK,IAAI,CAAA,IAAK,WAAA,CAAY,KAAK,IAAI,CAAA;AACrD;AAEO,SAAS,gBACd,EAAA,EACe;AACf,EAAA,IAAI,EAAA,CAAG,SAAS,cAAA,CAAe,mBAAA,IAAuB,GAAG,EAAA,EAAI,OAAO,GAAG,EAAA,CAAG,IAAA;AAC1E,EAAA,IAAI,EAAA,CAAG,SAAS,cAAA,CAAe,kBAAA,IAAsB,GAAG,EAAA,EAAI,OAAO,GAAG,EAAA,CAAG,IAAA;AACzE,EAAA,MAAM,SAAS,EAAA,CAAG,MAAA;AAClB,EAAA,IAAI,MAAA,IAAU,MAAA,CAAO,IAAA,KAAS,cAAA,CAAe,kBAAA,EAAoB;AAC/D,IAAA,MAAM,KAAK,MAAA,CAAO,EAAA;AAClB,IAAA,IAAI,EAAA,CAAG,IAAA,KAAS,cAAA,CAAe,UAAA,SAAmB,EAAA,CAAG,IAAA;AAAA,EACvD;AACA,EAAA,IAAI,MAAA,IAAU,MAAA,CAAO,IAAA,KAAS,cAAA,CAAe,QAAA,EAAU;AACrD,IAAA,MAAM,MAAM,MAAA,CAAO,GAAA;AACnB,IAAA,IAAI,GAAA,CAAI,IAAA,KAAS,cAAA,CAAe,UAAA,SAAmB,GAAA,CAAI,IAAA;AAAA,EACzD;AACA,EAAA,OAAO,IAAA;AACT;;;ACvHA,IAAM,aAAa,WAAA,CAAY,WAAA;AAAA,EAC7B,CAAC,IAAA,KACC,CAAA,mFAAA,EAAsF,IAAI,CAAA,GAAA;AAC9F,CAAA;AAoBO,IAAM,uBAAuB,UAAA,CAAW;AAAA,EAC7C,IAAA,EAAM,uBAAA;AAAA,EACN,IAAA,EAAM;AAAA,IACJ,IAAA,EAAM,SAAA;AAAA,IACN,IAAA,EAAM;AAAA,MACJ,WAAA,EACE;AAAA,KACJ;AAAA,IACA,QAAA,EAAU;AAAA,MACR,OAAA,EACE;AAAA,KACJ;AAAA,IACA,QAAQ;AAAC,GACX;AAAA,EACA,gBAAgB,EAAC;AAAA,EACjB,OAAO,OAAA,EAAS;AACd,IAAA,MAAM,WAA0B,EAAC;AACjC,IAAA,MAAM,QAAA,uBAAe,GAAA,EAAyB;AAE9C,IAAA,SAAS,MAAA,CAAO,YAAoB,aAAA,EAA6B;AAC/D,MAAA,IAAI,GAAA,GAAM,QAAA,CAAS,GAAA,CAAI,UAAU,CAAA;AACjC,MAAA,IAAI,CAAC,GAAA,EAAK;AACR,QAAA,GAAA,uBAAU,GAAA,EAAI;AACd,QAAA,QAAA,CAAS,GAAA,CAAI,YAAY,GAAG,CAAA;AAAA,MAC9B;AACA,MAAA,GAAA,CAAI,IAAI,aAAa,CAAA;AAAA,IACvB;AAEA,IAAA,OAAO;AAAA,MACL,eAAe,IAAA,EAAM;AACnB,QAAA,IAAI,mBAAA,CAAoB,IAAI,CAAA,EAAG;AAE7B,UAAA,MAAM,SAAS,IAAA,CAAK,MAAA;AACpB,UAAA,IAAI,CAAC,MAAA,IAAU,MAAA,CAAO,IAAA,KAASA,eAAe,kBAAA,EAAoB;AAClE,UAAA,IAAI,MAAA,CAAO,EAAA,CAAG,IAAA,KAASA,cAAAA,CAAe,UAAA,EAAY;AAClD,UAAA,MAAM,OAAA,GAAU,OAAO,EAAA,CAAG,IAAA;AAE1B,UAAA,MAAM,MAAA,GAAS,IAAA,CAAK,SAAA,CAAU,CAAC,CAAA;AAC/B,UAAA,IAAI,CAAC,MAAA,IAAU,MAAA,CAAO,IAAA,KAASA,eAAe,gBAAA,EAAkB;AAChE,UAAA,MAAM,YAAA,GAAe,OAAO,UAAA,CAAW,IAAA;AAAA,YACrC,CAAC,CAAA,KACC,CAAA,CAAE,IAAA,KAASA,cAAAA,CAAe,QAAA,IAC1B,CAAA,CAAE,GAAA,CAAI,IAAA,KAASA,cAAAA,CAAe,UAAA,IAC9B,CAAA,CAAE,IAAI,IAAA,KAAS;AAAA,WACnB;AACA,UAAA,IAAI,CAAC,YAAA,EAAc;AACnB,UAAA,IAAI,YAAA,CAAa,KAAA,CAAM,IAAA,KAASA,cAAAA,CAAe,eAAA,EAAiB;AAChE,UAAA,MAAM,WAAqB,EAAC;AAC5B,UAAA,KAAA,MAAW,EAAA,IAAM,YAAA,CAAa,KAAA,CAAM,QAAA,EAAU;AAC5C,YAAA,MAAM,IAAA,GAAO,qBAAA,CAAsB,EAAA,IAAM,MAAS,CAAA;AAClD,YAAA,IAAI,IAAA,EAAM,QAAA,CAAS,IAAA,CAAK,IAAI,CAAA;AAAA,UAC9B;AACA,UAAA,IAAI,QAAA,CAAS,MAAA,GAAS,CAAA,EAAG,QAAA,CAAS,IAAA,CAAK,EAAE,OAAA,EAAS,QAAA,EAAU,IAAA,EAAM,YAAA,CAAa,KAAA,EAAO,CAAA;AACtF,UAAA;AAAA,QACF;AAEA,QAAA,IACE,IAAA,CAAK,OAAO,IAAA,KAASA,cAAAA,CAAe,cACpC,IAAA,CAAK,MAAA,CAAO,IAAA,KAAS,cAAA,IACrB,IAAA,CAAK,SAAA,CAAU,CAAC,CAAA,IAChB,IAAA,CAAK,SAAA,CAAU,CAAC,CAAA,CAAE,IAAA,KAASA,eAAe,UAAA,IAC1C,IAAA,CAAK,SAAA,CAAU,CAAC,CAAA,EAChB;AACA,UAAA,MAAM,UAAA,GAAa,IAAA,CAAK,SAAA,CAAU,CAAC,CAAA,CAAE,IAAA;AACrC,UAAA,MAAM,aAAA,GAAgB,qBAAA,CAAsB,IAAA,CAAK,SAAA,CAAU,CAAC,CAAC,CAAA;AAC7D,UAAA,IAAI,aAAA,EAAe,MAAA,CAAO,UAAA,EAAY,aAAa,CAAA;AAAA,QACrD;AAAA,MACF,CAAA;AAAA,MACA,cAAA,GAAiB;AACf,QAAA,KAAA,MAAW,WAAW,QAAA,EAAU;AAC9B,UAAA,MAAM,MAAM,QAAA,CAAS,GAAA,CAAI,QAAQ,OAAO,CAAA,wBAAS,GAAA,EAAY;AAC7D,UAAA,KAAA,MAAW,IAAA,IAAQ,QAAQ,QAAA,EAAU;AACnC,YAAA,IAAI,CAAC,GAAA,CAAI,GAAA,CAAI,IAAI,CAAA,EAAG;AAClB,cAAA,OAAA,CAAQ,MAAA,CAAO;AAAA,gBACb,MAAM,OAAA,CAAQ,IAAA;AAAA,gBACd,SAAA,EAAW,SAAA;AAAA,gBACX,IAAA,EAAM,EAAE,OAAA,EAAS,OAAA,CAAQ,SAAS,IAAA;AAAK,eACxC,CAAA;AAAA,YACH;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,KACF;AAAA,EACF;AACF,CAAC,CAAA;AC3GD,IAAMC,cAAaC,WAAAA,CAAY,WAAA;AAAA,EAC7B,CAAC,IAAA,KACC,CAAA,mFAAA,EAAsF,IAAI,CAAA,GAAA;AAC9F,CAAA;AAcO,IAAM,qBAAqBD,WAAAA,CAAW;AAAA,EAC3C,IAAA,EAAM,qBAAA;AAAA,EACN,IAAA,EAAM;AAAA,IACJ,IAAA,EAAM,YAAA;AAAA,IACN,IAAA,EAAM;AAAA,MACJ,WAAA,EACE;AAAA,KACJ;AAAA,IACA,QAAA,EAAU;AAAA,MACR,OAAA,EACE;AAAA,KACJ;AAAA,IACA,QAAQ;AAAC,GACX;AAAA,EACA,gBAAgB,EAAC;AAAA,EACjB,OAAO,OAAA,EAAS;AACd,IAAA,OAAO;AAAA,MACL,eAAe,IAAA,EAAM;AACnB,QAAA,IAAI,CAAC,mBAAA,CAAoB,IAAI,CAAA,EAAG;AAChC,QAAA,MAAM,MAAA,GAAS,IAAA,CAAK,SAAA,CAAU,CAAC,CAAA;AAC/B,QAAA,IAAI,CAAC,MAAA,IAAU,MAAA,CAAO,IAAA,KAASD,eAAe,gBAAA,EAAkB;AAChE,QAAA,MAAM,WAAA,GAAc,OAAO,UAAA,CAAW,IAAA;AAAA,UACpC,CAAC,CAAA,KACC,CAAA,CAAE,IAAA,KAASA,cAAAA,CAAe,QAAA,IAC1B,CAAA,CAAE,GAAA,CAAI,IAAA,KAASA,cAAAA,CAAe,UAAA,IAC9B,CAAA,CAAE,IAAI,IAAA,KAAS;AAAA,SACnB;AACA,QAAA,IAAI,CAAC,aAAa,OAAA,CAAQ,MAAA,CAAO,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAA,EAAW,SAAA,EAAW,CAAA;AAAA,MACzE;AAAA,KACF;AAAA,EACF;AACF,CAAC,CAAA;AC5CD,IAAMC,cAAaC,WAAAA,CAAY,WAAA;AAAA,EAC7B,CAAC,IAAA,KACC,CAAA,mFAAA,EAAsF,IAAI,CAAA,GAAA;AAC9F,CAAA;AAeO,IAAM,YAAYD,WAAAA,CAAW;AAAA,EAClC,IAAA,EAAM,YAAA;AAAA,EACN,IAAA,EAAM;AAAA,IACJ,IAAA,EAAM,SAAA;AAAA,IACN,IAAA,EAAM;AAAA,MACJ,WAAA,EACE;AAAA,KACJ;AAAA,IACA,QAAA,EAAU;AAAA,MACR,OAAA,EACE;AAAA,KACJ;AAAA,IACA,QAAQ;AAAC,GACX;AAAA,EACA,gBAAgB,EAAC;AAAA,EACjB,OAAO,OAAA,EAAS;AACd,IAAA,OAAO;AAAA,MACL,eAAe,IAAA,EAAM;AACnB,QAAA,IAAI,IAAA,CAAK,MAAA,CAAO,IAAA,KAASD,cAAAA,CAAe,UAAA,EAAY;AACpD,QAAA,MAAM,IAAA,GAAO,KAAK,MAAA,CAAO,IAAA;AACzB,QAAA,IAAI,CAAC,mBAAA,CAAoB,GAAA,CAAI,IAAI,CAAA,EAAG;AACpC,QAAA,MAAM,EAAA,GAAK,sBAAsB,IAAI,CAAA;AACrC,QAAA,IAAI,CAAC,EAAA,IAAM,CAAC,yBAAA,CAA0B,EAAE,CAAA,EAAG;AACzC,UAAA,OAAA,CAAQ,MAAA,CAAO,EAAE,IAAA,EAAM,SAAA,EAAW,WAAW,IAAA,EAAM,EAAE,IAAA,EAAK,EAAG,CAAA;AAAA,QAC/D;AAAA,MACF;AAAA,KACF;AAAA,EACF;AACF,CAAC,CAAA;AClDD,IAAMC,cAAaC,WAAAA,CAAY,WAAA;AAAA,EAC7B,CAAC,IAAA,KACC,CAAA,mFAAA,EAAsF,IAAI,CAAA,GAAA;AAC9F,CAAA;AAWO,IAAM,iBAAiBD,WAAAA,CAA2C;AAAA,EACvE,IAAA,EAAM,kBAAA;AAAA,EACN,IAAA,EAAM;AAAA,IACJ,IAAA,EAAM,YAAA;AAAA,IACN,IAAA,EAAM;AAAA,MACJ,WAAA,EAAa;AAAA,KACf;AAAA,IACA,QAAA,EAAU;AAAA,MACR,QAAA,EACE;AAAA,KACJ;AAAA,IACA,MAAA,EAAQ;AAAA,MACN;AAAA,QACE,IAAA,EAAM,QAAA;AAAA,QACN,oBAAA,EAAsB,KAAA;AAAA,QACtB,UAAA,EAAY;AAAA,UACV,GAAA,EAAK,EAAE,IAAA,EAAM,SAAA,EAAW,SAAS,CAAA;AAAE;AACrC;AACF;AACF,GACF;AAAA,EACA,cAAA,EAAgB,CAAC,EAAE,GAAA,EAAK,IAAI,CAAA;AAAA,EAC5B,MAAA,CAAO,OAAA,EAAS,CAAC,OAAO,CAAA,EAAG;AACzB,IAAA,MAAM,GAAA,GAAM,QAAQ,GAAA,IAAO,EAAA;AAC3B,IAAA,OAAO;AAAA,MACL,eAAe,IAAA,EAAM;AACnB,QAAA,IAAI,CAAC,mBAAA,CAAoB,IAAI,CAAA,EAAG;AAChC,QAAA,MAAM,MAAA,GAAS,IAAA,CAAK,SAAA,CAAU,CAAC,CAAA;AAC/B,QAAA,IAAI,CAAC,MAAA,IAAU,MAAA,CAAO,IAAA,KAASD,eAAe,gBAAA,EAAkB;AAChE,QAAA,KAAA,MAAW,IAAA,IAAQ,OAAO,UAAA,EAAY;AACpC,UAAA,IAAI,IAAA,CAAK,IAAA,KAASA,cAAAA,CAAe,QAAA,EAAU;AAC3C,UAAA,IAAI,IAAA,CAAK,IAAI,IAAA,KAASA,cAAAA,CAAe,cAAc,IAAA,CAAK,GAAA,CAAI,SAAS,SAAA,EAAW;AAChF,UAAA,MAAM,UAAU,IAAA,CAAK,KAAA;AACrB,UAAA,IAAI,IAAA,GAAuC,IAAA;AAC3C,UAAA,IACE,QAAQ,IAAA,KAASA,cAAAA,CAAe,sBAChC,OAAA,CAAQ,IAAA,KAASA,eAAe,uBAAA,EAChC;AACA,YAAA,IAAI,QAAQ,IAAA,CAAK,IAAA,KAASA,cAAAA,CAAe,cAAA,SAAuB,OAAA,CAAQ,IAAA;AAAA,UAC1E;AACA,UAAA,IAAI,CAAC,IAAA,EAAM;AACX,UAAA,MAAM,KAAA,GAAQ,KAAK,IAAA,CAAK,MAAA;AACxB,UAAA,IAAI,QAAQ,GAAA,EAAK;AACf,YAAA,OAAA,CAAQ,MAAA,CAAO;AAAA,cACb,IAAA,EAAM,OAAA;AAAA,cACN,SAAA,EAAW,UAAA;AAAA,cACX,IAAA,EAAM,EAAE,KAAA,EAAO,GAAA;AAAI,aACpB,CAAA;AAAA,UACH;AACA,UAAA;AAAA,QACF;AAAA,MACF;AAAA,KACF;AAAA,EACF;AACF,CAAC,CAAA;ACpED,IAAMC,cAAaC,WAAAA,CAAY,WAAA;AAAA,EAC7B,CAAC,IAAA,KACC,CAAA,mFAAA,EAAsF,IAAI,CAAA,GAAA;AAC9F,CAAA;AAgBO,IAAM,qBAAqBD,WAAAA,CAGhC;AAAA,EACA,IAAA,EAAM,uBAAA;AAAA,EACN,IAAA,EAAM;AAAA,IACJ,IAAA,EAAM,YAAA;AAAA,IACN,IAAA,EAAM;AAAA,MACJ,WAAA,EACE;AAAA,KACJ;AAAA,IACA,QAAA,EAAU;AAAA,MACR,YAAA,EACE,6FAAA;AAAA,MACF,aAAA,EACE,iGAAA;AAAA,MACF,iBAAA,EACE;AAAA,KACJ;AAAA,IACA,MAAA,EAAQ;AAAA,MACN;AAAA,QACE,IAAA,EAAM,QAAA;AAAA,QACN,oBAAA,EAAsB,KAAA;AAAA,QACtB,UAAA,EAAY;AAAA,UACV,SAAA,EAAW,EAAE,IAAA,EAAM,SAAA,EAAW,SAAS,CAAA,EAAE;AAAA,UACzC,aAAA,EAAe,EAAE,IAAA,EAAM,SAAA,EAAW,SAAS,CAAA,EAAE;AAAA,UAC7C,QAAA,EAAU,EAAE,IAAA,EAAM,SAAA,EAAW,SAAS,CAAA;AAAE;AAC1C;AACF;AACF,GACF;AAAA,EACA,cAAA,EAAgB,CAAC,EAAE,SAAA,EAAW,GAAG,aAAA,EAAe,CAAA,EAAG,QAAA,EAAU,EAAA,EAAI,CAAA;AAAA,EACjE,MAAA,CAAO,OAAA,EAAS,CAAC,OAAO,CAAA,EAAG;AACzB,IAAA,MAAM,SAAA,GAAY,QAAQ,SAAA,IAAa,CAAA;AACvC,IAAA,MAAM,aAAA,GAAgB,QAAQ,aAAA,IAAiB,CAAA;AAC/C,IAAA,MAAM,QAAA,GAAW,QAAQ,QAAA,IAAY,EAAA;AAErC,IAAA,SAAS,oBAAA,CAAqB,QAAmC,QAAA,EAA0B;AACzF,MAAA,MAAM,IAAA,GAAO,OAAO,UAAA,CAAW,IAAA;AAAA,QAC7B,CAAC,CAAA,KACC,CAAA,CAAE,IAAA,KAASD,cAAAA,CAAe,QAAA,IAC1B,CAAA,CAAE,GAAA,CAAI,IAAA,KAASA,cAAAA,CAAe,UAAA,IAC9B,CAAA,CAAE,IAAI,IAAA,KAAS;AAAA,OACnB;AACA,MAAA,IAAI,CAAC,MAAM,OAAO,CAAA;AAClB,MAAA,IAAI,IAAA,CAAK,KAAA,CAAM,IAAA,KAASA,cAAAA,CAAe,iBAAiB,OAAO,CAAA;AAC/D,MAAA,OAAO,IAAA,CAAK,MAAM,QAAA,CAAS,MAAA;AAAA,IAC7B;AAEA,IAAA,OAAO;AAAA,MACL,eAAe,IAAA,EAAM;AACnB,QAAA,IAAI,CAAC,mBAAA,CAAoB,IAAI,CAAA,EAAG;AAChC,QAAA,MAAM,MAAA,GAAS,IAAA,CAAK,SAAA,CAAU,CAAC,CAAA;AAC/B,QAAA,IAAI,CAAC,MAAA,IAAU,MAAA,CAAO,IAAA,KAASA,eAAe,gBAAA,EAAkB;AAChE,QAAA,MAAM,MAAA,GAAS,oBAAA,CAAqB,MAAA,EAAQ,QAAQ,CAAA;AACpD,QAAA,MAAM,QAAA,GAAW,oBAAA,CAAqB,MAAA,EAAQ,UAAU,CAAA;AACxD,QAAA,MAAM,QAAQ,MAAA,GAAS,QAAA;AAEvB,QAAA,IAAI,SAAS,SAAA,EAAW;AACtB,UAAA,OAAA,CAAQ,MAAA,CAAO;AAAA,YACb,IAAA,EAAM,MAAA;AAAA,YACN,SAAA,EAAW,eAAA;AAAA,YACX,IAAA,EAAM,EAAE,KAAA,EAAO,MAAA,EAAQ,KAAK,SAAA;AAAU,WACvC,CAAA;AAAA,QACH;AACA,QAAA,IAAI,WAAW,aAAA,EAAe;AAC5B,UAAA,OAAA,CAAQ,MAAA,CAAO;AAAA,YACb,IAAA,EAAM,MAAA;AAAA,YACN,SAAA,EAAW,mBAAA;AAAA,YACX,IAAA,EAAM,EAAE,KAAA,EAAO,QAAA,EAAU,KAAK,aAAA;AAAc,WAC7C,CAAA;AAAA,QACH;AACA,QAAA,IAAI,QAAQ,QAAA,EAAU;AACpB,UAAA,OAAA,CAAQ,MAAA,CAAO;AAAA,YACb,IAAA,EAAM,MAAA;AAAA,YACN,SAAA,EAAW,cAAA;AAAA,YACX,IAAA,EAAM,EAAE,KAAA,EAAO,KAAA,EAAO,KAAK,QAAA;AAAS,WACrC,CAAA;AAAA,QACH;AAAA,MACF;AAAA,KACF;AAAA,EACF;AACF,CAAC,CAAA;ACrGD,IAAMC,cAAaC,WAAAA,CAAY,WAAA;AAAA,EAC7B,CAAC,IAAA,KACC,CAAA,mFAAA,EAAsF,IAAI,CAAA,GAAA;AAC9F,CAAA;AAUO,IAAM,cAAcD,WAAAA,CAAW;AAAA,EACpC,IAAA,EAAM,eAAA;AAAA,EACN,IAAA,EAAM;AAAA,IACJ,IAAA,EAAM,SAAA;AAAA,IACN,IAAA,EAAM;AAAA,MACJ,WAAA,EAAa;AAAA,KACf;AAAA,IACA,QAAA,EAAU;AAAA,MACR,OAAA,EACE;AAAA,KACJ;AAAA,IACA,QAAQ;AAAC,GACX;AAAA,EACA,gBAAgB,EAAC;AAAA,EACjB,OAAO,OAAA,EAAS;AACd,IAAA,OAAO;AAAA,MACL,eAAe,IAAA,EAAM;AACnB,QAAA,IAAI,CAAC,mBAAA,CAAoB,IAAI,CAAA,EAAG;AAChC,QAAA,MAAM,MAAA,GAAS,IAAA,CAAK,SAAA,CAAU,CAAC,CAAA;AAC/B,QAAA,IAAI,CAAC,MAAA,IAAU,MAAA,CAAO,IAAA,KAASD,eAAe,gBAAA,EAAkB;AAChE,QAAA,KAAA,MAAW,IAAA,IAAQ,OAAO,UAAA,EAAY;AACpC,UAAA,IAAI,IAAA,CAAK,IAAA,KAASA,cAAAA,CAAe,QAAA,EAAU;AAC3C,UAAA,IAAI,IAAA,CAAK,IAAI,IAAA,KAASA,cAAAA,CAAe,cAAc,IAAA,CAAK,GAAA,CAAI,SAAS,IAAA,EAAM;AAC3E,UAAA,IAAI,qBAAA,CAAsB,IAAA,CAAK,KAAK,CAAA,KAAM,IAAA,EAAM;AAC9C,YAAA,OAAA,CAAQ,OAAO,EAAE,IAAA,EAAM,KAAK,KAAA,EAAO,SAAA,EAAW,WAAW,CAAA;AAAA,UAC3D;AACA,UAAA;AAAA,QACF;AAAA,MACF;AAAA,KACF;AAAA,EACF;AACF,CAAC,CAAA;AC7CD,IAAMC,cAAaC,WAAAA,CAAY,WAAA;AAAA,EAC7B,CAAC,IAAA,KACC,CAAA,mFAAA,EAAsF,IAAI,CAAA,GAAA;AAC9F,CAAA;AAYO,IAAM,iBAAiBD,WAAAA,CAAW;AAAA,EACvC,IAAA,EAAM,kBAAA;AAAA,EACN,IAAA,EAAM;AAAA,IACJ,IAAA,EAAM,SAAA;AAAA,IACN,IAAA,EAAM;AAAA,MACJ,WAAA,EAAa;AAAA,KACf;AAAA,IACA,QAAA,EAAU;AAAA,MACR,OAAA,EACE;AAAA,KACJ;AAAA,IACA,QAAQ;AAAC,GACX;AAAA,EACA,gBAAgB,EAAC;AAAA,EACjB,OAAO,OAAA,EAAS;AAId,IAAA,MAAM,oBAAqC,EAAC;AAE5C,IAAA,SAAS,2BAA2B,IAAA,EAA8B;AAChE,MAAA,MAAM,SAAS,IAAA,CAAK,MAAA;AACpB,MAAA,IAAI,CAAC,MAAA,IAAU,MAAA,CAAO,IAAA,KAASD,cAAAA,CAAe,gBAAgB,OAAO,KAAA;AAErE,MAAA,IAAI,MAAA,CAAO,SAAA,CAAU,CAAC,CAAA,KAAM,MAAM,OAAO,KAAA;AACzC,MAAA,MAAM,SAAS,MAAA,CAAO,MAAA;AACtB,MAAA,OAAO,MAAA,CAAO,IAAA,KAASA,cAAAA,CAAe,UAAA,IAAc,OAAO,IAAA,KAAS,WAAA;AAAA,IACtE;AAEA,IAAA,SAAS,QACP,IAAA,EAIM;AACN,MAAA,IAAI,0BAAA,CAA2B,IAAI,CAAA,EAAG,iBAAA,CAAkB,KAAK,IAAI,CAAA;AAAA,IACnE;AAEA,IAAA,SAAS,OACP,IAAA,EAIM;AACN,MAAA,IAAI,kBAAkB,iBAAA,CAAkB,MAAA,GAAS,CAAC,CAAA,KAAM,IAAA,oBAAwB,GAAA,EAAI;AAAA,IACtF;AAEA,IAAA,OAAO;AAAA,MACL,kBAAA,EAAoB,OAAA;AAAA,MACpB,uBAAA,EAAyB,OAAA;AAAA,MACzB,mBAAA,EAAqB,OAAA;AAAA,MACrB,yBAAA,EAA2B,MAAA;AAAA,MAC3B,8BAAA,EAAgC,MAAA;AAAA,MAChC,0BAAA,EAA4B,MAAA;AAAA,MAC5B,eAAe,IAAA,EAAM;AACnB,QAAA,IAAI,iBAAA,CAAkB,WAAW,CAAA,EAAG;AACpC,QAAA,IAAI,IAAA,CAAK,MAAA,CAAO,IAAA,KAASA,cAAAA,CAAe,UAAA,EAAY;AACpD,QAAA,IAAI,IAAA,CAAK,MAAA,CAAO,IAAA,KAAS,UAAA,EAAY;AACrC,QAAA,OAAA,CAAQ,MAAA,CAAO;AAAA,UACb,IAAA;AAAA,UACA,SAAA,EAAW,SAAA;AAAA,UACX,IAAA,EAAM,EAAE,MAAA,EAAQ,IAAA,CAAK,OAAO,IAAA;AAAK,SAClC,CAAA;AAAA,MACH;AAAA,KACF;AAAA,EACF;AACF,CAAC,CAAA;AChFD,IAAMC,cAAaC,WAAAA,CAAY,WAAA;AAAA,EAC7B,CAAC,IAAA,KACC,CAAA,mFAAA,EAAsF,IAAI,CAAA,GAAA;AAC9F,CAAA;AAQO,IAAM,kBAAkBD,WAAAA,CAA+C;AAAA,EAC5E,IAAA,EAAM,mBAAA;AAAA,EACN,IAAA,EAAM;AAAA,IACJ,IAAA,EAAM,YAAA;AAAA,IACN,IAAA,EAAM;AAAA,MACJ,WAAA,EACE;AAAA,KACJ;AAAA,IACA,QAAA,EAAU;AAAA,MACR,MAAA,EACE;AAAA,KACJ;AAAA,IACA,MAAA,EAAQ;AAAA,MACN;AAAA,QACE,IAAA,EAAM,QAAA;AAAA,QACN,oBAAA,EAAsB,KAAA;AAAA,QACtB,UAAA,EAAY;AAAA,UACV,SAAA,EAAW,EAAE,IAAA,EAAM,SAAA,EAAW,SAAS,CAAA;AAAE;AAC3C;AACF;AACF,GACF;AAAA,EACA,cAAA,EAAgB,CAAC,EAAE,SAAA,EAAW,GAAG,CAAA;AAAA,EACjC,MAAA,CAAO,OAAA,EAAS,CAAC,OAAO,CAAA,EAAG;AACzB,IAAA,MAAM,SAAA,GAAY,QAAQ,SAAA,IAAa,CAAA;AACvC,IAAA,MAAM,YAAkF,EAAC;AAEzF,IAAA,SAAS,UAAU,IAAA,EAAsB;AACvC,MAAA,QAAQ,IAAA;AAAM,QACZ,KAAK,UAAA;AACH,UAAA,OAAO,OAAA;AAAA,QACT,KAAK,cAAA;AACH,UAAA,OAAO,WAAA;AAAA,QACT,KAAK,WAAA;AACH,UAAA,OAAO,QAAA;AAAA,QACT;AACE,UAAA,OAAO,EAAA;AAAA;AACX,IACF;AAEA,IAAA,OAAO;AAAA,MACL,eAAe,IAAA,EAAM;AACnB,QAAA,IAAI,IAAA,CAAK,MAAA,CAAO,IAAA,KAASD,cAAAA,CAAe,UAAA,EAAY;AACpD,QAAA,MAAM,IAAA,GAAO,KAAK,MAAA,CAAO,IAAA;AACzB,QAAA,IAAI,CAAC,CAAC,UAAA,EAAY,cAAA,EAAgB,WAAW,CAAA,CAAE,QAAA,CAAS,IAAI,CAAA,EAAG;AAC/D,QAAA,MAAM,OAAA,GAAU,IAAA,CAAK,SAAA,CAAU,CAAC,CAAA;AAChC,QAAA,MAAM,QAAA,GAAW,sBAAsB,OAAO,CAAA;AAC9C,QAAA,IAAI,CAAC,QAAA,EAAU;AACf,QAAA,SAAA,CAAU,KAAK,EAAE,IAAA,EAAM,MAAM,IAAA,EAAM,IAAA,EAAM,UAAU,CAAA;AAAA,MACrD,CAAA;AAAA,MACA,cAAA,GAAiB;AACf,QAAA,IAAI,SAAA,CAAU,SAAS,SAAA,EAAW;AAClC,QAAA,KAAA,MAAW,QAAQ,SAAA,EAAW;AAC5B,UAAA,MAAM,UAAA,GAAa,CAAA,GAAA,EAAM,YAAA,CAAa,IAAA,CAAK,IAAI,CAAC,CAAA,EAAG,SAAA,CAAU,IAAA,CAAK,IAAI,CAAC,CAAA,CAAA;AACvE,UAAA,OAAA,CAAQ,MAAA,CAAO;AAAA,YACb,MAAM,IAAA,CAAK,IAAA;AAAA,YACX,SAAA,EAAW,QAAA;AAAA,YACX,IAAA,EAAM;AAAA,cACJ,UAAA;AAAA,cACA,SAAS,CAAA,EAAG,IAAA,CAAK,IAAI,CAAA,UAAA,EAAQ,KAAK,IAAI,CAAA,UAAA,CAAA;AAAA,cACtC,OAAO,SAAA,CAAU,MAAA;AAAA,cACjB;AAAA;AACF,WACD,CAAA;AAAA,QACH;AAAA,MACF;AAAA,KACF;AAAA,EACF;AACF,CAAC,CAAA;;;ACxED,IAAM,KAAA,GAAQ;AAAA,EACZ,uBAAA,EAAyB,oBAAA;AAAA,EACzB,qBAAA,EAAuB,kBAAA;AAAA,EACvB,YAAA,EAAc,SAAA;AAAA,EACd,kBAAA,EAAoB,cAAA;AAAA,EACpB,uBAAA,EAAyB,kBAAA;AAAA,EACzB,eAAA,EAAiB,WAAA;AAAA,EACjB,kBAAA,EAAoB,cAAA;AAAA,EACpB,mBAAA,EAAqB;AACvB;AAEA,IAAM,IAAA,GAAO;AAAA,EACX,IAAA,EAAM,yBAAA;AAAA,EACN,OAAA,EAAS;AACX,CAAA;AAgBA,IAAM,MAAA,GAAwB;AAAA,EAC5B,IAAA;AAAA,EACA;AACF,CAAA;AAEA,IAAM,WAAA,GAA6B;AAAA,EACjC,OAAA,EAAS,EAAE,WAAA,EAAa,MAAA,EAAO;AAAA,EAC/B,KAAA,EAAO;AAAA,IACL,yBAAA,EAA2B,OAAA;AAAA,IAC3B,4BAAA,EAA8B,OAAA;AAAA,IAC9B,sBAAA,EAAwB,OAAA;AAAA,IACxB,iCAAA,EAAmC,MAAA;AAAA,IACnC,+BAAA,EAAiC,MAAA;AAAA,IACjC,4BAAA,EAA8B,MAAA;AAAA,IAC9B,iCAAA,EAAmC;AAAA;AAEvC;AAEA,IAAM,MAAA,GAAwB;AAAA,EAC5B,OAAA,EAAS,EAAE,WAAA,EAAa,MAAA,EAAO;AAAA,EAC/B,KAAA,EAAO;AAAA,IACL,yBAAA,EAA2B,OAAA;AAAA,IAC3B,4BAAA,EAA8B,OAAA;AAAA,IAC9B,sBAAA,EAAwB,OAAA;AAAA,IACxB,iCAAA,EAAmC,OAAA;AAAA,IACnC,+BAAA,EAAiC,OAAA;AAAA,IACjC,8BAA8B,CAAC,OAAA,EAAS,EAAE,GAAA,EAAK,IAAI,CAAA;AAAA,IACnD,iCAAA,EAAmC,CAAC,OAAA,EAAS,EAAE,SAAA,EAAW,GAAG,aAAA,EAAe,CAAA,EAAG,QAAA,EAAU,CAAA,EAAG,CAAA;AAAA,IAC5F,+BAA+B,CAAC,MAAA,EAAQ,EAAE,SAAA,EAAW,GAAG;AAAA;AAE5D;AAEA,IAAM,WAAA,GAA6B;AAAA,EACjC,GAAG,MAAA;AAAA,EACH,OAAA,EAAS;AAAA,IACP,WAAA;AAAA,IACA;AAAA;AAEJ,CAAA;AAEA,IAAO,aAAA,GAAQ","file":"index.js","sourcesContent":["import { AST_NODE_TYPES, type TSESTree } from '@typescript-eslint/utils';\n\n/**\n * Detect calls of the form `useEvent(trigger, 'event-name')`,\n * `useCondition(trigger, 'name', ...)`, `useAction(trigger, 'name', handler)`,\n * etc. The first positional argument is always the trigger reference,\n * the second is the port name as a string literal (or template literal).\n *\n * We match on the callee name only — adopters import these hooks under their\n * canonical names (and the `no-restricted-imports` rule in our recommended\n * config keeps it that way).\n */\nexport const TRIGGERY_HOOK_NAMES = new Set([\n 'useEvent',\n 'useCondition',\n 'useAction',\n 'useInlineTrigger',\n]);\n\nexport function isTriggeryHookCall(node: TSESTree.CallExpression): boolean {\n return (\n node.callee.type === AST_NODE_TYPES.Identifier && TRIGGERY_HOOK_NAMES.has(node.callee.name)\n );\n}\n\nexport function isCreateTriggerCall(node: TSESTree.CallExpression): boolean {\n const callee = node.callee;\n if (callee.type === AST_NODE_TYPES.Identifier && callee.name === 'createTrigger') return true;\n // Allow `triggery.createTrigger(...)` style.\n if (\n callee.type === AST_NODE_TYPES.MemberExpression &&\n callee.property.type === AST_NODE_TYPES.Identifier &&\n callee.property.name === 'createTrigger'\n ) {\n return true;\n }\n return false;\n}\n\nexport function getStringLiteralValue(node: TSESTree.Node | undefined): string | null {\n if (!node) return null;\n if (node.type === AST_NODE_TYPES.Literal && typeof node.value === 'string') {\n return node.value;\n }\n if (\n node.type === AST_NODE_TYPES.TemplateLiteral &&\n node.expressions.length === 0 &&\n node.quasis.length === 1 &&\n node.quasis[0]\n ) {\n return node.quasis[0].value.cooked;\n }\n return null;\n}\n\n/**\n * Convert `'new-message'` → `'NewMessage'` so we can suggest\n * `useNewMessageEvent` etc.\n */\nexport function toPascalCase(name: string): string {\n return name\n .split(/[-_\\s]+/)\n .filter(Boolean)\n .map((part) => part.charAt(0).toUpperCase() + part.slice(1))\n .join('');\n}\n\n/**\n * Walks up the AST looking for the enclosing function (declaration, expression\n * or arrow). Returns the function node, or `null` if the call is at module top\n * level.\n */\nexport function findEnclosingFunction(\n node: TSESTree.Node,\n):\n | TSESTree.FunctionDeclaration\n | TSESTree.FunctionExpression\n | TSESTree.ArrowFunctionExpression\n | null {\n let current: TSESTree.Node | undefined = node.parent;\n while (current) {\n if (\n current.type === AST_NODE_TYPES.FunctionDeclaration ||\n current.type === AST_NODE_TYPES.FunctionExpression ||\n current.type === AST_NODE_TYPES.ArrowFunctionExpression\n ) {\n return current;\n }\n current = current.parent;\n }\n return null;\n}\n\n/**\n * Heuristic — a function \"looks like a React/Solid/Vue component or hook\" if\n * its name starts with an uppercase letter (Component) or with `use`\n * (custom hook). For anonymous arrows we fall back to looking at the\n * variable declarator name.\n */\nexport function isComponentOrHookFunction(\n fn: TSESTree.FunctionDeclaration | TSESTree.FunctionExpression | TSESTree.ArrowFunctionExpression,\n): boolean {\n const name = getFunctionName(fn);\n if (!name) return false;\n return /^[A-Z]/.test(name) || /^use[A-Z]/.test(name);\n}\n\nexport function getFunctionName(\n fn: TSESTree.FunctionDeclaration | TSESTree.FunctionExpression | TSESTree.ArrowFunctionExpression,\n): string | null {\n if (fn.type === AST_NODE_TYPES.FunctionDeclaration && fn.id) return fn.id.name;\n if (fn.type === AST_NODE_TYPES.FunctionExpression && fn.id) return fn.id.name;\n const parent = fn.parent;\n if (parent && parent.type === AST_NODE_TYPES.VariableDeclarator) {\n const id = parent.id;\n if (id.type === AST_NODE_TYPES.Identifier) return id.name;\n }\n if (parent && parent.type === AST_NODE_TYPES.Property) {\n const key = parent.key;\n if (key.type === AST_NODE_TYPES.Identifier) return key.name;\n }\n return null;\n}\n","import { AST_NODE_TYPES, ESLintUtils, type TSESTree } from '@typescript-eslint/utils';\nimport { getStringLiteralValue, isCreateTriggerCall } from '../utils/triggery-call.ts';\n\nconst createRule = ESLintUtils.RuleCreator(\n (name) =>\n `https://github.com/triggeryjs/triggery/blob/main/packages/eslint-plugin/docs/rules/${name}.md`,\n);\n\ninterface TriggerInfo {\n readonly varName: string;\n readonly required: readonly string[];\n readonly node: TSESTree.Node;\n}\n\n/**\n * Cross-checks `required` against `useCondition` registrations *within the\n * same file*. If the trigger declares `required: ['user', 'settings']` and\n * the file never calls `useCondition(thisTrigger, 'user', ...)` for one of\n * them, the handler will always skip with `missing-required` — which is\n * almost certainly not what the author intended.\n *\n * File-local scope by design: cross-file analysis is significantly more\n * expensive and the file-local heuristic catches ~80% of real mistakes (a\n * trigger and its first provider almost always sit in the same module\n * during initial wiring).\n */\nexport const exhaustiveConditions = createRule({\n name: 'exhaustive-conditions',\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Ensure every `required` condition of a trigger has at least one `useCondition` registration in the same file.',\n },\n messages: {\n missing:\n \"Trigger `{{trigger}}` declares `required: [..., '{{name}}', ...]`, but this file has no `useCondition({{trigger}}, '{{name}}', ...)`. The handler will always skip with `missing-required`.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const triggers: TriggerInfo[] = [];\n const provided = new Map<string, Set<string>>(); // trigger var → set of condition names\n\n function record(triggerVar: string, conditionName: string): void {\n let set = provided.get(triggerVar);\n if (!set) {\n set = new Set();\n provided.set(triggerVar, set);\n }\n set.add(conditionName);\n }\n\n return {\n CallExpression(node) {\n if (isCreateTriggerCall(node)) {\n // Find the variable being assigned: `export const foo = createTrigger({...})`.\n const parent = node.parent;\n if (!parent || parent.type !== AST_NODE_TYPES.VariableDeclarator) return;\n if (parent.id.type !== AST_NODE_TYPES.Identifier) return;\n const varName = parent.id.name;\n\n const config = node.arguments[0];\n if (!config || config.type !== AST_NODE_TYPES.ObjectExpression) return;\n const requiredProp = config.properties.find(\n (p): p is TSESTree.Property =>\n p.type === AST_NODE_TYPES.Property &&\n p.key.type === AST_NODE_TYPES.Identifier &&\n p.key.name === 'required',\n );\n if (!requiredProp) return;\n if (requiredProp.value.type !== AST_NODE_TYPES.ArrayExpression) return;\n const required: string[] = [];\n for (const el of requiredProp.value.elements) {\n const name = getStringLiteralValue(el ?? undefined);\n if (name) required.push(name);\n }\n if (required.length > 0) triggers.push({ varName, required, node: requiredProp.value });\n return;\n }\n\n if (\n node.callee.type === AST_NODE_TYPES.Identifier &&\n node.callee.name === 'useCondition' &&\n node.arguments[0] &&\n node.arguments[0].type === AST_NODE_TYPES.Identifier &&\n node.arguments[1]\n ) {\n const triggerVar = node.arguments[0].name;\n const conditionName = getStringLiteralValue(node.arguments[1]);\n if (conditionName) record(triggerVar, conditionName);\n }\n },\n 'Program:exit'() {\n for (const trigger of triggers) {\n const set = provided.get(trigger.varName) ?? new Set<string>();\n for (const name of trigger.required) {\n if (!set.has(name)) {\n context.report({\n node: trigger.node,\n messageId: 'missing',\n data: { trigger: trigger.varName, name },\n });\n }\n }\n }\n },\n };\n },\n});\n","import { AST_NODE_TYPES, ESLintUtils, type TSESTree } from '@typescript-eslint/utils';\nimport { isCreateTriggerCall } from '../utils/triggery-call.ts';\n\nconst createRule = ESLintUtils.RuleCreator(\n (name) =>\n `https://github.com/triggeryjs/triggery/blob/main/packages/eslint-plugin/docs/rules/${name}.md`,\n);\n\n/**\n * If a trigger declares conditions in its schema generic but no `required`\n * array, the handler must do explicit `if (!conditions.x) return` for every\n * one — easy to forget. We can't read the type generic from the AST without\n * type info, but we can warn when `required` is missing on a trigger that\n * has a non-trivial handler with `conditions.*` accesses.\n *\n * Heuristic: any `createTrigger({...})` without a `required:` key — flagged\n * with severity `suggestion`. If you genuinely want no required conditions\n * (e.g. a trigger with only events and actions, or a pure analytics\n * pipeline), add `required: []` explicitly.\n */\nexport const exhaustiveRequired = createRule({\n name: 'exhaustive-required',\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Require an explicit `required` array on every createTrigger call (use `required: []` to opt out).',\n },\n messages: {\n missing:\n 'Trigger config is missing `required: [...]`. Add it explicitly so the runtime gate is unambiguous (use `required: []` if no conditions are required).',\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n return {\n CallExpression(node) {\n if (!isCreateTriggerCall(node)) return;\n const config = node.arguments[0];\n if (!config || config.type !== AST_NODE_TYPES.ObjectExpression) return;\n const hasRequired = config.properties.some(\n (p: TSESTree.ObjectLiteralElement) =>\n p.type === AST_NODE_TYPES.Property &&\n p.key.type === AST_NODE_TYPES.Identifier &&\n p.key.name === 'required',\n );\n if (!hasRequired) context.report({ node: config, messageId: 'missing' });\n },\n };\n },\n});\n","import { AST_NODE_TYPES, ESLintUtils } from '@typescript-eslint/utils';\nimport {\n findEnclosingFunction,\n isComponentOrHookFunction,\n TRIGGERY_HOOK_NAMES,\n} from '../utils/triggery-call.ts';\n\nconst createRule = ESLintUtils.RuleCreator(\n (name) =>\n `https://github.com/triggeryjs/triggery/blob/main/packages/eslint-plugin/docs/rules/${name}.md`,\n);\n\n/**\n * Triggery hooks (`useEvent`, `useCondition`, `useAction`, `useInlineTrigger`)\n * obey React's rules-of-hooks contract: they may only be called from the\n * top level of a component / hook, never inside conditionals, loops, or\n * regular functions.\n *\n * `react-hooks/rules-of-hooks` covers React; this rule is framework-neutral\n * (Solid + Vue setup() also rely on stable invocation order).\n *\n * Heuristic: the enclosing function name must start with an uppercase\n * letter (a component) or with `use` followed by an uppercase letter (a\n * custom hook). Calls at module scope are also flagged.\n */\nexport const hookRules = createRule({\n name: 'hook-rules',\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Allow Triggery hooks (useEvent/useCondition/useAction) only inside components or other hooks.',\n },\n messages: {\n outside:\n '`{{hook}}` may only be called from a component or a custom hook (function name starting with an uppercase letter or with `use[A-Z]`).',\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n return {\n CallExpression(node) {\n if (node.callee.type !== AST_NODE_TYPES.Identifier) return;\n const hook = node.callee.name;\n if (!TRIGGERY_HOOK_NAMES.has(hook)) return;\n const fn = findEnclosingFunction(node);\n if (!fn || !isComponentOrHookFunction(fn)) {\n context.report({ node, messageId: 'outside', data: { hook } });\n }\n },\n };\n },\n});\n","import { AST_NODE_TYPES, ESLintUtils, type TSESTree } from '@typescript-eslint/utils';\nimport { isCreateTriggerCall } from '../utils/triggery-call.ts';\n\nconst createRule = ESLintUtils.RuleCreator(\n (name) =>\n `https://github.com/triggeryjs/triggery/blob/main/packages/eslint-plugin/docs/rules/${name}.md`,\n);\n\n/**\n * Limits the size of a trigger's `handler` body. A 200-line handler is a\n * smell: the file no longer reads like a spec. The escape hatch is the\n * `extract-trigger` codemod (decompose into sub-triggers) or a refactor of\n * actions into smaller, more focused ones.\n *\n * The rule counts top-level statements in the handler body, not characters\n * or AST nodes — matches the way humans skim code.\n */\nexport const maxHandlerSize = createRule<[{ max?: number }], 'tooLarge'>({\n name: 'max-handler-size',\n meta: {\n type: 'suggestion',\n docs: {\n description: 'Limit the size of a trigger handler body (default 50 statements).',\n },\n messages: {\n tooLarge:\n 'Trigger handler has {{count}} top-level statements, which exceeds the limit of {{max}}. Consider splitting it (see the `extract-trigger` codemod) or moving logic into smaller actions.',\n },\n schema: [\n {\n type: 'object',\n additionalProperties: false,\n properties: {\n max: { type: 'integer', minimum: 1 },\n },\n },\n ],\n },\n defaultOptions: [{ max: 50 }],\n create(context, [options]) {\n const max = options.max ?? 50;\n return {\n CallExpression(node) {\n if (!isCreateTriggerCall(node)) return;\n const config = node.arguments[0];\n if (!config || config.type !== AST_NODE_TYPES.ObjectExpression) return;\n for (const prop of config.properties) {\n if (prop.type !== AST_NODE_TYPES.Property) continue;\n if (prop.key.type !== AST_NODE_TYPES.Identifier || prop.key.name !== 'handler') continue;\n const handler = prop.value;\n let body: TSESTree.BlockStatement | null = null;\n if (\n handler.type === AST_NODE_TYPES.FunctionExpression ||\n handler.type === AST_NODE_TYPES.ArrowFunctionExpression\n ) {\n if (handler.body.type === AST_NODE_TYPES.BlockStatement) body = handler.body;\n }\n if (!body) return;\n const count = body.body.length;\n if (count > max) {\n context.report({\n node: handler,\n messageId: 'tooLarge',\n data: { count, max },\n });\n }\n return;\n }\n },\n };\n },\n});\n","import { AST_NODE_TYPES, ESLintUtils, type TSESTree } from '@typescript-eslint/utils';\nimport { isCreateTriggerCall } from '../utils/triggery-call.ts';\n\nconst createRule = ESLintUtils.RuleCreator(\n (name) =>\n `https://github.com/triggeryjs/triggery/blob/main/packages/eslint-plugin/docs/rules/${name}.md`,\n);\n\n/**\n * A trigger with 15 events × 12 conditions × 8 actions is a god-trigger.\n * The \"scenario reads like a spec\" property is lost.\n *\n * We approximate the port count from the runtime-visible config:\n *\n * - `events` — count of strings in the array\n * - `required` — count of strings in the array\n * - + the generic schema's `actions` keys we can see (TypeScript type\n * args aren't AST-visible without type info; we count what's in the\n * config instead, which is a fair lower bound).\n *\n * Configurable per-port via options: { maxEvents, maxConditions, maxTotal }.\n */\nexport const maxPortsPerTrigger = createRule<\n [{ maxEvents?: number; maxConditions?: number; maxTotal?: number }],\n 'tooManyTotal' | 'tooManyEvents' | 'tooManyConditions'\n>({\n name: 'max-ports-per-trigger',\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Cap the number of events / required-conditions per trigger to keep scenarios spec-like.',\n },\n messages: {\n tooManyTotal:\n 'Trigger has {{count}} ports declared in its config (limit: {{max}}). Consider splitting it.',\n tooManyEvents:\n 'Trigger lists {{count}} events (limit: {{max}}). Split scenarios that fire on different events.',\n tooManyConditions:\n 'Trigger requires {{count}} conditions (limit: {{max}}). Consider whether all are truly required, or move some to opt-in checks via `check.is()`.',\n },\n schema: [\n {\n type: 'object',\n additionalProperties: false,\n properties: {\n maxEvents: { type: 'integer', minimum: 1 },\n maxConditions: { type: 'integer', minimum: 1 },\n maxTotal: { type: 'integer', minimum: 1 },\n },\n },\n ],\n },\n defaultOptions: [{ maxEvents: 8, maxConditions: 8, maxTotal: 12 }],\n create(context, [options]) {\n const maxEvents = options.maxEvents ?? 8;\n const maxConditions = options.maxConditions ?? 8;\n const maxTotal = options.maxTotal ?? 12;\n\n function countStringArrayProp(config: TSESTree.ObjectExpression, propName: string): number {\n const prop = config.properties.find(\n (p): p is TSESTree.Property =>\n p.type === AST_NODE_TYPES.Property &&\n p.key.type === AST_NODE_TYPES.Identifier &&\n p.key.name === propName,\n );\n if (!prop) return 0;\n if (prop.value.type !== AST_NODE_TYPES.ArrayExpression) return 0;\n return prop.value.elements.length;\n }\n\n return {\n CallExpression(node) {\n if (!isCreateTriggerCall(node)) return;\n const config = node.arguments[0];\n if (!config || config.type !== AST_NODE_TYPES.ObjectExpression) return;\n const events = countStringArrayProp(config, 'events');\n const required = countStringArrayProp(config, 'required');\n const total = events + required;\n\n if (events > maxEvents) {\n context.report({\n node: config,\n messageId: 'tooManyEvents',\n data: { count: events, max: maxEvents },\n });\n }\n if (required > maxConditions) {\n context.report({\n node: config,\n messageId: 'tooManyConditions',\n data: { count: required, max: maxConditions },\n });\n }\n if (total > maxTotal) {\n context.report({\n node: config,\n messageId: 'tooManyTotal',\n data: { count: total, max: maxTotal },\n });\n }\n },\n };\n },\n});\n","import { AST_NODE_TYPES, ESLintUtils } from '@typescript-eslint/utils';\nimport { getStringLiteralValue, isCreateTriggerCall } from '../utils/triggery-call.ts';\n\nconst createRule = ESLintUtils.RuleCreator(\n (name) =>\n `https://github.com/triggeryjs/triggery/blob/main/packages/eslint-plugin/docs/rules/${name}.md`,\n);\n\n/**\n * Trigger ids are used as keys in the runtime registry, in the inspector and\n * in devtools/Redux action labels. They MUST be string literals so that:\n *\n * - Auto-discovery (`@triggery/vite`) can produce a stable virtual module.\n * - The inspector and devtools UI can group runs by id deterministically.\n * - `triggery graph` / `triggery scaffold` can detect collisions.\n */\nexport const noDynamicId = createRule({\n name: 'no-dynamic-id',\n meta: {\n type: 'problem',\n docs: {\n description: 'Require createTrigger({ id }) to be a string literal.',\n },\n messages: {\n dynamic:\n '`createTrigger` id must be a string literal, not a dynamic expression. The id is used as a registry key, in devtools and in graph(), and must be deterministic.',\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n return {\n CallExpression(node) {\n if (!isCreateTriggerCall(node)) return;\n const config = node.arguments[0];\n if (!config || config.type !== AST_NODE_TYPES.ObjectExpression) return;\n for (const prop of config.properties) {\n if (prop.type !== AST_NODE_TYPES.Property) continue;\n if (prop.key.type !== AST_NODE_TYPES.Identifier || prop.key.name !== 'id') continue;\n if (getStringLiteralValue(prop.value) === null) {\n context.report({ node: prop.value, messageId: 'dynamic' });\n }\n return;\n }\n },\n };\n },\n});\n","import { AST_NODE_TYPES, ESLintUtils, type TSESTree } from '@typescript-eslint/utils';\n\nconst createRule = ESLintUtils.RuleCreator(\n (name) =>\n `https://github.com/triggeryjs/triggery/blob/main/packages/eslint-plugin/docs/rules/${name}.md`,\n);\n\n/**\n * Calling `useEvent(...)` inside a `useAction(...)` handler creates an\n * implicit \"action → fireEvent\" cascade. Runtime allows cascades up to\n * `maxCascadeDepth` (default 3), but in source they should be explicit,\n * because debugging a hidden cascade across files is painful.\n *\n * If a cascade is *intentional*, the recommended pattern is to expose the\n * downstream event by passing the fire-fn down explicitly, or by composing\n * triggers via a higher-level orchestrator.\n */\nexport const noEventCascade = createRule({\n name: 'no-event-cascade',\n meta: {\n type: 'problem',\n docs: {\n description: 'Disallow calling useEvent inside a useAction handler (implicit cascade).',\n },\n messages: {\n cascade:\n 'Avoid calling `{{callee}}` inside a `useAction` handler — this creates an implicit cascade. Compose triggers explicitly instead.',\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n // Stack of CallExpression nodes representing the currently-open\n // `useAction(...)` handlers. We push when entering the handler argument\n // and pop on exit.\n const useActionHandlers: TSESTree.Node[] = [];\n\n function isUseActionHandlerArgument(node: TSESTree.Node): boolean {\n const parent = node.parent;\n if (!parent || parent.type !== AST_NODE_TYPES.CallExpression) return false;\n // Third positional argument is the handler.\n if (parent.arguments[2] !== node) return false;\n const callee = parent.callee;\n return callee.type === AST_NODE_TYPES.Identifier && callee.name === 'useAction';\n }\n\n function enterFn(\n node:\n | TSESTree.FunctionDeclaration\n | TSESTree.FunctionExpression\n | TSESTree.ArrowFunctionExpression,\n ): void {\n if (isUseActionHandlerArgument(node)) useActionHandlers.push(node);\n }\n\n function exitFn(\n node:\n | TSESTree.FunctionDeclaration\n | TSESTree.FunctionExpression\n | TSESTree.ArrowFunctionExpression,\n ): void {\n if (useActionHandlers[useActionHandlers.length - 1] === node) useActionHandlers.pop();\n }\n\n return {\n FunctionExpression: enterFn,\n ArrowFunctionExpression: enterFn,\n FunctionDeclaration: enterFn,\n 'FunctionExpression:exit': exitFn,\n 'ArrowFunctionExpression:exit': exitFn,\n 'FunctionDeclaration:exit': exitFn,\n CallExpression(node) {\n if (useActionHandlers.length === 0) return;\n if (node.callee.type !== AST_NODE_TYPES.Identifier) return;\n if (node.callee.name !== 'useEvent') return;\n context.report({\n node,\n messageId: 'cascade',\n data: { callee: node.callee.name },\n });\n },\n };\n },\n});\n","import { AST_NODE_TYPES, ESLintUtils, type TSESTree } from '@typescript-eslint/utils';\nimport { getStringLiteralValue, toPascalCase } from '../utils/triggery-call.ts';\n\nconst createRule = ESLintUtils.RuleCreator(\n (name) =>\n `https://github.com/triggeryjs/triggery/blob/main/packages/eslint-plugin/docs/rules/${name}.md`,\n);\n\n/**\n * In files with many `useEvent/useCondition/useAction` calls, the named\n * variants (`useNewMessageEvent`, `useUserCondition`, …) read much better.\n * The runtime exposes them via `messageTrigger.namedHooks()`; this rule\n * suggests the switch once a file has enough port calls to benefit.\n */\nexport const preferNamedHook = createRule<[{ threshold?: number }], 'prefer'>({\n name: 'prefer-named-hook',\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'In files with many port calls, prefer named hooks (useFooEvent) over generic useEvent(\"foo\").',\n },\n messages: {\n prefer:\n 'Consider using `{{suggestion}}` instead of `{{generic}}` once a file has many ({{count}} ≥ {{threshold}}) port calls.',\n },\n schema: [\n {\n type: 'object',\n additionalProperties: false,\n properties: {\n threshold: { type: 'integer', minimum: 1 },\n },\n },\n ],\n },\n defaultOptions: [{ threshold: 4 }],\n create(context, [options]) {\n const threshold = options.threshold ?? 4;\n const portCalls: Array<{ node: TSESTree.CallExpression; kind: string; name: string }> = [];\n\n function suffixFor(kind: string): string {\n switch (kind) {\n case 'useEvent':\n return 'Event';\n case 'useCondition':\n return 'Condition';\n case 'useAction':\n return 'Action';\n default:\n return '';\n }\n }\n\n return {\n CallExpression(node) {\n if (node.callee.type !== AST_NODE_TYPES.Identifier) return;\n const name = node.callee.name;\n if (!['useEvent', 'useCondition', 'useAction'].includes(name)) return;\n const portArg = node.arguments[1];\n const portName = getStringLiteralValue(portArg);\n if (!portName) return;\n portCalls.push({ node, kind: name, name: portName });\n },\n 'Program:exit'() {\n if (portCalls.length < threshold) return;\n for (const call of portCalls) {\n const suggestion = `use${toPascalCase(call.name)}${suffixFor(call.kind)}`;\n context.report({\n node: call.node,\n messageId: 'prefer',\n data: {\n suggestion,\n generic: `${call.kind}(…, '${call.name}', …)`,\n count: portCalls.length,\n threshold,\n },\n });\n }\n },\n };\n },\n});\n","import type { ESLint, Linter } from 'eslint';\nimport { exhaustiveConditions } from './rules/exhaustive-conditions.ts';\nimport { exhaustiveRequired } from './rules/exhaustive-required.ts';\nimport { hookRules } from './rules/hook-rules.ts';\nimport { maxHandlerSize } from './rules/max-handler-size.ts';\nimport { maxPortsPerTrigger } from './rules/max-ports-per-trigger.ts';\nimport { noDynamicId } from './rules/no-dynamic-id.ts';\nimport { noEventCascade } from './rules/no-event-cascade.ts';\nimport { preferNamedHook } from './rules/prefer-named-hook.ts';\n\nconst rules = {\n 'exhaustive-conditions': exhaustiveConditions,\n 'exhaustive-required': exhaustiveRequired,\n 'hook-rules': hookRules,\n 'max-handler-size': maxHandlerSize,\n 'max-ports-per-trigger': maxPortsPerTrigger,\n 'no-dynamic-id': noDynamicId,\n 'no-event-cascade': noEventCascade,\n 'prefer-named-hook': preferNamedHook,\n};\n\nconst meta = {\n name: '@triggery/eslint-plugin',\n version: '0.0.0',\n};\n\n/**\n * Drop-in flat config presets. Adopters either spread our preset or pick the\n * individual rule keys they want — both work with ESLint 9.x flat config.\n *\n * @example\n * ```js\n * import triggery from '@triggery/eslint-plugin';\n *\n * export default [\n * triggery.configs.recommended,\n * triggery.configs.strict, // optionally on top\n * ];\n * ```\n */\nconst plugin: ESLint.Plugin = {\n meta,\n rules: rules as unknown as ESLint.Plugin['rules'],\n};\n\nconst recommended: Linter.Config = {\n plugins: { '@triggery': plugin },\n rules: {\n '@triggery/no-dynamic-id': 'error',\n '@triggery/no-event-cascade': 'error',\n '@triggery/hook-rules': 'error',\n '@triggery/exhaustive-conditions': 'warn',\n '@triggery/exhaustive-required': 'warn',\n '@triggery/max-handler-size': 'warn',\n '@triggery/max-ports-per-trigger': 'warn',\n },\n};\n\nconst strict: Linter.Config = {\n plugins: { '@triggery': plugin },\n rules: {\n '@triggery/no-dynamic-id': 'error',\n '@triggery/no-event-cascade': 'error',\n '@triggery/hook-rules': 'error',\n '@triggery/exhaustive-conditions': 'error',\n '@triggery/exhaustive-required': 'error',\n '@triggery/max-handler-size': ['error', { max: 30 }],\n '@triggery/max-ports-per-trigger': ['error', { maxEvents: 5, maxConditions: 5, maxTotal: 8 }],\n '@triggery/prefer-named-hook': ['warn', { threshold: 3 }],\n },\n};\n\nconst finalPlugin: ESLint.Plugin = {\n ...plugin,\n configs: {\n recommended,\n strict,\n },\n};\n\nexport default finalPlugin;\nexport { recommended, rules, strict };\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@triggery/eslint-plugin",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "ESLint plugin for Triggery — catches mis-use of createTrigger / useEvent / useCondition / useAction, suggests named hooks, enforces handler size budgets.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Aleksey Skhomenko",
|
|
7
|
+
"homepage": "https://triggeryjs.github.io/triggery",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/triggeryjs/triggery.git",
|
|
11
|
+
"directory": "packages/eslint-plugin"
|
|
12
|
+
},
|
|
13
|
+
"bugs": "https://github.com/triggeryjs/triggery/issues",
|
|
14
|
+
"funding": [
|
|
15
|
+
{
|
|
16
|
+
"type": "patreon",
|
|
17
|
+
"url": "https://www.patreon.com/triggery"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"type": "boosty",
|
|
21
|
+
"url": "https://boosty.to/triggery"
|
|
22
|
+
}
|
|
23
|
+
],
|
|
24
|
+
"keywords": [
|
|
25
|
+
"triggery",
|
|
26
|
+
"eslint",
|
|
27
|
+
"eslint-plugin",
|
|
28
|
+
"lint",
|
|
29
|
+
"rules",
|
|
30
|
+
"flat-config",
|
|
31
|
+
"typescript"
|
|
32
|
+
],
|
|
33
|
+
"type": "module",
|
|
34
|
+
"main": "./dist/index.js",
|
|
35
|
+
"module": "./dist/index.js",
|
|
36
|
+
"types": "./dist/index.d.ts",
|
|
37
|
+
"exports": {
|
|
38
|
+
".": {
|
|
39
|
+
"source": "./src/index.ts",
|
|
40
|
+
"types": "./dist/index.d.ts",
|
|
41
|
+
"import": "./dist/index.js",
|
|
42
|
+
"default": "./dist/index.js"
|
|
43
|
+
},
|
|
44
|
+
"./package.json": "./package.json"
|
|
45
|
+
},
|
|
46
|
+
"files": [
|
|
47
|
+
"dist",
|
|
48
|
+
"README.md",
|
|
49
|
+
"LICENSE",
|
|
50
|
+
"CHANGELOG.md"
|
|
51
|
+
],
|
|
52
|
+
"sideEffects": false,
|
|
53
|
+
"publishConfig": {
|
|
54
|
+
"access": "public"
|
|
55
|
+
},
|
|
56
|
+
"peerDependencies": {
|
|
57
|
+
"eslint": ">=9.0.0",
|
|
58
|
+
"typescript": ">=5.0.0"
|
|
59
|
+
},
|
|
60
|
+
"dependencies": {
|
|
61
|
+
"@typescript-eslint/utils": "^8.0.0"
|
|
62
|
+
},
|
|
63
|
+
"devDependencies": {
|
|
64
|
+
"@types/estree": "^1.0.6",
|
|
65
|
+
"@typescript-eslint/parser": "^8.0.0",
|
|
66
|
+
"@typescript-eslint/rule-tester": "^8.0.0",
|
|
67
|
+
"eslint": "^9.0.0",
|
|
68
|
+
"tsup": "^8.5.1",
|
|
69
|
+
"typescript": "^6.0.3",
|
|
70
|
+
"vitest": "^4.1.6"
|
|
71
|
+
},
|
|
72
|
+
"scripts": {
|
|
73
|
+
"build": "tsup",
|
|
74
|
+
"dev": "tsup --watch",
|
|
75
|
+
"test": "vitest run",
|
|
76
|
+
"test:watch": "vitest",
|
|
77
|
+
"test:coverage": "vitest run --coverage",
|
|
78
|
+
"clean": "rm -rf dist *.tsbuildinfo"
|
|
79
|
+
}
|
|
80
|
+
}
|