UNPKG

zombiebox

Version:

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

250 lines (212 loc) 7.21 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 fs = require('fs'); const fse = require('fs-extra'); const path = require('path'); const klawSync = require('klaw-sync'); const {mergeConfigs} = require('./utils'); const archiver = require('archiver'); const postcss = require('postcss'); const postcssPresetEnv = require('postcss-preset-env'); const postcssCsso = require('postcss-csso'); const postcssUrl = require('postcss-url'); const postcssImport = require('postcss-import'); const ClosureCompiler = require('google-closure-compiler').compiler; const Application = require('./application'); const PathHelper = require('./path-helper'); const logger = require('./logger').createChild('Build'); const kleur = require('kleur'); /** */ class BuildHelper { /** * @param {Application} application */ constructor(application) { /** * @type {Application} * @protected */ this._application = application; } /** * @param {Object} flags * @return {Promise<{stderr: string, stdout: string}>} */ getCompressedScripts(flags = {}) { logger.verbose('Starting compilation'); const entryPoint = this._application.getGeneratedEntryPoint(); logger.silly(`Application entry point: ${entryPoint}`); let aliases = this._application.getAliases(); let externs = []; for (const entity of this._application.getConfig().include) { externs = externs.concat(entity.externs || []); aliases = Object.assign(aliases, entity.aliases || {}); } const pathHelper = this._application.getPathHelper(); externs = externs.map((file) => pathHelper.resolveAbsolutePath(file)); logger.silly(`Externs: \n\t${externs.join('\n\t')}`); const gccMapString = Array.from(aliases.keys()) .map((name) => `${name}/=${aliases.get(name)}/`); const compilerOptions = mergeConfigs({ 'compilation_level': 'ADVANCED_OPTIMIZATIONS', 'language_in': 'ECMASCRIPT_2019', 'language_out': 'ES5', 'warning_level': 'VERBOSE', 'error_format': 'JSON', 'charset': 'UTF8', 'module_resolution': 'BROWSER_WITH_TRANSFORMED_PREFIXES', 'browser_resolver_prefix_replacements': gccMapString, 'dependency_mode': 'PRUNE', 'entry_point': entryPoint, 'summary_detail_level': '3', 'externs': externs, 'js': this._application.getCompilationScripts() }, flags, this._application.getConfig().gcc); logger.debug(`GCC flags: ${JSON.stringify(compilerOptions, (key, value) => { if (key === 'js' || key === 'externs') { return ['<truncated>']; } return value; }, '\t')}`); const closureCompiler = new ClosureCompiler(compilerOptions); return new Promise((resolve) => { logger.debug('Running Google Closure Compiler'); closureCompiler.run((exitCode, stdout, stderr) => { logger.silly(`GCC exited with ${exitCode}`); if (exitCode) { resolve({ stdout, stderr: stderr.replace(/^(.*)\n/, '').trim()// Discard command part (java -jar ...) }); } else { resolve({ stdout, stderr: stderr.trim() }); } }); }); } /** * @param {string} resourcesTargetPath * @return {Promise<{ * css: string, * messages: Array<Object> * }>} */ async getCompressedStyles(resourcesTargetPath) { const allStyles = await this._application.getSortedStyles(); const postcssConfig = this._application.getConfig().postcss; const to = path.join(this._application.getPathHelper().resolveAbsolutePath(resourcesTargetPath), 'dummy.file'); const prepareProcessor = postcss([ postcssPresetEnv({...postcssConfig.presetEnv}), ...postcssConfig.filePlugins, postcssUrl({...postcssConfig.url}) ]); const importBundleProcessor = postcss([ postcssImport({ resolve: (id) => { if (PathHelper.isLocal(id)) { return id; } return this._application.aliasedPathToFsPath(id); }, load: (fsPath) => processFile(prepareProcessor, fsPath) }) ]); const finalProcessor = postcss([ ...postcssConfig.bundlePlugins, postcssCsso({...postcssConfig.csso}) ]); const processFile = async (processor, fsPath) => { const pluginNames = processor.plugins.map((plugin) => plugin && plugin.postcssPlugin).join(', '); logger.silly(`Running ${kleur.cyan(pluginNames)} on ${kleur.underline(fsPath)}`); const content = await fse.readFile(fsPath, 'utf-8'); const result = await processor.process(content, {from: fsPath, to}); return result.css; }; let bundle; if (postcssConfig.importEntryPoints) { bundle = (await Promise.all( postcssConfig.importEntryPoints.map(async (fsPath) => processFile(importBundleProcessor, fsPath)) )).join('\n'); } else { bundle = (await Promise.all( allStyles.map((fsPath) => processFile(prepareProcessor, fsPath)) )).join('\n'); } const pluginNames = finalProcessor.plugins.map((plugin) => plugin && plugin.postcssPlugin).join(', '); logger.silly(`Running ${kleur.cyan(pluginNames)} on bundle`); return finalProcessor.process(bundle, {from: to, to}); } /** * @param {string} dst */ async copyStaticFiles(dst) { const pathHelper = this._application.getPathHelper(); const absoluteDestination = pathHelper.resolveAbsolutePath(dst); let staticFiles = {}; for (const entity of this._application.getConfig().include) { staticFiles = Object.assign(staticFiles, entity.static || {}); } logger.verbose(`Copying static files \n${JSON.stringify(staticFiles, null, '\t')}`); await Promise.all(Object.entries(staticFiles).map(async ([alias, sourcePath]) => { const targetPath = path.join(absoluteDestination, alias); const targetDir = path.dirname(targetPath); await fse.ensureDir(targetDir); await fse.copy(pathHelper.resolveAbsolutePath(sourcePath), targetPath); })); } /** * @param {string} dir * @param {Object<string, string>} archiveMap * @return {Object<string, string>} */ addDirToArchiveMap(dir, archiveMap = {}) { logger.verbose(`Adding ${dir} to archive`); klawSync(dir, {nodir: true}) .forEach((file) => (archiveMap[file.path] = path.relative(dir, file.path))); return archiveMap; } /** * @param {string} filename * @param {Object<string, string>} files * @return {Promise} */ async writeZIPArchive(filename, files) { logger.verbose(`Writing zip archive ${filename}`); logger.silly(`Archive files: ${JSON.stringify(files, null, '\t')}`); const output = fs.createWriteStream(filename); const archive = archiver('zip'); archive.pipe(output); await Promise.all(Object.keys(files).map(async (src) => { const stat = await fse.lstat(src); if (stat.isSymbolicLink()) { logger.warn(`Skipping symlink ${src}.`); // Symlinks not supported in archiver api :'( return; } archive.append(fs.createReadStream(src), { name: files[src], stats: stat }); })); archive.finalize(); return new Promise((resolve, reject) => { output.on('finish', () => { resolve(archive.pointer()); }); output.on('error', (err) => { reject(err); }); }); } } module.exports = BuildHelper;