client/build: Clean up build process

Fixes incorrect URIs of iOS splash screens and OpenSans font
Files get gzipped inside build script
Better nginx configuration
build.js uses more consistent, synchronous code
This commit is contained in:
Shyam Sunder 2018-09-03 17:36:06 -04:00 committed by rr-
parent e6445b431f
commit a5a06bf2d1
4 changed files with 222 additions and 196 deletions

View file

@ -8,10 +8,7 @@ COPY . ./
ARG BUILD_INFO="docker-latest" ARG BUILD_INFO="docker-latest"
ARG CLIENT_BUILD_ARGS="" ARG CLIENT_BUILD_ARGS=""
RUN BASE_URL="__BASEURL__" node build.js ${CLIENT_BUILD_ARGS} RUN BASE_URL="__BASEURL__" node build.js --gzip ${CLIENT_BUILD_ARGS}
RUN find public/ -name public/index.html -prune -o -type f -size +5k \
-print0 | xargs -0 -- gzip -6 -k
FROM nginx:alpine FROM nginx:alpine
@ -23,7 +20,7 @@ RUN \
echo 'sed -i "s|__BACKEND__|${BACKEND_HOST}|" /etc/nginx/nginx.conf' \ echo 'sed -i "s|__BACKEND__|${BACKEND_HOST}|" /etc/nginx/nginx.conf' \
>> /init && \ >> /init && \
echo 'sed -i "s|__BASEURL__|${BASE_URL:-/}|" /var/www/index.htm' >> /init && \ echo 'sed -i "s|__BASEURL__|${BASE_URL:-/}|" /var/www/index.htm' >> /init && \
echo 'exec nginx -g "daemon off;"' >> /init && \ echo 'exec nginx' >> /init && \
chmod a+x /init chmod a+x /init
CMD ["/init"] CMD ["/init"]

283
client/build.js Normal file → Executable file
View file

