hanzio 1.0.0 → 1.0.3

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.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  A small TypeScript-first utility library for common application code: arrays,
4
4
  strings, promises, caching, state machines, dates, URLs, Zod helpers, API clients,
5
- JWTs, gzip, sitemap crawling, and queues.
5
+ JWTs, gzip, sitemap crawling, queues, and colorful terminal logging.
6
6
 
7
7
  ## Install
8
8
 
@@ -30,6 +30,7 @@ import { jwtSign } from 'hanzio/jwt'
30
30
  import { compressString } from 'hanzio/gzip'
31
31
  import { getDomainSitemap } from 'hanzio/sitemap'
32
32
  import PQueue from 'hanzio/p-queue'
33
+ import { createCoolLogger } from 'hanzio/cool-console-log'
33
34
  ```
34
35
 
35
36
  The package currently publishes `src/**/*.ts` intentionally so Bun can consume
@@ -500,6 +501,34 @@ try {
500
501
  }
501
502
  ```
502
503
 
504
+ ### Cool console log
505
+
506
+ ANSI-colored logging for terminals. Exports include `terminalColors`, `createCoolLogger`,
507
+ `logOperationSummary`, `colorize`, and `logBanner`. By default colors and banners are
508
+ development-only (`NODE_ENV !== 'production'`); loggers honor `colorsInDevOnly`
509
+ and callbacks via `CoolLoggerOptions.onLog`.
510
+
511
+ ```ts
512
+ import {
513
+ createCoolLogger,
514
+ colorize,
515
+ logBanner,
516
+ logOperationSummary,
517
+ terminalColors
518
+ } from 'hanzio'
519
+
520
+ const log = createCoolLogger()
521
+ log.info('User logged in', { userId: '123' })
522
+ log.warn('Rate limit approaching', { remaining: 10 })
523
+
524
+ const start = performance.now()
525
+ await doSomething()
526
+ logOperationSummary('user.create', performance.now() - start, true)
527
+
528
+ console.log(colorize('OK', 'brightGreen'))
529
+ logBanner('Server started', 'cyan')
530
+ ```
531
+
503
532
  ## Development
504
533
 
