mirror of
				https://github.com/home-assistant/frontend.git
				synced 2025-10-25 03:29:41 +00:00 
			
		
		
		
	Compare commits
	
		
			9 Commits
		
	
	
		
			power-char
			...
			conditiona
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 67166be0d7 | ||
|   | 4b386c0661 | ||
|   | 3a03abbd3a | ||
|   | 873ce2b405 | ||
|   | 89b53a76f0 | ||
|   | 15522d4926 | ||
|   | c6e6255d19 | ||
|   | 8bcd730501 | ||
|   | 689cd3d145 | 
| @@ -1,36 +1,39 @@ | ||||
| [modern] | ||||
| # Modern builds target recent browsers supporting the latest features to minimize transpilation, polyfills, etc. | ||||
| # It is served to browsers meeting the following requirements: | ||||
| # - released in the last year + current alpha/beta versions | ||||
| # - Firefox extended support release (ESR) | ||||
| # - with global utilization at or above 0.5% | ||||
| # - exclude dead browsers (no security maintenance for 2+ years) | ||||
| # - exclude KaiOS, QQ, and UC browsers due to lack of sufficient feature support data | ||||
| unreleased versions | ||||
| last 1 year | ||||
| Firefox ESR | ||||
| >= 0.5% | ||||
| not dead | ||||
| # Support for dynamic import is the main litmus test for serving modern builds. | ||||
| # Although officially a ES2020 feature, browsers implemented it early, so this | ||||
| # enables all of ES2017 and some features in ES2018. | ||||
| supports es6-module-dynamic-import | ||||
|  | ||||
| # Exclude Safari 11-12 because of a bug in tagged template literals | ||||
| # https://bugs.webkit.org/show_bug.cgi?id=190756 | ||||
| # Note: Dropping version 11 also enables several more ES2018 features | ||||
| not Safari < 13 | ||||
| not iOS < 13 | ||||
|  | ||||
| # Exclude KaiOS, QQ, and UC browsers due to lack of sufficient feature support data | ||||
| # Babel ignores these automatically, but we need here for Webpack to output ESM with dynamic imports | ||||
| not KaiOS > 0 | ||||
| not QQAndroid > 0 | ||||
| not UCAndroid > 0 | ||||
|  | ||||
| # Exclude unsupported browsers | ||||
| not dead | ||||
|  | ||||
| [legacy] | ||||
| # Legacy builds are served when modern requirements are not met and support browsers: | ||||
| # - released in the last 7 years + current alpha/beta versionss | ||||
| # - with global utilization at or above 0.05% | ||||
| # - exclude dead browsers (no security maintenance for 2+ years) | ||||
| # - exclude Opera Mini which does not support web sockets | ||||
| # - with global utilization above 0.05% | ||||
| # The lattermost query ensures that support for popular old browsers is not dropped too early | ||||
| # (e.g. IE 11, Android 4.4, or Samsung 4). | ||||
| # | ||||
| # In addition, legacy browsers must support some minimum features that cannot be polyfilled: | ||||
| # - ES5 (strict mode) | ||||
| # - web sockets to communicate with backend | ||||
| # - inline SVG used widely in buttons, widgets, etc. | ||||
| # - custom events used for most user interactions | ||||
| # - CSS flexbox used in the majority of the layout | ||||
| # Nearly all of these are redundant with the above rules. | ||||
| # As of May 2023, only web sockets must be added to the query. | ||||
| unreleased versions | ||||
| last 7 years | ||||
| >= 0.05% | ||||
| not dead | ||||
| not op_mini all | ||||
|  | ||||
| [legacy-sw] | ||||
| # Same as legacy plus supports service workers | ||||
| unreleased versions | ||||
| last 7 years | ||||
| >= 0.05% and supports serviceworkers | ||||
| not dead | ||||
| not op_mini all | ||||
| > 0.05% and supports websockets | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| FROM mcr.microsoft.com/devcontainers/python:1-3.13 | ||||
| # See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.148.1/containers/python-3/.devcontainer/base.Dockerfile | ||||
| FROM mcr.microsoft.com/vscode/devcontainers/python:0-3.11 | ||||
|  | ||||
| ENV \ | ||||
|   DEBIAN_FRONTEND=noninteractive \ | ||||
|   | ||||
| @@ -5,15 +5,10 @@ | ||||
|     "context": ".." | ||||
|   }, | ||||
|   "appPort": "8124:8123", | ||||
|   "postCreateCommand": "./.devcontainer/post_create.sh", | ||||
|   "postStartCommand": "script/bootstrap", | ||||
|   "containerEnv": { | ||||
|     "DEV_CONTAINER": "1", | ||||
|     "WORKSPACE_DIRECTORY": "${containerWorkspaceFolder}" | ||||
|   }, | ||||
|   "remoteEnv": { | ||||
|     "NODE_OPTIONS": "--max_old_space_size=8192" | ||||
|   }, | ||||
|   "customizations": { | ||||
|     "vscode": { | ||||
|       "extensions": [ | ||||
| @@ -21,8 +16,7 @@ | ||||
|         "esbenp.prettier-vscode", | ||||
|         "runem.lit-plugin", | ||||
|         "github.vscode-pull-request-github", | ||||
|         "eamodio.gitlens", | ||||
|         "yeion7.styled-global-variables-autocomplete" | ||||
|         "eamodio.gitlens" | ||||
|       ], | ||||
|       "settings": { | ||||
|         "files.eol": "\n", | ||||
|   | ||||
| @@ -1,22 +0,0 @@ | ||||
| #!/bin/bash | ||||
|  | ||||
| # This script will run after the container is created | ||||
|  | ||||
| # add github cli | ||||
| (type -p wget >/dev/null || (sudo apt update && sudo apt-get install wget -y)) \ | ||||
| 	&& sudo mkdir -p -m 755 /etc/apt/keyrings \ | ||||
|         && out=$(mktemp) && wget -nv -O$out https://cli.github.com/packages/githubcli-archive-keyring.gpg \ | ||||
|         && cat $out | sudo tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null \ | ||||
| 	&& sudo chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \ | ||||
| 	&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null | ||||
|  | ||||
| # Update package lists | ||||
| sudo apt-get update | ||||
|  | ||||
| sudo apt upgrade -y | ||||
|  | ||||
| # Install necessary packages | ||||
| sudo apt-get install -y libpcap-dev gh | ||||
|  | ||||
| # Display a message | ||||
| echo "Post-create script has been executed successfully." | ||||
							
								
								
									
										130
									
								
								.eslintrc.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										130
									
								
								.eslintrc.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,130 @@ | ||||
| { | ||||
|   "extends": [ | ||||
|     "airbnb-base", | ||||
|     "airbnb-typescript/base", | ||||
|     "plugin:@typescript-eslint/recommended", | ||||
|     "plugin:wc/recommended", | ||||
|     "plugin:lit/all", | ||||
|     "plugin:lit-a11y/recommended", | ||||
|     "prettier" | ||||
|   ], | ||||
|   "parser": "@typescript-eslint/parser", | ||||
|   "parserOptions": { | ||||
|     "ecmaVersion": 2020, | ||||
|     "ecmaFeatures": { | ||||
|       "modules": true | ||||
|     }, | ||||
|     "sourceType": "module", | ||||
|     "project": "./tsconfig.json" | ||||
|   }, | ||||
|   "settings": { | ||||
|     "import/resolver": { | ||||
|       "webpack": { | ||||
|         "config": "./webpack.config.cjs" | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   "globals": { | ||||
|     "__DEV__": false, | ||||
|     "__DEMO__": false, | ||||
|     "__BUILD__": false, | ||||
|     "__VERSION__": false, | ||||
|     "__STATIC_PATH__": false, | ||||
|     "__SUPERVISOR__": false, | ||||
|     "Polymer": true | ||||
|   }, | ||||
|   "env": { | ||||
|     "browser": true, | ||||
|     "es6": true | ||||
|   }, | ||||
|   "rules": { | ||||
|     "class-methods-use-this": "off", | ||||
|     "new-cap": "off", | ||||
|     "prefer-template": "off", | ||||
|     "object-shorthand": "off", | ||||
|     "func-names": "off", | ||||
|     "no-underscore-dangle": "off", | ||||
|     "strict": "off", | ||||
|     "no-plusplus": "off", | ||||
|     "no-bitwise": "error", | ||||
|     "comma-dangle": "off", | ||||
|     "vars-on-top": "off", | ||||
|     "no-continue": "off", | ||||
|     "no-param-reassign": "off", | ||||
|     "no-multi-assign": "off", | ||||
|     "no-console": "error", | ||||
|     "radix": "off", | ||||
|     "no-alert": "off", | ||||
|     "no-nested-ternary": "off", | ||||
|     "prefer-destructuring": "off", | ||||
|     "no-restricted-globals": [2, "event"], | ||||
|     "prefer-promise-reject-errors": "off", | ||||
|     "import/prefer-default-export": "off", | ||||
|     "import/no-default-export": "off", | ||||
|     "import/no-unresolved": "off", | ||||
|     "import/no-cycle": "off", | ||||
|     "import/extensions": [ | ||||
|       "error", | ||||
|       "ignorePackages", | ||||
|       { | ||||
|         "ts": "never", | ||||
|         "js": "never" | ||||
|       } | ||||
|     ], | ||||
|     "no-restricted-syntax": ["error", "LabeledStatement", "WithStatement"], | ||||
|     "object-curly-newline": "off", | ||||
|     "default-case": "off", | ||||
|     "wc/no-self-class": "off", | ||||
|     "no-shadow": "off", | ||||
|     "@typescript-eslint/camelcase": "off", | ||||
|     "@typescript-eslint/ban-ts-comment": "off", | ||||
|     "@typescript-eslint/no-use-before-define": "off", | ||||
|     "@typescript-eslint/no-non-null-assertion": "off", | ||||
|     "@typescript-eslint/no-explicit-any": "off", | ||||
|     "@typescript-eslint/explicit-function-return-type": "off", | ||||
|     "@typescript-eslint/explicit-module-boundary-types": "off", | ||||
|     "@typescript-eslint/no-shadow": ["error"], | ||||
|     "@typescript-eslint/naming-convention": [ | ||||
|       "off", | ||||
|       { | ||||
|         "selector": "default", | ||||
|         "format": ["camelCase", "snake_case"], | ||||
|         "leadingUnderscore": "allow", | ||||
|         "trailingUnderscore": "allow" | ||||
|       }, | ||||
|       { | ||||
|         "selector": ["variable"], | ||||
|         "format": ["camelCase", "snake_case", "UPPER_CASE"], | ||||
|         "leadingUnderscore": "allow", | ||||
|         "trailingUnderscore": "allow" | ||||
|       }, | ||||
|       { | ||||
|         "selector": "typeLike", | ||||
|         "format": ["PascalCase"] | ||||
|       } | ||||
|     ], | ||||
|     "@typescript-eslint/no-unused-vars": "off", | ||||
|     "unused-imports/no-unused-vars": [ | ||||
|       "error", | ||||
|       { | ||||
|         "vars": "all", | ||||
|         "varsIgnorePattern": "^_", | ||||
|         "args": "after-used", | ||||
|         "argsIgnorePattern": "^_", | ||||
|         "ignoreRestSiblings": true | ||||
|       } | ||||
|     ], | ||||
|     "unused-imports/no-unused-imports": "error", | ||||
|     "lit/attribute-value-entities": "off", | ||||
|     "lit/no-template-map": "off", | ||||
|     "lit/no-native-attributes": "warn", | ||||
|     "lit/no-this-assign-in-render": "warn", | ||||
|     "lit-a11y/click-events-have-key-events": ["off"], | ||||
|     "lit-a11y/no-autofocus": "off", | ||||
|     "lit-a11y/alt-text": "warn", | ||||
|     "lit-a11y/anchor-is-valid": "warn", | ||||
|     "lit-a11y/role-has-required-aria-attrs": "warn" | ||||
|   }, | ||||
|   "plugins": ["disable", "unused-imports"], | ||||
|   "processor": "disable/disable" | ||||
| } | ||||
							
								
								
									
										13
									
								
								.github/ISSUE_TEMPLATE/bug_report.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										13
									
								
								.github/ISSUE_TEMPLATE/bug_report.yml
									
									
									
									
										vendored
									
									
								
							| @@ -7,11 +7,11 @@ body: | ||||
|       value: | | ||||
|         Make sure you are running the [latest version of Home Assistant][releases] before reporting an issue. | ||||
|  | ||||
|         If you have a feature or enhancement request for the frontend, please [start a discussion][fr] instead of creating an issue. | ||||
|         If you have a feature or enhancement request for the frontend, please [start an discussion][fr] instead of creating an issue. | ||||
|  | ||||
|         **Please do not report issues for custom cards.** | ||||
|         **Please not not report issues for custom cards.** | ||||
|  | ||||
|         [fr]: https://github.com/orgs/home-assistant/discussions | ||||
|         [fr]: https://github.com/home-assistant/frontend/discussions | ||||
|         [releases]: https://github.com/home-assistant/home-assistant/releases | ||||
|   - type: checkboxes | ||||
|     attributes: | ||||
| @@ -24,7 +24,6 @@ body: | ||||
|           required: true | ||||
|         - label: I have tried a different browser to see if it is related to my browser. | ||||
|           required: true | ||||
|         - label: I have tried reproducing the issue in [safe mode](https://www.home-assistant.io/blog/2023/11/01/release-202311/#restarting-into-safe-mode) to rule out problems with unsupported custom resources. | ||||
|   - type: markdown | ||||
|     attributes: | ||||
|       value: | | ||||
| @@ -74,7 +73,7 @@ body: | ||||
|         If known, otherwise leave blank. | ||||
|   - type: input | ||||
|     attributes: | ||||
|       label: In which browser are you experiencing the issue? | ||||
|       label: In which browser are you experiencing the issue with? | ||||
|       placeholder: Google Chrome 88.0.4324.150 | ||||
|       description: > | ||||
|         Provide the full name and don't forget to add the version! | ||||
| @@ -108,9 +107,9 @@ body: | ||||
|       render: yaml | ||||
|   - type: textarea | ||||
|     attributes: | ||||
|       label: JavaScript errors shown in your browser console/inspector | ||||
|       label: Javascript errors shown in your browser console/inspector | ||||
|       description: > | ||||
|         If you come across any JavaScript or other error logs, e.g., in your | ||||
|         If you come across any Javascript or other error logs, e.g., in your | ||||
|         browser console/inspector please provide them. | ||||
|       render: txt | ||||
|   - type: textarea | ||||
|   | ||||
							
								
								
									
										4
									
								
								.github/ISSUE_TEMPLATE/config.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/ISSUE_TEMPLATE/config.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,8 +1,8 @@ | ||||
| blank_issues_enabled: false | ||||
| contact_links: | ||||
|   - name: Request a feature for the UI / Dashboards | ||||
|     url: https://github.com/orgs/home-assistant/discussions | ||||
|     about: Request a new feature for the Home Assistant frontend. | ||||
|     url: https://github.com/home-assistant/frontend/discussions/category_choices | ||||
|     about: Request an new feature for the Home Assistant frontend. | ||||
|   - name: Report a bug that is NOT related to the UI / Dashboards | ||||
|     url: https://github.com/home-assistant/core/issues | ||||
|     about: This is the issue tracker for our frontend. Please report other issues in the backend ("core") repository. | ||||
|   | ||||
							
								
								
									
										53
									
								
								.github/ISSUE_TEMPLATE/task.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										53
									
								
								.github/ISSUE_TEMPLATE/task.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,53 +0,0 @@ | ||||
| name: Task | ||||
| description: For staff only - Create a task | ||||
| type: Task | ||||
| body: | ||||
|   - type: markdown | ||||
|     attributes: | ||||
|       value: | | ||||
|         ## ⚠️ RESTRICTED ACCESS | ||||
|  | ||||
|         **This form is restricted to Open Home Foundation staff and authorized contributors only.** | ||||
|  | ||||
|         If you are a community member wanting to contribute, please: | ||||
|         - For bug reports: Use the [bug report form](https://github.com/home-assistant/frontend/issues/new?template=bug_report.yml) | ||||
|         - For feature requests: Submit to [Feature Requests](https://github.com/orgs/home-assistant/discussions) | ||||
|  | ||||
|         --- | ||||
|  | ||||
|         ### For authorized contributors | ||||
|  | ||||
|         Use this form to create tasks for development work, improvements, or other actionable items that need to be tracked. | ||||
|   - type: textarea | ||||
|     id: description | ||||
|     attributes: | ||||
|       label: Description | ||||
|       description: | | ||||
|         Provide a clear and detailed description of the task that needs to be accomplished. | ||||
|  | ||||
|         Be specific about what needs to be done, why it's important, and any constraints or requirements. | ||||
|       placeholder: | | ||||
|         Describe the task, including: | ||||
|         - What needs to be done | ||||
|         - Why this task is needed | ||||
|         - Expected outcome | ||||
|         - Any constraints or requirements | ||||
|     validations: | ||||
|       required: true | ||||
|   - type: textarea | ||||
|     id: additional_context | ||||
|     attributes: | ||||
|       label: Additional context | ||||
|       description: | | ||||
|         Any additional information, links, research, or context that would be helpful. | ||||
|  | ||||
|         Include links to related issues, research, prototypes, roadmap opportunities etc. | ||||
|       placeholder: | | ||||
|         - Roadmap opportunity: [link] | ||||
|         - Epic: [link] | ||||
|         - Feature request: [link] | ||||
|         - Technical design documents: [link] | ||||
|         - Prototype/mockup: [link] | ||||
|         - Dependencies: [links] | ||||
|     validations: | ||||
|       required: false | ||||
							
								
								
									
										596
									
								
								.github/copilot-instructions.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										596
									
								
								.github/copilot-instructions.md
									
									
									
									
										vendored
									
									
								
							| @@ -1,596 +0,0 @@ | ||||
| # GitHub Copilot & Claude Code Instructions | ||||
|  | ||||
| You are an assistant helping with development of the Home Assistant frontend. The frontend is built using Lit-based Web Components and TypeScript, providing a responsive and performant interface for home automation control. | ||||
|  | ||||
| ## Table of Contents | ||||
|  | ||||
| - [Quick Reference](#quick-reference) | ||||
| - [Core Architecture](#core-architecture) | ||||
| - [Development Standards](#development-standards) | ||||
| - [Component Library](#component-library) | ||||
| - [Common Patterns](#common-patterns) | ||||
| - [Text and Copy Guidelines](#text-and-copy-guidelines) | ||||
| - [Development Workflow](#development-workflow) | ||||
| - [Review Guidelines](#review-guidelines) | ||||
|  | ||||
| ## Quick Reference | ||||
|  | ||||
| ### Essential Commands | ||||
|  | ||||
| ```bash | ||||
| yarn lint          # ESLint + Prettier + TypeScript + Lit | ||||
| yarn format        # Auto-fix ESLint + Prettier | ||||
| yarn lint:types    # TypeScript compiler | ||||
| yarn test          # Vitest | ||||
| script/develop     # Development server | ||||
| ``` | ||||
|  | ||||
| ### Component Prefixes | ||||
|  | ||||
| - `ha-` - Home Assistant components | ||||
| - `hui-` - Lovelace UI components | ||||
| - `dialog-` - Dialog components | ||||
|  | ||||
| ### Import Patterns | ||||
|  | ||||
| ```typescript | ||||
| import type { HomeAssistant } from "../types"; | ||||
| import { fireEvent } from "../common/dom/fire_event"; | ||||
| import { showAlertDialog } from "../dialogs/generic/show-alert-dialog"; | ||||
| ``` | ||||
|  | ||||
| ## Core Architecture | ||||
|  | ||||
| The Home Assistant frontend is a modern web application that: | ||||
|  | ||||
| - Uses Web Components (custom elements) built with Lit framework | ||||
| - Is written entirely in TypeScript with strict type checking | ||||
| - Communicates with the backend via WebSocket API | ||||
| - Provides comprehensive theming and internationalization | ||||
|  | ||||
| ## Development Standards | ||||
|  | ||||
| ### Code Quality Requirements | ||||
|  | ||||
| **Linting and Formatting (Enforced by Tools)** | ||||
|  | ||||
| - ESLint config extends Airbnb, TypeScript strict, Lit, Web Components, Accessibility | ||||
| - Prettier with ES5 trailing commas enforced | ||||
| - No console statements (`no-console: "error"`) - use proper logging | ||||
| - Import organization: No unused imports, consistent type imports | ||||
|  | ||||
| **Naming Conventions** | ||||
|  | ||||
| - PascalCase for types and classes | ||||
| - camelCase for variables, methods | ||||
| - Private methods require leading underscore | ||||
| - Public methods forbid leading underscore | ||||
|  | ||||
| ### TypeScript Usage | ||||
|  | ||||
| - **Always use strict TypeScript**: Enable all strict flags, avoid `any` types | ||||
| - **Proper type imports**: Use `import type` for type-only imports | ||||
| - **Define interfaces**: Create proper interfaces for data structures | ||||
| - **Type component properties**: All Lit properties must be properly typed | ||||
| - **No unused variables**: Prefix with `_` if intentionally unused | ||||
| - **Consistent imports**: Use `@typescript-eslint/consistent-type-imports` | ||||
|  | ||||
| ```typescript | ||||
| // Good | ||||
| import type { HomeAssistant } from "../types"; | ||||
|  | ||||
| interface EntityConfig { | ||||
|   entity: string; | ||||
|   name?: string; | ||||
| } | ||||
|  | ||||
| @property({ type: Object }) | ||||
| hass!: HomeAssistant; | ||||
|  | ||||
| // Bad | ||||
| @property() | ||||
| hass: any; | ||||
| ``` | ||||
|  | ||||
| ### Web Components with Lit | ||||
|  | ||||
| - **Use Lit 3.x patterns**: Follow modern Lit practices | ||||
| - **Extend appropriate base classes**: Use `LitElement`, `SubscribeMixin`, or other mixins as needed | ||||
| - **Define custom element names**: Use `ha-` prefix for components | ||||
|  | ||||
| ```typescript | ||||
| @customElement("ha-my-component") | ||||
| export class HaMyComponent extends LitElement { | ||||
|   @property({ attribute: false }) | ||||
|   hass!: HomeAssistant; | ||||
|  | ||||
|   @state() | ||||
|   private _config?: MyComponentConfig; | ||||
|  | ||||
|   static get styles() { | ||||
|     return css` | ||||
|       :host { | ||||
|         display: block; | ||||
|       } | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   render() { | ||||
|     return html`<div>Content</div>`; | ||||
|   } | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ### Component Guidelines | ||||
|  | ||||
| - **Use composition**: Prefer composition over inheritance | ||||
| - **Lazy load panels**: Heavy panels should be dynamically imported | ||||
| - **Optimize renders**: Use `@state()` for internal state, `@property()` for public API | ||||
| - **Handle loading states**: Always show appropriate loading indicators | ||||
| - **Support themes**: Use CSS custom properties from theme | ||||
|  | ||||
| ### Data Management | ||||
|  | ||||
| - **Use WebSocket API**: All backend communication via home-assistant-js-websocket | ||||
| - **Cache appropriately**: Use collections and caching for frequently accessed data | ||||
| - **Handle errors gracefully**: All API calls should have error handling | ||||
| - **Update real-time**: Subscribe to state changes for live updates | ||||
|  | ||||
| ```typescript | ||||
| // Good | ||||
| try { | ||||
|   const result = await fetchEntityRegistry(this.hass.connection); | ||||
|   this._processResult(result); | ||||
| } catch (err) { | ||||
|   showAlertDialog(this, { | ||||
|     text: `Failed to load: ${err.message}`, | ||||
|   }); | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ### Styling Guidelines | ||||
|  | ||||
| - **Use CSS custom properties**: Leverage the theme system | ||||
| - **Mobile-first responsive**: Design for mobile, enhance for desktop | ||||
| - **Follow Material Design**: Use Material Web Components where appropriate | ||||
| - **Support RTL**: Ensure all layouts work in RTL languages | ||||
|  | ||||
| ```typescript | ||||
| static get styles() { | ||||
|   return css` | ||||
|     :host { | ||||
|       --spacing: 16px; | ||||
|       padding: var(--spacing); | ||||
|       color: var(--primary-text-color); | ||||
|       background-color: var(--card-background-color); | ||||
|     } | ||||
|  | ||||
|     @media (max-width: 600px) { | ||||
|       :host { | ||||
|         --spacing: 8px; | ||||
|       } | ||||
|     } | ||||
|   `; | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ### Performance Best Practices | ||||
|  | ||||
| - **Code split**: Split code at the panel/dialog level | ||||
| - **Lazy load**: Use dynamic imports for heavy components | ||||
| - **Optimize bundle**: Keep initial bundle size minimal | ||||
| - **Use virtual scrolling**: For long lists, implement virtual scrolling | ||||
| - **Memoize computations**: Cache expensive calculations | ||||
|  | ||||
| ### Testing Requirements | ||||
|  | ||||
| - **Write tests**: Add tests for data processing and utilities | ||||
| - **Test with Vitest**: Use the established test framework | ||||
| - **Mock appropriately**: Mock WebSocket connections and API calls | ||||
| - **Test accessibility**: Ensure components are accessible | ||||
|  | ||||
| ## Component Library | ||||
|  | ||||
| ### Dialog Components | ||||
|  | ||||
| **Available Dialog Types:** | ||||
|  | ||||
| - `ha-md-dialog` - Preferred for new code (Material Design 3) | ||||
| - `ha-dialog` - Legacy component still widely used | ||||
|  | ||||
| **Opening Dialogs (Fire Event Pattern - Recommended):** | ||||
|  | ||||
| ```typescript | ||||
| fireEvent(this, "show-dialog", { | ||||
|   dialogTag: "dialog-example", | ||||
|   dialogImport: () => import("./dialog-example"), | ||||
|   dialogParams: { title: "Example", data: someData }, | ||||
| }); | ||||
| ``` | ||||
|  | ||||
| **Dialog Implementation Requirements:** | ||||
|  | ||||
| - Implement `HassDialog<T>` interface | ||||
| - Use `createCloseHeading()` for standard headers | ||||
| - Import `haStyleDialog` for consistent styling | ||||
| - Return `nothing` when no params (loading state) | ||||
| - Fire `dialog-closed` event when closing | ||||
| - Add `dialogInitialFocus` for accessibility | ||||
|  | ||||
| ```` | ||||
|  | ||||
| ### Form Component (ha-form) | ||||
| - Schema-driven using `HaFormSchema[]` | ||||
| - Supports entity, device, area, target, number, boolean, time, action, text, object, select, icon, media, location selectors | ||||
| - Built-in validation with error display | ||||
| - Use `dialogInitialFocus` in dialogs | ||||
| - Use `computeLabel`, `computeError`, `computeHelper` for translations | ||||
|  | ||||
| ```typescript | ||||
| <ha-form | ||||
|   .hass=${this.hass} | ||||
|   .data=${this._data} | ||||
|   .schema=${this._schema} | ||||
|   .error=${this._errors} | ||||
|   .computeLabel=${(schema) => this.hass.localize(`ui.panel.${schema.name}`)} | ||||
|   @value-changed=${this._valueChanged} | ||||
| ></ha-form> | ||||
| ```` | ||||
|  | ||||
| ### Alert Component (ha-alert) | ||||
|  | ||||
| - Types: `error`, `warning`, `info`, `success` | ||||
| - Properties: `title`, `alert-type`, `dismissable`, `icon`, `action`, `rtl` | ||||
| - Content announced by screen readers when dynamically displayed | ||||
|  | ||||
| ```html | ||||
| <ha-alert alert-type="error">Error message</ha-alert> | ||||
| <ha-alert alert-type="warning" title="Warning">Description</ha-alert> | ||||
| <ha-alert alert-type="success" dismissable>Success message</ha-alert> | ||||
| ``` | ||||
|  | ||||
| ## Common Patterns | ||||
|  | ||||
| ### Creating a Panel | ||||
|  | ||||
| ```typescript | ||||
| @customElement("ha-panel-myfeature") | ||||
| export class HaPanelMyFeature extends SubscribeMixin(LitElement) { | ||||
|   @property({ attribute: false }) | ||||
|   hass!: HomeAssistant; | ||||
|  | ||||
|   @property({ type: Boolean, reflect: true }) | ||||
|   narrow!: boolean; | ||||
|  | ||||
|   @property() | ||||
|   route!: Route; | ||||
|  | ||||
|   hassSubscribe() { | ||||
|     return [ | ||||
|       subscribeEntityRegistry(this.hass.connection, (entities) => { | ||||
|         this._entities = entities; | ||||
|       }), | ||||
|     ]; | ||||
|   } | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ### Creating a Dialog | ||||
|  | ||||
| ```typescript | ||||
| @customElement("dialog-my-feature") | ||||
| export class DialogMyFeature | ||||
|   extends LitElement | ||||
|   implements HassDialog<MyDialogParams> | ||||
| { | ||||
|   @property({ attribute: false }) | ||||
|   hass!: HomeAssistant; | ||||
|  | ||||
|   @state() | ||||
|   private _params?: MyDialogParams; | ||||
|  | ||||
|   public async showDialog(params: MyDialogParams): Promise<void> { | ||||
|     this._params = params; | ||||
|   } | ||||
|  | ||||
|   public closeDialog(): void { | ||||
|     this._params = undefined; | ||||
|     fireEvent(this, "dialog-closed", { dialog: this.localName }); | ||||
|   } | ||||
|  | ||||
|   protected render() { | ||||
|     if (!this._params) { | ||||
|       return nothing; | ||||
|     } | ||||
|  | ||||
|     return html` | ||||
|       <ha-dialog | ||||
|         open | ||||
|         @closed=${this.closeDialog} | ||||
|         .heading=${createCloseHeading(this.hass, this._params.title)} | ||||
|       > | ||||
|         <!-- Dialog content --> | ||||
|         <ha-button | ||||
|           appearance="plain" | ||||
|           @click=${this.closeDialog} | ||||
|           slot="secondaryAction" | ||||
|         > | ||||
|           ${this.hass.localize("ui.common.cancel")} | ||||
|         </ha-button> | ||||
|         <ha-button @click=${this._submit} slot="primaryAction"> | ||||
|           ${this.hass.localize("ui.common.save")} | ||||
|         </ha-button> | ||||
|       </ha-dialog> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   static styles = [haStyleDialog, css``]; | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ### Dialog Design Guidelines | ||||
|  | ||||
| - Max width: 560px (Alert/confirmation: 320px fixed width) | ||||
| - Close X-icon on top left (all screen sizes) | ||||
| - Submit button grouped with cancel at bottom right | ||||
| - Keep button labels short: "Save", "Delete", "Enable" | ||||
| - Destructive actions use red warning button | ||||
| - Always use a title (best practice) | ||||
| - Strive for minimalism | ||||
|  | ||||
| #### Creating a Lovelace Card | ||||
|  | ||||
| **Purpose**: Cards allow users to tell different stories about their house (based on gallery) | ||||
|  | ||||
| ```typescript | ||||
| @customElement("hui-my-card") | ||||
| export class HuiMyCard extends LitElement implements LovelaceCard { | ||||
|   @property({ attribute: false }) | ||||
|   hass!: HomeAssistant; | ||||
|  | ||||
|   @state() | ||||
|   private _config?: MyCardConfig; | ||||
|  | ||||
|   public setConfig(config: MyCardConfig): void { | ||||
|     if (!config.entity) { | ||||
|       throw new Error("Entity required"); | ||||
|     } | ||||
|     this._config = config; | ||||
|   } | ||||
|  | ||||
|   public getCardSize(): number { | ||||
|     return 3; // Height in grid units | ||||
|   } | ||||
|  | ||||
|   // Optional: Editor for card configuration | ||||
|   public static getConfigElement(): LovelaceCardEditor { | ||||
|     return document.createElement("hui-my-card-editor"); | ||||
|   } | ||||
|  | ||||
|   // Optional: Stub config for card picker | ||||
|   public static getStubConfig(): object { | ||||
|     return { entity: "" }; | ||||
|   } | ||||
| } | ||||
| ``` | ||||
|  | ||||
| **Card Guidelines:** | ||||
|  | ||||
| - Cards are highly customizable for different households | ||||
| - Implement `LovelaceCard` interface with `setConfig()` and `getCardSize()` | ||||
| - Use proper error handling in `setConfig()` | ||||
| - Consider all possible states (loading, error, unavailable) | ||||
| - Support different entity types and states | ||||
| - Follow responsive design principles | ||||
| - Add configuration editor when needed | ||||
|  | ||||
| ### Internationalization | ||||
|  | ||||
| - **Use localize**: Always use the localization system | ||||
| - **Add translation keys**: Add keys to src/translations/en.json | ||||
| - **Support placeholders**: Use proper placeholder syntax | ||||
|  | ||||
| ```typescript | ||||
| this.hass.localize("ui.panel.config.updates.update_available", { | ||||
|   count: 5, | ||||
| }); | ||||
| ``` | ||||
|  | ||||
| ### Accessibility | ||||
|  | ||||
| - **ARIA labels**: Add appropriate ARIA labels | ||||
| - **Keyboard navigation**: Ensure all interactions work with keyboard | ||||
| - **Screen reader support**: Test with screen readers | ||||
| - **Color contrast**: Meet WCAG AA standards | ||||
|  | ||||
| ## Development Workflow | ||||
|  | ||||
| ### Setup and Commands | ||||
|  | ||||
| 1. **Setup**: `script/setup` - Install dependencies | ||||
| 2. **Develop**: `script/develop` - Development server | ||||
| 3. **Lint**: `yarn lint` - Run all linting before committing | ||||
| 4. **Test**: `yarn test` - Add and run tests | ||||
| 5. **Build**: `script/build_frontend` - Test production build | ||||
|  | ||||
| ### Common Pitfalls to Avoid | ||||
|  | ||||
| - Don't use `querySelector` - Use refs or component properties | ||||
| - Don't manipulate DOM directly - Let Lit handle rendering | ||||
| - Don't use global styles - Scope styles to components | ||||
| - Don't block the main thread - Use web workers for heavy computation | ||||
| - Don't ignore TypeScript errors - Fix all type issues | ||||
|  | ||||
| ### Security Best Practices | ||||
|  | ||||
| - Sanitize HTML - Never use `unsafeHTML` with user content | ||||
| - Validate inputs - Always validate user inputs | ||||
| - Use HTTPS - All external resources must use HTTPS | ||||
| - CSP compliance - Ensure code works with Content Security Policy | ||||
|  | ||||
| ### Text and Copy Guidelines | ||||
|  | ||||
| #### Terminology Standards | ||||
|  | ||||
| **Delete vs Remove** (Based on gallery/src/pages/Text/remove-delete-add-create.markdown) | ||||
|  | ||||
| - **Use "Remove"** for actions that can be restored or reapplied: | ||||
|   - Removing a user's permission | ||||
|   - Removing a user from a group | ||||
|   - Removing links between items | ||||
|   - Removing a widget from dashboard | ||||
|   - Removing an item from a cart | ||||
| - **Use "Delete"** for permanent, non-recoverable actions: | ||||
|   - Deleting a field | ||||
|   - Deleting a value in a field | ||||
|   - Deleting a task | ||||
|   - Deleting a group | ||||
|   - Deleting a permission | ||||
|   - Deleting a calendar event | ||||
|  | ||||
| **Create vs Add** (Create pairs with Delete, Add pairs with Remove) | ||||
|  | ||||
| - **Use "Add"** for already-existing items: | ||||
|   - Adding a permission to a user | ||||
|   - Adding a user to a group | ||||
|   - Adding links between items | ||||
|   - Adding a widget to dashboard | ||||
|   - Adding an item to a cart | ||||
| - **Use "Create"** for something made from scratch: | ||||
|   - Creating a new field | ||||
|   - Creating a new task | ||||
|   - Creating a new group | ||||
|   - Creating a new permission | ||||
|   - Creating a new calendar event | ||||
|  | ||||
| #### Writing Style (Consistent with Home Assistant Documentation) | ||||
|  | ||||
| - **Use American English**: Standard spelling and terminology | ||||
| - **Friendly, informational tone**: Be inspiring, personal, comforting, engaging | ||||
| - **Address users directly**: Use "you" and "your" | ||||
| - **Be inclusive**: Objective, non-discriminatory language | ||||
| - **Be concise**: Use clear, direct language | ||||
| - **Be consistent**: Follow established terminology patterns | ||||
| - **Use active voice**: "Delete the automation" not "The automation should be deleted" | ||||
| - **Avoid jargon**: Use terms familiar to home automation users | ||||
|  | ||||
| #### Language Standards | ||||
|  | ||||
| - **Always use "Home Assistant"** in full, never "HA" or "HASS" | ||||
| - **Avoid abbreviations**: Spell out terms when possible | ||||
| - **Use sentence case everywhere**: Titles, headings, buttons, labels, UI elements | ||||
|   - ✅ "Create new automation" | ||||
|   - ❌ "Create New Automation" | ||||
|   - ✅ "Device settings" | ||||
|   - ❌ "Device Settings" | ||||
| - **Oxford comma**: Use in lists (item 1, item 2, and item 3) | ||||
| - **Replace Latin terms**: Use "like" instead of "e.g.", "for example" instead of "i.e." | ||||
| - **Avoid CAPS for emphasis**: Use bold or italics instead | ||||
| - **Write for all skill levels**: Both technical and non-technical users | ||||
|  | ||||
| #### Key Terminology | ||||
|  | ||||
| - **"add-on"** (hyphenated, not "addon") | ||||
| - **"integration"** (preferred over "component") | ||||
| - **Technical terms**: Use lowercase (automation, entity, device, service) | ||||
|  | ||||
| #### Translation Considerations | ||||
|  | ||||
| - **Add translation keys**: All user-facing text must be translatable | ||||
| - **Use placeholders**: Support dynamic content in translations | ||||
| - **Keep context**: Provide enough context for translators | ||||
|  | ||||
| ```typescript | ||||
| // Good | ||||
| this.hass.localize("ui.panel.config.automation.delete_confirm", { | ||||
|   name: automation.alias, | ||||
| }); | ||||
|  | ||||
| // Bad - hardcoded text | ||||
| ("Are you sure you want to delete this automation?"); | ||||
| ``` | ||||
|  | ||||
| ### Common Review Issues (From PR Analysis) | ||||
|  | ||||
| #### User Experience and Accessibility | ||||
|  | ||||
| - **Form validation**: Always provide proper field labels and validation feedback | ||||
| - **Form accessibility**: Prevent password managers from incorrectly identifying fields | ||||
| - **Loading states**: Show clear progress indicators during async operations | ||||
| - **Error handling**: Display meaningful error messages when operations fail | ||||
| - **Mobile responsiveness**: Ensure components work well on small screens | ||||
| - **Hit targets**: Make clickable areas large enough for touch interaction | ||||
| - **Visual feedback**: Provide clear indication of interactive states | ||||
|  | ||||
| #### Dialog and Modal Patterns | ||||
|  | ||||
| - **Dialog width constraints**: Respect minimum and maximum width requirements | ||||
| - **Interview progress**: Show clear progress for multi-step operations | ||||
| - **State persistence**: Handle dialog state properly during background operations | ||||
| - **Cancel behavior**: Ensure cancel/close buttons work consistently | ||||
| - **Form prefilling**: Use smart defaults but allow user override | ||||
|  | ||||
| #### Component Design Patterns | ||||
|  | ||||
| - **Terminology consistency**: Use "Join"/"Apply" instead of "Group" when appropriate | ||||
| - **Visual hierarchy**: Ensure proper font sizes and spacing ratios | ||||
| - **Grid alignment**: Components should align to the design grid system | ||||
| - **Badge placement**: Position badges and indicators consistently | ||||
| - **Color theming**: Respect theme variables and design system colors | ||||
|  | ||||
| #### Code Quality Issues | ||||
|  | ||||
| - **Null checking**: Always check if entities exist before accessing properties | ||||
| - **TypeScript safety**: Handle potentially undefined array/object access | ||||
| - **Import organization**: Remove unused imports and use proper type imports | ||||
| - **Event handling**: Properly subscribe and unsubscribe from events | ||||
| - **Memory leaks**: Clean up subscriptions and event listeners | ||||
|  | ||||
| #### Configuration and Props | ||||
|  | ||||
| - **Optional parameters**: Make configuration fields optional when sensible | ||||
| - **Smart defaults**: Provide reasonable default values | ||||
| - **Future extensibility**: Design APIs that can be extended later | ||||
| - **Validation**: Validate configuration before applying changes | ||||
|  | ||||
| ## Review Guidelines | ||||
|  | ||||
| ### Core Requirements Checklist | ||||
|  | ||||
| - [ ] TypeScript strict mode passes (`yarn lint:types`) | ||||
| - [ ] No ESLint errors or warnings (`yarn lint:eslint`) | ||||
| - [ ] Prettier formatting applied (`yarn lint:prettier`) | ||||
| - [ ] Lit analyzer passes (`yarn lint:lit`) | ||||
| - [ ] Component follows Lit best practices | ||||
| - [ ] Proper error handling implemented | ||||
| - [ ] Loading states handled | ||||
| - [ ] Mobile responsive | ||||
| - [ ] Theme variables used | ||||
| - [ ] Translations added | ||||
| - [ ] Accessible to screen readers | ||||
| - [ ] Tests added (where applicable) | ||||
| - [ ] No console statements (use proper logging) | ||||
| - [ ] Unused imports removed | ||||
| - [ ] Proper naming conventions | ||||
|  | ||||
| ### Text and Copy Checklist | ||||
|  | ||||
| - [ ] Follows terminology guidelines (Delete vs Remove, Create vs Add) | ||||
| - [ ] Localization keys added for all user-facing text | ||||
| - [ ] Uses "Home Assistant" (never "HA" or "HASS") | ||||
| - [ ] Sentence case for ALL text (titles, headings, buttons, labels) | ||||
| - [ ] American English spelling | ||||
| - [ ] Friendly, informational tone | ||||
| - [ ] Avoids abbreviations and jargon | ||||
| - [ ] Correct terminology (add-on not addon, integration not component) | ||||
|  | ||||
| ### Component-Specific Checks | ||||
|  | ||||
| - [ ] Dialogs implement HassDialog interface | ||||
| - [ ] Dialog styling uses haStyleDialog | ||||
| - [ ] Dialog accessibility includes dialogInitialFocus | ||||
| - [ ] ha-alert used correctly for messages | ||||
| - [ ] ha-form uses proper schema structure | ||||
| - [ ] Components handle all states (loading, error, unavailable) | ||||
| - [ ] Entity existence checked before property access | ||||
| - [ ] Event subscriptions properly cleaned up | ||||
							
								
								
									
										56
									
								
								.github/labeler.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										56
									
								
								.github/labeler.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,51 +1,31 @@ | ||||
| Build: | ||||
|   - changed-files: | ||||
|       - any-glob-to-any-file: | ||||
|           - build-scripts/** | ||||
|           - .browserslistrc | ||||
|           - gulpfile.js | ||||
|   - build-scripts/** | ||||
|   - .browserslistrc | ||||
|   - gulpfile.js | ||||
|  | ||||
| Cast: | ||||
|   - changed-files: | ||||
|       - any-glob-to-any-file: | ||||
|           - cast/src/** | ||||
|           - src/cast/** | ||||
|   - cast/src/** | ||||
|   - src/cast/** | ||||
|  | ||||
| Demo: | ||||
|   - changed-files: | ||||
|       - any-glob-to-any-file: | ||||
|           - demo/src/** | ||||
|           - src/fake_data/** | ||||
|   - demo/src/** | ||||
|   - src/fake_data/** | ||||
|  | ||||
| Design: | ||||
|   - changed-files: | ||||
|       - any-glob-to-any-file: | ||||
|           - gallery/src/** | ||||
|           - src/fake_data/** | ||||
|   - gallery/src/** | ||||
|   - src/fake_data/** | ||||
|  | ||||
| Dependencies: | ||||
|   - any: | ||||
|       - changed-files: | ||||
|           # Match when only these files are changed (i.e. don't match PRs that happen to add or remove packages) | ||||
|           - any-glob-to-all-files: | ||||
|               - package.json | ||||
|               - renovate.json | ||||
|               - yarn.lock | ||||
|               - .yarn/** | ||||
|               - .yarnrc.yml | ||||
|               - .nvmrc | ||||
|       # Dependabot and Renovate branches always match (i.e. compatibility tweaks by members considered minor) | ||||
|       - head-branch: | ||||
|           - "^renovate/" | ||||
|           - "^dependabot/" | ||||
|   - package.json | ||||
|   - renovate.json | ||||
|   - yarn.lock | ||||
|   - .yarn/** | ||||
|   - .yarnrc.yml | ||||
|   - .nvmrc | ||||
|  | ||||
| GitHub Actions: | ||||
|   - changed-files: | ||||
|       - any-glob-to-any-file: | ||||
|           - .github/workflows/** | ||||
|           - .github/*.yml | ||||
|   - .github/workflows/** | ||||
|   - .github/*.yml | ||||
|  | ||||
| Supervisor: | ||||
|   - changed-files: | ||||
|       - any-glob-to-any-file: | ||||
|           - hassio/src/** | ||||
|   - hassio/src/** | ||||
|   | ||||
							
								
								
									
										18
									
								
								.github/workflows/cast_deployment.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										18
									
								
								.github/workflows/cast_deployment.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -21,12 +21,12 @@ jobs: | ||||
|       url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} | ||||
|     steps: | ||||
|       - name: Check out files from GitHub | ||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | ||||
|         uses: actions/checkout@v4.1.0 | ||||
|         with: | ||||
|           ref: dev | ||||
|  | ||||
|       - name: Setup Node | ||||
|         uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 | ||||
|         uses: actions/setup-node@v3.8.1 | ||||
|         with: | ||||
|           node-version-file: ".nvmrc" | ||||
|           cache: yarn | ||||
| @@ -41,8 +41,9 @@ jobs: | ||||
|  | ||||
|       - name: Deploy to Netlify | ||||
|         id: deploy | ||||
|         run: | | ||||
|           npx -y netlify-cli@23.7.3 deploy --dir=cast/dist --alias dev | ||||
|         uses: netlify/actions/cli@master | ||||
|         with: | ||||
|           args: deploy --dir=cast/dist --alias dev | ||||
|         env: | ||||
|           NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} | ||||
|           NETLIFY_SITE_ID: ${{ secrets.NETLIFY_CAST_SITE_ID }} | ||||
| @@ -56,12 +57,12 @@ jobs: | ||||
|       url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} | ||||
|     steps: | ||||
|       - name: Check out files from GitHub | ||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | ||||
|         uses: actions/checkout@v4.1.0 | ||||
|         with: | ||||
|           ref: master | ||||
|  | ||||
|       - name: Setup Node | ||||
|         uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 | ||||
|         uses: actions/setup-node@v3.8.1 | ||||
|         with: | ||||
|           node-version-file: ".nvmrc" | ||||
|           cache: yarn | ||||
| @@ -76,8 +77,9 @@ jobs: | ||||
|  | ||||
|       - name: Deploy to Netlify | ||||
|         id: deploy | ||||
|         run: | | ||||
|           npx -y netlify-cli@23.7.3 deploy --dir=cast/dist --prod | ||||
|         uses: netlify/actions/cli@master | ||||
|         with: | ||||
|           args: deploy --dir=cast/dist --prod | ||||
|         env: | ||||
|           NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} | ||||
|           NETLIFY_SITE_ID: ${{ secrets.NETLIFY_CAST_SITE_ID }} | ||||
|   | ||||
							
								
								
									
										35
									
								
								.github/workflows/ci.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										35
									
								
								.github/workflows/ci.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -24,9 +24,9 @@ jobs: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Check out files from GitHub | ||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | ||||
|         uses: actions/checkout@v4.1.0 | ||||
|       - name: Setup Node | ||||
|         uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 | ||||
|         uses: actions/setup-node@v3.8.1 | ||||
|         with: | ||||
|           node-version-file: ".nvmrc" | ||||
|           cache: yarn | ||||
| @@ -37,20 +37,17 @@ jobs: | ||||
|       - name: Build resources | ||||
|         run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages | ||||
|       - name: Setup lint cache | ||||
|         uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 | ||||
|         uses: actions/cache@v3.3.2 | ||||
|         with: | ||||
|           path: | | ||||
|             node_modules/.cache/prettier | ||||
|             node_modules/.cache/eslint | ||||
|             node_modules/.cache/typescript | ||||
|           key: lint-${{ github.sha }} | ||||
|           restore-keys: lint- | ||||
|       - name: Run eslint | ||||
|         run: yarn run lint:eslint --quiet | ||||
|       - name: Run tsc | ||||
|         run: yarn run lint:types | ||||
|       - name: Run lit-analyzer | ||||
|         run: yarn run lint:lit --quiet | ||||
|       - name: Run prettier | ||||
|         run: yarn run lint:prettier | ||||
|   test: | ||||
| @@ -58,16 +55,16 @@ jobs: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Check out files from GitHub | ||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | ||||
|         uses: actions/checkout@v4.1.0 | ||||
|       - name: Setup Node | ||||
|         uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 | ||||
|         uses: actions/setup-node@v3.8.1 | ||||
|         with: | ||||
|           node-version-file: ".nvmrc" | ||||
|           cache: yarn | ||||
|       - name: Install dependencies | ||||
|         run: yarn install --immutable | ||||
|       - name: Build resources | ||||
|         run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data | ||||
|         run: ./node_modules/.bin/gulp build-translations build-locale-data | ||||
|       - name: Run Tests | ||||
|         run: yarn run test | ||||
|   build: | ||||
| @@ -76,9 +73,9 @@ jobs: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Check out files from GitHub | ||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | ||||
|         uses: actions/checkout@v4.1.0 | ||||
|       - name: Setup Node | ||||
|         uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 | ||||
|         uses: actions/setup-node@v3.8.1 | ||||
|         with: | ||||
|           node-version-file: ".nvmrc" | ||||
|           cache: yarn | ||||
| @@ -88,21 +85,15 @@ jobs: | ||||
|         run: ./node_modules/.bin/gulp build-app | ||||
|         env: | ||||
|           IS_TEST: "true" | ||||
|       - name: Upload bundle stats | ||||
|         uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 | ||||
|         with: | ||||
|           name: frontend-bundle-stats | ||||
|           path: build/stats/*.json | ||||
|           if-no-files-found: error | ||||
|   supervisor: | ||||
|     name: Build supervisor | ||||
|     needs: [lint, test] | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Check out files from GitHub | ||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | ||||
|         uses: actions/checkout@v4.1.0 | ||||
|       - name: Setup Node | ||||
|         uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 | ||||
|         uses: actions/setup-node@v3.8.1 | ||||
|         with: | ||||
|           node-version-file: ".nvmrc" | ||||
|           cache: yarn | ||||
| @@ -112,9 +103,3 @@ jobs: | ||||
|         run: ./node_modules/.bin/gulp build-hassio | ||||
|         env: | ||||
|           IS_TEST: "true" | ||||
|       - name: Upload bundle stats | ||||
|         uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 | ||||
|         with: | ||||
|           name: supervisor-bundle-stats | ||||
|           path: build/stats/*.json | ||||
|           if-no-files-found: error | ||||
|   | ||||
							
								
								
									
										8
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
								
							| @@ -23,7 +23,7 @@ jobs: | ||||
|  | ||||
|     steps: | ||||
|       - name: Checkout repository | ||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | ||||
|         uses: actions/checkout@v4.1.0 | ||||
|         with: | ||||
|           # We must fetch at least the immediate parents so that if this is | ||||
|           # a pull request then we can checkout the head. | ||||
| @@ -36,14 +36,14 @@ jobs: | ||||
|  | ||||
|       # Initializes the CodeQL tools for scanning. | ||||
|       - name: Initialize CodeQL | ||||
|         uses: github/codeql-action/init@f443b600d91635bebf5b0d9ebc620189c0d6fba5 # v4.30.8 | ||||
|         uses: github/codeql-action/init@v2 | ||||
|         with: | ||||
|           languages: ${{ matrix.language }} | ||||
|  | ||||
|       # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java). | ||||
|       # If this step fails, then you should remove it and run the build manually (see below) | ||||
|       - name: Autobuild | ||||
|         uses: github/codeql-action/autobuild@f443b600d91635bebf5b0d9ebc620189c0d6fba5 # v4.30.8 | ||||
|         uses: github/codeql-action/autobuild@v2 | ||||
|  | ||||
|       # ℹ️ Command-line programs to run using the OS shell. | ||||
|       # 📚 https://git.io/JvXDl | ||||
| @@ -57,4 +57,4 @@ jobs: | ||||
|       #   make release | ||||
|  | ||||
|       - name: Perform CodeQL Analysis | ||||
|         uses: github/codeql-action/analyze@f443b600d91635bebf5b0d9ebc620189c0d6fba5 # v4.30.8 | ||||
|         uses: github/codeql-action/analyze@v2 | ||||
|   | ||||
							
								
								
									
										18
									
								
								.github/workflows/demo_deployment.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										18
									
								
								.github/workflows/demo_deployment.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -22,12 +22,12 @@ jobs: | ||||
|       url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} | ||||
|     steps: | ||||
|       - name: Check out files from GitHub | ||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | ||||
|         uses: actions/checkout@v4.1.0 | ||||
|         with: | ||||
|           ref: dev | ||||
|  | ||||
|       - name: Setup Node | ||||
|         uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 | ||||
|         uses: actions/setup-node@v3.8.1 | ||||
|         with: | ||||
|           node-version-file: ".nvmrc" | ||||
|           cache: yarn | ||||
| @@ -42,8 +42,9 @@ jobs: | ||||
|  | ||||
|       - name: Deploy to Netlify | ||||
|         id: deploy | ||||
|         run: | | ||||
|           npx -y netlify-cli@23.7.3 deploy --dir=demo/dist --prod | ||||
|         uses: netlify/actions/cli@master | ||||
|         with: | ||||
|           args: deploy --dir=demo/dist --prod | ||||
|         env: | ||||
|           NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} | ||||
|           NETLIFY_SITE_ID: ${{ secrets.NETLIFY_DEMO_DEV_SITE_ID }} | ||||
| @@ -57,12 +58,12 @@ jobs: | ||||
|       url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} | ||||
|     steps: | ||||
|       - name: Check out files from GitHub | ||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | ||||
|         uses: actions/checkout@v4.1.0 | ||||
|         with: | ||||
|           ref: master | ||||
|  | ||||
|       - name: Setup Node | ||||
|         uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 | ||||
|         uses: actions/setup-node@v3.8.1 | ||||
|         with: | ||||
|           node-version-file: ".nvmrc" | ||||
|           cache: yarn | ||||
| @@ -77,8 +78,9 @@ jobs: | ||||
|  | ||||
|       - name: Deploy to Netlify | ||||
|         id: deploy | ||||
|         run: | | ||||
|           npx -y netlify-cli@23.7.3 deploy --dir=demo/dist --prod | ||||
|         uses: netlify/actions/cli@master | ||||
|         with: | ||||
|           args: deploy --dir=demo/dist --prod | ||||
|         env: | ||||
|           NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} | ||||
|           NETLIFY_SITE_ID: ${{ secrets.NETLIFY_DEMO_SITE_ID }} | ||||
|   | ||||
							
								
								
									
										9
									
								
								.github/workflows/design_deployment.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										9
									
								
								.github/workflows/design_deployment.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -16,10 +16,10 @@ jobs: | ||||
|       url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} | ||||
|     steps: | ||||
|       - name: Check out files from GitHub | ||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | ||||
|         uses: actions/checkout@v4.1.0 | ||||
|  | ||||
|       - name: Setup Node | ||||
|         uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 | ||||
|         uses: actions/setup-node@v3.8.1 | ||||
|         with: | ||||
|           node-version-file: ".nvmrc" | ||||
|           cache: yarn | ||||
| @@ -34,8 +34,9 @@ jobs: | ||||
|  | ||||
|       - name: Deploy to Netlify | ||||
|         id: deploy | ||||
|         run: | | ||||
|           npx -y netlify-cli@23.7.3 deploy --dir=gallery/dist --prod | ||||
|         uses: netlify/actions/cli@master | ||||
|         with: | ||||
|           args: deploy --dir=gallery/dist --prod | ||||
|         env: | ||||
|           NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} | ||||
|           NETLIFY_SITE_ID: ${{ secrets.NETLIFY_GALLERY_SITE_ID }} | ||||
|   | ||||
							
								
								
									
										13
									
								
								.github/workflows/design_preview.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										13
									
								
								.github/workflows/design_preview.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -21,10 +21,10 @@ jobs: | ||||
|     if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview') | ||||
|     steps: | ||||
|       - name: Check out files from GitHub | ||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | ||||
|         uses: actions/checkout@v4.1.0 | ||||
|  | ||||
|       - name: Setup Node | ||||
|         uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 | ||||
|         uses: actions/setup-node@v3.8.1 | ||||
|         with: | ||||
|           node-version-file: ".nvmrc" | ||||
|           cache: yarn | ||||
| @@ -39,14 +39,13 @@ jobs: | ||||
|  | ||||
|       - name: Deploy preview to Netlify | ||||
|         id: deploy | ||||
|         run: | | ||||
|           npx -y netlify-cli@23.7.3 deploy --dir=gallery/dist --alias "deploy-preview-${{ github.event.number }}" \ | ||||
|             --json > deploy_output.json | ||||
|         uses: netlify/actions/cli@master | ||||
|         with: | ||||
|           args: deploy --dir=gallery/dist --alias "deploy-preview-${{ github.event.number }}" | ||||
|         env: | ||||
|           NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} | ||||
|           NETLIFY_SITE_ID: ${{ secrets.NETLIFY_GALLERY_SITE_ID }} | ||||
|  | ||||
|       - name: Generate summary | ||||
|         run: | | ||||
|           NETLIFY_LIVE_URL=$(jq -r '.deploy_url' deploy_output.json) | ||||
|           echo "$NETLIFY_LIVE_URL" >> "$GITHUB_STEP_SUMMARY" | ||||
|           echo "${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}" >> "$GITHUB_STEP_SUMMARY" | ||||
|   | ||||
							
								
								
									
										2
									
								
								.github/workflows/labeler.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/labeler.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -10,6 +10,6 @@ jobs: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Apply labels | ||||
|         uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1 | ||||
|         uses: actions/labeler@v4.3.0 | ||||
|         with: | ||||
|           sync-labels: true | ||||
|   | ||||
							
								
								
									
										3
									
								
								.github/workflows/lock.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.github/workflows/lock.yml
									
									
									
									
										vendored
									
									
								
							| @@ -9,10 +9,9 @@ jobs: | ||||
|   lock: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5.0.1 | ||||
|       - uses: dessant/lock-threads@v4.0.1 | ||||
|         with: | ||||
|           github-token: ${{ github.token }} | ||||
|           process-only: "issues, prs" | ||||
|           issue-lock-inactive-days: "30" | ||||
|           issue-exclude-created-before: "2020-10-01T00:00:00Z" | ||||
|           issue-lock-reason: "" | ||||
|   | ||||
							
								
								
									
										14
									
								
								.github/workflows/nightly.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										14
									
								
								.github/workflows/nightly.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -6,7 +6,7 @@ on: | ||||
|     - cron: "0 1 * * *" | ||||
|  | ||||
| env: | ||||
|   PYTHON_VERSION: "3.13" | ||||
|   PYTHON_VERSION: "3.11" | ||||
|   NODE_OPTIONS: --max_old_space_size=6144 | ||||
|  | ||||
| permissions: | ||||
| @@ -20,15 +20,15 @@ jobs: | ||||
|       contents: write | ||||
|     steps: | ||||
|       - name: Checkout the repository | ||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | ||||
|         uses: actions/checkout@v4.1.0 | ||||
|  | ||||
|       - name: Set up Python ${{ env.PYTHON_VERSION }} | ||||
|         uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6 | ||||
|         uses: actions/setup-python@v4 | ||||
|         with: | ||||
|           python-version: ${{ env.PYTHON_VERSION }} | ||||
|  | ||||
|       - name: Setup Node | ||||
|         uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 | ||||
|         uses: actions/setup-node@v3.8.1 | ||||
|         with: | ||||
|           node-version-file: ".nvmrc" | ||||
|           cache: yarn | ||||
| @@ -42,7 +42,7 @@ jobs: | ||||
|           LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }} | ||||
|  | ||||
|       - name: Bump version | ||||
|         run: script/version_bump.js nightly | ||||
|         run: script/version_bump.cjs nightly | ||||
|  | ||||
|       - name: Build nightly Python wheels | ||||
|         run: | | ||||
| @@ -57,14 +57,14 @@ jobs: | ||||
|         run: tar -czvf translations.tar.gz translations | ||||
|  | ||||
|       - name: Upload build artifacts | ||||
|         uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 | ||||
|         uses: actions/upload-artifact@v3 | ||||
|         with: | ||||
|           name: wheels | ||||
|           path: dist/home_assistant_frontend*.whl | ||||
|           if-no-files-found: error | ||||
|  | ||||
|       - name: Upload translations | ||||
|         uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 | ||||
|         uses: actions/upload-artifact@v3 | ||||
|         with: | ||||
|           name: translations | ||||
|           path: translations.tar.gz | ||||
|   | ||||
							
								
								
									
										25
									
								
								.github/workflows/relative-ci.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										25
									
								
								.github/workflows/relative-ci.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -1,25 +0,0 @@ | ||||
| name: RelativeCI | ||||
|  | ||||
| on: | ||||
|   workflow_run: | ||||
|     workflows: [CI] | ||||
|     types: | ||||
|       - completed | ||||
|  | ||||
| jobs: | ||||
|   upload: | ||||
|     name: Upload stats | ||||
|     if: ${{ github.event.workflow_run.conclusion == 'success' }} | ||||
|     strategy: | ||||
|       matrix: | ||||
|         bundle: [frontend, supervisor] | ||||
|         build: [modern, legacy] | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Send bundle stats and build information to RelativeCI | ||||
|         uses: relative-ci/agent-action@1707825cbfcc7452b2913d273414705415ae64d4 # v3.0.1 | ||||
|         with: | ||||
|           key: ${{ secrets[format('RELATIVE_CI_KEY_{0}_{1}', matrix.bundle, matrix.build)] }} | ||||
|           token: ${{ github.token }} | ||||
|           artifactName: ${{ format('{0}-bundle-stats', matrix.bundle) }} | ||||
|           webpackStatsFile: ${{ format('{0}-{1}.json', matrix.bundle, matrix.build) }} | ||||
							
								
								
									
										2
									
								
								.github/workflows/release-drafter.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/release-drafter.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -18,6 +18,6 @@ jobs: | ||||
|       pull-requests: read | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - uses: release-drafter/release-drafter@b1476f6e6eb133afa41ed8589daba6dc69b4d3f5 # v6.1.0 | ||||
|       - uses: release-drafter/release-drafter@v5 | ||||
|         env: | ||||
|           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||||
|   | ||||
							
								
								
									
										81
									
								
								.github/workflows/release.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										81
									
								
								.github/workflows/release.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -6,7 +6,7 @@ on: | ||||
|       - published | ||||
|  | ||||
| env: | ||||
|   PYTHON_VERSION: "3.13" | ||||
|   PYTHON_VERSION: "3.11" | ||||
|   NODE_OPTIONS: --max_old_space_size=6144 | ||||
|  | ||||
| # Set default workflow permissions | ||||
| @@ -23,18 +23,18 @@ jobs: | ||||
|       contents: write # Required to upload release assets | ||||
|     steps: | ||||
|       - name: Checkout the repository | ||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | ||||
|  | ||||
|       - name: Set up Python ${{ env.PYTHON_VERSION }} | ||||
|         uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 | ||||
|         with: | ||||
|           python-version: ${{ env.PYTHON_VERSION }} | ||||
|         uses: actions/checkout@v4.1.0 | ||||
|  | ||||
|       - name: Verify version | ||||
|         uses: home-assistant/actions/helpers/verify-version@master | ||||
|  | ||||
|       - name: Set up Python ${{ env.PYTHON_VERSION }} | ||||
|         uses: actions/setup-python@v4 | ||||
|         with: | ||||
|           python-version: ${{ env.PYTHON_VERSION }} | ||||
|  | ||||
|       - name: Setup Node | ||||
|         uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 | ||||
|         uses: actions/setup-node@v3.8.1 | ||||
|         with: | ||||
|           node-version-file: ".nvmrc" | ||||
|           cache: yarn | ||||
| @@ -55,7 +55,7 @@ jobs: | ||||
|           script/release | ||||
|  | ||||
|       - name: Upload release assets | ||||
|         uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1 | ||||
|         uses: softprops/action-gh-release@v0.1.15 | ||||
|         with: | ||||
|           files: | | ||||
|             dist/*.whl | ||||
| @@ -73,70 +73,11 @@ jobs: | ||||
|           version=$(echo "${{ github.ref }}" | awk -F"/" '{print $NF}' ) | ||||
|           echo "home-assistant-frontend==$version" > ./requirements.txt | ||||
|  | ||||
|       # home-assistant/wheels doesn't support SHA pinning | ||||
|       - name: Build wheels | ||||
|         uses: home-assistant/wheels@2025.09.1 | ||||
|         uses: home-assistant/wheels@2023.09.1 | ||||
|         with: | ||||
|           abi: cp313 | ||||
|           abi: cp311 | ||||
|           tag: musllinux_1_2 | ||||
|           arch: amd64 | ||||
|           wheels-key: ${{ secrets.WHEELS_KEY }} | ||||
|           requirements: "requirements.txt" | ||||
|  | ||||
|   release-landing-page: | ||||
|     name: Release landing-page frontend | ||||
|     if: github.event.release.prerelease == false | ||||
|     runs-on: ubuntu-latest | ||||
|     permissions: | ||||
|       contents: write # Required to upload release assets | ||||
|     steps: | ||||
|       - name: Checkout the repository | ||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | ||||
|       - name: Setup Node | ||||
|         uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 | ||||
|         with: | ||||
|           node-version-file: ".nvmrc" | ||||
|           cache: yarn | ||||
|       - name: Install dependencies | ||||
|         run: yarn install | ||||
|       - name: Download Translations | ||||
|         run: ./script/translations_download | ||||
|         env: | ||||
|           LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }} | ||||
|       - name: Build landing-page | ||||
|         run: landing-page/script/build_landing_page | ||||
|       - name: Tar folder | ||||
|         run: tar -czf landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz -C landing-page/dist . | ||||
|       - name: Upload release asset | ||||
|         uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1 | ||||
|         with: | ||||
|           files: landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz | ||||
|  | ||||
|   release-supervisor: | ||||
|     name: Release supervisor frontend | ||||
|     if: github.event.release.prerelease == false | ||||
|     runs-on: ubuntu-latest | ||||
|     permissions: | ||||
|       contents: write # Required to upload release assets | ||||
|     steps: | ||||
|       - name: Checkout the repository | ||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | ||||
|       - name: Setup Node | ||||
|         uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 | ||||
|         with: | ||||
|           node-version-file: ".nvmrc" | ||||
|           cache: yarn | ||||
|       - name: Install dependencies | ||||
|         run: yarn install | ||||
|       - name: Download Translations | ||||
|         run: ./script/translations_download | ||||
|         env: | ||||
|           LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }} | ||||
|       - name: Build supervisor | ||||
|         run: hassio/script/build_hassio | ||||
|       - name: Tar folder | ||||
|         run: tar -czf hassio/home_assistant_frontend_supervisor-${{ github.event.release.tag_name }}.tar.gz -C hassio/build . | ||||
|       - name: Upload release asset | ||||
|         uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1 | ||||
|         with: | ||||
|           files: hassio/home_assistant_frontend_supervisor-${{ github.event.release.tag_name }}.tar.gz | ||||
|   | ||||
							
								
								
									
										58
									
								
								.github/workflows/restrict-task-creation.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										58
									
								
								.github/workflows/restrict-task-creation.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,58 +0,0 @@ | ||||
| name: Restrict task creation | ||||
|  | ||||
| # yamllint disable-line rule:truthy | ||||
| on: | ||||
|   issues: | ||||
|     types: [opened] | ||||
|  | ||||
| jobs: | ||||
|   check-authorization: | ||||
|     runs-on: ubuntu-latest | ||||
|     # Only run if this is a Task issue type (from the issue form) | ||||
|     if: github.event.issue.type.name == 'Task' | ||||
|     steps: | ||||
|       - name: Check if user is authorized | ||||
|         uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 | ||||
|         with: | ||||
|           script: | | ||||
|             const issueAuthor = context.payload.issue.user.login; | ||||
|  | ||||
|             // Check if user is an organization member | ||||
|             try { | ||||
|               await github.rest.orgs.checkMembershipForUser({ | ||||
|                 org: 'home-assistant', | ||||
|                 username: issueAuthor | ||||
|               }); | ||||
|               console.log(`✅ ${issueAuthor} is an organization member`); | ||||
|               return; // Authorized | ||||
|             } catch (error) { | ||||
|               console.log(`❌ ${issueAuthor} is not authorized to create Task issues`); | ||||
|             } | ||||
|  | ||||
|             // Close the issue with a comment | ||||
|             await github.rest.issues.createComment({ | ||||
|               owner: context.repo.owner, | ||||
|               repo: context.repo.repo, | ||||
|               issue_number: context.issue.number, | ||||
|               body: `Hi @${issueAuthor}, thank you for your contribution!\n\n` + | ||||
|                     `Task issues are restricted to Open Home Foundation staff and authorized contributors.\n\n` + | ||||
|                     `If you would like to:\n` + | ||||
|                     `- Report a bug: Please use the [bug report form](https://github.com/home-assistant/frontend/issues/new?template=bug_report.yml)\n` + | ||||
|                     `- Request a feature: Please submit to [Feature Requests](https://github.com/orgs/home-assistant/discussions)\n\n` + | ||||
|                     `If you believe you should have access to create Task issues, please contact the maintainers.` | ||||
|             }); | ||||
|  | ||||
|             await github.rest.issues.update({ | ||||
|               owner: context.repo.owner, | ||||
|               repo: context.repo.repo, | ||||
|               issue_number: context.issue.number, | ||||
|               state: 'closed' | ||||
|             }); | ||||
|  | ||||
|             // Add a label to indicate this was auto-closed | ||||
|             await github.rest.issues.addLabels({ | ||||
|               owner: context.repo.owner, | ||||
|               repo: context.repo.repo, | ||||
|               issue_number: context.issue.number, | ||||
|               labels: ['auto-closed'] | ||||
|             }); | ||||
							
								
								
									
										2
									
								
								.github/workflows/stale.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/stale.yml
									
									
									
									
										vendored
									
									
								
							| @@ -10,7 +10,7 @@ jobs: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: 90 days stale policy | ||||
|         uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0 | ||||
|         uses: actions/stale@v8.0.0 | ||||
|         with: | ||||
|           repo-token: ${{ secrets.GITHUB_TOKEN }} | ||||
|           days-before-stale: 90 | ||||
|   | ||||
							
								
								
									
										3
									
								
								.github/workflows/translations.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.github/workflows/translations.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -1,7 +1,6 @@ | ||||
| name: Translations | ||||
|  | ||||
| on: | ||||
|   workflow_dispatch: | ||||
|   push: | ||||
|     branches: | ||||
|       - dev | ||||
| @@ -14,7 +13,7 @@ jobs: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Checkout the repository | ||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | ||||
|         uses: actions/checkout@v4.1.0 | ||||
|  | ||||
|       - name: Upload Translations | ||||
|         run: | | ||||
|   | ||||
							
								
								
									
										10
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										10
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -47,13 +47,3 @@ src/cast/dev_const.ts | ||||
|  | ||||
| # Home Assistant config | ||||
| /config/ | ||||
|  | ||||
| # Jetbrains | ||||
| /.idea/ | ||||
|  | ||||
| # test coverage | ||||
| test/coverage/ | ||||
|  | ||||
| # AI tooling | ||||
| .claude | ||||
|  | ||||
|   | ||||
| @@ -1 +1,4 @@ | ||||
| yarn run lint-staged --relative | ||||
| #!/usr/bin/env sh | ||||
| . "$(dirname -- "$0")/_/husky.sh" | ||||
|  | ||||
| yarn run lint-staged --relative --shell "/bin/bash" | ||||
|   | ||||
							
								
								
									
										4
									
								
								.vscode/extensions.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.vscode/extensions.json
									
									
									
									
										vendored
									
									
								
							| @@ -4,8 +4,6 @@ | ||||
|     "esbenp.prettier-vscode", | ||||
|     "runem.lit-plugin", | ||||
|     "github.vscode-pull-request-github", | ||||
|     "eamodio.gitlens", | ||||
|     "vitest.explorer", | ||||
|     "yeion7.styled-global-variables-autocomplete" | ||||
|     "eamodio.gitlens" | ||||
|   ] | ||||
| } | ||||
|   | ||||
							
								
								
									
										74
									
								
								.vscode/tasks.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										74
									
								
								.vscode/tasks.json
									
									
									
									
										vendored
									
									
								
							| @@ -1,42 +1,6 @@ | ||||
| { | ||||
|   "version": "2.0.0", | ||||
|   "tasks": [ | ||||
|     { | ||||
|       "label": "Develop and serve Frontend", | ||||
|       "type": "shell", | ||||
|       "command": "script/develop_and_serve -c ${input:coreUrl}", | ||||
|       // Sync changes here to other tasks until issue resolved | ||||
|       // https://github.com/Microsoft/vscode/issues/61497 | ||||
|       "problemMatcher": { | ||||
|         "owner": "ha-build", | ||||
|         "source": "ha-build", | ||||
|         "fileLocation": "absolute", | ||||
|         "severity": "error", | ||||
|         "pattern": [ | ||||
|           { | ||||
|             "regexp": "(SyntaxError): (.+): (.+) \\((\\d+):(\\d+)\\)", | ||||
|             "severity": 1, | ||||
|             "file": 2, | ||||
|             "message": 3, | ||||
|             "line": 4, | ||||
|             "column": 5 | ||||
|           } | ||||
|         ], | ||||
|         "background": { | ||||
|           "activeOnStart": true, | ||||
|           "beginsPattern": "Changes detected. Starting compilation", | ||||
|           "endsPattern": "Build done @" | ||||
|         } | ||||
|       }, | ||||
|       "isBackground": true, | ||||
|       "group": { | ||||
|         "kind": "build", | ||||
|         "isDefault": true | ||||
|       }, | ||||
|       "runOptions": { | ||||
|         "instanceLimit": 1 | ||||
|       } | ||||
|     }, | ||||
|     { | ||||
|       "label": "Develop Frontend", | ||||
|       "type": "gulp", | ||||
| @@ -136,38 +100,6 @@ | ||||
|         "instanceLimit": 1 | ||||
|       } | ||||
|     }, | ||||
|     { | ||||
|       "label": "Develop Landing Page", | ||||
|       "type": "gulp", | ||||
|       "task": "develop-landing-page", | ||||
|       "problemMatcher": { | ||||
|         "owner": "ha-build", | ||||
|         "source": "ha-build", | ||||
|         "fileLocation": "absolute", | ||||
|         "severity": "error", | ||||
|         "pattern": [ | ||||
|           { | ||||
|             "regexp": "(SyntaxError): (.+): (.+) \\((\\d+):(\\d+)\\)", | ||||
|             "severity": 1, | ||||
|             "file": 2, | ||||
|             "message": 3, | ||||
|             "line": 4, | ||||
|             "column": 5 | ||||
|           } | ||||
|         ], | ||||
|         "background": { | ||||
|           "activeOnStart": true, | ||||
|           "beginsPattern": "Changes detected. Starting compilation", | ||||
|           "endsPattern": "Build done @" | ||||
|         } | ||||
|       }, | ||||
|  | ||||
|       "isBackground": true, | ||||
|       "group": "build", | ||||
|       "runOptions": { | ||||
|         "instanceLimit": 1 | ||||
|       } | ||||
|     }, | ||||
|     { | ||||
|       "label": "Develop Demo", | ||||
|       "type": "gulp", | ||||
| @@ -277,12 +209,6 @@ | ||||
|       "id": "supervisorToken", | ||||
|       "type": "promptString", | ||||
|       "description": "The token for the Remote API proxy add-on" | ||||
|     }, | ||||
|     { | ||||
|       "id": "coreUrl", | ||||
|       "type": "promptString", | ||||
|       "description": "The URL of the Home Assistant Core instance", | ||||
|       "default": "http://127.0.0.1:8123" | ||||
|     } | ||||
|   ] | ||||
| } | ||||
|   | ||||
| @@ -1,22 +0,0 @@ | ||||
| diff --git a/mwc-formfield-base.js b/mwc-formfield-base.js | ||||
| index 7b763326d7d51835ad52646bfbc80fe21989abd3..f2baa8224e6d03df1fdb0b9fd03f5c6d77fc8747 100644 | ||||
| --- a/mwc-formfield-base.js | ||||
| +++ b/mwc-formfield-base.js | ||||
| @@ -9,7 +9,7 @@ import { BaseElement } from '@material/mwc-base/base-element.js'; | ||||
|  import { FormElement } from '@material/mwc-base/form-element.js'; | ||||
|  import { observer } from '@material/mwc-base/observer.js'; | ||||
|  import { html } from 'lit'; | ||||
| -import { property, query, queryAssignedNodes } from 'lit/decorators.js'; | ||||
| +import { property, query, queryAssignedElements } from 'lit/decorators.js'; | ||||
|  import { classMap } from 'lit/directives/class-map.js'; | ||||
|  export class FormfieldBase extends BaseElement { | ||||
|      constructor() { | ||||
| @@ -96,7 +96,7 @@ __decorate([ | ||||
|      query('.mdc-form-field') | ||||
|  ], FormfieldBase.prototype, "mdcRoot", void 0); | ||||
|  __decorate([ | ||||
| -    queryAssignedNodes('', true, '*') | ||||
| +    queryAssignedElements({ slot: "", flatten: true, selector: "*" }) | ||||
|  ], FormfieldBase.prototype, "slottedInputs", void 0); | ||||
|  __decorate([ | ||||
|      query('label') | ||||
| @@ -1,26 +0,0 @@ | ||||
| diff --git a/mwc-list-base.js b/mwc-list-base.js | ||||
| index 1ba95b6a01dcecea4d85b5cbbbcc3dfb04c40d5f..dced13fdb7929c490d6661b1bbe7e9f96dcd2285 100644 | ||||
| --- a/mwc-list-base.js | ||||
| +++ b/mwc-list-base.js | ||||
| @@ -11,7 +11,7 @@ import { BaseElement } from '@material/mwc-base/base-element.js'; | ||||
|  import { observer } from '@material/mwc-base/observer.js'; | ||||
|  import { deepActiveElementPath, doesElementContainFocus, isNodeElement } from '@material/mwc-base/utils.js'; | ||||
|  import { html } from 'lit'; | ||||
| -import { property, query, queryAssignedNodes } from 'lit/decorators.js'; | ||||
| +import { property, query, queryAssignedElements } from 'lit/decorators.js'; | ||||
|  import { ifDefined } from 'lit/directives/if-defined.js'; | ||||
|  import MDCListFoundation, { isIndexSet } from './mwc-list-foundation.js'; | ||||
|  export { createSetFromIndex, isEventMulti, isIndexSet } from './mwc-list-foundation.js'; | ||||
| @@ -425,10 +425,10 @@ __decorate([ | ||||
|      query('.mdc-deprecated-list') | ||||
|  ], ListBase.prototype, "mdcRoot", void 0); | ||||
|  __decorate([ | ||||
| -    queryAssignedNodes('', true, '*') | ||||
| +    queryAssignedElements({ flatten: true, selector: "*" }) | ||||
|  ], ListBase.prototype, "assignedElements", void 0); | ||||
|  __decorate([ | ||||
| -    queryAssignedNodes('', true, '[tabindex="0"]') | ||||
| +    queryAssignedElements({ flatten: true, selector: '[tabindex="0"]' }) | ||||
|  ], ListBase.prototype, "tabbableElements", void 0); | ||||
|  __decorate([ | ||||
|      property({ type: Boolean }), | ||||
							
								
								
									
										34
									
								
								.yarn/patches/@polymer/polymer/pr-5569.patch
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								.yarn/patches/@polymer/polymer/pr-5569.patch
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| diff --git a/lib/legacy/class.js b/lib/legacy/class.js | ||||
| index aee2511be1cd9bf900ee552bc98190c1631c57c0..f2f499d68bf52034cac9c28307c99e8ce6b8417d 100644 | ||||
| --- a/lib/legacy/class.js | ||||
| +++ b/lib/legacy/class.js | ||||
| @@ -304,17 +304,23 @@ function GenerateClassFromInfo(info, Base, behaviors) { | ||||
|        // only proceed if the generated class' prototype has not been registered. | ||||
|        const generatedProto = PolymerGenerated.prototype; | ||||
|        if (!generatedProto.hasOwnProperty(JSCompiler_renameProperty('__hasRegisterFinished', generatedProto))) { | ||||
| -        generatedProto.__hasRegisterFinished = true; | ||||
| +        // make sure legacy lifecycle is called on the *element*'s prototype | ||||
| +        // and not the generated class prototype; if the element has been | ||||
| +        // extended, these are *not* the same. | ||||
| +        const proto = Object.getPrototypeOf(this); | ||||
| +        // Only set flag when generated prototype itself is registered, | ||||
| +        // as this element may be extended from, and needs to run `registered` | ||||
| +        // on all behaviors on the subclass as well. | ||||
| +        if (proto === generatedProto) { | ||||
| +          generatedProto.__hasRegisterFinished = true; | ||||
| +        } | ||||
|          // ensure superclass is registered first. | ||||
|          super._registered(); | ||||
|          // copy properties onto the generated class lazily if we're optimizing, | ||||
| -        if (legacyOptimizations) { | ||||
| +        if (legacyOptimizations && !Object.hasOwnProperty(generatedProto, '__hasCopiedProperties')) { | ||||
| +          generatedProto.__hasCopiedProperties = true; | ||||
|            copyPropertiesToProto(generatedProto); | ||||
|          } | ||||
| -        // make sure legacy lifecycle is called on the *element*'s prototype | ||||
| -        // and not the generated class prototype; if the element has been | ||||
| -        // extended, these are *not* the same. | ||||
| -        const proto = Object.getPrototypeOf(this); | ||||
|          let list = lifecycle.beforeRegister; | ||||
|          if (list) { | ||||
|            for (let i=0; i < list.length; i++) { | ||||
							
								
								
									
										39
									
								
								.yarn/patches/sortablejs-npm-1.15.0-f3a393abcc.patch
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								.yarn/patches/sortablejs-npm-1.15.0-f3a393abcc.patch
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | ||||
| diff --git a/modular/sortable.complete.esm.js b/modular/sortable.complete.esm.js | ||||
| index 02e9f2d6bebeb430fe6e7c1cc3f9c3c9df051f14..bb8268b0844a1faa4108cc92c0be2a3dbaf23f83 100644 | ||||
| --- a/modular/sortable.complete.esm.js | ||||
| +++ b/modular/sortable.complete.esm.js | ||||
| @@ -1657,7 +1657,7 @@ Sortable.prototype = | ||||
|            target = parent; // store last element | ||||
|          } | ||||
|          /* jshint boss:true */ | ||||
| -        while (parent = parent.parentNode); | ||||
| +        while (parent = parent.parentNode || parent.getRootNode().host); | ||||
|        } | ||||
|   | ||||
|        _unhideGhostForTarget(); | ||||
| diff --git a/modular/sortable.core.esm.js b/modular/sortable.core.esm.js | ||||
| index b04c8b4634f7c6b4ef1aadbb48afe6564306dea9..39a107163c8c336ebd669b5ea8a936af87e1c1e7 100644 | ||||
| --- a/modular/sortable.core.esm.js | ||||
| +++ b/modular/sortable.core.esm.js | ||||
| @@ -1657,7 +1657,7 @@ Sortable.prototype = | ||||
|            target = parent; // store last element | ||||
|          } | ||||
|          /* jshint boss:true */ | ||||
| -        while (parent = parent.parentNode); | ||||
| +        while (parent = parent.parentNode || parent.getRootNode().host); | ||||
|        } | ||||
|   | ||||
|        _unhideGhostForTarget(); | ||||
| diff --git a/modular/sortable.esm.js b/modular/sortable.esm.js | ||||
| index 6ec7ed1bb557e21c2578200161e989c65d23150b..0a05475a22904472fac6c13f524c674da76584b0 100644 | ||||
| --- a/modular/sortable.esm.js | ||||
| +++ b/modular/sortable.esm.js | ||||
| @@ -1657,7 +1657,7 @@ Sortable.prototype = | ||||
|            target = parent; // store last element | ||||
|          } | ||||
|          /* jshint boss:true */ | ||||
| -        while (parent = parent.parentNode); | ||||
| +        while (parent = parent.parentNode || parent.getRootNode().host); | ||||
|        } | ||||
|   | ||||
|        _unhideGhostForTarget(); | ||||
| @@ -1,60 +0,0 @@ | ||||
| diff --git a/modular/sortable.core.esm.js b/modular/sortable.core.esm.js | ||||
| index 8b5e49b011713c8859c669069fbe85ce53974e1d..6a0afc92787157b8a31c38cc5f67dfa526090a00 100644 | ||||
| --- a/modular/sortable.core.esm.js | ||||
| +++ b/modular/sortable.core.esm.js | ||||
| @@ -1781,11 +1781,16 @@ Sortable.prototype = /** @lends Sortable.prototype */{ | ||||
|          } | ||||
|          if (_onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt, !!target) !== false) { | ||||
|            capture(); | ||||
| -          if (elLastChild && elLastChild.nextSibling) { | ||||
| -            // the last draggable element is not the last node | ||||
| -            el.insertBefore(dragEl, elLastChild.nextSibling); | ||||
| -          } else { | ||||
| -            el.appendChild(dragEl); | ||||
| +          try { | ||||
| +            if (elLastChild && elLastChild.nextSibling) { | ||||
| +              // the last draggable element is not the last node | ||||
| +              el.insertBefore(dragEl, elLastChild.nextSibling); | ||||
| +            } else { | ||||
| +              el.appendChild(dragEl); | ||||
| +            } | ||||
| +          } | ||||
| +          catch(err) { | ||||
| +            return completed(false); | ||||
|            } | ||||
|            parentEl = el; // actualization | ||||
|   | ||||
| @@ -1802,7 +1807,12 @@ Sortable.prototype = /** @lends Sortable.prototype */{ | ||||
|          targetRect = getRect(target); | ||||
|          if (_onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt, false) !== false) { | ||||
|            capture(); | ||||
| -          el.insertBefore(dragEl, firstChild); | ||||
| +          try { | ||||
| +            el.insertBefore(dragEl, firstChild); | ||||
| +          } | ||||
| +          catch(err) { | ||||
| +            return completed(false); | ||||
| +          } | ||||
|            parentEl = el; // actualization | ||||
|   | ||||
|            changed(); | ||||
| @@ -1849,10 +1859,15 @@ Sortable.prototype = /** @lends Sortable.prototype */{ | ||||
|            _silent = true; | ||||
|            setTimeout(_unsilent, 30); | ||||
|            capture(); | ||||
| -          if (after && !nextSibling) { | ||||
| -            el.appendChild(dragEl); | ||||
| -          } else { | ||||
| -            target.parentNode.insertBefore(dragEl, after ? nextSibling : target); | ||||
| +          try { | ||||
| +            if (after && !nextSibling) { | ||||
| +              el.appendChild(dragEl); | ||||
| +            } else { | ||||
| +              target.parentNode.insertBefore(dragEl, after ? nextSibling : target); | ||||
| +            } | ||||
| +          } | ||||
| +          catch(err) { | ||||
| +            return completed(false); | ||||
|            } | ||||
|   | ||||
|            // Undo chrome's scroll adjustment (has no effect on other browsers) | ||||
| @@ -1,55 +0,0 @@ | ||||
| diff --git a/build/inject-manifest.js b/build/inject-manifest.js | ||||
| index 60e3d2bb51c11a19fbbedbad65e101082ec41c36..fed6026630f43f86e25446383982cf6fb694313b 100644 | ||||
| --- a/build/inject-manifest.js | ||||
| +++ b/build/inject-manifest.js | ||||
| @@ -104,7 +104,7 @@ async function injectManifest(config) { | ||||
|              replaceString: manifestString, | ||||
|              searchString: options.injectionPoint, | ||||
|          }); | ||||
| -        filesToWrite[options.swDest] = source; | ||||
| +        filesToWrite[options.swDest] = source.replace(url, encodeURI(upath_1.default.basename(destPath))); | ||||
|          filesToWrite[destPath] = map; | ||||
|      } | ||||
|      else { | ||||
| diff --git a/build/lib/translate-url-to-sourcemap-paths.js b/build/lib/translate-url-to-sourcemap-paths.js | ||||
| index 3220c5474eeac6e8a56ca9b2ac2bd9be48529e43..5f003879a904d4840529a42dd056d288fd213771 100644 | ||||
| --- a/build/lib/translate-url-to-sourcemap-paths.js | ||||
| +++ b/build/lib/translate-url-to-sourcemap-paths.js | ||||
| @@ -22,7 +22,7 @@ function translateURLToSourcemapPaths(url, swSrc, swDest) { | ||||
|          const possibleSrcPath = upath_1.default.resolve(upath_1.default.dirname(swSrc), url); | ||||
|          if (fs_extra_1.default.existsSync(possibleSrcPath)) { | ||||
|              srcPath = possibleSrcPath; | ||||
| -            destPath = upath_1.default.resolve(upath_1.default.dirname(swDest), url); | ||||
| +            destPath = `${swDest}.map`; | ||||
|          } | ||||
|          else { | ||||
|              warning = `${errors_1.errors['cant-find-sourcemap']} ${possibleSrcPath}`; | ||||
| diff --git a/src/inject-manifest.ts b/src/inject-manifest.ts | ||||
| index 8795ddcaa77aea7b0356417e4bc4b19e2b3f860c..fcdc68342d9ac53936c9ed40a9ccfc2f5070cad3 100644 | ||||
| --- a/src/inject-manifest.ts | ||||
| +++ b/src/inject-manifest.ts | ||||
| @@ -129,7 +129,10 @@ export async function injectManifest( | ||||
|        searchString: options.injectionPoint!, | ||||
|      }); | ||||
|   | ||||
| -    filesToWrite[options.swDest] = source; | ||||
| +    filesToWrite[options.swDest] = source.replace( | ||||
| +      url!, | ||||
| +      encodeURI(upath.basename(destPath)), | ||||
| +    ); | ||||
|      filesToWrite[destPath] = map; | ||||
|    } else { | ||||
|      // If there's no sourcemap associated with swSrc, a simple string | ||||
| diff --git a/src/lib/translate-url-to-sourcemap-paths.ts b/src/lib/translate-url-to-sourcemap-paths.ts | ||||
| index 072eac40d4ef5d095a01cb7f7e392a9e034853bd..f0bbe69e88ef3a415de18a7e9cb264daea273d71 100644 | ||||
| --- a/src/lib/translate-url-to-sourcemap-paths.ts | ||||
| +++ b/src/lib/translate-url-to-sourcemap-paths.ts | ||||
| @@ -28,7 +28,7 @@ export function translateURLToSourcemapPaths( | ||||
|      const possibleSrcPath = upath.resolve(upath.dirname(swSrc), url); | ||||
|      if (fse.existsSync(possibleSrcPath)) { | ||||
|        srcPath = possibleSrcPath; | ||||
| -      destPath = upath.resolve(upath.dirname(swDest), url); | ||||
| +      destPath = `${swDest}.map`; | ||||
|      } else { | ||||
|        warning = `${errors['cant-find-sourcemap']} ${possibleSrcPath}`; | ||||
|      } | ||||
							
								
								
									
										541
									
								
								.yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										541
									
								
								.yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										9
									
								
								.yarn/plugins/@yarnpkg/plugin-typescript.cjs
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								.yarn/plugins/@yarnpkg/plugin-typescript.cjs
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										874
									
								
								.yarn/releases/yarn-3.6.3.cjs
									
									
									
									
										vendored
									
									
										Executable file
									
								
							
							
						
						
									
										874
									
								
								.yarn/releases/yarn-3.6.3.cjs
									
									
									
									
										vendored
									
									
										Executable file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										942
									
								
								.yarn/releases/yarn-4.10.3.cjs
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										942
									
								
								.yarn/releases/yarn-4.10.3.cjs
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										12
									
								
								.yarnrc.yml
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								.yarnrc.yml
									
									
									
									
									
								
							| @@ -1,9 +1,11 @@ | ||||
| compressionLevel: mixed | ||||
|  | ||||
| defaultSemverRangePrefix: "" | ||||
|  | ||||
| enableGlobalCache: false | ||||
|  | ||||
| nodeLinker: node-modules | ||||
|  | ||||
| yarnPath: .yarn/releases/yarn-4.10.3.cjs | ||||
| plugins: | ||||
|   - path: .yarn/plugins/@yarnpkg/plugin-typescript.cjs | ||||
|     spec: "@yarnpkg/plugin-typescript" | ||||
|   - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs | ||||
|     spec: "@yarnpkg/plugin-interactive-tools" | ||||
|  | ||||
| yarnPath: .yarn/releases/yarn-3.6.3.cjs | ||||
|   | ||||
| @@ -1,8 +0,0 @@ | ||||
| # People marked here will be automatically requested for a review | ||||
| # when the code that they own is touched. | ||||
| # https://github.com/blog/2392-introducing-code-owners | ||||
| # https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners | ||||
|  | ||||
| # Part of the frontend that mobile developper should review | ||||
| src/external_app/ @bgoncal @TimoPtr | ||||
| test/external_app/ @bgoncal @TimoPtr | ||||
| @@ -27,5 +27,3 @@ A complete guide can be found at the following [link](https://www.home-assistant | ||||
| Home Assistant is open-source and Apache 2 licensed. Feel free to browse the repository, learn and reuse parts in your own projects. | ||||
|  | ||||
| We use [BrowserStack](https://www.browserstack.com) to test Home Assistant on a large variety of devices. | ||||
|  | ||||
| [](https://www.openhomefoundation.org/) | ||||
|   | ||||
							
								
								
									
										12
									
								
								build-scripts/.eslintrc.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								build-scripts/.eslintrc.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| { | ||||
|   "extends": "../.eslintrc.json", | ||||
|   "rules": { | ||||
|     "no-console": "off", | ||||
|     "import/no-extraneous-dependencies": "off", | ||||
|     "import/extensions": "off", | ||||
|     "import/no-dynamic-require": "off", | ||||
|     "global-require": "off", | ||||
|     "@typescript-eslint/no-var-requires": "off", | ||||
|     "prefer-arrow-callback": "off" | ||||
|   } | ||||
| } | ||||
| @@ -15,7 +15,7 @@ The Home Assistant build pipeline contains various steps to prepare a build. | ||||
|  | ||||
| Currently in Home Assistant we use a bundler to convert TypeScript, CSS and JSON files to JavaScript files that the browser understands. | ||||
|  | ||||
| We currently rely on Webpack. Both of these programs bundle the converted files in both production and development. | ||||
| We currently rely on Webpack but also have experimental Rollup support. Both of these programs bundle the converted files in both production and development. | ||||
|  | ||||
| For development, bundling is optional. We just want to get the right files in the browser. | ||||
|  | ||||
|   | ||||
| @@ -1,150 +0,0 @@ | ||||
| import defineProvider from "@babel/helper-define-polyfill-provider"; | ||||
| import { join } from "node:path"; | ||||
| import paths from "../paths.cjs"; | ||||
|  | ||||
| const POLYFILL_DIR = join(paths.root_dir, "src/resources/polyfills"); | ||||
|  | ||||
| // List of polyfill keys with supported browser targets for the functionality | ||||
| const polyfillSupport = { | ||||
|   // Note states and shadowRoot properties should be supported. | ||||
|   "element-internals": { | ||||
|     android: 90, | ||||
|     chrome: 90, | ||||
|     edge: 90, | ||||
|     firefox: 126, | ||||
|     ios: 17.4, | ||||
|     opera: 76, | ||||
|     opera_mobile: 64, | ||||
|     safari: 17.4, | ||||
|     samsung: 15.0, | ||||
|   }, | ||||
|   "element-getattributenames": { | ||||
|     android: 61, | ||||
|     chrome: 61, | ||||
|     edge: 18, | ||||
|     firefox: 45, | ||||
|     ios: 10.3, | ||||
|     opera: 48, | ||||
|     opera_mobile: 45, | ||||
|     safari: 10.1, | ||||
|     samsung: 8.0, | ||||
|   }, | ||||
|   "element-toggleattribute": { | ||||
|     android: 69, | ||||
|     chrome: 69, | ||||
|     edge: 18, | ||||
|     firefox: 63, | ||||
|     ios: 12.0, | ||||
|     opera: 56, | ||||
|     opera_mobile: 48, | ||||
|     safari: 12.0, | ||||
|     samsung: 10.0, | ||||
|   }, | ||||
|   // FormatJS polyfill detects fix for https://bugs.chromium.org/p/v8/issues/detail?id=10682, | ||||
|   // so adjusted to several months after that was marked fixed | ||||
|   "intl-getcanonicallocales": { | ||||
|     android: 90, | ||||
|     chrome: 90, | ||||
|     edge: 90, | ||||
|     firefox: 48, | ||||
|     ios: 10.3, | ||||
|     opera: 76, | ||||
|     opera_mobile: 64, | ||||
|     safari: 10.1, | ||||
|     samsung: 15.0, | ||||
|   }, | ||||
|   "intl-locale": { | ||||
|     android: 74, | ||||
|     chrome: 74, | ||||
|     edge: 79, | ||||
|     firefox: 75, | ||||
|     ios: 14.0, | ||||
|     opera: 62, | ||||
|     opera_mobile: 53, | ||||
|     safari: 14.0, | ||||
|     samsung: 11.0, | ||||
|   }, | ||||
|   "intl-other": { | ||||
|     // Not specified (i.e. always try polyfill) since compatibility depends on supported locales | ||||
|   }, | ||||
|   "resize-observer": { | ||||
|     android: 64, | ||||
|     chrome: 64, | ||||
|     edge: 79, | ||||
|     firefox: 69, | ||||
|     ios: 13.4, | ||||
|     opera: 51, | ||||
|     opera_mobile: 47, | ||||
|     safari: 13.1, | ||||
|     samsung: 9.0, | ||||
|   }, | ||||
| }; | ||||
|  | ||||
| // Map of global variables and/or instance and static properties to the | ||||
| // corresponding polyfill key and actual module to import | ||||
| const polyfillMap = { | ||||
|   global: { | ||||
|     ResizeObserver: { | ||||
|       key: "resize-observer", | ||||
|       module: join(POLYFILL_DIR, "resize-observer.ts"), | ||||
|     }, | ||||
|   }, | ||||
|   instance: { | ||||
|     attachInternals: { | ||||
|       key: "element-internals", | ||||
|       module: "element-internals-polyfill", | ||||
|     }, | ||||
|     ...Object.fromEntries( | ||||
|       ["getAttributeNames", "toggleAttribute"].map((prop) => { | ||||
|         const key = `element-${prop.toLowerCase()}`; | ||||
|         return [prop, { key, module: join(POLYFILL_DIR, `${key}.ts`) }]; | ||||
|       }) | ||||
|     ), | ||||
|   }, | ||||
|   static: { | ||||
|     Intl: { | ||||
|       getCanonicalLocales: { | ||||
|         key: "intl-getcanonicallocales", | ||||
|         module: join(POLYFILL_DIR, "intl-polyfill.ts"), | ||||
|       }, | ||||
|       Locale: { | ||||
|         key: "intl-locale", | ||||
|         module: join(POLYFILL_DIR, "intl-polyfill.ts"), | ||||
|       }, | ||||
|       ...Object.fromEntries( | ||||
|         [ | ||||
|           "DateTimeFormat", | ||||
|           "DurationFormat", | ||||
|           "DisplayNames", | ||||
|           "ListFormat", | ||||
|           "NumberFormat", | ||||
|           "PluralRules", | ||||
|           "RelativeTimeFormat", | ||||
|         ].map((obj) => [ | ||||
|           obj, | ||||
|           { key: "intl-other", module: join(POLYFILL_DIR, "intl-polyfill.ts") }, | ||||
|         ]) | ||||
|       ), | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
|  | ||||
| // Create plugin using the same factory as for CoreJS | ||||
| export default defineProvider( | ||||
|   ({ createMetaResolver, debug, shouldInjectPolyfill }) => { | ||||
|     const resolvePolyfill = createMetaResolver(polyfillMap); | ||||
|     return { | ||||
|       name: "custom-polyfill", | ||||
|       polyfills: polyfillSupport, | ||||
|       usageGlobal(meta, utils) { | ||||
|         const polyfill = resolvePolyfill(meta); | ||||
|         if (polyfill && shouldInjectPolyfill(polyfill.desc.key)) { | ||||
|           debug(polyfill.desc.key); | ||||
|           utils.injectGlobalImport(polyfill.desc.module); | ||||
|           return true; | ||||
|         } | ||||
|         return false; | ||||
|       }, | ||||
|     }; | ||||
|   } | ||||
| ); | ||||
| @@ -1,9 +1,6 @@ | ||||
| const path = require("path"); | ||||
| const env = require("./env.cjs"); | ||||
| const paths = require("./paths.cjs"); | ||||
| const { dependencies } = require("../package.json"); | ||||
|  | ||||
| const BABEL_PLUGINS = path.join(__dirname, "babel-plugins"); | ||||
|  | ||||
| // 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 | ||||
| @@ -15,38 +12,50 @@ module.exports.sourceMapURL = () => { | ||||
| }; | ||||
|  | ||||
| // Files from NPM Packages that should not be imported | ||||
| module.exports.ignorePackages = () => []; | ||||
| // eslint-disable-next-line unused-imports/no-unused-vars | ||||
| module.exports.ignorePackages = ({ latestBuild }) => [ | ||||
|   // Part of yaml.js and only used for !!js functions that we don't use | ||||
|   require.resolve("esprima"), | ||||
| ]; | ||||
|  | ||||
| // Files from NPM packages that we should replace with empty file | ||||
| module.exports.emptyPackages = ({ isHassioBuild }) => | ||||
| module.exports.emptyPackages = ({ latestBuild, isHassioBuild }) => | ||||
|   [ | ||||
|     // Contains all color definitions for all material color sets. | ||||
|     // We don't use it | ||||
|     require.resolve("@polymer/paper-styles/color.js"), | ||||
|     require.resolve("@polymer/paper-styles/default-theme.js"), | ||||
|     // Loads stuff from a CDN | ||||
|     require.resolve("@polymer/font-roboto/roboto.js"), | ||||
|     require.resolve("@vaadin/vaadin-material-styles/typography.js"), | ||||
|     require.resolve("@vaadin/vaadin-material-styles/font-icons.js"), | ||||
|     // Compatibility not needed for latest builds | ||||
|     latestBuild && | ||||
|       // wrapped in require.resolve so it blows up if file no longer exists | ||||
|       require.resolve( | ||||
|         path.resolve(paths.polymer_dir, "src/resources/compatibility.ts") | ||||
|       ), | ||||
|     // This polyfill is loaded in workers to support ES5, filter it out. | ||||
|     latestBuild && require.resolve("proxy-polyfill/src/index.js"), | ||||
|     // Icons in supervisor conflict with icons in HA so we don't load. | ||||
|     isHassioBuild && | ||||
|       require.resolve( | ||||
|         path.resolve(paths.root_dir, "src/components/ha-icon.ts") | ||||
|         path.resolve(paths.polymer_dir, "src/components/ha-icon.ts") | ||||
|       ), | ||||
|     isHassioBuild && | ||||
|       require.resolve( | ||||
|         path.resolve(paths.root_dir, "src/components/ha-icon-picker.ts") | ||||
|         path.resolve(paths.polymer_dir, "src/components/ha-icon-picker.ts") | ||||
|       ), | ||||
|   ].filter(Boolean); | ||||
|  | ||||
| module.exports.definedVars = ({ isProdBuild, latestBuild, defineOverlay }) => ({ | ||||
|   __DEV__: !isProdBuild, | ||||
|   __BUILD__: JSON.stringify(latestBuild ? "modern" : "legacy"), | ||||
|   __BUILD__: JSON.stringify(latestBuild ? "latest" : "es5"), | ||||
|   __VERSION__: JSON.stringify(env.version()), | ||||
|   __DEMO__: false, | ||||
|   __SUPERVISOR__: false, | ||||
|   __BACKWARDS_COMPAT__: false, | ||||
|   __STATIC_PATH__: "/static/", | ||||
|   __HASS_URL__: `\`${ | ||||
|     "HASS_URL" in process.env | ||||
|       ? process.env.HASS_URL | ||||
|       : // eslint-disable-next-line no-template-curly-in-string | ||||
|         "${location.protocol}//${location.host}" | ||||
|   }\``, | ||||
|   "process.env.NODE_ENV": JSON.stringify( | ||||
|     isProdBuild ? "production" : "development" | ||||
|   ), | ||||
| @@ -73,25 +82,7 @@ module.exports.terserOptions = ({ latestBuild, isTestBuild }) => ({ | ||||
|   sourceMap: !isTestBuild, | ||||
| }); | ||||
|  | ||||
| /** @type {import('@rspack/core').SwcLoaderOptions} */ | ||||
| module.exports.swcOptions = () => ({ | ||||
|   jsc: { | ||||
|     loose: true, | ||||
|     externalHelpers: true, | ||||
|     target: "ES2021", | ||||
|     parser: { | ||||
|       syntax: "typescript", | ||||
|       decorators: true, | ||||
|     }, | ||||
|   }, | ||||
| }); | ||||
|  | ||||
| module.exports.babelOptions = ({ | ||||
|   latestBuild, | ||||
|   isProdBuild, | ||||
|   isTestBuild, | ||||
|   sw, | ||||
| }) => ({ | ||||
| module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({ | ||||
|   babelrc: false, | ||||
|   compact: false, | ||||
|   assumptions: { | ||||
| @@ -99,21 +90,27 @@ module.exports.babelOptions = ({ | ||||
|     setPublicClassFields: true, | ||||
|     setSpreadProperties: true, | ||||
|   }, | ||||
|   browserslistEnv: latestBuild ? "modern" : `legacy${sw ? "-sw" : ""}`, | ||||
|   browserslistEnv: latestBuild ? "modern" : "legacy", | ||||
|   // Must be unambiguous because some dependencies are CommonJS only | ||||
|   sourceType: "unambiguous", | ||||
|   presets: [ | ||||
|     [ | ||||
|       "@babel/preset-env", | ||||
|       { | ||||
|         useBuiltIns: "usage", | ||||
|         corejs: dependencies["core-js"], | ||||
|         useBuiltIns: latestBuild ? false : "entry", | ||||
|         corejs: latestBuild ? false : { version: "3.32", proposals: true }, | ||||
|         bugfixes: true, | ||||
|         shippedProposals: true, | ||||
|       }, | ||||
|     ], | ||||
|     "@babel/preset-typescript", | ||||
|   ], | ||||
|   plugins: [ | ||||
|     [ | ||||
|       path.join(BABEL_PLUGINS, "inline-constants-plugin.cjs"), | ||||
|       path.resolve( | ||||
|         paths.polymer_dir, | ||||
|         "build-scripts/babel-plugins/inline-constants-plugin.cjs" | ||||
|       ), | ||||
|       { | ||||
|         modules: ["@mdi/js"], | ||||
|         ignoreModuleNotFound: true, | ||||
| @@ -124,72 +121,35 @@ module.exports.babelOptions = ({ | ||||
|       "template-html-minifier", | ||||
|       { | ||||
|         modules: { | ||||
|           ...Object.fromEntries( | ||||
|             ["lit", "lit-element", "lit-html"].map((m) => [ | ||||
|               m, | ||||
|               [ | ||||
|                 "html", | ||||
|                 { name: "svg", encapsulation: "svg" }, | ||||
|                 { name: "css", encapsulation: "style" }, | ||||
|               ], | ||||
|             ]) | ||||
|           ), | ||||
|           "@polymer/polymer/lib/utils/html-tag.js": ["html"], | ||||
|           lit: [ | ||||
|             "html", | ||||
|             { name: "svg", encapsulation: "svg" }, | ||||
|             { name: "css", encapsulation: "style" }, | ||||
|           ], | ||||
|           "@polymer/polymer/lib/utils/html-tag": ["html"], | ||||
|         }, | ||||
|         strictCSS: true, | ||||
|         htmlMinifier: module.exports.htmlMinifierOptions, | ||||
|         failOnError: false, // we can turn this off in case of false positives | ||||
|         failOnError: true, // we can turn this off in case of false positives | ||||
|       }, | ||||
|     ], | ||||
|     // Import helpers and regenerator from runtime package | ||||
|     [ | ||||
|       "@babel/plugin-transform-runtime", | ||||
|       { version: dependencies["@babel/runtime"] }, | ||||
|       { version: require("../package.json").dependencies["@babel/runtime"] }, | ||||
|     ], | ||||
|     "@babel/plugin-transform-class-properties", | ||||
|     "@babel/plugin-transform-private-methods", | ||||
|     // Support  some proposals still in TC39 process | ||||
|     ["@babel/plugin-proposal-decorators", { decoratorsBeforeExport: true }], | ||||
|   ].filter(Boolean), | ||||
|   exclude: [ | ||||
|     // \\ for Windows, / for Mac OS and Linux | ||||
|     /node_modules[\\/]core-js/, | ||||
|     /node_modules[\\/]webpack[\\/]buildin/, | ||||
|   ], | ||||
|   sourceMaps: !isTestBuild, | ||||
|   overrides: [ | ||||
|     { | ||||
|       // Add plugin to inject various polyfills, excluding the polyfills | ||||
|       // themselves to prevent self-injection. | ||||
|       plugins: [ | ||||
|         [ | ||||
|           path.join(BABEL_PLUGINS, "custom-polyfill-plugin.js"), | ||||
|           { method: "usage-global" }, | ||||
|         ], | ||||
|       ], | ||||
|       exclude: [ | ||||
|         path.join(paths.root_dir, "src/resources/polyfills"), | ||||
|         ...[ | ||||
|           "@formatjs/(?:ecma402-abstract|intl-\\w+)", | ||||
|           "@lit-labs/virtualizer/polyfills", | ||||
|           "@webcomponents/scoped-custom-element-registry", | ||||
|           "element-internals-polyfill", | ||||
|           "proxy-polyfill", | ||||
|           "unfetch", | ||||
|         ].map((p) => new RegExp(`/node_modules/${p}/`)), | ||||
|       ], | ||||
|     }, | ||||
|     { | ||||
|       // Use unambiguous for dependencies so that require() is correctly injected into CommonJS files | ||||
|       // Exclusions are needed in some cases where ES modules have no static imports or exports, such as polyfills | ||||
|       sourceType: "unambiguous", | ||||
|       include: /\/node_modules\//, | ||||
|       exclude: [ | ||||
|         "element-internals-polyfill", | ||||
|         "@?lit(?:-labs|-element|-html)?", | ||||
|       ].map((p) => new RegExp(`/node_modules/${p}/`)), | ||||
|     }, | ||||
|   ], | ||||
| }); | ||||
|  | ||||
| const nameSuffix = (latestBuild) => (latestBuild ? "-modern" : "-legacy"); | ||||
| const nameSuffix = (latestBuild) => (latestBuild ? "-latest" : "-es5"); | ||||
|  | ||||
| const outputPath = (outputRoot, latestBuild) => | ||||
|   path.resolve(outputRoot, latestBuild ? "frontend_latest" : "frontend_es5"); | ||||
| @@ -223,14 +183,9 @@ const publicPath = (latestBuild, root = "") => | ||||
| module.exports.config = { | ||||
|   app({ isProdBuild, latestBuild, isStatsBuild, isTestBuild, isWDS }) { | ||||
|     return { | ||||
|       name: "frontend" + nameSuffix(latestBuild), | ||||
|       name: "app" + nameSuffix(latestBuild), | ||||
|       entry: { | ||||
|         "service-worker": !latestBuild | ||||
|           ? { | ||||
|               import: "./src/entrypoints/service-worker.ts", | ||||
|               layer: "sw", | ||||
|             } | ||||
|           : "./src/entrypoints/service-worker.ts", | ||||
|         service_worker: "./src/entrypoints/service_worker.ts", | ||||
|         app: "./src/entrypoints/app.ts", | ||||
|         authorize: "./src/entrypoints/authorize.ts", | ||||
|         onboarding: "./src/entrypoints/onboarding.ts", | ||||
| @@ -326,17 +281,4 @@ module.exports.config = { | ||||
|       }, | ||||
|     }; | ||||
|   }, | ||||
|  | ||||
|   landingPage({ isProdBuild, latestBuild }) { | ||||
|     return { | ||||
|       name: "landing-page" + nameSuffix(latestBuild), | ||||
|       entry: { | ||||
|         entrypoint: path.resolve(paths.landingPage_dir, "src/entrypoint.js"), | ||||
|       }, | ||||
|       outputPath: outputPath(paths.landingPage_output_root, latestBuild), | ||||
|       publicPath: publicPath(latestBuild), | ||||
|       isProdBuild, | ||||
|       latestBuild, | ||||
|     }; | ||||
|   }, | ||||
| }; | ||||
|   | ||||
| @@ -2,33 +2,34 @@ const fs = require("fs"); | ||||
| const path = require("path"); | ||||
| const paths = require("./paths.cjs"); | ||||
|  | ||||
| const isTrue = (value) => value === "1" || value?.toLowerCase() === "true"; | ||||
|  | ||||
| module.exports = { | ||||
|   useRollup() { | ||||
|     return process.env.ROLLUP === "1"; | ||||
|   }, | ||||
|   useWDS() { | ||||
|     return process.env.WDS === "1"; | ||||
|   }, | ||||
|   isProdBuild() { | ||||
|     return ( | ||||
|       process.env.NODE_ENV === "production" || module.exports.isStatsBuild() | ||||
|     ); | ||||
|   }, | ||||
|   isStatsBuild() { | ||||
|     return isTrue(process.env.STATS); | ||||
|     return process.env.STATS === "1"; | ||||
|   }, | ||||
|   isTestBuild() { | ||||
|     return isTrue(process.env.IS_TEST); | ||||
|     return process.env.IS_TEST === "true"; | ||||
|   }, | ||||
|   isNetlify() { | ||||
|     return isTrue(process.env.NETLIFY); | ||||
|     return process.env.NETLIFY === "true"; | ||||
|   }, | ||||
|   version() { | ||||
|     const version = fs | ||||
|       .readFileSync(path.resolve(paths.root_dir, "pyproject.toml"), "utf8") | ||||
|       .readFileSync(path.resolve(paths.polymer_dir, "pyproject.toml"), "utf8") | ||||
|       .match(/version\W+=\W"(\d{8}\.\d(?:\.dev)?)"/); | ||||
|     if (!version) { | ||||
|       throw Error("Version not found"); | ||||
|     } | ||||
|     return version[1]; | ||||
|   }, | ||||
|   isDevContainer() { | ||||
|     return isTrue(process.env.DEV_CONTAINER); | ||||
|   }, | ||||
| }; | ||||
|   | ||||
| @@ -1,16 +0,0 @@ | ||||
| // @ts-check | ||||
|  | ||||
| import tseslint from "typescript-eslint"; | ||||
| import rootConfig from "../eslint.config.mjs"; | ||||
|  | ||||
| export default tseslint.config(...rootConfig, { | ||||
|   rules: { | ||||
|     "no-console": "off", | ||||
|     "import/no-extraneous-dependencies": "off", | ||||
|     "import/extensions": "off", | ||||
|     "import/no-dynamic-require": "off", | ||||
|     "global-require": "off", | ||||
|     "@typescript-eslint/no-require-imports": "off", | ||||
|     "prefer-arrow-callback": "off", | ||||
|   }, | ||||
| }); | ||||
| @@ -6,9 +6,11 @@ import "./entry-html.js"; | ||||
| import "./gather-static.js"; | ||||
| import "./gen-icons-json.js"; | ||||
| import "./locale-data.js"; | ||||
| import "./rollup.js"; | ||||
| import "./service-worker.js"; | ||||
| import "./translations.js"; | ||||
| import "./rspack.js"; | ||||
| import "./wds.js"; | ||||
| import "./webpack.js"; | ||||
|  | ||||
| gulp.task( | ||||
|   "develop-app", | ||||
| @@ -25,7 +27,11 @@ gulp.task( | ||||
|       "build-locale-data" | ||||
|     ), | ||||
|     "copy-static-app", | ||||
|     "rspack-watch-app" | ||||
|     env.useWDS() | ||||
|       ? "wds-watch-app" | ||||
|       : env.useRollup() | ||||
|       ? "rollup-watch-app" | ||||
|       : "webpack-watch-app" | ||||
|   ) | ||||
| ); | ||||
|  | ||||
| @@ -38,20 +44,9 @@ gulp.task( | ||||
|     "clean", | ||||
|     gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"), | ||||
|     "copy-static-app", | ||||
|     "rspack-prod-app", | ||||
|     gulp.parallel("gen-pages-app-prod", "gen-service-worker-app-prod"), | ||||
|     env.useRollup() ? "rollup-prod-app" : "webpack-prod-app", | ||||
|     // Don't compress running tests | ||||
|     ...(env.isTestBuild() || env.isStatsBuild() ? [] : ["compress-app"]) | ||||
|   ) | ||||
| ); | ||||
|  | ||||
| gulp.task( | ||||
|   "analyze-app", | ||||
|   gulp.series( | ||||
|     async function setEnv() { | ||||
|       process.env.STATS = "1"; | ||||
|     }, | ||||
|     "clean", | ||||
|     "rspack-prod-app" | ||||
|     ...(env.isTestBuild() ? [] : ["compress-app"]), | ||||
|     gulp.parallel("gen-pages-app-prod", "gen-service-worker-app-prod") | ||||
|   ) | ||||
| ); | ||||
|   | ||||
| @@ -1,10 +1,12 @@ | ||||
| import gulp from "gulp"; | ||||
| import env from "../env.cjs"; | ||||
| import "./clean.js"; | ||||
| import "./entry-html.js"; | ||||
| import "./gather-static.js"; | ||||
| import "./rollup.js"; | ||||
| import "./service-worker.js"; | ||||
| import "./translations.js"; | ||||
| import "./rspack.js"; | ||||
| import "./webpack.js"; | ||||
|  | ||||
| gulp.task( | ||||
|   "develop-cast", | ||||
| @@ -17,7 +19,7 @@ gulp.task( | ||||
|     gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"), | ||||
|     "copy-static-cast", | ||||
|     "gen-pages-cast-dev", | ||||
|     "rspack-dev-server-cast" | ||||
|     env.useRollup() ? "rollup-dev-server-cast" : "webpack-dev-server-cast" | ||||
|   ) | ||||
| ); | ||||
|  | ||||
| @@ -31,7 +33,7 @@ gulp.task( | ||||
|     "translations-enable-merge-backend", | ||||
|     gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"), | ||||
|     "copy-static-cast", | ||||
|     "rspack-prod-cast", | ||||
|     env.useRollup() ? "rollup-prod-cast" : "webpack-prod-cast", | ||||
|     "gen-pages-cast-prod" | ||||
|   ) | ||||
| ); | ||||
|   | ||||
| @@ -38,14 +38,3 @@ gulp.task( | ||||
|     ]) | ||||
|   ) | ||||
| ); | ||||
|  | ||||
| gulp.task( | ||||
|   "clean-landing-page", | ||||
|   gulp.parallel("clean-translations", async () => | ||||
|     deleteSync([ | ||||
|       paths.landingPage_output_root, | ||||
|       paths.landingPage_build, | ||||
|       paths.build_dir, | ||||
|     ]) | ||||
|   ) | ||||
| ); | ||||
|   | ||||
| @@ -1,86 +1,16 @@ | ||||
| // Tasks to compress | ||||
|  | ||||
| import { constants } from "node:zlib"; | ||||
| import gulp from "gulp"; | ||||
| import brotli from "gulp-brotli"; | ||||
| import zopfli from "gulp-zopfli-green"; | ||||
| import paths from "../paths.cjs"; | ||||
|  | ||||
| const filesGlob = "*.{js,json,css,svg,xml}"; | ||||
| const brotliOptions = { | ||||
|   skipLarger: true, | ||||
|   params: { | ||||
|     [constants.BROTLI_PARAM_QUALITY]: constants.BROTLI_MAX_QUALITY, | ||||
|   }, | ||||
| }; | ||||
| const zopfliOptions = { threshold: 150 }; | ||||
|  | ||||
| const compressModern = (rootDir, modernDir, compress) => | ||||
| const compressDist = (rootDir) => | ||||
|   gulp | ||||
|     .src([`${modernDir}/**/${filesGlob}`, `${rootDir}/sw-modern.js`], { | ||||
|       base: rootDir, | ||||
|       allowEmpty: true, | ||||
|     }) | ||||
|     .pipe(compress === "zopfli" ? zopfli(zopfliOptions) : brotli(brotliOptions)) | ||||
|     .src([`${rootDir}/**/*.{js,json,css,svg}`]) | ||||
|     .pipe(zopfli(zopfliOptions)) | ||||
|     .pipe(gulp.dest(rootDir)); | ||||
|  | ||||
| const compressOther = (rootDir, modernDir, compress) => | ||||
|   gulp | ||||
|     .src( | ||||
|       [ | ||||
|         `${rootDir}/**/${filesGlob}`, | ||||
|         `!${modernDir}/**/${filesGlob}`, | ||||
|         `!${rootDir}/{sw-modern,service_worker}.js`, | ||||
|         `${rootDir}/{authorize,onboarding}.html`, | ||||
|       ], | ||||
|       { base: rootDir, allowEmpty: true } | ||||
|     ) | ||||
|     .pipe(compress === "zopfli" ? zopfli(zopfliOptions) : brotli(brotliOptions)) | ||||
|     .pipe(gulp.dest(rootDir)); | ||||
|  | ||||
| const compressAppModernBrotli = () => | ||||
|   compressModern(paths.app_output_root, paths.app_output_latest, "brotli"); | ||||
| const compressAppModernZopfli = () => | ||||
|   compressModern(paths.app_output_root, paths.app_output_latest, "zopfli"); | ||||
|  | ||||
| const compressHassioModernBrotli = () => | ||||
|   compressModern( | ||||
|     paths.hassio_output_root, | ||||
|     paths.hassio_output_latest, | ||||
|     "brotli" | ||||
|   ); | ||||
| const compressHassioModernZopfli = () => | ||||
|   compressModern( | ||||
|     paths.hassio_output_root, | ||||
|     paths.hassio_output_latest, | ||||
|     "zopfli" | ||||
|   ); | ||||
|  | ||||
| const compressAppOtherBrotli = () => | ||||
|   compressOther(paths.app_output_root, paths.app_output_latest, "brotli"); | ||||
| const compressAppOtherZopfli = () => | ||||
|   compressOther(paths.app_output_root, paths.app_output_latest, "zopfli"); | ||||
|  | ||||
| const compressHassioOtherBrotli = () => | ||||
|   compressOther(paths.hassio_output_root, paths.hassio_output_latest, "brotli"); | ||||
| const compressHassioOtherZopfli = () => | ||||
|   compressOther(paths.hassio_output_root, paths.hassio_output_latest, "zopfli"); | ||||
|  | ||||
| gulp.task( | ||||
|   "compress-app", | ||||
|   gulp.parallel( | ||||
|     compressAppModernBrotli, | ||||
|     compressAppOtherBrotli, | ||||
|     compressAppModernZopfli, | ||||
|     compressAppOtherZopfli | ||||
|   ) | ||||
| ); | ||||
| gulp.task( | ||||
|   "compress-hassio", | ||||
|   gulp.parallel( | ||||
|     compressHassioModernBrotli, | ||||
|     compressHassioOtherBrotli, | ||||
|     compressHassioModernZopfli, | ||||
|     compressHassioOtherZopfli | ||||
|   ) | ||||
| ); | ||||
| gulp.task("compress-app", () => compressDist(paths.app_output_root)); | ||||
| gulp.task("compress-hassio", () => compressDist(paths.hassio_output_root)); | ||||
|   | ||||
| @@ -1,11 +1,13 @@ | ||||
| import gulp from "gulp"; | ||||
| import env from "../env.cjs"; | ||||
| import "./clean.js"; | ||||
| import "./entry-html.js"; | ||||
| import "./gather-static.js"; | ||||
| import "./gen-icons-json.js"; | ||||
| import "./rollup.js"; | ||||
| import "./service-worker.js"; | ||||
| import "./translations.js"; | ||||
| import "./rspack.js"; | ||||
| import "./webpack.js"; | ||||
|  | ||||
| gulp.task( | ||||
|   "develop-demo", | ||||
| @@ -22,7 +24,7 @@ gulp.task( | ||||
|       "build-locale-data" | ||||
|     ), | ||||
|     "copy-static-demo", | ||||
|     "rspack-dev-server-demo" | ||||
|     env.useRollup() ? "rollup-dev-server-demo" : "webpack-dev-server-demo" | ||||
|   ) | ||||
| ); | ||||
|  | ||||
| @@ -37,18 +39,7 @@ gulp.task( | ||||
|     "translations-enable-merge-backend", | ||||
|     gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"), | ||||
|     "copy-static-demo", | ||||
|     "rspack-prod-demo", | ||||
|     env.useRollup() ? "rollup-prod-demo" : "webpack-prod-demo", | ||||
|     "gen-pages-demo-prod" | ||||
|   ) | ||||
| ); | ||||
|  | ||||
| gulp.task( | ||||
|   "analyze-demo", | ||||
|   gulp.series( | ||||
|     async function setEnv() { | ||||
|       process.env.STATS = "1"; | ||||
|     }, | ||||
|     "clean", | ||||
|     "rspack-prod-demo" | ||||
|   ) | ||||
| ); | ||||
|   | ||||
| @@ -13,7 +13,7 @@ const srcMeta = "src/translations/translationMetadata.json"; | ||||
| const encoding = "utf8"; | ||||
|  | ||||
| function hasHtml(data) { | ||||
|   return /<\S*>/i.test(data); | ||||
|   return /<[a-z][\s\S]*>/i.test(data); | ||||
| } | ||||
|  | ||||
| function recursiveCheckHasHtml(file, data, errors, recKey) { | ||||
| @@ -127,7 +127,6 @@ gulp.task("fetch-lokalise", async function () { | ||||
|           replace_breaks: false, | ||||
|           json_unescaped_slashes: true, | ||||
|           export_empty_as: "skip", | ||||
|           filter_data: ["verified"], | ||||
|         }) | ||||
|         .then((download) => fetch(download.bundle_url)) | ||||
|         .then((response) => { | ||||
| @@ -162,10 +161,6 @@ gulp.task("fetch-lokalise", async function () { | ||||
|             }) | ||||
|           ); | ||||
|         }) | ||||
|         .catch((err) => { | ||||
|           console.error(err); | ||||
|           throw err; | ||||
|         }) | ||||
|     ) | ||||
|   ); | ||||
| }); | ||||
|   | ||||
| @@ -1,74 +1,28 @@ | ||||
| // Tasks to generate entry HTML | ||||
|  | ||||
| import { | ||||
|   applyVersionsToRegexes, | ||||
|   compileRegex, | ||||
|   getPreUserAgentRegexes, | ||||
| } from "browserslist-useragent-regexp"; | ||||
| import fs from "fs-extra"; | ||||
| import gulp from "gulp"; | ||||
| import { minify } from "html-minifier-terser"; | ||||
| import template from "lodash.template"; | ||||
| import { dirname, extname, resolve } from "node:path"; | ||||
| import path from "path"; | ||||
| import { htmlMinifierOptions, terserOptions } from "../bundle.cjs"; | ||||
| import env from "../env.cjs"; | ||||
| import paths from "../paths.cjs"; | ||||
|  | ||||
| // macOS companion app has no way to obtain the Safari version used by WKWebView, | ||||
| // and it is not in the default user agent string. So we add an additional regex | ||||
| // to serve modern based on a minimum macOS version. We take the minimum Safari | ||||
| // major version from browserslist and manually map that to a supported macOS | ||||
| // version. Note this assumes the user has kept Safari updated. | ||||
| const HA_MACOS_REGEX = | ||||
|   /Home Assistant\/[\d.]+ \(.+; macOS (\d+)\.(\d+)(?:\.(\d+))?\)/; | ||||
| const SAFARI_TO_MACOS = { | ||||
|   15: [10, 15, 0], | ||||
|   16: [11, 0, 0], | ||||
|   17: [12, 0, 0], | ||||
|   18: [13, 0, 0], | ||||
| }; | ||||
|  | ||||
| const getCommonTemplateVars = () => { | ||||
|   const browserRegexes = getPreUserAgentRegexes({ | ||||
|     env: "modern", | ||||
|     allowHigherVersions: true, | ||||
|     mobileToDesktop: true, | ||||
|     throwOnMissing: true, | ||||
|   }); | ||||
|   const minSafariVersion = browserRegexes.find( | ||||
|     (regex) => regex.family === "safari" | ||||
|   )?.matchedVersions[0][0]; | ||||
|   const minMacOSVersion = SAFARI_TO_MACOS[minSafariVersion]; | ||||
|   if (!minMacOSVersion) { | ||||
|     throw Error( | ||||
|       `Could not find minimum MacOS version for Safari ${minSafariVersion}.` | ||||
|     ); | ||||
|   } | ||||
|   const haMacOSRegex = applyVersionsToRegexes( | ||||
|     [ | ||||
|       { | ||||
|         family: "ha_macos", | ||||
|         regex: HA_MACOS_REGEX, | ||||
|         matchedVersions: [minMacOSVersion], | ||||
|         requestVersions: [minMacOSVersion], | ||||
|       }, | ||||
|     ], | ||||
|     { ignorePatch: true, allowHigherVersions: true } | ||||
|   ); | ||||
|   return { | ||||
|     modernRegex: compileRegex(browserRegexes.concat(haMacOSRegex)).toString(), | ||||
|     hassUrl: process.env.HASS_URL || "", | ||||
|   }; | ||||
| }; | ||||
|  | ||||
| const renderTemplate = (templateFile, data = {}) => { | ||||
|   const compiled = template( | ||||
|     fs.readFileSync(templateFile, { encoding: "utf-8" }) | ||||
|   ); | ||||
|   return compiled({ | ||||
|     ...data, | ||||
|     useRollup: env.useRollup(), | ||||
|     useWDS: env.useWDS(), | ||||
|     // Resolve any child/nested templates relative to the parent and pass the same data | ||||
|     renderTemplate: (childTemplate) => | ||||
|       renderTemplate(resolve(dirname(templateFile), childTemplate), data), | ||||
|       renderTemplate( | ||||
|         path.resolve(path.dirname(templateFile), childTemplate), | ||||
|         data | ||||
|       ), | ||||
|   }); | ||||
| }; | ||||
|  | ||||
| @@ -91,32 +45,36 @@ const minifyHtml = (content, ext) => { | ||||
| }; | ||||
|  | ||||
| // Function to generate a dev task for each project's configuration | ||||
| // Note Currently WDS paths are hard-coded to only work for app | ||||
| const genPagesDevTask = | ||||
|   ( | ||||
|     pageEntries, | ||||
|     inputRoot, | ||||
|     outputRoot, | ||||
|     useWDS = false, | ||||
|     inputSub = "src/html", | ||||
|     publicRoot = "" | ||||
|   ) => | ||||
|   async () => { | ||||
|     const commonVars = getCommonTemplateVars(); | ||||
|     for (const [page, entries] of Object.entries(pageEntries)) { | ||||
|       const content = renderTemplate( | ||||
|         resolve(inputRoot, inputSub, `${page}.template`), | ||||
|         path.resolve(inputRoot, inputSub, `${page}.template`), | ||||
|         { | ||||
|           ...commonVars, | ||||
|           latestEntryJS: entries.map( | ||||
|             (entry) => `${publicRoot}/frontend_latest/${entry}.js` | ||||
|           latestEntryJS: entries.map((entry) => | ||||
|             useWDS | ||||
|               ? `http://localhost:8000/src/entrypoints/${entry}.ts` | ||||
|               : `${publicRoot}/frontend_latest/${entry}.js` | ||||
|           ), | ||||
|           es5EntryJS: entries.map( | ||||
|             (entry) => `${publicRoot}/frontend_es5/${entry}.js` | ||||
|           ), | ||||
|           latestCustomPanelJS: `${publicRoot}/frontend_latest/custom-panel.js`, | ||||
|           latestCustomPanelJS: useWDS | ||||
|             ? "http://localhost:8000/src/entrypoints/custom-panel.ts" | ||||
|             : `${publicRoot}/frontend_latest/custom-panel.js`, | ||||
|           es5CustomPanelJS: `${publicRoot}/frontend_es5/custom-panel.js`, | ||||
|         } | ||||
|       ); | ||||
|       fs.outputFileSync(resolve(outputRoot, page), content); | ||||
|       fs.outputFileSync(path.resolve(outputRoot, page), content); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
| @@ -133,18 +91,16 @@ const genPagesProdTask = | ||||
|   ) => | ||||
|   async () => { | ||||
|     const latestManifest = fs.readJsonSync( | ||||
|       resolve(outputLatest, "manifest.json") | ||||
|       path.resolve(outputLatest, "manifest.json") | ||||
|     ); | ||||
|     const es5Manifest = outputES5 | ||||
|       ? fs.readJsonSync(resolve(outputES5, "manifest.json")) | ||||
|       ? fs.readJsonSync(path.resolve(outputES5, "manifest.json")) | ||||
|       : {}; | ||||
|     const commonVars = getCommonTemplateVars(); | ||||
|     const minifiedHTML = []; | ||||
|     for (const [page, entries] of Object.entries(pageEntries)) { | ||||
|       const content = renderTemplate( | ||||
|         resolve(inputRoot, inputSub, `${page}.template`), | ||||
|         path.resolve(inputRoot, inputSub, `${page}.template`), | ||||
|         { | ||||
|           ...commonVars, | ||||
|           latestEntryJS: entries.map((entry) => latestManifest[`${entry}.js`]), | ||||
|           es5EntryJS: entries.map((entry) => es5Manifest[`${entry}.js`]), | ||||
|           latestCustomPanelJS: latestManifest["custom-panel.js"], | ||||
| @@ -152,8 +108,8 @@ const genPagesProdTask = | ||||
|         } | ||||
|       ); | ||||
|       minifiedHTML.push( | ||||
|         minifyHtml(content, extname(page)).then((minified) => | ||||
|           fs.outputFileSync(resolve(outputRoot, page), minified) | ||||
|         minifyHtml(content, path.extname(page)).then((minified) => | ||||
|           fs.outputFileSync(path.resolve(outputRoot, page), minified) | ||||
|         ) | ||||
|       ); | ||||
|     } | ||||
| @@ -169,14 +125,19 @@ const APP_PAGE_ENTRIES = { | ||||
|  | ||||
| gulp.task( | ||||
|   "gen-pages-app-dev", | ||||
|   genPagesDevTask(APP_PAGE_ENTRIES, paths.root_dir, paths.app_output_root) | ||||
|   genPagesDevTask( | ||||
|     APP_PAGE_ENTRIES, | ||||
|     paths.polymer_dir, | ||||
|     paths.app_output_root, | ||||
|     env.useWDS() | ||||
|   ) | ||||
| ); | ||||
|  | ||||
| gulp.task( | ||||
|   "gen-pages-app-prod", | ||||
|   genPagesProdTask( | ||||
|     APP_PAGE_ENTRIES, | ||||
|     paths.root_dir, | ||||
|     paths.polymer_dir, | ||||
|     paths.app_output_root, | ||||
|     paths.app_output_latest, | ||||
|     paths.app_output_es5 | ||||
| @@ -245,28 +206,6 @@ gulp.task( | ||||
|   ) | ||||
| ); | ||||
|  | ||||
| const LANDING_PAGE_PAGE_ENTRIES = { "index.html": ["entrypoint"] }; | ||||
|  | ||||
| gulp.task( | ||||
|   "gen-pages-landing-page-dev", | ||||
|   genPagesDevTask( | ||||
|     LANDING_PAGE_PAGE_ENTRIES, | ||||
|     paths.landingPage_dir, | ||||
|     paths.landingPage_output_root | ||||
|   ) | ||||
| ); | ||||
|  | ||||
| gulp.task( | ||||
|   "gen-pages-landing-page-prod", | ||||
|   genPagesProdTask( | ||||
|     LANDING_PAGE_PAGE_ENTRIES, | ||||
|     paths.landingPage_dir, | ||||
|     paths.landingPage_output_root, | ||||
|     paths.landingPage_output_latest, | ||||
|     paths.landingPage_output_es5 | ||||
|   ) | ||||
| ); | ||||
|  | ||||
| const HASSIO_PAGE_ENTRIES = { "entrypoint.js": ["entrypoint"] }; | ||||
|  | ||||
| gulp.task( | ||||
| @@ -275,6 +214,7 @@ gulp.task( | ||||
|     HASSIO_PAGE_ENTRIES, | ||||
|     paths.hassio_dir, | ||||
|     paths.hassio_output_root, | ||||
|     undefined, | ||||
|     "src", | ||||
|     paths.hassio_publicPath | ||||
|   ) | ||||
|   | ||||
| @@ -9,7 +9,7 @@ import gulp from "gulp"; | ||||
| import jszip from "jszip"; | ||||
| import path from "path"; | ||||
| import process from "process"; | ||||
| import { extract } from "tar"; | ||||
| import tar from "tar"; | ||||
|  | ||||
| const MAX_AGE = 24; // hours | ||||
| const OWNER = "home-assistant"; | ||||
| @@ -66,7 +66,7 @@ gulp.task("fetch-nightly-translations", async function () { | ||||
|       tokenAuth = JSON.parse(await readFile(TOKEN_FILE, "utf-8")); | ||||
|     } catch { | ||||
|       if (!allowTokenSetup) { | ||||
|         console.log("No token found so build will continue with English only"); | ||||
|         console.log("No token found so  build wil continue with English only"); | ||||
|         return; | ||||
|       } | ||||
|       const auth = createOAuthDeviceAuth({ | ||||
| @@ -156,7 +156,7 @@ gulp.task("fetch-nightly-translations", async function () { | ||||
|   console.log("Unpacking downloaded translations..."); | ||||
|   const zip = await jszip.loadAsync(downloadResponse.data); | ||||
|   await deleteCurrent; | ||||
|   const extractStream = zip.file(/.*/)[0].nodeStream().pipe(extract()); | ||||
|   const extractStream = zip.file(/.*/)[0].nodeStream().pipe(tar.extract()); | ||||
|   await new Promise((resolve, reject) => { | ||||
|     extractStream.on("close", resolve).on("error", reject); | ||||
|   }); | ||||
|   | ||||
| @@ -4,14 +4,16 @@ import gulp from "gulp"; | ||||
| import yaml from "js-yaml"; | ||||
| import { marked } from "marked"; | ||||
| import path from "path"; | ||||
| import env from "../env.cjs"; | ||||
| import paths from "../paths.cjs"; | ||||
| import "./clean.js"; | ||||
| import "./entry-html.js"; | ||||
| import "./gather-static.js"; | ||||
| import "./gen-icons-json.js"; | ||||
| import "./rollup.js"; | ||||
| import "./service-worker.js"; | ||||
| import "./translations.js"; | ||||
| import "./rspack.js"; | ||||
| import "./webpack.js"; | ||||
|  | ||||
| gulp.task("gather-gallery-pages", async function gatherPages() { | ||||
|   const pageDir = path.resolve(paths.gallery_dir, "src/pages"); | ||||
| @@ -156,7 +158,9 @@ gulp.task( | ||||
|     "copy-static-gallery", | ||||
|     "gen-pages-gallery-dev", | ||||
|     gulp.parallel( | ||||
|       "rspack-dev-server-gallery", | ||||
|       env.useRollup() | ||||
|         ? "rollup-dev-server-gallery" | ||||
|         : "webpack-dev-server-gallery", | ||||
|       async function watchMarkdownFiles() { | ||||
|         gulp.watch( | ||||
|           [ | ||||
| @@ -185,7 +189,7 @@ gulp.task( | ||||
|       "gather-gallery-pages" | ||||
|     ), | ||||
|     "copy-static-gallery", | ||||
|     "rspack-prod-gallery", | ||||
|     env.useRollup() ? "rollup-prod-gallery" : "webpack-prod-gallery", | ||||
|     "gen-pages-gallery-prod" | ||||
|   ) | ||||
| ); | ||||
|   | ||||
| @@ -6,8 +6,8 @@ import path from "path"; | ||||
| import paths from "../paths.cjs"; | ||||
|  | ||||
| const npmPath = (...parts) => | ||||
|   path.resolve(paths.root_dir, "node_modules", ...parts); | ||||
| const polyPath = (...parts) => path.resolve(paths.root_dir, ...parts); | ||||
|   path.resolve(paths.polymer_dir, "node_modules", ...parts); | ||||
| const polyPath = (...parts) => path.resolve(paths.polymer_dir, ...parts); | ||||
|  | ||||
| const copyFileDir = (fromFile, toDir) => | ||||
|   fs.copySync(fromFile, path.join(toDir, path.basename(fromFile))); | ||||
| @@ -59,17 +59,12 @@ function copyPolyfills(staticDir) { | ||||
|     npmPath("@webcomponents/webcomponentsjs/webcomponents-bundle.js.map"), | ||||
|     staticPath("polyfills/") | ||||
|   ); | ||||
|   // Lit polyfill support | ||||
|   fs.copySync( | ||||
|     npmPath("lit/polyfill-support.js"), | ||||
|     path.join(staticPath("polyfills/"), "lit-polyfill-support.js") | ||||
|   ); | ||||
| } | ||||
|  | ||||
|   // dialog-polyfill css | ||||
|   copyFileDir( | ||||
|     npmPath("dialog-polyfill/dialog-polyfill.css"), | ||||
|     staticPath("polyfills/") | ||||
|   ); | ||||
| function copyLoaderJS(staticDir) { | ||||
|   const staticPath = genStaticPath(staticDir); | ||||
|   copyFileDir(npmPath("systemjs/dist/s.min.js"), staticPath("js")); | ||||
|   copyFileDir(npmPath("systemjs/dist/s.min.js.map"), staticPath("js")); | ||||
| } | ||||
|  | ||||
| function copyFonts(staticDir) { | ||||
| @@ -95,24 +90,12 @@ function copyMapPanel(staticDir) { | ||||
|     npmPath("leaflet/dist/leaflet.css"), | ||||
|     staticPath("images/leaflet/") | ||||
|   ); | ||||
|   copyFileDir( | ||||
|     npmPath("leaflet.markercluster/dist/MarkerCluster.css"), | ||||
|     staticPath("images/leaflet/") | ||||
|   ); | ||||
|   fs.copySync( | ||||
|     npmPath("leaflet/dist/images"), | ||||
|     staticPath("images/leaflet/images/") | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function copyZXingWasm(staticDir) { | ||||
|   const staticPath = genStaticPath(staticDir); | ||||
|   copyFileDir( | ||||
|     npmPath("zxing-wasm/dist/reader/zxing_reader.wasm"), | ||||
|     staticPath("js") | ||||
|   ); | ||||
| } | ||||
|  | ||||
| gulp.task("copy-locale-data", async () => { | ||||
|   const staticDir = paths.app_output_static; | ||||
|   copyLocaleData(staticDir); | ||||
| @@ -128,11 +111,6 @@ gulp.task("copy-translations-supervisor", async () => { | ||||
|   copyTranslations(staticDir); | ||||
| }); | ||||
|  | ||||
| gulp.task("copy-translations-landing-page", async () => { | ||||
|   const staticDir = paths.landingPage_output_static; | ||||
|   copyTranslations(staticDir); | ||||
| }); | ||||
|  | ||||
| gulp.task("copy-static-supervisor", async () => { | ||||
|   const staticDir = paths.hassio_output_static; | ||||
|   copyLocaleData(staticDir); | ||||
| @@ -143,6 +121,8 @@ gulp.task("copy-static-app", async () => { | ||||
|   const staticDir = paths.app_output_static; | ||||
|   // Basic static files | ||||
|   fs.copySync(polyPath("public"), paths.app_output_root); | ||||
|  | ||||
|   copyLoaderJS(staticDir); | ||||
|   copyPolyfills(staticDir); | ||||
|   copyFonts(staticDir); | ||||
|   copyTranslations(staticDir); | ||||
| @@ -153,7 +133,6 @@ gulp.task("copy-static-app", async () => { | ||||
|   copyMapPanel(staticDir); | ||||
|  | ||||
|   // Qr Scanner assets | ||||
|   copyZXingWasm(staticDir); | ||||
|   copyQrScannerWorker(staticDir); | ||||
| }); | ||||
|  | ||||
| @@ -165,6 +144,8 @@ gulp.task("copy-static-demo", async () => { | ||||
|   ); | ||||
|   // Copy demo static files | ||||
|   fs.copySync(path.resolve(paths.demo_dir, "public"), paths.demo_output_root); | ||||
|  | ||||
|   copyLoaderJS(paths.demo_output_static); | ||||
|   copyPolyfills(paths.demo_output_static); | ||||
|   copyMapPanel(paths.demo_output_static); | ||||
|   copyFonts(paths.demo_output_static); | ||||
| @@ -178,6 +159,8 @@ gulp.task("copy-static-cast", async () => { | ||||
|   fs.copySync(polyPath("public/static"), paths.cast_output_static); | ||||
|   // Copy cast static files | ||||
|   fs.copySync(path.resolve(paths.cast_dir, "public"), paths.cast_output_root); | ||||
|  | ||||
|   copyLoaderJS(paths.cast_output_static); | ||||
|   copyPolyfills(paths.cast_output_static); | ||||
|   copyMapPanel(paths.cast_output_static); | ||||
|   copyFonts(paths.cast_output_static); | ||||
| @@ -201,14 +184,3 @@ gulp.task("copy-static-gallery", async () => { | ||||
|   copyLocaleData(paths.gallery_output_static); | ||||
|   copyMdiIcons(paths.gallery_output_static); | ||||
| }); | ||||
|  | ||||
| gulp.task("copy-static-landing-page", async () => { | ||||
|   // Copy landing-page static files | ||||
|   fs.copySync( | ||||
|     path.resolve(paths.landingPage_dir, "public"), | ||||
|     paths.landingPage_output_root | ||||
|   ); | ||||
|  | ||||
|   copyFonts(paths.landingPage_output_static); | ||||
|   copyTranslations(paths.landingPage_output_static); | ||||
| }); | ||||
|   | ||||
| @@ -5,8 +5,9 @@ import "./compress.js"; | ||||
| import "./entry-html.js"; | ||||
| import "./gather-static.js"; | ||||
| import "./gen-icons-json.js"; | ||||
| import "./rollup.js"; | ||||
| import "./translations.js"; | ||||
| import "./rspack.js"; | ||||
| import "./webpack.js"; | ||||
|  | ||||
| gulp.task( | ||||
|   "develop-hassio", | ||||
| @@ -21,7 +22,7 @@ gulp.task( | ||||
|     "copy-translations-supervisor", | ||||
|     "build-locale-data", | ||||
|     "copy-static-supervisor", | ||||
|     "rspack-watch-hassio" | ||||
|     env.useRollup() ? "rollup-watch-hassio" : "webpack-watch-hassio" | ||||
|   ) | ||||
| ); | ||||
|  | ||||
| @@ -37,7 +38,7 @@ gulp.task( | ||||
|     "copy-translations-supervisor", | ||||
|     "build-locale-data", | ||||
|     "copy-static-supervisor", | ||||
|     "rspack-prod-hassio", | ||||
|     env.useRollup() ? "rollup-prod-hassio" : "webpack-prod-hassio", | ||||
|     "gen-pages-hassio-prod", | ||||
|     ...// Don't compress running tests | ||||
|     (env.isTestBuild() ? [] : ["compress-hassio"]) | ||||
|   | ||||
| @@ -1,17 +0,0 @@ | ||||
| import "./app.js"; | ||||
| import "./cast.js"; | ||||
| import "./clean.js"; | ||||
| import "./compress.js"; | ||||
| import "./demo.js"; | ||||
| import "./download-translations.js"; | ||||
| import "./entry-html.js"; | ||||
| import "./fetch-nightly-translations.js"; | ||||
| import "./gallery.js"; | ||||
| import "./gather-static.js"; | ||||
| import "./gen-icons-json.js"; | ||||
| import "./hassio.js"; | ||||
| import "./landing-page.js"; | ||||
| import "./locale-data.js"; | ||||
| import "./rspack.js"; | ||||
| import "./service-worker.js"; | ||||
| import "./translations.js"; | ||||
| @@ -1,41 +0,0 @@ | ||||
| import gulp from "gulp"; | ||||
| import "./clean.js"; | ||||
| import "./compress.js"; | ||||
| import "./entry-html.js"; | ||||
| import "./gather-static.js"; | ||||
| import "./gen-icons-json.js"; | ||||
| import "./translations.js"; | ||||
| import "./rspack.js"; | ||||
|  | ||||
| gulp.task( | ||||
|   "develop-landing-page", | ||||
|   gulp.series( | ||||
|     async function setEnv() { | ||||
|       process.env.NODE_ENV = "development"; | ||||
|     }, | ||||
|     "clean-landing-page", | ||||
|     "translations-enable-merge-backend", | ||||
|     "build-landing-page-translations", | ||||
|     "copy-translations-landing-page", | ||||
|     "build-locale-data", | ||||
|     "copy-static-landing-page", | ||||
|     "gen-pages-landing-page-dev", | ||||
|     "rspack-watch-landing-page" | ||||
|   ) | ||||
| ); | ||||
|  | ||||
| gulp.task( | ||||
|   "build-landing-page", | ||||
|   gulp.series( | ||||
|     async function setEnv() { | ||||
|       process.env.NODE_ENV = "production"; | ||||
|     }, | ||||
|     "clean-landing-page", | ||||
|     "build-landing-page-translations", | ||||
|     "copy-translations-landing-page", | ||||
|     "build-locale-data", | ||||
|     "copy-static-landing-page", | ||||
|     "rspack-prod-landing-page", | ||||
|     "gen-pages-landing-page-prod" | ||||
|   ) | ||||
| ); | ||||
| @@ -1,57 +1,51 @@ | ||||
| import { deleteSync } from "del"; | ||||
| import { mkdir, readFile, writeFile } from "fs/promises"; | ||||
| import gulp from "gulp"; | ||||
| import { join, resolve } from "node:path"; | ||||
| import path from "path"; | ||||
| import paths from "../paths.cjs"; | ||||
|  | ||||
| const formatjsDir = join(paths.root_dir, "node_modules", "@formatjs"); | ||||
| const outDir = join(paths.build_dir, "locale-data"); | ||||
| const outDir = path.join(paths.build_dir, "locale-data"); | ||||
|  | ||||
| const INTL_POLYFILLS = { | ||||
| const INTL_PACKAGES = { | ||||
|   "intl-relativetimeformat": "RelativeTimeFormat", | ||||
|   "intl-datetimeformat": "DateTimeFormat", | ||||
|   "intl-numberformat": "NumberFormat", | ||||
|   "intl-displaynames": "DisplayNames", | ||||
|   "intl-listformat": "ListFormat", | ||||
|   "intl-numberformat": "NumberFormat", | ||||
|   "intl-relativetimeformat": "RelativeTimeFormat", | ||||
| }; | ||||
|  | ||||
| const convertToJSON = async ( | ||||
|   pkg, | ||||
|   lang, | ||||
|   subDir = "locale-data", | ||||
|   addFunc = "__addLocaleData", | ||||
|   skipMissing = true | ||||
| ) => { | ||||
| const convertToJSON = async (pkg, lang) => { | ||||
|   let localeData; | ||||
|   try { | ||||
|     // use "pt" for "pt-BR", because "pt-BR" is unsupported by @formatjs | ||||
|     const language = lang === "pt-BR" ? "pt" : lang; | ||||
|  | ||||
|     localeData = await readFile( | ||||
|       join(formatjsDir, pkg, subDir, `${language}.js`), | ||||
|       path.resolve( | ||||
|         paths.polymer_dir, | ||||
|         `node_modules/@formatjs/${pkg}/locale-data/${lang}.js` | ||||
|       ), | ||||
|       "utf-8" | ||||
|     ); | ||||
|   } catch (e) { | ||||
|     // Ignore if language is missing (i.e. not supported by @formatjs) | ||||
|     if (e.code === "ENOENT" && skipMissing) { | ||||
|       console.warn(`Skipped missing data for language ${lang} from ${pkg}`); | ||||
|     if (e.code === "ENOENT") { | ||||
|       return; | ||||
|     } else { | ||||
|       throw e; | ||||
|     } | ||||
|     throw e; | ||||
|   } | ||||
|   // Convert to JSON | ||||
|   const obj = INTL_POLYFILLS[pkg]; | ||||
|   const dataRegex = new RegExp( | ||||
|     `Intl\\.${obj}\\.${addFunc}\\((?<data>.*)\\)`, | ||||
|     "s" | ||||
|   ); | ||||
|   localeData = localeData.match(dataRegex)?.groups?.data; | ||||
|   if (!localeData) { | ||||
|     throw Error(`Failed to extract data for language ${lang} from ${pkg}`); | ||||
|   } | ||||
|   const className = INTL_PACKAGES[pkg]; | ||||
|   localeData = localeData | ||||
|     .replace( | ||||
|       new RegExp( | ||||
|         `\\/\\*\\s*@generated\\s*\\*\\/\\s*\\/\\/\\s*prettier-ignore\\s*if\\s*\\(Intl\\.${className}\\s*&&\\s*typeof\\s*Intl\\.${className}\\.__addLocaleData\\s*===\\s*'function'\\)\\s*{\\s*Intl\\.${className}\\.__addLocaleData\\(`, | ||||
|         "im" | ||||
|       ), | ||||
|       "" | ||||
|     ) | ||||
|     .replace(/\)\s*}/im, ""); | ||||
|   // Parse to validate JSON, then stringify to minify | ||||
|   localeData = JSON.stringify(JSON.parse(localeData)); | ||||
|   await writeFile(join(outDir, `${pkg}/${lang}.json`), localeData); | ||||
|   await writeFile(path.join(outDir, `${pkg}/${lang}.json`), localeData); | ||||
| }; | ||||
|  | ||||
| gulp.task("clean-locale-data", async () => deleteSync([outDir])); | ||||
| @@ -59,27 +53,17 @@ gulp.task("clean-locale-data", async () => deleteSync([outDir])); | ||||
| gulp.task("create-locale-data", async () => { | ||||
|   const translationMeta = JSON.parse( | ||||
|     await readFile( | ||||
|       resolve(paths.translations_src, "translationMetadata.json"), | ||||
|       path.resolve(paths.translations_src, "translationMetadata.json"), | ||||
|       "utf-8" | ||||
|     ) | ||||
|   ); | ||||
|   const conversions = []; | ||||
|   for (const pkg of Object.keys(INTL_POLYFILLS)) { | ||||
|     // eslint-disable-next-line no-await-in-loop | ||||
|     await mkdir(join(outDir, pkg), { recursive: true }); | ||||
|   for (const pkg of Object.keys(INTL_PACKAGES)) { | ||||
|     await mkdir(path.join(outDir, pkg), { recursive: true }); | ||||
|     for (const lang of Object.keys(translationMeta)) { | ||||
|       conversions.push(convertToJSON(pkg, lang)); | ||||
|     } | ||||
|   } | ||||
|   conversions.push( | ||||
|     convertToJSON( | ||||
|       "intl-datetimeformat", | ||||
|       "add-all-tz", | ||||
|       ".", | ||||
|       "__addTZData", | ||||
|       false | ||||
|     ) | ||||
|   ); | ||||
|   await Promise.all(conversions); | ||||
| }); | ||||
|  | ||||
|   | ||||
							
								
								
									
										147
									
								
								build-scripts/gulp/rollup.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										147
									
								
								build-scripts/gulp/rollup.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,147 @@ | ||||
| // Tasks to run Rollup | ||||
|  | ||||
| import log from "fancy-log"; | ||||
| import gulp from "gulp"; | ||||
| import http from "http"; | ||||
| import open from "open"; | ||||
| import path from "path"; | ||||
| import { rollup } from "rollup"; | ||||
| import handler from "serve-handler"; | ||||
| import paths from "../paths.cjs"; | ||||
| import rollupConfig from "../rollup.cjs"; | ||||
|  | ||||
| const bothBuilds = (createConfigFunc, params) => | ||||
|   gulp.series( | ||||
|     async function buildLatest() { | ||||
|       await buildRollup( | ||||
|         createConfigFunc({ | ||||
|           ...params, | ||||
|           latestBuild: true, | ||||
|         }) | ||||
|       ); | ||||
|     }, | ||||
|     async function buildES5() { | ||||
|       await buildRollup( | ||||
|         createConfigFunc({ | ||||
|           ...params, | ||||
|           latestBuild: false, | ||||
|         }) | ||||
|       ); | ||||
|     } | ||||
|   ); | ||||
|  | ||||
| function createServer(serveOptions) { | ||||
|   const server = http.createServer((request, response) => | ||||
|     handler(request, response, { | ||||
|       public: serveOptions.root, | ||||
|     }) | ||||
|   ); | ||||
|  | ||||
|   server.listen( | ||||
|     serveOptions.port, | ||||
|     serveOptions.networkAccess ? "0.0.0.0" : undefined, | ||||
|     () => { | ||||
|       log.info(`Available at http://localhost:${serveOptions.port}`); | ||||
|       open(`http://localhost:${serveOptions.port}`); | ||||
|     } | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function watchRollup(createConfig, extraWatchSrc = [], serveOptions = null) { | ||||
|   const { inputOptions, outputOptions } = createConfig({ | ||||
|     isProdBuild: false, | ||||
|     latestBuild: true, | ||||
|   }); | ||||
|  | ||||
|   const watcher = rollup.watch({ | ||||
|     ...inputOptions, | ||||
|     output: [outputOptions], | ||||
|     watch: { | ||||
|       include: ["src/**"] + extraWatchSrc, | ||||
|     }, | ||||
|   }); | ||||
|  | ||||
|   let startedHttp = false; | ||||
|  | ||||
|   watcher.on("event", (event) => { | ||||
|     if (event.code === "BUNDLE_END") { | ||||
|       log(`Build done @ ${new Date().toLocaleTimeString()}`); | ||||
|     } else if (event.code === "ERROR") { | ||||
|       log.error(event.error); | ||||
|     } else if (event.code === "END") { | ||||
|       if (startedHttp || !serveOptions) { | ||||
|         return; | ||||
|       } | ||||
|       startedHttp = true; | ||||
|       createServer(serveOptions); | ||||
|     } | ||||
|   }); | ||||
|  | ||||
|   gulp.watch( | ||||
|     path.join(paths.translations_src, "en.json"), | ||||
|     gulp.series("build-translations", "copy-translations-app") | ||||
|   ); | ||||
| } | ||||
|  | ||||
| async function buildRollup(config) { | ||||
|   const bundle = await rollup.rollup(config.inputOptions); | ||||
|   await bundle.write(config.outputOptions); | ||||
| } | ||||
|  | ||||
| gulp.task("rollup-watch-app", () => { | ||||
|   watchRollup(rollupConfig.createAppConfig); | ||||
| }); | ||||
|  | ||||
| gulp.task("rollup-watch-hassio", () => { | ||||
|   watchRollup(rollupConfig.createHassioConfig, ["hassio/src/**"]); | ||||
| }); | ||||
|  | ||||
| gulp.task("rollup-dev-server-demo", () => { | ||||
|   watchRollup(rollupConfig.createDemoConfig, ["demo/src/**"], { | ||||
|     root: paths.demo_output_root, | ||||
|     port: 8090, | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| gulp.task("rollup-dev-server-cast", () => { | ||||
|   watchRollup(rollupConfig.createCastConfig, ["cast/src/**"], { | ||||
|     root: paths.cast_output_root, | ||||
|     port: 8080, | ||||
|     networkAccess: true, | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| gulp.task("rollup-dev-server-gallery", () => { | ||||
|   watchRollup(rollupConfig.createGalleryConfig, ["gallery/src/**"], { | ||||
|     root: paths.gallery_output_root, | ||||
|     port: 8100, | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| gulp.task( | ||||
|   "rollup-prod-app", | ||||
|   bothBuilds(rollupConfig.createAppConfig, { isProdBuild: true }) | ||||
| ); | ||||
|  | ||||
| gulp.task( | ||||
|   "rollup-prod-demo", | ||||
|   bothBuilds(rollupConfig.createDemoConfig, { isProdBuild: true }) | ||||
| ); | ||||
|  | ||||
| gulp.task( | ||||
|   "rollup-prod-cast", | ||||
|   bothBuilds(rollupConfig.createCastConfig, { isProdBuild: true }) | ||||
| ); | ||||
|  | ||||
| gulp.task("rollup-prod-hassio", () => | ||||
|   bothBuilds(rollupConfig.createHassioConfig, { isProdBuild: true }) | ||||
| ); | ||||
|  | ||||
| gulp.task("rollup-prod-gallery", () => | ||||
|   buildRollup( | ||||
|     rollupConfig.createGalleryConfig({ | ||||
|       isProdBuild: true, | ||||
|       latestBuild: true, | ||||
|     }) | ||||
|   ) | ||||
| ); | ||||
| @@ -1,19 +1,20 @@ | ||||
| // Generate service workers | ||||
| // Generate service worker. | ||||
| // Based on manifest, create a file with the content as service_worker.js | ||||
|  | ||||
| import { deleteAsync } from "del"; | ||||
| import fs from "fs-extra"; | ||||
| import gulp from "gulp"; | ||||
| import { mkdir, readFile, symlink, writeFile } from "node:fs/promises"; | ||||
| import { basename, join, relative } from "node:path"; | ||||
| import { injectManifest } from "workbox-build"; | ||||
| import path from "path"; | ||||
| import sourceMapUrl from "source-map-url"; | ||||
| import workboxBuild from "workbox-build"; | ||||
| import paths from "../paths.cjs"; | ||||
|  | ||||
| const SW_MAP = { | ||||
|   [paths.app_output_latest]: "modern", | ||||
|   [paths.app_output_es5]: "legacy", | ||||
| }; | ||||
| const swDest = path.resolve(paths.app_output_root, "service_worker.js"); | ||||
|  | ||||
| const SW_DEV = | ||||
|   ` | ||||
| const writeSW = (content) => fs.outputFileSync(swDest, content.trim() + "\n"); | ||||
|  | ||||
| gulp.task("gen-service-worker-app-dev", (done) => { | ||||
|   writeSW( | ||||
|     ` | ||||
| console.debug('Service worker disabled in development'); | ||||
|  | ||||
| self.addEventListener('install', (event) => { | ||||
| @@ -21,67 +22,72 @@ self.addEventListener('install', (event) => { | ||||
|   // removing any prod service worker the dev might have running | ||||
|   self.skipWaiting(); | ||||
| }); | ||||
|   `.trim() + "\n"; | ||||
|  | ||||
| gulp.task("gen-service-worker-app-dev", async () => { | ||||
|   await mkdir(paths.app_output_root, { recursive: true }); | ||||
|   await Promise.all( | ||||
|     Object.values(SW_MAP).map((build) => | ||||
|       writeFile(join(paths.app_output_root, `sw-${build}.js`), SW_DEV, { | ||||
|         encoding: "utf-8", | ||||
|       }) | ||||
|     ) | ||||
|   ` | ||||
|   ); | ||||
|   done(); | ||||
| }); | ||||
|  | ||||
| gulp.task("gen-service-worker-app-prod", () => | ||||
|   Promise.all( | ||||
|     Object.entries(SW_MAP).map(async ([outPath, build]) => { | ||||
|       const manifest = JSON.parse( | ||||
|         await readFile(join(outPath, "manifest.json"), "utf-8") | ||||
|       ); | ||||
|       const swSrc = join(paths.app_output_root, manifest["service-worker.js"]); | ||||
|       const swDest = join(paths.app_output_root, `sw-${build}.js`); | ||||
|       const buildDir = relative(paths.app_output_root, outPath); | ||||
|       const { warnings } = await injectManifest({ | ||||
|         swSrc, | ||||
|         swDest, | ||||
|         injectionPoint: "__WB_MANIFEST__", | ||||
|         // Files that mach this pattern will be considered unique and skip revision check | ||||
|         // ignore JS files + translation files | ||||
|         dontCacheBustURLsMatching: new RegExp( | ||||
|           `(?:${buildDir}/.+|static/translations/.+)` | ||||
|         ), | ||||
|         globDirectory: paths.app_output_root, | ||||
|         globPatterns: [ | ||||
|           `${buildDir}/*.js`, | ||||
|           // Cache all English translations because we catch them as fallback | ||||
|           // Using pattern to match hash instead of * to avoid caching en-GB | ||||
|           // 'v' added as valid hash letter because in dev we hash with 'dev' | ||||
|           "static/translations/**/en-+([a-fv0-9]).json", | ||||
|           // Icon shown on splash screen | ||||
|           "static/icons/favicon-192x192.png", | ||||
|           "static/icons/favicon.ico", | ||||
|           // Common fonts | ||||
|           "static/fonts/roboto/Roboto-Light.woff2", | ||||
|           "static/fonts/roboto/Roboto-Medium.woff2", | ||||
|           "static/fonts/roboto/Roboto-Regular.woff2", | ||||
|           "static/fonts/roboto/Roboto-Bold.woff2", | ||||
|         ], | ||||
|         globIgnores: [`${buildDir}/service-worker*`], | ||||
|       }); | ||||
|       if (warnings.length > 0) { | ||||
|         console.warn( | ||||
|           `Problems while injecting ${build} service worker:\n`, | ||||
|           warnings.join("\n") | ||||
|         ); | ||||
|       } | ||||
|       await deleteAsync(`${swSrc}?(.map)`); | ||||
|       // Needed to install new SW from a cached HTML | ||||
|       if (build === "modern") { | ||||
|         const swOld = join(paths.app_output_root, "service_worker.js"); | ||||
|         await symlink(basename(swDest), swOld); | ||||
|       } | ||||
|     }) | ||||
|   ) | ||||
| ); | ||||
| gulp.task("gen-service-worker-app-prod", async () => { | ||||
|   // Read bundled source file | ||||
|   const bundleManifestLatest = fs.readJsonSync( | ||||
|     path.resolve(paths.app_output_latest, "manifest.json") | ||||
|   ); | ||||
|   let serviceWorkerContent = fs.readFileSync( | ||||
|     paths.app_output_root + bundleManifestLatest["service_worker.js"], | ||||
|     "utf-8" | ||||
|   ); | ||||
|  | ||||
|   // Delete old file from frontend_latest so manifest won't pick it up | ||||
|   fs.removeSync( | ||||
|     paths.app_output_root + bundleManifestLatest["service_worker.js"] | ||||
|   ); | ||||
|   fs.removeSync( | ||||
|     paths.app_output_root + bundleManifestLatest["service_worker.js.map"] | ||||
|   ); | ||||
|  | ||||
|   // Remove ES5 | ||||
|   const bundleManifestES5 = fs.readJsonSync( | ||||
|     path.resolve(paths.app_output_es5, "manifest.json") | ||||
|   ); | ||||
|   fs.removeSync(paths.app_output_root + bundleManifestES5["service_worker.js"]); | ||||
|   fs.removeSync( | ||||
|     paths.app_output_root + bundleManifestES5["service_worker.js.map"] | ||||
|   ); | ||||
|  | ||||
|   const workboxManifest = await workboxBuild.getManifest({ | ||||
|     // Files that mach this pattern will be considered unique and skip revision check | ||||
|     // ignore JS files + translation files | ||||
|     dontCacheBustURLsMatching: /(frontend_latest\/.+|static\/translations\/.+)/, | ||||
|  | ||||
|     globDirectory: paths.app_output_root, | ||||
|     globPatterns: [ | ||||
|       "frontend_latest/*.js", | ||||
|       // Cache all English translations because we catch them as fallback | ||||
|       // Using pattern to match hash instead of * to avoid caching en-GB | ||||
|       // 'v' added as valid hash letter because in dev we hash with 'dev' | ||||
|       "static/translations/**/en-+([a-fv0-9]).json", | ||||
|       // Icon shown on splash screen | ||||
|       "static/icons/favicon-192x192.png", | ||||
|       "static/icons/favicon.ico", | ||||
|       // Common fonts | ||||
|       "static/fonts/roboto/Roboto-Light.woff2", | ||||
|       "static/fonts/roboto/Roboto-Medium.woff2", | ||||
|       "static/fonts/roboto/Roboto-Regular.woff2", | ||||
|       "static/fonts/roboto/Roboto-Bold.woff2", | ||||
|     ], | ||||
|   }); | ||||
|  | ||||
|   for (const warning of workboxManifest.warnings) { | ||||
|     console.warn(warning); | ||||
|   } | ||||
|  | ||||
|   // remove source map and add WB manifest | ||||
|   serviceWorkerContent = sourceMapUrl.removeFrom(serviceWorkerContent); | ||||
|   serviceWorkerContent = serviceWorkerContent.replace( | ||||
|     "WB_MANIFEST", | ||||
|     JSON.stringify(workboxManifest.manifestEntries) | ||||
|   ); | ||||
|  | ||||
|   // Write new file to root | ||||
|   fs.writeFileSync(swDest, serviceWorkerContent); | ||||
| }); | ||||
|   | ||||
| @@ -1,103 +1,97 @@ | ||||
| /* eslint-disable max-classes-per-file */ | ||||
|  | ||||
| import { deleteAsync } from "del"; | ||||
| import { glob } from "glob"; | ||||
| import { createHash } from "crypto"; | ||||
| import { deleteSync } from "del"; | ||||
| import { | ||||
|   mkdirSync, | ||||
|   readdirSync, | ||||
|   readFileSync, | ||||
|   renameSync, | ||||
|   writeFile, | ||||
| } from "fs"; | ||||
| import gulp from "gulp"; | ||||
| import flatmap from "gulp-flatmap"; | ||||
| import transform from "gulp-json-transform"; | ||||
| import merge from "gulp-merge-json"; | ||||
| import rename from "gulp-rename"; | ||||
| import merge from "lodash.merge"; | ||||
| import { createHash } from "node:crypto"; | ||||
| import { mkdir, readFile } from "node:fs/promises"; | ||||
| import { basename, join } from "node:path"; | ||||
| import { PassThrough, Transform } from "node:stream"; | ||||
| import { finished } from "node:stream/promises"; | ||||
| import path from "path"; | ||||
| import vinylBuffer from "vinyl-buffer"; | ||||
| import source from "vinyl-source-stream"; | ||||
| import env from "../env.cjs"; | ||||
| import paths from "../paths.cjs"; | ||||
| import { mapFiles } from "../util.cjs"; | ||||
| import "./fetch-nightly-translations.js"; | ||||
|  | ||||
| const inFrontendDir = "translations/frontend"; | ||||
| const inBackendDir = "translations/backend"; | ||||
| const workDir = "build/translations"; | ||||
| const outDir = join(workDir, "output"); | ||||
| const EN_SRC = join(paths.translations_src, "en.json"); | ||||
| const TEST_LOCALE = "en-x-test"; | ||||
|  | ||||
| const fullDir = workDir + "/full"; | ||||
| const coreDir = workDir + "/core"; | ||||
| const outDir = workDir + "/output"; | ||||
| let mergeBackend = false; | ||||
|  | ||||
| gulp.task( | ||||
|   "translations-enable-merge-backend", | ||||
|   gulp.parallel(async () => { | ||||
|   gulp.parallel((done) => { | ||||
|     mergeBackend = true; | ||||
|     done(); | ||||
|   }, "allow-setup-fetch-nightly-translations") | ||||
| ); | ||||
|  | ||||
| // Transform stream to apply a function on Vinyl JSON files (buffer mode only). | ||||
| // The provided function can either return a new object, or an array of | ||||
| // [object, subdirectory] pairs for fragmentizing the JSON. | ||||
| class CustomJSON extends Transform { | ||||
|   constructor(func, reviver = null) { | ||||
|     super({ objectMode: true }); | ||||
|     this._func = func; | ||||
|     this._reviver = reviver; | ||||
|   } | ||||
| // Panel translations which should be split from the core translations. | ||||
| const TRANSLATION_FRAGMENTS = Object.keys( | ||||
|   JSON.parse( | ||||
|     readFileSync( | ||||
|       path.resolve(paths.polymer_dir, "src/translations/en.json"), | ||||
|       "utf-8" | ||||
|     ) | ||||
|   ).ui.panel | ||||
| ); | ||||
|  | ||||
|   // eslint-disable-next-line @typescript-eslint/naming-convention | ||||
|   async _transform(file, _, callback) { | ||||
|     let obj = JSON.parse(file.contents.toString(), this._reviver); | ||||
|     if (this._func) obj = this._func(obj, file.path); | ||||
|     for (const [outObj, dir] of Array.isArray(obj) ? obj : [[obj, ""]]) { | ||||
|       const outFile = file.clone({ contents: false }); | ||||
|       outFile.contents = Buffer.from(JSON.stringify(outObj)); | ||||
|       outFile.dirname += `/${dir}`; | ||||
|       this.push(outFile); | ||||
|     } | ||||
|     callback(null); | ||||
|   } | ||||
| } | ||||
|  | ||||
| // Transform stream to merge Vinyl JSON files (buffer mode only). | ||||
| class MergeJSON extends Transform { | ||||
|   _objects = []; | ||||
|  | ||||
|   constructor(stem, startObj = {}, reviver = null) { | ||||
|     super({ objectMode: true, allowHalfOpen: false }); | ||||
|     this._stem = stem; | ||||
|     this._startObj = structuredClone(startObj); | ||||
|     this._reviver = reviver; | ||||
|   } | ||||
|  | ||||
|   // eslint-disable-next-line @typescript-eslint/naming-convention | ||||
|   async _transform(file, _, callback) { | ||||
|     this._objects.push(JSON.parse(file.contents.toString(), this._reviver)); | ||||
|     if (!this._outFile) this._outFile = file.clone({ contents: false }); | ||||
|     callback(null); | ||||
|   } | ||||
|  | ||||
|   // eslint-disable-next-line @typescript-eslint/naming-convention | ||||
|   async _flush(callback) { | ||||
|     const mergedObj = merge(this._startObj, ...this._objects); | ||||
|     this._outFile.contents = Buffer.from(JSON.stringify(mergedObj)); | ||||
|     this._outFile.stem = this._stem; | ||||
|     callback(null, this._outFile); | ||||
|   } | ||||
| } | ||||
|  | ||||
| // Utility to flatten object keys to single level using separator | ||||
| const flatten = (data, prefix = "", sep = ".") => { | ||||
|   const output = {}; | ||||
|   for (const [key, value] of Object.entries(data)) { | ||||
|     if (typeof value === "object") { | ||||
|       Object.assign(output, flatten(value, prefix + key + sep, sep)); | ||||
| function recursiveFlatten(prefix, data) { | ||||
|   let output = {}; | ||||
|   Object.keys(data).forEach((key) => { | ||||
|     if (typeof data[key] === "object") { | ||||
|       output = { | ||||
|         ...output, | ||||
|         ...recursiveFlatten(prefix + key + ".", data[key]), | ||||
|       }; | ||||
|     } else { | ||||
|       output[prefix + key] = value; | ||||
|       output[prefix + key] = data[key]; | ||||
|     } | ||||
|   } | ||||
|   }); | ||||
|   return output; | ||||
| }; | ||||
| } | ||||
|  | ||||
| // Filter functions that can be passed directly to JSON.parse() | ||||
| const emptyReviver = (_key, value) => value || undefined; | ||||
| const testReviver = (_key, value) => | ||||
|   value && typeof value === "string" ? "TRANSLATED" : value; | ||||
| function flatten(data) { | ||||
|   return recursiveFlatten("", data); | ||||
| } | ||||
|  | ||||
| function emptyFilter(data) { | ||||
|   const newData = {}; | ||||
|   Object.keys(data).forEach((key) => { | ||||
|     if (data[key]) { | ||||
|       if (typeof data[key] === "object") { | ||||
|         newData[key] = emptyFilter(data[key]); | ||||
|       } else { | ||||
|         newData[key] = data[key]; | ||||
|       } | ||||
|     } | ||||
|   }); | ||||
|   return newData; | ||||
| } | ||||
|  | ||||
| function recursiveEmpty(data) { | ||||
|   const newData = {}; | ||||
|   Object.keys(data).forEach((key) => { | ||||
|     if (data[key]) { | ||||
|       if (typeof data[key] === "object") { | ||||
|         newData[key] = recursiveEmpty(data[key]); | ||||
|       } else { | ||||
|         newData[key] = "TRANSLATED"; | ||||
|       } | ||||
|     } | ||||
|   }); | ||||
|   return newData; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Replace Lokalise key placeholders with their actual values. | ||||
| @@ -106,44 +100,64 @@ const testReviver = (_key, value) => | ||||
|  * be included in src/translations/en.json, but still be usable while | ||||
|  * developing locally. | ||||
|  * | ||||
|  * @link https://docs.lokalise.com/en/articles/1400528-key-referencing | ||||
|  * @link https://docs.lokalise.co/article/KO5SZWLLsy-key-referencing | ||||
|  */ | ||||
| const KEY_REFERENCE = /\[%key:([^%]+)%\]/; | ||||
| const lokaliseTransform = (data, path, original = data) => { | ||||
| const re_key_reference = /\[%key:([^%]+)%\]/; | ||||
| function lokaliseTransform(data, original, file) { | ||||
|   const output = {}; | ||||
|   for (const [key, value] of Object.entries(data)) { | ||||
|     if (typeof value === "object") { | ||||
|       output[key] = lokaliseTransform(value, path, original); | ||||
|   Object.entries(data).forEach(([key, value]) => { | ||||
|     if (value instanceof Object) { | ||||
|       output[key] = lokaliseTransform(value, original, file); | ||||
|     } else { | ||||
|       output[key] = value.replace(KEY_REFERENCE, (_match, lokalise_key) => { | ||||
|       output[key] = value.replace(re_key_reference, (_match, lokalise_key) => { | ||||
|         const replace = lokalise_key.split("::").reduce((tr, k) => { | ||||
|           if (!tr) { | ||||
|             throw Error(`Invalid key placeholder ${lokalise_key} in ${path}`); | ||||
|             throw Error( | ||||
|               `Invalid key placeholder ${lokalise_key} in ${file.path}` | ||||
|             ); | ||||
|           } | ||||
|           return tr[k]; | ||||
|         }, original); | ||||
|         if (typeof replace !== "string") { | ||||
|           throw Error(`Invalid key placeholder ${lokalise_key} in ${path}`); | ||||
|           throw Error( | ||||
|             `Invalid key placeholder ${lokalise_key} in ${file.path}` | ||||
|           ); | ||||
|         } | ||||
|         return replace; | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
|   }); | ||||
|   return output; | ||||
| }; | ||||
| } | ||||
|  | ||||
| gulp.task("clean-translations", () => deleteAsync([workDir])); | ||||
| gulp.task("clean-translations", async () => deleteSync([workDir])); | ||||
|  | ||||
| const makeWorkDir = () => mkdir(workDir, { recursive: true }); | ||||
| gulp.task("ensure-translations-build-dir", async () => { | ||||
|   mkdirSync(workDir, { recursive: true }); | ||||
| }); | ||||
|  | ||||
| const createTestTranslation = () => | ||||
|   env.isProdBuild() | ||||
|     ? Promise.resolve() | ||||
|     : gulp | ||||
|         .src(EN_SRC) | ||||
|         .pipe(new CustomJSON(null, testReviver)) | ||||
|         .pipe(rename(`${TEST_LOCALE}.json`)) | ||||
|         .pipe(gulp.dest(workDir)); | ||||
| gulp.task("create-test-metadata", (cb) => { | ||||
|   writeFile( | ||||
|     workDir + "/testMetadata.json", | ||||
|     JSON.stringify({ | ||||
|       test: { | ||||
|         nativeName: "Test", | ||||
|       }, | ||||
|     }), | ||||
|     cb | ||||
|   ); | ||||
| }); | ||||
|  | ||||
| gulp.task( | ||||
|   "create-test-translation", | ||||
|   gulp.series("create-test-metadata", () => | ||||
|     gulp | ||||
|       .src(path.join(paths.translations_src, "en.json")) | ||||
|       .pipe(transform((data, _file) => recursiveEmpty(data))) | ||||
|       .pipe(rename("test.json")) | ||||
|       .pipe(gulp.dest(workDir)) | ||||
|   ) | ||||
| ); | ||||
|  | ||||
| /** | ||||
|  * This task will build a master translation file, to be used as the base for | ||||
| @@ -154,174 +168,286 @@ const createTestTranslation = () => | ||||
|  * project is buildable immediately after merging new translation keys, since | ||||
|  * the Lokalise update to translations/en.json will not happen immediately. | ||||
|  */ | ||||
| const createMasterTranslation = () => | ||||
|   gulp | ||||
|     .src([EN_SRC, ...(mergeBackend ? [`${inBackendDir}/en.json`] : [])]) | ||||
|     .pipe(new CustomJSON(lokaliseTransform)) | ||||
|     .pipe(new MergeJSON("en")) | ||||
|     .pipe(gulp.dest(workDir)); | ||||
| gulp.task("build-master-translation", () => { | ||||
|   const src = [path.join(paths.translations_src, "en.json")]; | ||||
|  | ||||
| const FRAGMENTS = ["base"]; | ||||
|  | ||||
| const setFragment = (fragment) => async () => { | ||||
|   FRAGMENTS[0] = fragment; | ||||
| }; | ||||
|  | ||||
| const panelFragment = (fragment) => | ||||
|   fragment !== "base" && | ||||
|   fragment !== "supervisor" && | ||||
|   fragment !== "landing-page"; | ||||
|  | ||||
| const HASHES = new Map(); | ||||
|  | ||||
| const createTranslations = async () => { | ||||
|   // Parse and store the master to avoid repeating this for each locale, then | ||||
|   // add the panel fragments when processing the app. | ||||
|   const enMaster = JSON.parse(await readFile(`${workDir}/en.json`, "utf-8")); | ||||
|   if (FRAGMENTS[0] === "base") { | ||||
|     FRAGMENTS.push(...Object.keys(enMaster.ui.panel)); | ||||
|   if (mergeBackend) { | ||||
|     src.push(path.join(inBackendDir, "en.json")); | ||||
|   } | ||||
|  | ||||
|   // The downstream pipeline is setup first.  It hashes the merged data for | ||||
|   // each locale, then fragmentizes and flattens the data for final output. | ||||
|   const translationFiles = await glob([ | ||||
|     `${inFrontendDir}/!(en).json`, | ||||
|     ...(env.isProdBuild() ? [] : [`${workDir}/${TEST_LOCALE}.json`]), | ||||
|   ]); | ||||
|   const hashStream = new Transform({ | ||||
|     objectMode: true, | ||||
|     transform: async (file, _, callback) => { | ||||
|       const hash = env.isProdBuild() | ||||
|         ? createHash("md5").update(file.contents).digest("hex") | ||||
|         : "dev"; | ||||
|       HASHES.set(file.stem, hash); | ||||
|       file.stem += `-${hash}`; | ||||
|       callback(null, file); | ||||
|     }, | ||||
|   }).setMaxListeners(translationFiles.length + 1); | ||||
|   const fragmentsStream = hashStream | ||||
|   return gulp | ||||
|     .src(src) | ||||
|     .pipe(transform((data, file) => lokaliseTransform(data, data, file))) | ||||
|     .pipe( | ||||
|       new CustomJSON((data) => | ||||
|         FRAGMENTS.map((fragment) => { | ||||
|           switch (fragment) { | ||||
|             case "base": | ||||
|               // Remove the panels and supervisor to create the base translations | ||||
|               return [ | ||||
|                 flatten({ | ||||
|                   ...data, | ||||
|                   ui: { ...data.ui, panel: undefined }, | ||||
|                   supervisor: undefined, | ||||
|                 }), | ||||
|                 "", | ||||
|               ]; | ||||
|             case "supervisor": | ||||
|               // Supervisor key is at the top level | ||||
|               return [flatten(data.supervisor), ""]; | ||||
|             case "landing-page": | ||||
|               // landing-page key is at the top level | ||||
|               return [flatten(data["landing-page"]), ""]; | ||||
|             default: | ||||
|               // Create a fragment with only the given panel | ||||
|               return [ | ||||
|                 flatten(data.ui.panel[fragment], `ui.panel.${fragment}.`), | ||||
|                 fragment, | ||||
|               ]; | ||||
|           } | ||||
|         }) | ||||
|       ) | ||||
|     ) | ||||
|     .pipe(gulp.dest(outDir)); | ||||
|  | ||||
|   // Send the English master downstream first, then for each other locale | ||||
|   // generate merged JSON data to continue piping. It begins with the master | ||||
|   // translation as a failsafe for untranslated strings, and merges all parent | ||||
|   // tags into one file for each specific subtag | ||||
|   // | ||||
|   // TODO: This is a naive interpretation of BCP47 that should be improved. | ||||
|   //       Will be OK for now as long as we don't have anything more complicated | ||||
|   // than a base translation + region. | ||||
|   const masterStream = gulp | ||||
|     .src(`${workDir}/en.json`) | ||||
|     .pipe(new PassThrough({ objectMode: true })); | ||||
|   masterStream.pipe(hashStream, { end: false }); | ||||
|   const mergesFinished = [finished(masterStream)]; | ||||
|   for (const translationFile of translationFiles) { | ||||
|     const locale = basename(translationFile, ".json"); | ||||
|     const subtags = locale.split("-"); | ||||
|     const mergeFiles = []; | ||||
|     for (let i = 1; i <= subtags.length; i++) { | ||||
|       const lang = subtags.slice(0, i).join("-"); | ||||
|       if (lang === TEST_LOCALE) { | ||||
|         mergeFiles.push(`${workDir}/${TEST_LOCALE}.json`); | ||||
|       } else if (lang !== "en") { | ||||
|         mergeFiles.push(`${inFrontendDir}/${lang}.json`); | ||||
|         if (mergeBackend) { | ||||
|           mergeFiles.push(`${inBackendDir}/${lang}.json`); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     const mergeStream = gulp | ||||
|       .src(mergeFiles, { allowEmpty: true }) | ||||
|       .pipe(new MergeJSON(locale, enMaster, emptyReviver)); | ||||
|     mergesFinished.push(finished(mergeStream)); | ||||
|     mergeStream.pipe(hashStream, { end: false }); | ||||
|   } | ||||
|  | ||||
|   // Wait for all merges to finish, then it's safe to end writing to the | ||||
|   // downstream pipeline and wait for all fragments to finish writing. | ||||
|   await Promise.all(mergesFinished); | ||||
|   hashStream.end(); | ||||
|   await finished(fragmentsStream); | ||||
| }; | ||||
|  | ||||
| const writeTranslationMetaData = () => | ||||
|   gulp | ||||
|     .src([`${paths.translations_src}/translationMetadata.json`]) | ||||
|     .pipe( | ||||
|       new CustomJSON((meta) => { | ||||
|         // Add the test translation in development. | ||||
|         if (!env.isProdBuild()) { | ||||
|           meta[TEST_LOCALE] = { nativeName: "Translation Test" }; | ||||
|         } | ||||
|         // Filter out locales without a native name, and add the hashes. | ||||
|         for (const locale of Object.keys(meta)) { | ||||
|           if (!meta[locale].nativeName) { | ||||
|             meta[locale] = undefined; | ||||
|             console.warn( | ||||
|               `Skipping locale ${locale} because native name is not translated.` | ||||
|             ); | ||||
|           } else { | ||||
|             meta[locale].hash = HASHES.get(locale); | ||||
|           } | ||||
|         } | ||||
|         return { | ||||
|           fragments: FRAGMENTS.filter(panelFragment), | ||||
|           translations: meta, | ||||
|         }; | ||||
|       merge({ | ||||
|         fileName: "en.json", | ||||
|       }) | ||||
|     ) | ||||
|     .pipe(gulp.dest(workDir)); | ||||
|     .pipe(gulp.dest(fullDir)); | ||||
| }); | ||||
|  | ||||
| gulp.task("build-merged-translations", () => | ||||
|   gulp | ||||
|     .src( | ||||
|       [ | ||||
|         inFrontendDir + "/*.json", | ||||
|         "!" + inFrontendDir + "/en.json", | ||||
|         workDir + "/test.json", | ||||
|       ], | ||||
|       { | ||||
|         allowEmpty: true, | ||||
|       } | ||||
|     ) | ||||
|     .pipe(transform((data, file) => lokaliseTransform(data, data, file))) | ||||
|     .pipe( | ||||
|       flatmap((stream, file) => { | ||||
|         // For each language generate a merged json file. It begins with the master | ||||
|         // translation as a failsafe for untranslated strings, and merges all parent | ||||
|         // tags into one file for each specific subtag | ||||
|         // | ||||
|         // TODO: This is a naive interpretation of BCP47 that should be improved. | ||||
|         //       Will be OK for now as long as we don't have anything more complicated | ||||
|         //       than a base translation + region. | ||||
|         const tr = path.basename(file.history[0], ".json"); | ||||
|         const subtags = tr.split("-"); | ||||
|         const src = [fullDir + "/en.json"]; | ||||
|         for (let i = 1; i <= subtags.length; i++) { | ||||
|           const lang = subtags.slice(0, i).join("-"); | ||||
|           if (lang === "test") { | ||||
|             src.push(workDir + "/test.json"); | ||||
|           } else if (lang !== "en") { | ||||
|             src.push(inFrontendDir + "/" + lang + ".json"); | ||||
|             if (mergeBackend) { | ||||
|               src.push(inBackendDir + "/" + lang + ".json"); | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|         return gulp | ||||
|           .src(src, { allowEmpty: true }) | ||||
|           .pipe(transform((data) => emptyFilter(data))) | ||||
|           .pipe( | ||||
|             merge({ | ||||
|               fileName: tr + ".json", | ||||
|             }) | ||||
|           ) | ||||
|           .pipe(gulp.dest(fullDir)); | ||||
|       }) | ||||
|     ) | ||||
| ); | ||||
|  | ||||
| let taskName; | ||||
|  | ||||
| const splitTasks = []; | ||||
| TRANSLATION_FRAGMENTS.forEach((fragment) => { | ||||
|   taskName = "build-translation-fragment-" + fragment; | ||||
|   gulp.task(taskName, () => | ||||
|     // Return only the translations for this fragment. | ||||
|     gulp | ||||
|       .src(fullDir + "/*.json") | ||||
|       .pipe( | ||||
|         transform((data) => ({ | ||||
|           ui: { | ||||
|             panel: { | ||||
|               [fragment]: data.ui.panel[fragment], | ||||
|             }, | ||||
|           }, | ||||
|         })) | ||||
|       ) | ||||
|       .pipe(gulp.dest(workDir + "/" + fragment)) | ||||
|   ); | ||||
|   splitTasks.push(taskName); | ||||
| }); | ||||
|  | ||||
| taskName = "build-translation-core"; | ||||
| gulp.task(taskName, () => | ||||
|   // Remove the fragment translations from the core translation. | ||||
|   gulp | ||||
|     .src(fullDir + "/*.json") | ||||
|     .pipe( | ||||
|       transform((data, _file) => { | ||||
|         TRANSLATION_FRAGMENTS.forEach((fragment) => { | ||||
|           delete data.ui.panel[fragment]; | ||||
|         }); | ||||
|         delete data.supervisor; | ||||
|         return data; | ||||
|       }) | ||||
|     ) | ||||
|     .pipe(gulp.dest(coreDir)) | ||||
| ); | ||||
|  | ||||
| splitTasks.push(taskName); | ||||
|  | ||||
| gulp.task("build-flattened-translations", () => | ||||
|   // Flatten the split versions of our translations, and move them into outDir | ||||
|   gulp | ||||
|     .src( | ||||
|       TRANSLATION_FRAGMENTS.map( | ||||
|         (fragment) => workDir + "/" + fragment + "/*.json" | ||||
|       ).concat(coreDir + "/*.json"), | ||||
|       { base: workDir } | ||||
|     ) | ||||
|     .pipe( | ||||
|       transform((data) => | ||||
|         // Polymer.AppLocalizeBehavior requires flattened json | ||||
|         flatten(data) | ||||
|       ) | ||||
|     ) | ||||
|     .pipe( | ||||
|       rename((filePath) => { | ||||
|         if (filePath.dirname === "core") { | ||||
|           filePath.dirname = ""; | ||||
|         } | ||||
|         // In dev we create the file with the fake hash in the filename | ||||
|         if (!env.isProdBuild()) { | ||||
|           filePath.basename += "-dev"; | ||||
|         } | ||||
|       }) | ||||
|     ) | ||||
|     .pipe(gulp.dest(outDir)) | ||||
| ); | ||||
|  | ||||
| const fingerprints = {}; | ||||
|  | ||||
| gulp.task("build-translation-fingerprints", () => { | ||||
|   // Fingerprint full file of each language | ||||
|   const files = readdirSync(fullDir); | ||||
|  | ||||
|   for (let i = 0; i < files.length; i++) { | ||||
|     fingerprints[files[i].split(".")[0]] = { | ||||
|       // In dev we create fake hashes | ||||
|       hash: env.isProdBuild() | ||||
|         ? createHash("md5") | ||||
|             .update(readFileSync(path.join(fullDir, files[i]), "utf-8")) | ||||
|             .digest("hex") | ||||
|         : "dev", | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   // In dev we create the file with the fake hash in the filename | ||||
|   if (env.isProdBuild()) { | ||||
|     mapFiles(outDir, ".json", (filename) => { | ||||
|       const parsed = path.parse(filename); | ||||
|  | ||||
|       // nl.json -> nl-<hash>.json | ||||
|       if (!(parsed.name in fingerprints)) { | ||||
|         throw new Error(`Unable to find hash for ${filename}`); | ||||
|       } | ||||
|  | ||||
|       renameSync( | ||||
|         filename, | ||||
|         `${parsed.dir}/${parsed.name}-${fingerprints[parsed.name].hash}${ | ||||
|           parsed.ext | ||||
|         }` | ||||
|       ); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   const stream = source("translationFingerprints.json"); | ||||
|   stream.write(JSON.stringify(fingerprints)); | ||||
|   process.nextTick(() => stream.end()); | ||||
|   return stream.pipe(vinylBuffer()).pipe(gulp.dest(workDir)); | ||||
| }); | ||||
|  | ||||
| gulp.task("build-translation-fragment-supervisor", () => | ||||
|   gulp | ||||
|     .src(fullDir + "/*.json") | ||||
|     .pipe(transform((data) => data.supervisor)) | ||||
|     .pipe( | ||||
|       rename((filePath) => { | ||||
|         // In dev we create the file with the fake hash in the filename | ||||
|         if (!env.isProdBuild()) { | ||||
|           filePath.basename += "-dev"; | ||||
|         } | ||||
|       }) | ||||
|     ) | ||||
|     .pipe(gulp.dest(workDir + "/supervisor")) | ||||
| ); | ||||
|  | ||||
| gulp.task("build-translation-flatten-supervisor", () => | ||||
|   gulp | ||||
|     .src(workDir + "/supervisor/*.json") | ||||
|     .pipe( | ||||
|       transform((data) => | ||||
|         // Polymer.AppLocalizeBehavior requires flattened json | ||||
|         flatten(data) | ||||
|       ) | ||||
|     ) | ||||
|     .pipe(gulp.dest(outDir)) | ||||
| ); | ||||
|  | ||||
| gulp.task("build-translation-write-metadata", () => | ||||
|   gulp | ||||
|     .src( | ||||
|       [ | ||||
|         path.join(paths.translations_src, "translationMetadata.json"), | ||||
|         workDir + "/testMetadata.json", | ||||
|         workDir + "/translationFingerprints.json", | ||||
|       ], | ||||
|       { allowEmpty: true } | ||||
|     ) | ||||
|     .pipe(merge({})) | ||||
|     .pipe( | ||||
|       transform((data) => { | ||||
|         const newData = {}; | ||||
|         Object.entries(data).forEach(([key, value]) => { | ||||
|           // Filter out translations without native name. | ||||
|           if (value.nativeName) { | ||||
|             newData[key] = value; | ||||
|           } else { | ||||
|             console.warn( | ||||
|               `Skipping language ${key}. Native name was not translated.` | ||||
|             ); | ||||
|           } | ||||
|         }); | ||||
|         return newData; | ||||
|       }) | ||||
|     ) | ||||
|     .pipe( | ||||
|       transform((data) => ({ | ||||
|         fragments: TRANSLATION_FRAGMENTS, | ||||
|         translations: data, | ||||
|       })) | ||||
|     ) | ||||
|     .pipe(rename("translationMetadata.json")) | ||||
|     .pipe(gulp.dest(workDir)) | ||||
| ); | ||||
|  | ||||
| gulp.task( | ||||
|   "create-translations", | ||||
|   gulp.series( | ||||
|     ...(env.isProdBuild() ? [] : ["create-test-translation"]), | ||||
|     "build-master-translation", | ||||
|     "build-merged-translations", | ||||
|     gulp.parallel(...splitTasks), | ||||
|     "build-flattened-translations" | ||||
|   ) | ||||
| ); | ||||
|  | ||||
| gulp.task( | ||||
|   "build-translations", | ||||
|   gulp.series( | ||||
|     gulp.parallel( | ||||
|       "fetch-nightly-translations", | ||||
|       gulp.series("clean-translations", makeWorkDir) | ||||
|       gulp.series("clean-translations", "ensure-translations-build-dir") | ||||
|     ), | ||||
|     createTestTranslation, | ||||
|     createMasterTranslation, | ||||
|     createTranslations, | ||||
|     writeTranslationMetaData | ||||
|     "create-translations", | ||||
|     "build-translation-fingerprints", | ||||
|     "build-translation-write-metadata" | ||||
|   ) | ||||
| ); | ||||
|  | ||||
| gulp.task( | ||||
|   "build-supervisor-translations", | ||||
|   gulp.series(setFragment("supervisor"), "build-translations") | ||||
| ); | ||||
|  | ||||
| gulp.task( | ||||
|   "build-landing-page-translations", | ||||
|   gulp.series(setFragment("landing-page"), "build-translations") | ||||
|   gulp.series( | ||||
|     gulp.parallel( | ||||
|       "fetch-nightly-translations", | ||||
|       gulp.series("clean-translations", "ensure-translations-build-dir") | ||||
|     ), | ||||
|     "build-master-translation", | ||||
|     "build-merged-translations", | ||||
|     "build-translation-fragment-supervisor", | ||||
|     "build-translation-flatten-supervisor", | ||||
|     "build-translation-fingerprints", | ||||
|     "build-translation-write-metadata" | ||||
|   ) | ||||
| ); | ||||
|   | ||||
							
								
								
									
										10
									
								
								build-scripts/gulp/wds.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								build-scripts/gulp/wds.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| import gulp from "gulp"; | ||||
| import { startDevServer } from "@web/dev-server"; | ||||
|  | ||||
| gulp.task("wds-watch-app", async () => { | ||||
|   startDevServer({ | ||||
|     config: { | ||||
|       watch: true, | ||||
|     }, | ||||
|   }); | ||||
| }); | ||||
| @@ -1,11 +1,11 @@ | ||||
| // Tasks to run rspack.
 | ||||
| // Tasks to run webpack.
 | ||||
| 
 | ||||
| import fs from "fs"; | ||||
| import path from "path"; | ||||
| import log from "fancy-log"; | ||||
| import gulp from "gulp"; | ||||
| import rspack from "@rspack/core"; | ||||
| import { RspackDevServer } from "@rspack/dev-server"; | ||||
| import webpack from "webpack"; | ||||
| import WebpackDevServer from "webpack-dev-server"; | ||||
| import env from "../env.cjs"; | ||||
| import paths from "../paths.cjs"; | ||||
| import { | ||||
| @@ -14,8 +14,7 @@ import { | ||||
|   createDemoConfig, | ||||
|   createGalleryConfig, | ||||
|   createHassioConfig, | ||||
|   createLandingPageConfig, | ||||
| } from "../rspack.cjs"; | ||||
| } from "../webpack.cjs"; | ||||
| 
 | ||||
| const bothBuilds = (createConfigFunc, params) => [ | ||||
|   createConfigFunc({ ...params, latestBuild: true }), | ||||
| @@ -31,7 +30,7 @@ const isWsl = | ||||
| 
 | ||||
| /** | ||||
|  * @param {{ | ||||
|  *   compiler: import("@rspack/core").Compiler, | ||||
|  *   compiler: import("webpack").Compiler, | ||||
|  *   contentBase: string, | ||||
|  *   port: number, | ||||
|  *   listenHost?: string | ||||
| @@ -41,14 +40,9 @@ const runDevServer = async ({ | ||||
|   compiler, | ||||
|   contentBase, | ||||
|   port, | ||||
|   listenHost = undefined, | ||||
|   proxy = undefined, | ||||
|   listenHost = "localhost", | ||||
| }) => { | ||||
|   if (listenHost === undefined) { | ||||
|     // For dev container, we need to listen on all hosts
 | ||||
|     listenHost = env.isDevContainer() ? "0.0.0.0" : "localhost"; | ||||
|   } | ||||
|   const server = new RspackDevServer( | ||||
|   const server = new WebpackDevServer( | ||||
|     { | ||||
|       hot: false, | ||||
|       open: true, | ||||
| @@ -58,14 +52,13 @@ const runDevServer = async ({ | ||||
|         directory: contentBase, | ||||
|         watch: true, | ||||
|       }, | ||||
|       proxy, | ||||
|     }, | ||||
|     compiler | ||||
|   ); | ||||
| 
 | ||||
|   await server.start(); | ||||
|   // Server listening
 | ||||
|   log("[rspack-dev-server]", `Project is running at http://localhost:${port}`); | ||||
|   log("[webpack-dev-server]", `Project is running at http://localhost:${port}`); | ||||
| }; | ||||
| 
 | ||||
| const doneHandler = (done) => (err, stats) => { | ||||
| @@ -90,27 +83,27 @@ const doneHandler = (done) => (err, stats) => { | ||||
| 
 | ||||
| const prodBuild = (conf) => | ||||
|   new Promise((resolve) => { | ||||
|     rspack( | ||||
|     webpack( | ||||
|       conf, | ||||
|       // Resolve promise when done. Because we pass a callback, rspack closes itself
 | ||||
|       // Resolve promise when done. Because we pass a callback, webpack closes itself
 | ||||
|       doneHandler(resolve) | ||||
|     ); | ||||
|   }); | ||||
| 
 | ||||
| gulp.task("rspack-watch-app", () => { | ||||
| gulp.task("webpack-watch-app", () => { | ||||
|   // This command will run forever because we don't close compiler
 | ||||
|   rspack( | ||||
|   webpack( | ||||
|     process.env.ES5 | ||||
|       ? bothBuilds(createAppConfig, { isProdBuild: false }) | ||||
|       : createAppConfig({ isProdBuild: false, latestBuild: true }) | ||||
|   ).watch({ poll: isWsl }, doneHandler()); | ||||
|   gulp.watch( | ||||
|     path.join(paths.translations_src, "en.json"), | ||||
|     gulp.series("build-translations", "copy-translations-app") | ||||
|     gulp.series("create-translations", "copy-translations-app") | ||||
|   ); | ||||
| }); | ||||
| 
 | ||||
| gulp.task("rspack-prod-app", () => | ||||
| gulp.task("webpack-prod-app", () => | ||||
|   prodBuild( | ||||
|     bothBuilds(createAppConfig, { | ||||
|       isProdBuild: true, | ||||
| @@ -120,30 +113,25 @@ gulp.task("rspack-prod-app", () => | ||||
|   ) | ||||
| ); | ||||
| 
 | ||||
| gulp.task("rspack-dev-server-demo", () => | ||||
| gulp.task("webpack-dev-server-demo", () => | ||||
|   runDevServer({ | ||||
|     compiler: rspack( | ||||
|       createDemoConfig({ isProdBuild: false, latestBuild: true }) | ||||
|     ), | ||||
|     compiler: webpack(bothBuilds(createDemoConfig, { isProdBuild: false })), | ||||
|     contentBase: paths.demo_output_root, | ||||
|     port: 8090, | ||||
|   }) | ||||
| ); | ||||
| 
 | ||||
| gulp.task("rspack-prod-demo", () => | ||||
| gulp.task("webpack-prod-demo", () => | ||||
|   prodBuild( | ||||
|     bothBuilds(createDemoConfig, { | ||||
|       isProdBuild: true, | ||||
|       isStatsBuild: env.isStatsBuild(), | ||||
|     }) | ||||
|   ) | ||||
| ); | ||||
| 
 | ||||
| gulp.task("rspack-dev-server-cast", () => | ||||
| gulp.task("webpack-dev-server-cast", () => | ||||
|   runDevServer({ | ||||
|     compiler: rspack( | ||||
|       createCastConfig({ isProdBuild: false, latestBuild: true }) | ||||
|     ), | ||||
|     compiler: webpack(bothBuilds(createCastConfig, { isProdBuild: false })), | ||||
|     contentBase: paths.cast_output_root, | ||||
|     port: 8080, | ||||
|     // Accessible from the network, because that's how Cast hits it.
 | ||||
| @@ -151,7 +139,7 @@ gulp.task("rspack-dev-server-cast", () => | ||||
|   }) | ||||
| ); | ||||
| 
 | ||||
| gulp.task("rspack-prod-cast", () => | ||||
| gulp.task("webpack-prod-cast", () => | ||||
|   prodBuild( | ||||
|     bothBuilds(createCastConfig, { | ||||
|       isProdBuild: true, | ||||
| @@ -159,9 +147,9 @@ gulp.task("rspack-prod-cast", () => | ||||
|   ) | ||||
| ); | ||||
| 
 | ||||
| gulp.task("rspack-watch-hassio", () => { | ||||
| gulp.task("webpack-watch-hassio", () => { | ||||
|   // This command will run forever because we don't close compiler
 | ||||
|   rspack( | ||||
|   webpack( | ||||
|     createHassioConfig({ | ||||
|       isProdBuild: false, | ||||
|       latestBuild: true, | ||||
| @@ -174,7 +162,7 @@ gulp.task("rspack-watch-hassio", () => { | ||||
|   ); | ||||
| }); | ||||
| 
 | ||||
| gulp.task("rspack-prod-hassio", () => | ||||
| gulp.task("webpack-prod-hassio", () => | ||||
|   prodBuild( | ||||
|     bothBuilds(createHassioConfig, { | ||||
|       isProdBuild: true, | ||||
| @@ -184,18 +172,17 @@ gulp.task("rspack-prod-hassio", () => | ||||
|   ) | ||||
| ); | ||||
| 
 | ||||
| gulp.task("rspack-dev-server-gallery", () => | ||||
| gulp.task("webpack-dev-server-gallery", () => | ||||
|   runDevServer({ | ||||
|     compiler: rspack( | ||||
|       createGalleryConfig({ isProdBuild: false, latestBuild: true }) | ||||
|     ), | ||||
|     // We don't use the es5 build, but the dev server will fuck up the publicPath if we don't
 | ||||
|     compiler: webpack(bothBuilds(createGalleryConfig, { isProdBuild: false })), | ||||
|     contentBase: paths.gallery_output_root, | ||||
|     port: 8100, | ||||
|     listenHost: "0.0.0.0", | ||||
|   }) | ||||
| ); | ||||
| 
 | ||||
| gulp.task("rspack-prod-gallery", () => | ||||
| gulp.task("webpack-prod-gallery", () => | ||||
|   prodBuild( | ||||
|     createGalleryConfig({ | ||||
|       isProdBuild: true, | ||||
| @@ -203,30 +190,3 @@ gulp.task("rspack-prod-gallery", () => | ||||
|     }) | ||||
|   ) | ||||
| ); | ||||
| 
 | ||||
| gulp.task("rspack-watch-landing-page", () => { | ||||
|   // This command will run forever because we don't close compiler
 | ||||
|   rspack( | ||||
|     process.env.ES5 | ||||
|       ? bothBuilds(createLandingPageConfig, { isProdBuild: false }) | ||||
|       : createLandingPageConfig({ isProdBuild: false, latestBuild: true }) | ||||
|   ).watch({ poll: isWsl }, doneHandler()); | ||||
| 
 | ||||
|   gulp.watch( | ||||
|     path.join(paths.translations_src, "en.json"), | ||||
|     gulp.series( | ||||
|       "build-landing-page-translations", | ||||
|       "copy-translations-landing-page" | ||||
|     ) | ||||
|   ); | ||||
| }); | ||||
| 
 | ||||
| gulp.task("rspack-prod-landing-page", () => | ||||
|   prodBuild( | ||||
|     bothBuilds(createLandingPageConfig, { | ||||
|       isProdBuild: true, | ||||
|       isStatsBuild: env.isStatsBuild(), | ||||
|       isTestBuild: env.isTestBuild(), | ||||
|     }) | ||||
|   ) | ||||
| ); | ||||
| @@ -16,7 +16,6 @@ const detailsClose = "</details>\n"; | ||||
|  | ||||
| const dummyAPI = { | ||||
|   version: babelVersion, | ||||
|   // eslint-disable-next-line @typescript-eslint/no-empty-function | ||||
|   assertVersion: () => {}, | ||||
|   caller: (callback) => | ||||
|     callback({ | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| const path = require("path"); | ||||
|  | ||||
| module.exports = { | ||||
|   root_dir: path.resolve(__dirname, ".."), | ||||
|   polymer_dir: path.resolve(__dirname, ".."), | ||||
|  | ||||
|   build_dir: path.resolve(__dirname, "../build"), | ||||
|   app_output_root: path.resolve(__dirname, "../hass_frontend"), | ||||
| @@ -33,22 +33,6 @@ module.exports = { | ||||
|   ), | ||||
|   gallery_output_static: path.resolve(__dirname, "../gallery/dist/static"), | ||||
|  | ||||
|   landingPage_dir: path.resolve(__dirname, "../landing-page"), | ||||
|   landingPage_build: path.resolve(__dirname, "../landing-page/build"), | ||||
|   landingPage_output_root: path.resolve(__dirname, "../landing-page/dist"), | ||||
|   landingPage_output_latest: path.resolve( | ||||
|     __dirname, | ||||
|     "../landing-page/dist/frontend_latest" | ||||
|   ), | ||||
|   landingPage_output_es5: path.resolve( | ||||
|     __dirname, | ||||
|     "../landing-page/dist/frontend_es5" | ||||
|   ), | ||||
|   landingPage_output_static: path.resolve( | ||||
|     __dirname, | ||||
|     "../landing-page/dist/static" | ||||
|   ), | ||||
|  | ||||
|   hassio_dir: path.resolve(__dirname, "../hassio"), | ||||
|   hassio_output_root: path.resolve(__dirname, "../hassio/build"), | ||||
|   hassio_output_static: path.resolve(__dirname, "../hassio/build/static"), | ||||
|   | ||||
							
								
								
									
										14
									
								
								build-scripts/rollup-plugins/dont-hash-plugin.cjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								build-scripts/rollup-plugins/dont-hash-plugin.cjs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| module.exports = function (opts = {}) { | ||||
|   const dontHash = opts.dontHash || new Set(); | ||||
|  | ||||
|   return { | ||||
|     name: "dont-hash", | ||||
|     renderChunk(_code, chunk, _options) { | ||||
|       if (!chunk.isEntry || !dontHash.has(chunk.name)) { | ||||
|         return null; | ||||
|       } | ||||
|       chunk.fileName = `${chunk.name}.js`; | ||||
|       return null; | ||||
|     }, | ||||
|   }; | ||||
| }; | ||||
							
								
								
									
										24
									
								
								build-scripts/rollup-plugins/ignore-plugin.cjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								build-scripts/rollup-plugins/ignore-plugin.cjs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| module.exports = function (userOptions = {}) { | ||||
|   // Files need to be absolute paths. | ||||
|   // This only works if the file has no exports | ||||
|   // and only is imported for its side effects | ||||
|   const files = userOptions.files || []; | ||||
|  | ||||
|   if (files.length === 0) { | ||||
|     return { | ||||
|       name: "ignore", | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   return { | ||||
|     name: "ignore", | ||||
|  | ||||
|     load(id) { | ||||
|       return files.some((toIgnorePath) => id.startsWith(toIgnorePath)) | ||||
|         ? { | ||||
|             code: "", | ||||
|           } | ||||
|         : null; | ||||
|     }, | ||||
|   }; | ||||
| }; | ||||
							
								
								
									
										34
									
								
								build-scripts/rollup-plugins/manifest-plugin.cjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								build-scripts/rollup-plugins/manifest-plugin.cjs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| const url = require("url"); | ||||
|  | ||||
| const defaultOptions = { | ||||
|   publicPath: "", | ||||
| }; | ||||
|  | ||||
| module.exports = function (userOptions = {}) { | ||||
|   const options = { ...defaultOptions, ...userOptions }; | ||||
|  | ||||
|   return { | ||||
|     name: "manifest", | ||||
|     generateBundle(outputOptions, bundle) { | ||||
|       const manifest = {}; | ||||
|  | ||||
|       for (const chunk of Object.values(bundle)) { | ||||
|         if (!chunk.isEntry) { | ||||
|           continue; | ||||
|         } | ||||
|         // Add js extension to mimic Webpack manifest. | ||||
|         manifest[`${chunk.name}.js`] = url.resolve( | ||||
|           options.publicPath, | ||||
|           chunk.fileName | ||||
|         ); | ||||
|       } | ||||
|  | ||||
|       this.emitFile({ | ||||
|         type: "asset", | ||||
|         source: JSON.stringify(manifest, undefined, 2), | ||||
|         name: "manifest.json", | ||||
|         fileName: "manifest.json", | ||||
|       }); | ||||
|     }, | ||||
|   }; | ||||
| }; | ||||
							
								
								
									
										152
									
								
								build-scripts/rollup-plugins/worker-plugin.cjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										152
									
								
								build-scripts/rollup-plugins/worker-plugin.cjs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,152 @@ | ||||
| // Worker plugin | ||||
| // Each worker will include all of its dependencies | ||||
| // instead of relying on an importer. | ||||
|  | ||||
| // Forked from v.1.4.1 | ||||
| // https://github.com/surma/rollup-plugin-off-main-thread | ||||
| /** | ||||
|  * Copyright 2018 Google Inc. All Rights Reserved. | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
|  | ||||
| const rollup = require("rollup"); | ||||
| const path = require("path"); | ||||
| const MagicString = require("magic-string"); | ||||
|  | ||||
| const defaultOpts = { | ||||
|   // A RegExp to find `new Workers()` calls. The second capture group _must_ | ||||
|   // capture the provided file name without the quotes. | ||||
|   workerRegexp: /new Worker\((["'])(.+?)\1(,[^)]+)?\)/g, | ||||
|   plugins: ["node-resolve", "commonjs", "babel", "terser", "ignore"], | ||||
| }; | ||||
|  | ||||
| async function getBundledWorker(workerPath, rollupOptions) { | ||||
|   const bundle = await rollup.rollup({ | ||||
|     ...rollupOptions, | ||||
|     input: { | ||||
|       worker: workerPath, | ||||
|     }, | ||||
|   }); | ||||
|   const { output } = await bundle.generate({ | ||||
|     // Generates cleanest output, we shouldn't have any imports/exports | ||||
|     // that would be incompatible with ES5. | ||||
|     format: "es", | ||||
|     // We should not export anything. This will fail build if we are. | ||||
|     exports: "none", | ||||
|   }); | ||||
|  | ||||
|   let code; | ||||
|  | ||||
|   for (const chunkOrAsset of output) { | ||||
|     if (chunkOrAsset.name === "worker") { | ||||
|       code = chunkOrAsset.code; | ||||
|     } else if (chunkOrAsset.type !== "asset") { | ||||
|       throw new Error("Unexpected extra output"); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   return code; | ||||
| } | ||||
|  | ||||
| module.exports = function (opts = {}) { | ||||
|   opts = { ...defaultOpts, ...opts }; | ||||
|  | ||||
|   let rollupOptions; | ||||
|   let refIds; | ||||
|  | ||||
|   return { | ||||
|     name: "hass-worker", | ||||
|  | ||||
|     async buildStart(options) { | ||||
|       refIds = {}; | ||||
|       rollupOptions = { | ||||
|         plugins: options.plugins.filter((plugin) => | ||||
|           opts.plugins.includes(plugin.name) | ||||
|         ), | ||||
|       }; | ||||
|     }, | ||||
|  | ||||
|     async transform(code, id) { | ||||
|       // Copy the regexp as they are stateful and this hook is async. | ||||
|       const workerRegexp = new RegExp( | ||||
|         opts.workerRegexp.source, | ||||
|         opts.workerRegexp.flags | ||||
|       ); | ||||
|       if (!workerRegexp.test(code)) { | ||||
|         return undefined; | ||||
|       } | ||||
|  | ||||
|       const ms = new MagicString(code); | ||||
|       // Reset the regexp | ||||
|       workerRegexp.lastIndex = 0; | ||||
|       for (;;) { | ||||
|         const match = workerRegexp.exec(code); | ||||
|         if (!match) { | ||||
|           break; | ||||
|         } | ||||
|  | ||||
|         const workerFile = match[2]; | ||||
|         let optionsObject = {}; | ||||
|         // Parse the optional options object | ||||
|         if (match[3] && match[3].length > 0) { | ||||
|           // FIXME: ooooof! | ||||
|           // eslint-disable-next-line @typescript-eslint/no-implied-eval | ||||
|           optionsObject = new Function(`return ${match[3].slice(1)};`)(); | ||||
|         } | ||||
|         delete optionsObject.type; | ||||
|  | ||||
|         if (!/^.*\//.test(workerFile)) { | ||||
|           this.warn( | ||||
|             `Paths passed to the Worker constructor must be relative or absolute, i.e. start with /, ./ or ../ (just like dynamic import!). Ignoring "${workerFile}".` | ||||
|           ); | ||||
|           continue; | ||||
|         } | ||||
|  | ||||
|         // Find worker file and store it as a chunk with ID prefixed for our loader | ||||
|         // eslint-disable-next-line no-await-in-loop | ||||
|         const resolvedWorkerFile = (await this.resolve(workerFile, id)).id; | ||||
|         let chunkRefId; | ||||
|         if (resolvedWorkerFile in refIds) { | ||||
|           chunkRefId = refIds[resolvedWorkerFile]; | ||||
|         } else { | ||||
|           this.addWatchFile(resolvedWorkerFile); | ||||
|           // eslint-disable-next-line no-await-in-loop | ||||
|           const source = await getBundledWorker( | ||||
|             resolvedWorkerFile, | ||||
|             rollupOptions | ||||
|           ); | ||||
|           chunkRefId = refIds[resolvedWorkerFile] = this.emitFile({ | ||||
|             name: path.basename(resolvedWorkerFile), | ||||
|             source, | ||||
|             type: "asset", | ||||
|           }); | ||||
|         } | ||||
|  | ||||
|         const workerParametersStartIndex = match.index + "new Worker(".length; | ||||
|         const workerParametersEndIndex = | ||||
|           match.index + match[0].length - ")".length; | ||||
|  | ||||
|         ms.overwrite( | ||||
|           workerParametersStartIndex, | ||||
|           workerParametersEndIndex, | ||||
|           `import.meta.ROLLUP_FILE_URL_${chunkRefId}, ${JSON.stringify( | ||||
|             optionsObject | ||||
|           )}` | ||||
|         ); | ||||
|       } | ||||
|  | ||||
|       return { | ||||
|         code: ms.toString(), | ||||
|         map: ms.generateMap({ hires: true }), | ||||
|       }; | ||||
|     }, | ||||
|   }; | ||||
| }; | ||||
							
								
								
									
										146
									
								
								build-scripts/rollup.cjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										146
									
								
								build-scripts/rollup.cjs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,146 @@ | ||||
| const path = require("path"); | ||||
|  | ||||
| const commonjs = require("@rollup/plugin-commonjs"); | ||||
| const resolve = require("@rollup/plugin-node-resolve"); | ||||
| const json = require("@rollup/plugin-json"); | ||||
| const { babel } = require("@rollup/plugin-babel"); | ||||
| const replace = require("@rollup/plugin-replace"); | ||||
| const visualizer = require("rollup-plugin-visualizer"); | ||||
| const { string } = require("rollup-plugin-string"); | ||||
| const { terser } = require("rollup-plugin-terser"); | ||||
| const manifest = require("./rollup-plugins/manifest-plugin.cjs"); | ||||
| const worker = require("./rollup-plugins/worker-plugin.cjs"); | ||||
| const dontHashPlugin = require("./rollup-plugins/dont-hash-plugin.cjs"); | ||||
| const ignore = require("./rollup-plugins/ignore-plugin.cjs"); | ||||
|  | ||||
| const bundle = require("./bundle.cjs"); | ||||
| const paths = require("./paths.cjs"); | ||||
|  | ||||
| const extensions = [".js", ".ts"]; | ||||
|  | ||||
| /** | ||||
|  * @param {Object} arg | ||||
|  * @param { import("rollup").InputOption } arg.input | ||||
|  */ | ||||
| const createRollupConfig = ({ | ||||
|   entry, | ||||
|   outputPath, | ||||
|   defineOverlay, | ||||
|   isProdBuild, | ||||
|   latestBuild, | ||||
|   isStatsBuild, | ||||
|   publicPath, | ||||
|   dontHash, | ||||
|   isWDS, | ||||
| }) => ({ | ||||
|   /** | ||||
|    * @type { import("rollup").InputOptions } | ||||
|    */ | ||||
|   inputOptions: { | ||||
|     input: entry, | ||||
|     // Some entry points contain no JavaScript. This setting silences a warning about that. | ||||
|     // https://rollupjs.org/configuration-options/#preserveentrysignatures | ||||
|     preserveEntrySignatures: false, | ||||
|     plugins: [ | ||||
|       ignore({ | ||||
|         files: bundle | ||||
|           .emptyPackages({ latestBuild }) | ||||
|           // TEMP HACK: Makes Rollup build work again | ||||
|           .concat( | ||||
|             require.resolve( | ||||
|               "@webcomponents/scoped-custom-element-registry/scoped-custom-element-registry.min" | ||||
|             ) | ||||
|           ), | ||||
|       }), | ||||
|       resolve({ | ||||
|         extensions, | ||||
|         preferBuiltins: false, | ||||
|         browser: true, | ||||
|         rootDir: paths.polymer_dir, | ||||
|       }), | ||||
|       commonjs(), | ||||
|       json(), | ||||
|       babel({ | ||||
|         ...bundle.babelOptions({ latestBuild, isProdBuild }), | ||||
|         extensions, | ||||
|         babelHelpers: isWDS ? "inline" : "bundled", | ||||
|       }), | ||||
|       string({ | ||||
|         // Import certain extensions as strings | ||||
|         include: [path.join(paths.polymer_dir, "node_modules/**/*.css")], | ||||
|       }), | ||||
|       replace(bundle.definedVars({ isProdBuild, latestBuild, defineOverlay })), | ||||
|       !isWDS && | ||||
|         manifest({ | ||||
|           publicPath, | ||||
|         }), | ||||
|       !isWDS && worker(), | ||||
|       !isWDS && dontHashPlugin({ dontHash }), | ||||
|       !isWDS && isProdBuild && terser(bundle.terserOptions({ latestBuild })), | ||||
|       !isWDS && | ||||
|         isStatsBuild && | ||||
|         visualizer({ | ||||
|           // https://github.com/btd/rollup-plugin-visualizer#options | ||||
|           open: true, | ||||
|           sourcemap: true, | ||||
|         }), | ||||
|     ].filter(Boolean), | ||||
|   }, | ||||
|   /** | ||||
|    * @type { import("rollup").OutputOptions } | ||||
|    */ | ||||
|   outputOptions: { | ||||
|     // https://rollupjs.org/configuration-options/#output-dir | ||||
|     dir: outputPath, | ||||
|     // https://rollupjs.org/configuration-options/#output-format | ||||
|     format: latestBuild ? "es" : "systemjs", | ||||
|     // https://rollupjs.org/configuration-options/#output-externallivebindings | ||||
|     externalLiveBindings: false, | ||||
|     // https://rollupjs.org/configuration-options/#output-entryfilenames | ||||
|     // https://rollupjs.org/configuration-options/#output-chunkfilenames | ||||
|     // https://rollupjs.org/configuration-options/#output-assetfilenames | ||||
|     entryFileNames: | ||||
|       isProdBuild && !isStatsBuild ? "[name]-[hash].js" : "[name].js", | ||||
|     chunkFileNames: isProdBuild && !isStatsBuild ? "c.[hash].js" : "[name].js", | ||||
|     assetFileNames: isProdBuild && !isStatsBuild ? "a.[hash].js" : "[name].js", | ||||
|     // https://rollupjs.org/configuration-options/#output-sourcemap | ||||
|     sourcemap: isProdBuild ? true : "inline", | ||||
|   }, | ||||
| }); | ||||
|  | ||||
| const createAppConfig = ({ isProdBuild, latestBuild, isStatsBuild, isWDS }) => | ||||
|   createRollupConfig( | ||||
|     bundle.config.app({ | ||||
|       isProdBuild, | ||||
|       latestBuild, | ||||
|       isStatsBuild, | ||||
|       isWDS, | ||||
|     }) | ||||
|   ); | ||||
|  | ||||
| const createDemoConfig = ({ isProdBuild, latestBuild, isStatsBuild }) => | ||||
|   createRollupConfig( | ||||
|     bundle.config.demo({ | ||||
|       isProdBuild, | ||||
|       latestBuild, | ||||
|       isStatsBuild, | ||||
|     }) | ||||
|   ); | ||||
|  | ||||
| const createCastConfig = ({ isProdBuild, latestBuild }) => | ||||
|   createRollupConfig(bundle.config.cast({ isProdBuild, latestBuild })); | ||||
|  | ||||
| const createHassioConfig = ({ isProdBuild, latestBuild }) => | ||||
|   createRollupConfig(bundle.config.hassio({ isProdBuild, latestBuild })); | ||||
|  | ||||
| const createGalleryConfig = ({ isProdBuild, latestBuild }) => | ||||
|   createRollupConfig(bundle.config.gallery({ isProdBuild, latestBuild })); | ||||
|  | ||||
| module.exports = { | ||||
|   createAppConfig, | ||||
|   createDemoConfig, | ||||
|   createCastConfig, | ||||
|   createHassioConfig, | ||||
|   createGalleryConfig, | ||||
|   createRollupConfig, | ||||
| }; | ||||
							
								
								
									
										16
									
								
								build-scripts/util.cjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								build-scripts/util.cjs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| const path = require("path"); | ||||
| const fs = require("fs"); | ||||
|  | ||||
| // Helper function to map recursively over files in a folder and it's subfolders | ||||
| module.exports.mapFiles = function mapFiles(startPath, filter, mapFunc) { | ||||
|   const files = fs.readdirSync(startPath); | ||||
|   for (let i = 0; i < files.length; i++) { | ||||
|     const filename = path.join(startPath, files[i]); | ||||
|     const stat = fs.lstatSync(filename); | ||||
|     if (stat.isDirectory()) { | ||||
|       mapFiles(filename, filter, mapFunc); | ||||
|     } else if (filename.indexOf(filter) >= 0) { | ||||
|       mapFunc(filename); | ||||
|     } | ||||
|   } | ||||
| }; | ||||
| @@ -1,18 +1,10 @@ | ||||
| const { existsSync } = require("fs"); | ||||
| const path = require("path"); | ||||
| const rspack = require("@rspack/core"); | ||||
| // eslint-disable-next-line @typescript-eslint/naming-convention
 | ||||
| const { RsdoctorRspackPlugin } = require("@rsdoctor/rspack-plugin"); | ||||
| // eslint-disable-next-line @typescript-eslint/naming-convention
 | ||||
| const { StatsWriterPlugin } = require("webpack-stats-plugin"); | ||||
| const filterStats = require("@bundle-stats/plugin-webpack-filter"); | ||||
| // eslint-disable-next-line @typescript-eslint/naming-convention
 | ||||
| const webpack = require("webpack"); | ||||
| const TerserPlugin = require("terser-webpack-plugin"); | ||||
| // eslint-disable-next-line @typescript-eslint/naming-convention
 | ||||
| const { WebpackManifestPlugin } = require("rspack-manifest-plugin"); | ||||
| const { WebpackManifestPlugin } = require("webpack-manifest-plugin"); | ||||
| const log = require("fancy-log"); | ||||
| // eslint-disable-next-line @typescript-eslint/naming-convention
 | ||||
| const WebpackBar = require("webpackbar/rspack"); | ||||
| const WebpackBar = require("webpackbar"); | ||||
| const paths = require("./paths.cjs"); | ||||
| const bundle = require("./bundle.cjs"); | ||||
| 
 | ||||
| @@ -30,7 +22,7 @@ class LogStartCompilePlugin { | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| const createRspackConfig = ({ | ||||
| const createWebpackConfig = ({ | ||||
|   name, | ||||
|   entry, | ||||
|   outputPath, | ||||
| @@ -57,40 +49,25 @@ const createRspackConfig = ({ | ||||
|     devtool: isTestBuild | ||||
|       ? false | ||||
|       : isProdBuild | ||||
|         ? "nosources-source-map" | ||||
|         : "eval-cheap-module-source-map", | ||||
|       ? "nosources-source-map" | ||||
|       : "eval-cheap-module-source-map", | ||||
|     entry, | ||||
|     node: false, | ||||
|     module: { | ||||
|       rules: [ | ||||
|         { | ||||
|           test: /\.m?js$|\.ts$/, | ||||
|           exclude: /node_modules[\\/]core-js/, | ||||
|           use: (info) => [ | ||||
|             { | ||||
|               loader: "babel-loader", | ||||
|               options: { | ||||
|                 ...bundle.babelOptions({ | ||||
|                   latestBuild, | ||||
|                   isProdBuild, | ||||
|                   isTestBuild, | ||||
|                   sw: info.issuerLayer === "sw", | ||||
|                 }), | ||||
|                 cacheDirectory: !isProdBuild, | ||||
|                 cacheCompression: false, | ||||
|               }, | ||||
|           use: { | ||||
|             loader: "babel-loader", | ||||
|             options: { | ||||
|               ...bundle.babelOptions({ latestBuild, isProdBuild, isTestBuild }), | ||||
|               cacheDirectory: !isProdBuild, | ||||
|               cacheCompression: false, | ||||
|             }, | ||||
|             { | ||||
|               loader: "builtin:swc-loader", | ||||
|               options: bundle.swcOptions(), | ||||
|             }, | ||||
|           ], | ||||
|           }, | ||||
|           resolve: { | ||||
|             fullySpecified: false, | ||||
|           }, | ||||
|           parser: { | ||||
|             worker: ["*context.audioWorklet.addModule()", "..."], | ||||
|           }, | ||||
|         }, | ||||
|         { | ||||
|           test: /\.css$/, | ||||
| @@ -109,20 +86,11 @@ const createRspackConfig = ({ | ||||
|       moduleIds: isProdBuild && !isStatsBuild ? "deterministic" : "named", | ||||
|       chunkIds: isProdBuild && !isStatsBuild ? "deterministic" : "named", | ||||
|       splitChunks: { | ||||
|         // Disable splitting for web workers and worklets because imports of
 | ||||
|         // external chunks are broken for:
 | ||||
|         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
 | ||||
|             (chunk) => | ||||
|               !chunk.canBeInitial() && | ||||
|               !new RegExp( | ||||
|                 `^.+-work${latestBuild ? "(?:let|er)" : "let"}$` | ||||
|               ).test(chunk.name), | ||||
|         // Disable splitting for web workers with ESM output
 | ||||
|         // Imports of external chunks are broken
 | ||||
|         chunks: latestBuild | ||||
|           ? (chunk) => !chunk.canBeInitial() && !/^.+-worker$/.test(chunk.name) | ||||
|           : undefined, | ||||
|       }, | ||||
|     }, | ||||
|     plugins: [ | ||||
| @@ -131,10 +99,10 @@ const createRspackConfig = ({ | ||||
|         // Only include the JS of entrypoints
 | ||||
|         filter: (file) => file.isInitial && !file.name.endsWith(".map"), | ||||
|       }), | ||||
|       new rspack.DefinePlugin( | ||||
|       new webpack.DefinePlugin( | ||||
|         bundle.definedVars({ isProdBuild, latestBuild, defineOverlay }) | ||||
|       ), | ||||
|       new rspack.IgnorePlugin({ | ||||
|       new webpack.IgnorePlugin({ | ||||
|         checkResource(resource, context) { | ||||
|           // Only use ignore to intercept imports that we don't control
 | ||||
|           // inside node_module dependencies.
 | ||||
| @@ -143,8 +111,7 @@ const createRspackConfig = ({ | ||||
|             // calling define.amd will call require("!!webpack amd options")
 | ||||
|             resource.startsWith("!!webpack") || | ||||
|             // loaded by webpack dev server but doesn't exist.
 | ||||
|             resource === "webpack/hot" || | ||||
|             resource.startsWith("@swc/helpers") | ||||
|             resource === "webpack/hot" | ||||
|           ) { | ||||
|             return false; | ||||
|           } | ||||
| @@ -167,34 +134,28 @@ const createRspackConfig = ({ | ||||
|           ); | ||||
|         }, | ||||
|       }), | ||||
|       new rspack.NormalModuleReplacementPlugin( | ||||
|         new RegExp(bundle.emptyPackages({ isHassioBuild }).join("|")), | ||||
|         path.resolve(paths.root_dir, "src/util/empty.js") | ||||
|       new webpack.NormalModuleReplacementPlugin( | ||||
|         new RegExp( | ||||
|           bundle.emptyPackages({ latestBuild, isHassioBuild }).join("|") | ||||
|         ), | ||||
|         path.resolve(paths.polymer_dir, "src/util/empty.js") | ||||
|       ), | ||||
|       !isProdBuild && new LogStartCompilePlugin(), | ||||
|       isProdBuild && | ||||
|         new StatsWriterPlugin({ | ||||
|           filename: path.relative( | ||||
|             outputPath, | ||||
|             path.join(paths.build_dir, "stats", `${name}.json`) | ||||
|       // See `src/resources/intl-polyfill-legacy.ts` for explanation
 | ||||
|       !latestBuild && | ||||
|         new webpack.NormalModuleReplacementPlugin( | ||||
|           new RegExp( | ||||
|             path.resolve(paths.polymer_dir, "src/resources/intl-polyfill.ts") | ||||
|           ), | ||||
|           stats: { assets: true, chunks: true, modules: true }, | ||||
|           transform: (stats) => JSON.stringify(filterStats(stats)), | ||||
|         }), | ||||
|       isProdBuild && | ||||
|         isStatsBuild && | ||||
|         new RsdoctorRspackPlugin({ | ||||
|           reportDir: path.join(paths.build_dir, "rsdoctor"), | ||||
|           features: ["plugins", "bundle"], | ||||
|           supports: { | ||||
|             generateTileGraph: true, | ||||
|           }, | ||||
|         }), | ||||
|           path.resolve( | ||||
|             paths.polymer_dir, | ||||
|             "src/resources/intl-polyfill-legacy.ts" | ||||
|           ) | ||||
|         ), | ||||
|       !isProdBuild && new LogStartCompilePlugin(), | ||||
|     ].filter(Boolean), | ||||
|     resolve: { | ||||
|       extensions: [".ts", ".js", ".json"], | ||||
|       alias: { | ||||
|         "lit/static-html$": "lit/static-html.js", | ||||
|         "lit/decorators$": "lit/decorators.js", | ||||
|         "lit/directive$": "lit/directive.js", | ||||
|         "lit/directives/until$": "lit/directives/until.js", | ||||
| @@ -203,17 +164,13 @@ const createRspackConfig = ({ | ||||
|         "lit/directives/if-defined$": "lit/directives/if-defined.js", | ||||
|         "lit/directives/guard$": "lit/directives/guard.js", | ||||
|         "lit/directives/cache$": "lit/directives/cache.js", | ||||
|         "lit/directives/join$": "lit/directives/join.js", | ||||
|         "lit/directives/repeat$": "lit/directives/repeat.js", | ||||
|         "lit/directives/live$": "lit/directives/live.js", | ||||
|         "lit/directives/keyed$": "lit/directives/keyed.js", | ||||
|         "lit/polyfill-support$": "lit/polyfill-support.js", | ||||
|         "@lit-labs/virtualizer/layouts/grid": | ||||
|           "@lit-labs/virtualizer/layouts/grid.js", | ||||
|         "@lit-labs/virtualizer/polyfills/resize-observer-polyfill/ResizeObserver": | ||||
|           "@lit-labs/virtualizer/polyfills/resize-observer-polyfill/ResizeObserver.js", | ||||
|         "@lit-labs/observers/resize-controller": | ||||
|           "@lit-labs/observers/resize-controller.js", | ||||
|       }, | ||||
|     }, | ||||
|     output: { | ||||
| @@ -221,13 +178,14 @@ const createRspackConfig = ({ | ||||
|       filename: ({ chunk }) => | ||||
|         !isProdBuild || isStatsBuild || dontHash.has(chunk.name) | ||||
|           ? "[name].js" | ||||
|           : "[name].[contenthash].js", | ||||
|           : "[name]-[contenthash].js", | ||||
|       chunkFilename: | ||||
|         isProdBuild && !isStatsBuild ? "[name].[contenthash].js" : "[name].js", | ||||
|         isProdBuild && !isStatsBuild ? "[id]-[contenthash].js" : "[name].js", | ||||
|       assetModuleFilename: | ||||
|         isProdBuild && !isStatsBuild ? "[id].[contenthash][ext]" : "[id][ext]", | ||||
|       crossOriginLoading: "use-credentials", | ||||
|         isProdBuild && !isStatsBuild ? "[id]-[contenthash][ext]" : "[id][ext]", | ||||
|       hashFunction: "xxhash64", | ||||
|       hashDigest: "base64url", | ||||
|       hashDigestLength: 11, // full length of 64 bit base64url
 | ||||
|       path: outputPath, | ||||
|       publicPath, | ||||
|       // To silence warning in worker plugin
 | ||||
| @@ -257,7 +215,6 @@ const createRspackConfig = ({ | ||||
|       ), | ||||
|     }, | ||||
|     experiments: { | ||||
|       layers: true, | ||||
|       outputModule: true, | ||||
|     }, | ||||
|   }; | ||||
| @@ -269,17 +226,17 @@ const createAppConfig = ({ | ||||
|   isStatsBuild, | ||||
|   isTestBuild, | ||||
| }) => | ||||
|   createRspackConfig( | ||||
|   createWebpackConfig( | ||||
|     bundle.config.app({ isProdBuild, latestBuild, isStatsBuild, isTestBuild }) | ||||
|   ); | ||||
| 
 | ||||
| const createDemoConfig = ({ isProdBuild, latestBuild, isStatsBuild }) => | ||||
|   createRspackConfig( | ||||
|   createWebpackConfig( | ||||
|     bundle.config.demo({ isProdBuild, latestBuild, isStatsBuild }) | ||||
|   ); | ||||
| 
 | ||||
| const createCastConfig = ({ isProdBuild, latestBuild }) => | ||||
|   createRspackConfig(bundle.config.cast({ isProdBuild, latestBuild })); | ||||
|   createWebpackConfig(bundle.config.cast({ isProdBuild, latestBuild })); | ||||
| 
 | ||||
| const createHassioConfig = ({ | ||||
|   isProdBuild, | ||||
| @@ -287,7 +244,7 @@ const createHassioConfig = ({ | ||||
|   isStatsBuild, | ||||
|   isTestBuild, | ||||
| }) => | ||||
|   createRspackConfig( | ||||
|   createWebpackConfig( | ||||
|     bundle.config.hassio({ | ||||
|       isProdBuild, | ||||
|       latestBuild, | ||||
| @@ -297,10 +254,7 @@ const createHassioConfig = ({ | ||||
|   ); | ||||
| 
 | ||||
| const createGalleryConfig = ({ isProdBuild, latestBuild }) => | ||||
|   createRspackConfig(bundle.config.gallery({ isProdBuild, latestBuild })); | ||||
| 
 | ||||
| const createLandingPageConfig = ({ isProdBuild, latestBuild }) => | ||||
|   createRspackConfig(bundle.config.landingPage({ isProdBuild, latestBuild })); | ||||
|   createWebpackConfig(bundle.config.gallery({ isProdBuild, latestBuild })); | ||||
| 
 | ||||
| module.exports = { | ||||
|   createAppConfig, | ||||
| @@ -308,6 +262,5 @@ module.exports = { | ||||
|   createCastConfig, | ||||
|   createHassioConfig, | ||||
|   createGalleryConfig, | ||||
|   createRspackConfig, | ||||
|   createLandingPageConfig, | ||||
|   createWebpackConfig, | ||||
| }; | ||||
| @@ -25,7 +25,7 @@ Home Assistant Cast is made up of two separate applications: | ||||
|  | ||||
| ### Setting dev variables | ||||
|  | ||||
| Open `src/cast/dev_const.ts` and change `CAST_DEV_APP_ID` to the ID of the app you just created. And set the `CAST_DEV_HASS_URL` to the url of your development machine. | ||||
| Open `src/cast/dev_const.ts` and change `CAST_DEV_APP_ID` to the ID of the app you just created. And set the `CAST_DEV_HASS_URL` to the url of you development machine. | ||||
|  | ||||
| ### Changing configuration | ||||
|  | ||||
|   | ||||
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 9.8 KiB | 
| @@ -14,5 +14,5 @@ | ||||
|   "name": "Home Assistant Cast", | ||||
|   "short_name": "HA Cast", | ||||
|   "start_url": "/?homescreen=1", | ||||
|   "theme_color": "#009ac7" | ||||
|   "theme_color": "#03A9F4" | ||||
| } | ||||
|   | ||||
| @@ -1,5 +0,0 @@ | ||||
| "use strict"; | ||||
|  | ||||
| self.addEventListener("fetch", (event) => { | ||||
|   event.respondWith(fetch(event.request)); | ||||
| }); | ||||
							
								
								
									
										10
									
								
								cast/rollup.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								cast/rollup.config.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| import rollup from "../build-scripts/rollup.cjs"; | ||||
| import env from "../build-scripts/env.cjs"; | ||||
|  | ||||
| const config = rollup.createCastConfig({ | ||||
|   isProdBuild: env.isProdBuild(), | ||||
|   latestBuild: true, | ||||
|   isStatsBuild: env.isStatsBuild(), | ||||
| }); | ||||
|  | ||||
| export default { ...config.inputOptions, output: config.outputOptions }; | ||||
| @@ -36,7 +36,13 @@ | ||||
|   </head> | ||||
|   <body> | ||||
|     <%= renderTemplate("../../../src/html/_js_base.html.template") %> | ||||
|     <%= renderTemplate("../../../src/html/_script_loader.html.template") %> | ||||
|     <script> | ||||
|       <% for (const entry of latestEntryJS) { %> | ||||
|         import("<%= entry %>"); | ||||
|       <% } %> | ||||
|       window.latestJS = true; | ||||
|     </script> | ||||
|     <%= renderTemplate("../../../src/html/_script_load_es5.html.template") %> | ||||
|     <hc-layout subtitle="FAQ"> | ||||
|       <style> | ||||
|         a { | ||||
| @@ -139,7 +145,7 @@ | ||||
|         </p> | ||||
|       </div> | ||||
|  | ||||
|       <div class="section-header">What does Home Assistant Cast do?</div> | ||||
|       <div class="section-header">Wat does Home Assistant Cast do?</div> | ||||
|       <div class="card-content"> | ||||
|         <p> | ||||
|           Home Assistant Cast is a receiver application for the Chromecast. When | ||||
| @@ -226,5 +232,17 @@ http: | ||||
|         </p> | ||||
|       </div> | ||||
|     </hc-layout> | ||||
|  | ||||
|     <script> | ||||
|       var _gaq = [["_setAccount", "UA-57927901-9"], ["_trackPageview"]]; | ||||
|       (function (d, t) { | ||||
|         var g = d.createElement(t), | ||||
|           s = d.getElementsByTagName(t)[0]; | ||||
|         g.src = | ||||
|           ("https:" == location.protocol ? "//ssl" : "//www") + | ||||
|           ".google-analytics.com/ga.js"; | ||||
|         s.parentNode.insertBefore(g, s); | ||||
|       })(document, "script"); | ||||
|     </script> | ||||
|   </body> | ||||
| </html> | ||||
|   | ||||
| @@ -13,9 +13,15 @@ | ||||
|     <%= renderTemplate("_social_meta.html.template") %> | ||||
|   </head> | ||||
|   <body> | ||||
|     <hc-connect></hc-connect> | ||||
|     <%= renderTemplate("../../../src/html/_js_base.html.template") %> | ||||
|     <%= renderTemplate("../../../src/html/_script_loader.html.template") %> | ||||
|     <hc-connect></hc-connect> | ||||
|     <script> | ||||
|       <% for (const entry of latestEntryJS) { %> | ||||
|         import("<%= entry %>"); | ||||
|       <% } %> | ||||
|       window.latestJS = true; | ||||
|     </script> | ||||
|     <%= renderTemplate("../../../src/html/_script_load_es5.html.template") %> | ||||
|     <script> | ||||
|     (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ | ||||
|     (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), | ||||
|   | ||||
| @@ -14,10 +14,22 @@ | ||||
|         --background-color: #41bdf5; | ||||
|       } | ||||
|     </style> | ||||
|     <script> | ||||
|       var _gaq=[['_setAccount','UA-57927901-10'],['_trackPageview']]; | ||||
|       (function(d,t){var g=d.createElement(t),s=d.getElementsByTagName(t)[0]; | ||||
|       g.src=('https:'==location.protocol?'//ssl':'//www')+'.google-analytics.com/ga.js'; | ||||
|       s.parentNode.insertBefore(g,s)}(document,'script')); | ||||
|     </script> | ||||
|   </head> | ||||
|   <body> | ||||
|     <cast-media-player></cast-media-player> | ||||
|     <%= renderTemplate("../../../src/html/_js_base.html.template") %> | ||||
|     <%= renderTemplate("../../../src/html/_script_loader.html.template") %> | ||||
|     <cast-media-player></cast-media-player> | ||||
|     <script> | ||||
|       <% for (const entry of latestEntryJS) { %> | ||||
|         import("<%= entry %>"); | ||||
|       <% } %> | ||||
|       window.latestJS = true; | ||||
|     </script> | ||||
|     <%= renderTemplate("../../../src/html/_script_load_es5.html.template") %> | ||||
|   </body> | ||||
| </html> | ||||
|   | ||||
| @@ -7,7 +7,14 @@ | ||||
|   <%= renderTemplate("../../../src/html/_style_base.html.template") %> | ||||
|   <style> | ||||
|     body { | ||||
|       background-color: white; | ||||
|       font-size: initial; | ||||
|     } | ||||
|   </style> | ||||
|   <script> | ||||
|   var _gaq=[['_setAccount','UA-57927901-10'],['_trackPageview']]; | ||||
|   (function(d,t){var g=d.createElement(t),s=d.getElementsByTagName(t)[0]; | ||||
|   g.src=('https:'==location.protocol?'//ssl':'//www')+'.google-analytics.com/ga.js'; | ||||
|   s.parentNode.insertBefore(g,s)}(document,'script')); | ||||
|   </script> | ||||
| </html> | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import "../../../src/resources/safari-14-attachshadow-patch"; | ||||
| import "../../../src/resources/ha-style"; | ||||
| import "../../../src/resources/roboto"; | ||||
| import "./layout/hc-connect"; | ||||
|  | ||||
| import("../../../src/resources/append-ha-style"); | ||||
|   | ||||
| @@ -1,10 +1,11 @@ | ||||
| import type { ActionDetail } from "@material/mwc-list/mwc-list"; | ||||
| import { mdiCast, mdiCastConnected, mdiViewDashboard } from "@mdi/js"; | ||||
| import type { Auth, Connection } from "home-assistant-js-websocket"; | ||||
| import type { TemplateResult } from "lit"; | ||||
| import { LitElement, css, html } from "lit"; | ||||
| import "@material/mwc-button/mwc-button"; | ||||
| import { mdiCast, mdiCastConnected } from "@mdi/js"; | ||||
| import "@polymer/paper-item/paper-icon-item"; | ||||
| import "@polymer/paper-listbox/paper-listbox"; | ||||
| import { Auth, Connection } from "home-assistant-js-websocket"; | ||||
| import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import type { CastManager } from "../../../../src/cast/cast_manager"; | ||||
| import { CastManager } from "../../../../src/cast/cast_manager"; | ||||
| import { | ||||
|   castSendShowLovelaceView, | ||||
|   ensureConnectedCastSession, | ||||
| @@ -17,34 +18,30 @@ import { | ||||
| import { atLeastVersion } from "../../../../src/common/config/version"; | ||||
| import { toggleAttribute } from "../../../../src/common/dom/toggle_attribute"; | ||||
| import "../../../../src/components/ha-icon"; | ||||
| import "../../../../src/components/ha-list"; | ||||
| import "../../../../src/components/ha-button"; | ||||
| import "../../../../src/components/ha-list-item"; | ||||
| import "../../../../src/components/ha-svg-icon"; | ||||
| import { | ||||
|   getLegacyLovelaceCollection, | ||||
|   getLovelaceCollection, | ||||
|   LovelaceConfig, | ||||
| } from "../../../../src/data/lovelace"; | ||||
| import { isStrategyDashboard } from "../../../../src/data/lovelace/config/types"; | ||||
| import type { LovelaceViewConfig } from "../../../../src/data/lovelace/config/view"; | ||||
| import "../../../../src/layouts/hass-loading-screen"; | ||||
| import { generateDefaultViewConfig } from "../../../../src/panels/lovelace/common/generate-lovelace-config"; | ||||
| import "./hc-layout"; | ||||
|  | ||||
| @customElement("hc-cast") | ||||
| class HcCast extends LitElement { | ||||
|   @property({ attribute: false }) public auth!: Auth; | ||||
|   @property() public auth!: Auth; | ||||
|  | ||||
|   @property({ attribute: false }) public connection!: Connection; | ||||
|   @property() public connection!: Connection; | ||||
|  | ||||
|   @property({ attribute: false }) public castManager!: CastManager; | ||||
|   @property() public castManager!: CastManager; | ||||
|  | ||||
|   @state() private askWrite = false; | ||||
|  | ||||
|   @state() private lovelaceViews?: LovelaceViewConfig[] | null; | ||||
|   @state() private lovelaceConfig?: LovelaceConfig | null; | ||||
|  | ||||
|   protected render(): TemplateResult { | ||||
|     if (this.lovelaceViews === undefined) { | ||||
|     if (this.lovelaceConfig === undefined) { | ||||
|       return html`<hass-loading-screen no-toolbar></hass-loading-screen>`; | ||||
|     } | ||||
|  | ||||
| @@ -62,20 +59,12 @@ class HcCast extends LitElement { | ||||
|               <p class="question action-item"> | ||||
|                 Stay logged in? | ||||
|                 <span> | ||||
|                   <ha-button | ||||
|                     appearance="plain" | ||||
|                     size="small" | ||||
|                     @click=${this._handleSaveTokens} | ||||
|                   > | ||||
|                   <mwc-button @click=${this._handleSaveTokens}> | ||||
|                     YES | ||||
|                   </ha-button> | ||||
|                   <ha-button | ||||
|                     appearance="plain" | ||||
|                     size="small" | ||||
|                     @click=${this._handleSkipSaveTokens} | ||||
|                   > | ||||
|                   </mwc-button> | ||||
|                   <mwc-button @click=${this._handleSkipSaveTokens}> | ||||
|                     NO | ||||
|                   </ha-button> | ||||
|                   </mwc-button> | ||||
|                 </span> | ||||
|               </p> | ||||
|             ` | ||||
| @@ -83,67 +72,54 @@ class HcCast extends LitElement { | ||||
|         ${error | ||||
|           ? html` <div class="card-content">${error}</div> ` | ||||
|           : !this.castManager.status | ||||
|             ? html` | ||||
|                 <p class="center-item"> | ||||
|                   <ha-button @click=${this._handleLaunch}> | ||||
|                     <ha-svg-icon slot="start" .path=${mdiCast}></ha-svg-icon> | ||||
|                     Start Casting | ||||
|                   </ha-button> | ||||
|                 </p> | ||||
|               ` | ||||
|             : html` | ||||
|                 <div class="section-header">PICK A VIEW</div> | ||||
|                 <ha-list @action=${this._handlePickView} activatable> | ||||
|                   ${( | ||||
|                     this.lovelaceViews ?? [ | ||||
|                       generateDefaultViewConfig({}, {}, {}, {}, () => ""), | ||||
|                     ] | ||||
|                   ).map( | ||||
|                     (view, idx) => html` | ||||
|                       <ha-list-item | ||||
|                         graphic="avatar" | ||||
|                         .activated=${this.castManager.status?.lovelacePath === | ||||
|                         (view.path ?? idx)} | ||||
|                         .selected=${this.castManager.status?.lovelacePath === | ||||
|                         (view.path ?? idx)} | ||||
|                       > | ||||
|                         ${view.title || view.path || "Unnamed view"} | ||||
|                         ${view.icon | ||||
|                           ? html` | ||||
|                               <ha-icon | ||||
|                                 .icon=${view.icon} | ||||
|                                 slot="graphic" | ||||
|                               ></ha-icon> | ||||
|                             ` | ||||
|                           : html`<ha-svg-icon | ||||
|           ? html` | ||||
|               <p class="center-item"> | ||||
|                 <mwc-button raised @click=${this._handleLaunch}> | ||||
|                   <ha-svg-icon .path=${mdiCast}></ha-svg-icon> | ||||
|                   Start Casting | ||||
|                 </mwc-button> | ||||
|               </p> | ||||
|             ` | ||||
|           : html` | ||||
|               <div class="section-header">PICK A VIEW</div> | ||||
|               <paper-listbox | ||||
|                 attr-for-selected="data-path" | ||||
|                 .selected=${this.castManager.status.lovelacePath || ""} | ||||
|               > | ||||
|                 ${(this.lovelaceConfig | ||||
|                   ? this.lovelaceConfig.views | ||||
|                   : [generateDefaultViewConfig({}, {}, {}, {}, () => "")] | ||||
|                 ).map( | ||||
|                   (view, idx) => html` | ||||
|                     <paper-icon-item | ||||
|                       @click=${this._handlePickView} | ||||
|                       data-path=${view.path || idx} | ||||
|                     > | ||||
|                       ${view.icon | ||||
|                         ? html` | ||||
|                             <ha-icon | ||||
|                               .icon=${view.icon} | ||||
|                               slot="item-icon" | ||||
|                               .path=${mdiViewDashboard} | ||||
|                             ></ha-svg-icon>`} | ||||
|                       </ha-list-item> | ||||
|                     ` | ||||
|                   )}</ha-list | ||||
|                 > | ||||
|               `} | ||||
|  | ||||
|                             ></ha-icon> | ||||
|                           ` | ||||
|                         : ""} | ||||
|                       ${view.title || view.path} | ||||
|                     </paper-icon-item> | ||||
|                   ` | ||||
|                 )} | ||||
|               </paper-listbox> | ||||
|             `} | ||||
|         <div class="card-actions"> | ||||
|           ${this.castManager.status | ||||
|             ? html` | ||||
|                 <ha-button appearance="plain" @click=${this._handleLaunch}> | ||||
|                   <ha-svg-icon | ||||
|                     slot="start" | ||||
|                     .path=${mdiCastConnected} | ||||
|                   ></ha-svg-icon> | ||||
|                 <mwc-button @click=${this._handleLaunch}> | ||||
|                   <ha-svg-icon .path=${mdiCastConnected}></ha-svg-icon> | ||||
|                   Manage | ||||
|                 </ha-button> | ||||
|                 </mwc-button> | ||||
|               ` | ||||
|             : ""} | ||||
|           <div class="spacer"></div> | ||||
|           <ha-button | ||||
|             variant="danger" | ||||
|             appearance="plain" | ||||
|             @click=${this._handleLogout} | ||||
|             >Log out</ha-button | ||||
|           > | ||||
|           <mwc-button @click=${this._handleLogout}>Log out</mwc-button> | ||||
|         </div> | ||||
|       </hc-layout> | ||||
|     `; | ||||
| @@ -160,15 +136,11 @@ class HcCast extends LitElement { | ||||
|     llColl.refresh().then( | ||||
|       () => { | ||||
|         llColl.subscribe((config) => { | ||||
|           if (isStrategyDashboard(config)) { | ||||
|             this.lovelaceViews = null; | ||||
|           } else { | ||||
|             this.lovelaceViews = config.views; | ||||
|           } | ||||
|           this.lovelaceConfig = config; | ||||
|         }); | ||||
|       }, | ||||
|       async () => { | ||||
|         this.lovelaceViews = null; | ||||
|         this.lovelaceConfig = null; | ||||
|       } | ||||
|     ); | ||||
|  | ||||
| @@ -187,7 +159,9 @@ class HcCast extends LitElement { | ||||
|     toggleAttribute( | ||||
|       this, | ||||
|       "hide-icons", | ||||
|       this.lovelaceViews ? !this.lovelaceViews.some((view) => view.icon) : true | ||||
|       this.lovelaceConfig | ||||
|         ? !this.lovelaceConfig.views.some((view) => view.icon) | ||||
|         : true | ||||
|     ); | ||||
|   } | ||||
|  | ||||
| @@ -204,8 +178,8 @@ class HcCast extends LitElement { | ||||
|     this.castManager.requestSession(); | ||||
|   } | ||||
|  | ||||
|   private async _handlePickView(ev: CustomEvent<ActionDetail>) { | ||||
|     const path = this.lovelaceViews![ev.detail.index].path ?? ev.detail.index; | ||||
|   private async _handlePickView(ev: Event) { | ||||
|     const path = (ev.currentTarget as any).getAttribute("data-path"); | ||||
|     await ensureConnectedCastSession(this.castManager!, this.auth!); | ||||
|     castSendShowLovelaceView(this.castManager, this.auth.data.hassUrl, path); | ||||
|   } | ||||
| @@ -219,65 +193,83 @@ class HcCast extends LitElement { | ||||
|       } | ||||
|       this.connection.close(); | ||||
|       location.reload(); | ||||
|     } catch (_err: any) { | ||||
|     } catch (err: any) { | ||||
|       alert("Unable to log out!"); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   static styles = css` | ||||
|     .center-item { | ||||
|       display: flex; | ||||
|       justify-content: space-around; | ||||
|     } | ||||
|   static get styles(): CSSResultGroup { | ||||
|     return css` | ||||
|       .center-item { | ||||
|         display: flex; | ||||
|         justify-content: space-around; | ||||
|       } | ||||
|  | ||||
|     .action-item { | ||||
|       display: flex; | ||||
|       align-items: center; | ||||
|       justify-content: space-between; | ||||
|     } | ||||
|       .action-item { | ||||
|         display: flex; | ||||
|         align-items: center; | ||||
|         justify-content: space-between; | ||||
|       } | ||||
|  | ||||
|     .question { | ||||
|       position: relative; | ||||
|       padding: 8px 16px; | ||||
|     } | ||||
|       .question { | ||||
|         position: relative; | ||||
|         padding: 8px 16px; | ||||
|       } | ||||
|  | ||||
|     .question:before { | ||||
|       border-radius: var(--ha-border-radius-sm); | ||||
|       position: absolute; | ||||
|       top: 0; | ||||
|       right: 0; | ||||
|       bottom: 0; | ||||
|       left: 0; | ||||
|       pointer-events: none; | ||||
|       content: ""; | ||||
|       background-color: var(--primary-color); | ||||
|       opacity: 0.12; | ||||
|       will-change: opacity; | ||||
|     } | ||||
|       .question:before { | ||||
|         border-radius: 4px; | ||||
|         position: absolute; | ||||
|         top: 0; | ||||
|         right: 0; | ||||
|         bottom: 0; | ||||
|         left: 0; | ||||
|         pointer-events: none; | ||||
|         content: ""; | ||||
|         background-color: var(--primary-color); | ||||
|         opacity: 0.12; | ||||
|         will-change: opacity; | ||||
|       } | ||||
|  | ||||
|     .connection, | ||||
|     .connection a { | ||||
|       color: var(--secondary-text-color); | ||||
|     } | ||||
|       .connection, | ||||
|       .connection a { | ||||
|         color: var(--secondary-text-color); | ||||
|       } | ||||
|  | ||||
|     ha-list-item ha-icon, | ||||
|     ha-list-item ha-svg-icon { | ||||
|       padding: 12px; | ||||
|       color: var(--secondary-text-color); | ||||
|     } | ||||
|       mwc-button ha-svg-icon { | ||||
|         margin-right: 8px; | ||||
|         height: 18px; | ||||
|       } | ||||
|  | ||||
|     :host([hide-icons]) ha-icon { | ||||
|       display: none; | ||||
|     } | ||||
|       paper-listbox { | ||||
|         padding-top: 0; | ||||
|       } | ||||
|  | ||||
|     .spacer { | ||||
|       flex: 1; | ||||
|     } | ||||
|       paper-listbox ha-icon { | ||||
|         padding: 12px; | ||||
|         color: var(--secondary-text-color); | ||||
|       } | ||||
|  | ||||
|     .card-content a { | ||||
|       color: var(--primary-color); | ||||
|     } | ||||
|   `; | ||||
|       paper-icon-item { | ||||
|         cursor: pointer; | ||||
|       } | ||||
|  | ||||
|       paper-icon-item[disabled] { | ||||
|         cursor: initial; | ||||
|       } | ||||
|  | ||||
|       :host([hide-icons]) paper-icon-item { | ||||
|         --paper-item-icon-width: 0px; | ||||
|       } | ||||
|  | ||||
|       .spacer { | ||||
|         flex: 1; | ||||
|       } | ||||
|  | ||||
|       .card-content a { | ||||
|         color: var(--primary-color); | ||||
|       } | ||||
|     `; | ||||
|   } | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|   | ||||
| @@ -1,22 +1,20 @@ | ||||
| import "@material/mwc-button"; | ||||
| import { mdiCastConnected, mdiCast } from "@mdi/js"; | ||||
| import type { | ||||
| import "@polymer/paper-input/paper-input"; | ||||
| import { | ||||
|   Auth, | ||||
|   Connection, | ||||
|   getAuthOptions, | ||||
| } from "home-assistant-js-websocket"; | ||||
| import { | ||||
|   createConnection, | ||||
|   ERR_CANNOT_CONNECT, | ||||
|   ERR_HASS_HOST_REQUIRED, | ||||
|   ERR_INVALID_AUTH, | ||||
|   ERR_INVALID_HTTPS_TO_HTTP, | ||||
|   getAuth, | ||||
|   getAuthOptions, | ||||
| } from "home-assistant-js-websocket"; | ||||
| import type { TemplateResult } from "lit"; | ||||
| import { css, html, LitElement } from "lit"; | ||||
| import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||
| import { customElement, state } from "lit/decorators"; | ||||
| import type { CastManager } from "../../../../src/cast/cast_manager"; | ||||
| import { getCastManager } from "../../../../src/cast/cast_manager"; | ||||
| import { CastManager, getCastManager } from "../../../../src/cast/cast_manager"; | ||||
| import { castSendShowDemo } from "../../../../src/cast/receiver_messages"; | ||||
| import { | ||||
|   loadTokens, | ||||
| @@ -26,8 +24,6 @@ import "../../../../src/components/ha-svg-icon"; | ||||
| import "../../../../src/layouts/hass-loading-screen"; | ||||
| import { registerServiceWorker } from "../../../../src/util/register-service-worker"; | ||||
| import "./hc-layout"; | ||||
| import "../../../../src/components/ha-textfield"; | ||||
| import "../../../../src/components/ha-button"; | ||||
|  | ||||
| const seeFAQ = (qid) => html` | ||||
|   See <a href="./faq.html${qid ? `#${qid}` : ""}">the FAQ</a> for more | ||||
| @@ -37,13 +33,13 @@ const translateErr = (err) => | ||||
|   err === ERR_CANNOT_CONNECT | ||||
|     ? "Unable to connect" | ||||
|     : err === ERR_HASS_HOST_REQUIRED | ||||
|       ? "Please enter a Home Assistant URL." | ||||
|       : err === ERR_INVALID_HTTPS_TO_HTTP | ||||
|         ? html` | ||||
|             Cannot connect to Home Assistant instances over "http://". | ||||
|             ${seeFAQ("https")} | ||||
|           ` | ||||
|         : `Unknown error (${err}).`; | ||||
|     ? "Please enter a Home Assistant URL." | ||||
|     : err === ERR_INVALID_HTTPS_TO_HTTP | ||||
|     ? html` | ||||
|         Cannot connect to Home Assistant instances over "http://". | ||||
|         ${seeFAQ("https")} | ||||
|       ` | ||||
|     : `Unknown error (${err}).`; | ||||
|  | ||||
| const INTRO = html` | ||||
|   <p> | ||||
| @@ -83,14 +79,11 @@ export class HcConnect extends LitElement { | ||||
|             Unable to connect to ${tokens!.hassUrl}. | ||||
|           </div> | ||||
|           <div class="card-actions"> | ||||
|             <ha-button appearance="plain" href="/">Retry</ha-button> | ||||
|             <a href="/"> | ||||
|               <mwc-button> Retry </mwc-button> | ||||
|             </a> | ||||
|             <div class="spacer"></div> | ||||
|             <ha-button | ||||
|               appearance="plain" | ||||
|               variant="danger" | ||||
|               @click=${this._handleLogout} | ||||
|               >Log out</ha-button | ||||
|             > | ||||
|             <mwc-button @click=${this._handleLogout}>Log out</mwc-button> | ||||
|           </div> | ||||
|         </hc-layout> | ||||
|       `; | ||||
| @@ -123,27 +116,26 @@ export class HcConnect extends LitElement { | ||||
|               To get started, enter your Home Assistant URL and click authorize. | ||||
|               If you want a preview instead, click the show demo button. | ||||
|             </p> | ||||
|             <ha-textfield | ||||
|               label="Home Assistant URL" | ||||
|               placeholder="https://abcdefghijklmnop.ui.nabu.casa" | ||||
|               @keydown=${this._handleInputKeyDown} | ||||
|             ></ha-textfield> | ||||
|             <p> | ||||
|               <paper-input | ||||
|                 label="Home Assistant URL" | ||||
|                 placeholder="https://abcdefghijklmnop.ui.nabu.casa" | ||||
|                 @keydown=${this._handleInputKeyDown} | ||||
|               ></paper-input> | ||||
|             </p> | ||||
|             ${this.error ? html` <p class="error">${this.error}</p> ` : ""} | ||||
|           </div> | ||||
|           <div class="card-actions"> | ||||
|             <ha-button appearance="plain" @click=${this._handleDemo}> | ||||
|             <mwc-button @click=${this._handleDemo}> | ||||
|               Show Demo | ||||
|               <ha-svg-icon | ||||
|                 slot="end" | ||||
|                 .path=${this.castManager.castState === "CONNECTED" | ||||
|                   ? mdiCastConnected | ||||
|                   : mdiCast} | ||||
|               ></ha-svg-icon> | ||||
|             </ha-button> | ||||
|             </mwc-button> | ||||
|             <div class="spacer"></div> | ||||
|             <ha-button appearance="plain" @click=${this._handleConnect} | ||||
|               >Authorize</ha-button | ||||
|             > | ||||
|             <mwc-button @click=${this._handleConnect}>Authorize</mwc-button> | ||||
|           </div> | ||||
|         </hc-layout> | ||||
|       `; | ||||
| @@ -204,7 +196,7 @@ export class HcConnect extends LitElement { | ||||
|   } | ||||
|  | ||||
|   private async _handleConnect() { | ||||
|     const inputEl = this.shadowRoot!.querySelector("ha-textfield")!; | ||||
|     const inputEl = this.shadowRoot!.querySelector("paper-input")!; | ||||
|     const value = inputEl.value || ""; | ||||
|     this.error = undefined; | ||||
|  | ||||
| @@ -221,7 +213,7 @@ export class HcConnect extends LitElement { | ||||
|     let url: URL; | ||||
|     try { | ||||
|       url = new URL(value); | ||||
|     } catch (_err: any) { | ||||
|     } catch (err: any) { | ||||
|       this.error = "Invalid URL"; | ||||
|       return; | ||||
|     } | ||||
| @@ -258,7 +250,7 @@ export class HcConnect extends LitElement { | ||||
|       this.loading = false; | ||||
|       return; | ||||
|     } finally { | ||||
|       // Clear url if we have an auth callback in url. | ||||
|       // Clear url if we have a auth callback in url. | ||||
|       if (location.search.includes("auth_callback=1")) { | ||||
|         history.replaceState(null, "", location.pathname); | ||||
|       } | ||||
| @@ -294,35 +286,37 @@ export class HcConnect extends LitElement { | ||||
|     try { | ||||
|       saveTokens(null); | ||||
|       location.reload(); | ||||
|     } catch (_err: any) { | ||||
|     } catch (err: any) { | ||||
|       alert("Unable to log out!"); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   static styles = css` | ||||
|     .card-content a { | ||||
|       color: var(--primary-color); | ||||
|     } | ||||
|     .card-actions a { | ||||
|       text-decoration: none; | ||||
|     } | ||||
|     .error { | ||||
|       color: red; | ||||
|       font-weight: var(--ha-font-weight-bold); | ||||
|     } | ||||
|   static get styles(): CSSResultGroup { | ||||
|     return css` | ||||
|       .card-content a { | ||||
|         color: var(--primary-color); | ||||
|       } | ||||
|       .card-actions a { | ||||
|         text-decoration: none; | ||||
|       } | ||||
|       .error { | ||||
|         color: red; | ||||
|         font-weight: bold; | ||||
|       } | ||||
|  | ||||
|     .error a { | ||||
|       color: darkred; | ||||
|     } | ||||
|       .error a { | ||||
|         color: darkred; | ||||
|       } | ||||
|  | ||||
|     .spacer { | ||||
|       flex: 1; | ||||
|     } | ||||
|       mwc-button ha-svg-icon { | ||||
|         margin-left: 8px; | ||||
|       } | ||||
|  | ||||
|     ha-textfield { | ||||
|       width: 100%; | ||||
|     } | ||||
|   `; | ||||
|       .spacer { | ||||
|         flex: 1; | ||||
|       } | ||||
|     `; | ||||
|   } | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|   | ||||
| @@ -1,19 +1,22 @@ | ||||
| import type { Auth, Connection, HassUser } from "home-assistant-js-websocket"; | ||||
| import { getUser } from "home-assistant-js-websocket"; | ||||
| import type { TemplateResult } from "lit"; | ||||
| import { css, html, LitElement } from "lit"; | ||||
| import { | ||||
|   Auth, | ||||
|   Connection, | ||||
|   getUser, | ||||
|   HassUser, | ||||
| } from "home-assistant-js-websocket"; | ||||
| import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||
| import { customElement, property } from "lit/decorators"; | ||||
| import "../../../../src/components/ha-card"; | ||||
|  | ||||
| @customElement("hc-layout") | ||||
| class HcLayout extends LitElement { | ||||
|   @property() public subtitle?: string; | ||||
|   @property() public subtitle?: string | undefined; | ||||
|  | ||||
|   @property({ attribute: false }) public auth?: Auth; | ||||
|   @property() public auth?: Auth; | ||||
|  | ||||
|   @property({ attribute: false }) public connection?: Connection; | ||||
|   @property() public connection?: Connection; | ||||
|  | ||||
|   @property({ attribute: false }) public user?: HassUser; | ||||
|   @property() public user?: HassUser; | ||||
|  | ||||
|   protected render(): TemplateResult { | ||||
|     return html` | ||||
| @@ -63,95 +66,96 @@ class HcLayout extends LitElement { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   static styles = css` | ||||
|     :host { | ||||
|       display: flex; | ||||
|       min-height: 100%; | ||||
|       align-items: center; | ||||
|       justify-content: center; | ||||
|       flex-direction: column; | ||||
|     } | ||||
|  | ||||
|     ha-card { | ||||
|       display: flex; | ||||
|       width: 100%; | ||||
|       max-width: 500px; | ||||
|     } | ||||
|  | ||||
|     .layout { | ||||
|       display: flex; | ||||
|       flex-direction: column; | ||||
|     } | ||||
|  | ||||
|     .card-header { | ||||
|       color: var(--ha-card-header-color, var(--primary-text-color)); | ||||
|       font-family: var(--ha-card-header-font-family, inherit); | ||||
|       font-size: var(--ha-card-header-font-size, var(--ha-font-size-2xl)); | ||||
|       letter-spacing: -0.012em; | ||||
|       line-height: var(--ha-line-height-condensed); | ||||
|       padding: 24px 16px 16px; | ||||
|       display: block; | ||||
|       margin: 0; | ||||
|     } | ||||
|  | ||||
|     .hero { | ||||
|       border-radius: var(--ha-border-radius-sm) var(--ha-border-radius-sm) | ||||
|         var(--ha-border-radius-square) var(--ha-border-radius-square); | ||||
|     } | ||||
|     .subtitle { | ||||
|       font-size: var(--ha-font-size-m); | ||||
|       color: var(--secondary-text-color); | ||||
|       line-height: initial; | ||||
|     } | ||||
|     .subtitle a { | ||||
|       color: var(--secondary-text-color); | ||||
|     } | ||||
|  | ||||
|     :host ::slotted(.card-content:not(:first-child)), | ||||
|     slot:not(:first-child)::slotted(.card-content) { | ||||
|       padding-top: 0px; | ||||
|       margin-top: -8px; | ||||
|     } | ||||
|  | ||||
|     :host ::slotted(.section-header) { | ||||
|       font-weight: var(--ha-font-weight-medium); | ||||
|       padding: 4px 16px; | ||||
|       text-transform: uppercase; | ||||
|     } | ||||
|  | ||||
|     :host ::slotted(.card-content) { | ||||
|       padding: 16px; | ||||
|       flex: 1; | ||||
|     } | ||||
|  | ||||
|     :host ::slotted(.card-actions) { | ||||
|       border-top: 1px solid #e8e8e8; | ||||
|       padding: 5px 16px; | ||||
|       display: flex; | ||||
|     } | ||||
|  | ||||
|     img { | ||||
|       width: 100%; | ||||
|     } | ||||
|  | ||||
|     .footer { | ||||
|       text-align: center; | ||||
|       font-size: var(--ha-font-size-s); | ||||
|       padding: 8px 0 24px; | ||||
|       color: var(--secondary-text-color); | ||||
|     } | ||||
|     .footer a { | ||||
|       color: var(--secondary-text-color); | ||||
|     } | ||||
|  | ||||
|     @media all and (max-width: 500px) { | ||||
|   static get styles(): CSSResultGroup { | ||||
|     return css` | ||||
|       :host { | ||||
|         justify-content: flex-start; | ||||
|         min-height: 90%; | ||||
|         margin-bottom: 30px; | ||||
|         display: flex; | ||||
|         min-height: 100%; | ||||
|         align-items: center; | ||||
|         justify-content: center; | ||||
|         flex-direction: column; | ||||
|       } | ||||
|     } | ||||
|   `; | ||||
|  | ||||
|       ha-card { | ||||
|         display: flex; | ||||
|         width: 100%; | ||||
|         max-width: 500px; | ||||
|       } | ||||
|  | ||||
|       .layout { | ||||
|         display: flex; | ||||
|         flex-direction: column; | ||||
|       } | ||||
|  | ||||
|       .card-header { | ||||
|         color: var(--ha-card-header-color, --primary-text-color); | ||||
|         font-family: var(--ha-card-header-font-family, inherit); | ||||
|         font-size: var(--ha-card-header-font-size, 24px); | ||||
|         letter-spacing: -0.012em; | ||||
|         line-height: 32px; | ||||
|         padding: 24px 16px 16px; | ||||
|         display: block; | ||||
|         margin: 0; | ||||
|       } | ||||
|  | ||||
|       .hero { | ||||
|         border-radius: 4px 4px 0 0; | ||||
|       } | ||||
|       .subtitle { | ||||
|         font-size: 14px; | ||||
|         color: var(--secondary-text-color); | ||||
|         line-height: initial; | ||||
|       } | ||||
|       .subtitle a { | ||||
|         color: var(--secondary-text-color); | ||||
|       } | ||||
|  | ||||
|       :host ::slotted(.card-content:not(:first-child)), | ||||
|       slot:not(:first-child)::slotted(.card-content) { | ||||
|         padding-top: 0px; | ||||
|         margin-top: -8px; | ||||
|       } | ||||
|  | ||||
|       :host ::slotted(.section-header) { | ||||
|         font-weight: 500; | ||||
|         padding: 4px 16px; | ||||
|         text-transform: uppercase; | ||||
|       } | ||||
|  | ||||
|       :host ::slotted(.card-content) { | ||||
|         padding: 16px; | ||||
|         flex: 1; | ||||
|       } | ||||
|  | ||||
|       :host ::slotted(.card-actions) { | ||||
|         border-top: 1px solid #e8e8e8; | ||||
|         padding: 5px 16px; | ||||
|         display: flex; | ||||
|       } | ||||
|  | ||||
|       img { | ||||
|         width: 100%; | ||||
|       } | ||||
|  | ||||
|       .footer { | ||||
|         text-align: center; | ||||
|         font-size: 12px; | ||||
|         padding: 8px 0 24px; | ||||
|         color: var(--secondary-text-color); | ||||
|       } | ||||
|       .footer a { | ||||
|         color: var(--secondary-text-color); | ||||
|       } | ||||
|  | ||||
|       @media all and (max-width: 500px) { | ||||
|         :host { | ||||
|           justify-content: flex-start; | ||||
|           min-height: 90%; | ||||
|           margin-bottom: 30px; | ||||
|         } | ||||
|       } | ||||
|     `; | ||||
|   } | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|   | ||||
| @@ -1,5 +1,4 @@ | ||||
| import type { Entity } from "../../../../src/fake_data/entity"; | ||||
| import { convertEntities } from "../../../../src/fake_data/entity"; | ||||
| import { convertEntities, Entity } from "../../../../src/fake_data/entity"; | ||||
|  | ||||
| export const castDemoEntities: () => Entity[] = () => | ||||
|   convertEntities({ | ||||
| @@ -75,7 +74,7 @@ export const castDemoEntities: () => Entity[] = () => | ||||
|         longitude: 4.8903147, | ||||
|         radius: 100, | ||||
|         friendly_name: "Home", | ||||
|         icon: "mdi:home", | ||||
|         icon: "hass:home", | ||||
|       }, | ||||
|     }, | ||||
|     "input_number.harmonyvolume": { | ||||
| @@ -88,7 +87,7 @@ export const castDemoEntities: () => Entity[] = () => | ||||
|         step: 1, | ||||
|         mode: "slider", | ||||
|         friendly_name: "Volume", | ||||
|         icon: "mdi:volume-high", | ||||
|         icon: "hass:volume-high", | ||||
|       }, | ||||
|     }, | ||||
|     "climate.upstairs": { | ||||
|   | ||||
| @@ -1,5 +1,7 @@ | ||||
| import type { LovelaceCardConfig } from "../../../../src/data/lovelace/config/card"; | ||||
| import type { LovelaceConfig } from "../../../../src/data/lovelace/config/types"; | ||||
| import { | ||||
|   LovelaceCardConfig, | ||||
|   LovelaceConfig, | ||||
| } from "../../../../src/data/lovelace"; | ||||
| import { castContext } from "../cast_context"; | ||||
|  | ||||
| export const castDemoLovelace: () => LovelaceConfig = () => { | ||||
| @@ -56,7 +58,7 @@ export const castDemoLovelace: () => LovelaceConfig = () => { | ||||
|                 type: "weblink", | ||||
|                 url: "/lovelace/climate", | ||||
|                 name: "Climate controls", | ||||
|                 icon: "mdi:arrow-right", | ||||
|                 icon: "hass:arrow-right", | ||||
|               }, | ||||
|             ], | ||||
|           }, | ||||
| @@ -76,7 +78,7 @@ export const castDemoLovelace: () => LovelaceConfig = () => { | ||||
|                 type: "weblink", | ||||
|                 url: "/lovelace/overview", | ||||
|                 name: "Back", | ||||
|                 icon: "mdi:arrow-left", | ||||
|                 icon: "hass:arrow-left", | ||||
|               }, | ||||
|             ], | ||||
|           }, | ||||
|   | ||||
| @@ -1,10 +1,10 @@ | ||||
| import { framework } from "./cast_framework"; | ||||
| import { CAST_NS } from "../../../src/cast/const"; | ||||
| import type { HassMessage } from "../../../src/cast/receiver_messages"; | ||||
| import { HassMessage } from "../../../src/cast/receiver_messages"; | ||||
| import "../../../src/resources/custom-card-support"; | ||||
| import { castContext } from "./cast_context"; | ||||
| import { HcMain } from "./layout/hc-main"; | ||||
| import type { ReceivedMessage } from "./types"; | ||||
| import { ReceivedMessage } from "./types"; | ||||
|  | ||||
| const lovelaceController = new HcMain(); | ||||
| document.body.append(lovelaceController); | ||||
|   | ||||
| @@ -1,11 +1,13 @@ | ||||
| import { html, nothing } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import { mockHistory } from "../../../../demo/src/stubs/history"; | ||||
| import type { LovelaceConfig } from "../../../../src/data/lovelace/config/types"; | ||||
| import type { MockHomeAssistant } from "../../../../src/fake_data/provide_hass"; | ||||
| import { provideHass } from "../../../../src/fake_data/provide_hass"; | ||||
| import { LovelaceConfig } from "../../../../src/data/lovelace"; | ||||
| import { | ||||
|   MockHomeAssistant, | ||||
|   provideHass, | ||||
| } from "../../../../src/fake_data/provide_hass"; | ||||
| import { HassElement } from "../../../../src/state/hass-element"; | ||||
| import type { HomeAssistant } from "../../../../src/types"; | ||||
| import { HomeAssistant } from "../../../../src/types"; | ||||
| import { castDemoEntities } from "../demo/cast-demo-entities"; | ||||
| import { castDemoLovelace } from "../demo/cast-demo-lovelace"; | ||||
| import "./hc-lovelace"; | ||||
|   | ||||
| @@ -1,7 +1,6 @@ | ||||
| import type { TemplateResult } from "lit"; | ||||
| import { css, html, LitElement } from "lit"; | ||||
| import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||
| import { customElement, property } from "lit/decorators"; | ||||
| import type { HomeAssistant } from "../../../../src/types"; | ||||
| import { HomeAssistant } from "../../../../src/types"; | ||||
|  | ||||
| @customElement("hc-launch-screen") | ||||
| class HcLaunchScreen extends LitElement { | ||||
| @@ -13,8 +12,8 @@ class HcLaunchScreen extends LitElement { | ||||
|     return html` | ||||
|       <div class="container"> | ||||
|         <img | ||||
|           alt="Nabu Casa logo on left, Home Assistant logo on right, and red heart in center" | ||||
|           src="https://cast.home-assistant.io/images/nabu-loves-hass.png" | ||||
|           alt="Home Assistant logo on left, Nabu Casa logo on right, and red heart in center" | ||||
|           src="https://www.home-assistant.io/images/blog/2018-09-thinking-big/social.png" | ||||
|         /> | ||||
|         <div class="status"> | ||||
|           ${this.hass ? "Connected" : "Not Connected"} | ||||
| @@ -24,29 +23,31 @@ class HcLaunchScreen extends LitElement { | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   static styles = css` | ||||
|     :host { | ||||
|       display: block; | ||||
|       height: 100vh; | ||||
|       background-color: #f2f4f9; | ||||
|       font-size: var(--ha-font-size-2xl); | ||||
|     } | ||||
|     .container { | ||||
|       display: flex; | ||||
|       flex-direction: column; | ||||
|       text-align: center; | ||||
|       align-items: center; | ||||
|       height: 100%; | ||||
|       justify-content: space-evenly; | ||||
|     } | ||||
|     img { | ||||
|       max-width: 80%; | ||||
|       object-fit: cover; | ||||
|     } | ||||
|     .status { | ||||
|       color: #1d2126; | ||||
|     } | ||||
|   `; | ||||
|   static get styles(): CSSResultGroup { | ||||
|     return css` | ||||
|       :host { | ||||
|         display: block; | ||||
|         height: 100vh; | ||||
|         padding-top: 64px; | ||||
|         background-color: white; | ||||
|         font-size: 24px; | ||||
|       } | ||||
|       .container { | ||||
|         display: flex; | ||||
|         flex-direction: column; | ||||
|         text-align: center; | ||||
|       } | ||||
|       img { | ||||
|         width: 717px; | ||||
|         height: 376px; | ||||
|         display: block; | ||||
|         margin: 0 auto; | ||||
|       } | ||||
|       .status { | ||||
|         padding-right: 54px; | ||||
|       } | ||||
|     `; | ||||
|   } | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|   | ||||
| @@ -1,14 +1,11 @@ | ||||
| import { css, html, LitElement, type TemplateResult } from "lit"; | ||||
| import { customElement, property } from "lit/decorators"; | ||||
| import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||
| import { customElement, property, query } from "lit/decorators"; | ||||
| import { fireEvent } from "../../../../src/common/dom/fire_event"; | ||||
| import type { LovelaceConfig } from "../../../../src/data/lovelace/config/types"; | ||||
| import { getPanelTitleFromUrlPath } from "../../../../src/data/panel"; | ||||
| import type { Lovelace } from "../../../../src/panels/lovelace/types"; | ||||
| import { LovelaceConfig } from "../../../../src/data/lovelace"; | ||||
| import { Lovelace } from "../../../../src/panels/lovelace/types"; | ||||
| import "../../../../src/panels/lovelace/views/hui-view"; | ||||
| import "../../../../src/panels/lovelace/views/hui-view-container"; | ||||
| import type { HomeAssistant } from "../../../../src/types"; | ||||
| import { HomeAssistant } from "../../../../src/types"; | ||||
| import "./hc-launch-screen"; | ||||
| import "../../../../src/panels/lovelace/views/hui-view-background"; | ||||
|  | ||||
| (window as any).loadCardHelpers = () => | ||||
|   import("../../../../src/panels/lovelace/custom-card-helpers"); | ||||
| @@ -17,12 +14,13 @@ import "../../../../src/panels/lovelace/views/hui-view-background"; | ||||
| class HcLovelace extends LitElement { | ||||
|   @property({ attribute: false }) public hass!: HomeAssistant; | ||||
|  | ||||
|   @property({ attribute: false }) | ||||
|   public lovelaceConfig!: LovelaceConfig; | ||||
|   @property({ attribute: false }) public lovelaceConfig!: LovelaceConfig; | ||||
|  | ||||
|   @property({ attribute: false }) public viewPath?: string | number | null; | ||||
|   @property() public viewPath?: string | number; | ||||
|  | ||||
|   @property({ attribute: false }) public urlPath: string | null = null; | ||||
|   @property() public urlPath: string | null = null; | ||||
|  | ||||
|   @query("hui-view") private _huiView?: HTMLElement; | ||||
|  | ||||
|   protected render(): TemplateResult { | ||||
|     const index = this._viewIndex; | ||||
| @@ -45,22 +43,13 @@ class HcLovelace extends LitElement { | ||||
|       saveConfig: async () => undefined, | ||||
|       deleteConfig: async () => undefined, | ||||
|       setEditMode: () => undefined, | ||||
|       showToast: () => undefined, | ||||
|     }; | ||||
|  | ||||
|     const viewConfig = this.lovelaceConfig.views[index]; | ||||
|     const background = viewConfig.background || this.lovelaceConfig.background; | ||||
|  | ||||
|     return html` | ||||
|       <hui-view-container .hass=${this.hass} .theme=${viewConfig.theme}> | ||||
|         <hui-view-background .hass=${this.hass} .background=${background}> | ||||
|         </hui-view-background> | ||||
|         <hui-view | ||||
|           .hass=${this.hass} | ||||
|           .lovelace=${lovelace} | ||||
|           .index=${index} | ||||
|         ></hui-view> | ||||
|       </hui-view-container> | ||||
|       <hui-view | ||||
|         .hass=${this.hass} | ||||
|         .lovelace=${lovelace} | ||||
|         .index=${index} | ||||
|       ></hui-view> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
| @@ -71,12 +60,7 @@ class HcLovelace extends LitElement { | ||||
|       const index = this._viewIndex; | ||||
|  | ||||
|       if (index !== undefined) { | ||||
|         const title = getPanelTitleFromUrlPath( | ||||
|           this.hass, | ||||
|           this.urlPath || "lovelace" | ||||
|         ); | ||||
|  | ||||
|         const dashboardTitle = title || this.urlPath; | ||||
|         const dashboardTitle = this.lovelaceConfig.title || this.urlPath; | ||||
|  | ||||
|         const viewTitle = | ||||
|           this.lovelaceConfig.views[index].title || | ||||
| @@ -90,14 +74,24 @@ class HcLovelace extends LitElement { | ||||
|                 }${viewTitle || ""}` | ||||
|               : undefined, | ||||
|         }); | ||||
|  | ||||
|         const configBackground = | ||||
|           this.lovelaceConfig.views[index].background || | ||||
|           this.lovelaceConfig.background; | ||||
|  | ||||
|         if (configBackground) { | ||||
|           this._huiView!.style.setProperty( | ||||
|             "--lovelace-background", | ||||
|             configBackground | ||||
|           ); | ||||
|         } else { | ||||
|           this._huiView!.style.removeProperty("--lovelace-background"); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private get _viewIndex() { | ||||
|     if (this.viewPath === null) { | ||||
|       return 0; | ||||
|     } | ||||
|     const selectedView = this.viewPath; | ||||
|     const selectedViewInt = parseInt(selectedView as string, 10); | ||||
|     for (let i = 0; i < this.lovelaceConfig.views.length; i++) { | ||||
| @@ -111,18 +105,24 @@ class HcLovelace extends LitElement { | ||||
|     return undefined; | ||||
|   } | ||||
|  | ||||
|   static styles = css` | ||||
|     hui-view-container { | ||||
|       display: flex; | ||||
|       position: relative; | ||||
|       min-height: 100vh; | ||||
|       box-sizing: border-box; | ||||
|     } | ||||
|     hui-view-container > * { | ||||
|       flex: 1 1 100%; | ||||
|       max-width: 100%; | ||||
|     } | ||||
|   `; | ||||
|   static get styles(): CSSResultGroup { | ||||
|     return css` | ||||
|       :host { | ||||
|         min-height: 100vh; | ||||
|         height: 0; | ||||
|         display: flex; | ||||
|         flex-direction: column; | ||||
|         box-sizing: border-box; | ||||
|         background: var(--primary-background-color); | ||||
|       } | ||||
|       :host > * { | ||||
|         flex: 1; | ||||
|       } | ||||
|       hui-view { | ||||
|         background: var(--lovelace-background, var(--primary-background-color)); | ||||
|       } | ||||
|     `; | ||||
|   } | ||||
| } | ||||
|  | ||||
| export interface CastViewChanged { | ||||
|   | ||||
| @@ -1,46 +1,38 @@ | ||||
| import type { UnsubscribeFunc } from "home-assistant-js-websocket"; | ||||
| import { createConnection, getAuth } from "home-assistant-js-websocket"; | ||||
| import type { TemplateResult } from "lit"; | ||||
| import { html } from "lit"; | ||||
| import { | ||||
|   createConnection, | ||||
|   getAuth, | ||||
|   UnsubscribeFunc, | ||||
| } from "home-assistant-js-websocket"; | ||||
| import { html, TemplateResult } from "lit"; | ||||
| import { customElement, state } from "lit/decorators"; | ||||
| import { CAST_NS } from "../../../../src/cast/const"; | ||||
| import type { | ||||
| import { | ||||
|   ConnectMessage, | ||||
|   GetStatusMessage, | ||||
|   HassMessage, | ||||
|   ShowDemoMessage, | ||||
|   ShowLovelaceViewMessage, | ||||
| } from "../../../../src/cast/receiver_messages"; | ||||
| import type { | ||||
| import { | ||||
|   ReceiverErrorCode, | ||||
|   ReceiverErrorMessage, | ||||
|   ReceiverStatusMessage, | ||||
| } from "../../../../src/cast/sender_messages"; | ||||
| import { ReceiverErrorCode } from "../../../../src/cast/sender_messages"; | ||||
| import { atLeastVersion } from "../../../../src/common/config/version"; | ||||
| import { isNavigationClick } from "../../../../src/common/dom/is-navigation-click"; | ||||
| import { | ||||
|   fetchResources, | ||||
|   getLegacyLovelaceCollection, | ||||
|   getLovelaceCollection, | ||||
| } from "../../../../src/data/lovelace"; | ||||
| import type { | ||||
|   LegacyLovelaceConfig, | ||||
|   LovelaceConfig, | ||||
|   LovelaceDashboardStrategyConfig, | ||||
| } from "../../../../src/data/lovelace/config/types"; | ||||
| import { isStrategyDashboard } from "../../../../src/data/lovelace/config/types"; | ||||
| import { fetchResources } from "../../../../src/data/lovelace/resource"; | ||||
| } from "../../../../src/data/lovelace"; | ||||
| import { loadLovelaceResources } from "../../../../src/panels/lovelace/common/load-resources"; | ||||
| import { HassElement } from "../../../../src/state/hass-element"; | ||||
| import { castContext } from "../cast_context"; | ||||
| import "./hc-launch-screen"; | ||||
| import { getPanelTitleFromUrlPath } from "../../../../src/data/panel"; | ||||
| import { checkLovelaceConfig } from "../../../../src/panels/lovelace/common/check-lovelace-config"; | ||||
|  | ||||
| const DEFAULT_CONFIG: LovelaceDashboardStrategyConfig = { | ||||
|   strategy: { | ||||
|     type: "original-states", | ||||
|   }, | ||||
| }; | ||||
| const DEFAULT_STRATEGY = "original-states"; | ||||
|  | ||||
| let resourcesLoaded = false; | ||||
| @customElement("hc-main") | ||||
| @@ -51,10 +43,10 @@ export class HcMain extends HassElement { | ||||
|  | ||||
|   @state() private _lovelacePath: string | number | null = null; | ||||
|  | ||||
|   @state() private _urlPath?: string | null; | ||||
|  | ||||
|   @state() private _error?: string; | ||||
|  | ||||
|   @state() private _urlPath?: string | null; | ||||
|  | ||||
|   private _hassUUID?: string; | ||||
|  | ||||
|   private _unsubLovelace?: UnsubscribeFunc; | ||||
| @@ -81,7 +73,7 @@ export class HcMain extends HassElement { | ||||
|  | ||||
|     if ( | ||||
|       !this._lovelaceConfig || | ||||
|       this._urlPath === undefined || | ||||
|       this._lovelacePath === null || | ||||
|       // Guard against part of HA not being loaded yet. | ||||
|       !this.hass || | ||||
|       !this.hass.states || | ||||
| @@ -99,18 +91,16 @@ export class HcMain extends HassElement { | ||||
|       <hc-lovelace | ||||
|         .hass=${this.hass} | ||||
|         .lovelaceConfig=${this._lovelaceConfig} | ||||
|         .urlPath=${this._urlPath} | ||||
|         .viewPath=${this._lovelacePath} | ||||
|         @config-refresh=${this._generateDefaultLovelaceConfig} | ||||
|         .urlPath=${this._urlPath} | ||||
|         @config-refresh=${this._generateLovelaceConfig} | ||||
|       ></hc-lovelace> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   protected firstUpdated(changedProps) { | ||||
|     super.firstUpdated(changedProps); | ||||
|     import("./hc-lovelace"); | ||||
|     import("../../../../src/resources/append-ha-style"); | ||||
|  | ||||
|     import("../second-load"); | ||||
|     window.addEventListener("location-changed", () => { | ||||
|       const panelPath = `/${this._urlPath || "lovelace"}/`; | ||||
|       if (location.pathname.startsWith(panelPath)) { | ||||
| @@ -144,10 +134,10 @@ export class HcMain extends HassElement { | ||||
|     } | ||||
|  | ||||
|     if (senderId) { | ||||
|       this._sendMessage(senderId, status); | ||||
|       this.sendMessage(senderId, status); | ||||
|     } else { | ||||
|       for (const sender of castContext.getSenders()) { | ||||
|         this._sendMessage(sender.id, status); | ||||
|         this.sendMessage(sender.id, status); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| @@ -164,10 +154,10 @@ export class HcMain extends HassElement { | ||||
|     }; | ||||
|  | ||||
|     if (senderId) { | ||||
|       this._sendMessage(senderId, error); | ||||
|       this.sendMessage(senderId, error); | ||||
|     } else { | ||||
|       for (const sender of castContext.getSenders()) { | ||||
|         this._sendMessage(sender.id, error); | ||||
|         this.sendMessage(sender.id, error); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| @@ -205,6 +195,7 @@ export class HcMain extends HassElement { | ||||
|           expires_in: 0, | ||||
|         }), | ||||
|       }); | ||||
|       this._hassUUID = msg.hassUUID; | ||||
|     } catch (err: any) { | ||||
|       const errorMessage = this._getErrorMessage(err); | ||||
|       this._error = errorMessage; | ||||
| @@ -224,17 +215,6 @@ export class HcMain extends HassElement { | ||||
|       this.hass.connection.close(); | ||||
|     } | ||||
|     this.initializeHass(auth, connection); | ||||
|     if (this._hassUUID !== msg.hassUUID) { | ||||
|       this._hassUUID = msg.hassUUID; | ||||
|       this._lovelaceConfig = undefined; | ||||
|       this._urlPath = undefined; | ||||
|       this._lovelacePath = null; | ||||
|       if (this._unsubLovelace) { | ||||
|         this._unsubLovelace(); | ||||
|         this._unsubLovelace = undefined; | ||||
|       } | ||||
|       resourcesLoaded = false; | ||||
|     } | ||||
|     this._error = undefined; | ||||
|     this._sendStatus(); | ||||
|   } | ||||
| @@ -243,7 +223,7 @@ export class HcMain extends HassElement { | ||||
|     this._showDemo = false; | ||||
|     // We should not get this command before we are connected. | ||||
|     // Means a client got out of sync. Let's send status to them. | ||||
|     if (!this.hass?.connected) { | ||||
|     if (!this.hass) { | ||||
|       this._sendStatus(msg.senderId!); | ||||
|       this._error = "Cannot show Lovelace because we're not connected."; | ||||
|       this._sendError( | ||||
| @@ -270,7 +250,7 @@ export class HcMain extends HassElement { | ||||
|     } | ||||
|  | ||||
|     this._error = undefined; | ||||
|     if (msg.urlPath === "lovelace" || msg.urlPath === undefined) { | ||||
|     if (msg.urlPath === "lovelace") { | ||||
|       msg.urlPath = null; | ||||
|     } | ||||
|     this._lovelacePath = msg.viewPath; | ||||
| @@ -280,12 +260,13 @@ export class HcMain extends HassElement { | ||||
|           { | ||||
|             strategy: { | ||||
|               type: "energy", | ||||
|               show_date_selection: true, | ||||
|             }, | ||||
|           }, | ||||
|         ], | ||||
|       }; | ||||
|       this._urlPath = "energy"; | ||||
|       this._lovelacePath = null; | ||||
|       this._lovelacePath = 0; | ||||
|       this._sendStatus(); | ||||
|       return; | ||||
|     } | ||||
| @@ -294,7 +275,6 @@ export class HcMain extends HassElement { | ||||
|       this._lovelaceConfig = undefined; | ||||
|       if (this._unsubLovelace) { | ||||
|         this._unsubLovelace(); | ||||
|         this._unsubLovelace = undefined; | ||||
|       } | ||||
|       const llColl = atLeastVersion(this.hass.connection.haVersion, 0, 107) | ||||
|         ? getLovelaceCollection(this.hass.connection, msg.urlPath) | ||||
| @@ -303,20 +283,9 @@ export class HcMain extends HassElement { | ||||
|       // configuration. | ||||
|       try { | ||||
|         await llColl.refresh(); | ||||
|         this._unsubLovelace = llColl.subscribe(async (rawConfig) => { | ||||
|           if (isStrategyDashboard(rawConfig)) { | ||||
|             const { generateLovelaceDashboardStrategy } = await import( | ||||
|               "../../../../src/panels/lovelace/strategies/get-strategy" | ||||
|             ); | ||||
|             const config = await generateLovelaceDashboardStrategy( | ||||
|               rawConfig, | ||||
|               this.hass! | ||||
|             ); | ||||
|             this._handleNewLovelaceConfig(config); | ||||
|           } else { | ||||
|             this._handleNewLovelaceConfig(rawConfig); | ||||
|           } | ||||
|         }); | ||||
|         this._unsubLovelace = llColl.subscribe((lovelaceConfig) => | ||||
|           this._handleNewLovelaceConfig(lovelaceConfig) | ||||
|         ); | ||||
|       } catch (err: any) { | ||||
|         if ( | ||||
|           atLeastVersion(this.hass.connection.haVersion, 0, 107) && | ||||
| @@ -330,7 +299,7 @@ export class HcMain extends HassElement { | ||||
|         } | ||||
|         // Generate a Lovelace config. | ||||
|         this._unsubLovelace = () => undefined; | ||||
|         await this._generateDefaultLovelaceConfig(); | ||||
|         await this._generateLovelaceConfig(); | ||||
|       } | ||||
|     } | ||||
|     if (!resourcesLoaded) { | ||||
| @@ -339,31 +308,31 @@ export class HcMain extends HassElement { | ||||
|         ? await fetchResources(this.hass!.connection) | ||||
|         : (this._lovelaceConfig as LegacyLovelaceConfig).resources; | ||||
|       if (resources) { | ||||
|         loadLovelaceResources(resources, this.hass!); | ||||
|         loadLovelaceResources(resources, this.hass!.auth.data.hassUrl); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     this._sendStatus(); | ||||
|   } | ||||
|  | ||||
|   private async _generateDefaultLovelaceConfig() { | ||||
|   private async _generateLovelaceConfig() { | ||||
|     const { generateLovelaceDashboardStrategy } = await import( | ||||
|       "../../../../src/panels/lovelace/strategies/get-strategy" | ||||
|     ); | ||||
|     this._handleNewLovelaceConfig( | ||||
|       await generateLovelaceDashboardStrategy(DEFAULT_CONFIG, this.hass!) | ||||
|       await generateLovelaceDashboardStrategy( | ||||
|         { | ||||
|           type: DEFAULT_STRATEGY, | ||||
|         }, | ||||
|         this.hass!, | ||||
|         { narrow: false } | ||||
|       ) | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   private _handleNewLovelaceConfig(lovelaceConfig: LovelaceConfig) { | ||||
|     const title = getPanelTitleFromUrlPath( | ||||
|       this.hass!, | ||||
|       this._urlPath || "lovelace" | ||||
|     ); | ||||
|     castContext.setApplicationState(title || ""); | ||||
|     this._lovelaceConfig = checkLovelaceConfig( | ||||
|       lovelaceConfig | ||||
|     ) as LovelaceConfig; | ||||
|     castContext.setApplicationState(lovelaceConfig.title || ""); | ||||
|     this._lovelaceConfig = lovelaceConfig; | ||||
|   } | ||||
|  | ||||
|   private _handleShowDemo(_msg: ShowDemoMessage) { | ||||
| @@ -391,7 +360,7 @@ export class HcMain extends HassElement { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private _sendMessage(senderId: string, response: any) { | ||||
|   private sendMessage(senderId: string, response: any) { | ||||
|     castContext.sendCustomMessage(CAST_NS, senderId, response); | ||||
|   } | ||||
| } | ||||
|   | ||||
							
								
								
									
										3
									
								
								cast/src/receiver/second-load.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								cast/src/receiver/second-load.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| import "../../../src/resources/ha-style"; | ||||
| import "../../../src/resources/roboto"; | ||||
| import "./layout/hc-lovelace"; | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user