npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

js-cool

v6.0.0

Published

Collection of common JavaScript / TypeScript utilities

Downloads

2,435

Readme

js-cool

Collection of common JavaScript / TypeScript utilities

NPM version npm download tree shaking gzip

ChangelogMigration Guide v5→v6简体中文


Online Playground

Try js-cool directly in your browser with StackBlitz:

Open in StackBlitz


Installation

# pnpm
pnpm add js-cool

# npm
npm install js-cool

Usage

// ES Module
import { osVersion, copy, randomString } from 'js-cool'

// Node.js CommonJS
const { osVersion, copy, randomString } = require('js-cool')

// CDN (Browser)
<script src="https://unpkg.com/js-cool/dist/index.iife.min.js"></script>
<script>
  jsCool.browserVersion()
</script>

// Direct import (tree-shaking friendly)
import copy from 'js-cool/copy'
import { randomString } from 'js-cool'

Migration from v5.x to v6.x

📖 Full Migration Guide中文迁移指南

Quick Summary

| Change | v5.x | v6.x | | --------------------- | --------------------------- | ---------------------------------------------------- | | CJS output | dist/index.cjs.js | dist/index.js | | IIFE output | dist/index.global.prod.js | dist/index.iife.min.js | | Global variable | window.JsCool | window.jsCool | | Client module | client | ua | | getAppVersion() | ✅ | ❌ Use appVersion() | | getOsVersion() | ✅ | ❌ Use osVersion() | | getScrollPosition() | ✅ | ❌ Use scroll.getPosition() | | Storage functions | setCache, getCache, ... | storage.local, storage.session, storage.cookie |

clientua Migration

// v5.x
import { client } from 'js-cool'
client.isMobile()

// v6.x
import { ua } from 'js-cool'
ua.isMobile()

// Or tree-shake
import { isMobile, isWeChat } from 'js-cool/ua'

IE11 Compatibility

js-cool v6.x includes built-in IE11 compatibility without requiring external polyfills. All methods work seamlessly in IE11 through an internal compatibility layer.

How It Works

The library includes an internal _compat.ts module that provides IE11-compatible alternatives to ES6+ features:

| ES6+ Feature | IE11 Compatible Alternative | | ---------------------- | ----------------------------------- | | Array.from() | arrayFrom() | | Array.includes() | arrayIncludes() | | String.includes() | strIncludes() | | String.startsWith() | strStartsWith() | | String.endsWith() | strEndsWith() | | String.repeat() | repeatString() | | String.padStart() | padStart() | | String.padEnd() | padEnd() | | Number.isNaN() | isNumberNaN() | | Number.isFinite() | isNumberFinite() | | Number.isInteger() | isNumberInteger() | | Object.assign() | objectAssign() | | Object.values() | objectValues() | | Object.entries() | objectEntries() | | Object.fromEntries() | objectFromEntries() | | Object.hasOwn() | hasOwn() | | globalThis | getGlobalObject() | | new File() | createFile() (falls back to Blob) | | Symbol.iterator | isIterableCompat() | | [...new Set(arr)] | arrayUnique() |

Functions with IE11 Fallbacks

Some functions have built-in graceful degradation:

| Function | IE11 Behavior | | ---------------- | --------------------------------------------------------- | | isURL() | Falls back to regex validation when URL API unavailable | | getDirParams() | Uses regex parsing when URL API unavailable | | urlToBlob() | Uses XHR when fetch unavailable | | isDarkMode() | Returns false (media query not supported) | | base64ToFile() | Returns Blob with name property instead of File |

Type Considerations

// base64ToFile returns File | Blob in IE11
import { base64ToFile } from 'js-cool'

const file = base64ToFile(base64String, 'image.png')
// Type: File | Blob
// In modern browsers: File object
// In IE11: Blob with name property

Function Categories

js-cool provides 140+ utility functions organized into 16 categories:

| Category | Description | Functions | | ----------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | String | String manipulation | camel2Dash, dash2Camel, upperFirst, lowerFirst, capitalize, kebabCase, snakeCase, constantCase, dotCase, pascalCase, titleCase, swapCase, changeCase, reverse, count, truncate, clearHtml, clearAttr, cutCHSString, getCHSLength, template, words, escape, unescape | | Array | Array processing | unique, shuffle, sorter, sortPinyin, chunk, flatten, groupBy, keyBy, countBy, sample, sampleSize, intersect, intersectionBy, union, unionBy, differenceBy, minus, complement, contains, all, any, searchObject, drop, dropRight, take, takeRight, findIndex, findLastIndex, zip, unzip | | Object | Object manipulation | clone, extend, getProperty, setProperty, omit, pick, cleanData, safeParse, safeStringify, mapKeys, mapValues, invert, mergeWith, transform, arrayToCSV, CSVToArray | | Type Check | Type checking | getType, isArray, isObject, isPlainObject, isDate, isRegExp, isWindow, isIterable, isEqual, isEmpty, isNil | | Validate | Validation functions | isEmail, isPhone, isURL, isIDCard, isCreditCard | | URL & Browser | URL parsing and browser detection | getUrlParams, getUrlParam, parseUrlParam, spliceUrlParam, getDirParams, ua, appVersion, browserVersion, compareVersion, nextVersion | | DOM | DOM manipulation | addEvent, removeEvent, stopBubble, stopDefault, copy, windowSize, download, saveFile, downloadFile, downloadUrlFile | | Scroll | Scroll utilities | scroll, getPosition, getProgress, getDirection, isInViewport, scrollTo, scrollToTop, scrollToBottom, scrollBy, lockScroll, unlockScroll, getScrollbarWidth | | Storage | Browser storage | storage, local, session, cookie | | Convert | Format conversion | arrayToCSV, CSVToArray, CSVToJSON, JSONToCSV | | Binary | Binary data conversion (v6.0.0+) | binary, binary.from(), binary.base64, binary.blob, binary.arrayBuffer, binary.file, binary.url, binary.svg, binary.text, binary.dataURL, binary.hex, binary.hash, binary.meta | | Number | Number processing | clamp, round, sum, average, inRange | | Date | Date processing | date, DateParser, formatDate, dateDiff, relativeTime, isToday, isYesterday, isTomorrow, isWeekend, isLeapYear, getDaysInMonth, getQuarter, getDayOfYear, getWeekOfYear, addDate, subtractDate, startOf, endOf | | Color | Color manipulation | hexToRGB, rgbToHSL, RGBToHex, lighten, darken, isLightColor, randomColor | | Utility | General utilities | delay, uuid, randomString, randomNumber, randomNumbers, nextIndex, getFileType, getGlobal, getNumber, fixNumber, toThousands, openUrl, punctualTimer, waiting, fingerprint | | Async Flow | Async flow control | debounce, throttle, retry, awaitTo | | Encode | Encoding/decoding | encodeUtf8, decodeUtf8 | | Network | Network utilities | fillIPv6 |


API Reference

Global

ua

User Agent detection utility.

import { ua } from 'js-cool'

// Get all user agent info
ua.info
// { device: {...}, os: {...}, browser: {...}, environment: {...} }

// Get single info
ua.get('browser') // { name: 'Chrome', version: '123.0.0.0', engine: 'Blink' }
ua.get('device') // { type: 'mobile', mobile: true, tablet: false, ... }
ua.get('os') // { name: 'Android', version: '14' }
ua.get('engine') // { name: 'Blink' }

// Get multiple info
ua.getMultiple(['device', 'os'])
// { device: {...}, os: {...} }

// Quick checks
ua.isMobile() // true/false
ua.isTablet() // true/false
ua.isDesktop() // true/false
ua.isiOS() // true/false
ua.isAndroid() // true/false
ua.isHarmonyOS() // true/false
ua.isWeChat() // true/false
ua.isQQ() // true/false
ua.isMiniProgram() // true/false

// Check if name in UA
ua.has('Chrome') // true/false

// Get language
ua.getLanguage() // 'en-US'

// Get network info
ua.getNetwork() // { online, type, effectiveType, downlink, rtt, saveData }

// Get screen info
ua.getScreen() // { width, height, pixelRatio, orientation, colorDepth }

// Get orientation
ua.getOrientationStatus() // 'portrait' | 'landscape'

patterns

Unified patterns module combining validation, extract, and UA patterns.

import { patterns, validation, extract, DEVICE_PATTERNS, BROWSER_PATTERNS } from 'js-cool'

// Using patterns object
patterns.validation.email.test('[email protected]') // true
patterns.validation.mobile.test('13800138000') // true
patterns.ua.device.mobile.test(navigator.userAgent) // true/false
patterns.ua.browser.chrome.test(navigator.userAgent) // true/false

