From 6af04fcb65b391bb3819988c18c2980f18fe307b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Zori=C4=87?= Date: Sat, 9 May 2026 21:56:06 +0200 Subject: [PATCH 1/2] feat(stdlib/common): add toBoolean, isTruthy, isFalsy utilities Replaces the `boolean` npm package with a native @webiny/stdlib implementation. Exact coercion parity: truthy strings are "true", "t", "yes", "y", "on", "1" (case-insensitive, trimmed); truthy number is 1 only; booleans pass through. isTruthy and isFalsy are readable wrappers on top. Co-Authored-By: Claude Sonnet 4.6 --- __tests__/boolean.test.ts | 87 ++++++ .../plans/2026-05-09-boolean-utility.md | 262 ++++++++++++++++++ .../2026-05-09-boolean-utility-design.md | 68 +++++ src/common/index.ts | 1 + src/common/utils/boolean.ts | 32 +++ 5 files changed, 450 insertions(+) create mode 100644 __tests__/boolean.test.ts create mode 100644 docs/superpowers/plans/2026-05-09-boolean-utility.md create mode 100644 docs/superpowers/specs/2026-05-09-boolean-utility-design.md create mode 100644 src/common/utils/boolean.ts diff --git a/__tests__/boolean.test.ts b/__tests__/boolean.test.ts new file mode 100644 index 0000000..0c1ccfa --- /dev/null +++ b/__tests__/boolean.test.ts @@ -0,0 +1,87 @@ +import { describe, expect, it } from "vitest"; +import { toBoolean, isTruthy, isFalsy } from "../src/common/utils/boolean.js"; + +describe("toBoolean", () => { + describe("strings — truthy set", () => { + it.each(["true", "t", "yes", "y", "on", "1"])('returns true for "%s"', value => { + expect(toBoolean(value)).toBe(true); + }); + + it.each(["TRUE", "True", "YES", "Yes", "ON", "On", "T", "Y"])( + 'returns true for uppercase/mixed "%s"', + value => { + expect(toBoolean(value)).toBe(true); + } + ); + + it.each([" true ", " 1 ", " yes "])('returns true for whitespace-padded "%s"', value => { + expect(toBoolean(value)).toBe(true); + }); + }); + + describe("strings — falsy set", () => { + it.each(["false", "f", "no", "n", "off", "0", "", "banana", "2", "null"])( + 'returns false for "%s"', + value => { + expect(toBoolean(value)).toBe(false); + } + ); + }); + + describe("numbers", () => { + it("returns true for 1", () => { + expect(toBoolean(1)).toBe(true); + }); + + it.each([0, 2, -1, 100, NaN])("returns false for %s", value => { + expect(toBoolean(value)).toBe(false); + }); + }); + + describe("booleans", () => { + it("returns true for true", () => { + expect(toBoolean(true)).toBe(true); + }); + + it("returns false for false", () => { + expect(toBoolean(false)).toBe(false); + }); + }); + + describe("other types", () => { + it.each([null, undefined, {}, [], () => {}, Symbol("x")])( + "returns false for non-string/number/boolean values", + value => { + expect(toBoolean(value)).toBe(false); + } + ); + }); +}); + +describe("isTruthy", () => { + it("mirrors toBoolean for a truthy value", () => { + expect(isTruthy("true")).toBe(true); + expect(isTruthy(1)).toBe(true); + expect(isTruthy(true)).toBe(true); + }); + + it("mirrors toBoolean for a falsy value", () => { + expect(isTruthy("false")).toBe(false); + expect(isTruthy(0)).toBe(false); + expect(isTruthy(null)).toBe(false); + }); +}); + +describe("isFalsy", () => { + it("is the inverse of toBoolean for a truthy value", () => { + expect(isFalsy("true")).toBe(false); + expect(isFalsy(1)).toBe(false); + expect(isFalsy(true)).toBe(false); + }); + + it("is the inverse of toBoolean for a falsy value", () => { + expect(isFalsy("false")).toBe(true); + expect(isFalsy(0)).toBe(true); + expect(isFalsy(null)).toBe(true); + }); +}); diff --git a/docs/superpowers/plans/2026-05-09-boolean-utility.md b/docs/superpowers/plans/2026-05-09-boolean-utility.md new file mode 100644 index 0000000..853e283 --- /dev/null +++ b/docs/superpowers/plans/2026-05-09-boolean-utility.md @@ -0,0 +1,262 @@ +# Boolean Utility Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add `toBoolean`, `isTruthy`, and `isFalsy` pure utility functions to `@webiny/stdlib` as a drop-in replacement for the `boolean` npm package. + +**Architecture:** Three plain exported functions live in a new `src/common/utils/boolean.ts` file. `toBoolean` implements exact parity with the `boolean` package using `Object.prototype.toString` dispatch. `isTruthy` and `isFalsy` delegate to it. All three are re-exported from the existing `src/common/index.ts` barrel. + +**Tech Stack:** TypeScript (tsgo / `@typescript/native-preview`), Vitest for tests. + +--- + +## File Map + +| Action | Path | Responsibility | +|---|---|---| +| Create | `src/common/utils/boolean.ts` | The three utility functions | +| Modify | `src/common/index.ts` | Re-export `toBoolean`, `isTruthy`, `isFalsy` | +| Create | `__tests__/boolean.test.ts` | Full test coverage | + +--- + +### Task 1: Write the failing tests + +**Files:** +- Create: `__tests__/boolean.test.ts` + +- [ ] **Step 1: Create `__tests__/boolean.test.ts`** with the full test suite + +```ts +import { describe, expect, it } from "vitest"; +import { toBoolean, isTruthy, isFalsy } from "@webiny/stdlib"; + +describe("toBoolean", () => { + describe("strings — truthy set", () => { + it.each(["true", "t", "yes", "y", "on", "1"])( + 'returns true for "%s"', + value => { + expect(toBoolean(value)).toBe(true); + } + ); + + it.each(["TRUE", "True", "YES", "Yes", "ON", "On", "T", "Y"])( + 'returns true for uppercase/mixed "%s"', + value => { + expect(toBoolean(value)).toBe(true); + } + ); + + it.each([" true ", " 1 ", " yes "])( + 'returns true for whitespace-padded "%s"', + value => { + expect(toBoolean(value)).toBe(true); + } + ); + }); + + describe("strings — falsy set", () => { + it.each(["false", "f", "no", "n", "off", "0", "", "banana", "2", "null"])( + 'returns false for "%s"', + value => { + expect(toBoolean(value)).toBe(false); + } + ); + }); + + describe("numbers", () => { + it("returns true for 1", () => { + expect(toBoolean(1)).toBe(true); + }); + + it.each([0, 2, -1, 100, NaN])( + "returns false for %s", + value => { + expect(toBoolean(value)).toBe(false); + } + ); + }); + + describe("booleans", () => { + it("returns true for true", () => { + expect(toBoolean(true)).toBe(true); + }); + + it("returns false for false", () => { + expect(toBoolean(false)).toBe(false); + }); + }); + + describe("other types", () => { + it.each([null, undefined, {}, [], () => {}, Symbol("x")])( + "returns false for non-string/number/boolean values", + value => { + expect(toBoolean(value)).toBe(false); + } + ); + }); +}); + +describe("isTruthy", () => { + it("mirrors toBoolean for a truthy value", () => { + expect(isTruthy("true")).toBe(true); + expect(isTruthy(1)).toBe(true); + expect(isTruthy(true)).toBe(true); + }); + + it("mirrors toBoolean for a falsy value", () => { + expect(isTruthy("false")).toBe(false); + expect(isTruthy(0)).toBe(false); + expect(isTruthy(null)).toBe(false); + }); +}); + +describe("isFalsy", () => { + it("is the inverse of toBoolean for a truthy value", () => { + expect(isFalsy("true")).toBe(false); + expect(isFalsy(1)).toBe(false); + expect(isFalsy(true)).toBe(false); + }); + + it("is the inverse of toBoolean for a falsy value", () => { + expect(isFalsy("false")).toBe(true); + expect(isFalsy(0)).toBe(true); + expect(isFalsy(null)).toBe(true); + }); +}); +``` + +- [ ] **Step 2: Run the tests to verify they fail** + +```sh +yarn test --reporter=verbose __tests__/boolean.test.ts +``` + +Expected: all tests fail with `toBoolean is not a function` (or similar import error). + +--- + +### Task 2: Implement the utility functions + +**Files:** +- Create: `src/common/utils/boolean.ts` + +- [ ] **Step 1: Create `src/common/utils/boolean.ts`** + +```ts +/** + * Coerces a value to boolean with the same rules as the `boolean` npm package. + * + * Truthy strings (case-insensitive, trimmed): "true", "t", "yes", "y", "on", "1". + * Truthy number: 1 only. + * Booleans pass through. + * Everything else returns false. + */ +export function toBoolean(value: unknown): boolean { + switch (Object.prototype.toString.call(value)) { + case "[object String]": + return ["true", "t", "yes", "y", "on", "1"].includes( + (value as string).trim().toLowerCase() + ); + case "[object Number]": + return (value as number).valueOf() === 1; + case "[object Boolean]": + return (value as boolean).valueOf(); + default: + return false; + } +} + +/** Returns `toBoolean(value)`. Readable alias for use in predicates. */ +export function isTruthy(value: unknown): boolean { + return toBoolean(value); +} + +/** Returns `!toBoolean(value)`. Readable inverse of `isTruthy`. */ +export function isFalsy(value: unknown): boolean { + return !toBoolean(value); +} +``` + +- [ ] **Step 2: Run the tests to verify they pass** + +```sh +yarn test --reporter=verbose __tests__/boolean.test.ts +``` + +Expected: all tests pass. + +--- + +### Task 3: Export from the common barrel + +**Files:** +- Modify: `src/common/index.ts` + +- [ ] **Step 1: Add exports to `src/common/index.ts`** + +Add this line at the end of the file: + +```ts +export { toBoolean, isTruthy, isFalsy } from "./utils/boolean.js"; +``` + +The full file should now look like: + +```ts +export { Result, ResultAsync, BaseError, createAbstraction, createFeature } from "./core/index.js"; +export type { ErrorInput } from "./core/index.js"; +export { Logger, type ILogger } from "./features/Logger/abstractions/Logger.js"; +export { ConsoleLoggerConfig } from "./features/Logger/abstractions/ConsoleLoggerConfig.js"; +export { ConsoleLoggerFeature } from "./features/Logger/feature.js"; +export { ConsoleLogger } from "./features/Logger/ConsoleLogger.js"; +export { + Cache, + AsyncCache, + CacheError, + MemoryCacheFeature, + AsyncMemoryCacheFeature +} from "./features/Cache/index.js"; +export type { ICache, IAsyncCache } from "./features/Cache/index.js"; +export { toBoolean, isTruthy, isFalsy } from "./utils/boolean.js"; +``` + +- [ ] **Step 2: Run the full test suite to confirm nothing regressed** + +```sh +yarn test +``` + +Expected: all tests pass. + +--- + +### Task 4: Run the full pre-commit chain and commit + +**Files:** none new — validation only + +- [ ] **Step 1: Run the full pre-commit chain** + +```sh +yarn format:fix && yarn lint:fix && yarn typecheck && yarn build && yarn test:coverage +``` + +Expected: all five steps exit with code 0, zero errors, zero warnings. + +- [ ] **Step 2: Stage and commit** + +```sh +git add src/common/utils/boolean.ts src/common/index.ts __tests__/boolean.test.ts docs/superpowers/specs/2026-05-09-boolean-utility-design.md docs/superpowers/plans/2026-05-09-boolean-utility.md +git commit -m "$(cat <<'EOF' +feat(stdlib/common): add toBoolean, isTruthy, isFalsy utilities + +Replaces the `boolean` npm package with a native @webiny/stdlib +implementation. Exact coercion parity: truthy strings are +"true", "t", "yes", "y", "on", "1" (case-insensitive, trimmed); +truthy number is 1 only; booleans pass through. +isTruthy and isFalsy are readable wrappers on top. + +Co-Authored-By: Claude Sonnet 4.6 +EOF +)" +``` diff --git a/docs/superpowers/specs/2026-05-09-boolean-utility-design.md b/docs/superpowers/specs/2026-05-09-boolean-utility-design.md new file mode 100644 index 0000000..a3fe41a --- /dev/null +++ b/docs/superpowers/specs/2026-05-09-boolean-utility-design.md @@ -0,0 +1,68 @@ +# Boolean Utility Design + +**Date:** 2026-05-09 +**Package:** `@webiny/stdlib` (common slice) +**Status:** Approved + +## Goal + +Replace `import { boolean } from "boolean"` with a native `@webiny/stdlib` utility. Add `isTruthy` and `isFalsy` as convenience wrappers on top. + +## API + +Three plain exported functions — no DI, no classes, no `Result` wrapping. + +```ts +function toBoolean(value: unknown): boolean +function isTruthy(value: unknown): boolean +function isFalsy(value: unknown): boolean +``` + +### `toBoolean(value: unknown): boolean` + +Exact parity with the `boolean` npm package. Uses `Object.prototype.toString` for type dispatch: + +| Input type | Truthy values | All others | +|---|---|---| +| String (trimmed, lowercased) | `"true"`, `"t"`, `"yes"`, `"y"`, `"on"`, `"1"` | `false` | +| Number | `1` | `false` | +| Boolean | `true` | `false` | +| Anything else | — | `false` | + +### `isTruthy(value: unknown): boolean` + +Returns `toBoolean(value)`. Readable alias for use in filter predicates and condition checks. + +### `isFalsy(value: unknown): boolean` + +Returns `!toBoolean(value)`. Readable inverse. + +## File Layout + +``` +src/common/utils/boolean.ts ← new file +src/common/index.ts ← add exports for toBoolean, isTruthy, isFalsy +__tests__/boolean.test.ts ← new test file +``` + +No new `index.ts` barrel needed — single file, export directly from the common barrel. + +## Tests + +`__tests__/boolean.test.ts` covers: + +- All six truthy string variants: `"true"`, `"t"`, `"yes"`, `"y"`, `"on"`, `"1"` +- Case variants: `"TRUE"`, `"Yes"`, `"ON"` +- Whitespace: `" true "`, `" 1 "` +- Number `1` → `true`; numbers `0`, `2`, `-1` → `false` +- Boolean `true` → `true`; `false` → `false` +- `null`, `undefined`, `{}`, `[]`, `""` → `false` +- Unknown strings like `"banana"` → `false` +- `isTruthy` mirrors `toBoolean` +- `isFalsy` is the inverse + +## Out of Scope + +- `parseBoolean` returning `Result` — not needed for this replacement +- `isBoolean` type guard — not part of this spec +- Strict mode / throws on unknown input — lenient behaviour matches the `boolean` package diff --git a/src/common/index.ts b/src/common/index.ts index 703ab7b..3799fa6 100644 --- a/src/common/index.ts +++ b/src/common/index.ts @@ -12,3 +12,4 @@ export { AsyncMemoryCacheFeature } from "./features/Cache/index.js"; export type { ICache, IAsyncCache } from "./features/Cache/index.js"; +export { toBoolean, isTruthy, isFalsy } from "./utils/boolean.js"; diff --git a/src/common/utils/boolean.ts b/src/common/utils/boolean.ts new file mode 100644 index 0000000..dc4dbfa --- /dev/null +++ b/src/common/utils/boolean.ts @@ -0,0 +1,32 @@ +/** + * Coerces a value to boolean with the same rules as the `boolean` npm package. + * + * Truthy strings (case-insensitive, trimmed): "true", "t", "yes", "y", "on", "1". + * Truthy number: 1 only. + * Booleans pass through. + * Everything else returns false. + */ +export function toBoolean(value: unknown): boolean { + switch (Object.prototype.toString.call(value)) { + case "[object String]": + return ["true", "t", "yes", "y", "on", "1"].includes( + (value as string).trim().toLowerCase() + ); + case "[object Number]": + return (value as number).valueOf() === 1; + case "[object Boolean]": + return (value as boolean).valueOf(); + default: + return false; + } +} + +/** Returns `toBoolean(value)`. Readable alias for use in predicates. */ +export function isTruthy(value: unknown): boolean { + return toBoolean(value); +} + +/** Returns `!toBoolean(value)`. Readable inverse of `isTruthy`. */ +export function isFalsy(value: unknown): boolean { + return !toBoolean(value); +} From 76b8b4d1fa93fc10fad72167c2f4726fc7a4be80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Zori=C4=87?= Date: Sat, 9 May 2026 21:57:40 +0200 Subject: [PATCH 2/2] refactor(stdlib/common): trim boolean JSDoc, add Infinity test cases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit JSDoc on toBoolean was restating the visible implementation — collapsed to the parity claim only. Added Infinity/-Infinity to the number falsy set to close a regression gap in the Number branch. --- __tests__/boolean.test.ts | 2 +- src/common/utils/boolean.ts | 9 +-------- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/__tests__/boolean.test.ts b/__tests__/boolean.test.ts index 0c1ccfa..df46589 100644 --- a/__tests__/boolean.test.ts +++ b/__tests__/boolean.test.ts @@ -33,7 +33,7 @@ describe("toBoolean", () => { expect(toBoolean(1)).toBe(true); }); - it.each([0, 2, -1, 100, NaN])("returns false for %s", value => { + it.each([0, 2, -1, 100, NaN, Infinity, -Infinity])("returns false for %s", value => { expect(toBoolean(value)).toBe(false); }); }); diff --git a/src/common/utils/boolean.ts b/src/common/utils/boolean.ts index dc4dbfa..d4f4d4f 100644 --- a/src/common/utils/boolean.ts +++ b/src/common/utils/boolean.ts @@ -1,11 +1,4 @@ -/** - * Coerces a value to boolean with the same rules as the `boolean` npm package. - * - * Truthy strings (case-insensitive, trimmed): "true", "t", "yes", "y", "on", "1". - * Truthy number: 1 only. - * Booleans pass through. - * Everything else returns false. - */ +/** Exact semantic parity with the `boolean` npm package. */ export function toBoolean(value: unknown): boolean { switch (Object.prototype.toString.call(value)) { case "[object String]":