mirror of
https://github.com/home-assistant/frontend.git
synced 2026-07-19 17:45:57 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2c440976aa |
@@ -1,98 +0,0 @@
|
|||||||
---
|
|
||||||
name: ha-frontend-components
|
|
||||||
description: Home Assistant frontend component patterns. Use when implementing or reviewing dialogs, ha-form, ha-alert, keyboard shortcuts, tooltips, panels, Lovelace cards, or ha-button usage.
|
|
||||||
---
|
|
||||||
|
|
||||||
# HA Frontend Components
|
|
||||||
|
|
||||||
Use this skill when creating or reviewing Home Assistant UI components and common interaction patterns.
|
|
||||||
|
|
||||||
## Dialogs
|
|
||||||
|
|
||||||
Open dialogs through the fire-event pattern:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
fireEvent(this, "show-dialog", {
|
|
||||||
dialogTag: "dialog-example",
|
|
||||||
dialogImport: () => import("./dialog-example"),
|
|
||||||
dialogParams: { title: "Example", data: someData },
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
Dialog implementation requirements:
|
|
||||||
|
|
||||||
- Use `ha-dialog`.
|
|
||||||
- Implement `HassDialog<T>`.
|
|
||||||
- Use `@state() private _open = false` to control visibility.
|
|
||||||
- Set `_open = true` in `showDialog()` and `_open = false` in `closeDialog()`.
|
|
||||||
- Return `nothing` while required params are absent.
|
|
||||||
- Fire `dialog-closed` in the close handler.
|
|
||||||
- Use `header-title` and `header-subtitle` for simple header text.
|
|
||||||
- Use slots when standard header attributes are not enough.
|
|
||||||
- Use `ha-dialog-footer` with `primaryAction` and `secondaryAction` slots.
|
|
||||||
- Add `autofocus` to the first focusable element, such as `<ha-form autofocus>`, and forward it internally if needed.
|
|
||||||
|
|
||||||
Use standard dialog widths: `small`, `medium`, `large`, or `full`. Avoid custom dialog sizing unless there is a clear product need.
|
|
||||||
|
|
||||||
## Buttons
|
|
||||||
|
|
||||||
`ha-button` wraps the Web Awesome button in `src/components/ha-button.ts`.
|
|
||||||
|
|
||||||
Axes:
|
|
||||||
|
|
||||||
- `variant`: `brand`, `neutral`, `danger`, `warning`, `success`.
|
|
||||||
- `appearance`: `accent`, `filled`, `outlined`, `plain`.
|
|
||||||
- `size`: `xs`, `s`, `m`, `l`, `xl`.
|
|
||||||
|
|
||||||
Common usage:
|
|
||||||
|
|
||||||
- Use `appearance="filled"` for primary emphasis when needed.
|
|
||||||
- Use `appearance="plain"` for cancel and dismiss actions.
|
|
||||||
- Use `variant="danger"` for destructive actions.
|
|
||||||
- Place primary actions in `slot="primaryAction"` and secondary actions in `slot="secondaryAction"`.
|
|
||||||
|
|
||||||
## Forms
|
|
||||||
|
|
||||||
`ha-form` is schema-driven with `HaFormSchema[]` and supports common selectors for entities, devices, areas, targets, numbers, booleans, time, actions, text, objects, selects, icons, media, and location.
|
|
||||||
|
|
||||||
Use `computeLabel`, `computeError`, and `computeHelper` for translated labels, validation, and helper text.
|
|
||||||
|
|
||||||
```ts
|
|
||||||
<ha-form
|
|
||||||
.hass=${this.hass}
|
|
||||||
.data=${this._data}
|
|
||||||
.schema=${this._schema}
|
|
||||||
.error=${this._errors}
|
|
||||||
.computeLabel=${(schema) => this.hass.localize(`ui.panel.${schema.name}`)}
|
|
||||||
@value-changed=${this._valueChanged}
|
|
||||||
></ha-form>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Alerts
|
|
||||||
|
|
||||||
Use `ha-alert` for user-visible status messaging.
|
|
||||||
|
|
||||||
- Alert types: `error`, `warning`, `info`, `success`.
|
|
||||||
- Useful properties: `title`, `alert-type`, `dismissable`, `narrow`.
|
|
||||||
- Slots: `icon` for custom leading icon, `action` for custom action content.
|
|
||||||
- Content is announced by screen readers when dynamically displayed.
|
|
||||||
|
|
||||||
```html
|
|
||||||
<ha-alert alert-type="error">Error message</ha-alert>
|
|
||||||
<ha-alert alert-type="warning" title="Warning">Description</ha-alert>
|
|
||||||
<ha-alert alert-type="success" dismissable>Success message</ha-alert>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Shortcuts And Tooltips
|
|
||||||
|
|
||||||
Use `ShortcutManager` from `src/common/keyboard/shortcuts.ts` for keyboard shortcuts. It blocks shortcuts in input fields, can prevent shortcuts during text selection, and supports character and KeyCode shortcuts for non-latin keyboards. See `src/state/quick-bar-mixin.ts` for global shortcut examples.
|
|
||||||
|
|
||||||
Use `ha-tooltip` from `src/components/ha-tooltip.ts` for contextual hover help. See `src/components/ha-label.ts` for an example.
|
|
||||||
|
|
||||||
## Panels And Lovelace Cards
|
|
||||||
|
|
||||||
Panels commonly extend `SubscribeMixin(LitElement)` and receive route and narrow-layout properties.
|
|
||||||
|
|
||||||
Lovelace cards should implement `LovelaceCard`, validate config in `setConfig()`, handle loading, error, unavailable, and missing-entity states, and add a configuration editor when needed.
|
|
||||||
|
|
||||||
Cards are user-story surfaces. Support different households, entity types, responsive layouts, and accessible interaction states.
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
---
|
|
||||||
name: ha-frontend-contexts
|
|
||||||
description: Home Assistant frontend Lit context and hass migration guidance. Use when adding or changing component state access, replacing hass reads, consuming entity or registry contexts, or reviewing rerender behavior.
|
|
||||||
---
|
|
||||||
|
|
||||||
# HA Frontend Contexts
|
|
||||||
|
|
||||||
Use this skill when a component reads Home Assistant state, registries, localization, services, config, UI data, connection state, or API helpers.
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
|
|
||||||
Move leaf components away from the broad `hass: HomeAssistant` object. Broad `hass` access rerenders components for unrelated changes, hides the data a component depends on, and makes tests harder to mock.
|
|
||||||
|
|
||||||
Container components may keep `hass` when they own it and feed providers. Leaf components should consume the narrowest context that covers their reads.
|
|
||||||
|
|
||||||
## Core Files
|
|
||||||
|
|
||||||
- Context definitions: `src/data/context/index.ts`
|
|
||||||
- Entity-scoped consume helpers: `src/common/decorators/consume-context-entry.ts`
|
|
||||||
- Transform decorator: `src/common/decorators/transform.ts`
|
|
||||||
- Canonical migration example: `src/panels/lovelace/cards/hui-button-card.ts`
|
|
||||||
- Providers are wired by `contextMixin` on `HassBaseEl`; consumers do not wire providers manually.
|
|
||||||
|
|
||||||
## Context Selection
|
|
||||||
|
|
||||||
| Context | Replaces |
|
|
||||||
| -------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
|
|
||||||
| `statesContext` | `hass.states` |
|
|
||||||
| `entitiesContext`, `devicesContext`, `areasContext`, `floorsContext` | `hass.entities`, `hass.devices`, `hass.areas`, `hass.floors` |
|
|
||||||
| `registriesContext` | all four registries together |
|
|
||||||
| `servicesContext` | `hass.services` |
|
|
||||||
| `internationalizationContext` | `hass.localize`, `hass.locale`, `hass.language` |
|
|
||||||
| `formattersContext` | entity and attribute formatters |
|
|
||||||
| `configContext` | `hass.config`, `hass.user`, `hass.auth`, `hass.userData` |
|
|
||||||
| `connectionContext` | `hass.connection`, `hass.connected`, `hass.hassUrl` |
|
|
||||||
| `apiContext` | `hass.callService`, `hass.callApi`, `hass.callWS`, `hass.sendWS`, `hass.fetchWithAuth` |
|
|
||||||
| `uiContext` | themes, selected theme, panels, sidebar, and UI state |
|
|
||||||
| `narrowViewportContext` | narrow-layout boolean |
|
|
||||||
|
|
||||||
Lazy contexts subscribe on first consumer and tear down after the last consumer: `labelsContext`, `fullEntitiesContext`, `configEntriesContext`, and `manifestsContext`.
|
|
||||||
|
|
||||||
The single-field contexts such as `localizeContext`, `themesContext`, and `userContext` are deprecated. Use grouped contexts instead.
|
|
||||||
|
|
||||||
## Consumption Patterns
|
|
||||||
|
|
||||||
Use entity-scoped helpers when the component watches an entity id held on the host:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
@state() @consumeEntityState({ entityIdPath: ["_config", "entity"] })
|
|
||||||
private _stateObj?: HassEntity;
|
|
||||||
|
|
||||||
@state() @consumeEntityRegistryEntry({ entityIdPath: ["_config", "entity"] })
|
|
||||||
private _entity?: EntityRegistryDisplayEntry;
|
|
||||||
|
|
||||||
@state() @consumeLocalize()
|
|
||||||
private _localize!: LocalizeFunc;
|
|
||||||
```
|
|
||||||
|
|
||||||
For a single field from a grouped context, pair `@consume` with `@transform`:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
@state()
|
|
||||||
@consume({ context: uiContext, subscribe: true })
|
|
||||||
@transform<HomeAssistantUI, Themes>({ transformer: ({ themes }) => themes })
|
|
||||||
private _themes!: Themes;
|
|
||||||
```
|
|
||||||
|
|
||||||
Use `@transform` with `watch` when the transformer depends on a host property, such as a computed entity id. `consumeEntityState` only watches the first path segment.
|
|
||||||
|
|
||||||
To consume a whole group untransformed, omit `@transform` and type the field as `ContextType<typeof statesContext>` or the matching context type.
|
|
||||||
|
|
||||||
## Review Checklist
|
|
||||||
|
|
||||||
- The component consumes the narrowest context needed for the data it reads.
|
|
||||||
- A broad `hass` property is kept only when the component is a container or external API requires it.
|
|
||||||
- Entity-scoped reads use the consume helpers rather than ad hoc context transforms.
|
|
||||||
- Context fields are marked `@state()` so updates trigger rendering.
|
|
||||||
- Tests and mocks only provide the data the component actually consumes.
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
---
|
|
||||||
name: ha-frontend-review
|
|
||||||
description: Home Assistant frontend PR and review guidance. Use when reviewing frontend changes, preparing a PR, checking recurring review issues, or applying the PR template.
|
|
||||||
---
|
|
||||||
|
|
||||||
# HA Frontend Review
|
|
||||||
|
|
||||||
Use this skill when reviewing Home Assistant frontend changes or preparing a pull request.
|
|
||||||
|
|
||||||
## Pull Request Body
|
|
||||||
|
|
||||||
When creating a pull request, use `.github/PULL_REQUEST_TEMPLATE.md` as the body.
|
|
||||||
|
|
||||||
- Do not omit, reorder, or rewrite template sections.
|
|
||||||
- Check the appropriate "Type of change" box based on the actual change.
|
|
||||||
- Do not check checklist items on behalf of the user.
|
|
||||||
- If the PR includes UI changes, remind the user to add screenshots or a short video.
|
|
||||||
- Explain what the change does for users, not only implementation details.
|
|
||||||
- Use Markdown.
|
|
||||||
|
|
||||||
## Pre-Submission Checklist
|
|
||||||
|
|
||||||
- `yarn lint` passes when practical for the scope.
|
|
||||||
- `yarn test` or focused relevant tests are green when practical for the scope.
|
|
||||||
- Tests are added or updated for new data processing and utilities where applicable.
|
|
||||||
- User-facing text is localized and follows `ha-frontend-user-facing-text` guidance.
|
|
||||||
- Components handle loading, error, unavailable, and missing-entity states.
|
|
||||||
- Entity existence is checked before property access.
|
|
||||||
- Event listeners and subscriptions are cleaned up.
|
|
||||||
- UI is accessible to screen readers and keyboard users.
|
|
||||||
|
|
||||||
## Recurring Review Issues
|
|
||||||
|
|
||||||
User experience and accessibility:
|
|
||||||
|
|
||||||
- Forms need proper labels, helper text, and validation feedback.
|
|
||||||
- Form markup should not cause password managers to identify fields incorrectly.
|
|
||||||
- Clickable areas should be large enough for touch interaction.
|
|
||||||
- Hover, active, disabled, loading, and focus states should be clear.
|
|
||||||
|
|
||||||
Dialog and modal patterns:
|
|
||||||
|
|
||||||
- Multi-step operations should show progress.
|
|
||||||
- Dialog state should survive background operations correctly.
|
|
||||||
- Cancel and close buttons should behave consistently.
|
|
||||||
- Defaults should be helpful without blocking user override.
|
|
||||||
|
|
||||||
Component design patterns:
|
|
||||||
|
|
||||||
- Terminology should be consistent. Use words like "Join" or "Apply" instead of "Group" when that better matches the user action.
|
|
||||||
- Visual hierarchy should use appropriate font sizes, weights, and spacing ratios.
|
|
||||||
- Components should align to the design grid.
|
|
||||||
- Badges and indicators should be placed consistently.
|
|
||||||
|
|
||||||
Code quality:
|
|
||||||
|
|
||||||
- Null and undefined paths should be handled explicitly.
|
|
||||||
- Potentially undefined array and object access should be guarded.
|
|
||||||
- Event handlers, timers, observers, and subscriptions should be cleaned up.
|
|
||||||
|
|
||||||
Configuration and props:
|
|
||||||
|
|
||||||
- Make configuration fields optional when sensible.
|
|
||||||
- Provide reasonable defaults.
|
|
||||||
- Keep APIs extensible without adding speculative abstractions.
|
|
||||||
- Validate configuration before applying changes.
|
|
||||||
|
|
||||||
## Review Flow
|
|
||||||
|
|
||||||
- Identify behavioral regressions, bugs, accessibility issues, and missing tests first.
|
|
||||||
- Keep style-only comments secondary unless they affect maintainability or user experience.
|
|
||||||
- Prefer small, direct fixes over large refactors during review follow-up.
|
|
||||||
- Cross-load `ha-frontend-contexts`, `ha-frontend-components`, `ha-frontend-styling`, `ha-frontend-testing`, or `ha-frontend-user-facing-text` when a finding falls in that area.
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
---
|
|
||||||
name: ha-frontend-styling
|
|
||||||
description: Home Assistant frontend styling, theming, spacing, responsive layout, RTL, and View Transitions guidance. Use when editing CSS, layout, motion, or visual component structure.
|
|
||||||
---
|
|
||||||
|
|
||||||
# HA Frontend Styling
|
|
||||||
|
|
||||||
Use this skill when editing CSS, layout, visual hierarchy, theme integration, responsive behavior, RTL support, or view transitions.
|
|
||||||
|
|
||||||
## Theme And Layout Basics
|
|
||||||
|
|
||||||
- Use Home Assistant CSS custom properties instead of hardcoded colors.
|
|
||||||
- Use `--ha-space-*` spacing tokens instead of hardcoded spacing where possible.
|
|
||||||
- Keep components mobile-first and enhance for desktop.
|
|
||||||
- Keep layouts RTL-safe. Prefer logical properties when they fit.
|
|
||||||
- Prefer `ha-*` components and current Web Awesome wrappers.
|
|
||||||
- Avoid adding new legacy Material Web Components (`mwc-*`).
|
|
||||||
- Scope styles to the component. Do not rely on global styles for component internals.
|
|
||||||
|
|
||||||
Spacing tokens are defined in `src/resources/theme/core.globals.ts`. The scale runs from `--ha-space-1` at 4px through `--ha-space-20` at 80px in 4px increments. Common values are `--ha-space-2` at 8px, `--ha-space-4` at 16px, and `--ha-space-8` at 32px.
|
|
||||||
|
|
||||||
```ts
|
|
||||||
static get styles() {
|
|
||||||
return css`
|
|
||||||
:host {
|
|
||||||
padding: var(--ha-space-4);
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Interaction States
|
|
||||||
|
|
||||||
- Make touch targets large enough for mobile.
|
|
||||||
- Provide clear hover, active, focus, disabled, loading, error, and unavailable states.
|
|
||||||
- Preserve keyboard navigation and visible focus indicators.
|
|
||||||
- Maintain WCAG AA contrast for text and essential UI affordances.
|
|
||||||
|
|
||||||
## View Transitions
|
|
||||||
|
|
||||||
Use the View Transitions API only for meaningful continuity between DOM states.
|
|
||||||
|
|
||||||
Core resources:
|
|
||||||
|
|
||||||
- Utility wrapper: `src/common/util/view-transition.ts`, `withViewTransition()`.
|
|
||||||
- Launch-screen fade example: `src/util/launch-screen.ts`.
|
|
||||||
- Animation keyframes: `src/resources/theme/animations.globals.ts`.
|
|
||||||
- Animation duration tokens: `src/resources/theme/core.globals.ts`.
|
|
||||||
|
|
||||||
Implementation rules:
|
|
||||||
|
|
||||||
- Use `withViewTransition()` for fallback behavior.
|
|
||||||
- Keep transitions simple. Subtle fades and crossfades usually work best.
|
|
||||||
- Use `--ha-animation-duration-fast`, `--ha-animation-duration-normal`, or `--ha-animation-duration-slow` for timing.
|
|
||||||
- Ensure each `view-transition-name` is unique at any given time.
|
|
||||||
- Remember only one view transition can run at a time.
|
|
||||||
- View transitions operate at document level and do not work inside Shadow DOM style isolation. For web components, set `view-transition-name` on `:host` or use document-level transitions.
|
|
||||||
- The root gets `view-transition-name: root` by default. Target `::view-transition-group(root)` to customize the default page transition.
|
|
||||||
|
|
||||||
## Review Checklist
|
|
||||||
|
|
||||||
- Styling uses theme variables and spacing tokens where practical.
|
|
||||||
- Layout is mobile-first and RTL-safe.
|
|
||||||
- Component styles are scoped.
|
|
||||||
- Interactive states are clear and accessible.
|
|
||||||
- Motion is subtle, tokenized, and respects reduced-motion behavior through existing globals.
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
---
|
|
||||||
name: ha-frontend-testing
|
|
||||||
description: Home Assistant frontend validation workflow. Use when running lint, TypeScript checks, Vitest, Playwright e2e suites, dev servers, or chart-data benchmarks.
|
|
||||||
---
|
|
||||||
|
|
||||||
# HA Frontend Testing
|
|
||||||
|
|
||||||
Use this skill when choosing or running validation for frontend changes.
|
|
||||||
|
|
||||||
## Core Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
yarn lint # ESLint + Prettier + TypeScript + Lit
|
|
||||||
yarn format # Auto-fix ESLint + Prettier
|
|
||||||
yarn lint:types # TypeScript compiler, run without file arguments
|
|
||||||
yarn test # Vitest
|
|
||||||
yarn dev # App dev server
|
|
||||||
yarn dev:serve # Local serving dev server
|
|
||||||
```
|
|
||||||
|
|
||||||
Never run `tsc` or `yarn lint:types` with file arguments. File arguments make `tsc` ignore `tsconfig.json` and can emit `.js` files into `src/`.
|
|
||||||
|
|
||||||
For focused type feedback on one file, use editor diagnostics instead of a file-scoped `tsc` command.
|
|
||||||
|
|
||||||
## Unit And Utility Tests
|
|
||||||
|
|
||||||
- Add or update Vitest tests for data processing, utility code, and behavior that can be tested without a browser.
|
|
||||||
- Mock WebSocket connections and API calls at boundaries.
|
|
||||||
- Cover loading, error, unavailable, and missing-entity states where relevant.
|
|
||||||
- Test accessibility-sensitive behavior when it can be asserted without brittle DOM internals.
|
|
||||||
|
|
||||||
## Dev Servers
|
|
||||||
|
|
||||||
`yarn dev` builds and watches the app, served by a running Home Assistant core configured through `development_repo`.
|
|
||||||
|
|
||||||
`yarn dev:serve` also serves locally and supports `-c` for the core URL and `-p` for the port. Default local serving port is 8124.
|
|
||||||
|
|
||||||
Dev server commands support `--background`, `--status`, `--stop`, and `--logs [--follow]`.
|
|
||||||
|
|
||||||
## Playwright E2E
|
|
||||||
|
|
||||||
Each suite has its own dev server port. Playwright reuses an existing server locally when the port is already running; otherwise it performs a slow full build. The rspack watcher recompiles on save, so reruns should not need a restart.
|
|
||||||
|
|
||||||
Start the relevant suite server, then run that suite:
|
|
||||||
|
|
||||||
| Suite | Server | Test command |
|
|
||||||
| ------- | ------------------------------- | ----------------------- |
|
|
||||||
| App | `yarn test:e2e:app:dev` on 8095 | `yarn test:e2e:app` |
|
|
||||||
| Demo | `yarn dev:demo` on 8090 | `yarn test:e2e:demo` |
|
|
||||||
| Gallery | `yarn dev:gallery` on 8100 | `yarn test:e2e:gallery` |
|
|
||||||
|
|
||||||
Server reuse and `--stop` use the `/__ha_dev_status` health check, so starting or stopping twice is harmless.
|
|
||||||
|
|
||||||
Use `-g "<title>" --project=chromium` to narrow a run. `yarn test:e2e` runs all three suites. Run suites directly; piping through output truncation hides progress and failures.
|
|
||||||
|
|
||||||
The app suite uses a stripped-down harness for e2e. Demo and gallery use their normal dev servers.
|
|
||||||
|
|
||||||
## Benchmarks
|
|
||||||
|
|
||||||
For chart data transforms such as history, statistics, energy, and downsampling, follow `test/benchmarks/README.md`.
|
|
||||||
|
|
||||||
Use seeded fixtures, characterization snapshot tests, and `yarn test:bench` before and after optimization. Optimizations must keep output bit-identical.
|
|
||||||
|
|
||||||
## Verification Selection
|
|
||||||
|
|
||||||
- Documentation-only change: no code test required unless examples or commands changed.
|
|
||||||
- Type-only or utility change: run focused Vitest if available, then `yarn lint:types` if practical.
|
|
||||||
- Lit component change: run relevant tests plus lint or typecheck depending on scope.
|
|
||||||
- E2E-sensitive flow: start the relevant e2e dev server and run the narrow Playwright suite.
|
|
||||||
- Broad refactor: run `yarn lint` and relevant test suites when practical.
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
---
|
|
||||||
name: ha-frontend-user-facing-text
|
|
||||||
description: Home Assistant frontend copy, localization, terminology, and user-facing text guidance. Use when adding or reviewing labels, buttons, dialogs, errors, translations, or UI strings.
|
|
||||||
---
|
|
||||||
|
|
||||||
# HA Frontend User-Facing Text
|
|
||||||
|
|
||||||
Use this skill for all user-facing text, translations, labels, buttons, dialog copy, errors, helper text, and review comments about wording.
|
|
||||||
|
|
||||||
## Localization
|
|
||||||
|
|
||||||
- All user-facing text must be translatable.
|
|
||||||
- Add translation keys to `src/translations/en.json` when introducing new strings.
|
|
||||||
- Use the localization system instead of inline user-visible strings.
|
|
||||||
- Prefer complete localized strings with placeholders over concatenating translated fragments.
|
|
||||||
- Give translators enough context through key naming and placeholders.
|
|
||||||
|
|
||||||
```ts
|
|
||||||
this.hass.localize("ui.panel.config.updates.update_available", {
|
|
||||||
count: 5,
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## Voice And Style
|
|
||||||
|
|
||||||
- Use American English.
|
|
||||||
- Use a friendly, informational tone.
|
|
||||||
- Address users directly with "you" and "your" when appropriate.
|
|
||||||
- Be inclusive, objective, and non-discriminatory.
|
|
||||||
- Be concise and clear.
|
|
||||||
- Use active voice.
|
|
||||||
- Avoid jargon where a familiar home automation term works.
|
|
||||||
- Always write "Home Assistant" in full. Do not use "HA" or "HASS" in user-facing copy.
|
|
||||||
- Spell out terms when possible.
|
|
||||||
- Use sentence case for titles, headings, buttons, labels, and UI elements.
|
|
||||||
- Use the Oxford comma in lists.
|
|
||||||
- Prefer "like" over "e.g." and "for example" over "i.e.".
|
|
||||||
- Avoid all caps for emphasis. Use wording, bold, or italics instead.
|
|
||||||
- Write for both technical and non-technical users.
|
|
||||||
|
|
||||||
Sentence case examples:
|
|
||||||
|
|
||||||
- Use: "Create new automation"
|
|
||||||
- Avoid: "Create New Automation"
|
|
||||||
- Use: "Device settings"
|
|
||||||
- Avoid: "Device Settings"
|
|
||||||
|
|
||||||
## Terminology
|
|
||||||
|
|
||||||
Use "integration" instead of "component" for user-facing product language unless referring to a frontend component in developer context.
|
|
||||||
|
|
||||||
Technical product terms are lowercase in prose: automation, entity, device, service.
|
|
||||||
|
|
||||||
## Delete, Remove, Create, Add
|
|
||||||
|
|
||||||
Use "Remove" for actions that can be restored or reapplied:
|
|
||||||
|
|
||||||
- Removing a user's permission.
|
|
||||||
- Removing a user from a group.
|
|
||||||
- Removing links between items.
|
|
||||||
- Removing a widget from a dashboard.
|
|
||||||
- Removing an item from a cart.
|
|
||||||
|
|
||||||
Use "Delete" for permanent, non-recoverable actions:
|
|
||||||
|
|
||||||
- Deleting a field.
|
|
||||||
- Deleting a value in a field.
|
|
||||||
- Deleting a task.
|
|
||||||
- Deleting a group.
|
|
||||||
- Deleting a permission.
|
|
||||||
- Deleting a calendar event.
|
|
||||||
|
|
||||||
Use "Add" for already-existing items:
|
|
||||||
|
|
||||||
- Adding a permission to a user.
|
|
||||||
- Adding a user to a group.
|
|
||||||
- Adding links between items.
|
|
||||||
- Adding a widget to a dashboard.
|
|
||||||
- Adding an item to a cart.
|
|
||||||
|
|
||||||
Use "Create" for something made from scratch:
|
|
||||||
|
|
||||||
- Creating a new field.
|
|
||||||
- Creating a new task.
|
|
||||||
- Creating a new group.
|
|
||||||
- Creating a new permission.
|
|
||||||
- Creating a new calendar event.
|
|
||||||
|
|
||||||
Create pairs with Delete. Add pairs with Remove.
|
|
||||||
|
|
||||||
## Review Checklist
|
|
||||||
|
|
||||||
- Text is localized.
|
|
||||||
- Copy uses sentence case.
|
|
||||||
- "Home Assistant" is written in full.
|
|
||||||
- Delete/Remove and Create/Add match recoverability and object lifecycle.
|
|
||||||
- Placeholders are used instead of string concatenation.
|
|
||||||
- The wording is concise and understandable without implementation knowledge.
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
[modern]
|
|
||||||
# Modern builds target recent browsers supporting the latest features to minimize transpilation, polyfills, etc.
|
|
||||||
# It is served to browsers meeting the following requirements:
|
|
||||||
# - released in the last year + current alpha/beta versions
|
|
||||||
# - Firefox extended support release (ESR)
|
|
||||||
# - with global utilization at or above 0.5%
|
|
||||||
# - exclude dead browsers (no security maintenance for 2+ years)
|
|
||||||
# - exclude KaiOS, QQ, and UC browsers due to lack of sufficient feature support data
|
|
||||||
unreleased versions
|
|
||||||
last 1 year
|
|
||||||
Firefox ESR
|
|
||||||
>= 0.5%
|
|
||||||
not dead
|
|
||||||
not KaiOS > 0
|
|
||||||
not QQAndroid > 0
|
|
||||||
not UCAndroid > 0
|
|
||||||
|
|
||||||
[legacy]
|
|
||||||
# Legacy builds are served when modern requirements are not met and support browsers:
|
|
||||||
# - released in the last 7 years + current alpha/beta versionss
|
|
||||||
# - with global utilization at or above 0.05%
|
|
||||||
# - exclude dead browsers (no security maintenance for 2+ years)
|
|
||||||
# - exclude Opera Mini which does not support web sockets
|
|
||||||
unreleased versions
|
|
||||||
last 7 years
|
|
||||||
>= 0.05%
|
|
||||||
not dead
|
|
||||||
not op_mini all
|
|
||||||
|
|
||||||
[legacy-sw]
|
|
||||||
# Same as legacy plus supports service workers
|
|
||||||
unreleased versions
|
|
||||||
last 7 years
|
|
||||||
>= 0.05% and supports serviceworkers
|
|
||||||
not dead
|
|
||||||
not op_mini all
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
../.agents/skills
|
|
||||||
@@ -1,6 +1,13 @@
|
|||||||
FROM mcr.microsoft.com/devcontainers/python:3.14
|
# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.148.1/containers/python-3/.devcontainer/base.Dockerfile
|
||||||
|
FROM mcr.microsoft.com/vscode/devcontainers/python:0-3.9
|
||||||
|
|
||||||
ENV \
|
ENV \
|
||||||
DEBIAN_FRONTEND=noninteractive \
|
DEBIAN_FRONTEND=noninteractive \
|
||||||
DEVCONTAINER=true \
|
DEVCONTAINER=true \
|
||||||
PATH=$PATH:./node_modules/.bin
|
PATH=$PATH:./node_modules/.bin
|
||||||
|
|
||||||
|
# Install nvm
|
||||||
|
COPY .nvmrc /tmp/.nvmrc
|
||||||
|
RUN \
|
||||||
|
su vscode -c \
|
||||||
|
"source /usr/local/share/nvm/nvm.sh && nvm install $(cat /tmp/.nvmrc) 2>&1"
|
||||||
@@ -5,45 +5,30 @@
|
|||||||
"context": ".."
|
"context": ".."
|
||||||
},
|
},
|
||||||
"appPort": "8124:8123",
|
"appPort": "8124:8123",
|
||||||
"postCreateCommand": "./.devcontainer/post_create.sh",
|
"context": "..",
|
||||||
"postStartCommand": "script/bootstrap",
|
"postCreateCommand": "script/bootstrap",
|
||||||
"containerEnv": {
|
"extensions": [
|
||||||
"DEV_CONTAINER": "1",
|
"github.vscode-pull-request-github",
|
||||||
"WORKSPACE_DIRECTORY": "${containerWorkspaceFolder}"
|
"dbaeumer.vscode-eslint",
|
||||||
},
|
"ms-vscode.vscode-typescript-tslint-plugin",
|
||||||
"remoteEnv": {
|
"esbenp.prettier-vscode",
|
||||||
"NODE_OPTIONS": "--max_old_space_size=8192"
|
"bierner.lit-html",
|
||||||
},
|
"runem.lit-plugin",
|
||||||
"customizations": {
|
"ms-python.vscode-pylance"
|
||||||
"vscode": {
|
],
|
||||||
"extensions": [
|
"settings": {
|
||||||
"dbaeumer.vscode-eslint",
|
"terminal.integrated.shell.linux": "/bin/bash",
|
||||||
"esbenp.prettier-vscode",
|
"files.eol": "\n",
|
||||||
"runem.lit-plugin",
|
"editor.tabSize": 2,
|
||||||
"github.vscode-pull-request-github",
|
"editor.formatOnPaste": false,
|
||||||
"eamodio.gitlens",
|
"editor.formatOnSave": true,
|
||||||
"yeion7.styled-global-variables-autocomplete"
|
"editor.formatOnType": true,
|
||||||
],
|
"[typescript]": {
|
||||||
"settings": {
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
"files.eol": "\n",
|
},
|
||||||
"editor.tabSize": 2,
|
"[javascript]": {
|
||||||
"editor.formatOnPaste": false,
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
"editor.formatOnSave": true,
|
},
|
||||||
"editor.formatOnType": true,
|
"files.trimTrailingWhitespace": true
|
||||||
"editor.renderWhitespace": "boundary",
|
|
||||||
"editor.rulers": [80],
|
|
||||||
"[typescript]": {
|
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
|
||||||
},
|
|
||||||
"[javascript]": {
|
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
|
||||||
},
|
|
||||||
"files.trimTrailingWhitespace": true,
|
|
||||||
"terminal.integrated.shell.linux": "/usr/bin/zsh",
|
|
||||||
"gitlens.showWelcomeOnInstall": false,
|
|
||||||
"gitlens.showWhatsNewAfterUpgrades": false,
|
|
||||||
"workbench.startupEditor": "none"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# This script will run after the container is created
|
|
||||||
|
|
||||||
# add github cli
|
|
||||||
(type -p wget >/dev/null || (sudo apt update && sudo apt-get install wget -y)) \
|
|
||||||
&& sudo mkdir -p -m 755 /etc/apt/keyrings \
|
|
||||||
&& out=$(mktemp) && wget -nv -O$out https://cli.github.com/packages/githubcli-archive-keyring.gpg \
|
|
||||||
&& cat $out | sudo tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null \
|
|
||||||
&& sudo chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \
|
|
||||||
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null
|
|
||||||
|
|
||||||
# Update package lists
|
|
||||||
sudo apt-get update
|
|
||||||
|
|
||||||
sudo apt upgrade -y
|
|
||||||
|
|
||||||
# Install necessary packages
|
|
||||||
sudo apt-get install -y libpcap-dev gh
|
|
||||||
|
|
||||||
# Display a message
|
|
||||||
echo "Post-create script has been executed successfully."
|
|
||||||
+119
@@ -0,0 +1,119 @@
|
|||||||
|
{
|
||||||
|
"extends": [
|
||||||
|
"airbnb-base",
|
||||||
|
"airbnb-typescript/base",
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
"plugin:wc/recommended",
|
||||||
|
"plugin:lit/all",
|
||||||
|
"prettier"
|
||||||
|
],
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": 2020,
|
||||||
|
"ecmaFeatures": {
|
||||||
|
"modules": true
|
||||||
|
},
|
||||||
|
"sourceType": "module",
|
||||||
|
"project": "./tsconfig.json"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"import/resolver": {
|
||||||
|
"webpack": {
|
||||||
|
"config": "./webpack.config.js"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"globals": {
|
||||||
|
"__DEV__": false,
|
||||||
|
"__DEMO__": false,
|
||||||
|
"__BUILD__": false,
|
||||||
|
"__VERSION__": false,
|
||||||
|
"__STATIC_PATH__": false,
|
||||||
|
"Polymer": true
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"es6": true
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"class-methods-use-this": "off",
|
||||||
|
"new-cap": "off",
|
||||||
|
"prefer-template": "off",
|
||||||
|
"object-shorthand": "off",
|
||||||
|
"func-names": "off",
|
||||||
|
"no-underscore-dangle": "off",
|
||||||
|
"strict": "off",
|
||||||
|
"no-plusplus": "off",
|
||||||
|
"no-bitwise": "error",
|
||||||
|
"comma-dangle": "off",
|
||||||
|
"vars-on-top": "off",
|
||||||
|
"no-continue": "off",
|
||||||
|
"no-param-reassign": "off",
|
||||||
|
"no-multi-assign": "off",
|
||||||
|
"no-console": "error",
|
||||||
|
"radix": "off",
|
||||||
|
"no-alert": "off",
|
||||||
|
"no-nested-ternary": "off",
|
||||||
|
"prefer-destructuring": "off",
|
||||||
|
"no-restricted-globals": [2, "event"],
|
||||||
|
"prefer-promise-reject-errors": "off",
|
||||||
|
"import/prefer-default-export": "off",
|
||||||
|
"import/no-default-export": "off",
|
||||||
|
"import/no-unresolved": "off",
|
||||||
|
"import/no-cycle": "off",
|
||||||
|
"import/extensions": [
|
||||||
|
"error",
|
||||||
|
"ignorePackages",
|
||||||
|
{ "ts": "never", "js": "never" }
|
||||||
|
],
|
||||||
|
"no-restricted-syntax": ["error", "LabeledStatement", "WithStatement"],
|
||||||
|
"object-curly-newline": "off",
|
||||||
|
"default-case": "off",
|
||||||
|
"wc/no-self-class": "off",
|
||||||
|
"no-shadow": "off",
|
||||||
|
"@typescript-eslint/camelcase": "off",
|
||||||
|
"@typescript-eslint/ban-ts-comment": "off",
|
||||||
|
"@typescript-eslint/no-use-before-define": "off",
|
||||||
|
"@typescript-eslint/no-non-null-assertion": "off",
|
||||||
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
|
"@typescript-eslint/explicit-function-return-type": "off",
|
||||||
|
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||||
|
"@typescript-eslint/no-shadow": ["error"],
|
||||||
|
"@typescript-eslint/naming-convention": [
|
||||||
|
"off",
|
||||||
|
{
|
||||||
|
"selector": "default",
|
||||||
|
"format": ["camelCase", "snake_case"],
|
||||||
|
"leadingUnderscore": "allow",
|
||||||
|
"trailingUnderscore": "allow"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"selector": ["variable"],
|
||||||
|
"format": ["camelCase", "snake_case", "UPPER_CASE"],
|
||||||
|
"leadingUnderscore": "allow",
|
||||||
|
"trailingUnderscore": "allow"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"selector": "typeLike",
|
||||||
|
"format": ["PascalCase"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@typescript-eslint/no-unused-vars": "off",
|
||||||
|
"unused-imports/no-unused-vars": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"vars": "all",
|
||||||
|
"varsIgnorePattern": "^_",
|
||||||
|
"args": "after-used",
|
||||||
|
"argsIgnorePattern": "^_",
|
||||||
|
"ignoreRestSiblings": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unused-imports/no-unused-imports": "error",
|
||||||
|
"lit/attribute-value-entities": "off",
|
||||||
|
"lit/no-template-map": "off",
|
||||||
|
"lit/no-template-arrow": "warn"
|
||||||
|
},
|
||||||
|
"plugins": ["disable", "unused-imports"],
|
||||||
|
"processor": "disable/disable"
|
||||||
|
}
|
||||||
@@ -51,7 +51,7 @@ DO NOT DELETE ANY TEXT from this template! Otherwise, your issue may be closed w
|
|||||||
<!--
|
<!--
|
||||||
Provide details about the versions you are using, which helps us reproducing
|
Provide details about the versions you are using, which helps us reproducing
|
||||||
and finding the issue quicker. Version information is found in the
|
and finding the issue quicker. Version information is found in the
|
||||||
Home Assistant frontend: Settings -> About.
|
Home Assistant frontend: Configuration -> Info.
|
||||||
|
|
||||||
Browser version and operating system is important! Please try to replicate
|
Browser version and operating system is important! Please try to replicate
|
||||||
your issue in a different browser and be sure to include your findings.
|
your issue in a different browser and be sure to include your findings.
|
||||||
@@ -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
|
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.
|
and attributes for all situations with a screenshot of the UI.
|
||||||
You can find this information at `/config/tools/state`
|
You can find this information at `/developer-tools/state`
|
||||||
-->
|
-->
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
name: Report a bug with the UI / Dashboards
|
name: Report a bug with the UI, Frontend or Lovelace
|
||||||
description: Report an issue related to the Home Assistant frontend.
|
description: Report an issue related to the Home Assistant frontend.
|
||||||
labels: bug
|
labels: bug
|
||||||
body:
|
body:
|
||||||
@@ -7,11 +7,11 @@ body:
|
|||||||
value: |
|
value: |
|
||||||
Make sure you are running the [latest version of Home Assistant][releases] before reporting an issue.
|
Make sure you are running the [latest version of Home Assistant][releases] before reporting an issue.
|
||||||
|
|
||||||
If you have a feature or enhancement request for the frontend, please [start a discussion][fr] instead of creating an issue.
|
If you have a feature or enhancement request for the frontend, please [start an discussion][fr] instead of creating an issue.
|
||||||
|
|
||||||
**Please do not report issues for custom cards.**
|
**Please not not report issues for custom Lovelace cards.**
|
||||||
|
|
||||||
[fr]: https://github.com/orgs/home-assistant/discussions
|
[fr]: https://github.com/home-assistant/frontend/discussions
|
||||||
[releases]: https://github.com/home-assistant/home-assistant/releases
|
[releases]: https://github.com/home-assistant/home-assistant/releases
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
attributes:
|
attributes:
|
||||||
@@ -24,7 +24,6 @@ body:
|
|||||||
required: true
|
required: true
|
||||||
- label: I have tried a different browser to see if it is related to my browser.
|
- label: I have tried a different browser to see if it is related to my browser.
|
||||||
required: true
|
required: true
|
||||||
- label: I have tried reproducing the issue in [safe mode](https://www.home-assistant.io/blog/2023/11/01/release-202311/#restarting-into-safe-mode) to rule out problems with unsupported custom resources.
|
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
@@ -65,7 +64,7 @@ body:
|
|||||||
label: What version of Home Assistant Core has the issue?
|
label: What version of Home Assistant Core has the issue?
|
||||||
placeholder: core-
|
placeholder: core-
|
||||||
description: >
|
description: >
|
||||||
Can be found in: [Settings -> About](https://my.home-assistant.io/redirect/info/).
|
Can be found in the Configuration panel -> Info.
|
||||||
- type: input
|
- type: input
|
||||||
attributes:
|
attributes:
|
||||||
label: What was the last working version of Home Assistant Core?
|
label: What was the last working version of Home Assistant Core?
|
||||||
@@ -74,7 +73,7 @@ body:
|
|||||||
If known, otherwise leave blank.
|
If known, otherwise leave blank.
|
||||||
- type: input
|
- type: input
|
||||||
attributes:
|
attributes:
|
||||||
label: In which browser are you experiencing the issue?
|
label: In which browser are you experiencing the issue with?
|
||||||
placeholder: Google Chrome 88.0.4324.150
|
placeholder: Google Chrome 88.0.4324.150
|
||||||
description: >
|
description: >
|
||||||
Provide the full name and don't forget to add the version!
|
Provide the full name and don't forget to add the version!
|
||||||
@@ -94,8 +93,8 @@ body:
|
|||||||
label: State of relevant entities
|
label: State of relevant entities
|
||||||
description: >
|
description: >
|
||||||
If your issue is about how an entity is shown in the UI, please add the
|
If your issue is about how an entity is shown in the UI, please add the
|
||||||
state and attributes for all situations. You can find this
|
state and attributes for all situations. You can find this information
|
||||||
information in the Details view of the More info dialog.
|
at Developer Tools -> States.
|
||||||
render: txt
|
render: txt
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
@@ -108,9 +107,9 @@ body:
|
|||||||
render: yaml
|
render: yaml
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: JavaScript errors shown in your browser console/inspector
|
label: Javascript errors shown in your browser console/inspector
|
||||||
description: >
|
description: >
|
||||||
If you come across any JavaScript or other error logs, e.g., in your
|
If you come across any Javascript or other error logs, e.g., in your
|
||||||
browser console/inspector please provide them.
|
browser console/inspector please provide them.
|
||||||
render: txt
|
render: txt
|
||||||
- type: textarea
|
- type: textarea
|
||||||
|
|||||||
@@ -1,20 +1,17 @@
|
|||||||
blank_issues_enabled: false
|
blank_issues_enabled: false
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: Request a feature for the UI / Dashboards
|
- name: Request a feature for the UI, Frontend or Lovelace
|
||||||
url: https://github.com/orgs/home-assistant/discussions
|
url: https://github.com/home-assistant/frontend/discussions/category_choices
|
||||||
about: Request a new feature for the Home Assistant frontend.
|
about: Request an new feature for the Home Assistant frontend.
|
||||||
- name: Discuss UI or UX design
|
- name: Report a bug that is NOT related to the UI, Frontend or Lovelace
|
||||||
url: https://github.com/OpenHomeFoundation/ux-design/discussions
|
|
||||||
about: Share design feedback and discuss visual or UX changes with the design team.
|
|
||||||
- name: Report a bug that is NOT related to the UI / Dashboards
|
|
||||||
url: https://github.com/home-assistant/core/issues
|
url: https://github.com/home-assistant/core/issues
|
||||||
about: This is the issue tracker for our frontend. Please report other issues in the backend ("core") repository.
|
about: This is the issue tracker for our frontend. Please report other issues with the backend repository.
|
||||||
- name: Report incorrect or missing information on our website
|
- name: Report incorrect or missing information on our website
|
||||||
url: https://github.com/home-assistant/home-assistant.io/issues
|
url: https://github.com/home-assistant/home-assistant.io/issues
|
||||||
about: Our documentation has its own issue tracker. Please report issues with the website there.
|
about: Our documentation has its own issue tracker. Please report issues with the website there.
|
||||||
- name: I have a question or need support
|
- name: I have a question or need support
|
||||||
url: https://www.home-assistant.io/help
|
url: https://www.home-assistant.io/help
|
||||||
about: We use GitHub for tracking bugs. Check our website for resources on getting help.
|
about: We use GitHub for tracking bugs, check our website for resources on getting help.
|
||||||
- name: I'm unsure where to go
|
- name: I'm unsure where to go
|
||||||
url: https://www.home-assistant.io/join-chat
|
url: https://www.home-assistant.io/join-chat
|
||||||
about: If you are unsure where to go, then joining our chat is recommended; Just ask!
|
about: If you are unsure where to go, then joining our chat is recommended; Just ask!
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
name: Task
|
|
||||||
description: For staff only - Create a task
|
|
||||||
type: Task
|
|
||||||
body:
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
## ⚠️ RESTRICTED ACCESS
|
|
||||||
|
|
||||||
**This form is restricted to Open Home Foundation staff and authorized contributors only.**
|
|
||||||
|
|
||||||
If you are a community member wanting to contribute, please:
|
|
||||||
- For bug reports: Use the [bug report form](https://github.com/home-assistant/frontend/issues/new?template=bug_report.yml)
|
|
||||||
- For feature requests: Submit to [Feature Requests](https://github.com/orgs/home-assistant/discussions)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### For authorized contributors
|
|
||||||
|
|
||||||
Use this form to create tasks for development work, improvements, or other actionable items that need to be tracked.
|
|
||||||
- type: textarea
|
|
||||||
id: description
|
|
||||||
attributes:
|
|
||||||
label: Description
|
|
||||||
description: |
|
|
||||||
Provide a clear and detailed description of the task that needs to be accomplished.
|
|
||||||
|
|
||||||
Be specific about what needs to be done, why it's important, and any constraints or requirements.
|
|
||||||
placeholder: |
|
|
||||||
Describe the task, including:
|
|
||||||
- What needs to be done
|
|
||||||
- Why this task is needed
|
|
||||||
- Expected outcome
|
|
||||||
- Any constraints or requirements
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: additional_context
|
|
||||||
attributes:
|
|
||||||
label: Additional context
|
|
||||||
description: |
|
|
||||||
Any additional information, links, research, or context that would be helpful.
|
|
||||||
|
|
||||||
Include links to related issues, research, prototypes, roadmap opportunities etc.
|
|
||||||
placeholder: |
|
|
||||||
- Roadmap opportunity: [link]
|
|
||||||
- Epic: [link]
|
|
||||||
- Feature request: [link]
|
|
||||||
- Technical design documents: [link]
|
|
||||||
- Prototype/mockup: [link]
|
|
||||||
- Dependencies: [links]
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
@@ -2,7 +2,9 @@
|
|||||||
You are amazing! Thanks for contributing to our project!
|
You are amazing! Thanks for contributing to our project!
|
||||||
Please, DO NOT DELETE ANY TEXT from this template! (unless instructed).
|
Please, DO NOT DELETE ANY TEXT from this template! (unless instructed).
|
||||||
-->
|
-->
|
||||||
|
|
||||||
## Breaking change
|
## Breaking change
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
If your PR contains a breaking change for existing users, it is important
|
If your PR contains a breaking change for existing users, it is important
|
||||||
to tell them what breaks, how to make it work again and why we did this.
|
to tell them what breaks, how to make it work again and why we did this.
|
||||||
@@ -11,8 +13,8 @@
|
|||||||
Note: Remove this section if this PR is NOT a breaking change.
|
Note: Remove this section if this PR is NOT a breaking change.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
|
||||||
## Proposed change
|
## Proposed change
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
Describe the big picture of your changes here to communicate to the
|
Describe the big picture of your changes here to communicate to the
|
||||||
maintainers why we should accept this pull request. If it fixes a bug
|
maintainers why we should accept this pull request. If it fixes a bug
|
||||||
@@ -20,16 +22,8 @@
|
|||||||
in the additional information section.
|
in the additional information section.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
|
||||||
## 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
|
## Type of change
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
What type of change does your PR introduce to the Home Assistant frontend?
|
What type of change does your PR introduce to the Home Assistant frontend?
|
||||||
NOTE: Please, check only 1! box!
|
NOTE: Please, check only 1! box!
|
||||||
@@ -43,7 +37,19 @@
|
|||||||
- [ ] Breaking change (fix/feature causing existing functionality to break)
|
- [ ] Breaking change (fix/feature causing existing functionality to break)
|
||||||
- [ ] Code quality improvements to existing code or addition of tests
|
- [ ] 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
|
## Additional information
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
Details are important, and help maintainers processing your PR.
|
Details are important, and help maintainers processing your PR.
|
||||||
Please be sure to fill out additional details, if applicable.
|
Please be sure to fill out additional details, if applicable.
|
||||||
@@ -52,57 +58,26 @@
|
|||||||
- This PR fixes or closes issue: fixes #
|
- This PR fixes or closes issue: fixes #
|
||||||
- This PR is related to issue or discussion:
|
- This PR is related to issue or discussion:
|
||||||
- Link to documentation pull request:
|
- Link to documentation pull request:
|
||||||
- Link to developer documentation pull request:
|
|
||||||
- Link to backend pull request:
|
|
||||||
|
|
||||||
## Checklist
|
## Checklist
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
Put an `x` in the boxes that apply. You can also fill these out after
|
Put an `x` in the boxes that apply. You can also fill these out after
|
||||||
creating the PR. If you're unsure about any of them, don't hesitate to ask.
|
creating the PR. If you're unsure about any of them, don't hesitate to ask.
|
||||||
We're here to help! This is simply a reminder of what we are going to look
|
We're here to help! This is simply a reminder of what we are going to look
|
||||||
for before merging your code.
|
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.
|
- [ ] The code change is tested and works locally.
|
||||||
- [ ] There is no commented out code in this PR.
|
- [ ] There is no commented out code in this PR.
|
||||||
- [ ] I have followed the [perfect PR recommendations][perfect-pr]
|
- [ ] Tests have been added to verify that the new code works.
|
||||||
- [ ] Any generated code has been carefully reviewed for correctness and compliance with project standards.
|
|
||||||
|
|
||||||
If user exposed functionality or configuration variables are added/changed:
|
If user exposed functionality or configuration variables are added/changed:
|
||||||
|
|
||||||
- [ ] Documentation added/updated for [www.home-assistant.io][docs-repository]
|
- [ ] 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
|
Thank you for contributing <3
|
||||||
|
|
||||||
Below, some useful links you could explore:
|
|
||||||
-->
|
-->
|
||||||
|
|
||||||
[docs-repository]: https://github.com/home-assistant/home-assistant.io
|
[docs-repository]: https://github.com/home-assistant/home-assistant.io
|
||||||
[perfect-pr]: https://developers.home-assistant.io/docs/review-process/#creating-the-perfect-pr
|
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
name: Build frontend target
|
|
||||||
description: Run a gulp build target
|
|
||||||
|
|
||||||
inputs:
|
|
||||||
target:
|
|
||||||
description: gulp target to run
|
|
||||||
required: true
|
|
||||||
github-token:
|
|
||||||
description: GitHub token for fetching nightly translations; omit to build English-only
|
|
||||||
default: ""
|
|
||||||
is-test:
|
|
||||||
description: Set IS_TEST for the build (skips source maps and compression)
|
|
||||||
default: "false"
|
|
||||||
|
|
||||||
runs:
|
|
||||||
using: composite
|
|
||||||
steps:
|
|
||||||
- name: Build ${{ inputs.target }}
|
|
||||||
shell: bash
|
|
||||||
run: ./node_modules/.bin/gulp ${{ inputs.target }}
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ inputs.github-token }}
|
|
||||||
IS_TEST: ${{ inputs.is-test }}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
name: Deploy to Netlify
|
|
||||||
description: Deploy a directory to Netlify (production when alias is empty, otherwise to the alias)
|
|
||||||
|
|
||||||
inputs:
|
|
||||||
dir:
|
|
||||||
description: Directory to deploy
|
|
||||||
required: true
|
|
||||||
alias:
|
|
||||||
description: Deploy alias; leave empty to deploy to production
|
|
||||||
default: ""
|
|
||||||
auth-token:
|
|
||||||
description: NETLIFY_AUTH_TOKEN
|
|
||||||
required: true
|
|
||||||
site-id:
|
|
||||||
description: NETLIFY_SITE_ID
|
|
||||||
required: true
|
|
||||||
|
|
||||||
outputs:
|
|
||||||
netlify_url:
|
|
||||||
description: The deployed URL
|
|
||||||
value: ${{ steps.deploy.outputs.netlify_url }}
|
|
||||||
unique_deploy_url:
|
|
||||||
description: The unique hash-based deployment URL for this specific build
|
|
||||||
value: ${{ steps.deploy.outputs.unique_deploy_url }}
|
|
||||||
|
|
||||||
runs:
|
|
||||||
using: composite
|
|
||||||
steps:
|
|
||||||
- name: Deploy to Netlify
|
|
||||||
id: deploy
|
|
||||||
shell: bash
|
|
||||||
env:
|
|
||||||
DIR: ${{ inputs.dir }}
|
|
||||||
ALIAS: ${{ inputs.alias }}
|
|
||||||
NETLIFY_AUTH_TOKEN: ${{ inputs.auth-token }}
|
|
||||||
NETLIFY_SITE_ID: ${{ inputs.site-id }}
|
|
||||||
run: |
|
|
||||||
# Execute the deployment
|
|
||||||
if [ -n "$ALIAS" ]; then
|
|
||||||
npx -y netlify-cli deploy --dir="$DIR" --alias "$ALIAS" --json > deploy_output.json
|
|
||||||
else
|
|
||||||
npx -y netlify-cli deploy --dir="$DIR" --prod --json > deploy_output.json
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Collect the urls from the deployment output
|
|
||||||
NETLIFY_URL=$(jq -r '.url // .deploy_url' deploy_output.json)
|
|
||||||
SITE_NAME=$(jq -r '.site_name' deploy_output.json)
|
|
||||||
DEPLOY_ID=$(jq -r '.deploy_id' deploy_output.json)
|
|
||||||
UNIQUE_URL="https://${DEPLOY_ID}--${SITE_NAME}.netlify.app"
|
|
||||||
|
|
||||||
echo "netlify_url=$NETLIFY_URL" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "unique_deploy_url=$UNIQUE_URL" >> "$GITHUB_OUTPUT"
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
name: Setup Node and install
|
|
||||||
description: Set up Node from .nvmrc and install yarn dependencies
|
|
||||||
|
|
||||||
inputs:
|
|
||||||
immutable:
|
|
||||||
description: Pass --immutable to yarn install
|
|
||||||
default: "true"
|
|
||||||
cache:
|
|
||||||
description: Enable the yarn cache in setup-node
|
|
||||||
default: "true"
|
|
||||||
node-modules-cache:
|
|
||||||
description: Restore the exact shared node_modules cache before installing
|
|
||||||
default: "false"
|
|
||||||
|
|
||||||
runs:
|
|
||||||
using: composite
|
|
||||||
steps:
|
|
||||||
- name: Restore complete dependency tree
|
|
||||||
id: dependency-cache
|
|
||||||
if: inputs.node-modules-cache == 'true'
|
|
||||||
continue-on-error: true
|
|
||||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
node_modules
|
|
||||||
.yarn/install-state.gz
|
|
||||||
key: >-
|
|
||||||
node-modules-v1-${{ runner.os }}-${{ runner.arch }}-${{
|
|
||||||
hashFiles('.nvmrc', 'package.json', 'yarn.lock', '.yarnrc.yml', '.yarn/releases/**', '.yarn/patches/**') }}
|
|
||||||
|
|
||||||
- name: Setup Node
|
|
||||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
||||||
with:
|
|
||||||
node-version-file: ".nvmrc"
|
|
||||||
cache: ${{ inputs.cache == 'true' && (inputs.node-modules-cache != 'true' || steps.dependency-cache.outputs.cache-hit != 'true') && 'yarn' || '' }}
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
if: inputs.node-modules-cache != 'true' || steps.dependency-cache.outputs.cache-hit != 'true'
|
|
||||||
shell: bash
|
|
||||||
run: yarn install ${{ inputs.immutable == 'true' && '--immutable' || '' }}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
../AGENTS.md
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
version: 2
|
|
||||||
updates:
|
|
||||||
- package-ecosystem: "github-actions"
|
|
||||||
# Dependabot only scans .github/workflows by default; composite actions
|
|
||||||
# under .github/actions must be listed explicitly to stay updated.
|
|
||||||
# https://github.com/dependabot/dependabot-core/issues/6704
|
|
||||||
directories:
|
|
||||||
- "/"
|
|
||||||
- "/.github/actions/setup"
|
|
||||||
- "/.github/actions/build"
|
|
||||||
- "/.github/actions/netlify-deploy"
|
|
||||||
schedule:
|
|
||||||
interval: weekly
|
|
||||||
time: "06:00"
|
|
||||||
cooldown:
|
|
||||||
default-days: 7
|
|
||||||
open-pull-requests-limit: 10
|
|
||||||
labels:
|
|
||||||
- Dependencies
|
|
||||||
- GitHub Actions
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
Build:
|
|
||||||
- changed-files:
|
|
||||||
- any-glob-to-any-file:
|
|
||||||
- build-scripts/**
|
|
||||||
- .browserslistrc
|
|
||||||
- gulpfile.js
|
|
||||||
|
|
||||||
Cast:
|
|
||||||
- changed-files:
|
|
||||||
- any-glob-to-any-file:
|
|
||||||
- cast/src/**
|
|
||||||
- src/cast/**
|
|
||||||
|
|
||||||
Demo:
|
|
||||||
- changed-files:
|
|
||||||
- any-glob-to-any-file:
|
|
||||||
- demo/src/**
|
|
||||||
- src/fake_data/**
|
|
||||||
|
|
||||||
Design:
|
|
||||||
- changed-files:
|
|
||||||
- any-glob-to-any-file:
|
|
||||||
- gallery/src/**
|
|
||||||
- src/fake_data/**
|
|
||||||
|
|
||||||
Dependencies:
|
|
||||||
- any:
|
|
||||||
- changed-files:
|
|
||||||
# Match when only these files are changed (i.e. don't match PRs that happen to add or remove packages)
|
|
||||||
- any-glob-to-all-files:
|
|
||||||
- package.json
|
|
||||||
- renovate.json
|
|
||||||
- yarn.lock
|
|
||||||
- .yarn/**
|
|
||||||
- .yarnrc.yml
|
|
||||||
- .nvmrc
|
|
||||||
# Dependabot and Renovate branches always match (i.e. compatibility tweaks by members considered minor)
|
|
||||||
- head-branch:
|
|
||||||
- "^renovate/"
|
|
||||||
- "^dependabot/"
|
|
||||||
|
|
||||||
GitHub Actions:
|
|
||||||
- changed-files:
|
|
||||||
- any-glob-to-any-file:
|
|
||||||
- .github/actions/**
|
|
||||||
- .github/workflows/**
|
|
||||||
- .github/*.yml
|
|
||||||
|
|
||||||
"Tests: E2E":
|
|
||||||
- changed-files:
|
|
||||||
- any-glob-to-any-file:
|
|
||||||
- test/e2e/**
|
|
||||||
|
|
||||||
"Tests: App":
|
|
||||||
- changed-files:
|
|
||||||
- any-glob-to-any-file:
|
|
||||||
- test/e2e/app.spec.ts
|
|
||||||
- test/e2e/app/**
|
|
||||||
- test/e2e/playwright.app.config.ts
|
|
||||||
|
|
||||||
"Tests: Demo":
|
|
||||||
- changed-files:
|
|
||||||
- any-glob-to-any-file:
|
|
||||||
- test/e2e/demo.spec.ts
|
|
||||||
- test/e2e/playwright.demo.config.ts
|
|
||||||
|
|
||||||
"Tests: Design":
|
|
||||||
- changed-files:
|
|
||||||
- any-glob-to-any-file:
|
|
||||||
- test/e2e/gallery.spec.ts
|
|
||||||
- test/e2e/playwright.gallery.config.ts
|
|
||||||
|
|
||||||
"Tests: Unit":
|
|
||||||
- changed-files:
|
|
||||||
- any-glob-to-any-file:
|
|
||||||
- test/*.ts
|
|
||||||
- "test/!(e2e)/**"
|
|
||||||
@@ -1,8 +1,3 @@
|
|||||||
categories:
|
|
||||||
- title: "Dependency updates"
|
|
||||||
collapse-after: 3
|
|
||||||
labels:
|
|
||||||
- "Dependencies"
|
|
||||||
template: |
|
template: |
|
||||||
## What's Changed
|
## What's Changed
|
||||||
|
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
// Fails the check when a pull request carries a label that blocks merging, and
|
|
||||||
// writes the outcome to the job summary. Invoked from the `check` job in
|
|
||||||
// .github/workflows/blocking-labels.yaml via actions/github-script:
|
|
||||||
//
|
|
||||||
// const { default: checkBlockingLabels } =
|
|
||||||
// await import(`${process.env.GITHUB_WORKSPACE}/.github/scripts/check-blocking-labels.mjs`);
|
|
||||||
// await checkBlockingLabels({ github, context, core });
|
|
||||||
|
|
||||||
export default async function checkBlockingLabels({ context, core }) {
|
|
||||||
const blockingLabels = [
|
|
||||||
"wait for backend",
|
|
||||||
"Needs UX",
|
|
||||||
"Do Not Review",
|
|
||||||
"Blocked",
|
|
||||||
"has-parent",
|
|
||||||
];
|
|
||||||
const prLabels = context.payload.pull_request.labels.map((l) => l.name);
|
|
||||||
const found = blockingLabels.filter((bl) => prLabels.includes(bl));
|
|
||||||
if (found.length > 0) {
|
|
||||||
const message = `This Pull Request is blocked by label${found.length > 1 ? "s" : ""}: ${found.join(", ")}`;
|
|
||||||
await core.summary
|
|
||||||
.addHeading(":no_entry_sign: Pull Request is blocked", 2)
|
|
||||||
.addRaw(message)
|
|
||||||
.write();
|
|
||||||
core.setFailed(message);
|
|
||||||
} else {
|
|
||||||
await core.summary
|
|
||||||
.addHeading(
|
|
||||||
":white_check_mark: Pull Request is clear to merge after review",
|
|
||||||
2
|
|
||||||
)
|
|
||||||
.addRaw(
|
|
||||||
"This Pull Request is not blocked by any labels which prevent it from being merged."
|
|
||||||
)
|
|
||||||
.write();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,195 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
// Checks that a pull request follows the contribution standards: it must use the
|
|
||||||
// PR template, tick exactly one "Type of change" option, and describe the change.
|
|
||||||
// Labels and comments the PR when it does not, and fails the check so it blocks
|
|
||||||
// merging. Invoked from the `check` job in .github/workflows/pull-request-standards.yaml
|
|
||||||
// via actions/github-script:
|
|
||||||
//
|
|
||||||
// const { default: checkStandards } =
|
|
||||||
// await import(`${process.env.GITHUB_WORKSPACE}/.github/scripts/check-pull-request-standards.mjs`);
|
|
||||||
// await checkStandards({ github, context, core });
|
|
||||||
|
|
||||||
export default async function checkPullRequestStandards({
|
|
||||||
github,
|
|
||||||
context,
|
|
||||||
core,
|
|
||||||
}) {
|
|
||||||
const pr = context.payload.pull_request;
|
|
||||||
|
|
||||||
// Exempt bots (Copilot agent, dependabot), drafts, and maintainers.
|
|
||||||
if (pr.user.type === "Bot") {
|
|
||||||
core.info(`Skipping bot author: ${pr.user.login}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (pr.draft) {
|
|
||||||
core.info("Skipping draft pull request");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await github.rest.orgs.checkMembershipForUser({
|
|
||||||
org: "home-assistant",
|
|
||||||
username: pr.user.login,
|
|
||||||
});
|
|
||||||
core.info(`Skipping organization member: ${pr.user.login}`);
|
|
||||||
return;
|
|
||||||
} catch (_error) {
|
|
||||||
core.info(
|
|
||||||
`${pr.user.login} is not an organization member, checking standards`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const label = "Needs Template";
|
|
||||||
const marker = "<!-- pr-standards-check -->";
|
|
||||||
const { owner, repo } = context.repo;
|
|
||||||
const issue_number = pr.number;
|
|
||||||
|
|
||||||
let body = pr.body || "";
|
|
||||||
let previous;
|
|
||||||
do {
|
|
||||||
previous = body;
|
|
||||||
body = body.replace(/<!--[\s\S]*?-->/g, "");
|
|
||||||
} while (body !== previous);
|
|
||||||
const normalized = body.toLowerCase();
|
|
||||||
|
|
||||||
// Ignore 404s from mutations that race manual edits or cancelled runs.
|
|
||||||
const ignoreMissing = async (fn) => {
|
|
||||||
try {
|
|
||||||
await fn();
|
|
||||||
} catch (error) {
|
|
||||||
if (error.status === 404) {
|
|
||||||
core.info("Target already removed, nothing to do");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Hide/restore our comment via GraphQL (REST cannot minimize).
|
|
||||||
const setMinimized = async (subjectId, minimized) => {
|
|
||||||
const mutation = minimized
|
|
||||||
? `mutation($id: ID!) {
|
|
||||||
minimizeComment(input: { subjectId: $id, classifier: RESOLVED }) {
|
|
||||||
clientMutationId
|
|
||||||
}
|
|
||||||
}`
|
|
||||||
: `mutation($id: ID!) {
|
|
||||||
unminimizeComment(input: { subjectId: $id }) {
|
|
||||||
clientMutationId
|
|
||||||
}
|
|
||||||
}`;
|
|
||||||
try {
|
|
||||||
await github.graphql(mutation, { id: subjectId });
|
|
||||||
} catch (error) {
|
|
||||||
core.info(
|
|
||||||
`Could not ${minimized ? "minimize" : "restore"} comment: ${error.message}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Content of a "## <name>" section, or null when the heading is absent.
|
|
||||||
const section = (name) => {
|
|
||||||
const match = body.match(
|
|
||||||
new RegExp(`##\\s${name}([\\s\\S]*?)(?=\\n##\\s|$)`, "i")
|
|
||||||
);
|
|
||||||
return match ? match[1] : null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const problems = [];
|
|
||||||
|
|
||||||
const requiredHeadings = [
|
|
||||||
"## proposed change",
|
|
||||||
"## type of change",
|
|
||||||
"## checklist",
|
|
||||||
];
|
|
||||||
if (requiredHeadings.some((h) => !normalized.includes(h))) {
|
|
||||||
problems.push(
|
|
||||||
"Use the pull request template without removing its sections."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const typeOfChange = section("type of change");
|
|
||||||
if (typeOfChange !== null) {
|
|
||||||
const ticked = (typeOfChange.match(/-\s*\[[xX]\]/g) || []).length;
|
|
||||||
if (ticked !== 1) {
|
|
||||||
problems.push('Select exactly one option under "Type of change".');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const proposedChange = section("proposed change");
|
|
||||||
if (proposedChange !== null && proposedChange.trim().length === 0) {
|
|
||||||
problems.push('Describe your changes under "Proposed change".');
|
|
||||||
}
|
|
||||||
|
|
||||||
const isValid = problems.length === 0;
|
|
||||||
|
|
||||||
const comments = await github.paginate(github.rest.issues.listComments, {
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
issue_number,
|
|
||||||
per_page: 100,
|
|
||||||
});
|
|
||||||
const existing = comments.find((c) => c.body.includes(marker));
|
|
||||||
const hasLabel = pr.labels.some((l) => l.name === label);
|
|
||||||
|
|
||||||
if (isValid) {
|
|
||||||
core.info("Pull request standards met");
|
|
||||||
|
|
||||||
if (hasLabel) {
|
|
||||||
await ignoreMissing(() =>
|
|
||||||
github.rest.issues.removeLabel({
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
issue_number,
|
|
||||||
name: label,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (existing) {
|
|
||||||
await setMinimized(existing.node_id, true);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
core.info(`Pull request standards not met:\n- ${problems.join("\n- ")}`);
|
|
||||||
|
|
||||||
if (!hasLabel) {
|
|
||||||
await github.rest.issues.addLabels({
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
issue_number,
|
|
||||||
labels: [label],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const message =
|
|
||||||
`${marker}\n` +
|
|
||||||
`Hey @${pr.user.login}!\n\n` +
|
|
||||||
`Thank you for your contribution! To help reviewers, please update ` +
|
|
||||||
`this pull request to follow our pull request standards:\n\n` +
|
|
||||||
problems.map((p) => `- ${p}`).join("\n") +
|
|
||||||
`\n\n` +
|
|
||||||
`Please complete the ` +
|
|
||||||
`[PR template](https://github.com/home-assistant/frontend/blob/dev/.github/PULL_REQUEST_TEMPLATE.md?plain=1) ` +
|
|
||||||
`and see the [developer docs](https://developers.home-assistant.io/docs/review-process) ` +
|
|
||||||
`for more on creating a great pull request (see point 6).`;
|
|
||||||
|
|
||||||
if (existing) {
|
|
||||||
await github.rest.issues.updateComment({
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
comment_id: existing.id,
|
|
||||||
body: message,
|
|
||||||
});
|
|
||||||
await setMinimized(existing.node_id, false);
|
|
||||||
} else {
|
|
||||||
await github.rest.issues.createComment({
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
issue_number,
|
|
||||||
body: message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fail this check so it can block the PR from being merged
|
|
||||||
core.setFailed(`Pull request standards not met:\n- ${problems.join("\n- ")}`);
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
// Restricts Task issues to organization members: closes and labels the issue with
|
|
||||||
// an explanatory comment when the author is not an org member. Invoked from the
|
|
||||||
// `check-authorization` job in .github/workflows/restrict-task-creation.yaml via
|
|
||||||
// actions/github-script:
|
|
||||||
//
|
|
||||||
// const { default: checkTaskAuthorization } =
|
|
||||||
// await import(`${process.env.GITHUB_WORKSPACE}/.github/scripts/check-task-authorization.mjs`);
|
|
||||||
// await checkTaskAuthorization({ github, context, core });
|
|
||||||
|
|
||||||
export default async function checkTaskAuthorization({
|
|
||||||
github,
|
|
||||||
context,
|
|
||||||
core,
|
|
||||||
}) {
|
|
||||||
const issueAuthor = context.payload.issue.user.login;
|
|
||||||
|
|
||||||
// Check if user is an organization member
|
|
||||||
try {
|
|
||||||
await github.rest.orgs.checkMembershipForUser({
|
|
||||||
org: "home-assistant",
|
|
||||||
username: issueAuthor,
|
|
||||||
});
|
|
||||||
core.info(`✅ ${issueAuthor} is an organization member`);
|
|
||||||
return; // Authorized
|
|
||||||
} catch (_error) {
|
|
||||||
core.info(`❌ ${issueAuthor} is not authorized to create Task issues`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close the issue with a comment
|
|
||||||
await github.rest.issues.createComment({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: context.issue.number,
|
|
||||||
body:
|
|
||||||
`Hi @${issueAuthor}, thank you for your contribution!\n\n` +
|
|
||||||
`Task issues are restricted to Open Home Foundation staff and authorized contributors.\n\n` +
|
|
||||||
`If you would like to:\n` +
|
|
||||||
`- Report a bug: Please use the [bug report form](https://github.com/home-assistant/frontend/issues/new?template=bug_report.yml)\n` +
|
|
||||||
`- Request a feature: Please submit to [Feature Requests](https://github.com/orgs/home-assistant/discussions)\n\n` +
|
|
||||||
`If you believe you should have access to create Task issues, please contact the maintainers.`,
|
|
||||||
});
|
|
||||||
|
|
||||||
await github.rest.issues.update({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: context.issue.number,
|
|
||||||
state: "closed",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add a label to indicate this was auto-closed
|
|
||||||
await github.rest.issues.addLabels({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: context.issue.number,
|
|
||||||
labels: ["auto-closed"],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
name: Lint workflow files
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- dev
|
|
||||||
- master
|
|
||||||
paths:
|
|
||||||
- ".github/actions/**"
|
|
||||||
- ".github/workflows/**"
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- dev
|
|
||||||
- master
|
|
||||||
paths:
|
|
||||||
- ".github/actions/**"
|
|
||||||
- ".github/workflows/**"
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
actionlint:
|
|
||||||
name: Check workflow files
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
env:
|
|
||||||
# renovate: datasource=github-releases depName=rhysd/actionlint
|
|
||||||
ACTIONLINT_VERSION: 1.7.12
|
|
||||||
steps:
|
|
||||||
- name: Check out files from GitHub
|
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
- name: Run actionlint
|
|
||||||
run: |
|
|
||||||
curl -sSfL "https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz" \
|
|
||||||
| tar -xz actionlint
|
|
||||||
./actionlint -color
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
name: Blocking labels
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types:
|
|
||||||
- opened
|
|
||||||
- synchronize
|
|
||||||
- reopened
|
|
||||||
- labeled
|
|
||||||
- unlabeled
|
|
||||||
branches:
|
|
||||||
- dev
|
|
||||||
- master
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
check:
|
|
||||||
name: Check for labels which block the Pull Request from being merged
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Check out workflow scripts
|
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
sparse-checkout: .github/scripts
|
|
||||||
- name: Check for blocking labels
|
|
||||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
const { default: checkBlockingLabels } = await import(
|
|
||||||
`${process.env.GITHUB_WORKSPACE}/.github/scripts/check-blocking-labels.mjs`
|
|
||||||
);
|
|
||||||
await checkBlockingLabels({ github, context, core });
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
name: Cast deployment
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
schedule:
|
|
||||||
- cron: "0 0 * * *"
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
env:
|
|
||||||
NODE_OPTIONS: --max_old_space_size=6144
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
deploy_dev:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: Deploy Development
|
|
||||||
if: github.event_name != 'push'
|
|
||||||
environment:
|
|
||||||
name: Cast Development
|
|
||||||
url: ${{ steps.deploy.outputs.netlify_url }}
|
|
||||||
steps:
|
|
||||||
- name: Check out files from GitHub
|
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
||||||
with:
|
|
||||||
ref: dev
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Setup Node and install
|
|
||||||
uses: ./.github/actions/setup
|
|
||||||
|
|
||||||
- name: Build Cast
|
|
||||||
uses: ./.github/actions/build
|
|
||||||
with:
|
|
||||||
target: build-cast
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Deploy to Netlify
|
|
||||||
id: deploy
|
|
||||||
uses: ./.github/actions/netlify-deploy
|
|
||||||
with:
|
|
||||||
dir: cast/dist
|
|
||||||
alias: dev
|
|
||||||
auth-token: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
|
||||||
site-id: ${{ secrets.NETLIFY_CAST_SITE_ID }}
|
|
||||||
|
|
||||||
deploy_master:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: Deploy Production
|
|
||||||
if: github.event_name == 'push'
|
|
||||||
environment:
|
|
||||||
name: Cast Production
|
|
||||||
url: ${{ steps.deploy.outputs.netlify_url }}
|
|
||||||
steps:
|
|
||||||
- name: Check out files from GitHub
|
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
||||||
with:
|
|
||||||
ref: master
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Setup Node and install
|
|
||||||
uses: ./.github/actions/setup
|
|
||||||
|
|
||||||
- name: Build Cast
|
|
||||||
uses: ./.github/actions/build
|
|
||||||
with:
|
|
||||||
target: build-cast
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Deploy to Netlify
|
|
||||||
id: deploy
|
|
||||||
uses: ./.github/actions/netlify-deploy
|
|
||||||
with:
|
|
||||||
dir: cast/dist
|
|
||||||
auth-token: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
|
||||||
site-id: ${{ secrets.NETLIFY_CAST_SITE_ID }}
|
|
||||||
+57
-122
@@ -1,7 +1,6 @@
|
|||||||
name: CI
|
name: CI
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- dev
|
- dev
|
||||||
@@ -12,149 +11,85 @@ on:
|
|||||||
- master
|
- master
|
||||||
|
|
||||||
env:
|
env:
|
||||||
|
NODE_VERSION: 14
|
||||||
NODE_OPTIONS: --max_old_space_size=6144
|
NODE_OPTIONS: --max_old_space_size=6144
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
prepare-dependencies:
|
|
||||||
name: Prepare dependencies
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Check out files from GitHub
|
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
- name: Check for complete dependency tree
|
|
||||||
id: dependencies
|
|
||||||
continue-on-error: true
|
|
||||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
node_modules
|
|
||||||
.yarn/install-state.gz
|
|
||||||
key: >-
|
|
||||||
node-modules-v1-${{ runner.os }}-${{ runner.arch }}-${{
|
|
||||||
hashFiles('.nvmrc', 'package.json', 'yarn.lock', '.yarnrc.yml', '.yarn/releases/**', '.yarn/patches/**') }}
|
|
||||||
lookup-only: true
|
|
||||||
- name: Setup Node and install
|
|
||||||
if: steps.dependencies.outputs.cache-hit != 'true'
|
|
||||||
uses: ./.github/actions/setup
|
|
||||||
with:
|
|
||||||
cache: false
|
|
||||||
- name: Save complete dependency tree
|
|
||||||
if: steps.dependencies.outputs.cache-hit != 'true'
|
|
||||||
continue-on-error: true
|
|
||||||
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
node_modules
|
|
||||||
.yarn/install-state.gz
|
|
||||||
key: >-
|
|
||||||
node-modules-v1-${{ runner.os }}-${{ runner.arch }}-${{
|
|
||||||
hashFiles('.nvmrc', 'package.json', 'yarn.lock', '.yarnrc.yml', '.yarn/releases/**', '.yarn/patches/**') }}
|
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
name: Lint and check format
|
|
||||||
needs: prepare-dependencies
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@v2
|
||||||
|
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||||
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
- name: Setup Node with shared dependencies
|
cache: yarn
|
||||||
uses: ./.github/actions/setup
|
- name: Install dependencies
|
||||||
with:
|
run: yarn install
|
||||||
node-modules-cache: true
|
env:
|
||||||
|
CI: true
|
||||||
|
- name: Build resources
|
||||||
|
run: ./node_modules/.bin/gulp gen-icons-json build-translations gather-gallery-demos
|
||||||
|
- name: Run eslint
|
||||||
|
run: yarn run lint:eslint
|
||||||
|
- name: Run tsc
|
||||||
|
run: yarn run lint:types
|
||||||
|
- name: Run prettier
|
||||||
|
run: yarn run lint:prettier
|
||||||
- name: Check for duplicate dependencies
|
- name: Check for duplicate dependencies
|
||||||
run: yarn dedupe --check
|
run: yarn dedupe --check
|
||||||
- name: Build resources
|
|
||||||
id: build_resources
|
|
||||||
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
- name: Setup lint cache
|
|
||||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
node_modules/.cache/prettier
|
|
||||||
node_modules/.cache/eslint
|
|
||||||
node_modules/.cache/typescript
|
|
||||||
key: lint-${{ github.sha }}
|
|
||||||
restore-keys: lint-
|
|
||||||
- name: Run eslint
|
|
||||||
run: yarn run lint:eslint --quiet
|
|
||||||
- name: Run tsc
|
|
||||||
if: ${{ !cancelled() && steps.build_resources.outcome == 'success' }}
|
|
||||||
run: yarn run lint:types
|
|
||||||
- name: Run lit-analyzer
|
|
||||||
if: ${{ !cancelled() && steps.build_resources.outcome == 'success' }}
|
|
||||||
run: yarn run lint:lit --quiet
|
|
||||||
- name: Run prettier
|
|
||||||
if: ${{ !cancelled() && steps.build_resources.outcome == 'success' }}
|
|
||||||
run: yarn run lint:prettier
|
|
||||||
- name: Check dependency licenses
|
|
||||||
if: ${{ !cancelled() && steps.build_resources.outcome == 'success' }}
|
|
||||||
run: yarn run lint:licenses
|
|
||||||
test:
|
test:
|
||||||
name: Run tests
|
|
||||||
needs: prepare-dependencies
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@v2
|
||||||
|
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||||
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
- name: Setup Node with shared dependencies
|
cache: yarn
|
||||||
uses: ./.github/actions/setup
|
- name: Install dependencies
|
||||||
with:
|
run: yarn install
|
||||||
node-modules-cache: true
|
|
||||||
- name: Build resources
|
|
||||||
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data
|
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
CI: true
|
||||||
- name: Run Tests
|
- name: Run Tests
|
||||||
run: yarn run test
|
run: yarn run test
|
||||||
build:
|
build:
|
||||||
name: Build frontend
|
|
||||||
needs:
|
|
||||||
- prepare-dependencies
|
|
||||||
- lint
|
|
||||||
- test
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
needs: [lint, test]
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@v2
|
||||||
|
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||||
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
- name: Setup Node with shared dependencies
|
cache: yarn
|
||||||
uses: ./.github/actions/setup
|
- name: Install dependencies
|
||||||
with:
|
run: yarn install
|
||||||
node-modules-cache: true
|
env:
|
||||||
|
CI: true
|
||||||
- name: Build Application
|
- name: Build Application
|
||||||
uses: ./.github/actions/build
|
run: ./node_modules/.bin/gulp build-app
|
||||||
|
env:
|
||||||
|
IS_TEST: "true"
|
||||||
|
supervisor:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [lint, test]
|
||||||
|
steps:
|
||||||
|
- name: Check out files from GitHub
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||||
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
target: build-app
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
cache: yarn
|
||||||
is-test: true
|
- name: Install dependencies
|
||||||
- name: Upload bundle stats
|
run: yarn install
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
env:
|
||||||
with:
|
CI: true
|
||||||
name: frontend-bundle-stats
|
- name: Build Application
|
||||||
path: build/stats/*.json
|
run: ./node_modules/.bin/gulp build-hassio
|
||||||
if-no-files-found: error
|
env:
|
||||||
- name: Check entrypoint bundle size budget
|
IS_TEST: "true"
|
||||||
run: yarn run check-bundlesize
|
|
||||||
- name: Upload frontend build
|
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
||||||
with:
|
|
||||||
name: frontend-build
|
|
||||||
path: hass_frontend/
|
|
||||||
if-no-files-found: error
|
|
||||||
retention-days: 7
|
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
name: "CodeQL"
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- dev
|
|
||||||
- master
|
|
||||||
pull_request:
|
|
||||||
# The branches below must be a subset of the branches above
|
|
||||||
branches:
|
|
||||||
- dev
|
|
||||||
|
|
||||||
permissions: {}
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
analyze:
|
|
||||||
name: Analyze
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 360
|
|
||||||
permissions:
|
|
||||||
contents: read # To check out the repository
|
|
||||||
security-events: write # To upload CodeQL results
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Check out code from GitHub
|
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
|
||||||
uses: github/codeql-action/init@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
|
|
||||||
with:
|
|
||||||
languages: javascript-typescript
|
|
||||||
build-mode: none
|
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
|
||||||
uses: github/codeql-action/analyze@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
|
|
||||||
with:
|
|
||||||
category: "/language:javascript-typescript"
|
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
name: "CodeQL"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [dev, master]
|
||||||
|
pull_request:
|
||||||
|
# The branches below must be a subset of the branches above
|
||||||
|
branches: [dev]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analyze:
|
||||||
|
name: Analyze
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
# Override automatic language detection by changing the below list
|
||||||
|
# Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
|
||||||
|
language: ['javascript']
|
||||||
|
# Learn more...
|
||||||
|
# https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
# We must fetch at least the immediate parents so that if this is
|
||||||
|
# a pull request then we can checkout the head.
|
||||||
|
fetch-depth: 2
|
||||||
|
|
||||||
|
# If this run was triggered by a pull request event, then checkout
|
||||||
|
# the head of the pull request instead of the merge commit.
|
||||||
|
- run: git checkout HEAD^2
|
||||||
|
if: ${{ github.event_name == 'pull_request' }}
|
||||||
|
|
||||||
|
# Initializes the CodeQL tools for scanning.
|
||||||
|
- name: Initialize CodeQL
|
||||||
|
uses: github/codeql-action/init@v1
|
||||||
|
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@v1
|
||||||
|
|
||||||
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
|
# 📚 https://git.io/JvXDl
|
||||||
|
|
||||||
|
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||||
|
# and modify them (or add more) to build your code if your project
|
||||||
|
# uses a compiled language
|
||||||
|
|
||||||
|
#- run: |
|
||||||
|
# make bootstrap
|
||||||
|
# make release
|
||||||
|
|
||||||
|
- name: Perform CodeQL Analysis
|
||||||
|
uses: github/codeql-action/analyze@v1
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
name: Demo
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- dev
|
||||||
|
|
||||||
|
env:
|
||||||
|
NODE_VERSION: 14
|
||||||
|
NODE_OPTIONS: --max_old_space_size=6144
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check out files from GitHub
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||||
|
uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
|
cache: yarn
|
||||||
|
- name: Install dependencies
|
||||||
|
run: yarn install
|
||||||
|
env:
|
||||||
|
CI: true
|
||||||
|
- name: Build Demo
|
||||||
|
run: ./node_modules/.bin/gulp build-demo
|
||||||
|
- name: Deploy to Netlify
|
||||||
|
uses: netlify/actions/cli@master
|
||||||
|
env:
|
||||||
|
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||||
|
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_DEMO_DEV_SITE_ID }}
|
||||||
|
with:
|
||||||
|
args: deploy --dir=demo/dist --prod
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
name: Demo deployment
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
schedule:
|
|
||||||
- cron: "0 0 * * *"
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- dev
|
|
||||||
- master
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
env:
|
|
||||||
NODE_OPTIONS: --max_old_space_size=6144
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
deploy_dev:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: Demo Development
|
|
||||||
if: github.event_name != 'push' || github.ref_name != 'master'
|
|
||||||
environment:
|
|
||||||
name: Demo Development
|
|
||||||
url: ${{ steps.deploy.outputs.netlify_url }}
|
|
||||||
steps:
|
|
||||||
- name: Check out files from GitHub
|
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
||||||
with:
|
|
||||||
ref: dev
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Setup Node and install
|
|
||||||
uses: ./.github/actions/setup
|
|
||||||
|
|
||||||
- name: Build Demo
|
|
||||||
uses: ./.github/actions/build
|
|
||||||
with:
|
|
||||||
target: build-demo
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Deploy to Netlify
|
|
||||||
id: deploy
|
|
||||||
uses: ./.github/actions/netlify-deploy
|
|
||||||
with:
|
|
||||||
dir: demo/dist
|
|
||||||
auth-token: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
|
||||||
site-id: ${{ secrets.NETLIFY_DEMO_DEV_SITE_ID }}
|
|
||||||
|
|
||||||
deploy_master:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: Demo Production
|
|
||||||
if: github.event_name == 'push' && github.ref_name == 'master'
|
|
||||||
environment:
|
|
||||||
name: Demo Production
|
|
||||||
url: ${{ steps.deploy.outputs.netlify_url }}
|
|
||||||
steps:
|
|
||||||
- name: Check out files from GitHub
|
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
||||||
with:
|
|
||||||
ref: master
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Setup Node and install
|
|
||||||
uses: ./.github/actions/setup
|
|
||||||
|
|
||||||
- name: Build Demo
|
|
||||||
uses: ./.github/actions/build
|
|
||||||
with:
|
|
||||||
target: build-demo
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Deploy to Netlify
|
|
||||||
id: deploy
|
|
||||||
uses: ./.github/actions/netlify-deploy
|
|
||||||
with:
|
|
||||||
dir: demo/dist
|
|
||||||
auth-token: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
|
||||||
site-id: ${{ secrets.NETLIFY_DEMO_SITE_ID }}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
name: Design deployment
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
schedule:
|
|
||||||
- cron: "0 0 * * *"
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
env:
|
|
||||||
NODE_OPTIONS: --max_old_space_size=6144
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
deploy:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
environment:
|
|
||||||
name: Design
|
|
||||||
url: ${{ steps.deploy.outputs.netlify_url }}
|
|
||||||
steps:
|
|
||||||
- name: Check out files from GitHub
|
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Setup Node and install
|
|
||||||
uses: ./.github/actions/setup
|
|
||||||
|
|
||||||
- name: Build Gallery
|
|
||||||
uses: ./.github/actions/build
|
|
||||||
with:
|
|
||||||
target: build-gallery
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Deploy to Netlify
|
|
||||||
id: deploy
|
|
||||||
uses: ./.github/actions/netlify-deploy
|
|
||||||
with:
|
|
||||||
dir: gallery/dist
|
|
||||||
auth-token: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
|
||||||
site-id: ${{ secrets.NETLIFY_GALLERY_SITE_ID }}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
name: Design preview
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types:
|
|
||||||
- opened
|
|
||||||
- synchronize
|
|
||||||
- reopened
|
|
||||||
- labeled
|
|
||||||
branches:
|
|
||||||
- dev
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
env:
|
|
||||||
NODE_OPTIONS: --max_old_space_size=6144
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
preview:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
# Skip running on forks since it won't have access to secrets
|
|
||||||
# Skip running PRs without 'needs design preview' label
|
|
||||||
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
|
|
||||||
steps:
|
|
||||||
- name: Check out files from GitHub
|
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Setup Node and install
|
|
||||||
uses: ./.github/actions/setup
|
|
||||||
|
|
||||||
- name: Build Gallery
|
|
||||||
uses: ./.github/actions/build
|
|
||||||
with:
|
|
||||||
target: build-gallery
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Deploy preview to Netlify
|
|
||||||
id: deploy
|
|
||||||
uses: ./.github/actions/netlify-deploy
|
|
||||||
with:
|
|
||||||
dir: gallery/dist
|
|
||||||
alias: deploy-preview-${{ github.event.number }}
|
|
||||||
auth-token: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
|
||||||
site-id: ${{ secrets.NETLIFY_GALLERY_SITE_ID }}
|
|
||||||
|
|
||||||
- name: Generate summary
|
|
||||||
run: echo "${{ steps.deploy.outputs.netlify_url }}" >> "$GITHUB_STEP_SUMMARY"
|
|
||||||
@@ -1,219 +0,0 @@
|
|||||||
name: E2E Tests
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- dev
|
|
||||||
- master
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- dev
|
|
||||||
- master
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
env:
|
|
||||||
NODE_OPTIONS: --max_old_space_size=6144
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
# ── Build the demo once and share it across test jobs via artifact ──────────
|
|
||||||
build-demo:
|
|
||||||
name: Build demo
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Check out files from GitHub
|
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Setup Node and install
|
|
||||||
uses: ./.github/actions/setup
|
|
||||||
|
|
||||||
- name: Build demo
|
|
||||||
uses: ./.github/actions/build
|
|
||||||
with:
|
|
||||||
target: build-demo
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Upload demo build
|
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
||||||
with:
|
|
||||||
name: demo-dist
|
|
||||||
path: demo/dist/
|
|
||||||
if-no-files-found: error
|
|
||||||
retention-days: 3
|
|
||||||
|
|
||||||
# ── Build the e2e test app and share it via artifact ────────────────────────
|
|
||||||
build-e2e-test-app:
|
|
||||||
name: Build e2e test app
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Check out files from GitHub
|
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Setup Node and install
|
|
||||||
uses: ./.github/actions/setup
|
|
||||||
|
|
||||||
- name: Build e2e test app
|
|
||||||
uses: ./.github/actions/build
|
|
||||||
with:
|
|
||||||
target: build-e2e-test-app
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Upload e2e test app build
|
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
||||||
with:
|
|
||||||
name: e2e-test-app-dist
|
|
||||||
path: test/e2e/app/dist/
|
|
||||||
if-no-files-found: error
|
|
||||||
retention-days: 3
|
|
||||||
|
|
||||||
# ── Build the gallery and share it via artifact ─────────────────────────────
|
|
||||||
build-gallery:
|
|
||||||
name: Build gallery
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Check out files from GitHub
|
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Setup Node and install
|
|
||||||
uses: ./.github/actions/setup
|
|
||||||
|
|
||||||
- name: Build gallery
|
|
||||||
uses: ./.github/actions/build
|
|
||||||
with:
|
|
||||||
target: build-gallery
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Upload gallery build
|
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
||||||
with:
|
|
||||||
name: gallery-dist
|
|
||||||
path: gallery/dist/
|
|
||||||
if-no-files-found: error
|
|
||||||
retention-days: 3
|
|
||||||
|
|
||||||
# ── Run Playwright tests locally against Chromium ──────────────────────────
|
|
||||||
e2e-local:
|
|
||||||
name: E2E (local Chromium)
|
|
||||||
needs: [build-demo, build-e2e-test-app, build-gallery]
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
# Fail fast if anything hangs. The whole suite should take < 15 minutes on
|
|
||||||
# Chromium; anything longer is almost certainly an install or webServer
|
|
||||||
# hang.
|
|
||||||
timeout-minutes: 30
|
|
||||||
steps:
|
|
||||||
- name: Check out files from GitHub
|
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Setup Node and install
|
|
||||||
uses: ./.github/actions/setup
|
|
||||||
|
|
||||||
# Resolve the installed Playwright version so the browser cache tracks
|
|
||||||
# Playwright itself, not every unrelated dependency bump.
|
|
||||||
- name: Resolve Playwright version
|
|
||||||
id: playwright-version
|
|
||||||
run: echo "version=$(node -p 'require("@playwright/test/package.json").version')" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
# Cache the downloaded browser build keyed on the installed Playwright
|
|
||||||
# version, so re-runs skip the ~170 MB download unless Playwright changes.
|
|
||||||
- name: Cache Playwright browsers
|
|
||||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
|
||||||
with:
|
|
||||||
path: ~/.cache/ms-playwright
|
|
||||||
key: ${{ runner.os }}-playwright-${{ steps.playwright-version.outputs.version }}
|
|
||||||
|
|
||||||
- name: Install Playwright browsers
|
|
||||||
run: yarn playwright install --with-deps chromium
|
|
||||||
timeout-minutes: 10
|
|
||||||
|
|
||||||
- name: Download demo build
|
|
||||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
||||||
with:
|
|
||||||
name: demo-dist
|
|
||||||
path: demo/dist/
|
|
||||||
|
|
||||||
- name: Download e2e test app build
|
|
||||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
||||||
with:
|
|
||||||
name: e2e-test-app-dist
|
|
||||||
path: test/e2e/app/dist/
|
|
||||||
|
|
||||||
- name: Download gallery build
|
|
||||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
||||||
with:
|
|
||||||
name: gallery-dist
|
|
||||||
path: gallery/dist/
|
|
||||||
|
|
||||||
- name: Run Playwright tests (local)
|
|
||||||
run: yarn test:e2e
|
|
||||||
timeout-minutes: 15
|
|
||||||
|
|
||||||
- name: Upload blob report
|
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
||||||
if: always()
|
|
||||||
with:
|
|
||||||
name: blob-report-local
|
|
||||||
path: test/e2e/reports/
|
|
||||||
retention-days: 3
|
|
||||||
|
|
||||||
# ── Merge local blob reports and post PR comment ───────────────────────────
|
|
||||||
report:
|
|
||||||
name: Report
|
|
||||||
needs: [e2e-local]
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: ${{ !cancelled() }}
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
pull-requests: write
|
|
||||||
steps:
|
|
||||||
- name: Check out files from GitHub
|
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Setup Node and install
|
|
||||||
uses: ./.github/actions/setup
|
|
||||||
|
|
||||||
- name: Download blob report (local)
|
|
||||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
||||||
continue-on-error: true
|
|
||||||
with:
|
|
||||||
name: blob-report-local
|
|
||||||
path: test/e2e/reports/
|
|
||||||
|
|
||||||
- name: Stage blobs for merge
|
|
||||||
run: node test/e2e/collect-blob-reports.mjs
|
|
||||||
|
|
||||||
- name: Merge blob reports
|
|
||||||
run: npx playwright merge-reports -c test/e2e/playwright.merge.config.ts test/e2e/reports/blob
|
|
||||||
|
|
||||||
- name: Upload merged HTML report
|
|
||||||
id: upload-report
|
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
||||||
with:
|
|
||||||
name: playwright-report
|
|
||||||
path: test/e2e/reports/combined/
|
|
||||||
retention-days: 14
|
|
||||||
|
|
||||||
- name: Post report to PR
|
|
||||||
if: github.event_name == 'pull_request' && needs.e2e-local.result == 'failure'
|
|
||||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
const { default: postReportComment } = await import(
|
|
||||||
`${process.env.GITHUB_WORKSPACE}/test/e2e/post-report-comment.mjs`
|
|
||||||
);
|
|
||||||
await postReportComment({ github, context, core });
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
name: "Pull Request Labeler"
|
|
||||||
|
|
||||||
on: pull_request_target # zizmor: ignore[dangerous-triggers] -- safe: only runs actions/labeler, no PR code checkout
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
triage:
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
pull-requests: write
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Apply labels
|
|
||||||
uses: actions/labeler@f27b608878404679385c85cfa523b85ccb86e213 # v6.1.0
|
|
||||||
with:
|
|
||||||
sync-labels: true
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
name: Lock
|
|
||||||
|
|
||||||
# yamllint disable-line rule:truthy
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: "0 * * * *"
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
issues: write
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
lock:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: dessant/lock-threads@89ae32b08ed1a541efecbab17912962a5e38981c # v6.0.2
|
|
||||||
with:
|
|
||||||
github-token: ${{ github.token }}
|
|
||||||
process-only: "issues, prs"
|
|
||||||
issue-inactive-days: "30"
|
|
||||||
issue-lock-reason: ""
|
|
||||||
pr-inactive-days: "1"
|
|
||||||
pr-lock-reason: ""
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
name: Lock
|
||||||
|
|
||||||
|
# yamllint disable-line rule:truthy
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "0 * * * *"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lock:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: dessant/lock-threads@v2.0.1
|
||||||
|
with:
|
||||||
|
github-token: ${{ github.token }}
|
||||||
|
issue-lock-inactive-days: "30"
|
||||||
|
issue-exclude-created-before: "2020-10-01T00:00:00Z"
|
||||||
|
issue-lock-reason: ""
|
||||||
|
pr-lock-inactive-days: "1"
|
||||||
|
pr-exclude-created-before: "2020-11-01T00:00:00Z"
|
||||||
|
pr-lock-reason: ""
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
name: Netlify
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "0 0 * * *"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
trigger_builds:
|
||||||
|
name: Trigger netlify build preview
|
||||||
|
runs-on: "ubuntu-latest"
|
||||||
|
steps:
|
||||||
|
- name: Trigger Cast build
|
||||||
|
run: curl -X POST -d {} https://api.netlify.com/build_hooks/${{ secrets.NETLIFY_CAST_DEV_BUILD_HOOK }}
|
||||||
|
|
||||||
|
- name: Trigger Demo build
|
||||||
|
run: curl -X POST -d {} https://api.netlify.com/build_hooks/${{ secrets.NETLIFY_DEMO_DEV_BUILD_HOOK }}
|
||||||
|
|
||||||
|
- name: Trigger Gallery build
|
||||||
|
run: curl -X POST -d {} https://api.netlify.com/build_hooks/${{ secrets.NETLIFY_GALLERY_DEV_BUILD_HOOK }}
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
name: Nightly
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
schedule:
|
|
||||||
- cron: "0 1 * * *"
|
|
||||||
|
|
||||||
env:
|
|
||||||
PYTHON_VERSION: "3.14"
|
|
||||||
NODE_OPTIONS: --max_old_space_size=6144
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
actions: none
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
nightly:
|
|
||||||
name: Nightly
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
steps:
|
|
||||||
- name: Checkout the repository
|
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
|
||||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
|
|
||||||
with:
|
|
||||||
python-version: ${{ env.PYTHON_VERSION }}
|
|
||||||
|
|
||||||
- name: Setup Node and install
|
|
||||||
uses: ./.github/actions/setup
|
|
||||||
with:
|
|
||||||
immutable: false
|
|
||||||
|
|
||||||
- name: Download translations
|
|
||||||
run: ./script/translations_download
|
|
||||||
env:
|
|
||||||
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }}
|
|
||||||
|
|
||||||
- name: Bump version
|
|
||||||
run: script/version_bump.js nightly
|
|
||||||
|
|
||||||
- name: Build nightly Python wheels
|
|
||||||
run: |
|
|
||||||
pip install build
|
|
||||||
yarn install
|
|
||||||
export SKIP_FETCH_NIGHTLY_TRANSLATIONS=1
|
|
||||||
script/build_frontend
|
|
||||||
rm -rf dist home_assistant_frontend.egg-info
|
|
||||||
python3 -m build
|
|
||||||
|
|
||||||
- name: Archive translations
|
|
||||||
run: tar -czvf translations.tar.gz translations
|
|
||||||
|
|
||||||
- name: Upload build artifacts
|
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
||||||
with:
|
|
||||||
name: wheels
|
|
||||||
path: dist/home_assistant_frontend*.whl
|
|
||||||
if-no-files-found: error
|
|
||||||
|
|
||||||
- name: Upload translations
|
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
||||||
with:
|
|
||||||
name: translations
|
|
||||||
path: translations.tar.gz
|
|
||||||
if-no-files-found: error
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
name: Pull request standards
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request_target: # zizmor: ignore[dangerous-triggers] -- safe: reads PR metadata from event payload only, checks out base repo scripts only, never PR head code
|
|
||||||
types:
|
|
||||||
- opened
|
|
||||||
- edited
|
|
||||||
- reopened
|
|
||||||
- ready_for_review
|
|
||||||
branches:
|
|
||||||
- dev
|
|
||||||
|
|
||||||
permissions: {}
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
check:
|
|
||||||
name: Check pull request follows contribution standards
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
pull-requests: write # To label and comment on pull requests
|
|
||||||
steps:
|
|
||||||
- name: Check out workflow scripts
|
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
sparse-checkout: .github/scripts
|
|
||||||
- name: Check pull request standards
|
|
||||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
const { default: checkStandards } = await import(
|
|
||||||
`${process.env.GITHUB_WORKSPACE}/.github/scripts/check-pull-request-standards.mjs`
|
|
||||||
);
|
|
||||||
await checkStandards({ github, context, core });
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
name: RelativeCI
|
|
||||||
|
|
||||||
on:
|
|
||||||
# zizmor: ignore[dangerous-triggers] -- safe: only downloads artifacts, no PR code checkout
|
|
||||||
workflow_run:
|
|
||||||
workflows: [CI]
|
|
||||||
types:
|
|
||||||
- completed
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
actions: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
upload-frontend-modern:
|
|
||||||
name: Upload stats (frontend/modern)
|
|
||||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Send bundle stats and build information to RelativeCI
|
|
||||||
uses: relative-ci/agent-action@fcf45416581928e8dd62eded78ce98c78e5149f8 # v3.2.3
|
|
||||||
with:
|
|
||||||
key: ${{ secrets.RELATIVE_CI_KEY_frontend_modern }}
|
|
||||||
token: ${{ github.token }}
|
|
||||||
artifactName: frontend-bundle-stats
|
|
||||||
webpackStatsFile: frontend-modern.json
|
|
||||||
|
|
||||||
upload-frontend-legacy:
|
|
||||||
name: Upload stats (frontend/legacy)
|
|
||||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Send bundle stats and build information to RelativeCI
|
|
||||||
uses: relative-ci/agent-action@fcf45416581928e8dd62eded78ce98c78e5149f8 # v3.2.3
|
|
||||||
with:
|
|
||||||
key: ${{ secrets.RELATIVE_CI_KEY_frontend_legacy }}
|
|
||||||
token: ${{ github.token }}
|
|
||||||
artifactName: frontend-bundle-stats
|
|
||||||
webpackStatsFile: frontend-legacy.json
|
|
||||||
@@ -5,19 +5,10 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- dev
|
- dev
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
update_release_draft:
|
update_release_draft:
|
||||||
permissions:
|
|
||||||
# write permission for contents is required to create a github release
|
|
||||||
contents: write
|
|
||||||
# write permission for pull-requests is required for autolabeler
|
|
||||||
# otherwise, read permission is required at least
|
|
||||||
pull-requests: read
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: release-drafter/release-drafter@4d75298e00d9e34c483e5ff8c68d0ea1c1940c1e # v7.5.1
|
- uses: release-drafter/release-drafter@v5
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
@@ -6,131 +6,86 @@ on:
|
|||||||
- published
|
- published
|
||||||
|
|
||||||
env:
|
env:
|
||||||
PYTHON_VERSION: "3.14"
|
PYTHON_VERSION: 3.8
|
||||||
|
NODE_VERSION: 14
|
||||||
NODE_OPTIONS: --max_old_space_size=6144
|
NODE_OPTIONS: --max_old_space_size=6144
|
||||||
|
|
||||||
# Set default workflow permissions
|
|
||||||
# All scopes not mentioned here are set to no access
|
|
||||||
# https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token
|
|
||||||
permissions:
|
|
||||||
actions: none
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
name: Release
|
name: Release
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
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:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@v2
|
||||||
with:
|
|
||||||
persist-credentials: false
|
- name: Verify version
|
||||||
|
uses: home-assistant/actions/helpers/verify-version@master
|
||||||
|
|
||||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
||||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
uses: actions/setup-python@v2
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.PYTHON_VERSION }}
|
python-version: ${{ env.PYTHON_VERSION }}
|
||||||
|
|
||||||
- name: Verify version
|
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||||
uses: home-assistant/actions/helpers/verify-version@f4ca6f671bd429efb108c0f2fa0ae8af0215986c # master
|
uses: actions/setup-node@v2
|
||||||
|
|
||||||
- name: Setup Node and install
|
|
||||||
uses: ./.github/actions/setup
|
|
||||||
with:
|
with:
|
||||||
immutable: false
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
cache: false
|
cache: yarn
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: yarn install
|
||||||
|
|
||||||
- name: Download Translations
|
- name: Download Translations
|
||||||
run: ./script/translations_download
|
run: ./script/translations_download
|
||||||
env:
|
env:
|
||||||
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }}
|
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }}
|
||||||
|
|
||||||
- name: Build and release package
|
- name: Build and release package
|
||||||
run: |
|
run: |
|
||||||
python3 -m pip install build
|
python3 -m pip install twine
|
||||||
export SKIP_FETCH_NIGHTLY_TRANSLATIONS=1
|
export TWINE_USERNAME="__token__"
|
||||||
|
export TWINE_PASSWORD="${{ secrets.TWINE_TOKEN }}"
|
||||||
|
|
||||||
script/release
|
script/release
|
||||||
|
|
||||||
- name: Publish to PyPI
|
|
||||||
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
|
|
||||||
with:
|
|
||||||
skip-existing: true
|
|
||||||
|
|
||||||
- name: Upload release assets
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ github.token }}
|
|
||||||
TAG_NAME: ${{ github.event.release.tag_name }}
|
|
||||||
run: gh release upload "$TAG_NAME" dist/*.whl dist/*.tar.gz --clobber
|
|
||||||
|
|
||||||
wheels-init:
|
wheels-init:
|
||||||
name: Init wheels build
|
name: Init wheels build
|
||||||
needs: release
|
needs: release
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Generate requirements.txt
|
- name: Generate requirements.txt
|
||||||
env:
|
|
||||||
GITHUB_REF: ${{ github.ref }}
|
|
||||||
run: |
|
run: |
|
||||||
version=$(echo "$GITHUB_REF" | awk -F"/" '{print $NF}' )
|
# Sleep to give pypi time to populate the new version across mirrors
|
||||||
# Wait for the package to become available on PyPI
|
sleep 240
|
||||||
echo "Waiting for home-assistant-frontend==$version to appear on PyPI..."
|
version=$(echo "${{ github.ref }}" | awk -F"/" '{print $NF}' )
|
||||||
for i in $(seq 1 30); do
|
|
||||||
status=$(curl -s -o /dev/null -w "%{http_code}" "https://pypi.org/pypi/home-assistant-frontend/$version/json")
|
|
||||||
if [ "$status" = "200" ]; then
|
|
||||||
echo "Package is available on PyPI!"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
if [ "$i" = "30" ]; then
|
|
||||||
echo "Timed out waiting for package to appear on PyPI"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "Not available yet (HTTP $status), retrying in 30 seconds... ($i/30)"
|
|
||||||
sleep 30
|
|
||||||
done
|
|
||||||
echo "home-assistant-frontend==$version" > ./requirements.txt
|
echo "home-assistant-frontend==$version" > ./requirements.txt
|
||||||
|
|
||||||
# home-assistant/wheels doesn't support SHA pinning
|
- name: Upload requirements.txt
|
||||||
- name: Build wheels
|
uses: actions/upload-artifact@v2
|
||||||
uses: home-assistant/wheels@9e17ab1ed5c4c79d8b61e29fa63de25ca2710716 # 2026.07.0
|
|
||||||
with:
|
with:
|
||||||
abi: cp314
|
name: requirements
|
||||||
tag: musllinux_1_2
|
path: ./requirements.txt
|
||||||
arch: amd64
|
|
||||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
|
||||||
requirements: "requirements.txt"
|
|
||||||
|
|
||||||
release-landing-page:
|
build-wheels:
|
||||||
name: Release landing-page frontend
|
name: Build wheels for ${{ matrix.arch }}
|
||||||
if: github.event.release.prerelease == false
|
needs: wheels-init
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
strategy:
|
||||||
contents: write # Required to upload release assets
|
matrix:
|
||||||
|
arch: ["aarch64", "armhf", "armv7", "amd64", "i386"]
|
||||||
|
tag:
|
||||||
|
- "3.9-alpine3.14"
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Download requirements.txt
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/download-artifact@v2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
name: requirements
|
||||||
- name: Setup Node and install
|
|
||||||
uses: ./.github/actions/setup
|
- name: Build wheels
|
||||||
|
uses: home-assistant/wheels@master
|
||||||
with:
|
with:
|
||||||
immutable: false
|
tag: ${{ matrix.tag }}
|
||||||
cache: false
|
arch: ${{ matrix.arch }}
|
||||||
- name: Download Translations
|
wheels-host: ${{ secrets.WHEELS_HOST }}
|
||||||
run: ./script/translations_download
|
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||||
env:
|
wheels-user: wheels
|
||||||
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }}
|
requirements: "requirements.txt"
|
||||||
- name: Build landing-page
|
|
||||||
run: landing-page/script/build_landing_page
|
|
||||||
- name: Tar folder
|
|
||||||
env:
|
|
||||||
TAG_NAME: ${{ github.event.release.tag_name }}
|
|
||||||
run: tar -czf "landing-page/home_assistant_frontend_landingpage-${TAG_NAME}.tar.gz" -C landing-page/dist .
|
|
||||||
- name: Upload release asset
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ github.token }}
|
|
||||||
TAG_NAME: ${{ github.event.release.tag_name }}
|
|
||||||
run: gh release upload "$TAG_NAME" "landing-page/home_assistant_frontend_landingpage-${TAG_NAME}.tar.gz" --clobber
|
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
name: Restrict task creation
|
|
||||||
|
|
||||||
# yamllint disable-line rule:truthy
|
|
||||||
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@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.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:
|
|
||||||
contents: read # To check out workflow scripts
|
|
||||||
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'
|
|
||||||
steps:
|
|
||||||
- name: Check out workflow scripts
|
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
sparse-checkout: .github/scripts
|
|
||||||
- name: Check if user is authorized
|
|
||||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
const { default: checkTaskAuthorization } = await import(
|
|
||||||
`${process.env.GITHUB_WORKSPACE}/.github/scripts/check-task-authorization.mjs`
|
|
||||||
);
|
|
||||||
await checkTaskAuthorization({ github, context, core });
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
name: Stale
|
|
||||||
|
|
||||||
# yamllint disable-line rule:truthy
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: "0 * * * *"
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
actions: write
|
|
||||||
issues: write
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
stale:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: 90 days stale policy
|
|
||||||
uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0
|
|
||||||
with:
|
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
days-before-stale: 90
|
|
||||||
days-before-close: 7
|
|
||||||
operations-per-run: 25
|
|
||||||
remove-stale-when-updated: true
|
|
||||||
stale-issue-label: "stale"
|
|
||||||
exempt-issue-labels: "no-stale,Help%20wanted,help-wanted,feature-request,feature%20request"
|
|
||||||
stale-issue-message: >
|
|
||||||
There hasn't been any activity on this issue recently. Due to the
|
|
||||||
high number of incoming GitHub notifications, we have to clean some
|
|
||||||
of the old issues, as many of them have already been resolved with
|
|
||||||
the latest updates.
|
|
||||||
|
|
||||||
Please make sure to update to the latest Home Assistant version and
|
|
||||||
check if that solves the issue. Let us know if that works for you by
|
|
||||||
adding a comment 👍
|
|
||||||
|
|
||||||
This issue has now been marked as stale and will be closed if no
|
|
||||||
further activity occurs. Thank you for your contributions.
|
|
||||||
|
|
||||||
stale-pr-label: "stale"
|
|
||||||
exempt-pr-labels: "no-stale"
|
|
||||||
stale-pr-message: >
|
|
||||||
There hasn't been any activity on this pull request recently. This
|
|
||||||
pull request has been automatically marked as stale because of that
|
|
||||||
and will be closed if no further activity occurs within 7 days.
|
|
||||||
|
|
||||||
Thank you for your contributions.
|
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
name: Stale
|
||||||
|
|
||||||
|
# yamllint disable-line rule:truthy
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "0 * * * *"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
stale:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: 90 days stale policy
|
||||||
|
uses: actions/stale@v3.0.13
|
||||||
|
with:
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
days-before-stale: 90
|
||||||
|
days-before-close: 7
|
||||||
|
operations-per-run: 25
|
||||||
|
remove-stale-when-updated: true
|
||||||
|
stale-issue-label: "stale"
|
||||||
|
exempt-issue-labels: "no-stale,Help%20wanted,help-wanted,feature-request,feature%20request"
|
||||||
|
stale-issue-message: >
|
||||||
|
There hasn't been any activity on this issue recently. Due to the
|
||||||
|
high number of incoming GitHub notifications, we have to clean some
|
||||||
|
of the old issues, as many of them have already been resolved with
|
||||||
|
the latest updates.
|
||||||
|
|
||||||
|
Please make sure to update to the latest Home Assistant version and
|
||||||
|
check if that solves the issue. Let us know if that works for you by
|
||||||
|
adding a comment 👍
|
||||||
|
|
||||||
|
This issue has now been marked as stale and will be closed if no
|
||||||
|
further activity occurs. Thank you for your contributions.
|
||||||
|
|
||||||
|
stale-pr-label: "stale"
|
||||||
|
exempt-pr-labels: "no-stale"
|
||||||
|
stale-pr-message: >
|
||||||
|
There hasn't been any activity on this pull request recently. This
|
||||||
|
pull request has been automatically marked as stale because of that
|
||||||
|
and will be closed if no further activity occurs within 7 days.
|
||||||
|
|
||||||
|
Thank you for your contributions.
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
name: Sync numeric device classes
|
|
||||||
|
|
||||||
# Mirrors Home Assistant Core's numeric `SensorDeviceClass` list into the
|
|
||||||
# build-time default in src/data/sensor_numeric_device_classes.ts and opens a PR
|
|
||||||
# when it drifts. Reads homeassistant/generated/sensor.json from core.
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
schedule:
|
|
||||||
- cron: "0 4 * * *" # Daily, 04:00 UTC
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
sync:
|
|
||||||
name: Sync
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
pull-requests: write
|
|
||||||
steps:
|
|
||||||
- name: Checkout the repository
|
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Setup Node and install
|
|
||||||
uses: ./.github/actions/setup
|
|
||||||
|
|
||||||
- name: Regenerate numeric device classes
|
|
||||||
run: ./script/gen_numeric_device_classes
|
|
||||||
|
|
||||||
- name: Format
|
|
||||||
run: yarn prettier --write src/data/sensor_numeric_device_classes.ts
|
|
||||||
|
|
||||||
- name: Create pull request
|
|
||||||
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
|
|
||||||
with:
|
|
||||||
branch: chore/sync-numeric-device-classes
|
|
||||||
commit-message: Update numeric sensor device classes
|
|
||||||
title: Update numeric sensor device classes
|
|
||||||
body: |
|
|
||||||
Regenerated `SENSOR_NUMERIC_DEVICE_CLASSES` from Home Assistant Core's
|
|
||||||
`SensorDeviceClass`.
|
|
||||||
|
|
||||||
Automated by `.github/workflows/sync-numeric-device-classes.yaml`.
|
|
||||||
@@ -1,16 +1,14 @@
|
|||||||
name: Translations
|
name: Translations
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- dev
|
- dev
|
||||||
paths:
|
paths:
|
||||||
- .github/workflows/translations.yaml
|
|
||||||
- src/translations/en.json
|
- src/translations/en.json
|
||||||
|
|
||||||
permissions:
|
env:
|
||||||
contents: read
|
NODE_VERSION: 14
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
upload:
|
upload:
|
||||||
@@ -18,11 +16,10 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@v2
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Upload Translations
|
- name: Upload Translations
|
||||||
run: ./script/translations_upload_base
|
run: |
|
||||||
env:
|
export LOKALISE_TOKEN="${{ secrets.LOKALISE_TOKEN }}"
|
||||||
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }}
|
|
||||||
|
./script/translations_upload_base
|
||||||
|
|||||||
+7
-32
@@ -2,12 +2,10 @@
|
|||||||
.reify-cache
|
.reify-cache
|
||||||
|
|
||||||
# build
|
# build
|
||||||
build/
|
build
|
||||||
dist/
|
build-translations/*
|
||||||
/hass_frontend/
|
hass_frontend/*
|
||||||
/translations/
|
dist
|
||||||
# Composite action source, not build output
|
|
||||||
!/.github/actions/build/
|
|
||||||
|
|
||||||
# yarn
|
# yarn
|
||||||
.yarn/*
|
.yarn/*
|
||||||
@@ -17,7 +15,7 @@ dist/
|
|||||||
!.yarn/sdks
|
!.yarn/sdks
|
||||||
!.yarn/versions
|
!.yarn/versions
|
||||||
.pnp.*
|
.pnp.*
|
||||||
node_modules/
|
node_modules/*
|
||||||
yarn-error.log
|
yarn-error.log
|
||||||
npm-debug.log
|
npm-debug.log
|
||||||
|
|
||||||
@@ -29,7 +27,7 @@ npm-debug.log
|
|||||||
# venv stuff
|
# venv stuff
|
||||||
pyvenv.cfg
|
pyvenv.cfg
|
||||||
pip-selfcheck.json
|
pip-selfcheck.json
|
||||||
/venv/
|
venv/*
|
||||||
.venv
|
.venv
|
||||||
|
|
||||||
# vscode
|
# vscode
|
||||||
@@ -48,27 +46,4 @@ src/cast/dev_const.ts
|
|||||||
.tool-versions
|
.tool-versions
|
||||||
|
|
||||||
# Home Assistant config
|
# Home Assistant config
|
||||||
/config/
|
/config
|
||||||
|
|
||||||
# Jetbrains
|
|
||||||
/.idea/
|
|
||||||
|
|
||||||
# test coverage
|
|
||||||
test/coverage/
|
|
||||||
|
|
||||||
# Playwright e2e output
|
|
||||||
test/e2e/reports/
|
|
||||||
test/e2e/test-results/
|
|
||||||
# E2E test app build output
|
|
||||||
test/e2e/app/dist/
|
|
||||||
# MCP server
|
|
||||||
.playwright-mcp/
|
|
||||||
|
|
||||||
# AI tooling
|
|
||||||
.claude/*
|
|
||||||
!.claude/skills
|
|
||||||
.cursor
|
|
||||||
.opencode
|
|
||||||
.serena
|
|
||||||
|
|
||||||
test/benchmarks/results/
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
yarn run lint-staged --relative
|
|
||||||
+10
-4
@@ -1,4 +1,10 @@
|
|||||||
CLA.md
|
build
|
||||||
CODE_OF_CONDUCT.md
|
build-translations/*
|
||||||
LICENSE.md
|
translations/*
|
||||||
PULL_REQUEST_TEMPLATE.md
|
node_modules/*
|
||||||
|
hass_frontend/*
|
||||||
|
pip-selfcheck.json
|
||||||
|
|
||||||
|
# vscode
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
|||||||
Vendored
+2
-5
@@ -2,10 +2,7 @@
|
|||||||
"recommendations": [
|
"recommendations": [
|
||||||
"dbaeumer.vscode-eslint",
|
"dbaeumer.vscode-eslint",
|
||||||
"esbenp.prettier-vscode",
|
"esbenp.prettier-vscode",
|
||||||
"runem.lit-plugin",
|
"bierner.lit-html",
|
||||||
"github.vscode-pull-request-github",
|
"runem.lit-plugin"
|
||||||
"eamodio.gitlens",
|
|
||||||
"vitest.explorer",
|
|
||||||
"yeion7.styled-global-variables-autocomplete"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+4
-2
@@ -9,7 +9,9 @@
|
|||||||
"webRoot": "${workspaceFolder}/hass_frontend",
|
"webRoot": "${workspaceFolder}/hass_frontend",
|
||||||
"disableNetworkCache": true,
|
"disableNetworkCache": true,
|
||||||
"preLaunchTask": "Develop Frontend",
|
"preLaunchTask": "Develop Frontend",
|
||||||
"outFiles": ["${workspaceFolder}/hass_frontend/frontend_latest/*.js"]
|
"outFiles": [
|
||||||
|
"${workspaceFolder}/hass_frontend/frontend_latest/*.js"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Debug Gallery",
|
"name": "Debug Gallery",
|
||||||
@@ -37,6 +39,6 @@
|
|||||||
"webRoot": "${workspaceFolder}/cast/dist",
|
"webRoot": "${workspaceFolder}/cast/dist",
|
||||||
"disableNetworkCache": true,
|
"disableNetworkCache": true,
|
||||||
"preLaunchTask": "Develop Cast"
|
"preLaunchTask": "Develop Cast"
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+23
-48
@@ -1,42 +1,6 @@
|
|||||||
{
|
{
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"tasks": [
|
"tasks": [
|
||||||
{
|
|
||||||
"label": "Develop and serve Frontend",
|
|
||||||
"type": "shell",
|
|
||||||
"command": "script/develop_and_serve -c ${input:coreUrl}",
|
|
||||||
// Sync changes here to other tasks until issue resolved
|
|
||||||
// https://github.com/Microsoft/vscode/issues/61497
|
|
||||||
"problemMatcher": {
|
|
||||||
"owner": "ha-build",
|
|
||||||
"source": "ha-build",
|
|
||||||
"fileLocation": "absolute",
|
|
||||||
"severity": "error",
|
|
||||||
"pattern": [
|
|
||||||
{
|
|
||||||
"regexp": "(SyntaxError): (.+): (.+) \\((\\d+):(\\d+)\\)",
|
|
||||||
"severity": 1,
|
|
||||||
"file": 2,
|
|
||||||
"message": 3,
|
|
||||||
"line": 4,
|
|
||||||
"column": 5
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"background": {
|
|
||||||
"activeOnStart": true,
|
|
||||||
"beginsPattern": "Changes detected. Starting compilation",
|
|
||||||
"endsPattern": "Build done @"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"isBackground": true,
|
|
||||||
"group": {
|
|
||||||
"kind": "build",
|
|
||||||
"isDefault": true
|
|
||||||
},
|
|
||||||
"runOptions": {
|
|
||||||
"instanceLimit": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"label": "Develop Frontend",
|
"label": "Develop Frontend",
|
||||||
"type": "gulp",
|
"type": "gulp",
|
||||||
@@ -74,9 +38,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Develop Gallery",
|
"label": "Develop Supervisor panel",
|
||||||
"type": "gulp",
|
"type": "gulp",
|
||||||
"task": "develop-gallery",
|
"task": "develop-hassio",
|
||||||
"problemMatcher": {
|
"problemMatcher": {
|
||||||
"owner": "ha-build",
|
"owner": "ha-build",
|
||||||
"source": "ha-build",
|
"source": "ha-build",
|
||||||
@@ -98,7 +62,6 @@
|
|||||||
"endsPattern": "Build done @"
|
"endsPattern": "Build done @"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
"isBackground": true,
|
"isBackground": true,
|
||||||
"group": "build",
|
"group": "build",
|
||||||
"runOptions": {
|
"runOptions": {
|
||||||
@@ -106,9 +69,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Develop Landing Page",
|
"label": "Develop Gallery",
|
||||||
"type": "gulp",
|
"type": "gulp",
|
||||||
"task": "develop-landing-page",
|
"task": "develop-gallery",
|
||||||
"problemMatcher": {
|
"problemMatcher": {
|
||||||
"owner": "ha-build",
|
"owner": "ha-build",
|
||||||
"source": "ha-build",
|
"source": "ha-build",
|
||||||
@@ -216,18 +179,30 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Setup and fetch nightly translations",
|
"label": "Run HA Core for Supervisor in devcontainer",
|
||||||
"type": "gulp",
|
"type": "shell",
|
||||||
"task": "setup-and-fetch-nightly-translations",
|
"command": "HASSIO=${input:supervisorHost} HASSIO_TOKEN=${input:supervisorToken} script/core",
|
||||||
"problemMatcher": []
|
"isBackground": true,
|
||||||
|
"group": {
|
||||||
|
"kind": "build",
|
||||||
|
"isDefault": true
|
||||||
|
},
|
||||||
|
"problemMatcher": [],
|
||||||
|
"runOptions": {
|
||||||
|
"instanceLimit": 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"inputs": [
|
"inputs": [
|
||||||
{
|
{
|
||||||
"id": "coreUrl",
|
"id": "supervisorHost",
|
||||||
"type": "promptString",
|
"type": "promptString",
|
||||||
"description": "The URL of the Home Assistant Core instance",
|
"description": "The IP of the Supervisor host running the Remote API proxy add-on"
|
||||||
"default": "http://127.0.0.1:8123"
|
},
|
||||||
|
{
|
||||||
|
"id": "supervisorToken",
|
||||||
|
"type": "promptString",
|
||||||
|
"description": "The token for the Remote API proxy add-on"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,29 @@
|
|||||||
|
diff --git a/lib/uni-virtualizer/lib/polyfillLoaders/EventTarget.js b/lib/uni-virtualizer/lib/polyfillLoaders/EventTarget.js
|
||||||
|
index d92179f7fd5315203f870a6963e871dc8ddf6c0c..362e284121b97e0fba0925225777aebc32e26b8d 100644
|
||||||
|
--- a/lib/uni-virtualizer/lib/polyfillLoaders/EventTarget.js
|
||||||
|
+++ b/lib/uni-virtualizer/lib/polyfillLoaders/EventTarget.js
|
||||||
|
@@ -1,14 +1,15 @@
|
||||||
|
-let _ET, ET;
|
||||||
|
+let _ET;
|
||||||
|
+let ET;
|
||||||
|
export default async function EventTarget() {
|
||||||
|
- return ET || init();
|
||||||
|
+ return ET || init();
|
||||||
|
}
|
||||||
|
async function init() {
|
||||||
|
- _ET = window.EventTarget;
|
||||||
|
- try {
|
||||||
|
- new _ET();
|
||||||
|
- }
|
||||||
|
- catch (_a) {
|
||||||
|
- _ET = (await import('event-target-shim')).EventTarget;
|
||||||
|
- }
|
||||||
|
- return (ET = _ET);
|
||||||
|
+ _ET = window.EventTarget;
|
||||||
|
+ try {
|
||||||
|
+ new _ET();
|
||||||
|
+ } catch (_a) {
|
||||||
|
+ _ET = (await import("event-target-shim")).default.EventTarget;
|
||||||
|
+ }
|
||||||
|
+ return (ET = _ET);
|
||||||
|
}
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
diff --git a/mwc-formfield-base.js b/mwc-formfield-base.js
|
|
||||||
index 7b763326d7d51835ad52646bfbc80fe21989abd3..f2baa8224e6d03df1fdb0b9fd03f5c6d77fc8747 100644
|
|
||||||
--- a/mwc-formfield-base.js
|
|
||||||
+++ b/mwc-formfield-base.js
|
|
||||||
@@ -9,7 +9,7 @@ import { BaseElement } from '@material/mwc-base/base-element.js';
|
|
||||||
import { FormElement } from '@material/mwc-base/form-element.js';
|
|
||||||
import { observer } from '@material/mwc-base/observer.js';
|
|
||||||
import { html } from 'lit';
|
|
||||||
-import { property, query, queryAssignedNodes } from 'lit/decorators.js';
|
|
||||||
+import { property, query, queryAssignedElements } from 'lit/decorators.js';
|
|
||||||
import { classMap } from 'lit/directives/class-map.js';
|
|
||||||
export class FormfieldBase extends BaseElement {
|
|
||||||
constructor() {
|
|
||||||
@@ -96,7 +96,7 @@ __decorate([
|
|
||||||
query('.mdc-form-field')
|
|
||||||
], FormfieldBase.prototype, "mdcRoot", void 0);
|
|
||||||
__decorate([
|
|
||||||
- queryAssignedNodes('', true, '*')
|
|
||||||
+ queryAssignedElements({ slot: "", flatten: true, selector: "*" })
|
|
||||||
], FormfieldBase.prototype, "slottedInputs", void 0);
|
|
||||||
__decorate([
|
|
||||||
query('label')
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
diff --git a/mwc-list-base.js b/mwc-list-base.js
|
|
||||||
index 1ba95b6a01dcecea4d85b5cbbbcc3dfb04c40d5f..dced13fdb7929c490d6661b1bbe7e9f96dcd2285 100644
|
|
||||||
--- a/mwc-list-base.js
|
|
||||||
+++ b/mwc-list-base.js
|
|
||||||
@@ -11,7 +11,7 @@ import { BaseElement } from '@material/mwc-base/base-element.js';
|
|
||||||
import { observer } from '@material/mwc-base/observer.js';
|
|
||||||
import { deepActiveElementPath, doesElementContainFocus, isNodeElement } from '@material/mwc-base/utils.js';
|
|
||||||
import { html } from 'lit';
|
|
||||||
-import { property, query, queryAssignedNodes } from 'lit/decorators.js';
|
|
||||||
+import { property, query, queryAssignedElements } from 'lit/decorators.js';
|
|
||||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
|
||||||
import MDCListFoundation, { isIndexSet } from './mwc-list-foundation.js';
|
|
||||||
export { createSetFromIndex, isEventMulti, isIndexSet } from './mwc-list-foundation.js';
|
|
||||||
@@ -425,10 +425,10 @@ __decorate([
|
|
||||||
query('.mdc-deprecated-list')
|
|
||||||
], ListBase.prototype, "mdcRoot", void 0);
|
|
||||||
__decorate([
|
|
||||||
- queryAssignedNodes('', true, '*')
|
|
||||||
+ queryAssignedElements({ flatten: true, selector: "*" })
|
|
||||||
], ListBase.prototype, "assignedElements", void 0);
|
|
||||||
__decorate([
|
|
||||||
- queryAssignedNodes('', true, '[tabindex="0"]')
|
|
||||||
+ queryAssignedElements({ flatten: true, selector: '[tabindex="0"]' })
|
|
||||||
], ListBase.prototype, "tabbableElements", void 0);
|
|
||||||
__decorate([
|
|
||||||
property({ type: Boolean }),
|
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
diff --git a/lib/legacy/class.js b/lib/legacy/class.js
|
||||||
|
index aee2511be1cd9bf900ee552bc98190c1631c57c0..f2f499d68bf52034cac9c28307c99e8ce6b8417d 100644
|
||||||
|
--- a/lib/legacy/class.js
|
||||||
|
+++ b/lib/legacy/class.js
|
||||||
|
@@ -304,17 +304,23 @@ function GenerateClassFromInfo(info, Base, behaviors) {
|
||||||
|
// only proceed if the generated class' prototype has not been registered.
|
||||||
|
const generatedProto = PolymerGenerated.prototype;
|
||||||
|
if (!generatedProto.hasOwnProperty(JSCompiler_renameProperty('__hasRegisterFinished', generatedProto))) {
|
||||||
|
- generatedProto.__hasRegisterFinished = true;
|
||||||
|
+ // make sure legacy lifecycle is called on the *element*'s prototype
|
||||||
|
+ // and not the generated class prototype; if the element has been
|
||||||
|
+ // extended, these are *not* the same.
|
||||||
|
+ const proto = Object.getPrototypeOf(this);
|
||||||
|
+ // Only set flag when generated prototype itself is registered,
|
||||||
|
+ // as this element may be extended from, and needs to run `registered`
|
||||||
|
+ // on all behaviors on the subclass as well.
|
||||||
|
+ if (proto === generatedProto) {
|
||||||
|
+ generatedProto.__hasRegisterFinished = true;
|
||||||
|
+ }
|
||||||
|
// ensure superclass is registered first.
|
||||||
|
super._registered();
|
||||||
|
// copy properties onto the generated class lazily if we're optimizing,
|
||||||
|
- if (legacyOptimizations) {
|
||||||
|
+ if (legacyOptimizations && !Object.hasOwnProperty(generatedProto, '__hasCopiedProperties')) {
|
||||||
|
+ generatedProto.__hasCopiedProperties = true;
|
||||||
|
copyPropertiesToProto(generatedProto);
|
||||||
|
}
|
||||||
|
- // make sure legacy lifecycle is called on the *element*'s prototype
|
||||||
|
- // and not the generated class prototype; if the element has been
|
||||||
|
- // extended, these are *not* the same.
|
||||||
|
- const proto = Object.getPrototypeOf(this);
|
||||||
|
let list = lifecycle.beforeRegister;
|
||||||
|
if (list) {
|
||||||
|
for (let i=0; i < list.length; i++) {
|
||||||
File diff suppressed because one or more lines are too long
@@ -1,60 +0,0 @@
|
|||||||
diff --git a/modular/sortable.core.esm.js b/modular/sortable.core.esm.js
|
|
||||||
index 8b5e49b011713c8859c669069fbe85ce53974e1d..6a0afc92787157b8a31c38cc5f67dfa526090a00 100644
|
|
||||||
--- a/modular/sortable.core.esm.js
|
|
||||||
+++ b/modular/sortable.core.esm.js
|
|
||||||
@@ -1781,11 +1781,16 @@ Sortable.prototype = /** @lends Sortable.prototype */{
|
|
||||||
}
|
|
||||||
if (_onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt, !!target) !== false) {
|
|
||||||
capture();
|
|
||||||
- if (elLastChild && elLastChild.nextSibling) {
|
|
||||||
- // the last draggable element is not the last node
|
|
||||||
- el.insertBefore(dragEl, elLastChild.nextSibling);
|
|
||||||
- } else {
|
|
||||||
- el.appendChild(dragEl);
|
|
||||||
+ try {
|
|
||||||
+ if (elLastChild && elLastChild.nextSibling) {
|
|
||||||
+ // the last draggable element is not the last node
|
|
||||||
+ el.insertBefore(dragEl, elLastChild.nextSibling);
|
|
||||||
+ } else {
|
|
||||||
+ el.appendChild(dragEl);
|
|
||||||
+ }
|
|
||||||
+ }
|
|
||||||
+ catch(err) {
|
|
||||||
+ return completed(false);
|
|
||||||
}
|
|
||||||
parentEl = el; // actualization
|
|
||||||
|
|
||||||
@@ -1802,7 +1807,12 @@ Sortable.prototype = /** @lends Sortable.prototype */{
|
|
||||||
targetRect = getRect(target);
|
|
||||||
if (_onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt, false) !== false) {
|
|
||||||
capture();
|
|
||||||
- el.insertBefore(dragEl, firstChild);
|
|
||||||
+ try {
|
|
||||||
+ el.insertBefore(dragEl, firstChild);
|
|
||||||
+ }
|
|
||||||
+ catch(err) {
|
|
||||||
+ return completed(false);
|
|
||||||
+ }
|
|
||||||
parentEl = el; // actualization
|
|
||||||
|
|
||||||
changed();
|
|
||||||
@@ -1849,10 +1859,15 @@ Sortable.prototype = /** @lends Sortable.prototype */{
|
|
||||||
_silent = true;
|
|
||||||
setTimeout(_unsilent, 30);
|
|
||||||
capture();
|
|
||||||
- if (after && !nextSibling) {
|
|
||||||
- el.appendChild(dragEl);
|
|
||||||
- } else {
|
|
||||||
- target.parentNode.insertBefore(dragEl, after ? nextSibling : target);
|
|
||||||
+ try {
|
|
||||||
+ if (after && !nextSibling) {
|
|
||||||
+ el.appendChild(dragEl);
|
|
||||||
+ } else {
|
|
||||||
+ target.parentNode.insertBefore(dragEl, after ? nextSibling : target);
|
|
||||||
+ }
|
|
||||||
+ }
|
|
||||||
+ catch(err) {
|
|
||||||
+ return completed(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Undo chrome's scroll adjustment (has no effect on other browsers)
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
diff --git a/dist/tinykeys.cjs b/dist/tinykeys.cjs
|
|
||||||
index 08c98b6eff3b8fb4b727fe8e6b096951d6ef6347..9c44f14862f582766ea1733b6dc0e97f962800d8 100644
|
|
||||||
--- a/dist/tinykeys.cjs
|
|
||||||
+++ b/dist/tinykeys.cjs
|
|
||||||
@@ -61,6 +61,18 @@ function defaultKeybindingsHandlerIgnore(event) {
|
|
||||||
function getModifierState(event, mod) {
|
|
||||||
return typeof event.getModifierState === "function" ? event.getModifierState(mod) || ALT_GRAPH_ALIASES.includes(mod) && event.getModifierState("AltGraph") : false;
|
|
||||||
}
|
|
||||||
+function splitKeybindingPress(press) {
|
|
||||||
+ let parts = [];
|
|
||||||
+ let start = 0;
|
|
||||||
+ for (let index = 0; index < press.length; index++) {
|
|
||||||
+ if (press[index] === "+" && /[\w\]]/.test(press[index - 1] || "")) {
|
|
||||||
+ parts.push(press.slice(start, index));
|
|
||||||
+ start = index + 1;
|
|
||||||
+ }
|
|
||||||
+ }
|
|
||||||
+ parts.push(press.slice(start));
|
|
||||||
+ return parts;
|
|
||||||
+}
|
|
||||||
/**
|
|
||||||
* Parses a keybinding string into its parts.
|
|
||||||
*
|
|
||||||
@@ -76,10 +88,10 @@ function getModifierState(event, mod) {
|
|
||||||
*/
|
|
||||||
function parseKeybinding(str) {
|
|
||||||
return str.trim().split(" ").map((press) => {
|
|
||||||
- let parts = press.split(/(?<=\w|\])\+/);
|
|
||||||
+ let parts = splitKeybindingPress(press);
|
|
||||||
let last = parts.pop();
|
|
||||||
let regex = last.match(/^\((.+)\)$/);
|
|
||||||
- let key = regex ? new RegExp(`^(?:${regex[1]})$`, "iv") : last;
|
|
||||||
+ let key = regex ? new RegExp(`^(?:${regex[1]})$`, "i") : last;
|
|
||||||
let requiredModifiers = [];
|
|
||||||
let optionalModifiers = [];
|
|
||||||
for (const part of parts) {
|
|
||||||
@@ -201,5 +213,3 @@ exports.defaultKeybindingsHandlerIgnore = defaultKeybindingsHandlerIgnore;
|
|
||||||
exports.matchKeybindingPress = matchKeybindingPress;
|
|
||||||
exports.parseKeybinding = parseKeybinding;
|
|
||||||
exports.tinykeys = tinykeys;
|
|
||||||
-
|
|
||||||
-//# sourceMappingURL=tinykeys.cjs.map
|
|
||||||
\ No newline at end of file
|
|
||||||
diff --git a/dist/tinykeys.mjs b/dist/tinykeys.mjs
|
|
||||||
index c289972d2728e03d9b272268c38fd3392e8845bf..e22897b00aae6cdb0dbbb971445227c07be52918 100644
|
|
||||||
--- a/dist/tinykeys.mjs
|
|
||||||
+++ b/dist/tinykeys.mjs
|
|
||||||
@@ -60,6 +60,18 @@ function defaultKeybindingsHandlerIgnore(event) {
|
|
||||||
function getModifierState(event, mod) {
|
|
||||||
return typeof event.getModifierState === "function" ? event.getModifierState(mod) || ALT_GRAPH_ALIASES.includes(mod) && event.getModifierState("AltGraph") : false;
|
|
||||||
}
|
|
||||||
+function splitKeybindingPress(press) {
|
|
||||||
+ let parts = [];
|
|
||||||
+ let start = 0;
|
|
||||||
+ for (let index = 0; index < press.length; index++) {
|
|
||||||
+ if (press[index] === "+" && /[\w\]]/.test(press[index - 1] || "")) {
|
|
||||||
+ parts.push(press.slice(start, index));
|
|
||||||
+ start = index + 1;
|
|
||||||
+ }
|
|
||||||
+ }
|
|
||||||
+ parts.push(press.slice(start));
|
|
||||||
+ return parts;
|
|
||||||
+}
|
|
||||||
/**
|
|
||||||
* Parses a keybinding string into its parts.
|
|
||||||
*
|
|
||||||
@@ -75,10 +87,10 @@ function getModifierState(event, mod) {
|
|
||||||
*/
|
|
||||||
function parseKeybinding(str) {
|
|
||||||
return str.trim().split(" ").map((press) => {
|
|
||||||
- let parts = press.split(/(?<=\w|\])\+/);
|
|
||||||
+ let parts = splitKeybindingPress(press);
|
|
||||||
let last = parts.pop();
|
|
||||||
let regex = last.match(/^\((.+)\)$/);
|
|
||||||
- let key = regex ? new RegExp(`^(?:${regex[1]})$`, "iv") : last;
|
|
||||||
+ let key = regex ? new RegExp(`^(?:${regex[1]})$`, "i") : last;
|
|
||||||
let requiredModifiers = [];
|
|
||||||
let optionalModifiers = [];
|
|
||||||
for (const part of parts) {
|
|
||||||
@@ -196,5 +208,3 @@ function tinykeys(target, keybindingMap, options = {}) {
|
|
||||||
}
|
|
||||||
//#endregion
|
|
||||||
export { createKeybindingsHandler, defaultKeybindingsHandlerIgnore, matchKeybindingPress, parseKeybinding, tinykeys };
|
|
||||||
-
|
|
||||||
-//# sourceMappingURL=tinykeys.mjs.map
|
|
||||||
\ No newline at end of file
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
diff --git a/build/inject-manifest.js b/build/inject-manifest.js
|
|
||||||
index 60e3d2bb51c11a19fbbedbad65e101082ec41c36..fed6026630f43f86e25446383982cf6fb694313b 100644
|
|
||||||
--- a/build/inject-manifest.js
|
|
||||||
+++ b/build/inject-manifest.js
|
|
||||||
@@ -104,7 +104,7 @@ async function injectManifest(config) {
|
|
||||||
replaceString: manifestString,
|
|
||||||
searchString: options.injectionPoint,
|
|
||||||
});
|
|
||||||
- filesToWrite[options.swDest] = source;
|
|
||||||
+ filesToWrite[options.swDest] = source.replace(url, encodeURI(upath_1.default.basename(destPath)));
|
|
||||||
filesToWrite[destPath] = map;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
diff --git a/build/lib/translate-url-to-sourcemap-paths.js b/build/lib/translate-url-to-sourcemap-paths.js
|
|
||||||
index 3220c5474eeac6e8a56ca9b2ac2bd9be48529e43..5f003879a904d4840529a42dd056d288fd213771 100644
|
|
||||||
--- a/build/lib/translate-url-to-sourcemap-paths.js
|
|
||||||
+++ b/build/lib/translate-url-to-sourcemap-paths.js
|
|
||||||
@@ -22,7 +22,7 @@ function translateURLToSourcemapPaths(url, swSrc, swDest) {
|
|
||||||
const possibleSrcPath = upath_1.default.resolve(upath_1.default.dirname(swSrc), url);
|
|
||||||
if (fs_extra_1.default.existsSync(possibleSrcPath)) {
|
|
||||||
srcPath = possibleSrcPath;
|
|
||||||
- destPath = upath_1.default.resolve(upath_1.default.dirname(swDest), url);
|
|
||||||
+ destPath = `${swDest}.map`;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
warning = `${errors_1.errors['cant-find-sourcemap']} ${possibleSrcPath}`;
|
|
||||||
diff --git a/src/inject-manifest.ts b/src/inject-manifest.ts
|
|
||||||
index 8795ddcaa77aea7b0356417e4bc4b19e2b3f860c..fcdc68342d9ac53936c9ed40a9ccfc2f5070cad3 100644
|
|
||||||
--- a/src/inject-manifest.ts
|
|
||||||
+++ b/src/inject-manifest.ts
|
|
||||||
@@ -129,7 +129,10 @@ export async function injectManifest(
|
|
||||||
searchString: options.injectionPoint!,
|
|
||||||
});
|
|
||||||
|
|
||||||
- filesToWrite[options.swDest] = source;
|
|
||||||
+ filesToWrite[options.swDest] = source.replace(
|
|
||||||
+ url!,
|
|
||||||
+ encodeURI(upath.basename(destPath)),
|
|
||||||
+ );
|
|
||||||
filesToWrite[destPath] = map;
|
|
||||||
} else {
|
|
||||||
// If there's no sourcemap associated with swSrc, a simple string
|
|
||||||
diff --git a/src/lib/translate-url-to-sourcemap-paths.ts b/src/lib/translate-url-to-sourcemap-paths.ts
|
|
||||||
index 072eac40d4ef5d095a01cb7f7e392a9e034853bd..f0bbe69e88ef3a415de18a7e9cb264daea273d71 100644
|
|
||||||
--- a/src/lib/translate-url-to-sourcemap-paths.ts
|
|
||||||
+++ b/src/lib/translate-url-to-sourcemap-paths.ts
|
|
||||||
@@ -28,7 +28,7 @@ export function translateURLToSourcemapPaths(
|
|
||||||
const possibleSrcPath = upath.resolve(upath.dirname(swSrc), url);
|
|
||||||
if (fse.existsSync(possibleSrcPath)) {
|
|
||||||
srcPath = possibleSrcPath;
|
|
||||||
- destPath = upath.resolve(upath.dirname(swDest), url);
|
|
||||||
+ destPath = `${swDest}.map`;
|
|
||||||
} else {
|
|
||||||
warning = `${errors['cant-find-sourcemap']} ${possibleSrcPath}`;
|
|
||||||
}
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+631
File diff suppressed because one or more lines are too long
Vendored
-944
File diff suppressed because one or more lines are too long
+6
-13
@@ -1,16 +1,9 @@
|
|||||||
approvedGitRepositories:
|
|
||||||
- "**"
|
|
||||||
|
|
||||||
compressionLevel: mixed
|
|
||||||
|
|
||||||
defaultSemverRangePrefix: ""
|
|
||||||
|
|
||||||
enableGlobalCache: false
|
|
||||||
|
|
||||||
enableScripts: true
|
|
||||||
|
|
||||||
nodeLinker: node-modules
|
nodeLinker: node-modules
|
||||||
|
|
||||||
npmMinimalAgeGate: 3d
|
plugins:
|
||||||
|
- path: .yarn/plugins/@yarnpkg/plugin-typescript.cjs
|
||||||
|
spec: "@yarnpkg/plugin-typescript"
|
||||||
|
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
|
||||||
|
spec: "@yarnpkg/plugin-interactive-tools"
|
||||||
|
|
||||||
yarnPath: .yarn/releases/yarn-4.17.1.cjs
|
yarnPath: .yarn/releases/yarn-3.0.2.cjs
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
# Home Assistant Frontend Agent Guide
|
|
||||||
|
|
||||||
You are helping develop the Home Assistant frontend. This repository is a TypeScript application built from Lit-based Web Components for the Home Assistant web UI.
|
|
||||||
|
|
||||||
For gallery-specific documentation, demos, page structure, and examples, read `gallery/AGENTS.md` when working under `gallery/`.
|
|
||||||
|
|
||||||
## Essential Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
yarn lint # ESLint + Prettier + TypeScript + Lit
|
|
||||||
yarn format # Auto-fix ESLint + Prettier
|
|
||||||
yarn lint:types # TypeScript compiler, run without file arguments
|
|
||||||
yarn test # Vitest
|
|
||||||
yarn dev # App dev server, supports --background/--status/--stop/--logs
|
|
||||||
yarn dev:serve # Local serving dev server, supports -c core URL, -p port, and dev flags
|
|
||||||
```
|
|
||||||
|
|
||||||
Never run `tsc` or `yarn lint:types` with file arguments. When `tsc` receives file arguments, it ignores `tsconfig.json` and can emit `.js` files into `src/`. Always run `yarn lint:types` without arguments. For individual file type checking, rely on editor diagnostics.
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
- The frontend uses custom elements built with Lit and TypeScript strict mode.
|
|
||||||
- Components communicate with the backend through the Home Assistant WebSocket API.
|
|
||||||
- Use `ha-` for Home Assistant components, `hui-` for Lovelace UI components, and `dialog-` for dialogs.
|
|
||||||
- Prefer `ha-*` components and current Web Awesome wrappers. Avoid adding new legacy `mwc-*` usage.
|
|
||||||
- Leaf components should consume narrow Lit contexts instead of taking the broad `hass` object unless they are containers that own and provide `hass`.
|
|
||||||
|
|
||||||
## Development Standards
|
|
||||||
|
|
||||||
- Use strict TypeScript, proper interfaces, and `import type` for type-only imports.
|
|
||||||
- Avoid `any`; model data with existing Home Assistant types or narrow new types.
|
|
||||||
- Keep imports organized and remove unused imports.
|
|
||||||
- Do not use `console`; use existing logging or user-visible error patterns.
|
|
||||||
- Use `@state()` for internal Lit state and `@property()` for public API.
|
|
||||||
- Do not query or manipulate DOM manually when Lit decorators, component refs, or render state are appropriate.
|
|
||||||
- Scope styles to components, use theme custom properties, and keep layouts mobile-first and RTL-safe.
|
|
||||||
- All user-facing text must be localized through the translation system.
|
|
||||||
|
|
||||||
## Project Skills
|
|
||||||
|
|
||||||
Detailed guidance lives in project skills under `.agents/skills/`. Load the matching skill before detailed implementation or review:
|
|
||||||
|
|
||||||
- `ha-frontend-contexts`: Lit contexts, `hass` migration, and rerender-sensitive state access.
|
|
||||||
- `ha-frontend-components`: dialogs, forms, alerts, shortcuts, tooltips, panels, and Lovelace cards.
|
|
||||||
- `ha-frontend-styling`: theme variables, spacing tokens, responsive layout, RTL, and view transitions.
|
|
||||||
- `ha-frontend-testing`: lint, typecheck, Vitest, Playwright e2e dev servers, and benchmarks.
|
|
||||||
- `ha-frontend-user-facing-text`: localization, terminology, sentence case, and Home Assistant text style.
|
|
||||||
- `ha-frontend-review`: PR template use, review checklist, and recurring review issues.
|
|
||||||
|
|
||||||
## Pull Requests
|
|
||||||
|
|
||||||
When creating a pull request, use `.github/PULL_REQUEST_TEMPLATE.md` as the PR body. Preserve template sections, check only the appropriate type-of-change boxes, and do not check checklist items on behalf of the user. If the PR includes UI changes, remind the user to add screenshots or a short video.
|
|
||||||
@@ -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
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
include README.md
|
include README.md
|
||||||
|
include LICENSE.md
|
||||||
graft hass_frontend
|
graft hass_frontend
|
||||||
graft hass_frontend_es5
|
graft hass_frontend_es5
|
||||||
recursive-exclude * *.py[co]
|
recursive-exclude * *.py[co]
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
This is the repository for the official [Home Assistant](https://home-assistant.io) frontend.
|
This is the repository for the official [Home Assistant](https://home-assistant.io) frontend.
|
||||||
|
|
||||||
[](https://demo.home-assistant.io/)
|
[](https://demo.home-assistant.io/)
|
||||||
|
|
||||||
- [View demo of Home Assistant](https://demo.home-assistant.io/)
|
- [View demo of Home Assistant](https://demo.home-assistant.io/)
|
||||||
- [More information about Home Assistant](https://home-assistant.io)
|
- [More information about Home Assistant](https://home-assistant.io)
|
||||||
@@ -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/)
|
- Development: [Instructions](https://developers.home-assistant.io/docs/frontend/development/)
|
||||||
- Production build: `script/build_frontend`
|
- Production build: `script/build_frontend`
|
||||||
- Gallery: `cd gallery && script/develop_gallery`
|
- Gallery: `cd gallery && script/develop_gallery`
|
||||||
|
- Supervisor: [Instructions](https://developers.home-assistant.io/docs/supervisor/developing)
|
||||||
|
|
||||||
## Frontend development
|
## Frontend development
|
||||||
|
|
||||||
@@ -26,5 +27,3 @@ A complete guide can be found at the following [link](https://www.home-assistant
|
|||||||
Home Assistant is open-source and Apache 2 licensed. Feel free to browse the repository, learn and reuse parts in your own projects.
|
Home Assistant is open-source and Apache 2 licensed. Feel free to browse the repository, learn and reuse parts in your own projects.
|
||||||
|
|
||||||
We use [BrowserStack](https://www.browserstack.com) to test Home Assistant on a large variety of devices.
|
We use [BrowserStack](https://www.browserstack.com) to test Home Assistant on a large variety of devices.
|
||||||
|
|
||||||
[](https://www.openhomefoundation.org/)
|
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"rules": {
|
||||||
|
"import/no-extraneous-dependencies": 0,
|
||||||
|
"no-restricted-syntax": 0,
|
||||||
|
"no-console": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"extends": "../.eslintrc.json",
|
||||||
|
"rules": {
|
||||||
|
"import/no-extraneous-dependencies": 0,
|
||||||
|
"global-require": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@ The Home Assistant build pipeline contains various steps to prepare a build.
|
|||||||
|
|
||||||
Currently in Home Assistant we use a bundler to convert TypeScript, CSS and JSON files to JavaScript files that the browser understands.
|
Currently in Home Assistant we use a bundler to convert TypeScript, CSS and JSON files to JavaScript files that the browser understands.
|
||||||
|
|
||||||
We currently rely on Webpack. Both of these programs bundle the converted files in both production and development.
|
We currently rely on Webpack but also have experimental Rollup support. Both of these programs bundle the converted files in both production and development.
|
||||||
|
|
||||||
For development, bundling is optional. We just want to get the right files in the browser.
|
For development, bundling is optional. We just want to get the right files in the browser.
|
||||||
|
|
||||||
|
|||||||
@@ -1,150 +0,0 @@
|
|||||||
import defineProvider from "@babel/helper-define-polyfill-provider";
|
|
||||||
import { join } from "node:path";
|
|
||||||
import paths from "../paths.cjs";
|
|
||||||
|
|
||||||
const POLYFILL_DIR = join(paths.root_dir, "src/resources/polyfills");
|
|
||||||
|
|
||||||
// List of polyfill keys with supported browser targets for the functionality
|
|
||||||
const polyfillSupport = {
|
|
||||||
// Note states and shadowRoot properties should be supported.
|
|
||||||
"element-internals": {
|
|
||||||
android: 90,
|
|
||||||
chrome: 90,
|
|
||||||
edge: 90,
|
|
||||||
firefox: 126,
|
|
||||||
ios: 17.4,
|
|
||||||
opera: 76,
|
|
||||||
opera_mobile: 64,
|
|
||||||
safari: 17.4,
|
|
||||||
samsung: 15.0,
|
|
||||||
},
|
|
||||||
"element-getattributenames": {
|
|
||||||
android: 61,
|
|
||||||
chrome: 61,
|
|
||||||
edge: 18,
|
|
||||||
firefox: 45,
|
|
||||||
ios: 10.3,
|
|
||||||
opera: 48,
|
|
||||||
opera_mobile: 45,
|
|
||||||
safari: 10.1,
|
|
||||||
samsung: 8.0,
|
|
||||||
},
|
|
||||||
"element-toggleattribute": {
|
|
||||||
android: 69,
|
|
||||||
chrome: 69,
|
|
||||||
edge: 18,
|
|
||||||
firefox: 63,
|
|
||||||
ios: 12.0,
|
|
||||||
opera: 56,
|
|
||||||
opera_mobile: 48,
|
|
||||||
safari: 12.0,
|
|
||||||
samsung: 10.0,
|
|
||||||
},
|
|
||||||
// FormatJS polyfill detects fix for https://bugs.chromium.org/p/v8/issues/detail?id=10682,
|
|
||||||
// so adjusted to several months after that was marked fixed
|
|
||||||
"intl-getcanonicallocales": {
|
|
||||||
android: 90,
|
|
||||||
chrome: 90,
|
|
||||||
edge: 90,
|
|
||||||
firefox: 48,
|
|
||||||
ios: 10.3,
|
|
||||||
opera: 76,
|
|
||||||
opera_mobile: 64,
|
|
||||||
safari: 10.1,
|
|
||||||
samsung: 15.0,
|
|
||||||
},
|
|
||||||
"intl-locale": {
|
|
||||||
android: 74,
|
|
||||||
chrome: 74,
|
|
||||||
edge: 79,
|
|
||||||
firefox: 75,
|
|
||||||
ios: 14.0,
|
|
||||||
opera: 62,
|
|
||||||
opera_mobile: 53,
|
|
||||||
safari: 14.0,
|
|
||||||
samsung: 11.0,
|
|
||||||
},
|
|
||||||
"intl-other": {
|
|
||||||
// Not specified (i.e. always try polyfill) since compatibility depends on supported locales
|
|
||||||
},
|
|
||||||
"resize-observer": {
|
|
||||||
android: 64,
|
|
||||||
chrome: 64,
|
|
||||||
edge: 79,
|
|
||||||
firefox: 69,
|
|
||||||
ios: 13.4,
|
|
||||||
opera: 51,
|
|
||||||
opera_mobile: 47,
|
|
||||||
safari: 13.1,
|
|
||||||
samsung: 9.0,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Map of global variables and/or instance and static properties to the
|
|
||||||
// corresponding polyfill key and actual module to import
|
|
||||||
const polyfillMap = {
|
|
||||||
global: {
|
|
||||||
ResizeObserver: {
|
|
||||||
key: "resize-observer",
|
|
||||||
module: join(POLYFILL_DIR, "resize-observer.ts"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
instance: {
|
|
||||||
attachInternals: {
|
|
||||||
key: "element-internals",
|
|
||||||
module: "element-internals-polyfill",
|
|
||||||
},
|
|
||||||
...Object.fromEntries(
|
|
||||||
["getAttributeNames", "toggleAttribute"].map((prop) => {
|
|
||||||
const key = `element-${prop.toLowerCase()}`;
|
|
||||||
return [prop, { key, module: join(POLYFILL_DIR, `${key}.ts`) }];
|
|
||||||
})
|
|
||||||
),
|
|
||||||
},
|
|
||||||
static: {
|
|
||||||
Intl: {
|
|
||||||
getCanonicalLocales: {
|
|
||||||
key: "intl-getcanonicallocales",
|
|
||||||
module: join(POLYFILL_DIR, "intl-polyfill.ts"),
|
|
||||||
},
|
|
||||||
Locale: {
|
|
||||||
key: "intl-locale",
|
|
||||||
module: join(POLYFILL_DIR, "intl-polyfill.ts"),
|
|
||||||
},
|
|
||||||
...Object.fromEntries(
|
|
||||||
[
|
|
||||||
"DateTimeFormat",
|
|
||||||
"DurationFormat",
|
|
||||||
"DisplayNames",
|
|
||||||
"ListFormat",
|
|
||||||
"NumberFormat",
|
|
||||||
"PluralRules",
|
|
||||||
"RelativeTimeFormat",
|
|
||||||
].map((obj) => [
|
|
||||||
obj,
|
|
||||||
{ key: "intl-other", module: join(POLYFILL_DIR, "intl-polyfill.ts") },
|
|
||||||
])
|
|
||||||
),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create plugin using the same factory as for CoreJS
|
|
||||||
export default defineProvider(
|
|
||||||
({ createMetaResolver, debug, shouldInjectPolyfill }) => {
|
|
||||||
const resolvePolyfill = createMetaResolver(polyfillMap);
|
|
||||||
return {
|
|
||||||
name: "custom-polyfill",
|
|
||||||
polyfills: polyfillSupport,
|
|
||||||
usageGlobal(meta, utils) {
|
|
||||||
const polyfill = resolvePolyfill(meta);
|
|
||||||
if (polyfill && shouldInjectPolyfill(polyfill.desc.key)) {
|
|
||||||
debug(polyfill.desc.key);
|
|
||||||
utils.injectGlobalImport(polyfill.desc.module);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
const path = require("path");
|
|
||||||
|
|
||||||
// Currently only supports CommonJS modules, as require is synchronous. `import` would need babel running asynchronous.
|
|
||||||
module.exports = function inlineConstants(babel, options, cwd) {
|
|
||||||
const t = babel.types;
|
|
||||||
|
|
||||||
if (!Array.isArray(options.modules)) {
|
|
||||||
throw new TypeError(
|
|
||||||
"babel-plugin-inline-constants: expected a `modules` array to be passed"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.resolveExtensions && !Array.isArray(options.resolveExtensions)) {
|
|
||||||
throw new TypeError(
|
|
||||||
"babel-plugin-inline-constants: expected `resolveExtensions` to be an array"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const ignoreModuleNotFound = options.ignoreModuleNotFound;
|
|
||||||
const resolveExtensions = options.resolveExtensions;
|
|
||||||
|
|
||||||
const hasRelativeModules = options.modules.some(
|
|
||||||
(module) => module.startsWith(".") || module.startsWith("/")
|
|
||||||
);
|
|
||||||
|
|
||||||
const modules = Object.fromEntries(
|
|
||||||
options.modules.map((module) => {
|
|
||||||
const absolute = module.startsWith(".")
|
|
||||||
? require.resolve(module, { paths: [cwd] })
|
|
||||||
: module;
|
|
||||||
return [absolute, require(absolute)];
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const toLiteral = (value) => {
|
|
||||||
if (typeof value === "string") {
|
|
||||||
return t.stringLiteral(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof value === "number") {
|
|
||||||
return t.numericLiteral(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof value === "boolean") {
|
|
||||||
return t.booleanLiteral(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value === null) {
|
|
||||||
return t.nullLiteral();
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(
|
|
||||||
"babel-plugin-inline-constants: cannot handle non-literal `" + value + "`"
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const resolveAbsolute = (value, state, resolveExtensionIndex) => {
|
|
||||||
if (!state.filename) {
|
|
||||||
throw new TypeError(
|
|
||||||
"babel-plugin-inline-constants: expected a `filename` to be set for files"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (resolveExtensions && resolveExtensionIndex !== undefined) {
|
|
||||||
value += resolveExtensions[resolveExtensionIndex];
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return require.resolve(value, { paths: [path.dirname(state.filename)] });
|
|
||||||
} catch (error) {
|
|
||||||
if (
|
|
||||||
error.code === "MODULE_NOT_FOUND" &&
|
|
||||||
resolveExtensions &&
|
|
||||||
(resolveExtensionIndex === undefined ||
|
|
||||||
resolveExtensionIndex < resolveExtensions.length - 1)
|
|
||||||
) {
|
|
||||||
const resolveExtensionIdx = (resolveExtensionIndex || -1) + 1;
|
|
||||||
return resolveAbsolute(value, state, resolveExtensionIdx);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error.code === "MODULE_NOT_FOUND" && ignoreModuleNotFound) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const importDeclaration = (p, state) => {
|
|
||||||
if (p.node.type !== "ImportDeclaration") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const absolute =
|
|
||||||
hasRelativeModules && p.node.source.value.startsWith(".")
|
|
||||||
? resolveAbsolute(p.node.source.value, state)
|
|
||||||
: p.node.source.value;
|
|
||||||
|
|
||||||
if (!absolute || !(absolute in modules)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const module = modules[absolute];
|
|
||||||
|
|
||||||
for (const specifier of p.node.specifiers) {
|
|
||||||
if (
|
|
||||||
specifier.type === "ImportDefaultSpecifier" &&
|
|
||||||
specifier.local &&
|
|
||||||
specifier.local.type === "Identifier"
|
|
||||||
) {
|
|
||||||
if (!("default" in module)) {
|
|
||||||
throw new Error(
|
|
||||||
"babel-plugin-inline-constants: cannot access default export from `" +
|
|
||||||
p.node.source.value +
|
|
||||||
"`"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const variableValue = toLiteral(module.default);
|
|
||||||
const variable = t.variableDeclarator(
|
|
||||||
t.identifier(specifier.local.name),
|
|
||||||
variableValue
|
|
||||||
);
|
|
||||||
|
|
||||||
p.insertBefore({
|
|
||||||
type: "VariableDeclaration",
|
|
||||||
kind: "const",
|
|
||||||
declarations: [variable],
|
|
||||||
});
|
|
||||||
} else if (
|
|
||||||
specifier.type === "ImportSpecifier" &&
|
|
||||||
specifier.imported &&
|
|
||||||
specifier.imported.type === "Identifier" &&
|
|
||||||
specifier.local &&
|
|
||||||
specifier.local.type === "Identifier"
|
|
||||||
) {
|
|
||||||
if (!(specifier.imported.name in module)) {
|
|
||||||
throw new Error(
|
|
||||||
"babel-plugin-inline-constants: cannot access `" +
|
|
||||||
specifier.imported.name +
|
|
||||||
"` from `" +
|
|
||||||
p.node.source.value +
|
|
||||||
"`"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const variableValue = toLiteral(module[specifier.imported.name]);
|
|
||||||
const variable = t.variableDeclarator(
|
|
||||||
t.identifier(specifier.local.name),
|
|
||||||
variableValue
|
|
||||||
);
|
|
||||||
|
|
||||||
p.insertBefore({
|
|
||||||
type: "VariableDeclaration",
|
|
||||||
kind: "const",
|
|
||||||
declarations: [variable],
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
throw new Error("Cannot handle specifier `" + specifier.type + "`");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
p.remove();
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
visitor: {
|
|
||||||
ImportDeclaration: importDeclaration,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
// Currently only supports CommonJS modules, as require is synchronous. `import` would need babel running asynchronous.
|
||||||
|
module.exports = function inlineConstants(babel, options, cwd) {
|
||||||
|
const t = babel.types;
|
||||||
|
|
||||||
|
if (!Array.isArray(options.modules)) {
|
||||||
|
throw new TypeError(
|
||||||
|
"babel-plugin-inline-constants: expected a `modules` array to be passed"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.resolveExtensions && !Array.isArray(options.resolveExtensions)) {
|
||||||
|
throw new TypeError(
|
||||||
|
"babel-plugin-inline-constants: expected `resolveExtensions` to be an array"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ignoreModuleNotFound = options.ignoreModuleNotFound;
|
||||||
|
const resolveExtensions = options.resolveExtensions;
|
||||||
|
|
||||||
|
const hasRelativeModules = options.modules.some(
|
||||||
|
(module) => module.startsWith(".") || module.startsWith("/")
|
||||||
|
);
|
||||||
|
|
||||||
|
const modules = Object.fromEntries(
|
||||||
|
options.modules.map((module) => {
|
||||||
|
const absolute = module.startsWith(".")
|
||||||
|
? require.resolve(module, { paths: [cwd] })
|
||||||
|
: module;
|
||||||
|
// eslint-disable-next-line import/no-dynamic-require
|
||||||
|
return [absolute, require(absolute)];
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const toLiteral = (value) => {
|
||||||
|
if (typeof value === "string") {
|
||||||
|
return t.stringLiteral(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === "number") {
|
||||||
|
return t.numericLiteral(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === "boolean") {
|
||||||
|
return t.booleanLiteral(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value === null) {
|
||||||
|
return t.nullLiteral();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
"babel-plugin-inline-constants: cannot handle non-literal `" + value + "`"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveAbsolute = (value, state, resolveExtensionIndex) => {
|
||||||
|
if (!state.filename) {
|
||||||
|
throw new TypeError(
|
||||||
|
"babel-plugin-inline-constants: expected a `filename` to be set for files"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resolveExtensions && resolveExtensionIndex !== undefined) {
|
||||||
|
value += resolveExtensions[resolveExtensionIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return require.resolve(value, { paths: [path.dirname(state.filename)] });
|
||||||
|
} catch (error) {
|
||||||
|
if (
|
||||||
|
error.code === "MODULE_NOT_FOUND" &&
|
||||||
|
resolveExtensions &&
|
||||||
|
(resolveExtensionIndex === undefined ||
|
||||||
|
resolveExtensionIndex < resolveExtensions.length - 1)
|
||||||
|
) {
|
||||||
|
const resolveExtensionIdx = (resolveExtensionIndex || -1) + 1;
|
||||||
|
return resolveAbsolute(value, state, resolveExtensionIdx);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.code === "MODULE_NOT_FOUND" && ignoreModuleNotFound) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const importDeclaration = (p, state) => {
|
||||||
|
if (p.node.type !== "ImportDeclaration") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const absolute =
|
||||||
|
hasRelativeModules && p.node.source.value.startsWith(".")
|
||||||
|
? resolveAbsolute(p.node.source.value, state)
|
||||||
|
: p.node.source.value;
|
||||||
|
|
||||||
|
if (!absolute || !(absolute in modules)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const module = modules[absolute];
|
||||||
|
|
||||||
|
for (const specifier of p.node.specifiers) {
|
||||||
|
if (
|
||||||
|
specifier.type === "ImportDefaultSpecifier" &&
|
||||||
|
specifier.local &&
|
||||||
|
specifier.local.type === "Identifier"
|
||||||
|
) {
|
||||||
|
if (!("default" in module)) {
|
||||||
|
throw new Error(
|
||||||
|
"babel-plugin-inline-constants: cannot access default export from `" +
|
||||||
|
p.node.source.value +
|
||||||
|
"`"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const variableValue = toLiteral(module.default);
|
||||||
|
const variable = t.variableDeclarator(
|
||||||
|
t.identifier(specifier.local.name),
|
||||||
|
variableValue
|
||||||
|
);
|
||||||
|
|
||||||
|
p.insertBefore({
|
||||||
|
type: "VariableDeclaration",
|
||||||
|
kind: "const",
|
||||||
|
declarations: [variable],
|
||||||
|
});
|
||||||
|
} else if (
|
||||||
|
specifier.type === "ImportSpecifier" &&
|
||||||
|
specifier.imported &&
|
||||||
|
specifier.imported.type === "Identifier" &&
|
||||||
|
specifier.local &&
|
||||||
|
specifier.local.type === "Identifier"
|
||||||
|
) {
|
||||||
|
if (!(specifier.imported.name in module)) {
|
||||||
|
throw new Error(
|
||||||
|
"babel-plugin-inline-constants: cannot access `" +
|
||||||
|
specifier.imported.name +
|
||||||
|
"` from `" +
|
||||||
|
p.node.source.value +
|
||||||
|
"`"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const variableValue = toLiteral(module[specifier.imported.name]);
|
||||||
|
const variable = t.variableDeclarator(
|
||||||
|
t.identifier(specifier.local.name),
|
||||||
|
variableValue
|
||||||
|
);
|
||||||
|
|
||||||
|
p.insertBefore({
|
||||||
|
type: "VariableDeclaration",
|
||||||
|
kind: "const",
|
||||||
|
declarations: [variable],
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new Error("Cannot handle specifier `" + specifier.type + "`");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.remove();
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
visitor: {
|
||||||
|
ImportDeclaration: importDeclaration,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"_comment": "Initial JS budget (raw/uncompressed bytes) for the cold-load critical entrypoints. Enforced by build-scripts/check-bundle-size.cjs in CI. Re-seed after an intentional change with `--update --headroom=<percent>`.",
|
|
||||||
"frontend-modern": {
|
|
||||||
"app": 561513,
|
|
||||||
"core": 54473,
|
|
||||||
"authorize": 544272,
|
|
||||||
"onboarding": 647136
|
|
||||||
},
|
|
||||||
"frontend-legacy": {
|
|
||||||
"app": 790323,
|
|
||||||
"core": 237208,
|
|
||||||
"authorize": 765464,
|
|
||||||
"onboarding": 918679
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,326 +0,0 @@
|
|||||||
const path = require("path");
|
|
||||||
const env = require("./env.cjs");
|
|
||||||
const paths = require("./paths.cjs");
|
|
||||||
const { dependencies } = require("../package.json");
|
|
||||||
|
|
||||||
const BABEL_PLUGINS = path.join(__dirname, "babel-plugins");
|
|
||||||
|
|
||||||
// GitHub base URL to use for production source maps
|
|
||||||
// Nightly builds use the commit SHA, otherwise assumes there is a tag that matches the version
|
|
||||||
module.exports.sourceMapURL = () => {
|
|
||||||
const ref = env.version().endsWith("dev")
|
|
||||||
? process.env.GITHUB_SHA || "dev"
|
|
||||||
: env.version();
|
|
||||||
return `https://raw.githubusercontent.com/home-assistant/frontend/${ref}/`;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Files from NPM Packages that should not be imported
|
|
||||||
module.exports.ignorePackages = () => [];
|
|
||||||
|
|
||||||
// Files from NPM packages that we should replace with empty file
|
|
||||||
module.exports.emptyPackages = ({ isLandingPageBuild }) =>
|
|
||||||
[
|
|
||||||
// Icons in landingpage conflict with icons in HA so we don't load.
|
|
||||||
isLandingPageBuild &&
|
|
||||||
require.resolve(
|
|
||||||
path.resolve(paths.root_dir, "src/components/ha-icon.ts")
|
|
||||||
),
|
|
||||||
isLandingPageBuild &&
|
|
||||||
require.resolve(
|
|
||||||
path.resolve(paths.root_dir, "src/components/ha-icon-picker.ts")
|
|
||||||
),
|
|
||||||
].filter(Boolean);
|
|
||||||
|
|
||||||
module.exports.definedVars = ({ isProdBuild, latestBuild, defineOverlay }) => ({
|
|
||||||
__DEV__: !isProdBuild,
|
|
||||||
__BUILD__: JSON.stringify(latestBuild ? "modern" : "legacy"),
|
|
||||||
__VERSION__: JSON.stringify(env.version()),
|
|
||||||
__DEMO__: false,
|
|
||||||
__BACKWARDS_COMPAT__: false,
|
|
||||||
__STATIC_PATH__: "/static/",
|
|
||||||
__HASS_URL__: `\`${
|
|
||||||
"HASS_URL" in process.env
|
|
||||||
? process.env.HASS_URL
|
|
||||||
: // eslint-disable-next-line no-template-curly-in-string
|
|
||||||
"${location.protocol}//${location.host}"
|
|
||||||
}\``,
|
|
||||||
"process.env.NODE_ENV": JSON.stringify(
|
|
||||||
isProdBuild ? "production" : "development"
|
|
||||||
),
|
|
||||||
...defineOverlay,
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports.htmlMinifierOptions = {
|
|
||||||
caseSensitive: true,
|
|
||||||
collapseWhitespace: true,
|
|
||||||
conservativeCollapse: true,
|
|
||||||
decodeEntities: true,
|
|
||||||
removeComments: true,
|
|
||||||
removeRedundantAttributes: true,
|
|
||||||
minifyCSS: {
|
|
||||||
compatibility: "*,-properties.zeroUnits",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports.terserOptions = ({ latestBuild, isTestBuild }) => ({
|
|
||||||
safari10: !latestBuild,
|
|
||||||
ecma: latestBuild ? 2015 : 5,
|
|
||||||
module: latestBuild,
|
|
||||||
format: { comments: false },
|
|
||||||
sourceMap: !isTestBuild,
|
|
||||||
});
|
|
||||||
|
|
||||||
/** @type {import('@rspack/core').SwcLoaderOptions} */
|
|
||||||
module.exports.swcOptions = () => ({
|
|
||||||
jsc: {
|
|
||||||
loose: true,
|
|
||||||
externalHelpers: true,
|
|
||||||
target: "ES2021",
|
|
||||||
parser: {
|
|
||||||
syntax: "typescript",
|
|
||||||
decorators: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports.babelOptions = ({ latestBuild, isTestBuild, sw }) => ({
|
|
||||||
babelrc: false,
|
|
||||||
compact: false,
|
|
||||||
assumptions: {
|
|
||||||
privateFieldsAsProperties: true,
|
|
||||||
setPublicClassFields: true,
|
|
||||||
setSpreadProperties: true,
|
|
||||||
},
|
|
||||||
browserslistEnv: latestBuild ? "modern" : `legacy${sw ? "-sw" : ""}`,
|
|
||||||
presets: [
|
|
||||||
[
|
|
||||||
"@babel/preset-env",
|
|
||||||
{
|
|
||||||
shippedProposals: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
],
|
|
||||||
plugins: [
|
|
||||||
// Inject Core-JS polyfills on demand. Babel 8 removed preset-env's
|
|
||||||
// `useBuiltIns`/`corejs` options, so the equivalent polyfill provider is
|
|
||||||
// configured directly here (`usage-global` matches the old `useBuiltIns: "usage"`).
|
|
||||||
[
|
|
||||||
"babel-plugin-polyfill-corejs3",
|
|
||||||
{
|
|
||||||
method: "usage-global",
|
|
||||||
version: dependencies["core-js"],
|
|
||||||
shippedProposals: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[
|
|
||||||
path.join(BABEL_PLUGINS, "inline-constants-plugin.cjs"),
|
|
||||||
{
|
|
||||||
modules: ["@mdi/js"],
|
|
||||||
ignoreModuleNotFound: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
// Import helpers and regenerator from runtime package.
|
|
||||||
// `moduleName` is pinned so helpers resolve from `@babel/runtime`: the
|
|
||||||
// corejs3 polyfill provider above otherwise redirects them to the
|
|
||||||
// (uninstalled) `@babel/runtime-corejs3`, which preset-env used to suppress
|
|
||||||
// internally when it owned the polyfill injection via `useBuiltIns`.
|
|
||||||
[
|
|
||||||
"@babel/plugin-transform-runtime",
|
|
||||||
{ version: dependencies["@babel/runtime"], moduleName: "@babel/runtime" },
|
|
||||||
],
|
|
||||||
"@babel/plugin-transform-class-properties",
|
|
||||||
"@babel/plugin-transform-private-methods",
|
|
||||||
].filter(Boolean),
|
|
||||||
exclude: [
|
|
||||||
// \\ for Windows, / for Mac OS and Linux
|
|
||||||
/node_modules[\\/]core-js/,
|
|
||||||
],
|
|
||||||
sourceMaps: !isTestBuild,
|
|
||||||
overrides: [
|
|
||||||
{
|
|
||||||
// Add plugin to inject various polyfills, excluding the polyfills
|
|
||||||
// themselves to prevent self-injection.
|
|
||||||
plugins: [
|
|
||||||
[
|
|
||||||
path.join(BABEL_PLUGINS, "custom-polyfill-plugin.js"),
|
|
||||||
{ method: "usage-global" },
|
|
||||||
],
|
|
||||||
],
|
|
||||||
exclude: [
|
|
||||||
path.join(paths.root_dir, "src/resources/polyfills"),
|
|
||||||
...[
|
|
||||||
"@formatjs/(?:ecma402-abstract|intl-\\w+)",
|
|
||||||
"@lit-labs/virtualizer/polyfills",
|
|
||||||
"@webcomponents/scoped-custom-element-registry",
|
|
||||||
"element-internals-polyfill",
|
|
||||||
"proxy-polyfill",
|
|
||||||
"unfetch",
|
|
||||||
].map((p) => new RegExp(`/node_modules/${p}/`)),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// Use unambiguous for dependencies so that require() is correctly injected into CommonJS files
|
|
||||||
// Exclusions are needed in some cases where ES modules have no static imports or exports, such as polyfills
|
|
||||||
// (otherwise babel-plugin-polyfill-corejs3 injects bare require("core-js/modules/...") calls
|
|
||||||
// that rspack does not transform, causing ReferenceError in browsers like Safari 14).
|
|
||||||
sourceType: "unambiguous",
|
|
||||||
include: /\/node_modules\//,
|
|
||||||
exclude: [
|
|
||||||
"element-internals-polyfill",
|
|
||||||
"@?lit(?:-labs|-element|-html)?",
|
|
||||||
"@formatjs/(?:ecma402-abstract|intl-\\w+)",
|
|
||||||
].map((p) => new RegExp(`/node_modules/${p}/`)),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
const nameSuffix = (latestBuild) => (latestBuild ? "-modern" : "-legacy");
|
|
||||||
|
|
||||||
const outputPath = (outputRoot, latestBuild) =>
|
|
||||||
path.resolve(outputRoot, latestBuild ? "frontend_latest" : "frontend_es5");
|
|
||||||
|
|
||||||
const publicPath = (latestBuild, root = "") =>
|
|
||||||
latestBuild ? `${root}/frontend_latest/` : `${root}/frontend_es5/`;
|
|
||||||
|
|
||||||
/*
|
|
||||||
BundleConfig {
|
|
||||||
// Object with entrypoints that need to be bundled
|
|
||||||
entry: { [name: string]: pathToFile },
|
|
||||||
// Folder where bundled files need to be written
|
|
||||||
outputPath: string,
|
|
||||||
// absolute url-path where bundled files can be found
|
|
||||||
publicPath: string,
|
|
||||||
// extra definitions that we need to replace in source
|
|
||||||
defineOverlay: {[name: string]: value },
|
|
||||||
// if this is a production build
|
|
||||||
isProdBuild: boolean,
|
|
||||||
// If we're targeting latest browsers
|
|
||||||
latestBuild: boolean,
|
|
||||||
// If we're doing a stats build (create nice chunk names)
|
|
||||||
isStatsBuild: boolean,
|
|
||||||
// If it's just a test build in CI, skip time on source map generation
|
|
||||||
isTestBuild: boolean,
|
|
||||||
// Names of entrypoints that should not be hashed
|
|
||||||
dontHash: Set<string>
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
module.exports.config = {
|
|
||||||
app({ isProdBuild, latestBuild, isStatsBuild, isTestBuild, isWDS }) {
|
|
||||||
return {
|
|
||||||
name: "frontend" + nameSuffix(latestBuild),
|
|
||||||
entry: {
|
|
||||||
"service-worker": !latestBuild
|
|
||||||
? {
|
|
||||||
import: "./src/entrypoints/service-worker.ts",
|
|
||||||
layer: "sw",
|
|
||||||
}
|
|
||||||
: "./src/entrypoints/service-worker.ts",
|
|
||||||
app: "./src/entrypoints/app.ts",
|
|
||||||
authorize: "./src/entrypoints/authorize.ts",
|
|
||||||
onboarding: "./src/entrypoints/onboarding.ts",
|
|
||||||
core: "./src/entrypoints/core.ts",
|
|
||||||
"custom-panel": "./src/entrypoints/custom-panel.ts",
|
|
||||||
},
|
|
||||||
outputPath: outputPath(paths.app_output_root, latestBuild),
|
|
||||||
publicPath: publicPath(latestBuild),
|
|
||||||
isProdBuild,
|
|
||||||
latestBuild,
|
|
||||||
isStatsBuild,
|
|
||||||
isTestBuild,
|
|
||||||
isWDS,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
demo({ isProdBuild, latestBuild, isStatsBuild }) {
|
|
||||||
return {
|
|
||||||
name: "demo" + nameSuffix(latestBuild),
|
|
||||||
entry: {
|
|
||||||
main: path.resolve(paths.demo_dir, "src/entrypoint.ts"),
|
|
||||||
},
|
|
||||||
outputPath: outputPath(paths.demo_output_root, latestBuild),
|
|
||||||
publicPath: publicPath(latestBuild),
|
|
||||||
defineOverlay: {
|
|
||||||
__VERSION__: JSON.stringify(`DEMO-${env.version()}`),
|
|
||||||
__DEMO__: true,
|
|
||||||
},
|
|
||||||
isProdBuild,
|
|
||||||
latestBuild,
|
|
||||||
isStatsBuild,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
cast({ isProdBuild, latestBuild }) {
|
|
||||||
const entry = {
|
|
||||||
launcher: path.resolve(paths.cast_dir, "src/launcher/entrypoint.ts"),
|
|
||||||
media: path.resolve(paths.cast_dir, "src/media/entrypoint.ts"),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (latestBuild) {
|
|
||||||
entry.receiver = path.resolve(
|
|
||||||
paths.cast_dir,
|
|
||||||
"src/receiver/entrypoint.ts"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: "cast" + nameSuffix(latestBuild),
|
|
||||||
entry,
|
|
||||||
outputPath: outputPath(paths.cast_output_root, latestBuild),
|
|
||||||
publicPath: publicPath(latestBuild),
|
|
||||||
isProdBuild,
|
|
||||||
latestBuild,
|
|
||||||
defineOverlay: {
|
|
||||||
__BACKWARDS_COMPAT__: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
gallery({ isProdBuild, latestBuild }) {
|
|
||||||
return {
|
|
||||||
name: "gallery" + nameSuffix(latestBuild),
|
|
||||||
entry: {
|
|
||||||
entrypoint: path.resolve(paths.gallery_dir, "src/entrypoint.js"),
|
|
||||||
},
|
|
||||||
outputPath: outputPath(paths.gallery_output_root, latestBuild),
|
|
||||||
publicPath: publicPath(latestBuild),
|
|
||||||
isProdBuild,
|
|
||||||
latestBuild,
|
|
||||||
defineOverlay: {
|
|
||||||
__DEMO__: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
landingPage({ isProdBuild, latestBuild }) {
|
|
||||||
return {
|
|
||||||
name: "landing-page" + nameSuffix(latestBuild),
|
|
||||||
entry: {
|
|
||||||
entrypoint: path.resolve(paths.landingPage_dir, "src/entrypoint.js"),
|
|
||||||
},
|
|
||||||
outputPath: outputPath(paths.landingPage_output_root, latestBuild),
|
|
||||||
publicPath: publicPath(latestBuild),
|
|
||||||
isProdBuild,
|
|
||||||
latestBuild,
|
|
||||||
isLandingPageBuild: true,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
e2eTestApp({ isProdBuild, latestBuild, isStatsBuild }) {
|
|
||||||
return {
|
|
||||||
name: "e2e-test-app" + nameSuffix(latestBuild),
|
|
||||||
entry: {
|
|
||||||
main: path.resolve(paths.e2eTestApp_dir, "src/entrypoint.ts"),
|
|
||||||
},
|
|
||||||
outputPath: outputPath(paths.e2eTestApp_output_root, latestBuild),
|
|
||||||
publicPath: publicPath(latestBuild),
|
|
||||||
defineOverlay: {
|
|
||||||
__VERSION__: JSON.stringify(`E2E-TEST-${env.version()}`),
|
|
||||||
__DEMO__: true,
|
|
||||||
},
|
|
||||||
isProdBuild,
|
|
||||||
latestBuild,
|
|
||||||
isStatsBuild,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,211 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
|
const path = require("path");
|
||||||
|
const env = require("./env.js");
|
||||||
|
const paths = require("./paths.js");
|
||||||
|
|
||||||
|
// Files from NPM Packages that should not be imported
|
||||||
|
module.exports.ignorePackages = ({ latestBuild }) => [
|
||||||
|
// Part of yaml.js and only used for !!js functions that we don't use
|
||||||
|
require.resolve("esprima"),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Files from NPM packages that we should replace with empty file
|
||||||
|
module.exports.emptyPackages = ({ latestBuild }) =>
|
||||||
|
[
|
||||||
|
// Contains all color definitions for all material color sets.
|
||||||
|
// We don't use it
|
||||||
|
require.resolve("@polymer/paper-styles/color.js"),
|
||||||
|
require.resolve("@polymer/paper-styles/default-theme.js"),
|
||||||
|
// Loads stuff from a CDN
|
||||||
|
require.resolve("@polymer/font-roboto/roboto.js"),
|
||||||
|
require.resolve("@vaadin/vaadin-material-styles/typography.js"),
|
||||||
|
require.resolve("@vaadin/vaadin-material-styles/font-icons.js"),
|
||||||
|
// Compatibility not needed for latest builds
|
||||||
|
latestBuild &&
|
||||||
|
// wrapped in require.resolve so it blows up if file no longer exists
|
||||||
|
require.resolve(
|
||||||
|
path.resolve(paths.polymer_dir, "src/resources/compatibility.ts")
|
||||||
|
),
|
||||||
|
// This polyfill is loaded in workers to support ES5, filter it out.
|
||||||
|
latestBuild && require.resolve("proxy-polyfill/src/index.js"),
|
||||||
|
].filter(Boolean);
|
||||||
|
|
||||||
|
module.exports.definedVars = ({ isProdBuild, latestBuild, defineOverlay }) => ({
|
||||||
|
__DEV__: !isProdBuild,
|
||||||
|
__BUILD__: JSON.stringify(latestBuild ? "latest" : "es5"),
|
||||||
|
__VERSION__: JSON.stringify(env.version()),
|
||||||
|
__DEMO__: false,
|
||||||
|
__BACKWARDS_COMPAT__: false,
|
||||||
|
__STATIC_PATH__: "/static/",
|
||||||
|
"process.env.NODE_ENV": JSON.stringify(
|
||||||
|
isProdBuild ? "production" : "development"
|
||||||
|
),
|
||||||
|
...defineOverlay,
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports.terserOptions = (latestBuild) => ({
|
||||||
|
safari10: !latestBuild,
|
||||||
|
ecma: latestBuild ? undefined : 5,
|
||||||
|
output: { comments: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports.babelOptions = ({ latestBuild }) => ({
|
||||||
|
babelrc: false,
|
||||||
|
compact: false,
|
||||||
|
presets: [
|
||||||
|
!latestBuild && [
|
||||||
|
"@babel/preset-env",
|
||||||
|
{
|
||||||
|
useBuiltIns: "entry",
|
||||||
|
corejs: "3.15",
|
||||||
|
bugfixes: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"@babel/preset-typescript",
|
||||||
|
].filter(Boolean),
|
||||||
|
plugins: [
|
||||||
|
[
|
||||||
|
path.resolve(
|
||||||
|
paths.polymer_dir,
|
||||||
|
"build-scripts/babel-plugins/inline-constants-plugin.js"
|
||||||
|
),
|
||||||
|
{
|
||||||
|
modules: ["@mdi/js"],
|
||||||
|
ignoreModuleNotFound: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
// Part of ES2018. Converts {...a, b: 2} to Object.assign({}, a, {b: 2})
|
||||||
|
!latestBuild && [
|
||||||
|
"@babel/plugin-proposal-object-rest-spread",
|
||||||
|
{ loose: true, useBuiltIns: true },
|
||||||
|
],
|
||||||
|
// Only support the syntax, Webpack will handle it.
|
||||||
|
"@babel/plugin-syntax-import-meta",
|
||||||
|
"@babel/plugin-syntax-dynamic-import",
|
||||||
|
"@babel/plugin-syntax-top-level-await",
|
||||||
|
"@babel/plugin-proposal-optional-chaining",
|
||||||
|
"@babel/plugin-proposal-nullish-coalescing-operator",
|
||||||
|
["@babel/plugin-proposal-decorators", { decoratorsBeforeExport: true }],
|
||||||
|
["@babel/plugin-proposal-private-methods", { loose: true }],
|
||||||
|
["@babel/plugin-proposal-private-property-in-object", { loose: true }],
|
||||||
|
["@babel/plugin-proposal-class-properties", { loose: true }],
|
||||||
|
].filter(Boolean),
|
||||||
|
exclude: [
|
||||||
|
// \\ for Windows, / for Mac OS and Linux
|
||||||
|
/node_modules[\\/]core-js/,
|
||||||
|
/node_modules[\\/]webpack[\\/]buildin/,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const outputPath = (outputRoot, latestBuild) =>
|
||||||
|
path.resolve(outputRoot, latestBuild ? "frontend_latest" : "frontend_es5");
|
||||||
|
|
||||||
|
const publicPath = (latestBuild, root = "") =>
|
||||||
|
latestBuild ? `${root}/frontend_latest/` : `${root}/frontend_es5/`;
|
||||||
|
|
||||||
|
/*
|
||||||
|
BundleConfig {
|
||||||
|
// Object with entrypoints that need to be bundled
|
||||||
|
entry: { [name: string]: pathToFile },
|
||||||
|
// Folder where bundled files need to be written
|
||||||
|
outputPath: string,
|
||||||
|
// absolute url-path where bundled files can be found
|
||||||
|
publicPath: string,
|
||||||
|
// extra definitions that we need to replace in source
|
||||||
|
defineOverlay: {[name: string]: value },
|
||||||
|
// if this is a production build
|
||||||
|
isProdBuild: boolean,
|
||||||
|
// If we're targeting latest browsers
|
||||||
|
latestBuild: boolean,
|
||||||
|
// If we're doing a stats build (create nice chunk names)
|
||||||
|
isStatsBuild: boolean,
|
||||||
|
// Names of entrypoints that should not be hashed
|
||||||
|
dontHash: Set<string>
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports.config = {
|
||||||
|
app({ isProdBuild, latestBuild, isStatsBuild, isWDS }) {
|
||||||
|
return {
|
||||||
|
entry: {
|
||||||
|
service_worker: "./src/entrypoints/service_worker.ts",
|
||||||
|
app: "./src/entrypoints/app.ts",
|
||||||
|
authorize: "./src/entrypoints/authorize.ts",
|
||||||
|
onboarding: "./src/entrypoints/onboarding.ts",
|
||||||
|
core: "./src/entrypoints/core.ts",
|
||||||
|
"custom-panel": "./src/entrypoints/custom-panel.ts",
|
||||||
|
},
|
||||||
|
outputPath: outputPath(paths.app_output_root, latestBuild),
|
||||||
|
publicPath: publicPath(latestBuild),
|
||||||
|
isProdBuild,
|
||||||
|
latestBuild,
|
||||||
|
isStatsBuild,
|
||||||
|
isWDS,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
demo({ isProdBuild, latestBuild, isStatsBuild }) {
|
||||||
|
return {
|
||||||
|
entry: {
|
||||||
|
main: path.resolve(paths.demo_dir, "src/entrypoint.ts"),
|
||||||
|
},
|
||||||
|
outputPath: outputPath(paths.demo_output_root, latestBuild),
|
||||||
|
publicPath: publicPath(latestBuild),
|
||||||
|
defineOverlay: {
|
||||||
|
__VERSION__: JSON.stringify(`DEMO-${env.version()}`),
|
||||||
|
__DEMO__: true,
|
||||||
|
},
|
||||||
|
isProdBuild,
|
||||||
|
latestBuild,
|
||||||
|
isStatsBuild,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
cast({ isProdBuild, latestBuild }) {
|
||||||
|
const entry = {
|
||||||
|
launcher: path.resolve(paths.cast_dir, "src/launcher/entrypoint.ts"),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (latestBuild) {
|
||||||
|
entry.receiver = path.resolve(
|
||||||
|
paths.cast_dir,
|
||||||
|
"src/receiver/entrypoint.ts"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
entry,
|
||||||
|
outputPath: outputPath(paths.cast_output_root, latestBuild),
|
||||||
|
publicPath: publicPath(latestBuild),
|
||||||
|
isProdBuild,
|
||||||
|
latestBuild,
|
||||||
|
defineOverlay: {
|
||||||
|
__BACKWARDS_COMPAT__: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
hassio({ isProdBuild, latestBuild }) {
|
||||||
|
return {
|
||||||
|
entry: {
|
||||||
|
entrypoint: path.resolve(paths.hassio_dir, "src/entrypoint.ts"),
|
||||||
|
},
|
||||||
|
outputPath: outputPath(paths.hassio_output_root, latestBuild),
|
||||||
|
publicPath: publicPath(latestBuild, paths.hassio_publicPath),
|
||||||
|
isProdBuild,
|
||||||
|
latestBuild,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
gallery({ isProdBuild, latestBuild }) {
|
||||||
|
return {
|
||||||
|
entry: {
|
||||||
|
entrypoint: path.resolve(paths.gallery_dir, "src/entrypoint.js"),
|
||||||
|
},
|
||||||
|
outputPath: outputPath(paths.gallery_output_root, latestBuild),
|
||||||
|
publicPath: publicPath(latestBuild),
|
||||||
|
isProdBuild,
|
||||||
|
latestBuild,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,155 +0,0 @@
|
|||||||
/* global require, process, __dirname */
|
|
||||||
// Enforce a strict size budget on the initial JS of the most critical
|
|
||||||
// entrypoints (`app` and `core`). These two are downloaded on every cold load
|
|
||||||
// before anything interactive can happen, so unintended growth here hurts
|
|
||||||
// first-load performance directly.
|
|
||||||
//
|
|
||||||
// In production rspack does not split initial chunks (splitChunks only operates
|
|
||||||
// on `!chunk.canBeInitial()`), so each entrypoint resolves to a single initial
|
|
||||||
// JS asset. We read the per-build stats written by StatsWriterPlugin and compare
|
|
||||||
// the entrypoint's initial JS size against a committed budget.
|
|
||||||
//
|
|
||||||
// Usage:
|
|
||||||
// node build-scripts/check-bundle-size.cjs # enforce, exit 1 on regression
|
|
||||||
// node build-scripts/check-bundle-size.cjs --update # rewrite budgets from current sizes
|
|
||||||
// node build-scripts/check-bundle-size.cjs --update --headroom=3 # current + 3% headroom
|
|
||||||
|
|
||||||
const fs = require("fs");
|
|
||||||
const path = require("path");
|
|
||||||
const paths = require("./paths.cjs");
|
|
||||||
|
|
||||||
// Entrypoints whose initial JS we hold to a strict budget. These are all
|
|
||||||
// downloaded on a user-facing cold load before anything interactive can happen:
|
|
||||||
// `app`/`core` for the main app, plus the standalone `authorize` and
|
|
||||||
// `onboarding` pages. `custom-panel` is intentionally excluded (only loaded
|
|
||||||
// when a custom panel is opened).
|
|
||||||
const TRACKED_ENTRYPOINTS = ["app", "core", "authorize", "onboarding"];
|
|
||||||
|
|
||||||
// App build stats files, as written by StatsWriterPlugin (`${name}.json`).
|
|
||||||
const BUILDS = ["frontend-modern", "frontend-legacy"];
|
|
||||||
|
|
||||||
const BUDGET_FILE = path.join(__dirname, "bundle-budget.json");
|
|
||||||
const STATS_DIR = path.join(paths.build_dir, "stats");
|
|
||||||
|
|
||||||
const readStats = (build) => {
|
|
||||||
const file = path.join(STATS_DIR, `${build}.json`);
|
|
||||||
if (!fs.existsSync(file)) {
|
|
||||||
throw new Error(
|
|
||||||
`Missing stats file: ${path.relative(process.cwd(), file)}.\n` +
|
|
||||||
`Run a production build first (e.g. \`gulp build-app\`), then re-run this check.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Initial JS bytes for an entrypoint = sum of the .js asset sizes of its initial
|
|
||||||
// entry chunk(s). Sizes are raw (uncompressed) bytes, matching the stats output.
|
|
||||||
const entrypointInitialJS = (stats, entrypoint) => {
|
|
||||||
const assetSize = new Map(stats.assets.map((a) => [a.name, a.size]));
|
|
||||||
let total = 0;
|
|
||||||
let found = false;
|
|
||||||
for (const chunk of stats.chunks) {
|
|
||||||
if (!chunk.entry || !chunk.initial) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!(chunk.names || []).includes(entrypoint)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
found = true;
|
|
||||||
for (const file of chunk.files || []) {
|
|
||||||
if (file.endsWith(".js") && assetSize.has(file)) {
|
|
||||||
total += assetSize.get(file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!found) {
|
|
||||||
throw new Error(`Entrypoint "${entrypoint}" not found in bundle stats.`);
|
|
||||||
}
|
|
||||||
return total;
|
|
||||||
};
|
|
||||||
|
|
||||||
const kib = (bytes) => `${(bytes / 1024).toFixed(1)} KiB`;
|
|
||||||
|
|
||||||
const main = () => {
|
|
||||||
const update = process.argv.includes("--update");
|
|
||||||
const headroomArg = process.argv.find((a) => a.startsWith("--headroom="));
|
|
||||||
const headroom = headroomArg ? Number(headroomArg.split("=")[1]) : 0;
|
|
||||||
|
|
||||||
const current = {};
|
|
||||||
for (const build of BUILDS) {
|
|
||||||
const stats = readStats(build);
|
|
||||||
current[build] = {};
|
|
||||||
for (const entrypoint of TRACKED_ENTRYPOINTS) {
|
|
||||||
current[build][entrypoint] = entrypointInitialJS(stats, entrypoint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (update) {
|
|
||||||
const budget = { _comment: BUDGET_COMMENT };
|
|
||||||
for (const build of BUILDS) {
|
|
||||||
budget[build] = {};
|
|
||||||
for (const entrypoint of TRACKED_ENTRYPOINTS) {
|
|
||||||
budget[build][entrypoint] = Math.ceil(
|
|
||||||
current[build][entrypoint] * (1 + headroom / 100)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fs.writeFileSync(BUDGET_FILE, `${JSON.stringify(budget, null, 2)}\n`);
|
|
||||||
console.log(
|
|
||||||
`Updated ${path.relative(process.cwd(), BUDGET_FILE)} from current sizes` +
|
|
||||||
(headroom ? ` (+${headroom}% headroom).` : ".")
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!fs.existsSync(BUDGET_FILE)) {
|
|
||||||
throw new Error(
|
|
||||||
`Missing budget file ${path.relative(process.cwd(), BUDGET_FILE)}.\n` +
|
|
||||||
`Seed it from a production build with: node build-scripts/check-bundle-size.cjs --update --headroom=3`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const budget = JSON.parse(fs.readFileSync(BUDGET_FILE, "utf8"));
|
|
||||||
|
|
||||||
let failed = false;
|
|
||||||
console.log("Initial JS budget (entry chunks, raw bytes):\n");
|
|
||||||
for (const build of BUILDS) {
|
|
||||||
for (const entrypoint of TRACKED_ENTRYPOINTS) {
|
|
||||||
const actual = current[build][entrypoint];
|
|
||||||
const limit = budget[build] && budget[build][entrypoint];
|
|
||||||
if (typeof limit !== "number") {
|
|
||||||
failed = true;
|
|
||||||
console.log(
|
|
||||||
` ✗ ${build} / ${entrypoint}: no budget set (current ${kib(actual)})`
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const ok = actual <= limit;
|
|
||||||
const delta = (((actual - limit) / limit) * 100).toFixed(1);
|
|
||||||
console.log(
|
|
||||||
` ${ok ? "✓" : "✗"} ${build} / ${entrypoint}: ` +
|
|
||||||
`${kib(actual)} / ${kib(limit)}${ok ? "" : ` (+${delta}% over budget)`}`
|
|
||||||
);
|
|
||||||
if (!ok) {
|
|
||||||
failed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (failed) {
|
|
||||||
console.error(
|
|
||||||
"\nInitial JS budget exceeded for a critical entrypoint.\n" +
|
|
||||||
"Investigate what was pulled into the entry chunk (a static import that should be lazy?).\n" +
|
|
||||||
"If the growth is intentional, re-seed the budget:\n" +
|
|
||||||
" node build-scripts/check-bundle-size.cjs --update --headroom=3"
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
console.log("\nAll tracked entrypoints within budget.");
|
|
||||||
};
|
|
||||||
|
|
||||||
const BUDGET_COMMENT =
|
|
||||||
"Initial JS budget (raw/uncompressed bytes) for the cold-load critical entrypoints. " +
|
|
||||||
"Enforced by build-scripts/check-bundle-size.cjs in CI. " +
|
|
||||||
"Re-seed after an intentional change with `--update --headroom=<percent>`.";
|
|
||||||
|
|
||||||
main();
|
|
||||||
@@ -1,678 +0,0 @@
|
|||||||
// Manage a Home Assistant frontend dev server with an agent-friendly interface.
|
|
||||||
//
|
|
||||||
// node build-scripts/dev-server.mjs --suite <suite> [mode] [extra args]
|
|
||||||
//
|
|
||||||
// (no mode) Run in the foreground.
|
|
||||||
// --background Start detached, wait until it is ready, print the URL
|
|
||||||
// (when it has one) and pid, then exit and leave it running.
|
|
||||||
// --status Report whether the suite's dev server is running.
|
|
||||||
// --stop Stop a running background dev server.
|
|
||||||
// --logs [--follow] Print (or follow) the background dev server log.
|
|
||||||
//
|
|
||||||
// Extra args (for example -p or -c on app-serve) are forwarded to the underlying
|
|
||||||
// script. Suites use one of two liveness models:
|
|
||||||
//
|
|
||||||
// health demo, gallery, e2e-app: a fixed port plus the /__ha_dev_status
|
|
||||||
// endpoint each dev server exposes (see runDevServer in
|
|
||||||
// build-scripts/gulp/rspack.js). The port is the source of truth and
|
|
||||||
// the pid is found from it; no state file.
|
|
||||||
// process app (yarn dev) and app-serve (yarn dev:serve): the app watcher has
|
|
||||||
// no health endpoint, and plain yarn dev has no port at all, so these
|
|
||||||
// track a pidfile and treat the first "Build done" log line as ready.
|
|
||||||
|
|
||||||
import { spawn, execFileSync } from "node:child_process";
|
|
||||||
import fs from "node:fs";
|
|
||||||
import path from "node:path";
|
|
||||||
import { fileURLToPath } from "node:url";
|
|
||||||
|
|
||||||
const repoRoot = path.resolve(
|
|
||||||
path.dirname(fileURLToPath(import.meta.url)),
|
|
||||||
".."
|
|
||||||
);
|
|
||||||
const gulpBin = path.join(repoRoot, "node_modules", ".bin", "gulp");
|
|
||||||
const developAndServeScript = path.join(
|
|
||||||
repoRoot,
|
|
||||||
"script",
|
|
||||||
"develop_and_serve"
|
|
||||||
);
|
|
||||||
const logDir = path.join(repoRoot, "node_modules", ".cache", "ha-dev-server");
|
|
||||||
|
|
||||||
// Each suite names its yarn alias (for hints), a liveness model, and how to
|
|
||||||
// spawn it. health suites carry a fixed port; process suites carry the log line
|
|
||||||
// that means "ready" and, for app-serve, forward extra args to the script.
|
|
||||||
const SUITES = {
|
|
||||||
"e2e-app": {
|
|
||||||
alias: "test:e2e:app:dev",
|
|
||||||
liveness: "health",
|
|
||||||
port: 8095,
|
|
||||||
spawn: { cmd: gulpBin, args: ["develop-e2e-test-app"] },
|
|
||||||
},
|
|
||||||
demo: {
|
|
||||||
alias: "dev:demo",
|
|
||||||
liveness: "health",
|
|
||||||
port: 8090,
|
|
||||||
spawn: { cmd: gulpBin, args: ["develop-demo"] },
|
|
||||||
},
|
|
||||||
gallery: {
|
|
||||||
alias: "dev:gallery",
|
|
||||||
liveness: "health",
|
|
||||||
port: 8100,
|
|
||||||
spawn: { cmd: gulpBin, args: ["develop-gallery"] },
|
|
||||||
},
|
|
||||||
app: {
|
|
||||||
alias: "dev",
|
|
||||||
liveness: "process",
|
|
||||||
readyLog: /Build done @/,
|
|
||||||
spawn: { cmd: gulpBin, args: ["develop-app"] },
|
|
||||||
},
|
|
||||||
"app-serve": {
|
|
||||||
alias: "dev:serve",
|
|
||||||
liveness: "process",
|
|
||||||
acceptsArgs: true,
|
|
||||||
readyLog: /Build done @/,
|
|
||||||
spawn: { cmd: developAndServeScript, args: [] },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Cover a cold build on a slow machine before the server starts listening.
|
|
||||||
// Override with HA_DEV_SERVER_TIMEOUT (seconds).
|
|
||||||
const READY_TIMEOUT_MS =
|
|
||||||
Number(process.env.HA_DEV_SERVER_TIMEOUT || "180") * 1000;
|
|
||||||
|
|
||||||
// Detect a coding agent from a small set of environment markers set by common
|
|
||||||
// agent CLIs (env-only; no process-ancestry detection).
|
|
||||||
const detectAgent = () => {
|
|
||||||
const env = process.env;
|
|
||||||
const has = (name) => Boolean(env[name]);
|
|
||||||
const eq = (name, value) => env[name] === value;
|
|
||||||
const signals = {
|
|
||||||
opencode: () =>
|
|
||||||
[
|
|
||||||
"OPENCODE",
|
|
||||||
"OPENCODE_BIN_PATH",
|
|
||||||
"OPENCODE_SERVER",
|
|
||||||
"OPENCODE_APP_INFO",
|
|
||||||
].some(has),
|
|
||||||
"claude-code": () => has("CLAUDECODE"),
|
|
||||||
cursor: () => has("CURSOR_TRACE_ID"),
|
|
||||||
"github-copilot": () =>
|
|
||||||
eq("TERM_PROGRAM", "vscode") && eq("GIT_PAGER", "cat"),
|
|
||||||
// Convention shared by several agents (Crush, Amp, ...).
|
|
||||||
generic: () => has("AGENT") || has("AI_AGENT"),
|
|
||||||
};
|
|
||||||
return Object.keys(signals).find((id) => signals[id]());
|
|
||||||
};
|
|
||||||
|
|
||||||
const usage = () => {
|
|
||||||
const suites = Object.keys(SUITES).join("|");
|
|
||||||
process.stderr.write(
|
|
||||||
`Usage: node build-scripts/dev-server.mjs --suite <${suites}> ` +
|
|
||||||
`[--background | --status | --stop | --logs [--follow]]\n`
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const parseArgs = (argv) => {
|
|
||||||
const args = {
|
|
||||||
mode: "foreground",
|
|
||||||
follow: false,
|
|
||||||
suite: undefined,
|
|
||||||
passthrough: [],
|
|
||||||
};
|
|
||||||
for (let i = 0; i < argv.length; i++) {
|
|
||||||
const arg = argv[i];
|
|
||||||
switch (arg) {
|
|
||||||
case "--suite":
|
|
||||||
args.suite = argv[++i];
|
|
||||||
break;
|
|
||||||
case "--background":
|
|
||||||
args.mode = "background";
|
|
||||||
break;
|
|
||||||
case "--status":
|
|
||||||
args.mode = "status";
|
|
||||||
break;
|
|
||||||
case "--stop":
|
|
||||||
args.mode = "stop";
|
|
||||||
break;
|
|
||||||
case "--logs":
|
|
||||||
args.mode = "logs";
|
|
||||||
break;
|
|
||||||
case "--follow":
|
|
||||||
args.follow = true;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
// Anything unrecognised is forwarded to the underlying script.
|
|
||||||
args.passthrough.push(arg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return args;
|
|
||||||
};
|
|
||||||
|
|
||||||
const sleep = (ms) =>
|
|
||||||
new Promise((resolve) => {
|
|
||||||
setTimeout(resolve, ms);
|
|
||||||
});
|
|
||||||
|
|
||||||
const logFileFor = (suite) => path.join(logDir, `${suite}.log`);
|
|
||||||
const pidFileFor = (suite) => path.join(logDir, `${suite}.pid`);
|
|
||||||
|
|
||||||
const hints = (suite) => {
|
|
||||||
const alias = `yarn ${SUITES[suite].alias}`;
|
|
||||||
return (
|
|
||||||
` Stop: ${alias} --stop\n` +
|
|
||||||
` Status: ${alias} --status\n` +
|
|
||||||
` Logs: ${alias} --logs\n`
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// --- shared spawning and lifecycle ------------------------------------------
|
|
||||||
|
|
||||||
// Signal the whole process group (the background server is its group leader),
|
|
||||||
// falling back to the bare pid if that is not permitted.
|
|
||||||
const killProcessTree = (pid, sig) => {
|
|
||||||
try {
|
|
||||||
process.kill(-pid, sig);
|
|
||||||
} catch {
|
|
||||||
try {
|
|
||||||
process.kill(pid, sig);
|
|
||||||
} catch {
|
|
||||||
// Already gone.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const urlSuffix = (port) => (port ? ` at http://localhost:${port}` : "");
|
|
||||||
|
|
||||||
// Run a server in the foreground, inheriting stdio; resolve with its exit code.
|
|
||||||
const spawnInherit = (cmd, args) =>
|
|
||||||
new Promise((resolve) => {
|
|
||||||
const child = spawn(cmd, args, { cwd: repoRoot, stdio: "inherit" });
|
|
||||||
child.on("exit", (code) => resolve(code ?? 0));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Spawn a detached server that writes stdout and stderr to the suite's log file.
|
|
||||||
const spawnDetachedToLog = (suite, cmd, args) => {
|
|
||||||
fs.mkdirSync(logDir, { recursive: true });
|
|
||||||
const logFile = logFileFor(suite);
|
|
||||||
const fd = fs.openSync(logFile, "w");
|
|
||||||
const child = spawn(cmd, args, {
|
|
||||||
cwd: repoRoot,
|
|
||||||
detached: true,
|
|
||||||
stdio: ["ignore", fd, fd],
|
|
||||||
});
|
|
||||||
fs.closeSync(fd);
|
|
||||||
child.unref();
|
|
||||||
return { child, logFile };
|
|
||||||
};
|
|
||||||
|
|
||||||
// Poll until the server is ready, the child exits, or we time out. Prints the
|
|
||||||
// progress dots and outcome; returns 0 when ready, 1 otherwise. onExit runs if
|
|
||||||
// the child dies before it is ready (used to clear a stale pidfile).
|
|
||||||
const awaitReady = async ({ suite, child, logFile, port, isReady, onExit }) => {
|
|
||||||
let childExited = false;
|
|
||||||
child.on("exit", () => {
|
|
||||||
childExited = true;
|
|
||||||
});
|
|
||||||
const deadline = Date.now() + READY_TIMEOUT_MS;
|
|
||||||
process.stdout.write(`Starting ${suite} dev server`);
|
|
||||||
/* eslint-disable no-await-in-loop -- poll until the server is ready */
|
|
||||||
while (Date.now() < deadline) {
|
|
||||||
if (childExited) {
|
|
||||||
process.stdout.write("\n");
|
|
||||||
process.stderr.write(
|
|
||||||
`Dev server (${suite}) exited before it was ready. See ${logFile}\n`
|
|
||||||
);
|
|
||||||
onExit?.();
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
if (await isReady()) {
|
|
||||||
process.stdout.write("\n");
|
|
||||||
process.stdout.write(
|
|
||||||
`Dev server (${suite}) running${urlSuffix(port)} ` +
|
|
||||||
`(pid ${child.pid})\n${hints(suite)}`
|
|
||||||
);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
process.stdout.write(".");
|
|
||||||
await sleep(1000);
|
|
||||||
}
|
|
||||||
/* eslint-enable no-await-in-loop */
|
|
||||||
process.stdout.write("\n");
|
|
||||||
process.stderr.write(
|
|
||||||
`Dev server (${suite}) did not become ready within ${
|
|
||||||
READY_TIMEOUT_MS / 1000
|
|
||||||
}s. See ${logFile}\n`
|
|
||||||
);
|
|
||||||
return 1;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Stop a running background server: SIGTERM, wait for it to go, then SIGKILL.
|
|
||||||
// isStopped reports when it is gone; onStopped runs on success (pidfile cleanup).
|
|
||||||
const terminate = async (suite, pid, isStopped, onStopped) => {
|
|
||||||
killProcessTree(pid, "SIGTERM");
|
|
||||||
const deadline = Date.now() + 10_000;
|
|
||||||
/* eslint-disable no-await-in-loop -- poll until the server is gone */
|
|
||||||
while (Date.now() < deadline) {
|
|
||||||
await sleep(300);
|
|
||||||
if (await isStopped()) {
|
|
||||||
onStopped?.();
|
|
||||||
process.stdout.write(`Stopped dev server (${suite}) (pid ${pid}).\n`);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/* eslint-enable no-await-in-loop */
|
|
||||||
// Escalate if it is still up.
|
|
||||||
killProcessTree(pid, "SIGKILL");
|
|
||||||
await sleep(300);
|
|
||||||
if (!(await isStopped())) {
|
|
||||||
process.stderr.write(
|
|
||||||
`Failed to stop dev server (${suite}) (pid ${pid}). Stop it manually.\n`
|
|
||||||
);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
onStopped?.();
|
|
||||||
process.stdout.write(`Stopped dev server (${suite}) (pid ${pid}).\n`);
|
|
||||||
return 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
// --- health liveness (port + /__ha_dev_status) ------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Probe the health endpoint. Dev servers bind IPv4 or IPv6 localhost depending
|
|
||||||
* on the OS, so try each; the port is "free" only if every address refuses.
|
|
||||||
* @returns {Promise<{state: "ours" | "foreign" | "free", suite?: string}>}
|
|
||||||
*/
|
|
||||||
const PROBE_HOSTS = ["localhost", "127.0.0.1", "[::1]"];
|
|
||||||
|
|
||||||
const probe = async (port, timeoutMs = 1000) => {
|
|
||||||
let sawResponse = false;
|
|
||||||
/* eslint-disable no-await-in-loop -- probe localhost addresses in order, stopping at the first that answers */
|
|
||||||
for (const host of PROBE_HOSTS) {
|
|
||||||
const controller = new AbortController();
|
|
||||||
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
||||||
try {
|
|
||||||
const res = await fetch(`http://${host}:${port}/__ha_dev_status`, {
|
|
||||||
signal: controller.signal,
|
|
||||||
});
|
|
||||||
sawResponse = true;
|
|
||||||
if (res.ok) {
|
|
||||||
const body = await res.json().catch(() => null);
|
|
||||||
if (body && body.server === "ha-frontend-dev") {
|
|
||||||
return { state: "ours", suite: body.suite };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Try the next address.
|
|
||||||
} finally {
|
|
||||||
clearTimeout(timer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/* eslint-enable no-await-in-loop */
|
|
||||||
return sawResponse ? { state: "foreign" } : { state: "free" };
|
|
||||||
};
|
|
||||||
|
|
||||||
// Find the pid listening on a port via the first available tool (no state file).
|
|
||||||
const pidFromPort = (port) => {
|
|
||||||
const attempts = [
|
|
||||||
[
|
|
||||||
"lsof",
|
|
||||||
["-ti", `tcp:${port}`, "-sTCP:LISTEN"],
|
|
||||||
(out) => out.trim().split("\n")[0],
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"ss",
|
|
||||||
["-ltnpH", `sport = :${port}`],
|
|
||||||
(out) => out.match(/pid=(\d+)/)?.[1],
|
|
||||||
],
|
|
||||||
["fuser", [`${port}/tcp`], (out) => out.trim().split(/\s+/)[0]],
|
|
||||||
];
|
|
||||||
for (const [cmd, cmdArgs, extract] of attempts) {
|
|
||||||
try {
|
|
||||||
const out = execFileSync(cmd, cmdArgs, {
|
|
||||||
encoding: "utf8",
|
|
||||||
stdio: ["ignore", "pipe", "ignore"],
|
|
||||||
});
|
|
||||||
const pid = Number(extract(out));
|
|
||||||
if (Number.isInteger(pid) && pid > 0) {
|
|
||||||
return pid;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Try the next tool.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
const runForegroundHealth = async (suite, cfg) => {
|
|
||||||
const { port } = cfg;
|
|
||||||
const status = await probe(port);
|
|
||||||
if (status.state === "ours" && status.suite === suite) {
|
|
||||||
process.stdout.write(
|
|
||||||
`Dev server (${suite}) is already running at http://localhost:${port}\n`
|
|
||||||
);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
if (status.state === "foreign") {
|
|
||||||
process.stderr.write(
|
|
||||||
`Port ${port} is in use by another process; not the ${suite} dev server.\n`
|
|
||||||
);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return spawnInherit(cfg.spawn.cmd, cfg.spawn.args);
|
|
||||||
};
|
|
||||||
|
|
||||||
const runBackgroundHealth = async (suite, cfg) => {
|
|
||||||
const { port } = cfg;
|
|
||||||
const preflight = await probe(port);
|
|
||||||
if (preflight.state === "ours" && preflight.suite === suite) {
|
|
||||||
const pid = pidFromPort(port);
|
|
||||||
process.stdout.write(
|
|
||||||
`Dev server (${suite}) already running at http://localhost:${port}` +
|
|
||||||
`${pid ? ` (pid ${pid})` : ""}\n${hints(suite)}`
|
|
||||||
);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
if (preflight.state === "foreign") {
|
|
||||||
process.stderr.write(
|
|
||||||
`Port ${port} is in use by another process; not the ${suite} dev server.\n`
|
|
||||||
);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { child, logFile } = spawnDetachedToLog(
|
|
||||||
suite,
|
|
||||||
cfg.spawn.cmd,
|
|
||||||
cfg.spawn.args
|
|
||||||
);
|
|
||||||
return awaitReady({
|
|
||||||
suite,
|
|
||||||
child,
|
|
||||||
logFile,
|
|
||||||
port,
|
|
||||||
isReady: async () => {
|
|
||||||
const status = await probe(port, 1000);
|
|
||||||
return status.state === "ours" && status.suite === suite;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const runStatusHealth = async (suite, cfg) => {
|
|
||||||
const { port } = cfg;
|
|
||||||
const status = await probe(port);
|
|
||||||
if (status.state === "ours" && status.suite === suite) {
|
|
||||||
const pid = pidFromPort(port);
|
|
||||||
process.stdout.write(
|
|
||||||
`Dev server (${suite}) running at http://localhost:${port}` +
|
|
||||||
`${pid ? ` (pid ${pid})` : ""}\n`
|
|
||||||
);
|
|
||||||
} else if (status.state === "ours") {
|
|
||||||
process.stdout.write(
|
|
||||||
`Port ${port} is serving a different Home Assistant frontend dev server (suite ${status.suite ?? "unknown"}); not ${suite}.\n`
|
|
||||||
);
|
|
||||||
} else if (status.state === "foreign") {
|
|
||||||
process.stdout.write(
|
|
||||||
`Port ${port} is in use by another process; not the ${suite} dev server.\n`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
process.stdout.write(`Dev server (${suite}) not running.\n`);
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
const runStopHealth = async (suite, cfg) => {
|
|
||||||
const { port } = cfg;
|
|
||||||
const status = await probe(port);
|
|
||||||
if (!(status.state === "ours" && status.suite === suite)) {
|
|
||||||
// Idempotent: stopping something that is not running is a success.
|
|
||||||
process.stdout.write(`Dev server (${suite}) not running.\n`);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
const pid = pidFromPort(port);
|
|
||||||
if (!pid) {
|
|
||||||
process.stderr.write(
|
|
||||||
`Dev server (${suite}) is running but its pid could not be found ` +
|
|
||||||
`(no lsof/ss/fuser?). Stop it manually.\n`
|
|
||||||
);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return terminate(
|
|
||||||
suite,
|
|
||||||
pid,
|
|
||||||
async () => (await probe(port, 800)).state === "free"
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// --- process liveness (pidfile + log-readiness) -----------------------------
|
|
||||||
|
|
||||||
const isAlive = (pid) => {
|
|
||||||
if (!Number.isInteger(pid) || pid <= 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
process.kill(pid, 0);
|
|
||||||
return true;
|
|
||||||
} catch (err) {
|
|
||||||
// EPERM means the process exists but is owned by someone else.
|
|
||||||
return err.code === "EPERM";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const readPidFile = (suite) => {
|
|
||||||
try {
|
|
||||||
const data = JSON.parse(fs.readFileSync(pidFileFor(suite), "utf8"));
|
|
||||||
if (data && Number.isInteger(data.pid)) {
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Missing or corrupt.
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
const writePidFile = (suite, data) => {
|
|
||||||
fs.mkdirSync(logDir, { recursive: true });
|
|
||||||
fs.writeFileSync(pidFileFor(suite), JSON.stringify(data));
|
|
||||||
};
|
|
||||||
|
|
||||||
const removePidFile = (suite) => {
|
|
||||||
try {
|
|
||||||
fs.rmSync(pidFileFor(suite));
|
|
||||||
} catch {
|
|
||||||
// Already gone.
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const logIsReady = (logFile, readyLog) => {
|
|
||||||
try {
|
|
||||||
return readyLog.test(fs.readFileSync(logFile, "utf8"));
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// app-serve serves on 8124 by default (8123 in a devcontainer), or whatever -p
|
|
||||||
// the caller passed. Used only to show a URL; liveness comes from the pidfile.
|
|
||||||
const resolveServePort = (passthrough) => {
|
|
||||||
const i = passthrough.indexOf("-p");
|
|
||||||
if (i !== -1) {
|
|
||||||
const port = Number(passthrough[i + 1]);
|
|
||||||
if (Number.isInteger(port) && port > 0) {
|
|
||||||
return port;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return process.env.DEVCONTAINER ? 8123 : 8124;
|
|
||||||
};
|
|
||||||
|
|
||||||
const spawnArgs = (cfg, passthrough) => [
|
|
||||||
...cfg.spawn.args,
|
|
||||||
...(cfg.acceptsArgs ? passthrough : []),
|
|
||||||
];
|
|
||||||
|
|
||||||
const runForegroundProcess = async (suite, cfg, passthrough) => {
|
|
||||||
const existing = readPidFile(suite);
|
|
||||||
if (existing && isAlive(existing.pid)) {
|
|
||||||
process.stdout.write(
|
|
||||||
`Dev server (${suite}) already running in the background ` +
|
|
||||||
`(pid ${existing.pid}). Stop it with yarn ${cfg.alias} --stop.\n`
|
|
||||||
);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
if (existing) {
|
|
||||||
removePidFile(suite);
|
|
||||||
}
|
|
||||||
return spawnInherit(cfg.spawn.cmd, spawnArgs(cfg, passthrough));
|
|
||||||
};
|
|
||||||
|
|
||||||
const runBackgroundProcess = async (suite, cfg, passthrough) => {
|
|
||||||
const existing = readPidFile(suite);
|
|
||||||
if (existing && isAlive(existing.pid)) {
|
|
||||||
process.stdout.write(
|
|
||||||
`Dev server (${suite}) already running${urlSuffix(existing.port)} ` +
|
|
||||||
`(pid ${existing.pid})\n${hints(suite)}`
|
|
||||||
);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
if (existing) {
|
|
||||||
removePidFile(suite);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { child, logFile } = spawnDetachedToLog(
|
|
||||||
suite,
|
|
||||||
cfg.spawn.cmd,
|
|
||||||
spawnArgs(cfg, passthrough)
|
|
||||||
);
|
|
||||||
|
|
||||||
const port = cfg.acceptsArgs ? resolveServePort(passthrough) : cfg.port;
|
|
||||||
writePidFile(suite, { pid: child.pid, port });
|
|
||||||
|
|
||||||
return awaitReady({
|
|
||||||
suite,
|
|
||||||
child,
|
|
||||||
logFile,
|
|
||||||
port,
|
|
||||||
isReady: () => logIsReady(logFile, cfg.readyLog),
|
|
||||||
onExit: () => removePidFile(suite),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const runStatusProcess = async (suite) => {
|
|
||||||
const existing = readPidFile(suite);
|
|
||||||
if (existing && isAlive(existing.pid)) {
|
|
||||||
process.stdout.write(
|
|
||||||
`Dev server (${suite}) running${urlSuffix(existing.port)} ` +
|
|
||||||
`(pid ${existing.pid})\n`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
if (existing) {
|
|
||||||
removePidFile(suite);
|
|
||||||
}
|
|
||||||
process.stdout.write(`Dev server (${suite}) not running.\n`);
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
const runStopProcess = async (suite) => {
|
|
||||||
const existing = readPidFile(suite);
|
|
||||||
if (!existing || !isAlive(existing.pid)) {
|
|
||||||
// Idempotent: stopping something that is not running is a success.
|
|
||||||
if (existing) {
|
|
||||||
removePidFile(suite);
|
|
||||||
}
|
|
||||||
process.stdout.write(`Dev server (${suite}) not running.\n`);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
const { pid } = existing;
|
|
||||||
return terminate(
|
|
||||||
suite,
|
|
||||||
pid,
|
|
||||||
() => !isAlive(pid),
|
|
||||||
() => removePidFile(suite)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// --- shared -----------------------------------------------------------------
|
|
||||||
|
|
||||||
const runLogs = (suite, follow) => {
|
|
||||||
const logFile = logFileFor(suite);
|
|
||||||
if (!fs.existsSync(logFile)) {
|
|
||||||
process.stdout.write(
|
|
||||||
`No log for the ${suite} dev server yet (${logFile}).\n`
|
|
||||||
);
|
|
||||||
return Promise.resolve(0);
|
|
||||||
}
|
|
||||||
if (!follow) {
|
|
||||||
process.stdout.write(fs.readFileSync(logFile, "utf8"));
|
|
||||||
return Promise.resolve(0);
|
|
||||||
}
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const tail = spawn("tail", ["-f", logFile], { stdio: "inherit" });
|
|
||||||
tail.on("error", () => {
|
|
||||||
// No tail available; fall back to a one-shot dump.
|
|
||||||
process.stdout.write(fs.readFileSync(logFile, "utf8"));
|
|
||||||
resolve(0);
|
|
||||||
});
|
|
||||||
tail.on("exit", (code) => resolve(code ?? 0));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const main = async () => {
|
|
||||||
const args = parseArgs(process.argv.slice(2));
|
|
||||||
const cfg = SUITES[args.suite];
|
|
||||||
if (!cfg) {
|
|
||||||
usage();
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
if (args.passthrough.length && !cfg.acceptsArgs) {
|
|
||||||
process.stderr.write(
|
|
||||||
`Ignoring unexpected arguments: ${args.passthrough.join(" ")}\n`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// A plain dev:<suite> under a coding agent backgrounds itself; explicit modes
|
|
||||||
// are untouched.
|
|
||||||
let { mode } = args;
|
|
||||||
if (
|
|
||||||
mode === "foreground" &&
|
|
||||||
!["0", "false"].includes(process.env.HA_DEV_BACKGROUND)
|
|
||||||
) {
|
|
||||||
const agent = detectAgent();
|
|
||||||
if (agent) {
|
|
||||||
process.stdout.write(
|
|
||||||
`Detected coding agent (${agent}); starting in the background. ` +
|
|
||||||
`Set HA_DEV_BACKGROUND=0 to force foreground.\n`
|
|
||||||
);
|
|
||||||
mode = "background";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const health = cfg.liveness === "health";
|
|
||||||
switch (mode) {
|
|
||||||
case "background":
|
|
||||||
return health
|
|
||||||
? runBackgroundHealth(args.suite, cfg)
|
|
||||||
: runBackgroundProcess(args.suite, cfg, args.passthrough);
|
|
||||||
case "status":
|
|
||||||
return health
|
|
||||||
? runStatusHealth(args.suite, cfg)
|
|
||||||
: runStatusProcess(args.suite);
|
|
||||||
case "stop":
|
|
||||||
return health
|
|
||||||
? runStopHealth(args.suite, cfg)
|
|
||||||
: runStopProcess(args.suite);
|
|
||||||
case "logs":
|
|
||||||
return runLogs(args.suite, args.follow);
|
|
||||||
default:
|
|
||||||
return health
|
|
||||||
? runForegroundHealth(args.suite, cfg)
|
|
||||||
: runForegroundProcess(args.suite, cfg, args.passthrough);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
main().then(
|
|
||||||
(code) => {
|
|
||||||
process.exitCode = code;
|
|
||||||
},
|
|
||||||
(err) => {
|
|
||||||
process.stderr.write(`${err?.stack || err}\n`);
|
|
||||||
process.exitCode = 1;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
@@ -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);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const paths = require("./paths.js");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
useRollup() {
|
||||||
|
return process.env.ROLLUP === "1";
|
||||||
|
},
|
||||||
|
useWDS() {
|
||||||
|
return process.env.WDS === "1";
|
||||||
|
},
|
||||||
|
isProdBuild() {
|
||||||
|
return (
|
||||||
|
process.env.NODE_ENV === "production" || module.exports.isStatsBuild()
|
||||||
|
);
|
||||||
|
},
|
||||||
|
isStatsBuild() {
|
||||||
|
return process.env.STATS === "1";
|
||||||
|
},
|
||||||
|
isTest() {
|
||||||
|
return process.env.IS_TEST === "true";
|
||||||
|
},
|
||||||
|
isNetlify() {
|
||||||
|
return process.env.NETLIFY === "true";
|
||||||
|
},
|
||||||
|
version() {
|
||||||
|
const version = fs
|
||||||
|
.readFileSync(path.resolve(paths.polymer_dir, "setup.py"), "utf8")
|
||||||
|
.match(/\d{8}\.\d+/);
|
||||||
|
if (!version) {
|
||||||
|
throw Error("Version not found");
|
||||||
|
}
|
||||||
|
return version[0];
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
// @ts-check
|
|
||||||
|
|
||||||
import globals from "globals";
|
|
||||||
import tseslint from "typescript-eslint";
|
|
||||||
import rootConfig from "../eslint.config.mjs";
|
|
||||||
|
|
||||||
export default tseslint.config(...rootConfig, {
|
|
||||||
languageOptions: {
|
|
||||||
globals: globals.node,
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
"no-console": "off",
|
|
||||||
"import-x/no-extraneous-dependencies": "off",
|
|
||||||
"import-x/extensions": "off",
|
|
||||||
"import-x/no-dynamic-require": "off",
|
|
||||||
"global-require": "off",
|
|
||||||
"@typescript-eslint/no-require-imports": "off",
|
|
||||||
"prefer-arrow-callback": "off",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
// Browser-only replacement for core-js/internals/get-built-in-node-module.
|
|
||||||
// The original helper evaluates `Function('return require("...")')()`
|
|
||||||
// when it detects a Node environment, which causes a runtime
|
|
||||||
// ReferenceError on browsers (notably Safari 14) if environment
|
|
||||||
// detection mis-classifies the page. Since browser bundles never need to
|
|
||||||
// access Node built-in modules, return undefined unconditionally.
|
|
||||||
//
|
|
||||||
// Wired up via rspack `NormalModuleReplacementPlugin` in build-scripts/rspack.cjs.
|
|
||||||
module.exports = function () {
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
+31
-36
@@ -1,15 +1,18 @@
|
|||||||
import gulp from "gulp";
|
// Run HA develop mode
|
||||||
import env from "../env.cjs";
|
const gulp = require("gulp");
|
||||||
import "./clean.js";
|
|
||||||
import "./compress.js";
|
const env = require("../env");
|
||||||
import "./entry-html.js";
|
|
||||||
import "./gather-static.js";
|
require("./clean.js");
|
||||||
import "./gen-icons-json.js";
|
require("./translations.js");
|
||||||
import "./licenses.js";
|
require("./gen-icons-json.js");
|
||||||
import "./locale-data.js";
|
require("./gather-static.js");
|
||||||
import "./service-worker.js";
|
require("./compress.js");
|
||||||
import "./translations.js";
|
require("./webpack.js");
|
||||||
import "./rspack.js";
|
require("./service-worker.js");
|
||||||
|
require("./entry-html.js");
|
||||||
|
require("./rollup.js");
|
||||||
|
require("./wds.js");
|
||||||
|
|
||||||
gulp.task(
|
gulp.task(
|
||||||
"develop-app",
|
"develop-app",
|
||||||
@@ -21,12 +24,16 @@ gulp.task(
|
|||||||
gulp.parallel(
|
gulp.parallel(
|
||||||
"gen-service-worker-app-dev",
|
"gen-service-worker-app-dev",
|
||||||
"gen-icons-json",
|
"gen-icons-json",
|
||||||
"gen-pages-app-dev",
|
"gen-pages-dev",
|
||||||
"build-translations",
|
"gen-index-app-dev",
|
||||||
"build-locale-data"
|
"build-translations"
|
||||||
),
|
),
|
||||||
"copy-static-app",
|
"copy-static-app",
|
||||||
"rspack-watch-app"
|
env.useWDS()
|
||||||
|
? "wds-watch-app"
|
||||||
|
: env.useRollup()
|
||||||
|
? "rollup-watch-app"
|
||||||
|
: "webpack-watch-app"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -37,27 +44,15 @@ gulp.task(
|
|||||||
process.env.NODE_ENV = "production";
|
process.env.NODE_ENV = "production";
|
||||||
},
|
},
|
||||||
"clean",
|
"clean",
|
||||||
gulp.parallel(
|
gulp.parallel("gen-icons-json", "build-translations"),
|
||||||
"gen-icons-json",
|
|
||||||
"build-translations",
|
|
||||||
"build-locale-data",
|
|
||||||
"gen-licenses"
|
|
||||||
),
|
|
||||||
"copy-static-app",
|
"copy-static-app",
|
||||||
"rspack-prod-app",
|
env.useRollup() ? "rollup-prod-app" : "webpack-prod-app",
|
||||||
gulp.parallel("gen-pages-app-prod", "gen-service-worker-app-prod"),
|
|
||||||
// Don't compress running tests
|
// Don't compress running tests
|
||||||
...(env.isTestBuild() || env.isStatsBuild() ? [] : ["compress-app"])
|
...(env.isTest() ? [] : ["compress-app"]),
|
||||||
)
|
gulp.parallel(
|
||||||
);
|
"gen-pages-prod",
|
||||||
|
"gen-index-app-prod",
|
||||||
gulp.task(
|
"gen-service-worker-app-prod"
|
||||||
"analyze-app",
|
)
|
||||||
gulp.series(
|
|
||||||
async function setEnv() {
|
|
||||||
process.env.STATS = "1";
|
|
||||||
},
|
|
||||||
"clean",
|
|
||||||
"rspack-prod-app"
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
+17
-13
@@ -1,10 +1,14 @@
|
|||||||
import gulp from "gulp";
|
const gulp = require("gulp");
|
||||||
import "./clean.js";
|
|
||||||
import "./entry-html.js";
|
const env = require("../env");
|
||||||
import "./gather-static.js";
|
|
||||||
import "./service-worker.js";
|
require("./clean.js");
|
||||||
import "./translations.js";
|
require("./translations.js");
|
||||||
import "./rspack.js";
|
require("./gather-static.js");
|
||||||
|
require("./webpack.js");
|
||||||
|
require("./service-worker.js");
|
||||||
|
require("./entry-html.js");
|
||||||
|
require("./rollup.js");
|
||||||
|
|
||||||
gulp.task(
|
gulp.task(
|
||||||
"develop-cast",
|
"develop-cast",
|
||||||
@@ -14,10 +18,10 @@ gulp.task(
|
|||||||
},
|
},
|
||||||
"clean-cast",
|
"clean-cast",
|
||||||
"translations-enable-merge-backend",
|
"translations-enable-merge-backend",
|
||||||
gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"),
|
gulp.parallel("gen-icons-json", "build-translations"),
|
||||||
"copy-static-cast",
|
"copy-static-cast",
|
||||||
"gen-pages-cast-dev",
|
"gen-index-cast-dev",
|
||||||
"rspack-dev-server-cast"
|
env.useRollup() ? "rollup-dev-server-cast" : "webpack-dev-server-cast"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -29,9 +33,9 @@ gulp.task(
|
|||||||
},
|
},
|
||||||
"clean-cast",
|
"clean-cast",
|
||||||
"translations-enable-merge-backend",
|
"translations-enable-merge-backend",
|
||||||
gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"),
|
gulp.parallel("gen-icons-json", "build-translations"),
|
||||||
"copy-static-cast",
|
"copy-static-cast",
|
||||||
"rspack-prod-cast",
|
env.useRollup() ? "rollup-prod-cast" : "webpack-prod-cast",
|
||||||
"gen-pages-cast-prod"
|
"gen-index-cast-prod"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
+16
-34
@@ -1,54 +1,36 @@
|
|||||||
import { deleteSync } from "del";
|
const del = require("del");
|
||||||
import gulp from "gulp";
|
const gulp = require("gulp");
|
||||||
import paths from "../paths.cjs";
|
const paths = require("../paths");
|
||||||
import "./translations.js";
|
require("./translations");
|
||||||
|
|
||||||
gulp.task(
|
gulp.task(
|
||||||
"clean",
|
"clean",
|
||||||
gulp.parallel("clean-translations", async () =>
|
gulp.parallel("clean-translations", () =>
|
||||||
deleteSync([paths.app_output_root, paths.build_dir])
|
del([paths.app_output_root, paths.build_dir])
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
gulp.task(
|
gulp.task(
|
||||||
"clean-demo",
|
"clean-demo",
|
||||||
gulp.parallel("clean-translations", async () =>
|
gulp.parallel("clean-translations", () =>
|
||||||
deleteSync([paths.demo_output_root, paths.build_dir])
|
del([paths.demo_output_root, paths.build_dir])
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
gulp.task(
|
gulp.task(
|
||||||
"clean-cast",
|
"clean-cast",
|
||||||
gulp.parallel("clean-translations", async () =>
|
gulp.parallel("clean-translations", () =>
|
||||||
deleteSync([paths.cast_output_root, paths.build_dir])
|
del([paths.cast_output_root, paths.build_dir])
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
gulp.task("clean-hassio", () =>
|
||||||
|
del([paths.hassio_output_root, paths.build_dir])
|
||||||
|
);
|
||||||
|
|
||||||
gulp.task(
|
gulp.task(
|
||||||
"clean-gallery",
|
"clean-gallery",
|
||||||
gulp.parallel("clean-translations", async () =>
|
gulp.parallel("clean-translations", () =>
|
||||||
deleteSync([
|
del([paths.gallery_output_root, paths.build_dir])
|
||||||
paths.gallery_output_root,
|
|
||||||
paths.gallery_build,
|
|
||||||
paths.build_dir,
|
|
||||||
])
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
gulp.task(
|
|
||||||
"clean-landing-page",
|
|
||||||
gulp.parallel("clean-translations", async () =>
|
|
||||||
deleteSync([
|
|
||||||
paths.landingPage_output_root,
|
|
||||||
paths.landingPage_build,
|
|
||||||
paths.build_dir,
|
|
||||||
])
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
gulp.task(
|
|
||||||
"clean-e2e-test-app",
|
|
||||||
gulp.parallel("clean-translations", async () =>
|
|
||||||
deleteSync([paths.e2eTestApp_output_root, paths.build_dir])
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,59 +1,45 @@
|
|||||||
// Tasks to compress
|
// Tasks to compress
|
||||||
|
|
||||||
import { constants } from "node:zlib";
|
const gulp = require("gulp");
|
||||||
import gulp from "gulp";
|
const zopfli = require("gulp-zopfli-green");
|
||||||
import brotli from "gulp-brotli";
|
const merge = require("merge-stream");
|
||||||
import zopfli from "gulp-zopfli-green";
|
const path = require("path");
|
||||||
import paths from "../paths.cjs";
|
const paths = require("../paths");
|
||||||
|
|
||||||
const filesGlob = "*.{js,json,css,svg,xml}";
|
|
||||||
const brotliOptions = {
|
|
||||||
skipLarger: true,
|
|
||||||
params: {
|
|
||||||
[constants.BROTLI_PARAM_QUALITY]: constants.BROTLI_MAX_QUALITY,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const zopfliOptions = { threshold: 150 };
|
const zopfliOptions = { threshold: 150 };
|
||||||
|
|
||||||
const compressModern = (rootDir, modernDir, compress) =>
|
gulp.task("compress-app", function compressApp() {
|
||||||
gulp
|
const jsLatest = gulp
|
||||||
.src([`${modernDir}/**/${filesGlob}`, `${rootDir}/sw-modern.js`], {
|
.src(path.resolve(paths.app_output_latest, "**/*.js"))
|
||||||
base: rootDir,
|
.pipe(zopfli(zopfliOptions))
|
||||||
allowEmpty: true,
|
.pipe(gulp.dest(paths.app_output_latest));
|
||||||
})
|
|
||||||
.pipe(compress === "zopfli" ? zopfli(zopfliOptions) : brotli(brotliOptions))
|
|
||||||
.pipe(gulp.dest(rootDir));
|
|
||||||
|
|
||||||
const compressOther = (rootDir, modernDir, compress) =>
|
const jsEs5 = gulp
|
||||||
gulp
|
.src(path.resolve(paths.app_output_es5, "**/*.js"))
|
||||||
.src(
|
.pipe(zopfli(zopfliOptions))
|
||||||
[
|
.pipe(gulp.dest(paths.app_output_es5));
|
||||||
`${rootDir}/**/${filesGlob}`,
|
|
||||||
`!${modernDir}/**/${filesGlob}`,
|
|
||||||
`!${rootDir}/{sw-modern,service_worker}.js`,
|
|
||||||
`${rootDir}/{authorize,onboarding}.html`,
|
|
||||||
],
|
|
||||||
{ base: rootDir, allowEmpty: true }
|
|
||||||
)
|
|
||||||
.pipe(compress === "zopfli" ? zopfli(zopfliOptions) : brotli(brotliOptions))
|
|
||||||
.pipe(gulp.dest(rootDir));
|
|
||||||
|
|
||||||
const compressAppModernBrotli = () =>
|
const polyfills = gulp
|
||||||
compressModern(paths.app_output_root, paths.app_output_latest, "brotli");
|
.src(path.resolve(paths.app_output_static, "polyfills/*.js"))
|
||||||
const compressAppModernZopfli = () =>
|
.pipe(zopfli(zopfliOptions))
|
||||||
compressModern(paths.app_output_root, paths.app_output_latest, "zopfli");
|
.pipe(gulp.dest(path.resolve(paths.app_output_static, "polyfills")));
|
||||||
|
|
||||||
const compressAppOtherBrotli = () =>
|
const translations = gulp
|
||||||
compressOther(paths.app_output_root, paths.app_output_latest, "brotli");
|
.src(path.resolve(paths.app_output_static, "translations/**/*.json"))
|
||||||
const compressAppOtherZopfli = () =>
|
.pipe(zopfli(zopfliOptions))
|
||||||
compressOther(paths.app_output_root, paths.app_output_latest, "zopfli");
|
.pipe(gulp.dest(path.resolve(paths.app_output_static, "translations")));
|
||||||
|
|
||||||
gulp.task(
|
const icons = gulp
|
||||||
"compress-app",
|
.src(path.resolve(paths.app_output_static, "mdi/*.json"))
|
||||||
gulp.parallel(
|
.pipe(zopfli(zopfliOptions))
|
||||||
compressAppModernBrotli,
|
.pipe(gulp.dest(path.resolve(paths.app_output_static, "mdi")));
|
||||||
compressAppOtherBrotli,
|
|
||||||
compressAppModernZopfli,
|
return merge(jsLatest, jsEs5, polyfills, translations, icons);
|
||||||
compressAppOtherZopfli
|
});
|
||||||
)
|
|
||||||
);
|
gulp.task("compress-hassio", function compressApp() {
|
||||||
|
return gulp
|
||||||
|
.src(path.resolve(paths.hassio_output_root, "**/*.js"))
|
||||||
|
.pipe(zopfli(zopfliOptions))
|
||||||
|
.pipe(gulp.dest(paths.hassio_output_root));
|
||||||
|
});
|
||||||
|
|||||||
+18
-29
@@ -1,11 +1,16 @@
|
|||||||
import gulp from "gulp";
|
// Run demo develop mode
|
||||||
import "./clean.js";
|
const gulp = require("gulp");
|
||||||
import "./entry-html.js";
|
|
||||||
import "./gather-static.js";
|
const env = require("../env");
|
||||||
import "./gen-icons-json.js";
|
|
||||||
import "./service-worker.js";
|
require("./clean.js");
|
||||||
import "./translations.js";
|
require("./translations.js");
|
||||||
import "./rspack.js";
|
require("./gen-icons-json.js");
|
||||||
|
require("./gather-static.js");
|
||||||
|
require("./webpack.js");
|
||||||
|
require("./service-worker.js");
|
||||||
|
require("./entry-html.js");
|
||||||
|
require("./rollup.js");
|
||||||
|
|
||||||
gulp.task(
|
gulp.task(
|
||||||
"develop-demo",
|
"develop-demo",
|
||||||
@@ -15,14 +20,9 @@ gulp.task(
|
|||||||
},
|
},
|
||||||
"clean-demo",
|
"clean-demo",
|
||||||
"translations-enable-merge-backend",
|
"translations-enable-merge-backend",
|
||||||
gulp.parallel(
|
gulp.parallel("gen-icons-json", "gen-index-demo-dev", "build-translations"),
|
||||||
"gen-icons-json",
|
|
||||||
"gen-pages-demo-dev",
|
|
||||||
"build-translations",
|
|
||||||
"build-locale-data"
|
|
||||||
),
|
|
||||||
"copy-static-demo",
|
"copy-static-demo",
|
||||||
"rspack-dev-server-demo"
|
env.useRollup() ? "rollup-dev-server-demo" : "webpack-dev-server-demo"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -35,20 +35,9 @@ gulp.task(
|
|||||||
"clean-demo",
|
"clean-demo",
|
||||||
// Cast needs to be backwards compatible and older HA has no translations
|
// Cast needs to be backwards compatible and older HA has no translations
|
||||||
"translations-enable-merge-backend",
|
"translations-enable-merge-backend",
|
||||||
gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"),
|
gulp.parallel("gen-icons-json", "build-translations"),
|
||||||
"copy-static-demo",
|
"copy-static-demo",
|
||||||
"rspack-prod-demo",
|
env.useRollup() ? "rollup-prod-demo" : "webpack-prod-demo",
|
||||||
"gen-pages-demo-prod"
|
"gen-index-demo-prod"
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
gulp.task(
|
|
||||||
"analyze-demo",
|
|
||||||
gulp.series(
|
|
||||||
async function setEnv() {
|
|
||||||
process.env.STATS = "1";
|
|
||||||
},
|
|
||||||
"clean",
|
|
||||||
"rspack-prod-demo"
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,224 +0,0 @@
|
|||||||
import fs from "fs/promises";
|
|
||||||
import gulp from "gulp";
|
|
||||||
import path from "path";
|
|
||||||
import mapStream from "map-stream";
|
|
||||||
import transform from "gulp-json-transform";
|
|
||||||
import { LokaliseApi } from "@lokalise/node-api";
|
|
||||||
import JSZip from "jszip";
|
|
||||||
|
|
||||||
const inDir = "translations";
|
|
||||||
const inDirFrontend = `${inDir}/frontend`;
|
|
||||||
const inDirBackend = `${inDir}/backend`;
|
|
||||||
const srcMeta = "src/translations/translationMetadata.json";
|
|
||||||
const encoding = "utf8";
|
|
||||||
|
|
||||||
function hasHtml(data) {
|
|
||||||
return /<\S*>/i.test(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
function recursiveCheckHasHtml(file, data, errors, recKey) {
|
|
||||||
Object.keys(data).forEach(function (key) {
|
|
||||||
if (typeof data[key] === "object") {
|
|
||||||
const nextRecKey = recKey ? `${recKey}.${key}` : key;
|
|
||||||
recursiveCheckHasHtml(file, data[key], errors, nextRecKey);
|
|
||||||
} else if (hasHtml(data[key])) {
|
|
||||||
errors.push(`HTML found in ${file.path} at key ${recKey}.${key}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkHtml() {
|
|
||||||
const errors = [];
|
|
||||||
|
|
||||||
return mapStream(function (file, cb) {
|
|
||||||
const content = file.contents;
|
|
||||||
let error;
|
|
||||||
if (content) {
|
|
||||||
if (hasHtml(String(content))) {
|
|
||||||
const data = JSON.parse(String(content));
|
|
||||||
recursiveCheckHasHtml(file, data, errors);
|
|
||||||
if (errors.length > 0) {
|
|
||||||
error = errors.join("\r\n");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cb(error, file);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function convertBackendTranslations(data, _file) {
|
|
||||||
const output = { component: {} };
|
|
||||||
if (!data.component) {
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
Object.keys(data.component).forEach((domain) => {
|
|
||||||
if (!("entity_component" in data.component[domain])) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
output.component[domain] = { entity_component: {} };
|
|
||||||
Object.keys(data.component[domain].entity_component).forEach((key) => {
|
|
||||||
output.component[domain].entity_component[key] =
|
|
||||||
data.component[domain].entity_component[key];
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
|
|
||||||
gulp.task("convert-backend-translations", function () {
|
|
||||||
return gulp
|
|
||||||
.src([`${inDirBackend}/*.json`])
|
|
||||||
.pipe(transform((data, file) => convertBackendTranslations(data, file)))
|
|
||||||
.pipe(gulp.dest(inDirBackend));
|
|
||||||
});
|
|
||||||
|
|
||||||
gulp.task("check-translations-html", function () {
|
|
||||||
return gulp
|
|
||||||
.src([`${inDirFrontend}/*.json`, `${inDirBackend}/*.json`])
|
|
||||||
.pipe(checkHtml());
|
|
||||||
});
|
|
||||||
|
|
||||||
gulp.task("check-all-files-exist", async function () {
|
|
||||||
const file = await fs.readFile(srcMeta, { encoding });
|
|
||||||
const meta = JSON.parse(file);
|
|
||||||
const writings = [];
|
|
||||||
Object.keys(meta).forEach((lang) => {
|
|
||||||
writings.push(
|
|
||||||
fs.writeFile(`${inDirFrontend}/${lang}.json`, JSON.stringify({}), {
|
|
||||||
flag: "wx",
|
|
||||||
}),
|
|
||||||
fs.writeFile(`${inDirBackend}/${lang}.json`, JSON.stringify({}), {
|
|
||||||
flag: "wx",
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
await Promise.allSettled(writings);
|
|
||||||
});
|
|
||||||
|
|
||||||
const lokaliseProjects = {
|
|
||||||
backend: "130246255a974bd3b5e8a1.51616605",
|
|
||||||
frontend: "3420425759f6d6d241f598.13594006",
|
|
||||||
};
|
|
||||||
|
|
||||||
const POLL_INTERVAL_MS = 1000;
|
|
||||||
|
|
||||||
/* eslint-disable no-await-in-loop */
|
|
||||||
async function pollProcess(lokaliseApi, projectId, processId) {
|
|
||||||
while (true) {
|
|
||||||
const process = await lokaliseApi
|
|
||||||
.queuedProcesses()
|
|
||||||
.get(processId, { project_id: projectId });
|
|
||||||
|
|
||||||
const project =
|
|
||||||
projectId === lokaliseProjects.backend ? "backend" : "frontend";
|
|
||||||
|
|
||||||
if (process.status === "finished") {
|
|
||||||
console.log(`Lokalise export process for ${project} finished`);
|
|
||||||
return process;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.status === "failed" || process.status === "cancelled") {
|
|
||||||
throw new Error(
|
|
||||||
`Lokalise export process for ${project} ${process.status}: ${process.message}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`Lokalise export process for ${project} in progress...`,
|
|
||||||
process.status,
|
|
||||||
process.details?.items_to_process
|
|
||||||
? `${Math.round(((process.details.items_processed || 0) / process.details.items_to_process) * 100)}% (${process.details.items_processed}/${process.details.items_to_process})`
|
|
||||||
: ""
|
|
||||||
);
|
|
||||||
|
|
||||||
await new Promise((resolve) => {
|
|
||||||
setTimeout(resolve, POLL_INTERVAL_MS);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/* eslint-enable no-await-in-loop */
|
|
||||||
|
|
||||||
gulp.task("fetch-lokalise", async function () {
|
|
||||||
let apiKey;
|
|
||||||
try {
|
|
||||||
apiKey =
|
|
||||||
process.env.LOKALISE_TOKEN ||
|
|
||||||
(await fs.readFile(".lokalise_token", { encoding }));
|
|
||||||
} catch {
|
|
||||||
throw new Error(
|
|
||||||
"An Administrator Lokalise API token is required to download the latest set of translations. Place your token in a new file `.lokalise_token` in the repo root directory."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const lokaliseApi = new LokaliseApi({ apiKey });
|
|
||||||
|
|
||||||
const mkdirPromise = Promise.all([
|
|
||||||
fs.mkdir(inDirFrontend, { recursive: true }),
|
|
||||||
fs.mkdir(inDirBackend, { recursive: true }),
|
|
||||||
]);
|
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
Object.entries(lokaliseProjects).map(async ([project, projectId]) => {
|
|
||||||
try {
|
|
||||||
const exportProcess = await lokaliseApi
|
|
||||||
.files()
|
|
||||||
.async_download(projectId, {
|
|
||||||
format: "json",
|
|
||||||
original_filenames: false,
|
|
||||||
replace_breaks: false,
|
|
||||||
json_unescaped_slashes: true,
|
|
||||||
export_empty_as: "skip",
|
|
||||||
filter_data: ["verified"],
|
|
||||||
});
|
|
||||||
|
|
||||||
const finishedProcess = await pollProcess(
|
|
||||||
lokaliseApi,
|
|
||||||
projectId,
|
|
||||||
exportProcess.process_id
|
|
||||||
);
|
|
||||||
|
|
||||||
const bundleUrl = finishedProcess.details.download_url;
|
|
||||||
|
|
||||||
console.log(`Downloading translations from: ${bundleUrl}`);
|
|
||||||
|
|
||||||
const response = await fetch(bundleUrl);
|
|
||||||
|
|
||||||
if (response.status !== 200 && response.status !== 0) {
|
|
||||||
throw new Error(response.statusText);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Extracting translations...`);
|
|
||||||
|
|
||||||
const contents = await JSZip.loadAsync(await response.arrayBuffer());
|
|
||||||
|
|
||||||
await mkdirPromise;
|
|
||||||
await Promise.all(
|
|
||||||
Object.keys(contents.files).map(async (filename) => {
|
|
||||||
const file = contents.file(filename);
|
|
||||||
if (!file) {
|
|
||||||
// no file, probably a directory
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const content = await file.async("nodebuffer");
|
|
||||||
await fs.writeFile(
|
|
||||||
path.join(inDir, project, filename.split("/").splice(-1)[0]),
|
|
||||||
content,
|
|
||||||
{ flag: "w", encoding }
|
|
||||||
);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
gulp.task(
|
|
||||||
"download-translations",
|
|
||||||
gulp.series(
|
|
||||||
"fetch-lokalise",
|
|
||||||
"convert-backend-translations",
|
|
||||||
"check-translations-html",
|
|
||||||
"check-all-files-exist"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
const del = require("del");
|
||||||
|
const gulp = require("gulp");
|
||||||
|
const fs = require("fs");
|
||||||
|
const mapStream = require("map-stream");
|
||||||
|
|
||||||
|
const inDirFrontend = "translations/frontend";
|
||||||
|
const inDirBackend = "translations/backend";
|
||||||
|
const downloadDir = "translations/downloads";
|
||||||
|
const srcMeta = "src/translations/translationMetadata.json";
|
||||||
|
|
||||||
|
const encoding = "utf8";
|
||||||
|
|
||||||
|
const tasks = [];
|
||||||
|
|
||||||
|
function hasHtml(data) {
|
||||||
|
return /<[a-z][\s\S]*>/i.test(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
function recursiveCheckHasHtml(file, data, errors, recKey) {
|
||||||
|
Object.keys(data).forEach(function (key) {
|
||||||
|
if (typeof data[key] === "object") {
|
||||||
|
const nextRecKey = recKey ? `${recKey}.${key}` : key;
|
||||||
|
recursiveCheckHasHtml(file, data[key], errors, nextRecKey);
|
||||||
|
} else if (hasHtml(data[key])) {
|
||||||
|
errors.push(`HTML found in ${file.path} at key ${recKey}.${key}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkHtml() {
|
||||||
|
const errors = [];
|
||||||
|
|
||||||
|
return mapStream(function (file, cb) {
|
||||||
|
const content = file.contents;
|
||||||
|
let error;
|
||||||
|
if (content) {
|
||||||
|
if (hasHtml(String(content))) {
|
||||||
|
const data = JSON.parse(String(content));
|
||||||
|
recursiveCheckHasHtml(file, data, errors);
|
||||||
|
if (errors.length > 0) {
|
||||||
|
error = errors.join("\r\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cb(error, file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let taskName = "clean-downloaded-translations";
|
||||||
|
gulp.task(taskName, function () {
|
||||||
|
return del([`${downloadDir}/**`]);
|
||||||
|
});
|
||||||
|
tasks.push(taskName);
|
||||||
|
|
||||||
|
taskName = "check-translations-html";
|
||||||
|
gulp.task(taskName, function () {
|
||||||
|
return gulp.src(`${downloadDir}/*.json`).pipe(checkHtml());
|
||||||
|
});
|
||||||
|
tasks.push(taskName);
|
||||||
|
|
||||||
|
taskName = "check-all-files-exist";
|
||||||
|
gulp.task(taskName, function () {
|
||||||
|
const file = fs.readFileSync(srcMeta, { encoding });
|
||||||
|
const meta = JSON.parse(file);
|
||||||
|
Object.keys(meta).forEach((lang) => {
|
||||||
|
if (!fs.existsSync(`${inDirFrontend}/${lang}.json`)) {
|
||||||
|
fs.writeFileSync(`${inDirFrontend}/${lang}.json`, JSON.stringify({}));
|
||||||
|
}
|
||||||
|
if (!fs.existsSync(`${inDirBackend}/${lang}.json`)) {
|
||||||
|
fs.writeFileSync(`${inDirBackend}/${lang}.json`, JSON.stringify({}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return Promise.resolve();
|
||||||
|
});
|
||||||
|
tasks.push(taskName);
|
||||||
|
|
||||||
|
taskName = "move-downloaded-translations";
|
||||||
|
gulp.task(taskName, function () {
|
||||||
|
return gulp.src(`${downloadDir}/*.json`).pipe(gulp.dest(inDirFrontend));
|
||||||
|
});
|
||||||
|
tasks.push(taskName);
|
||||||
|
|
||||||
|
taskName = "check-downloaded-translations";
|
||||||
|
gulp.task(
|
||||||
|
taskName,
|
||||||
|
gulp.series(
|
||||||
|
"check-translations-html",
|
||||||
|
"move-downloaded-translations",
|
||||||
|
"check-all-files-exist",
|
||||||
|
"clean-downloaded-translations"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
tasks.push(taskName);
|
||||||
|
|
||||||
|
module.exports = tasks;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user