refactor(frontend): replace recharts with uPlot for charts

Swap the Sparkline chart component (the only chart in the panel) from recharts to uPlot, keeping its public prop API identical so the three consumers (SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel) are untouched. This drops recharts and 32 transitive deps (es-toolkit, victory-vendor, d3, redux, immer), shrinking the chart vendor chunk to ~51KB (22KB gzip).

The uPlot port reimplements every recharts feature on canvas: gradient area fill, spline curves, up to three series, dashed horizontal grid, formatted axes, hover tooltip and marker, reference lines, and min/max extrema dots. Because canvas cannot read CSS variables, axis and ring colors are resolved via getComputedStyle and repainted on theme changes through a MutationObserver on the body class and documentElement data-theme.

Also removes the es-toolkit/compat resolver shim from vite.config.js, which existed only for recharts, and swaps the manualChunks entry to vendor-uplot.

Note: repaint with redraw(false); a bare uPlot redraw() re-runs _setScale and nulls the index-based x-scale, which collapsed the series to a flat, partial line.
This commit is contained in:
MHSanaei
2026-07-08 02:20:30 +02:00
parent bc309ed9f8
commit 8ee79cf447
6 changed files with 487 additions and 581 deletions
+1 -1
View File
@@ -63,7 +63,7 @@ Two key ideas that explain most of the complexity:
**Frontend (`frontend/`):**
- **React 19** + **Ant Design 6** + **Vite 8** + **TypeScript**.
- Data layer: **TanStack Query** (`@tanstack/react-query`) over the native **Fetch API**; **Zod 4** schemas.
- Router: **react-router-dom 7**. Charts: **recharts**. Editor: **CodeMirror 6**.
- Router: **react-router-dom 7**. Charts: **uPlot** (`frontend/src/components/viz/Sparkline.tsx`). Editor: **CodeMirror 6**.
- **Build output goes to `internal/web/dist/`** (see `vite.config.js``outDir`) and is
embedded into the Go binary with `go:embed`. Three HTML entries: `index.html` (panel SPA),
`login.html`, `subpage.html`. The Go server serves the SPA; there is no separate frontend
+8 -332
View File
@@ -24,8 +24,8 @@
"react-dom": "^19.2.7",
"react-i18next": "^17.0.8",
"react-router-dom": "^7.18.1",
"recharts": "^3.9.1",
"swagger-ui-react": "^5.32.8",
"uplot": "^1.6.32",
"zod": "^4.4.3"
},
"devDependencies": {
@@ -1816,32 +1816,6 @@
"react-dom": ">=18.0.0"
}
},
"node_modules/@reduxjs/toolkit": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.12.0.tgz",
"integrity": "sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==",
"license": "MIT",
"dependencies": {
"@standard-schema/spec": "^1.0.0",
"@standard-schema/utils": "^0.3.0",
"immer": "^11.0.0",
"redux": "^5.0.1",
"redux-thunk": "^3.1.0",
"reselect": "^5.1.0"
},
"peerDependencies": {
"react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
"react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
},
"peerDependenciesMeta": {
"react": {
"optional": true
},
"react-redux": {
"optional": true
}
}
},
"node_modules/@rolldown/binding-android-arm64": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.4.tgz",
@@ -2135,12 +2109,7 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
"license": "MIT"
},
"node_modules/@standard-schema/utils": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
"dev": true,
"license": "MIT"
},
"node_modules/@swagger-api/apidom-ast": {
@@ -2932,69 +2901,6 @@
"assertion-error": "^2.0.1"
}
},
"node_modules/@types/d3-array": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
"integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
"license": "MIT"
},
"node_modules/@types/d3-color": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
"license": "MIT"
},
"node_modules/@types/d3-ease": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
"integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
"license": "MIT"
},
"node_modules/@types/d3-interpolate": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
"license": "MIT",
"dependencies": {
"@types/d3-color": "*"
}
},
"node_modules/@types/d3-path": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
"integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
"license": "MIT"
},
"node_modules/@types/d3-scale": {
"version": "4.0.9",
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
"integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
"license": "MIT",
"dependencies": {
"@types/d3-time": "*"
}
},
"node_modules/@types/d3-shape": {
"version": "3.1.8",
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz",
"integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==",
"license": "MIT",
"dependencies": {
"@types/d3-path": "*"
}
},
"node_modules/@types/d3-time": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
"integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
"license": "MIT"
},
"node_modules/@types/d3-timer": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
"license": "MIT"
},
"node_modules/@types/deep-eql": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
@@ -4259,127 +4165,6 @@
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"license": "MIT"
},
"node_modules/d3-array": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
"license": "ISC",
"dependencies": {
"internmap": "1 - 2"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-color": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-ease": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-format": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz",
"integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-interpolate": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
"license": "ISC",
"dependencies": {
"d3-color": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-path": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
"integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-scale": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
"license": "ISC",
"dependencies": {
"d3-array": "2.10.0 - 3",
"d3-format": "1 - 3",
"d3-interpolate": "1.2.0 - 3",
"d3-time": "2.1.1 - 3",
"d3-time-format": "2 - 4"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-shape": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
"integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
"license": "ISC",
"dependencies": {
"d3-path": "^3.1.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-time": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
"license": "ISC",
"dependencies": {
"d3-array": "2 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-time-format": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
"license": "ISC",
"dependencies": {
"d3-time": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-timer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/damerau-levenshtein": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
@@ -4485,12 +4270,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/decimal.js-light": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
"integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
"license": "MIT"
},
"node_modules/decode-named-character-reference": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz",
@@ -4833,16 +4612,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/es-toolkit": {
"version": "1.49.0",
"resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.49.0.tgz",
"integrity": "sha512-G5iZ6Pc/FNRY/soKZHC+TxGDD83rHUDXxzaWhGCX44vAv/tMs56WMusnm/KMNK+luUPsgA9U28cGr4RDlSzL2g==",
"license": "MIT",
"workspaces": [
"docs",
"benchmarks"
]
},
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
@@ -5122,12 +4891,6 @@
"node": ">=0.10.0"
}
},
"node_modules/eventemitter3": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
"integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
"license": "MIT"
},
"node_modules/expect-type": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz",
@@ -5746,16 +5509,6 @@
"node": ">= 4"
}
},
"node_modules/immer": {
"version": "11.1.9",
"resolved": "https://registry.npmjs.org/immer/-/immer-11.1.9.tgz",
"integrity": "sha512-sc/z0Cyti70bZa0ZU4sWfAElfovFb9Ni8tArJZLuklYWxegPiK3pDOql1Rq5H0FIRAW9LSQRG6OX4KqBldbhBA==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
}
},
"node_modules/immutable": {
"version": "3.8.3",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.3.tgz",
@@ -5796,15 +5549,6 @@
"node": ">= 0.4"
}
},
"node_modules/internmap": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/invariant": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
@@ -7633,13 +7377,6 @@
"react": "^18.0.0 || ^19.0.0"
}
},
"node_modules/react-is": {
"version": "19.2.7",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.7.tgz",
"integrity": "sha512-kZFnouyVv7eP/Phmrlo9FK+zcAdriZJvzxXHF1Sl1P377WSGe2G/JxVolhTrB/jeV47lKImhNUsijjHAAbcl/A==",
"license": "MIT",
"peer": true
},
"node_modules/react-redux": {
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.3.0.tgz",
@@ -7721,36 +7458,6 @@
"react": ">= 0.14.0"
}
},
"node_modules/recharts": {
"version": "3.9.1",
"resolved": "https://registry.npmjs.org/recharts/-/recharts-3.9.1.tgz",
"integrity": "sha512-WMcwlXcB7l+BbxiEdyClkG+1sxrMHNZpzT577LEvU4+rXPd8oTAy1wXk72hnk2KOOmxuLvw3z5DtXT7HEAydtg==",
"license": "MIT",
"workspaces": [
"www"
],
"dependencies": {
"@reduxjs/toolkit": "^1.9.0 || 2.x.x",
"clsx": "^2.1.1",
"decimal.js-light": "^2.5.1",
"es-toolkit": "^1.39.3",
"eventemitter3": "^5.0.1",
"immer": "^11.1.8",
"react-redux": "8.x.x || 9.x.x",
"reselect": "5.2.0",
"tiny-invariant": "^1.3.3",
"use-sync-external-store": "^1.2.2",
"victory-vendor": "^37.0.2"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/redux": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
@@ -7766,15 +7473,6 @@
"immutable": "^3.8.1 || ^4.0.0-rc.1"
}
},
"node_modules/redux-thunk": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
"integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
"license": "MIT",
"peerDependencies": {
"redux": "^5.0.0"
}
},
"node_modules/reflect.getprototypeof": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
@@ -8503,12 +8201,6 @@
"node": ">=12.22"
}
},
"node_modules/tiny-invariant": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
"license": "MIT"
},
"node_modules/tinybench": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
@@ -8897,6 +8589,12 @@
"browserslist": ">= 4.21.0"
}
},
"node_modules/uplot": {
"version": "1.6.32",
"resolved": "https://registry.npmjs.org/uplot/-/uplot-1.6.32.tgz",
"integrity": "sha512-KIMVnG68zvu5XXUbC4LQEPnhwOxBuLyW1AHtpm6IKTXImkbLgkMy+jabjLgSLMasNuGGzQm/ep3tOkyTxpiQIw==",
"license": "MIT"
},
"node_modules/uri-js": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
@@ -8926,28 +8624,6 @@
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/victory-vendor": {
"version": "37.3.6",
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz",
"integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==",
"license": "MIT AND ISC",
"dependencies": {
"@types/d3-array": "^3.0.3",
"@types/d3-ease": "^3.0.0",
"@types/d3-interpolate": "^3.0.1",
"@types/d3-scale": "^4.0.2",
"@types/d3-shape": "^3.1.0",
"@types/d3-time": "^3.0.0",
"@types/d3-timer": "^3.0.0",
"d3-array": "^3.1.6",
"d3-ease": "^3.0.1",
"d3-interpolate": "^3.0.1",
"d3-scale": "^4.0.2",
"d3-shape": "^3.1.0",
"d3-time": "^3.0.0",
"d3-timer": "^3.0.1"
}
},
"node_modules/vite": {
"version": "8.1.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.1.3.tgz",
+1 -1
View File
@@ -37,8 +37,8 @@
"react-dom": "^19.2.7",
"react-i18next": "^17.0.8",
"react-router-dom": "^7.18.1",
"recharts": "^3.9.1",
"swagger-ui-react": "^5.32.8",
"uplot": "^1.6.32",
"zod": "^4.4.3"
},
"devDependencies": {
+55 -1
View File
@@ -1,13 +1,67 @@
.sparkline-svg {
.sparkline-plot {
display: block;
width: 100%;
}
.sparkline-plot .uplot,
.sparkline-plot .u-wrap {
width: 100%;
}
.sparkline-plot .u-cursor-x {
border-right: 1px dashed var(--ant-color-border);
}
.sparkline-container {
position: relative;
width: 100%;
}
.sparkline-tooltip {
position: absolute;
top: 0;
left: 0;
pointer-events: none;
white-space: nowrap;
z-index: 2;
padding: 6px 10px;
border: 1px solid var(--ant-color-border-secondary);
border-radius: 6px;
background: var(--ant-color-bg-elevated);
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.12);
color: var(--ant-color-text);
font-size: 12px;
}
.sparkline-tooltip .spk-tt-label {
margin-bottom: 4px;
color: var(--ant-color-text-tertiary);
font-size: 11px;
}
.sparkline-tooltip .spk-tt-row {
display: flex;
align-items: center;
gap: 6px;
font-weight: 500;
line-height: 16px;
}
.sparkline-tooltip .spk-tt-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex: none;
}
.sparkline-tooltip .spk-tt-name {
color: var(--ant-color-text-tertiary);
}
.sparkline-tooltip .spk-tt-val {
margin-left: auto;
}
.sparkline-extrema {
position: absolute;
top: 2px;
+420 -196
View File
@@ -1,15 +1,6 @@
import { useId, useMemo } from 'react';
import {
Area,
AreaChart,
CartesianGrid,
ReferenceDot,
ReferenceLine,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from 'recharts';
import { useEffect, useMemo, useRef } from 'react';
import uPlot from 'uplot';
import 'uplot/dist/uPlot.min.css';
import './Sparkline.css';
export interface SparklineReferenceLine {
@@ -26,8 +17,14 @@ export interface SparklineExtrema {
maxColor?: string;
}
const DEFAULT_STROKE = '#008771';
const DEFAULT_STROKE2 = '#722ed1';
const DEFAULT_STROKE3 = '#a0d911';
const DEFAULT_MIN_COLOR = '#52c41a';
const DEFAULT_MAX_COLOR = '#fa541c';
const GRID_COLOR = 'rgba(128, 128, 140, 0.35)';
const AXIS_FONT = '10px system-ui, -apple-system, "Segoe UI", Roboto, sans-serif';
const LABEL_FONT = 'system-ui, -apple-system, "Segoe UI", Roboto, sans-serif';
interface SparklineProps {
data: number[];
@@ -68,41 +65,79 @@ interface ChartPoint {
label: string;
}
export default function Sparkline({
data,
data2 = [],
data3 = [],
stroke2 = '#722ed1',
stroke3 = '#a0d911',
name1,
name2,
name3,
labels = [],
height = 80,
stroke = '#008771',
strokeWidth = 2,
maxPoints = 120,
showGrid = true,
fillOpacity = 0.22,
showMarker = true,
markerRadius = 3,
showAxes = false,
yTickStep = 25,
tickCountX = 4,
showTooltip = false,
valueMin = 0,
valueMax = 100,
yFormatter = (v: number) => `${Math.round(v)}%`,
tooltipFormatter = null,
tooltipLabelFormatter = null,
referenceLines,
extrema,
}: SparklineProps) {
const reactId = useId();
const safeId = reactId.replace(/[^a-zA-Z0-9]/g, '');
const gradId = `spkGrad-${safeId}`;
const gradId2 = `spkGrad2-${safeId}`;
const gradId3 = `spkGrad3-${safeId}`;
interface ExtremaResult {
min: ChartPoint;
max: ChartPoint;
minIdx: number;
maxIdx: number;
}
interface SparklineView {
points: ChartPoint[];
yDomain: [number, number];
yTicks: number[] | undefined;
xTickIndexes: number[] | undefined;
extremaPoints: ExtremaResult | null;
}
function hexToRgba(hex: string, alpha: number): string {
let h = hex.trim();
if (h.startsWith('#')) h = h.slice(1);
if (h.length === 3) h = h.split('').map((c) => c + c).join('');
if (h.length !== 6) return hex;
const int = Number.parseInt(h, 16);
if (Number.isNaN(int)) return hex;
const r = (int >> 16) & 255;
const g = (int >> 8) & 255;
const b = int & 255;
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
}
function cssVar(el: HTMLElement, name: string, fallback: string): string {
const v = getComputedStyle(el).getPropertyValue(name).trim();
return v || fallback;
}
function parseDash(dash: string, dpr: number): number[] {
return dash.trim().split(/\s+/).map((n) => (Number(n) || 0) * dpr);
}
function dprOf(u: uPlot): number {
return u.width > 0 ? u.ctx.canvas.width / u.width : (uPlot.pxRatio || 1);
}
export default function Sparkline(props: SparklineProps) {
const {
data,
data2 = [],
data3 = [],
stroke = DEFAULT_STROKE,
stroke2 = DEFAULT_STROKE2,
stroke3 = DEFAULT_STROKE3,
name1,
name2,
name3,
labels = [],
height = 80,
strokeWidth = 2,
maxPoints = 120,
showGrid = true,
fillOpacity = 0.22,
showMarker = true,
markerRadius = 3,
showAxes = false,
yTickStep = 25,
tickCountX = 4,
showTooltip = false,
valueMin = 0,
valueMax = 100,
yFormatter = (v: number) => `${Math.round(v)}%`,
tooltipFormatter = null,
tooltipLabelFormatter = null,
referenceLines,
extrema,
} = props;
const hasSeries2 = data2.length > 0;
const hasSeries3 = data3.length > 0;
const multiSeries = hasSeries2 || hasSeries3;
@@ -135,7 +170,7 @@ export default function Sparkline({
return [valueMin, max * 1.1];
}, [points, valueMin, valueMax, hasSeries2, hasSeries3]);
const yTicks = useMemo(() => {
const yTicks = useMemo<number[] | undefined>(() => {
if (!showAxes) return undefined;
const [min, max] = yDomain;
if (valueMax === 100 && valueMin === 0 && yTickStep > 0) {
@@ -147,15 +182,13 @@ export default function Sparkline({
return Array.from({ length: n }, (_, i) => min + ((max - min) * i) / (n - 1));
}, [showAxes, yDomain, valueMin, valueMax, yTickStep]);
const xTickIndexes = useMemo(() => {
const xTickIndexes = useMemo<number[] | undefined>(() => {
if (!showAxes || points.length === 0) return undefined;
const m = Math.max(2, tickCountX);
return Array.from({ length: m }, (_, i) => Math.round((i * (points.length - 1)) / (m - 1)));
}, [showAxes, tickCountX, points.length]);
const fmtTooltip = tooltipFormatter ?? yFormatter;
const extremaPoints = useMemo(() => {
const extremaPoints = useMemo<ExtremaResult | null>(() => {
if (!extrema?.show || multiSeries || points.length < 2) return null;
let minIdx = 0;
let maxIdx = 0;
@@ -191,6 +224,340 @@ export default function Sparkline({
return parts.join(', ');
}, [points, name1, name2, name3, hasSeries2, hasSeries3, yFormatter]);
const cfg = {
stroke,
stroke2,
stroke3,
strokeWidth,
fillOpacity,
markerRadius,
showGrid,
showMarker,
showAxes,
showTooltip,
height,
name1,
name2,
name3,
yFormatter,
tooltipFormatter,
tooltipLabelFormatter,
referenceLines,
extrema,
};
const cfgRef = useRef(cfg);
cfgRef.current = cfg;
const viewRef = useRef<SparklineView>({ points, yDomain, yTicks, xTickIndexes, extremaPoints });
viewRef.current = { points, yDomain, yTicks, xTickIndexes, extremaPoints };
const containerRef = useRef<HTMLDivElement>(null);
const plotRef = useRef<uPlot | null>(null);
useEffect(() => {
const container = containerRef.current;
if (!container) return;
let tooltipEl: HTMLDivElement | null = null;
const seriesColor = (idx: number): string => {
const p = cfgRef.current;
if (idx <= 1) return p.stroke ?? DEFAULT_STROKE;
if (idx === 2) return p.stroke2 ?? DEFAULT_STROKE2;
return p.stroke3 ?? DEFAULT_STROKE3;
};
const gridTicks = (): number[] => {
const yt = viewRef.current.yTicks;
if (yt && yt.length) return yt;
const [mn, mx] = viewRef.current.yDomain;
const n = 4;
return Array.from({ length: n + 1 }, (_, i) => mn + ((mx - mn) * i) / n);
};
const buildData = (): uPlot.AlignedData => {
const v = viewRef.current;
const xs = v.points.map((_, i) => i);
const series: number[][] = [v.points.map((p) => p.value)];
if (hasSeries2) series.push(v.points.map((p) => p.value2));
if (hasSeries3) series.push(v.points.map((p) => p.value3));
return [xs, ...series];
};
const makeSeries = (): uPlot.Series => ({
stroke: (_u, sidx) => seriesColor(sidx),
width: cfgRef.current.strokeWidth ?? 2,
fill: (u, sidx) => {
const { ctx, bbox } = u;
const color = seriesColor(sidx);
const grad = ctx.createLinearGradient(0, bbox.top, 0, bbox.top + bbox.height);
grad.addColorStop(0, hexToRgba(color, cfgRef.current.fillOpacity ?? 0.22));
grad.addColorStop(1, hexToRgba(color, 0));
return grad;
},
paths: uPlot.paths.spline?.(),
points: { show: false },
spanGaps: true,
});
const series: uPlot.Series[] = [{}, makeSeries()];
if (hasSeries2) series.push(makeSeries());
if (hasSeries3) series.push(makeSeries());
const axisStroke = (u: uPlot) => cssVar(u.root, '--ant-color-text-tertiary', '#8c8c8c');
const axes: uPlot.Axis[] = [
{
show: showAxes,
stroke: axisStroke,
grid: { show: false },
ticks: { show: false },
font: AXIS_FONT,
gap: 6,
size: 28,
splits: () => viewRef.current.xTickIndexes ?? [],
values: (_u, splits) => splits.map((i) => viewRef.current.points[i]?.label ?? ''),
},
{
show: showAxes,
scale: 'y',
side: 3,
stroke: axisStroke,
grid: { show: false },
ticks: { show: false },
font: AXIS_FONT,
gap: 4,
size: 56,
splits: () => viewRef.current.yTicks ?? [],
values: (_u, splits) =>
splits.map((v) => (cfgRef.current.yFormatter ? cfgRef.current.yFormatter(v) : String(v))),
},
];
const drawGrid = (u: uPlot) => {
if (cfgRef.current.showGrid === false) return;
const { ctx, bbox } = u;
const dpr = dprOf(u);
ctx.save();
ctx.strokeStyle = GRID_COLOR;
ctx.lineWidth = dpr;
ctx.setLineDash([3 * dpr, 4 * dpr]);
ctx.beginPath();
for (const ty of gridTicks()) {
const py = Math.round(u.valToPos(ty, 'y', true)) + 0.5;
ctx.moveTo(bbox.left, py);
ctx.lineTo(bbox.left + bbox.width, py);
}
ctx.stroke();
ctx.restore();
};
const drawOverlay = (u: uPlot) => {
const p = cfgRef.current;
const v = viewRef.current;
const { ctx, bbox } = u;
const dpr = dprOf(u);
const right = bbox.left + bbox.width;
if (p.referenceLines?.length) {
for (const rl of p.referenceLines) {
const color = rl.color || p.stroke || DEFAULT_STROKE;
const py = Math.round(u.valToPos(rl.y, 'y', true)) + 0.5;
ctx.save();
ctx.strokeStyle = color;
ctx.lineWidth = 1.4 * dpr;
ctx.setLineDash(parseDash(rl.dash ?? '5 4', dpr));
ctx.beginPath();
ctx.moveTo(bbox.left, py);
ctx.lineTo(right, py);
ctx.stroke();
ctx.restore();
if (rl.label) {
ctx.save();
ctx.fillStyle = color;
ctx.font = `600 ${10 * dpr}px ${LABEL_FONT}`;
ctx.textAlign = 'right';
ctx.textBaseline = 'bottom';
ctx.fillText(rl.label, right - 4 * dpr, py - 3 * dpr);
ctx.restore();
}
}
}
const ex = v.extremaPoints;
if (p.extrema?.show && ex) {
const ringColor = cssVar(u.root, '--ant-color-bg-elevated', '#ffffff');
const dot = (value: number, idx: number, color: string) => {
const px = u.valToPos(idx, 'x', true);
const py = u.valToPos(value, 'y', true);
ctx.save();
ctx.beginPath();
ctx.arc(px, py, 4.5 * dpr, 0, Math.PI * 2);
ctx.fillStyle = color;
ctx.fill();
ctx.lineWidth = 2 * dpr;
ctx.strokeStyle = ringColor;
ctx.stroke();
ctx.restore();
};
dot(ex.max.value, ex.maxIdx, p.extrema.maxColor ?? DEFAULT_MAX_COLOR);
dot(ex.min.value, ex.minIdx, p.extrema.minColor ?? DEFAULT_MIN_COLOR);
}
};
const updateTooltip = (u: uPlot) => {
if (!tooltipEl) return;
const idx = u.cursor.idx;
const v = viewRef.current;
const p = cfgRef.current;
if (idx == null || idx < 0 || idx >= v.points.length) {
tooltipEl.style.display = 'none';
return;
}
const pt = v.points[idx];
const fmt = p.tooltipFormatter ?? p.yFormatter ?? ((x: number) => String(x));
const label = p.tooltipLabelFormatter ? p.tooltipLabelFormatter(String(pt.label)) : String(pt.label);
const multi = hasSeries2 || hasSeries3;
tooltipEl.textContent = '';
const labelDiv = document.createElement('div');
labelDiv.className = 'spk-tt-label';
labelDiv.textContent = label;
tooltipEl.appendChild(labelDiv);
const rows = [
{ name: p.name1, color: p.stroke ?? DEFAULT_STROKE, val: pt.value, on: true },
{ name: p.name2, color: p.stroke2 ?? DEFAULT_STROKE2, val: pt.value2, on: hasSeries2 },
{ name: p.name3, color: p.stroke3 ?? DEFAULT_STROKE3, val: pt.value3, on: hasSeries3 },
];
for (const r of rows) {
if (!r.on) continue;
const row = document.createElement('div');
row.className = 'spk-tt-row';
if (multi) {
const marker = document.createElement('span');
marker.className = 'spk-tt-dot';
marker.style.background = r.color;
row.appendChild(marker);
const nm = document.createElement('span');
nm.className = 'spk-tt-name';
nm.textContent = r.name ?? '';
row.appendChild(nm);
}
const val = document.createElement('span');
val.className = 'spk-tt-val';
val.textContent = fmt(r.val);
row.appendChild(val);
tooltipEl.appendChild(row);
}
tooltipEl.style.display = '';
const overW = u.over.clientWidth;
const overH = u.over.clientHeight;
const cx = u.cursor.left ?? 0;
const cy = u.cursor.top ?? 0;
const w = tooltipEl.offsetWidth;
const h = tooltipEl.offsetHeight;
let x = cx + 12;
if (x + w + 8 > overW) x = cx - w - 12;
if (x < 0) x = 4;
let y = cy - h - 12;
if (y < 0) y = Math.min(cy + 12, overH - h - 4);
tooltipEl.style.transform = `translate(${Math.round(x)}px, ${Math.round(y)}px)`;
};
const opts: uPlot.Options = {
width: container.clientWidth || 600,
height,
padding: [8, 8, showAxes ? 0 : 2, showAxes ? 0 : 2],
legend: { show: false },
cursor: {
show: showTooltip,
x: showTooltip,
y: false,
drag: { x: false, y: false, setScale: false },
points: {
show: showMarker,
size: () => (cfgRef.current.markerRadius ?? 3) * 2,
width: 0,
stroke: (_u, sidx) => seriesColor(sidx),
fill: (_u, sidx) => seriesColor(sidx),
},
},
scales: {
x: {
time: false,
range: (_u, dmin, dmax) => (dmin === dmax ? [dmin - 0.5, dmax + 0.5] : [dmin, dmax]),
},
y: {
range: () => {
const [mn, mx] = viewRef.current.yDomain;
return [mn, mx];
},
},
},
series,
axes,
hooks: {
init: [
(u) => {
if (!cfgRef.current.showTooltip) return;
tooltipEl = document.createElement('div');
tooltipEl.className = 'sparkline-tooltip';
tooltipEl.style.display = 'none';
u.over.appendChild(tooltipEl);
},
],
drawClear: [drawGrid],
draw: [drawOverlay],
setCursor: [updateTooltip],
},
};
const u = new uPlot(opts, buildData(), container);
plotRef.current = u;
const ro = new ResizeObserver(() => {
const w = container.clientWidth;
if (w > 0) u.setSize({ width: w, height });
});
ro.observe(container);
return () => {
ro.disconnect();
u.destroy();
plotRef.current = null;
tooltipEl = null;
};
}, [hasSeries2, hasSeries3, showAxes, showTooltip, showMarker, height]);
useEffect(() => {
plotRef.current?.setData(
(() => {
const xs = points.map((_, i) => i);
const s: number[][] = [points.map((p) => p.value)];
if (hasSeries2) s.push(points.map((p) => p.value2));
if (hasSeries3) s.push(points.map((p) => p.value3));
return [xs, ...s] as uPlot.AlignedData;
})(),
);
}, [points, hasSeries2, hasSeries3, valueMin, valueMax]);
useEffect(() => {
plotRef.current?.redraw(false);
});
useEffect(() => {
const redraw = () => plotRef.current?.redraw(false);
const moBody = new MutationObserver(redraw);
moBody.observe(document.body, { attributes: true, attributeFilter: ['class'] });
const moRoot = new MutationObserver(redraw);
moRoot.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
return () => {
moBody.disconnect();
moRoot.disconnect();
};
}, []);
return (
<div className="sparkline-container" role={ariaSummary ? 'img' : undefined} aria-label={ariaSummary || undefined}>
{extremaPoints && (
@@ -210,150 +577,7 @@ export default function Sparkline({
))}
</div>
)}
<ResponsiveContainer width="100%" height={height} className="sparkline-svg">
<AreaChart
data={points}
margin={{
top: showAxes ? 14 : 6,
right: showAxes ? 12 : 6,
bottom: showAxes ? 26 : 4,
left: 4,
}}
>
<defs>
<linearGradient id={gradId} x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor={stroke} stopOpacity={fillOpacity} />
<stop offset="100%" stopColor={stroke} stopOpacity={0} />
</linearGradient>
<linearGradient id={gradId2} x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor={stroke2} stopOpacity={fillOpacity} />
<stop offset="100%" stopColor={stroke2} stopOpacity={0} />
</linearGradient>
<linearGradient id={gradId3} x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor={stroke3} stopOpacity={fillOpacity} />
<stop offset="100%" stopColor={stroke3} stopOpacity={0} />
</linearGradient>
</defs>
{showGrid && (
<CartesianGrid stroke="rgba(128, 128, 140, 0.35)" strokeDasharray="3 4" vertical={false} />
)}
<XAxis
dataKey="label"
hide={!showAxes}
tick={{ fontSize: 10, fill: 'var(--ant-color-text-tertiary)' }}
axisLine={false}
tickLine={false}
tickMargin={14}
interval={0}
ticks={xTickIndexes?.map((i) => points[i]?.label).filter(Boolean) as string[] | undefined}
/>
<YAxis
domain={yDomain}
hide={!showAxes}
tick={{ fontSize: 10, fill: 'var(--ant-color-text-tertiary)', dx: -4 }}
axisLine={false}
tickLine={false}
tickMargin={8}
tickFormatter={yFormatter}
ticks={yTicks}
width={56}
/>
{showTooltip && (
<Tooltip
cursor={{ stroke: 'var(--ant-color-border)', strokeDasharray: '2 4' }}
contentStyle={{
background: 'var(--ant-color-bg-elevated)',
border: '1px solid var(--ant-color-border-secondary)',
borderRadius: 6,
fontSize: 12,
padding: '6px 10px',
boxShadow: '0 4px 14px rgba(0, 0, 0, 0.12)',
}}
labelStyle={{ color: 'var(--ant-color-text-tertiary)', marginBottom: 4, fontSize: 11 }}
itemStyle={{ color: 'var(--ant-color-text)', padding: 0, fontWeight: 500 }}
formatter={(v, name) => [fmtTooltip(Number(v) || 0), multiSeries && typeof name === 'string' ? name : '']}
labelFormatter={(label) => (tooltipLabelFormatter ? tooltipLabelFormatter(String(label)) : String(label))}
separator={multiSeries ? ': ' : ''}
/>
)}
{referenceLines?.map((rl, idx) => (
<ReferenceLine
key={`ref-${idx}-${rl.y}`}
y={rl.y}
stroke={rl.color || stroke}
strokeDasharray={rl.dash || '5 4'}
strokeWidth={1.4}
label={rl.label ? {
value: rl.label,
position: 'insideTopRight',
fill: rl.color || stroke,
fontSize: 10,
fontWeight: 600,
} : undefined}
ifOverflow="extendDomain"
/>
))}
{extremaPoints && (
<>
<ReferenceDot
x={extremaPoints.max.label}
y={extremaPoints.max.value}
r={4.5}
fill={maxColor}
stroke="var(--ant-color-bg-elevated)"
strokeWidth={2}
ifOverflow="extendDomain"
/>
<ReferenceDot
x={extremaPoints.min.label}
y={extremaPoints.min.value}
r={4.5}
fill={minColor}
stroke="var(--ant-color-bg-elevated)"
strokeWidth={2}
ifOverflow="extendDomain"
/>
</>
)}
<Area
type="monotone"
dataKey="value"
name={multiSeries ? name1 : undefined}
stroke={stroke}
strokeWidth={strokeWidth}
fill={`url(#${gradId})`}
dot={false}
activeDot={showMarker ? { r: markerRadius, fill: stroke, strokeWidth: 0 } : false}
isAnimationActive={false}
/>
{hasSeries2 && (
<Area
type="monotone"
dataKey="value2"
name={name2}
stroke={stroke2}
strokeWidth={strokeWidth}
fill={`url(#${gradId2})`}
dot={false}
activeDot={showMarker ? { r: markerRadius, fill: stroke2, strokeWidth: 0 } : false}
isAnimationActive={false}
/>
)}
{hasSeries3 && (
<Area
type="monotone"
dataKey="value3"
name={name3}
stroke={stroke3}
strokeWidth={strokeWidth}
fill={`url(#${gradId3})`}
dot={false}
activeDot={showMarker ? { r: markerRadius, fill: stroke3, strokeWidth: 0 } : false}
isAnimationActive={false}
/>
)}
</AreaChart>
</ResponsiveContainer>
<div ref={containerRef} className="sparkline-plot" style={{ height }} />
</div>
);
}
+2 -50
View File
@@ -77,45 +77,6 @@ function injectBasePathPlugin() {
};
}
// es-toolkit's `./compat/*` exports map only declares a CJS condition, so deep
// imports like `es-toolkit/compat/get` resolve to a CJS shim. That shim uses a
// `require_X.Y` pattern that Vite's optimizer and Rolldown both mishandle
// (TypeError: require_isUnsafeProperty is not a function). The ESM build at
// `dist/compat/<category>/<name>.mjs` is fine but only carries a named export,
// while consumers like recharts use default imports — so emit a virtual module
// that re-exports the named symbol as default.
const ES_TOOLKIT_COMPAT_DIRS = ['array', 'function', 'math', 'object', 'predicate', 'string', 'util'];
const ES_TOOLKIT_SHIM_PREFIX = '\0es-toolkit-compat:';
function findEsToolkitCompatMjs(name) {
for (const sub of ES_TOOLKIT_COMPAT_DIRS) {
const candidate = path.resolve(__dirname, 'node_modules/es-toolkit/dist/compat', sub, `${name}.mjs`);
if (fs.existsSync(candidate)) return candidate;
}
return null;
}
function esToolkitCompatEsmResolver() {
return {
name: 'es-toolkit-compat-esm',
enforce: 'pre',
resolveId(id) {
const m = id.match(/^es-toolkit\/compat\/(.+)$/);
if (!m) return null;
if (!findEsToolkitCompatMjs(m[1])) return null;
return ES_TOOLKIT_SHIM_PREFIX + m[1];
},
load(id) {
if (!id.startsWith(ES_TOOLKIT_SHIM_PREFIX)) return null;
const name = id.slice(ES_TOOLKIT_SHIM_PREFIX.length);
const target = findEsToolkitCompatMjs(name);
if (!target) return null;
const url = target.replace(/\\/g, '/');
return `import { ${name} } from ${JSON.stringify(url)};\nexport { ${name} };\nexport default ${name};\n`;
},
};
}
function bypassMigratedRoute(req) {
if (req.method !== 'GET') return undefined;
const url = req.url.split('?')[0];
@@ -179,17 +140,12 @@ function makeBackendProxy(target) {
}
export default defineConfig({
plugins: [esToolkitCompatEsmResolver(), react(), injectBasePathPlugin()],
plugins: [react(), injectBasePathPlugin()],
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
optimizeDeps: {
rolldownOptions: {
plugins: [esToolkitCompatEsmResolver()],
},
},
experimental: {
renderBuiltUrl(filename, { hostType }) {
if (hostType === 'js') {
@@ -249,11 +205,7 @@ export default defineConfig({
|| id.includes('/node_modules/swagger-ui/')
|| id.includes('/node_modules/swagger-client/')
) return 'vendor-swagger';
if (
id.includes('/node_modules/recharts/')
|| id.includes('/node_modules/victory-vendor/')
|| id.includes('/node_modules/d3-')
) return 'vendor-recharts';
if (id.includes('/node_modules/uplot/')) return 'vendor-uplot';
if (id.includes('dayjs')) return 'vendor-dayjs';
return 'vendor';
},