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 @@

npm version - npm pre-release npm downloads release status license 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;