Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: ai/nanoid
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 3.1.25
Choose a base ref
...
head repository: ai/nanoid
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 3.3.1
Choose a head ref

Commits on Sep 10, 2021

  1. Add chinese doc (#296)

    * docs: add chinese docs
    
    * docs: add chinese docs
    
    * docs: update docs format
    
    Co-authored-by: wenqiang <wenqiang@didiglobal.com>
    Qiang and wenqiang authored Sep 10, 2021

    Verified

    This commit was signed with the committer’s verified signature.
    primeos Michael Weiss
    Copy the full SHA
    4a4d1da View commit details
  2. Copy the full SHA
    7bbd7a5 View commit details
  3. Update dependencies

    ai committed Sep 10, 2021
    Copy the full SHA
    71dace5 View commit details
  4. Copy the full SHA
    e6c0f61 View commit details
  5. Try to mark current language

    ai committed Sep 10, 2021
    Copy the full SHA
    5ec1fb0 View commit details

Commits on Sep 12, 2021

  1. Fix Chinese docs TOC links

    ai committed Sep 12, 2021
    Copy the full SHA
    10f8091 View commit details
  2. Add Russian translation

    ai committed Sep 12, 2021
    Copy the full SHA
    57884ee View commit details
  3. Update dependencies

    ai committed Sep 12, 2021
    Copy the full SHA
    9a37a81 View commit details
  4. Typo

    ai committed Sep 12, 2021
    Copy the full SHA
    6220e6e View commit details
  5. Change symbols

    ai committed Sep 12, 2021
    Copy the full SHA
    5578ee0 View commit details

Commits on Sep 13, 2021

  1. Fix some word and navigation failures (#297)

    * docs: add chinese docs
    
    * docs: add chinese docs
    
    * docs: update docs format
    
    * Update layout style
    
    * Fix some word and navigation failures
    
    Co-authored-by: wenqiang <wenqiang@didiglobal.com>
    Qiang and wenqiang authored Sep 13, 2021
    Copy the full SHA
    c46937e View commit details

Commits on Sep 14, 2021

  1. Copy the full SHA
    0b244bb View commit details
  2. Update README.ru.md (#298)

    * Update README.ru.md
    
    * Update README.ru.md
    
    Co-authored-by: Andrey Sitnik <andrey@sitnik.ru>
    gwer and ai authored Sep 14, 2021
    Copy the full SHA
    439f95f View commit details
  3. Typo

    ai committed Sep 14, 2021
    6
    Copy the full SHA
    3c4202a View commit details
  4. Update dependencies

    ai committed Sep 14, 2021
    Copy the full SHA
    85d1574 View commit details
  5. Fix URL

    ai authored Sep 14, 2021
    Copy the full SHA
    849e04a View commit details

Commits on Sep 15, 2021

  1. Big update README.ru.md (#300)

    * Big update README.ru.md
    
    * Fixes after review
    
    * Remove yarn
    
    * Fix ToC, remove cryptography
    
    * устойчивость к подбору ID
    
    * Строки не длиннее 80 символов
    gwer authored Sep 15, 2021
    Copy the full SHA
    72eeaa6 View commit details
  2. Change section order

    ai committed Sep 15, 2021
    1
    Copy the full SHA
    b655154 View commit details

Commits on Sep 16, 2021

  1. Move Tools below

    ai committed Sep 16, 2021
    Copy the full SHA
    9ff2290 View commit details
  2. Add code format

    ai committed Sep 16, 2021
    Copy the full SHA
    289b5b2 View commit details
  3. 2
    Copy the full SHA
    292fe82 View commit details
  4. Add chinese doc (#301)

    * docs: add chinese docs
    
    * docs: add chinese docs
    
    * docs: update docs format
    
    * Update layout style
    
    * Fix some word and navigation failures
    
    * Update chinese doc
    
    * Update chinese doc
    
    * Update chinese doc
    
    * Update chinese doc
    
    Co-authored-by: wenqiang <wenqiang@didiglobal.com>
    Qiang and wenqiang authored Sep 16, 2021
    Copy the full SHA
    f23f7bf View commit details

Commits on Sep 21, 2021

  1. Bump nth-check from 2.0.0 to 2.0.1 (#302)

    Bumps [nth-check](https://github.com/fb55/nth-check) from 2.0.0 to 2.0.1.
    - [Release notes](https://github.com/fb55/nth-check/releases)
    - [Commits](fb55/nth-check@v2.0.0...v2.0.1)
    
    ---
    updated-dependencies:
    - dependency-name: nth-check
      dependency-type: indirect
    ...
    
    Signed-off-by: dependabot[bot] <support@github.com>
    
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
    dependabot[bot] authored Sep 21, 2021
    1
    Copy the full SHA
    6fbda5a View commit details
  2. Replace colorette with nanocolors

    ai committed Sep 21, 2021
    1
    Copy the full SHA
    70deb10 View commit details
  3. Update dependencies

    ai committed Sep 21, 2021
    1
    Copy the full SHA
    fbc100b View commit details

Commits on Sep 26, 2021

  1. 1
    Copy the full SHA
    eafb6b3 View commit details
  2. Normalize names

    ai committed Sep 26, 2021
    1
    Copy the full SHA
    1c4fe7e View commit details
  3. Update dependencies

    ai committed Sep 26, 2021
    1
    Copy the full SHA
    7493375 View commit details
  4. Update benchmark results

    ai committed Sep 26, 2021
    1
    Copy the full SHA
    1db1435 View commit details
  5. Update dependencies

    ai committed Sep 26, 2021
    1
    Copy the full SHA
    50fa9b4 View commit details
  6. Update dual-publish

    ai committed Sep 26, 2021
    1
    Copy the full SHA
    43d31a4 View commit details
  7. Release 3.1.26 version

    ai committed Sep 26, 2021
    1
    Copy the full SHA
    4b2d97e View commit details
  8. Fix devDependnecies

    ai committed Sep 26, 2021
    Copy the full SHA
    4510bc1 View commit details
  9. Release 3.1.27 version

    ai committed Sep 26, 2021
    Copy the full SHA
    6c668be View commit details
  10. Fix README translation cleaning

    ai committed Sep 26, 2021
    Copy the full SHA
    3c22f99 View commit details
  11. Release 3.1.28 version

    ai committed Sep 26, 2021
    Copy the full SHA
    61b6fe8 View commit details

Commits on Sep 29, 2021

  1. add crypto.randomUUID() to benchmarks (#305)

    * add crypto.randomUUID() to benchmarks
    
    * set disableEntropyCache by true
    Eugeno authored Sep 29, 2021
    Copy the full SHA
    7c7ca20 View commit details
  2. Update dependencies

    ai committed Sep 29, 2021
    Copy the full SHA
    fe10156 View commit details
  3. Copy the full SHA
    b0d7bfa View commit details

Commits on Oct 5, 2021

  1. Copy the full SHA
    c66da35 View commit details
  2. Copy the full SHA
    17ef239 View commit details
  3. Better way to use GitHub Actions

    ai committed Oct 5, 2021
    Copy the full SHA
    8df5be5 View commit details
  4. Fix CI

    ai committed Oct 5, 2021
    Copy the full SHA
    8e31b35 View commit details
  5. Add benchmark to CI

    ai committed Oct 5, 2021
    Copy the full SHA
    ac7ba2e View commit details
  6. Clean up benchmark code

    ai committed Oct 5, 2021
    Copy the full SHA
    140d2df View commit details
  7. Fix tool name in benchmark

    ai committed Oct 5, 2021
    Copy the full SHA
    c332c1d View commit details
  8. Add uid and update benchmark

    ai committed Oct 5, 2021
    5
    Copy the full SHA
    769d281 View commit details
  9. Clean comments in npm package

    ai committed Oct 5, 2021
    Copy the full SHA
    14b0889 View commit details
  10. Release 3.1.29 version

    ai committed Oct 5, 2021
    Copy the full SHA
    5870ed0 View commit details
  11. Copy the full SHA
    464bc82 View commit details
73 changes: 63 additions & 10 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -1,43 +1,96 @@
name: Test
on:
push:
branches:
- main
pull_request:
env:
FORCE_COLOR: 2
jobs:
full:
name: Node.js 16 Full
name: Node.js 17 Full
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@v2
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: latest
- name: Install Node.js
uses: actions/setup-node@v2
with:
node-version: 16
node-version: 17
cache: pnpm
- name: Install dependencies
uses: bahmutov/npm-install@v1
run: pnpm install --frozen-lockfile --ignore-scripts
- name: Run tests
run: yarn test
run: pnpm test
env:
FORCE_COLOR: 2
short:
runs-on: ubuntu-latest
strategy:
matrix:
node-version:
- 16
- 14
- 12
- 10
name: Node.js ${{ matrix.node-version }} Quick
steps:
- name: Checkout the repository
uses: actions/checkout@v2
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: latest
- name: Install Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
cache: pnpm
- name: Install dependencies
uses: bahmutov/npm-install@v1
run: pnpm install --frozen-lockfile --ignore-scripts
- name: Run unit tests
run: pnpm unit
env:
FORCE_COLOR: 2
old:
runs-on: ubuntu-latest
name: Node.js 10 Quick
steps:
- name: Checkout the repository
uses: actions/checkout@v2
- name: Install pnpm
uses: pnpm/action-setup@v1
with:
install-command: yarn install --ignore-engines
version: 3
env:
ACTIONS_ALLOW_UNSECURE_COMMANDS: true
- name: Install Node.js 10
uses: actions/setup-node@v2
with:
node-version: 10
- name: Install dependencies
run: pnpm install --frozen-lockfile --ignore-scripts
- name: Run unit tests
run: npx jest
run: pnpm unit
env:
FORCE_COLOR: 2
benchmark:
name: Benchmark
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@v2
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: latest
- name: Install Node.js
uses: actions/setup-node@v2
with:
node-version: 16
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile --ignore-scripts
- name: Run benchmark
run: node ./test/benchmark.js
4 changes: 0 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
node_modules/
yarn-error.log

coverage/

test/demo/build
.parcel-cache
4 changes: 0 additions & 4 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
yarn-error.log
yarn.lock

test/
tsconfig.json
coverage/

img/
.parcel-cache
32 changes: 32 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,38 @@
# Change Log
This project adheres to [Semantic Versioning](http://semver.org/).

## 3.3.1
* Reduced package size.

## 3.3
* Added `size` argument to function from `customAlphabet` (by Stefan Sundin).

## 3.2
* Added `--size` and `--alphabet` arguments to binary (by Vitaly Baev).

## 3.1.32
* Reduced `async` exports size (by Artyom Arutyunyan).
* Moved from Jest to uvu (by Vitaly Baev).

## 3.1.31
* Fixed collision vulnerability on object in `size` (by Artyom Arutyunyan).

## 3.1.30
* Reduced size for project with `brotli` compression (by Anton Khlynovskiy).

## 3.1.29
* Reduced npm package size.

## 3.1.28
* Reduced npm package size.

## 3.1.27
* Cleaned `dependencies` from development tools.

## 3.1.26
* Improved performance (by Eitan Har-Shoshanim).
* Reduced npm package size.

## 3.1.25
* Fixed `browserify` support.

408 changes: 408 additions & 0 deletions README.id-ID.md

Large diffs are not rendered by default.

436 changes: 228 additions & 208 deletions README.md

Large diffs are not rendered by default.

521 changes: 521 additions & 0 deletions README.ru.md

Large diffs are not rendered by default.

499 changes: 499 additions & 0 deletions README.zh-CN.md

Large diffs are not rendered by default.

15 changes: 7 additions & 8 deletions async/index.browser.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
let random = bytes =>
Promise.resolve(crypto.getRandomValues(new Uint8Array(bytes)))
let random = async bytes => crypto.getRandomValues(new Uint8Array(bytes))

let customAlphabet = (alphabet, size) => {
let customAlphabet = (alphabet, defaultSize = 21) => {
// First, a bitmask is necessary to generate the ID. The bitmask makes bytes
// values closer to the alphabet size. The bitmask calculates the closest
// `2^31 - 1` number, which exceeds the alphabet size.
@@ -23,9 +22,9 @@ let customAlphabet = (alphabet, size) => {

// `-~f => Math.ceil(f)` if f is a float
// `-~i => i + 1` if i is an integer
let step = -~((1.6 * mask * size) / alphabet.length)
let step = -~((1.6 * mask * defaultSize) / alphabet.length)

return () => {
return async (size = defaultSize) => {
let id = ''
while (true) {
let bytes = crypto.getRandomValues(new Uint8Array(step))
@@ -34,13 +33,13 @@ let customAlphabet = (alphabet, size) => {
while (i--) {
// Adding `|| ''` refuses a random byte that exceeds the alphabet size.
id += alphabet[bytes[i] & mask] || ''
if (id.length === size) return Promise.resolve(id)
if (id.length === size) return id
}
}
}
}

let nanoid = (size = 21) => {
let nanoid = async (size = 21) => {
let id = ''
let bytes = crypto.getRandomValues(new Uint8Array(size))

@@ -64,7 +63,7 @@ let nanoid = (size = 21) => {
id += '-'
}
}
return Promise.resolve(id)
return id
}

module.exports = { nanoid, customAlphabet, random }
8 changes: 4 additions & 4 deletions async/index.d.ts
Original file line number Diff line number Diff line change
@@ -24,8 +24,8 @@ export function nanoid(size?: number): Promise<string>
* will not be secure.
*
* @param alphabet Alphabet used to generate the ID.
* @param size Size of the ID.
* @returns A promise with a random string.
* @param defaultSize Size of the ID. The default size is 21.
* @returns A function that returns a promise with a random string.
*
* ```js
* import { customAlphabet } from 'nanoid/async'
@@ -37,8 +37,8 @@ export function nanoid(size?: number): Promise<string>
*/
export function customAlphabet(
alphabet: string,
size: number
): () => Promise<string>
defaultSize?: number
): (size?: number) => Promise<string>

/**
* Generate an array of random bytes collected from hardware noise.
10 changes: 5 additions & 5 deletions async/index.js
Original file line number Diff line number Diff line change
@@ -18,7 +18,7 @@ let random = bytes =>
})
})

let customAlphabet = (alphabet, size) => {
let customAlphabet = (alphabet, defaultSize = 21) => {
// First, a bitmask is necessary to generate the ID. The bitmask makes bytes
// values closer to the alphabet size. The bitmask calculates the closest
// `2^31 - 1` number, which exceeds the alphabet size.
@@ -36,9 +36,9 @@ let customAlphabet = (alphabet, size) => {
// The number of random bytes gets decided upon the ID size, mask,
// alphabet size, and magic number 1.6 (using 1.6 peaks at performance
// according to benchmarks).
let step = Math.ceil((1.6 * mask * size) / alphabet.length)
let step = Math.ceil((1.6 * mask * defaultSize) / alphabet.length)

let tick = id =>
let tick = (id, size = defaultSize) =>
random(step).then(bytes => {
// A compact alternative for `for (var i = 0; i < step; i++)`.
let i = step
@@ -47,10 +47,10 @@ let customAlphabet = (alphabet, size) => {
id += alphabet[bytes[i] & mask] || ''
if (id.length === size) return id
}
return tick(id)
return tick(id, size)
})

return () => tick('')
return size => tick('', size)
}

let nanoid = (size = 21) =>
10 changes: 5 additions & 5 deletions async/index.native.js
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@ let { urlAlphabet } = require('../url-alphabet')

let random = getRandomBytesAsync

let customAlphabet = (alphabet, size) => {
let customAlphabet = (alphabet, defaultSize = 21) => {
// First, a bitmask is necessary to generate the ID. The bitmask makes bytes
// values closer to the alphabet size. The bitmask calculates the closest
// `2^31 - 1` number, which exceeds the alphabet size.
@@ -22,9 +22,9 @@ let customAlphabet = (alphabet, size) => {
// The number of random bytes gets decided upon the ID size, mask,
// alphabet size, and magic number 1.6 (using 1.6 peaks at performance
// according to benchmarks).
let step = Math.ceil((1.6 * mask * size) / alphabet.length)
let step = Math.ceil((1.6 * mask * defaultSize) / alphabet.length)

let tick = id =>
let tick = (id, size = defaultSize) =>
random(step).then(bytes => {
// A compact alternative for `for (var i = 0; i < step; i++)`.
let i = step
@@ -33,10 +33,10 @@ let customAlphabet = (alphabet, size) => {
id += alphabet[bytes[i] & mask] || ''
if (id.length === size) return id
}
return tick(id)
return tick(id, size)
})

return () => tick('')
return size => tick('', size)
}

let nanoid = (size = 21) =>
54 changes: 52 additions & 2 deletions bin/nanoid.cjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,55 @@
#!/usr/bin/env node

let { nanoid } = require('..')
let { nanoid, customAlphabet } = require('..')

process.stdout.write(nanoid() + '\n')
function print(msg) {
process.stdout.write(msg + '\n')
}

function error(msg) {
process.stderr.write(msg + '\n')
process.exit(1)
}

if (process.argv.includes('--help') || process.argv.includes('-h')) {
print(`
Usage
$ nanoid [options]
Options
-s, --size Generated ID size
-a, --alphabet Alphabet to use
-h, --help Show this help
Examples
$ nano --s 15
S9sBF77U6sDB8Yg
$ nano --size 10 --alphabet abc
bcabababca`)
process.exit()
}

let alphabet, size
for (let i = 2; i < process.argv.length; i++) {
let arg = process.argv[i]
if (arg === '--size' || arg === '-s') {
size = Number(process.argv[i + 1])
i += 1
if (Number.isNaN(size) || size <= 0) {
error('Size must be positive integer')
}
} else if (arg === '--alphabet' || arg === '-a') {
alphabet = process.argv[i + 1]
i += 1
} else {
error('Unknown argument ' + arg)
}
}

if (alphabet) {
let customNanoid = customAlphabet(alphabet, size)
print(customNanoid())
} else {
print(nanoid(size))
}
11 changes: 0 additions & 11 deletions bin/nanoid.test.js

This file was deleted.

37 changes: 5 additions & 32 deletions index.browser.js
Original file line number Diff line number Diff line change
@@ -3,37 +3,9 @@

let { urlAlphabet } = require('./url-alphabet')

if (process.env.NODE_ENV !== 'production') {
// All bundlers will remove this block in the production bundle.
if (
typeof navigator !== 'undefined' &&
navigator.product === 'ReactNative' &&
typeof crypto === 'undefined'
) {
throw new Error(
'React Native does not have a built-in secure random generator. ' +
'If you don’t need unpredictable IDs use `nanoid/non-secure`. ' +
'For secure IDs, import `react-native-get-random-values` ' +
'before Nano ID.'
)
}
if (typeof msCrypto !== 'undefined' && typeof crypto === 'undefined') {
throw new Error(
'Import file with `if (!window.crypto) window.crypto = window.msCrypto`' +
' before importing Nano ID to fix IE 11 support'
)
}
if (typeof crypto === 'undefined') {
throw new Error(
'Your browser does not have secure random generator. ' +
'If you don’t need unpredictable IDs, you can use nanoid/non-secure.'
)
}
}

let random = bytes => crypto.getRandomValues(new Uint8Array(bytes))

let customRandom = (alphabet, size, getRandom) => {
let customRandom = (alphabet, defaultSize, getRandom) => {
// First, a bitmask is necessary to generate the ID. The bitmask makes bytes
// values closer to the alphabet size. The bitmask calculates the closest
// `2^31 - 1` number, which exceeds the alphabet size.
@@ -55,9 +27,9 @@ let customRandom = (alphabet, size, getRandom) => {

// `-~f => Math.ceil(f)` if f is a float
// `-~i => i + 1` if i is an integer
let step = -~((1.6 * mask * size) / alphabet.length)
let step = -~((1.6 * mask * defaultSize) / alphabet.length)

return () => {
return (size = defaultSize) => {
let id = ''
while (true) {
let bytes = getRandom(step)
@@ -72,7 +44,8 @@ let customRandom = (alphabet, size, getRandom) => {
}
}

let customAlphabet = (alphabet, size) => customRandom(alphabet, size, random)
let customAlphabet = (alphabet, size = 21) =>
customRandom(alphabet, size, random)

let nanoid = (size = 21) => {
let id = ''
7 changes: 5 additions & 2 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -21,7 +21,7 @@ export function nanoid(size?: number): string
* will not be secure.
*
* @param alphabet Alphabet used to generate the ID.
* @param size Size of the ID.
* @param defaultSize Size of the ID. The default size is 21.
* @returns A random string generator.
*
* ```js
@@ -30,7 +30,10 @@ export function nanoid(size?: number): string
* nanoid() //=> "8ё56а"
* ```
*/
export function customAlphabet(alphabet: string, size: number): () => string
export function customAlphabet(
alphabet: string,
defaultSize?: number
): (size?: number) => string

/**
* Generate unique ID with custom random generator and alphabet.
31 changes: 18 additions & 13 deletions index.js
Original file line number Diff line number Diff line change
@@ -7,10 +7,10 @@ let { urlAlphabet } = require('./url-alphabet')
// pool. The pool is a Buffer that is larger than the initial random
// request size by this multiplier. The pool is enlarged if subsequent
// requests exceed the maximum buffer size.
const POOL_SIZE_MULTIPLIER = 32
const POOL_SIZE_MULTIPLIER = 128
let pool, poolOffset

let random = bytes => {
let fillPool = bytes => {
if (!pool || pool.length < bytes) {
pool = Buffer.allocUnsafe(bytes * POOL_SIZE_MULTIPLIER)
crypto.randomFillSync(pool)
@@ -19,13 +19,16 @@ let random = bytes => {
crypto.randomFillSync(pool)
poolOffset = 0
}

let res = pool.subarray(poolOffset, poolOffset + bytes)
poolOffset += bytes
return res
}

let customRandom = (alphabet, size, getRandom) => {
let random = bytes => {
// `-=` convert `bytes` to number to prevent `valueOf` abusing
fillPool((bytes -= 0))
return pool.subarray(poolOffset - bytes, poolOffset)
}

let customRandom = (alphabet, defaultSize, getRandom) => {
// First, a bitmask is necessary to generate the ID. The bitmask makes bytes
// values closer to the alphabet size. The bitmask calculates the closest
// `2^31 - 1` number, which exceeds the alphabet size.
@@ -43,9 +46,9 @@ let customRandom = (alphabet, size, getRandom) => {
// The number of random bytes gets decided upon the ID size, mask,
// alphabet size, and magic number 1.6 (using 1.6 peaks at performance
// according to benchmarks).
let step = Math.ceil((1.6 * mask * size) / alphabet.length)
let step = Math.ceil((1.6 * mask * defaultSize) / alphabet.length)

return () => {
return (size = defaultSize) => {
let id = ''
while (true) {
let bytes = getRandom(step)
@@ -60,19 +63,21 @@ let customRandom = (alphabet, size, getRandom) => {
}
}

let customAlphabet = (alphabet, size) => customRandom(alphabet, size, random)
let customAlphabet = (alphabet, size = 21) =>
customRandom(alphabet, size, random)

let nanoid = (size = 21) => {
let bytes = random(size)
// `-=` convert `size` to number to prevent `valueOf` abusing
fillPool((size -= 0))
let id = ''
// A compact alternative for `for (let i = 0; i < size; i++)`.
while (size--) {
// We are reading directly from the random pool to avoid creating new array
for (let i = poolOffset - size; i < poolOffset; i++) {
// It is incorrect to use bytes exceeding the alphabet size.
// The following mask reduces the random byte in the 0-255 value
// range to the 0-63 value range. Therefore, adding hacks, such
// as empty string fallback or magic numbers, is unneccessary because
// the bitmask trims bytes down to the alphabet size.
id += urlAlphabet[bytes[size] & 63]
id += urlAlphabet[pool[i] & 63]
}
return id
}
11 changes: 7 additions & 4 deletions non-secure/index.d.ts
Original file line number Diff line number Diff line change
@@ -13,18 +13,21 @@
export function nanoid(size?: number): string

/**
* Generate URL-friendly unique ID based on the custom alphabet.
* Generate a unique ID based on a custom alphabet.
* This method uses the non-secure predictable random generator
* with bigger collision probability.
*
* @param alphabet Alphabet used to generate the ID.
* @param size Size of the ID.
* @returns A random string.
* @param defaultSize Size of the ID. The default size is 21.
* @returns A random string generator.
*
* ```js
* import { customAlphabet } from 'nanoid/non-secure'
* const nanoid = customAlphabet('0123456789абвгдеё', 5)
* model.id = //=> "8ё56а"
* ```
*/
export function customAlphabet(alphabet: string, size: number): () => string
export function customAlphabet(
alphabet: string,
defaultSize?: number
): (size?: number) => string
14 changes: 9 additions & 5 deletions non-secure/index.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
// This alphabet uses `A-Za-z0-9_-` symbols. The genetic algorithm helped
// optimize the gzip compression for this alphabet.
// This alphabet uses `A-Za-z0-9_-` symbols.
// The order of characters is optimized for better gzip and brotli compression.
// References to the same file (works both for gzip and brotli):
// `'use`, `andom`, and `rict'`
// References to the brotli default dictionary:
// `-26T`, `1983`, `40px`, `75px`, `bush`, `jack`, `mind`, `very`, and `wolf`
let urlAlphabet =
'ModuleSymbhasOwnPr-0123456789ABCDEFGHNRVfgctiUvz_KqYTJkLxpZXIjQW'
'useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict'

let customAlphabet = (alphabet, size) => {
return () => {
let customAlphabet = (alphabet, defaultSize = 21) => {
return (size = defaultSize) => {
let id = ''
// A compact alternative for `for (var i = 0; i < step; i++)`.
let i = size
181 changes: 65 additions & 116 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
{
"name": "nanoid",
"version": "3.1.25",
"description": "A tiny (108 bytes), secure URL-friendly unique string ID generator",
"version": "3.3.1",
"description": "A tiny (130 bytes), secure URL-friendly unique string ID generator",
"keywords": [
"uuid",
"random",
"id",
"url"
],
"scripts": {
"test": "jest --coverage && eslint . && check-dts && size-limit && yaspeller *.md",
"start": "parcel test/demo/index.html --dist-dir test/demo/build --open"
"clean": "rm -R coverage",
"unit": "uvu . .test.js$",
"test": "c8 pnpm unit && eslint . && pnpm clean && size-limit",
"start": "vite test/demo/ --open"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
@@ -30,94 +32,101 @@
"sideEffects": false,
"types": "./index.d.ts",
"devDependencies": {
"@babel/core": "^7.15.0",
"@logux/eslint-config": "^45.4.7",
"@logux/sharec-config": "^0.11.0",
"@parcel/optimizer-cssnano": "^2.0.0-rc.0",
"@parcel/optimizer-htmlnano": "^2.0.0-rc.0",
"@parcel/packager-css": "^2.0.0-rc.0",
"@parcel/packager-html": "^2.0.0-rc.0",
"@parcel/transformer-css": "^2.0.0-rc.0",
"@parcel/transformer-html": "^2.0.0-rc.0",
"@parcel/transformer-postcss": "^2.0.0-rc.0",
"@parcel/transformer-posthtml": "^2.0.0-rc.0",
"@size-limit/dual-publish": "^5.0.2",
"@size-limit/preset-small-lib": "^5.0.2",
"@typescript-eslint/eslint-plugin": "^4.29.1",
"@typescript-eslint/parser": "^4.29.1",
"@babel/core": "^7.17.4",
"@logux/eslint-config": "^46.1.1",
"@lukeed/uuid": "^2.0.0",
"@originjs/vite-plugin-commonjs": "^1.0.3",
"@size-limit/dual-publish": "^7.0.8",
"@size-limit/file": "^7.0.8",
"@size-limit/webpack": "^7.0.8",
"@types/node": "^17.0.18",
"benchmark": "^2.1.4",
"check-dts": "^0.5.5",
"colorette": "^1.3.0",
"c8": "^7.11.0",
"cuid": "^2.1.8",
"dual-publish": "^1.0.8",
"eslint": "^7.32.0",
"dual-publish": "^3.0.0",
"eslint": "^8.9.0",
"eslint-config-standard": "^16.0.3",
"eslint-plugin-import": "^2.24.0",
"eslint-plugin-jest": "^24.4.0",
"eslint-plugin-import": "^2.25.4",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prefer-let": "^1.1.0",
"eslint-plugin-promise": "^5.1.0",
"eslint-plugin-security": "^1.4.0",
"eslint-plugin-unicorn": "^35.0.0",
"jest": "^27.0.6",
"lint-staged": "^11.1.2",
"parcel": "^2.0.0-rc.0",
"postcss": "^8.3.6",
"prettier": "^2.3.2",
"eslint-plugin-prefer-let": "^3.0.1",
"eslint-plugin-promise": "^6.0.0",
"nanospy": "^0.5.0",
"picocolors": "^1.0.0",
"postcss": "^8.4.6",
"rndm": "^1.2.0",
"secure-random-string": "^1.1.3",
"shortid": "^2.2.16",
"simple-git-hooks": "^2.5.1",
"size-limit": "^5.0.2",
"terser": "^5.7.1",
"typescript": "^4.3.5",
"size-limit": "^7.0.8",
"terser": "^5.10.0",
"uid": "^2.0.0",
"uid-safe": "^2.1.5",
"uuid": "^8.3.2",
"yaspeller": "^7.0.0"
"uvu": "^0.5.3",
"vite": "^2.8.3"
},
"size-limit": [
{
"name": "nanoid",
"import": "{ nanoid }",
"limit": "108 B"
"limit": "130 B"
},
{
"name": "customAlphabet",
"import": "{ customAlphabet }",
"limit": "141 B"
"limit": "174 B"
},
{
"name": "urlAlphabet",
"import": "{ urlAlphabet }",
"limit": "59 B"
"limit": "61 B"
},
{
"name": "non-secure nanoid",
"import": "{ nanoid }",
"path": "non-secure/index.js",
"limit": "103 B"
"limit": "118 B"
},
{
"name": "non-secure customAlphabet",
"import": "{ customAlphabet }",
"path": "non-secure/index.js",
"limit": "45 B"
"limit": "69 B"
},
{
"name": "async nanoid",
"import": "{ nanoid }",
"path": "async/index.js",
"limit": "118 B"
"limit": "136 B"
},
{
"name": "async customAlphabet",
"import": "{ customAlphabet }",
"path": "async/index.js",
"limit": "138 B"
"limit": "157 B"
},
{
"name": "Brotli all",
"brotli": true,
"import": "{ nanoid, customAlphabet, urlAlphabet }",
"limit": "271 B"
},
{
"name": "Brotli non-secure",
"brotli": true,
"import": "{ nanoid, customAlphabet }",
"path": "non-secure/index.js",
"limit": "116 B"
},
{
"name": "Brotli async",
"brotli": true,
"import": "{ nanoid, customAlphabet }",
"path": "async/index.js",
"limit": "211 B"
}
],
"eslintConfig": {
"extends": "@logux/eslint-config/ts",
"extends": "@logux/eslint-config",
"rules": {
"consistent-return": "off",
"func-style": "off",
@@ -138,21 +147,6 @@
"nanoid.js",
"**/errors.ts"
],
"lint-staged": {
"index.browser.js": "test/update-prebuild.js",
"*.md": "yaspeller",
"*.js": [
"prettier --write",
"eslint --fix"
],
"*.ts": [
"prettier --write",
"eslint --fix"
]
},
"simple-git-hooks": {
"pre-commit": "npx lint-staged"
},
"prettier": {
"arrowParens": "avoid",
"jsxSingleQuote": false,
@@ -161,61 +155,16 @@
"singleQuote": true,
"trailingComma": "none"
},
"jest": {
"testEnvironment": "node",
"coverageThreshold": {
"global": {
"statements": 100
}
}
"clean-publish": {
"cleanDocs": true,
"cleanComments": true
},
"yaspeller": {
"lang": "en",
"ignoreCapitalization": true,
"ignoreText": [
" \\(by [^)]+\\)."
"c8": {
"exclude": [
"**/*.test.*"
],
"dictionary": [
"Async",
"CLI",
"Clojure",
"ClojureScript",
"Crypto",
"cryptographically",
"gzipped",
"Haskell",
"js",
"JS",
"JSDoc",
"nanoid",
"Nim",
"npm",
"UUID",
"v4",
"Versioning",
"PouchDB",
"CouchDB",
"Tidelift",
"as ESM in",
"ES",
"webpack",
"CDN",
"XPS",
"jsDelivr",
"bundlers",
"ES2016",
"github",
"ai",
"Rollup",
"transpile",
"workaround",
"Deno",
"polyfill",
"CommonJS"
]
},
"sharec": {
"config": "@logux/sharec-config",
"version": "0.11.0"
"lines": 100,
"reporter": "lcov",
"check-coverage": true
}
}
2,785 changes: 2,785 additions & 0 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

293 changes: 153 additions & 140 deletions test/async.test.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
let { suite } = require('uvu')
let { spy } = require('nanospy')
let { is, match, ok } = require('uvu/assert')
let crypto = require('crypto')

global.crypto = {
@@ -22,167 +25,177 @@ function times(size, callback) {
}

for (let type of ['node', 'browser']) {
describe(`${type}`, () => {
let { nanoid, customAlphabet, random } = type === 'node' ? node : browser
let { nanoid, customAlphabet, random } = type === 'node' ? node : browser

function mock(callback) {
crypto.randomFill = callback
jest.resetModules()
nanoid = require('../async').nanoid
}
function mock(callback) {
crypto.randomFill = callback
delete require.cache[require.resolve('../async')]
nanoid = require('../async').nanoid
}

describe('nanoid', () => {
if (type === 'node') {
let originFill = crypto.randomFill
afterEach(() => {
mock(originFill)
})
}
let nanoidSuite = suite(`${type} / nanoid`)

it('generates URL-friendly IDs', async () => {
await Promise.all(
times(100, async () => {
let id = await nanoid()
expect(id).toHaveLength(21)
expect(typeof id).toEqual('string')
for (let char of id) {
expect(urlAlphabet).toContain(char)
}
})
)
})
if (type === 'node') {
let originFill = crypto.randomFill
nanoidSuite.after.each(() => {
mock(originFill)
})
}

it('changes ID length', async () => {
let id = await nanoid(10)
expect(id).toHaveLength(10)
nanoidSuite('generates URL-friendly IDs', async () => {
await Promise.all(
times(100, async () => {
let id = await nanoid()
is(id.length, 21)
is(typeof id, 'string')
for (let char of id) {
match(urlAlphabet, new RegExp(char, "g"))
}
})
)
})

it('has no collisions', async () => {
let ids = await Promise.all(times(50 * 1000, () => nanoid()))
ids.reduce((used, id) => {
expect(used[id]).toBeUndefined()
used[id] = true
return used
}, [])
})
nanoidSuite('changes ID length', async () => {
let id = await nanoid(10)
is(id.length, 10)
})

it('has flat distribution', async () => {
let COUNT = 100 * 1000
let LENGTH = (await nanoid()).length

let chars = {}
await Promise.all(
times(COUNT, async () => {
let id = await nanoid()
for (let char of id) {
if (!chars[char]) chars[char] = 0
chars[char] += 1
}
})
)
expect(Object.keys(chars)).toHaveLength(urlAlphabet.length)
let max = 0
let min = Number.MAX_SAFE_INTEGER
for (let k in chars) {
let distribution = (chars[k] * urlAlphabet.length) / (COUNT * LENGTH)
if (distribution > max) max = distribution
if (distribution < min) min = distribution
nanoidSuite('has no collisions', async () => {
let ids = await Promise.all(times(50 * 1000, () => nanoid()))
ids.reduce((used, id) => {
is(used[id], undefined)
used[id] = true
return used
}, [])
})

nanoidSuite('has flat distribution', async () => {
let COUNT = 100 * 1000
let LENGTH = (await nanoid()).length

let chars = {}
await Promise.all(
times(COUNT, async () => {
let id = await nanoid()
for (let char of id) {
if (!chars[char]) chars[char] = 0
chars[char] += 1
}
expect(max - min).toBeLessThanOrEqual(0.05)
})
)
is(Object.keys(chars).length, urlAlphabet.length)
let max = 0
let min = Number.MAX_SAFE_INTEGER
for (let k in chars) {
let distribution = (chars[k] * urlAlphabet.length) / (COUNT * LENGTH)
if (distribution > max) max = distribution
if (distribution < min) min = distribution
}
ok(max - min <= 0.05)
})

if (type === 'node') {
it('rejects Promise on error', async () => {
let error = new Error('test')
mock((buffer, callback) => {
callback(error)
})
let catched
try {
await nanoid()
} catch (e) {
catched = e
}
expect(catched).toBe(error)
})
if (type === 'node') {
nanoidSuite('rejects Promise on error', async () => {
let error = new Error('test')
mock((buffer, callback) => {
callback(error)
})
let catched
try {
await nanoid()
} catch (e) {
catched = e
}
is(catched, error)
})
}

describe('random', () => {
it('generates small random buffers', async () => {
expect(await random(10)).toHaveLength(10)
})
nanoidSuite.run()

it('generates random buffers', async () => {
let numbers = {}
let bytes = await random(10000)
expect(bytes).toHaveLength(10000)
for (let byte of bytes) {
if (!numbers[byte]) numbers[byte] = 0
numbers[byte] += 1
expect(typeof byte).toEqual('number')
expect(byte).toBeLessThanOrEqual(255)
expect(byte).toBeGreaterThanOrEqual(0)
}
})
})
let randomSuite = suite(`${type} / random`)

describe('customAlphabet', () => {
if (type === 'node') {
let originFill = crypto.randomFill
afterEach(() => {
mock(originFill)
})
}
randomSuite('generates small random buffers', async () => {
is((await random(10)).length, 10)
})

it('has options', async () => {
let nanoidA = customAlphabet('a', 5)
let id = await nanoidA()
expect(id).toEqual('aaaaa')
})
randomSuite('generates random buffers', async () => {
let numbers = {}
let bytes = await random(10000)
is(bytes.length, 10000)
for (let byte of bytes) {
if (!numbers[byte]) numbers[byte] = 0
numbers[byte] += 1
is(typeof byte, 'number')
ok(byte <= 255)
ok(byte >= 0)
}
})

randomSuite.run()

let customAlphabetSuite = suite(`${type} / customAlphabet`)

if (type === 'node') {
let originFill = crypto.randomFill
customAlphabetSuite.after.each(() => {
mock(originFill)
})
}

it('has flat distribution', async () => {
let COUNT = 50 * 1000
let LENGTH = 30
let ALPHABET = 'abcdefghijklmnopqrstuvwxy'
let nanoid2 = customAlphabet(ALPHABET, LENGTH)

let chars = {}
await Promise.all(
times(100, async () => {
let id = await nanoid2()
expect(id).toHaveLength(LENGTH)
for (let char of id) {
if (!chars[char]) chars[char] = 0
chars[char] += 1
}
})
)
expect(Object.keys(chars)).toHaveLength(ALPHABET.length)
let max = 0
let min = Number.MAX_SAFE_INTEGER
for (let k in chars) {
let distribution = (chars[k] * ALPHABET.length) / (COUNT * LENGTH)
if (distribution > max) max = distribution
if (distribution < min) min = distribution
customAlphabetSuite('has options', async () => {
let nanoidA = customAlphabet('a', 5)
let id = await nanoidA()
is(id, 'aaaaa')
})

customAlphabetSuite('has flat distribution', async () => {
let COUNT = 50 * 1000
let LENGTH = 30
let ALPHABET = 'abcdefghijklmnopqrstuvwxy'
let nanoid2 = customAlphabet(ALPHABET, LENGTH)

let chars = {}
await Promise.all(
times(100, async () => {
let id = await nanoid2()
is(id.length, LENGTH)
for (let char of id) {
if (!chars[char]) chars[char] = 0
chars[char] += 1
}
expect(max - min).toBeLessThanOrEqual(0.05)
})
)
is(Object.keys(chars).length, ALPHABET.length)
let max = 0
let min = Number.MAX_SAFE_INTEGER
for (let k in chars) {
let distribution = (chars[k] * ALPHABET.length) / (COUNT * LENGTH)
if (distribution > max) max = distribution
if (distribution < min) min = distribution
}
ok(max - min <= 0.05)
})

customAlphabetSuite('changes size', async () => {
let nanoidA = customAlphabet('a')
let id = await nanoidA(10)
is(id, 'aaaaaaaaaa')
})

if (type === 'node') {
it('should call random two times', async () => {
let randomFillMock = jest.fn((buffer, callback) =>
callback(null, [220, 215, 129, 35, 242, 202, 137, 180])
)
mock(randomFillMock)
if (type === 'node') {
customAlphabetSuite('should call random two times', async () => {
let randomFillMock = spy((buffer, callback) =>
callback(null, [220, 215, 129, 35, 242, 202, 137, 180])
)
mock(randomFillMock)

let nanoidA = customAlphabet('a', 5)
let id = await nanoidA()
let nanoidA = customAlphabet('a', 5)
let id = await nanoidA()

expect(randomFillMock).toHaveBeenCalledTimes(2)
expect(id).toEqual('aaaaa')
})
}
is(randomFillMock.callCount, 2)
is(id, 'aaaaa')
})
})
}

customAlphabetSuite.run()
}
44 changes: 30 additions & 14 deletions test/benchmark.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
#!/usr/bin/env node

let { uid: uidSecure } = require('uid/secure')
let { v4: lukeed4 } = require('@lukeed/uuid')
let { v4: uuid4 } = require('uuid')
let benchmark = require('benchmark')
let { bold } = require('colorette')
let shortid = require('shortid')
let uidSafe = require('uid-safe')
let { uid } = require('uid')
let crypto = require('crypto')
let pico = require('picocolors')
let cuid = require('cuid')
let rndm = require('rndm')
let srs = require('secure-random-string')
let uid = require('uid-safe')

let { nanoid: aNanoid, customAlphabet: aCustomAlphabet } = require('../async')
let { nanoid, customAlphabet } = require('../')
@@ -21,10 +25,19 @@ let asyncNanoid2 = aCustomAlphabet('1234567890abcdef-', 10)
function formatNumber(number) {
return String(number)
.replace(/\d{3}$/, ',$&')
.replace(/^(\d)(\d{3})/, '$1,$2')
.replace(/^(\d|\d\d)(\d{3},)/, '$1,$2')
}

suite
.add('crypto.randomUUID', () => {
crypto.randomUUID()
})
.add('uid/secure', () => {
uidSecure(32)
})
.add('@lukeed/uuid', () => {
lukeed4()
})
.add('nanoid', () => {
nanoid()
})
@@ -34,19 +47,19 @@ suite
.add('uuid v4', () => {
uuid4()
})
.add('uid.sync', () => {
uid.sync(14)
})
.add('secure-random-string', () => {
srs()
})
.add('uid-safe.sync', () => {
uidSafe.sync(14)
})
.add('cuid', () => {
cuid()
})
.add('shortid', () => {
shortid()
})
.add('async nanoid', {
.add('nanoid/async', {
defer: true,
fn(defer) {
aNanoid().then(() => {
@@ -70,28 +83,31 @@ suite
})
}
})
.add('uid', {
.add('uid-safe', {
defer: true,
fn(defer) {
uid(14).then(() => {
uidSafe(14).then(() => {
defer.resolve()
})
}
})
.add('non-secure nanoid', () => {
.add('uid', () => {
uid(32)
})
.add('nanoid/non-secure', () => {
nonSecure()
})
.add('rndm', () => {
rndm(21)
})
.on('cycle', event => {
let name = event.target.name.padEnd('async secure-random-string'.length)
let hz = formatNumber(event.target.hz.toFixed(0)).padStart(9)
if (event.target.name === 'async nanoid') {
let hz = formatNumber(event.target.hz.toFixed(0)).padStart(10)
if (event.target.name === 'nanoid/async') {
name = '\nAsync:\n' + name
} else if (event.target.name === 'non-secure nanoid') {
} else if (event.target.name === 'uid') {
name = '\nNon-secure:\n' + name
}
process.stdout.write(`${name}${bold(hz)} ops/sec\n`)
process.stdout.write(`${name}${pico.bold(hz)}${pico.dim(' ops/sec')}\n`)
})
.run()
62 changes: 62 additions & 0 deletions test/bin.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
let { is, match } = require('uvu/assert')
let { promisify } = require('util')
let { test } = require('uvu')
let { join } = require('path')
let child = require('child_process')

let exec = promisify(child.exec)

const BIN = join(__dirname, '..', 'bin', 'nanoid.cjs')

test('prints unique ID', async () => {
let { stdout, stderr } = await exec('node ' + BIN)
is(stderr, '')
match(stdout, /^[\w-]{21}\n$/)
})

test('uses size', async () => {
let { stdout, stderr } = await exec('node ' + BIN + ' --size 10')
is(stderr, '')
match(stdout, /^[\w-]{10}\n$/)
})

test('uses alphabet', async () => {
let { stdout, stderr } = await exec(
'node ' + BIN + ' --alphabet abc --size 15'
)
is(stderr, '')
match(stdout, /^[abc]{15}\n$/)
})

test('shows an error on unknown argument', async () => {
try {
await exec('node ' + BIN + ' -test')
} catch (e) {
match(e, /Unknown argument -test/)
}
})

test('shows an error if size is not a number', async () => {
try {
await exec('node ' + BIN + ' -s abc')
} catch (e) {
match(e, /Size must be positive integer/)
}
})

test('requires error if size is a negative number', async () => {
try {
await exec('node ' + BIN + ' --size "-1"')
} catch (e) {
match(e, /Size must be positive integer/)
}
})

test('displays help', async () => {
let { stdout, stderr } = await exec('node ' + BIN + ' --help')
is(stderr, '')
match(stdout, /Usage/)
match(stdout, /\$ nanoid \[options]/)
})

test.run()
1 change: 0 additions & 1 deletion test/demo/browserslist

This file was deleted.

2 changes: 1 addition & 1 deletion test/demo/index.html
Original file line number Diff line number Diff line change
@@ -42,6 +42,6 @@
</style>
</head>
<body>
<script src="./index.js"></script>
<script src="./index.js" type="module"></script>
</body>
</html>
11 changes: 5 additions & 6 deletions test/demo/index.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { v4 as uuid4 } from 'uuid'
import shortid from 'shortid'
import rndm from 'rndm'
import uid from 'uid-safe'

import { nanoid, customAlphabet, random } from '../../'
import { nanoid as nonSecure } from '../../non-secure'
import nanoidExport from '../../index.browser.js'
import nonSecureExport from '../../non-secure/index.js'

let { nanoid, customAlphabet, random } = nanoidExport
let nonSecure = nonSecureExport.nanoid

const COUNT = 50 * 1000
const ALPHABET = 'abcdefghijklmnopqrstuvwxyz'
@@ -74,10 +75,8 @@ let tasks = [
}),
() => printDistr('nanoid', () => nanoid()),
() => printDistr('nanoid2', () => nanoid2()),
() => printDistr('uid.sync', () => uid.sync(21)),
() => printDistr('uuid/v4', () => uuid4()),
() => printDistr('shortid', () => shortid()),
() => printDistr('rndm', () => rndm()),
() => printDistr('nanoid/non-secure', () => nonSecure()),
() =>
printDistr('random % alphabet', () => {
5 changes: 5 additions & 0 deletions test/demo/vite.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { viteCommonjs } from '@originjs/vite-plugin-commonjs'

export default {
plugins: [viteCommonjs()]
}
7 changes: 0 additions & 7 deletions test/ie.test.js

This file was deleted.

316 changes: 178 additions & 138 deletions test/index.test.js
Original file line number Diff line number Diff line change
@@ -1,155 +1,195 @@
global.crypto = {
getRandomValues(array) {
for (let i = 0; i < array.length; i++) {
array[i] = Math.floor(Math.random() * 256)
}
return array
}
}
let { test } = require('uvu')
let { is, ok, equal, match } = require('uvu/assert')

let browser = require('../index.browser.js')
let node = require('../index.js')

test.before(() => {
global.crypto = {
getRandomValues(array) {
for (let i = 0; i < array.length; i++) {
array[i] = Math.floor(Math.random() * 256)
}
return array
}
}
})

test.after(() => {
delete global.crypto
})

for (let type of ['node', 'browser']) {
describe(`${type}`, () => {
let { nanoid, customAlphabet, customRandom, random, urlAlphabet } =
type === 'node' ? node : browser

describe('nanoid', () => {
it('generates URL-friendly IDs', () => {
for (let i = 0; i < 100; i++) {
let id = nanoid()
expect(id).toHaveLength(21)
expect(typeof id).toEqual('string')
for (let char of id) {
expect(urlAlphabet).toContain(char)
}
}
})

it('changes ID length', () => {
expect(nanoid(10)).toHaveLength(10)
})

it('accepts string', () => {
expect(nanoid('10')).toHaveLength(10)
})

it('has no collisions', () => {
let used = {}
for (let i = 0; i < 50 * 1000; i++) {
let id = nanoid()
expect(used[id]).toBeUndefined()
used[id] = true
}
})

it('has flat distribution', () => {
let COUNT = 100 * 1000
let LENGTH = nanoid().length

let chars = {}
for (let i = 0; i < COUNT; i++) {
let id = nanoid()
for (let char of id) {
if (!chars[char]) chars[char] = 0
chars[char] += 1
}
}
let { nanoid, customAlphabet, customRandom, random, urlAlphabet } =
type === 'node' ? node : browser

test(`${type} / nanoid / generates URL-friendly IDs`, () => {
for (let i = 0; i < 100; i++) {
let id = nanoid()
is(id.length, 21)
is(typeof id, 'string')
for (let char of id) {
match(urlAlphabet, new RegExp(char, "g"))
}
}
})

expect(Object.keys(chars)).toHaveLength(urlAlphabet.length)
test(`${type} / nanoid / changes ID length`, () => {
is(nanoid(10).length, 10)
})

let max = 0
let min = Number.MAX_SAFE_INTEGER
for (let k in chars) {
let distribution = (chars[k] * urlAlphabet.length) / (COUNT * LENGTH)
if (distribution > max) max = distribution
if (distribution < min) min = distribution
}
expect(max - min).toBeLessThanOrEqual(0.05)
})
})
test(`${type} / nanoid / accepts string`, () => {
is(nanoid('10').length, 10)
})

describe('customAlphabet', () => {
it('has options', () => {
let nanoidA = customAlphabet('a', 5)
expect(nanoidA()).toEqual('aaaaa')
})

it('has flat distribution', () => {
let COUNT = 50 * 1000
let LENGTH = 30
let ALPHABET = 'abcdefghijklmnopqrstuvwxyz'
let nanoid2 = customAlphabet(ALPHABET, LENGTH)

let chars = {}
for (let i = 0; i < COUNT; i++) {
let id = nanoid2()
for (let char of id) {
if (!chars[char]) chars[char] = 0
chars[char] += 1
}
}
test(`${type} / nanoid / has no collisions`, () => {
let used = {}
for (let i = 0; i < 50 * 1000; i++) {
let id = nanoid()
is(used[id], undefined)
used[id] = true
}
})

expect(Object.keys(chars)).toHaveLength(ALPHABET.length)
test(`${type} / nanoid / has flat distribution`, () => {
let COUNT = 100 * 1000
let LENGTH = nanoid().length

let chars = {}
for (let i = 0; i < COUNT; i++) {
let id = nanoid()
for (let char of id) {
if (!chars[char]) chars[char] = 0
chars[char] += 1
}
}

let max = 0
let min = Number.MAX_SAFE_INTEGER
for (let k in chars) {
let distribution = (chars[k] * ALPHABET.length) / (COUNT * LENGTH)
if (distribution > max) max = distribution
if (distribution < min) min = distribution
}
expect(max - min).toBeLessThanOrEqual(0.05)
})
})
is(Object.keys(chars).length, urlAlphabet.length)

describe('customRandom', () => {
it('supports generator', () => {
let sequence = [2, 255, 3, 7, 7, 7, 7, 7, 0, 1]
function fakeRandom(size) {
let bytes = []
for (let i = 0; i < size; i += sequence.length) {
bytes = bytes.concat(sequence.slice(0, size - i))
}
return bytes
}
let nanoid4 = customRandom('abcde', 4, fakeRandom)
let nanoid18 = customRandom('abcde', 18, fakeRandom)
expect(nanoid4()).toEqual('adca')
expect(nanoid18()).toEqual('cbadcbadcbadcbadcc')
})
})
let max = 0
let min = Number.MAX_SAFE_INTEGER
for (let k in chars) {
let distribution = (chars[k] * urlAlphabet.length) / (COUNT * LENGTH)
if (distribution > max) max = distribution
if (distribution < min) min = distribution
}
ok(max - min <= 0.05)
})

describe('urlAlphabet', () => {
it('is string', () => {
expect(typeof urlAlphabet).toEqual('string')
})
test(`${type} / customAlphabet / has options`, () => {
let nanoidA = customAlphabet('a', 5)
is(nanoidA(), 'aaaaa')
})

it('has no duplicates', () => {
for (let i = 0; i < urlAlphabet.length; i++) {
expect(urlAlphabet.lastIndexOf(urlAlphabet[i])).toEqual(i)
}
})
})
test(`${type} / customAlphabet / has flat distribution`, () => {
let COUNT = 50 * 1000
let LENGTH = 30
let ALPHABET = 'abcdefghijklmnopqrstuvwxyz'
let nanoid2 = customAlphabet(ALPHABET, LENGTH)

let chars = {}
for (let i = 0; i < COUNT; i++) {
let id = nanoid2()
for (let char of id) {
if (!chars[char]) chars[char] = 0
chars[char] += 1
}
}

is(Object.keys(chars).length, ALPHABET.length)

let max = 0
let min = Number.MAX_SAFE_INTEGER
for (let k in chars) {
let distribution = (chars[k] * ALPHABET.length) / (COUNT * LENGTH)
if (distribution > max) max = distribution
if (distribution < min) min = distribution
}
ok(max - min <= 0.05)
})

test(`${type} / customAlphabet / changes size`, () => {
let nanoidA = customAlphabet('a')
is(nanoidA(10), 'aaaaaaaaaa')
})

test(`${type} / customRandom / supports generator`, () => {
let sequence = [2, 255, 3, 7, 7, 7, 7, 7, 0, 1]
function fakeRandom(size) {
let bytes = []
for (let i = 0; i < size; i += sequence.length) {
bytes = bytes.concat(sequence.slice(0, size - i))
}
return bytes
}
let nanoid4 = customRandom('abcde', 4, fakeRandom)
let nanoid18 = customRandom('abcde', 18, fakeRandom)
is(nanoid4(), 'adca')
is(nanoid18(), 'cbadcbadcbadcbadcc')
})

test(`${type} / urlAlphabet / is string`, () => {
is(typeof urlAlphabet, 'string')
})

describe('random', () => {
it('generates small random buffers', () => {
expect(random(10)).toHaveLength(10)
})

it('generates random buffers', () => {
let numbers = {}
let bytes = random(10000)
expect(bytes).toHaveLength(10000)
for (let byte of bytes) {
if (!numbers[byte]) numbers[byte] = 0
numbers[byte] += 1
expect(typeof byte).toEqual('number')
expect(byte).toBeLessThanOrEqual(255)
expect(byte).toBeGreaterThanOrEqual(0)
test(`${type} / urlAlphabet / has no duplicates`, () => {
for (let i = 0; i < urlAlphabet.length; i++) {
equal(urlAlphabet.lastIndexOf(urlAlphabet[i]), i)
}
})

test(`${type} / random / generates small random buffers`, () => {
for (let i = 0; i < urlAlphabet.length; i++) {
is(random(10).length, 10)
}
})

test(`${type} / random / generates random buffers`, () => {
let numbers = {}
let bytes = random(10000)
is(bytes.length, 10000)
for (let byte of bytes) {
if (!numbers[byte]) numbers[byte] = 0
numbers[byte] += 1
is(typeof byte, 'number')
ok(byte <= 255)
ok(byte >= 0)
}
})

if (type === 'node') {
test(`${type} / proxy number / prevent collision`, () => {
let makeProxyNumberToReproducePreviousID = () => {
let step = 0
return {
valueOf() {
// "if (!pool || pool.length < bytes) {"
if (step === 0) {
step++
return 0
}
// "} else if (poolOffset + bytes > pool.length) {"
if (step === 1) {
step++
return -Infinity
}
// "poolOffset += bytes"
if (step === 2) {
step++
return 0
}

return 21
}
}
})
}

let ID1 = nanoid()
let ID2 = nanoid(makeProxyNumberToReproducePreviousID())

is.not(ID1, ID2)
})
})
}
}

test.run()
141 changes: 71 additions & 70 deletions test/non-secure.test.js
Original file line number Diff line number Diff line change
@@ -1,90 +1,91 @@
let { test } = require('uvu')
let { is, match, ok } = require('uvu/assert')

let { nanoid, customAlphabet } = require('../non-secure')
let { urlAlphabet } = require('..')

describe('nanoid', () => {
it('generates URL-friendly IDs', () => {
for (let i = 0; i < 10; i++) {
let id = nanoid()
expect(id).toHaveLength(21)
for (let char of id) {
expect(urlAlphabet).toContain(char)
}
test('nanoid / generates URL-friendly IDs', () => {
for (let i = 0; i < 10; i++) {
let id = nanoid()
is(id.length, 21)
for (let char of id) {
match(urlAlphabet, new RegExp(char, "g"))
}
})
}
})

it('changes ID length', () => {
expect(nanoid(10)).toHaveLength(10)
})
test('nanoid / changes ID length', () => {
is(nanoid(10).length, 10)
})

it('accepts string', () => {
expect(nanoid('10')).toHaveLength(10)
})
test('nanoid / accepts string', () => {
is(nanoid('10').length, 10)
})

it('has no collisions', () => {
let used = {}
for (let i = 0; i < 100 * 1000; i++) {
let id = nanoid()
expect(used[id]).toBeUndefined()
used[id] = true
}
})
test('nanoid / has no collisions', () => {
let used = {}
for (let i = 0; i < 100 * 1000; i++) {
let id = nanoid()
is(used[id], undefined)
used[id] = true
}
})

it('has flat distribution', () => {
let COUNT = 100 * 1000
let LENGTH = nanoid().length
test('nanoid / has flat distribution', () => {
let COUNT = 100 * 1000
let LENGTH = nanoid().length

let chars = {}
for (let i = 0; i < COUNT; i++) {
let id = nanoid()
for (let char of id) {
if (!chars[char]) chars[char] = 0
chars[char] += 1
}
let chars = {}
for (let i = 0; i < COUNT; i++) {
let id = nanoid()
for (let char of id) {
if (!chars[char]) chars[char] = 0
chars[char] += 1
}
}

expect(Object.keys(chars)).toHaveLength(urlAlphabet.length)
is(Object.keys(chars).length, urlAlphabet.length)

let max = 0
let min = Number.MAX_SAFE_INTEGER
for (let k in chars) {
let distribution = (chars[k] * urlAlphabet.length) / (COUNT * LENGTH)
if (distribution > max) max = distribution
if (distribution < min) min = distribution
}
expect(max - min).toBeLessThanOrEqual(0.05)
})
let max = 0
let min = Number.MAX_SAFE_INTEGER
for (let k in chars) {
let distribution = (chars[k] * urlAlphabet.length) / (COUNT * LENGTH)
if (distribution > max) max = distribution
if (distribution < min) min = distribution
}
ok(max - min <= 0.05)
})

describe('customAlphabet', () => {
it('has options', () => {
let nanoidA = customAlphabet('a', 5)
expect(nanoidA()).toEqual('aaaaa')
})
test('customAlphabet / has options', () => {
let nanoidA = customAlphabet('a', 5)
is(nanoidA(), 'aaaaa')
})

it('has flat distribution', () => {
let COUNT = 100 * 1000
let LENGTH = 5
let ALPHABET = 'abcdefghijklmnopqrstuvwxyz'
let nanoid2 = customAlphabet(ALPHABET, LENGTH)
test('customAlphabet / has flat distribution', () => {
let COUNT = 100 * 1000
let LENGTH = 5
let ALPHABET = 'abcdefghijklmnopqrstuvwxyz'
let nanoid2 = customAlphabet(ALPHABET, LENGTH)

let chars = {}
for (let i = 0; i < COUNT; i++) {
let id = nanoid2()
for (let char of id) {
if (!chars[char]) chars[char] = 0
chars[char] += 1
}
let chars = {}
for (let i = 0; i < COUNT; i++) {
let id = nanoid2()
for (let char of id) {
if (!chars[char]) chars[char] = 0
chars[char] += 1
}
}

expect(Object.keys(chars)).toHaveLength(ALPHABET.length)
is(Object.keys(chars).length, ALPHABET.length)

let max = 0
let min = Number.MAX_SAFE_INTEGER
for (let k in chars) {
let distribution = (chars[k] * ALPHABET.length) / (COUNT * LENGTH)
if (distribution > max) max = distribution
if (distribution < min) min = distribution
}
expect(max - min).toBeLessThanOrEqual(0.05)
})
let max = 0
let min = Number.MAX_SAFE_INTEGER
for (let k in chars) {
let distribution = (chars[k] * ALPHABET.length) / (COUNT * LENGTH)
if (distribution > max) max = distribution
if (distribution < min) min = distribution
}
ok(max - min <= 0.05)
})

test.run()
5 changes: 0 additions & 5 deletions test/old-browser.test.js

This file was deleted.

37 changes: 25 additions & 12 deletions test/react-native-polyfill.test.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,30 @@
global.navigator = {
product: 'ReactNative'
}
global.crypto = {
getRandomValues(array) {
for (let i = 0; i < array.length; i++) {
array[i] = Math.floor(Math.random() * 256)
let { test } = require('uvu')
let { is } = require('uvu/assert')

test.before(() => {
global.navigator = {
product: 'ReactNative'
}

global.crypto = {
getRandomValues(array) {
for (let i = 0; i < array.length; i++) {
array[i] = Math.floor(Math.random() * 256)
}
return array
}
return array
}
}
})

let { nanoid } = require('../index.browser')
test.after(() => {
delete global.navigator
delete global.crypto
})

test('works with polyfill', () => {
let { nanoid } = require('../index.browser')

it('works with polyfill', () => {
expect(typeof nanoid()).toEqual('string')
is(typeof nanoid(), 'string')
})

test.run()
9 changes: 0 additions & 9 deletions test/react-native.test.js

This file was deleted.

12 changes: 0 additions & 12 deletions tsconfig.json

This file was deleted.

7 changes: 4 additions & 3 deletions url-alphabet/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// This alphabet uses `A-Za-z0-9_-` symbols. The genetic algorithm helped
// optimize the gzip compression for this alphabet.
// This alphabet uses `A-Za-z0-9_-` symbols.
// The order of characters is optimized for better gzip and brotli compression.
// Same as in non-secure/index.js
let urlAlphabet =
'ModuleSymbhasOwnPr-0123456789ABCDEFGHNRVfgctiUvz_KqYTJkLxpZXIjQW'
'useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict'

module.exports = { urlAlphabet }
9,344 changes: 0 additions & 9,344 deletions yarn.lock

This file was deleted.