mirror of
https://github.com/home-assistant/frontend.git
synced 2026-02-20 08:27:59 +00:00
Compare commits
8 Commits
dialog-nex
...
gulp-ts
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
efece17f50 | ||
|
|
79e5c59fdf | ||
|
|
0aa34a14dd | ||
|
|
1ced9959fa | ||
|
|
1b67a6f358 | ||
|
|
62f2b286ae | ||
|
|
8f7760f88f | ||
|
|
ff3b65605e |
@@ -1,4 +1,4 @@
|
||||
FROM mcr.microsoft.com/devcontainers/python:3.14
|
||||
FROM mcr.microsoft.com/devcontainers/python:1-3.13
|
||||
|
||||
ENV \
|
||||
DEBIAN_FRONTEND=noninteractive \
|
||||
|
||||
2
.github/ISSUE_TEMPLATE.md
vendored
2
.github/ISSUE_TEMPLATE.md
vendored
@@ -67,7 +67,7 @@ DO NOT DELETE ANY TEXT from this template! Otherwise, your issue may be closed w
|
||||
<!--
|
||||
If your issue is about how an entity is shown in the UI, please add the state
|
||||
and attributes for all situations with a screenshot of the UI.
|
||||
You can find this information at `/config/developer-tools/state`
|
||||
You can find this information at `/developer-tools/state`
|
||||
-->
|
||||
|
||||
```yaml
|
||||
|
||||
56
.github/PULL_REQUEST_TEMPLATE.md
vendored
56
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -21,14 +21,6 @@
|
||||
-->
|
||||
|
||||
|
||||
## Screenshots
|
||||
<!--
|
||||
If your PR includes visual changes, please add screenshots or a short video
|
||||
showing the before and after. This helps reviewers understand the impact of
|
||||
your changes.
|
||||
Note: Remove this section if this PR has no visual changes.
|
||||
-->
|
||||
|
||||
## Type of change
|
||||
<!--
|
||||
What type of change does your PR introduce to the Home Assistant frontend?
|
||||
@@ -43,6 +35,16 @@
|
||||
- [ ] Breaking change (fix/feature causing existing functionality to break)
|
||||
- [ ] Code quality improvements to existing code or addition of tests
|
||||
|
||||
## Example configuration
|
||||
<!--
|
||||
Supplying a configuration snippet, makes it easier for a maintainer to test
|
||||
your PR.
|
||||
-->
|
||||
|
||||
```yaml
|
||||
|
||||
```
|
||||
|
||||
## Additional information
|
||||
<!--
|
||||
Details are important, and help maintainers processing your PR.
|
||||
@@ -52,8 +54,6 @@
|
||||
- This PR fixes or closes issue: fixes #
|
||||
- This PR is related to issue or discussion:
|
||||
- Link to documentation pull request:
|
||||
- Link to developer documentation pull request:
|
||||
- Link to backend pull request:
|
||||
|
||||
## Checklist
|
||||
<!--
|
||||
@@ -61,50 +61,18 @@
|
||||
creating the PR. If you're unsure about any of them, don't hesitate to ask.
|
||||
We're here to help! This is simply a reminder of what we are going to look
|
||||
for before merging your code.
|
||||
|
||||
AI tools are welcome, but contributors are responsible for *fully*
|
||||
understanding the code before submitting a PR.
|
||||
-->
|
||||
|
||||
- [ ] I understand the code I am submitting and can explain how it works.
|
||||
- [ ] The code change is tested and works locally.
|
||||
- [ ] There is no commented out code in this PR.
|
||||
- [ ] I have followed the [development checklist][dev-checklist]
|
||||
- [ ] I have followed the [perfect PR recommendations][perfect-pr]
|
||||
- [ ] Any generated code has been carefully reviewed for correctness and compliance with project standards.
|
||||
- [ ] Tests have been added to verify that the new code works.
|
||||
|
||||
If user exposed functionality or configuration variables are added/changed:
|
||||
|
||||
- [ ] Documentation added/updated for [www.home-assistant.io][docs-repository]
|
||||
|
||||
<!--
|
||||
This project is very active and we have a high turnover of pull requests.
|
||||
|
||||
Unfortunately, the number of incoming pull requests is higher than what our
|
||||
reviewers can review and merge so there is a long backlog of pull requests
|
||||
waiting for review. You can help here!
|
||||
|
||||
By reviewing another pull request, you will help raise the code quality of
|
||||
that pull request and the final review will be faster. This way the general
|
||||
pace of pull request reviews will go up and your wait time will go down.
|
||||
|
||||
When picking a pull request to review, try to choose one that hasn't yet
|
||||
been reviewed.
|
||||
|
||||
Thanks for helping out!
|
||||
-->
|
||||
|
||||
To help with the load of incoming pull requests:
|
||||
|
||||
- [ ] I have reviewed two other [open pull requests][prs] in this repository.
|
||||
|
||||
[prs]: https://github.com/home-assistant/frontend/pulls?q=is%3Aopen+is%3Apr+-author%3A%40me+-draft%3Atrue+sort%3Acreated-desc+review%3Anone+-status%3Afailure
|
||||
|
||||
<!--
|
||||
Thank you for contributing <3
|
||||
|
||||
Below, some useful links you could explore:
|
||||
-->
|
||||
[dev-checklist]: https://developers.home-assistant.io/docs/development_checklist/
|
||||
|
||||
[docs-repository]: https://github.com/home-assistant/home-assistant.io
|
||||
[perfect-pr]: https://developers.home-assistant.io/docs/review-process/#creating-the-perfect-pr
|
||||
|
||||
200
.github/copilot-instructions.md
vendored
200
.github/copilot-instructions.md
vendored
@@ -2,8 +2,6 @@
|
||||
|
||||
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.
|
||||
|
||||
**Note**: This file contains high-level guidelines and references to implementation patterns. For detailed component documentation, API references, and usage examples, refer to the `gallery/` directory.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Quick Reference](#quick-reference)
|
||||
@@ -22,13 +20,11 @@ You are an assistant helping with development of the Home Assistant frontend. Th
|
||||
```bash
|
||||
yarn lint # ESLint + Prettier + TypeScript + Lit
|
||||
yarn format # Auto-fix ESLint + Prettier
|
||||
yarn lint:types # TypeScript compiler (run WITHOUT file arguments)
|
||||
yarn lint:types # TypeScript compiler
|
||||
yarn test # Vitest
|
||||
script/develop # Development server
|
||||
```
|
||||
|
||||
> **WARNING:** Never run `tsc` or `yarn lint:types` with file arguments (e.g., `yarn lint:types src/file.ts`). When `tsc` receives file arguments, it ignores `tsconfig.json` and emits `.js` files into `src/`, polluting the codebase. Always run `yarn lint:types` without arguments. For individual file type checking, rely on IDE diagnostics. If `.js` files are accidentally generated, clean up with `git clean -fd src/`.
|
||||
|
||||
### Component Prefixes
|
||||
|
||||
- `ha-` - Home Assistant components
|
||||
@@ -155,10 +151,6 @@ try {
|
||||
### Styling Guidelines
|
||||
|
||||
- **Use CSS custom properties**: Leverage the theme system
|
||||
- **Use spacing tokens**: Prefer `--ha-space-*` tokens over hardcoded values for consistent spacing
|
||||
- Spacing scale: `--ha-space-1` (4px) through `--ha-space-20` (80px) in 4px increments
|
||||
- Defined in `src/resources/theme/core.globals.ts`
|
||||
- Common values: `--ha-space-2` (8px), `--ha-space-4` (16px), `--ha-space-8` (32px)
|
||||
- **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
|
||||
@@ -167,61 +159,21 @@ try {
|
||||
static get styles() {
|
||||
return css`
|
||||
:host {
|
||||
padding: var(--ha-space-4);
|
||||
--spacing: 16px;
|
||||
padding: var(--spacing);
|
||||
color: var(--primary-text-color);
|
||||
background-color: var(--card-background-color);
|
||||
}
|
||||
|
||||
.content {
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
:host {
|
||||
padding: var(--ha-space-2);
|
||||
--spacing: 8px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
```
|
||||
|
||||
### View Transitions
|
||||
|
||||
The View Transitions API creates smooth animations between DOM state changes. When implementing view transitions:
|
||||
|
||||
**Core Resources:**
|
||||
|
||||
- **Utility wrapper**: `src/common/util/view-transition.ts` - `withViewTransition()` function with graceful fallback
|
||||
- **Real-world example**: `src/util/launch-screen.ts` - Launch screen fade pattern with browser support detection
|
||||
- **Animation keyframes**: `src/resources/theme/animations.globals.ts` - Global `fade-in`, `fade-out`, `scale` animations
|
||||
- **Animation duration**: `src/resources/theme/core.globals.ts` - `--ha-animation-duration-fast` (150ms), `--ha-animation-duration-normal` (250ms), `--ha-animation-duration-slow` (350ms) (all respect `prefers-reduced-motion`)
|
||||
|
||||
**Implementation Guidelines:**
|
||||
|
||||
1. Always use `withViewTransition()` wrapper for automatic fallback
|
||||
2. Keep transitions simple (subtle crossfades and fades work best)
|
||||
3. Use `--ha-animation-duration-*` CSS variables for consistent timing (`fast`, `normal`, `slow`)
|
||||
4. Assign unique `view-transition-name` to elements (must be unique at any given time)
|
||||
5. For Lit components: Override `performUpdate()` or use `::part()` for internal elements
|
||||
|
||||
**Default Root Transition:**
|
||||
|
||||
By default, `:root` receives `view-transition-name: root`, creating a full-page crossfade. Target with [`::view-transition-group(root)`](https://developer.mozilla.org/en-US/docs/Web/CSS/::view-transition-group) to customize the default page transition.
|
||||
|
||||
**Important Constraints:**
|
||||
|
||||
- Each `view-transition-name` must be unique at any given time
|
||||
- Only one view transition can run at a time
|
||||
- **Shadow DOM incompatibility**: View transitions operate at document level and do not work within Shadow DOM due to style isolation ([spec discussion](https://github.com/w3c/csswg-drafts/issues/10303)). For web components, set `view-transition-name` on the `:host` element or use document-level transitions
|
||||
|
||||
**Specification & Documentation:**
|
||||
|
||||
For browser support, API details, and current specifications, refer to these authoritative sources (note: check publication dates as specs evolve):
|
||||
|
||||
- [MDN: View Transition API](https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API) - Comprehensive API reference
|
||||
- [Chrome for Developers: View Transitions](https://developer.chrome.com/docs/web-platform/view-transitions) - Implementation guide and examples
|
||||
- [W3C Draft Specification](https://drafts.csswg.org/css-view-transitions/) - Official specification (evolving)
|
||||
|
||||
### Performance Best Practices
|
||||
|
||||
- **Code split**: Split code at the panel/dialog level
|
||||
@@ -239,7 +191,12 @@ For browser support, API details, and current specifications, refer to these aut
|
||||
|
||||
## Component Library
|
||||
|
||||
### Dialog Component
|
||||
### 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):**
|
||||
|
||||
@@ -253,41 +210,20 @@ fireEvent(this, "show-dialog", {
|
||||
|
||||
**Dialog Implementation Requirements:**
|
||||
|
||||
- Use `ha-dialog` component
|
||||
- Implement `HassDialog<T>` interface
|
||||
- Use `@state() private _open = false` to control dialog visibility
|
||||
- Set `_open = true` in `showDialog()`, `_open = false` in `closeDialog()`
|
||||
- Use `createCloseHeading()` for standard headers
|
||||
- Import `haStyleDialog` for consistent styling
|
||||
- Return `nothing` when no params (loading state)
|
||||
- Fire `dialog-closed` event in `_dialogClosed()` handler
|
||||
- Use `header-title` attribute for simple titles
|
||||
- Use `header-subtitle` attribute for simple subtitles
|
||||
- Use slots for custom content where the standard attributes are not enough
|
||||
- Use `ha-dialog-footer` with `primaryAction`/`secondaryAction` slots for footer content
|
||||
- Add `autofocus` to first focusable element (e.g., `<ha-form autofocus>`). The component may need to forward this attribute internally.
|
||||
- Fire `dialog-closed` event when closing
|
||||
- Add `dialogInitialFocus` for accessibility
|
||||
|
||||
**Dialog Sizing:**
|
||||
|
||||
- Use `width` attribute with predefined sizes: `"small"` (320px), `"medium"` (560px - default), `"large"` (720px), or `"full"`
|
||||
- Custom sizing is NOT recommended - use the standard width presets
|
||||
|
||||
**Button Appearance Guidelines:**
|
||||
|
||||
- **Primary action buttons**: Default appearance (no appearance attribute) or omit for standard styling
|
||||
- **Secondary action buttons**: Use `appearance="plain"` for cancel/dismiss actions
|
||||
- **Destructive actions**: Use `appearance="filled"` for delete/remove operations (combined with appropriate semantic styling)
|
||||
- **Button sizes**: Use `size="small"` (32px height) or default/medium (40px height)
|
||||
- Always place primary action in `slot="primaryAction"` and secondary in `slot="secondaryAction"` within `ha-dialog-footer`
|
||||
|
||||
**Gallery Documentation:**
|
||||
|
||||
- `gallery/src/pages/components/ha-dialog.markdown`
|
||||
- `gallery/src/pages/components/ha-dialogs.markdown`
|
||||
````
|
||||
|
||||
### 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
|
||||
@@ -299,11 +235,7 @@ fireEvent(this, "show-dialog", {
|
||||
.computeLabel=${(schema) => this.hass.localize(`ui.panel.${schema.name}`)}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
```
|
||||
|
||||
**Gallery Documentation:**
|
||||
|
||||
- `gallery/src/pages/components/ha-form.markdown`
|
||||
````
|
||||
|
||||
### Alert Component (ha-alert)
|
||||
|
||||
@@ -317,35 +249,6 @@ fireEvent(this, "show-dialog", {
|
||||
<ha-alert alert-type="success" dismissable>Success message</ha-alert>
|
||||
```
|
||||
|
||||
**Gallery Documentation:**
|
||||
|
||||
- `gallery/src/pages/components/ha-alert.markdown`
|
||||
|
||||
### Keyboard Shortcuts (ShortcutManager)
|
||||
|
||||
The `ShortcutManager` class provides a unified way to register keyboard shortcuts with automatic input field protection.
|
||||
|
||||
**Key Features:**
|
||||
|
||||
- Automatically blocks shortcuts when input fields are focused
|
||||
- Prevents shortcuts during text selection (configurable via `allowWhenTextSelected`)
|
||||
- Supports both character-based and KeyCode-based shortcuts (for non-latin keyboards)
|
||||
|
||||
**Implementation:**
|
||||
|
||||
- **Class definition**: `src/common/keyboard/shortcuts.ts`
|
||||
- **Real-world example**: `src/state/quick-bar-mixin.ts` - Global shortcuts (e, c, d, m, a, Shift+?) with non-latin keyboard fallbacks
|
||||
|
||||
### Tooltip Component (ha-tooltip)
|
||||
|
||||
The `ha-tooltip` component wraps Web Awesome tooltip with Home Assistant theming. Use for providing contextual help text on hover.
|
||||
|
||||
**Implementation:**
|
||||
|
||||
- **Component definition**: `src/components/ha-tooltip.ts`
|
||||
- **Usage example**: `src/components/ha-label.ts`
|
||||
- **Gallery documentation**: `gallery/src/pages/components/ha-tooltip.markdown`
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Creating a Panel
|
||||
@@ -372,6 +275,65 @@ export class HaPanelMyFeature extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
```
|
||||
|
||||
### 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)
|
||||
@@ -462,10 +424,6 @@ this.hass.localize("ui.panel.config.updates.update_available", {
|
||||
- Use HTTPS - All external resources must use HTTPS
|
||||
- CSP compliance - Ensure code works with Content Security Policy
|
||||
|
||||
### Pull Requests
|
||||
|
||||
When creating a pull request, you **must** use the PR template located at `.github/PULL_REQUEST_TEMPLATE.md`. Read the template file and use its full content as the PR body, filling in each section appropriately. Do not omit, reorder, or rewrite the template sections. Do not check the checklist items on behalf of the user — those are the user's responsibility to review and check. If the PR includes UI changes, remind the user to add screenshots or a short video to the PR after creating it.
|
||||
|
||||
### Text and Copy Guidelines
|
||||
|
||||
#### Terminology Standards
|
||||
@@ -528,6 +486,7 @@ When creating a pull request, you **must** use the PR template located at `.gith
|
||||
|
||||
#### Key Terminology
|
||||
|
||||
- **"add-on"** (hyphenated, not "addon")
|
||||
- **"integration"** (preferred over "component")
|
||||
- **Technical terms**: Use lowercase (automation, entity, device, service)
|
||||
|
||||
@@ -619,10 +578,13 @@ this.hass.localize("ui.panel.config.automation.delete_confirm", {
|
||||
- [ ] American English spelling
|
||||
- [ ] Friendly, informational tone
|
||||
- [ ] Avoids abbreviations and jargon
|
||||
- [ ] Correct terminology (integration not component)
|
||||
- [ ] 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)
|
||||
|
||||
5
.github/labeler.yml
vendored
5
.github/labeler.yml
vendored
@@ -44,3 +44,8 @@ GitHub Actions:
|
||||
- any-glob-to-any-file:
|
||||
- .github/workflows/**
|
||||
- .github/*.yml
|
||||
|
||||
Supervisor:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- hassio/src/**
|
||||
|
||||
16
.github/workflows/cast_deployment.yaml
vendored
16
.github/workflows/cast_deployment.yaml
vendored
@@ -21,12 +21,12 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@v4.2.2
|
||||
with:
|
||||
ref: dev
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
uses: actions/setup-node@v4.4.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -35,14 +35,14 @@ jobs:
|
||||
run: yarn install --immutable
|
||||
|
||||
- name: Build Cast
|
||||
run: ./node_modules/.bin/gulp 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@23.7.3 deploy --dir=cast/dist --alias dev
|
||||
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 }}
|
||||
@@ -56,12 +56,12 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@v4.2.2
|
||||
with:
|
||||
ref: master
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
uses: actions/setup-node@v4.4.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -70,14 +70,14 @@ jobs:
|
||||
run: yarn install --immutable
|
||||
|
||||
- name: Build Cast
|
||||
run: ./node_modules/.bin/gulp 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@23.7.3 deploy --dir=cast/dist --prod
|
||||
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 }}
|
||||
|
||||
49
.github/workflows/ci.yaml
vendored
49
.github/workflows/ci.yaml
vendored
@@ -24,9 +24,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
uses: actions/setup-node@v4.4.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -35,9 +35,9 @@ jobs:
|
||||
- name: Check for duplicate dependencies
|
||||
run: yarn dedupe --check
|
||||
- name: Build resources
|
||||
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages
|
||||
run: yarn run-task gen-icons-json build-translations build-locale-data gather-gallery-pages
|
||||
- name: Setup lint cache
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
uses: actions/cache@v4.2.3
|
||||
with:
|
||||
path: |
|
||||
node_modules/.cache/prettier
|
||||
@@ -58,16 +58,16 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
uses: actions/setup-node@v4.4.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
- name: Install dependencies
|
||||
run: yarn install --immutable
|
||||
- name: Build resources
|
||||
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data
|
||||
run: yarn run-task gen-icons-json build-translations build-locale-data
|
||||
- name: Run Tests
|
||||
run: yarn run test
|
||||
build:
|
||||
@@ -76,28 +76,45 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
uses: actions/setup-node@v4.4.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
- name: Install dependencies
|
||||
run: yarn install --immutable
|
||||
- name: Build Application
|
||||
run: ./node_modules/.bin/gulp build-app
|
||||
run: yarn run-task build-app
|
||||
env:
|
||||
IS_TEST: "true"
|
||||
- name: Upload bundle stats
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@v4.6.2
|
||||
with:
|
||||
name: frontend-bundle-stats
|
||||
path: build/stats/*.json
|
||||
if-no-files-found: error
|
||||
- name: Upload frontend build
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
supervisor:
|
||||
name: Build supervisor
|
||||
needs: [lint, test]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.4.0
|
||||
with:
|
||||
name: frontend-build
|
||||
path: hass_frontend/
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
- name: Install dependencies
|
||||
run: yarn install --immutable
|
||||
- name: Build Application
|
||||
run: yarn run-task build-hassio
|
||||
env:
|
||||
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
|
||||
retention-days: 7
|
||||
|
||||
8
.github/workflows/codeql-analysis.yml
vendored
8
.github/workflows/codeql-analysis.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@v4.2.2
|
||||
with:
|
||||
# We must fetch at least the immediate parents so that if this is
|
||||
# a pull request then we can checkout the head.
|
||||
@@ -36,14 +36,14 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
@@ -57,4 +57,4 @@ jobs:
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3
|
||||
uses: github/codeql-action/analyze@v3
|
||||
|
||||
16
.github/workflows/demo_deployment.yaml
vendored
16
.github/workflows/demo_deployment.yaml
vendored
@@ -22,12 +22,12 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@v4.2.2
|
||||
with:
|
||||
ref: dev
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
uses: actions/setup-node@v4.4.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -36,14 +36,14 @@ jobs:
|
||||
run: yarn install --immutable
|
||||
|
||||
- name: Build Demo
|
||||
run: ./node_modules/.bin/gulp 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@23.7.3 deploy --dir=demo/dist --prod
|
||||
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 }}
|
||||
@@ -57,12 +57,12 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@v4.2.2
|
||||
with:
|
||||
ref: master
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
uses: actions/setup-node@v4.4.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -71,14 +71,14 @@ jobs:
|
||||
run: yarn install --immutable
|
||||
|
||||
- name: Build Demo
|
||||
run: ./node_modules/.bin/gulp 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@23.7.3 deploy --dir=demo/dist --prod
|
||||
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 }}
|
||||
|
||||
8
.github/workflows/design_deployment.yaml
vendored
8
.github/workflows/design_deployment.yaml
vendored
@@ -16,10 +16,10 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
uses: actions/setup-node@v4.4.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -28,14 +28,14 @@ jobs:
|
||||
run: yarn install --immutable
|
||||
|
||||
- name: Build Gallery
|
||||
run: ./node_modules/.bin/gulp 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@23.7.3 deploy --dir=gallery/dist --prod
|
||||
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 }}
|
||||
|
||||
8
.github/workflows/design_preview.yaml
vendored
8
.github/workflows/design_preview.yaml
vendored
@@ -21,10 +21,10 @@ jobs:
|
||||
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
uses: actions/setup-node@v4.4.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -33,14 +33,14 @@ jobs:
|
||||
run: yarn install --immutable
|
||||
|
||||
- name: Build Gallery
|
||||
run: ./node_modules/.bin/gulp 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@23.7.3 deploy --dir=gallery/dist --alias "deploy-preview-${{ github.event.number }}" \
|
||||
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 }}
|
||||
|
||||
2
.github/workflows/labeler.yaml
vendored
2
.github/workflows/labeler.yaml
vendored
@@ -10,6 +10,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Apply labels
|
||||
uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1
|
||||
uses: actions/labeler@v5.0.0
|
||||
with:
|
||||
sync-labels: true
|
||||
|
||||
2
.github/workflows/lock.yml
vendored
2
.github/workflows/lock.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
||||
lock:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@7266a7ce5c1df01b1c6db85bf8cd86c737dadbe7 # v6.0.0
|
||||
- uses: dessant/lock-threads@v5.0.1
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
process-only: "issues, prs"
|
||||
|
||||
12
.github/workflows/nightly.yaml
vendored
12
.github/workflows/nightly.yaml
vendored
@@ -6,7 +6,7 @@ on:
|
||||
- cron: "0 1 * * *"
|
||||
|
||||
env:
|
||||
PYTHON_VERSION: "3.14"
|
||||
PYTHON_VERSION: "3.13"
|
||||
NODE_OPTIONS: --max_old_space_size=6144
|
||||
|
||||
permissions:
|
||||
@@ -20,15 +20,15 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
uses: actions/setup-node@v4.4.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -57,14 +57,14 @@ jobs:
|
||||
run: tar -czvf translations.tar.gz translations
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
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@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@v4.6.2
|
||||
with:
|
||||
name: translations
|
||||
path: translations.tar.gz
|
||||
|
||||
4
.github/workflows/relative-ci.yaml
vendored
4
.github/workflows/relative-ci.yaml
vendored
@@ -12,12 +12,12 @@ jobs:
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
strategy:
|
||||
matrix:
|
||||
bundle: [frontend]
|
||||
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@3c681926017930047fc03acaa35cd6a44efcbfc3 # v3.2.2
|
||||
uses: relative-ci/agent-action@v3.0.0
|
||||
with:
|
||||
key: ${{ secrets[format('RELATIVE_CI_KEY_{0}_{1}', matrix.bundle, matrix.build)] }}
|
||||
token: ${{ github.token }}
|
||||
|
||||
2
.github/workflows/release-drafter.yaml
vendored
2
.github/workflows/release-drafter.yaml
vendored
@@ -18,6 +18,6 @@ jobs:
|
||||
pull-requests: read
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: release-drafter/release-drafter@6db134d15f3909ccc9eefd369f02bd1e9cffdf97 # v6.2.0
|
||||
- uses: release-drafter/release-drafter@v6.1.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
63
.github/workflows/release.yaml
vendored
63
.github/workflows/release.yaml
vendored
@@ -6,7 +6,7 @@ on:
|
||||
- published
|
||||
|
||||
env:
|
||||
PYTHON_VERSION: "3.14"
|
||||
PYTHON_VERSION: "3.13"
|
||||
NODE_OPTIONS: --max_old_space_size=6144
|
||||
|
||||
# Set default workflow permissions
|
||||
@@ -19,17 +19,14 @@ jobs:
|
||||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
environment: pypi
|
||||
permissions:
|
||||
contents: write # Required to upload release assets
|
||||
id-token: write # For "Trusted Publisher" to PyPi
|
||||
if: github.repository_owner == 'home-assistant'
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
@@ -37,7 +34,7 @@ jobs:
|
||||
uses: home-assistant/actions/helpers/verify-version@master
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
uses: actions/setup-node@v4.4.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -49,20 +46,16 @@ jobs:
|
||||
run: ./script/translations_download
|
||||
env:
|
||||
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }}
|
||||
|
||||
- name: Build and release package
|
||||
run: |
|
||||
python3 -m pip install build
|
||||
python3 -m pip install twine build
|
||||
export TWINE_USERNAME="__token__"
|
||||
export TWINE_PASSWORD="${{ secrets.TWINE_TOKEN }}"
|
||||
export SKIP_FETCH_NIGHTLY_TRANSLATIONS=1
|
||||
script/release
|
||||
|
||||
- name: Publish to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
|
||||
with:
|
||||
skip-existing: true
|
||||
|
||||
- name: Upload release assets
|
||||
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
|
||||
uses: softprops/action-gh-release@v2.3.2
|
||||
with:
|
||||
files: |
|
||||
dist/*.whl
|
||||
@@ -80,11 +73,10 @@ jobs:
|
||||
version=$(echo "${{ github.ref }}" | awk -F"/" '{print $NF}' )
|
||||
echo "home-assistant-frontend==$version" > ./requirements.txt
|
||||
|
||||
# home-assistant/wheels doesn't support SHA pinning
|
||||
- name: Build wheels
|
||||
uses: home-assistant/wheels@2025.12.0
|
||||
uses: home-assistant/wheels@2025.03.0
|
||||
with:
|
||||
abi: cp314
|
||||
abi: cp313
|
||||
tag: musllinux_1_2
|
||||
arch: amd64
|
||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||
@@ -98,9 +90,9 @@ jobs:
|
||||
contents: write # Required to upload release assets
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
uses: actions/setup-node@v4.4.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -115,6 +107,35 @@ jobs:
|
||||
- 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@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
|
||||
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
|
||||
|
||||
33
.github/workflows/restrict-task-creation.yml
vendored
33
.github/workflows/restrict-task-creation.yml
vendored
@@ -5,43 +5,14 @@ on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.issue.number }}
|
||||
|
||||
jobs:
|
||||
add-no-stale:
|
||||
name: Add no-stale label
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write # To add labels to issues
|
||||
if: >-
|
||||
github.event.issue.type.name == 'Task'
|
||||
|| github.event.issue.type.name == 'Epic'
|
||||
|| github.event.issue.type.name == 'Opportunity'
|
||||
steps:
|
||||
- name: Add no-stale label
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
script: |
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
labels: ['no-stale']
|
||||
});
|
||||
|
||||
check-authorization:
|
||||
name: Check authorization
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write # To comment on, label, and close issues
|
||||
# Only run if this is a Task issue type (from the issue form)
|
||||
if: github.event.issue.type.name == 'Task'
|
||||
if: github.event.issue.issue_type == 'Task'
|
||||
steps:
|
||||
- name: Check if user is authorized
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const issueAuthor = context.payload.issue.user.login;
|
||||
|
||||
2
.github/workflows/stale.yml
vendored
2
.github/workflows/stale.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 90 days stale policy
|
||||
uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
|
||||
uses: actions/stale@v9.1.0
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
days-before-stale: 90
|
||||
|
||||
2
.github/workflows/translations.yaml
vendored
2
.github/workflows/translations.yaml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Upload Translations
|
||||
run: |
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -15,7 +15,7 @@ dist/
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
.pnp.*
|
||||
node_modules/
|
||||
/node_modules/
|
||||
yarn-error.log
|
||||
npm-debug.log
|
||||
|
||||
@@ -56,5 +56,4 @@ test/coverage/
|
||||
|
||||
# AI tooling
|
||||
.claude
|
||||
.cursor
|
||||
|
||||
|
||||
55
.vscode/tasks.json
vendored
55
.vscode/tasks.json
vendored
@@ -73,6 +73,37 @@
|
||||
"instanceLimit": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Develop Supervisor panel",
|
||||
"type": "gulp",
|
||||
"task": "develop-hassio",
|
||||
"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 Gallery",
|
||||
"type": "gulp",
|
||||
@@ -215,6 +246,20 @@
|
||||
"instanceLimit": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Run HA Core for Supervisor in devcontainer",
|
||||
"type": "shell",
|
||||
"command": "SUPERVISOR=${input:supervisorHost} SUPERVISOR_TOKEN=${input:supervisorToken} script/core",
|
||||
"isBackground": true,
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
"problemMatcher": [],
|
||||
"runOptions": {
|
||||
"instanceLimit": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Setup and fetch nightly translations",
|
||||
"type": "gulp",
|
||||
@@ -223,6 +268,16 @@
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"id": "supervisorHost",
|
||||
"type": "promptString",
|
||||
"description": "The IP of the Supervisor host running the Remote API proxy add-on"
|
||||
},
|
||||
{
|
||||
"id": "supervisorToken",
|
||||
"type": "promptString",
|
||||
"description": "The token for the Remote API proxy add-on"
|
||||
},
|
||||
{
|
||||
"id": "coreUrl",
|
||||
"type": "promptString",
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,11 +1,9 @@
|
||||
compressionLevel: mixed
|
||||
|
||||
npmMinimalAgeGate: "3d"
|
||||
|
||||
defaultSemverRangePrefix: ""
|
||||
|
||||
enableGlobalCache: false
|
||||
|
||||
nodeLinker: node-modules
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.12.0.cjs
|
||||
yarnPath: .yarn/releases/yarn-4.9.2.cjs
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
# People marked here will be automatically requested for a review
|
||||
# when the code that they own is touched.
|
||||
# https://github.com/blog/2392-introducing-code-owners
|
||||
# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
|
||||
|
||||
# Part of the frontend that mobile developper should review
|
||||
src/external_app/ @bgoncal @TimoPtr
|
||||
test/external_app/ @bgoncal @TimoPtr
|
||||
@@ -14,6 +14,7 @@ This is the repository for the official [Home Assistant](https://home-assistant.
|
||||
- Development: [Instructions](https://developers.home-assistant.io/docs/frontend/development/)
|
||||
- Production build: `script/build_frontend`
|
||||
- Gallery: `cd gallery && script/develop_gallery`
|
||||
- Supervisor: [Instructions](https://developers.home-assistant.io/docs/supervisor/developing)
|
||||
|
||||
## Frontend development
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import defineProvider from "@babel/helper-define-polyfill-provider";
|
||||
import { join } from "node:path";
|
||||
import paths from "../paths.cjs";
|
||||
import paths from "../paths";
|
||||
|
||||
const POLYFILL_DIR = join(paths.root_dir, "src/resources/polyfills");
|
||||
|
||||
@@ -1,41 +1,43 @@
|
||||
const path = require("path");
|
||||
const env = require("./env.cjs");
|
||||
const paths = require("./paths.cjs");
|
||||
const { dependencies } = require("../package.json");
|
||||
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 BABEL_PLUGINS = path.join(__dirname, "babel-plugins");
|
||||
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
|
||||
module.exports.sourceMapURL = () => {
|
||||
const ref = env.version().endsWith("dev")
|
||||
export const sourceMapURL = () => {
|
||||
const ref = version().endsWith("dev")
|
||||
? process.env.GITHUB_SHA || "dev"
|
||||
: env.version();
|
||||
: version();
|
||||
return `https://raw.githubusercontent.com/home-assistant/frontend/${ref}/`;
|
||||
};
|
||||
|
||||
// Files from NPM Packages that should not be imported
|
||||
module.exports.ignorePackages = () => [];
|
||||
|
||||
// Files from NPM packages that we should replace with empty file
|
||||
module.exports.emptyPackages = ({ isLandingPageBuild }) =>
|
||||
export const emptyPackages = ({ isHassioBuild }) =>
|
||||
[
|
||||
// Icons in landingpage conflict with icons in HA so we don't load.
|
||||
isLandingPageBuild &&
|
||||
require.resolve(
|
||||
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")
|
||||
),
|
||||
isLandingPageBuild &&
|
||||
require.resolve(
|
||||
isHassioBuild &&
|
||||
import.meta.resolve(
|
||||
path.resolve(paths.root_dir, "src/components/ha-icon-picker.ts")
|
||||
),
|
||||
].filter(Boolean);
|
||||
|
||||
module.exports.definedVars = ({ isProdBuild, latestBuild, defineOverlay }) => ({
|
||||
export const definedVars = ({ isProdBuild, latestBuild, defineOverlay }) => ({
|
||||
__DEV__: !isProdBuild,
|
||||
__BUILD__: JSON.stringify(latestBuild ? "modern" : "legacy"),
|
||||
__VERSION__: JSON.stringify(env.version()),
|
||||
__VERSION__: JSON.stringify(version()),
|
||||
__DEMO__: false,
|
||||
__SUPERVISOR__: false,
|
||||
__BACKWARDS_COMPAT__: false,
|
||||
__STATIC_PATH__: "/static/",
|
||||
__HASS_URL__: `\`${
|
||||
@@ -50,7 +52,7 @@ module.exports.definedVars = ({ isProdBuild, latestBuild, defineOverlay }) => ({
|
||||
...defineOverlay,
|
||||
});
|
||||
|
||||
module.exports.htmlMinifierOptions = {
|
||||
export const htmlMinifierOptions = {
|
||||
caseSensitive: true,
|
||||
collapseWhitespace: true,
|
||||
conservativeCollapse: true,
|
||||
@@ -62,16 +64,16 @@ module.exports.htmlMinifierOptions = {
|
||||
},
|
||||
};
|
||||
|
||||
module.exports.terserOptions = ({ latestBuild, isTestBuild }) => ({
|
||||
export const terserOptions = ({ latestBuild, isTestBuild }) => ({
|
||||
safari10: !latestBuild,
|
||||
ecma: latestBuild ? 2015 : 5,
|
||||
ecma: latestBuild ? (2015 as const) : (5 as const),
|
||||
module: latestBuild,
|
||||
format: { comments: false },
|
||||
sourceMap: !isTestBuild,
|
||||
});
|
||||
|
||||
/** @type {import('@rspack/core').SwcLoaderOptions} */
|
||||
module.exports.swcOptions = () => ({
|
||||
export const swcOptions = () => ({
|
||||
jsc: {
|
||||
loose: true,
|
||||
externalHelpers: true,
|
||||
@@ -83,11 +85,16 @@ module.exports.swcOptions = () => ({
|
||||
},
|
||||
});
|
||||
|
||||
module.exports.babelOptions = ({
|
||||
export const babelOptions = ({
|
||||
latestBuild,
|
||||
isProdBuild,
|
||||
isTestBuild,
|
||||
sw,
|
||||
}: {
|
||||
latestBuild?: boolean;
|
||||
isProdBuild?: boolean;
|
||||
isTestBuild?: boolean;
|
||||
sw?: boolean;
|
||||
}) => ({
|
||||
babelrc: false,
|
||||
compact: false,
|
||||
@@ -134,7 +141,7 @@ module.exports.babelOptions = ({
|
||||
"@polymer/polymer/lib/utils/html-tag.js": ["html"],
|
||||
},
|
||||
strictCSS: true,
|
||||
htmlMinifier: module.exports.htmlMinifierOptions,
|
||||
htmlMinifier: htmlMinifierOptions,
|
||||
failOnError: false, // we can turn this off in case of false positives
|
||||
},
|
||||
],
|
||||
@@ -157,7 +164,7 @@ module.exports.babelOptions = ({
|
||||
// themselves to prevent self-injection.
|
||||
plugins: [
|
||||
[
|
||||
path.join(BABEL_PLUGINS, "custom-polyfill-plugin.js"),
|
||||
path.join(BABEL_PLUGINS, "custom-polyfill-plugin.ts"),
|
||||
{ method: "usage-global" },
|
||||
],
|
||||
],
|
||||
@@ -180,6 +187,7 @@ module.exports.babelOptions = ({
|
||||
include: /\/node_modules\//,
|
||||
exclude: [
|
||||
"element-internals-polyfill",
|
||||
"@shoelace-style",
|
||||
"@?lit(?:-labs|-element|-html)?",
|
||||
].map((p) => new RegExp(`/node_modules/${p}/`)),
|
||||
},
|
||||
@@ -217,8 +225,20 @@ const publicPath = (latestBuild, root = "") =>
|
||||
}
|
||||
*/
|
||||
|
||||
module.exports.config = {
|
||||
app({ isProdBuild, latestBuild, isStatsBuild, isTestBuild, isWDS }) {
|
||||
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: {
|
||||
@@ -253,7 +273,7 @@ module.exports.config = {
|
||||
outputPath: outputPath(paths.demo_output_root, latestBuild),
|
||||
publicPath: publicPath(latestBuild),
|
||||
defineOverlay: {
|
||||
__VERSION__: JSON.stringify(`DEMO-${env.version()}`),
|
||||
__VERSION__: JSON.stringify(`DEMO-${version()}`),
|
||||
__DEMO__: true,
|
||||
},
|
||||
isProdBuild,
|
||||
@@ -263,7 +283,7 @@ module.exports.config = {
|
||||
},
|
||||
|
||||
cast({ isProdBuild, latestBuild }) {
|
||||
const entry = {
|
||||
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"),
|
||||
};
|
||||
@@ -288,6 +308,26 @@ module.exports.config = {
|
||||
};
|
||||
},
|
||||
|
||||
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),
|
||||
@@ -314,7 +354,6 @@ module.exports.config = {
|
||||
publicPath: publicPath(latestBuild),
|
||||
isProdBuild,
|
||||
latestBuild,
|
||||
isLandingPageBuild: true,
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -1,34 +0,0 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const paths = require("./paths.cjs");
|
||||
|
||||
const isTrue = (value) => value === "1" || value?.toLowerCase() === "true";
|
||||
|
||||
module.exports = {
|
||||
isProdBuild() {
|
||||
return (
|
||||
process.env.NODE_ENV === "production" || module.exports.isStatsBuild()
|
||||
);
|
||||
},
|
||||
isStatsBuild() {
|
||||
return isTrue(process.env.STATS);
|
||||
},
|
||||
isTestBuild() {
|
||||
return isTrue(process.env.IS_TEST);
|
||||
},
|
||||
isNetlify() {
|
||||
return isTrue(process.env.NETLIFY);
|
||||
},
|
||||
version() {
|
||||
const version = fs
|
||||
.readFileSync(path.resolve(paths.root_dir, "pyproject.toml"), "utf8")
|
||||
.match(/version\W+=\W"(\d{8}\.\d(?:\.dev)?)"/);
|
||||
if (!version) {
|
||||
throw Error("Version not found");
|
||||
}
|
||||
return version[1];
|
||||
},
|
||||
isDevContainer() {
|
||||
return isTrue(process.env.DEV_CONTAINER);
|
||||
},
|
||||
};
|
||||
21
build-scripts/env.ts
Normal file
21
build-scripts/env.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
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,57 +0,0 @@
|
||||
import gulp from "gulp";
|
||||
import env from "../env.cjs";
|
||||
import "./clean.js";
|
||||
import "./compress.js";
|
||||
import "./entry-html.js";
|
||||
import "./gather-static.js";
|
||||
import "./gen-icons-json.js";
|
||||
import "./locale-data.js";
|
||||
import "./service-worker.js";
|
||||
import "./translations.js";
|
||||
import "./rspack.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-app-dev",
|
||||
"build-translations",
|
||||
"build-locale-data"
|
||||
),
|
||||
"copy-static-app",
|
||||
"rspack-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",
|
||||
"rspack-prod-app",
|
||||
gulp.parallel("gen-pages-app-prod", "gen-service-worker-app-prod"),
|
||||
// Don't compress running tests
|
||||
...(env.isTestBuild() || env.isStatsBuild() ? [] : ["compress-app"])
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"analyze-app",
|
||||
gulp.series(
|
||||
async function setEnv() {
|
||||
process.env.STATS = "1";
|
||||
},
|
||||
"clean",
|
||||
"rspack-prod-app"
|
||||
)
|
||||
);
|
||||
54
build-scripts/gulp/app.ts
Normal file
54
build-scripts/gulp/app.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
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
|
||||
);
|
||||
@@ -1,37 +0,0 @@
|
||||
import gulp from "gulp";
|
||||
import "./clean.js";
|
||||
import "./entry-html.js";
|
||||
import "./gather-static.js";
|
||||
import "./service-worker.js";
|
||||
import "./translations.js";
|
||||
import "./rspack.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-pages-cast-dev",
|
||||
"rspack-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",
|
||||
"rspack-prod-cast",
|
||||
"gen-pages-cast-prod"
|
||||
)
|
||||
);
|
||||
38
build-scripts/gulp/cast.ts
Normal file
38
build-scripts/gulp/cast.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
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
|
||||
);
|
||||
@@ -1,47 +0,0 @@
|
||||
import { deleteSync } from "del";
|
||||
import gulp from "gulp";
|
||||
import paths from "../paths.cjs";
|
||||
import "./translations.js";
|
||||
|
||||
gulp.task(
|
||||
"clean",
|
||||
gulp.parallel("clean-translations", async () =>
|
||||
deleteSync([paths.app_output_root, paths.build_dir])
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"clean-demo",
|
||||
gulp.parallel("clean-translations", async () =>
|
||||
deleteSync([paths.demo_output_root, paths.build_dir])
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"clean-cast",
|
||||
gulp.parallel("clean-translations", async () =>
|
||||
deleteSync([paths.cast_output_root, paths.build_dir])
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"clean-gallery",
|
||||
gulp.parallel("clean-translations", async () =>
|
||||
deleteSync([
|
||||
paths.gallery_output_root,
|
||||
paths.gallery_build,
|
||||
paths.build_dir,
|
||||
])
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"clean-landing-page",
|
||||
gulp.parallel("clean-translations", async () =>
|
||||
deleteSync([
|
||||
paths.landingPage_output_root,
|
||||
paths.landingPage_build,
|
||||
paths.build_dir,
|
||||
])
|
||||
)
|
||||
);
|
||||
31
build-scripts/gulp/clean.ts
Normal file
31
build-scripts/gulp/clean.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
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,
|
||||
])
|
||||
);
|
||||
@@ -1,59 +0,0 @@
|
||||
// Tasks to compress
|
||||
|
||||
import { constants } from "node:zlib";
|
||||
import gulp from "gulp";
|
||||
import brotli from "gulp-brotli";
|
||||
import zopfli from "gulp-zopfli-green";
|
||||
import paths from "../paths.cjs";
|
||||
|
||||
const filesGlob = "*.{js,json,css,svg,xml}";
|
||||
const brotliOptions = {
|
||||
skipLarger: true,
|
||||
params: {
|
||||
[constants.BROTLI_PARAM_QUALITY]: constants.BROTLI_MAX_QUALITY,
|
||||
},
|
||||
};
|
||||
const zopfliOptions = { threshold: 150 };
|
||||
|
||||
const compressModern = (rootDir, modernDir, compress) =>
|
||||
gulp
|
||||
.src([`${modernDir}/**/${filesGlob}`, `${rootDir}/sw-modern.js`], {
|
||||
base: rootDir,
|
||||
allowEmpty: true,
|
||||
})
|
||||
.pipe(compress === "zopfli" ? zopfli(zopfliOptions) : brotli(brotliOptions))
|
||||
.pipe(gulp.dest(rootDir));
|
||||
|
||||
const compressOther = (rootDir, modernDir, compress) =>
|
||||
gulp
|
||||
.src(
|
||||
[
|
||||
`${rootDir}/**/${filesGlob}`,
|
||||
`!${modernDir}/**/${filesGlob}`,
|
||||
`!${rootDir}/{sw-modern,service_worker}.js`,
|
||||
`${rootDir}/{authorize,onboarding}.html`,
|
||||
],
|
||||
{ base: rootDir, allowEmpty: true }
|
||||
)
|
||||
.pipe(compress === "zopfli" ? zopfli(zopfliOptions) : brotli(brotliOptions))
|
||||
.pipe(gulp.dest(rootDir));
|
||||
|
||||
const compressAppModernBrotli = () =>
|
||||
compressModern(paths.app_output_root, paths.app_output_latest, "brotli");
|
||||
const compressAppModernZopfli = () =>
|
||||
compressModern(paths.app_output_root, paths.app_output_latest, "zopfli");
|
||||
|
||||
const compressAppOtherBrotli = () =>
|
||||
compressOther(paths.app_output_root, paths.app_output_latest, "brotli");
|
||||
const compressAppOtherZopfli = () =>
|
||||
compressOther(paths.app_output_root, paths.app_output_latest, "zopfli");
|
||||
|
||||
gulp.task(
|
||||
"compress-app",
|
||||
gulp.parallel(
|
||||
compressAppModernBrotli,
|
||||
compressAppOtherBrotli,
|
||||
compressAppModernZopfli,
|
||||
compressAppOtherZopfli
|
||||
)
|
||||
);
|
||||
79
build-scripts/gulp/compress.ts
Normal file
79
build-scripts/gulp/compress.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
// 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
|
||||
);
|
||||
@@ -1,54 +0,0 @@
|
||||
import gulp from "gulp";
|
||||
import "./clean.js";
|
||||
import "./entry-html.js";
|
||||
import "./gather-static.js";
|
||||
import "./gen-icons-json.js";
|
||||
import "./service-worker.js";
|
||||
import "./translations.js";
|
||||
import "./rspack.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-pages-demo-dev",
|
||||
"build-translations",
|
||||
"build-locale-data"
|
||||
),
|
||||
"copy-static-demo",
|
||||
"rspack-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",
|
||||
"rspack-prod-demo",
|
||||
"gen-pages-demo-prod"
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"analyze-demo",
|
||||
gulp.series(
|
||||
async function setEnv() {
|
||||
process.env.STATS = "1";
|
||||
},
|
||||
"clean",
|
||||
"rspack-prod-demo"
|
||||
)
|
||||
);
|
||||
47
build-scripts/gulp/demo.ts
Normal file
47
build-scripts/gulp/demo.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
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,10 +1,10 @@
|
||||
import fs from "fs/promises";
|
||||
import gulp from "gulp";
|
||||
import path from "path";
|
||||
import mapStream from "map-stream";
|
||||
import transform from "gulp-json-transform";
|
||||
import { LokaliseApi } from "@lokalise/node-api";
|
||||
import { 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`;
|
||||
@@ -12,11 +12,14 @@ const inDirBackend = `${inDir}/backend`;
|
||||
const srcMeta = "src/translations/translationMetadata.json";
|
||||
const encoding = "utf8";
|
||||
|
||||
function hasHtml(data) {
|
||||
return /<\S*>/i.test(data);
|
||||
}
|
||||
const hasHtml = (data) => /<\S*>/i.test(data);
|
||||
|
||||
function recursiveCheckHasHtml(file, data, errors, recKey) {
|
||||
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;
|
||||
@@ -25,9 +28,9 @@ function recursiveCheckHasHtml(file, data, errors, recKey) {
|
||||
errors.push(`HTML found in ${file.path} at key ${recKey}.${key}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function checkHtml() {
|
||||
const checkHtml = () => {
|
||||
const errors = [];
|
||||
|
||||
return mapStream(function (file, cb) {
|
||||
@@ -44,9 +47,9 @@ function checkHtml() {
|
||||
}
|
||||
cb(error, file);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function convertBackendTranslations(data, _file) {
|
||||
const convertBackendTranslationsTransform = (data, _file) => {
|
||||
const output = { component: {} };
|
||||
if (!data.component) {
|
||||
return output;
|
||||
@@ -62,25 +65,22 @@ function convertBackendTranslations(data, _file) {
|
||||
});
|
||||
});
|
||||
return output;
|
||||
}
|
||||
};
|
||||
|
||||
gulp.task("convert-backend-translations", function () {
|
||||
return gulp
|
||||
.src([`${inDirBackend}/*.json`])
|
||||
.pipe(transform((data, file) => convertBackendTranslations(data, file)))
|
||||
.pipe(gulp.dest(inDirBackend));
|
||||
});
|
||||
const convertBackendTranslations = () =>
|
||||
src([`${inDirBackend}/*.json`])
|
||||
.pipe(
|
||||
transform((data, file) => convertBackendTranslationsTransform(data, file))
|
||||
)
|
||||
.pipe(dest(inDirBackend));
|
||||
|
||||
gulp.task("check-translations-html", function () {
|
||||
return gulp
|
||||
.src([`${inDirFrontend}/*.json`, `${inDirBackend}/*.json`])
|
||||
.pipe(checkHtml());
|
||||
});
|
||||
const checkTranslationsHtml = () =>
|
||||
src([`${inDirFrontend}/*.json`, `${inDirBackend}/*.json`]).pipe(checkHtml());
|
||||
|
||||
gulp.task("check-all-files-exist", async function () {
|
||||
const checkAllFilesExist = async () => {
|
||||
const file = await fs.readFile(srcMeta, { encoding });
|
||||
const meta = JSON.parse(file);
|
||||
const writings = [];
|
||||
const writings: Promise<void>[] = [];
|
||||
Object.keys(meta).forEach((lang) => {
|
||||
writings.push(
|
||||
fs.writeFile(`${inDirFrontend}/${lang}.json`, JSON.stringify({}), {
|
||||
@@ -92,14 +92,14 @@ gulp.task("check-all-files-exist", async function () {
|
||||
);
|
||||
});
|
||||
await Promise.allSettled(writings);
|
||||
});
|
||||
};
|
||||
|
||||
const lokaliseProjects = {
|
||||
backend: "130246255a974bd3b5e8a1.51616605",
|
||||
frontend: "3420425759f6d6d241f598.13594006",
|
||||
};
|
||||
|
||||
gulp.task("fetch-lokalise", async function () {
|
||||
const fetchLokalise = async () => {
|
||||
let apiKey;
|
||||
try {
|
||||
apiKey =
|
||||
@@ -168,14 +168,11 @@ gulp.task("fetch-lokalise", async function () {
|
||||
})
|
||||
)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
gulp.task(
|
||||
"download-translations",
|
||||
gulp.series(
|
||||
"fetch-lokalise",
|
||||
"convert-backend-translations",
|
||||
"check-translations-html",
|
||||
"check-all-files-exist"
|
||||
)
|
||||
export const downloadTranslations = series(
|
||||
fetchLokalise,
|
||||
convertBackendTranslations,
|
||||
checkTranslationsHtml,
|
||||
checkAllFilesExist
|
||||
);
|
||||
@@ -6,12 +6,11 @@ import {
|
||||
getPreUserAgentRegexes,
|
||||
} from "browserslist-useragent-regexp";
|
||||
import fs from "fs-extra";
|
||||
import gulp from "gulp";
|
||||
import { minify } from "html-minifier-terser";
|
||||
import template from "lodash.template";
|
||||
import { dirname, extname, resolve } from "node:path";
|
||||
import { htmlMinifierOptions, terserOptions } from "../bundle.cjs";
|
||||
import paths from "../paths.cjs";
|
||||
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
|
||||
@@ -34,9 +33,9 @@ const getCommonTemplateVars = () => {
|
||||
mobileToDesktop: true,
|
||||
throwOnMissing: true,
|
||||
});
|
||||
const minSafariVersion = browserRegexes.find(
|
||||
(regex) => regex.family === "safari"
|
||||
)?.matchedVersions[0][0];
|
||||
const minSafariVersion =
|
||||
browserRegexes.find((regex) => regex.family === "safari")
|
||||
?.matchedVersions[0][0] ?? 18;
|
||||
const minMacOSVersion = SAFARI_TO_MACOS[minSafariVersion];
|
||||
if (!minMacOSVersion) {
|
||||
throw Error(
|
||||
@@ -106,10 +105,10 @@ const genPagesDevTask =
|
||||
resolve(inputRoot, inputSub, `${page}.template`),
|
||||
{
|
||||
...commonVars,
|
||||
latestEntryJS: entries.map(
|
||||
latestEntryJS: (entries as string[]).map(
|
||||
(entry) => `${publicRoot}/frontend_latest/${entry}.js`
|
||||
),
|
||||
es5EntryJS: entries.map(
|
||||
es5EntryJS: (entries as string[]).map(
|
||||
(entry) => `${publicRoot}/frontend_es5/${entry}.js`
|
||||
),
|
||||
latestCustomPanelJS: `${publicRoot}/frontend_latest/custom-panel.js`,
|
||||
@@ -128,7 +127,7 @@ const genPagesProdTask =
|
||||
inputRoot,
|
||||
outputRoot,
|
||||
outputLatest,
|
||||
outputES5,
|
||||
outputES5?: string,
|
||||
inputSub = "src/html"
|
||||
) =>
|
||||
async () => {
|
||||
@@ -139,14 +138,18 @@ const genPagesProdTask =
|
||||
? fs.readJsonSync(resolve(outputES5, "manifest.json"))
|
||||
: {};
|
||||
const commonVars = getCommonTemplateVars();
|
||||
const minifiedHTML = [];
|
||||
const minifiedHTML: Promise<void>[] = [];
|
||||
for (const [page, entries] of Object.entries(pageEntries)) {
|
||||
const content = renderTemplate(
|
||||
resolve(inputRoot, inputSub, `${page}.template`),
|
||||
{
|
||||
...commonVars,
|
||||
latestEntryJS: entries.map((entry) => latestManifest[`${entry}.js`]),
|
||||
es5EntryJS: entries.map((entry) => es5Manifest[`${entry}.js`]),
|
||||
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"],
|
||||
}
|
||||
@@ -167,20 +170,18 @@ const APP_PAGE_ENTRIES = {
|
||||
"index.html": ["core", "app"],
|
||||
};
|
||||
|
||||
gulp.task(
|
||||
"gen-pages-app-dev",
|
||||
genPagesDevTask(APP_PAGE_ENTRIES, paths.root_dir, paths.app_output_root)
|
||||
export const genPagesAppDev = genPagesDevTask(
|
||||
APP_PAGE_ENTRIES,
|
||||
paths.root_dir,
|
||||
paths.app_output_root
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"gen-pages-app-prod",
|
||||
genPagesProdTask(
|
||||
APP_PAGE_ENTRIES,
|
||||
paths.root_dir,
|
||||
paths.app_output_root,
|
||||
paths.app_output_latest,
|
||||
paths.app_output_es5
|
||||
)
|
||||
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 = {
|
||||
@@ -190,79 +191,82 @@ const CAST_PAGE_ENTRIES = {
|
||||
"receiver.html": ["receiver"],
|
||||
};
|
||||
|
||||
gulp.task(
|
||||
"gen-pages-cast-dev",
|
||||
genPagesDevTask(CAST_PAGE_ENTRIES, paths.cast_dir, paths.cast_output_root)
|
||||
export const genPagesCastDev = genPagesDevTask(
|
||||
CAST_PAGE_ENTRIES,
|
||||
paths.cast_dir,
|
||||
paths.cast_output_root
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"gen-pages-cast-prod",
|
||||
genPagesProdTask(
|
||||
CAST_PAGE_ENTRIES,
|
||||
paths.cast_dir,
|
||||
paths.cast_output_root,
|
||||
paths.cast_output_latest,
|
||||
paths.cast_output_es5
|
||||
)
|
||||
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"] };
|
||||
|
||||
gulp.task(
|
||||
"gen-pages-demo-dev",
|
||||
genPagesDevTask(DEMO_PAGE_ENTRIES, paths.demo_dir, paths.demo_output_root)
|
||||
export const genPagesDemoDev = genPagesDevTask(
|
||||
DEMO_PAGE_ENTRIES,
|
||||
paths.demo_dir,
|
||||
paths.demo_output_root
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"gen-pages-demo-prod",
|
||||
genPagesProdTask(
|
||||
DEMO_PAGE_ENTRIES,
|
||||
paths.demo_dir,
|
||||
paths.demo_output_root,
|
||||
paths.demo_output_latest,
|
||||
paths.demo_output_es5
|
||||
)
|
||||
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"] };
|
||||
|
||||
gulp.task(
|
||||
"gen-pages-gallery-dev",
|
||||
genPagesDevTask(
|
||||
GALLERY_PAGE_ENTRIES,
|
||||
paths.gallery_dir,
|
||||
paths.gallery_output_root
|
||||
)
|
||||
export const genPagesGalleryDev = genPagesDevTask(
|
||||
GALLERY_PAGE_ENTRIES,
|
||||
paths.gallery_dir,
|
||||
paths.gallery_output_root
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"gen-pages-gallery-prod",
|
||||
genPagesProdTask(
|
||||
GALLERY_PAGE_ENTRIES,
|
||||
paths.gallery_dir,
|
||||
paths.gallery_output_root,
|
||||
paths.gallery_output_latest
|
||||
)
|
||||
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"] };
|
||||
|
||||
gulp.task(
|
||||
"gen-pages-landing-page-dev",
|
||||
genPagesDevTask(
|
||||
LANDING_PAGE_PAGE_ENTRIES,
|
||||
paths.landingPage_dir,
|
||||
paths.landingPage_output_root
|
||||
)
|
||||
export const genPagesLandingPageDev = genPagesDevTask(
|
||||
LANDING_PAGE_PAGE_ENTRIES,
|
||||
paths.landingPage_dir,
|
||||
paths.landingPage_output_root
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"gen-pages-landing-page-prod",
|
||||
genPagesProdTask(
|
||||
LANDING_PAGE_PAGE_ENTRIES,
|
||||
paths.landingPage_dir,
|
||||
paths.landingPage_output_root,
|
||||
paths.landingPage_output_latest,
|
||||
paths.landingPage_output_es5
|
||||
)
|
||||
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,14 +1,14 @@
|
||||
// Task to download the latest Lokalise translations from the nightly workflow artifacts
|
||||
// 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 { mkdir, readFile, writeFile } from "fs/promises";
|
||||
import gulp from "gulp";
|
||||
import { series } from "gulp";
|
||||
import jszip from "jszip";
|
||||
import path from "path";
|
||||
import process from "process";
|
||||
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
|
||||
@@ -22,12 +22,13 @@ const TOKEN_FILE = path.posix.join(EXTRACT_DIR, "token.json");
|
||||
const ARTIFACT_FILE = path.posix.join(EXTRACT_DIR, "artifact.json");
|
||||
|
||||
let allowTokenSetup = false;
|
||||
gulp.task("allow-setup-fetch-nightly-translations", (done) => {
|
||||
|
||||
export const allowSetupFetchNightlyTranslations = (done) => {
|
||||
allowTokenSetup = true;
|
||||
done();
|
||||
});
|
||||
};
|
||||
|
||||
gulp.task("fetch-nightly-translations", async function () {
|
||||
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");
|
||||
@@ -54,7 +55,7 @@ gulp.task("fetch-nightly-translations", async function () {
|
||||
|
||||
// To store file writing promises
|
||||
const createExtractDir = mkdir(EXTRACT_DIR, { recursive: true });
|
||||
const writings = [];
|
||||
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
|
||||
@@ -87,7 +88,7 @@ gulp.task("fetch-nightly-translations", async function () {
|
||||
});
|
||||
tokenAuth = await auth({ type: "oauth" });
|
||||
writings.push(
|
||||
createExtractDir.then(
|
||||
createExtractDir.then(() =>
|
||||
writeFile(TOKEN_FILE, JSON.stringify(tokenAuth, null, 2))
|
||||
)
|
||||
);
|
||||
@@ -131,13 +132,13 @@ gulp.task("fetch-nightly-translations", async function () {
|
||||
throw Error("Latest nightly workflow run has no translations artifact");
|
||||
}
|
||||
writings.push(
|
||||
createExtractDir.then(
|
||||
createExtractDir.then(() =>
|
||||
writeFile(ARTIFACT_FILE, JSON.stringify(latestArtifact, null, 2))
|
||||
)
|
||||
);
|
||||
|
||||
// Remove the current translations
|
||||
const deleteCurrent = Promise.all(writings).then(
|
||||
const deleteCurrent = Promise.all(writings).then(() =>
|
||||
deleteAsync([`${EXTRACT_DIR}/*`, `!${ARTIFACT_FILE}`, `!${TOKEN_FILE}`])
|
||||
);
|
||||
|
||||
@@ -148,24 +149,22 @@ gulp.task("fetch-nightly-translations", async function () {
|
||||
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);
|
||||
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);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
gulp.task(
|
||||
"setup-and-fetch-nightly-translations",
|
||||
gulp.series(
|
||||
"allow-setup-fetch-nightly-translations",
|
||||
"fetch-nightly-translations"
|
||||
)
|
||||
export const setupAndFetchNightlyTranslations = series(
|
||||
allowSetupFetchNightlyTranslations,
|
||||
fetchNightlyTranslations
|
||||
);
|
||||
@@ -1,19 +1,23 @@
|
||||
import fs from "fs";
|
||||
import { glob } from "glob";
|
||||
import gulp from "gulp";
|
||||
import { parallel, series, watch } from "gulp";
|
||||
import yaml from "js-yaml";
|
||||
import { marked } from "marked";
|
||||
import path from "path";
|
||||
import paths from "../paths.cjs";
|
||||
import "./clean.js";
|
||||
import "./entry-html.js";
|
||||
import "./gather-static.js";
|
||||
import "./gen-icons-json.js";
|
||||
import "./service-worker.js";
|
||||
import "./translations.js";
|
||||
import "./rspack.js";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import paths from "../paths.ts";
|
||||
import { cleanGallery } from "./clean.ts";
|
||||
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";
|
||||
|
||||
gulp.task("gather-gallery-pages", async function gatherPages() {
|
||||
// gather-gallery-pages
|
||||
export const gatherGalleryPages = async function gatherPages() {
|
||||
const pageDir = path.resolve(paths.gallery_dir, "src/pages");
|
||||
const files = await glob(path.resolve(pageDir, "**/*"));
|
||||
|
||||
@@ -22,7 +26,7 @@ gulp.task("gather-gallery-pages", async function gatherPages() {
|
||||
|
||||
let content = "export const PAGES = {\n";
|
||||
|
||||
const processed = new Set();
|
||||
const processed = new Set<string>();
|
||||
|
||||
for (const file of files) {
|
||||
if (fs.lstatSync(file).isDirectory()) {
|
||||
@@ -47,7 +51,9 @@ gulp.task("gather-gallery-pages", async function gatherPages() {
|
||||
|
||||
if (descriptionContent.startsWith("---")) {
|
||||
const metadataEnd = descriptionContent.indexOf("---", 3);
|
||||
metadata = yaml.load(descriptionContent.substring(3, metadataEnd));
|
||||
metadata = yaml.load(
|
||||
descriptionContent.substring(3, metadataEnd)
|
||||
) as any;
|
||||
descriptionContent = descriptionContent
|
||||
.substring(metadataEnd + 3)
|
||||
.trim();
|
||||
@@ -57,7 +63,9 @@ gulp.task("gather-gallery-pages", async function gatherPages() {
|
||||
if (descriptionContent === "") {
|
||||
hasDescription = false;
|
||||
} else {
|
||||
descriptionContent = marked(descriptionContent).replace(/`/g, "\\`");
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
descriptionContent = await marked(descriptionContent);
|
||||
descriptionContent = descriptionContent.replace(/`/g, "\\`");
|
||||
fs.mkdirSync(path.resolve(galleryBuild, category), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.resolve(galleryBuild, `${pageId}-description.ts`),
|
||||
@@ -95,7 +103,10 @@ gulp.task("gather-gallery-pages", async function gatherPages() {
|
||||
pagesToProcess[category].add(page);
|
||||
}
|
||||
|
||||
for (const group of Object.values(sidebar)) {
|
||||
for (const group of Object.values(sidebar) as {
|
||||
category: string;
|
||||
pages?: string[];
|
||||
}[]) {
|
||||
const toProcess = pagesToProcess[group.category];
|
||||
delete pagesToProcess[group.category];
|
||||
|
||||
@@ -118,7 +129,7 @@ gulp.task("gather-gallery-pages", async function gatherPages() {
|
||||
group.pages = [];
|
||||
}
|
||||
for (const page of Array.from(toProcess).sort()) {
|
||||
group.pages.push(page);
|
||||
group.pages.push(page as string);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,7 +137,7 @@ gulp.task("gather-gallery-pages", async function gatherPages() {
|
||||
sidebar.push({
|
||||
category,
|
||||
header: category,
|
||||
pages: Array.from(pages).sort(),
|
||||
pages: Array.from(pages as Set<string>).sort(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -137,55 +148,48 @@ gulp.task("gather-gallery-pages", async function gatherPages() {
|
||||
content,
|
||||
"utf-8"
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
gulp.task(
|
||||
"develop-gallery",
|
||||
gulp.series(
|
||||
async function setEnv() {
|
||||
process.env.NODE_ENV = "development";
|
||||
},
|
||||
"clean-gallery",
|
||||
"translations-enable-merge-backend",
|
||||
gulp.parallel(
|
||||
"gen-icons-json",
|
||||
"build-translations",
|
||||
"build-locale-data",
|
||||
"gather-gallery-pages"
|
||||
),
|
||||
"copy-static-gallery",
|
||||
"gen-pages-gallery-dev",
|
||||
gulp.parallel(
|
||||
"rspack-dev-server-gallery",
|
||||
async function watchMarkdownFiles() {
|
||||
gulp.watch(
|
||||
[
|
||||
path.resolve(paths.gallery_dir, "src/pages/**/*.markdown"),
|
||||
path.resolve(paths.gallery_dir, "sidebar.js"),
|
||||
],
|
||||
gulp.series("gather-gallery-pages")
|
||||
);
|
||||
}
|
||||
)
|
||||
)
|
||||
// develop-gallery
|
||||
export const developGallery = series(
|
||||
async function setEnv() {
|
||||
process.env.NODE_ENV = "development";
|
||||
},
|
||||
cleanGallery,
|
||||
translationsEnableMergeBackend,
|
||||
parallel(
|
||||
genIconsJson,
|
||||
buildTranslations,
|
||||
buildLocaleData,
|
||||
gatherGalleryPages
|
||||
),
|
||||
copyStaticGallery,
|
||||
genPagesGalleryDev,
|
||||
parallel(rspackDevServerGallery, async function watchMarkdownFiles() {
|
||||
watch(
|
||||
[
|
||||
path.resolve(paths.gallery_dir, "src/pages/**/*.markdown"),
|
||||
path.resolve(paths.gallery_dir, "sidebar.js"),
|
||||
],
|
||||
series(gatherGalleryPages)
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"build-gallery",
|
||||
gulp.series(
|
||||
async function setEnv() {
|
||||
process.env.NODE_ENV = "production";
|
||||
},
|
||||
"clean-gallery",
|
||||
"translations-enable-merge-backend",
|
||||
gulp.parallel(
|
||||
"gen-icons-json",
|
||||
"build-translations",
|
||||
"build-locale-data",
|
||||
"gather-gallery-pages"
|
||||
),
|
||||
"copy-static-gallery",
|
||||
"rspack-prod-gallery",
|
||||
"gen-pages-gallery-prod"
|
||||
)
|
||||
// build-gallery
|
||||
export const buildGallery = series(
|
||||
async function setEnv() {
|
||||
process.env.NODE_ENV = "production";
|
||||
},
|
||||
cleanGallery,
|
||||
translationsEnableMergeBackend,
|
||||
parallel(
|
||||
genIconsJson,
|
||||
buildTranslations,
|
||||
buildLocaleData,
|
||||
gatherGalleryPages
|
||||
),
|
||||
copyStaticGallery,
|
||||
rspackProdGallery,
|
||||
genPagesGalleryProd
|
||||
);
|
||||
@@ -1,9 +1,8 @@
|
||||
// Gulp task to gather all static files.
|
||||
|
||||
import fs from "fs-extra";
|
||||
import gulp from "gulp";
|
||||
import path from "path";
|
||||
import paths from "../paths.cjs";
|
||||
import path from "node:path";
|
||||
import paths from "../paths.ts";
|
||||
|
||||
const npmPath = (...parts) =>
|
||||
path.resolve(paths.root_dir, "node_modules", ...parts);
|
||||
@@ -17,7 +16,7 @@ const genStaticPath =
|
||||
(...parts) =>
|
||||
path.resolve(staticDir, ...parts);
|
||||
|
||||
function copyTranslations(staticDir) {
|
||||
const copyTranslations = (staticDir) => {
|
||||
const staticPath = genStaticPath(staticDir);
|
||||
|
||||
// Translation output
|
||||
@@ -25,23 +24,23 @@ function copyTranslations(staticDir) {
|
||||
polyPath("build/translations/output"),
|
||||
staticPath("translations")
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
function copyLocaleData(staticDir) {
|
||||
const copyLocaleData = (staticDir) => {
|
||||
const staticPath = genStaticPath(staticDir);
|
||||
|
||||
// Locale data output
|
||||
fs.copySync(polyPath("build/locale-data"), staticPath("locale-data"));
|
||||
}
|
||||
};
|
||||
|
||||
function copyMdiIcons(staticDir) {
|
||||
const copyMdiIcons = (staticDir) => {
|
||||
const staticPath = genStaticPath(staticDir);
|
||||
|
||||
// MDI icons output
|
||||
fs.copySync(polyPath("build/mdi"), staticPath("mdi"));
|
||||
}
|
||||
};
|
||||
|
||||
function copyPolyfills(staticDir) {
|
||||
const copyPolyfills = (staticDir) => {
|
||||
const staticPath = genStaticPath(staticDir);
|
||||
|
||||
// For custom panels using ES5 builds that don't use Babel 7+
|
||||
@@ -70,9 +69,9 @@ function copyPolyfills(staticDir) {
|
||||
npmPath("dialog-polyfill/dialog-polyfill.css"),
|
||||
staticPath("polyfills/")
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
function copyFonts(staticDir) {
|
||||
const copyFonts = (staticDir) => {
|
||||
const staticPath = genStaticPath(staticDir);
|
||||
// Local fonts
|
||||
fs.copySync(
|
||||
@@ -82,14 +81,14 @@ function copyFonts(staticDir) {
|
||||
filter: (src) => !src.includes(".") || src.endsWith(".woff2"),
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
function copyQrScannerWorker(staticDir) {
|
||||
const copyQrScannerWorker = (staticDir) => {
|
||||
const staticPath = genStaticPath(staticDir);
|
||||
copyFileDir(npmPath("qr-scanner/qr-scanner-worker.min.js"), staticPath("js"));
|
||||
}
|
||||
};
|
||||
|
||||
function copyMapPanel(staticDir) {
|
||||
const copyMapPanel = (staticDir) => {
|
||||
const staticPath = genStaticPath(staticDir);
|
||||
copyFileDir(
|
||||
npmPath("leaflet/dist/leaflet.css"),
|
||||
@@ -103,32 +102,38 @@ function copyMapPanel(staticDir) {
|
||||
npmPath("leaflet/dist/images"),
|
||||
staticPath("images/leaflet/images/")
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
function copyZXingWasm(staticDir) {
|
||||
const copyZXingWasm = (staticDir) => {
|
||||
const staticPath = genStaticPath(staticDir);
|
||||
copyFileDir(
|
||||
npmPath("zxing-wasm/dist/reader/zxing_reader.wasm"),
|
||||
staticPath("js")
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
gulp.task("copy-locale-data", async () => {
|
||||
const staticDir = paths.app_output_static;
|
||||
copyLocaleData(staticDir);
|
||||
});
|
||||
|
||||
gulp.task("copy-translations-app", async () => {
|
||||
export const copyTranslationsApp = async () => {
|
||||
const staticDir = paths.app_output_static;
|
||||
copyTranslations(staticDir);
|
||||
});
|
||||
};
|
||||
|
||||
gulp.task("copy-translations-landing-page", async () => {
|
||||
export const copyTranslationsSupervisor = async () => {
|
||||
const staticDir = paths.hassio_output_static;
|
||||
copyTranslations(staticDir);
|
||||
};
|
||||
|
||||
export const copyTranslationsLandingPage = async () => {
|
||||
const staticDir = paths.landingPage_output_static;
|
||||
copyTranslations(staticDir);
|
||||
});
|
||||
};
|
||||
|
||||
gulp.task("copy-static-app", async () => {
|
||||
export const copyStaticSupervisor = async () => {
|
||||
const staticDir = paths.hassio_output_static;
|
||||
copyLocaleData(staticDir);
|
||||
copyFonts(staticDir);
|
||||
};
|
||||
|
||||
export const copyStaticApp = async () => {
|
||||
const staticDir = paths.app_output_static;
|
||||
// Basic static files
|
||||
fs.copySync(polyPath("public"), paths.app_output_root);
|
||||
@@ -144,9 +149,9 @@ gulp.task("copy-static-app", async () => {
|
||||
// Qr Scanner assets
|
||||
copyZXingWasm(staticDir);
|
||||
copyQrScannerWorker(staticDir);
|
||||
});
|
||||
};
|
||||
|
||||
gulp.task("copy-static-demo", async () => {
|
||||
export const copyStaticDemo = async () => {
|
||||
// Copy app static files
|
||||
fs.copySync(
|
||||
polyPath("public/static"),
|
||||
@@ -160,9 +165,9 @@ gulp.task("copy-static-demo", async () => {
|
||||
copyTranslations(paths.demo_output_static);
|
||||
copyLocaleData(paths.demo_output_static);
|
||||
copyMdiIcons(paths.demo_output_static);
|
||||
});
|
||||
};
|
||||
|
||||
gulp.task("copy-static-cast", async () => {
|
||||
export const copyStaticCast = async () => {
|
||||
// Copy app static files
|
||||
fs.copySync(polyPath("public/static"), paths.cast_output_static);
|
||||
// Copy cast static files
|
||||
@@ -173,9 +178,9 @@ gulp.task("copy-static-cast", async () => {
|
||||
copyTranslations(paths.cast_output_static);
|
||||
copyLocaleData(paths.cast_output_static);
|
||||
copyMdiIcons(paths.cast_output_static);
|
||||
});
|
||||
};
|
||||
|
||||
gulp.task("copy-static-gallery", async () => {
|
||||
export const copyStaticGallery = async () => {
|
||||
// Copy app static files
|
||||
fs.copySync(polyPath("public/static"), paths.gallery_output_static);
|
||||
// Copy gallery static files
|
||||
@@ -189,9 +194,9 @@ gulp.task("copy-static-gallery", async () => {
|
||||
copyTranslations(paths.gallery_output_static);
|
||||
copyLocaleData(paths.gallery_output_static);
|
||||
copyMdiIcons(paths.gallery_output_static);
|
||||
});
|
||||
};
|
||||
|
||||
gulp.task("copy-static-landing-page", async () => {
|
||||
export const copyStaticLandingPage = async () => {
|
||||
// Copy landing-page static files
|
||||
fs.copySync(
|
||||
path.resolve(paths.landingPage_dir, "public"),
|
||||
@@ -200,4 +205,4 @@ gulp.task("copy-static-landing-page", async () => {
|
||||
|
||||
copyFonts(paths.landingPage_output_static);
|
||||
copyTranslations(paths.landingPage_output_static);
|
||||
});
|
||||
};
|
||||
@@ -1,8 +1,7 @@
|
||||
import fs from "fs";
|
||||
import gulp from "gulp";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import hash from "object-hash";
|
||||
import path from "path";
|
||||
import paths from "../paths.cjs";
|
||||
import paths from "../paths.ts";
|
||||
|
||||
const ICON_PACKAGE_PATH = path.resolve("node_modules/@mdi/svg/");
|
||||
const META_PATH = path.resolve(ICON_PACKAGE_PATH, "meta.json");
|
||||
@@ -21,7 +20,7 @@ const getMeta = () => {
|
||||
encoding,
|
||||
});
|
||||
return {
|
||||
path: svg.match(/ d="([^"]+)"/)[1],
|
||||
path: svg.match(/ d="([^"]+)"/)?.[1],
|
||||
name: icon.name,
|
||||
tags: icon.tags,
|
||||
aliases: icon.aliases,
|
||||
@@ -55,14 +54,14 @@ const orderMeta = (meta) => {
|
||||
};
|
||||
|
||||
const splitBySize = (meta) => {
|
||||
const chunks = [];
|
||||
const chunks: any[] = [];
|
||||
const CHUNK_SIZE = 50000;
|
||||
|
||||
let curSize = 0;
|
||||
let startKey;
|
||||
let icons = [];
|
||||
let icons: any[] = [];
|
||||
|
||||
Object.values(meta).forEach((icon) => {
|
||||
Object.values(meta).forEach((icon: any) => {
|
||||
if (startKey === undefined) {
|
||||
startKey = icon.name;
|
||||
}
|
||||
@@ -94,10 +93,10 @@ const findDifferentiator = (curString, prevString) => {
|
||||
return curString.substring(0, i + 1);
|
||||
}
|
||||
}
|
||||
throw new Error("Cannot find differentiator", curString, prevString);
|
||||
throw new Error(`Cannot find differentiator; ${curString}; ${prevString}`);
|
||||
};
|
||||
|
||||
gulp.task("gen-icons-json", (done) => {
|
||||
export const genIconsJson = (done) => {
|
||||
const meta = getMeta();
|
||||
|
||||
const metaAndRemoved = addRemovedMeta(meta);
|
||||
@@ -106,7 +105,7 @@ gulp.task("gen-icons-json", (done) => {
|
||||
if (!fs.existsSync(OUTPUT_DIR)) {
|
||||
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
||||
}
|
||||
const parts = [];
|
||||
const parts: any[] = [];
|
||||
|
||||
let lastEnd;
|
||||
split.forEach((chunk) => {
|
||||
@@ -153,13 +152,13 @@ gulp.task("gen-icons-json", (done) => {
|
||||
);
|
||||
|
||||
done();
|
||||
});
|
||||
};
|
||||
|
||||
gulp.task("gen-dummy-icons-json", (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.ts
Normal file
45
build-scripts/gulp/hassio.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
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,16 +0,0 @@
|
||||
import "./app.js";
|
||||
import "./cast.js";
|
||||
import "./clean.js";
|
||||
import "./compress.js";
|
||||
import "./demo.js";
|
||||
import "./download-translations.js";
|
||||
import "./entry-html.js";
|
||||
import "./fetch-nightly-translations.js";
|
||||
import "./gallery.js";
|
||||
import "./gather-static.js";
|
||||
import "./gen-icons-json.js";
|
||||
import "./landing-page.js";
|
||||
import "./locale-data.js";
|
||||
import "./rspack.js";
|
||||
import "./service-worker.js";
|
||||
import "./translations.js";
|
||||
42
build-scripts/gulp/index.ts
Normal file
42
build-scripts/gulp/index.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
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,41 +0,0 @@
|
||||
import gulp from "gulp";
|
||||
import "./clean.js";
|
||||
import "./compress.js";
|
||||
import "./entry-html.js";
|
||||
import "./gather-static.js";
|
||||
import "./gen-icons-json.js";
|
||||
import "./translations.js";
|
||||
import "./rspack.js";
|
||||
|
||||
gulp.task(
|
||||
"develop-landing-page",
|
||||
gulp.series(
|
||||
async function setEnv() {
|
||||
process.env.NODE_ENV = "development";
|
||||
},
|
||||
"clean-landing-page",
|
||||
"translations-enable-merge-backend",
|
||||
"build-landing-page-translations",
|
||||
"copy-translations-landing-page",
|
||||
"build-locale-data",
|
||||
"copy-static-landing-page",
|
||||
"gen-pages-landing-page-dev",
|
||||
"rspack-watch-landing-page"
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"build-landing-page",
|
||||
gulp.series(
|
||||
async function setEnv() {
|
||||
process.env.NODE_ENV = "production";
|
||||
},
|
||||
"clean-landing-page",
|
||||
"build-landing-page-translations",
|
||||
"copy-translations-landing-page",
|
||||
"build-locale-data",
|
||||
"copy-static-landing-page",
|
||||
"rspack-prod-landing-page",
|
||||
"gen-pages-landing-page-prod"
|
||||
)
|
||||
);
|
||||
46
build-scripts/gulp/landing-page.ts
Normal file
46
build-scripts/gulp/landing-page.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
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
|
||||
);
|
||||
@@ -1,8 +1,8 @@
|
||||
import { deleteSync } from "del";
|
||||
import { mkdir, readFile, writeFile } from "fs/promises";
|
||||
import gulp from "gulp";
|
||||
import { series } from "gulp";
|
||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { join, resolve } from "node:path";
|
||||
import paths from "../paths.cjs";
|
||||
import paths from "../paths.ts";
|
||||
|
||||
const formatjsDir = join(paths.root_dir, "node_modules", "@formatjs");
|
||||
const outDir = join(paths.build_dir, "locale-data");
|
||||
@@ -31,7 +31,7 @@ const convertToJSON = async (
|
||||
join(formatjsDir, pkg, subDir, `${language}.js`),
|
||||
"utf-8"
|
||||
);
|
||||
} catch (e) {
|
||||
} 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}`);
|
||||
@@ -54,16 +54,16 @@ const convertToJSON = async (
|
||||
await writeFile(join(outDir, `${pkg}/${lang}.json`), localeData);
|
||||
};
|
||||
|
||||
gulp.task("clean-locale-data", async () => deleteSync([outDir]));
|
||||
const cleanLocaleData = async () => deleteSync([outDir]);
|
||||
|
||||
gulp.task("create-locale-data", async () => {
|
||||
const createLocaleData = async () => {
|
||||
const translationMeta = JSON.parse(
|
||||
await readFile(
|
||||
resolve(paths.translations_src, "translationMetadata.json"),
|
||||
"utf-8"
|
||||
)
|
||||
);
|
||||
const conversions = [];
|
||||
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 });
|
||||
@@ -81,9 +81,6 @@ gulp.task("create-locale-data", async () => {
|
||||
)
|
||||
);
|
||||
await Promise.all(conversions);
|
||||
});
|
||||
};
|
||||
|
||||
gulp.task(
|
||||
"build-locale-data",
|
||||
gulp.series("clean-locale-data", "create-locale-data")
|
||||
);
|
||||
export const buildLocaleData = series(cleanLocaleData, createLocaleData);
|
||||
@@ -1,20 +1,31 @@
|
||||
// Tasks to run rspack.
|
||||
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import log from "fancy-log";
|
||||
import gulp from "gulp";
|
||||
import rspack from "@rspack/core";
|
||||
import { RspackDevServer } from "@rspack/dev-server";
|
||||
import env from "../env.cjs";
|
||||
import paths from "../paths.cjs";
|
||||
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.cjs";
|
||||
} 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 }),
|
||||
@@ -28,6 +39,14 @@ const isWsl =
|
||||
.toLocaleLowerCase()
|
||||
.includes("microsoft");
|
||||
|
||||
interface RunDevServer {
|
||||
compiler: any;
|
||||
contentBase: string;
|
||||
port: number;
|
||||
listenHost?: string;
|
||||
proxy?: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* compiler: import("@rspack/core").Compiler,
|
||||
@@ -40,12 +59,12 @@ const runDevServer = async ({
|
||||
compiler,
|
||||
contentBase,
|
||||
port,
|
||||
listenHost = undefined,
|
||||
proxy = undefined,
|
||||
}) => {
|
||||
listenHost,
|
||||
proxy,
|
||||
}: RunDevServer) => {
|
||||
if (listenHost === undefined) {
|
||||
// For dev container, we need to listen on all hosts
|
||||
listenHost = env.isDevContainer() ? "0.0.0.0" : "localhost";
|
||||
listenHost = isDevContainer() ? "0.0.0.0" : "localhost";
|
||||
}
|
||||
const server = new RspackDevServer(
|
||||
{
|
||||
@@ -67,7 +86,7 @@ const runDevServer = async ({
|
||||
log("[rspack-dev-server]", `Project is running at http://localhost:${port}`);
|
||||
};
|
||||
|
||||
const doneHandler = (done) => (err, stats) => {
|
||||
const doneHandler = (done?: (value?: unknown) => void) => (err, stats) => {
|
||||
if (err) {
|
||||
log.error(err.stack || err);
|
||||
if (err.details) {
|
||||
@@ -96,49 +115,46 @@ const prodBuild = (conf) =>
|
||||
);
|
||||
});
|
||||
|
||||
gulp.task("rspack-watch-app", () => {
|
||||
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());
|
||||
gulp.watch(
|
||||
watch(
|
||||
path.join(paths.translations_src, "en.json"),
|
||||
gulp.series("build-translations", "copy-translations-app")
|
||||
series(buildTranslations, copyTranslationsApp)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
gulp.task("rspack-prod-app", () =>
|
||||
export const rspackProdApp = () =>
|
||||
prodBuild(
|
||||
bothBuilds(createAppConfig, {
|
||||
isProdBuild: true,
|
||||
isStatsBuild: env.isStatsBuild(),
|
||||
isTestBuild: env.isTestBuild(),
|
||||
isStatsBuild: isStatsBuild(),
|
||||
isTestBuild: isTestBuild(),
|
||||
})
|
||||
)
|
||||
);
|
||||
);
|
||||
|
||||
gulp.task("rspack-dev-server-demo", () =>
|
||||
export const rspackDevServerDemo = () =>
|
||||
runDevServer({
|
||||
compiler: rspack(
|
||||
createDemoConfig({ isProdBuild: false, latestBuild: true })
|
||||
),
|
||||
contentBase: paths.demo_output_root,
|
||||
port: 8090,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
gulp.task("rspack-prod-demo", () =>
|
||||
export const rspackProdDemo = () =>
|
||||
prodBuild(
|
||||
bothBuilds(createDemoConfig, {
|
||||
isProdBuild: true,
|
||||
isStatsBuild: env.isStatsBuild(),
|
||||
isStatsBuild: isStatsBuild(),
|
||||
})
|
||||
)
|
||||
);
|
||||
);
|
||||
|
||||
gulp.task("rspack-dev-server-cast", () =>
|
||||
export const rspackDevServerCast = () =>
|
||||
runDevServer({
|
||||
compiler: rspack(
|
||||
createCastConfig({ isProdBuild: false, latestBuild: true })
|
||||
@@ -147,18 +163,40 @@ gulp.task("rspack-dev-server-cast", () =>
|
||||
port: 8080,
|
||||
// Accessible from the network, because that's how Cast hits it.
|
||||
listenHost: "0.0.0.0",
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
gulp.task("rspack-prod-cast", () =>
|
||||
export const rspackProdCast = () =>
|
||||
prodBuild(
|
||||
bothBuilds(createCastConfig, {
|
||||
isProdBuild: true,
|
||||
})
|
||||
)
|
||||
);
|
||||
);
|
||||
|
||||
gulp.task("rspack-dev-server-gallery", () =>
|
||||
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 })
|
||||
@@ -166,19 +204,17 @@ gulp.task("rspack-dev-server-gallery", () =>
|
||||
contentBase: paths.gallery_output_root,
|
||||
port: 8100,
|
||||
listenHost: "0.0.0.0",
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
gulp.task("rspack-prod-gallery", () =>
|
||||
export const rspackProdGallery = () =>
|
||||
prodBuild(
|
||||
createGalleryConfig({
|
||||
isProdBuild: true,
|
||||
latestBuild: true,
|
||||
})
|
||||
)
|
||||
);
|
||||
);
|
||||
|
||||
gulp.task("rspack-watch-landing-page", () => {
|
||||
export const rspackWatchLandingPage = () => {
|
||||
// This command will run forever because we don't close compiler
|
||||
rspack(
|
||||
process.env.ES5
|
||||
@@ -186,21 +222,17 @@ gulp.task("rspack-watch-landing-page", () => {
|
||||
: createLandingPageConfig({ isProdBuild: false, latestBuild: true })
|
||||
).watch({ poll: isWsl }, doneHandler());
|
||||
|
||||
gulp.watch(
|
||||
watch(
|
||||
path.join(paths.translations_src, "en.json"),
|
||||
gulp.series(
|
||||
"build-landing-page-translations",
|
||||
"copy-translations-landing-page"
|
||||
)
|
||||
series(buildLandingPageTranslations, copyTranslationsLandingPage)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
gulp.task("rspack-prod-landing-page", () =>
|
||||
export const rspackProdLandingPage = () =>
|
||||
prodBuild(
|
||||
bothBuilds(createLandingPageConfig, {
|
||||
isProdBuild: true,
|
||||
isStatsBuild: env.isStatsBuild(),
|
||||
isTestBuild: env.isTestBuild(),
|
||||
isStatsBuild: isStatsBuild(),
|
||||
isTestBuild: isTestBuild(),
|
||||
})
|
||||
)
|
||||
);
|
||||
);
|
||||
@@ -1,11 +1,10 @@
|
||||
// Generate service workers
|
||||
|
||||
import { deleteAsync } from "del";
|
||||
import gulp from "gulp";
|
||||
import { mkdir, readFile, symlink, writeFile } from "node:fs/promises";
|
||||
import { basename, join, relative } from "node:path";
|
||||
import { injectManifest } from "workbox-build";
|
||||
import paths from "../paths.cjs";
|
||||
import paths from "../paths.ts";
|
||||
|
||||
const SW_MAP = {
|
||||
[paths.app_output_latest]: "modern",
|
||||
@@ -23,7 +22,7 @@ self.addEventListener('install', (event) => {
|
||||
});
|
||||
`.trim() + "\n";
|
||||
|
||||
gulp.task("gen-service-worker-app-dev", async () => {
|
||||
export const genServiceWorkerAppDev = async () => {
|
||||
await mkdir(paths.app_output_root, { recursive: true });
|
||||
await Promise.all(
|
||||
Object.values(SW_MAP).map((build) =>
|
||||
@@ -32,9 +31,9 @@ gulp.task("gen-service-worker-app-dev", async () => {
|
||||
})
|
||||
)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
gulp.task("gen-service-worker-app-prod", () =>
|
||||
export const genServiceWorkerAppProd = () =>
|
||||
Promise.all(
|
||||
Object.entries(SW_MAP).map(async ([outPath, build]) => {
|
||||
const manifest = JSON.parse(
|
||||
@@ -83,5 +82,4 @@ gulp.task("gen-service-worker-app-prod", () =>
|
||||
await symlink(basename(swDest), swOld);
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
);
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { deleteAsync } from "del";
|
||||
import { glob } from "glob";
|
||||
import gulp from "gulp";
|
||||
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";
|
||||
@@ -10,9 +10,12 @@ 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 env from "../env.cjs";
|
||||
import paths from "../paths.cjs";
|
||||
import "./fetch-nightly-translations.js";
|
||||
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";
|
||||
@@ -23,18 +26,20 @@ const TEST_LOCALE = "en-x-test";
|
||||
|
||||
let mergeBackend = false;
|
||||
|
||||
gulp.task(
|
||||
"translations-enable-merge-backend",
|
||||
gulp.parallel(async () => {
|
||||
mergeBackend = true;
|
||||
}, "allow-setup-fetch-nightly-translations")
|
||||
);
|
||||
// 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 {
|
||||
constructor(func, reviver = null) {
|
||||
_func: any;
|
||||
|
||||
_reviver: any;
|
||||
|
||||
constructor(func, reviver: any = null) {
|
||||
super({ objectMode: true });
|
||||
this._func = func;
|
||||
this._reviver = reviver;
|
||||
@@ -56,9 +61,17 @@ class CustomJSON extends Transform {
|
||||
|
||||
// Transform stream to merge Vinyl JSON files (buffer mode only).
|
||||
class MergeJSON extends Transform {
|
||||
_objects = [];
|
||||
_objects: any[] = [];
|
||||
|
||||
constructor(stem, startObj = {}, reviver = null) {
|
||||
_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);
|
||||
@@ -111,11 +124,12 @@ const testReviver = (_key, value) =>
|
||||
const KEY_REFERENCE = /\[%key:([^%]+)%\]/;
|
||||
const lokaliseTransform = (data, path, original = data) => {
|
||||
const output = {};
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
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) => {
|
||||
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}`);
|
||||
@@ -132,18 +146,17 @@ const lokaliseTransform = (data, path, original = data) => {
|
||||
return output;
|
||||
};
|
||||
|
||||
gulp.task("clean-translations", () => deleteAsync([workDir]));
|
||||
export const cleanTranslations = () => deleteAsync([workDir]);
|
||||
|
||||
const makeWorkDir = () => mkdir(workDir, { recursive: true });
|
||||
|
||||
const createTestTranslation = () =>
|
||||
env.isProdBuild()
|
||||
isProdBuild()
|
||||
? Promise.resolve()
|
||||
: gulp
|
||||
.src(EN_SRC)
|
||||
: glupSrc(EN_SRC)
|
||||
.pipe(new CustomJSON(null, testReviver))
|
||||
.pipe(rename(`${TEST_LOCALE}.json`))
|
||||
.pipe(gulp.dest(workDir));
|
||||
.pipe(gulpDest(workDir));
|
||||
|
||||
/**
|
||||
* This task will build a master translation file, to be used as the base for
|
||||
@@ -155,13 +168,10 @@ const createTestTranslation = () =>
|
||||
* the Lokalise update to translations/en.json will not happen immediately.
|
||||
*/
|
||||
const createMasterTranslation = () =>
|
||||
gulp
|
||||
.src([EN_SRC, ...(mergeBackend ? [`${inBackendDir}/en.json`] : [])], {
|
||||
allowEmpty: true,
|
||||
})
|
||||
glupSrc([EN_SRC, ...(mergeBackend ? [`${inBackendDir}/en.json`] : [])])
|
||||
.pipe(new CustomJSON(lokaliseTransform))
|
||||
.pipe(new MergeJSON("en"))
|
||||
.pipe(gulp.dest(workDir));
|
||||
.pipe(gulpDest(workDir));
|
||||
|
||||
const FRAGMENTS = ["base"];
|
||||
|
||||
@@ -170,7 +180,9 @@ const setFragment = (fragment) => async () => {
|
||||
};
|
||||
|
||||
const panelFragment = (fragment) =>
|
||||
fragment !== "base" && fragment !== "landing-page";
|
||||
fragment !== "base" &&
|
||||
fragment !== "supervisor" &&
|
||||
fragment !== "landing-page";
|
||||
|
||||
const HASHES = new Map();
|
||||
|
||||
@@ -186,12 +198,12 @@ const createTranslations = async () => {
|
||||
// each locale, then fragmentizes and flattens the data for final output.
|
||||
const translationFiles = await glob([
|
||||
`${inFrontendDir}/!(en).json`,
|
||||
...(env.isProdBuild() ? [] : [`${workDir}/${TEST_LOCALE}.json`]),
|
||||
...(isProdBuild() ? [] : [`${workDir}/${TEST_LOCALE}.json`]),
|
||||
]);
|
||||
const hashStream = new Transform({
|
||||
objectMode: true,
|
||||
transform: async (file, _, callback) => {
|
||||
const hash = env.isProdBuild()
|
||||
const hash = isProdBuild()
|
||||
? createHash("md5").update(file.contents).digest("hex")
|
||||
: "dev";
|
||||
HASHES.set(file.stem, hash);
|
||||
@@ -205,15 +217,18 @@ const createTranslations = async () => {
|
||||
FRAGMENTS.map((fragment) => {
|
||||
switch (fragment) {
|
||||
case "base":
|
||||
// Remove the panels and landing-page to create the base translations
|
||||
// Remove the panels and supervisor to create the base translations
|
||||
return [
|
||||
flatten({
|
||||
...data,
|
||||
ui: { ...data.ui, panel: undefined },
|
||||
"landing-page": 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"]), ""];
|
||||
@@ -227,7 +242,7 @@ const createTranslations = async () => {
|
||||
})
|
||||
)
|
||||
)
|
||||
.pipe(gulp.dest(outDir));
|
||||
.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
|
||||
@@ -237,15 +252,15 @@ const createTranslations = async () => {
|
||||
// TODO: This is a naive interpretation of BCP47 that should be improved.
|
||||
// Will be OK for now as long as we don't have anything more complicated
|
||||
// than a base translation + region.
|
||||
const masterStream = gulp
|
||||
.src(`${workDir}/en.json`)
|
||||
.pipe(new PassThrough({ objectMode: true }));
|
||||
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 = [];
|
||||
const mergeFiles: string[] = [];
|
||||
for (let i = 1; i <= subtags.length; i++) {
|
||||
const lang = subtags.slice(0, i).join("-");
|
||||
if (lang === TEST_LOCALE) {
|
||||
@@ -257,9 +272,9 @@ const createTranslations = async () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
const mergeStream = gulp
|
||||
.src(mergeFiles, { allowEmpty: true })
|
||||
.pipe(new MergeJSON(locale, enMaster, emptyReviver));
|
||||
const mergeStream = glupSrc(mergeFiles, { allowEmpty: true }).pipe(
|
||||
new MergeJSON(locale, enMaster, emptyReviver)
|
||||
);
|
||||
mergesFinished.push(finished(mergeStream));
|
||||
mergeStream.pipe(hashStream, { end: false });
|
||||
}
|
||||
@@ -272,12 +287,11 @@ const createTranslations = async () => {
|
||||
};
|
||||
|
||||
const writeTranslationMetaData = () =>
|
||||
gulp
|
||||
.src([`${paths.translations_src}/translationMetadata.json`])
|
||||
glupSrc([`${paths.translations_src}/translationMetadata.json`])
|
||||
.pipe(
|
||||
new CustomJSON((meta) => {
|
||||
// Add the test translation in development.
|
||||
if (!env.isProdBuild()) {
|
||||
if (!isProdBuild()) {
|
||||
meta[TEST_LOCALE] = { nativeName: "Translation Test" };
|
||||
}
|
||||
// Filter out locales without a native name, and add the hashes.
|
||||
@@ -297,23 +311,22 @@ const writeTranslationMetaData = () =>
|
||||
};
|
||||
})
|
||||
)
|
||||
.pipe(gulp.dest(workDir));
|
||||
.pipe(gulpDest(workDir));
|
||||
|
||||
gulp.task(
|
||||
"build-translations",
|
||||
gulp.series(
|
||||
gulp.parallel(
|
||||
"fetch-nightly-translations",
|
||||
gulp.series("clean-translations", makeWorkDir)
|
||||
),
|
||||
createTestTranslation,
|
||||
createMasterTranslation,
|
||||
createTranslations,
|
||||
writeTranslationMetaData
|
||||
)
|
||||
export const buildTranslations = series(
|
||||
parallel(fetchNightlyTranslations, series(cleanTranslations, makeWorkDir)),
|
||||
createTestTranslation,
|
||||
createMasterTranslation,
|
||||
createTranslations,
|
||||
writeTranslationMetaData
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"build-landing-page-translations",
|
||||
gulp.series(setFragment("landing-page"), "build-translations")
|
||||
export const buildSupervisorTranslations = series(
|
||||
setFragment("supervisor"),
|
||||
buildTranslations
|
||||
);
|
||||
|
||||
export const buildLandingPageTranslations = series(
|
||||
setFragment("landing-page"),
|
||||
buildTranslations
|
||||
);
|
||||
@@ -5,10 +5,11 @@ import { version as babelVersion } from "@babel/core";
|
||||
import presetEnv from "@babel/preset-env";
|
||||
import compilationTargets from "@babel/helper-compilation-targets";
|
||||
import coreJSCompat from "core-js-compat";
|
||||
|
||||
import { logPlugin } from "@babel/preset-env/lib/debug.js";
|
||||
// eslint-disable-next-line import/no-relative-packages
|
||||
import shippedPolyfills from "../node_modules/babel-plugin-polyfill-corejs3/lib/shipped-proposals.js";
|
||||
import { babelOptions } from "./bundle.cjs";
|
||||
import { babelOptions } from "./bundle.ts";
|
||||
|
||||
const detailsOpen = (heading) =>
|
||||
`<details>\n<summary><h4>${heading}</h4></summary>\n`;
|
||||
@@ -50,6 +51,12 @@ for (const buildType of ["Modern", "Legacy"]) {
|
||||
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, {
|
||||
@@ -1,53 +0,0 @@
|
||||
const path = require("path");
|
||||
|
||||
module.exports = {
|
||||
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"
|
||||
),
|
||||
|
||||
translations_src: path.resolve(__dirname, "../src/translations"),
|
||||
};
|
||||
63
build-scripts/paths.ts
Normal file
63
build-scripts/paths.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
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"),
|
||||
};
|
||||
@@ -1,20 +1,25 @@
|
||||
const { existsSync } = require("fs");
|
||||
const path = require("path");
|
||||
const rspack = require("@rspack/core");
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const { RsdoctorRspackPlugin } = require("@rsdoctor/rspack-plugin");
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const { StatsWriterPlugin } = require("webpack-stats-plugin");
|
||||
const filterStats = require("@bundle-stats/plugin-webpack-filter");
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const TerserPlugin = require("terser-webpack-plugin");
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const { WebpackManifestPlugin } = require("rspack-manifest-plugin");
|
||||
const log = require("fancy-log");
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const WebpackBar = require("webpackbar/rspack");
|
||||
const paths = require("./paths.cjs");
|
||||
const bundle = require("./bundle.cjs");
|
||||
import filterStats from "@bundle-stats/plugin-webpack-filter";
|
||||
import { RsdoctorRspackPlugin } from "@rsdoctor/rspack-plugin";
|
||||
import { DefinePlugin, NormalModuleReplacementPlugin } from "@rspack/core";
|
||||
import { defineConfig } from "@rspack/cli";
|
||||
import log from "fancy-log";
|
||||
import { existsSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { WebpackManifestPlugin } from "rspack-manifest-plugin";
|
||||
import TerserPlugin from "terser-webpack-plugin";
|
||||
import { StatsWriterPlugin } from "webpack-stats-plugin";
|
||||
// @ts-ignore
|
||||
import WebpackBar from "webpackbar/rspack";
|
||||
import {
|
||||
babelOptions,
|
||||
config,
|
||||
definedVars,
|
||||
emptyPackages,
|
||||
sourceMapURL,
|
||||
swcOptions,
|
||||
terserOptions,
|
||||
} from "./bundle.ts";
|
||||
import paths from "./paths.ts";
|
||||
|
||||
class LogStartCompilePlugin {
|
||||
ignoredFirst = false;
|
||||
@@ -30,7 +35,7 @@ class LogStartCompilePlugin {
|
||||
}
|
||||
}
|
||||
|
||||
const createRspackConfig = ({
|
||||
export const createRspackConfig = ({
|
||||
name,
|
||||
entry,
|
||||
outputPath,
|
||||
@@ -40,14 +45,25 @@ const createRspackConfig = ({
|
||||
latestBuild,
|
||||
isStatsBuild,
|
||||
isTestBuild,
|
||||
isLandingPageBuild,
|
||||
isHassioBuild,
|
||||
dontHash,
|
||||
}: {
|
||||
name: string;
|
||||
entry: any;
|
||||
outputPath: string;
|
||||
publicPath: string;
|
||||
defineOverlay?: Record<string, any>;
|
||||
isProdBuild?: boolean;
|
||||
latestBuild?: boolean;
|
||||
isStatsBuild?: boolean;
|
||||
isTestBuild?: boolean;
|
||||
isHassioBuild?: boolean;
|
||||
dontHash?: Set<string>;
|
||||
}) => {
|
||||
if (!dontHash) {
|
||||
dontHash = new Set();
|
||||
}
|
||||
const ignorePackages = bundle.ignorePackages({ latestBuild });
|
||||
return {
|
||||
return defineConfig({
|
||||
name,
|
||||
mode: isProdBuild ? "production" : "development",
|
||||
target: `browserslist:${latestBuild ? "modern" : "legacy"}`,
|
||||
@@ -70,7 +86,7 @@ const createRspackConfig = ({
|
||||
{
|
||||
loader: "babel-loader",
|
||||
options: {
|
||||
...bundle.babelOptions({
|
||||
...babelOptions({
|
||||
latestBuild,
|
||||
isProdBuild,
|
||||
isTestBuild,
|
||||
@@ -82,7 +98,7 @@ const createRspackConfig = ({
|
||||
},
|
||||
{
|
||||
loader: "builtin:swc-loader",
|
||||
options: bundle.swcOptions(),
|
||||
options: swcOptions(),
|
||||
},
|
||||
],
|
||||
resolve: {
|
||||
@@ -103,7 +119,7 @@ const createRspackConfig = ({
|
||||
new TerserPlugin({
|
||||
parallel: true,
|
||||
extractComments: true,
|
||||
terserOptions: bundle.terserOptions({ latestBuild, isTestBuild }),
|
||||
terserOptions: terserOptions({ latestBuild, isTestBuild }),
|
||||
}),
|
||||
],
|
||||
moduleIds: isProdBuild && !isStatsBuild ? "deterministic" : "named",
|
||||
@@ -122,7 +138,7 @@ const createRspackConfig = ({
|
||||
!chunk.canBeInitial() &&
|
||||
!new RegExp(
|
||||
`^.+-work${latestBuild ? "(?:let|er)" : "let"}$`
|
||||
).test(chunk.name),
|
||||
).test(chunk?.name || ""),
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
@@ -131,48 +147,13 @@ const createRspackConfig = ({
|
||||
// Only include the JS of entrypoints
|
||||
filter: (file) => file.isInitial && !file.name.endsWith(".map"),
|
||||
}),
|
||||
new rspack.DefinePlugin(
|
||||
bundle.definedVars({ isProdBuild, latestBuild, defineOverlay })
|
||||
new DefinePlugin(
|
||||
definedVars({ isProdBuild, latestBuild, defineOverlay })
|
||||
),
|
||||
new NormalModuleReplacementPlugin(
|
||||
new RegExp(emptyPackages({ isHassioBuild }).join("|")),
|
||||
path.resolve(paths.root_dir, "src/util/empty.js")
|
||||
),
|
||||
new rspack.IgnorePlugin({
|
||||
checkResource(resource, context) {
|
||||
// Only use ignore to intercept imports that we don't control
|
||||
// inside node_module dependencies.
|
||||
if (
|
||||
!context.includes("/node_modules/") ||
|
||||
// calling define.amd will call require("!!webpack amd options")
|
||||
resource.startsWith("!!webpack") ||
|
||||
// loaded by webpack dev server but doesn't exist.
|
||||
resource === "webpack/hot" ||
|
||||
resource.startsWith("@swc/helpers")
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
let fullPath;
|
||||
try {
|
||||
fullPath = resource.startsWith(".")
|
||||
? path.resolve(context, resource)
|
||||
: require.resolve(resource);
|
||||
} catch (err) {
|
||||
console.error(
|
||||
"Error in Home Assistant ignore plugin",
|
||||
resource,
|
||||
context
|
||||
);
|
||||
throw err;
|
||||
}
|
||||
|
||||
return ignorePackages.some((toIgnorePath) =>
|
||||
fullPath.startsWith(toIgnorePath)
|
||||
);
|
||||
},
|
||||
}),
|
||||
bundle.emptyPackages({ isLandingPageBuild }).length
|
||||
? new rspack.NormalModuleReplacementPlugin(
|
||||
new RegExp(bundle.emptyPackages({ isLandingPageBuild }).join("|")),
|
||||
path.resolve(paths.root_dir, "src/util/empty.js")
|
||||
)
|
||||
: false,
|
||||
!isProdBuild && new LogStartCompilePlugin(),
|
||||
isProdBuild &&
|
||||
new StatsWriterPlugin({
|
||||
@@ -186,7 +167,9 @@ const createRspackConfig = ({
|
||||
isProdBuild &&
|
||||
isStatsBuild &&
|
||||
new RsdoctorRspackPlugin({
|
||||
reportDir: path.join(paths.build_dir, "rsdoctor"),
|
||||
output: {
|
||||
reportDir: path.join(paths.build_dir, "rsdoctor"),
|
||||
},
|
||||
features: ["plugins", "bundle"],
|
||||
supports: {
|
||||
generateTileGraph: true,
|
||||
@@ -200,7 +183,6 @@ const createRspackConfig = ({
|
||||
"lit/decorators$": "lit/decorators.js",
|
||||
"lit/directive$": "lit/directive.js",
|
||||
"lit/directives/until$": "lit/directives/until.js",
|
||||
"lit/directives/ref$": "lit/directives/ref.js",
|
||||
"lit/directives/class-map$": "lit/directives/class-map.js",
|
||||
"lit/directives/style-map$": "lit/directives/style-map.js",
|
||||
"lit/directives/if-defined$": "lit/directives/if-defined.js",
|
||||
@@ -209,9 +191,7 @@ const createRspackConfig = ({
|
||||
"lit/directives/join$": "lit/directives/join.js",
|
||||
"lit/directives/repeat$": "lit/directives/repeat.js",
|
||||
"lit/directives/live$": "lit/directives/live.js",
|
||||
"lit/directives/keyed$": latestBuild
|
||||
? "lit/directives/keyed.js"
|
||||
: path.resolve(__dirname, "../src/common/lit/keyed-es5.ts"),
|
||||
"lit/directives/keyed$": "lit/directives/keyed.js",
|
||||
"lit/polyfill-support$": "lit/polyfill-support.js",
|
||||
"@lit-labs/virtualizer/layouts/grid":
|
||||
"@lit-labs/virtualizer/layouts/grid.js",
|
||||
@@ -219,48 +199,14 @@ const createRspackConfig = ({
|
||||
"@lit-labs/virtualizer/polyfills/resize-observer-polyfill/ResizeObserver.js",
|
||||
"@lit-labs/observers/resize-controller":
|
||||
"@lit-labs/observers/resize-controller.js",
|
||||
"@formatjs/intl-durationformat/should-polyfill$":
|
||||
"@formatjs/intl-durationformat/should-polyfill.js",
|
||||
"@formatjs/intl-durationformat/polyfill-force$":
|
||||
"@formatjs/intl-durationformat/polyfill-force.js",
|
||||
"@formatjs/intl-datetimeformat/should-polyfill":
|
||||
"@formatjs/intl-datetimeformat/should-polyfill.js",
|
||||
"@formatjs/intl-datetimeformat/polyfill-force":
|
||||
"@formatjs/intl-datetimeformat/polyfill-force.js",
|
||||
"@formatjs/intl-displaynames/should-polyfill":
|
||||
"@formatjs/intl-displaynames/should-polyfill.js",
|
||||
"@formatjs/intl-displaynames/polyfill-force":
|
||||
"@formatjs/intl-displaynames/polyfill-force.js",
|
||||
"@formatjs/intl-getcanonicallocales/should-polyfill":
|
||||
"@formatjs/intl-getcanonicallocales/should-polyfill.js",
|
||||
"@formatjs/intl-getcanonicallocales/polyfill-force":
|
||||
"@formatjs/intl-getcanonicallocales/polyfill-force.js",
|
||||
"@formatjs/intl-listformat/should-polyfill":
|
||||
"@formatjs/intl-listformat/should-polyfill.js",
|
||||
"@formatjs/intl-listformat/polyfill-force":
|
||||
"@formatjs/intl-listformat/polyfill-force.js",
|
||||
"@formatjs/intl-locale/should-polyfill":
|
||||
"@formatjs/intl-locale/should-polyfill.js",
|
||||
"@formatjs/intl-locale/polyfill-force":
|
||||
"@formatjs/intl-locale/polyfill-force.js",
|
||||
"@formatjs/intl-numberformat/should-polyfill":
|
||||
"@formatjs/intl-numberformat/should-polyfill.js",
|
||||
"@formatjs/intl-numberformat/polyfill-force":
|
||||
"@formatjs/intl-numberformat/polyfill-force.js",
|
||||
"@formatjs/intl-pluralrules/should-polyfill":
|
||||
"@formatjs/intl-pluralrules/should-polyfill.js",
|
||||
"@formatjs/intl-pluralrules/polyfill-force":
|
||||
"@formatjs/intl-pluralrules/polyfill-force.js",
|
||||
"@formatjs/intl-relativetimeformat/should-polyfill":
|
||||
"@formatjs/intl-relativetimeformat/should-polyfill.js",
|
||||
"@formatjs/intl-relativetimeformat/polyfill-force":
|
||||
"@formatjs/intl-relativetimeformat/polyfill-force.js",
|
||||
},
|
||||
},
|
||||
output: {
|
||||
module: latestBuild,
|
||||
filename: ({ chunk }) =>
|
||||
!isProdBuild || isStatsBuild || dontHash.has(chunk.name)
|
||||
!isProdBuild ||
|
||||
isStatsBuild ||
|
||||
(chunk?.name && dontHash.has(chunk.name))
|
||||
? "[name].js"
|
||||
: "[name].[contenthash].js",
|
||||
chunkFilename:
|
||||
@@ -291,47 +237,70 @@ const createRspackConfig = ({
|
||||
// dev tools, and they stay happy getting 404s with valid requests.
|
||||
return `/unknown${path.resolve("/", info.resourcePath)}`;
|
||||
}
|
||||
return new URL(info.resourcePath, bundle.sourceMapURL()).href;
|
||||
return new URL(info.resourcePath, sourceMapURL()).href;
|
||||
}
|
||||
: undefined,
|
||||
])
|
||||
),
|
||||
},
|
||||
experiments: {
|
||||
layers: true,
|
||||
outputModule: true,
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const createAppConfig = ({
|
||||
export const createAppConfig = ({
|
||||
isProdBuild,
|
||||
latestBuild,
|
||||
isStatsBuild,
|
||||
isTestBuild,
|
||||
}: {
|
||||
isProdBuild?: boolean;
|
||||
latestBuild?: boolean;
|
||||
isStatsBuild?: boolean;
|
||||
isTestBuild?: boolean;
|
||||
}) =>
|
||||
createRspackConfig(
|
||||
bundle.config.app({ isProdBuild, latestBuild, isStatsBuild, isTestBuild })
|
||||
config.app({ isProdBuild, latestBuild, isStatsBuild, isTestBuild })
|
||||
);
|
||||
|
||||
const createDemoConfig = ({ isProdBuild, latestBuild, isStatsBuild }) =>
|
||||
export const createDemoConfig = ({
|
||||
isProdBuild,
|
||||
latestBuild,
|
||||
isStatsBuild,
|
||||
}: {
|
||||
isProdBuild?: boolean;
|
||||
latestBuild?: boolean;
|
||||
isStatsBuild?: boolean;
|
||||
}) =>
|
||||
createRspackConfig(config.demo({ isProdBuild, latestBuild, isStatsBuild }));
|
||||
|
||||
export const createCastConfig = ({ isProdBuild, latestBuild }) =>
|
||||
createRspackConfig(config.cast({ isProdBuild, latestBuild }));
|
||||
|
||||
export const createHassioConfig = ({
|
||||
isProdBuild,
|
||||
latestBuild,
|
||||
isStatsBuild,
|
||||
isTestBuild,
|
||||
}: {
|
||||
isProdBuild?: boolean;
|
||||
latestBuild?: boolean;
|
||||
isStatsBuild?: boolean;
|
||||
isTestBuild?: boolean;
|
||||
}) =>
|
||||
createRspackConfig(
|
||||
bundle.config.demo({ isProdBuild, latestBuild, isStatsBuild })
|
||||
config.hassio({
|
||||
isProdBuild,
|
||||
latestBuild,
|
||||
isStatsBuild,
|
||||
isTestBuild,
|
||||
})
|
||||
);
|
||||
|
||||
const createCastConfig = ({ isProdBuild, latestBuild }) =>
|
||||
createRspackConfig(bundle.config.cast({ isProdBuild, latestBuild }));
|
||||
export const createGalleryConfig = ({ isProdBuild, latestBuild }) =>
|
||||
createRspackConfig(config.gallery({ isProdBuild, latestBuild }));
|
||||
|
||||
const createGalleryConfig = ({ isProdBuild, latestBuild }) =>
|
||||
createRspackConfig(bundle.config.gallery({ isProdBuild, latestBuild }));
|
||||
|
||||
const createLandingPageConfig = ({ isProdBuild, latestBuild }) =>
|
||||
createRspackConfig(bundle.config.landingPage({ isProdBuild, latestBuild }));
|
||||
|
||||
module.exports = {
|
||||
createAppConfig,
|
||||
createDemoConfig,
|
||||
createCastConfig,
|
||||
createGalleryConfig,
|
||||
createRspackConfig,
|
||||
createLandingPageConfig,
|
||||
};
|
||||
export const createLandingPageConfig = ({ isProdBuild, latestBuild }) =>
|
||||
createRspackConfig(config.landingPage({ isProdBuild, latestBuild }));
|
||||
42
build-scripts/runTask.ts
Normal file
42
build-scripts/runTask.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
// run-build.ts
|
||||
import { series } from "gulp";
|
||||
import { availableParallelism } from "node:os";
|
||||
import tasks from "./gulp/index.ts";
|
||||
|
||||
process.env.UV_THREADPOOL_SIZE = availableParallelism().toString();
|
||||
|
||||
const runGulpTask = async (runTasks: string[]) => {
|
||||
try {
|
||||
for (const taskName of runTasks) {
|
||||
if (tasks[taskName] === undefined) {
|
||||
console.error(`Gulp task "${taskName}" does not exist.`);
|
||||
console.log("Available tasks:");
|
||||
Object.keys(tasks).forEach((task) => {
|
||||
console.log(` - ${task}`);
|
||||
});
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
series(...runTasks.map((taskName) => tasks[taskName]))((err?: Error) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
process.exit(0);
|
||||
} catch (error: any) {
|
||||
console.error(`Error running Gulp task "${runTasks}":`, error);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
// Get the task name from command line arguments
|
||||
// TODO arg validation
|
||||
const tasksToRun = process.argv.slice(2);
|
||||
|
||||
runGulpTask(tasksToRun);
|
||||
@@ -14,5 +14,5 @@
|
||||
"name": "Home Assistant Cast",
|
||||
"short_name": "HA Cast",
|
||||
"start_url": "/?homescreen=1",
|
||||
"theme_color": "#009ac7"
|
||||
"theme_color": "#03A9F4"
|
||||
}
|
||||
|
||||
@@ -6,4 +6,4 @@ set -e
|
||||
|
||||
cd "$(dirname "$0")/../.."
|
||||
|
||||
./node_modules/.bin/gulp build-cast
|
||||
yarn run-task build-cast
|
||||
|
||||
@@ -6,4 +6,4 @@ set -e
|
||||
|
||||
cd "$(dirname "$0")/../.."
|
||||
|
||||
./node_modules/.bin/gulp develop-cast
|
||||
yarn run-task develop-cast
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
|
||||
import type { ActionDetail } from "@material/mwc-list/mwc-list";
|
||||
import { mdiCast, mdiCastConnected, mdiViewDashboard } from "@mdi/js";
|
||||
import type { Auth, Connection } from "home-assistant-js-websocket";
|
||||
@@ -16,7 +18,6 @@ import {
|
||||
} from "../../../../src/common/auth/token_storage";
|
||||
import { atLeastVersion } from "../../../../src/common/config/version";
|
||||
import { toggleAttribute } from "../../../../src/common/dom/toggle_attribute";
|
||||
import "../../../../src/components/ha-button";
|
||||
import "../../../../src/components/ha-icon";
|
||||
import "../../../../src/components/ha-list";
|
||||
import "../../../../src/components/ha-list-item";
|
||||
@@ -28,6 +29,7 @@ import {
|
||||
import { isStrategyDashboard } from "../../../../src/data/lovelace/config/types";
|
||||
import type { LovelaceViewConfig } from "../../../../src/data/lovelace/config/view";
|
||||
import "../../../../src/layouts/hass-loading-screen";
|
||||
import { generateDefaultViewConfig } from "../../../../src/panels/lovelace/common/generate-lovelace-config";
|
||||
import "./hc-layout";
|
||||
|
||||
@customElement("hc-cast")
|
||||
@@ -61,20 +63,12 @@ class HcCast extends LitElement {
|
||||
<p class="question action-item">
|
||||
Stay logged in?
|
||||
<span>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
size="small"
|
||||
@click=${this._handleSaveTokens}
|
||||
>
|
||||
<mwc-button @click=${this._handleSaveTokens}>
|
||||
YES
|
||||
</ha-button>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
size="small"
|
||||
@click=${this._handleSkipSaveTokens}
|
||||
>
|
||||
</mwc-button>
|
||||
<mwc-button @click=${this._handleSkipSaveTokens}>
|
||||
NO
|
||||
</ha-button>
|
||||
</mwc-button>
|
||||
</span>
|
||||
</p>
|
||||
`
|
||||
@@ -84,10 +78,10 @@ class HcCast extends LitElement {
|
||||
: !this.castManager.status
|
||||
? html`
|
||||
<p class="center-item">
|
||||
<ha-button @click=${this._handleLaunch}>
|
||||
<ha-svg-icon slot="start" .path=${mdiCast}></ha-svg-icon>
|
||||
<mwc-button raised @click=${this._handleLaunch}>
|
||||
<ha-svg-icon .path=${mdiCast}></ha-svg-icon>
|
||||
Start Casting
|
||||
</ha-button>
|
||||
</mwc-button>
|
||||
</p>
|
||||
`
|
||||
: html`
|
||||
@@ -95,9 +89,7 @@ class HcCast extends LitElement {
|
||||
<ha-list @action=${this._handlePickView} activatable>
|
||||
${(
|
||||
this.lovelaceViews ?? [
|
||||
{
|
||||
title: "Home",
|
||||
},
|
||||
generateDefaultViewConfig({}, {}, {}, {}, () => ""),
|
||||
]
|
||||
).map(
|
||||
(view, idx) => html`
|
||||
@@ -129,22 +121,14 @@ class HcCast extends LitElement {
|
||||
<div class="card-actions">
|
||||
${this.castManager.status
|
||||
? html`
|
||||
<ha-button appearance="plain" @click=${this._handleLaunch}>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiCastConnected}
|
||||
></ha-svg-icon>
|
||||
<mwc-button @click=${this._handleLaunch}>
|
||||
<ha-svg-icon .path=${mdiCastConnected}></ha-svg-icon>
|
||||
Manage
|
||||
</ha-button>
|
||||
</mwc-button>
|
||||
`
|
||||
: ""}
|
||||
<div class="spacer"></div>
|
||||
<ha-button
|
||||
variant="danger"
|
||||
appearance="plain"
|
||||
@click=${this._handleLogout}
|
||||
>Log out</ha-button
|
||||
>
|
||||
<mwc-button @click=${this._handleLogout}>Log out</mwc-button>
|
||||
</div>
|
||||
</hc-layout>
|
||||
`;
|
||||
@@ -206,7 +190,7 @@ class HcCast extends LitElement {
|
||||
}
|
||||
|
||||
private async _handlePickView(ev: CustomEvent<ActionDetail>) {
|
||||
const path = this.lovelaceViews?.[ev.detail.index]?.path ?? ev.detail.index;
|
||||
const path = this.lovelaceViews![ev.detail.index].path ?? ev.detail.index;
|
||||
await ensureConnectedCastSession(this.castManager!, this.auth!);
|
||||
castSendShowLovelaceView(this.castManager, this.auth.data.hassUrl, path);
|
||||
}
|
||||
@@ -243,7 +227,7 @@ class HcCast extends LitElement {
|
||||
}
|
||||
|
||||
.question:before {
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
border-radius: 4px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
@@ -261,6 +245,13 @@ class HcCast extends LitElement {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
mwc-button ha-svg-icon {
|
||||
margin-right: 8px;
|
||||
margin-inline-end: 8px;
|
||||
margin-inline-start: initial;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
ha-list-item ha-icon,
|
||||
ha-list-item ha-svg-icon {
|
||||
padding: 12px;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import "@material/mwc-button";
|
||||
import { mdiCastConnected, mdiCast } from "@mdi/js";
|
||||
import type {
|
||||
Auth,
|
||||
@@ -27,7 +28,6 @@ import "../../../../src/layouts/hass-loading-screen";
|
||||
import { registerServiceWorker } from "../../../../src/util/register-service-worker";
|
||||
import "./hc-layout";
|
||||
import "../../../../src/components/ha-textfield";
|
||||
import "../../../../src/components/ha-button";
|
||||
|
||||
const seeFAQ = (qid) => html`
|
||||
See <a href="./faq.html${qid ? `#${qid}` : ""}">the FAQ</a> for more
|
||||
@@ -83,14 +83,11 @@ export class HcConnect extends LitElement {
|
||||
Unable to connect to ${tokens!.hassUrl}.
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-button appearance="plain" href="/">Retry</ha-button>
|
||||
<a href="/">
|
||||
<mwc-button> Retry </mwc-button>
|
||||
</a>
|
||||
<div class="spacer"></div>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
variant="danger"
|
||||
@click=${this._handleLogout}
|
||||
>Log out</ha-button
|
||||
>
|
||||
<mwc-button @click=${this._handleLogout}>Log out</mwc-button>
|
||||
</div>
|
||||
</hc-layout>
|
||||
`;
|
||||
@@ -131,19 +128,16 @@ export class HcConnect extends LitElement {
|
||||
${this.error ? html` <p class="error">${this.error}</p> ` : ""}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-button appearance="plain" @click=${this._handleDemo}>
|
||||
<mwc-button @click=${this._handleDemo}>
|
||||
Show Demo
|
||||
<ha-svg-icon
|
||||
slot="end"
|
||||
.path=${this.castManager.castState === "CONNECTED"
|
||||
? mdiCastConnected
|
||||
: mdiCast}
|
||||
></ha-svg-icon>
|
||||
</ha-button>
|
||||
</mwc-button>
|
||||
<div class="spacer"></div>
|
||||
<ha-button appearance="plain" @click=${this._handleConnect}
|
||||
>Authorize</ha-button
|
||||
>
|
||||
<mwc-button @click=${this._handleConnect}>Authorize</mwc-button>
|
||||
</div>
|
||||
</hc-layout>
|
||||
`;
|
||||
@@ -315,6 +309,10 @@ export class HcConnect extends LitElement {
|
||||
color: darkred;
|
||||
}
|
||||
|
||||
mwc-button ha-svg-icon {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
@@ -95,8 +95,7 @@ class HcLayout extends LitElement {
|
||||
}
|
||||
|
||||
.hero {
|
||||
border-radius: var(--ha-border-radius-sm) var(--ha-border-radius-sm)
|
||||
var(--ha-border-radius-square) var(--ha-border-radius-square);
|
||||
border-radius: 4px 4px 0 0;
|
||||
}
|
||||
.subtitle {
|
||||
font-size: var(--ha-font-size-m);
|
||||
|
||||
@@ -5,19 +5,17 @@ const castContext = framework.CastReceiverContext.getInstance();
|
||||
const playerManager = castContext.getPlayerManager();
|
||||
|
||||
playerManager.setMessageInterceptor(
|
||||
"LOAD" as framework.messages.MessageType.LOAD,
|
||||
framework.messages.MessageType.LOAD,
|
||||
(loadRequestData) => {
|
||||
const media = loadRequestData.media;
|
||||
// Special handling if it came from Google Assistant
|
||||
if (media.entity) {
|
||||
media.contentId = media.entity;
|
||||
media.streamType = "LIVE" as framework.messages.StreamType.LIVE;
|
||||
media.streamType = framework.messages.StreamType.LIVE;
|
||||
media.contentType = "application/vnd.apple.mpegurl";
|
||||
// @ts-ignore
|
||||
// type definition is wrong, should be "FMP4" instead of "fmp4"
|
||||
// https://developers.google.com/cast/docs/reference/web_receiver/cast.framework.messages#.HlsVideoSegmentFormat
|
||||
media.hlsVideoSegmentFormat =
|
||||
"FMP4" as framework.messages.HlsVideoSegmentFormat.FMP4;
|
||||
framework.messages.HlsVideoSegmentFormat.FMP4;
|
||||
}
|
||||
return loadRequestData;
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ export const castDemoEntities: () => Entity[] = () =>
|
||||
longitude: 4.8903147,
|
||||
radius: 100,
|
||||
friendly_name: "Home",
|
||||
icon: "mdi:home",
|
||||
icon: "hass:home",
|
||||
},
|
||||
},
|
||||
"input_number.harmonyvolume": {
|
||||
@@ -88,7 +88,7 @@ export const castDemoEntities: () => Entity[] = () =>
|
||||
step: 1,
|
||||
mode: "slider",
|
||||
friendly_name: "Volume",
|
||||
icon: "mdi:volume-high",
|
||||
icon: "hass:volume-high",
|
||||
},
|
||||
},
|
||||
"climate.upstairs": {
|
||||
|
||||
@@ -56,7 +56,7 @@ export const castDemoLovelace: () => LovelaceConfig = () => {
|
||||
type: "weblink",
|
||||
url: "/lovelace/climate",
|
||||
name: "Climate controls",
|
||||
icon: "mdi:arrow-right",
|
||||
icon: "hass:arrow-right",
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -76,7 +76,7 @@ export const castDemoLovelace: () => LovelaceConfig = () => {
|
||||
type: "weblink",
|
||||
url: "/lovelace/overview",
|
||||
name: "Back",
|
||||
icon: "mdi:arrow-left",
|
||||
icon: "hass:arrow-left",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { framework } from "./cast_framework";
|
||||
import { CAST_NS } from "../../../src/cast/const";
|
||||
import type { HassMessage } from "../../../src/cast/receiver_messages";
|
||||
import "../../../src/resources/custom-card-support";
|
||||
import { castContext } from "./cast_context";
|
||||
import { framework } from "./cast_framework";
|
||||
import { HcMain } from "./layout/hc-main";
|
||||
import type { ReceivedMessage } from "./types";
|
||||
|
||||
const lovelaceController = new HcMain();
|
||||
document.body.append(lovelaceController);
|
||||
@@ -39,8 +40,7 @@ const playDummyMedia = (viewTitle?: string) => {
|
||||
loadRequestData.media.contentId =
|
||||
"https://cast.home-assistant.io/images/google-nest-hub.png";
|
||||
loadRequestData.media.contentType = "image/jpeg";
|
||||
loadRequestData.media.streamType =
|
||||
"NONE" as framework.messages.StreamType.NONE;
|
||||
loadRequestData.media.streamType = framework.messages.StreamType.NONE;
|
||||
const metadata = new framework.messages.GenericMediaMetadata();
|
||||
metadata.title = viewTitle;
|
||||
loadRequestData.media.metadata = metadata;
|
||||
@@ -89,30 +89,31 @@ const showMediaPlayer = () => {
|
||||
const options = new framework.CastReceiverOptions();
|
||||
options.disableIdleTimeout = true;
|
||||
options.customNamespaces = {
|
||||
// type definition is wrong, should be "JSON" instead of "json"
|
||||
// https://developers.google.com/cast/docs/reference/web_receiver/cast.framework.system#.MessageType
|
||||
[CAST_NS]: "JSON" as framework.system.MessageType.JSON,
|
||||
[CAST_NS]: framework.system.MessageType.JSON,
|
||||
};
|
||||
|
||||
castContext.addCustomMessageListener(CAST_NS, (ev) => {
|
||||
// We received a show Lovelace command, stop media from playing, hide media player and show Lovelace controller
|
||||
if (
|
||||
playerManager.getPlayerState() !==
|
||||
("IDLE" as framework.messages.PlayerState.IDLE)
|
||||
) {
|
||||
playerManager.stop();
|
||||
} else {
|
||||
showLovelaceController();
|
||||
castContext.addCustomMessageListener(
|
||||
CAST_NS,
|
||||
// @ts-ignore
|
||||
(ev: ReceivedMessage<HassMessage>) => {
|
||||
// We received a show Lovelace command, stop media from playing, hide media player and show Lovelace controller
|
||||
if (
|
||||
playerManager.getPlayerState() !== framework.messages.PlayerState.IDLE
|
||||
) {
|
||||
playerManager.stop();
|
||||
} else {
|
||||
showLovelaceController();
|
||||
}
|
||||
const msg = ev.data;
|
||||
msg.senderId = ev.senderId;
|
||||
lovelaceController.processIncomingMessage(msg);
|
||||
}
|
||||
const msg = ev.data as HassMessage;
|
||||
msg.senderId = ev.senderId;
|
||||
lovelaceController.processIncomingMessage(msg);
|
||||
});
|
||||
);
|
||||
|
||||
const playerManager = castContext.getPlayerManager();
|
||||
|
||||
playerManager.setMessageInterceptor(
|
||||
"LOAD" as framework.messages.MessageType.LOAD,
|
||||
framework.messages.MessageType.LOAD,
|
||||
(loadRequestData) => {
|
||||
if (
|
||||
loadRequestData.media.contentId ===
|
||||
@@ -126,26 +127,24 @@ playerManager.setMessageInterceptor(
|
||||
// Special handling if it came from Google Assistant
|
||||
if (media.entity) {
|
||||
media.contentId = media.entity;
|
||||
media.streamType = "LIVE" as framework.messages.StreamType.LIVE;
|
||||
media.streamType = framework.messages.StreamType.LIVE;
|
||||
media.contentType = "application/vnd.apple.mpegurl";
|
||||
// type definition is wrong, should be "FMP4" instead of "fmp4"
|
||||
// https://developers.google.com/cast/docs/reference/web_receiver/cast.framework.messages#.HlsVideoSegmentFormat
|
||||
// @ts-ignore
|
||||
media.hlsVideoSegmentFormat =
|
||||
"FMP4" as framework.messages.HlsVideoSegmentFormat.FMP4;
|
||||
framework.messages.HlsVideoSegmentFormat.FMP4;
|
||||
}
|
||||
return loadRequestData;
|
||||
}
|
||||
);
|
||||
|
||||
playerManager.addEventListener(
|
||||
"MEDIA_STATUS" as framework.events.EventType.MEDIA_STATUS,
|
||||
framework.events.EventType.MEDIA_STATUS,
|
||||
(event) => {
|
||||
if (
|
||||
event.mediaStatus?.playerState ===
|
||||
("IDLE" as framework.messages.PlayerState.IDLE) &&
|
||||
event.mediaStatus?.playerState === framework.messages.PlayerState.IDLE &&
|
||||
event.mediaStatus?.idleReason &&
|
||||
event.mediaStatus?.idleReason !==
|
||||
("INTERRUPTED" as framework.messages.IdleReason.INTERRUPTED)
|
||||
framework.messages.IdleReason.INTERRUPTED
|
||||
) {
|
||||
// media finished or stopped, return to default Lovelace
|
||||
showLovelaceController();
|
||||
|
||||
@@ -305,8 +305,9 @@ export class HcMain extends HassElement {
|
||||
await llColl.refresh();
|
||||
this._unsubLovelace = llColl.subscribe(async (rawConfig) => {
|
||||
if (isStrategyDashboard(rawConfig)) {
|
||||
const { generateLovelaceDashboardStrategy } =
|
||||
await import("../../../../src/panels/lovelace/strategies/get-strategy");
|
||||
const { generateLovelaceDashboardStrategy } = await import(
|
||||
"../../../../src/panels/lovelace/strategies/get-strategy"
|
||||
);
|
||||
const config = await generateLovelaceDashboardStrategy(
|
||||
rawConfig,
|
||||
this.hass!
|
||||
@@ -346,8 +347,9 @@ export class HcMain extends HassElement {
|
||||
}
|
||||
|
||||
private async _generateDefaultLovelaceConfig() {
|
||||
const { generateLovelaceDashboardStrategy } =
|
||||
await import("../../../../src/panels/lovelace/strategies/get-strategy");
|
||||
const { generateLovelaceDashboardStrategy } = await import(
|
||||
"../../../../src/panels/lovelace/strategies/get-strategy"
|
||||
);
|
||||
this._handleNewLovelaceConfig(
|
||||
await generateLovelaceDashboardStrategy(DEFAULT_CONFIG, this.hass!)
|
||||
);
|
||||
|
||||
6
cast/src/receiver/types.ts
Normal file
6
cast/src/receiver/types.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface ReceivedMessage<T> {
|
||||
gj: boolean;
|
||||
data: T;
|
||||
senderId: string;
|
||||
type: "message";
|
||||
}
|
||||
@@ -75,5 +75,5 @@
|
||||
"name": "Home Assistant Demo",
|
||||
"short_name": "HA Demo",
|
||||
"start_url": "/?homescreen=1",
|
||||
"theme_color": "#009ac7"
|
||||
"theme_color": "#03A9F4"
|
||||
}
|
||||
|
||||
@@ -6,4 +6,4 @@ set -e
|
||||
|
||||
cd "$(dirname "$0")/../.."
|
||||
|
||||
./node_modules/.bin/gulp build-demo
|
||||
yarn run-task build-demo
|
||||
|
||||
@@ -6,4 +6,4 @@ set -e
|
||||
|
||||
cd "$(dirname "$0")/../.."
|
||||
|
||||
./node_modules/.bin/gulp develop-demo
|
||||
yarn run-task develop-demo
|
||||
|
||||
@@ -6,4 +6,4 @@ set -e
|
||||
|
||||
cd "$(dirname "$0")/../.."
|
||||
|
||||
./node_modules/.bin/gulp analyze-demo
|
||||
yarn run-task analyze-demo
|
||||
@@ -143,7 +143,7 @@ export const demoEntitiesArsaboo: DemoConfig["entities"] = (localize) =>
|
||||
state: "on",
|
||||
attributes: {
|
||||
friendly_name: "Home Automation",
|
||||
icon: "mdi:home-automation",
|
||||
icon: "hass:home-automation",
|
||||
},
|
||||
},
|
||||
"input_boolean.tvtime": {
|
||||
|
||||
@@ -4,7 +4,7 @@ export const demoLovelaceArsaboo: DemoConfig["lovelace"] = (localize) => ({
|
||||
title: "Home Assistant",
|
||||
views: [
|
||||
{
|
||||
icon: "mdi:home-assistant",
|
||||
icon: "hass:home-assistant",
|
||||
id: "home",
|
||||
title: "Home",
|
||||
cards: [
|
||||
|
||||
@@ -1236,7 +1236,7 @@ export const demoLovelaceJimpower: DemoConfig["lovelace"] = () => ({
|
||||
},
|
||||
],
|
||||
path: "security",
|
||||
icon: "mdi:shield-home",
|
||||
icon: "hass:shield-home",
|
||||
name: "Security",
|
||||
background:
|
||||
'center / cover no-repeat url("/assets/jimpower/background-15.jpg") fixed',
|
||||
|
||||
@@ -89,14 +89,11 @@ export class HADemoCard extends LitElement implements LovelaceCard {
|
||||
)}
|
||||
</div>
|
||||
<div class="actions small-hidden">
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
size="small"
|
||||
href="https://www.home-assistant.io"
|
||||
target="_blank"
|
||||
>
|
||||
${this.hass.localize("ui.panel.page-demo.cards.demo.learn_more")}
|
||||
</ha-button>
|
||||
<a href="https://www.home-assistant.io" target="_blank">
|
||||
<ha-button>
|
||||
${this.hass.localize("ui.panel.page-demo.cards.demo.learn_more")}
|
||||
</ha-button>
|
||||
</a>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
|
||||
@@ -9,14 +9,11 @@ import { selectedDemoConfig } from "./configs/demo-configs";
|
||||
import { mockAreaRegistry } from "./stubs/area_registry";
|
||||
import { mockAuth } from "./stubs/auth";
|
||||
import { mockConfigEntries } from "./stubs/config_entries";
|
||||
import { mockDeviceRegistry } from "./stubs/device_registry";
|
||||
import { mockEnergy } from "./stubs/energy";
|
||||
import { energyEntities } from "./stubs/entities";
|
||||
import { mockEntityRegistry } from "./stubs/entity_registry";
|
||||
import { mockEvents } from "./stubs/events";
|
||||
import { mockFloorRegistry } from "./stubs/floor_registry";
|
||||
import { mockFrontend } from "./stubs/frontend";
|
||||
import { mockLabelRegistry } from "./stubs/label_registry";
|
||||
import { mockIcons } from "./stubs/icons";
|
||||
import { mockHistory } from "./stubs/history";
|
||||
import { mockLovelace } from "./stubs/lovelace";
|
||||
@@ -63,9 +60,6 @@ export class HaDemo extends HomeAssistantAppEl {
|
||||
mockPersistentNotification(hass);
|
||||
mockConfigEntries(hass);
|
||||
mockAreaRegistry(hass);
|
||||
mockDeviceRegistry(hass);
|
||||
mockFloorRegistry(hass);
|
||||
mockLabelRegistry(hass);
|
||||
mockEntityRegistry(hass, [
|
||||
{
|
||||
config_entry_id: "co2signal",
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
}
|
||||
#ha-launch-screen .ha-launch-screen-spacer-top {
|
||||
flex: 1;
|
||||
margin-top: calc( 2 * max(var(--safe-area-inset-top, 0px), 48px) + 46px );
|
||||
margin-top: calc( 2 * max(var(--safe-area-inset-bottom), 48px) + 46px );
|
||||
padding-top: 48px;
|
||||
}
|
||||
#ha-launch-screen .ha-launch-screen-spacer-bottom {
|
||||
@@ -76,7 +76,7 @@
|
||||
padding-top: 48px;
|
||||
}
|
||||
.ohf-logo {
|
||||
margin: max(var(--safe-area-inset-bottom, 0px), 48px) 0;
|
||||
margin: max(var(--safe-area-inset-bottom), 48px) 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AreaRegistryEntry } from "../../../src/data/area/area_registry";
|
||||
import type { AreaRegistryEntry } from "../../../src/data/area_registry";
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
export const mockAreaRegistry = (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { DeviceRegistryEntry } from "../../../src/data/device/device_registry";
|
||||
import type { DeviceRegistryEntry } from "../../../src/data/device_registry";
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
export const mockDeviceRegistry = (
|
||||
|
||||
@@ -14,42 +14,48 @@ export const mockEnergy = (hass: MockHomeAssistant) => {
|
||||
energy_sources: [
|
||||
{
|
||||
type: "grid",
|
||||
stat_energy_from: "sensor.energy_consumption_tarif_1",
|
||||
stat_energy_to: "sensor.energy_production_tarif_1",
|
||||
stat_cost: "sensor.energy_consumption_tarif_1_cost",
|
||||
stat_compensation: "sensor.energy_production_tarif_1_compensation",
|
||||
entity_energy_price: null,
|
||||
number_energy_price: null,
|
||||
entity_energy_price_export: null,
|
||||
number_energy_price_export: null,
|
||||
stat_rate: "sensor.power_grid",
|
||||
cost_adjustment_day: 0,
|
||||
},
|
||||
{
|
||||
type: "grid",
|
||||
stat_energy_from: "sensor.energy_consumption_tarif_2",
|
||||
stat_energy_to: "sensor.energy_production_tarif_2",
|
||||
stat_cost: "sensor.energy_consumption_tarif_2_cost",
|
||||
stat_compensation: "sensor.energy_production_tarif_2_compensation",
|
||||
entity_energy_price: null,
|
||||
number_energy_price: null,
|
||||
entity_energy_price_export: null,
|
||||
number_energy_price_export: null,
|
||||
stat_rate: "sensor.power_grid_return",
|
||||
flow_from: [
|
||||
{
|
||||
stat_energy_from: "sensor.energy_consumption_tarif_1",
|
||||
stat_cost: "sensor.energy_consumption_tarif_1_cost",
|
||||
entity_energy_price: null,
|
||||
number_energy_price: null,
|
||||
},
|
||||
{
|
||||
stat_energy_from: "sensor.energy_consumption_tarif_2",
|
||||
stat_cost: "sensor.energy_consumption_tarif_2_cost",
|
||||
entity_energy_price: null,
|
||||
number_energy_price: null,
|
||||
},
|
||||
],
|
||||
flow_to: [
|
||||
{
|
||||
stat_energy_to: "sensor.energy_production_tarif_1",
|
||||
stat_compensation:
|
||||
"sensor.energy_production_tarif_1_compensation",
|
||||
entity_energy_price: null,
|
||||
number_energy_price: null,
|
||||
},
|
||||
{
|
||||
stat_energy_to: "sensor.energy_production_tarif_2",
|
||||
stat_compensation:
|
||||
"sensor.energy_production_tarif_2_compensation",
|
||||
entity_energy_price: null,
|
||||
number_energy_price: null,
|
||||
},
|
||||
],
|
||||
cost_adjustment_day: 0,
|
||||
},
|
||||
{
|
||||
type: "solar",
|
||||
stat_energy_from: "sensor.solar_production",
|
||||
stat_rate: "sensor.power_solar",
|
||||
config_entry_solar_forecast: ["solar_forecast"],
|
||||
},
|
||||
{
|
||||
/* {
|
||||
type: "battery",
|
||||
stat_energy_from: "sensor.battery_output",
|
||||
stat_energy_to: "sensor.battery_input",
|
||||
stat_rate: "sensor.power_battery",
|
||||
},
|
||||
}, */
|
||||
{
|
||||
type: "gas",
|
||||
stat_energy_from: "sensor.energy_gas",
|
||||
@@ -57,46 +63,25 @@ export const mockEnergy = (hass: MockHomeAssistant) => {
|
||||
entity_energy_price: null,
|
||||
number_energy_price: null,
|
||||
},
|
||||
{
|
||||
type: "water",
|
||||
stat_energy_from: "sensor.energy_water",
|
||||
stat_cost: "sensor.energy_water_cost",
|
||||
entity_energy_price: null,
|
||||
number_energy_price: null,
|
||||
},
|
||||
],
|
||||
device_consumption: [
|
||||
{
|
||||
stat_consumption: "sensor.energy_car",
|
||||
stat_rate: "sensor.power_car",
|
||||
},
|
||||
{
|
||||
stat_consumption: "sensor.energy_ac",
|
||||
stat_rate: "sensor.power_ac",
|
||||
},
|
||||
{
|
||||
stat_consumption: "sensor.energy_washing_machine",
|
||||
stat_rate: "sensor.power_washing_machine",
|
||||
},
|
||||
{
|
||||
stat_consumption: "sensor.energy_dryer",
|
||||
stat_rate: "sensor.power_dryer",
|
||||
},
|
||||
{
|
||||
stat_consumption: "sensor.energy_heat_pump",
|
||||
stat_rate: "sensor.power_heat_pump",
|
||||
},
|
||||
{
|
||||
stat_consumption: "sensor.energy_boiler",
|
||||
stat_rate: "sensor.power_boiler",
|
||||
},
|
||||
],
|
||||
device_consumption_water: [
|
||||
{
|
||||
stat_consumption: "sensor.water_kitchen",
|
||||
},
|
||||
{
|
||||
stat_consumption: "sensor.water_garden",
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
@@ -154,38 +154,6 @@ export const energyEntities = () =>
|
||||
unit_of_measurement: "EUR",
|
||||
},
|
||||
},
|
||||
"sensor.power_grid": {
|
||||
entity_id: "sensor.power_grid",
|
||||
state: "500",
|
||||
attributes: {
|
||||
state_class: "measurement",
|
||||
unit_of_measurement: "W",
|
||||
},
|
||||
},
|
||||
"sensor.power_grid_return": {
|
||||
entity_id: "sensor.power_grid_return",
|
||||
state: "-100",
|
||||
attributes: {
|
||||
state_class: "measurement",
|
||||
unit_of_measurement: "W",
|
||||
},
|
||||
},
|
||||
"sensor.power_solar": {
|
||||
entity_id: "sensor.power_solar",
|
||||
state: "200",
|
||||
attributes: {
|
||||
state_class: "measurement",
|
||||
unit_of_measurement: "W",
|
||||
},
|
||||
},
|
||||
"sensor.power_battery": {
|
||||
entity_id: "sensor.power_battery",
|
||||
state: "100",
|
||||
attributes: {
|
||||
state_class: "measurement",
|
||||
unit_of_measurement: "W",
|
||||
},
|
||||
},
|
||||
"sensor.energy_gas_cost": {
|
||||
entity_id: "sensor.energy_gas_cost",
|
||||
state: "2",
|
||||
@@ -203,15 +171,6 @@ export const energyEntities = () =>
|
||||
unit_of_measurement: "m³",
|
||||
},
|
||||
},
|
||||
"sensor.energy_water": {
|
||||
entity_id: "sensor.energy_water",
|
||||
state: "4000",
|
||||
attributes: {
|
||||
last_reset: "1970-01-01T00:00:00:00+00",
|
||||
friendly_name: "Water",
|
||||
unit_of_measurement: "L",
|
||||
},
|
||||
},
|
||||
"sensor.energy_car": {
|
||||
entity_id: "sensor.energy_car",
|
||||
state: "4",
|
||||
@@ -266,58 +225,4 @@ export const energyEntities = () =>
|
||||
unit_of_measurement: "kWh",
|
||||
},
|
||||
},
|
||||
"sensor.power_car": {
|
||||
entity_id: "sensor.power_car",
|
||||
state: "40",
|
||||
attributes: {
|
||||
state_class: "measurement",
|
||||
friendly_name: "Electric car",
|
||||
unit_of_measurement: "W",
|
||||
},
|
||||
},
|
||||
"sensor.power_ac": {
|
||||
entity_id: "sensor.power_ac",
|
||||
state: "30",
|
||||
attributes: {
|
||||
state_class: "measurement",
|
||||
friendly_name: "Air conditioning",
|
||||
unit_of_measurement: "W",
|
||||
},
|
||||
},
|
||||
"sensor.power_washing_machine": {
|
||||
entity_id: "sensor.power_washing_machine",
|
||||
state: "60",
|
||||
attributes: {
|
||||
state_class: "measurement",
|
||||
friendly_name: "Washing machine",
|
||||
unit_of_measurement: "W",
|
||||
},
|
||||
},
|
||||
"sensor.power_dryer": {
|
||||
entity_id: "sensor.power_dryer",
|
||||
state: "55",
|
||||
attributes: {
|
||||
state_class: "measurement",
|
||||
friendly_name: "Dryer",
|
||||
unit_of_measurement: "W",
|
||||
},
|
||||
},
|
||||
"sensor.power_heat_pump": {
|
||||
entity_id: "sensor.power_heat_pump",
|
||||
state: "60",
|
||||
attributes: {
|
||||
state_class: "measurement",
|
||||
friendly_name: "Heat pump",
|
||||
unit_of_measurement: "W",
|
||||
},
|
||||
},
|
||||
"sensor.power_boiler": {
|
||||
entity_id: "sensor.power_boiler",
|
||||
state: "70",
|
||||
attributes: {
|
||||
state_class: "measurement",
|
||||
friendly_name: "Boiler",
|
||||
unit_of_measurement: "W",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { EntityRegistryEntry } from "../../../src/data/entity/entity_registry";
|
||||
import type { EntityRegistryEntry } from "../../../src/data/entity_registry";
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
export const mockEntityRegistry = (
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
let sidebarChangeCallback;
|
||||
let changeFunction;
|
||||
|
||||
export const mockFrontend = (hass: MockHomeAssistant) => {
|
||||
hass.mockWS("frontend/get_user_data", () => ({ value: null }));
|
||||
hass.mockWS("frontend/get_user_data", () => ({
|
||||
value: null,
|
||||
}));
|
||||
hass.mockWS("frontend/set_user_data", ({ key, value }) => {
|
||||
if (key === "sidebar") {
|
||||
sidebarChangeCallback?.({
|
||||
changeFunction?.({
|
||||
value: {
|
||||
panelOrder: value.panelOrder || [],
|
||||
hiddenPanels: value.hiddenPanels || [],
|
||||
@@ -14,34 +16,15 @@ export const mockFrontend = (hass: MockHomeAssistant) => {
|
||||
});
|
||||
}
|
||||
});
|
||||
hass.mockWS("frontend/subscribe_user_data", (msg, _hass, onChange) => {
|
||||
if (msg.key === "sidebar") {
|
||||
sidebarChangeCallback = onChange;
|
||||
}
|
||||
onChange?.({ value: null });
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
return () => {};
|
||||
});
|
||||
hass.mockWS(
|
||||
"frontend/subscribe_system_data",
|
||||
(_msg, currentHass, onChange) => {
|
||||
onChange?.({
|
||||
value: currentHass.systemData,
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
return () => {};
|
||||
}
|
||||
);
|
||||
hass.mockWS("labs/subscribe", (_msg, _currentHass, onChange) => {
|
||||
hass.mockWS("frontend/subscribe_user_data", (_msg, _hass, onChange) => {
|
||||
changeFunction = onChange;
|
||||
onChange?.({
|
||||
preview_feature: _msg.preview_feature,
|
||||
domain: _msg.domain,
|
||||
enabled: false,
|
||||
is_built_in: true,
|
||||
value: {
|
||||
panelOrder: [],
|
||||
hiddenPanels: [],
|
||||
},
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
return () => {};
|
||||
});
|
||||
hass.mockWS("repairs/list_issues", () => ({ issues: [] }));
|
||||
hass.mockWS("frontend/get_themes", (_msg, currentHass) => currentHass.themes);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { LabelRegistryEntry } from "../../../src/data/label/label_registry";
|
||||
import type { LabelRegistryEntry } from "../../../src/data/label_registry";
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
export const mockLabelRegistry = (
|
||||
|
||||
@@ -29,7 +29,6 @@ export const mockLovelace = (
|
||||
|
||||
hass.mockWS("lovelace/config/save", () => Promise.resolve());
|
||||
hass.mockWS("lovelace/resources", () => Promise.resolve([]));
|
||||
hass.mockWS("lovelace/dashboards/list", () => Promise.resolve([]));
|
||||
};
|
||||
|
||||
customElements.whenDefined("hui-root").then(() => {
|
||||
|
||||
@@ -17,15 +17,17 @@ const generateMeanStatistics = (
|
||||
end: Date,
|
||||
// eslint-disable-next-line default-param-last
|
||||
period: "5minute" | "hour" | "day" | "month" = "hour",
|
||||
initValue: number,
|
||||
maxDiff: number
|
||||
): StatisticValue[] => {
|
||||
const statistics: StatisticValue[] = [];
|
||||
let currentDate = new Date(start);
|
||||
currentDate.setMinutes(0, 0, 0);
|
||||
let lastVal = initValue;
|
||||
const now = new Date();
|
||||
while (end > currentDate && currentDate < now) {
|
||||
const delta = Math.random() * maxDiff;
|
||||
const mean = delta;
|
||||
const mean = lastVal + delta;
|
||||
statistics.push({
|
||||
start: currentDate.getTime(),
|
||||
end: currentDate.getTime(),
|
||||
@@ -36,6 +38,7 @@ const generateMeanStatistics = (
|
||||
state: mean,
|
||||
sum: null,
|
||||
});
|
||||
lastVal = mean;
|
||||
currentDate =
|
||||
period === "day"
|
||||
? addDays(currentDate, 1)
|
||||
@@ -333,6 +336,7 @@ export const mockRecorder = (mockHass: MockHomeAssistant) => {
|
||||
start,
|
||||
end,
|
||||
period,
|
||||
state,
|
||||
state * (state > 80 ? 0.05 : 0.1)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,18 +7,8 @@ export const mockTemplate = (hass: MockHomeAssistant) => {
|
||||
})
|
||||
);
|
||||
hass.mockWS("render_template", (msg, _hass, onChange) => {
|
||||
let result = msg.template;
|
||||
// Simple variable substitution for demo purposes
|
||||
if (msg.variables) {
|
||||
for (const [key, value] of Object.entries(msg.variables)) {
|
||||
result = result.replace(
|
||||
new RegExp(`\\{\\{\\s*${key}\\s*\\}\\}`, "g"),
|
||||
String(value)
|
||||
);
|
||||
}
|
||||
}
|
||||
onChange!({
|
||||
result,
|
||||
result: msg.template,
|
||||
listeners: { all: false, domains: [], entities: [], time: false },
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
|
||||
@@ -12,7 +12,6 @@ import eslintConfigPrettier from "eslint-config-prettier";
|
||||
import { configs as litConfigs } from "eslint-plugin-lit";
|
||||
import { configs as wcConfigs } from "eslint-plugin-wc";
|
||||
import { configs as a11yConfigs } from "eslint-plugin-lit-a11y";
|
||||
import html from "@html-eslint/eslint-plugin";
|
||||
|
||||
const _filename = fileURLToPath(import.meta.url);
|
||||
const _dirname = path.dirname(_filename);
|
||||
@@ -44,6 +43,7 @@ export default tseslint.config(
|
||||
__BUILD__: false,
|
||||
__VERSION__: false,
|
||||
__STATIC_PATH__: false,
|
||||
__SUPERVISOR__: false,
|
||||
},
|
||||
|
||||
parser: tseslint.parser,
|
||||
@@ -56,15 +56,6 @@ export default tseslint.config(
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
settings: {
|
||||
"import/resolver": {
|
||||
webpack: {
|
||||
config: "./rspack.config.cjs",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
rules: {
|
||||
"class-methods-use-this": "off",
|
||||
"new-cap": "off",
|
||||
@@ -187,19 +178,12 @@ export default tseslint.config(
|
||||
],
|
||||
"no-use-before-define": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["src/util/recorder-worklet.js"],
|
||||
languageOptions: {
|
||||
globals: globals.audioWorklet,
|
||||
},
|
||||
},
|
||||
{
|
||||
plugins: {
|
||||
html,
|
||||
},
|
||||
rules: {
|
||||
"html/no-invalid-attr-value": "error",
|
||||
settings: {
|
||||
"import/resolver": {
|
||||
node: {
|
||||
extensions: [".ts", ".js"],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -6,4 +6,4 @@ set -e
|
||||
|
||||
cd "$(dirname "$0")/../.."
|
||||
|
||||
./node_modules/.bin/gulp build-gallery
|
||||
yarn run-task build-gallery
|
||||
|
||||
@@ -6,4 +6,4 @@ set -e
|
||||
|
||||
cd "$(dirname "$0")/../.."
|
||||
|
||||
./node_modules/.bin/gulp develop-gallery
|
||||
yarn run-task develop-gallery
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import type { Button } from "@material/mwc-button";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { html, LitElement, css, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { applyThemesOnElement } from "../../../src/common/dom/apply_themes_on_element";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
import "../../../src/components/ha-card";
|
||||
import "../../../src/components/ha-button";
|
||||
import type { HaButton } from "../../../src/components/ha-button";
|
||||
|
||||
@customElement("demo-black-white-row")
|
||||
class DemoBlackWhiteRow extends LitElement {
|
||||
@@ -25,9 +25,12 @@ class DemoBlackWhiteRow extends LitElement {
|
||||
<slot name="light"></slot>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-button .disabled=${this.disabled} @click=${this.handleSubmit}>
|
||||
<mwc-button
|
||||
.disabled=${this.disabled}
|
||||
@click=${this.handleSubmit}
|
||||
>
|
||||
Submit
|
||||
</ha-button>
|
||||
</mwc-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
</div>
|
||||
@@ -37,9 +40,12 @@ class DemoBlackWhiteRow extends LitElement {
|
||||
<slot name="dark"></slot>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-button .disabled=${this.disabled} @click=${this.handleSubmit}>
|
||||
<mwc-button
|
||||
.disabled=${this.disabled}
|
||||
@click=${this.handleSubmit}
|
||||
>
|
||||
Submit
|
||||
</ha-button>
|
||||
</mwc-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
${this.value
|
||||
@@ -68,7 +74,7 @@ class DemoBlackWhiteRow extends LitElement {
|
||||
}
|
||||
|
||||
handleSubmit(ev) {
|
||||
const content = (ev.target as HaButton).closest(".content")!;
|
||||
const content = (ev.target as Button).closest(".content")!;
|
||||
fireEvent(this, "submitted" as any, {
|
||||
slot: content.classList.contains("light") ? "light" : "dark",
|
||||
});
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../../../src/components/ha-card";
|
||||
import "../../../src/dialogs/more-info/more-info-content";
|
||||
import "../../../src/state-summary/state-card-content";
|
||||
import "../ha-demo-options";
|
||||
import type { HomeAssistant } from "../../../src/types";
|
||||
import { computeShowNewMoreInfo } from "../../../src/dialogs/more-info/const";
|
||||
|
||||
@customElement("demo-more-info")
|
||||
class DemoMoreInfo extends LitElement {
|
||||
@@ -22,13 +21,11 @@ class DemoMoreInfo extends LitElement {
|
||||
<div class="root">
|
||||
<div id="card">
|
||||
<ha-card>
|
||||
${!computeShowNewMoreInfo(state)
|
||||
? html`<state-card-content
|
||||
.stateObj=${state}
|
||||
.hass=${this.hass}
|
||||
in-dialog
|
||||
></state-card-content>`
|
||||
: nothing}
|
||||
<state-card-content
|
||||
.stateObj=${state}
|
||||
.hass=${this.hass}
|
||||
in-dialog
|
||||
></state-card-content>
|
||||
|
||||
<more-info-content
|
||||
.hass=${this.hass}
|
||||
|
||||
@@ -1106,7 +1106,7 @@ export default {
|
||||
friendly_name: "Philips Hue",
|
||||
entity_picture: null,
|
||||
description:
|
||||
"Press the button on the bridge to register Philips Hue with Home Assistant.",
|
||||
"Press the button on the bridge to register Philips Hue with Home Assistant.\n\n",
|
||||
submit_caption: "I have pressed the button",
|
||||
},
|
||||
last_changed: "2018-07-19T10:44:46.515160+00:00",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user