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..631fd1c0 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,15 @@ export default defineConfig({ '@': path.resolve(__dirname, './src'), }, }, + build: { + target: LEGACY_BROWSER_TARGETS, + cssTarget: LEGACY_BROWSER_TARGETS, + }, + optimizeDeps: { + esbuildOptions: { + target: LEGACY_BROWSER_TARGETS, + }, + }, test: { exclude: ['**/node_modules/**', '**/e2e/**'], },