@cratis/eslint-plugin-components 4.14.1 → 4.14.3

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 CHANGED
@@ -7,7 +7,7 @@ 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 an `onBeforeExecute` callback to return the command values. The runtime guards a missing return by keeping the current object and warning, but a replacement value is discarded and the callback violates the transformer contract. |
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. |
@@ -173,15 +173,7 @@ relies on the fallback.
173
173
  <CommandDialog onBeforeExecute={values => { return; }} />
174
174
  ```
175
175
 
176
- It flags JSX attribute (`onBeforeExecute={…}`), object property (`{ onBeforeExecute: … }`),
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.
180
-
181
- The rule matches the `onBeforeExecute` name, not the component. Arc's `CommandScope` also has an
182
- `onBeforeExecute` prop, but it is typed `(command) => void` and is not a transformer; the rule
183
- still reports a callback there that returns nothing. Return the command (harmless) or disable the
184
- rule for that line.
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.
185
177
 
186
178
  ### `no-hooks-in-view-model`
187
179
 
@@ -32,39 +32,22 @@ const keyNameOf = (key) => {
32
32
  return undefined;
33
33
  };
34
34
 
35
- // True when `node` is the function assigned to an `onBeforeExecute` binding — a JSX
36
- // attribute (`onBeforeExecute={...}`), an object property (`{ onBeforeExecute: ... }`), or a
37
- // variable (`const onBeforeExecute = ...`).
38
- const isOnBeforeExecuteCallback = (node) => {
39
- const parent = node.parent;
40
- if (!parent) return false;
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
- if (
43
- parent.type === 'JSXExpressionContainer' &&
44
- parent.parent?.type === 'JSXAttribute' &&
45
- parent.parent.name?.name === 'onBeforeExecute'
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 (!isOnBeforeExecuteCallback(node)) return;
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.1",
3
+ "version": "4.14.3",
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",