// Extract patterns (NEW in v6.0.0)
'Price: $99.99'.match(patterns.extract.number) // ['99.99']
'Chrome/120.0.6099.109'.match(patterns.extract.version) // ['120.0.6099.109']
'abc123def456'.match(patterns.extract.integer) // ['123', '456']

// Or import directly
validation.email.test('[email protected]') // true
DEVICE_PATTERNS.mobile.test(navigator.userAgent) // true/false
BROWSER_PATTERNS.chrome.test(navigator.userAgent) // true/false

// Utility functions
patterns.ua.getUserAgent() // get UA string safely
patterns.ua.matchPattern(ua, /Chrome/i) // check if pattern exists
patterns.ua.extractVersion(ua, /Chrome\/(\d+)/i) // '91.0'

// Available validation patterns:
// validation.any, validation.email, validation.mobile, validation.url
// validation.number, validation.chinese, validation.idCard, validation.qq
// validation.ipv4, validation.ipv6, validation.ipv4Private, validation.mac
// validation.uuid, validation.semver, validation.base64, validation.slug
// validation.bankCard, validation.creditCard, validation.hexColor
// validation.password, validation.postcode, validation.username, validation.tel
// validation.json, validation.array, validation.float, validation.string
// validation.date, validation.time, validation.datetime

// Available extract patterns (NEW in v6.0.0):
// extract.number - matches numbers (including decimals)
// extract.integer - matches integers (including negative)
// extract.decimal - matches decimal numbers
// extract.positiveInteger - matches positive integers
// extract.version - matches version strings

// Available UA patterns:
// DEVICE_PATTERNS - mobile, tablet, phone, touch, iphone, ipad, androidPhone, androidTablet
// OS_PATTERNS - windows, macOS, iOS, iPadOS, android, linux, chromeOS, harmonyOS
// BROWSER_PATTERNS - chrome, firefox, safari, edge, opera, ie, samsung, uc, quark, vivaldi, arc, brave, yandex
// ENGINE_PATTERNS - blink, gecko, webkit, trident, edgeHTML
// ENV_PATTERNS - wechat, wxwork, dingtalk, qq, qqBrowser, weibo, alipay, douyin, kuaishou, baidu
//                xiaohongshu, meituan, dianping, taobao, tmall, jd, pinduoduo, miniProgram, miniGame

URL Utilities

URLSearchParams-like API for URL parsing and building, plus a chainable Url class.

import {
  url,
  Url,
  // Query string parsing & building (descriptive names)
  parseQueryString,
  stringifyQueryString,
  // URLSearchParams-like methods (descriptive names)
  getQueryParamValue,
  getAllQueryParamValues,
  hasQueryParam,
  setQueryParam,
  appendQueryParam,
  deleteParam,
  getQueryParamKeys,
  getQueryParamValues,
  getQueryParamEntries,
  // URL property extraction
  getOrigin,
  getHost,
  getHostname,
  getPathname,
  getSearch,
  getHash,
} from 'js-cool'

// Or use short names directly
import { get, getAll, has, set, append, keys, values, entries, parse, stringify } from 'js-cool/url'

// ============ Method 1: Url class (chainable) ============
const u = new Url('https://example.com?id=123')
u.get('id') // '123'
u.set('page', 2).delete('id').toString()
// 'https://example.com?page=2'

// Chainable URL building
new Url('https://api.example.com')
  .path('users', '123')
  .set('fields', 'name')
  .setHash('section')
  .toString()
// 'https://api.example.com/users/123?fields=name#section'

// URL property getters
u.origin // 'https://example.com'
u.host // 'example.com:8080' (with port)
u.hostname // 'example.com' (without port)
u.pathname // '/api/users'
u.search // '?id=123'
u.hash // '#section'

// Hash parameter support
const u2 = new Url('https://a.cn/?ss=1#/path?bb=343')
u2.get('ss') // '1' (from search)
u2.get('bb') // '343' (from hash)
u2.get('ss', 'search') // '1' - explicit scope
u2.get('bb', 'hash') // '343' - explicit scope
u2.toObject() // { ss: '1', bb: '343' }
u2.toDetailObject() // { search: {...}, hash: {...}, all: {...}, source: {...} }

// ============ Method 2: url namespace (static) ============
url.parse('?a=1&b=true', { convert: true }) // { a: 1, b: true }
url.stringify({ a: 1, b: 2 }) // '?a=1&b=2'
url.getOrigin('https://example.com:8080/path') // 'https://example.com:8080'

// URLSearchParams-like (static)
url.get('id', 'https://example.com?id=123') // '123'
url.getAll('id', 'https://example.com?id=1&id=2') // ['1', '2']
url.has('token', 'https://example.com?token=abc') // true
url.set('page', 2, 'https://example.com') // 'https://example.com/?page=2'
url.append('id', 3, 'https://example.com?id=1') // 'https://example.com/?id=1&id=3'
url.delete('token', 'https://example.com?token=abc&id=1') // 'https://example.com/?id=1'

// Iteration
url.keys('https://example.com?a=1&b=2') // ['a', 'b']
url.values('https://example.com?a=1&b=2') // ['1', '2']
url.entries('https://example.com?a=1&b=2') // [['a', '1'], ['b', '2']]

// URL property extraction (static)
url.getOrigin('https://example.com:8080/path') // 'https://example.com:8080'
url.getHost('https://example.com:8080/path') // 'example.com:8080'
url.getHostname('https://example.com:8080/path') // 'example.com'
url.getPathname('https://example.com/api/users?id=1') // '/api/users'
url.getSearch('https://example.com?key=value') // '?key=value'
url.getHash('https://example.com/path#section') // '#section'

// ============ Method 3: Direct function imports ============
import { get, set, parse, stringify } from 'js-cool'

get('id', 'https://example.com?id=123') // '123'
set('page', 2, 'https://example.com') // 'https://example.com/?page=2'
parse('?key1=100&key2=true', { convert: true }) // { key1: 100, key2: true }
stringify({ a: 1, b: 2 }) // '?a=1&b=2'

String

clearAttr

Remove all HTML tag attributes.

import { clearAttr } from 'js-cool'

clearAttr('<div id="test" class="box">content</div>')
// '<div>content</div>'

clearAttr('<a href="url" target="_blank">link</a>')
// '<a>link</a>'

clearAttr('<input type="text" name="field" />')
// '<input />'

clearAttr('<img src="pic.jpg" alt="image" />')
// '<img />'

clearHtml

Remove HTML tags.

import { clearHtml } from 'js-cool'

clearHtml('<div>test<br/>string</div>') // 'teststring'
clearHtml('<p>Hello <b>World</b></p>') // 'Hello World'
clearHtml('<a href="#">link</a>') // 'link'
clearHtml('plain text') // 'plain text'

escape / unescape

Escape/unescape HTML special characters.

import { escape, unescape } from 'js-cool'

// Escape
escape('<div>test</div>') // '&lt;div&gt;test&lt;/div&gt;'
escape('a < b & c > d') // 'a &lt; b &amp; c &gt; d'
escape('"hello" & \'world\'') // '&quot;hello&quot; &amp; &#39;world&#39;'

// Unescape
unescape('&lt;div&gt;test&lt;/div&gt;') // '<div>test</div>'
unescape('&amp;lt;') // '&lt;'
unescape('&quot;hello&quot;') // '"hello"'

getNumber

Extract number(s) from string.

import { getNumber } from 'js-cool'

// Basic usage - returns string
getNumber('Chrome123.45') // '123.45'
getNumber('price: $99.99') // '99.99'
getNumber('version 2.0.1') // '2.0.1'
getNumber('no numbers here') // ''
getNumber('123abc456') // '123456'
getNumber('-12.34') // '-12.34'

// Return as number type
getNumber('Price: $99.99', { type: 'number' }) // 99.99

// Extract all numbers
getNumber('a1b2c3', { multiple: true }) // ['1', '2', '3']
getNumber('Range: 10-20', { multiple: true }) // [10, 20]

// With decimal places
getNumber('Temperature: 36.567°', { decimals: 1 }) // '36.6'

// Multiple numbers as numbers
getNumber('1, 2, 3', { multiple: true, type: 'number' }) // [1, 2, 3]

camel2Dash / dash2Camel

Convert between camelCase and kebab-case.

import { camel2Dash, dash2Camel } from 'js-cool'

