@ix-xs/node-comfort 2.0.0 → 2.1.0

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 (77) hide show
  1. package/CHANGELOG.md +150 -0
  2. package/README.md +32 -30
  3. package/index.js +3 -3
  4. package/package.json +8 -7
  5. package/src/Arr.js +222 -58
  6. package/src/Async.js +322 -90
  7. package/src/Cache.js +148 -44
  8. package/src/Checker.js +257 -26
  9. package/src/Cli.js +404 -145
  10. package/src/Color.js +224 -83
  11. package/src/Crypto.js +481 -106
  12. package/src/Emitter.js +104 -37
  13. package/src/Env.js +176 -42
  14. package/src/FS.js +203 -60
  15. package/src/Func.js +226 -76
  16. package/src/Http.js +311 -149
  17. package/src/Id.js +281 -96
  18. package/src/Logger.js +302 -78
  19. package/src/Num.js +182 -60
  20. package/src/Obj.js +340 -125
  21. package/src/SQLite.js +426 -105
  22. package/src/Schema.js +602 -87
  23. package/src/Str.js +316 -86
  24. package/src/Sys.js +197 -54
  25. package/src/Time.js +572 -317
  26. package/src/Utils.js +62 -41
  27. package/src/errors.js +54 -11
  28. package/src/internal/shared.js +415 -0
  29. package/types/index.d.ts +59 -17
  30. package/types/src/Arr.d.ts +78 -0
  31. package/types/src/Arr.d.ts.map +1 -1
  32. package/types/src/Async.d.ts +52 -1
  33. package/types/src/Async.d.ts.map +1 -1
  34. package/types/src/Cache.d.ts +33 -7
  35. package/types/src/Cache.d.ts.map +1 -1
  36. package/types/src/Checker.d.ts +155 -2
  37. package/types/src/Checker.d.ts.map +1 -1
  38. package/types/src/Cli.d.ts +97 -15
  39. package/types/src/Cli.d.ts.map +1 -1
  40. package/types/src/Color.d.ts +7 -1
  41. package/types/src/Color.d.ts.map +1 -1
  42. package/types/src/Crypto.d.ts +99 -13
  43. package/types/src/Crypto.d.ts.map +1 -1
  44. package/types/src/Emitter.d.ts +36 -17
  45. package/types/src/Emitter.d.ts.map +1 -1
  46. package/types/src/Env.d.ts +31 -1
  47. package/types/src/Env.d.ts.map +1 -1
  48. package/types/src/FS.d.ts +36 -5
  49. package/types/src/FS.d.ts.map +1 -1
  50. package/types/src/Func.d.ts +147 -19
  51. package/types/src/Func.d.ts.map +1 -1
  52. package/types/src/Http.d.ts +113 -43
  53. package/types/src/Http.d.ts.map +1 -1
  54. package/types/src/Id.d.ts +60 -8
  55. package/types/src/Id.d.ts.map +1 -1
  56. package/types/src/Logger.d.ts +53 -2
  57. package/types/src/Logger.d.ts.map +1 -1
  58. package/types/src/Num.d.ts +48 -7
  59. package/types/src/Num.d.ts.map +1 -1
  60. package/types/src/Obj.d.ts +67 -4
  61. package/types/src/Obj.d.ts.map +1 -1
  62. package/types/src/SQLite.d.ts +123 -14
  63. package/types/src/SQLite.d.ts.map +1 -1
  64. package/types/src/Schema.d.ts +256 -7
  65. package/types/src/Schema.d.ts.map +1 -1
  66. package/types/src/Str.d.ts +49 -1
  67. package/types/src/Str.d.ts.map +1 -1
  68. package/types/src/Sys.d.ts +51 -6
  69. package/types/src/Sys.d.ts.map +1 -1
  70. package/types/src/Time.d.ts +130 -34
  71. package/types/src/Time.d.ts.map +1 -1
  72. package/types/src/Utils.d.ts +15 -11
  73. package/types/src/Utils.d.ts.map +1 -1
  74. package/types/src/errors.d.ts +27 -3
  75. package/types/src/errors.d.ts.map +1 -1
  76. package/types/src/internal/shared.d.ts +123 -0
  77. package/types/src/internal/shared.d.ts.map +1 -0
package/src/Checker.js CHANGED
@@ -11,6 +11,18 @@
11
11
  */
12
12
 
13
13
  const { types } = require("node:util");
