@schalkneethling/miyagi-core 4.1.1 → 4.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/api/index.js +98 -10
- package/lib/cli/lint.js +142 -94
- package/lib/constants/lint-log-levels.js +11 -0
- package/lib/default-config.js +5 -0
- package/lib/i18n/en.js +4 -0
- package/lib/init/config.js +9 -0
- package/lib/logger.js +37 -2
- package/lib/state/file-contents.js +46 -3
- package/lib/state/index.js +1 -0
- package/lib/validator/mocks.js +31 -26
- package/lib/validator/schemas.js +234 -0
- package/package.json +1 -1
package/api/index.js
CHANGED
|
@@ -7,6 +7,11 @@ import build from "../lib/build/index.js";
|
|
|
7
7
|
import generateMockData from "../lib/generator/mocks.js";
|
|
8
8
|
import generateComponent from "../lib/generator/component.js";
|
|
9
9
|
import validateMockData from "../lib/validator/mocks.js";
|
|
10
|
+
import {
|
|
11
|
+
getSchemaValidationMode,
|
|
12
|
+
toSchemaValidationResult,
|
|
13
|
+
validateSchemas,
|
|
14
|
+
} from "../lib/validator/schemas.js";
|
|
10
15
|
|
|
11
16
|
/**
|
|
12
17
|
* @param {object} obj
|
|
@@ -172,14 +177,44 @@ export const createComponent = async ({ component, only = [], skip = [] }) => {
|
|
|
172
177
|
|
|
173
178
|
export const lintComponents = async () => {
|
|
174
179
|
global.app = await init("api");
|
|
175
|
-
const
|
|
180
|
+
const mode = getSchemaValidationMode();
|
|
181
|
+
const components = global.state.routes.filter((route) => route.paths.tpl);
|
|
182
|
+
const schemaValidation = validateSchemas({
|
|
183
|
+
components,
|
|
184
|
+
});
|
|
185
|
+
const schemaErrorsByComponent = new Map();
|
|
186
|
+
|
|
187
|
+
schemaValidation.errors.forEach((entry) => {
|
|
188
|
+
if (!schemaErrorsByComponent.has(entry.component)) {
|
|
189
|
+
schemaErrorsByComponent.set(entry.component, []);
|
|
190
|
+
}
|
|
191
|
+
schemaErrorsByComponent
|
|
192
|
+
.get(entry.component)
|
|
193
|
+
.push(toSchemaValidationResult(entry));
|
|
194
|
+
});
|
|
176
195
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
196
|
+
if (mode === "fail-fast" && schemaValidation.errors.length > 0) {
|
|
197
|
+
return {
|
|
198
|
+
success: false,
|
|
199
|
+
data: getLintComponentErrorsInRouteOrder({
|
|
200
|
+
components,
|
|
201
|
+
errorMap: schemaErrorsByComponent,
|
|
202
|
+
}),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const promises = components
|
|
207
|
+
.filter((route) => !schemaErrorsByComponent.has(route.paths.dir.short))
|
|
208
|
+
.map(
|
|
209
|
+
(route) =>
|
|
180
210
|
new Promise((resolve) => {
|
|
181
211
|
getComponentData(route).then((data) => {
|
|
182
|
-
const validation = validateMockData(
|
|
212
|
+
const validation = validateMockData(
|
|
213
|
+
route,
|
|
214
|
+
data || [],
|
|
215
|
+
true,
|
|
216
|
+
schemaValidation.validSchemas,
|
|
217
|
+
);
|
|
183
218
|
|
|
184
219
|
resolve({
|
|
185
220
|
component: route.alias,
|
|
@@ -187,13 +222,25 @@ export const lintComponents = async () => {
|
|
|
187
222
|
});
|
|
188
223
|
});
|
|
189
224
|
}),
|
|
190
|
-
|
|
191
|
-
}
|
|
192
|
-
});
|
|
225
|
+
);
|
|
193
226
|
|
|
194
227
|
return await Promise.all(promises)
|
|
195
228
|
.then((res) => {
|
|
196
|
-
|
|
229
|
+
res.forEach((result) => {
|
|
230
|
+
if (!result?.errors?.length) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
const componentErrors = schemaErrorsByComponent.get(result.component) || [];
|
|
234
|
+
schemaErrorsByComponent.set(result.component, [
|
|
235
|
+
...componentErrors,
|
|
236
|
+
...result.errors,
|
|
237
|
+
]);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const errors = getLintComponentErrorsInRouteOrder({
|
|
241
|
+
components,
|
|
242
|
+
errorMap: schemaErrorsByComponent,
|
|
243
|
+
});
|
|
197
244
|
|
|
198
245
|
return {
|
|
199
246
|
success: errors.length === 0,
|
|
@@ -216,8 +263,26 @@ export const lintComponent = async ({ component }) => {
|
|
|
216
263
|
message: `The component ${component} does not seem to exist.`,
|
|
217
264
|
};
|
|
218
265
|
|
|
266
|
+
const allSchemaValidation = validateSchemas({
|
|
267
|
+
components: [componentObject],
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
if (allSchemaValidation.errors.length > 0) {
|
|
271
|
+
return {
|
|
272
|
+
success: false,
|
|
273
|
+
data: allSchemaValidation.errors.map((entry) =>
|
|
274
|
+
toSchemaValidationResult(entry),
|
|
275
|
+
),
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
219
279
|
const data = await getComponentData(componentObject);
|
|
220
|
-
const errors = validateMockData(
|
|
280
|
+
const errors = validateMockData(
|
|
281
|
+
componentObject,
|
|
282
|
+
data || [],
|
|
283
|
+
true,
|
|
284
|
+
allSchemaValidation.validSchemas,
|
|
285
|
+
);
|
|
221
286
|
|
|
222
287
|
return {
|
|
223
288
|
success: errors === null || errors?.length === 0,
|
|
@@ -234,3 +299,26 @@ function getComponentsObject(component) {
|
|
|
234
299
|
(route) => route.paths.dir.short === component,
|
|
235
300
|
);
|
|
236
301
|
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* @param {object} params
|
|
305
|
+
* @param {Array<object>} params.components
|
|
306
|
+
* @param {Map<string, Array<object>>} params.errorMap
|
|
307
|
+
* @returns {Array<object>}
|
|
308
|
+
*/
|
|
309
|
+
function getLintComponentErrorsInRouteOrder({ components, errorMap }) {
|
|
310
|
+
return components
|
|
311
|
+
.map((route) => {
|
|
312
|
+
const componentErrors = errorMap.get(route.alias) || [];
|
|
313
|
+
|
|
314
|
+
if (componentErrors.length === 0) {
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
component: route.alias,
|
|
320
|
+
errors: componentErrors,
|
|
321
|
+
};
|
|
322
|
+
})
|
|
323
|
+
.filter(Boolean);
|
|
324
|
+
}
|
package/lib/cli/lint.js
CHANGED
|
@@ -4,6 +4,11 @@ import getConfig from "../config.js";
|
|
|
4
4
|
import log from "../logger.js";
|
|
5
5
|
import { getComponentData } from "../mocks/index.js";
|
|
6
6
|
import validateMockData from "../validator/mocks.js";
|
|
7
|
+
import {
|
|
8
|
+
getSchemaValidationMode,
|
|
9
|
+
toSchemaValidationResult,
|
|
10
|
+
validateSchemas,
|
|
11
|
+
} from "../validator/schemas.js";
|
|
7
12
|
import { t } from "../i18n/index.js";
|
|
8
13
|
|
|
9
14
|
/**
|
|
@@ -14,6 +19,8 @@ export default async function lint(args) {
|
|
|
14
19
|
|
|
15
20
|
const componentArg = args._.slice(1)[0];
|
|
16
21
|
const config = await getConfig(args);
|
|
22
|
+
process.env.MIYAGI_LOG_CONTEXT = "lint";
|
|
23
|
+
process.env.MIYAGI_LOG_LEVEL = config.lint?.logLevel || "error";
|
|
17
24
|
global.app = await init(config);
|
|
18
25
|
|
|
19
26
|
if (componentArg) {
|
|
@@ -23,8 +30,20 @@ export default async function lint(args) {
|
|
|
23
30
|
);
|
|
24
31
|
|
|
25
32
|
if (component) {
|
|
33
|
+
const schemaValidation = validateSchemas({
|
|
34
|
+
components: [component],
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
if (schemaValidation.errors.length > 0) {
|
|
38
|
+
reportSchemaErrors(schemaValidation.errors);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
log("success", "All schemas valid.");
|
|
43
|
+
|
|
26
44
|
await validateComponentMockData({
|
|
27
45
|
component,
|
|
46
|
+
validSchemas: schemaValidation.validSchemas,
|
|
28
47
|
});
|
|
29
48
|
} else {
|
|
30
49
|
log("error", `The component ${componentArg} does not seem to exist.`);
|
|
@@ -40,81 +59,90 @@ export default async function lint(args) {
|
|
|
40
59
|
*/
|
|
41
60
|
async function validateAllMockData(exitProcess = true) {
|
|
42
61
|
log("info", t("linter.all.start"));
|
|
62
|
+
const mode = getSchemaValidationMode();
|
|
63
|
+
const components = global.state.routes.filter(
|
|
64
|
+
(route) => route.type === "components" && route.paths.tpl,
|
|
65
|
+
);
|
|
66
|
+
const schemaValidation = validateSchemas({
|
|
67
|
+
components,
|
|
68
|
+
});
|
|
69
|
+
const invalidSchemaComponents = new Set(
|
|
70
|
+
schemaValidation.errors.map((entry) => entry.component),
|
|
71
|
+
);
|
|
43
72
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
);
|
|
73
|
+
if (schemaValidation.errors.length === 0) {
|
|
74
|
+
log("success", "All schemas valid.");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (schemaValidation.errors.length > 0 && mode === "fail-fast") {
|
|
78
|
+
reportSchemaErrors(schemaValidation.errors);
|
|
79
|
+
log(
|
|
80
|
+
"error",
|
|
81
|
+
schemaValidation.errors.length === 1
|
|
82
|
+
? t("linter.all.schema.invalid.one")
|
|
83
|
+
: t("linter.all.schema.invalid.other").replace(
|
|
84
|
+
"{{amount}}",
|
|
85
|
+
schemaValidation.errors.length,
|
|
86
|
+
),
|
|
87
|
+
);
|
|
88
|
+
if (exitProcess) {
|
|
89
|
+
process.exit(1);
|
|
62
90
|
}
|
|
63
|
-
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
64
93
|
|
|
65
|
-
Promise.all(
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
94
|
+
const results = await Promise.all(
|
|
95
|
+
components
|
|
96
|
+
.filter((route) => !invalidSchemaComponents.has(route.paths.dir.short))
|
|
97
|
+
.map((component) =>
|
|
98
|
+
validateComponentMockData({
|
|
99
|
+
component,
|
|
100
|
+
silent: true,
|
|
101
|
+
exitProcess: false,
|
|
102
|
+
validSchemas: schemaValidation.validSchemas,
|
|
103
|
+
}),
|
|
104
|
+
),
|
|
105
|
+
);
|
|
106
|
+
const mockInvalidResults = results.filter(
|
|
107
|
+
(result) => result?.valid === false && result.type === "mocks",
|
|
108
|
+
);
|
|
73
109
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
110
|
+
if (mode === "collect-all" && schemaValidation.errors.length > 0) {
|
|
111
|
+
reportSchemaErrors(schemaValidation.errors);
|
|
112
|
+
log(
|
|
113
|
+
"error",
|
|
114
|
+
schemaValidation.errors.length === 1
|
|
115
|
+
? t("linter.all.schema.invalid.one")
|
|
116
|
+
: t("linter.all.schema.invalid.other").replace(
|
|
117
|
+
"{{amount}}",
|
|
118
|
+
schemaValidation.errors.length,
|
|
119
|
+
),
|
|
120
|
+
);
|
|
121
|
+
}
|
|
83
122
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
123
|
+
if (mockInvalidResults.length > 0) {
|
|
124
|
+
log(
|
|
125
|
+
"error",
|
|
126
|
+
mockInvalidResults.length === 1
|
|
127
|
+
? t("linter.all.mocks.invalid.one")
|
|
128
|
+
: t("linter.all.mocks.invalid.other").replace(
|
|
129
|
+
"{{amount}}",
|
|
130
|
+
mockInvalidResults.length,
|
|
131
|
+
),
|
|
132
|
+
);
|
|
133
|
+
}
|
|
95
134
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
mockInvalidResults.length,
|
|
104
|
-
),
|
|
105
|
-
);
|
|
106
|
-
}
|
|
135
|
+
if (mockInvalidResults.length === 0 && schemaValidation.errors.length === 0) {
|
|
136
|
+
log("success", t("linter.all.valid"));
|
|
137
|
+
if (exitProcess) {
|
|
138
|
+
process.exit(0);
|
|
139
|
+
}
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
107
142
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
})
|
|
112
|
-
.catch((err) => {
|
|
113
|
-
console.error(err);
|
|
114
|
-
if (exitProcess) {
|
|
115
|
-
process.exit(1);
|
|
116
|
-
}
|
|
117
|
-
});
|
|
143
|
+
if (exitProcess) {
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
118
146
|
}
|
|
119
147
|
|
|
120
148
|
/**
|
|
@@ -122,12 +150,14 @@ async function validateAllMockData(exitProcess = true) {
|
|
|
122
150
|
* @param {object} obj.component
|
|
123
151
|
* @param {boolean} [obj.silent]
|
|
124
152
|
* @param {boolean} [obj.exitProcess]
|
|
153
|
+
* @param {Array<object>} [obj.validSchemas]
|
|
125
154
|
* @returns {Promise<object|null>}
|
|
126
155
|
*/
|
|
127
156
|
async function validateComponentMockData({
|
|
128
157
|
component,
|
|
129
158
|
silent,
|
|
130
159
|
exitProcess = true,
|
|
160
|
+
validSchemas = [],
|
|
131
161
|
}) {
|
|
132
162
|
if (!silent) {
|
|
133
163
|
log(
|
|
@@ -139,42 +169,60 @@ async function validateComponentMockData({
|
|
|
139
169
|
);
|
|
140
170
|
}
|
|
141
171
|
|
|
142
|
-
const data = await getComponentData(component);
|
|
172
|
+
const data = (await getComponentData(component)) || [];
|
|
143
173
|
|
|
144
|
-
if (data) {
|
|
145
|
-
for (const { messages } of data) {
|
|
174
|
+
if (data.length > 0) {
|
|
175
|
+
for (const { messages = [] } of data) {
|
|
146
176
|
for (const { type, text, verbose } of messages) {
|
|
147
177
|
log(type, text, verbose);
|
|
148
178
|
}
|
|
149
179
|
}
|
|
180
|
+
}
|
|
150
181
|
|
|
151
|
-
|
|
182
|
+
const results = validateMockData(component, data, false, validSchemas);
|
|
152
183
|
|
|
153
|
-
|
|
184
|
+
if (!results) return null;
|
|
154
185
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
186
|
+
if (results.length === 0) {
|
|
187
|
+
if (!silent) {
|
|
188
|
+
log("success", t("linter.component.valid"));
|
|
189
|
+
}
|
|
159
190
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
} else {
|
|
163
|
-
return {
|
|
164
|
-
valid: true,
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
} else {
|
|
168
|
-
if (exitProcess) {
|
|
169
|
-
process.exit(0);
|
|
170
|
-
} else {
|
|
171
|
-
return {
|
|
172
|
-
valid: false,
|
|
173
|
-
type: results[0].type,
|
|
174
|
-
};
|
|
175
|
-
}
|
|
191
|
+
if (exitProcess) {
|
|
192
|
+
process.exit(0);
|
|
176
193
|
}
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
valid: true,
|
|
197
|
+
};
|
|
177
198
|
}
|
|
178
199
|
|
|
179
|
-
|
|
200
|
+
if (exitProcess) {
|
|
201
|
+
process.exit(1);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
valid: false,
|
|
206
|
+
type: results[0].type,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* @param {Array<object>} schemaErrors
|
|
212
|
+
*/
|
|
213
|
+
function reportSchemaErrors(schemaErrors) {
|
|
214
|
+
schemaErrors.forEach((entry) => {
|
|
215
|
+
const result = toSchemaValidationResult(entry);
|
|
216
|
+
log("error", `${entry.component}:\n${result.data[0].message}`);
|
|
217
|
+
log("error", `schema: ${entry.schemaFile}`);
|
|
218
|
+
if (entry.schemaPath || entry.instancePath) {
|
|
219
|
+
log(
|
|
220
|
+
"error",
|
|
221
|
+
`schemaPath: ${entry.schemaPath || "-"} | instancePath: ${entry.instancePath || "-"}`,
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
if (entry.hint) {
|
|
225
|
+
log("warn", entry.hint);
|
|
226
|
+
}
|
|
227
|
+
});
|
|
180
228
|
}
|
package/lib/default-config.js
CHANGED
|
@@ -46,6 +46,9 @@ export default {
|
|
|
46
46
|
render: null,
|
|
47
47
|
options: {},
|
|
48
48
|
},
|
|
49
|
+
lint: {
|
|
50
|
+
logLevel: "error",
|
|
51
|
+
},
|
|
49
52
|
extensions: [],
|
|
50
53
|
files: {
|
|
51
54
|
css: {
|
|
@@ -96,7 +99,9 @@ export default {
|
|
|
96
99
|
},
|
|
97
100
|
schema: {
|
|
98
101
|
ajv: AJV,
|
|
102
|
+
verbose: false,
|
|
99
103
|
},
|
|
104
|
+
schemaValidationMode: "collect-all",
|
|
100
105
|
},
|
|
101
106
|
projectName: "miyagi",
|
|
102
107
|
defaultPort: 5000,
|
package/lib/i18n/en.js
CHANGED
|
@@ -70,6 +70,10 @@ export default {
|
|
|
70
70
|
invalid: "Mock data does not match schema file.",
|
|
71
71
|
noSchemaFound:
|
|
72
72
|
"No schema file found or the schema file could not be parsed as valid JSON.",
|
|
73
|
+
schemaMissing:
|
|
74
|
+
"Component {{component}} has no schema file (expected: {{schemaFile}}). Consider adding it to components.ignores if this is expected.",
|
|
75
|
+
schemaParseFailed:
|
|
76
|
+
"Schema file {{schemaFile}} could not be parsed as {{format}}.",
|
|
73
77
|
},
|
|
74
78
|
},
|
|
75
79
|
serverStarted: "Running miyagi server at http://localhost:{{port}}",
|
package/lib/init/config.js
CHANGED
|
@@ -7,6 +7,7 @@ import deepMerge from "deepmerge";
|
|
|
7
7
|
import log from "../logger.js";
|
|
8
8
|
import appConfig from "../default-config.js";
|
|
9
9
|
import { t, available as langAvailable } from "../i18n/index.js";
|
|
10
|
+
import { LINT_LOG_LEVELS } from "../constants/lint-log-levels.js";
|
|
10
11
|
import fs from "fs";
|
|
11
12
|
import path from "path";
|
|
12
13
|
|
|
@@ -326,6 +327,14 @@ export default (userConfig = {}) => {
|
|
|
326
327
|
merged.ui.lang = "en";
|
|
327
328
|
}
|
|
328
329
|
|
|
330
|
+
if (!Object.values(LINT_LOG_LEVELS).includes(merged.lint.logLevel)) {
|
|
331
|
+
log(
|
|
332
|
+
"warn",
|
|
333
|
+
`Invalid config.lint.logLevel "${merged.lint.logLevel}". Falling back to "${defaultUserConfig.lint.logLevel}".`,
|
|
334
|
+
);
|
|
335
|
+
merged.lint.logLevel = defaultUserConfig.lint.logLevel;
|
|
336
|
+
}
|
|
337
|
+
|
|
329
338
|
return merged;
|
|
330
339
|
};
|
|
331
340
|
|
package/lib/logger.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import {
|
|
2
|
+
LINT_LOG_LEVEL_ORDER,
|
|
3
|
+
LINT_LOG_LEVELS,
|
|
4
|
+
} from "./constants/lint-log-levels.js";
|
|
5
|
+
|
|
1
6
|
const COLORS = {
|
|
2
7
|
grey: "\x1b[90m",
|
|
3
8
|
red: "\x1b[31m",
|
|
@@ -21,8 +26,17 @@ const TYPES = {
|
|
|
21
26
|
* @param {string|Error} [verboseMessage]
|
|
22
27
|
*/
|
|
23
28
|
export default function log(type, message, verboseMessage) {
|
|
24
|
-
if (process.env.MIYAGI_JS_API)
|
|
25
|
-
|
|
29
|
+
if (process.env.MIYAGI_JS_API) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!(type in TYPES)) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!shouldLogType(type)) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
26
40
|
|
|
27
41
|
const date = new Date();
|
|
28
42
|
const year = date.getFullYear();
|
|
@@ -59,6 +73,27 @@ export default function log(type, message, verboseMessage) {
|
|
|
59
73
|
}
|
|
60
74
|
}
|
|
61
75
|
|
|
76
|
+
/**
|
|
77
|
+
* @param {string} type
|
|
78
|
+
* @returns {boolean}
|
|
79
|
+
*/
|
|
80
|
+
function shouldLogType(type) {
|
|
81
|
+
if (process.env.MIYAGI_LOG_CONTEXT !== "lint") {
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const configuredLevel = process.env.MIYAGI_LOG_LEVEL || "error";
|
|
86
|
+
const normalizedType = type === "success" ? "info" : type;
|
|
87
|
+
const configuredLevelValue =
|
|
88
|
+
LINT_LOG_LEVEL_ORDER[configuredLevel] ??
|
|
89
|
+
LINT_LOG_LEVEL_ORDER[LINT_LOG_LEVELS.ERROR];
|
|
90
|
+
const typeLevelValue =
|
|
91
|
+
LINT_LOG_LEVEL_ORDER[normalizedType] ??
|
|
92
|
+
LINT_LOG_LEVEL_ORDER[LINT_LOG_LEVELS.INFO];
|
|
93
|
+
|
|
94
|
+
return typeLevelValue <= configuredLevelValue;
|
|
95
|
+
}
|
|
96
|
+
|
|
62
97
|
/**
|
|
63
98
|
* @param {string} color
|
|
64
99
|
* @param {string} str
|
|
@@ -11,6 +11,31 @@ import { marked as Markdown } from "marked";
|
|
|
11
11
|
import * as helpers from "../helpers.js";
|
|
12
12
|
import log from "../logger.js";
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* @param {string} fileName
|
|
16
|
+
* @returns {boolean}
|
|
17
|
+
*/
|
|
18
|
+
function isSchemaFile(fileName) {
|
|
19
|
+
const schemaFileName = `${global.config.files.schema.name}.${global.config.files.schema.extension}`;
|
|
20
|
+
return path.basename(fileName) === schemaFileName;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {string} fileName
|
|
25
|
+
* @param {Error & { code?: string }} err
|
|
26
|
+
*/
|
|
27
|
+
function markFileReadError(fileName, err) {
|
|
28
|
+
if (!global.state?.fileReadErrors || !isSchemaFile(fileName)) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
global.state.fileReadErrors[fileName] = {
|
|
33
|
+
type: "schema-parse",
|
|
34
|
+
code: err?.code || null,
|
|
35
|
+
message: err?.message || String(err),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
14
39
|
/**
|
|
15
40
|
* Checks if a given array of file paths includes a given file path
|
|
16
41
|
* @param {string} file - file path string
|
|
@@ -134,8 +159,13 @@ export const readFile = async function (fileName) {
|
|
|
134
159
|
case [".yaml", ".yml"].includes(path.extname(fileName)):
|
|
135
160
|
{
|
|
136
161
|
try {
|
|
137
|
-
|
|
162
|
+
const content = await getYamlFileContent(fileName);
|
|
163
|
+
if (global.state?.fileReadErrors) {
|
|
164
|
+
delete global.state.fileReadErrors[fileName];
|
|
165
|
+
}
|
|
166
|
+
return content;
|
|
138
167
|
} catch (err) {
|
|
168
|
+
markFileReadError(fileName, err);
|
|
139
169
|
log("error", `Error when reading file ${fileName}`, err);
|
|
140
170
|
}
|
|
141
171
|
}
|
|
@@ -153,8 +183,13 @@ export const readFile = async function (fileName) {
|
|
|
153
183
|
[".js", ".mjs"].includes(path.extname(fileName)):
|
|
154
184
|
{
|
|
155
185
|
try {
|
|
156
|
-
|
|
186
|
+
const content = await getJsFileContent(fileName);
|
|
187
|
+
if (global.state?.fileReadErrors) {
|
|
188
|
+
delete global.state.fileReadErrors[fileName];
|
|
189
|
+
}
|
|
190
|
+
return content;
|
|
157
191
|
} catch (err) {
|
|
192
|
+
markFileReadError(fileName, err);
|
|
158
193
|
log("error", `Error when reading file ${fileName}`, err);
|
|
159
194
|
}
|
|
160
195
|
}
|
|
@@ -162,8 +197,13 @@ export const readFile = async function (fileName) {
|
|
|
162
197
|
case fileName.endsWith(".json"):
|
|
163
198
|
{
|
|
164
199
|
try {
|
|
165
|
-
|
|
200
|
+
const content = await getParsedJsonFileContent(fileName);
|
|
201
|
+
if (global.state?.fileReadErrors) {
|
|
202
|
+
delete global.state.fileReadErrors[fileName];
|
|
203
|
+
}
|
|
204
|
+
return content;
|
|
166
205
|
} catch (err) {
|
|
206
|
+
markFileReadError(fileName, err);
|
|
167
207
|
log("error", `Error when reading file ${fileName}`, err);
|
|
168
208
|
}
|
|
169
209
|
}
|
|
@@ -187,6 +227,9 @@ export const readFile = async function (fileName) {
|
|
|
187
227
|
*/
|
|
188
228
|
export const getFileContents = async function (sourceTree) {
|
|
189
229
|
const fileContents = {};
|
|
230
|
+
if (global.state) {
|
|
231
|
+
global.state.fileReadErrors = {};
|
|
232
|
+
}
|
|
190
233
|
const paths = await getFilePaths(sourceTree);
|
|
191
234
|
|
|
192
235
|
if (paths) {
|
package/lib/state/index.js
CHANGED
package/lib/validator/mocks.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import jsYaml from "js-yaml";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
2
3
|
import deepMerge from "deepmerge";
|
|
3
4
|
import log from "../logger.js";
|
|
4
5
|
import { t } from "../i18n/index.js";
|
|
@@ -9,31 +10,19 @@ import { t } from "../i18n/index.js";
|
|
|
9
10
|
* @param {object} component
|
|
10
11
|
* @param {Array} dataArray - an array with mock data
|
|
11
12
|
* @param {boolean} [noCli]
|
|
13
|
+
* @param {Array<object>} [validSchemas]
|
|
12
14
|
* @returns {null|object[]} null if there is no schema or an array with booleans defining the validity of the entries in the data array
|
|
13
15
|
*/
|
|
14
|
-
export default function validateMockData(
|
|
16
|
+
export default function validateMockData(
|
|
17
|
+
component,
|
|
18
|
+
dataArray,
|
|
19
|
+
noCli,
|
|
20
|
+
validSchemas = [],
|
|
21
|
+
) {
|
|
15
22
|
const componentSchema =
|
|
16
23
|
global.state.fileContents[component.paths.schema.full];
|
|
17
24
|
|
|
18
25
|
if (componentSchema) {
|
|
19
|
-
const schemas = [];
|
|
20
|
-
|
|
21
|
-
Object.entries(global.state.fileContents).forEach(([key, value]) => {
|
|
22
|
-
if (
|
|
23
|
-
key.endsWith(
|
|
24
|
-
`${global.config.files.schema.name}.${global.config.files.schema.extension}`,
|
|
25
|
-
)
|
|
26
|
-
) {
|
|
27
|
-
const arr = Array.isArray(value) ? value : [value];
|
|
28
|
-
|
|
29
|
-
arr.forEach((schema) => {
|
|
30
|
-
if (schema && componentSchema.$id !== schema.$id) {
|
|
31
|
-
schemas.push(schema);
|
|
32
|
-
}
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
});
|
|
36
|
-
|
|
37
26
|
const validity = [];
|
|
38
27
|
let validate;
|
|
39
28
|
let jsonSchemaValidator;
|
|
@@ -43,16 +32,19 @@ export default function validateMockData(component, dataArray, noCli) {
|
|
|
43
32
|
deepMerge(
|
|
44
33
|
{
|
|
45
34
|
allErrors: true,
|
|
46
|
-
schemas: schemas.map((schema, i) => {
|
|
47
|
-
if (!schema.$id) {
|
|
48
|
-
schema.$id = i.toString();
|
|
49
|
-
}
|
|
50
|
-
return schema;
|
|
51
|
-
}),
|
|
52
35
|
},
|
|
53
36
|
global.config.schema.options || {},
|
|
54
37
|
),
|
|
55
38
|
);
|
|
39
|
+
|
|
40
|
+
validSchemas.forEach((entry) => {
|
|
41
|
+
// Preload only other validated schemas for cross-component $ref resolution.
|
|
42
|
+
// The current component schema is compiled below and must not be added twice.
|
|
43
|
+
if (entry?.schemaFile !== component.paths.schema.full && entry?.schema) {
|
|
44
|
+
jsonSchemaValidator.addSchema(entry.schema);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
56
48
|
validate = jsonSchemaValidator.compile(componentSchema);
|
|
57
49
|
} catch (e) {
|
|
58
50
|
const message = e.toString();
|
|
@@ -95,9 +87,22 @@ export default function validateMockData(component, dataArray, noCli) {
|
|
|
95
87
|
}
|
|
96
88
|
|
|
97
89
|
if (!global.config.isBuild && !noCli) {
|
|
90
|
+
const parseError = global.state.fileReadErrors?.[component.paths.schema.full];
|
|
91
|
+
const schemaExistsOnDisk = existsSync(component.paths.schema.full);
|
|
92
|
+
const warningMessage = parseError
|
|
93
|
+
? t("validator.mocks.schemaParseFailed")
|
|
94
|
+
.replace("{{schemaFile}}", component.paths.schema.short)
|
|
95
|
+
.replace("{{format}}", "JSON or YAML")
|
|
96
|
+
: schemaExistsOnDisk
|
|
97
|
+
? t("validator.mocks.schemaParseFailed")
|
|
98
|
+
.replace("{{schemaFile}}", component.paths.schema.short)
|
|
99
|
+
.replace("{{format}}", "JSON or YAML")
|
|
100
|
+
: t("validator.mocks.schemaMissing")
|
|
101
|
+
.replace("{{component}}", component.paths.dir.short)
|
|
102
|
+
.replace("{{schemaFile}}", component.paths.schema.short);
|
|
98
103
|
log(
|
|
99
104
|
"warn",
|
|
100
|
-
|
|
105
|
+
warningMessage,
|
|
101
106
|
);
|
|
102
107
|
}
|
|
103
108
|
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import deepMerge from "deepmerge";
|
|
4
|
+
|
|
5
|
+
const DEFAULT_SCHEMA_VALIDATION_MODE = "collect-all";
|
|
6
|
+
const ALLOWED_SCHEMA_VALIDATION_MODES = new Set(["collect-all", "fail-fast"]);
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @returns {"collect-all"|"fail-fast"}
|
|
10
|
+
*/
|
|
11
|
+
export function getSchemaValidationMode() {
|
|
12
|
+
const mode = global.config.schemaValidationMode ?? DEFAULT_SCHEMA_VALIDATION_MODE;
|
|
13
|
+
|
|
14
|
+
if (!ALLOWED_SCHEMA_VALIDATION_MODES.has(mode)) {
|
|
15
|
+
return DEFAULT_SCHEMA_VALIDATION_MODE;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return mode;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {object} options
|
|
23
|
+
* @param {Array<object>} [options.components]
|
|
24
|
+
* @returns {{ valid: boolean, errors: Array<object>, validSchemas: Array<object> }}
|
|
25
|
+
*/
|
|
26
|
+
export function validateSchemas({ components } = {}) {
|
|
27
|
+
const validSchemas = [];
|
|
28
|
+
const errors = [];
|
|
29
|
+
const componentRoutes =
|
|
30
|
+
components ??
|
|
31
|
+
global.state.routes.filter((route) => route.type === "components" && route.paths.tpl);
|
|
32
|
+
const validator = new global.config.schema.ajv(
|
|
33
|
+
deepMerge(
|
|
34
|
+
{
|
|
35
|
+
allErrors: true,
|
|
36
|
+
},
|
|
37
|
+
global.config.schema.options || {},
|
|
38
|
+
),
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
let pendingSchemas = componentRoutes
|
|
42
|
+
.map((component, index) => {
|
|
43
|
+
// Absolute schema file path.
|
|
44
|
+
const schemaFile = component.paths.schema.full;
|
|
45
|
+
// Parsed schema from in-memory state cache.
|
|
46
|
+
const schemaFromState = global.state.fileContents[schemaFile];
|
|
47
|
+
|
|
48
|
+
if (!schemaFromState) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const schema = structuredClone(schemaFromState);
|
|
53
|
+
if (!schema.$id) {
|
|
54
|
+
schema.$id = component.paths.schema.short || schemaFile || index.toString();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
component: component.paths.dir.short,
|
|
59
|
+
schemaFile,
|
|
60
|
+
rawSchema: schemaFromState,
|
|
61
|
+
schema,
|
|
62
|
+
};
|
|
63
|
+
})
|
|
64
|
+
.filter(Boolean);
|
|
65
|
+
|
|
66
|
+
while (pendingSchemas.length > 0) {
|
|
67
|
+
let progress = false;
|
|
68
|
+
const retrySchemas = [];
|
|
69
|
+
|
|
70
|
+
pendingSchemas.forEach((entry) => {
|
|
71
|
+
try {
|
|
72
|
+
validator.compile(entry.schema);
|
|
73
|
+
if (!validator.getSchema(entry.schema.$id)) {
|
|
74
|
+
validator.addSchema(entry.schema);
|
|
75
|
+
}
|
|
76
|
+
validSchemas.push({
|
|
77
|
+
component: entry.component,
|
|
78
|
+
schemaFile: entry.schemaFile,
|
|
79
|
+
schema: entry.schema,
|
|
80
|
+
});
|
|
81
|
+
progress = true;
|
|
82
|
+
} catch (error) {
|
|
83
|
+
if (isUnresolvedRefError(error)) {
|
|
84
|
+
retrySchemas.push(entry);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
errors.push(
|
|
89
|
+
buildSchemaValidationError({
|
|
90
|
+
error,
|
|
91
|
+
component: entry.component,
|
|
92
|
+
schemaFile: entry.schemaFile,
|
|
93
|
+
rawSchema: entry.rawSchema,
|
|
94
|
+
}),
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
if (!progress) {
|
|
100
|
+
retrySchemas.forEach((entry) => {
|
|
101
|
+
const error = new Error(
|
|
102
|
+
`can't resolve reference while validating schema ${entry.schemaFile}`,
|
|
103
|
+
);
|
|
104
|
+
errors.push(
|
|
105
|
+
buildSchemaValidationError({
|
|
106
|
+
error,
|
|
107
|
+
component: entry.component,
|
|
108
|
+
schemaFile: entry.schemaFile,
|
|
109
|
+
rawSchema: entry.rawSchema,
|
|
110
|
+
}),
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
pendingSchemas = retrySchemas;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
valid: errors.length === 0,
|
|
121
|
+
errors,
|
|
122
|
+
validSchemas,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* @param {object} obj
|
|
128
|
+
* @param {Error & { errors?: Array<object> }} obj.error
|
|
129
|
+
* @param {string} obj.component
|
|
130
|
+
* @param {string} obj.schemaFile
|
|
131
|
+
* @param {object} obj.rawSchema
|
|
132
|
+
* @returns {object}
|
|
133
|
+
*/
|
|
134
|
+
function buildSchemaValidationError({ error, component, schemaFile, rawSchema }) {
|
|
135
|
+
const ajvErrors = Array.isArray(error?.errors) ? error.errors : [];
|
|
136
|
+
const [firstAjvError] = ajvErrors;
|
|
137
|
+
const hint = getSchemaHint(rawSchema, ajvErrors);
|
|
138
|
+
const type = isUnresolvedRefError(error) ? "schema-ref" : "schema";
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
type,
|
|
142
|
+
component,
|
|
143
|
+
schemaFile,
|
|
144
|
+
message: error?.toString?.() || "Unknown schema validation error",
|
|
145
|
+
schemaPath: firstAjvError?.schemaPath || "",
|
|
146
|
+
instancePath: firstAjvError?.instancePath || "",
|
|
147
|
+
hint,
|
|
148
|
+
details: ajvErrors.map((entry) => ({
|
|
149
|
+
keyword: entry.keyword,
|
|
150
|
+
message: entry.message,
|
|
151
|
+
schemaPath: entry.schemaPath,
|
|
152
|
+
instancePath: entry.instancePath,
|
|
153
|
+
params: entry.params,
|
|
154
|
+
})),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* @param {object} schema
|
|
160
|
+
* @param {Array<object>} ajvErrors
|
|
161
|
+
* @returns {string|undefined}
|
|
162
|
+
*/
|
|
163
|
+
function getSchemaHint(schema, ajvErrors) {
|
|
164
|
+
if (schema?.properties === null) {
|
|
165
|
+
return "Hint: `properties` resolves to null. In YAML this often means `properties:` has no nested keys.";
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (
|
|
169
|
+
ajvErrors.some(
|
|
170
|
+
(error) =>
|
|
171
|
+
error?.schemaPath?.endsWith("/properties/type") &&
|
|
172
|
+
error?.instancePath?.includes("/properties/"),
|
|
173
|
+
)
|
|
174
|
+
) {
|
|
175
|
+
return "Hint: check each field `type` value; it must be a valid JSON Schema type.";
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* @param {Error & { message?: string, missingRef?: string, missingSchema?: string, code?: string }} error
|
|
183
|
+
* @returns {boolean}
|
|
184
|
+
*/
|
|
185
|
+
function isUnresolvedRefError(error) {
|
|
186
|
+
if (typeof error?.missingRef === "string" && error.missingRef.length > 0) {
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (
|
|
191
|
+
typeof error?.missingSchema === "string" &&
|
|
192
|
+
error.missingSchema.length > 0
|
|
193
|
+
) {
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (error?.code === "ERR_MISSING_REF") {
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return /can't resolve reference|missing ref|missing schema/i.test(
|
|
202
|
+
error?.message || "",
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* @param {object} schemaError
|
|
208
|
+
* @param {object} [options]
|
|
209
|
+
* @param {boolean} [options.verbose]
|
|
210
|
+
* @returns {{ type: "schema"|"schema-ref", data: Array<object> }}
|
|
211
|
+
*/
|
|
212
|
+
export function toSchemaValidationResult(schemaError, options = {}) {
|
|
213
|
+
const useVerbose =
|
|
214
|
+
options.verbose ?? global.config?.schema?.verbose === true;
|
|
215
|
+
const formattedError = {
|
|
216
|
+
message: schemaError.message,
|
|
217
|
+
component: schemaError.component,
|
|
218
|
+
schemaFile: schemaError.schemaFile,
|
|
219
|
+
hint: schemaError.hint,
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
if (useVerbose) {
|
|
223
|
+
formattedError.schemaPath = schemaError.schemaPath;
|
|
224
|
+
formattedError.instancePath = schemaError.instancePath;
|
|
225
|
+
formattedError.details = schemaError.details;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
type: schemaError.type || "schema",
|
|
230
|
+
data: [
|
|
231
|
+
formattedError,
|
|
232
|
+
],
|
|
233
|
+
};
|
|
234
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@schalkneethling/miyagi-core",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.2.0",
|
|
4
4
|
"description": "miyagi is a component development tool for JavaScript template engines.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"author": "Schalk Neethling <schalkneethling@duck.com>, Michael Großklaus <mail@mgrossklaus.de> (https://www.mgrossklaus.de)",
|