// camelCase to kebab-case
camel2Dash('jsCool') // 'js-cool'
camel2Dash('backgroundColor') // 'background-color'
camel2Dash('marginTop') // 'margin-top'
camel2Dash('XMLParser') // 'xml-parser' (consecutive uppercase handled)
camel2Dash('HTMLElement') // 'html-element'
camel2Dash('XMLHttpRequest') // 'xml-http-request'

// kebab-case to camelCase
dash2Camel('js-cool') // 'jsCool'
dash2Camel('background-color') // 'backgroundColor'
dash2Camel('margin-top') // 'marginTop'
dash2Camel('-webkit-transform') // 'WebkitTransform'

upperFirst

Capitalize first letter.

import { upperFirst } from 'js-cool'

upperFirst('hello') // 'Hello'
upperFirst('HELLO') // 'HELLO'
upperFirst('h') // 'H'
upperFirst('') // ''

lowerFirst

Lowercase first letter.

import { lowerFirst } from 'js-cool'

lowerFirst('Fred') // 'fred'
lowerFirst('FRED') // 'fRED'
lowerFirst('hello') // 'hello'
lowerFirst('A') // 'a'
lowerFirst('') // ''

capitalize

Capitalize first character and lowercase rest.

import { capitalize } from 'js-cool'

capitalize('FRED') // 'Fred'
capitalize('HELLO WORLD') // 'Hello world'
capitalize('Hello') // 'Hello'
capitalize('a') // 'A'
capitalize('') // ''

randomString

Generate random string with various options.

import { randomString } from 'js-cool'

// Default: 32 chars with uppercase, lowercase, numbers
randomString() // 'aB3dE7fG9hJ2kL5mN8pQ1rS4tU6vW0xY'

// Specify length
randomString(8) // 'xY7mN2pQ'
randomString(16) // 'aB3dE7fG9hJ2kL5m'

// Using options object
randomString({ length: 16 })
// 'kL5mN8pQ1rS4tU6v'

// Only numbers
randomString({ length: 6, charTypes: 'number' })
// '847291'

// Only lowercase letters
randomString({ length: 8, charTypes: 'lowercase' })
// 'qwertyui'

// Only uppercase letters
randomString({ length: 8, charTypes: 'uppercase' })
// 'ASDFGHJK'

// Multiple char types
randomString({ length: 16, charTypes: ['uppercase', 'number'] })
// 'A3B7C9D2E5F8G1H4'

// Include special characters
randomString({ length: 16, charTypes: ['lowercase', 'number', 'special'] })
// 'a1@b2#c3$d4%e5^f6'

// All char types
randomString({
  length: 20,
  charTypes: ['uppercase', 'lowercase', 'number', 'special'],
})
// 'A1a@B2b#C3c$D4d%E5e^'

// Exclude confusing characters (o, O, 0, l, 1, I)
randomString({ length: 16, noConfuse: true })
// 'aB3dE7fG9hJ2kL5m' (no o, O, 0, l, 1, I)

// Strict mode: must include at least one of each char type
randomString({
  length: 16,
  charTypes: ['uppercase', 'lowercase', 'number'],
  strict: true,
})
// Guaranteed to have at least 1 uppercase, 1 lowercase, 1 number

// Old API style (still supported)
randomString(16, true) // 16 chars with special characters
randomString(true) // 32 chars with special characters

getCHSLength

Get string byte length (full-width chars = 2 bytes). Includes Chinese, emoji, and other wide characters.

import { getCHSLength, isFullWidth } from 'js-cool'

getCHSLength('hello') // 5
getCHSLength('你好') // 4 (2 Chinese chars × 2)
getCHSLength('hello世界') // 9 (5 + 4)
getCHSLength('🎉') // 2 (emoji is full-width)
getCHSLength('') // 0

// Check if a character is full-width
isFullWidth('中') // true
isFullWidth('a') // false
isFullWidth('🎉') // true

cutCHSString

Truncate string by byte length (full-width chars = 2 bytes).

import { cutCHSString } from 'js-cool'

cutCHSString('hello世界', 6) // 'hello世' (5 + 2 = 7 bytes, but we stop at 6)
cutCHSString('hello世界', 6, true) // 'hello世...' (with ellipsis)
cutCHSString('测试字符串', 4) // '测试' (4 bytes = 2 Chinese chars)
cutCHSString('abc', 10) // 'abc' (no truncation needed)

words

Split string into an array of words.

import { words } from 'js-cool'

// Default: split by word boundaries
words('fred, barney, & pebbles') // ['fred', 'barney', 'pebbles']
words('camelCaseHTML') // ['camel', 'Case', 'HTML']
words('PascalCase') // ['Pascal', 'Case']
words('snake_case_string') // ['snake', 'case', 'string']
words('kebab-case-string') // ['kebab', 'case', 'string']

// With custom pattern
words('camelCaseHTML', /[A-Z]{2,}/g) // ['HTML']
words('hello world', /\w+/g) // ['hello', 'world']

// Handle numbers
words('version2Update') // ['version', '2', 'Update']

// Consecutive uppercase
words('HTMLParser') // ['HTML', 'Parser']

constantCase

Convert string to CONSTANT_CASE.

import { constantCase } from 'js-cool'

constantCase('foo-bar') // 'FOO_BAR'
constantCase('foo_bar') // 'FOO_BAR'
constantCase('foo bar') // 'FOO_BAR'
constantCase('fooBar') // 'FOO_BAR'
constantCase('XML-parser') // 'XML_PARSER'

dotCase

Convert string to dot.case.

import { dotCase } from 'js-cool'

dotCase('fooBar') // 'foo.bar'
dotCase('foo-bar') // 'foo.bar'
dotCase('foo_bar') // 'foo.bar'
dotCase('foo bar') // 'foo.bar'

pascalCase

Convert string to PascalCase.

import { pascalCase } from 'js-cool'

pascalCase('foo-bar') // 'FooBar'
pascalCase('foo_bar') // 'FooBar'
pascalCase('foo bar') // 'FooBar'
pascalCase('XML-parser') // 'XmlParser'

titleCase

Convert string to Title Case.

import { titleCase } from 'js-cool'

titleCase('hello world') // 'Hello World'
titleCase('foo-bar-baz') // 'Foo Bar Baz'
titleCase('foo_bar_baz') // 'Foo Bar Baz'
titleCase('fooBarBaz') // 'Foo Bar Baz'

swapCase

Swap the case of each character in a string.

import { swapCase } from 'js-cool'

swapCase('Hello World') // 'hELLO wORLD'
swapCase('JavaScript') // 'jAVAsCRIPT'
swapCase('ABCdef') // 'abcDEF'
swapCase('123abc') // '123ABC'

changeCase

Unified API for all case conversions.

import { changeCase } from 'js-cool'

changeCase('fooBar', 'kebab') // 'foo-bar'
changeCase('foo-bar', 'camel') // 'fooBar'
changeCase('foo_bar', 'pascal') // 'FooBar'
changeCase('fooBar', 'snake') // 'foo_bar'
changeCase('fooBar', 'constant') // 'FOO_BAR'
changeCase('fooBar', 'dot') // 'foo.bar'
changeCase('foo bar', 'title') // 'Foo Bar'
changeCase('Hello', 'swap') // 'hELLO'
changeCase('hello', 'upper') // 'HELLO'
changeCase('HELLO', 'lower') // 'hello'
changeCase('hello', 'upperFirst') // 'Hello'
changeCase('Hello', 'lowerFirst') // 'hello'

reverse

Reverse a string (Unicode aware).

import { reverse } from 'js-cool'

reverse('hello') // 'olleh'
reverse('你好世界') // '界世好你'
reverse('Hello 世界') // '界世 olleH'
reverse('café') // 'éfac'

count

Count occurrences of a substring.

import { count } from 'js-cool'

count('hello hello hello', 'hello') // 3
count('aaa', 'aa') // 1 (non-overlapping by default)
count('aaa', 'aa', { overlapping: true }) // 2 (overlapping matches)
count('Hello World', 'hello', { caseSensitive: false }) // 1
count('abc', 'd') // 0

template

Simple template engine with variable interpolation.

import { template } from 'js-cool'

// Basic usage
const compiled = template('Hello, {{ name }}!')
compiled({ name: 'World' }) // 'Hello, World!'

// HTML escaping (default)
const safe = template('{{ content }}')
safe({ content: '<script>alert("xss")</script>' })
// '&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;'

// Raw output (triple braces)
const raw = template('{{{ html }}}')
raw({ html: '<strong>bold</strong>' }) // '<strong>bold</strong>'

