UNPKG

zombiebox

Version:

ZombieBox is a JavaScript framework for development of Smart TV and STB applications

354 lines (306 loc) 7.98 kB
/* * This file is part of the ZombieBox package. * * Copyright © 2012-2021, Interfaced * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ const path = require('path'); const fse = require('fs-extra'); const kleur = require('kleur'); const SourceProviderBase = require('./source-provider-base'); const ISourceProvider = require('./i-source-provider'); const AbstractExtension = require('../addons/abstract-extension'); const AddonLoader = require('../addons/loader'); const {IZombieBoxConfig} = require('../config/interface'); const PathHelper = require('../path-helper'); const TemplateHelper = require('../template-helper'); const logger = require('../logger').createChild('Generated'); /** * @implements {ISourceProvider} */ class SourceProviderGenerated extends SourceProviderBase { /** * @param {CodeSource} codeSource * @param {AddonLoader} addonLoader * @param {PathHelper} pathHelper * @param {TemplateHelper} templateHelper * @param {IZombieBoxConfig} buildConfig * @param {Object} packageJson */ constructor(codeSource, addonLoader, pathHelper, templateHelper, buildConfig, packageJson) { super(); /** * @type {PathHelper} * @protected */ this._pathHelper = pathHelper; /** * @type {string} * @protected */ this._root = pathHelper.resolveAbsolutePath(buildConfig.generatedCode); /** * @type {TemplateHelper} * @protected */ this._templateHelper = templateHelper; /** * @type {IZombieBoxConfig} * @protected */ this._buildConfig = buildConfig; /** * @type {Object} * @protected */ this._packageJson = packageJson; /** * @type {AddonLoader} * @protected */ this._addonLoader = addonLoader; /** * @type {Map<AbstractExtension, function()>} * @protected */ this._extensionListeners = new Map(); this._readyPromise = codeSource.fs.ready() .then(() => this.generate()); } /** * @override */ watch() { this._addonLoader.getExtensions().forEach((extension) => { const listener = (sources) => { this._onExtensionGeneratedCode(extension, sources); }; extension.on(AbstractExtension.EVENT_GENERATED, listener); this._extensionListeners.set(extension, listener); }); } /** * @override */ stopWatching() { for (const [extension, listener] of this._extensionListeners) { extension.off(listener); } this._extensionListeners.clear(); } /** */ async clean() { logger.debug(`Cleaning up ${kleur.underline(this._root)}`); await fse.emptyDir(this._root); this._files = []; } /** * Clean and build code. */ async generate() { await this.clean(); await Promise.all([ this.generateBaseApp(), this.generateExtensionsCode(), this.generateDefines() ]); } /** */ async generateBaseApp() { const platformNames = this._addonLoader.getPlatforms() .map((platform) => platform.getName()); // PC is a special platform that can't be detected properly and should be the last in the list if (platformNames.includes('pc')) { platformNames.splice(platformNames.findIndex((name) => name === 'pc'), 1); platformNames.push('pc'); } const mainPath = this._buildConfig.project.name + '/' + path.relative( this._pathHelper.resolveAbsolutePath(this._buildConfig.project.src), this._pathHelper.resolveAbsolutePath(this._buildConfig.project.entry) ).replace(/\\/g, '/'); await Promise.all([ this._writeFile( 'base-application.js', this._templateHelper.render('base-application.js.tpl', { platforms: platformNames }) ), this._writeFile( 'app.js', this._templateHelper.render('app.js.tpl', { path: mainPath.replace(/\.js$/, '') }) ) ]); } /** */ async generateExtensionsCode() { await Promise.all( this._addonLoader.getExtensions().map((extension) => { const sources = this._resolveAddonRelativeSources(extension, extension.generateCode(this._buildConfig)); return this._writeSources(sources); }) ); } /** */ async generateDefines() { /** * @param {Array<*>} array * @return {string} */ const getArrayContentsType = (array) => { if (!array.length) { return '*'; } const elementTypes = array.map((element) => getGCCType(element)); return Array.from(new Set(elementTypes)) .join('|'); }; /** * @param {*} value * @return {string} */ const getGCCType = (value) => { const jsType = typeof value; if (jsType === 'function') { return 'Function'; } if (jsType !== 'object') { return jsType; } if (value === null) { return 'null'; } if (Array.isArray(value)) { return `Array<${getArrayContentsType(value)}>`; } return 'Object'; }; /** * @param {string} type * @return {string} */ const printTypeTag = (type) => type === 'Object' ? '@struct' : `@const {${type}}`; /** * @param {...string} tags * @return {string} */ const printJsdoc = (...tags) => [ '/**', ...tags.map((tag) => ` * ${tag}`), ' */', '' ].join('\n'); /** * @param {string} string * @return {string} */ const indent = (string) => '\t' + string.split('\n') .join('\n\t'); /** * @param {Object} object * @return {string} */ const printObject = (object) => [ '{', indent( Object.keys(object) .map((key) => { const value = object[key]; const type = getGCCType(value); return printJsdoc(printTypeTag(type)) + `${key}: ${printValue(type, value)}`; }) .join(',\n\n') ), '}' ].join('\n'); /** * @param {string} type * @param {*} value * @return {string} */ const printValue = (type, value) => { switch (type) { case 'Object': return printObject(value); case 'number': case 'Function': return value.toString(); default: return JSON.stringify(value); } }; const content = Object.keys(this._buildConfig.define) .map((topLevelKey) => { const value = this._buildConfig.define[topLevelKey]; const type = getGCCType(value); return printJsdoc(printTypeTag(type)) + `export const ${topLevelKey} = ${printValue(type, value)};`; }) .join('\n\n'); await this._writeFile('define.js', content); } /** * @param {AbstractExtension} addon * @param {Object<string, string>} sources * @return {Object<string, string>} * @protected */ _resolveAddonRelativeSources(addon, sources) { const filePaths = Object.keys(sources); return filePaths.reduce((result, filePath) => { const addonRelativePath = path.join(addon.getName(), filePath); return Object.assign(result, {[addonRelativePath]: sources[filePath]}); }, {}); } /** * @param {AbstractExtension} extension * @param {Object<string, string>} sources * @protected */ async _onExtensionGeneratedCode(extension, sources) { logger.debug( `Extension ${kleur.green(extension.getName())} generated files: \n\t` + Object.keys(sources).join('\n\t') ); await this._writeSources(this._resolveAddonRelativeSources(extension, sources)); } /** * @param {Object<string, string>} sources * @protected */ async _writeSources(sources) { return Promise.all( Object.entries(sources) .map(([src, content]) => this._writeFile(src, content)) ); } /** * @param {string} src * @param {string} content * @protected */ async _writeFile(src, content) { const filename = path.join(this._root, src); const dir = path.dirname(filename); await fse.ensureDir(dir); logger.silly(`Generated file ${kleur.underline(filename)}`); await fse.writeFile(filename, content, 'utf-8'); if (!this._files.includes(filename)) { this._files.push(filename); } this.emit(ISourceProvider.EVENT_CHANGED, filename); this.emit(ISourceProvider.EVENT_ANY, ISourceProvider.EVENT_CHANGED, filename); } } module.exports = SourceProviderGenerated;