UNPKG

@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
/** @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