mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-27 03:06:41 +00:00
Compare commits
No commits in common. "dev" and "20221102.1" have entirely different histories.
dev
...
20221102.1
@ -1,36 +0,0 @@
|
|||||||
[modern]
|
|
||||||
# Modern builds target recent browsers supporting the latest features to minimize transpilation, polyfills, etc.
|
|
||||||
# It is served to browsers meeting the following requirements:
|
|
||||||
# - released in the last year + current alpha/beta versions
|
|
||||||
# - Firefox extended support release (ESR)
|
|
||||||
# - with global utilization at or above 0.5%
|
|
||||||
# - exclude dead browsers (no security maintenance for 2+ years)
|
|
||||||
# - exclude KaiOS, QQ, and UC browsers due to lack of sufficient feature support data
|
|
||||||
unreleased versions
|
|
||||||
last 1 year
|
|
||||||
Firefox ESR
|
|
||||||
>= 0.5%
|
|
||||||
not dead
|
|
||||||
not KaiOS > 0
|
|
||||||
not QQAndroid > 0
|
|
||||||
not UCAndroid > 0
|
|
||||||
|
|
||||||
[legacy]
|
|
||||||
# Legacy builds are served when modern requirements are not met and support browsers:
|
|
||||||
# - released in the last 7 years + current alpha/beta versionss
|
|
||||||
# - with global utilization at or above 0.05%
|
|
||||||
# - exclude dead browsers (no security maintenance for 2+ years)
|
|
||||||
# - exclude Opera Mini which does not support web sockets
|
|
||||||
unreleased versions
|
|
||||||
last 7 years
|
|
||||||
>= 0.05%
|
|
||||||
not dead
|
|
||||||
not op_mini all
|
|
||||||
|
|
||||||
[legacy-sw]
|
|
||||||
# Same as legacy plus supports service workers
|
|
||||||
unreleased versions
|
|
||||||
last 7 years
|
|
||||||
>= 0.05% and supports serviceworkers
|
|
||||||
not dead
|
|
||||||
not op_mini all
|
|
@ -1,6 +1,13 @@
|
|||||||
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.9
|
||||||
|
|
||||||
ENV \
|
ENV \
|
||||||
DEBIAN_FRONTEND=noninteractive \
|
DEBIAN_FRONTEND=noninteractive \
|
||||||
DEVCONTAINER=true \
|
DEVCONTAINER=true \
|
||||||
PATH=$PATH:./node_modules/.bin
|
PATH=$PATH:./node_modules/.bin
|
||||||
|
|
||||||
|
# Install nvm
|
||||||
|
COPY .nvmrc /tmp/.nvmrc
|
||||||
|
RUN \
|
||||||
|
su vscode -c \
|
||||||
|
"source /usr/local/share/nvm/nvm.sh && nvm install $(cat /tmp/.nvmrc) 2>&1"
|
@ -5,45 +5,33 @@
|
|||||||
"context": ".."
|
"context": ".."
|
||||||
},
|
},
|
||||||
"appPort": "8124:8123",
|
"appPort": "8124:8123",
|
||||||
"postCreateCommand": "./.devcontainer/post_create.sh",
|
"context": "..",
|
||||||
"postStartCommand": "script/bootstrap",
|
"postCreateCommand": "script/bootstrap",
|
||||||
|
"extensions": [
|
||||||
|
"github.vscode-pull-request-github",
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"ms-vscode.vscode-typescript-tslint-plugin",
|
||||||
|
"esbenp.prettier-vscode",
|
||||||
|
"bierner.lit-html",
|
||||||
|
"runem.lit-plugin",
|
||||||
|
"ms-python.vscode-pylance"
|
||||||
|
],
|
||||||
"containerEnv": {
|
"containerEnv": {
|
||||||
"DEV_CONTAINER": "1",
|
|
||||||
"WORKSPACE_DIRECTORY": "${containerWorkspaceFolder}"
|
"WORKSPACE_DIRECTORY": "${containerWorkspaceFolder}"
|
||||||
},
|
},
|
||||||
"remoteEnv": {
|
|
||||||
"NODE_OPTIONS": "--max_old_space_size=8192"
|
|
||||||
},
|
|
||||||
"customizations": {
|
|
||||||
"vscode": {
|
|
||||||
"extensions": [
|
|
||||||
"dbaeumer.vscode-eslint",
|
|
||||||
"esbenp.prettier-vscode",
|
|
||||||
"runem.lit-plugin",
|
|
||||||
"github.vscode-pull-request-github",
|
|
||||||
"eamodio.gitlens",
|
|
||||||
"yeion7.styled-global-variables-autocomplete"
|
|
||||||
],
|
|
||||||
"settings": {
|
"settings": {
|
||||||
|
"terminal.integrated.shell.linux": "/bin/bash",
|
||||||
"files.eol": "\n",
|
"files.eol": "\n",
|
||||||
"editor.tabSize": 2,
|
"editor.tabSize": 2,
|
||||||
"editor.formatOnPaste": false,
|
"editor.formatOnPaste": false,
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"editor.formatOnType": true,
|
"editor.formatOnType": true,
|
||||||
"editor.renderWhitespace": "boundary",
|
|
||||||
"editor.rulers": [80],
|
|
||||||
"[typescript]": {
|
"[typescript]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
},
|
},
|
||||||
"[javascript]": {
|
"[javascript]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
},
|
},
|
||||||
"files.trimTrailingWhitespace": true,
|
"files.trimTrailingWhitespace": true
|
||||||
"terminal.integrated.shell.linux": "/usr/bin/zsh",
|
|
||||||
"gitlens.showWelcomeOnInstall": false,
|
|
||||||
"gitlens.showWhatsNewAfterUpgrades": false,
|
|
||||||
"workbench.startupEditor": "none"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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."
|
|
119
.eslintrc.json
Normal file
119
.eslintrc.json
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
{
|
||||||
|
"extends": [
|
||||||
|
"airbnb-base",
|
||||||
|
"airbnb-typescript/base",
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
"plugin:wc/recommended",
|
||||||
|
"plugin:lit/all",
|
||||||
|
"prettier"
|
||||||
|
],
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": 2020,
|
||||||
|
"ecmaFeatures": {
|
||||||
|
"modules": true
|
||||||
|
},
|
||||||
|
"sourceType": "module",
|
||||||
|
"project": "./tsconfig.json"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"import/resolver": {
|
||||||
|
"webpack": {
|
||||||
|
"config": "./webpack.config.js"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
"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.
|
||||||
|
592
.github/copilot-instructions.md
vendored
592
.github/copilot-instructions.md
vendored
@ -1,592 +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 @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
|
|
3
.github/dependabot.yml
vendored
3
.github/dependabot.yml
vendored
@ -6,6 +6,3 @@ updates:
|
|||||||
interval: weekly
|
interval: weekly
|
||||||
time: "06:00"
|
time: "06:00"
|
||||||
open-pull-requests-limit: 10
|
open-pull-requests-limit: 10
|
||||||
labels:
|
|
||||||
- Dependencies
|
|
||||||
- GitHub Actions
|
|
||||||
|
51
.github/labeler.yml
vendored
51
.github/labeler.yml
vendored
@ -1,51 +0,0 @@
|
|||||||
Build:
|
|
||||||
- changed-files:
|
|
||||||
- any-glob-to-any-file:
|
|
||||||
- build-scripts/**
|
|
||||||
- .browserslistrc
|
|
||||||
- gulpfile.js
|
|
||||||
|
|
||||||
Cast:
|
|
||||||
- changed-files:
|
|
||||||
- any-glob-to-any-file:
|
|
||||||
- cast/src/**
|
|
||||||
- src/cast/**
|
|
||||||
|
|
||||||
Demo:
|
|
||||||
- changed-files:
|
|
||||||
- any-glob-to-any-file:
|
|
||||||
- demo/src/**
|
|
||||||
- src/fake_data/**
|
|
||||||
|
|
||||||
Design:
|
|
||||||
- changed-files:
|
|
||||||
- any-glob-to-any-file:
|
|
||||||
- gallery/src/**
|
|
||||||
- src/fake_data/**
|
|
||||||
|
|
||||||
Dependencies:
|
|
||||||
- any:
|
|
||||||
- changed-files:
|
|
||||||
# Match when only these files are changed (i.e. don't match PRs that happen to add or remove packages)
|
|
||||||
- any-glob-to-all-files:
|
|
||||||
- package.json
|
|
||||||
- renovate.json
|
|
||||||
- yarn.lock
|
|
||||||
- .yarn/**
|
|
||||||
- .yarnrc.yml
|
|
||||||
- .nvmrc
|
|
||||||
# Dependabot and Renovate branches always match (i.e. compatibility tweaks by members considered minor)
|
|
||||||
- head-branch:
|
|
||||||
- "^renovate/"
|
|
||||||
- "^dependabot/"
|
|
||||||
|
|
||||||
GitHub Actions:
|
|
||||||
- changed-files:
|
|
||||||
- any-glob-to-any-file:
|
|
||||||
- .github/workflows/**
|
|
||||||
- .github/*.yml
|
|
||||||
|
|
||||||
Supervisor:
|
|
||||||
- changed-files:
|
|
||||||
- any-glob-to-any-file:
|
|
||||||
- hassio/src/**
|
|
5
.github/release-drafter.yml
vendored
5
.github/release-drafter.yml
vendored
@ -1,8 +1,3 @@
|
|||||||
categories:
|
|
||||||
- title: "Dependency updates"
|
|
||||||
collapse-after: 3
|
|
||||||
labels:
|
|
||||||
- "Dependencies"
|
|
||||||
template: |
|
template: |
|
||||||
## What's Changed
|
## What's Changed
|
||||||
|
|
||||||
|
83
.github/workflows/cast_deployment.yaml
vendored
83
.github/workflows/cast_deployment.yaml
vendored
@ -1,83 +0,0 @@
|
|||||||
name: Cast deployment
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
schedule:
|
|
||||||
- cron: "0 0 * * *"
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
|
|
||||||
env:
|
|
||||||
NODE_OPTIONS: --max_old_space_size=6144
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
deploy_dev:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: Deploy Development
|
|
||||||
if: github.event_name != 'push'
|
|
||||||
environment:
|
|
||||||
name: Cast Development
|
|
||||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
|
||||||
steps:
|
|
||||||
- name: Check out files from GitHub
|
|
||||||
uses: actions/checkout@v4.2.2
|
|
||||||
with:
|
|
||||||
ref: dev
|
|
||||||
|
|
||||||
- name: Setup Node
|
|
||||||
uses: actions/setup-node@v4.4.0
|
|
||||||
with:
|
|
||||||
node-version-file: ".nvmrc"
|
|
||||||
cache: yarn
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: yarn install --immutable
|
|
||||||
|
|
||||||
- name: Build Cast
|
|
||||||
run: ./node_modules/.bin/gulp build-cast
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Deploy to Netlify
|
|
||||||
id: deploy
|
|
||||||
run: |
|
|
||||||
npx -y netlify-cli deploy --dir=cast/dist --alias dev
|
|
||||||
env:
|
|
||||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
|
||||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_CAST_SITE_ID }}
|
|
||||||
|
|
||||||
deploy_master:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: Deploy Production
|
|
||||||
if: github.event_name == 'push'
|
|
||||||
environment:
|
|
||||||
name: Cast Production
|
|
||||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
|
||||||
steps:
|
|
||||||
- name: Check out files from GitHub
|
|
||||||
uses: actions/checkout@v4.2.2
|
|
||||||
with:
|
|
||||||
ref: master
|
|
||||||
|
|
||||||
- name: Setup Node
|
|
||||||
uses: actions/setup-node@v4.4.0
|
|
||||||
with:
|
|
||||||
node-version-file: ".nvmrc"
|
|
||||||
cache: yarn
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: yarn install --immutable
|
|
||||||
|
|
||||||
- name: Build Cast
|
|
||||||
run: ./node_modules/.bin/gulp build-cast
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Deploy to Netlify
|
|
||||||
id: deploy
|
|
||||||
run: |
|
|
||||||
npx -y netlify-cli deploy --dir=cast/dist --prod
|
|
||||||
env:
|
|
||||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
|
||||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_CAST_SITE_ID }}
|
|
93
.github/workflows/ci.yaml
vendored
93
.github/workflows/ci.yaml
vendored
@ -11,110 +11,87 @@ on:
|
|||||||
- master
|
- master
|
||||||
|
|
||||||
env:
|
env:
|
||||||
|
NODE_VERSION: 16
|
||||||
NODE_OPTIONS: --max_old_space_size=6144
|
NODE_OPTIONS: --max_old_space_size=6144
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
name: Lint and check format
|
|
||||||
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@v4.2.2
|
uses: actions/checkout@v3
|
||||||
- name: Setup Node
|
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||||
uses: actions/setup-node@v4.4.0
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
cache: yarn
|
cache: yarn
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: yarn install --immutable
|
run: yarn install
|
||||||
- name: Check for duplicate dependencies
|
env:
|
||||||
run: yarn dedupe --check
|
CI: true
|
||||||
- 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
|
|
||||||
uses: actions/cache@v4.2.3
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
node_modules/.cache/prettier
|
|
||||||
node_modules/.cache/eslint
|
|
||||||
node_modules/.cache/typescript
|
|
||||||
key: lint-${{ github.sha }}
|
|
||||||
restore-keys: lint-
|
|
||||||
- name: Run eslint
|
- name: Run eslint
|
||||||
run: yarn run lint:eslint --quiet
|
run: yarn run lint:eslint
|
||||||
- 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
|
||||||
|
- name: Check for duplicate dependencies
|
||||||
|
run: yarn dedupe --check
|
||||||
test:
|
test:
|
||||||
name: Run tests
|
|
||||||
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@v4.2.2
|
uses: actions/checkout@v3
|
||||||
- name: Setup Node
|
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||||
uses: actions/setup-node@v4.4.0
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
cache: yarn
|
cache: yarn
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: yarn install --immutable
|
run: yarn install
|
||||||
|
env:
|
||||||
|
CI: true
|
||||||
- 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:
|
||||||
name: Build frontend
|
|
||||||
needs: [lint, test]
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
needs: [lint, test]
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v3
|
||||||
- name: Setup Node
|
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||||
uses: actions/setup-node@v4.4.0
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
cache: yarn
|
cache: yarn
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: yarn install --immutable
|
run: yarn install
|
||||||
|
env:
|
||||||
|
CI: true
|
||||||
- name: Build Application
|
- name: Build Application
|
||||||
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@v4.6.2
|
|
||||||
with:
|
|
||||||
name: frontend-bundle-stats
|
|
||||||
path: build/stats/*.json
|
|
||||||
if-no-files-found: error
|
|
||||||
supervisor:
|
supervisor:
|
||||||
name: Build supervisor
|
|
||||||
needs: [lint, test]
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
needs: [lint, test]
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v3
|
||||||
- name: Setup Node
|
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||||
uses: actions/setup-node@v4.4.0
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
cache: yarn
|
cache: yarn
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: yarn install --immutable
|
run: yarn install
|
||||||
|
env:
|
||||||
|
CI: true
|
||||||
- name: Build Application
|
- name: Build Application
|
||||||
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@v4.6.2
|
|
||||||
with:
|
|
||||||
name: supervisor-bundle-stats
|
|
||||||
path: build/stats/*.json
|
|
||||||
if-no-files-found: error
|
|
||||||
|
10
.github/workflows/codeql-analysis.yml
vendored
10
.github/workflows/codeql-analysis.yml
vendored
@ -17,13 +17,13 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
# Override automatic language detection by changing the below list
|
# Override automatic language detection by changing the below list
|
||||||
# Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
|
# Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
|
||||||
language: ["javascript"]
|
language: ['javascript']
|
||||||
# Learn more...
|
# Learn more...
|
||||||
# https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
|
# https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v3
|
||||||
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@v3
|
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@v3
|
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@v3
|
uses: github/codeql-action/analyze@v2
|
||||||
|
33
.github/workflows/demo.yaml
vendored
Normal file
33
.github/workflows/demo.yaml
vendored
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
name: Demo
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- dev
|
||||||
|
|
||||||
|
env:
|
||||||
|
NODE_VERSION: 16
|
||||||
|
NODE_OPTIONS: --max_old_space_size=6144
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check out files from GitHub
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
|
cache: yarn
|
||||||
|
- name: Install dependencies
|
||||||
|
run: yarn install
|
||||||
|
env:
|
||||||
|
CI: true
|
||||||
|
- name: Build Demo
|
||||||
|
run: ./node_modules/.bin/gulp build-demo
|
||||||
|
- name: Deploy to Netlify
|
||||||
|
run: npx netlify-cli deploy --dir=demo/dist --prod
|
||||||
|
env:
|
||||||
|
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||||
|
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_DEMO_DEV_SITE_ID }}
|
84
.github/workflows/demo_deployment.yaml
vendored
84
.github/workflows/demo_deployment.yaml
vendored
@ -1,84 +0,0 @@
|
|||||||
name: Demo deployment
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
schedule:
|
|
||||||
- cron: "0 0 * * *"
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- dev
|
|
||||||
- master
|
|
||||||
|
|
||||||
env:
|
|
||||||
NODE_OPTIONS: --max_old_space_size=6144
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
deploy_dev:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: Demo Development
|
|
||||||
if: github.event_name != 'push' || github.ref_name != 'master'
|
|
||||||
environment:
|
|
||||||
name: Demo Development
|
|
||||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
|
||||||
steps:
|
|
||||||
- name: Check out files from GitHub
|
|
||||||
uses: actions/checkout@v4.2.2
|
|
||||||
with:
|
|
||||||
ref: dev
|
|
||||||
|
|
||||||
- name: Setup Node
|
|
||||||
uses: actions/setup-node@v4.4.0
|
|
||||||
with:
|
|
||||||
node-version-file: ".nvmrc"
|
|
||||||
cache: yarn
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: yarn install --immutable
|
|
||||||
|
|
||||||
- name: Build Demo
|
|
||||||
run: ./node_modules/.bin/gulp build-demo
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Deploy to Netlify
|
|
||||||
id: deploy
|
|
||||||
run: |
|
|
||||||
npx -y netlify-cli deploy --dir=demo/dist --prod
|
|
||||||
env:
|
|
||||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
|
||||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_DEMO_DEV_SITE_ID }}
|
|
||||||
|
|
||||||
deploy_master:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: Demo Production
|
|
||||||
if: github.event_name == 'push' && github.ref_name == 'master'
|
|
||||||
environment:
|
|
||||||
name: Demo Production
|
|
||||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
|
||||||
steps:
|
|
||||||
- name: Check out files from GitHub
|
|
||||||
uses: actions/checkout@v4.2.2
|
|
||||||
with:
|
|
||||||
ref: master
|
|
||||||
|
|
||||||
- name: Setup Node
|
|
||||||
uses: actions/setup-node@v4.4.0
|
|
||||||
with:
|
|
||||||
node-version-file: ".nvmrc"
|
|
||||||
cache: yarn
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: yarn install --immutable
|
|
||||||
|
|
||||||
- name: Build Demo
|
|
||||||
run: ./node_modules/.bin/gulp build-demo
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Deploy to Netlify
|
|
||||||
id: deploy
|
|
||||||
run: |
|
|
||||||
npx -y netlify-cli deploy --dir=demo/dist --prod
|
|
||||||
env:
|
|
||||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
|
||||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_DEMO_SITE_ID }}
|
|
41
.github/workflows/design_deployment.yaml
vendored
41
.github/workflows/design_deployment.yaml
vendored
@ -1,41 +0,0 @@
|
|||||||
name: Design deployment
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
schedule:
|
|
||||||
- cron: "0 0 * * *"
|
|
||||||
|
|
||||||
env:
|
|
||||||
NODE_OPTIONS: --max_old_space_size=6144
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
deploy:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
environment:
|
|
||||||
name: Design
|
|
||||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
|
||||||
steps:
|
|
||||||
- name: Check out files from GitHub
|
|
||||||
uses: actions/checkout@v4.2.2
|
|
||||||
|
|
||||||
- name: Setup Node
|
|
||||||
uses: actions/setup-node@v4.4.0
|
|
||||||
with:
|
|
||||||
node-version-file: ".nvmrc"
|
|
||||||
cache: yarn
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: yarn install --immutable
|
|
||||||
|
|
||||||
- name: Build Gallery
|
|
||||||
run: ./node_modules/.bin/gulp build-gallery
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Deploy to Netlify
|
|
||||||
id: deploy
|
|
||||||
run: |
|
|
||||||
npx -y netlify-cli deploy --dir=gallery/dist --prod
|
|
||||||
env:
|
|
||||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
|
||||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_GALLERY_SITE_ID }}
|
|
52
.github/workflows/design_preview.yaml
vendored
52
.github/workflows/design_preview.yaml
vendored
@ -1,52 +0,0 @@
|
|||||||
name: Design preview
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types:
|
|
||||||
- opened
|
|
||||||
- synchronize
|
|
||||||
- reopened
|
|
||||||
- labeled
|
|
||||||
branches:
|
|
||||||
- dev
|
|
||||||
|
|
||||||
env:
|
|
||||||
NODE_OPTIONS: --max_old_space_size=6144
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
preview:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
# Skip running on forks since it won't have access to secrets
|
|
||||||
# Skip running PRs without 'needs design preview' label
|
|
||||||
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
|
|
||||||
steps:
|
|
||||||
- name: Check out files from GitHub
|
|
||||||
uses: actions/checkout@v4.2.2
|
|
||||||
|
|
||||||
- name: Setup Node
|
|
||||||
uses: actions/setup-node@v4.4.0
|
|
||||||
with:
|
|
||||||
node-version-file: ".nvmrc"
|
|
||||||
cache: yarn
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: yarn install --immutable
|
|
||||||
|
|
||||||
- name: Build Gallery
|
|
||||||
run: ./node_modules/.bin/gulp build-gallery
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Deploy preview to Netlify
|
|
||||||
id: deploy
|
|
||||||
run: |
|
|
||||||
npx -y netlify-cli deploy --dir=gallery/dist --alias "deploy-preview-${{ github.event.number }}" \
|
|
||||||
--json > deploy_output.json
|
|
||||||
env:
|
|
||||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
|
||||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_GALLERY_SITE_ID }}
|
|
||||||
|
|
||||||
- name: Generate summary
|
|
||||||
run: |
|
|
||||||
NETLIFY_LIVE_URL=$(jq -r '.deploy_url' deploy_output.json)
|
|
||||||
echo "$NETLIFY_LIVE_URL" >> "$GITHUB_STEP_SUMMARY"
|
|
15
.github/workflows/labeler.yaml
vendored
15
.github/workflows/labeler.yaml
vendored
@ -1,15 +0,0 @@
|
|||||||
name: "Pull Request Labeler"
|
|
||||||
|
|
||||||
on: pull_request_target
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
triage:
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
pull-requests: write
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Apply labels
|
|
||||||
uses: actions/labeler@v5.0.0
|
|
||||||
with:
|
|
||||||
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@v5.0.1
|
- uses: dessant/lock-threads@v3.0.0
|
||||||
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: ""
|
||||||
|
19
.github/workflows/netflify.yml
vendored
Normal file
19
.github/workflows/netflify.yml
vendored
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
name: Netlify
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "0 0 * * *"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
trigger_builds:
|
||||||
|
name: Trigger netlify build preview
|
||||||
|
runs-on: "ubuntu-latest"
|
||||||
|
steps:
|
||||||
|
- name: Trigger Cast build
|
||||||
|
run: curl -X POST -d {} https://api.netlify.com/build_hooks/${{ secrets.NETLIFY_CAST_DEV_BUILD_HOOK }}
|
||||||
|
|
||||||
|
- name: Trigger Demo build
|
||||||
|
run: curl -X POST -d {} https://api.netlify.com/build_hooks/${{ secrets.NETLIFY_DEMO_DEV_BUILD_HOOK }}
|
||||||
|
|
||||||
|
- name: Trigger Design build
|
||||||
|
run: curl -X POST -d "NIGHTLY" https://api.netlify.com/build_hooks/${{ secrets.NETLIFY_GALLERY_DEV_BUILD_HOOK }}
|
20
.github/workflows/nightly.yaml
vendored
20
.github/workflows/nightly.yaml
vendored
@ -6,7 +6,8 @@ on:
|
|||||||
- cron: "0 1 * * *"
|
- cron: "0 1 * * *"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
PYTHON_VERSION: "3.13"
|
PYTHON_VERSION: "3.10"
|
||||||
|
NODE_VERSION: 16
|
||||||
NODE_OPTIONS: --max_old_space_size=6144
|
NODE_OPTIONS: --max_old_space_size=6144
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
@ -20,17 +21,17 @@ jobs:
|
|||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.PYTHON_VERSION }}
|
python-version: ${{ env.PYTHON_VERSION }}
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||||
uses: actions/setup-node@v4.4.0
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
cache: yarn
|
cache: yarn
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
@ -48,8 +49,9 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
pip install build
|
pip install build
|
||||||
yarn install
|
yarn install
|
||||||
export SKIP_FETCH_NIGHTLY_TRANSLATIONS=1
|
|
||||||
script/build_frontend
|
script/build_frontend
|
||||||
|
|
||||||
rm -rf dist home_assistant_frontend.egg-info
|
rm -rf dist home_assistant_frontend.egg-info
|
||||||
python3 -m build
|
python3 -m build
|
||||||
|
|
||||||
@ -57,14 +59,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@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@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@v3.0.0
|
|
||||||
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) }}
|
|
11
.github/workflows/release-drafter.yaml
vendored
11
.github/workflows/release-drafter.yaml
vendored
@ -5,19 +5,10 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- dev
|
- dev
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
update_release_draft:
|
update_release_draft:
|
||||||
permissions:
|
|
||||||
# write permission for contents is required to create a github release
|
|
||||||
contents: write
|
|
||||||
# write permission for pull-requests is required for autolabeler
|
|
||||||
# otherwise, read permission is required at least
|
|
||||||
pull-requests: read
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: release-drafter/release-drafter@v6.1.0
|
- uses: release-drafter/release-drafter@v5
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
87
.github/workflows/release.yaml
vendored
87
.github/workflows/release.yaml
vendored
@ -6,7 +6,8 @@ on:
|
|||||||
- published
|
- published
|
||||||
|
|
||||||
env:
|
env:
|
||||||
PYTHON_VERSION: "3.13"
|
PYTHON_VERSION: "3.10"
|
||||||
|
NODE_VERSION: 16
|
||||||
NODE_OPTIONS: --max_old_space_size=6144
|
NODE_OPTIONS: --max_old_space_size=6144
|
||||||
|
|
||||||
# Set default workflow permissions
|
# Set default workflow permissions
|
||||||
@ -23,20 +24,20 @@ 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@v4.2.2
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
|
||||||
uses: actions/setup-python@v5
|
|
||||||
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: Setup Node
|
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
||||||
uses: actions/setup-node@v4.4.0
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
python-version: ${{ env.PYTHON_VERSION }}
|
||||||
|
|
||||||
|
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
cache: yarn
|
cache: yarn
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
@ -51,11 +52,11 @@ jobs:
|
|||||||
python3 -m pip install twine build
|
python3 -m pip install twine build
|
||||||
export TWINE_USERNAME="__token__"
|
export TWINE_USERNAME="__token__"
|
||||||
export TWINE_PASSWORD="${{ secrets.TWINE_TOKEN }}"
|
export TWINE_PASSWORD="${{ secrets.TWINE_TOKEN }}"
|
||||||
export SKIP_FETCH_NIGHTLY_TRANSLATIONS=1
|
|
||||||
script/release
|
script/release
|
||||||
|
|
||||||
- name: Upload release assets
|
- name: Upload release assets
|
||||||
uses: softprops/action-gh-release@v2.3.2
|
uses: softprops/action-gh-release@v0.1.14
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
dist/*.whl
|
dist/*.whl
|
||||||
@ -74,68 +75,10 @@ jobs:
|
|||||||
echo "home-assistant-frontend==$version" > ./requirements.txt
|
echo "home-assistant-frontend==$version" > ./requirements.txt
|
||||||
|
|
||||||
- name: Build wheels
|
- name: Build wheels
|
||||||
uses: home-assistant/wheels@2025.03.0
|
uses: home-assistant/wheels@2022.10.1
|
||||||
with:
|
with:
|
||||||
abi: cp313
|
abi: cp310
|
||||||
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@v4.2.2
|
|
||||||
- name: Setup Node
|
|
||||||
uses: actions/setup-node@v4.4.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@v2.3.2
|
|
||||||
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@v4.2.2
|
|
||||||
- name: Setup Node
|
|
||||||
uses: actions/setup-node@v4.4.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@v2.3.2
|
|
||||||
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.issue_type == 'Task'
|
|
||||||
steps:
|
|
||||||
- name: Check if user is authorized
|
|
||||||
uses: actions/github-script@v7
|
|
||||||
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@v9.1.0
|
uses: actions/stale@v6.0.1
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
days-before-stale: 90
|
days-before-stale: 90
|
||||||
|
7
.github/workflows/translations.yaml
vendored
7
.github/workflows/translations.yaml
vendored
@ -1,22 +1,25 @@
|
|||||||
name: Translations
|
name: Translations
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- dev
|
- dev
|
||||||
paths:
|
paths:
|
||||||
- src/translations/en.json
|
- src/translations/en.json
|
||||||
|
|
||||||
|
env:
|
||||||
|
NODE_VERSION: 16
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
upload:
|
upload:
|
||||||
name: Upload
|
name: Upload
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Upload Translations
|
- name: Upload Translations
|
||||||
run: |
|
run: |
|
||||||
export LOKALISE_TOKEN="${{ secrets.LOKALISE_TOKEN }}"
|
export LOKALISE_TOKEN="${{ secrets.LOKALISE_TOKEN }}"
|
||||||
|
|
||||||
./script/translations_upload_base
|
./script/translations_upload_base
|
||||||
|
23
.gitignore
vendored
23
.gitignore
vendored
@ -2,10 +2,9 @@
|
|||||||
.reify-cache
|
.reify-cache
|
||||||
|
|
||||||
# build
|
# build
|
||||||
build/
|
build
|
||||||
dist/
|
hass_frontend/*
|
||||||
/hass_frontend/
|
dist
|
||||||
/translations/
|
|
||||||
|
|
||||||
# yarn
|
# yarn
|
||||||
.yarn/*
|
.yarn/*
|
||||||
@ -15,7 +14,7 @@ dist/
|
|||||||
!.yarn/sdks
|
!.yarn/sdks
|
||||||
!.yarn/versions
|
!.yarn/versions
|
||||||
.pnp.*
|
.pnp.*
|
||||||
/node_modules/
|
node_modules/*
|
||||||
yarn-error.log
|
yarn-error.log
|
||||||
npm-debug.log
|
npm-debug.log
|
||||||
|
|
||||||
@ -27,7 +26,7 @@ npm-debug.log
|
|||||||
# venv stuff
|
# venv stuff
|
||||||
pyvenv.cfg
|
pyvenv.cfg
|
||||||
pip-selfcheck.json
|
pip-selfcheck.json
|
||||||
/venv/
|
venv/*
|
||||||
.venv
|
.venv
|
||||||
|
|
||||||
# vscode
|
# vscode
|
||||||
@ -46,14 +45,4 @@ src/cast/dev_const.ts
|
|||||||
.tool-versions
|
.tool-versions
|
||||||
|
|
||||||
# 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,9 @@
|
|||||||
CLA.md
|
build
|
||||||
CODE_OF_CONDUCT.md
|
translations/*
|
||||||
LICENSE.md
|
node_modules/*
|
||||||
PULL_REQUEST_TEMPLATE.md
|
hass_frontend/*
|
||||||
|
pip-selfcheck.json
|
||||||
|
|
||||||
|
# vscode
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
7
.vscode/extensions.json
vendored
7
.vscode/extensions.json
vendored
@ -2,10 +2,7 @@
|
|||||||
"recommendations": [
|
"recommendations": [
|
||||||
"dbaeumer.vscode-eslint",
|
"dbaeumer.vscode-eslint",
|
||||||
"esbenp.prettier-vscode",
|
"esbenp.prettier-vscode",
|
||||||
"runem.lit-plugin",
|
"bierner.lit-html",
|
||||||
"github.vscode-pull-request-github",
|
"runem.lit-plugin"
|
||||||
"eamodio.gitlens",
|
|
||||||
"vitest.explorer",
|
|
||||||
"yeion7.styled-global-variables-autocomplete"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
6
.vscode/launch.json
vendored
6
.vscode/launch.json
vendored
@ -9,7 +9,9 @@
|
|||||||
"webRoot": "${workspaceFolder}/hass_frontend",
|
"webRoot": "${workspaceFolder}/hass_frontend",
|
||||||
"disableNetworkCache": true,
|
"disableNetworkCache": true,
|
||||||
"preLaunchTask": "Develop Frontend",
|
"preLaunchTask": "Develop Frontend",
|
||||||
"outFiles": ["${workspaceFolder}/hass_frontend/frontend_latest/*.js"]
|
"outFiles": [
|
||||||
|
"${workspaceFolder}/hass_frontend/frontend_latest/*.js"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Debug Gallery",
|
"name": "Debug Gallery",
|
||||||
@ -37,6 +39,6 @@
|
|||||||
"webRoot": "${workspaceFolder}/cast/dist",
|
"webRoot": "${workspaceFolder}/cast/dist",
|
||||||
"disableNetworkCache": true,
|
"disableNetworkCache": true,
|
||||||
"preLaunchTask": "Develop Cast"
|
"preLaunchTask": "Develop Cast"
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
80
.vscode/tasks.json
vendored
80
.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",
|
||||||
@ -259,12 +191,6 @@
|
|||||||
"runOptions": {
|
"runOptions": {
|
||||||
"instanceLimit": 1
|
"instanceLimit": 1
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "Setup and fetch nightly translations",
|
|
||||||
"type": "gulp",
|
|
||||||
"task": "setup-and-fetch-nightly-translations",
|
|
||||||
"problemMatcher": []
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"inputs": [
|
"inputs": [
|
||||||
@ -277,12 +203,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"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
29
.yarn/patches/@lit-labs/virtualizer/event-target-shim.patch
Normal file
29
.yarn/patches/@lit-labs/virtualizer/event-target-shim.patch
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
diff --git a/polyfillLoaders/EventTarget.js b/polyfillLoaders/EventTarget.js
|
||||||
|
index 4e18ade7ba485849f17f28c94c42f0e0e01ac387..8f34f4f646c7f7becc208fb5a546c96034fc74dc 100644
|
||||||
|
--- a/polyfillLoaders/EventTarget.js
|
||||||
|
+++ b/polyfillLoaders/EventTarget.js
|
||||||
|
@@ -6,16 +6,15 @@
|
||||||
|
let _ET;
|
||||||
|
let ET;
|
||||||
|
export default async function EventTarget() {
|
||||||
|
- return ET || init();
|
||||||
|
+ return ET || init();
|
||||||
|
}
|
||||||
|
async function init() {
|
||||||
|
- _ET = window.EventTarget;
|
||||||
|
- try {
|
||||||
|
- new _ET();
|
||||||
|
- }
|
||||||
|
- catch (_a) {
|
||||||
|
- _ET = (await import('event-target-shim')).EventTarget;
|
||||||
|
- }
|
||||||
|
- return (ET = _ET);
|
||||||
|
+ _ET = window.EventTarget;
|
||||||
|
+ try {
|
||||||
|
+ new _ET();
|
||||||
|
+ } catch (_a) {
|
||||||
|
+ _ET = (await import("event-target-shim")).default.EventTarget;
|
||||||
|
+ }
|
||||||
|
+ return (ET = _ET);
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=EventTarget.js.map
|
@ -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 }),
|
|
12
.yarn/patches/@material/mwc-icon-button/remove-icon.patch
Normal file
12
.yarn/patches/@material/mwc-icon-button/remove-icon.patch
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
diff --git a/mwc-icon-button-base.js b/mwc-icon-button-base.js
|
||||||
|
index 45cdaab93ccc0a6daaaaabc01266dcdc32e46bfd..b3ea5b541597308d85f86ce6c23fd00785fda835 100644
|
||||||
|
--- a/mwc-icon-button-base.js
|
||||||
|
+++ b/mwc-icon-button-base.js
|
||||||
|
@@ -63,7 +63,6 @@ export class IconButtonBase extends LitElement {
|
||||||
|
@touchend="${this.handleRippleDeactivate}"
|
||||||
|
@touchcancel="${this.handleRippleDeactivate}"
|
||||||
|
>${this.renderRipple()}
|
||||||
|
- <i class="material-icons">${this.icon}</i>
|
||||||
|
<span
|
||||||
|
><slot></slot
|
||||||
|
></span>
|
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++) {
|
File diff suppressed because one or more lines are too long
@ -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}`;
|
|
||||||
}
|
|
77
.yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
vendored
Normal file
77
.yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
vendored
Normal file
File diff suppressed because one or more lines are too long
8
.yarn/plugins/@yarnpkg/plugin-typescript.cjs
vendored
Normal file
8
.yarn/plugins/@yarnpkg/plugin-typescript.cjs
vendored
Normal file
File diff suppressed because one or more lines are too long
783
.yarn/releases/yarn-3.2.3.cjs
vendored
Executable file
783
.yarn/releases/yarn-3.2.3.cjs
vendored
Executable file
File diff suppressed because one or more lines are too long
942
.yarn/releases/yarn-4.9.2.cjs
vendored
942
.yarn/releases/yarn-4.9.2.cjs
vendored
File diff suppressed because one or more lines are too long
14
.yarnrc.yml
14
.yarnrc.yml
@ -1,9 +1,9 @@
|
|||||||
compressionLevel: mixed
|
|
||||||
|
|
||||||
defaultSemverRangePrefix: ""
|
|
||||||
|
|
||||||
enableGlobalCache: false
|
|
||||||
|
|
||||||
nodeLinker: node-modules
|
nodeLinker: node-modules
|
||||||
|
|
||||||
yarnPath: .yarn/releases/yarn-4.9.2.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.2.3.cjs
|
||||||
|
@ -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/)
|
|
||||||
|
7
build-scripts/.eslintrc
Normal file
7
build-scripts/.eslintrc
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"rules": {
|
||||||
|
"import/no-extraneous-dependencies": 0,
|
||||||
|
"no-restricted-syntax": 0,
|
||||||
|
"no-console": 0
|
||||||
|
}
|
||||||
|
}
|
7
build-scripts/.eslintrc.json
Normal file
7
build-scripts/.eslintrc.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"extends": "../.eslintrc.json",
|
||||||
|
"rules": {
|
||||||
|
"import/no-extraneous-dependencies": 0,
|
||||||
|
"global-require": 0
|
||||||
|
}
|
||||||
|
}
|
@ -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,3 +1,4 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
|
||||||
// Currently only supports CommonJS modules, as require is synchronous. `import` would need babel running asynchronous.
|
// Currently only supports CommonJS modules, as require is synchronous. `import` would need babel running asynchronous.
|
||||||
@ -28,6 +29,7 @@ module.exports = function inlineConstants(babel, options, cwd) {
|
|||||||
const absolute = module.startsWith(".")
|
const absolute = module.startsWith(".")
|
||||||
? require.resolve(module, { paths: [cwd] })
|
? require.resolve(module, { paths: [cwd] })
|
||||||
: module;
|
: module;
|
||||||
|
// eslint-disable-next-line import/no-dynamic-require
|
||||||
return [absolute, require(absolute)];
|
return [absolute, require(absolute)];
|
||||||
})
|
})
|
||||||
);
|
);
|
@ -1,343 +0,0 @@
|
|||||||
const path = require("path");
|
|
||||||
const env = require("./env.cjs");
|
|
||||||
const paths = require("./paths.cjs");
|
|
||||||
const { dependencies } = require("../package.json");
|
|
||||||
|
|
||||||
const BABEL_PLUGINS = path.join(__dirname, "babel-plugins");
|
|
||||||
|
|
||||||
// GitHub base URL to use for production source maps
|
|
||||||
// Nightly builds use the commit SHA, otherwise assumes there is a tag that matches the version
|
|
||||||
module.exports.sourceMapURL = () => {
|
|
||||||
const ref = env.version().endsWith("dev")
|
|
||||||
? process.env.GITHUB_SHA || "dev"
|
|
||||||
: env.version();
|
|
||||||
return `https://raw.githubusercontent.com/home-assistant/frontend/${ref}/`;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Files from NPM Packages that should not be imported
|
|
||||||
module.exports.ignorePackages = () => [];
|
|
||||||
|
|
||||||
// Files from NPM packages that we should replace with empty file
|
|
||||||
module.exports.emptyPackages = ({ isHassioBuild }) =>
|
|
||||||
[
|
|
||||||
require.resolve("@vaadin/vaadin-material-styles/typography.js"),
|
|
||||||
require.resolve("@vaadin/vaadin-material-styles/font-icons.js"),
|
|
||||||
// Icons in supervisor conflict with icons in HA so we don't load.
|
|
||||||
isHassioBuild &&
|
|
||||||
require.resolve(
|
|
||||||
path.resolve(paths.root_dir, "src/components/ha-icon.ts")
|
|
||||||
),
|
|
||||||
isHassioBuild &&
|
|
||||||
require.resolve(
|
|
||||||
path.resolve(paths.root_dir, "src/components/ha-icon-picker.ts")
|
|
||||||
),
|
|
||||||
].filter(Boolean);
|
|
||||||
|
|
||||||
module.exports.definedVars = ({ isProdBuild, latestBuild, defineOverlay }) => ({
|
|
||||||
__DEV__: !isProdBuild,
|
|
||||||
__BUILD__: JSON.stringify(latestBuild ? "modern" : "legacy"),
|
|
||||||
__VERSION__: JSON.stringify(env.version()),
|
|
||||||
__DEMO__: false,
|
|
||||||
__SUPERVISOR__: false,
|
|
||||||
__BACKWARDS_COMPAT__: false,
|
|
||||||
__STATIC_PATH__: "/static/",
|
|
||||||
__HASS_URL__: `\`${
|
|
||||||
"HASS_URL" in process.env
|
|
||||||
? process.env.HASS_URL
|
|
||||||
: // eslint-disable-next-line no-template-curly-in-string
|
|
||||||
"${location.protocol}//${location.host}"
|
|
||||||
}\``,
|
|
||||||
"process.env.NODE_ENV": JSON.stringify(
|
|
||||||
isProdBuild ? "production" : "development"
|
|
||||||
),
|
|
||||||
...defineOverlay,
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports.htmlMinifierOptions = {
|
|
||||||
caseSensitive: true,
|
|
||||||
collapseWhitespace: true,
|
|
||||||
conservativeCollapse: true,
|
|
||||||
decodeEntities: true,
|
|
||||||
removeComments: true,
|
|
||||||
removeRedundantAttributes: true,
|
|
||||||
minifyCSS: {
|
|
||||||
compatibility: "*,-properties.zeroUnits",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports.terserOptions = ({ latestBuild, isTestBuild }) => ({
|
|
||||||
safari10: !latestBuild,
|
|
||||||
ecma: latestBuild ? 2015 : 5,
|
|
||||||
module: latestBuild,
|
|
||||||
format: { comments: false },
|
|
||||||
sourceMap: !isTestBuild,
|
|
||||||
});
|
|
||||||
|
|
||||||
/** @type {import('@rspack/core').SwcLoaderOptions} */
|
|
||||||
module.exports.swcOptions = () => ({
|
|
||||||
jsc: {
|
|
||||||
loose: true,
|
|
||||||
externalHelpers: true,
|
|
||||||
target: "ES2021",
|
|
||||||
parser: {
|
|
||||||
syntax: "typescript",
|
|
||||||
decorators: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports.babelOptions = ({
|
|
||||||
latestBuild,
|
|
||||||
isProdBuild,
|
|
||||||
isTestBuild,
|
|
||||||
sw,
|
|
||||||
}) => ({
|
|
||||||
babelrc: false,
|
|
||||||
compact: false,
|
|
||||||
assumptions: {
|
|
||||||
privateFieldsAsProperties: true,
|
|
||||||
setPublicClassFields: true,
|
|
||||||
setSpreadProperties: true,
|
|
||||||
},
|
|
||||||
browserslistEnv: latestBuild ? "modern" : `legacy${sw ? "-sw" : ""}`,
|
|
||||||
presets: [
|
|
||||||
[
|
|
||||||
"@babel/preset-env",
|
|
||||||
{
|
|
||||||
useBuiltIns: "usage",
|
|
||||||
corejs: dependencies["core-js"],
|
|
||||||
bugfixes: true,
|
|
||||||
shippedProposals: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
],
|
|
||||||
plugins: [
|
|
||||||
[
|
|
||||||
path.join(BABEL_PLUGINS, "inline-constants-plugin.cjs"),
|
|
||||||
{
|
|
||||||
modules: ["@mdi/js"],
|
|
||||||
ignoreModuleNotFound: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
// Minify template literals for production
|
|
||||||
isProdBuild && [
|
|
||||||
"template-html-minifier",
|
|
||||||
{
|
|
||||||
modules: {
|
|
||||||
...Object.fromEntries(
|
|
||||||
["lit", "lit-element", "lit-html"].map((m) => [
|
|
||||||
m,
|
|
||||||
[
|
|
||||||
"html",
|
|
||||||
{ name: "svg", encapsulation: "svg" },
|
|
||||||
{ name: "css", encapsulation: "style" },
|
|
||||||
],
|
|
||||||
])
|
|
||||||
),
|
|
||||||
"@polymer/polymer/lib/utils/html-tag.js": ["html"],
|
|
||||||
},
|
|
||||||
strictCSS: true,
|
|
||||||
htmlMinifier: module.exports.htmlMinifierOptions,
|
|
||||||
failOnError: false, // we can turn this off in case of false positives
|
|
||||||
},
|
|
||||||
],
|
|
||||||
// Import helpers and regenerator from runtime package
|
|
||||||
[
|
|
||||||
"@babel/plugin-transform-runtime",
|
|
||||||
{ version: dependencies["@babel/runtime"] },
|
|
||||||
],
|
|
||||||
"@babel/plugin-transform-class-properties",
|
|
||||||
"@babel/plugin-transform-private-methods",
|
|
||||||
].filter(Boolean),
|
|
||||||
exclude: [
|
|
||||||
// \\ for Windows, / for Mac OS and Linux
|
|
||||||
/node_modules[\\/]core-js/,
|
|
||||||
],
|
|
||||||
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",
|
|
||||||
"@shoelace-style",
|
|
||||||
"@?lit(?:-labs|-element|-html)?",
|
|
||||||
].map((p) => new RegExp(`/node_modules/${p}/`)),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
const nameSuffix = (latestBuild) => (latestBuild ? "-modern" : "-legacy");
|
|
||||||
|
|
||||||
const outputPath = (outputRoot, latestBuild) =>
|
|
||||||
path.resolve(outputRoot, latestBuild ? "frontend_latest" : "frontend_es5");
|
|
||||||
|
|
||||||
const publicPath = (latestBuild, root = "") =>
|
|
||||||
latestBuild ? `${root}/frontend_latest/` : `${root}/frontend_es5/`;
|
|
||||||
|
|
||||||
/*
|
|
||||||
BundleConfig {
|
|
||||||
// Object with entrypoints that need to be bundled
|
|
||||||
entry: { [name: string]: pathToFile },
|
|
||||||
// Folder where bundled files need to be written
|
|
||||||
outputPath: string,
|
|
||||||
// absolute url-path where bundled files can be found
|
|
||||||
publicPath: string,
|
|
||||||
// extra definitions that we need to replace in source
|
|
||||||
defineOverlay: {[name: string]: value },
|
|
||||||
// if this is a production build
|
|
||||||
isProdBuild: boolean,
|
|
||||||
// If we're targeting latest browsers
|
|
||||||
latestBuild: boolean,
|
|
||||||
// If we're doing a stats build (create nice chunk names)
|
|
||||||
isStatsBuild: boolean,
|
|
||||||
// If it's just a test build in CI, skip time on source map generation
|
|
||||||
isTestBuild: boolean,
|
|
||||||
// Names of entrypoints that should not be hashed
|
|
||||||
dontHash: Set<string>
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
module.exports.config = {
|
|
||||||
app({ isProdBuild, latestBuild, isStatsBuild, isTestBuild, isWDS }) {
|
|
||||||
return {
|
|
||||||
name: "frontend" + nameSuffix(latestBuild),
|
|
||||||
entry: {
|
|
||||||
"service-worker": !latestBuild
|
|
||||||
? {
|
|
||||||
import: "./src/entrypoints/service-worker.ts",
|
|
||||||
layer: "sw",
|
|
||||||
}
|
|
||||||
: "./src/entrypoints/service-worker.ts",
|
|
||||||
app: "./src/entrypoints/app.ts",
|
|
||||||
authorize: "./src/entrypoints/authorize.ts",
|
|
||||||
onboarding: "./src/entrypoints/onboarding.ts",
|
|
||||||
core: "./src/entrypoints/core.ts",
|
|
||||||
"custom-panel": "./src/entrypoints/custom-panel.ts",
|
|
||||||
},
|
|
||||||
outputPath: outputPath(paths.app_output_root, latestBuild),
|
|
||||||
publicPath: publicPath(latestBuild),
|
|
||||||
isProdBuild,
|
|
||||||
latestBuild,
|
|
||||||
isStatsBuild,
|
|
||||||
isTestBuild,
|
|
||||||
isWDS,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
demo({ isProdBuild, latestBuild, isStatsBuild }) {
|
|
||||||
return {
|
|
||||||
name: "demo" + nameSuffix(latestBuild),
|
|
||||||
entry: {
|
|
||||||
main: path.resolve(paths.demo_dir, "src/entrypoint.ts"),
|
|
||||||
},
|
|
||||||
outputPath: outputPath(paths.demo_output_root, latestBuild),
|
|
||||||
publicPath: publicPath(latestBuild),
|
|
||||||
defineOverlay: {
|
|
||||||
__VERSION__: JSON.stringify(`DEMO-${env.version()}`),
|
|
||||||
__DEMO__: true,
|
|
||||||
},
|
|
||||||
isProdBuild,
|
|
||||||
latestBuild,
|
|
||||||
isStatsBuild,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
cast({ isProdBuild, latestBuild }) {
|
|
||||||
const entry = {
|
|
||||||
launcher: path.resolve(paths.cast_dir, "src/launcher/entrypoint.ts"),
|
|
||||||
media: path.resolve(paths.cast_dir, "src/media/entrypoint.ts"),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (latestBuild) {
|
|
||||||
entry.receiver = path.resolve(
|
|
||||||
paths.cast_dir,
|
|
||||||
"src/receiver/entrypoint.ts"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: "cast" + nameSuffix(latestBuild),
|
|
||||||
entry,
|
|
||||||
outputPath: outputPath(paths.cast_output_root, latestBuild),
|
|
||||||
publicPath: publicPath(latestBuild),
|
|
||||||
isProdBuild,
|
|
||||||
latestBuild,
|
|
||||||
defineOverlay: {
|
|
||||||
__BACKWARDS_COMPAT__: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
hassio({ isProdBuild, latestBuild, isStatsBuild, isTestBuild }) {
|
|
||||||
return {
|
|
||||||
name: "supervisor" + nameSuffix(latestBuild),
|
|
||||||
entry: {
|
|
||||||
entrypoint: path.resolve(paths.hassio_dir, "src/entrypoint.ts"),
|
|
||||||
},
|
|
||||||
outputPath: outputPath(paths.hassio_output_root, latestBuild),
|
|
||||||
publicPath: publicPath(latestBuild, paths.hassio_publicPath),
|
|
||||||
isProdBuild,
|
|
||||||
latestBuild,
|
|
||||||
isStatsBuild,
|
|
||||||
isTestBuild,
|
|
||||||
isHassioBuild: true,
|
|
||||||
defineOverlay: {
|
|
||||||
__SUPERVISOR__: true,
|
|
||||||
__STATIC_PATH__: `"${paths.hassio_publicPath}/static/"`,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
gallery({ isProdBuild, latestBuild }) {
|
|
||||||
return {
|
|
||||||
name: "gallery" + nameSuffix(latestBuild),
|
|
||||||
entry: {
|
|
||||||
entrypoint: path.resolve(paths.gallery_dir, "src/entrypoint.js"),
|
|
||||||
},
|
|
||||||
outputPath: outputPath(paths.gallery_output_root, latestBuild),
|
|
||||||
publicPath: publicPath(latestBuild),
|
|
||||||
isProdBuild,
|
|
||||||
latestBuild,
|
|
||||||
defineOverlay: {
|
|
||||||
__DEMO__: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
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,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
229
build-scripts/bundle.js
Normal file
229
build-scripts/bundle.js
Normal file
@ -0,0 +1,229 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
|
const path = require("path");
|
||||||
|
const env = require("./env.js");
|
||||||
|
const paths = require("./paths.js");
|
||||||
|
|
||||||
|
// Files from NPM Packages that should not be imported
|
||||||
|
module.exports.ignorePackages = ({ latestBuild }) => [
|
||||||
|
// Part of yaml.js and only used for !!js functions that we don't use
|
||||||
|
require.resolve("esprima"),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Files from NPM packages that we should replace with empty file
|
||||||
|
module.exports.emptyPackages = ({ latestBuild, isHassioBuild }) =>
|
||||||
|
[
|
||||||
|
// Contains all color definitions for all material color sets.
|
||||||
|
// We don't use it
|
||||||
|
require.resolve("@polymer/paper-styles/color.js"),
|
||||||
|
require.resolve("@polymer/paper-styles/default-theme.js"),
|
||||||
|
// Loads stuff from a CDN
|
||||||
|
require.resolve("@polymer/font-roboto/roboto.js"),
|
||||||
|
require.resolve("@vaadin/vaadin-material-styles/typography.js"),
|
||||||
|
require.resolve("@vaadin/vaadin-material-styles/font-icons.js"),
|
||||||
|
// Compatibility not needed for latest builds
|
||||||
|
latestBuild &&
|
||||||
|
// wrapped in require.resolve so it blows up if file no longer exists
|
||||||
|
require.resolve(
|
||||||
|
path.resolve(paths.polymer_dir, "src/resources/compatibility.ts")
|
||||||
|
),
|
||||||
|
// This polyfill is loaded in workers to support ES5, filter it out.
|
||||||
|
latestBuild && require.resolve("proxy-polyfill/src/index.js"),
|
||||||
|
// Icons in supervisor conflict with icons in HA so we don't load.
|
||||||
|
isHassioBuild &&
|
||||||
|
require.resolve(
|
||||||
|
path.resolve(paths.polymer_dir, "src/components/ha-icon.ts")
|
||||||
|
),
|
||||||
|
isHassioBuild &&
|
||||||
|
require.resolve(
|
||||||
|
path.resolve(paths.polymer_dir, "src/components/ha-icon-picker.ts")
|
||||||
|
),
|
||||||
|
].filter(Boolean);
|
||||||
|
|
||||||
|
module.exports.definedVars = ({ isProdBuild, latestBuild, defineOverlay }) => ({
|
||||||
|
__DEV__: !isProdBuild,
|
||||||
|
__BUILD__: JSON.stringify(latestBuild ? "latest" : "es5"),
|
||||||
|
__VERSION__: JSON.stringify(env.version()),
|
||||||
|
__DEMO__: false,
|
||||||
|
__SUPERVISOR__: false,
|
||||||
|
__BACKWARDS_COMPAT__: false,
|
||||||
|
__STATIC_PATH__: "/static/",
|
||||||
|
"process.env.NODE_ENV": JSON.stringify(
|
||||||
|
isProdBuild ? "production" : "development"
|
||||||
|
),
|
||||||
|
...defineOverlay,
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports.terserOptions = (latestBuild) => ({
|
||||||
|
safari10: !latestBuild,
|
||||||
|
ecma: latestBuild ? undefined : 5,
|
||||||
|
output: { comments: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports.babelOptions = ({ latestBuild }) => ({
|
||||||
|
babelrc: false,
|
||||||
|
compact: false,
|
||||||
|
presets: [
|
||||||
|
!latestBuild && [
|
||||||
|
"@babel/preset-env",
|
||||||
|
{
|
||||||
|
useBuiltIns: "entry",
|
||||||
|
corejs: "3.15",
|
||||||
|
bugfixes: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"@babel/preset-typescript",
|
||||||
|
].filter(Boolean),
|
||||||
|
plugins: [
|
||||||
|
[
|
||||||
|
path.resolve(
|
||||||
|
paths.polymer_dir,
|
||||||
|
"build-scripts/babel-plugins/inline-constants-plugin.js"
|
||||||
|
),
|
||||||
|
{
|
||||||
|
modules: ["@mdi/js"],
|
||||||
|
ignoreModuleNotFound: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
// Part of ES2018. Converts {...a, b: 2} to Object.assign({}, a, {b: 2})
|
||||||
|
!latestBuild && [
|
||||||
|
"@babel/plugin-proposal-object-rest-spread",
|
||||||
|
{ loose: true, useBuiltIns: true },
|
||||||
|
],
|
||||||
|
// Only support the syntax, Webpack will handle it.
|
||||||
|
"@babel/plugin-syntax-import-meta",
|
||||||
|
"@babel/plugin-syntax-dynamic-import",
|
||||||
|
"@babel/plugin-syntax-top-level-await",
|
||||||
|
"@babel/plugin-proposal-optional-chaining",
|
||||||
|
"@babel/plugin-proposal-nullish-coalescing-operator",
|
||||||
|
["@babel/plugin-proposal-decorators", { decoratorsBeforeExport: true }],
|
||||||
|
["@babel/plugin-proposal-private-methods", { loose: true }],
|
||||||
|
["@babel/plugin-proposal-private-property-in-object", { loose: true }],
|
||||||
|
["@babel/plugin-proposal-class-properties", { loose: true }],
|
||||||
|
].filter(Boolean),
|
||||||
|
exclude: [
|
||||||
|
// \\ for Windows, / for Mac OS and Linux
|
||||||
|
/node_modules[\\/]core-js/,
|
||||||
|
/node_modules[\\/]webpack[\\/]buildin/,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const outputPath = (outputRoot, latestBuild) =>
|
||||||
|
path.resolve(outputRoot, latestBuild ? "frontend_latest" : "frontend_es5");
|
||||||
|
|
||||||
|
const publicPath = (latestBuild, root = "") =>
|
||||||
|
latestBuild ? `${root}/frontend_latest/` : `${root}/frontend_es5/`;
|
||||||
|
|
||||||
|
/*
|
||||||
|
BundleConfig {
|
||||||
|
// Object with entrypoints that need to be bundled
|
||||||
|
entry: { [name: string]: pathToFile },
|
||||||
|
// Folder where bundled files need to be written
|
||||||
|
outputPath: string,
|
||||||
|
// absolute url-path where bundled files can be found
|
||||||
|
publicPath: string,
|
||||||
|
// extra definitions that we need to replace in source
|
||||||
|
defineOverlay: {[name: string]: value },
|
||||||
|
// if this is a production build
|
||||||
|
isProdBuild: boolean,
|
||||||
|
// If we're targeting latest browsers
|
||||||
|
latestBuild: boolean,
|
||||||
|
// If we're doing a stats build (create nice chunk names)
|
||||||
|
isStatsBuild: boolean,
|
||||||
|
// Names of entrypoints that should not be hashed
|
||||||
|
dontHash: Set<string>
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports.config = {
|
||||||
|
app({ isProdBuild, latestBuild, isStatsBuild, isWDS }) {
|
||||||
|
return {
|
||||||
|
entry: {
|
||||||
|
service_worker: "./src/entrypoints/service_worker.ts",
|
||||||
|
app: "./src/entrypoints/app.ts",
|
||||||
|
authorize: "./src/entrypoints/authorize.ts",
|
||||||
|
onboarding: "./src/entrypoints/onboarding.ts",
|
||||||
|
core: "./src/entrypoints/core.ts",
|
||||||
|
"custom-panel": "./src/entrypoints/custom-panel.ts",
|
||||||
|
},
|
||||||
|
outputPath: outputPath(paths.app_output_root, latestBuild),
|
||||||
|
publicPath: publicPath(latestBuild),
|
||||||
|
isProdBuild,
|
||||||
|
latestBuild,
|
||||||
|
isStatsBuild,
|
||||||
|
isWDS,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
demo({ isProdBuild, latestBuild, isStatsBuild }) {
|
||||||
|
return {
|
||||||
|
entry: {
|
||||||
|
main: path.resolve(paths.demo_dir, "src/entrypoint.ts"),
|
||||||
|
},
|
||||||
|
outputPath: outputPath(paths.demo_output_root, latestBuild),
|
||||||
|
publicPath: publicPath(latestBuild),
|
||||||
|
defineOverlay: {
|
||||||
|
__VERSION__: JSON.stringify(`DEMO-${env.version()}`),
|
||||||
|
__DEMO__: true,
|
||||||
|
},
|
||||||
|
isProdBuild,
|
||||||
|
latestBuild,
|
||||||
|
isStatsBuild,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
cast({ isProdBuild, latestBuild }) {
|
||||||
|
const entry = {
|
||||||
|
launcher: path.resolve(paths.cast_dir, "src/launcher/entrypoint.ts"),
|
||||||
|
media: path.resolve(paths.cast_dir, "src/media/entrypoint.ts"),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (latestBuild) {
|
||||||
|
entry.receiver = path.resolve(
|
||||||
|
paths.cast_dir,
|
||||||
|
"src/receiver/entrypoint.ts"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
entry,
|
||||||
|
outputPath: outputPath(paths.cast_output_root, latestBuild),
|
||||||
|
publicPath: publicPath(latestBuild),
|
||||||
|
isProdBuild,
|
||||||
|
latestBuild,
|
||||||
|
defineOverlay: {
|
||||||
|
__BACKWARDS_COMPAT__: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
hassio({ isProdBuild, latestBuild }) {
|
||||||
|
return {
|
||||||
|
entry: {
|
||||||
|
entrypoint: path.resolve(paths.hassio_dir, "src/entrypoint.ts"),
|
||||||
|
},
|
||||||
|
outputPath: outputPath(paths.hassio_output_root, latestBuild),
|
||||||
|
publicPath: publicPath(latestBuild, paths.hassio_publicPath),
|
||||||
|
isProdBuild,
|
||||||
|
latestBuild,
|
||||||
|
isHassioBuild: true,
|
||||||
|
defineOverlay: {
|
||||||
|
__SUPERVISOR__: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
gallery({ isProdBuild, latestBuild }) {
|
||||||
|
return {
|
||||||
|
entry: {
|
||||||
|
entrypoint: path.resolve(paths.gallery_dir, "src/entrypoint.js"),
|
||||||
|
},
|
||||||
|
outputPath: outputPath(paths.gallery_output_root, latestBuild),
|
||||||
|
publicPath: publicPath(latestBuild),
|
||||||
|
isProdBuild,
|
||||||
|
latestBuild,
|
||||||
|
defineOverlay: {
|
||||||
|
__DEMO__: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
@ -1,34 +1,36 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const paths = require("./paths.cjs");
|
const paths = require("./paths.js");
|
||||||
|
|
||||||
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() {
|
isTest() {
|
||||||
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",
|
|
||||||
},
|
|
||||||
});
|
|
@ -1,14 +1,19 @@
|
|||||||
import gulp from "gulp";
|
// Run HA develop mode
|
||||||
import env from "../env.cjs";
|
const gulp = require("gulp");
|
||||||
import "./clean.js";
|
|
||||||
import "./compress.js";
|
const env = require("../env");
|
||||||
import "./entry-html.js";
|
|
||||||
import "./gather-static.js";
|
require("./clean.js");
|
||||||
import "./gen-icons-json.js";
|
require("./translations.js");
|
||||||
import "./locale-data.js";
|
require("./locale-data.js");
|
||||||
import "./service-worker.js";
|
require("./gen-icons-json.js");
|
||||||
import "./translations.js";
|
require("./gather-static.js");
|
||||||
import "./rspack.js";
|
require("./compress.js");
|
||||||
|
require("./webpack.js");
|
||||||
|
require("./service-worker.js");
|
||||||
|
require("./entry-html.js");
|
||||||
|
require("./rollup.js");
|
||||||
|
require("./wds.js");
|
||||||
|
|
||||||
gulp.task(
|
gulp.task(
|
||||||
"develop-app",
|
"develop-app",
|
||||||
@ -20,12 +25,17 @@ gulp.task(
|
|||||||
gulp.parallel(
|
gulp.parallel(
|
||||||
"gen-service-worker-app-dev",
|
"gen-service-worker-app-dev",
|
||||||
"gen-icons-json",
|
"gen-icons-json",
|
||||||
"gen-pages-app-dev",
|
"gen-pages-dev",
|
||||||
|
"gen-index-app-dev",
|
||||||
"build-translations",
|
"build-translations",
|
||||||
"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 +48,13 @@ 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.isTest() ? [] : ["compress-app"]),
|
||||||
)
|
gulp.parallel(
|
||||||
);
|
"gen-pages-prod",
|
||||||
|
"gen-index-app-prod",
|
||||||
gulp.task(
|
"gen-service-worker-app-prod"
|
||||||
"analyze-app",
|
)
|
||||||
gulp.series(
|
|
||||||
async function setEnv() {
|
|
||||||
process.env.STATS = "1";
|
|
||||||
},
|
|
||||||
"clean",
|
|
||||||
"rspack-prod-app"
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
@ -1,10 +1,14 @@
|
|||||||
import gulp from "gulp";
|
const gulp = require("gulp");
|
||||||
import "./clean.js";
|
|
||||||
import "./entry-html.js";
|
const env = require("../env");
|
||||||
import "./gather-static.js";
|
|
||||||
import "./service-worker.js";
|
require("./clean.js");
|
||||||
import "./translations.js";
|
require("./translations.js");
|
||||||
import "./rspack.js";
|
require("./gather-static.js");
|
||||||
|
require("./webpack.js");
|
||||||
|
require("./service-worker.js");
|
||||||
|
require("./entry-html.js");
|
||||||
|
require("./rollup.js");
|
||||||
|
|
||||||
gulp.task(
|
gulp.task(
|
||||||
"develop-cast",
|
"develop-cast",
|
||||||
@ -16,8 +20,8 @@ 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",
|
||||||
"gen-pages-cast-dev",
|
"gen-index-cast-dev",
|
||||||
"rspack-dev-server-cast"
|
env.useRollup() ? "rollup-dev-server-cast" : "webpack-dev-server-cast"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -31,7 +35,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-index-cast-prod"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
@ -1,51 +1,36 @@
|
|||||||
import { deleteSync } from "del";
|
const del = require("del");
|
||||||
import gulp from "gulp";
|
const gulp = require("gulp");
|
||||||
import paths from "../paths.cjs";
|
const paths = require("../paths");
|
||||||
import "./translations.js";
|
require("./translations");
|
||||||
|
|
||||||
gulp.task(
|
gulp.task(
|
||||||
"clean",
|
"clean",
|
||||||
gulp.parallel("clean-translations", async () =>
|
gulp.parallel("clean-translations", () =>
|
||||||
deleteSync([paths.app_output_root, paths.build_dir])
|
del([paths.app_output_root, paths.build_dir])
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
gulp.task(
|
gulp.task(
|
||||||
"clean-demo",
|
"clean-demo",
|
||||||
gulp.parallel("clean-translations", async () =>
|
gulp.parallel("clean-translations", () =>
|
||||||
deleteSync([paths.demo_output_root, paths.build_dir])
|
del([paths.demo_output_root, paths.build_dir])
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
gulp.task(
|
gulp.task(
|
||||||
"clean-cast",
|
"clean-cast",
|
||||||
gulp.parallel("clean-translations", async () =>
|
gulp.parallel("clean-translations", () =>
|
||||||
deleteSync([paths.cast_output_root, paths.build_dir])
|
del([paths.cast_output_root, paths.build_dir])
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
gulp.task("clean-hassio", async () =>
|
gulp.task("clean-hassio", () =>
|
||||||
deleteSync([paths.hassio_output_root, paths.build_dir])
|
del([paths.hassio_output_root, paths.build_dir])
|
||||||
);
|
);
|
||||||
|
|
||||||
gulp.task(
|
gulp.task(
|
||||||
"clean-gallery",
|
"clean-gallery",
|
||||||
gulp.parallel("clean-translations", async () =>
|
gulp.parallel("clean-translations", () =>
|
||||||
deleteSync([
|
del([paths.gallery_output_root, paths.gallery_build, paths.build_dir])
|
||||||
paths.gallery_output_root,
|
|
||||||
paths.gallery_build,
|
|
||||||
paths.build_dir,
|
|
||||||
])
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
gulp.task(
|
|
||||||
"clean-landing-page",
|
|
||||||
gulp.parallel("clean-translations", async () =>
|
|
||||||
deleteSync([
|
|
||||||
paths.landingPage_output_root,
|
|
||||||
paths.landingPage_build,
|
|
||||||
paths.build_dir,
|
|
||||||
])
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
@ -1,86 +1,45 @@
|
|||||||
// Tasks to compress
|
// Tasks to compress
|
||||||
|
|
||||||
import { constants } from "node:zlib";
|
const gulp = require("gulp");
|
||||||
import gulp from "gulp";
|
const zopfli = require("gulp-zopfli-green");
|
||||||
import brotli from "gulp-brotli";
|
const merge = require("merge-stream");
|
||||||
import zopfli from "gulp-zopfli-green";
|
const path = require("path");
|
||||||
import paths from "../paths.cjs";
|
const paths = require("../paths");
|
||||||
|
|
||||||
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) =>
|
gulp.task("compress-app", function compressApp() {
|
||||||
gulp
|
const jsLatest = gulp
|
||||||
.src([`${modernDir}/**/${filesGlob}`, `${rootDir}/sw-modern.js`], {
|
.src(path.resolve(paths.app_output_latest, "**/*.js"))
|
||||||
base: rootDir,
|
.pipe(zopfli(zopfliOptions))
|
||||||
allowEmpty: true,
|
.pipe(gulp.dest(paths.app_output_latest));
|
||||||
})
|
|
||||||
.pipe(compress === "zopfli" ? zopfli(zopfliOptions) : brotli(brotliOptions))
|
|
||||||
.pipe(gulp.dest(rootDir));
|
|
||||||
|
|
||||||
const compressOther = (rootDir, modernDir, compress) =>
|
const jsEs5 = gulp
|
||||||
gulp
|
.src(path.resolve(paths.app_output_es5, "**/*.js"))
|
||||||
.src(
|
.pipe(zopfli(zopfliOptions))
|
||||||
[
|
.pipe(gulp.dest(paths.app_output_es5));
|
||||||
`${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 = () =>
|
const polyfills = gulp
|
||||||
compressModern(paths.app_output_root, paths.app_output_latest, "brotli");
|
.src(path.resolve(paths.app_output_static, "polyfills/*.js"))
|
||||||
const compressAppModernZopfli = () =>
|
.pipe(zopfli(zopfliOptions))
|
||||||
compressModern(paths.app_output_root, paths.app_output_latest, "zopfli");
|
.pipe(gulp.dest(path.resolve(paths.app_output_static, "polyfills")));
|
||||||
|
|
||||||
const compressHassioModernBrotli = () =>
|
const translations = gulp
|
||||||
compressModern(
|
.src(path.resolve(paths.app_output_static, "translations/**/*.json"))
|
||||||
paths.hassio_output_root,
|
.pipe(zopfli(zopfliOptions))
|
||||||
paths.hassio_output_latest,
|
.pipe(gulp.dest(path.resolve(paths.app_output_static, "translations")));
|
||||||
"brotli"
|
|
||||||
);
|
|
||||||
const compressHassioModernZopfli = () =>
|
|
||||||
compressModern(
|
|
||||||
paths.hassio_output_root,
|
|
||||||
paths.hassio_output_latest,
|
|
||||||
"zopfli"
|
|
||||||
);
|
|
||||||
|
|
||||||
const compressAppOtherBrotli = () =>
|
const icons = gulp
|
||||||
compressOther(paths.app_output_root, paths.app_output_latest, "brotli");
|
.src(path.resolve(paths.app_output_static, "mdi/*.json"))
|
||||||
const compressAppOtherZopfli = () =>
|
.pipe(zopfli(zopfliOptions))
|
||||||
compressOther(paths.app_output_root, paths.app_output_latest, "zopfli");
|
.pipe(gulp.dest(path.resolve(paths.app_output_static, "mdi")));
|
||||||
|
|
||||||
const compressHassioOtherBrotli = () =>
|
return merge(jsLatest, jsEs5, polyfills, translations, icons);
|
||||||
compressOther(paths.hassio_output_root, paths.hassio_output_latest, "brotli");
|
});
|
||||||
const compressHassioOtherZopfli = () =>
|
|
||||||
compressOther(paths.hassio_output_root, paths.hassio_output_latest, "zopfli");
|
|
||||||
|
|
||||||
gulp.task(
|
gulp.task("compress-hassio", function compressApp() {
|
||||||
"compress-app",
|
return gulp
|
||||||
gulp.parallel(
|
.src(path.resolve(paths.hassio_output_root, "**/*.js"))
|
||||||
compressAppModernBrotli,
|
.pipe(zopfli(zopfliOptions))
|
||||||
compressAppOtherBrotli,
|
.pipe(gulp.dest(paths.hassio_output_root));
|
||||||
compressAppModernZopfli,
|
});
|
||||||
compressAppOtherZopfli
|
|
||||||
)
|
|
||||||
);
|
|
||||||
gulp.task(
|
|
||||||
"compress-hassio",
|
|
||||||
gulp.parallel(
|
|
||||||
compressHassioModernBrotli,
|
|
||||||
compressHassioOtherBrotli,
|
|
||||||
compressHassioModernZopfli,
|
|
||||||
compressHassioOtherZopfli
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
@ -1,11 +1,16 @@
|
|||||||
import gulp from "gulp";
|
// Run demo develop mode
|
||||||
import "./clean.js";
|
const gulp = require("gulp");
|
||||||
import "./entry-html.js";
|
|
||||||
import "./gather-static.js";
|
const env = require("../env");
|
||||||
import "./gen-icons-json.js";
|
|
||||||
import "./service-worker.js";
|
require("./clean.js");
|
||||||
import "./translations.js";
|
require("./translations.js");
|
||||||
import "./rspack.js";
|
require("./gen-icons-json.js");
|
||||||
|
require("./gather-static.js");
|
||||||
|
require("./webpack.js");
|
||||||
|
require("./service-worker.js");
|
||||||
|
require("./entry-html.js");
|
||||||
|
require("./rollup.js");
|
||||||
|
|
||||||
gulp.task(
|
gulp.task(
|
||||||
"develop-demo",
|
"develop-demo",
|
||||||
@ -17,12 +22,12 @@ gulp.task(
|
|||||||
"translations-enable-merge-backend",
|
"translations-enable-merge-backend",
|
||||||
gulp.parallel(
|
gulp.parallel(
|
||||||
"gen-icons-json",
|
"gen-icons-json",
|
||||||
"gen-pages-demo-dev",
|
"gen-index-demo-dev",
|
||||||
"build-translations",
|
"build-translations",
|
||||||
"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 +42,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-index-demo-prod"
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
gulp.task(
|
|
||||||
"analyze-demo",
|
|
||||||
gulp.series(
|
|
||||||
async function setEnv() {
|
|
||||||
process.env.STATS = "1";
|
|
||||||
},
|
|
||||||
"clean",
|
|
||||||
"rspack-prod-demo"
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
@ -1,181 +0,0 @@
|
|||||||
import fs from "fs/promises";
|
|
||||||
import gulp from "gulp";
|
|
||||||
import path from "path";
|
|
||||||
import mapStream from "map-stream";
|
|
||||||
import transform from "gulp-json-transform";
|
|
||||||
import { LokaliseApi } from "@lokalise/node-api";
|
|
||||||
import JSZip from "jszip";
|
|
||||||
|
|
||||||
const inDir = "translations";
|
|
||||||
const inDirFrontend = `${inDir}/frontend`;
|
|
||||||
const inDirBackend = `${inDir}/backend`;
|
|
||||||
const srcMeta = "src/translations/translationMetadata.json";
|
|
||||||
const encoding = "utf8";
|
|
||||||
|
|
||||||
function hasHtml(data) {
|
|
||||||
return /<\S*>/i.test(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
function recursiveCheckHasHtml(file, data, errors, recKey) {
|
|
||||||
Object.keys(data).forEach(function (key) {
|
|
||||||
if (typeof data[key] === "object") {
|
|
||||||
const nextRecKey = recKey ? `${recKey}.${key}` : key;
|
|
||||||
recursiveCheckHasHtml(file, data[key], errors, nextRecKey);
|
|
||||||
} else if (hasHtml(data[key])) {
|
|
||||||
errors.push(`HTML found in ${file.path} at key ${recKey}.${key}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkHtml() {
|
|
||||||
const errors = [];
|
|
||||||
|
|
||||||
return mapStream(function (file, cb) {
|
|
||||||
const content = file.contents;
|
|
||||||
let error;
|
|
||||||
if (content) {
|
|
||||||
if (hasHtml(String(content))) {
|
|
||||||
const data = JSON.parse(String(content));
|
|
||||||
recursiveCheckHasHtml(file, data, errors);
|
|
||||||
if (errors.length > 0) {
|
|
||||||
error = errors.join("\r\n");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cb(error, file);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function convertBackendTranslations(data, _file) {
|
|
||||||
const output = { component: {} };
|
|
||||||
if (!data.component) {
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
Object.keys(data.component).forEach((domain) => {
|
|
||||||
if (!("entity_component" in data.component[domain])) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
output.component[domain] = { entity_component: {} };
|
|
||||||
Object.keys(data.component[domain].entity_component).forEach((key) => {
|
|
||||||
output.component[domain].entity_component[key] =
|
|
||||||
data.component[domain].entity_component[key];
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
|
|
||||||
gulp.task("convert-backend-translations", function () {
|
|
||||||
return gulp
|
|
||||||
.src([`${inDirBackend}/*.json`])
|
|
||||||
.pipe(transform((data, file) => convertBackendTranslations(data, file)))
|
|
||||||
.pipe(gulp.dest(inDirBackend));
|
|
||||||
});
|
|
||||||
|
|
||||||
gulp.task("check-translations-html", function () {
|
|
||||||
return gulp
|
|
||||||
.src([`${inDirFrontend}/*.json`, `${inDirBackend}/*.json`])
|
|
||||||
.pipe(checkHtml());
|
|
||||||
});
|
|
||||||
|
|
||||||
gulp.task("check-all-files-exist", async function () {
|
|
||||||
const file = await fs.readFile(srcMeta, { encoding });
|
|
||||||
const meta = JSON.parse(file);
|
|
||||||
const writings = [];
|
|
||||||
Object.keys(meta).forEach((lang) => {
|
|
||||||
writings.push(
|
|
||||||
fs.writeFile(`${inDirFrontend}/${lang}.json`, JSON.stringify({}), {
|
|
||||||
flag: "wx",
|
|
||||||
}),
|
|
||||||
fs.writeFile(`${inDirBackend}/${lang}.json`, JSON.stringify({}), {
|
|
||||||
flag: "wx",
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
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(
|
|
||||||
"download-translations",
|
|
||||||
gulp.series(
|
|
||||||
"fetch-lokalise",
|
|
||||||
"convert-backend-translations",
|
|
||||||
"check-translations-html",
|
|
||||||
"check-all-files-exist"
|
|
||||||
)
|
|
||||||
);
|
|
66
build-scripts/gulp/download_translations.js
Normal file
66
build-scripts/gulp/download_translations.js
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
const gulp = require("gulp");
|
||||||
|
const fs = require("fs");
|
||||||
|
const mapStream = require("map-stream");
|
||||||
|
|
||||||
|
const inDirFrontend = "translations/frontend";
|
||||||
|
const inDirBackend = "translations/backend";
|
||||||
|
const srcMeta = "src/translations/translationMetadata.json";
|
||||||
|
const encoding = "utf8";
|
||||||
|
|
||||||
|
function hasHtml(data) {
|
||||||
|
return /<[a-z][\s\S]*>/i.test(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
function recursiveCheckHasHtml(file, data, errors, recKey) {
|
||||||
|
Object.keys(data).forEach(function (key) {
|
||||||
|
if (typeof data[key] === "object") {
|
||||||
|
const nextRecKey = recKey ? `${recKey}.${key}` : key;
|
||||||
|
recursiveCheckHasHtml(file, data[key], errors, nextRecKey);
|
||||||
|
} else if (hasHtml(data[key])) {
|
||||||
|
errors.push(`HTML found in ${file.path} at key ${recKey}.${key}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkHtml() {
|
||||||
|
const errors = [];
|
||||||
|
|
||||||
|
return mapStream(function (file, cb) {
|
||||||
|
const content = file.contents;
|
||||||
|
let error;
|
||||||
|
if (content) {
|
||||||
|
if (hasHtml(String(content))) {
|
||||||
|
const data = JSON.parse(String(content));
|
||||||
|
recursiveCheckHasHtml(file, data, errors);
|
||||||
|
if (errors.length > 0) {
|
||||||
|
error = errors.join("\r\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cb(error, file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backend translations do not currently pass HTML check so are excluded here for now
|
||||||
|
gulp.task("check-translations-html", function () {
|
||||||
|
return gulp.src([`${inDirFrontend}/*.json`]).pipe(checkHtml());
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task("check-all-files-exist", function () {
|
||||||
|
const file = fs.readFileSync(srcMeta, { encoding });
|
||||||
|
const meta = JSON.parse(file);
|
||||||
|
Object.keys(meta).forEach((lang) => {
|
||||||
|
if (!fs.existsSync(`${inDirFrontend}/${lang}.json`)) {
|
||||||
|
fs.writeFileSync(`${inDirFrontend}/${lang}.json`, JSON.stringify({}));
|
||||||
|
}
|
||||||
|
if (!fs.existsSync(`${inDirBackend}/${lang}.json`)) {
|
||||||
|
fs.writeFileSync(`${inDirBackend}/${lang}.json`, JSON.stringify({}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return Promise.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task(
|
||||||
|
"check-downloaded-translations",
|
||||||
|
gulp.series("check-translations-html", "check-all-files-exist")
|
||||||
|
);
|
@ -1,293 +1,344 @@
|
|||||||
// Tasks to generate entry HTML
|
// Tasks to generate entry HTML
|
||||||
|
/* eslint-disable import/no-dynamic-require */
|
||||||
|
/* eslint-disable global-require */
|
||||||
|
const gulp = require("gulp");
|
||||||
|
const fs = require("fs-extra");
|
||||||
|
const path = require("path");
|
||||||
|
const template = require("lodash.template");
|
||||||
|
const minify = require("html-minifier").minify;
|
||||||
|
const paths = require("../paths.js");
|
||||||
|
const env = require("../env.js");
|
||||||
|
|
||||||
import {
|
const templatePath = (tpl) =>
|
||||||
applyVersionsToRegexes,
|
path.resolve(paths.polymer_dir, "src/html/", `${tpl}.html.template`);
|
||||||
compileRegex,
|
|
||||||
getPreUserAgentRegexes,
|
|
||||||
} from "browserslist-useragent-regexp";
|
|
||||||
import fs from "fs-extra";
|
|
||||||
import gulp from "gulp";
|
|
||||||
import { minify } from "html-minifier-terser";
|
|
||||||
import template from "lodash.template";
|
|
||||||
import { dirname, extname, resolve } from "node:path";
|
|
||||||
import { htmlMinifierOptions, terserOptions } from "../bundle.cjs";
|
|
||||||
import paths from "../paths.cjs";
|
|
||||||
|
|
||||||
// macOS companion app has no way to obtain the Safari version used by WKWebView,
|
const readFile = (pth) => fs.readFileSync(pth).toString();
|
||||||
// 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 renderTemplate = (pth, data = {}, pathFunc = templatePath) => {
|
||||||
const browserRegexes = getPreUserAgentRegexes({
|
const compiled = template(readFile(pathFunc(pth)));
|
||||||
env: "modern",
|
|
||||||
allowHigherVersions: true,
|
|
||||||
mobileToDesktop: true,
|
|
||||||
throwOnMissing: true,
|
|
||||||
});
|
|
||||||
const minSafariVersion = browserRegexes.find(
|
|
||||||
(regex) => regex.family === "safari"
|
|
||||||
)?.matchedVersions[0][0];
|
|
||||||
const minMacOSVersion = SAFARI_TO_MACOS[minSafariVersion];
|
|
||||||
if (!minMacOSVersion) {
|
|
||||||
throw Error(
|
|
||||||
`Could not find minimum MacOS version for Safari ${minSafariVersion}.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const haMacOSRegex = applyVersionsToRegexes(
|
|
||||||
[
|
|
||||||
{
|
|
||||||
family: "ha_macos",
|
|
||||||
regex: HA_MACOS_REGEX,
|
|
||||||
matchedVersions: [minMacOSVersion],
|
|
||||||
requestVersions: [minMacOSVersion],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
{ ignorePatch: true, allowHigherVersions: true }
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
modernRegex: compileRegex(browserRegexes.concat(haMacOSRegex)).toString(),
|
|
||||||
hassUrl: process.env.HASS_URL || "",
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderTemplate = (templateFile, data = {}) => {
|
|
||||||
const compiled = template(
|
|
||||||
fs.readFileSync(templateFile, { encoding: "utf-8" })
|
|
||||||
);
|
|
||||||
return compiled({
|
return compiled({
|
||||||
...data,
|
...data,
|
||||||
// Resolve any child/nested templates relative to the parent and pass the same data
|
useRollup: env.useRollup(),
|
||||||
renderTemplate: (childTemplate) =>
|
useWDS: env.useWDS(),
|
||||||
renderTemplate(resolve(dirname(templateFile), childTemplate), data),
|
renderTemplate,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const WRAP_TAGS = { ".js": "script", ".css": "style" };
|
const renderDemoTemplate = (pth, data = {}) =>
|
||||||
|
renderTemplate(pth, data, (tpl) =>
|
||||||
|
path.resolve(paths.demo_dir, "src/html/", `${tpl}.html.template`)
|
||||||
|
);
|
||||||
|
|
||||||
const minifyHtml = (content, ext) => {
|
const renderCastTemplate = (pth, data = {}) =>
|
||||||
const wrapTag = WRAP_TAGS[ext] || "";
|
renderTemplate(pth, data, (tpl) =>
|
||||||
const begTag = wrapTag && `<${wrapTag}>`;
|
path.resolve(paths.cast_dir, "src/html/", `${tpl}.html.template`)
|
||||||
const endTag = wrapTag && `</${wrapTag}>`;
|
|
||||||
return minify(begTag + content + endTag, {
|
|
||||||
...htmlMinifierOptions,
|
|
||||||
conservativeCollapse: false,
|
|
||||||
minifyJS: terserOptions({
|
|
||||||
latestBuild: false, // Shared scripts should be ES5
|
|
||||||
isTestBuild: true, // Don't need source maps
|
|
||||||
}),
|
|
||||||
}).then((wrapped) =>
|
|
||||||
wrapTag ? wrapped.slice(begTag.length, -endTag.length) : wrapped
|
|
||||||
);
|
);
|
||||||
};
|
|
||||||
|
|
||||||
// Function to generate a dev task for each project's configuration
|
const renderGalleryTemplate = (pth, data = {}) =>
|
||||||
const genPagesDevTask =
|
renderTemplate(pth, data, (tpl) =>
|
||||||
(
|
path.resolve(paths.gallery_dir, "src/html/", `${tpl}.html.template`)
|
||||||
pageEntries,
|
|
||||||
inputRoot,
|
|
||||||
outputRoot,
|
|
||||||
inputSub = "src/html",
|
|
||||||
publicRoot = ""
|
|
||||||
) =>
|
|
||||||
async () => {
|
|
||||||
const commonVars = getCommonTemplateVars();
|
|
||||||
for (const [page, entries] of Object.entries(pageEntries)) {
|
|
||||||
const content = renderTemplate(
|
|
||||||
resolve(inputRoot, inputSub, `${page}.template`),
|
|
||||||
{
|
|
||||||
...commonVars,
|
|
||||||
latestEntryJS: entries.map(
|
|
||||||
(entry) => `${publicRoot}/frontend_latest/${entry}.js`
|
|
||||||
),
|
|
||||||
es5EntryJS: entries.map(
|
|
||||||
(entry) => `${publicRoot}/frontend_es5/${entry}.js`
|
|
||||||
),
|
|
||||||
latestCustomPanelJS: `${publicRoot}/frontend_latest/custom-panel.js`,
|
|
||||||
es5CustomPanelJS: `${publicRoot}/frontend_es5/custom-panel.js`,
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
fs.outputFileSync(resolve(outputRoot, page), content);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Same as previous but for production builds
|
const minifyHtml = (content) =>
|
||||||
// (includes minification and hashed file names from manifest)
|
minify(content, {
|
||||||
const genPagesProdTask =
|
collapseWhitespace: true,
|
||||||
(
|
minifyJS: true,
|
||||||
pageEntries,
|
minifyCSS: true,
|
||||||
inputRoot,
|
removeComments: true,
|
||||||
outputRoot,
|
});
|
||||||
outputLatest,
|
|
||||||
outputES5,
|
const PAGES = ["onboarding", "authorize"];
|
||||||
inputSub = "src/html"
|
|
||||||
) =>
|
gulp.task("gen-pages-dev", (done) => {
|
||||||
async () => {
|
for (const page of PAGES) {
|
||||||
const latestManifest = fs.readJsonSync(
|
const content = renderTemplate(page, {
|
||||||
resolve(outputLatest, "manifest.json")
|
latestPageJS: `/frontend_latest/${page}.js`,
|
||||||
);
|
|
||||||
const es5Manifest = outputES5
|
es5PageJS: `/frontend_es5/${page}.js`,
|
||||||
? fs.readJsonSync(resolve(outputES5, "manifest.json"))
|
});
|
||||||
: {};
|
|
||||||
const commonVars = getCommonTemplateVars();
|
fs.outputFileSync(
|
||||||
const minifiedHTML = [];
|
path.resolve(paths.app_output_root, `${page}.html`),
|
||||||
for (const [page, entries] of Object.entries(pageEntries)) {
|
content
|
||||||
const content = renderTemplate(
|
|
||||||
resolve(inputRoot, inputSub, `${page}.template`),
|
|
||||||
{
|
|
||||||
...commonVars,
|
|
||||||
latestEntryJS: entries.map((entry) => latestManifest[`${entry}.js`]),
|
|
||||||
es5EntryJS: entries.map((entry) => es5Manifest[`${entry}.js`]),
|
|
||||||
latestCustomPanelJS: latestManifest["custom-panel.js"],
|
|
||||||
es5CustomPanelJS: es5Manifest["custom-panel.js"],
|
|
||||||
}
|
|
||||||
);
|
|
||||||
minifiedHTML.push(
|
|
||||||
minifyHtml(content, extname(page)).then((minified) =>
|
|
||||||
fs.outputFileSync(resolve(outputRoot, page), minified)
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
await Promise.all(minifiedHTML);
|
done();
|
||||||
};
|
});
|
||||||
|
|
||||||
// Map HTML pages to their required entrypoints
|
gulp.task("gen-pages-prod", (done) => {
|
||||||
const APP_PAGE_ENTRIES = {
|
const latestManifest = require(path.resolve(
|
||||||
"authorize.html": ["authorize"],
|
|
||||||
"onboarding.html": ["onboarding"],
|
|
||||||
"index.html": ["core", "app"],
|
|
||||||
};
|
|
||||||
|
|
||||||
gulp.task(
|
|
||||||
"gen-pages-app-dev",
|
|
||||||
genPagesDevTask(APP_PAGE_ENTRIES, paths.root_dir, paths.app_output_root)
|
|
||||||
);
|
|
||||||
|
|
||||||
gulp.task(
|
|
||||||
"gen-pages-app-prod",
|
|
||||||
genPagesProdTask(
|
|
||||||
APP_PAGE_ENTRIES,
|
|
||||||
paths.root_dir,
|
|
||||||
paths.app_output_root,
|
|
||||||
paths.app_output_latest,
|
paths.app_output_latest,
|
||||||
paths.app_output_es5
|
"manifest.json"
|
||||||
)
|
));
|
||||||
);
|
const es5Manifest = require(path.resolve(
|
||||||
|
paths.app_output_es5,
|
||||||
|
"manifest.json"
|
||||||
|
));
|
||||||
|
|
||||||
const CAST_PAGE_ENTRIES = {
|
for (const page of PAGES) {
|
||||||
"faq.html": ["launcher"],
|
const content = renderTemplate(page, {
|
||||||
"index.html": ["launcher"],
|
latestPageJS: latestManifest[`${page}.js`],
|
||||||
"media.html": ["media"],
|
|
||||||
"receiver.html": ["receiver"],
|
|
||||||
};
|
|
||||||
|
|
||||||
gulp.task(
|
es5PageJS: es5Manifest[`${page}.js`],
|
||||||
"gen-pages-cast-dev",
|
});
|
||||||
genPagesDevTask(CAST_PAGE_ENTRIES, paths.cast_dir, paths.cast_output_root)
|
|
||||||
);
|
|
||||||
|
|
||||||
gulp.task(
|
fs.outputFileSync(
|
||||||
"gen-pages-cast-prod",
|
path.resolve(paths.app_output_root, `${page}.html`),
|
||||||
genPagesProdTask(
|
minifyHtml(content)
|
||||||
CAST_PAGE_ENTRIES,
|
);
|
||||||
paths.cast_dir,
|
}
|
||||||
paths.cast_output_root,
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task("gen-index-app-dev", (done) => {
|
||||||
|
let latestAppJS, latestCoreJS, latestCustomPanelJS;
|
||||||
|
|
||||||
|
if (env.useWDS()) {
|
||||||
|
latestAppJS = "http://localhost:8000/src/entrypoints/app.ts";
|
||||||
|
latestCoreJS = "http://localhost:8000/src/entrypoints/core.ts";
|
||||||
|
latestCustomPanelJS =
|
||||||
|
"http://localhost:8000/src/entrypoints/custom-panel.ts";
|
||||||
|
} else {
|
||||||
|
latestAppJS = "/frontend_latest/app.js";
|
||||||
|
latestCoreJS = "/frontend_latest/core.js";
|
||||||
|
latestCustomPanelJS = "/frontend_latest/custom-panel.js";
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = renderTemplate("index", {
|
||||||
|
latestAppJS,
|
||||||
|
latestCoreJS,
|
||||||
|
latestCustomPanelJS,
|
||||||
|
|
||||||
|
es5AppJS: "/frontend_es5/app.js",
|
||||||
|
es5CoreJS: "/frontend_es5/core.js",
|
||||||
|
es5CustomPanelJS: "/frontend_es5/custom-panel.js",
|
||||||
|
}).replace(/#THEMEC/g, "{{ theme_color }}");
|
||||||
|
|
||||||
|
fs.outputFileSync(path.resolve(paths.app_output_root, "index.html"), content);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task("gen-index-app-prod", (done) => {
|
||||||
|
const latestManifest = require(path.resolve(
|
||||||
|
paths.app_output_latest,
|
||||||
|
"manifest.json"
|
||||||
|
));
|
||||||
|
const es5Manifest = require(path.resolve(
|
||||||
|
paths.app_output_es5,
|
||||||
|
"manifest.json"
|
||||||
|
));
|
||||||
|
const content = renderTemplate("index", {
|
||||||
|
latestAppJS: latestManifest["app.js"],
|
||||||
|
latestCoreJS: latestManifest["core.js"],
|
||||||
|
latestCustomPanelJS: latestManifest["custom-panel.js"],
|
||||||
|
|
||||||
|
es5AppJS: es5Manifest["app.js"],
|
||||||
|
es5CoreJS: es5Manifest["core.js"],
|
||||||
|
es5CustomPanelJS: es5Manifest["custom-panel.js"],
|
||||||
|
});
|
||||||
|
const minified = minifyHtml(content).replace(/#THEMEC/g, "{{ theme_color }}");
|
||||||
|
|
||||||
|
fs.outputFileSync(
|
||||||
|
path.resolve(paths.app_output_root, "index.html"),
|
||||||
|
minified
|
||||||
|
);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task("gen-index-cast-dev", (done) => {
|
||||||
|
const contentReceiver = renderCastTemplate("receiver", {
|
||||||
|
latestReceiverJS: "/frontend_latest/receiver.js",
|
||||||
|
});
|
||||||
|
fs.outputFileSync(
|
||||||
|
path.resolve(paths.cast_output_root, "receiver.html"),
|
||||||
|
contentReceiver
|
||||||
|
);
|
||||||
|
|
||||||
|
const contentMedia = renderCastTemplate("media", {
|
||||||
|
latestMediaJS: "/frontend_latest/media.js",
|
||||||
|
es5MediaJS: "/frontend_es5/media.js",
|
||||||
|
});
|
||||||
|
fs.outputFileSync(
|
||||||
|
path.resolve(paths.cast_output_root, "media.html"),
|
||||||
|
contentMedia
|
||||||
|
);
|
||||||
|
|
||||||
|
const contentFAQ = renderCastTemplate("launcher-faq", {
|
||||||
|
latestLauncherJS: "/frontend_latest/launcher.js",
|
||||||
|
es5LauncherJS: "/frontend_es5/launcher.js",
|
||||||
|
});
|
||||||
|
fs.outputFileSync(
|
||||||
|
path.resolve(paths.cast_output_root, "faq.html"),
|
||||||
|
contentFAQ
|
||||||
|
);
|
||||||
|
|
||||||
|
const contentLauncher = renderCastTemplate("launcher", {
|
||||||
|
latestLauncherJS: "/frontend_latest/launcher.js",
|
||||||
|
es5LauncherJS: "/frontend_es5/launcher.js",
|
||||||
|
});
|
||||||
|
fs.outputFileSync(
|
||||||
|
path.resolve(paths.cast_output_root, "index.html"),
|
||||||
|
contentLauncher
|
||||||
|
);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task("gen-index-cast-prod", (done) => {
|
||||||
|
const latestManifest = require(path.resolve(
|
||||||
paths.cast_output_latest,
|
paths.cast_output_latest,
|
||||||
paths.cast_output_es5
|
"manifest.json"
|
||||||
)
|
));
|
||||||
);
|
const es5Manifest = require(path.resolve(
|
||||||
|
paths.cast_output_es5,
|
||||||
|
"manifest.json"
|
||||||
|
));
|
||||||
|
|
||||||
const DEMO_PAGE_ENTRIES = { "index.html": ["main"] };
|
const contentReceiver = renderCastTemplate("receiver", {
|
||||||
|
latestReceiverJS: latestManifest["receiver.js"],
|
||||||
|
});
|
||||||
|
fs.outputFileSync(
|
||||||
|
path.resolve(paths.cast_output_root, "receiver.html"),
|
||||||
|
contentReceiver
|
||||||
|
);
|
||||||
|
|
||||||
gulp.task(
|
const contentMedia = renderCastTemplate("media", {
|
||||||
"gen-pages-demo-dev",
|
latestMediaJS: latestManifest["media.js"],
|
||||||
genPagesDevTask(DEMO_PAGE_ENTRIES, paths.demo_dir, paths.demo_output_root)
|
es5MediaJS: es5Manifest["media.js"],
|
||||||
);
|
});
|
||||||
|
fs.outputFileSync(
|
||||||
|
path.resolve(paths.cast_output_root, "media.html"),
|
||||||
|
contentMedia
|
||||||
|
);
|
||||||
|
|
||||||
gulp.task(
|
const contentFAQ = renderCastTemplate("launcher-faq", {
|
||||||
"gen-pages-demo-prod",
|
latestLauncherJS: latestManifest["launcher.js"],
|
||||||
genPagesProdTask(
|
es5LauncherJS: es5Manifest["launcher.js"],
|
||||||
DEMO_PAGE_ENTRIES,
|
});
|
||||||
paths.demo_dir,
|
fs.outputFileSync(
|
||||||
paths.demo_output_root,
|
path.resolve(paths.cast_output_root, "faq.html"),
|
||||||
|
contentFAQ
|
||||||
|
);
|
||||||
|
|
||||||
|
const contentLauncher = renderCastTemplate("launcher", {
|
||||||
|
latestLauncherJS: latestManifest["launcher.js"],
|
||||||
|
es5LauncherJS: es5Manifest["launcher.js"],
|
||||||
|
});
|
||||||
|
fs.outputFileSync(
|
||||||
|
path.resolve(paths.cast_output_root, "index.html"),
|
||||||
|
contentLauncher
|
||||||
|
);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task("gen-index-demo-dev", (done) => {
|
||||||
|
const content = renderDemoTemplate("index", {
|
||||||
|
latestDemoJS: "/frontend_latest/main.js",
|
||||||
|
|
||||||
|
es5DemoJS: "/frontend_es5/main.js",
|
||||||
|
});
|
||||||
|
|
||||||
|
fs.outputFileSync(
|
||||||
|
path.resolve(paths.demo_output_root, "index.html"),
|
||||||
|
content
|
||||||
|
);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task("gen-index-demo-prod", (done) => {
|
||||||
|
const latestManifest = require(path.resolve(
|
||||||
paths.demo_output_latest,
|
paths.demo_output_latest,
|
||||||
paths.demo_output_es5
|
"manifest.json"
|
||||||
)
|
));
|
||||||
);
|
const es5Manifest = require(path.resolve(
|
||||||
|
paths.demo_output_es5,
|
||||||
|
"manifest.json"
|
||||||
|
));
|
||||||
|
const content = renderDemoTemplate("index", {
|
||||||
|
latestDemoJS: latestManifest["main.js"],
|
||||||
|
|
||||||
const GALLERY_PAGE_ENTRIES = { "index.html": ["entrypoint"] };
|
es5DemoJS: es5Manifest["main.js"],
|
||||||
|
});
|
||||||
|
const minified = minifyHtml(content);
|
||||||
|
|
||||||
gulp.task(
|
fs.outputFileSync(
|
||||||
"gen-pages-gallery-dev",
|
path.resolve(paths.demo_output_root, "index.html"),
|
||||||
genPagesDevTask(
|
minified
|
||||||
GALLERY_PAGE_ENTRIES,
|
);
|
||||||
paths.gallery_dir,
|
done();
|
||||||
paths.gallery_output_root
|
});
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
gulp.task(
|
gulp.task("gen-index-gallery-dev", (done) => {
|
||||||
"gen-pages-gallery-prod",
|
const content = renderGalleryTemplate("index", {
|
||||||
genPagesProdTask(
|
latestGalleryJS: "./frontend_latest/entrypoint.js",
|
||||||
GALLERY_PAGE_ENTRIES,
|
});
|
||||||
paths.gallery_dir,
|
|
||||||
paths.gallery_output_root,
|
|
||||||
paths.gallery_output_latest
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const LANDING_PAGE_PAGE_ENTRIES = { "index.html": ["entrypoint"] };
|
fs.outputFileSync(
|
||||||
|
path.resolve(paths.gallery_output_root, "index.html"),
|
||||||
|
content
|
||||||
|
);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
gulp.task(
|
gulp.task("gen-index-gallery-prod", (done) => {
|
||||||
"gen-pages-landing-page-dev",
|
const latestManifest = require(path.resolve(
|
||||||
genPagesDevTask(
|
paths.gallery_output_latest,
|
||||||
LANDING_PAGE_PAGE_ENTRIES,
|
"manifest.json"
|
||||||
paths.landingPage_dir,
|
));
|
||||||
paths.landingPage_output_root
|
const content = renderGalleryTemplate("index", {
|
||||||
)
|
latestGalleryJS: latestManifest["entrypoint.js"],
|
||||||
);
|
});
|
||||||
|
const minified = minifyHtml(content);
|
||||||
|
|
||||||
gulp.task(
|
fs.outputFileSync(
|
||||||
"gen-pages-landing-page-prod",
|
path.resolve(paths.gallery_output_root, "index.html"),
|
||||||
genPagesProdTask(
|
minified
|
||||||
LANDING_PAGE_PAGE_ENTRIES,
|
);
|
||||||
paths.landingPage_dir,
|
done();
|
||||||
paths.landingPage_output_root,
|
});
|
||||||
paths.landingPage_output_latest,
|
|
||||||
paths.landingPage_output_es5
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const HASSIO_PAGE_ENTRIES = { "entrypoint.js": ["entrypoint"] };
|
gulp.task("gen-index-hassio-dev", async () => {
|
||||||
|
writeHassioEntrypoint(
|
||||||
|
`${paths.hassio_publicPath}/frontend_latest/entrypoint.js`,
|
||||||
|
`${paths.hassio_publicPath}/frontend_es5/entrypoint.js`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
gulp.task(
|
gulp.task("gen-index-hassio-prod", async () => {
|
||||||
"gen-pages-hassio-dev",
|
const latestManifest = require(path.resolve(
|
||||||
genPagesDevTask(
|
|
||||||
HASSIO_PAGE_ENTRIES,
|
|
||||||
paths.hassio_dir,
|
|
||||||
paths.hassio_output_root,
|
|
||||||
"src",
|
|
||||||
paths.hassio_publicPath
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
gulp.task(
|
|
||||||
"gen-pages-hassio-prod",
|
|
||||||
genPagesProdTask(
|
|
||||||
HASSIO_PAGE_ENTRIES,
|
|
||||||
paths.hassio_dir,
|
|
||||||
paths.hassio_output_root,
|
|
||||||
paths.hassio_output_latest,
|
paths.hassio_output_latest,
|
||||||
|
"manifest.json"
|
||||||
|
));
|
||||||
|
const es5Manifest = require(path.resolve(
|
||||||
paths.hassio_output_es5,
|
paths.hassio_output_es5,
|
||||||
"src"
|
"manifest.json"
|
||||||
)
|
));
|
||||||
);
|
writeHassioEntrypoint(
|
||||||
|
latestManifest["entrypoint.js"],
|
||||||
|
es5Manifest["entrypoint.js"]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
function writeHassioEntrypoint(latestEntrypoint, es5Entrypoint) {
|
||||||
|
fs.mkdirSync(paths.hassio_output_root, { recursive: true });
|
||||||
|
// Safari 12 and below does not have a compliant ES2015 implementation of template literals, so we ship ES5
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.resolve(paths.hassio_output_root, "entrypoint.js"),
|
||||||
|
`
|
||||||
|
function loadES5() {
|
||||||
|
var el = document.createElement('script');
|
||||||
|
el.src = '${es5Entrypoint}';
|
||||||
|
document.body.appendChild(el);
|
||||||
|
}
|
||||||
|
if (/.*Version\\/(?:11|12)(?:\\.\\d+)*.*Safari\\//.test(navigator.userAgent)) {
|
||||||
|
loadES5();
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
new Function("import('${latestEntrypoint}')")();
|
||||||
|
} catch (err) {
|
||||||
|
loadES5();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
{ encoding: "utf-8" }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -1,171 +0,0 @@
|
|||||||
// Task to download the latest Lokalise translations from the nightly workflow artifacts
|
|
||||||
|
|
||||||
import { createOAuthDeviceAuth } from "@octokit/auth-oauth-device";
|
|
||||||
import { retry } from "@octokit/plugin-retry";
|
|
||||||
import { Octokit } from "@octokit/rest";
|
|
||||||
import { deleteAsync } from "del";
|
|
||||||
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
||||||
import gulp from "gulp";
|
|
||||||
import jszip from "jszip";
|
|
||||||
import path from "path";
|
|
||||||
import process from "process";
|
|
||||||
import { extract } from "tar";
|
|
||||||
|
|
||||||
const MAX_AGE = 24; // hours
|
|
||||||
const OWNER = "home-assistant";
|
|
||||||
const REPO = "frontend";
|
|
||||||
const WORKFLOW_NAME = "nightly.yaml";
|
|
||||||
const ARTIFACT_NAME = "translations";
|
|
||||||
const CLIENT_ID = "Iv1.3914e28cb27834d1";
|
|
||||||
const EXTRACT_DIR = "translations";
|
|
||||||
const TOKEN_FILE = path.posix.join(EXTRACT_DIR, "token.json");
|
|
||||||
const ARTIFACT_FILE = path.posix.join(EXTRACT_DIR, "artifact.json");
|
|
||||||
|
|
||||||
let allowTokenSetup = false;
|
|
||||||
gulp.task("allow-setup-fetch-nightly-translations", (done) => {
|
|
||||||
allowTokenSetup = true;
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
|
|
||||||
gulp.task("fetch-nightly-translations", async function () {
|
|
||||||
// Skip all when environment flag is set (assumes translations are already in place)
|
|
||||||
if (process.env?.SKIP_FETCH_NIGHTLY_TRANSLATIONS) {
|
|
||||||
console.log("Skipping fetch due to environment signal");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read current translations artifact info if it exists,
|
|
||||||
// and stop if they are not old enough
|
|
||||||
let currentArtifact;
|
|
||||||
try {
|
|
||||||
currentArtifact = JSON.parse(await readFile(ARTIFACT_FILE, "utf-8"));
|
|
||||||
const currentAge =
|
|
||||||
(Date.now() - Date.parse(currentArtifact.created_at)) / 3600000;
|
|
||||||
if (currentAge < MAX_AGE) {
|
|
||||||
console.log(
|
|
||||||
"Keeping current translations (only %s hours old)",
|
|
||||||
currentAge.toFixed(1)
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
currentArtifact = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// To store file writing promises
|
|
||||||
const createExtractDir = mkdir(EXTRACT_DIR, { recursive: true });
|
|
||||||
const writings = [];
|
|
||||||
|
|
||||||
// Authenticate to GitHub using GitHub action token if it exists,
|
|
||||||
// otherwise look for a saved user token or generate a new one if none
|
|
||||||
let tokenAuth;
|
|
||||||
if (process.env.GITHUB_TOKEN) {
|
|
||||||
tokenAuth = { token: process.env.GITHUB_TOKEN };
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
tokenAuth = JSON.parse(await readFile(TOKEN_FILE, "utf-8"));
|
|
||||||
} catch {
|
|
||||||
if (!allowTokenSetup) {
|
|
||||||
console.log("No token found so build will continue with English only");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const auth = createOAuthDeviceAuth({
|
|
||||||
clientType: "github-app",
|
|
||||||
clientId: CLIENT_ID,
|
|
||||||
onVerification: (verification) => {
|
|
||||||
console.log(
|
|
||||||
"Task needs to authenticate to GitHub to fetch the translations from nightly workflow\n" +
|
|
||||||
"Please go to %s to authorize this task\n" +
|
|
||||||
"\nEnter user code: %s\n\n" +
|
|
||||||
"This code will expire in %s minutes\n" +
|
|
||||||
"Task will automatically continue after authorization and token will be saved for future use",
|
|
||||||
verification.verification_uri,
|
|
||||||
verification.user_code,
|
|
||||||
(verification.expires_in / 60).toFixed(0)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
tokenAuth = await auth({ type: "oauth" });
|
|
||||||
writings.push(
|
|
||||||
createExtractDir.then(
|
|
||||||
writeFile(TOKEN_FILE, JSON.stringify(tokenAuth, null, 2))
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Authenticate with token and request workflow runs from GitHub
|
|
||||||
console.log("Fetching new translations...");
|
|
||||||
const octokit = new (Octokit.plugin(retry))({
|
|
||||||
userAgent: "Fetch Nightly Translations",
|
|
||||||
auth: tokenAuth.token,
|
|
||||||
});
|
|
||||||
|
|
||||||
const workflowRunsResponse = await octokit.rest.actions.listWorkflowRuns({
|
|
||||||
owner: OWNER,
|
|
||||||
repo: REPO,
|
|
||||||
workflow_id: WORKFLOW_NAME,
|
|
||||||
status: "success",
|
|
||||||
event: "schedule",
|
|
||||||
per_page: 1,
|
|
||||||
exclude_pull_requests: true,
|
|
||||||
});
|
|
||||||
if (workflowRunsResponse.data.total_count === 0) {
|
|
||||||
throw Error("No successful nightly workflow runs found");
|
|
||||||
}
|
|
||||||
const latestNightlyRun = workflowRunsResponse.data.workflow_runs[0];
|
|
||||||
|
|
||||||
// Stop if current is already the latest, otherwise Find the translations artifact
|
|
||||||
if (currentArtifact?.workflow_run.id === latestNightlyRun.id) {
|
|
||||||
console.log("Stopping because current translations are still the latest");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const latestArtifact = (
|
|
||||||
await octokit.actions.listWorkflowRunArtifacts({
|
|
||||||
owner: OWNER,
|
|
||||||
repo: REPO,
|
|
||||||
run_id: latestNightlyRun.id,
|
|
||||||
})
|
|
||||||
).data.artifacts.find((artifact) => artifact.name === ARTIFACT_NAME);
|
|
||||||
if (!latestArtifact) {
|
|
||||||
throw Error("Latest nightly workflow run has no translations artifact");
|
|
||||||
}
|
|
||||||
writings.push(
|
|
||||||
createExtractDir.then(
|
|
||||||
writeFile(ARTIFACT_FILE, JSON.stringify(latestArtifact, null, 2))
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Remove the current translations
|
|
||||||
const deleteCurrent = Promise.all(writings).then(
|
|
||||||
deleteAsync([`${EXTRACT_DIR}/*`, `!${ARTIFACT_FILE}`, `!${TOKEN_FILE}`])
|
|
||||||
);
|
|
||||||
|
|
||||||
// Get the download URL and follow the redirect to download (stored as ArrayBuffer)
|
|
||||||
const downloadResponse = await octokit.actions.downloadArtifact({
|
|
||||||
owner: OWNER,
|
|
||||||
repo: REPO,
|
|
||||||
artifact_id: latestArtifact.id,
|
|
||||||
archive_format: "zip",
|
|
||||||
});
|
|
||||||
if (downloadResponse.status !== 200) {
|
|
||||||
throw Error("Failure downloading translations artifact");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Artifact is a tarball, but GitHub adds it to a zip file
|
|
||||||
console.log("Unpacking downloaded translations...");
|
|
||||||
const zip = await jszip.loadAsync(downloadResponse.data);
|
|
||||||
await deleteCurrent;
|
|
||||||
const extractStream = zip.file(/.*/)[0].nodeStream().pipe(extract());
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
extractStream.on("close", resolve).on("error", reject);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
gulp.task(
|
|
||||||
"setup-and-fetch-nightly-translations",
|
|
||||||
gulp.series(
|
|
||||||
"allow-setup-fetch-nightly-translations",
|
|
||||||
"fetch-nightly-translations"
|
|
||||||
)
|
|
||||||
);
|
|
@ -1,21 +1,27 @@
|
|||||||
import fs from "fs";
|
/* eslint-disable */
|
||||||
import { glob } from "glob";
|
// Run demo develop mode
|
||||||
import gulp from "gulp";
|
const gulp = require("gulp");
|
||||||
import yaml from "js-yaml";
|
const fs = require("fs");
|
||||||
import { marked } from "marked";
|
const path = require("path");
|
||||||
import path from "path";
|
const { marked } = require("marked");
|
||||||
import paths from "../paths.cjs";
|
const glob = require("glob");
|
||||||
import "./clean.js";
|
const yaml = require("js-yaml");
|
||||||
import "./entry-html.js";
|
|
||||||
import "./gather-static.js";
|
const env = require("../env");
|
||||||
import "./gen-icons-json.js";
|
const paths = require("../paths");
|
||||||
import "./service-worker.js";
|
|
||||||
import "./translations.js";
|
require("./clean.js");
|
||||||
import "./rspack.js";
|
require("./translations.js");
|
||||||
|
require("./gen-icons-json.js");
|
||||||
|
require("./gather-static.js");
|
||||||
|
require("./webpack.js");
|
||||||
|
require("./service-worker.js");
|
||||||
|
require("./entry-html.js");
|
||||||
|
require("./rollup.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");
|
||||||
const files = await glob(path.resolve(pageDir, "**/*"));
|
const files = glob.sync(path.resolve(pageDir, "**/*"));
|
||||||
|
|
||||||
const galleryBuild = path.resolve(paths.gallery_dir, "build");
|
const galleryBuild = path.resolve(paths.gallery_dir, "build");
|
||||||
fs.mkdirSync(galleryBuild, { recursive: true });
|
fs.mkdirSync(galleryBuild, { recursive: true });
|
||||||
@ -35,7 +41,7 @@ gulp.task("gather-gallery-pages", async function gatherPages() {
|
|||||||
}
|
}
|
||||||
processed.add(pageId);
|
processed.add(pageId);
|
||||||
|
|
||||||
const [category] = pageId.split("/", 2);
|
const [category, name] = pageId.split("/", 2);
|
||||||
|
|
||||||
const demoFile = path.resolve(pageDir, `${pageId}.ts`);
|
const demoFile = path.resolve(pageDir, `${pageId}.ts`);
|
||||||
const descriptionFile = path.resolve(pageDir, `${pageId}.markdown`);
|
const descriptionFile = path.resolve(pageDir, `${pageId}.markdown`);
|
||||||
@ -84,7 +90,9 @@ gulp.task("gather-gallery-pages", async function gatherPages() {
|
|||||||
|
|
||||||
// Generate sidebar
|
// Generate sidebar
|
||||||
const sidebarPath = path.resolve(paths.gallery_dir, "sidebar.js");
|
const sidebarPath = path.resolve(paths.gallery_dir, "sidebar.js");
|
||||||
const sidebar = (await import(sidebarPath)).default;
|
// To make watch work during development
|
||||||
|
delete require.cache[sidebarPath];
|
||||||
|
const sidebar = require(sidebarPath);
|
||||||
|
|
||||||
const pagesToProcess = {};
|
const pagesToProcess = {};
|
||||||
for (const key of processed) {
|
for (const key of processed) {
|
||||||
@ -154,9 +162,11 @@ gulp.task(
|
|||||||
"gather-gallery-pages"
|
"gather-gallery-pages"
|
||||||
),
|
),
|
||||||
"copy-static-gallery",
|
"copy-static-gallery",
|
||||||
"gen-pages-gallery-dev",
|
"gen-index-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 +195,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-index-gallery-prod"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
// Gulp task to gather all static files.
|
// Gulp task to gather all static files.
|
||||||
|
|
||||||
import fs from "fs-extra";
|
const gulp = require("gulp");
|
||||||
import gulp from "gulp";
|
const path = require("path");
|
||||||
import path from "path";
|
const fs = require("fs-extra");
|
||||||
import paths from "../paths.cjs";
|
const paths = require("../paths");
|
||||||
|
|
||||||
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,21 +111,17 @@ gulp.task("copy-translations-supervisor", async () => {
|
|||||||
copyTranslations(staticDir);
|
copyTranslations(staticDir);
|
||||||
});
|
});
|
||||||
|
|
||||||
gulp.task("copy-translations-landing-page", async () => {
|
gulp.task("copy-locale-data-supervisor", async () => {
|
||||||
const staticDir = paths.landingPage_output_static;
|
|
||||||
copyTranslations(staticDir);
|
|
||||||
});
|
|
||||||
|
|
||||||
gulp.task("copy-static-supervisor", async () => {
|
|
||||||
const staticDir = paths.hassio_output_static;
|
const staticDir = paths.hassio_output_static;
|
||||||
copyLocaleData(staticDir);
|
copyLocaleData(staticDir);
|
||||||
copyFonts(staticDir);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
gulp.task("copy-static-app", async () => {
|
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 +132,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 +143,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 +158,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 +183,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);
|
|
||||||
});
|
|
||||||
|
@ -1,15 +1,17 @@
|
|||||||
import fs from "fs";
|
const gulp = require("gulp");
|
||||||
import gulp from "gulp";
|
const path = require("path");
|
||||||
import hash from "object-hash";
|
const fs = require("fs");
|
||||||
import path from "path";
|
const hash = require("object-hash");
|
||||||
import paths from "../paths.cjs";
|
|
||||||
|
|
||||||
const ICON_PACKAGE_PATH = path.resolve("node_modules/@mdi/svg/");
|
const ICON_PACKAGE_PATH = path.resolve(
|
||||||
|
__dirname,
|
||||||
|
"../../node_modules/@mdi/svg/"
|
||||||
|
);
|
||||||
const META_PATH = path.resolve(ICON_PACKAGE_PATH, "meta.json");
|
const META_PATH = path.resolve(ICON_PACKAGE_PATH, "meta.json");
|
||||||
const PACKAGE_PATH = path.resolve(ICON_PACKAGE_PATH, "package.json");
|
const PACKAGE_PATH = path.resolve(ICON_PACKAGE_PATH, "package.json");
|
||||||
const ICON_PATH = path.resolve(ICON_PACKAGE_PATH, "svg");
|
const ICON_PATH = path.resolve(ICON_PACKAGE_PATH, "svg");
|
||||||
const OUTPUT_DIR = path.resolve(paths.build_dir, "mdi");
|
const OUTPUT_DIR = path.resolve(__dirname, "../../build/mdi");
|
||||||
const REMOVED_ICONS_PATH = new URL("../removedIcons.json", import.meta.url);
|
const REMOVED_ICONS_PATH = path.resolve(__dirname, "../removedIcons.json");
|
||||||
|
|
||||||
const encoding = "utf8";
|
const encoding = "utf8";
|
||||||
|
|
||||||
@ -132,11 +134,11 @@ gulp.task("gen-icons-json", (done) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const file = fs.readFileSync(PACKAGE_PATH, { encoding });
|
const file = fs.readFileSync(PACKAGE_PATH, { encoding });
|
||||||
const packageMeta = JSON.parse(file);
|
const package = JSON.parse(file);
|
||||||
|
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
path.resolve(OUTPUT_DIR, "iconMetadata.json"),
|
path.resolve(OUTPUT_DIR, "iconMetadata.json"),
|
||||||
JSON.stringify({ version: packageMeta.version, parts })
|
JSON.stringify({ version: package.version, parts })
|
||||||
);
|
);
|
||||||
|
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
import gulp from "gulp";
|
const gulp = require("gulp");
|
||||||
import env from "../env.cjs";
|
|
||||||
import "./clean.js";
|
const env = require("../env");
|
||||||
import "./compress.js";
|
|
||||||
import "./entry-html.js";
|
require("./clean.js");
|
||||||
import "./gather-static.js";
|
require("./gen-icons-json.js");
|
||||||
import "./gen-icons-json.js";
|
require("./webpack.js");
|
||||||
import "./translations.js";
|
require("./compress.js");
|
||||||
import "./rspack.js";
|
require("./rollup.js");
|
||||||
|
require("./gather-static.js");
|
||||||
|
require("./translations.js");
|
||||||
|
require("./gen-icons-json.js");
|
||||||
|
|
||||||
gulp.task(
|
gulp.task(
|
||||||
"develop-hassio",
|
"develop-hassio",
|
||||||
@ -16,12 +19,12 @@ gulp.task(
|
|||||||
},
|
},
|
||||||
"clean-hassio",
|
"clean-hassio",
|
||||||
"gen-dummy-icons-json",
|
"gen-dummy-icons-json",
|
||||||
"gen-pages-hassio-dev",
|
"gen-index-hassio-dev",
|
||||||
"build-supervisor-translations",
|
"build-supervisor-translations",
|
||||||
"copy-translations-supervisor",
|
"copy-translations-supervisor",
|
||||||
"build-locale-data",
|
"build-locale-data",
|
||||||
"copy-static-supervisor",
|
"copy-locale-data-supervisor",
|
||||||
"rspack-watch-hassio"
|
env.useRollup() ? "rollup-watch-hassio" : "webpack-watch-hassio"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -36,10 +39,10 @@ gulp.task(
|
|||||||
"build-supervisor-translations",
|
"build-supervisor-translations",
|
||||||
"copy-translations-supervisor",
|
"copy-translations-supervisor",
|
||||||
"build-locale-data",
|
"build-locale-data",
|
||||||
"copy-static-supervisor",
|
"copy-locale-data-supervisor",
|
||||||
"rspack-prod-hassio",
|
env.useRollup() ? "rollup-prod-hassio" : "webpack-prod-hassio",
|
||||||
"gen-pages-hassio-prod",
|
"gen-index-hassio-prod",
|
||||||
...// Don't compress running tests
|
...// Don't compress running tests
|
||||||
(env.isTestBuild() ? [] : ["compress-hassio"])
|
(env.isTest() ? [] : ["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,89 +1,74 @@
|
|||||||
import { deleteSync } from "del";
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
||||||
import gulp from "gulp";
|
|
||||||
import { join, resolve } from "node:path";
|
|
||||||
import paths from "../paths.cjs";
|
|
||||||
|
|
||||||
const formatjsDir = join(paths.root_dir, "node_modules", "@formatjs");
|
const del = require("del");
|
||||||
const outDir = join(paths.build_dir, "locale-data");
|
const path = require("path");
|
||||||
|
const gulp = require("gulp");
|
||||||
|
const fs = require("fs");
|
||||||
|
const paths = require("../paths");
|
||||||
|
|
||||||
const INTL_POLYFILLS = {
|
const outDir = "build/locale-data";
|
||||||
"intl-datetimeformat": "DateTimeFormat",
|
|
||||||
"intl-displaynames": "DisplayNames",
|
gulp.task("clean-locale-data", () => del([outDir]));
|
||||||
"intl-listformat": "ListFormat",
|
|
||||||
"intl-numberformat": "NumberFormat",
|
gulp.task("ensure-locale-data-build-dir", (done) => {
|
||||||
|
if (!fs.existsSync(outDir)) {
|
||||||
|
fs.mkdirSync(outDir, { recursive: true });
|
||||||
|
}
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
const modules = {
|
||||||
"intl-relativetimeformat": "RelativeTimeFormat",
|
"intl-relativetimeformat": "RelativeTimeFormat",
|
||||||
|
"intl-datetimeformat": "DateTimeFormat",
|
||||||
|
"intl-numberformat": "NumberFormat",
|
||||||
};
|
};
|
||||||
|
|
||||||
const convertToJSON = async (
|
gulp.task("create-locale-data", (done) => {
|
||||||
pkg,
|
const translationMeta = JSON.parse(
|
||||||
lang,
|
fs.readFileSync(
|
||||||
subDir = "locale-data",
|
path.join(paths.translations_src, "translationMetadata.json")
|
||||||
addFunc = "__addLocaleData",
|
)
|
||||||
skipMissing = true
|
);
|
||||||
) => {
|
Object.entries(modules).forEach(([module, className]) => {
|
||||||
let localeData;
|
Object.keys(translationMeta).forEach((lang) => {
|
||||||
try {
|
try {
|
||||||
// use "pt" for "pt-BR", because "pt-BR" is unsupported by @formatjs
|
const localeData = String(
|
||||||
const language = lang === "pt-BR" ? "pt" : lang;
|
fs.readFileSync(
|
||||||
|
require.resolve(`@formatjs/${module}/locale-data/${lang}.js`)
|
||||||
localeData = await readFile(
|
)
|
||||||
join(formatjsDir, pkg, subDir, `${language}.js`),
|
)
|
||||||
"utf-8"
|
.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, "");
|
||||||
|
// make sure we have valid JSON
|
||||||
|
JSON.parse(localeData);
|
||||||
|
if (!fs.existsSync(path.join(outDir, module))) {
|
||||||
|
fs.mkdirSync(path.join(outDir, module), { recursive: true });
|
||||||
|
}
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(outDir, `${module}/${lang}.json`),
|
||||||
|
localeData
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Ignore if language is missing (i.e. not supported by @formatjs)
|
if (e.code !== "MODULE_NOT_FOUND") {
|
||||||
if (e.code === "ENOENT" && skipMissing) {
|
|
||||||
console.warn(`Skipped missing data for language ${lang} from ${pkg}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
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}`);
|
|
||||||
}
|
}
|
||||||
// Parse to validate JSON, then stringify to minify
|
});
|
||||||
localeData = JSON.stringify(JSON.parse(localeData));
|
done();
|
||||||
await writeFile(join(outDir, `${pkg}/${lang}.json`), localeData);
|
});
|
||||||
};
|
|
||||||
|
|
||||||
gulp.task("clean-locale-data", async () => deleteSync([outDir]));
|
|
||||||
|
|
||||||
gulp.task("create-locale-data", async () => {
|
|
||||||
const translationMeta = JSON.parse(
|
|
||||||
await readFile(
|
|
||||||
resolve(paths.translations_src, "translationMetadata.json"),
|
|
||||||
"utf-8"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
const conversions = [];
|
|
||||||
for (const pkg of Object.keys(INTL_POLYFILLS)) {
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
|
||||||
await mkdir(join(outDir, pkg), { recursive: true });
|
|
||||||
for (const lang of Object.keys(translationMeta)) {
|
|
||||||
conversions.push(convertToJSON(pkg, lang));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
conversions.push(
|
|
||||||
convertToJSON(
|
|
||||||
"intl-datetimeformat",
|
|
||||||
"add-all-tz",
|
|
||||||
".",
|
|
||||||
"__addTZData",
|
|
||||||
false
|
|
||||||
)
|
|
||||||
);
|
|
||||||
await Promise.all(conversions);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
gulp.task(
|
gulp.task(
|
||||||
"build-locale-data",
|
"build-locale-data",
|
||||||
gulp.series("clean-locale-data", "create-locale-data")
|
gulp.series(
|
||||||
|
"clean-locale-data",
|
||||||
|
"ensure-locale-data-build-dir",
|
||||||
|
"create-locale-data"
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
146
build-scripts/gulp/rollup.js
Normal file
146
build-scripts/gulp/rollup.js
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
// Tasks to run Rollup
|
||||||
|
const path = require("path");
|
||||||
|
const gulp = require("gulp");
|
||||||
|
const rollup = require("rollup");
|
||||||
|
const handler = require("serve-handler");
|
||||||
|
const http = require("http");
|
||||||
|
const log = require("fancy-log");
|
||||||
|
const rollupConfig = require("../rollup");
|
||||||
|
const paths = require("../paths");
|
||||||
|
const open = require("open");
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
return 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) {
|
||||||
|
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,20 @@
|
|||||||
// Generate service workers
|
// Generate service worker.
|
||||||
|
// Based on manifest, create a file with the content as service_worker.js
|
||||||
|
/* eslint-disable import/no-dynamic-require */
|
||||||
|
/* eslint-disable global-require */
|
||||||
|
const gulp = require("gulp");
|
||||||
|
const path = require("path");
|
||||||
|
const fs = require("fs-extra");
|
||||||
|
const workboxBuild = require("workbox-build");
|
||||||
|
const sourceMapUrl = require("source-map-url");
|
||||||
|
const paths = require("../paths.js");
|
||||||
|
|
||||||
import { deleteAsync } from "del";
|
const swDest = path.resolve(paths.app_output_root, "service_worker.js");
|
||||||
import gulp from "gulp";
|
|
||||||
import { mkdir, readFile, symlink, writeFile } from "node:fs/promises";
|
|
||||||
import { basename, join, relative } from "node:path";
|
|
||||||
import { injectManifest } from "workbox-build";
|
|
||||||
import paths from "../paths.cjs";
|
|
||||||
|
|
||||||
const SW_MAP = {
|
const writeSW = (content) => fs.outputFileSync(swDest, content.trim() + "\n");
|
||||||
[paths.app_output_latest]: "modern",
|
|
||||||
[paths.app_output_es5]: "legacy",
|
|
||||||
};
|
|
||||||
|
|
||||||
const SW_DEV =
|
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 +23,48 @@ 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 = require(path.resolve(
|
||||||
const manifest = JSON.parse(
|
paths.app_output_latest,
|
||||||
await readFile(join(outPath, "manifest.json"), "utf-8")
|
"manifest.json"
|
||||||
|
));
|
||||||
|
let serviceWorkerContent = fs.readFileSync(
|
||||||
|
paths.app_output_root + bundleManifestLatest["service_worker.js"],
|
||||||
|
"utf-8"
|
||||||
);
|
);
|
||||||
const swSrc = join(paths.app_output_root, manifest["service-worker.js"]);
|
|
||||||
const swDest = join(paths.app_output_root, `sw-${build}.js`);
|
// Delete old file from frontend_latest so manifest won't pick it up
|
||||||
const buildDir = relative(paths.app_output_root, outPath);
|
fs.removeSync(
|
||||||
const { warnings } = await injectManifest({
|
paths.app_output_root + bundleManifestLatest["service_worker.js"]
|
||||||
swSrc,
|
);
|
||||||
swDest,
|
fs.removeSync(
|
||||||
injectionPoint: "__WB_MANIFEST__",
|
paths.app_output_root + bundleManifestLatest["service_worker.js.map"]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Remove ES5
|
||||||
|
const bundleManifestES5 = require(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 +78,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,84 @@
|
|||||||
/* eslint-disable max-classes-per-file */
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
|
|
||||||
import { deleteAsync } from "del";
|
const crypto = require("crypto");
|
||||||
import { glob } from "glob";
|
const del = require("del");
|
||||||
import gulp from "gulp";
|
const path = require("path");
|
||||||
import rename from "gulp-rename";
|
const source = require("vinyl-source-stream");
|
||||||
import merge from "lodash.merge";
|
const vinylBuffer = require("vinyl-buffer");
|
||||||
import { createHash } from "node:crypto";
|
const gulp = require("gulp");
|
||||||
import { mkdir, readFile } from "node:fs/promises";
|
const fs = require("fs");
|
||||||
import { basename, join } from "node:path";
|
const flatmap = require("gulp-flatmap");
|
||||||
import { PassThrough, Transform } from "node:stream";
|
const merge = require("gulp-merge-json");
|
||||||
import { finished } from "node:stream/promises";
|
const rename = require("gulp-rename");
|
||||||
import env from "../env.cjs";
|
const transform = require("gulp-json-transform");
|
||||||
import paths from "../paths.cjs";
|
const { mapFiles } = require("../util");
|
||||||
import "./fetch-nightly-translations.js";
|
const env = require("../env");
|
||||||
|
const paths = require("../paths");
|
||||||
|
|
||||||
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", (done) => {
|
||||||
"translations-enable-merge-backend",
|
|
||||||
gulp.parallel(async () => {
|
|
||||||
mergeBackend = true;
|
mergeBackend = true;
|
||||||
}, "allow-setup-fetch-nightly-translations")
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Panel translations which should be split from the core translations.
|
||||||
|
const TRANSLATION_FRAGMENTS = Object.keys(
|
||||||
|
require("../../src/translations/en.json").ui.panel
|
||||||
);
|
);
|
||||||
|
|
||||||
// Transform stream to apply a function on Vinyl JSON files (buffer mode only).
|
function recursiveFlatten(prefix, data) {
|
||||||
// The provided function can either return a new object, or an array of
|
let output = {};
|
||||||
// [object, subdirectory] pairs for fragmentizing the JSON.
|
Object.keys(data).forEach((key) => {
|
||||||
class CustomJSON extends Transform {
|
if (typeof data[key] === "object") {
|
||||||
constructor(func, reviver = null) {
|
output = {
|
||||||
super({ objectMode: true });
|
...output,
|
||||||
this._func = func;
|
...recursiveFlatten(prefix + key + ".", data[key]),
|
||||||
this._reviver = reviver;
|
};
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
||||||
async _transform(file, _, callback) {
|
|
||||||
let obj = JSON.parse(file.contents.toString(), this._reviver);
|
|
||||||
if (this._func) obj = this._func(obj, file.path);
|
|
||||||
for (const [outObj, dir] of Array.isArray(obj) ? obj : [[obj, ""]]) {
|
|
||||||
const outFile = file.clone({ contents: false });
|
|
||||||
outFile.contents = Buffer.from(JSON.stringify(outObj));
|
|
||||||
outFile.dirname += `/${dir}`;
|
|
||||||
this.push(outFile);
|
|
||||||
}
|
|
||||||
callback(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transform stream to merge Vinyl JSON files (buffer mode only).
|
|
||||||
class MergeJSON extends Transform {
|
|
||||||
_objects = [];
|
|
||||||
|
|
||||||
constructor(stem, startObj = {}, reviver = null) {
|
|
||||||
super({ objectMode: true, allowHalfOpen: false });
|
|
||||||
this._stem = stem;
|
|
||||||
this._startObj = structuredClone(startObj);
|
|
||||||
this._reviver = reviver;
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
||||||
async _transform(file, _, callback) {
|
|
||||||
this._objects.push(JSON.parse(file.contents.toString(), this._reviver));
|
|
||||||
if (!this._outFile) this._outFile = file.clone({ contents: false });
|
|
||||||
callback(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
||||||
async _flush(callback) {
|
|
||||||
const mergedObj = merge(this._startObj, ...this._objects);
|
|
||||||
this._outFile.contents = Buffer.from(JSON.stringify(mergedObj));
|
|
||||||
this._outFile.stem = this._stem;
|
|
||||||
callback(null, this._outFile);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Utility to flatten object keys to single level using separator
|
|
||||||
const flatten = (data, prefix = "", sep = ".") => {
|
|
||||||
const output = {};
|
|
||||||
for (const [key, value] of Object.entries(data)) {
|
|
||||||
if (typeof value === "object") {
|
|
||||||
Object.assign(output, flatten(value, prefix + key + sep, sep));
|
|
||||||
} 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 +87,67 @@ 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", () => del([workDir]));
|
||||||
|
|
||||||
const makeWorkDir = () => mkdir(workDir, { recursive: true });
|
gulp.task("ensure-translations-build-dir", (done) => {
|
||||||
|
if (!fs.existsSync(workDir)) {
|
||||||
|
fs.mkdirSync(workDir, { recursive: true });
|
||||||
|
}
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
const createTestTranslation = () =>
|
gulp.task("create-test-metadata", (cb) => {
|
||||||
env.isProdBuild()
|
fs.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 +158,277 @@ 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: "translationMaster.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(workDir));
|
||||||
.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", 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 = [workDir + "/translationMaster.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 = fs.readdirSync(fullDir);
|
||||||
|
|
||||||
|
for (let i = 0; i < files.length; i++) {
|
||||||
|
fingerprints[files[i].split(".")[0]] = {
|
||||||
|
// In dev we create fake hashes
|
||||||
|
hash: env.isProdBuild()
|
||||||
|
? crypto
|
||||||
|
.createHash("md5")
|
||||||
|
.update(fs.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}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.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 {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
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() ? (done) => done() : "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(
|
"clean-translations",
|
||||||
"fetch-nightly-translations",
|
"ensure-translations-build-dir",
|
||||||
gulp.series("clean-translations", makeWorkDir)
|
"create-translations",
|
||||||
),
|
"build-translation-fingerprints",
|
||||||
createTestTranslation,
|
"build-translation-write-metadata"
|
||||||
createMasterTranslation,
|
|
||||||
createTranslations,
|
|
||||||
writeTranslationMetaData
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
gulp.task(
|
gulp.task(
|
||||||
"build-supervisor-translations",
|
"build-supervisor-translations",
|
||||||
gulp.series(setFragment("supervisor"), "build-translations")
|
gulp.series(
|
||||||
);
|
"clean-translations",
|
||||||
|
"ensure-translations-build-dir",
|
||||||
gulp.task(
|
"build-master-translation",
|
||||||
"build-landing-page-translations",
|
"build-merged-translations",
|
||||||
gulp.series(setFragment("landing-page"), "build-translations")
|
"build-translation-fragment-supervisor",
|
||||||
|
"build-translation-flatten-supervisor",
|
||||||
|
"build-translation-fingerprints",
|
||||||
|
"build-translation-write-metadata"
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
11
build-scripts/gulp/wds.js
Normal file
11
build-scripts/gulp/wds.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
// Tasks to run Rollup
|
||||||
|
const gulp = require("gulp");
|
||||||
|
const { startDevServer } = require("@web/dev-server");
|
||||||
|
|
||||||
|
gulp.task("wds-watch-app", () => {
|
||||||
|
startDevServer({
|
||||||
|
config: {
|
||||||
|
watch: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
@ -1,21 +1,19 @@
|
|||||||
// Tasks to run rspack.
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
|
// Tasks to run webpack.
|
||||||
import fs from "fs";
|
const fs = require("fs");
|
||||||
import path from "path";
|
const gulp = require("gulp");
|
||||||
import log from "fancy-log";
|
const webpack = require("webpack");
|
||||||
import gulp from "gulp";
|
const WebpackDevServer = require("webpack-dev-server");
|
||||||
import rspack from "@rspack/core";
|
const log = require("fancy-log");
|
||||||
import { RspackDevServer } from "@rspack/dev-server";
|
const path = require("path");
|
||||||
import env from "../env.cjs";
|
const paths = require("../paths");
|
||||||
import paths from "../paths.cjs";
|
const {
|
||||||
import {
|
|
||||||
createAppConfig,
|
createAppConfig,
|
||||||
createCastConfig,
|
|
||||||
createDemoConfig,
|
createDemoConfig,
|
||||||
createGalleryConfig,
|
createCastConfig,
|
||||||
createHassioConfig,
|
createHassioConfig,
|
||||||
createLandingPageConfig,
|
createGalleryConfig,
|
||||||
} from "../rspack.cjs";
|
} = require("../webpack");
|
||||||
|
|
||||||
const bothBuilds = (createConfigFunc, params) => [
|
const bothBuilds = (createConfigFunc, params) => [
|
||||||
createConfigFunc({ ...params, latestBuild: true }),
|
createConfigFunc({ ...params, latestBuild: true }),
|
||||||
@ -31,7 +29,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,16 +39,10 @@ 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,
|
|
||||||
open: true,
|
open: true,
|
||||||
host: listenHost,
|
host: listenHost,
|
||||||
port,
|
port,
|
||||||
@ -58,14 +50,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) => {
|
||||||
@ -78,6 +69,7 @@ const doneHandler = (done) => (err, stats) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (stats.hasErrors() || stats.hasWarnings()) {
|
if (stats.hasErrors() || stats.hasWarnings()) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
console.log(stats.toString("minimal"));
|
console.log(stats.toString("minimal"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,60 +82,53 @@ 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,
|
||||||
isStatsBuild: env.isStatsBuild(),
|
|
||||||
isTestBuild: env.isTestBuild(),
|
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
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 +136,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 +144,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,28 +159,25 @@ 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,
|
||||||
isStatsBuild: env.isStatsBuild(),
|
|
||||||
isTestBuild: env.isTestBuild(),
|
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
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 +185,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(),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
);
|
|
@ -1,84 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
// Script to print Babel plugins and Core JS polyfills that will be used by browserslist environments
|
|
||||||
|
|
||||||
import { version as babelVersion } from "@babel/core";
|
|
||||||
import presetEnv from "@babel/preset-env";
|
|
||||||
import compilationTargets from "@babel/helper-compilation-targets";
|
|
||||||
import coreJSCompat from "core-js-compat";
|
|
||||||
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";
|
|
||||||
|
|
||||||
const detailsOpen = (heading) =>
|
|
||||||
`<details>\n<summary><h4>${heading}</h4></summary>\n`;
|
|
||||||
const detailsClose = "</details>\n";
|
|
||||||
|
|
||||||
const dummyAPI = {
|
|
||||||
version: babelVersion,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
||||||
assertVersion: () => {},
|
|
||||||
caller: (callback) =>
|
|
||||||
callback({
|
|
||||||
name: "Dummy Bundler",
|
|
||||||
supportsStaticESM: true,
|
|
||||||
supportsDynamicImport: true,
|
|
||||||
supportsTopLevelAwait: true,
|
|
||||||
supportsExportNamespaceFrom: true,
|
|
||||||
}),
|
|
||||||
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"]) {
|
|
||||||
const browserslistEnv = buildType.toLowerCase();
|
|
||||||
const babelOpts = babelOptions({ latestBuild: browserslistEnv === "modern" });
|
|
||||||
const presetEnvOpts = babelOpts.presets[0][1];
|
|
||||||
|
|
||||||
// Invoking preset-env in debug mode will log the included plugins
|
|
||||||
console.log(detailsOpen(`${buildType} Build Babel Plugins`));
|
|
||||||
presetEnv.default(dummyAPI, {
|
|
||||||
...presetEnvOpts,
|
|
||||||
browserslistEnv,
|
|
||||||
debug: true,
|
|
||||||
});
|
|
||||||
console.log(detailsClose);
|
|
||||||
|
|
||||||
// Manually log the Core-JS polyfills using the same technique
|
|
||||||
if (presetEnvOpts.useBuiltIns) {
|
|
||||||
console.log(detailsOpen(`${buildType} Build Core-JS Polyfills`));
|
|
||||||
const targets = compilationTargets.default(babelOpts?.targets, {
|
|
||||||
browserslistEnv,
|
|
||||||
});
|
|
||||||
const polyfillList = coreJSCompat({ targets }).list.filter(
|
|
||||||
polyfillFilter(
|
|
||||||
`${presetEnvOpts.useBuiltIns}-global`,
|
|
||||||
presetEnvOpts?.corejs?.proposals,
|
|
||||||
presetEnvOpts?.shippedProposals
|
|
||||||
)
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
"The following %i polyfills may be injected by Babel:\n",
|
|
||||||
polyfillList.length
|
|
||||||
);
|
|
||||||
for (const polyfill of polyfillList) {
|
|
||||||
logPlugin(polyfill, targets, coreJSCompat.data);
|
|
||||||
}
|
|
||||||
console.log(detailsClose);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,7 +1,8 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
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 +34,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"),
|
@ -1 +1,30 @@
|
|||||||
[]
|
[
|
||||||
|
{
|
||||||
|
"path": "M20,20H7A2,2 0 0,1 5,18V8.94L2.23,5.64C2.09,5.47 2,5.24 2,5A1,1 0 0,1 3,4H20A2,2 0 0,1 22,6V18A2,2 0 0,1 20,20M8.5,7A0.5,0.5 0 0,0 8,7.5V8.5A0.5,0.5 0 0,0 8.5,9H18.5A0.5,0.5 0 0,0 19,8.5V7.5A0.5,0.5 0 0,0 18.5,7H8.5M8.5,11A0.5,0.5 0 0,0 8,11.5V12.5A0.5,0.5 0 0,0 8.5,13H18.5A0.5,0.5 0 0,0 19,12.5V11.5A0.5,0.5 0 0,0 18.5,11H8.5M8.5,15A0.5,0.5 0 0,0 8,15.5V16.5A0.5,0.5 0 0,0 8.5,17H13.5A0.5,0.5 0 0,0 14,16.5V15.5A0.5,0.5 0 0,0 13.5,15H8.5Z",
|
||||||
|
"name": "android-messages"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "M4,6H2V20A2,2 0 0,0 4,22H18V20H4V6M20,2H8A2,2 0 0,0 6,4V16A2,2 0 0,0 8,18H20A2,2 0 0,0 22,16V4A2,2 0 0,0 20,2M20,12L17.5,10.5L15,12V4H20V12Z",
|
||||||
|
"name": "book-variant-multiple"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "M21,14H3V4H21M21,2H3C1.89,2 1,2.89 1,4V16A2,2 0 0,0 3,18H10L8,21V22H16V21L14,18H21A2,2 0 0,0 23,16V4C23,2.89 22.1,2 21,2Z",
|
||||||
|
"name": "desktop-mac"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "M21,14V4H3V14H21M21,2A2,2 0 0,1 23,4V16A2,2 0 0,1 21,18H14L16,21V22H8V21L10,18H3C1.89,18 1,17.1 1,16V4C1,2.89 1.89,2 3,2H21M4,5H15V10H4V5M16,5H20V7H16V5M20,8V13H16V8H20M4,11H9V13H4V11M10,11H15V13H10V11Z",
|
||||||
|
"name": "desktop-mac-dashboard"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "M22,24L16.75,19L17.38,21H4.5A2.5,2.5 0 0,1 2,18.5V3.5A2.5,2.5 0 0,1 4.5,1H19.5A2.5,2.5 0 0,1 22,3.5V24M12,6.8C9.32,6.8 7.44,7.95 7.44,7.95C8.47,7.03 10.27,6.5 10.27,6.5L10.1,6.33C8.41,6.36 6.88,7.53 6.88,7.53C5.16,11.12 5.27,14.22 5.27,14.22C6.67,16.03 8.75,15.9 8.75,15.9L9.46,15C8.21,14.73 7.42,13.62 7.42,13.62C7.42,13.62 9.3,14.9 12,14.9C14.7,14.9 16.58,13.62 16.58,13.62C16.58,13.62 15.79,14.73 14.54,15L15.25,15.9C15.25,15.9 17.33,16.03 18.73,14.22C18.73,14.22 18.84,11.12 17.12,7.53C17.12,7.53 15.59,6.36 13.9,6.33L13.73,6.5C13.73,6.5 15.53,7.03 16.56,7.95C16.56,7.95 14.68,6.8 12,6.8M9.93,10.59C10.58,10.59 11.11,11.16 11.1,11.86C11.1,12.55 10.58,13.13 9.93,13.13C9.29,13.13 8.77,12.55 8.77,11.86C8.77,11.16 9.28,10.59 9.93,10.59M14.1,10.59C14.75,10.59 15.27,11.16 15.27,11.86C15.27,12.55 14.75,13.13 14.1,13.13C13.46,13.13 12.94,12.55 12.94,11.86C12.94,11.16 13.45,10.59 14.1,10.59Z",
|
||||||
|
"name": "discord"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "M8.06,7.78C7.5,7.78 7.17,7.73 7.08,7.64L6.66,13.73C7.19,14.05 7.88,14.3 8.72,14.5C9.56,14.71 10.78,14.77 12.38,14.67C13.97,14.58 15.63,14.23 17.34,13.64L16.55,4.22C15.67,5.09 14.38,5.91 12.66,6.66C11.13,7.31 9.81,7.69 8.72,7.78H8.06M7.97,5.34C7.28,5.94 7,6.34 7.13,6.56C7.22,6.78 7.7,6.84 8.58,6.75C9.67,6.66 10.91,6.31 12.28,5.72C13.22,5.31 14.03,4.88 14.72,4.41C15.41,3.94 15.88,3.55 16.13,3.23C16.38,2.92 16.47,2.7 16.41,2.58C16.34,2.42 16.03,2.34 15.47,2.34C14.34,2.34 12.94,2.7 11.25,3.42C9.81,4.05 8.72,4.69 7.97,5.34M17.34,2.2C17.41,2.33 17.44,2.47 17.44,2.63L18.61,17C18.61,18.73 18,20.09 16.83,21.07C15.64,22.05 14.03,22.55 12,22.55C10,22.55 8.4,22.04 7.2,21C6,20 5.39,18.64 5.39,16.92L6.09,6.47C6.09,6.22 6.2,5.94 6.42,5.63C6.64,5.31 6.84,5.06 7.03,4.88L7.36,4.59C8.33,3.78 9.5,3.08 10.88,2.5C11.81,2.08 12.73,1.77 13.62,1.57C14.5,1.37 15.3,1.3 16,1.38C16.71,1.46 17.16,1.73 17.34,2.2Z",
|
||||||
|
"name": "google-home"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "M19.25,19H4.75V3H19.25M14,22H10V21H14M18,0H6A3,3 0 0,0 3,3V21A3,3 0 0,0 6,24H18A3,3 0 0,0 21,21V3A3,3 0 0,0 18,0Z",
|
||||||
|
"name": "tablet-android"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
14
build-scripts/rollup-plugins/dont-hash-plugin.js
Normal file
14
build-scripts/rollup-plugins/dont-hash-plugin.js
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.js
Normal file
24
build-scripts/rollup-plugins/ignore-plugin.js
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.js
Normal file
34
build-scripts/rollup-plugins/manifest-plugin.js
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",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
149
build-scripts/rollup-plugins/worker-plugin.js
Normal file
149
build-scripts/rollup-plugins/worker-plugin.js
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ms = new MagicString(code);
|
||||||
|
// Reset the regexp
|
||||||
|
workerRegexp.lastIndex = 0;
|
||||||
|
while (true) {
|
||||||
|
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!
|
||||||
|
optionsObject = new Function(`return ${match[3].slice(1)};`)();
|
||||||
|
}
|
||||||
|
delete optionsObject.type;
|
||||||
|
|
||||||
|
if (!new RegExp("^.*/").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
|
||||||
|
const resolvedWorkerFile = (await this.resolve(workerFile, id)).id;
|
||||||
|
let chunkRefId;
|
||||||
|
if (resolvedWorkerFile in refIds) {
|
||||||
|
chunkRefId = refIds[resolvedWorkerFile];
|
||||||
|
} else {
|
||||||
|
this.addWatchFile(resolvedWorkerFile);
|
||||||
|
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 }),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
139
build-scripts/rollup.js
Normal file
139
build-scripts/rollup.js
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
|
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").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");
|
||||||
|
const worker = require("./rollup-plugins/worker-plugin");
|
||||||
|
const dontHashPlugin = require("./rollup-plugins/dont-hash-plugin");
|
||||||
|
const ignore = require("./rollup-plugins/ignore-plugin");
|
||||||
|
|
||||||
|
const bundle = require("./bundle");
|
||||||
|
const paths = require("./paths");
|
||||||
|
|
||||||
|
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/guide/en/#preserveentrysignatures
|
||||||
|
preserveEntrySignatures: false,
|
||||||
|
plugins: [
|
||||||
|
ignore({
|
||||||
|
files: bundle.emptyPackages({ latestBuild }),
|
||||||
|
}),
|
||||||
|
resolve({
|
||||||
|
extensions,
|
||||||
|
preferBuiltins: false,
|
||||||
|
browser: true,
|
||||||
|
rootDir: paths.polymer_dir,
|
||||||
|
}),
|
||||||
|
commonjs(),
|
||||||
|
json(),
|
||||||
|
babel({
|
||||||
|
...bundle.babelOptions({ latestBuild }),
|
||||||
|
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/guide/en/#outputdir
|
||||||
|
dir: outputPath,
|
||||||
|
// https://rollupjs.org/guide/en/#outputformat
|
||||||
|
format: latestBuild ? "es" : "systemjs",
|
||||||
|
// https://rollupjs.org/guide/en/#outputexternallivebindings
|
||||||
|
externalLiveBindings: false,
|
||||||
|
// https://rollupjs.org/guide/en/#outputentryfilenames
|
||||||
|
// https://rollupjs.org/guide/en/#outputchunkfilenames
|
||||||
|
// https://rollupjs.org/guide/en/#outputassetfilenames
|
||||||
|
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/guide/en/#outputsourcemap
|
||||||
|
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,
|
||||||
|
};
|
@ -1,313 +0,0 @@
|
|||||||
const { existsSync } = require("fs");
|
|
||||||
const path = require("path");
|
|
||||||
const rspack = require("@rspack/core");
|
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
||||||
const { RsdoctorRspackPlugin } = require("@rsdoctor/rspack-plugin");
|
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
||||||
const { StatsWriterPlugin } = require("webpack-stats-plugin");
|
|
||||||
const filterStats = require("@bundle-stats/plugin-webpack-filter");
|
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
||||||
const TerserPlugin = require("terser-webpack-plugin");
|
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
||||||
const { WebpackManifestPlugin } = require("rspack-manifest-plugin");
|
|
||||||
const log = require("fancy-log");
|
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
||||||
const WebpackBar = require("webpackbar/rspack");
|
|
||||||
const paths = require("./paths.cjs");
|
|
||||||
const bundle = require("./bundle.cjs");
|
|
||||||
|
|
||||||
class LogStartCompilePlugin {
|
|
||||||
ignoredFirst = false;
|
|
||||||
|
|
||||||
apply(compiler) {
|
|
||||||
compiler.hooks.beforeCompile.tap("LogStartCompilePlugin", () => {
|
|
||||||
if (!this.ignoredFirst) {
|
|
||||||
this.ignoredFirst = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
log("Changes detected. Starting compilation");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const createRspackConfig = ({
|
|
||||||
name,
|
|
||||||
entry,
|
|
||||||
outputPath,
|
|
||||||
publicPath,
|
|
||||||
defineOverlay,
|
|
||||||
isProdBuild,
|
|
||||||
latestBuild,
|
|
||||||
isStatsBuild,
|
|
||||||
isTestBuild,
|
|
||||||
isHassioBuild,
|
|
||||||
dontHash,
|
|
||||||
}) => {
|
|
||||||
if (!dontHash) {
|
|
||||||
dontHash = new Set();
|
|
||||||
}
|
|
||||||
const ignorePackages = bundle.ignorePackages({ latestBuild });
|
|
||||||
return {
|
|
||||||
name,
|
|
||||||
mode: isProdBuild ? "production" : "development",
|
|
||||||
target: `browserslist:${latestBuild ? "modern" : "legacy"}`,
|
|
||||||
// For tests/CI, source maps are skipped to gain build speed
|
|
||||||
// For production, generate source maps for accurate stack traces without source code
|
|
||||||
// For development, generate "cheap" versions that can map to original line numbers
|
|
||||||
devtool: isTestBuild
|
|
||||||
? false
|
|
||||||
: isProdBuild
|
|
||||||
? "nosources-source-map"
|
|
||||||
: "eval-cheap-module-source-map",
|
|
||||||
entry,
|
|
||||||
node: false,
|
|
||||||
module: {
|
|
||||||
rules: [
|
|
||||||
{
|
|
||||||
test: /\.m?js$|\.ts$/,
|
|
||||||
exclude: /node_modules[\\/]core-js/,
|
|
||||||
use: (info) => [
|
|
||||||
{
|
|
||||||
loader: "babel-loader",
|
|
||||||
options: {
|
|
||||||
...bundle.babelOptions({
|
|
||||||
latestBuild,
|
|
||||||
isProdBuild,
|
|
||||||
isTestBuild,
|
|
||||||
sw: info.issuerLayer === "sw",
|
|
||||||
}),
|
|
||||||
cacheDirectory: !isProdBuild,
|
|
||||||
cacheCompression: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
loader: "builtin:swc-loader",
|
|
||||||
options: bundle.swcOptions(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
resolve: {
|
|
||||||
fullySpecified: false,
|
|
||||||
},
|
|
||||||
parser: {
|
|
||||||
worker: ["*context.audioWorklet.addModule()", "..."],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.css$/,
|
|
||||||
type: "asset/source",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
optimization: {
|
|
||||||
minimizer: [
|
|
||||||
new TerserPlugin({
|
|
||||||
parallel: true,
|
|
||||||
extractComments: true,
|
|
||||||
terserOptions: bundle.terserOptions({ latestBuild, isTestBuild }),
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
moduleIds: isProdBuild && !isStatsBuild ? "deterministic" : "named",
|
|
||||||
chunkIds: isProdBuild && !isStatsBuild ? "deterministic" : "named",
|
|
||||||
splitChunks: {
|
|
||||||
// Disable splitting for web workers and worklets because imports of
|
|
||||||
// external chunks are broken for:
|
|
||||||
chunks: !isProdBuild
|
|
||||||
? // improve incremental build speed, but blows up bundle size
|
|
||||||
new RegExp(
|
|
||||||
`^(?!(${Object.keys(entry).join("|")}|.*work(?:er|let))$)`
|
|
||||||
)
|
|
||||||
: // - ESM output: https://github.com/webpack/webpack/issues/17014
|
|
||||||
// - Worklets use `importScripts`: https://github.com/webpack/webpack/issues/11543
|
|
||||||
(chunk) =>
|
|
||||||
!chunk.canBeInitial() &&
|
|
||||||
!new RegExp(
|
|
||||||
`^.+-work${latestBuild ? "(?:let|er)" : "let"}$`
|
|
||||||
).test(chunk.name),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
!isStatsBuild && new WebpackBar({ fancy: !isProdBuild }),
|
|
||||||
new WebpackManifestPlugin({
|
|
||||||
// Only include the JS of entrypoints
|
|
||||||
filter: (file) => file.isInitial && !file.name.endsWith(".map"),
|
|
||||||
}),
|
|
||||||
new rspack.DefinePlugin(
|
|
||||||
bundle.definedVars({ isProdBuild, latestBuild, defineOverlay })
|
|
||||||
),
|
|
||||||
new rspack.IgnorePlugin({
|
|
||||||
checkResource(resource, context) {
|
|
||||||
// Only use ignore to intercept imports that we don't control
|
|
||||||
// inside node_module dependencies.
|
|
||||||
if (
|
|
||||||
!context.includes("/node_modules/") ||
|
|
||||||
// calling define.amd will call require("!!webpack amd options")
|
|
||||||
resource.startsWith("!!webpack") ||
|
|
||||||
// loaded by webpack dev server but doesn't exist.
|
|
||||||
resource === "webpack/hot" ||
|
|
||||||
resource.startsWith("@swc/helpers")
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
let fullPath;
|
|
||||||
try {
|
|
||||||
fullPath = resource.startsWith(".")
|
|
||||||
? path.resolve(context, resource)
|
|
||||||
: require.resolve(resource);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(
|
|
||||||
"Error in Home Assistant ignore plugin",
|
|
||||||
resource,
|
|
||||||
context
|
|
||||||
);
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
|
|
||||||
return ignorePackages.some((toIgnorePath) =>
|
|
||||||
fullPath.startsWith(toIgnorePath)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
new rspack.NormalModuleReplacementPlugin(
|
|
||||||
new RegExp(bundle.emptyPackages({ isHassioBuild }).join("|")),
|
|
||||||
path.resolve(paths.root_dir, "src/util/empty.js")
|
|
||||||
),
|
|
||||||
!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),
|
|
||||||
resolve: {
|
|
||||||
extensions: [".ts", ".js", ".json"],
|
|
||||||
alias: {
|
|
||||||
"lit/static-html$": "lit/static-html.js",
|
|
||||||
"lit/decorators$": "lit/decorators.js",
|
|
||||||
"lit/directive$": "lit/directive.js",
|
|
||||||
"lit/directives/until$": "lit/directives/until.js",
|
|
||||||
"lit/directives/class-map$": "lit/directives/class-map.js",
|
|
||||||
"lit/directives/style-map$": "lit/directives/style-map.js",
|
|
||||||
"lit/directives/if-defined$": "lit/directives/if-defined.js",
|
|
||||||
"lit/directives/guard$": "lit/directives/guard.js",
|
|
||||||
"lit/directives/cache$": "lit/directives/cache.js",
|
|
||||||
"lit/directives/join$": "lit/directives/join.js",
|
|
||||||
"lit/directives/repeat$": "lit/directives/repeat.js",
|
|
||||||
"lit/directives/live$": "lit/directives/live.js",
|
|
||||||
"lit/directives/keyed$": "lit/directives/keyed.js",
|
|
||||||
"lit/polyfill-support$": "lit/polyfill-support.js",
|
|
||||||
"@lit-labs/virtualizer/layouts/grid":
|
|
||||||
"@lit-labs/virtualizer/layouts/grid.js",
|
|
||||||
"@lit-labs/virtualizer/polyfills/resize-observer-polyfill/ResizeObserver":
|
|
||||||
"@lit-labs/virtualizer/polyfills/resize-observer-polyfill/ResizeObserver.js",
|
|
||||||
"@lit-labs/observers/resize-controller":
|
|
||||||
"@lit-labs/observers/resize-controller.js",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
output: {
|
|
||||||
module: latestBuild,
|
|
||||||
filename: ({ chunk }) =>
|
|
||||||
!isProdBuild || isStatsBuild || dontHash.has(chunk.name)
|
|
||||||
? "[name].js"
|
|
||||||
: "[name].[contenthash].js",
|
|
||||||
chunkFilename:
|
|
||||||
isProdBuild && !isStatsBuild ? "[name].[contenthash].js" : "[name].js",
|
|
||||||
assetModuleFilename:
|
|
||||||
isProdBuild && !isStatsBuild ? "[id].[contenthash][ext]" : "[id][ext]",
|
|
||||||
crossOriginLoading: "use-credentials",
|
|
||||||
hashFunction: "xxhash64",
|
|
||||||
path: outputPath,
|
|
||||||
publicPath,
|
|
||||||
// To silence warning in worker plugin
|
|
||||||
globalObject: "self",
|
|
||||||
// Since production source maps don't include sources, we need to point to them elsewhere
|
|
||||||
// For dependencies, just provide the path (no source in browser)
|
|
||||||
// Otherwise, point to the raw code on GitHub for browser to load
|
|
||||||
...Object.fromEntries(
|
|
||||||
["", "Fallback"].map((v) => [
|
|
||||||
`devtool${v}ModuleFilenameTemplate`,
|
|
||||||
!isTestBuild && isProdBuild
|
|
||||||
? (info) => {
|
|
||||||
if (
|
|
||||||
!path.isAbsolute(info.absoluteResourcePath) ||
|
|
||||||
!existsSync(info.resourcePath) ||
|
|
||||||
info.resourcePath.startsWith("./node_modules")
|
|
||||||
) {
|
|
||||||
// Source URLs are unknown for dependencies, so we use a relative URL with a
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
])
|
|
||||||
),
|
|
||||||
},
|
|
||||||
experiments: {
|
|
||||||
layers: true,
|
|
||||||
outputModule: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const createAppConfig = ({
|
|
||||||
isProdBuild,
|
|
||||||
latestBuild,
|
|
||||||
isStatsBuild,
|
|
||||||
isTestBuild,
|
|
||||||
}) =>
|
|
||||||
createRspackConfig(
|
|
||||||
bundle.config.app({ isProdBuild, latestBuild, isStatsBuild, isTestBuild })
|
|
||||||
);
|
|
||||||
|
|
||||||
const createDemoConfig = ({ isProdBuild, latestBuild, isStatsBuild }) =>
|
|
||||||
createRspackConfig(
|
|
||||||
bundle.config.demo({ isProdBuild, latestBuild, isStatsBuild })
|
|
||||||
);
|
|
||||||
|
|
||||||
const createCastConfig = ({ isProdBuild, latestBuild }) =>
|
|
||||||
createRspackConfig(bundle.config.cast({ isProdBuild, latestBuild }));
|
|
||||||
|
|
||||||
const createHassioConfig = ({
|
|
||||||
isProdBuild,
|
|
||||||
latestBuild,
|
|
||||||
isStatsBuild,
|
|
||||||
isTestBuild,
|
|
||||||
}) =>
|
|
||||||
createRspackConfig(
|
|
||||||
bundle.config.hassio({
|
|
||||||
isProdBuild,
|
|
||||||
latestBuild,
|
|
||||||
isStatsBuild,
|
|
||||||
isTestBuild,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const createGalleryConfig = ({ isProdBuild, latestBuild }) =>
|
|
||||||
createRspackConfig(bundle.config.gallery({ isProdBuild, latestBuild }));
|
|
||||||
|
|
||||||
const createLandingPageConfig = ({ isProdBuild, latestBuild }) =>
|
|
||||||
createRspackConfig(bundle.config.landingPage({ isProdBuild, latestBuild }));
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
createAppConfig,
|
|
||||||
createDemoConfig,
|
|
||||||
createCastConfig,
|
|
||||||
createHassioConfig,
|
|
||||||
createGalleryConfig,
|
|
||||||
createRspackConfig,
|
|
||||||
createLandingPageConfig,
|
|
||||||
};
|
|
17
build-scripts/util.js
Normal file
17
build-scripts/util.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
190
build-scripts/webpack.js
Normal file
190
build-scripts/webpack.js
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
|
const webpack = require("webpack");
|
||||||
|
const path = require("path");
|
||||||
|
const TerserPlugin = require("terser-webpack-plugin");
|
||||||
|
const { WebpackManifestPlugin } = require("webpack-manifest-plugin");
|
||||||
|
const log = require("fancy-log");
|
||||||
|
const WebpackBar = require("webpackbar");
|
||||||
|
const paths = require("./paths.js");
|
||||||
|
const bundle = require("./bundle.js");
|
||||||
|
|
||||||
|
class LogStartCompilePlugin {
|
||||||
|
ignoredFirst = false;
|
||||||
|
|
||||||
|
apply(compiler) {
|
||||||
|
compiler.hooks.beforeCompile.tap("LogStartCompilePlugin", () => {
|
||||||
|
if (!this.ignoredFirst) {
|
||||||
|
this.ignoredFirst = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
log("Changes detected. Starting compilation");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const createWebpackConfig = ({
|
||||||
|
entry,
|
||||||
|
outputPath,
|
||||||
|
publicPath,
|
||||||
|
defineOverlay,
|
||||||
|
isProdBuild,
|
||||||
|
latestBuild,
|
||||||
|
isStatsBuild,
|
||||||
|
isHassioBuild,
|
||||||
|
dontHash,
|
||||||
|
}) => {
|
||||||
|
if (!dontHash) {
|
||||||
|
dontHash = new Set();
|
||||||
|
}
|
||||||
|
const ignorePackages = bundle.ignorePackages({ latestBuild });
|
||||||
|
return {
|
||||||
|
mode: isProdBuild ? "production" : "development",
|
||||||
|
target: ["web", latestBuild ? "es2017" : "es5"],
|
||||||
|
devtool: isProdBuild
|
||||||
|
? "cheap-module-source-map"
|
||||||
|
: "eval-cheap-module-source-map",
|
||||||
|
entry,
|
||||||
|
node: false,
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.m?js$|\.ts$/,
|
||||||
|
use: {
|
||||||
|
loader: "babel-loader",
|
||||||
|
options: {
|
||||||
|
...bundle.babelOptions({ latestBuild }),
|
||||||
|
cacheDirectory: !isProdBuild,
|
||||||
|
cacheCompression: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.css$/,
|
||||||
|
type: "asset/source",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
optimization: {
|
||||||
|
minimizer: [
|
||||||
|
new TerserPlugin({
|
||||||
|
parallel: true,
|
||||||
|
extractComments: true,
|
||||||
|
terserOptions: bundle.terserOptions(latestBuild),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
moduleIds: isProdBuild && !isStatsBuild ? "deterministic" : "named",
|
||||||
|
chunkIds: isProdBuild && !isStatsBuild ? "deterministic" : "named",
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
!isStatsBuild && new WebpackBar({ fancy: !isProdBuild }),
|
||||||
|
new WebpackManifestPlugin({
|
||||||
|
// Only include the JS of entrypoints
|
||||||
|
filter: (file) => file.isInitial && !file.name.endsWith(".map"),
|
||||||
|
}),
|
||||||
|
new webpack.DefinePlugin(
|
||||||
|
bundle.definedVars({ isProdBuild, latestBuild, defineOverlay })
|
||||||
|
),
|
||||||
|
new webpack.IgnorePlugin({
|
||||||
|
checkResource(resource, context) {
|
||||||
|
// Only use ignore to intercept imports that we don't control
|
||||||
|
// inside node_module dependencies.
|
||||||
|
if (
|
||||||
|
!context.includes("/node_modules/") ||
|
||||||
|
// calling define.amd will call require("!!webpack amd options")
|
||||||
|
resource.startsWith("!!webpack") ||
|
||||||
|
// loaded by webpack dev server but doesn't exist.
|
||||||
|
resource === "webpack/hot"
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let fullPath;
|
||||||
|
try {
|
||||||
|
fullPath = resource.startsWith(".")
|
||||||
|
? path.resolve(context, resource)
|
||||||
|
: require.resolve(resource);
|
||||||
|
} catch (err) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(
|
||||||
|
"Error in Home Assistant ignore plugin",
|
||||||
|
resource,
|
||||||
|
context
|
||||||
|
);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ignorePackages.some((toIgnorePath) =>
|
||||||
|
fullPath.startsWith(toIgnorePath)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
new webpack.NormalModuleReplacementPlugin(
|
||||||
|
new RegExp(
|
||||||
|
bundle.emptyPackages({ latestBuild, isHassioBuild }).join("|")
|
||||||
|
),
|
||||||
|
path.resolve(paths.polymer_dir, "src/util/empty.js")
|
||||||
|
),
|
||||||
|
!isProdBuild && new LogStartCompilePlugin(),
|
||||||
|
].filter(Boolean),
|
||||||
|
resolve: {
|
||||||
|
extensions: [".ts", ".js", ".json"],
|
||||||
|
alias: {
|
||||||
|
"lit/decorators$": "lit/decorators.js",
|
||||||
|
"lit/directive$": "lit/directive.js",
|
||||||
|
"lit/directives/until$": "lit/directives/until.js",
|
||||||
|
"lit/directives/class-map$": "lit/directives/class-map.js",
|
||||||
|
"lit/directives/style-map$": "lit/directives/style-map.js",
|
||||||
|
"lit/directives/if-defined$": "lit/directives/if-defined.js",
|
||||||
|
"lit/directives/guard$": "lit/directives/guard.js",
|
||||||
|
"lit/directives/cache$": "lit/directives/cache.js",
|
||||||
|
"lit/directives/repeat$": "lit/directives/repeat.js",
|
||||||
|
"lit/polyfill-support$": "lit/polyfill-support.js",
|
||||||
|
"@lit-labs/virtualizer/layouts/grid":
|
||||||
|
"@lit-labs/virtualizer/layouts/grid.js",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
filename: ({ chunk }) => {
|
||||||
|
if (!isProdBuild || isStatsBuild || dontHash.has(chunk.name)) {
|
||||||
|
return `${chunk.name}.js`;
|
||||||
|
}
|
||||||
|
return `${chunk.name}.${chunk.hash.substr(0, 8)}.js`;
|
||||||
|
},
|
||||||
|
chunkFilename:
|
||||||
|
isProdBuild && !isStatsBuild ? "[chunkhash:8].js" : "[id].chunk.js",
|
||||||
|
path: outputPath,
|
||||||
|
publicPath,
|
||||||
|
// To silence warning in worker plugin
|
||||||
|
globalObject: "self",
|
||||||
|
},
|
||||||
|
experiments: {
|
||||||
|
topLevelAwait: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const createAppConfig = ({ isProdBuild, latestBuild, isStatsBuild }) =>
|
||||||
|
createWebpackConfig(
|
||||||
|
bundle.config.app({ isProdBuild, latestBuild, isStatsBuild })
|
||||||
|
);
|
||||||
|
|
||||||
|
const createDemoConfig = ({ isProdBuild, latestBuild, isStatsBuild }) =>
|
||||||
|
createWebpackConfig(
|
||||||
|
bundle.config.demo({ isProdBuild, latestBuild, isStatsBuild })
|
||||||
|
);
|
||||||
|
|
||||||
|
const createCastConfig = ({ isProdBuild, latestBuild }) =>
|
||||||
|
createWebpackConfig(bundle.config.cast({ isProdBuild, latestBuild }));
|
||||||
|
|
||||||
|
const createHassioConfig = ({ isProdBuild, latestBuild }) =>
|
||||||
|
createWebpackConfig(bundle.config.hassio({ isProdBuild, latestBuild }));
|
||||||
|
|
||||||
|
const createGalleryConfig = ({ isProdBuild, latestBuild }) =>
|
||||||
|
createWebpackConfig(bundle.config.gallery({ isProdBuild, latestBuild }));
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
createAppConfig,
|
||||||
|
createDemoConfig,
|
||||||
|
createCastConfig,
|
||||||
|
createHassioConfig,
|
||||||
|
createGalleryConfig,
|
||||||
|
};
|
@ -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 |
3
cast/public/service_worker.js
Normal file
3
cast/public/service_worker.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
self.addEventListener("fetch", function(event) {
|
||||||
|
event.respondWith(fetch(event.request));
|
||||||
|
});
|
@ -1,5 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
|
|
||||||
self.addEventListener("fetch", (event) => {
|
|
||||||
event.respondWith(fetch(event.request));
|
|
||||||
});
|
|
@ -1,3 +0,0 @@
|
|||||||
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 @@
|
|||||||
|
const rollup = require("../build-scripts/rollup.js");
|
||||||
|
const env = require("../build-scripts/env.js");
|
||||||
|
|
||||||
|
const config = rollup.createCastConfig({
|
||||||
|
isProdBuild: env.isProdBuild(),
|
||||||
|
latestBuild: true,
|
||||||
|
isStatsBuild: env.isStatsBuild(),
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = { ...config.inputOptions, output: config.outputOptions };
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user