diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 76eea84..acfdd99 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,7 +46,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: lts/* + node-version: 24 - name: Yarn run: yarn install --frozen-lockfile @@ -56,10 +56,79 @@ jobs: yarn preparemetadata yarn test + - name: Build + run: yarn build + - name: Release + id: semantic_release if: github.ref == 'refs/heads/develop' run: | git config --global user.email "support@dev.me" git config --global user.name "DEV.ME Team" npm i -g semantic-release @semantic-release/git @semantic-release/github conventional-changelog-conventionalcommits - npx semantic-release --no-ci --debug + npx semantic-release --no-ci --debug 2>&1 | tee release-output.txt + + # Extract version and tag info from release output + if grep -q "Published release" release-output.txt; then + echo "release_published=true" >> $GITHUB_OUTPUT + VERSION=$(grep -oP 'Published release \K[0-9]+\.[0-9]+\.[0-9]+' release-output.txt | head -1) + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "tag=v$VERSION" >> $GITHUB_OUTPUT + else + echo "release_published=false" >> $GITHUB_OUTPUT + fi + + - name: Add CI Summary + if: always() + run: | + echo "## đŸ”Ŧ CI Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Check if release step was run (only on develop branch) + if [[ "${{ github.ref }}" == "refs/heads/develop" ]]; then + if [[ "${{ steps.semantic_release.outputs.release_published }}" == "true" ]]; then + echo "### ✅ Pre-release Published Successfully!" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "- **Version:** \`${{ steps.semantic_release.outputs.version }}\`" >> $GITHUB_STEP_SUMMARY + echo "- **Tag:** \`${{ steps.semantic_release.outputs.tag }}\`" >> $GITHUB_STEP_SUMMARY + echo "- **Branch:** \`${{ github.ref_name }}\`" >> $GITHUB_STEP_SUMMARY + echo "- **Commit:** \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### 🔗 Links" >> $GITHUB_STEP_SUMMARY + echo "- [NPM Package](https://www.npmjs.com/package/@devmehq/email-validator-js/v/${{ steps.semantic_release.outputs.version }})" >> $GITHUB_STEP_SUMMARY + echo "- [GitHub Release](https://github.com/${{ github.repository }}/releases/tag/${{ steps.semantic_release.outputs.tag }})" >> $GITHUB_STEP_SUMMARY + else + echo "### â„šī¸ No Pre-release Published" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "No pre-release was created. This could be because:" >> $GITHUB_STEP_SUMMARY + echo "- No relevant commits found for release" >> $GITHUB_STEP_SUMMARY + echo "- Commits don't follow conventional commit format" >> $GITHUB_STEP_SUMMARY + echo "- Release conditions not met" >> $GITHUB_STEP_SUMMARY + fi + else + echo "### ✅ CI Tests Passed" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "All tests completed successfully on branch \`${{ github.ref_name }}\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "â„šī¸ **Note:** Releases are only created from the \`develop\` branch" >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "### 📊 Build Information" >> $GITHUB_STEP_SUMMARY + echo "- **Workflow:** \`${{ github.workflow }}\`" >> $GITHUB_STEP_SUMMARY + echo "- **Branch:** \`${{ github.ref_name }}\`" >> $GITHUB_STEP_SUMMARY + echo "- **Commit:** \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY + echo "- **Run ID:** \`${{ github.run_id }}\`" >> $GITHUB_STEP_SUMMARY + echo "- **Run Number:** \`${{ github.run_number }}\`" >> $GITHUB_STEP_SUMMARY + echo "- **Actor:** \`${{ github.actor }}\`" >> $GITHUB_STEP_SUMMARY + echo "- **Event:** \`${{ github.event_name }}\`" >> $GITHUB_STEP_SUMMARY + + # Add PR information if available + if [[ "${{ github.event_name }}" == "pull_request" ]]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "### 🔀 Pull Request Information" >> $GITHUB_STEP_SUMMARY + echo "- **PR Number:** #${{ github.event.pull_request.number }}" >> $GITHUB_STEP_SUMMARY + echo "- **PR Title:** ${{ github.event.pull_request.title }}" >> $GITHUB_STEP_SUMMARY + echo "- **Base Branch:** \`${{ github.event.pull_request.base.ref }}\`" >> $GITHUB_STEP_SUMMARY + echo "- **Head Branch:** \`${{ github.event.pull_request.head.ref }}\`" >> $GITHUB_STEP_SUMMARY + fi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 27e9387..3b239a6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -39,7 +39,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: lts/* + node-version: 24 - name: Yarn run: yarn install --frozen-lockfile @@ -49,9 +49,57 @@ jobs: yarn preparemetadata yarn test + - name: Build + run: yarn build + - name: Release + id: semantic_release run: | git config --global user.email "support@dev.me" git config --global user.name "DEV.ME Team" npm i -g semantic-release @semantic-release/git @semantic-release/github conventional-changelog-conventionalcommits - npx semantic-release --no-ci --debug + npx semantic-release --no-ci --debug 2>&1 | tee release-output.txt + + # Extract version and tag info from release output + if grep -q "Published release" release-output.txt; then + echo "release_published=true" >> $GITHUB_OUTPUT + VERSION=$(grep -oP 'Published release \K[0-9]+\.[0-9]+\.[0-9]+' release-output.txt | head -1) + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "tag=v$VERSION" >> $GITHUB_OUTPUT + else + echo "release_published=false" >> $GITHUB_OUTPUT + fi + + - name: Add Release Summary + if: always() + run: | + echo "## đŸ“Ļ Release Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [[ "${{ steps.semantic_release.outputs.release_published }}" == "true" ]]; then + echo "### ✅ Release Published Successfully!" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "- **Version:** \`${{ steps.semantic_release.outputs.version }}\`" >> $GITHUB_STEP_SUMMARY + echo "- **Tag:** \`${{ steps.semantic_release.outputs.tag }}\`" >> $GITHUB_STEP_SUMMARY + echo "- **Branch:** \`${{ github.ref_name }}\`" >> $GITHUB_STEP_SUMMARY + echo "- **Commit:** \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### 🔗 Links" >> $GITHUB_STEP_SUMMARY + echo "- [NPM Package](https://www.npmjs.com/package/@devmehq/email-validator-js/v/${{ steps.semantic_release.outputs.version }})" >> $GITHUB_STEP_SUMMARY + echo "- [GitHub Release](https://github.com/${{ github.repository }}/releases/tag/${{ steps.semantic_release.outputs.tag }})" >> $GITHUB_STEP_SUMMARY + else + echo "### â„šī¸ No Release Published" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "No release was created. This could be because:" >> $GITHUB_STEP_SUMMARY + echo "- No relevant commits found for release" >> $GITHUB_STEP_SUMMARY + echo "- Commits don't follow conventional commit format" >> $GITHUB_STEP_SUMMARY + echo "- Release conditions not met" >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "### 📊 Build Information" >> $GITHUB_STEP_SUMMARY + echo "- **Workflow:** \`${{ github.workflow }}\`" >> $GITHUB_STEP_SUMMARY + echo "- **Run ID:** \`${{ github.run_id }}\`" >> $GITHUB_STEP_SUMMARY + echo "- **Run Number:** \`${{ github.run_number }}\`" >> $GITHUB_STEP_SUMMARY + echo "- **Actor:** \`${{ github.actor }}\`" >> $GITHUB_STEP_SUMMARY + echo "- **Event:** \`${{ github.event_name }}\`" >> $GITHUB_STEP_SUMMARY diff --git a/.prettierrc.yaml b/.prettierrc.yaml deleted file mode 100644 index 46be197..0000000 --- a/.prettierrc.yaml +++ /dev/null @@ -1,4 +0,0 @@ -trailingComma: "es5" -tabWidth: 2 -semi: false -singleQuote: true diff --git a/.scripts/prepare.js b/.scripts/prepare.js index a8807c4..13c3425 100644 --- a/.scripts/prepare.js +++ b/.scripts/prepare.js @@ -2,13 +2,7 @@ * This script loads the geocoder and carrier data from the libphonenumber repository, * creates bson files from them and autogenerated the types in src/locales.ts */ -const { - readdirSync, - writeFileSync, - lstatSync, - createReadStream, - mkdirSync, -} = require('fs') +const { readdirSync, writeFileSync, lstatSync, createReadStream, mkdirSync } = require('fs') const { join, basename } = require('path') const { createInterface } = require('readline') const { execSync } = require('child_process') @@ -21,7 +15,7 @@ async function prepareLocale(localePath, locale, type) { const files = readdirSync(localePath).filter((source) => fileRe.test(source)) const lineRe = /^([0-9]+)\|(.*)$/ for (let i = 0; i < files.length; ++i) { - let data = {} + const data = {} const file = files[i] const countryCode = basename(file, '.txt') const ccRe = new RegExp(`^${countryCode}`) @@ -49,9 +43,7 @@ async function prepareLocale(localePath, locale, type) { } async function preparePath(dataPath, type) { - const locales = readdirSync(dataPath).filter((source) => - isDir(join(dataPath, source)) - ) + const locales = readdirSync(dataPath).filter((source) => isDir(join(dataPath, source))) const promises = [] for (let i = 0; i < locales.length; ++i) { const locale = locales[i] @@ -65,11 +57,8 @@ async function preparePath(dataPath, type) { async function prepareTimezones() { const lineRe = /^([0-9]+)\|(.*)$/ - let data = {} - const file = join( - __dirname, - '/../resources/libphonenumber/resources/timezones/map_data.txt' - ) + const data = {} + const file = join(__dirname, '/../resources/libphonenumber/resources/timezones/map_data.txt') const fileStream = createReadStream(file) const rl = createInterface({ input: fileStream, @@ -93,10 +82,7 @@ const prepare = async () => { mkdirSync(join(__dirname, '/../resources/geocodes'), { recursive: true }) mkdirSync(join(__dirname, '/../resources/carrier'), { recursive: true }) execSync( - `cd ${join( - __dirname, - '/../resources' - )} && git clone https://github.com/google/libphonenumber` + `cd ${join(__dirname, '/../resources')} && git clone https://github.com/google/libphonenumber` ) console.log('Preparing metadata...') const dataBasePath = join(__dirname, '/../resources/libphonenumber/resources') @@ -104,9 +90,7 @@ const prepare = async () => { const geocodingPath = join(dataBasePath, 'geocoding') const { locales: geoLocales } = await preparePath(geocodingPath, 'geocodes') - generatedTypes += `export type GeocoderLocale = ${geoLocales - .map((l) => `'${l}'`) - .join(' | ')};\n` + generatedTypes += `export type GeocoderLocale = ${geoLocales.map((l) => `'${l}'`).join(' | ')};\n` const carrierPath = join(dataBasePath, 'carrier') const { locales: carrierLocales } = await preparePath(carrierPath, 'carrier') diff --git a/README.md b/README.md index b4c9b70..41050da 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![NPM version](https://badgen.net/npm/v/@devmehq/phone-number-validator-js)](https://npm.im/@devmehq/phone-number-validator-js) [![Build Status](https://github.com/devmehq/phone-number-validator-js/workflows/CI/badge.svg)](https://github.com/devmehq/phone-number-validator-js/actions) -[![Downloads](https://img.shields.io/npm/dm/@devmehq/phone-number-validator-js.svg)](https://www.npmjs.com/package/phone-number-validator-js) +[![Downloads](https://img.shields.io/npm/dm/@devmehq/phone-number-validator-js.svg)](https://www.npmjs.com/package/@devmehq/phone-number-validator-js) [![UNPKG](https://img.shields.io/badge/UNPKG-OK-179BD7.svg)](https://unpkg.com/browse/@devmehq/phone-number-validator-js@latest/) ### Verify phone number, validate format, checking carrier name, geo and timezone infos. @@ -34,6 +34,8 @@ ✅ TypeScript support with strict type safety +✅ Serverless architecture support (AWS Lambda, Cloudflare Workers, Vercel Edge, Deno) + ## Use cases - Increase delivery rate of SMS campaigns by removing invalid phone numbers @@ -174,6 +176,154 @@ setCacheSize(50) clearCache() ``` +## Serverless Support + +The library provides a lightweight serverless version that's optimized for edge environments like AWS Lambda, Cloudflare Workers, Vercel Edge Functions, and Deno Deploy. + +### Features +- **244KB bundle size** (minified) - fits well under most size limits +- **No Node.js dependencies** - runs in any JavaScript environment +- **Resource loader pattern** - load data from your preferred storage (S3, R2, KV, etc.) +- **Same API** - drop-in replacement for the standard version + +### Installation for Serverless + +```js +// Use the serverless entry point +import { + setResourceLoader, + parsePhoneNumber, + geocoder, + carrier, + timezones +} from '@devmehq/phone-number-validator-js/serverless' +``` + +### Serverless Examples + +#### AWS Lambda +```js +import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3' +import { setResourceLoader, geocoder, parsePhoneNumber } from '@devmehq/phone-number-validator-js/serverless' + +const s3 = new S3Client() + +// Set up resource loader +setResourceLoader({ + async loadResource(path) { + try { + const command = new GetObjectCommand({ + Bucket: process.env.RESOURCES_BUCKET, + Key: `phone-validator/${path}` + }) + const response = await s3.send(command) + return new Uint8Array(await response.Body.transformToByteArray()) + } catch { + return null + } + } +}) + +// Lambda handler +export async function handler(event) { + const phoneNumber = parsePhoneNumber(event.phone, event.country) + const location = await geocoder(phoneNumber) + + return { + statusCode: 200, + body: JSON.stringify({ location }) + } +} +``` + +#### Cloudflare Workers +```js +import { setResourceLoader, carrier, parsePhoneNumber } from '@devmehq/phone-number-validator-js/serverless' + +// Use R2 storage for resources +setResourceLoader({ + async loadResource(path) { + const object = await env.RESOURCES_BUCKET.get(`phone-validator/${path}`) + if (!object) return null + const buffer = await object.arrayBuffer() + return new Uint8Array(buffer) + } +}) + +export default { + async fetch(request, env) { + const url = new URL(request.url) + const phone = url.searchParams.get('phone') + + const phoneNumber = parsePhoneNumber(phone) + const carrierInfo = await carrier(phoneNumber) + + return Response.json({ carrier: carrierInfo }) + } +} +``` + +#### Vercel Edge Functions +```js +import { setResourceLoader, timezones, parsePhoneNumber } from '@devmehq/phone-number-validator-js/serverless' + +// Use Vercel Blob storage +setResourceLoader({ + async loadResource(path) { + const response = await fetch(`${process.env.BLOB_URL}/phone-validator/${path}`) + if (!response.ok) return null + const buffer = await response.arrayBuffer() + return new Uint8Array(buffer) + } +}) + +export const config = { runtime: 'edge' } + +export default async function handler(req) { + const { phone } = await req.json() + const phoneNumber = parsePhoneNumber(phone) + const tzs = await timezones(phoneNumber) + + return Response.json({ timezones: tzs }) +} +``` + +### Resource Files + +The serverless version requires resource files to be deployed separately. Download them from the npm package: + +```bash +# Extract resource files from the npm package +npm pack @devmehq/phone-number-validator-js +tar -xf devmehq-phone-number-validator-js-*.tgz +cp -r package/resources/* your-storage-location/ +``` + +Then upload to your preferred storage (S3, R2, Blob storage, etc.) and configure the resource loader accordingly. + +### Performance Tips + +1. **Use caching**: The library includes built-in LRU caching +2. **Deploy resources to the same region** as your functions for lower latency +3. **Consider using CDN** for resource files if serving globally +4. **Use sync loader** when possible for better performance: + +```js +// Sync loader for environments that support it +setResourceLoader({ + loadResourceSync(path) { + // Synchronous loading implementation + return loadFromCacheSync(path) + }, + async loadResource(path) { + // Async fallback + return loadFromCacheAsync(path) + } +}) +``` + +For detailed serverless deployment guides, see [SERVERLESS.md](./SERVERLESS.md). + ## Performance diff --git a/SERVERLESS.md b/SERVERLESS.md new file mode 100644 index 0000000..15cce4d --- /dev/null +++ b/SERVERLESS.md @@ -0,0 +1,421 @@ +# Serverless Phone Number Validator + +This library now supports serverless environments without Node.js dependencies! The serverless version uses a lightweight resource loader pattern, allowing you to load phone number metadata from your preferred storage backend. + +## Features + +- **Zero Node.js dependencies** - Works in any JavaScript runtime +- **Lightweight** - Only 244KB minified +- **Flexible resource loading** - Load metadata from KV, S3, CDN, or any storage backend +- **Multiple formats** - ESM, CommonJS, and UMD builds available +- **Platform agnostic** - Works on AWS Lambda, Cloudflare Workers, Vercel Edge, Deno Deploy, and more + +## Installation + +```bash +npm install @devmehq/phone-number-validator-js +# or +yarn add @devmehq/phone-number-validator-js +``` + +## Building for Serverless + +```bash +# Build serverless versions +yarn build:serverless +``` + +Creates in `lib/`: +- `serverless.esm.js` - ES Module (555KB) +- `serverless.esm.min.js` - ES Module minified (244KB) +- `serverless.cjs.js` - CommonJS (556KB) +- `serverless.umd.js` - UMD (568KB) +- `serverless.umd.min.js` - UMD minified (244KB) + +## Usage + +### Using the Serverless Version + +The serverless version requires you to provide a resource loader for accessing phone number metadata: + +```javascript +import { + setResourceLoader, + parsePhoneNumber, + geocoderAsync, + carrierAsync, + timezonesAsync +} from '@devmehq/phone-number-validator-js/lib/serverless.esm.min.js'; + +// Set up your resource loader (see examples below) +import { CloudflareKVLoader } from './resource-loaders.js'; +const loader = new CloudflareKVLoader(env.PHONE_DATA); +setResourceLoader(loader); + +// Parse and validate +const phoneNumber = parsePhoneNumber('+14155552671', 'US'); + +if (phoneNumber && phoneNumber.isValid()) { + // Use async methods with lite version + const [geo, car, tz] = await Promise.all([ + geocoderAsync(phoneNumber), + carrierAsync(phoneNumber), + timezonesAsync(phoneNumber) + ]); + + console.log({ + international: phoneNumber.formatInternational(), + geocoder: geo, + carrier: car, + timezones: tz + }); +} +``` + + +## Platform-Specific Deployment + +### AWS Lambda + +Deploy as a Lambda function with Node.js 18+ runtime: + +```javascript +// handler.js +import { parsePhoneNumber, geocoder, carrier, timezones } from './serverless.esm.js'; + +export const handler = async (event) => { + const { phoneNumber, countryCode } = JSON.parse(event.body); + const parsed = parsePhoneNumber(phoneNumber, countryCode); + + if (!parsed || !parsed.isValid()) { + return { + statusCode: 400, + body: JSON.stringify({ error: 'Invalid phone number' }) + }; + } + + return { + statusCode: 200, + body: JSON.stringify({ + valid: true, + country: parsed.country, + geocoder: geocoder(parsed), + carrier: carrier(parsed), + timezones: timezones(parsed) + }) + }; +}; +``` + +Deploy with AWS SAM: + +```yaml +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 + +Resources: + PhoneValidatorFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: . + Handler: handler.handler + Runtime: nodejs18.x + MemorySize: 256 + Timeout: 10 +``` + +### Cloudflare Workers + +```javascript +// worker.js +import { parsePhoneNumber, geocoder, carrier, timezones } from './serverless.esm.js'; + +export default { + async fetch(request) { + if (request.method !== 'POST') { + return new Response('Method not allowed', { status: 405 }); + } + + const { phoneNumber, countryCode } = await request.json(); + const parsed = parsePhoneNumber(phoneNumber, countryCode); + + if (!parsed || !parsed.isValid()) { + return Response.json({ error: 'Invalid phone number' }, { status: 400 }); + } + + return Response.json({ + valid: true, + country: parsed.country, + geocoder: geocoder(parsed), + carrier: carrier(parsed), + timezones: timezones(parsed) + }); + } +}; +``` + +Deploy with Wrangler: + +```toml +# wrangler.toml +name = "phone-validator" +main = "worker.js" +compatibility_date = "2023-05-18" + +[build] +command = "yarn build:serverless" +``` + +```bash +wrangler deploy +``` + +### Vercel Edge Functions + +```javascript +// api/validate.js +import { parsePhoneNumber, geocoder, carrier, timezones } from '../lib/serverless.esm.js'; + +export const config = { + runtime: 'edge', +}; + +export default async function handler(request) { + const { phoneNumber, countryCode } = await request.json(); + const parsed = parsePhoneNumber(phoneNumber, countryCode); + + if (!parsed || !parsed.isValid()) { + return Response.json({ error: 'Invalid phone number' }, { status: 400 }); + } + + return Response.json({ + valid: true, + country: parsed.country, + geocoder: geocoder(parsed), + carrier: carrier(parsed), + timezones: timezones(parsed) + }); +} +``` + +Deploy with Vercel CLI: + +```bash +vercel deploy +``` + +### Deno Deploy + +```typescript +// main.ts +import { parsePhoneNumber, geocoder, carrier, timezones } from './serverless.esm.js'; + +async function handler(request: Request): Promise { + if (request.method !== 'POST') { + return new Response('Method not allowed', { status: 405 }); + } + + const { phoneNumber, countryCode } = await request.json(); + const parsed = parsePhoneNumber(phoneNumber, countryCode); + + if (!parsed || !parsed.isValid()) { + return Response.json({ error: 'Invalid phone number' }, { status: 400 }); + } + + return Response.json({ + valid: true, + country: parsed.country, + geocoder: geocoder(parsed), + carrier: carrier(parsed), + timezones: timezones(parsed) + }); +} + +Deno.serve(handler); +``` + +Deploy with deployctl: + +```bash +deployctl deploy --project=phone-validator main.ts +``` + +### Netlify Edge Functions + +```javascript +// netlify/edge-functions/validate.js +import { parsePhoneNumber, geocoder, carrier, timezones } from '../../lib/serverless.esm.js'; + +export default async (request, context) => { + const { phoneNumber, countryCode } = await request.json(); + const parsed = parsePhoneNumber(phoneNumber, countryCode); + + if (!parsed || !parsed.isValid()) { + return Response.json({ error: 'Invalid phone number' }, { status: 400 }); + } + + return Response.json({ + valid: true, + country: parsed.country, + geocoder: geocoder(parsed), + carrier: carrier(parsed), + timezones: timezones(parsed) + }); +}; + +export const config = { path: "/api/validate" }; +``` + +## API Reference + +All functions from the main library are available in the serverless build: + +### Core Functions +- `parsePhoneNumber(text, defaultCountry?)` - Parse a phone number +- `parsePhoneNumberFromString(text, defaultCountry?)` - Parse with error handling +- `isValidNumber(phoneNumber)` - Check if valid +- `isPossibleNumber(phoneNumber)` - Check if possibly valid +- `getNumberType(phoneNumber)` - Get number type (MOBILE, FIXED_LINE, etc.) + +### Additional Metadata Functions +- `geocoder(phoneNumber, locale?)` - Get geographical location +- `carrier(phoneNumber, locale?)` - Get carrier information +- `timezones(phoneNumber)` - Get timezone information + +### Cache Management +- `clearCache()` - Clear internal cache +- `getCacheSize()` - Get current cache size +- `setCacheSize(size)` - Set maximum cache size + +## Resource Loaders + +The serverless version requires a resource loader to fetch phone number metadata. See `examples/serverless/resource-loaders.js` for implementations: + +### Available Loaders + +- **CloudflareKVLoader** - Uses Cloudflare KV storage +- **S3ResourceLoader** - Loads from AWS S3 +- **RemoteFetchLoader** - Fetches from CDN/HTTP endpoints +- **DenoKVLoader** - Uses Deno KV storage +- **BundledResourceLoader** - Pre-loaded resources in memory +- **CDNResourceLoader** - Optimized CDN fetching with edge caching +- **RedisResourceLoader** - Uses Redis for resource storage +- **MultiTierLoader** - Combines multiple loaders with fallback + +### Creating a Custom Loader + +```javascript +class CustomResourceLoader { + async loadResource(path) { + // Return Uint8Array of the BSON file or null if not found + const data = await fetchFromYourSource(path); + return data ? new Uint8Array(data) : null; + } + + // Optional: sync version for synchronous functions + loadResourceSync(path) { + const data = fetchFromYourSourceSync(path); + return data ? new Uint8Array(data) : null; + } +} +``` + +## Performance Considerations + +### Bundle Sizes +- ESM minified: **244KB** +- UMD minified: **244KB** +- ESM unminified: 555KB +- CommonJS: 556KB +- UMD unminified: 568KB + +### Memory Usage +- Initial load: ~2MB +- Runtime with cache: ~3-5MB depending on usage + +### Cold Start Times +- AWS Lambda: ~200-400ms +- Cloudflare Workers: ~50-100ms +- Vercel Edge: ~100-200ms +- Deno Deploy: ~50-150ms + +## Optimization Tips + +1. **Use appropriate cache size**: Default is 100 entries. Adjust based on your usage patterns. + +```javascript +import { setCacheSize } from './serverless.esm.js'; +setCacheSize(50); // Reduce memory usage +``` + +2. **Reuse parsed numbers**: Parse once and reuse the PhoneNumber object. + +```javascript +const parsed = parsePhoneNumber(input); +// Use parsed multiple times +const geo = geocoder(parsed); +const car = carrier(parsed); +const tz = timezones(parsed); +``` + +3. **Enable response caching**: Add cache headers to your responses. + +```javascript +return new Response(JSON.stringify(result), { + headers: { + 'Content-Type': 'application/json', + 'Cache-Control': 'public, max-age=3600' + } +}); +``` + +## Migration from Node.js Version + +The serverless API is identical to the Node.js version. Simply change your import: + +```javascript +// Before (Node.js only) +import { parsePhoneNumber } from '@devmehq/phone-number-validator-js'; + +// After (Serverless) +import { parsePhoneNumber } from '@devmehq/phone-number-validator-js/lib/serverless.esm.js'; +``` + +## Troubleshooting + +### Module not found errors +Ensure you've built the serverless version: +```bash +yarn build:serverless +``` + +### Large bundle size warnings +The library includes comprehensive phone metadata. Use tree-shaking and compression: +```javascript +// Only import what you need +import { parsePhoneNumber } from './serverless.esm.js'; +``` + +### Memory issues +Reduce cache size for memory-constrained environments: +```javascript +import { setCacheSize } from './serverless.esm.js'; +setCacheSize(20); +``` + +## Examples + +Complete working examples for each platform are available in the `examples/serverless/` directory: + +- `aws-lambda.js` - AWS Lambda implementation +- `cloudflare-worker.js` - Cloudflare Workers implementation +- `vercel-edge.js` - Vercel Edge Functions implementation +- `deno-deploy.ts` - Deno Deploy implementation + +## Support + +For issues or questions about serverless deployment, please open an issue on [GitHub](https://github.com/devmehq/phone-number-validator-js/issues). + +## License + +BSL 1.1 - See LICENSE file for details. \ No newline at end of file diff --git a/__tests__/comprehensive.test.ts b/__tests__/comprehensive.test.ts index 814a90d..75c1db5 100644 --- a/__tests__/comprehensive.test.ts +++ b/__tests__/comprehensive.test.ts @@ -69,7 +69,7 @@ describe('Comprehensive Phone Number Validation Tests', () => { const phoneNr = parsePhoneNumberFromString('invalid_number') const location = geocoder(phoneNr) expect(location).toBeNull() - + // Also test with undefined expect(geocoder(undefined)).toBeNull() }) @@ -142,13 +142,13 @@ describe('Comprehensive Phone Number Validation Tests', () => { describe('Cache Management', () => { it('should cache loaded data files', () => { expect(getCacheSize()).toBe(0) - + const phoneNr = parsePhoneNumberFromString('+41431234567') geocoder(phoneNr) - + const cacheSize1 = getCacheSize() expect(cacheSize1).toBeGreaterThan(0) - + // Same lookup should use cache geocoder(phoneNr) expect(getCacheSize()).toBe(cacheSize1) @@ -157,23 +157,23 @@ describe('Comprehensive Phone Number Validation Tests', () => { it('should clear cache when requested', () => { const phoneNr = parsePhoneNumberFromString('+41431234567') geocoder(phoneNr) - + expect(getCacheSize()).toBeGreaterThan(0) - + clearCache() expect(getCacheSize()).toBe(0) }) it('should handle cache for different resource types', () => { const phoneNr = parsePhoneNumberFromString('+8619912345678') - + geocoder(phoneNr) const size1 = getCacheSize() - + carrier(phoneNr) const size2 = getCacheSize() expect(size2).toBeGreaterThan(size1) - + timezones(phoneNr) const size3 = getCacheSize() expect(size3).toBeGreaterThan(size2) @@ -182,22 +182,22 @@ describe('Comprehensive Phone Number Validation Tests', () => { it('should allow cache size configuration', () => { // Set a smaller cache size setCacheSize(2) - + // Load multiple different resources const phoneNr1 = parsePhoneNumberFromString('+41431234567') const phoneNr2 = parsePhoneNumberFromString('+8619912345678') const phoneNr3 = parsePhoneNumberFromString('+49301234567') - + geocoder(phoneNr1) expect(getCacheSize()).toBe(1) - + geocoder(phoneNr2) expect(getCacheSize()).toBe(2) - + // This should evict the oldest entry geocoder(phoneNr3) expect(getCacheSize()).toBeLessThanOrEqual(2) - + // Reset to default setCacheSize(100) }) @@ -218,15 +218,13 @@ describe('Comprehensive Phone Number Validation Tests', () => { }) it('should handle numbers in different formats', () => { - const formats = [ - '+41431234567', - '0041431234567', - '+41 43 123 45 67', - '043 123 45 67', - ] - - formats.forEach(format => { - const phoneNr = parsePhoneNumberFromString(format, format.startsWith('0') && !format.startsWith('00') ? 'CH' : undefined) + const formats = ['+41431234567', '0041431234567', '+41 43 123 45 67', '043 123 45 67'] + + formats.forEach((format) => { + const phoneNr = parsePhoneNumberFromString( + format, + format.startsWith('0') && !format.startsWith('00') ? 'CH' : undefined + ) if (phoneNr && phoneNr.isValid()) { const location = geocoder(phoneNr) expect(location).toBeTruthy() @@ -235,15 +233,10 @@ describe('Comprehensive Phone Number Validation Tests', () => { }) it('should handle concurrent lookups', async () => { - const numbers = [ - '+41431234567', - '+12124567890', - '+8619912345678', - '+49301234567', - ] - + const numbers = ['+41431234567', '+12124567890', '+8619912345678', '+49301234567'] + const results = await Promise.all( - numbers.map(async num => { + numbers.map(async (num) => { const phoneNr = parsePhoneNumberFromString(num) return { geo: geocoder(phoneNr), @@ -252,8 +245,8 @@ describe('Comprehensive Phone Number Validation Tests', () => { } }) ) - - results.forEach(result => { + + results.forEach((result) => { expect(result).toBeDefined() }) }) @@ -262,11 +255,11 @@ describe('Comprehensive Phone Number Validation Tests', () => { describe('Locale Fallback Behavior', () => { it('should fallback correctly for geocoder', () => { const phoneNr = parsePhoneNumberFromString('+41431234567') - + // Test with supported locale const supportedResult = geocoder(phoneNr, 'de') expect(supportedResult).toBeTruthy() - + // Test with unsupported locale (should fallback to en) const unsupportedResult = geocoder(phoneNr, 'en') expect(unsupportedResult).toBeTruthy() @@ -274,11 +267,11 @@ describe('Comprehensive Phone Number Validation Tests', () => { it('should fallback correctly for carrier', () => { const phoneNr = parsePhoneNumberFromString('+8619912345678') - + // Test with supported locale const supportedResult = carrier(phoneNr, 'zh') expect(supportedResult).toBeTruthy() - + // Test with fallback to English const englishResult = carrier(phoneNr, 'en') expect(englishResult).toBeTruthy() @@ -289,19 +282,19 @@ describe('Comprehensive Phone Number Validation Tests', () => { it('should handle multiple lookups efficiently', () => { const startTime = Date.now() const phoneNr = parsePhoneNumberFromString('+41431234567') - + // Perform 100 lookups for (let i = 0; i < 100; i++) { geocoder(phoneNr) carrier(phoneNr) timezones(phoneNr) } - + const endTime = Date.now() const duration = endTime - startTime - + // Should complete in reasonable time (< 1 second for 300 operations) expect(duration).toBeLessThan(1000) }) }) -}) \ No newline at end of file +}) diff --git a/__tests__/index.test.ts b/__tests__/index.test.ts index 4bb7809..8b9434a 100644 --- a/__tests__/index.test.ts +++ b/__tests__/index.test.ts @@ -1,9 +1,4 @@ -import { - carrier, - geocoder, - parsePhoneNumberFromString, - timezones, -} from '../src' +import { carrier, geocoder, parsePhoneNumberFromString, timezones } from '../src' it('geocodes with default locale en', async () => { const phoneNr = parsePhoneNumberFromString('+41431234567') diff --git a/__tests__/serverless.test.ts b/__tests__/serverless.test.ts new file mode 100644 index 0000000..a10fef5 --- /dev/null +++ b/__tests__/serverless.test.ts @@ -0,0 +1,219 @@ +import { + setResourceLoader, + parsePhoneNumberWithError, + parsePhoneNumberFromString, + geocoderAsync, + carrierAsync, + timezonesAsync, + geocoder, + carrier, + timezones, + clearCache, + getCacheSize, + setCacheSize, + type ResourceLoader, +} from '../src/index.serverless' + +// Mock resource loader for testing +class MockResourceLoader implements ResourceLoader { + private resources: Map = new Map() + + constructor() { + // Add some mock data + this.addMockResource( + 'geocodes/en/1.bson', + this.createMockBsonData({ '415555': 'San Francisco, CA' }) + ) + this.addMockResource('carrier/en/1.bson', this.createMockBsonData({ '415555': 'Verizon' })) + this.addMockResource( + 'timezones.bson', + this.createMockBsonData({ '1415555': 'America/Los_Angeles' }) + ) + } + + private createMockBsonData(data: any): Uint8Array { + // Use actual BSON serialization + const { serialize } = require('bson') + return new Uint8Array(serialize(data)) + } + + addMockResource(path: string, data: Uint8Array) { + this.resources.set(path, data) + } + + async loadResource(path: string): Promise { + return this.resources.get(path) || null + } + + loadResourceSync(path: string): Uint8Array | null { + return this.resources.get(path) || null + } +} + +describe('Serverless Lite Version', () => { + let mockLoader: MockResourceLoader + let originalConsoleError: any + + beforeEach(() => { + mockLoader = new MockResourceLoader() + setResourceLoader(mockLoader) + clearCache() + // Suppress console.error for these tests since BSON errors are expected + originalConsoleError = console.error + console.error = jest.fn() + }) + + afterEach(() => { + // Restore console.error + console.error = originalConsoleError + }) + + describe('Phone Number Parsing', () => { + it('should parse valid US phone number', () => { + const parsed = parsePhoneNumberWithError('+14155552671', 'US') + expect(parsed).toBeDefined() + expect(parsed?.isValid()).toBe(true) + expect(parsed?.country).toBe('US') + expect(parsed?.countryCallingCode).toBe('1') + }) + + it('should parse international format', () => { + const parsed = parsePhoneNumberWithError('+44 20 7946 0958', 'GB') + expect(parsed).toBeDefined() + expect(parsed?.isValid()).toBe(true) + expect(parsed?.country).toBe('GB') + }) + + it('should handle invalid numbers', () => { + // parsePhoneNumber throws for completely invalid input + // Use parsePhoneNumberFromString for safer parsing + const parsed = parsePhoneNumberFromString('invalid', 'US') + expect(parsed).toBeUndefined() + }) + }) + + describe('Async Resource Loading', () => { + it('should load geocoder data asynchronously', async () => { + const parsed = parsePhoneNumberWithError('+14155552671', 'US') + const geo = await geocoderAsync(parsed) + // Mock data returns "San Francisco, CA" for prefix 415555 + expect(geo).toBe('San Francisco, CA') + }) + + it('should load carrier data asynchronously', async () => { + const parsed = parsePhoneNumberWithError('+14155552671', 'US') + const car = await carrierAsync(parsed) + // Mock data returns "Verizon" for prefix 415555 + expect(car).toBe('Verizon') + }) + + it('should load timezone data asynchronously', async () => { + const parsed = parsePhoneNumberWithError('+14155552671', 'US') + const tz = await timezonesAsync(parsed) + // Mock data returns ["America/Los_Angeles"] for prefix 1415555 + expect(tz).toEqual(['America/Los_Angeles']) + }) + }) + + describe('Sync Resource Loading', () => { + it('should load geocoder data synchronously', () => { + const parsed = parsePhoneNumberWithError('+14155552671', 'US') + const geo = geocoder(parsed) + expect(geo).toBe('San Francisco, CA') + }) + + it('should load carrier data synchronously', () => { + const parsed = parsePhoneNumberWithError('+14155552671', 'US') + const car = carrier(parsed) + expect(car).toBe('Verizon') + }) + + it('should load timezone data synchronously', () => { + const parsed = parsePhoneNumberWithError('+14155552671', 'US') + const tz = timezones(parsed) + expect(tz).toEqual(['America/Los_Angeles']) + }) + }) + + describe('Cache Management', () => { + it('should clear cache', () => { + const parsed = parsePhoneNumberWithError('+14155552671', 'US') + geocoder(parsed) + expect(getCacheSize()).toBeGreaterThanOrEqual(0) + clearCache() + expect(getCacheSize()).toBe(0) + }) + + it('should set cache size', () => { + setCacheSize(50) + // Cache size is internal implementation detail + // Just verify the function doesn't throw + expect(() => setCacheSize(100)).not.toThrow() + }) + }) + + describe('Resource Loader', () => { + it('should handle missing resources gracefully', async () => { + const parsed = parsePhoneNumberWithError('+12125551234', 'US') + const geo = await geocoderAsync(parsed) + expect(geo).toBeNull() + }) + + it('should work without resource loader', async () => { + setResourceLoader(null as any) + const parsed = parsePhoneNumberWithError('+14155552671', 'US') + const geo = await geocoderAsync(parsed) + expect(geo).toBeNull() + }) + }) + + describe('Phone Number Formatting', () => { + it('should format in various styles', () => { + const parsed = parsePhoneNumberWithError('+14155552671', 'US') + expect(parsed?.formatInternational()).toBe('+1 415 555 2671') + expect(parsed?.formatNational()).toBe('(415) 555-2671') + expect(parsed?.format('E.164')).toBe('+14155552671') + expect(parsed?.format('RFC3966')).toBe('tel:+14155552671') + }) + }) + + describe('Number Types', () => { + it('should identify number type', () => { + // Use a US mobile number for more predictable type detection + const mobile = parsePhoneNumberWithError('+14155552671', 'US') + const type = mobile?.getType() + // US numbers can be FIXED_LINE_OR_MOBILE + expect(['MOBILE', 'FIXED_LINE_OR_MOBILE']).toContain(type) + }) + }) +}) + +describe('Resource Loader Implementations', () => { + it('should support custom resource loader', () => { + class CustomLoader implements ResourceLoader { + async loadResource(path: string): Promise { + return new Uint8Array([1, 2, 3]) + } + } + + const loader = new CustomLoader() + setResourceLoader(loader) + expect(() => setResourceLoader(loader)).not.toThrow() + }) + + it('should support sync and async loaders', () => { + class DualLoader implements ResourceLoader { + async loadResource(path: string): Promise { + return new Uint8Array([1, 2, 3]) + } + + loadResourceSync(path: string): Uint8Array | null { + return new Uint8Array([1, 2, 3]) + } + } + + const loader = new DualLoader() + setResourceLoader(loader) + expect(() => setResourceLoader(loader)).not.toThrow() + }) +}) diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..2e14c59 --- /dev/null +++ b/biome.json @@ -0,0 +1,89 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.2.2/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "ignoreUnknown": false, + "includes": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx", "**/*.json"], + "experimentalScannerIgnores": [ + "node_modules", + "lib", + "dist", + "build", + "coverage", + "*.min.js", + "resources" + ] + }, + "formatter": { + "enabled": true, + "formatWithErrors": false, + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 100, + "lineEnding": "lf", + "attributePosition": "auto" + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "complexity": { + "noBannedTypes": "error", + "noUselessConstructor": "error", + "noUselessTypeConstraint": "error" + }, + "correctness": { + "noConstAssign": "error", + "noConstantCondition": "error", + "noUndeclaredVariables": "error", + "noUnreachable": "error", + "noUnusedVariables": "warn" + }, + "style": { + "noNonNullAssertion": "warn", + "useConst": "error", + "useTemplate": "warn" + }, + "suspicious": { + "noAsyncPromiseExecutor": "error", + "noCatchAssign": "error", + "noCommentText": "warn", + "noDuplicateCase": "error", + "noDuplicateObjectKeys": "error", + "noExplicitAny": "warn", + "noFallthroughSwitchClause": "error" + } + } + }, + "javascript": { + "formatter": { + "quoteStyle": "single", + "trailingCommas": "es5", + "semicolons": "asNeeded", + "arrowParentheses": "always", + "bracketSpacing": true, + "bracketSameLine": false + }, + "globals": [ + "jest", + "it", + "describe", + "expect", + "beforeEach", + "afterEach", + "Deno" + ] + }, + "json": { + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 80 + } + } +} diff --git a/examples/serverless/aws-lambda.js b/examples/serverless/aws-lambda.js new file mode 100644 index 0000000..ecd2012 --- /dev/null +++ b/examples/serverless/aws-lambda.js @@ -0,0 +1,79 @@ +// AWS Lambda Function Example +// Deploy this as a Lambda function with Node.js 18+ runtime + +import { parsePhoneNumber, geocoder, carrier, timezones } from '../../lib/serverless.esm.js' + +export const handler = async (event) => { + try { + const { phoneNumber, countryCode } = JSON.parse(event.body || '{}') + + if (!phoneNumber) { + return { + statusCode: 400, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ error: 'Phone number is required' }), + } + } + + const parsed = parsePhoneNumber(phoneNumber, countryCode) + + if (!parsed || !parsed.isValid()) { + return { + statusCode: 400, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ error: 'Invalid phone number' }), + } + } + + const result = { + valid: true, + number: { + international: parsed.formatInternational(), + national: parsed.formatNational(), + e164: parsed.format('E.164'), + rfc3966: parsed.format('RFC3966'), + }, + country: parsed.country, + countryCallingCode: parsed.countryCallingCode, + nationalNumber: parsed.nationalNumber, + type: parsed.getType(), + geocoder: geocoder(parsed), + carrier: carrier(parsed), + timezones: timezones(parsed), + } + + return { + statusCode: 200, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(result), + } + } catch (error) { + return { + statusCode: 500, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ error: 'Internal server error', message: error.message }), + } + } +} + +// Example deployment with AWS SAM template.yaml: +/* +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 + +Resources: + PhoneValidatorFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: . + Handler: aws-lambda.handler + Runtime: nodejs18.x + MemorySize: 256 + Timeout: 10 + Events: + Api: + Type: Api + Properties: + Path: /validate + Method: POST +*/ diff --git a/examples/serverless/browser.html b/examples/serverless/browser.html new file mode 100644 index 0000000..0b11023 --- /dev/null +++ b/examples/serverless/browser.html @@ -0,0 +1,215 @@ + + + + + + Phone Number Validator - Browser Example + + + +
+

Phone Number Validator

+ +
+ + +
+ +
+ + +
+ + + +
+
+ + + + + + + \ No newline at end of file diff --git a/examples/serverless/cloudflare-worker.js b/examples/serverless/cloudflare-worker.js new file mode 100644 index 0000000..cce4efa --- /dev/null +++ b/examples/serverless/cloudflare-worker.js @@ -0,0 +1,100 @@ +// Cloudflare Worker Example +// Deploy this using Wrangler CLI or Cloudflare Dashboard + +import { parsePhoneNumber, geocoder, carrier, timezones } from '../../lib/serverless.esm.js' + +export default { + async fetch(request, env, ctx) { + // Handle CORS + if (request.method === 'OPTIONS') { + return new Response(null, { + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'POST, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type', + }, + }) + } + + if (request.method !== 'POST') { + return new Response(JSON.stringify({ error: 'Method not allowed' }), { + status: 405, + headers: { 'Content-Type': 'application/json' }, + }) + } + + try { + const { phoneNumber, countryCode } = await request.json() + + if (!phoneNumber) { + return new Response(JSON.stringify({ error: 'Phone number is required' }), { + status: 400, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }, + }) + } + + const parsed = parsePhoneNumber(phoneNumber, countryCode) + + if (!parsed || !parsed.isValid()) { + return new Response(JSON.stringify({ error: 'Invalid phone number' }), { + status: 400, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }, + }) + } + + const result = { + valid: true, + number: { + international: parsed.formatInternational(), + national: parsed.formatNational(), + e164: parsed.format('E.164'), + rfc3966: parsed.format('RFC3966'), + }, + country: parsed.country, + countryCallingCode: parsed.countryCallingCode, + nationalNumber: parsed.nationalNumber, + type: parsed.getType(), + geocoder: geocoder(parsed), + carrier: carrier(parsed), + timezones: timezones(parsed), + } + + return new Response(JSON.stringify(result), { + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + 'Cache-Control': 'public, max-age=3600', + }, + }) + } catch (error) { + return new Response( + JSON.stringify({ error: 'Internal server error', message: error.message }), + { + status: 500, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }, + } + ) + } + }, +} + +// Example wrangler.toml configuration: +/* +name = "phone-validator" +main = "cloudflare-worker.js" +compatibility_date = "2023-05-18" + +[env.production] +routes = [ + { pattern = "api.example.com/phone-validate", zone_id = "YOUR_ZONE_ID" } +] +*/ diff --git a/examples/serverless/deno-deploy.ts b/examples/serverless/deno-deploy.ts new file mode 100644 index 0000000..a536d71 --- /dev/null +++ b/examples/serverless/deno-deploy.ts @@ -0,0 +1,98 @@ +// Deno Deploy Example +// Deploy this using deployctl or Deno Deploy dashboard + +import { parsePhoneNumber, geocoder, carrier, timezones } from '../../lib/serverless.esm.js' + +async function handleRequest(request: Request): Promise { + // Handle CORS preflight + if (request.method === 'OPTIONS') { + return new Response(null, { + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'POST, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type', + }, + }) + } + + if (request.method !== 'POST') { + return new Response(JSON.stringify({ error: 'Method not allowed' }), { + status: 405, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }, + }) + } + + try { + const { phoneNumber, countryCode } = await request.json() + + if (!phoneNumber) { + return new Response(JSON.stringify({ error: 'Phone number is required' }), { + status: 400, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }, + }) + } + + const parsed = parsePhoneNumber(phoneNumber, countryCode) + + if (!parsed || !parsed.isValid()) { + return new Response(JSON.stringify({ error: 'Invalid phone number' }), { + status: 400, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }, + }) + } + + const result = { + valid: true, + number: { + international: parsed.formatInternational(), + national: parsed.formatNational(), + e164: parsed.format('E.164'), + rfc3966: parsed.format('RFC3966'), + }, + country: parsed.country, + countryCallingCode: parsed.countryCallingCode, + nationalNumber: parsed.nationalNumber, + type: parsed.getType(), + geocoder: geocoder(parsed), + carrier: carrier(parsed), + timezones: timezones(parsed), + } + + return new Response(JSON.stringify(result), { + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + 'Cache-Control': 'public, max-age=3600', + }, + }) + } catch (error) { + return new Response( + JSON.stringify({ error: 'Internal server error', message: error.message }), + { + status: 500, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }, + } + ) + } +} + +// Start the server +Deno.serve(handleRequest) + +// Deploy command: +// deployctl deploy --project=phone-validator deno-deploy.ts + +// Alternative: Use with Deno locally +// deno run --allow-net deno-deploy.ts diff --git a/examples/serverless/resource-loaders.js b/examples/serverless/resource-loaders.js new file mode 100644 index 0000000..7d65fc0 --- /dev/null +++ b/examples/serverless/resource-loaders.js @@ -0,0 +1,296 @@ +// Resource loader implementations for different serverless platforms + +// ============================================ +// Cloudflare Workers with KV Storage +// ============================================ +export class CloudflareKVLoader { + constructor(namespace) { + this.namespace = namespace // Your KV namespace binding + } + + async loadResource(path) { + const key = `phone-validator:${path}` + const data = await this.namespace.get(key, 'arrayBuffer') + return data ? new Uint8Array(data) : null + } +} + +// Usage in Cloudflare Worker: +/* +import { setResourceLoader, parsePhoneNumber, geocoderAsync } from './serverless.esm.js'; + +export default { + async fetch(request, env) { + setResourceLoader(new CloudflareKVLoader(env.PHONE_DATA)); + + const { phoneNumber } = await request.json(); + const parsed = parsePhoneNumber(phoneNumber); + const geo = await geocoderAsync(parsed); + + return Response.json({ geo }); + } +} +*/ + +// ============================================ +// AWS Lambda with S3 +// ============================================ +export class S3ResourceLoader { + constructor(s3Client, bucketName, prefix = 'phone-validator/') { + this.s3 = s3Client + this.bucket = bucketName + this.prefix = prefix + } + + async loadResource(path) { + try { + const response = await this.s3 + .getObject({ + Bucket: this.bucket, + Key: this.prefix + path, + }) + .promise() + + return new Uint8Array(response.Body) + } catch (error) { + if (error.code === 'NoSuchKey') { + return null + } + throw error + } + } +} + +// Usage in Lambda: +/* +import AWS from 'aws-sdk'; +import { setResourceLoader, parsePhoneNumber, geocoderAsync } from './serverless.esm.js'; + +const s3 = new AWS.S3(); +const loader = new S3ResourceLoader(s3, 'my-bucket'); + +export const handler = async (event) => { + setResourceLoader(loader); + + const { phoneNumber } = JSON.parse(event.body); + const parsed = parsePhoneNumber(phoneNumber); + const geo = await geocoderAsync(parsed); + + return { + statusCode: 200, + body: JSON.stringify({ geo }) + }; +}; +*/ + +// ============================================ +// Vercel Edge with Remote Fetch +// ============================================ +export class RemoteFetchLoader { + constructor(baseUrl, cacheTime = 3600) { + this.baseUrl = baseUrl + this.cacheTime = cacheTime + this.cache = new Map() + } + + async loadResource(path) { + // Check cache first + const cached = this.cache.get(path) + if (cached && cached.expires > Date.now()) { + return cached.data + } + + try { + const response = await fetch(`${this.baseUrl}/${path}`) + if (!response.ok) { + return null + } + + const buffer = await response.arrayBuffer() + const data = new Uint8Array(buffer) + + // Cache the result + this.cache.set(path, { + data, + expires: Date.now() + this.cacheTime * 1000, + }) + + return data + } catch (error) { + console.error(`Failed to fetch resource: ${path}`, error) + return null + } + } +} + +// Usage in Vercel Edge Function: +/* +import { setResourceLoader, parsePhoneNumber, geocoderAsync } from './serverless.esm.js'; + +const loader = new RemoteFetchLoader('https://cdn.example.com/phone-data'); + +export default async function handler(request) { + setResourceLoader(loader); + + const { phoneNumber } = await request.json(); + const parsed = parsePhoneNumber(phoneNumber); + const geo = await geocoderAsync(parsed); + + return Response.json({ geo }); +} +*/ + +// ============================================ +// Deno Deploy with Deno KV +// ============================================ +export class DenoKVLoader { + constructor(kv) { + this.kv = kv // Deno.openKv() instance + } + + async loadResource(path) { + const key = ['phone-validator', path] + const entry = await this.kv.get(key) + return entry.value ? new Uint8Array(entry.value) : null + } +} + +// Usage in Deno Deploy: +/* +import { setResourceLoader, parsePhoneNumber, geocoderAsync } from './serverless.esm.js'; + +const kv = await Deno.openKv(); +const loader = new DenoKVLoader(kv); + +Deno.serve(async (request) => { + setResourceLoader(loader); + + const { phoneNumber } = await request.json(); + const parsed = parsePhoneNumber(phoneNumber); + const geo = await geocoderAsync(parsed); + + return Response.json({ geo }); +}); +*/ + +// ============================================ +// Bundled Resources Loader (Pre-loaded) +// ============================================ +export class BundledResourceLoader { + constructor(resources) { + this.resources = resources // Map or object of path -> Uint8Array + } + + async loadResource(path) { + return this.resources.get ? this.resources.get(path) : this.resources[path] || null + } + + loadResourceSync(path) { + return this.resources.get ? this.resources.get(path) : this.resources[path] || null + } +} + +// Usage with pre-bundled resources: +/* +import { setResourceLoader, parsePhoneNumber, geocoder } from './serverless.esm.js'; +import resources from './bundled-resources.js'; + +const loader = new BundledResourceLoader(resources); +setResourceLoader(loader); + +// Now you can use sync functions +const parsed = parsePhoneNumber('+14155552671'); +const geo = geocoder(parsed); // Sync call +*/ + +// ============================================ +// CDN with Edge Caching +// ============================================ +export class CDNResourceLoader { + constructor(cdnUrl, options = {}) { + this.cdnUrl = cdnUrl + this.headers = options.headers || {} + this.timeout = options.timeout || 5000 + } + + async loadResource(path) { + const controller = new AbortController() + const timeoutId = setTimeout(() => controller.abort(), this.timeout) + + try { + const response = await fetch(`${this.cdnUrl}/${path}`, { + headers: this.headers, + signal: controller.signal, + cf: { + // Cloudflare specific caching + cacheTtl: 3600, + cacheEverything: true, + }, + }) + + clearTimeout(timeoutId) + + if (!response.ok) { + return null + } + + const buffer = await response.arrayBuffer() + return new Uint8Array(buffer) + } catch (error) { + clearTimeout(timeoutId) + if (error.name === 'AbortError') { + console.error(`Timeout loading resource: ${path}`) + } + return null + } + } +} + +// ============================================ +// Redis Resource Loader +// ============================================ +export class RedisResourceLoader { + constructor(redisClient, keyPrefix = 'phone:') { + this.redis = redisClient + this.keyPrefix = keyPrefix + } + + async loadResource(path) { + const key = this.keyPrefix + path + const data = await this.redis.getBuffer(key) + return data ? new Uint8Array(data) : null + } +} + +// ============================================ +// Multi-tier Caching Loader +// ============================================ +export class MultiTierLoader { + constructor(loaders) { + this.loaders = loaders // Array of loaders in priority order + } + + async loadResource(path) { + for (const loader of this.loaders) { + try { + const data = await loader.loadResource(path) + if (data) { + // Optionally populate higher-tier caches + return data + } + } catch (error) { + console.error(`Loader failed for ${path}:`, error) + } + } + return null + } +} + +// Usage with multiple fallback sources: +/* +const loader = new MultiTierLoader([ + new BundledResourceLoader(cachedResources), // L1: Memory + new RedisResourceLoader(redis), // L2: Redis + new CDNResourceLoader('https://cdn.example.com/phone-data') // L3: CDN +]); +*/ diff --git a/examples/serverless/vercel-edge.js b/examples/serverless/vercel-edge.js new file mode 100644 index 0000000..72ee9b4 --- /dev/null +++ b/examples/serverless/vercel-edge.js @@ -0,0 +1,112 @@ +// Vercel Edge Function Example +// Deploy this to api/validate.js in your Vercel project + +import { parsePhoneNumber, geocoder, carrier, timezones } from '../../lib/serverless.esm.js' + +export const config = { + runtime: 'edge', +} + +export default async function handler(request) { + // Handle CORS preflight + if (request.method === 'OPTIONS') { + return new Response(null, { + status: 200, + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'POST, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type', + }, + }) + } + + if (request.method !== 'POST') { + return new Response(JSON.stringify({ error: 'Method not allowed' }), { + status: 405, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }, + }) + } + + try { + const { phoneNumber, countryCode } = await request.json() + + if (!phoneNumber) { + return new Response(JSON.stringify({ error: 'Phone number is required' }), { + status: 400, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }, + }) + } + + const parsed = parsePhoneNumber(phoneNumber, countryCode) + + if (!parsed || !parsed.isValid()) { + return new Response(JSON.stringify({ error: 'Invalid phone number' }), { + status: 400, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }, + }) + } + + const result = { + valid: true, + number: { + international: parsed.formatInternational(), + national: parsed.formatNational(), + e164: parsed.format('E.164'), + rfc3966: parsed.format('RFC3966'), + }, + country: parsed.country, + countryCallingCode: parsed.countryCallingCode, + nationalNumber: parsed.nationalNumber, + type: parsed.getType(), + geocoder: geocoder(parsed), + carrier: carrier(parsed), + timezones: timezones(parsed), + } + + return new Response(JSON.stringify(result), { + status: 200, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + 'Cache-Control': 'public, s-maxage=3600', + }, + }) + } catch (error) { + return new Response( + JSON.stringify({ error: 'Internal server error', message: error.message }), + { + status: 500, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }, + } + ) + } +} + +// Example vercel.json configuration: +/* +{ + "functions": { + "api/validate.js": { + "maxDuration": 10 + } + }, + "rewrites": [ + { + "source": "/api/validate", + "destination": "/api/validate.js" + } + ] +} +*/ diff --git a/package.json b/package.json index b8bbf6a..3f7a44d 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,18 @@ }, "license": "BSL 1.1", "author": "DEV.ME (https://dev.me)", + "exports": { + ".": { + "import": "./lib/index.es.js", + "require": "./lib/index.js", + "types": "./lib/index.d.ts" + }, + "./serverless": { + "import": "./lib/serverless.esm.js", + "require": "./lib/serverless.cjs.js", + "types": "./lib/index.serverless.d.ts" + } + }, "main": "lib/index.js", "module": "lib/index.es.js", "types": "lib/index.d.ts", @@ -33,12 +45,19 @@ "resources" ], "scripts": { - "build": "rm -rf lib && rollup -c --bundleConfigAsCjs", + "build": "yarn build:main && yarn build:serverless", + "build:main": "rm -rf lib && rollup -c rollup.config.cjs && yarn build:types", + "build:serverless": "rollup -c rollup.config.serverless.cjs", + "build:types": "tsc --declaration --emitDeclarationOnly --outDir lib --declarationMap", + "check": "biome check .", + "format": "biome format --write .", + "lint": "biome lint .", + "lint:fix": "biome lint --write .", "preparemetadata": "rm -rf resources && node .scripts/prepare.js && rm -rf resources/libphonenumber", - "prepublishOnly": "yarn build", - "prettier": "prettier --write \\\"src/**/*.ts\\\" \\\"__tests__/**/*.ts\\\"", + "prepublishOnly": "yarn build:all", "test": "jest", - "watch": "rm -rf lib && rollup -cw" + "watch": "rm -rf lib && rollup -cw rollup.config.cjs", + "watch:serverless": "rollup -cw rollup.config.serverless.cjs" }, "dependencies": { "bson": "^6.10.4", @@ -46,14 +65,20 @@ "tiny-lru": "^11.4.5" }, "devDependencies": { + "@biomejs/biome": "^2.2.2", + "@rollup/plugin-commonjs": "^28.0.6", + "@rollup/plugin-json": "^6.1.0", + "@rollup/plugin-node-resolve": "^16.0.1", + "@rollup/plugin-terser": "^0.4.4", "@types/bson": "^4.2.4", "@types/jest": "^30.0.0", "@types/node": "^24.3.0", "@types/shelljs": "^0.8.17", + "esbuild": "^0.25.9", "jest": "^30.1.1", "prettier": "^3.6.2", "rollup": "^4.49.0", - "rollup-plugin-typescript2": "^0.36.0", + "rollup-plugin-esbuild": "^6.2.1", "shelljs": "^0.10.0", "ts-jest": "^29.4.1", "tslib": "^2.8.1", diff --git a/release.config.js b/release.config.js index fbba153..49e0eb7 100644 --- a/release.config.js +++ b/release.config.js @@ -70,8 +70,7 @@ module.exports = { 'npm-shrinkwrap.json', 'CHANGELOG.md', ], - message: - 'chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}', + message: 'chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}', GIT_AUTHOR_NAME: pkg.author.name, GIT_AUTHOR_EMAIL: pkg.author.email, GIT_COMMITTER_NAME: pkg.author.name, diff --git a/rollup.config.cjs b/rollup.config.cjs new file mode 100644 index 0000000..d923ca7 --- /dev/null +++ b/rollup.config.cjs @@ -0,0 +1,32 @@ +const esbuild = require('rollup-plugin-esbuild').default +const { readFileSync } = require('fs') + +const pkg = JSON.parse(readFileSync('./package.json', 'utf-8')) + +module.exports = { + input: 'src/index.ts', + output: [ + { + file: pkg.main, + format: 'cjs', + }, + { + file: pkg.module, + format: 'es', + }, + ], + external: [...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.peerDependencies || {})], + plugins: [ + esbuild({ + include: /\.ts$/, + exclude: /node_modules/, + sourceMap: false, + minify: false, + target: 'es2015', + tsconfig: './tsconfig.json', + loaders: { + '.ts': 'ts', + }, + }), + ], +} \ No newline at end of file diff --git a/rollup.config.js b/rollup.config.js deleted file mode 100644 index 23d6b69..0000000 --- a/rollup.config.js +++ /dev/null @@ -1,21 +0,0 @@ -import typescript from "rollup-plugin-typescript2"; -import pkg from "./package.json"; - -export default { - input: "src/index.ts", - output: [ - { - file: pkg.main, - format: "cjs" - }, - { - file: pkg.module, - format: "es" - } - ], - external: [ - ...Object.keys(pkg.dependencies || {}), - ...Object.keys(pkg.peerDependencies || {}) - ], - plugins: [typescript()] -}; diff --git a/rollup.config.serverless.cjs b/rollup.config.serverless.cjs new file mode 100644 index 0000000..e89e265 --- /dev/null +++ b/rollup.config.serverless.cjs @@ -0,0 +1,96 @@ +const esbuild = require('rollup-plugin-esbuild').default +const resolve = require('@rollup/plugin-node-resolve') +const commonjs = require('@rollup/plugin-commonjs') +const json = require('@rollup/plugin-json') +const terser = require('@rollup/plugin-terser') + +const plugins = [ + resolve({ + preferBuiltins: false, + browser: true, + }), + commonjs(), + json(), + esbuild({ + include: /\.ts$/, + exclude: /node_modules/, + sourceMap: false, + minify: false, + target: 'es2017', + tsconfig: './tsconfig.json', + loaders: { + '.ts': 'ts', + }, + }), +] + +module.exports = [ + // ESM build + { + input: 'src/index.serverless.ts', + output: { + file: 'lib/serverless.esm.js', + format: 'es', + }, + plugins, + }, + // ESM minified + { + input: 'src/index.serverless.ts', + output: { + file: 'lib/serverless.esm.min.js', + format: 'es', + }, + plugins: [ + ...plugins, + terser({ + compress: { + drop_console: true, + passes: 2, + }, + mangle: true, + }), + ], + }, + // CommonJS build + { + input: 'src/index.serverless.ts', + output: { + file: 'lib/serverless.cjs.js', + format: 'cjs', + exports: 'named', + }, + plugins, + }, + // UMD build for browsers + { + input: 'src/index.serverless.ts', + output: { + file: 'lib/serverless.umd.js', + format: 'umd', + name: 'PhoneNumberValidator', + exports: 'named', + }, + plugins, + }, + // UMD minified + { + input: 'src/index.serverless.ts', + output: { + file: 'lib/serverless.umd.min.js', + format: 'umd', + name: 'PhoneNumberValidator', + exports: 'named', + }, + plugins: [ + ...plugins, + terser({ + compress: { + drop_console: true, + passes: 2, + }, + mangle: true, + }), + ], + }, +] \ No newline at end of file diff --git a/scripts/deploy-resources.js b/scripts/deploy-resources.js new file mode 100644 index 0000000..e566575 --- /dev/null +++ b/scripts/deploy-resources.js @@ -0,0 +1,243 @@ +#!/usr/bin/env node + +/** + * Deploy phone number resources to various cloud platforms + * Usage: node deploy-resources.js [platform] [options] + */ + +const fs = require('fs') +const path = require('path') +const { promisify } = require('util') +const readdir = promisify(fs.readdir) +const stat = promisify(fs.stat) +const readFile = promisify(fs.readFile) + +const RESOURCES_DIR = path.join(__dirname, '..', 'resources') + +async function getAllResourceFiles(dir = RESOURCES_DIR, basePath = '') { + const files = [] + const items = await readdir(dir) + + for (const item of items) { + const fullPath = path.join(dir, item) + const itemStat = await stat(fullPath) + + if (itemStat.isDirectory()) { + const subFiles = await getAllResourceFiles(fullPath, path.join(basePath, item)) + files.push(...subFiles) + } else if (path.extname(item) === '.bson') { + files.push({ + path: path.join(basePath, item).replace(/\\/g, '/'), + fullPath, + }) + } + } + + return files +} + +// Deploy to Cloudflare KV +async function deployToCloudflareKV(namespace, apiToken, accountId) { + console.log('Deploying to Cloudflare KV...') + const files = await getAllResourceFiles() + + for (const file of files) { + const key = `phone-validator:${file.path}` + const data = await readFile(file.fullPath) + + const response = await fetch( + `https://api.cloudflare.com/client/v4/accounts/${accountId}/storage/kv/namespaces/${namespace}/values/${key}`, + { + method: 'PUT', + headers: { + Authorization: `Bearer ${apiToken}`, + 'Content-Type': 'application/octet-stream', + }, + body: data, + } + ) + + if (response.ok) { + console.log(`✓ Uploaded ${file.path}`) + } else { + console.error(`✗ Failed to upload ${file.path}:`, await response.text()) + } + } + + console.log('Cloudflare KV deployment complete!') +} + +// Deploy to AWS S3 +async function deployToS3(bucketName, region = 'us-east-1') { + console.log('Deploying to AWS S3...') + const AWS = require('aws-sdk') + const s3 = new AWS.S3({ region }) + + const files = await getAllResourceFiles() + + for (const file of files) { + const key = `phone-validator/${file.path}` + const data = await readFile(file.fullPath) + + try { + await s3 + .putObject({ + Bucket: bucketName, + Key: key, + Body: data, + ContentType: 'application/octet-stream', + CacheControl: 'public, max-age=31536000', + }) + .promise() + + console.log(`✓ Uploaded ${file.path}`) + } catch (error) { + console.error(`✗ Failed to upload ${file.path}:`, error.message) + } + } + + console.log('S3 deployment complete!') +} + +// Deploy to CDN (using a generic HTTP PUT endpoint) +async function deployToCDN(endpoint, authHeader) { + console.log('Deploying to CDN...') + const files = await getAllResourceFiles() + + for (const file of files) { + const data = await readFile(file.fullPath) + const url = `${endpoint}/${file.path}` + + try { + const response = await fetch(url, { + method: 'PUT', + headers: { + Authorization: authHeader, + 'Content-Type': 'application/octet-stream', + }, + body: data, + }) + + if (response.ok) { + console.log(`✓ Uploaded ${file.path}`) + } else { + console.error(`✗ Failed to upload ${file.path}:`, await response.text()) + } + } catch (error) { + console.error(`✗ Failed to upload ${file.path}:`, error.message) + } + } + + console.log('CDN deployment complete!') +} + +// Generate a JSON manifest of all resources +async function generateManifest(outputFile = 'resources-manifest.json') { + console.log('Generating resources manifest...') + const files = await getAllResourceFiles() + + const manifest = { + version: new Date().toISOString(), + files: files.map((f) => ({ + path: f.path, + size: fs.statSync(f.fullPath).size, + })), + totalFiles: files.length, + totalSize: files.reduce((sum, f) => sum + fs.statSync(f.fullPath).size, 0), + } + + fs.writeFileSync(outputFile, JSON.stringify(manifest, null, 2)) + console.log(`Manifest generated: ${outputFile}`) + console.log(`Total files: ${manifest.totalFiles}`) + console.log(`Total size: ${(manifest.totalSize / 1024 / 1024).toFixed(2)} MB`) +} + +// Main CLI +async function main() { + const args = process.argv.slice(2) + const command = args[0] + + switch (command) { + case 'cloudflare': { + const namespace = process.env.CF_KV_NAMESPACE || args[1] + const apiToken = process.env.CF_API_TOKEN || args[2] + const accountId = process.env.CF_ACCOUNT_ID || args[3] + + if (!namespace || !apiToken || !accountId) { + console.error('Usage: deploy-resources cloudflare [namespace] [api-token] [account-id]') + console.error('Or set environment variables: CF_KV_NAMESPACE, CF_API_TOKEN, CF_ACCOUNT_ID') + process.exit(1) + } + + await deployToCloudflareKV(namespace, apiToken, accountId) + break + } + + case 's3': { + const bucketName = process.env.AWS_S3_BUCKET || args[1] + const region = process.env.AWS_REGION || args[2] || 'us-east-1' + + if (!bucketName) { + console.error('Usage: deploy-resources s3 [bucket-name] [region]') + console.error('Or set environment variables: AWS_S3_BUCKET, AWS_REGION') + console.error('Make sure AWS credentials are configured') + process.exit(1) + } + + await deployToS3(bucketName, region) + break + } + + case 'cdn': { + const endpoint = process.env.CDN_ENDPOINT || args[1] + const authHeader = process.env.CDN_AUTH || args[2] + + if (!endpoint || !authHeader) { + console.error('Usage: deploy-resources cdn [endpoint] [auth-header]') + console.error('Or set environment variables: CDN_ENDPOINT, CDN_AUTH') + process.exit(1) + } + + await deployToCDN(endpoint, authHeader) + break + } + + case 'manifest': { + const outputFile = args[1] || 'resources-manifest.json' + await generateManifest(outputFile) + break + } + + default: + console.log('Phone Number Validator - Resource Deployment Tool') + console.log('') + console.log('Usage: node deploy-resources.js [command] [options]') + console.log('') + console.log('Commands:') + console.log(' cloudflare [namespace] [api-token] [account-id] - Deploy to Cloudflare KV') + console.log(' s3 [bucket] [region] - Deploy to AWS S3') + console.log(' cdn [endpoint] [auth] - Deploy to CDN endpoint') + console.log(' manifest [output-file] - Generate resource manifest') + console.log('') + console.log('Environment variables:') + console.log(' Cloudflare: CF_KV_NAMESPACE, CF_API_TOKEN, CF_ACCOUNT_ID') + console.log(' AWS S3: AWS_S3_BUCKET, AWS_REGION (+ AWS credentials)') + console.log(' CDN: CDN_ENDPOINT, CDN_AUTH') + break + } +} + +if (require.main === module) { + main().catch((error) => { + console.error('Error:', error) + process.exit(1) + }) +} + +module.exports = { + getAllResourceFiles, + deployToCloudflareKV, + deployToS3, + deployToCDN, + generateManifest, +} diff --git a/src/index.serverless.ts b/src/index.serverless.ts new file mode 100644 index 0000000..ae56d5f --- /dev/null +++ b/src/index.serverless.ts @@ -0,0 +1,244 @@ +// Lightweight serverless version - requires resource loading at runtime +export * from 'libphonenumber-js' +import type { PhoneNumber } from 'libphonenumber-js' +import type { CarrierLocale, GeocoderLocale } from './locales' +import { deserialize, type Document } from 'bson' +import { lru, type LRU } from 'tiny-lru' + +const DEFAULT_CACHE_SIZE = 100 +let codeDataCache: LRU = lru(DEFAULT_CACHE_SIZE) + +// Resource loader interface - platforms must implement this +export interface ResourceLoader { + loadResource(path: string): Promise + loadResourceSync?(path: string): Uint8Array | null +} + +let resourceLoader: ResourceLoader | null = null + +export function setResourceLoader(loader: ResourceLoader) { + resourceLoader = loader +} + +async function getCodeAsync(dataPath: string, nationalNumber: string): Promise { + if (!dataPath || !nationalNumber || !resourceLoader) { + return null + } + + try { + let data = codeDataCache.get(dataPath) + + if (!data) { + const bData = await resourceLoader.loadResource(dataPath) + if (!bData) { + return null + } + data = deserialize(Buffer.from(bData)) + codeDataCache.set(dataPath, data) + } + + let prefix = nationalNumber + while (prefix.length > 0) { + const description = data[prefix] + if (description) { + return description as string + } + prefix = prefix.substring(0, prefix.length - 1) + } + } catch (err) { + console.error(`Error loading data from ${dataPath}:`, err) + } + return null +} + +function getCodeSync(dataPath: string, nationalNumber: string): string | null { + if (!dataPath || !nationalNumber || !resourceLoader || !resourceLoader.loadResourceSync) { + return null + } + + try { + let data = codeDataCache.get(dataPath) + + if (!data) { + const bData = resourceLoader.loadResourceSync(dataPath) + if (!bData) { + return null + } + data = deserialize(Buffer.from(bData)) + codeDataCache.set(dataPath, data) + } + + let prefix = nationalNumber + while (prefix.length > 0) { + const description = data[prefix] + if (description) { + return description as string + } + prefix = prefix.substring(0, prefix.length - 1) + } + } catch (err) { + console.error(`Error loading data from ${dataPath}:`, err) + } + return null +} + +async function getLocalizedDataAsync( + resourceType: 'geocodes' | 'carrier', + phonenumber: PhoneNumber | undefined, + locale: string, + fallbackLocale: string = 'en' +): Promise { + if (!phonenumber) { + return null + } + + const nationalNumber = phonenumber.nationalNumber?.toString() + const countryCallingCode = phonenumber.countryCallingCode?.toString() + + if (!nationalNumber || !countryCallingCode) { + return null + } + + let dataPath = `${resourceType}/${locale}/${countryCallingCode}.bson` + + const code = await getCodeAsync(dataPath, nationalNumber) + if (code) { + return code + } + + if (locale !== fallbackLocale) { + dataPath = `${resourceType}/${fallbackLocale}/${countryCallingCode}.bson` + return getCodeAsync(dataPath, nationalNumber) + } + + return null +} + +function getLocalizedDataSync( + resourceType: 'geocodes' | 'carrier', + phonenumber: PhoneNumber | undefined, + locale: string, + fallbackLocale: string = 'en' +): string | null { + if (!phonenumber) { + return null + } + + const nationalNumber = phonenumber.nationalNumber?.toString() + const countryCallingCode = phonenumber.countryCallingCode?.toString() + + if (!nationalNumber || !countryCallingCode) { + return null + } + + let dataPath = `${resourceType}/${locale}/${countryCallingCode}.bson` + + const code = getCodeSync(dataPath, nationalNumber) + if (code) { + return code + } + + if (locale !== fallbackLocale) { + dataPath = `${resourceType}/${fallbackLocale}/${countryCallingCode}.bson` + return getCodeSync(dataPath, nationalNumber) + } + + return null +} + +// Async versions +export async function geocoderAsync( + phonenumber: PhoneNumber | undefined, + locale: GeocoderLocale = 'en' +): Promise { + return getLocalizedDataAsync('geocodes', phonenumber, locale, 'en') +} + +export async function carrierAsync( + phonenumber: PhoneNumber | undefined, + locale: CarrierLocale = 'en' +): Promise { + return getLocalizedDataAsync('carrier', phonenumber, locale, 'en') +} + +export async function timezonesAsync( + phonenumber: PhoneNumber | undefined +): Promise { + if (!phonenumber || !phonenumber.number) { + return null + } + + let nr = phonenumber.number.toString() + if (!nr) { + return null + } + + nr = nr.replace(/^\+/, '') + const dataPath = 'timezones.bson' + const zones = await getCodeAsync(dataPath, nr) + + if (typeof zones === 'string' && zones.length > 0) { + return zones.split('&').filter((zone) => zone.length > 0) + } + + return null +} + +// Sync versions (requires sync resource loader) +export function geocoder( + phonenumber: PhoneNumber | undefined, + locale: GeocoderLocale = 'en' +): string | null { + return getLocalizedDataSync('geocodes', phonenumber, locale, 'en') +} + +export function carrier( + phonenumber: PhoneNumber | undefined, + locale: CarrierLocale = 'en' +): string | null { + return getLocalizedDataSync('carrier', phonenumber, locale, 'en') +} + +export function timezones(phonenumber: PhoneNumber | undefined): string[] | null { + if (!phonenumber || !phonenumber.number) { + return null + } + + let nr = phonenumber.number.toString() + if (!nr) { + return null + } + + nr = nr.replace(/^\+/, '') + const dataPath = 'timezones.bson' + const zones = getCodeSync(dataPath, nr) + + if (typeof zones === 'string' && zones.length > 0) { + return zones.split('&').filter((zone) => zone.length > 0) + } + + return null +} + +export function clearCache(): void { + codeDataCache.clear() +} + +export function getCacheSize(): number { + return codeDataCache.size +} + +export function setCacheSize(size: number): void { + const oldCache = codeDataCache + codeDataCache = lru(size) + + const entries = oldCache.entries() + entries.reverse() + for (const [key, value] of entries) { + if (codeDataCache.size < size) { + codeDataCache.set(key, value) + } else { + break + } + } +} diff --git a/src/index.ts b/src/index.ts index aba3cbc..fcfbb87 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,10 +1,10 @@ export * from 'libphonenumber-js' -import { PhoneNumber } from 'libphonenumber-js' -import { CarrierLocale, GeocoderLocale } from './locales' -import { readFileSync } from 'fs' -import { deserialize, Document } from 'bson' -import { join } from 'path' -import { lru, LRU } from 'tiny-lru' +import type { PhoneNumber } from 'libphonenumber-js' +import type { CarrierLocale, GeocoderLocale } from './locales' +import { readFileSync } from 'node:fs' +import { deserialize, type Document } from 'bson' +import { join } from 'node:path' +import { lru, type LRU } from 'tiny-lru' const DEFAULT_CACHE_SIZE = 100 let codeDataCache: LRU = lru(DEFAULT_CACHE_SIZE) @@ -25,13 +25,13 @@ function getCode(dataPath: string, nationalNumber: string): string | null { try { // Use tiny-lru cache let data = codeDataCache.get(dataPath) - + if (!data) { const bData = readFileSync(dataPath) data = deserialize(bData) codeDataCache.set(dataPath, data) } - + let prefix = nationalNumber // Find the longest match while (prefix.length > 0) { @@ -63,14 +63,14 @@ function getLocalizedData( if (!phonenumber) { return null } - + const nationalNumber = phonenumber.nationalNumber?.toString() const countryCallingCode = phonenumber.countryCallingCode?.toString() - + if (!nationalNumber || !countryCallingCode) { return null } - + // Try with requested locale let dataPath = join( __dirname, @@ -79,12 +79,12 @@ function getLocalizedData( locale, `${countryCallingCode}.bson` ) - - let code = getCode(dataPath, nationalNumber) + + const code = getCode(dataPath, nationalNumber) if (code) { return code } - + // Fallback to default locale if different if (locale !== fallbackLocale) { dataPath = join( @@ -96,7 +96,7 @@ function getLocalizedData( ) return getCode(dataPath, nationalNumber) } - + return null } @@ -138,20 +138,20 @@ export function timezones(phonenumber: PhoneNumber | undefined): string[] | null if (!phonenumber || !phonenumber.number) { return null } - + let nr = phonenumber.number.toString() if (!nr) { return null } - + nr = nr.replace(/^\+/, '') const dataPath = join(__dirname, '../resources/timezones.bson') const zones = getCode(dataPath, nr) - + if (typeof zones === 'string' && zones.length > 0) { - return zones.split('&').filter(zone => zone.length > 0) + return zones.split('&').filter((zone) => zone.length > 0) } - + return null } @@ -178,7 +178,7 @@ export function setCacheSize(size: number): void { // Create a new cache with the new size and transfer existing data const oldCache = codeDataCache codeDataCache = lru(size) - + // Transfer entries from old cache to new cache (most recent first) const entries = oldCache.entries() entries.reverse() // Start with most recent diff --git a/src/locales.ts b/src/locales.ts index 9d4412e..91820ee 100644 --- a/src/locales.ts +++ b/src/locales.ts @@ -1,3 +1,36 @@ /* THIS FILE IS AUTOGENERATED. */ -export type GeocoderLocale = 'ar' | 'be' | 'bg' | 'bs' | 'de' | 'el' | 'en' | 'es' | 'fa' | 'fi' | 'fr' | 'hr' | 'hu' | 'hy' | 'id' | 'it' | 'iw' | 'ja' | 'ko' | 'nl' | 'pl' | 'pt' | 'ro' | 'ru' | 'sq' | 'sr' | 'sv' | 'th' | 'tr' | 'uk' | 'vi' | 'zh' | 'zh_Hant'; -export type CarrierLocale = 'ar' | 'be' | 'en' | 'fa' | 'ko' | 'ru' | 'uk' | 'zh' | 'zh_Hant'; +export type GeocoderLocale = + | 'ar' + | 'be' + | 'bg' + | 'bs' + | 'de' + | 'el' + | 'en' + | 'es' + | 'fa' + | 'fi' + | 'fr' + | 'hr' + | 'hu' + | 'hy' + | 'id' + | 'it' + | 'iw' + | 'ja' + | 'ko' + | 'nl' + | 'pl' + | 'pt' + | 'ro' + | 'ru' + | 'sq' + | 'sr' + | 'sv' + | 'th' + | 'tr' + | 'uk' + | 'vi' + | 'zh' + | 'zh_Hant' +export type CarrierLocale = 'ar' | 'be' | 'en' | 'fa' | 'ko' | 'ru' | 'uk' | 'zh' | 'zh_Hant' diff --git a/tsconfig.json b/tsconfig.json index 1cf7a6d..7fecf8c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,25 +2,15 @@ "compilerOptions": { "target": "ES5", "module": "esnext", - "lib": [ - "ESNext" - ], + "lib": ["ESNext"], "declaration": true, "strict": true, "noImplicitAny": true, "moduleResolution": "node", - "types": [ - "node", - "jest" - ], + "types": ["node", "jest"], "esModuleInterop": true, "downlevelIteration": true }, - "include": [ - "src/**/*" - ], - "exclude": [ - "node_modules", - "**/*.test.ts" - ] + "include": ["src/**/*"], + "exclude": ["node_modules", "**/*.test.ts"] } diff --git a/yarn.lock b/yarn.lock index bb8a588..f402e8c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -278,6 +278,60 @@ resolved "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== +"@biomejs/biome@^2.2.2": + version "2.2.2" + resolved "https://registry.npmjs.org/@biomejs/biome/-/biome-2.2.2.tgz" + integrity sha512-j1omAiQWCkhuLgwpMKisNKnsM6W8Xtt1l0WZmqY/dFj8QPNkIoTvk4tSsi40FaAAkBE1PU0AFG2RWFBWenAn+w== + optionalDependencies: + "@biomejs/cli-darwin-arm64" "2.2.2" + "@biomejs/cli-darwin-x64" "2.2.2" + "@biomejs/cli-linux-arm64" "2.2.2" + "@biomejs/cli-linux-arm64-musl" "2.2.2" + "@biomejs/cli-linux-x64" "2.2.2" + "@biomejs/cli-linux-x64-musl" "2.2.2" + "@biomejs/cli-win32-arm64" "2.2.2" + "@biomejs/cli-win32-x64" "2.2.2" + +"@biomejs/cli-darwin-arm64@2.2.2": + version "2.2.2" + resolved "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.2.2.tgz" + integrity sha512-6ePfbCeCPryWu0CXlzsWNZgVz/kBEvHiPyNpmViSt6A2eoDf4kXs3YnwQPzGjy8oBgQulrHcLnJL0nkCh80mlQ== + +"@biomejs/cli-darwin-x64@2.2.2": + version "2.2.2" + resolved "https://registry.yarnpkg.com/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.2.2.tgz#68bf6e2dc4384f96d590b2c342bfa09fbb7be492" + integrity sha512-Tn4JmVO+rXsbRslml7FvKaNrlgUeJot++FkvYIhl1OkslVCofAtS35MPlBMhXgKWF9RNr9cwHanrPTUUXcYGag== + +"@biomejs/cli-linux-arm64-musl@2.2.2": + version "2.2.2" + resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.2.2.tgz#3f091595615739c69ccc300a5eb3acbefca3996c" + integrity sha512-/MhYg+Bd6renn6i1ylGFL5snYUn/Ct7zoGVKhxnro3bwekiZYE8Kl39BSb0MeuqM+72sThkQv4TnNubU9njQRw== + +"@biomejs/cli-linux-arm64@2.2.2": + version "2.2.2" + resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.2.2.tgz#9ed17fc01681e83a1d52efd366f9edc3efbca0ae" + integrity sha512-JfrK3gdmWWTh2J5tq/rcWCOsImVyzUnOS2fkjhiYKCQ+v8PqM+du5cfB7G1kXas+7KQeKSWALv18iQqdtIMvzw== + +"@biomejs/cli-linux-x64-musl@2.2.2": + version "2.2.2" + resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.2.2.tgz#01bcb119f2f94af5e5610a961b9ffcfa26cf2a3b" + integrity sha512-ZCLXcZvjZKSiRY/cFANKg+z6Fhsf9MHOzj+NrDQcM+LbqYRT97LyCLWy2AS+W2vP+i89RyRM+kbGpUzbRTYWig== + +"@biomejs/cli-linux-x64@2.2.2": + version "2.2.2" + resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-x64/-/cli-linux-x64-2.2.2.tgz#c5d0c6ce58b90e30f123e2cfdb29d2add65e2384" + integrity sha512-Ogb+77edO5LEP/xbNicACOWVLt8mgC+E1wmpUakr+O4nKwLt9vXe74YNuT3T1dUBxC/SnrVmlzZFC7kQJEfquQ== + +"@biomejs/cli-win32-arm64@2.2.2": + version "2.2.2" + resolved "https://registry.yarnpkg.com/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.2.2.tgz#26e0fe782de6d83f3ecb4f247322a483104d749a" + integrity sha512-wBe2wItayw1zvtXysmHJQoQqXlTzHSpQRyPpJKiNIR21HzH/CrZRDFic1C1jDdp+zAPtqhNExa0owKMbNwW9cQ== + +"@biomejs/cli-win32-x64@2.2.2": + version "2.2.2" + resolved "https://registry.yarnpkg.com/@biomejs/cli-win32-x64/-/cli-win32-x64-2.2.2.tgz#8c08d82e50b06ad50e4bc54b4bb41428d4261b5c" + integrity sha512-DAuHhHekGfiGb6lCcsT4UyxQmVwQiBCBUMwVra/dcOSs9q8OhfaZgey51MlekT3p8UwRqtXQfFuEJBhJNdLZwg== + "@emnapi/core@^1.4.3": version "1.4.5" resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.4.5.tgz#bfbb0cbbbb9f96ec4e2c4fd917b7bbe5495ceccb" @@ -300,6 +354,136 @@ dependencies: tslib "^2.4.0" +"@esbuild/aix-ppc64@0.25.9": + version "0.25.9" + resolved "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz" + integrity sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA== + +"@esbuild/android-arm64@0.25.9": + version "0.25.9" + resolved "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz" + integrity sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg== + +"@esbuild/android-arm@0.25.9": + version "0.25.9" + resolved "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz" + integrity sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ== + +"@esbuild/android-x64@0.25.9": + version "0.25.9" + resolved "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz" + integrity sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw== + +"@esbuild/darwin-arm64@0.25.9": + version "0.25.9" + resolved "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz" + integrity sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg== + +"@esbuild/darwin-x64@0.25.9": + version "0.25.9" + resolved "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz" + integrity sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ== + +"@esbuild/freebsd-arm64@0.25.9": + version "0.25.9" + resolved "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz" + integrity sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q== + +"@esbuild/freebsd-x64@0.25.9": + version "0.25.9" + resolved "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz" + integrity sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg== + +"@esbuild/linux-arm64@0.25.9": + version "0.25.9" + resolved "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz" + integrity sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw== + +"@esbuild/linux-arm@0.25.9": + version "0.25.9" + resolved "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz" + integrity sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw== + +"@esbuild/linux-ia32@0.25.9": + version "0.25.9" + resolved "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz" + integrity sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A== + +"@esbuild/linux-loong64@0.25.9": + version "0.25.9" + resolved "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz" + integrity sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ== + +"@esbuild/linux-mips64el@0.25.9": + version "0.25.9" + resolved "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz" + integrity sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA== + +"@esbuild/linux-ppc64@0.25.9": + version "0.25.9" + resolved "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz" + integrity sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w== + +"@esbuild/linux-riscv64@0.25.9": + version "0.25.9" + resolved "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz" + integrity sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg== + +"@esbuild/linux-s390x@0.25.9": + version "0.25.9" + resolved "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz" + integrity sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA== + +"@esbuild/linux-x64@0.25.9": + version "0.25.9" + resolved "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz" + integrity sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg== + +"@esbuild/netbsd-arm64@0.25.9": + version "0.25.9" + resolved "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz" + integrity sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q== + +"@esbuild/netbsd-x64@0.25.9": + version "0.25.9" + resolved "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz" + integrity sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g== + +"@esbuild/openbsd-arm64@0.25.9": + version "0.25.9" + resolved "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz" + integrity sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ== + +"@esbuild/openbsd-x64@0.25.9": + version "0.25.9" + resolved "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz" + integrity sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA== + +"@esbuild/openharmony-arm64@0.25.9": + version "0.25.9" + resolved "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz" + integrity sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg== + +"@esbuild/sunos-x64@0.25.9": + version "0.25.9" + resolved "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz" + integrity sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw== + +"@esbuild/win32-arm64@0.25.9": + version "0.25.9" + resolved "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz" + integrity sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ== + +"@esbuild/win32-ia32@0.25.9": + version "0.25.9" + resolved "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz" + integrity sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww== + +"@esbuild/win32-x64@0.25.9": + version "0.25.9" + resolved "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz" + integrity sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ== + "@isaacs/balanced-match@^4.0.1": version "4.0.1" resolved "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz" @@ -342,7 +526,7 @@ "@jest/console@30.1.1": version "30.1.1" - resolved "https://registry.yarnpkg.com/@jest/console/-/console-30.1.1.tgz#20e1fb7fe99ba85d68757cf23c1d17a1dc89485a" + resolved "https://registry.npmjs.org/@jest/console/-/console-30.1.1.tgz" integrity sha512-f7TGqR1k4GtN5pyFrKmq+ZVndesiwLU33yDpJIGMS9aW+j6hKjue7ljeAdznBsH9kAnxUWe2Y+Y3fLV/FJt3gA== dependencies: "@jest/types" "30.0.5" @@ -354,7 +538,7 @@ "@jest/core@30.1.1": version "30.1.1" - resolved "https://registry.yarnpkg.com/@jest/core/-/core-30.1.1.tgz#4b698d7182b9ae41f9613cb4392a4587c593d147" + resolved "https://registry.npmjs.org/@jest/core/-/core-30.1.1.tgz" integrity sha512-3ncU9peZ3D2VdgRkdZtUceTrDgX5yiDRwAFjtxNfU22IiZrpVWlv/FogzDLYSJQptQGfFo3PcHK86a2oG6WUGg== dependencies: "@jest/console" "30.1.1" @@ -393,7 +577,7 @@ "@jest/environment@30.1.1": version "30.1.1" - resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-30.1.1.tgz#ce8f245a14ff47c8fbc2ac17e2fbe9b984df245d" + resolved "https://registry.npmjs.org/@jest/environment/-/environment-30.1.1.tgz" integrity sha512-yWHbU+3j7ehQE+NRpnxRvHvpUhoohIjMePBbIr8lfe0cWVb0WeTf80DNux1GPJa18CDHiIU5DtksGUfxcDE+Rw== dependencies: "@jest/fake-timers" "30.1.1" @@ -410,14 +594,14 @@ "@jest/expect-utils@30.1.1": version "30.1.1" - resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-30.1.1.tgz#f57553f708b445a8d20c5b365bc9c84f87cba2ac" + resolved "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.1.1.tgz" integrity sha512-5YUHr27fpJ64dnvtu+tt11ewATynrHkGYD+uSFgRr8V2eFJis/vEXgToyLwccIwqBihVfz9jwio+Zr1ab1Zihw== dependencies: "@jest/get-type" "30.1.0" "@jest/expect@30.1.1": version "30.1.1" - resolved "https://registry.yarnpkg.com/@jest/expect/-/expect-30.1.1.tgz#d9274c0dc6af430ab4c5b1e6692e62cec757d229" + resolved "https://registry.npmjs.org/@jest/expect/-/expect-30.1.1.tgz" integrity sha512-3vHIHsF+qd3D8FU2c7U5l3rg1fhDwAYcGyHyZAi94YIlTwcJ+boNhRyJf373cl4wxbOX+0Q7dF40RTrTFTSuig== dependencies: expect "30.1.1" @@ -425,7 +609,7 @@ "@jest/fake-timers@30.1.1": version "30.1.1" - resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-30.1.1.tgz#eb0cce02f8ca5a69cc9754780836068461ccaa45" + resolved "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.1.1.tgz" integrity sha512-fK/25dNgBNYPw3eLi2CRs57g1H04qBAFNMsUY3IRzkfx/m4THe0E1zF+yGQBOMKKc2XQVdc9EYbJ4hEm7/2UtA== dependencies: "@jest/types" "30.0.5" @@ -442,12 +626,12 @@ "@jest/get-type@30.1.0": version "30.1.0" - resolved "https://registry.yarnpkg.com/@jest/get-type/-/get-type-30.1.0.tgz#4fcb4dc2ebcf0811be1c04fd1cb79c2dba431cbc" + resolved "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz" integrity sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA== "@jest/globals@30.1.1": version "30.1.1" - resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-30.1.1.tgz#0d1cdfb7f6d73ee1a6e9f679fcd706971804b39e" + resolved "https://registry.npmjs.org/@jest/globals/-/globals-30.1.1.tgz" integrity sha512-NNUUkHT2TU/xztZl6r1UXvJL+zvCwmZsQDmK69fVHHcB9fBtlu3FInnzOve/ZoyKnWY8JXWJNT+Lkmu1+ubXUA== dependencies: "@jest/environment" "30.1.1" @@ -465,7 +649,7 @@ "@jest/reporters@30.1.1": version "30.1.1" - resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-30.1.1.tgz#5795b0cad1a18fb1b7228be1e9acfd3ed7fe1721" + resolved "https://registry.npmjs.org/@jest/reporters/-/reporters-30.1.1.tgz" integrity sha512-Hb2Bq80kahOC6Sv2waEaH1rEU6VdFcM6WHaRBWQF9tf30+nJHxhl/Upbgo9+25f0mOgbphxvbwSMjSgy9gW/FA== dependencies: "@bcoe/v8-coverage" "^0.2.3" @@ -501,7 +685,7 @@ "@jest/snapshot-utils@30.1.1": version "30.1.1" - resolved "https://registry.yarnpkg.com/@jest/snapshot-utils/-/snapshot-utils-30.1.1.tgz#cfb8eaff6e487954437335613646009b530e9959" + resolved "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.1.1.tgz" integrity sha512-TkVBc9wuN22TT8hESRFmjjg/xIMu7z0J3UDYtIRydzCqlLPTB7jK1DDBKdnTUZ4zL3z3rnPpzV6rL1Uzh87sXg== dependencies: "@jest/types" "30.0.5" @@ -520,7 +704,7 @@ "@jest/test-result@30.1.1": version "30.1.1" - resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-30.1.1.tgz#05ab260c7d727bfc23fb072a575ee95c4a5c37b6" + resolved "https://registry.npmjs.org/@jest/test-result/-/test-result-30.1.1.tgz" integrity sha512-bMdj7fNu8iZuBPSnbVir5ezvWmVo4jrw7xDE+A33Yb3ENCoiJK9XgOLgal+rJ9XSKjsL7aPUMIo87zhN7I5o2w== dependencies: "@jest/console" "30.1.1" @@ -530,7 +714,7 @@ "@jest/test-sequencer@30.1.1": version "30.1.1" - resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-30.1.1.tgz#7de235d2c63cb6bbb0fc81a344484b0e1781bbc3" + resolved "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.1.1.tgz" integrity sha512-yruRdLXSA3HYD/MTNykgJ6VYEacNcXDFRMqKVAwlYegmxICUiT/B++CNuhJnYJzKYks61iYnjVsMwbUqmmAYJg== dependencies: "@jest/test-result" "30.1.1" @@ -540,7 +724,7 @@ "@jest/transform@30.1.1": version "30.1.1" - resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-30.1.1.tgz#9ed736ee0e8787d5648401193603e0026d11f0b0" + resolved "https://registry.npmjs.org/@jest/transform/-/transform-30.1.1.tgz" integrity sha512-PHIA2AbAASBfk6evkNifvmx9lkOSkmvaQoO6VSpuL8+kQqDMHeDoJ7RU3YP1wWAMD7AyQn9UL5iheuFYCC4lqQ== dependencies: "@babel/core" "^7.27.4" @@ -585,11 +769,24 @@ resolved "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz" integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== +"@jridgewell/source-map@^0.3.3": + version "0.3.11" + resolved "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz" + integrity sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + "@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0": version "1.5.4" resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz" integrity sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw== +"@jridgewell/sourcemap-codec@^1.5.5": + version "1.5.5" + resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz" + integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og== + "@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.23", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25", "@jridgewell/trace-mapping@^0.3.28": version "0.3.29" resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz" @@ -638,13 +835,54 @@ resolved "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz" integrity sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA== -"@rollup/pluginutils@^4.1.2": - version "4.2.1" - resolved "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz" - integrity sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ== +"@rollup/plugin-commonjs@^28.0.6": + version "28.0.6" + resolved "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.6.tgz" + integrity sha512-XSQB1K7FUU5QP+3lOQmVCE3I0FcbbNvmNT4VJSj93iUjayaARrTQeoRdiYQoftAJBLrR9t2agwAd3ekaTgHNlw== + dependencies: + "@rollup/pluginutils" "^5.0.1" + commondir "^1.0.1" + estree-walker "^2.0.2" + fdir "^6.2.0" + is-reference "1.2.1" + magic-string "^0.30.3" + picomatch "^4.0.2" + +"@rollup/plugin-json@^6.1.0": + version "6.1.0" + resolved "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz" + integrity sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA== + dependencies: + "@rollup/pluginutils" "^5.1.0" + +"@rollup/plugin-node-resolve@^16.0.1": + version "16.0.1" + resolved "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.1.tgz" + integrity sha512-tk5YCxJWIG81umIvNkSod2qK5KyQW19qcBF/B78n1bjtOON6gzKoVeSzAE8yHCZEDmqkHKkxplExA8KzdJLJpA== dependencies: - estree-walker "^2.0.1" - picomatch "^2.2.2" + "@rollup/pluginutils" "^5.0.1" + "@types/resolve" "1.20.2" + deepmerge "^4.2.2" + is-module "^1.0.0" + resolve "^1.22.1" + +"@rollup/plugin-terser@^0.4.4": + version "0.4.4" + resolved "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz" + integrity sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A== + dependencies: + serialize-javascript "^6.0.1" + smob "^1.0.0" + terser "^5.17.4" + +"@rollup/pluginutils@^5.0.1", "@rollup/pluginutils@^5.1.0": + version "5.2.0" + resolved "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.2.0.tgz" + integrity sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw== + dependencies: + "@types/estree" "^1.0.0" + estree-walker "^2.0.2" + picomatch "^4.0.2" "@rollup/rollup-android-arm-eabi@4.49.0": version "4.49.0" @@ -658,7 +896,7 @@ "@rollup/rollup-darwin-arm64@4.49.0": version "4.49.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.49.0.tgz#788fad425b4129875639e0c14b6441c5f3b69d46" + resolved "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.49.0.tgz" integrity sha512-99kMMSMQT7got6iYX3yyIiJfFndpojBmkHfTc1rIje8VbjhmqBXE+nb7ZZP3A5skLyujvT0eIUCUsxAe6NjWbw== "@rollup/rollup-darwin-x64@4.49.0": @@ -812,9 +1050,9 @@ dependencies: bson "*" -"@types/estree@1.0.8": +"@types/estree@*", "@types/estree@1.0.8", "@types/estree@^1.0.0": version "1.0.8" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e" + resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz" integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w== "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.1", "@types/istanbul-lib-coverage@^2.0.6": @@ -851,6 +1089,11 @@ dependencies: undici-types "~7.10.0" +"@types/resolve@1.20.2": + version "1.20.2" + resolved "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz" + integrity sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q== + "@types/shelljs@^0.8.17": version "0.8.17" resolved "https://registry.npmjs.org/@types/shelljs/-/shelljs-0.8.17.tgz" @@ -978,6 +1221,11 @@ resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz#538b1e103bf8d9864e7b85cc96fa8d6fb6c40777" integrity sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g== +acorn@^8.14.0: + version "8.15.0" + resolved "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz" + integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg== + ansi-escapes@^4.3.2: version "4.3.2" resolved "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz" @@ -1029,7 +1277,7 @@ argparse@^1.0.7: babel-jest@30.1.1: version "30.1.1" - resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-30.1.1.tgz#6813b0a89c3f141ffad1f5b9bde304c26df8fbfd" + resolved "https://registry.npmjs.org/babel-jest/-/babel-jest-30.1.1.tgz" integrity sha512-1bZfC/V03qBCzASvZpNFhx3Ouj6LgOd4KFJm4br/fYOS+tSSvVCE61QmcAVbMTwq/GoB7KN4pzGMoyr9cMxSvQ== dependencies: "@jest/transform" "30.1.1" @@ -1224,6 +1472,11 @@ color-name@~1.1.4: resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +commander@^2.20.0: + version "2.20.3" + resolved "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + commondir@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz" @@ -1248,7 +1501,7 @@ cross-spawn@^7.0.3, cross-spawn@^7.0.6: shebang-command "^2.0.0" which "^2.0.1" -debug@^4.1.0, debug@^4.1.1, debug@^4.3.1: +debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.4.0: version "4.4.1" resolved "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz" integrity sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ== @@ -1260,7 +1513,7 @@ dedent@^1.6.0: resolved "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz" integrity sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA== -deepmerge@^4.3.1: +deepmerge@^4.2.2, deepmerge@^4.3.1: version "4.3.1" resolved "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz" integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== @@ -1302,6 +1555,43 @@ error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" +es-module-lexer@^1.6.0: + version "1.7.0" + resolved "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz" + integrity sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA== + +esbuild@^0.25.9: + version "0.25.9" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.25.9.tgz#15ab8e39ae6cdc64c24ff8a2c0aef5b3fd9fa976" + integrity sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g== + optionalDependencies: + "@esbuild/aix-ppc64" "0.25.9" + "@esbuild/android-arm" "0.25.9" + "@esbuild/android-arm64" "0.25.9" + "@esbuild/android-x64" "0.25.9" + "@esbuild/darwin-arm64" "0.25.9" + "@esbuild/darwin-x64" "0.25.9" + "@esbuild/freebsd-arm64" "0.25.9" + "@esbuild/freebsd-x64" "0.25.9" + "@esbuild/linux-arm" "0.25.9" + "@esbuild/linux-arm64" "0.25.9" + "@esbuild/linux-ia32" "0.25.9" + "@esbuild/linux-loong64" "0.25.9" + "@esbuild/linux-mips64el" "0.25.9" + "@esbuild/linux-ppc64" "0.25.9" + "@esbuild/linux-riscv64" "0.25.9" + "@esbuild/linux-s390x" "0.25.9" + "@esbuild/linux-x64" "0.25.9" + "@esbuild/netbsd-arm64" "0.25.9" + "@esbuild/netbsd-x64" "0.25.9" + "@esbuild/openbsd-arm64" "0.25.9" + "@esbuild/openbsd-x64" "0.25.9" + "@esbuild/openharmony-arm64" "0.25.9" + "@esbuild/sunos-x64" "0.25.9" + "@esbuild/win32-arm64" "0.25.9" + "@esbuild/win32-ia32" "0.25.9" + "@esbuild/win32-x64" "0.25.9" + escalade@^3.1.1, escalade@^3.2.0: version "3.2.0" resolved "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz" @@ -1317,7 +1607,7 @@ esprima@^4.0.0: resolved "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== -estree-walker@^2.0.1: +estree-walker@^2.0.2: version "2.0.2" resolved "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz" integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== @@ -1344,7 +1634,7 @@ exit-x@^0.2.2: expect@30.1.1: version "30.1.1" - resolved "https://registry.yarnpkg.com/expect/-/expect-30.1.1.tgz#165bbdf514880bc9d4377b5b73716a38ab97b2ad" + resolved "https://registry.npmjs.org/expect/-/expect-30.1.1.tgz" integrity sha512-OKe7cdic4qbfWd/CcgwJvvCrNX2KWfuMZee9AfJHL1gTYmvqjBjZG1a2NwfhspBzxzlXwsN75WWpKTYfsJpBxg== dependencies: "@jest/expect-utils" "30.1.1" @@ -1396,6 +1686,11 @@ fb-watchman@^2.0.2: dependencies: bser "2.1.1" +fdir@^6.2.0: + version "6.5.0" + resolved "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz" + integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg== + fill-range@^7.1.1: version "7.1.1" resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz" @@ -1403,15 +1698,6 @@ fill-range@^7.1.1: dependencies: to-regex-range "^5.0.1" -find-cache-dir@^3.3.2: - version "3.3.2" - resolved "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz" - integrity sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig== - dependencies: - commondir "^1.0.1" - make-dir "^3.0.2" - pkg-dir "^4.1.0" - find-up@^4.0.0, find-up@^4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz" @@ -1428,15 +1714,6 @@ foreground-child@^3.1.0, foreground-child@^3.3.1: cross-spawn "^7.0.6" signal-exit "^4.0.1" -fs-extra@^10.0.0: - version "10.1.0" - resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz" - integrity sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ== - dependencies: - graceful-fs "^4.2.0" - jsonfile "^6.0.1" - universalify "^2.0.0" - fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" @@ -1447,6 +1724,11 @@ fsevents@^2.3.3, fsevents@~2.3.2: resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz" integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + gensync@^1.0.0-beta.2: version "1.0.0-beta.2" resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz" @@ -1467,6 +1749,13 @@ get-stream@^6.0.0: resolved "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz" integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== +get-tsconfig@^4.10.0: + version "4.10.1" + resolved "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz" + integrity sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ== + dependencies: + resolve-pkg-maps "^1.0.0" + glob-parent@^5.1.2: version "5.1.2" resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz" @@ -1510,7 +1799,7 @@ glob@^7.1.4: once "^1.3.0" path-is-absolute "^1.0.0" -graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.11: +graceful-fs@^4.2.11: version "4.2.11" resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== @@ -1532,6 +1821,13 @@ has-flag@^4.0.0: resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== +hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + html-escaper@^2.0.0: version "2.0.2" resolved "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz" @@ -1573,6 +1869,13 @@ is-arrayish@^0.2.1: resolved "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz" integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== +is-core-module@^2.16.0: + version "2.16.1" + resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz" + integrity sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w== + dependencies: + hasown "^2.0.2" + is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz" @@ -1595,11 +1898,23 @@ is-glob@^4.0.1: dependencies: is-extglob "^2.1.1" +is-module@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz" + integrity sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g== + is-number@^7.0.0: version "7.0.0" resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== +is-reference@1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz" + integrity sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ== + dependencies: + "@types/estree" "*" + is-stream@^2.0.0: version "2.0.1" resolved "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz" @@ -1679,7 +1994,7 @@ jest-changed-files@30.0.5: jest-circus@30.1.1: version "30.1.1" - resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-30.1.1.tgz#9f5b942e20641e7ee0c5e9f2e0c7690f75371d39" + resolved "https://registry.npmjs.org/jest-circus/-/jest-circus-30.1.1.tgz" integrity sha512-M3Vd4x5wD7eSJspuTvRF55AkOOBndRxgW3gqQBDlFvbH3X+ASdi8jc+EqXEeAFd/UHulVYIlC4XKJABOhLw6UA== dependencies: "@jest/environment" "30.1.1" @@ -1705,7 +2020,7 @@ jest-circus@30.1.1: jest-cli@30.1.1: version "30.1.1" - resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-30.1.1.tgz#1911a86ec51f3e55f6fc0b64002400bcc8bb75a8" + resolved "https://registry.npmjs.org/jest-cli/-/jest-cli-30.1.1.tgz" integrity sha512-xm9llxuh5OoI5KZaYzlMhklryHBwg9LZy/gEaaMlXlxb+cZekGNzukU0iblbDo3XOBuN6N0CgK4ykgNRYSEb6g== dependencies: "@jest/core" "30.1.1" @@ -1721,7 +2036,7 @@ jest-cli@30.1.1: jest-config@30.1.1: version "30.1.1" - resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-30.1.1.tgz#d0dc7fa49190076cba26499542c43f5f4162071c" + resolved "https://registry.npmjs.org/jest-config/-/jest-config-30.1.1.tgz" integrity sha512-xuPGUGDw+9fPPnGmddnLnHS/mhKUiJOW7K65vErYmglEPKq65NKwSRchkQ7iv6gqjs2l+YNEsAtbsplxozdOWg== dependencies: "@babel/core" "^7.27.4" @@ -1761,7 +2076,7 @@ jest-diff@30.0.5: jest-diff@30.1.1: version "30.1.1" - resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-30.1.1.tgz#cfe8327c059178affac17d4c003e7096ad19583c" + resolved "https://registry.npmjs.org/jest-diff/-/jest-diff-30.1.1.tgz" integrity sha512-LUU2Gx8EhYxpdzTR6BmjL1ifgOAQJQELTHOiPv9KITaKjZvJ9Jmgigx01tuZ49id37LorpGc9dPBPlXTboXScw== dependencies: "@jest/diff-sequences" "30.0.1" @@ -1778,7 +2093,7 @@ jest-docblock@30.0.1: jest-each@30.1.0: version "30.1.0" - resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-30.1.0.tgz#228756d5ea9e4dcb462fc2e90a44ec27dd482d23" + resolved "https://registry.npmjs.org/jest-each/-/jest-each-30.1.0.tgz" integrity sha512-A+9FKzxPluqogNahpCv04UJvcZ9B3HamqpDNWNKDjtxVRYB8xbZLFuCr8JAJFpNp83CA0anGQFlpQna9Me+/tQ== dependencies: "@jest/get-type" "30.1.0" @@ -1789,7 +2104,7 @@ jest-each@30.1.0: jest-environment-node@30.1.1: version "30.1.1" - resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-30.1.1.tgz#e9a026147d25637e7bc03d2b7d03b41d5488dfc4" + resolved "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.1.1.tgz" integrity sha512-IaMoaA6saxnJimqCppUDqKck+LKM0Jg+OxyMUIvs1yGd2neiC22o8zXo90k04+tO+49OmgMR4jTgM5e4B0S62Q== dependencies: "@jest/environment" "30.1.1" @@ -1802,7 +2117,7 @@ jest-environment-node@30.1.1: jest-haste-map@30.1.0: version "30.1.0" - resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-30.1.0.tgz#e54d84e07fac15ea3a98903b735048e36d7d2ed3" + resolved "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.1.0.tgz" integrity sha512-JLeM84kNjpRkggcGpQLsV7B8W4LNUWz7oDNVnY1Vjj22b5/fAb3kk3htiD+4Na8bmJmjJR7rBtS2Rmq/NEcADg== dependencies: "@jest/types" "30.0.5" @@ -1820,7 +2135,7 @@ jest-haste-map@30.1.0: jest-leak-detector@30.1.0: version "30.1.0" - resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-30.1.0.tgz#8b86e7c5f1e3e4f2a32d930ec769103ad0985874" + resolved "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.1.0.tgz" integrity sha512-AoFvJzwxK+4KohH60vRuHaqXfWmeBATFZpzpmzNmYTtmRMiyGPVhkXpBqxUQunw+dQB48bDf4NpUs6ivVbRv1g== dependencies: "@jest/get-type" "30.1.0" @@ -1838,7 +2153,7 @@ jest-matcher-utils@30.0.5: jest-matcher-utils@30.1.1: version "30.1.1" - resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-30.1.1.tgz#e45419d966cd2e5e7d7ade6da747035c6a3b8afc" + resolved "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.1.1.tgz" integrity sha512-SuH2QVemK48BNTqReti6FtjsMPFsSOD/ZzRxU1TttR7RiRsRSe78d03bb4Cx6D4bQC/80Q8U4VnaaAH9FlbZ9w== dependencies: "@jest/get-type" "30.1.0" @@ -1863,7 +2178,7 @@ jest-message-util@30.0.5: jest-message-util@30.1.0: version "30.1.0" - resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-30.1.0.tgz#653a9bb1a33306eddf13455ce0666ba621b767c4" + resolved "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.1.0.tgz" integrity sha512-HizKDGG98cYkWmaLUHChq4iN+oCENohQLb7Z5guBPumYs+/etonmNFlg1Ps6yN9LTPyZn+M+b/9BbnHx3WTMDg== dependencies: "@babel/code-frame" "^7.27.1" @@ -1897,7 +2212,7 @@ jest-regex-util@30.0.1: jest-resolve-dependencies@30.1.1: version "30.1.1" - resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-30.1.1.tgz#7ed42f87a017a53655a46681cc2ec63609efcaec" + resolved "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.1.1.tgz" integrity sha512-tRtaaoH8Ws1Gn1o/9pedt19dvVgr81WwdmvJSP9Ow3amOUOP2nN9j94u5jC9XlIfa2Q1FQKIWWQwL4ajqsjCGQ== dependencies: jest-regex-util "30.0.1" @@ -1905,7 +2220,7 @@ jest-resolve-dependencies@30.1.1: jest-resolve@30.1.0: version "30.1.0" - resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-30.1.0.tgz#f434f576578a5b9c4e5a73352c08e99ea319e840" + resolved "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.1.0.tgz" integrity sha512-hASe7D/wRtZw8Cm607NrlF7fi3HWC5wmA5jCVc2QjQAB2pTwP9eVZILGEi6OeSLNUtE1zb04sXRowsdh5CUjwA== dependencies: chalk "^4.1.2" @@ -1919,7 +2234,7 @@ jest-resolve@30.1.0: jest-runner@30.1.1: version "30.1.1" - resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-30.1.1.tgz#22c8d892293504a5afa085261cd04559e1cd5f16" + resolved "https://registry.npmjs.org/jest-runner/-/jest-runner-30.1.1.tgz" integrity sha512-ATe6372SOfJvCRExtCAr06I4rGujwFdKg44b6i7/aOgFnULwjxzugJ0Y4AnG+jeSeQi8dU7R6oqLGmsxRUbErQ== dependencies: "@jest/console" "30.1.1" @@ -1947,7 +2262,7 @@ jest-runner@30.1.1: jest-runtime@30.1.1: version "30.1.1" - resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-30.1.1.tgz#f1738e72912b1b86b6f48593c6d015d758246606" + resolved "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.1.1.tgz" integrity sha512-7sOyR0Oekw4OesQqqBHuYJRB52QtXiq0NNgLRzVogiMSxKCMiliUd6RrXHCnG5f12Age/ggidCBiQftzcA9XKw== dependencies: "@jest/environment" "30.1.1" @@ -1975,7 +2290,7 @@ jest-runtime@30.1.1: jest-snapshot@30.1.1: version "30.1.1" - resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-30.1.1.tgz#ef9bfdc22b4e807622e14fe32fd40745a2c031e5" + resolved "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.1.1.tgz" integrity sha512-7/iBEzoJqEt2TjkQY+mPLHP8cbPhLReZVkkxjTMzIzoTC4cZufg7HzKo/n9cIkXKj2LG0x3mmBHsZto+7TOmFg== dependencies: "@babel/core" "^7.27.4" @@ -2014,7 +2329,7 @@ jest-util@30.0.5: jest-validate@30.1.0: version "30.1.0" - resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-30.1.0.tgz#585aae6c9ee1ac138dbacbece8a7838ca7773e60" + resolved "https://registry.npmjs.org/jest-validate/-/jest-validate-30.1.0.tgz" integrity sha512-7P3ZlCFW/vhfQ8pE7zW6Oi4EzvuB4sgR72Q1INfW9m0FGo0GADYlPwIkf4CyPq7wq85g+kPMtPOHNAdWHeBOaA== dependencies: "@jest/get-type" "30.1.0" @@ -2026,7 +2341,7 @@ jest-validate@30.1.0: jest-watcher@30.1.1: version "30.1.1" - resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-30.1.1.tgz#de9341d38d8efb0e466f5f4cd7fde8479d15c998" + resolved "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.1.1.tgz" integrity sha512-CrAQ73LlaS6KGQQw6NBi71g7qvP7scy+4+2c0jKX6+CWaYg85lZiig5nQQVTsS5a5sffNPL3uxXnaE9d7v9eQg== dependencies: "@jest/test-result" "30.1.1" @@ -2040,7 +2355,7 @@ jest-watcher@30.1.1: jest-worker@30.1.0: version "30.1.0" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-30.1.0.tgz#a89c36772be449d4bdb60697fb695a1673b12ac2" + resolved "https://registry.npmjs.org/jest-worker/-/jest-worker-30.1.0.tgz" integrity sha512-uvWcSjlwAAgIu133Tt77A05H7RIk3Ho8tZL50bQM2AkvLdluw9NG48lRCl3Dt+MOH719n/0nnb5YxUwcuJiKRA== dependencies: "@types/node" "*" @@ -2051,7 +2366,7 @@ jest-worker@30.1.0: jest@^30.1.1: version "30.1.1" - resolved "https://registry.yarnpkg.com/jest/-/jest-30.1.1.tgz#43c4ea62f280ea978f17629d507f0c44eb552bef" + resolved "https://registry.npmjs.org/jest/-/jest-30.1.1.tgz" integrity sha512-yC3JvpP/ZcAZX5rYCtXO/g9k6VTCQz0VFE2v1FpxytWzUqfDtu0XL/pwnNvptzYItvGwomh1ehomRNMOyhCJKw== dependencies: "@jest/core" "30.1.1" @@ -2087,15 +2402,6 @@ json5@^2.2.3: resolved "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== -jsonfile@^6.0.1: - version "6.1.0" - resolved "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz" - integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== - dependencies: - universalify "^2.0.0" - optionalDependencies: - graceful-fs "^4.1.6" - leven@^3.1.0: version "3.1.0" resolved "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz" @@ -2103,7 +2409,7 @@ leven@^3.1.0: libphonenumber-js@^1.12.14: version "1.12.14" - resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.12.14.tgz#4a49aed7b975db7f3bf0eb17dbb9d2e99d68d916" + resolved "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.14.tgz" integrity sha512-HBAMAV7f3yGYy7ZZN5FxQ1tXJTwC77G5/96Yn/SH/HPyKX2EMLGFuCIYUmdLU7CxxJlQcvJymP/PGLzyapurhQ== lines-and-columns@^1.1.6: @@ -2140,12 +2446,12 @@ lru-cache@^5.1.1: dependencies: yallist "^3.0.2" -make-dir@^3.0.2: - version "3.1.0" - resolved "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz" - integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== +magic-string@^0.30.3: + version "0.30.18" + resolved "https://registry.npmjs.org/magic-string/-/magic-string-0.30.18.tgz" + integrity sha512-yi8swmWbO17qHhwIBNeeZxTceJMeBvWJaId6dyvTSOwTipqeHhMhOrz6513r1sOKnpvQ7zkhlG8tPrpilwTxHQ== dependencies: - semver "^6.0.0" + "@jridgewell/sourcemap-codec" "^1.5.5" make-dir@^4.0.0: version "4.0.0" @@ -2332,6 +2638,11 @@ path-key@^3.0.0, path-key@^3.1.0: resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + path-scurry@^1.11.1: version "1.11.1" resolved "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz" @@ -2348,17 +2659,22 @@ path-scurry@^2.0.0: lru-cache "^11.0.0" minipass "^7.1.2" +pathe@^2.0.3: + version "2.0.3" + resolved "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz" + integrity sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w== + picocolors@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz" integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== -picomatch@^2.0.4, picomatch@^2.2.2, picomatch@^2.3.1: +picomatch@^2.0.4, picomatch@^2.3.1: version "2.3.1" resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== -picomatch@^4.0.2: +picomatch@^4.0.2, picomatch@^4.0.3: version "4.0.3" resolved "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz" integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q== @@ -2368,7 +2684,7 @@ pirates@^4.0.7: resolved "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz" integrity sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA== -pkg-dir@^4.1.0, pkg-dir@^4.2.0: +pkg-dir@^4.2.0: version "4.2.0" resolved "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz" integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== @@ -2399,6 +2715,13 @@ queue-microtask@^1.2.2: resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== +randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + react-is@^18.3.1: version "18.3.1" resolved "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz" @@ -2421,25 +2744,38 @@ resolve-from@^5.0.0: resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz" integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== +resolve-pkg-maps@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz" + integrity sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw== + +resolve@^1.22.1: + version "1.22.10" + resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz" + integrity sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w== + dependencies: + is-core-module "^2.16.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + reusify@^1.0.4: version "1.1.0" resolved "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz" integrity sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw== -rollup-plugin-typescript2@^0.36.0: - version "0.36.0" - resolved "https://registry.npmjs.org/rollup-plugin-typescript2/-/rollup-plugin-typescript2-0.36.0.tgz" - integrity sha512-NB2CSQDxSe9+Oe2ahZbf+B4bh7pHwjV5L+RSYpCu7Q5ROuN94F9b6ioWwKfz3ueL3KTtmX4o2MUH2cgHDIEUsw== +rollup-plugin-esbuild@^6.2.1: + version "6.2.1" + resolved "https://registry.npmjs.org/rollup-plugin-esbuild/-/rollup-plugin-esbuild-6.2.1.tgz" + integrity sha512-jTNOMGoMRhs0JuueJrJqbW8tOwxumaWYq+V5i+PD+8ecSCVkuX27tGW7BXqDgoULQ55rO7IdNxPcnsWtshz3AA== dependencies: - "@rollup/pluginutils" "^4.1.2" - find-cache-dir "^3.3.2" - fs-extra "^10.0.0" - semver "^7.5.4" - tslib "^2.6.2" + debug "^4.4.0" + es-module-lexer "^1.6.0" + get-tsconfig "^4.10.0" + unplugin-utils "^0.2.4" rollup@^4.49.0: version "4.49.0" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.49.0.tgz#9751ad9d06a47a4496c3c5c238b27b1422c8b0eb" + resolved "https://registry.npmjs.org/rollup/-/rollup-4.49.0.tgz" integrity sha512-3IVq0cGJ6H7fKXXEdVt+RcYvRCt8beYY9K1760wGQwSAHZcS9eot1zDG5axUbcp/kWRi5zKIIDX8MoKv/TzvZA== dependencies: "@types/estree" "1.0.8" @@ -2473,7 +2809,12 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" -semver@^6.0.0, semver@^6.3.1: +safe-buffer@^5.1.0: + version "5.2.1" + resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +semver@^6.3.1: version "6.3.1" resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== @@ -2483,6 +2824,13 @@ semver@^7.5.3, semver@^7.5.4, semver@^7.7.2: resolved "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz" integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA== +serialize-javascript@^6.0.1: + version "6.0.2" + resolved "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz" + integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== + dependencies: + randombytes "^2.1.0" + shebang-command@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz" @@ -2518,6 +2866,11 @@ slash@^3.0.0: resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== +smob@^1.0.0: + version "1.5.0" + resolved "https://registry.npmjs.org/smob/-/smob-1.5.0.tgz" + integrity sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig== + source-map-support@0.5.13: version "0.5.13" resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz" @@ -2526,6 +2879,14 @@ source-map-support@0.5.13: buffer-from "^1.0.0" source-map "^0.6.0" +source-map-support@~0.5.20: + version "0.5.21" + resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + source-map@^0.6.0, source-map@^0.6.1: version "0.6.1" resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz" @@ -2628,6 +2989,11 @@ supports-color@^8.1.1: dependencies: has-flag "^4.0.0" +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + synckit@^0.11.8: version "0.11.11" resolved "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz" @@ -2635,6 +3001,16 @@ synckit@^0.11.8: dependencies: "@pkgr/core" "^0.2.9" +terser@^5.17.4: + version "5.43.1" + resolved "https://registry.npmjs.org/terser/-/terser-5.43.1.tgz" + integrity sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg== + dependencies: + "@jridgewell/source-map" "^0.3.3" + acorn "^8.14.0" + commander "^2.20.0" + source-map-support "~0.5.20" + test-exclude@^6.0.0: version "6.0.0" resolved "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz" @@ -2646,7 +3022,7 @@ test-exclude@^6.0.0: tiny-lru@^11.4.5: version "11.4.5" - resolved "https://registry.yarnpkg.com/tiny-lru/-/tiny-lru-11.4.5.tgz#0672b1b049f3d783021da5b99314264702f814c9" + resolved "https://registry.npmjs.org/tiny-lru/-/tiny-lru-11.4.5.tgz" integrity sha512-hkcz3FjNJfKXjV4mjQ1OrXSLAehg8Hw+cEZclOVT+5c/cWQWImQ9wolzTjth+dmmDe++p3bme3fTxz6Q4Etsqw== tmpl@1.0.5: @@ -2676,7 +3052,7 @@ ts-jest@^29.4.1: type-fest "^4.41.0" yargs-parser "^21.1.1" -tslib@^2.4.0, tslib@^2.6.2, tslib@^2.8.1: +tslib@^2.4.0, tslib@^2.8.1: version "2.8.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== @@ -2698,7 +3074,7 @@ type-fest@^4.41.0: typescript@^5.9.2: version "5.9.2" - resolved "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.2.tgz#d93450cddec5154a2d5cabe3b8102b83316fb2a6" integrity sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A== uglify-js@^3.1.4: @@ -2711,10 +3087,13 @@ undici-types@~7.10.0: resolved "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz" integrity sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag== -universalify@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz" - integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== +unplugin-utils@^0.2.4: + version "0.2.5" + resolved "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.2.5.tgz" + integrity sha512-gwXJnPRewT4rT7sBi/IvxKTjsms7jX7QIDLOClApuZwR49SXbrB1z2NLUZ+vDHyqCj/n58OzRRqaW+B8OZi8vg== + dependencies: + pathe "^2.0.3" + picomatch "^4.0.3" unrs-resolver@^1.7.11: version "1.11.1"