From c6c7d684a128e9e845978df144400072c9622f28 Mon Sep 17 00:00:00 2001 From: Matt Hillsdon Date: Thu, 17 Sep 2026 09:28:56 +0100 Subject: [PATCH 1/2] Polyfill Array.prototype.at for browsers below the support floor marked 18 calls `.at` while parsing, which is ES2022 and so needs Chrome 92, Safari 15.4 or Firefox 90. Our floor is chrome90/firefox88/safari14.1/ios14.5 (BUILD_TARGETS in vite.config.ts, mirrored in browserslist), and esbuild lowers syntax but never built-ins, so Safari and iOS 14.1-15.3 users get a TypeError from the lexer. The error boundary catches it, so the whole docs panel is replaced by the error page rather than the markdown degrading. Reported once from the wild against v3.1.13 (PYTHON-EDITOR-NEXT-3V7) from a browser below the floor, but the same call fails on Safari versions we do claim to support. tsconfig's ES2021 lib already stops our own source calling `.at`; this is only needed because the call is in a dependency. Prime candidate for removing on next floor bump. --- src/index.tsx | 2 ++ src/polyfills.test.ts | 39 +++++++++++++++++++++++++++++++++++++++ src/polyfills.ts | 28 ++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+) create mode 100644 src/polyfills.test.ts create mode 100644 src/polyfills.ts diff --git a/src/index.tsx b/src/index.tsx index 644d8b364..a9bb6dd0f 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -3,6 +3,8 @@ * * SPDX-License-Identifier: MIT */ +// Before everything else so the shims are in place for module initialisation. +import "./polyfills"; // The CSS entry point: declares the cascade-layer order Panda's PostCSS // plugin fills, and the vendor-layer imports. First so app styles cascade // after it. diff --git a/src/polyfills.test.ts b/src/polyfills.test.ts new file mode 100644 index 000000000..f08702367 --- /dev/null +++ b/src/polyfills.test.ts @@ -0,0 +1,39 @@ +/** + * (c) 2026, Micro:bit Educational Foundation and contributors + * + * SPDX-License-Identifier: MIT + */ +import { describe, expect, it } from "vitest"; +import { at } from "./polyfills"; + +describe("at", () => { + const call = (target: ArrayLike, index: number) => + at.call(target, index) as T | undefined; + + it("indexes from the start", () => { + expect(call(["a", "b", "c"], 0)).toEqual("a"); + expect(call(["a", "b", "c"], 2)).toEqual("c"); + }); + + it("indexes from the end", () => { + expect(call(["a", "b", "c"], -1)).toEqual("c"); + expect(call(["a", "b", "c"], -3)).toEqual("a"); + }); + + it("returns undefined when out of range", () => { + expect(call(["a"], 1)).toBeUndefined(); + expect(call(["a"], -2)).toBeUndefined(); + expect(call([], -1)).toBeUndefined(); + }); + + it("truncates and coerces the index", () => { + expect(call(["a", "b", "c"], 1.9)).toEqual("b"); + expect(call(["a", "b", "c"], -1.9)).toEqual("c"); + expect(call(["a", "b", "c"], NaN)).toEqual("a"); + expect(call(["a", "b", "c"], undefined as unknown as number)).toEqual("a"); + }); + + it("works on strings", () => { + expect(call("abc", -1)).toEqual("c"); + }); +}); diff --git a/src/polyfills.ts b/src/polyfills.ts new file mode 100644 index 000000000..0ad44d5e6 --- /dev/null +++ b/src/polyfills.ts @@ -0,0 +1,28 @@ +/** + * (c) 2026, Micro:bit Educational Foundation and contributors + * + * SPDX-License-Identifier: MIT + */ +// `Array.prototype.at` is ES2022: Chrome 92, Safari 15.4, Firefox 90. That is +// above the support floor in vite.config.ts, and esbuild lowers syntax but +// never built-ins, so anything below the floor needs this shim. marked calls +// it while parsing, which takes out the docs panel and language-server hovers +// on affected browsers rather than degrading quietly. +// Exported for tests: the install below is a no-op in any runtime new +// enough to run them, so the arithmetic would otherwise never be exercised. +export function at(this: ArrayLike, index: number): T | undefined { + const i = Math.trunc(index) || 0; + return this[i < 0 ? this.length + i : i]; +} + +for (const proto of [Array.prototype, String.prototype]) { + if (!(proto as { at?: unknown }).at) { + // Non-enumerable, matching the native method: a plain assignment would + // show up in `for...in` over an array. + Object.defineProperty(proto, "at", { + value: at, + writable: true, + configurable: true, + }); + } +} From c84ced26a7c5127010f618d38e1f33fb41cedf29 Mon Sep 17 00:00:00 2001 From: Matt Hillsdon Date: Thu, 17 Sep 2026 10:05:52 +0100 Subject: [PATCH 2/2] Tweak comments --- src/polyfills.ts | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/polyfills.ts b/src/polyfills.ts index 0ad44d5e6..c9ab87e36 100644 --- a/src/polyfills.ts +++ b/src/polyfills.ts @@ -3,13 +3,8 @@ * * SPDX-License-Identifier: MIT */ -// `Array.prototype.at` is ES2022: Chrome 92, Safari 15.4, Firefox 90. That is -// above the support floor in vite.config.ts, and esbuild lowers syntax but -// never built-ins, so anything below the floor needs this shim. marked calls -// it while parsing, which takes out the docs panel and language-server hovers -// on affected browsers rather than degrading quietly. -// Exported for tests: the install below is a no-op in any runtime new -// enough to run them, so the arithmetic would otherwise never be exercised. +// Remove when we drop Safari < 15.4. +// Also seen helping some v old Chrome but not reasonable to maintain support here past when we drop older Safari. export function at(this: ArrayLike, index: number): T | undefined { const i = Math.trunc(index) || 0; return this[i < 0 ? this.length + i : i]; @@ -17,8 +12,6 @@ export function at(this: ArrayLike, index: number): T | undefined { for (const proto of [Array.prototype, String.prototype]) { if (!(proto as { at?: unknown }).at) { - // Non-enumerable, matching the native method: a plain assignment would - // show up in `for...in` over an array. Object.defineProperty(proto, "at", { value: at, writable: true,