diff --git a/.eslintrc.cjs b/.eslintrc.cjs index a06d1e0..6cc8fb0 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -30,7 +30,7 @@ const commonTypescriptIgnoredRules = { const commonNodeIgnoredRules = { ...commonIgnoredRules, - "node/no-process-env": "off", + "import/no-useless-path-segments": "off", }; /** @@ -56,13 +56,6 @@ module.exports = { }, rules: { ...commonNodeIgnoredRules, - "import/extensions": [ - "error", - "always", - { - ignorePackages: true, - }, - ], }, overrides: [ { @@ -72,7 +65,7 @@ module.exports = { ], }, { - files: ["./*.ts", "src/**/*.ts"], + files: ["./*.ts", "scripts/**/*.ts", "src/**/*.ts"], extends: [ "canonical", "canonical/node", diff --git a/package.json b/package.json index bd7480d..f780d30 100644 --- a/package.json +++ b/package.json @@ -75,8 +75,12 @@ ] }, "dependencies": { + "consola": "3.2.2", "dotenv": "16.3.1", - "envalid": "7.3.1" + "envalid": "7.3.1", + "winston": "3.9.0", + "winston-daily-rotate-file": "4.7.1", + "zod": "^3.21.4" }, "devDependencies": { "@babel/plugin-syntax-import-assertions": "7.22.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1ca5452..e247149 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,12 +1,24 @@ lockfileVersion: '6.0' dependencies: + consola: + specifier: 3.2.2 + version: 3.2.2 dotenv: specifier: 16.3.1 version: 16.3.1 envalid: specifier: 7.3.1 version: 7.3.1 + winston: + specifier: 3.9.0 + version: 3.9.0 + winston-daily-rotate-file: + specifier: 4.7.1 + version: 4.7.1(winston@3.9.0) + zod: + specifier: ^3.21.4 + version: 3.21.4 devDependencies: '@babel/plugin-syntax-import-assertions': @@ -454,6 +466,11 @@ packages: to-fast-properties: 2.0.0 dev: true + /@colors/colors@1.5.0: + resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} + engines: {node: '>=0.1.90'} + dev: false + /@commitlint/cli@17.6.6: resolution: {integrity: sha512-sTKpr2i/Fjs9OmhU+beBxjPavpnLSqZaO6CzwKVq2Tc4UYVTMFgpKOslDhUBVlfAUBfjVO8ParxC/MXkIOevEA==} engines: {node: '>=v14'} @@ -927,6 +944,14 @@ packages: '@jridgewell/trace-mapping': 0.3.9 dev: true + /@dabh/diagnostics@2.0.3: + resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==} + dependencies: + colorspace: 1.1.4 + enabled: 2.0.0 + kuler: 2.0.0 + dev: false + /@es-joy/jsdoccomment@0.38.0: resolution: {integrity: sha512-TFac4Bnv0ZYNkEeDnOWHQhaS1elWlvOCQxH06iHeu5iffs+hCaLVIZJwF+FqksQi68R4i66Pu+4DfFGvble+Uw==} engines: {node: '>=16'} @@ -1929,6 +1954,10 @@ packages: resolution: {integrity: sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==} dev: true + /@types/triple-beam@1.3.2: + resolution: {integrity: sha512-txGIh+0eDFzKGC25zORnswy+br1Ha7hj5cMVwKIU7+s0U2AxxJru/jZSMU6OC9MJWP6+pc/hc6ZjyZShpsyY2g==} + dev: false + /@types/ws@8.5.4: resolution: {integrity: sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg==} dependencies: @@ -2472,7 +2501,6 @@ packages: /async@3.2.4: resolution: {integrity: sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==} - dev: true /available-typed-arrays@1.0.5: resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} @@ -2946,7 +2974,6 @@ packages: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} dependencies: color-name: 1.1.3 - dev: true /color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} @@ -2957,16 +2984,35 @@ packages: /color-name@1.1.3: resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} - dev: true /color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - dev: true + + /color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.2 + dev: false + + /color@3.2.1: + resolution: {integrity: sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==} + dependencies: + color-convert: 1.9.3 + color-string: 1.9.1 + dev: false /colorette@2.0.19: resolution: {integrity: sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==} dev: true + /colorspace@1.1.4: + resolution: {integrity: sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==} + dependencies: + color: 3.2.1 + text-hex: 1.0.0 + dev: false + /commander@10.0.0: resolution: {integrity: sha512-zS5PnTI22FIRM6ylNW8G4Ap0IEOyk62fhLSD0+uHRT9McRCLGpkVNvao4bjimpK/GShynyQkFFxHhwMcETmduA==} engines: {node: '>=14'} @@ -3060,6 +3106,11 @@ packages: xdg-basedir: 5.1.0 dev: true + /consola@3.2.2: + resolution: {integrity: sha512-r921u0vbF4lQsoIqYvSSER+yZLPQGijOHrYcWoCNVNBZmn/bRR+xT/DgerTze/nLD9TTGzdDa378TVhx7RDOYg==} + engines: {node: ^14.18.0 || >=16.10.0} + dev: false + /conventional-changelog-angular@5.0.13: resolution: {integrity: sha512-i/gipMxs7s8L/QeuavPF2hLnJgH6pEZAttySB6aiQLWcX3puWDL3ACVmvBhJGxnAy52Qc15ua26BufY6KpmrVA==} engines: {node: '>=10'} @@ -3759,6 +3810,10 @@ packages: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} dev: true + /enabled@2.0.0: + resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==} + dev: false + /end-of-stream@1.4.4: resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} dependencies: @@ -4792,6 +4847,10 @@ packages: reusify: 1.0.4 dev: true + /fecha@4.2.3: + resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} + dev: false + /fetch-blob@3.2.0: resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} engines: {node: ^12.20 || >= 14.13} @@ -4822,6 +4881,12 @@ packages: flat-cache: 3.0.4 dev: true + /file-stream-rotator@0.6.1: + resolution: {integrity: sha512-u+dBid4PvZw17PmDeRcNOtCP9CCK/9lRN2w+r1xIS7yOL9JFrIBKTvrYsxT4P0pGtThYTn++QS5ChHaUov3+zQ==} + dependencies: + moment: 2.29.4 + dev: false + /fill-range@7.0.1: resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} engines: {node: '>=8'} @@ -4873,6 +4938,10 @@ packages: resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==} dev: true + /fn.name@1.1.0: + resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==} + dev: false + /for-each@0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} dependencies: @@ -5595,7 +5664,6 @@ packages: /inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - dev: true /ini@1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} @@ -5698,6 +5766,10 @@ packages: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} dev: true + /is-arrayish@0.3.2: + resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + dev: false + /is-bigint@1.0.4: resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} dependencies: @@ -5953,7 +6025,6 @@ packages: /is-stream@2.0.1: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} - dev: true /is-stream@3.0.0: resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} @@ -6318,6 +6389,10 @@ packages: zod-validation-error: 1.3.1(zod@3.21.4) dev: true + /kuler@2.0.0: + resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} + dev: false + /language-subtag-registry@0.3.22: resolution: {integrity: sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==} dev: true @@ -6573,6 +6648,17 @@ packages: log-symbols: 3.0.0 dev: true + /logform@2.5.1: + resolution: {integrity: sha512-9FyqAm9o9NKKfiAKfZoYo9bGXXuwMkxQiQttkT4YjjVtQVIQtK6LmVtlxmCaFswo6N4AfEkHqZTV0taDtPotNg==} + dependencies: + '@colors/colors': 1.5.0 + '@types/triple-beam': 1.3.2 + fecha: 4.2.3 + ms: 2.1.3 + safe-stable-stringify: 2.4.3 + triple-beam: 1.3.0 + dev: false + /loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -6878,13 +6964,16 @@ packages: engines: {node: '>=0.10.0'} dev: true + /moment@2.29.4: + resolution: {integrity: sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==} + dev: false + /ms@2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} dev: true /ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - dev: true /mute-stream@0.0.8: resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} @@ -7086,6 +7175,11 @@ packages: engines: {node: '>=0.10.0'} dev: true + /object-hash@2.2.0: + resolution: {integrity: sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==} + engines: {node: '>= 6'} + dev: false + /object-inspect@1.12.3: resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==} dev: true @@ -7162,6 +7256,12 @@ packages: wrappy: 1.0.2 dev: true + /one-time@1.0.0: + resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==} + dependencies: + fn.name: 1.1.0 + dev: false + /onetime@5.1.2: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} @@ -7838,7 +7938,6 @@ packages: inherits: 2.0.4 string_decoder: 1.3.0 util-deprecate: 1.0.2 - dev: true /readdirp@3.6.0: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} @@ -8189,7 +8288,6 @@ packages: /safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - dev: true /safe-regex-test@1.0.0: resolution: {integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==} @@ -8208,7 +8306,6 @@ packages: /safe-stable-stringify@2.4.3: resolution: {integrity: sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==} engines: {node: '>=10'} - dev: true /safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -8334,6 +8431,12 @@ packages: engines: {node: '>=14'} dev: true + /simple-swizzle@0.2.2: + resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + dependencies: + is-arrayish: 0.3.2 + dev: false + /simple-update-notifier@1.1.0: resolution: {integrity: sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==} engines: {node: '>=8.10.0'} @@ -8467,6 +8570,10 @@ packages: through: 2.3.8 dev: true + /stack-trace@0.0.10: + resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} + dev: false + /stdin-discarder@0.1.0: resolution: {integrity: sha512-xhV7w8S+bUwlPTb4bAOUQhv8/cSS5offJuX8GQGq32ONF0ZtDWKfkdomM3HMRA+LhX6um/FZ0COqlwsjD53LeQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -8574,7 +8681,6 @@ packages: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} dependencies: safe-buffer: 5.2.1 - dev: true /strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} @@ -8689,6 +8795,10 @@ packages: engines: {node: '>=0.10'} dev: true + /text-hex@1.0.0: + resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==} + dev: false + /text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} dev: true @@ -8820,6 +8930,10 @@ packages: engines: {node: '>=12'} dev: true + /triple-beam@1.3.0: + resolution: {integrity: sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==} + dev: false + /ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} dev: true @@ -9220,7 +9334,6 @@ packages: /util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - dev: true /v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} @@ -9378,6 +9491,45 @@ packages: execa: 5.1.1 dev: true + /winston-daily-rotate-file@4.7.1(winston@3.9.0): + resolution: {integrity: sha512-7LGPiYGBPNyGHLn9z33i96zx/bd71pjBn9tqQzO3I4Tayv94WPmBNwKC7CO1wPHdP9uvu+Md/1nr6VSH9h0iaA==} + engines: {node: '>=8'} + peerDependencies: + winston: ^3 + dependencies: + file-stream-rotator: 0.6.1 + object-hash: 2.2.0 + triple-beam: 1.3.0 + winston: 3.9.0 + winston-transport: 4.5.0 + dev: false + + /winston-transport@4.5.0: + resolution: {integrity: sha512-YpZzcUzBedhlTAfJg6vJDlyEai/IFMIVcaEZZyl3UXIl4gmqRpU7AE89AHLkbzLUsv0NVmw7ts+iztqKxxPW1Q==} + engines: {node: '>= 6.4.0'} + dependencies: + logform: 2.5.1 + readable-stream: 3.6.2 + triple-beam: 1.3.0 + dev: false + + /winston@3.9.0: + resolution: {integrity: sha512-jW51iW/X95BCW6MMtZWr2jKQBP4hV5bIDq9QrIjfDk6Q9QuxvTKEAlpUNAzP+HYHFFCeENhph16s0zEunu4uuQ==} + engines: {node: '>= 12.0.0'} + dependencies: + '@colors/colors': 1.5.0 + '@dabh/diagnostics': 2.0.3 + async: 3.2.4 + is-stream: 2.0.1 + logform: 2.5.1 + one-time: 1.0.0 + readable-stream: 3.6.2 + safe-stable-stringify: 2.4.3 + stack-trace: 0.0.10 + triple-beam: 1.3.0 + winston-transport: 4.5.0 + dev: false + /word-wrap@1.2.3: resolution: {integrity: sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==} engines: {node: '>=0.10.0'} @@ -9592,4 +9744,3 @@ packages: /zod@3.21.4: resolution: {integrity: sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==} - dev: true diff --git a/src/config/index.ts b/src/config/index.ts index ee401d5..8e7c367 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -1,15 +1,30 @@ import { config } from "dotenv"; +import { z } from "zod"; + +import { validateEnvironmentVariables } from "../utils/validateEnvironmentVariables.js"; + +// eslint-disable-next-line node/no-process-env +config({ path: `.env.${process.env.NODE_ENV ?? "development"}` }); /** - * Loads environment variables from a .env file. - * @function - * @returns {void} + * Schema for the environment variables. + * @typedef {object} EnvironmentVariablesSchema + * @property {string} NODE_ENV - The environment the application is running in. + * @property {string} LOG_FORMAT - The format of the logs. + * @property {string} LOG_DIR - The directory to store the logs in. + * @see https://env.t3.gg/docs/recipes */ -export const loadEnvironmentVariables = (): void => { - config({ path: `.env.${process.env.NODE_ENV ?? "development"}` }); +const environmentVariablesSchema = { + NODE_ENV: z + .enum(["development", "production", "staging"]) + .optional() + .default("development"), + LOG_FORMAT: z.string().optional().default("combined"), + LOG_DIR: z.string().optional().default("./logs"), }; -/** - * The environment the application is running in. - */ -export const NODE_ENV = process.env.NODE_ENV; +export const environment = validateEnvironmentVariables( + environmentVariablesSchema, +); + +export const { NODE_ENV, LOG_FORMAT, LOG_DIR } = environment; diff --git a/src/config/validateEnvironment.ts b/src/config/validateEnvironment.ts deleted file mode 100644 index 6bdb211..0000000 --- a/src/config/validateEnvironment.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { cleanEnv, str } from "envalid"; - -/** - * Validates the environment variables needed for the application to run. - * @function - * @returns {void} - */ -export const validateEnvironmentVariables = (): void => { - cleanEnv(process.env, { - NODE_ENV: str(), - }); -}; - -declare global { - // By default, we do not want any namespace in Start UI [web] as it is more - // error prone and not useful in front end applications. - namespace NodeJS { - // eslint-disable-next-line @typescript-eslint/consistent-type-definitions, unicorn/prevent-abbreviations - interface ProcessEnv { - NODE_ENV: "development" | "production" | "test"; - } - } -} diff --git a/src/server.ts b/src/server.ts index 07a3b1c..49fd519 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,19 +1,19 @@ import http, { type IncomingMessage, type ServerResponse } from "node:http"; -import { loadEnvironmentVariables } from "./config"; -import { validateEnvironmentVariables } from "./config/validateEnvironment"; +import { log } from "./utils/log.js"; +import { logError } from "./utils/logError.js"; -loadEnvironmentVariables(); -validateEnvironmentVariables(); +try { + const server = http.createServer( + (_request: IncomingMessage, response: ServerResponse) => { + response.writeHead(200, { "Content-Type": "text/plain" }); + response.end("Hello, World!"); + }, + ); -const server = http.createServer( - (_request: IncomingMessage, response: ServerResponse) => { - response.writeHead(200, { "Content-Type": "text/plain" }); - response.end("Hello, World!"); - }, -); - -server.listen(3_000, () => { - // eslint-disable-next-line no-console - console.log("Server is listening on port 3000"); -}); + server.listen(3_000, () => { + log("Server is listening on port 3000"); + }); +} catch (error) { + logError(`Error in server.ts`, error); +} diff --git a/src/utils/Prettify.ts b/src/utils/Prettify.ts new file mode 100644 index 0000000..4a39215 --- /dev/null +++ b/src/utils/Prettify.ts @@ -0,0 +1,4 @@ +// https://twitter.com/mattpocockuk/status/1622730173446557697 +export type Prettify = { + [P in keyof T]: T[P]; +} & {}; diff --git a/src/utils/addAdditionalErrorMessage.ts b/src/utils/addAdditionalErrorMessage.ts new file mode 100644 index 0000000..19cda9b --- /dev/null +++ b/src/utils/addAdditionalErrorMessage.ts @@ -0,0 +1,14 @@ +import { getErrorMessage } from "./getErrorMessage.js"; + +/** + * Adds additional error message to the existing error message. + * @param {unknown} error - The error object. + * @param {string} errorMessage - The additional error message to be added. + * @returns {string} - The updated error message. + */ +export function addAdditionalErrorMessage( + error: unknown, + errorMessage: string, +): string { + return `${errorMessage} - ${getErrorMessage(error)}`; +} diff --git a/src/utils/ensureDirectoryAccess.ts b/src/utils/ensureDirectoryAccess.ts new file mode 100644 index 0000000..8a3903f --- /dev/null +++ b/src/utils/ensureDirectoryAccess.ts @@ -0,0 +1,33 @@ +import fsSync from "node:fs"; +import fs from "node:fs/promises"; + +import { throwError } from "./throwError.js"; + +/** + * Checks if a directory exists and is readable and writable. If it doesn't exist, it creates it. If it exists but is not readable or writable, it makes it both. + * @async + * @function + * @param {string} directoryPath - The path of the directory to check. + * @returns {Promise} A promise that resolves to a void indicating whether the directory exists and is readable and writable. + */ +export async function ensureDirectoryAccess( + directoryPath: string, +): Promise { + try { + await fs.access( + directoryPath, + // eslint-disable-next-line no-bitwise + fsSync.constants.F_OK | fsSync.constants.W_OK | fsSync.constants.R_OK, + ); + } catch { + try { + await fs.mkdir(directoryPath, { recursive: true }); + await fs.chmod(directoryPath, 0o777); + } catch (error) { + throwError( + error, + `${directoryPath} does not exists / writable / readable in ensureDirectoryAccess function`, + ); + } + } +} diff --git a/src/utils/errorLoggerWithStatus.ts b/src/utils/errorLoggerWithStatus.ts new file mode 100644 index 0000000..a0f1d9a --- /dev/null +++ b/src/utils/errorLoggerWithStatus.ts @@ -0,0 +1,9 @@ +import { logger } from "./logger.js"; + +export function errorLoggerWithStatus( + status: number, + message: string, + error?: unknown, +) { + logger.error(`StatusCode:: ${status}, Message:: ${message} -`, error); +} diff --git a/src/utils/getErrorMessage.ts b/src/utils/getErrorMessage.ts new file mode 100644 index 0000000..354a21c --- /dev/null +++ b/src/utils/getErrorMessage.ts @@ -0,0 +1,56 @@ +/** + * Represents an error object with a message. + */ +type ErrorWithMessage = { + message: string; +}; + +/** + * Determines whether a value is an Error object. + * @param {unknown} error - The value to test. + * @returns {boolean} True if the value is an Error object, false otherwise. + */ +export function isErrorObject(error: unknown): error is Error { + return error !== null && typeof error === "object"; +} + +/** + * Determines if an object is an ErrorWithMessage. + * @param {unknown} error - The object to check. + * @returns {boolean} True if the object is an ErrorWithMessage, false otherwise. + */ +function isErrorWithMessage(error: unknown): error is ErrorWithMessage { + return ( + isErrorObject(error) && + "message" in error && + typeof (error as ErrorWithMessage).message === "string" + ); +} + +/** + * Converts an object to an ErrorWithMessage. + * @param {unknown} maybeError - The object to convert. + * @returns {ErrorWithMessage} An ErrorWithMessage object. + */ +function toErrorWithMessage(maybeError: unknown): ErrorWithMessage { + if (isErrorWithMessage(maybeError)) { + return maybeError; + } + + try { + return new Error(JSON.stringify(maybeError)); + } catch { + // fallback in case there's an error stringify the maybeError + // like with circular references for example. + return new Error(String(maybeError)); + } +} + +/** + * Gets the message property of an ErrorWithMessage object. + * @param {unknown} error - The ErrorWithMessage object. + * @returns {string} The message property of the ErrorWithMessage object. + */ +export function getErrorMessage(error: unknown): string { + return toErrorWithMessage(error).message; +} diff --git a/src/utils/log.ts b/src/utils/log.ts new file mode 100644 index 0000000..a60bc38 --- /dev/null +++ b/src/utils/log.ts @@ -0,0 +1,5 @@ +import { logger } from "./logger.js"; + +export function log(message: string) { + logger.info(message); +} diff --git a/src/utils/logError.ts b/src/utils/logError.ts new file mode 100644 index 0000000..d6e3b49 --- /dev/null +++ b/src/utils/logError.ts @@ -0,0 +1,5 @@ +import { logger } from "./logger.js"; + +export function logError(message: string, error?: unknown) { + logger.error(`Message:: ${message} -`, error); +} diff --git a/src/utils/logger.ts b/src/utils/logger.ts new file mode 100644 index 0000000..b19c69b --- /dev/null +++ b/src/utils/logger.ts @@ -0,0 +1,106 @@ +import { join } from "node:path"; + +import { consola } from "consola"; +import { createLogger, format } from "winston"; +import winstonDaily from "winston-daily-rotate-file"; + +import { LOG_DIR, NODE_ENV } from "../config/index.js"; + +import { ensureDirectoryAccess } from "./ensureDirectoryAccess.js"; +import { throwError } from "./throwError.js"; + +/** + * The directory path where log files are stored. + */ +const logDirectory = join(LOG_DIR ?? "./logs"); + +// Ensure that the log directory exists +await ensureDirectoryAccess(logDirectory).catch((error) => + throwError(error, `Failed to access log directory ${logDirectory}`), +); + +const { combine, timestamp, printf, errors, prettyPrint } = format; + +// Define log format +const logFormat = printf( + ({ level, message, timestamp: _timestamp }) => + `${_timestamp} ${level}: ${message}`, +); +const jsonLogFileFormat = combine( + errors({ stack: true }), + timestamp({ + format: "YYYY-MM-DD HH:mm:ss", + }), + logFormat, + prettyPrint(), +); + +const isDevelopment = NODE_ENV === "development"; + +/* + * Log Level + * error: 0, warn: 1, info: 2, http: 3, verbose: 4, debug: 5, silly: 6 + */ +const winstonLogger = createLogger({ + format: jsonLogFileFormat, + transports: [ + // debug log setting + new winstonDaily({ + level: "debug", + datePattern: "YYYY-MM-DD", + // log file /logs/debug/*.log in save + dirname: logDirectory + "/debug", + filename: `%DATE%.log`, + // 30 Days saved + maxFiles: 30, + json: false, + zippedArchive: true, + }), + // error log setting + new winstonDaily({ + level: "error", + datePattern: "YYYY-MM-DD", + // log file /logs/error/*.log in save + dirname: logDirectory + "/error", + filename: `%DATE%.log`, + // 30 Days saved + maxFiles: 30, + handleExceptions: true, + json: false, + zippedArchive: true, + }), + ], +}); + +// Add console transport for logging to console +// if (isDevelopment) { +// winstonLogger.add( +// new transports.Console({ +// format: combine( +// errors({ stack: true }), +// colorize(), +// printf(({ level, message, timestamp: _timestamp, stack }) => { +// if (stack) { +// // print log trace +// console.error(stack); +// } + +// return `${_timestamp} ${level}: ${message}`; +// }), +// ), +// }), +// ); +// } + +// Define a stream for morgan to use for logging HTTP requests +const stream = { + write: (message: string) => { + winstonLogger.info( + message.slice(0, Math.max(0, message.lastIndexOf("\n"))), + ); + }, +}; + +const logger = isDevelopment ? consola : winstonLogger; + +export { logger, stream }; diff --git a/src/utils/throwError.ts b/src/utils/throwError.ts new file mode 100644 index 0000000..b8937f6 --- /dev/null +++ b/src/utils/throwError.ts @@ -0,0 +1,13 @@ +import { addAdditionalErrorMessage } from "./addAdditionalErrorMessage.js"; + +/** + * Throws an error with an additional error message. + * @param error - The original error. + * @param errorMessage - The additional error message. + * @throws {Error} + */ +export function throwError(error: unknown, errorMessage: string): never { + throw new Error(addAdditionalErrorMessage(error, errorMessage), { + cause: error, + }); +} diff --git a/src/utils/validateEnvironmentVariables.ts b/src/utils/validateEnvironmentVariables.ts new file mode 100644 index 0000000..73513d1 --- /dev/null +++ b/src/utils/validateEnvironmentVariables.ts @@ -0,0 +1,50 @@ +// https://github.com/t3-oss/t3-env/blob/main/packages/core/index.ts + +import { z, type ZodError, type ZodObject, type ZodType } from "zod"; + +import { type Prettify } from "./Prettify.js"; +import { throwError } from "./throwError.js"; + +const onValidationError = (error: ZodError) => { + console.error( + "❌ Invalid environment variables:", + error.flatten().fieldErrors, + ); + + throw new Error("Invalid environment variables"); +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type Impossible> = Partial< + Record +>; + +type InferredSchema> = Partial<{ + [Key in keyof Schema]: Schema[Key]; +}>; + +type ValidateEnvironmentVariablesOptions< + Schema extends Record, +> = Impossible> | InferredSchema; + +export function validateEnvironmentVariables< + Schema extends Record = NonNullable, +>( + schemaObject: ValidateEnvironmentVariablesOptions, +): Prettify>> { + try { + const _schemaObject = + typeof schemaObject === "object" ? (schemaObject as z.ZodRawShape) : {}; + + // eslint-disable-next-line node/no-process-env + const parsed = z.object(_schemaObject).safeParse(process.env); + + if (!parsed.success) { + return onValidationError(parsed.error); + } + + return parsed.data as unknown as Prettify>>; + } catch (error) { + return throwError(error, "Error in validateEnvironmentVariables function"); + } +}