505
534
  ```bash
@@ -0,0 +1,108 @@
1
+ /** ANSI color codes for terminal output */
2
+ export declare const terminalColors: {
3
+ readonly reset: "\u001B[0m";
4
+ readonly bold: "\u001B[1m";
5
+ readonly dim: "\u001B[2m";
6
+ readonly italic: "\u001B[3m";
7
+ readonly underline: "\u001B[4m";
8
+ readonly black: "\u001B[30m";
9
+ readonly red: "\u001B[31m";
10
+ readonly green: "\u001B[32m";
11
+ readonly yellow: "\u001B[33m";
12
+ readonly blue: "\u001B[34m";
13
+ readonly magenta: "\u001B[35m";
14
+ readonly cyan: "\u001B[36m";
15
+ readonly white: "\u001B[37m";
16
+ readonly gray: "\u001B[90m";
17
+ readonly brightRed: "\u001B[91m";
18
+ readonly brightGreen: "\u001B[92m";
19
+ readonly brightYellow: "\u001B[93m";
20
+ readonly brightBlue: "\u001B[94m";
21
+ readonly brightMagenta: "\u001B[95m";
22
+ readonly brightCyan: "\u001B[96m";
23
+ readonly brightWhite: "\u001B[97m";
24
+ readonly bgBlack: "\u001B[40m";
25
+ readonly bgRed: "\u001B[41m";
26
+ readonly bgGreen: "\u001B[42m";
27
+ readonly bgYellow: "\u001B[43m";
28
+ readonly bgBlue: "\u001B[44m";
29
+ readonly bgMagenta: "\u001B[45m";
30
+ readonly bgCyan: "\u001B[46m";
31
+ readonly bgWhite: "\u001B[47m";
32
+ };
33
+ export type TerminalColor = keyof typeof terminalColors;
34
+ /** Log levels supported by the logger */
35
+ export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
36
+ /** Attributes that can be passed to log methods */
37
+ export type LogAttributes = Record<string, string | number | boolean>;
38
+ /** Log method signature */
39
+ export type LogMethod = (message: string, attributes?: LogAttributes) => void;
40
+ /** Logger interface with colored output */
41
+ export type CoolLogger = {
42
+ debug: LogMethod;
43
+ info: LogMethod;
44
+ warn: LogMethod;
45
+ error: LogMethod;
46
+ };
47
+ /**
48
+ * Options for creating a cool logger
49
+ */
50
+ export type CoolLoggerOptions = {
51
+ /** Only use colors in development (default: true) */
52
+ colorsInDevOnly?: boolean;
53
+ /** Callback for each log entry (e.g., to add to OpenTelemetry span) */
54
+ onLog?: (level: LogLevel, message: string, attributes?: LogAttributes) => void;
55
+ };
56
+ /**
57
+ * Creates a logger with colorful console output.
58
+ *
59
+ * @example
60
+ * ```ts
61
+ * const logger = createCoolLogger()
62
+ * logger.info('User logged in', { userId: '123' })
63
+ * logger.warn('Rate limit approaching', { remaining: 10 })
64
+ * logger.error('Payment failed', { reason: 'insufficient_funds' })
65
+ * ```
66
+ */
67
+ export declare function createCoolLogger(options?: CoolLoggerOptions): CoolLogger;
68
+ /**
69
+ * Logs a request/operation summary with colored status and duration.
70
+ * Shows: `operationName ✓ · 12.3ms` (green) or `operationName ✗ · 45.2ms` (red)
71
+ *
72
+ * @example
73
+ * ```ts
74
+ * const start = performance.now()
75
+ * try {
76
+ * await doSomething()
77
+ * logOperationSummary('user.create', performance.now() - start, true)
78
+ * } catch {
79
+ * logOperationSummary('user.create', performance.now() - start, false)
80
+ * }
81
+ * ```
82
+ */
83
+ export declare function logOperationSummary(operationName: string, durationMs: number, success: boolean, options?: {
84
+ devOnly?: boolean;
85
+ error?: Error | unknown;
86
+ }): void;
87
+ /**
88
+ * Helper to colorize text for console output
89
+ *
90
+ * @example
91
+ * ```ts
92
+ * console.log(colorize('Success!', 'green', 'bold'))
93
+ * console.log(colorize('Warning', 'yellow'))
94
+ * ```
95
+ */
96
+ export declare function colorize(text: string, color: TerminalColor, ...modifiers: TerminalColor[]): string;
97
+ /**
98
+ * Log a styled box/banner to the console (dev only)
99
+ *
100
+ * @example
101
+ * ```ts
102
+ * logBanner('Server Started', 'green')
103
+ * logBanner('⚠️ Warning', 'yellow')
104
+ * ```
105
+ */
106
+ export declare function logBanner(text: string, color?: TerminalColor, options?: {
107
+ devOnly?: boolean;
108
+ }): void;
@@ -0,0 +1,111 @@
1
+ // src/cool-console-log/index.ts
2
+ var terminalColors = {
3
+ reset: "\x1B[0m",
4
+ bold: "\x1B[1m",
5
+ dim: "\x1B[2m",
6
+ italic: "\x1B[3m",
7
+ underline: "\x1B[4m",
8
+ black: "\x1B[30m",
9
+ red: "\x1B[31m",
10
+ green: "\x1B[32m",
11
+ yellow: "\x1B[33m",
12
+ blue: "\x1B[34m",
13
+ magenta: "\x1B[35m",
14
+ cyan: "\x1B[36m",
15
+ white: "\x1B[37m",
16
+ gray: "\x1B[90m",
17
+ brightRed: "\x1B[91m",
18
+ brightGreen: "\x1B[92m",
19
+ brightYellow: "\x1B[93m",
20
+ brightBlue: "\x1B[94m",
21
+ brightMagenta: "\x1B[95m",
22
+ brightCyan: "\x1B[96m",
23
+ brightWhite: "\x1B[97m",
24
+ bgBlack: "\x1B[40m",
25
+ bgRed: "\x1B[41m",
26
+ bgGreen: "\x1B[42m",
27
+ bgYellow: "\x1B[43m",
28
+ bgBlue: "\x1B[44m",
29
+ bgMagenta: "\x1B[45m",
30
+ bgCyan: "\x1B[46m",
31
+ bgWhite: "\x1B[47m"
32
+ };
33
+ var levelColors = {
34
+ debug: { badge: terminalColors.gray, text: terminalColors.dim },
35
+ info: { badge: terminalColors.cyan, text: terminalColors.reset },
36
+ warn: { badge: terminalColors.yellow, text: terminalColors.yellow },
37
+ error: { badge: terminalColors.red, text: terminalColors.brightRed }
38
+ };
39
+ function formatAttributes(attrs) {
40
+ if (!attrs || Object.keys(attrs).length === 0)
41
+ return "";
42
+ const formatted = Object.entries(attrs).map(([k, v]) => `${terminalColors.dim}${k}=${terminalColors.reset}${v}`).join(" ");
43
+ return ` ${formatted}`;
44
+ }
45
+ function createCoolLogger(options = {}) {
46
+ const { colorsInDevOnly = true, onLog } = options;
47
+ const isDev = true;
48
+ const useColors = colorsInDevOnly ? isDev : true;
49
+ const createLogMethod = (level) => (message, attributes) => {
50
+ onLog?.(level, message, attributes);
51
+ if (level === "debug" && !isDev)
52
+ return;
53
+ if (useColors) {
54
+ const { badge, text } = levelColors[level];
55
+ const label = level.toUpperCase().padEnd(5);
56
+ const attrStr = formatAttributes(attributes);
57
+ console.log(`${badge}[${label}]${terminalColors.reset} ${text}${message}${terminalColors.reset}${attrStr}`);
58
+ } else {
59
+ console[level](`[${level.toUpperCase()}]`, message, attributes ?? "");
60
+ }
61
+ };
62
+ return {
63
+ debug: createLogMethod("debug"),
64
+ info: createLogMethod("info"),
65
+ warn: createLogMethod("warn"),
66
+ error: createLogMethod("error")
67
+ };
68
+ }
69
+ function logOperationSummary(operationName, durationMs, success, options = {}) {
70
+ const { devOnly = true, error } = options;
71
+ const isDev = true;
72
+ if (devOnly && !isDev)
73
+ return;
74
+ const c = terminalColors;
75
+ const statusIcon = success ? `${c.brightGreen}✓${c.reset}` : `${c.brightRed}✗${c.reset}`;
76
+ const statusColor = success ? c.green : c.red;
77
+ const durationStr = `${c.dim}${durationMs.toFixed(1)}ms${c.reset}`;
78
+ const errorMessage = error ? ` ${c.dim}·${c.reset} ${c.red}${error instanceof Error ? error.message : String(error)}${c.reset}` : "";
79
+ console.log(`${statusColor}${c.bold}${operationName}${c.reset} ${statusIcon} ${c.dim}·${c.reset} ${durationStr}${errorMessage}`);
80
+ }
81
+ function colorize(text, color, ...modifiers) {
82
+ const isDev = true;
83
+ if (!isDev)
84
+ return text;
85
+ const colorCodes = [
86
+ terminalColors[color],
87
+ ...modifiers.map((m) => terminalColors[m])
88
+ ].join("");
89
+ return `${colorCodes}${text}${terminalColors.reset}`;
90
+ }
91
+ function logBanner(text, color = "cyan", options = {}) {
92
+ const { devOnly = true } = options;
93
+ const isDev = true;
94
+ if (devOnly && !isDev) {
95
+ console.log(`=== ${text} ===`);
96
+ return;
97
+ }
98
+ const c = terminalColors;
99
+ const padding = 2;
100
+ const line = "═".repeat(text.length + padding * 2);
101
+ console.log(`${c[color]}╔${line}╗${c.reset}`);
102
+ console.log(`${c[color]}║${" ".repeat(padding)}${c.bold}${text}${c.reset}${c[color]}${" ".repeat(padding)}║${c.reset}`);
103
+ console.log(`${c[color]}╚${line}╝${c.reset}`);
104
+ }
105
+ export {
106
+ terminalColors,
107
+ logOperationSummary,
108
+ logBanner,
109
+ createCoolLogger,
110
+ colorize
111
+ };
package/dist/index.d.ts CHANGED
@@ -10,3 +10,7 @@ export * from './url';
10
10
  export * from './zod';
11
11
  export * from './api-wrapper';
12
12
  export * from './p-queue';
13
+ export * from './cool-console-log';
14
+ export * from './jwt';
15
+ export * from './gzip';
16
+ export * from './sitemap';
package/dist/index.js CHANGED
@@ -879,6 +879,150 @@ class PQueue {
879
879
  }
880
880
  }
881
881
 
882
+ // src/jwt/index.ts
883
+ import * as jose from "jose";
884
+ async function jwtSign(payload, secret) {
885
+ const secretKey = new TextEncoder().encode(secret);
886
+ return await new jose.SignJWT(payload).setProtectedHeader({ alg: "HS256" }).setIssuedAt().sign(secretKey);
887
+ }
888
+ async function jwtVerify2(token, secret) {
889
+ try {
890
+ const secretKey = new TextEncoder().encode(secret);
891
+ const { payload } = await jose.jwtVerify(token, secretKey);
892
+ return payload;
893
+ } catch (_error) {
894
+ return null;
895
+ }
896
+ }
897
+
898
+ // src/gzip/index.ts
899
+ import pako from "pako";
900
+ function compressString(input) {
901
+ const uint8Array = new TextEncoder().encode(input);
902
+ const compressed = pako.deflate(uint8Array);
903
+ return btoa(String.fromCharCode.apply(null, compressed));
904
+ }
905
+ function decompressString(compressedString) {
906
+ const compressedData = new Uint8Array(atob(compressedString).split("").map((char) => char.charCodeAt(0)));
907
+ const decompressed = pako.inflate(compressedData);
908
+ return new TextDecoder().decode(decompressed);
909
+ }
910
+
911
+ // src/sitemap/index.ts
912
+ var createTimeoutController = (timeout) => {
913
+ const controller = new AbortController;
914
+ setTimeout(() => controller.abort(), timeout);
915
+ return controller;
916
+ };
917
+ var discoverSitemaps = async (domain) => {
918
+ const normalizedDomain = domain.replace(/\/$/, "");
919
+ const commonLocations = [
920
+ "/sitemap.xml",
921
+ "/sitemap_index.xml",
922
+ "/sitemap/",
923
+ "/sitemaps.xml",
924
+ "/sitemap/sitemap.xml"
925
+ ];
926
+ const sitemaps = new Set;
927
+ const requestOptions = {
928
+ headers: {
929
+ "User-Agent": "SitemapCrawler/1.0"
930
+ },
931
+ signal: createTimeoutController(5000).signal
932
+ };
933
+ try {
934
+ const robotsTxt = await fetch(`${normalizedDomain}/robots.txt`, {
935
+ ...requestOptions
936
+ });
937
+ if (robotsTxt.ok) {
938
+ const text = await robotsTxt.text();
939
+ const sitemapMatches = text.match(/Sitemap: (.*)/gi);
940
+ if (sitemapMatches) {
941
+ sitemapMatches.forEach((match) => {
942
+ const url = match.replace("Sitemap:", "").trim();
943
+ sitemaps.add(url);
944
+ });
945
+ }
946
+ if (sitemaps.size > 0) {
947
+ return [...sitemaps];
948
+ }
949
+ }
950
+ } catch (error) {
951
+ console.warn("Could not fetch robots.txt:", error instanceof Error ? error.message : "Unknown error");
952
+ }
953
+ for (const location of commonLocations) {
954
+ try {
955
+ const response = await fetch(`${normalizedDomain}${location}`, {
956
+ ...requestOptions,
957
+ method: "HEAD"
958
+ });
959
+ if (response.ok) {
960
+ return [`${normalizedDomain}${location}`];
961
+ }
962
+ } catch (_error) {}
963
+ }
964
+ return [];
965
+ };
966
+ var getDomainSitemap = async (domain, options = {}) => {
967
+ const {
968
+ filterIndexes,
969
+ concurrency = 10,
970
+ maxDepth = 5,
971
+ timeout = 1e4
972
+ } = options;
973
+ let normalizedDomain = domain.replace(/\/$/, "");
974
+ if (!normalizedDomain.startsWith("http")) {
975
+ normalizedDomain = `https://${normalizedDomain}`;
976
+ }
977
+ const sitemapUrls = await discoverSitemaps(normalizedDomain);
978
+ if (sitemapUrls.length === 0) {
979
+ throw new Error("No sitemaps found for the domain");
980
+ }
981
+ const queue = new PQueue({ concurrency });
982
+ const result = {
983
+ links: new Set,
984
+ visited: new Set
985
+ };
986
+ const locRegex = /<loc>([^<]+)<\/loc>/g;
987
+ const isXmlSitemap = (url) => url.endsWith(".xml") || url.toLowerCase().includes("sitemap");
988
+ const processSitemap = async (sitemapUrl, depth = 0) => {
989
+ if (depth > maxDepth || result.visited.has(sitemapUrl)) {
990
+ return;
991
+ }
992
+ result.visited.add(sitemapUrl);
993
+ try {
994
+ const controller = createTimeoutController(timeout);
995
+ const response = await fetch(sitemapUrl, {
996
+ headers: {
997
+ "User-Agent": "SitemapCrawler/1.0"
998
+ },
999
+ signal: controller.signal
1000
+ });
1001
+ if (!response.ok)
1002
+ throw new Error(`HTTP error! status: ${response.status}`);
1003
+ const data = await response.text();
1004
+ const matches = [...data.matchAll(locRegex)].map((match) => match[1].trim()).filter((link) => {
1005
+ if (filterIndexes && isXmlSitemap(link)) {
1006
+ return link.includes(filterIndexes);
1007
+ }
1008
+ return true;
1009
+ });
1010
+ for (const link of matches) {
1011
+ if (isXmlSitemap(link)) {
1012
+ queue.add(() => processSitemap(link, depth + 1));
1013
+ } else {
1014
+ result.links.add(link);
1015
+ }
1016
+ }
1017
+ } catch (error) {
1018
+ console.warn(`Failed to process sitemap ${sitemapUrl}:`, error instanceof Error ? error.message : "Unknown error");
1019
+ }
1020
+ };
1021
+ await Promise.all(sitemapUrls.map((sitemapUrl) => queue.add(() => processSitemap(sitemapUrl))));
1022
+ await queue.onIdle();
1023
+ return [...result.links];
1024
+ };
1025
+
882
1026
  // src/typedswitch/index.ts
