@communities-webruntime/services
Version:
If you would like to run Lightning Web Runtime without the CLI, we expose some of our programmatic APIs available in Node.js. If you're looking for the CLI documentation [you can find that here](https://www.npmjs.com/package/@communities-webruntime/cli).
211 lines • 8.85 kB
JavaScript
/** @hidden */
/**
* Copyright (c) 2019, salesforce.com, inc.
* All rights reserved.
* SPDX-License-Identifier: MIT
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/
import MetadataValidator from '@communities-webruntime/metadata-schema';
import { LoadingCache } from '../utils/loading-cache.js';
import { assert } from '../utils/assert.js';
const { log } = console;
import 'colors';
// Routes reserved for Web Runtime framework and API requests
// TODO (W-5966583): These are hardcoded for now. Make these routes configurable by the application
const RESERVED_ROUTES = ['webruntime', 'assets'];
const DECLARATIVE_RESERVED_ROUTES = ['main', 'sfsites']; // site.com constants
const validatorCache = new LoadingCache();
function validate({ routes, brandingProperties, theme, views, partials, labels, locales }, schema) {
// add a validation function which returns a promise to this array
const validationTasks = [
validateRoutes.bind(null, schema, routes),
validateRoutesExtended.bind(null, routes),
validateTheme.bind(null, schema, theme),
validateViews.bind(null, schema, views),
validateViewsExtended.bind(null, theme, views),
validatePartials.bind(null, partials),
validateBranding.bind(null, schema, brandingProperties),
validateBrandingExtended.bind(null, brandingProperties),
validateLabels.bind(null, labels),
validateLocales.bind(null, schema, locales),
];
let errors = [];
// reduce ensures that the promises are processed sequentially
return validationTasks
.reduce((promiseChain, currentPromise) => {
return promiseChain.then(currentPromise().catch((err) => {
logErrors(err.errors, err.schemaId, err.id);
errors = errors.concat(err.errors);
}));
}, Promise.resolve())
.then(() => {
if (errors.length) {
return Promise.reject({ message: 'There was an error during validation', errors });
}
return Promise.resolve();
});
}
function validateViews(schema, views) {
return Promise.all(views.map((view) => {
return getValidator(schema).validate(view, 'view');
}));
}
function validateRoutes(schema, routes) {
return getValidator(schema).validate(routes, 'routes');
}
function validateRoutesExtended(routes) {
return new Promise(() => {
validateRootRoute(routes);
validateIsDefaultRoute(routes);
validateReservedRoutes(routes);
validateUniqueRouteId(routes);
validateUniqueRoutePath(routes);
}).catch((error) => {
return Promise.reject({ errors: [error], schemaId: 'routes' });
});
}
function validateTheme(schema, theme) {
return getValidator(schema).validate(theme, 'theme');
}
function validateViewsExtended(theme, views) {
return new Promise(() => {
validateViewThemeLayoutType(theme, views);
validateUniqueViewDevName(views);
}).catch((error) => {
return Promise.reject({ errors: [error], schemaId: 'views' });
});
}
function validateViewThemeLayoutType(theme, views) {
const themeLayoutIds = new Set();
const themeLayoutViewDevNames = new Set();
Object.entries(theme.themeLayouts).forEach((entry) => {
themeLayoutIds.add(entry[0]);
themeLayoutViewDevNames.add(entry[1].view);
});
// verify that every view that is not a theme layout specifies a themeLayoutType
// that corresponds to one of the themeLayouts specified in the theme.
// and verify that no theme layout view have defined a themeLayoutType.
views.forEach((view) => {
if (themeLayoutViewDevNames.has(view.devName)) {
assert(!('themeLayoutType' in view), `The theme layout view "${view.devName}" shouldn't have a themeLayoutType property set`);
}
else {
assert(view.themeLayoutType, `A themeLayoutType is required for view "${view.devName}"`);
assert(themeLayoutIds.has(view.themeLayoutType), `The themeLayoutType "${view.themeLayoutType}" for view "${view.devName}" is not a defined theme layout. Defined theme layouts are: [${Array.from(themeLayoutIds).join(', ')}].`);
}
});
}
function validateUniqueViewDevName(views) {
const duplicate = getDuplicateProperty(views, 'devName');
assert(!duplicate, `Multiple views found with the same devName: ${duplicate}`);
}
function validateBranding(schema, brandingProps) {
return getValidator(schema).validate(brandingProps, 'branding');
}
function validateBrandingExtended(brandingProps) {
return new Promise(() => {
validateUniqueBrandingId(brandingProps);
}).catch((error) => {
return Promise.reject({ errors: [error], schemaId: 'branding' });
});
}
function validateRootRoute(routes) {
const rootRoutes = routes.filter((route) => {
return route.isRoot;
});
assert(rootRoutes.length === 1, 'One and only one root route should be defined');
}
function validateIsDefaultRoute(routes) {
const defaultRoutes = routes.filter((route) => {
return route.isDefault;
});
assert(defaultRoutes.length === 1, 'One and only one default route should be defined');
}
function validateReservedRoutes(routes) {
// TODO (W-5966583): When reserved routes are configurable, should not always be checking declarative routes
const allReservedRoutes = RESERVED_ROUTES.concat(DECLARATIVE_RESERVED_ROUTES);
const reservedRoutesRegex = `^/(${allReservedRoutes.join('|')})(/.*)?$`;
const routeViolations = routes.filter((route) => {
// Page.js router is case-insensitive
return route.path.toLowerCase().match(reservedRoutesRegex);
});
assert(routeViolations.length === 0, `Cannot use reserved paths [${allReservedRoutes.join(', ')}] for route: ${routeViolations[0] && routeViolations[0].path}`);
}
function validateUniqueRouteId(routes) {
const duplicateId = getDuplicateProperty(routes, 'id');
assert(!duplicateId, `Multiple routes found with the same id: ${duplicateId}`);
}
function validateUniqueRoutePath(routes) {
// Page.js router is case-insensitive
const duplicatePath = getDuplicateProperty(routes, 'path', true);
if (duplicatePath) {
throw new Error(`Multiple routes found with the same path: ${duplicatePath.toLowerCase()}`);
}
}
function validateUniqueBrandingId(brandingProps) {
const duplicateId = getDuplicateProperty(brandingProps, 'id');
assert(!duplicateId, `Multiple branding properties found with the same id: ${duplicateId}`);
}
function validateLabels(labels) {
return new Promise(() => {
const isObject = typeof labels === 'object';
assert(isObject, `Labels should be a map of pages`);
for (const label of Object.keys(labels)) {
const labelIsObject = typeof labels[label] === 'object';
assert(labelIsObject, `Label should be a map of values`);
}
}).catch((error) => {
return Promise.reject({
errors: [error],
schemaId: 'partials',
});
});
}
function validateLocales(schema, locales) {
return getValidator(schema).validate(locales, 'locales');
}
function validatePartials(partials) {
return new Promise(() => {
const isObject = typeof partials === 'object';
assert(isObject, `Partials should be a map of file name and content`);
}).catch((error) => {
return Promise.reject({
errors: [error],
schemaId: 'partials',
});
});
}
function getDuplicateProperty(arr, prop, caseInsensitive = false) {
const props = new Set();
let duplicateValue;
// array.some will short circuit when returning true
arr.some((item) => {
const value = caseInsensitive ? item[prop].toLowerCase() : item[prop];
if (!props.has(value)) {
props.add(value);
return false;
}
duplicateValue = item[prop];
return true;
});
return duplicateValue;
}
function logErrors(errors, schemaId, id) {
let schema = schemaId.bold;
schema += id ? ` (${id})` : '';
log(`There were ${errors.length} error(s) validating against ${schema}: `.red);
errors.forEach((error) => {
const schemaPath = error.schemaPath || '';
const dataPath = error.dataPath || '';
const errorParams = (error.params && JSON.stringify(error.params)) || '';
const errorData = (error.data && `, for ${JSON.stringify(error.data)}`) || '';
log(`- ${schemaPath} ${dataPath} ${error.message} ${errorParams}${errorData}`.red);
});
}
function getValidator(schema) {
return validatorCache.get(schema, () => {
return new MetadataValidator(schema);
});
}
export { validate };
//# sourceMappingURL=metadata-validation.js.map