mirror of
https://github.com/n8n-io/n8n.git
synced 2026-09-24 23:22:38 +08:00
fix(editor): Auto-reload editor on stale Vite chunk preload error (#31747)
Co-authored-by: n8n-cat-bot[bot] <n8n-cat-bot[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
n8n-cat-bot[bot]
Claude Opus 4.7
parent
5fa988104b
commit
2f3ebb620c
@@ -38,6 +38,16 @@ describe('beforeSend', () => {
|
||||
expect(beforeSend(event, hint)).toBeNull();
|
||||
});
|
||||
|
||||
it.each([
|
||||
'Failed to fetch dynamically imported module: https://example.com/assets/RunDataSearch-abc.js',
|
||||
'error loading dynamically imported module: https://example.com/assets/RunDataSearch-abc.js',
|
||||
'Importing a module script failed.',
|
||||
])('should return null for stale-chunk preload TypeError: %s', (message) => {
|
||||
const event = createErrorEvent();
|
||||
const hint = { originalException: new TypeError(message) };
|
||||
expect(beforeSend(event, hint)).toBeNull();
|
||||
});
|
||||
|
||||
it('should return event when originalException does not match any ignoredErrors', () => {
|
||||
const event = createErrorEvent();
|
||||
const hint = { originalException: new Error('Some other error') };
|
||||
|
||||
@@ -14,6 +14,12 @@ const ignoredErrors = [
|
||||
{ instanceof: RangeError, message: /Invalid change range \d+ to \d+/ },
|
||||
{ instanceof: RangeError, message: /Selection points outside of document$/ },
|
||||
{ instanceof: Error, message: /ResizeObserver/ },
|
||||
// Stale-chunk preload errors after a deploy — the page auto-reloads to recover.
|
||||
// Known blind spot: a broken deploy (chunk missing for everyone) produces the same
|
||||
// message and is also suppressed; rely on CI / smoke checks for that signal.
|
||||
{ instanceof: TypeError, message: /Failed to fetch dynamically imported module/ },
|
||||
{ instanceof: TypeError, message: /error loading dynamically imported module/ },
|
||||
{ instanceof: TypeError, message: /Importing a module script failed/ },
|
||||
] as const;
|
||||
|
||||
type SentryConfig = {
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { registerVitePreloadErrorHandler } from '@/app/plugins/vitePreloadError';
|
||||
|
||||
const STORAGE_KEY = 'n8n:vite-preload-reloaded-at';
|
||||
|
||||
function dispatchPreloadError() {
|
||||
const event = new Event('vite:preloadError', { cancelable: true });
|
||||
window.dispatchEvent(event);
|
||||
return event;
|
||||
}
|
||||
|
||||
describe('registerVitePreloadErrorHandler', () => {
|
||||
let reloadSpy: ReturnType<typeof vi.fn>;
|
||||
let originalLocation: Location;
|
||||
let unregister: () => void;
|
||||
|
||||
beforeEach(() => {
|
||||
sessionStorage.clear();
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date('2026-01-01T00:00:00Z'));
|
||||
|
||||
originalLocation = window.location;
|
||||
reloadSpy = vi.fn();
|
||||
Object.defineProperty(window, 'location', {
|
||||
configurable: true,
|
||||
value: { ...originalLocation, reload: reloadSpy },
|
||||
});
|
||||
|
||||
unregister = registerVitePreloadErrorHandler();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
unregister();
|
||||
vi.useRealTimers();
|
||||
Object.defineProperty(window, 'location', {
|
||||
configurable: true,
|
||||
value: originalLocation,
|
||||
});
|
||||
sessionStorage.clear();
|
||||
});
|
||||
|
||||
it('reloads and stores a timestamp on the first preload error', () => {
|
||||
const event = dispatchPreloadError();
|
||||
|
||||
expect(reloadSpy).toHaveBeenCalledTimes(1);
|
||||
expect(event.defaultPrevented).toBe(true);
|
||||
expect(Number(sessionStorage.getItem(STORAGE_KEY))).toBe(Date.now());
|
||||
});
|
||||
|
||||
it('does not reload on a second preload error within the throttle window', () => {
|
||||
dispatchPreloadError();
|
||||
vi.advanceTimersByTime(5_000);
|
||||
const event = dispatchPreloadError();
|
||||
|
||||
expect(reloadSpy).toHaveBeenCalledTimes(1);
|
||||
expect(event.defaultPrevented).toBe(false);
|
||||
});
|
||||
|
||||
it('reloads again after the throttle window has elapsed', () => {
|
||||
dispatchPreloadError();
|
||||
vi.advanceTimersByTime(10_001);
|
||||
dispatchPreloadError();
|
||||
|
||||
expect(reloadSpy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
const VITE_PRELOAD_RELOAD_TS = 'n8n:vite-preload-reloaded-at';
|
||||
const RELOAD_THROTTLE_MS = 10_000;
|
||||
|
||||
export function registerVitePreloadErrorHandler(): () => void {
|
||||
const handler = (event: Event) => {
|
||||
const last = Number(sessionStorage.getItem(VITE_PRELOAD_RELOAD_TS) ?? 0);
|
||||
if (Date.now() - last < RELOAD_THROTTLE_MS) return;
|
||||
sessionStorage.setItem(VITE_PRELOAD_RELOAD_TS, String(Date.now()));
|
||||
event.preventDefault();
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
window.addEventListener('vite:preloadError', handler);
|
||||
return () => window.removeEventListener('vite:preloadError', handler);
|
||||
}
|
||||
@@ -27,11 +27,14 @@ import { GlobalDirectivesPlugin } from '@/app/plugins/directives';
|
||||
import { createPinia, PiniaVuePlugin } from 'pinia';
|
||||
import { ChartJSPlugin } from '@/app/plugins/chartjs';
|
||||
import { SentryPlugin } from '@/app/plugins/sentry';
|
||||
import { registerVitePreloadErrorHandler } from '@/app/plugins/vitePreloadError';
|
||||
import { registerModuleRoutes } from '@/app/moduleInitializer/moduleInitializer';
|
||||
import { installRenderTracker } from '@/app/dev/render-tracker';
|
||||
|
||||
import type { VueScanOptions } from 'z-vue-scan';
|
||||
|
||||
registerVitePreloadErrorHandler();
|
||||
|
||||
const pinia = createPinia();
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
Reference in New Issue
Block a user