883
1027
  function runSwitch(value, keyOrCases, casesOrDefault, defaultCase) {
884
1028
  let key;
@@ -1514,6 +1658,110 @@ function formatPeriodLabel(start, end, includeDay = false) {
1514
1658
  function getDomainFaviconUrl(domain, size = 128) {
1515
1659
  return `https://www.google.com/s2/favicons?domain=${encodeURIComponent(domain)}&sz=${size}`;
1516
1660
  }
1661
+ // src/cool-console-log/index.ts
1662
+ var terminalColors = {
1663
+ reset: "\x1B[0m",
1664
+ bold: "\x1B[1m",
1665
+ dim: "\x1B[2m",
1666
+ italic: "\x1B[3m",
1667
+ underline: "\x1B[4m",
1668
+ black: "\x1B[30m",
1669
+ red: "\x1B[31m",
1670
+ green: "\x1B[32m",
1671
+ yellow: "\x1B[33m",
1672
+ blue: "\x1B[34m",
1673
+ magenta: "\x1B[35m",
1674
+ cyan: "\x1B[36m",
1675
+ white: "\x1B[37m",
1676
+ gray: "\x1B[90m",
1677
+ brightRed: "\x1B[91m",
1678
+ brightGreen: "\x1B[92m",
1679
+ brightYellow: "\x1B[93m",
1680
+ brightBlue: "\x1B[94m",
1681
+ brightMagenta: "\x1B[95m",
1682
+ brightCyan: "\x1B[96m",
1683
+ brightWhite: "\x1B[97m",
1684
+ bgBlack: "\x1B[40m",
1685
+ bgRed: "\x1B[41m",
1686
+ bgGreen: "\x1B[42m",
1687
+ bgYellow: "\x1B[43m",
1688
+ bgBlue: "\x1B[44m",
1689
+ bgMagenta: "\x1B[45m",
1690
+ bgCyan: "\x1B[46m",
1691
+ bgWhite: "\x1B[47m"
1692
+ };
1693
+ var levelColors = {
1694
+ debug: { badge: terminalColors.gray, text: terminalColors.dim },
1695
+ info: { badge: terminalColors.cyan, text: terminalColors.reset },
1696
+ warn: { badge: terminalColors.yellow, text: terminalColors.yellow },
1697
+ error: { badge: terminalColors.red, text: terminalColors.brightRed }
1698
+ };
1699
+ function formatAttributes(attrs) {
1700
+ if (!attrs || Object.keys(attrs).length === 0)
1701
+ return "";
1702
+ const formatted = Object.entries(attrs).map(([k, v]) => `${terminalColors.dim}${k}=${terminalColors.reset}${v}`).join(" ");
1703
+ return ` ${formatted}`;
1704
+ }
1705
+ function createCoolLogger(options = {}) {
1706
+ const { colorsInDevOnly = true, onLog } = options;
1707
+ const isDev = true;
1708
+ const useColors = colorsInDevOnly ? isDev : true;
1709
+ const createLogMethod = (level) => (message, attributes) => {
1710
+ onLog?.(level, message, attributes);
1711
+ if (level === "debug" && !isDev)
1712
+ return;
1713
+ if (useColors) {
1714
+ const { badge, text } = levelColors[level];
1715
+ const label = level.toUpperCase().padEnd(5);
1716
+ const attrStr = formatAttributes(attributes);
1717
+ console.log(`${badge}[${label}]${terminalColors.reset} ${text}${message}${terminalColors.reset}${attrStr}`);
1718
+ } else {
1719
+ console[level](`[${level.toUpperCase()}]`, message, attributes ?? "");
1720
+ }
1721
+ };
1722
+ return {
1723
+ debug: createLogMethod("debug"),
1724
+ info: createLogMethod("info"),
1725
+ warn: createLogMethod("warn"),
1726
+ error: createLogMethod("error")
1727
+ };
1728
+ }
1729
+ function logOperationSummary(operationName, durationMs, success, options = {}) {
1730
+ const { devOnly = true, error } = options;
1731
+ const isDev = true;
1732
+ if (devOnly && !isDev)
1733
+ return;
1734
+ const c = terminalColors;
1735
+ const statusIcon = success ? `${c.brightGreen}✓${c.reset}` : `${c.brightRed}✗${c.reset}`;
1736
+ const statusColor = success ? c.green : c.red;
1737
+ const durationStr = `${c.dim}${durationMs.toFixed(1)}ms${c.reset}`;
1738
+ const errorMessage = error ? ` ${c.dim}·${c.reset} ${c.red}${error instanceof Error ? error.message : String(error)}${c.reset}` : "";
1739
+ console.log(`${statusColor}${c.bold}${operationName}${c.reset} ${statusIcon} ${c.dim}·${c.reset} ${durationStr}${errorMessage}`);
1740
+ }
1741
+ function colorize(text, color, ...modifiers) {
1742
+ const isDev = true;
1743
+ if (!isDev)
1744
+ return text;
1745
+ const colorCodes = [
1746
+ terminalColors[color],
1747
+ ...modifiers.map((m) => terminalColors[m])
1748
+ ].join("");
1749
+ return `${colorCodes}${text}${terminalColors.reset}`;
1750
+ }
1751
+ function logBanner(text, color = "cyan", options = {}) {
1752
+ const { devOnly = true } = options;
1753
+ const isDev = true;
1754
+ if (devOnly && !isDev) {
1755
+ console.log(`=== ${text} ===`);
1756
+ return;
1757
+ }
1758
+ const c = terminalColors;
1759
+ const padding = 2;
1760
+ const line = "═".repeat(text.length + padding * 2);
1761
+ console.log(`${c[color]}╔${line}╗${c.reset}`);
1762
+ console.log(`${c[color]}║${" ".repeat(padding)}${c.bold}${text}${c.reset}${c[color]}${" ".repeat(padding)}║${c.reset}`);
1763
+ console.log(`${c[color]}╚${line}╝${c.reset}`);
1764
+ }
1517
1765
  export {
1518
1766
  zodToTypeString,
1519
1767
  zodToExample,
@@ -1524,6 +1772,7 @@ export {
1524
1772
  typedSwitch,
1525
1773
  tryCatch,
1526
1774
  toGteLteFilter,
1775
+ terminalColors,
1527
1776
  slugify,
1528
1777
  seriesColorKey,
1529
1778
  resolveSeriesColorMap,
@@ -1531,8 +1780,12 @@ export {
1531
1780
  pickItemsInArray,
1532
1781
  partition,
1533
1782
  normalizeSingleOrArray,
1783
+ logOperationSummary,
1784
+ logBanner,
1534
1785
  linearRegressionTrend,
1535
1786
  keyBy,
1787
+ jwtVerify2 as jwtVerify,
1788
+ jwtSign,
1536
1789
  jsonToZodSchemaMapper,
1537
1790
  isSuccess,
1538
1791
  isFailure,
@@ -1542,16 +1795,21 @@ export {
1542
1795
  getUniqueValues,
1543
1796
  getPreviousReportPeriod,
1544
1797
  getHeatmapCellStyle,
1798
+ getDomainSitemap,
1545
1799
  getDomainFaviconUrl,
1546
1800
  generateId,
1547
1801
  formatPeriodLabel,
1548
1802
  findArrayDifferenceByKey,
1549
1803
  extractNumber,
1804
+ decompressString,
1550
1805
  createZId,
1551
1806
  createResolveColorsByKeys,
1807
+ createCoolLogger,
1552
1808
  createApiWrapper,
1553
1809
  createApiClient,
1810
+ compressString,
1554
1811
  compact,
1812
+ colorize,
1555
1813
  chunkArray,
1556
1814
  cacheFunction,
1557
1815
  backgroundPromiseSync,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hanzio",
3
- "version": "1.0.0",
3
+ "version": "1.0.3",
4
4
  "description": "A set of utility functions for TypeScript",
5
5
  "type": "module",
6
6
  "module": "./dist/index.js",
@@ -34,42 +34,6 @@
34
34
  "bun": "./src/index.ts",
35
35
  "import": "./dist/index.js",
36
36
  "default": "./dist/index.js"
37
- },
38
- "./zod": {
39
- "types": "./dist/zod/index.d.ts",
40
- "bun": "./src/zod/index.ts",
41
- "import": "./dist/zod/index.js",
42
- "default": "./dist/zod/index.js"
43
- },
44
- "./api-wrapper": {
45
- "types": "./dist/api-wrapper/index.d.ts",
46
- "bun": "./src/api-wrapper/index.ts",
47
- "import": "./dist/api-wrapper/index.js",
48
- "default": "./dist/api-wrapper/index.js"
49
- },
50
- "./jwt": {
51
- "types": "./dist/jwt/index.d.ts",
52
- "bun": "./src/jwt/index.ts",
53
- "import": "./dist/jwt/index.js",
54
- "default": "./dist/jwt/index.js"
55
- },
56
- "./gzip": {
57
- "types": "./dist/gzip/index.d.ts",
58
- "bun": "./src/gzip/index.ts",
59
- "import": "./dist/gzip/index.js",
60
- "default": "./dist/gzip/index.js"
61
- },
62
- "./sitemap": {
63
- "types": "./dist/sitemap/index.d.ts",
64
- "bun": "./src/sitemap/index.ts",
65
- "import": "./dist/sitemap/index.js",
66
- "default": "./dist/sitemap/index.js"
67
- },
68
- "./p-queue": {
69
- "types": "./dist/p-queue/index.d.ts",
70
- "bun": "./src/p-queue/index.ts",
71
- "import": "./dist/p-queue/index.js",
72
- "default": "./dist/p-queue/index.js"
73
37
  }
74
38
  },
75
39
  "scripts": {
@@ -0,0 +1,229 @@
1
+ // ═══════════════════════════════════════════════════════════════════════════
2
+ // Cool Console Log - Colorful Terminal Logging Utility
3
+ // ═══════════════════════════════════════════════════════════════════════════
4
+
5
+ /** ANSI color codes for terminal output */
6
+ export const terminalColors = {
7
+ reset: '\x1b[0m',
8
+ bold: '\x1b[1m',
9
+ dim: '\x1b[2m',
10
+ italic: '\x1b[3m',
11
+ underline: '\x1b[4m',
12
+ // Foreground colors
13
+ black: '\x1b[30m',
14
+ red: '\x1b[31m',
15
+ green: '\x1b[32m',
16
+ yellow: '\x1b[33m',
17
+ blue: '\x1b[34m',
18
+ magenta: '\x1b[35m',
19
+ cyan: '\x1b[36m',
20
+ white: '\x1b[37m',
21
+ gray: '\x1b[90m',
22
+ // Bright variants
23
+ brightRed: '\x1b[91m',
24
+ brightGreen: '\x1b[92m',
25
+ brightYellow: '\x1b[93m',
26
+ brightBlue: '\x1b[94m',
27
+ brightMagenta: '\x1b[95m',
28
+ brightCyan: '\x1b[96m',
29
+ brightWhite: '\x1b[97m',
30
+ // Background colors
31
+ bgBlack: '\x1b[40m',
32
+ bgRed: '\x1b[41m',
33
+ bgGreen: '\x1b[42m',
34
+ bgYellow: '\x1b[43m',
35
+ bgBlue: '\x1b[44m',
36
+ bgMagenta: '\x1b[45m',
37
+ bgCyan: '\x1b[46m',
38
+ bgWhite: '\x1b[47m'
39
+ } as const
40
+
41
+ export type TerminalColor = keyof typeof terminalColors
42
+
43
+ /** Log levels supported by the logger */
44
+ export type LogLevel = 'debug' | 'info' | 'warn' | 'error'
45
+
46
+ /** Attributes that can be passed to log methods */
47
+ export type LogAttributes = Record<string, string | number | boolean>
48
+
49
+ /** Log method signature */
50
+ export type LogMethod = (message: string, attributes?: LogAttributes) => void
51
+
52
+ /** Logger interface with colored output */
53
+ export type CoolLogger = {
54
+ debug: LogMethod
55
+ info: LogMethod
56
+ warn: LogMethod
57
+ error: LogMethod
58
+ }
59
+
60
+ /** Color config per log level */
61
+ const levelColors: Record<LogLevel, { badge: string; text: string }> = {
62
+ debug: { badge: terminalColors.gray, text: terminalColors.dim },
63
+ info: { badge: terminalColors.cyan, text: terminalColors.reset },
64
+ warn: { badge: terminalColors.yellow, text: terminalColors.yellow },
65
+ error: { badge: terminalColors.red, text: terminalColors.brightRed }
66
+ }
67
+
68
+ /** Format attributes as a compact colored string */
69
+ function formatAttributes(attrs?: LogAttributes): string {
70
+ if (!attrs || Object.keys(attrs).length === 0) return ''
71
+ const formatted = Object.entries(attrs)
72
+ .map(([k, v]) => `${terminalColors.dim}${k}=${terminalColors.reset}${v}`)
73
+ .join(' ')
74
+ return ` ${formatted}`
75
+ }
76
+
77
+ /**
78
+ * Options for creating a cool logger
79
+ */
80
+ export type CoolLoggerOptions = {
81
+ /** Only use colors in development (default: true) */
82
+ colorsInDevOnly?: boolean
83
+ /** Callback for each log entry (e.g., to add to OpenTelemetry span) */
84
+ onLog?: (level: LogLevel, message: string, attributes?: LogAttributes) => void
85
+ }
86
+
87
+ /**
88
+ * Creates a logger with colorful console output.
89
+ *
90
+ * @example
91
+ * ```ts
92
+ * const logger = createCoolLogger()
93
+ * logger.info('User logged in', { userId: '123' })
94
+ * logger.warn('Rate limit approaching', { remaining: 10 })
95
+ * logger.error('Payment failed', { reason: 'insufficient_funds' })
96
+ * ```
97
+ */
98
+ export function createCoolLogger(options: CoolLoggerOptions = {}): CoolLogger {
99
+ const { colorsInDevOnly = true, onLog } = options
100
+ const isDev = process.env.NODE_ENV !== 'production'
101
+ const useColors = colorsInDevOnly ? isDev : true
102
+
103
+ const createLogMethod =
104
+ (level: LogLevel): LogMethod =>
105
+ (message, attributes) => {
106
+ // Call the onLog callback if provided
107
+ onLog?.(level, message, attributes)
108
+
109
+ // Skip debug logs in production
110
+ if (level === 'debug' && !isDev) return
111
+
112
+ if (useColors) {
113
+ const { badge, text } = levelColors[level]
114
+ const label = level.toUpperCase().padEnd(5)
115
+ const attrStr = formatAttributes(attributes)
116
+ console.log(
117
+ `${badge}[${label}]${terminalColors.reset} ${text}${message}${terminalColors.reset}${attrStr}`
118
+ )
119
+ } else {
120
+ console[level](`[${level.toUpperCase()}]`, message, attributes ?? '')
121
+ }
122
+ }
123
+
124
+ return {
125
+ debug: createLogMethod('debug'),
126
+ info: createLogMethod('info'),
127
+ warn: createLogMethod('warn'),
128
+ error: createLogMethod('error')
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Logs a request/operation summary with colored status and duration.
134
+ * Shows: `operationName ✓ · 12.3ms` (green) or `operationName ✗ · 45.2ms` (red)
135
+ *
136
+ * @example
137
+ * ```ts
138
+ * const start = performance.now()
139
+ * try {
140
+ * await doSomething()
141
+ * logOperationSummary('user.create', performance.now() - start, true)
142
+ * } catch {
143
+ * logOperationSummary('user.create', performance.now() - start, false)
144
+ * }
145
+ * ```
146
+ */
147
+ export function logOperationSummary(
148
+ operationName: string,
149
+ durationMs: number,
150
+ success: boolean,
151
+ options: { devOnly?: boolean; error?: Error | unknown } = {}
152
+ ): void {
153
+ const { devOnly = true, error } = options
154
+ const isDev = process.env.NODE_ENV !== 'production'
155
+
156
+ if (devOnly && !isDev) return
157
+
158
+ const c = terminalColors
159
+ const statusIcon = success
160
+ ? `${c.brightGreen}✓${c.reset}`
161
+ : `${c.brightRed}✗${c.reset}`
162
+ const statusColor = success ? c.green : c.red
163
+ const durationStr = `${c.dim}${durationMs.toFixed(1)}ms${c.reset}`
164
+
165
+ const errorMessage = error
166
+ ? ` ${c.dim}·${c.reset} ${c.red}${error instanceof Error ? error.message : String(error)}${c.reset}`
167
+ : ''
168
+
169
+ console.log(
170
+ `${statusColor}${c.bold}${operationName}${c.reset} ${statusIcon} ${c.dim}·${c.reset} ${durationStr}${errorMessage}`
171
+ )
172
+ }
173
+
174
+ /**
175
+ * Helper to colorize text for console output
176
+ *
177
+ * @example
178
+ * ```ts
179
+ * console.log(colorize('Success!', 'green', 'bold'))
180
+ * console.log(colorize('Warning', 'yellow'))
181
+ * ```
182
+ */
183
+ export function colorize(
184
+ text: string,
185
+ color: TerminalColor,
186
+ ...modifiers: TerminalColor[]
187
+ ): string {
188
+ const isDev = process.env.NODE_ENV !== 'production'
189
+ if (!isDev) return text
190
+
191
+ const colorCodes = [
192
+ terminalColors[color],
193
+ ...modifiers.map((m) => terminalColors[m])
194
+ ].join('')
195
+ return `${colorCodes}${text}${terminalColors.reset}`
196
+ }
197
+
198
+ /**
199
+ * Log a styled box/banner to the console (dev only)
200
+ *
201
+ * @example
202
+ * ```ts
203
+ * logBanner('Server Started', 'green')
204
+ * logBanner('⚠️ Warning', 'yellow')
205
+ * ```
206
+ */
207
+ export function logBanner(
208
+ text: string,
209
+ color: TerminalColor = 'cyan',
210
+ options: { devOnly?: boolean } = {}
211
+ ): void {
212
+ const { devOnly = true } = options
213
+ const isDev = process.env.NODE_ENV !== 'production'
214
+
215
+ if (devOnly && !isDev) {
216
+ console.log(`=== ${text} ===`)
217
+ return
218
+ }
219
+
220
+ const c = terminalColors
221
+ const padding = 2
222
+ const line = '═'.repeat(text.length + padding * 2)
223
+
224
+ console.log(`${c[color]}╔${line}╗${c.reset}`)
225
+ console.log(
226
+ `${c[color]}║${' '.repeat(padding)}${c.bold}${text}${c.reset}${c[color]}${' '.repeat(padding)}║${c.reset}`
227
+ )
228
+ console.log(`${c[color]}╚${line}╝${c.reset}`)
229
+ }
package/src/index.ts CHANGED
@@ -10,3 +10,7 @@ export * from './url'
10
10
  export * from './zod'
11
11
  export * from './api-wrapper'
12
12
  export * from './p-queue'
13
+ export * from './cool-console-log'
14
+ export * from './jwt'
15
+ export * from './gzip'
16
+ export * from './sitemap'