diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index 73a3627..69099cf 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -1,5 +1,5 @@
{
- "name": "H4",
+ "name": "H4 Typescript",
"image": "mcr.microsoft.com/devcontainers/typescript-node:24",
"features": {
"ghcr.io/devcontainers/features/github-cli:1": {},
@@ -28,4 +28,4 @@
"onAutoForward": "notify"
}
}
-}
\ No newline at end of file
+}
diff --git a/.devcontainer/workflow.log b/.devcontainer/workflow.log
new file mode 100644
index 0000000..44a623f
--- /dev/null
+++ b/.devcontainer/workflow.log
@@ -0,0 +1,47 @@
+Run echo "Running strict security audit..."
+Running strict security audit...
+Audit level: low
+┌─────────────────────┬────────────────────────────────────────────────────────┐
+│ high │ Vite Vulnerable to Arbitrary File Read via Vite Dev │
+│ │ Server WebSocket │
+├─────────────────────┼────────────────────────────────────────────────────────┤
+│ Package │ vite │
+├─────────────────────┼────────────────────────────────────────────────────────┤
+│ Vulnerable versions │ >=8.0.0 <=8.0.4 │
+├─────────────────────┼────────────────────────────────────────────────────────┤
+│ Patched versions │ >=8.0.5 │
+├─────────────────────┼────────────────────────────────────────────────────────┤
+│ Paths │ .>vite │
+├─────────────────────┼────────────────────────────────────────────────────────┤
+│ More info │ https://github.com/advisories/GHSA-p9ff-h696-f583 │
+└─────────────────────┴────────────────────────────────────────────────────────┘
+┌─────────────────────┬────────────────────────────────────────────────────────┐
+│ high │ Vite: `server.fs.deny` bypassed with queries │
+├─────────────────────┼────────────────────────────────────────────────────────┤
+│ Package │ vite │
+├─────────────────────┼────────────────────────────────────────────────────────┤
+│ Vulnerable versions │ >=8.0.0 <=8.0.4 │
+├─────────────────────┼────────────────────────────────────────────────────────┤
+│ Patched versions │ >=8.0.5 │
+├─────────────────────┼────────────────────────────────────────────────────────┤
+│ Paths │ .>vite │
+├─────────────────────┼────────────────────────────────────────────────────────┤
+│ More info │ https://github.com/advisories/GHSA-v2wj-q39q-566r │
+└─────────────────────┴────────────────────────────────────────────────────────┘
+┌─────────────────────┬────────────────────────────────────────────────────────┐
+│ moderate │ Vite Vulnerable to Path Traversal in Optimized Deps │
+│ │ `.map` Handling │
+├─────────────────────┼────────────────────────────────────────────────────────┤
+│ Package │ vite │
+├─────────────────────┼────────────────────────────────────────────────────────┤
+│ Vulnerable versions │ >=8.0.0 <=8.0.4 │
+├─────────────────────┼────────────────────────────────────────────────────────┤
+│ Patched versions │ >=8.0.5 │
+├─────────────────────┼────────────────────────────────────────────────────────┤
+│ Paths │ .>vite │
+├─────────────────────┼────────────────────────────────────────────────────────┤
+│ More info │ https://github.com/advisories/GHSA-4w7w-66w2-5vf9 │
+└─────────────────────┴────────────────────────────────────────────────────────┘
+3 vulnerabilities found
+Severity: 1 moderate | 2 high
+Error: Process completed with exit code 1.
diff --git a/.eslintrc.json b/.eslintrc.json
deleted file mode 100644
index 3df01b2..0000000
--- a/.eslintrc.json
+++ /dev/null
@@ -1,66 +0,0 @@
-{
- "root": true,
- "parser": "@typescript-eslint/parser",
- "parserOptions": {
- "project": "./tsconfig.json"
- },
- "env": {
- "es6": true
- },
- "ignorePatterns": [
- "build",
- "coverage",
- "node_modules",
- "package.json"
- ],
- "plugins": [
- "import",
- "eslint-comments",
- "functional"
- ],
- "extends": [
- "eslint:recommended",
- "plugin:eslint-comments/recommended",
- "plugin:@typescript-eslint/recommended",
- "plugin:import/typescript",
- "plugin:functional/lite",
- "prettier"
- ],
- "globals": {
- "BigInt": true,
- "console": true,
- "WebAssembly": true
- },
- "rules": {
- "@typescript-eslint/explicit-module-boundary-types": "off",
- "eslint-comments/disable-enable-pair": [
- "error",
- {
- "allowWholeFile": true
- }
- ],
- "eslint-comments/no-unused-disable": "error",
- "import/order": [
- "error",
- {
- "newlines-between": "always",
- "alphabetize": {
- "order": "asc"
- }
- }
- ],
- "sort-imports": [
- "error",
- {
- "ignoreDeclarationSort": true,
- "ignoreCase": true
- }
- ],
- "prettier/prettier": [
- "error",
- {
- "singleQuote": false
- }
- ]
- }
-}
\ No newline at end of file
diff --git a/.oxlintrc.json b/.oxlintrc.json
new file mode 100644
index 0000000..e00e2d7
--- /dev/null
+++ b/.oxlintrc.json
@@ -0,0 +1,39 @@
+{
+ "$schema": "./node_modules/oxlint/configuration_schema.json",
+ "plugins": [
+ "typescript",
+ "unicorn",
+ "import",
+ "jsdoc",
+ "oxc"
+ ],
+ "categories": {
+ "correctness": "error"
+ },
+ "rules": {
+ "no-explicit-any": "warn",
+ "sort-imports": "warn",
+ "no-unused-vars": [
+ "error",
+ {
+ "argsIgnorePattern": "^_",
+ "varsIgnorePattern": "^_"
+ }
+ ]
+ },
+ "overrides": [
+ {
+ "files": ["**/*.test.ts", "**/*.spec.ts", "**/*.bench.ts", "**/*.example.ts"],
+ "rules": {
+ "no-explicit-any": "off"
+ }
+ }
+ ],
+ "ignorePatterns": [
+ "build",
+ "coverage",
+ "node_modules",
+ "reports",
+ ".stryker-tmp"
+ ]
+}
diff --git a/.template/category/README.md b/.template/category/README.md
index 118d20b..a51b70c 100644
--- a/.template/category/README.md
+++ b/.template/category/README.md
@@ -9,7 +9,7 @@ A set of helpers for working with URLs.
## Documentation
-[https://helpers4.js.org/{{category}}](https://helpers4.js.org/{{category}})
+[https://helpers4.dev/typescript/categories/{{category}}/](https://helpers4.dev/typescript/categories/{{category}}/)
- method
diff --git a/.template/category/package.json b/.template/category/package.json
index a65eea9..ae6fcae 100644
--- a/.template/category/package.json
+++ b/.template/category/package.json
@@ -19,6 +19,7 @@
},
"./examples.json": "./examples.json",
"./api.json": "./api.json",
+ "./licenses.json": "./licenses.json",
"./package.json": "./package.json"
},
"keywords": [
@@ -32,6 +33,7 @@
"lib/index.js.map",
"examples.json",
"api.json",
+ "licenses.json",
"LICENSE.md",
"package.json",
"README.md"
diff --git a/README.md b/README.md
index 48e09c7..871a23a 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,6 @@
-
diff --git a/helpers/array/deepCompare.test.ts b/helpers/array/deepCompare.test.ts
index cc1273d..aa5a035 100644
--- a/helpers/array/deepCompare.test.ts
+++ b/helpers/array/deepCompare.test.ts
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: LGPL-3.0-or-later
*/
-import { describe, it, expect } from 'vitest';
+import { describe, expect, it } from 'vitest';
import { deepCompare } from './deepCompare';
describe('deepCompare', () => {
diff --git a/helpers/array/quickCompare.test.ts b/helpers/array/quickCompare.test.ts
index 3fd1c37..2d681e9 100644
--- a/helpers/array/quickCompare.test.ts
+++ b/helpers/array/quickCompare.test.ts
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: LGPL-3.0-or-later
*/
-import { describe, it, expect } from 'vitest';
+import { describe, expect, it } from 'vitest';
import { quickCompare } from './quickCompare';
describe('quickCompare', () => {
diff --git a/helpers/array/sort.example.ts b/helpers/array/sort.example.ts
index 68a70d5..c8424cf 100644
--- a/helpers/array/sort.example.ts
+++ b/helpers/array/sort.example.ts
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: LGPL-3.0-or-later
*/
-import { sortNumberAscFn, sortStringAscFn, createSortByStringFn } from './sort';
+import { createSortByStringFn, sortNumberAscFn, sortStringAscFn } from './sort';
import type { HelperExamples } from '../../scripts/examples/types';
const examples: HelperExamples = {
diff --git a/helpers/array/sort.test.ts b/helpers/array/sort.test.ts
index aae28c1..341ef42 100644
--- a/helpers/array/sort.test.ts
+++ b/helpers/array/sort.test.ts
@@ -6,14 +6,14 @@
import { describe, expect, it } from "vitest";
import {
+ createSortByDateFn,
+ createSortByNumberFn,
+ createSortByStringFn,
sortNumberAscFn,
sortNumberDescFn,
sortStringAscFn,
- sortStringDescFn,
sortStringAscInsensitiveFn,
- createSortByStringFn,
- createSortByNumberFn,
- createSortByDateFn
+ sortStringDescFn
} from "./sort";
describe("sort functions", () => {
diff --git a/helpers/date/compare.test.ts b/helpers/date/compare.test.ts
index dd9b588..43e7c38 100644
--- a/helpers/date/compare.test.ts
+++ b/helpers/date/compare.test.ts
@@ -4,8 +4,8 @@
* SPDX-License-Identifier: LGPL-3.0-or-later
*/
-import { describe, it, expect } from 'vitest';
-import { compare, DateCompareOptions } from './compare';
+import { describe, expect, it } from 'vitest';
+import { DateCompareOptions, compare } from './compare';
describe('compare', () => {
const date1 = new Date('2023-01-01T12:30:45.123Z');
diff --git a/helpers/date/difference.test.ts b/helpers/date/difference.test.ts
index 545e716..3026b1a 100644
--- a/helpers/date/difference.test.ts
+++ b/helpers/date/difference.test.ts
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: LGPL-3.0-or-later
*/
-import { describe, it, expect } from 'vitest';
+import { describe, expect, it } from 'vitest';
import { daysDifference } from './difference';
describe('daysDifference', () => {
diff --git a/helpers/date/format.example.ts b/helpers/date/format.example.ts
index 1fa8760..25d7215 100644
--- a/helpers/date/format.example.ts
+++ b/helpers/date/format.example.ts
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: LGPL-3.0-or-later
*/
-import { toISO8601, toRFC3339, toRFC2822 } from './format';
+import { toISO8601, toRFC2822, toRFC3339 } from './format';
import type { HelperExamples } from '../../scripts/examples/types';
const examples: HelperExamples = {
diff --git a/helpers/date/format.test.ts b/helpers/date/format.test.ts
index 4264287..ae0290b 100644
--- a/helpers/date/format.test.ts
+++ b/helpers/date/format.test.ts
@@ -108,12 +108,26 @@ describe('toRFC2822', () => {
});
it('should handle different months', () => {
- expect(toRFC2822('2025-02-15T12:00:00Z')).toBe(
- 'Sat, 15 Feb 2025 12:00:00 +0000'
- );
- expect(toRFC2822('2025-12-25T12:00:00Z')).toBe(
- 'Thu, 25 Dec 2025 12:00:00 +0000'
- );
+ // Test all 12 months to kill month string mutations
+ const monthExpected = [
+ ['2025-01-15', 'Jan'],
+ ['2025-02-15', 'Feb'],
+ ['2025-03-15', 'Mar'],
+ ['2025-04-15', 'Apr'],
+ ['2025-05-15', 'May'],
+ ['2025-06-15', 'Jun'],
+ ['2025-07-15', 'Jul'],
+ ['2025-08-15', 'Aug'],
+ ['2025-09-15', 'Sep'],
+ ['2025-10-15', 'Oct'],
+ ['2025-11-15', 'Nov'],
+ ['2025-12-15', 'Dec'],
+ ] as const;
+
+ for (const [dateStr, monthAbbr] of monthExpected) {
+ const result = toRFC2822(`${dateStr}T12:00:00Z`);
+ expect(result).toContain(monthAbbr);
+ }
});
it('should pad single digit days', () => {
diff --git a/helpers/date/is.test.ts b/helpers/date/is.test.ts
index d18e48e..a0354bd 100644
--- a/helpers/date/is.test.ts
+++ b/helpers/date/is.test.ts
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: LGPL-3.0-or-later
*/
-import { describe, it, expect } from 'vitest';
+import { describe, expect, it } from 'vitest';
import { isSameDay } from './is';
describe('isSameDay', () => {
@@ -59,4 +59,22 @@ describe('isSameDay', () => {
const utcEvening = new Date('2023-01-01T22:00:00.000Z');
expect(isSameDay(utcMorning, utcEvening)).toBe(true);
});
+
+ it('should return false for same month and day but different year', () => {
+ const date1 = new Date('2023-06-15T12:00:00.000Z');
+ const date2 = new Date('2024-06-15T12:00:00.000Z');
+ expect(isSameDay(date1, date2)).toBe(false);
+ });
+
+ it('should return false for same year and day but different month', () => {
+ const date1 = new Date('2023-03-15T12:00:00.000Z');
+ const date2 = new Date('2023-04-15T12:00:00.000Z');
+ expect(isSameDay(date1, date2)).toBe(false);
+ });
+
+ it('should return false for same year and month but different day', () => {
+ const date1 = new Date('2023-06-15T12:00:00.000Z');
+ const date2 = new Date('2023-06-16T12:00:00.000Z');
+ expect(isSameDay(date1, date2)).toBe(false);
+ });
});
diff --git a/helpers/date/safeDate.test.ts b/helpers/date/safeDate.test.ts
index f9ffa1d..693820a 100644
--- a/helpers/date/safeDate.test.ts
+++ b/helpers/date/safeDate.test.ts
@@ -5,7 +5,7 @@
*/
import { describe, expect, it } from "vitest";
-import { safeDate, dateToISOString } from "./safeDate";
+import { dateToISOString, safeDate } from "./safeDate";
describe("safe date utilities", () => {
describe("safeDate", () => {
@@ -20,11 +20,23 @@ describe("safe date utilities", () => {
expect(date).toBeInstanceOf(Date);
});
- it("should return null for invalid inputs", () => {
+ it("should return null for null", () => {
expect(safeDate(null)).toBe(null);
+ });
+
+ it("should return null for undefined", () => {
expect(safeDate(undefined)).toBe(null);
+ });
+
+ it("should return null for empty string", () => {
expect(safeDate("")).toBe(null);
+ });
+
+ it("should return null for zero", () => {
expect(safeDate(0)).toBe(null);
+ });
+
+ it("should return null for invalid date string", () => {
expect(safeDate("invalid")).toBe(null);
});
diff --git a/helpers/function/throttle.test.ts b/helpers/function/throttle.test.ts
index 15fbe2f..bd821bc 100644
--- a/helpers/function/throttle.test.ts
+++ b/helpers/function/throttle.test.ts
@@ -4,11 +4,19 @@
* SPDX-License-Identifier: LGPL-3.0-or-later
*/
-import { describe, expect, it } from "vitest";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { throttle } from "./throttle";
describe("throttle", () => {
- it("should throttle function calls", async () => {
+ beforeEach(() => {
+ vi.useFakeTimers();
+ });
+
+ afterEach(() => {
+ vi.useRealTimers();
+ });
+
+ it("should throttle function calls", () => {
let callCount = 0;
const throttledFunc = throttle(() => callCount++, 100);
@@ -18,7 +26,7 @@ describe("throttle", () => {
expect(callCount).toBe(1);
- await new Promise(resolve => setTimeout(resolve, 150));
+ vi.advanceTimersByTime(150);
expect(callCount).toBe(2);
});
@@ -31,4 +39,36 @@ describe("throttle", () => {
throttledFunc(1, 'test', true);
expect(lastArgs).toEqual([1, 'test', true]);
});
+
+ it("should invoke immediately when called exactly at the wait boundary", () => {
+ let callCount = 0;
+ const wait = 50;
+ const throttledFunc = throttle(() => callCount++, wait);
+
+ throttledFunc();
+ expect(callCount).toBe(1);
+
+ // Advance exactly the throttle duration then call again
+ vi.advanceTimersByTime(wait);
+ throttledFunc();
+ expect(callCount).toBe(2);
+ });
+
+ it("should schedule trailing call with correct remaining delay", () => {
+ let callCount = 0;
+ const wait = 100;
+ const throttledFunc = throttle(() => callCount++, wait);
+
+ throttledFunc(); // Immediate call
+ expect(callCount).toBe(1);
+
+ // Call again quickly (after 30ms, well within the wait window)
+ vi.advanceTimersByTime(30);
+ throttledFunc();
+ expect(callCount).toBe(1); // Still throttled
+
+ // The trailing call should fire after the remaining ~70ms
+ vi.advanceTimersByTime(70);
+ expect(callCount).toBe(2);
+ });
});
diff --git a/helpers/object/deepCompare.test.ts b/helpers/object/deepCompare.test.ts
index 446998b..899494f 100644
--- a/helpers/object/deepCompare.test.ts
+++ b/helpers/object/deepCompare.test.ts
@@ -4,8 +4,8 @@
* SPDX-License-Identifier: LGPL-3.0-or-later
*/
-import { describe, it, expect } from 'vitest';
-import { deepCompare, DeepCompareResult } from './deepCompare';
+import { describe, expect, it } from 'vitest';
+import { DeepCompareResult, deepCompare } from './deepCompare';
describe('deepCompare', () => {
it('should return true for identical objects', () => {
@@ -255,6 +255,85 @@ describe('deepCompare', () => {
expect(result).toEqual({ level1: { level2: { y: false } } });
});
+ // --- Mutation-killing tests ---
+
+ it('should return false when objA is valid object and objB is null', () => {
+ expect(deepCompare({ a: 1 }, null)).toBe(false);
+ });
+
+ it('should return false when objA is valid object and objB is undefined', () => {
+ expect(deepCompare({ a: 1 }, undefined)).toBe(false);
+ });
+
+ it('should return false when comparing Date with plain object at root', () => {
+ expect(deepCompare(new Date('2023-01-01'), { a: 1 })).toBe(false);
+ });
+
+ it('should return false when comparing plain object with Date at root', () => {
+ expect(deepCompare({ a: 1 }, new Date('2023-01-01'))).toBe(false);
+ });
+
+ it('should return false when comparing array with plain object at root', () => {
+ expect(deepCompare([1, 2], { a: 1 })).toBe(false);
+ });
+
+ it('should return false when comparing plain object with array at root', () => {
+ expect(deepCompare({ a: 1 }, [1, 2])).toBe(false);
+ });
+
+ it('should return false when one root arg is special object and other is plain', () => {
+ expect(deepCompare(/test/, { a: 1 })).toBe(false);
+ expect(deepCompare({ a: 1 }, new Map())).toBe(false);
+ });
+
+ it('should handle nested object vs null value', () => {
+ expect(deepCompare({ a: { x: 1 } }, { a: null })).toEqual({ a: false });
+ });
+
+ it('should handle nested null vs object value', () => {
+ expect(deepCompare({ a: null }, { a: { x: 1 } })).toEqual({ a: false });
+ });
+
+ it('should handle nested object vs undefined value', () => {
+ expect(deepCompare({ a: { x: 1 } }, { a: undefined })).toEqual({ a: false });
+ });
+
+ it('should handle nested undefined vs object value', () => {
+ expect(deepCompare({ a: undefined }, { a: { x: 1 } })).toEqual({ a: false });
+ });
+
+ it('should handle nested special object vs plain object value', () => {
+ expect(deepCompare({ a: /regex/ }, { a: { x: 1 } })).toEqual({ a: false });
+ });
+
+ it('should handle nested plain object vs special object value', () => {
+ expect(deepCompare({ a: { x: 1 } }, { a: /regex/ })).toEqual({ a: false });
+ });
+
+ it('should handle nested Date vs non-Date value', () => {
+ expect(deepCompare({ d: new Date('2023-01-01') }, { d: 'not-a-date' })).toEqual({ d: false });
+ });
+
+ it('should handle nested non-Date vs Date value', () => {
+ expect(deepCompare({ d: 'not-a-date' }, { d: new Date('2023-01-01') })).toEqual({ d: false });
+ });
+
+ it('should handle nested array vs non-array value', () => {
+ expect(deepCompare({ arr: [1, 2] }, { arr: 'string' })).toEqual({ arr: false });
+ });
+
+ it('should handle nested non-array vs array value', () => {
+ expect(deepCompare({ arr: 'string' }, { arr: [1, 2] })).toEqual({ arr: false });
+ });
+
+ it('should handle primitive number vs object in property values', () => {
+ expect(deepCompare({ a: 42 }, { a: { x: 1 } })).toEqual({ a: false });
+ });
+
+ it('should handle object vs primitive number in property values', () => {
+ expect(deepCompare({ a: { x: 1 } }, { a: 42 })).toEqual({ a: false });
+ });
+
it('should handle nested objects returning false (incompatible types)', () => {
// Test where nestedResult is exactly `false` due to type incompatibility
const obj1 = { nested: { a: 1 } };
diff --git a/helpers/object/quickCompare.test.ts b/helpers/object/quickCompare.test.ts
index d2a999a..99f50e5 100644
--- a/helpers/object/quickCompare.test.ts
+++ b/helpers/object/quickCompare.test.ts
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: LGPL-3.0-or-later
*/
-import { describe, it, expect } from 'vitest';
+import { describe, expect, it } from 'vitest';
import { quickCompare } from './quickCompare';
describe('quickCompare', () => {
@@ -102,4 +102,36 @@ describe('quickCompare', () => {
const obj2 = { a: 1, b: undefined };
expect(quickCompare(obj1, obj2)).toBe(true);
});
+
+ it('should return true for same reference via early return', () => {
+ const obj = { a: 1, b: { c: 2 } };
+ expect(quickCompare(obj, obj)).toBe(true);
+ });
+
+ it('should return false when only first argument is a function', () => {
+ expect(quickCompare(() => {}, 'not a function')).toBe(false);
+ });
+
+ it('should return false when only second argument is a function', () => {
+ expect(quickCompare('not a function', () => {})).toBe(false);
+ });
+
+ it('should return true for same function reference', () => {
+ const fn = () => {};
+ expect(quickCompare(fn, fn)).toBe(true);
+ });
+
+ it('should return false for different function references', () => {
+ expect(quickCompare(() => {}, () => {})).toBe(false);
+ });
+
+ it('should handle circular references gracefully', () => {
+ const obj1: Record = { a: 1 };
+ obj1.self = obj1;
+ const obj2: Record = { a: 1 };
+ obj2.self = obj2;
+
+ // Different circular objects should return false (fallback to ===)
+ expect(quickCompare(obj1, obj2)).toBe(false);
+ });
});
diff --git a/helpers/observable/combineLatest.ts b/helpers/observable/combineLatest.ts
index a7b2533..0ff6e0e 100644
--- a/helpers/observable/combineLatest.ts
+++ b/helpers/observable/combineLatest.ts
@@ -47,7 +47,7 @@ export function combineLatest>>(
*
* @param {ObservableInput} [observables] An array of input Observables to combine with each other.
* An array of Observables must be given as the first argument.
- * @return {Observable} An Observable of projected values from the most recent
+ * @returns {Observable} An Observable of projected values from the most recent
* values from each input Observable, or an array of the most recent values from
* each input Observable.
*/
diff --git a/helpers/string/errorToReadableMessage.test.ts b/helpers/string/errorToReadableMessage.test.ts
index af6b19a..dba1dfc 100644
--- a/helpers/string/errorToReadableMessage.test.ts
+++ b/helpers/string/errorToReadableMessage.test.ts
@@ -8,7 +8,7 @@
* This program is under the terms of the GNU Lesser General Public License version 3
* The full license information can be found in LICENSE in the root directory of this project.
*/
-import { expect, test, describe } from "vitest";
+import { describe, expect, test } from "vitest";
import { errorToReadableMessage } from "./errorToReadableMessage";
describe('errorToReadableMessage', () => {
diff --git a/helpers/string/labelize.test.ts b/helpers/string/labelize.test.ts
index 1345592..9337844 100644
--- a/helpers/string/labelize.test.ts
+++ b/helpers/string/labelize.test.ts
@@ -8,7 +8,7 @@
* This program is under the terms of the GNU Lesser General Public License version 3
* The full license information can be found in LICENSE in the root directory of this project.
*/
-import { expect, test } from "vitest";
+import { describe, expect, it, test } from "vitest";
import { labelize } from "./labelize";
// -- labelize -----------------------------------------------------------------
@@ -34,3 +34,29 @@ import { labelize } from "./labelize";
.forEach((word) => expect(word[0]).toBe(word[0].toUpperCase()));
});
});
+
+describe('labelize', () => {
+ it('should split on hyphens and capitalize each word', () => {
+ expect(labelize('hello-world')).toBe('Hello World');
+ });
+
+ it('should split on underscores and capitalize each word', () => {
+ expect(labelize('hello_world')).toBe('Hello World');
+ });
+
+ it('should split on spaces and capitalize each word', () => {
+ expect(labelize('hello world')).toBe('Hello World');
+ });
+
+ it('should lowercase the rest of each word', () => {
+ expect(labelize('HELLO-WORLD')).toBe('Hello World');
+ });
+
+ it('should handle mixed separators', () => {
+ expect(labelize('foo-bar_baz qux')).toBe('Foo Bar Baz Qux');
+ });
+
+ it('should handle consecutive separators', () => {
+ expect(labelize('foo--bar__baz')).toBe('Foo Bar Baz');
+ });
+});
diff --git a/helpers/string/slugify.test.ts b/helpers/string/slugify.test.ts
index cac96b0..485fbe5 100644
--- a/helpers/string/slugify.test.ts
+++ b/helpers/string/slugify.test.ts
@@ -47,4 +47,31 @@ describe('slugify', () => {
it('should avoid leading and trailing hyphens', () => {
expect(slugify('---Hello world---')).toBe('hello-world');
});
+
+ it('should replace non-alphanumeric characters with hyphens', () => {
+ expect(slugify('hello@world#test')).toBe('hello-world-test');
+ });
+
+ it('should collapse multiple consecutive hyphens into one', () => {
+ expect(slugify('a---b')).toBe('a-b');
+ });
+
+ it('should remove leading hyphens only', () => {
+ expect(slugify('---hello')).toBe('hello');
+ });
+
+ it('should remove trailing hyphens only', () => {
+ expect(slugify('hello---')).toBe('hello');
+ });
+
+ it('should normalize unicode characters', () => {
+ expect(slugify('naïve résumé')).toBe('naive-resume');
+ });
+
+ it('should produce hyphen-separated words not concatenated', () => {
+ // Ensures replace(/[^a-z0-9]+/g, '-') uses '-' not ''
+ const result = slugify('hello world');
+ expect(result).toBe('hello-world');
+ expect(result).not.toBe('helloworld');
+ });
});
diff --git a/helpers/type/isSpecialObject.test.ts b/helpers/type/isSpecialObject.test.ts
index eca7877..4000fed 100644
--- a/helpers/type/isSpecialObject.test.ts
+++ b/helpers/type/isSpecialObject.test.ts
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: LGPL-3.0-or-later
*/
-import { describe, it, expect } from 'vitest';
+import { describe, expect, it } from 'vitest';
import { isSpecialObject } from './isSpecialObject';
describe('isSpecialObject', () => {
@@ -90,15 +90,16 @@ describe('isSpecialObject', () => {
});
it('should return true for HTMLElement in browser', () => {
- // In happy-dom, HTMLElement may or may not be fully implemented
- // Test that our code doesn't crash when checking HTMLElement
- try {
- const element = document.createElement('div');
- expect(isSpecialObject(element)).toBe(true);
- } catch {
- // If HTMLElement is not available, just skip
- expect(true).toBe(true);
- }
+ const element = document.createElement('div');
+ expect(isSpecialObject(element)).toBe(true);
+ });
+
+ it('should return false for undefined passed directly', () => {
+ expect(isSpecialObject(undefined)).toBe(false);
+ });
+
+ it('should return false for arrays (not special)', () => {
+ expect(isSpecialObject([1, 2])).toBe(false);
});
it('should return true for built-in types by constructor name', () => {
@@ -107,6 +108,15 @@ describe('isSpecialObject', () => {
expect(isSpecialObject(buffer)).toBe(true);
});
+ it('should return true for objects with matching Web API constructor names', () => {
+ const webApiNames = ['File', 'Blob', 'FormData', 'Headers', 'Request', 'Response', 'EventTarget', 'Symbol'];
+ for (const name of webApiNames) {
+ const mock = Object.create({ constructor: { name } });
+ mock.constructor = { name };
+ expect(isSpecialObject(mock)).toBe(true);
+ }
+ });
+
it('should return false for objects with non-matching constructor names', () => {
const customObj = { constructor: { name: 'CustomClass' } };
expect(isSpecialObject(customObj)).toBe(false);
diff --git a/helpers/type/typeChecks.example.ts b/helpers/type/typeChecks.example.ts
index 5da0fae..cae7846 100644
--- a/helpers/type/typeChecks.example.ts
+++ b/helpers/type/typeChecks.example.ts
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: LGPL-3.0-or-later
*/
-import { isString, isNumber, isBoolean, isArray, isObject, isDate, isSet, isFunction, isValidRegex } from './typeChecks';
+import { isArray, isBoolean, isDate, isFunction, isNumber, isObject, isSet, isString, isValidRegex } from './typeChecks';
import type { HelperExamples } from '../../scripts/examples/types';
const examples: HelperExamples = {
diff --git a/helpers/type/typeChecks.test.ts b/helpers/type/typeChecks.test.ts
index a86fcae..8d1929b 100644
--- a/helpers/type/typeChecks.test.ts
+++ b/helpers/type/typeChecks.test.ts
@@ -6,14 +6,14 @@
import { describe, expect, it } from "vitest";
import {
- isSet,
- isString,
- isNumber,
- isBoolean,
isArray,
- isObject,
- isFunction,
+ isBoolean,
isDate,
+ isFunction,
+ isNumber,
+ isObject,
+ isSet,
+ isString,
isValidRegex
} from "./typeChecks";
diff --git a/helpers/url/cleanPath.test.ts b/helpers/url/cleanPath.test.ts
index 2a4dfc7..df5a2a1 100644
--- a/helpers/url/cleanPath.test.ts
+++ b/helpers/url/cleanPath.test.ts
@@ -8,7 +8,7 @@
* This program is under the terms of the GNU Lesser General Public License version 3
* The full license information can be found in LICENSE in the root directory of this project.
*/
-import { expect, test, describe } from "vitest";
+import { describe, expect, test } from "vitest";
import { cleanPath } from './cleanPath';
describe('cleanPath', () => {
diff --git a/helpers/url/extractPureURI.test.ts b/helpers/url/extractPureURI.test.ts
index 5291fda..6c3fbbb 100644
--- a/helpers/url/extractPureURI.test.ts
+++ b/helpers/url/extractPureURI.test.ts
@@ -8,7 +8,7 @@
* This program is under the terms of the GNU Lesser General Public License version 3
* The full license information can be found in LICENSE in the root directory of this project.
*/
-import { expect, test, describe } from "vitest";
+import { describe, expect, test } from "vitest";
import { extractPureURI } from "./extractPureURI";
describe('extractPureURI', () => {
diff --git a/helpers/url/onlyPath.test.ts b/helpers/url/onlyPath.test.ts
index d4b2708..2ed3938 100644
--- a/helpers/url/onlyPath.test.ts
+++ b/helpers/url/onlyPath.test.ts
@@ -8,7 +8,7 @@
* This program is under the terms of the GNU Lesser General Public License version 3
* The full license information can be found in LICENSE in the root directory of this project.
*/
-import { expect, test, describe } from "vitest";
+import { describe, expect, test } from "vitest";
import { onlyPath } from './onlyPath';
describe('onlyPath', () => {
diff --git a/helpers/url/relativeURLToAbsolute.test.ts b/helpers/url/relativeURLToAbsolute.test.ts
index caace62..82ecd89 100644
--- a/helpers/url/relativeURLToAbsolute.test.ts
+++ b/helpers/url/relativeURLToAbsolute.test.ts
@@ -8,7 +8,7 @@
* This program is under the terms of the GNU Lesser General Public License version 3
* The full license information can be found in LICENSE in the root directory of this project.
*/
-import { expect, test, describe } from "vitest";
+import { describe, expect, test } from "vitest";
import { relativeURLToAbsolute } from "./relativeURLToAbsolute";
describe('relativeURLToAbsolute', () => {
diff --git a/helpers/url/withLeadingSlash.test.ts b/helpers/url/withLeadingSlash.test.ts
index 1b72064..bc3b67a 100644
--- a/helpers/url/withLeadingSlash.test.ts
+++ b/helpers/url/withLeadingSlash.test.ts
@@ -8,7 +8,7 @@
* This program is under the terms of the GNU Lesser General Public License version 3
* The full license information can be found in LICENSE in the root directory of this project.
*/
-import { expect, test, describe } from "vitest";
+import { describe, expect, test } from "vitest";
import { withLeadingSlash } from './withLeadingSlash';
describe('withLeadingSlash', () => {
diff --git a/helpers/url/withTrailingSlash.test.ts b/helpers/url/withTrailingSlash.test.ts
index de40257..c97fdf4 100644
--- a/helpers/url/withTrailingSlash.test.ts
+++ b/helpers/url/withTrailingSlash.test.ts
@@ -8,7 +8,7 @@
* This program is under the terms of the GNU Lesser General Public License version 3
* The full license information can be found in LICENSE in the root directory of this project.
*/
-import { expect, test, describe } from "vitest";
+import { describe, expect, test } from "vitest";
import { withTrailingSlash } from './withTrailingSlash';
describe('withTrailingSlash', () => {
diff --git a/helpers/url/withoutLeadingSlash.test.ts b/helpers/url/withoutLeadingSlash.test.ts
index acdb5a3..2d9c5da 100644
--- a/helpers/url/withoutLeadingSlash.test.ts
+++ b/helpers/url/withoutLeadingSlash.test.ts
@@ -8,7 +8,7 @@
* This program is under the terms of the GNU Lesser General Public License version 3
* The full license information can be found in LICENSE in the root directory of this project.
*/
-import { expect, test, describe } from "vitest";
+import { describe, expect, test } from "vitest";
import { withoutLeadingSlash } from './withoutLeadingSlash';
describe('withoutLeadingSlash', () => {
diff --git a/helpers/url/withoutTrailingSlash.test.ts b/helpers/url/withoutTrailingSlash.test.ts
index 8550a71..de25aef 100644
--- a/helpers/url/withoutTrailingSlash.test.ts
+++ b/helpers/url/withoutTrailingSlash.test.ts
@@ -8,7 +8,7 @@
* This program is under the terms of the GNU Lesser General Public License version 3
* The full license information can be found in LICENSE in the root directory of this project.
*/
-import { expect, test, describe } from "vitest";
+import { describe, expect, test } from "vitest";
import { withoutTrailingSlash } from './withoutTrailingSlash';
describe('withoutTrailingSlash', () => {
diff --git a/helpers/version/stripV.test.ts b/helpers/version/stripV.test.ts
index 415e732..dddc7ef 100644
--- a/helpers/version/stripV.test.ts
+++ b/helpers/version/stripV.test.ts
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: LGPL-3.0-or-later
*/
-import { describe, it, expect } from "vitest";
+import { describe, expect, it } from "vitest";
import { stripV } from "./stripV";
describe("stripV", () => {
diff --git a/package.json b/package.json
index c42fbc6..9ea83a7 100644
--- a/package.json
+++ b/package.json
@@ -79,7 +79,7 @@
"tsx": "^4.19.2",
"typedoc": "^0.28.18",
"typescript": "^6.0.2",
- "vite": "^8.0.3",
+ "vite": "^8.0.7",
"vitest": "^4.1.2"
},
"packageManager": "pnpm@10.28.2+sha512.41872f037ad22f7348e3b1debbaf7e867cfd448f2726d9cf74c08f19507c31d2c8e7a11525b983febc2df640b5438dee6023ebb1f84ed43cc2d654d2bc326264",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 3f558cf..92b098a 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -23,7 +23,7 @@ importers:
version: 9.6.0(@types/node@25.5.2)
'@stryker-mutator/vitest-runner':
specifier: ^9.6.0
- version: 9.6.0(@stryker-mutator/core@9.6.0(@types/node@25.5.2))(vitest@4.1.2(@types/node@25.5.2)(happy-dom@20.8.9)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.2)(esbuild@0.27.2)(tsx@4.21.0)(yaml@2.8.3)))
+ version: 9.6.0(@stryker-mutator/core@9.6.0(@types/node@25.5.2))(vitest@4.1.2(@types/node@25.5.2)(happy-dom@20.8.9)(vite@8.0.7(@types/node@25.5.2)(esbuild@0.27.2)(tsx@4.21.0)(yaml@2.8.3)))
'@types/fs-extra':
specifier: ^11.0.4
version: 11.0.4
@@ -35,7 +35,7 @@ importers:
version: 7.0.0-dev.20260407.1
'@vitest/coverage-v8':
specifier: ^4.1.2
- version: 4.1.2(vitest@4.1.2(@types/node@25.5.2)(happy-dom@20.8.9)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.2)(esbuild@0.27.2)(tsx@4.21.0)(yaml@2.8.3)))
+ version: 4.1.2(vitest@4.1.2(@types/node@25.5.2)(happy-dom@20.8.9)(vite@8.0.7(@types/node@25.5.2)(esbuild@0.27.2)(tsx@4.21.0)(yaml@2.8.3)))
fs-extra:
specifier: ^11.3.4
version: 11.3.4
@@ -64,11 +64,11 @@ importers:
specifier: ^6.0.2
version: 6.0.2
vite:
- specifier: ^8.0.3
- version: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.2)(esbuild@0.27.2)(tsx@4.21.0)(yaml@2.8.3)
+ specifier: ^8.0.7
+ version: 8.0.7(@types/node@25.5.2)(esbuild@0.27.2)(tsx@4.21.0)(yaml@2.8.3)
vitest:
specifier: ^4.1.2
- version: 4.1.2(@types/node@25.5.2)(happy-dom@20.8.9)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.2)(esbuild@0.27.2)(tsx@4.21.0)(yaml@2.8.3))
+ version: 4.1.2(@types/node@25.5.2)(happy-dom@20.8.9)(vite@8.0.7(@types/node@25.5.2)(esbuild@0.27.2)(tsx@4.21.0)(yaml@2.8.3))
packages:
@@ -558,8 +558,8 @@ packages:
'@emnapi/core': ^1.7.1
'@emnapi/runtime': ^1.7.1
- '@oxc-project/types@0.122.0':
- resolution: {integrity: sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==}
+ '@oxc-project/types@0.123.0':
+ resolution: {integrity: sha512-YtECP/y8Mj1lSHiUWGSRzy/C6teUKlS87dEfuVKT09LgQbUsBW1rNg+MiJ4buGu3yuADV60gbIvo9/HplA56Ew==}
'@oxlint/binding-android-arm-eabi@1.58.0':
resolution: {integrity: sha512-1T7UN3SsWWxpWyWGn1cT3ASNJOo+pI3eUkmEl7HgtowapcV8kslYpFQcYn431VuxghXakPNlbjRwhqmR37PFOg==}
@@ -683,103 +683,103 @@ packages:
cpu: [x64]
os: [win32]
- '@rolldown/binding-android-arm64@1.0.0-rc.12':
- resolution: {integrity: sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==}
+ '@rolldown/binding-android-arm64@1.0.0-rc.13':
+ resolution: {integrity: sha512-5ZiiecKH2DXAVJTNN13gNMUcCDg4Jy8ZjbXEsPnqa248wgOVeYRX0iqXXD5Jz4bI9BFHgKsI2qmyJynstbmr+g==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [android]
- '@rolldown/binding-darwin-arm64@1.0.0-rc.12':
- resolution: {integrity: sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==}
+ '@rolldown/binding-darwin-arm64@1.0.0-rc.13':
+ resolution: {integrity: sha512-tz/v/8G77seu8zAB3A5sK3UFoOl06zcshEzhUO62sAEtrEuW/H1CcyoupOrD+NbQJytYgA4CppXPzlrmp4JZKA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [darwin]
- '@rolldown/binding-darwin-x64@1.0.0-rc.12':
- resolution: {integrity: sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==}
+ '@rolldown/binding-darwin-x64@1.0.0-rc.13':
+ resolution: {integrity: sha512-8DakphqOz8JrMYWTJmWA+vDJxut6LijZ8Xcdc4flOlAhU7PNVwo2MaWBF9iXjJAPo5rC/IxEFZDhJ3GC7NHvug==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [darwin]
- '@rolldown/binding-freebsd-x64@1.0.0-rc.12':
- resolution: {integrity: sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==}
+ '@rolldown/binding-freebsd-x64@1.0.0-rc.13':
+ resolution: {integrity: sha512-4wBQFfjDuXYN/SVI8inBF3Aa+isq40rc6VMFbk5jcpolUBTe5cYnMsHZ51nFWsx3PVyyNN3vgoESki0Hmr/4BA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [freebsd]
- '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12':
- resolution: {integrity: sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==}
+ '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.13':
+ resolution: {integrity: sha512-JW/e4yPIXLms+jmnbwwy5LA/LxVwZUWLN8xug+V200wzaVi5TEGIWQlh8o91gWYFxW609euI98OCCemmWGuPrw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [linux]
- '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12':
- resolution: {integrity: sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==}
+ '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.13':
+ resolution: {integrity: sha512-ZfKWpXiUymDnavepCaM6KG/uGydJ4l2nBmMxg60Ci4CbeefpqjPWpfaZM7PThOhk2dssqBAcwLc6rAyr0uTdXg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
- '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12':
- resolution: {integrity: sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==}
+ '@rolldown/binding-linux-arm64-musl@1.0.0-rc.13':
+ resolution: {integrity: sha512-bmRg3O6Z0gq9yodKKWCIpnlH051sEfdVwt+6m5UDffAQMUUqU0xjnQqqAUm+Gu7ofAAly9DqiQDtKu2nPDEABA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [musl]
- '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12':
- resolution: {integrity: sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==}
+ '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.13':
+ resolution: {integrity: sha512-8Wtnbw4k7pMYN9B/mOEAsQ8HOiq7AZ31Ig4M9BKn2So4xRaFEhtCSa4ZJaOutOWq50zpgR4N5+L/opnlaCx8wQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ppc64]
os: [linux]
libc: [glibc]
- '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12':
- resolution: {integrity: sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==}
+ '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.13':
+ resolution: {integrity: sha512-D/0Nlo8mQuxSMohNJUF2lDXWRsFDsHldfRRgD9bRgktj+EndGPj4DOV37LqDKPYS+osdyhZEH7fTakTAEcW7qg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
- '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12':
- resolution: {integrity: sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==}
+ '@rolldown/binding-linux-x64-gnu@1.0.0-rc.13':
+ resolution: {integrity: sha512-eRrPvat2YaVQcwwKi/JzOP6MKf1WRnOCr+VaI3cTWz3ZoLcP/654z90lVCJ4dAuMEpPdke0n+qyAqXDZdIC4rA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [glibc]
- '@rolldown/binding-linux-x64-musl@1.0.0-rc.12':
- resolution: {integrity: sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==}
+ '@rolldown/binding-linux-x64-musl@1.0.0-rc.13':
+ resolution: {integrity: sha512-PsdONiFRp8hR8KgVjTWjZ9s7uA3uueWL0t74/cKHfM4dR5zXYv4AjB8BvA+QDToqxAFg4ZkcVEqeu5F7inoz5w==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [musl]
- '@rolldown/binding-openharmony-arm64@1.0.0-rc.12':
- resolution: {integrity: sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==}
+ '@rolldown/binding-openharmony-arm64@1.0.0-rc.13':
+ resolution: {integrity: sha512-hCNXgC5dI3TVOLrPT++PKFNZ+1EtS0mLQwfXXXSUD/+rGlB65gZDwN/IDuxLpQP4x8RYYHqGomlUXzpO8aVI2w==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [openharmony]
- '@rolldown/binding-wasm32-wasi@1.0.0-rc.12':
- resolution: {integrity: sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==}
+ '@rolldown/binding-wasm32-wasi@1.0.0-rc.13':
+ resolution: {integrity: sha512-viLS5C5et8NFtLWw9Sw3M/w4vvnVkbWkO7wSNh3C+7G1+uCkGpr6PcjNDSFcNtmXY/4trjPBqUfcOL+P3sWy/g==}
engines: {node: '>=14.0.0'}
cpu: [wasm32]
- '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12':
- resolution: {integrity: sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==}
+ '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.13':
+ resolution: {integrity: sha512-Fqa3Tlt1xL4wzmAYxGNFV36Hb+VfPc9PYU+E25DAnswXv3ODDu/yyWjQDbXMo5AGWkQVjLgQExuVu8I/UaZhPQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [win32]
- '@rolldown/binding-win32-x64-msvc@1.0.0-rc.12':
- resolution: {integrity: sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==}
+ '@rolldown/binding-win32-x64-msvc@1.0.0-rc.13':
+ resolution: {integrity: sha512-/pLI5kPkGEi44TDlnbio3St/5gUFeN51YWNAk/Gnv6mEQBOahRBh52qVFVBpmrnU01n2yysvBML9Ynu7K4kGAQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [win32]
- '@rolldown/pluginutils@1.0.0-rc.12':
- resolution: {integrity: sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==}
+ '@rolldown/pluginutils@1.0.0-rc.13':
+ resolution: {integrity: sha512-3ngTAv6F/Py35BsYbeeLeecvhMKdsKm4AoOETVhAA+Qc8nrA2I0kF7oa93mE9qnIurngOSpMnQ0x2nQY2FPviA==}
'@rollup/rollup-android-arm-eabi@4.60.1':
resolution: {integrity: sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==}
@@ -1604,8 +1604,8 @@ packages:
resolve-pkg-maps@1.0.0:
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
- rolldown@1.0.0-rc.12:
- resolution: {integrity: sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==}
+ rolldown@1.0.0-rc.13:
+ resolution: {integrity: sha512-bvVj8YJmf0rq4pSFmH7laLa6pYrhghv3PRzrCdRAr23g66zOKVJ4wkvFtgohtPLWmthgg8/rkaqRHrpUEh0Zbw==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
@@ -1771,14 +1771,14 @@ packages:
peerDependencies:
browserslist: '>= 4.21.0'
- vite@8.0.3:
- resolution: {integrity: sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==}
+ vite@8.0.7:
+ resolution: {integrity: sha512-P1PbweD+2/udplnThz3btF4cf6AgPky7kk23RtHUkJIU5BIxwPprhRGmOAHs6FTI7UiGbTNrgNP6jSYD6JaRnw==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
peerDependencies:
'@types/node': ^20.19.0 || >=22.12.0
'@vitejs/devtools': ^0.1.0
- esbuild: ^0.27.0
+ esbuild: ^0.27.0 || ^0.28.0
jiti: '>=1.21.0'
less: ^4.0.0
sass: ^1.70.0
@@ -2364,7 +2364,7 @@ snapshots:
'@tybys/wasm-util': 0.10.1
optional: true
- '@oxc-project/types@0.122.0': {}
+ '@oxc-project/types@0.123.0': {}
'@oxlint/binding-android-arm-eabi@1.58.0':
optional: true
@@ -2423,57 +2423,56 @@ snapshots:
'@oxlint/binding-win32-x64-msvc@1.58.0':
optional: true
- '@rolldown/binding-android-arm64@1.0.0-rc.12':
+ '@rolldown/binding-android-arm64@1.0.0-rc.13':
optional: true
- '@rolldown/binding-darwin-arm64@1.0.0-rc.12':
+ '@rolldown/binding-darwin-arm64@1.0.0-rc.13':
optional: true
- '@rolldown/binding-darwin-x64@1.0.0-rc.12':
+ '@rolldown/binding-darwin-x64@1.0.0-rc.13':
optional: true
- '@rolldown/binding-freebsd-x64@1.0.0-rc.12':
+ '@rolldown/binding-freebsd-x64@1.0.0-rc.13':
optional: true
- '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12':
+ '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.13':
optional: true
- '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12':
+ '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.13':
optional: true
- '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12':
+ '@rolldown/binding-linux-arm64-musl@1.0.0-rc.13':
optional: true
- '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12':
+ '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.13':
optional: true
- '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12':
+ '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.13':
optional: true
- '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12':
+ '@rolldown/binding-linux-x64-gnu@1.0.0-rc.13':
optional: true
- '@rolldown/binding-linux-x64-musl@1.0.0-rc.12':
+ '@rolldown/binding-linux-x64-musl@1.0.0-rc.13':
optional: true
- '@rolldown/binding-openharmony-arm64@1.0.0-rc.12':
+ '@rolldown/binding-openharmony-arm64@1.0.0-rc.13':
optional: true
- '@rolldown/binding-wasm32-wasi@1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)':
+ '@rolldown/binding-wasm32-wasi@1.0.0-rc.13':
dependencies:
+ '@emnapi/core': 1.9.1
+ '@emnapi/runtime': 1.9.1
'@napi-rs/wasm-runtime': 1.1.2(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)
- transitivePeerDependencies:
- - '@emnapi/core'
- - '@emnapi/runtime'
optional: true
- '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12':
+ '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.13':
optional: true
- '@rolldown/binding-win32-x64-msvc@1.0.0-rc.12':
+ '@rolldown/binding-win32-x64-msvc@1.0.0-rc.13':
optional: true
- '@rolldown/pluginutils@1.0.0-rc.12': {}
+ '@rolldown/pluginutils@1.0.0-rc.13': {}
'@rollup/rollup-android-arm-eabi@4.60.1':
optional: true
@@ -2634,13 +2633,13 @@ snapshots:
'@stryker-mutator/util@9.6.0': {}
- '@stryker-mutator/vitest-runner@9.6.0(@stryker-mutator/core@9.6.0(@types/node@25.5.2))(vitest@4.1.2(@types/node@25.5.2)(happy-dom@20.8.9)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.2)(esbuild@0.27.2)(tsx@4.21.0)(yaml@2.8.3)))':
+ '@stryker-mutator/vitest-runner@9.6.0(@stryker-mutator/core@9.6.0(@types/node@25.5.2))(vitest@4.1.2(@types/node@25.5.2)(happy-dom@20.8.9)(vite@8.0.7(@types/node@25.5.2)(esbuild@0.27.2)(tsx@4.21.0)(yaml@2.8.3)))':
dependencies:
'@stryker-mutator/api': 9.6.0
'@stryker-mutator/core': 9.6.0(@types/node@25.5.2)
'@stryker-mutator/util': 9.6.0
tslib: 2.8.1
- vitest: 4.1.2(@types/node@25.5.2)(happy-dom@20.8.9)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.2)(esbuild@0.27.2)(tsx@4.21.0)(yaml@2.8.3))
+ vitest: 4.1.2(@types/node@25.5.2)(happy-dom@20.8.9)(vite@8.0.7(@types/node@25.5.2)(esbuild@0.27.2)(tsx@4.21.0)(yaml@2.8.3))
'@tybys/wasm-util@0.10.1':
dependencies:
@@ -2712,7 +2711,7 @@ snapshots:
'@typescript/native-preview-win32-arm64': 7.0.0-dev.20260407.1
'@typescript/native-preview-win32-x64': 7.0.0-dev.20260407.1
- '@vitest/coverage-v8@4.1.2(vitest@4.1.2(@types/node@25.5.2)(happy-dom@20.8.9)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.2)(esbuild@0.27.2)(tsx@4.21.0)(yaml@2.8.3)))':
+ '@vitest/coverage-v8@4.1.2(vitest@4.1.2(@types/node@25.5.2)(happy-dom@20.8.9)(vite@8.0.7(@types/node@25.5.2)(esbuild@0.27.2)(tsx@4.21.0)(yaml@2.8.3)))':
dependencies:
'@bcoe/v8-coverage': 1.0.2
'@vitest/utils': 4.1.2
@@ -2724,7 +2723,7 @@ snapshots:
obug: 2.1.1
std-env: 4.0.0
tinyrainbow: 3.1.0
- vitest: 4.1.2(@types/node@25.5.2)(happy-dom@20.8.9)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.2)(esbuild@0.27.2)(tsx@4.21.0)(yaml@2.8.3))
+ vitest: 4.1.2(@types/node@25.5.2)(happy-dom@20.8.9)(vite@8.0.7(@types/node@25.5.2)(esbuild@0.27.2)(tsx@4.21.0)(yaml@2.8.3))
'@vitest/expect@4.1.2':
dependencies:
@@ -2735,13 +2734,13 @@ snapshots:
chai: 6.2.2
tinyrainbow: 3.1.0
- '@vitest/mocker@4.1.2(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.2)(esbuild@0.27.2)(tsx@4.21.0)(yaml@2.8.3))':
+ '@vitest/mocker@4.1.2(vite@8.0.7(@types/node@25.5.2)(esbuild@0.27.2)(tsx@4.21.0)(yaml@2.8.3))':
dependencies:
'@vitest/spy': 4.1.2
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
- vite: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.2)(esbuild@0.27.2)(tsx@4.21.0)(yaml@2.8.3)
+ vite: 8.0.7(@types/node@25.5.2)(esbuild@0.27.2)(tsx@4.21.0)(yaml@2.8.3)
'@vitest/pretty-format@4.1.2':
dependencies:
@@ -3254,29 +3253,26 @@ snapshots:
resolve-pkg-maps@1.0.0: {}
- rolldown@1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1):
+ rolldown@1.0.0-rc.13:
dependencies:
- '@oxc-project/types': 0.122.0
- '@rolldown/pluginutils': 1.0.0-rc.12
+ '@oxc-project/types': 0.123.0
+ '@rolldown/pluginutils': 1.0.0-rc.13
optionalDependencies:
- '@rolldown/binding-android-arm64': 1.0.0-rc.12
- '@rolldown/binding-darwin-arm64': 1.0.0-rc.12
- '@rolldown/binding-darwin-x64': 1.0.0-rc.12
- '@rolldown/binding-freebsd-x64': 1.0.0-rc.12
- '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.12
- '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.12
- '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.12
- '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.12
- '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.12
- '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.12
- '@rolldown/binding-linux-x64-musl': 1.0.0-rc.12
- '@rolldown/binding-openharmony-arm64': 1.0.0-rc.12
- '@rolldown/binding-wasm32-wasi': 1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)
- '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.12
- '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.12
- transitivePeerDependencies:
- - '@emnapi/core'
- - '@emnapi/runtime'
+ '@rolldown/binding-android-arm64': 1.0.0-rc.13
+ '@rolldown/binding-darwin-arm64': 1.0.0-rc.13
+ '@rolldown/binding-darwin-x64': 1.0.0-rc.13
+ '@rolldown/binding-freebsd-x64': 1.0.0-rc.13
+ '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.13
+ '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.13
+ '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.13
+ '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.13
+ '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.13
+ '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.13
+ '@rolldown/binding-linux-x64-musl': 1.0.0-rc.13
+ '@rolldown/binding-openharmony-arm64': 1.0.0-rc.13
+ '@rolldown/binding-wasm32-wasi': 1.0.0-rc.13
+ '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.13
+ '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.13
rollup-plugin-dts@6.4.1(rollup@4.60.1)(typescript@6.0.2):
dependencies:
@@ -3447,12 +3443,12 @@ snapshots:
escalade: 3.2.0
picocolors: 1.1.1
- vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.2)(esbuild@0.27.2)(tsx@4.21.0)(yaml@2.8.3):
+ vite@8.0.7(@types/node@25.5.2)(esbuild@0.27.2)(tsx@4.21.0)(yaml@2.8.3):
dependencies:
lightningcss: 1.32.0
picomatch: 4.0.4
postcss: 8.5.8
- rolldown: 1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)
+ rolldown: 1.0.0-rc.13
tinyglobby: 0.2.15
optionalDependencies:
'@types/node': 25.5.2
@@ -3460,14 +3456,11 @@ snapshots:
fsevents: 2.3.3
tsx: 4.21.0
yaml: 2.8.3
- transitivePeerDependencies:
- - '@emnapi/core'
- - '@emnapi/runtime'
- vitest@4.1.2(@types/node@25.5.2)(happy-dom@20.8.9)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.2)(esbuild@0.27.2)(tsx@4.21.0)(yaml@2.8.3)):
+ vitest@4.1.2(@types/node@25.5.2)(happy-dom@20.8.9)(vite@8.0.7(@types/node@25.5.2)(esbuild@0.27.2)(tsx@4.21.0)(yaml@2.8.3)):
dependencies:
'@vitest/expect': 4.1.2
- '@vitest/mocker': 4.1.2(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.2)(esbuild@0.27.2)(tsx@4.21.0)(yaml@2.8.3))
+ '@vitest/mocker': 4.1.2(vite@8.0.7(@types/node@25.5.2)(esbuild@0.27.2)(tsx@4.21.0)(yaml@2.8.3))
'@vitest/pretty-format': 4.1.2
'@vitest/runner': 4.1.2
'@vitest/snapshot': 4.1.2
@@ -3484,7 +3477,7 @@ snapshots:
tinyexec: 1.0.4
tinyglobby: 0.2.15
tinyrainbow: 3.1.0
- vite: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.2)(esbuild@0.27.2)(tsx@4.21.0)(yaml@2.8.3)
+ vite: 8.0.7(@types/node@25.5.2)(esbuild@0.27.2)(tsx@4.21.0)(yaml@2.8.3)
why-is-node-running: 2.3.0
optionalDependencies:
'@types/node': 25.5.2
diff --git a/scripts/build/build-licenses.ts b/scripts/build/build-licenses.ts
new file mode 100644
index 0000000..f5ce2bf
--- /dev/null
+++ b/scripts/build/build-licenses.ts
@@ -0,0 +1,68 @@
+/**
+ * This file is part of helpers4.
+ * Copyright (C) 2025 baxyz
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
+import { join } from 'node:path';
+import { DIR } from '../constants';
+import { readFileJson, writeFile } from '../utils';
+import { getExternalDependencies } from './helpers/get-external-dependencies.helper';
+
+interface DependencyLicense {
+ readonly name: string;
+ readonly license: string;
+ readonly homepage?: string;
+ readonly repository?: string;
+}
+
+interface CategoryLicensesJson {
+ readonly category: string;
+ readonly dependencies: readonly DependencyLicense[];
+}
+
+/**
+ * Read license metadata from a dependency's package.json in node_modules.
+ */
+function readDependencyLicense(packageName: string): DependencyLicense {
+ const pkgJsonPath = join(DIR.ROOT, 'node_modules', packageName, 'package.json');
+ const pkg = readFileJson>(pkgJsonPath);
+
+ const repository = typeof pkg.repository === 'string'
+ ? pkg.repository
+ : (pkg.repository as Record | undefined)?.url;
+
+ // Strip git+ prefix and .git suffix for clean URLs
+ const cleanRepo = repository
+ ?.replace(/^git\+/, '')
+ ?.replace(/\.git$/, '');
+
+ const license = typeof pkg.license === 'string'
+ ? pkg.license
+ : (pkg.license as Record | undefined)?.type ?? 'UNKNOWN';
+
+ return {
+ name: packageName,
+ license,
+ ...(pkg.homepage ? { homepage: pkg.homepage as string } : {}),
+ ...(cleanRepo ? { repository: cleanRepo } : {}),
+ };
+}
+
+/**
+ * Generates a `licenses.json` file in each built category directory.
+ * Lists third-party dependencies with their license field from package.json.
+ *
+ * @param validCategories - Categories that were successfully built
+ */
+export async function buildLicenses(validCategories: string[]): Promise {
+ for (const category of validCategories) {
+ const externalDeps = await getExternalDependencies(category);
+
+ const dependencies = externalDeps.map(readDependencyLicense);
+
+ const json: CategoryLicensesJson = { category, dependencies };
+ const outputPath = join(DIR.BUILD, category, 'licenses.json');
+ writeFile(outputPath, JSON.stringify(json, null, 2));
+ }
+}
diff --git a/scripts/build/index.ts b/scripts/build/index.ts
index a7d53b9..b923659 100644
--- a/scripts/build/index.ts
+++ b/scripts/build/index.ts
@@ -10,6 +10,7 @@ import { buildCategories } from "./build-categories";
import { buildBundle } from "./build-bundle";
import { buildExamples } from "./build-examples";
import { buildApiDocs } from "./build-api-docs";
+import { buildLicenses } from "./build-licenses";
async function main() {
// Create or empty the /build directory
@@ -27,6 +28,10 @@ async function main() {
await buildApiDocs(validCategories);
console.info(" ✔️📖 Built API docs");
+ // Generate licenses.json for each category
+ await buildLicenses(validCategories);
+ console.info(" ✔️⚖️ Built licenses");
+
// Build the bundle package with all valid categories
await buildBundle(validCategories);
}
diff --git a/scripts/publish/index.ts b/scripts/publish/index.ts
index 79d0f40..b7cf65f 100644
--- a/scripts/publish/index.ts
+++ b/scripts/publish/index.ts
@@ -8,10 +8,10 @@
import path from 'path';
import {
- checkNpmAuth,
- publishPackage,
PublishOptions,
- PublishResult
+ PublishResult,
+ checkNpmAuth,
+ publishPackage
} from './helpers/npm-utils';
import {
discoverPackages,
diff --git a/scripts/version/version-manager.ts b/scripts/version/version-manager.ts
index 1b52318..c53af62 100644
--- a/scripts/version/version-manager.ts
+++ b/scripts/version/version-manager.ts
@@ -8,7 +8,7 @@
import fs from 'fs-extra';
import path from 'path';
-import { calculateVersionFromCommits, promptVersionType, VersionType } from './commit-analyzer';
+import { VersionType, calculateVersionFromCommits, promptVersionType } from './commit-analyzer';
interface VersionComponents {
major: number;