From db220791ec7786e1acfa7b44852fe5315ebef0ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ngh=E1=BB=8B=20PC?= Date: Fri, 13 Feb 2026 16:29:48 +0700 Subject: [PATCH 1/4] Convert to TypeScript, restructure project, and add ES module support --- .claude/settings.local.json | 10 +++ .gitignore | 5 +- .npmignore | 5 +- CLAUDE.md | 21 ++++++ README.md | 11 ++- bin/randomstring | 2 +- index.js | 1 - lib/charset.js | 71 -------------------- lib/randomstring.js | 106 ----------------------------- package.json | 31 +++++++-- src/charset.ts | 69 +++++++++++++++++++ src/index.ts | 129 ++++++++++++++++++++++++++++++++++++ src/randombytes.d.ts | 5 ++ test/cjs.js | 41 ++++++++++++ test/esm.mjs | 41 ++++++++++++ tsconfig.json | 15 +++++ tsup.config.ts | 9 +++ 17 files changed, 385 insertions(+), 187 deletions(-) create mode 100644 .claude/settings.local.json create mode 100644 CLAUDE.md delete mode 100644 index.js delete mode 100644 lib/charset.js delete mode 100644 lib/randomstring.js create mode 100644 src/charset.ts create mode 100644 src/index.ts create mode 100644 src/randombytes.d.ts create mode 100644 test/cjs.js create mode 100644 test/esm.mjs create mode 100644 tsconfig.json create mode 100644 tsup.config.ts diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..6966082 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,10 @@ +{ + "permissions": { + "allow": [ + "Bash(npm install:*)", + "Bash(npm run build:*)", + "Bash(npm test:*)", + "Bash(node bin/randomstring:*)" + ] + } +} diff --git a/.gitignore b/.gitignore index 825c014..73961ab 100755 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,7 @@ *.log node_modules package-lock.json -test.js \ No newline at end of file +test.js +.idea/ +dist/ +bun.lock diff --git a/.npmignore b/.npmignore index 8f42016..75e9a53 100755 --- a/.npmignore +++ b/.npmignore @@ -1,3 +1,6 @@ .DS_Store *.log -test.js \ No newline at end of file +test.js +src/ +tsconfig.json +tsup.config.ts diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..9bb0823 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,21 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Commands + +- **Install dependencies:** `npm install` +- **Run all tests:** `npm test` (runs mocha) +- **Run a single test:** `npx mocha --grep "test name pattern"` + +## Architecture + +This is the `randomstring` npm package — a small library for generating random strings with configurable charset, length, and capitalization. + +- `index.js` — Entry point, re-exports `lib/randomstring.js` +- `lib/randomstring.js` — Core `generate(options, cb)` function. Supports sync (return value) and async (callback) modes. Uses `randombytes` for crypto-safe randomness with a `Math.random()` fallback for environments where crypto is unavailable (e.g. React Native) +- `lib/charset.js` — `Charset` class that builds the character pool from named presets (`alphanumeric`, `alphabetic`, `numeric`, `hex`, `binary`, `octal`) or custom strings. Handles capitalization, readability filtering, and deduplication +- `bin/randomstring` — CLI entry point, parses `length=N`, `charset=X`, `capitalization=X`, `readable` args +- `test/index.js` — Mocha tests covering all options (sync and async), uniqueness, and distribution bias checks + +The string generation works by requesting random bytes and mapping each byte to a character in the charset, skipping bytes above `maxByte` (256 - 256 % charsetLength) to avoid modulo bias. For async mode, it recursively requests more bytes until the target length is reached. diff --git a/README.md b/README.md index 5b4c6b3..e56d770 100755 --- a/README.md +++ b/README.md @@ -15,7 +15,11 @@ npm install randomstring ## Usage ```javascript -var randomstring = require("randomstring"); +// CommonJS +const randomstring = require("randomstring"); + +// ES Modules +import randomstring from "randomstring"; randomstring.generate(); // >> "XwPp9xazJ0ku5CZnlmgAx2Dld8SHkAeT" @@ -46,6 +50,8 @@ randomstring.generate({ ``` +TypeScript types are included out of the box — no need to install `@types` separately. + ## API `randomstring.` @@ -81,10 +87,11 @@ randomstring.generate({ $ randomstring length=24 charset=github readable > hthbtgiguihgbuttuutubugg -## Tests +## Development ``` npm install +npm run build npm test ``` diff --git a/bin/randomstring b/bin/randomstring index 30a6e27..1772685 100755 --- a/bin/randomstring +++ b/bin/randomstring @@ -1,6 +1,6 @@ #!/usr/bin/env node -var randomstring = require('..'); +var randomstring = require('../dist/index.js'); var options = {}; diff --git a/index.js b/index.js deleted file mode 100644 index 1fddded..0000000 --- a/index.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require("./lib/randomstring"); \ No newline at end of file diff --git a/lib/charset.js b/lib/charset.js deleted file mode 100644 index 8abb79a..0000000 --- a/lib/charset.js +++ /dev/null @@ -1,71 +0,0 @@ -function Charset() { - this.chars = ''; -} - -Charset.prototype.setType = function(type) { - if (Array.isArray(type)) { - for (var i=0; i < type.length; i++) { - this.chars += this.getCharacters(type[i]); - } - } - else { - this.chars = this.getCharacters(type); - } -} - -Charset.prototype.getCharacters = function(type) { - var chars; - - var numbers = '0123456789'; - var charsLower = 'abcdefghijklmnopqrstuvwxyz'; - var charsUpper = charsLower.toUpperCase(); - var hexChars = 'abcdef'; - var binaryChars = '01'; - var octalChars = '01234567'; - - if (type === 'alphanumeric') { - chars = numbers + charsLower + charsUpper; - } - else if (type === 'numeric') { - chars = numbers; - } - else if (type === 'alphabetic') { - chars = charsLower + charsUpper; - } - else if (type === 'hex') { - chars = numbers + hexChars; - } - else if (type === 'binary') { - chars = binaryChars; - } - else if (type === 'octal') { - chars = octalChars; - } - else { - chars = type; - } - - return chars; -} - -Charset.prototype.removeUnreadable = function() { - var unreadableChars = /[0OIl]/g; - this.chars = this.chars.replace(unreadableChars, ''); -} - -Charset.prototype.setcapitalization = function(capitalization) { - if (capitalization === 'uppercase') { - this.chars = this.chars.toUpperCase(); - } - else if (capitalization === 'lowercase') { - this.chars = this.chars.toLowerCase(); - } -} - -Charset.prototype.removeDuplicates = function() { - var charMap = this.chars.split(''); - charMap = [...new Set(charMap)]; - this.chars = charMap.join(''); -} - -module.exports = exports = Charset; diff --git a/lib/randomstring.js b/lib/randomstring.js deleted file mode 100644 index 2200f71..0000000 --- a/lib/randomstring.js +++ /dev/null @@ -1,106 +0,0 @@ -"use strict"; - -var randomBytes = require('randombytes'); -var Charset = require('./charset.js'); - - -function unsafeRandomBytes(length) { - var stack = []; - for (var i = 0; i < length; i++) { - stack.push(Math.floor(Math.random() * 255)); - } - - return { - length, - readUInt8: function (index) { - return stack[index]; - } - }; -} - -function safeRandomBytes(length) { - try { - return randomBytes(length); - } catch (e) { - /* React/React Native Fix + Eternal loop removed */ - return unsafeRandomBytes(length); - } -} - -function processString(buf, initialString, chars, reqLen, maxByte) { - var string = initialString; - for (var i = 0; i < buf.length && string.length < reqLen; i++) { - var randomByte = buf.readUInt8(i); - if (randomByte < maxByte) { - string += chars.charAt(randomByte % chars.length); - } - } - return string; -} - -function getAsyncString(string, chars, length, maxByte, cb) { - randomBytes(length, function(err, buf) { - if (err) { - // Since it is waiting for entropy, errors are legit and we shouldn't just keep retrying - cb(err); - } - var generatedString = processString(buf, string, chars, length, maxByte); - if (generatedString.length < length) { - getAsyncString(generatedString, chars, length, maxByte, cb); - } else { - cb(null, generatedString); - } - }) -} - -exports.generate = function(options, cb) { - var charset = new Charset(); - - var length, chars, capitalization, string = ''; - - // Handle options - if (typeof options === 'object') { - length = typeof options.length === 'number' ? options.length : 32; - - if (options.charset) { - charset.setType(options.charset); - } - else { - charset.setType('alphanumeric'); - } - - if (options.capitalization) { - charset.setcapitalization(options.capitalization); - } - - if (options.readable) { - charset.removeUnreadable(); - } - - charset.removeDuplicates(); - } - else if (typeof options === 'number') { - length = options; - charset.setType('alphanumeric'); - } - else { - length = 32; - charset.setType('alphanumeric'); - } - - // Generate the string - var charsLen = charset.chars.length; - var maxByte = 256 - (256 % charsLen); - - if (!cb) { - while (string.length < length) { - var buf = safeRandomBytes(Math.ceil(length * 256 / maxByte)); - string = processString(buf, string, charset.chars, length, maxByte); - } - - return string; - } - - getAsyncString(string, charset.chars, length, maxByte, cb); - -}; diff --git a/package.json b/package.json index 1950de0..20a2805 100755 --- a/package.json +++ b/package.json @@ -1,26 +1,49 @@ { "name": "randomstring", "version": "1.3.1", - "author": "Elias Klughammer (http://www.klughammer.com)", + "author": "Elias Klughammer (http://www.klughammer.com), Nelie Taylor (https://n96.dev)", "description": "A module for generating random strings", "homepage": "https://github.com/klughammer/node-randomstring", "repository": { "type": "git", "url": "git://github.com/klughammer/node-randomstring.git" }, - "main": "./index", + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "import": { + "types": "./dist/index.d.mts", + "default": "./dist/index.mjs" + }, + "require": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + } + }, "engines": { "node": "*" }, + "files": [ + "dist", + "bin" + ], "dependencies": { "randombytes": "2.1.0" }, "devDependencies": { - "mocha": "^10.0.0" + "@types/node": "^25.2.3", + "mocha": "^11.7.5", + "tsup": "^8.4.0", + "typescript": "^5.7.0" }, "license": "MIT", "scripts": { - "test": "mocha" + "build": "tsup", + "test": "npm run build && mocha", + "prepublishOnly": "npm run build" }, "bin": "bin/randomstring" } diff --git a/src/charset.ts b/src/charset.ts new file mode 100644 index 0000000..97aa55b --- /dev/null +++ b/src/charset.ts @@ -0,0 +1,69 @@ +export type CharsetType = + | 'alphanumeric' + | 'numeric' + | 'alphabetic' + | 'hex' + | 'binary' + | 'octal' + | (string & {}); + +export class Charset { + chars: string; + + constructor() { + this.chars = ''; + } + + setType(type: CharsetType | CharsetType[]): void { + if (Array.isArray(type)) { + for (let i = 0; i < type.length; i++) { + this.chars += this.getCharacters(type[i]); + } + } else { + this.chars = this.getCharacters(type); + } + } + + getCharacters(type: CharsetType): string { + const numbers = '0123456789'; + const charsLower = 'abcdefghijklmnopqrstuvwxyz'; + const charsUpper = charsLower.toUpperCase(); + const hexChars = 'abcdef'; + const binaryChars = '01'; + const octalChars = '01234567'; + + if (type === 'alphanumeric') { + return numbers + charsLower + charsUpper; + } else if (type === 'numeric') { + return numbers; + } else if (type === 'alphabetic') { + return charsLower + charsUpper; + } else if (type === 'hex') { + return numbers + hexChars; + } else if (type === 'binary') { + return binaryChars; + } else if (type === 'octal') { + return octalChars; + } else { + return type; + } + } + + removeUnreadable(): void { + const unreadableChars = /[0OIl]/g; + this.chars = this.chars.replace(unreadableChars, ''); + } + + setcapitalization(capitalization: string): void { + if (capitalization === 'uppercase') { + this.chars = this.chars.toUpperCase(); + } else if (capitalization === 'lowercase') { + this.chars = this.chars.toLowerCase(); + } + } + + removeDuplicates(): void { + const charMap = this.chars.split(''); + this.chars = [...new Set(charMap)].join(''); + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..4b6f8fa --- /dev/null +++ b/src/index.ts @@ -0,0 +1,129 @@ +import randomBytes from 'randombytes'; +import { Charset, CharsetType } from './charset'; + +export interface GenerateOptions { + length?: number; + charset?: CharsetType | CharsetType[]; + capitalization?: 'uppercase' | 'lowercase'; + readable?: boolean; +} + +type GenerateCallback = (err: Error | null, result?: string) => void; + +interface UnsafeBuffer { + length: number; + readUInt8(index: number): number; +} + +function unsafeRandomBytes(length: number): UnsafeBuffer { + const stack: number[] = []; + for (let i = 0; i < length; i++) { + stack.push(Math.floor(Math.random() * 255)); + } + + return { + length, + readUInt8(index: number) { + return stack[index]; + }, + }; +} + +function safeRandomBytes(length: number): Buffer | UnsafeBuffer { + try { + return randomBytes(length); + } catch (e) { + /* React/React Native Fix + Eternal loop removed */ + return unsafeRandomBytes(length); + } +} + +function processString( + buf: Buffer | UnsafeBuffer, + initialString: string, + chars: string, + reqLen: number, + maxByte: number, +): string { + let string = initialString; + for (let i = 0; i < buf.length && string.length < reqLen; i++) { + const randomByte = buf.readUInt8(i); + if (randomByte < maxByte) { + string += chars.charAt(randomByte % chars.length); + } + } + return string; +} + +function getAsyncString( + string: string, + chars: string, + length: number, + maxByte: number, + cb: GenerateCallback, +): void { + randomBytes(length, function (err: Error | null, buf: Buffer) { + if (err) { + // Since it is waiting for entropy, errors are legit and we shouldn't just keep retrying + cb(err); + return; + } + const generatedString = processString(buf, string, chars, length, maxByte); + if (generatedString.length < length) { + getAsyncString(generatedString, chars, length, maxByte, cb); + } else { + cb(null, generatedString); + } + }); +} + +export function generate(options?: GenerateOptions | number | null, cb?: GenerateCallback): string | void { + const charset = new Charset(); + + let length: number; + let string = ''; + + // Handle options + if (typeof options === 'object' && options !== null) { + length = typeof options.length === 'number' ? options.length : 32; + + if (options.charset) { + charset.setType(options.charset); + } else { + charset.setType('alphanumeric'); + } + + if (options.capitalization) { + charset.setcapitalization(options.capitalization); + } + + if (options.readable) { + charset.removeUnreadable(); + } + + charset.removeDuplicates(); + } else if (typeof options === 'number') { + length = options; + charset.setType('alphanumeric'); + } else { + length = 32; + charset.setType('alphanumeric'); + } + + // Generate the string + const charsLen = charset.chars.length; + const maxByte = 256 - (256 % charsLen); + + if (!cb) { + while (string.length < length) { + const buf = safeRandomBytes(Math.ceil((length * 256) / maxByte)); + string = processString(buf, string, charset.chars, length, maxByte); + } + + return string; + } + + getAsyncString(string, charset.chars, length, maxByte, cb); +} + +export default { generate }; diff --git a/src/randombytes.d.ts b/src/randombytes.d.ts new file mode 100644 index 0000000..7840f17 --- /dev/null +++ b/src/randombytes.d.ts @@ -0,0 +1,5 @@ +declare module 'randombytes' { + function randomBytes(size: number): Buffer; + function randomBytes(size: number, callback: (err: Error | null, buf: Buffer) => void): void; + export default randomBytes; +} diff --git a/test/cjs.js b/test/cjs.js new file mode 100644 index 0000000..b2398e7 --- /dev/null +++ b/test/cjs.js @@ -0,0 +1,41 @@ +"use strict"; + +var assert = require("assert"); + +describe("CJS require", function() { + + it("exports generate as a named export", function() { + var randomstring = require(".."); + assert.equal(typeof randomstring.generate, "function"); + }); + + it("generate returns a string", function() { + var randomstring = require(".."); + var result = randomstring.generate(); + assert.equal(typeof result, "string"); + assert.equal(result.length, 32); + }); + + it("generate accepts options", function() { + var randomstring = require(".."); + var result = randomstring.generate({ length: 10, charset: "numeric" }); + assert.equal(result.length, 10); + assert.equal(result.search(/\D/), -1); + }); + + it("generate works with callback", function(done) { + var randomstring = require(".."); + randomstring.generate({ length: 16 }, function(err, result) { + assert.equal(err, null); + assert.equal(typeof result, "string"); + assert.equal(result.length, 16); + done(); + }); + }); + + it("default export contains generate", function() { + var randomstring = require(".."); + assert.equal(typeof randomstring.default, "object"); + assert.equal(typeof randomstring.default.generate, "function"); + }); +}); diff --git a/test/esm.mjs b/test/esm.mjs new file mode 100644 index 0000000..bf860ed --- /dev/null +++ b/test/esm.mjs @@ -0,0 +1,41 @@ +import assert from "assert"; +import randomstring from "../dist/index.mjs"; +import { generate } from "../dist/index.mjs"; + +describe("ESM import", function() { + + it("default export has generate", function() { + assert.equal(typeof randomstring.generate, "function"); + }); + + it("named export generate works", function() { + assert.equal(typeof generate, "function"); + }); + + it("default export generate returns a string", function() { + var result = randomstring.generate(); + assert.equal(typeof result, "string"); + assert.equal(result.length, 32); + }); + + it("named generate returns a string", function() { + var result = generate(); + assert.equal(typeof result, "string"); + assert.equal(result.length, 32); + }); + + it("generate accepts options", function() { + var result = generate({ length: 10, charset: "numeric" }); + assert.equal(result.length, 10); + assert.equal(result.search(/\D/), -1); + }); + + it("generate works with callback", function(done) { + generate({ length: 16 }, function(err, result) { + assert.equal(err, null); + assert.equal(typeof result, "string"); + assert.equal(result.length, 16); + done(); + }); + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..ae6ee35 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2018", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "declaration": true, + "outDir": "dist", + "rootDir": "src", + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src"] +} diff --git a/tsup.config.ts b/tsup.config.ts new file mode 100644 index 0000000..aa709a7 --- /dev/null +++ b/tsup.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + entry: ['src/index.ts'], + format: ['cjs', 'esm'], + dts: true, + clean: true, + splitting: false, +}); From 6de64b25a05fdfb822fa211ef1d47e74048166b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ngh=E1=BB=8B=20PC?= Date: Fri, 13 Feb 2026 16:31:31 +0700 Subject: [PATCH 2/4] Bump version to 2.0.0 for major update following TypeScript conversion and ES module support. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 20a2805..154b69e 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "randomstring", - "version": "1.3.1", + "version": "2.0.0", "author": "Elias Klughammer (http://www.klughammer.com), Nelie Taylor (https://n96.dev)", "description": "A module for generating random strings", "homepage": "https://github.com/klughammer/node-randomstring", From 7864febfa76abcdd2c51942e91a110dbff121103 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ngh=E1=BB=8B=20PC?= Date: Fri, 13 Feb 2026 16:32:38 +0700 Subject: [PATCH 3/4] Remove deprecated settings file and update .gitignore to exclude .claude/ directory. --- .claude/settings.local.json | 10 ---------- .gitignore | 1 + 2 files changed, 1 insertion(+), 10 deletions(-) delete mode 100644 .claude/settings.local.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index 6966082..0000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(npm install:*)", - "Bash(npm run build:*)", - "Bash(npm test:*)", - "Bash(node bin/randomstring:*)" - ] - } -} diff --git a/.gitignore b/.gitignore index 73961ab..393ec00 100755 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ test.js .idea/ dist/ bun.lock +.claude/ From 9d33181d0cb15422c08c15670f005a0942aa9e69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ngh=E1=BB=8B=20PC?= Date: Fri, 13 Feb 2026 16:33:04 +0700 Subject: [PATCH 4/4] Remove CLAUDE.md as it is no longer relevant to the project structure. --- CLAUDE.md | 21 --------------------- 1 file changed, 21 deletions(-) delete mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 9bb0823..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,21 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Commands - -- **Install dependencies:** `npm install` -- **Run all tests:** `npm test` (runs mocha) -- **Run a single test:** `npx mocha --grep "test name pattern"` - -## Architecture - -This is the `randomstring` npm package — a small library for generating random strings with configurable charset, length, and capitalization. - -- `index.js` — Entry point, re-exports `lib/randomstring.js` -- `lib/randomstring.js` — Core `generate(options, cb)` function. Supports sync (return value) and async (callback) modes. Uses `randombytes` for crypto-safe randomness with a `Math.random()` fallback for environments where crypto is unavailable (e.g. React Native) -- `lib/charset.js` — `Charset` class that builds the character pool from named presets (`alphanumeric`, `alphabetic`, `numeric`, `hex`, `binary`, `octal`) or custom strings. Handles capitalization, readability filtering, and deduplication -- `bin/randomstring` — CLI entry point, parses `length=N`, `charset=X`, `capitalization=X`, `readable` args -- `test/index.js` — Mocha tests covering all options (sync and async), uniqueness, and distribution bias checks - -The string generation works by requesting random bytes and mapping each byte to a character in the charset, skipping bytes above `maxByte` (256 - 256 % charsetLength) to avoid modulo bias. For async mode, it recursively requests more bytes until the target length is reached.