Stacks Utilities
Umbrella package providing utility functions plus re-exports from specialized packages.
Key Paths
- Core package:
storage/framework/core/utils/src/ - Package:
@stacksjs/utils
Architecture
The index.ts re-exports from these local modules:
clean.ts— project cleanup (cleanProject())config.ts— config builder re-exports from@stacksjs/configequal.ts— deep equalityexport-size.ts— re-exports fromsize.tsfind.ts— find Stacks projects on filesystemgit.ts— git status checkhash.ts— path hashing for cloud deployhelpers.ts— project helpers (env, YAML, version, npm scripts, etc.)macroable.ts— dynamic method registration on classesversions.ts— semver comparison viaBun.semvermerge.ts— deep object merge (defu replacement)detect.ts— indentation and newline detectiondebounce.ts— debounce, throttle, delaybytes.ts— byte formatting and parsingcolors.ts— ANSI terminal color outputmarkdown.ts— markdown table generationsize.ts— file/export size calculationobserver.ts— ResizeObserver polyfillpipeline.ts— Pipeline class for data transformation chains
Also re-exports from @stacksjs/browser:
export * as browserUtils from '@stacksjs/browser'
export { clamp, notNullish, retry, useOnline } from '@stacksjs/browser'
Deep Merge (merge.ts)
Replacement for defu. Arrays are concatenated, objects are recursively merged, special objects (Date, RegExp, Error, Promise, Map, Set, WeakMap, WeakSet, ArrayBuffer views) are NOT deeply merged.
import { merge, mergeDefaults, createMerger, defu } from '@stacksjs/utils'
merge({ a: 1 }, { b: 2 }) // { a: 1, b: 2 }
merge({ a: [1] }, { a: [2] }) // { a: [1, 2] } -- arrays concatenated
merge({ a: { x: 1 } }, { a: { y: 2 } }) // { a: { x: 1, y: 2 } } -- objects merged
mergeDefaults(obj, defaults1, defaults2) // merge defaults first, obj wins
const merger = createMerger(defaults) // reusable merger function
defu(obj, defaults1, defaults2) // alias for mergeDefaults
Key behavior: undefined values in source objects are skipped. Later arguments take precedence in merge(), but the first argument takes precedence in mergeDefaults()/defu().
Debounce & Throttle (debounce.ts)
interface DebounceOptions {
leading?: boolean // default: false
trailing?: boolean // default: true
}
const debouncedFn = debounce(fn, 300, { leading: false, trailing: true })
debouncedFn.cancel() // cancel pending invocation
debouncedFn.flush() // immediately invoke pending, returns result
const throttledFn = throttle(fn, 100)
throttledFn.cancel() // cancel pending invocation
await delay(1000) // sleep for 1 second (returns Promise<void>)
Note: unlike some implementations, this debounce does NOT support maxWait. The throttle uses Date.now() for timing, not setTimeout stacking.
Color Output (colors.ts)
ANSI terminal color library. Auto-detects support via process.env.NO_COLOR, process.env.FORCE_COLOR, and process.stdout.isTTY.
Text Colors
black('text'), red('text'), green('text'), yellow('text')
blue('text'), magenta('text'), cyan('text'), white('text')
gray('text'), grey('text') // grey is alias for gray
Bright Text Colors
lightRed('text'), lightGreen('text'), lightYellow('text'), lightBlue('text')
lightMagenta('text'), lightCyan('text'), lightGray('text'), lightGrey('text')
Background Colors
bgBlack('text'), bgRed('text'), bgGreen('text'), bgYellow('text')
bgBlue('text'), bgMagenta('text'), bgCyan('text'), bgWhite('text')
Bright Background Colors
bgLightRed('text'), bgLightGreen('text'), bgLightYellow('text'), bgLightBlue('text')
bgLightMagenta('text'), bgLightCyan('text')
Text Formatting
bold('text'), dim('text'), italic('text'), underline('text')
inverse('text'), hidden('text'), strikethrough('text'), reset('text')
Utilities
stripColors(coloredText) // remove all ANSI escape codes
supportsColor() // boolean -- is color output supported?
All color functions accept string | number and return string. When colors are unsupported, they return String(text) unchanged.
Byte Formatting (bytes.ts)
interface BytesOptions {
precision?: number // default: 1
binary?: boolean // use KiB/MiB/GiB (default: false, uses KB/MB/GB)
space?: boolean // space between number and unit (default: true)
locale?: string // number formatting locale (default: 'en-US')
minimumFractionDigits?: number // default: 0
maximumFractionDigits?: number // default: precision
}
formatBytes(1024) // '1 KB'
formatBytes(1024, { binary: true }) // '1 KiB'
formatBytes(1234567, { precision: 2 }) // '1.23 MB'
formatBytes(-1024) // '-1 KB'
formatBytes(0.5) // '0.5 B'
parseBytes('1 KB') // 1000
parseBytes('1 KiB') // 1024
parseBytes('1.5 MB') // 1500000
compareBytes('1 MB', '1 GB') // negative number (a < b)
compareBytes(1024, '1 KB') // 24 (a > b)
// Aliases
prettyBytes(1024) // alias for formatBytes
readableSize(1024) // alias for formatBytes
Units: B, KB, MB, GB, TB, PB, EB, ZB, YB (decimal) or B, KiB, MiB, GiB, TiB, PiB, EiB, ZiB, YiB (binary).
Export Size Calculation (size.ts)
interface ExportSize {
name: string // relative path
size: number // bytes
readable: string // human-readable
}
interface PackageExportsSizeResult {
totalSize: number
totalReadable: string
exports: ExportSize[] // sorted by size descending
}
await getFileSize('/path/to/file') // number (bytes), returns 0 on error
const result = await getExportsSize('./dist') // PackageExportsSizeResult
estimateGzipSize(size) // Math.floor(size * 0.3)
estimateBrotliSize(size) // Math.floor(size * 0.25)
formatExportSizes(result) // formatted multi-line string with table
getExportsSize scans a directory for .js, .mjs, .cjs files and calculates their sizes.
Pipeline (pipeline.ts)
Laravel-inspired pipeline for chaining data transformations.
type PipeFunction<T> = (data: T, next: (data: T) => T) => T
interface PipeClass<T> { handle: PipeFunction<T> }
// Array-based chaining
const result = Pipeline.send(data)
.through([transform1, transform2, transform3])
.via('process') // method name on pipe classes (default: 'handle')
.then((data) => data) // final transformation
// Fluent pipe chaining
const result2 = Pipeline.send(data)
.pipe(fn1)
.pipe(fn2)
.thenReturn() // returns data as-is after pipeline
// Pipes can be functions or objects with a handle/via method
Pipeline.send('hello')
.through([
(data, next) => next(data.toUpperCase()),
(data, next) => next(data + '!'),
])
.then(data => data) // 'HELLO!'
Internally uses reduceRight so pipes execute in order (first pipe wraps outermost).
Markdown Tables (markdown.ts)
interface MarkdownTableOptions {
align?: Array<'l' | 'c' | 'r' | 'left' | 'center' | 'right' | null>
padding?: boolean // default: true
delimiter?: string // default: '|'
delimiterStart?: boolean // default: true
delimiterEnd?: boolean // default: true
alignDelimiter?: string // default: '-'
}
markdownTable([
['Name', 'Age', 'City'],
['Alice', '30', 'NYC'],
['Bob', '25', 'LA']
])
// | Name | Age | City |
// | ----- | --- | ---- |
// | Alice | 30 | NYC |
// | Bob | 25 | LA |
markdownTable([['Left', 'Center', 'Right'], ['a', 'b', 'c']], { align: ['l', 'c', 'r'] })
// Uses :--- for left, :---: for center, ---: for right
markdownTableFromObjects([
{ name: 'Alice', age: 30 },
{ name: 'Bob', age: 25 }
])
// Auto-generates header from object keys
Cell values can be string | number | boolean | null | undefined -- all are converted to strings.
YAML (helpers.ts)
Uses Bun.YAML natively:
parseYaml(yamlString) // Bun.YAML.parse(content)
loadYaml(content) // alias for parseYaml
dumpYaml(object) // Bun.YAML.stringify(content), falls back to JSON.stringify
Macroable (macroable.ts)
Laravel-inspired dynamic method registration on classes.
import { Macroable, createMacroable, macroable } from '@stacksjs/utils'
// Option 1: Extend Macroable
class MyClass extends Macroable {}
MyClass.macro('greet', function() { return 'Hello!' })
const instance = new MyClass()
;(instance as any).greet() // 'Hello!'
MyClass.hasMacro('greet') // true
MyClass.getMacros() // ['greet']
MyClass.flushMacros() // remove all macros
// Mixin multiple methods from an object
MyClass.mixin({ hello() { return 'hi' }, bye() { return 'bye' } })
// replace parameter (default: true for mixin, false for macro)
MyClass.macro('greet', newFn, true) // replace existing
// Option 2: Create macroable from existing class
const MacroClass = createMacroable(BaseClass)
// Option 3: Add macroable functionality to existing class
const Enhanced = macroable(ExistingClass)
Enhanced.macro('newMethod', fn)
Static methods: macro(name, fn, replace?), mixin(obj, replace?), hasMacro(name), flushMacros(), getMacros().
ResizeObserver (observer.ts)
isResizeObserverSupported() // boolean
const RO = getResizeObserver() // native or polyfill class
const cleanup = createResizeObserver(callback, element)
const cleanup2 = createResizeObserver(callback, [el1, el2]) // multiple elements
cleanup() // disconnect observer
const stop = observeElementSize(element, (entry) => {
console.log(entry.contentRect.width, entry.contentRect.height)
})
stop() // stop observing
The polyfill uses requestAnimationFrame (or setTimeout fallback) to poll element sizes via getBoundingClientRect().
Deep Equality (equal.ts)
isDeepEqual(obj1, obj2) // deep structural equality
isDeepEqual([1, 2], [1, 2]) // true
isDeepEqual({ a: 1 }, { a: 1 }) // true
isDeepEqual(NaN, NaN) // true (uses Object.is)
Compares arrays element-by-element, objects key-by-key, primitives with Object.is.
Version Comparison (versions.ts)
Uses Bun.semver natively:
const semver: typeof Bun.semver = Bun.semver
isVersionGreaterThan('2.0.0', '1.0.0') // true
isVersionLessThan('1.0.0', '2.0.0') // true
isVersionEqual('1.0.0', '1.0.0') // true
isVersionGreaterThanOrEqual('1.0.0', '1.0.0') // true
isVersionLessThanOrEqual('1.0.0', '2.0.0') // true
// Also exports the framework version
import { version } from '@stacksjs/utils' // from package.json
Detection Utilities (detect.ts)
Separate from the strings package detection -- this is the utils-specific implementation:
interface IndentInfo {
amount: number
type: 'space' | 'tab' | null
indent: string
}
detectIndent(text: string): IndentInfo
detectNewline(text: string): '\r\n' | '\n' | '\r' | null
detectNewlineGraceful(text, fallback?): '\r\n' | '\n' | '\r' // fallback default: '\n'
normalizeNewlines(text, '\n'): string // replace all newlines with specified type
countNewlines(text): { crlf, lf, cr, total }
Project Utilities (helpers.ts)
await packageManager() // reads from framework package.json
await frameworkVersion() // reads version from framework package.json
await isAppKeySet() // checks .env for APP_KEY with length > 16
await initProject() // generates application key via Action.KeyGenerate
await ensureProjectIsInitialized() // copies .env.example if needed, checks app key
await installIfVersionMismatch() // currently a no-op (placeholder)
await setEnvValue('KEY', 'value') // set/update value in .env file
await runNpmScript('build', options?) // run npm script via bun, validates script exists
hasScript(manifest, 'test') // check if script exists in package.json manifest
determineDebugLevel(options?) // returns true if --verbose or app.debug === true
determineResetPreset(preset?) // returns CSS reset import array for UnoCSS
isManifest(obj) // type guard for package.json manifest
isOptionalString(value) // type guard for string | null | undefined
isIpv6(address) // check if AddressInfo is IPv6 (handles node >=18 quirks)
Find Stacks Projects (find.ts)
await findStacksProjects(dir?, options?) // recursively search for Stacks projects
// A Stacks project = directory with 'buddy' file + 'storage/framework/core/buddy' structure
// Excludes: node_modules, dist, vendor, storage/framework, ~/Documents, ~/Pictures, ~/Library, ~/.Trash, dotfolders
Git Utilities (git.ts)
isGitClean(): boolean // runs `git diff-index --quiet HEAD --`, returns true if clean
Clean Project (clean.ts)
await cleanProject() // removes bun.lock, yarn.lock, node_modules, dist, cdk.out, etc.
Config Builders (re-exported from @stacksjs/config)
defineApp()
defineCache()
defineCdn()
defineChat()
defineCli()
defineDatabase()
defineDependencies()
defineDns()
defineEmail()
defineEmailConfig()
defineGit()
defineHashing()
defineLibrary()
defineNotification()
definePayment()
defineQueue()
defineSearchEngine()
defineServices()
defineSms()
defineFilesystems()
defineUi()
Hash Utilities (hash.ts)
Internal cloud deployment helpers:
originRequestFunctionHash() // hash of cloud edge function source
websiteSourceHash() // hash of website/docs source (depends on docMode config)
docsSourceHash() // hash of docs source
Glob (re-export)
export { glob } from '@stacksjs/storage'
Gotchas
merge()concatenates arrays instead of replacing them -- this differs from spread/Object.assign behaviormergeDefaults()/defu()gives precedence to the FIRST argument (the object), not the defaults- Color functions only work in terminal environments -- check
supportsColor()first debouncedoes NOT supportmaxWait-- use throttle if you need guaranteed maximum delaythrottle.cancel()exists but there is nothrottle.flush()- Pipeline uses
reduceRightinternally so pipes execute in array order, not reverse - YAML operations use
Bun.YAML--dumpYamlfalls back toJSON.stringifyifBun.YAML.stringifyis unavailable detectIndentin utils (detect.ts) is a different, simpler implementation than the one in strings (detect-indent.ts)isDeepEqualusesgetTypeNamefrom@stacksjs/typesfor type checking- Version comparison uses
Bun.semver.order()which returns -1, 0, or 1 cleanProject()usesBun.$shell -- destructive operation that removes lock files, node_modules, and dist