14
+ const shared = require("./internal/shared.js");
15
+
16
+ /** @type {typeof import("node:net") | undefined} */
17
+ let _net;
18
+
19
+ const EMAIL = /^(?!\.)(?!.*\.\.)[\p{L}\p{N}.!#$%&'*+/=?^_`{|}~-]{1,64}(?<!\.)@(?:[\p{L}\p{N}](?:[\p{L}\p{N}-]{0,61}[\p{L}\p{N}])?\.)+(?:\p{L}{2,}|xn--[a-z0-9-]{2,})$/iu;
20
+ const TLD = /\.(?:[a-z]{2,}|xn--[a-z0-9-]{2,})\.?$/i;
21
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-([1-8])[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
22
+ const UUID_NIL_OR_MAX = /^(?:0{8}-0{4}-0{4}-0{4}-0{12}|f{8}-f{4}-f{4}-f{4}-f{12})$/i;
23
+ const ISO_DATE = /^(\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2})(?::(\d{2})(?:\.\d{1,9})?)?(?:Z|([+-])(\d{2}):?(\d{2}))?)?$/;
24
+ const HOST_LABEL = /^[\p{L}\p{N}](?:[\p{L}\p{N}-]{0,61}[\p{L}\p{N}])?$/u;
25
+ const DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
14
26
 
15
27
  /**
16
28
  * Any built-in typed array.
@@ -35,6 +47,12 @@ const { types } = require("node:util");
35
47
  * @property {1|2|3|4|5|6|7|8} [version] Only accept this version. Otherwise versions 1 to 8 pass, plus the nil and max UUIDs.
36
48
  */
37
49
 
50
+ /**
51
+ * Options for `isHostname()`.
52
+ * @typedef {object} IsHostnameOptions
53
+ * @property {boolean} [requireTld] Require a domain like `example.com`, rejecting `localhost`.
54
+ */
55
+
38
56
  /**
39
57
  * Options for `isBase64()`.
40
58
  * @typedef {object} IsBase64Options
@@ -348,10 +366,7 @@ function isObject(value) {
348
366
  * @returns {value is Record<string, unknown>}
349
367
  */
350
368
  function isPlainObject(value) {
351
- if (typeof value !== "object" || value === null) return false;
352
- const proto = Object.getPrototypeOf(value);
353
- // The last check accepts objects from another realm (a worker, a `vm`).
354
- return proto === null || proto === Object.prototype || Object.getPrototypeOf(proto) === null;
369
+ return shared.isPlainObject(value);
355
370
  }
356
371
 
357
372
  /**
@@ -495,7 +510,8 @@ function isError(value) {
495
510
 
496
511
  /**
497
512
  * Is it empty? `null`, `undefined`, `""`, `[]`, `{}` and empty maps and sets
498
- * are. Numbers, booleans and functions never are.
513
+ * are. Numbers, booleans, functions, dates, regular expressions and errors
514
+ * never are.
499
515
  *
500
516
  * @example
501
517
  * nc.isEmpty({}); // true
@@ -508,9 +524,10 @@ function isError(value) {
508
524
  function isEmpty(value) {
509
525
  if (value === null || value === undefined) return true;
510
526
  if (typeof value === "string" || Array.isArray(value)) return value.length === 0;
527
+ if (typeof value !== "object") return false;
511
528
  if (types.isMap(value) || types.isSet(value)) return /** @type {Map<unknown, unknown>} */ (value).size === 0;
512
- if (typeof value === "object") return Object.keys(value).length === 0;
513
- return false;
529
+ if (types.isDate(value) || types.isRegExp(value) || types.isNativeError(value)) return false;
530
+ return Object.keys(value).length === 0;
514
531
  }
515
532
 
516
533
  /**
@@ -574,8 +591,7 @@ function isOneOf(value, allowed) {
574
591
  * @returns {value is string}
575
592
  */
