diff --git a/.github/workflows/release-drafter.yaml b/.github/workflows/release-drafter.yaml new file mode 100644 index 0000000000..00954a2615 --- /dev/null +++ b/.github/workflows/release-drafter.yaml @@ -0,0 +1,14 @@ +name: Release Drafter + +on: + push: + branches: + - dev + +jobs: + update_release_draft: + runs-on: ubuntu-latest + steps: + - uses: release-drafter/release-drafter@v5 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 5d2149dce0..f9b1ea7931 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -2,79 +2,139 @@ ## Our Pledge -In the interest of fostering an open and welcoming environment, we as -contributors and maintainers pledge to making participation in our project and -our community a harassment-free experience for everyone, regardless of age, body -size, disability, ethnicity, gender identity and expression, level of experience, -nationality, personal appearance, race, religion, or sexual identity and -orientation. +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. ## Our Standards -Examples of behavior that contributes to creating a positive environment -include: +Examples of behavior that contributes to a positive environment for our +community include: -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community -Examples of unacceptable behavior by participants include: +Examples of unacceptable behavior include: -* The use of sexualized language or imagery and unwelcome sexual attention or -advances -* Trolling, insulting/derogatory comments, and personal or political attacks +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment -* Publishing others' private information, such as a physical or electronic - address, without explicit permission +* Publishing others' private information, such as a physical or email + address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting -## Our Responsibilities +## Enforcement Responsibilities -Project maintainers are responsible for clarifying the standards of acceptable -behavior and are expected to take appropriate and fair corrective action in -response to any instances of unacceptable behavior. +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. -Project maintainers have the right and responsibility to remove, edit, or -reject comments, commits, code, wiki edits, issues, and other contributions -that are not aligned to this Code of Conduct, or to ban temporarily or -permanently any contributor for other behaviors that they deem inappropriate, -threatening, offensive, or harmful. +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. ## Scope -This Code of Conduct applies both within project spaces and in public spaces -when an individual is representing the project or its community. Examples of -representing a project or community include using an official project e-mail -address, posting via an official social media account, or acting as an appointed -representative at an online or offline event. Representation of a project may be -further defined and clarified by project maintainers. +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project team at [safety@home-assistant.io][email]. All -complaints will be reviewed and investigated and will result in a response that -is deemed necessary and appropriate to the circumstances. The project team is -obligated to maintain confidentiality with regard to the reporter of an incident. -Further details of specific enforcement policies may be posted separately. +reported to the community leaders responsible for enforcement at +[safety@home-assistant.io][email] or by using the report/flag feature of +the medium used. All complaints will be reviewed and investigated promptly and +fairly. -Project maintainers who do not follow or enforce the Code of Conduct in good -faith may face temporary or permanent repercussions as determined by other -members of the project's leadership. +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. ## Attribution -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, -available [here][version]. +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available [here][version]. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder][mozilla]. ## Adoption -This Code of Conduct was first adopted January 21st, 2017 and announced in [this][coc-blog] blog post. +This Code of Conduct was first adopted January 21st, 2017 and announced in +[this][coc-blog] blog post and has been updated on May 25th, 2020 to version +2.0 of the [Contributor Covenant][homepage] as announced in [this][coc2-blog] +blog post. -[homepage]: http://contributor-covenant.org -[version]: http://contributor-covenant.org/version/1/4/ +For answers to common questions about this code of conduct, see the FAQ at +. Translations are available at +. + +[coc-blog]: /blog/2017/01/21/home-assistant-governance/ +[coc2-blog]: /blog/2020/05/25/code-of-conduct-updated/ [email]: mailto:safety@home-assistant.io -[coc-blog]: https://home-assistant.io/blog/2017/01/21/home-assistant-governance/ +[homepage]: http://contributor-covenant.org +[mozilla]: https://github.com/mozilla/diversity +[version]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html diff --git a/build-scripts/babel.js b/build-scripts/babel.js deleted file mode 100644 index 8e8abee81c..0000000000 --- a/build-scripts/babel.js +++ /dev/null @@ -1,39 +0,0 @@ -const options = ({ latestBuild }) => ({ - presets: [ - !latestBuild && [require("@babel/preset-env").default, { modules: false }], - require("@babel/preset-typescript").default, - ].filter(Boolean), - plugins: [ - // Part of ES2018. Converts {...a, b: 2} to Object.assign({}, a, {b: 2}) - [ - "@babel/plugin-proposal-object-rest-spread", - { loose: true, useBuiltIns: true }, - ], - // Only support the syntax, Webpack will handle it. - "@babel/syntax-dynamic-import", - "@babel/plugin-proposal-optional-chaining", - "@babel/plugin-proposal-nullish-coalescing-operator", - [ - require("@babel/plugin-proposal-decorators").default, - { decoratorsBeforeExport: true }, - ], - [ - require("@babel/plugin-proposal-class-properties").default, - { loose: true }, - ], - ], -}); - -module.exports.babelLoaderConfig = ({ latestBuild }) => { - if (latestBuild === undefined) { - throw Error("latestBuild not defined for babel loader config"); - } - return { - test: /\.m?js$|\.tsx?$/, - exclude: [require.resolve("@mdi/js/mdi.js"), require.resolve("hls.js")], - use: { - loader: "babel-loader", - options: options({ latestBuild }), - }, - }; -}; diff --git a/build-scripts/bundle.js b/build-scripts/bundle.js new file mode 100644 index 0000000000..4d1ec71682 --- /dev/null +++ b/build-scripts/bundle.js @@ -0,0 +1,199 @@ +const path = require("path"); +const env = require("./env.js"); +const paths = require("./paths.js"); + +// Files from NPM Packages that should not be imported +module.exports.ignorePackages = ({ latestBuild }) => [ + // Bloats bundle and it's not used. + path.resolve(require.resolve("moment"), "../locale"), + // Part of yaml.js and only used for !!js functions that we don't use + require.resolve("esprima"), +]; + +// Files from NPM packages that we should replace with empty file +module.exports.emptyPackages = ({ latestBuild }) => + [ + // Contains all color definitions for all material color sets. + // We don't use it + require.resolve("@polymer/paper-styles/color.js"), + require.resolve("@polymer/paper-styles/default-theme.js"), + // Loads stuff from a CDN + require.resolve("@polymer/font-roboto/roboto.js"), + require.resolve("@vaadin/vaadin-material-styles/font-roboto.js"), + // Compatibility not needed for latest builds + latestBuild && + // wrapped in require.resolve so it blows up if file no longer exists + require.resolve( + path.resolve(paths.polymer_dir, "src/resources/compatibility.ts") + ), + // This polyfill is loaded in workers to support ES5, filter it out. + latestBuild && require.resolve("proxy-polyfill/src/index.js"), + ].filter(Boolean); + +module.exports.definedVars = ({ isProdBuild, latestBuild, defineOverlay }) => ({ + __DEV__: !isProdBuild, + __BUILD__: JSON.stringify(latestBuild ? "latest" : "es5"), + __VERSION__: JSON.stringify(env.version()), + __DEMO__: false, + __BACKWARDS_COMPAT__: false, + __STATIC_PATH__: "/static/", + "process.env.NODE_ENV": JSON.stringify( + isProdBuild ? "production" : "development" + ), + ...defineOverlay, +}); + +module.exports.terserOptions = (latestBuild) => ({ + safari10: true, + ecma: latestBuild ? undefined : 5, + output: { comments: false }, +}); + +module.exports.babelOptions = ({ latestBuild }) => ({ + babelrc: false, + presets: [ + !latestBuild && [require("@babel/preset-env").default, { modules: false }], + require("@babel/preset-typescript").default, + ].filter(Boolean), + plugins: [ + // Part of ES2018. Converts {...a, b: 2} to Object.assign({}, a, {b: 2}) + [ + "@babel/plugin-proposal-object-rest-spread", + { loose: true, useBuiltIns: true }, + ], + // Only support the syntax, Webpack will handle it. + "@babel/syntax-dynamic-import", + "@babel/plugin-proposal-optional-chaining", + "@babel/plugin-proposal-nullish-coalescing-operator", + [ + require("@babel/plugin-proposal-decorators").default, + { decoratorsBeforeExport: true }, + ], + [ + require("@babel/plugin-proposal-class-properties").default, + { loose: true }, + ], + ], +}); + +// Are already ES5, cause warnings when babelified. +module.exports.babelExclude = () => [ + require.resolve("@mdi/js/mdi.js"), + require.resolve("hls.js"), +]; + +const outputPath = (outputRoot, latestBuild) => + path.resolve(outputRoot, latestBuild ? "frontend_latest" : "frontend_es5"); + +const publicPath = (latestBuild) => + latestBuild ? "/frontend_latest/" : "/frontend_es5/"; + +/* +BundleConfig { + // Object with entrypoints that need to be bundled + entry: { [name: string]: pathToFile }, + // Folder where bundled files need to be written + outputPath: string, + // absolute url-path where bundled files can be found + publicPath: string, + // extra definitions that we need to replace in source + defineOverlay: {[name: string]: value }, + // if this is a production build + isProdBuild: boolean, + // If we're targeting latest browsers + latestBuild: boolean, + // If we're doing a stats build (create nice chunk names) + isStatsBuild: boolean, + // Names of entrypoints that should not be hashed + dontHash: Set +} +*/ + +module.exports.config = { + app({ isProdBuild, latestBuild, isStatsBuild }) { + return { + entry: { + service_worker: "./src/entrypoints/service_worker.ts", + app: "./src/entrypoints/app.ts", + authorize: "./src/entrypoints/authorize.ts", + onboarding: "./src/entrypoints/onboarding.ts", + core: "./src/entrypoints/core.ts", + "custom-panel": "./src/entrypoints/custom-panel.ts", + }, + outputPath: outputPath(paths.app_output_root, latestBuild), + publicPath: publicPath(latestBuild), + isProdBuild, + latestBuild, + isStatsBuild, + }; + }, + + demo({ isProdBuild, latestBuild, isStatsBuild }) { + return { + entry: { + main: path.resolve(paths.demo_dir, "src/entrypoint.ts"), + }, + outputPath: outputPath(paths.demo_output_root, latestBuild), + publicPath: publicPath(latestBuild), + defineOverlay: { + __VERSION__: JSON.stringify(`DEMO-${env.version()}`), + __DEMO__: true, + }, + isProdBuild, + latestBuild, + isStatsBuild, + }; + }, + + cast({ isProdBuild, latestBuild }) { + const entry = { + launcher: path.resolve(paths.cast_dir, "src/launcher/entrypoint.ts"), + }; + + if (latestBuild) { + entry.receiver = path.resolve( + paths.cast_dir, + "src/receiver/entrypoint.ts" + ); + } + + return { + entry, + outputPath: outputPath(paths.cast_output_root, latestBuild), + publicPath: publicPath(latestBuild), + isProdBuild, + latestBuild, + defineOverlay: { + __BACKWARDS_COMPAT__: true, + }, + }; + }, + + hassio({ isProdBuild, latestBuild }) { + if (latestBuild) { + throw new Error("Hass.io does not support latest build!"); + } + return { + entry: { + entrypoint: path.resolve(paths.hassio_dir, "src/entrypoint.ts"), + }, + outputPath: paths.hassio_output_root, + publicPath: paths.hassio_publicPath, + isProdBuild, + latestBuild, + dontHash: new Set(["entrypoint"]), + }; + }, + + gallery({ isProdBuild, latestBuild }) { + return { + entry: { + entrypoint: path.resolve(paths.gallery_dir, "src/entrypoint.js"), + }, + outputPath: outputPath(paths.gallery_output_root, latestBuild), + publicPath: publicPath(latestBuild), + isProdBuild, + latestBuild, + }; + }, +}; diff --git a/build-scripts/env.js b/build-scripts/env.js index 72a9e794d2..8980d3bf92 100644 --- a/build-scripts/env.js +++ b/build-scripts/env.js @@ -3,8 +3,13 @@ const path = require("path"); const paths = require("./paths.js"); module.exports = { + useRollup() { + return process.env.ROLLUP === "1"; + }, isProdBuild() { - return process.env.NODE_ENV === "production"; + return ( + process.env.NODE_ENV === "production" || module.exports.isStatsBuild() + ); }, isStatsBuild() { return process.env.STATS === "1"; diff --git a/build-scripts/gulp/app.js b/build-scripts/gulp/app.js index 1bd5c4750b..9429260617 100644 --- a/build-scripts/gulp/app.js +++ b/build-scripts/gulp/app.js @@ -1,7 +1,7 @@ // Run HA develop mode const gulp = require("gulp"); -const envVars = require("../env"); +const env = require("../env"); require("./clean.js"); require("./translations.js"); @@ -11,6 +11,7 @@ require("./compress.js"); require("./webpack.js"); require("./service-worker.js"); require("./entry-html.js"); +require("./rollup.js"); gulp.task( "develop-app", @@ -26,8 +27,8 @@ gulp.task( "gen-index-app-dev", "build-translations" ), - "copy-static", - "webpack-watch-app" + "copy-static-app", + env.useRollup() ? "rollup-watch-app" : "webpack-watch-app" ) ); @@ -39,10 +40,10 @@ gulp.task( }, "clean", gulp.parallel("gen-icons-json", "build-translations"), - "copy-static", - "webpack-prod-app", + "copy-static-app", + env.useRollup() ? "rollup-prod-app" : "webpack-prod-app", ...// Don't compress running tests - (envVars.isTest() ? [] : ["compress-app"]), + (env.isTest() ? [] : ["compress-app"]), gulp.parallel( "gen-pages-prod", "gen-index-app-prod", diff --git a/build-scripts/gulp/cast.js b/build-scripts/gulp/cast.js index ad393f74b4..4d9a7fe34f 100644 --- a/build-scripts/gulp/cast.js +++ b/build-scripts/gulp/cast.js @@ -1,11 +1,14 @@ const gulp = require("gulp"); +const env = require("../env"); + require("./clean.js"); require("./translations.js"); require("./gather-static.js"); require("./webpack.js"); require("./service-worker.js"); require("./entry-html.js"); +require("./rollup.js"); gulp.task( "develop-cast", @@ -17,7 +20,7 @@ gulp.task( "translations-enable-merge-backend", gulp.parallel("gen-icons-json", "build-translations"), "copy-static-cast", - "webpack-dev-server-cast" + env.useRollup() ? "rollup-dev-server-cast" : "webpack-dev-server-cast" ) ); @@ -31,7 +34,7 @@ gulp.task( "translations-enable-merge-backend", gulp.parallel("gen-icons-json", "build-translations"), "copy-static-cast", - "webpack-prod-cast", + env.useRollup() ? "rollup-prod-cast" : "webpack-prod-cast", "gen-index-cast-prod" ) ); diff --git a/build-scripts/gulp/clean.js b/build-scripts/gulp/clean.js index 41fec51e69..caad547641 100644 --- a/build-scripts/gulp/clean.js +++ b/build-scripts/gulp/clean.js @@ -1,39 +1,36 @@ const del = require("del"); const gulp = require("gulp"); -const config = require("../paths"); +const paths = require("../paths"); require("./translations"); gulp.task( "clean", gulp.parallel("clean-translations", function cleanOutputAndBuildDir() { - return del([config.root, config.build_dir]); + return del([paths.app_output_root, paths.build_dir]); }) ); gulp.task( "clean-demo", gulp.parallel("clean-translations", function cleanOutputAndBuildDir() { - return del([config.demo_root, config.build_dir]); + return del([paths.demo_output_root, paths.build_dir]); }) ); gulp.task( "clean-cast", gulp.parallel("clean-translations", function cleanOutputAndBuildDir() { - return del([config.cast_root, config.build_dir]); + return del([paths.cast_output_root, paths.build_dir]); }) ); -gulp.task( - "clean-hassio", - gulp.parallel("clean-translations", function cleanOutputAndBuildDir() { - return del([config.hassio_root, config.build_dir]); - }) -); +gulp.task("clean-hassio", function cleanOutputAndBuildDir() { + return del([paths.hassio_output_root, paths.build_dir]); +}); gulp.task( "clean-gallery", gulp.parallel("clean-translations", function cleanOutputAndBuildDir() { - return del([config.gallery_root, config.build_dir]); + return del([paths.gallery_output_root, paths.build_dir]); }) ); diff --git a/build-scripts/gulp/compress.js b/build-scripts/gulp/compress.js index aee165c5f7..12157bc637 100644 --- a/build-scripts/gulp/compress.js +++ b/build-scripts/gulp/compress.js @@ -8,36 +8,36 @@ const paths = require("../paths"); gulp.task("compress-app", function compressApp() { const jsLatest = gulp - .src(path.resolve(paths.output, "**/*.js")) + .src(path.resolve(paths.app_output_latest, "**/*.js")) .pipe(zopfli({ threshold: 150 })) - .pipe(gulp.dest(paths.output)); + .pipe(gulp.dest(paths.app_output_latest)); const jsEs5 = gulp - .src(path.resolve(paths.output_es5, "**/*.js")) + .src(path.resolve(paths.app_output_es5, "**/*.js")) .pipe(zopfli({ threshold: 150 })) - .pipe(gulp.dest(paths.output_es5)); + .pipe(gulp.dest(paths.app_output_es5)); const polyfills = gulp - .src(path.resolve(paths.static, "polyfills/*.js")) + .src(path.resolve(paths.app_output_static, "polyfills/*.js")) .pipe(zopfli({ threshold: 150 })) - .pipe(gulp.dest(path.resolve(paths.static, "polyfills"))); + .pipe(gulp.dest(path.resolve(paths.app_output_static, "polyfills"))); const translations = gulp - .src(path.resolve(paths.static, "translations/**/*.json")) + .src(path.resolve(paths.app_output_static, "translations/**/*.json")) .pipe(zopfli({ threshold: 150 })) - .pipe(gulp.dest(path.resolve(paths.static, "translations"))); + .pipe(gulp.dest(path.resolve(paths.app_output_static, "translations"))); const icons = gulp - .src(path.resolve(paths.static, "mdi/*.json")) + .src(path.resolve(paths.app_output_static, "mdi/*.json")) .pipe(zopfli({ threshold: 150 })) - .pipe(gulp.dest(path.resolve(paths.static, "mdi"))); + .pipe(gulp.dest(path.resolve(paths.app_output_static, "mdi"))); return merge(jsLatest, jsEs5, polyfills, translations, icons); }); gulp.task("compress-hassio", function compressApp() { return gulp - .src(path.resolve(paths.hassio_root, "**/*.js")) + .src(path.resolve(paths.hassio_output_root, "**/*.js")) .pipe(zopfli()) - .pipe(gulp.dest(paths.hassio_root)); + .pipe(gulp.dest(paths.hassio_output_root)); }); diff --git a/build-scripts/gulp/demo.js b/build-scripts/gulp/demo.js index eef9ebbecf..466ade84b2 100644 --- a/build-scripts/gulp/demo.js +++ b/build-scripts/gulp/demo.js @@ -1,6 +1,8 @@ // Run demo develop mode const gulp = require("gulp"); +const env = require("../env"); + require("./clean.js"); require("./translations.js"); require("./gen-icons-json.js"); @@ -8,6 +10,7 @@ require("./gather-static.js"); require("./webpack.js"); require("./service-worker.js"); require("./entry-html.js"); +require("./rollup.js"); gulp.task( "develop-demo", @@ -19,7 +22,7 @@ gulp.task( "translations-enable-merge-backend", gulp.parallel("gen-icons-json", "gen-index-demo-dev", "build-translations"), "copy-static-demo", - "webpack-dev-server-demo" + env.useRollup() ? "rollup-dev-server-demo" : "webpack-dev-server-demo" ) ); @@ -34,7 +37,7 @@ gulp.task( "translations-enable-merge-backend", gulp.parallel("gen-icons-json", "build-translations"), "copy-static-demo", - "webpack-prod-demo", + env.useRollup() ? "rollup-prod-demo" : "webpack-prod-demo", "gen-index-demo-prod" ) ); diff --git a/build-scripts/gulp/entry-html.js b/build-scripts/gulp/entry-html.js index 71fd5dd0e8..c1be430039 100644 --- a/build-scripts/gulp/entry-html.js +++ b/build-scripts/gulp/entry-html.js @@ -6,31 +6,36 @@ 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 paths = require("../paths.js"); +const env = require("../env.js"); const templatePath = (tpl) => - path.resolve(config.polymer_dir, "src/html/", `${tpl}.html.template`); + path.resolve(paths.polymer_dir, "src/html/", `${tpl}.html.template`); const readFile = (pth) => fs.readFileSync(pth).toString(); const renderTemplate = (pth, data = {}, pathFunc = templatePath) => { const compiled = template(readFile(pathFunc(pth))); - return compiled({ ...data, renderTemplate }); + return compiled({ + ...data, + useRollup: env.useRollup(), + renderTemplate, + }); }; const renderDemoTemplate = (pth, data = {}) => renderTemplate(pth, data, (tpl) => - path.resolve(config.demo_dir, "src/html/", `${tpl}.html.template`) + path.resolve(paths.demo_dir, "src/html/", `${tpl}.html.template`) ); const renderCastTemplate = (pth, data = {}) => renderTemplate(pth, data, (tpl) => - path.resolve(config.cast_dir, "src/html/", `${tpl}.html.template`) + path.resolve(paths.cast_dir, "src/html/", `${tpl}.html.template`) ); const renderGalleryTemplate = (pth, data = {}) => renderTemplate(pth, data, (tpl) => - path.resolve(config.gallery_dir, "src/html/", `${tpl}.html.template`) + path.resolve(paths.gallery_dir, "src/html/", `${tpl}.html.template`) ); const minifyHtml = (content) => @@ -48,29 +53,36 @@ gulp.task("gen-pages-dev", (done) => { const content = renderTemplate(page, { latestPageJS: `/frontend_latest/${page}.js`, - es5Compatibility: "/frontend_es5/compatibility.js", es5PageJS: `/frontend_es5/${page}.js`, }); - fs.outputFileSync(path.resolve(config.root, `${page}.html`), content); + fs.outputFileSync( + path.resolve(paths.app_output_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")); + const latestManifest = require(path.resolve( + paths.app_output_latest, + "manifest.json" + )); + const es5Manifest = require(path.resolve( + paths.app_output_es5, + "manifest.json" + )); for (const page of PAGES) { const content = renderTemplate(page, { latestPageJS: latestManifest[`${page}.js`], - es5Compatibility: es5Manifest["compatibility.js"], es5PageJS: es5Manifest[`${page}.js`], }); fs.outputFileSync( - path.resolve(config.root, `${page}.html`), + path.resolve(paths.app_output_root, `${page}.html`), minifyHtml(content) ); } @@ -85,32 +97,39 @@ gulp.task("gen-index-app-dev", (done) => { latestCoreJS: "/frontend_latest/core.js", latestCustomPanelJS: "/frontend_latest/custom-panel.js", - es5Compatibility: "/frontend_es5/compatibility.js", es5AppJS: "/frontend_es5/app.js", es5CoreJS: "/frontend_es5/core.js", es5CustomPanelJS: "/frontend_es5/custom-panel.js", }).replace(/#THEMEC/g, "{{ theme_color }}"); - fs.outputFileSync(path.resolve(config.root, "index.html"), content); + fs.outputFileSync(path.resolve(paths.app_output_root, "index.html"), content); done(); }); gulp.task("gen-index-app-prod", (done) => { - const latestManifest = require(path.resolve(config.output, "manifest.json")); - const es5Manifest = require(path.resolve(config.output_es5, "manifest.json")); + const latestManifest = require(path.resolve( + paths.app_output_latest, + "manifest.json" + )); + const es5Manifest = require(path.resolve( + paths.app_output_es5, + "manifest.json" + )); const content = renderTemplate("index", { latestAppJS: latestManifest["app.js"], latestCoreJS: latestManifest["core.js"], latestCustomPanelJS: latestManifest["custom-panel.js"], - es5Compatibility: es5Manifest["compatibility.js"], es5AppJS: es5Manifest["app.js"], es5CoreJS: es5Manifest["core.js"], es5CustomPanelJS: es5Manifest["custom-panel.js"], }); const minified = minifyHtml(content).replace(/#THEMEC/g, "{{ theme_color }}"); - fs.outputFileSync(path.resolve(config.root, "index.html"), minified); + fs.outputFileSync( + path.resolve(paths.app_output_root, "index.html"), + minified + ); done(); }); @@ -119,7 +138,7 @@ gulp.task("gen-index-cast-dev", (done) => { latestReceiverJS: "/frontend_latest/receiver.js", }); fs.outputFileSync( - path.resolve(config.cast_root, "receiver.html"), + path.resolve(paths.cast_output_root, "receiver.html"), contentReceiver ); @@ -127,14 +146,17 @@ gulp.task("gen-index-cast-dev", (done) => { latestLauncherJS: "/frontend_latest/launcher.js", es5LauncherJS: "/frontend_es5/launcher.js", }); - fs.outputFileSync(path.resolve(config.cast_root, "faq.html"), contentFAQ); + fs.outputFileSync( + path.resolve(paths.cast_output_root, "faq.html"), + contentFAQ + ); const contentLauncher = renderCastTemplate("launcher", { latestLauncherJS: "/frontend_latest/launcher.js", es5LauncherJS: "/frontend_es5/launcher.js", }); fs.outputFileSync( - path.resolve(config.cast_root, "index.html"), + path.resolve(paths.cast_output_root, "index.html"), contentLauncher ); done(); @@ -142,11 +164,11 @@ gulp.task("gen-index-cast-dev", (done) => { gulp.task("gen-index-cast-prod", (done) => { const latestManifest = require(path.resolve( - config.cast_output, + paths.cast_output_latest, "manifest.json" )); const es5Manifest = require(path.resolve( - config.cast_output_es5, + paths.cast_output_es5, "manifest.json" )); @@ -154,7 +176,7 @@ gulp.task("gen-index-cast-prod", (done) => { latestReceiverJS: latestManifest["receiver.js"], }); fs.outputFileSync( - path.resolve(config.cast_root, "receiver.html"), + path.resolve(paths.cast_output_root, "receiver.html"), contentReceiver ); @@ -162,14 +184,17 @@ gulp.task("gen-index-cast-prod", (done) => { latestLauncherJS: latestManifest["launcher.js"], es5LauncherJS: es5Manifest["launcher.js"], }); - fs.outputFileSync(path.resolve(config.cast_root, "faq.html"), contentFAQ); + fs.outputFileSync( + path.resolve(paths.cast_output_root, "faq.html"), + contentFAQ + ); const contentLauncher = renderCastTemplate("launcher", { latestLauncherJS: latestManifest["launcher.js"], es5LauncherJS: es5Manifest["launcher.js"], }); fs.outputFileSync( - path.resolve(config.cast_root, "index.html"), + path.resolve(paths.cast_output_root, "index.html"), contentLauncher ); done(); @@ -181,32 +206,36 @@ gulp.task("gen-index-demo-dev", (done) => { const content = renderDemoTemplate("index", { latestDemoJS: "/frontend_latest/main.js", - es5Compatibility: "/frontend_es5/compatibility.js", es5DemoJS: "/frontend_es5/main.js", }); - fs.outputFileSync(path.resolve(config.demo_root, "index.html"), content); + fs.outputFileSync( + path.resolve(paths.demo_output_root, "index.html"), + content + ); done(); }); gulp.task("gen-index-demo-prod", (done) => { const latestManifest = require(path.resolve( - config.demo_output, + paths.demo_output_latest, "manifest.json" )); const es5Manifest = require(path.resolve( - config.demo_output_es5, + paths.demo_output_es5, "manifest.json" )); const content = renderDemoTemplate("index", { latestDemoJS: latestManifest["main.js"], - es5Compatibility: es5Manifest["compatibility.js"], es5DemoJS: es5Manifest["main.js"], }); const minified = minifyHtml(content); - fs.outputFileSync(path.resolve(config.demo_root, "index.html"), minified); + fs.outputFileSync( + path.resolve(paths.demo_output_root, "index.html"), + minified + ); done(); }); @@ -217,13 +246,16 @@ gulp.task("gen-index-gallery-dev", (done) => { latestGalleryJS: "./frontend_latest/entrypoint.js", }); - fs.outputFileSync(path.resolve(config.gallery_root, "index.html"), content); + fs.outputFileSync( + path.resolve(paths.gallery_output_root, "index.html"), + content + ); done(); }); gulp.task("gen-index-gallery-prod", (done) => { const latestManifest = require(path.resolve( - config.gallery_output, + paths.gallery_output_latest, "manifest.json" )); const content = renderGalleryTemplate("index", { @@ -231,6 +263,9 @@ gulp.task("gen-index-gallery-prod", (done) => { }); const minified = minifyHtml(content); - fs.outputFileSync(path.resolve(config.gallery_root, "index.html"), minified); + fs.outputFileSync( + path.resolve(paths.gallery_output_root, "index.html"), + minified + ); done(); }); diff --git a/build-scripts/gulp/gallery.js b/build-scripts/gulp/gallery.js index 559b0debde..ec430118e5 100644 --- a/build-scripts/gulp/gallery.js +++ b/build-scripts/gulp/gallery.js @@ -1,6 +1,8 @@ // Run demo develop mode const gulp = require("gulp"); +const env = require("../env"); + require("./clean.js"); require("./translations.js"); require("./gen-icons-json.js"); @@ -8,6 +10,7 @@ require("./gather-static.js"); require("./webpack.js"); require("./service-worker.js"); require("./entry-html.js"); +require("./rollup.js"); gulp.task( "develop-gallery", @@ -20,7 +23,7 @@ gulp.task( gulp.parallel("gen-icons-json", "build-translations"), "copy-static-gallery", "gen-index-gallery-dev", - "webpack-dev-server-gallery" + env.useRollup() ? "rollup-dev-server-gallery" : "webpack-dev-server-gallery" ) ); @@ -34,7 +37,7 @@ gulp.task( "translations-enable-merge-backend", gulp.parallel("gen-icons-json", "build-translations"), "copy-static-gallery", - "webpack-prod-gallery", + env.useRollup() ? "rollup-prod-gallery" : "webpack-prod-gallery", "gen-index-gallery-prod" ) ); diff --git a/build-scripts/gulp/gather-static.js b/build-scripts/gulp/gather-static.js index 7dc4e19443..0a16bf636e 100644 --- a/build-scripts/gulp/gather-static.js +++ b/build-scripts/gulp/gather-static.js @@ -51,6 +51,12 @@ function copyPolyfills(staticDir) { ); } +function copyLoaderJS(staticDir) { + const staticPath = genStaticPath(staticDir); + copyFileDir(npmPath("systemjs/dist/s.min.js"), staticPath("js")); + copyFileDir(npmPath("systemjs/dist/s.min.js.map"), staticPath("js")); +} + function copyFonts(staticDir) { const staticPath = genStaticPath(staticDir); // Local fonts @@ -72,17 +78,17 @@ function copyMapPanel(staticDir) { ); } -gulp.task("copy-translations", (done) => { - const staticDir = paths.static; +gulp.task("copy-translations-app", async () => { + const staticDir = paths.app_output_static; copyTranslations(staticDir); - done(); }); -gulp.task("copy-static", (done) => { - const staticDir = paths.static; +gulp.task("copy-static-app", async () => { + const staticDir = paths.app_output_static; // Basic static files - fs.copySync(polyPath("public"), paths.root); + fs.copySync(polyPath("public"), paths.app_output_root); + copyLoaderJS(staticDir); copyPolyfills(staticDir); copyFonts(staticDir); copyTranslations(staticDir); @@ -90,48 +96,50 @@ gulp.task("copy-static", (done) => { // Panel assets copyMapPanel(staticDir); - done(); }); -gulp.task("copy-static-demo", (done) => { +gulp.task("copy-static-demo", async () => { // Copy app static files fs.copySync( polyPath("public/static"), - path.resolve(paths.demo_root, "static") + path.resolve(paths.demo_output_root, "static") ); // Copy demo static files - fs.copySync(path.resolve(paths.demo_dir, "public"), paths.demo_root); + fs.copySync(path.resolve(paths.demo_dir, "public"), paths.demo_output_root); - copyPolyfills(paths.demo_static); - copyMapPanel(paths.demo_static); - copyFonts(paths.demo_static); - copyTranslations(paths.demo_static); - copyMdiIcons(paths.demo_static); - done(); + copyLoaderJS(paths.demo_output_static); + copyPolyfills(paths.demo_output_static); + copyMapPanel(paths.demo_output_static); + copyFonts(paths.demo_output_static); + copyTranslations(paths.demo_output_static); + copyMdiIcons(paths.demo_output_static); }); -gulp.task("copy-static-cast", (done) => { +gulp.task("copy-static-cast", async () => { // Copy app static files - fs.copySync(polyPath("public/static"), paths.cast_static); + fs.copySync(polyPath("public/static"), paths.cast_output_static); // Copy cast static files - fs.copySync(path.resolve(paths.cast_dir, "public"), paths.cast_root); + fs.copySync(path.resolve(paths.cast_dir, "public"), paths.cast_output_root); - copyMapPanel(paths.cast_static); - copyFonts(paths.cast_static); - copyTranslations(paths.cast_static); - copyMdiIcons(paths.cast_static); - done(); + copyLoaderJS(paths.cast_output_static); + copyPolyfills(paths.cast_output_static); + copyMapPanel(paths.cast_output_static); + copyFonts(paths.cast_output_static); + copyTranslations(paths.cast_output_static); + copyMdiIcons(paths.cast_output_static); }); -gulp.task("copy-static-gallery", (done) => { +gulp.task("copy-static-gallery", async () => { // Copy app static files - fs.copySync(polyPath("public/static"), paths.gallery_static); + fs.copySync(polyPath("public/static"), paths.gallery_output_static); // Copy gallery static files - fs.copySync(path.resolve(paths.gallery_dir, "public"), paths.gallery_root); + fs.copySync( + path.resolve(paths.gallery_dir, "public"), + paths.gallery_output_root + ); - copyMapPanel(paths.gallery_static); - copyFonts(paths.gallery_static); - copyTranslations(paths.gallery_static); - copyMdiIcons(paths.gallery_static); - done(); + copyMapPanel(paths.gallery_output_static); + copyFonts(paths.gallery_output_static); + copyTranslations(paths.gallery_output_static); + copyMdiIcons(paths.gallery_output_static); }); diff --git a/build-scripts/gulp/hassio.js b/build-scripts/gulp/hassio.js index 9591d60278..595752008c 100644 --- a/build-scripts/gulp/hassio.js +++ b/build-scripts/gulp/hassio.js @@ -1,11 +1,12 @@ const gulp = require("gulp"); -const envVars = require("../env"); +const env = require("../env"); require("./clean.js"); require("./gen-icons-json.js"); require("./webpack.js"); require("./compress.js"); +require("./rollup.js"); gulp.task( "develop-hassio", @@ -15,7 +16,7 @@ gulp.task( }, "clean-hassio", "gen-icons-json", - "webpack-watch-hassio" + env.useRollup() ? "rollup-watch-hassio" : "webpack-watch-hassio" ) ); @@ -27,8 +28,8 @@ gulp.task( }, "clean-hassio", "gen-icons-json", - "webpack-prod-hassio", + env.useRollup() ? "rollup-prod-hassio" : "webpack-prod-hassio", ...// Don't compress running tests - (envVars.isTest() ? [] : ["compress-hassio"]) + (env.isTest() ? [] : ["compress-hassio"]) ) ); diff --git a/build-scripts/gulp/rollup.js b/build-scripts/gulp/rollup.js new file mode 100644 index 0000000000..b186f15407 --- /dev/null +++ b/build-scripts/gulp/rollup.js @@ -0,0 +1,155 @@ +// Tasks to run Rollup +const path = require("path"); +const gulp = require("gulp"); +const rollup = require("rollup"); +const handler = require("serve-handler"); +const http = require("http"); +const log = require("fancy-log"); +const rollupConfig = require("../rollup"); +const paths = require("../paths"); +const open = require("open"); + +const bothBuilds = (createConfigFunc, params) => + gulp.series( + async function buildLatest() { + await buildRollup( + createConfigFunc({ + ...params, + latestBuild: true, + }) + ); + }, + async function buildES5() { + await buildRollup( + createConfigFunc({ + ...params, + latestBuild: false, + }) + ); + } + ); + +function createServer(serveOptions) { + const server = http.createServer((request, response) => { + return handler(request, response, { + public: serveOptions.root, + }); + }); + + server.listen( + serveOptions.port, + serveOptions.networkAccess ? "0.0.0.0" : undefined, + () => { + log.info(`Available at http://localhost:${serveOptions.port}`); + open(`http://localhost:${serveOptions.port}`); + } + ); +} + +function watchRollup(createConfig, extraWatchSrc = [], serveOptions) { + const { inputOptions, outputOptions } = createConfig({ + isProdBuild: false, + latestBuild: true, + }); + + const watcher = rollup.watch({ + ...inputOptions, + output: [outputOptions], + watch: { + include: ["src/**"] + extraWatchSrc, + }, + }); + + let startedHttp = false; + + watcher.on("event", (event) => { + if (event.code === "BUNDLE_END") { + log(`Build done @ ${new Date().toLocaleTimeString()}`); + } else if (event.code === "ERROR") { + log.error(event.error); + } else if (event.code === "END") { + if (startedHttp || !serveOptions) { + return; + } + startedHttp = true; + createServer(serveOptions); + } + }); + + gulp.watch( + path.join(paths.translations_src, "en.json"), + gulp.series("build-translations", "copy-translations-app") + ); +} + +async function buildRollup(config) { + const bundle = await rollup.rollup(config.inputOptions); + await bundle.write(config.outputOptions); +} + +gulp.task("rollup-watch-app", () => { + watchRollup(rollupConfig.createAppConfig); +}); + +gulp.task("rollup-watch-hassio", () => { + watchRollup( + // Force latestBuild = false for hassio config. + (conf) => rollupConfig.createHassioConfig({ ...conf, latestBuild: false }), + ["hassio/src/**"] + ); +}); + +gulp.task("rollup-dev-server-demo", () => { + watchRollup(rollupConfig.createDemoConfig, ["demo/src/**"], { + root: paths.demo_output_root, + port: 8090, + }); +}); + +gulp.task("rollup-dev-server-cast", () => { + watchRollup(rollupConfig.createCastConfig, ["cast/src/**"], { + root: paths.cast_output_root, + port: 8080, + networkAccess: true, + }); +}); + +gulp.task("rollup-dev-server-gallery", () => { + watchRollup(rollupConfig.createGalleryConfig, ["gallery/src/**"], { + root: paths.gallery_output_root, + port: 8100, + }); +}); + +gulp.task( + "rollup-prod-app", + bothBuilds(rollupConfig.createAppConfig, { isProdBuild: true }) +); + +gulp.task( + "rollup-prod-demo", + bothBuilds(rollupConfig.createDemoConfig, { isProdBuild: true }) +); + +gulp.task( + "rollup-prod-cast", + bothBuilds(rollupConfig.createCastConfig, { isProdBuild: true }) +); + +gulp.task("rollup-prod-hassio", () => + buildRollup( + rollupConfig.createHassioConfig({ + isProdBuild: true, + latestBuild: false, + }) + ) +); + +gulp.task("rollup-prod-gallery", () => + buildRollup( + rollupConfig.createGalleryConfig({ + isProdBuild: true, + latestBuild: true, + }) + ) +); diff --git a/build-scripts/gulp/service-worker.js b/build-scripts/gulp/service-worker.js index 01e7a97323..d0b16f7c9c 100644 --- a/build-scripts/gulp/service-worker.js +++ b/build-scripts/gulp/service-worker.js @@ -9,7 +9,7 @@ const workboxBuild = require("workbox-build"); const sourceMapUrl = require("source-map-url"); const paths = require("../paths.js"); -const swDest = path.resolve(paths.root, "service_worker.js"); +const swDest = path.resolve(paths.app_output_root, "service_worker.js"); const writeSW = (content) => fs.outputFileSync(swDest, content.trim() + "\n"); @@ -31,32 +31,38 @@ self.addEventListener('install', (event) => { gulp.task("gen-service-worker-app-prod", async () => { // Read bundled source file const bundleManifestLatest = require(path.resolve( - paths.output, + paths.app_output_latest, "manifest.json" )); let serviceWorkerContent = fs.readFileSync( - paths.root + bundleManifestLatest["service_worker.js"], + paths.app_output_root + bundleManifestLatest["service_worker.js"], "utf-8" ); // Delete old file from frontend_latest so manifest won't pick it up - fs.removeSync(paths.root + bundleManifestLatest["service_worker.js"]); - fs.removeSync(paths.root + bundleManifestLatest["service_worker.js.map"]); + fs.removeSync( + paths.app_output_root + bundleManifestLatest["service_worker.js"] + ); + fs.removeSync( + paths.app_output_root + bundleManifestLatest["service_worker.js.map"] + ); // Remove ES5 const bundleManifestES5 = require(path.resolve( - paths.output_es5, + paths.app_output_es5, "manifest.json" )); - fs.removeSync(paths.root + bundleManifestES5["service_worker.js"]); - fs.removeSync(paths.root + bundleManifestES5["service_worker.js.map"]); + fs.removeSync(paths.app_output_root + bundleManifestES5["service_worker.js"]); + fs.removeSync( + paths.app_output_root + bundleManifestES5["service_worker.js.map"] + ); const workboxManifest = await workboxBuild.getManifest({ // Files that mach this pattern will be considered unique and skip revision check // ignore JS files + translation files dontCacheBustURLsMatching: /(frontend_latest\/.+|static\/translations\/.+)/, - globDirectory: paths.root, + globDirectory: paths.app_output_root, globPatterns: [ "frontend_latest/*.js", // Cache all English translations because we catch them as fallback diff --git a/build-scripts/gulp/webpack.js b/build-scripts/gulp/webpack.js index a9e39c7bd5..e7e58eb726 100644 --- a/build-scripts/gulp/webpack.js +++ b/build-scripts/gulp/webpack.js @@ -38,9 +38,9 @@ const runDevServer = ({ const handler = (done) => (err, stats) => { if (err) { - console.log(err.stack || err); + log.error(err.stack || err); if (err.details) { - console.log(err.details); + log.error(err.details); } return; } @@ -48,7 +48,7 @@ const handler = (done) => (err, stats) => { log(`Build done @ ${new Date().toLocaleTimeString()}`); if (stats.hasErrors() || stats.hasWarnings()) { - console.log(stats.toString("minimal")); + log.warn(stats.toString("minimal")); } if (done) { @@ -64,7 +64,7 @@ gulp.task("webpack-watch-app", () => { ); gulp.watch( path.join(paths.translations_src, "en.json"), - gulp.series("build-translations", "copy-translations") + gulp.series("build-translations", "copy-translations-app") ); }); @@ -82,7 +82,7 @@ gulp.task( gulp.task("webpack-dev-server-demo", () => { runDevServer({ compiler: webpack(bothBuilds(createDemoConfig, { isProdBuild: false })), - contentBase: paths.demo_root, + contentBase: paths.demo_output_root, port: 8090, }); }); @@ -103,7 +103,7 @@ gulp.task( gulp.task("webpack-dev-server-cast", () => { runDevServer({ compiler: webpack(bothBuilds(createCastConfig, { isProdBuild: false })), - contentBase: paths.cast_root, + contentBase: paths.cast_output_root, port: 8080, // Accessible from the network, because that's how Cast hits it. listenHost: "0.0.0.0", @@ -152,7 +152,7 @@ gulp.task("webpack-dev-server-gallery", () => { runDevServer({ // We don't use the es5 build, but the dev server will fuck up the publicPath if we don't compiler: webpack(bothBuilds(createGalleryConfig, { isProdBuild: false })), - contentBase: paths.gallery_root, + contentBase: paths.gallery_output_root, port: 8100, }); }); diff --git a/build-scripts/paths.js b/build-scripts/paths.js index 315cd554db..8289890c59 100644 --- a/build-scripts/paths.js +++ b/build-scripts/paths.js @@ -4,30 +4,36 @@ 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"), + app_output_root: path.resolve(__dirname, "../hass_frontend"), + app_output_static: path.resolve(__dirname, "../hass_frontend/static"), + app_output_latest: path.resolve( + __dirname, + "../hass_frontend/frontend_latest" + ), + app_output_es5: path.resolve(__dirname, "../hass_frontend/frontend_es5"), demo_dir: path.resolve(__dirname, "../demo"), - demo_root: path.resolve(__dirname, "../demo/dist"), - demo_static: path.resolve(__dirname, "../demo/dist/static"), - demo_output: path.resolve(__dirname, "../demo/dist/frontend_latest"), + demo_output_root: path.resolve(__dirname, "../demo/dist"), + demo_output_static: path.resolve(__dirname, "../demo/dist/static"), + demo_output_latest: path.resolve(__dirname, "../demo/dist/frontend_latest"), demo_output_es5: path.resolve(__dirname, "../demo/dist/frontend_es5"), cast_dir: path.resolve(__dirname, "../cast"), - cast_root: path.resolve(__dirname, "../cast/dist"), - cast_static: path.resolve(__dirname, "../cast/dist/static"), - cast_output: path.resolve(__dirname, "../cast/dist/frontend_latest"), + cast_output_root: path.resolve(__dirname, "../cast/dist"), + cast_output_static: path.resolve(__dirname, "../cast/dist/static"), + cast_output_latest: path.resolve(__dirname, "../cast/dist/frontend_latest"), cast_output_es5: path.resolve(__dirname, "../cast/dist/frontend_es5"), gallery_dir: path.resolve(__dirname, "../gallery"), - gallery_root: path.resolve(__dirname, "../gallery/dist"), - gallery_output: path.resolve(__dirname, "../gallery/dist/frontend_latest"), - gallery_static: path.resolve(__dirname, "../gallery/dist/static"), + gallery_output_root: path.resolve(__dirname, "../gallery/dist"), + gallery_output_latest: path.resolve( + __dirname, + "../gallery/dist/frontend_latest" + ), + gallery_output_static: path.resolve(__dirname, "../gallery/dist/static"), hassio_dir: path.resolve(__dirname, "../hassio"), - hassio_root: path.resolve(__dirname, "../hassio/build"), + hassio_output_root: path.resolve(__dirname, "../hassio/build"), hassio_publicPath: "/api/hassio/app/", translations_src: path.resolve(__dirname, "../src/translations"), diff --git a/build-scripts/rollup-plugins/dont-hash-plugin.js b/build-scripts/rollup-plugins/dont-hash-plugin.js new file mode 100644 index 0000000000..89082b90c2 --- /dev/null +++ b/build-scripts/rollup-plugins/dont-hash-plugin.js @@ -0,0 +1,14 @@ +module.exports = function (opts = {}) { + const dontHash = opts.dontHash || new Set(); + + return { + name: "dont-hash", + renderChunk(_code, chunk, _options) { + if (!chunk.isEntry || !dontHash.has(chunk.name)) { + return null; + } + chunk.fileName = `${chunk.name}.js`; + return null; + }, + }; +}; diff --git a/build-scripts/rollup-plugins/ignore-plugin.js b/build-scripts/rollup-plugins/ignore-plugin.js new file mode 100644 index 0000000000..4bebb69095 --- /dev/null +++ b/build-scripts/rollup-plugins/ignore-plugin.js @@ -0,0 +1,26 @@ +const path = require("path"); + +module.exports = function (userOptions = {}) { + // Files need to be absolute paths. + // This only works if the file has no exports + // and only is imported for its side effects + const files = userOptions.files || []; + + if (files.length === 0) { + return { + name: "ignore", + }; + } + + return { + name: "ignore", + + load(id) { + return files.some((toIgnorePath) => id.startsWith(toIgnorePath)) + ? { + code: "", + } + : null; + }, + }; +}; diff --git a/build-scripts/rollup-plugins/manifest-plugin.js b/build-scripts/rollup-plugins/manifest-plugin.js new file mode 100644 index 0000000000..bf4bbaac05 --- /dev/null +++ b/build-scripts/rollup-plugins/manifest-plugin.js @@ -0,0 +1,34 @@ +const url = require("url"); + +const defaultOptions = { + publicPath: "", +}; + +module.exports = function (userOptions = {}) { + const options = { ...defaultOptions, ...userOptions }; + + return { + name: "manifest", + generateBundle(outputOptions, bundle) { + const manifest = {}; + + for (const chunk of Object.values(bundle)) { + if (!chunk.isEntry) { + continue; + } + // Add js extension to mimic Webpack manifest. + manifest[`${chunk.name}.js`] = url.resolve( + options.publicPath, + chunk.fileName + ); + } + + this.emitFile({ + type: "asset", + source: JSON.stringify(manifest, undefined, 2), + name: "manifest.json", + fileName: "manifest.json", + }); + }, + }; +}; diff --git a/build-scripts/rollup-plugins/worker-plugin.js b/build-scripts/rollup-plugins/worker-plugin.js new file mode 100644 index 0000000000..0aa05273ca --- /dev/null +++ b/build-scripts/rollup-plugins/worker-plugin.js @@ -0,0 +1,149 @@ +// Worker plugin +// Each worker will include all of its dependencies +// instead of relying on an importer. + +// Forked from v.1.4.1 +// https://github.com/surma/rollup-plugin-off-main-thread +/** + * Copyright 2018 Google Inc. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const rollup = require("rollup"); +const path = require("path"); +const MagicString = require("magic-string"); + +const defaultOpts = { + // A RegExp to find `new Workers()` calls. The second capture group _must_ + // capture the provided file name without the quotes. + workerRegexp: /new Worker\((["'])(.+?)\1(,[^)]+)?\)/g, + plugins: ["node-resolve", "commonjs", "babel", "terser", "ignore"], +}; + +async function getBundledWorker(workerPath, rollupOptions) { + const bundle = await rollup.rollup({ + ...rollupOptions, + input: { + worker: workerPath, + }, + }); + const { output } = await bundle.generate({ + // Generates cleanest output, we shouldn't have any imports/exports + // that would be incompatible with ES5. + format: "es", + // We should not export anything. This will fail build if we are. + exports: "none", + }); + + let code; + + for (const chunkOrAsset of output) { + if (chunkOrAsset.name === "worker") { + code = chunkOrAsset.code; + } else if (chunkOrAsset.type !== "asset") { + throw new Error("Unexpected extra output"); + } + } + + return code; +} + +module.exports = function (opts = {}) { + opts = { ...defaultOpts, ...opts }; + + let rollupOptions; + let refIds; + + return { + name: "hass-worker", + + async buildStart(options) { + refIds = {}; + rollupOptions = { + plugins: options.plugins.filter((plugin) => + opts.plugins.includes(plugin.name) + ), + }; + }, + + async transform(code, id) { + // Copy the regexp as they are stateful and this hook is async. + const workerRegexp = new RegExp( + opts.workerRegexp.source, + opts.workerRegexp.flags + ); + if (!workerRegexp.test(code)) { + return; + } + + const ms = new MagicString(code); + // Reset the regexp + workerRegexp.lastIndex = 0; + while (true) { + const match = workerRegexp.exec(code); + if (!match) { + break; + } + + const workerFile = match[2]; + let optionsObject = {}; + // Parse the optional options object + if (match[3] && match[3].length > 0) { + // FIXME: ooooof! + optionsObject = new Function(`return ${match[3].slice(1)};`)(); + } + delete optionsObject.type; + + if (!new RegExp("^.*/").test(workerFile)) { + this.warn( + `Paths passed to the Worker constructor must be relative or absolute, i.e. start with /, ./ or ../ (just like dynamic import!). Ignoring "${workerFile}".` + ); + continue; + } + + // Find worker file and store it as a chunk with ID prefixed for our loader + const resolvedWorkerFile = (await this.resolve(workerFile, id)).id; + let chunkRefId; + if (resolvedWorkerFile in refIds) { + chunkRefId = refIds[resolvedWorkerFile]; + } else { + this.addWatchFile(resolvedWorkerFile); + const source = await getBundledWorker( + resolvedWorkerFile, + rollupOptions + ); + chunkRefId = refIds[resolvedWorkerFile] = this.emitFile({ + name: path.basename(resolvedWorkerFile), + source, + type: "asset", + }); + } + + const workerParametersStartIndex = match.index + "new Worker(".length; + const workerParametersEndIndex = + match.index + match[0].length - ")".length; + + ms.overwrite( + workerParametersStartIndex, + workerParametersEndIndex, + `import.meta.ROLLUP_FILE_URL_${chunkRefId}, ${JSON.stringify( + optionsObject + )}` + ); + } + + return { + code: ms.toString(), + map: ms.generateMap({ hires: true }), + }; + }, + }; +}; diff --git a/build-scripts/rollup.js b/build-scripts/rollup.js new file mode 100644 index 0000000000..3757f25866 --- /dev/null +++ b/build-scripts/rollup.js @@ -0,0 +1,151 @@ +const path = require("path"); + +const commonjs = require("@rollup/plugin-commonjs"); +const resolve = require("@rollup/plugin-node-resolve"); +const json = require("@rollup/plugin-json"); +const babel = require("rollup-plugin-babel"); +const replace = require("@rollup/plugin-replace"); +const visualizer = require("rollup-plugin-visualizer"); +const { string } = require("rollup-plugin-string"); +const { terser } = require("rollup-plugin-terser"); +const manifest = require("./rollup-plugins/manifest-plugin"); +const worker = require("./rollup-plugins/worker-plugin"); +const dontHashPlugin = require("./rollup-plugins/dont-hash-plugin"); +const ignore = require("./rollup-plugins/ignore-plugin"); + +const bundle = require("./bundle"); +const paths = require("./paths"); + +const extensions = [".js", ".ts"]; + +/** + * @param {Object} arg + * @param { import("rollup").InputOption } arg.input + */ +const createRollupConfig = ({ + entry, + outputPath, + defineOverlay, + isProdBuild, + latestBuild, + isStatsBuild, + publicPath, + dontHash, +}) => { + return { + /** + * @type { import("rollup").InputOptions } + */ + inputOptions: { + input: entry, + // Some entry points contain no JavaScript. This setting silences a warning about that. + // https://rollupjs.org/guide/en/#preserveentrysignatures + preserveEntrySignatures: false, + plugins: [ + ignore({ + files: bundle.emptyPackages({ latestBuild }), + }), + resolve({ + extensions, + preferBuiltins: false, + browser: true, + rootDir: paths.polymer_dir, + }), + commonjs({ + namedExports: { + "js-yaml": ["safeDump", "safeLoad"], + }, + }), + json(), + babel({ + ...bundle.babelOptions({ latestBuild }), + extensions, + exclude: bundle.babelExclude(), + }), + string({ + // Import certain extensions as strings + include: [path.join(paths.polymer_dir, "node_modules/**/*.css")], + }), + replace( + bundle.definedVars({ isProdBuild, latestBuild, defineOverlay }) + ), + manifest({ + publicPath, + }), + worker(), + dontHashPlugin({ dontHash }), + isProdBuild && terser(bundle.terserOptions(latestBuild)), + isStatsBuild && + visualizer({ + // https://github.com/btd/rollup-plugin-visualizer#options + open: true, + sourcemap: true, + }), + ], + }, + /** + * @type { import("rollup").OutputOptions } + */ + outputOptions: { + // https://rollupjs.org/guide/en/#outputdir + dir: outputPath, + // https://rollupjs.org/guide/en/#outputformat + format: latestBuild ? "es" : "systemjs", + // https://rollupjs.org/guide/en/#outputexternallivebindings + externalLiveBindings: false, + // https://rollupjs.org/guide/en/#outputentryfilenames + // https://rollupjs.org/guide/en/#outputchunkfilenames + // https://rollupjs.org/guide/en/#outputassetfilenames + entryFileNames: + isProdBuild && !isStatsBuild ? "[name]-[hash].js" : "[name].js", + chunkFileNames: + isProdBuild && !isStatsBuild ? "c.[hash].js" : "[name].js", + assetFileNames: + isProdBuild && !isStatsBuild ? "a.[hash].js" : "[name].js", + // https://rollupjs.org/guide/en/#outputsourcemap + sourcemap: isProdBuild ? true : "inline", + }, + }; +}; + +const createAppConfig = ({ isProdBuild, latestBuild, isStatsBuild }) => { + return createRollupConfig( + bundle.config.app({ + isProdBuild, + latestBuild, + isStatsBuild, + }) + ); +}; + +const createDemoConfig = ({ isProdBuild, latestBuild, isStatsBuild }) => { + return createRollupConfig( + bundle.config.demo({ + isProdBuild, + latestBuild, + isStatsBuild, + }) + ); +}; + +const createCastConfig = ({ isProdBuild, latestBuild }) => { + return createRollupConfig(bundle.config.cast({ isProdBuild, latestBuild })); +}; + +const createHassioConfig = ({ isProdBuild, latestBuild }) => { + return createRollupConfig(bundle.config.hassio({ isProdBuild, latestBuild })); +}; + +const createGalleryConfig = ({ isProdBuild, latestBuild }) => { + return createRollupConfig( + bundle.config.gallery({ isProdBuild, latestBuild }) + ); +}; + +module.exports = { + createAppConfig, + createDemoConfig, + createCastConfig, + createHassioConfig, + createGalleryConfig, +}; diff --git a/build-scripts/webpack.js b/build-scripts/webpack.js index 08d419c446..df310b5532 100644 --- a/build-scripts/webpack.js +++ b/build-scripts/webpack.js @@ -4,12 +4,12 @@ const TerserPlugin = require("terser-webpack-plugin"); const ManifestPlugin = require("webpack-manifest-plugin"); const WorkerPlugin = require("worker-plugin"); const paths = require("./paths.js"); -const env = require("./env.js"); -const { babelLoaderConfig } = require("./babel.js"); +const bundle = require("./bundle"); const createWebpackConfig = ({ entry, - outputRoot, + outputPath, + publicPath, defineOverlay, isProdBuild, latestBuild, @@ -19,24 +19,30 @@ const createWebpackConfig = ({ if (!dontHash) { dontHash = new Set(); } + const ignorePackages = bundle.ignorePackages({ latestBuild }); return { mode: isProdBuild ? "production" : "development", devtool: isProdBuild ? "cheap-module-source-map" : "eval-cheap-module-source-map", entry, + node: false, module: { rules: [ - babelLoaderConfig({ latestBuild }), + { + test: /\.js$|\.ts$/, + exclude: bundle.babelExclude(), + use: { + loader: "babel-loader", + options: bundle.babelOptions({ latestBuild }), + }, + }, { test: /\.css$/, use: "raw-loader", }, ], }, - externals: { - esprima: "esprima", - }, optimization: { minimizer: [ new TerserPlugin({ @@ -44,50 +50,50 @@ const createWebpackConfig = ({ parallel: true, extractComments: true, sourceMap: true, - terserOptions: { - safari10: true, - ecma: latestBuild ? undefined : 5, - }, + terserOptions: bundle.terserOptions(latestBuild), }), ], }, plugins: [ new WorkerPlugin(), - new ManifestPlugin(), - new webpack.DefinePlugin({ - __DEV__: !isProdBuild, - __BUILD__: JSON.stringify(latestBuild ? "latest" : "es5"), - __VERSION__: JSON.stringify(env.version()), - __DEMO__: false, - __BACKWARDS_COMPAT__: false, - __STATIC_PATH__: "/static/", - "process.env.NODE_ENV": JSON.stringify( - isProdBuild ? "production" : "development" - ), - ...defineOverlay, + new ManifestPlugin({ + // Only include the JS of entrypoints + filter: (file) => file.isInitial && !file.name.endsWith(".map"), + }), + new webpack.DefinePlugin( + bundle.definedVars({ isProdBuild, latestBuild, defineOverlay }) + ), + new webpack.IgnorePlugin({ + checkResource(resource, context) { + // Only use ignore to intercept imports that we don't control + // inside node_module dependencies. + if ( + !context.includes("/node_modules/") || + // calling define.amd will call require("!!webpack amd options") + resource.startsWith("!!webpack") + ) { + return false; + } + let fullPath; + try { + fullPath = resource.startsWith(".") + ? path.resolve(context, resource) + : require.resolve(resource); + } catch (err) { + console.error("Error in ignore plugin", resource, context); + throw err; + } + + return ignorePackages.some((toIgnorePath) => + fullPath.startsWith(toIgnorePath) + ); + }, }), - // 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$/, + new RegExp(bundle.emptyPackages({ latestBuild }).join("|")), 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") - ), - new webpack.NormalModuleReplacementPlugin( - /@vaadin\/vaadin-material-styles\/font-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") - ), - ].filter(Boolean), + ], resolve: { extensions: [".ts", ".js", ".json"], }, @@ -102,11 +108,8 @@ const createWebpackConfig = ({ isProdBuild && !isStatsBuild ? "chunk.[chunkhash].js" : "[name].chunk.js", - path: path.resolve( - outputRoot, - latestBuild ? "frontend_latest" : "frontend_es5" - ), - publicPath: latestBuild ? "/frontend_latest/" : "/frontend_es5/", + path: outputPath, + publicPath, // To silence warning in worker plugin globalObject: "self", }, @@ -114,94 +117,31 @@ const createWebpackConfig = ({ }; const createAppConfig = ({ isProdBuild, latestBuild, isStatsBuild }) => { - return createWebpackConfig({ - entry: { - service_worker: "./src/entrypoints/service_worker.ts", - 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", - }, - outputRoot: paths.root, - isProdBuild, - latestBuild, - isStatsBuild, - }); + return createWebpackConfig( + bundle.config.app({ isProdBuild, latestBuild, isStatsBuild }) + ); }; const createDemoConfig = ({ isProdBuild, latestBuild, isStatsBuild }) => { - return createWebpackConfig({ - entry: { - main: path.resolve(paths.demo_dir, "src/entrypoint.ts"), - compatibility: path.resolve( - paths.polymer_dir, - "src/entrypoints/compatibility.ts" - ), - }, - outputRoot: paths.demo_root, - defineOverlay: { - __VERSION__: JSON.stringify(`DEMO-${env.version()}`), - __DEMO__: true, - }, - isProdBuild, - latestBuild, - isStatsBuild, - }); + return createWebpackConfig( + bundle.config.demo({ isProdBuild, latestBuild, isStatsBuild }) + ); }; const createCastConfig = ({ isProdBuild, latestBuild }) => { - const entry = { - launcher: path.resolve(paths.cast_dir, "src/launcher/entrypoint.ts"), - }; - - if (latestBuild) { - entry.receiver = path.resolve(paths.cast_dir, "src/receiver/entrypoint.ts"); - } - - return createWebpackConfig({ - entry, - outputRoot: paths.cast_root, - isProdBuild, - latestBuild, - defineOverlay: { - __BACKWARDS_COMPAT__: true, - }, - }); + return createWebpackConfig(bundle.config.cast({ isProdBuild, latestBuild })); }; const createHassioConfig = ({ isProdBuild, latestBuild }) => { - if (latestBuild) { - throw new Error("Hass.io does not support latest build!"); - } - const config = createWebpackConfig({ - entry: { - entrypoint: path.resolve(paths.hassio_dir, "src/entrypoint.ts"), - }, - outputRoot: "", - isProdBuild, - latestBuild, - dontHash: new Set(["entrypoint"]), - }); - - config.output.path = paths.hassio_root; - config.output.publicPath = paths.hassio_publicPath; - - return config; + return createWebpackConfig( + bundle.config.hassio({ isProdBuild, latestBuild }) + ); }; const createGalleryConfig = ({ isProdBuild, latestBuild }) => { - const config = createWebpackConfig({ - entry: { - entrypoint: path.resolve(paths.gallery_dir, "src/entrypoint.js"), - }, - outputRoot: paths.gallery_root, - isProdBuild, - latestBuild, - }); - - return config; + return createWebpackConfig( + bundle.config.gallery({ isProdBuild, latestBuild }) + ); }; module.exports = { diff --git a/cast/rollup.config.js b/cast/rollup.config.js new file mode 100644 index 0000000000..5460a82216 --- /dev/null +++ b/cast/rollup.config.js @@ -0,0 +1,10 @@ +const rollup = require("../build-scripts/rollup.js"); +const env = require("../build-scripts/env.js"); + +const config = rollup.createCastConfig({ + isProdBuild: env.isProdBuild(), + latestBuild: true, + isStatsBuild: env.isStatsBuild(), +}); + +module.exports = { ...config.inputOptions, output: config.outputOptions }; diff --git a/cast/src/html/launcher-faq.html.template b/cast/src/html/launcher-faq.html.template index 20640e8b54..30589f2703 100644 --- a/cast/src/html/launcher-faq.html.template +++ b/cast/src/html/launcher-faq.html.template @@ -46,7 +46,13 @@ // // Safari 10.1 supports type=module but ignores nomodule, so we add this check. if (!isS101) { _ls("/static/polyfills/custom-elements-es5-adapter.js"); - _ls("<%= es5LauncherJS %>"); + <% if (useRollup) { %> + _ls("/static/js/s.min.js").onload = function() { + System.import("<%= es5LauncherJS %>"); + }; + <% } else { %> + _ls("<%= es5LauncherJS %>"); + <% } %> } })(); diff --git a/cast/src/html/launcher.html.template b/cast/src/html/launcher.html.template index 25b2eba1a5..fd176a1e59 100644 --- a/cast/src/html/launcher.html.template +++ b/cast/src/html/launcher.html.template @@ -37,7 +37,13 @@ // // Safari 10.1 supports type=module but ignores nomodule, so we add this check. if (!isS101) { _ls("/static/polyfills/custom-elements-es5-adapter.js"); - _ls("<%= es5LauncherJS %>"); + <% if (useRollup) { %> + _ls("/static/js/s.min.js").onload = function() { + System.import("<%= es5LauncherJS %>"); + }; + <% } else { %> + _ls("<%= es5LauncherJS %>"); + <% } %> } })(); diff --git a/cast/src/receiver/layout/hc-main.ts b/cast/src/receiver/layout/hc-main.ts index 1b0504b759..564665846f 100644 --- a/cast/src/receiver/layout/hc-main.ts +++ b/cast/src/receiver/layout/hc-main.ts @@ -82,6 +82,7 @@ export class HcMain extends HassElement { .hass=${this.hass} .lovelaceConfig=${this._lovelaceConfig} .viewPath=${this._lovelacePath} + @config-refresh=${this._generateLovelaceConfig} > `; } @@ -193,12 +194,7 @@ export class HcMain extends HassElement { } catch (err) { // Generate a Lovelace config. this._unsubLovelace = () => undefined; - const { generateLovelaceConfigFromHass } = await import( - "../../../../src/panels/lovelace/common/generate-lovelace-config" - ); - this._handleNewLovelaceConfig( - await generateLovelaceConfigFromHass(this.hass!) - ); + await this._generateLovelaceConfig(); } } if (!resourcesLoaded) { @@ -218,6 +214,15 @@ export class HcMain extends HassElement { this._sendStatus(); } + private async _generateLovelaceConfig() { + const { generateLovelaceConfigFromHass } = await import( + "../../../../src/panels/lovelace/common/generate-lovelace-config" + ); + this._handleNewLovelaceConfig( + await generateLovelaceConfigFromHass(this.hass!) + ); + } + private _handleNewLovelaceConfig(lovelaceConfig: LovelaceConfig) { castContext.setApplicationState(lovelaceConfig.title!); this._lovelaceConfig = lovelaceConfig; diff --git a/cast/src/receiver/second-load.ts b/cast/src/receiver/second-load.ts index 39dd115e96..bb691527f6 100644 --- a/cast/src/receiver/second-load.ts +++ b/cast/src/receiver/second-load.ts @@ -1,3 +1,4 @@ import "web-animations-js/web-animations-next-lite.min"; import "../../../src/resources/roboto"; +import "../../../src/resources/ha-style"; import "./layout/hc-lovelace"; diff --git a/demo/public/manifest.json b/demo/public/manifest.json index f364cab750..901a8780a1 100644 --- a/demo/public/manifest.json +++ b/demo/public/manifest.json @@ -1,6 +1,6 @@ { "background_color": "#FFFFFF", - "description": "Open-source home automation platform running on Python 3.", + "description": "Home automation platform that puts local control and privacy first.", "dir": "ltr", "display": "standalone", "icons": [ @@ -31,7 +31,7 @@ ], "lang": "en-US", "name": "Home Assistant Demo", - "short_name": "Demo", + "short_name": "HA Demo", "start_url": "/?homescreen=1", "theme_color": "#03A9F4" } diff --git a/demo/rollup.config.js b/demo/rollup.config.js new file mode 100644 index 0000000000..d236491002 --- /dev/null +++ b/demo/rollup.config.js @@ -0,0 +1,10 @@ +const rollup = require("../build-scripts/rollup.js"); +const env = require("../build-scripts/env.js"); + +const config = rollup.createDemoConfig({ + isProdBuild: env.isProdBuild(), + latestBuild: true, + isStatsBuild: env.isStatsBuild(), +}); + +module.exports = { ...config.inputOptions, output: config.outputOptions }; diff --git a/demo/src/entrypoint.ts b/demo/src/entrypoint.ts index e2b8c447ce..5ddbcc0ca4 100644 --- a/demo/src/entrypoint.ts +++ b/demo/src/entrypoint.ts @@ -1,4 +1,3 @@ -import "@polymer/paper-styles/typography"; import "@polymer/polymer/lib/elements/dom-if"; import "@polymer/polymer/lib/elements/dom-repeat"; import "../../src/resources/ha-style"; diff --git a/demo/src/ha-demo.ts b/demo/src/ha-demo.ts index 6fd33b7f57..c49d522d65 100644 --- a/demo/src/ha-demo.ts +++ b/demo/src/ha-demo.ts @@ -1,3 +1,4 @@ +import "../../src/resources/compatibility"; import { isNavigationClick } from "../../src/common/dom/is-navigation-click"; import { navigate } from "../../src/common/navigate"; import { diff --git a/demo/src/html/index.html.template b/demo/src/html/index.html.template index a9decd4502..0b045fddd8 100644 --- a/demo/src/html/index.html.template +++ b/demo/src/html/index.html.template @@ -5,18 +5,6 @@ - - <%= renderTemplate('_js_base') %> + <%= renderTemplate('_preload_roboto') %> @@ -104,8 +93,13 @@ // // Safari 10.1 supports type=module but ignores nomodule, so we add this check. if (!isS101) { _ls("/static/polyfills/custom-elements-es5-adapter.js"); - _ls("<%= es5Compatibility %>"); - _ls("<%= es5DemoJS %>"); + <% if (useRollup) { %> + _ls("/static/js/s.min.js").onload = function() { + System.import("<%= es5DemoJS %>"); + }; + <% } else { %> + _ls("<%= es5DemoJS %>"); + <% } %> } })(); diff --git a/gallery/rollup.config.js b/gallery/rollup.config.js new file mode 100644 index 0000000000..787bf5a448 --- /dev/null +++ b/gallery/rollup.config.js @@ -0,0 +1,10 @@ +const rollup = require("../build-scripts/rollup.js"); +const env = require("../build-scripts/env.js"); + +const config = rollup.createGalleryConfig({ + isProdBuild: env.isProdBuild(), + latestBuild: true, + isStatsBuild: env.isStatsBuild(), +}); + +module.exports = { ...config.inputOptions, output: config.outputOptions }; diff --git a/gallery/src/entrypoint.js b/gallery/src/entrypoint.js index f803a3c6a3..095530ec00 100644 --- a/gallery/src/entrypoint.js +++ b/gallery/src/entrypoint.js @@ -1,4 +1,3 @@ -import "@polymer/paper-styles/typography"; import "@polymer/polymer/lib/elements/dom-if"; import "@polymer/polymer/lib/elements/dom-repeat"; import "../../src/resources/ha-style"; diff --git a/gallery/src/ha-gallery.js b/gallery/src/ha-gallery.js index de8c424e5a..3352048e38 100644 --- a/gallery/src/ha-gallery.js +++ b/gallery/src/ha-gallery.js @@ -10,6 +10,7 @@ import { html } from "@polymer/polymer/lib/utils/html-tag"; import { PolymerElement } from "@polymer/polymer/polymer-element"; import "../../src/components/ha-card"; import "../../src/managers/notification-manager"; +import "../../src/styles/polymer-ha-style"; // eslint-disable-next-line no-undef const DEMOS = require.context("./demos", true, /^(.*\.(ts$))[^.]*$/im); diff --git a/hassio/rollup.config.js b/hassio/rollup.config.js new file mode 100644 index 0000000000..4beee60e48 --- /dev/null +++ b/hassio/rollup.config.js @@ -0,0 +1,10 @@ +const rollup = require("../build-scripts/rollup.js"); +const env = require("../build-scripts/env.js"); + +const config = rollup.createHassioConfig({ + isProdBuild: env.isProdBuild(), + latestBuild: false, + isStatsBuild: env.isStatsBuild(), +}); + +module.exports = { ...config.inputOptions, output: config.outputOptions }; diff --git a/hassio/src/addon-view/documentation/hassio-addon-documentation-tab.ts b/hassio/src/addon-view/documentation/hassio-addon-documentation-tab.ts index 5a25c5e30d..34d0087120 100644 --- a/hassio/src/addon-view/documentation/hassio-addon-documentation-tab.ts +++ b/hassio/src/addon-view/documentation/hassio-addon-documentation-tab.ts @@ -64,6 +64,9 @@ class HassioAddonDocumentationDashboard extends LitElement { padding: 8px; max-width: 1024px; } + ha-markdown { + padding: 16px; + } `, ]; } diff --git a/hassio/src/addon-view/info/hassio-addon-info.ts b/hassio/src/addon-view/info/hassio-addon-info.ts index 59f0a88466..7ba518c194 100644 --- a/hassio/src/addon-view/info/hassio-addon-info.ts +++ b/hassio/src/addon-view/info/hassio-addon-info.ts @@ -630,14 +630,10 @@ class HassioAddonInfo extends LitElement { .right { float: right; } - ha-markdown img { - max-width: 100%; - } protection-enable mwc-button { --mdc-theme-primary: white; } - .description a, - ha-markdown a { + .description a { color: var(--primary-color); } .red { @@ -675,6 +671,9 @@ class HassioAddonInfo extends LitElement { text-decoration: underline; cursor: pointer; } + ha-markdown { + padding: 16px; + } `, ]; } diff --git a/hassio/src/components/hassio-filter-addons.ts b/hassio/src/components/hassio-filter-addons.ts index 270c8e7612..ddb56188aa 100644 --- a/hassio/src/components/hassio-filter-addons.ts +++ b/hassio/src/components/hassio-filter-addons.ts @@ -1,13 +1,13 @@ -import * as Fuse from "fuse.js"; +import Fuse from "fuse.js"; import { HassioAddonInfo } from "../../../src/data/hassio/addon"; export function filterAndSort(addons: HassioAddonInfo[], filter: string) { - const options: Fuse.FuseOptions = { + const options: Fuse.IFuseOptions = { keys: ["name", "description", "slug"], - caseSensitive: false, + isCaseSensitive: false, minMatchCharLength: 2, threshold: 0.2, }; const fuse = new Fuse(addons, options); - return fuse.search(filter); + return fuse.search(filter).map((result) => result.item); } diff --git a/hassio/src/dialogs/markdown/dialog-hassio-markdown.ts b/hassio/src/dialogs/markdown/dialog-hassio-markdown.ts index 947a42f236..8feae8e709 100644 --- a/hassio/src/dialogs/markdown/dialog-hassio-markdown.ts +++ b/hassio/src/dialogs/markdown/dialog-hassio-markdown.ts @@ -90,6 +90,9 @@ class HassioMarkdownDialog extends LitElement { color: var(--text-primary-color); background-color: var(--primary-color); } + ha-markdown { + padding: 16px; + } } `, ]; diff --git a/hassio/src/hassio-panel.ts b/hassio/src/hassio-panel.ts index b5f4770807..1e92ec923b 100644 --- a/hassio/src/hassio-panel.ts +++ b/hassio/src/hassio-panel.ts @@ -12,7 +12,6 @@ import { HassioSupervisorInfo, } from "../../src/data/hassio/supervisor"; import type { PageNavigation } from "../../src/layouts/hass-tabs-subpage"; -import "../../src/resources/ha-style"; import { HomeAssistant, Route } from "../../src/types"; import "./hassio-panel-router"; diff --git a/package.json b/package.json index 2c08ccb36c..32f5d1fe8d 100644 --- a/package.json +++ b/package.json @@ -86,10 +86,10 @@ "deep-freeze": "^0.0.1", "es6-object-assign": "^1.1.0", "fecha": "^4.2.0", - "fuse.js": "^3.4.4", + "fuse.js": "^6.0.0", "google-timezones-json": "^1.0.2", "hls.js": "^0.12.4", - "home-assistant-js-websocket": "^5.1.2", + "home-assistant-js-websocket": "^5.2.1", "idb-keyval": "^3.2.0", "intl-messageformat": "^8.3.9", "js-yaml": "^3.13.1", @@ -126,6 +126,10 @@ "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/preset-env": "^7.9.5", "@babel/preset-typescript": "^7.9.0", + "@rollup/plugin-commonjs": "^11.1.0", + "@rollup/plugin-json": "^4.0.3", + "@rollup/plugin-node-resolve": "^7.1.3", + "@rollup/plugin-replace": "^2.3.2", "@types/chai": "^4.1.7", "@types/chromecast-caf-receiver": "^3.0.12", "@types/codemirror": "^0.0.78", @@ -165,22 +169,30 @@ "lint-staged": "^8.1.5", "lit-analyzer": "^1.1.10", "lodash.template": "^4.5.0", + "magic-string": "^0.25.7", "map-stream": "^0.0.7", "merge-stream": "^1.0.1", "mocha": "^6.0.2", "object-hash": "^2.0.3", + "open": "^7.0.4", "prettier": "^2.0.4", "raw-loader": "^2.0.0", "require-dir": "^1.2.0", + "rollup": "^2.8.2", + "rollup-plugin-babel": "^4.4.0", + "rollup-plugin-string": "^3.0.0", + "rollup-plugin-terser": "^5.3.0", + "rollup-plugin-visualizer": "^4.0.4", + "serve": "^11.3.0", "sinon": "^7.3.1", "source-map-url": "^0.4.0", + "systemjs": "^6.3.2", "terser-webpack-plugin": "^1.2.3", "ts-lit-plugin": "^1.1.10", "ts-mocha": "^6.0.0", "typescript": "^3.8.3", "vinyl-buffer": "^1.0.1", "vinyl-source-stream": "^2.0.0", - "web-component-tester": "^6.9.2", "webpack": "^4.40.2", "webpack-cli": "^3.3.9", "webpack-dev-server": "^3.10.3", diff --git a/rollup.config.js b/rollup.config.js new file mode 100644 index 0000000000..2eb552dbfe --- /dev/null +++ b/rollup.config.js @@ -0,0 +1,10 @@ +const rollup = require("./build-scripts/rollup.js"); +const env = require("./build-scripts/env.js"); + +const config = rollup.createAppConfig({ + isProdBuild: env.isProdBuild(), + latestBuild: true, + isStatsBuild: env.isStatsBuild(), +}); + +module.exports = { ...config.inputOptions, output: config.outputOptions }; diff --git a/setup.py b/setup.py index df198d527f..47a91881c5 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="home-assistant-frontend", - version="20200519.5", + version="20200603.0", description="The Home Assistant frontend", url="https://github.com/home-assistant/home-assistant-polymer", author="The Home Assistant Authors", diff --git a/src/auth/ha-auth-flow.ts b/src/auth/ha-auth-flow.ts index 05f48449f7..88afcbedae 100644 --- a/src/auth/ha-auth-flow.ts +++ b/src/auth/ha-auth-flow.ts @@ -83,12 +83,24 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) { ${this._renderStep(this._step)}
${this._step.type === "form" ? "Next" : "Start over"}${this._step.type === "form" + ? this.localize("ui.panel.page-authorize.form.next") + : this.localize( + "ui.panel.page-authorize.form.start_over" + )}
`; case "error": - return html`
Error: ${this._errorMessage}
`; + return html` +
+ ${this.localize( + "ui.panel.page-authorize.form.error", + "error", + this._errorMessage + )} +
+ `; case "loading": return html` ${this.localize("ui.panel.page-authorize.form.working")} `; default: diff --git a/src/components/data-table/sort-filter.ts b/src/components/data-table/sort-filter.ts index 9e89e866df..33fa6ecdb9 100644 --- a/src/components/data-table/sort-filter.ts +++ b/src/components/data-table/sort-filter.ts @@ -1,9 +1,11 @@ import { wrap } from "comlink"; -type FilterDataType = typeof import("./sort_filter_worker").api["filterData"]; +import type { api } from "./sort_filter_worker"; + +type FilterDataType = api["filterData"]; type FilterDataParamTypes = Parameters; -type SortDataType = typeof import("./sort_filter_worker").api["sortData"]; +type SortDataType = api["sortData"]; type SortDataParamTypes = Parameters; let worker: any | undefined; diff --git a/src/components/data-table/sort_filter_worker.ts b/src/components/data-table/sort_filter_worker.ts index de3e1c67b2..4c0d6d1987 100644 --- a/src/components/data-table/sort_filter_worker.ts +++ b/src/components/data-table/sort_filter_worker.ts @@ -67,10 +67,11 @@ const sortData = ( return 0; }); -// Export for types -export const api = { +const api = { filterData, sortData, }; +export type api = typeof api; + expose(api); diff --git a/src/components/device/ha-device-picker.ts b/src/components/device/ha-device-picker.ts index 73686240ec..be4fb6bbe4 100644 --- a/src/components/device/ha-device-picker.ts +++ b/src/components/device/ha-device-picker.ts @@ -52,7 +52,7 @@ const rowRenderer = (root: HTMLElement, _owner, model: { item: Device }) => { } - +
[[item.name]]
[[item.area]]
@@ -188,7 +188,9 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { this.hass, deviceEntityLookup[device.id] ), - area: device.area_id ? areaLookup[device.area_id].name : "No area", + area: device.area_id + ? areaLookup[device.area_id].name + : this.hass.localize("ui.components.device-picker.no_area"), }; }); if (outputDevices.length === 1) { diff --git a/src/components/ha-markdown-element.ts b/src/components/ha-markdown-element.ts new file mode 100644 index 0000000000..69844b3ad2 --- /dev/null +++ b/src/components/ha-markdown-element.ts @@ -0,0 +1,71 @@ +import { customElement, property, UpdatingElement } from "lit-element"; +import { fireEvent } from "../common/dom/fire_event"; +import { renderMarkdown } from "../resources/render-markdown"; + +@customElement("ha-markdown-element") +class HaMarkdownElement extends UpdatingElement { + @property() public content?; + + @property({ type: Boolean }) public allowSvg = false; + + @property({ type: Boolean }) public breaks = false; + + protected update(changedProps) { + super.update(changedProps); + if (this.content !== undefined) { + this._render(); + } + } + + private async _render() { + this.innerHTML = await renderMarkdown( + this.content, + { + breaks: this.breaks, + gfm: true, + tables: true, + }, + { + allowSvg: this.allowSvg, + } + ); + + this._resize(); + + const walker = document.createTreeWalker( + this, + 1 /* SHOW_ELEMENT */, + null, + false + ); + + while (walker.nextNode()) { + const node = walker.currentNode; + + // Open external links in a new window + if ( + node instanceof HTMLAnchorElement && + node.host !== document.location.host + ) { + node.target = "_blank"; + node.rel = "noreferrer"; + + // protect referrer on external links and deny window.opener access for security reasons + // (see https://mathiasbynens.github.io/rel-noopener/) + node.rel = "noreferrer noopener"; + + // Fire a resize event when images loaded to notify content resized + } else if (node instanceof HTMLImageElement) { + node.addEventListener("load", this._resize); + } + } + } + + private _resize = () => fireEvent(this, "iron-resize"); +} + +declare global { + interface HTMLElementTagNameMap { + "ha-markdown-element": HaMarkdownElement; + } +} diff --git a/src/components/ha-markdown.ts b/src/components/ha-markdown.ts index 162a344daf..b3f6b6b0c8 100644 --- a/src/components/ha-markdown.ts +++ b/src/components/ha-markdown.ts @@ -1,65 +1,80 @@ -import { customElement, property, UpdatingElement } from "lit-element"; -import { fireEvent } from "../common/dom/fire_event"; -import { renderMarkdown } from "../resources/render-markdown"; +import { + css, + CSSResult, + customElement, + html, + LitElement, + property, + TemplateResult, +} from "lit-element"; + +import "./ha-markdown-element"; @customElement("ha-markdown") -class HaMarkdown extends UpdatingElement { - @property() public content = ""; +class HaMarkdown extends LitElement { + @property() public content?; @property({ type: Boolean }) public allowSvg = false; @property({ type: Boolean }) public breaks = false; - protected update(changedProps) { - super.update(changedProps); - this._render(); - } - - private async _render() { - this.innerHTML = await renderMarkdown( - this.content, - { - breaks: this.breaks, - gfm: true, - tables: true, - }, - { - allowSvg: this.allowSvg, - } - ); - - this._resize(); - - const walker = document.createTreeWalker( - this, - 1 /* SHOW_ELEMENT */, - null, - false - ); - - while (walker.nextNode()) { - const node = walker.currentNode; - - // Open external links in a new window - if ( - node instanceof HTMLAnchorElement && - node.host !== document.location.host - ) { - node.target = "_blank"; - node.rel = "noreferrer"; - - // protect referrer on external links and deny window.opener access for security reasons - // (see https://mathiasbynens.github.io/rel-noopener/) - node.rel = "noreferrer noopener"; - - // Fire a resize event when images loaded to notify content resized - } else if (node instanceof HTMLImageElement) { - node.addEventListener("load", this._resize); - } + protected render(): TemplateResult { + if (!this.content) { + return html``; } + + return html``; } - private _resize = () => fireEvent(this, "iron-resize"); + static get styles(): CSSResult { + return css` + :host { + display: block; + } + ha-markdown-element { + -ms-user-select: text; + -webkit-user-select: text; + -moz-user-select: text; + } + ha-markdown-element > *:first-child { + margin-top: 0; + } + ha-markdown-element > *:last-child { + margin-bottom: 0; + } + ha-markdown-element a { + color: var(--primary-color); + } + ha-markdown-element img { + max-width: 100%; + } + ha-markdown-element code, + pre { + background-color: var(--markdown-code-background-color, #f6f8fa); + border-radius: 3px; + } + ha-markdown-element code { + font-size: 85%; + padding: 0.2em 0.4em; + } + ha-markdown-element pre code { + padding: 0; + } + ha-markdown-element pre { + padding: 16px; + overflow: auto; + line-height: 1.45; + } + ha-markdown-element h2 { + font-size: 1.5em !important; + font-weight: bold !important; + } + `; + } } declare global { diff --git a/src/data/device_automation.ts b/src/data/device_automation.ts index 03c58c36c1..38b808fd3b 100644 --- a/src/data/device_automation.ts +++ b/src/data/device_automation.ts @@ -65,14 +65,15 @@ export const fetchDeviceTriggerCapabilities = ( trigger, }); -const whitelist = [ - "above", - "below", - "brightness_pct", - "code", - "for", - "position", - "set_brightness", +const deviceAutomationIdentifiers = [ + "device_id", + "domain", + "entity_id", + "type", + "subtype", + "event", + "condition", + "platform", ]; export const deviceAutomationsEqual = ( @@ -84,7 +85,7 @@ export const deviceAutomationsEqual = ( } for (const property in a) { - if (whitelist.includes(property)) { + if (!deviceAutomationIdentifiers.includes(property)) { continue; } if (!Object.is(a[property], b[property])) { @@ -92,7 +93,7 @@ export const deviceAutomationsEqual = ( } } for (const property in b) { - if (whitelist.includes(property)) { + if (!deviceAutomationIdentifiers.includes(property)) { continue; } if (!Object.is(a[property], b[property])) { diff --git a/src/data/history.ts b/src/data/history.ts index adfe9eba00..39c383659d 100644 --- a/src/data/history.ts +++ b/src/data/history.ts @@ -56,7 +56,8 @@ export const fetchRecent = ( startTime, endTime, skipInitialState = false, - significantChangesOnly?: boolean + significantChangesOnly?: boolean, + minimalResponse = true ): Promise => { let url = "history/period"; if (startTime) { @@ -72,6 +73,9 @@ export const fetchRecent = ( if (significantChangesOnly !== undefined) { url += `&significant_changes_only=${Number(significantChangesOnly)}`; } + if (minimalResponse) { + url += "&minimal_response"; + } return hass.callApi("GET", url); }; @@ -83,14 +87,17 @@ export const fetchDate = ( ): Promise => { return hass.callApi( "GET", - `history/period/${startTime.toISOString()}?end_time=${endTime.toISOString()}` + `history/period/${startTime.toISOString()}?end_time=${endTime.toISOString()}&minimal_response` ); }; const equalState = (obj1: LineChartState, obj2: LineChartState) => obj1.state === obj2.state && - // They either both have an attributes object or not + // Only compare attributes if both states have an attributes object. + // When `minimal_response` is sent, only the first and last state + // will have attributes except for domains in DOMAINS_USE_LAST_UPDATED. (!obj1.attributes || + !obj2.attributes || LINE_ATTRIBUTES_TO_KEEP.every( (attr) => obj1.attributes![attr] === obj2.attributes![attr] )); @@ -101,12 +108,20 @@ const processTimelineEntity = ( states: HassEntity[] ): TimelineEntity => { const data: TimelineState[] = []; + const last_element = states.length - 1; for (const state of states) { if (data.length > 0 && state.state === data[data.length - 1].state) { continue; } + // Copy the data from the last element as its the newest + // and is only needed to localize the data + if (!state.entity_id) { + state.attributes = states[last_element].attributes; + state.entity_id = states[last_element].entity_id; + } + data.push({ state_localize: computeStateDisplay(localize, state, language), state: state.state, @@ -198,7 +213,7 @@ export const computeHistory = ( } const stateWithUnit = stateInfo.find( - (state) => "unit_of_measurement" in state.attributes + (state) => state.attributes && "unit_of_measurement" in state.attributes ); let unit: string | undefined; diff --git a/src/dialogs/config-flow/dialog-data-entry-flow.ts b/src/dialogs/config-flow/dialog-data-entry-flow.ts index 3ece18035b..8ee7b60587 100644 --- a/src/dialogs/config-flow/dialog-data-entry-flow.ts +++ b/src/dialogs/config-flow/dialog-data-entry-flow.ts @@ -28,7 +28,6 @@ import { subscribeDeviceRegistry, } from "../../data/device_registry"; import { PolymerChangedEvent } from "../../polymer-types"; -import "../../resources/ha-style"; import { haStyleDialog } from "../../resources/styles"; import type { HomeAssistant } from "../../types"; import { DataEntryFlowDialogParams } from "./show-dialog-data-entry-flow"; diff --git a/src/dialogs/config-flow/step-flow-form.ts b/src/dialogs/config-flow/step-flow-form.ts index 21644637ab..7822a21f6f 100644 --- a/src/dialogs/config-flow/step-flow-form.ts +++ b/src/dialogs/config-flow/step-flow-form.ts @@ -16,7 +16,6 @@ import "../../components/ha-form/ha-form"; import type { HaFormSchema } from "../../components/ha-form/ha-form"; import "../../components/ha-markdown"; import type { DataEntryFlowStepForm } from "../../data/data_entry_flow"; -import "../../resources/ha-style"; import type { HomeAssistant } from "../../types"; import type { FlowConfig } from "./show-dialog-data-entry-flow"; import { configFlowContentStyles } from "./styles"; diff --git a/src/dialogs/config-flow/step-flow-pick-handler.ts b/src/dialogs/config-flow/step-flow-pick-handler.ts index 879824a657..68a54c92dd 100644 --- a/src/dialogs/config-flow/step-flow-pick-handler.ts +++ b/src/dialogs/config-flow/step-flow-pick-handler.ts @@ -1,7 +1,7 @@ import "@polymer/paper-item/paper-icon-item"; import "@polymer/paper-item/paper-item-body"; import "@polymer/paper-spinner/paper-spinner-lite"; -import * as Fuse from "fuse.js"; +import Fuse from "fuse.js"; import { css, CSSResult, @@ -52,14 +52,14 @@ class StepFlowPickHandler extends LitElement { }); if (filter) { - const options: Fuse.FuseOptions = { + const options: Fuse.IFuseOptions = { keys: ["name", "slug"], - caseSensitive: false, + isCaseSensitive: false, minMatchCharLength: 2, threshold: 0.2, }; const fuse = new Fuse(handlers, options); - return fuse.search(filter); + return fuse.search(filter).map((result) => result.item); } return handlers.sort((a, b) => a.name.toUpperCase() < b.name.toUpperCase() ? -1 : 1 diff --git a/src/dialogs/ha-more-info-dialog.js b/src/dialogs/ha-more-info-dialog.js index 6f716ada0d..1f80d19aec 100644 --- a/src/dialogs/ha-more-info-dialog.js +++ b/src/dialogs/ha-more-info-dialog.js @@ -5,7 +5,7 @@ import { html } from "@polymer/polymer/lib/utils/html-tag"; import { PolymerElement } from "@polymer/polymer/polymer-element"; import { computeStateDomain } from "../common/entity/compute_state_domain"; import DialogMixin from "../mixins/dialog-mixin"; -import "../resources/ha-style"; +import "../styles/polymer-ha-style-dialog"; import "./more-info/more-info-controls"; /* diff --git a/src/dialogs/ha-store-auth-card.js b/src/dialogs/ha-store-auth-card.js index 28df7c7249..a3b39c1681 100644 --- a/src/dialogs/ha-store-auth-card.js +++ b/src/dialogs/ha-store-auth-card.js @@ -4,7 +4,7 @@ import { html } from "@polymer/polymer/lib/utils/html-tag"; import { PolymerElement } from "@polymer/polymer/polymer-element"; import { enableWrite } from "../common/auth/token_storage"; import LocalizeMixin from "../mixins/localize-mixin"; -import "../resources/ha-style"; +import "../styles/polymer-ha-style"; class HaStoreAuth extends LocalizeMixin(PolymerElement) { static get template() { diff --git a/src/dialogs/more-info/more-info-controls.js b/src/dialogs/more-info/more-info-controls.js index bb12335b21..34d5e97ebc 100644 --- a/src/dialogs/more-info/more-info-controls.js +++ b/src/dialogs/more-info/more-info-controls.js @@ -17,7 +17,7 @@ import "../../data/ha-state-history-data"; import { EventsMixin } from "../../mixins/events-mixin"; import LocalizeMixin from "../../mixins/localize-mixin"; import { showEntityEditorDialog } from "../../panels/config/entities/show-dialog-entity-editor"; -import "../../resources/ha-style"; +import "../../styles/polymer-ha-style-dialog"; import "../../state-summary/state-card-content"; import { showConfirmationDialog } from "../generic/show-dialog-box"; import "./controls/more-info-content"; diff --git a/src/entrypoints/app.ts b/src/entrypoints/app.ts index 9be05793ac..934affb498 100644 --- a/src/entrypoints/app.ts +++ b/src/entrypoints/app.ts @@ -1,10 +1,7 @@ -// Load polyfill first so HTML imports start resolving -/* eslint-disable import/first */ -import "@polymer/paper-styles/typography"; import { setPassiveTouchGestures } from "@polymer/polymer/lib/utils/settings"; -import "../layouts/home-assistant"; -import "../resources/html-import/polyfill"; import "../resources/roboto"; +import "../resources/ha-style"; +import "../layouts/home-assistant"; import "../util/legacy-support"; setPassiveTouchGestures(true); @@ -12,3 +9,5 @@ setPassiveTouchGestures(true); document.createElement = Document.prototype.createElement; (window as any).frontendVersion = __VERSION__; + +import("../resources/html-import/polyfill"); diff --git a/src/entrypoints/authorize.ts b/src/entrypoints/authorize.ts index 959a8fc0ef..2a691c973f 100644 --- a/src/entrypoints/authorize.ts +++ b/src/entrypoints/authorize.ts @@ -1,3 +1,5 @@ +// Compat needs to be first import +import "../resources/compatibility"; import "@polymer/polymer/lib/elements/dom-if"; import "@polymer/polymer/lib/elements/dom-repeat"; import "../auth/ha-authorize"; diff --git a/src/entrypoints/core.ts b/src/entrypoints/core.ts index a80af0d428..8a6bedf9db 100644 --- a/src/entrypoints/core.ts +++ b/src/entrypoints/core.ts @@ -1,3 +1,5 @@ +// Compat needs to be first import +import "../resources/compatibility"; import { Auth, Connection, @@ -26,6 +28,7 @@ import { HomeAssistant } from "../types"; declare global { interface Window { hassConnection: Promise<{ auth: Auth; conn: Connection }>; + hassConnectionReady?: (hassConnection: Window["hassConnection"]) => void; } } @@ -80,6 +83,11 @@ window.hassConnection = (authProm() as Promise).then( connProm ); +// This is set if app was somehow loaded before core. +if (window.hassConnectionReady) { + window.hassConnectionReady(window.hassConnection); +} + // Start fetching some of the data that we will need. window.hassConnection.then(({ conn }) => { const noop = () => { diff --git a/src/entrypoints/custom-panel.ts b/src/entrypoints/custom-panel.ts index 6ba448c0b4..d43d881679 100644 --- a/src/entrypoints/custom-panel.ts +++ b/src/entrypoints/custom-panel.ts @@ -1,3 +1,4 @@ +import "../resources/compatibility"; import { PolymerElement } from "@polymer/polymer"; import { fireEvent } from "../common/dom/fire_event"; import { loadJS } from "../common/dom/load_resource"; @@ -17,12 +18,9 @@ let es5Loaded: Promise | undefined; window.loadES5Adapter = () => { if (!es5Loaded) { - es5Loaded = Promise.all([ - loadJS( - `${__STATIC_PATH__}polyfills/custom-elements-es5-adapter.js` - ).catch(), - import(/* webpackChunkName: "compat" */ "./compatibility"), - ]); + es5Loaded = loadJS( + `${__STATIC_PATH__}polyfills/custom-elements-es5-adapter.js` + ).catch(); // Swallow errors as it raises errors on old browsers. } return es5Loaded; }; @@ -51,7 +49,6 @@ function initialize(panel: CustomPanelInfo, properties: {}) { } if (__BUILD__ === "es5") { - // Load ES5 adapter. Swallow errors as it raises errors on old browsers. start = start.then(() => window.loadES5Adapter()); } diff --git a/src/entrypoints/onboarding.ts b/src/entrypoints/onboarding.ts index ffe47dc55e..0c30daed0a 100644 --- a/src/entrypoints/onboarding.ts +++ b/src/entrypoints/onboarding.ts @@ -1,3 +1,5 @@ +// Compat needs to be first import +import "../resources/compatibility"; import "../onboarding/ha-onboarding"; import "../resources/ha-style"; import "../resources/roboto"; diff --git a/src/fake_data/demo_config.ts b/src/fake_data/demo_config.ts index e1d342e45a..86b5484b33 100644 --- a/src/fake_data/demo_config.ts +++ b/src/fake_data/demo_config.ts @@ -1,4 +1,4 @@ -import { HassConfig } from "home-assistant-js-websocket"; +import { HassConfig, STATE_RUNNING } from "home-assistant-js-websocket"; export const demoConfig: HassConfig = { location_name: "Home", @@ -18,6 +18,7 @@ export const demoConfig: HassConfig = { whitelist_external_dirs: [], config_source: "storage", safe_mode: false, + state: STATE_RUNNING, internal_url: "http://homeassistant.local:8123", external_url: null, }; diff --git a/src/html/_js_base.html.template b/src/html/_js_base.html.template index c9bf4d4b49..2864581ce2 100644 --- a/src/html/_js_base.html.template +++ b/src/html/_js_base.html.template @@ -7,6 +7,7 @@ ); script.defer = true; script.src = src; + return script; } window.Polymer = { lazyRegister: true, diff --git a/src/html/_preload_roboto.html.template b/src/html/_preload_roboto.html.template new file mode 100644 index 0000000000..6ac777eb8e --- /dev/null +++ b/src/html/_preload_roboto.html.template @@ -0,0 +1,16 @@ + diff --git a/src/html/authorize.html.template b/src/html/authorize.html.template index 3169c62b45..3cbcfda5ce 100644 --- a/src/html/authorize.html.template +++ b/src/html/authorize.html.template @@ -3,18 +3,6 @@ Home Assistant - - <%= renderTemplate('_header') %> - +
- +
@@ -68,13 +76,18 @@ class ZwaveGroups extends PolymerElement {
- Other Nodes in this group: + [[localize('ui.panel.config.zwave.node_management.nodes_in_group')]]
- Max Associations: [[_maxAssociations]] + [[localize('ui.panel.config.zwave.node_management.max_associations')]] + [[_maxAssociations]]
@@ -90,7 +103,7 @@ class ZwaveGroups extends PolymerElement { service="change_association" service-data="[[_addAssocServiceData]]" > - Add To Group + [[localize('ui.panel.config.zwave.node_management.add_to_group')]]
diff --git a/src/panels/config/zwave/zwave-log-dialog.js b/src/panels/config/zwave/zwave-log-dialog.js index 8f2c271c62..f3e16d2c63 100644 --- a/src/panels/config/zwave/zwave-log-dialog.js +++ b/src/panels/config/zwave/zwave-log-dialog.js @@ -4,7 +4,7 @@ import { html } from "@polymer/polymer/lib/utils/html-tag"; import { PolymerElement } from "@polymer/polymer/polymer-element"; import "../../../components/dialog/ha-paper-dialog"; import { EventsMixin } from "../../../mixins/events-mixin"; -import "../../../resources/ha-style"; +import "../../../styles/polymer-ha-style-dialog"; class ZwaveLogDialog extends EventsMixin(PolymerElement) { static get template() { diff --git a/src/panels/config/zwave/zwave-log.js b/src/panels/config/zwave/zwave-log.js index b933bdd7ee..05ea015bb6 100755 --- a/src/panels/config/zwave/zwave-log.js +++ b/src/panels/config/zwave/zwave-log.js @@ -9,6 +9,7 @@ import "../../../components/ha-card"; import { EventsMixin } from "../../../mixins/events-mixin"; import LocalizeMixin from "../../../mixins/localize-mixin"; import "../ha-config-section"; +import "../../../styles/polymer-ha-style"; let registeredDialog = false; @@ -41,12 +42,12 @@ class OzwLog extends LocalizeMixin(EventsMixin(PolymerElement)) {
- +
- Load - Tail + [[localize('ui.panel.config.zwave.ozw_log.load')]] + [[localize('ui.panel.config.zwave.ozw_log.tail')]] `; diff --git a/src/panels/config/zwave/zwave-node-protection.js b/src/panels/config/zwave/zwave-node-protection.js index c980187c5c..292fb80bfe 100644 --- a/src/panels/config/zwave/zwave-node-protection.js +++ b/src/panels/config/zwave/zwave-node-protection.js @@ -7,6 +7,7 @@ import { html } from "@polymer/polymer/lib/utils/html-tag"; import { PolymerElement } from "@polymer/polymer/polymer-element"; import "../../../components/buttons/ha-call-api-button"; import "../../../components/ha-card"; +import "../../../styles/polymer-ha-style"; class ZwaveNodeProtection extends PolymerElement { static get template() { @@ -32,9 +33,9 @@ class ZwaveNodeProtection extends PolymerElement {
- +
- +