@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.
@@ -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