diff --git a/.gitignore b/.gitignore index f9a704e5d9..743889ba4e 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,6 @@ node_modules/* npm-debug.log .DS_Store hass_frontend/* -hass_frontend_es5/* .reify-cache demo/hademo-icons.html diff --git a/gulp/.eslintrc b/build-scripts/.eslintrc similarity index 100% rename from gulp/.eslintrc rename to build-scripts/.eslintrc diff --git a/config/.eslintrc.json b/build-scripts/.eslintrc.json similarity index 100% rename from config/.eslintrc.json rename to build-scripts/.eslintrc.json diff --git a/config/babel.js b/build-scripts/babel.js similarity index 100% rename from config/babel.js rename to build-scripts/babel.js diff --git a/build-scripts/gulp/clean.js b/build-scripts/gulp/clean.js new file mode 100644 index 0000000000..d3eaed526a --- /dev/null +++ b/build-scripts/gulp/clean.js @@ -0,0 +1,5 @@ +const del = require("del"); +const gulp = require("gulp"); +const config = require("../paths"); + +gulp.task("clean", () => del([config.root, config.build_dir])); diff --git a/build-scripts/gulp/develop.js b/build-scripts/gulp/develop.js new file mode 100644 index 0000000000..2dbcbaac45 --- /dev/null +++ b/build-scripts/gulp/develop.js @@ -0,0 +1,29 @@ +// Run HA develop mode +const gulp = require("gulp"); + +require("./clean.js"); +require("./translations.js"); +require("./gen-icons.js"); +require("./gather-static.js"); +require("./webpack.js"); +require("./service-worker.js"); +require("./entry-html.js"); + +gulp.task( + "develop", + gulp.series( + async function setEnv() { + process.env.NODE_ENV = "development"; + }, + "clean", + gulp.parallel( + "copy-static", + "gen-service-worker-dev", + "gen-icons", + "gen-pages-dev", + "gen-index-html-dev", + gulp.series("build-translations", "copy-translations") + ), + "webpack-watch" + ) +); diff --git a/build-scripts/gulp/entry-html.js b/build-scripts/gulp/entry-html.js new file mode 100644 index 0000000000..235a7336d5 --- /dev/null +++ b/build-scripts/gulp/entry-html.js @@ -0,0 +1,108 @@ +// Tasks to generate entry HTML +/* eslint-disable import/no-dynamic-require */ +/* eslint-disable global-require */ +const gulp = require("gulp"); +const fs = require("fs-extra"); +const path = require("path"); +const template = require("lodash.template"); +const minify = require("html-minifier").minify; +const config = require("../paths.js"); + +const templatePath = (tpl) => + path.resolve(config.polymer_dir, "src/html/", `${tpl}.html.template`); + +const readFile = (pth) => fs.readFileSync(pth).toString(); + +const renderTemplate = (pth, data = {}) => { + const compiled = template(readFile(templatePath(pth))); + return compiled({ ...data, renderTemplate }); +}; + +const minifyHtml = (content) => + minify(content, { + collapseWhitespace: true, + minifyJS: true, + minifyCSS: true, + removeComments: true, + }); + +const PAGES = ["onboarding", "authorize"]; + +gulp.task("gen-pages-dev", (done) => { + for (const page of PAGES) { + const content = renderTemplate(page, { + latestPageJS: `/frontend_latest/${page}.js`, + latestHassIconsJS: "/frontend_latest/hass-icons.js", + + es5Compatibility: "/frontend_es5/compatibility.js", + es5PageJS: `/frontend_es5/${page}.js`, + es5HassIconsJS: "/frontend_es5/hass-icons.js", + }); + + fs.outputFileSync(path.resolve(config.root, `${page}.html`), content); + } + done(); +}); + +gulp.task("gen-pages-prod", (done) => { + const latestManifest = require(path.resolve(config.output, "manifest.json")); + const es5Manifest = require(path.resolve(config.output_es5, "manifest.json")); + + for (const page of PAGES) { + const content = renderTemplate(page, { + latestPageJS: latestManifest[`${page}.js`], + latestHassIconsJS: latestManifest["hass-icons.js"], + + es5Compatibility: es5Manifest["compatibility.js"], + es5PageJS: es5Manifest[`${page}.js`], + es5HassIconsJS: es5Manifest["hass-icons.js"], + }); + + fs.outputFileSync( + path.resolve(config.root, `${page}.html`), + minifyHtml(content) + ); + } + done(); +}); + +gulp.task("gen-index-html-dev", (done) => { + // In dev mode we don't mangle names, so we hardcode urls. That way we can + // run webpack as last in watch mode, which blocks output. + const content = renderTemplate("index", { + latestAppJS: "/frontend_latest/app.js", + latestCoreJS: "/frontend_latest/core.js", + latestCustomPanelJS: "/frontend_latest/custom-panel.js", + latestHassIconsJS: "/frontend_latest/hass-icons.js", + + es5Compatibility: "/frontend_es5/compatibility.js", + es5AppJS: "/frontend_es5/app.js", + es5CoreJS: "/frontend_es5/core.js", + es5CustomPanelJS: "/frontend_es5/custom-panel.js", + es5HassIconsJS: "/frontend_es5/hass-icons.js", + }); + + fs.outputFileSync(path.resolve(config.root, "index.html"), content); + done(); +}); + +gulp.task("gen-index-html-prod", (done) => { + const latestManifest = require(path.resolve(config.output, "manifest.json")); + const es5Manifest = require(path.resolve(config.output_es5, "manifest.json")); + const content = renderTemplate("index", { + latestAppJS: latestManifest["app.js"], + latestCoreJS: latestManifest["core.js"], + latestCustomPanelJS: latestManifest["custom-panel.js"], + latestHassIconsJS: latestManifest["hass-icons.js"], + + es5Compatibility: es5Manifest["compatibility.js"], + es5AppJS: es5Manifest["app.js"], + es5CoreJS: es5Manifest["core.js"], + es5CustomPanelJS: es5Manifest["custom-panel.js"], + es5HassIconsJS: es5Manifest["hass-icons.js"], + }); + const minified = minifyHtml(content).replace(/#THEMEC/g, "{{ theme_color }}"); + + fs.outputFileSync(path.resolve(config.root, "index.html"), minified); + done(); +}); diff --git a/build-scripts/gulp/gather-static.js b/build-scripts/gulp/gather-static.js new file mode 100644 index 0000000000..733c0e876c --- /dev/null +++ b/build-scripts/gulp/gather-static.js @@ -0,0 +1,87 @@ +// Gulp task to gather all static files. + +const gulp = require("gulp"); +const path = require("path"); +const fs = require("fs-extra"); +const zopfli = require("gulp-zopfli-green"); +const merge = require("merge-stream"); +const config = require("../paths"); + +const npmPath = (...parts) => + path.resolve(config.polymer_dir, "node_modules", ...parts); +const polyPath = (...parts) => path.resolve(config.polymer_dir, ...parts); +const staticPath = (...parts) => path.resolve(config.root, "static", ...parts); + +const copyFileDir = (fromFile, toDir) => + fs.copySync(fromFile, path.join(toDir, path.basename(fromFile))); + +function copyTranslations() { + // Translation output + fs.copySync( + polyPath("build-translations/output"), + staticPath("translations") + ); +} + +function copyStatic() { + // Basic static files + fs.copySync(polyPath("public"), config.root); + + // Web Component polyfills and adapters + copyFileDir( + npmPath("@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"), + staticPath("polyfills/") + ); + copyFileDir( + npmPath("@webcomponents/webcomponentsjs/webcomponents-bundle.js"), + staticPath("polyfills/") + ); + copyFileDir( + npmPath("@webcomponents/webcomponentsjs/webcomponents-bundle.js.map"), + staticPath("polyfills/") + ); + + // Local fonts + fs.copySync(npmPath("@polymer/font-roboto-local/fonts"), staticPath("fonts")); + + // External dependency assets + copyFileDir( + npmPath("react-big-calendar/lib/css/react-big-calendar.css"), + staticPath("panels/calendar/") + ); + copyFileDir( + npmPath("leaflet/dist/leaflet.css"), + staticPath("images/leaflet/") + ); + fs.copySync( + npmPath("leaflet/dist/images"), + staticPath("images/leaflet/images/") + ); +} + +gulp.task("copy-static", (done) => { + copyStatic(); + done(); +}); + +gulp.task("compress-static", () => { + const fonts = gulp + .src(staticPath("fonts/**/*.ttf")) + .pipe(zopfli()) + .pipe(gulp.dest(staticPath("fonts"))); + const polyfills = gulp + .src(staticPath("polyfills/*.js")) + .pipe(zopfli()) + .pipe(gulp.dest(staticPath("polyfills"))); + const translations = gulp + .src(staticPath("translations/*.json")) + .pipe(zopfli()) + .pipe(gulp.dest(staticPath("translations"))); + + return merge(fonts, polyfills, translations); +}); + +gulp.task("copy-translations", (done) => { + copyTranslations(); + done(); +}); diff --git a/gulp/tasks/gen-icons.js b/build-scripts/gulp/gen-icons.js similarity index 88% rename from gulp/tasks/gen-icons.js rename to build-scripts/gulp/gen-icons.js index c47b42f360..6563b9a8f1 100644 --- a/gulp/tasks/gen-icons.js +++ b/build-scripts/gulp/gen-icons.js @@ -1,7 +1,6 @@ const gulp = require("gulp"); const path = require("path"); const fs = require("fs"); -const config = require("../config"); const ICON_PACKAGE_PATH = path.resolve( __dirname, @@ -38,12 +37,12 @@ function loadIcon(name) { function transformXMLtoPolymer(name, xml) { const start = xml.indexOf(">${path}`; + const pth = xml.substr(start, end); + return `${pth}`; } // Given an iconset name and icon names, generate a polymer iconset -function generateIconset(name, iconNames) { +function generateIconset(iconsetName, iconNames) { const iconDefs = Array.from(iconNames) .map((name) => { const iconDef = loadIcon(name); @@ -53,7 +52,7 @@ function generateIconset(name, iconNames) { return transformXMLtoPolymer(name, iconDef); }) .join(""); - return `${iconDefs}`; + return `${iconDefs}`; } // Generate the full MDI iconset @@ -62,7 +61,9 @@ function genMDIIcons() { fs.readFileSync(path.resolve(ICON_PACKAGE_PATH, META_PATH), "UTF-8") ); const iconNames = meta.map((iconInfo) => iconInfo.name); - fs.existsSync(OUTPUT_DIR) || fs.mkdirSync(OUTPUT_DIR); + if (!fs.existsSync(OUTPUT_DIR)) { + fs.mkdirSync(OUTPUT_DIR); + } fs.writeFileSync(MDI_OUTPUT_PATH, generateIconset("mdi", iconNames)); } @@ -81,7 +82,7 @@ function mapFiles(startPath, filter, mapFunc) { } // Find all icons used by the project. -function findIcons(path, iconsetName) { +function findIcons(searchPath, iconsetName) { const iconRegex = new RegExp(`${iconsetName}:[\\w-]+`, "g"); const icons = new Set(); function processFile(filename) { @@ -93,8 +94,8 @@ function findIcons(path, iconsetName) { icons.add(match[0].substr(iconsetName.length + 1)); } } - mapFiles(path, ".js", processFile); - mapFiles(path, ".ts", processFile); + mapFiles(searchPath, ".js", processFile); + mapFiles(searchPath, ".ts", processFile); return icons; } diff --git a/build-scripts/gulp/release.js b/build-scripts/gulp/release.js new file mode 100644 index 0000000000..f7e26b45ca --- /dev/null +++ b/build-scripts/gulp/release.js @@ -0,0 +1,31 @@ +// Run HA develop mode +const gulp = require("gulp"); + +require("./clean.js"); +require("./translations.js"); +require("./gen-icons.js"); +require("./gather-static.js"); +require("./webpack.js"); +require("./service-worker.js"); +require("./entry-html.js"); + +gulp.task( + "build-release", + gulp.series( + async function setEnv() { + process.env.NODE_ENV = "production"; + }, + "clean", + gulp.parallel( + "copy-static", + "gen-icons", + gulp.series("build-translations", "copy-translations") + ), + gulp.parallel("webpack-prod", "compress-static"), + gulp.parallel( + "gen-pages-prod", + "gen-index-html-prod", + "gen-service-worker-prod" + ) + ) +); diff --git a/build-scripts/gulp/service-worker.js b/build-scripts/gulp/service-worker.js new file mode 100644 index 0000000000..1303983790 --- /dev/null +++ b/build-scripts/gulp/service-worker.js @@ -0,0 +1,29 @@ +// Generate service worker. +// Based on manifest, create a file with the content as service_worker.js +/* eslint-disable import/no-dynamic-require */ +/* eslint-disable global-require */ +const gulp = require("gulp"); +const path = require("path"); +const fs = require("fs-extra"); +const config = require("../paths.js"); + +const swPath = path.resolve(config.root, "service_worker.js"); + +const writeSW = (content) => fs.outputFileSync(swPath, content.trim() + "\n"); + +gulp.task("gen-service-worker-dev", (done) => { + writeSW( + ` +console.debug('Service worker disabled in development'); + ` + ); + done(); +}); + +gulp.task("gen-service-worker-prod", (done) => { + fs.copySync( + path.resolve(config.output, "service_worker.js"), + path.resolve(config.root, "service_worker.js") + ); + done(); +}); diff --git a/gulp/tasks/translations.js b/build-scripts/gulp/translations.js similarity index 100% rename from gulp/tasks/translations.js rename to build-scripts/gulp/translations.js diff --git a/build-scripts/gulp/webpack.js b/build-scripts/gulp/webpack.js new file mode 100644 index 0000000000..6ba6fa1e4a --- /dev/null +++ b/build-scripts/gulp/webpack.js @@ -0,0 +1,63 @@ +// Tasks to run webpack. +const gulp = require("gulp"); +const webpack = require("webpack"); +const { createAppConfig } = require("../webpack"); + +const handler = (done) => (err, stats) => { + if (err) { + console.log(err.stack || err); + if (err.details) { + console.log(err.details); + } + return; + } + + console.log(`Build done @ ${new Date().toLocaleTimeString()}`); + + if (stats.hasErrors() || stats.hasWarnings()) { + console.log(stats.toString("minimal")); + } + + if (done) { + done(); + } +}; + +gulp.task("webpack-watch", () => { + const compiler = webpack([ + createAppConfig({ + isProdBuild: false, + latestBuild: true, + isStatsBuild: false, + }), + createAppConfig({ + isProdBuild: false, + latestBuild: false, + isStatsBuild: false, + }), + ]); + compiler.watch({}, handler()); + // we are not calling done, so this command will run forever +}); + +gulp.task( + "webpack-prod", + () => + new Promise((resolve) => + webpack( + [ + createAppConfig({ + isProdBuild: true, + latestBuild: true, + isStatsBuild: false, + }), + createAppConfig({ + isProdBuild: true, + latestBuild: false, + isStatsBuild: false, + }), + ], + handler(resolve) + ) + ) +); diff --git a/build-scripts/paths.js b/build-scripts/paths.js new file mode 100644 index 0000000000..28d553717b --- /dev/null +++ b/build-scripts/paths.js @@ -0,0 +1,10 @@ +var path = require("path"); + +module.exports = { + polymer_dir: path.resolve(__dirname, ".."), + build_dir: path.resolve(__dirname, "../build"), + root: path.resolve(__dirname, "../hass_frontend"), + static: path.resolve(__dirname, "../hass_frontend/static"), + output: path.resolve(__dirname, "../hass_frontend/frontend_latest"), + output_es5: path.resolve(__dirname, "../hass_frontend/frontend_es5"), +}; diff --git a/build-scripts/webpack.js b/build-scripts/webpack.js new file mode 100644 index 0000000000..1a48454661 --- /dev/null +++ b/build-scripts/webpack.js @@ -0,0 +1,184 @@ +const webpack = require("webpack"); +const fs = require("fs"); +const path = require("path"); +const TerserPlugin = require("terser-webpack-plugin"); +const WorkboxPlugin = require("workbox-webpack-plugin"); +const CompressionPlugin = require("compression-webpack-plugin"); +const zopfli = require("@gfx/zopfli"); +const ManifestPlugin = require("webpack-manifest-plugin"); +const paths = require("./paths.js"); +const { babelLoaderConfig } = require("./babel.js"); + +let version = fs + .readFileSync(path.resolve(paths.polymer_dir, "setup.py"), "utf8") + .match(/\d{8}\.\d+/); +if (!version) { + throw Error("Version not found"); +} +version = version[0]; + +const resolve = { + extensions: [".ts", ".js", ".json", ".tsx"], + alias: { + react: "preact-compat", + "react-dom": "preact-compat", + // Not necessary unless you consume a module using `createClass` + "create-react-class": "preact-compat/lib/create-react-class", + // Not necessary unless you consume a module requiring `react-dom-factories` + "react-dom-factories": "preact-compat/lib/react-dom-factories", + }, +}; + +const plugins = [ + // Ignore moment.js locales + new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/), + // Color.js is bloated, it contains all color definitions for all material color sets. + new webpack.NormalModuleReplacementPlugin( + /@polymer\/paper-styles\/color\.js$/, + path.resolve(paths.polymer_dir, "src/util/empty.js") + ), + // Ignore roboto pointing at CDN. We use local font-roboto-local. + new webpack.NormalModuleReplacementPlugin( + /@polymer\/font-roboto\/roboto\.js$/, + path.resolve(paths.polymer_dir, "src/util/empty.js") + ), + // Ignore mwc icons pointing at CDN. + new webpack.NormalModuleReplacementPlugin( + /@material\/mwc-icon\/mwc-icon-font\.js$/, + path.resolve(paths.polymer_dir, "src/util/empty.js") + ), +]; + +const optimization = (latestBuild) => ({ + minimizer: [ + new TerserPlugin({ + cache: true, + parallel: true, + extractComments: true, + terserOptions: { + ecma: latestBuild ? undefined : 5, + }, + }), + ], +}); + +const createAppConfig = ({ isProdBuild, latestBuild, isStatsBuild }) => { + const isCI = process.env.CI === "true"; + + // Create an object mapping browser urls to their paths during build + const translationMetadata = require("../build-translations/translationMetadata.json"); + const workBoxTranslationsTemplatedURLs = {}; + const englishFP = translationMetadata["translations"]["en"]["fingerprints"]; + Object.keys(englishFP).forEach((key) => { + workBoxTranslationsTemplatedURLs[ + `/static/translations/${englishFP[key]}` + ] = `build-translations/output/${key}.json`; + }); + + const publicPath = latestBuild ? "/frontend_latest/" : "/frontend_es5/"; + + const entry = { + app: "./src/entrypoints/app.ts", + authorize: "./src/entrypoints/authorize.ts", + onboarding: "./src/entrypoints/onboarding.ts", + core: "./src/entrypoints/core.ts", + compatibility: "./src/entrypoints/compatibility.ts", + "custom-panel": "./src/entrypoints/custom-panel.ts", + "hass-icons": "./src/entrypoints/hass-icons.ts", + }; + + return { + mode: isProdBuild ? "production" : "development", + devtool: isProdBuild + ? "cheap-source-map " + : "inline-cheap-module-source-map", + entry, + module: { + rules: [ + babelLoaderConfig({ latestBuild }), + { + test: /\.css$/, + use: "raw-loader", + }, + { + test: /\.(html)$/, + use: { + loader: "html-loader", + options: { + exportAsEs6Default: true, + }, + }, + }, + ], + }, + optimization: optimization(latestBuild), + plugins: [ + new ManifestPlugin(), + new webpack.DefinePlugin({ + __DEV__: JSON.stringify(!isProdBuild), + __DEMO__: false, + __BUILD__: JSON.stringify(latestBuild ? "latest" : "es5"), + __VERSION__: JSON.stringify(version), + __STATIC_PATH__: "/static/", + "process.env.NODE_ENV": JSON.stringify( + isProdBuild ? "production" : "development" + ), + }), + ...plugins, + isProdBuild && + !isCI && + !isStatsBuild && + new CompressionPlugin({ + cache: true, + exclude: [/\.js\.map$/, /\.LICENSE$/, /\.py$/, /\.txt$/], + algorithm(input, compressionOptions, callback) { + return zopfli.gzip(input, compressionOptions, callback); + }, + }), + latestBuild && + new WorkboxPlugin.InjectManifest({ + swSrc: "./src/entrypoints/service-worker-hass.js", + swDest: "service_worker.js", + importWorkboxFrom: "local", + include: [/\.js$/], + templatedURLs: { + ...workBoxTranslationsTemplatedURLs, + "/static/icons/favicon-192x192.png": + "public/icons/favicon-192x192.png", + "/static/fonts/roboto/Roboto-Light.ttf": + "node_modules/@polymer/font-roboto-local/fonts/roboto/Roboto-Light.ttf", + "/static/fonts/roboto/Roboto-Medium.ttf": + "node_modules/@polymer/font-roboto-local/fonts/roboto/Roboto-Medium.ttf", + "/static/fonts/roboto/Roboto-Regular.ttf": + "node_modules/@polymer/font-roboto-local/fonts/roboto/Roboto-Regular.ttf", + "/static/fonts/roboto/Roboto-Bold.ttf": + "node_modules/@polymer/font-roboto-local/fonts/roboto/Roboto-Bold.ttf", + }, + }), + ].filter(Boolean), + output: { + filename: ({ chunk }) => { + const dontHash = new Set([ + // Files who'se names should not be hashed. + // We currently have none. + ]); + if (!isProdBuild || dontHash.has(chunk.name)) return `${chunk.name}.js`; + return `${chunk.name}.${chunk.hash.substr(0, 8)}.js`; + }, + chunkFilename: + isProdBuild && !isStatsBuild + ? "chunk.[chunkhash].js" + : "[name].chunk.js", + path: latestBuild ? paths.output : paths.output_es5, + publicPath, + }, + resolve, + }; +}; + +module.exports = { + resolve, + plugins, + optimization, + createAppConfig, +}; diff --git a/config/webpack.js b/config/webpack.js deleted file mode 100644 index 9c61f7fb84..0000000000 --- a/config/webpack.js +++ /dev/null @@ -1,48 +0,0 @@ -const webpack = require("webpack"); -const path = require("path"); -const TerserPlugin = require("terser-webpack-plugin"); - -module.exports.resolve = { - extensions: [".ts", ".js", ".json", ".tsx"], - alias: { - react: "preact-compat", - "react-dom": "preact-compat", - // Not necessary unless you consume a module using `createClass` - "create-react-class": "preact-compat/lib/create-react-class", - // Not necessary unless you consume a module requiring `react-dom-factories` - "react-dom-factories": "preact-compat/lib/react-dom-factories", - }, -}; - -module.exports.plugins = [ - // Ignore moment.js locales - new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/), - // Color.js is bloated, it contains all color definitions for all material color sets. - new webpack.NormalModuleReplacementPlugin( - /@polymer\/paper-styles\/color\.js$/, - path.resolve(__dirname, "../src/util/empty.js") - ), - // Ignore roboto pointing at CDN. We use local font-roboto-local. - new webpack.NormalModuleReplacementPlugin( - /@polymer\/font-roboto\/roboto\.js$/, - path.resolve(__dirname, "../src/util/empty.js") - ), - // Ignore mwc icons pointing at CDN. - new webpack.NormalModuleReplacementPlugin( - /@material\/mwc-icon\/mwc-icon-font\.js$/, - path.resolve(__dirname, "../src/util/empty.js") - ), -]; - -module.exports.optimization = (latestBuild) => ({ - minimizer: [ - new TerserPlugin({ - cache: true, - parallel: true, - extractComments: true, - terserOptions: { - ecma: latestBuild ? undefined : 5, - }, - }), - ], -}); diff --git a/demo/script/gen-icons.js b/demo/script/gen-icons.js index 40a0f15a0d..b3c20a842a 100755 --- a/demo/script/gen-icons.js +++ b/demo/script/gen-icons.js @@ -4,7 +4,7 @@ const { findIcons, generateIconset, genMDIIcons, -} = require("../../gulp/tasks/gen-icons.js"); +} = require("../../build-scripts/gulp/gen-icons.js"); function genHademoIcons() { const iconNames = findIcons("./src", "hademo"); diff --git a/demo/webpack.config.js b/demo/webpack.config.js index 35c1ccdc93..a58d562e52 100644 --- a/demo/webpack.config.js +++ b/demo/webpack.config.js @@ -2,8 +2,8 @@ const path = require("path"); const webpack = require("webpack"); const CopyWebpackPlugin = require("copy-webpack-plugin"); const WorkboxPlugin = require("workbox-webpack-plugin"); -const { babelLoaderConfig } = require("../config/babel.js"); -const webpackBase = require("../config/webpack.js"); +const { babelLoaderConfig } = require("../build-scripts/babel.js"); +const webpackBase = require("../build-scripts/webpack.js"); const isProd = process.env.NODE_ENV === "production"; const isStatsBuild = process.env.STATS === "1"; @@ -72,7 +72,7 @@ module.exports = { ...webpackBase.plugins, isProd && new WorkboxPlugin.GenerateSW({ - swDest: "service_worker_es5.js", + swDest: "service_worker.js", importWorkboxFrom: "local", include: [], }), diff --git a/gallery/webpack.config.js b/gallery/webpack.config.js index 0db9c2f935..b252c03551 100644 --- a/gallery/webpack.config.js +++ b/gallery/webpack.config.js @@ -1,7 +1,7 @@ const path = require("path"); const CopyWebpackPlugin = require("copy-webpack-plugin"); -const { babelLoaderConfig } = require("../config/babel.js"); -const webpackBase = require("../config/webpack.js"); +const { babelLoaderConfig } = require("../build-scripts/babel.js"); +const webpackBase = require("../build-scripts/webpack.js"); const isProd = process.env.NODE_ENV === "production"; const chunkFilename = isProd ? "chunk.[chunkhash].js" : "[name].chunk.js"; diff --git a/gulp/config.js b/gulp/config.js deleted file mode 100644 index b011a04cf3..0000000000 --- a/gulp/config.js +++ /dev/null @@ -1,8 +0,0 @@ -var path = require("path"); - -module.exports = { - polymer_dir: path.resolve(__dirname, ".."), - build_dir: path.resolve(__dirname, "../build"), - output: path.resolve(__dirname, "../hass_frontend"), - output_es5: path.resolve(__dirname, "../hass_frontend_es5"), -}; diff --git a/gulpfile.js b/gulpfile.js index f3cb47a7e6..78578bad7b 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -1,3 +1,3 @@ -var requireDir = require('require-dir'); +var requireDir = require("require-dir"); -requireDir('./gulp/tasks/'); +requireDir("./build-scripts/gulp/"); diff --git a/hassio/script/gen-icons.js b/hassio/script/gen-icons.js index 61fc39e212..b355ef752b 100755 --- a/hassio/script/gen-icons.js +++ b/hassio/script/gen-icons.js @@ -4,7 +4,7 @@ const { findIcons, generateIconset, genMDIIcons, -} = require("../../gulp/tasks/gen-icons.js"); +} = require("../../build-scripts/gulp/gen-icons.js"); function genHassioIcons() { const iconNames = findIcons("./src", "hassio"); diff --git a/hassio/webpack.config.js b/hassio/webpack.config.js index 022bbb563c..e58d2df301 100644 --- a/hassio/webpack.config.js +++ b/hassio/webpack.config.js @@ -3,8 +3,8 @@ const CompressionPlugin = require("compression-webpack-plugin"); const zopfli = require("@gfx/zopfli"); const config = require("./config.js"); -const { babelLoaderConfig } = require("../config/babel.js"); -const webpackBase = require("../config/webpack.js"); +const { babelLoaderConfig } = require("../build-scripts/babel.js"); +const webpackBase = require("../build-scripts/webpack.js"); const isProdBuild = process.env.NODE_ENV === "production"; const isCI = process.env.CI === "true"; diff --git a/package.json b/package.json index fc793d7e09..5b3213a8a7 100644 --- a/package.json +++ b/package.json @@ -125,6 +125,7 @@ "eslint-plugin-import": "^2.16.0", "eslint-plugin-prettier": "^3.0.1", "eslint-plugin-react": "^7.12.4", + "fs-extra": "^7.0.1", "gulp": "^4.0.0", "gulp-foreach": "^0.1.0", "gulp-hash": "^4.2.2", @@ -134,10 +135,12 @@ "gulp-jsonminify": "^1.1.0", "gulp-merge-json": "^1.3.1", "gulp-rename": "^1.4.0", + "gulp-zopfli-green": "^3.0.1", "html-loader": "^0.5.5", "html-webpack-plugin": "^3.2.0", "husky": "^1.3.1", "lint-staged": "^8.1.5", + "lodash.template": "^4.4.0", "merge-stream": "^1.0.1", "mocha": "^6.0.2", "parse5": "^5.1.0", @@ -160,6 +163,7 @@ "webpack": "^4.29.6", "webpack-cli": "^3.3.0", "webpack-dev-server": "^3.2.1", + "webpack-manifest-plugin": "^2.0.4", "workbox-webpack-plugin": "^4.1.1" }, "resolutions": { diff --git a/public/__init__.py b/public/__init__.py index 50b6b4a508..1debde5485 100644 --- a/public/__init__.py +++ b/public/__init__.py @@ -1,30 +1,7 @@ """Frontend for Home Assistant.""" -import os -from user_agents import parse - -FAMILY_MIN_VERSION = { - 'Chrome': 55, # Async/await - 'Chrome Mobile': 55, - 'Firefox': 52, # Async/await - 'Firefox Mobile': 52, - 'Opera': 42, # Async/await - 'Edge': 15, # Async/await - 'Safari': 10.1, # Async/await -} +from pathlib import Path def where(): """Return path to the frontend.""" - return os.path.dirname(__file__) - - -def version(useragent): - """Get the version for given user agent.""" - useragent = parse(useragent) - - # on iOS every browser uses the Safari engine - if useragent.os.family == 'iOS': - return useragent.os.version[0] >= FAMILY_MIN_VERSION['Safari'] - - version = FAMILY_MIN_VERSION.get(useragent.browser.family) - return version and useragent.browser.version[0] >= version + return Path(__file__).parent diff --git a/public/icons/favicon-1024x1024.png b/public/static/icons/favicon-1024x1024.png similarity index 100% rename from public/icons/favicon-1024x1024.png rename to public/static/icons/favicon-1024x1024.png diff --git a/public/icons/favicon-192x192.png b/public/static/icons/favicon-192x192.png similarity index 100% rename from public/icons/favicon-192x192.png rename to public/static/icons/favicon-192x192.png diff --git a/public/icons/favicon-384x384.png b/public/static/icons/favicon-384x384.png similarity index 100% rename from public/icons/favicon-384x384.png rename to public/static/icons/favicon-384x384.png diff --git a/public/icons/favicon-512x512.png b/public/static/icons/favicon-512x512.png similarity index 100% rename from public/icons/favicon-512x512.png rename to public/static/icons/favicon-512x512.png diff --git a/public/icons/favicon-apple-180x180.png b/public/static/icons/favicon-apple-180x180.png similarity index 100% rename from public/icons/favicon-apple-180x180.png rename to public/static/icons/favicon-apple-180x180.png diff --git a/public/icons/favicon.ico b/public/static/icons/favicon.ico similarity index 100% rename from public/icons/favicon.ico rename to public/static/icons/favicon.ico diff --git a/public/icons/mask-icon.svg b/public/static/icons/mask-icon.svg similarity index 100% rename from public/icons/mask-icon.svg rename to public/static/icons/mask-icon.svg diff --git a/public/icons/tile-win-150x150.png b/public/static/icons/tile-win-150x150.png similarity index 100% rename from public/icons/tile-win-150x150.png rename to public/static/icons/tile-win-150x150.png diff --git a/public/icons/tile-win-310x150.png b/public/static/icons/tile-win-310x150.png similarity index 100% rename from public/icons/tile-win-310x150.png rename to public/static/icons/tile-win-310x150.png diff --git a/public/icons/tile-win-310x310.png b/public/static/icons/tile-win-310x310.png similarity index 100% rename from public/icons/tile-win-310x310.png rename to public/static/icons/tile-win-310x310.png diff --git a/public/icons/tile-win-70x70.png b/public/static/icons/tile-win-70x70.png similarity index 100% rename from public/icons/tile-win-70x70.png rename to public/static/icons/tile-win-70x70.png diff --git a/public/images/card_media_player_bg.png b/public/static/images/card_media_player_bg.png similarity index 100% rename from public/images/card_media_player_bg.png rename to public/static/images/card_media_player_bg.png diff --git a/public/images/config_ecobee_thermostat.png b/public/static/images/config_ecobee_thermostat.png similarity index 100% rename from public/images/config_ecobee_thermostat.png rename to public/static/images/config_ecobee_thermostat.png diff --git a/public/images/config_fitbit_app.png b/public/static/images/config_fitbit_app.png similarity index 100% rename from public/images/config_fitbit_app.png rename to public/static/images/config_fitbit_app.png diff --git a/public/images/config_flows/config_homematicip_cloud.png b/public/static/images/config_flows/config_homematicip_cloud.png similarity index 100% rename from public/images/config_flows/config_homematicip_cloud.png rename to public/static/images/config_flows/config_homematicip_cloud.png diff --git a/public/images/config_icloud.png b/public/static/images/config_icloud.png similarity index 100% rename from public/images/config_icloud.png rename to public/static/images/config_icloud.png diff --git a/public/images/config_insteon.png b/public/static/images/config_insteon.png similarity index 100% rename from public/images/config_insteon.png rename to public/static/images/config_insteon.png diff --git a/public/images/config_philips_hue.jpg b/public/static/images/config_philips_hue.jpg similarity index 100% rename from public/images/config_philips_hue.jpg rename to public/static/images/config_philips_hue.jpg diff --git a/public/images/config_webos.png b/public/static/images/config_webos.png similarity index 100% rename from public/images/config_webos.png rename to public/static/images/config_webos.png diff --git a/public/images/config_wink.png b/public/static/images/config_wink.png similarity index 100% rename from public/images/config_wink.png rename to public/static/images/config_wink.png diff --git a/public/images/darksky/weather-cloudy.svg b/public/static/images/darksky/weather-cloudy.svg similarity index 100% rename from public/images/darksky/weather-cloudy.svg rename to public/static/images/darksky/weather-cloudy.svg diff --git a/public/images/darksky/weather-fog.svg b/public/static/images/darksky/weather-fog.svg similarity index 100% rename from public/images/darksky/weather-fog.svg rename to public/static/images/darksky/weather-fog.svg diff --git a/public/images/darksky/weather-hail.svg b/public/static/images/darksky/weather-hail.svg similarity index 100% rename from public/images/darksky/weather-hail.svg rename to public/static/images/darksky/weather-hail.svg diff --git a/public/images/darksky/weather-night.svg b/public/static/images/darksky/weather-night.svg similarity index 100% rename from public/images/darksky/weather-night.svg rename to public/static/images/darksky/weather-night.svg diff --git a/public/images/darksky/weather-partlycloudy.svg b/public/static/images/darksky/weather-partlycloudy.svg similarity index 100% rename from public/images/darksky/weather-partlycloudy.svg rename to public/static/images/darksky/weather-partlycloudy.svg diff --git a/public/images/darksky/weather-pouring.svg b/public/static/images/darksky/weather-pouring.svg similarity index 100% rename from public/images/darksky/weather-pouring.svg rename to public/static/images/darksky/weather-pouring.svg diff --git a/public/images/darksky/weather-rainy.svg b/public/static/images/darksky/weather-rainy.svg similarity index 100% rename from public/images/darksky/weather-rainy.svg rename to public/static/images/darksky/weather-rainy.svg diff --git a/public/images/darksky/weather-snowy.svg b/public/static/images/darksky/weather-snowy.svg similarity index 100% rename from public/images/darksky/weather-snowy.svg rename to public/static/images/darksky/weather-snowy.svg diff --git a/public/images/darksky/weather-sunny.svg b/public/static/images/darksky/weather-sunny.svg similarity index 100% rename from public/images/darksky/weather-sunny.svg rename to public/static/images/darksky/weather-sunny.svg diff --git a/public/images/darksky/weather-windy.svg b/public/static/images/darksky/weather-windy.svg similarity index 100% rename from public/images/darksky/weather-windy.svg rename to public/static/images/darksky/weather-windy.svg diff --git a/public/images/image-broken.svg b/public/static/images/image-broken.svg similarity index 100% rename from public/images/image-broken.svg rename to public/static/images/image-broken.svg diff --git a/public/images/logo_automatic.png b/public/static/images/logo_automatic.png similarity index 100% rename from public/images/logo_automatic.png rename to public/static/images/logo_automatic.png diff --git a/public/images/logo_axis.png b/public/static/images/logo_axis.png similarity index 100% rename from public/images/logo_axis.png rename to public/static/images/logo_axis.png diff --git a/public/images/logo_deconz.jpeg b/public/static/images/logo_deconz.jpeg similarity index 100% rename from public/images/logo_deconz.jpeg rename to public/static/images/logo_deconz.jpeg diff --git a/public/images/logo_philips_hue.png b/public/static/images/logo_philips_hue.png similarity index 100% rename from public/images/logo_philips_hue.png rename to public/static/images/logo_philips_hue.png diff --git a/public/images/logo_plex_mediaserver.png b/public/static/images/logo_plex_mediaserver.png similarity index 100% rename from public/images/logo_plex_mediaserver.png rename to public/static/images/logo_plex_mediaserver.png diff --git a/public/images/notification-badge.png b/public/static/images/notification-badge.png similarity index 100% rename from public/images/notification-badge.png rename to public/static/images/notification-badge.png diff --git a/public/images/smart-tv.png b/public/static/images/smart-tv.png similarity index 100% rename from public/images/smart-tv.png rename to public/static/images/smart-tv.png diff --git a/script/build_frontend b/script/build_frontend index 81b4986f3b..eddb895a53 100755 --- a/script/build_frontend +++ b/script/build_frontend @@ -6,19 +6,4 @@ set -e cd "$(dirname "$0")/.." -BUILD_DIR=build -BUILD_TRANSLATIONS_DIR=build-translations -OUTPUT_DIR=hass_frontend -OUTPUT_DIR_ES5=hass_frontend_es5 - -rm -rf $OUTPUT_DIR $OUTPUT_DIR_ES5 $BUILD_DIR $BUILD_TRANSLATIONS_DIR - -# Build frontend -./node_modules/.bin/gulp build-translations gen-icons -NODE_ENV=production ./node_modules/.bin/webpack - -# Generate the __init__ file -echo "VERSION = '`git rev-parse HEAD`'" >> $OUTPUT_DIR/__init__.py -echo "CREATED_AT = `date +%s`" >> $OUTPUT_DIR/__init__.py -echo "VERSION = '`git rev-parse HEAD`'" >> $OUTPUT_DIR_ES5/__init__.py -echo "CREATED_AT = `date +%s`" >> $OUTPUT_DIR_ES5/__init__.py +./node_modules/.bin/gulp build-release diff --git a/script/develop b/script/develop index 24c1253dc0..f0be37820c 100755 --- a/script/develop +++ b/script/develop @@ -6,14 +6,4 @@ set -e cd "$(dirname "$0")/.." -BUILD_DIR=build -OUTPUT_DIR=hass_frontend -OUTPUT_DIR_ES5=hass_frontend_es5 - -rm -rf $OUTPUT_DIR $OUTPUT_DIR_ES5 $BUILD_DIR -mkdir $OUTPUT_DIR $OUTPUT_DIR_ES5 -# Needed in case frontend repo installed with pip3 install -e -cp -r public/__init__.py $OUTPUT_DIR_ES5/ - -./node_modules/.bin/gulp build-translations gen-icons -./node_modules/.bin/webpack --watch --progress +./node_modules/.bin/gulp develop diff --git a/script/size_stats b/script/size_stats index 4b6601a480..f6c95545eb 100755 --- a/script/size_stats +++ b/script/size_stats @@ -7,5 +7,5 @@ set -e cd "$(dirname "$0")/.." STATS=1 NODE_ENV=production webpack --profile --json > compilation-stats.json -npx webpack-bundle-analyzer compilation-stats.json hass_frontend +npx webpack-bundle-analyzer compilation-stats.json hass_frontend/frontend_latest rm compilation-stats.json diff --git a/setup.py b/setup.py index 0e754b6250..144d06ceb2 100644 --- a/setup.py +++ b/setup.py @@ -8,15 +8,7 @@ setup( author="The Home Assistant Authors", author_email="hello@home-assistant.io", license="Apache License 2.0", - packages=find_packages( - include=[ - "hass_frontend", - "hass_frontend_es5", - "hass_frontend.*", - "hass_frontend_es5.*", - ] - ), - install_requires=["user-agents==2.0.0"], + packages=find_packages(include=["hass_frontend", "hass_frontend.*"]), include_package_data=True, zip_safe=False, ) diff --git a/src/auth/ha-authorize.ts b/src/auth/ha-authorize.ts index 10a190cca8..4935fac9dd 100644 --- a/src/auth/ha-authorize.ts +++ b/src/auth/ha-authorize.ts @@ -8,7 +8,7 @@ import { css, } from "lit-element"; import "./ha-auth-flow"; -import { AuthProvider } from "../data/auth"; +import { AuthProvider, fetchAuthProviders } from "../data/auth"; import { registerServiceWorker } from "../util/register-service-worker"; import(/* webpackChunkName: "pick-auth-provider" */ "../auth/ha-pick-auth-provider"); @@ -135,7 +135,9 @@ class HaAuthorize extends litLocalizeLiteMixin(LitElement) { private async _fetchAuthProviders() { // Fetch auth providers try { - const response = await (window as any).providersPromise; + // We prefetch this data on page load in authorize.html.template for modern builds + const response = await ((window as any).providersPromise || + fetchAuthProviders()); const authProviders = await response.json(); // Forward to main screen which will redirect to right onboarding page. diff --git a/src/data/auth.ts b/src/data/auth.ts index 0c5d1a9493..77c7b40d17 100644 --- a/src/data/auth.ts +++ b/src/data/auth.ts @@ -18,3 +18,8 @@ export const getSignedPath = ( hass: HomeAssistant, path: string ): Promise => hass.callWS({ type: "auth/sign_path", path }); + +export const fetchAuthProviders = () => + fetch("/auth/providers", { + credentials: "same-origin", + }); diff --git a/src/data/onboarding.ts b/src/data/onboarding.ts index 05a54be1a0..83002acd6b 100644 --- a/src/data/onboarding.ts +++ b/src/data/onboarding.ts @@ -9,7 +9,10 @@ interface UserStepResponse { auth_code: string; } -export const onboardUserStep = async (params: { +export const fetchOnboardingOverview = () => + fetch("/api/onboarding", { credentials: "same-origin" }); + +export const onboardUserStep = (params: { client_id: string; name: string; username: string; diff --git a/src/entrypoints/service-worker-bootstrap.js b/src/entrypoints/service-worker-bootstrap.js deleted file mode 100644 index e296139be7..0000000000 --- a/src/entrypoints/service-worker-bootstrap.js +++ /dev/null @@ -1,2 +0,0 @@ -/* global importScripts */ -importScripts("/static/service-worker-hass.js"); diff --git a/src/entrypoints/service-worker-hass.js b/src/entrypoints/service-worker-hass.js index 80eea35497..9e0aab0e70 100644 --- a/src/entrypoints/service-worker-hass.js +++ b/src/entrypoints/service-worker-hass.js @@ -1,3 +1,7 @@ +/* + This file is not run through webpack, but instead is directly manipulated + by Workbox Webpack plugin. So we cannot use __DEV__ or other constants. +*/ /* global workbox clients */ function initRouting() { @@ -17,9 +21,7 @@ function initRouting() { // Get manifest and service worker from network. workbox.routing.registerRoute( - new RegExp( - `${location.host}/(service_worker.js|service_worker_es5.js|manifest.json)` - ), + new RegExp(`${location.host}/(service_worker.js|manifest.json)`), new workbox.strategies.NetworkOnly() ); @@ -161,11 +163,8 @@ self.addEventListener("message", (message) => { }); workbox.setConfig({ - debug: __DEV__, + debug: false, }); -if (!__DEV__) { - initRouting(); -} - +initRouting(); initPushNotifications(); diff --git a/src/html/_js_base.html.template b/src/html/_js_base.html.template new file mode 100644 index 0000000000..f9fac27261 --- /dev/null +++ b/src/html/_js_base.html.template @@ -0,0 +1,25 @@ + diff --git a/src/html/authorize.html.template b/src/html/authorize.html.template index a8feb6560b..7adeebd2cc 100644 --- a/src/html/authorize.html.template +++ b/src/html/authorize.html.template @@ -1,10 +1,21 @@ - + Home Assistant - - - <%= require('raw-loader!./_header.html.template').default %> + + + + <%= renderTemplate('_header') %> - -
- - - <% if (!latestBuild) { %> - - - <% } %> - - - - + + + {% for extra_url in extra_urls -%} - + {% endfor -%} diff --git a/src/html/onboarding.html.template b/src/html/onboarding.html.template index 50d1e52ec1..3febae22e3 100644 --- a/src/html/onboarding.html.template +++ b/src/html/onboarding.html.template @@ -1,10 +1,21 @@ - + Home Assistant - - - <%= require('raw-loader!./_header.html.template').default %> + + + + <%= renderTemplate('_header') %>