From e869a4d3653f1b1ec58e5f4a0a7fe132f1403e1d Mon Sep 17 00:00:00 2001 From: Cheney <970320820@qq.com> Date: Sat, 9 May 2026 13:55:50 +0800 Subject: [PATCH 1/2] fix(web): restore compatibility with Chromium 83 (Debian 10) Publish page dropdowns (namespace/visibility) failed to open on older Chromium because Vite 6 defaults build target to chrome87 and some bundled deps call runtime APIs absent in Chrome 83 (replaceAll, .at, hasOwn). Lower esbuild/vite target to chrome83, add browserslist, drop ??= in bootstrap, and inject runtime polyfills before main loads. --- web/.browserslistrc | 4 ++ web/src/bootstrap.ts | 19 +++++---- web/src/legacy-polyfills.ts | 78 +++++++++++++++++++++++++++++++++++++ web/vite.config.ts | 14 +++++++ 4 files changed, 107 insertions(+), 8 deletions(-) create mode 100644 web/.browserslistrc create mode 100644 web/src/legacy-polyfills.ts diff --git a/web/.browserslistrc b/web/.browserslistrc new file mode 100644 index 00000000..fdca1742 --- /dev/null +++ b/web/.browserslistrc @@ -0,0 +1,4 @@ +Chrome >= 83 +Edge >= 83 +Firefox >= 78 +Safari >= 14 diff --git a/web/src/bootstrap.ts b/web/src/bootstrap.ts index a8ac6927..203d7ebb 100644 --- a/web/src/bootstrap.ts +++ b/web/src/bootstrap.ts @@ -4,6 +4,7 @@ * Deployments inject `/runtime-config.js` at startup, and this file guarantees the app sees either * that config or a safe fallback object before importing the main entry. */ +import './legacy-polyfills' async function loadRuntimeConfig() { await new Promise((resolve, reject) => { const script = document.createElement('script') @@ -16,14 +17,16 @@ async function loadRuntimeConfig() { } function ensureRuntimeConfigFallback() { - window.__SKILLHUB_RUNTIME_CONFIG__ ??= { - apiBaseUrl: '', - appBaseUrl: '', - authDirectEnabled: 'false', - authDirectProvider: '', - authSessionBootstrapEnabled: 'false', - authSessionBootstrapProvider: '', - authSessionBootstrapAuto: 'false', + if (!window.__SKILLHUB_RUNTIME_CONFIG__) { + window.__SKILLHUB_RUNTIME_CONFIG__ = { + apiBaseUrl: '', + appBaseUrl: '', + authDirectEnabled: 'false', + authDirectProvider: '', + authSessionBootstrapEnabled: 'false', + authSessionBootstrapProvider: '', + authSessionBootstrapAuto: 'false', + } } } diff --git a/web/src/legacy-polyfills.ts b/web/src/legacy-polyfills.ts new file mode 100644 index 00000000..16112303 --- /dev/null +++ b/web/src/legacy-polyfills.ts @@ -0,0 +1,78 @@ +/** + * Polyfills for older browsers (e.g. Chromium 83 on Debian 10) that lack a few + * ES2021+ runtime methods used by bundled dependencies. esbuild only transpiles + * syntax, not runtime APIs, so we patch the prototypes here before any other + * module runs. + * + * TypeScript's lib target is ES2020, so the methods we patch are referenced via + * string keys / loose casts to avoid compile-time type errors. + */ + +type AnyStringReplacer = (substring: string, ...args: unknown[]) => string + +const StringProto = String.prototype as unknown as Record +const ArrayProto = Array.prototype as unknown as Record +const ObjectCtor = Object as unknown as Record + +if (typeof StringProto.replaceAll !== 'function') { + Object.defineProperty(String.prototype, 'replaceAll', { + configurable: true, + writable: true, + value: function replaceAll( + this: string, + search: string | RegExp, + replacement: string | AnyStringReplacer, + ): string { + if (search instanceof RegExp) { + if (!search.flags.includes('g')) { + throw new TypeError('String.prototype.replaceAll called with a non-global RegExp argument') + } + return this.replace(search, replacement as string) + } + const needle = String(search) + if (needle === '') { + return this.replace(new RegExp('', 'g'), replacement as string) + } + const escaped = needle.replace(/[-\\^$*+?.()|[\]{}]/g, '\\$&') + return this.replace(new RegExp(escaped, 'g'), replacement as string) + }, + }) +} + +if (typeof ArrayProto.at !== 'function') { + Object.defineProperty(Array.prototype, 'at', { + configurable: true, + writable: true, + value: function at(this: unknown[], index: number) { + const len = this.length + const i = Math.trunc(index) || 0 + const resolved = i < 0 ? len + i : i + if (resolved < 0 || resolved >= len) return undefined + return this[resolved] + }, + }) +} + +if (typeof StringProto.at !== 'function') { + Object.defineProperty(String.prototype, 'at', { + configurable: true, + writable: true, + value: function at(this: string, index: number) { + const len = this.length + const i = Math.trunc(index) || 0 + const resolved = i < 0 ? len + i : i + if (resolved < 0 || resolved >= len) return undefined + return this.charAt(resolved) + }, + }) +} + +if (typeof ObjectCtor.hasOwn !== 'function') { + Object.defineProperty(Object, 'hasOwn', { + configurable: true, + writable: true, + value: function hasOwn(target: object, property: PropertyKey) { + return Object.prototype.hasOwnProperty.call(target, property) + }, + }) +} diff --git a/web/vite.config.ts b/web/vite.config.ts index aff7ad67..2fae8e82 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -2,6 +2,8 @@ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import path from 'path' +const LEGACY_BROWSER_TARGETS = ['chrome83', 'edge83', 'firefox78', 'safari14'] + export default defineConfig({ plugins: [react()], resolve: { @@ -9,6 +11,18 @@ export default defineConfig({ '@': path.resolve(__dirname, './src'), }, }, + build: { + target: LEGACY_BROWSER_TARGETS, + cssTarget: LEGACY_BROWSER_TARGETS, + }, + esbuild: { + target: LEGACY_BROWSER_TARGETS, + }, + optimizeDeps: { + esbuildOptions: { + target: LEGACY_BROWSER_TARGETS, + }, + }, test: { exclude: ['**/node_modules/**', '**/e2e/**'], }, From 81f7e3943f76887dd85ef26c994db6f3e54805ec Mon Sep 17 00:00:00 2001 From: Cheney <970320820@qq.com> Date: Sat, 9 May 2026 13:55:57 +0800 Subject: [PATCH 2/2] fix(web): scope legacy browser target to production build only The top-level esbuild.target also applied to dev/test transforms, which broke vitest suites that use top-level await (Chromium 83 / ES2020 does not support it). Only build.target and optimizeDeps.esbuildOptions.target need the legacy target; remove the global esbuild.target override. --- web/vite.config.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/web/vite.config.ts b/web/vite.config.ts index 2fae8e82..631fd1c0 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -15,9 +15,6 @@ export default defineConfig({ target: LEGACY_BROWSER_TARGETS, cssTarget: LEGACY_BROWSER_TARGETS, }, - esbuild: { - target: LEGACY_BROWSER_TARGETS, - }, optimizeDeps: { esbuildOptions: { target: LEGACY_BROWSER_TARGETS,