* Add rspack

* Remove TransformAsyncModulesPlugin from rspack

* Migrate all webpack usage to rspack

* Migrate tests to vitest

* Fix test suites

* Remove chai dependency

* Fix compute_state_display tests

* Fix resolveTimeZone

* Reduces test pipeline

* Revert test ci

* optimize chunk filtering

* Migrate landing-page to rspack

* Update rspack dependencies

* Add rsdoctor

* Fix prod build bundle size

* Use rsdoctor for demo stats

* Remove unused webpack configs

* Update build-scripts/rspack.cjs

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>

* Fix eslint

* Update rspack

* Remove unused code

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
This commit is contained in:
Wendelin 2024-11-26 14:49:13 +01:00 committed by GitHub
parent 09c5dab69f
commit bd0bfc1fbe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
72 changed files with 2199 additions and 1232 deletions

View File

@ -4,6 +4,7 @@
"esbenp.prettier-vscode",
"runem.lit-plugin",
"github.vscode-pull-request-github",
"eamodio.gitlens"
"eamodio.gitlens",
"vitest.explorer"
]
}

View File

@ -152,7 +152,6 @@ module.exports.babelOptions = ({
exclude: [
// \\ for Windows, / for Mac OS and Linux
/node_modules[\\/]core-js/,
/node_modules[\\/]webpack[\\/]buildin/,
],
sourceMaps: !isTestBuild,
overrides: [

View File

@ -9,7 +9,7 @@ import "./locale-data.js";
import "./service-worker.js";
import "./translations.js";
import "./wds.js";
import "./webpack.js";
import "./rspack.js";
gulp.task(
"develop-app",
@ -26,7 +26,7 @@ gulp.task(
"build-locale-data"
),
"copy-static-app",
env.useWDS() ? "wds-watch-app" : "webpack-watch-app"
env.useWDS() ? "wds-watch-app" : "rspack-watch-app"
)
);
@ -39,9 +39,20 @@ gulp.task(
"clean",
gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"),
"copy-static-app",
"webpack-prod-app",
"rspack-prod-app",
gulp.parallel("gen-pages-app-prod", "gen-service-worker-app-prod"),
// Don't compress running tests
...(env.isTestBuild() ? [] : ["compress-app"])
...(env.isTestBuild() || env.isStatsBuild() ? [] : ["compress-app"])
)
);
gulp.task(
"analyze-app",
gulp.series(
async function setEnv() {
process.env.STATS = "1";
},
"clean",
"rspack-prod-app"
)
);

View File

@ -4,7 +4,7 @@ import "./entry-html.js";
import "./gather-static.js";
import "./service-worker.js";
import "./translations.js";
import "./webpack.js";
import "./rspack.js";
gulp.task(
"develop-cast",
@ -17,7 +17,7 @@ gulp.task(
gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"),
"copy-static-cast",
"gen-pages-cast-dev",
"webpack-dev-server-cast"
"rspack-dev-server-cast"
)
);
@ -31,7 +31,7 @@ gulp.task(
"translations-enable-merge-backend",
gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"),
"copy-static-cast",
"webpack-prod-cast",
"rspack-prod-cast",
"gen-pages-cast-prod"
)
);

View File

@ -5,7 +5,7 @@ import "./gather-static.js";
import "./gen-icons-json.js";
import "./service-worker.js";
import "./translations.js";
import "./webpack.js";
import "./rspack.js";
gulp.task(
"develop-demo",
@ -22,7 +22,7 @@ gulp.task(
"build-locale-data"
),
"copy-static-demo",
"webpack-dev-server-demo"
"rspack-dev-server-demo"
)
);
@ -37,7 +37,18 @@ gulp.task(
"translations-enable-merge-backend",
gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"),
"copy-static-demo",
"webpack-prod-demo",
"rspack-prod-demo",
"gen-pages-demo-prod"
)
);
gulp.task(
"analyze-demo",
gulp.series(
async function setEnv() {
process.env.STATS = "1";
},
"clean",
"rspack-prod-demo"
)
);

View File

