From 2f3cf9f132142a325c15b8a09d9e72e7980dab3f Mon Sep 17 00:00:00 2001 From: Hugo Ruiz-Mireles Date: Sat, 17 Jan 2026 03:40:24 +0000 Subject: [PATCH 1/4] extract version from objects in p5Versions (#3762) Updated parseVersionString to handle object format instead of assuming string format. --- client/utils/parseURLParams.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/client/utils/parseURLParams.js b/client/utils/parseURLParams.js index 3770c918e3..abfe9cd990 100644 --- a/client/utils/parseURLParams.js +++ b/client/utils/parseURLParams.js @@ -7,6 +7,10 @@ const DEFAULTS = { data: false }; +function getVersionString(item) { + return typeof item === 'string' ? item : item.version; +} + /** * Sorts version strings in descending order and returns the highest version * @param {string[]} versions - Array of version strings (e.g., ['1.11.2', '1.11.1']) @@ -30,13 +34,15 @@ function validateVersion(version) { const ver = String(version).trim(); - if (p5Versions.includes(ver)) return ver; + const versions = p5Versions.map(getVersionString); + + if (versions.includes(ver)) return ver; // if only major.minor provided like "1.11" const majorMinorMatch = /^(\d+)\.(\d+)$/.exec(ver); if (majorMinorMatch) { const [, major, minor] = majorMinorMatch; - const matches = p5Versions.filter((v) => { + const matches = versions.filter((v) => { const parts = v.split('.'); return parts[0] === major && parts[1] === minor; }); @@ -49,7 +55,7 @@ function validateVersion(version) { const majorOnlyMatch = /^(\d+)$/.exec(ver); if (majorOnlyMatch) { const [, major] = majorOnlyMatch; - const matches = p5Versions.filter((v) => v.split('.')[0] === major); + const matches = versions.filter((v) => v.split('.')[0] === major); if (matches.length) { return getNewestVersion(matches); } From 38986b9295c42d4a02417cbcff9537dcb3eeb6b9 Mon Sep 17 00:00:00 2001 From: Hugo Ruiz-Mireles Date: Sat, 17 Jan 2026 05:17:33 +0000 Subject: [PATCH 2/4] migrate parseUrlParams to TypeScript (#3761) --- .../{parseURLParams.js => parseURLParams.ts} | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) rename client/utils/{parseURLParams.js => parseURLParams.ts} (81%) diff --git a/client/utils/parseURLParams.js b/client/utils/parseURLParams.ts similarity index 81% rename from client/utils/parseURLParams.js rename to client/utils/parseURLParams.ts index abfe9cd990..b63f265e93 100644 --- a/client/utils/parseURLParams.js +++ b/client/utils/parseURLParams.ts @@ -1,5 +1,13 @@ import { p5Versions, currentP5Version } from '../../common/p5Versions'; +export interface ParsedUrlParams { + version: string; + sound: boolean; + preload: boolean; + shapes: boolean; + data: boolean; +} + const DEFAULTS = { sound: true, preload: false, @@ -7,16 +15,13 @@ const DEFAULTS = { data: false }; -function getVersionString(item) { +function getVersionString( + item: string | { version: string; label: string } +): string { return typeof item === 'string' ? item : item.version; } -/** - * Sorts version strings in descending order and returns the highest version - * @param {string[]} versions - Array of version strings (e.g., ['1.11.2', '1.11.1']) - * @returns {string} The highest version from the array - */ -function getNewestVersion(versions) { +function getNewestVersion(versions: string[]): string { return versions.sort((a, b) => { const pa = a.split('.').map((n) => parseInt(n, 10)); const pb = b.split('.').map((n) => parseInt(n, 10)); @@ -29,7 +34,7 @@ function getNewestVersion(versions) { })[0]; } -function validateVersion(version) { +function validateVersion(version: string | null): string { if (!version) return currentP5Version; const ver = String(version).trim(); @@ -64,7 +69,7 @@ function validateVersion(version) { return currentP5Version; } -function validateBool(value, defaultValue) { +function validateBool(value: string | null, defaultValue: boolean): boolean { if (!value) return defaultValue; const v = String(value).trim().toLowerCase(); @@ -78,7 +83,7 @@ function validateBool(value, defaultValue) { return defaultValue; } -export function parseUrlParams(url) { +export function parseUrlParams(url: string): ParsedUrlParams { const params = new URLSearchParams( new URL(url, 'https://dummy.origin').search ); From 221eddda9f7237c916fe27ad980e52c18897b256 Mon Sep 17 00:00:00 2001 From: Hugo Ruiz-Mireles Date: Sat, 17 Jan 2026 07:03:51 +0000 Subject: [PATCH 3/4] improve and migrate tests for parseURLParams - Add regression tests for major/major.minor version patterns - Add tests for invalid input fallback behavior - Migrate test file to TypeScript - Export p5VersionStrings from parseUrlParams for test validation - Use regex assertions to avoid breaking when p5Versions updates --- client/utils/parseURLParams.test.js | 51 ------------ client/utils/parseURLParams.test.ts | 118 ++++++++++++++++++++++++++++ client/utils/parseURLParams.ts | 10 +-- 3 files changed, 123 insertions(+), 56 deletions(-) delete mode 100644 client/utils/parseURLParams.test.js create mode 100644 client/utils/parseURLParams.test.ts diff --git a/client/utils/parseURLParams.test.js b/client/utils/parseURLParams.test.js deleted file mode 100644 index 56e0b52611..0000000000 --- a/client/utils/parseURLParams.test.js +++ /dev/null @@ -1,51 +0,0 @@ -import { parseUrlParams } from './parseURLParams'; -import { currentP5Version } from '../../common/p5Versions'; - -describe('parseUrlParams', () => { - test('returns defaults when no params are provided', () => { - const url = 'https://example.com'; - const result = parseUrlParams(url); - - expect(result).toEqual({ - version: currentP5Version, - sound: true, - preload: false, - shapes: false, - data: false - }); - }); - - test('parses a valid p5 version and falls back for invalid versions', () => { - const good = parseUrlParams('https://example.com?version=1.4.0'); - expect(good.version).toBe('1.4.0'); - - const bad = parseUrlParams('https://example.com?version=9.9.9'); - expect(bad.version).toBe(currentP5Version); - }); - - test('parses boolean-like params for sound/preload/shapes/data (true variants)', () => { - const trueVariants = ['on', 'true', '1', 'ON', 'True']; - - trueVariants.forEach((v) => { - const url = `https://example.com?sound=${v}&preload=${v}&shapes=${v}&data=${v}`; - const result = parseUrlParams(url); - expect(result.sound).toBe(true); - expect(result.preload).toBe(true); - expect(result.shapes).toBe(true); - expect(result.data).toBe(true); - }); - }); - - test('parses boolean-like params for sound/preload/shapes/data (false variants)', () => { - const falseVariants = ['off', 'false', '0', 'OFF', 'False']; - - falseVariants.forEach((v) => { - const url = `https://example.com?sound=${v}&preload=${v}&shapes=${v}&data=${v}`; - const result = parseUrlParams(url); - expect(result.sound).toBe(false); - expect(result.preload).toBe(false); - expect(result.shapes).toBe(false); - expect(result.data).toBe(false); - }); - }); -}); diff --git a/client/utils/parseURLParams.test.ts b/client/utils/parseURLParams.test.ts new file mode 100644 index 0000000000..6a6cf8e151 --- /dev/null +++ b/client/utils/parseURLParams.test.ts @@ -0,0 +1,118 @@ +import { parseUrlParams, p5VersionStrings } from './parseURLParams'; +import { currentP5Version } from '../../common/p5Versions'; + +describe('parseUrlParams', () => { + describe('default behavior', () => { + test('returns defaults when no params are provided', () => { + const url = 'https://example.com'; + const result = parseUrlParams(url); + + expect(result).toEqual({ + version: currentP5Version, + sound: true, + preload: false, + shapes: false, + data: false + }); + }); + + test('falls back to defaults for unsupported inputs', () => { + const url = + 'https://example.com?version=A&sound=A&preload=A&shapes=A&data=A'; + const result = parseUrlParams(url); + + expect(result).toEqual({ + version: currentP5Version, + sound: true, + preload: false, + shapes: false, + data: false + }); + }); + }); + + describe('version parsing', () => { + // Uses regex since p5Versions may be updated over time. + // Checks to ensure version is valid too. + + test('parses a valid p5 version and falls back for invalid versions', () => { + const good = parseUrlParams('https://example.com?version=1.4.0'); + expect(good.version).toBe('1.4.0'); + + const bad = parseUrlParams('https://example.com?version=9.9.9'); + expect(bad.version).toBe(currentP5Version); + }); + + test('parses major.minor version 0.5.x to newest patch', () => { + const url = 'https://example.com?version=0.5'; + const result = parseUrlParams(url); + + expect(result.version).toMatch(/^0\.5\.\d+$/); + expect(p5VersionStrings).toContain(result.version); + }); + + test('parses major version 0 to newest 0.x version', () => { + const url = 'https://example.com?version=0'; + const result = parseUrlParams(url); + + expect(result.version).toMatch(/^0\.\d+\.\d+$/); + expect(p5VersionStrings).toContain(result.version); + }); + + test('parses major version 1 to newest 1.x version', () => { + const url = 'https://example.com?version=1'; + const result = parseUrlParams(url); + expect(result.version).toMatch(/^1\.\d+\.\d+$/); + expect(p5VersionStrings).toContain(result.version); + }); + + test('parses major.minor version 1.1.x to newest patch', () => { + const url = 'https://example.com?version=1.1'; + const result = parseUrlParams(url); + expect(result.version).toMatch(/^1\.1\.\d+$/); + expect(p5VersionStrings).toContain(result.version); + }); + + test('parses major version 2 to newest 2.x version', () => { + const url = 'https://example.com?version=2'; + const result = parseUrlParams(url); + expect(result.version).toMatch(/^2\.\d+\.\d+$/); + expect(p5VersionStrings).toContain(result.version); + }); + + test('parses major.minor version 2.0.x to newest patch', () => { + const url = 'https://example.com?version=2.0'; + const result = parseUrlParams(url); + expect(result.version).toMatch(/^2\.0\.\d+$/); + expect(p5VersionStrings).toContain(result.version); + }); + }); + + describe('boolean param parsing', () => { + test('parses boolean-like params for sound/preload/shapes/data (true variants)', () => { + const trueVariants = ['on', 'true', '1', 'ON', 'True']; + + trueVariants.forEach((v) => { + const url = `https://example.com?sound=${v}&preload=${v}&shapes=${v}&data=${v}`; + const result = parseUrlParams(url); + expect(result.sound).toBe(true); + expect(result.preload).toBe(true); + expect(result.shapes).toBe(true); + expect(result.data).toBe(true); + }); + }); + + test('parses boolean-like params for sound/preload/shapes/data (false variants)', () => { + const falseVariants = ['off', 'false', '0', 'OFF', 'False']; + + falseVariants.forEach((v) => { + const url = `https://example.com?sound=${v}&preload=${v}&shapes=${v}&data=${v}`; + const result = parseUrlParams(url); + expect(result.sound).toBe(false); + expect(result.preload).toBe(false); + expect(result.shapes).toBe(false); + expect(result.data).toBe(false); + }); + }); + }); +}); diff --git a/client/utils/parseURLParams.ts b/client/utils/parseURLParams.ts index b63f265e93..5e47683818 100644 --- a/client/utils/parseURLParams.ts +++ b/client/utils/parseURLParams.ts @@ -21,6 +21,8 @@ function getVersionString( return typeof item === 'string' ? item : item.version; } +export const p5VersionStrings = p5Versions.map(getVersionString); + function getNewestVersion(versions: string[]): string { return versions.sort((a, b) => { const pa = a.split('.').map((n) => parseInt(n, 10)); @@ -39,15 +41,13 @@ function validateVersion(version: string | null): string { const ver = String(version).trim(); - const versions = p5Versions.map(getVersionString); - - if (versions.includes(ver)) return ver; + if (p5VersionStrings.includes(ver)) return ver; // if only major.minor provided like "1.11" const majorMinorMatch = /^(\d+)\.(\d+)$/.exec(ver); if (majorMinorMatch) { const [, major, minor] = majorMinorMatch; - const matches = versions.filter((v) => { + const matches = p5VersionStrings.filter((v) => { const parts = v.split('.'); return parts[0] === major && parts[1] === minor; }); @@ -60,7 +60,7 @@ function validateVersion(version: string | null): string { const majorOnlyMatch = /^(\d+)$/.exec(ver); if (majorOnlyMatch) { const [, major] = majorOnlyMatch; - const matches = versions.filter((v) => v.split('.')[0] === major); + const matches = p5VersionStrings.filter((v) => v.split('.')[0] === major); if (matches.length) { return getNewestVersion(matches); } From 59ae2fd113ba8a2cce157ae9383d0d5ac5c02137 Mon Sep 17 00:00:00 2001 From: Hugo Ruiz-Mireles Date: Sat, 24 Jan 2026 03:50:41 +0000 Subject: [PATCH 4/4] address review feedback - Add JSDoc to parseUrlParams - Remove export from p5VersionStrings and copy its logic to test file - Fix invalid version test to use 'invalid-version' instead of '9.9.9' --- client/utils/parseURLParams.test.ts | 14 +++++++++++--- client/utils/parseURLParams.ts | 8 +++++++- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/client/utils/parseURLParams.test.ts b/client/utils/parseURLParams.test.ts index 6a6cf8e151..0a143a7aa3 100644 --- a/client/utils/parseURLParams.test.ts +++ b/client/utils/parseURLParams.test.ts @@ -1,5 +1,13 @@ -import { parseUrlParams, p5VersionStrings } from './parseURLParams'; -import { currentP5Version } from '../../common/p5Versions'; +import { parseUrlParams } from './parseURLParams'; +import { p5Versions, currentP5Version } from '../../common/p5Versions'; + +function getVersionString( + item: string | { version: string; label: string } +): string { + return typeof item === 'string' ? item : item.version; +} + +const p5VersionStrings = p5Versions.map(getVersionString); describe('parseUrlParams', () => { describe('default behavior', () => { @@ -39,7 +47,7 @@ describe('parseUrlParams', () => { const good = parseUrlParams('https://example.com?version=1.4.0'); expect(good.version).toBe('1.4.0'); - const bad = parseUrlParams('https://example.com?version=9.9.9'); + const bad = parseUrlParams('https://example.com?version=invalid-version'); expect(bad.version).toBe(currentP5Version); }); diff --git a/client/utils/parseURLParams.ts b/client/utils/parseURLParams.ts index 5e47683818..f882d69164 100644 --- a/client/utils/parseURLParams.ts +++ b/client/utils/parseURLParams.ts @@ -21,7 +21,7 @@ function getVersionString( return typeof item === 'string' ? item : item.version; } -export const p5VersionStrings = p5Versions.map(getVersionString); +const p5VersionStrings = p5Versions.map(getVersionString); function getNewestVersion(versions: string[]): string { return versions.sort((a, b) => { @@ -83,6 +83,12 @@ function validateBool(value: string | null, defaultValue: boolean): boolean { return defaultValue; } +/** + * Parses URL parameters for version and boolean flags. + * + * @param url - The URL string to parse. + * @returns Parsed and validated URL parameters including version and library flags. + */ export function parseUrlParams(url: string): ParsedUrlParams { const params = new URLSearchParams( new URL(url, 'https://dummy.origin').search