From a5a06bf2d10b2a2e16ae3e91cf119c56cb050532 Mon Sep 17 00:00:00 2001 From: Shyam Sunder Date: Mon, 3 Sep 2018 17:36:06 -0400 Subject: [PATCH] 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 --- client/Dockerfile | 7 +- client/build.js | 407 +++++++++++++++++++---------------- client/css/core-general.styl | 2 +- client/nginx.conf.docker | 2 + 4 files changed, 222 insertions(+), 196 deletions(-) mode change 100644 => 100755 client/build.js diff --git a/client/Dockerfile b/client/Dockerfile index 69555a4..a3d0ffc 100644 --- a/client/Dockerfile +++ b/client/Dockerfile @@ -8,10 +8,7 @@ COPY . ./ ARG BUILD_INFO="docker-latest" ARG CLIENT_BUILD_ARGS="" -RUN BASE_URL="__BASEURL__" node build.js ${CLIENT_BUILD_ARGS} - -RUN find public/ -name public/index.html -prune -o -type f -size +5k \ - -print0 | xargs -0 -- gzip -6 -k +RUN BASE_URL="__BASEURL__" node build.js --gzip ${CLIENT_BUILD_ARGS} FROM nginx:alpine @@ -23,7 +20,7 @@ RUN \ echo 'sed -i "s|__BACKEND__|${BACKEND_HOST}|" /etc/nginx/nginx.conf' \ >> /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 CMD ["/init"] diff --git a/client/build.js b/client/build.js old mode 100644 new mode 100755 index fdc5fd0..dd63a5f --- a/client/build.js +++ b/client/build.js @@ -1,5 +1,35 @@ +#!/usr/bin/env node '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 glob = require('glob'); const path = require('path'); @@ -10,227 +40,224 @@ function readTextFile(path) { return fs.readFileSync(path, 'utf-8'); } -function writeFile(path, content) { - return fs.writeFileSync(path, content); +function gzipFile(file) { + 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'; +// ------------------------------------------------- + +function bundleHtml() { + const underscore = require('underscore'); + const babelify = require('babelify'); + + const baseUrl = process.env.BASE_URL ? process.env.BASE_URL : '/'; + + function minifyHtml(html) { + return require('html-minifier').minify(html, { + removeComments: true, + collapseWhitespace: true, + conservativeCollapse: true, + }).trim(); + } + + const baseHtml = readTextFile('./html/index.htm') + .replace('', ``); + fs.writeFileSync('./public/index.htm', minifyHtml(baseHtml)); + + let compiledTemplateJs = [ + `'use strict';`, + `let _ = require('underscore');`, + `let templates = {};` + ]; + + for (const file of glob.sync('./html/**/*.tpl')) { + const name = path.basename(file, '.tpl').replace(/_/g, '-'); + const placeholders = []; + let templateText = readTextFile(file); + templateText = templateText.replace( + /<%.*?%>/ig, + (match) => { + const ret = '%%%TEMPLATE' + placeholders.length; + placeholders.push(match); + return ret; + }); + templateText = minifyHtml(templateText); + templateText = templateText.replace( + /%%%TEMPLATE(\d+)/g, + (match, number) => { return placeholders[number]; }); + + const functionText = underscore.template( + templateText, {variable: 'ctx'}).source; + + compiledTemplateJs.push(`templates['${name}'] = ${functionText};`); + } + compiledTemplateJs.push('module.exports = templates;'); + + fs.writeFileSync('./js/.templates.autogen.js', compiledTemplateJs.join('\n')); + console.info('Bundled HTML'); +} + +function bundleCss() { + const stylus = require('stylus'); + + 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', + './public/css/vendor.min.css'); + if (process.argv.includes('--gzip')) { + gzipFile('./public/css/vendor.min.css'); + } + + console.info('Bundled CSS'); +} + +function bundleJs() { + const browserify = require('browserify'); + + 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); + b.bundle().pipe(outputFile); + outputFile.on('finish', () => { + if (compress) { + fs.writeFileSync(path, minifyJs(path)); + } + callback(); + }); + } + + if (!process.argv.includes('--no-vendor-js')) { + let b = browserify(); + for (let lib of external_js) { + b.require(lib); } - return build_info.trim(); + if (!process.argv.includes('--no-transpile')) { + b.add(require.resolve('babel-polyfill')); + } + const file = './public/js/vendor.min.js'; + writeJsBundle(b, file, true, () => { + if (process.argv.includes('--gzip')) { + gzipFile(file); + } + console.info('Bundled vendor JS'); + }); + } + + if (!process.argv.includes('--no-app-js')) { + let b = browserify({debug: process.argv.includes('--debug')}); + if (!process.argv.includes('--no-transpile')) { + b = b.transform('babelify'); + } + b = b.external(external_js).add(glob.sync('./js/**/*.js')); + const compress = !process.argv.includes('--debug'); + const file = './public/js/app.min.js'; + writeJsBundle(b, file, compress, () => { + if (process.argv.includes('--gzip')) { + gzipFile(file); + } + console.info('Bundled app JS'); + }); } } -function getConfig() { - let config = { +function bundleConfig() { + function getVersion() { + let build_info = process.env.BUILD_INFO; + if (!build_info) { + try { + build_info = execSync('git describe --always --dirty --long --tags').toString(); + } catch (e) { + console.warn('Cannot find build version'); + build_info = 'unknown'; + } + } + return build_info.trim(); + } + const config = { meta: { version: getVersion(), buildDate: new Date().toUTCString() } }; - return config; -} - -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) { - return require('html-minifier').minify(html, { - removeComments: true, - collapseWhitespace: true, - conservativeCollapse: true, - }).trim(); -} - -function bundleHtml() { - const underscore = require('underscore'); - const babelify = require('babelify'); - const baseHtml = readTextFile('./html/index.htm', 'utf-8'); - const baseUrl = process.env.BASE_URL ? process.env.BASE_URL : '/'; - const finalHtml = baseHtml.replace( - '', ``); - writeFile('./public/index.htm', minifyHtml(finalHtml)); - - glob('./html/**/*.tpl', {}, (er, files) => { - let compiledTemplateJs = '\'use strict\'\n'; - compiledTemplateJs += 'let _ = require(\'underscore\');'; - compiledTemplateJs += 'let templates = {};'; - for (const file of files) { - const name = path.basename(file, '.tpl').replace(/_/g, '-'); - const placeholders = []; - let templateText = readTextFile(file, 'utf-8'); - templateText = templateText.replace( - /<%.*?%>/ig, - (match) => { - const ret = '%%%TEMPLATE' + placeholders.length; - placeholders.push(match); - return ret; - }); - templateText = minifyHtml(templateText); - templateText = templateText.replace( - /%%%TEMPLATE(\d+)/g, - (match, number) => { return placeholders[number]; }); - - const functionText = underscore.template( - templateText, {variable: 'ctx'}).source; - compiledTemplateJs += `templates['${name}'] = ${functionText};`; - } - compiledTemplateJs += 'module.exports = templates;'; - writeFile('./js/.templates.autogen.js', compiledTemplateJs); - console.info('Bundled HTML'); - }); -} - -function bundleCss() { - 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( - './node_modules/font-awesome/css/font-awesome.min.css', - './public/css/vendor.min.css'); - - console.info('Bundled CSS'); - }); -} - -function bundleJs() { - const browserify = require('browserify'); - const external = [ - 'underscore', - 'superagent', - 'mousetrap', - 'js-cookie', - 'nprogress', - ]; - - function writeJsBundle(b, path, message, compress) { - let outputFile = fs.createWriteStream(path); - b.bundle().pipe(outputFile); - outputFile.on('finish', function() { - if (compress) { - writeFile(path, minifyJs(path)); - } - console.info(message); - }); - } - - glob('./js/**/*.js', {}, (er, files) => { - if (!process.argv.includes('--no-vendor-js')) { - let b = browserify(); - for (let lib of external) { - b.require(lib); - } - if (!process.argv.includes('--no-transpile')) { - b.add(require.resolve('babel-polyfill')); - } - writeJsBundle( - b, './public/js/vendor.min.js', 'Bundled vendor JS', true); - } - - if (!process.argv.includes('--no-app-js')) { - let outputFile = fs.createWriteStream('./public/js/app.min.js'); - let b = browserify({debug: process.argv.includes('--debug')}); - if (!process.argv.includes('--no-transpile')) { - b = b.transform('babelify'); - } - writeJsBundle( - b.external(external).add(files), - './public/js/app.min.js', - 'Bundled app JS', - !process.argv.includes('--debug')); - } - }); -} - -function bundleConfig(config) { - writeFile( - './js/.config.autogen.json', JSON.stringify(config)); - glob('./node_modules/font-awesome/fonts/*.*', {}, (er, files) => { - for (let file of files) { - if (fs.lstatSync(file).isDirectory()) { - continue; - } - copyFile(file, path.join('./public/fonts/', path.basename(file))); - } - }); + fs.writeFileSync('./js/.config.autogen.json', JSON.stringify(config)); } function bundleBinaryAssets() { - copyFile('./img/favicon.png', './public/img/favicon.png'); - copyFile('./img/transparency_grid.png', './public/img/transparency_grid.png'); + fs.copyFileSync('./img/favicon.png', './public/img/favicon.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'); - for (let icon of [ - {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} - ]) { - Jimp.read('./img/app.png', (err, infile) => { - infile - .resize(icon.size, Jimp.AUTO, Jimp.RESIZE_BEZIER) + fs.copyFileSync('./app/manifest.json', './public/manifest.json'); + + Promise.all(webapp_icons.map(icon => { + return Jimp.read('./img/app.png') + .then(file => { + file.resize(icon.size, Jimp.AUTO, Jimp.RESIZE_BEZIER) .write(path.join('./public/img/', icon.name)); }); - } - console.info('Generated webapp icons'); + })) + .then(() => { + console.info('Generated webapp icons'); + }); - for (let dim of [ - {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} - ]) { - Jimp.read('./img/splash.png', (err, infile) => { - infile - .resize(dim.center, Jimp.AUTO, Jimp.RESIZE_BEZIER) + Promise.all(webapp_splash_screens.map(dim => { + return Jimp.read('./img/splash.png') + .then(file => { + file.resize(dim.center, Jimp.AUTO, Jimp.RESIZE_BEZIER) .background(0xFFFFFFFF) .contain(dim.w, dim.center, Jimp.HORIZONTAL_ALIGN_CENTER | Jimp.VERTICAL_ALIGN_MIDDLE) .contain(dim.w, dim.h, Jimp.HORIZONTAL_ALIGN_CENTER | Jimp.VERTICAL_ALIGN_MIDDLE) .write(path.join('./public/img/', - 'apple-touch-startup-image-' - + dim.w + '-' + dim.h + '.png')); + 'apple-touch-startup-image-' + dim.w + 'x' + dim.h + '.png')); }); - } - console.info('Generated splash screens'); + })) + .then(() => { + console.info('Generated splash screens'); + }); } -function bundleWebAppFiles() { - copyFile('./app/manifest.json', './public/manifest.json'); -} +// ------------------------------------------------- -const config = getConfig(); -bundleConfig(config); +bundleConfig(); bundleBinaryAssets(); bundleWebAppFiles(); if (!process.argv.includes('--no-html')) { diff --git a/client/css/core-general.styl b/client/css/core-general.styl index f6d1382..df30782 100644 --- a/client/css/core-general.styl +++ b/client/css/core-general.styl @@ -6,7 +6,7 @@ font-family: 'Open Sans'; font-style: normal; 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; /* make cover entire viewport */ diff --git a/client/nginx.conf.docker b/client/nginx.conf.docker index 6af4095..c9d77ea 100644 --- a/client/nginx.conf.docker +++ b/client/nginx.conf.docker @@ -1,4 +1,5 @@ worker_processes 1; +user nginx; error_log /dev/stderr warn; pid /var/run/nginx.pid; @@ -54,3 +55,4 @@ http { } } } +daemon off;