@ -1,5 +1,35 @@
#!/usr/bin/env node
'use strict'; 'use strict';
// -------------------------------------------------
const webapp_icons = [
{name: 'android-chrome-192x192.png', size: 192},
{name: 'android-chrome-512x512.png', size: 512},
{name: 'apple-touch-icon.png', size: 180},
{name: 'mstile-150x150.png', size: 150}
];
const webapp_splash_screens = [
{w: 640, h: 1136, center: 320},
{w: 750, h: 1294, center: 375},
{w: 1125, h: 2436, center: 565},
{w: 1242, h: 2148, center: 625},
{w: 1536, h: 2048, center: 770},
{w: 1668, h: 2224, center: 820},
{w: 2048, h: 2732, center: 1024}
];
const external_js = [
'underscore',
'superagent',
'mousetrap',
'js-cookie',
'nprogress',
];
// -------------------------------------------------
const fs = require('fs'); const fs = require('fs');
const glob = require('glob'); const glob = require('glob');
const path = require('path'); const path = require('path');
@ -10,48 +40,18 @@ function readTextFile(path) {
return fs.readFileSync(path, 'utf-8'); return fs.readFileSync(path, 'utf-8');
} }
function writeFile(path, content) { function gzipFile(file) {
return fs.writeFileSync(path, content); file = path.normalize(file);
execSync('gzip -6 -k ' + file);
} }
function getVersion() { // -------------------------------------------------
let build_info = process.env.BUILD_INFO;
if (build_info) {
return build_info.trim();
} else {
try {
build_info = execSync('git describe --always --dirty --long --tags')
.toString();
} catch (e) {
console.warn('Cannot find build version');
return 'unknown';
}
return build_info.trim();
}
}
function getConfig() { function bundleHtml() {
let config = { const underscore = require('underscore');
meta: { const babelify = require('babelify');
version: getVersion(),
buildDate: new Date().toUTCString()
}
};
return config; const baseUrl = process.env.BASE_URL ? process.env.BASE_URL : '/';
}
function copyFile(source, target) {
fs.createReadStream(source).pipe(fs.createWriteStream(target));
}
function minifyJs(path) {
return require('terser').minify(fs.readFileSync(path, 'utf-8'), {compress: {unused: false}}).code;
}
function minifyCss(css) {
return require('csso').minify(css).css;
}
function minifyHtml(html) { function minifyHtml(html) {
return require('html-minifier').minify(html, { return require('html-minifier').minify(html, {
@ -61,23 +61,20 @@ function minifyHtml(html) {
}).trim(); }).trim();
} }
function bundleHtml() { const baseHtml = readTextFile('./html/index.htm')
const underscore = require('underscore'); .replace('<!-- Base HTML Placeholder -->', `<base href="${baseUrl}"/>`);
const babelify = require('babelify'); fs.writeFileSync('./public/index.htm', minifyHtml(baseHtml));
const baseHtml = readTextFile('./html/index.htm', 'utf-8');
const baseUrl = process.env.BASE_URL ? process.env.BASE_URL : '/';
const finalHtml = baseHtml.replace(
'<!-- Base HTML Placeholder -->', `<base href="${baseUrl}"/>`);
writeFile('./public/index.htm', minifyHtml(finalHtml));
glob('./html/**/*.tpl', {}, (er, files) => { let compiledTemplateJs = [
let compiledTemplateJs = '\'use strict\'\n'; `'use strict';`,
compiledTemplateJs += 'let _ = require(\'underscore\');'; `let _ = require('underscore');`,
compiledTemplateJs += 'let templates = {};'; `let templates = {};`
for (const file of files) { ];
for (const file of glob.sync('./html/**/*.tpl')) {
const name = path.basename(file, '.tpl').replace(/_/g, '-'); const name = path.basename(file, '.tpl').replace(/_/g, '-');
const placeholders = []; const placeholders = [];
let templateText = readTextFile(file, 'utf-8'); let templateText = readTextFile(file);
templateText = templateText.replace( templateText = templateText.replace(
/<%.*?%>/ig, /<%.*?%>/ig,
(match) => { (match) => {
@ -92,145 +89,175 @@ function bundleHtml() {
const functionText = underscore.template( const functionText = underscore.template(
templateText, {variable: 'ctx'}).source; templateText, {variable: 'ctx'}).source;
compiledTemplateJs += `templates['${name}'] = ${functionText};`;
compiledTemplateJs.push(`templates['${name}'] = ${functionText};`);
} }
compiledTemplateJs += 'module.exports = templates;'; compiledTemplateJs.push('module.exports = templates;');
writeFile('./js/.templates.autogen.js', compiledTemplateJs);
fs.writeFileSync('./js/.templates.autogen.js', compiledTemplateJs.join('\n'));
console.info('Bundled HTML'); console.info('Bundled HTML');
});
} }
function bundleCss() { function bundleCss() {
const stylus = require('stylus'); const stylus = require('stylus');
glob('./css/**/*.styl', {}, (er, files) => {
let css = '';
for (const file of files) {
css += stylus.render(
readTextFile(file), {filename: file});
}
writeFile('./public/css/app.min.css', minifyCss(css));
copyFile( function minifyCss(css) {
return require('csso').minify(css).css;
}
let css = '';
for (const file of glob.sync('./css/**/*.styl')) {
css += stylus.render(readTextFile(file), {filename: file});
}
fs.writeFileSync('./public/css/app.min.css', minifyCss(css));
if (process.argv.includes('--gzip')) {
gzipFile('./public/css/app.min.css');
}
fs.copyFileSync(
'./node_modules/font-awesome/css/font-awesome.min.css', './node_modules/font-awesome/css/font-awesome.min.css',
'./public/css/vendor.min.css'); './public/css/vendor.min.css');
if (process.argv.includes('--gzip')) {
gzipFile('./public/css/vendor.min.css');
}
console.info('Bundled CSS'); console.info('Bundled CSS');
});
} }
function bundleJs() { function bundleJs() {
const browserify = require('browserify'); const browserify = require('browserify');
const external = [
'underscore',
'superagent',
'mousetrap',
'js-cookie',
'nprogress',
];
function writeJsBundle(b, path, message, compress) { function minifyJs(path) {
return require('terser').minify(
fs.readFileSync(path, 'utf-8'), {compress: {unused: false}}).code;
}
function writeJsBundle(b, path, compress, callback) {
let outputFile = fs.createWriteStream(path); let outputFile = fs.createWriteStream(path);
b.bundle().pipe(outputFile); b.bundle().pipe(outputFile);
outputFile.on('finish', function() { outputFile.on('finish', () => {
if (compress) { if (compress) {
writeFile(path, minifyJs(path)); fs.writeFileSync(path, minifyJs(path));
} }
console.info(message); callback();
}); });
} }
glob('./js/**/*.js', {}, (er, files) => {
if (!process.argv.includes('--no-vendor-js')) { if (!process.argv.includes('--no-vendor-js')) {
let b = browserify(); let b = browserify();
for (let lib of external) { for (let lib of external_js) {
b.require(lib); b.require(lib);
} }
if (!process.argv.includes('--no-transpile')) { if (!process.argv.includes('--no-transpile')) {
b.add(require.resolve('babel-polyfill')); b.add(require.resolve('babel-polyfill'));
} }
writeJsBundle( const file = './public/js/vendor.min.js';
b, './public/js/vendor.min.js', 'Bundled vendor JS', true); writeJsBundle(b, file, true, () => {
if (process.argv.includes('--gzip')) {
gzipFile(file);
}
console.info('Bundled vendor JS');
});
} }
if (!process.argv.includes('--no-app-js')) { if (!process.argv.includes('--no-app-js')) {
let outputFile = fs.createWriteStream('./public/js/app.min.js');
let b = browserify({debug: process.argv.includes('--debug')}); let b = browserify({debug: process.argv.includes('--debug')});
if (!process.argv.includes('--no-transpile')) { if (!process.argv.includes('--no-transpile')) {
b = b.transform('babelify'); b = b.transform('babelify');
} }
writeJsBundle( b = b.external(external_js).add(glob.sync('./js/**/*.js'));
b.external(external).add(files), const compress = !process.argv.includes('--debug');
'./public/js/app.min.js', const file = './public/js/app.min.js';
'Bundled app JS', writeJsBundle(b, file, compress, () => {
!process.argv.includes('--debug')); if (process.argv.includes('--gzip')) {
gzipFile(file);
} }
console.info('Bundled app JS');
}); });
} }
}
function bundleConfig(config) { function bundleConfig() {
writeFile( function getVersion() {
'./js/.config.autogen.json', JSON.stringify(config)); let build_info = process.env.BUILD_INFO;
glob('./node_modules/font-awesome/fonts/*.*', {}, (er, files) => { if (!build_info) {
for (let file of files) { try {
if (fs.lstatSync(file).isDirectory()) { build_info = execSync('git describe --always --dirty --long --tags').toString();
continue; } catch (e) {
console.warn('Cannot find build version');
build_info = 'unknown';
} }
copyFile(file, path.join('./public/fonts/', path.basename(file)));
} }
}); return build_info.trim();
}
const config = {
meta: {
version: getVersion(),
buildDate: new Date().toUTCString()
}
};
fs.writeFileSync('./js/.config.autogen.json', JSON.stringify(config));
} }
function bundleBinaryAssets() { function bundleBinaryAssets() {
copyFile('./img/favicon.png', './public/img/favicon.png'); fs.copyFileSync('./img/favicon.png', './public/img/favicon.png');
copyFile('./img/transparency_grid.png', './public/img/transparency_grid.png'); fs.copyFileSync('./img/transparency_grid.png', './public/img/transparency_grid.png');
for (let file of glob.sync('./node_modules/font-awesome/fonts/*.*')) {
if (fs.lstatSync(file).isDirectory()) {
continue;
}
fs.copyFileSync(file, path.join('./public/fonts/', path.basename(file)));
}
if (process.argv.includes('--gzip')) {
for (let file of glob.sync('./public/fonts/*.*')) {
if (file.endsWith('woff2')) {
continue;
}
gzipFile(file);
}
}
console.info('Copied Fonts')
}
function bundleWebAppFiles() {
const Jimp = require('jimp'); const Jimp = require('jimp');
for (let icon of [ fs.copyFileSync('./app/manifest.json', './public/manifest.json');
{name: 'android-chrome-192x192.png', size: 192},
{name: 'android-chrome-512x512.png', size: 512}, Promise.all(webapp_icons.map(icon => {
{name: 'apple-touch-icon.png', size: 180}, return Jimp.read('./img/app.png')
{name: 'mstile-150x150.png', size: 150} .then(file => {
]) { file.resize(icon.size, Jimp.AUTO, Jimp.RESIZE_BEZIER)
Jimp.read('./img/app.png', (err, infile) => {
infile
.resize(icon.size, Jimp.AUTO, Jimp.RESIZE_BEZIER)
.write(path.join('./public/img/', icon.name)); .write(path.join('./public/img/', icon.name));
}); });
} }))
.then(() => {
console.info('Generated webapp icons'); console.info('Generated webapp icons');
});
for (let dim of [ Promise.all(webapp_splash_screens.map(dim => {
{w: 640, h: 1136, center: 320}, return Jimp.read('./img/splash.png')
{w: 750, h: 1294, center: 375}, .then(file => {
{w: 1125, h: 2436, center: 565}, file.resize(dim.center, Jimp.AUTO, Jimp.RESIZE_BEZIER)
{w: 1242, h: 2148, center: 625},
{w: 1536, h: 2048, center: 770},
{w: 1668, h: 2224, center: 820},
{w: 2048, h: 2732, center: 1024}
]) {
Jimp.read('./img/splash.png', (err, infile) => {
infile
.resize(dim.center, Jimp.AUTO, Jimp.RESIZE_BEZIER)
.background(0xFFFFFFFF) .background(0xFFFFFFFF)
.contain(dim.w, dim.center, .contain(dim.w, dim.center,
Jimp.HORIZONTAL_ALIGN_CENTER | Jimp.VERTICAL_ALIGN_MIDDLE) Jimp.HORIZONTAL_ALIGN_CENTER | Jimp.VERTICAL_ALIGN_MIDDLE)
.contain(dim.w, dim.h, .contain(dim.w, dim.h,
Jimp.HORIZONTAL_ALIGN_CENTER | Jimp.VERTICAL_ALIGN_MIDDLE) Jimp.HORIZONTAL_ALIGN_CENTER | Jimp.VERTICAL_ALIGN_MIDDLE)
.write(path.join('./public/img/', .write(path.join('./public/img/',
'apple-touch-startup-image-' 'apple-touch-startup-image-' + dim.w + 'x' + dim.h + '.png'));
+ dim.w + '-' + dim.h + '.png')); });
}))
.then(() => {
console.info('Generated splash screens');
}); });
} }
console.info('Generated splash screens');
}
function bundleWebAppFiles() { // -------------------------------------------------
copyFile('./app/manifest.json', './public/manifest.json');
}
const config = getConfig(); bundleConfig();
bundleConfig(config);
bundleBinaryAssets(); bundleBinaryAssets();
bundleWebAppFiles(); bundleWebAppFiles();
if (!process.argv.includes('--no-html')) { if (!process.argv.includes('--no-html')) {

View file

@ -6,7 +6,7 @@
font-family: 'Open Sans'; font-family: 'Open Sans';
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
src: local('Open Sans'), local('OpenSans'), url(fonts/open_sans.woff2) format('woff2'); src: local('Open Sans'), local('OpenSans'), url(../fonts/open_sans.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000; unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
/* make <body> cover entire viewport */ /* make <body> cover entire viewport */

View file

@ -1,4 +1,5 @@
worker_processes 1; worker_processes 1;
user nginx;
error_log /dev/stderr warn; error_log /dev/stderr warn;
pid /var/run/nginx.pid; pid /var/run/nginx.pid;
@ -54,3 +55,4 @@ http {
} }
} }
} }
daemon off;