chore: use vite+ (#32297)

Co-authored-by: Michael Geers <michael@geers.tv>
This commit is contained in:
Maschga 2026-07-31 11:43:57 +02:00 • committed by GitHub
parent 828f4ab2f2
commit d07a0741ec
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
55 changed files with 2176 additions and 2932 deletions

View file

@ -18,10 +18,10 @@ runs:
go-version-file: go.mod go-version-file: go.mod
cache: false # managed explicitly below to keep the scoped namespaces cache: false # managed explicitly below to keep the scoped namespaces
- uses: actions/setup-node@v6 - uses: voidzero-dev/setup-vp@v1
with: with:
node-version: "26" node-version: "26"
cache: "npm" cache: true
# Writes are namespaced by cache-scope so a PR build cannot poison the # Writes are namespaced by cache-scope so a PR build cannot poison the
# shared "main" cache; PRs still read it through the fallback restore-key. # shared "main" cache; PRs still read it through the fallback restore-key.

View file

@ -26,11 +26,16 @@ jobs:
with: with:
go-version-file: go.mod go-version-file: go.mod
- uses: voidzero-dev/setup-vp@v1
with:
node-version: "26"
cache: true
- name: Generate CLI docs - name: Generate CLI docs
run: mkdir dist && touch dist/empty && go run main.go gendoc ./cli-docs run: mkdir dist && touch dist/empty && go run main.go gendoc ./cli-docs
- name: Format - name: Format
run: npx prettier@3.9.4 --no-config --write "./cli-docs/**/*.md" run: vp fmt "./cli-docs/**/*.md"
- name: Deploy CLI docs to docs repo - name: Deploy CLI docs to docs repo
uses: peaceiris/actions-gh-pages@84c30a85c19949d7eee79c4ff27748b70285e453 # v4 uses: peaceiris/actions-gh-pages@84c30a85c19949d7eee79c4ff27748b70285e453 # v4

View file

@ -76,10 +76,10 @@ jobs:
${{ runner.os }}-go-build- ${{ runner.os }}-go-build-
${{ runner.os }}-go- ${{ runner.os }}-go-
- uses: actions/setup-node@v6 - uses: voidzero-dev/setup-vp@v1
with: with:
node-version: "26" node-version: "26"
cache: "npm" cache: true
- run: mkdir dist && touch dist/empty - run: mkdir dist && touch dist/empty
@ -168,10 +168,10 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
- uses: actions/setup-node@v6 - uses: voidzero-dev/setup-vp@v1
with: with:
node-version: "26" node-version: "26"
cache: "npm" cache: true
- name: Install - name: Install
run: make install-ui run: make install-ui
@ -230,10 +230,10 @@ jobs:
${{ runner.os }}-go-integration- ${{ runner.os }}-go-integration-
${{ runner.os }}-go- ${{ runner.os }}-go-
- uses: actions/setup-node@v6 - uses: voidzero-dev/setup-vp@v1
with: with:
node-version: "26" node-version: "26"
cache: "npm" cache: true
- name: Build UI - name: Build UI
run: make install-ui ui run: make install-ui ui
@ -254,11 +254,11 @@ jobs:
- name: Install Playwright (browsers + deps) - name: Install Playwright (browsers + deps)
if: steps.playwright-cache.outputs.cache-hit != 'true' if: steps.playwright-cache.outputs.cache-hit != 'true'
run: npx playwright install --with-deps chromium run: vpx playwright install --with-deps chromium
timeout-minutes: 5 timeout-minutes: 5
- name: Run tests - name: Run tests
run: npx playwright test run: vpx playwright test
timeout-minutes: 20 timeout-minutes: 20
env: env:
TZ: Europe/Berlin TZ: Europe/Berlin

View file

@ -1 +0,0 @@
tests/custom-css.css

View file

@ -5,5 +5,6 @@
"golang.go", "golang.go",
"esbenp.prettier-vscode", "esbenp.prettier-vscode",
"yoavbls.pretty-ts-errors", "yoavbls.pretty-ts-errors",
"VoidZero.vite-plus-extension-pack"
] ]
} }

View file

