mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-26 13:19:34 +07:00
🐛 fix: skew plugin (#12669)
* fix: skew plugin Signed-off-by: Innei <tukon479@gmail.com> * refactor(vite): enhance vercelSkewProtection to handle static imports and improve coverage - Added handling for static import/export declarations to ensure correct deployment links. - Updated coverage documentation to reflect new handling for static imports and additional cases. - Adjusted comment numbering for clarity in the processing steps. Signed-off-by: Innei <tukon479@gmail.com> * fix: dev proxy Signed-off-by: Innei <tukon479@gmail.com> * refactor(AssistantGroup): streamline contentId handling in GroupMessage component - Simplified the logic for determining contentId by directly using lastAssistantMsg?.id. - Moved the creation and generation state checks to follow the contentId assignment for better clarity. Signed-off-by: Innei <tukon479@gmail.com> * ♻️ refactor: remove chunk error reload retry, keep notification only Made-with: Cursor * fix: inject Signed-off-by: Innei <tukon479@gmail.com> --------- Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
160
plugins/vite/vercelSkewProtection.ts
Normal file
160
plugins/vite/vercelSkewProtection.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
/**
|
||||
* Vite plugin: Vercel Skew Protection
|
||||
*
|
||||
* Injects ?dpl=<VERCEL_DEPLOYMENT_ID> into built asset URLs so Vercel Edge
|
||||
* routes requests to the correct deployment, preventing "Failed to fetch
|
||||
* dynamically imported module" errors caused by version skew.
|
||||
*
|
||||
* Coverage:
|
||||
* 1. static import/export — renderChunk (post-enforce, after Vite internals)
|
||||
* 2. dynamic import() — renderChunk
|
||||
* 3. CSS url() — generateBundle
|
||||
* 4. HTML <script>/<link> — transformIndexHtml
|
||||
* 5. Web Worker URLs — renderChunk
|
||||
* 6. __vite__mapDeps — renderChunk (preload deps array)
|
||||
*
|
||||
* Why enforce:'post'?
|
||||
* Vite's buildImportAnalysisPlugin rewrites dynamic imports in its own
|
||||
* renderChunk hook. Using renderDynamicImport is ineffective because Vite
|
||||
* regenerates the import() expressions afterward. By running post-enforce,
|
||||
* our renderChunk sees the FINAL chunk code and can reliably modify it.
|
||||
*
|
||||
* Prerequisite: Enable Skew Protection in Vercel Dashboard.
|
||||
*/
|
||||
|
||||
import type { Plugin } from 'vite';
|
||||
|
||||
export function vercelSkewProtection(deploymentId?: string): Plugin {
|
||||
const id = deploymentId || process.env.VERCEL_DEPLOYMENT_ID || '';
|
||||
let enabled = false;
|
||||
const dplParam = `dpl=${id}`;
|
||||
|
||||
function appendDpl(url: string): string {
|
||||
if (url.includes('dpl=')) return url;
|
||||
return url + (url.includes('?') ? '&' : '?') + dplParam;
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'vite-plugin-vercel-skew-protection',
|
||||
enforce: 'post',
|
||||
|
||||
// ── 0. Only active in production builds with a valid deployment ID ──
|
||||
config(_, env) {
|
||||
enabled = env.command === 'build' && id.length > 0;
|
||||
if (!enabled) return;
|
||||
return {
|
||||
define: {
|
||||
'import.meta.env.VITE_VERCEL_DEPLOYMENT_ID': JSON.stringify(id),
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
// ── 1+2. Rewrite JS chunks (runs AFTER Vite's internal plugins) ──
|
||||
renderChunk(code) {
|
||||
if (!enabled) return;
|
||||
|
||||
let modified = code;
|
||||
let changed = false;
|
||||
|
||||
// 1a. Rewrite static import/export declarations
|
||||
//
|
||||
// After Vite processing, static imports/exports between chunks look like:
|
||||
// import { x } from "./chunk-hash.js";
|
||||
// import "./chunk-hash.js";
|
||||
// export { foo } from "./chunk-hash.js";
|
||||
// We append ?dpl= to the specifier so browsers request the correct deployment.
|
||||
const staticImportRe =
|
||||
/((?:import|export)\s*(?:\{[^}]*\}\s*from\s*)?["'])(\.\.?\/[^"']+)(["'])/g;
|
||||
modified = modified.replaceAll(
|
||||
staticImportRe,
|
||||
(_, before: string, path: string, after: string) => {
|
||||
changed = true;
|
||||
return before + appendDpl(path) + after;
|
||||
},
|
||||
);
|
||||
|
||||
// 1b. Rewrite dynamic import() with relative paths
|
||||
//
|
||||
// After Vite processing, dynamic imports look like:
|
||||
// import("./chunk-hash.js")
|
||||
// We append ?dpl= directly to the specifier.
|
||||
const importRe = /(import\(["'])(\.\.?\/[^"']+)(["']\))/g;
|
||||
modified = modified.replaceAll(importRe, (_, before: string, path: string, after: string) => {
|
||||
changed = true;
|
||||
return before + appendDpl(path) + after;
|
||||
});
|
||||
|
||||
// 1c. Rewrite __vite__mapDeps dep array
|
||||
//
|
||||
// Vite 7 format:
|
||||
// const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=[
|
||||
// "assets/index-BN2fWmdo.js","vendor/vendor-motion-xxx.js",...
|
||||
// ])))=>i.map(i=>d[i]);
|
||||
const mapDepsRe = /(m\.f\|\|\(m\.f=\[)([\s\S]*?)(\]\))/g;
|
||||
modified = modified.replaceAll(
|
||||
mapDepsRe,
|
||||
(_, before: string, paths: string, after: string) => {
|
||||
const rewritten = paths.replaceAll(/"([^"]+)"/g, (_m: string, p: string) => {
|
||||
return `"${appendDpl(p)}"`;
|
||||
});
|
||||
changed = true;
|
||||
return before + rewritten + after;
|
||||
},
|
||||
);
|
||||
|
||||
// 1d. Rewrite Worker URLs
|
||||
// new Worker(new URL("./worker-hash.js", import.meta.url))
|
||||
const workerRe = /(new\s+(?:Shared)?Worker\(\s*new\s+URL\(\s*")([^"]+)(")/g;
|
||||
modified = modified.replaceAll(workerRe, (_, before: string, path: string, after: string) => {
|
||||
changed = true;
|
||||
return before + appendDpl(path) + after;
|
||||
});
|
||||
|
||||
if (changed) return { code: modified, map: null };
|
||||
},
|
||||
|
||||
// ── 2. Rewrite CSS url() references ──
|
||||
generateBundle(_, bundle) {
|
||||
if (!enabled) return;
|
||||
for (const [fileName, asset] of Object.entries(bundle)) {
|
||||
if (
|
||||
asset.type !== 'asset' ||
|
||||
!fileName.endsWith('.css') ||
|
||||
typeof asset.source !== 'string'
|
||||
)
|
||||
continue;
|
||||
|
||||
// Match url("...") or url('...') or url(...) — avoid data:/blob:/#
|
||||
const urlRe = /url\(["'](?!data:|#|blob:)([^"']+)["']\)|url\((?!data:|#|blob:)([^)]+)\)/g;
|
||||
asset.source = asset.source.replaceAll(urlRe, (match, quoted: string, bare: string) => {
|
||||
const url = quoted || bare;
|
||||
if (!url || url.includes('dpl=')) return match;
|
||||
return match.replace(url, appendDpl(url));
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// ── 3. Rewrite HTML <script src> and <link href> ──
|
||||
transformIndexHtml(html) {
|
||||
if (!enabled) return;
|
||||
// <script ... src="...">
|
||||
html = html.replaceAll(
|
||||
/(<script[^>]+src=["'])([^"']+)(["'][^>]*>)/g,
|
||||
(match, before, src, after) => {
|
||||
if (src.startsWith('data:') || src.includes('dpl=')) return match;
|
||||
return `${before}${appendDpl(src)}${after}`;
|
||||
},
|
||||
);
|
||||
// <link rel="stylesheet|modulepreload" href="...">
|
||||
html = html.replaceAll(
|
||||
/(<link[^>]+href=["'])([^"']+)(["'][^>]*>)/g,
|
||||
(match, before, href, after) => {
|
||||
if (href.startsWith('data:') || href.includes('dpl=')) return match;
|
||||
if (!match.includes('stylesheet') && !match.includes('modulepreload')) return match;
|
||||
return `${before}${appendDpl(href)}${after}`;
|
||||
},
|
||||
);
|
||||
return html;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -8,6 +8,17 @@
|
||||
globalThis['__DEBUG_PROXY__'] = true;
|
||||
|
||||
// --- 1. 解析 debug host ---
|
||||
function isValidDevHost(urlStr) {
|
||||
try {
|
||||
var u = new URL(urlStr);
|
||||
if (['http:', 'https:'].indexOf(u.protocol) === -1) return false;
|
||||
var h = u.hostname.toLowerCase();
|
||||
return h === 'localhost' || h === '127.0.0.1' || h === '[::1]';
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
var searchParams = new URLSearchParams(window.location.search);
|
||||
var debugHost = searchParams.get('debug-host');
|
||||
|
||||
@@ -16,31 +27,38 @@
|
||||
}
|
||||
|
||||
var storedHost = sessionStorage.getItem('debug-host');
|
||||
var host = debugHost || storedHost || 'http://localhost:9876';
|
||||
|
||||
if (debugHost) {
|
||||
var host = 'http://localhost:9876';
|
||||
if (debugHost && isValidDevHost(debugHost)) {
|
||||
host = debugHost;
|
||||
sessionStorage.setItem('debug-host', debugHost);
|
||||
} else if (storedHost && isValidDevHost(storedHost)) {
|
||||
host = storedHost;
|
||||
}
|
||||
|
||||
// --- 2. Worker 跨域补丁(必须在任何模块加载前注入)---
|
||||
var workerPatch = document.createElement('script');
|
||||
workerPatch.textContent = '(function(){' +
|
||||
'var O=globalThis.Worker;' +
|
||||
'globalThis.Worker=function(u,o){' +
|
||||
'var h=typeof u==="string"?u:u instanceof URL?u.href:"";' +
|
||||
'if(h.startsWith("'+host+'")){' +
|
||||
'var b=new Blob(["import \\\\""+h+"\\\\";"],{type:"application/javascript"});' +
|
||||
'return new O(URL.createObjectURL(b),Object.assign({},o,{type:"module"}));' +
|
||||
'}return new O(u,o)};' +
|
||||
'globalThis.Worker.prototype=O.prototype;' +
|
||||
'})();';
|
||||
workerPatch.textContent = `(function () {
|
||||
var O = globalThis.Worker;
|
||||
globalThis.Worker = function (u, o) {
|
||||
var h = typeof u === "string" ? u : u instanceof URL ? u.href : "";
|
||||
if (h.startsWith(${JSON.stringify(host)})) {
|
||||
var code = 'import "' + h + '";';
|
||||
var b = new Blob([code], { type: "application/javascript" });
|
||||
return new O(URL.createObjectURL(b), Object.assign({}, o, { type: "module" }));
|
||||
}
|
||||
return new O(u, o);
|
||||
};
|
||||
globalThis.Worker.prototype = O.prototype;
|
||||
})();`;
|
||||
document.head.insertBefore(workerPatch, document.head.firstChild);
|
||||
|
||||
// --- 3. 注入 React Refresh runtime(HMR 前置条件)---
|
||||
var refreshScript = document.createElement('script');
|
||||
refreshScript.type = 'module';
|
||||
refreshScript.textContent =
|
||||
'import RefreshRuntime from "' + host + '/@react-refresh";' +
|
||||
'import RefreshRuntime from "' +
|
||||
host +
|
||||
'/@react-refresh";' +
|
||||
'RefreshRuntime.injectIntoGlobalHook(window);' +
|
||||
'window.$RefreshReg$ = () => {};' +
|
||||
'window.$RefreshSig$ = () => (type) => type;' +
|
||||
@@ -54,11 +72,11 @@
|
||||
) || 'en-US';
|
||||
|
||||
var configReady = fetch('/spa/' + locale + '/chat')
|
||||
.then(function(res) { return res.text(); })
|
||||
.then(function(html) {
|
||||
var match = html.match(
|
||||
/window\.__SERVER_CONFIG__\s*=\s*(\{[\s\S]*?\});/,
|
||||
);
|
||||
.then(function (res) {
|
||||
return res.text();
|
||||
})
|
||||
.then(function (html) {
|
||||
var match = html.match(/window\.__SERVER_CONFIG__\s*=\s*(\{[\s\S]*?\});/);
|
||||
if (match) {
|
||||
var configScript = document.createElement('script');
|
||||
configScript.textContent = 'window.__SERVER_CONFIG__ = ' + match[1] + ';';
|
||||
@@ -68,33 +86,35 @@
|
||||
|
||||
// --- 5. Fetch dev server HTML 并注入 ---
|
||||
var devHtmlReady = fetch(host)
|
||||
.then(function(res) { return res.text(); })
|
||||
.then(function(html) {
|
||||
.then(function (res) {
|
||||
return res.text();
|
||||
})
|
||||
.then(function (html) {
|
||||
var parser = new DOMParser();
|
||||
var doc = parser.parseFromString(html, 'text/html');
|
||||
|
||||
var scripts = doc.querySelectorAll('script');
|
||||
scripts.forEach(function(s) { s.remove(); });
|
||||
scripts.forEach(function (s) {
|
||||
s.remove();
|
||||
});
|
||||
|
||||
doc.head.querySelectorAll('meta').forEach(function(meta) {
|
||||
doc.head.querySelectorAll('meta').forEach(function (meta) {
|
||||
document.head.append(meta);
|
||||
});
|
||||
|
||||
doc.head
|
||||
.querySelectorAll('style, link[rel="stylesheet"]')
|
||||
.forEach(function(el) {
|
||||
if (el.tagName === 'LINK') {
|
||||
var href = el.getAttribute('href');
|
||||
if (href && href.startsWith('/')) {
|
||||
el.setAttribute('href', new URL(href, host).toString());
|
||||
}
|
||||
doc.head.querySelectorAll('style, link[rel="stylesheet"]').forEach(function (el) {
|
||||
if (el.tagName === 'LINK') {
|
||||
var href = el.getAttribute('href');
|
||||
if (href && href.startsWith('/')) {
|
||||
el.setAttribute('href', new URL(href, host).toString());
|
||||
}
|
||||
document.head.append(el);
|
||||
});
|
||||
}
|
||||
document.head.append(el);
|
||||
});
|
||||
|
||||
document.body.innerHTML = doc.body.innerHTML;
|
||||
|
||||
scripts.forEach(function(script) {
|
||||
scripts.forEach(function (script) {
|
||||
var s = document.createElement('script');
|
||||
s.type = 'module';
|
||||
if (script.crossOrigin) s.crossOrigin = script.crossOrigin;
|
||||
@@ -107,7 +127,9 @@
|
||||
} else if (script.textContent) {
|
||||
s.textContent = script.textContent.replace(
|
||||
/from\s+["'](\/[@\w].*?)["']/g,
|
||||
function(_, p) { return 'from "' + new URL(p, host).toString() + '"'; },
|
||||
function (_, p) {
|
||||
return 'from "' + new URL(p, host).toString() + '"';
|
||||
},
|
||||
);
|
||||
} else {
|
||||
return;
|
||||
@@ -117,11 +139,8 @@
|
||||
});
|
||||
});
|
||||
|
||||
Promise.all([configReady, devHtmlReady]).then(function() {
|
||||
console.log(
|
||||
'%c[Debug Proxy] Loaded from ' + host,
|
||||
'color: #52c41a; font-weight: bold;',
|
||||
);
|
||||
Promise.all([configReady, devHtmlReady]).then(function () {
|
||||
console.log('%c[Debug Proxy] Loaded from ' + host, 'color: #52c41a; font-weight: bold;');
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
|
||||
@@ -5,6 +5,8 @@ import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
import utc from 'dayjs/plugin/utc';
|
||||
import { enableMapSet } from 'immer';
|
||||
|
||||
import { isChunkLoadError, notifyChunkError } from '@/utils/chunkError';
|
||||
|
||||
enableMapSet();
|
||||
|
||||
// Dayjs plugins - extend once at app init to avoid duplicate extensions in components
|
||||
@@ -12,3 +14,18 @@ dayjs.extend(relativeTime);
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(isToday);
|
||||
dayjs.extend(isYesterday);
|
||||
|
||||
// Global fallback: catch async chunk-load failures that escape Error Boundaries
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('vite:preloadError', (event) => {
|
||||
event.preventDefault();
|
||||
notifyChunkError();
|
||||
});
|
||||
|
||||
window.addEventListener('unhandledrejection', (event) => {
|
||||
if (isChunkLoadError(event.reason)) {
|
||||
event.preventDefault();
|
||||
notifyChunkError();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import '../initialize';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { RouterProvider } from 'react-router-dom';
|
||||
|
||||
import BootErrorBoundary from '@/components/BootErrorBoundary';
|
||||
import SPAGlobalProvider from '@/layout/SPAGlobalProvider';
|
||||
import { createAppRouter } from '@/utils/router';
|
||||
|
||||
@@ -17,7 +18,9 @@ const basename =
|
||||
const router = createAppRouter(desktopRoutes, { basename });
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<SPAGlobalProvider>
|
||||
<RouterProvider router={router} />
|
||||
</SPAGlobalProvider>,
|
||||
<BootErrorBoundary>
|
||||
<SPAGlobalProvider>
|
||||
<RouterProvider router={router} />
|
||||
</SPAGlobalProvider>
|
||||
</BootErrorBoundary>,
|
||||
);
|
||||
|
||||
31
src/utils/chunkError.ts
Normal file
31
src/utils/chunkError.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { toast } from '@lobehub/ui';
|
||||
|
||||
const CHUNK_ERROR_PATTERNS = [
|
||||
'Failed to fetch dynamically imported module', // Chrome / Vite
|
||||
'error loading dynamically imported module', // Firefox
|
||||
'Importing a module script failed', // Safari
|
||||
'Failed to load module script', // Safari variant
|
||||
'Loading chunk', // Webpack
|
||||
'Loading CSS chunk', // Webpack CSS
|
||||
'ChunkLoadError', // Webpack error name
|
||||
];
|
||||
|
||||
/**
|
||||
* Detect whether an error (or its message) was caused by a failed chunk / dynamic import.
|
||||
*/
|
||||
export function isChunkLoadError(error: unknown): boolean {
|
||||
if (!error) return false;
|
||||
|
||||
const name = (error as Error).name ?? '';
|
||||
const message = (error as Error).message ?? String(error);
|
||||
const combined = `${name} ${message}`;
|
||||
|
||||
return CHUNK_ERROR_PATTERNS.some((p) => combined.includes(p));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show user notification for chunk load error (no reload).
|
||||
*/
|
||||
export function notifyChunkError(): void {
|
||||
toast.info('Web app has been updated so it needs to be reloaded.');
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { toast } from '@lobehub/ui';
|
||||
import { type ComponentType, type ReactElement } from 'react';
|
||||
import { createElement, lazy, memo, Suspense, useCallback, useEffect, useRef } from 'react';
|
||||
import { createElement, lazy, memo, Suspense, useCallback, useEffect } from 'react';
|
||||
import type { RouteObject } from 'react-router-dom';
|
||||
import {
|
||||
createBrowserRouter,
|
||||
@@ -16,6 +15,21 @@ import BusinessGlobalProvider from '@/business/client/BusinessGlobalProvider';
|
||||
import ErrorCapture from '@/components/Error';
|
||||
import Loading from '@/components/Loading/BrandTextLoading';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
import { isChunkLoadError, notifyChunkError } from '@/utils/chunkError';
|
||||
|
||||
async function importModule<T>(importFn: () => Promise<T>): Promise<T> {
|
||||
return importFn();
|
||||
}
|
||||
|
||||
function resolveLazyModule<P>(module: { default: ComponentType<P> } | ComponentType<P>) {
|
||||
if (typeof module === 'function') {
|
||||
return { default: module };
|
||||
}
|
||||
if ('default' in module) {
|
||||
return module as { default: ComponentType<P> };
|
||||
}
|
||||
return { default: module as unknown as ComponentType<P> };
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to create a dynamic page element directly for router configuration
|
||||
@@ -34,15 +48,8 @@ export function dynamicElement<P = NonNullable<unknown>>(
|
||||
debugId?: string,
|
||||
): ReactElement {
|
||||
const LazyComponent = lazy(async () => {
|
||||
// eslint-disable-next-line @next/next/no-assign-module-variable
|
||||
const module = await importFn();
|
||||
if (typeof module === 'function') {
|
||||
return { default: module };
|
||||
}
|
||||
if ('default' in module) {
|
||||
return module as { default: ComponentType<P> };
|
||||
}
|
||||
return { default: module as unknown as ComponentType<P> };
|
||||
const mod = await importModule(importFn);
|
||||
return resolveLazyModule(mod);
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
@@ -63,15 +70,8 @@ export function dynamicLayout<P = NonNullable<unknown>>(
|
||||
debugId?: string,
|
||||
): ReactElement {
|
||||
const LazyComponent = lazy(async () => {
|
||||
// eslint-disable-next-line @next/next/no-assign-module-variable
|
||||
const module = await importFn();
|
||||
if (typeof module === 'function') {
|
||||
return { default: module };
|
||||
}
|
||||
if ('default' in module) {
|
||||
return module as { default: ComponentType<P> };
|
||||
}
|
||||
return { default: module as unknown as ComponentType<P> };
|
||||
const mod = await importModule(importFn);
|
||||
return resolveLazyModule(mod);
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
@@ -102,35 +102,13 @@ export interface ErrorBoundaryProps {
|
||||
|
||||
export const ErrorBoundary = ({ resetPath }: ErrorBoundaryProps) => {
|
||||
const error = useRouteError() as Error;
|
||||
const reloadRef = useRef(false);
|
||||
const navigate = useNavigate();
|
||||
const reset = useCallback(() => {
|
||||
navigate(resetPath);
|
||||
}, [navigate, resetPath]);
|
||||
let message = '';
|
||||
|
||||
if (error instanceof Error) {
|
||||
message = error.message;
|
||||
} else if (typeof error === 'string') {
|
||||
message = error;
|
||||
} else if (error && typeof error === 'object' && 'statusText' in error) {
|
||||
const statusText = (error as { statusText?: unknown }).statusText;
|
||||
if (typeof statusText === 'string') message = statusText;
|
||||
}
|
||||
|
||||
if (
|
||||
typeof window !== 'undefined' &&
|
||||
message?.startsWith('Failed to fetch dynamically imported module') &&
|
||||
window.sessionStorage.getItem('reload') !== '1'
|
||||
) {
|
||||
if (reloadRef.current) return null;
|
||||
|
||||
toast.info('Web app has been updated so it needs to be reloaded.');
|
||||
window.sessionStorage.setItem('reload', '1');
|
||||
window.location.reload();
|
||||
reloadRef.current = true;
|
||||
|
||||
return null;
|
||||
if (typeof window !== 'undefined' && isChunkLoadError(error)) {
|
||||
notifyChunkError();
|
||||
}
|
||||
|
||||
return createElement(ErrorCapture, { error, reset });
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
sharedRendererPlugins,
|
||||
sharedRollupOutput,
|
||||
} from './plugins/vite/sharedRendererConfig';
|
||||
import { vercelSkewProtection } from './plugins/vite/vercelSkewProtection';
|
||||
|
||||
const isMobile = process.env.MOBILE === 'true';
|
||||
const mode = process.env.NODE_ENV === 'production' ? 'production' : 'development';
|
||||
@@ -32,6 +33,7 @@ export default defineConfig({
|
||||
define: sharedRendererDefine({ isMobile, isElectron: false }),
|
||||
optimizeDeps: sharedOptimizeDeps,
|
||||
plugins: [
|
||||
vercelSkewProtection(),
|
||||
viteEnvRestartKeys(['APP_URL']),
|
||||
...sharedRendererPlugins({ platform }),
|
||||
|
||||
|
||||
Reference in New Issue
Block a user