@zedtech/zedkit-quality 0.0.0-stage → 0.2.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 +21 -0
- package/README.md +76 -2
- package/package.json +74 -3
- package/src/eslint.d.mts +21 -0
- package/src/eslint.mjs +188 -0
- package/src/index.d.mts +2 -0
- package/src/index.mjs +1 -0
- package/tsconfig.base.json +21 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 zedtech-fr
|
|
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
|
@@ -1,3 +1,77 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @zedtech/zedkit-quality
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@zedtech/zedkit-quality)
|
|
4
|
+
[](https://github.com/zedtech-fr/zedkit/blob/main/LICENSE)
|
|
5
|
+
|
|
6
|
+
Shared quality setup: UI lint preset, route/view/data layer boundaries,
|
|
7
|
+
and one strict TypeScript base. Consume it from any repo — do not copy
|
|
8
|
+
it. Original work by the same author.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
pnpm add -D @zedtech/zedkit-quality
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Peer dependencies (`eslint`, `eslint-plugin-jsx-a11y`,
|
|
17
|
+
`eslint-plugin-better-tailwindcss`) install alongside it.
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
```js
|
|
22
|
+
// eslint.config.mjs
|
|
23
|
+
import { defineLayers, uiPreset } from "@zedtech/zedkit-quality";
|
|
24
|
+
|
|
25
|
+
export default [
|
|
26
|
+
...uiPreset({ entryPoint: "./app/globals.css" }),
|
|
27
|
+
...defineLayers({
|
|
28
|
+
kitPackage: "@acme/ui",
|
|
29
|
+
viewDirs: ["app/**/ui/**"],
|
|
30
|
+
dataDirs: ["lib/**"],
|
|
31
|
+
actionPatterns: ["@/lib/*", "**/actions", "**/actions/**"],
|
|
32
|
+
}),
|
|
33
|
+
];
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
```jsonc
|
|
37
|
+
// tsconfig.json
|
|
38
|
+
{
|
|
39
|
+
"extends": "@zedtech/zedkit-quality/tsconfig",
|
|
40
|
+
"compilerOptions": { "outDir": "dist", "rootDir": "src" },
|
|
41
|
+
"include": ["src"]
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## When to use
|
|
46
|
+
|
|
47
|
+
Reach for this package when several repos must enforce the same gates:
|
|
48
|
+
UI lint rules, layer boundaries, one strict `tsconfig` base. One import
|
|
49
|
+
per need, versioned updates everywhere. For AI agent setup (skills,
|
|
50
|
+
subagents, commands) see `@zedtech/zedkit-ai` instead.
|
|
51
|
+
|
|
52
|
+
## API
|
|
53
|
+
|
|
54
|
+
- `uiPreset({ entryPoint })` — flat-config blocks for `.tsx`/`.jsx`:
|
|
55
|
+
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.
|
|
58
|
+
- `defineLayers({ kitPackage, viewDirs, dataDirs, actionPatterns })` —
|
|
59
|
+
`no-restricted-imports` blocks for routes -> views -> data: the UI kit
|
|
60
|
+
is only imported from views, views never import data clients or
|
|
61
|
+
actions, data files import no UI.
|
|
62
|
+
- `@zedtech/zedkit-quality/tsconfig` — the strict base (`strict`,
|
|
63
|
+
`exactOptionalPropertyTypes`, `noUncheckedIndexedAccess`,
|
|
64
|
+
`noUnusedLocals`, and more). Extend it, do not copy it.
|
|
65
|
+
|
|
66
|
+
Find helpers by need in [CATALOG.md](CATALOG.md).
|
|
67
|
+
|
|
68
|
+
## Install or copy?
|
|
69
|
+
|
|
70
|
+
Install it: these configs must stay identical across repos, and a copy
|
|
71
|
+
drifts silently. Versioned updates propagate through normal bump PRs.
|
|
72
|
+
|
|
73
|
+
## Verify
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
pnpm --filter @zedtech/zedkit-quality typecheck && pnpm --filter @zedtech/zedkit-quality lint && pnpm --filter @zedtech/zedkit-quality test
|
|
77
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,77 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zedtech/zedkit-quality",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public",
|
|
6
|
+
"registry": "https://registry.npmjs.org/"
|
|
7
|
+
},
|
|
8
|
+
"description": "Shared quality setup: UI lint preset, route/view/data layer boundaries, and one strict TypeScript base — consume, do not copy.",
|
|
9
|
+
"keywords": [
|
|
10
|
+
"eslint",
|
|
11
|
+
"eslint-config",
|
|
12
|
+
"tsconfig",
|
|
13
|
+
"typescript",
|
|
14
|
+
"strict",
|
|
15
|
+
"layers",
|
|
16
|
+
"architecture",
|
|
17
|
+
"a11y",
|
|
18
|
+
"tailwindcss",
|
|
19
|
+
"code-quality"
|
|
20
|
+
],
|
|
21
|
+
"homepage": "https://github.com/zedtech-fr/zedkit/tree/main/packages/quality",
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/zedtech-fr/zedkit/issues"
|
|
24
|
+
},
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "https://github.com/zedtech-fr/zedkit",
|
|
29
|
+
"directory": "packages/quality"
|
|
30
|
+
},
|
|
31
|
+
"type": "module",
|
|
32
|
+
"sideEffects": false,
|
|
33
|
+
"imports": {
|
|
34
|
+
"#*": "./src/*"
|
|
35
|
+
},
|
|
36
|
+
"main": "./src/index.mjs",
|
|
37
|
+
"types": "./src/index.d.mts",
|
|
38
|
+
"exports": {
|
|
39
|
+
".": {
|
|
40
|
+
"types": "./src/index.d.mts",
|
|
41
|
+
"import": "./src/index.mjs"
|
|
42
|
+
},
|
|
43
|
+
"./eslint": {
|
|
44
|
+
"types": "./src/eslint.d.mts",
|
|
45
|
+
"import": "./src/eslint.mjs"
|
|
46
|
+
},
|
|
47
|
+
"./tsconfig": "./tsconfig.base.json",
|
|
48
|
+
"./package.json": "./package.json"
|
|
49
|
+
},
|
|
50
|
+
"files": [
|
|
51
|
+
"src/index.mjs",
|
|
52
|
+
"src/index.d.mts",
|
|
53
|
+
"src/eslint.mjs",
|
|
54
|
+
"src/eslint.d.mts",
|
|
55
|
+
"tsconfig.base.json"
|
|
56
|
+
],
|
|
57
|
+
"zedkit": {
|
|
58
|
+
"noBuild": true
|
|
59
|
+
},
|
|
60
|
+
"peerDependencies": {
|
|
61
|
+
"eslint": "9.39.5",
|
|
62
|
+
"eslint-plugin-better-tailwindcss": "4.10.0",
|
|
63
|
+
"eslint-plugin-jsx-a11y": "6.10.2"
|
|
64
|
+
},
|
|
65
|
+
"devDependencies": {
|
|
66
|
+
"eslint": "9.39.5",
|
|
67
|
+
"eslint-plugin-better-tailwindcss": "4.10.0",
|
|
68
|
+
"eslint-plugin-jsx-a11y": "6.10.2",
|
|
69
|
+
"typescript": "5.9.3",
|
|
70
|
+
"vitest": "2.1.9"
|
|
71
|
+
},
|
|
72
|
+
"scripts": {
|
|
73
|
+
"test": "vitest run",
|
|
74
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
75
|
+
"lint": "eslint src tests"
|
|
76
|
+
}
|
|
6
77
|
}
|
package/src/eslint.d.mts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Linter } from "eslint";
|
|
2
|
+
|
|
3
|
+
export interface UiPresetOptions {
|
|
4
|
+
/** Path to the repo's Tailwind CSS entry. Required, no default. */
|
|
5
|
+
readonly entryPoint: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function uiPreset(options: UiPresetOptions): Linter.Config[];
|
|
9
|
+
|
|
10
|
+
export interface DefineLayersOptions {
|
|
11
|
+
/** UI kit specifier (e.g. `@acme/ui`). */
|
|
12
|
+
readonly kitPackage: string;
|
|
13
|
+
/** Minimatch globs for view folders. */
|
|
14
|
+
readonly viewDirs: readonly string[];
|
|
15
|
+
/** Minimatch globs for data-access folders. */
|
|
16
|
+
readonly dataDirs: readonly string[];
|
|
17
|
+
/** Import specifiers for data clients and server actions. */
|
|
18
|
+
readonly actionPatterns: readonly string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function defineLayers(options: DefineLayersOptions): Linter.Config[];
|
package/src/eslint.mjs
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// Shareable UI lint preset: enforces docs/frontend-charter.md by tools.
|
|
2
|
+
//
|
|
3
|
+
// What it enforces on every .tsx/.jsx file:
|
|
4
|
+
// - jsx-a11y recommended (labels, keyboard, focus, ARIA)
|
|
5
|
+
// - Tailwind class hygiene (order, no duplicates, no conflicts, no unknown)
|
|
6
|
+
// - Tokens only: no raw palette colors (bg-red-500), no arbitrary hex/rgb
|
|
7
|
+
// colors, no raw px font sizes — use the semantic theme tokens
|
|
8
|
+
// - No `style` attributes; no raw try/catch (use a tryCatch wrapper)
|
|
9
|
+
//
|
|
10
|
+
// Consumer setup (no copy: install the package, pass your Tailwind entry):
|
|
11
|
+
// `import { uiPreset } from "@zedtech/zedkit-quality"` then
|
|
12
|
+
// `export default [...uiPreset({ entryPoint: "./app/globals.css" })]`.
|
|
13
|
+
import jsxA11y from "eslint-plugin-jsx-a11y";
|
|
14
|
+
import betterTailwindcss from "eslint-plugin-better-tailwindcss";
|
|
15
|
+
|
|
16
|
+
const PALETTE =
|
|
17
|
+
"slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|fuchsia|pink|rose";
|
|
18
|
+
const COLOR_UTILS =
|
|
19
|
+
"bg|text|border|ring|ring-offset|divide|from|via|to|decoration|outline|accent|caret|fill|stroke|shadow|placeholder";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* UI lint preset for .tsx/.jsx files.
|
|
23
|
+
*
|
|
24
|
+
* @param {{ entryPoint: string }} options - Path to the repo's Tailwind CSS
|
|
25
|
+
* entry, so the plugin knows the theme tokens. Required, no default: the
|
|
26
|
+
* per-repo CSS path is the one thing that varies.
|
|
27
|
+
* @returns {import("eslint").Linter.Config[]} Flat config blocks.
|
|
28
|
+
*/
|
|
29
|
+
export function uiPreset({ entryPoint }) {
|
|
30
|
+
if (typeof entryPoint !== "string" || entryPoint === "") {
|
|
31
|
+
throw new Error("uiPreset: `entryPoint` is required (path to your Tailwind CSS entry).");
|
|
32
|
+
}
|
|
33
|
+
return [
|
|
34
|
+
{
|
|
35
|
+
name: "zedkit/ui-a11y",
|
|
36
|
+
files: ["**/*.tsx", "**/*.jsx"],
|
|
37
|
+
...jsxA11y.flatConfigs.recommended,
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: "zedkit/ui-tailwind",
|
|
41
|
+
files: ["**/*.tsx", "**/*.jsx"],
|
|
42
|
+
plugins: { "better-tailwindcss": betterTailwindcss },
|
|
43
|
+
settings: {
|
|
44
|
+
"better-tailwindcss": {
|
|
45
|
+
entryPoint,
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
rules: {
|
|
49
|
+
"better-tailwindcss/enforce-consistent-class-order": "error",
|
|
50
|
+
"better-tailwindcss/no-duplicate-classes": "error",
|
|
51
|
+
"better-tailwindcss/no-conflicting-classes": "error",
|
|
52
|
+
"better-tailwindcss/no-unknown-classes": "error",
|
|
53
|
+
"better-tailwindcss/no-restricted-classes": [
|
|
54
|
+
"error",
|
|
55
|
+
{
|
|
56
|
+
restrict: [
|
|
57
|
+
{
|
|
58
|
+
pattern: `(^|:)(${COLOR_UTILS})-(${PALETTE}|white|black)(-|/|$)`,
|
|
59
|
+
message:
|
|
60
|
+
"Raw palette colors are banned. Use a semantic theme token (see docs/frontend-charter.md).",
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
pattern: "\\[(#|rgb|hsl)",
|
|
64
|
+
message:
|
|
65
|
+
"Arbitrary color values are banned. Use a semantic theme token (see docs/frontend-charter.md).",
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
pattern: "text-\\[[^\\]]*px",
|
|
69
|
+
message: "Raw px font sizes are banned. Use the type scale (see docs/frontend-charter.md).",
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
// NOTE: flat-config rules replace (not merge) by key, so this block
|
|
75
|
+
// restates the root TryStatement ban alongside the style-attribute
|
|
76
|
+
// ban; without the restatement, .tsx files would lose the ban.
|
|
77
|
+
"no-restricted-syntax": [
|
|
78
|
+
"error",
|
|
79
|
+
{
|
|
80
|
+
selector: "TryStatement",
|
|
81
|
+
message: "Raw try/catch is banned. Use tryCatch/tryCatchAsync from @zedtech/zedkit-core.",
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
selector: 'JSXAttribute[name.name="style"]',
|
|
85
|
+
message: "Inline style attributes are banned. Use theme tokens (see docs/frontend-charter.md).",
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// JS files the kit-scope block applies to (minus the view/data ignores).
|
|
94
|
+
const JS_FILES = ["**/*.ts", "**/*.tsx", "**/*.mts", "**/*.cts", "**/*.js", "**/*.jsx", "**/*.mjs", "**/*.cjs"];
|
|
95
|
+
const JS_BRACE = "{ts,tsx,mts,cts,js,jsx,mjs,cjs}";
|
|
96
|
+
|
|
97
|
+
// Flat-config `files` never matches a bare directory (`app/ui/**` matches
|
|
98
|
+
// nothing): dir-style entries become `dir/**/*.{exts}`. Entries that
|
|
99
|
+
// already name files (`**/*.tsx`, braces) pass through untouched. A leaf
|
|
100
|
+
// containing a dot counts as a file pattern, so a directory whose own
|
|
101
|
+
// name has a dot must use a trailing `/**` to be treated as a folder.
|
|
102
|
+
function asFiles(patterns) {
|
|
103
|
+
return patterns.map((entry) => {
|
|
104
|
+
const leaf = entry.slice(entry.lastIndexOf("/") + 1);
|
|
105
|
+
if (entry.endsWith("}") || leaf.includes(".")) return entry;
|
|
106
|
+
const stripped = entry.endsWith("/**") ? entry.slice(0, -3) : entry;
|
|
107
|
+
const dir = stripped.endsWith("/") ? stripped.slice(0, -1) : stripped;
|
|
108
|
+
return `${dir}/**/*.${JS_BRACE}`;
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Route/view/data layer boundaries as `no-restricted-imports` blocks.
|
|
114
|
+
* Dependencies run one way (routes -> views -> data):
|
|
115
|
+
* - the UI kit is only imported from view files;
|
|
116
|
+
* - views receive data as props and never import data clients or actions;
|
|
117
|
+
* - data files import no UI (no React runtime, no UI kit).
|
|
118
|
+
*
|
|
119
|
+
* @param {{ kitPackage: string, viewDirs: string[], dataDirs: string[], actionPatterns: string[] }} 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
|
+
* @returns {import("eslint").Linter.Config[]} Flat config blocks.
|
|
125
|
+
*/
|
|
126
|
+
export function defineLayers({ kitPackage, viewDirs, dataDirs, actionPatterns }) {
|
|
127
|
+
const kitPatterns = [kitPackage, `${kitPackage}/*`];
|
|
128
|
+
const views = asFiles(viewDirs);
|
|
129
|
+
const data = asFiles(dataDirs);
|
|
130
|
+
return [
|
|
131
|
+
{
|
|
132
|
+
name: "zedkit/layers-kit-scope",
|
|
133
|
+
files: JS_FILES,
|
|
134
|
+
ignores: [...views, ...data],
|
|
135
|
+
rules: {
|
|
136
|
+
"no-restricted-imports": [
|
|
137
|
+
"error",
|
|
138
|
+
{
|
|
139
|
+
patterns: [
|
|
140
|
+
{
|
|
141
|
+
group: kitPatterns,
|
|
142
|
+
message: "UI kit imports are only allowed from view files. Move this import into a view.",
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
},
|
|
146
|
+
],
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
name: "zedkit/layers-view-data",
|
|
151
|
+
files: views,
|
|
152
|
+
rules: {
|
|
153
|
+
"no-restricted-imports": [
|
|
154
|
+
"error",
|
|
155
|
+
{
|
|
156
|
+
patterns: [
|
|
157
|
+
{
|
|
158
|
+
group: [...actionPatterns],
|
|
159
|
+
message: "Views receive data as props and never import data clients or server actions.",
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
},
|
|
163
|
+
],
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
name: "zedkit/layers-data-ui",
|
|
168
|
+
files: data,
|
|
169
|
+
rules: {
|
|
170
|
+
"no-restricted-imports": [
|
|
171
|
+
"error",
|
|
172
|
+
{
|
|
173
|
+
patterns: [
|
|
174
|
+
{
|
|
175
|
+
group: ["react", "react-dom", "react-dom/*", "react/jsx-runtime"],
|
|
176
|
+
message: "Data code is UI-free: no React runtime imports.",
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
group: kitPatterns,
|
|
180
|
+
message: "Data code is UI-free: no UI kit imports.",
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
},
|
|
184
|
+
],
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
];
|
|
188
|
+
}
|
package/src/index.d.mts
ADDED
package/src/index.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { defineLayers, uiPreset } from "./eslint.mjs";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"strict": true,
|
|
8
|
+
"exactOptionalPropertyTypes": true,
|
|
9
|
+
"noUncheckedIndexedAccess": true,
|
|
10
|
+
"noUnusedLocals": true,
|
|
11
|
+
"noUnusedParameters": true,
|
|
12
|
+
"noFallthroughCasesInSwitch": true,
|
|
13
|
+
"declaration": true,
|
|
14
|
+
"declarationMap": true,
|
|
15
|
+
"sourceMap": true,
|
|
16
|
+
"skipLibCheck": true,
|
|
17
|
+
"esModuleInterop": true,
|
|
18
|
+
"forceConsistentCasingInFileNames": true,
|
|
19
|
+
"resolveJsonModule": true
|
|
20
|
+
}
|
|
21
|
+
}
|