576
593
  function isEmail(value) {
577
- if (typeof value !== "string" || value.length > 254) return false;
578
- return /^(?!\.)(?!.*\.\.)[\p{L}\p{N}.!#$%&'*+/=?^_`{|}~-]+(?<!\.)@(?:[\p{L}\p{N}](?:[\p{L}\p{N}-]{0,61}[\p{L}\p{N}])?\.)+\p{L}{2,}$/u.test(value);
594
+ return typeof value === "string" && value.length <= 254 && EMAIL.test(value);
579
595
  }
580
596
 
581
597
  /**
@@ -592,15 +608,11 @@ function isEmail(value) {
592
608
  */
593
609
  function isURL(value, options = {}) {
594
610
  if (typeof value !== "string") return false;
595
- const protocols = options.protocols ?? ["http:", "https:"];
596
- try {
597
- const url = new URL(value);
598
- if (!protocols.includes(url.protocol)) return false;
599
- if (options.requireTld && !/\.[a-z]{2,}$/i.test(url.hostname)) return false;
600
- return true;
601
- } catch {
602
- return false;
603
- }
611
+ const url = URL.parse(value);
612
+ if (!url) return false;
613
+ const protocols = options.protocols;
614
+ if (protocols ? !protocols.includes(url.protocol) : url.protocol !== "http:" && url.protocol !== "https:") return false;
615
+ return !options.requireTld || TLD.test(url.hostname);
604
616
  }
605
617
 
606
618
  /**
@@ -616,9 +628,10 @@ function isURL(value, options = {}) {
616
628
  */
617
629
  function isUUID(value, options = {}) {
618
630
  if (typeof value !== "string") return false;
619
- if (options.version === undefined && /^(0{8}-0{4}-0{4}-0{4}-0{12}|f{8}-f{4}-f{4}-f{4}-f{12})$/i.test(value)) return true;
620
- const version = options.version === undefined ? "[1-8]" : String(options.version);
621
- return new RegExp(`^[0-9a-f]{8}-[0-9a-f]{4}-${version}[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$`, "i").test(value);
631
+ if (value.length !== 36) return false;
632
+ const match = UUID.exec(value);
633
+ if (!match) return options.version === undefined && UUID_NIL_OR_MAX.test(value);
634
+ return options.version === undefined || Number(match[1]) === options.version;
622
635
  }
623
636
 
624
637
  /**
@@ -672,7 +685,7 @@ function isNumeric(value) {
672
685
  */
673
686
  function isIP(value, version) {
674
687
  if (typeof value !== "string") return false;
675
- const detected = require("node:net").isIP(value);
688
+ const detected = (_net ??= require("node:net")).isIP(value);
676
689
  return version === undefined ? detected !== 0 : detected === version;
677
690
  }
678
691
 
@@ -781,15 +794,16 @@ function isSemver(value) {
781
794
  */
782
795
  function isISODate(value) {
783
796
  if (typeof value !== "string") return false;
784
- const match = /^(\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2})(?::(\d{2})(?:\.\d{1,9})?)?(Z|[+-]\d{2}:?\d{2})?)?$/.exec(value);
797
+ const match = ISO_DATE.exec(value);
785
798
  if (!match) return false;
786
- const [, y, m, d, hh = "0", mm = "0", ss = "0"] = match;
799
+ const [, y, m, d, hh = "0", mm = "0", ss = "0", , offsetHours = "0", offsetMinutes = "0"] = match;
787
800
  const year = Number(y);
788
801
  const month = Number(m);
789
802
  const day = Number(d);
790
803
  if (month < 1 || month > 12 || day < 1) return false;
791
- const daysInMonth = new Date(Date.UTC(year, month, 0)).getUTCDate();
792
- return day <= daysInMonth && Number(hh) <= 23 && Number(mm) <= 59 && Number(ss) <= 59;
804
+ const leap = (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
805
+ if (day > (month === 2 && leap ? 29 : DAYS_IN_MONTH[month - 1])) return false;
806
+ return Number(hh) <= 23 && Number(mm) <= 59 && Number(ss) <= 59 && Number(offsetHours) <= 14 && Number(offsetMinutes) <= 59;
793
807
  }
794
808
 
795
809
  /**
@@ -874,6 +888,211 @@ function isJWT(value) {
874
888
  }
875
889
  }
876
890
 
891
+ /**
892
+ * Is it a host name, like `example.com`, `api.internal` or `localhost`?
893
+ * International names are accepted. IP addresses aren't: see `isIP`.
894
+ *
895
+ * @example
896
+ * nc.isHostname("api.example.com"); // true
897
+ * nc.isHostname("localhost"); // true
898
+ * nc.isHostname("localhost", { requireTld: true }); // false
899
+ * nc.isHostname("-bad-.com"); // false
900
+ *
901
+ * @param {unknown} value
902
+ * @param {IsHostnameOptions} [options]
903
+ * @returns {value is string}
904
+ */
905
+ function isHostname(value, options = {}) {
906
+ if (typeof value !== "string" || value.length === 0 || value.length > 253) return false;
907
+ const labels = value.split(".");
908
+ if (!labels.every((label) => HOST_LABEL.test(label))) return false;
909
+ if (/^\d+$/.test(labels[labels.length - 1])) return false;
910
+ return !options.requireTld || (labels.length > 1 && TLD.test(value));
911
+ }
912
+
913
+ /**
914
+ * Is it a MAC address? Accepts `aa:bb:cc:dd:ee:ff`, `aa-bb-cc-dd-ee-ff` and
915
+ * `aabb.ccdd.eeff`.
916
+ *
917
+ * @example
918
+ * nc.isMACAddress("00:1B:44:11:3A:B7"); // true
919
+ * nc.isMACAddress("00:1B:44:11:3A"); // false
920
+ *
921
+ * @param {unknown} value
922
+ * @returns {value is string}
923
+ */
924
+ function isMACAddress(value) {
925
+ return typeof value === "string" && /^(?:[0-9a-f]{2}(?::[0-9a-f]{2}){5}|[0-9a-f]{2}(?:-[0-9a-f]{2}){5}|[0-9a-f]{4}(?:\.[0-9a-f]{4}){2})$/i.test(value);
926
+ }
927
+
928
+ /**
929
+ * Is it a valid IBAN? Checks the format and the checksum, spaces allowed.
930
+ *
931
+ * @example
932
+ * nc.isIBAN("FR76 3000 6000 0112 3456 7890 189"); // true
933
+ * nc.isIBAN("FR76 3000 6000 0112 3456 7890 180"); // false, wrong checksum
934
+ *
935
+ * @param {unknown} value
936
+ * @returns {value is string}
937
+ */
938
+ function isIBAN(value) {
939
+ if (typeof value !== "string") return false;
940
+ const iban = value.replace(/\s+/g, "").toUpperCase();
941
+ if (!/^[A-Z]{2}\d{2}[A-Z0-9]{11,30}$/.test(iban)) return false;
942
+ const rearranged = iban.slice(4) + iban.slice(0, 4);
943
+ let remainder = 0;
944
+ for (let i = 0; i < rearranged.length; i++) {
945
+ const code = rearranged.charCodeAt(i);
946
+ // Letters count as two digits (A = 10 ... Z = 35).
947
+ remainder = code >= 65 ? (remainder * 100 + code - 55) % 97 : (remainder * 10 + code - 48) % 97;
948
+ }
949
+ return remainder === 1;
950
+ }
951
+
952
+ /**
953
+ * Is it a phone number in the international E.164 format: a `+`, then 7 to
954
+ * 15 digits, no spaces? That's what SMS and telephony APIs expect.
955
+ *
956
+ * @example
957
+ * nc.isE164("+33612345678"); // true
958
+ * nc.isE164("06 12 34 56 78"); // false
959
+ *
960
+ * @param {unknown} value
961
+ * @returns {value is string}
962
+ */
963
+ function isE164(value) {
964
+ return typeof value === "string" && /^\+[1-9]\d{6,14}$/.test(value);
965
+ }
966
+
967
+ /**
968
+ * Is it a MongoDB ObjectId, 24 hexadecimal characters?
969
+ *
970
+ * @example
971
+ * nc.isObjectId("507f1f77bcf86cd799439011"); // true
972
+ *
973
+ * @param {unknown} value
974
+ * @returns {value is string}
975
+ */
976
+ function isObjectId(value) {
977
+ return typeof value === "string" && value.length === 24 && /^[0-9a-f]{24}$/i.test(value);
978
+ }
979
+
980
+ /**
981
+ * Is it a ULID, like the ones `nc.id.ulid()` makes?
982
+ *
983
+ * @example
984
+ * nc.isULID(nc.id.ulid()); // true
985
+ *
986
+ * @param {unknown} value
987
+ * @returns {value is string}
988
+ */
989
+ function isULID(value) {
990
+ return typeof value === "string" && value.length === 26 && /^[0-7][0-9A-HJKMNP-TV-Z]{25}$/i.test(value);
991
+ }
992
+
993
+ /**
994
+ * Is it a `data:` URL, like an inline image?
995
+ *
996
+ * @example
997
+ * nc.isDataURL("data:image/png;base64,iVBORw0KGgo="); // true
998
+ * nc.isDataURL("data:,Hello"); // true
999
+ *
1000
+ * @param {unknown} value
1001
+ * @returns {value is string}
1002
+ */
1003
+ function isDataURL(value) {
1004
+ return typeof value === "string" && /^data:(?:[a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+)?(?:;[a-z0-9-]+=[^;,]*)*(?:;base64)?,/i.test(value);
1005
+ }
1006
+
1007
+ /**
1008
+ * Is it a media type, like `application/json` or
1009
+ * `text/html; charset=utf-8`?
1010
+ *
1011
+ * @example
1012
+ * nc.isMimeType("image/svg+xml"); // true
1013
+ * nc.isMimeType("json"); // false
1014
+ *
1015
+ * @param {unknown} value
1016
+ * @returns {value is string}
1017
+ */
1018
+ function isMimeType(value) {
1019
+ return typeof value === "string" && /^[a-z0-9][a-z0-9!#$&^_.+-]*\/[a-z0-9][a-z0-9!#$&^_.+-]*(?:\s*;\s*[a-z0-9-]+=(?:"[^"]*"|[^;\s]+))*$/i.test(value);
1020
+ }
1021
+
1022
+ /**
1023
+ * Is it a time zone Node.js knows, like `"Europe/Paris"` or `"UTC"`?
1024
+ *
1025
+ * @example
1026
+ * nc.isTimeZone("America/New_York"); // true
1027
+ * nc.isTimeZone("Mars/Olympus"); // false
1028
+ *
1029
+ * @param {unknown} value
1030
+ * @returns {value is string}
1031
+ */
1032
+ function isTimeZone(value) {
1033
+ if (typeof value !== "string" || value === "") return false;
1034
+ try {
1035
+ shared.intl.dateTime("en", { timeZone: value });
1036
+ return true;
1037
+ } catch {
1038
+ return false;
1039
+ }
1040
+ }
1041
+
1042
+ /**
1043
+ * Is it a well-formed locale, like `"fr"`, `"en-GB"` or `"zh-Hant-TW"`?
1044
+ *
1045
+ * @example
1046
+ * nc.isLocale("pt-BR"); // true
1047
+ * nc.isLocale("en_US"); // false, use a dash
1048
+ *
1049
+ * @param {unknown} value
1050
+ * @returns {value is string}
1051
+ */
1052
+ function isLocale(value) {
1053
+ if (typeof value !== "string" || value === "") return false;
1054
+ try {
1055
+ return Intl.getCanonicalLocales(value).length === 1;
1056
+ } catch {
1057
+ return false;
1058
+ }
1059
+ }
1060
+
1061
+ /**
1062
+ * Is it a cron expression `nc.time.cron()` understands?
1063
+ *
1064
+ * @example
1065
+ * nc.isCron("0 9 * * 1-5"); // true
1066
+ * nc.isCron("@daily"); // true
1067
+ * nc.isCron("61 * * * *"); // false
1068
+ *
1069
+ * @param {unknown} value
1070
+ * @returns {value is string}
1071
+ */
1072
+ function isCron(value) {
1073
+ if (typeof value !== "string") return false;
1074
+ try {
1075
+ shared.parseCron(value);
1076
+ return true;
1077
+ } catch {
1078
+ return false;
1079
+ }
1080
+ }
1081
+
1082
+ /**
1083
+ * Is it made only of ASCII characters?
1084
+ *
1085
+ * @example
1086
+ * nc.isASCII("hello"); // true
1087
+ * nc.isASCII("héllo"); // false
1088
+ *
1089
+ * @param {unknown} value
1090
+ * @returns {value is string}
1091
+ */
1092
+ function isASCII(value) {
1093
+ return typeof value === "string" && shared.ASCII.test(value);
1094
+ }
1095
+
877
1096
  /**
878
1097
  * Throws an `AssertionError` if `condition` is falsy. Afterwards, TypeScript
879
1098
  * knows the condition holds.
@@ -974,6 +1193,18 @@ module.exports = {
974
1193
  isAlphanumeric,
975
1194
  isCreditCard,
976
1195
  isJWT,
1196
+ isHostname,
1197
+ isMACAddress,
1198
+ isIBAN,
1199
+ isE164,
1200
+ isObjectId,
1201
+ isULID,
1202
+ isDataURL,
1203
+ isMimeType,
1204
+ isTimeZone,
1205
+ isLocale,
1206
+ isCron,
1207
+ isASCII,
977
1208
  assert,
978
1209
  assertType,
979
1210
  };