// Nested properties
const nested = template('{{ user.name }} is {{ user.age }} years old.')
nested({ user: { name: 'John', age: 30 } }) // 'John is 30 years old.'

// Using function as data resolver
const fnCompiled = template('Hello, {{ name }}!')
fnCompiled((path) => ({ name: 'World' }[path])) // 'Hello, World!'

// Custom delimiters
const custom = template('Hello, ${ name }!', { open: '${', close: '}' })
custom({ name: 'World' }) // 'Hello, World!'

// Multiple variables
const multi = template('{{ a }} and {{ b }} and {{ c }}')
multi({ a: 1, b: 2, c: 3 }) // '1 and 2 and 3'

Array

shuffle

Shuffle array or string.

import { shuffle } from 'js-cool'

// Shuffle array
shuffle([1, 2, 3, 4, 5]) // [3, 1, 5, 2, 4]
shuffle(['a', 'b', 'c']) // ['c', 'a', 'b']

// Shuffle string
shuffle('hello') // 'lleho'
shuffle('abcdefg') // 'gfedcba'

// Shuffle with size limit
shuffle([1, 2, 3, 4, 5], 3) // [4, 1, 5] (3 random elements)
shuffle('hello', 3) // 'leh' (3 random chars)

unique

Remove duplicates from array.

import { unique } from 'js-cool'

unique([1, 2, 2, 3, 3, 3]) // [1, 2, 3]
unique(['a', 'b', 'a', 'c']) // ['a', 'b', 'c']
unique([1, '1', 1]) // [1, '1']
unique([true, false, true]) // [true, false]
unique([null, null, undefined]) // [null, undefined]

intersect

Intersection of multiple arrays.

import { intersect } from 'js-cool'

intersect([1, 2, 3], [2, 3, 4]) // [2, 3]
intersect([1, 2, 3], [2, 3, 4], [3, 4, 5]) // [3]
intersect(['a', 'b'], ['b', 'c']) // ['b']
intersect([1, 2], [3, 4]) // []

union

Union of multiple arrays.

import { union } from 'js-cool'

union([1, 2], [3, 4]) // [1, 2, 3, 4]
union([1, 2], [2, 3]) // [1, 2, 3]
union([1, 2], [2, 3], [3, 4]) // [1, 2, 3, 4]
union(['a'], ['b'], ['c']) // ['a', 'b', 'c']

minus

Difference of multiple arrays (elements in first but not in others).

import { minus } from 'js-cool'

minus([1, 2, 3], [2, 3, 4]) // [1]
minus([1, 2, 3, 4], [2, 3]) // [1, 4]
minus([1, 2, 3], [2], [3]) // [1]
minus(['a', 'b', 'c'], ['b']) // ['a', 'c']

complement

Complement of multiple arrays (elements not in all arrays combined).

import { complement } from 'js-cool'

complement([1, 2], [2, 3]) // [1, 3]
complement([1, 2], [3, 4]) // [1, 2, 3, 4]
complement(['a', 'b'], ['b', 'c']) // ['a', 'c']

contains

Check if array contains element.

import { contains } from 'js-cool'

contains([1, 2, 3], 2) // true
contains([1, 2, 3], 4) // false
contains(['a', 'b'], 'a') // true
contains([null], null) // true
contains([NaN], NaN) // true

all / any

Check array elements against predicate.

import { all, any } from 'js-cool'

// all - check if all elements pass
all([1, 2, 3], x => x > 0) // true
all([1, 2, 3], x => x > 1) // false
all(['a', 'b'], x => x.length === 1) // true
all([], x => x > 0) // true (empty array)

// any - check if any element passes
any([1, 2, 3], x => x > 2) // true
any([1, 2, 3], x => x > 10) // false
any(['hello', 'world'], x => x.includes('o')) // true
any([], x => x > 0) // false (empty array)

countBy

Count occurrences grouped by iteratee.

import { countBy } from 'js-cool'

countBy([6.1, 4.2, 6.3], Math.floor) // { '4': 1, '6': 2 }
countBy(['one', 'two', 'three'], 'length') // { '3': 2, '5': 1 }
countBy([{ type: 'a' }, { type: 'b' }, { type: 'a' }], 'type') // { a: 2, b: 1 }
countBy(['apple', 'banana', 'apricot'], item => item[0]) // { a: 2, b: 1 }

intersectionBy

Intersection with iteratee.

import { intersectionBy } from 'js-cool'

intersectionBy([2.1, 1.2], [2.3, 3.4], Math.floor) // [2.1]
intersectionBy([{ x: 1 }, { x: 2 }], [{ x: 1 }], 'x') // [{ x: 1 }]
intersectionBy([1, 2, 3], [2, 3, 4], n => n) // [2, 3]

unionBy

Union with iteratee.

import { unionBy } from 'js-cool'

unionBy([2.1], [1.2, 2.3], Math.floor) // [2.1, 1.2]
unionBy([{ x: 1 }], [{ x: 2 }, { x: 1 }], 'x') // [{ x: 1 }, { x: 2 }]
unionBy([1, 2], [2, 3], [3, 4]) // [1, 2, 3, 4]

differenceBy

Difference with iteratee.

import { differenceBy } from 'js-cool'

differenceBy([2.1, 1.2], [2.3, 3.4], Math.floor) // [1.2]
differenceBy([{ x: 2 }, { x: 1 }], [{ x: 1 }], 'x') // [{ x: 2 }]
differenceBy([1, 2, 3], [], n => n) // [1, 2, 3]

drop / dropRight

Drop elements from array.

import { drop, dropRight } from 'js-cool'

// drop - from beginning
drop([1, 2, 3, 4, 5], 3) // [4, 5]
drop([1, 2, 3]) // [2, 3] (default: 1)
drop([1, 2, 3], 0) // [1, 2, 3]
drop([1, 2, 3], 5) // []

// dropRight - from end
dropRight([1, 2, 3, 4, 5], 3) // [1, 2]
dropRight([1, 2, 3]) // [1, 2] (default: 1)
dropRight([1, 2, 3], 0) // [1, 2, 3]
dropRight([1, 2, 3], 5) // []

take / takeRight

Take elements from array.

import { take, takeRight } from 'js-cool'

// take - from beginning
take([1, 2, 3, 4, 5], 3) // [1, 2, 3]
take([1, 2, 3]) // [1] (default: 1)
take([1, 2, 3], 0) // []
take([1, 2, 3], 5) // [1, 2, 3]

// takeRight - from end
takeRight([1, 2, 3, 4, 5], 3) // [3, 4, 5]
takeRight([1, 2, 3]) // [3] (default: 1)
takeRight([1, 2, 3], 0) // []
takeRight([1, 2, 3], 5) // [1, 2, 3]

findIndex / findLastIndex

Find index with predicate.

import { findIndex, findLastIndex } from 'js-cool'

const users = [
  { user: 'barney', active: false },
  { user: 'fred', active: false },
  { user: 'pebbles', active: true },
]

// findIndex - from beginning
findIndex(users, ({ active }) => active) // 2
findIndex(users, { user: 'fred' }) // 1
findIndex(users, ['user', 'barney']) // 0
findIndex(users, 'active') // 2 (truthy check)

// findLastIndex - from end
findLastIndex([1, 2, 3, 4], n => n > 2) // 3
findLastIndex(users, { user: 'fred' }) // 1
findLastIndex(users, 'active') // 2

zip / unzip

Zip and unzip arrays.

import { zip, unzip } from 'js-cool'

// zip - combine arrays
zip(['a', 'b', 'c'], [1, 2, 3]) // [['a', 1], ['b', 2], ['c', 3]]
zip(['a', 'b'], [1, 2], [true, false]) // [['a', 1, true], ['b', 2, false]]

// unzip - separate arrays
unzip([['a', 1], ['b', 2]]) // [['a', 'b'], [1, 2]]
unzip([['a', 1, true], ['b', 2, false]]) // [['a', 'b'], [1, 2], [true, false]]

Object

extend

Deep merge objects.

import { extend } from 'js-cool'

// Shallow copy
const result1 = extend({}, { a: 1 }, { b: 2 })
// { a: 1, b: 2 }

// Deep merge
const result2 = extend(true, {}, { a: { b: 1 } }, { a: { c: 2 } })
// { a: { b: 1, c: 2 } }

// Override values
const result3 = extend(true, {}, { a: 1, b: 2 }, { b: 3 })
// { a: 1, b: 3 }

// Merge arrays
const result4 = extend(true, {}, { arr: [1, 2] }, { arr: [3] })
// { arr: [3, 2] }