@ -14,13 +14,13 @@ This file provides guidance to AI coding agents when working with code in this r
- `make build` - build Go binary only - `make build` - build Go binary only
- `make ui` - build UI assets only - `make ui` - build UI assets only
- `make install` - install Go tools and dependencies - `make install` - install Go tools and dependencies
- `make install-ui` - install Node.js dependencies (`npm ci`) - `make install-ui` - install Node.js dependencies (`vp install`)
- `make test` - run Go tests - `make test` - run Go tests
- `make test-ui` - run frontend tests - `make test-ui` - run frontend tests
- `make lint` - run Go linting (golangci-lint) - `make lint` - run Go linting (golangci-lint)
- `make lint-ui` - run frontend linting - `make lint-ui` - run frontend linting
- `npm run dev` - start Vue dev server (http://127.0.0.1:7071) - `vp run dev` - start Vue dev server (http://127.0.0.1:7071)
- `npm run playwright` - run integration tests - `vp run playwright` - run integration tests
- `evcc --template-type [type] --template [file]` - test device templates - `evcc --template-type [type] --template [file]` - test device templates
- `make docs` - generate template documentation - `make docs` - generate template documentation
@ -28,15 +28,15 @@ This file provides guidance to AI coding agents when working with code in this r
Deep documentation on specific subsystems is available in `docs/agents/`. Load what you need based on the task: Deep documentation on specific subsystems is available in `docs/agents/`. Load what you need based on the task:
| File | When to load | | File | When to load |
|------|-------------| | ------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| [Core Domain](docs/agents/core-domain.md) | Control loop, loadpoint logic, PV surplus, charge modes, tariffs, interfaces | | [Core Domain](docs/agents/core-domain.md) | Control loop, loadpoint logic, PV surplus, charge modes, tariffs, interfaces |
| [Hardware Integrations](docs/agents/hardware-integrations.md) | Charger/meter/vehicle implementations, adding new devices | | [Hardware Integrations](docs/agents/hardware-integrations.md) | Charger/meter/vehicle implementations, adding new devices |
| [Easee Architecture](docs/agents/easee-architecture.md) | Easee charger (REST+SignalR, async correlation, concurrency) | | [Easee Architecture](docs/agents/easee-architecture.md) | Easee charger (REST+SignalR, async correlation, concurrency) |
| [OCPP Forwarder](docs/agents/ocpp-forwarder.md) | OCPP proxy/forwarder (sidecar relay to upstream OCPP server, read-only mode) | | [OCPP Forwarder](docs/agents/ocpp-forwarder.md) | OCPP proxy/forwarder (sidecar relay to upstream OCPP server, read-only mode) |
| [Plugin System](docs/agents/plugin-system.md) | Plugin layer (HTTP, MQTT, Modbus, SunSpec, JS) | | [Plugin System](docs/agents/plugin-system.md) | Plugin layer (HTTP, MQTT, Modbus, SunSpec, JS) |
| [Web UI & API](docs/agents/web-ui-api.md) | REST API, WebSocket, Vue frontend, authentication | | [Web UI & API](docs/agents/web-ui-api.md) | REST API, WebSocket, Vue frontend, authentication |
| [API Security](docs/agents/api-security.md) | Auth modes, JWT/API key/session, two-tier checks, credential storage | | [API Security](docs/agents/api-security.md) | Auth modes, JWT/API key/session, two-tier checks, credential storage |
### Loading guide by task type ### Loading guide by task type
@ -98,7 +98,7 @@ Deep documentation on specific subsystems is available in `docs/agents/`. Load w
## Comment Style ## Comment Style
- Prefer self-documenting code over comments; comment the *why*, not the *what* - Prefer self-documenting code over comments; comment the _why_, not the _what_
- Default to no comment. Only add one for a non-obvious constraint, invariant, workaround, or surprising behavior. Keep it to one line, two if necessary - Default to no comment. Only add one for a non-obvious constraint, invariant, workaround, or surprising behavior. Keep it to one line, two if necessary
- Skip refs to the current task, PR, issue, or caller ("added for X flow", "see #1234"). Git history covers that - Skip refs to the current task, PR, issue, or caller ("added for X flow", "see #1234"). Git history covers that
- Exception: Go exported identifiers follow godoc convention. Short `// FuncName does X` summary starting with the identifier name - Exception: Go exported identifiers follow godoc convention. Short `// FuncName does X` summary starting with the identifier name
@ -122,7 +122,7 @@ Deep documentation on specific subsystems is available in `docs/agents/`. Load w
- `_enumer.go` - generated enum code - `_enumer.go` - generated enum code
- `*_decorators.go` - generated decorator pattern implementations - `*_decorators.go` - generated decorator pattern implementations
- Validate interface implementations: `var _ Interface = (*Type)(nil)` - Validate interface implementations: `var _ Interface = (*Type)(nil)`
- Capabilities: register via `implement.Has`/`May` only when a capability is *conditional* (runtime/config detection, e.g. `if cp.PhaseSwitching { implement.Has(...) }`). For capabilities present on every code path, declare a plain exported method plus `var _ api.Interface = (*Type)(nil)` instead. `api.Cap` resolves static methods via direct type assertion, so unconditional `implement.Has` is redundant. A type with no conditional capabilities needs neither the `implement.Caps` embed nor `implement.New()` - Capabilities: register via `implement.Has`/`May` only when a capability is _conditional_ (runtime/config detection, e.g. `if cp.PhaseSwitching { implement.Has(...) }`). For capabilities present on every code path, declare a plain exported method plus `var _ api.Interface = (*Type)(nil)` instead. `api.Cap` resolves static methods via direct type assertion, so unconditional `implement.Has` is redundant. A type with no conditional capabilities needs neither the `implement.Caps` embed nor `implement.New()`
### Error Handling ### Error Handling
@ -236,9 +236,9 @@ Deep documentation on specific subsystems is available in `docs/agents/`. Load w
### Essential Commands ### Essential Commands
- Must build before testing executing playwright `make ui build` since it uses the binary. For manual testing assets are build and reloaded automatically (vite dev). - Must build before testing executing playwright `make ui build` since it uses the binary. For manual testing assets are build and reloaded automatically (vite dev).
- Run tests: `npm run playwright` or `npx playwright test` - Run tests: `vp run playwright` or `vpx playwright test`
- Debug: `npx playwright test --debug` - Debug: `vpx playwright test --debug`
- Specific test: `npx playwright test tests/config-loadpoint.spec.ts` - Specific test: `vpx playwright test tests/config-loadpoint.spec.ts`
### Selector Strategy ### Selector Strategy
@ -270,7 +270,7 @@ Deep documentation on specific subsystems is available in `docs/agents/`. Load w
- Device types: chargers, meters, vehicles, tariffs - Device types: chargers, meters, vehicles, tariffs
- Plugin protocols: Modbus, HTTP, MQTT, JavaScript, Go - Plugin protocols: Modbus, HTTP, MQTT, JavaScript, Go
- Define device capabilities and configuration in templates at `templates/definition/[type]/` - Define device capabilities and configuration in templates at `templates/definition/[type]/`
- Don't restate param properties that `util/templates/defaults.yaml` already defines for that param name. Properties (description, help, type, unit, default, example, required, advanced, mask, private, usages, …) are inherited from defaults; only specify a property in a template to give it a *different* value. Restating the same value is redundant duplication: reference the param by `name` alone. - Don't restate param properties that `util/templates/defaults.yaml` already defines for that param name. Properties (description, help, type, unit, default, example, required, advanced, mask, private, usages, …) are inherited from defaults; only specify a property in a template to give it a _different_ value. Restating the same value is redundant duplication: reference the param by `name` alone.
- Test templates: `evcc --template-type [type] --template [file]` - Test templates: `evcc --template-type [type] --template [file]`
- Update docs after template changes: `make docs` - Update docs after template changes: `make docs`
- When implementing or debugging against a third-party device library (eebus-go/ship/spine-go, ocpp-go, modbus/SunSpec), consult the library's current upstream documentation before coding rather than relying on recalled API details - When implementing or debugging against a third-party device library (eebus-go/ship/spine-go, ocpp-go, modbus/SunSpec), consult the library's current upstream documentation before coding rather than relying on recalled API details

View file

@ -4,7 +4,7 @@
### Development environment ### Development environment
Developing evcc requires [Go][1] and [Node][2]. We recommend VSCode with the [Go](https://marketplace.visualstudio.com/items?itemName=golang.Go), [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) and [Vetur](https://marketplace.visualstudio.com/items?itemName=octref.vetur) extensions. Developing evcc requires [Go][1] [Node][2] and [Vite+][3]. We recommend VSCode with the [Go](https://marketplace.visualstudio.com/items?itemName=golang.Go), [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) and [Vetur](https://marketplace.visualstudio.com/items?itemName=octref.vetur) extensions.
Alternatively, if you use VS Code and [devcontainers](https://code.visualstudio.com/docs/devcontainers/containers), you can use the "Dev containers: Clone repository in container volume" action. This will create a devcontainer with the required toolchain and install the prerequisites as explained below. Wait until the startup log says "Done. Press any key to close the terminal." and check for any errors. Alternatively, if you use VS Code and [devcontainers](https://code.visualstudio.com/docs/devcontainers/containers), you can use the "Dev containers: Clone repository in container volume" action. This will create a devcontainer with the required toolchain and install the prerequisites as explained below. Wait until the startup log says "Done. Press any key to close the terminal." and check for any errors.
@ -98,8 +98,8 @@ You can adjust the referred configuration as needed to e.g. use your live config
For frontend development start the Vue toolchain in dev-mode. Open http://127.0.0.1:7071/ to get to the live reloading development server. It pulls its data from port 7070 (see above). For frontend development start the Vue toolchain in dev-mode. Open http://127.0.0.1:7071/ to get to the live reloading development server. It pulls its data from port 7070 (see above).
```sh ```sh
npm install vp install
npm run dev vp run dev
``` ```
### Storybook ### Storybook
@ -107,7 +107,7 @@ npm run dev
We're using storybook to develop and visualize UI components in different states. Running the command below will open your browser at http://127.0.0.1:6006/. We're using storybook to develop and visualize UI components in different states. Running the command below will open your browser at http://127.0.0.1:6006/.
```sh ```sh
npm run storybook vp run storybook
``` ```
### Integration testing ### Integration testing
@ -116,7 +116,7 @@ We use Playwright for end-to-end integration tests. They start a local evcc inst
```sh ```sh
make ui build make ui build
npm run playwright vp run playwright
``` ```
### Simulating device state ### Simulating device state
@ -124,7 +124,7 @@ npm run playwright
Since we don't want to run tests against real devices or cloud services, we've build a simple simulator that lets you emulated meters, vehicles and loadpoints. The simulators web interface runs on http://localhost:7072. Since we don't want to run tests against real devices or cloud services, we've build a simple simulator that lets you emulated meters, vehicles and loadpoints. The simulators web interface runs on http://localhost:7072.
``` ```
npm run simulator vp run simulator
``` ```
Run an evcc instance that uses simulator data. This configuration runs with a very high refresh interval to speed up testing. Run an evcc instance that uses simulator data. This configuration runs with a very high refresh interval to speed up testing.
@ -185,6 +185,7 @@ Note: To ensure the build succeeds after creating new translations, make sure to
[1]: https://go.dev [1]: https://go.dev
[2]: https://nodejs.org/ [2]: https://nodejs.org/
[3]: https://viteplus.dev/guide/#install-vp
## Documentation, Website and iOS/Android App ## Documentation, Website and iOS/Android App

View file

@ -1,7 +1,7 @@
# STEP 1 build ui # STEP 1 build ui
FROM --platform=$BUILDPLATFORM node:26-alpine AS node FROM --platform=$BUILDPLATFORM node:26-alpine AS node
RUN apk update && apk add --no-cache make RUN apk update && apk add --no-cache make && curl -fsSL https://vite.plus | bash
WORKDIR /build WORKDIR /build

View file

@ -40,10 +40,10 @@ install::
go install tool go install tool
install-ui:: install-ui::
npm ci vp install
ui:: ui::
npm run build vp run build
assets:: assets::
go generate ./... go generate ./...
@ -59,7 +59,7 @@ modernize:
go tool modernize -test -fix -stringsbuilder=false -omitzero=false ./... go tool modernize -test -fix -stringsbuilder=false -omitzero=false ./...
lint-ui:: lint-ui::
npm run lint vp run lint
license:: license::
go run github.com/google/go-licenses/v2@latest check \ go run github.com/google/go-licenses/v2@latest check \
@ -74,10 +74,10 @@ license::
./... ./...
license-ui:: license-ui::
npm run license vp run license
test-ui:: test-ui::
npm test vp run test
test:: test::
@echo "Running testsuite" @echo "Running testsuite"

View file

@ -1,4 +1,4 @@
import { describe, it, expect } from "vitest"; import { describe, it, expect } from "vite-plus/test";
import colors, { import colors, {
dimColor, dimColor,
lighterColor, lighterColor,

View file

@ -313,7 +313,7 @@ export default defineComponent({
}, },
batteryDetails() { batteryDetails() {
if (!this.batteryDevices.length) { if (!this.batteryDevices.length) {
return; return "";
} }
const multipleBatteries = this.batteryDevices.length > 1; const multipleBatteries = this.batteryDevices.length > 1;
return this.batteryDevices return this.batteryDevices

View file

@ -1,4 +1,4 @@
import { describe, it, expect } from "vitest"; import { describe, it, expect } from "vite-plus/test";
import { historyToSeries, forecastToSeries, buildChartBatteries } from "./history"; import { historyToSeries, forecastToSeries, buildChartBatteries } from "./history";
import type { BatteryMeter, EvOpt } from "@/types/evcc"; import type { BatteryMeter, EvOpt } from "@/types/evcc";
import type { BatteryHistorySeries } from "./types"; import type { BatteryHistorySeries } from "./types";

View file

@ -1,5 +1,5 @@
import { mount, config } from "@vue/test-utils"; import { mount, config } from "@vue/test-utils";
import { beforeAll, describe, expect, test } from "vitest"; import { beforeAll, describe, expect, test } from "vite-plus/test";
import Preview from "./Preview.vue"; import Preview from "./Preview.vue";
import type { Slot } from "@/types/evcc"; import type { Slot } from "@/types/evcc";

View file

@ -126,7 +126,7 @@ export default defineComponent({
return null; return null;
}, },
targetOffset(): number | undefined { targetOffset(): number | undefined {
if (!this.targetTime) return; if (!this.targetTime) return undefined;
const start = new Date(this.startTime); const start = new Date(this.startTime);
start.setMinutes(start.getMinutes() - (start.getMinutes() % 15)); start.setMinutes(start.getMinutes() - (start.getMinutes() % 15));
start.setSeconds(0); start.setSeconds(0);

View file

@ -1,4 +1,4 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vite-plus/test";
import { createServiceEndpoints, type TemplateParam } from "./index"; import { createServiceEndpoints, type TemplateParam } from "./index";
const buildParam = (name: string, service?: string): TemplateParam => ({ const buildParam = (name: string, service?: string): TemplateParam => ({

View file

@ -186,7 +186,7 @@
import type { PropType } from "vue"; import type { PropType } from "vue";
import QRCode from "qrcode"; import QRCode from "qrcode";
import "@h2d2/shopicons/es/regular/trash"; import "@h2d2/shopicons/es/regular/trash";
// eslint-disable-next-line @typescript-eslint/no-unused-vars // oxlint-disable-next-line @typescript-eslint/no-unused-vars
import type { EebusConfig, EebusPairing, EebusStatus, YamlSource } from "@/types/evcc"; import type { EebusConfig, EebusPairing, EebusStatus, YamlSource } from "@/types/evcc";
import api from "@/api"; import api from "@/api";
import JsonModal from "./JsonModal.vue"; import JsonModal from "./JsonModal.vue";

View file

@ -58,7 +58,7 @@ export default defineComponent({
}, },
{ {
label: "primary", label: "primary",
options: [...products.filter((p: Product) => p.group !== "generic")], options: products.filter((p: Product) => p.group !== "generic"),
}, },
]; ];
}, },

View file

@ -25,7 +25,7 @@
</template> </template>
<script> <script>
/* eslint-disable vue/prop-name-casing */ /* oxlint-disable vue/prop-name-casing */
import FormRow from "./FormRow.vue"; import FormRow from "./FormRow.vue";
import PropertyField from "./PropertyField.vue"; import PropertyField from "./PropertyField.vue";

View file

@ -513,7 +513,7 @@ export default defineComponent({
}, },
batteryGridChargeLimitFmt() { batteryGridChargeLimitFmt() {
if (!this.batteryGridChargeLimitSet) { if (!this.batteryGridChargeLimitSet) {
return; return undefined;
} }
if (this.smartCostCo2) { if (this.smartCostCo2) {
return this.fmtCo2Short(this.batteryGridChargeLimit); return this.fmtCo2Short(this.batteryGridChargeLimit);

View file

@ -246,7 +246,7 @@ export default defineComponent({
return Math.round(Math.min(maxWidth, Math.max(minWidth, scale * maxWidth))); return Math.round(Math.min(maxWidth, Math.max(minWidth, scale * maxWidth)));
}, },
options() { options() {
// eslint-disable-next-line @typescript-eslint/no-this-alias // oxlint-disable-next-line @typescript-eslint/no-this-alias
const vThis = this; const vThis = this;
return { return {
...commonOptions, ...commonOptions,

View file

@ -89,7 +89,7 @@ export default defineComponent({
const priceColor = colors.price || ""; const priceColor = colors.price || "";
const exportColor = colors.export || ""; const exportColor = colors.export || "";
// eslint-disable-next-line @typescript-eslint/no-this-alias // oxlint-disable-next-line @typescript-eslint/no-this-alias
const vThis = this; const vThis = this;
return { return {
animationDuration: 0, animationDuration: 0,

View file

@ -77,7 +77,7 @@ export default defineComponent({
chartOption(): Record<string, unknown> { chartOption(): Record<string, unknown> {
const color = this.color; const color = this.color;
// eslint-disable-next-line @typescript-eslint/no-this-alias // oxlint-disable-next-line @typescript-eslint/no-this-alias
const vThis = this; const vThis = this;
return { return {
animationDuration: 0, animationDuration: 0,

View file

@ -220,7 +220,7 @@ export default defineComponent({
// Which category slots carry a bar, so hover can skip empty slots. // Which category slots carry a bar, so hover can skip empty slots.
slotsWithData(): boolean[] { slotsWithData(): boolean[] {
const index = new Map(this.categoryKeys.map((k, i) => [k, i])); const index = new Map(this.categoryKeys.map((k, i) => [k, i]));
const has = new Array(this.categoryKeys.length).fill(false); const has = Array.from({ length: this.categoryKeys.length }, () => false);
for (const s of this.visibleSeries) { for (const s of this.visibleSeries) {
for (const slot of s.data) { for (const slot of s.data) {
if (slot.energy <= 0 && slot.returnEnergy <= 0) continue; if (slot.energy <= 0 && slot.returnEnergy <= 0) continue;
@ -264,7 +264,10 @@ export default defineComponent({
// Always render overlay slot (line series) so series structure is stable; // Always render overlay slot (line series) so series structure is stable;
// data is all-null when toggled off. Prepend so it renders BEHIND bars. // data is all-null when toggled off. Prepend so it renders BEHIND bars.
const overlayValues: (number | null)[] = new Array(cats.length).fill(null); const overlayValues: (number | null)[] = Array.from(
{ length: cats.length },
() => null
);
if (this.showOverlay && this.overlay.length) { if (this.showOverlay && this.overlay.length) {
for (const s of this.overlay) { for (const s of this.overlay) {
for (const slot of s.data) { for (const slot of s.data) {
@ -297,8 +300,14 @@ export default defineComponent({
const energyByEntity: (number | null)[][] = []; const energyByEntity: (number | null)[][] = [];
const returnEnergyByEntity: (number | null)[][] = []; const returnEnergyByEntity: (number | null)[][] = [];
this.series.forEach((s, i) => { this.series.forEach((s, i) => {
const energyValues: (number | null)[] = new Array(cats.length).fill(null); const energyValues: (number | null)[] = Array.from(
const returnEnergyValues: (number | null)[] = new Array(cats.length).fill(null); { length: cats.length },
() => null
);
const returnEnergyValues: (number | null)[] = Array.from(
{ length: cats.length },
() => null
);
const hidden = const hidden =
this.focusedEntity !== null && this.focusedEntity !== (s.paletteIndex ?? i); this.focusedEntity !== null && this.focusedEntity !== (s.paletteIndex ?? i);
if (!hidden) { if (!hidden) {
@ -315,8 +324,8 @@ export default defineComponent({
}); });
// Per slot: index of the topmost (largest i) entity with a non-zero // Per slot: index of the topmost (largest i) entity with a non-zero
// value. -1 = no entity has data at that slot. // value. -1 = no entity has data at that slot.
const topEnergyPerSlot: number[] = new Array(cats.length).fill(-1); const topEnergyPerSlot: number[] = Array.from({ length: cats.length }, () => -1);
const topReturnEnergyPerSlot: number[] = new Array(cats.length).fill(-1); const topReturnEnergyPerSlot: number[] = Array.from({ length: cats.length }, () => -1);
for (let i = 0; i < this.series.length; i++) { for (let i = 0; i < this.series.length; i++) {
for (let idx = 0; idx < cats.length; idx++) { for (let idx = 0; idx < cats.length; idx++) {
if ((energyByEntity[i]![idx] ?? 0) > 0) topEnergyPerSlot[idx] = i; if ((energyByEntity[i]![idx] ?? 0) > 0) topEnergyPerSlot[idx] = i;
@ -350,7 +359,9 @@ export default defineComponent({
// top entity doesn't drop the rounding; a focused entity is solo. // top entity doesn't drop the rounding; a focused entity is solo.
const stableIdx = s.paletteIndex ?? i; const stableIdx = s.paletteIndex ?? i;
const energyData: ( const energyData: (
number | null | { value: number; itemStyle: { borderRadius: number[] } } | number
| null
| { value: number; itemStyle: { borderRadius: number[] } }
)[] = energyValues.map((v, idx) => { )[] = energyValues.map((v, idx) => {
if (v == null) return v; if (v == null) return v;
const isTop = const isTop =
@ -361,7 +372,9 @@ export default defineComponent({
return { value: v, itemStyle: { borderRadius: [radius, radius, 0, 0] } }; return { value: v, itemStyle: { borderRadius: [radius, radius, 0, 0] } };
}); });
const returnEnergyData: ( const returnEnergyData: (
number | null | { value: number; itemStyle: { borderRadius: number[] } } | number
| null
| { value: number; itemStyle: { borderRadius: number[] } }
)[] = returnEnergyValues.map((v, idx) => { )[] = returnEnergyValues.map((v, idx) => {
if (v == null) return v; if (v == null) return v;
const isBottom = const isBottom =
@ -486,7 +499,7 @@ export default defineComponent({
if (p.value == null) continue; if (p.value == null) continue;
hasBar = true; hasBar = true;
if (typeof p.value === "number" && p.value > 0) { if (typeof p.value === "number" && p.value > 0) {
if (/-energy$/.test(p.seriesId)) sum += p.value; if (p.seriesId.endsWith("-energy")) sum += p.value;
} }
} }
let x = point[0] - w / 2; let x = point[0] - w / 2;

View file

@ -1,4 +1,4 @@
import { describe, it, expect } from "vitest"; import { describe, it, expect } from "vite-plus/test";
import { formatJson } from "./format"; import { formatJson } from "./format";
describe("formatJson", () => { describe("formatJson", () => {

View file

@ -1,4 +1,4 @@
import { describe, it, expect } from "vitest"; import { describe, it, expect } from "vite-plus/test";
import { generateGitHubContent } from "./template"; import { generateGitHubContent } from "./template";
import type { IssueData, Sections } from "./types"; import type { IssueData, Sections } from "./types";

View file

@ -176,8 +176,13 @@ export default defineComponent({
return this.loadpoints[index]?.id; return this.loadpoints[index]?.id;
}, },
handleCarouselScroll() { handleCarouselScroll() {
const { scrollLeft } = this.$refs["carousel"] as HTMLElement; const carousel = this.$refs["carousel"] as HTMLElement | undefined;
const { offsetWidth } = this.$refs["carousel"]?.children[0] as HTMLElement; if (!carousel || !carousel.children.length) {
return;
}
const { scrollLeft } = carousel;
const { offsetWidth } = carousel.children[0] as HTMLElement;
this.highlightedIndex = Math.round((scrollLeft - 7.5) / offsetWidth); this.highlightedIndex = Math.round((scrollLeft - 7.5) / offsetWidth);
// save scroll position to url if not changing for 2s // save scroll position to url if not changing for 2s
@ -203,7 +208,11 @@ export default defineComponent({
this.viewportHeight = window.innerHeight; this.viewportHeight = window.innerHeight;
}, },
left(index: number) { left(index: number) {
return (this.$refs["carousel"]?.children[0] as HTMLElement).offsetWidth * index; const carousel = this.$refs["carousel"] as HTMLElement | undefined;
if (!carousel || !carousel.children.length) {
return 0;
}
return (carousel.children[0] as HTMLElement).offsetWidth * index;
}, },
scrollTo(index: number) { scrollTo(index: number) {
this.highlightedIndex = index; this.highlightedIndex = index;
@ -224,12 +233,14 @@ export default defineComponent({
}, },
openChargingPlanModal(loadpointId: string) { openChargingPlanModal(loadpointId: string) {
const modal = this.$refs["chargingPlanModal"] as const modal = this.$refs["chargingPlanModal"] as
InstanceType<typeof ChargingPlanModal> | undefined; | InstanceType<typeof ChargingPlanModal>
| undefined;
modal?.open(loadpointId); modal?.open(loadpointId);
}, },
openSettingsModal(loadpointId: string) { openSettingsModal(loadpointId: string) {
const modal = this.$refs["settingsModal"] as const modal = this.$refs["settingsModal"] as
InstanceType<typeof SettingsModal> | undefined; | InstanceType<typeof SettingsModal>
| undefined;
modal?.open(loadpointId); modal?.open(loadpointId);
}, },
}, },

View file

@ -115,9 +115,7 @@
moneySaved, moneySaved,
currency, currency,
false false
)} ${fmtCurrencySymbol(currency)} ${$t( )} ${fmtCurrencySymbol(currency)} ${$t('footer.savings.moneySaved')}`
'footer.savings.moneySaved'
)}`
: '' : ''
" "
/> />
@ -131,11 +129,7 @@
unit="g/kWh" unit="g/kWh"
:sub1=" :sub1="
region && co2Configured region && co2Configured
? `${fmtNumber( ? `${fmtNumber(co2Saved, 0, 'kilogram')} ${$t('footer.savings.co2Saved')}`
co2Saved,
0,
'kilogram'
)} ${$t('footer.savings.co2Saved')}`
: '' : ''
" "
/> />

View file

@ -249,7 +249,7 @@ export default defineComponent({
}, },
options() { options() {
// capture vue component this to be used in chartjs callbacks // capture vue component this to be used in chartjs callbacks
// eslint-disable-next-line @typescript-eslint/no-this-alias // oxlint-disable-next-line @typescript-eslint/no-this-alias
const vThis = this; const vThis = this;
return { return {
...commonOptions, ...commonOptions,

View file

@ -163,7 +163,7 @@ export default defineComponent({
}, },
options() { options() {
// capture vue component this to be used in chartjs callbacks // capture vue component this to be used in chartjs callbacks
// eslint-disable-next-line @typescript-eslint/no-this-alias // oxlint-disable-next-line @typescript-eslint/no-this-alias
const vThis = this; const vThis = this;
return { return {
...commonOptions, ...commonOptions,

View file

@ -27,7 +27,8 @@ export default defineComponent({
methods: { methods: {
requestAuthProvider(providerId: string) { requestAuthProvider(providerId: string) {
const navigationArea = this.$refs["navigationArea"] as const navigationArea = this.$refs["navigationArea"] as
InstanceType<typeof TopNavigationArea> | undefined; | InstanceType<typeof TopNavigationArea>
| undefined;
navigationArea?.requestAuthProvider(providerId); navigationArea?.requestAuthProvider(providerId);
}, },
}, },

View file

@ -1,5 +1,5 @@
import { mount, config } from "@vue/test-utils"; import { mount, config } from "@vue/test-utils";
import { describe, expect, test } from "vitest"; import { describe, expect, test } from "vite-plus/test";
import Status from "./Status.vue"; import Status from "./Status.vue";
import { CURRENCY } from "@/types/evcc"; import { CURRENCY } from "@/types/evcc";
import en from "../../../../i18n/en.json"; import en from "../../../../i18n/en.json";

View file

@ -199,7 +199,8 @@ export default defineComponent({
displayLimitSoc: this.effectiveLimitSoc, displayLimitSoc: this.effectiveLimitSoc,
statusOverride: undefined as VehicleStatus | undefined, statusOverride: undefined as VehicleStatus | undefined,
chargingPlanModal: this.$refs["chargingPlanModal"] as chargingPlanModal: this.$refs["chargingPlanModal"] as
InstanceType<typeof ChargingPlanModal> | undefined, | InstanceType<typeof ChargingPlanModal>
| undefined,
}; };
}, },
computed: { computed: {

View file

@ -1,4 +1,4 @@
import { describe, expect, test } from "vitest"; import { describe, expect, test } from "vite-plus/test";
import { parseKey, parseQueryString, buildQuery, extractQueryString } from "./configModal"; import { parseKey, parseQueryString, buildQuery, extractQueryString } from "./configModal";
describe("parseKey", () => { describe("parseKey", () => {

View file

@ -37,7 +37,7 @@ export default {
self.updateBreakpoint(); self.updateBreakpoint();
window.addEventListener("resize", self.updateBreakpoint); window.addEventListener("resize", self.updateBreakpoint);
}, },
beforeDestroy() { beforeUnmount() {
const self = this as any; const self = this as any;
window.removeEventListener("resize", self.updateBreakpoint); window.removeEventListener("resize", self.updateBreakpoint);
}, },

View file

@ -1,5 +1,5 @@
import { mount, config } from "@vue/test-utils"; import { mount, config } from "@vue/test-utils";
import { describe, expect, test, vi } from "vitest"; import { describe, expect, test, vi } from "vite-plus/test";
import formatter, { POWER_UNIT } from "./formatter"; import formatter, { POWER_UNIT } from "./formatter";
import * as units from "../units"; import * as units from "../units";
import { defineComponent } from "vue"; import { defineComponent } from "vue";

View file

@ -1,4 +1,4 @@
import { describe, expect, test } from "vitest"; import { describe, expect, test } from "vite-plus/test";
import { stringifyQuery } from "./router"; import { stringifyQuery } from "./router";
describe("stringifyQuery", () => { describe("stringifyQuery", () => {

View file

@ -764,7 +764,13 @@ export interface SelectOption<T> {
} }
export type DeviceType = export type DeviceType =
"charger" | "meter" | "vehicle" | "loadpoint" | "messenger" | "tariff" | "hems"; | "charger"
| "meter"
| "vehicle"
| "loadpoint"
| "messenger"
| "tariff"
| "hems";
export type MeterType = "grid" | "pv" | "battery" | "charge" | "aux" | "ext" | "consumer"; export type MeterType = "grid" | "pv" | "battery" | "charge" | "aux" | "ext" | "consumer";
export type MeterTemplateUsage = "grid" | "pv" | "battery" | "charge" | "aux"; export type MeterTemplateUsage = "grid" | "pv" | "battery" | "charge" | "aux";
export type TariffType = "grid" | "feedIn" | "co2" | "planner" | "solar" | "temperature"; export type TariffType = "grid" | "feedIn" | "co2" | "planner" | "solar" | "temperature";

View file

@ -1,4 +1,4 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars // oxlint-disable-next-line @typescript-eslint/no-unused-vars
import type { ComponentCustomProperties } from "vue"; import type { ComponentCustomProperties } from "vue";
declare module "vue" { declare module "vue" {

View file

@ -1,4 +1,4 @@
import { describe, expect, test } from "vitest"; import { describe, expect, test } from "vite-plus/test";
import { circuitTree } from "./circuits"; import { circuitTree } from "./circuits";
describe("circuitTree", () => { describe("circuitTree", () => {

View file

@ -1,4 +1,4 @@
import { describe, expect, test } from "vitest"; import { describe, expect, test } from "vite-plus/test";
import { cleanYaml } from "./cleanYaml"; import { cleanYaml } from "./cleanYaml";
describe("cleanYaml", () => { describe("cleanYaml", () => {

View file

@ -1,4 +1,4 @@
import { describe, expect, test } from "vitest"; import { describe, expect, test } from "vite-plus/test";
import { extractDomain } from "./extractDomain"; import { extractDomain } from "./extractDomain";
describe("extractDomain", () => { describe("extractDomain", () => {

View file

@ -1,4 +1,4 @@
import { describe, expect, test } from "vitest"; import { describe, expect, test } from "vite-plus/test";
import { findLowestSumSlotIndex, isStaticTariff } from "./forecast"; import { findLowestSumSlotIndex, isStaticTariff } from "./forecast";
describe("findLowestSumSlotIndex", () => { describe("findLowestSumSlotIndex", () => {

View file

@ -1,4 +1,4 @@
import { describe, it, expect } from "vitest"; import { describe, it, expect } from "vite-plus/test";
import { extractPlaceholders as extract, replacePlaceholders as replace } from "./placeholder"; import { extractPlaceholders as extract, replacePlaceholders as replace } from "./placeholder";
describe("placeholder", () => { describe("placeholder", () => {

View file

@ -1,4 +1,4 @@
import { describe, expect, test } from "vitest"; import { describe, expect, test } from "vite-plus/test";
import { calculateCostRange, findRateInRange, generateRateSlots } from "./tariffSlots"; import { calculateCostRange, findRateInRange, generateRateSlots } from "./tariffSlots";
import type { Rate, Slot } from "../types/evcc"; import type { Rate, Slot } from "../types/evcc";

6
env.d.ts vendored
View file

@ -1 +1,7 @@
/// <reference types="vite/client" /> /// <reference types="vite/client" />
declare module "*.vue" {
import type { DefineComponent } from "vue";
const component: DefineComponent<{}, {}, unknown>;
export default component;
}

View file

@ -1,39 +0,0 @@
import globals from "globals";
import js from "@eslint/js";
import pluginVue from "eslint-plugin-vue";
import { defineConfigWithVueTs, vueTsConfigs } from "@vue/eslint-config-typescript";
import skipFormattingConfig from "@vue/eslint-config-prettier/skip-formatting";
export default [
...defineConfigWithVueTs(
js.configs.recommended,
...pluginVue.configs["flat/recommended"],
vueTsConfigs.recommended,
skipFormattingConfig,
{
languageOptions: {
globals: {
...globals.browser,
...globals.node,
},
ecmaVersion: "latest",
sourceType: "module",
},
rules: {
"vue/require-default-prop": "off",
"vue/attribute-hyphenation": "off",
"vue/multi-word-component-names": "off",
"vue/no-reserved-component-names": "off",
/*"vue/no-undef-properties": "warn",*/
"no-param-reassign": "error",
"vue/block-lang": "off",
"@typescript-eslint/no-explicit-any": "off",
},
}
).map((config) => ({
...config,
files: ["assets/**/*.{ts,js,vue}", "tests/**/*.ts"],
})),
];

6
oxfmt.config.ts Normal file
View file

@ -0,0 +1,6 @@
import { defineConfig } from "oxfmt";
export default defineConfig({
ignorePatterns: ["tests/custom-css.css"],
overrides: [{ files: ["**/*.vue"], options: { tabWidth: 4 } }],
});

4712
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -3,17 +3,17 @@
"description": "evcc UI", "description": "evcc UI",
"author": "evcc-io", "author": "evcc-io",
"scripts": { "scripts": {
"build": "vite build", "build": "vp build",
"test": "cross-env TZ=Europe/Berlin NODE_OPTIONS=--no-experimental-webstorage vitest", "test": "cross-env TZ=Europe/Berlin NODE_OPTIONS=--no-experimental-webstorage vp test",
"lint": "npm run lint:prettier && npm run lint:eslint && npm run lint:tsc && npm run lint:i18n", "lint": "vp run lint:format && vp run lint:lint && vp run lint:tsc && vp run lint:i18n",
"lint:prettier": "prettier assets tests **/*.{yaml,sh} i18n/*.json --write", "lint:format": "vp fmt assets tests **/*.{yaml,sh} i18n/*.json --write",
"lint:eslint": "eslint --fix --max-warnings=0", "lint:lint": "vp lint --fix --max-warnings=0",
"lint:tsc": "vue-tsc --noEmit", "lint:tsc": "vue-tsc --noEmit",
"lint:i18n": "tsx i18n/check.ts", "lint:i18n": "tsx i18n/check.ts",
"dev": "vite", "dev": "vp dev",
"playwright": "playwright test --ui", "playwright": "playwright install && playwright test --ui",
"playwright:ci": "cross-env CI=true playwright test", "playwright:ci": "cross-env CI=true playwright test",
"simulator": "vite tests/simulator", "simulator": "vp dev tests/simulator",
"storybook": "storybook dev -p 6006", "storybook": "storybook dev -p 6006",
"license": "license-compliance --production --report detailed --allow \"MIT;Apache-2.0;ISC;BSD-2-Clause;BSD-3-Clause;0BSD\"" "license": "license-compliance --production --report detailed --allow \"MIT;Apache-2.0;ISC;BSD-2-Clause;BSD-3-Clause;0BSD\""
}, },
@ -51,11 +51,15 @@
"npm": ">=10.0.0", "npm": ">=10.0.0",
"node": ">=26.0.0" "node": ">=26.0.0"
}, },
"devEngines": {
"packageManager": {
"name": "npm",
"version": ">=10.0.0"
}
},
"license": "MIT", "license": "MIT",
"repository": "github:evcc-io/evcc", "repository": "github:evcc-io/evcc",
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3.3.6",
"@eslint/js": "^9.39.4",
"@jest/types": "^30.4.1", "@jest/types": "^30.4.1",
"@playwright/test": "^1.62.0", "@playwright/test": "^1.62.0",
"@storybook/vue3": "^10.4.4", "@storybook/vue3": "^10.4.4",
@ -71,40 +75,34 @@
"@vitejs/plugin-legacy": "^8.2.2", "@vitejs/plugin-legacy": "^8.2.2",
"@vitejs/plugin-vue": "^6.0.8", "@vitejs/plugin-vue": "^6.0.8",
"@vue/compiler-sfc": "^3.5.38", "@vue/compiler-sfc": "^3.5.38",
"@vue/eslint-config-prettier": "^10.2.0",
"@vue/eslint-config-typescript": "^14.9.0",
"@vue/test-utils": "^2.4.11", "@vue/test-utils": "^2.4.11",
"@vue/tsconfig": "^0.9.1", "@vue/tsconfig": "^0.9.1",
"body-parser": "^2.3.0", "body-parser": "^2.3.0",
"cross-env": "^10.1.0", "cross-env": "^10.1.0",
"eslint": "^9.39.5",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^5.5.6", "eslint-plugin-prettier": "^5.5.6",
"eslint-plugin-promise": "^7.3.0",
"eslint-plugin-vue": "^10.10.0",
"globals": "^17.8.0", "globals": "^17.8.0",
"happy-dom": "^20.11.1", "happy-dom": "^20.11.1",
"jiti": "^2.7.0", "jiti": "^2.7.0",
"kill-port": "^2.0.1", "kill-port": "^2.0.1",
"license-compliance": "^3.0.1", "license-compliance": "^3.0.1",
"mqtt": "^5.15.2", "mqtt": "^5.15.2",
"prettier": "^3.9.6",
"prettier-plugin-sh": "^0.18.1",
"prettier-plugin-sort-json": "^4.2.0",
"rollup-plugin-visualizer": "^7.0.1", "rollup-plugin-visualizer": "^7.0.1",
"storybook": "^10.5.5", "storybook": "^10.5.5",
"terser": "^5.49.0", "terser": "^5.49.0",
"tsx": "^4.23.1", "tsx": "^4.23.1",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"typescript-eslint-language-service": "^5.0.5", "typescript-eslint-language-service": "^5.0.5",
"vite": "^8.1.5", "vite": "npm:@voidzero-dev/vite-plus-core@0.2.6",
"vitest": "^4.1.10", "vitest": "4.1.10",
"vue-eslint-parser": "^10.4.1", "vue-eslint-parser": "^10.4.1",
"vue-hot-reload-api": "^2.3.4", "vue-hot-reload-api": "^2.3.4",
"vue-tsc": "^3.3.8", "vue-tsc": "^3.3.8",
"wait-on": "^9.1.0", "wait-on": "^9.1.0",
"ws": "^8.21.1" "ws": "^8.21.1",
"vite-plus": "0.2.6"
},
"overrides": {
"vite": "npm:@voidzero-dev/vite-plus-core@0.2.6",
"vitest": "4.1.10"
} }
} }

View file

@ -1,11 +0,0 @@
/** @type {import("prettier").Config} */
export default {
printWidth: 100,
trailingComma: "es5",
plugins: ["prettier-plugin-sh"],
// mirror .editorconfig for editors that resolve prettier config without it (e.g. Zed)
overrides: [
{ files: ["*.vue", "*.sh"], options: { useTabs: true, tabWidth: 4 } },
{ files: "*.css", options: { useTabs: true, tabWidth: 2 } },
],
};

View file

@ -62,7 +62,7 @@ export async function startSimulator() {
log(`wait until port ${port} is available`); log(`wait until port ${port} is available`);
await waitOn({ resources: [`tcp:${port}`], reverse: true, log: LOG_ENABLED }); await waitOn({ resources: [`tcp:${port}`], reverse: true, log: LOG_ENABLED });
const instance = spawn("npm", ["run", "simulator", "--", "--port", port.toString()]); const instance = spawn("vp", ["run", "simulator", "--port", port.toString()]);
const steamLog = createSteamLog(); const steamLog = createSteamLog();
instance.stdout.pipe(steamLog); instance.stdout.pipe(steamLog);

View file

@ -1,4 +1,4 @@
import { defineConfig } from "vite"; import { defineConfig } from "vite-plus";
import vue from "@vitejs/plugin-vue"; import vue from "@vitejs/plugin-vue";
import api from "./api"; import api from "./api";

View file

@ -9,11 +9,9 @@
"noPropertyAccessFromIndexSignature": true, "noPropertyAccessFromIndexSignature": true,
"noUnusedLocals": true, "noUnusedLocals": true,
"noUnusedParameters": true, "noUnusedParameters": true,
"composite": true,
"allowJs": true, "allowJs": true,
"baseUrl": ".",
"paths": { "paths": {
"@/*": ["assets/js/*"] "@/*": ["./assets/js/*"]
}, },
"plugins": [ "plugins": [
{ {

View file

@ -1,4 +1,4 @@
import { defineConfig } from "vite"; import { defineConfig, lazyPlugins } from "vite-plus";
import vuePlugin from "@vitejs/plugin-vue"; import vuePlugin from "@vitejs/plugin-vue";
import legacy from "@vitejs/plugin-legacy"; import legacy from "@vitejs/plugin-legacy";
import { browserslistToTargets } from "lightningcss"; import { browserslistToTargets } from "lightningcss";
@ -7,6 +7,52 @@ import { visualizer } from "rollup-plugin-visualizer";
import path from "path"; import path from "path";
export default defineConfig({ export default defineConfig({
staged: {
"*": "vp check --fix",
},
lint: {
plugins: ["oxc", "typescript", "unicorn", "react"],
categories: {
correctness: "warn",
},
env: {
builtin: true,
},
overrides: [
{
files: ["assets/**/*.{ts,js,vue}", "tests/**/*.ts"],
rules: {
"no-param-reassign": "error",
"vue/require-default-prop": "off",
"vue/no-reserved-component-names": "off",
"typescript/no-explicit-any": "off",
},
plugins: ["vue"],
env: {
es2026: true,
browser: true,
node: true,
},
},
],
options: {},
jsPlugins: [
{
name: "vite-plus",
specifier: "vite-plus/oxlint-plugin",
},
],
rules: {
"vite-plus/prefer-vite-plus-imports": "error",
},
},
fmt: {
printWidth: 100,
trailingComma: "es5",
sortPackageJson: false,
ignorePatterns: ["tests/custom-css.css"],
overrides: [{ files: ["**/*.vue"], options: { tabWidth: 4 } }],
},
root: "./assets", root: "./assets",
publicDir: "public", publicDir: "public",
base: "./", base: "./",
@ -41,7 +87,7 @@ export default defineConfig({
}, },
}; };
})(), })(),
plugins: [ plugins: lazyPlugins(() => [
legacy({ legacy({
modernPolyfills: ["es.promise.all-settled"], modernPolyfills: ["es.promise.all-settled"],
}), }),
@ -53,5 +99,5 @@ export default defineConfig({
}, },
}), }),
visualizer({ filename: "asset-stats.html" }), visualizer({ filename: "asset-stats.html" }),
], ]),
}); });

View file

@ -1,5 +1,5 @@
import { mergeConfig } from "vite"; import { mergeConfig } from "vite-plus";
import { defineConfig } from "vitest/config"; import { defineConfig } from "vite-plus";
import viteConfig from "./vite.config"; import viteConfig from "./vite.config";
export default mergeConfig( export default mergeConfig(