@lwrjs/config 0.8.0-alpha.3
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 +10 -0
- package/build/cjs/env-config.cjs +339 -0
- package/build/cjs/index.cjs +32 -0
- package/build/cjs/validation/app-config-context.cjs +406 -0
- package/build/cjs/validation/app-config.cjs +192 -0
- package/build/cjs/validation/helpers.cjs +34 -0
- package/build/es/env-config.d.ts +14 -0
- package/build/es/env-config.js +352 -0
- package/build/es/index.d.ts +4 -0
- package/build/es/index.js +5 -0
- package/build/es/validation/app-config-context.d.ts +55 -0
- package/build/es/validation/app-config-context.js +391 -0
- package/build/es/validation/app-config.d.ts +10 -0
- package/build/es/validation/app-config.js +245 -0
- package/build/es/validation/helpers.d.ts +8 -0
- package/build/es/validation/helpers.js +23 -0
- package/package.cjs +9 -0
- package/package.json +48 -0
- package/runtime-configs/compat.json +13 -0
- package/runtime-configs/dev.json +13 -0
- package/runtime-configs/prod-compat.json +13 -0
- package/runtime-configs/prod.json +13 -0
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import { findNodeAtLocation } from 'jsonc-parser';
|
|
2
|
+
import { descriptions } from '@lwrjs/diagnostics';
|
|
3
|
+
import { calculatePositionFromSource } from './helpers.js';
|
|
4
|
+
// Run the duplicate and missing property checks against an object of a given type
|
|
5
|
+
function createKeys(p, t) {
|
|
6
|
+
return p && t;
|
|
7
|
+
}
|
|
8
|
+
// These properties are kept in sync with the NormalizedLwrGlobalConfig type via ts checks
|
|
9
|
+
export const ROOT_ATTRIBUTE_KEYS = createKeys('root', [
|
|
10
|
+
'amdLoader',
|
|
11
|
+
'apiVersion',
|
|
12
|
+
'assets',
|
|
13
|
+
'assetProviders',
|
|
14
|
+
'assetTransformers',
|
|
15
|
+
'bundleConfig',
|
|
16
|
+
'cacheDir',
|
|
17
|
+
'contentDir',
|
|
18
|
+
'environment',
|
|
19
|
+
'errorRoutes',
|
|
20
|
+
'esmLoader',
|
|
21
|
+
'staticSiteGenerator',
|
|
22
|
+
'globalData',
|
|
23
|
+
'globalDataDir',
|
|
24
|
+
'hooks',
|
|
25
|
+
'ignoreLwrConfigFile',
|
|
26
|
+
'lwrConfigFile',
|
|
27
|
+
'layoutsDir',
|
|
28
|
+
'locker',
|
|
29
|
+
'lwc',
|
|
30
|
+
'lwrVersion',
|
|
31
|
+
'moduleProviders',
|
|
32
|
+
'port',
|
|
33
|
+
'basePath',
|
|
34
|
+
'resourceProviders',
|
|
35
|
+
'rootDir',
|
|
36
|
+
'routes',
|
|
37
|
+
'serverMode',
|
|
38
|
+
'serverType',
|
|
39
|
+
'templateEngine',
|
|
40
|
+
'viewProviders',
|
|
41
|
+
'viewTransformers',
|
|
42
|
+
]);
|
|
43
|
+
export const ASSET_DIR_ATTRIBUTE_KEYS = createKeys('assetDir', ['alias', 'dir', 'urlPath']);
|
|
44
|
+
export const ASSET_FILE_ATTRIBUTE_KEYS = createKeys('assetFile', ['alias', 'file', 'urlPath']);
|
|
45
|
+
export const LOCKER_ATTRIBUTE_KEYS = createKeys('locker', ['enabled', 'trustedComponents', 'clientOnly']);
|
|
46
|
+
export const ROUTE_ATTRIBUTE_KEYS = createKeys('routes', [
|
|
47
|
+
'bootstrap',
|
|
48
|
+
'contentTemplate',
|
|
49
|
+
'id',
|
|
50
|
+
'cache',
|
|
51
|
+
'layoutTemplate',
|
|
52
|
+
'method',
|
|
53
|
+
'path',
|
|
54
|
+
'rootComponent',
|
|
55
|
+
'routeHandler',
|
|
56
|
+
'properties',
|
|
57
|
+
]);
|
|
58
|
+
export const ERROR_ROUTE_ATTRIBUTE_KEYS = createKeys('errorRoutes', [
|
|
59
|
+
'bootstrap',
|
|
60
|
+
'contentTemplate',
|
|
61
|
+
'id',
|
|
62
|
+
'layoutTemplate',
|
|
63
|
+
'rootComponent',
|
|
64
|
+
'routeHandler',
|
|
65
|
+
'status',
|
|
66
|
+
'properties',
|
|
67
|
+
'cache',
|
|
68
|
+
]);
|
|
69
|
+
export const BOOTSTRAP_ATTRIBUTE_KEYS = createKeys('bootstrap', [
|
|
70
|
+
'autoBoot',
|
|
71
|
+
'syntheticShadow',
|
|
72
|
+
'workers',
|
|
73
|
+
'services',
|
|
74
|
+
'configAsSrc',
|
|
75
|
+
'experimentalSSR',
|
|
76
|
+
]);
|
|
77
|
+
const SPECIFIER_REGEX = /^@?[\w-]+(\/[\w-]+)*$/;
|
|
78
|
+
function isNotEmptyString(node) {
|
|
79
|
+
return node.type === 'string' && node.value.length > 0;
|
|
80
|
+
}
|
|
81
|
+
export const BASE_PATH_REGEX = /^(\/[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)+$/g;
|
|
82
|
+
export class ValidationContext {
|
|
83
|
+
constructor(sourceText) {
|
|
84
|
+
this.diagnostics = [];
|
|
85
|
+
this.sourceText = sourceText;
|
|
86
|
+
}
|
|
87
|
+
getLocationFromNode(node) {
|
|
88
|
+
return calculatePositionFromSource(this.sourceText, node);
|
|
89
|
+
}
|
|
90
|
+
assertIsObject(node, property) {
|
|
91
|
+
if (node.type !== 'object') {
|
|
92
|
+
this.diagnostics.push({
|
|
93
|
+
description: descriptions.CONFIG_PARSER.INCORRECT_NODE_TYPE(property, 'object', node.type),
|
|
94
|
+
location: this.getLocationFromNode(node),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
assertIsBoolean(node, property) {
|
|
99
|
+
if (node && node.type !== 'boolean') {
|
|
100
|
+
this.diagnostics.push({
|
|
101
|
+
description: descriptions.CONFIG_PARSER.INCORRECT_NODE_TYPE(property, 'boolean', node.type),
|
|
102
|
+
location: this.getLocationFromNode(node),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
assertIsArray(node, property) {
|
|
107
|
+
if (node && node.type !== 'array') {
|
|
108
|
+
this.diagnostics.push({
|
|
109
|
+
description: descriptions.CONFIG_PARSER.INCORRECT_NODE_TYPE(property, 'array', node.type),
|
|
110
|
+
location: this.getLocationFromNode(node),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
assertIsSpecifier(node, property) {
|
|
115
|
+
if (node && (node.type !== 'string' || !SPECIFIER_REGEX.test(node.value))) {
|
|
116
|
+
this.diagnostics.push({
|
|
117
|
+
description: descriptions.CONFIG_PARSER.INVALID_SPECIFIER(property, node.value),
|
|
118
|
+
location: this.getLocationFromNode(node),
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
assertIsPath(node, property) {
|
|
123
|
+
if (node && (node.type !== 'string' || node.value[0] !== '/')) {
|
|
124
|
+
this.diagnostics.push({
|
|
125
|
+
description: descriptions.CONFIG_PARSER.INVALID_PATH(property, node.value),
|
|
126
|
+
location: this.getLocationFromNode(node),
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
assertIsPort(node, property) {
|
|
131
|
+
if (node && (node.type !== 'number' || node.value < 0 || node.value > 65353)) {
|
|
132
|
+
this.diagnostics.push({
|
|
133
|
+
description: descriptions.CONFIG_PARSER.INVALID_PORT(property, node.value),
|
|
134
|
+
location: this.getLocationFromNode(node),
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
assertIsServerType(node, property) {
|
|
139
|
+
if (node && node.value !== 'express' && node.value !== 'koa' && node.value !== 'fs') {
|
|
140
|
+
this.diagnostics.push({
|
|
141
|
+
description: descriptions.CONFIG_PARSER.INVALID_SERVER_TYPE(property, node.value),
|
|
142
|
+
location: this.getLocationFromNode(node),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
assertIsStaticSiteGenerator(node, property) {
|
|
147
|
+
if (node && node.type !== 'object') {
|
|
148
|
+
this.diagnostics.push({
|
|
149
|
+
description: descriptions.CONFIG_PARSER.INVALID_GENERATOR_CONFIG(property, node.value),
|
|
150
|
+
location: this.getLocationFromNode(node),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
assertIsMethod(node, property) {
|
|
155
|
+
if (node && node.value !== 'get' && node.value !== 'post') {
|
|
156
|
+
this.diagnostics.push({
|
|
157
|
+
description: descriptions.CONFIG_PARSER.INVALID_METHOD(property, node.value),
|
|
158
|
+
location: this.getLocationFromNode(node),
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
assertIsStatus(node, property) {
|
|
163
|
+
if (node && node.value !== 404 && node.value !== 500) {
|
|
164
|
+
this.diagnostics.push({
|
|
165
|
+
description: descriptions.CONFIG_PARSER.INVALID_STATUS(property, node.value),
|
|
166
|
+
location: this.getLocationFromNode(node),
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
assertIsEnvironment(node, property) {
|
|
171
|
+
if (!node) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
let defaultProperty;
|
|
175
|
+
let supportedProperty;
|
|
176
|
+
if (node.type === 'object' && node.children?.length) {
|
|
177
|
+
for (const child of node.children) {
|
|
178
|
+
if (child.type === 'property' && child.children?.length) {
|
|
179
|
+
if (child.children[0].value === 'default' && isNotEmptyString(child.children[1])) {
|
|
180
|
+
defaultProperty = child;
|
|
181
|
+
}
|
|
182
|
+
if (child.children[0].value === 'supported') {
|
|
183
|
+
supportedProperty = child;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (node.type !== 'object' || (supportedProperty && !defaultProperty)) {
|
|
189
|
+
this.diagnostics.push({
|
|
190
|
+
description: descriptions.CONFIG_PARSER.INVALID_ENVIRONMENT(property),
|
|
191
|
+
location: this.getLocationFromNode(node),
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
assertIsBasePath(node, property) {
|
|
196
|
+
if (!node) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (node.type !== 'string') {
|
|
200
|
+
this.diagnostics.push({
|
|
201
|
+
description: descriptions.CONFIG_PARSER.INCORRECT_NODE_TYPE(property, 'string', node.type),
|
|
202
|
+
location: this.getLocationFromNode(node),
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
else if (node.value === '') {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
else if (node.value.match(BASE_PATH_REGEX) === null) {
|
|
209
|
+
this.diagnostics.push({
|
|
210
|
+
description: descriptions.CONFIG_PARSER.INVALID_BASEPATH(property, node.value),
|
|
211
|
+
location: this.getLocationFromNode(node),
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
assertNotEmptyString(node, property) {
|
|
216
|
+
if (!node) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
if (node.type !== 'string') {
|
|
220
|
+
this.diagnostics.push({
|
|
221
|
+
description: descriptions.CONFIG_PARSER.INCORRECT_NODE_TYPE(property, 'string', node.type),
|
|
222
|
+
location: this.getLocationFromNode(node),
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
else if (!isNotEmptyString(node)) {
|
|
226
|
+
this.diagnostics.push({
|
|
227
|
+
description: descriptions.CONFIG_PARSER.NON_EMPTY_STRING(property, node.value),
|
|
228
|
+
location: this.getLocationFromNode(node),
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
assertNotEmptyArray(node, property) {
|
|
233
|
+
if (!node) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (node.type !== 'array') {
|
|
237
|
+
this.diagnostics.push({
|
|
238
|
+
description: descriptions.CONFIG_PARSER.INCORRECT_NODE_TYPE(property, 'array', node.type),
|
|
239
|
+
location: this.getLocationFromNode(node),
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
else if (!node.children || node.children.length === 0) {
|
|
243
|
+
this.diagnostics.push({
|
|
244
|
+
description: descriptions.CONFIG_PARSER.NON_EMPTY_ARRAY(property, '[]'),
|
|
245
|
+
location: this.getLocationFromNode(node),
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
assertHasOneOrMore(node, property, childProps) {
|
|
250
|
+
// At least one of the given child properties of node must exist
|
|
251
|
+
if (!childProps.some((p) => findNodeAtLocation(node, [p]) !== undefined)) {
|
|
252
|
+
this.diagnostics.push({
|
|
253
|
+
description: descriptions.CONFIG_PARSER.MISSING_ONE_OF(property, childProps),
|
|
254
|
+
location: this.getLocationFromNode(node),
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
assertHasOnlyOne(node, property, childProps) {
|
|
259
|
+
// One and ONLY one of the given child properties of node must exist
|
|
260
|
+
if (childProps.filter((p) => findNodeAtLocation(node, [p])).length !== 1) {
|
|
261
|
+
this.diagnostics.push({
|
|
262
|
+
description: descriptions.CONFIG_PARSER.TOO_MANY(property, childProps),
|
|
263
|
+
location: this.getLocationFromNode(node),
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
assertArrayOfStrings(node, property) {
|
|
268
|
+
if (!node) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
if (node.type !== 'array') {
|
|
272
|
+
this.diagnostics.push({
|
|
273
|
+
description: descriptions.CONFIG_PARSER.INCORRECT_NODE_TYPE(property, 'array', node.type),
|
|
274
|
+
location: this.getLocationFromNode(node),
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
else if (node.children && node.children.length > 0) {
|
|
278
|
+
node.children.forEach((n, index) => {
|
|
279
|
+
if (!isNotEmptyString(n)) {
|
|
280
|
+
this.diagnostics.push({
|
|
281
|
+
description: descriptions.CONFIG_PARSER.NON_EMPTY_STRING(`${property}[${index}]`, n.value),
|
|
282
|
+
location: this.getLocationFromNode(n),
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
assertArrayOfSpecifiers(node, property) {
|
|
289
|
+
if (!node) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
if (node.type !== 'array') {
|
|
293
|
+
this.diagnostics.push({
|
|
294
|
+
description: descriptions.CONFIG_PARSER.INCORRECT_NODE_TYPE(property, 'array', node.type),
|
|
295
|
+
location: this.getLocationFromNode(node),
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
else if (node.children && node.children.length > 0) {
|
|
299
|
+
node.children.forEach((n, index) => {
|
|
300
|
+
if (n.type !== 'string' || !SPECIFIER_REGEX.test(n.value)) {
|
|
301
|
+
this.diagnostics.push({
|
|
302
|
+
description: descriptions.CONFIG_PARSER.INVALID_SPECIFIER(`${property}[${index}]`, n.value),
|
|
303
|
+
location: this.getLocationFromNode(n),
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
assertArrayOfServices(node, property) {
|
|
310
|
+
if (!node) {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (node.type !== 'array') {
|
|
314
|
+
this.diagnostics.push({
|
|
315
|
+
description: descriptions.CONFIG_PARSER.INCORRECT_NODE_TYPE(property, 'array', node.type),
|
|
316
|
+
location: this.getLocationFromNode(node),
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
else if (node.children && node.children.length > 0) {
|
|
320
|
+
node.children.forEach((n, index) => {
|
|
321
|
+
if (n.type !== 'string' && n.type !== 'array') {
|
|
322
|
+
this.diagnostics.push({
|
|
323
|
+
description: descriptions.CONFIG_PARSER.INCORRECT_NODE_TYPE(`${property}[${index}]`, 'string or array', n.type),
|
|
324
|
+
location: this.getLocationFromNode(n),
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
if ((n.type === 'string' && n.value.length === 0) ||
|
|
328
|
+
(n.type === 'array' &&
|
|
329
|
+
n.children &&
|
|
330
|
+
(n.children.length !== 2 || !isNotEmptyString(n.children[0])))) {
|
|
331
|
+
this.diagnostics.push({
|
|
332
|
+
description: descriptions.CONFIG_PARSER.INVALID_SERVICE(`${property}[${index}]`, n.value === undefined && n.children
|
|
333
|
+
? `invalid Array[${n.children.length}]`
|
|
334
|
+
: n.value),
|
|
335
|
+
location: this.getLocationFromNode(n),
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
assertUniqueIds(nodes, property) {
|
|
342
|
+
const ids = nodes
|
|
343
|
+
.map((n) => {
|
|
344
|
+
const idNode = findNodeAtLocation(n, ['id']);
|
|
345
|
+
return idNode ? idNode.value : undefined;
|
|
346
|
+
})
|
|
347
|
+
.filter((id) => id !== undefined);
|
|
348
|
+
const dupeIds = ids.filter((id, index) => ids.indexOf(id) !== index);
|
|
349
|
+
if (dupeIds.length > 0) {
|
|
350
|
+
this.diagnostics.push({
|
|
351
|
+
description: descriptions.CONFIG_PARSER.DUPLICATE_IDS(property, dupeIds),
|
|
352
|
+
location: this.getLocationFromNode(nodes[0]),
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
assertRequiredKeys(node, property, requiredPropertyKeys) {
|
|
357
|
+
// All of the given properties must exist on the node
|
|
358
|
+
const missingProps = requiredPropertyKeys.filter((p) => findNodeAtLocation(node, [p]) === undefined);
|
|
359
|
+
if (missingProps.length > 0) {
|
|
360
|
+
this.diagnostics.push({
|
|
361
|
+
description: descriptions.CONFIG_PARSER.MISSING_REQUIRED(property, missingProps),
|
|
362
|
+
location: this.getLocationFromNode(node),
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
assertValidKeys(node, property, validPropertyKeys) {
|
|
367
|
+
const { children } = node;
|
|
368
|
+
if (!children) {
|
|
369
|
+
this.diagnostics.push({
|
|
370
|
+
description: descriptions.CONFIG_PARSER.INVALID_EMPTY_NODE(property),
|
|
371
|
+
location: this.getLocationFromNode(node),
|
|
372
|
+
});
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
for (const propertyNode of children) {
|
|
377
|
+
if (propertyNode.type === 'property' && propertyNode.children) {
|
|
378
|
+
const [keyNode] = propertyNode.children;
|
|
379
|
+
const { type, value } = keyNode;
|
|
380
|
+
if (type === 'string' && !validPropertyKeys.includes(value)) {
|
|
381
|
+
this.diagnostics.push({
|
|
382
|
+
description: descriptions.CONFIG_PARSER.INVALID_PROPERTY(property, value),
|
|
383
|
+
location: this.getLocationFromNode(keyNode),
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
//# sourceMappingURL=app-config-context.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { LwrGlobalConfig } from '@lwrjs/types';
|
|
2
|
+
export declare const SOURCE_BY_PHASE: {
|
|
3
|
+
file: string;
|
|
4
|
+
pre: string;
|
|
5
|
+
post: string;
|
|
6
|
+
};
|
|
7
|
+
declare type ConfigPhase = 'file' | 'pre' | 'post';
|
|
8
|
+
export declare function validateLwrAppConfig(jsonSourceText: string, phase: ConfigPhase): LwrGlobalConfig;
|
|
9
|
+
export {};
|
|
10
|
+
//# sourceMappingURL=app-config.d.ts.map
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { parse, parseTree, printParseErrorCode, findNodeAtLocation as findNode, } from 'jsonc-parser';
|
|
2
|
+
import { createSingleDiagnosticError, descriptions, LwrConfigValidationError } from '@lwrjs/diagnostics';
|
|
3
|
+
import { ASSET_DIR_ATTRIBUTE_KEYS, ASSET_FILE_ATTRIBUTE_KEYS, BOOTSTRAP_ATTRIBUTE_KEYS, ERROR_ROUTE_ATTRIBUTE_KEYS, LOCKER_ATTRIBUTE_KEYS, ROOT_ATTRIBUTE_KEYS, ROUTE_ATTRIBUTE_KEYS, ValidationContext, } from './app-config-context.js';
|
|
4
|
+
import { calculatePositionFromSource } from './helpers.js';
|
|
5
|
+
export const SOURCE_BY_PHASE = {
|
|
6
|
+
file: 'lwr.config.json',
|
|
7
|
+
pre: 'argument passed to createServer',
|
|
8
|
+
post: 'configuration hooks',
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Check config.routes[x].bootstrap:
|
|
12
|
+
* - services: optional array of specifiers
|
|
13
|
+
* - autoBoot: optional boolean
|
|
14
|
+
* - syntheticShadow: optional boolean
|
|
15
|
+
* - workers: optional map of string:specifier pairs
|
|
16
|
+
* - configAsSrc: optional boolean to include the client bootstrap config as src via in-lined
|
|
17
|
+
*/
|
|
18
|
+
function validateBootstrap(node, validationContext, propPrefix) {
|
|
19
|
+
if (node) {
|
|
20
|
+
validationContext.assertIsObject(node, 'bootstrap');
|
|
21
|
+
validationContext.assertValidKeys(node, 'bootstrap', BOOTSTRAP_ATTRIBUTE_KEYS);
|
|
22
|
+
validationContext.assertArrayOfSpecifiers(findNode(node, ['services']), `${propPrefix}.services`);
|
|
23
|
+
validationContext.assertIsBoolean(findNode(node, ['autoBoot']), `${propPrefix}.autoBoot`);
|
|
24
|
+
validationContext.assertIsBoolean(findNode(node, ['experimentalSSR']), `${propPrefix}.experimentalSSR`);
|
|
25
|
+
validationContext.assertIsBoolean(findNode(node, ['configAsSrc']), `${propPrefix}.configAsSrc`);
|
|
26
|
+
validationContext.assertIsBoolean(findNode(node, ['syntheticShadow']), `${propPrefix}.syntheticShadow`);
|
|
27
|
+
// Each value in the worker map msut be a specifier
|
|
28
|
+
const workers = findNode(node, ['workers']);
|
|
29
|
+
if (workers && workers.children) {
|
|
30
|
+
workers.children.forEach((w, index) => {
|
|
31
|
+
if (w.children && w.children.length > 1) {
|
|
32
|
+
// get the value in a map entry: { "key": "value" }
|
|
33
|
+
validationContext.assertIsSpecifier(w.children[1], `${propPrefix}.workers[${index}]`);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Check common properties of config.routes[] and config.errorRoutes[]:
|
|
41
|
+
* - id: required string
|
|
42
|
+
* - each route must have at least 1 of the following:
|
|
43
|
+
* - rootComponent
|
|
44
|
+
* - contentTemplate
|
|
45
|
+
* - routeHandler
|
|
46
|
+
* - rootComponent: optional specifier
|
|
47
|
+
* - contentTemplate: optional string
|
|
48
|
+
* - layoutTemplate: optional string
|
|
49
|
+
* - routeHandler: optional string
|
|
50
|
+
* - optional bootstrap...
|
|
51
|
+
*/
|
|
52
|
+
function validateRouteCommon(node, validationContext, propPrefix) {
|
|
53
|
+
validationContext.assertHasOneOrMore(node, propPrefix, [
|
|
54
|
+
'rootComponent',
|
|
55
|
+
'contentTemplate',
|
|
56
|
+
'routeHandler',
|
|
57
|
+
]);
|
|
58
|
+
validationContext.assertNotEmptyString(findNode(node, ['id']), `${propPrefix}.id`);
|
|
59
|
+
validationContext.assertIsSpecifier(findNode(node, ['rootComponent']), `${propPrefix}.rootComponent`);
|
|
60
|
+
validationContext.assertNotEmptyString(findNode(node, ['contentTemplate']), `${propPrefix}.contentTemplate`);
|
|
61
|
+
validationContext.assertNotEmptyString(findNode(node, ['layoutTemplate']), `${propPrefix}.layoutTemplate`);
|
|
62
|
+
validationContext.assertNotEmptyString(findNode(node, ['routeHandler']), `${propPrefix}.routeHandler`);
|
|
63
|
+
validateBootstrap(findNode(node, ['bootstrap']), validationContext, `${propPrefix}.bootstrap`);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Check config.routes[]:
|
|
67
|
+
* - must have length > 0
|
|
68
|
+
* - path: required path segment string
|
|
69
|
+
* - method: optional 'get' | 'post'
|
|
70
|
+
*/
|
|
71
|
+
function validateRoutes(node, validationContext) {
|
|
72
|
+
if (node) {
|
|
73
|
+
validationContext.assertNotEmptyArray(node, 'routes');
|
|
74
|
+
if (node.children) {
|
|
75
|
+
node.children.forEach((n, index) => {
|
|
76
|
+
const propPrefix = `routes[${index}]`;
|
|
77
|
+
validationContext.assertIsObject(n, 'routes');
|
|
78
|
+
validationContext.assertValidKeys(n, 'routes', ROUTE_ATTRIBUTE_KEYS);
|
|
79
|
+
validationContext.assertRequiredKeys(n, propPrefix, ['id', 'path']);
|
|
80
|
+
validationContext.assertIsPath(findNode(n, ['path']), `${propPrefix}.path`);
|
|
81
|
+
validationContext.assertIsMethod(findNode(n, ['method']), `${propPrefix}.method`);
|
|
82
|
+
validateRouteCommon(n, validationContext, propPrefix);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Check config.errorRoutes[]:
|
|
89
|
+
* - status: required 404 | 500
|
|
90
|
+
*/
|
|
91
|
+
function validateErrorRoutes(node, validationContext) {
|
|
92
|
+
if (node) {
|
|
93
|
+
validationContext.assertIsArray(node, 'errorRoutes');
|
|
94
|
+
if (node.children) {
|
|
95
|
+
node.children.forEach((n, index) => {
|
|
96
|
+
const propPrefix = `errorRoutes[${index}]`;
|
|
97
|
+
validationContext.assertIsObject(n, 'errorRoutes');
|
|
98
|
+
validationContext.assertValidKeys(n, 'errorRoutes', ERROR_ROUTE_ATTRIBUTE_KEYS);
|
|
99
|
+
validationContext.assertRequiredKeys(n, propPrefix, ['id', 'status']);
|
|
100
|
+
validationContext.assertIsStatus(findNode(n, ['status']), `${propPrefix}.status`);
|
|
101
|
+
validateRouteCommon(n, validationContext, propPrefix);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Check string config.asset OR
|
|
108
|
+
* Check array config.assets[]:
|
|
109
|
+
* - must have length > 0
|
|
110
|
+
* - must have either dir or file, NOT both
|
|
111
|
+
* - dir: optional string
|
|
112
|
+
* - file: optional string
|
|
113
|
+
* - urlPath: required path segment string
|
|
114
|
+
* - alias: optional string
|
|
115
|
+
*/
|
|
116
|
+
function validateAssets(node, validationContext, preMerge) {
|
|
117
|
+
if (node) {
|
|
118
|
+
// assets can only be a string before the config is merged and normalized
|
|
119
|
+
if (preMerge && node.type === 'string') {
|
|
120
|
+
validationContext.assertNotEmptyString(node, 'assets');
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
validationContext.assertNotEmptyArray(node, 'assets');
|
|
124
|
+
if (node.children) {
|
|
125
|
+
node.children.forEach((n, index) => {
|
|
126
|
+
const dirNode = findNode(n, ['dir']);
|
|
127
|
+
const fileNode = findNode(n, ['file']);
|
|
128
|
+
if (dirNode?.type === 'string') {
|
|
129
|
+
validationContext.assertIsObject(n, 'assetDir');
|
|
130
|
+
validationContext.assertValidKeys(n, 'assetDir', ASSET_DIR_ATTRIBUTE_KEYS);
|
|
131
|
+
}
|
|
132
|
+
else if (fileNode?.type === 'string') {
|
|
133
|
+
validationContext.assertIsObject(n, 'assetFile');
|
|
134
|
+
validationContext.assertValidKeys(n, 'assetFile', ASSET_FILE_ATTRIBUTE_KEYS);
|
|
135
|
+
}
|
|
136
|
+
validationContext.assertRequiredKeys(n, `assets[${index}]`, ['urlPath']);
|
|
137
|
+
validationContext.assertHasOnlyOne(n, `assets[${index}]`, ['dir', 'file']);
|
|
138
|
+
validationContext.assertNotEmptyString(dirNode, `assets[${index}].dir`);
|
|
139
|
+
validationContext.assertNotEmptyString(fileNode, `assets[${index}].file`);
|
|
140
|
+
validationContext.assertIsPath(findNode(n, ['urlPath']), `assets[${index}].urlPath`);
|
|
141
|
+
validationContext.assertNotEmptyString(findNode(n, ['alias']), `assets[${index}].alias`);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Check config.locker:
|
|
149
|
+
* - enabled: required boolean
|
|
150
|
+
* - trustedComponents: optional array of strings
|
|
151
|
+
*/
|
|
152
|
+
function validateLocker(node, validationContext) {
|
|
153
|
+
if (node) {
|
|
154
|
+
validationContext.assertIsObject(node, 'locker');
|
|
155
|
+
validationContext.assertValidKeys(node, 'locker', LOCKER_ATTRIBUTE_KEYS);
|
|
156
|
+
validationContext.assertRequiredKeys(node, 'locker', ['enabled']);
|
|
157
|
+
validationContext.assertIsBoolean(findNode(node, ['enabled']), 'locker.enabled');
|
|
158
|
+
validationContext.assertArrayOfStrings(findNode(node, ['trustedComponents']), 'locker.trustedComponents');
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Check the normalized application configuration:
|
|
163
|
+
* - post-normalization, all properties are required
|
|
164
|
+
* - routes...
|
|
165
|
+
* - errorRoutes...
|
|
166
|
+
* - assets...
|
|
167
|
+
* - route and errorRoute ids must be unique
|
|
168
|
+
* - apiVersion: string
|
|
169
|
+
* - lwrVersion: string
|
|
170
|
+
* - amdLoader: specifier
|
|
171
|
+
* - esmLoader: specifier
|
|
172
|
+
* - port: number, 0 to 65353
|
|
173
|
+
* - serverMode: string
|
|
174
|
+
* - serverType: string
|
|
175
|
+
* - rootDir, cacheDir, contentDir, layoutsDir, globalDataDir: strings
|
|
176
|
+
* - hooks: array of strings
|
|
177
|
+
* - templateEngine: string
|
|
178
|
+
* - moduleProviders: array of services
|
|
179
|
+
* - viewProviders: array of services
|
|
180
|
+
* - resourceProviders: array of services
|
|
181
|
+
* - assetProviders: array of services
|
|
182
|
+
* - lwc.modules: array
|
|
183
|
+
* - basePath: valid subdomain part
|
|
184
|
+
* Notes:
|
|
185
|
+
* - ignore `bundleConfig` because it is not yet RFCed
|
|
186
|
+
* - specifier strings are validated for type and shape only, NOT if they are resolvable
|
|
187
|
+
*/
|
|
188
|
+
function validateRoot(node, validationContext, preMerge) {
|
|
189
|
+
validationContext.assertIsObject(node, 'root');
|
|
190
|
+
validationContext.assertValidKeys(node, 'root', ROOT_ATTRIBUTE_KEYS);
|
|
191
|
+
!preMerge && validationContext.assertRequiredKeys(node, 'root', ROOT_ATTRIBUTE_KEYS);
|
|
192
|
+
const routes = findNode(node, ['routes']);
|
|
193
|
+
const errorRoutes = findNode(node, ['errorRoutes']);
|
|
194
|
+
validationContext.assertUniqueIds([...(routes?.children || []), ...(errorRoutes?.children || [])], 'routes');
|
|
195
|
+
validateRoutes(routes, validationContext);
|
|
196
|
+
validateErrorRoutes(errorRoutes, validationContext);
|
|
197
|
+
validateAssets(findNode(node, ['assets']), validationContext, preMerge);
|
|
198
|
+
validateLocker(findNode(node, ['locker']), validationContext);
|
|
199
|
+
validationContext.assertNotEmptyString(findNode(node, ['apiVersion']), 'apiVersion');
|
|
200
|
+
validationContext.assertNotEmptyString(findNode(node, ['lwrVersion']), 'lwrVersion');
|
|
201
|
+
validationContext.assertIsSpecifier(findNode(node, ['amdLoader']), 'amdLoader');
|
|
202
|
+
validationContext.assertIsSpecifier(findNode(node, ['esmLoader']), 'esmLoader');
|
|
203
|
+
validationContext.assertIsPort(findNode(node, ['port']), 'port');
|
|
204
|
+
validationContext.assertNotEmptyString(findNode(node, ['serverMode']), 'serverMode');
|
|
205
|
+
validationContext.assertIsServerType(findNode(node, ['serverType']), 'serverType');
|
|
206
|
+
validationContext.assertIsStaticSiteGenerator(findNode(node, ['staticSiteGenerator']), 'staticSiteGenerator');
|
|
207
|
+
validationContext.assertNotEmptyString(findNode(node, ['rootDir']), 'rootDir');
|
|
208
|
+
validationContext.assertNotEmptyString(findNode(node, ['cacheDir']), 'cacheDir');
|
|
209
|
+
validationContext.assertNotEmptyString(findNode(node, ['contentDir']), 'contentDir');
|
|
210
|
+
validationContext.assertNotEmptyString(findNode(node, ['layoutsDir']), 'layoutsDir');
|
|
211
|
+
validationContext.assertNotEmptyString(findNode(node, ['globalDataDir']), 'globalDataDir');
|
|
212
|
+
validationContext.assertArrayOfServices(findNode(node, ['hooks']), 'hooks');
|
|
213
|
+
validationContext.assertNotEmptyString(findNode(node, ['templateEngine']), 'templateEngine');
|
|
214
|
+
validationContext.assertArrayOfServices(findNode(node, ['moduleProviders']), 'moduleProviders');
|
|
215
|
+
validationContext.assertArrayOfServices(findNode(node, ['viewProviders']), 'viewProviders');
|
|
216
|
+
validationContext.assertArrayOfServices(findNode(node, ['resourceProviders']), 'resourceProviders');
|
|
217
|
+
validationContext.assertArrayOfServices(findNode(node, ['assetProviders']), 'assetProviders');
|
|
218
|
+
validationContext.assertNotEmptyArray(findNode(node, ['lwc', 'modules']), 'lwc.modules');
|
|
219
|
+
validationContext.assertIsEnvironment(findNode(node, ['environment']), 'environment');
|
|
220
|
+
validationContext.assertIsBasePath(findNode(node, ['basePath']), 'basePath');
|
|
221
|
+
}
|
|
222
|
+
export function validateLwrAppConfig(jsonSourceText, phase) {
|
|
223
|
+
const errors = [];
|
|
224
|
+
const preMerge = phase !== 'post'; // meaning the config has not yet been merged and normalized
|
|
225
|
+
const rootNode = parseTree(jsonSourceText, errors);
|
|
226
|
+
// Fatal error: JSON could not be parsed
|
|
227
|
+
if (errors.length) {
|
|
228
|
+
const { error, length, offset } = errors[0];
|
|
229
|
+
const message = printParseErrorCode(error);
|
|
230
|
+
const sourceLocation = calculatePositionFromSource(jsonSourceText, { length, offset });
|
|
231
|
+
throw createSingleDiagnosticError({
|
|
232
|
+
location: sourceLocation,
|
|
233
|
+
description: descriptions.CONFIG_PARSER.INVALID_JSON(message),
|
|
234
|
+
}, LwrConfigValidationError);
|
|
235
|
+
}
|
|
236
|
+
// Validate from root
|
|
237
|
+
const validationContext = new ValidationContext(jsonSourceText);
|
|
238
|
+
validateRoot(rootNode, validationContext, preMerge);
|
|
239
|
+
// Throw an error with all diagnostics at once
|
|
240
|
+
if (validationContext.diagnostics.length) {
|
|
241
|
+
throw new LwrConfigValidationError(`Configuration validation errors in ${SOURCE_BY_PHASE[phase]}`, validationContext.diagnostics);
|
|
242
|
+
}
|
|
243
|
+
return parse(jsonSourceText);
|
|
244
|
+
}
|
|
245
|
+
//# sourceMappingURL=app-config.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { SourceLocation } from '@lwrjs/diagnostics';
|
|
2
|
+
interface SourcePosition {
|
|
3
|
+
offset: number;
|
|
4
|
+
length: number;
|
|
5
|
+
}
|
|
6
|
+
export declare function calculatePositionFromSource(source: string, partialPosition: SourcePosition): SourceLocation;
|
|
7
|
+
export {};
|
|
8
|
+
//# sourceMappingURL=helpers.d.ts.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function calculatePositionFromSource(source, partialPosition) {
|
|
2
|
+
const lines = source.split('\n');
|
|
3
|
+
const { offset, length } = partialPosition;
|
|
4
|
+
let countedChars = 0;
|
|
5
|
+
for (const [index, line] of lines.entries()) {
|
|
6
|
+
const newLength = countedChars + line.length;
|
|
7
|
+
if (newLength >= offset - 1) {
|
|
8
|
+
return {
|
|
9
|
+
start: {
|
|
10
|
+
line: index + 1,
|
|
11
|
+
column: offset - countedChars,
|
|
12
|
+
},
|
|
13
|
+
end: {
|
|
14
|
+
line: index + 1,
|
|
15
|
+
column: offset - countedChars + length,
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
countedChars = newLength + 1;
|
|
20
|
+
}
|
|
21
|
+
throw new Error('Unable to calculate position from source: Unreachable offset');
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=helpers.js.map
|