zombiebox
Version:
ZombieBox is a JavaScript framework for development of Smart TV and STB applications
220 lines (186 loc) • 5.85 kB
JavaScript
/*
* This file is part of the ZombieBox package.
*
* Copyright © 2012-2019, 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 ClosureCompiler = require('google-closure-compiler').compiler;
const Application = require('./application');
/**
*/
class BuildHelper {
/**
* @param {Application} application
*/
constructor(application) {
/**
* @type {Application}
* @protected
*/
this._application = application;
}
/**
* @param {Object} flags
* @return {Promise<{stderr: string, stdout: string}>}
*/
getCompressedScripts(flags = {}) {
const entryPoint = this._application.getGeneratedEntryPoint();
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));
const gccMapString = Array.from(aliases.keys())
.map((name) => `${name}/=${aliases.get(name)}/`);
const compilerOptions = mergeConfigs({
'compilation_level': 'ADVANCED_OPTIMIZATIONS',
'language_in': 'ECMASCRIPT_2018',
'language_out': 'ES5',
'warning_level': 'VERBOSE',
'charset': 'UTF8',
'module_resolution': 'BROWSER_WITH_TRANSFORMED_PREFIXES',
'browser_resolver_prefix_replacements': gccMapString,
'dependency_mode': 'STRICT',
'entry_point': entryPoint,
'summary_detail_level': '3',
'externs': externs,
'js': this._application.getCompilationScripts()
}, flags, this._application.getConfig().gcc);
const closureCompiler = new ClosureCompiler(compilerOptions);
return new Promise((resolve, reject) => {
closureCompiler.run((exitCode, stdout, stderr) => {
if (exitCode) {
reject(stderr.replace(/^(.*)\n/, '').trim()); // Discard command part (java -jar ...)
} else {
resolve({
stdout,
stderr: stderr.trim()
});
}
});
});
}
/**
* @param {string} resourcesTargetPath
* @return {Promise<string>}
*/
getCompressedStyles(resourcesTargetPath) {
const styles = this._application.getSortedStyles();
const postcssConfig = this._application.getConfig().postcss;
const processor = postcss([
postcssPresetEnv({...postcssConfig.presetEnv}),
...postcssConfig.extraPlugins,
postcssUrl({...postcssConfig.url}),
postcssCsso({...postcssConfig.csso})
]);
const to = path.join(this._application.getPathHelper().resolveAbsolutePath(resourcesTargetPath), 'dummy.file');
return Promise.all(
styles.map((stylePath) =>
fse.readFile(stylePath, 'utf-8')
.then((content) => processor.process(content, {
from: stylePath,
to
}))
.then((result) => result.css)
)
)
.then((content) => content.join('\n'));
}
/**
* @param {string} filename
* @return {Promise<string>} Resolved with GCC warnings.
*/
writeIndexHTML(filename) {
const absoluteFilename = this._application.getPathHelper().resolveAbsolutePath(filename);
return Promise.all([
this.getCompressedScripts(),
this.getCompressedStyles(path.dirname(absoluteFilename))
])
.then(([compilationResult, compressedStyles]) => {
const indexHTMLContent = this._application.getIndexHTMLContent({
inlineScripts: [compilationResult.stdout],
inlineStyles: [compressedStyles]
});
return fse.writeFile(absoluteFilename, indexHTMLContent, 'utf-8')
.then(() => compilationResult.stderr);
});
}
/**
* @param {string} dst
*/
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 || {});
}
for (const [alias, sourcePath] of Object.entries(staticFiles)) {
const targetPath = path.join(absoluteDestination, alias);
const targetDir = path.dirname(targetPath);
if (!fs.existsSync(targetDir)) {
fse.ensureDirSync(targetDir);
}
fse.copySync(pathHelper.resolveAbsolutePath(sourcePath), targetPath);
}
}
/**
* @param {string} dir
* @param {Object<string, string>} archiveMap
* @return {Object<string, string>}
*/
addDirToArchiveMap(dir, archiveMap = {}) {
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}
*/
writeZIPArchive(filename, files) {
const output = fs.createWriteStream(filename);
const archive = archiver('zip');
const promise = new Promise((resolve, reject) => {
output.on('finish', () => {
resolve(archive.pointer());
});
output.on('error', (err) => {
reject(err);
});
});
archive.pipe(output);
Object.keys(files)
.forEach((src) => {
const stat = fs.lstatSync(src);
if (stat.isSymbolicLink()) {
console.warn(`Skip symlink ${src}.`);
// Symlinks not supported in archiver api :'(
return;
}
archive.append(fs.createReadStream(src), {
name: files[src],
stats: stat
});
});
archive.finalize();
return promise;
}
}
module.exports = BuildHelper;