@cratis/eslint-plugin-components 4.14.0 → 4.14.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -19
- package/lib/onbeforeexecuteMustReturn.js +56 -32
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,22 +7,17 @@ Cratis base config, [`@cratis/eslint-config`](https://www.npmjs.com/package/@cra
|
|
|
7
7
|
| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
8
8
|
| `no-root-barrel-import` | Disallows importing a removed Components 3 component namespace from the Components 4 setup-only root. Use the exact component subpath (`@cratis/components/CommandDialog`, `@cratis/components/DataPage`, `@cratis/components/Toolbar`, …). Package-wide provider/configuration symbols remain allowed at the root; an unambiguous namespace violation is autofixed. |
|
|
9
9
|
| `no-primereact-dialog` | Disallows importing `Dialog` from `primereact/dialog`. Use `CommandDialog` from `@cratis/components/CommandDialog`, or `Dialog` from `@cratis/components/Dialogs` — the wrappers add Arc command binding, overlay/focus fixes, and theming. |
|
|
10
|
-
| `onbeforeexecute-must-return` | Requires
|
|
10
|
+
| `onbeforeexecute-must-return` | Requires `onBeforeExecute` on imported Components command dialogs/steppers or Arc `CommandForm` to return command values. It does not flag Arc `CommandScope`, whose callback is void. |
|
|
11
11
|
| `no-hooks-in-view-model` | Disallows React hooks (including generated Arc proxies' `.use()`) inside a view model class. View models must be plain, hook-free classes that receive injected abstractions. |
|
|
12
12
|
| `no-raw-command-form-marker` | Disallows identifying a CommandForm field or column by a hand-written `displayName` string, in either direction. Use `markAsCommandFormField`/`markAsCommandFormColumn` and `isCommandFormField`/`isCommandFormColumn` from `@cratis/components/CommandForm` — they go through a marker a build transform cannot rewrite. |
|
|
13
13
|
| `no-react-in-kernel` | Repository architecture rule that rejects React, React DOM, React Aria Components, and browser DOM globals in the explicit Components kernel inventory. It is intentionally not part of the consumer `recommended` config. |
|
|
14
14
|
|
|
15
15
|
Both import rules cover static `import` and re-`export … from` forms. The root-barrel rule also reports TypeScript `import = require(...)`, dynamic `import(...)`, and CommonJS `require(...)` of the package root; ambiguous forms are never autofixed.
|
|
16
16
|
|
|
17
|
-
> **Publication status:** The install examples target the owner-authorized 4.0.0 npm release. When
|
|
18
|
-
> reading this README from repository source before that release, verify availability with
|
|
19
|
-
> `npm view @cratis/eslint-plugin-components@4.0.0 version`; source contributors use the workspace
|
|
20
|
-
> commands in this checkout instead.
|
|
21
|
-
|
|
22
17
|
## Install
|
|
23
18
|
|
|
24
|
-
|
|
25
|
-
`
|
|
19
|
+
The plugin is released at the same version as `@cratis/components`. Install the Components 4
|
|
20
|
+
tooling train bounded to `>=4 <5` so a future major cannot arrive unannounced. Never use `latest`:
|
|
26
21
|
|
|
27
22
|
```sh
|
|
28
23
|
TOOLING_RANGE='^4.0.0' # Shell-safe equivalent of >=4 <5.
|
|
@@ -100,7 +95,8 @@ Components 4 removes every component-family namespace from the package root. The
|
|
|
100
95
|
|
|
101
96
|
- `CratisComponentsProvider`, `useCratisComponentsConfig`, `cratisDefaults`, and `mergeCratisComponentsConfig`;
|
|
102
97
|
- `CratisComponentsConfig`, `CratisComponentsProviderProps`, and `CratisComponentsMessages`;
|
|
103
|
-
- `CratisPaginatorMessages`, `CratisDatePickerMessages`, `CratisDropdownMessages`, `CratisDialogMessages`, `CratisStepperMessages`, `CratisNotificationsMessages`, `CratisDataTableMessages`, and `CratisColumnFilterMessages
|
|
98
|
+
- `CratisPaginatorMessages`, `CratisDatePickerMessages`, `CratisDropdownMessages`, `CratisDialogMessages`, `CratisStepperMessages`, `CratisNotificationsMessages`, `CratisDataTableMessages`, and `CratisColumnFilterMessages`;
|
|
99
|
+
- `CratisComponentsIcons`, `CratisIconResolver`, and `useCratisIcon`.
|
|
104
100
|
|
|
105
101
|
```ts
|
|
106
102
|
// ✅ approved setup symbols stay at the root
|
|
@@ -142,8 +138,10 @@ The rule never guesses. Each of these is flagged with guidance but **not** autof
|
|
|
142
138
|
- The Components 3.6 `Compatibility` namespace and direct pass-through exports — reported
|
|
143
139
|
as known removals with typed-parts guidance and never autofixed because Components 4 has
|
|
144
140
|
no compatibility subpath.
|
|
145
|
-
- Any `export … from '@cratis/components'` re-export
|
|
146
|
-
|
|
141
|
+
- Any `export … from '@cratis/components'` re-export of a namespace, removed compatibility
|
|
142
|
+
export, or unknown symbol, and any wildcard re-export — flagged with the same subpath
|
|
143
|
+
guidance, but re-exports are never autofixed. Re-exporting an approved setup symbol is
|
|
144
|
+
allowed.
|
|
147
145
|
|
|
148
146
|
A companion, standalone codemod applies the equivalent rewrite across a whole project in one
|
|
149
147
|
pass, and additionally autofixes a named `export { Canvas } from '@cratis/components'`
|
|
@@ -153,10 +151,12 @@ form with guidance but never autofixes any of them; see
|
|
|
153
151
|
|
|
154
152
|
### `onbeforeexecute-must-return`
|
|
155
153
|
|
|
156
|
-
`onBeforeExecute`
|
|
157
|
-
**transformer**: it receives the current command values and must **return** them (mutated or
|
|
158
|
-
not).
|
|
159
|
-
guard (`applyBeforeExecute`) falls back to the current object and logs a
|
|
154
|
+
`onBeforeExecute` on `CommandDialog`, `StepperCommandDialog`, `CommandStepper`, and Arc's
|
|
155
|
+
`CommandForm` is a **transformer**: it receives the current command values and must **return** them (mutated or
|
|
156
|
+
not). In the Components wrappers, a callback that returns nothing does not run the command with
|
|
157
|
+
`undefined` — a runtime guard (`applyBeforeExecute`) falls back to the current object and logs a
|
|
158
|
+
`console.warn`. Arc's standalone `CommandForm` (22.16.0) uses the returned value directly, with no
|
|
159
|
+
such fallback.
|
|
160
160
|
In-place mutations may therefore remain, but a replacement object is discarded and the call
|
|
161
161
|
still violates the transformer contract. TypeScript catches this at fully typed call sites;
|
|
162
162
|
this rule is the static backstop for JavaScript and loosely typed consumers, so behavior never
|
|
@@ -173,10 +173,7 @@ relies on the fallback.
|
|
|
173
173
|
<CommandDialog onBeforeExecute={values => { return; }} />
|
|
174
174
|
```
|
|
175
175
|
|
|
176
|
-
|
|
177
|
-
and variable (`const onBeforeExecute = …`) forms. It is a lint backstop, not a full
|
|
178
|
-
control-flow analysis: a callback that returns a value on some branches but can still fall
|
|
179
|
-
through is not flagged.
|
|
176
|
+
The rule checks JSX callbacks only when the component resolves through an import from `@cratis/components/CommandDialog` (`CommandDialog`, `StepperCommandDialog`, `CommandStepper`), `@cratis/components/CommandStepper` (`CommandStepper`), or `@cratis/arc.react/commands` (`CommandForm`). Aliases and namespace imports are supported. Unknown components, object properties, and standalone variables are not flagged because their callback contract cannot be inferred. In particular, Arc `CommandScope` has a void-returning `onBeforeExecute` side-effect hook, not a transformer, so it is not flagged. This rule is not a full control-flow analysis: a callback that returns a value on some branches but can still fall through is not flagged.
|
|
180
177
|
|
|
181
178
|
### `no-hooks-in-view-model`
|
|
182
179
|
|
|
@@ -32,39 +32,22 @@ const keyNameOf = (key) => {
|
|
|
32
32
|
return undefined;
|
|
33
33
|
};
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
35
|
+
const transformerExports = new Map([
|
|
36
|
+
[
|
|
37
|
+
'@cratis/components/CommandDialog',
|
|
38
|
+
new Set(['CommandDialog', 'StepperCommandDialog', 'CommandStepper']),
|
|
39
|
+
],
|
|
40
|
+
['@cratis/components/CommandStepper', new Set(['CommandStepper'])],
|
|
41
|
+
['@cratis/arc.react/commands', new Set(['CommandForm'])],
|
|
42
|
+
]);
|
|
41
43
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
return true;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if (
|
|
51
|
-
(parent.type === 'Property' || parent.type === 'PropertyDefinition') &&
|
|
52
|
-
parent.value === node &&
|
|
53
|
-
keyNameOf(parent.key) === 'onBeforeExecute'
|
|
54
|
-
) {
|
|
55
|
-
return true;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
if (
|
|
59
|
-
parent.type === 'VariableDeclarator' &&
|
|
60
|
-
parent.init === node &&
|
|
61
|
-
parent.id?.type === 'Identifier' &&
|
|
62
|
-
parent.id.name === 'onBeforeExecute'
|
|
63
|
-
) {
|
|
64
|
-
return true;
|
|
44
|
+
const importBinding = (scope, name) => {
|
|
45
|
+
for (let current = scope; current; current = current.upper) {
|
|
46
|
+
const variable = current.set.get(name);
|
|
47
|
+
if (variable)
|
|
48
|
+
return variable.defs.find(definition => definition.type === 'ImportBinding')?.node;
|
|
65
49
|
}
|
|
66
|
-
|
|
67
|
-
return false;
|
|
50
|
+
return undefined;
|
|
68
51
|
};
|
|
69
52
|
|
|
70
53
|
// Require an `onBeforeExecute` callback to return the command values on every path. It is a
|
|
@@ -94,8 +77,34 @@ export const onbeforeexecuteMustReturn = {
|
|
|
94
77
|
},
|
|
95
78
|
},
|
|
96
79
|
create(context) {
|
|
80
|
+
const imports = new Map();
|
|
81
|
+
const isTransformer = (node) => {
|
|
82
|
+
const attribute = node.parent?.parent;
|
|
83
|
+
if (
|
|
84
|
+
node.parent?.type !== 'JSXExpressionContainer' ||
|
|
85
|
+
attribute?.type !== 'JSXAttribute' ||
|
|
86
|
+
attribute.name?.name !== 'onBeforeExecute'
|
|
87
|
+
) return false;
|
|
88
|
+
const opening = attribute.parent;
|
|
89
|
+
if (opening?.type !== 'JSXOpeningElement') return false;
|
|
90
|
+
const tag = opening.name;
|
|
91
|
+
const identifier = tag.type === 'JSXIdentifier'
|
|
92
|
+
? tag
|
|
93
|
+
: tag.type === 'JSXMemberExpression' &&
|
|
94
|
+
tag.object.type === 'JSXIdentifier' &&
|
|
95
|
+
tag.property.type === 'JSXIdentifier'
|
|
96
|
+
? tag.object
|
|
97
|
+
: undefined;
|
|
98
|
+
if (!identifier) return false;
|
|
99
|
+
const specifier = importBinding(context.sourceCode.getScope(node), identifier.name);
|
|
100
|
+
const imported = imports.get(specifier);
|
|
101
|
+
if (!imported) return false;
|
|
102
|
+
return tag.type === 'JSXIdentifier'
|
|
103
|
+
? imported.kind === 'named'
|
|
104
|
+
: imported.kind === 'namespace' && imported.exports.has(tag.property.name);
|
|
105
|
+
};
|
|
97
106
|
const check = (node) => {
|
|
98
|
-
if (!
|
|
107
|
+
if (!isTransformer(node)) return;
|
|
99
108
|
// An expression-bodied arrow (`values => values`) always returns a value.
|
|
100
109
|
if (
|
|
101
110
|
node.type === 'ArrowFunctionExpression' &&
|
|
@@ -118,6 +127,21 @@ export const onbeforeexecuteMustReturn = {
|
|
|
118
127
|
};
|
|
119
128
|
|
|
120
129
|
return {
|
|
130
|
+
ImportDeclaration(node) {
|
|
131
|
+
const exports = transformerExports.get(node.source.value);
|
|
132
|
+
if (!exports || node.importKind === 'type') return;
|
|
133
|
+
for (const specifier of node.specifiers) {
|
|
134
|
+
if (specifier.type === 'ImportNamespaceSpecifier') {
|
|
135
|
+
imports.set(specifier, { kind: 'namespace', exports });
|
|
136
|
+
} else if (
|
|
137
|
+
specifier.type === 'ImportSpecifier' &&
|
|
138
|
+
specifier.importKind !== 'type' &&
|
|
139
|
+
exports.has(keyNameOf(specifier.imported))
|
|
140
|
+
) {
|
|
141
|
+
imports.set(specifier, { kind: 'named' });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
},
|
|
121
145
|
ArrowFunctionExpression: check,
|
|
122
146
|
FunctionExpression: check,
|
|
123
147
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cratis/eslint-plugin-components",
|
|
3
|
-
"version": "4.14.
|
|
3
|
+
"version": "4.14.2",
|
|
4
4
|
"description": "Cratis Components ESLint rules: import from subpaths not the root barrel, use the Cratis dialog wrappers instead of primereact/dialog, require onBeforeExecute callbacks to return their values, keep React hooks out of view models, protect the repository-owned React-free kernel, and identify CommandForm fields by a tamper-resistant marker rather than a raw displayName string. Compose on top of @cratis/eslint-config.",
|
|
5
5
|
"author": "Cratis",
|
|
6
6
|
"license": "MIT",
|