// Multiple sources
const result5 = extend(true, {}, { a: 1 }, { b: 2 }, { c: 3 })
// { a: 1, b: 2, c: 3 }

clone

Deep clone object.

import { clone } from 'js-cool'

const obj = { a: { b: 1, c: [1, 2, 3] } }
const cloned = clone(obj)

cloned.a.b = 2
cloned.a.c.push(4)

obj.a.b // still 1
obj.a.c // still [1, 2, 3]

// Clone array
const arr = [{ a: 1 }, { b: 2 }]
const clonedArr = clone(arr)

// Clone Date
const date = new Date()
const clonedDate = clone(date)

// Clone RegExp
const regex = /test/gi
const clonedRegex = clone(regex)

isEqual

Deep equality comparison.

import { isEqual } from 'js-cool'

isEqual({ a: 1 }, { a: 1 }) // true
isEqual({ a: 1, b: 2 }, { b: 2, a: 1 }) // true (order doesn't matter)
isEqual([1, 2, 3], [1, 2, 3]) // true
isEqual(NaN, NaN) // true
isEqual(null, null) // true
isEqual(undefined, undefined) // true

isEqual({ a: 1 }, { a: 2 }) // false
isEqual([1, 2], [2, 1]) // false (order matters)
isEqual({ a: 1 }, { a: 1, b: 2 }) // false

// Deep comparison
isEqual({ a: { b: { c: 1 } } }, { a: { b: { c: 1 } } }) // true

getProperty / setProperty

Get/set nested property by path.

import { getProperty, setProperty } from 'js-cool'

const obj = {
  a: {
    b: [{ c: 1 }, { c: 2 }],
    d: { e: 'hello' },
  },
}

// Get property
getProperty(obj, 'a.b.0.c') // 1
getProperty(obj, 'a.b.1.c') // 2
getProperty(obj, 'a.d.e') // 'hello'
getProperty(obj, 'a.b') // [{ c: 1 }, { c: 2 }]

// With default value
getProperty(obj, 'a.b.5.c', 'default') // 'default'
getProperty(obj, 'x.y.z', null) // null

// Set property
setProperty(obj, 'a.b.0.c', 100)
// obj.a.b[0].c === 100

setProperty(obj, 'a.d.e', 'world')
// obj.a.d.e === 'world'

setProperty(obj, 'a.new', 'value')
// obj.a.new === 'value'

mapKeys / mapValues

Transform object keys or values.

import { mapKeys, mapValues } from 'js-cool'

// mapKeys - transform keys
mapKeys({ a: 1, b: 2 }, (value, key) => key + value) // { a1: 1, b2: 2 }

// mapValues - transform values
mapValues({ a: 1, b: 2 }, n => n * 2) // { a: 2, b: 4 }
mapValues({ a: { x: 1 }, b: { x: 2 } }, 'x') // { a: 1, b: 2 }

invert

Invert object keys and values.

import { invert } from 'js-cool'

invert({ a: '1', b: '2', c: '3' }) // { '1': 'a', '2': 'b', '3': 'c' }
invert({ a: 1, b: 2, c: 1 }) // { '1': 'c', '2': 'b' } (duplicate values: last wins)
invert({ x: 'apple', y: 'banana' }) // { apple: 'x', banana: 'y' }

mergeWith

Merge objects with custom strategy function.

import { mergeWith } from 'js-cool'

// Custom array merge (concat instead of replace)
mergeWith({ a: [1, 2] }, { a: [3, 4] }, (objValue, srcValue) => {
  if (Array.isArray(objValue)) {
    return objValue.concat(srcValue)
  }
})
// { a: [1, 2, 3, 4] }

// Skip certain properties
mergeWith({ a: 1, b: 2 }, { a: 10, b: 20 }, (objValue, srcValue, key) => {
  if (key === 'b') return objValue // keep original
})
// { a: 10, b: 2 }

// Merge multiple objects
mergeWith({ a: 1 }, { b: 2 }, { c: 3 }, (objValue, srcValue) => srcValue ?? objValue)
// { a: 1, b: 2, c: 3 }

transform

Transform object or array with custom accumulator.

import { transform } from 'js-cool'

// Transform object to array
transform({ a: 1, b: 2, c: 3 }, (result, value, key) => {
  result.push({ key, value })
  return result
}, [])
// [{ key: 'a', value: 1 }, { key: 'b', value: 2 }, { key: 'c', value: 3 }]

// Filter and transform
transform({ a: 1, b: 2, c: 3, d: 4 }, (result, value, key) => {
  if (value % 2 === 0) result[key] = value * 2
}, {})
// { b: 4, d: 8 }

// Early exit by returning false
transform({ a: 1, b: 2, c: 3 }, (result, value, key) => {
  result[key] = value
  if (key === 'b') return false
}, {})
// { a: 1, b: 2 }

searchObject

Deep search in object tree.

import { searchObject } from 'js-cool'

const tree = {
  id: 1,
  name: 'root',
  children: [
    { id: 2, name: 'child1', children: [] },
    { id: 3, name: 'child2', children: [{ id: 4, name: 'grandchild' }] },
  ],
}

// Search by predicate
searchObject(tree, item => item.id === 3, { children: 'children' })
// [{ id: 3, name: 'child2', ... }]

// Search by name
searchObject(tree, item => item.name.includes('child'), {
  children: 'children',
})
// [{ id: 2, ... }, { id: 3, ... }, { id: 4, ... }]

// Custom key set
searchObject(tree, item => item.id > 2, {
  children: 'children',
  id: 'id',
})

Type Checking

import { isArray, isDate, isIterable, isObject, isPlainObject, isRegExp, isWindow } from 'js-cool'

// isArray
isArray([1, 2, 3]) // true
isArray('array') // false
isArray(null) // false

// isObject
isObject({}) // true
isObject([]) // true (arrays are objects)
isObject(null) // false
isObject(function () {}) // true

// isPlainObject
isPlainObject({}) // true
isPlainObject(Object.create(null)) // true
isPlainObject([]) // false
isPlainObject(new Date()) // false

// isDate
isDate(new Date()) // true
isDate('2024-01-01') // false
isDate(1234567890000) // false

// isRegExp
isRegExp(/test/) // true
isRegExp(new RegExp('test')) // true
isRegExp('/test/') // false

// isWindow
isWindow(window) // true (in browser)
isWindow({}) // false

// isIterable
isIterable([1, 2, 3]) // true
isIterable('string') // true
isIterable(new Set()) // true
isIterable(new Map()) // true
isIterable({}) // false
isIterable(null) // false

Environment

import { inBrowser, inNodeJs, isDarkMode, windowSize } from 'js-cool'

// inBrowser - check if running in browser
inBrowser() // true in browser, false in Node.js

// inNodeJs - check if running in Node.js
inNodeJs() // true in Node.js, false in browser

// isDarkMode - check if dark mode is enabled
isDarkMode() // true if user prefers dark mode

// windowSize - get window dimensions
windowSize() // { width: 1920, height: 1080 }
windowSize() // { width: 375, height: 667 } (mobile)

Date

js-cool provides a comprehensive date module with chainable API.

date namespace

import { date, DateParser } from 'js-cool'

// Create DateParser instance
const parser = date() // now
const parser = date('2024-01-15') // from string
const parser = date(1705286400000) // from timestamp

// Chainable API
date('2024-01-15').add(1, 'day').format('YYYY-MM-DD') // '2024-01-16'

date().startOf('month').format() // First day of month at 00:00:00

// Date properties
parser.year // 2024
parser.month // 1-12
parser.day // 1-31
parser.hours // 0-23
parser.minutes // 0-59
parser.seconds // 0-59
parser.dayOfWeek // 0-6 (Sunday = 0)

// Comparison methods
parser.isToday()
parser.isYesterday()
parser.isTomorrow()
parser.isWeekend()
parser.isWeekday()
parser.isLeapYear()
parser.isBefore('2024-02-01')
parser.isAfter('2024-01-01')
parser.isSame('2024-01-15', 'day')

// Manipulation (returns new instance)
parser.add(1, 'day') // Add 1 day
parser.subtract(1, 'week') // Subtract 1 week
parser.startOf('month') // Start of month
parser.endOf('day') // End of day (23:59:59)

// Getters
parser.getQuarter() // 1-4
parser.getWeekOfYear() // 1-52
parser.getDayOfYear() // 1-366
parser.getDaysInMonth() // Days in current month

// Difference
const diff = date('2024-01-01').diff('2024-01-03')
// { days: 2, hours: 0, minutes: 0, ... }

