From 58c85b4758099527367191899b68a13ad95b3a99 Mon Sep 17 00:00:00 2001 From: Eric Fennis Date: Mon, 2 Nov 2026 20:18:25 +0100 Subject: [PATCH] fix(dev): avoid polynomial regex backtracking in toCamelCase helper (#5148) * fix(dev): avoid polynomial regex backtracking in toCamelCase helper * Add test for toCamelCase with non-word character separators Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- tools/build-helpers/package.json | 3 ++ tools/build-helpers/src/toCamelCase.spec.ts | 42 +++++++++++++++++++++ tools/build-helpers/src/toCamelCase.ts | 4 +- 3 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 tools/build-helpers/src/toCamelCase.spec.ts diff --git a/tools/build-helpers/package.json b/tools/build-helpers/package.json index 8daa385f1..94229efa6 100644 --- a/tools/build-helpers/package.json +++ b/tools/build-helpers/package.json @@ -5,6 +5,9 @@ "description": "A internal used package with helpers.", "main": "helpers.ts", "types": "helpers.ts", + "scripts": { + "test": "vitest run" + }, "author": "", "license": "ISC", "type": "module", diff --git a/tools/build-helpers/src/toCamelCase.spec.ts b/tools/build-helpers/src/toCamelCase.spec.ts new file mode 100644 index 000000000..c382160ab --- /dev/null +++ b/tools/build-helpers/src/toCamelCase.spec.ts @@ -0,0 +1,42 @@ +import { describe, expect, it } from 'vitest'; +import { toCamelCase } from './toCamelCase.ts'; + +describe('toCamelCase', () => { + it('converts kebab-case icon names', () => { + expect(toCamelCase('arrow-up')).toBe('arrowUp'); + expect(toCamelCase('circle-arrow-out-down-left')).toBe('circleArrowOutDownLeft'); + expect(toCamelCase('arrow-up-0-1')).toBe('arrowUp01'); + }); + + it('treats whitespace, hyphens and underscores as separators', () => { + expect(toCamelCase('foo_bar')).toBe('fooBar'); + expect(toCamelCase('foo bar')).toBe('fooBar'); + expect(toCamelCase('foo\tbar')).toBe('fooBar'); + expect(toCamelCase('foo - _\tbar')).toBe('fooBar'); + expect(toCamelCase('-foo')).toBe('Foo'); + }); + + it('lowercases only a leading capital letter', () => { + expect(toCamelCase('Foo-bar')).toBe('fooBar'); + expect(toCamelCase('FooBar')).toBe('fooBar'); + expect(toCamelCase('alreadyCamelCase')).toBe('alreadyCamelCase'); + }); + + it('keeps separators that are not followed by a word character', () => { + expect(toCamelCase('foo-')).toBe('foo-'); + expect(toCamelCase('foo__')).toBe('foo__'); + expect(toCamelCase('foo\t!')).toBe('foo\t!'); + expect(toCamelCase('')).toBe(''); + }); + + it('handles long runs of tabs in linear time', () => { + const input = `a${'\t'.repeat(100_000)}!`; + + const start = performance.now(); + const result = toCamelCase(input); + const duration = performance.now() - start; + + expect(result).toBe(input); + expect(duration).toBeLessThan(100); + }); +}); diff --git a/tools/build-helpers/src/toCamelCase.ts b/tools/build-helpers/src/toCamelCase.ts index 4b049144e..3e25202bd 100644 --- a/tools/build-helpers/src/toCamelCase.ts +++ b/tools/build-helpers/src/toCamelCase.ts @@ -5,6 +5,6 @@ * @returns {string} A camelized string */ export const toCamelCase = (string: string): string => - string.replace(/^([A-Z])|[\s-_]+(\w)/g, (match, p1, p2) => - p2 ? p2.toUpperCase() : p1.toLowerCase(), + string.replace(/^([A-Z])|[\s-_]+(\w)?/g, (match, p1, p2) => + p1 ? p1.toLowerCase() : p2 ? p2.toUpperCase() : match, );