@avidly/utils
v1.0.2
Published
Pure utility functions for Avidly monorepo - zero dependencies
Downloads
1,050
Readme
@avidly/utils
Pure utility functions for the Avidly monorepo with zero runtime dependencies.
Philosophy
This package contains only pure utility functions that:
- Have no external dependencies (zero deps!)
- Work in both browser and Node.js environments (where applicable)
- Are framework-agnostic
- Can be tree-shaken for optimal bundle sizes
Installation
# Already installed in monorepo
bun add @avidly/utilsUsage
Category-Specific Imports (Recommended)
Import only what you need for better tree-shaking:
// String utilities
import { toCamelCase, sanitize, isString } from '@avidly/utils/string';
// Array utilities
import { sorter, groupItems, unique } from '@avidly/utils/array';
// Object utilities
import { get } from '@avidly/utils/object';
// Date utilities
import { formatDateSSR, formatDateFinnishSSR } from '@avidly/utils/date';
// DOM utilities (client-side only)
import { onClickOutside, scrollToAnchor } from '@avidly/utils/dom';
// ID utilities
import { getId, getAreaId } from '@avidly/utils/id';
// Server-only utilities (Node.js/Remix loaders)
import { isProduction, getCacheConfig, getCacheHeaders } from '@avidly/utils/server';
// Types
import type { SortKey, SortOrder, DateInput } from '@avidly/utils/types';Main Barrel Import
// Import everything (less efficient for tree-shaking)
import { toCamelCase, sorter, formatDateSSR } from '@avidly/utils';API Reference
String Utilities
toCamelCase(str: string): string
Convert kebab-case or snake_case to camelCase.
toCamelCase('hello-world'); // 'helloWorld'
toCamelCase('hello_world'); // 'helloWorld'sanitize(text: string): string
Sanitize text for URL usage. Handles Finnish characters (ä, ö, å).
sanitize('Hello World!'); // 'hello-world'
sanitize('Mäkinen Ärkkölä'); // 'makinen-arkkola'isString(value: any): boolean
Type guard to check if value is a string.
isString('hello'); // true
isString(123); // falseArray Utilities
sorter<T>(property, key, order?)
Generic array sorter function.
const items = [
{ name: 'banana', price: 2 },
{ name: 'apple', price: 1 },
];
// Sort by string
items.sort(sorter('name', 'string', 'asc'));
// Sort by number
items.sort(sorter('price', 'number', 'desc'));
// Sort by date
events.sort(sorter('date', 'date', 'asc'));groupItems<T>(items, groupBy): Record<string, T[]>
Group array items by a property.
const items = [
{ category: 'fruit', name: 'apple' },
{ category: 'fruit', name: 'banana' },
{ category: 'vegetable', name: 'carrot' },
];
const grouped = groupItems(items, 'category');
// {
// fruit: [{ category: 'fruit', name: 'apple' }, { category: 'fruit', name: 'banana' }],
// vegetable: [{ category: 'vegetable', name: 'carrot' }]
// }unique<T>(item, i, k?): string
Generate unique keys for list items.
unique({ id: 'abc' }, 0); // 'abc_0'
unique({ text: 'Hello' }, 0); // 'Hello_0'
unique({}, 0, 1); // 'item_0_1'Object Utilities
get(object, path, defval?): any
Get nested property value (like Lodash's _.get()).
const obj = { user: { name: 'John', address: { city: 'NYC' } } };
get(obj, 'user.name'); // 'John'
get(obj, 'user.address.city'); // 'NYC'
get(obj, ['user', 'name']); // 'John'
get(obj, 'user.age', 25); // 25 (default value)Date Utilities
All date utilities are SSR-safe and provide consistent formatting across server and client.
formatDateSSR(date: DateInput): string
Format date as YYYY-MM-DD.
formatDateSSR(new Date('2024-01-15')); // '2024-01-15'
formatDateSSR('2024-01-15T10:30:00Z'); // '2024-01-15'formatTimeSSR(date: DateInput): string
Format time as HH:MM (24-hour format).
formatTimeSSR(new Date('2024-01-15T10:30:00')); // '10:30'formatDateTimeSSR(date: DateInput): string
Format date and time as YYYY-MM-DD HH:MM.
formatDateTimeSSR(new Date('2024-01-15T10:30:00')); // '2024-01-15 10:30'formatDateFinnishSSR(date: DateInput): string
Format date as DD.MM.YYYY (Finnish format).
formatDateFinnishSSR(new Date('2024-01-15')); // '15.01.2024'getRelativeDateLabelSSR(date: DateInput): string
Get relative date label.
getRelativeDateLabelSSR(new Date()); // 'Today'
getRelativeDateLabelSSR(new Date(Date.now() - 86400000)); // 'Yesterday'
getRelativeDateLabelSSR(new Date(Date.now() - 259200000)); // '3 days ago'DOM Utilities
Client-side utilities (require browser environment).
onClickOutside(element: string, callback: () => void)
Set up click outside event listener.
const cleanup = onClickOutside('dropdown-menu', () => {
console.log('Clicked outside!');
closeDropdown();
});
// Later: cleanup();elementInViewport(el: HTMLElement): boolean
Check if element is in viewport.
const button = document.getElementById('my-button');
if (elementInViewport(button)) {
// Element is visible
}scrollToAnchor(url?: string, linkText?: string): boolean
Smooth scroll to anchor element.
scrollToAnchor('#section-title'); // Scroll to #section-title
scrollToAnchor('/page#contact', 'Contact'); // Try #contact or generated IDassetIdIsEmpty(src: any): boolean
Check if asset ID is empty or invalid.
assetIdIsEmpty(''); // true
assetIdIsEmpty('asset-123'); // false
assetIdIsEmpty({ desktop: 'img.jpg', mobile: 'img-m.jpg' }); // false
assetIdIsEmpty({ desktop: '', mobile: '' }); // trueID Utilities
getId(label: string): string
Generate camelCase ID from label.
getId('hello-world'); // 'helloWorld'
getId('my_component'); // 'myComponent'getAreaId(id: string): string
Generate sanitized area ID (takes first 1-2 words).
getAreaId('Contact Us Now'); // 'contact-us'
getAreaId('Features'); // 'features'
getAreaId('Möbel & Wöhne'); // 'mobel-wohne'Server Utilities (Node.js/SSR Only)
⚠️ Server-only utilities - require Node.js environment. Do NOT import in client components!
isVercelProduction(): boolean
Check if running in Vercel production environment (VERCEL_ENV === 'production').
import { isVercelProduction } from '@avidly/utils/server';
export const loader = async () => {
const isProdEnv = isVercelProduction();
// true only if VERCEL_ENV === 'production'
};isProductionDomain(request: Request): boolean
Check if on the actual production domain (not .vercel.app).
import { isProductionDomain } from '@avidly/utils/server';
export const loader = async ({ request }: LoaderFunctionArgs) => {
const isProdDomain = isProductionDomain(request);
// true if hostname is avidly.fi or www.avidly.fi
// false for *.vercel.app domains
};isProduction(request?: Request): boolean
Check if this is true production (BOTH environment AND domain). Use this for SEO-related decisions (robots meta tags, etc.).
import { isProduction } from '@avidly/utils/server';
export const loader = async ({ request }: LoaderFunctionArgs) => {
// Full check: environment + domain
const isProd = isProduction(request);
// Legacy: just environment check (no request needed)
const isProdEnv = isProduction();
return { isProduction: isProd };
};getCacheConfig(): CacheConfig
Get Builder.io cache configuration based on environment. Production gets longer cache times and excludes unpublished content.
import { getCacheConfig } from '@avidly/utils/server';
import { fetchOneEntry } from '@builder.io/sdk-react';
const content = await fetchOneEntry({
model: 'page',
apiKey,
options: {
includeRefs: true,
...getCacheConfig(), // Injects: includeUnpublished, staleCacheSeconds, cachebust
},
});getCacheHeaders(): HeadersInit
Get HTTP cache headers for responses. Production gets aggressive caching (1 hour), others get no-cache.
import { getCacheHeaders } from '@avidly/utils/server';
import { json } from '@remix-run/node';
export const loader = async () => {
const data = await fetchData();
// Automatically sets correct Cache-Control header
const headers = new Headers(getCacheHeaders());
return json(data, { headers });
};Types
All utility types are available for import:
import type {
// Array types
SortKey,
SortOrder,
UniqueItem,
// Object types
PropertyPath,
// Date types
DateInput,
DateFormat,
RelativeDateLabel,
// DOM types
ElementId,
OutsideClickCallback,
AssetId,
} from '@avidly/utils/types';Development
# Build package
cd packages/utils
bun run build
# Watch mode
bun run dev
# Type check
bun run typecheckDesign Decisions
Why Zero Dependencies?
- Smaller bundle sizes - No external dependencies means less code
- Faster builds - Fewer packages to process
- Better reliability - No dependency updates to track
- Framework agnostic - Works anywhere JavaScript runs
Why Category Files?
- Better tree-shaking - Import only what you need
- Easier navigation - Find utilities by category
- Clear organization - Related utilities stay together
- Type safety - Category-specific types co-located with utilities
Why Extract from UI Package?
- Separation of concerns - Pure utilities vs UI-specific utilities
- Reusability - Can be used by any package or app
- Single source of truth - No duplication across packages
What's NOT Included
This package intentionally does NOT include:
- Validation utilities (use
@avidly/ui/validation- depends on i18n) - Theme utilities (use
@avidly/public-theme/utils- theme-specific) - Builder.io utilities (use
@avidly/ui/utils/builder- UI-specific) - API utilities (use
@avidly/ui/utils/api- UI-specific) - Error utilities (use
@avidly/ui/utils/error- UI-specific) - Any utilities with package dependencies
Contributing
When adding new utilities:
- Ensure they are pure functions with no dependencies
- Add appropriate types in
src/types/[category].ts - Implement utility in
src/[category].ts - Export from
src/index.tsbarrel - Update this README with examples
- Add JSDoc comments with examples
License
Private - Avidly monorepo only