// Relative time
date(Date.now() - 3600000).relativeTime() // '1 hour(s) ago'
date(Date.now() - 3600000).relativeTime(undefined, 'zh') // '1小时前'

// Static methods on date namespace
date.format(new Date(), 'YYYY-MM-DD')
date.diff('2024-01-01', '2024-01-03')
date.isToday(new Date())
date.isLeapYear(2024) // true
date.getDaysInMonth(2024, 1) // 29 (February in leap year)
date.getQuarter('2024-06-15') // 2
date.compare('2024-01-01', '2024-01-02') // -1
date.min('2024-01-01', '2024-01-02').format() // '2024-01-01'
date.max('2024-01-01', '2024-01-02').format() // '2024-01-02'

formatDate

import { formatDate } from 'js-cool'

formatDate(new Date('2024-01-15T10:30:45'), 'YYYY-MM-DD HH:mm:ss')
// '2024-01-15 10:30:45'

formatDate(new Date(), 'YYYY年MM月DD日')
// '2024年01月15日'

formatDate(new Date('2024-01-15T14:30:45'), 'hh:mm A')
// '02:30 PM'

// Format tokens: YYYY, YY, MM, M, DD, D, HH, H, hh, h, mm, m, ss, s, SSS, A, a

dateDiff

import { dateDiff } from 'js-cool'

const diff = dateDiff('2024-01-01', '2024-01-03')
// {
//   days: 2,
//   hours: 0,
//   minutes: 0,
//   seconds: 0,
//   milliseconds: 0,
//   total: { days: 2, hours: 48, minutes: 2880, ... }
// }

relativeTime

import { relativeTime } from 'js-cool'

relativeTime(new Date(Date.now() - 5000)) // '5 seconds ago'
relativeTime(new Date(Date.now() - 3600000)) // '1 hour(s) ago'
relativeTime(new Date(Date.now() + 3600000)) // 'in 1 hour(s)'

// Chinese locale
relativeTime(new Date(Date.now() - 3600000), undefined, 'zh') // '1小时前'

Other date functions

import {
  isToday,
  isYesterday,
  isTomorrow,
  isWeekend,
  isLeapYear,
  isBefore,
  isAfter,
  isSame,
  getDaysInMonth,
  getQuarter,
  getDayOfYear,
  getWeekOfYear,
  addDate as add,
  subtractDate as subtract,
  startOf,
  endOf,
} from 'js-cool'

isToday(new Date()) // true
isYesterday(new Date(Date.now() - 86400000)) // true
isTomorrow(new Date(Date.now() + 86400000)) // true
isWeekend(new Date('2024-03-16')) // true (Saturday)
isLeapYear(2024) // true

getDaysInMonth(2024, 1) // 29 (February in leap year)
getQuarter('2024-06-15') // 2
getDayOfYear('2024-12-31') // 366 (leap year)
getWeekOfYear('2024-01-01') // 1

// Date manipulation
const tomorrow = add(new Date(), 1, 'day')
const yesterday = subtract(new Date(), 1, 'day')
const start = startOf(new Date(), 'month')
const end = endOf(new Date(), 'day')

Tree-shaking with subpath import

// Import only what you need
import { date, DateParser } from 'js-cool/date'
import { formatDate, relativeTime } from 'js-cool/date'
import { isToday, isLeapYear } from 'js-cool/date'

Browser Detection

import { appVersion, browserVersion, fingerprint, isNumberBrowser, osVersion } from 'js-cool'

// appVersion - get app version from UA
appVersion('Chrome') // '123.0.0.0'
appVersion('Safari') // '17.0'
appVersion('Firefox') // '123.0'
appVersion('MicroMessenger') // '8.0' (WeChat)

// With custom UA
appVersion('Chrome', 'Mozilla/5.0 Chrome/100.0.0.0')
// '100.0.0.0'

// osVersion - get OS name and version
osVersion()
// { name: 'Windows', version: '10.0' }
// { name: 'Mac OS', version: '10.15.7' }
// { name: 'Android', version: '13' }
// { name: 'iOS', version: '17.0' }
// { name: 'Harmony', version: '4.0' }

// browserVersion - get browser name and version
browserVersion()
// { name: 'Chrome', version: '123.0.0.0' }
// { name: 'Safari', version: '17.0' }
// { name: 'Firefox', version: '123.0' }
// { name: 'Edge', version: '123.0.0.0' }

// isNumberBrowser - check if 360 browser
isNumberBrowser() // true if 360 browser

// fingerprint - generate browser fingerprint
fingerprint() // 'wc7sWJJA8'
fingerprint('example.com') // 'xK9mN2pL5' (with custom domain)

URL & Query

compareVersion

Compare version numbers.

import { compareVersion } from 'js-cool'

compareVersion('1.2.3', '1.2.2') // 1 (greater)
compareVersion('1.2.3', '1.2.3') // 0 (equal)
compareVersion('1.2.3', '1.2.4') // -1 (less)
compareVersion('2.0.0', '1.9.9') // 1
compareVersion('1.10.0', '1.9.0') // 1

// With pre-release tags (priority: rc > beta > alpha)
compareVersion('1.0.0-rc.1', '1.0.0-beta.1') // 1
compareVersion('1.0.0-beta.1', '1.0.0-alpha.1') // 1
compareVersion('1.0.0-alpha.1', '1.0.0') // -1

// Practical usage
const needsUpdate = compareVersion(currentVersion, minVersion) < 0

parseUrlParam

Parse URL parameters string.

import { parseUrlParam } from 'js-cool'

// Basic parsing
parseUrlParam('a=1&b=2&c=3')
// { a: '1', b: '2', c: '3' }

// With type conversion
parseUrlParam('a=1&b=2&c=true', true)
// { a: 1, b: 2, c: true }

// Complex values
parseUrlParam('name=hello%20world&list=1,2,3')
// { name: 'hello world', list: '1,2,3' }

// Empty string
parseUrlParam('') // {}

// Special values (not converted even with true)
parseUrlParam('hex=0xFF&bin=0b111&oct=0o77&exp=1e3', true)
// { hex: '0xFF', bin: '0b111', oct: '0o77', exp: '1e3' }

spliceUrlParam

Build URL parameters string.

import { spliceUrlParam } from 'js-cool'

spliceUrlParam({ a: 1, b: 2 })
// 'a=1&b=2'

spliceUrlParam({ name: 'hello world' })
// 'name=hello%20world'

spliceUrlParam({ a: 1, b: null, c: undefined })
// 'a=1' (null and undefined are skipped)

// With options
spliceUrlParam({ a: 1, b: 2 }, { encode: true })
// 'a=1&b=2'

spliceUrlParam({ a: 1, b: 2 }, { encode: false })
// 'a=1&b=2'

// Complex values
spliceUrlParam({ arr: [1, 2, 3] })
// 'arr=1,2,3'

getUrlParam / getUrlParams

