check-rule-mate 0.5.3 → 0.5.5
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 +94 -1
- package/bin/generate-docs-playground-experimental.js +617 -0
- package/bin/verify-templates.js +226 -0
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -58,6 +58,7 @@ This separation makes the system flexible, scalable, and easy to maintain.
|
|
|
58
58
|
- i18n-ready error messages
|
|
59
59
|
- Framework-agnostic (frontend or backend)
|
|
60
60
|
- Auto documentation (automatic generated by a CLI command)
|
|
61
|
+
- Template checker (automatic checked by a CLI command)
|
|
61
62
|
|
|
62
63
|
## Table of Contents
|
|
63
64
|
|
|
@@ -69,6 +70,8 @@ This separation makes the system flexible, scalable, and easy to maintain.
|
|
|
69
70
|
- [Basic Usage](#Basic-Usage)
|
|
70
71
|
- [Lifecycle Hooks](#Lifecycle-Hooks)
|
|
71
72
|
- [Auto documentation](#Auto-Documentation)
|
|
73
|
+
- [[Experimental] - Documentation Playground](#experimental-documentation-playground)
|
|
74
|
+
- [Auto Checker for Templates](#Auto-Checker-for-Templates)
|
|
72
75
|
- [Defining Validation](#Defining-Validation)
|
|
73
76
|
- [1. Schema](#Defining-a-Schema-What-to-validate)
|
|
74
77
|
- [2. Rules](#Defining-Rules-How-to-validate)
|
|
@@ -291,11 +294,101 @@ await validator.validate();
|
|
|
291
294
|
To use that it is simple, you only need to run this command:
|
|
292
295
|
|
|
293
296
|
```bash
|
|
294
|
-
npx check-rule-mate-auto-docs --rules rules
|
|
297
|
+
npx check-rule-mate-auto-docs --rules {rules path} --schemas {schemas path} --errors {errors path} --out {file.html}
|
|
295
298
|
```
|
|
296
299
|
|
|
297
300
|
This will generate a HTML file containing the rules, schemas and errors.
|
|
298
301
|
|
|
302
|
+
### [Experimental] Documentation Playground
|
|
303
|
+
|
|
304
|
+
> ⚠️ The playground executes bundled client-side JavaScript.
|
|
305
|
+
> Do not use untrusted validation code.
|
|
306
|
+
|
|
307
|
+
The **Docs Playground** allows you to generate an interactive HTML page where you can **test your schemas, rules, and validators directly in the browser**.
|
|
308
|
+
|
|
309
|
+
This feature is **experimental** and intended mainly for development and exploration purposes. It bundles your validation logic into a client-side format, enabling real-time validation without any backend setup.
|
|
310
|
+
|
|
311
|
+
> ⚠️ Since this feature is experimental, APIs and behavior may change.
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
|
|
315
|
+
#### Requirements
|
|
316
|
+
|
|
317
|
+
The playground generator depends on **optional dependencies** that are **not installed by default**.
|
|
318
|
+
|
|
319
|
+
Before using it, make sure you have the required optional dependencies installed:
|
|
320
|
+
|
|
321
|
+
```bash
|
|
322
|
+
npm install esbuild
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
#### Usage
|
|
326
|
+
|
|
327
|
+
Run the playground generator using the CLI:
|
|
328
|
+
```bash
|
|
329
|
+
npx check-rule-mate-auto-docs-playground-experimental \
|
|
330
|
+
--rules {rules-path} \
|
|
331
|
+
--schemas {schemas-path} \
|
|
332
|
+
--errors {errors-path} \
|
|
333
|
+
--options {options-path} \
|
|
334
|
+
--out {output-file.html}
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
This command will generate a self-contained HTML file with:
|
|
338
|
+
- Documentation for your rules and schemas
|
|
339
|
+
- A playground to test validation behavior in real time
|
|
340
|
+
|
|
341
|
+
#### Options File
|
|
342
|
+
|
|
343
|
+
The playground requires an **options file** to map validator names to their source files.
|
|
344
|
+
|
|
345
|
+
Example:
|
|
346
|
+
```json
|
|
347
|
+
{
|
|
348
|
+
"validators": {
|
|
349
|
+
"myValidator": "./examples/vanilla/src/check-rule-mate-rules/validators/validators.js",
|
|
350
|
+
"nameValidator": "./examples/vanilla/src/check-rule-mate-rules/validators/validators.js",
|
|
351
|
+
"myValidator2": "./examples/vanilla/src/check-rule-mate-rules/validators/validator2.js"
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
```
|
|
355
|
+
**How it works**:
|
|
356
|
+
- **Keys** represent the **exported object/function/class** name of your **validator**
|
|
357
|
+
- **Values** are paths to the **files** where those validators are defined
|
|
358
|
+
- These validators are bundled and made available inside the playground
|
|
359
|
+
|
|
360
|
+
This explicit mapping ensures predictable builds and avoids magic imports.
|
|
361
|
+
|
|
362
|
+
### Auto Checker for Templates
|
|
363
|
+
You can **auto check** if your template it is working properly with all necessary properties or have something missing.
|
|
364
|
+
|
|
365
|
+
It is works with: `Schemas`, `Rules` and `Errors`
|
|
366
|
+
|
|
367
|
+
```bash
|
|
368
|
+
npx check-rule-mate-verify-templates --rules {rules path} --schemas {schemas path} --errors {errors path}
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
If everything it is **working properly** this should be your message:
|
|
372
|
+
```bash
|
|
373
|
+
✔ Schemas loaded: 4
|
|
374
|
+
✔ Rules loaded: 6
|
|
375
|
+
✔ Errors loaded: 6
|
|
376
|
+
|
|
377
|
+
✔ No issues found
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
Else if some error was found could be something like that:
|
|
381
|
+
```bash
|
|
382
|
+
✔ Schemas loaded: 4
|
|
383
|
+
✔ Rules loaded: 6
|
|
384
|
+
✔ Errors loaded: 6
|
|
385
|
+
|
|
386
|
+
❌ Validation failed
|
|
387
|
+
|
|
388
|
+
❌ Schema "contactUs.json" → field "phone" references rule "cellphone" which does not exist
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
|
|
299
392
|
## Defining Validation
|
|
300
393
|
|
|
301
394
|
### Defining a Schema (What to validate)
|
|
@@ -0,0 +1,617 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* check-rule-mate — Auto Documentation Generator
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// import fs from "fs";
|
|
7
|
+
// import path from "path";
|
|
8
|
+
// import process from "process";
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const process = require('process');
|
|
13
|
+
let esbuild;
|
|
14
|
+
try {
|
|
15
|
+
esbuild = require('esbuild');
|
|
16
|
+
} catch {
|
|
17
|
+
console.warn(
|
|
18
|
+
'[check-rule-mate] esbuild not found. ' +
|
|
19
|
+
'Install it to enable playground generation.'
|
|
20
|
+
);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/* ---------------------------------------
|
|
25
|
+
* CLI ARGS
|
|
26
|
+
* -------------------------------------*/
|
|
27
|
+
const args = process.argv.slice(2);
|
|
28
|
+
const getArg = (flag) => {
|
|
29
|
+
const index = args.indexOf(flag);
|
|
30
|
+
return index !== -1 ? args[index + 1] : null;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const RULES_DIR = getArg('--rules');
|
|
34
|
+
const SCHEMAS_DIR = getArg('--schemas');
|
|
35
|
+
const ERRORS_DIR = getArg('--errors')
|
|
36
|
+
const OPTIONS_DIR = getArg('--options');
|
|
37
|
+
const OUTPUT = getArg('--out') || "check-rule-mate-docs.html";
|
|
38
|
+
const OUTPUT_DIR = path.dirname(OUTPUT);
|
|
39
|
+
|
|
40
|
+
const OPTIONS = OPTIONS_DIR ? JSON.parse(fs.readFileSync(OPTIONS_DIR, "utf-8")) : null;
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
if (OPTIONS) {
|
|
44
|
+
let entryPointFile = Object.keys(OPTIONS.validators).map((key) => `var ${key} = require('${OPTIONS.validators[key]}')`).join('\n');
|
|
45
|
+
entryPointFile += `\n window.validatorHelpers = {${Object.keys(OPTIONS.validators).map((key) => `${key}: ${key}`)}}`
|
|
46
|
+
|
|
47
|
+
fs.writeFileSync(`./check-rule-mate.validators.docs.js`, entryPointFile);
|
|
48
|
+
|
|
49
|
+
esbuild.build({
|
|
50
|
+
entryPoints: ['./node_modules/check-rule-mate/src/main.js'],
|
|
51
|
+
bundle: true,
|
|
52
|
+
outfile: `${OUTPUT_DIR}/check-rule-mate.js`,
|
|
53
|
+
platform: 'browser',
|
|
54
|
+
target: 'node16',
|
|
55
|
+
format: 'iife',
|
|
56
|
+
minify: false
|
|
57
|
+
}).then(() => {
|
|
58
|
+
console.log('Build completed successfully: check-rule-mate.js');
|
|
59
|
+
|
|
60
|
+
const checkRuleMateFIle = fs.readFileSync(`${OUTPUT_DIR}/check-rule-mate.js`, 'utf-8');
|
|
61
|
+
const lines = checkRuleMateFIle.split('\n')
|
|
62
|
+
lines.shift();
|
|
63
|
+
lines.splice(lines.length - 2, 1)
|
|
64
|
+
fs.writeFileSync(`${OUTPUT_DIR}/check-rule-mate.js`, lines.join('\n'))
|
|
65
|
+
}).catch(() => process.exit(1));
|
|
66
|
+
|
|
67
|
+
esbuild.build({
|
|
68
|
+
entryPoints: [`./check-rule-mate.validators.docs.js`],
|
|
69
|
+
bundle: true,
|
|
70
|
+
outfile: `${OUTPUT_DIR}/index.js`,
|
|
71
|
+
platform: 'browser',
|
|
72
|
+
target: 'node16',
|
|
73
|
+
format: 'iife',
|
|
74
|
+
minify: false
|
|
75
|
+
}).then(() => {
|
|
76
|
+
console.log('Build completed successfully: index.js');
|
|
77
|
+
fs.rmSync('./check-rule-mate.validators.docs.js')
|
|
78
|
+
}).catch(() => process.exit(1));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
if (!RULES_DIR || !SCHEMAS_DIR) {
|
|
83
|
+
console.error(`
|
|
84
|
+
Usage:
|
|
85
|
+
npx check-rule-mate-auto-docs --rules ./rules --schemas ./schemas --errors ./errors --out docs.html
|
|
86
|
+
`);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/* ---------------------------------------
|
|
91
|
+
* HELPERS
|
|
92
|
+
* -------------------------------------*/
|
|
93
|
+
const readJSONFiles = (dir) => {
|
|
94
|
+
return fs.readdirSync(dir)
|
|
95
|
+
.filter(f => f.endsWith(".json"))
|
|
96
|
+
.map(file => ({
|
|
97
|
+
name: file.replace(".json", ""),
|
|
98
|
+
content: JSON.parse(fs.readFileSync(path.join(dir, file), "utf-8"))
|
|
99
|
+
}));
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const rulesFiles = readJSONFiles(RULES_DIR);
|
|
103
|
+
const schemasFiles = readJSONFiles(SCHEMAS_DIR);
|
|
104
|
+
const errorsFiles = readJSONFiles(ERRORS_DIR);
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
/* ---------------------------------------
|
|
108
|
+
* INDEX RELATIONSHIPS
|
|
109
|
+
* -------------------------------------*/
|
|
110
|
+
const ruleUsageMap = {};
|
|
111
|
+
const errorUsageMap = {}
|
|
112
|
+
|
|
113
|
+
schemasFiles.forEach(schemaFile => {
|
|
114
|
+
Object.entries(schemaFile.content).forEach(([field, config]) => {
|
|
115
|
+
const ruleName = config.rule.split("--")[0];
|
|
116
|
+
if (!ruleUsageMap[ruleName]) ruleUsageMap[ruleName] = [];
|
|
117
|
+
ruleUsageMap[ruleName].push({
|
|
118
|
+
schema: schemaFile.name,
|
|
119
|
+
field,
|
|
120
|
+
rule: config.rule
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
rulesFiles.forEach(rulesFile => {
|
|
126
|
+
Object.entries(rulesFile.content).forEach(([field, config]) => {
|
|
127
|
+
Object.entries(config.error).forEach(([key, value]) => {
|
|
128
|
+
const errorName = value;
|
|
129
|
+
if (!errorUsageMap[errorName]) errorUsageMap[errorName] = [];
|
|
130
|
+
errorUsageMap[errorName].push({
|
|
131
|
+
file: rulesFile.name,
|
|
132
|
+
field,
|
|
133
|
+
error: value
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
if (config?.modifier) {
|
|
137
|
+
Object.entries(config.modifier).forEach(([modifierKey, modifier]) => {
|
|
138
|
+
if (modifier?.error) {
|
|
139
|
+
Object.entries(modifier.error).forEach(([key, value]) => {
|
|
140
|
+
const errorName = value;
|
|
141
|
+
if (!errorUsageMap[errorName]) errorUsageMap[errorName] = [];
|
|
142
|
+
errorUsageMap[errorName].push({
|
|
143
|
+
file: rulesFile.name,
|
|
144
|
+
field,
|
|
145
|
+
error: value
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
/* ---------------------------------------
|
|
156
|
+
* HTML GENERATION
|
|
157
|
+
* -------------------------------------*/
|
|
158
|
+
const html = `
|
|
159
|
+
<!DOCTYPE html>
|
|
160
|
+
<html lang="en">
|
|
161
|
+
<head>
|
|
162
|
+
<meta charset="UTF-8" />
|
|
163
|
+
<title>check-rule-mate — Documentation</title>
|
|
164
|
+
<style>
|
|
165
|
+
${generateCSS()}
|
|
166
|
+
</style>
|
|
167
|
+
${OPTIONS ? `
|
|
168
|
+
<script src="./check-rule-mate.js" defer></script>
|
|
169
|
+
<script src="./index.js" defer></script>`
|
|
170
|
+
: ''}
|
|
171
|
+
</head>
|
|
172
|
+
<body>
|
|
173
|
+
|
|
174
|
+
<aside class="sidebar">
|
|
175
|
+
<input id="search" placeholder="Search..." />
|
|
176
|
+
|
|
177
|
+
<h3>Schemas</h3>
|
|
178
|
+
${schemasFiles.map(s => `
|
|
179
|
+
<a href="#schema-${s.name}">${s.name}</a>
|
|
180
|
+
`).join("")}
|
|
181
|
+
|
|
182
|
+
<h3>Rules</h3>
|
|
183
|
+
${rulesFiles.map(rf =>
|
|
184
|
+
Object.keys(rf.content).map(rule => `
|
|
185
|
+
<a href="#rule-${rule}">${rf.name} - ${rule}</a>
|
|
186
|
+
`).join("")
|
|
187
|
+
).join("")}
|
|
188
|
+
|
|
189
|
+
<h3>Errors</h3>
|
|
190
|
+
${errorsFiles.map(errorFile =>
|
|
191
|
+
Object.keys(errorFile.content).map(error => `
|
|
192
|
+
<a href="#error-${error}">${errorFile.name} - ${error}</a>
|
|
193
|
+
`).join("")
|
|
194
|
+
).join("")}
|
|
195
|
+
</aside>
|
|
196
|
+
|
|
197
|
+
<main>
|
|
198
|
+
<section>
|
|
199
|
+
<h1>check-rule-mate</h1>
|
|
200
|
+
<p class="muted">
|
|
201
|
+
Visual documentation of validation rules and schemas.
|
|
202
|
+
</p>
|
|
203
|
+
</section>
|
|
204
|
+
|
|
205
|
+
${renderSchemas(schemasFiles)}
|
|
206
|
+
${renderRules(rulesFiles, ruleUsageMap)}
|
|
207
|
+
${renderErrors(errorsFiles, errorUsageMap)}
|
|
208
|
+
</main>
|
|
209
|
+
|
|
210
|
+
<script>
|
|
211
|
+
${generateClientJS()}
|
|
212
|
+
</script>
|
|
213
|
+
|
|
214
|
+
</body>
|
|
215
|
+
</html>
|
|
216
|
+
`;
|
|
217
|
+
|
|
218
|
+
fs.writeFileSync(OUTPUT, html);
|
|
219
|
+
console.log(`✔ Documentation generated at ${OUTPUT}`);
|
|
220
|
+
|
|
221
|
+
/* ---------------------------------------
|
|
222
|
+
* RENDERERS
|
|
223
|
+
* -------------------------------------*/
|
|
224
|
+
function renderSchemas(schemas) {
|
|
225
|
+
return schemas.map(schema => `
|
|
226
|
+
<section id="schema-${schema.name}" class="card">
|
|
227
|
+
<h2>Schema: ${schema.name}</h2>
|
|
228
|
+
|
|
229
|
+
<table>
|
|
230
|
+
<thead>
|
|
231
|
+
<tr>
|
|
232
|
+
<th>Field</th>
|
|
233
|
+
<th>Rule</th>
|
|
234
|
+
<th class="text-center">Required</th>
|
|
235
|
+
<th class="text-center">Cache</th>
|
|
236
|
+
</tr>
|
|
237
|
+
</thead>
|
|
238
|
+
<tbody>
|
|
239
|
+
${Object.entries(schema.content).map(([field, cfg]) => `
|
|
240
|
+
<tr>
|
|
241
|
+
<td>${field}</td>
|
|
242
|
+
<td>
|
|
243
|
+
<a href="#rule-${cfg.rule.split("--")[0]}">
|
|
244
|
+
${cfg.rule}
|
|
245
|
+
</a>
|
|
246
|
+
</td>
|
|
247
|
+
<td class="text-center">${cfg.required ? "✔" : "optional"}</td>
|
|
248
|
+
<td class="text-center">${cfg.cache === false ? "off" : "✔"}</td>
|
|
249
|
+
</tr>
|
|
250
|
+
`).join("")}
|
|
251
|
+
</tbody>
|
|
252
|
+
</table>
|
|
253
|
+
${OPTIONS ? `
|
|
254
|
+
<details class="schema-test">
|
|
255
|
+
<summary>Schema Test (Experimental)</summary>
|
|
256
|
+
<div class="schema-test-container">
|
|
257
|
+
<form id="schema:${schema.name}" data-form="schema--${schema.name}">
|
|
258
|
+
${Object.entries(schema.content).map(([field, cfg]) => {
|
|
259
|
+
const attributesHTML = cfg.attributesHTML ? Object.keys(cfg.attributesHTML).map((key) => `${key}="${cfg?.attributesHTML[key]}"`).join(' ') : null
|
|
260
|
+
return `
|
|
261
|
+
<div style="display: flex; flex-direction: column;">
|
|
262
|
+
<label for="${field}">${field} (${cfg.rule})</label>
|
|
263
|
+
<input name="${field}" ${cfg.required ? 'required': ''} ${attributesHTML}/>
|
|
264
|
+
</div>
|
|
265
|
+
`}).join("")}
|
|
266
|
+
<div style="display: flex; flex-direction: column;">
|
|
267
|
+
<label for="check-rule-mate-validators">VALIDATORS</label>
|
|
268
|
+
<input name="check-rule-mate-validators" placeholder="Add the validator object/function/class name - example: myValidator" required/>
|
|
269
|
+
</div>
|
|
270
|
+
<div style="display: flex; flex-direction: column;">
|
|
271
|
+
<label for="check-rule-mate-rules">RULES</label>
|
|
272
|
+
<input name="check-rule-mate-rules" placeholder="Add the rule file name - example: myRules" required/>
|
|
273
|
+
</div>
|
|
274
|
+
<button type="submit">Validate Schema</button>
|
|
275
|
+
</form>
|
|
276
|
+
<div class="schema-test-result">
|
|
277
|
+
<span>Result:</span>
|
|
278
|
+
<textarea id="textarea-schema-${schema.name}"></textarea>
|
|
279
|
+
</div>
|
|
280
|
+
</div>
|
|
281
|
+
</details>
|
|
282
|
+
` : ''}
|
|
283
|
+
</section>
|
|
284
|
+
`).join("");
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function renderRules(rulesFiles, usageMap) {
|
|
288
|
+
return rulesFiles.map(file =>
|
|
289
|
+
Object.entries(file.content).map(([ruleName, rule]) => `
|
|
290
|
+
<section id="rule-${ruleName}" class="card">
|
|
291
|
+
<h2>Rule: ${ruleName} (${file.name})</h2>
|
|
292
|
+
${rule?.docs?.description ?
|
|
293
|
+
`<p>${rule.docs.description}</p>`
|
|
294
|
+
: ''}
|
|
295
|
+
|
|
296
|
+
<h3>Validation Flow</h3>
|
|
297
|
+
<div class="flow">
|
|
298
|
+
${rule.validate.map(v => `
|
|
299
|
+
<div class="flow-step">${v}</div>
|
|
300
|
+
<div class="flow-arrow">→</div>
|
|
301
|
+
`).join("")}
|
|
302
|
+
<div class="flow-step success">valid</div>
|
|
303
|
+
</div>
|
|
304
|
+
|
|
305
|
+
<h3>Error Codes</h3>
|
|
306
|
+
${renderRulesErrors(rule.error)}
|
|
307
|
+
|
|
308
|
+
${rule.modifier ? `
|
|
309
|
+
<h3>Modifiers</h3>
|
|
310
|
+
${Object.entries(rule.modifier).map(([mod, modRule]) => `
|
|
311
|
+
<div class="modifier">
|
|
312
|
+
<span class="tag modifier">${mod}</span>
|
|
313
|
+
${modRule?.docs?.description ? `<p>${modRule.docs.description}</p>` : ''}
|
|
314
|
+
|
|
315
|
+
<div class="flow">
|
|
316
|
+
${modRule.validate.map(v => `
|
|
317
|
+
<div class="flow-step">${v}</div>
|
|
318
|
+
<div class="flow-arrow">→</div>
|
|
319
|
+
`).join("")}
|
|
320
|
+
<div class="flow-step success">valid</div>
|
|
321
|
+
</div>
|
|
322
|
+
|
|
323
|
+
<h4>Error Codes</h4>
|
|
324
|
+
${renderRulesErrors(modRule.error)}
|
|
325
|
+
</div>
|
|
326
|
+
`).join("")}
|
|
327
|
+
` : ""}
|
|
328
|
+
|
|
329
|
+
<h3>Used by Schemas</h3>
|
|
330
|
+
<ul>
|
|
331
|
+
${(usageMap[ruleName] || []).map(u =>
|
|
332
|
+
`<li>${u.schema} → <strong>${u.field}</strong> (${u.rule})</li>`
|
|
333
|
+
).join("") || "<li>Not used</li>"}
|
|
334
|
+
</ul>
|
|
335
|
+
|
|
336
|
+
${rule?.docs?.notes ?
|
|
337
|
+
`
|
|
338
|
+
<h3>Notes</h3>
|
|
339
|
+
<div class="rule-notes">${rule?.docs?.notes}</div>
|
|
340
|
+
`
|
|
341
|
+
: ''}
|
|
342
|
+
</section>
|
|
343
|
+
`).join("")
|
|
344
|
+
).join("");
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function renderRulesErrors(errors = {}) {
|
|
348
|
+
return `
|
|
349
|
+
<ul>
|
|
350
|
+
${Object.entries(errors).map(([k, v]) =>
|
|
351
|
+
`<li><a href="#error-${v.split('.')[0]}" class="tag error">${k}</a> ${v}</li>`
|
|
352
|
+
).join("")}
|
|
353
|
+
</ul>
|
|
354
|
+
`;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function renderErrors(errorFiles, usageMap) {
|
|
358
|
+
return errorFiles.map(file =>
|
|
359
|
+
Object.entries(file.content).map(([errorName, errors]) => `
|
|
360
|
+
<section id="error-${errorName}" class="card">
|
|
361
|
+
<h2>Error: ${errorName} (${file.name})</h2>
|
|
362
|
+
<ul>
|
|
363
|
+
${Object.entries(errors).map(([key, value]) => `
|
|
364
|
+
<li><span class="tag error">${key}</span> ${value} </li>
|
|
365
|
+
`).join('')}
|
|
366
|
+
</ul>
|
|
367
|
+
|
|
368
|
+
<h3>Used by Rules</h3>
|
|
369
|
+
<ul>
|
|
370
|
+
${Object.entries(errors).map(([key, value]) => `
|
|
371
|
+
${(usageMap[`${errorName}.${key}`] || []).map(u =>
|
|
372
|
+
`<li>${u.file} → <strong>${u.field}</strong> (${u.error})</li>`
|
|
373
|
+
).join("") || "<li>Not used</li>"}
|
|
374
|
+
`).join('')}
|
|
375
|
+
</ul>
|
|
376
|
+
</section>
|
|
377
|
+
`).join('')).join('');
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/* ---------------------------------------
|
|
381
|
+
* CSS
|
|
382
|
+
* -------------------------------------*/
|
|
383
|
+
function generateCSS() {
|
|
384
|
+
return `
|
|
385
|
+
body {
|
|
386
|
+
margin: 0;
|
|
387
|
+
font-family: Inter, system-ui, sans-serif;
|
|
388
|
+
display: grid;
|
|
389
|
+
grid-template-columns: 280px 1fr;
|
|
390
|
+
background: #0f172a;
|
|
391
|
+
color: #e5e7eb;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
.sidebar {
|
|
395
|
+
padding: 16px;
|
|
396
|
+
background: #020617;
|
|
397
|
+
overflow-y: auto;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
.sidebar input {
|
|
401
|
+
width: 100%;
|
|
402
|
+
padding: 8px;
|
|
403
|
+
border-radius: 6px;
|
|
404
|
+
border: none;
|
|
405
|
+
margin-bottom: 16px;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
.sidebar a {
|
|
409
|
+
display: block;
|
|
410
|
+
padding: 6px 10px;
|
|
411
|
+
color: #e5e7eb;
|
|
412
|
+
text-decoration: none;
|
|
413
|
+
border-radius: 6px;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
.sidebar a:hover {
|
|
417
|
+
background: rgba(56,189,248,0.2);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
a {
|
|
421
|
+
color: #e5e7eb;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
main {
|
|
425
|
+
padding: 32px;
|
|
426
|
+
overflow-y: auto;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
.card {
|
|
430
|
+
background: #020617;
|
|
431
|
+
border-radius: 12px;
|
|
432
|
+
padding: 24px;
|
|
433
|
+
margin-bottom: 32px;
|
|
434
|
+
border: 1px solid rgba(255,255,255,0.05);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
.flow {
|
|
438
|
+
display: flex;
|
|
439
|
+
align-items: center;
|
|
440
|
+
gap: 12px;
|
|
441
|
+
flex-wrap: wrap;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
.flow-step {
|
|
445
|
+
padding: 10px 14px;
|
|
446
|
+
border-radius: 8px;
|
|
447
|
+
background: rgba(56,189,248,0.15);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
.flow-arrow {
|
|
451
|
+
opacity: 0.5;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
.success {
|
|
455
|
+
background: rgba(34,197,94,0.2);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
.tag {
|
|
459
|
+
padding: 4px 8px;
|
|
460
|
+
border-radius: 999px;
|
|
461
|
+
font-size: 14px;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
.tag.error {
|
|
465
|
+
display: inline-block;
|
|
466
|
+
margin-bottom: 8px;
|
|
467
|
+
background: rgba(239,68,68,0.2);
|
|
468
|
+
text-decoration: none;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
.tag.modifier {
|
|
472
|
+
display: inline-block;
|
|
473
|
+
font-size: 16px;
|
|
474
|
+
margin-bottom: 8px;
|
|
475
|
+
font-weight: 600;
|
|
476
|
+
background: rgba(167,139,250,0.2);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
table {
|
|
480
|
+
width: 100%;
|
|
481
|
+
border-collapse: collapse;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
td, th {
|
|
485
|
+
padding: 8px;
|
|
486
|
+
border-bottom: 1px solid rgba(255,255,255,0.05);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
th {
|
|
490
|
+
text-align: left;
|
|
491
|
+
border-bottom: 1px solid #f4f4f4;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
.rule-notes {
|
|
495
|
+
padding: 16px;
|
|
496
|
+
color: black;
|
|
497
|
+
background: #f4f4f4;
|
|
498
|
+
border-radius: 12px;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
.text-center {
|
|
502
|
+
text-align: center;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
.schema-test {
|
|
506
|
+
margin-top: 32px;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
.schema-test-container {
|
|
510
|
+
display: flex;
|
|
511
|
+
width: 100%;
|
|
512
|
+
margin: 32px 0 24px 0;
|
|
513
|
+
gap: 40px;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
.schema-test-container > * {
|
|
517
|
+
width: 100%;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
.schema-test-result span {
|
|
521
|
+
display: block;
|
|
522
|
+
font-size: 20px;
|
|
523
|
+
font-weight: 600;
|
|
524
|
+
margin-bottom: 16px;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
.schema-test-result textarea {
|
|
528
|
+
width: 100%;
|
|
529
|
+
height: 80%;
|
|
530
|
+
color: #f4f4f4;
|
|
531
|
+
background-color: #111111;
|
|
532
|
+
border: none;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
button {
|
|
536
|
+
cursor: pointer;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
form button[type="submit"] {
|
|
540
|
+
width: 100%;
|
|
541
|
+
padding: 16px 24px;
|
|
542
|
+
min-width: 120px;
|
|
543
|
+
min-height: 48px;
|
|
544
|
+
margin-top: 16px;
|
|
545
|
+
font-size: 16px;
|
|
546
|
+
font-weight: 600;
|
|
547
|
+
color: #444444;
|
|
548
|
+
background-color: #eede91;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
label {
|
|
552
|
+
margin-bottom: 4px;
|
|
553
|
+
font-size: 16px;
|
|
554
|
+
color: #e5e7eb;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
.schema-test input {
|
|
558
|
+
min-height: 28px;
|
|
559
|
+
padding: 4px 8px 4px 8px;
|
|
560
|
+
margin-bottom: 16px;
|
|
561
|
+
font-size: 16px;
|
|
562
|
+
color: #e5e7eb;
|
|
563
|
+
background-color: transparent;
|
|
564
|
+
border: none;
|
|
565
|
+
border-bottom: 1px solid white;
|
|
566
|
+
}
|
|
567
|
+
`;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/* ---------------------------------------
|
|
571
|
+
* CLIENT JS
|
|
572
|
+
* -------------------------------------*/
|
|
573
|
+
function generateClientJS() {
|
|
574
|
+
return `
|
|
575
|
+
const search = document.getElementById("search");
|
|
576
|
+
search.addEventListener("input", e => {
|
|
577
|
+
const value = e.target.value.toLowerCase();
|
|
578
|
+
document.querySelectorAll("section.card").forEach(card => {
|
|
579
|
+
card.style.display = card.innerText.toLowerCase().includes(value)
|
|
580
|
+
? ""
|
|
581
|
+
: "none";
|
|
582
|
+
});
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
${OPTIONS ? `
|
|
586
|
+
setTimeout(() => {
|
|
587
|
+
const rulesFiles = ${JSON.stringify(rulesFiles)}
|
|
588
|
+
const schemasFiles = ${JSON.stringify(schemasFiles)}
|
|
589
|
+
|
|
590
|
+
const formElements = document.querySelectorAll('form[data-form]');
|
|
591
|
+
formElements.forEach((formElement) => {
|
|
592
|
+
formElement.addEventListener('submit', async (e) => {
|
|
593
|
+
e.preventDefault();
|
|
594
|
+
const inputElements = formElement.querySelectorAll('input');
|
|
595
|
+
const formName = formElement.dataset.form.split('--')[1];
|
|
596
|
+
const formData = {};
|
|
597
|
+
inputElements.forEach((inputElement) => {
|
|
598
|
+
formData[inputElement.name] = inputElement.value;
|
|
599
|
+
})
|
|
600
|
+
|
|
601
|
+
const validatorHelper = window.validatorHelpers[formData["check-rule-mate-validators"]][formData["check-rule-mate-validators"]];
|
|
602
|
+
|
|
603
|
+
const rules = rulesFiles.filter((rulesFile) => rulesFile.name === formData["check-rule-mate-rules"])[0].content;
|
|
604
|
+
const schema = schemasFiles.filter((schemaFile) => schemaFile.name === formName)[0].content;
|
|
605
|
+
|
|
606
|
+
const validator = createValidator(formData, {validationHelpers: validatorHelper, rules: rules, schema: schema, options: { propertiesMustMatch: false, abortEarly: false, cache: true}});
|
|
607
|
+
const result = await validator.validate();
|
|
608
|
+
console.log(formName, result);
|
|
609
|
+
|
|
610
|
+
const textAreaElement = document.querySelector('#textarea-schema-'+formName);
|
|
611
|
+
textAreaElement.value = JSON.stringify(result);
|
|
612
|
+
});
|
|
613
|
+
});
|
|
614
|
+
}, 1000);
|
|
615
|
+
`: ''}
|
|
616
|
+
`;
|
|
617
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// import fs from 'fs';
|
|
4
|
+
// import path from 'path';
|
|
5
|
+
// import process from 'process';
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const process = require('process');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* ---------------------------
|
|
13
|
+
* Utils
|
|
14
|
+
* ---------------------------
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
function readJSONFilesFromDir(dirPath) {
|
|
18
|
+
if (!fs.existsSync(dirPath)) {
|
|
19
|
+
throw new Error(`Directory not found: ${dirPath}`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const files = fs.readdirSync(dirPath).filter(f => f.endsWith('.json'));
|
|
23
|
+
|
|
24
|
+
const data = {};
|
|
25
|
+
for (const file of files) {
|
|
26
|
+
const fullPath = path.join(dirPath, file);
|
|
27
|
+
const content = JSON.parse(fs.readFileSync(fullPath, 'utf-8'));
|
|
28
|
+
data[file] = content;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return data;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function parseRuleName(ruleName) {
|
|
35
|
+
const [base, modifier] = ruleName.split('--');
|
|
36
|
+
return { base, modifier };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function error(msg) {
|
|
40
|
+
return { type: 'error', message: msg };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function warning(msg) {
|
|
44
|
+
return { type: 'warning', message: msg };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* ---------------------------
|
|
49
|
+
* Core Validators
|
|
50
|
+
* ---------------------------
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
function validateSchemas({ schemas, rules }) {
|
|
54
|
+
const issues = [];
|
|
55
|
+
|
|
56
|
+
for (const [schemaFile, schema] of Object.entries(schemas)) {
|
|
57
|
+
for (const [field, config] of Object.entries(schema)) {
|
|
58
|
+
if (!config.rule) continue;
|
|
59
|
+
|
|
60
|
+
const { base, modifier } = parseRuleName(config.rule);
|
|
61
|
+
|
|
62
|
+
if (!rules[base]) {
|
|
63
|
+
issues.push(
|
|
64
|
+
error(
|
|
65
|
+
`Schema "${schemaFile}" → field "${field}" references rule "${base}" which does not exist`
|
|
66
|
+
)
|
|
67
|
+
);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (modifier) {
|
|
72
|
+
if (!rules[base].modifier || !rules[base].modifier[modifier]) {
|
|
73
|
+
issues.push(
|
|
74
|
+
error(
|
|
75
|
+
`Schema "${schemaFile}" → field "${field}" references modifier "${modifier}" on rule "${base}" which does not exist`
|
|
76
|
+
)
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return issues;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function validateRuleErrors({ rules, errors }) {
|
|
87
|
+
const issues = [];
|
|
88
|
+
|
|
89
|
+
for (const [ruleName, rule] of Object.entries(rules)) {
|
|
90
|
+
const checkErrorMap = (errorMap, context) => {
|
|
91
|
+
if (!errorMap) return;
|
|
92
|
+
|
|
93
|
+
for (const errorKey of Object.values(errorMap)) {
|
|
94
|
+
const [namespace, key] = errorKey.split('.');
|
|
95
|
+
if (!errors[namespace] || !errors[namespace][key]) {
|
|
96
|
+
issues.push(
|
|
97
|
+
error(
|
|
98
|
+
`Rule "${ruleName}"${context} references error key "${errorKey}" which does not exist`
|
|
99
|
+
)
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
checkErrorMap(rule.error, '');
|
|
106
|
+
|
|
107
|
+
if (rule.modifier) {
|
|
108
|
+
for (const [modifierName, modifier] of Object.entries(rule.modifier)) {
|
|
109
|
+
checkErrorMap(modifier.error, ` (modifier "${modifierName}")`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return issues;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function findUnusedErrors({ rules, errors }) {
|
|
118
|
+
const issues = [];
|
|
119
|
+
const usedErrors = new Set();
|
|
120
|
+
|
|
121
|
+
const collectErrors = errorMap => {
|
|
122
|
+
if (!errorMap) return;
|
|
123
|
+
Object.values(errorMap).forEach(e => usedErrors.add(e));
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
for (const rule of Object.values(rules)) {
|
|
127
|
+
collectErrors(rule.error);
|
|
128
|
+
if (rule.modifier) {
|
|
129
|
+
Object.values(rule.modifier).forEach(m => collectErrors(m.error));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
for (const [namespace, keys] of Object.entries(errors)) {
|
|
134
|
+
for (const key of Object.keys(keys)) {
|
|
135
|
+
const fullKey = `${namespace}.${key}`;
|
|
136
|
+
if (!usedErrors.has(fullKey)) {
|
|
137
|
+
issues.push(
|
|
138
|
+
warning(`Unused error message detected: "${fullKey}"`)
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return issues;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* ---------------------------
|
|
149
|
+
* CLI Argument Parsing
|
|
150
|
+
* ---------------------------
|
|
151
|
+
*/
|
|
152
|
+
|
|
153
|
+
function parseArgs() {
|
|
154
|
+
const args = process.argv.slice(2);
|
|
155
|
+
const options = {};
|
|
156
|
+
|
|
157
|
+
for (let i = 0; i < args.length; i++) {
|
|
158
|
+
if (args[i].startsWith('--')) {
|
|
159
|
+
const key = args[i].replace('--', '');
|
|
160
|
+
options[key] = args[i + 1];
|
|
161
|
+
i++;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return options;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* ---------------------------
|
|
170
|
+
* Runner
|
|
171
|
+
* ---------------------------
|
|
172
|
+
*/
|
|
173
|
+
|
|
174
|
+
async function runVerify() {
|
|
175
|
+
try {
|
|
176
|
+
const args = parseArgs();
|
|
177
|
+
|
|
178
|
+
if (!args.schemas || !args.rules || !args.errors) {
|
|
179
|
+
console.error(
|
|
180
|
+
'Usage: check-rule-mate-verify-templates --schemas <path> --rules <path> --errors <path>'
|
|
181
|
+
);
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const schemas = readJSONFilesFromDir(args.schemas);
|
|
186
|
+
const rulesRaw = readJSONFilesFromDir(args.rules);
|
|
187
|
+
const errorsRaw = readJSONFilesFromDir(args.errors);
|
|
188
|
+
|
|
189
|
+
// Merge all rule files into one object
|
|
190
|
+
const rules = Object.assign({}, ...Object.values(rulesRaw));
|
|
191
|
+
const errors = Object.assign({}, ...Object.values(errorsRaw));
|
|
192
|
+
|
|
193
|
+
let issues = [];
|
|
194
|
+
|
|
195
|
+
issues.push(...validateSchemas({ schemas, rules }));
|
|
196
|
+
issues.push(...validateRuleErrors({ rules, errors }));
|
|
197
|
+
issues.push(...findUnusedErrors({ rules, errors }));
|
|
198
|
+
|
|
199
|
+
const hasErrors = issues.some(i => i.type === 'error');
|
|
200
|
+
|
|
201
|
+
console.log('✔ Schemas loaded:', Object.keys(schemas).length);
|
|
202
|
+
console.log('✔ Rules loaded:', Object.keys(rules).length);
|
|
203
|
+
console.log('✔ Errors loaded:', Object.keys(errors).length);
|
|
204
|
+
console.log('');
|
|
205
|
+
|
|
206
|
+
if (issues.length === 0) {
|
|
207
|
+
console.log('✔ No issues found');
|
|
208
|
+
process.exit(0);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
console.log(hasErrors ? '❌ Validation failed' : '⚠️ Validation warnings');
|
|
212
|
+
console.log('');
|
|
213
|
+
|
|
214
|
+
for (const issue of issues) {
|
|
215
|
+
const prefix = issue.type === 'error' ? '❌' : '⚠️';
|
|
216
|
+
console.log(`${prefix} ${issue.message}`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
process.exit(hasErrors ? 1 : 0);
|
|
220
|
+
} catch (err) {
|
|
221
|
+
console.error('Fatal error:', err.message);
|
|
222
|
+
process.exit(1);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
runVerify();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "check-rule-mate",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.5",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "./dist/main.cjs.js",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -18,7 +18,9 @@
|
|
|
18
18
|
"check rule mate"
|
|
19
19
|
],
|
|
20
20
|
"bin": {
|
|
21
|
-
"check-rule-mate-auto-docs": "./bin/generate-docs.js"
|
|
21
|
+
"check-rule-mate-auto-docs": "./bin/generate-docs.js",
|
|
22
|
+
"check-rule-mate-auto-docs-playground-experimental": "./bin/generate-docs-playground-experimental.js",
|
|
23
|
+
"check-rule-mate-verify-templates": "./bin/verify-templates.js"
|
|
22
24
|
},
|
|
23
25
|
"scripts": {
|
|
24
26
|
"start": "node ./examples/vanilla/src/index.js",
|
|
@@ -29,6 +31,9 @@
|
|
|
29
31
|
},
|
|
30
32
|
"author": "João Rocha",
|
|
31
33
|
"license": "ISC",
|
|
34
|
+
"optionalDependencies": {
|
|
35
|
+
"esbuild": "^0.27.2"
|
|
36
|
+
},
|
|
32
37
|
"devDependencies": {
|
|
33
38
|
"esbuild": "^0.27.2",
|
|
34
39
|
"express": "^4.21.2",
|