@hydrogen-ui/utils
v0.1.0
Published
Utility functions for Hydrogen UI - formatters, validators, and helpers for Shopify storefronts
Maintainers
Readme
@hydrogen-ui/utils
A comprehensive utility library for Shopify Hydrogen storefronts. Provides formatting, validation, helper functions, and API utilities optimized for e-commerce applications.
Installation
npm install @hydrogen-ui/utils
# or
yarn add @hydrogen-ui/utils
# or
pnpm add @hydrogen-ui/utilsFeatures
- 💰 Money Formatting - Multi-currency support with proper formatting
- 📅 Date Formatting - Locale-aware date and time formatting
- ✅ Validation - Email, phone, postal code, and password validation
- 🛍️ Product Helpers - Variant selection, availability, and URL generation
- 🖼️ Image Optimization - Shopify CDN transformations and responsive images
- 🔤 String Utilities - Text manipulation and formatting
- 🌐 URL Helpers - URL building and manipulation
- 🚀 API Utilities - Rate limiting, batching, and Storefront API client
- 🌍 International - Multi-locale and multi-currency support
- 🔒 Type Safe - Full TypeScript support
Quick Start
import {
formatMoney,
validateEmail,
getVariantByOptions,
transformImageUrl
} from '@hydrogen-ui/utils';
// Format money
const price = formatMoney(29.99, 'USD'); // $29.99
// Validate email
const isValid = validateEmail('[email protected]'); // true
// Get product variant
const variant = getVariantByOptions(product, {
Color: 'Blue',
Size: 'Medium'
});
// Optimize image
const imageUrl = transformImageUrl(product.image, {
width: 800,
height: 600,
crop: 'center'
});API Reference
Formatters
Money Formatting
import { formatMoney, formatPriceRange, Money } from '@hydrogen-ui/utils/formatters';
// Format single price
formatMoney(29.99, 'USD'); // $29.99
formatMoney(29.99, 'EUR', 'de-DE'); // 29,99 €
formatMoney(2999, 'JPY', 'ja-JP'); // ¥2,999
// Format price range
formatPriceRange(
{ amount: 19.99, currencyCode: 'USD' },
{ amount: 49.99, currencyCode: 'USD' }
); // $19.99 - $49.99
// Money arithmetic
const price = new Money(2999, 'USD');
const tax = price.multiply(0.08);
const total = price.add(tax);
console.log(total.format()); // $32.39
// Calculate discount
const discount = calculateDiscountPercentage(
{ amount: 100, currencyCode: 'USD' },
{ amount: 75, currencyCode: 'USD' }
); // 25Date Formatting
import {
formatDate,
formatRelativeTime,
formatOrderDate,
formatDateRange
} from '@hydrogen-ui/utils/formatters';
// Format dates
formatDate(new Date(), 'en-US'); // October 15, 2023
formatDate(new Date(), 'en-US', { format: 'short' }); // 10/15/23
// Relative time
formatRelativeTime(new Date(Date.now() - 3600000)); // 1 hour ago
formatRelativeTime(new Date(Date.now() + 86400000)); // in 1 day
// Order dates
formatOrderDate('2023-10-15T10:30:00Z'); // October 15, 2023 at 10:30 AM
// Date ranges
formatDateRange(
new Date('2023-10-15'),
new Date('2023-10-20'),
'en-US'
); // Oct 15 - 20, 2023Tax Formatting
import { calculateTax, formatTax, calculateTotal } from '@hydrogen-ui/utils/formatters';
// Calculate tax
const subtotal = { amount: 100, currencyCode: 'USD' };
const taxRate = 0.08; // 8%
const tax = calculateTax(subtotal, taxRate); // { amount: 8, currencyCode: 'USD' }
// Format tax display
formatTax(tax); // $8.00 (8%)
// Calculate total
const shipping = { amount: 10, currencyCode: 'USD' };
const total = calculateTotal(subtotal, tax, shipping); // { amount: 118, currencyCode: 'USD' }Validators
Email Validation
import { validateEmail, normalizeEmail } from '@hydrogen-ui/utils/validators';
// Validate email
validateEmail('[email protected]'); // true
validateEmail('invalid.email'); // false
// Advanced validation
validateEmail('[email protected]', {
allowDisplayName: true,
requireDisplayName: false,
allowUnicode: true,
requireTld: true,
allowIpDomain: false,
domainSpecificValidation: true,
blacklistedDomains: ['tempmail.com']
});
// Normalize email
normalizeEmail('[email protected]'); // [email protected]Phone Validation
import { validatePhone, formatPhone, parsePhone } from '@hydrogen-ui/utils/validators';
// Validate phone
validatePhone('+1 (555) 123-4567', 'US'); // true
validatePhone('07700 900123', 'GB'); // true
// Format phone
formatPhone('5551234567', 'US'); // (555) 123-4567
formatPhone('7700900123', 'GB'); // 07700 900123
// Parse phone
const parsed = parsePhone('+1 (555) 123-4567');
// { countryCode: '1', areaCode: '555', number: '1234567' }Postal Code Validation
import { validatePostalCode, formatPostalCode } from '@hydrogen-ui/utils/validators';
// Validate postal codes
validatePostalCode('12345', 'US'); // true
validatePostalCode('K1A 0B1', 'CA'); // true
validatePostalCode('SW1A 1AA', 'GB'); // true
// Format postal codes
formatPostalCode('12345-6789', 'US'); // 12345-6789
formatPostalCode('k1a0b1', 'CA'); // K1A 0B1Password Validation
import { validatePassword, getPasswordStrength } from '@hydrogen-ui/utils/validators';
// Basic validation
validatePassword('SecurePass123!'); // true
// Custom requirements
const result = validatePassword('MyP@ssw0rd', {
minLength: 8,
maxLength: 128,
requireUppercase: true,
requireLowercase: true,
requireNumbers: true,
requireSpecialChars: true,
disallowCommonPasswords: true,
customBlacklist: ['password', 'shopify']
});
if (!result.isValid) {
console.log(result.errors);
// ['Password must be at least 12 characters long']
}
// Password strength
const strength = getPasswordStrength('MySecureP@ssw0rd123');
// { score: 4, level: 'strong', feedback: [] }Helpers
Product Helpers
import {
getVariantByOptions,
isProductAvailable,
isOnSale,
getProductUrl,
getLowestPrice,
getHighestPrice
} from '@hydrogen-ui/utils/helpers';
// Get variant by options
const variant = getVariantByOptions(product, {
Size: 'Medium',
Color: 'Blue'
});
// Check availability
isProductAvailable(product); // true
isProductAvailable(variant); // true
// Check if on sale
isOnSale(product); // true
// Generate product URL
getProductUrl(product); // /products/t-shirt
getProductUrl(product, {
includeVariant: true,
variant: variant.id
}); // /products/t-shirt?variant=123
// Get price range
const lowest = getLowestPrice(product); // { amount: 19.99, currencyCode: 'USD' }
const highest = getHighestPrice(product); // { amount: 39.99, currencyCode: 'USD' }Image Helpers
import {
transformImageUrl,
generateSrcSet,
getImageAspectRatio,
preloadImage
} from '@hydrogen-ui/utils/helpers';
// Transform Shopify CDN images
const optimized = transformImageUrl(
'https://cdn.shopify.com/image.jpg',
{
width: 800,
height: 600,
crop: 'center',
scale: 2,
format: 'webp'
}
); // https://cdn.shopify.com/[email protected]
// Generate responsive srcset
const srcset = generateSrcSet(imageUrl, {
widths: [400, 800, 1200],
format: 'webp'
});
// image_400w.webp 400w, image_800w.webp 800w, image_1200w.webp 1200w
// Get aspect ratio
const ratio = getImageAspectRatio(image); // 1.5
// Preload critical images
preloadImage(heroImage, { as: 'image', type: 'image/webp' });String Helpers
import {
truncate,
slugify,
titleCase,
pluralize,
stripHtml,
capitalize,
camelCase,
kebabCase
} from '@hydrogen-ui/utils/helpers';
// Text truncation
truncate('Long text that needs truncating', 20); // "Long text that need..."
truncate('Long text', 20, { suffix: '…' }); // "Long text…"
// Generate slugs
slugify('Product Name! (2023)'); // "product-name-2023"
slugify('Café Rouge', { lower: false }); // "Cafe-Rouge"
// Case transformations
titleCase('hello world'); // "Hello World"
capitalize('hello'); // "Hello"
camelCase('hello-world'); // "helloWorld"
kebabCase('HelloWorld'); // "hello-world"
// Pluralization
pluralize(1, 'product'); // "product"
pluralize(5, 'product'); // "products"
pluralize(1, 'box', 'boxes'); // "box"
pluralize(2, 'box', 'boxes'); // "boxes"
// HTML stripping
stripHtml('<p>Hello <strong>world</strong></p>'); // "Hello world"URL Helpers
import {
buildUrl,
parseUrl,
addQueryParams,
removeQueryParams,
getQueryParam
} from '@hydrogen-ui/utils/helpers';
// Build URLs
buildUrl('/products', { sort: 'price', order: 'asc' });
// "/products?sort=price&order=asc"
// Parse URLs
const parsed = parseUrl('/products?sort=price#reviews');
// { pathname: '/products', search: '?sort=price', hash: '#reviews' }
// Query parameters
const url = '/products?sort=price&filter=sale';
addQueryParams(url, { page: 2 }); // "/products?sort=price&filter=sale&page=2"
removeQueryParams(url, ['filter']); // "/products?sort=price"
getQueryParam(url, 'sort'); // "price"API Utilities
Storefront API Client
import { StorefrontClient } from '@hydrogen-ui/utils/api';
// Initialize client
const client = new StorefrontClient({
domain: 'my-store.myshopify.com',
storefrontAccessToken: 'token',
apiVersion: '2023-10'
});
// Execute queries
const { data } = await client.query({
query: `
query GetProduct($handle: String!) {
product(handle: $handle) {
id
title
description
}
}
`,
variables: { handle: 'my-product' }
});
// With caching
const cachedData = await client.query({
query: productQuery,
variables: { handle: 'my-product' },
cache: {
maxAge: 300, // 5 minutes
staleWhileRevalidate: 600 // 10 minutes
}
});Rate Limiter
import { RateLimiter, TokenBucketRateLimiter } from '@hydrogen-ui/utils/api';
// Simple rate limiter
const limiter = new RateLimiter({
maxRequests: 10,
windowMs: 60000 // 1 minute
});
// Execute with rate limiting
await limiter.execute(async () => {
return fetch('/api/endpoint');
});
// Token bucket algorithm
const bucketLimiter = new TokenBucketRateLimiter({
capacity: 100,
refillRate: 10, // tokens per second
initialTokens: 100
});
// Check if request allowed
if (await bucketLimiter.tryConsume(1)) {
// Make request
}Request Batcher
import { RequestBatcher } from '@hydrogen-ui/utils/api';
// Create batcher
const batcher = new RequestBatcher({
maxBatchSize: 50,
maxBatchWindow: 10, // ms
fetch: (requests) => {
// Batch multiple requests
return fetch('/api/batch', {
method: 'POST',
body: JSON.stringify(requests)
});
}
});
// Requests are automatically batched
const [result1, result2] = await Promise.all([
batcher.fetch('/api/product/1'),
batcher.fetch('/api/product/2')
]);TypeScript Support
The package includes comprehensive TypeScript definitions:
import type {
MoneyV2,
Product,
ProductVariant,
ValidationResult,
ImageTransformOptions,
StorefrontClientConfig
} from '@hydrogen-ui/utils';
// Use types in your code
function formatProductPrice(product: Product): string {
const money: MoneyV2 = product.priceRange.minVariantPrice;
return formatMoney(money.amount, money.currencyCode);
}
// Validation with types
function validateUserInput(email: string): ValidationResult {
return validateEmail(email);
}Advanced Usage
Custom Validators
import { createValidator, ValidationRule } from '@hydrogen-ui/utils/validators';
// Create custom validator
const validateUsername = createValidator<string>([
{
test: (value) => value.length >= 3,
message: 'Username must be at least 3 characters'
},
{
test: (value) => /^[a-zA-Z0-9_]+$/.test(value),
message: 'Username can only contain letters, numbers, and underscores'
},
{
test: async (value) => {
const exists = await checkUsernameExists(value);
return !exists;
},
message: 'Username already taken'
}
]);
// Use validator
const result = await validateUsername('john_doe');Custom Formatters
import { createFormatter } from '@hydrogen-ui/utils/formatters';
// Create custom formatter
const formatOrderNumber = createFormatter({
prefix: 'ORD-',
padLength: 6,
padChar: '0'
});
formatOrderNumber(123); // "ORD-000123"Internationalization
import {
formatMoney,
formatDate,
validatePhone
} from '@hydrogen-ui/utils';
// Automatic locale detection
const locale = navigator.language; // 'fr-FR'
// Format for French users
formatMoney(29.99, 'EUR', locale); // "29,99 €"
formatDate(new Date(), locale); // "15 octobre 2023"
validatePhone('06 12 34 56 78', 'FR'); // truePerformance Optimization
import { memoize } from '@hydrogen-ui/utils/helpers';
// Memoize expensive operations
const getExpensiveData = memoize(async (productId: string) => {
const response = await fetch(`/api/products/${productId}/details`);
return response.json();
});
// Subsequent calls with same ID return cached result
const data1 = await getExpensiveData('123'); // API call
const data2 = await getExpensiveData('123'); // From cacheError Handling
All validators return consistent error objects:
const result = validateEmail('invalid');
if (!result.isValid) {
console.error(result.errors);
// ['Please enter a valid email address']
// With error codes
console.error(result.errorCodes);
// ['INVALID_EMAIL_FORMAT']
}Best Practices
Import only what you need for optimal bundle size:
// ✅ Good import { formatMoney } from '@hydrogen-ui/utils/formatters'; // ❌ Avoid import * as utils from '@hydrogen-ui/utils';Use TypeScript for better development experience:
// Type-safe options const options: ImageTransformOptions = { width: 800, height: 600, crop: 'center' };Handle errors gracefully:
try { const formatted = formatMoney(amount, currency); } catch (error) { console.error('Invalid currency:', error); return 'Price unavailable'; }Leverage caching for expensive operations:
const client = new StorefrontClient({ cache: { ttl: 300, // 5 minutes storage: 'memory' } });
Browser Support
- Modern browsers (Chrome, Firefox, Safari, Edge)
- Node.js 14+
- React Native (with polyfills)
Contributing
Adding New Utilities
- Create utility in appropriate directory (
formatters/,validators/, etc.) - Add comprehensive tests
- Export from category index
- Add TypeScript definitions
- Update documentation
Testing
# Run all tests
npm test
# Run specific category
npm test validators
# Watch mode
npm test -- --watchLicense
MIT
