diff --git a/.gitignore b/.gitignore index 12e590e..9dbcd1b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ node_modules/ *.log +*.local.md .env* .DS_Store dist/ diff --git a/README.md b/README.md index 8a193b8..34e65a2 100644 --- a/README.md +++ b/README.md @@ -9,13 +9,16 @@ A lightweight TypeScript library that converts any number into consistent, memorable codename from a curated word list. It's designed for use cases where human-readability is more important than collision resistance, such as generating preview URLs or readable test IDs. ```typescript -// Codename generator using the most memorable 20 cities -import codename from "codenames/cities-20"; +// Default: uses the cities-20 word list +import codename from "codenames"; -codename(1234); // "london", uses "cities" +codename(1234); // "london" codename(1234); // "london" (always the same) codename(5678); // "paris" codename(6789); // "berlin" + +// With custom words +codename(1234, ["one", "two", "three"]); // "two" ``` ## Features @@ -23,22 +26,39 @@ codename(6789); // "berlin" - **🎯 Deterministic** - Same input always produces the same codename - **💬 Human-Readable** - Memorable names instead of random strings - **🚀 Zero Dependencies** - Lightweight and fast with no external packages -- **⚡ Fast** - 50,000+ generations per second -- **📦 <3KB Core** - Ultra-minimal footprint, themes add ~2KB each +- **⚡ Fast & Tiny** - 50,000+ generations per second, <3KB core + ~2KB per theme - **🌐 Universal Runtime** - Works in Node.js, Bun, Deno, browsers, and edge runtimes - **🎨 Multiple Themes** - Cities, animals, colors, space, and more built-in themes -- **📦 Modern JavaScript** - TypeScript with full type safety, ESM package -- **📝 Customizable** - Create your own themes and word lists - **🤖 CLI Included** - Generate codenames directly from your terminal ## Use Cases -- **Preview Deployments** - Generate unique URLs for pull requests -- **Container Names** - Memorable Docker container identifiers -- **Session IDs** - User-friendly session identifiers for support -- **Feature Flags** - Human-friendly names for A/B tests -- **Test Environments** - Readable identifiers for testing pipelines -- **Test Data Generation** - Consistent test data generation +### Preview Deployments + +Managing preview environments can get messy. You end up tracking which PR is deployed where, maintaining state, dealing with conflicts. Here's a simpler approach: use deterministic hashing to map PR numbers to memorable names. + +```typescript +import codename from "codenames/cities-20"; + +// PR #1234 always maps to the same URL +const previewUrl = `https://${codename(1234)}.example.com`; +// => https://london.example.com + +// PR #5678 gets a different one +const anotherUrl = `https://${codename(5678)}.example.com`; +// => https://paris.example.com +``` + +With 20 city names, you get 20 deployment slots. No database needed. The same PR number always produces the same city name, so URLs stay consistent throughout the PR lifecycle. + +Plus, it's easier to share "london.example.com" in Slack than "preview-env-1234.k8s.us-east-1.example.com". + +### Other Uses + +- **Docker Containers**: `docker run --name "app-${codename(buildId)}" myapp` → `app-tokyo` +- **Session IDs**: `vienna-support` is friendlier than `sess_kJ8Hg2Bx9` +- **Feature Flags**: `berlin-experiment` instead of `experiment_42` +- **Test Data**: Generate predictable usernames (`cat`, `dog`, `bird`) ## Getting Started @@ -47,23 +67,82 @@ npm install codenames ``` ```typescript -// Import the codename generator using the top 20 -// words from the curated list of world cities -import codename from "codenames/cities-20"; +// Default: uses cities-20 word list +import codename from "codenames"; +// OR +import { codename } from "codenames"; -// Get a codename for the number 1234 const name = codename(1234); // "london" ``` -You can use different word lists and list sizes by importing the appropriate module: +### Using Different Themes + +You can import specific themed word lists: + +```typescript +// Import from a specific theme +import codename from "codenames/animals-20"; +// OR +import { codename } from "codenames/animals-20"; + +const name = codename(1234); // "cat" +``` + +### Using Custom Word Lists + +For maximum flexibility, use the core function with your own words: ```typescript -import codename from "codenames/colors-50"; +// Use your own custom word list +import { codename } from "codenames/core"; -const name = codename(1234); // "blue" +const name = codename(1234, ["alpha", "beta", "gamma"]); // "beta" ``` -Supported list sizes are 10, 20, 30, 50, and 100. The default is 20. +## API Reference + +### Default API (with cities-20) + +```typescript +import codename from "codenames"; // default export +import { codename } from "codenames"; // named export + +codename(input: number, words?: readonly string[]): string +``` + +- `input` - The number to convert +- `words` - Optional array of words to use (defaults to cities-20) + +### Core API (with custom words) + +```typescript +import codename from "codenames/core"; // default export +import { codename } from "codenames/core"; // named export + +codename(input: number, words: readonly string[]): string +``` + +- `input` - The number to convert +- `words` - Required array of words to use + +### Themed APIs + +```typescript +import codename from "codenames/cities-20"; // default export +import { codename } from "codenames/animals-50"; // named export +// ... and many more + +codename(input: number): string +``` + +All APIs support the same input types: + +- Positive integers: `123`, `1234` +- Negative integers: `-42` +- Decimals: `3.14` (converted to integers internally) +- Large numbers: up to `Number.MAX_SAFE_INTEGER` + +Supported list sizes are 10, 20, 30, 50, and 100. The default theme uses 20 words. ## Command Line Interface @@ -132,18 +211,20 @@ done ## Word Lists -- **Adjectives**: good, bad, big, small, new, old, hot, cold, fast, slow, ... -- **Animals**: cat, dog, fish, bird, cow, pig, bee, ant, bat, fly, ... -- **Cities**: paris, london, rome, tokyo, berlin, madrid, sydney, moscow, cairo, dubai, ... -- **Clothing**: shirt, jeans, shoe, hat, sock, dress, coat, belt, tie, pants, ... -- **Colors**: red, blue, green, yellow, black, white, gray, pink, orange, purple, ... -- **Countries**: china, japan, india, france, italy, spain, canada, mexico, brazil, germany, ... -- **Elements**: gold, iron, lead, zinc, tin, copper, silver, carbon, oxygen, helium, ... -- **Emotions**: love, hate, joy, sad, fear, mad, happy, angry, glad, calm, ... -- **Food**: bread, milk, egg, rice, meat, fish, cake, apple, cheese, pasta, ... -- **Gems**: ruby, pearl, jade, opal, amber, diamond, emerald, gold, silver, topaz, ... -- **Nature**: tree, sun, sky, rain, moon, star, wind, sea, water, rock, ... -- **Snacks**: chips, nuts, cookie, pretzel, popcorn, candy, fruit, cheese, cracker, yogurt, ... +Each theme is available in multiple sizes: 10, 20, 30, 50, and 100 words. Choose based on your collision tolerance needs. + +- **[Adjectives](words/adjectives.txt)**: good, bad, big, small, new, old, hot, cold, fast, slow, ... +- **[Animals](words/animals.txt)**: cat, dog, fish, bird, cow, pig, bee, ant, bat, fly, ... +- **[Cities](words/cities.txt)**: paris, london, rome, tokyo, berlin, madrid, sydney, moscow, cairo, dubai, ... +- **[Clothing](words/clothing.txt)**: shirt, jeans, shoe, hat, sock, dress, coat, belt, tie, pants, ... +- **[Colors](words/colors.txt)**: red, blue, green, yellow, black, white, gray, pink, orange, purple, ... +- **[Countries](words/countries.txt)**: china, japan, india, france, italy, spain, canada, mexico, brazil, germany, ... +- **[Elements](words/elements.txt)**: gold, iron, lead, zinc, tin, copper, silver, carbon, oxygen, helium, ... +- **[Emotions](words/emotions.txt)**: love, hate, joy, sad, fear, mad, happy, angry, glad, calm, ... +- **[Food](words/food.txt)**: bread, milk, egg, rice, meat, fish, cake, apple, cheese, pasta, ... +- **[Gems](words/gems.txt)**: ruby, pearl, jade, opal, amber, diamond, emerald, gold, silver, topaz, ... +- **[Nature](words/nature.txt)**: tree, sun, sky, rain, moon, star, wind, sea, water, rock, ... +- **[Snacks](words/snacks.txt)**: chips, nuts, cookie, pretzel, popcorn, candy, fruit, cheese, cracker, yogurt, ... ## Contributing diff --git a/cities.test.ts b/cities.test.ts index 32fbdb0..ec959c5 100644 --- a/cities.test.ts +++ b/cities.test.ts @@ -1,5 +1,8 @@ +/* SPDX-FileCopyrightText: 2025-present Kriasoft */ +/* SPDX-License-Identifier: MIT */ + import { describe, expect, test } from "bun:test"; -import cityCodename, { cities, type City } from "./words/cities-20"; +import cityCodename, { cities, codename, type City } from "./words/cities-20"; describe("cities-20 theme", () => { describe("structure validation", () => { @@ -26,37 +29,54 @@ describe("cities-20 theme", () => { expect(cities).toContain(result); }); + test("exports named codename function", () => { + const result = codename(1234); + expect(cities).toContain(result); + expect(result).toBe(cityCodename(1234)); + }); + test("returns consistent results", () => { expect(cityCodename(1)).toBe(cityCodename(1)); + expect(codename(1)).toBe(codename(1)); expect(cityCodename(1234)).toBe(cityCodename(1234)); + expect(codename(1234)).toBe(codename(1234)); expect(cityCodename(-42)).toBe(cityCodename(-42)); + expect(codename(-42)).toBe(codename(-42)); }); test("TypeScript types work correctly", () => { const city: City = cityCodename(123); expect(cities).toContain(city); - const typedResult: City = cityCodename(1234); + const typedResult: City = codename(1234); expect(typedResult).toBe(cityCodename(1234)); }); test("works as factory-created codename function", () => { expect(typeof cityCodename).toBe("function"); + expect(typeof codename).toBe("function"); const result = cityCodename(1234); expect(cities).toContain(result); }); test("handles various inputs", () => { expect(cities).toContain(cityCodename(0)); + expect(cities).toContain(codename(0)); expect(cities).toContain(cityCodename(-1)); + expect(cities).toContain(codename(-1)); expect(cities).toContain(cityCodename(999999)); + expect(cities).toContain(codename(999999)); expect(cities).toContain(cityCodename(Number.MAX_SAFE_INTEGER)); + expect(cities).toContain(codename(Number.MAX_SAFE_INTEGER)); }); test("throws on invalid inputs", () => { expect(() => cityCodename(NaN)).toThrow(); + expect(() => codename(NaN)).toThrow(); expect(() => cityCodename(Infinity)).toThrow(); + expect(() => codename(Infinity)).toThrow(); expect(() => cityCodename(-Infinity)).toThrow(); + expect(() => codename(-Infinity)).toThrow(); }); }); }); diff --git a/cli.ts b/cli.ts index 6ced1e6..f66ce5f 100644 --- a/cli.ts +++ b/cli.ts @@ -4,73 +4,73 @@ import process from "node:process"; // Import theme modules dynamically const themes = { - adjectives: () => import("./words/adjectives-20.js"), + adjectives: () => import("./words/adjectives-100.js"), "adjectives-10": () => import("./words/adjectives-10.js"), "adjectives-20": () => import("./words/adjectives-20.js"), "adjectives-30": () => import("./words/adjectives-30.js"), "adjectives-50": () => import("./words/adjectives-50.js"), "adjectives-100": () => import("./words/adjectives-100.js"), - animals: () => import("./words/animals-20.js"), + animals: () => import("./words/animals-100.js"), "animals-10": () => import("./words/animals-10.js"), "animals-20": () => import("./words/animals-20.js"), "animals-30": () => import("./words/animals-30.js"), "animals-50": () => import("./words/animals-50.js"), "animals-100": () => import("./words/animals-100.js"), - cities: () => import("./words/cities-20.js"), + cities: () => import("./words/cities-100.js"), "cities-10": () => import("./words/cities-10.js"), "cities-20": () => import("./words/cities-20.js"), "cities-30": () => import("./words/cities-30.js"), "cities-50": () => import("./words/cities-50.js"), "cities-100": () => import("./words/cities-100.js"), - clothing: () => import("./words/clothing-20.js"), + clothing: () => import("./words/clothing-100.js"), "clothing-10": () => import("./words/clothing-10.js"), "clothing-20": () => import("./words/clothing-20.js"), "clothing-30": () => import("./words/clothing-30.js"), "clothing-50": () => import("./words/clothing-50.js"), "clothing-100": () => import("./words/clothing-100.js"), - colors: () => import("./words/colors-20.js"), + colors: () => import("./words/colors-100.js"), "colors-10": () => import("./words/colors-10.js"), "colors-20": () => import("./words/colors-20.js"), "colors-30": () => import("./words/colors-30.js"), "colors-50": () => import("./words/colors-50.js"), "colors-100": () => import("./words/colors-100.js"), - countries: () => import("./words/countries-20.js"), + countries: () => import("./words/countries-100.js"), "countries-10": () => import("./words/countries-10.js"), "countries-20": () => import("./words/countries-20.js"), "countries-30": () => import("./words/countries-30.js"), "countries-50": () => import("./words/countries-50.js"), "countries-100": () => import("./words/countries-100.js"), - elements: () => import("./words/elements-20.js"), + elements: () => import("./words/elements-100.js"), "elements-10": () => import("./words/elements-10.js"), "elements-20": () => import("./words/elements-20.js"), "elements-30": () => import("./words/elements-30.js"), "elements-50": () => import("./words/elements-50.js"), "elements-100": () => import("./words/elements-100.js"), - emotions: () => import("./words/emotions-20.js"), + emotions: () => import("./words/emotions-100.js"), "emotions-10": () => import("./words/emotions-10.js"), "emotions-20": () => import("./words/emotions-20.js"), "emotions-30": () => import("./words/emotions-30.js"), "emotions-50": () => import("./words/emotions-50.js"), "emotions-100": () => import("./words/emotions-100.js"), - food: () => import("./words/food-20.js"), + food: () => import("./words/food-100.js"), "food-10": () => import("./words/food-10.js"), "food-20": () => import("./words/food-20.js"), "food-30": () => import("./words/food-30.js"), "food-50": () => import("./words/food-50.js"), "food-100": () => import("./words/food-100.js"), - gems: () => import("./words/gems-20.js"), + gems: () => import("./words/gems-100.js"), "gems-10": () => import("./words/gems-10.js"), "gems-20": () => import("./words/gems-20.js"), "gems-30": () => import("./words/gems-30.js"), "gems-50": () => import("./words/gems-50.js"), "gems-100": () => import("./words/gems-100.js"), - nature: () => import("./words/nature-20.js"), + nature: () => import("./words/nature-100.js"), "nature-10": () => import("./words/nature-10.js"), "nature-20": () => import("./words/nature-20.js"), "nature-30": () => import("./words/nature-30.js"), "nature-50": () => import("./words/nature-50.js"), "nature-100": () => import("./words/nature-100.js"), - snacks: () => import("./words/snacks-20.js"), + snacks: () => import("./words/snacks-100.js"), "snacks-10": () => import("./words/snacks-10.js"), "snacks-20": () => import("./words/snacks-20.js"), "snacks-30": () => import("./words/snacks-30.js"), @@ -84,7 +84,7 @@ type ThemeName = keyof typeof themes; const args = process.argv.slice(2); // Simple argument parsing -let theme: ThemeName = "cities-20"; +let theme: ThemeName | "default" = "default"; let inputNumber: number | undefined; for (let i = 0; i < args.length; i++) { @@ -115,6 +115,9 @@ for (let i = 0; i < args.length; i++) { } } +/** + * Display help information for the CLI + */ function showHelp(): void { console.log(` codenames - Convert numbers to human-readable codenames @@ -138,7 +141,9 @@ Examples: `); } -// Main CLI logic +/** + * Main CLI logic - processes arguments and generates codename + */ async function main(): Promise { if (inputNumber === undefined) { console.error("Error: Please provide a number to convert"); @@ -147,9 +152,19 @@ async function main(): Promise { } try { - const themeModule = await themes[theme](); - const codename = themeModule.default; - const result = codename(inputNumber); + let result: string; + + if (theme === "default") { + // Use the main export with default cities-20 + const mainModule = await import("./index.js"); + result = mainModule.default(inputNumber); + } else { + // Use specific theme + const themeModule = await themes[theme](); + const codename = themeModule.default; + result = codename(inputNumber); + } + console.log(result); } catch (error) { console.error( diff --git a/core/codename.ts b/core/codename.ts new file mode 100644 index 0000000..4e8aa58 --- /dev/null +++ b/core/codename.ts @@ -0,0 +1,67 @@ +/* SPDX-FileCopyrightText: 2025-present Kriasoft */ +/* SPDX-License-Identifier: MIT */ + +/** + * Converts a number to a human-readable codename using the provided word list. + * This function is used internally by themed word files. + * + * @param words - Array of words to use for codenames + * @param input - The number to convert + * @returns A string codename + * + * @example + * ```typescript + * const cities = ["paris", "london", "tokyo"]; + * codename(cities, 1234) // "london" + * ``` + */ +export function codename(words: readonly string[], input: number): string { + if (!Number.isFinite(input)) { + throw new Error(`Invalid input: expected a finite number, got ${input}`); + } + + if (words.length === 0) { + throw new Error("Word list cannot be empty"); + } + + const hash = fnv1a(input); + const index = Math.abs(hash) % words.length; + return words[index]!; +} + +/** + * FNV-1a hash function for consistent deterministic output. + * + * @param input - Number to hash + * @returns 32-bit hash value + */ +function fnv1a(input: number): number { + const FNV_PRIME = 0x01000193; + const FNV_OFFSET_BASIS = 0x811c9dc5; + + let hash = FNV_OFFSET_BASIS; + const bytes = numberToBytes(input); + + for (const byte of bytes) { + hash ^= byte; + hash = Math.imul(hash, FNV_PRIME); + } + + return hash >>> 0; +} + +/** + * Convert number to byte array for hashing. + * + * @param num - Number to convert + * @returns Array of bytes + */ +function numberToBytes(num: number): number[] { + const buffer = new ArrayBuffer(8); + const view = new DataView(buffer); + view.setFloat64(0, num, true); + return Array.from(new Uint8Array(buffer)); +} + +export type CodenameFunction = typeof codename; +export default codename; diff --git a/core/factory.ts b/core/factory.ts index 7e30eab..206a557 100644 --- a/core/factory.ts +++ b/core/factory.ts @@ -1,23 +1,28 @@ -import codename from "../index.js"; +import { codename } from "./index.js"; /** - * Creates a codename function for a specific word list + * Creates a codename function for a specific word list. + * * @param words - Array of words to use for codenames * @returns A function that converts numbers to codenames + * * @example - * import { createCodename } from "codenames/core/factory"; + * ```typescript + * import { createCodename } from "codenames/core"; * const cityCodename = createCodename(["paris", "london", "tokyo"]); * cityCodename(1234) // "london" + * ``` */ export function createCodename(words: readonly string[]) { - return (input: number): string => codename(words, input); + return (input: number): string => codename(input, words); } /** - * Creates a typed codename function for a specific word list + * Creates a typed codename function for a specific word list. + * * @param words - Array of words to use for codenames * @returns A function that converts numbers to codenames with proper typing */ export function createTypedCodename(words: T) { - return (input: number): T[number] => codename(words, input) as T[number]; + return (input: number): T[number] => codename(input, words) as T[number]; } diff --git a/core/index.test.ts b/core/index.test.ts new file mode 100644 index 0000000..92d2098 --- /dev/null +++ b/core/index.test.ts @@ -0,0 +1,69 @@ +import { describe, expect, test } from "bun:test"; +import { codename } from "./index"; + +describe("codename (core with custom words)", () => { + const testWords = ["alpha", "beta", "gamma", "delta", "epsilon"]; + + test("converts numbers consistently", () => { + const result1 = codename(1234, testWords); + const result2 = codename(1234, testWords); + expect(result1).toBe(result2); + }); + + test("returns different names for different numbers", () => { + const results = new Set(); + for (let i = 0; i < 100; i++) { + results.add(codename(i, testWords)); + } + expect(results.size).toBeGreaterThan(1); + }); + + test("handles negative numbers", () => { + const result = codename(-42, testWords); + expect(testWords).toContain(result); + }); + + test("handles very large numbers", () => { + const result = codename(Number.MAX_SAFE_INTEGER, testWords); + expect(testWords).toContain(result); + }); + + test("handles decimal numbers", () => { + const result = codename(3.14159, testWords); + expect(testWords).toContain(result); + }); + + test("throws error for non-finite numbers", () => { + expect(() => codename(NaN, testWords)).toThrow( + "Invalid input: expected a finite number, got NaN", + ); + expect(() => codename(Infinity, testWords)).toThrow( + "Invalid input: expected a finite number, got Infinity", + ); + expect(() => codename(-Infinity, testWords)).toThrow( + "Invalid input: expected a finite number, got -Infinity", + ); + }); + + test("throws error for empty word list", () => { + expect(() => codename(123, [])).toThrow("Word list cannot be empty"); + }); + + test("distributes values evenly across word list", () => { + const distribution: Record = {}; + const iterations = 10000; + + for (let i = 0; i < iterations; i++) { + const result = codename(i, testWords); + distribution[result] = (distribution[result] || 0) + 1; + } + + const expectedCount = iterations / testWords.length; + const tolerance = expectedCount * 0.2; + + for (const word of testWords) { + expect(distribution[word]).toBeGreaterThan(expectedCount - tolerance); + expect(distribution[word]).toBeLessThan(expectedCount + tolerance); + } + }); +}); diff --git a/core/index.ts b/core/index.ts new file mode 100644 index 0000000..ccd0601 --- /dev/null +++ b/core/index.ts @@ -0,0 +1,65 @@ +/* SPDX-FileCopyrightText: 2025-present Kriasoft */ +/* SPDX-License-Identifier: MIT */ + +/** + * Converts a number to a human-readable codename using the provided word list. + * + * @param input - The number to convert + * @param words - Array of words to use for codenames + * @returns A string codename + * + * @example + * ```typescript + * import { codename } from "codenames/core"; + * codename(123, ["one", "two", "three"]) // "two" + * ``` + */ +export function codename(input: number, words: readonly string[]): string { + if (!Number.isFinite(input)) { + throw new Error(`Invalid input: expected a finite number, got ${input}`); + } + + if (words.length === 0) { + throw new Error("Word list cannot be empty"); + } + + const hash = fnv1a(input); + const index = Math.abs(hash) % words.length; + return words[index]!; +} + +/** + * FNV-1a hash function for consistent deterministic output. + * + * @param input - Number to hash + * @returns 32-bit hash value + */ +function fnv1a(input: number): number { + const FNV_PRIME = 0x01000193; + const FNV_OFFSET_BASIS = 0x811c9dc5; + + let hash = FNV_OFFSET_BASIS; + const bytes = numberToBytes(input); + + for (const byte of bytes) { + hash ^= byte; + hash = Math.imul(hash, FNV_PRIME); + } + + return hash >>> 0; +} + +/** + * Convert number to byte array for hashing. + * + * @param num - Number to convert + * @returns Array of bytes + */ +function numberToBytes(num: number): number[] { + const buffer = new ArrayBuffer(8); + const view = new DataView(buffer); + view.setFloat64(0, num, true); + return Array.from(new Uint8Array(buffer)); +} + +export default codename; diff --git a/docs/blog/2025-07-23-preview-deployments.md b/docs/blog/2025-07-23-preview-deployments.md new file mode 100644 index 0000000..52808c9 --- /dev/null +++ b/docs/blog/2025-07-23-preview-deployments.md @@ -0,0 +1,392 @@ +# A Practical Guide to Preview Deployments for Every PR + +## Introduction + +Ever had that sinking feeling when you merge a pull request, only to have it blow up in production? We've all been there. You swear it worked on your machine, but the production environment has a different opinion. + +That's where preview deployments come in. Think of them as a dress rehearsal for your code. For every pull request, you get a dedicated, temporary environment that's a spitting image of production. It's a safe space to test your changes, share them with your team, and catch those pesky bugs before they ever see the light of day. + +In this guide, we'll walk you through how to set up preview deployments for your own projects. We'll even use our own [`codenames`](https://github.com/kriasoft/codenames) library to give our preview environments some memorable names. Because `feature-branch-123.example.com` is boring, but `london.example.com` is a bit more pragmatic (more on that later). + +## Core Concepts + +Let's break down what makes preview deployments tick. It's not rocket science, but getting the pieces to play nicely together does require understanding a few key concepts. + +### The CI/CD Pipeline: Your Deployment Assembly Line + +At the heart of preview deployments is your CI/CD pipeline. If you're using GitHub, this means GitHub Actions. Think of it as your deployment assembly line – when someone opens a pull request, the machinery kicks into gear. Your workflow file (`.github/workflows/preview.yml`) becomes the conductor, orchestrating everything from building your app to spinning up infrastructure. + +Here's where speed matters. Nobody wants to wait 20 minutes for their preview URL. The trick? Pre-configured deployment slots. Instead of provisioning fresh infrastructure every time, you maintain a pool of ready-to-go environments. When PR #1234 comes in, it claims slot 3, which already has Cloudflare Workers configured, a Neon database branch ready, and cloud storage buckets pre-configured. Your deployment time drops from minutes to seconds. + +### GitHub Deployments: More Than Just a Status Badge + +GitHub's Deployments feature is criminally underused. It's not just about showing a fancy "deployed" badge on your PR. Deployments give you a proper audit trail – who deployed what, when, and where. They integrate beautifully with GitHub Actions, letting you track preview environments alongside your production deployments. Plus, they make cleanup a breeze when PRs get merged or closed. + +### Infrastructure as Code: Terraform is Your Friend + +Here's where things get interesting. You could click around in various dashboards to set up your preview infrastructure, but that's a recipe for "it worked yesterday" syndrome. Enter Infrastructure as Code, with Terraform leading the charge. + +Your Terraform configs define everything – the Cloudflare Worker, the Neon database branch, the routing rules. When PR #1234 needs an environment, Terraform spins it up consistently, every single time. No more "works on my preview" surprises. + +### The URL Game: Making Previews Memorable + +This is where our `codenames` library shines. Instead of `pr-1234-feature-new-checkout.preview.example.com`, you get `london.example.com`. Short, memorable, and infinitely easier to share in Slack. Your product manager will thank you when they can actually remember the URL during a demo. + +The beauty is in the determinism – PR #1234 always maps to "london", so your preview URLs stay consistent across deployments. It's a small touch that makes a big difference in day-to-day developer experience. + +### Versioning: Keeping Track of What's What + +Preview deployments need versioning too. Not just for your app code, but for the deployment configuration itself. When someone asks "which version of the API is running on the preview?", you need a good answer. Tag your deployments, use semantic versioning, and make it easy to trace from PR to deployed code to actual running environment. + +With these pieces in place, preview deployments transform from a nice-to-have into an essential part of your development workflow. They're your safety net, your collaboration tool, and your "let me show you something cool" superpower all rolled into one. + +## Step-by-Step Guide + +Ready to roll up your sleeves? Let's build a preview deployment system that would make even the most jaded DevOps engineer crack a smile. We'll start with the foundation and work our way up to a fully automated setup. + +### Step 1: Prepare Your Deployment Slot Pool + +Before we dive into automation, let's talk about why pre-configured deployment slots are a game-changer. Imagine provisioning a new Cloudflare Worker, creating a database branch, and setting up storage buckets for every single PR. Your developers would age visibly while waiting for their preview URLs. + +Instead, we'll create a pool of 10-20 deployment slots that sit ready and waiting. Think of them as parking spaces – when a PR rolls in, it just needs to find an empty spot. + +First, let's set up our Terraform configuration for the slot pool: + +```hcl +# terraform/modules/preview-slot/main.tf +variable "slot_number" { + type = number +} + +resource "cloudflare_worker_script" "preview" { + account_id = var.cloudflare_account_id + name = "preview-slot-${var.slot_number}" + content = file("../worker-template.js") + + # Pre-configure but leave dormant until needed + routes = [] +} + +resource "neon_branch" "preview" { + project_id = var.neon_project_id + name = "preview-slot-${var.slot_number}" + parent_id = var.main_branch_id +} + +resource "google_storage_bucket" "assets" { + name = "${var.project_name}-preview-${var.slot_number}" + location = "US" + + lifecycle_rule { + condition { + age = 7 # Auto-cleanup after a week + } + action { + type = "Delete" + } + } +} +``` + +Now, provision your slots (this is a one-time setup): + +```bash +# terraform/environments/preview/main.tf +module "preview_slots" { + source = "../../modules/preview-slot" + count = 20 # Adjust based on your PR velocity + + slot_number = count.index + 1 + # ... other variables +} +``` + +Run `terraform apply` and grab a coffee. When you come back, you'll have 20 deployment slots ready to rock. + +### Step 2: Set Up the GitHub Actions Workflow + +Here's where the magic happens. Our workflow needs to be fast, reliable, and smart enough to handle the entire PR lifecycle. + +Create `.github/workflows/preview.yml`: + +```yaml +name: Preview Deployment + +on: + pull_request: + types: [opened, synchronize, reopened, closed] + +jobs: + deploy: + if: github.event.action != 'closed' + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Generate preview name and slot + id: preview + run: | + # Use codenames for memorable URLs + npm install -g codenames + PREVIEW_NAME=$(codenames ${{ github.event.pull_request.number }}) + echo "name=$PREVIEW_NAME" >> $GITHUB_OUTPUT + + # Deterministically map PR to slot using FNV-1a hash + # Same PR always gets same slot (modulo number of slots) + SLOT_NUMBER=$(( ${{ github.event.pull_request.number }} % 20 + 1 )) + echo "slot=preview-slot-${SLOT_NUMBER}" >> $GITHUB_OUTPUT + echo "slot_number=${SLOT_NUMBER}" >> $GITHUB_OUTPUT + + - name: Create deployment + id: deployment + uses: actions/github-script@v7 + with: + script: | + const deployment = await github.rest.repos.createDeployment({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: context.payload.pull_request.head.sha, + environment: '${{ steps.preview.outputs.slot }}', + description: `Preview for PR #${{ github.event.pull_request.number }}`, + transient_environment: true, + auto_merge: false, + required_contexts: [], + payload: { + pr_number: '${{ github.event.pull_request.number }}', + preview_name: '${{ steps.preview.outputs.name }}' + } + }); + return deployment.data.id; + + - name: Build application + run: | + npm ci + npm run build + + - name: Configure slot + run: | + # Update Cloudflare Worker route + curl -X PUT "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/workers/routes" \ + -H "Authorization: Bearer $CF_API_TOKEN" \ + -H "Content-Type: application/json" \ + --data '{ + "pattern": "${{ steps.preview.outputs.name }}.example.com/*", + "script": "${{ steps.preview.outputs.slot }}" + }' + + - name: Deploy to slot + run: | + # Deploy to Cloudflare Worker + wrangler deploy --name ${{ steps.preview.outputs.slot }} \ + --var PREVIEW_NAME:${{ steps.preview.outputs.name }} \ + --var PR_NUMBER:${{ github.event.pull_request.number }} + + # Sync assets to GCS + gsutil -m rsync -r ./dist gs://${{ env.PROJECT_NAME }}-${{ steps.preview.outputs.slot }}/ + + - name: Update deployment status + if: always() + uses: actions/github-script@v7 + with: + script: | + await github.rest.repos.createDeploymentStatus({ + owner: context.repo.owner, + repo: context.repo.repo, + deployment_id: ${{ steps.deployment.outputs.result }}, + state: '${{ job.status }}', + environment_url: 'https://${{ steps.preview.outputs.name }}.example.com', + description: 'Preview deployment' + }); +``` + +The beauty here? We're not provisioning anything new – just deterministically mapping each PR to its designated slot. The `codenames` library uses FNV-1a hashing to ensure PR #1234 always gets "london" as its subdomain, while the modulo operation ensures it always maps to the same slot number. No searching for available slots, no race conditions, just pure deterministic assignment. The whole process takes under a minute. + +### Step 3: Database Branch Magic with Neon + +Neon databases are perfect for preview deployments because they support branching. Just like Git branches for your code, you can branch your database. Why Neon? It spins up new branches in seconds, not minutes, and you only pay for the actual storage diff. + +Add this to your deployment workflow: + +```yaml +- name: Reset database branch + run: | + # Reset the branch to match production + curl -X POST https://console.neon.tech/api/v2/projects/$NEON_PROJECT_ID/branches/${{ steps.preview.outputs.slot }}/reset \ + -H "Authorization: Bearer $NEON_API_KEY" \ + -H "Content-Type: application/json" \ + --data '{ + "parent_branch_id": "${{ env.MAIN_BRANCH_ID }}" + }' + +- name: Run migrations + env: + DATABASE_URL: ${{ secrets.NEON_SLOT_URLS[steps.preview.outputs.slot] }} + run: | + npm run db:migrate + +- name: Seed preview data + if: github.event.pull_request.labels.*.name contains 'needs-seed-data' + run: | + npm run db:seed:preview +``` + +Pro tip: Use PR labels to control seeding. Sometimes you want production data, sometimes you want squeaky clean test data. + +### Step 4: Cloudflare Hyperdrive for Speed + +Database connections from edge workers can be slow. Enter Cloudflare Hyperdrive – it maintains connection pools close to your workers, dramatically reducing latency. Since we're using pre-configured slots, each slot already has its own Hyperdrive configuration pointing to its Neon branch. + +In your worker code: + +```javascript +export default { + async fetch(request, env) { + // Hyperdrive connection is pre-configured per slot + const db = env.HYPERDRIVE.connect(); + + // Your app logic here + const result = await db.query("SELECT * FROM ..."); + + return new Response(JSON.stringify(result), { + headers: { "content-type": "application/json" }, + }); + }, +}; +``` + +### Step 5: Making URLs Memorable with Codenames + +This is where we add that touch of elegance. Instead of `preview-slot-3.example.com`, your team gets `tokyo.example.com`. The `codenames` library uses the FNV-1a hash algorithm internally, ensuring PR #1234 always deterministically maps to the same memorable name. + +Install it in your workflow: + +```bash +npm install -g codenames +``` + +Then use it to generate consistent, memorable names: + +```javascript +// In your GitHub Action +const codenames = require("codenames/cities-20"); +const previewName = codenames(prNumber); +console.log(`PR #${prNumber} deployed to https://${previewName}.example.com`); +``` + +The magic lies in the determinism. FNV-1a (Fowler-Noll-Vo) is a fast, non-cryptographic hash function that distributes values evenly across the wordlist. This means: + +- PR #1234 → always "london" +- PR #1235 → always "paris" +- PR #1236 → always "tokyo" + +No database lookups, no state management, just pure mathematical beauty. + +Configure your DNS with a wildcard record: + +```text +*.example.com → your-cloudflare-worker.workers.dev +``` + +Now any subdomain automatically routes to your worker, which can handle the request based on the hostname. + +### Step 6: Automatic Cleanup + +Don't be that team with 200 zombie preview environments eating up resources. Add a cleanup job: + +```yaml +cleanup: + if: github.event.action == 'closed' + runs-on: ubuntu-latest + + steps: + - name: Generate preview info + id: preview + run: | + # Regenerate the same deterministic values + npm install -g codenames + PREVIEW_NAME=$(codenames ${{ github.event.pull_request.number }}) + SLOT_NUMBER=$(( ${{ github.event.pull_request.number }} % 20 + 1 )) + echo "name=$PREVIEW_NAME" >> $GITHUB_OUTPUT + echo "slot=preview-slot-${SLOT_NUMBER}" >> $GITHUB_OUTPUT + + - name: Remove route + run: | + # Remove the Cloudflare route + ROUTE_ID=$(curl -X GET "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/workers/routes" \ + -H "Authorization: Bearer $CF_API_TOKEN" | + jq -r '.result[] | select(.pattern | contains("${{ steps.preview.outputs.name }}")) | .id') + + curl -X DELETE "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/workers/routes/$ROUTE_ID" \ + -H "Authorization: Bearer $CF_API_TOKEN" + + - name: Clean up assets + run: | + # Clear the GCS bucket for this slot + gsutil -m rm -r gs://${{ env.PROJECT_NAME }}-${{ steps.preview.outputs.slot }}/* || true +``` + +Since we're using deterministic slot assignment, there's no need to mark slots as available. When the next PR claims the same slot (through the modulo operation), it'll automatically reset and redeploy. Simple, elegant, and collision-free for most teams. + +### Step 7: Monitoring and Debugging + +Add some observability to your preview deployments: + +```yaml +- name: Add preview comment + uses: actions/github-script@v7 + with: + script: | + const body = `### 🚀 Preview Deployment Ready! + + **URL:** https://${{ steps.preview.outputs.name }}.example.com + **Slot:** ${{ steps.preview.outputs.slot }} + **Deploy Time:** ${{ steps.deploy-timer.outputs.time }}s + +
+ Deployment Details + + - Worker Version: ${{ github.sha }} + - Database Branch: ${{ steps.preview.outputs.slot }} + - Assets Bucket: gs://${{ env.PROJECT_NAME }}-${{ steps.preview.outputs.slot }} + +
+ + To redeploy, push a new commit or re-run the workflow.`; + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: body + }); +``` + +This gives everyone visibility into what's deployed where, and helps debug issues when they inevitably crop up. + +### Putting It All Together + +With everything in place, here's what happens when a developer opens a PR: + +1. GitHub Actions triggers the preview workflow +2. The workflow generates a memorable name using `codenames` (FNV-1a hash ensures consistency) +3. It calculates the slot number deterministically (PR number % 20) +4. The app is built and deployed to the pre-assigned Cloudflare Worker slot +5. The database branch is reset and migrations run +6. Routes are updated to point the subdomain to the correct worker +7. A comment appears on the PR with the preview URL +8. When the PR is closed, route cleanup happens automatically + +From PR to preview URL in under 60 seconds. Not bad for a day's work. + +The elegance of this approach? No state management, no race conditions, no searching for available slots. Just pure deterministic math. PR #1234 always gets "london" and always uses slot 14. If someone else is using slot 14? They get overwritten – but that's fine because their PR was closed anyway. + +Remember, the key to fast preview deployments is preparation. Those pre-configured slots are like having a pot of water already boiling – you just need to drop in the pasta. Or in this case, your code. + +## Tips and Tricks + +... + +## Conclusion + +... diff --git a/index.test.ts b/index.test.ts index 66e5bdc..ed7d2d0 100644 --- a/index.test.ts +++ b/index.test.ts @@ -1,69 +1,111 @@ -import { describe, expect, test } from "bun:test"; -import codename from "./index"; +/* SPDX-FileCopyrightText: 2025-present Kriasoft */ +/* SPDX-License-Identifier: MIT */ -describe("codename", () => { - const testWords = ["alpha", "beta", "gamma", "delta", "epsilon"]; +import { describe, expect, test } from "bun:test"; +import defaultCodename, { codename } from "./index"; +describe("codename (default export)", () => { test("converts numbers consistently", () => { - const result1 = codename(testWords, 1234); - const result2 = codename(testWords, 1234); + const result1 = defaultCodename(1234); + const result2 = defaultCodename(1234); expect(result1).toBe(result2); }); test("returns different names for different numbers", () => { const results = new Set(); for (let i = 0; i < 100; i++) { - results.add(codename(testWords, i)); + results.add(defaultCodename(i)); } expect(results.size).toBeGreaterThan(1); }); test("handles negative numbers", () => { - const result = codename(testWords, -42); - expect(testWords).toContain(result); + const result = defaultCodename(-42); + expect(typeof result).toBe("string"); + expect(result.length).toBeGreaterThan(0); }); test("handles very large numbers", () => { - const result = codename(testWords, Number.MAX_SAFE_INTEGER); - expect(testWords).toContain(result); + const result = defaultCodename(Number.MAX_SAFE_INTEGER); + expect(typeof result).toBe("string"); + expect(result.length).toBeGreaterThan(0); }); test("handles decimal numbers", () => { - const result = codename(testWords, 3.14159); - expect(testWords).toContain(result); + const result = defaultCodename(3.14159); + expect(typeof result).toBe("string"); + expect(result.length).toBeGreaterThan(0); }); test("throws error for non-finite numbers", () => { - expect(() => codename(testWords, NaN)).toThrow( + expect(() => defaultCodename(NaN)).toThrow( "Invalid input: expected a finite number, got NaN", ); - expect(() => codename(testWords, Infinity)).toThrow( + expect(() => defaultCodename(Infinity)).toThrow( "Invalid input: expected a finite number, got Infinity", ); - expect(() => codename(testWords, -Infinity)).toThrow( + expect(() => defaultCodename(-Infinity)).toThrow( "Invalid input: expected a finite number, got -Infinity", ); }); - test("throws error for empty word list", () => { - expect(() => codename([], 123)).toThrow("Word list cannot be empty"); + test("returns city names by default", () => { + const result = defaultCodename(123); + expect(typeof result).toBe("string"); + expect(result.length).toBeGreaterThan(0); }); - test("distributes values evenly across word list", () => { - const distribution: Record = {}; - const iterations = 10000; + test("accepts custom words", () => { + const testWords = ["alpha", "beta", "gamma", "delta", "epsilon"]; + const result = defaultCodename(123, testWords); + expect(testWords).toContain(result); + }); +}); - for (let i = 0; i < iterations; i++) { - const result = codename(testWords, i); - distribution[result] = (distribution[result] || 0) + 1; - } +describe("codename (named export)", () => { + test("converts numbers consistently with default cities", () => { + const result1 = codename(1234); + const result2 = codename(1234); + expect(result1).toBe(result2); + }); - const expectedCount = iterations / testWords.length; - const tolerance = expectedCount * 0.2; + test("default export and named export return same result", () => { + const result1 = defaultCodename(1234); + const result2 = codename(1234); + expect(result1).toBe(result2); + }); - for (const word of testWords) { - expect(distribution[word]).toBeGreaterThan(expectedCount - tolerance); - expect(distribution[word]).toBeLessThan(expectedCount + tolerance); - } + const testWords = ["alpha", "beta", "gamma", "delta", "epsilon"]; + + test("converts numbers consistently with custom words", () => { + const result1 = codename(1234, testWords); + const result2 = codename(1234, testWords); + expect(result1).toBe(result2); + }); + + test("returns words from custom list", () => { + const result = codename(123, testWords); + expect(testWords).toContain(result); + }); + + test("handles negative numbers with custom words", () => { + const result = codename(-42, testWords); + expect(testWords).toContain(result); + }); + + test("throws error for empty word list", () => { + expect(() => codename(123, [])).toThrow("Word list cannot be empty"); + }); + + test("throws error for non-finite numbers", () => { + expect(() => codename(NaN, testWords)).toThrow( + "Invalid input: expected a finite number, got NaN", + ); + expect(() => codename(Infinity, testWords)).toThrow( + "Invalid input: expected a finite number, got Infinity", + ); + expect(() => codename(-Infinity, testWords)).toThrow( + "Invalid input: expected a finite number, got -Infinity", + ); }); }); diff --git a/index.ts b/index.ts index 50dcef3..9ea1efe 100644 --- a/index.ts +++ b/index.ts @@ -1,66 +1,43 @@ /* SPDX-FileCopyrightText: 2025-present Kriasoft */ /* SPDX-License-Identifier: MIT */ +import { cities } from "./words/cities-20.js"; +import { codename as codenameCore } from "./core/index.js"; + /** * Converts a number to a human-readable codename using the provided word list. * + * @param input - The number to convert + * @param words - Optional array of words to use for codenames (defaults to cities-20) + * @returns A string codename + * * @example * ```typescript - * const cities = ["paris", "london", "tokyo"]; - * codename(cities, 1234) // "london" + * import { codename } from "codenames"; + * codename(1234) // "london" (using default cities-20) + * codename(1234, ["one", "two", "three"]) // "two" * ``` - * - * @param words - Array of words to use for codenames - * @param input - The number to convert - * @returns A string codename */ -export function codename(words: readonly string[], input: number): string { - if (!Number.isFinite(input)) { - throw new Error(`Invalid input: expected a finite number, got ${input}`); - } - - if (words.length === 0) { - throw new Error("Word list cannot be empty"); - } - - const hash = fnv1a(input); - const index = Math.abs(hash) % words.length; - return words[index]!; +export function codename(input: number, words?: readonly string[]): string { + return codenameCore(input, words ?? cities); } /** - * FNV-1a hash function for consistent deterministic output. + * Converts a number to a human-readable codename using cities-20 list by default. * - * @param input - Number to hash - * @returns 32-bit hash value - */ -function fnv1a(input: number): number { - const FNV_PRIME = 0x01000193; - const FNV_OFFSET_BASIS = 0x811c9dc5; - - let hash = FNV_OFFSET_BASIS; - const bytes = numberToBytes(input); - - for (const byte of bytes) { - hash ^= byte; - hash = Math.imul(hash, FNV_PRIME); - } - - return hash >>> 0; -} - -/** - * Convert number to byte array for hashing. + * @param input - The number to convert + * @param words - Optional array of words to use for codenames (defaults to cities-20) + * @returns A string codename * - * @param num - Number to convert - * @returns Array of bytes + * @example + * ```typescript + * import codename from "codenames"; + * codename(1234) // "london" + * ``` */ -function numberToBytes(num: number): number[] { - const buffer = new ArrayBuffer(8); - const view = new DataView(buffer); - view.setFloat64(0, num, true); - return Array.from(new Uint8Array(buffer)); +export default function defaultCodename( + input: number, + words?: readonly string[], +): string { + return codenameCore(input, words ?? cities); } - -export type CodenameFunction = typeof codename; -export default codename; diff --git a/integration.test.ts b/integration.test.ts index bfb599b..1428ab4 100644 --- a/integration.test.ts +++ b/integration.test.ts @@ -1,12 +1,24 @@ import { describe, expect, test } from "bun:test"; -import codename from "./index"; +import codename, { codename as codenameWithWords } from "./index"; +import { codename as coreCodename } from "./core/index"; import { cities } from "./words/cities-20"; import cityCodename from "./words/cities-20"; import { createCodename, createTypedCodename } from "./core/factory"; +import { codename as allCodename, themes } from "./words/all"; describe("integration tests", () => { - test("base codename function works with city list", () => { - const result = codename(cities, 1234); + test("default codename function works with cities-20", () => { + const result = codename(1234); + expect(cities).toContain(result); + }); + + test("named codename function works with custom word list", () => { + const result = codenameWithWords(1234, cities); + expect(cities).toContain(result); + }); + + test("core codename function works with custom word list", () => { + const result = coreCodename(1234, cities); expect(cities).toContain(result); }); @@ -14,8 +26,8 @@ describe("integration tests", () => { const result = cityCodename(1234); expect(cities).toContain(result); - // Should produce same result as base function - expect(result).toBe(codename(cities, 1234)); + // Should produce same result as default function + expect(result).toBe(codename(1234)); }); test("factory function creates working codename functions", () => { @@ -36,11 +48,48 @@ describe("integration tests", () => { test("consistent results across different usage patterns", () => { const testNumber = 9876; - const result1 = codename(cities, testNumber); - const result2 = cityCodename(testNumber); - const result3 = createCodename(cities)(testNumber); + const result1 = codename(testNumber); // default with cities-20 + const result2 = cityCodename(testNumber); // cities-20 specific + const result3 = codenameWithWords(testNumber, cities); // named export with cities + const result4 = coreCodename(testNumber, cities); // core with cities + const result5 = createCodename(cities)(testNumber); // factory expect(result1).toBe(result2); expect(result2).toBe(result3); + expect(result3).toBe(result4); + expect(result4).toBe(result5); + }); + + test("all APIs use same underlying implementation", () => { + const testNumber = 42; + + const defaultResult = codename(testNumber); + const namedResult = codenameWithWords(testNumber, cities); + const coreResult = coreCodename(testNumber, cities); + const themeResult = cityCodename(testNumber); + + expect(defaultResult).toBe(namedResult); + expect(namedResult).toBe(coreResult); + expect(coreResult).toBe(themeResult); + }); + + test("all.ts export produces consistent results with individual theme files", () => { + const testNumber = 1234; + + // Compare cities-20 from individual file vs all.ts + const cityResult = cityCodename(testNumber); + const allCityResult = allCodename(testNumber, "cities-20"); + expect(allCityResult).toBe(cityResult); + + // Compare with default export (which uses cities-20) + const defaultResult = codename(testNumber); + expect(allCityResult).toBe(defaultResult); + }); + + test("all.ts themes array includes all expected themes", () => { + expect(themes).toContain("cities-20"); + expect(themes).toContain("animals-10"); + expect(themes).toContain("colors-50"); + expect(themes.length).toBe(60); // 12 categories × 5 sizes }); }); diff --git a/package.json b/package.json index cd3e1ff..4a605a4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codenames", - "version": "1.0.0", + "version": "1.1.0", "description": "Converts numerical values into human-readable names following a specific theme (e.g., cities)", "license": "MIT", "author": "Konstantin Tarkus ", @@ -39,6 +39,18 @@ "types": "./dist/index.d.ts", "import": "./dist/index.js" }, + "./all": { + "bun": "./words/all.ts", + "deno": "./words/all.ts", + "types": "./dist/words/all.d.ts", + "import": "./dist/words/all.js" + }, + "./core": { + "bun": "./core/index.ts", + "deno": "./core/index.ts", + "types": "./dist/core/index.d.ts", + "import": "./dist/core/index.js" + }, "./adjectives": { "bun": "./words/adjectives-20.ts", "deno": "./words/adjectives-20.ts", diff --git a/scripts/analyze-hash-distribution.ts b/scripts/analyze-hash-distribution.ts index 8f02669..c85d110 100644 --- a/scripts/analyze-hash-distribution.ts +++ b/scripts/analyze-hash-distribution.ts @@ -2,6 +2,9 @@ /** * Test script to compare hash distribution between simpleHash and fnv1a algorithms + * + * SPDX-FileCopyrightText: 2025-present Kriasoft + * SPDX-License-Identifier: MIT */ // Simple hash implementation from proposal.js diff --git a/scripts/generate.ts b/scripts/generate.ts index 7b41887..48823d6 100644 --- a/scripts/generate.ts +++ b/scripts/generate.ts @@ -4,15 +4,125 @@ * * Usage: bun run ./scripts/generate.ts * Example: bun run ./scripts/generate.ts cities City + * + * SPDX-FileCopyrightText: 2025-present Kriasoft + * SPDX-License-Identifier: MIT */ import { readFile, writeFile } from "node:fs/promises"; import { join } from "node:path"; +import { readdirSync } from "node:fs"; +import { codename } from "../core/index.js"; + +/** + * Get the appropriate article (a/an) for a word + */ +function getArticle(word: string): string { + const vowels = ["a", "e", "i", "o", "u"]; + return vowels.includes(word.toLowerCase().charAt(0)) ? "An" : "A"; +} + +async function generateAllFile() { + console.log("Generating words/all.ts..."); + + // Get all theme files from words directory + const wordFiles = readdirSync("words") + .filter((file) => file.endsWith(".txt")) + .map((file) => file.replace(".txt", "")) + .sort(); + + const limits = [10, 20, 30, 50, 100]; + + // Generate imports + const imports: string[] = []; + const themeEntries: string[] = []; + const themesArray: string[] = []; + const switchCases: string[] = []; + + for (const theme of wordFiles) { + for (const limit of limits) { + const themeName = `${theme}-${limit}`; + const importName = `${theme}${limit}`; + + imports.push(`import ${importName} from "./${themeName}.js";`); + themeEntries.push(` | "${themeName}"`); + themesArray.push(` "${themeName}",`); + switchCases.push( + ` case "${themeName}":\n return ${importName}(input);`, + ); + } + } + + const content = `/** + * Auto-generated theme aggregator file. Do not edit manually. + * Run 'bun run scripts/generate.ts all' to regenerate. + * + * SPDX-FileCopyrightText: 2025-present Kriasoft + * SPDX-License-Identifier: MIT + */ + +${imports.join("\n")} + +export type Theme = +${themeEntries.join("\n")}; + +/** + * Array of all available theme names for discoverability and validation. + */ +export const themes: readonly Theme[] = [ +${themesArray.join("\n")} +] as const; + +/** + * Converts a number to a human-readable codename using the specified theme. + * + * @param input - The number to convert + * @param theme - The theme name to use + * @returns A string codename + * + * @example + * \`\`\`typescript + * import codename from "codenames/all"; + * codename(123, "cities-20") // "tokyo" + * codename(456, "animals-50") // "cat" + * \`\`\` + */ +export function codename(input: number, theme: Theme): string { + switch (theme) { +${switchCases.join("\n\n")} + + default: + throw new Error( + \`Unknown theme: \${theme}. Available themes: \${themes.join(", ")}\`, + ); + } +} + +export default codename; +`; + + await writeFile("words/all.ts", content); + console.log("Generated words/all.ts"); +} const [list, listItemName] = Bun.argv.slice(2); -if (!list || !listItemName) { +if (!list) { + console.error("Usage: bun run ./scripts/generate.ts "); + console.error(" bun run ./scripts/generate.ts all"); + console.error("Example: bun run ./scripts/generate.ts cities City"); + process.exit(1); +} + +// Handle special "all" case +if (list === "all") { + await generateAllFile(); + process.exit(0); +} + +if (!listItemName) { console.error("Usage: bun run ./scripts/generate.ts "); + console.error(" bun run ./scripts/generate.ts all"); console.error("Example: bun run ./scripts/generate.ts cities City"); process.exit(1); } @@ -37,6 +147,7 @@ for (const limit of limits) { } const selectedWords = words.slice(0, limit); + const exampleOutput = codename(1234, selectedWords); const tsContent = `/** * Auto-generated from ${list}.txt. Do not edit manually. * Run 'bun run scripts/generate.ts ${list} ${listItemName}' to regenerate. @@ -55,15 +166,17 @@ export type ${listItemName} = (typeof ${list})[number]; /** * Converts a number to a ${listItemName} codename + * * @param input - The number to convert - * @returns A ${listItemName} name + * @returns ${getArticle(listItemName)} ${listItemName} name + * * @example * \`\`\`typescript * import codename from "codenames/${list}-${limit}"; - * codename(1234) // "${words[0]}" + * codename(1234) // "${exampleOutput}" * \`\`\` */ -const codename = createTypedCodename(${list}); +export const codename = createTypedCodename(${list}); export default codename; `; diff --git a/words/adjectives-10.ts b/words/adjectives-10.ts index 9dbb63a..005ef2f 100644 --- a/words/adjectives-10.ts +++ b/words/adjectives-10.ts @@ -25,14 +25,16 @@ export type Adjective = (typeof adjectives)[number]; /** * Converts a number to a Adjective codename + * * @param input - The number to convert - * @returns A Adjective name + * @returns An Adjective name + * * @example * ```typescript * import codename from "codenames/adjectives-10"; * codename(1234) // "good" * ``` */ -const codename = createTypedCodename(adjectives); +export const codename = createTypedCodename(adjectives); export default codename; diff --git a/words/adjectives-100.ts b/words/adjectives-100.ts index bb2ee44..d5216c8 100644 --- a/words/adjectives-100.ts +++ b/words/adjectives-100.ts @@ -115,14 +115,16 @@ export type Adjective = (typeof adjectives)[number]; /** * Converts a number to a Adjective codename + * * @param input - The number to convert - * @returns A Adjective name + * @returns An Adjective name + * * @example * ```typescript * import codename from "codenames/adjectives-100"; - * codename(1234) // "good" + * codename(1234) // "fuzzy" * ``` */ -const codename = createTypedCodename(adjectives); +export const codename = createTypedCodename(adjectives); export default codename; diff --git a/words/adjectives-20.ts b/words/adjectives-20.ts index 3eb039c..4c1a6dc 100644 --- a/words/adjectives-20.ts +++ b/words/adjectives-20.ts @@ -35,14 +35,16 @@ export type Adjective = (typeof adjectives)[number]; /** * Converts a number to a Adjective codename + * * @param input - The number to convert - * @returns A Adjective name + * @returns An Adjective name + * * @example * ```typescript * import codename from "codenames/adjectives-20"; - * codename(1234) // "good" + * codename(1234) // "long" * ``` */ -const codename = createTypedCodename(adjectives); +export const codename = createTypedCodename(adjectives); export default codename; diff --git a/words/adjectives-30.ts b/words/adjectives-30.ts index cc13612..b05a988 100644 --- a/words/adjectives-30.ts +++ b/words/adjectives-30.ts @@ -45,14 +45,16 @@ export type Adjective = (typeof adjectives)[number]; /** * Converts a number to a Adjective codename + * * @param input - The number to convert - * @returns A Adjective name + * @returns An Adjective name + * * @example * ```typescript * import codename from "codenames/adjectives-30"; - * codename(1234) // "good" + * codename(1234) // "long" * ``` */ -const codename = createTypedCodename(adjectives); +export const codename = createTypedCodename(adjectives); export default codename; diff --git a/words/adjectives-50.ts b/words/adjectives-50.ts index d72fd0e..c4dd3f5 100644 --- a/words/adjectives-50.ts +++ b/words/adjectives-50.ts @@ -65,14 +65,16 @@ export type Adjective = (typeof adjectives)[number]; /** * Converts a number to a Adjective codename + * * @param input - The number to convert - * @returns A Adjective name + * @returns An Adjective name + * * @example * ```typescript * import codename from "codenames/adjectives-50"; - * codename(1234) // "good" + * codename(1234) // "happy" * ``` */ -const codename = createTypedCodename(adjectives); +export const codename = createTypedCodename(adjectives); export default codename; diff --git a/words/all.test.ts b/words/all.test.ts new file mode 100644 index 0000000..f0b5b98 --- /dev/null +++ b/words/all.test.ts @@ -0,0 +1,239 @@ +import { describe, expect, test } from "bun:test"; +import { codename, themes, type Theme } from "./all"; + +describe("words/all.ts exports", () => { + test("themes array contains all expected themes", () => { + expect(themes).toBeInstanceOf(Array); + expect(themes.length).toBeGreaterThan(0); + + // Should contain themes for all main categories + const categories = [ + "adjectives", + "animals", + "cities", + "clothing", + "colors", + "countries", + "elements", + "emotions", + "food", + "gems", + "nature", + "snacks", + ]; + const limits = ["10", "20", "30", "50", "100"]; + + for (const category of categories) { + for (const limit of limits) { + const themeName = `${category}-${limit}` as Theme; + expect(themes).toContain(themeName); + } + } + }); + + test("themes array has correct length", () => { + // 12 categories × 5 limits = 60 themes + expect(themes.length).toBe(60); + }); + + test("all themes in array are unique", () => { + const uniqueThemes = new Set(themes); + expect(uniqueThemes.size).toBe(themes.length); + }); + + test("codename function works with cities themes", () => { + const result10 = codename(123, "cities-10"); + const result20 = codename(123, "cities-20"); + const result30 = codename(123, "cities-30"); + const result50 = codename(123, "cities-50"); + const result100 = codename(123, "cities-100"); + + expect(typeof result10).toBe("string"); + expect(typeof result20).toBe("string"); + expect(typeof result30).toBe("string"); + expect(typeof result50).toBe("string"); + expect(typeof result100).toBe("string"); + + expect(result10.length).toBeGreaterThan(0); + expect(result20.length).toBeGreaterThan(0); + expect(result30.length).toBeGreaterThan(0); + expect(result50.length).toBeGreaterThan(0); + expect(result100.length).toBeGreaterThan(0); + }); + + test("codename function works with animals themes", () => { + const result10 = codename(456, "animals-10"); + const result20 = codename(456, "animals-20"); + const result30 = codename(456, "animals-30"); + const result50 = codename(456, "animals-50"); + const result100 = codename(456, "animals-100"); + + expect(typeof result10).toBe("string"); + expect(typeof result20).toBe("string"); + expect(typeof result30).toBe("string"); + expect(typeof result50).toBe("string"); + expect(typeof result100).toBe("string"); + }); + + test("codename function works with all theme categories", () => { + const testCategories = [ + "adjectives", + "animals", + "cities", + "clothing", + "colors", + "countries", + "elements", + "emotions", + "food", + "gems", + "nature", + "snacks", + ]; + + for (const category of testCategories) { + const themeName = `${category}-20` as Theme; + const result = codename(789, themeName); + expect(typeof result).toBe("string"); + expect(result.length).toBeGreaterThan(0); + } + }); + + test("codename function produces consistent results", () => { + const testNumber = 1234; + const theme: Theme = "colors-20"; + + const result1 = codename(testNumber, theme); + const result2 = codename(testNumber, theme); + const result3 = codename(testNumber, theme); + + expect(result1).toBe(result2); + expect(result2).toBe(result3); + }); + + test("codename function produces different results for different inputs", () => { + const theme: Theme = "nature-30"; + + const result1 = codename(100, theme); + const result2 = codename(200, theme); + const result3 = codename(300, theme); + + // While not guaranteed, it's very likely these will be different + const allSame = result1 === result2 && result2 === result3; + expect(allSame).toBe(false); + }); + + test("codename function handles edge cases", () => { + const theme: Theme = "food-10"; + + expect(() => codename(0, theme)).not.toThrow(); + expect(() => codename(1, theme)).not.toThrow(); + expect(() => codename(999999, theme)).not.toThrow(); + + const result0 = codename(0, theme); + const result1 = codename(1, theme); + const resultLarge = codename(999999, theme); + + expect(typeof result0).toBe("string"); + expect(typeof result1).toBe("string"); + expect(typeof resultLarge).toBe("string"); + }); + + test("codename function throws error for invalid theme", () => { + expect(() => { + // @ts-expect-error - Testing invalid theme + codename(123, "invalid-theme"); + }).toThrow(); + + expect(() => { + // @ts-expect-error - Testing invalid theme + codename(123, "cities-5"); + }).toThrow(); + + expect(() => { + // @ts-expect-error - Testing invalid theme + codename(123, "nonexistent-20"); + }).toThrow(); + }); + + test("error message includes available themes", () => { + try { + // @ts-expect-error - Testing invalid theme + codename(123, "invalid-theme"); + expect(true).toBe(false); // Should not reach here + } catch (error) { + expect(error).toBeInstanceOf(Error); + const message = (error as Error).message; + expect(message).toContain("Unknown theme"); + expect(message).toContain("Available themes"); + expect(message).toContain("cities-20"); + expect(message).toContain("animals-10"); + } + }); + + test("Theme type includes all expected theme names", () => { + // This is a compile-time check, but we can verify at runtime too + const sampleThemes: Theme[] = [ + "cities-20", + "animals-10", + "colors-50", + "food-100", + "nature-30", + ]; + + for (const theme of sampleThemes) { + expect(themes).toContain(theme); + expect(() => codename(123, theme)).not.toThrow(); + } + }); + + test("different theme sizes produce valid results", () => { + const testNumber = 555; + + const result10 = codename(testNumber, "gems-10"); + const result20 = codename(testNumber, "gems-20"); + const result30 = codename(testNumber, "gems-30"); + const result50 = codename(testNumber, "gems-50"); + const result100 = codename(testNumber, "gems-100"); + + expect(typeof result10).toBe("string"); + expect(typeof result20).toBe("string"); + expect(typeof result30).toBe("string"); + expect(typeof result50).toBe("string"); + expect(typeof result100).toBe("string"); + + // All should be valid gem names (lowercase strings) + expect(result10).toMatch(/^[a-z]+$/); + expect(result20).toMatch(/^[a-z]+$/); + expect(result30).toMatch(/^[a-z]+$/); + expect(result50).toMatch(/^[a-z]+$/); + expect(result100).toMatch(/^[a-z]+$/); + }); + + test("integration with existing theme files", () => { + // Import a specific theme to compare + import("./cities-20.js").then((citiesModule) => { + const testNumber = 888; + const citiesResult = citiesModule.default(testNumber); + const allResult = codename(testNumber, "cities-20"); + + expect(allResult).toBe(citiesResult); + }); + }); + + test("themes array is properly typed as readonly", () => { + // TypeScript compile-time check - the array should be typed as readonly + // This test verifies the themes array exists and has the expected structure + expect(Array.isArray(themes)).toBe(true); + expect(themes.length).toBe(60); + + // Verify all items are strings matching the expected pattern + for (const theme of themes) { + expect(typeof theme).toBe("string"); + expect(theme).toMatch(/^[a-z]+-\d+$/); + } + + // Note: TypeScript provides compile-time readonly protection, + // but JavaScript arrays are mutable at runtime by design + }); +}); diff --git a/words/all.ts b/words/all.ts new file mode 100644 index 0000000..2dea4bf --- /dev/null +++ b/words/all.ts @@ -0,0 +1,401 @@ +/** + * Auto-generated theme aggregator file. Do not edit manually. + * Run 'bun run scripts/generate.ts all' to regenerate. + * + * SPDX-FileCopyrightText: 2025-present Kriasoft + * SPDX-License-Identifier: MIT + */ + +import adjectives10 from "./adjectives-10.js"; +import adjectives20 from "./adjectives-20.js"; +import adjectives30 from "./adjectives-30.js"; +import adjectives50 from "./adjectives-50.js"; +import adjectives100 from "./adjectives-100.js"; +import animals10 from "./animals-10.js"; +import animals20 from "./animals-20.js"; +import animals30 from "./animals-30.js"; +import animals50 from "./animals-50.js"; +import animals100 from "./animals-100.js"; +import cities10 from "./cities-10.js"; +import cities20 from "./cities-20.js"; +import cities30 from "./cities-30.js"; +import cities50 from "./cities-50.js"; +import cities100 from "./cities-100.js"; +import clothing10 from "./clothing-10.js"; +import clothing20 from "./clothing-20.js"; +import clothing30 from "./clothing-30.js"; +import clothing50 from "./clothing-50.js"; +import clothing100 from "./clothing-100.js"; +import colors10 from "./colors-10.js"; +import colors20 from "./colors-20.js"; +import colors30 from "./colors-30.js"; +import colors50 from "./colors-50.js"; +import colors100 from "./colors-100.js"; +import countries10 from "./countries-10.js"; +import countries20 from "./countries-20.js"; +import countries30 from "./countries-30.js"; +import countries50 from "./countries-50.js"; +import countries100 from "./countries-100.js"; +import elements10 from "./elements-10.js"; +import elements20 from "./elements-20.js"; +import elements30 from "./elements-30.js"; +import elements50 from "./elements-50.js"; +import elements100 from "./elements-100.js"; +import emotions10 from "./emotions-10.js"; +import emotions20 from "./emotions-20.js"; +import emotions30 from "./emotions-30.js"; +import emotions50 from "./emotions-50.js"; +import emotions100 from "./emotions-100.js"; +import food10 from "./food-10.js"; +import food20 from "./food-20.js"; +import food30 from "./food-30.js"; +import food50 from "./food-50.js"; +import food100 from "./food-100.js"; +import gems10 from "./gems-10.js"; +import gems20 from "./gems-20.js"; +import gems30 from "./gems-30.js"; +import gems50 from "./gems-50.js"; +import gems100 from "./gems-100.js"; +import nature10 from "./nature-10.js"; +import nature20 from "./nature-20.js"; +import nature30 from "./nature-30.js"; +import nature50 from "./nature-50.js"; +import nature100 from "./nature-100.js"; +import snacks10 from "./snacks-10.js"; +import snacks20 from "./snacks-20.js"; +import snacks30 from "./snacks-30.js"; +import snacks50 from "./snacks-50.js"; +import snacks100 from "./snacks-100.js"; + +export type Theme = + | "adjectives-10" + | "adjectives-20" + | "adjectives-30" + | "adjectives-50" + | "adjectives-100" + | "animals-10" + | "animals-20" + | "animals-30" + | "animals-50" + | "animals-100" + | "cities-10" + | "cities-20" + | "cities-30" + | "cities-50" + | "cities-100" + | "clothing-10" + | "clothing-20" + | "clothing-30" + | "clothing-50" + | "clothing-100" + | "colors-10" + | "colors-20" + | "colors-30" + | "colors-50" + | "colors-100" + | "countries-10" + | "countries-20" + | "countries-30" + | "countries-50" + | "countries-100" + | "elements-10" + | "elements-20" + | "elements-30" + | "elements-50" + | "elements-100" + | "emotions-10" + | "emotions-20" + | "emotions-30" + | "emotions-50" + | "emotions-100" + | "food-10" + | "food-20" + | "food-30" + | "food-50" + | "food-100" + | "gems-10" + | "gems-20" + | "gems-30" + | "gems-50" + | "gems-100" + | "nature-10" + | "nature-20" + | "nature-30" + | "nature-50" + | "nature-100" + | "snacks-10" + | "snacks-20" + | "snacks-30" + | "snacks-50" + | "snacks-100"; + +/** + * Array of all available theme names for discoverability and validation. + */ +export const themes: readonly Theme[] = [ + "adjectives-10", + "adjectives-20", + "adjectives-30", + "adjectives-50", + "adjectives-100", + "animals-10", + "animals-20", + "animals-30", + "animals-50", + "animals-100", + "cities-10", + "cities-20", + "cities-30", + "cities-50", + "cities-100", + "clothing-10", + "clothing-20", + "clothing-30", + "clothing-50", + "clothing-100", + "colors-10", + "colors-20", + "colors-30", + "colors-50", + "colors-100", + "countries-10", + "countries-20", + "countries-30", + "countries-50", + "countries-100", + "elements-10", + "elements-20", + "elements-30", + "elements-50", + "elements-100", + "emotions-10", + "emotions-20", + "emotions-30", + "emotions-50", + "emotions-100", + "food-10", + "food-20", + "food-30", + "food-50", + "food-100", + "gems-10", + "gems-20", + "gems-30", + "gems-50", + "gems-100", + "nature-10", + "nature-20", + "nature-30", + "nature-50", + "nature-100", + "snacks-10", + "snacks-20", + "snacks-30", + "snacks-50", + "snacks-100", +] as const; + +/** + * Converts a number to a human-readable codename using the specified theme. + * + * @param input - The number to convert + * @param theme - The theme name to use + * @returns A string codename + * + * @example + * ```typescript + * import codename from "codenames/all"; + * codename(123, "cities-20") // "tokyo" + * codename(456, "animals-50") // "cat" + * ``` + */ +export function codename(input: number, theme: Theme): string { + switch (theme) { + case "adjectives-10": + return adjectives10(input); + + case "adjectives-20": + return adjectives20(input); + + case "adjectives-30": + return adjectives30(input); + + case "adjectives-50": + return adjectives50(input); + + case "adjectives-100": + return adjectives100(input); + + case "animals-10": + return animals10(input); + + case "animals-20": + return animals20(input); + + case "animals-30": + return animals30(input); + + case "animals-50": + return animals50(input); + + case "animals-100": + return animals100(input); + + case "cities-10": + return cities10(input); + + case "cities-20": + return cities20(input); + + case "cities-30": + return cities30(input); + + case "cities-50": + return cities50(input); + + case "cities-100": + return cities100(input); + + case "clothing-10": + return clothing10(input); + + case "clothing-20": + return clothing20(input); + + case "clothing-30": + return clothing30(input); + + case "clothing-50": + return clothing50(input); + + case "clothing-100": + return clothing100(input); + + case "colors-10": + return colors10(input); + + case "colors-20": + return colors20(input); + + case "colors-30": + return colors30(input); + + case "colors-50": + return colors50(input); + + case "colors-100": + return colors100(input); + + case "countries-10": + return countries10(input); + + case "countries-20": + return countries20(input); + + case "countries-30": + return countries30(input); + + case "countries-50": + return countries50(input); + + case "countries-100": + return countries100(input); + + case "elements-10": + return elements10(input); + + case "elements-20": + return elements20(input); + + case "elements-30": + return elements30(input); + + case "elements-50": + return elements50(input); + + case "elements-100": + return elements100(input); + + case "emotions-10": + return emotions10(input); + + case "emotions-20": + return emotions20(input); + + case "emotions-30": + return emotions30(input); + + case "emotions-50": + return emotions50(input); + + case "emotions-100": + return emotions100(input); + + case "food-10": + return food10(input); + + case "food-20": + return food20(input); + + case "food-30": + return food30(input); + + case "food-50": + return food50(input); + + case "food-100": + return food100(input); + + case "gems-10": + return gems10(input); + + case "gems-20": + return gems20(input); + + case "gems-30": + return gems30(input); + + case "gems-50": + return gems50(input); + + case "gems-100": + return gems100(input); + + case "nature-10": + return nature10(input); + + case "nature-20": + return nature20(input); + + case "nature-30": + return nature30(input); + + case "nature-50": + return nature50(input); + + case "nature-100": + return nature100(input); + + case "snacks-10": + return snacks10(input); + + case "snacks-20": + return snacks20(input); + + case "snacks-30": + return snacks30(input); + + case "snacks-50": + return snacks50(input); + + case "snacks-100": + return snacks100(input); + + default: + throw new Error( + `Unknown theme: ${theme}. Available themes: ${themes.join(", ")}`, + ); + } +} + +export default codename; diff --git a/words/animals-10.ts b/words/animals-10.ts index 049dd0a..0660d88 100644 --- a/words/animals-10.ts +++ b/words/animals-10.ts @@ -25,14 +25,16 @@ export type Animal = (typeof animals)[number]; /** * Converts a number to a Animal codename + * * @param input - The number to convert - * @returns A Animal name + * @returns An Animal name + * * @example * ```typescript * import codename from "codenames/animals-10"; * codename(1234) // "cat" * ``` */ -const codename = createTypedCodename(animals); +export const codename = createTypedCodename(animals); export default codename; diff --git a/words/animals-100.ts b/words/animals-100.ts index adcc5fb..def41bb 100644 --- a/words/animals-100.ts +++ b/words/animals-100.ts @@ -115,14 +115,16 @@ export type Animal = (typeof animals)[number]; /** * Converts a number to a Animal codename + * * @param input - The number to convert - * @returns A Animal name + * @returns An Animal name + * * @example * ```typescript * import codename from "codenames/animals-100"; - * codename(1234) // "cat" + * codename(1234) // "shrimp" * ``` */ -const codename = createTypedCodename(animals); +export const codename = createTypedCodename(animals); export default codename; diff --git a/words/animals-20.ts b/words/animals-20.ts index f22e92a..de10753 100644 --- a/words/animals-20.ts +++ b/words/animals-20.ts @@ -35,14 +35,16 @@ export type Animal = (typeof animals)[number]; /** * Converts a number to a Animal codename + * * @param input - The number to convert - * @returns A Animal name + * @returns An Animal name + * * @example * ```typescript * import codename from "codenames/animals-20"; - * codename(1234) // "cat" + * codename(1234) // "rat" * ``` */ -const codename = createTypedCodename(animals); +export const codename = createTypedCodename(animals); export default codename; diff --git a/words/animals-30.ts b/words/animals-30.ts index 452e1f0..1dc5827 100644 --- a/words/animals-30.ts +++ b/words/animals-30.ts @@ -45,14 +45,16 @@ export type Animal = (typeof animals)[number]; /** * Converts a number to a Animal codename + * * @param input - The number to convert - * @returns A Animal name + * @returns An Animal name + * * @example * ```typescript * import codename from "codenames/animals-30"; - * codename(1234) // "cat" + * codename(1234) // "rat" * ``` */ -const codename = createTypedCodename(animals); +export const codename = createTypedCodename(animals); export default codename; diff --git a/words/animals-50.ts b/words/animals-50.ts index 5139840..5978d69 100644 --- a/words/animals-50.ts +++ b/words/animals-50.ts @@ -65,14 +65,16 @@ export type Animal = (typeof animals)[number]; /** * Converts a number to a Animal codename + * * @param input - The number to convert - * @returns A Animal name + * @returns An Animal name + * * @example * ```typescript * import codename from "codenames/animals-50"; - * codename(1234) // "cat" + * codename(1234) // "wolf" * ``` */ -const codename = createTypedCodename(animals); +export const codename = createTypedCodename(animals); export default codename; diff --git a/words/cities-10.ts b/words/cities-10.ts index 5fd9e41..30818d1 100644 --- a/words/cities-10.ts +++ b/words/cities-10.ts @@ -25,14 +25,16 @@ export type City = (typeof cities)[number]; /** * Converts a number to a City codename + * * @param input - The number to convert * @returns A City name + * * @example * ```typescript * import codename from "codenames/cities-10"; * codename(1234) // "paris" * ``` */ -const codename = createTypedCodename(cities); +export const codename = createTypedCodename(cities); export default codename; diff --git a/words/cities-100.ts b/words/cities-100.ts index 74ffb44..b806bf4 100644 --- a/words/cities-100.ts +++ b/words/cities-100.ts @@ -115,14 +115,16 @@ export type City = (typeof cities)[number]; /** * Converts a number to a City codename + * * @param input - The number to convert * @returns A City name + * * @example * ```typescript * import codename from "codenames/cities-100"; - * codename(1234) // "paris" + * codename(1234) // "utrecht" * ``` */ -const codename = createTypedCodename(cities); +export const codename = createTypedCodename(cities); export default codename; diff --git a/words/cities-20.ts b/words/cities-20.ts index 8ec95cd..9f92f6a 100644 --- a/words/cities-20.ts +++ b/words/cities-20.ts @@ -35,14 +35,16 @@ export type City = (typeof cities)[number]; /** * Converts a number to a City codename + * * @param input - The number to convert * @returns A City name + * * @example * ```typescript * import codename from "codenames/cities-20"; - * codename(1234) // "paris" + * codename(1234) // "milan" * ``` */ -const codename = createTypedCodename(cities); +export const codename = createTypedCodename(cities); export default codename; diff --git a/words/cities-30.ts b/words/cities-30.ts index bb6b671..b9bc226 100644 --- a/words/cities-30.ts +++ b/words/cities-30.ts @@ -45,14 +45,16 @@ export type City = (typeof cities)[number]; /** * Converts a number to a City codename + * * @param input - The number to convert * @returns A City name + * * @example * ```typescript * import codename from "codenames/cities-30"; - * codename(1234) // "paris" + * codename(1234) // "milan" * ``` */ -const codename = createTypedCodename(cities); +export const codename = createTypedCodename(cities); export default codename; diff --git a/words/cities-50.ts b/words/cities-50.ts index d1ac079..8a37604 100644 --- a/words/cities-50.ts +++ b/words/cities-50.ts @@ -65,14 +65,16 @@ export type City = (typeof cities)[number]; /** * Converts a number to a City codename + * * @param input - The number to convert * @returns A City name + * * @example * ```typescript * import codename from "codenames/cities-50"; - * codename(1234) // "paris" + * codename(1234) // "lisbon" * ``` */ -const codename = createTypedCodename(cities); +export const codename = createTypedCodename(cities); export default codename; diff --git a/words/clothing-10.ts b/words/clothing-10.ts index c1c2c35..a11384f 100644 --- a/words/clothing-10.ts +++ b/words/clothing-10.ts @@ -25,14 +25,16 @@ export type Clothing = (typeof clothing)[number]; /** * Converts a number to a Clothing codename + * * @param input - The number to convert * @returns A Clothing name + * * @example * ```typescript * import codename from "codenames/clothing-10"; * codename(1234) // "shirt" * ``` */ -const codename = createTypedCodename(clothing); +export const codename = createTypedCodename(clothing); export default codename; diff --git a/words/clothing-100.ts b/words/clothing-100.ts index c963dc2..3d7c741 100644 --- a/words/clothing-100.ts +++ b/words/clothing-100.ts @@ -115,14 +115,16 @@ export type Clothing = (typeof clothing)[number]; /** * Converts a number to a Clothing codename + * * @param input - The number to convert * @returns A Clothing name + * * @example * ```typescript * import codename from "codenames/clothing-100"; - * codename(1234) // "shirt" + * codename(1234) // "halter" * ``` */ -const codename = createTypedCodename(clothing); +export const codename = createTypedCodename(clothing); export default codename; diff --git a/words/clothing-20.ts b/words/clothing-20.ts index de9dd6e..f08817c 100644 --- a/words/clothing-20.ts +++ b/words/clothing-20.ts @@ -35,14 +35,16 @@ export type Clothing = (typeof clothing)[number]; /** * Converts a number to a Clothing codename + * * @param input - The number to convert * @returns A Clothing name + * * @example * ```typescript * import codename from "codenames/clothing-20"; - * codename(1234) // "shirt" + * codename(1234) // "glove" * ``` */ -const codename = createTypedCodename(clothing); +export const codename = createTypedCodename(clothing); export default codename; diff --git a/words/clothing-30.ts b/words/clothing-30.ts index b51d9e3..747cfb9 100644 --- a/words/clothing-30.ts +++ b/words/clothing-30.ts @@ -45,14 +45,16 @@ export type Clothing = (typeof clothing)[number]; /** * Converts a number to a Clothing codename + * * @param input - The number to convert * @returns A Clothing name + * * @example * ```typescript * import codename from "codenames/clothing-30"; - * codename(1234) // "shirt" + * codename(1234) // "glove" * ``` */ -const codename = createTypedCodename(clothing); +export const codename = createTypedCodename(clothing); export default codename; diff --git a/words/clothing-50.ts b/words/clothing-50.ts index 470f7dd..7684ed1 100644 --- a/words/clothing-50.ts +++ b/words/clothing-50.ts @@ -65,14 +65,16 @@ export type Clothing = (typeof clothing)[number]; /** * Converts a number to a Clothing codename + * * @param input - The number to convert * @returns A Clothing name + * * @example * ```typescript * import codename from "codenames/clothing-50"; - * codename(1234) // "shirt" + * codename(1234) // "shorts" * ``` */ -const codename = createTypedCodename(clothing); +export const codename = createTypedCodename(clothing); export default codename; diff --git a/words/colors-10.ts b/words/colors-10.ts index 06ea8f6..ff2ca40 100644 --- a/words/colors-10.ts +++ b/words/colors-10.ts @@ -25,14 +25,16 @@ export type Color = (typeof colors)[number]; /** * Converts a number to a Color codename + * * @param input - The number to convert * @returns A Color name + * * @example * ```typescript * import codename from "codenames/colors-10"; * codename(1234) // "red" * ``` */ -const codename = createTypedCodename(colors); +export const codename = createTypedCodename(colors); export default codename; diff --git a/words/colors-100.ts b/words/colors-100.ts index b8b27d5..7972090 100644 --- a/words/colors-100.ts +++ b/words/colors-100.ts @@ -115,14 +115,16 @@ export type Color = (typeof colors)[number]; /** * Converts a number to a Color codename + * * @param input - The number to convert * @returns A Color name + * * @example * ```typescript * import codename from "codenames/colors-100"; - * codename(1234) // "red" + * codename(1234) // "citrine" * ``` */ -const codename = createTypedCodename(colors); +export const codename = createTypedCodename(colors); export default codename; diff --git a/words/colors-20.ts b/words/colors-20.ts index 00feee4..8cb9b43 100644 --- a/words/colors-20.ts +++ b/words/colors-20.ts @@ -35,14 +35,16 @@ export type Color = (typeof colors)[number]; /** * Converts a number to a Color codename + * * @param input - The number to convert * @returns A Color name + * * @example * ```typescript * import codename from "codenames/colors-20"; - * codename(1234) // "red" + * codename(1234) // "brown" * ``` */ -const codename = createTypedCodename(colors); +export const codename = createTypedCodename(colors); export default codename; diff --git a/words/colors-30.ts b/words/colors-30.ts index 7f7feb0..48583ef 100644 --- a/words/colors-30.ts +++ b/words/colors-30.ts @@ -45,14 +45,16 @@ export type Color = (typeof colors)[number]; /** * Converts a number to a Color codename + * * @param input - The number to convert * @returns A Color name + * * @example * ```typescript * import codename from "codenames/colors-30"; - * codename(1234) // "red" + * codename(1234) // "brown" * ``` */ -const codename = createTypedCodename(colors); +export const codename = createTypedCodename(colors); export default codename; diff --git a/words/colors-50.ts b/words/colors-50.ts index 7e01f6f..88e5f44 100644 --- a/words/colors-50.ts +++ b/words/colors-50.ts @@ -65,14 +65,16 @@ export type Color = (typeof colors)[number]; /** * Converts a number to a Color codename + * * @param input - The number to convert * @returns A Color name + * * @example * ```typescript * import codename from "codenames/colors-50"; - * codename(1234) // "red" + * codename(1234) // "maroon" * ``` */ -const codename = createTypedCodename(colors); +export const codename = createTypedCodename(colors); export default codename; diff --git a/words/countries-10.ts b/words/countries-10.ts index 8e3f00a..008b3cc 100644 --- a/words/countries-10.ts +++ b/words/countries-10.ts @@ -25,14 +25,16 @@ export type Country = (typeof countries)[number]; /** * Converts a number to a Country codename + * * @param input - The number to convert * @returns A Country name + * * @example * ```typescript * import codename from "codenames/countries-10"; * codename(1234) // "china" * ``` */ -const codename = createTypedCodename(countries); +export const codename = createTypedCodename(countries); export default codename; diff --git a/words/countries-100.ts b/words/countries-100.ts index 4204e95..0985f4c 100644 --- a/words/countries-100.ts +++ b/words/countries-100.ts @@ -115,14 +115,16 @@ export type Country = (typeof countries)[number]; /** * Converts a number to a Country codename + * * @param input - The number to convert * @returns A Country name + * * @example * ```typescript * import codename from "codenames/countries-100"; - * codename(1234) // "china" + * codename(1234) // "nigeria" * ``` */ -const codename = createTypedCodename(countries); +export const codename = createTypedCodename(countries); export default codename; diff --git a/words/countries-20.ts b/words/countries-20.ts index a0143c3..85e5924 100644 --- a/words/countries-20.ts +++ b/words/countries-20.ts @@ -35,14 +35,16 @@ export type Country = (typeof countries)[number]; /** * Converts a number to a Country codename + * * @param input - The number to convert * @returns A Country name + * * @example * ```typescript * import codename from "codenames/countries-20"; - * codename(1234) // "china" + * codename(1234) // "russia" * ``` */ -const codename = createTypedCodename(countries); +export const codename = createTypedCodename(countries); export default codename; diff --git a/words/countries-30.ts b/words/countries-30.ts index cc0d228..3504fb2 100644 --- a/words/countries-30.ts +++ b/words/countries-30.ts @@ -45,14 +45,16 @@ export type Country = (typeof countries)[number]; /** * Converts a number to a Country codename + * * @param input - The number to convert * @returns A Country name + * * @example * ```typescript * import codename from "codenames/countries-30"; - * codename(1234) // "china" + * codename(1234) // "russia" * ``` */ -const codename = createTypedCodename(countries); +export const codename = createTypedCodename(countries); export default codename; diff --git a/words/countries-50.ts b/words/countries-50.ts index 0851cee..ae1cd41 100644 --- a/words/countries-50.ts +++ b/words/countries-50.ts @@ -65,14 +65,16 @@ export type Country = (typeof countries)[number]; /** * Converts a number to a Country codename + * * @param input - The number to convert * @returns A Country name + * * @example * ```typescript * import codename from "codenames/countries-50"; - * codename(1234) // "china" + * codename(1234) // "iraq" * ``` */ -const codename = createTypedCodename(countries); +export const codename = createTypedCodename(countries); export default codename; diff --git a/words/elements-10.ts b/words/elements-10.ts index 3691103..d4eae06 100644 --- a/words/elements-10.ts +++ b/words/elements-10.ts @@ -25,14 +25,16 @@ export type Element = (typeof elements)[number]; /** * Converts a number to a Element codename + * * @param input - The number to convert - * @returns A Element name + * @returns An Element name + * * @example * ```typescript * import codename from "codenames/elements-10"; * codename(1234) // "gold" * ``` */ -const codename = createTypedCodename(elements); +export const codename = createTypedCodename(elements); export default codename; diff --git a/words/elements-100.ts b/words/elements-100.ts index a655803..398654d 100644 --- a/words/elements-100.ts +++ b/words/elements-100.ts @@ -115,14 +115,16 @@ export type Element = (typeof elements)[number]; /** * Converts a number to a Element codename + * * @param input - The number to convert - * @returns A Element name + * @returns An Element name + * * @example * ```typescript * import codename from "codenames/elements-100"; - * codename(1234) // "gold" + * codename(1234) // "berkelium" * ``` */ -const codename = createTypedCodename(elements); +export const codename = createTypedCodename(elements); export default codename; diff --git a/words/elements-20.ts b/words/elements-20.ts index 5a2107a..f6a53e1 100644 --- a/words/elements-20.ts +++ b/words/elements-20.ts @@ -35,14 +35,16 @@ export type Element = (typeof elements)[number]; /** * Converts a number to a Element codename + * * @param input - The number to convert - * @returns A Element name + * @returns An Element name + * * @example * ```typescript * import codename from "codenames/elements-20"; - * codename(1234) // "gold" + * codename(1234) // "neon" * ``` */ -const codename = createTypedCodename(elements); +export const codename = createTypedCodename(elements); export default codename; diff --git a/words/elements-30.ts b/words/elements-30.ts index 1e444df..d53c53e 100644 --- a/words/elements-30.ts +++ b/words/elements-30.ts @@ -45,14 +45,16 @@ export type Element = (typeof elements)[number]; /** * Converts a number to a Element codename + * * @param input - The number to convert - * @returns A Element name + * @returns An Element name + * * @example * ```typescript * import codename from "codenames/elements-30"; - * codename(1234) // "gold" + * codename(1234) // "neon" * ``` */ -const codename = createTypedCodename(elements); +export const codename = createTypedCodename(elements); export default codename; diff --git a/words/elements-50.ts b/words/elements-50.ts index 9a2bc33..730943a 100644 --- a/words/elements-50.ts +++ b/words/elements-50.ts @@ -65,14 +65,16 @@ export type Element = (typeof elements)[number]; /** * Converts a number to a Element codename + * * @param input - The number to convert - * @returns A Element name + * @returns An Element name + * * @example * ```typescript * import codename from "codenames/elements-50"; - * codename(1234) // "gold" + * codename(1234) // "uranium" * ``` */ -const codename = createTypedCodename(elements); +export const codename = createTypedCodename(elements); export default codename; diff --git a/words/emotions-10.ts b/words/emotions-10.ts index 68058a3..3de3630 100644 --- a/words/emotions-10.ts +++ b/words/emotions-10.ts @@ -25,14 +25,16 @@ export type Emotion = (typeof emotions)[number]; /** * Converts a number to a Emotion codename + * * @param input - The number to convert - * @returns A Emotion name + * @returns An Emotion name + * * @example * ```typescript * import codename from "codenames/emotions-10"; * codename(1234) // "love" * ``` */ -const codename = createTypedCodename(emotions); +export const codename = createTypedCodename(emotions); export default codename; diff --git a/words/emotions-100.ts b/words/emotions-100.ts index 7acf062..60f8dd9 100644 --- a/words/emotions-100.ts +++ b/words/emotions-100.ts @@ -115,14 +115,16 @@ export type Emotion = (typeof emotions)[number]; /** * Converts a number to a Emotion codename + * * @param input - The number to convert - * @returns A Emotion name + * @returns An Emotion name + * * @example * ```typescript * import codename from "codenames/emotions-100"; - * codename(1234) // "love" + * codename(1234) // "wary" * ``` */ -const codename = createTypedCodename(emotions); +export const codename = createTypedCodename(emotions); export default codename; diff --git a/words/emotions-20.ts b/words/emotions-20.ts index 4fca033..85a4fdd 100644 --- a/words/emotions-20.ts +++ b/words/emotions-20.ts @@ -35,14 +35,16 @@ export type Emotion = (typeof emotions)[number]; /** * Converts a number to a Emotion codename + * * @param input - The number to convert - * @returns A Emotion name + * @returns An Emotion name + * * @example * ```typescript * import codename from "codenames/emotions-20"; - * codename(1234) // "love" + * codename(1234) // "hurt" * ``` */ -const codename = createTypedCodename(emotions); +export const codename = createTypedCodename(emotions); export default codename; diff --git a/words/emotions-30.ts b/words/emotions-30.ts index 353cdba..cb06d09 100644 --- a/words/emotions-30.ts +++ b/words/emotions-30.ts @@ -45,14 +45,16 @@ export type Emotion = (typeof emotions)[number]; /** * Converts a number to a Emotion codename + * * @param input - The number to convert - * @returns A Emotion name + * @returns An Emotion name + * * @example * ```typescript * import codename from "codenames/emotions-30"; - * codename(1234) // "love" + * codename(1234) // "hurt" * ``` */ -const codename = createTypedCodename(emotions); +export const codename = createTypedCodename(emotions); export default codename; diff --git a/words/emotions-50.ts b/words/emotions-50.ts index 755f95d..d2ec3ea 100644 --- a/words/emotions-50.ts +++ b/words/emotions-50.ts @@ -65,14 +65,16 @@ export type Emotion = (typeof emotions)[number]; /** * Converts a number to a Emotion codename + * * @param input - The number to convert - * @returns A Emotion name + * @returns An Emotion name + * * @example * ```typescript * import codename from "codenames/emotions-50"; - * codename(1234) // "love" + * codename(1234) // "peace" * ``` */ -const codename = createTypedCodename(emotions); +export const codename = createTypedCodename(emotions); export default codename; diff --git a/words/food-10.ts b/words/food-10.ts index 31bccd4..29d6285 100644 --- a/words/food-10.ts +++ b/words/food-10.ts @@ -25,14 +25,16 @@ export type Food = (typeof food)[number]; /** * Converts a number to a Food codename + * * @param input - The number to convert * @returns A Food name + * * @example * ```typescript * import codename from "codenames/food-10"; * codename(1234) // "bread" * ``` */ -const codename = createTypedCodename(food); +export const codename = createTypedCodename(food); export default codename; diff --git a/words/food-100.ts b/words/food-100.ts index ad8230b..c0be175 100644 --- a/words/food-100.ts +++ b/words/food-100.ts @@ -115,14 +115,16 @@ export type Food = (typeof food)[number]; /** * Converts a number to a Food codename + * * @param input - The number to convert * @returns A Food name + * * @example * ```typescript * import codename from "codenames/food-100"; - * codename(1234) // "bread" + * codename(1234) // "wheat" * ``` */ -const codename = createTypedCodename(food); +export const codename = createTypedCodename(food); export default codename; diff --git a/words/food-20.ts b/words/food-20.ts index 0550e3e..01ff356 100644 --- a/words/food-20.ts +++ b/words/food-20.ts @@ -35,14 +35,16 @@ export type Food = (typeof food)[number]; /** * Converts a number to a Food codename + * * @param input - The number to convert * @returns A Food name + * * @example * ```typescript * import codename from "codenames/food-20"; - * codename(1234) // "bread" + * codename(1234) // "pizza" * ``` */ -const codename = createTypedCodename(food); +export const codename = createTypedCodename(food); export default codename; diff --git a/words/food-30.ts b/words/food-30.ts index fa985f2..6289c63 100644 --- a/words/food-30.ts +++ b/words/food-30.ts @@ -45,14 +45,16 @@ export type Food = (typeof food)[number]; /** * Converts a number to a Food codename + * * @param input - The number to convert * @returns A Food name + * * @example * ```typescript * import codename from "codenames/food-30"; - * codename(1234) // "bread" + * codename(1234) // "pizza" * ``` */ -const codename = createTypedCodename(food); +export const codename = createTypedCodename(food); export default codename; diff --git a/words/food-50.ts b/words/food-50.ts index ac60796..e693629 100644 --- a/words/food-50.ts +++ b/words/food-50.ts @@ -65,14 +65,16 @@ export type Food = (typeof food)[number]; /** * Converts a number to a Food codename + * * @param input - The number to convert * @returns A Food name + * * @example * ```typescript * import codename from "codenames/food-50"; - * codename(1234) // "bread" + * codename(1234) // "honey" * ``` */ -const codename = createTypedCodename(food); +export const codename = createTypedCodename(food); export default codename; diff --git a/words/gems-10.ts b/words/gems-10.ts index 64fb669..7b17fa5 100644 --- a/words/gems-10.ts +++ b/words/gems-10.ts @@ -25,14 +25,16 @@ export type Gem = (typeof gems)[number]; /** * Converts a number to a Gem codename + * * @param input - The number to convert * @returns A Gem name + * * @example * ```typescript * import codename from "codenames/gems-10"; * codename(1234) // "ruby" * ``` */ -const codename = createTypedCodename(gems); +export const codename = createTypedCodename(gems); export default codename; diff --git a/words/gems-100.ts b/words/gems-100.ts index 5886021..78d7e3d 100644 --- a/words/gems-100.ts +++ b/words/gems-100.ts @@ -115,14 +115,16 @@ export type Gem = (typeof gems)[number]; /** * Converts a number to a Gem codename + * * @param input - The number to convert * @returns A Gem name + * * @example * ```typescript * import codename from "codenames/gems-100"; - * codename(1234) // "ruby" + * codename(1234) // "carneli" * ``` */ -const codename = createTypedCodename(gems); +export const codename = createTypedCodename(gems); export default codename; diff --git a/words/gems-20.ts b/words/gems-20.ts index 5c4dc08..8d305f1 100644 --- a/words/gems-20.ts +++ b/words/gems-20.ts @@ -35,14 +35,16 @@ export type Gem = (typeof gems)[number]; /** * Converts a number to a Gem codename + * * @param input - The number to convert * @returns A Gem name + * * @example * ```typescript * import codename from "codenames/gems-20"; - * codename(1234) // "ruby" + * codename(1234) // "crystal" * ``` */ -const codename = createTypedCodename(gems); +export const codename = createTypedCodename(gems); export default codename; diff --git a/words/gems-30.ts b/words/gems-30.ts index 0b26a44..913bc45 100644 --- a/words/gems-30.ts +++ b/words/gems-30.ts @@ -45,14 +45,16 @@ export type Gem = (typeof gems)[number]; /** * Converts a number to a Gem codename + * * @param input - The number to convert * @returns A Gem name + * * @example * ```typescript * import codename from "codenames/gems-30"; - * codename(1234) // "ruby" + * codename(1234) // "crystal" * ``` */ -const codename = createTypedCodename(gems); +export const codename = createTypedCodename(gems); export default codename; diff --git a/words/gems-50.ts b/words/gems-50.ts index f97c44e..99708f8 100644 --- a/words/gems-50.ts +++ b/words/gems-50.ts @@ -65,14 +65,16 @@ export type Gem = (typeof gems)[number]; /** * Converts a number to a Gem codename + * * @param input - The number to convert * @returns A Gem name + * * @example * ```typescript * import codename from "codenames/gems-50"; - * codename(1234) // "ruby" + * codename(1234) // "spinel" * ``` */ -const codename = createTypedCodename(gems); +export const codename = createTypedCodename(gems); export default codename; diff --git a/words/nature-10.ts b/words/nature-10.ts index b434179..fc612cd 100644 --- a/words/nature-10.ts +++ b/words/nature-10.ts @@ -25,14 +25,16 @@ export type Nature = (typeof nature)[number]; /** * Converts a number to a Nature codename + * * @param input - The number to convert * @returns A Nature name + * * @example * ```typescript * import codename from "codenames/nature-10"; * codename(1234) // "tree" * ``` */ -const codename = createTypedCodename(nature); +export const codename = createTypedCodename(nature); export default codename; diff --git a/words/nature-100.ts b/words/nature-100.ts index 3473857..7b58042 100644 --- a/words/nature-100.ts +++ b/words/nature-100.ts @@ -115,14 +115,16 @@ export type Nature = (typeof nature)[number]; /** * Converts a number to a Nature codename + * * @param input - The number to convert * @returns A Nature name + * * @example * ```typescript * import codename from "codenames/nature-100"; - * codename(1234) // "tree" + * codename(1234) // "snake" * ``` */ -const codename = createTypedCodename(nature); +export const codename = createTypedCodename(nature); export default codename; diff --git a/words/nature-20.ts b/words/nature-20.ts index 806b19b..4472ebf 100644 --- a/words/nature-20.ts +++ b/words/nature-20.ts @@ -35,14 +35,16 @@ export type Nature = (typeof nature)[number]; /** * Converts a number to a Nature codename + * * @param input - The number to convert * @returns A Nature name + * * @example * ```typescript * import codename from "codenames/nature-20"; - * codename(1234) // "tree" + * codename(1234) // "leaf" * ``` */ -const codename = createTypedCodename(nature); +export const codename = createTypedCodename(nature); export default codename; diff --git a/words/nature-30.ts b/words/nature-30.ts index fb9b2dc..b5e71df 100644 --- a/words/nature-30.ts +++ b/words/nature-30.ts @@ -45,14 +45,16 @@ export type Nature = (typeof nature)[number]; /** * Converts a number to a Nature codename + * * @param input - The number to convert * @returns A Nature name + * * @example * ```typescript * import codename from "codenames/nature-30"; - * codename(1234) // "tree" + * codename(1234) // "leaf" * ``` */ -const codename = createTypedCodename(nature); +export const codename = createTypedCodename(nature); export default codename; diff --git a/words/nature-50.ts b/words/nature-50.ts index 4f9364b..16d520a 100644 --- a/words/nature-50.ts +++ b/words/nature-50.ts @@ -65,14 +65,16 @@ export type Nature = (typeof nature)[number]; /** * Converts a number to a Nature codename + * * @param input - The number to convert * @returns A Nature name + * * @example * ```typescript * import codename from "codenames/nature-50"; - * codename(1234) // "tree" + * codename(1234) // "ocean" * ``` */ -const codename = createTypedCodename(nature); +export const codename = createTypedCodename(nature); export default codename; diff --git a/words/snacks-10.ts b/words/snacks-10.ts index 0e09cb0..9958bf5 100644 --- a/words/snacks-10.ts +++ b/words/snacks-10.ts @@ -25,14 +25,16 @@ export type Snack = (typeof snacks)[number]; /** * Converts a number to a Snack codename + * * @param input - The number to convert * @returns A Snack name + * * @example * ```typescript * import codename from "codenames/snacks-10"; * codename(1234) // "chips" * ``` */ -const codename = createTypedCodename(snacks); +export const codename = createTypedCodename(snacks); export default codename; diff --git a/words/snacks-100.ts b/words/snacks-100.ts index 7a49939..919f3dc 100644 --- a/words/snacks-100.ts +++ b/words/snacks-100.ts @@ -115,14 +115,16 @@ export type Snack = (typeof snacks)[number]; /** * Converts a number to a Snack codename + * * @param input - The number to convert * @returns A Snack name + * * @example * ```typescript * import codename from "codenames/snacks-100"; - * codename(1234) // "chips" + * codename(1234) // "brittle" * ``` */ -const codename = createTypedCodename(snacks); +export const codename = createTypedCodename(snacks); export default codename; diff --git a/words/snacks-20.ts b/words/snacks-20.ts index 3e26b99..b3b6e8d 100644 --- a/words/snacks-20.ts +++ b/words/snacks-20.ts @@ -35,14 +35,16 @@ export type Snack = (typeof snacks)[number]; /** * Converts a number to a Snack codename + * * @param input - The number to convert * @returns A Snack name + * * @example * ```typescript * import codename from "codenames/snacks-20"; - * codename(1234) // "chips" + * codename(1234) // "apple" * ``` */ -const codename = createTypedCodename(snacks); +export const codename = createTypedCodename(snacks); export default codename; diff --git a/words/snacks-30.ts b/words/snacks-30.ts index f81da79..889c5b3 100644 --- a/words/snacks-30.ts +++ b/words/snacks-30.ts @@ -45,14 +45,16 @@ export type Snack = (typeof snacks)[number]; /** * Converts a number to a Snack codename + * * @param input - The number to convert * @returns A Snack name + * * @example * ```typescript * import codename from "codenames/snacks-30"; - * codename(1234) // "chips" + * codename(1234) // "apple" * ``` */ -const codename = createTypedCodename(snacks); +export const codename = createTypedCodename(snacks); export default codename; diff --git a/words/snacks-50.ts b/words/snacks-50.ts index a816a1e..6f7c8b9 100644 --- a/words/snacks-50.ts +++ b/words/snacks-50.ts @@ -65,14 +65,16 @@ export type Snack = (typeof snacks)[number]; /** * Converts a number to a Snack codename + * * @param input - The number to convert * @returns A Snack name + * * @example * ```typescript * import codename from "codenames/snacks-50"; - * codename(1234) // "chips" + * codename(1234) // "grape" * ``` */ -const codename = createTypedCodename(snacks); +export const codename = createTypedCodename(snacks); export default codename;