free-coding-models 0.5.86 β 0.5.88
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +109 -888
- package/bin/free-coding-models.js +4 -1
- package/changelog/v0.5.87.md +48 -0
- package/changelog/v0.5.88.md +16 -0
- package/package.json +13 -5
- package/sources.js +4 -4
- package/src/core/cache.js +3 -1
- package/src/core/config.js +43 -5
- package/src/core/endpoint-installer.js +23 -12
- package/src/core/installed-models-manager.js +43 -20
- package/src/core/kilo-config.js +51 -5
- package/src/core/kilo.js +5 -3
- package/src/core/legacy-proxy-cleanup.js +22 -5
- package/src/core/model-merger.js +4 -0
- package/src/core/models-drift.js +9 -0
- package/src/core/opencode-config.js +57 -5
- package/src/core/opencode.js +67 -41
- package/src/core/playground.js +7 -0
- package/src/core/probe-cache.js +35 -4
- package/src/core/provider-key-tester.js +10 -2
- package/src/core/provider-metadata.js +10 -0
- package/src/core/provider-quota-fetchers.js +14 -7
- package/src/core/router-daemon.js +208 -26
- package/src/core/runtime-telemetry.js +27 -1
- package/src/core/security.js +170 -24
- package/src/core/shared-helpers.js +12 -1
- package/src/core/shell-env.js +3 -1
- package/src/core/telemetry.js +18 -5
- package/src/core/tool-launchers.js +9 -6
- package/src/core/updater.js +28 -2
- package/src/core/utils.js +173 -71
- package/src/tui/app.js +38 -14
- package/src/tui/command-palette.js +22 -2
- package/src/tui/key-handler.js +123 -179
- package/src/tui/overlays.js +21 -71
- package/src/tui/render-helpers.js +62 -4
- package/src/tui/render-table.js +34 -3
- package/src/tui/theme.js +36 -5
- package/src/tui/tui-filters.js +28 -8
- package/src/tui/tui-state.js +22 -6
- package/web/dist/assets/index-CAzFIt8P.css +1 -0
- package/web/dist/assets/index-DFg1h0Nd.js +44 -0
- package/web/dist/index.html +5 -9
- package/web/index.html +3 -7
- package/web/server.js +97 -23
- package/web/vite.config.js +8 -0
- package/web/app.legacy.js +0 -900
- package/web/dist/assets/index-C5-Ywvv3.css +0 -1
- package/web/dist/assets/index-Dm-Xfby9.js +0 -40
- package/web/public/favicon.ico +0 -0
- package/web/public/favicons/apple-touch-icon.png +0 -0
- package/web/public/favicons/browserconfig.xml +0 -12
- package/web/public/favicons/favicon-16x16.png +0 -0
- package/web/public/favicons/favicon-192x192.png +0 -0
- package/web/public/favicons/favicon-32x32.png +0 -0
- package/web/public/favicons/favicon-48x48.png +0 -0
- package/web/public/favicons/favicon-512x512.png +0 -0
- package/web/public/favicons/favicon-96x96.png +0 -0
- package/web/public/favicons/favicon.ico +0 -0
- package/web/public/favicons/mstile-150x150.png +0 -0
- package/web/public/favicons/mstile-310x150.png +0 -0
- package/web/public/favicons/mstile-310x310.png +0 -0
- package/web/public/favicons/mstile-512x512.png +0 -0
- package/web/public/favicons/mstile-70x70.png +0 -0
- package/web/public/favicons/site.webmanifest +0 -25
- package/web/src/App.jsx +0 -521
- package/web/src/components/analytics/AnalyticsView.jsx +0 -147
- package/web/src/components/analytics/AnalyticsView.module.css +0 -190
- package/web/src/components/analytics/TokenUsagePanel.jsx +0 -105
- package/web/src/components/analytics/TokenUsagePanel.module.css +0 -126
- package/web/src/components/atoms/AILatencyCell.jsx +0 -38
- package/web/src/components/atoms/AILatencyCell.module.css +0 -51
- package/web/src/components/atoms/HealthCell.jsx +0 -143
- package/web/src/components/atoms/HealthCell.module.css +0 -60
- package/web/src/components/atoms/LastPingCell.jsx +0 -35
- package/web/src/components/atoms/LastPingCell.module.css +0 -45
- package/web/src/components/atoms/MoodCell.jsx +0 -25
- package/web/src/components/atoms/MoodCell.module.css +0 -6
- package/web/src/components/atoms/NoKeyIcon.jsx +0 -65
- package/web/src/components/atoms/NoKeyIcon.module.css +0 -30
- package/web/src/components/atoms/ProviderLogo.jsx +0 -187
- package/web/src/components/atoms/ProviderLogo.module.css +0 -103
- package/web/src/components/atoms/RankCell.jsx +0 -9
- package/web/src/components/atoms/RankCell.module.css +0 -9
- package/web/src/components/atoms/Sparkline.jsx +0 -63
- package/web/src/components/atoms/StabilityCell.jsx +0 -18
- package/web/src/components/atoms/StabilityCell.module.css +0 -13
- package/web/src/components/atoms/StatusDot.jsx +0 -13
- package/web/src/components/atoms/StatusDot.module.css +0 -29
- package/web/src/components/atoms/TPSCell.jsx +0 -36
- package/web/src/components/atoms/TPSCell.module.css +0 -44
- package/web/src/components/atoms/TierBadge.jsx +0 -10
- package/web/src/components/atoms/TierBadge.module.css +0 -29
- package/web/src/components/atoms/Toast.jsx +0 -25
- package/web/src/components/atoms/Toast.module.css +0 -41
- package/web/src/components/atoms/ToastContainer.jsx +0 -16
- package/web/src/components/atoms/ToastContainer.module.css +0 -10
- package/web/src/components/atoms/VerdictBadge.jsx +0 -36
- package/web/src/components/atoms/VerdictBadge.module.css +0 -40
- package/web/src/components/changelog/ChangelogView.jsx +0 -135
- package/web/src/components/changelog/ChangelogView.module.css +0 -159
- package/web/src/components/dashboard/DetailPanel.jsx +0 -217
- package/web/src/components/dashboard/DetailPanel.module.css +0 -260
- package/web/src/components/dashboard/ExpandedDetailRow.jsx +0 -361
- package/web/src/components/dashboard/ExpandedDetailRow.module.css +0 -356
- package/web/src/components/dashboard/ExportModal.jsx +0 -84
- package/web/src/components/dashboard/ExportModal.module.css +0 -99
- package/web/src/components/dashboard/FilterBar.jsx +0 -338
- package/web/src/components/dashboard/FilterBar.module.css +0 -342
- package/web/src/components/dashboard/ModelTable.jsx +0 -672
- package/web/src/components/dashboard/ModelTable.module.css +0 -446
- package/web/src/components/dashboard/ProviderDropdown.jsx +0 -156
- package/web/src/components/dashboard/ProviderDropdown.module.css +0 -248
- package/web/src/components/help/HelpView.jsx +0 -201
- package/web/src/components/help/HelpView.module.css +0 -156
- package/web/src/components/install/InstallEndpointsView.jsx +0 -309
- package/web/src/components/install/InstallEndpointsView.module.css +0 -427
- package/web/src/components/installed/InstalledModelsView.jsx +0 -89
- package/web/src/components/installed/InstalledModelsView.module.css +0 -200
- package/web/src/components/launch/IncompatibleFallbackModal.jsx +0 -69
- package/web/src/components/launch/LaunchButton.jsx +0 -30
- package/web/src/components/launch/LaunchButton.module.css +0 -35
- package/web/src/components/launch/LaunchModal.module.css +0 -125
- package/web/src/components/layout/Footer.jsx +0 -19
- package/web/src/components/layout/Footer.module.css +0 -10
- package/web/src/components/layout/Header.jsx +0 -265
- package/web/src/components/layout/Header.module.css +0 -357
- package/web/src/components/palette/CommandPalette.jsx +0 -301
- package/web/src/components/palette/CommandPalette.module.css +0 -121
- package/web/src/components/playground/PlaygroundChat.jsx +0 -527
- package/web/src/components/playground/PlaygroundChat.module.css +0 -382
- package/web/src/components/playground/PlaygroundView.jsx +0 -371
- package/web/src/components/playground/PlaygroundView.module.css +0 -504
- package/web/src/components/recommend/RecommendView.jsx +0 -121
- package/web/src/components/recommend/RecommendView.module.css +0 -57
- package/web/src/components/router/RouterView.jsx +0 -1212
- package/web/src/components/router/RouterView.module.css +0 -1289
- package/web/src/components/settings/SettingsView.jsx +0 -555
- package/web/src/components/settings/SettingsView.module.css +0 -550
- package/web/src/components/tools/ToolPicker.jsx +0 -86
- package/web/src/components/tools/ToolPicker.module.css +0 -97
- package/web/src/components/update/UpdateChip.jsx +0 -104
- package/web/src/components/update/UpdateChip.module.css +0 -146
- package/web/src/global.css +0 -298
- package/web/src/hooks/urlState.constants.js +0 -28
- package/web/src/hooks/useChangelog.js +0 -51
- package/web/src/hooks/useColumnSizing.js +0 -211
- package/web/src/hooks/useFavorites.js +0 -150
- package/web/src/hooks/useFilter.js +0 -310
- package/web/src/hooks/useInstalledModels.js +0 -42
- package/web/src/hooks/useRecommend.js +0 -67
- package/web/src/hooks/useRouterDashboard.js +0 -133
- package/web/src/hooks/useSSE.js +0 -73
- package/web/src/hooks/useSocket.js +0 -203
- package/web/src/hooks/useTheme.js +0 -83
- package/web/src/hooks/useTokenUsage.js +0 -103
- package/web/src/hooks/useToolMode.js +0 -77
- package/web/src/hooks/useUpdateChecker.js +0 -91
- package/web/src/hooks/useUrlState.js +0 -170
- package/web/src/main.jsx +0 -15
- package/web/src/utils/download.js +0 -15
- package/web/src/utils/format.js +0 -42
- package/web/src/utils/m3.js +0 -57
- package/web/src/utils/ranks.js +0 -37
- package/web/styles.legacy.css +0 -963
package/web/dist/index.html
CHANGED
|
@@ -45,17 +45,13 @@
|
|
|
45
45
|
<meta name="twitter:description" content="Find the fastest free coding LLM model in seconds.">
|
|
46
46
|
<meta name="twitter:image" content="/favicons/favicon-512x512.png">
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
<script type="module" crossorigin src="/assets/index-
|
|
52
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
48
|
+
<!-- π No webfont links: the dashboard must work fully offline / air-gapped.
|
|
49
|
+
π Font stacks live in web/src/global.css and fall back to system fonts
|
|
50
|
+
π (locally installed Inter / JetBrains Mono are still picked up if present). -->
|
|
51
|
+
<script type="module" crossorigin src="/assets/index-DFg1h0Nd.js"></script>
|
|
52
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CAzFIt8P.css">
|
|
53
53
|
</head>
|
|
54
54
|
<body>
|
|
55
55
|
<div id="root"></div>
|
|
56
|
-
<!-- π PostHog analytics β privacy-first, self-hosted option available -->
|
|
57
|
-
<script>
|
|
58
|
-
!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.crossOrigin="anonymous",p.async=!0,p.src=s.api_host.replace(".i.",".js."),function(t,e,o,n){var i,s;for(n=function(t,e){return t[e]},s=e.split("."),i=0;i<s.length-1;i++)n=n(s[i],t);(a=n[s.pop()])&&a.apply(t,[].slice.call(arguments,0))}(e,"5.0.0")},e.init('phc_PLACEHOLDER_REPLACE_WITH_YOUR_KEY',{api_host:'https://us.i.posthog.com'}))}(document,window.posthog||[]);
|
|
59
|
-
</script>
|
|
60
56
|
</body>
|
|
61
57
|
</html>
|
package/web/index.html
CHANGED
|
@@ -45,16 +45,12 @@
|
|
|
45
45
|
<meta name="twitter:description" content="Find the fastest free coding LLM model in seconds.">
|
|
46
46
|
<meta name="twitter:image" content="/favicons/favicon-512x512.png">
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
<!-- π No webfont links: the dashboard must work fully offline / air-gapped.
|
|
49
|
+
π Font stacks live in web/src/global.css and fall back to system fonts
|
|
50
|
+
π (locally installed Inter / JetBrains Mono are still picked up if present). -->
|
|
51
51
|
</head>
|
|
52
52
|
<body>
|
|
53
53
|
<div id="root"></div>
|
|
54
54
|
<script type="module" src="/src/main.jsx"></script>
|
|
55
|
-
<!-- π PostHog analytics β privacy-first, self-hosted option available -->
|
|
56
|
-
<script>
|
|
57
|
-
!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.crossOrigin="anonymous",p.async=!0,p.src=s.api_host.replace(".i.",".js."),function(t,e,o,n){var i,s;for(n=function(t,e){return t[e]},s=e.split("."),i=0;i<s.length-1;i++)n=n(s[i],t);(a=n[s.pop()])&&a.apply(t,[].slice.call(arguments,0))}(e,"5.0.0")},e.init('phc_PLACEHOLDER_REPLACE_WITH_YOUR_KEY',{api_host:'https://us.i.posthog.com'}))}(document,window.posthog||[]);
|
|
58
|
-
</script>
|
|
59
55
|
</body>
|
|
60
56
|
</html>
|
package/web/server.js
CHANGED
|
@@ -696,11 +696,67 @@ function buildRecommendReason(result, answers) {
|
|
|
696
696
|
return `${bits.join(' Β· ') || 'Strong catalog fit'} for ${priority.toLowerCase()} priority.`
|
|
697
697
|
}
|
|
698
698
|
|
|
699
|
+
// βββ Same-origin guard (mirrors src/core/router-daemon.js) ββββββββββββββββββ
|
|
700
|
+
|
|
701
|
+
// π Loopback hostnames are always trusted: the dashboard is a local tool.
|
|
702
|
+
function isLoopbackHostname(hostname) {
|
|
703
|
+
if (!hostname) return false
|
|
704
|
+
const h = hostname.toLowerCase()
|
|
705
|
+
return h === 'localhost' || h === '127.0.0.1' || h === '[::1]' || h === '::1' || h.endsWith('.localhost')
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// π FCM_ALLOWED_ORIGINS: comma-separated extra origins allowed to call the
|
|
709
|
+
// π dashboard, e.g. "http://mybox:3333,http://10.0.0.5:3333". Used for LAN
|
|
710
|
+
// π setups when the server is bound to a non-loopback FCM_HOST.
|
|
711
|
+
function getAllowedOrigins() {
|
|
712
|
+
return (process.env.FCM_ALLOWED_ORIGINS || '')
|
|
713
|
+
.split(',')
|
|
714
|
+
.map((s) => s.trim())
|
|
715
|
+
.filter(Boolean)
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// π Origin guard applied to every route: only loopback origins, requests
|
|
719
|
+
// π whose Origin matches the Host the browser is already talking to, or
|
|
720
|
+
// π explicitly allowed origins may call the dashboard. Non-browser clients
|
|
721
|
+
// π (curl, TUI, proxies) send no Origin/Referer and pass. An `Origin: null`
|
|
722
|
+
// π (sandboxed iframe) is rejected unless a loopback Referer accompanies it.
|
|
723
|
+
function isRequestOriginAllowed(req) {
|
|
724
|
+
const origin = req.headers.origin
|
|
725
|
+
const referer = req.headers.referer || req.headers.referrer
|
|
726
|
+
const hasOrigin = typeof origin === 'string' && origin.length > 0
|
|
727
|
+
const candidates = []
|
|
728
|
+
if (hasOrigin && origin !== 'null') {
|
|
729
|
+
candidates.push(origin)
|
|
730
|
+
} else if (hasOrigin && origin === 'null') {
|
|
731
|
+
if (typeof referer !== 'string' || !referer) return false
|
|
732
|
+
candidates.push(referer)
|
|
733
|
+
} else if (typeof referer === 'string' && referer) {
|
|
734
|
+
candidates.push(referer)
|
|
735
|
+
}
|
|
736
|
+
if (candidates.length === 0) return true
|
|
737
|
+
|
|
738
|
+
const hostHeader = typeof req.headers.host === 'string' ? req.headers.host.toLowerCase() : ''
|
|
739
|
+
for (const c of candidates) {
|
|
740
|
+
try {
|
|
741
|
+
const parsed = new URL(c)
|
|
742
|
+
if (isLoopbackHostname(parsed.hostname)) return true
|
|
743
|
+
if (hostHeader && parsed.host.toLowerCase() === hostHeader) return true
|
|
744
|
+
if (getAllowedOrigins().includes(c)) return true
|
|
745
|
+
} catch {
|
|
746
|
+
return false
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
return false
|
|
750
|
+
}
|
|
751
|
+
|
|
699
752
|
async function handleRequest(req, res) {
|
|
753
|
+
// π Same-origin guard first: never let a hostile page read this server.
|
|
754
|
+
if (!isRequestOriginAllowed(req)) {
|
|
755
|
+
res.writeHead(403, { 'Content-Type': 'text/plain; charset=utf-8' })
|
|
756
|
+
res.end('Forbidden cross-origin request')
|
|
757
|
+
return
|
|
758
|
+
}
|
|
700
759
|
res.setHeader('X-FCM-Server', SERVER_SIGNATURE)
|
|
701
|
-
res.setHeader('Access-Control-Allow-Origin', '*')
|
|
702
|
-
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
|
|
703
|
-
res.setHeader('Access-Control-Allow-Headers', 'Content-Type')
|
|
704
760
|
|
|
705
761
|
if (req.method === 'OPTIONS') {
|
|
706
762
|
res.writeHead(204)
|
|
@@ -738,9 +794,10 @@ async function handleRequest(req, res) {
|
|
|
738
794
|
return
|
|
739
795
|
}
|
|
740
796
|
|
|
741
|
-
// π Single-provider key
|
|
742
|
-
// π
|
|
743
|
-
// π
|
|
797
|
+
// π Single-provider key endpoint. Returns the MASKED key plus a hasKey
|
|
798
|
+
// π flag only: the raw key never leaves the config file over HTTP. The
|
|
799
|
+
// π M2 /api/key/:provider/test route is matched above and uses a stricter
|
|
800
|
+
// π regex (one segment + /test) so the two routes coexist cleanly.
|
|
744
801
|
const keyMatch = url.pathname.match(/^\/api\/key\/([^/]+)$/)
|
|
745
802
|
if (keyMatch) {
|
|
746
803
|
const providerKey = decodeURIComponent(keyMatch[1])
|
|
@@ -748,7 +805,8 @@ async function handleRequest(req, res) {
|
|
|
748
805
|
sendJson(res, 404, { error: 'Unknown provider' })
|
|
749
806
|
return
|
|
750
807
|
}
|
|
751
|
-
|
|
808
|
+
const rawKey = getApiKey(config, providerKey)
|
|
809
|
+
sendJson(res, 200, { key: rawKey ? maskApiKey(rawKey) : null, hasKey: Boolean(rawKey) })
|
|
752
810
|
return
|
|
753
811
|
}
|
|
754
812
|
|
|
@@ -1886,12 +1944,17 @@ async function handleRequest(req, res) {
|
|
|
1886
1944
|
}
|
|
1887
1945
|
|
|
1888
1946
|
function checkPortInUse(port) {
|
|
1889
|
-
|
|
1947
|
+
// π Probe both the wildcard and the loopback address: macOS SO_REUSEADDR
|
|
1948
|
+
// π lets a wildcard probe succeed even when 127.0.0.1:port is taken (the
|
|
1949
|
+
// π dashboard binds loopback by default), and a loopback probe succeed
|
|
1950
|
+
// π when another app owns the wildcard bind. Either hit means "in use".
|
|
1951
|
+
const probe = (host) => new Promise((resolve) => {
|
|
1890
1952
|
const s = createServer()
|
|
1891
1953
|
s.once('error', (err) => { if (err.code === 'EADDRINUSE') resolve(true); else resolve(false) })
|
|
1892
1954
|
s.once('listening', () => { s.close(); resolve(false) })
|
|
1893
|
-
s.listen(port)
|
|
1955
|
+
s.listen(port, host)
|
|
1894
1956
|
})
|
|
1957
|
+
return Promise.all([probe(undefined), probe('127.0.0.1')]).then(([wildcard, loopback]) => wildcard || loopback)
|
|
1895
1958
|
}
|
|
1896
1959
|
|
|
1897
1960
|
export async function inspectExistingWebServer(port) {
|
|
@@ -1982,20 +2045,31 @@ export async function startWebServer(port = DEFAULT_WEB_PORT, { open = true, sta
|
|
|
1982
2045
|
socket.on('models:refresh', () => socket.emit('models:update', getModelsPayload()))
|
|
1983
2046
|
})
|
|
1984
2047
|
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
2048
|
+
// π Bind loopback by default so the dashboard is never reachable from the
|
|
2049
|
+
// π LAN by accident; set FCM_HOST (e.g. "0.0.0.0" for Docker) to override.
|
|
2050
|
+
// π Await the actual bind so callers can hit the port as soon as this
|
|
2051
|
+
// π resolves (tests read server.address() right after startup).
|
|
2052
|
+
const bindHost = process.env.FCM_HOST || '127.0.0.1'
|
|
2053
|
+
await new Promise((resolve, reject) => {
|
|
2054
|
+
const onError = (err) => reject(err)
|
|
2055
|
+
server.once('error', onError)
|
|
2056
|
+
server.listen(resolvedPort, bindHost, () => {
|
|
2057
|
+
server.off('error', onError)
|
|
2058
|
+
console.log()
|
|
2059
|
+
console.log(' β‘ free-coding-models Web Dashboard')
|
|
2060
|
+
console.log(` π ${url}`)
|
|
2061
|
+
console.log(` π Monitoring ${results.filter((r) => !r.cliOnly).length} models across ${Object.keys(sources).length} providers`)
|
|
2062
|
+
console.log()
|
|
2063
|
+
console.log(' Press Ctrl+C to stop')
|
|
2064
|
+
console.log()
|
|
2065
|
+
if (startPingLoop && !pingLoopTimer) {
|
|
2066
|
+
runtime.lastPingTime = Date.now()
|
|
2067
|
+
runtime.nextPingAt = runtime.lastPingTime + runtime.activePingInterval
|
|
2068
|
+
startPingCycle()
|
|
2069
|
+
}
|
|
2070
|
+
if (open) openBrowser(url)
|
|
2071
|
+
resolve()
|
|
2072
|
+
})
|
|
1999
2073
|
})
|
|
2000
2074
|
|
|
2001
2075
|
server.on('close', () => {
|
package/web/vite.config.js
CHANGED
|
@@ -13,6 +13,14 @@ const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url),
|
|
|
13
13
|
export default defineConfig({
|
|
14
14
|
plugins: [react()],
|
|
15
15
|
root: 'web',
|
|
16
|
+
// π Force a single React instance across the app + every dependency graph
|
|
17
|
+
// π (react-table, react-virtual, tablerβ¦), same as the root vite.config.js
|
|
18
|
+
// π used for production builds. Without this, pnpm's nested peer-dep
|
|
19
|
+
// π resolution can make Vite's esbuild pre-bundler ship two copies of
|
|
20
|
+
// π React β "Invalid hook call" crashes at runtime in dev too.
|
|
21
|
+
resolve: {
|
|
22
|
+
dedupe: ['react', 'react-dom'],
|
|
23
|
+
},
|
|
16
24
|
build: {
|
|
17
25
|
outDir: 'dist',
|
|
18
26
|
emptyOutDir: true,
|