diff --git a/packages/frontend/admin/src/setup.ts b/packages/frontend/admin/src/setup.ts
index dd29d6bab..c89c68027 100644
--- a/packages/frontend/admin/src/setup.ts
+++ b/packages/frontend/admin/src/setup.ts
@@ -1,3 +1 @@
-import { setupBrowser } from '@affine/core/bootstrap';
-
-await setupBrowser();
+import '@affine/core/bootstrap/browser';
diff --git a/packages/frontend/apps/electron/renderer/setup.ts b/packages/frontend/apps/electron/renderer/setup.ts
index 9970cae66..2b5935864 100644
--- a/packages/frontend/apps/electron/renderer/setup.ts
+++ b/packages/frontend/apps/electron/renderer/setup.ts
@@ -1,6 +1,3 @@
+import '@affine/core/bootstrap/electron';
import '@affine/component/theme';
import './global.css';
-
-import { setupElectron } from '@affine/core/bootstrap';
-
-setupElectron();
diff --git a/packages/frontend/apps/electron/renderer/shell/setup.ts b/packages/frontend/apps/electron/renderer/shell/setup.ts
index 5e76f1368..fe6c84404 100644
--- a/packages/frontend/apps/electron/renderer/shell/setup.ts
+++ b/packages/frontend/apps/electron/renderer/shell/setup.ts
@@ -1,6 +1,3 @@
+import '@affine/core/bootstrap/electron';
import '@affine/component/theme';
import '../global.css';
-
-import { setupElectron } from '@affine/core/bootstrap';
-
-setupElectron();
diff --git a/packages/frontend/apps/mobile/src/setup.ts b/packages/frontend/apps/mobile/src/setup.ts
index a2a83a681..27bd3f467 100644
--- a/packages/frontend/apps/mobile/src/setup.ts
+++ b/packages/frontend/apps/mobile/src/setup.ts
@@ -1,6 +1,3 @@
+import '@affine/core/bootstrap/browser';
import '@affine/component/theme';
import '@affine/core/mobile/styles/mobile.css';
-
-import { setupBrowser } from '@affine/core/bootstrap';
-
-await setupBrowser();
diff --git a/packages/frontend/apps/web/src/setup.ts b/packages/frontend/apps/web/src/setup.ts
index 472648fde..d7348bf50 100644
--- a/packages/frontend/apps/web/src/setup.ts
+++ b/packages/frontend/apps/web/src/setup.ts
@@ -1,5 +1,2 @@
+import '@affine/core/bootstrap/browser';
import '@affine/component/theme';
-
-import { setupBrowser } from '@affine/core/bootstrap';
-
-await setupBrowser();
diff --git a/packages/frontend/core/src/bootstrap/app.ts b/packages/frontend/core/src/bootstrap/app.ts
deleted file mode 100644
index 2a1ead92b..000000000
--- a/packages/frontend/core/src/bootstrap/app.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-import '@affine/env/constant';
-import '../types/types.d.ts';
-
-export { setupGlobal as setupEnvironment } from '@affine/env/global';
diff --git a/packages/frontend/core/src/bootstrap/browser.ts b/packages/frontend/core/src/bootstrap/browser.ts
new file mode 100644
index 000000000..cac814e59
--- /dev/null
+++ b/packages/frontend/core/src/bootstrap/browser.ts
@@ -0,0 +1,5 @@
+// ORDER MATTERS
+import './global-error-handler';
+import './env';
+import './public-path';
+import './polyfill/browser';
diff --git a/packages/frontend/core/src/bootstrap/electron.ts b/packages/frontend/core/src/bootstrap/electron.ts
new file mode 100644
index 000000000..f0391ff46
--- /dev/null
+++ b/packages/frontend/core/src/bootstrap/electron.ts
@@ -0,0 +1,4 @@
+// ORDER MATTERS
+import './env';
+import './public-path';
+import './polyfill/electron';
diff --git a/packages/frontend/core/src/bootstrap/env.ts b/packages/frontend/core/src/bootstrap/env.ts
new file mode 100644
index 000000000..3d5837888
--- /dev/null
+++ b/packages/frontend/core/src/bootstrap/env.ts
@@ -0,0 +1,6 @@
+import '@affine/env/constant';
+import '../types/types.d.ts';
+
+import { setupGlobal } from '@affine/env/global';
+
+setupGlobal();
diff --git a/packages/frontend/core/src/bootstrap/error.png b/packages/frontend/core/src/bootstrap/error.png
new file mode 100644
index 000000000..03327d40e
Binary files /dev/null and b/packages/frontend/core/src/bootstrap/error.png differ
diff --git a/packages/frontend/core/src/bootstrap/global-error-handler.ts b/packages/frontend/core/src/bootstrap/global-error-handler.ts
new file mode 100644
index 000000000..597769e85
--- /dev/null
+++ b/packages/frontend/core/src/bootstrap/global-error-handler.ts
@@ -0,0 +1,81 @@
+/* eslint-disable no-var */
+import errorImg from './error.png';
+
+var errorEl: HTMLDivElement | null = null;
+function showGlobalErrorPage() {
+ if (errorEl) {
+ return;
+ }
+
+ errorEl = document.createElement('div');
+ errorEl.innerHTML = [
+ '',
+ '
',
+ '

',
+ '
',
+ '
Unsupported Environment
',
+ '
',
+ 'It looks like AFFiNE cannot run in this environment.',
+ "Please ensure you are using a supported browser or update your device's operating system to the latest version.",
+ 'If the issue persists, visit our support page for further assistance.',
+ '
',
+ '
',
+ '
',
+ ].join('');
+ errorEl.setAttribute(
+ 'style',
+ 'position:absolute;top:0;left:0;height:100vh;width:100vw;display:flex;flex-direction:column;align-items:center;justify-content:center;background:white;z-index:999;'
+ );
+ document.body.append(errorEl);
+}
+
+function registerGlobalErrorHandler() {
+ function handleGlobalUnrecoverableError(
+ e: ErrorEvent | PromiseRejectionEvent
+ ) {
+ var error =
+ 'error' in e ? e.error : e.reason instanceof Error ? e.reason : null;
+ console.error('unhandled unrecoverable error', error);
+
+ const shouldCache =
+ // syntax error
+ error && error instanceof SyntaxError;
+
+ if (!shouldCache) {
+ return;
+ }
+
+ e.stopImmediatePropagation();
+ showGlobalErrorPage();
+ }
+ if (typeof document !== 'undefined') {
+ globalThis.addEventListener(
+ 'unhandledrejection',
+ handleGlobalUnrecoverableError
+ );
+ globalThis.addEventListener('error', handleGlobalUnrecoverableError);
+ }
+}
+
+function ensureBasicEnvironment() {
+ var globals = ['Promise', 'Map', 'fetch', 'customElements'];
+
+ // eslint-disable-next-line @typescript-eslint/prefer-for-of
+ for (var i = 0; i < globals.length; i++) {
+ if (!(globals[i] in globalThis)) {
+ showGlobalErrorPage();
+ return;
+ }
+ }
+}
+
+registerGlobalErrorHandler();
+ensureBasicEnvironment();
diff --git a/packages/frontend/core/src/bootstrap/index.ts b/packages/frontend/core/src/bootstrap/index.ts
deleted file mode 100644
index e15e37ed8..000000000
--- a/packages/frontend/core/src/bootstrap/index.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import '../types/types.d.ts';
-
-import { setupEnvironment } from './app';
-import { polyfillBrowser, polyfillElectron } from './polyfill';
-
-export function setupElectron() {
- setupEnvironment();
- polyfillElectron();
-}
-
-export async function setupBrowser() {
- setupEnvironment();
- __webpack_public_path__ = environment.publicPath;
- await polyfillBrowser();
-}
diff --git a/packages/frontend/core/src/bootstrap/polyfill/index.ts b/packages/frontend/core/src/bootstrap/polyfill/browser.ts
similarity index 51%
rename from packages/frontend/core/src/bootstrap/polyfill/index.ts
rename to packages/frontend/core/src/bootstrap/polyfill/browser.ts
index 3551f4994..b84ee578d 100644
--- a/packages/frontend/core/src/bootstrap/polyfill/index.ts
+++ b/packages/frontend/core/src/bootstrap/polyfill/browser.ts
@@ -3,13 +3,7 @@ import { polyfillPromise } from './promise-with-resolvers';
import { polyfillEventLoop } from './request-idle-callback';
import { polyfillResizeObserver } from './resize-observer';
-export function polyfillElectron() {
- polyfillResizeObserver();
-}
-
-export async function polyfillBrowser() {
- polyfillResizeObserver();
- polyfillEventLoop();
- await polyfillPromise();
- await polyfillDispose();
-}
+polyfillResizeObserver();
+polyfillEventLoop();
+await polyfillPromise();
+await polyfillDispose();
diff --git a/packages/frontend/core/src/bootstrap/polyfill/electron.ts b/packages/frontend/core/src/bootstrap/polyfill/electron.ts
new file mode 100644
index 000000000..707662820
--- /dev/null
+++ b/packages/frontend/core/src/bootstrap/polyfill/electron.ts
@@ -0,0 +1,3 @@
+import { polyfillResizeObserver } from './resize-observer';
+
+polyfillResizeObserver();
diff --git a/packages/frontend/core/src/bootstrap/public-path.ts b/packages/frontend/core/src/bootstrap/public-path.ts
new file mode 100644
index 000000000..a5b6398c2
--- /dev/null
+++ b/packages/frontend/core/src/bootstrap/public-path.ts
@@ -0,0 +1 @@
+__webpack_public_path__ = environment.publicPath;
diff --git a/tools/cli/src/webpack/cache-group.ts b/tools/cli/src/webpack/cache-group.ts
index 1ec621cf7..032900971 100644
--- a/tools/cli/src/webpack/cache-group.ts
+++ b/tools/cli/src/webpack/cache-group.ts
@@ -5,6 +5,11 @@ function testPackageName(regexp: RegExp): (module: any) => boolean {
// https://hackernoon.com/the-100-correct-way-to-split-your-chunks-with-webpack-f8a9df5b7758
export const productionCacheGroups = {
+ errorHandler: {
+ test: /global-error-handler/,
+ priority: 1000,
+ enforce: true,
+ },
asyncVendor: {
test: /[\\/]node_modules[\\/]/,
name(module: any) {
diff --git a/tools/cli/src/webpack/config.ts b/tools/cli/src/webpack/config.ts
index f81337b58..a4a63c05d 100644
--- a/tools/cli/src/webpack/config.ts
+++ b/tools/cli/src/webpack/config.ts
@@ -56,13 +56,7 @@ const OptimizeOptionOptions: (
minChunks: 1,
maxInitialRequests: Number.MAX_SAFE_INTEGER,
maxAsyncRequests: Number.MAX_SAFE_INTEGER,
- cacheGroups:
- buildFlags.mode === 'production'
- ? productionCacheGroups
- : {
- default: false,
- vendors: false,
- },
+ cacheGroups: productionCacheGroups,
},
});
@@ -404,6 +398,11 @@ export const createConfiguration: (
maxInitialRequests: Infinity,
chunks: 'all',
cacheGroups: {
+ errorHandler: {
+ test: /global-error-handler/,
+ priority: 1000,
+ enforce: true,
+ },
defaultVendors: {
test: `[\\/]node_modules[\\/](?!.*vanilla-extract)`,
priority: -10,