@zedtech/zedkit-quality 0.3.2 → 0.5.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 +13 -4
- package/package.json +5 -3
- package/src/eslint.d.mts +35 -20
- package/src/eslint.mjs +210 -30
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
|
|
17
|
+
`eslint-plugin-better-tailwindcss`, `typescript-eslint`) install
|
|
18
|
+
alongside it.
|
|
18
19
|
|
|
19
20
|
## Usage
|
|
20
21
|
|
|
@@ -52,9 +53,12 @@ subagents, commands) see `@zedtech/zedkit-ai` instead.
|
|
|
52
53
|
## API
|
|
53
54
|
|
|
54
55
|
- `uiPreset({ entryPoint })` — flat-config blocks for `.tsx`/`.jsx`:
|
|
55
|
-
jsx-a11y recommended, Tailwind hygiene, tokens-only classes
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
jsx-a11y recommended, Tailwind hygiene, tokens-only classes (no
|
|
57
|
+
`w-[13px]` / `gap-[0.75rem]`; Radix `w-[var(--radix-*)]` stays), no `style`
|
|
58
|
+
attributes, no `forwardRef` (ref is a prop; see `packages/ui/src/button.tsx`),
|
|
59
|
+
no explicit `any`, files ≤ 300 lines and functions ≤ 60, no raw `try`/`catch`.
|
|
60
|
+
`entryPoint` (your Tailwind CSS entry) is required: it is the one thing
|
|
61
|
+
that varies per repo.
|
|
58
62
|
- `defineLayers({ kitPackage, viewDirs, dataDirs, actionPatterns, extraPatterns })` —
|
|
59
63
|
`no-restricted-imports` blocks for routes -> views -> data: the UI kit
|
|
60
64
|
is only imported from views, views never import data clients or
|
|
@@ -62,6 +66,11 @@ subagents, commands) see `@zedtech/zedkit-ai` instead.
|
|
|
62
66
|
`{ kitScope?, viewData?, dataUi? }`) merges your own bans into each
|
|
63
67
|
block — use it instead of a second `no-restricted-imports` block,
|
|
64
68
|
which would replace these per flat-config key semantics.
|
|
69
|
+
- `defineLayers({ preset: "feature-folders", kitPackage, featureRoot })` —
|
|
70
|
+
same helper, feature-folder mapping: UI imports ports (not adapters),
|
|
71
|
+
adapters are injected at the composition root, model is pure. Bans
|
|
72
|
+
apply only under `featureRoot`. Live example:
|
|
73
|
+
`apps/docs/features/note-list` (running at `/examples/note-list`).
|
|
65
74
|
- `@zedtech/zedkit-quality/tsconfig` — the strict base (`strict`,
|
|
66
75
|
`exactOptionalPropertyTypes`, `noUncheckedIndexedAccess`,
|
|
67
76
|
`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
|
+
"version": "0.5.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
|
-
"
|
|
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
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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,21 @@
|
|
|
1
|
-
// Shareable UI lint preset:
|
|
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
|
-
// colors, no raw px font sizes
|
|
8
|
-
// -
|
|
8
|
+
// colors, no raw px font sizes, no arbitrary px/rem in className
|
|
9
|
+
// (w-[13px], gap-[0.75rem]) — use the semantic theme tokens
|
|
10
|
+
// - No `style` attributes; no forwardRef (ref is a prop); no explicit `any`;
|
|
11
|
+
// files ≤ 300 lines, functions ≤ 60; no raw try/catch (use a tryCatch wrapper)
|
|
9
12
|
//
|
|
10
13
|
// Consumer setup (no copy: install the package, pass your Tailwind entry):
|
|
11
14
|
// `import { uiPreset } from "@zedtech/zedkit-quality"` then
|
|
12
15
|
// `export default [...uiPreset({ entryPoint: "./app/globals.css" })]`.
|
|
13
16
|
import jsxA11y from "eslint-plugin-jsx-a11y";
|
|
14
17
|
import betterTailwindcss from "eslint-plugin-better-tailwindcss";
|
|
18
|
+
import { plugin as tsPlugin } from "typescript-eslint";
|
|
15
19
|
|
|
16
20
|
const PALETTE =
|
|
17
21
|
"slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|fuchsia|pink|rose";
|
|
@@ -57,36 +61,76 @@ export function uiPreset({ entryPoint }) {
|
|
|
57
61
|
{
|
|
58
62
|
pattern: `(^|:)(${COLOR_UTILS})-(${PALETTE}|white|black)(-|/|$)`,
|
|
59
63
|
message:
|
|
60
|
-
"
|
|
64
|
+
"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
65
|
},
|
|
62
66
|
{
|
|
63
67
|
pattern: "\\[(#|rgb|hsl)",
|
|
64
68
|
message:
|
|
65
|
-
"
|
|
69
|
+
"tokens-color: arbitrary hex/rgb/hsl colors are banned. Use a semantic token (background, foreground, primary, …).",
|
|
66
70
|
},
|
|
67
71
|
{
|
|
68
72
|
pattern: "text-\\[[^\\]]*px",
|
|
69
|
-
message:
|
|
73
|
+
message:
|
|
74
|
+
"tokens-style: raw px font sizes are banned. Use the type scale (text-xs … text-2xl).",
|
|
75
|
+
},
|
|
76
|
+
// Radix CSS variables (w-[var(--radix-popover-trigger-width)])
|
|
77
|
+
// are not matched: they are not a px/rem literal. Overlays
|
|
78
|
+
// must follow the trigger size. Hardcoded colours are already
|
|
79
|
+
// tokens-color (palette + hex/rgb/hsl).
|
|
80
|
+
{
|
|
81
|
+
pattern: "\\[[0-9.]+(?:px|rem)\\]",
|
|
82
|
+
message:
|
|
83
|
+
"tokens-arbitrary: raw px/rem in className (w-[13px], gap-[0.75rem]) are banned — use the spacing scale (w-4, gap-3) or the type scale (text-sm).",
|
|
70
84
|
},
|
|
71
85
|
],
|
|
72
86
|
},
|
|
73
87
|
],
|
|
74
88
|
// NOTE: flat-config rules replace (not merge) by key, so this block
|
|
75
89
|
// restates the root TryStatement ban alongside the style-attribute
|
|
76
|
-
//
|
|
90
|
+
// and forwardRef bans; without the restatement, .tsx files would lose them.
|
|
77
91
|
"no-restricted-syntax": [
|
|
78
92
|
"error",
|
|
79
93
|
{
|
|
80
94
|
selector: "TryStatement",
|
|
81
|
-
message:
|
|
95
|
+
message:
|
|
96
|
+
"try-catch: raw try/catch is banned. Use tryCatch/tryCatchAsync from @zedtech/zedkit-core so thrown values become Error with cause.",
|
|
82
97
|
},
|
|
83
98
|
{
|
|
84
99
|
selector: 'JSXAttribute[name.name="style"]',
|
|
85
|
-
message:
|
|
100
|
+
message:
|
|
101
|
+
"tokens-style: inline style attributes are banned. Use theme tokens and utility classes.",
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
selector: "Identifier[name='forwardRef']",
|
|
105
|
+
message:
|
|
106
|
+
"ui-ref: forwardRef is banned. Pass ref as a prop on ComponentProps<\"button\"> (see packages/ui/src/button.tsx).",
|
|
86
107
|
},
|
|
87
108
|
],
|
|
88
109
|
},
|
|
89
110
|
},
|
|
111
|
+
{
|
|
112
|
+
name: "zedkit/ui-types",
|
|
113
|
+
files: ["**/*.tsx", "**/*.jsx"],
|
|
114
|
+
plugins: { "@typescript-eslint": tsPlugin },
|
|
115
|
+
rules: {
|
|
116
|
+
"@typescript-eslint/no-explicit-any": "error",
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
name: "zedkit/ui-size",
|
|
121
|
+
files: ["**/*.tsx", "**/*.jsx"],
|
|
122
|
+
ignores: ["**/*.test.*", "**/*.spec.*", "**/tests/**", "**/__tests__/**"],
|
|
123
|
+
rules: {
|
|
124
|
+
"max-lines": [
|
|
125
|
+
"error",
|
|
126
|
+
{ max: 300, skipBlankLines: true, skipComments: true },
|
|
127
|
+
],
|
|
128
|
+
"max-lines-per-function": [
|
|
129
|
+
"error",
|
|
130
|
+
{ max: 60, skipBlankLines: true, skipComments: true, IIFEs: true },
|
|
131
|
+
],
|
|
132
|
+
},
|
|
133
|
+
},
|
|
90
134
|
];
|
|
91
135
|
}
|
|
92
136
|
|
|
@@ -109,6 +153,18 @@ function asFiles(patterns) {
|
|
|
109
153
|
});
|
|
110
154
|
}
|
|
111
155
|
|
|
156
|
+
function assertExtraPatterns(extraPatterns, keys) {
|
|
157
|
+
const want = keys.join(", ").replace(/, ([^,]+)$/, " or $1");
|
|
158
|
+
for (const [key, value] of Object.entries(extraPatterns)) {
|
|
159
|
+
if (!keys.includes(key)) {
|
|
160
|
+
throw new Error(`defineLayers: unknown extraPatterns key "${key}" (want ${want})`);
|
|
161
|
+
}
|
|
162
|
+
if (!Array.isArray(value)) {
|
|
163
|
+
throw new Error(`defineLayers: extraPatterns.${key} must be a list of no-restricted-imports pattern objects`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
112
168
|
/**
|
|
113
169
|
* Route/view/data layer boundaries as `no-restricted-imports` blocks.
|
|
114
170
|
* Dependencies run one way (routes -> views -> data):
|
|
@@ -116,30 +172,29 @@ function asFiles(patterns) {
|
|
|
116
172
|
* - views receive data as props and never import data clients or actions;
|
|
117
173
|
* - data files import no UI (no React runtime, no UI kit).
|
|
118
174
|
*
|
|
119
|
-
* @param {
|
|
120
|
-
* - `kitPackage
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
* Keys: `kitScope`, `viewData`, `dataUi`; each a list of standard
|
|
131
|
-
* `no-restricted-imports` pattern objects, passed through untouched.
|
|
175
|
+
* @param {object} options
|
|
176
|
+
* - Classic: `{ kitPackage, viewDirs, dataDirs, actionPatterns, extraPatterns? }`.
|
|
177
|
+
* `actionPatterns` may be empty (views legitimately import data): the
|
|
178
|
+
* view ban is then omitted, and the view block is dropped when it
|
|
179
|
+
* would carry no bans at all. `extraPatterns` merges consumer bans
|
|
180
|
+
* into each block (keys: `kitScope`, `viewData`, `dataUi`).
|
|
181
|
+
* - Preset `feature-folders`: `{ preset: "feature-folders", kitPackage,
|
|
182
|
+
* featureRoot, extraPatterns? }`. UI may import ports, not adapters;
|
|
183
|
+
* adapters are injected at the feature composition root; model is
|
|
184
|
+
* pure. Bans apply only under `featureRoot` (extraPatterns also
|
|
185
|
+
* accepts `model`).
|
|
132
186
|
* @returns {import("eslint").Linter.Config[]} Flat config blocks.
|
|
133
187
|
*/
|
|
134
|
-
export function defineLayers(
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
139
|
-
if (!Array.isArray(value)) {
|
|
140
|
-
throw new Error(`defineLayers: extraPatterns.${key} must be a list of no-restricted-imports pattern objects`);
|
|
141
|
-
}
|
|
188
|
+
export function defineLayers(options) {
|
|
189
|
+
const { preset, extraPatterns = {} } = options;
|
|
190
|
+
if (preset === "feature-folders") {
|
|
191
|
+
return featureFolderLayers(options);
|
|
142
192
|
}
|
|
193
|
+
if (preset !== undefined) {
|
|
194
|
+
throw new Error(`defineLayers: unknown preset "${preset}" (want feature-folders)`);
|
|
195
|
+
}
|
|
196
|
+
const { kitPackage, viewDirs, dataDirs, actionPatterns } = options;
|
|
197
|
+
assertExtraPatterns(extraPatterns, ["kitScope", "viewData", "dataUi"]);
|
|
143
198
|
if (!Array.isArray(actionPatterns)) {
|
|
144
199
|
throw new Error("defineLayers: actionPatterns must be a list of import specifiers (may be empty)");
|
|
145
200
|
}
|
|
@@ -216,3 +271,128 @@ export function defineLayers({ kitPackage, viewDirs, dataDirs, actionPatterns, e
|
|
|
216
271
|
},
|
|
217
272
|
];
|
|
218
273
|
}
|
|
274
|
+
|
|
275
|
+
const ADAPTER_GROUP = ["**/adapters", "**/adapters/**"];
|
|
276
|
+
const REACT_GROUP = ["react", "react-dom", "react-dom/*", "react/jsx-runtime"];
|
|
277
|
+
|
|
278
|
+
function featureFolderLayers({
|
|
279
|
+
kitPackage,
|
|
280
|
+
featureRoot,
|
|
281
|
+
extraPatterns = {},
|
|
282
|
+
viewDirs,
|
|
283
|
+
dataDirs,
|
|
284
|
+
actionPatterns,
|
|
285
|
+
}) {
|
|
286
|
+
if (viewDirs !== undefined || dataDirs !== undefined || actionPatterns !== undefined) {
|
|
287
|
+
throw new Error("defineLayers: preset feature-folders uses featureRoot, not viewDirs/dataDirs/actionPatterns");
|
|
288
|
+
}
|
|
289
|
+
if (typeof featureRoot !== "string" || featureRoot === "") {
|
|
290
|
+
throw new Error("defineLayers: featureRoot is required for preset feature-folders");
|
|
291
|
+
}
|
|
292
|
+
assertExtraPatterns(extraPatterns, ["kitScope", "viewData", "dataUi", "model"]);
|
|
293
|
+
const root = featureRoot.replace(/\/$/, "");
|
|
294
|
+
const kitPatterns = [kitPackage, `${kitPackage}/*`];
|
|
295
|
+
const views = asFiles([`${root}/**/ui/**`]);
|
|
296
|
+
const adapters = asFiles([`${root}/**/adapters/**`]);
|
|
297
|
+
const model = asFiles([`${root}/**/model/**`]);
|
|
298
|
+
const compose = [`${root}/*/index.ts`, `${root}/*/index.tsx`, `${root}/*/compose.ts`, `${root}/*/compose.tsx`];
|
|
299
|
+
const extras = { kitScope: [], viewData: [], dataUi: [], model: [], ...extraPatterns };
|
|
300
|
+
return [
|
|
301
|
+
{
|
|
302
|
+
name: "zedkit/layers-kit-scope",
|
|
303
|
+
files: asFiles([root]),
|
|
304
|
+
ignores: [...views, ...adapters, ...model, ...compose],
|
|
305
|
+
rules: {
|
|
306
|
+
"no-restricted-imports": [
|
|
307
|
+
"error",
|
|
308
|
+
{
|
|
309
|
+
patterns: [
|
|
310
|
+
{
|
|
311
|
+
group: kitPatterns,
|
|
312
|
+
message:
|
|
313
|
+
"layers-features: UI kit imports are only allowed from view files. Move this import into a view.",
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
group: REACT_GROUP,
|
|
317
|
+
message: "layers-features: only ui/ and the composition root import React.",
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
group: ADAPTER_GROUP,
|
|
321
|
+
message:
|
|
322
|
+
"layers-features: inject adapters at the feature composition root (index.ts or compose.ts).",
|
|
323
|
+
},
|
|
324
|
+
...extras.kitScope,
|
|
325
|
+
],
|
|
326
|
+
},
|
|
327
|
+
],
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
name: "zedkit/layers-view-data",
|
|
332
|
+
files: views,
|
|
333
|
+
rules: {
|
|
334
|
+
"no-restricted-imports": [
|
|
335
|
+
"error",
|
|
336
|
+
{
|
|
337
|
+
patterns: [
|
|
338
|
+
{
|
|
339
|
+
group: ADAPTER_GROUP,
|
|
340
|
+
message:
|
|
341
|
+
"layers-features: UI files import ports, not adapters. Inject the adapter at the feature composition root.",
|
|
342
|
+
},
|
|
343
|
+
...extras.viewData,
|
|
344
|
+
],
|
|
345
|
+
},
|
|
346
|
+
],
|
|
347
|
+
},
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
name: "zedkit/layers-data-ui",
|
|
351
|
+
files: adapters,
|
|
352
|
+
rules: {
|
|
353
|
+
"no-restricted-imports": [
|
|
354
|
+
"error",
|
|
355
|
+
{
|
|
356
|
+
patterns: [
|
|
357
|
+
{
|
|
358
|
+
group: REACT_GROUP,
|
|
359
|
+
message: "layers-features: data code is UI-free: no React runtime imports.",
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
group: kitPatterns,
|
|
363
|
+
message: "layers-features: data code is UI-free: no UI kit imports.",
|
|
364
|
+
},
|
|
365
|
+
...extras.dataUi,
|
|
366
|
+
],
|
|
367
|
+
},
|
|
368
|
+
],
|
|
369
|
+
},
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
name: "zedkit/layers-model-pure",
|
|
373
|
+
files: model,
|
|
374
|
+
rules: {
|
|
375
|
+
"no-restricted-imports": [
|
|
376
|
+
"error",
|
|
377
|
+
{
|
|
378
|
+
patterns: [
|
|
379
|
+
{
|
|
380
|
+
group: REACT_GROUP,
|
|
381
|
+
message: "layers-features: model is pure: no React runtime. Keep I/O in adapters.",
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
group: kitPatterns,
|
|
385
|
+
message: "layers-features: model is pure: no UI kit imports.",
|
|
386
|
+
},
|
|
387
|
+
{
|
|
388
|
+
group: ADAPTER_GROUP,
|
|
389
|
+
message: "layers-features: model is pure: adapters are injected at the composition root.",
|
|
390
|
+
},
|
|
391
|
+
...extras.model,
|
|
392
|
+
],
|
|
393
|
+
},
|
|
394
|
+
],
|
|
395
|
+
},
|
|
396
|
+
},
|
|
397
|
+
];
|
|
398
|
+
}
|