Skip to content

Build / Config Audit — non-Chromium Browser Compatibility

Temporary audit document — supports the planning report at ../non-chromium-browser-compatibility-analysis.md. Delete once findings are tracked in tickets.

Summary

  • Total findings: 11
  • Severity breakdown: critical 2 / major 6 / minor 3

Current configuration snapshot

browserslist

No .browserslistrc exists. No browserslist field in package.json. No browsers field in angular.json.

The project ships with zero explicit browser targets. It therefore inherits the Angular 21 / @angular/build default, which (as of Angular 17+) is approximately:

last 2 Chrome version
last 1 Firefox version
last 2 Edge major versions
last 2 Safari major versions
last 2 iOS major versions
Firefox ESR

This is a narrow evergreen default (no > 0.5%, no not dead). It does not include the long Safari tail (Safari 14/15 still common on macOS Big Sur / Monterey and locked-down iOS devices) or any Firefox ESR users beyond the single newest ESR.

There is also no production vs development split, so the dev build and prod build use the same target — which is a small positive (no surprise differential drops).

tsconfig target

tsconfig.json line 41:

"target": "ES2022"

tsconfig.build.json extends tsconfig.json and does not override target, so the production build emits ES2022.

ES2022 features (Object.hasOwn, class fields with #private, at(), top-level await semantics, error cause, Array.prototype.findLast/findLastIndex, regex /d flag) are not safe across the implicit Angular browserslist tail (Safari 15 partial, Safari 14 missing several). The TypeScript compiler will down-emit nothing for these because the target is ES2022 — they are passed through to the browser as-is.

useDefineForClassFields is set to false (line 45). This is the older Angular semantics — class fields are defined via assignment in the constructor instead of Object.defineProperty. This is generally compatible but conflicts with the ES2022 target: the class field syntax itself (post-Stage 4) is still emitted at ES2022, so older Safari (<14) chokes on the syntax even with the legacy semantics flag.

lib is not specified — defaults to the target (ES2022 + DOM, DOM.Iterable). No explicit lib trimming.

types is ["node", "user-agent-data-types"]user-agent-data-types is fine (declarations only), but it suggests the app uses navigator.userAgentData which is Chromium-only (not in Safari, not in Firefox).

polyfills.ts contents

src/polyfills.ts (58 lines, mostly comments):

import 'zone.js';

That is the entirety of the runtime polyfill. No core-js imports, no web-streams-polyfill, no intersection-observer, no resize-observer-polyfill, no whatwg-fetch, no url-polyfill, no requestIdleCallback shim, no structuredClone shim.

The header comment still references "Safari >= 10, Chrome >= 55, Edge >= 13, iOS 10" — this comment is stale; it predates the Angular 12+ default-browserslist tightening, but more importantly the file no longer contains any of the polyfills that comment claims to support. Anything beyond zone.js is assumed native.

autoprefixer / PostCSS

No postcss.config.js, no postcss.config.cjs, no tailwind.config.js, no tailwind.config.ts, no .postcssrc* anywhere in src/services/web/.

The @angular/build:application builder (esbuild-backed in v17+) ships with autoprefixer baked in and reads from the resolved browserslist. With no browserslist file present, autoprefixer falls back to the Angular default targets above, which means:

  • -webkit- prefixes: still emitted for Safari ≥ N-2 features (good for current Safari)
  • -moz- prefixes: emitted only for last 1 Firefox (questionable for Firefox ESR users on older builds)
  • No -ms- prefixes (IE/Edge legacy gone — fine)

There is no Tailwind. CSS comes from SCSS (Material + custom), Bootstrap CDN (@import url(...font-awesome...)), and Handsontable's handsontable.full.css. Handsontable 14.x targets evergreen browsers; their CSS may use unprefixed :has(), aspect-ratio, container queries — which autoprefixer cannot polyfill, only prefix.

angular.json relevant excerpt

"build": {
  "builder": "@angular/build:application",
  "options": {
    "polyfills": ["src/polyfills.ts"],
    "tsConfig": "tsconfig.build.json",
    ...
  },
  "configurations": {
    "production": {
      "optimization": { "fonts": false },
      "outputHashing": "all",
      "sourceMap": { "hidden": false, ... },
      "extractLicenses": true
    }
  }
}

Notable points:

  • Builder is @angular/build:application (Angular 17+ esbuild builder, correct for v21).
  • optimization is set as an object with fonts: false in production. This shorthand collapses scripts, styles and fonts to defaults, so styles.minify and styles.inlineCritical are both enabled by default (truthy) — this matters for finding F4 below.
  • optimization.fonts: false disables inline-font optimization (Beasties/Critters does not run on fonts). Critical CSS extraction (inlineCritical) is still on unless explicitly disabled.
  • No differentialLoading field (correctly absent in v21 — would be ignored anyway).
  • No crossOrigin field — module scripts will be emitted without crossorigin attribute by default.
  • sourceMap.hidden: false in production — sourcemaps are uploaded then stripped via remove-sourcemaps.sh at container start. Not a compat issue but unusual.
  • No define field (no static replacement for process.env etc).
  • No browser target specified at the angular.json level.

There is a leftover custom-webpack.config.js in the project root with a stale CopyPlugin configuration. This file is not referenced by angular.json (the builder is @angular/build:application, not @angular-builders/custom-webpack:browser), so it is dead code — but its presence is misleading and suggests previous webpack tooling.

Findings

F1. No browserslist configuration — implicit Angular default excludes Safari 14/15 and Firefox ESR tail

  • File: entire workspace (no .browserslistrc, no browserslist in package.json, no browsers in angular.json)
  • Browsers affected: Safari ≤ 15 (any user on macOS ≤ Monterey, iOS ≤ 15), Firefox ESR users on N-1 ESR or older
  • Severity: critical
  • Symptom: Autoprefixer omits prefixes those browsers still need; modern CSS (:has(), container-type, aspect-ratio, :where() cascade, logical properties) is emitted unprefixed and unguarded; ES2022 syntax passes straight through. Page partially renders or throws SyntaxError at script-parse time on older Safari.
  • Cause: Angular's default browserslist is "last 2 Safari major" only. With Safari 18 current at audit time, that means Safari 17/18 only — Safari 16 already excluded.
  • Fix: Add .browserslistrc at src/services/web/.browserslistrc with explicit broader targets:
    > 0.5%
    last 2 versions
    Firefox ESR
    not dead
    not IE 11
    Safari >= 14
    iOS >= 14
    
    Then verify with pnpm exec browserslist.
  • Effort: trivial

F2. tsconfig.json target: ES2022 emits syntax that pre-Safari-15.4 cannot parse

  • File: src/services/web/tsconfig.json:41
  • Browsers affected: Safari ≤ 15.3 (full), Safari 15.4–15.6 (partial — class static blocks, error cause), Firefox ≤ 92 (private fields), Firefox ESR 91 (no Object.hasOwn, no at())
  • Severity: critical
  • Symptom: Bundle-time SyntaxError because TypeScript does not down-emit syntax once target is reached — these features are passed through. A single Object.hasOwn(x, 'k') from any dependency will throw TypeError: Object.hasOwn is not a function on Safari 14.
  • Cause: ES2022 is the maximally aggressive Angular-supported target; combined with no polyfill imports, runtime APIs added in ES2022 are unguarded. Even String.prototype.at shipped in Safari 15.4.
  • Fix: Lower target to ES2020 (safe baseline for Safari 14+) or keep ES2022 and add core-js/stable and core-js/proposals/... imports in polyfills.ts covering Object.hasOwn, Array.prototype.at, Array.prototype.findLast, Error.cause, structuredClone. The first option is lower-risk.
  • Effort: low (target change) / medium (core-js audit)

F3. polyfills.ts is effectively empty — only zone.js

  • File: src/services/web/src/polyfills.ts:1-58
  • Browsers affected: Safari ≤ 15 (structuredClone, Object.hasOwn), Firefox ≤ 95 (Array.at), iOS Safari ≤ 16 (requestIdleCallback), all non-Chromium (navigator.userAgentData)
  • Severity: major
  • Symptom: Code that calls structuredClone({}), arr.at(-1), obj.hasOwn(k), or requestIdleCallback(cb) throws ReferenceError/TypeError at runtime in older non-Chromium browsers.
  • Cause: When Angular's CLI bumped from webpack to esbuild they removed implicit polyfills; this file was never updated. The header comment still reads "Safari >= 10" — wholly inaccurate.
  • Fix: Add explicit imports based on what the code actually uses. Start by grepping the source for structuredClone, \.at(, Object.hasOwn, requestIdleCallback, IntersectionObserver, ResizeObserver. For each found, import the polyfill in polyfills.ts. Suggested baseline:
    import 'core-js/actual/structured-clone';
    import 'core-js/actual/object/has-own';
    import 'core-js/actual/array/at';
    import 'core-js/actual/array/find-last';
    import 'core-js/actual/array/find-last-index';
    import 'requestidlecallback-polyfill';
    
    Also delete the stale "Safari >= 10" header comment and replace with a current statement of what is and is not polyfilled.
  • Effort: medium

F4. optimization shorthand in production leaves inlineCritical on — Beasties may strip @supports/prefixed fallbacks

  • File: src/services/web/angular.json:60-62
  • Browsers affected: Safari (any version) — Beasties/Critters has historically dropped @supports blocks and conditional fallback rules during critical-CSS extraction.
  • Severity: major
  • Symptom: Layout breaks on Safari when SCSS uses @supports (display: grid) { ... } or vendor-prefixed fallbacks — Beasties extracts only the "winning" rule for the critical viewport and drops the others.
  • Cause: optimization: { fonts: false } is shorthand that defaults styles.inlineCritical: true. There is no opt-out.
  • Fix: Set optimization.styles.inlineCritical: false in the production configuration:
    "optimization": {
      "fonts": false,
      "styles": { "inlineCritical": false, "minify": true },
      "scripts": true
    }
    
    Verify by diffing the produced index.html <style> block before/after.
  • Effort: trivial

F5. useDefineForClassFields: false masks Safari class-field timing bugs that re-appear at ES2022

  • File: src/services/web/tsconfig.json:45
  • Browsers affected: Safari 14 (no native class fields at all), Safari 15.0–15.3 (partial — public fields ok, #private not), Firefox ≤ 89
  • Severity: major
  • Symptom: With target: ES2022, even if useDefineForClassFields: false chooses constructor-assignment semantics, the syntax class Foo { x = 1; } still emits as class Foo { x = 1; }. Old Safari rejects at parse time.
  • Cause: The flag controls semantics, not syntax emission. Lowering target is the only way to down-compile the syntax.
  • Fix: Couples with F2 — drop target to ES2020. Then useDefineForClassFields actually does what the comment implies.
  • Effort: low (paired with F2)

F6. tsconfig.json includes user-agent-data-types — strongly hints code reads navigator.userAgentData, which is Chromium-only

  • File: src/services/web/tsconfig.json:28
  • Browsers affected: Safari (all), Firefox (all)
  • Severity: major
  • Symptom: Code paths gated on navigator.userAgentData.platform/.brands evaluate undefined on Safari/Firefox, taking unintended branches (e.g., "OS unknown") or throwing if accessed without optional chaining.
  • Cause: The type package only adds compile-time types; it does not polyfill. Source code that uses these types is still expected to feature-detect at runtime — see source-code audit for actual call sites.
  • Fix: Audit usages (grep -rn "userAgentData" src/) — every site must use if ('userAgentData' in navigator) or optional chaining; if any site assumes presence, fix it. Consider replacing with navigator.userAgent parsing or bowser.
  • Effort: medium (depends on call-site count — see source-code audit)

F7. optimization.styles.minify (default true) on autoprefixed CSS via lightning-css can strip vendor-prefix duplicates

  • File: src/services/web/angular.json:60-62 (implicit default)
  • Browsers affected: Safari (when relying on -webkit- prefix), Firefox legacy
  • Severity: minor
  • Symptom: A stylesheet with transform: ...; -webkit-transform: ...; may have one stripped if lightning-css decides they're redundant for the (over-narrow) browserslist.
  • Cause: Angular 17+ uses lightning-css for CSS minification; it consults browserslist and removes prefixes deemed unnecessary. With the implicit-narrow browserslist (F1), it removes prefixes that Safari 15 still wants.
  • Fix: Resolved by F1 (broader browserslist tells lightning-css to keep prefixes). No separate action needed once F1 is in.
  • Effort: trivial (subsumed by F1)

F8. index.html has no <meta name="theme-color">, no apple-mobile-web-app-*, no iOS PWA hints

  • File: src/services/web/src/index.html:16-32
  • Browsers affected: iOS Safari (visual chrome), iOS PWA mode
  • Severity: minor
  • Symptom: iOS Safari renders white address bar instead of branded color; "Add to Home Screen" produces an unbranded entry. Not a compat-break, but degraded UX.
  • Cause: Original Angular CLI starter index.html was never enhanced for iOS.
  • Fix: Add:
    <meta name="theme-color" content="#ffffff">
    <meta name="apple-mobile-web-app-capable" content="yes">
    <meta name="apple-mobile-web-app-status-bar-style" content="default">
    <link rel="apple-touch-icon" href="/assets/images/apple-touch-icon.png">
    
  • Effort: trivial
  • File: src/services/web/src/index.html:20-31
  • Browsers affected: Safari (font loading more sensitive to CORS), Firefox (occasional FOIT delay)
  • Severity: minor
  • Symptom: Slower first paint, occasional invisible-text flash on Safari due to missing preconnect handshake; Sentry may report network-related font-load errors disproportionately on non-Chromium.
  • Cause: No <link rel="preconnect" href="https://fonts.googleapis.com" crossorigin> and no <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>.
  • Fix: Prepend preconnect links above the stylesheet links. Pure performance/UX, not a correctness fix.
  • Effort: trivial

F10. Bootstrap CDN @import url(...) inside styles.scss — hard runtime dependency on third-party CDN

  • File: src/services/web/src/global-styles/styles.scss:8
  • Browsers affected: all (but Safari ITP / Firefox ETP can block third-party requests in stricter privacy modes)
  • Severity: minor
  • Symptom: Font Awesome icons render as missing-glyph boxes when MaxCDN is blocked by tracking-protection extensions or by Safari Cross-Site Tracking Prevention. More common in Firefox/Safari than Chrome.
  • Cause: Stylesheet @import url('https://maxcdn.bootstrapcdn.com/...') issues an extra render-blocking request that some users block.
  • Fix: Self-host Font Awesome (pnpm add @fortawesome/fontawesome-free, import its CSS locally), or migrate to Material Symbols (already loaded in index.html) and remove this @import.
  • Effort: low

F11. Stale custom-webpack.config.js and outdated polyfills.ts header comment — config drift signal

  • File: src/services/web/custom-webpack.config.js:1-11, src/services/web/src/polyfills.ts:11-14
  • Browsers affected: indirect — risk of someone trusting the stale "Safari >= 10" comment when reasoning about compat
  • Severity: minor
  • Symptom: Future maintainers add code thinking Safari 10 polyfills are present; debugging session ends in confusion.
  • Cause: Migration from @angular-builders/custom-webpack to @angular/build:application left files behind.
  • Fix: Delete custom-webpack.config.js. Rewrite the polyfills.ts header comment to reflect the actual (post-fix) polyfill set.
  • Effort: trivial

Suggested .browserslistrc (create at src/services/web/.browserslistrc)

# Production targets — adjust based on analytics
> 0.5%
last 2 versions
Firefox ESR
Safari >= 14
iOS >= 14
not dead
not IE 11
not op_mini all

Suggested tsconfig.json change

"target": "ES2020",
"useDefineForClassFields": false,

(ES2020 covers Safari 14+, Firefox ESR, Edge 80+. ES2021 needs Safari 15.4+. ES2022 needs Safari 16+ — too aggressive given the existing user base assumption.)

Suggested polyfills.ts body

// Polyfills for ES features beyond the TS target / for non-Chromium browsers.
import 'core-js/actual/structured-clone';
import 'core-js/actual/object/has-own';
import 'core-js/actual/array/at';
import 'core-js/actual/array/find-last';
import 'core-js/actual/array/find-last-index';
import 'core-js/actual/string/at';

// requestIdleCallback is missing in Safari ≤ 16
import 'requestidlecallback-polyfill';

// ResizeObserver / IntersectionObserver are present everywhere we target,
// but uncomment if user analytics show iOS < 14 traffic:
// import 'intersection-observer';
// import '@juggle/resize-observer';

// Angular framework requirement.
import 'zone.js';

(Adjust based on actual source-code audit findings — only add polyfills for APIs the codebase actually uses.)

Suggested angular.json production-block change

"production": {
  "budgets": [...],
  "fileReplacements": [...],
  "optimization": {
    "fonts": false,
    "styles": { "inlineCritical": false, "minify": true },
    "scripts": true
  },
  "outputHashing": "all",
  "sourceMap": { ... },
  "extractLicenses": true
}