This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
@countrystatecity/countries - A server-side Node.js package providing lazy-loaded geographic data (countries, states, cities) with iOS/Safari compatibility and minimal bundle size (<10KB initial load).
Critical Constraint: This is a server-side only package that requires Node.js file system access. It cannot run directly in browsers.
npm run build # Build package with tsup (ESM + CJS + types)
npm run dev # Build in watch mode
npm run typecheck # Run TypeScript type checkingnpm test # Run all tests with Vitest
npm run test:watch # Run tests in watch mode
npm run test:ios # Run iOS/Safari compatibility tests only
npx vitest tests/unit/loaders.test.ts # Run specific test filenpm run generate-data # Transform source data into split structure
# Expects data file path as argumentPublishing is fully automated via GitHub Actions on version bumps. Do not run npm publish manually.
The package uses dynamic imports with multi-path fallback to work across different environments:
- Primary: Dynamic
import()for ESM/bundlers - Fallback: Node.js
fs.readFileSync()for CommonJS/serverless - Path Resolution: Tries 3 paths to locate data files:
- Relative to module location (local dev)
- Parent directory relative (bundler variations)
- Absolute through node_modules (Vercel/serverless)
src/
├── index.ts # Public API exports
├── loaders.ts # Core data loading functions with environment detection
├── utils.ts # Helper utilities (validation, search)
├── types.ts # TypeScript interfaces
└── data/
├── countries.json # ~5KB - All countries list
└── {CountryName-CODE}/
├── meta.json # ~5KB - Country metadata
├── states.json # ~10-100KB - States list
└── {StateName-CODE}/
└── cities.json # ~5-200KB - Cities list
Why this structure?
- Enables lazy loading (load only what you need)
- Prevents iOS stack overflow (no massive static imports)
- Keeps initial bundle <10KB
- Allows granular code-splitting
The loaders.ts file uses /* webpackIgnore: true */ to prevent webpack from bundling Node.js modules (fs, path, url):
case 'fs':
return import(/* webpackIgnore: true */ 'fs');Expected behavior: Webpack will show a "Critical dependency" warning - this is harmless and indicates the protection is working.
- Bundles TypeScript code (ESM + CJS)
- Generates type definitions (.d.ts + .d.cts)
- Important:
onSuccesshook copiessrc/data/todist/data/ - Data files are NOT bundled, kept as separate JSON files
function isNodeEnvironment(): boolean {
return typeof process !== 'undefined' &&
process.versions != null &&
process.versions.node != null;
}This prevents errors when Node.js modules aren't available and provides helpful error messages for browser usage.
Users MUST add to next.config.js:
module.exports = {
serverExternalPackages: ['@countrystatecity/countries'],
}Why: Prevents webpack from bundling the package, which would break fs imports.
See docs/VERCEL_DEPLOYMENT.md for full details.
Package cannot run in Vite frontend code. Two solutions documented in docs/VITE_DEPLOYMENT.md:
- Recommended: Use
import.meta.globto load JSON files directly from node_modules - Alternative: Create backend API endpoints that use this package
tests/
├── unit/ # Individual function tests
├── integration/ # Workflow tests (multiple function calls)
└── compatibility/ # iOS/Safari stack overflow prevention tests
iOS Tests: Verify that loading data doesn't cause stack overflow errors on Safari/iOS by testing deep recursive operations.
Automated: GitHub Actions workflow runs weekly (Sundays 00:00 UTC):
- Downloads latest from countries-states-cities-database
- Transforms into split structure via
scripts/generate-data.cjs - Runs tests
- Creates PR if changes detected
Manual: Run npm run generate-data <path-to-source-json> then test and commit.
Cause: Code is running in browser or webpack is trying to bundle
Solution: Ensure webpackIgnore comments are present and user has correct Next.js config
Cause: Data files not included in deployment or path resolution failed
Solution: Check that dist/data/ exists after build; verify multi-path fallback logic
Expected: Package throws clear error explaining it's server-side only
Solution: Point users to docs/VITE_DEPLOYMENT.md for workarounds
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.cjs"
},
"./data/*": "./dist/data/*"
}Direct data access: Users can import JSON directly via @countrystatecity/countries/data/countries.json (useful for Vite import.meta.glob).
All data-related issues (wrong names, missing cities, incorrect coordinates) should be reported to: https://github.com/dr5hn/countries-states-cities-database/issues
This package consumes that data; fixes happen upstream first.
- ci.yml: Runs on every push/PR - builds, tests, type checks
- publish.yml: Triggers on version change in package.json - publishes to npm
- update-data.yml: Weekly schedule + manual trigger - updates data from source
Country/state codes don't always match directory names (e.g., "United States-US" vs "US"). The package:
- Scans
data/directory on first use - Builds maps:
countryCode → directoryName - Caches maps for performance
This allows users to call getStatesOfCountry('US') without knowing the directory is "United States-US".
All data loading is async and on-demand:
getCountries()- 5KBgetStatesOfCountry('US')- Loads only when requestedgetCitiesOfState('US', 'CA')- Loads only specific state
Anti-pattern: getAllCitiesInWorld() exists but loads 8MB+ - document when advising against.
- Use explicit return types for all functions
- Functions include JSDoc with
@bundletag indicating size impact - Prefer
nulloverundefinedfor "not found" cases - Always provide helpful error messages mentioning server-side requirement