mirror of
https://github.com/home-assistant/frontend.git
synced 2025-08-23 16:19:25 +00:00
Compare commits
3 Commits
gulp-ts
...
Move-Devel
Author | SHA1 | Date | |
---|---|---|---|
![]() |
728ea265e2 | ||
![]() |
d859b61365 | ||
![]() |
50bf69860f |
@@ -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,30 @@
|
|||||||
"context": ".."
|
"context": ".."
|
||||||
},
|
},
|
||||||
"appPort": "8124:8123",
|
"appPort": "8124:8123",
|
||||||
"postCreateCommand": "./.devcontainer/post_create.sh",
|
"context": "..",
|
||||||
"postStartCommand": "script/bootstrap",
|
"postCreateCommand": "script/bootstrap",
|
||||||
"containerEnv": {
|
|
||||||
"DEV_CONTAINER": "1",
|
|
||||||
"WORKSPACE_DIRECTORY": "${containerWorkspaceFolder}"
|
|
||||||
},
|
|
||||||
"remoteEnv": {
|
|
||||||
"NODE_OPTIONS": "--max_old_space_size=8192"
|
|
||||||
},
|
|
||||||
"customizations": {
|
|
||||||
"vscode": {
|
|
||||||
"extensions": [
|
"extensions": [
|
||||||
"dbaeumer.vscode-eslint",
|
|
||||||
"esbenp.prettier-vscode",
|
|
||||||
"runem.lit-plugin",
|
|
||||||
"github.vscode-pull-request-github",
|
"github.vscode-pull-request-github",
|
||||||
"eamodio.gitlens",
|
"dbaeumer.vscode-eslint",
|
||||||
"yeion7.styled-global-variables-autocomplete"
|
"ms-vscode.vscode-typescript-tslint-plugin",
|
||||||
|
"esbenp.prettier-vscode",
|
||||||
|
"bierner.lit-html",
|
||||||
|
"runem.lit-plugin",
|
||||||
|
"ms-python.vscode-pylance"
|
||||||
],
|
],
|
||||||
"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"
|
||||||
|
}
|
2
.github/ISSUE_TEMPLATE.md
vendored
2
.github/ISSUE_TEMPLATE.md
vendored
@@ -51,7 +51,7 @@ DO NOT DELETE ANY TEXT from this template! Otherwise, your issue may be closed w
|
|||||||
<!--
|
<!--
|
||||||
Provide details about the versions you are using, which helps us reproducing
|
Provide details about the versions you are using, which helps us reproducing
|
||||||
and finding the issue quicker. Version information is found in the
|
and finding the issue quicker. Version information is found in the
|
||||||
Home Assistant frontend: Settings -> About.
|
Home Assistant frontend: Configuration -> Info.
|
||||||
|
|
||||||
Browser version and operating system is important! Please try to replicate
|
Browser version and operating system is important! Please try to replicate
|
||||||
your issue in a different browser and be sure to include your findings.
|
your issue in a different browser and be sure to include your findings.
|
||||||
|
17
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
17
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,4 +1,4 @@
|
|||||||
name: Report a bug with the UI / Dashboards
|
name: Report a bug with the UI, Frontend or Lovelace
|
||||||
description: Report an issue related to the Home Assistant frontend.
|
description: Report an issue related to the Home Assistant frontend.
|
||||||
labels: bug
|
labels: bug
|
||||||
body:
|
body:
|
||||||
@@ -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 Lovelace 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: |
|
||||||
@@ -65,7 +64,7 @@ body:
|
|||||||
label: What version of Home Assistant Core has the issue?
|
label: What version of Home Assistant Core has the issue?
|
||||||
placeholder: core-
|
placeholder: core-
|
||||||
description: >
|
description: >
|
||||||
Can be found in: [Settings -> About](https://my.home-assistant.io/redirect/info/).
|
Can be found in the Configuration panel -> Info.
|
||||||
- type: input
|
- type: input
|
||||||
attributes:
|
attributes:
|
||||||
label: What was the last working version of Home Assistant Core?
|
label: What was the last working version of Home Assistant Core?
|
||||||
@@ -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
|
||||||
|
12
.github/ISSUE_TEMPLATE/config.yml
vendored
12
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,17 +1,17 @@
|
|||||||
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, Frontend or Lovelace
|
||||||
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, Frontend or Lovelace
|
||||||
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 with the backend repository.
|
||||||
- name: Report incorrect or missing information on our website
|
- name: Report incorrect or missing information on our website
|
||||||
url: https://github.com/home-assistant/home-assistant.io/issues
|
url: https://github.com/home-assistant/home-assistant.io/issues
|
||||||
about: Our documentation has its own issue tracker. Please report issues with the website there.
|
about: Our documentation has its own issue tracker. Please report issues with the website there.
|
||||||
- name: I have a question or need support
|
- name: I have a question or need support
|
||||||
url: https://www.home-assistant.io/help
|
url: https://www.home-assistant.io/help
|
||||||
about: We use GitHub for tracking bugs. Check our website for resources on getting help.
|
about: We use GitHub for tracking bugs, check our website for resources on getting help.
|
||||||
- name: I'm unsure where to go
|
- name: I'm unsure where to go
|
||||||
url: https://www.home-assistant.io/join-chat
|
url: https://www.home-assistant.io/join-chat
|
||||||
about: If you are unsure where to go, then joining our chat is recommended; Just ask!
|
about: If you are unsure where to go, then joining our chat is recommended; Just ask!
|
||||||
|
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
|
|
11
.github/dependabot.yml
vendored
11
.github/dependabot.yml
vendored
@@ -1,11 +0,0 @@
|
|||||||
version: 2
|
|
||||||
updates:
|
|
||||||
- package-ecosystem: "github-actions"
|
|
||||||
directory: "/"
|
|
||||||
schedule:
|
|
||||||
interval: weekly
|
|
||||||
time: "06:00"
|
|
||||||
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: yarn run-task 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: yarn run-task 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 }}
|
|
99
.github/workflows/ci.yaml
vendored
99
.github/workflows/ci.yaml
vendored
@@ -11,110 +11,87 @@ on:
|
|||||||
- master
|
- master
|
||||||
|
|
||||||
env:
|
env:
|
||||||
|
NODE_VERSION: 14
|
||||||
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@v2
|
||||||
- name: Setup Node
|
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||||
uses: actions/setup-node@v4.4.0
|
uses: actions/setup-node@v2
|
||||||
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: yarn run-task 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@v2
|
||||||
- name: Setup Node
|
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||||
uses: actions/setup-node@v4.4.0
|
uses: actions/setup-node@v2
|
||||||
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: yarn run-task 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@v2
|
||||||
- name: Setup Node
|
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||||
uses: actions/setup-node@v4.4.0
|
uses: actions/setup-node@v2
|
||||||
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: yarn run-task 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@v2
|
||||||
- name: Setup Node
|
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||||
uses: actions/setup-node@v4.4.0
|
uses: actions/setup-node@v2
|
||||||
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: yarn run-task 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@v2
|
||||||
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@v1
|
||||||
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@v1
|
||||||
|
|
||||||
# ℹ️ 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@v1
|
||||||
|
35
.github/workflows/demo.yaml
vendored
Normal file
35
.github/workflows/demo.yaml
vendored
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
name: Demo
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- dev
|
||||||
|
|
||||||
|
env:
|
||||||
|
NODE_VERSION: 14
|
||||||
|
NODE_OPTIONS: --max_old_space_size=6144
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check out files from GitHub
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||||
|
uses: actions/setup-node@v2
|
||||||
|
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
|
||||||
|
uses: netlify/actions/cli@master
|
||||||
|
env:
|
||||||
|
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||||
|
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_DEMO_DEV_SITE_ID }}
|
||||||
|
with:
|
||||||
|
args: deploy --dir=demo/dist --prod
|
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: yarn run-task 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: yarn run-task 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: yarn run-task 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: yarn run-task 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@v2.0.1
|
||||||
with:
|
with:
|
||||||
github-token: ${{ github.token }}
|
github-token: ${{ github.token }}
|
||||||
process-only: "issues, prs"
|
|
||||||
issue-lock-inactive-days: "30"
|
issue-lock-inactive-days: "30"
|
||||||
issue-exclude-created-before: "2020-10-01T00:00:00Z"
|
issue-exclude-created-before: "2020-10-01T00:00:00Z"
|
||||||
issue-lock-reason: ""
|
issue-lock-reason: ""
|
||||||
|
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 }}
|
71
.github/workflows/nightly.yaml
vendored
71
.github/workflows/nightly.yaml
vendored
@@ -1,71 +0,0 @@
|
|||||||
name: Nightly
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
schedule:
|
|
||||||
- cron: "0 1 * * *"
|
|
||||||
|
|
||||||
env:
|
|
||||||
PYTHON_VERSION: "3.13"
|
|
||||||
NODE_OPTIONS: --max_old_space_size=6144
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
actions: none
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
nightly:
|
|
||||||
name: Nightly
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
steps:
|
|
||||||
- name: Checkout the repository
|
|
||||||
uses: actions/checkout@v4.2.2
|
|
||||||
|
|
||||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
|
||||||
uses: actions/setup-python@v5
|
|
||||||
with:
|
|
||||||
python-version: ${{ env.PYTHON_VERSION }}
|
|
||||||
|
|
||||||
- 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: Bump version
|
|
||||||
run: script/version_bump.js nightly
|
|
||||||
|
|
||||||
- name: Build nightly Python wheels
|
|
||||||
run: |
|
|
||||||
pip install build
|
|
||||||
yarn install
|
|
||||||
export SKIP_FETCH_NIGHTLY_TRANSLATIONS=1
|
|
||||||
script/build_frontend
|
|
||||||
rm -rf dist home_assistant_frontend.egg-info
|
|
||||||
python3 -m build
|
|
||||||
|
|
||||||
- name: Archive translations
|
|
||||||
run: tar -czvf translations.tar.gz translations
|
|
||||||
|
|
||||||
- name: Upload build artifacts
|
|
||||||
uses: actions/upload-artifact@v4.6.2
|
|
||||||
with:
|
|
||||||
name: wheels
|
|
||||||
path: dist/home_assistant_frontend*.whl
|
|
||||||
if-no-files-found: error
|
|
||||||
|
|
||||||
- name: Upload translations
|
|
||||||
uses: actions/upload-artifact@v4.6.2
|
|
||||||
with:
|
|
||||||
name: translations
|
|
||||||
path: translations.tar.gz
|
|
||||||
if-no-files-found: error
|
|
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 }}
|
||||||
|
128
.github/workflows/release.yaml
vendored
128
.github/workflows/release.yaml
vendored
@@ -6,37 +6,30 @@ on:
|
|||||||
- published
|
- published
|
||||||
|
|
||||||
env:
|
env:
|
||||||
PYTHON_VERSION: "3.13"
|
PYTHON_VERSION: 3.8
|
||||||
|
NODE_VERSION: 14
|
||||||
NODE_OPTIONS: --max_old_space_size=6144
|
NODE_OPTIONS: --max_old_space_size=6144
|
||||||
|
|
||||||
# Set default workflow permissions
|
|
||||||
# All scopes not mentioned here are set to no access
|
|
||||||
# https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token
|
|
||||||
permissions:
|
|
||||||
actions: none
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
name: Release
|
name: Release
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
|
||||||
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@v2
|
||||||
|
|
||||||
- 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@v2
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
python-version: ${{ env.PYTHON_VERSION }}
|
||||||
|
|
||||||
|
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||||
|
uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
cache: yarn
|
cache: yarn
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
@@ -48,18 +41,11 @@ jobs:
|
|||||||
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }}
|
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }}
|
||||||
- name: Build and release package
|
- name: Build and release package
|
||||||
run: |
|
run: |
|
||||||
python3 -m pip install twine build
|
python3 -m pip install twine
|
||||||
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
|
|
||||||
|
|
||||||
- name: Upload release assets
|
script/release
|
||||||
uses: softprops/action-gh-release@v2.3.2
|
|
||||||
with:
|
|
||||||
files: |
|
|
||||||
dist/*.whl
|
|
||||||
dist/*.tar.gz
|
|
||||||
|
|
||||||
wheels-init:
|
wheels-init:
|
||||||
name: Init wheels build
|
name: Init wheels build
|
||||||
@@ -73,69 +59,33 @@ jobs:
|
|||||||
version=$(echo "${{ github.ref }}" | awk -F"/" '{print $NF}' )
|
version=$(echo "${{ github.ref }}" | awk -F"/" '{print $NF}' )
|
||||||
echo "home-assistant-frontend==$version" > ./requirements.txt
|
echo "home-assistant-frontend==$version" > ./requirements.txt
|
||||||
|
|
||||||
|
- name: Upload requirements.txt
|
||||||
|
uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: requirements
|
||||||
|
path: ./requirements.txt
|
||||||
|
|
||||||
|
build-wheels:
|
||||||
|
name: Build wheels for ${{ matrix.arch }}
|
||||||
|
needs: wheels-init
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
arch: ["aarch64", "armhf", "armv7", "amd64", "i386"]
|
||||||
|
tag:
|
||||||
|
- "3.9-alpine3.14"
|
||||||
|
steps:
|
||||||
|
- name: Download requirements.txt
|
||||||
|
uses: actions/download-artifact@v2
|
||||||
|
with:
|
||||||
|
name: requirements
|
||||||
|
|
||||||
- name: Build wheels
|
- name: Build wheels
|
||||||
uses: home-assistant/wheels@2025.03.0
|
uses: home-assistant/wheels@master
|
||||||
with:
|
with:
|
||||||
abi: cp313
|
tag: ${{ matrix.tag }}
|
||||||
tag: musllinux_1_2
|
arch: ${{ matrix.arch }}
|
||||||
arch: amd64
|
wheels-host: ${{ secrets.WHEELS_HOST }}
|
||||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||||
|
wheels-user: wheels
|
||||||
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@v3.0.13
|
||||||
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: 14
|
||||||
|
|
||||||
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@v2
|
||||||
|
|
||||||
- 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 +0,0 @@
|
|||||||
yarn run lint-staged --relative
|
|
@@ -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"
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
82
.vscode/tasks.json
vendored
82
.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",
|
||||||
@@ -249,7 +181,7 @@
|
|||||||
{
|
{
|
||||||
"label": "Run HA Core for Supervisor in devcontainer",
|
"label": "Run HA Core for Supervisor in devcontainer",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "SUPERVISOR=${input:supervisorHost} SUPERVISOR_TOKEN=${input:supervisorToken} script/core",
|
"command": "HASSIO=${input:supervisorHost} HASSIO_TOKEN=${input:supervisorToken} script/core",
|
||||||
"isBackground": true,
|
"isBackground": true,
|
||||||
"group": {
|
"group": {
|
||||||
"kind": "build",
|
"kind": "build",
|
||||||
@@ -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"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
1536
.yarn/patches/@lit-labs/virtualizer/0.7.0.patch
Normal file
1536
.yarn/patches/@lit-labs/virtualizer/0.7.0.patch
Normal file
File diff suppressed because one or more lines are too long
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/lib/uni-virtualizer/lib/polyfillLoaders/EventTarget.js b/lib/uni-virtualizer/lib/polyfillLoaders/EventTarget.js
|
||||||
|
index d92179f7fd5315203f870a6963e871dc8ddf6c0c..362e284121b97e0fba0925225777aebc32e26b8d 100644
|
||||||
|
--- a/lib/uni-virtualizer/lib/polyfillLoaders/EventTarget.js
|
||||||
|
+++ b/lib/uni-virtualizer/lib/polyfillLoaders/EventTarget.js
|
||||||
|
@@ -1,14 +1,15 @@
|
||||||
|
-let _ET, ET;
|
||||||
|
+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);
|
||||||
|
}
|
@@ -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
631
.yarn/releases/yarn-3.0.2.cjs
vendored
Executable file
631
.yarn/releases/yarn-3.0.2.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.0.2.cjs
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
include README.md
|
include README.md
|
||||||
|
include LICENSE.md
|
||||||
graft hass_frontend
|
graft hass_frontend
|
||||||
graft hass_frontend_es5
|
graft hass_frontend_es5
|
||||||
recursive-exclude * *.py[co]
|
recursive-exclude * *.py[co]
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
This is the repository for the official [Home Assistant](https://home-assistant.io) frontend.
|
This is the repository for the official [Home Assistant](https://home-assistant.io) frontend.
|
||||||
|
|
||||||
[](https://demo.home-assistant.io/)
|
[](https://demo.home-assistant.io/)
|
||||||
|
|
||||||
- [View demo of Home Assistant](https://demo.home-assistant.io/)
|
- [View demo of Home Assistant](https://demo.home-assistant.io/)
|
||||||
- [More information about Home Assistant](https://home-assistant.io)
|
- [More information about Home Assistant](https://home-assistant.io)
|
||||||
@@ -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";
|
|
||||||
|
|
||||||
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)];
|
||||||
})
|
})
|
||||||
);
|
);
|
219
build-scripts/bundle.js
Normal file
219
build-scripts/bundle.js
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
/* 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 }) =>
|
||||||
|
[
|
||||||
|
// 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"),
|
||||||
|
].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,
|
||||||
|
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,359 +0,0 @@
|
|||||||
import path from "node:path";
|
|
||||||
import packageJson from "../package.json" assert { type: "json" };
|
|
||||||
import { version } from "./env.ts";
|
|
||||||
import paths, { dirname } from "./paths.ts";
|
|
||||||
|
|
||||||
const dependencies = packageJson.dependencies;
|
|
||||||
|
|
||||||
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
|
|
||||||
export const sourceMapURL = () => {
|
|
||||||
const ref = version().endsWith("dev")
|
|
||||||
? process.env.GITHUB_SHA || "dev"
|
|
||||||
: version();
|
|
||||||
return `https://raw.githubusercontent.com/home-assistant/frontend/${ref}/`;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Files from NPM packages that we should replace with empty file
|
|
||||||
export const emptyPackages = ({ isHassioBuild }) =>
|
|
||||||
[
|
|
||||||
import.meta.resolve("@vaadin/vaadin-material-styles/typography.js"),
|
|
||||||
import.meta.resolve("@vaadin/vaadin-material-styles/font-icons.js"),
|
|
||||||
// Icons in supervisor conflict with icons in HA so we don't load.
|
|
||||||
isHassioBuild &&
|
|
||||||
import.meta.resolve(
|
|
||||||
path.resolve(paths.root_dir, "src/components/ha-icon.ts")
|
|
||||||
),
|
|
||||||
isHassioBuild &&
|
|
||||||
import.meta.resolve(
|
|
||||||
path.resolve(paths.root_dir, "src/components/ha-icon-picker.ts")
|
|
||||||
),
|
|
||||||
].filter(Boolean);
|
|
||||||
|
|
||||||
export const definedVars = ({ isProdBuild, latestBuild, defineOverlay }) => ({
|
|
||||||
__DEV__: !isProdBuild,
|
|
||||||
__BUILD__: JSON.stringify(latestBuild ? "modern" : "legacy"),
|
|
||||||
__VERSION__: JSON.stringify(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,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const htmlMinifierOptions = {
|
|
||||||
caseSensitive: true,
|
|
||||||
collapseWhitespace: true,
|
|
||||||
conservativeCollapse: true,
|
|
||||||
decodeEntities: true,
|
|
||||||
removeComments: true,
|
|
||||||
removeRedundantAttributes: true,
|
|
||||||
minifyCSS: {
|
|
||||||
compatibility: "*,-properties.zeroUnits",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const terserOptions = ({ latestBuild, isTestBuild }) => ({
|
|
||||||
safari10: !latestBuild,
|
|
||||||
ecma: latestBuild ? (2015 as const) : (5 as const),
|
|
||||||
module: latestBuild,
|
|
||||||
format: { comments: false },
|
|
||||||
sourceMap: !isTestBuild,
|
|
||||||
});
|
|
||||||
|
|
||||||
/** @type {import('@rspack/core').SwcLoaderOptions} */
|
|
||||||
export const swcOptions = () => ({
|
|
||||||
jsc: {
|
|
||||||
loose: true,
|
|
||||||
externalHelpers: true,
|
|
||||||
target: "ES2021",
|
|
||||||
parser: {
|
|
||||||
syntax: "typescript",
|
|
||||||
decorators: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const babelOptions = ({
|
|
||||||
latestBuild,
|
|
||||||
isProdBuild,
|
|
||||||
isTestBuild,
|
|
||||||
sw,
|
|
||||||
}: {
|
|
||||||
latestBuild?: boolean;
|
|
||||||
isProdBuild?: boolean;
|
|
||||||
isTestBuild?: boolean;
|
|
||||||
sw?: boolean;
|
|
||||||
}) => ({
|
|
||||||
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: 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.ts"),
|
|
||||||
{ 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>
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const config = {
|
|
||||||
app({
|
|
||||||
isProdBuild,
|
|
||||||
latestBuild,
|
|
||||||
isStatsBuild,
|
|
||||||
isTestBuild,
|
|
||||||
isWDS,
|
|
||||||
}: {
|
|
||||||
isProdBuild?: boolean;
|
|
||||||
latestBuild?: boolean;
|
|
||||||
isStatsBuild?: boolean;
|
|
||||||
isTestBuild?: boolean;
|
|
||||||
isWDS?: boolean;
|
|
||||||
}) {
|
|
||||||
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-${version()}`),
|
|
||||||
__DEMO__: true,
|
|
||||||
},
|
|
||||||
isProdBuild,
|
|
||||||
latestBuild,
|
|
||||||
isStatsBuild,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
cast({ isProdBuild, latestBuild }) {
|
|
||||||
const entry: Record<string, string> = {
|
|
||||||
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,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
36
build-scripts/env.js
Normal file
36
build-scripts/env.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const paths = require("./paths.js");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
useRollup() {
|
||||||
|
return process.env.ROLLUP === "1";
|
||||||
|
},
|
||||||
|
useWDS() {
|
||||||
|
return process.env.WDS === "1";
|
||||||
|
},
|
||||||
|
isProdBuild() {
|
||||||
|
return (
|
||||||
|
process.env.NODE_ENV === "production" || module.exports.isStatsBuild()
|
||||||
|
);
|
||||||
|
},
|
||||||
|
isStatsBuild() {
|
||||||
|
return process.env.STATS === "1";
|
||||||
|
},
|
||||||
|
isTest() {
|
||||||
|
return process.env.IS_TEST === "true";
|
||||||
|
},
|
||||||
|
isNetlify() {
|
||||||
|
return process.env.NETLIFY === "true";
|
||||||
|
},
|
||||||
|
version() {
|
||||||
|
const version = fs
|
||||||
|
.readFileSync(path.resolve(paths.polymer_dir, "setup.py"), "utf8")
|
||||||
|
.match(/\d{8}\.\d+/);
|
||||||
|
if (!version) {
|
||||||
|
throw Error("Version not found");
|
||||||
|
}
|
||||||
|
return version[0];
|
||||||
|
},
|
||||||
|
};
|
@@ -1,21 +0,0 @@
|
|||||||
import fs from "node:fs";
|
|
||||||
import path from "node:path";
|
|
||||||
import paths from "./paths.ts";
|
|
||||||
|
|
||||||
const isTrue = (value) => value === "1" || value?.toLowerCase() === "true";
|
|
||||||
|
|
||||||
export const isProdBuild = () =>
|
|
||||||
process.env.NODE_ENV === "production" || isStatsBuild();
|
|
||||||
export const isStatsBuild = () => isTrue(process.env.STATS);
|
|
||||||
export const isTestBuild = () => isTrue(process.env.IS_TEST);
|
|
||||||
export const isNetlify = () => isTrue(process.env.NETLIFY);
|
|
||||||
export const version = () => {
|
|
||||||
const pyProjectVersion = fs
|
|
||||||
.readFileSync(path.resolve(paths.root_dir, "pyproject.toml"), "utf8")
|
|
||||||
.match(/version\W+=\W"(\d{8}\.\d(?:\.dev)?)"/);
|
|
||||||
if (!pyProjectVersion) {
|
|
||||||
throw Error("Version not found");
|
|
||||||
}
|
|
||||||
return pyProjectVersion[1];
|
|
||||||
};
|
|
||||||
export const isDevContainer = () => 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",
|
|
||||||
},
|
|
||||||
});
|
|
60
build-scripts/gulp/app.js
Normal file
60
build-scripts/gulp/app.js
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
// Run HA develop mode
|
||||||
|
const gulp = require("gulp");
|
||||||
|
|
||||||
|
const env = require("../env");
|
||||||
|
|
||||||
|
require("./clean.js");
|
||||||
|
require("./translations.js");
|
||||||
|
require("./locale-data.js");
|
||||||
|
require("./gen-icons-json.js");
|
||||||
|
require("./gather-static.js");
|
||||||
|
require("./compress.js");
|
||||||
|
require("./webpack.js");
|
||||||
|
require("./service-worker.js");
|
||||||
|
require("./entry-html.js");
|
||||||
|
require("./rollup.js");
|
||||||
|
require("./wds.js");
|
||||||
|
|
||||||
|
gulp.task(
|
||||||
|
"develop-app",
|
||||||
|
gulp.series(
|
||||||
|
async function setEnv() {
|
||||||
|
process.env.NODE_ENV = "development";
|
||||||
|
},
|
||||||
|
"clean",
|
||||||
|
gulp.parallel(
|
||||||
|
"gen-service-worker-app-dev",
|
||||||
|
"gen-icons-json",
|
||||||
|
"gen-pages-dev",
|
||||||
|
"gen-index-app-dev",
|
||||||
|
"build-translations",
|
||||||
|
"build-locale-data"
|
||||||
|
),
|
||||||
|
"copy-static-app",
|
||||||
|
env.useWDS()
|
||||||
|
? "wds-watch-app"
|
||||||
|
: env.useRollup()
|
||||||
|
? "rollup-watch-app"
|
||||||
|
: "webpack-watch-app"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
gulp.task(
|
||||||
|
"build-app",
|
||||||
|
gulp.series(
|
||||||
|
async function setEnv() {
|
||||||
|
process.env.NODE_ENV = "production";
|
||||||
|
},
|
||||||
|
"clean",
|
||||||
|
gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"),
|
||||||
|
"copy-static-app",
|
||||||
|
env.useRollup() ? "rollup-prod-app" : "webpack-prod-app",
|
||||||
|
// Don't compress running tests
|
||||||
|
...(env.isTest() ? [] : ["compress-app"]),
|
||||||
|
gulp.parallel(
|
||||||
|
"gen-pages-prod",
|
||||||
|
"gen-index-app-prod",
|
||||||
|
"gen-service-worker-app-prod"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
@@ -1,54 +0,0 @@
|
|||||||
import { parallel, series } from "gulp";
|
|
||||||
import { isStatsBuild, isTestBuild } from "../env.ts";
|
|
||||||
import { clean } from "./clean.ts";
|
|
||||||
import { compressApp } from "./compress.ts";
|
|
||||||
import { genPagesAppDev, genPagesAppProd } from "./entry-html.ts";
|
|
||||||
import { copyStaticApp } from "./gather-static.ts";
|
|
||||||
import { genIconsJson } from "./gen-icons-json.ts";
|
|
||||||
import { buildLocaleData } from "./locale-data.ts";
|
|
||||||
import { rspackProdApp, rspackWatchApp } from "./rspack.ts";
|
|
||||||
import {
|
|
||||||
genServiceWorkerAppDev,
|
|
||||||
genServiceWorkerAppProd,
|
|
||||||
} from "./service-worker.ts";
|
|
||||||
import { buildTranslations } from "./translations.ts";
|
|
||||||
|
|
||||||
// develop-app
|
|
||||||
export const developApp = series(
|
|
||||||
async () => {
|
|
||||||
process.env.NODE_ENV = "development";
|
|
||||||
},
|
|
||||||
clean,
|
|
||||||
parallel(
|
|
||||||
genServiceWorkerAppDev,
|
|
||||||
genIconsJson,
|
|
||||||
genPagesAppDev,
|
|
||||||
buildTranslations,
|
|
||||||
buildLocaleData
|
|
||||||
),
|
|
||||||
copyStaticApp,
|
|
||||||
rspackWatchApp
|
|
||||||
);
|
|
||||||
|
|
||||||
// build-app
|
|
||||||
export const buildApp = series(
|
|
||||||
async () => {
|
|
||||||
process.env.NODE_ENV = "production";
|
|
||||||
},
|
|
||||||
clean,
|
|
||||||
parallel(genIconsJson, buildTranslations, buildLocaleData),
|
|
||||||
copyStaticApp,
|
|
||||||
rspackProdApp,
|
|
||||||
parallel(genPagesAppProd, genServiceWorkerAppProd),
|
|
||||||
// Don't compress running tests
|
|
||||||
...(isTestBuild() || isStatsBuild() ? [] : [compressApp])
|
|
||||||
);
|
|
||||||
|
|
||||||
// analyze-app
|
|
||||||
export const analyzeApp = series(
|
|
||||||
async () => {
|
|
||||||
process.env.STATS = "1";
|
|
||||||
},
|
|
||||||
clean,
|
|
||||||
rspackProdApp
|
|
||||||
);
|
|
41
build-scripts/gulp/cast.js
Normal file
41
build-scripts/gulp/cast.js
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
const gulp = require("gulp");
|
||||||
|
|
||||||
|
const env = require("../env");
|
||||||
|
|
||||||
|
require("./clean.js");
|
||||||
|
require("./translations.js");
|
||||||
|
require("./gather-static.js");
|
||||||
|
require("./webpack.js");
|
||||||
|
require("./service-worker.js");
|
||||||
|
require("./entry-html.js");
|
||||||
|
require("./rollup.js");
|
||||||
|
|
||||||
|
gulp.task(
|
||||||
|
"develop-cast",
|
||||||
|
gulp.series(
|
||||||
|
async function setEnv() {
|
||||||
|
process.env.NODE_ENV = "development";
|
||||||
|
},
|
||||||
|
"clean-cast",
|
||||||
|
"translations-enable-merge-backend",
|
||||||
|
gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"),
|
||||||
|
"copy-static-cast",
|
||||||
|
"gen-index-cast-dev",
|
||||||
|
env.useRollup() ? "rollup-dev-server-cast" : "webpack-dev-server-cast"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
gulp.task(
|
||||||
|
"build-cast",
|
||||||
|
gulp.series(
|
||||||
|
async function setEnv() {
|
||||||
|
process.env.NODE_ENV = "production";
|
||||||
|
},
|
||||||
|
"clean-cast",
|
||||||
|
"translations-enable-merge-backend",
|
||||||
|
gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"),
|
||||||
|
"copy-static-cast",
|
||||||
|
env.useRollup() ? "rollup-prod-cast" : "webpack-prod-cast",
|
||||||
|
"gen-index-cast-prod"
|
||||||
|
)
|
||||||
|
);
|
@@ -1,38 +0,0 @@
|
|||||||
import { parallel, series } from "gulp";
|
|
||||||
import { cleanCast } from "./clean.ts";
|
|
||||||
import { genPagesCastDev, genPagesCastProd } from "./entry-html.ts";
|
|
||||||
import { copyStaticCast } from "./gather-static.ts";
|
|
||||||
import { genIconsJson } from "./gen-icons-json.ts";
|
|
||||||
import { buildLocaleData } from "./locale-data.ts";
|
|
||||||
import { rspackDevServerCast, rspackProdCast } from "./rspack.ts";
|
|
||||||
import "./service-worker.ts";
|
|
||||||
import {
|
|
||||||
buildTranslations,
|
|
||||||
translationsEnableMergeBackend,
|
|
||||||
} from "./translations.ts";
|
|
||||||
|
|
||||||
// develop-cast
|
|
||||||
export const developCast = series(
|
|
||||||
async () => {
|
|
||||||
process.env.NODE_ENV = "development";
|
|
||||||
},
|
|
||||||
cleanCast,
|
|
||||||
translationsEnableMergeBackend,
|
|
||||||
parallel(genIconsJson, buildTranslations, buildLocaleData),
|
|
||||||
copyStaticCast,
|
|
||||||
genPagesCastDev,
|
|
||||||
rspackDevServerCast
|
|
||||||
);
|
|
||||||
|
|
||||||
// build-cast
|
|
||||||
export const buildCast = series(
|
|
||||||
async () => {
|
|
||||||
process.env.NODE_ENV = "production";
|
|
||||||
},
|
|
||||||
cleanCast,
|
|
||||||
translationsEnableMergeBackend,
|
|
||||||
parallel(genIconsJson, buildTranslations, buildLocaleData),
|
|
||||||
copyStaticCast,
|
|
||||||
rspackProdCast,
|
|
||||||
genPagesCastProd
|
|
||||||
);
|
|
36
build-scripts/gulp/clean.js
Normal file
36
build-scripts/gulp/clean.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
const del = require("del");
|
||||||
|
const gulp = require("gulp");
|
||||||
|
const paths = require("../paths");
|
||||||
|
require("./translations");
|
||||||
|
|
||||||
|
gulp.task(
|
||||||
|
"clean",
|
||||||
|
gulp.parallel("clean-translations", () =>
|
||||||
|
del([paths.app_output_root, paths.build_dir])
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
gulp.task(
|
||||||
|
"clean-demo",
|
||||||
|
gulp.parallel("clean-translations", () =>
|
||||||
|
del([paths.demo_output_root, paths.build_dir])
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
gulp.task(
|
||||||
|
"clean-cast",
|
||||||
|
gulp.parallel("clean-translations", () =>
|
||||||
|
del([paths.cast_output_root, paths.build_dir])
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
gulp.task("clean-hassio", () =>
|
||||||
|
del([paths.hassio_output_root, paths.build_dir])
|
||||||
|
);
|
||||||
|
|
||||||
|
gulp.task(
|
||||||
|
"clean-gallery",
|
||||||
|
gulp.parallel("clean-translations", () =>
|
||||||
|
del([paths.gallery_output_root, paths.gallery_build, paths.build_dir])
|
||||||
|
)
|
||||||
|
);
|
@@ -1,31 +0,0 @@
|
|||||||
import { deleteSync } from "del";
|
|
||||||
import { parallel } from "gulp";
|
|
||||||
import paths from "../paths.ts";
|
|
||||||
import { cleanTranslations } from "./translations.ts";
|
|
||||||
|
|
||||||
export const clean = parallel(cleanTranslations, async () =>
|
|
||||||
deleteSync([paths.app_output_root, paths.build_dir])
|
|
||||||
);
|
|
||||||
|
|
||||||
export const cleanDemo = parallel(cleanTranslations, async () =>
|
|
||||||
deleteSync([paths.demo_output_root, paths.build_dir])
|
|
||||||
);
|
|
||||||
|
|
||||||
export const cleanCast = parallel(cleanTranslations, async () =>
|
|
||||||
deleteSync([paths.cast_output_root, paths.build_dir])
|
|
||||||
);
|
|
||||||
|
|
||||||
export const cleanHassio = async () =>
|
|
||||||
deleteSync([paths.hassio_output_root, paths.build_dir]);
|
|
||||||
|
|
||||||
export const cleanGallery = parallel(cleanTranslations, async () =>
|
|
||||||
deleteSync([paths.gallery_output_root, paths.gallery_build, paths.build_dir])
|
|
||||||
);
|
|
||||||
|
|
||||||
export const cleanLandingPage = parallel(cleanTranslations, async () =>
|
|
||||||
deleteSync([
|
|
||||||
paths.landingPage_output_root,
|
|
||||||
paths.landingPage_build,
|
|
||||||
paths.build_dir,
|
|
||||||
])
|
|
||||||
);
|
|
45
build-scripts/gulp/compress.js
Normal file
45
build-scripts/gulp/compress.js
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
// Tasks to compress
|
||||||
|
|
||||||
|
const gulp = require("gulp");
|
||||||
|
const zopfli = require("gulp-zopfli-green");
|
||||||
|
const merge = require("merge-stream");
|
||||||
|
const path = require("path");
|
||||||
|
const paths = require("../paths");
|
||||||
|
|
||||||
|
const zopfliOptions = { threshold: 150 };
|
||||||
|
|
||||||
|
gulp.task("compress-app", function compressApp() {
|
||||||
|
const jsLatest = gulp
|
||||||
|
.src(path.resolve(paths.app_output_latest, "**/*.js"))
|
||||||
|
.pipe(zopfli(zopfliOptions))
|
||||||
|
.pipe(gulp.dest(paths.app_output_latest));
|
||||||
|
|
||||||
|
const jsEs5 = gulp
|
||||||
|
.src(path.resolve(paths.app_output_es5, "**/*.js"))
|
||||||
|
.pipe(zopfli(zopfliOptions))
|
||||||
|
.pipe(gulp.dest(paths.app_output_es5));
|
||||||
|
|
||||||
|
const polyfills = gulp
|
||||||
|
.src(path.resolve(paths.app_output_static, "polyfills/*.js"))
|
||||||
|
.pipe(zopfli(zopfliOptions))
|
||||||
|
.pipe(gulp.dest(path.resolve(paths.app_output_static, "polyfills")));
|
||||||
|
|
||||||
|
const translations = gulp
|
||||||
|
.src(path.resolve(paths.app_output_static, "translations/**/*.json"))
|
||||||
|
.pipe(zopfli(zopfliOptions))
|
||||||
|
.pipe(gulp.dest(path.resolve(paths.app_output_static, "translations")));
|
||||||
|
|
||||||
|
const icons = gulp
|
||||||
|
.src(path.resolve(paths.app_output_static, "mdi/*.json"))
|
||||||
|
.pipe(zopfli(zopfliOptions))
|
||||||
|
.pipe(gulp.dest(path.resolve(paths.app_output_static, "mdi")));
|
||||||
|
|
||||||
|
return merge(jsLatest, jsEs5, polyfills, translations, icons);
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task("compress-hassio", function compressApp() {
|
||||||
|
return gulp
|
||||||
|
.src(path.resolve(paths.hassio_output_root, "**/*.js"))
|
||||||
|
.pipe(zopfli(zopfliOptions))
|
||||||
|
.pipe(gulp.dest(paths.hassio_output_root));
|
||||||
|
});
|
@@ -1,79 +0,0 @@
|
|||||||
// Tasks to compress
|
|
||||||
|
|
||||||
import { dest, parallel, src } from "gulp";
|
|
||||||
import brotli from "gulp-brotli";
|
|
||||||
import zopfli from "gulp-zopfli-green";
|
|
||||||
import { constants } from "node:zlib";
|
|
||||||
import paths from "../paths.ts";
|
|
||||||
|
|
||||||
const filesGlob = "*.{js,json,css,svg,xml}";
|
|
||||||
const brotliOptions = {
|
|
||||||
skipLarger: true,
|
|
||||||
params: {
|
|
||||||
[constants.BROTLI_PARAM_QUALITY]: constants.BROTLI_MAX_QUALITY,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const zopfliOptions = { threshold: 150 };
|
|
||||||
|
|
||||||
const compressModern = (rootDir, modernDir, compress) =>
|
|
||||||
src([`${modernDir}/**/${filesGlob}`, `${rootDir}/sw-modern.js`], {
|
|
||||||
base: rootDir,
|
|
||||||
allowEmpty: true,
|
|
||||||
})
|
|
||||||
.pipe(compress === "zopfli" ? zopfli(zopfliOptions) : brotli(brotliOptions))
|
|
||||||
.pipe(dest(rootDir));
|
|
||||||
|
|
||||||
const compressOther = (rootDir, modernDir, compress) =>
|
|
||||||
src(
|
|
||||||
[
|
|
||||||
`${rootDir}/**/${filesGlob}`,
|
|
||||||
`!${modernDir}/**/${filesGlob}`,
|
|
||||||
`!${rootDir}/{sw-modern,service_worker}.js`,
|
|
||||||
`${rootDir}/{authorize,onboarding}.html`,
|
|
||||||
],
|
|
||||||
{ base: rootDir, allowEmpty: true }
|
|
||||||
)
|
|
||||||
.pipe(compress === "zopfli" ? zopfli(zopfliOptions) : brotli(brotliOptions))
|
|
||||||
.pipe(dest(rootDir));
|
|
||||||
|
|
||||||
const compressAppModernBrotli = () =>
|
|
||||||
compressModern(paths.app_output_root, paths.app_output_latest, "brotli");
|
|
||||||
const compressAppModernZopfli = () =>
|
|
||||||
compressModern(paths.app_output_root, paths.app_output_latest, "zopfli");
|
|
||||||
|
|
||||||
const compressHassioModernBrotli = () =>
|
|
||||||
compressModern(
|
|
||||||
paths.hassio_output_root,
|
|
||||||
paths.hassio_output_latest,
|
|
||||||
"brotli"
|
|
||||||
);
|
|
||||||
const compressHassioModernZopfli = () =>
|
|
||||||
compressModern(
|
|
||||||
paths.hassio_output_root,
|
|
||||||
paths.hassio_output_latest,
|
|
||||||
"zopfli"
|
|
||||||
);
|
|
||||||
|
|
||||||
const compressAppOtherBrotli = () =>
|
|
||||||
compressOther(paths.app_output_root, paths.app_output_latest, "brotli");
|
|
||||||
const compressAppOtherZopfli = () =>
|
|
||||||
compressOther(paths.app_output_root, paths.app_output_latest, "zopfli");
|
|
||||||
|
|
||||||
const compressHassioOtherBrotli = () =>
|
|
||||||
compressOther(paths.hassio_output_root, paths.hassio_output_latest, "brotli");
|
|
||||||
const compressHassioOtherZopfli = () =>
|
|
||||||
compressOther(paths.hassio_output_root, paths.hassio_output_latest, "zopfli");
|
|
||||||
|
|
||||||
export const compressApp = parallel(
|
|
||||||
compressAppModernBrotli,
|
|
||||||
compressAppOtherBrotli,
|
|
||||||
compressAppModernZopfli,
|
|
||||||
compressAppOtherZopfli
|
|
||||||
);
|
|
||||||
|
|
||||||
export const compressHassio = parallel(
|
|
||||||
compressHassioModernBrotli,
|
|
||||||
compressHassioOtherBrotli,
|
|
||||||
compressHassioModernZopfli,
|
|
||||||
compressHassioOtherZopfli
|
|
||||||
);
|
|
48
build-scripts/gulp/demo.js
Normal file
48
build-scripts/gulp/demo.js
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
// Run demo develop mode
|
||||||
|
const gulp = require("gulp");
|
||||||
|
|
||||||
|
const env = require("../env");
|
||||||
|
|
||||||
|
require("./clean.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(
|
||||||
|
"develop-demo",
|
||||||
|
gulp.series(
|
||||||
|
async function setEnv() {
|
||||||
|
process.env.NODE_ENV = "development";
|
||||||
|
},
|
||||||
|
"clean-demo",
|
||||||
|
"translations-enable-merge-backend",
|
||||||
|
gulp.parallel(
|
||||||
|
"gen-icons-json",
|
||||||
|
"gen-index-demo-dev",
|
||||||
|
"build-translations",
|
||||||
|
"build-locale-data"
|
||||||
|
),
|
||||||
|
"copy-static-demo",
|
||||||
|
env.useRollup() ? "rollup-dev-server-demo" : "webpack-dev-server-demo"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
gulp.task(
|
||||||
|
"build-demo",
|
||||||
|
gulp.series(
|
||||||
|
async function setEnv() {
|
||||||
|
process.env.NODE_ENV = "production";
|
||||||
|
},
|
||||||
|
"clean-demo",
|
||||||
|
// Cast needs to be backwards compatible and older HA has no translations
|
||||||
|
"translations-enable-merge-backend",
|
||||||
|
gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"),
|
||||||
|
"copy-static-demo",
|
||||||
|
env.useRollup() ? "rollup-prod-demo" : "webpack-prod-demo",
|
||||||
|
"gen-index-demo-prod"
|
||||||
|
)
|
||||||
|
);
|
@@ -1,47 +0,0 @@
|
|||||||
import { parallel, series } from "gulp";
|
|
||||||
import { clean, cleanDemo } from "./clean.ts";
|
|
||||||
import { genPagesDemoDev, genPagesDemoProd } from "./entry-html.ts";
|
|
||||||
import { copyStaticDemo } from "./gather-static.ts";
|
|
||||||
import { genIconsJson } from "./gen-icons-json.ts";
|
|
||||||
import { buildLocaleData } from "./locale-data.ts";
|
|
||||||
import { rspackDevServerDemo, rspackProdDemo } from "./rspack.ts";
|
|
||||||
import "./service-worker.ts";
|
|
||||||
import {
|
|
||||||
buildTranslations,
|
|
||||||
translationsEnableMergeBackend,
|
|
||||||
} from "./translations.ts";
|
|
||||||
|
|
||||||
// develop-demo
|
|
||||||
export const developDemo = series(
|
|
||||||
async function setEnv() {
|
|
||||||
process.env.NODE_ENV = "development";
|
|
||||||
},
|
|
||||||
cleanDemo,
|
|
||||||
translationsEnableMergeBackend,
|
|
||||||
parallel(genIconsJson, genPagesDemoDev, buildTranslations, buildLocaleData),
|
|
||||||
copyStaticDemo,
|
|
||||||
rspackDevServerDemo
|
|
||||||
);
|
|
||||||
|
|
||||||
// build-demo
|
|
||||||
export const buildDemo = series(
|
|
||||||
async function setEnv() {
|
|
||||||
process.env.NODE_ENV = "production";
|
|
||||||
},
|
|
||||||
cleanDemo,
|
|
||||||
// Cast needs to be backwards compatible and older HA has no translations
|
|
||||||
translationsEnableMergeBackend,
|
|
||||||
parallel(genIconsJson, buildTranslations, buildLocaleData),
|
|
||||||
copyStaticDemo,
|
|
||||||
rspackProdDemo,
|
|
||||||
genPagesDemoProd
|
|
||||||
);
|
|
||||||
|
|
||||||
// analyze-demo
|
|
||||||
export const analyzeDemo = series(
|
|
||||||
async function setEnv() {
|
|
||||||
process.env.STATS = "1";
|
|
||||||
},
|
|
||||||
clean,
|
|
||||||
rspackProdDemo
|
|
||||||
);
|
|
@@ -1,178 +0,0 @@
|
|||||||
import { LokaliseApi } from "@lokalise/node-api";
|
|
||||||
import { dest, series, src } from "gulp";
|
|
||||||
import transform from "gulp-json-transform";
|
|
||||||
import JSZip from "jszip";
|
|
||||||
import mapStream from "map-stream";
|
|
||||||
import fs from "node:fs/promises";
|
|
||||||
import path from "node:path";
|
|
||||||
|
|
||||||
const inDir = "translations";
|
|
||||||
const inDirFrontend = `${inDir}/frontend`;
|
|
||||||
const inDirBackend = `${inDir}/backend`;
|
|
||||||
const srcMeta = "src/translations/translationMetadata.json";
|
|
||||||
const encoding = "utf8";
|
|
||||||
|
|
||||||
const hasHtml = (data) => /<\S*>/i.test(data);
|
|
||||||
|
|
||||||
const recursiveCheckHasHtml = (
|
|
||||||
file,
|
|
||||||
data,
|
|
||||||
errors: string[],
|
|
||||||
recKey?: string
|
|
||||||
) => {
|
|
||||||
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}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const 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);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const convertBackendTranslationsTransform = (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;
|
|
||||||
};
|
|
||||||
|
|
||||||
const convertBackendTranslations = () =>
|
|
||||||
src([`${inDirBackend}/*.json`])
|
|
||||||
.pipe(
|
|
||||||
transform((data, file) => convertBackendTranslationsTransform(data, file))
|
|
||||||
)
|
|
||||||
.pipe(dest(inDirBackend));
|
|
||||||
|
|
||||||
const checkTranslationsHtml = () =>
|
|
||||||
src([`${inDirFrontend}/*.json`, `${inDirBackend}/*.json`]).pipe(checkHtml());
|
|
||||||
|
|
||||||
const checkAllFilesExist = async () => {
|
|
||||||
const file = await fs.readFile(srcMeta, { encoding });
|
|
||||||
const meta = JSON.parse(file);
|
|
||||||
const writings: Promise<void>[] = [];
|
|
||||||
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",
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchLokalise = async () => {
|
|
||||||
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;
|
|
||||||
})
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const downloadTranslations = series(
|
|
||||||
fetchLokalise,
|
|
||||||
convertBackendTranslations,
|
|
||||||
checkTranslationsHtml,
|
|
||||||
checkAllFilesExist
|
|
||||||
);
|
|
95
build-scripts/gulp/download_translations.js
Normal file
95
build-scripts/gulp/download_translations.js
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
const del = require("del");
|
||||||
|
const gulp = require("gulp");
|
||||||
|
const fs = require("fs");
|
||||||
|
const mapStream = require("map-stream");
|
||||||
|
|
||||||
|
const inDirFrontend = "translations/frontend";
|
||||||
|
const inDirBackend = "translations/backend";
|
||||||
|
const downloadDir = "translations/downloads";
|
||||||
|
const srcMeta = "src/translations/translationMetadata.json";
|
||||||
|
|
||||||
|
const encoding = "utf8";
|
||||||
|
|
||||||
|
const tasks = [];
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let taskName = "clean-downloaded-translations";
|
||||||
|
gulp.task(taskName, function () {
|
||||||
|
return del([`${downloadDir}/**`]);
|
||||||
|
});
|
||||||
|
tasks.push(taskName);
|
||||||
|
|
||||||
|
taskName = "check-translations-html";
|
||||||
|
gulp.task(taskName, function () {
|
||||||
|
return gulp.src(`${downloadDir}/*.json`).pipe(checkHtml());
|
||||||
|
});
|
||||||
|
tasks.push(taskName);
|
||||||
|
|
||||||
|
taskName = "check-all-files-exist";
|
||||||
|
gulp.task(taskName, 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();
|
||||||
|
});
|
||||||
|
tasks.push(taskName);
|
||||||
|
|
||||||
|
taskName = "move-downloaded-translations";
|
||||||
|
gulp.task(taskName, function () {
|
||||||
|
return gulp.src(`${downloadDir}/*.json`).pipe(gulp.dest(inDirFrontend));
|
||||||
|
});
|
||||||
|
tasks.push(taskName);
|
||||||
|
|
||||||
|
taskName = "check-downloaded-translations";
|
||||||
|
gulp.task(
|
||||||
|
taskName,
|
||||||
|
gulp.series(
|
||||||
|
"check-translations-html",
|
||||||
|
"move-downloaded-translations",
|
||||||
|
"check-all-files-exist",
|
||||||
|
"clean-downloaded-translations"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
tasks.push(taskName);
|
||||||
|
|
||||||
|
module.exports = tasks;
|
344
build-scripts/gulp/entry-html.js
Normal file
344
build-scripts/gulp/entry-html.js
Normal file
@@ -0,0 +1,344 @@
|
|||||||
|
// 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");
|
||||||
|
|
||||||
|
const templatePath = (tpl) =>
|
||||||
|
path.resolve(paths.polymer_dir, "src/html/", `${tpl}.html.template`);
|
||||||
|
|
||||||
|
const readFile = (pth) => fs.readFileSync(pth).toString();
|
||||||
|
|
||||||
|
const renderTemplate = (pth, data = {}, pathFunc = templatePath) => {
|
||||||
|
const compiled = template(readFile(pathFunc(pth)));
|
||||||
|
return compiled({
|
||||||
|
...data,
|
||||||
|
useRollup: env.useRollup(),
|
||||||
|
useWDS: env.useWDS(),
|
||||||
|
renderTemplate,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderDemoTemplate = (pth, data = {}) =>
|
||||||
|
renderTemplate(pth, data, (tpl) =>
|
||||||
|
path.resolve(paths.demo_dir, "src/html/", `${tpl}.html.template`)
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderCastTemplate = (pth, data = {}) =>
|
||||||
|
renderTemplate(pth, data, (tpl) =>
|
||||||
|
path.resolve(paths.cast_dir, "src/html/", `${tpl}.html.template`)
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderGalleryTemplate = (pth, data = {}) =>
|
||||||
|
renderTemplate(pth, data, (tpl) =>
|
||||||
|
path.resolve(paths.gallery_dir, "src/html/", `${tpl}.html.template`)
|
||||||
|
);
|
||||||
|
|
||||||
|
const minifyHtml = (content) =>
|
||||||
|
minify(content, {
|
||||||
|
collapseWhitespace: true,
|
||||||
|
minifyJS: true,
|
||||||
|
minifyCSS: true,
|
||||||
|
removeComments: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const PAGES = ["onboarding", "authorize"];
|
||||||
|
|
||||||
|
gulp.task("gen-pages-dev", (done) => {
|
||||||
|
for (const page of PAGES) {
|
||||||
|
const content = renderTemplate(page, {
|
||||||
|
latestPageJS: `/frontend_latest/${page}.js`,
|
||||||
|
|
||||||
|
es5PageJS: `/frontend_es5/${page}.js`,
|
||||||
|
});
|
||||||
|
|
||||||
|
fs.outputFileSync(
|
||||||
|
path.resolve(paths.app_output_root, `${page}.html`),
|
||||||
|
content
|
||||||
|
);
|
||||||
|
}
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task("gen-pages-prod", (done) => {
|
||||||
|
const latestManifest = require(path.resolve(
|
||||||
|
paths.app_output_latest,
|
||||||
|
"manifest.json"
|
||||||
|
));
|
||||||
|
const es5Manifest = require(path.resolve(
|
||||||
|
paths.app_output_es5,
|
||||||
|
"manifest.json"
|
||||||
|
));
|
||||||
|
|
||||||
|
for (const page of PAGES) {
|
||||||
|
const content = renderTemplate(page, {
|
||||||
|
latestPageJS: latestManifest[`${page}.js`],
|
||||||
|
|
||||||
|
es5PageJS: es5Manifest[`${page}.js`],
|
||||||
|
});
|
||||||
|
|
||||||
|
fs.outputFileSync(
|
||||||
|
path.resolve(paths.app_output_root, `${page}.html`),
|
||||||
|
minifyHtml(content)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
"manifest.json"
|
||||||
|
));
|
||||||
|
const es5Manifest = require(path.resolve(
|
||||||
|
paths.cast_output_es5,
|
||||||
|
"manifest.json"
|
||||||
|
));
|
||||||
|
|
||||||
|
const contentReceiver = renderCastTemplate("receiver", {
|
||||||
|
latestReceiverJS: latestManifest["receiver.js"],
|
||||||
|
});
|
||||||
|
fs.outputFileSync(
|
||||||
|
path.resolve(paths.cast_output_root, "receiver.html"),
|
||||||
|
contentReceiver
|
||||||
|
);
|
||||||
|
|
||||||
|
const contentMedia = renderCastTemplate("media", {
|
||||||
|
latestMediaJS: latestManifest["media.js"],
|
||||||
|
es5MediaJS: es5Manifest["media.js"],
|
||||||
|
});
|
||||||
|
fs.outputFileSync(
|
||||||
|
path.resolve(paths.cast_output_root, "media.html"),
|
||||||
|
contentMedia
|
||||||
|
);
|
||||||
|
|
||||||
|
const contentFAQ = renderCastTemplate("launcher-faq", {
|
||||||
|
latestLauncherJS: latestManifest["launcher.js"],
|
||||||
|
es5LauncherJS: es5Manifest["launcher.js"],
|
||||||
|
});
|
||||||
|
fs.outputFileSync(
|
||||||
|
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,
|
||||||
|
"manifest.json"
|
||||||
|
));
|
||||||
|
const es5Manifest = require(path.resolve(
|
||||||
|
paths.demo_output_es5,
|
||||||
|
"manifest.json"
|
||||||
|
));
|
||||||
|
const content = renderDemoTemplate("index", {
|
||||||
|
latestDemoJS: latestManifest["main.js"],
|
||||||
|
|
||||||
|
es5DemoJS: es5Manifest["main.js"],
|
||||||
|
});
|
||||||
|
const minified = minifyHtml(content);
|
||||||
|
|
||||||
|
fs.outputFileSync(
|
||||||
|
path.resolve(paths.demo_output_root, "index.html"),
|
||||||
|
minified
|
||||||
|
);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task("gen-index-gallery-dev", (done) => {
|
||||||
|
const content = renderGalleryTemplate("index", {
|
||||||
|
latestGalleryJS: "./frontend_latest/entrypoint.js",
|
||||||
|
});
|
||||||
|
|
||||||
|
fs.outputFileSync(
|
||||||
|
path.resolve(paths.gallery_output_root, "index.html"),
|
||||||
|
content
|
||||||
|
);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task("gen-index-gallery-prod", (done) => {
|
||||||
|
const latestManifest = require(path.resolve(
|
||||||
|
paths.gallery_output_latest,
|
||||||
|
"manifest.json"
|
||||||
|
));
|
||||||
|
const content = renderGalleryTemplate("index", {
|
||||||
|
latestGalleryJS: latestManifest["entrypoint.js"],
|
||||||
|
});
|
||||||
|
const minified = minifyHtml(content);
|
||||||
|
|
||||||
|
fs.outputFileSync(
|
||||||
|
path.resolve(paths.gallery_output_root, "index.html"),
|
||||||
|
minified
|
||||||
|
);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task("gen-index-hassio-dev", async () => {
|
||||||
|
writeHassioEntrypoint(
|
||||||
|
`${paths.hassio_publicPath}/frontend_latest/entrypoint.js`,
|
||||||
|
`${paths.hassio_publicPath}/frontend_es5/entrypoint.js`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task("gen-index-hassio-prod", async () => {
|
||||||
|
const latestManifest = require(path.resolve(
|
||||||
|
paths.hassio_output_latest,
|
||||||
|
"manifest.json"
|
||||||
|
));
|
||||||
|
const es5Manifest = require(path.resolve(
|
||||||
|
paths.hassio_output_es5,
|
||||||
|
"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,272 +0,0 @@
|
|||||||
// Tasks to generate entry HTML
|
|
||||||
|
|
||||||
import {
|
|
||||||
applyVersionsToRegexes,
|
|
||||||
compileRegex,
|
|
||||||
getPreUserAgentRegexes,
|
|
||||||
} from "browserslist-useragent-regexp";
|
|
||||||
import fs from "fs-extra";
|
|
||||||
import { minify } from "html-minifier-terser";
|
|
||||||
import template from "lodash.template";
|
|
||||||
import { dirname, extname, resolve } from "node:path";
|
|
||||||
import { htmlMinifierOptions, terserOptions } from "../bundle.ts";
|
|
||||||
import paths from "../paths.ts";
|
|
||||||
|
|
||||||
// macOS companion app has no way to obtain the Safari version used by WKWebView,
|
|
||||||
// and it is not in the default user agent string. So we add an additional regex
|
|
||||||
// to serve modern based on a minimum macOS version. We take the minimum Safari
|
|
||||||
// major version from browserslist and manually map that to a supported macOS
|
|
||||||
// version. Note this assumes the user has kept Safari updated.
|
|
||||||
const HA_MACOS_REGEX =
|
|
||||||
/Home Assistant\/[\d.]+ \(.+; macOS (\d+)\.(\d+)(?:\.(\d+))?\)/;
|
|
||||||
const SAFARI_TO_MACOS = {
|
|
||||||
15: [10, 15, 0],
|
|
||||||
16: [11, 0, 0],
|
|
||||||
17: [12, 0, 0],
|
|
||||||
18: [13, 0, 0],
|
|
||||||
};
|
|
||||||
|
|
||||||
const getCommonTemplateVars = () => {
|
|
||||||
const browserRegexes = getPreUserAgentRegexes({
|
|
||||||
env: "modern",
|
|
||||||
allowHigherVersions: true,
|
|
||||||
mobileToDesktop: true,
|
|
||||||
throwOnMissing: true,
|
|
||||||
});
|
|
||||||
const minSafariVersion =
|
|
||||||
browserRegexes.find((regex) => regex.family === "safari")
|
|
||||||
?.matchedVersions[0][0] ?? 18;
|
|
||||||
const minMacOSVersion = SAFARI_TO_MACOS[minSafariVersion];
|
|
||||||
if (!minMacOSVersion) {
|
|
||||||
throw Error(
|
|
||||||
`Could not find minimum MacOS version for Safari ${minSafariVersion}.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const haMacOSRegex = applyVersionsToRegexes(
|
|
||||||
[
|
|
||||||
{
|
|
||||||
family: "ha_macos",
|
|
||||||
regex: HA_MACOS_REGEX,
|
|
||||||
matchedVersions: [minMacOSVersion],
|
|
||||||
requestVersions: [minMacOSVersion],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
{ ignorePatch: true, allowHigherVersions: true }
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
modernRegex: compileRegex(browserRegexes.concat(haMacOSRegex)).toString(),
|
|
||||||
hassUrl: process.env.HASS_URL || "",
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderTemplate = (templateFile, data = {}) => {
|
|
||||||
const compiled = template(
|
|
||||||
fs.readFileSync(templateFile, { encoding: "utf-8" })
|
|
||||||
);
|
|
||||||
return compiled({
|
|
||||||
...data,
|
|
||||||
// Resolve any child/nested templates relative to the parent and pass the same data
|
|
||||||
renderTemplate: (childTemplate) =>
|
|
||||||
renderTemplate(resolve(dirname(templateFile), childTemplate), data),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const WRAP_TAGS = { ".js": "script", ".css": "style" };
|
|
||||||
|
|
||||||
const minifyHtml = (content, ext) => {
|
|
||||||
const wrapTag = WRAP_TAGS[ext] || "";
|
|
||||||
const begTag = wrapTag && `<${wrapTag}>`;
|
|
||||||
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 genPagesDevTask =
|
|
||||||
(
|
|
||||||
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 as string[]).map(
|
|
||||||
(entry) => `${publicRoot}/frontend_latest/${entry}.js`
|
|
||||||
),
|
|
||||||
es5EntryJS: (entries as string[]).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
|
|
||||||
// (includes minification and hashed file names from manifest)
|
|
||||||
const genPagesProdTask =
|
|
||||||
(
|
|
||||||
pageEntries,
|
|
||||||
inputRoot,
|
|
||||||
outputRoot,
|
|
||||||
outputLatest,
|
|
||||||
outputES5?: string,
|
|
||||||
inputSub = "src/html"
|
|
||||||
) =>
|
|
||||||
async () => {
|
|
||||||
const latestManifest = fs.readJsonSync(
|
|
||||||
resolve(outputLatest, "manifest.json")
|
|
||||||
);
|
|
||||||
const es5Manifest = outputES5
|
|
||||||
? fs.readJsonSync(resolve(outputES5, "manifest.json"))
|
|
||||||
: {};
|
|
||||||
const commonVars = getCommonTemplateVars();
|
|
||||||
const minifiedHTML: Promise<void>[] = [];
|
|
||||||
for (const [page, entries] of Object.entries(pageEntries)) {
|
|
||||||
const content = renderTemplate(
|
|
||||||
resolve(inputRoot, inputSub, `${page}.template`),
|
|
||||||
{
|
|
||||||
...commonVars,
|
|
||||||
latestEntryJS: (entries as string[]).map(
|
|
||||||
(entry) => latestManifest[`${entry}.js`]
|
|
||||||
),
|
|
||||||
es5EntryJS: (entries as string[]).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);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Map HTML pages to their required entrypoints
|
|
||||||
const APP_PAGE_ENTRIES = {
|
|
||||||
"authorize.html": ["authorize"],
|
|
||||||
"onboarding.html": ["onboarding"],
|
|
||||||
"index.html": ["core", "app"],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const genPagesAppDev = genPagesDevTask(
|
|
||||||
APP_PAGE_ENTRIES,
|
|
||||||
paths.root_dir,
|
|
||||||
paths.app_output_root
|
|
||||||
);
|
|
||||||
|
|
||||||
export const genPagesAppProd = genPagesProdTask(
|
|
||||||
APP_PAGE_ENTRIES,
|
|
||||||
paths.root_dir,
|
|
||||||
paths.app_output_root,
|
|
||||||
paths.app_output_latest,
|
|
||||||
paths.app_output_es5
|
|
||||||
);
|
|
||||||
|
|
||||||
const CAST_PAGE_ENTRIES = {
|
|
||||||
"faq.html": ["launcher"],
|
|
||||||
"index.html": ["launcher"],
|
|
||||||
"media.html": ["media"],
|
|
||||||
"receiver.html": ["receiver"],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const genPagesCastDev = genPagesDevTask(
|
|
||||||
CAST_PAGE_ENTRIES,
|
|
||||||
paths.cast_dir,
|
|
||||||
paths.cast_output_root
|
|
||||||
);
|
|
||||||
|
|
||||||
export const genPagesCastProd = genPagesProdTask(
|
|
||||||
CAST_PAGE_ENTRIES,
|
|
||||||
paths.cast_dir,
|
|
||||||
paths.cast_output_root,
|
|
||||||
paths.cast_output_latest,
|
|
||||||
paths.cast_output_es5
|
|
||||||
);
|
|
||||||
|
|
||||||
const DEMO_PAGE_ENTRIES = { "index.html": ["main"] };
|
|
||||||
|
|
||||||
export const genPagesDemoDev = genPagesDevTask(
|
|
||||||
DEMO_PAGE_ENTRIES,
|
|
||||||
paths.demo_dir,
|
|
||||||
paths.demo_output_root
|
|
||||||
);
|
|
||||||
|
|
||||||
export const genPagesDemoProd = genPagesProdTask(
|
|
||||||
DEMO_PAGE_ENTRIES,
|
|
||||||
paths.demo_dir,
|
|
||||||
paths.demo_output_root,
|
|
||||||
paths.demo_output_latest,
|
|
||||||
paths.demo_output_es5
|
|
||||||
);
|
|
||||||
|
|
||||||
const GALLERY_PAGE_ENTRIES = { "index.html": ["entrypoint"] };
|
|
||||||
|
|
||||||
export const genPagesGalleryDev = genPagesDevTask(
|
|
||||||
GALLERY_PAGE_ENTRIES,
|
|
||||||
paths.gallery_dir,
|
|
||||||
paths.gallery_output_root
|
|
||||||
);
|
|
||||||
|
|
||||||
export const genPagesGalleryProd = genPagesProdTask(
|
|
||||||
GALLERY_PAGE_ENTRIES,
|
|
||||||
paths.gallery_dir,
|
|
||||||
paths.gallery_output_root,
|
|
||||||
paths.gallery_output_latest
|
|
||||||
);
|
|
||||||
|
|
||||||
const LANDING_PAGE_PAGE_ENTRIES = { "index.html": ["entrypoint"] };
|
|
||||||
|
|
||||||
export const genPagesLandingPageDev = genPagesDevTask(
|
|
||||||
LANDING_PAGE_PAGE_ENTRIES,
|
|
||||||
paths.landingPage_dir,
|
|
||||||
paths.landingPage_output_root
|
|
||||||
);
|
|
||||||
|
|
||||||
export const genPagesLandingPageProd = genPagesProdTask(
|
|
||||||
LANDING_PAGE_PAGE_ENTRIES,
|
|
||||||
paths.landingPage_dir,
|
|
||||||
paths.landingPage_output_root,
|
|
||||||
paths.landingPage_output_latest,
|
|
||||||
paths.landingPage_output_es5
|
|
||||||
);
|
|
||||||
|
|
||||||
const HASSIO_PAGE_ENTRIES = { "entrypoint.js": ["entrypoint"] };
|
|
||||||
|
|
||||||
export const genPagesHassioDev = genPagesDevTask(
|
|
||||||
HASSIO_PAGE_ENTRIES,
|
|
||||||
paths.hassio_dir,
|
|
||||||
paths.hassio_output_root,
|
|
||||||
"src",
|
|
||||||
paths.hassio_publicPath
|
|
||||||
);
|
|
||||||
|
|
||||||
export const genPagesHassioProd = genPagesProdTask(
|
|
||||||
HASSIO_PAGE_ENTRIES,
|
|
||||||
paths.hassio_dir,
|
|
||||||
paths.hassio_output_root,
|
|
||||||
paths.hassio_output_latest,
|
|
||||||
paths.hassio_output_es5,
|
|
||||||
"src"
|
|
||||||
);
|
|
@@ -1,170 +0,0 @@
|
|||||||
// Task to download the latest 00Lokalise 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 { series } from "gulp";
|
|
||||||
import jszip from "jszip";
|
|
||||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
||||||
import path from "node:path";
|
|
||||||
import process from "node: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;
|
|
||||||
|
|
||||||
export const allowSetupFetchNightlyTranslations = (done) => {
|
|
||||||
allowTokenSetup = true;
|
|
||||||
done();
|
|
||||||
};
|
|
||||||
|
|
||||||
export const fetchNightlyTranslations = async () => {
|
|
||||||
// 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: Promise<void>[] = [];
|
|
||||||
|
|
||||||
// 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",
|
|
||||||
});
|
|
||||||
// @ts-ignore OctokitResponse<unknown, 302> doesn't allow to check for 200
|
|
||||||
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 as any);
|
|
||||||
await deleteCurrent;
|
|
||||||
const extractStream = zip.file(/.*/)[0].nodeStream().pipe(extract());
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
extractStream.on("close", resolve).on("error", reject);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const setupAndFetchNightlyTranslations = series(
|
|
||||||
allowSetupFetchNightlyTranslations,
|
|
||||||
fetchNightlyTranslations
|
|
||||||
);
|
|
@@ -1,32 +1,34 @@
|
|||||||
import { glob } from "glob";
|
/* eslint-disable */
|
||||||
import { parallel, series, watch } from "gulp";
|
// Run demo develop mode
|
||||||
import yaml from "js-yaml";
|
const gulp = require("gulp");
|
||||||
import { marked } from "marked";
|
const fs = require("fs");
|
||||||
import fs from "node:fs";
|
const path = require("path");
|
||||||
import path from "node:path";
|
const marked = require("marked");
|
||||||
import paths from "../paths.ts";
|
const glob = require("glob");
|
||||||
import { cleanGallery } from "./clean.ts";
|
const yaml = require("js-yaml");
|
||||||
import { genPagesGalleryDev, genPagesGalleryProd } from "./entry-html.ts";
|
|
||||||
import { copyStaticGallery } from "./gather-static.ts";
|
|
||||||
import { genIconsJson } from "./gen-icons-json.ts";
|
|
||||||
import { buildLocaleData } from "./locale-data.ts";
|
|
||||||
import { rspackDevServerGallery, rspackProdGallery } from "./rspack.ts";
|
|
||||||
import {
|
|
||||||
buildTranslations,
|
|
||||||
translationsEnableMergeBackend,
|
|
||||||
} from "./translations.ts";
|
|
||||||
|
|
||||||
// gather-gallery-pages
|
const env = require("../env");
|
||||||
export const gatherGalleryPages = async function gatherPages() {
|
const paths = require("../paths");
|
||||||
|
|
||||||
|
require("./clean.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() {
|
||||||
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 });
|
||||||
|
|
||||||
let content = "export const PAGES = {\n";
|
let content = "export const PAGES = {\n";
|
||||||
|
|
||||||
const processed = new Set<string>();
|
const processed = new Set();
|
||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
if (fs.lstatSync(file).isDirectory()) {
|
if (fs.lstatSync(file).isDirectory()) {
|
||||||
@@ -39,7 +41,7 @@ export const gatherGalleryPages = 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`);
|
||||||
@@ -51,9 +53,7 @@ export const gatherGalleryPages = async function gatherPages() {
|
|||||||
|
|
||||||
if (descriptionContent.startsWith("---")) {
|
if (descriptionContent.startsWith("---")) {
|
||||||
const metadataEnd = descriptionContent.indexOf("---", 3);
|
const metadataEnd = descriptionContent.indexOf("---", 3);
|
||||||
metadata = yaml.load(
|
metadata = yaml.load(descriptionContent.substring(3, metadataEnd));
|
||||||
descriptionContent.substring(3, metadataEnd)
|
|
||||||
) as any;
|
|
||||||
descriptionContent = descriptionContent
|
descriptionContent = descriptionContent
|
||||||
.substring(metadataEnd + 3)
|
.substring(metadataEnd + 3)
|
||||||
.trim();
|
.trim();
|
||||||
@@ -63,9 +63,7 @@ export const gatherGalleryPages = async function gatherPages() {
|
|||||||
if (descriptionContent === "") {
|
if (descriptionContent === "") {
|
||||||
hasDescription = false;
|
hasDescription = false;
|
||||||
} else {
|
} else {
|
||||||
// eslint-disable-next-line no-await-in-loop
|
descriptionContent = marked(descriptionContent).replace(/`/g, "\\`");
|
||||||
descriptionContent = await marked(descriptionContent);
|
|
||||||
descriptionContent = descriptionContent.replace(/`/g, "\\`");
|
|
||||||
fs.mkdirSync(path.resolve(galleryBuild, category), { recursive: true });
|
fs.mkdirSync(path.resolve(galleryBuild, category), { recursive: true });
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
path.resolve(galleryBuild, `${pageId}-description.ts`),
|
path.resolve(galleryBuild, `${pageId}-description.ts`),
|
||||||
@@ -92,7 +90,9 @@ export const gatherGalleryPages = 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) {
|
||||||
@@ -103,10 +103,7 @@ export const gatherGalleryPages = async function gatherPages() {
|
|||||||
pagesToProcess[category].add(page);
|
pagesToProcess[category].add(page);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const group of Object.values(sidebar) as {
|
for (const group of Object.values(sidebar)) {
|
||||||
category: string;
|
|
||||||
pages?: string[];
|
|
||||||
}[]) {
|
|
||||||
const toProcess = pagesToProcess[group.category];
|
const toProcess = pagesToProcess[group.category];
|
||||||
delete pagesToProcess[group.category];
|
delete pagesToProcess[group.category];
|
||||||
|
|
||||||
@@ -129,7 +126,7 @@ export const gatherGalleryPages = async function gatherPages() {
|
|||||||
group.pages = [];
|
group.pages = [];
|
||||||
}
|
}
|
||||||
for (const page of Array.from(toProcess).sort()) {
|
for (const page of Array.from(toProcess).sort()) {
|
||||||
group.pages.push(page as string);
|
group.pages.push(page);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,7 +134,7 @@ export const gatherGalleryPages = async function gatherPages() {
|
|||||||
sidebar.push({
|
sidebar.push({
|
||||||
category,
|
category,
|
||||||
header: category,
|
header: category,
|
||||||
pages: Array.from(pages as Set<string>).sort(),
|
pages: Array.from(pages).sort(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,48 +145,57 @@ export const gatherGalleryPages = async function gatherPages() {
|
|||||||
content,
|
content,
|
||||||
"utf-8"
|
"utf-8"
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
// develop-gallery
|
gulp.task(
|
||||||
export const developGallery = series(
|
"develop-gallery",
|
||||||
|
gulp.series(
|
||||||
async function setEnv() {
|
async function setEnv() {
|
||||||
process.env.NODE_ENV = "development";
|
process.env.NODE_ENV = "development";
|
||||||
},
|
},
|
||||||
cleanGallery,
|
"clean-gallery",
|
||||||
translationsEnableMergeBackend,
|
"translations-enable-merge-backend",
|
||||||
parallel(
|
gulp.parallel(
|
||||||
genIconsJson,
|
"gen-icons-json",
|
||||||
buildTranslations,
|
"build-translations",
|
||||||
buildLocaleData,
|
"build-locale-data",
|
||||||
gatherGalleryPages
|
"gather-gallery-pages"
|
||||||
),
|
),
|
||||||
copyStaticGallery,
|
"copy-static-gallery",
|
||||||
genPagesGalleryDev,
|
"gen-index-gallery-dev",
|
||||||
parallel(rspackDevServerGallery, async function watchMarkdownFiles() {
|
gulp.parallel(
|
||||||
watch(
|
env.useRollup()
|
||||||
|
? "rollup-dev-server-gallery"
|
||||||
|
: "webpack-dev-server-gallery",
|
||||||
|
async function watchMarkdownFiles() {
|
||||||
|
gulp.watch(
|
||||||
[
|
[
|
||||||
path.resolve(paths.gallery_dir, "src/pages/**/*.markdown"),
|
path.resolve(paths.gallery_dir, "src/pages/**/*.markdown"),
|
||||||
path.resolve(paths.gallery_dir, "sidebar.js"),
|
path.resolve(paths.gallery_dir, "sidebar.js"),
|
||||||
],
|
],
|
||||||
series(gatherGalleryPages)
|
gulp.series("gather-gallery-pages")
|
||||||
);
|
);
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// build-gallery
|
gulp.task(
|
||||||
export const buildGallery = series(
|
"build-gallery",
|
||||||
|
gulp.series(
|
||||||
async function setEnv() {
|
async function setEnv() {
|
||||||
process.env.NODE_ENV = "production";
|
process.env.NODE_ENV = "production";
|
||||||
},
|
},
|
||||||
cleanGallery,
|
"clean-gallery",
|
||||||
translationsEnableMergeBackend,
|
"translations-enable-merge-backend",
|
||||||
parallel(
|
gulp.parallel(
|
||||||
genIconsJson,
|
"gen-icons-json",
|
||||||
buildTranslations,
|
"build-translations",
|
||||||
buildLocaleData,
|
"build-locale-data",
|
||||||
gatherGalleryPages
|
"gather-gallery-pages"
|
||||||
),
|
),
|
||||||
copyStaticGallery,
|
"copy-static-gallery",
|
||||||
rspackProdGallery,
|
env.useRollup() ? "rollup-prod-gallery" : "webpack-prod-gallery",
|
||||||
genPagesGalleryProd
|
"gen-index-gallery-prod"
|
||||||
|
)
|
||||||
);
|
);
|
@@ -1,12 +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 path from "node:path";
|
const path = require("path");
|
||||||
import paths from "../paths.ts";
|
const fs = require("fs-extra");
|
||||||
|
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)));
|
||||||
@@ -16,7 +17,7 @@ const genStaticPath =
|
|||||||
(...parts) =>
|
(...parts) =>
|
||||||
path.resolve(staticDir, ...parts);
|
path.resolve(staticDir, ...parts);
|
||||||
|
|
||||||
const copyTranslations = (staticDir) => {
|
function copyTranslations(staticDir) {
|
||||||
const staticPath = genStaticPath(staticDir);
|
const staticPath = genStaticPath(staticDir);
|
||||||
|
|
||||||
// Translation output
|
// Translation output
|
||||||
@@ -24,23 +25,23 @@ const copyTranslations = (staticDir) => {
|
|||||||
polyPath("build/translations/output"),
|
polyPath("build/translations/output"),
|
||||||
staticPath("translations")
|
staticPath("translations")
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
const copyLocaleData = (staticDir) => {
|
function copyLocaleData(staticDir) {
|
||||||
const staticPath = genStaticPath(staticDir);
|
const staticPath = genStaticPath(staticDir);
|
||||||
|
|
||||||
// Locale data output
|
// Locale data output
|
||||||
fs.copySync(polyPath("build/locale-data"), staticPath("locale-data"));
|
fs.copySync(polyPath("build/locale-data"), staticPath("locale-data"));
|
||||||
};
|
}
|
||||||
|
|
||||||
const copyMdiIcons = (staticDir) => {
|
function copyMdiIcons(staticDir) {
|
||||||
const staticPath = genStaticPath(staticDir);
|
const staticPath = genStaticPath(staticDir);
|
||||||
|
|
||||||
// MDI icons output
|
// MDI icons output
|
||||||
fs.copySync(polyPath("build/mdi"), staticPath("mdi"));
|
fs.copySync(polyPath("build/mdi"), staticPath("mdi"));
|
||||||
};
|
}
|
||||||
|
|
||||||
const copyPolyfills = (staticDir) => {
|
function copyPolyfills(staticDir) {
|
||||||
const staticPath = genStaticPath(staticDir);
|
const staticPath = genStaticPath(staticDir);
|
||||||
|
|
||||||
// For custom panels using ES5 builds that don't use Babel 7+
|
// For custom panels using ES5 builds that don't use Babel 7+
|
||||||
@@ -58,20 +59,15 @@ const 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"));
|
||||||
);
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const copyFonts = (staticDir) => {
|
function copyFonts(staticDir) {
|
||||||
const staticPath = genStaticPath(staticDir);
|
const staticPath = genStaticPath(staticDir);
|
||||||
// Local fonts
|
// Local fonts
|
||||||
fs.copySync(
|
fs.copySync(
|
||||||
@@ -81,62 +77,51 @@ const copyFonts = (staticDir) => {
|
|||||||
filter: (src) => !src.includes(".") || src.endsWith(".woff2"),
|
filter: (src) => !src.includes(".") || src.endsWith(".woff2"),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
const copyQrScannerWorker = (staticDir) => {
|
function copyQrScannerWorker(staticDir) {
|
||||||
const staticPath = genStaticPath(staticDir);
|
const staticPath = genStaticPath(staticDir);
|
||||||
copyFileDir(npmPath("qr-scanner/qr-scanner-worker.min.js"), staticPath("js"));
|
copyFileDir(npmPath("qr-scanner/qr-scanner-worker.min.js"), staticPath("js"));
|
||||||
};
|
}
|
||||||
|
|
||||||
const copyMapPanel = (staticDir) => {
|
function copyMapPanel(staticDir) {
|
||||||
const staticPath = genStaticPath(staticDir);
|
const staticPath = genStaticPath(staticDir);
|
||||||
copyFileDir(
|
copyFileDir(
|
||||||
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/")
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
const copyZXingWasm = (staticDir) => {
|
gulp.task("copy-locale-data", async () => {
|
||||||
const staticPath = genStaticPath(staticDir);
|
const staticDir = paths.app_output_static;
|
||||||
copyFileDir(
|
copyLocaleData(staticDir);
|
||||||
npmPath("zxing-wasm/dist/reader/zxing_reader.wasm"),
|
});
|
||||||
staticPath("js")
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const copyTranslationsApp = async () => {
|
gulp.task("copy-translations-app", async () => {
|
||||||
const staticDir = paths.app_output_static;
|
const staticDir = paths.app_output_static;
|
||||||
copyTranslations(staticDir);
|
copyTranslations(staticDir);
|
||||||
};
|
});
|
||||||
|
|
||||||
export const copyTranslationsSupervisor = async () => {
|
gulp.task("copy-translations-supervisor", async () => {
|
||||||
const staticDir = paths.hassio_output_static;
|
const staticDir = paths.hassio_output_static;
|
||||||
copyTranslations(staticDir);
|
copyTranslations(staticDir);
|
||||||
};
|
});
|
||||||
|
|
||||||
export const copyTranslationsLandingPage = async () => {
|
gulp.task("copy-locale-data-supervisor", async () => {
|
||||||
const staticDir = paths.landingPage_output_static;
|
|
||||||
copyTranslations(staticDir);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const copyStaticSupervisor = async () => {
|
|
||||||
const staticDir = paths.hassio_output_static;
|
const staticDir = paths.hassio_output_static;
|
||||||
copyLocaleData(staticDir);
|
copyLocaleData(staticDir);
|
||||||
copyFonts(staticDir);
|
});
|
||||||
};
|
|
||||||
|
|
||||||
export const copyStaticApp = 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);
|
||||||
@@ -147,11 +132,10 @@ export const copyStaticApp = async () => {
|
|||||||
copyMapPanel(staticDir);
|
copyMapPanel(staticDir);
|
||||||
|
|
||||||
// Qr Scanner assets
|
// Qr Scanner assets
|
||||||
copyZXingWasm(staticDir);
|
|
||||||
copyQrScannerWorker(staticDir);
|
copyQrScannerWorker(staticDir);
|
||||||
};
|
});
|
||||||
|
|
||||||
export const copyStaticDemo = async () => {
|
gulp.task("copy-static-demo", async () => {
|
||||||
// Copy app static files
|
// Copy app static files
|
||||||
fs.copySync(
|
fs.copySync(
|
||||||
polyPath("public/static"),
|
polyPath("public/static"),
|
||||||
@@ -159,28 +143,32 @@ export const copyStaticDemo = 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);
|
||||||
copyTranslations(paths.demo_output_static);
|
copyTranslations(paths.demo_output_static);
|
||||||
copyLocaleData(paths.demo_output_static);
|
copyLocaleData(paths.demo_output_static);
|
||||||
copyMdiIcons(paths.demo_output_static);
|
copyMdiIcons(paths.demo_output_static);
|
||||||
};
|
});
|
||||||
|
|
||||||
export const copyStaticCast = async () => {
|
gulp.task("copy-static-cast", async () => {
|
||||||
// Copy app static files
|
// Copy app static files
|
||||||
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);
|
||||||
copyTranslations(paths.cast_output_static);
|
copyTranslations(paths.cast_output_static);
|
||||||
copyLocaleData(paths.cast_output_static);
|
copyLocaleData(paths.cast_output_static);
|
||||||
copyMdiIcons(paths.cast_output_static);
|
copyMdiIcons(paths.cast_output_static);
|
||||||
};
|
});
|
||||||
|
|
||||||
export const copyStaticGallery = async () => {
|
gulp.task("copy-static-gallery", async () => {
|
||||||
// Copy app static files
|
// Copy app static files
|
||||||
fs.copySync(polyPath("public/static"), paths.gallery_output_static);
|
fs.copySync(polyPath("public/static"), paths.gallery_output_static);
|
||||||
// Copy gallery static files
|
// Copy gallery static files
|
||||||
@@ -194,15 +182,4 @@ export const copyStaticGallery = async () => {
|
|||||||
copyTranslations(paths.gallery_output_static);
|
copyTranslations(paths.gallery_output_static);
|
||||||
copyLocaleData(paths.gallery_output_static);
|
copyLocaleData(paths.gallery_output_static);
|
||||||
copyMdiIcons(paths.gallery_output_static);
|
copyMdiIcons(paths.gallery_output_static);
|
||||||
};
|
});
|
||||||
|
|
||||||
export const copyStaticLandingPage = 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,14 +1,17 @@
|
|||||||
import fs from "node:fs";
|
const gulp = require("gulp");
|
||||||
import path from "node:path";
|
const path = require("path");
|
||||||
import hash from "object-hash";
|
const fs = require("fs");
|
||||||
import paths from "../paths.ts";
|
const hash = require("object-hash");
|
||||||
|
|
||||||
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";
|
||||||
|
|
||||||
@@ -20,7 +23,7 @@ const getMeta = () => {
|
|||||||
encoding,
|
encoding,
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
path: svg.match(/ d="([^"]+)"/)?.[1],
|
path: svg.match(/ d="([^"]+)"/)[1],
|
||||||
name: icon.name,
|
name: icon.name,
|
||||||
tags: icon.tags,
|
tags: icon.tags,
|
||||||
aliases: icon.aliases,
|
aliases: icon.aliases,
|
||||||
@@ -54,14 +57,14 @@ const orderMeta = (meta) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const splitBySize = (meta) => {
|
const splitBySize = (meta) => {
|
||||||
const chunks: any[] = [];
|
const chunks = [];
|
||||||
const CHUNK_SIZE = 50000;
|
const CHUNK_SIZE = 50000;
|
||||||
|
|
||||||
let curSize = 0;
|
let curSize = 0;
|
||||||
let startKey;
|
let startKey;
|
||||||
let icons: any[] = [];
|
let icons = [];
|
||||||
|
|
||||||
Object.values(meta).forEach((icon: any) => {
|
Object.values(meta).forEach((icon) => {
|
||||||
if (startKey === undefined) {
|
if (startKey === undefined) {
|
||||||
startKey = icon.name;
|
startKey = icon.name;
|
||||||
}
|
}
|
||||||
@@ -93,10 +96,10 @@ const findDifferentiator = (curString, prevString) => {
|
|||||||
return curString.substring(0, i + 1);
|
return curString.substring(0, i + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw new Error(`Cannot find differentiator; ${curString}; ${prevString}`);
|
throw new Error("Cannot find differentiator", curString, prevString);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const genIconsJson = (done) => {
|
gulp.task("gen-icons-json", (done) => {
|
||||||
const meta = getMeta();
|
const meta = getMeta();
|
||||||
|
|
||||||
const metaAndRemoved = addRemovedMeta(meta);
|
const metaAndRemoved = addRemovedMeta(meta);
|
||||||
@@ -105,7 +108,7 @@ export const genIconsJson = (done) => {
|
|||||||
if (!fs.existsSync(OUTPUT_DIR)) {
|
if (!fs.existsSync(OUTPUT_DIR)) {
|
||||||
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
||||||
}
|
}
|
||||||
const parts: any[] = [];
|
const parts = [];
|
||||||
|
|
||||||
let lastEnd;
|
let lastEnd;
|
||||||
split.forEach((chunk) => {
|
split.forEach((chunk) => {
|
||||||
@@ -131,11 +134,11 @@ export const genIconsJson = (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(
|
||||||
@@ -152,13 +155,4 @@ export const genIconsJson = (done) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
done();
|
done();
|
||||||
};
|
});
|
||||||
|
|
||||||
export const genDummyIconsJson = (done) => {
|
|
||||||
if (!fs.existsSync(OUTPUT_DIR)) {
|
|
||||||
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.writeFileSync(path.resolve(OUTPUT_DIR, "iconList.json"), "[]");
|
|
||||||
done();
|
|
||||||
};
|
|
45
build-scripts/gulp/hassio.js
Normal file
45
build-scripts/gulp/hassio.js
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
const gulp = require("gulp");
|
||||||
|
|
||||||
|
const env = require("../env");
|
||||||
|
|
||||||
|
require("./clean.js");
|
||||||
|
require("./gen-icons-json.js");
|
||||||
|
require("./webpack.js");
|
||||||
|
require("./compress.js");
|
||||||
|
require("./rollup.js");
|
||||||
|
require("./gather-static.js");
|
||||||
|
require("./translations.js");
|
||||||
|
|
||||||
|
gulp.task(
|
||||||
|
"develop-hassio",
|
||||||
|
gulp.series(
|
||||||
|
async function setEnv() {
|
||||||
|
process.env.NODE_ENV = "development";
|
||||||
|
},
|
||||||
|
"clean-hassio",
|
||||||
|
"gen-index-hassio-dev",
|
||||||
|
"build-supervisor-translations",
|
||||||
|
"copy-translations-supervisor",
|
||||||
|
"build-locale-data",
|
||||||
|
"copy-locale-data-supervisor",
|
||||||
|
env.useRollup() ? "rollup-watch-hassio" : "webpack-watch-hassio"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
gulp.task(
|
||||||
|
"build-hassio",
|
||||||
|
gulp.series(
|
||||||
|
async function setEnv() {
|
||||||
|
process.env.NODE_ENV = "production";
|
||||||
|
},
|
||||||
|
"clean-hassio",
|
||||||
|
"build-supervisor-translations",
|
||||||
|
"copy-translations-supervisor",
|
||||||
|
"build-locale-data",
|
||||||
|
"copy-locale-data-supervisor",
|
||||||
|
env.useRollup() ? "rollup-prod-hassio" : "webpack-prod-hassio",
|
||||||
|
"gen-index-hassio-prod",
|
||||||
|
...// Don't compress running tests
|
||||||
|
(env.isTest() ? [] : ["compress-hassio"])
|
||||||
|
)
|
||||||
|
);
|
@@ -1,45 +0,0 @@
|
|||||||
import { series } from "gulp";
|
|
||||||
import { isTestBuild } from "../env.ts";
|
|
||||||
import { cleanHassio } from "./clean.ts";
|
|
||||||
import { compressHassio } from "./compress.ts";
|
|
||||||
import { genPagesHassioDev, genPagesHassioProd } from "./entry-html.ts";
|
|
||||||
import {
|
|
||||||
copyStaticSupervisor,
|
|
||||||
copyTranslationsSupervisor,
|
|
||||||
} from "./gather-static.ts";
|
|
||||||
import { genDummyIconsJson } from "./gen-icons-json.ts";
|
|
||||||
import { buildLocaleData } from "./locale-data.ts";
|
|
||||||
import { rspackProdHassio, rspackWatchHassio } from "./rspack.ts";
|
|
||||||
import { buildSupervisorTranslations } from "./translations.ts";
|
|
||||||
|
|
||||||
// develop-hassio
|
|
||||||
export const developHassio = series(
|
|
||||||
async function setEnv() {
|
|
||||||
process.env.NODE_ENV = "development";
|
|
||||||
},
|
|
||||||
cleanHassio,
|
|
||||||
genDummyIconsJson,
|
|
||||||
genPagesHassioDev,
|
|
||||||
buildSupervisorTranslations,
|
|
||||||
copyTranslationsSupervisor,
|
|
||||||
buildLocaleData,
|
|
||||||
copyStaticSupervisor,
|
|
||||||
rspackWatchHassio
|
|
||||||
);
|
|
||||||
|
|
||||||
// build-hassio
|
|
||||||
export const buildHassio = series(
|
|
||||||
async function setEnv() {
|
|
||||||
process.env.NODE_ENV = "production";
|
|
||||||
},
|
|
||||||
cleanHassio,
|
|
||||||
genDummyIconsJson,
|
|
||||||
buildSupervisorTranslations,
|
|
||||||
copyTranslationsSupervisor,
|
|
||||||
buildLocaleData,
|
|
||||||
copyStaticSupervisor,
|
|
||||||
rspackProdHassio,
|
|
||||||
genPagesHassioProd,
|
|
||||||
...// Don't compress running tests
|
|
||||||
(isTestBuild() ? [] : [compressHassio])
|
|
||||||
);
|
|
@@ -1,42 +0,0 @@
|
|||||||
import { analyzeApp, buildApp, developApp } from "./app";
|
|
||||||
import { buildCast, developCast } from "./cast";
|
|
||||||
import { analyzeDemo, buildDemo, developDemo } from "./demo";
|
|
||||||
import { downloadTranslations } from "./download-translations";
|
|
||||||
import { setupAndFetchNightlyTranslations } from "./fetch-nightly-translations";
|
|
||||||
import { buildGallery, developGallery, gatherGalleryPages } from "./gallery";
|
|
||||||
import { genIconsJson } from "./gen-icons-json";
|
|
||||||
import { buildHassio, developHassio } from "./hassio";
|
|
||||||
import { buildLandingPage, developLandingPage } from "./landing-page";
|
|
||||||
import { buildLocaleData } from "./locale-data";
|
|
||||||
import { buildTranslations } from "./translations";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
"develop-app": developApp,
|
|
||||||
"build-app": buildApp,
|
|
||||||
"analyze-app": analyzeApp,
|
|
||||||
|
|
||||||
"develop-cast": developCast,
|
|
||||||
"build-cast": buildCast,
|
|
||||||
|
|
||||||
"develop-demo": developDemo,
|
|
||||||
"build-demo": buildDemo,
|
|
||||||
"analyze-demo": analyzeDemo,
|
|
||||||
|
|
||||||
"develop-gallery": developGallery,
|
|
||||||
"build-gallery": buildGallery,
|
|
||||||
"gather-gallery-pages": gatherGalleryPages,
|
|
||||||
|
|
||||||
"develop-hassio": developHassio,
|
|
||||||
"build-hassio": buildHassio,
|
|
||||||
|
|
||||||
"develop-landing-page": developLandingPage,
|
|
||||||
"build-landing-page": buildLandingPage,
|
|
||||||
|
|
||||||
"setup-and-fetch-nightly-translations": setupAndFetchNightlyTranslations,
|
|
||||||
"download-translations": downloadTranslations,
|
|
||||||
"build-translations": buildTranslations,
|
|
||||||
|
|
||||||
"gen-icons-json": genIconsJson,
|
|
||||||
|
|
||||||
"build-locale-data": buildLocaleData,
|
|
||||||
};
|
|
@@ -1,46 +0,0 @@
|
|||||||
import { series } from "gulp";
|
|
||||||
import { cleanLandingPage } from "./clean.ts";
|
|
||||||
import "./compress.ts";
|
|
||||||
import {
|
|
||||||
genPagesLandingPageDev,
|
|
||||||
genPagesLandingPageProd,
|
|
||||||
} from "./entry-html.ts";
|
|
||||||
import {
|
|
||||||
copyStaticLandingPage,
|
|
||||||
copyTranslationsLandingPage,
|
|
||||||
} from "./gather-static.ts";
|
|
||||||
import { buildLocaleData } from "./locale-data.ts";
|
|
||||||
import { rspackProdLandingPage, rspackWatchLandingPage } from "./rspack.ts";
|
|
||||||
import {
|
|
||||||
buildLandingPageTranslations,
|
|
||||||
translationsEnableMergeBackend,
|
|
||||||
} from "./translations.ts";
|
|
||||||
|
|
||||||
// develop-landing-page
|
|
||||||
export const developLandingPage = series(
|
|
||||||
async function setEnv() {
|
|
||||||
process.env.NODE_ENV = "development";
|
|
||||||
},
|
|
||||||
cleanLandingPage,
|
|
||||||
translationsEnableMergeBackend,
|
|
||||||
buildLandingPageTranslations,
|
|
||||||
copyTranslationsLandingPage,
|
|
||||||
buildLocaleData,
|
|
||||||
copyStaticLandingPage,
|
|
||||||
genPagesLandingPageDev,
|
|
||||||
rspackWatchLandingPage
|
|
||||||
);
|
|
||||||
|
|
||||||
// build-landing-page
|
|
||||||
export const buildLandingPage = series(
|
|
||||||
async function setEnv() {
|
|
||||||
process.env.NODE_ENV = "production";
|
|
||||||
},
|
|
||||||
cleanLandingPage,
|
|
||||||
buildLandingPageTranslations,
|
|
||||||
copyTranslationsLandingPage,
|
|
||||||
buildLocaleData,
|
|
||||||
copyStaticLandingPage,
|
|
||||||
rspackProdLandingPage,
|
|
||||||
genPagesLandingPageProd
|
|
||||||
);
|
|
74
build-scripts/gulp/locale-data.js
Executable file
74
build-scripts/gulp/locale-data.js
Executable file
@@ -0,0 +1,74 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
|
|
||||||
|
const del = require("del");
|
||||||
|
const path = require("path");
|
||||||
|
const gulp = require("gulp");
|
||||||
|
const fs = require("fs");
|
||||||
|
const paths = require("../paths");
|
||||||
|
|
||||||
|
const outDir = "build/locale-data";
|
||||||
|
|
||||||
|
gulp.task("clean-locale-data", () => del([outDir]));
|
||||||
|
|
||||||
|
gulp.task("ensure-locale-data-build-dir", (done) => {
|
||||||
|
if (!fs.existsSync(outDir)) {
|
||||||
|
fs.mkdirSync(outDir, { recursive: true });
|
||||||
|
}
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
const modules = {
|
||||||
|
"intl-relativetimeformat": "RelativeTimeFormat",
|
||||||
|
"intl-datetimeformat": "DateTimeFormat",
|
||||||
|
"intl-numberformat": "NumberFormat",
|
||||||
|
};
|
||||||
|
|
||||||
|
gulp.task("create-locale-data", (done) => {
|
||||||
|
const translationMeta = JSON.parse(
|
||||||
|
fs.readFileSync(
|
||||||
|
path.join(paths.translations_src, "translationMetadata.json")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
Object.entries(modules).forEach(([module, className]) => {
|
||||||
|
Object.keys(translationMeta).forEach((lang) => {
|
||||||
|
try {
|
||||||
|
const localeData = String(
|
||||||
|
fs.readFileSync(
|
||||||
|
require.resolve(`@formatjs/${module}/locale-data/${lang}.js`)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.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) {
|
||||||
|
if (e.code !== "MODULE_NOT_FOUND") {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task(
|
||||||
|
"build-locale-data",
|
||||||
|
gulp.series(
|
||||||
|
"clean-locale-data",
|
||||||
|
"ensure-locale-data-build-dir",
|
||||||
|
"create-locale-data"
|
||||||
|
)
|
||||||
|
);
|
@@ -1,86 +0,0 @@
|
|||||||
import { deleteSync } from "del";
|
|
||||||
import { series } from "gulp";
|
|
||||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
||||||
import { join, resolve } from "node:path";
|
|
||||||
import paths from "../paths.ts";
|
|
||||||
|
|
||||||
const formatjsDir = join(paths.root_dir, "node_modules", "@formatjs");
|
|
||||||
const outDir = join(paths.build_dir, "locale-data");
|
|
||||||
|
|
||||||
const INTL_POLYFILLS = {
|
|
||||||
"intl-datetimeformat": "DateTimeFormat",
|
|
||||||
"intl-displaynames": "DisplayNames",
|
|
||||||
"intl-listformat": "ListFormat",
|
|
||||||
"intl-numberformat": "NumberFormat",
|
|
||||||
"intl-relativetimeformat": "RelativeTimeFormat",
|
|
||||||
};
|
|
||||||
|
|
||||||
const convertToJSON = async (
|
|
||||||
pkg,
|
|
||||||
lang,
|
|
||||||
subDir = "locale-data",
|
|
||||||
addFunc = "__addLocaleData",
|
|
||||||
skipMissing = true
|
|
||||||
) => {
|
|
||||||
let localeData;
|
|
||||||
try {
|
|
||||||
// use "pt" for "pt-BR", because "pt-BR" is unsupported by @formatjs
|
|
||||||
const language = lang === "pt-BR" ? "pt" : lang;
|
|
||||||
|
|
||||||
localeData = await readFile(
|
|
||||||
join(formatjsDir, pkg, subDir, `${language}.js`),
|
|
||||||
"utf-8"
|
|
||||||
);
|
|
||||||
} catch (e: any) {
|
|
||||||
// Ignore if language is missing (i.e. not supported by @formatjs)
|
|
||||||
if (e.code === "ENOENT" && skipMissing) {
|
|
||||||
console.warn(`Skipped missing data for language ${lang} from ${pkg}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
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));
|
|
||||||
await writeFile(join(outDir, `${pkg}/${lang}.json`), localeData);
|
|
||||||
};
|
|
||||||
|
|
||||||
const cleanLocaleData = async () => deleteSync([outDir]);
|
|
||||||
|
|
||||||
const createLocaleData = async () => {
|
|
||||||
const translationMeta = JSON.parse(
|
|
||||||
await readFile(
|
|
||||||
resolve(paths.translations_src, "translationMetadata.json"),
|
|
||||||
"utf-8"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
const conversions: any[] = [];
|
|
||||||
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);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const buildLocaleData = series(cleanLocaleData, createLocaleData);
|
|
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,238 +0,0 @@
|
|||||||
// Tasks to run rspack.
|
|
||||||
|
|
||||||
import rspack from "@rspack/core";
|
|
||||||
import { RspackDevServer } from "@rspack/dev-server";
|
|
||||||
import log from "fancy-log";
|
|
||||||
import { series, watch } from "gulp";
|
|
||||||
import fs from "node:fs";
|
|
||||||
import path from "node:path";
|
|
||||||
import { isDevContainer, isStatsBuild, isTestBuild } from "../env.ts";
|
|
||||||
import paths from "../paths.ts";
|
|
||||||
import {
|
|
||||||
createAppConfig,
|
|
||||||
createCastConfig,
|
|
||||||
createDemoConfig,
|
|
||||||
createGalleryConfig,
|
|
||||||
createHassioConfig,
|
|
||||||
createLandingPageConfig,
|
|
||||||
} from "../rspack.ts";
|
|
||||||
import {
|
|
||||||
copyTranslationsApp,
|
|
||||||
copyTranslationsLandingPage,
|
|
||||||
copyTranslationsSupervisor,
|
|
||||||
} from "./gather-static.ts";
|
|
||||||
import {
|
|
||||||
buildLandingPageTranslations,
|
|
||||||
buildSupervisorTranslations,
|
|
||||||
buildTranslations,
|
|
||||||
} from "./translations.ts";
|
|
||||||
|
|
||||||
const bothBuilds = (createConfigFunc, params) => [
|
|
||||||
createConfigFunc({ ...params, latestBuild: true }),
|
|
||||||
createConfigFunc({ ...params, latestBuild: false }),
|
|
||||||
];
|
|
||||||
|
|
||||||
const isWsl =
|
|
||||||
fs.existsSync("/proc/version") &&
|
|
||||||
fs
|
|
||||||
.readFileSync("/proc/version", "utf-8")
|
|
||||||
.toLocaleLowerCase()
|
|
||||||
.includes("microsoft");
|
|
||||||
|
|
||||||
interface RunDevServer {
|
|
||||||
compiler: any;
|
|
||||||
contentBase: string;
|
|
||||||
port: number;
|
|
||||||
listenHost?: string;
|
|
||||||
proxy?: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {{
|
|
||||||
* compiler: import("@rspack/core").Compiler,
|
|
||||||
* contentBase: string,
|
|
||||||
* port: number,
|
|
||||||
* listenHost?: string
|
|
||||||
* }}
|
|
||||||
*/
|
|
||||||
const runDevServer = async ({
|
|
||||||
compiler,
|
|
||||||
contentBase,
|
|
||||||
port,
|
|
||||||
listenHost,
|
|
||||||
proxy,
|
|
||||||
}: RunDevServer) => {
|
|
||||||
if (listenHost === undefined) {
|
|
||||||
// For dev container, we need to listen on all hosts
|
|
||||||
listenHost = isDevContainer() ? "0.0.0.0" : "localhost";
|
|
||||||
}
|
|
||||||
const server = new RspackDevServer(
|
|
||||||
{
|
|
||||||
hot: false,
|
|
||||||
open: true,
|
|
||||||
host: listenHost,
|
|
||||||
port,
|
|
||||||
static: {
|
|
||||||
directory: contentBase,
|
|
||||||
watch: true,
|
|
||||||
},
|
|
||||||
proxy,
|
|
||||||
},
|
|
||||||
compiler
|
|
||||||
);
|
|
||||||
|
|
||||||
await server.start();
|
|
||||||
// Server listening
|
|
||||||
log("[rspack-dev-server]", `Project is running at http://localhost:${port}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const doneHandler = (done?: (value?: unknown) => void) => (err, stats) => {
|
|
||||||
if (err) {
|
|
||||||
log.error(err.stack || err);
|
|
||||||
if (err.details) {
|
|
||||||
log.error(err.details);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stats.hasErrors() || stats.hasWarnings()) {
|
|
||||||
console.log(stats.toString("minimal"));
|
|
||||||
}
|
|
||||||
|
|
||||||
log(`Build done @ ${new Date().toLocaleTimeString()}`);
|
|
||||||
|
|
||||||
if (done) {
|
|
||||||
done();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const prodBuild = (conf) =>
|
|
||||||
new Promise((resolve) => {
|
|
||||||
rspack(
|
|
||||||
conf,
|
|
||||||
// Resolve promise when done. Because we pass a callback, rspack closes itself
|
|
||||||
doneHandler(resolve)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
export const rspackWatchApp = () => {
|
|
||||||
// This command will run forever because we don't close compiler
|
|
||||||
rspack(
|
|
||||||
process.env.ES5
|
|
||||||
? bothBuilds(createAppConfig, { isProdBuild: false })
|
|
||||||
: createAppConfig({ isProdBuild: false, latestBuild: true })
|
|
||||||
).watch({ poll: isWsl }, doneHandler());
|
|
||||||
watch(
|
|
||||||
path.join(paths.translations_src, "en.json"),
|
|
||||||
series(buildTranslations, copyTranslationsApp)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const rspackProdApp = () =>
|
|
||||||
prodBuild(
|
|
||||||
bothBuilds(createAppConfig, {
|
|
||||||
isProdBuild: true,
|
|
||||||
isStatsBuild: isStatsBuild(),
|
|
||||||
isTestBuild: isTestBuild(),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
export const rspackDevServerDemo = () =>
|
|
||||||
runDevServer({
|
|
||||||
compiler: rspack(
|
|
||||||
createDemoConfig({ isProdBuild: false, latestBuild: true })
|
|
||||||
),
|
|
||||||
contentBase: paths.demo_output_root,
|
|
||||||
port: 8090,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const rspackProdDemo = () =>
|
|
||||||
prodBuild(
|
|
||||||
bothBuilds(createDemoConfig, {
|
|
||||||
isProdBuild: true,
|
|
||||||
isStatsBuild: isStatsBuild(),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
export const rspackDevServerCast = () =>
|
|
||||||
runDevServer({
|
|
||||||
compiler: rspack(
|
|
||||||
createCastConfig({ isProdBuild: false, latestBuild: true })
|
|
||||||
),
|
|
||||||
contentBase: paths.cast_output_root,
|
|
||||||
port: 8080,
|
|
||||||
// Accessible from the network, because that's how Cast hits it.
|
|
||||||
listenHost: "0.0.0.0",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const rspackProdCast = () =>
|
|
||||||
prodBuild(
|
|
||||||
bothBuilds(createCastConfig, {
|
|
||||||
isProdBuild: true,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
export const rspackWatchHassio = () => {
|
|
||||||
// This command will run forever because we don't close compiler
|
|
||||||
rspack(
|
|
||||||
createHassioConfig({
|
|
||||||
isProdBuild: false,
|
|
||||||
latestBuild: true,
|
|
||||||
})
|
|
||||||
).watch({ ignored: /build/, poll: isWsl }, doneHandler());
|
|
||||||
|
|
||||||
watch(
|
|
||||||
path.join(paths.translations_src, "en.json"),
|
|
||||||
series(buildSupervisorTranslations, copyTranslationsSupervisor)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const rspackProdHassio = () =>
|
|
||||||
prodBuild(
|
|
||||||
bothBuilds(createHassioConfig, {
|
|
||||||
isProdBuild: true,
|
|
||||||
isStatsBuild: isStatsBuild(),
|
|
||||||
isTestBuild: isTestBuild(),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
export const rspackDevServerGallery = () =>
|
|
||||||
runDevServer({
|
|
||||||
compiler: rspack(
|
|
||||||
createGalleryConfig({ isProdBuild: false, latestBuild: true })
|
|
||||||
),
|
|
||||||
contentBase: paths.gallery_output_root,
|
|
||||||
port: 8100,
|
|
||||||
listenHost: "0.0.0.0",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const rspackProdGallery = () =>
|
|
||||||
prodBuild(
|
|
||||||
createGalleryConfig({
|
|
||||||
isProdBuild: true,
|
|
||||||
latestBuild: true,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
export const rspackWatchLandingPage = () => {
|
|
||||||
// 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());
|
|
||||||
|
|
||||||
watch(
|
|
||||||
path.join(paths.translations_src, "en.json"),
|
|
||||||
series(buildLandingPageTranslations, copyTranslationsLandingPage)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const rspackProdLandingPage = () =>
|
|
||||||
prodBuild(
|
|
||||||
bothBuilds(createLandingPageConfig, {
|
|
||||||
isProdBuild: true,
|
|
||||||
isStatsBuild: isStatsBuild(),
|
|
||||||
isTestBuild: isTestBuild(),
|
|
||||||
})
|
|
||||||
);
|
|
96
build-scripts/gulp/service-worker.js
Normal file
96
build-scripts/gulp/service-worker.js
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
// 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");
|
||||||
|
|
||||||
|
const swDest = path.resolve(paths.app_output_root, "service_worker.js");
|
||||||
|
|
||||||
|
const writeSW = (content) => fs.outputFileSync(swDest, content.trim() + "\n");
|
||||||
|
|
||||||
|
gulp.task("gen-service-worker-app-dev", (done) => {
|
||||||
|
writeSW(
|
||||||
|
`
|
||||||
|
console.debug('Service worker disabled in development');
|
||||||
|
|
||||||
|
self.addEventListener('install', (event) => {
|
||||||
|
// This will activate the dev service worker,
|
||||||
|
// removing any prod service worker the dev might have running
|
||||||
|
self.skipWaiting();
|
||||||
|
});
|
||||||
|
`
|
||||||
|
);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task("gen-service-worker-app-prod", async () => {
|
||||||
|
// Read bundled source file
|
||||||
|
const bundleManifestLatest = require(path.resolve(
|
||||||
|
paths.app_output_latest,
|
||||||
|
"manifest.json"
|
||||||
|
));
|
||||||
|
let serviceWorkerContent = fs.readFileSync(
|
||||||
|
paths.app_output_root + bundleManifestLatest["service_worker.js"],
|
||||||
|
"utf-8"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Delete old file from frontend_latest so manifest won't pick it up
|
||||||
|
fs.removeSync(
|
||||||
|
paths.app_output_root + bundleManifestLatest["service_worker.js"]
|
||||||
|
);
|
||||||
|
fs.removeSync(
|
||||||
|
paths.app_output_root + bundleManifestLatest["service_worker.js.map"]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Remove ES5
|
||||||
|
const bundleManifestES5 = 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
|
||||||
|
// ignore JS files + translation files
|
||||||
|
dontCacheBustURLsMatching: /(frontend_latest\/.+|static\/translations\/.+)/,
|
||||||
|
|
||||||
|
globDirectory: paths.app_output_root,
|
||||||
|
globPatterns: [
|
||||||
|
"frontend_latest/*.js",
|
||||||
|
// Cache all English translations because we catch them as fallback
|
||||||
|
// Using pattern to match hash instead of * to avoid caching en-GB
|
||||||
|
// 'v' added as valid hash letter because in dev we hash with 'dev'
|
||||||
|
"static/translations/**/en-+([a-fv0-9]).json",
|
||||||
|
// Icon shown on splash screen
|
||||||
|
"static/icons/favicon-192x192.png",
|
||||||
|
"static/icons/favicon.ico",
|
||||||
|
// Common fonts
|
||||||
|
"static/fonts/roboto/Roboto-Light.woff2",
|
||||||
|
"static/fonts/roboto/Roboto-Medium.woff2",
|
||||||
|
"static/fonts/roboto/Roboto-Regular.woff2",
|
||||||
|
"static/fonts/roboto/Roboto-Bold.woff2",
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const warning of workboxManifest.warnings) {
|
||||||
|
console.warn(warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove source map and add WB manifest
|
||||||
|
serviceWorkerContent = sourceMapUrl.removeFrom(serviceWorkerContent);
|
||||||
|
serviceWorkerContent = serviceWorkerContent.replace(
|
||||||
|
"WB_MANIFEST",
|
||||||
|
JSON.stringify(workboxManifest.manifestEntries)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Write new file to root
|
||||||
|
fs.writeFileSync(swDest, serviceWorkerContent);
|
||||||
|
});
|
@@ -1,85 +0,0 @@
|
|||||||
// Generate service workers
|
|
||||||
|
|
||||||
import { deleteAsync } from "del";
|
|
||||||
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.ts";
|
|
||||||
|
|
||||||
const SW_MAP = {
|
|
||||||
[paths.app_output_latest]: "modern",
|
|
||||||
[paths.app_output_es5]: "legacy",
|
|
||||||
};
|
|
||||||
|
|
||||||
const SW_DEV =
|
|
||||||
`
|
|
||||||
console.debug('Service worker disabled in development');
|
|
||||||
|
|
||||||
self.addEventListener('install', (event) => {
|
|
||||||
// This will activate the dev service worker,
|
|
||||||
// removing any prod service worker the dev might have running
|
|
||||||
self.skipWaiting();
|
|
||||||
});
|
|
||||||
`.trim() + "\n";
|
|
||||||
|
|
||||||
export const genServiceWorkerAppDev = 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",
|
|
||||||
})
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const genServiceWorkerAppProd = () =>
|
|
||||||
Promise.all(
|
|
||||||
Object.entries(SW_MAP).map(async ([outPath, build]) => {
|
|
||||||
const manifest = JSON.parse(
|
|
||||||
await readFile(join(outPath, "manifest.json"), "utf-8")
|
|
||||||
);
|
|
||||||
const swSrc = join(paths.app_output_root, manifest["service-worker.js"]);
|
|
||||||
const swDest = join(paths.app_output_root, `sw-${build}.js`);
|
|
||||||
const buildDir = relative(paths.app_output_root, outPath);
|
|
||||||
const { warnings } = await injectManifest({
|
|
||||||
swSrc,
|
|
||||||
swDest,
|
|
||||||
injectionPoint: "__WB_MANIFEST__",
|
|
||||||
// Files that mach this pattern will be considered unique and skip revision check
|
|
||||||
// ignore JS files + translation files
|
|
||||||
dontCacheBustURLsMatching: new RegExp(
|
|
||||||
`(?:${buildDir}/.+|static/translations/.+)`
|
|
||||||
),
|
|
||||||
globDirectory: paths.app_output_root,
|
|
||||||
globPatterns: [
|
|
||||||
`${buildDir}/*.js`,
|
|
||||||
// Cache all English translations because we catch them as fallback
|
|
||||||
// Using pattern to match hash instead of * to avoid caching en-GB
|
|
||||||
// 'v' added as valid hash letter because in dev we hash with 'dev'
|
|
||||||
"static/translations/**/en-+([a-fv0-9]).json",
|
|
||||||
// Icon shown on splash screen
|
|
||||||
"static/icons/favicon-192x192.png",
|
|
||||||
"static/icons/favicon.ico",
|
|
||||||
// Common fonts
|
|
||||||
"static/fonts/roboto/Roboto-Light.woff2",
|
|
||||||
"static/fonts/roboto/Roboto-Medium.woff2",
|
|
||||||
"static/fonts/roboto/Roboto-Regular.woff2",
|
|
||||||
"static/fonts/roboto/Roboto-Bold.woff2",
|
|
||||||
],
|
|
||||||
globIgnores: [`${buildDir}/service-worker*`],
|
|
||||||
});
|
|
||||||
if (warnings.length > 0) {
|
|
||||||
console.warn(
|
|
||||||
`Problems while injecting ${build} service worker:\n`,
|
|
||||||
warnings.join("\n")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
await deleteAsync(`${swSrc}?(.map)`);
|
|
||||||
// Needed to install new SW from a cached HTML
|
|
||||||
if (build === "modern") {
|
|
||||||
const swOld = join(paths.app_output_root, "service_worker.js");
|
|
||||||
await symlink(basename(swDest), swOld);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
434
build-scripts/gulp/translations.js
Executable file
434
build-scripts/gulp/translations.js
Executable file
@@ -0,0 +1,434 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
|
|
||||||
|
const crypto = require("crypto");
|
||||||
|
const del = require("del");
|
||||||
|
const path = require("path");
|
||||||
|
const source = require("vinyl-source-stream");
|
||||||
|
const vinylBuffer = require("vinyl-buffer");
|
||||||
|
const gulp = require("gulp");
|
||||||
|
const fs = require("fs");
|
||||||
|
const foreach = require("gulp-foreach");
|
||||||
|
const merge = require("gulp-merge-json");
|
||||||
|
const rename = require("gulp-rename");
|
||||||
|
const transform = require("gulp-json-transform");
|
||||||
|
const { mapFiles } = require("../util");
|
||||||
|
const env = require("../env");
|
||||||
|
const paths = require("../paths");
|
||||||
|
|
||||||
|
const inFrontendDir = "translations/frontend";
|
||||||
|
const inBackendDir = "translations/backend";
|
||||||
|
const workDir = "build/translations";
|
||||||
|
const fullDir = workDir + "/full";
|
||||||
|
const coreDir = workDir + "/core";
|
||||||
|
const outDir = workDir + "/output";
|
||||||
|
let mergeBackend = false;
|
||||||
|
|
||||||
|
gulp.task("translations-enable-merge-backend", (done) => {
|
||||||
|
mergeBackend = true;
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Panel translations which should be split from the core translations.
|
||||||
|
const TRANSLATION_FRAGMENTS = Object.keys(
|
||||||
|
require("../../src/translations/en.json").ui.panel
|
||||||
|
);
|
||||||
|
|
||||||
|
function recursiveFlatten(prefix, data) {
|
||||||
|
let output = {};
|
||||||
|
Object.keys(data).forEach((key) => {
|
||||||
|
if (typeof data[key] === "object") {
|
||||||
|
output = {
|
||||||
|
...output,
|
||||||
|
...recursiveFlatten(prefix + key + ".", data[key]),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
output[prefix + key] = data[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
function flatten(data) {
|
||||||
|
return recursiveFlatten("", data);
|
||||||
|
}
|
||||||
|
|
||||||
|
function emptyFilter(data) {
|
||||||
|
const newData = {};
|
||||||
|
Object.keys(data).forEach((key) => {
|
||||||
|
if (data[key]) {
|
||||||
|
if (typeof data[key] === "object") {
|
||||||
|
newData[key] = emptyFilter(data[key]);
|
||||||
|
} else {
|
||||||
|
newData[key] = data[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return newData;
|
||||||
|
}
|
||||||
|
|
||||||
|
function recursiveEmpty(data) {
|
||||||
|
const newData = {};
|
||||||
|
Object.keys(data).forEach((key) => {
|
||||||
|
if (data[key]) {
|
||||||
|
if (typeof data[key] === "object") {
|
||||||
|
newData[key] = recursiveEmpty(data[key]);
|
||||||
|
} else {
|
||||||
|
newData[key] = "TRANSLATED";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return newData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace Lokalise key placeholders with their actual values.
|
||||||
|
*
|
||||||
|
* We duplicate the behavior of Lokalise here so that placeholders can
|
||||||
|
* be included in src/translations/en.json, but still be usable while
|
||||||
|
* developing locally.
|
||||||
|
*
|
||||||
|
* @link https://docs.lokalise.co/article/KO5SZWLLsy-key-referencing
|
||||||
|
*/
|
||||||
|
const re_key_reference = /\[%key:([^%]+)%\]/;
|
||||||
|
function lokaliseTransform(data, original, file) {
|
||||||
|
const output = {};
|
||||||
|
Object.entries(data).forEach(([key, value]) => {
|
||||||
|
if (value instanceof Object) {
|
||||||
|
output[key] = lokaliseTransform(value, original, file);
|
||||||
|
} else {
|
||||||
|
output[key] = value.replace(re_key_reference, (_match, lokalise_key) => {
|
||||||
|
const replace = lokalise_key.split("::").reduce((tr, k) => {
|
||||||
|
if (!tr) {
|
||||||
|
throw Error(
|
||||||
|
`Invalid key placeholder ${lokalise_key} in ${file.path}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return tr[k];
|
||||||
|
}, original);
|
||||||
|
if (typeof replace !== "string") {
|
||||||
|
throw Error(
|
||||||
|
`Invalid key placeholder ${lokalise_key} in ${file.path}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return replace;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
gulp.task("clean-translations", () => del([workDir]));
|
||||||
|
|
||||||
|
gulp.task("ensure-translations-build-dir", (done) => {
|
||||||
|
if (!fs.existsSync(workDir)) {
|
||||||
|
fs.mkdirSync(workDir, { recursive: true });
|
||||||
|
}
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task("create-test-metadata", (cb) => {
|
||||||
|
fs.writeFile(
|
||||||
|
workDir + "/testMetadata.json",
|
||||||
|
JSON.stringify({
|
||||||
|
test: {
|
||||||
|
nativeName: "Test",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
cb
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task(
|
||||||
|
"create-test-translation",
|
||||||
|
gulp.series("create-test-metadata", () =>
|
||||||
|
gulp
|
||||||
|
.src(path.join(paths.translations_src, "en.json"))
|
||||||
|
.pipe(transform((data, _file) => recursiveEmpty(data)))
|
||||||
|
.pipe(rename("test.json"))
|
||||||
|
.pipe(gulp.dest(workDir))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This task will build a master translation file, to be used as the base for
|
||||||
|
* all languages. This starts with src/translations/en.json, and replaces all
|
||||||
|
* Lokalise key placeholders with their target values. Under normal circumstances,
|
||||||
|
* this will be the same as translations/en.json However, we build it here to
|
||||||
|
* facilitate both making changes in development mode, and to ensure that the
|
||||||
|
* project is buildable immediately after merging new translation keys, since
|
||||||
|
* the Lokalise update to translations/en.json will not happen immediately.
|
||||||
|
*/
|
||||||
|
gulp.task("build-master-translation", () => {
|
||||||
|
const src = [path.join(paths.translations_src, "en.json")];
|
||||||
|
|
||||||
|
if (mergeBackend) {
|
||||||
|
src.push(path.join(inBackendDir, "en.json"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return gulp
|
||||||
|
.src(src)
|
||||||
|
.pipe(transform((data, file) => lokaliseTransform(data, data, file)))
|
||||||
|
.pipe(
|
||||||
|
merge({
|
||||||
|
fileName: "translationMaster.json",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.pipe(gulp.dest(workDir));
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task("build-merged-translations", () =>
|
||||||
|
gulp
|
||||||
|
.src([inFrontendDir + "/*.json", workDir + "/test.json"], {
|
||||||
|
allowEmpty: true,
|
||||||
|
})
|
||||||
|
.pipe(transform((data, file) => lokaliseTransform(data, data, file)))
|
||||||
|
.pipe(
|
||||||
|
foreach((stream, file) => {
|
||||||
|
// For each language generate a merged json file. It begins with the master
|
||||||
|
// translation as a failsafe for untranslated strings, and merges all parent
|
||||||
|
// tags into one file for each specific subtag
|
||||||
|
//
|
||||||
|
// TODO: This is a naive interpretation of BCP47 that should be improved.
|
||||||
|
// Will be OK for now as long as we don't have anything more complicated
|
||||||
|
// than a base translation + region.
|
||||||
|
const tr = path.basename(file.history[0], ".json");
|
||||||
|
const subtags = tr.split("-");
|
||||||
|
const src = [workDir + "/translationMaster.json"];
|
||||||
|
for (let i = 1; i <= subtags.length; i++) {
|
||||||
|
const lang = subtags.slice(0, i).join("-");
|
||||||
|
if (lang === "test") {
|
||||||
|
src.push(workDir + "/test.json");
|
||||||
|
} else if (lang !== "en") {
|
||||||
|
src.push(inFrontendDir + "/" + lang + ".json");
|
||||||
|
if (mergeBackend) {
|
||||||
|
src.push(inBackendDir + "/" + lang + ".json");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return gulp
|
||||||
|
.src(src, { allowEmpty: true })
|
||||||
|
.pipe(transform((data) => emptyFilter(data)))
|
||||||
|
.pipe(
|
||||||
|
merge({
|
||||||
|
fileName: tr + ".json",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.pipe(gulp.dest(fullDir));
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
let taskName;
|
||||||
|
|
||||||
|
const splitTasks = [];
|
||||||
|
TRANSLATION_FRAGMENTS.forEach((fragment) => {
|
||||||
|
taskName = "build-translation-fragment-" + fragment;
|
||||||
|
gulp.task(taskName, () =>
|
||||||
|
// Return only the translations for this fragment.
|
||||||
|
gulp
|
||||||
|
.src(fullDir + "/*.json")
|
||||||
|
.pipe(
|
||||||
|
transform((data) => ({
|
||||||
|
ui: {
|
||||||
|
panel: {
|
||||||
|
[fragment]: data.ui.panel[fragment],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
.pipe(gulp.dest(workDir + "/" + fragment))
|
||||||
|
);
|
||||||
|
splitTasks.push(taskName);
|
||||||
|
});
|
||||||
|
|
||||||
|
taskName = "build-translation-core";
|
||||||
|
gulp.task(taskName, () =>
|
||||||
|
// Remove the fragment translations from the core translation.
|
||||||
|
gulp
|
||||||
|
.src(fullDir + "/*.json")
|
||||||
|
.pipe(
|
||||||
|
transform((data, _file) => {
|
||||||
|
TRANSLATION_FRAGMENTS.forEach((fragment) => {
|
||||||
|
delete data.ui.panel[fragment];
|
||||||
|
});
|
||||||
|
delete data.supervisor;
|
||||||
|
return data;
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.pipe(gulp.dest(coreDir))
|
||||||
|
);
|
||||||
|
|
||||||
|
splitTasks.push(taskName);
|
||||||
|
|
||||||
|
gulp.task("build-flattened-translations", () =>
|
||||||
|
// Flatten the split versions of our translations, and move them into outDir
|
||||||
|
gulp
|
||||||
|
.src(
|
||||||
|
TRANSLATION_FRAGMENTS.map(
|
||||||
|
(fragment) => workDir + "/" + fragment + "/*.json"
|
||||||
|
).concat(coreDir + "/*.json"),
|
||||||
|
{ base: workDir }
|
||||||
|
)
|
||||||
|
.pipe(
|
||||||
|
transform((data) =>
|
||||||
|
// Polymer.AppLocalizeBehavior requires flattened json
|
||||||
|
flatten(data)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.pipe(
|
||||||
|
rename((filePath) => {
|
||||||
|
if (filePath.dirname === "core") {
|
||||||
|
filePath.dirname = "";
|
||||||
|
}
|
||||||
|
// In dev we create the file with the fake hash in the filename
|
||||||
|
if (!env.isProdBuild()) {
|
||||||
|
filePath.basename += "-dev";
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.pipe(gulp.dest(outDir))
|
||||||
|
);
|
||||||
|
|
||||||
|
const fingerprints = {};
|
||||||
|
|
||||||
|
gulp.task("build-translation-fingerprints", () => {
|
||||||
|
// Fingerprint full file of each language
|
||||||
|
const files = 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(
|
||||||
|
"build-translations",
|
||||||
|
gulp.series(
|
||||||
|
"clean-translations",
|
||||||
|
"ensure-translations-build-dir",
|
||||||
|
"create-translations",
|
||||||
|
"build-translation-fingerprints",
|
||||||
|
"build-translation-write-metadata"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
gulp.task(
|
||||||
|
"build-supervisor-translations",
|
||||||
|
gulp.series(
|
||||||
|
"clean-translations",
|
||||||
|
"ensure-translations-build-dir",
|
||||||
|
"build-master-translation",
|
||||||
|
"build-merged-translations",
|
||||||
|
"build-translation-fragment-supervisor",
|
||||||
|
"build-translation-flatten-supervisor",
|
||||||
|
"build-translation-fingerprints",
|
||||||
|
"build-translation-write-metadata"
|
||||||
|
)
|
||||||
|
);
|
@@ -1,332 +0,0 @@
|
|||||||
/* eslint-disable max-classes-per-file */
|
|
||||||
|
|
||||||
import { deleteAsync } from "del";
|
|
||||||
import { glob } from "glob";
|
|
||||||
import { src as glupSrc, dest as gulpDest, parallel, series } from "gulp";
|
|
||||||
import rename from "gulp-rename";
|
|
||||||
import merge from "lodash.merge";
|
|
||||||
import { createHash } from "node:crypto";
|
|
||||||
import { mkdir, readFile } from "node:fs/promises";
|
|
||||||
import { basename, join } from "node:path";
|
|
||||||
import { PassThrough, Transform } from "node:stream";
|
|
||||||
import { finished } from "node:stream/promises";
|
|
||||||
import { isProdBuild } from "../env.ts";
|
|
||||||
import paths from "../paths.ts";
|
|
||||||
import {
|
|
||||||
allowSetupFetchNightlyTranslations,
|
|
||||||
fetchNightlyTranslations,
|
|
||||||
} from "./fetch-nightly-translations.ts";
|
|
||||||
|
|
||||||
const inFrontendDir = "translations/frontend";
|
|
||||||
const inBackendDir = "translations/backend";
|
|
||||||
const workDir = "build/translations";
|
|
||||||
const outDir = join(workDir, "output");
|
|
||||||
const EN_SRC = join(paths.translations_src, "en.json");
|
|
||||||
const TEST_LOCALE = "en-x-test";
|
|
||||||
|
|
||||||
let mergeBackend = false;
|
|
||||||
|
|
||||||
// translations-enable-merge-backend
|
|
||||||
export const translationsEnableMergeBackend = parallel(async () => {
|
|
||||||
mergeBackend = true;
|
|
||||||
}, allowSetupFetchNightlyTranslations);
|
|
||||||
|
|
||||||
// Transform stream to apply a function on Vinyl JSON files (buffer mode only).
|
|
||||||
// The provided function can either return a new object, or an array of
|
|
||||||
// [object, subdirectory] pairs for fragmentizing the JSON.
|
|
||||||
class CustomJSON extends Transform {
|
|
||||||
_func: any;
|
|
||||||
|
|
||||||
_reviver: any;
|
|
||||||
|
|
||||||
constructor(func, reviver: any = null) {
|
|
||||||
super({ objectMode: true });
|
|
||||||
this._func = func;
|
|
||||||
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: any[] = [];
|
|
||||||
|
|
||||||
_stem: any;
|
|
||||||
|
|
||||||
_startObj: any;
|
|
||||||
|
|
||||||
_reviver: any;
|
|
||||||
|
|
||||||
_outFile: any;
|
|
||||||
|
|
||||||
constructor(stem, startObj = {}, reviver: any = 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 {
|
|
||||||
output[prefix + key] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return output;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Filter functions that can be passed directly to JSON.parse()
|
|
||||||
const emptyReviver = (_key, value) => value || undefined;
|
|
||||||
const testReviver = (_key, value) =>
|
|
||||||
value && typeof value === "string" ? "TRANSLATED" : value;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Replace Lokalise key placeholders with their actual values.
|
|
||||||
*
|
|
||||||
* We duplicate the behavior of Lokalise here so that placeholders can
|
|
||||||
* be included in src/translations/en.json, but still be usable while
|
|
||||||
* developing locally.
|
|
||||||
*
|
|
||||||
* @link https://docs.lokalise.com/en/articles/1400528-key-referencing
|
|
||||||
*/
|
|
||||||
const KEY_REFERENCE = /\[%key:([^%]+)%\]/;
|
|
||||||
const lokaliseTransform = (data, path, original = data) => {
|
|
||||||
const output = {};
|
|
||||||
for (const entry of Object.entries(data)) {
|
|
||||||
const [key, value] = entry as [string, string];
|
|
||||||
if (typeof value === "object") {
|
|
||||||
output[key] = lokaliseTransform(value, path, original);
|
|
||||||
} else {
|
|
||||||
output[key] = value?.replace(KEY_REFERENCE, (_match, lokalise_key) => {
|
|
||||||
const replace = lokalise_key.split("::").reduce((tr, k) => {
|
|
||||||
if (!tr) {
|
|
||||||
throw Error(`Invalid key placeholder ${lokalise_key} in ${path}`);
|
|
||||||
}
|
|
||||||
return tr[k];
|
|
||||||
}, original);
|
|
||||||
if (typeof replace !== "string") {
|
|
||||||
throw Error(`Invalid key placeholder ${lokalise_key} in ${path}`);
|
|
||||||
}
|
|
||||||
return replace;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return output;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const cleanTranslations = () => deleteAsync([workDir]);
|
|
||||||
|
|
||||||
const makeWorkDir = () => mkdir(workDir, { recursive: true });
|
|
||||||
|
|
||||||
const createTestTranslation = () =>
|
|
||||||
isProdBuild()
|
|
||||||
? Promise.resolve()
|
|
||||||
: glupSrc(EN_SRC)
|
|
||||||
.pipe(new CustomJSON(null, testReviver))
|
|
||||||
.pipe(rename(`${TEST_LOCALE}.json`))
|
|
||||||
.pipe(gulpDest(workDir));
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This task will build a master translation file, to be used as the base for
|
|
||||||
* all languages. This starts with src/translations/en.json, and replaces all
|
|
||||||
* Lokalise key placeholders with their target values. Under normal circumstances,
|
|
||||||
* this will be the same as translations/en.json However, we build it here to
|
|
||||||
* facilitate both making changes in development mode, and to ensure that the
|
|
||||||
* project is buildable immediately after merging new translation keys, since
|
|
||||||
* the Lokalise update to translations/en.json will not happen immediately.
|
|
||||||
*/
|
|
||||||
const createMasterTranslation = () =>
|
|
||||||
glupSrc([EN_SRC, ...(mergeBackend ? [`${inBackendDir}/en.json`] : [])])
|
|
||||||
.pipe(new CustomJSON(lokaliseTransform))
|
|
||||||
.pipe(new MergeJSON("en"))
|
|
||||||
.pipe(gulpDest(workDir));
|
|
||||||
|
|
||||||
const FRAGMENTS = ["base"];
|
|
||||||
|
|
||||||
const setFragment = (fragment) => async () => {
|
|
||||||
FRAGMENTS[0] = fragment;
|
|
||||||
};
|
|
||||||
|
|
||||||
const panelFragment = (fragment) =>
|
|
||||||
fragment !== "base" &&
|
|
||||||
fragment !== "supervisor" &&
|
|
||||||
fragment !== "landing-page";
|
|
||||||
|
|
||||||
const HASHES = new Map();
|
|
||||||
|
|
||||||
const createTranslations = async () => {
|
|
||||||
// Parse and store the master to avoid repeating this for each locale, then
|
|
||||||
// add the panel fragments when processing the app.
|
|
||||||
const enMaster = JSON.parse(await readFile(`${workDir}/en.json`, "utf-8"));
|
|
||||||
if (FRAGMENTS[0] === "base") {
|
|
||||||
FRAGMENTS.push(...Object.keys(enMaster.ui.panel));
|
|
||||||
}
|
|
||||||
|
|
||||||
// The downstream pipeline is setup first. It hashes the merged data for
|
|
||||||
// each locale, then fragmentizes and flattens the data for final output.
|
|
||||||
const translationFiles = await glob([
|
|
||||||
`${inFrontendDir}/!(en).json`,
|
|
||||||
...(isProdBuild() ? [] : [`${workDir}/${TEST_LOCALE}.json`]),
|
|
||||||
]);
|
|
||||||
const hashStream = new Transform({
|
|
||||||
objectMode: true,
|
|
||||||
transform: async (file, _, callback) => {
|
|
||||||
const hash = 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(
|
|
||||||
new CustomJSON((data) =>
|
|
||||||
FRAGMENTS.map((fragment) => {
|
|
||||||
switch (fragment) {
|
|
||||||
case "base":
|
|
||||||
// Remove the panels and supervisor to create the base translations
|
|
||||||
return [
|
|
||||||
flatten({
|
|
||||||
...data,
|
|
||||||
ui: { ...data.ui, panel: undefined },
|
|
||||||
supervisor: undefined,
|
|
||||||
}),
|
|
||||||
"",
|
|
||||||
];
|
|
||||||
case "supervisor":
|
|
||||||
// Supervisor key is at the top level
|
|
||||||
return [flatten(data.supervisor), ""];
|
|
||||||
case "landing-page":
|
|
||||||
// landing-page key is at the top level
|
|
||||||
return [flatten(data["landing-page"]), ""];
|
|
||||||
default:
|
|
||||||
// Create a fragment with only the given panel
|
|
||||||
return [
|
|
||||||
flatten(data.ui.panel[fragment], `ui.panel.${fragment}.`),
|
|
||||||
fragment,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.pipe(gulpDest(outDir));
|
|
||||||
|
|
||||||
// Send the English master downstream first, then for each other locale
|
|
||||||
// generate merged JSON data to continue piping. It begins with the master
|
|
||||||
// translation as a failsafe for untranslated strings, and merges all parent
|
|
||||||
// tags into one file for each specific subtag
|
|
||||||
//
|
|
||||||
// TODO: This is a naive interpretation of BCP47 that should be improved.
|
|
||||||
// Will be OK for now as long as we don't have anything more complicated
|
|
||||||
// than a base translation + region.
|
|
||||||
const masterStream = glupSrc(`${workDir}/en.json`).pipe(
|
|
||||||
new PassThrough({ objectMode: true })
|
|
||||||
);
|
|
||||||
masterStream.pipe(hashStream, { end: false });
|
|
||||||
const mergesFinished = [finished(masterStream)];
|
|
||||||
for (const translationFile of translationFiles) {
|
|
||||||
const locale = basename(translationFile, ".json");
|
|
||||||
const subtags = locale.split("-");
|
|
||||||
const mergeFiles: string[] = [];
|
|
||||||
for (let i = 1; i <= subtags.length; i++) {
|
|
||||||
const lang = subtags.slice(0, i).join("-");
|
|
||||||
if (lang === TEST_LOCALE) {
|
|
||||||
mergeFiles.push(`${workDir}/${TEST_LOCALE}.json`);
|
|
||||||
} else if (lang !== "en") {
|
|
||||||
mergeFiles.push(`${inFrontendDir}/${lang}.json`);
|
|
||||||
if (mergeBackend) {
|
|
||||||
mergeFiles.push(`${inBackendDir}/${lang}.json`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const mergeStream = glupSrc(mergeFiles, { allowEmpty: true }).pipe(
|
|
||||||
new MergeJSON(locale, enMaster, emptyReviver)
|
|
||||||
);
|
|
||||||
mergesFinished.push(finished(mergeStream));
|
|
||||||
mergeStream.pipe(hashStream, { end: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for all merges to finish, then it's safe to end writing to the
|
|
||||||
// downstream pipeline and wait for all fragments to finish writing.
|
|
||||||
await Promise.all(mergesFinished);
|
|
||||||
hashStream.end();
|
|
||||||
await finished(fragmentsStream);
|
|
||||||
};
|
|
||||||
|
|
||||||
const writeTranslationMetaData = () =>
|
|
||||||
glupSrc([`${paths.translations_src}/translationMetadata.json`])
|
|
||||||
.pipe(
|
|
||||||
new CustomJSON((meta) => {
|
|
||||||
// Add the test translation in development.
|
|
||||||
if (!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(gulpDest(workDir));
|
|
||||||
|
|
||||||
export const buildTranslations = series(
|
|
||||||
parallel(fetchNightlyTranslations, series(cleanTranslations, makeWorkDir)),
|
|
||||||
createTestTranslation,
|
|
||||||
createMasterTranslation,
|
|
||||||
createTranslations,
|
|
||||||
writeTranslationMetaData
|
|
||||||
);
|
|
||||||
|
|
||||||
export const buildSupervisorTranslations = series(
|
|
||||||
setFragment("supervisor"),
|
|
||||||
buildTranslations
|
|
||||||
);
|
|
||||||
|
|
||||||
export const buildLandingPageTranslations = series(
|
|
||||||
setFragment("landing-page"),
|
|
||||||
buildTranslations
|
|
||||||
);
|
|
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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
187
build-scripts/gulp/webpack.js
Normal file
187
build-scripts/gulp/webpack.js
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
|
// Tasks to run webpack.
|
||||||
|
const fs = require("fs");
|
||||||
|
const gulp = require("gulp");
|
||||||
|
const webpack = require("webpack");
|
||||||
|
const WebpackDevServer = require("webpack-dev-server");
|
||||||
|
const log = require("fancy-log");
|
||||||
|
const path = require("path");
|
||||||
|
const paths = require("../paths");
|
||||||
|
const {
|
||||||
|
createAppConfig,
|
||||||
|
createDemoConfig,
|
||||||
|
createCastConfig,
|
||||||
|
createHassioConfig,
|
||||||
|
createGalleryConfig,
|
||||||
|
} = require("../webpack");
|
||||||
|
|
||||||
|
const bothBuilds = (createConfigFunc, params) => [
|
||||||
|
createConfigFunc({ ...params, latestBuild: true }),
|
||||||
|
createConfigFunc({ ...params, latestBuild: false }),
|
||||||
|
];
|
||||||
|
|
||||||
|
const isWsl =
|
||||||
|
fs.existsSync("/proc/version") &&
|
||||||
|
fs
|
||||||
|
.readFileSync("/proc/version", "utf-8")
|
||||||
|
.toLocaleLowerCase()
|
||||||
|
.includes("microsoft");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {{
|
||||||
|
* compiler: import("webpack").Compiler,
|
||||||
|
* contentBase: string,
|
||||||
|
* port: number,
|
||||||
|
* listenHost?: string
|
||||||
|
* }}
|
||||||
|
*/
|
||||||
|
const runDevServer = async ({
|
||||||
|
compiler,
|
||||||
|
contentBase,
|
||||||
|
port,
|
||||||
|
listenHost = "localhost",
|
||||||
|
}) => {
|
||||||
|
const server = new WebpackDevServer(
|
||||||
|
{
|
||||||
|
open: true,
|
||||||
|
host: listenHost,
|
||||||
|
port,
|
||||||
|
static: {
|
||||||
|
directory: contentBase,
|
||||||
|
watch: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
compiler
|
||||||
|
);
|
||||||
|
|
||||||
|
await server.start();
|
||||||
|
// Server listening
|
||||||
|
log("[webpack-dev-server]", `Project is running at http://localhost:${port}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const doneHandler = (done) => (err, stats) => {
|
||||||
|
if (err) {
|
||||||
|
log.error(err.stack || err);
|
||||||
|
if (err.details) {
|
||||||
|
log.error(err.details);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stats.hasErrors() || stats.hasWarnings()) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(stats.toString("minimal"));
|
||||||
|
}
|
||||||
|
|
||||||
|
log(`Build done @ ${new Date().toLocaleTimeString()}`);
|
||||||
|
|
||||||
|
if (done) {
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const prodBuild = (conf) =>
|
||||||
|
new Promise((resolve) => {
|
||||||
|
webpack(
|
||||||
|
conf,
|
||||||
|
// Resolve promise when done. Because we pass a callback, webpack closes itself
|
||||||
|
doneHandler(resolve)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task("webpack-watch-app", () => {
|
||||||
|
// This command will run forever because we don't close compiler
|
||||||
|
webpack(
|
||||||
|
process.env.ES5
|
||||||
|
? bothBuilds(createAppConfig, { isProdBuild: false })
|
||||||
|
: createAppConfig({ isProdBuild: false, latestBuild: true })
|
||||||
|
).watch({ poll: isWsl }, doneHandler());
|
||||||
|
gulp.watch(
|
||||||
|
path.join(paths.translations_src, "en.json"),
|
||||||
|
gulp.series("create-translations", "copy-translations-app")
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task("webpack-prod-app", () =>
|
||||||
|
prodBuild(
|
||||||
|
bothBuilds(createAppConfig, {
|
||||||
|
isProdBuild: true,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
gulp.task("webpack-dev-server-demo", () =>
|
||||||
|
runDevServer({
|
||||||
|
compiler: webpack(bothBuilds(createDemoConfig, { isProdBuild: false })),
|
||||||
|
contentBase: paths.demo_output_root,
|
||||||
|
port: 8090,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
gulp.task("webpack-prod-demo", () =>
|
||||||
|
prodBuild(
|
||||||
|
bothBuilds(createDemoConfig, {
|
||||||
|
isProdBuild: true,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
gulp.task("webpack-dev-server-cast", () =>
|
||||||
|
runDevServer({
|
||||||
|
compiler: webpack(bothBuilds(createCastConfig, { isProdBuild: false })),
|
||||||
|
contentBase: paths.cast_output_root,
|
||||||
|
port: 8080,
|
||||||
|
// Accessible from the network, because that's how Cast hits it.
|
||||||
|
listenHost: "0.0.0.0",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
gulp.task("webpack-prod-cast", () =>
|
||||||
|
prodBuild(
|
||||||
|
bothBuilds(createCastConfig, {
|
||||||
|
isProdBuild: true,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
gulp.task("webpack-watch-hassio", () => {
|
||||||
|
// This command will run forever because we don't close compiler
|
||||||
|
webpack(
|
||||||
|
createHassioConfig({
|
||||||
|
isProdBuild: false,
|
||||||
|
latestBuild: true,
|
||||||
|
})
|
||||||
|
).watch({ ignored: /build/, poll: isWsl }, doneHandler());
|
||||||
|
|
||||||
|
gulp.watch(
|
||||||
|
path.join(paths.translations_src, "en.json"),
|
||||||
|
gulp.series("build-supervisor-translations", "copy-translations-supervisor")
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task("webpack-prod-hassio", () =>
|
||||||
|
prodBuild(
|
||||||
|
bothBuilds(createHassioConfig, {
|
||||||
|
isProdBuild: true,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
gulp.task("webpack-dev-server-gallery", () =>
|
||||||
|
runDevServer({
|
||||||
|
// We don't use the es5 build, but the dev server will fuck up the publicPath if we don't
|
||||||
|
compiler: webpack(bothBuilds(createGalleryConfig, { isProdBuild: false })),
|
||||||
|
contentBase: paths.gallery_output_root,
|
||||||
|
port: 8100,
|
||||||
|
listenHost: "0.0.0.0",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
gulp.task("webpack-prod-gallery", () =>
|
||||||
|
prodBuild(
|
||||||
|
createGalleryConfig({
|
||||||
|
isProdBuild: true,
|
||||||
|
latestBuild: true,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
@@ -1,91 +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.ts";
|
|
||||||
|
|
||||||
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];
|
|
||||||
|
|
||||||
if (typeof presetEnvOpts !== "object") {
|
|
||||||
throw new Error(
|
|
||||||
"The first preset in babelOptions is not an object. This is unexpected."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
48
build-scripts/paths.js
Normal file
48
build-scripts/paths.js
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
polymer_dir: path.resolve(__dirname, ".."),
|
||||||
|
|
||||||
|
build_dir: path.resolve(__dirname, "../build"),
|
||||||
|
app_output_root: path.resolve(__dirname, "../hass_frontend"),
|
||||||
|
app_output_static: path.resolve(__dirname, "../hass_frontend/static"),
|
||||||
|
app_output_latest: path.resolve(
|
||||||
|
__dirname,
|
||||||
|
"../hass_frontend/frontend_latest"
|
||||||
|
),
|
||||||
|
app_output_es5: path.resolve(__dirname, "../hass_frontend/frontend_es5"),
|
||||||
|
|
||||||
|
demo_dir: path.resolve(__dirname, "../demo"),
|
||||||
|
demo_output_root: path.resolve(__dirname, "../demo/dist"),
|
||||||
|
demo_output_static: path.resolve(__dirname, "../demo/dist/static"),
|
||||||
|
demo_output_latest: path.resolve(__dirname, "../demo/dist/frontend_latest"),
|
||||||
|
demo_output_es5: path.resolve(__dirname, "../demo/dist/frontend_es5"),
|
||||||
|
|
||||||
|
cast_dir: path.resolve(__dirname, "../cast"),
|
||||||
|
cast_output_root: path.resolve(__dirname, "../cast/dist"),
|
||||||
|
cast_output_static: path.resolve(__dirname, "../cast/dist/static"),
|
||||||
|
cast_output_latest: path.resolve(__dirname, "../cast/dist/frontend_latest"),
|
||||||
|
cast_output_es5: path.resolve(__dirname, "../cast/dist/frontend_es5"),
|
||||||
|
|
||||||
|
gallery_dir: path.resolve(__dirname, "../gallery"),
|
||||||
|
gallery_build: path.resolve(__dirname, "../gallery/build"),
|
||||||
|
gallery_output_root: path.resolve(__dirname, "../gallery/dist"),
|
||||||
|
gallery_output_latest: path.resolve(
|
||||||
|
__dirname,
|
||||||
|
"../gallery/dist/frontend_latest"
|
||||||
|
),
|
||||||
|
gallery_output_static: path.resolve(__dirname, "../gallery/dist/static"),
|
||||||
|
|
||||||
|
hassio_dir: path.resolve(__dirname, "../hassio"),
|
||||||
|
hassio_output_root: path.resolve(__dirname, "../hassio/build"),
|
||||||
|
hassio_output_static: path.resolve(__dirname, "../hassio/build/static"),
|
||||||
|
hassio_output_latest: path.resolve(
|
||||||
|
__dirname,
|
||||||
|
"../hassio/build/frontend_latest"
|
||||||
|
),
|
||||||
|
hassio_output_es5: path.resolve(__dirname, "../hassio/build/frontend_es5"),
|
||||||
|
hassio_publicPath: "/api/hassio/app",
|
||||||
|
|
||||||
|
translations_src: path.resolve(__dirname, "../src/translations"),
|
||||||
|
};
|
@@ -1,63 +0,0 @@
|
|||||||
import path, { dirname as pathDirname } from "node:path";
|
|
||||||
import { fileURLToPath } from "node:url";
|
|
||||||
|
|
||||||
export const dirname = pathDirname(fileURLToPath(import.meta.url));
|
|
||||||
|
|
||||||
export default {
|
|
||||||
root_dir: path.resolve(dirname, ".."),
|
|
||||||
|
|
||||||
build_dir: path.resolve(dirname, "../build"),
|
|
||||||
app_output_root: path.resolve(dirname, "../hass_frontend"),
|
|
||||||
app_output_static: path.resolve(dirname, "../hass_frontend/static"),
|
|
||||||
app_output_latest: path.resolve(dirname, "../hass_frontend/frontend_latest"),
|
|
||||||
app_output_es5: path.resolve(dirname, "../hass_frontend/frontend_es5"),
|
|
||||||
|
|
||||||
demo_dir: path.resolve(dirname, "../demo"),
|
|
||||||
demo_output_root: path.resolve(dirname, "../demo/dist"),
|
|
||||||
demo_output_static: path.resolve(dirname, "../demo/dist/static"),
|
|
||||||
demo_output_latest: path.resolve(dirname, "../demo/dist/frontend_latest"),
|
|
||||||
demo_output_es5: path.resolve(dirname, "../demo/dist/frontend_es5"),
|
|
||||||
|
|
||||||
cast_dir: path.resolve(dirname, "../cast"),
|
|
||||||
cast_output_root: path.resolve(dirname, "../cast/dist"),
|
|
||||||
cast_output_static: path.resolve(dirname, "../cast/dist/static"),
|
|
||||||
cast_output_latest: path.resolve(dirname, "../cast/dist/frontend_latest"),
|
|
||||||
cast_output_es5: path.resolve(dirname, "../cast/dist/frontend_es5"),
|
|
||||||
|
|
||||||
gallery_dir: path.resolve(dirname, "../gallery"),
|
|
||||||
gallery_build: path.resolve(dirname, "../gallery/build"),
|
|
||||||
gallery_output_root: path.resolve(dirname, "../gallery/dist"),
|
|
||||||
gallery_output_latest: path.resolve(
|
|
||||||
dirname,
|
|
||||||
"../gallery/dist/frontend_latest"
|
|
||||||
),
|
|
||||||
gallery_output_static: path.resolve(dirname, "../gallery/dist/static"),
|
|
||||||
|
|
||||||
landingPage_dir: path.resolve(dirname, "../landing-page"),
|
|
||||||
landingPage_build: path.resolve(dirname, "../landing-page/build"),
|
|
||||||
landingPage_output_root: path.resolve(dirname, "../landing-page/dist"),
|
|
||||||
landingPage_output_latest: path.resolve(
|
|
||||||
dirname,
|
|
||||||
"../landing-page/dist/frontend_latest"
|
|
||||||
),
|
|
||||||
landingPage_output_es5: path.resolve(
|
|
||||||
dirname,
|
|
||||||
"../landing-page/dist/frontend_es5"
|
|
||||||
),
|
|
||||||
landingPage_output_static: path.resolve(
|
|
||||||
dirname,
|
|
||||||
"../landing-page/dist/static"
|
|
||||||
),
|
|
||||||
|
|
||||||
hassio_dir: path.resolve(dirname, "../hassio"),
|
|
||||||
hassio_output_root: path.resolve(dirname, "../hassio/build"),
|
|
||||||
hassio_output_static: path.resolve(dirname, "../hassio/build/static"),
|
|
||||||
hassio_output_latest: path.resolve(
|
|
||||||
dirname,
|
|
||||||
"../hassio/build/frontend_latest"
|
|
||||||
),
|
|
||||||
hassio_output_es5: path.resolve(dirname, "../hassio/build/frontend_es5"),
|
|
||||||
hassio_publicPath: "/api/hassio/app",
|
|
||||||
|
|
||||||
translations_src: path.resolve(dirname, "../src/translations"),
|
|
||||||
};
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user