From e4ec5e9f488cc7bc088e5c82dacf4c0788f1972c Mon Sep 17 00:00:00 2001 From: Kris McGinnes Date: Thu, 8 Oct 2026 19:20:54 -0500 Subject: [PATCH] Use native String.prototype.isWellFormed now that it is inside the floor --- REVIEW.md | 2 +- .../connector/openCypher/fragments.test.ts | 3 +- .../src/connector/queryValueError.ts | 4 +- .../src/core/icons/iconSource.ts | 3 +- packages/graph-explorer/src/utils/index.ts | 1 - .../src/utils/isWellFormedString.test.ts | 66 ------------------- .../src/utils/isWellFormedString.ts | 15 ----- packages/graph-explorer/tsconfig.json | 2 +- 8 files changed, 5 insertions(+), 91 deletions(-) delete mode 100644 packages/graph-explorer/src/utils/isWellFormedString.test.ts delete mode 100644 packages/graph-explorer/src/utils/isWellFormedString.ts diff --git a/REVIEW.md b/REVIEW.md index a0d7bdcc9..870cf590f 100644 --- a/REVIEW.md +++ b/REVIEW.md @@ -12,7 +12,7 @@ Reserve Important for the cases below. Everything else is Nit at most. That incl - Values that reach a query outside a Query Fragment, or any other break of the rules in `docs/agents/connectors.md`. - A limit that doesn't bound the scan. - A change to one connector, or to shared query code, that changes behavior in the other query languages without saying why. -- **Runtime APIs newer than the browser floor in `packages/graph-explorer/.browserslistrc`,** such as `Error.isError` or Iterator helpers. The browser packages set `lib: ES2023`, so `tsc` catches ES2024 and later APIs, but not ES2023 APIs that shipped after the floor, or DOM APIs, which `lib` doesn't version. +- **Runtime APIs newer than the browser floor in `packages/graph-explorer/.browserslistrc`,** such as `Error.isError` or Iterator helpers. The browser packages set `lib: ES2023` (graph-explorer adds `ES2024.String`, which is inside the floor), so `tsc` catches the rest of ES2024 and later APIs, but not ES2023 APIs that shipped after the floor, or DOM APIs, which `lib` doesn't version. - **Production code that imports a `devDependency`.** - **Work that grows with graph or schema size:** one query or subscription per item, quadratic loops, or copying passes over large collections. - **Tests that can't fail.** Ask whether the test would fail if the change were reverted. Flag these: diff --git a/packages/graph-explorer/src/connector/openCypher/fragments.test.ts b/packages/graph-explorer/src/connector/openCypher/fragments.test.ts index 775d58883..12008386c 100644 --- a/packages/graph-explorer/src/connector/openCypher/fragments.test.ts +++ b/packages/graph-explorer/src/connector/openCypher/fragments.test.ts @@ -2,7 +2,6 @@ import fc from "fast-check"; import { describe, expect, it } from "vitest"; import { createEdgeId, createVertexId } from "@/core"; -import { isWellFormedString } from "@/utils/isWellFormedString"; import { EmptyIdentifierError, @@ -44,7 +43,7 @@ describe("fragment.string", () => { it("should round-trip arbitrary strings through the literal", () => { fc.assert( fc.property(fc.string(), value => { - fc.pre(isWellFormedString(value) && !value.includes("\0")); + fc.pre(value.isWellFormed() && !value.includes("\0")); expect(JSON.parse(fragment.string(value))).toBe(value); }), ); diff --git a/packages/graph-explorer/src/connector/queryValueError.ts b/packages/graph-explorer/src/connector/queryValueError.ts index 347115ba2..b480e01d1 100644 --- a/packages/graph-explorer/src/connector/queryValueError.ts +++ b/packages/graph-explorer/src/connector/queryValueError.ts @@ -1,7 +1,5 @@ import type { QueryEngine } from "@shared/types"; -import { isWellFormedString } from "@/utils"; - import type { FragmentPosition } from "./queryFragment"; /** @@ -85,7 +83,7 @@ export class UnrepresentableStringError extends UnrepresentableValueError { * escaped text and always pass. */ export function assertRepresentable(value: string): void { - if (!isWellFormedString(value) || value.includes("\0")) { + if (!value.isWellFormed() || value.includes("\0")) { throw new UnrepresentableStringError(value); } } diff --git a/packages/graph-explorer/src/core/icons/iconSource.ts b/packages/graph-explorer/src/core/icons/iconSource.ts index f78d52f1a..5e48dbf7f 100644 --- a/packages/graph-explorer/src/core/icons/iconSource.ts +++ b/packages/graph-explorer/src/core/icons/iconSource.ts @@ -1,6 +1,5 @@ import type { Branded } from "@/utils/branded"; -import { isWellFormedString } from "@/utils/isWellFormedString"; import { getLucideName } from "@/utils/lucideIcons"; /** @@ -34,7 +33,7 @@ const ICON_VALUE_PATTERN = * too: `encodeURIComponent` throws on one during style computation. */ export function isAllowedIconValue(value: string): boolean { - return ICON_VALUE_PATTERN.test(value) && isWellFormedString(value); + return ICON_VALUE_PATTERN.test(value) && value.isWellFormed(); } /** Identity of an icon, independent of the color any vertex type renders it in. */ diff --git a/packages/graph-explorer/src/utils/index.ts b/packages/graph-explorer/src/utils/index.ts index 8d8481bf1..a15f8dd74 100644 --- a/packages/graph-explorer/src/utils/index.ts +++ b/packages/graph-explorer/src/utils/index.ts @@ -26,7 +26,6 @@ export * from "./formatRelativeDate"; export * from "./numbers"; export * from "./isCancellationError"; export * from "./isVisible"; -export * from "./isWellFormedString"; export * from "./parseNumberSafely"; export * from "./setHelpers"; export * from "./typedEntries"; diff --git a/packages/graph-explorer/src/utils/isWellFormedString.test.ts b/packages/graph-explorer/src/utils/isWellFormedString.test.ts deleted file mode 100644 index 9e3ac3f92..000000000 --- a/packages/graph-explorer/src/utils/isWellFormedString.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -import fc from "fast-check"; -import { describe, expect, it } from "vitest"; - -import { isWellFormedString } from "./isWellFormedString"; - -describe("isWellFormedString", () => { - it("returns true for well-formed strings", () => { - expect(isWellFormedString("hello")).toBe(true); - expect(isWellFormedString("café")).toBe(true); - expect(isWellFormedString("emoji 🚀")).toBe(true); - }); - - it("returns false for a lone high surrogate", () => { - expect(isWellFormedString("\uD800")).toBe(false); - }); - - it("returns false for a lone low surrogate", () => { - expect(isWellFormedString("\uDC00")).toBe(false); - }); - - it("returns false for an unpaired surrogate in a larger string", () => { - expect(isWellFormedString("hello \uD800 world")).toBe(false); - }); - - it("returns false for a high surrogate at the end of the string", () => { - expect(isWellFormedString("a\uD800")).toBe(false); - }); - - it("returns false for a surrogate pair in reverse order", () => { - expect(isWellFormedString("\uDC00\uD800")).toBe(false); - }); - - it("returns false for two adjacent high surrogates", () => { - expect(isWellFormedString("\uD800\uD800\uDC00")).toBe(false); - }); - - it("returns true for a valid surrogate pair", () => { - expect(isWellFormedString("\uD83D\uDE80")).toBe(true); - }); - - it("returns true for an empty string", () => { - expect(isWellFormedString("")).toBe(true); - }); - - it("agrees with encodeURIComponent, which throws on unpaired surrogates", () => { - const codeUnit = fc.oneof( - fc.integer({ min: 0xd800, max: 0xdfff }), - fc.integer({ min: 0, max: 0xffff }), - ); - fc.assert( - fc.property(fc.array(codeUnit, { maxLength: 8 }), codes => { - const value = String.fromCharCode(...codes); - expect(isWellFormedString(value)).toBe(encodes(value)); - }), - ); - }); -}); - -function encodes(value: string): boolean { - try { - encodeURIComponent(value); - return true; - } catch { - return false; - } -} diff --git a/packages/graph-explorer/src/utils/isWellFormedString.ts b/packages/graph-explorer/src/utils/isWellFormedString.ts deleted file mode 100644 index 618e02cd6..000000000 --- a/packages/graph-explorer/src/utils/isWellFormedString.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** - * With the `u` flag, paired surrogates match as one astral code point, so only - * unpaired surrogates have the Surrogate (`Cs`) general category. - */ -const UNPAIRED_SURROGATE = /\p{Cs}/u; - -/** - * Returns `true` if the string contains no unpaired UTF-16 surrogates. - * - * Equivalent to `String.prototype.isWellFormed()`, an ES2024 API that the - * `lib: ES2023` setting excludes. - */ -export function isWellFormedString(value: string): boolean { - return !UNPAIRED_SURROGATE.test(value); -} diff --git a/packages/graph-explorer/tsconfig.json b/packages/graph-explorer/tsconfig.json index 00a9a1782..7390b04e2 100644 --- a/packages/graph-explorer/tsconfig.json +++ b/packages/graph-explorer/tsconfig.json @@ -13,7 +13,7 @@ "jsx": "react-jsx", /* Type Resolution */ - "lib": ["ES2023", "DOM", "DOM.Iterable"], + "lib": ["ES2023", "ES2024.String", "DOM", "DOM.Iterable"], "types": ["vite/client", "@types/wicg-file-system-access"], "paths": { "@/*": ["./src/*"],