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:
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):
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). optimizationis set as an object withfonts: falsein production. This shorthand collapsesscripts,stylesandfontsto defaults, sostyles.minifyandstyles.inlineCriticalare both enabled by default (truthy) — this matters for finding F4 below.optimization.fonts: falsedisables inline-font optimization (Beasties/Critters does not run on fonts). Critical CSS extraction (inlineCritical) is still on unless explicitly disabled.- No
differentialLoadingfield (correctly absent in v21 — would be ignored anyway). - No
crossOriginfield — module scripts will be emitted withoutcrossoriginattribute by default. sourceMap.hidden: falsein production — sourcemaps are uploaded then stripped viaremove-sourcemaps.shat container start. Not a compat issue but unusual.- No
definefield (no static replacement forprocess.envetc). - 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, nobrowserslistinpackage.json, nobrowsersinangular.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 throwsSyntaxErrorat 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
.browserslistrcatsrc/services/web/.browserslistrcwith explicit broader targets: Then verify withpnpm 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, noat()) - Severity: critical
- Symptom: Bundle-time
SyntaxErrorbecause TypeScript does not down-emit syntax oncetargetis reached — these features are passed through. A singleObject.hasOwn(x, 'k')from any dependency will throwTypeError: Object.hasOwn is not a functionon 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.atshipped in Safari 15.4. - Fix: Lower target to
ES2020(safe baseline for Safari 14+) or keep ES2022 and addcore-js/stableandcore-js/proposals/...imports inpolyfills.tscoveringObject.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), orrequestIdleCallback(cb)throwsReferenceError/TypeErrorat 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 inpolyfills.ts. Suggested baseline: 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
@supportsblocks 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 defaultsstyles.inlineCritical: true. There is no opt-out. - Fix: Set
optimization.styles.inlineCritical: falsein the production configuration:Verify by diffing the produced"optimization": { "fonts": false, "styles": { "inlineCritical": false, "minify": true }, "scripts": true }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,
#privatenot), Firefox ≤ 89 - Severity: major
- Symptom: With
target: ES2022, even ifuseDefineForClassFields: falsechooses constructor-assignment semantics, the syntaxclass Foo { x = 1; }still emits asclass Foo { x = 1; }. Old Safari rejects at parse time. - Cause: The flag controls semantics, not syntax emission. Lowering
targetis the only way to down-compile the syntax. - Fix: Couples with F2 — drop target to ES2020. Then
useDefineForClassFieldsactually 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/.brandsevaluateundefinedon 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 useif ('userAgentData' in navigator)or optional chaining; if any site assumes presence, fix it. Consider replacing withnavigator.userAgentparsing orbowser. - 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.htmlwas never enhanced for iOS. - Fix: Add:
- Effort: trivial
F9. index.html loads three remote font stylesheets without <link rel="preconnect"> or crossorigin¶
- 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 inindex.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-webpackto@angular/build:applicationleft files behind. - Fix: Delete
custom-webpack.config.js. Rewrite thepolyfills.tsheader comment to reflect the actual (post-fix) polyfill set. - Effort: trivial
Recommended target updates¶
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¶
(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.)