From 512b07963b912b973212c14efb7d6250dcd3b81c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 2 Aug 2017 21:31:04 -0700 Subject: [PATCH] Add build using polymer-build (#344) * Add build using polymer-build * Use bundle strategies to tweak stripExcludes * Only vulcanize hass.io panel * Rename hassio panel generate script * Remove hydrolysis * Get it all somewhat working * Fixes * Allow ES2015 + fix minify JS * Clarify we need to fix service worker minify * Move service worker template out of tasks folder * Fix broken CSS * Wrap it up * Fix maps --- .travis.yml | 2 +- gulp/config.js | 7 + {script => gulp}/service-worker.js.tmpl | 0 gulp/tasks/build.js | 107 + gulp/tasks/clean.js | 6 + gulp/tasks/default.js | 9 + gulp/tasks/gen-service-worker.js | 110 + .../tasks/hassio-panel.js | 66 +- gulp/tasks/rollup.js | 29 + gulpfile.babel.js | 32 +- package.json | 47 +- panels/hassio/todo.md | 13 - panels/map/ha-panel-map.html | 17 +- polymer.json | 8 +- rollup.config.js | 18 +- script/gen-service-worker.js | 100 - src/cards/ha-introduction-card.html | 2 +- src/entrypoint.html | 1 + yarn.lock | 3052 ++++++++++++----- 19 files changed, 2562 insertions(+), 1064 deletions(-) create mode 100644 gulp/config.js rename {script => gulp}/service-worker.js.tmpl (100%) create mode 100644 gulp/tasks/build.js create mode 100644 gulp/tasks/clean.js create mode 100644 gulp/tasks/default.js create mode 100755 gulp/tasks/gen-service-worker.js rename script/vulcanize.js => gulp/tasks/hassio-panel.js (53%) create mode 100644 gulp/tasks/rollup.js delete mode 100644 panels/hassio/todo.md delete mode 100755 script/gen-service-worker.js create mode 100644 src/entrypoint.html diff --git a/.travis.yml b/.travis.yml index 22ce3a7c66..22d870d277 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,7 +15,7 @@ addons: packages: - google-chrome-stable script: - - npm run js_prod + - npm run build - npm run test - xvfb-run wct - if [ "${TRAVIS_PULL_REQUEST}" = "false" ]; then wct --plugin sauce; fi diff --git a/gulp/config.js b/gulp/config.js new file mode 100644 index 0000000000..ff4ef9a25f --- /dev/null +++ b/gulp/config.js @@ -0,0 +1,7 @@ +var path = require('path'); + +module.exports = { + static_dir: path.resolve(__dirname, '../..'), + polymer_dir: path.resolve(__dirname, '..'), + build_dir: path.resolve(__dirname, '../build'), +}; diff --git a/script/service-worker.js.tmpl b/gulp/service-worker.js.tmpl similarity index 100% rename from script/service-worker.js.tmpl rename to gulp/service-worker.js.tmpl diff --git a/gulp/tasks/build.js b/gulp/tasks/build.js new file mode 100644 index 0000000000..a20c03ef15 --- /dev/null +++ b/gulp/tasks/build.js @@ -0,0 +1,107 @@ +import buble from 'gulp-buble'; +import uglify from 'gulp-uglify'; +import { gulp as cssSlam } from 'css-slam'; +import gulp from 'gulp'; +import filter from 'gulp-filter'; +import htmlMinifier from 'gulp-html-minifier'; +import gulpif from 'gulp-if'; +import { PolymerProject, HtmlSplitter } from 'polymer-build'; +import { + composeStrategies, + generateShellMergeStrategy, +} from 'polymer-bundler'; +import mergeStream from 'merge-stream'; +import rename from 'gulp-rename'; + +import polymerConfig from '../../polymer'; + +function minifyStream(stream) { + const sourcesHtmlSplitter = new HtmlSplitter(); + return stream + .pipe(sourcesHtmlSplitter.split()) + .pipe(gulpif(/\.js$/, buble())) + .pipe(gulpif(/\.js$/, uglify({ sourceMap : false }))) + .pipe(gulpif(/\.css$/, cssSlam())) + .pipe(gulpif(/\.html$/, cssSlam())) + .pipe(gulpif(/\.html$/, htmlMinifier({ + collapseWhitespace: true, + removeComments: true + }))) + .pipe(sourcesHtmlSplitter.rejoin()); +} + +function renamePanel(path) { + // Rename panels to be panels/* and not their subdir + if (path.basename.substr(0, 9) === 'ha-panel-' && path.extname === '.html') { + path.dirname = 'panels/'; + } + + // Rename frontend + if (path.dirname === 'src' && path.basename === 'home-assistant' && + path.extname === '.html') { + path.dirname = ''; + path.basename = 'frontend'; + } +} + +/** + * Polymer build strategy to strip imports, even if explictely imported + */ +function generateStripStrategy(urls) { + return (bundles) => { + for (const bundle of bundles) { + for (const url of urls) { + bundle.stripImports.add(url); + } + } + return bundles; + }; +} + +/** + * Polymer build strategy to strip everything but the entrypoints + * for bundles that match a specific entry point. + */ +function stripAllButEntrypoint(entryPoint) { + return (bundles) => { + for (const bundle of bundles) { + if (bundle.entrypoints.size === 1 && + bundle.entrypoints.has(entryPoint)) { + for (const file of bundle.files) { + if (!bundle.entrypoints.has(file)) { + bundle.stripImports.add(file); + } + } + } + } + return bundles; + }; +} + + +gulp.task('build', ['ru_all'], () => { + const strategy = composeStrategies([ + generateShellMergeStrategy(polymerConfig.shell), + generateStripStrategy([ + 'bower_components/font-roboto/roboto.html', + 'bower_components/paper-styles/color.html', + ]), + stripAllButEntrypoint('panels/hassio/ha-panel-hassio.html') + ]); + const project = new PolymerProject(polymerConfig); + + return mergeStream(minifyStream(project.sources()), + minifyStream(project.dependencies())) + .pipe(project.bundler({ + strategy, + strip: true, + sourcemaps: false, + stripComments: true, + inlineScripts: true, + inlineCss: true, + implicitStrip: true, + })) + .pipe(rename(renamePanel)) + .pipe(filter(['**', '!src/entrypoint.html'])) + .pipe(gulp.dest('build/')); +}); diff --git a/gulp/tasks/clean.js b/gulp/tasks/clean.js new file mode 100644 index 0000000000..5110b23a0c --- /dev/null +++ b/gulp/tasks/clean.js @@ -0,0 +1,6 @@ +import del from 'del'; +import gulp from 'gulp'; + +gulp.task('clean', () => { + return del(['build', 'build-temp']); +}); diff --git a/gulp/tasks/default.js b/gulp/tasks/default.js new file mode 100644 index 0000000000..3c0cce5f35 --- /dev/null +++ b/gulp/tasks/default.js @@ -0,0 +1,9 @@ +import gulp from 'gulp'; +import runSequence from 'run-sequence'; + +gulp.task('default', () => { + return runSequence.use(gulp)( + 'clean', + 'build', + ); +}); diff --git a/gulp/tasks/gen-service-worker.js b/gulp/tasks/gen-service-worker.js new file mode 100755 index 0000000000..55db581769 --- /dev/null +++ b/gulp/tasks/gen-service-worker.js @@ -0,0 +1,110 @@ +/* +Generate a caching service worker for HA + +Will be called as part of build_frontend. + +Expects home-assistant-polymer repo as submodule of HA repo. +Creates a caching service worker based on the CURRENT content of HA repo. +Output service worker to build/service_worker.js + +TODO: + - Use gulp streams + - Fix minifying the stream +*/ +var gulp = require('gulp'); +var crypto = require('crypto'); +var fs = require('fs'); +var path = require('path'); +var swPrecache = require('sw-precache'); +var uglifyJS = require('uglify-js'); + +const config = require('../config'); + +const DEV = !!JSON.parse(process.env.BUILD_DEV || 'true'); + +var rootDir = config.static_dir; +var panelDir = path.resolve(rootDir, 'panels'); + +var dynamicUrlToDependencies = { + '/': [ + rootDir + '/frontend.html', + rootDir + '/core.js', + rootDir + '/compatibility.js', + ], +}; + +var staticFingerprinted = [ + 'frontend.html', + 'mdi.html', + 'core.js', + 'compatibility.js', +]; + +// These panels will always be registered inside HA and thus can +// be safely assumed to be able to preload. +var panelsFingerprinted = [ + 'map', 'dev-event', 'dev-info', 'dev-service', 'dev-state', 'dev-template', + 'dev-mqtt', +]; + +function md5(filename) { + return crypto.createHash('md5') + .update(fs.readFileSync(filename)).digest('hex'); +} + +gulp.task('gen-service-worker', () => { + // Create fingerprinted versions of our dependencies. + staticFingerprinted.forEach(fn => { + var parts = path.parse(fn); + var hash = md5(rootDir + '/' + parts.name + parts.ext); + var url = '/static/' + parts.name + '-' + hash + parts.ext; + var fpath = rootDir + '/' + parts.name + parts.ext; + dynamicUrlToDependencies[url] = [fpath]; + }); + + panelsFingerprinted.forEach(panel => { + var fpath = panelDir + '/ha-panel-' + panel + '.html'; + var hash = md5(fpath); + var url = '/frontend/panels/' + panel + '-' + hash + '.html'; + dynamicUrlToDependencies[url] = [fpath]; + }); + + var options = { + navigateFallback: '/', + navigateFallbackWhitelist: [/^((?!(static|api|local|service_worker.js|manifest.json)).)*$/], + dynamicUrlToDependencies: dynamicUrlToDependencies, + staticFileGlobs: [ + rootDir + '/icons/favicon.ico', + rootDir + '/icons/favicon-192x192.png', + rootDir + '/webcomponents-lite.min.js', + rootDir + '/fonts/roboto/Roboto-Light.ttf', + rootDir + '/fonts/roboto/Roboto-Medium.ttf', + rootDir + '/fonts/roboto/Roboto-Regular.ttf', + rootDir + '/fonts/roboto/Roboto-Bold.ttf', + rootDir + '/images/card_media_player_bg.png', + ], + stripPrefix: '..', + replacePrefix: 'static', + verbose: true, + }; + + var devBase = 'console.warn("Service worker caching disabled in development")'; + + var swHass = fs.readFileSync(path.resolve(__dirname, '../service-worker.js.tmpl'), 'UTF-8'); + + var genPromise = DEV ? Promise.resolve(devBase) : swPrecache.generate(options); + + genPromise = genPromise.then(swString => swString + '\n' + swHass); + + // Fix this + // if (!DEV) { + // genPromise = genPromise.then( + // swString => uglifyJS.minify(swString, { fromString: true }).code); + // } + + genPromise.then(swString => { + fs.writeFileSync(path.resolve(config.build_dir, 'service_worker.js'), swString); + }); + + return genPromise; +}); diff --git a/script/vulcanize.js b/gulp/tasks/hassio-panel.js similarity index 53% rename from script/vulcanize.js rename to gulp/tasks/hassio-panel.js index 4ccec5bbdc..df3dc096c9 100755 --- a/script/vulcanize.js +++ b/gulp/tasks/hassio-panel.js @@ -1,17 +1,13 @@ -#!/usr/bin/env node - +/* +TODO: + - Use gulp streams + - Use polymer bundler to vulcanize +*/ +var gulp = require('gulp'); var Vulcanize = require('vulcanize'); var minify = require('html-minifier'); -var hyd = require('hydrolysis'); var fs = require('fs'); -if (!fs.existsSync('build')) { - fs.mkdirSync('build'); -} -if (!fs.existsSync('build/panels')) { - fs.mkdirSync('build/panels'); -} - function minifyHTML(html) { return minify.minify(html, { customAttrAssign: [/\$=/], @@ -33,30 +29,12 @@ const baseVulcanOptions = { stripComments: true, }; -const panelVulcan = new Vulcanize({ - inlineScripts: true, - inlineCss: true, - implicitStrip: true, - stripComments: true, - stripExcludes: [ - 'panels/hassio/hassio-main.html' - ], -}); - const baseExcludes = [ 'bower_components/font-roboto/roboto.html', 'bower_components/paper-styles/color.html', ]; const toProcess = [ - // This is the main entry point - { - source: './src/home-assistant.html', - output: './build/frontend.html', - vulcan: new Vulcanize(Object.assign({}, baseVulcanOptions, { - stripExcludes: baseExcludes, - })), - }, // This is the Hass.io configuration panel // It's build standalone because it is embedded in the supervisor. { @@ -71,14 +49,6 @@ const toProcess = [ }, ]; -fs.readdirSync('./panels').forEach((panel) => { - toProcess.push({ - source: `./panels/${panel}/ha-panel-${panel}.html`, - output: `./build/panels/ha-panel-${panel}.html`, - vulcan: panelVulcan, - }); -}); - function vulcanizeEntry(entry) { return new Promise((resolve, reject) => { console.log('Processing', entry.source); @@ -95,16 +65,14 @@ function vulcanizeEntry(entry) { }); } -// Fetch all dependencies of main app and exclude them from panels -hyd.Analyzer.analyze('src/home-assistant.html') - .then(function (analyzer) { - return analyzer._getDependencies('src/home-assistant.html'); - }) - .then((deps) => { - panelVulcan.stripExcludes = panelVulcan.stripExcludes.concat(deps); - }) - // Chain all vulcanizing work as promises - .then(() => toProcess.reduce( - (p, entry) => p.then(() => vulcanizeEntry(entry)), - Promise.resolve())) - .catch(err => console.error('Something went wrong!', err)); +gulp.task('hassio-panel', () => { + if (!fs.existsSync('build-temp')) { + fs.mkdirSync('build-temp'); + } + + toProcess.reduce( + (p, entry) => p.then(() => vulcanizeEntry(entry)), + Promise.resolve()); + + return toProcess; +}); diff --git a/gulp/tasks/rollup.js b/gulp/tasks/rollup.js new file mode 100644 index 0000000000..f6b449e5ef --- /dev/null +++ b/gulp/tasks/rollup.js @@ -0,0 +1,29 @@ +import gulp from 'gulp'; +import rollupEach from 'gulp-rollup-each'; +import rollupConfig from '../../rollup.config'; + +gulp.task('run_rollup', () => { + return gulp.src([ + 'js/core.js', + 'js/compatibility.js', + 'js/editor/editor.js', + 'demo_data/demo_data.js', + ]) + .pipe(rollupEach(rollupConfig)) + .pipe(gulp.dest('build-temp')); +}); + +gulp.task('ru_all', ['run_rollup'], () => { + gulp.src([ + 'build-temp/core.js', + 'build-temp/compatibility.js', + ]) + .pipe(gulp.dest('build/')); +}); + +gulp.task('watch_ru_all', ['ru_all'], () => { + gulp.watch([ + 'js/**/*.js', + 'demo_data/**/*.js' + ], ['ru_all']); +}); diff --git a/gulpfile.babel.js b/gulpfile.babel.js index 54e9de899c..f3cb47a7e6 100644 --- a/gulpfile.babel.js +++ b/gulpfile.babel.js @@ -1,31 +1,3 @@ -'use strict'; +var requireDir = require('require-dir'); -import gulp from 'gulp'; -import rollupEach from 'gulp-rollup-each'; -import rollupConfig from './rollup.config'; - -gulp.task('run_rollup', () => { - return gulp.src([ - 'js/core.js', - 'js/compatibility.js', - 'js/editor/editor.js', - 'demo_data/demo_data.js', - ]) - .pipe(rollupEach(rollupConfig)) - .pipe(gulp.dest('build-temp')); -}); - -gulp.task('ru_all', ['run_rollup'], () => { - gulp.src([ - 'build-temp/core.js', - 'build-temp/compatibility.js', - ]) - .pipe(gulp.dest('build/')); -}); - -gulp.task('watch_ru_all', ['ru_all'], () => { - gulp.watch([ - 'js/**/*.js', - 'demo_data/**/*.js' - ], ['ru_all']); -}); +requireDir('./gulp/tasks/'); diff --git a/package.json b/package.json index 786227750b..4ed8b33fc1 100644 --- a/package.json +++ b/package.json @@ -9,16 +9,11 @@ "scripts": { "clean": "rm -rf build/* build-temp/*", "gulp": "gulp", - "js_dev": "node script/gen-service-worker.js && npm run gulp watch_ru_all", - "js_dev_demo": "BUILD_DEMO=1 npm run gulp watch_ru_all", - "js_prod": "BUILD_DEV=0 npm run gulp ru_all", - "js_demo": "BUILD_DEV=0 BUILD_DEMO=1 npm run gulp ru_all", - "frontend_html": "node script/vulcanize.js", - "frontend_prod": "npm run js_prod && npm run frontend_html", - "frontend_demo": "npm run js_demo && npm run frontend_html", - "ru_all": "npm run gulp ru_all", - "watch_ru_all": "npm run gulp watch_ru_all", - "lint_js": "eslint src panels preact-src --ext js,html", + "build": "BUILD_DEV=0 gulp", + "build_demo": "BUILD_DEV=0 BUILD_DEMO=1 gulp", + "dev": "npm run gulp ru_all", + "dev-watch": "npm run gulp watch_ru_all", + "lint_js": "eslint src panels js --ext js,html", "lint_html": "ls -1 src/home-assistant.html panels/**/ha-panel-*.html | xargs polymer lint --input", "test": "npm run lint_js && npm run lint_html" }, @@ -33,29 +28,43 @@ "babel-plugin-external-helpers": "^6.22.0", "babel-plugin-transform-object-rest-spread": "^6.23.0", "babel-plugin-transform-react-jsx": "^6.24.1", + "babel-preset-babili": "^0.1.4", "babel-preset-es2015": "^6.24.1", "bower": "^1.8.0", + "css-slam": "^1.2.1", + "del": "^3.0.0", "eslint": "^3.19.0", "eslint-config-airbnb-base": "^11.1.3", "eslint-plugin-html": "^2.0.1", "eslint-plugin-import": "^2.2.0", "eslint-plugin-react": "^7.0.0", "gulp": "^3.9.1", + "gulp-babel": "^6.1.2", + "gulp-buble": "^0.8.0", + "gulp-filter": "^5.0.0", + "gulp-html-minifier": "^0.1.8", + "gulp-if": "^2.0.2", + "gulp-rename": "^1.2.2", "gulp-rollup-each": "^1.0.2", - "html-minifier": "^3.4.3", - "hydrolysis": "^1.24.1", - "polymer-cli": "^0.17.0", + "gulp-uglify": "^3.0.0", + "html-minifier": "^3.5.3", + "merge-stream": "^1.0.1", + "polymer-build": "^2.0.0", + "polymer-bundler": "^3.0.1", + "polymer-cli": "^1.3.1", "polymer-lint": "^0.8.3", - "rollup": "^0.41.6", + "require-dir": "^0.3.2", + "rollup": "^0.45.2", "rollup-plugin-babel": "^2.7.1", "rollup-plugin-commonjs": "^8.0.2", "rollup-plugin-node-resolve": "^3.0.0", "rollup-plugin-replace": "^1.1.1", - "rollup-plugin-uglify": "^1.0.1", - "rollup-watch": "^3.2.2", - "sw-precache": "^5.1.0", - "uglify-js": "^2.8.22", - "vulcanize": "^1.15.4", + "rollup-plugin-uglify": "^2.0.1", + "rollup-watch": "^4.3.1", + "run-sequence": "^2.1.0", + "sw-precache": "^5.2.0", + "uglify-js": "^3.0.27", + "vulcanize": "^1.16.0", "web-component-tester": "^5.0.1" } } diff --git a/panels/hassio/todo.md b/panels/hassio/todo.md deleted file mode 100644 index c534a0e23c..0000000000 --- a/panels/hassio/todo.md +++ /dev/null @@ -1,13 +0,0 @@ -Host: - - Use feature detection - -Supervisor: - - logs - - toggle beta channel (/options) - -Network: - - show - - update - -Home Assistant: - - Logs diff --git a/panels/map/ha-panel-map.html b/panels/map/ha-panel-map.html index c95ac51d97..c64a43b932 100644 --- a/panels/map/ha-panel-map.html +++ b/panels/map/ha-panel-map.html @@ -8,13 +8,12 @@ + +