mirror of
				https://github.com/home-assistant/frontend.git
				synced 2025-10-26 03:59:43 +00:00 
			
		
		
		
	Compare commits
	
		
			3 Commits
		
	
	
		
			button-hea
			...
			allow-to-s
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 531363ae5d | ||
|   | b19fe68686 | ||
|   | 05e08cdcc0 | 
| @@ -1,36 +1,39 @@ | |||||||
| [modern] | [modern] | ||||||
| # Modern builds target recent browsers supporting the latest features to minimize transpilation, polyfills, etc. | # Support for dynamic import is the main litmus test for serving modern builds. | ||||||
| # It is served to browsers meeting the following requirements: | # Although officially a ES2020 feature, browsers implemented it early, so this | ||||||
| # - released in the last year + current alpha/beta versions | # enables all of ES2017 and some features in ES2018. | ||||||
| # - Firefox extended support release (ESR) | supports es6-module-dynamic-import | ||||||
| # - with global utilization at or above 0.5% |  | ||||||
| # - exclude dead browsers (no security maintenance for 2+ years) | # Exclude Safari 11-12 because of a bug in tagged template literals | ||||||
| # - exclude KaiOS, QQ, and UC browsers due to lack of sufficient feature support data | # https://bugs.webkit.org/show_bug.cgi?id=190756 | ||||||
| unreleased versions | # Note: Dropping version 11 also enables several more ES2018 features | ||||||
| last 1 year | not Safari < 13 | ||||||
| Firefox ESR | not iOS < 13 | ||||||
| >= 0.5% |  | ||||||
| not dead | # 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 KaiOS > 0 | ||||||
| not QQAndroid > 0 | not QQAndroid > 0 | ||||||
| not UCAndroid > 0 | not UCAndroid > 0 | ||||||
|  |  | ||||||
|  | # Exclude unsupported browsers | ||||||
|  | not dead | ||||||
|  |  | ||||||
| [legacy] | [legacy] | ||||||
| # Legacy builds are served when modern requirements are not met and support browsers: | # Legacy builds are served when modern requirements are not met and support browsers: | ||||||
| # - released in the last 7 years + current alpha/beta versionss | # - released in the last 7 years + current alpha/beta versionss | ||||||
| # - with global utilization at or above 0.05% | # - with global utilization above 0.05% | ||||||
| # - exclude dead browsers (no security maintenance for 2+ years) | # The lattermost query ensures that support for popular old browsers is not dropped too early | ||||||
| # - exclude Opera Mini which does not support web sockets | # (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 | unreleased versions | ||||||
| last 7 years | last 7 years | ||||||
| >= 0.05% | > 0.05% and supports websockets | ||||||
| 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 |  | ||||||
|   | |||||||
| @@ -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 \ | ENV \ | ||||||
|   DEBIAN_FRONTEND=noninteractive \ |   DEBIAN_FRONTEND=noninteractive \ | ||||||
|   | |||||||
| @@ -5,15 +5,10 @@ | |||||||
|     "context": ".." |     "context": ".." | ||||||
|   }, |   }, | ||||||
|   "appPort": "8124:8123", |   "appPort": "8124:8123", | ||||||
|   "postCreateCommand": "./.devcontainer/post_create.sh", |  | ||||||
|   "postStartCommand": "script/bootstrap", |   "postStartCommand": "script/bootstrap", | ||||||
|   "containerEnv": { |   "containerEnv": { | ||||||
|     "DEV_CONTAINER": "1", |  | ||||||
|     "WORKSPACE_DIRECTORY": "${containerWorkspaceFolder}" |     "WORKSPACE_DIRECTORY": "${containerWorkspaceFolder}" | ||||||
|   }, |   }, | ||||||
|   "remoteEnv": { |  | ||||||
|     "NODE_OPTIONS": "--max_old_space_size=8192" |  | ||||||
|   }, |  | ||||||
|   "customizations": { |   "customizations": { | ||||||
|     "vscode": { |     "vscode": { | ||||||
|       "extensions": [ |       "extensions": [ | ||||||
| @@ -21,8 +16,7 @@ | |||||||
|         "esbenp.prettier-vscode", |         "esbenp.prettier-vscode", | ||||||
|         "runem.lit-plugin", |         "runem.lit-plugin", | ||||||
|         "github.vscode-pull-request-github", |         "github.vscode-pull-request-github", | ||||||
|         "eamodio.gitlens", |         "eamodio.gitlens" | ||||||
|         "yeion7.styled-global-variables-autocomplete" |  | ||||||
|       ], |       ], | ||||||
|       "settings": { |       "settings": { | ||||||
|         "files.eol": "\n", |         "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: | |       value: | | ||||||
|         Make sure you are running the [latest version of Home Assistant][releases] before reporting an issue. |         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 |         [releases]: https://github.com/home-assistant/home-assistant/releases | ||||||
|   - type: checkboxes |   - type: checkboxes | ||||||
|     attributes: |     attributes: | ||||||
| @@ -24,7 +24,6 @@ body: | |||||||
|           required: true |           required: true | ||||||
|         - label: I have tried a different browser to see if it is related to my browser. |         - label: I have tried a different browser to see if it is related to my browser. | ||||||
|           required: true |           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 |   - type: markdown | ||||||
|     attributes: |     attributes: | ||||||
|       value: | |       value: | | ||||||
| @@ -74,7 +73,7 @@ body: | |||||||
|         If known, otherwise leave blank. |         If known, otherwise leave blank. | ||||||
|   - type: input |   - type: input | ||||||
|     attributes: |     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 |       placeholder: Google Chrome 88.0.4324.150 | ||||||
|       description: > |       description: > | ||||||
|         Provide the full name and don't forget to add the version! |         Provide the full name and don't forget to add the version! | ||||||
| @@ -108,9 +107,9 @@ body: | |||||||
|       render: yaml |       render: yaml | ||||||
|   - type: textarea |   - type: textarea | ||||||
|     attributes: |     attributes: | ||||||
|       label: JavaScript errors shown in your browser console/inspector |       label: Javascript errors shown in your browser console/inspector | ||||||
|       description: > |       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. |         browser console/inspector please provide them. | ||||||
|       render: txt |       render: txt | ||||||
|   - type: textarea |   - type: textarea | ||||||
|   | |||||||
							
								
								
									
										4
									
								
								.github/ISSUE_TEMPLATE/config.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/ISSUE_TEMPLATE/config.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,8 +1,8 @@ | |||||||
| blank_issues_enabled: false | blank_issues_enabled: false | ||||||
| contact_links: | contact_links: | ||||||
|   - name: Request a feature for the UI / Dashboards |   - name: Request a feature for the UI / Dashboards | ||||||
|     url: https://github.com/orgs/home-assistant/discussions |     url: https://github.com/home-assistant/frontend/discussions/category_choices | ||||||
|     about: Request a new feature for the Home Assistant frontend. |     about: Request an new feature for the Home Assistant frontend. | ||||||
|   - name: Report a bug that is NOT related to the UI / Dashboards |   - name: Report a bug that is NOT related to the UI / Dashboards | ||||||
|     url: https://github.com/home-assistant/core/issues |     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. |     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 |  | ||||||
							
								
								
									
										9
									
								
								.github/PULL_REQUEST_TEMPLATE.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										9
									
								
								.github/PULL_REQUEST_TEMPLATE.md
									
									
									
									
										vendored
									
									
								
							| @@ -2,7 +2,9 @@ | |||||||
|   You are amazing! Thanks for contributing to our project! |   You are amazing! Thanks for contributing to our project! | ||||||
|   Please, DO NOT DELETE ANY TEXT from this template! (unless instructed). |   Please, DO NOT DELETE ANY TEXT from this template! (unless instructed). | ||||||
| --> | --> | ||||||
|  |  | ||||||
| ## Breaking change | ## Breaking change | ||||||
|  |  | ||||||
| <!-- | <!-- | ||||||
|   If your PR contains a breaking change for existing users, it is important |   If your PR contains a breaking change for existing users, it is important | ||||||
|   to tell them what breaks, how to make it work again and why we did this. |   to tell them what breaks, how to make it work again and why we did this. | ||||||
| @@ -11,8 +13,8 @@ | |||||||
|   Note: Remove this section if this PR is NOT a breaking change. |   Note: Remove this section if this PR is NOT a breaking change. | ||||||
| --> | --> | ||||||
|  |  | ||||||
|  |  | ||||||
| ## Proposed change | ## Proposed change | ||||||
|  |  | ||||||
| <!-- | <!-- | ||||||
|   Describe the big picture of your changes here to communicate to the |   Describe the big picture of your changes here to communicate to the | ||||||
|   maintainers why we should accept this pull request. If it fixes a bug |   maintainers why we should accept this pull request. If it fixes a bug | ||||||
| @@ -20,8 +22,8 @@ | |||||||
|   in the additional information section. |   in the additional information section. | ||||||
| --> | --> | ||||||
|  |  | ||||||
|  |  | ||||||
| ## Type of change | ## Type of change | ||||||
|  |  | ||||||
| <!-- | <!-- | ||||||
|   What type of change does your PR introduce to the Home Assistant frontend? |   What type of change does your PR introduce to the Home Assistant frontend? | ||||||
|   NOTE: Please, check only 1! box! |   NOTE: Please, check only 1! box! | ||||||
| @@ -36,6 +38,7 @@ | |||||||
| - [ ] Code quality improvements to existing code or addition of tests | - [ ] Code quality improvements to existing code or addition of tests | ||||||
|  |  | ||||||
| ## Example configuration | ## Example configuration | ||||||
|  |  | ||||||
| <!-- | <!-- | ||||||
|   Supplying a configuration snippet, makes it easier for a maintainer to test |   Supplying a configuration snippet, makes it easier for a maintainer to test | ||||||
|   your PR. |   your PR. | ||||||
| @@ -46,6 +49,7 @@ | |||||||
| ``` | ``` | ||||||
|  |  | ||||||
| ## Additional information | ## Additional information | ||||||
|  |  | ||||||
| <!-- | <!-- | ||||||
|   Details are important, and help maintainers processing your PR. |   Details are important, and help maintainers processing your PR. | ||||||
|   Please be sure to fill out additional details, if applicable. |   Please be sure to fill out additional details, if applicable. | ||||||
| @@ -56,6 +60,7 @@ | |||||||
| - Link to documentation pull request: | - Link to documentation pull request: | ||||||
|  |  | ||||||
| ## Checklist | ## Checklist | ||||||
|  |  | ||||||
| <!-- | <!-- | ||||||
|   Put an `x` in the boxes that apply. You can also fill these out after |   Put an `x` in the boxes that apply. You can also fill these out after | ||||||
|   creating the PR. If you're unsure about any of them, don't hesitate to ask. |   creating the PR. If you're unsure about any of them, don't hesitate to ask. | ||||||
|   | |||||||
							
								
								
									
										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 |  | ||||||
							
								
								
									
										20
									
								
								.github/labeler.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										20
									
								
								.github/labeler.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,51 +1,31 @@ | |||||||
| Build: | Build: | ||||||
|   - changed-files: |  | ||||||
|       - any-glob-to-any-file: |  | ||||||
|   - build-scripts/** |   - build-scripts/** | ||||||
|   - .browserslistrc |   - .browserslistrc | ||||||
|   - gulpfile.js |   - gulpfile.js | ||||||
|  |  | ||||||
| Cast: | Cast: | ||||||
|   - changed-files: |  | ||||||
|       - any-glob-to-any-file: |  | ||||||
|   - cast/src/** |   - cast/src/** | ||||||
|   - src/cast/** |   - src/cast/** | ||||||
|  |  | ||||||
| Demo: | Demo: | ||||||
|   - changed-files: |  | ||||||
|       - any-glob-to-any-file: |  | ||||||
|   - demo/src/** |   - demo/src/** | ||||||
|   - src/fake_data/** |   - src/fake_data/** | ||||||
|  |  | ||||||
| Design: | Design: | ||||||
|   - changed-files: |  | ||||||
|       - any-glob-to-any-file: |  | ||||||
|   - gallery/src/** |   - gallery/src/** | ||||||
|   - src/fake_data/** |   - src/fake_data/** | ||||||
|  |  | ||||||
| Dependencies: | 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 |   - package.json | ||||||
|   - renovate.json |   - renovate.json | ||||||
|   - yarn.lock |   - yarn.lock | ||||||
|   - .yarn/** |   - .yarn/** | ||||||
|   - .yarnrc.yml |   - .yarnrc.yml | ||||||
|   - .nvmrc |   - .nvmrc | ||||||
|       # Dependabot and Renovate branches always match (i.e. compatibility tweaks by members considered minor) |  | ||||||
|       - head-branch: |  | ||||||
|           - "^renovate/" |  | ||||||
|           - "^dependabot/" |  | ||||||
|  |  | ||||||
| GitHub Actions: | GitHub Actions: | ||||||
|   - changed-files: |  | ||||||
|       - any-glob-to-any-file: |  | ||||||
|   - .github/workflows/** |   - .github/workflows/** | ||||||
|   - .github/*.yml |   - .github/*.yml | ||||||
|  |  | ||||||
| Supervisor: | 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 }} |       url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} | ||||||
|     steps: |     steps: | ||||||
|       - name: Check out files from GitHub |       - name: Check out files from GitHub | ||||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 |         uses: actions/checkout@v3.5.3 | ||||||
|         with: |         with: | ||||||
|           ref: dev |           ref: dev | ||||||
|  |  | ||||||
|       - name: Setup Node |       - name: Setup Node | ||||||
|         uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 |         uses: actions/setup-node@v3.7.0 | ||||||
|         with: |         with: | ||||||
|           node-version-file: ".nvmrc" |           node-version-file: ".nvmrc" | ||||||
|           cache: yarn |           cache: yarn | ||||||
| @@ -41,8 +41,9 @@ jobs: | |||||||
|  |  | ||||||
|       - name: Deploy to Netlify |       - name: Deploy to Netlify | ||||||
|         id: deploy |         id: deploy | ||||||
|         run: | |         uses: netlify/actions/cli@master | ||||||
|           npx -y netlify-cli@23.7.3 deploy --dir=cast/dist --alias dev |         with: | ||||||
|  |           args: deploy --dir=cast/dist --alias dev | ||||||
|         env: |         env: | ||||||
|           NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} |           NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} | ||||||
|           NETLIFY_SITE_ID: ${{ secrets.NETLIFY_CAST_SITE_ID }} |           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 }} |       url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} | ||||||
|     steps: |     steps: | ||||||
|       - name: Check out files from GitHub |       - name: Check out files from GitHub | ||||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 |         uses: actions/checkout@v3.5.3 | ||||||
|         with: |         with: | ||||||
|           ref: master |           ref: master | ||||||
|  |  | ||||||
|       - name: Setup Node |       - name: Setup Node | ||||||
|         uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 |         uses: actions/setup-node@v3.7.0 | ||||||
|         with: |         with: | ||||||
|           node-version-file: ".nvmrc" |           node-version-file: ".nvmrc" | ||||||
|           cache: yarn |           cache: yarn | ||||||
| @@ -76,8 +77,9 @@ jobs: | |||||||
|  |  | ||||||
|       - name: Deploy to Netlify |       - name: Deploy to Netlify | ||||||
|         id: deploy |         id: deploy | ||||||
|         run: | |         uses: netlify/actions/cli@master | ||||||
|           npx -y netlify-cli@23.7.3 deploy --dir=cast/dist --prod |         with: | ||||||
|  |           args: deploy --dir=cast/dist --prod | ||||||
|         env: |         env: | ||||||
|           NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} |           NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} | ||||||
|           NETLIFY_SITE_ID: ${{ secrets.NETLIFY_CAST_SITE_ID }} |           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 |     runs-on: ubuntu-latest | ||||||
|     steps: |     steps: | ||||||
|       - name: Check out files from GitHub |       - name: Check out files from GitHub | ||||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 |         uses: actions/checkout@v3.5.3 | ||||||
|       - name: Setup Node |       - name: Setup Node | ||||||
|         uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 |         uses: actions/setup-node@v3.7.0 | ||||||
|         with: |         with: | ||||||
|           node-version-file: ".nvmrc" |           node-version-file: ".nvmrc" | ||||||
|           cache: yarn |           cache: yarn | ||||||
| @@ -37,20 +37,17 @@ jobs: | |||||||
|       - name: Build resources |       - name: Build resources | ||||||
|         run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages |         run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages | ||||||
|       - name: Setup lint cache |       - name: Setup lint cache | ||||||
|         uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 |         uses: actions/cache@v3.3.1 | ||||||
|         with: |         with: | ||||||
|           path: | |           path: | | ||||||
|             node_modules/.cache/prettier |             node_modules/.cache/prettier | ||||||
|             node_modules/.cache/eslint |             node_modules/.cache/eslint | ||||||
|             node_modules/.cache/typescript |  | ||||||
|           key: lint-${{ github.sha }} |           key: lint-${{ github.sha }} | ||||||
|           restore-keys: lint- |           restore-keys: lint- | ||||||
|       - name: Run eslint |       - name: Run eslint | ||||||
|         run: yarn run lint:eslint --quiet |         run: yarn run lint:eslint --quiet | ||||||
|       - name: Run tsc |       - name: Run tsc | ||||||
|         run: yarn run lint:types |         run: yarn run lint:types | ||||||
|       - name: Run lit-analyzer |  | ||||||
|         run: yarn run lint:lit --quiet |  | ||||||
|       - name: Run prettier |       - name: Run prettier | ||||||
|         run: yarn run lint:prettier |         run: yarn run lint:prettier | ||||||
|   test: |   test: | ||||||
| @@ -58,16 +55,16 @@ jobs: | |||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     steps: |     steps: | ||||||
|       - name: Check out files from GitHub |       - name: Check out files from GitHub | ||||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 |         uses: actions/checkout@v3.5.3 | ||||||
|       - name: Setup Node |       - name: Setup Node | ||||||
|         uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 |         uses: actions/setup-node@v3.7.0 | ||||||
|         with: |         with: | ||||||
|           node-version-file: ".nvmrc" |           node-version-file: ".nvmrc" | ||||||
|           cache: yarn |           cache: yarn | ||||||
|       - name: Install dependencies |       - name: Install dependencies | ||||||
|         run: yarn install --immutable |         run: yarn install --immutable | ||||||
|       - name: Build resources |       - 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 |       - name: Run Tests | ||||||
|         run: yarn run test |         run: yarn run test | ||||||
|   build: |   build: | ||||||
| @@ -76,9 +73,9 @@ jobs: | |||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     steps: |     steps: | ||||||
|       - name: Check out files from GitHub |       - name: Check out files from GitHub | ||||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 |         uses: actions/checkout@v3.5.3 | ||||||
|       - name: Setup Node |       - name: Setup Node | ||||||
|         uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 |         uses: actions/setup-node@v3.7.0 | ||||||
|         with: |         with: | ||||||
|           node-version-file: ".nvmrc" |           node-version-file: ".nvmrc" | ||||||
|           cache: yarn |           cache: yarn | ||||||
| @@ -88,21 +85,15 @@ jobs: | |||||||
|         run: ./node_modules/.bin/gulp build-app |         run: ./node_modules/.bin/gulp build-app | ||||||
|         env: |         env: | ||||||
|           IS_TEST: "true" |           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: |   supervisor: | ||||||
|     name: Build supervisor |     name: Build supervisor | ||||||
|     needs: [lint, test] |     needs: [lint, test] | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     steps: |     steps: | ||||||
|       - name: Check out files from GitHub |       - name: Check out files from GitHub | ||||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 |         uses: actions/checkout@v3.5.3 | ||||||
|       - name: Setup Node |       - name: Setup Node | ||||||
|         uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 |         uses: actions/setup-node@v3.7.0 | ||||||
|         with: |         with: | ||||||
|           node-version-file: ".nvmrc" |           node-version-file: ".nvmrc" | ||||||
|           cache: yarn |           cache: yarn | ||||||
| @@ -112,9 +103,3 @@ jobs: | |||||||
|         run: ./node_modules/.bin/gulp build-hassio |         run: ./node_modules/.bin/gulp build-hassio | ||||||
|         env: |         env: | ||||||
|           IS_TEST: "true" |           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: |     steps: | ||||||
|       - name: Checkout repository |       - name: Checkout repository | ||||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 |         uses: actions/checkout@v3.5.3 | ||||||
|         with: |         with: | ||||||
|           # We must fetch at least the immediate parents so that if this is |           # We must fetch at least the immediate parents so that if this is | ||||||
|           # a pull request then we can checkout the head. |           # a pull request then we can checkout the head. | ||||||
| @@ -36,14 +36,14 @@ jobs: | |||||||
|  |  | ||||||
|       # Initializes the CodeQL tools for scanning. |       # Initializes the CodeQL tools for scanning. | ||||||
|       - name: Initialize CodeQL |       - name: Initialize CodeQL | ||||||
|         uses: github/codeql-action/init@64d10c13136e1c5bce3e5fbde8d4906eeaafc885 # v3.30.6 |         uses: github/codeql-action/init@v2 | ||||||
|         with: |         with: | ||||||
|           languages: ${{ matrix.language }} |           languages: ${{ matrix.language }} | ||||||
|  |  | ||||||
|       # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java). |       # 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) |       # If this step fails, then you should remove it and run the build manually (see below) | ||||||
|       - name: Autobuild |       - name: Autobuild | ||||||
|         uses: github/codeql-action/autobuild@64d10c13136e1c5bce3e5fbde8d4906eeaafc885 # v3.30.6 |         uses: github/codeql-action/autobuild@v2 | ||||||
|  |  | ||||||
|       # ℹ️ Command-line programs to run using the OS shell. |       # ℹ️ Command-line programs to run using the OS shell. | ||||||
|       # 📚 https://git.io/JvXDl |       # 📚 https://git.io/JvXDl | ||||||
| @@ -57,4 +57,4 @@ jobs: | |||||||
|       #   make release |       #   make release | ||||||
|  |  | ||||||
|       - name: Perform CodeQL Analysis |       - name: Perform CodeQL Analysis | ||||||
|         uses: github/codeql-action/analyze@64d10c13136e1c5bce3e5fbde8d4906eeaafc885 # v3.30.6 |         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 }} |       url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} | ||||||
|     steps: |     steps: | ||||||
|       - name: Check out files from GitHub |       - name: Check out files from GitHub | ||||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 |         uses: actions/checkout@v3.5.3 | ||||||
|         with: |         with: | ||||||
|           ref: dev |           ref: dev | ||||||
|  |  | ||||||
|       - name: Setup Node |       - name: Setup Node | ||||||
|         uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 |         uses: actions/setup-node@v3.7.0 | ||||||
|         with: |         with: | ||||||
|           node-version-file: ".nvmrc" |           node-version-file: ".nvmrc" | ||||||
|           cache: yarn |           cache: yarn | ||||||
| @@ -42,8 +42,9 @@ jobs: | |||||||
|  |  | ||||||
|       - name: Deploy to Netlify |       - name: Deploy to Netlify | ||||||
|         id: deploy |         id: deploy | ||||||
|         run: | |         uses: netlify/actions/cli@master | ||||||
|           npx -y netlify-cli@23.7.3 deploy --dir=demo/dist --prod |         with: | ||||||
|  |           args: deploy --dir=demo/dist --prod | ||||||
|         env: |         env: | ||||||
|           NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} |           NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} | ||||||
|           NETLIFY_SITE_ID: ${{ secrets.NETLIFY_DEMO_DEV_SITE_ID }} |           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 }} |       url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} | ||||||
|     steps: |     steps: | ||||||
|       - name: Check out files from GitHub |       - name: Check out files from GitHub | ||||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 |         uses: actions/checkout@v3.5.3 | ||||||
|         with: |         with: | ||||||
|           ref: master |           ref: master | ||||||
|  |  | ||||||
|       - name: Setup Node |       - name: Setup Node | ||||||
|         uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 |         uses: actions/setup-node@v3.7.0 | ||||||
|         with: |         with: | ||||||
|           node-version-file: ".nvmrc" |           node-version-file: ".nvmrc" | ||||||
|           cache: yarn |           cache: yarn | ||||||
| @@ -77,8 +78,9 @@ jobs: | |||||||
|  |  | ||||||
|       - name: Deploy to Netlify |       - name: Deploy to Netlify | ||||||
|         id: deploy |         id: deploy | ||||||
|         run: | |         uses: netlify/actions/cli@master | ||||||
|           npx -y netlify-cli@23.7.3 deploy --dir=demo/dist --prod |         with: | ||||||
|  |           args: deploy --dir=demo/dist --prod | ||||||
|         env: |         env: | ||||||
|           NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} |           NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} | ||||||
|           NETLIFY_SITE_ID: ${{ secrets.NETLIFY_DEMO_SITE_ID }} |           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 }} |       url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} | ||||||
|     steps: |     steps: | ||||||
|       - name: Check out files from GitHub |       - name: Check out files from GitHub | ||||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 |         uses: actions/checkout@v3.5.3 | ||||||
|  |  | ||||||
|       - name: Setup Node |       - name: Setup Node | ||||||
|         uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 |         uses: actions/setup-node@v3.7.0 | ||||||
|         with: |         with: | ||||||
|           node-version-file: ".nvmrc" |           node-version-file: ".nvmrc" | ||||||
|           cache: yarn |           cache: yarn | ||||||
| @@ -34,8 +34,9 @@ jobs: | |||||||
|  |  | ||||||
|       - name: Deploy to Netlify |       - name: Deploy to Netlify | ||||||
|         id: deploy |         id: deploy | ||||||
|         run: | |         uses: netlify/actions/cli@master | ||||||
|           npx -y netlify-cli@23.7.3 deploy --dir=gallery/dist --prod |         with: | ||||||
|  |           args: deploy --dir=gallery/dist --prod | ||||||
|         env: |         env: | ||||||
|           NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} |           NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} | ||||||
|           NETLIFY_SITE_ID: ${{ secrets.NETLIFY_GALLERY_SITE_ID }} |           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') |     if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview') | ||||||
|     steps: |     steps: | ||||||
|       - name: Check out files from GitHub |       - name: Check out files from GitHub | ||||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 |         uses: actions/checkout@v3.5.3 | ||||||
|  |  | ||||||
|       - name: Setup Node |       - name: Setup Node | ||||||
|         uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 |         uses: actions/setup-node@v3.7.0 | ||||||
|         with: |         with: | ||||||
|           node-version-file: ".nvmrc" |           node-version-file: ".nvmrc" | ||||||
|           cache: yarn |           cache: yarn | ||||||
| @@ -39,14 +39,13 @@ jobs: | |||||||
|  |  | ||||||
|       - name: Deploy preview to Netlify |       - name: Deploy preview to Netlify | ||||||
|         id: deploy |         id: deploy | ||||||
|         run: | |         uses: netlify/actions/cli@master | ||||||
|           npx -y netlify-cli@23.7.3 deploy --dir=gallery/dist --alias "deploy-preview-${{ github.event.number }}" \ |         with: | ||||||
|             --json > deploy_output.json |           args: deploy --dir=gallery/dist --alias "deploy-preview-${{ github.event.number }}" | ||||||
|         env: |         env: | ||||||
|           NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} |           NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} | ||||||
|           NETLIFY_SITE_ID: ${{ secrets.NETLIFY_GALLERY_SITE_ID }} |           NETLIFY_SITE_ID: ${{ secrets.NETLIFY_GALLERY_SITE_ID }} | ||||||
|  |  | ||||||
|       - name: Generate summary |       - name: Generate summary | ||||||
|         run: | |         run: | | ||||||
|           NETLIFY_LIVE_URL=$(jq -r '.deploy_url' deploy_output.json) |           echo "${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}" >> "$GITHUB_STEP_SUMMARY" | ||||||
|           echo "$NETLIFY_LIVE_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 |     runs-on: ubuntu-latest | ||||||
|     steps: |     steps: | ||||||
|       - name: Apply labels |       - name: Apply labels | ||||||
|         uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1 |         uses: actions/labeler@v4.3.0 | ||||||
|         with: |         with: | ||||||
|           sync-labels: true |           sync-labels: true | ||||||
|   | |||||||
							
								
								
									
										3
									
								
								.github/workflows/lock.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.github/workflows/lock.yml
									
									
									
									
										vendored
									
									
								
							| @@ -9,10 +9,9 @@ jobs: | |||||||
|   lock: |   lock: | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     steps: |     steps: | ||||||
|       - uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5.0.1 |       - uses: dessant/lock-threads@v4.0.1 | ||||||
|         with: |         with: | ||||||
|           github-token: ${{ github.token }} |           github-token: ${{ github.token }} | ||||||
|           process-only: "issues, prs" |  | ||||||
|           issue-lock-inactive-days: "30" |           issue-lock-inactive-days: "30" | ||||||
|           issue-exclude-created-before: "2020-10-01T00:00:00Z" |           issue-exclude-created-before: "2020-10-01T00:00:00Z" | ||||||
|           issue-lock-reason: "" |           issue-lock-reason: "" | ||||||
|   | |||||||
							
								
								
									
										14
									
								
								.github/workflows/nightly.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										14
									
								
								.github/workflows/nightly.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -6,7 +6,7 @@ on: | |||||||
|     - cron: "0 1 * * *" |     - cron: "0 1 * * *" | ||||||
|  |  | ||||||
| env: | env: | ||||||
|   PYTHON_VERSION: "3.13" |   PYTHON_VERSION: "3.11" | ||||||
|   NODE_OPTIONS: --max_old_space_size=6144 |   NODE_OPTIONS: --max_old_space_size=6144 | ||||||
|  |  | ||||||
| permissions: | permissions: | ||||||
| @@ -20,15 +20,15 @@ jobs: | |||||||
|       contents: write |       contents: write | ||||||
|     steps: |     steps: | ||||||
|       - name: Checkout the repository |       - name: Checkout the repository | ||||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 |         uses: actions/checkout@v3.5.3 | ||||||
|  |  | ||||||
|       - name: Set up Python ${{ env.PYTHON_VERSION }} |       - name: Set up Python ${{ env.PYTHON_VERSION }} | ||||||
|         uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6 |         uses: actions/setup-python@v4 | ||||||
|         with: |         with: | ||||||
|           python-version: ${{ env.PYTHON_VERSION }} |           python-version: ${{ env.PYTHON_VERSION }} | ||||||
|  |  | ||||||
|       - name: Setup Node |       - name: Setup Node | ||||||
|         uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 |         uses: actions/setup-node@v3.7.0 | ||||||
|         with: |         with: | ||||||
|           node-version-file: ".nvmrc" |           node-version-file: ".nvmrc" | ||||||
|           cache: yarn |           cache: yarn | ||||||
| @@ -42,7 +42,7 @@ jobs: | |||||||
|           LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }} |           LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }} | ||||||
|  |  | ||||||
|       - name: Bump version |       - name: Bump version | ||||||
|         run: script/version_bump.js nightly |         run: script/version_bump.cjs nightly | ||||||
|  |  | ||||||
|       - name: Build nightly Python wheels |       - name: Build nightly Python wheels | ||||||
|         run: | |         run: | | ||||||
| @@ -57,14 +57,14 @@ jobs: | |||||||
|         run: tar -czvf translations.tar.gz translations |         run: tar -czvf translations.tar.gz translations | ||||||
|  |  | ||||||
|       - name: Upload build artifacts |       - name: Upload build artifacts | ||||||
|         uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 |         uses: actions/upload-artifact@v3 | ||||||
|         with: |         with: | ||||||
|           name: wheels |           name: wheels | ||||||
|           path: dist/home_assistant_frontend*.whl |           path: dist/home_assistant_frontend*.whl | ||||||
|           if-no-files-found: error |           if-no-files-found: error | ||||||
|  |  | ||||||
|       - name: Upload translations |       - name: Upload translations | ||||||
|         uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 |         uses: actions/upload-artifact@v3 | ||||||
|         with: |         with: | ||||||
|           name: translations |           name: translations | ||||||
|           path: translations.tar.gz |           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 |       pull-requests: read | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     steps: |     steps: | ||||||
|       - uses: release-drafter/release-drafter@b1476f6e6eb133afa41ed8589daba6dc69b4d3f5 # v6.1.0 |       - uses: release-drafter/release-drafter@v5 | ||||||
|         env: |         env: | ||||||
|           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} |           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||||||
|   | |||||||
							
								
								
									
										81
									
								
								.github/workflows/release.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										81
									
								
								.github/workflows/release.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -6,7 +6,7 @@ on: | |||||||
|       - published |       - published | ||||||
|  |  | ||||||
| env: | env: | ||||||
|   PYTHON_VERSION: "3.13" |   PYTHON_VERSION: "3.11" | ||||||
|   NODE_OPTIONS: --max_old_space_size=6144 |   NODE_OPTIONS: --max_old_space_size=6144 | ||||||
|  |  | ||||||
| # Set default workflow permissions | # Set default workflow permissions | ||||||
| @@ -23,18 +23,18 @@ jobs: | |||||||
|       contents: write # Required to upload release assets |       contents: write # Required to upload release assets | ||||||
|     steps: |     steps: | ||||||
|       - name: Checkout the repository |       - name: Checkout the repository | ||||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 |         uses: actions/checkout@v3.5.3 | ||||||
|  |  | ||||||
|       - name: Set up Python ${{ env.PYTHON_VERSION }} |  | ||||||
|         uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 |  | ||||||
|         with: |  | ||||||
|           python-version: ${{ env.PYTHON_VERSION }} |  | ||||||
|  |  | ||||||
|       - name: Verify version |       - name: Verify version | ||||||
|         uses: home-assistant/actions/helpers/verify-version@master |         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 |       - name: Setup Node | ||||||
|         uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 |         uses: actions/setup-node@v3.7.0 | ||||||
|         with: |         with: | ||||||
|           node-version-file: ".nvmrc" |           node-version-file: ".nvmrc" | ||||||
|           cache: yarn |           cache: yarn | ||||||
| @@ -55,7 +55,7 @@ jobs: | |||||||
|           script/release |           script/release | ||||||
|  |  | ||||||
|       - name: Upload release assets |       - name: Upload release assets | ||||||
|         uses: softprops/action-gh-release@62c96d0c4e8a889135c1f3a25910db8dbe0e85f7 # v2.3.4 |         uses: softprops/action-gh-release@v0.1.15 | ||||||
|         with: |         with: | ||||||
|           files: | |           files: | | ||||||
|             dist/*.whl |             dist/*.whl | ||||||
| @@ -73,70 +73,11 @@ jobs: | |||||||
|           version=$(echo "${{ github.ref }}" | awk -F"/" '{print $NF}' ) |           version=$(echo "${{ github.ref }}" | awk -F"/" '{print $NF}' ) | ||||||
|           echo "home-assistant-frontend==$version" > ./requirements.txt |           echo "home-assistant-frontend==$version" > ./requirements.txt | ||||||
|  |  | ||||||
|       # home-assistant/wheels doesn't support SHA pinning |  | ||||||
|       - name: Build wheels |       - name: Build wheels | ||||||
|         uses: home-assistant/wheels@2025.09.1 |         uses: home-assistant/wheels@2023.04.0 | ||||||
|         with: |         with: | ||||||
|           abi: cp313 |           abi: cp311 | ||||||
|           tag: musllinux_1_2 |           tag: musllinux_1_2 | ||||||
|           arch: amd64 |           arch: amd64 | ||||||
|           wheels-key: ${{ secrets.WHEELS_KEY }} |           wheels-key: ${{ secrets.WHEELS_KEY }} | ||||||
|           requirements: "requirements.txt" |           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@62c96d0c4e8a889135c1f3a25910db8dbe0e85f7 # v2.3.4 |  | ||||||
|         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@62c96d0c4e8a889135c1f3a25910db8dbe0e85f7 # v2.3.4 |  | ||||||
|         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 |     runs-on: ubuntu-latest | ||||||
|     steps: |     steps: | ||||||
|       - name: 90 days stale policy |       - name: 90 days stale policy | ||||||
|         uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0 |         uses: actions/stale@v8.0.0 | ||||||
|         with: |         with: | ||||||
|           repo-token: ${{ secrets.GITHUB_TOKEN }} |           repo-token: ${{ secrets.GITHUB_TOKEN }} | ||||||
|           days-before-stale: 90 |           days-before-stale: 90 | ||||||
|   | |||||||
							
								
								
									
										3
									
								
								.github/workflows/translations.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.github/workflows/translations.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -1,7 +1,6 @@ | |||||||
| name: Translations | name: Translations | ||||||
|  |  | ||||||
| on: | on: | ||||||
|   workflow_dispatch: |  | ||||||
|   push: |   push: | ||||||
|     branches: |     branches: | ||||||
|       - dev |       - dev | ||||||
| @@ -14,7 +13,7 @@ jobs: | |||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     steps: |     steps: | ||||||
|       - name: Checkout the repository |       - name: Checkout the repository | ||||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 |         uses: actions/checkout@v3.5.3 | ||||||
|  |  | ||||||
|       - name: Upload Translations |       - name: Upload Translations | ||||||
|         run: | |         run: | | ||||||
|   | |||||||
							
								
								
									
										10
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										10
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -47,13 +47,3 @@ src/cast/dev_const.ts | |||||||
|  |  | ||||||
| # Home Assistant config | # Home Assistant config | ||||||
| /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" | ||||||
|   | |||||||
| @@ -1,4 +1,3 @@ | |||||||
| CLA.md | CLA.md | ||||||
| CODE_OF_CONDUCT.md | CODE_OF_CONDUCT.md | ||||||
| LICENSE.md | LICENSE.md | ||||||
| PULL_REQUEST_TEMPLATE.md |  | ||||||
							
								
								
									
										4
									
								
								.vscode/extensions.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.vscode/extensions.json
									
									
									
									
										vendored
									
									
								
							| @@ -4,8 +4,6 @@ | |||||||
|     "esbenp.prettier-vscode", |     "esbenp.prettier-vscode", | ||||||
|     "runem.lit-plugin", |     "runem.lit-plugin", | ||||||
|     "github.vscode-pull-request-github", |     "github.vscode-pull-request-github", | ||||||
|     "eamodio.gitlens", |     "eamodio.gitlens" | ||||||
|     "vitest.explorer", |  | ||||||
|     "yeion7.styled-global-variables-autocomplete" |  | ||||||
|   ] |   ] | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										74
									
								
								.vscode/tasks.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										74
									
								
								.vscode/tasks.json
									
									
									
									
										vendored
									
									
								
							| @@ -1,42 +1,6 @@ | |||||||
| { | { | ||||||
|   "version": "2.0.0", |   "version": "2.0.0", | ||||||
|   "tasks": [ |   "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", |       "label": "Develop Frontend", | ||||||
|       "type": "gulp", |       "type": "gulp", | ||||||
| @@ -136,38 +100,6 @@ | |||||||
|         "instanceLimit": 1 |         "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", |       "label": "Develop Demo", | ||||||
|       "type": "gulp", |       "type": "gulp", | ||||||
| @@ -277,12 +209,6 @@ | |||||||
|       "id": "supervisorToken", |       "id": "supervisorToken", | ||||||
|       "type": "promptString", |       "type": "promptString", | ||||||
|       "description": "The token for the Remote API proxy add-on" |       "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.1.cjs
									
									
									
									
										vendored
									
									
										Executable file
									
								
							
							
						
						
									
										874
									
								
								.yarn/releases/yarn-3.6.1.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: "" | defaultSemverRangePrefix: "" | ||||||
|  |  | ||||||
| enableGlobalCache: false |  | ||||||
|  |  | ||||||
| nodeLinker: node-modules | 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.1.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. | 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. | 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. | 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. | 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 path = require("path"); | ||||||
| const env = require("./env.cjs"); | const env = require("./env.cjs"); | ||||||
| const paths = require("./paths.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 | // 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 | // Nightly builds use the commit SHA, otherwise assumes there is a tag that matches the version | ||||||
| @@ -11,42 +8,54 @@ module.exports.sourceMapURL = () => { | |||||||
|   const ref = env.version().endsWith("dev") |   const ref = env.version().endsWith("dev") | ||||||
|     ? process.env.GITHUB_SHA || "dev" |     ? process.env.GITHUB_SHA || "dev" | ||||||
|     : env.version(); |     : env.version(); | ||||||
|   return `https://raw.githubusercontent.com/home-assistant/frontend/${ref}/`; |   return `https://raw.githubusercontent.com/home-assistant/frontend/${ref}`; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| // Files from NPM Packages that should not be imported | // 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 | // 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/typography.js"), | ||||||
|     require.resolve("@vaadin/vaadin-material-styles/font-icons.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. |     // Icons in supervisor conflict with icons in HA so we don't load. | ||||||
|     isHassioBuild && |     isHassioBuild && | ||||||
|       require.resolve( |       require.resolve( | ||||||
|         path.resolve(paths.root_dir, "src/components/ha-icon.ts") |         path.resolve(paths.polymer_dir, "src/components/ha-icon.ts") | ||||||
|       ), |       ), | ||||||
|     isHassioBuild && |     isHassioBuild && | ||||||
|       require.resolve( |       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); |   ].filter(Boolean); | ||||||
|  |  | ||||||
| module.exports.definedVars = ({ isProdBuild, latestBuild, defineOverlay }) => ({ | module.exports.definedVars = ({ isProdBuild, latestBuild, defineOverlay }) => ({ | ||||||
|   __DEV__: !isProdBuild, |   __DEV__: !isProdBuild, | ||||||
|   __BUILD__: JSON.stringify(latestBuild ? "modern" : "legacy"), |   __BUILD__: JSON.stringify(latestBuild ? "latest" : "es5"), | ||||||
|   __VERSION__: JSON.stringify(env.version()), |   __VERSION__: JSON.stringify(env.version()), | ||||||
|   __DEMO__: false, |   __DEMO__: false, | ||||||
|   __SUPERVISOR__: false, |   __SUPERVISOR__: false, | ||||||
|   __BACKWARDS_COMPAT__: false, |   __BACKWARDS_COMPAT__: false, | ||||||
|   __STATIC_PATH__: "/static/", |   __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( |   "process.env.NODE_ENV": JSON.stringify( | ||||||
|     isProdBuild ? "production" : "development" |     isProdBuild ? "production" : "development" | ||||||
|   ), |   ), | ||||||
| @@ -73,25 +82,7 @@ module.exports.terserOptions = ({ latestBuild, isTestBuild }) => ({ | |||||||
|   sourceMap: !isTestBuild, |   sourceMap: !isTestBuild, | ||||||
| }); | }); | ||||||
|  |  | ||||||
| /** @type {import('@rspack/core').SwcLoaderOptions} */ | module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({ | ||||||
| module.exports.swcOptions = () => ({ |  | ||||||
|   jsc: { |  | ||||||
|     loose: true, |  | ||||||
|     externalHelpers: true, |  | ||||||
|     target: "ES2021", |  | ||||||
|     parser: { |  | ||||||
|       syntax: "typescript", |  | ||||||
|       decorators: true, |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| module.exports.babelOptions = ({ |  | ||||||
|   latestBuild, |  | ||||||
|   isProdBuild, |  | ||||||
|   isTestBuild, |  | ||||||
|   sw, |  | ||||||
| }) => ({ |  | ||||||
|   babelrc: false, |   babelrc: false, | ||||||
|   compact: false, |   compact: false, | ||||||
|   assumptions: { |   assumptions: { | ||||||
| @@ -99,21 +90,26 @@ module.exports.babelOptions = ({ | |||||||
|     setPublicClassFields: true, |     setPublicClassFields: true, | ||||||
|     setSpreadProperties: 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: [ |   presets: [ | ||||||
|     [ |     [ | ||||||
|       "@babel/preset-env", |       "@babel/preset-env", | ||||||
|       { |       { | ||||||
|         useBuiltIns: "usage", |         useBuiltIns: latestBuild ? false : "entry", | ||||||
|         corejs: dependencies["core-js"], |         corejs: latestBuild ? false : { version: "3.32", proposals: true }, | ||||||
|         bugfixes: true, |         bugfixes: true, | ||||||
|         shippedProposals: true, |  | ||||||
|       }, |       }, | ||||||
|     ], |     ], | ||||||
|  |     "@babel/preset-typescript", | ||||||
|   ], |   ], | ||||||
|   plugins: [ |   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"], |         modules: ["@mdi/js"], | ||||||
|         ignoreModuleNotFound: true, |         ignoreModuleNotFound: true, | ||||||
| @@ -124,72 +120,35 @@ module.exports.babelOptions = ({ | |||||||
|       "template-html-minifier", |       "template-html-minifier", | ||||||
|       { |       { | ||||||
|         modules: { |         modules: { | ||||||
|           ...Object.fromEntries( |           lit: [ | ||||||
|             ["lit", "lit-element", "lit-html"].map((m) => [ |  | ||||||
|               m, |  | ||||||
|               [ |  | ||||||
|             "html", |             "html", | ||||||
|             { name: "svg", encapsulation: "svg" }, |             { name: "svg", encapsulation: "svg" }, | ||||||
|             { name: "css", encapsulation: "style" }, |             { name: "css", encapsulation: "style" }, | ||||||
|           ], |           ], | ||||||
|             ]) |           "@polymer/polymer/lib/utils/html-tag": ["html"], | ||||||
|           ), |  | ||||||
|           "@polymer/polymer/lib/utils/html-tag.js": ["html"], |  | ||||||
|         }, |         }, | ||||||
|         strictCSS: true, |         strictCSS: true, | ||||||
|         htmlMinifier: module.exports.htmlMinifierOptions, |         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 |     // Import helpers and regenerator from runtime package | ||||||
|     [ |     [ | ||||||
|       "@babel/plugin-transform-runtime", |       "@babel/plugin-transform-runtime", | ||||||
|       { version: dependencies["@babel/runtime"] }, |       { version: require("../package.json").dependencies["@babel/runtime"] }, | ||||||
|     ], |     ], | ||||||
|     "@babel/plugin-transform-class-properties", |     // Support  some proposals still in TC39 process | ||||||
|     "@babel/plugin-transform-private-methods", |     ["@babel/plugin-proposal-decorators", { decoratorsBeforeExport: true }], | ||||||
|   ].filter(Boolean), |   ].filter(Boolean), | ||||||
|   exclude: [ |   exclude: [ | ||||||
|     // \\ for Windows, / for Mac OS and Linux |     // \\ for Windows, / for Mac OS and Linux | ||||||
|     /node_modules[\\/]core-js/, |     /node_modules[\\/]core-js/, | ||||||
|  |     /node_modules[\\/]webpack[\\/]buildin/, | ||||||
|   ], |   ], | ||||||
|   sourceMaps: !isTestBuild, |   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) => | const outputPath = (outputRoot, latestBuild) => | ||||||
|   path.resolve(outputRoot, latestBuild ? "frontend_latest" : "frontend_es5"); |   path.resolve(outputRoot, latestBuild ? "frontend_latest" : "frontend_es5"); | ||||||
| @@ -223,14 +182,9 @@ const publicPath = (latestBuild, root = "") => | |||||||
| module.exports.config = { | module.exports.config = { | ||||||
|   app({ isProdBuild, latestBuild, isStatsBuild, isTestBuild, isWDS }) { |   app({ isProdBuild, latestBuild, isStatsBuild, isTestBuild, isWDS }) { | ||||||
|     return { |     return { | ||||||
|       name: "frontend" + nameSuffix(latestBuild), |       name: "app" + nameSuffix(latestBuild), | ||||||
|       entry: { |       entry: { | ||||||
|         "service-worker": !latestBuild |         service_worker: "./src/entrypoints/service_worker.ts", | ||||||
|           ? { |  | ||||||
|               import: "./src/entrypoints/service-worker.ts", |  | ||||||
|               layer: "sw", |  | ||||||
|             } |  | ||||||
|           : "./src/entrypoints/service-worker.ts", |  | ||||||
|         app: "./src/entrypoints/app.ts", |         app: "./src/entrypoints/app.ts", | ||||||
|         authorize: "./src/entrypoints/authorize.ts", |         authorize: "./src/entrypoints/authorize.ts", | ||||||
|         onboarding: "./src/entrypoints/onboarding.ts", |         onboarding: "./src/entrypoints/onboarding.ts", | ||||||
| @@ -326,17 +280,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 path = require("path"); | ||||||
| const paths = require("./paths.cjs"); | const paths = require("./paths.cjs"); | ||||||
|  |  | ||||||
| const isTrue = (value) => value === "1" || value?.toLowerCase() === "true"; |  | ||||||
|  |  | ||||||
| module.exports = { | module.exports = { | ||||||
|  |   useRollup() { | ||||||
|  |     return process.env.ROLLUP === "1"; | ||||||
|  |   }, | ||||||
|  |   useWDS() { | ||||||
|  |     return process.env.WDS === "1"; | ||||||
|  |   }, | ||||||
|   isProdBuild() { |   isProdBuild() { | ||||||
|     return ( |     return ( | ||||||
|       process.env.NODE_ENV === "production" || module.exports.isStatsBuild() |       process.env.NODE_ENV === "production" || module.exports.isStatsBuild() | ||||||
|     ); |     ); | ||||||
|   }, |   }, | ||||||
|   isStatsBuild() { |   isStatsBuild() { | ||||||
|     return isTrue(process.env.STATS); |     return process.env.STATS === "1"; | ||||||
|   }, |   }, | ||||||
|   isTestBuild() { |   isTestBuild() { | ||||||
|     return isTrue(process.env.IS_TEST); |     return process.env.IS_TEST === "true"; | ||||||
|   }, |   }, | ||||||
|   isNetlify() { |   isNetlify() { | ||||||
|     return isTrue(process.env.NETLIFY); |     return process.env.NETLIFY === "true"; | ||||||
|   }, |   }, | ||||||
|   version() { |   version() { | ||||||
|     const version = fs |     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)?)"/); |       .match(/version\W+=\W"(\d{8}\.\d(?:\.dev)?)"/); | ||||||
|     if (!version) { |     if (!version) { | ||||||
|       throw Error("Version not found"); |       throw Error("Version not found"); | ||||||
|     } |     } | ||||||
|     return version[1]; |     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 "./gather-static.js"; | ||||||
| import "./gen-icons-json.js"; | import "./gen-icons-json.js"; | ||||||
| import "./locale-data.js"; | import "./locale-data.js"; | ||||||
|  | import "./rollup.js"; | ||||||
| import "./service-worker.js"; | import "./service-worker.js"; | ||||||
| import "./translations.js"; | import "./translations.js"; | ||||||
| import "./rspack.js"; | import "./wds.js"; | ||||||
|  | import "./webpack.js"; | ||||||
|  |  | ||||||
| gulp.task( | gulp.task( | ||||||
|   "develop-app", |   "develop-app", | ||||||
| @@ -25,7 +27,11 @@ gulp.task( | |||||||
|       "build-locale-data" |       "build-locale-data" | ||||||
|     ), |     ), | ||||||
|     "copy-static-app", |     "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", |     "clean", | ||||||
|     gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"), |     gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"), | ||||||
|     "copy-static-app", |     "copy-static-app", | ||||||
|     "rspack-prod-app", |     env.useRollup() ? "rollup-prod-app" : "webpack-prod-app", | ||||||
|     gulp.parallel("gen-pages-app-prod", "gen-service-worker-app-prod"), |  | ||||||
|     // Don't compress running tests |     // Don't compress running tests | ||||||
|     ...(env.isTestBuild() || env.isStatsBuild() ? [] : ["compress-app"]) |     ...(env.isTestBuild() ? [] : ["compress-app"]), | ||||||
|   ) |     gulp.parallel("gen-pages-app-prod", "gen-service-worker-app-prod") | ||||||
| ); |  | ||||||
|  |  | ||||||
| gulp.task( |  | ||||||
|   "analyze-app", |  | ||||||
|   gulp.series( |  | ||||||
|     async function setEnv() { |  | ||||||
|       process.env.STATS = "1"; |  | ||||||
|     }, |  | ||||||
|     "clean", |  | ||||||
|     "rspack-prod-app" |  | ||||||
|   ) |   ) | ||||||
| ); | ); | ||||||
|   | |||||||
| @@ -1,10 +1,12 @@ | |||||||
| import gulp from "gulp"; | import gulp from "gulp"; | ||||||
|  | import env from "../env.cjs"; | ||||||
| import "./clean.js"; | import "./clean.js"; | ||||||
| import "./entry-html.js"; | import "./entry-html.js"; | ||||||
| import "./gather-static.js"; | import "./gather-static.js"; | ||||||
|  | import "./rollup.js"; | ||||||
| import "./service-worker.js"; | import "./service-worker.js"; | ||||||
| import "./translations.js"; | import "./translations.js"; | ||||||
| import "./rspack.js"; | import "./webpack.js"; | ||||||
|  |  | ||||||
| gulp.task( | gulp.task( | ||||||
|   "develop-cast", |   "develop-cast", | ||||||
| @@ -17,7 +19,7 @@ gulp.task( | |||||||
|     gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"), |     gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"), | ||||||
|     "copy-static-cast", |     "copy-static-cast", | ||||||
|     "gen-pages-cast-dev", |     "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", |     "translations-enable-merge-backend", | ||||||
|     gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"), |     gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"), | ||||||
|     "copy-static-cast", |     "copy-static-cast", | ||||||
|     "rspack-prod-cast", |     env.useRollup() ? "rollup-prod-cast" : "webpack-prod-cast", | ||||||
|     "gen-pages-cast-prod" |     "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 | // Tasks to compress | ||||||
|  |  | ||||||
| import { constants } from "node:zlib"; |  | ||||||
| import gulp from "gulp"; | import gulp from "gulp"; | ||||||
| import brotli from "gulp-brotli"; |  | ||||||
| import zopfli from "gulp-zopfli-green"; | import zopfli from "gulp-zopfli-green"; | ||||||
| import paths from "../paths.cjs"; | 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 zopfliOptions = { threshold: 150 }; | ||||||
|  |  | ||||||
| const compressModern = (rootDir, modernDir, compress) => | const compressDist = (rootDir) => | ||||||
|   gulp |   gulp | ||||||
|     .src([`${modernDir}/**/${filesGlob}`, `${rootDir}/sw-modern.js`], { |     .src([`${rootDir}/**/*.{js,json,css,svg}`]) | ||||||
|       base: rootDir, |     .pipe(zopfli(zopfliOptions)) | ||||||
|       allowEmpty: true, |  | ||||||
|     }) |  | ||||||
|     .pipe(compress === "zopfli" ? zopfli(zopfliOptions) : brotli(brotliOptions)) |  | ||||||
|     .pipe(gulp.dest(rootDir)); |     .pipe(gulp.dest(rootDir)); | ||||||
|  |  | ||||||
| const compressOther = (rootDir, modernDir, compress) => | gulp.task("compress-app", () => compressDist(paths.app_output_root)); | ||||||
|   gulp | gulp.task("compress-hassio", () => compressDist(paths.hassio_output_root)); | ||||||
|     .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 |  | ||||||
|   ) |  | ||||||
| ); |  | ||||||
|   | |||||||
| @@ -1,11 +1,13 @@ | |||||||
| import gulp from "gulp"; | import gulp from "gulp"; | ||||||
|  | import env from "../env.cjs"; | ||||||
| import "./clean.js"; | import "./clean.js"; | ||||||
| import "./entry-html.js"; | import "./entry-html.js"; | ||||||
| import "./gather-static.js"; | import "./gather-static.js"; | ||||||
| import "./gen-icons-json.js"; | import "./gen-icons-json.js"; | ||||||
|  | import "./rollup.js"; | ||||||
| import "./service-worker.js"; | import "./service-worker.js"; | ||||||
| import "./translations.js"; | import "./translations.js"; | ||||||
| import "./rspack.js"; | import "./webpack.js"; | ||||||
|  |  | ||||||
| gulp.task( | gulp.task( | ||||||
|   "develop-demo", |   "develop-demo", | ||||||
| @@ -22,7 +24,7 @@ gulp.task( | |||||||
|       "build-locale-data" |       "build-locale-data" | ||||||
|     ), |     ), | ||||||
|     "copy-static-demo", |     "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", |     "translations-enable-merge-backend", | ||||||
|     gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"), |     gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"), | ||||||
|     "copy-static-demo", |     "copy-static-demo", | ||||||
|     "rspack-prod-demo", |     env.useRollup() ? "rollup-prod-demo" : "webpack-prod-demo", | ||||||
|     "gen-pages-demo-prod" |     "gen-pages-demo-prod" | ||||||
|   ) |   ) | ||||||
| ); | ); | ||||||
|  |  | ||||||
| gulp.task( |  | ||||||
|   "analyze-demo", |  | ||||||
|   gulp.series( |  | ||||||
|     async function setEnv() { |  | ||||||
|       process.env.STATS = "1"; |  | ||||||
|     }, |  | ||||||
|     "clean", |  | ||||||
|     "rspack-prod-demo" |  | ||||||
|   ) |  | ||||||
| ); |  | ||||||
|   | |||||||
| @@ -1,19 +1,15 @@ | |||||||
| import fs from "fs/promises"; | import fs from "fs/promises"; | ||||||
| import gulp from "gulp"; | import gulp from "gulp"; | ||||||
| import path from "path"; |  | ||||||
| import mapStream from "map-stream"; | import mapStream from "map-stream"; | ||||||
| import transform from "gulp-json-transform"; | import transform from "gulp-json-transform"; | ||||||
| import { LokaliseApi } from "@lokalise/node-api"; |  | ||||||
| import JSZip from "jszip"; |  | ||||||
|  |  | ||||||
| const inDir = "translations"; | const inDirFrontend = "translations/frontend"; | ||||||
| const inDirFrontend = `${inDir}/frontend`; | const inDirBackend = "translations/backend"; | ||||||
| const inDirBackend = `${inDir}/backend`; |  | ||||||
| const srcMeta = "src/translations/translationMetadata.json"; | const srcMeta = "src/translations/translationMetadata.json"; | ||||||
| const encoding = "utf8"; | const encoding = "utf8"; | ||||||
|  |  | ||||||
| function hasHtml(data) { | function hasHtml(data) { | ||||||
|   return /<\S*>/i.test(data); |   return /<[a-z][\s\S]*>/i.test(data); | ||||||
| } | } | ||||||
|  |  | ||||||
| function recursiveCheckHasHtml(file, data, errors, recKey) { | function recursiveCheckHasHtml(file, data, errors, recKey) { | ||||||
| @@ -72,9 +68,8 @@ gulp.task("convert-backend-translations", function () { | |||||||
| }); | }); | ||||||
|  |  | ||||||
| gulp.task("check-translations-html", function () { | gulp.task("check-translations-html", function () { | ||||||
|   return gulp |   // We exclude backend translations because they are not compliant with the HTML rule for now | ||||||
|     .src([`${inDirFrontend}/*.json`, `${inDirBackend}/*.json`]) |   return gulp.src([`${inDirFrontend}/*.json`]).pipe(checkHtml()); | ||||||
|     .pipe(checkHtml()); |  | ||||||
| }); | }); | ||||||
|  |  | ||||||
| gulp.task("check-all-files-exist", async function () { | gulp.task("check-all-files-exist", async function () { | ||||||
| @@ -94,88 +89,7 @@ gulp.task("check-all-files-exist", async function () { | |||||||
|   await Promise.allSettled(writings); |   await Promise.allSettled(writings); | ||||||
| }); | }); | ||||||
|  |  | ||||||
| const lokaliseProjects = { |  | ||||||
|   backend: "130246255a974bd3b5e8a1.51616605", |  | ||||||
|   frontend: "3420425759f6d6d241f598.13594006", |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| gulp.task("fetch-lokalise", async function () { |  | ||||||
|   let apiKey; |  | ||||||
|   try { |  | ||||||
|     apiKey = |  | ||||||
|       process.env.LOKALISE_TOKEN || |  | ||||||
|       (await fs.readFile(".lokalise_token", { encoding })); |  | ||||||
|   } catch { |  | ||||||
|     throw new Error( |  | ||||||
|       "An Administrator Lokalise API token is required to download the latest set of translations. Place your token in a new file `.lokalise_token` in the repo root directory." |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|   const lokaliseApi = new LokaliseApi({ apiKey }); |  | ||||||
|  |  | ||||||
|   const mkdirPromise = Promise.all([ |  | ||||||
|     fs.mkdir(inDirFrontend, { recursive: true }), |  | ||||||
|     fs.mkdir(inDirBackend, { recursive: true }), |  | ||||||
|   ]); |  | ||||||
|  |  | ||||||
|   await Promise.all( |  | ||||||
|     Object.entries(lokaliseProjects).map(([project, projectId]) => |  | ||||||
|       lokaliseApi |  | ||||||
|         .files() |  | ||||||
|         .download(projectId, { |  | ||||||
|           format: "json", |  | ||||||
|           original_filenames: false, |  | ||||||
|           replace_breaks: false, |  | ||||||
|           json_unescaped_slashes: true, |  | ||||||
|           export_empty_as: "skip", |  | ||||||
|           filter_data: ["verified"], |  | ||||||
|         }) |  | ||||||
|         .then((download) => fetch(download.bundle_url)) |  | ||||||
|         .then((response) => { |  | ||||||
|           if (response.status === 200 || response.status === 0) { |  | ||||||
|             return response.arrayBuffer(); |  | ||||||
|           } |  | ||||||
|           throw new Error(response.statusText); |  | ||||||
|         }) |  | ||||||
|         .then(JSZip.loadAsync) |  | ||||||
|         .then(async (contents) => { |  | ||||||
|           await mkdirPromise; |  | ||||||
|           return Promise.all( |  | ||||||
|             Object.keys(contents.files).map(async (filename) => { |  | ||||||
|               const file = contents.file(filename); |  | ||||||
|               if (!file) { |  | ||||||
|                 // no file, probably a directory |  | ||||||
|                 return Promise.resolve(); |  | ||||||
|               } |  | ||||||
|               return file |  | ||||||
|                 .async("nodebuffer") |  | ||||||
|                 .then((content) => |  | ||||||
|                   fs.writeFile( |  | ||||||
|                     path.join( |  | ||||||
|                       inDir, |  | ||||||
|                       project, |  | ||||||
|                       filename.split("/").splice(-1)[0] |  | ||||||
|                     ), |  | ||||||
|                     content, |  | ||||||
|                     { flag: "w", encoding } |  | ||||||
|                   ) |  | ||||||
|                 ); |  | ||||||
|             }) |  | ||||||
|           ); |  | ||||||
|         }) |  | ||||||
|         .catch((err) => { |  | ||||||
|           console.error(err); |  | ||||||
|           throw err; |  | ||||||
|         }) |  | ||||||
|     ) |  | ||||||
|   ); |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| gulp.task( | gulp.task( | ||||||
|   "download-translations", |   "check-downloaded-translations", | ||||||
|   gulp.series( |   gulp.series("check-translations-html", "check-all-files-exist") | ||||||
|     "fetch-lokalise", |  | ||||||
|     "convert-backend-translations", |  | ||||||
|     "check-translations-html", |  | ||||||
|     "check-all-files-exist" |  | ||||||
|   ) |  | ||||||
| ); | ); | ||||||
|   | |||||||
| @@ -1,74 +1,28 @@ | |||||||
| // Tasks to generate entry HTML | // Tasks to generate entry HTML | ||||||
|  |  | ||||||
| import { |  | ||||||
|   applyVersionsToRegexes, |  | ||||||
|   compileRegex, |  | ||||||
|   getPreUserAgentRegexes, |  | ||||||
| } from "browserslist-useragent-regexp"; |  | ||||||
| import fs from "fs-extra"; | import fs from "fs-extra"; | ||||||
| import gulp from "gulp"; | import gulp from "gulp"; | ||||||
| import { minify } from "html-minifier-terser"; | import { minify } from "html-minifier-terser"; | ||||||
| import template from "lodash.template"; | import template from "lodash.template"; | ||||||
| import { dirname, extname, resolve } from "node:path"; | import path from "path"; | ||||||
| import { htmlMinifierOptions, terserOptions } from "../bundle.cjs"; | import { htmlMinifierOptions, terserOptions } from "../bundle.cjs"; | ||||||
|  | import env from "../env.cjs"; | ||||||
| import paths from "../paths.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 renderTemplate = (templateFile, data = {}) => { | ||||||
|   const compiled = template( |   const compiled = template( | ||||||
|     fs.readFileSync(templateFile, { encoding: "utf-8" }) |     fs.readFileSync(templateFile, { encoding: "utf-8" }) | ||||||
|   ); |   ); | ||||||
|   return compiled({ |   return compiled({ | ||||||
|     ...data, |     ...data, | ||||||
|  |     useRollup: env.useRollup(), | ||||||
|  |     useWDS: env.useWDS(), | ||||||
|     // Resolve any child/nested templates relative to the parent and pass the same data |     // Resolve any child/nested templates relative to the parent and pass the same data | ||||||
|     renderTemplate: (childTemplate) => |     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 | // 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 = | const genPagesDevTask = | ||||||
|   ( |   ( | ||||||
|     pageEntries, |     pageEntries, | ||||||
|     inputRoot, |     inputRoot, | ||||||
|     outputRoot, |     outputRoot, | ||||||
|  |     useWDS = false, | ||||||
|     inputSub = "src/html", |     inputSub = "src/html", | ||||||
|     publicRoot = "" |     publicRoot = "" | ||||||
|   ) => |   ) => | ||||||
|   async () => { |   async () => { | ||||||
|     const commonVars = getCommonTemplateVars(); |  | ||||||
|     for (const [page, entries] of Object.entries(pageEntries)) { |     for (const [page, entries] of Object.entries(pageEntries)) { | ||||||
|       const content = renderTemplate( |       const content = renderTemplate( | ||||||
|         resolve(inputRoot, inputSub, `${page}.template`), |         path.resolve(inputRoot, inputSub, `${page}.template`), | ||||||
|         { |         { | ||||||
|           ...commonVars, |           latestEntryJS: entries.map((entry) => | ||||||
|           latestEntryJS: entries.map( |             useWDS | ||||||
|             (entry) => `${publicRoot}/frontend_latest/${entry}.js` |               ? `http://localhost:8000/src/entrypoints/${entry}.ts` | ||||||
|  |               : `${publicRoot}/frontend_latest/${entry}.js` | ||||||
|           ), |           ), | ||||||
|           es5EntryJS: entries.map( |           es5EntryJS: entries.map( | ||||||
|             (entry) => `${publicRoot}/frontend_es5/${entry}.js` |             (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`, |           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 () => { |   async () => { | ||||||
|     const latestManifest = fs.readJsonSync( |     const latestManifest = fs.readJsonSync( | ||||||
|       resolve(outputLatest, "manifest.json") |       path.resolve(outputLatest, "manifest.json") | ||||||
|     ); |     ); | ||||||
|     const es5Manifest = outputES5 |     const es5Manifest = outputES5 | ||||||
|       ? fs.readJsonSync(resolve(outputES5, "manifest.json")) |       ? fs.readJsonSync(path.resolve(outputES5, "manifest.json")) | ||||||
|       : {}; |       : {}; | ||||||
|     const commonVars = getCommonTemplateVars(); |  | ||||||
|     const minifiedHTML = []; |     const minifiedHTML = []; | ||||||
|     for (const [page, entries] of Object.entries(pageEntries)) { |     for (const [page, entries] of Object.entries(pageEntries)) { | ||||||
|       const content = renderTemplate( |       const content = renderTemplate( | ||||||
|         resolve(inputRoot, inputSub, `${page}.template`), |         path.resolve(inputRoot, inputSub, `${page}.template`), | ||||||
|         { |         { | ||||||
|           ...commonVars, |  | ||||||
|           latestEntryJS: entries.map((entry) => latestManifest[`${entry}.js`]), |           latestEntryJS: entries.map((entry) => latestManifest[`${entry}.js`]), | ||||||
|           es5EntryJS: entries.map((entry) => es5Manifest[`${entry}.js`]), |           es5EntryJS: entries.map((entry) => es5Manifest[`${entry}.js`]), | ||||||
|           latestCustomPanelJS: latestManifest["custom-panel.js"], |           latestCustomPanelJS: latestManifest["custom-panel.js"], | ||||||
| @@ -152,8 +108,8 @@ const genPagesProdTask = | |||||||
|         } |         } | ||||||
|       ); |       ); | ||||||
|       minifiedHTML.push( |       minifiedHTML.push( | ||||||
|         minifyHtml(content, extname(page)).then((minified) => |         minifyHtml(content, path.extname(page)).then((minified) => | ||||||
|           fs.outputFileSync(resolve(outputRoot, page), minified) |           fs.outputFileSync(path.resolve(outputRoot, page), minified) | ||||||
|         ) |         ) | ||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
| @@ -169,14 +125,19 @@ const APP_PAGE_ENTRIES = { | |||||||
|  |  | ||||||
| gulp.task( | gulp.task( | ||||||
|   "gen-pages-app-dev", |   "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( | gulp.task( | ||||||
|   "gen-pages-app-prod", |   "gen-pages-app-prod", | ||||||
|   genPagesProdTask( |   genPagesProdTask( | ||||||
|     APP_PAGE_ENTRIES, |     APP_PAGE_ENTRIES, | ||||||
|     paths.root_dir, |     paths.polymer_dir, | ||||||
|     paths.app_output_root, |     paths.app_output_root, | ||||||
|     paths.app_output_latest, |     paths.app_output_latest, | ||||||
|     paths.app_output_es5 |     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"] }; | const HASSIO_PAGE_ENTRIES = { "entrypoint.js": ["entrypoint"] }; | ||||||
|  |  | ||||||
| gulp.task( | gulp.task( | ||||||
| @@ -275,6 +214,7 @@ gulp.task( | |||||||
|     HASSIO_PAGE_ENTRIES, |     HASSIO_PAGE_ENTRIES, | ||||||
|     paths.hassio_dir, |     paths.hassio_dir, | ||||||
|     paths.hassio_output_root, |     paths.hassio_output_root, | ||||||
|  |     undefined, | ||||||
|     "src", |     "src", | ||||||
|     paths.hassio_publicPath |     paths.hassio_publicPath | ||||||
|   ) |   ) | ||||||
|   | |||||||
| @@ -9,7 +9,7 @@ import gulp from "gulp"; | |||||||
| import jszip from "jszip"; | import jszip from "jszip"; | ||||||
| import path from "path"; | import path from "path"; | ||||||
| import process from "process"; | import process from "process"; | ||||||
| import { extract } from "tar"; | import tar from "tar"; | ||||||
|  |  | ||||||
| const MAX_AGE = 24; // hours | const MAX_AGE = 24; // hours | ||||||
| const OWNER = "home-assistant"; | const OWNER = "home-assistant"; | ||||||
| @@ -66,7 +66,7 @@ gulp.task("fetch-nightly-translations", async function () { | |||||||
|       tokenAuth = JSON.parse(await readFile(TOKEN_FILE, "utf-8")); |       tokenAuth = JSON.parse(await readFile(TOKEN_FILE, "utf-8")); | ||||||
|     } catch { |     } catch { | ||||||
|       if (!allowTokenSetup) { |       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; |         return; | ||||||
|       } |       } | ||||||
|       const auth = createOAuthDeviceAuth({ |       const auth = createOAuthDeviceAuth({ | ||||||
| @@ -156,7 +156,7 @@ gulp.task("fetch-nightly-translations", async function () { | |||||||
|   console.log("Unpacking downloaded translations..."); |   console.log("Unpacking downloaded translations..."); | ||||||
|   const zip = await jszip.loadAsync(downloadResponse.data); |   const zip = await jszip.loadAsync(downloadResponse.data); | ||||||
|   await deleteCurrent; |   await deleteCurrent; | ||||||
|   const extractStream = zip.file(/.*/)[0].nodeStream().pipe(extract()); |   const extractStream = zip.file(/.*/)[0].nodeStream().pipe(tar.extract()); | ||||||
|   await new Promise((resolve, reject) => { |   await new Promise((resolve, reject) => { | ||||||
|     extractStream.on("close", resolve).on("error", reject); |     extractStream.on("close", resolve).on("error", reject); | ||||||
|   }); |   }); | ||||||
|   | |||||||
| @@ -4,14 +4,16 @@ import gulp from "gulp"; | |||||||
| import yaml from "js-yaml"; | import yaml from "js-yaml"; | ||||||
| import { marked } from "marked"; | import { marked } from "marked"; | ||||||
| import path from "path"; | import path from "path"; | ||||||
|  | import env from "../env.cjs"; | ||||||
| import paths from "../paths.cjs"; | import paths from "../paths.cjs"; | ||||||
| import "./clean.js"; | import "./clean.js"; | ||||||
| import "./entry-html.js"; | import "./entry-html.js"; | ||||||
| import "./gather-static.js"; | import "./gather-static.js"; | ||||||
| import "./gen-icons-json.js"; | import "./gen-icons-json.js"; | ||||||
|  | import "./rollup.js"; | ||||||
| import "./service-worker.js"; | import "./service-worker.js"; | ||||||
| import "./translations.js"; | import "./translations.js"; | ||||||
| import "./rspack.js"; | import "./webpack.js"; | ||||||
|  |  | ||||||
| gulp.task("gather-gallery-pages", async function gatherPages() { | gulp.task("gather-gallery-pages", async function gatherPages() { | ||||||
|   const pageDir = path.resolve(paths.gallery_dir, "src/pages"); |   const pageDir = path.resolve(paths.gallery_dir, "src/pages"); | ||||||
| @@ -156,7 +158,9 @@ gulp.task( | |||||||
|     "copy-static-gallery", |     "copy-static-gallery", | ||||||
|     "gen-pages-gallery-dev", |     "gen-pages-gallery-dev", | ||||||
|     gulp.parallel( |     gulp.parallel( | ||||||
|       "rspack-dev-server-gallery", |       env.useRollup() | ||||||
|  |         ? "rollup-dev-server-gallery" | ||||||
|  |         : "webpack-dev-server-gallery", | ||||||
|       async function watchMarkdownFiles() { |       async function watchMarkdownFiles() { | ||||||
|         gulp.watch( |         gulp.watch( | ||||||
|           [ |           [ | ||||||
| @@ -185,7 +189,7 @@ gulp.task( | |||||||
|       "gather-gallery-pages" |       "gather-gallery-pages" | ||||||
|     ), |     ), | ||||||
|     "copy-static-gallery", |     "copy-static-gallery", | ||||||
|     "rspack-prod-gallery", |     env.useRollup() ? "rollup-prod-gallery" : "webpack-prod-gallery", | ||||||
|     "gen-pages-gallery-prod" |     "gen-pages-gallery-prod" | ||||||
|   ) |   ) | ||||||
| ); | ); | ||||||
|   | |||||||
| @@ -6,8 +6,8 @@ import path from "path"; | |||||||
| import paths from "../paths.cjs"; | import paths from "../paths.cjs"; | ||||||
|  |  | ||||||
| const npmPath = (...parts) => | const npmPath = (...parts) => | ||||||
|   path.resolve(paths.root_dir, "node_modules", ...parts); |   path.resolve(paths.polymer_dir, "node_modules", ...parts); | ||||||
| const polyPath = (...parts) => path.resolve(paths.root_dir, ...parts); | const polyPath = (...parts) => path.resolve(paths.polymer_dir, ...parts); | ||||||
|  |  | ||||||
| const copyFileDir = (fromFile, toDir) => | const copyFileDir = (fromFile, toDir) => | ||||||
|   fs.copySync(fromFile, path.join(toDir, path.basename(fromFile))); |   fs.copySync(fromFile, path.join(toDir, path.basename(fromFile))); | ||||||
| @@ -59,17 +59,12 @@ function copyPolyfills(staticDir) { | |||||||
|     npmPath("@webcomponents/webcomponentsjs/webcomponents-bundle.js.map"), |     npmPath("@webcomponents/webcomponentsjs/webcomponents-bundle.js.map"), | ||||||
|     staticPath("polyfills/") |     staticPath("polyfills/") | ||||||
|   ); |   ); | ||||||
|   // Lit polyfill support | } | ||||||
|   fs.copySync( |  | ||||||
|     npmPath("lit/polyfill-support.js"), |  | ||||||
|     path.join(staticPath("polyfills/"), "lit-polyfill-support.js") |  | ||||||
|   ); |  | ||||||
|  |  | ||||||
|   // dialog-polyfill css | function copyLoaderJS(staticDir) { | ||||||
|   copyFileDir( |   const staticPath = genStaticPath(staticDir); | ||||||
|     npmPath("dialog-polyfill/dialog-polyfill.css"), |   copyFileDir(npmPath("systemjs/dist/s.min.js"), staticPath("js")); | ||||||
|     staticPath("polyfills/") |   copyFileDir(npmPath("systemjs/dist/s.min.js.map"), staticPath("js")); | ||||||
|   ); |  | ||||||
| } | } | ||||||
|  |  | ||||||
| function copyFonts(staticDir) { | function copyFonts(staticDir) { | ||||||
| @@ -95,24 +90,12 @@ function copyMapPanel(staticDir) { | |||||||
|     npmPath("leaflet/dist/leaflet.css"), |     npmPath("leaflet/dist/leaflet.css"), | ||||||
|     staticPath("images/leaflet/") |     staticPath("images/leaflet/") | ||||||
|   ); |   ); | ||||||
|   copyFileDir( |  | ||||||
|     npmPath("leaflet.markercluster/dist/MarkerCluster.css"), |  | ||||||
|     staticPath("images/leaflet/") |  | ||||||
|   ); |  | ||||||
|   fs.copySync( |   fs.copySync( | ||||||
|     npmPath("leaflet/dist/images"), |     npmPath("leaflet/dist/images"), | ||||||
|     staticPath("images/leaflet/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 () => { | gulp.task("copy-locale-data", async () => { | ||||||
|   const staticDir = paths.app_output_static; |   const staticDir = paths.app_output_static; | ||||||
|   copyLocaleData(staticDir); |   copyLocaleData(staticDir); | ||||||
| @@ -128,11 +111,6 @@ gulp.task("copy-translations-supervisor", async () => { | |||||||
|   copyTranslations(staticDir); |   copyTranslations(staticDir); | ||||||
| }); | }); | ||||||
|  |  | ||||||
| gulp.task("copy-translations-landing-page", async () => { |  | ||||||
|   const staticDir = paths.landingPage_output_static; |  | ||||||
|   copyTranslations(staticDir); |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| gulp.task("copy-static-supervisor", async () => { | gulp.task("copy-static-supervisor", async () => { | ||||||
|   const staticDir = paths.hassio_output_static; |   const staticDir = paths.hassio_output_static; | ||||||
|   copyLocaleData(staticDir); |   copyLocaleData(staticDir); | ||||||
| @@ -143,6 +121,8 @@ gulp.task("copy-static-app", async () => { | |||||||
|   const staticDir = paths.app_output_static; |   const staticDir = paths.app_output_static; | ||||||
|   // Basic static files |   // Basic static files | ||||||
|   fs.copySync(polyPath("public"), paths.app_output_root); |   fs.copySync(polyPath("public"), paths.app_output_root); | ||||||
|  |  | ||||||
|  |   copyLoaderJS(staticDir); | ||||||
|   copyPolyfills(staticDir); |   copyPolyfills(staticDir); | ||||||
|   copyFonts(staticDir); |   copyFonts(staticDir); | ||||||
|   copyTranslations(staticDir); |   copyTranslations(staticDir); | ||||||
| @@ -153,7 +133,6 @@ gulp.task("copy-static-app", async () => { | |||||||
|   copyMapPanel(staticDir); |   copyMapPanel(staticDir); | ||||||
|  |  | ||||||
|   // Qr Scanner assets |   // Qr Scanner assets | ||||||
|   copyZXingWasm(staticDir); |  | ||||||
|   copyQrScannerWorker(staticDir); |   copyQrScannerWorker(staticDir); | ||||||
| }); | }); | ||||||
|  |  | ||||||
| @@ -165,6 +144,8 @@ gulp.task("copy-static-demo", async () => { | |||||||
|   ); |   ); | ||||||
|   // Copy demo static files |   // Copy demo static files | ||||||
|   fs.copySync(path.resolve(paths.demo_dir, "public"), paths.demo_output_root); |   fs.copySync(path.resolve(paths.demo_dir, "public"), paths.demo_output_root); | ||||||
|  |  | ||||||
|  |   copyLoaderJS(paths.demo_output_static); | ||||||
|   copyPolyfills(paths.demo_output_static); |   copyPolyfills(paths.demo_output_static); | ||||||
|   copyMapPanel(paths.demo_output_static); |   copyMapPanel(paths.demo_output_static); | ||||||
|   copyFonts(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); |   fs.copySync(polyPath("public/static"), paths.cast_output_static); | ||||||
|   // Copy cast static files |   // Copy cast static files | ||||||
|   fs.copySync(path.resolve(paths.cast_dir, "public"), paths.cast_output_root); |   fs.copySync(path.resolve(paths.cast_dir, "public"), paths.cast_output_root); | ||||||
|  |  | ||||||
|  |   copyLoaderJS(paths.cast_output_static); | ||||||
|   copyPolyfills(paths.cast_output_static); |   copyPolyfills(paths.cast_output_static); | ||||||
|   copyMapPanel(paths.cast_output_static); |   copyMapPanel(paths.cast_output_static); | ||||||
|   copyFonts(paths.cast_output_static); |   copyFonts(paths.cast_output_static); | ||||||
| @@ -201,14 +184,3 @@ gulp.task("copy-static-gallery", async () => { | |||||||
|   copyLocaleData(paths.gallery_output_static); |   copyLocaleData(paths.gallery_output_static); | ||||||
|   copyMdiIcons(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 "./entry-html.js"; | ||||||
| import "./gather-static.js"; | import "./gather-static.js"; | ||||||
| import "./gen-icons-json.js"; | import "./gen-icons-json.js"; | ||||||
|  | import "./rollup.js"; | ||||||
| import "./translations.js"; | import "./translations.js"; | ||||||
| import "./rspack.js"; | import "./webpack.js"; | ||||||
|  |  | ||||||
| gulp.task( | gulp.task( | ||||||
|   "develop-hassio", |   "develop-hassio", | ||||||
| @@ -21,7 +22,7 @@ gulp.task( | |||||||
|     "copy-translations-supervisor", |     "copy-translations-supervisor", | ||||||
|     "build-locale-data", |     "build-locale-data", | ||||||
|     "copy-static-supervisor", |     "copy-static-supervisor", | ||||||
|     "rspack-watch-hassio" |     env.useRollup() ? "rollup-watch-hassio" : "webpack-watch-hassio" | ||||||
|   ) |   ) | ||||||
| ); | ); | ||||||
|  |  | ||||||
| @@ -37,7 +38,7 @@ gulp.task( | |||||||
|     "copy-translations-supervisor", |     "copy-translations-supervisor", | ||||||
|     "build-locale-data", |     "build-locale-data", | ||||||
|     "copy-static-supervisor", |     "copy-static-supervisor", | ||||||
|     "rspack-prod-hassio", |     env.useRollup() ? "rollup-prod-hassio" : "webpack-prod-hassio", | ||||||
|     "gen-pages-hassio-prod", |     "gen-pages-hassio-prod", | ||||||
|     ...// Don't compress running tests |     ...// Don't compress running tests | ||||||
|     (env.isTestBuild() ? [] : ["compress-hassio"]) |     (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 { deleteSync } from "del"; | ||||||
| import { mkdir, readFile, writeFile } from "fs/promises"; | import { mkdir, readFile, writeFile } from "fs/promises"; | ||||||
| import gulp from "gulp"; | import gulp from "gulp"; | ||||||
| import { join, resolve } from "node:path"; | import path from "path"; | ||||||
| import paths from "../paths.cjs"; | import paths from "../paths.cjs"; | ||||||
|  |  | ||||||
| const formatjsDir = join(paths.root_dir, "node_modules", "@formatjs"); | const outDir = path.join(paths.build_dir, "locale-data"); | ||||||
| const outDir = join(paths.build_dir, "locale-data"); |  | ||||||
|  |  | ||||||
| const INTL_POLYFILLS = { | const INTL_PACKAGES = { | ||||||
|  |   "intl-relativetimeformat": "RelativeTimeFormat", | ||||||
|   "intl-datetimeformat": "DateTimeFormat", |   "intl-datetimeformat": "DateTimeFormat", | ||||||
|  |   "intl-numberformat": "NumberFormat", | ||||||
|   "intl-displaynames": "DisplayNames", |   "intl-displaynames": "DisplayNames", | ||||||
|   "intl-listformat": "ListFormat", |   "intl-listformat": "ListFormat", | ||||||
|   "intl-numberformat": "NumberFormat", |  | ||||||
|   "intl-relativetimeformat": "RelativeTimeFormat", |  | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const convertToJSON = async ( | const convertToJSON = async (pkg, lang) => { | ||||||
|   pkg, |  | ||||||
|   lang, |  | ||||||
|   subDir = "locale-data", |  | ||||||
|   addFunc = "__addLocaleData", |  | ||||||
|   skipMissing = true |  | ||||||
| ) => { |  | ||||||
|   let localeData; |   let localeData; | ||||||
|   try { |   try { | ||||||
|     // use "pt" for "pt-BR", because "pt-BR" is unsupported by @formatjs |  | ||||||
|     const language = lang === "pt-BR" ? "pt" : lang; |  | ||||||
|  |  | ||||||
|     localeData = await readFile( |     localeData = await readFile( | ||||||
|       join(formatjsDir, pkg, subDir, `${language}.js`), |       path.resolve( | ||||||
|  |         paths.polymer_dir, | ||||||
|  |         `node_modules/@formatjs/${pkg}/locale-data/${lang}.js` | ||||||
|  |       ), | ||||||
|       "utf-8" |       "utf-8" | ||||||
|     ); |     ); | ||||||
|   } catch (e) { |   } catch (e) { | ||||||
|     // Ignore if language is missing (i.e. not supported by @formatjs) |     // Ignore if language is missing (i.e. not supported by @formatjs) | ||||||
|     if (e.code === "ENOENT" && skipMissing) { |     if (e.code === "ENOENT") { | ||||||
|       console.warn(`Skipped missing data for language ${lang} from ${pkg}`); |  | ||||||
|       return; |       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}`); |  | ||||||
|   } |   } | ||||||
|  |   // Convert to JSON | ||||||
|  |   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 |   // Parse to validate JSON, then stringify to minify | ||||||
|   localeData = JSON.stringify(JSON.parse(localeData)); |   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])); | 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 () => { | gulp.task("create-locale-data", async () => { | ||||||
|   const translationMeta = JSON.parse( |   const translationMeta = JSON.parse( | ||||||
|     await readFile( |     await readFile( | ||||||
|       resolve(paths.translations_src, "translationMetadata.json"), |       path.resolve(paths.translations_src, "translationMetadata.json"), | ||||||
|       "utf-8" |       "utf-8" | ||||||
|     ) |     ) | ||||||
|   ); |   ); | ||||||
|   const conversions = []; |   const conversions = []; | ||||||
|   for (const pkg of Object.keys(INTL_POLYFILLS)) { |   for (const pkg of Object.keys(INTL_PACKAGES)) { | ||||||
|     // eslint-disable-next-line no-await-in-loop |     await mkdir(path.join(outDir, pkg), { recursive: true }); | ||||||
|     await mkdir(join(outDir, pkg), { recursive: true }); |  | ||||||
|     for (const lang of Object.keys(translationMeta)) { |     for (const lang of Object.keys(translationMeta)) { | ||||||
|       conversions.push(convertToJSON(pkg, lang)); |       conversions.push(convertToJSON(pkg, lang)); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   conversions.push( |  | ||||||
|     convertToJSON( |  | ||||||
|       "intl-datetimeformat", |  | ||||||
|       "add-all-tz", |  | ||||||
|       ".", |  | ||||||
|       "__addTZData", |  | ||||||
|       false |  | ||||||
|     ) |  | ||||||
|   ); |  | ||||||
|   await Promise.all(conversions); |   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,18 +1,19 @@ | |||||||
| // 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 gulp from "gulp"; | ||||||
| import { mkdir, readFile, symlink, writeFile } from "node:fs/promises"; | import path from "path"; | ||||||
| import { basename, join, relative } from "node:path"; | import sourceMapUrl from "source-map-url"; | ||||||
| import { injectManifest } from "workbox-build"; | import workboxBuild from "workbox-build"; | ||||||
| import paths from "../paths.cjs"; | import paths from "../paths.cjs"; | ||||||
|  |  | ||||||
| const SW_MAP = { | const swDest = path.resolve(paths.app_output_root, "service_worker.js"); | ||||||
|   [paths.app_output_latest]: "modern", |  | ||||||
|   [paths.app_output_es5]: "legacy", |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| 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'); | console.debug('Service worker disabled in development'); | ||||||
|  |  | ||||||
| @@ -21,40 +22,46 @@ self.addEventListener('install', (event) => { | |||||||
|   // removing any prod service worker the dev might have running |   // removing any prod service worker the dev might have running | ||||||
|   self.skipWaiting(); |   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", () => | gulp.task("gen-service-worker-app-prod", async () => { | ||||||
|   Promise.all( |   // Read bundled source file | ||||||
|     Object.entries(SW_MAP).map(async ([outPath, build]) => { |   const bundleManifestLatest = fs.readJsonSync( | ||||||
|       const manifest = JSON.parse( |     path.resolve(paths.app_output_latest, "manifest.json") | ||||||
|         await readFile(join(outPath, "manifest.json"), "utf-8") |  | ||||||
|   ); |   ); | ||||||
|       const swSrc = join(paths.app_output_root, manifest["service-worker.js"]); |   let serviceWorkerContent = fs.readFileSync( | ||||||
|       const swDest = join(paths.app_output_root, `sw-${build}.js`); |     paths.app_output_root + bundleManifestLatest["service_worker.js"], | ||||||
|       const buildDir = relative(paths.app_output_root, outPath); |     "utf-8" | ||||||
|       const { warnings } = await injectManifest({ |   ); | ||||||
|         swSrc, |  | ||||||
|         swDest, |   // Delete old file from frontend_latest so manifest won't pick it up | ||||||
|         injectionPoint: "__WB_MANIFEST__", |   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 |     // Files that mach this pattern will be considered unique and skip revision check | ||||||
|     // ignore JS files + translation files |     // ignore JS files + translation files | ||||||
|         dontCacheBustURLsMatching: new RegExp( |     dontCacheBustURLsMatching: /(frontend_latest\/.+|static\/translations\/.+)/, | ||||||
|           `(?:${buildDir}/.+|static/translations/.+)` |  | ||||||
|         ), |  | ||||||
|     globDirectory: paths.app_output_root, |     globDirectory: paths.app_output_root, | ||||||
|     globPatterns: [ |     globPatterns: [ | ||||||
|           `${buildDir}/*.js`, |       "frontend_latest/*.js", | ||||||
|       // Cache all English translations because we catch them as fallback |       // Cache all English translations because we catch them as fallback | ||||||
|       // Using pattern to match hash instead of * to avoid caching en-GB |       // 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' |       // 'v' added as valid hash letter because in dev we hash with 'dev' | ||||||
| @@ -68,20 +75,19 @@ gulp.task("gen-service-worker-app-prod", () => | |||||||
|       "static/fonts/roboto/Roboto-Regular.woff2", |       "static/fonts/roboto/Roboto-Regular.woff2", | ||||||
|       "static/fonts/roboto/Roboto-Bold.woff2", |       "static/fonts/roboto/Roboto-Bold.woff2", | ||||||
|     ], |     ], | ||||||
|         globIgnores: [`${buildDir}/service-worker*`], |  | ||||||
|   }); |   }); | ||||||
|       if (warnings.length > 0) { |  | ||||||
|         console.warn( |   for (const warning of workboxManifest.warnings) { | ||||||
|           `Problems while injecting ${build} service worker:\n`, |     console.warn(warning); | ||||||
|           warnings.join("\n") |   } | ||||||
|  |  | ||||||
|  |   // remove source map and add WB manifest | ||||||
|  |   serviceWorkerContent = sourceMapUrl.removeFrom(serviceWorkerContent); | ||||||
|  |   serviceWorkerContent = serviceWorkerContent.replace( | ||||||
|  |     "WB_MANIFEST", | ||||||
|  |     JSON.stringify(workboxManifest.manifestEntries) | ||||||
|   ); |   ); | ||||||
|       } |  | ||||||
|       await deleteAsync(`${swSrc}?(.map)`); |   // Write new file to root | ||||||
|       // Needed to install new SW from a cached HTML |   fs.writeFileSync(swDest, serviceWorkerContent); | ||||||
|       if (build === "modern") { | }); | ||||||
|         const swOld = join(paths.app_output_root, "service_worker.js"); |  | ||||||
|         await symlink(basename(swDest), swOld); |  | ||||||
|       } |  | ||||||
|     }) |  | ||||||
|   ) |  | ||||||
| ); |  | ||||||
|   | |||||||
| @@ -1,103 +1,97 @@ | |||||||
| /* eslint-disable max-classes-per-file */ | import { createHash } from "crypto"; | ||||||
|  | import { deleteSync } from "del"; | ||||||
| import { deleteAsync } from "del"; | import { | ||||||
| import { glob } from "glob"; |   mkdirSync, | ||||||
|  |   readdirSync, | ||||||
|  |   readFileSync, | ||||||
|  |   renameSync, | ||||||
|  |   writeFile, | ||||||
|  | } from "fs"; | ||||||
| import gulp from "gulp"; | 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 rename from "gulp-rename"; | ||||||
| import merge from "lodash.merge"; | import path from "path"; | ||||||
| import { createHash } from "node:crypto"; | import vinylBuffer from "vinyl-buffer"; | ||||||
| import { mkdir, readFile } from "node:fs/promises"; | import source from "vinyl-source-stream"; | ||||||
| import { basename, join } from "node:path"; |  | ||||||
| import { PassThrough, Transform } from "node:stream"; |  | ||||||
| import { finished } from "node:stream/promises"; |  | ||||||
| import env from "../env.cjs"; | import env from "../env.cjs"; | ||||||
| import paths from "../paths.cjs"; | import paths from "../paths.cjs"; | ||||||
|  | import { mapFiles } from "../util.cjs"; | ||||||
| import "./fetch-nightly-translations.js"; | import "./fetch-nightly-translations.js"; | ||||||
|  |  | ||||||
| const inFrontendDir = "translations/frontend"; | const inFrontendDir = "translations/frontend"; | ||||||
| const inBackendDir = "translations/backend"; | const inBackendDir = "translations/backend"; | ||||||
| const workDir = "build/translations"; | const workDir = "build/translations"; | ||||||
| const outDir = join(workDir, "output"); | const fullDir = workDir + "/full"; | ||||||
| const EN_SRC = join(paths.translations_src, "en.json"); | const coreDir = workDir + "/core"; | ||||||
| const TEST_LOCALE = "en-x-test"; | const outDir = workDir + "/output"; | ||||||
|  |  | ||||||
| let mergeBackend = false; | let mergeBackend = false; | ||||||
|  |  | ||||||
| gulp.task( | gulp.task( | ||||||
|   "translations-enable-merge-backend", |   "translations-enable-merge-backend", | ||||||
|   gulp.parallel(async () => { |   gulp.parallel((done) => { | ||||||
|     mergeBackend = true; |     mergeBackend = true; | ||||||
|  |     done(); | ||||||
|   }, "allow-setup-fetch-nightly-translations") |   }, "allow-setup-fetch-nightly-translations") | ||||||
| ); | ); | ||||||
|  |  | ||||||
| // Transform stream to apply a function on Vinyl JSON files (buffer mode only). | // Panel translations which should be split from the core translations. | ||||||
| // The provided function can either return a new object, or an array of | const TRANSLATION_FRAGMENTS = Object.keys( | ||||||
| // [object, subdirectory] pairs for fragmentizing the JSON. |   JSON.parse( | ||||||
| class CustomJSON extends Transform { |     readFileSync( | ||||||
|   constructor(func, reviver = null) { |       path.resolve(paths.polymer_dir, "src/translations/en.json"), | ||||||
|     super({ objectMode: true }); |       "utf-8" | ||||||
|     this._func = func; |     ) | ||||||
|     this._reviver = reviver; |   ).ui.panel | ||||||
|   } | ); | ||||||
|  |  | ||||||
|   // eslint-disable-next-line @typescript-eslint/naming-convention | function recursiveFlatten(prefix, data) { | ||||||
|   async _transform(file, _, callback) { |   let output = {}; | ||||||
|     let obj = JSON.parse(file.contents.toString(), this._reviver); |   Object.keys(data).forEach((key) => { | ||||||
|     if (this._func) obj = this._func(obj, file.path); |     if (typeof data[key] === "object") { | ||||||
|     for (const [outObj, dir] of Array.isArray(obj) ? obj : [[obj, ""]]) { |       output = { | ||||||
|       const outFile = file.clone({ contents: false }); |         ...output, | ||||||
|       outFile.contents = Buffer.from(JSON.stringify(outObj)); |         ...recursiveFlatten(prefix + key + ".", data[key]), | ||||||
|       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)); |  | ||||||
|     } else { |     } else { | ||||||
|       output[prefix + key] = value; |       output[prefix + key] = data[key]; | ||||||
|     } |  | ||||||
|     } |     } | ||||||
|  |   }); | ||||||
|   return output; |   return output; | ||||||
| }; | } | ||||||
|  |  | ||||||
| // Filter functions that can be passed directly to JSON.parse() | function flatten(data) { | ||||||
| const emptyReviver = (_key, value) => value || undefined; |   return recursiveFlatten("", data); | ||||||
| const testReviver = (_key, value) => | } | ||||||
|   value && typeof value === "string" ? "TRANSLATED" : value; |  | ||||||
|  | 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. |  * 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 |  * be included in src/translations/en.json, but still be usable while | ||||||
|  * developing locally. |  * 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 re_key_reference = /\[%key:([^%]+)%\]/; | ||||||
| const lokaliseTransform = (data, path, original = data) => { | function lokaliseTransform(data, original, file) { | ||||||
|   const output = {}; |   const output = {}; | ||||||
|   for (const [key, value] of Object.entries(data)) { |   Object.entries(data).forEach(([key, value]) => { | ||||||
|     if (typeof value === "object") { |     if (value instanceof Object) { | ||||||
|       output[key] = lokaliseTransform(value, path, original); |       output[key] = lokaliseTransform(value, original, file); | ||||||
|     } else { |     } 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) => { |         const replace = lokalise_key.split("::").reduce((tr, k) => { | ||||||
|           if (!tr) { |           if (!tr) { | ||||||
|             throw Error(`Invalid key placeholder ${lokalise_key} in ${path}`); |             throw Error( | ||||||
|  |               `Invalid key placeholder ${lokalise_key} in ${file.path}` | ||||||
|  |             ); | ||||||
|           } |           } | ||||||
|           return tr[k]; |           return tr[k]; | ||||||
|         }, original); |         }, original); | ||||||
|         if (typeof replace !== "string") { |         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 replace; | ||||||
|       }); |       }); | ||||||
|     } |     } | ||||||
|   } |   }); | ||||||
|   return output; |   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 = () => | gulp.task("create-test-metadata", (cb) => { | ||||||
|   env.isProdBuild() |   writeFile( | ||||||
|     ? Promise.resolve() |     workDir + "/testMetadata.json", | ||||||
|     : gulp |     JSON.stringify({ | ||||||
|         .src(EN_SRC) |       test: { | ||||||
|         .pipe(new CustomJSON(null, testReviver)) |         nativeName: "Test", | ||||||
|         .pipe(rename(`${TEST_LOCALE}.json`)) |       }, | ||||||
|         .pipe(gulp.dest(workDir)); |     }), | ||||||
|  |     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 |  * 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 |  * project is buildable immediately after merging new translation keys, since | ||||||
|  * the Lokalise update to translations/en.json will not happen immediately. |  * the Lokalise update to translations/en.json will not happen immediately. | ||||||
|  */ |  */ | ||||||
| const createMasterTranslation = () => | gulp.task("build-master-translation", () => { | ||||||
|   gulp |   const src = [path.join(paths.translations_src, "en.json")]; | ||||||
|     .src([EN_SRC, ...(mergeBackend ? [`${inBackendDir}/en.json`] : [])]) |  | ||||||
|     .pipe(new CustomJSON(lokaliseTransform)) |  | ||||||
|     .pipe(new MergeJSON("en")) |  | ||||||
|     .pipe(gulp.dest(workDir)); |  | ||||||
|  |  | ||||||
| const FRAGMENTS = ["base"]; |   if (mergeBackend) { | ||||||
|  |     src.push(path.join(inBackendDir, "en.json")); | ||||||
| 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)); |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   // The downstream pipeline is setup first.  It hashes the merged data for |   return gulp | ||||||
|   // each locale, then fragmentizes and flattens the data for final output. |     .src(src) | ||||||
|   const translationFiles = await glob([ |     .pipe(transform((data, file) => lokaliseTransform(data, data, file))) | ||||||
|     `${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 |  | ||||||
|     .pipe( |     .pipe( | ||||||
|       new CustomJSON((data) => |       merge({ | ||||||
|         FRAGMENTS.map((fragment) => { |         fileName: "en.json", | ||||||
|           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(fullDir)); | ||||||
|     .pipe(gulp.dest(outDir)); | }); | ||||||
|  |  | ||||||
|   // Send the English master downstream first, then for each other locale | gulp.task("build-merged-translations", () => | ||||||
|   // generate merged JSON data to continue piping. It begins with the master |   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 |         // translation as a failsafe for untranslated strings, and merges all parent | ||||||
|         // tags into one file for each specific subtag |         // tags into one file for each specific subtag | ||||||
|         // |         // | ||||||
|         // TODO: This is a naive interpretation of BCP47 that should be improved. |         // 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 |         //       Will be OK for now as long as we don't have anything more complicated | ||||||
|         //       than a base translation + region. |         //       than a base translation + region. | ||||||
|   const masterStream = gulp |         const tr = path.basename(file.history[0], ".json"); | ||||||
|     .src(`${workDir}/en.json`) |         const subtags = tr.split("-"); | ||||||
|     .pipe(new PassThrough({ objectMode: true })); |         const src = [fullDir + "/en.json"]; | ||||||
|   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++) { |         for (let i = 1; i <= subtags.length; i++) { | ||||||
|           const lang = subtags.slice(0, i).join("-"); |           const lang = subtags.slice(0, i).join("-"); | ||||||
|       if (lang === TEST_LOCALE) { |           if (lang === "test") { | ||||||
|         mergeFiles.push(`${workDir}/${TEST_LOCALE}.json`); |             src.push(workDir + "/test.json"); | ||||||
|           } else if (lang !== "en") { |           } else if (lang !== "en") { | ||||||
|         mergeFiles.push(`${inFrontendDir}/${lang}.json`); |             src.push(inFrontendDir + "/" + lang + ".json"); | ||||||
|             if (mergeBackend) { |             if (mergeBackend) { | ||||||
|           mergeFiles.push(`${inBackendDir}/${lang}.json`); |               src.push(inBackendDir + "/" + lang + ".json"); | ||||||
|             } |             } | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
|     const mergeStream = gulp |         return gulp | ||||||
|       .src(mergeFiles, { allowEmpty: true }) |           .src(src, { allowEmpty: true }) | ||||||
|       .pipe(new MergeJSON(locale, enMaster, emptyReviver)); |           .pipe(transform((data) => emptyFilter(data))) | ||||||
|     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( |           .pipe( | ||||||
|       new CustomJSON((meta) => { |             merge({ | ||||||
|         // Add the test translation in development. |               fileName: tr + ".json", | ||||||
|         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, |  | ||||||
|         }; |  | ||||||
|             }) |             }) | ||||||
|           ) |           ) | ||||||
|     .pipe(gulp.dest(workDir)); |           .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( | gulp.task( | ||||||
|   "build-translations", |   "build-translations", | ||||||
|   gulp.series( |   gulp.series( | ||||||
|     gulp.parallel( |     gulp.parallel( | ||||||
|       "fetch-nightly-translations", |       "fetch-nightly-translations", | ||||||
|       gulp.series("clean-translations", makeWorkDir) |       gulp.series("clean-translations", "ensure-translations-build-dir") | ||||||
|     ), |     ), | ||||||
|     createTestTranslation, |     "create-translations", | ||||||
|     createMasterTranslation, |     "build-translation-fingerprints", | ||||||
|     createTranslations, |     "build-translation-write-metadata" | ||||||
|     writeTranslationMetaData |  | ||||||
|   ) |   ) | ||||||
| ); | ); | ||||||
|  |  | ||||||
| gulp.task( | gulp.task( | ||||||
|   "build-supervisor-translations", |   "build-supervisor-translations", | ||||||
|   gulp.series(setFragment("supervisor"), "build-translations") |   gulp.series( | ||||||
| ); |     gulp.parallel( | ||||||
|  |       "fetch-nightly-translations", | ||||||
| gulp.task( |       gulp.series("clean-translations", "ensure-translations-build-dir") | ||||||
|   "build-landing-page-translations", |     ), | ||||||
|   gulp.series(setFragment("landing-page"), "build-translations") |     "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 fs from "fs"; | ||||||
| import path from "path"; | import path from "path"; | ||||||
| import log from "fancy-log"; | import log from "fancy-log"; | ||||||
| import gulp from "gulp"; | import gulp from "gulp"; | ||||||
| import rspack from "@rspack/core"; | import webpack from "webpack"; | ||||||
| import { RspackDevServer } from "@rspack/dev-server"; | import WebpackDevServer from "webpack-dev-server"; | ||||||
| import env from "../env.cjs"; | import env from "../env.cjs"; | ||||||
| import paths from "../paths.cjs"; | import paths from "../paths.cjs"; | ||||||
| import { | import { | ||||||
| @@ -14,8 +14,7 @@ import { | |||||||
|   createDemoConfig, |   createDemoConfig, | ||||||
|   createGalleryConfig, |   createGalleryConfig, | ||||||
|   createHassioConfig, |   createHassioConfig, | ||||||
|   createLandingPageConfig, | } from "../webpack.cjs"; | ||||||
| } from "../rspack.cjs"; |  | ||||||
| 
 | 
 | ||||||
| const bothBuilds = (createConfigFunc, params) => [ | const bothBuilds = (createConfigFunc, params) => [ | ||||||
|   createConfigFunc({ ...params, latestBuild: true }), |   createConfigFunc({ ...params, latestBuild: true }), | ||||||
| @@ -31,7 +30,7 @@ const isWsl = | |||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * @param {{ |  * @param {{ | ||||||
|  *   compiler: import("@rspack/core").Compiler, |  *   compiler: import("webpack").Compiler, | ||||||
|  *   contentBase: string, |  *   contentBase: string, | ||||||
|  *   port: number, |  *   port: number, | ||||||
|  *   listenHost?: string |  *   listenHost?: string | ||||||
| @@ -41,14 +40,9 @@ const runDevServer = async ({ | |||||||
|   compiler, |   compiler, | ||||||
|   contentBase, |   contentBase, | ||||||
|   port, |   port, | ||||||
|   listenHost = undefined, |   listenHost = "localhost", | ||||||
|   proxy = undefined, |  | ||||||
| }) => { | }) => { | ||||||
|   if (listenHost === undefined) { |   const server = new WebpackDevServer( | ||||||
|     // For dev container, we need to listen on all hosts
 |  | ||||||
|     listenHost = env.isDevContainer() ? "0.0.0.0" : "localhost"; |  | ||||||
|   } |  | ||||||
|   const server = new RspackDevServer( |  | ||||||
|     { |     { | ||||||
|       hot: false, |       hot: false, | ||||||
|       open: true, |       open: true, | ||||||
| @@ -58,14 +52,13 @@ const runDevServer = async ({ | |||||||
|         directory: contentBase, |         directory: contentBase, | ||||||
|         watch: true, |         watch: true, | ||||||
|       }, |       }, | ||||||
|       proxy, |  | ||||||
|     }, |     }, | ||||||
|     compiler |     compiler | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   await server.start(); |   await server.start(); | ||||||
|   // Server listening
 |   // 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) => { | const doneHandler = (done) => (err, stats) => { | ||||||
| @@ -90,27 +83,27 @@ const doneHandler = (done) => (err, stats) => { | |||||||
| 
 | 
 | ||||||
| const prodBuild = (conf) => | const prodBuild = (conf) => | ||||||
|   new Promise((resolve) => { |   new Promise((resolve) => { | ||||||
|     rspack( |     webpack( | ||||||
|       conf, |       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) |       doneHandler(resolve) | ||||||
|     ); |     ); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
| gulp.task("rspack-watch-app", () => { | gulp.task("webpack-watch-app", () => { | ||||||
|   // This command will run forever because we don't close compiler
 |   // This command will run forever because we don't close compiler
 | ||||||
|   rspack( |   webpack( | ||||||
|     process.env.ES5 |     process.env.ES5 | ||||||
|       ? bothBuilds(createAppConfig, { isProdBuild: false }) |       ? bothBuilds(createAppConfig, { isProdBuild: false }) | ||||||
|       : createAppConfig({ isProdBuild: false, latestBuild: true }) |       : createAppConfig({ isProdBuild: false, latestBuild: true }) | ||||||
|   ).watch({ poll: isWsl }, doneHandler()); |   ).watch({ poll: isWsl }, doneHandler()); | ||||||
|   gulp.watch( |   gulp.watch( | ||||||
|     path.join(paths.translations_src, "en.json"), |     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( |   prodBuild( | ||||||
|     bothBuilds(createAppConfig, { |     bothBuilds(createAppConfig, { | ||||||
|       isProdBuild: true, |       isProdBuild: true, | ||||||
| @@ -120,30 +113,25 @@ gulp.task("rspack-prod-app", () => | |||||||
|   ) |   ) | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| gulp.task("rspack-dev-server-demo", () => | gulp.task("webpack-dev-server-demo", () => | ||||||
|   runDevServer({ |   runDevServer({ | ||||||
|     compiler: rspack( |     compiler: webpack(bothBuilds(createDemoConfig, { isProdBuild: false })), | ||||||
|       createDemoConfig({ isProdBuild: false, latestBuild: true }) |  | ||||||
|     ), |  | ||||||
|     contentBase: paths.demo_output_root, |     contentBase: paths.demo_output_root, | ||||||
|     port: 8090, |     port: 8090, | ||||||
|   }) |   }) | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| gulp.task("rspack-prod-demo", () => | gulp.task("webpack-prod-demo", () => | ||||||
|   prodBuild( |   prodBuild( | ||||||
|     bothBuilds(createDemoConfig, { |     bothBuilds(createDemoConfig, { | ||||||
|       isProdBuild: true, |       isProdBuild: true, | ||||||
|       isStatsBuild: env.isStatsBuild(), |  | ||||||
|     }) |     }) | ||||||
|   ) |   ) | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| gulp.task("rspack-dev-server-cast", () => | gulp.task("webpack-dev-server-cast", () => | ||||||
|   runDevServer({ |   runDevServer({ | ||||||
|     compiler: rspack( |     compiler: webpack(bothBuilds(createCastConfig, { isProdBuild: false })), | ||||||
|       createCastConfig({ isProdBuild: false, latestBuild: true }) |  | ||||||
|     ), |  | ||||||
|     contentBase: paths.cast_output_root, |     contentBase: paths.cast_output_root, | ||||||
|     port: 8080, |     port: 8080, | ||||||
|     // Accessible from the network, because that's how Cast hits it.
 |     // 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( |   prodBuild( | ||||||
|     bothBuilds(createCastConfig, { |     bothBuilds(createCastConfig, { | ||||||
|       isProdBuild: true, |       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
 |   // This command will run forever because we don't close compiler
 | ||||||
|   rspack( |   webpack( | ||||||
|     createHassioConfig({ |     createHassioConfig({ | ||||||
|       isProdBuild: false, |       isProdBuild: false, | ||||||
|       latestBuild: true, |       latestBuild: true, | ||||||
| @@ -174,7 +162,7 @@ gulp.task("rspack-watch-hassio", () => { | |||||||
|   ); |   ); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| gulp.task("rspack-prod-hassio", () => | gulp.task("webpack-prod-hassio", () => | ||||||
|   prodBuild( |   prodBuild( | ||||||
|     bothBuilds(createHassioConfig, { |     bothBuilds(createHassioConfig, { | ||||||
|       isProdBuild: true, |       isProdBuild: true, | ||||||
| @@ -184,18 +172,17 @@ gulp.task("rspack-prod-hassio", () => | |||||||
|   ) |   ) | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| gulp.task("rspack-dev-server-gallery", () => | gulp.task("webpack-dev-server-gallery", () => | ||||||
|   runDevServer({ |   runDevServer({ | ||||||
|     compiler: rspack( |     // We don't use the es5 build, but the dev server will fuck up the publicPath if we don't
 | ||||||
|       createGalleryConfig({ isProdBuild: false, latestBuild: true }) |     compiler: webpack(bothBuilds(createGalleryConfig, { isProdBuild: false })), | ||||||
|     ), |  | ||||||
|     contentBase: paths.gallery_output_root, |     contentBase: paths.gallery_output_root, | ||||||
|     port: 8100, |     port: 8100, | ||||||
|     listenHost: "0.0.0.0", |     listenHost: "0.0.0.0", | ||||||
|   }) |   }) | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| gulp.task("rspack-prod-gallery", () => | gulp.task("webpack-prod-gallery", () => | ||||||
|   prodBuild( |   prodBuild( | ||||||
|     createGalleryConfig({ |     createGalleryConfig({ | ||||||
|       isProdBuild: true, |       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(), |  | ||||||
|     }) |  | ||||||
|   ) |  | ||||||
| ); |  | ||||||
| @@ -6,8 +6,6 @@ import presetEnv from "@babel/preset-env"; | |||||||
| import compilationTargets from "@babel/helper-compilation-targets"; | import compilationTargets from "@babel/helper-compilation-targets"; | ||||||
| import coreJSCompat from "core-js-compat"; | import coreJSCompat from "core-js-compat"; | ||||||
| import { logPlugin } from "@babel/preset-env/lib/debug.js"; | import { logPlugin } from "@babel/preset-env/lib/debug.js"; | ||||||
| // eslint-disable-next-line import/no-relative-packages |  | ||||||
| import shippedPolyfills from "../node_modules/babel-plugin-polyfill-corejs3/lib/shipped-proposals.js"; |  | ||||||
| import { babelOptions } from "./bundle.cjs"; | import { babelOptions } from "./bundle.cjs"; | ||||||
|  |  | ||||||
| const detailsOpen = (heading) => | const detailsOpen = (heading) => | ||||||
| @@ -16,7 +14,6 @@ const detailsClose = "</details>\n"; | |||||||
|  |  | ||||||
| const dummyAPI = { | const dummyAPI = { | ||||||
|   version: babelVersion, |   version: babelVersion, | ||||||
|   // eslint-disable-next-line @typescript-eslint/no-empty-function |  | ||||||
|   assertVersion: () => {}, |   assertVersion: () => {}, | ||||||
|   caller: (callback) => |   caller: (callback) => | ||||||
|     callback({ |     callback({ | ||||||
| @@ -29,22 +26,6 @@ const dummyAPI = { | |||||||
|   targets: () => ({}), |   targets: () => ({}), | ||||||
| }; | }; | ||||||
|  |  | ||||||
| // Generate filter function based on proposal/method inputs |  | ||||||
| // Copied and adapted from babel-plugin-polyfill-corejs3/esm/index.mjs |  | ||||||
| const polyfillFilter = (method, proposals, shippedProposals) => (name) => { |  | ||||||
|   if (proposals || method === "entry-global") return true; |  | ||||||
|   if (shippedProposals && shippedPolyfills.default.has(name)) { |  | ||||||
|     return true; |  | ||||||
|   } |  | ||||||
|   if (name.startsWith("esnext.")) { |  | ||||||
|     const esName = `es.${name.slice(7)}`; |  | ||||||
|     // If its imaginative esName is not in latest compat data, it means the proposal is not stage 4 |  | ||||||
|     return esName in coreJSCompat.data; |  | ||||||
|   } |  | ||||||
|   return true; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| // Log the plugins and polyfills for each build environment |  | ||||||
| for (const buildType of ["Modern", "Legacy"]) { | for (const buildType of ["Modern", "Legacy"]) { | ||||||
|   const browserslistEnv = buildType.toLowerCase(); |   const browserslistEnv = buildType.toLowerCase(); | ||||||
|   const babelOpts = babelOptions({ latestBuild: browserslistEnv === "modern" }); |   const babelOpts = babelOptions({ latestBuild: browserslistEnv === "modern" }); | ||||||
| @@ -65,13 +46,7 @@ for (const buildType of ["Modern", "Legacy"]) { | |||||||
|     const targets = compilationTargets.default(babelOpts?.targets, { |     const targets = compilationTargets.default(babelOpts?.targets, { | ||||||
|       browserslistEnv, |       browserslistEnv, | ||||||
|     }); |     }); | ||||||
|     const polyfillList = coreJSCompat({ targets }).list.filter( |     const polyfillList = coreJSCompat({ targets }).list; | ||||||
|       polyfillFilter( |  | ||||||
|         `${presetEnvOpts.useBuiltIns}-global`, |  | ||||||
|         presetEnvOpts?.corejs?.proposals, |  | ||||||
|         presetEnvOpts?.shippedProposals |  | ||||||
|       ) |  | ||||||
|     ); |  | ||||||
|     console.log( |     console.log( | ||||||
|       "The following %i polyfills may be injected by Babel:\n", |       "The following %i polyfills may be injected by Babel:\n", | ||||||
|       polyfillList.length |       polyfillList.length | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| const path = require("path"); | const path = require("path"); | ||||||
|  |  | ||||||
| module.exports = { | module.exports = { | ||||||
|   root_dir: path.resolve(__dirname, ".."), |   polymer_dir: path.resolve(__dirname, ".."), | ||||||
|  |  | ||||||
|   build_dir: path.resolve(__dirname, "../build"), |   build_dir: path.resolve(__dirname, "../build"), | ||||||
|   app_output_root: path.resolve(__dirname, "../hass_frontend"), |   app_output_root: path.resolve(__dirname, "../hass_frontend"), | ||||||
| @@ -33,22 +33,6 @@ module.exports = { | |||||||
|   ), |   ), | ||||||
|   gallery_output_static: path.resolve(__dirname, "../gallery/dist/static"), |   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_dir: path.resolve(__dirname, "../hassio"), | ||||||
|   hassio_output_root: path.resolve(__dirname, "../hassio/build"), |   hassio_output_root: path.resolve(__dirname, "../hassio/build"), | ||||||
|   hassio_output_static: path.resolve(__dirname, "../hassio/build/static"), |   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,9 @@ | |||||||
| const { existsSync } = require("fs"); | const webpack = require("webpack"); | ||||||
| const path = require("path"); | 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 TerserPlugin = require("terser-webpack-plugin"); | const TerserPlugin = require("terser-webpack-plugin"); | ||||||
| // eslint-disable-next-line @typescript-eslint/naming-convention
 | const { WebpackManifestPlugin } = require("webpack-manifest-plugin"); | ||||||
| const { WebpackManifestPlugin } = require("rspack-manifest-plugin"); |  | ||||||
| const log = require("fancy-log"); | const log = require("fancy-log"); | ||||||
| // eslint-disable-next-line @typescript-eslint/naming-convention
 | const WebpackBar = require("webpackbar"); | ||||||
| const WebpackBar = require("webpackbar/rspack"); |  | ||||||
| const paths = require("./paths.cjs"); | const paths = require("./paths.cjs"); | ||||||
| const bundle = require("./bundle.cjs"); | const bundle = require("./bundle.cjs"); | ||||||
| 
 | 
 | ||||||
| @@ -30,7 +21,7 @@ class LogStartCompilePlugin { | |||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| const createRspackConfig = ({ | const createWebpackConfig = ({ | ||||||
|   name, |   name, | ||||||
|   entry, |   entry, | ||||||
|   outputPath, |   outputPath, | ||||||
| @@ -65,32 +56,17 @@ const createRspackConfig = ({ | |||||||
|       rules: [ |       rules: [ | ||||||
|         { |         { | ||||||
|           test: /\.m?js$|\.ts$/, |           test: /\.m?js$|\.ts$/, | ||||||
|           exclude: /node_modules[\\/]core-js/, |           use: { | ||||||
|           use: (info) => [ |  | ||||||
|             { |  | ||||||
|             loader: "babel-loader", |             loader: "babel-loader", | ||||||
|             options: { |             options: { | ||||||
|                 ...bundle.babelOptions({ |               ...bundle.babelOptions({ latestBuild, isProdBuild, isTestBuild }), | ||||||
|                   latestBuild, |  | ||||||
|                   isProdBuild, |  | ||||||
|                   isTestBuild, |  | ||||||
|                   sw: info.issuerLayer === "sw", |  | ||||||
|                 }), |  | ||||||
|               cacheDirectory: !isProdBuild, |               cacheDirectory: !isProdBuild, | ||||||
|               cacheCompression: false, |               cacheCompression: false, | ||||||
|             }, |             }, | ||||||
|           }, |           }, | ||||||
|             { |  | ||||||
|               loader: "builtin:swc-loader", |  | ||||||
|               options: bundle.swcOptions(), |  | ||||||
|             }, |  | ||||||
|           ], |  | ||||||
|           resolve: { |           resolve: { | ||||||
|             fullySpecified: false, |             fullySpecified: false, | ||||||
|           }, |           }, | ||||||
|           parser: { |  | ||||||
|             worker: ["*context.audioWorklet.addModule()", "..."], |  | ||||||
|           }, |  | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|           test: /\.css$/, |           test: /\.css$/, | ||||||
| @@ -109,20 +85,11 @@ const createRspackConfig = ({ | |||||||
|       moduleIds: isProdBuild && !isStatsBuild ? "deterministic" : "named", |       moduleIds: isProdBuild && !isStatsBuild ? "deterministic" : "named", | ||||||
|       chunkIds: isProdBuild && !isStatsBuild ? "deterministic" : "named", |       chunkIds: isProdBuild && !isStatsBuild ? "deterministic" : "named", | ||||||
|       splitChunks: { |       splitChunks: { | ||||||
|         // Disable splitting for web workers and worklets because imports of
 |         // Disable splitting for web workers with ESM output
 | ||||||
|         // external chunks are broken for:
 |         // Imports of external chunks are broken
 | ||||||
|         chunks: !isProdBuild |         chunks: latestBuild | ||||||
|           ? // improve incremental build speed, but blows up bundle size
 |           ? (chunk) => !chunk.canBeInitial() && !/^.+-worker$/.test(chunk.name) | ||||||
|             new RegExp( |           : undefined, | ||||||
|               `^(?!(${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), |  | ||||||
|       }, |       }, | ||||||
|     }, |     }, | ||||||
|     plugins: [ |     plugins: [ | ||||||
| @@ -131,10 +98,10 @@ const createRspackConfig = ({ | |||||||
|         // Only include the JS of entrypoints
 |         // Only include the JS of entrypoints
 | ||||||
|         filter: (file) => file.isInitial && !file.name.endsWith(".map"), |         filter: (file) => file.isInitial && !file.name.endsWith(".map"), | ||||||
|       }), |       }), | ||||||
|       new rspack.DefinePlugin( |       new webpack.DefinePlugin( | ||||||
|         bundle.definedVars({ isProdBuild, latestBuild, defineOverlay }) |         bundle.definedVars({ isProdBuild, latestBuild, defineOverlay }) | ||||||
|       ), |       ), | ||||||
|       new rspack.IgnorePlugin({ |       new webpack.IgnorePlugin({ | ||||||
|         checkResource(resource, context) { |         checkResource(resource, context) { | ||||||
|           // Only use ignore to intercept imports that we don't control
 |           // Only use ignore to intercept imports that we don't control
 | ||||||
|           // inside node_module dependencies.
 |           // inside node_module dependencies.
 | ||||||
| @@ -143,8 +110,7 @@ const createRspackConfig = ({ | |||||||
|             // calling define.amd will call require("!!webpack amd options")
 |             // calling define.amd will call require("!!webpack amd options")
 | ||||||
|             resource.startsWith("!!webpack") || |             resource.startsWith("!!webpack") || | ||||||
|             // loaded by webpack dev server but doesn't exist.
 |             // loaded by webpack dev server but doesn't exist.
 | ||||||
|             resource === "webpack/hot" || |             resource === "webpack/hot" | ||||||
|             resource.startsWith("@swc/helpers") |  | ||||||
|           ) { |           ) { | ||||||
|             return false; |             return false; | ||||||
|           } |           } | ||||||
| @@ -167,34 +133,28 @@ const createRspackConfig = ({ | |||||||
|           ); |           ); | ||||||
|         }, |         }, | ||||||
|       }), |       }), | ||||||
|       new rspack.NormalModuleReplacementPlugin( |       new webpack.NormalModuleReplacementPlugin( | ||||||
|         new RegExp(bundle.emptyPackages({ isHassioBuild }).join("|")), |         new RegExp( | ||||||
|         path.resolve(paths.root_dir, "src/util/empty.js") |           bundle.emptyPackages({ latestBuild, isHassioBuild }).join("|") | ||||||
|  |         ), | ||||||
|  |         path.resolve(paths.polymer_dir, "src/util/empty.js") | ||||||
|  |       ), | ||||||
|  |       // 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") | ||||||
|  |           ), | ||||||
|  |           path.resolve( | ||||||
|  |             paths.polymer_dir, | ||||||
|  |             "src/resources/intl-polyfill-legacy.ts" | ||||||
|  |           ) | ||||||
|         ), |         ), | ||||||
|       !isProdBuild && new LogStartCompilePlugin(), |       !isProdBuild && new LogStartCompilePlugin(), | ||||||
|       isProdBuild && |  | ||||||
|         new StatsWriterPlugin({ |  | ||||||
|           filename: path.relative( |  | ||||||
|             outputPath, |  | ||||||
|             path.join(paths.build_dir, "stats", `${name}.json`) |  | ||||||
|           ), |  | ||||||
|           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, |  | ||||||
|           }, |  | ||||||
|         }), |  | ||||||
|     ].filter(Boolean), |     ].filter(Boolean), | ||||||
|     resolve: { |     resolve: { | ||||||
|       extensions: [".ts", ".js", ".json"], |       extensions: [".ts", ".js", ".json"], | ||||||
|       alias: { |       alias: { | ||||||
|         "lit/static-html$": "lit/static-html.js", |  | ||||||
|         "lit/decorators$": "lit/decorators.js", |         "lit/decorators$": "lit/decorators.js", | ||||||
|         "lit/directive$": "lit/directive.js", |         "lit/directive$": "lit/directive.js", | ||||||
|         "lit/directives/until$": "lit/directives/until.js", |         "lit/directives/until$": "lit/directives/until.js", | ||||||
| @@ -203,17 +163,12 @@ const createRspackConfig = ({ | |||||||
|         "lit/directives/if-defined$": "lit/directives/if-defined.js", |         "lit/directives/if-defined$": "lit/directives/if-defined.js", | ||||||
|         "lit/directives/guard$": "lit/directives/guard.js", |         "lit/directives/guard$": "lit/directives/guard.js", | ||||||
|         "lit/directives/cache$": "lit/directives/cache.js", |         "lit/directives/cache$": "lit/directives/cache.js", | ||||||
|         "lit/directives/join$": "lit/directives/join.js", |  | ||||||
|         "lit/directives/repeat$": "lit/directives/repeat.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/polyfill-support$": "lit/polyfill-support.js", | ||||||
|         "@lit-labs/virtualizer/layouts/grid": |         "@lit-labs/virtualizer/layouts/grid": | ||||||
|           "@lit-labs/virtualizer/layouts/grid.js", |           "@lit-labs/virtualizer/layouts/grid.js", | ||||||
|         "@lit-labs/virtualizer/polyfills/resize-observer-polyfill/ResizeObserver": |         "@lit-labs/virtualizer/polyfills/resize-observer-polyfill/ResizeObserver": | ||||||
|           "@lit-labs/virtualizer/polyfills/resize-observer-polyfill/ResizeObserver.js", |           "@lit-labs/virtualizer/polyfills/resize-observer-polyfill/ResizeObserver.js", | ||||||
|         "@lit-labs/observers/resize-controller": |  | ||||||
|           "@lit-labs/observers/resize-controller.js", |  | ||||||
|       }, |       }, | ||||||
|     }, |     }, | ||||||
|     output: { |     output: { | ||||||
| @@ -221,13 +176,14 @@ const createRspackConfig = ({ | |||||||
|       filename: ({ chunk }) => |       filename: ({ chunk }) => | ||||||
|         !isProdBuild || isStatsBuild || dontHash.has(chunk.name) |         !isProdBuild || isStatsBuild || dontHash.has(chunk.name) | ||||||
|           ? "[name].js" |           ? "[name].js" | ||||||
|           : "[name].[contenthash].js", |           : "[name]-[contenthash].js", | ||||||
|       chunkFilename: |       chunkFilename: | ||||||
|         isProdBuild && !isStatsBuild ? "[name].[contenthash].js" : "[name].js", |         isProdBuild && !isStatsBuild ? "[id]-[contenthash].js" : "[name].js", | ||||||
|       assetModuleFilename: |       assetModuleFilename: | ||||||
|         isProdBuild && !isStatsBuild ? "[id].[contenthash][ext]" : "[id][ext]", |         isProdBuild && !isStatsBuild ? "[id]-[contenthash][ext]" : "[id][ext]", | ||||||
|       crossOriginLoading: "use-credentials", |  | ||||||
|       hashFunction: "xxhash64", |       hashFunction: "xxhash64", | ||||||
|  |       hashDigest: "base64url", | ||||||
|  |       hashDigestLength: 11, // full length of 64 bit base64url
 | ||||||
|       path: outputPath, |       path: outputPath, | ||||||
|       publicPath, |       publicPath, | ||||||
|       // To silence warning in worker plugin
 |       // To silence warning in worker plugin
 | ||||||
| @@ -235,29 +191,21 @@ const createRspackConfig = ({ | |||||||
|       // Since production source maps don't include sources, we need to point to them elsewhere
 |       // Since production source maps don't include sources, we need to point to them elsewhere
 | ||||||
|       // For dependencies, just provide the path (no source in browser)
 |       // For dependencies, just provide the path (no source in browser)
 | ||||||
|       // Otherwise, point to the raw code on GitHub for browser to load
 |       // Otherwise, point to the raw code on GitHub for browser to load
 | ||||||
|       ...Object.fromEntries( |       devtoolModuleFilenameTemplate: | ||||||
|         ["", "Fallback"].map((v) => [ |  | ||||||
|           `devtool${v}ModuleFilenameTemplate`, |  | ||||||
|         !isTestBuild && isProdBuild |         !isTestBuild && isProdBuild | ||||||
|           ? (info) => { |           ? (info) => { | ||||||
|  |               const sourcePath = info.resourcePath.replace(/^\.\//, ""); | ||||||
|               if ( |               if ( | ||||||
|                   !path.isAbsolute(info.absoluteResourcePath) || |                 sourcePath.startsWith("node_modules") || | ||||||
|                   !existsSync(info.resourcePath) || |                 sourcePath.startsWith("webpack") | ||||||
|                   info.resourcePath.startsWith("./node_modules") |  | ||||||
|               ) { |               ) { | ||||||
|                   // Source URLs are unknown for dependencies, so we use a relative URL with a
 |                 return `no-source/${sourcePath}`; | ||||||
|                   // non - existent top directory.  This results in a clean source tree in browser
 |  | ||||||
|                   // dev tools, and they stay happy getting 404s with valid requests.
 |  | ||||||
|                   return `/unknown${path.resolve("/", info.resourcePath)}`; |  | ||||||
|               } |               } | ||||||
|                 return new URL(info.resourcePath, bundle.sourceMapURL()).href; |               return `${bundle.sourceMapURL()}/${sourcePath}`; | ||||||
|             } |             } | ||||||
|           : undefined, |           : undefined, | ||||||
|         ]) |  | ||||||
|       ), |  | ||||||
|     }, |     }, | ||||||
|     experiments: { |     experiments: { | ||||||
|       layers: true, |  | ||||||
|       outputModule: true, |       outputModule: true, | ||||||
|     }, |     }, | ||||||
|   }; |   }; | ||||||
| @@ -269,17 +217,17 @@ const createAppConfig = ({ | |||||||
|   isStatsBuild, |   isStatsBuild, | ||||||
|   isTestBuild, |   isTestBuild, | ||||||
| }) => | }) => | ||||||
|   createRspackConfig( |   createWebpackConfig( | ||||||
|     bundle.config.app({ isProdBuild, latestBuild, isStatsBuild, isTestBuild }) |     bundle.config.app({ isProdBuild, latestBuild, isStatsBuild, isTestBuild }) | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
| const createDemoConfig = ({ isProdBuild, latestBuild, isStatsBuild }) => | const createDemoConfig = ({ isProdBuild, latestBuild, isStatsBuild }) => | ||||||
|   createRspackConfig( |   createWebpackConfig( | ||||||
|     bundle.config.demo({ isProdBuild, latestBuild, isStatsBuild }) |     bundle.config.demo({ isProdBuild, latestBuild, isStatsBuild }) | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
| const createCastConfig = ({ isProdBuild, latestBuild }) => | const createCastConfig = ({ isProdBuild, latestBuild }) => | ||||||
|   createRspackConfig(bundle.config.cast({ isProdBuild, latestBuild })); |   createWebpackConfig(bundle.config.cast({ isProdBuild, latestBuild })); | ||||||
| 
 | 
 | ||||||
| const createHassioConfig = ({ | const createHassioConfig = ({ | ||||||
|   isProdBuild, |   isProdBuild, | ||||||
| @@ -287,7 +235,7 @@ const createHassioConfig = ({ | |||||||
|   isStatsBuild, |   isStatsBuild, | ||||||
|   isTestBuild, |   isTestBuild, | ||||||
| }) => | }) => | ||||||
|   createRspackConfig( |   createWebpackConfig( | ||||||
|     bundle.config.hassio({ |     bundle.config.hassio({ | ||||||
|       isProdBuild, |       isProdBuild, | ||||||
|       latestBuild, |       latestBuild, | ||||||
| @@ -297,10 +245,7 @@ const createHassioConfig = ({ | |||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
| const createGalleryConfig = ({ isProdBuild, latestBuild }) => | const createGalleryConfig = ({ isProdBuild, latestBuild }) => | ||||||
|   createRspackConfig(bundle.config.gallery({ isProdBuild, latestBuild })); |   createWebpackConfig(bundle.config.gallery({ isProdBuild, latestBuild })); | ||||||
| 
 |  | ||||||
| const createLandingPageConfig = ({ isProdBuild, latestBuild }) => |  | ||||||
|   createRspackConfig(bundle.config.landingPage({ isProdBuild, latestBuild })); |  | ||||||
| 
 | 
 | ||||||
| module.exports = { | module.exports = { | ||||||
|   createAppConfig, |   createAppConfig, | ||||||
| @@ -308,6 +253,5 @@ module.exports = { | |||||||
|   createCastConfig, |   createCastConfig, | ||||||
|   createHassioConfig, |   createHassioConfig, | ||||||
|   createGalleryConfig, |   createGalleryConfig, | ||||||
|   createRspackConfig, |   createWebpackConfig, | ||||||
|   createLandingPageConfig, |  | ||||||
| }; | }; | ||||||
| @@ -25,7 +25,7 @@ Home Assistant Cast is made up of two separate applications: | |||||||
|  |  | ||||||
| ### Setting dev variables | ### 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 | ### Changing configuration | ||||||
|  |  | ||||||
|   | |||||||
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 16 KiB | 
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 9.8 KiB | 
| @@ -14,5 +14,5 @@ | |||||||
|   "name": "Home Assistant Cast", |   "name": "Home Assistant Cast", | ||||||
|   "short_name": "HA Cast", |   "short_name": "HA Cast", | ||||||
|   "start_url": "/?homescreen=1", |   "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> |   </head> | ||||||
|   <body> |   <body> | ||||||
|     <%= renderTemplate("../../../src/html/_js_base.html.template") %> |     <%= 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"> |     <hc-layout subtitle="FAQ"> | ||||||
|       <style> |       <style> | ||||||
|         a { |         a { | ||||||
| @@ -139,7 +145,7 @@ | |||||||
|         </p> |         </p> | ||||||
|       </div> |       </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"> |       <div class="card-content"> | ||||||
|         <p> |         <p> | ||||||
|           Home Assistant Cast is a receiver application for the Chromecast. When |           Home Assistant Cast is a receiver application for the Chromecast. When | ||||||
| @@ -226,5 +232,17 @@ http: | |||||||
|         </p> |         </p> | ||||||
|       </div> |       </div> | ||||||
|     </hc-layout> |     </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> |   </body> | ||||||
| </html> | </html> | ||||||
|   | |||||||
| @@ -13,9 +13,15 @@ | |||||||
|     <%= renderTemplate("_social_meta.html.template") %> |     <%= renderTemplate("_social_meta.html.template") %> | ||||||
|   </head> |   </head> | ||||||
|   <body> |   <body> | ||||||
|     <hc-connect></hc-connect> |  | ||||||
|     <%= renderTemplate("../../../src/html/_js_base.html.template") %> |     <%= 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> |     <script> | ||||||
|     (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ |     (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), |     (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; |         --background-color: #41bdf5; | ||||||
|       } |       } | ||||||
|     </style> |     </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> |   </head> | ||||||
|   <body> |   <body> | ||||||
|     <cast-media-player></cast-media-player> |  | ||||||
|     <%= renderTemplate("../../../src/html/_js_base.html.template") %> |     <%= 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> |   </body> | ||||||
| </html> | </html> | ||||||
|   | |||||||
| @@ -7,7 +7,14 @@ | |||||||
|   <%= renderTemplate("../../../src/html/_style_base.html.template") %> |   <%= renderTemplate("../../../src/html/_style_base.html.template") %> | ||||||
|   <style> |   <style> | ||||||
|     body { |     body { | ||||||
|  |       background-color: white; | ||||||
|       font-size: initial; |       font-size: initial; | ||||||
|     } |     } | ||||||
|   </style> |   </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> | </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 "./layout/hc-connect"; | ||||||
|  |  | ||||||
| import("../../../src/resources/append-ha-style"); |  | ||||||
|   | |||||||
| @@ -1,10 +1,11 @@ | |||||||
| import type { ActionDetail } from "@material/mwc-list/mwc-list"; | import "@material/mwc-button/mwc-button"; | ||||||
| import { mdiCast, mdiCastConnected, mdiViewDashboard } from "@mdi/js"; | import { mdiCast, mdiCastConnected } from "@mdi/js"; | ||||||
| import type { Auth, Connection } from "home-assistant-js-websocket"; | import "@polymer/paper-item/paper-icon-item"; | ||||||
| import type { TemplateResult } from "lit"; | import "@polymer/paper-listbox/paper-listbox"; | ||||||
| import { LitElement, css, html } from "lit"; | import { Auth, Connection } from "home-assistant-js-websocket"; | ||||||
|  | import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||||
| import { customElement, property, state } from "lit/decorators"; | import { customElement, property, state } from "lit/decorators"; | ||||||
| import type { CastManager } from "../../../../src/cast/cast_manager"; | import { CastManager } from "../../../../src/cast/cast_manager"; | ||||||
| import { | import { | ||||||
|   castSendShowLovelaceView, |   castSendShowLovelaceView, | ||||||
|   ensureConnectedCastSession, |   ensureConnectedCastSession, | ||||||
| @@ -17,34 +18,30 @@ import { | |||||||
| import { atLeastVersion } from "../../../../src/common/config/version"; | import { atLeastVersion } from "../../../../src/common/config/version"; | ||||||
| import { toggleAttribute } from "../../../../src/common/dom/toggle_attribute"; | import { toggleAttribute } from "../../../../src/common/dom/toggle_attribute"; | ||||||
| import "../../../../src/components/ha-icon"; | 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 "../../../../src/components/ha-svg-icon"; | ||||||
| import { | import { | ||||||
|   getLegacyLovelaceCollection, |   getLegacyLovelaceCollection, | ||||||
|   getLovelaceCollection, |   getLovelaceCollection, | ||||||
|  |   LovelaceConfig, | ||||||
| } from "../../../../src/data/lovelace"; | } 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 "../../../../src/layouts/hass-loading-screen"; | ||||||
| import { generateDefaultViewConfig } from "../../../../src/panels/lovelace/common/generate-lovelace-config"; | import { generateDefaultViewConfig } from "../../../../src/panels/lovelace/common/generate-lovelace-config"; | ||||||
| import "./hc-layout"; | import "./hc-layout"; | ||||||
|  |  | ||||||
| @customElement("hc-cast") | @customElement("hc-cast") | ||||||
| class HcCast extends LitElement { | 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 askWrite = false; | ||||||
|  |  | ||||||
|   @state() private lovelaceViews?: LovelaceViewConfig[] | null; |   @state() private lovelaceConfig?: LovelaceConfig | null; | ||||||
|  |  | ||||||
|   protected render(): TemplateResult { |   protected render(): TemplateResult { | ||||||
|     if (this.lovelaceViews === undefined) { |     if (this.lovelaceConfig === undefined) { | ||||||
|       return html`<hass-loading-screen no-toolbar></hass-loading-screen>`; |       return html`<hass-loading-screen no-toolbar></hass-loading-screen>`; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -62,20 +59,12 @@ class HcCast extends LitElement { | |||||||
|               <p class="question action-item"> |               <p class="question action-item"> | ||||||
|                 Stay logged in? |                 Stay logged in? | ||||||
|                 <span> |                 <span> | ||||||
|                   <ha-button |                   <mwc-button @click=${this._handleSaveTokens}> | ||||||
|                     appearance="plain" |  | ||||||
|                     size="small" |  | ||||||
|                     @click=${this._handleSaveTokens} |  | ||||||
|                   > |  | ||||||
|                     YES |                     YES | ||||||
|                   </ha-button> |                   </mwc-button> | ||||||
|                   <ha-button |                   <mwc-button @click=${this._handleSkipSaveTokens}> | ||||||
|                     appearance="plain" |  | ||||||
|                     size="small" |  | ||||||
|                     @click=${this._handleSkipSaveTokens} |  | ||||||
|                   > |  | ||||||
|                     NO |                     NO | ||||||
|                   </ha-button> |                   </mwc-button> | ||||||
|                 </span> |                 </span> | ||||||
|               </p> |               </p> | ||||||
|             ` |             ` | ||||||
| @@ -85,65 +74,52 @@ class HcCast extends LitElement { | |||||||
|           : !this.castManager.status |           : !this.castManager.status | ||||||
|           ? html` |           ? html` | ||||||
|               <p class="center-item"> |               <p class="center-item"> | ||||||
|                   <ha-button @click=${this._handleLaunch}> |                 <mwc-button raised @click=${this._handleLaunch}> | ||||||
|                     <ha-svg-icon slot="start" .path=${mdiCast}></ha-svg-icon> |                   <ha-svg-icon .path=${mdiCast}></ha-svg-icon> | ||||||
|                   Start Casting |                   Start Casting | ||||||
|                   </ha-button> |                 </mwc-button> | ||||||
|               </p> |               </p> | ||||||
|             ` |             ` | ||||||
|           : html` |           : html` | ||||||
|               <div class="section-header">PICK A VIEW</div> |               <div class="section-header">PICK A VIEW</div> | ||||||
|                 <ha-list @action=${this._handlePickView} activatable> |               <paper-listbox | ||||||
|                   ${( |                 attr-for-selected="data-path" | ||||||
|                     this.lovelaceViews ?? [ |                 .selected=${this.castManager.status.lovelacePath || ""} | ||||||
|                       generateDefaultViewConfig({}, {}, {}, {}, () => ""), |               > | ||||||
|                     ] |                 ${(this.lovelaceConfig | ||||||
|  |                   ? this.lovelaceConfig.views | ||||||
|  |                   : [generateDefaultViewConfig({}, {}, {}, {}, () => "")] | ||||||
|                 ).map( |                 ).map( | ||||||
|                   (view, idx) => html` |                   (view, idx) => html` | ||||||
|                       <ha-list-item |                     <paper-icon-item | ||||||
|                         graphic="avatar" |                       @click=${this._handlePickView} | ||||||
|                         .activated=${this.castManager.status?.lovelacePath === |                       data-path=${view.path || idx} | ||||||
|                         (view.path ?? idx)} |  | ||||||
|                         .selected=${this.castManager.status?.lovelacePath === |  | ||||||
|                         (view.path ?? idx)} |  | ||||||
|                     > |                     > | ||||||
|                         ${view.title || view.path || "Unnamed view"} |  | ||||||
|                       ${view.icon |                       ${view.icon | ||||||
|                         ? html` |                         ? html` | ||||||
|                             <ha-icon |                             <ha-icon | ||||||
|                               .icon=${view.icon} |                               .icon=${view.icon} | ||||||
|                                 slot="graphic" |                               slot="item-icon" | ||||||
|                             ></ha-icon> |                             ></ha-icon> | ||||||
|                           ` |                           ` | ||||||
|                           : html`<ha-svg-icon |                         : ""} | ||||||
|                               slot="item-icon" |                       ${view.title || view.path} | ||||||
|                               .path=${mdiViewDashboard} |                     </paper-icon-item> | ||||||
|                             ></ha-svg-icon>`} |  | ||||||
|                       </ha-list-item> |  | ||||||
|                   ` |                   ` | ||||||
|                   )}</ha-list |                 )} | ||||||
|                 > |               </paper-listbox> | ||||||
|             `} |             `} | ||||||
|  |  | ||||||
|         <div class="card-actions"> |         <div class="card-actions"> | ||||||
|           ${this.castManager.status |           ${this.castManager.status | ||||||
|             ? html` |             ? html` | ||||||
|                 <ha-button appearance="plain" @click=${this._handleLaunch}> |                 <mwc-button @click=${this._handleLaunch}> | ||||||
|                   <ha-svg-icon |                   <ha-svg-icon .path=${mdiCastConnected}></ha-svg-icon> | ||||||
|                     slot="start" |  | ||||||
|                     .path=${mdiCastConnected} |  | ||||||
|                   ></ha-svg-icon> |  | ||||||
|                   Manage |                   Manage | ||||||
|                 </ha-button> |                 </mwc-button> | ||||||
|               ` |               ` | ||||||
|             : ""} |             : ""} | ||||||
|           <div class="spacer"></div> |           <div class="spacer"></div> | ||||||
|           <ha-button |           <mwc-button @click=${this._handleLogout}>Log out</mwc-button> | ||||||
|             variant="danger" |  | ||||||
|             appearance="plain" |  | ||||||
|             @click=${this._handleLogout} |  | ||||||
|             >Log out</ha-button |  | ||||||
|           > |  | ||||||
|         </div> |         </div> | ||||||
|       </hc-layout> |       </hc-layout> | ||||||
|     `; |     `; | ||||||
| @@ -160,15 +136,11 @@ class HcCast extends LitElement { | |||||||
|     llColl.refresh().then( |     llColl.refresh().then( | ||||||
|       () => { |       () => { | ||||||
|         llColl.subscribe((config) => { |         llColl.subscribe((config) => { | ||||||
|           if (isStrategyDashboard(config)) { |           this.lovelaceConfig = config; | ||||||
|             this.lovelaceViews = null; |  | ||||||
|           } else { |  | ||||||
|             this.lovelaceViews = config.views; |  | ||||||
|           } |  | ||||||
|         }); |         }); | ||||||
|       }, |       }, | ||||||
|       async () => { |       async () => { | ||||||
|         this.lovelaceViews = null; |         this.lovelaceConfig = null; | ||||||
|       } |       } | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
| @@ -187,7 +159,9 @@ class HcCast extends LitElement { | |||||||
|     toggleAttribute( |     toggleAttribute( | ||||||
|       this, |       this, | ||||||
|       "hide-icons", |       "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(); |     this.castManager.requestSession(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private async _handlePickView(ev: CustomEvent<ActionDetail>) { |   private async _handlePickView(ev: Event) { | ||||||
|     const path = this.lovelaceViews![ev.detail.index].path ?? ev.detail.index; |     const path = (ev.currentTarget as any).getAttribute("data-path"); | ||||||
|     await ensureConnectedCastSession(this.castManager!, this.auth!); |     await ensureConnectedCastSession(this.castManager!, this.auth!); | ||||||
|     castSendShowLovelaceView(this.castManager, this.auth.data.hassUrl, path); |     castSendShowLovelaceView(this.castManager, this.auth.data.hassUrl, path); | ||||||
|   } |   } | ||||||
| @@ -219,12 +193,13 @@ class HcCast extends LitElement { | |||||||
|       } |       } | ||||||
|       this.connection.close(); |       this.connection.close(); | ||||||
|       location.reload(); |       location.reload(); | ||||||
|     } catch (_err: any) { |     } catch (err: any) { | ||||||
|       alert("Unable to log out!"); |       alert("Unable to log out!"); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   static styles = css` |   static get styles(): CSSResultGroup { | ||||||
|  |     return css` | ||||||
|       .center-item { |       .center-item { | ||||||
|         display: flex; |         display: flex; | ||||||
|         justify-content: space-around; |         justify-content: space-around; | ||||||
| @@ -242,7 +217,7 @@ class HcCast extends LitElement { | |||||||
|       } |       } | ||||||
|  |  | ||||||
|       .question:before { |       .question:before { | ||||||
|       border-radius: var(--ha-border-radius-sm); |         border-radius: 4px; | ||||||
|         position: absolute; |         position: absolute; | ||||||
|         top: 0; |         top: 0; | ||||||
|         right: 0; |         right: 0; | ||||||
| @@ -260,14 +235,30 @@ class HcCast extends LitElement { | |||||||
|         color: var(--secondary-text-color); |         color: var(--secondary-text-color); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|     ha-list-item ha-icon, |       mwc-button ha-svg-icon { | ||||||
|     ha-list-item ha-svg-icon { |         margin-right: 8px; | ||||||
|  |         height: 18px; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       paper-listbox { | ||||||
|  |         padding-top: 0; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       paper-listbox ha-icon { | ||||||
|         padding: 12px; |         padding: 12px; | ||||||
|         color: var(--secondary-text-color); |         color: var(--secondary-text-color); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|     :host([hide-icons]) ha-icon { |       paper-icon-item { | ||||||
|       display: none; |         cursor: pointer; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       paper-icon-item[disabled] { | ||||||
|  |         cursor: initial; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       :host([hide-icons]) paper-icon-item { | ||||||
|  |         --paper-item-icon-width: 0px; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .spacer { |       .spacer { | ||||||
| @@ -278,6 +269,7 @@ class HcCast extends LitElement { | |||||||
|         color: var(--primary-color); |         color: var(--primary-color); | ||||||
|       } |       } | ||||||
|     `; |     `; | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| declare global { | declare global { | ||||||
|   | |||||||
| @@ -1,22 +1,20 @@ | |||||||
|  | import "@material/mwc-button"; | ||||||
| import { mdiCastConnected, mdiCast } from "@mdi/js"; | import { mdiCastConnected, mdiCast } from "@mdi/js"; | ||||||
| import type { | import "@polymer/paper-input/paper-input"; | ||||||
|  | import { | ||||||
|   Auth, |   Auth, | ||||||
|   Connection, |   Connection, | ||||||
|   getAuthOptions, |  | ||||||
| } from "home-assistant-js-websocket"; |  | ||||||
| import { |  | ||||||
|   createConnection, |   createConnection, | ||||||
|   ERR_CANNOT_CONNECT, |   ERR_CANNOT_CONNECT, | ||||||
|   ERR_HASS_HOST_REQUIRED, |   ERR_HASS_HOST_REQUIRED, | ||||||
|   ERR_INVALID_AUTH, |   ERR_INVALID_AUTH, | ||||||
|   ERR_INVALID_HTTPS_TO_HTTP, |   ERR_INVALID_HTTPS_TO_HTTP, | ||||||
|   getAuth, |   getAuth, | ||||||
|  |   getAuthOptions, | ||||||
| } from "home-assistant-js-websocket"; | } from "home-assistant-js-websocket"; | ||||||
| import type { TemplateResult } from "lit"; | import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||||
| import { css, html, LitElement } from "lit"; |  | ||||||
| import { customElement, state } from "lit/decorators"; | import { customElement, state } from "lit/decorators"; | ||||||
| import type { CastManager } from "../../../../src/cast/cast_manager"; | import { CastManager, getCastManager } from "../../../../src/cast/cast_manager"; | ||||||
| import { getCastManager } from "../../../../src/cast/cast_manager"; |  | ||||||
| import { castSendShowDemo } from "../../../../src/cast/receiver_messages"; | import { castSendShowDemo } from "../../../../src/cast/receiver_messages"; | ||||||
| import { | import { | ||||||
|   loadTokens, |   loadTokens, | ||||||
| @@ -26,8 +24,6 @@ import "../../../../src/components/ha-svg-icon"; | |||||||
| import "../../../../src/layouts/hass-loading-screen"; | import "../../../../src/layouts/hass-loading-screen"; | ||||||
| import { registerServiceWorker } from "../../../../src/util/register-service-worker"; | import { registerServiceWorker } from "../../../../src/util/register-service-worker"; | ||||||
| import "./hc-layout"; | import "./hc-layout"; | ||||||
| import "../../../../src/components/ha-textfield"; |  | ||||||
| import "../../../../src/components/ha-button"; |  | ||||||
|  |  | ||||||
| const seeFAQ = (qid) => html` | const seeFAQ = (qid) => html` | ||||||
|   See <a href="./faq.html${qid ? `#${qid}` : ""}">the FAQ</a> for more |   See <a href="./faq.html${qid ? `#${qid}` : ""}">the FAQ</a> for more | ||||||
| @@ -83,14 +79,11 @@ export class HcConnect extends LitElement { | |||||||
|             Unable to connect to ${tokens!.hassUrl}. |             Unable to connect to ${tokens!.hassUrl}. | ||||||
|           </div> |           </div> | ||||||
|           <div class="card-actions"> |           <div class="card-actions"> | ||||||
|             <ha-button appearance="plain" href="/">Retry</ha-button> |             <a href="/"> | ||||||
|  |               <mwc-button> Retry </mwc-button> | ||||||
|  |             </a> | ||||||
|             <div class="spacer"></div> |             <div class="spacer"></div> | ||||||
|             <ha-button |             <mwc-button @click=${this._handleLogout}>Log out</mwc-button> | ||||||
|               appearance="plain" |  | ||||||
|               variant="danger" |  | ||||||
|               @click=${this._handleLogout} |  | ||||||
|               >Log out</ha-button |  | ||||||
|             > |  | ||||||
|           </div> |           </div> | ||||||
|         </hc-layout> |         </hc-layout> | ||||||
|       `; |       `; | ||||||
| @@ -123,27 +116,26 @@ export class HcConnect extends LitElement { | |||||||
|               To get started, enter your Home Assistant URL and click authorize. |               To get started, enter your Home Assistant URL and click authorize. | ||||||
|               If you want a preview instead, click the show demo button. |               If you want a preview instead, click the show demo button. | ||||||
|             </p> |             </p> | ||||||
|             <ha-textfield |             <p> | ||||||
|  |               <paper-input | ||||||
|                 label="Home Assistant URL" |                 label="Home Assistant URL" | ||||||
|                 placeholder="https://abcdefghijklmnop.ui.nabu.casa" |                 placeholder="https://abcdefghijklmnop.ui.nabu.casa" | ||||||
|                 @keydown=${this._handleInputKeyDown} |                 @keydown=${this._handleInputKeyDown} | ||||||
|             ></ha-textfield> |               ></paper-input> | ||||||
|  |             </p> | ||||||
|             ${this.error ? html` <p class="error">${this.error}</p> ` : ""} |             ${this.error ? html` <p class="error">${this.error}</p> ` : ""} | ||||||
|           </div> |           </div> | ||||||
|           <div class="card-actions"> |           <div class="card-actions"> | ||||||
|             <ha-button appearance="plain" @click=${this._handleDemo}> |             <mwc-button @click=${this._handleDemo}> | ||||||
|               Show Demo |               Show Demo | ||||||
|               <ha-svg-icon |               <ha-svg-icon | ||||||
|                 slot="end" |  | ||||||
|                 .path=${this.castManager.castState === "CONNECTED" |                 .path=${this.castManager.castState === "CONNECTED" | ||||||
|                   ? mdiCastConnected |                   ? mdiCastConnected | ||||||
|                   : mdiCast} |                   : mdiCast} | ||||||
|               ></ha-svg-icon> |               ></ha-svg-icon> | ||||||
|             </ha-button> |             </mwc-button> | ||||||
|             <div class="spacer"></div> |             <div class="spacer"></div> | ||||||
|             <ha-button appearance="plain" @click=${this._handleConnect} |             <mwc-button @click=${this._handleConnect}>Authorize</mwc-button> | ||||||
|               >Authorize</ha-button |  | ||||||
|             > |  | ||||||
|           </div> |           </div> | ||||||
|         </hc-layout> |         </hc-layout> | ||||||
|       `; |       `; | ||||||
| @@ -204,7 +196,7 @@ export class HcConnect extends LitElement { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   private async _handleConnect() { |   private async _handleConnect() { | ||||||
|     const inputEl = this.shadowRoot!.querySelector("ha-textfield")!; |     const inputEl = this.shadowRoot!.querySelector("paper-input")!; | ||||||
|     const value = inputEl.value || ""; |     const value = inputEl.value || ""; | ||||||
|     this.error = undefined; |     this.error = undefined; | ||||||
|  |  | ||||||
| @@ -221,7 +213,7 @@ export class HcConnect extends LitElement { | |||||||
|     let url: URL; |     let url: URL; | ||||||
|     try { |     try { | ||||||
|       url = new URL(value); |       url = new URL(value); | ||||||
|     } catch (_err: any) { |     } catch (err: any) { | ||||||
|       this.error = "Invalid URL"; |       this.error = "Invalid URL"; | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
| @@ -258,7 +250,7 @@ export class HcConnect extends LitElement { | |||||||
|       this.loading = false; |       this.loading = false; | ||||||
|       return; |       return; | ||||||
|     } finally { |     } 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")) { |       if (location.search.includes("auth_callback=1")) { | ||||||
|         history.replaceState(null, "", location.pathname); |         history.replaceState(null, "", location.pathname); | ||||||
|       } |       } | ||||||
| @@ -294,12 +286,13 @@ export class HcConnect extends LitElement { | |||||||
|     try { |     try { | ||||||
|       saveTokens(null); |       saveTokens(null); | ||||||
|       location.reload(); |       location.reload(); | ||||||
|     } catch (_err: any) { |     } catch (err: any) { | ||||||
|       alert("Unable to log out!"); |       alert("Unable to log out!"); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   static styles = css` |   static get styles(): CSSResultGroup { | ||||||
|  |     return css` | ||||||
|       .card-content a { |       .card-content a { | ||||||
|         color: var(--primary-color); |         color: var(--primary-color); | ||||||
|       } |       } | ||||||
| @@ -308,21 +301,22 @@ export class HcConnect extends LitElement { | |||||||
|       } |       } | ||||||
|       .error { |       .error { | ||||||
|         color: red; |         color: red; | ||||||
|       font-weight: var(--ha-font-weight-bold); |         font-weight: bold; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .error a { |       .error a { | ||||||
|         color: darkred; |         color: darkred; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|  |       mwc-button ha-svg-icon { | ||||||
|  |         margin-left: 8px; | ||||||
|  |       } | ||||||
|  |  | ||||||
|       .spacer { |       .spacer { | ||||||
|         flex: 1; |         flex: 1; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|     ha-textfield { |  | ||||||
|       width: 100%; |  | ||||||
|     } |  | ||||||
|     `; |     `; | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| declare global { | declare global { | ||||||
|   | |||||||
| @@ -1,19 +1,22 @@ | |||||||
| import type { Auth, Connection, HassUser } from "home-assistant-js-websocket"; | import { | ||||||
| import { getUser } from "home-assistant-js-websocket"; |   Auth, | ||||||
| import type { TemplateResult } from "lit"; |   Connection, | ||||||
| import { css, html, LitElement } from "lit"; |   getUser, | ||||||
|  |   HassUser, | ||||||
|  | } from "home-assistant-js-websocket"; | ||||||
|  | import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||||
| import { customElement, property } from "lit/decorators"; | import { customElement, property } from "lit/decorators"; | ||||||
| import "../../../../src/components/ha-card"; | import "../../../../src/components/ha-card"; | ||||||
|  |  | ||||||
| @customElement("hc-layout") | @customElement("hc-layout") | ||||||
| class HcLayout extends LitElement { | 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 { |   protected render(): TemplateResult { | ||||||
|     return html` |     return html` | ||||||
| @@ -63,7 +66,8 @@ class HcLayout extends LitElement { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   static styles = css` |   static get styles(): CSSResultGroup { | ||||||
|  |     return css` | ||||||
|       :host { |       :host { | ||||||
|         display: flex; |         display: flex; | ||||||
|         min-height: 100%; |         min-height: 100%; | ||||||
| @@ -84,22 +88,21 @@ class HcLayout extends LitElement { | |||||||
|       } |       } | ||||||
|  |  | ||||||
|       .card-header { |       .card-header { | ||||||
|       color: var(--ha-card-header-color, var(--primary-text-color)); |         color: var(--ha-card-header-color, --primary-text-color); | ||||||
|         font-family: var(--ha-card-header-font-family, inherit); |         font-family: var(--ha-card-header-font-family, inherit); | ||||||
|       font-size: var(--ha-card-header-font-size, var(--ha-font-size-2xl)); |         font-size: var(--ha-card-header-font-size, 24px); | ||||||
|         letter-spacing: -0.012em; |         letter-spacing: -0.012em; | ||||||
|       line-height: var(--ha-line-height-condensed); |         line-height: 32px; | ||||||
|         padding: 24px 16px 16px; |         padding: 24px 16px 16px; | ||||||
|         display: block; |         display: block; | ||||||
|         margin: 0; |         margin: 0; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .hero { |       .hero { | ||||||
|       border-radius: var(--ha-border-radius-sm) var(--ha-border-radius-sm) |         border-radius: 4px 4px 0 0; | ||||||
|         var(--ha-border-radius-square) var(--ha-border-radius-square); |  | ||||||
|       } |       } | ||||||
|       .subtitle { |       .subtitle { | ||||||
|       font-size: var(--ha-font-size-m); |         font-size: 14px; | ||||||
|         color: var(--secondary-text-color); |         color: var(--secondary-text-color); | ||||||
|         line-height: initial; |         line-height: initial; | ||||||
|       } |       } | ||||||
| @@ -114,7 +117,7 @@ class HcLayout extends LitElement { | |||||||
|       } |       } | ||||||
|  |  | ||||||
|       :host ::slotted(.section-header) { |       :host ::slotted(.section-header) { | ||||||
|       font-weight: var(--ha-font-weight-medium); |         font-weight: 500; | ||||||
|         padding: 4px 16px; |         padding: 4px 16px; | ||||||
|         text-transform: uppercase; |         text-transform: uppercase; | ||||||
|       } |       } | ||||||
| @@ -136,7 +139,7 @@ class HcLayout extends LitElement { | |||||||
|  |  | ||||||
|       .footer { |       .footer { | ||||||
|         text-align: center; |         text-align: center; | ||||||
|       font-size: var(--ha-font-size-s); |         font-size: 12px; | ||||||
|         padding: 8px 0 24px; |         padding: 8px 0 24px; | ||||||
|         color: var(--secondary-text-color); |         color: var(--secondary-text-color); | ||||||
|       } |       } | ||||||
| @@ -152,6 +155,7 @@ class HcLayout extends LitElement { | |||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     `; |     `; | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| declare global { | declare global { | ||||||
|   | |||||||
| @@ -1,5 +1,4 @@ | |||||||
| import type { Entity } from "../../../../src/fake_data/entity"; | import { convertEntities, Entity } from "../../../../src/fake_data/entity"; | ||||||
| import { convertEntities } from "../../../../src/fake_data/entity"; |  | ||||||
|  |  | ||||||
| export const castDemoEntities: () => Entity[] = () => | export const castDemoEntities: () => Entity[] = () => | ||||||
|   convertEntities({ |   convertEntities({ | ||||||
| @@ -75,7 +74,7 @@ export const castDemoEntities: () => Entity[] = () => | |||||||
|         longitude: 4.8903147, |         longitude: 4.8903147, | ||||||
|         radius: 100, |         radius: 100, | ||||||
|         friendly_name: "Home", |         friendly_name: "Home", | ||||||
|         icon: "mdi:home", |         icon: "hass:home", | ||||||
|       }, |       }, | ||||||
|     }, |     }, | ||||||
|     "input_number.harmonyvolume": { |     "input_number.harmonyvolume": { | ||||||
| @@ -88,7 +87,7 @@ export const castDemoEntities: () => Entity[] = () => | |||||||
|         step: 1, |         step: 1, | ||||||
|         mode: "slider", |         mode: "slider", | ||||||
|         friendly_name: "Volume", |         friendly_name: "Volume", | ||||||
|         icon: "mdi:volume-high", |         icon: "hass:volume-high", | ||||||
|       }, |       }, | ||||||
|     }, |     }, | ||||||
|     "climate.upstairs": { |     "climate.upstairs": { | ||||||
|   | |||||||
| @@ -1,5 +1,7 @@ | |||||||
| import type { LovelaceCardConfig } from "../../../../src/data/lovelace/config/card"; | import { | ||||||
| import type { LovelaceConfig } from "../../../../src/data/lovelace/config/types"; |   LovelaceCardConfig, | ||||||
|  |   LovelaceConfig, | ||||||
|  | } from "../../../../src/data/lovelace"; | ||||||
| import { castContext } from "../cast_context"; | import { castContext } from "../cast_context"; | ||||||
|  |  | ||||||
| export const castDemoLovelace: () => LovelaceConfig = () => { | export const castDemoLovelace: () => LovelaceConfig = () => { | ||||||
| @@ -56,7 +58,7 @@ export const castDemoLovelace: () => LovelaceConfig = () => { | |||||||
|                 type: "weblink", |                 type: "weblink", | ||||||
|                 url: "/lovelace/climate", |                 url: "/lovelace/climate", | ||||||
|                 name: "Climate controls", |                 name: "Climate controls", | ||||||
|                 icon: "mdi:arrow-right", |                 icon: "hass:arrow-right", | ||||||
|               }, |               }, | ||||||
|             ], |             ], | ||||||
|           }, |           }, | ||||||
| @@ -76,7 +78,7 @@ export const castDemoLovelace: () => LovelaceConfig = () => { | |||||||
|                 type: "weblink", |                 type: "weblink", | ||||||
|                 url: "/lovelace/overview", |                 url: "/lovelace/overview", | ||||||
|                 name: "Back", |                 name: "Back", | ||||||
|                 icon: "mdi:arrow-left", |                 icon: "hass:arrow-left", | ||||||
|               }, |               }, | ||||||
|             ], |             ], | ||||||
|           }, |           }, | ||||||
|   | |||||||
| @@ -1,10 +1,10 @@ | |||||||
| import { framework } from "./cast_framework"; | import { framework } from "./cast_framework"; | ||||||
| import { CAST_NS } from "../../../src/cast/const"; | 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 "../../../src/resources/custom-card-support"; | ||||||
| import { castContext } from "./cast_context"; | import { castContext } from "./cast_context"; | ||||||
| import { HcMain } from "./layout/hc-main"; | import { HcMain } from "./layout/hc-main"; | ||||||
| import type { ReceivedMessage } from "./types"; | import { ReceivedMessage } from "./types"; | ||||||
|  |  | ||||||
| const lovelaceController = new HcMain(); | const lovelaceController = new HcMain(); | ||||||
| document.body.append(lovelaceController); | document.body.append(lovelaceController); | ||||||
|   | |||||||
| @@ -1,11 +1,13 @@ | |||||||
| import { html, nothing } from "lit"; | import { html, nothing } from "lit"; | ||||||
| import { customElement, property, state } from "lit/decorators"; | import { customElement, property, state } from "lit/decorators"; | ||||||
| import { mockHistory } from "../../../../demo/src/stubs/history"; | import { mockHistory } from "../../../../demo/src/stubs/history"; | ||||||
| import type { LovelaceConfig } from "../../../../src/data/lovelace/config/types"; | import { LovelaceConfig } from "../../../../src/data/lovelace"; | ||||||
| import type { MockHomeAssistant } from "../../../../src/fake_data/provide_hass"; | import { | ||||||
| import { provideHass } from "../../../../src/fake_data/provide_hass"; |   MockHomeAssistant, | ||||||
|  |   provideHass, | ||||||
|  | } from "../../../../src/fake_data/provide_hass"; | ||||||
| import { HassElement } from "../../../../src/state/hass-element"; | 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 { castDemoEntities } from "../demo/cast-demo-entities"; | ||||||
| import { castDemoLovelace } from "../demo/cast-demo-lovelace"; | import { castDemoLovelace } from "../demo/cast-demo-lovelace"; | ||||||
| import "./hc-lovelace"; | import "./hc-lovelace"; | ||||||
|   | |||||||
| @@ -1,7 +1,6 @@ | |||||||
| import type { TemplateResult } from "lit"; | import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||||
| import { css, html, LitElement } from "lit"; |  | ||||||
| import { customElement, property } from "lit/decorators"; | import { customElement, property } from "lit/decorators"; | ||||||
| import type { HomeAssistant } from "../../../../src/types"; | import { HomeAssistant } from "../../../../src/types"; | ||||||
|  |  | ||||||
| @customElement("hc-launch-screen") | @customElement("hc-launch-screen") | ||||||
| class HcLaunchScreen extends LitElement { | class HcLaunchScreen extends LitElement { | ||||||
| @@ -13,8 +12,8 @@ class HcLaunchScreen extends LitElement { | |||||||
|     return html` |     return html` | ||||||
|       <div class="container"> |       <div class="container"> | ||||||
|         <img |         <img | ||||||
|           alt="Nabu Casa logo on left, Home Assistant logo on right, and red heart in center" |           alt="Home Assistant logo on left, Nabu Casa logo on right, and red heart in center" | ||||||
|           src="https://cast.home-assistant.io/images/nabu-loves-hass.png" |           src="https://www.home-assistant.io/images/blog/2018-09-thinking-big/social.png" | ||||||
|         /> |         /> | ||||||
|         <div class="status"> |         <div class="status"> | ||||||
|           ${this.hass ? "Connected" : "Not Connected"} |           ${this.hass ? "Connected" : "Not Connected"} | ||||||
| @@ -24,29 +23,31 @@ class HcLaunchScreen extends LitElement { | |||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   static styles = css` |   static get styles(): CSSResultGroup { | ||||||
|  |     return css` | ||||||
|       :host { |       :host { | ||||||
|         display: block; |         display: block; | ||||||
|         height: 100vh; |         height: 100vh; | ||||||
|       background-color: #f2f4f9; |         padding-top: 64px; | ||||||
|       font-size: var(--ha-font-size-2xl); |         background-color: white; | ||||||
|  |         font-size: 24px; | ||||||
|       } |       } | ||||||
|       .container { |       .container { | ||||||
|         display: flex; |         display: flex; | ||||||
|         flex-direction: column; |         flex-direction: column; | ||||||
|         text-align: center; |         text-align: center; | ||||||
|       align-items: center; |  | ||||||
|       height: 100%; |  | ||||||
|       justify-content: space-evenly; |  | ||||||
|       } |       } | ||||||
|       img { |       img { | ||||||
|       max-width: 80%; |         width: 717px; | ||||||
|       object-fit: cover; |         height: 376px; | ||||||
|  |         display: block; | ||||||
|  |         margin: 0 auto; | ||||||
|       } |       } | ||||||
|       .status { |       .status { | ||||||
|       color: #1d2126; |         padding-right: 54px; | ||||||
|       } |       } | ||||||
|     `; |     `; | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| declare global { | declare global { | ||||||
|   | |||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user