npm package: @hydrogen-ui/utils
Description: Utility functions for Hydrogen UI - formatters, validators, and helpers for Shopify storefronts
Author: Hydrogen UI Team
Latest version: 0.1.0

# @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

```bash
npm install @hydrogen-ui/utils
# or
yarn add @hydrogen-ui/utils
# or
pnpm add @hydrogen-ui/utils
```

## Features

- 💰 **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

```typescript
import { 
  formatMoney, 
  validateEmail, 
  getVariantByOptions,
  transformImageUrl 
} from '@hydrogen-ui/utils';

// Format money
const price = formatMoney(29.99, 'USD'); // $29.99

// Validate email
const isValid = validateEmail('user@example.com'); // 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

```typescript
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' }
); // 25
```

#### Date Formatting

```typescript
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, 2023
```

#### Tax Formatting

```typescript
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

```typescript
import { validateEmail, normalizeEmail } from '@hydrogen-ui/utils/validators';

// Validate email
validateEmail('user@example.com'); // true
validateEmail('invalid.email'); // false

// Advanced validation
validateEmail('user@example.com', {
  allowDisplayName: true,
  requireDisplayName: false,
  allowUnicode: true,
  requireTld: true,
  allowIpDomain: false,
  domainSpecificValidation: true,
  blacklistedDomains: ['tempmail.com']
});

// Normalize email
normalizeEmail('User+tag@EXAMPLE.COM'); // user@example.com
```

#### Phone Validation

```typescript
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

```typescript
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 0B1
```

#### Password Validation

```typescript
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

```typescript
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

```typescript
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/image_800x600_crop_center@2x.webp

// 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

```typescript
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

```typescript
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

```typescript
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

```typescript
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

```typescript
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:

```typescript
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

```typescript
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

```typescript
import { createFormatter } from '@hydrogen-ui/utils/formatters';

// Create custom formatter
const formatOrderNumber = createFormatter({
  prefix: 'ORD-',
  padLength: 6,
  padChar: '0'
});

formatOrderNumber(123); // "ORD-000123"
```

### Internationalization

```typescript
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'); // true
```

### Performance Optimization

```typescript
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 cache
```

## Error Handling

All validators return consistent error objects:

```typescript
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

1. **Import only what you need** for optimal bundle size:
   ```typescript
   // ✅ Good
   import { formatMoney } from '@hydrogen-ui/utils/formatters';
   
   // ❌ Avoid
   import * as utils from '@hydrogen-ui/utils';
   ```

2. **Use TypeScript** for better development experience:
   ```typescript
   // Type-safe options
   const options: ImageTransformOptions = {
     width: 800,
     height: 600,
     crop: 'center'
   };
   ```

3. **Handle errors gracefully**:
   ```typescript
   try {
     const formatted = formatMoney(amount, currency);
   } catch (error) {
     console.error('Invalid currency:', error);
     return 'Price unavailable';
   }
   ```

4. **Leverage caching** for expensive operations:
   ```typescript
   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

1. Create utility in appropriate directory (`formatters/`, `validators/`, etc.)
2. Add comprehensive tests
3. Export from category index
4. Add TypeScript definitions
5. Update documentation

### Testing

```bash
# Run all tests
npm test

# Run specific category
npm test validators

# Watch mode
npm test -- --watch
```

## License

MIT