Gulpify build pipeline (#3145)

* Gulpify build pipeline

* Update build frontend script

* Fixes

* Limit service worker to latest build

* Use shorthand

* Fix hassio build
This commit is contained in:
Paulus Schoutsen 2019-05-02 11:35:46 -07:00 committed by GitHub
parent 8b98f375c2
commit 6c41c7b1ab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
79 changed files with 850 additions and 460 deletions

1
.gitignore vendored
View File

@ -4,7 +4,6 @@ node_modules/*
npm-debug.log
.DS_Store
hass_frontend/*
hass_frontend_es5/*
.reify-cache
demo/hademo-icons.html

View File

@ -0,0 +1,5 @@
const del = require("del");
const gulp = require("gulp");
const config = require("../paths");
gulp.task("clean", () => del([config.root, config.build_dir]));

View File

@ -0,0 +1,29 @@
// Run HA develop mode
const gulp = require("gulp");
require("./clean.js");
require("./translations.js");
require("./gen-icons.js");
require("./gather-static.js");
require("./webpack.js");
require("./service-worker.js");
require("./entry-html.js");
gulp.task(
"develop",
gulp.series(
async function setEnv() {
process.env.NODE_ENV = "development";
},
"clean",
gulp.parallel(
"copy-static",
"gen-service-worker-dev",
"gen-icons",
"gen-pages-dev",
"gen-index-html-dev",
gulp.series("build-translations", "copy-translations")
),
"webpack-watch"
)
);

View File

@ -0,0 +1,108 @@
// Tasks to generate entry HTML
/* eslint-disable import/no-dynamic-require */
/* eslint-disable global-require */
const gulp = require("gulp");
const fs = require("fs-extra");
const path = require("path");
const template = require("lodash.template");
const minify = require("html-minifier").minify;
const config = require("../paths.js");
const templatePath = (tpl) =>
path.resolve(config.polymer_dir, "src/html/", `${tpl}.html.template`);
const readFile = (pth) => fs.readFileSync(pth).toString();
const renderTemplate = (pth, data = {}) => {
const compiled = template(readFile(templatePath(pth)));
return compiled({ ...data, renderTemplate });
};
const minifyHtml = (content) =>
minify(content, {
collapseWhitespace: true,
minifyJS: true,
minifyCSS: true,
removeComments: true,
});
const PAGES = ["onboarding", "authorize"];
gulp.task("gen-pages-dev", (done) => {
for (const page of PAGES) {
const content = renderTemplate(page, {
latestPageJS: `/frontend_latest/${page}.js`,
latestHassIconsJS: "/frontend_latest/hass-icons.js",
es5Compatibility: "/frontend_es5/compatibility.js",
es5PageJS: `/frontend_es5/${page}.js`,
es5HassIconsJS: "/frontend_es5/hass-icons.js",
});
fs.outputFileSync(path.resolve(config.root, `${page}.html`), content);
}
done();
});
gulp.task("gen-pages-prod", (done) => {
const latestManifest = require(path.resolve(config.output, "manifest.json"));
const es5Manifest = require(path.resolve(config.output_es5, "manifest.json"));
for (const page of PAGES) {
const content = renderTemplate(page, {
latestPageJS: latestManifest[`${page}.js`],
latestHassIconsJS: latestManifest["hass-icons.js"],
es5Compatibility: es5Manifest["compatibility.js"],
es5PageJS: es5Manifest[`${page}.js`],
es5HassIconsJS: es5Manifest["hass-icons.js"],
});
fs.outputFileSync(
path.resolve(config.root, `${page}.html`),
minifyHtml(content)
);
}
done();
});
gulp.task("gen-index-html-dev", (done) => {
// In dev mode we don't mangle names, so we hardcode urls. That way we can
// run webpack as last in watch mode, which blocks output.
const content = renderTemplate("index", {
latestAppJS: "/frontend_latest/app.js",
latestCoreJS: "/frontend_latest/core.js",
latestCustomPanelJS: "/frontend_latest/custom-panel.js",
latestHassIconsJS: "/frontend_latest/hass-icons.js",
es5Compatibility: "/frontend_es5/compatibility.js",
es5AppJS: "/frontend_es5/app.js",
es5CoreJS: "/frontend_es5/core.js",
es5CustomPanelJS: "/frontend_es5/custom-panel.js",
es5HassIconsJS: "/frontend_es5/hass-icons.js",
});
fs.outputFileSync(path.resolve(config.root, "index.html"), content);
done();
});
gulp.task("gen-index-html-prod", (done) => {
const latestManifest = require(path.resolve(config.output, "manifest.json"));
const es5Manifest = require(path.resolve(config.output_es5, "manifest.json"));
const content = renderTemplate("index", {
latestAppJS: latestManifest["app.js"],
latestCoreJS: latestManifest["core.js"],
latestCustomPanelJS: latestManifest["custom-panel.js"],
latestHassIconsJS: latestManifest["hass-icons.js"],
es5Compatibility: es5Manifest["compatibility.js"],
es5AppJS: es5Manifest["app.js"],
es5CoreJS: es5Manifest["core.js"],
es5CustomPanelJS: es5Manifest["custom-panel.js"],
es5HassIconsJS: es5Manifest["hass-icons.js"],
});
const minified = minifyHtml(content).replace(/#THEMEC/g, "{{ theme_color }}");
fs.outputFileSync(path.resolve(config.root, "index.html"), minified);
done();
});

View File

@ -0,0 +1,87 @@
// Gulp task to gather all static files.
const gulp = require("gulp");
const path = require("path");
const fs = require("fs-extra");
const zopfli = require("gulp-zopfli-green");
const merge = require("merge-stream");
const config = require("../paths");
const npmPath = (...parts) =>
path.resolve(config.polymer_dir, "node_modules", ...parts);
const polyPath = (...parts) => path.resolve(config.polymer_dir, ...parts);
const staticPath = (...parts) => path.resolve(config.root, "static", ...parts);
const copyFileDir = (fromFile, toDir) =>
fs.copySync(fromFile, path.join(toDir, path.basename(fromFile)));
function copyTranslations() {
// Translation output
fs.copySync(
polyPath("build-translations/output"),
staticPath("translations")
);
}
function copyStatic() {
// Basic static files
fs.copySync(polyPath("public"), config.root);
// Web Component polyfills and adapters
copyFileDir(
npmPath("@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"),
staticPath("polyfills/")
);
copyFileDir(
npmPath("@webcomponents/webcomponentsjs/webcomponents-bundle.js"),
staticPath("polyfills/")
);
copyFileDir(
npmPath("@webcomponents/webcomponentsjs/webcomponents-bundle.js.map"),
staticPath("polyfills/")
);
// Local fonts
fs.copySync(npmPath("@polymer/font-roboto-local/fonts"), staticPath("fonts"));
// External dependency assets
copyFileDir(
npmPath("react-big-calendar/lib/css/react-big-calendar.css"),
staticPath("panels/calendar/")
);
copyFileDir(
npmPath("leaflet/dist/leaflet.css"),
staticPath("images/leaflet/")
);
fs.copySync(
npmPath("leaflet/dist/images"),
staticPath("images/leaflet/images/")
);
}
gulp.task("copy-static", (done) => {
copyStatic();
done();
});
gulp.task("compress-static", () => {
const fonts = gulp
.src(staticPath("fonts/**/*.ttf"))
.pipe(zopfli())
.pipe(gulp.dest(staticPath("fonts")));
const polyfills = gulp
.src(staticPath("polyfills/*.js"))
.pipe(zopfli())
.pipe(gulp.dest(staticPath("polyfills")));
const translations = gulp
.src(staticPath("translations/*.json"))
.pipe(zopfli())
.pipe(gulp.dest(staticPath("translations")));
return merge(fonts, polyfills, translations);
});
gulp.task("copy-translations", (done) => {
copyTranslations();
done();
});

View File

@ -1,7 +1,6 @@
const gulp = require("gulp");
const path = require("path");
const fs = require("fs");
const config = require("../config");
const ICON_PACKAGE_PATH = path.resolve(
__dirname,
@ -38,12 +37,12 @@ function loadIcon(name) {
function transformXMLtoPolymer(name, xml) {
const start = xml.indexOf("><path") + 1;
const end = xml.length - start - 6;
const path = xml.substr(start, end);
return `<g id="${name}">${path}</g>`;
const pth = xml.substr(start, end);
return `<g id="${name}">${pth}</g>`;
}
// Given an iconset name and icon names, generate a polymer iconset
function generateIconset(name, iconNames) {
function generateIconset(iconsetName, iconNames) {
const iconDefs = Array.from(iconNames)
.map((name) => {
const iconDef = loadIcon(name);
@ -53,7 +52,7 @@ function generateIconset(name, iconNames) {
return transformXMLtoPolymer(name, iconDef);
})
.join("");
return `<ha-iconset-svg name="${name}" size="24"><svg><defs>${iconDefs}</defs></svg></ha-iconset-svg>`;
return `<ha-iconset-svg name="${iconsetName}" size="24"><svg><defs>${iconDefs}</defs></svg></ha-iconset-svg>`;
}
// Generate the full MDI iconset
@ -62,7 +61,9 @@ function genMDIIcons() {
fs.readFileSync(path.resolve(ICON_PACKAGE_PATH, META_PATH), "UTF-8")
);
const iconNames = meta.map((iconInfo) => iconInfo.name);
fs.existsSync(OUTPUT_DIR) || fs.mkdirSync(OUTPUT_DIR);
if (!fs.existsSync(OUTPUT_DIR)) {
fs.mkdirSync(OUTPUT_DIR);
}
fs.writeFileSync(MDI_OUTPUT_PATH, generateIconset("mdi", iconNames));
}
@ -81,7 +82,7 @@ function mapFiles(startPath, filter, mapFunc) {
}
// Find all icons used by the project.
function findIcons(path, iconsetName) {
function findIcons(searchPath, iconsetName) {
const iconRegex = new RegExp(`${iconsetName}:[\\w-]+`, "g");
const icons = new Set();
function processFile(filename) {
@ -93,8 +94,8 @@ function findIcons(path, iconsetName) {
icons.add(match[0].substr(iconsetName.length + 1));
}
}
mapFiles(path, ".js", processFile);
mapFiles(path, ".ts", processFile);
mapFiles(searchPath, ".js", processFile);
mapFiles(searchPath, ".ts", processFile);
return icons;
}

View File

@ -0,0 +1,31 @@
// Run HA develop mode
const gulp = require("gulp");
require("./clean.js");
require("./translations.js");
require("./gen-icons.js");
require("./gather-static.js");
require("./webpack.js");
require("./service-worker.js");
require("./entry-html.js");
gulp.task(
"build-release",
gulp.series(
async function setEnv() {
process.env.NODE_ENV = "production";
},
"clean",
gulp.parallel(
"copy-static",
"gen-icons",
gulp.series("build-translations", "copy-translations")
),
gulp.parallel("webpack-prod", "compress-static"),
gulp.parallel(
"gen-pages-prod",
"gen-index-html-prod",
"gen-service-worker-prod"
)
)
);

View File

@ -0,0 +1,29 @@
// Generate service worker.
// Based on manifest, create a file with the content as service_worker.js
/* eslint-disable import/no-dynamic-require */
/* eslint-disable global-require */
const gulp = require("gulp");
const path = require("path");
const fs = require("fs-extra");
const config = require("../paths.js");
const swPath = path.resolve(config.root, "service_worker.js");
const writeSW = (content) => fs.outputFileSync(swPath, content.trim() + "\n");
gulp.task("gen-service-worker-dev", (done) => {
writeSW(
`
console.debug('Service worker disabled in development');
`
);
done();
});
gulp.task("gen-service-worker-prod", (done) => {
fs.copySync(
path.resolve(config.output, "service_worker.js"),
path.resolve(config.root, "service_worker.js")
);
done();
});

View File

@ -0,0 +1,63 @@
// Tasks to run webpack.
const gulp = require("gulp");
const webpack = require("webpack");
const { createAppConfig } = require("../webpack");
const handler = (done) => (err, stats) => {
if (err) {
console.log(err.stack || err);
if (err.details) {
console.log(err.details);
}
return;
}
console.log(`Build done @ ${new Date().toLocaleTimeString()}`);
if (stats.hasErrors() || stats.hasWarnings()) {
console.log(stats.toString("minimal"));
}
if (done) {
done();
}
};
gulp.task("webpack-watch", () => {
const compiler = webpack([
createAppConfig({
isProdBuild: false,
latestBuild: true,
isStatsBuild: false,
}),
createAppConfig({
isProdBuild: false,
latestBuild: false,
isStatsBuild: false,
}),
]);
compiler.watch({}, handler());
// we are not calling done, so this command will run forever
});
gulp.task(
"webpack-prod",
() =>
new Promise((resolve) =>
webpack(
[
createAppConfig({
isProdBuild: true,
latestBuild: true,
isStatsBuild: false,
}),
createAppConfig({
isProdBuild: true,
latestBuild: false,
isStatsBuild: false,
}),
],
handler(resolve)
)
)
);

10
build-scripts/paths.js Normal file
View File

@ -0,0 +1,10 @@
var path = require("path");
module.exports = {
polymer_dir: path.resolve(__dirname, ".."),
build_dir: path.resolve(__dirname, "../build"),
root: path.resolve(__dirname, "../hass_frontend"),
static: path.resolve(__dirname, "../hass_frontend/static"),
output: path.resolve(__dirname, "../hass_frontend/frontend_latest"),
output_es5: path.resolve(__dirname, "../hass_frontend/frontend_es5"),
};

184
build-scripts/webpack.js Normal file
View File

@ -0,0 +1,184 @@
const webpack = require("webpack");
const fs = require("fs");
const path = require("path");
const TerserPlugin = require("terser-webpack-plugin");
const WorkboxPlugin = require("workbox-webpack-plugin");
const CompressionPlugin = require("compression-webpack-plugin");
const zopfli = require("@gfx/zopfli");
const ManifestPlugin = require("webpack-manifest-plugin");
const paths = require("./paths.js");
const { babelLoaderConfig } = require("./babel.js");
let version = fs
.readFileSync(path.resolve(paths.polymer_dir, "setup.py"), "utf8")
.match(/\d{8}\.\d+/);
if (!version) {
throw Error("Version not found");
}
version = version[0];
const resolve = {
extensions: [".ts", ".js", ".json", ".tsx"],
alias: {
react: "preact-compat",
"react-dom": "preact-compat",
// Not necessary unless you consume a module using `createClass`
"create-react-class": "preact-compat/lib/create-react-class",
// Not necessary unless you consume a module requiring `react-dom-factories`
"react-dom-factories": "preact-compat/lib/react-dom-factories",
},
};
const plugins = [
// Ignore moment.js locales
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
// Color.js is bloated, it contains all color definitions for all material color sets.
new webpack.NormalModuleReplacementPlugin(
/@polymer\/paper-styles\/color\.js$/,
path.resolve(paths.polymer_dir, "src/util/empty.js")
),
// Ignore roboto pointing at CDN. We use local font-roboto-local.
new webpack.NormalModuleReplacementPlugin(
/@polymer\/font-roboto\/roboto\.js$/,
path.resolve(paths.polymer_dir, "src/util/empty.js")
),
// Ignore mwc icons pointing at CDN.
new webpack.NormalModuleReplacementPlugin(
/@material\/mwc-icon\/mwc-icon-font\.js$/,
path.resolve(paths.polymer_dir, "src/util/empty.js")
),
];
const optimization = (latestBuild) => ({
minimizer: [
new TerserPlugin({
cache: true,
parallel: true,
extractComments: true,
terserOptions: {
ecma: latestBuild ? undefined : 5,
},
}),
],
});
const createAppConfig = ({ isProdBuild, latestBuild, isStatsBuild }) => {
const isCI = process.env.CI === "true";
// Create an object mapping browser urls to their paths during build
const translationMetadata = require("../build-translations/translationMetadata.json");
const workBoxTranslationsTemplatedURLs = {};
const englishFP = translationMetadata["translations"]["en"]["fingerprints"];
Object.keys(englishFP).forEach((key) => {
workBoxTranslationsTemplatedURLs[
`/static/translations/${englishFP[key]}`
] = `build-translations/output/${key}.json`;
});
const publicPath = latestBuild ? "/frontend_latest/" : "/frontend_es5/";
const entry = {
app: "./src/entrypoints/app.ts",
authorize: "./src/entrypoints/authorize.ts",
onboarding: "./src/entrypoints/onboarding.ts",
core: "./src/entrypoints/core.ts",
compatibility: "./src/entrypoints/compatibility.ts",
"custom-panel": "./src/entrypoints/custom-panel.ts",
"hass-icons": "./src/entrypoints/hass-icons.ts",
};
return {
mode: isProdBuild ? "production" : "development",
devtool: isProdBuild
? "cheap-source-map "
: "inline-cheap-module-source-map",
entry,
module: {
rules: [
babelLoaderConfig({ latestBuild }),
{
test: /\.css$/,
use: "raw-loader",
},
{
test: /\.(html)$/,
use: {
loader: "html-loader",
options: {
exportAsEs6Default: true,
},
},
},
],
},
optimization: optimization(latestBuild),
plugins: [
new ManifestPlugin(),
new webpack.DefinePlugin({
__DEV__: JSON.stringify(!isProdBuild),
__DEMO__: false,
__BUILD__: JSON.stringify(latestBuild ? "latest" : "es5"),
__VERSION__: JSON.stringify(version),
__STATIC_PATH__: "/static/",
"process.env.NODE_ENV": JSON.stringify(
isProdBuild ? "production" : "development"
),
}),
...plugins,
isProdBuild &&
!isCI &&
!isStatsBuild &&
new CompressionPlugin({
cache: true,
exclude: [/\.js\.map$/, /\.LICENSE$/, /\.py$/, /\.txt$/],
algorithm(input, compressionOptions, callback) {
return zopfli.gzip(input, compressionOptions, callback);
},
}),
latestBuild &&
new WorkboxPlugin.InjectManifest({
swSrc: "./src/entrypoints/service-worker-hass.js",
swDest: "service_worker.js",
importWorkboxFrom: "local",
include: [/\.js$/],
templatedURLs: {
...workBoxTranslationsTemplatedURLs,
"/static/icons/favicon-192x192.png":
"public/icons/favicon-192x192.png",
"/static/fonts/roboto/Roboto-Light.ttf":
"node_modules/@polymer/font-roboto-local/fonts/roboto/Roboto-Light.ttf",
"/static/fonts/roboto/Roboto-Medium.ttf":
"node_modules/@polymer/font-roboto-local/fonts/roboto/Roboto-Medium.ttf",
"/static/fonts/roboto/Roboto-Regular.ttf":
"node_modules/@polymer/font-roboto-local/fonts/roboto/Roboto-Regular.ttf",
"/static/fonts/roboto/Roboto-Bold.ttf":
"node_modules/@polymer/font-roboto-local/fonts/roboto/Roboto-Bold.ttf",
},
}),
].filter(Boolean),
output: {
filename: ({ chunk }) => {
const dontHash = new Set([
// Files who'se names should not be hashed.
// We currently have none.
]);
if (!isProdBuild || dontHash.has(chunk.name)) return `${chunk.name}.js`;
return `${chunk.name}.${chunk.hash.substr(0, 8)}.js`;
},
chunkFilename:
isProdBuild && !isStatsBuild
? "chunk.[chunkhash].js"
: "[name].chunk.js",
path: latestBuild ? paths.output : paths.output_es5,
publicPath,
},
resolve,
};
};
module.exports = {
resolve,
plugins,
optimization,
createAppConfig,
};

View File

@ -1,48 +0,0 @@
const webpack = require("webpack");
const path = require("path");
const TerserPlugin = require("terser-webpack-plugin");
module.exports.resolve = {
extensions: [".ts", ".js", ".json", ".tsx"],
alias: {
react: "preact-compat",
"react-dom": "preact-compat",
// Not necessary unless you consume a module using `createClass`
"create-react-class": "preact-compat/lib/create-react-class",
// Not necessary unless you consume a module requiring `react-dom-factories`
"react-dom-factories": "preact-compat/lib/react-dom-factories",
},
};
module.exports.plugins = [
// Ignore moment.js locales
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
// Color.js is bloated, it contains all color definitions for all material color sets.
new webpack.NormalModuleReplacementPlugin(
/@polymer\/paper-styles\/color\.js$/,
path.resolve(__dirname, "../src/util/empty.js")
),
// Ignore roboto pointing at CDN. We use local font-roboto-local.
new webpack.NormalModuleReplacementPlugin(
/@polymer\/font-roboto\/roboto\.js$/,
path.resolve(__dirname, "../src/util/empty.js")
),
// Ignore mwc icons pointing at CDN.
new webpack.NormalModuleReplacementPlugin(
/@material\/mwc-icon\/mwc-icon-font\.js$/,
path.resolve(__dirname, "../src/util/empty.js")
),
];
module.exports.optimization = (latestBuild) => ({
minimizer: [
new TerserPlugin({
cache: true,
parallel: true,
extractComments: true,
terserOptions: {
ecma: latestBuild ? undefined : 5,
},
}),
],
});

View File

@ -4,7 +4,7 @@ const {
findIcons,
generateIconset,
genMDIIcons,
} = require("../../gulp/tasks/gen-icons.js");
} = require("../../build-scripts/gulp/gen-icons.js");
function genHademoIcons() {
const iconNames = findIcons("./src", "hademo");

View File

@ -2,8 +2,8 @@ const path = require("path");
const webpack = require("webpack");
const CopyWebpackPlugin = require("copy-webpack-plugin");
const WorkboxPlugin = require("workbox-webpack-plugin");
const { babelLoaderConfig } = require("../config/babel.js");
const webpackBase = require("../config/webpack.js");
const { babelLoaderConfig } = require("../build-scripts/babel.js");
const webpackBase = require("../build-scripts/webpack.js");
const isProd = process.env.NODE_ENV === "production";
const isStatsBuild = process.env.STATS === "1";
@ -72,7 +72,7 @@ module.exports = {
...webpackBase.plugins,
isProd &&
new WorkboxPlugin.GenerateSW({
swDest: "service_worker_es5.js",
swDest: "service_worker.js",
importWorkboxFrom: "local",
include: [],
}),

View File

@ -1,7 +1,7 @@
const path = require("path");
const CopyWebpackPlugin = require("copy-webpack-plugin");
const { babelLoaderConfig } = require("../config/babel.js");
const webpackBase = require("../config/webpack.js");
const { babelLoaderConfig } = require("../build-scripts/babel.js");
const webpackBase = require("../build-scripts/webpack.js");
const isProd = process.env.NODE_ENV === "production";
const chunkFilename = isProd ? "chunk.[chunkhash].js" : "[name].chunk.js";

View File

@ -1,8 +0,0 @@
var path = require("path");
module.exports = {
polymer_dir: path.resolve(__dirname, ".."),
build_dir: path.resolve(__dirname, "../build"),
output: path.resolve(__dirname, "../hass_frontend"),
output_es5: path.resolve(__dirname, "../hass_frontend_es5"),
};

View File

@ -1,3 +1,3 @@
var requireDir = require('require-dir');
var requireDir = require("require-dir");
requireDir('./gulp/tasks/');
requireDir("./build-scripts/gulp/");

View File

@ -4,7 +4,7 @@ const {
findIcons,
generateIconset,
genMDIIcons,
} = require("../../gulp/tasks/gen-icons.js");
} = require("../../build-scripts/gulp/gen-icons.js");
function genHassioIcons() {
const iconNames = findIcons("./src", "hassio");

View File

@ -3,8 +3,8 @@ const CompressionPlugin = require("compression-webpack-plugin");
const zopfli = require("@gfx/zopfli");
const config = require("./config.js");
const { babelLoaderConfig } = require("../config/babel.js");
const webpackBase = require("../config/webpack.js");
const { babelLoaderConfig } = require("../build-scripts/babel.js");
const webpackBase = require("../build-scripts/webpack.js");
const isProdBuild = process.env.NODE_ENV === "production";
const isCI = process.env.CI === "true";

View File

@ -125,6 +125,7 @@
"eslint-plugin-import": "^2.16.0",
"eslint-plugin-prettier": "^3.0.1",
"eslint-plugin-react": "^7.12.4",
"fs-extra": "^7.0.1",
"gulp": "^4.0.0",
"gulp-foreach": "^0.1.0",
"gulp-hash": "^4.2.2",
@ -134,10 +135,12 @@
"gulp-jsonminify": "^1.1.0",
"gulp-merge-json": "^1.3.1",
"gulp-rename": "^1.4.0",
"gulp-zopfli-green": "^3.0.1",
"html-loader": "^0.5.5",
"html-webpack-plugin": "^3.2.0",
"husky": "^1.3.1",
"lint-staged": "^8.1.5",
"lodash.template": "^4.4.0",
"merge-stream": "^1.0.1",
"mocha": "^6.0.2",
"parse5": "^5.1.0",
@ -160,6 +163,7 @@
"webpack": "^4.29.6",
"webpack-cli": "^3.3.0",
"webpack-dev-server": "^3.2.1",
"webpack-manifest-plugin": "^2.0.4",
"workbox-webpack-plugin": "^4.1.1"
},
"resolutions": {

View File

@ -1,30 +1,7 @@
"""Frontend for Home Assistant."""
import os
from user_agents import parse
FAMILY_MIN_VERSION = {
'Chrome': 55, # Async/await
'Chrome Mobile': 55,
'Firefox': 52, # Async/await
'Firefox Mobile': 52,
'Opera': 42, # Async/await
'Edge': 15, # Async/await
'Safari': 10.1, # Async/await
}
from pathlib import Path
def where():
"""Return path to the frontend."""
return os.path.dirname(__file__)
def version(useragent):
"""Get the version for given user agent."""
useragent = parse(useragent)
# on iOS every browser uses the Safari engine
if useragent.os.family == 'iOS':
return useragent.os.version[0] >= FAMILY_MIN_VERSION['Safari']
version = FAMILY_MIN_VERSION.get(useragent.browser.family)
return version and useragent.browser.version[0] >= version
return Path(__file__).parent

View File

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

Before

Width:  |  Height:  |  Size: 824 B

After

Width:  |  Height:  |  Size: 824 B

View File

Before

Width:  |  Height:  |  Size: 292 B

After

Width:  |  Height:  |  Size: 292 B

View File

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

Before

Width:  |  Height:  |  Size: 8.3 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

View File

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

View File

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Before

Width:  |  Height:  |  Size: 424 B

After

Width:  |  Height:  |  Size: 424 B

View File

Before

Width:  |  Height:  |  Size: 683 B

After

Width:  |  Height:  |  Size: 683 B

View File

Before

Width:  |  Height:  |  Size: 734 B

After

Width:  |  Height:  |  Size: 734 B

View File

Before

Width:  |  Height:  |  Size: 790 B

After

Width:  |  Height:  |  Size: 790 B

View File

Before

Width:  |  Height:  |  Size: 1016 B

After

Width:  |  Height:  |  Size: 1016 B

View File

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

Before

Width:  |  Height:  |  Size: 768 B

After

Width:  |  Height:  |  Size: 768 B

View File

Before

Width:  |  Height:  |  Size: 1022 B

After

Width:  |  Height:  |  Size: 1022 B

View File

Before

Width:  |  Height:  |  Size: 803 B

After

Width:  |  Height:  |  Size: 803 B

View File

Before

Width:  |  Height:  |  Size: 822 B

After

Width:  |  Height:  |  Size: 822 B

View File

Before

Width:  |  Height:  |  Size: 571 B

After

Width:  |  Height:  |  Size: 571 B

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 947 B

After

Width:  |  Height:  |  Size: 947 B

View File

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

Before

Width:  |  Height:  |  Size: 852 B

After

Width:  |  Height:  |  Size: 852 B

View File

Before

Width:  |  Height:  |  Size: 263 B

After

Width:  |  Height:  |  Size: 263 B

View File

@ -6,19 +6,4 @@ set -e
cd "$(dirname "$0")/.."
BUILD_DIR=build
BUILD_TRANSLATIONS_DIR=build-translations
OUTPUT_DIR=hass_frontend
OUTPUT_DIR_ES5=hass_frontend_es5
rm -rf $OUTPUT_DIR $OUTPUT_DIR_ES5 $BUILD_DIR $BUILD_TRANSLATIONS_DIR
# Build frontend
./node_modules/.bin/gulp build-translations gen-icons
NODE_ENV=production ./node_modules/.bin/webpack
# Generate the __init__ file
echo "VERSION = '`git rev-parse HEAD`'" >> $OUTPUT_DIR/__init__.py
echo "CREATED_AT = `date +%s`" >> $OUTPUT_DIR/__init__.py
echo "VERSION = '`git rev-parse HEAD`'" >> $OUTPUT_DIR_ES5/__init__.py
echo "CREATED_AT = `date +%s`" >> $OUTPUT_DIR_ES5/__init__.py
./node_modules/.bin/gulp build-release

View File

@ -6,14 +6,4 @@ set -e
cd "$(dirname "$0")/.."
BUILD_DIR=build
OUTPUT_DIR=hass_frontend
OUTPUT_DIR_ES5=hass_frontend_es5
rm -rf $OUTPUT_DIR $OUTPUT_DIR_ES5 $BUILD_DIR
mkdir $OUTPUT_DIR $OUTPUT_DIR_ES5
# Needed in case frontend repo installed with pip3 install -e
cp -r public/__init__.py $OUTPUT_DIR_ES5/
./node_modules/.bin/gulp build-translations gen-icons
./node_modules/.bin/webpack --watch --progress
./node_modules/.bin/gulp develop

View File

@ -7,5 +7,5 @@ set -e
cd "$(dirname "$0")/.."
STATS=1 NODE_ENV=production webpack --profile --json > compilation-stats.json
npx webpack-bundle-analyzer compilation-stats.json hass_frontend
npx webpack-bundle-analyzer compilation-stats.json hass_frontend/frontend_latest
rm compilation-stats.json

View File

@ -8,15 +8,7 @@ setup(
author="The Home Assistant Authors",
author_email="hello@home-assistant.io",
license="Apache License 2.0",
packages=find_packages(
include=[
"hass_frontend",
"hass_frontend_es5",
"hass_frontend.*",
"hass_frontend_es5.*",
]
),
install_requires=["user-agents==2.0.0"],
packages=find_packages(include=["hass_frontend", "hass_frontend.*"]),
include_package_data=True,
zip_safe=False,
)

View File

@ -8,7 +8,7 @@ import {
css,
} from "lit-element";
import "./ha-auth-flow";
import { AuthProvider } from "../data/auth";
import { AuthProvider, fetchAuthProviders } from "../data/auth";
import { registerServiceWorker } from "../util/register-service-worker";
import(/* webpackChunkName: "pick-auth-provider" */ "../auth/ha-pick-auth-provider");
@ -135,7 +135,9 @@ class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
private async _fetchAuthProviders() {
// Fetch auth providers
try {
const response = await (window as any).providersPromise;
// We prefetch this data on page load in authorize.html.template for modern builds
const response = await ((window as any).providersPromise ||
fetchAuthProviders());
const authProviders = await response.json();
// Forward to main screen which will redirect to right onboarding page.

View File

@ -18,3 +18,8 @@ export const getSignedPath = (
hass: HomeAssistant,
path: string
): Promise<SignedPath> => hass.callWS({ type: "auth/sign_path", path });
export const fetchAuthProviders = () =>
fetch("/auth/providers", {
credentials: "same-origin",
});

View File

@ -9,7 +9,10 @@ interface UserStepResponse {
auth_code: string;
}
export const onboardUserStep = async (params: {
export const fetchOnboardingOverview = () =>
fetch("/api/onboarding", { credentials: "same-origin" });
export const onboardUserStep = (params: {
client_id: string;
name: string;
username: string;

View File

@ -1,2 +0,0 @@
/* global importScripts */
importScripts("/static/service-worker-hass.js");

View File

@ -1,3 +1,7 @@
/*
This file is not run through webpack, but instead is directly manipulated
by Workbox Webpack plugin. So we cannot use __DEV__ or other constants.
*/
/* global workbox clients */
function initRouting() {
@ -17,9 +21,7 @@ function initRouting() {
// Get manifest and service worker from network.
workbox.routing.registerRoute(
new RegExp(
`${location.host}/(service_worker.js|service_worker_es5.js|manifest.json)`
),
new RegExp(`${location.host}/(service_worker.js|manifest.json)`),
new workbox.strategies.NetworkOnly()
);
@ -161,11 +163,8 @@ self.addEventListener("message", (message) => {
});
workbox.setConfig({
debug: __DEV__,
debug: false,
});
if (!__DEV__) {
initRouting();
}
initRouting();
initPushNotifications();

View File

@ -0,0 +1,25 @@
<script>
function _ls(src) {
var doc = document.documentElement;
var script = doc.insertBefore(
document.createElement("script"),
doc.lastChild
);
script.type = "text/javascript";
script.src = src;
}
window.Polymer = {
lazyRegister: true,
useNativeCSSProperties: true,
dom: "shadow",
suppressTemplateNotifications: true,
suppressBindingNotifications: true,
};
var webComponentsSupported =
"customElements" in window &&
"content" in document.createElement("template");
if (!webComponentsSupported) {
_ls("/static/polyfills/webcomponents-bundle.js");
}
var isS101 = /\s+Version\/10\.1(?:\.\d+)?\s+Safari\//.test(navigator.userAgent);
</script>

View File

@ -1,10 +1,21 @@
<!doctype html>
<!DOCTYPE html>
<html lang="en">
<head>
<title>Home Assistant</title>
<link rel='preload' href='/static/fonts/roboto/Roboto-Light.ttf' as='font' crossorigin />
<link rel='preload' href='/static/fonts/roboto/Roboto-Regular.ttf' as='font' crossorigin />
<%= require('raw-loader!./_header.html.template').default %>
<link rel="preload" href="<%= latestPageJS %>" as="script" crossorigin />
<link
rel="preload"
href="/static/fonts/roboto/Roboto-Light.ttf"
as="font"
crossorigin
/>
<link
rel="preload"
href="/static/fonts/roboto/Roboto-Regular.ttf"
as="font"
crossorigin
/>
<%= renderTemplate('_header') %>
<style>
.content {
padding: 20px 16px;
@ -27,29 +38,33 @@
</head>
<body>
<div class="content">
<div class='header'>
<img src="/static/icons/favicon-192x192.png" height="52">
<div class="header">
<img src="/static/icons/favicon-192x192.png" height="52" />
Home Assistant
</div>
<ha-authorize><p>Initializing</p></ha-authorize>
</div>
<% if (!latestBuild) { %>
<script src="/static/custom-elements-es5-adapter.js"></script>
<script src="<%= compatibility %>"></script>
<% } %>
<script>
window.providersPromise = fetch('/auth/providers', { credentials: 'same-origin' });
var webComponentsSupported = (
'customElements' in window &&
'content' in document.createElement('template'));
if (!webComponentsSupported) {
var e = document.createElement('script');
e.src = '/static/webcomponents-bundle.js';
document.write(e.outerHTML);
}
<%= renderTemplate('_js_base') %>
<script type="module">
import "<%= latestPageJS %>";
import "<%= latestHassIconsJS %>";
window.providersPromise = fetch("/auth/providers", {
credentials: "same-origin",
});
</script>
<script nomodule>
(function() {
// 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("<%= es5PageJS %>");
_ls("<%= es5HassIconsJS %>");
}
})();
</script>
<script src="<%= entrypoint %>"></script>
<script src='<%= hassIconsJS %>' async></script>
</body>
</html>

View File

@ -1,70 +1,87 @@
<!doctype html>
<!DOCTYPE html>
<html lang="en">
<head>
<link rel='preload' href='<%= coreJS %>' as='script'/>
<link rel='preload' href='/static/fonts/roboto/Roboto-Regular.ttf' as='font' crossorigin />
<link rel='preload' href='/static/fonts/roboto/Roboto-Medium.ttf' as='font' crossorigin />
<%= require('raw-loader!./_header.html.template').default %>
<link rel="preload" href="<%= latestCoreJS %>" as="script" crossorigin />
<link
rel="preload"
href="/static/fonts/roboto/Roboto-Regular.ttf"
as="font"
crossorigin
/>
<link
rel="preload"
href="/static/fonts/roboto/Roboto-Medium.ttf"
as="font"
crossorigin
/>
<%= renderTemplate('_header') %>
<title>Home Assistant</title>
<link rel='apple-touch-icon' sizes='180x180'
href='/static/icons/favicon-apple-180x180.png'>
<link rel="mask-icon" href="/static/icons/mask-icon.svg" color="#03a9f4">
<meta name="apple-itunes-app" content="app-id=1099568401">
<meta name='apple-mobile-web-app-capable' content='yes'>
<meta name="msapplication-square70x70logo" content="/static/icons/tile-win-70x70.png"/>
<meta name="msapplication-square150x150logo" content="/static/icons/tile-win-150x150.png"/>
<meta name="msapplication-wide310x150logo" content="/static/icons/tile-win-310x150.png"/>
<meta name="msapplication-square310x310logo" content="/static/icons/tile-win-310x310.png"/>
<meta name="msapplication-TileColor" content="#03a9f4ff"/>
<meta name='mobile-web-app-capable' content='yes'>
<meta name='referrer' content='same-origin'>
<meta name='theme-color' content='{{ theme_color }}'>
<link
rel="apple-touch-icon"
sizes="180x180"
href="/static/icons/favicon-apple-180x180.png"
/>
<link rel="mask-icon" href="/static/icons/mask-icon.svg" color="#03a9f4" />
<meta name="apple-itunes-app" content="app-id=1099568401" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta
name="msapplication-square70x70logo"
content="/static/icons/tile-win-70x70.png"
/>
<meta
name="msapplication-square150x150logo"
content="/static/icons/tile-win-150x150.png"
/>
<meta
name="msapplication-wide310x150logo"
content="/static/icons/tile-win-310x150.png"
/>
<meta
name="msapplication-square310x310logo"
content="/static/icons/tile-win-310x310.png"
/>
<meta name="msapplication-TileColor" content="#03a9f4ff" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="referrer" content="same-origin" />
<meta name="theme-color" content="#THEMEC" />
<style>
#ha-init-skeleton::before {
display: block;
content: "";
height: 112px;
background-color: {{ theme_color }};
background-color: #THEMEC;
}
</style>
<script>
window.customPanelJS = '<%= customPanelJS %>';
window.noAuth = '{{ no_auth }}';
window.useOAuth = '{{ use_oauth }}'
window.Polymer = {
lazyRegister: true,
useNativeCSSProperties: true,
dom: 'shadow',
suppressTemplateNotifications: true,
suppressBindingNotifications: true,
};
</script>
</head>
<body>
<div id='ha-init-skeleton'></div>
<home-assistant>
</home-assistant>
<% if (!latestBuild) { %>
<script src="/static/custom-elements-es5-adapter.js"></script>
<script src="<%= compatibility %>"></script>
<% } %>
<script>
var webComponentsSupported = (
'customElements' in window &&
'content' in document.createElement('template'));
if (!webComponentsSupported) {
(function() {
var e = document.createElement('script');
e.src = '/static/webcomponents-bundle.js';
document.write(e.outerHTML);
}());
}
<div id="ha-init-skeleton"></div>
<home-assistant> </home-assistant>
<%= renderTemplate('_js_base') %>
<script type="module">
import "<%= latestCoreJS %>";
import "<%= latestAppJS %>";
import "<%= latestHassIconsJS %>";
window.customPanelJS = "<%= latestCustomPanelJS %>";
</script>
<script src='<%= coreJS %>'></script>
<script src='<%= appJS %>'></script>
<script src='<%= hassIconsJS %>' async></script>
<script nomodule>
(function() {
// // Safari 10.1 supports type=module but ignores nomodule, so we add this check.
if (!isS101) {
window.customPanelJS = "<%= es5CustomPanelJS %>";
_ls("/static/polyfills/custom-elements-es5-adapter.js");
_ls("<%= es5Compatibility %>");
_ls("<%= es5CoreJS %>");
_ls("<%= es5AppJS %>");
_ls("<%= es5HassIconsJS %>");
}
})();
</script>
{% for extra_url in extra_urls -%}
<link rel='import' href='{{ extra_url }}' async>
<link rel="import" href="{{ extra_url }}" async />
{% endfor -%}
</body>
</html>

View File

@ -1,10 +1,21 @@
<!doctype html>
<!DOCTYPE html>
<html lang="en">
<head>
<title>Home Assistant</title>
<link rel='preload' href='/static/fonts/roboto/Roboto-Light.ttf' as='font' crossorigin />
<link rel='preload' href='/static/fonts/roboto/Roboto-Regular.ttf' as='font' crossorigin />
<%= require('raw-loader!./_header.html.template').default %>
<link rel="preload" href="<%= latestPageJS %>" as="script" crossorigin />
<link
rel="preload"
href="/static/fonts/roboto/Roboto-Light.ttf"
as="font"
crossorigin
/>
<link
rel="preload"
href="/static/fonts/roboto/Roboto-Regular.ttf"
as="font"
crossorigin
/>
<%= renderTemplate('_header') %>
<style>
.content {
padding: 20px 16px;
@ -28,30 +39,34 @@
</head>
<body>
<div class="content">
<div class='header'>
<img src="/static/icons/favicon-192x192.png" height="52">
<div class="header">
<img src="/static/icons/favicon-192x192.png" height="52" />
Home Assistant
</div>
<ha-onboarding>Initializing</ha-onboarding>
</div>
<% if (!latestBuild) { %>
<script src="/static/custom-elements-es5-adapter.js"></script>
<script src="<%= compatibility %>"></script>
<% } %>
<script>
window.stepsPromise = fetch('/api/onboarding', { credentials: 'same-origin' });
var webComponentsSupported = (
'customElements' in window &&
'content' in document.createElement('template'));
if (!webComponentsSupported) {
var e = document.createElement('script');
e.src = '/static/webcomponents-bundle.js';
document.write(e.outerHTML);
}
<%= renderTemplate('_js_base') %>
<script type="module">
import "<%= latestPageJS %>";
import "<%= latestHassIconsJS %>";
window.stepsPromise = fetch("/api/onboarding", {
credentials: "same-origin",
});
</script>
<script nomodule>
(function() {
// 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("<%= es5PageJS %>");
_ls("<%= es5HassIconsJS %>");
}
})();
</script>
<script src="<%= entrypoint %>"></script>
<script src='<%= hassIconsJS %>' async></script>
</body>
</html>

View File

@ -1,6 +1,3 @@
const serviceWorkerUrl =
__BUILD__ === "latest" ? "/service_worker.js" : "/service_worker_es5.js";
export const registerServiceWorker = (notifyUpdate = true) => {
if (
!("serviceWorker" in navigator) ||
@ -9,7 +6,7 @@ export const registerServiceWorker = (notifyUpdate = true) => {
return;
}
navigator.serviceWorker.register(serviceWorkerUrl).then((reg) => {
navigator.serviceWorker.register("/service_worker.js").then((reg) => {
reg.addEventListener("updatefound", () => {
const installingWorker = reg.installing;
if (!installingWorker || !notifyUpdate) {

View File

@ -1,207 +1,24 @@
const fs = require("fs");
const path = require("path");
const webpack = require("webpack");
const CopyWebpackPlugin = require("copy-webpack-plugin");
const WorkboxPlugin = require("workbox-webpack-plugin");
const CompressionPlugin = require("compression-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const zopfli = require("@gfx/zopfli");
const translationMetadata = require("./build-translations/translationMetadata.json");
const { babelLoaderConfig } = require("./config/babel.js");
const webpackBase = require("./config/webpack.js");
const version = fs.readFileSync("setup.py", "utf8").match(/\d{8}\.\d+/);
if (!version) {
throw Error("Version not found");
}
const VERSION = version[0];
const isCI = process.env.CI === "true";
const isStatsBuild = process.env.STATS === "1";
const generateJSPage = (entrypoint, latestBuild) => {
return new HtmlWebpackPlugin({
inject: false,
template: `./src/html/${entrypoint}.html.template`,
// Default templateParameterGenerator code
// https://github.com/jantimon/html-webpack-plugin/blob/master/index.js#L719
templateParameters: (compilation, assets, option) => ({
latestBuild,
compatibility: assets.chunks.compatibility.entry,
entrypoint: assets.chunks[entrypoint].entry,
hassIconsJS: assets.chunks["hass-icons"].entry,
}),
filename: `${entrypoint}.html`,
});
};
// Create an object mapping browser urls to their paths during build
const workBoxTranslationsTemplatedURLs = {};
const englishFP = translationMetadata["translations"]["en"]["fingerprints"];
Object.keys(englishFP).forEach((key) => {
workBoxTranslationsTemplatedURLs[
`/static/translations/${englishFP[key]}`
] = `build-translations/output/${key}.json`;
});
function createConfig(isProdBuild, latestBuild) {
const buildPath = latestBuild ? "hass_frontend/" : "hass_frontend_es5/";
const publicPath = latestBuild ? "/frontend_latest/" : "/frontend_es5/";
const entry = {
app: "./src/entrypoints/app.ts",
authorize: "./src/entrypoints/authorize.ts",
onboarding: "./src/entrypoints/onboarding.ts",
core: "./src/entrypoints/core.ts",
compatibility: "./src/entrypoints/compatibility.ts",
"custom-panel": "./src/entrypoints/custom-panel.ts",
"hass-icons": "./src/entrypoints/hass-icons.ts",
};
if (latestBuild) {
entry["service-worker-hass"] = "./src/entrypoints/service-worker-hass.js";
}
return {
mode: isProdBuild ? "production" : "development",
devtool: isProdBuild
? "cheap-source-map "
: "inline-cheap-module-source-map",
entry,
module: {
rules: [
babelLoaderConfig({ latestBuild }),
{
test: /\.css$/,
use: "raw-loader",
},
{
test: /\.(html)$/,
use: {
loader: "html-loader",
options: {
exportAsEs6Default: true,
},
},
},
],
},
optimization: webpackBase.optimization(latestBuild),
plugins: [
new webpack.DefinePlugin({
__DEV__: JSON.stringify(!isProdBuild),
__DEMO__: false,
__BUILD__: JSON.stringify(latestBuild ? "latest" : "es5"),
__VERSION__: JSON.stringify(VERSION),
__STATIC_PATH__: "/static/",
"process.env.NODE_ENV": JSON.stringify(
isProdBuild ? "production" : "development"
),
}),
new CopyWebpackPlugin(
[
latestBuild &&
"node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js",
latestBuild && { from: "public", to: "." },
latestBuild && {
from: "build-translations/output",
to: `translations`,
},
latestBuild && {
from: "node_modules/@polymer/font-roboto-local/fonts",
to: "fonts",
},
latestBuild &&
"node_modules/@webcomponents/webcomponentsjs/webcomponents-bundle.js",
latestBuild &&
"node_modules/@webcomponents/webcomponentsjs/webcomponents-bundle.js.map",
latestBuild && {
from:
"node_modules/react-big-calendar/lib/css/react-big-calendar.css",
to: "panels/calendar/",
},
latestBuild && {
from: "node_modules/leaflet/dist/leaflet.css",
to: `images/leaflet/`,
},
latestBuild && {
from: "node_modules/leaflet/dist/images",
to: `images/leaflet/`,
},
!latestBuild && "public/__init__.py",
].filter(Boolean)
),
...webpackBase.plugins,
isProdBuild &&
!isCI &&
!isStatsBuild &&
new CompressionPlugin({
cache: true,
exclude: [/\.js\.map$/, /\.LICENSE$/, /\.py$/, /\.txt$/],
algorithm(input, compressionOptions, callback) {
return zopfli.gzip(input, compressionOptions, callback);
},
}),
new HtmlWebpackPlugin({
inject: false,
template: "./src/html/index.html.template",
// Default templateParameterGenerator code
// https://github.com/jantimon/html-webpack-plugin/blob/master/index.js#L719
templateParameters: (compilation, assets, option) => ({
latestBuild,
compatibility: assets.chunks.compatibility.entry,
appJS: assets.chunks.app.entry,
coreJS: assets.chunks.core.entry,
customPanelJS: assets.chunks["custom-panel"].entry,
hassIconsJS: assets.chunks["hass-icons"].entry,
}),
filename: `index.html`,
}),
generateJSPage("onboarding", latestBuild),
generateJSPage("authorize", latestBuild),
new WorkboxPlugin.InjectManifest({
swSrc: "./src/entrypoints/service-worker-bootstrap.js",
swDest: "service_worker.js",
importWorkboxFrom: "local",
include: [/\.js$/],
templatedURLs: {
...workBoxTranslationsTemplatedURLs,
"/static/icons/favicon-192x192.png":
"public/icons/favicon-192x192.png",
"/static/fonts/roboto/Roboto-Light.ttf":
"node_modules/@polymer/font-roboto-local/fonts/roboto/Roboto-Light.ttf",
"/static/fonts/roboto/Roboto-Medium.ttf":
"node_modules/@polymer/font-roboto-local/fonts/roboto/Roboto-Medium.ttf",
"/static/fonts/roboto/Roboto-Regular.ttf":
"node_modules/@polymer/font-roboto-local/fonts/roboto/Roboto-Regular.ttf",
"/static/fonts/roboto/Roboto-Bold.ttf":
"node_modules/@polymer/font-roboto-local/fonts/roboto/Roboto-Bold.ttf",
},
}),
].filter(Boolean),
output: {
filename: ({ chunk }) => {
const dontHash = new Set([
// This is loaded from service-worker-bootstrap.js
// which is processed by Workbox, not Webpack
"service-worker-hass",
]);
if (!isProdBuild || dontHash.has(chunk.name)) return `${chunk.name}.js`;
return `${chunk.name}.${chunk.hash.substr(0, 8)}.js`;
},
chunkFilename:
isProdBuild && !isStatsBuild
? "chunk.[chunkhash].js"
: "[name].chunk.js",
path: path.resolve(__dirname, buildPath),
publicPath,
},
resolve: webpackBase.resolve,
};
}
const { createAppConfig } = require("./build-scripts/webpack.js");
const isProdBuild = process.env.NODE_ENV === "production";
const configs = [createConfig(isProdBuild, /* latestBuild */ true)];
const isStatsBuild = process.env.STATS === "1";
const configs = [
createAppConfig({
isProdBuild,
isStatsBuild,
latestBuild: true,
}),
];
// const configs = [createConfig(isProdBuild, /* latestBuild */ true)];
if (isProdBuild && !isStatsBuild) {
configs.push(createConfig(isProdBuild, /* latestBuild */ false));
configs.push(
createAppConfig({
isProdBuild,
isStatsBuild,
latestBuild: false,
})
);
}
module.exports = configs;

View File

@ -719,7 +719,7 @@
lodash "^4.17.11"
to-fast-properties "^2.0.0"
"@gfx/zopfli@^1.0.11":
"@gfx/zopfli@^1.0.11", "@gfx/zopfli@^1.0.9":
version "1.0.11"
resolved "https://registry.yarnpkg.com/@gfx/zopfli/-/zopfli-1.0.11.tgz#6ced06b4566a5feb0036fe6a1e0262ce6cb1d6c5"
integrity sha512-wW+hi+bqdYCpBIvL8g7RYub9YXf5crhZK9SNk/VZmbF177Em1VwFv488qyh8iBpCo6GptcP1Zam0bJfY3VmMbg==
@ -2599,7 +2599,7 @@ any-observable@^0.3.0:
resolved "https://registry.yarnpkg.com/any-observable/-/any-observable-0.3.0.tgz#af933475e5806a67d0d7df090dd5e8bef65d119b"
integrity sha512-/FQM1EDkTsf63Ub2C6O7GuYFDsSXUwsaZDurV0np41ocwq0jthUAYCmhBX9f+KwlaCgIuWyr/4WlUQUBfKfZog==
any-promise@^1.0.0:
any-promise@^1.0.0, any-promise@^1.1.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f"
integrity sha1-q8av7tzqUugJzcA3au0845Y10X8=
@ -3749,6 +3749,11 @@ bytes@3.0.0:
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048"
integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=
bytes@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6"
integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==
cacache@^11.0.2, cacache@^11.2.0, cacache@^11.3.1:
version "11.3.2"
resolved "https://registry.yarnpkg.com/cacache/-/cacache-11.3.2.tgz#2d81e308e3d258ca38125b676b98b2ac9ce69bfa"
@ -4157,7 +4162,7 @@ clone-stats@^1.0.0:
resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-1.0.0.tgz#b3782dff8bb5474e18b9b6bf0fdfe782f8777680"
integrity sha1-s3gt/4u1R04Yuba/D9/ngvh3doA=
clone@^1.0.0:
clone@^1.0.0, clone@^1.0.2:
version "1.0.4"
resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4=
@ -4901,6 +4906,13 @@ default-resolution@^2.0.0:
resolved "https://registry.yarnpkg.com/default-resolution/-/default-resolution-2.0.0.tgz#bcb82baa72ad79b426a76732f1a81ad6df26d684"
integrity sha1-vLgrqnKtebQmp2cy8aga1t8m1oQ=
defaults@^1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.3.tgz#c656051e9817d9ff08ed881477f3fe4019f3ef7d"
integrity sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=
dependencies:
clone "^1.0.2"
define-properties@^1.1.2, define-properties@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1"
@ -6393,7 +6405,7 @@ fresh@0.5.2:
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=
from2@^2.1.0:
from2@^2.1.0, from2@^2.1.1:
version "2.3.0"
resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af"
integrity sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=
@ -6420,6 +6432,15 @@ fs-extra@^4.0.2:
jsonfile "^4.0.0"
universalify "^0.1.0"
fs-extra@^7.0.0, fs-extra@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9"
integrity sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==
dependencies:
graceful-fs "^4.1.2"
jsonfile "^4.0.0"
universalify "^0.1.0"
fs-minipass@^1.2.5:
version "1.2.5"
resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.5.tgz#06c277218454ec288df77ada54a03b8702aacb9d"
@ -7001,6 +7022,20 @@ gulp-util@~2.2.14:
through2 "^0.5.0"
vinyl "^0.2.1"
gulp-zopfli-green@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/gulp-zopfli-green/-/gulp-zopfli-green-3.0.1.tgz#5a7dc7b2eb5e33eacaf7fb5a0e5434f15e893d9b"
integrity sha512-/ZLtK7VXe8sRpMiVMpu1VtGK/tUEA/B7QPUC0/6leMgsijfMaNelcmcerXTex0+F9SozVPUt+kjK/hU88QtvXA==
dependencies:
"@gfx/zopfli" "^1.0.9"
bytes "^3.0.0"
defaults "^1.0.2"
fancy-log "^1.3.2"
into-stream "^4.0.0"
plugin-error "^1.0.1"
stream-to-array "^2.0.2"
through2 "^3.0.0"
gulp@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/gulp/-/gulp-4.0.0.tgz#95766c601dade4a77ed3e7b2b6dc03881b596366"
@ -7566,6 +7601,14 @@ intl-messageformat@^2.2.0:
dependencies:
intl-messageformat-parser "1.4.0"
into-stream@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/into-stream/-/into-stream-4.0.0.tgz#ef10ee2ffb6f78af34c93194bbdc36c35f7d8a9d"
integrity sha512-i29KNyE5r0Y/UQzcQ0IbZO1MYJ53Jn0EcFRZPj5FzWKYH17kDFEOwuA+3jroymOI06SW1dEDnly9A1CAreC5dg==
dependencies:
from2 "^2.1.1"
p-is-promise "^2.0.0"
invariant@^2.2.0, invariant@^2.2.2, invariant@^2.2.4:
version "2.2.4"
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
@ -8885,7 +8928,7 @@ lodash.values@~2.4.1:
dependencies:
lodash.keys "~2.4.1"
lodash@4.17.11, lodash@^4.11.1, lodash@^4.16.6, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.3.0, lodash@^4.8.0:
lodash@4.17.11, "lodash@>=3.5 <5", lodash@^4.11.1, lodash@^4.16.6, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.3.0, lodash@^4.8.0:
version "4.17.11"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d"
integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==
@ -12389,6 +12432,13 @@ stream-shift@^1.0.0:
resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952"
integrity sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=
stream-to-array@^2.0.2:
version "2.3.0"
resolved "https://registry.yarnpkg.com/stream-to-array/-/stream-to-array-2.3.0.tgz#bbf6b39f5f43ec30bc71babcb37557acecf34353"
integrity sha1-u/azn19D7DC8cbq8s3VXrOzzQ1M=
dependencies:
any-promise "^1.1.0"
stream@0.0.2:
version "0.0.2"
resolved "https://registry.yarnpkg.com/stream/-/stream-0.0.2.tgz#7f5363f057f6592c5595f00bc80a27f5cec1f0ef"
@ -13908,6 +13958,15 @@ webpack-log@^2.0.0:
ansi-colors "^3.0.0"
uuid "^3.3.2"
webpack-manifest-plugin@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/webpack-manifest-plugin/-/webpack-manifest-plugin-2.0.4.tgz#e4ca2999b09557716b8ba4475fb79fab5986f0cd"
integrity sha512-nejhOHexXDBKQOj/5v5IZSfCeTO3x1Dt1RZEcGfBSul891X/eLIcIVH31gwxPDdsi2Z8LKKFGpM4w9+oTBOSCg==
dependencies:
fs-extra "^7.0.0"
lodash ">=3.5 <5"
tapable "^1.0.0"
webpack-sources@^1.0.1, webpack-sources@^1.1.0, webpack-sources@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.3.0.tgz#2a28dcb9f1f45fe960d8f1493252b5ee6530fa85"