@zedtech/zedkit-quality 0.3.2 → 0.4.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/README.md CHANGED
@@ -14,7 +14,8 @@ pnpm add -D @zedtech/zedkit-quality
14
14
  ```
15
15
 
16
16
  Peer dependencies (`eslint`, `eslint-plugin-jsx-a11y`,
17
- `eslint-plugin-better-tailwindcss`) install alongside it.
17
+ `eslint-plugin-better-tailwindcss`, `typescript-eslint`) install
18
+ alongside it.
18
19
 
19
20
  ## Usage
20
21
 
@@ -53,8 +54,10 @@ subagents, commands) see `@zedtech/zedkit-ai` instead.
53
54
 
54
55
  - `uiPreset({ entryPoint })` — flat-config blocks for `.tsx`/`.jsx`:
55
56
  jsx-a11y recommended, Tailwind hygiene, tokens-only classes, no `style`
56
- attributes, no raw `try`/`catch`. `entryPoint` (your Tailwind CSS
57
- entry) is required: it is the one thing that varies per repo.
57
+ attributes, no `forwardRef` (ref is a prop; see `packages/ui/src/button.tsx`),
58
+ no explicit `any`, files ≤ 300 lines and functions ≤ 60, no raw `try`/`catch`.
59
+ `entryPoint` (your Tailwind CSS entry) is required: it is the one thing
60
+ that varies per repo.
58
61
  - `defineLayers({ kitPackage, viewDirs, dataDirs, actionPatterns, extraPatterns })` —
59
62
  `no-restricted-imports` blocks for routes -> views -> data: the UI kit
60
63
  is only imported from views, views never import data clients or
@@ -62,6 +65,11 @@ subagents, commands) see `@zedtech/zedkit-ai` instead.
62
65
  `{ kitScope?, viewData?, dataUi? }`) merges your own bans into each
63
66
  block — use it instead of a second `no-restricted-imports` block,
64
67
  which would replace these per flat-config key semantics.
68
+ - `defineLayers({ preset: "feature-folders", kitPackage, featureRoot })` —
69
+ same helper, feature-folder mapping: UI imports ports (not adapters),
70
+ adapters are injected at the composition root, model is pure. Bans
71
+ apply only under `featureRoot`. Live example:
72
+ `apps/docs/features/note-list` (running at `/examples/note-list`).
65
73
  - `@zedtech/zedkit-quality/tsconfig` — the strict base (`strict`,
66
74
  `exactOptionalPropertyTypes`, `noUncheckedIndexedAccess`,
67
75
  `noUnusedLocals`, and more). Extend it, do not copy it.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zedtech/zedkit-quality",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "registry": "https://registry.npmjs.org/"
@@ -60,14 +60,16 @@
60
60
  "peerDependencies": {
61
61
  "eslint": "9.39.5",
62
62
  "eslint-plugin-better-tailwindcss": "4.10.0",
63
- "eslint-plugin-jsx-a11y": "6.10.2"
63
+ "eslint-plugin-jsx-a11y": "6.10.2",
64
+ "typescript-eslint": "8.72.0"
64
65
  },
65
66
  "devDependencies": {
66
67
  "eslint": "9.39.5",
67
68
  "eslint-plugin-better-tailwindcss": "4.10.0",
68
69
  "eslint-plugin-jsx-a11y": "6.10.2",
69
70
  "typescript": "5.9.3",
70
- "vitest": "2.1.9"
71
+ "typescript-eslint": "8.72.0",
72
+ "vitest": "5.0.5"
71
73
  },
72
74
  "scripts": {
73
75
  "test": "vitest run",
package/src/eslint.d.mts CHANGED
@@ -15,25 +15,40 @@ export interface RestrictedImportPattern {
15
15
  readonly allowTypeImports?: boolean;
16
16
  }
17
17
 
18
- export interface DefineLayersOptions {
19
- /** UI kit specifier (e.g. `@acme/ui`). */
20
- readonly kitPackage: string;
21
- /** Minimatch globs for view folders. */
22
- readonly viewDirs: readonly string[];
23
- /** Minimatch globs for data-access folders. */
24
- readonly dataDirs: readonly string[];
25
- /** Import specifiers for data clients and server actions. May be empty. */
26
- readonly actionPatterns: readonly string[];
27
- /**
28
- * Consumer bans merged into each block's own patterns (flat-config
29
- * rules replace per key, so a consumer block setting
30
- * `no-restricted-imports` would otherwise drop these).
31
- */
32
- readonly extraPatterns?: {
33
- readonly kitScope?: readonly RestrictedImportPattern[];
34
- readonly viewData?: readonly RestrictedImportPattern[];
35
- readonly dataUi?: readonly RestrictedImportPattern[];
36
- };
37
- }
18
+ export type DefineLayersOptions =
19
+ | {
20
+ readonly preset?: undefined;
21
+ /** UI kit specifier (e.g. `@acme/ui`). */
22
+ readonly kitPackage: string;
23
+ /** Minimatch globs for view folders. */
24
+ readonly viewDirs: readonly string[];
25
+ /** Minimatch globs for data-access folders. */
26
+ readonly dataDirs: readonly string[];
27
+ /** Import specifiers for data clients and server actions. May be empty. */
28
+ readonly actionPatterns: readonly string[];
29
+ /**
30
+ * Consumer bans merged into each block's own patterns (flat-config
31
+ * rules replace per key, so a consumer block setting
32
+ * `no-restricted-imports` would otherwise drop these).
33
+ */
34
+ readonly extraPatterns?: {
35
+ readonly kitScope?: readonly RestrictedImportPattern[];
36
+ readonly viewData?: readonly RestrictedImportPattern[];
37
+ readonly dataUi?: readonly RestrictedImportPattern[];
38
+ };
39
+ }
40
+ | {
41
+ readonly preset: "feature-folders";
42
+ /** UI kit specifier (e.g. `@acme/ui`). */
43
+ readonly kitPackage: string;
44
+ /** Feature-folder root (e.g. `features`). Bans apply only under this tree. */
45
+ readonly featureRoot: string;
46
+ readonly extraPatterns?: {
47
+ readonly kitScope?: readonly RestrictedImportPattern[];
48
+ readonly viewData?: readonly RestrictedImportPattern[];
49
+ readonly dataUi?: readonly RestrictedImportPattern[];
50
+ readonly model?: readonly RestrictedImportPattern[];
51
+ };
52
+ };
38
53
 
39
54
  export function defineLayers(options: DefineLayersOptions): Linter.Config[];
package/src/eslint.mjs CHANGED
@@ -1,17 +1,20 @@
1
- // Shareable UI lint preset: enforces docs/frontend-charter.md by tools.
1
+ // Shareable UI lint preset: charter rule ids live in docs/frontend-charter.md;
2
+ // the messages below carry the explanation.
2
3
  //
3
4
  // What it enforces on every .tsx/.jsx file:
4
5
  // - jsx-a11y recommended (labels, keyboard, focus, ARIA)
5
6
  // - Tailwind class hygiene (order, no duplicates, no conflicts, no unknown)
6
7
  // - Tokens only: no raw palette colors (bg-red-500), no arbitrary hex/rgb
7
8
  // colors, no raw px font sizes — use the semantic theme tokens
8
- // - No `style` attributes; no raw try/catch (use a tryCatch wrapper)
9
+ // - No `style` attributes; no forwardRef (ref is a prop); no explicit `any`;
10
+ // files ≤ 300 lines, functions ≤ 60; no raw try/catch (use a tryCatch wrapper)
9
11
  //
10
12
  // Consumer setup (no copy: install the package, pass your Tailwind entry):
11
13
  // `import { uiPreset } from "@zedtech/zedkit-quality"` then
12
14
  // `export default [...uiPreset({ entryPoint: "./app/globals.css" })]`.
13
15
  import jsxA11y from "eslint-plugin-jsx-a11y";
14
16
  import betterTailwindcss from "eslint-plugin-better-tailwindcss";
17
+ import { plugin as tsPlugin } from "typescript-eslint";
15
18
 
16
19
  const PALETTE =
17
20
  "slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|fuchsia|pink|rose";
@@ -57,36 +60,67 @@ export function uiPreset({ entryPoint }) {
57
60
  {
58
61
  pattern: `(^|:)(${COLOR_UTILS})-(${PALETTE}|white|black)(-|/|$)`,
59
62
  message:
60
- "Raw palette colors are banned. Use a semantic theme token (see docs/frontend-charter.md).",
63
+ "tokens-color: raw palette classes (bg-red-500) are banned. Use a semantic token (background, foreground, primary, destructive, …) so light and dark stay in sync.",
61
64
  },
62
65
  {
63
66
  pattern: "\\[(#|rgb|hsl)",
64
67
  message:
65
- "Arbitrary color values are banned. Use a semantic theme token (see docs/frontend-charter.md).",
68
+ "tokens-color: arbitrary hex/rgb/hsl colors are banned. Use a semantic token (background, foreground, primary, …).",
66
69
  },
67
70
  {
68
71
  pattern: "text-\\[[^\\]]*px",
69
- message: "Raw px font sizes are banned. Use the type scale (see docs/frontend-charter.md).",
72
+ message:
73
+ "tokens-style: raw px font sizes are banned. Use the type scale (text-xs … text-2xl).",
70
74
  },
71
75
  ],
72
76
  },
73
77
  ],
74
78
  // NOTE: flat-config rules replace (not merge) by key, so this block
75
79
  // restates the root TryStatement ban alongside the style-attribute
76
- // ban; without the restatement, .tsx files would lose the ban.
80
+ // and forwardRef bans; without the restatement, .tsx files would lose them.
77
81
  "no-restricted-syntax": [
78
82
  "error",
79
83
  {
80
84
  selector: "TryStatement",
81
- message: "Raw try/catch is banned. Use tryCatch/tryCatchAsync from @zedtech/zedkit-core.",
85
+ message:
86
+ "try-catch: raw try/catch is banned. Use tryCatch/tryCatchAsync from @zedtech/zedkit-core so thrown values become Error with cause.",
82
87
  },
83
88
  {
84
89
  selector: 'JSXAttribute[name.name="style"]',
85
- message: "Inline style attributes are banned. Use theme tokens (see docs/frontend-charter.md).",
90
+ message:
91
+ "tokens-style: inline style attributes are banned. Use theme tokens and utility classes.",
92
+ },
93
+ {
94
+ selector: "Identifier[name='forwardRef']",
95
+ message:
96
+ "ui-ref: forwardRef is banned. Pass ref as a prop on ComponentProps<\"button\"> (see packages/ui/src/button.tsx).",
86
97
  },
87
98
  ],
88
99
  },
89
100
  },
101
+ {
102
+ name: "zedkit/ui-types",
103
+ files: ["**/*.tsx", "**/*.jsx"],
104
+ plugins: { "@typescript-eslint": tsPlugin },
105
+ rules: {
106
+ "@typescript-eslint/no-explicit-any": "error",
107
+ },
108
+ },
109
+ {
110
+ name: "zedkit/ui-size",
111
+ files: ["**/*.tsx", "**/*.jsx"],
112
+ ignores: ["**/*.test.*", "**/*.spec.*", "**/tests/**", "**/__tests__/**"],
113
+ rules: {
114
+ "max-lines": [
115
+ "error",
116
+ { max: 300, skipBlankLines: true, skipComments: true },
117
+ ],
118
+ "max-lines-per-function": [
119
+ "error",
120
+ { max: 60, skipBlankLines: true, skipComments: true, IIFEs: true },
121
+ ],
122
+ },
123
+ },
90
124
  ];
91
125
  }
92
126
 
@@ -109,6 +143,18 @@ function asFiles(patterns) {
109
143
  });
110
144
  }
111
145
 
146
+ function assertExtraPatterns(extraPatterns, keys) {
147
+ const want = keys.join(", ").replace(/, ([^,]+)$/, " or $1");
148
+ for (const [key, value] of Object.entries(extraPatterns)) {
149
+ if (!keys.includes(key)) {
150
+ throw new Error(`defineLayers: unknown extraPatterns key "${key}" (want ${want})`);
151
+ }
152
+ if (!Array.isArray(value)) {
153
+ throw new Error(`defineLayers: extraPatterns.${key} must be a list of no-restricted-imports pattern objects`);
154
+ }
155
+ }
156
+ }
157
+
112
158
  /**
113
159
  * Route/view/data layer boundaries as `no-restricted-imports` blocks.
114
160
  * Dependencies run one way (routes -> views -> data):
@@ -116,30 +162,29 @@ function asFiles(patterns) {
116
162
  * - views receive data as props and never import data clients or actions;
117
163
  * - data files import no UI (no React runtime, no UI kit).
118
164
  *
119
- * @param {{ kitPackage: string, viewDirs: string[], dataDirs: string[], actionPatterns: string[], extraPatterns?: { kitScope?: object[], viewData?: object[], dataUi?: object[] } }} options
120
- * - `kitPackage`: UI kit specifier (e.g. `@acme/ui`).
121
- * - `viewDirs`: view folders (e.g. `app/ui/**`) or file globs.
122
- * - `dataDirs`: data-access folders or file globs, same shapes.
123
- * - `actionPatterns`: import specifiers for data clients and server actions.
124
- * May be empty (views legitimately import data): the view ban is
125
- * then omitted, and the view block is dropped when it would carry
126
- * no bans at all.
127
- * - `extraPatterns`: consumer bans merged into each block's own
128
- * patterns (flat-config rules replace per key, so a consumer block
129
- * setting `no-restricted-imports` would otherwise drop these).
130
- * Keys: `kitScope`, `viewData`, `dataUi`; each a list of standard
131
- * `no-restricted-imports` pattern objects, passed through untouched.
165
+ * @param {object} options
166
+ * - Classic: `{ kitPackage, viewDirs, dataDirs, actionPatterns, extraPatterns? }`.
167
+ * `actionPatterns` may be empty (views legitimately import data): the
168
+ * view ban is then omitted, and the view block is dropped when it
169
+ * would carry no bans at all. `extraPatterns` merges consumer bans
170
+ * into each block (keys: `kitScope`, `viewData`, `dataUi`).
171
+ * - Preset `feature-folders`: `{ preset: "feature-folders", kitPackage,
172
+ * featureRoot, extraPatterns? }`. UI may import ports, not adapters;
173
+ * adapters are injected at the feature composition root; model is
174
+ * pure. Bans apply only under `featureRoot` (extraPatterns also
175
+ * accepts `model`).
132
176
  * @returns {import("eslint").Linter.Config[]} Flat config blocks.
133
177
  */
