diff --git a/build-scripts/bundle.js b/build-scripts/bundle.js index c02e03e8fc..dfd7e12fd4 100644 --- a/build-scripts/bundle.js +++ b/build-scripts/bundle.js @@ -2,6 +2,15 @@ const path = require("path"); const env = require("./env.js"); const paths = require("./paths.js"); +// GitHub base URL to use for production source maps +// Nightly builds use the commit SHA, otherwise assumes there is a tag that matches the version +module.exports.sourceMapURL = () => { + const ref = env.version().endsWith("dev") + ? process.env.GITHUB_SHA || "dev" + : env.version(); + return `https://raw.githubusercontent.com/home-assistant/frontend/${ref}`; +}; + // Files from NPM Packages that should not be imported // eslint-disable-next-line unused-imports/no-unused-vars module.exports.ignorePackages = ({ latestBuild }) => [ @@ -65,13 +74,14 @@ const htmlMinifierOptions = { }, }; -module.exports.terserOptions = (latestBuild) => ({ +module.exports.terserOptions = ({ latestBuild, isTestBuild }) => ({ safari10: !latestBuild, ecma: latestBuild ? undefined : 5, - output: { comments: false }, + format: { comments: false }, + sourceMap: !isTestBuild, }); -module.exports.babelOptions = ({ latestBuild, isProdBuild }) => ({ +module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({ babelrc: false, compact: false, presets: [ @@ -135,8 +145,11 @@ module.exports.babelOptions = ({ latestBuild, isProdBuild }) => ({ /node_modules[\\/]core-js/, /node_modules[\\/]webpack[\\/]buildin/, ], + sourceMaps: !isTestBuild, }); +const nameSuffix = (latestBuild) => (latestBuild ? "-latest" : "-es5"); + const outputPath = (outputRoot, latestBuild) => path.resolve(outputRoot, latestBuild ? "frontend_latest" : "frontend_es5"); @@ -159,14 +172,17 @@ BundleConfig { latestBuild: boolean, // If we're doing a stats build (create nice chunk names) isStatsBuild: boolean, + // If it's just a test build in CI, skip time on source map generation + isTestBuild: boolean, // Names of entrypoints that should not be hashed dontHash: Set } */ module.exports.config = { - app({ isProdBuild, latestBuild, isStatsBuild, isWDS }) { + app({ isProdBuild, latestBuild, isStatsBuild, isTestBuild, isWDS }) { return { + name: "app" + nameSuffix(latestBuild), entry: { service_worker: "./src/entrypoints/service_worker.ts", app: "./src/entrypoints/app.ts", @@ -180,12 +196,14 @@ module.exports.config = { isProdBuild, latestBuild, isStatsBuild, + isTestBuild, isWDS, }; }, demo({ isProdBuild, latestBuild, isStatsBuild }) { return { + name: "demo" + nameSuffix(latestBuild), entry: { main: path.resolve(paths.demo_dir, "src/entrypoint.ts"), }, @@ -215,6 +233,7 @@ module.exports.config = { } return { + name: "cast" + nameSuffix(latestBuild), entry, outputPath: outputPath(paths.cast_output_root, latestBuild), publicPath: publicPath(latestBuild), @@ -226,8 +245,9 @@ module.exports.config = { }; }, - hassio({ isProdBuild, latestBuild }) { + hassio({ isProdBuild, latestBuild, isStatsBuild, isTestBuild }) { return { + name: "supervisor" + nameSuffix(latestBuild), entry: { entrypoint: path.resolve(paths.hassio_dir, "src/entrypoint.ts"), }, @@ -235,6 +255,8 @@ module.exports.config = { publicPath: publicPath(latestBuild, paths.hassio_publicPath), isProdBuild, latestBuild, + isStatsBuild, + isTestBuild, isHassioBuild: true, defineOverlay: { __SUPERVISOR__: true, @@ -244,6 +266,7 @@ module.exports.config = { gallery({ isProdBuild, latestBuild }) { return { + name: "gallery" + nameSuffix(latestBuild), entry: { entrypoint: path.resolve(paths.gallery_dir, "src/entrypoint.js"), }, diff --git a/build-scripts/env.js b/build-scripts/env.js index 3f21328050..034e069181 100644 --- a/build-scripts/env.js +++ b/build-scripts/env.js @@ -17,7 +17,7 @@ module.exports = { isStatsBuild() { return process.env.STATS === "1"; }, - isTest() { + isTestBuild() { return process.env.IS_TEST === "true"; }, isNetlify() { diff --git a/build-scripts/gulp/app.js b/build-scripts/gulp/app.js index 2959ad6a55..f97a210930 100644 --- a/build-scripts/gulp/app.js +++ b/build-scripts/gulp/app.js @@ -1,8 +1,7 @@ // Run HA develop mode + const gulp = require("gulp"); - const env = require("../env"); - require("./clean.js"); require("./translations.js"); require("./locale-data.js"); @@ -50,7 +49,7 @@ gulp.task( "copy-static-app", env.useRollup() ? "rollup-prod-app" : "webpack-prod-app", // Don't compress running tests - ...(env.isTest() ? [] : ["compress-app"]), + ...(env.isTestBuild() ? [] : ["compress-app"]), gulp.parallel( "gen-pages-prod", "gen-index-app-prod", diff --git a/build-scripts/gulp/hassio.js b/build-scripts/gulp/hassio.js index 5b5e31650d..e1bfcb86e6 100644 --- a/build-scripts/gulp/hassio.js +++ b/build-scripts/gulp/hassio.js @@ -1,7 +1,5 @@ const gulp = require("gulp"); - const env = require("../env"); - require("./clean.js"); require("./gen-icons-json.js"); require("./webpack.js"); @@ -43,6 +41,6 @@ gulp.task( env.useRollup() ? "rollup-prod-hassio" : "webpack-prod-hassio", "gen-index-hassio-prod", ...// Don't compress running tests - (env.isTest() ? [] : ["compress-hassio"]) + (env.isTestBuild() ? [] : ["compress-hassio"]) ) ); diff --git a/build-scripts/gulp/webpack.js b/build-scripts/gulp/webpack.js index 051fe07fa5..ed9fdb3ba6 100644 --- a/build-scripts/gulp/webpack.js +++ b/build-scripts/gulp/webpack.js @@ -5,6 +5,7 @@ const webpack = require("webpack"); const WebpackDevServer = require("webpack-dev-server"); const log = require("fancy-log"); const path = require("path"); +const env = require("../env"); const paths = require("../paths"); const { createAppConfig, @@ -104,6 +105,8 @@ gulp.task("webpack-prod-app", () => prodBuild( bothBuilds(createAppConfig, { isProdBuild: true, + isStatsBuild: env.isStatsBuild(), + isTestBuild: env.isTestBuild(), }) ) ); @@ -161,6 +164,8 @@ gulp.task("webpack-prod-hassio", () => prodBuild( bothBuilds(createHassioConfig, { isProdBuild: true, + isStatsBuild: env.isStatsBuild(), + isTestBuild: env.isTestBuild(), }) ) ); diff --git a/build-scripts/rollup.js b/build-scripts/rollup.js index ed030a774a..ff734930a2 100644 --- a/build-scripts/rollup.js +++ b/build-scripts/rollup.js @@ -76,7 +76,7 @@ const createRollupConfig = ({ }), !isWDS && worker(), !isWDS && dontHashPlugin({ dontHash }), - !isWDS && isProdBuild && terser(bundle.terserOptions(latestBuild)), + !isWDS && isProdBuild && terser(bundle.terserOptions({ latestBuild })), !isWDS && isStatsBuild && visualizer({ diff --git a/build-scripts/webpack.js b/build-scripts/webpack.js index 77b78a1dc7..bce6291b98 100644 --- a/build-scripts/webpack.js +++ b/build-scripts/webpack.js @@ -22,6 +22,7 @@ class LogStartCompilePlugin { } const createWebpackConfig = ({ + name, entry, outputPath, publicPath, @@ -29,6 +30,7 @@ const createWebpackConfig = ({ isProdBuild, latestBuild, isStatsBuild, + isTestBuild, isHassioBuild, dontHash, }) => { @@ -37,10 +39,16 @@ const createWebpackConfig = ({ } const ignorePackages = bundle.ignorePackages({ latestBuild }); return { + name, mode: isProdBuild ? "production" : "development", target: ["web", latestBuild ? "es2017" : "es5"], - devtool: isProdBuild - ? "cheap-module-source-map" + // For tests/CI, source maps are skipped to gain build speed + // For production, generate source maps for accurate stack traces without source code + // For development, generate "cheap" versions that can map to original line numbers + devtool: isTestBuild + ? false + : isProdBuild + ? "nosources-source-map" : "eval-cheap-module-source-map", entry, node: false, @@ -51,7 +59,7 @@ const createWebpackConfig = ({ use: { loader: "babel-loader", options: { - ...bundle.babelOptions({ latestBuild, isProdBuild }), + ...bundle.babelOptions({ latestBuild, isProdBuild, isTestBuild }), cacheDirectory: !isProdBuild, cacheCompression: false, }, @@ -68,7 +76,7 @@ const createWebpackConfig = ({ new TerserPlugin({ parallel: true, extractComments: true, - terserOptions: bundle.terserOptions(latestBuild), + terserOptions: bundle.terserOptions({ latestBuild, isTestBuild }), }), ], moduleIds: isProdBuild && !isStatsBuild ? "deterministic" : "named", @@ -153,6 +161,22 @@ const createWebpackConfig = ({ publicPath, // To silence warning in worker plugin globalObject: "self", + // Since production source maps don't include sources, we need to point to them elsewhere + // For dependencies, just provide the path (no source in browser) + // Otherwise, point to the raw code on GitHub for browser to load + devtoolModuleFilenameTemplate: + !isTestBuild && isProdBuild + ? (info) => { + const sourcePath = info.resourcePath.replace(/^\.\//, ""); + if ( + sourcePath.startsWith("node_modules") || + sourcePath.startsWith("webpack") + ) { + return `no-source/${sourcePath}`; + } + return `${bundle.sourceMapURL()}/${sourcePath}`; + } + : undefined, }, experiments: { topLevelAwait: true, @@ -160,9 +184,14 @@ const createWebpackConfig = ({ }; }; -const createAppConfig = ({ isProdBuild, latestBuild, isStatsBuild }) => +const createAppConfig = ({ + isProdBuild, + latestBuild, + isStatsBuild, + isTestBuild, +}) => createWebpackConfig( - bundle.config.app({ isProdBuild, latestBuild, isStatsBuild }) + bundle.config.app({ isProdBuild, latestBuild, isStatsBuild, isTestBuild }) ); const createDemoConfig = ({ isProdBuild, latestBuild, isStatsBuild }) => @@ -173,8 +202,20 @@ const createDemoConfig = ({ isProdBuild, latestBuild, isStatsBuild }) => const createCastConfig = ({ isProdBuild, latestBuild }) => createWebpackConfig(bundle.config.cast({ isProdBuild, latestBuild })); -const createHassioConfig = ({ isProdBuild, latestBuild }) => - createWebpackConfig(bundle.config.hassio({ isProdBuild, latestBuild })); +const createHassioConfig = ({ + isProdBuild, + latestBuild, + isStatsBuild, + isTestBuild, +}) => + createWebpackConfig( + bundle.config.hassio({ + isProdBuild, + latestBuild, + isStatsBuild, + isTestBuild, + }) + ); const createGalleryConfig = ({ isProdBuild, latestBuild }) => createWebpackConfig(bundle.config.gallery({ isProdBuild, latestBuild })); diff --git a/webpack.config.js b/webpack.config.js index fdd9227cb9..fb6e7d6a2f 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,5 +1,12 @@ +/* eslint-disable import/extensions */ +/* eslint-disable @typescript-eslint/no-var-requires */ + const { createAppConfig } = require("./build-scripts/webpack.js"); -const { isProdBuild, isStatsBuild } = require("./build-scripts/env.js"); +const { + isProdBuild, + isStatsBuild, + isTestBuild, +} = require("./build-scripts/env.js"); // This file exists because we haven't migrated the stats script yet @@ -7,6 +14,7 @@ const configs = [ createAppConfig({ isProdBuild: isProdBuild(), isStatsBuild: isStatsBuild(), + isTestBuild: isTestBuild(), latestBuild: true, }), ]; @@ -16,6 +24,7 @@ if (isProdBuild && !isStatsBuild) { createAppConfig({ isProdBuild: isProdBuild(), isStatsBuild: isStatsBuild(), + isTestBuild: isTestBuild(), latestBuild: false, }) );