@ -11,7 +11,7 @@ import "./gather-static.js";
import "./gen-icons-json.js";
import "./service-worker.js";
import "./translations.js";
import "./webpack.js";
import "./rspack.js";
gulp.task("gather-gallery-pages", async function gatherPages() {
const pageDir = path.resolve(paths.gallery_dir, "src/pages");
@ -156,7 +156,7 @@ gulp.task(
"copy-static-gallery",
"gen-pages-gallery-dev",
gulp.parallel(
"webpack-dev-server-gallery",
"rspack-dev-server-gallery",
async function watchMarkdownFiles() {
gulp.watch(
[
@ -185,7 +185,7 @@ gulp.task(
"gather-gallery-pages"
),
"copy-static-gallery",
"webpack-prod-gallery",
"rspack-prod-gallery",
"gen-pages-gallery-prod"
)
);

View File

@ -6,7 +6,7 @@ import "./entry-html.js";
import "./gather-static.js";
import "./gen-icons-json.js";
import "./translations.js";
import "./webpack.js";
import "./rspack.js";
gulp.task(
"develop-hassio",
@ -21,7 +21,7 @@ gulp.task(
"copy-translations-supervisor",
"build-locale-data",
"copy-static-supervisor",
"webpack-watch-hassio"
"rspack-watch-hassio"
)
);
@ -37,7 +37,7 @@ gulp.task(
"copy-translations-supervisor",
"build-locale-data",
"copy-static-supervisor",
"webpack-prod-hassio",
"rspack-prod-hassio",
"gen-pages-hassio-prod",
...// Don't compress running tests
(env.isTestBuild() ? [] : ["compress-hassio"])

View File

@ -5,7 +5,7 @@ import "./entry-html.js";
import "./gather-static.js";
import "./gen-icons-json.js";
import "./translations.js";
import "./webpack.js";
import "./rspack.js";
gulp.task(
"develop-landing-page",
@ -20,7 +20,7 @@ gulp.task(
"build-locale-data",
"copy-static-landing-page",
"gen-pages-landing-page-dev",
"webpack-watch-landing-page"
"rspack-watch-landing-page"
)
);
@ -35,7 +35,7 @@ gulp.task(
"copy-translations-landing-page",
"build-locale-data",
"copy-static-landing-page",
"webpack-prod-landing-page",
"rspack-prod-landing-page",
"gen-pages-landing-page-prod"
)
);

View File

@ -1,11 +1,11 @@
// Tasks to run webpack.
// Tasks to run rspack.
import fs from "fs";
import path from "path";
import log from "fancy-log";
import gulp from "gulp";
import webpack from "webpack";
import WebpackDevServer from "webpack-dev-server";
import rspack from "@rspack/core";
import { RspackDevServer } from "@rspack/dev-server";
import env from "../env.cjs";
import paths from "../paths.cjs";
import {
@ -15,7 +15,7 @@ import {
createGalleryConfig,
createHassioConfig,
createLandingPageConfig,
} from "../webpack.cjs";
} from "../rspack.cjs";
const bothBuilds = (createConfigFunc, params) => [
createConfigFunc({ ...params, latestBuild: true }),
@ -31,7 +31,7 @@ const isWsl =
/**
* @param {{
* compiler: import("webpack").Compiler,
* compiler: import("@rspack/core").Compiler,
* contentBase: string,
* port: number,
* listenHost?: string
@ -48,7 +48,7 @@ const runDevServer = async ({
// For dev container, we need to listen on all hosts
listenHost = env.isDevContainer() ? "0.0.0.0" : "localhost";
}
const server = new WebpackDevServer(
const server = new RspackDevServer(
{
hot: false,
open: true,
@ -65,7 +65,7 @@ const runDevServer = async ({
await server.start();
// Server listening
log("[webpack-dev-server]", `Project is running at http://localhost:${port}`);
log("[rspack-dev-server]", `Project is running at http://localhost:${port}`);
};
const doneHandler = (done) => (err, stats) => {
@ -90,16 +90,16 @@ const doneHandler = (done) => (err, stats) => {
const prodBuild = (conf) =>
new Promise((resolve) => {
webpack(
rspack(
conf,
// Resolve promise when done. Because we pass a callback, webpack closes itself
// Resolve promise when done. Because we pass a callback, rspack closes itself
doneHandler(resolve)
);
});
gulp.task("webpack-watch-app", () => {
gulp.task("rspack-watch-app", () => {
// This command will run forever because we don't close compiler
webpack(
rspack(
process.env.ES5
? bothBuilds(createAppConfig, { isProdBuild: false })
: createAppConfig({ isProdBuild: false, latestBuild: true })
@ -110,7 +110,7 @@ gulp.task("webpack-watch-app", () => {
);
});
gulp.task("webpack-prod-app", () =>
gulp.task("rspack-prod-app", () =>
prodBuild(
bothBuilds(createAppConfig, {
isProdBuild: true,
@ -120,9 +120,9 @@ gulp.task("webpack-prod-app", () =>
)
);
gulp.task("webpack-dev-server-demo", () =>
gulp.task("rspack-dev-server-demo", () =>
runDevServer({
compiler: webpack(
compiler: rspack(
createDemoConfig({ isProdBuild: false, latestBuild: true })
),
contentBase: paths.demo_output_root,
@ -130,17 +130,18 @@ gulp.task("webpack-dev-server-demo", () =>
})
);
gulp.task("webpack-prod-demo", () =>
gulp.task("rspack-prod-demo", () =>
prodBuild(
bothBuilds(createDemoConfig, {
isProdBuild: true,
isStatsBuild: env.isStatsBuild(),
})
)
);
gulp.task("webpack-dev-server-cast", () =>
gulp.task("rspack-dev-server-cast", () =>
runDevServer({
compiler: webpack(
compiler: rspack(
createCastConfig({ isProdBuild: false, latestBuild: true })
),
contentBase: paths.cast_output_root,
@ -150,7 +151,7 @@ gulp.task("webpack-dev-server-cast", () =>
})
);
gulp.task("webpack-prod-cast", () =>
gulp.task("rspack-prod-cast", () =>
prodBuild(
bothBuilds(createCastConfig, {
isProdBuild: true,
@ -158,9 +159,9 @@ gulp.task("webpack-prod-cast", () =>
)
);
gulp.task("webpack-watch-hassio", () => {
gulp.task("rspack-watch-hassio", () => {
// This command will run forever because we don't close compiler
webpack(
rspack(
createHassioConfig({
isProdBuild: false,
latestBuild: true,
@ -173,7 +174,7 @@ gulp.task("webpack-watch-hassio", () => {
);
});
gulp.task("webpack-prod-hassio", () =>
gulp.task("rspack-prod-hassio", () =>
prodBuild(
bothBuilds(createHassioConfig, {
isProdBuild: true,
@ -183,9 +184,9 @@ gulp.task("webpack-prod-hassio", () =>
)
);
gulp.task("webpack-dev-server-gallery", () =>
gulp.task("rspack-dev-server-gallery", () =>
runDevServer({
compiler: webpack(
compiler: rspack(
createGalleryConfig({ isProdBuild: false, latestBuild: true })
),
contentBase: paths.gallery_output_root,
@ -194,7 +195,7 @@ gulp.task("webpack-dev-server-gallery", () =>
})
);
gulp.task("webpack-prod-gallery", () =>
gulp.task("rspack-prod-gallery", () =>
prodBuild(
createGalleryConfig({
isProdBuild: true,
@ -203,9 +204,9 @@ gulp.task("webpack-prod-gallery", () =>
)
);
gulp.task("webpack-watch-landing-page", () => {
gulp.task("rspack-watch-landing-page", () => {
// This command will run forever because we don't close compiler
webpack(
rspack(
process.env.ES5
? bothBuilds(createLandingPageConfig, { isProdBuild: false })
: createLandingPageConfig({ isProdBuild: false, latestBuild: true })
@ -220,7 +221,7 @@ gulp.task("webpack-watch-landing-page", () => {
);
});
gulp.task("webpack-prod-landing-page", () =>
gulp.task("rspack-prod-landing-page", () =>
prodBuild(
bothBuilds(createLandingPageConfig, {
isProdBuild: true,

View File

@ -1,16 +1,13 @@
const { existsSync } = require("fs");
const path = require("path");
const webpack = require("webpack");
const rspack = require("@rspack/core");
const { RsdoctorRspackPlugin } = require("@rsdoctor/rspack-plugin");
const { StatsWriterPlugin } = require("webpack-stats-plugin");
const filterStats = require("@bundle-stats/plugin-webpack-filter").default;
const TerserPlugin = require("terser-webpack-plugin");
const { WebpackManifestPlugin } = require("webpack-manifest-plugin");
const { WebpackManifestPlugin } = require("rspack-manifest-plugin");
const log = require("fancy-log");
const WebpackBar = require("webpackbar");
const {
TransformAsyncModulesPlugin,
} = require("transform-async-modules-webpack-plugin");
const { dependencies } = require("../package.json");
const WebpackBar = require("webpackbar/rspack");
const paths = require("./paths.cjs");
const bundle = require("./bundle.cjs");
@ -28,7 +25,7 @@ class LogStartCompilePlugin {
}
}
const createWebpackConfig = ({
const createRspackConfig = ({
name,
entry,
outputPath,
@ -102,13 +99,18 @@ const createWebpackConfig = ({
splitChunks: {
// Disable splitting for web workers and worklets because imports of
// external chunks are broken for:
// - ESM output: https://github.com/webpack/webpack/issues/17014
chunks: !isProdBuild
? // improve incremental build speed, but blows up bundle size
new RegExp(
`^(?!(${Object.keys(entry).join("|")}|.*work(?:er|let))$)`
)
: // - ESM output: https://github.com/webpack/webpack/issues/17014
// - Worklets use `importScripts`: https://github.com/webpack/webpack/issues/11543
chunks: (chunk) =>
(chunk) =>
!chunk.canBeInitial() &&
!new RegExp(`^.+-work${latestBuild ? "(?:let|er)" : "let"}$`).test(
chunk.name
),
!new RegExp(
`^.+-work${latestBuild ? "(?:let|er)" : "let"}$`
).test(chunk.name),
},
},
plugins: [
@ -117,10 +119,10 @@ const createWebpackConfig = ({
// Only include the JS of entrypoints
filter: (file) => file.isInitial && !file.name.endsWith(".map"),
}),
new webpack.DefinePlugin(
new rspack.DefinePlugin(
bundle.definedVars({ isProdBuild, latestBuild, defineOverlay })
),
new webpack.IgnorePlugin({
new rspack.IgnorePlugin({
checkResource(resource, context) {
// Only use ignore to intercept imports that we don't control
// inside node_module dependencies.
@ -152,7 +154,7 @@ const createWebpackConfig = ({
);
},
}),
new webpack.NormalModuleReplacementPlugin(
new rspack.NormalModuleReplacementPlugin(
new RegExp(
bundle.emptyPackages({ latestBuild, isHassioBuild }).join("|")
),
@ -168,10 +170,14 @@ const createWebpackConfig = ({
stats: { assets: true, chunks: true, modules: true },
transform: (stats) => JSON.stringify(filterStats(stats)),
}),
!latestBuild &&
new TransformAsyncModulesPlugin({
browserslistEnv: "legacy",
runtime: { version: dependencies["@babel/runtime"] },
isProdBuild &&
isStatsBuild &&
new RsdoctorRspackPlugin({
reportDir: path.join(paths.build_dir, "rsdoctor"),
features: ["plugins", "bundle"],
supports: {
generateTileGraph: true,
},
}),
].filter(Boolean),
resolve: {
@ -210,8 +216,6 @@ const createWebpackConfig = ({
isProdBuild && !isStatsBuild ? "[id].[contenthash][ext]" : "[id][ext]",
crossOriginLoading: "use-credentials",
hashFunction: "xxhash64",
hashDigest: "base64url",
hashDigestLength: 11, // full length of 64 bit base64url
path: outputPath,
publicPath,
// To silence warning in worker plugin
@ -253,17 +257,17 @@ const createAppConfig = ({
isStatsBuild,
isTestBuild,
}) =>
createWebpackConfig(
createRspackConfig(
bundle.config.app({ isProdBuild, latestBuild, isStatsBuild, isTestBuild })
);
const createDemoConfig = ({ isProdBuild, latestBuild, isStatsBuild }) =>
createWebpackConfig(
createRspackConfig(
bundle.config.demo({ isProdBuild, latestBuild, isStatsBuild })
);
const createCastConfig = ({ isProdBuild, latestBuild }) =>
createWebpackConfig(bundle.config.cast({ isProdBuild, latestBuild }));
createRspackConfig(bundle.config.cast({ isProdBuild, latestBuild }));
const createHassioConfig = ({
isProdBuild,
@ -271,7 +275,7 @@ const createHassioConfig = ({
isStatsBuild,
isTestBuild,
}) =>
createWebpackConfig(
createRspackConfig(
bundle.config.hassio({
isProdBuild,
latestBuild,
@ -281,10 +285,10 @@ const createHassioConfig = ({
);
const createGalleryConfig = ({ isProdBuild, latestBuild }) =>
createWebpackConfig(bundle.config.gallery({ isProdBuild, latestBuild }));
createRspackConfig(bundle.config.gallery({ isProdBuild, latestBuild }));
const createLandingPageConfig = ({ isProdBuild, latestBuild }) =>
createWebpackConfig(bundle.config.landingPage({ isProdBuild, latestBuild }));
createRspackConfig(bundle.config.landingPage({ isProdBuild, latestBuild }));
module.exports = {
createAppConfig,
@ -292,6 +296,6 @@ module.exports = {
createCastConfig,
createHassioConfig,
createGalleryConfig,
createRspackConfig,
createLandingPageConfig,
createWebpackConfig,
};

View File

@ -1,8 +0,0 @@
import webpack from "../build-scripts/webpack.cjs";
import env from "../build-scripts/env.cjs";
export default webpack.createCastConfig({
isProdBuild: env.isProdBuild(),
isStatsBuild: env.isStatsBuild(),
latestBuild: true,
});

View File

@ -4,11 +4,6 @@
# Stop on errors
set -e
cd "$(dirname "$0")/.."
cd "$(dirname "$0")/../.."
export STATS=1
statsfile="compilation-stats-demo.json"
./node_modules/.bin/webpack-cli --profile --node-env=production --json=$statsfile
npx webpack-bundle-analyzer $statsfile dist/frontend_latest
rm -f $statsfile
./node_modules/.bin/gulp analyze-demo

View File

@ -1,11 +0,0 @@
import webpack from "../build-scripts/webpack.cjs";
import env from "../build-scripts/env.cjs";
// File just used for stats builds
const latestBuild = true;
export default webpack.createDemoConfig({
isProdBuild: env.isProdBuild(),
isStatsBuild: env.isStatsBuild(),
latestBuild,
});

View File

@ -1,3 +1,4 @@
/* eslint-disable import/no-extraneous-dependencies */
import unusedImports from "eslint-plugin-unused-imports";
import globals from "globals";
import tsParser from "@typescript-eslint/parser";
@ -57,7 +58,7 @@ export default [
settings: {
"import/resolver": {
webpack: {
config: "./webpack.config.cjs",
config: "./rspack.config.cjs",
},
},
},

View File

@ -1,4 +1,3 @@
/* eslint-disable lit/no-template-arrow */
import type { TemplateResult } from "lit";
import { LitElement, html, css } from "lit";
import { customElement, state } from "lit/decorators";

View File

@ -1,8 +0,0 @@
import webpack from "../build-scripts/webpack.cjs";
import env from "../build-scripts/env.cjs";
export default webpack.createGalleryConfig({
isProdBuild: env.isProdBuild(),
isStatsBuild: env.isStatsBuild(),
latestBuild: true,
});

View File

@ -1,8 +0,0 @@
import webpack from "../build-scripts/webpack.cjs";
import env from "../build-scripts/env.cjs";
export default webpack.createHassioConfig({
isProdBuild: env.isProdBuild(),
isStatsBuild: env.isStatsBuild(),
latestBuild: true,
});

View File

@ -1,8 +0,0 @@
import webpack from "../build-scripts/webpack.cjs";
import env from "../build-scripts/env.cjs";
export default webpack.createLandingPageConfig({
isProdBuild: env.isProdBuild(),
isStatsBuild: env.isStatsBuild(),
latestBuild: true,
});

View File

@ -19,7 +19,7 @@
"postinstall": "husky",
"prepack": "pinst --disable",
"postpack": "pinst --enable",
"test": "instant-mocha --webpack-config ./test/webpack.config.js --require ./test/setup.cjs \"test/**/*.ts\""
"test": "vitest run --config test/vitest.config.ts"
},
"author": "Paulus Schoutsen <Paulus@PaulusSchoutsen.nl> (http://paulusschoutsen.nl)",
"license": "Apache-2.0",
@ -166,6 +166,9 @@
"@octokit/plugin-retry": "7.1.2",
"@octokit/rest": "21.0.2",
"@open-wc/dev-server-hmr": "0.1.4",
"@rsdoctor/rspack-plugin": "0.4.8",
"@rspack/cli": "1.1.4",
"@rspack/core": "1.1.4",
"@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.19",
"@types/chromecast-caf-sender": "1.0.11",
@ -190,7 +193,6 @@
"babel-loader": "9.2.1",
"babel-plugin-template-html-minifier": "4.1.0",
"browserslist-useragent-regexp": "4.1.3",
"chai": "5.1.2",
"del": "8.0.0",
"eslint": "9.15.0",
"eslint-config-airbnb-base": "15.0.0",
@ -212,7 +214,6 @@
"gulp-zopfli-green": "6.0.2",
"html-minifier-terser": "7.2.0",
"husky": "9.1.7",
"instant-mocha": "1.5.3",
"jszip": "3.10.1",
"lint-staged": "15.2.10",
"lit-analyzer": "2.0.3",
@ -220,11 +221,11 @@
"lodash.template": "4.5.0",
"magic-string": "0.30.13",
"map-stream": "0.0.7",
"mocha": "10.8.2",
"object-hash": "3.0.0",
"open": "10.1.0",
"pinst": "3.0.0",
"prettier": "3.3.3",
"rspack-manifest-plugin": "5.0.2",
"serve-handler": "6.1.6",
"sinon": "19.0.2",
"systemjs": "6.15.1",
@ -233,10 +234,7 @@
"transform-async-modules-webpack-plugin": "1.1.1",
"ts-lit-plugin": "2.0.2",
"typescript": "5.7.2",
"webpack": "5.96.1",
"webpack-cli": "5.1.4",
"webpack-dev-server": "5.1.0",
"webpack-manifest-plugin": "5.0.0",
"vitest": "2.1.5",
"webpack-stats-plugin": "1.1.3",
"webpackbar": "7.0.0",
"workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch"

View File

@ -1,12 +1,12 @@
/* eslint-disable @typescript-eslint/no-var-requires */
// Needs to remain CommonJS until eslint-import-resolver-webpack supports ES modules
const webpack = require("./build-scripts/webpack.cjs");
const rspack = require("./build-scripts/rspack.cjs");
const env = require("./build-scripts/env.cjs");
// This file exists because we haven't migrated the stats script yet
const configs = [
webpack.createAppConfig({
rspack.createAppConfig({
isProdBuild: env.isProdBuild(),
isStatsBuild: env.isStatsBuild(),
isTestBuild: env.isTestBuild(),
@ -16,7 +16,7 @@ const configs = [
if (env.isProdBuild() && !env.isStatsBuild()) {
configs.push(
webpack.createAppConfig({
rspack.createAppConfig({
isProdBuild: env.isProdBuild(),
isStatsBuild: env.isStatsBuild(),
isTestBuild: env.isTestBuild(),

View File

@ -6,9 +6,4 @@ set -e
cd "$(dirname "$0")/.."
export STATS=1
statsfile="compilation-stats.json"
./node_modules/.bin/webpack-cli --profile --node-env=production --json=$statsfile
npx webpack-bundle-analyzer $statsfile hass_frontend/frontend_latest
rm -f $statsfile
./node_modules/.bin/gulp analyze-app

View File

@ -1,15 +1,12 @@
import { TimeZone } from "../../data/translation";
const RESOLVED_TIME_ZONE = Intl.DateTimeFormat?.().resolvedOptions?.().timeZone;
// Browser time zone can be determined from Intl, with fallback to UTC for polyfill or no support.
// Alternatively, we could fallback to a fixed offset IANA zone (e.g. "Etc/GMT+5") using
// Date.prototype.getTimeOffset(), but IANA only has whole hour Etc zones, and problems
// might occur with relative time due to DST.
// Use optional chain instead of polyfill import since polyfill will always return UTC
export const LOCAL_TIME_ZONE =
Intl.DateTimeFormat?.().resolvedOptions?.().timeZone ?? "UTC";
export const LOCAL_TIME_ZONE = RESOLVED_TIME_ZONE ?? "UTC";
// Pick time zone based on user profile option. Core zone is used when local cannot be determined.
export const resolveTimeZone = (option: TimeZone, serverTimeZone: string) =>
option === TimeZone.local && LOCAL_TIME_ZONE !== "UTC"
option === TimeZone.local && RESOLVED_TIME_ZONE
? LOCAL_TIME_ZONE
: serverTimeZone;

View File

@ -262,7 +262,6 @@ export interface IconSelector {
}
export interface ImageSelector {
// eslint-disable-next-line @typescript-eslint/ban-types
image: { original?: boolean; crop?: CropOptions } | null;
}
@ -334,7 +333,6 @@ export interface ObjectSelector {
}
export interface AssistPipelineSelector {
// eslint-disable-next-line @typescript-eslint/ban-types
assist_pipeline: {
include_last_used?: boolean;
} | null;
@ -453,7 +451,6 @@ export interface UiActionSelector {
}
export interface UiColorSelector {
// eslint-disable-next-line @typescript-eslint/ban-types
ui_color: {
default_color?: string;
include_none?: boolean;
@ -462,7 +459,6 @@ export interface UiColorSelector {
}
export interface UiStateContentSelector {
// eslint-disable-next-line @typescript-eslint/ban-types
ui_state_content: {
entity_id?: string;
allow_name?: boolean;

View File

@ -60,7 +60,7 @@ declare global {
};
}
// For loading workers in webpack
// For loading workers in rspack
interface ImportMeta {
url: string;
}

View File

@ -1,7 +1,4 @@
{
"env": {
"mocha": true
},
"rules": {
"import/no-extraneous-dependencies": 0
}

View File

@ -1,4 +1,4 @@
import { assert } from "chai";
import { assert, describe, it } from "vitest";
import { atLeastVersion } from "../../../src/common/config/version";
const testTruthyData = [

View File

@ -1,4 +1,4 @@
import { assert } from "chai";
import { assert, describe, it } from "vitest";
import checkValidDate from "../../../src/common/datetime/check_valid_date";

View File

@ -1,4 +1,4 @@
import { assert } from "chai";
import { assert, describe, it } from "vitest";
import { formatDuration } from "../../../src/common/datetime/duration";

View File

@ -1,4 +1,4 @@
import { assert } from "chai";
import { assert, describe, it } from "vitest";
import durationToSeconds from "../../../src/common/datetime/duration_to_seconds";

View File

@ -1,4 +1,4 @@
import { assert } from "chai";
import { assert, describe, it } from "vitest";
import { formatDate } from "../../../src/common/datetime/format_date";
import {

View File

@ -1,4 +1,4 @@
import { assert } from "chai";
import { assert, describe, it } from "vitest";
import {
formatDateTime,

View File

@ -1,4 +1,4 @@
import { assert } from "chai";
import { assert, describe, it } from "vitest";
import {
formatTime,

View File

@ -1,4 +1,4 @@
import { assert } from "chai";
import { assert, describe, it } from "vitest";
import millisecondsToDuration from "../../../src/common/datetime/milliseconds_to_duration";

View File

@ -1,4 +1,4 @@
import { assert } from "chai";
import { assert, describe, it } from "vitest";
import { relativeTime } from "../../../src/common/datetime/relative_time";
import {

View File

@ -0,0 +1,12 @@
import { expect, test } from "vitest";
import {
LOCAL_TIME_ZONE,
resolveTimeZone,
} from "../../../src/common/datetime/resolve-time-zone";
import { TimeZone } from "../../../src/data/translation";
test("resolveTimeZone", () => {
const serverTimeZone = "Vienna/Austria";
expect(resolveTimeZone(TimeZone.local, serverTimeZone)).toBe(LOCAL_TIME_ZONE);
expect(resolveTimeZone(TimeZone.server, serverTimeZone)).toBe(serverTimeZone);
});

View File

@ -1,4 +1,4 @@
import { assert } from "chai";
import { assert, describe, it } from "vitest";
import secondsToDuration from "../../../src/common/datetime/seconds_to_duration";

View File

@ -1,4 +1,4 @@
import { assert } from "chai";
import { assert, describe, it } from "vitest";
import { attributeClassNames } from "../../../src/common/entity/attribute_class_names";
describe("attributeClassNames", () => {

View File

@ -1,4 +1,4 @@
import { assert } from "chai";
import { assert, describe, it } from "vitest";
import { canToggleDomain } from "../../../src/common/entity/can_toggle_domain";

View File

@ -1,4 +1,4 @@
import { assert } from "chai";
import { assert, describe, it } from "vitest";
import { canToggleState } from "../../../src/common/entity/can_toggle_state";

View File

@ -1,4 +1,4 @@
import { assert } from "chai";
import { assert, describe, it } from "vitest";
import { computeDomain } from "../../../src/common/entity/compute_domain";

View File

@ -1,4 +1,4 @@
import { assert } from "chai";
import { assert, describe, it, beforeEach } from "vitest";
import { computeStateDisplay } from "../../../src/common/entity/compute_state_display";
import { UNKNOWN } from "../../../src/data/entity";
import type { FrontendLocaleData } from "../../../src/data/translation";
@ -67,7 +67,7 @@ describe("computeStateDisplay", () => {
demoConfig,
{}
),
"component.binary_sensor.state.moisture.off"
"component.binary_sensor.entity_component.moisture.state.off"
);
});
@ -94,7 +94,7 @@ describe("computeStateDisplay", () => {
demoConfig,
{}
),
"component.binary_sensor.state.invalid_device_class.off"
"component.binary_sensor.entity_component.invalid_device_class.state.off"
);
});

View File

@ -1,4 +1,4 @@
import { assert } from "chai";
import { assert, describe, it } from "vitest";
import { computeStateDomain } from "../../../src/common/entity/compute_state_domain";

View File

@ -1,4 +1,4 @@
import { assert } from "chai";
import { assert, describe, it } from "vitest";
import { generateFilter } from "../../../src/common/entity/entity_filter";

View File

@ -1,4 +1,4 @@
import { assert } from "chai";
import { assert, describe, it } from "vitest";
import { DEFAULT_VIEW_ENTITY_ID } from "../../../src/common/const";
import { extractViews } from "../../../src/common/entity/extract_views";
import { createEntities, createView } from "./test_util";

View File

@ -1,7 +1,7 @@
import { assert } from "chai";
import { assert, describe, it } from "vitest";
import type { HassEntity } from "home-assistant-js-websocket";
import { featureClassNames } from "../../../src/common/entity/feature_class_names";
import { HassEntity } from "home-assistant-js-websocket";
describe("featureClassNames", () => {
const classNames = {

View File

@ -1,4 +1,4 @@
import { assert } from "chai";
import { assert, describe, it } from "vitest";
import { getGroupEntities } from "../../../src/common/entity/get_group_entities";

View File

@ -1,4 +1,4 @@
import { assert } from "chai";
import { assert, describe, it } from "vitest";
import { getViewEntities } from "../../../src/common/entity/get_view_entities";

View File

@ -1,4 +1,4 @@
import { assert } from "chai";
import { assert, describe, it } from "vitest";
import { hasLocation } from "../../../src/common/entity/has_location";

View File

@ -1,4 +1,4 @@
import { assert } from "chai";
import { assert, describe, it } from "vitest";
import { splitByGroups } from "../../../src/common/entity/split_by_groups";

View File

@ -1,4 +1,4 @@
import { assert } from "chai";
import { assert, describe, it } from "vitest";
import { stateCardType } from "../../../src/common/entity/state_card_type";

View File

@ -1,4 +1,4 @@
import { assert } from "chai";
import { assert, describe, it } from "vitest";
import { stateMoreInfoType } from "../../../src/dialogs/more-info/state_more_info_control";

View File

@ -1,4 +1,4 @@
import { assert } from "chai";
import { assert, describe, it, beforeEach, afterEach } from "vitest";
import { useFakeTimers } from "sinon";
import { timerTimeRemaining } from "../../../src/data/timer";

View File

@ -1,5 +1,5 @@
import { assert } from "chai";
import { HassEntity } from "home-assistant-js-websocket";
import { assert, describe, it } from "vitest";
import type { HassEntity } from "home-assistant-js-websocket";
import {
formatNumber,
@ -7,7 +7,7 @@ import {
getNumberFormatOptions,
} from "../../../src/common/number/format_number";
import {
FrontendLocaleData,
type FrontendLocaleData,
NumberFormat,
TimeFormat,
FirstWeekday,
@ -157,7 +157,8 @@ describe("formatNumber", () => {
getNumberFormatOptions({
state: "3.0",
attributes: { step: 0.5 },
} as unknown as HassEntity)
} as unknown as HassEntity),
undefined
);
});

View File

@ -0,0 +1,12 @@
import { expect, test } from "vitest";
import { isDate } from "../../../src/common/string/is_date";
test("isDate", () => {
expect(isDate("ABC")).toBe(false);
expect(isDate("2021-02-03", false)).toBe(true);
expect(isDate("2021-02-03", true)).toBe(true);
expect(isDate("2021-05-25T19:23:52+00:00", true)).toBe(true);
expect(isDate("2021-05-25T19:23:52+00:00", false)).toBe(false);
});

View File

@ -1,12 +0,0 @@
import { assert } from "chai";
import { isDate } from "../../../src/common/string/is_date";
describe("isDate", () => {
assert.strictEqual(isDate("ABC"), false);
assert.strictEqual(isDate("2021-02-03", false), true);
assert.strictEqual(isDate("2021-02-03", true), true);
assert.strictEqual(isDate("2021-05-25T19:23:52+00:00", true), true);
assert.strictEqual(isDate("2021-05-25T19:23:52+00:00", false), false);
});

View File

@ -1,9 +1,9 @@
import { assert } from "chai";
import { describe, assert, it } from "vitest";
import {
fuzzyFilterSort,
fuzzySequentialMatch,
ScorableTextItem,
type ScorableTextItem,
} from "../../../src/common/string/filter/sequence-matching";
describe("fuzzySequentialMatch", () => {

View File

@ -0,0 +1,37 @@
import { assert, describe, it } from "vitest";
import { slugify } from "../../../src/common/string/slugify";
describe("slugify", () => {
it("works", () => {
// With default delimiter
assert.strictEqual(slugify("abc"), "abc");
assert.strictEqual(slugify("ABC"), "abc");
assert.strictEqual(slugify("abc DEF"), "abc_def");
assert.strictEqual(slugify("abc.DEF"), "abc_def");
assert.strictEqual(
slugify("1`-=~!@#$%^&*()_+[];',./{}:\"<>?\\| aA"),
"1_aa"
);
assert.strictEqual(slugify("abc-DEF"), "abc_def");
assert.strictEqual(slugify("abc_DEF"), "abc_def");
assert.strictEqual(slugify("1,1"), "11");
assert.strictEqual(slugify("abc å DEF"), "abc_a_def");
assert.strictEqual(slugify("abc:DEF"), "abc_def");
assert.strictEqual(slugify("abc&DEF"), "abc_def");
assert.strictEqual(slugify("abc^^DEF"), "abc_def");
assert.strictEqual(slugify("abc DEF"), "abc_def");
assert.strictEqual(slugify("_abc DEF"), "abc_def");
assert.strictEqual(slugify("abc DEF_"), "abc_def");
assert.strictEqual(slugify("abc-DEF ghi"), "abc_def_ghi");
assert.strictEqual(slugify("abc-DEF-ghi"), "abc_def_ghi");
assert.strictEqual(slugify("abc - DEF - ghi"), "abc_def_ghi");
assert.strictEqual(slugify("abc---DEF---ghi"), "abc_def_ghi");
assert.strictEqual(slugify("___abc___DEF___ghi___"), "abc_def_ghi");
assert.strictEqual(slugify("___"), "unknown");
assert.strictEqual(slugify(""), "");
// With custom delimiter
assert.strictEqual(slugify("abc def", "-"), "abc-def");
assert.strictEqual(slugify("abc-def", "-"), "abc-def");
});
});

View File

@ -1,32 +0,0 @@
import { assert } from "chai";
import { slugify } from "../../../src/common/string/slugify";
describe("slugify", () => {
// With default delimiter
assert.strictEqual(slugify("abc"), "abc");
assert.strictEqual(slugify("ABC"), "abc");
assert.strictEqual(slugify("abc DEF"), "abc_def");
assert.strictEqual(slugify("abc.DEF"), "abc_def");
assert.strictEqual(slugify("1`-=~!@#$%^&*()_+[];',./{}:\"<>?\\| aA"), "1_aa");
assert.strictEqual(slugify("abc-DEF"), "abc_def");
assert.strictEqual(slugify("abc_DEF"), "abc_def");
assert.strictEqual(slugify("1,1"), "11");
assert.strictEqual(slugify("abc å DEF"), "abc_a_def");
assert.strictEqual(slugify("abc:DEF"), "abc_def");
assert.strictEqual(slugify("abc&DEF"), "abc_def");
assert.strictEqual(slugify("abc^^DEF"), "abc_def");
assert.strictEqual(slugify("abc DEF"), "abc_def");
assert.strictEqual(slugify("_abc DEF"), "abc_def");
assert.strictEqual(slugify("abc DEF_"), "abc_def");
assert.strictEqual(slugify("abc-DEF ghi"), "abc_def_ghi");
assert.strictEqual(slugify("abc-DEF-ghi"), "abc_def_ghi");
assert.strictEqual(slugify("abc - DEF - ghi"), "abc_def_ghi");
assert.strictEqual(slugify("abc---DEF---ghi"), "abc_def_ghi");
assert.strictEqual(slugify("___abc___DEF___ghi___"), "abc_def_ghi");
assert.strictEqual(slugify("___"), "unknown");
assert.strictEqual(slugify(""), "");
// With custom delimiter
assert.strictEqual(slugify("abc def", "-"), "abc-def");
assert.strictEqual(slugify("abc-def", "-"), "abc-def");
});

View File

@ -1,4 +1,4 @@
import { assert } from "chai";
import { assert, describe, it } from "vitest";
import parseAspectRatio from "../../../src/common/util/parse-aspect-ratio";

View File

@ -1,8 +1,8 @@
import { assert } from "chai";
import { assert, describe, it, beforeEach } from "vitest";
import {
ExternalMessaging,
EMMessage,
type EMMessage,
} from "../../src/external_app/external_messaging";
// @ts-ignore
@ -46,7 +46,7 @@ describe("ExternalMessaging", () => {
const result = await sendMessageProm;
assert.deepEqual(result, {
hello: "world",
});
} as any);
});
it("Send fail results", async () => {

View File

@ -1,15 +1,15 @@
import { assert } from "chai";
import { assert, describe, it } from "vitest";
import { createHassioSession } from "../../src/data/hassio/ingress";
describe("Create hassio session", function () {
describe("Create hassio session", () => {
const hass = {
config: { version: "1.0.0" },
callApi: async function () {
return { data: { session: "fhdsu73rh3io4h8f3irhjel8ousafehf8f3yh" } };
},
callApi: async () => ({
data: { session: "fhdsu73rh3io4h8f3irhjel8ousafehf8f3yh" },
}),
};
it("Test create session without HTTPS", async function () {
it("Test create session without HTTPS", async () => {
// @ts-ignore
global.document = {};
// @ts-ignore
@ -22,7 +22,7 @@ describe("Create hassio session", function () {
"ingress_session=fhdsu73rh3io4h8f3irhjel8ousafehf8f3yh;path=/api/hassio_ingress/;SameSite=Strict"
);
});
it("Test create session with HTTPS", async function () {
it("Test create session with HTTPS", async () => {
// @ts-ignore
global.document = {};
// @ts-ignore
@ -41,10 +41,10 @@ describe("Create hassio session", function () {
// @ts-ignore
global.location = {};
});
it("Test fail to create", async function () {
it("Test fail to create", async () => {
const createSessionPromise = createHassioSession({
// @ts-ignore
callApi: async function () {
callApi: async () => {
// noop
},
}).then(

View File

@ -1,6 +1,6 @@
import { assert } from "chai";
import { assert, describe, it } from "vitest";
import { LovelaceConfig } from "../../../../src/data/lovelace/config/types";
import type { LovelaceConfig } from "../../../../src/data/lovelace/config/types";
import {
moveCardToContainer,
swapView,

View File

@ -1,15 +0,0 @@
const fs = require("fs");
const path = require("path");
process.env.TZ = "Etc/UTC";
process.env.IS_TEST = "true";
global.window = {};
global.navigator = {};
const MDI_OUTPUT_DIR = path.resolve(__dirname, "../build/mdi");
if (!fs.existsSync(MDI_OUTPUT_DIR)) {
fs.mkdirSync(MDI_OUTPUT_DIR, { recursive: true });
fs.writeFileSync(path.resolve(MDI_OUTPUT_DIR, "iconMetadata.json"), "{}");
}

8
test/setup.ts Normal file
View File

@ -0,0 +1,8 @@
import { beforeAll } from "vitest";
beforeAll(() => {
global.window = {} as any;
global.navigator = {} as any;
global.__DEMO__ = false;
});

View File

@ -1 +0,0 @@
global.__BACKWARDS_COMPAT__ = false;

View File

@ -1,7 +0,0 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"module": "commonjs",
"esModuleInterop": true
}
}

View File

@ -1,23 +1,23 @@
import { assert } from "chai";
import { assert, describe, it } from "vitest";
import {
getValueInPercentage,
normalize,
roundWithOneDecimal,
} from "../../src/util/calculate";
describe("Calculate tests", function () {
it("Test getValueInPercentage", function () {
describe("Calculate tests", () => {
it("Test getValueInPercentage", () => {
assert.strictEqual(getValueInPercentage(10, 0, 100), 10);
assert.strictEqual(getValueInPercentage(120, 0, 100), 120);
assert.strictEqual(getValueInPercentage(-10, 0, 100), -10);
assert.strictEqual(getValueInPercentage(10.33333, 0, 100), 10.33333);
});
it("Test normalize", function () {
it("Test normalize", () => {
assert.strictEqual(normalize(10, 0, 100), 10);
assert.strictEqual(normalize(1, 10, 100), 10);
assert.strictEqual(normalize(100, 0, 10), 10);
});
it("Test roundWithOneDecimal", function () {
it("Test roundWithOneDecimal", () => {
assert.strictEqual(roundWithOneDecimal(10), 10);
assert.strictEqual(roundWithOneDecimal(10.3), 10.3);
assert.strictEqual(roundWithOneDecimal(10.3333), 10.3);

View File

@ -1,4 +1,4 @@
import { assert } from "chai";
import { assert, describe, it } from "vitest";
import { brandsUrl } from "../../src/util/brands-url";
describe("Generate brands Url", () => {

View File

@ -1,15 +1,15 @@
import { assert } from "chai";
import { assert, describe, it } from "vitest";
import { documentationUrl } from "../../src/util/documentation-url";
describe("Generate documentation URL", function () {
it("Generate documentation url for stable", function () {
describe("Generate documentation URL", () => {
it("Generate documentation url for stable", () => {
assert.strictEqual(
// @ts-ignore
documentationUrl({ config: { version: "1.0.0" } }, "/blog"),
"https://www.home-assistant.io/blog"
);
});
it("Generate documentation url for rc", function () {
it("Generate documentation url for rc", () => {
assert.strictEqual(
// @ts-ignore
documentationUrl({ config: { version: "1.0.0b0" } }, "/blog"),

11
test/vitest.config.ts Normal file
View File

@ -0,0 +1,11 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
env: {
TZ: "Etc/UTC",
IS_TEST: "true",
},
setupFiles: ["./test/setup.ts"],
},
});

View File

@ -1,13 +0,0 @@
import webpack from "../build-scripts/webpack.cjs";
const config = webpack.createAppConfig({
isProdBuild: false,
latestBuild: true,
isStatsBuild: false,
isTestBuild: true,
});
// instant-mocha forces a CJS library, so cannot output ESM
config.output.module = false;
export default config;

2860
yarn.lock

File diff suppressed because it is too large Load Diff