@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 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 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. |
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
- Use the independently released Components 4 tooling train bounded to `>=4 <5`. Never use
25
- `latest`; plugin patches can release independently from Components Core:
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 form — flagged with the same subpath
146
- guidance, but re-exports are never autofixed.
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` (on `CommandDialog`, `StepperCommandDialog`, `CommandScope`, …) is a
157
- **transformer**: it receives the current command values and must **return** them (mutated or
158
- not). A callback that returns nothing does not run the command with `undefined` — a runtime
159
- guard (`applyBeforeExecute`) falls back to the current object and logs a `console.warn`.
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
- 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.
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
- // 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.0",
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",