134
- export function defineLayers({ kitPackage, viewDirs, dataDirs, actionPatterns, extraPatterns = {} }) {
135
- for (const [key, value] of Object.entries(extraPatterns)) {
136
- if (key !== "kitScope" && key !== "viewData" && key !== "dataUi") {
137
- throw new Error(`defineLayers: unknown extraPatterns key "${key}" (want kitScope, viewData or dataUi)`);
138
- }
139
- if (!Array.isArray(value)) {
140
- throw new Error(`defineLayers: extraPatterns.${key} must be a list of no-restricted-imports pattern objects`);
141
- }
178
+ export function defineLayers(options) {
179
+ const { preset, extraPatterns = {} } = options;
180
+ if (preset === "feature-folders") {
181
+ return featureFolderLayers(options);
142
182
  }
183
+ if (preset !== undefined) {
184
+ throw new Error(`defineLayers: unknown preset "${preset}" (want feature-folders)`);
185
+ }
186
+ const { kitPackage, viewDirs, dataDirs, actionPatterns } = options;
187
+ assertExtraPatterns(extraPatterns, ["kitScope", "viewData", "dataUi"]);
143
188
  if (!Array.isArray(actionPatterns)) {
144
189
  throw new Error("defineLayers: actionPatterns must be a list of import specifiers (may be empty)");
145
190
  }
@@ -216,3 +261,128 @@ export function defineLayers({ kitPackage, viewDirs, dataDirs, actionPatterns, e
216
261
  },
217
262
  ];
218
263
  }
264
+
265
+ const ADAPTER_GROUP = ["**/adapters", "**/adapters/**"];
266
+ const REACT_GROUP = ["react", "react-dom", "react-dom/*", "react/jsx-runtime"];
267
+
268
+ function featureFolderLayers({
269
+ kitPackage,
270
+ featureRoot,
271
+ extraPatterns = {},
272
+ viewDirs,
273
+ dataDirs,
274
+ actionPatterns,
275
+ }) {
276
+ if (viewDirs !== undefined || dataDirs !== undefined || actionPatterns !== undefined) {
277
+ throw new Error("defineLayers: preset feature-folders uses featureRoot, not viewDirs/dataDirs/actionPatterns");
278
+ }
279
+ if (typeof featureRoot !== "string" || featureRoot === "") {
280
+ throw new Error("defineLayers: featureRoot is required for preset feature-folders");
281
+ }
282
+ assertExtraPatterns(extraPatterns, ["kitScope", "viewData", "dataUi", "model"]);
283
+ const root = featureRoot.replace(/\/$/, "");
284
+ const kitPatterns = [kitPackage, `${kitPackage}/*`];
285
+ const views = asFiles([`${root}/**/ui/**`]);
286
+ const adapters = asFiles([`${root}/**/adapters/**`]);
287
+ const model = asFiles([`${root}/**/model/**`]);
288
+ const compose = [`${root}/*/index.ts`, `${root}/*/index.tsx`, `${root}/*/compose.ts`, `${root}/*/compose.tsx`];
289
+ const extras = { kitScope: [], viewData: [], dataUi: [], model: [], ...extraPatterns };
290
+ return [
291
+ {
292
+ name: "zedkit/layers-kit-scope",
293
+ files: asFiles([root]),
294
+ ignores: [...views, ...adapters, ...model, ...compose],
295
+ rules: {
296
+ "no-restricted-imports": [
297
+ "error",
298
+ {
299
+ patterns: [
300
+ {
301
+ group: kitPatterns,
302
+ message:
303
+ "layers-features: UI kit imports are only allowed from view files. Move this import into a view.",
304
+ },
305
+ {
306
+ group: REACT_GROUP,
307
+ message: "layers-features: only ui/ and the composition root import React.",
308
+ },
309
+ {
310
+ group: ADAPTER_GROUP,
311
+ message:
312
+ "layers-features: inject adapters at the feature composition root (index.ts or compose.ts).",
313
+ },
314
+ ...extras.kitScope,
315
+ ],
316
+ },
317
+ ],
318
+ },
319
+ },
320
+ {
321
+ name: "zedkit/layers-view-data",
322
+ files: views,
323
+ rules: {
324
+ "no-restricted-imports": [
325
+ "error",
326
+ {
327
+ patterns: [
328
+ {
329
+ group: ADAPTER_GROUP,
330
+ message:
331
+ "layers-features: UI files import ports, not adapters. Inject the adapter at the feature composition root.",
332
+ },
333
+ ...extras.viewData,
334
+ ],
335
+ },
336
+ ],
337
+ },
338
+ },
339
+ {
340
+ name: "zedkit/layers-data-ui",
341
+ files: adapters,
342
+ rules: {
343
+ "no-restricted-imports": [
344
+ "error",
345
+ {
346
+ patterns: [
347
+ {
348
+ group: REACT_GROUP,
349
+ message: "layers-features: data code is UI-free: no React runtime imports.",
350
+ },
351
+ {
352
+ group: kitPatterns,
353
+ message: "layers-features: data code is UI-free: no UI kit imports.",
354
+ },
355
+ ...extras.dataUi,
356
+ ],
357
+ },
358
+ ],
359
+ },
360
+ },
361
+ {
362
+ name: "zedkit/layers-model-pure",
363
+ files: model,
364
+ rules: {
365
+ "no-restricted-imports": [
366
+ "error",
367
+ {
368
+ patterns: [
369
+ {
370
+ group: REACT_GROUP,
371
+ message: "layers-features: model is pure: no React runtime. Keep I/O in adapters.",
372
+ },
373
+ {
374
+ group: kitPatterns,
375
+ message: "layers-features: model is pure: no UI kit imports.",
376
+ },
377
+ {
378
+ group: ADAPTER_GROUP,
379
+ message: "layers-features: model is pure: adapters are injected at the composition root.",
380
+ },
381
+ ...extras.model,
382
+ ],
383
+ },
384
+ ],
385
+ },
386
+ },
387
+ ];
388
+ }