@cratis/eslint-plugin-components 3.6.0 → 4.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Cratis
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 CHANGED
@@ -3,20 +3,37 @@
3
3
  ESLint rules for projects that consume Cratis Components. Compose these on top of the
4
4
  Cratis base config, [`@cratis/eslint-config`](https://www.npmjs.com/package/@cratis/eslint-config).
5
5
 
6
- | Rule | What it does |
7
- |---|---|
8
- | `no-root-barrel-import` | Disallows importing from the `@cratis/components` root barrel. Use a subpath export (`@cratis/components/CommandDialog`, `@cratis/components/DataPage`, `@cratis/components/Toolbar`, …) — the root pulls the whole optional-peer-heavy surface and hides intent. |
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. `onBeforeExecute` is a transformer — a body that can complete without returning executes the command with `undefined` (silent data loss). |
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
- | `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
-
14
- The two import rules cover `import` and re-`export … from` forms.
6
+ | Rule | What it does |
7
+ | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
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
+ | `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. |
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
+ | `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
+ | `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
+
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
+
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.
15
21
 
16
22
  ## Install
17
23
 
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:
26
+
18
27
  ```sh
19
- yarn add -D @cratis/eslint-plugin-components @cratis/eslint-config eslint
28
+ TOOLING_RANGE='^4.0.0' # Shell-safe equivalent of >=4 <5.
29
+
30
+ # Choose the application package manager:
31
+ npm install --save-dev "@cratis/eslint-plugin-components@$TOOLING_RANGE" \
32
+ @cratis/eslint-config eslint
33
+ pnpm add --save-dev "@cratis/eslint-plugin-components@$TOOLING_RANGE" \
34
+ @cratis/eslint-config eslint
35
+ yarn add --dev "@cratis/eslint-plugin-components@$TOOLING_RANGE" \
36
+ @cratis/eslint-config eslint
20
37
  ```
21
38
 
22
39
  ## Use
@@ -38,7 +55,7 @@ export default [
38
55
  ```js
39
56
  '@cratis/components/no-root-barrel-import': ['error', {
40
57
  packageName: '@cratis/components', // barrel to forbid
41
- allow: [], // exact specifiers to permit
58
+ allow: [], // exact specifiers to permit wholesale
42
59
  }],
43
60
  '@cratis/components/no-primereact-dialog': ['error', {
44
61
  source: 'primereact/dialog', // module to forbid
@@ -47,24 +64,112 @@ export default [
47
64
 
48
65
  `onbeforeexecute-must-return`, `no-hooks-in-view-model` and `no-raw-command-form-marker` take no options.
49
66
 
67
+ `no-react-in-kernel` is a repository-owned architecture guard and is not enabled by the consumer preset. Components configures it with the canonical inventory in `lib/kernelBoundary.js`; an explicit `kernelPaths` array may be supplied when testing or applying the rule to another owned tree.
68
+
69
+ ## Repository kernel boundary
70
+
71
+ `no-react-in-kernel` applies only when the linted filename is one of the explicitly declared `kernelPaths`. It reports static imports and re-exports, TypeScript import assignments, dynamic imports, CommonJS `require(...)`, package subpaths, and browser DOM global references. Near-miss package names and files outside the inventory are left alone. The repository decision and exact included/excluded modules are documented in `Documentation/decisions/0003-kernel-boundary.md`.
72
+
73
+ ## Considered and rejected: a hardcoded-owned-label rule
74
+
75
+ During the issue #174 localization pass, a static rule flagging a hardcoded English literal that
76
+ bypasses `CratisComponentsProvider` messages (e.g. a `title`/`aria-label` string sitting next to an
77
+ already-localized prop) was considered and rejected. Every rule above is a syntactic, structural
78
+ pattern an AST visitor resolves unambiguously (an import specifier, a known display-name
79
+ constant, a callback's control-flow shape). Detecting an _owned-label bypass_ instead requires
80
+ understanding which of a component's many string literals are user-facing chrome versus CSS class
81
+ names, `data-*` values, decorative glyphs, or genuinely correct per-instance defaults — a semantic
82
+ judgment call, not a syntactic one. A rule broad enough to catch a real bypass (any string literal
83
+ passed to `title`/`aria-label`/`placeholder`/a `Tooltip`'s `content`) flags the large fraction of
84
+ those call sites that are already correct, and an allowlist narrow enough to avoid that noise stops
85
+ catching new bypasses the moment a new component or prop name is introduced — the allowlist would
86
+ need updating in lockstep with every future component, which defeats the purpose of a ratchet.
87
+ Render-time sentinel-provider specs (see
88
+ `Source/Common/for_CratisComponentsProvider/when_every_owned_label_is_overridden.tsx`) are the
89
+ reliable gate instead: they render each audited surface under a provider whose messages are all
90
+ unmistakable sentinel strings and assert the rendered label is the sentinel, never the English
91
+ default — a real behavioral check that a static rule cannot approximate without the same false-positive
92
+ cost. Revisit a lint rule if a narrower, high-confidence pattern emerges (for example, once every
93
+ owned-label prop follows one consistent naming and JSDoc convention).
94
+
50
95
  ## Rules
51
96
 
97
+ ### `no-root-barrel-import`
98
+
99
+ Components 4 removes every component-family namespace from the package root. The rule keeps a fixed map from each Components 3 namespace to its Components 4 subpath, plus the exact setup allowlist that remains at the root:
100
+
101
+ - `CratisComponentsProvider`, `useCratisComponentsConfig`, `cratisDefaults`, and `mergeCratisComponentsConfig`;
102
+ - `CratisComponentsConfig`, `CratisComponentsProviderProps`, and `CratisComponentsMessages`;
103
+ - `CratisPaginatorMessages`, `CratisDatePickerMessages`, `CratisDropdownMessages`, `CratisDialogMessages`, `CratisStepperMessages`, `CratisNotificationsMessages`, `CratisDataTableMessages`, and `CratisColumnFilterMessages`.
104
+
105
+ ```ts
106
+ // ✅ approved setup symbols stay at the root
107
+ import { CratisComponentsProvider } from '@cratis/components';
108
+
109
+ // ❌ a component namespace imported from the root barrel
110
+ import { Canvas } from '@cratis/components';
111
+
112
+ // ✅ the same namespace, imported from its subpath
113
+ import * as Canvas from '@cratis/components/Canvas';
114
+
115
+ // ❌ whole-package forms are ambiguous and reported without an autofix
116
+ import Components = require('@cratis/components');
117
+ const lazyComponents = await import('@cratis/components');
118
+ ```
119
+
120
+ A single-namespace violation is autofixed to the namespace form shown above, preserving an alias (`Canvas as C`) and `import type` (`import type { Canvas } from '@cratis/components'` becomes `import type * as Canvas from '@cratis/components/Canvas'`, and a per-specifier `type` modifier is honored the same way). The historical `CommandStepper` namespace is mapped to `@cratis/components/CommandDialog`, because that root namespace exposed the complete CommandDialog module rather than only the narrower standalone stepper entry. A mixed import naming both a setup symbol and a namespace is split — the setup symbol stays imported from the root, the namespace moves:
121
+
122
+ ```ts
123
+ // Before
124
+ import { CratisComponentsProvider, Canvas } from '@cratis/components';
125
+
126
+ // After (autofixed)
127
+ import { CratisComponentsProvider } from '@cratis/components';
128
+ import * as Canvas from '@cratis/components/Canvas';
129
+ ```
130
+
131
+ The rule never guesses. Each of these is flagged with guidance but **not** autofixed:
132
+
133
+ - A namespace import of the whole package (`import * as Everything from '@cratis/components'`)
134
+ — which subpath each later member access belongs to cannot be inferred from the import alone.
135
+ - A default import (`import Everything from '@cratis/components'`) — the package has no
136
+ default export.
137
+ - A side-effect-only import (`import '@cratis/components'`) — there is no binding to infer a
138
+ subpath from.
139
+ - A named import of a symbol that is neither an approved setup symbol nor a known namespace
140
+ (for example a member that only exists _inside_ a namespace, not at the root) — the whole
141
+ statement is left unfixed rather than partially migrated.
142
+ - The Components 3.6 `Compatibility` namespace and direct pass-through exports — reported
143
+ as known removals with typed-parts guidance and never autofixed because Components 4 has
144
+ 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.
147
+
148
+ A companion, standalone codemod applies the equivalent rewrite across a whole project in one
149
+ pass, and additionally autofixes a named `export { Canvas } from '@cratis/components'`
150
+ re-export the same way it autofixes the matching import — this rule flags every re-export
151
+ form with guidance but never autofixes any of them; see
152
+ [`@cratis/components.migrator`](https://www.npmjs.com/package/@cratis/components.migrator).
153
+
52
154
  ### `onbeforeexecute-must-return`
53
155
 
54
156
  `onBeforeExecute` (on `CommandDialog`, `StepperCommandDialog`, `CommandScope`, …) is a
55
157
  **transformer**: it receives the current command values and must **return** them (mutated or
56
- not). A callback that returns nothing runs the command with `undefined` — every field is
57
- wiped at submit time. TypeScript catches this at fully-typed call sites; this rule is the
58
- backstop for JavaScript and loosely-typed consumers.
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`.
160
+ In-place mutations may therefore remain, but a replacement object is discarded and the call
161
+ still violates the transformer contract. TypeScript catches this at fully typed call sites;
162
+ this rule is the static backstop for JavaScript and loosely typed consumers, so behavior never
163
+ relies on the fallback.
59
164
 
60
165
  ```tsx
61
166
  // ✅ returns the values
62
167
  <CommandDialog onBeforeExecute={values => { values.id = Guid.create(); return values; }} />
63
168
  <CommandDialog onBeforeExecute={values => values} />
64
169
 
65
- // ❌ side-effect only — command executes with undefined
66
- <CommandDialog onBeforeExecute={values => { values.id = Guid.create(); }} />
67
- // ❌ bare return
170
+ // ❌ replacement is discarded — the runtime falls back to `values` and warns
171
+ <CommandDialog onBeforeExecute={values => { const updated = { ...values, id: Guid.create() }; }} />
172
+ // ❌ bare return — same guarded fallback
68
173
  <CommandDialog onBeforeExecute={values => { return; }} />
69
174
  ```
70
175
 
@@ -83,7 +188,9 @@ service) instead of calling a hook.
83
188
  ```ts
84
189
  // ❌ generated Arc proxy hook inside a view model
85
190
  class AuthorsViewModel {
86
- load() { const [authors] = AllAuthors.use(); } // flagged
191
+ load() {
192
+ const [authors] = AllAuthors.use();
193
+ } // flagged
87
194
  }
88
195
 
89
196
  // ✅ inject the abstraction; call hooks only in the component
@@ -103,7 +210,7 @@ inside a nested non–view-model class are not.
103
210
  `CommandForm`, `CommandDialog` and `CommandStepper` decide which children are fields by
104
211
  inspecting the child's component type. Historically that test was a single string
105
212
  comparison against `displayName` — and `displayName` is React's public, writable
106
- *diagnostic* name, a routine target for build tooling. Storybook's
213
+ _diagnostic_ name, a routine target for build tooling. Storybook's
107
214
  `reactDocgen: 'react-docgen-typescript'` setting rewrites it by default.
108
215
 
109
216
  When it is rewritten, the child stops being recognized as a field: it renders with no
@@ -118,13 +225,20 @@ properties, and that shared shape is what carries the contract across the two pa
118
225
  ```ts
119
226
  // ❌ a build transform that rewrites displayName silently unbinds this field
120
227
  MyField.displayName = 'CommandFormField';
121
- if (component.displayName === 'CommandFormField') { wrap(component); }
228
+ if (component.displayName === 'CommandFormField') {
229
+ wrap(component);
230
+ }
122
231
 
123
232
  // ✅ marker first, legacy displayName still set and still honoured
124
- import { markAsCommandFormField, isCommandFormField } from '@cratis/components/CommandForm';
233
+ import {
234
+ markAsCommandFormField,
235
+ isCommandFormField,
236
+ } from '@cratis/components/CommandForm';
125
237
 
126
238
  markAsCommandFormField(MyField);
127
- if (isCommandFormField(component)) { wrap(component); }
239
+ if (isCommandFormField(component)) {
240
+ wrap(component);
241
+ }
128
242
  ```
129
243
 
130
244
  Flagged in both directions: assignment (`C.displayName = '…'`, including computed and
package/index.js CHANGED
@@ -4,6 +4,7 @@ import { noRootBarrelImport } from './lib/noRootBarrelImport.js';
4
4
  import { onbeforeexecuteMustReturn } from './lib/onbeforeexecuteMustReturn.js';
5
5
  import { noHooksInViewModel } from './lib/noHooksInViewModel.js';
6
6
  import { noRawCommandFormMarker } from './lib/noRawCommandFormMarker.js';
7
+ import { noReactInKernel } from './lib/noReactInKernel.js';
7
8
 
8
9
  const { version } = createRequire(import.meta.url)('./package.json');
9
10
 
@@ -19,6 +20,7 @@ const plugin = {
19
20
  'onbeforeexecute-must-return': onbeforeexecuteMustReturn,
20
21
  'no-hooks-in-view-model': noHooksInViewModel,
21
22
  'no-raw-command-form-marker': noRawCommandFormMarker,
23
+ 'no-react-in-kernel': noReactInKernel,
22
24
  },
23
25
  configs: {},
24
26
  };
@@ -32,7 +34,7 @@ Object.assign(plugin.configs, {
32
34
  recommended: [
33
35
  {
34
36
  name: '@cratis/components/recommended',
35
- files: ['**/*.ts', '**/*.tsx'],
37
+ files: ['**/*.{js,jsx,mjs,cjs,ts,tsx,mts,cts}'],
36
38
  plugins: { '@cratis/components': plugin },
37
39
  rules: {
38
40
  '@cratis/components/no-primereact-dialog': 'error',
@@ -47,4 +49,4 @@ Object.assign(plugin.configs, {
47
49
 
48
50
  export default plugin;
49
51
  export const { configs, rules, meta } = plugin;
50
- export { noPrimereactDialog, noRootBarrelImport, onbeforeexecuteMustReturn, noHooksInViewModel, noRawCommandFormMarker };
52
+ export { noPrimereactDialog, noRootBarrelImport, onbeforeexecuteMustReturn, noHooksInViewModel, noRawCommandFormMarker, noReactInKernel };
@@ -0,0 +1,139 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import path from 'node:path';
5
+
6
+ /**
7
+ * Repository-owned source modules whose contracts are deliberately independent of React and the browser DOM.
8
+ *
9
+ * This is the canonical kernel inventory. The root ESLint config, the no-react-in-kernel rule, and the
10
+ * emitted package-graph gate all consume this exact list.
11
+ */
12
+ export const kernelSourcePaths = Object.freeze([
13
+ 'Source/PivotViewer/constants.ts',
14
+ 'Source/PivotViewer/engine/layout.ts',
15
+ 'Source/PivotViewer/engine/requestCorrelator.ts',
16
+ 'Source/PivotViewer/engine/store.ts',
17
+ 'Source/PivotViewer/engine/types.ts',
18
+ 'Source/Canvas/canvasGesture.ts',
19
+ 'Source/Canvas/canvasTransformActivity.ts',
20
+ 'Source/Canvas/panMomentum.ts',
21
+ 'Source/Canvas/pinchGesture.ts',
22
+ 'Source/Canvas/shapes/Region/regionContainment.ts',
23
+ 'Source/Filter/types.ts',
24
+ 'Source/Filter/utils.ts',
25
+ 'Source/DataTables/DataTableFilterMatcherRegistry.ts',
26
+ 'Source/DataTables/DataTableFilterMeta.ts',
27
+ 'Source/DataTables/paginatorRange.ts',
28
+ 'Source/DataTables/selectionKeys.ts',
29
+ 'Source/CommandDialog/applyBeforeExecute.ts',
30
+ 'Source/CommandForm/commandFormMarkers.ts',
31
+ 'Source/CommandForm/fields/chipValues.ts',
32
+ 'Source/CommandForm/fields/fieldValueFromEvent.ts',
33
+ 'Source/SchemaEditor/schemaHelpers.ts',
34
+ 'Source/Chat/isTopicUnnamed.ts',
35
+ 'Source/Chat/shouldRequestTopicName.ts',
36
+ 'Source/Chat/topicsByActivity.ts',
37
+ 'Source/Chat/Kit/findOwnReaction.ts',
38
+ 'Source/Chat/Kit/reactionsExcludingUser.ts',
39
+ 'Source/Chat/Kit/Mentions/MentionCandidate.ts',
40
+ 'Source/Chat/Kit/Mentions/MentionQuery.ts',
41
+ 'Source/Chat/Kit/Mentions/activeMentionQuery.ts',
42
+ 'Source/Chat/Kit/Mentions/applyMention.ts',
43
+ 'Source/Chat/Kit/Mentions/extractMentions.ts',
44
+ 'Source/Chat/Kit/Mentions/findMentionRanges.ts',
45
+ 'Source/Chat/Kit/Mentions/matchCandidates.ts',
46
+ 'Source/Chat/Kit/Mentions/mentionSegments.ts',
47
+ ]);
48
+
49
+ /** React package roots forbidden from kernel source and emitted closures. */
50
+ const reactKernelPackages = Object.freeze([
51
+ 'react',
52
+ 'react-dom',
53
+ 'react-aria-components',
54
+ ]);
55
+
56
+ /** Browser DOM globals forbidden from kernel source and emitted runtime closures. */
57
+ export const browserDomGlobals = Object.freeze([
58
+ 'CSSStyleDeclaration',
59
+ 'ClipboardEvent',
60
+ 'CustomEvent',
61
+ 'DOMRect',
62
+ 'Document',
63
+ 'DragEvent',
64
+ 'Element',
65
+ 'Event',
66
+ 'EventTarget',
67
+ 'FocusEvent',
68
+ 'HTMLButtonElement',
69
+ 'HTMLCanvasElement',
70
+ 'HTMLDivElement',
71
+ 'HTMLElement',
72
+ 'HTMLFormElement',
73
+ 'HTMLInputElement',
74
+ 'HTMLTextAreaElement',
75
+ 'InputEvent',
76
+ 'IntersectionObserver',
77
+ 'KeyboardEvent',
78
+ 'MessageEvent',
79
+ 'MouseEvent',
80
+ 'MutationObserver',
81
+ 'Node',
82
+ 'PointerEvent',
83
+ 'ResizeObserver',
84
+ 'TouchEvent',
85
+ 'WheelEvent',
86
+ 'Window',
87
+ 'Worker',
88
+ 'cancelAnimationFrame',
89
+ 'document',
90
+ 'getComputedStyle',
91
+ 'history',
92
+ 'localStorage',
93
+ 'location',
94
+ 'matchMedia',
95
+ 'navigator',
96
+ 'performance',
97
+ 'requestAnimationFrame',
98
+ 'sessionStorage',
99
+ 'self',
100
+ 'window',
101
+ ]);
102
+
103
+ /** True when a package specifier is React, React DOM, React Aria Components, or one of their subpaths. */
104
+ export const isKernelReactSpecifier = (specifier) =>
105
+ reactKernelPackages.some(
106
+ (packageName) =>
107
+ specifier === packageName || specifier.startsWith(`${packageName}/`),
108
+ );
109
+
110
+ const normalized = (value) => value.split(path.sep).join('/').replace(/^\.\//u, '');
111
+
112
+ /** True when an ESLint filename identifies one of the declared repository kernel modules. */
113
+ export function isKernelSourcePath(filename, paths = kernelSourcePaths, cwd = process.cwd()) {
114
+ if (!filename || filename === '<input>' || filename === '<text>') return false;
115
+
116
+ const normalizedFilename = normalized(filename);
117
+ const normalizedCwd = normalized(cwd).replace(/\/$/u, '');
118
+ const relative = normalizedFilename.startsWith(`${normalizedCwd}/`)
119
+ ? normalizedFilename.slice(normalizedCwd.length + 1)
120
+ : normalizedFilename;
121
+
122
+ return paths.some((candidate) => {
123
+ const normalizedCandidate = normalized(candidate);
124
+ return (
125
+ relative === normalizedCandidate ||
126
+ normalizedFilename.endsWith(`/${normalizedCandidate}`)
127
+ );
128
+ });
129
+ }
130
+
131
+ /** Maps a declared TypeScript module under `Source/` to its emitted `dist/esm` path. */
132
+ export function kernelEmittedPath(sourcePath, extension) {
133
+ if (extension !== '.js' && extension !== '.d.ts') {
134
+ throw new Error(`Unsupported kernel emitted extension '${extension}'.`);
135
+ }
136
+ return normalized(sourcePath)
137
+ .replace(/^Source\//u, '')
138
+ .replace(/\.(?:tsx?|mts|cts)$/u, extension);
139
+ }
@@ -0,0 +1,116 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import {
5
+ browserDomGlobals,
6
+ isKernelReactSpecifier,
7
+ isKernelSourcePath,
8
+ kernelSourcePaths,
9
+ } from './kernelBoundary.js';
10
+
11
+ /**
12
+ * Keeps the repository's explicitly declared computation kernel independent of React and browser DOM APIs.
13
+ * The filename check is intentional: this is an owned architectural boundary, not a consumer-wide ban.
14
+ */
15
+ export const noReactInKernel = {
16
+ meta: {
17
+ type: 'problem',
18
+ docs: {
19
+ description:
20
+ 'Disallow React package edges and browser DOM globals in explicitly declared Components kernel modules.',
21
+ recommended: false,
22
+ url: 'https://github.com/Cratis/Components/blob/main/Documentation/decisions/0003-kernel-boundary.md',
23
+ },
24
+ schema: [
25
+ {
26
+ type: 'object',
27
+ properties: {
28
+ kernelPaths: {
29
+ type: 'array',
30
+ items: { type: 'string' },
31
+ uniqueItems: true,
32
+ },
33
+ },
34
+ additionalProperties: false,
35
+ },
36
+ ],
37
+ messages: {
38
+ forbiddenDependency:
39
+ "Kernel module '{{file}}' must not depend on '{{specifier}}'. Keep React, React DOM, and React Aria Components outside the repository-owned kernel.",
40
+ forbiddenBrowserGlobal:
41
+ "Kernel module '{{file}}' must not use browser DOM global '{{name}}'. Pass data through a React/DOM adapter outside the kernel instead.",
42
+ },
43
+ },
44
+ create(context) {
45
+ const kernelPaths = context.options[0]?.kernelPaths ?? kernelSourcePaths;
46
+ const filename = context.filename;
47
+ if (!isKernelSourcePath(filename, kernelPaths, context.cwd ?? process.cwd())) {
48
+ return {};
49
+ }
50
+
51
+ const file = kernelPaths.find((candidate) =>
52
+ isKernelSourcePath(filename, [candidate], context.cwd ?? process.cwd()),
53
+ );
54
+ const reportedGlobals = new Set();
55
+
56
+ const reportSpecifier = (node) => {
57
+ if (!node || typeof node.value !== 'string') return;
58
+ if (!isKernelReactSpecifier(node.value)) return;
59
+ context.report({
60
+ node,
61
+ messageId: 'forbiddenDependency',
62
+ data: { file, specifier: node.value },
63
+ });
64
+ };
65
+
66
+ return {
67
+ ImportDeclaration(node) {
68
+ reportSpecifier(node.source);
69
+ },
70
+ ExportNamedDeclaration(node) {
71
+ reportSpecifier(node.source);
72
+ },
73
+ ExportAllDeclaration(node) {
74
+ reportSpecifier(node.source);
75
+ },
76
+ TSImportEqualsDeclaration(node) {
77
+ const reference = node.moduleReference;
78
+ if (reference?.type === 'TSExternalModuleReference') {
79
+ reportSpecifier(reference.expression);
80
+ }
81
+ },
82
+ ImportExpression(node) {
83
+ reportSpecifier(node.source);
84
+ },
85
+ CallExpression(node) {
86
+ if (node.callee.type !== 'Identifier' || node.callee.name !== 'require') {
87
+ return;
88
+ }
89
+ const [argument] = node.arguments;
90
+ if (!argument || argument.type === 'SpreadElement') return;
91
+ reportSpecifier(argument);
92
+ },
93
+ 'Program:exit'() {
94
+ const forbidden = new Set(browserDomGlobals);
95
+ const scopes = context.sourceCode.scopeManager?.scopes ?? [];
96
+ for (const scope of scopes) {
97
+ for (const reference of scope.references) {
98
+ const identifier = reference.identifier;
99
+ if (!forbidden.has(identifier.name)) continue;
100
+ if (reference.resolved?.defs?.length > 0) continue;
101
+ const key = `${identifier.range?.[0] ?? identifier.loc.start.line}:${identifier.name}`;
102
+ if (reportedGlobals.has(key)) continue;
103
+ reportedGlobals.add(key);
104
+ context.report({
105
+ node: identifier,
106
+ messageId: 'forbiddenBrowserGlobal',
107
+ data: { file, name: identifier.name },
108
+ });
109
+ }
110
+ }
111
+ },
112
+ };
113
+ },
114
+ };
115
+
116
+ export default noReactInKernel;
@@ -1,48 +1,279 @@
1
+ import {
2
+ namespaceSubpaths,
3
+ approvedRootSymbols,
4
+ removedRootSymbols,
5
+ } from './rootNamespaceMap.js';
6
+
1
7
  const DEFAULTS = { packageName: '@cratis/components', allow: [] };
2
8
 
3
- // Disallow importing from the Cratis Components root barrel. The package exposes
4
- // purpose-built subpath exports (`@cratis/components/CommandDialog`,
5
- // `@cratis/components/DataPage`, `@cratis/components/Toolbar`, …); importing the root
6
- // pulls the whole optional-peer-heavy surface and hides intent. Covers `import` and
7
- // re-`export … from` forms.
9
+ // Disallow importing a component namespace from the Cratis Components root barrel. The
10
+ // package exposes purpose-built subpath exports (`@cratis/components/CommandDialog`,
11
+ // `@cratis/components/DataPage`, `@cratis/components/Toolbar`, …); Components 4 removes
12
+ // component namespaces from the setup-only root. The small,
13
+ // approved setup surface (`CratisComponentsProvider` and friends) remains importable from
14
+ // the root. Covers static imports/re-exports, TypeScript import assignments, dynamic imports,
15
+ // and CommonJS require calls. Only named static imports are autofixed, and only when every
16
+ // specifier in the statement is unambiguous.
8
17
  export const noRootBarrelImport = {
9
18
  meta: {
10
19
  type: 'suggestion',
20
+ fixable: 'code',
11
21
  docs: {
12
- description: 'Disallow importing from the @cratis/components root barrel; use a subpath export.',
22
+ description:
23
+ 'Disallow importing a component namespace from the @cratis/components root barrel; use a subpath export. Approved setup symbols (e.g. CratisComponentsProvider) remain allowed at the root.',
13
24
  recommended: true,
14
25
  url: 'https://github.com/Cratis/Components/blob/main/ESLint/README.md',
15
26
  },
16
- schema: [{
17
- type: 'object',
18
- properties: {
19
- packageName: { type: 'string' },
20
- allow: { type: 'array', items: { type: 'string' } },
27
+ schema: [
28
+ {
29
+ type: 'object',
30
+ properties: {
31
+ packageName: { type: 'string' },
32
+ allow: { type: 'array', items: { type: 'string' } },
33
+ },
34
+ additionalProperties: false,
21
35
  },
22
- additionalProperties: false,
23
- }],
36
+ ],
24
37
  messages: {
25
- useSubpath: "Import from a '{{packageName}}' subpath (e.g. '{{packageName}}/CommandDialog'), not the package root barrel.",
38
+ useNamespaceSubpath:
39
+ "Import '{{name}}' from '{{packageName}}/{{subpath}}' as a namespace instead of the package root barrel: import * as {{name}} from '{{packageName}}/{{subpath}}'.",
40
+ useNamespaceSubpathExport:
41
+ "Re-export '{{name}}' from '{{packageName}}/{{subpath}}' instead of the package root barrel: export * as {{name}} from '{{packageName}}/{{subpath}}'. This is not autofixed — verify downstream default-export expectations.",
42
+ removedCompatibilitySymbol:
43
+ "'{{name}}' was a Components 3 PrimeReact compatibility export and has no Components 4 subpath. Migrate renderer slots to typed Cratis parts and remove this import manually.",
44
+ removedCompatibilitySymbolExport:
45
+ "'{{name}}' was a Components 3 PrimeReact compatibility export and has no Components 4 subpath. Migrate renderer slots to typed Cratis parts and remove this re-export manually.",
46
+ unknownSymbol:
47
+ "'{{name}}' is not a recognized '{{packageName}}' root export (neither an approved setup symbol nor a known namespace). Import it from its specific subpath instead of the package root.",
48
+ unknownSymbolExport:
49
+ "'{{name}}' is not a recognized '{{packageName}}' root export (neither an approved setup symbol nor a known namespace). Re-export it from its specific subpath instead of the package root.",
50
+ ambiguousImport:
51
+ "This import of '{{packageName}}' cannot be safely auto-migrated off the root barrel — move each symbol to its subpath import manually. See ESLint/README.md.",
52
+ ambiguousExport:
53
+ "This wildcard re-export of '{{packageName}}' cannot be safely auto-migrated off the root barrel — replace it with explicit subpath re-exports.",
26
54
  },
27
55
  },
28
56
  create(context) {
29
57
  const options = { ...DEFAULTS, ...(context.options[0] ?? {}) };
58
+ const packageName = options.packageName;
30
59
  const allow = new Set(options.allow);
31
60
 
32
- const check = node => {
33
- const source = node.source;
34
- if (!source || typeof source.value !== 'string') return;
35
- if (source.value === options.packageName && !allow.has(source.value)) {
36
- context.report({ node: source, messageId: 'useSubpath', data: { packageName: options.packageName } });
37
- }
38
- };
61
+ const isRootSpecifier = (source) =>
62
+ !!source &&
63
+ typeof source.value === 'string' &&
64
+ source.value === packageName &&
65
+ !allow.has(source.value);
39
66
 
40
67
  return {
41
- ImportDeclaration: check,
42
- ExportNamedDeclaration: check,
43
- ExportAllDeclaration: check,
68
+ ImportDeclaration(node) {
69
+ if (!isRootSpecifier(node.source)) return;
70
+ checkImport(context, node, packageName);
71
+ },
72
+ TSImportEqualsDeclaration(node) {
73
+ const reference = node.moduleReference;
74
+ if (
75
+ reference?.type !== 'TSExternalModuleReference' ||
76
+ !isRootSpecifier(reference.expression)
77
+ )
78
+ return;
79
+ context.report({
80
+ node: reference.expression,
81
+ messageId: 'ambiguousImport',
82
+ data: { packageName },
83
+ });
84
+ },
85
+ ImportExpression(node) {
86
+ if (!isRootSpecifier(node.source)) return;
87
+ context.report({
88
+ node: node.source,
89
+ messageId: 'ambiguousImport',
90
+ data: { packageName },
91
+ });
92
+ },
93
+ CallExpression(node) {
94
+ if (node.callee.type !== 'Identifier' || node.callee.name !== 'require')
95
+ return;
96
+ const [argument] = node.arguments;
97
+ if (!argument || argument.type === 'SpreadElement') return;
98
+ if (!isRootSpecifier(argument)) return;
99
+ context.report({
100
+ node: argument,
101
+ messageId: 'ambiguousImport',
102
+ data: { packageName },
103
+ });
104
+ },
105
+ ExportNamedDeclaration(node) {
106
+ if (!isRootSpecifier(node.source)) return;
107
+ checkNamedExport(context, node, packageName);
108
+ },
109
+ ExportAllDeclaration(node) {
110
+ if (!isRootSpecifier(node.source)) return;
111
+ context.report({
112
+ node: node.source,
113
+ messageId: 'ambiguousExport',
114
+ data: { packageName },
115
+ });
116
+ },
44
117
  };
45
118
  },
46
119
  };
47
120
 
121
+ function checkImport(context, node, packageName) {
122
+ // No specifiers at all: `import '@cratis/components';` — nothing to infer a subpath
123
+ // from.
124
+ if (node.specifiers.length === 0) {
125
+ context.report({
126
+ node: node.source,
127
+ messageId: 'ambiguousImport',
128
+ data: { packageName },
129
+ });
130
+ return;
131
+ }
132
+
133
+ // `import Default from '...'` and/or `import * as Namespace from '...'`.
134
+ const hasDefault = node.specifiers.some((s) => s.type === 'ImportDefaultSpecifier');
135
+ const hasNamespace = node.specifiers.some(
136
+ (s) => s.type === 'ImportNamespaceSpecifier',
137
+ );
138
+ if (hasDefault || hasNamespace) {
139
+ context.report({
140
+ node: node.source,
141
+ messageId: 'ambiguousImport',
142
+ data: { packageName },
143
+ });
144
+ return;
145
+ }
146
+
147
+ const named = node.specifiers.filter((s) => s.type === 'ImportSpecifier');
148
+ const kept = [];
149
+ const namespaced = [];
150
+ let hasUnknown = false;
151
+
152
+ for (const specifier of named) {
153
+ const importedName = specifier.imported.name ?? specifier.imported.value;
154
+
155
+ if (approvedRootSymbols.has(importedName)) {
156
+ kept.push(specifier);
157
+ continue;
158
+ }
159
+ if (Object.hasOwn(namespaceSubpaths, importedName)) {
160
+ namespaced.push({ specifier, subpath: namespaceSubpaths[importedName] });
161
+ continue;
162
+ }
163
+ if (removedRootSymbols.has(importedName)) {
164
+ hasUnknown = true;
165
+ context.report({
166
+ node: specifier,
167
+ messageId: 'removedCompatibilitySymbol',
168
+ data: { name: importedName },
169
+ });
170
+ continue;
171
+ }
172
+
173
+ hasUnknown = true;
174
+ context.report({
175
+ node: specifier,
176
+ messageId: 'unknownSymbol',
177
+ data: { name: importedName, packageName },
178
+ });
179
+ }
180
+
181
+ if (namespaced.length === 0) return; // nothing to move (all approved, or all unknown-and-already-reported).
182
+
183
+ // Never guess: an unrecognized specifier means no autofix for the whole statement,
184
+ // even for the specifiers this rule does recognize.
185
+ const fix = hasUnknown
186
+ ? undefined
187
+ : (fixer) => [
188
+ fixer.replaceText(
189
+ node,
190
+ buildReplacementText({
191
+ packageName,
192
+ declTypeOnly: node.importKind === 'type',
193
+ kept,
194
+ namespaced,
195
+ }),
196
+ ),
197
+ ];
198
+
199
+ for (const { specifier, subpath } of namespaced) {
200
+ const importedName = specifier.imported.name ?? specifier.imported.value;
201
+ context.report({
202
+ node: specifier,
203
+ messageId: 'useNamespaceSubpath',
204
+ data: { name: importedName, packageName, subpath },
205
+ fix,
206
+ });
207
+ }
208
+ }
209
+
210
+ function checkNamedExport(context, node, packageName) {
211
+ if (!node.specifiers || node.specifiers.length === 0) {
212
+ // `export {} from '@cratis/components';` — nothing to report.
213
+ return;
214
+ }
215
+
216
+ for (const specifier of node.specifiers) {
217
+ const localNode = specifier.local ?? specifier;
218
+ const exportedName = localNode.name ?? localNode.value;
219
+
220
+ if (approvedRootSymbols.has(exportedName)) continue;
221
+ if (Object.hasOwn(namespaceSubpaths, exportedName)) {
222
+ context.report({
223
+ node: specifier,
224
+ messageId: 'useNamespaceSubpathExport',
225
+ data: {
226
+ name: exportedName,
227
+ packageName,
228
+ subpath: namespaceSubpaths[exportedName],
229
+ },
230
+ });
231
+ continue;
232
+ }
233
+ if (removedRootSymbols.has(exportedName)) {
234
+ context.report({
235
+ node: specifier,
236
+ messageId: 'removedCompatibilitySymbolExport',
237
+ data: { name: exportedName },
238
+ });
239
+ continue;
240
+ }
241
+
242
+ context.report({
243
+ node: specifier,
244
+ messageId: 'unknownSymbolExport',
245
+ data: { name: exportedName, packageName },
246
+ });
247
+ }
248
+ }
249
+
250
+ function specifierText(specifier, prefix = '') {
251
+ const importedName = specifier.imported.name ?? specifier.imported.value;
252
+ const localName = specifier.local.name;
253
+ const typeOnly = specifier.importKind === 'type';
254
+ const alias = localName === importedName ? '' : ` as ${localName}`;
255
+ return `${prefix}${typeOnly ? 'type ' : ''}${importedName}${alias}`;
256
+ }
257
+
258
+ function buildReplacementText({ packageName, declTypeOnly, kept, namespaced }) {
259
+ const lines = [];
260
+
261
+ if (kept.length > 0) {
262
+ const specifiers = kept.map((specifier) => specifierText(specifier)).join(', ');
263
+ lines.push(
264
+ `import ${declTypeOnly ? 'type ' : ''}{ ${specifiers} } from '${packageName}';`,
265
+ );
266
+ }
267
+
268
+ for (const { specifier, subpath } of namespaced) {
269
+ const localName = specifier.local.name;
270
+ const typeOnly = declTypeOnly || specifier.importKind === 'type';
271
+ lines.push(
272
+ `import ${typeOnly ? 'type ' : ''}* as ${localName} from '${packageName}/${subpath}';`,
273
+ );
274
+ }
275
+
276
+ return lines.join('\n');
277
+ }
278
+
48
279
  export default noRootBarrelImport;
@@ -3,7 +3,11 @@
3
3
  // a return's own argument (which may itself contain a nested arrow).
4
4
  const collectOwnReturns = (node, returns) => {
5
5
  if (!node || typeof node.type !== 'string') return;
6
- if (node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') {
6
+ if (
7
+ node.type === 'FunctionDeclaration' ||
8
+ node.type === 'FunctionExpression' ||
9
+ node.type === 'ArrowFunctionExpression'
10
+ ) {
7
11
  return;
8
12
  }
9
13
  if (node.type === 'ReturnStatement') {
@@ -35,22 +39,28 @@ const isOnBeforeExecuteCallback = (node) => {
35
39
  const parent = node.parent;
36
40
  if (!parent) return false;
37
41
 
38
- if (parent.type === 'JSXExpressionContainer' &&
42
+ if (
43
+ parent.type === 'JSXExpressionContainer' &&
39
44
  parent.parent?.type === 'JSXAttribute' &&
40
- parent.parent.name?.name === 'onBeforeExecute') {
45
+ parent.parent.name?.name === 'onBeforeExecute'
46
+ ) {
41
47
  return true;
42
48
  }
43
49
 
44
- if ((parent.type === 'Property' || parent.type === 'PropertyDefinition') &&
50
+ if (
51
+ (parent.type === 'Property' || parent.type === 'PropertyDefinition') &&
45
52
  parent.value === node &&
46
- keyNameOf(parent.key) === 'onBeforeExecute') {
53
+ keyNameOf(parent.key) === 'onBeforeExecute'
54
+ ) {
47
55
  return true;
48
56
  }
49
57
 
50
- if (parent.type === 'VariableDeclarator' &&
58
+ if (
59
+ parent.type === 'VariableDeclarator' &&
51
60
  parent.init === node &&
52
61
  parent.id?.type === 'Identifier' &&
53
- parent.id.name === 'onBeforeExecute') {
62
+ parent.id.name === 'onBeforeExecute'
63
+ ) {
54
64
  return true;
55
65
  }
56
66
 
@@ -58,30 +68,40 @@ const isOnBeforeExecuteCallback = (node) => {
58
68
  };
59
69
 
60
70
  // Require an `onBeforeExecute` callback to return the command values on every path. It is a
61
- // transformer, not a side-effect hook: a body that can complete without returning executes
62
- // the command with `undefined` (silent data loss). A lint backstop for consumers whose call
63
- // sites are not fully typed — it flags a block-bodied callback that returns nothing, or a
64
- // bare `return;`. It does not do full path analysis, so a callback that returns a value on
65
- // some branches but can still fall through is not flagged.
71
+ // transformer, not a side-effect hook. A runtime guard (`applyBeforeExecute`) prevents a
72
+ // missing return from executing with `undefined`: it falls back to the current object and logs
73
+ // a warning. In-place mutations may remain because the callback received that same object, but
74
+ // a replacement object is discarded and the callback still violates its contract. This lint
75
+ // backstop keeps JavaScript and loosely typed call sites from relying on that fallback. It flags
76
+ // a block-bodied callback that returns nothing, or a bare `return;`. It does not do full path
77
+ // analysis, so a callback that returns a value on some branches but can still fall through is
78
+ // not flagged.
66
79
  export const onbeforeexecuteMustReturn = {
67
80
  meta: {
68
81
  type: 'problem',
69
82
  docs: {
70
- description: 'Require an onBeforeExecute callback to return the command values; returning nothing executes the command with undefined.',
83
+ description:
84
+ 'Require an onBeforeExecute callback to return the command values; on a missing return the runtime falls back to the current object and warns, so replacement values are discarded.',
71
85
  recommended: true,
72
86
  url: 'https://github.com/Cratis/Components/blob/main/ESLint/README.md',
73
87
  },
74
88
  schema: [],
75
89
  messages: {
76
- missingReturn: 'onBeforeExecute is a transformer and must return the command values. This callback can complete without returning, which executes the command with undefined values. Return the values (mutated or not).',
77
- emptyReturn: 'onBeforeExecute must return the command values, not `return;`. Return the values (mutated or not).',
90
+ missingReturn:
91
+ 'onBeforeExecute is a transformer and must return the command values. This callback can complete without returning, so the runtime falls back to the current object and logs a warning; any replacement value is discarded. Return the values (mutated or not).',
92
+ emptyReturn:
93
+ 'onBeforeExecute must return the command values, not `return;`. The runtime falls back to the current object and logs a warning; any replacement value is discarded. Return the values (mutated or not).',
78
94
  },
79
95
  },
80
96
  create(context) {
81
97
  const check = (node) => {
82
98
  if (!isOnBeforeExecuteCallback(node)) return;
83
99
  // An expression-bodied arrow (`values => values`) always returns a value.
84
- if (node.type === 'ArrowFunctionExpression' && node.body.type !== 'BlockStatement') return;
100
+ if (
101
+ node.type === 'ArrowFunctionExpression' &&
102
+ node.body.type !== 'BlockStatement'
103
+ )
104
+ return;
85
105
 
86
106
  const returns = [];
87
107
  collectOwnReturns(node.body, returns);
@@ -0,0 +1,79 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ /**
5
+ * Single source of truth for `no-root-barrel-import`'s namespace-aware behavior: every
6
+ * component namespace removed from the Components 3 root, and the small setup surface
7
+ * that remains supported at the Components 4 root.
8
+ *
9
+ * Mirrors `Source/index.ts` and the `exports` map in `Source/package.json`. When a
10
+ * namespace subpath is added, renamed, or removed there, update `namespaceSubpaths` here
11
+ * (and the matching copy in `Migrator/lib/namespaceMap.js`) in the same change. Neither
12
+ * file imports the other: this plugin and the codemods package ship and publish
13
+ * independently, so the map is intentionally duplicated rather than shared across a
14
+ * workspace boundary.
15
+ */
16
+
17
+ /**
18
+ * Every retained namespace removed from the Components 3 root, mapped to the Components 4
19
+ * subpath it belongs to: importing `Canvas` from the root should instead be
20
+ * `import * as Canvas from '@cratis/components/Canvas'`. `Types` is the one case where the
21
+ * namespace name and its subpath differ in casing (`./types`, lowercase).
22
+ */
23
+ export const namespaceSubpaths = {
24
+ Canvas: 'Canvas',
25
+ Chat: 'Chat',
26
+ CommandDialog: 'CommandDialog',
27
+ // The former root CommandStepper namespace aliased the entire CommandDialog module.
28
+ // Preserve that module identity; the narrower ./CommandStepper subpath does not.
29
+ CommandStepper: 'CommandDialog',
30
+ CommandForm: 'CommandForm',
31
+ Common: 'Common',
32
+ DataPage: 'DataPage',
33
+ DataTables: 'DataTables',
34
+ Dialogs: 'Dialogs',
35
+ Display: 'Display',
36
+ Dropdown: 'Dropdown',
37
+ Filter: 'Filter',
38
+ Notifications: 'Notifications',
39
+ ObjectContentEditor: 'ObjectContentEditor',
40
+ ObjectNavigationalBar: 'ObjectNavigationalBar',
41
+ PivotViewer: 'PivotViewer',
42
+ SchemaEditor: 'SchemaEditor',
43
+ TimeMachine: 'TimeMachine',
44
+ Toolbar: 'Toolbar',
45
+ Types: 'types',
46
+ };
47
+
48
+ /** Components 3 renderer-compatibility exports removed without a Components 4 subpath. */
49
+ export const removedRootSymbols = new Set([
50
+ 'Compatibility',
51
+ 'assertPrimeReact11PassThroughCompatibility',
52
+ 'components3PrimeReact11PassThroughContract',
53
+ 'primeReact11PassThroughSentinelAttribute',
54
+ 'primeReact11PassThroughSentinelPreset',
55
+ 'PrimeReact11PassThroughComponent',
56
+ ]);
57
+
58
+ /**
59
+ * The primary setup surface `Source/index.ts` re-exports directly from the root so a
60
+ * consumer that only wants provider setup does not need to know about the `Common`
61
+ * subpath. Importing one of these from the package root is allowed.
62
+ */
63
+ export const approvedRootSymbols = new Set([
64
+ 'CratisComponentsProvider',
65
+ 'useCratisComponentsConfig',
66
+ 'cratisDefaults',
67
+ 'mergeCratisComponentsConfig',
68
+ 'CratisComponentsConfig',
69
+ 'CratisComponentsProviderProps',
70
+ 'CratisComponentsMessages',
71
+ 'CratisPaginatorMessages',
72
+ 'CratisDatePickerMessages',
73
+ 'CratisDropdownMessages',
74
+ 'CratisDialogMessages',
75
+ 'CratisStepperMessages',
76
+ 'CratisNotificationsMessages',
77
+ 'CratisDataTableMessages',
78
+ 'CratisColumnFilterMessages',
79
+ ]);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@cratis/eslint-plugin-components",
3
- "version": "3.6.0",
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, and identify CommandForm fields by a tamper-resistant marker rather than a raw displayName string. Compose on top of @cratis/eslint-config.",
3
+ "version": "4.0.0",
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",
7
7
  "type": "module",
@@ -22,7 +22,8 @@
22
22
  "files": [
23
23
  "index.js",
24
24
  "lib",
25
- "README.md"
25
+ "README.md",
26
+ "LICENSE"
26
27
  ],
27
28
  "exports": {
28
29
  "./package.json": "./package.json",
@@ -30,9 +31,10 @@
30
31
  },
31
32
  "scripts": {
32
33
  "test": "yarn g:test",
34
+ "verify-package": "node ./test/verify-packed-package.mjs",
33
35
  "ci": "yarn g:test"
34
36
  },
35
37
  "peerDependencies": {
36
38
  "eslint": ">=9"
37
39
  }
38
- }
40
+ }