postal-code-scraper 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/.mocharc.json +4 -0
  2. package/LICENSE +21 -0
  3. package/README.md +194 -0
  4. package/build/test/src/index.js +26 -0
  5. package/build/test/src/index.js.map +1 -0
  6. package/build/test/src/scraper/fetchers.js +49 -0
  7. package/build/test/src/scraper/fetchers.js.map +1 -0
  8. package/build/test/src/scraper/parsers.js +63 -0
  9. package/build/test/src/scraper/parsers.js.map +1 -0
  10. package/build/test/src/scraper/queue.js +69 -0
  11. package/build/test/src/scraper/queue.js.map +1 -0
  12. package/build/test/src/scraper/scrapers.js +148 -0
  13. package/build/test/src/scraper/scrapers.js.map +1 -0
  14. package/build/test/src/types.js +3 -0
  15. package/build/test/src/types.js.map +1 -0
  16. package/build/test/src/utils/id-generator.js +33 -0
  17. package/build/test/src/utils/id-generator.js.map +1 -0
  18. package/build/test/src/utils/logger.js +87 -0
  19. package/build/test/src/utils/logger.js.map +1 -0
  20. package/build/test/tests/postal-code-scraper.test.js +14 -0
  21. package/build/test/tests/postal-code-scraper.test.js.map +1 -0
  22. package/dist/index.d.ts +3 -0
  23. package/dist/index.js +25 -0
  24. package/dist/scraper/fetchers.d.ts +9 -0
  25. package/dist/scraper/fetchers.js +48 -0
  26. package/dist/scraper/parsers.d.ts +7 -0
  27. package/dist/scraper/parsers.js +62 -0
  28. package/dist/scraper/queue.d.ts +12 -0
  29. package/dist/scraper/queue.js +67 -0
  30. package/dist/scraper/scrapers.d.ts +19 -0
  31. package/dist/scraper/scrapers.js +149 -0
  32. package/dist/types.d.ts +32 -0
  33. package/dist/types.js +2 -0
  34. package/dist/utils/env-config.d.ts +1 -0
  35. package/dist/utils/env-config.js +7 -0
  36. package/dist/utils/id-generator.d.ts +4 -0
  37. package/dist/utils/id-generator.js +26 -0
  38. package/dist/utils/logger.d.ts +33 -0
  39. package/dist/utils/logger.js +86 -0
  40. package/dist/utils/string-utils.d.ts +1 -0
  41. package/dist/utils/string-utils.js +13 -0
  42. package/package.json +61 -0
  43. package/src/index.ts +3 -0
  44. package/src/scraper/fetchers.ts +30 -0
  45. package/src/scraper/parsers.ts +67 -0
  46. package/src/scraper/queue.ts +55 -0
  47. package/src/scraper/scrapers.ts +143 -0
  48. package/src/types.ts +37 -0
  49. package/src/utils/env-config.ts +3 -0
  50. package/src/utils/id-generator.ts +35 -0
  51. package/src/utils/logger.ts +105 -0
  52. package/src/utils/string-utils.ts +9 -0
  53. package/tests/postal-code-scraper.test.ts +100 -0
  54. package/tests/tsconfig.json +13 -0
  55. package/tsconfig.json +15 -0
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Logger = void 0;
4
+ class Logger {
5
+ static configure(config) {
6
+ if (config.level)
7
+ this.logLevel = config.level;
8
+ if (config.colors !== undefined)
9
+ this.useColors = config.colors;
10
+ if (config.prefix)
11
+ this.prefix = config.prefix;
12
+ if (config.logger)
13
+ this.instance = config.logger;
14
+ }
15
+ static getInstance() {
16
+ return this.instance || new Logger();
17
+ }
18
+ static debug(message, ...args) {
19
+ this.log("debug", message, args);
20
+ }
21
+ static info(message, ...args) {
22
+ this.log("info", message, args);
23
+ }
24
+ static warn(message, ...args) {
25
+ this.log("warn", message, args);
26
+ }
27
+ static error(message, ...args) {
28
+ this.log("error", message, args);
29
+ }
30
+ static shouldLog(level) {
31
+ if (this.logLevel === "silent")
32
+ return false;
33
+ const levels = ["error", "warn", "info", "debug"];
34
+ return levels.indexOf(level) <= levels.indexOf(this.logLevel);
35
+ }
36
+ static log(level, message, args) {
37
+ if (!this.shouldLog(level))
38
+ return;
39
+ const logger = this.getInstance();
40
+ const formatted = this.formatMessage(level, message);
41
+ logger[level](formatted, ...args);
42
+ }
43
+ static formatMessage(level, message) {
44
+ const timestamp = new Date().toISOString();
45
+ const levelColor = this.getLevelColor(level);
46
+ const messageColor = this.useColors ? "\x1b[37m" : "";
47
+ return [
48
+ this.useColors ? "\x1b[90m" : "",
49
+ `${this.prefix} `,
50
+ `${timestamp} `,
51
+ levelColor,
52
+ `[${level.toUpperCase()}]`,
53
+ this.useColors ? "\x1b[0m" : "",
54
+ messageColor,
55
+ ` ${message}`,
56
+ this.useColors ? "\x1b[0m" : "",
57
+ ].join("");
58
+ }
59
+ static getLevelColor(level) {
60
+ if (!this.useColors)
61
+ return "";
62
+ return {
63
+ error: "\x1b[31m", // Red
64
+ warn: "\x1b[33m", // Yellow
65
+ info: "\x1b[36m", // Cyan
66
+ debug: "\x1b[35m", // Magenta
67
+ }[level];
68
+ }
69
+ // Instance methods to implement LoggerInterface
70
+ debug(message, ...args) {
71
+ console.debug(message, ...args);
72
+ }
73
+ info(message, ...args) {
74
+ console.log(message, ...args);
75
+ }
76
+ warn(message, ...args) {
77
+ console.warn(message, ...args);
78
+ }
79
+ error(message, ...args) {
80
+ console.error(message, ...args);
81
+ }
82
+ }
83
+ exports.Logger = Logger;
84
+ Logger.logLevel = "info";
85
+ Logger.useColors = true;
86
+ Logger.prefix = "[POSTAL-CODE-SCRAPER]";
@@ -0,0 +1 @@
1
+ export declare const normalizeString: (str: string) => string;
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.normalizeString = void 0;
4
+ const normalizeString = (str) => {
5
+ return str
6
+ .trim()
7
+ .toLowerCase()
8
+ .normalize("NFD")
9
+ .replace(/[\u0300-\u036f]/g, "")
10
+ .replace(/\s+/g, "-")
11
+ .replace(/[^a-z0-9.-]/g, "");
12
+ };
13
+ exports.normalizeString = normalizeString;
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "postal-code-scraper",
3
+ "version": "1.0.2",
4
+ "description": "A tool for scraping country data, including regions and their postal codes",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "start": "tsx src/index.ts",
9
+ "prebuild": "shx rm -rf dist",
10
+ "build": "tsc",
11
+ "pretest": "npm run build",
12
+ "test": "mocha -r tsx tests/**/*.test.ts",
13
+ "prepare": "npm run build",
14
+ "pack": "npm pack",
15
+ "prepublishOnly": "npm test"
16
+ },
17
+ "type": "module",
18
+ "devDependencies": {
19
+ "@types/chai": "^5.0.1",
20
+ "@types/cheerio": "^0.22.35",
21
+ "@types/mocha": "^10.0.10",
22
+ "@types/node": "^22.13.9",
23
+ "@types/p-limit": "^2.1.0",
24
+ "@types/puppeteer": "^5.4.7",
25
+ "@types/sinon": "^17.0.4",
26
+ "chai": "^5.2.0",
27
+ "mocha": "^11.1.0",
28
+ "sinon": "^19.0.2",
29
+ "tslib": "^2.8.1",
30
+ "tsx": "^3.14.0",
31
+ "typescript": "^5.8.2"
32
+ },
33
+ "dependencies": {
34
+ "cheerio": "^1.0.0",
35
+ "p-limit": "^6.2.0",
36
+ "puppeteer": "^24.2.1"
37
+ },
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "git+https://github.com/sasiasas/postal-code-scraper.git"
41
+ },
42
+ "keywords": [
43
+ "postal-codes",
44
+ "scraper",
45
+ "geodata",
46
+ "zip-codes",
47
+ "region",
48
+ "country",
49
+ "county",
50
+ "location",
51
+ "address"
52
+ ],
53
+ "author": "Sallai Tamás-Levente",
54
+ "license": "MIT",
55
+ "bugs": {
56
+ "url": "https://github.com/sasiasas/postal-code-scraper/issues"
57
+ },
58
+ "engines": {
59
+ "node": ">=18"
60
+ }
61
+ }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from "./types";
2
+ export { PostalCodeScraper } from "./scraper/scrapers";
3
+ export { default } from "./scraper/scrapers";
@@ -0,0 +1,30 @@
1
+ import { Browser } from "puppeteer";
2
+ import { ScraperConfig } from "../types";
3
+
4
+ export class Fetcher {
5
+ constructor(private browser: Browser, private config: ScraperConfig) {}
6
+
7
+ async fetchHtml(url: string): Promise<string> {
8
+ const page = await this.browser.newPage();
9
+ try {
10
+ page.setDefaultNavigationTimeout(60000);
11
+ await page.goto(url, { waitUntil: "domcontentloaded" });
12
+ return await page.content();
13
+ } finally {
14
+ await page.close();
15
+ }
16
+ }
17
+
18
+ async fetchWithRetry(url: string, retries = this.config.maxRetries || 5): Promise<string> {
19
+ try {
20
+ return await this.fetchHtml(url);
21
+ } catch (error) {
22
+ this.config.logger?.warn(`Retrying (${this.config.maxRetries! - retries + 1}) for: ${url}`);
23
+ if (retries > 0) {
24
+ await new Promise((resolve) => setTimeout(resolve, Math.random() * 7000 + 5000));
25
+ return this.fetchWithRetry(url, retries - 1);
26
+ }
27
+ throw new Error(`Failed to fetch: ${url} after ${this.config.maxRetries} attempts`);
28
+ }
29
+ }
30
+ }
@@ -0,0 +1,67 @@
1
+ import { Region, ScraperConfig } from "../types";
2
+
3
+ export class Parser {
4
+ static parseRegions($: cheerio.Root, config: ScraperConfig): Region[] {
5
+ return $("h2:contains('Regions')")
6
+ .next(".regions")
7
+ .find("a")
8
+ .map((_index, element) => {
9
+ const path = $(element).attr("href");
10
+ const prettyName = $(element).text().trim();
11
+ if (!path || !prettyName) return null;
12
+
13
+ return {
14
+ name: path.split("/").filter(Boolean).pop()!,
15
+ prettyName,
16
+ path,
17
+ };
18
+ })
19
+ .get()
20
+ .filter(Boolean) as Region[];
21
+ }
22
+
23
+ static parsePostalCodes($: cheerio.Root, config: ScraperConfig): Record<string, string[]> {
24
+ const codes: Record<string, string[]> = {};
25
+
26
+ $(".codes .container").each((_i, element) => {
27
+ const place = $(element).find(".place").text().trim();
28
+ const codesList = $(element)
29
+ .find(".code span")
30
+ .map((_j, el) => $(el).text().trim())
31
+ .get();
32
+
33
+ if (place) {
34
+ const key = config.usePrettyName ? place : place.toLowerCase().replace(/\s+/g, "-");
35
+ codes[key] = codesList;
36
+ }
37
+ });
38
+
39
+ return codes;
40
+ }
41
+
42
+ static parseCountries($: cheerio.Root, config: ScraperConfig): Region[] {
43
+ return $(".regions div a")
44
+ .map((_i, element) => {
45
+ const path = $(element).attr("href");
46
+ return path ? { name: path.replace(/\//g, ""), prettyName: $(element).text().trim(), path } : null;
47
+ })
48
+ .get()
49
+ .filter(Boolean) as Region[];
50
+ }
51
+
52
+ static parseCountryByName($: cheerio.Root, config: ScraperConfig, name: string): Region | null {
53
+ const countryElement = $(`.regions div a`).filter((_, el) => $(el).attr("href")?.replace(/\//g, "") === name.toLowerCase().trim());
54
+
55
+ if (!countryElement.length) return null;
56
+
57
+ const path = countryElement.attr("href");
58
+ const prettyName = countryElement.text().trim();
59
+ return path && prettyName
60
+ ? {
61
+ name: path.replace(/\//g, ""),
62
+ prettyName,
63
+ path,
64
+ }
65
+ : null;
66
+ }
67
+ }
@@ -0,0 +1,55 @@
1
+ import { load } from "cheerio";
2
+ import { Region, ProcessingQueueItem, ScraperConfig, RegionData } from "../types";
3
+ import { Fetcher } from "./fetchers";
4
+ import pLimit from "p-limit";
5
+ import { Parser } from "./parsers";
6
+ import { getBaseUrl } from "../utils/env-config";
7
+
8
+ export class ProcessingQueue {
9
+ private queue: ProcessingQueueItem[] = [];
10
+ private visitedUrls = new Set<string>();
11
+ private limit: ReturnType<typeof pLimit>;
12
+
13
+ constructor(private fetcher: Fetcher, private config: ScraperConfig) {
14
+ this.limit = pLimit(config.concurrency || 15);
15
+ }
16
+
17
+ async process(startRegion: Region, data: RegionData): Promise<void> {
18
+ this.queue.push({ region: startRegion, currData: data });
19
+
20
+ while (this.queue.length > 0) {
21
+ const tasks = this.queue.map((item) => this.limit(() => this.processItem(item)));
22
+ this.queue = [];
23
+ await Promise.all(tasks);
24
+ }
25
+ }
26
+
27
+ private async processItem(item: ProcessingQueueItem): Promise<void> {
28
+ const url = `${getBaseUrl()}${item.region.path}`;
29
+
30
+ if (this.visitedUrls.has(url)) return;
31
+ this.visitedUrls.add(url);
32
+
33
+ this.config.logger?.info(`Fetching: ${url}`);
34
+
35
+ try {
36
+ const html = await this.fetcher.fetchWithRetry(url);
37
+ const $ = load(html);
38
+
39
+ const regions = Parser.parseRegions($, this.config);
40
+ regions.forEach((region) => {
41
+ const key = this.config.usePrettyName ? region.prettyName : region.name;
42
+ item.currData[key] = {};
43
+ this.queue.push({
44
+ region,
45
+ currData: item.currData[key],
46
+ });
47
+ });
48
+
49
+ const codes = Parser.parsePostalCodes($, this.config);
50
+ Object.assign(item.currData, codes);
51
+ } catch (error) {
52
+ this.config.logger?.error(`Error processing ${url}:`, error);
53
+ }
54
+ }
55
+ }
@@ -0,0 +1,143 @@
1
+ import path from "path";
2
+ import puppeteer, { Browser } from "puppeteer";
3
+ import { ProcessingQueue } from "./queue";
4
+ import { Fetcher } from "./fetchers";
5
+ import { Region, ScraperConfig, LookupData, RegionData } from "../types";
6
+ import { createRegionIdGenerator, RegionIdGenerator } from "../utils/id-generator";
7
+ import { writeFileSync, mkdirSync } from "fs";
8
+ import { load } from "cheerio";
9
+ import { Parser } from "./parsers";
10
+ import { getBaseUrl } from "../utils/env-config";
11
+ import { normalizeString } from "../utils/string-utils";
12
+ import { Logger } from "../utils/logger";
13
+
14
+ export class PostalCodeScraper {
15
+ private browser!: Browser;
16
+ private queue!: ProcessingQueue;
17
+ private fetcher!: Fetcher;
18
+
19
+ constructor(private config: ScraperConfig = {}) {
20
+ this.config = {
21
+ concurrency: 15,
22
+ maxRetries: 5,
23
+ headless: true,
24
+ directory: "src/data",
25
+ logger: Logger,
26
+ usePrettyName: false,
27
+ ...config,
28
+ };
29
+ }
30
+
31
+ async scrapeCountry(countryName: string) {
32
+ await this.initBrowser();
33
+ try {
34
+ const country = await this.getCountryDetails(countryName);
35
+ if (!country) {
36
+ this.config.logger?.warn(`Country not found: ${countryName}`);
37
+ return null;
38
+ }
39
+
40
+ const data: RegionData = {};
41
+ await this.queue.process(country, data);
42
+
43
+ this.saveData(data, `${country.name}-postal-codes.json`, this.config.directory!);
44
+
45
+ const postalCodeLookup = this.generatePostalCodeLookup(data);
46
+ this.saveData(postalCodeLookup, `${country.name}-lookup.json`, this.config.directory!);
47
+ } finally {
48
+ await this.cleanup();
49
+ }
50
+ }
51
+
52
+ async scrapeCountries() {
53
+ await this.initBrowser();
54
+ try {
55
+ const countries = await this.getCountriesDetails();
56
+ if (countries.length === 0) {
57
+ this.config.logger?.warn("No countries found.");
58
+ return null;
59
+ }
60
+
61
+ for (const country of countries) {
62
+ const key = this.config.usePrettyName ? country.prettyName : country.name;
63
+ const countryData: RegionData = {};
64
+ this.config.logger?.info(`Processing country: ${key}`);
65
+
66
+ await this.queue.process(country, countryData);
67
+ this.saveData(countryData, `${key}-postal-codes.json`, this.config.directory!);
68
+
69
+ const postalCodeLookup = this.generatePostalCodeLookup(countryData);
70
+ this.saveData(postalCodeLookup, `${key}-lookup.json`, this.config.directory!);
71
+ }
72
+ } finally {
73
+ await this.cleanup();
74
+ }
75
+ }
76
+
77
+ private async initBrowser() {
78
+ this.browser = await puppeteer.launch({ headless: this.config.headless });
79
+ this.fetcher = new Fetcher(this.browser, this.config);
80
+ this.queue = new ProcessingQueue(this.fetcher, this.config);
81
+ }
82
+
83
+ private async getCountryDetails(name: string): Promise<Region | null> {
84
+ try {
85
+ const html = await this.fetcher.fetchWithRetry(getBaseUrl());
86
+ return Parser.parseCountryByName(load(html), this.config, name);
87
+ } catch (error) {
88
+ this.config.logger?.error(`Error fetching country details: ${name}`, error);
89
+ return null;
90
+ }
91
+ }
92
+
93
+ private async getCountriesDetails(): Promise<Region[]> {
94
+ try {
95
+ const html = await this.fetcher.fetchWithRetry(getBaseUrl());
96
+ return Parser.parseCountries(load(html), this.config);
97
+ } catch (error) {
98
+ this.config.logger?.error("Error fetching countries details", error);
99
+ return [];
100
+ }
101
+ }
102
+
103
+ private generatePostalCodeLookup(data: RegionData): LookupData {
104
+ return this.buildLookup(data, createRegionIdGenerator());
105
+ }
106
+
107
+ private buildLookup(
108
+ regionObj: RegionData | string[],
109
+ idGenerator: RegionIdGenerator,
110
+ acc: string[] = [],
111
+ result: LookupData = { postalCodeMap: {}, regions: {} }
112
+ ): LookupData {
113
+ if (Array.isArray(regionObj)) {
114
+ for (const item of regionObj) {
115
+ const id = idGenerator(acc);
116
+ result.postalCodeMap[item] = id;
117
+ result.regions[id] = [...acc];
118
+ }
119
+ } else if (typeof regionObj === "object" && regionObj !== null) {
120
+ for (const [regionKey, regionValue] of Object.entries(regionObj)) {
121
+ this.buildLookup(regionValue, idGenerator, [...acc, regionKey], result);
122
+ }
123
+ }
124
+ return result;
125
+ }
126
+
127
+ private saveData(data: any, fileName: string, directory: string = "src/data") {
128
+ try {
129
+ mkdirSync(directory, { recursive: true });
130
+ const filePath = path.join(directory, normalizeString(fileName));
131
+ writeFileSync(filePath, JSON.stringify(data, null, 2), { flag: "w" });
132
+ this.config.logger?.info(`Saved data to ${filePath}`);
133
+ } catch (error) {
134
+ this.config.logger?.error(`Error saving data to ${fileName}`, error);
135
+ }
136
+ }
137
+
138
+ private async cleanup() {
139
+ await this.browser?.close();
140
+ }
141
+ }
142
+
143
+ export default new PostalCodeScraper();
package/src/types.ts ADDED
@@ -0,0 +1,37 @@
1
+ export type Region = {
2
+ path: string;
3
+ name: string;
4
+ prettyName: string;
5
+ };
6
+
7
+ export type ScraperConfig = {
8
+ usePrettyName?: boolean;
9
+ directory?: string;
10
+ concurrency?: number;
11
+ maxRetries?: number;
12
+ headless?: boolean;
13
+ logger?: any;
14
+ };
15
+
16
+ export type ProcessingQueueItem = {
17
+ region: Region;
18
+ currData: RegionData;
19
+ };
20
+
21
+ export interface LookupData {
22
+ postalCodeMap: {
23
+ [postalCode: string]: string;
24
+ };
25
+ regions: {
26
+ [code: string]: string[];
27
+ };
28
+ }
29
+
30
+ export interface PostalCodeData {
31
+ rawData: RegionData;
32
+ postalCodeLookup: LookupData;
33
+ }
34
+
35
+ export interface RegionData {
36
+ [key: string]: RegionData | string[];
37
+ }
@@ -0,0 +1,3 @@
1
+ export const getBaseUrl = () => {
2
+ return "https://worldpostalcode.com";
3
+ };
@@ -0,0 +1,35 @@
1
+ export interface RegionIdGenerator {
2
+ (regions: string[]): string;
3
+ }
4
+
5
+ export const createRegionIdGenerator = (): RegionIdGenerator => {
6
+ const regionRegistry = new Map<string, string>();
7
+ const counterMap = new Map<string, number>();
8
+
9
+ return (regions: string[]): string => {
10
+ const normalized = regions.map((region) =>
11
+ region
12
+ .trim()
13
+ .toLowerCase()
14
+ .normalize("NFD")
15
+ .replace(/[\u0300-\u036f]/g, "")
16
+ .replace(/\s+/g, "_")
17
+ );
18
+
19
+ const compositeKey = normalized.join("|");
20
+
21
+ if (regionRegistry.has(compositeKey)) {
22
+ return regionRegistry.get(compositeKey)!;
23
+ }
24
+
25
+ const baseName = normalized[normalized.length - 1];
26
+ const count = (counterMap.get(baseName) || 0) + 1;
27
+ counterMap.set(baseName, count);
28
+
29
+ const newId = `${baseName}_${count}`;
30
+
31
+ regionRegistry.set(compositeKey, newId);
32
+
33
+ return newId;
34
+ };
35
+ };
@@ -0,0 +1,105 @@
1
+ export type LogMethod = "error" | "warn" | "info" | "debug";
2
+ export type LogLevel = LogMethod | "silent";
3
+
4
+ export interface LoggerInterface {
5
+ debug(message: string, ...args: any[]): void;
6
+ info(message: string, ...args: any[]): void;
7
+ warn(message: string, ...args: any[]): void;
8
+ error(message: string, ...args: any[]): void;
9
+ }
10
+
11
+ export class Logger implements LoggerInterface {
12
+ private static logLevel: LogLevel = "info";
13
+ private static useColors: boolean = true;
14
+ private static prefix: string = "[POSTAL-CODE-SCRAPER]";
15
+ private static instance: LoggerInterface;
16
+
17
+ static configure(config: { level?: LogLevel; colors?: boolean; prefix?: string; logger?: LoggerInterface }): void {
18
+ if (config.level) this.logLevel = config.level;
19
+ if (config.colors !== undefined) this.useColors = config.colors;
20
+ if (config.prefix) this.prefix = config.prefix;
21
+ if (config.logger) this.instance = config.logger;
22
+ }
23
+
24
+ static getInstance(): LoggerInterface {
25
+ return this.instance || new Logger();
26
+ }
27
+
28
+ static debug(message: string, ...args: any[]): void {
29
+ this.log("debug", message, args);
30
+ }
31
+
32
+ static info(message: string, ...args: any[]): void {
33
+ this.log("info", message, args);
34
+ }
35
+
36
+ static warn(message: string, ...args: any[]): void {
37
+ this.log("warn", message, args);
38
+ }
39
+
40
+ static error(message: string, ...args: any[]): void {
41
+ this.log("error", message, args);
42
+ }
43
+
44
+ private static shouldLog(level: LogLevel): boolean {
45
+ if (this.logLevel === "silent") return false;
46
+
47
+ const levels: LogMethod[] = ["error", "warn", "info", "debug"];
48
+ return levels.indexOf(level as LogMethod) <= levels.indexOf(this.logLevel as LogMethod);
49
+ }
50
+
51
+ private static log(level: LogMethod, message: string, args: any[]): void {
52
+ if (!this.shouldLog(level)) return;
53
+
54
+ const logger = this.getInstance();
55
+ const formatted = this.formatMessage(level, message);
56
+
57
+ logger[level](formatted, ...args);
58
+ }
59
+
60
+ private static formatMessage(level: LogMethod, message: string): string {
61
+ const timestamp = new Date().toISOString();
62
+ const levelColor = this.getLevelColor(level);
63
+ const messageColor = this.useColors ? "\x1b[37m" : "";
64
+
65
+ return [
66
+ this.useColors ? "\x1b[90m" : "",
67
+ `${this.prefix} `,
68
+ `${timestamp} `,
69
+ levelColor,
70
+ `[${level.toUpperCase()}]`,
71
+ this.useColors ? "\x1b[0m" : "",
72
+ messageColor,
73
+ ` ${message}`,
74
+ this.useColors ? "\x1b[0m" : "",
75
+ ].join("");
76
+ }
77
+
78
+ private static getLevelColor(level: LogMethod): string {
79
+ if (!this.useColors) return "";
80
+
81
+ return {
82
+ error: "\x1b[31m", // Red
83
+ warn: "\x1b[33m", // Yellow
84
+ info: "\x1b[36m", // Cyan
85
+ debug: "\x1b[35m", // Magenta
86
+ }[level];
87
+ }
88
+
89
+ // Instance methods to implement LoggerInterface
90
+ debug(message: string, ...args: any[]): void {
91
+ console.debug(message, ...args);
92
+ }
93
+
94
+ info(message: string, ...args: any[]): void {
95
+ console.log(message, ...args);
96
+ }
97
+
98
+ warn(message: string, ...args: any[]): void {
99
+ console.warn(message, ...args);
100
+ }
101
+
102
+ error(message: string, ...args: any[]): void {
103
+ console.error(message, ...args);
104
+ }
105
+ }
@@ -0,0 +1,9 @@
1
+ export const normalizeString = (str: string): string => {
2
+ return str
3
+ .trim()
4
+ .toLowerCase()
5
+ .normalize("NFD")
6
+ .replace(/[\u0300-\u036f]/g, "")
7
+ .replace(/\s+/g, "-")
8
+ .replace(/[^a-z0-9.-]/g, "");
9
+ };