From b5660d33c8db7cba211a15f04c6ec9313ea6360a Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Oct 2026 04:55:21 +0000 Subject: [PATCH 1/3] fix(shared): drop regex lookbehind so the module parses on Safari < 16.4 LONE_SURROGATE_REGEX used a negative lookbehind, which Safari/WebKit only parses from 16.4. unbuild ships regex literals verbatim, so on iOS 15, iOS 16.0-16.3 and older WKWebViews the bundled module failed to load with a SyntaxError before any code ran, breaking browser apps that use the fetch or peer adapters. Use the u-flag class /[\uD800-\uDFFF]/gu instead: in unicode mode a well-formed surrogate pair is a single code point, so only lone surrogates match. Add a repo-level test that fails if any published source file contains a lookbehind, since oxlint has no rule for it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Dws6kfo5SZKrsmMVu1ADve --- packages/shared/src/uri.test.ts | 3 +++ packages/shared/src/uri.ts | 4 ++-- tests/browser-compat.test.ts | 37 +++++++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 tests/browser-compat.test.ts diff --git a/packages/shared/src/uri.test.ts b/packages/shared/src/uri.test.ts index 34fef348..eb41ca05 100644 --- a/packages/shared/src/uri.test.ts +++ b/packages/shared/src/uri.test.ts @@ -17,6 +17,9 @@ describe('safeEncodeURIComponent', () => { expect(safeEncodeURIComponent('\uDC00\uD800')).toBe('%EF%BF%BD%EF%BF%BD') // a well-formed pair next to a lone surrogate is kept intact expect(safeEncodeURIComponent('😀\uD83D')).toBe('%F0%9F%98%80%EF%BF%BD') + expect(safeEncodeURIComponent('\u{1F600}\uDE00')).toBe('%F0%9F%98%80%EF%BF%BD') + expect(safeEncodeURIComponent('\u{10FFFF}\uD800')).toBe('%F4%8F%BF%BF%EF%BF%BD') + expect(safeEncodeURIComponent('\uD800\u{10000}\uDFFF')).toBe('%EF%BF%BD%F0%90%80%80%EF%BF%BD') }) }) diff --git a/packages/shared/src/uri.ts b/packages/shared/src/uri.ts index 7804be01..d16700d2 100644 --- a/packages/shared/src/uri.ts +++ b/packages/shared/src/uri.ts @@ -1,5 +1,5 @@ -const LONE_SURROGATE_REGEX = - /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(? { + const srcDir = join('packages', pkg, 'src') + + return readdirSync(join(ROOT_DIR, srcDir), { recursive: true, encoding: 'utf8' }) + .filter((file) => file.endsWith('.ts') && !/\.(?:test|test-d|bench)\.ts$/.test(file)) + .map((file) => join(srcDir, file)) + }) +} + +describe('published source', () => { + it('lists the source files', () => { + expect(listSourceFiles()).toContain(join('packages', 'shared', 'src', 'uri.ts')) + expect(listSourceFiles()).not.toContain(join('packages', 'shared', 'src', 'uri.test.ts')) + }) + + // unbuild does not down-level regex syntax, and a regex literal Safari can't parse + // (lookbehind before 16.4) is a SyntaxError that stops the whole module from loading + it('has no regex lookbehind', () => { + const matches = listSourceFiles().flatMap((file) => + readFileSync(join(ROOT_DIR, file), 'utf8') + .split('\n') + .flatMap((line, index) => (/\(\?<[=!]/.test(line) ? [`${file}:${index + 1}`] : [])), + ) + + expect(matches).toEqual([]) + }) +}) From f174a103388dabcd8e395e713c5061977aa189c3 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Oct 2026 06:02:38 +0000 Subject: [PATCH 2/3] test: simplify the regex lookbehind guard Fold the two tests into one so the file list proven non-empty is the one that gets scanned, drop the assertion that only tested the helper's own filter, match per file instead of per line, and resolve the repo root with import.meta.dirname. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Dws6kfo5SZKrsmMVu1ADve --- tests/browser-compat.test.ts | 28 ++++++++++------------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/tests/browser-compat.test.ts b/tests/browser-compat.test.ts index a6702778..c16f6417 100644 --- a/tests/browser-compat.test.ts +++ b/tests/browser-compat.test.ts @@ -1,8 +1,7 @@ import { readdirSync, readFileSync } from 'node:fs' import { join } from 'node:path' -import { fileURLToPath } from 'node:url' -const ROOT_DIR = fileURLToPath(new URL('..', import.meta.url)) +const ROOT_DIR = join(import.meta.dirname, '..') /** * Source files that end up in the published builds, relative to the repo root. @@ -17,21 +16,14 @@ function listSourceFiles(): string[] { }) } -describe('published source', () => { - it('lists the source files', () => { - expect(listSourceFiles()).toContain(join('packages', 'shared', 'src', 'uri.ts')) - expect(listSourceFiles()).not.toContain(join('packages', 'shared', 'src', 'uri.test.ts')) - }) - - // unbuild does not down-level regex syntax, and a regex literal Safari can't parse - // (lookbehind before 16.4) is a SyntaxError that stops the whole module from loading - it('has no regex lookbehind', () => { - const matches = listSourceFiles().flatMap((file) => - readFileSync(join(ROOT_DIR, file), 'utf8') - .split('\n') - .flatMap((line, index) => (/\(\?<[=!]/.test(line) ? [`${file}:${index + 1}`] : [])), - ) +// unbuild does not down-level regex syntax, and a regex literal Safari can't parse +// (lookbehind before 16.4) is a SyntaxError that stops the whole module from loading +it('published source has no regex lookbehind', () => { + const files = listSourceFiles() + expect(files).toContain(join('packages', 'shared', 'src', 'uri.ts')) - expect(matches).toEqual([]) - }) + const matches = files.filter((file) => + /\(\?<[=!]/.test(readFileSync(join(ROOT_DIR, file), 'utf8')), + ) + expect(matches).toEqual([]) }) From de35cb9a9275239549dc442dce66b50f2720b6a5 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Oct 2026 07:53:31 +0000 Subject: [PATCH 3/3] test: remove the regex lookbehind guard Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Dws6kfo5SZKrsmMVu1ADve --- tests/browser-compat.test.ts | 29 ----------------------------- 1 file changed, 29 deletions(-) delete mode 100644 tests/browser-compat.test.ts diff --git a/tests/browser-compat.test.ts b/tests/browser-compat.test.ts deleted file mode 100644 index c16f6417..00000000 --- a/tests/browser-compat.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { readdirSync, readFileSync } from 'node:fs' -import { join } from 'node:path' - -const ROOT_DIR = join(import.meta.dirname, '..') - -/** - * Source files that end up in the published builds, relative to the repo root. - */ -function listSourceFiles(): string[] { - return readdirSync(join(ROOT_DIR, 'packages')).flatMap((pkg) => { - const srcDir = join('packages', pkg, 'src') - - return readdirSync(join(ROOT_DIR, srcDir), { recursive: true, encoding: 'utf8' }) - .filter((file) => file.endsWith('.ts') && !/\.(?:test|test-d|bench)\.ts$/.test(file)) - .map((file) => join(srcDir, file)) - }) -} - -// unbuild does not down-level regex syntax, and a regex literal Safari can't parse -// (lookbehind before 16.4) is a SyntaxError that stops the whole module from loading -it('published source has no regex lookbehind', () => { - const files = listSourceFiles() - expect(files).toContain(join('packages', 'shared', 'src', 'uri.ts')) - - const matches = files.filter((file) => - /\(\?<[=!]/.test(readFileSync(join(ROOT_DIR, file), 'utf8')), - ) - expect(matches).toEqual([]) -})