Get URL parameters (from location.search, before #).

import { getUrlParam, getUrlParams } from 'js-cool'

// Get single param
getUrlParam('a', '?a=1&b=2') // '1'
getUrlParam('b', '?a=1&b=2') // '2'
getUrlParam('c', '?a=1&b=2') // null

// Get all params
getUrlParams('?a=1&b=2&c=3')
// { a: '1', b: '2', c: '3' }

// With type conversion
getUrlParams('?a=1&b=2', true)
// { a: 1, b: 2 }

// From full URL
getUrlParams('https://example.com?foo=bar')
// { foo: 'bar' }

getQueryParam / getQueryParams

Get query parameters (after #).

import { getQueryParam, getQueryParams } from 'js-cool'

// Get single query param
getQueryParam('a', '#/?a=1&b=2') // '1'
getQueryParam('b', 'https://example.com#/page?a=1&b=2') // '1'

// Get all query params
getQueryParams('#/?a=1&b=2')
// { a: '1', b: '2' }

// With type conversion
getQueryParams('#/?a=1&b=true', true)
// { a: 1, b: true }

getDirParams

Get directory-style URL params with structured result.

import { getDirParams } from 'js-cool'

getDirParams('https://example.com/a/b/c')
// {
//   origin: 'https://example.com',
//   host: 'example.com',
//   hostname: 'example.com',
//   pathname: '/a/b/c',
//   segments: ['a', 'b', 'c'],
//   query: '',
//   hash: ''
// }

getDirParams('/user/123/profile')
// {
//   origin: '',
//   host: '',
//   hostname: '',
//   pathname: '/user/123/profile',
//   segments: ['user', '123', 'profile'],
//   query: '',
//   hash: ''
// }

// With query and hash
getDirParams('https://example.com/api/users?id=123#section')
// {
//   origin: 'https://example.com',
//   host: 'example.com',
//   hostname: 'example.com',
//   pathname: '/api/users',
//   segments: ['api', 'users'],
//   query: 'id=123',
//   hash: 'section'
// }

Storage

The storage namespace provides a unified API for browser storage with expiration support, generic types, and error handling.

import { storage } from 'js-cool'

// Or from subpath for tree-shaking
import { storage, local, session, cookie } from 'js-cool/storage'

storage.local (localStorage)

// Set item
storage.local.set('name', 'value')
storage.local.set('token', 'abc', { expires: 3600 }) // 1 hour expiration

// Get item (returns null if not found or expired)
const name = storage.local.get('name')

// Check existence
storage.local.has('name') // true

// Delete item
storage.local.delete('name')

// Get all keys
storage.local.keys() // ['key1', 'key2', ...]

// Get count
storage.local.length // 2

// Clear all
storage.local.clear()

// Generic type support
interface User { id: number; name: string }
storage.local.set<User>('user', { id: 1, name: 'John' })
const user = storage.local.get<User>('user') // User | null

storage.session (sessionStorage)

Same API as storage.local:

storage.session.set('temp', 'value', { expires: 1800 }) // 30 min expiration
const temp = storage.session.get('temp')
storage.session.has('temp')
storage.session.delete('temp')
storage.session.keys()
storage.session.clear()
storage.session.length

storage.cookie (Cookie)

// Set cookie (default: 1 day)
storage.cookie.set('name', 'value')

// Set with full options
storage.cookie.set('session', 'xyz', {
  expires: 86400, // Expiration in seconds
  path: '/', // Cookie path
  domain: '.example.com', // Cookie domain
  secure: true, // HTTPS only
  sameSite: 'Strict', // 'Strict' | 'Lax' | 'None'
})

// Get cookie
storage.cookie.get('name') // 'value'
storage.cookie.get('nonexistent') // null

// Get all cookies
storage.cookie.getAll() // { name: 'value', other: 'data' }

// Check existence
storage.cookie.has('name') // true

// Delete cookie
storage.cookie.delete('name')
storage.cookie.delete('name', { path: '/', domain: '.example.com' })

// Clear all cookies
storage.cookie.clear()

Error Handling

import { storage, StorageQuotaError, StorageUnavailableError } from 'js-cool'

try {
  storage.local.set('key', largeData)
} catch (e) {
  if (e instanceof StorageQuotaError) {
    console.error('Storage quota exceeded')
  } else if (e instanceof StorageUnavailableError) {
    console.error('Storage not available (SSR or private mode)')
  }
}

Migration from v5.x

| v5.x | v6.x | | -------------------------- | ------------------------------------------------ | | setCache(k, v) | storage.local.set(k, v) | | setCache(k, v, seconds) | storage.local.set(k, v, { expires: seconds }) | | getCache(k) | storage.local.get(k) | | delCache(k) | storage.local.delete(k) | | setSession(k, v) | storage.session.set(k, v) | | getSession(k) | storage.session.get(k) | | delSession(k) | storage.session.delete(k) | | setCookie(k, v, seconds) | storage.cookie.set(k, v, { expires: seconds }) | | getCookie(k) | storage.cookie.get(k) | | getCookies() | storage.cookie.getAll() | | delCookie(k) | storage.cookie.delete(k) |


Encoding

Base64

import { decodeBase64, encodeBase64 } from 'js-cool'

// Encode
encodeBase64('hello') // 'aGVsbG8='
encodeBase64('你好') // '5L2g5aW9'
encodeBase64('{"a":1}') // 'eyJhIjoxfQ=='
encodeBase64(12345) // 'MTIzNDU='

// Decode
decodeBase64('aGVsbG8=') // 'hello'
decodeBase64('5L2g5aW9') // '你好'
decodeBase64('eyJhIjoxfQ==') // '{"a":1}'

UTF-8

import { decodeUtf8, encodeUtf8 } from 'js-cool'

encodeUtf8('hello') // encoded string
encodeUtf8('你好') // encoded string
decodeUtf8(encoded) // original string

Safe JSON

import { safeParse, safeStringify } from 'js-cool'

// safeParse - never throws
safeParse('{"a":1}') // { a: 1 }
safeParse('invalid json') // null (no error!)
safeParse('{"a":BigInt(1)}') // { a: BigInt(1) }
safeParse(null) // null
safeParse(undefined) // null

// safeStringify - handles BigInt, circular refs
safeStringify({ a: 1 }) // '{"a":1}'
safeStringify({ a: BigInt(9007199254740993n) }) // handles BigInt
safeStringify({ a: () => {} }) // '{"a":null}'

Event

import { addEvent, removeEvent, stopBubble, stopDefault } from 'js-cool'

// Stop bubbling
document.getElementById('btn').addEventListener('click', e => {
  stopBubble(e) // e.stopPropagation()
})

// Prevent default
document.getElementById('link').addEventListener('click', e => {
  stopDefault(e) // e.preventDefault()
})

// Event delegation
const handler = e => {
  console.log('clicked:', e.target)
}

// Add delegated event
addEvent(document, 'click', handler)

// Remove delegated event
removeEvent(document, 'click', handler)

// Delegate to specific container
addEvent(document.getElementById('list'), 'click', e => {
  if (e.target.tagName === 'LI') {
    console.log('List item clicked')
  }
})

Scroll Utilities

import {
  scroll,
  getPosition,
  getProgress,
  createDirectionTracker,
  isInViewport,
  scrollTo,
  scrollToTop,
  scrollToBottom,
  scrollBy,
  lockScroll,
  unlockScroll,
  getScrollbarWidth,
} from 'js-cool'

// Get scroll position state
getPosition() // 'top' | 'bottom' | undefined
getPosition(element, 5) // with custom element and threshold

// Get scroll progress (0-100)
getProgress() // 0-100
getProgress(element) // custom element

// Track scroll direction
const tracker = createDirectionTracker()
window.addEventListener('scroll', () => {
  const dir = tracker() // 'up' | 'down' | null
})

// Check if element is in viewport
isInViewport(element) // true | false
isInViewport(element, { fully: false }) // allow partial visibility

// Scroll to element
scrollTo('#section')
scrollTo(element, { offset: -80, behavior: 'smooth' })

// Scroll to top/bottom
scrollToTop()
scrollToBottom()

// Scroll by amount
scrollBy(200) // scroll down 200px
scrollBy(-100) // scroll up 100px

// Lock/unlock scroll (useful for modals)
lockScroll()
unlockScroll()
scroll.toggle() // toggle lock state
scroll.isLocked() // check if locked

// Get scrollbar width
getScrollbarWidth() // e.g., 15

Utility

uuid

import { uuid } from 'js-cool'

uuid() // '550e8400-e29b-41d4-a716-446655440000'
uuid() // '6ba7b810-9dad-11d1-80b4-00c04fd430c8'
uuid() // 'f47ac10b-58cc-4372-a567-0e02b2c3d479'

copy

import { copy } from 'js-cool'

// Basic usage
await copy('text to copy') // true

// In async function
async function handleCopy() {
  const success = await copy('Copied text')
  if (success) {
    console.log('Copied!')
  } else {
    console.log('Copy failed')
  }
}

// Copy with fallback
const result = await copy('fallback text')
console.log(result ? 'Success' : 'Failed')

download

import { download, saveFile, downloadFile, downloadUrlFile } from 'js-cool'

// Download with URL (multiple types)
download('https://example.com/file.pdf') // default: anchor download
download('https://example.com/file.pdf', 'document.pdf', 'open') // open in new tab
download('https://example.com/file.pdf', 'document.pdf', 'href') // navigate directly
download('https://example.com/file.pdf', 'document.pdf', 'request') // XHR download

// Save data directly as file
saveFile('Hello World', 'hello.txt')
saveFile(JSON.stringify(data), 'data.json')

// Save Blob or ArrayBuffer
const blob = new Blob(['content'], { type: 'text/plain' })
saveFile(blob, 'file.txt')

// Download file using anchor element
downloadFile('https://example.com/file.pdf', 'document.pdf')

// Download via XHR (for authenticated downloads)
downloadUrlFile('https://api.example.com/download', 'data.json')

openUrl

import { openUrl } from 'js-cool'

openUrl('https://example.com') // Opens in new tab
openUrl('https://example.com/file.pdf') // Downloads if can't parse

toThousands

Format number with thousands separator.

import { toThousands } from 'js-cool'

toThousands(1234567.89) // '1,234,567.89'
toThousands(1234567890) // '1,234,567,890'
toThousands(1234.567) // '1,234.567'
toThousands('1234567') // '1,234,567'
toThousands(null) // ''
toThousands(undefined) // ''

// Custom separator
toThousands(1234567.89, { separator: ' ' }) // '1 234 567.89'
toThousands(1234567.89, { separator: "'" }) // "1'234'567.89"

// With decimals limit
toThousands(1234.5678, { decimals: 2 }) // '1,234.57'

// With currency prefix
toThousands(1234.56, { prefix: '$' }) // '$1,234.56'
toThousands(1234.56, { prefix: '¥', decimals: 2 }) // '¥1,234.56'

// With suffix
toThousands(98.5, { suffix: '%', decimals: 1 }) // '98.5%'

// Combined options
toThousands(1234567.89, { prefix: '$', separator: ' ', decimals: 2 }) // '$1 234 567.89'

randomNumber / randomNumbers

import { randomNumber, randomNumbers } from 'js-cool'

// Random integer
randomNumber() // random int between 1-10
randomNumber(1, 100) // random int between 1-100
randomNumber(0, 1) // 0 or 1
randomNumber(-10, 10) // random int between -10 and 10

// Random float
randomNumber(0.1, 0.9) // random float between 0.1 and 0.9

// Random numbers with fixed sum
randomNumbers(4, 100) // [25, 30, 20, 25] (sum = 100)
randomNumbers(3, 10) // [3, 4, 3] (sum = 10)
randomNumbers(5, 1) // [0, 0, 1, 0, 0] (sum = 1)

// Allow zeros (default: true)
randomNumbers(4, 100, true) // no zeros allowed
randomNumbers(4, 100, false) // zeros allowed

randomColor

import { randomColor } from 'js-cool'

// Random color
randomColor() // '#bf444b'

// Lighter colors (higher minimum)
randomColor(200) // '#d6e9d7' (all channels >= 200)
randomColor(128) // '#a1b2c3' (all channels >= 128)

// Range for all channels
randomColor(200, 255) // '#d3f9e4' (200-255)

// Individual channel ranges
randomColor([0, 100, 0], [100, 255, 100])
// Red: 0-100, Green: 100-255, Blue: 0-100

// Warm colors (more red/yellow)
randomColor([200, 100, 0], [255, 200, 100])

// Cool colors (more blue/green)
randomColor([0, 100, 200], [100, 200, 255])

// Dark colors
randomColor(0, 100) // '#3a2b1c'

// Pastel colors
randomColor(150, 230) // '#c8e6c9'

nextIndex

import { nextIndex } from 'js-cool'

nextIndex() // 1001
nextIndex() // 1002
nextIndex() // 1003

// Useful for modals, tooltips
modal.style.zIndex = nextIndex()

nextVersion

import { nextVersion } from 'js-cool'

nextVersion('1.2.3', 'major') // '2.0.0'
nextVersion('1.2.3', 'minor') // '1.3.0'
nextVersion('1.2.3', 'patch') // '1.2.4'
nextVersion('1.2.3', 'premajor') // '2.0.0-0'
nextVersion('1.2.3', 'preminor') // '1.3.0-0'
nextVersion('1.2.3', 'prepatch') // '1.2.4-0'
nextVersion('1.2.3-alpha.1', 'prerelease') // '1.2.3-alpha.2'

// Default is patch
nextVersion('1.2.3') // '1.2.4'

getType

import { getType } from 'js-cool'

getType([1, 2, 3]) // 'array'
getType({}) // 'object'
getType(null) // 'null'
getType(undefined) // 'undefined'
getType('string') // 'string'
getType(123) // 'number'
getType(true) // 'boolean'
getType(() => {}) // 'function'
getType(new Date()) // 'date'
getType(/regex/) // 'regexp'
getType(new Error()) // 'error'
getType(new Map()) // 'map'
getType(new Set()) // 'set'
getType(Symbol()) // 'symbol'
getType(BigInt(1)) // 'bigint'

getFileType

import { getFileType } from 'js-cool'

getFileType('document.pdf') // 'pdf'
getFileType('image.png') // 'image'
getFileType('video.mp4') // 'video'
getFileType('audio.mp3') // 'audio'
getFileType('archive.zip') // 'archive'
getFileType('code.js') // 'code'
getFileType('https://example.com/file.pdf') // 'pdf'

getGlobal

Safely get a global value by path. A secure alternative to dynamic code execution.

import { getGlobal } from 'js-cool'

// Get global functions
getGlobal('JSON.parse') // ƒ parse()
getGlobal('Number') // ƒ Number()
getGlobal('console.log') // ƒ log()

// Get nested properties
getGlobal('document.body') // <body> element (browser)

// Non-existent path
getGlobal('nonExistent') // undefined
getGlobal('a.b.c') // undefined

fixNumber

import { fixNumber } from 'js-cool'

fixNumber(3.14159) // '3.14' (default 2 decimal places)
fixNumber(3.14159, 2) // '3.14'
fixNumber(3.14159, 4) // '3.1416'
fixNumber(3.1, 4) // '3.1' (no trailing zeros)
fixNumber(100, 2) // '100'

delay

import { delay } from 'js-cool'

const d = delay()

// Debounce mode (first trigger executes immediately)
d.register('search', () => console.log('search'), 300, true)

// Throttle mode (delayed execution)
d.register('scroll', () => console.log('scroll'), 300, false)

// Destroy specific handler
d.destroy('search')

// The delay object stores all registered handlers

waiting

import { waiting } from 'js-cool'

// Basic wait
await waiting(1000) // wait 1 second

// With throw on timeout
await waiting(5000, true) // throws after 5 seconds

// In async function
async function example() {
  console.log('start')
  await waiting(1000)
  console.log('after 1 second')
}

// Practical usage
async function poll() {
  while (true) {
    const result = await checkStatus()
    if (result.done) break
    await waiting(1000) // wait before next poll
  }
}

mapTemplate

import { mapTemplate } from 'js-cool'

// ${xxx} style
mapTemplate('Hello, ${name}!', { name: 'World' })
// 'Hello, World!'

// {{xxx}} style
mapTemplate('Hello, {{name}}!', { name: 'World' })
// 'Hello, World!'

// {xxx} style
mapTemplate('Hello, {name}!', { name: 'World' })
// 'Hello, World!'

// Multiple variables
mapTemplate('${greeting}, ${name}! You have ${count} messages.', {
  greeting: 'Hello',
  name: 'John',
  count: 5,
})
// 'Hello, John! You have 5 messages.'

// Nested object
mapTemplate('User: ${user.name}', { user: { name: 'John' } })
// 'User: John'

sorter

import { sorter } from 'js-cool'

const users = [
  { name: 'Bob', age: 30 },
  { name: 'Alice', age: 25 },
  { name: 'Charlie', age: 35 },
]

// Sort by string field
users.sort(sorter('name', 'asc'))
// [{ name: 'Alice', age: 25 }, { name: 'Bob', age: 30 }, ...]

users.sort(sorter('name', 'desc'))
// [{ name: 'Charlie', age: 35 }, { name: 'Bob', age: 30 }, ...]

// Sort by number field
users.sort(sorter('age', 'asc'))
// [{ name: 'Alice', age: 25 }, { name: 'Bob', age: 30 }, ...]

// With transform function
users.sort(sorter('name', 'asc', name => name.toLowerCase()))

sortPinyin

Sort Chinese by pinyin with accurate character detection.

import { sortPinyin } from 'js-cool'

// As compare function
['张三', '李四', '王五'].sort(sortPinyin)
// ['李四', '王五', '张三']

// Using sort method (returns new array)
sortPinyin.sort(['张三', '李四', '王五'])
// ['李四', '王五', '张三']

// Mixed content with null/undefined
sortPinyin.sort(['中文', null, 'English', undefined])
// ['English', '中文', null, undefined] - null/undefined at the end

// With options
sortPinyin.sort(['张三', '李四'], { numeric: true })

punctualTimer

import { punctualTimer } from 'js-cool'

// Create timer
const timer = punctualTimer(() => {
  console.log('Tick at:', new Date())
}, 1000)

// Stop timer
timer.stop()

// Restart timer
timer.start()

// Check if running
timer.isRunning // true/false

awaitTo

import { awaitTo } from 'js-cool'

// Basic usage
const [err, data] = await awaitTo(fetch('/api/data'))
if (err) {
  console.error('Error:', err)
  return
}
console.log('Data:', data)

// With asyn