UNPKG

catberry

Version:

Catberry is an isomorphic framework for building universal front-end apps using components, Flux architecture and progressive rendering.

229 lines (205 loc) 7.34 kB
'use strict'; const path = require('path'); const pfs = require('../promises/fs'); const hrTimeHelper = require('../helpers/hrTimeHelper'); const moduleHelper = require('../helpers/moduleHelper'); const templateHelper = require('../helpers/templateHelper'); const loadHelper = require('../helpers/loadHelper'); const requireHelper = require('../helpers/requireHelper'); const RouteParser = require('../tokenizers/RouteParser'); const APP_DEFINITIONS_FILENAME = 'appDefinitions.js'; const LIB_ROOT_PATH = path.join(__dirname, '..'); const STORES_REPLACE = '/** __stores **/'; const COMPONENTS_REPLACE = '/** __components **/'; const ROUTE_DESCRIPTORS_REPLACE = '\'__routes\''; const ROUTE_DEFINITIONS_REPLACE = '\'__routeDefinitions\''; const ROUTE_DEFINITIONS_FILENAME = 'routes.js'; /** * Implements the app definitions builder module. */ class AppDefinitionsBuilder { /** * Creates a new instance of the app definitions builder. * @param {ServiceLocator} locator The Service Locator for resolving dependencies. */ constructor(locator) { /** * Current event bus. * @type {EventEmitter} * @private */ this._eventBus = locator.resolve('eventBus'); /** * Current template provider list. * @type {Array<TemplateProvider>} * @private */ this._templateProviders = templateHelper.resolveTemplateProviders(locator); /** * Current route parser. * @type {RouteParser} * @private */ this._routeParser = new RouteParser(); } /** * Creates a real app definitions code from the template. * @param {Object} stores The found stores by their names. * @param {Object} components The found components by their names. * @returns {Promise<string>} The promise for the source code of the app definitions. */ build(stores, components) { const appDefinitionsTemplatePath = path.join( LIB_ROOT_PATH, APP_DEFINITIONS_FILENAME ); const routeDefinitionsPath = path.join( process.cwd(), ROUTE_DEFINITIONS_FILENAME ); const startTime = hrTimeHelper.get(); this._eventBus.emit('info', `Building app definitions using the template "${appDefinitionsTemplatePath}"...`); return pfs.readFile(appDefinitionsTemplatePath, { encoding: 'utf8' }) .then(file => Promise .all([ this._generateStoreDescriptors(stores), this._generateComponentDescriptors(components) ]) .then(results => ({ file, stores: results[0], components: results[1] }))) // check if paths exist and create require statements or undefined .then(context => pfs.exists(routeDefinitionsPath) .then(isExists => { const filePath = path.relative( process.cwd(), requireHelper.getValidPath(routeDefinitionsPath) ); const requireString = isExists ? `require('./${filePath}')` : 'null'; const routeDescriptors = []; if (isExists) { const routeDefinitions = require(routeDefinitionsPath); /* eslint max-nested-callbacks: 0 */ routeDefinitions.forEach(definition => { if (typeof (definition) === 'string') { routeDescriptors.push(this._routeParser.parseRouteExpression(definition)); } if (typeof (definition) === 'object' && typeof (definition.expression) === 'string') { routeDescriptors.push(this._routeParser.parseRouteExpression(definition.expression)); } }); } return context.file .replace(COMPONENTS_REPLACE, context.components) .replace(STORES_REPLACE, context.stores) .replace(ROUTE_DEFINITIONS_REPLACE, requireString) .replace(ROUTE_DESCRIPTORS_REPLACE, JSON.stringify(routeDescriptors)); }) ) .then(appDefinitions => { const hrTime = hrTimeHelper.get(startTime); this._eventBus.emit('appDefinitionsBuilt', { template: appDefinitionsTemplatePath, hrTime, time: hrTimeHelper.toMilliseconds(hrTime) }); return appDefinitions; }) .catch(reason => this._eventBus.emit('error', reason)); } /** * Generates inline descriptors for each store. * @param {Object} stores The found stores by their names. * @returns {Promise<string>} The promise for the JSON string that describes the stores. * @private */ _generateStoreDescriptors(stores) { const storeRequires = []; Object .keys(stores) .forEach(storeName => { const filePath = requireHelper.getValidPath( path.relative(process.cwd(), stores[storeName].path) ); const requireExpression = stores[storeName].path ? `require('./${filePath}')` : null; if (!requireExpression) { return; } storeRequires.push(`\n{name: '${storeName}', constructor: ${requireExpression}}`); }); return Promise.resolve(storeRequires.join(',')); } /** * Generates inline descriptors for each component. * @param {Object} components The found components by their names. * @returns {Promise<string>} The promise for the JSON string that describes the components. * @private */ _generateComponentDescriptors(components) { const promises = []; Object.keys(components) .forEach(componentName => { const componentDetails = components[componentName]; const logicFile = components[componentName].properties.logic || moduleHelper.DEFAULT_LOGIC_FILENAME; const logicPath = path.resolve(path.dirname(componentDetails.path), logicFile); const relativeLogicPath = path.relative(process.cwd(), logicPath); var constructor; try { constructor = require(logicPath); } catch (e) { this._eventBus.emit('error', e); } if (typeof (constructor) !== 'function' || typeof (componentDetails.properties.template) !== 'string') { return; } const component = Object.create(componentDetails); const promise = loadHelper.loadTemplateSources(component) .then(() => loadHelper.assignTemplateProviders(component, this._templateProviders)) .then(() => loadHelper.compileTemplates(component)) .then(() => { const filePath = requireHelper.getValidPath(relativeLogicPath); const requireString = `require('./${filePath}')`; const errorTemplateString = component.compiledErrorTemplate !== null ? `'${escapeTemplateSource(component.compiledErrorTemplate)}'` : 'null'; const errorTemplateProvider = component.errorTemplateProvider !== null ? `'${component.errorTemplateProvider.getName()}'` : 'null'; return ` { name: '${componentName}', constructor: ${requireString}, properties: ${JSON.stringify(componentDetails.properties)}, templateProviderName: '${component.templateProvider.getName()}', compiledTemplate: '${escapeTemplateSource(component.compiledTemplate)}', errorTemplateProviderName: ${errorTemplateProvider}, compiledErrorTemplate: ${errorTemplateString} }`; }); promises.push(promise); }); return Promise .all(promises) .then(components => components.join(',')); } } /** * Escapes template source code for including into the app definitions. * @param {string} source The compiled template source. * @returns {string} The escaped string. */ function escapeTemplateSource(source) { return source .replace(/(\\.)/g, '\\$&') .replace(/'/g, '\\\'') .replace(/\r/g, '\\r') .replace(/\n/g, '\\n') .replace(/\t/g, '\\t'); } module.exports = AppDefinitionsBuilder;