diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..b05c2df --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +__snapshots__ diff --git a/package.json b/package.json index 4cf3ca6..b39016e 100644 --- a/package.json +++ b/package.json @@ -65,10 +65,10 @@ "prepublishOnly": "run-s -l test build" }, "peerDependencies": { - "zod": "^3.24.2" + "zod": "^4" }, "devDependencies": { - "@arethetypeswrong/cli": "^0.17.4", + "@arethetypeswrong/cli": "^0.18.2", "@types/jest": "^29.5.14", "@types/node": "^22.13.10", "eslint": "^9.22.0", @@ -78,8 +78,8 @@ "prettier": "^3.5.3", "ts-jest": "^29.2.6", "ts-node": "^10.9.2", - "typescript": "^5.8.2", - "zod": "~3.24.2" + "typescript": "^5.8.3", + "zod": "~4.1.12" }, "jest": { "preset": "ts-jest/presets/default-esm", @@ -100,5 +100,5 @@ ] } }, - "packageManager": "pnpm@10.6.5+sha512.cdf928fca20832cd59ec53826492b7dc25dc524d4370b6b4adbf65803d32efaa6c1c88147c0ae4e8d579a6c9eec715757b50d4fa35eea179d868eada4ed043af" + "packageManager": "pnpm@10.11.0+sha512.6540583f41cc5f628eb3d9773ecee802f4f9ef9923cc45b69890fb47991d4b092964694ec3a4f738a420c918a333062c8b925d312f42e4f0c263eb603551f977" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 27253af..acfde16 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: devDependencies: '@arethetypeswrong/cli': - specifier: ^0.17.4 - version: 0.17.4 + specifier: ^0.18.2 + version: 0.18.2 '@types/jest': specifier: ^29.5.14 version: 29.5.14 @@ -22,10 +22,10 @@ importers: version: 9.23.0 eslint-config-lostfictions: specifier: ^7.0.0 - version: 7.0.0(@typescript-eslint/parser@8.27.0(eslint@9.23.0)(typescript@5.8.2))(@typescript-eslint/utils@8.27.0(eslint@9.23.0)(typescript@5.8.2))(eslint@9.23.0)(is-bun-module@1.3.0)(typescript@5.8.2) + version: 7.0.0(@typescript-eslint/parser@8.27.0(eslint@9.23.0)(typescript@5.8.3))(@typescript-eslint/utils@8.27.0(eslint@9.23.0)(typescript@5.8.3))(eslint@9.23.0)(is-bun-module@1.3.0)(typescript@5.8.3) jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@22.13.11)(ts-node@10.9.2(@types/node@22.13.11)(typescript@5.8.2)) + version: 29.7.0(@types/node@22.13.11)(ts-node@10.9.2(@types/node@22.13.11)(typescript@5.8.3)) npm-run-all: specifier: ^4.1.5 version: 4.1.5 @@ -34,16 +34,16 @@ importers: version: 3.5.3 ts-jest: specifier: ^29.2.6 - version: 29.2.6(@babel/core@7.26.10)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.10))(jest@29.7.0(@types/node@22.13.11)(ts-node@10.9.2(@types/node@22.13.11)(typescript@5.8.2)))(typescript@5.8.2) + version: 29.2.6(@babel/core@7.26.10)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.10))(jest@29.7.0(@types/node@22.13.11)(ts-node@10.9.2(@types/node@22.13.11)(typescript@5.8.3)))(typescript@5.8.3) ts-node: specifier: ^10.9.2 - version: 10.9.2(@types/node@22.13.11)(typescript@5.8.2) + version: 10.9.2(@types/node@22.13.11)(typescript@5.8.3) typescript: - specifier: ^5.8.2 - version: 5.8.2 + specifier: ^5.8.3 + version: 5.8.3 zod: - specifier: ~3.24.2 - version: 3.24.2 + specifier: ~4.1.12 + version: 4.1.12 packages: @@ -54,14 +54,14 @@ packages: '@andrewbranch/untar.js@1.0.3': resolution: {integrity: sha512-Jh15/qVmrLGhkKJBdXlK1+9tY4lZruYjsgkDFj08ZmDiWVBLJcqkok7Z0/R0In+i1rScBpJlSvrTS2Lm41Pbnw==} - '@arethetypeswrong/cli@0.17.4': - resolution: {integrity: sha512-AeiKxtf67XD/NdOqXgBOE5TZWH3EOCt+0GkbUpekOzngc+Q/cRZ5azjWyMxISxxfp0EItgm5NoSld9p7BAA5xQ==} - engines: {node: '>=18'} + '@arethetypeswrong/cli@0.18.2': + resolution: {integrity: sha512-PcFM20JNlevEDKBg4Re29Rtv2xvjvQZzg7ENnrWFSS0PHgdP2njibVFw+dRUhNkPgNfac9iUqO0ohAXqQL4hbw==} + engines: {node: '>=20'} hasBin: true - '@arethetypeswrong/core@0.17.4': - resolution: {integrity: sha512-Izvir8iIoU+X4SKtDAa5kpb+9cpifclzsbA8x/AZY0k0gIfXYQ1fa1B6Epfe6vNA2YfDX8VtrZFgvnXB6aPEoQ==} - engines: {node: '>=18'} + '@arethetypeswrong/core@0.18.2': + resolution: {integrity: sha512-GiwTmBFOU1/+UVNqqCGzFJYfBXEytUkiI+iRZ6Qx7KmUVtLm00sYySkfe203C9QtPG11yOz1ZaMek8dT/xnlgg==} + engines: {node: '>=20'} '@babel/code-frame@7.26.2': resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} @@ -1833,8 +1833,9 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true - lru-cache@10.4.3: - resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@11.1.0: + resolution: {integrity: sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==} + engines: {node: 20 || >=22} lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -2519,8 +2520,8 @@ packages: engines: {node: '>=14.17'} hasBin: true - typescript@5.8.2: - resolution: {integrity: sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==} + typescript@5.8.3: + resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} engines: {node: '>=14.17'} hasBin: true @@ -2632,8 +2633,8 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - zod@3.24.2: - resolution: {integrity: sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==} + zod@4.1.12: + resolution: {integrity: sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==} snapshots: @@ -2644,9 +2645,9 @@ snapshots: '@andrewbranch/untar.js@1.0.3': {} - '@arethetypeswrong/cli@0.17.4': + '@arethetypeswrong/cli@0.18.2': dependencies: - '@arethetypeswrong/core': 0.17.4 + '@arethetypeswrong/core': 0.18.2 chalk: 4.1.2 cli-table3: 0.6.5 commander: 10.0.1 @@ -2654,13 +2655,13 @@ snapshots: marked-terminal: 7.3.0(marked@9.1.6) semver: 7.7.1 - '@arethetypeswrong/core@0.17.4': + '@arethetypeswrong/core@0.18.2': dependencies: '@andrewbranch/untar.js': 1.0.3 '@loaderkit/resolve': 1.0.4 cjs-module-lexer: 1.4.3 fflate: 0.8.2 - lru-cache: 10.4.3 + lru-cache: 11.1.0 semver: 7.7.1 typescript: 5.6.1-rc validate-npm-package-name: 5.0.1 @@ -2971,7 +2972,7 @@ snapshots: jest-util: 29.7.0 slash: 3.0.0 - '@jest/core@29.7.0(ts-node@10.9.2(@types/node@22.13.11)(typescript@5.8.2))': + '@jest/core@29.7.0(ts-node@10.9.2(@types/node@22.13.11)(typescript@5.8.3))': dependencies: '@jest/console': 29.7.0 '@jest/reporters': 29.7.0 @@ -2985,7 +2986,7 @@ snapshots: exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@22.13.11)(ts-node@10.9.2(@types/node@22.13.11)(typescript@5.8.2)) + jest-config: 29.7.0(@types/node@22.13.11)(ts-node@10.9.2(@types/node@22.13.11)(typescript@5.8.3)) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -3256,32 +3257,32 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@8.27.0(@typescript-eslint/parser@8.27.0(eslint@9.23.0)(typescript@5.8.2))(eslint@9.23.0)(typescript@5.8.2)': + '@typescript-eslint/eslint-plugin@8.27.0(@typescript-eslint/parser@8.27.0(eslint@9.23.0)(typescript@5.8.3))(eslint@9.23.0)(typescript@5.8.3)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.27.0(eslint@9.23.0)(typescript@5.8.2) + '@typescript-eslint/parser': 8.27.0(eslint@9.23.0)(typescript@5.8.3) '@typescript-eslint/scope-manager': 8.27.0 - '@typescript-eslint/type-utils': 8.27.0(eslint@9.23.0)(typescript@5.8.2) - '@typescript-eslint/utils': 8.27.0(eslint@9.23.0)(typescript@5.8.2) + '@typescript-eslint/type-utils': 8.27.0(eslint@9.23.0)(typescript@5.8.3) + '@typescript-eslint/utils': 8.27.0(eslint@9.23.0)(typescript@5.8.3) '@typescript-eslint/visitor-keys': 8.27.0 eslint: 9.23.0 graphemer: 1.4.0 ignore: 5.3.2 natural-compare: 1.4.0 - ts-api-utils: 2.1.0(typescript@5.8.2) - typescript: 5.8.2 + ts-api-utils: 2.1.0(typescript@5.8.3) + typescript: 5.8.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.27.0(eslint@9.23.0)(typescript@5.8.2)': + '@typescript-eslint/parser@8.27.0(eslint@9.23.0)(typescript@5.8.3)': dependencies: '@typescript-eslint/scope-manager': 8.27.0 '@typescript-eslint/types': 8.27.0 - '@typescript-eslint/typescript-estree': 8.27.0(typescript@5.8.2) + '@typescript-eslint/typescript-estree': 8.27.0(typescript@5.8.3) '@typescript-eslint/visitor-keys': 8.27.0 debug: 4.4.0 eslint: 9.23.0 - typescript: 5.8.2 + typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -3290,20 +3291,20 @@ snapshots: '@typescript-eslint/types': 8.27.0 '@typescript-eslint/visitor-keys': 8.27.0 - '@typescript-eslint/type-utils@8.27.0(eslint@9.23.0)(typescript@5.8.2)': + '@typescript-eslint/type-utils@8.27.0(eslint@9.23.0)(typescript@5.8.3)': dependencies: - '@typescript-eslint/typescript-estree': 8.27.0(typescript@5.8.2) - '@typescript-eslint/utils': 8.27.0(eslint@9.23.0)(typescript@5.8.2) + '@typescript-eslint/typescript-estree': 8.27.0(typescript@5.8.3) + '@typescript-eslint/utils': 8.27.0(eslint@9.23.0)(typescript@5.8.3) debug: 4.4.0 eslint: 9.23.0 - ts-api-utils: 2.1.0(typescript@5.8.2) - typescript: 5.8.2 + ts-api-utils: 2.1.0(typescript@5.8.3) + typescript: 5.8.3 transitivePeerDependencies: - supports-color '@typescript-eslint/types@8.27.0': {} - '@typescript-eslint/typescript-estree@8.27.0(typescript@5.8.2)': + '@typescript-eslint/typescript-estree@8.27.0(typescript@5.8.3)': dependencies: '@typescript-eslint/types': 8.27.0 '@typescript-eslint/visitor-keys': 8.27.0 @@ -3312,19 +3313,19 @@ snapshots: is-glob: 4.0.3 minimatch: 9.0.5 semver: 7.7.1 - ts-api-utils: 2.1.0(typescript@5.8.2) - typescript: 5.8.2 + ts-api-utils: 2.1.0(typescript@5.8.3) + typescript: 5.8.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.27.0(eslint@9.23.0)(typescript@5.8.2)': + '@typescript-eslint/utils@8.27.0(eslint@9.23.0)(typescript@5.8.3)': dependencies: '@eslint-community/eslint-utils': 4.5.1(eslint@9.23.0) '@typescript-eslint/scope-manager': 8.27.0 '@typescript-eslint/types': 8.27.0 - '@typescript-eslint/typescript-estree': 8.27.0(typescript@5.8.2) + '@typescript-eslint/typescript-estree': 8.27.0(typescript@5.8.3) eslint: 9.23.0 - typescript: 5.8.2 + typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -3368,12 +3369,12 @@ snapshots: '@unrs/rspack-resolver-binding-win32-x64-msvc@1.2.2': optional: true - '@vitest/eslint-plugin@1.1.38(@typescript-eslint/utils@8.27.0(eslint@9.23.0)(typescript@5.8.2))(eslint@9.23.0)(typescript@5.8.2)': + '@vitest/eslint-plugin@1.1.38(@typescript-eslint/utils@8.27.0(eslint@9.23.0)(typescript@5.8.3))(eslint@9.23.0)(typescript@5.8.3)': dependencies: - '@typescript-eslint/utils': 8.27.0(eslint@9.23.0)(typescript@5.8.2) + '@typescript-eslint/utils': 8.27.0(eslint@9.23.0)(typescript@5.8.3) eslint: 9.23.0 optionalDependencies: - typescript: 5.8.2 + typescript: 5.8.3 acorn-jsx@5.3.2(acorn@8.14.1): dependencies: @@ -3696,13 +3697,13 @@ snapshots: dependencies: browserslist: 4.24.4 - create-jest@29.7.0(@types/node@22.13.11)(ts-node@10.9.2(@types/node@22.13.11)(typescript@5.8.2)): + create-jest@29.7.0(@types/node@22.13.11)(ts-node@10.9.2(@types/node@22.13.11)(typescript@5.8.3)): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@22.13.11)(ts-node@10.9.2(@types/node@22.13.11)(typescript@5.8.2)) + jest-config: 29.7.0(@types/node@22.13.11)(ts-node@10.9.2(@types/node@22.13.11)(typescript@5.8.3)) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -3921,25 +3922,25 @@ snapshots: eslint: 9.23.0 semver: 7.7.1 - eslint-config-lostfictions@7.0.0(@typescript-eslint/parser@8.27.0(eslint@9.23.0)(typescript@5.8.2))(@typescript-eslint/utils@8.27.0(eslint@9.23.0)(typescript@5.8.2))(eslint@9.23.0)(is-bun-module@1.3.0)(typescript@5.8.2): + eslint-config-lostfictions@7.0.0(@typescript-eslint/parser@8.27.0(eslint@9.23.0)(typescript@5.8.3))(@typescript-eslint/utils@8.27.0(eslint@9.23.0)(typescript@5.8.3))(eslint@9.23.0)(is-bun-module@1.3.0)(typescript@5.8.3): dependencies: '@eslint-community/eslint-plugin-eslint-comments': 4.4.1(eslint@9.23.0) '@eslint/eslintrc': 3.3.1 '@eslint/js': 9.23.0 '@eslint/json': 0.9.1 - '@vitest/eslint-plugin': 1.1.38(@typescript-eslint/utils@8.27.0(eslint@9.23.0)(typescript@5.8.2))(eslint@9.23.0)(typescript@5.8.2) + '@vitest/eslint-plugin': 1.1.38(@typescript-eslint/utils@8.27.0(eslint@9.23.0)(typescript@5.8.3))(eslint@9.23.0)(typescript@5.8.3) eslint: 9.23.0 eslint-config-prettier: 10.1.1(eslint@9.23.0) - eslint-import-resolver-typescript: 4.2.2(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.27.0(eslint@9.23.0)(typescript@5.8.2))(eslint@9.23.0))(eslint@9.23.0)(is-bun-module@1.3.0) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.27.0(eslint@9.23.0)(typescript@5.8.2))(eslint-import-resolver-typescript@4.2.2(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.27.0(eslint@9.23.0)(typescript@5.8.2))(eslint@9.23.0))(eslint@9.23.0)(is-bun-module@1.3.0))(eslint@9.23.0) + eslint-import-resolver-typescript: 4.2.2(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.27.0(eslint@9.23.0)(typescript@5.8.3))(eslint@9.23.0))(eslint@9.23.0)(is-bun-module@1.3.0) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.27.0(eslint@9.23.0)(typescript@5.8.3))(eslint-import-resolver-typescript@4.2.2(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.27.0(eslint@9.23.0)(typescript@5.8.3))(eslint@9.23.0))(eslint@9.23.0)(is-bun-module@1.3.0))(eslint@9.23.0) eslint-plugin-n: 17.16.2(eslint@9.23.0) eslint-plugin-react: 7.37.4(eslint@9.23.0) eslint-plugin-react-hooks: 5.2.0(eslint@9.23.0) eslint-plugin-sonarjs: 3.0.2(eslint@9.23.0) eslint-plugin-unicorn: 56.0.1(eslint@9.23.0) globals: 16.0.0 - typescript: 5.8.2 - typescript-eslint: 8.27.0(eslint@9.23.0)(typescript@5.8.2) + typescript: 5.8.3 + typescript-eslint: 8.27.0(eslint@9.23.0)(typescript@5.8.3) transitivePeerDependencies: - '@typescript-eslint/parser' - '@typescript-eslint/utils' @@ -3961,7 +3962,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@4.2.2(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.27.0(eslint@9.23.0)(typescript@5.8.2))(eslint@9.23.0))(eslint@9.23.0)(is-bun-module@1.3.0): + eslint-import-resolver-typescript@4.2.2(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.27.0(eslint@9.23.0)(typescript@5.8.3))(eslint@9.23.0))(eslint@9.23.0)(is-bun-module@1.3.0): dependencies: debug: 4.4.0 eslint: 9.23.0 @@ -3970,19 +3971,19 @@ snapshots: stable-hash: 0.0.5 tinyglobby: 0.2.12 optionalDependencies: - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.27.0(eslint@9.23.0)(typescript@5.8.2))(eslint-import-resolver-typescript@4.2.2(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.27.0(eslint@9.23.0)(typescript@5.8.2))(eslint@9.23.0))(eslint@9.23.0)(is-bun-module@1.3.0))(eslint@9.23.0) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.27.0(eslint@9.23.0)(typescript@5.8.3))(eslint-import-resolver-typescript@4.2.2(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.27.0(eslint@9.23.0)(typescript@5.8.3))(eslint@9.23.0))(eslint@9.23.0)(is-bun-module@1.3.0))(eslint@9.23.0) is-bun-module: 1.3.0 transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.27.0(eslint@9.23.0)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.2.2(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.27.0(eslint@9.23.0)(typescript@5.8.2))(eslint@9.23.0))(eslint@9.23.0)(is-bun-module@1.3.0))(eslint@9.23.0): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.27.0(eslint@9.23.0)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.2.2(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.27.0(eslint@9.23.0)(typescript@5.8.3))(eslint@9.23.0))(eslint@9.23.0)(is-bun-module@1.3.0))(eslint@9.23.0): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.27.0(eslint@9.23.0)(typescript@5.8.2) + '@typescript-eslint/parser': 8.27.0(eslint@9.23.0)(typescript@5.8.3) eslint: 9.23.0 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 4.2.2(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.27.0(eslint@9.23.0)(typescript@5.8.2))(eslint@9.23.0))(eslint@9.23.0)(is-bun-module@1.3.0) + eslint-import-resolver-typescript: 4.2.2(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.27.0(eslint@9.23.0)(typescript@5.8.3))(eslint@9.23.0))(eslint@9.23.0)(is-bun-module@1.3.0) transitivePeerDependencies: - supports-color @@ -3993,7 +3994,7 @@ snapshots: eslint: 9.23.0 eslint-compat-utils: 0.5.1(eslint@9.23.0) - eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.27.0(eslint@9.23.0)(typescript@5.8.2))(eslint-import-resolver-typescript@4.2.2(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.27.0(eslint@9.23.0)(typescript@5.8.2))(eslint@9.23.0))(eslint@9.23.0)(is-bun-module@1.3.0))(eslint@9.23.0): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.27.0(eslint@9.23.0)(typescript@5.8.3))(eslint-import-resolver-typescript@4.2.2(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.27.0(eslint@9.23.0)(typescript@5.8.3))(eslint@9.23.0))(eslint@9.23.0)(is-bun-module@1.3.0))(eslint@9.23.0): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 @@ -4004,7 +4005,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.23.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.27.0(eslint@9.23.0)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.2.2(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.27.0(eslint@9.23.0)(typescript@5.8.2))(eslint@9.23.0))(eslint@9.23.0)(is-bun-module@1.3.0))(eslint@9.23.0) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.27.0(eslint@9.23.0)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.2.2(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.27.0(eslint@9.23.0)(typescript@5.8.3))(eslint@9.23.0))(eslint@9.23.0)(is-bun-module@1.3.0))(eslint@9.23.0) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -4016,7 +4017,7 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.27.0(eslint@9.23.0)(typescript@5.8.2) + '@typescript-eslint/parser': 8.27.0(eslint@9.23.0)(typescript@5.8.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -4071,7 +4072,7 @@ snapshots: minimatch: 9.0.5 scslre: 0.3.0 semver: 7.7.1 - typescript: 5.8.2 + typescript: 5.8.3 eslint-plugin-unicorn@56.0.1(eslint@9.23.0): dependencies: @@ -4613,16 +4614,16 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@29.7.0(@types/node@22.13.11)(ts-node@10.9.2(@types/node@22.13.11)(typescript@5.8.2)): + jest-cli@29.7.0(@types/node@22.13.11)(ts-node@10.9.2(@types/node@22.13.11)(typescript@5.8.3)): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@22.13.11)(typescript@5.8.2)) + '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@22.13.11)(typescript@5.8.3)) '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@22.13.11)(ts-node@10.9.2(@types/node@22.13.11)(typescript@5.8.2)) + create-jest: 29.7.0(@types/node@22.13.11)(ts-node@10.9.2(@types/node@22.13.11)(typescript@5.8.3)) exit: 0.1.2 import-local: 3.2.0 - jest-config: 29.7.0(@types/node@22.13.11)(ts-node@10.9.2(@types/node@22.13.11)(typescript@5.8.2)) + jest-config: 29.7.0(@types/node@22.13.11)(ts-node@10.9.2(@types/node@22.13.11)(typescript@5.8.3)) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -4632,7 +4633,7 @@ snapshots: - supports-color - ts-node - jest-config@29.7.0(@types/node@22.13.11)(ts-node@10.9.2(@types/node@22.13.11)(typescript@5.8.2)): + jest-config@29.7.0(@types/node@22.13.11)(ts-node@10.9.2(@types/node@22.13.11)(typescript@5.8.3)): dependencies: '@babel/core': 7.26.10 '@jest/test-sequencer': 29.7.0 @@ -4658,7 +4659,7 @@ snapshots: strip-json-comments: 3.1.1 optionalDependencies: '@types/node': 22.13.11 - ts-node: 10.9.2(@types/node@22.13.11)(typescript@5.8.2) + ts-node: 10.9.2(@types/node@22.13.11)(typescript@5.8.3) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -4878,12 +4879,12 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.7.0(@types/node@22.13.11)(ts-node@10.9.2(@types/node@22.13.11)(typescript@5.8.2)): + jest@29.7.0(@types/node@22.13.11)(ts-node@10.9.2(@types/node@22.13.11)(typescript@5.8.3)): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@22.13.11)(typescript@5.8.2)) + '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@22.13.11)(typescript@5.8.3)) '@jest/types': 29.6.3 import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@22.13.11)(ts-node@10.9.2(@types/node@22.13.11)(typescript@5.8.2)) + jest-cli: 29.7.0(@types/node@22.13.11)(ts-node@10.9.2(@types/node@22.13.11)(typescript@5.8.3)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -4966,7 +4967,7 @@ snapshots: dependencies: js-tokens: 4.0.0 - lru-cache@10.4.3: {} + lru-cache@11.1.0: {} lru-cache@5.1.1: dependencies: @@ -5604,22 +5605,22 @@ snapshots: dependencies: is-number: 7.0.0 - ts-api-utils@2.1.0(typescript@5.8.2): + ts-api-utils@2.1.0(typescript@5.8.3): dependencies: - typescript: 5.8.2 + typescript: 5.8.3 - ts-jest@29.2.6(@babel/core@7.26.10)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.10))(jest@29.7.0(@types/node@22.13.11)(ts-node@10.9.2(@types/node@22.13.11)(typescript@5.8.2)))(typescript@5.8.2): + ts-jest@29.2.6(@babel/core@7.26.10)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.10))(jest@29.7.0(@types/node@22.13.11)(ts-node@10.9.2(@types/node@22.13.11)(typescript@5.8.3)))(typescript@5.8.3): dependencies: bs-logger: 0.2.6 ejs: 3.1.10 fast-json-stable-stringify: 2.1.0 - jest: 29.7.0(@types/node@22.13.11)(ts-node@10.9.2(@types/node@22.13.11)(typescript@5.8.2)) + jest: 29.7.0(@types/node@22.13.11)(ts-node@10.9.2(@types/node@22.13.11)(typescript@5.8.3)) jest-util: 29.7.0 json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 semver: 7.7.1 - typescript: 5.8.2 + typescript: 5.8.3 yargs-parser: 21.1.1 optionalDependencies: '@babel/core': 7.26.10 @@ -5627,7 +5628,7 @@ snapshots: '@jest/types': 29.6.3 babel-jest: 29.7.0(@babel/core@7.26.10) - ts-node@10.9.2(@types/node@22.13.11)(typescript@5.8.2): + ts-node@10.9.2(@types/node@22.13.11)(typescript@5.8.3): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.11 @@ -5641,7 +5642,7 @@ snapshots: create-require: 1.1.1 diff: 4.0.2 make-error: 1.3.6 - typescript: 5.8.2 + typescript: 5.8.3 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 @@ -5700,19 +5701,19 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 - typescript-eslint@8.27.0(eslint@9.23.0)(typescript@5.8.2): + typescript-eslint@8.27.0(eslint@9.23.0)(typescript@5.8.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.27.0(@typescript-eslint/parser@8.27.0(eslint@9.23.0)(typescript@5.8.2))(eslint@9.23.0)(typescript@5.8.2) - '@typescript-eslint/parser': 8.27.0(eslint@9.23.0)(typescript@5.8.2) - '@typescript-eslint/utils': 8.27.0(eslint@9.23.0)(typescript@5.8.2) + '@typescript-eslint/eslint-plugin': 8.27.0(@typescript-eslint/parser@8.27.0(eslint@9.23.0)(typescript@5.8.3))(eslint@9.23.0)(typescript@5.8.3) + '@typescript-eslint/parser': 8.27.0(eslint@9.23.0)(typescript@5.8.3) + '@typescript-eslint/utils': 8.27.0(eslint@9.23.0)(typescript@5.8.3) eslint: 9.23.0 - typescript: 5.8.2 + typescript: 5.8.3 transitivePeerDependencies: - supports-color typescript@5.6.1-rc: {} - typescript@5.8.2: {} + typescript@5.8.3: {} unbox-primitive@1.1.0: dependencies: @@ -5850,4 +5851,4 @@ snapshots: yocto-queue@0.1.0: {} - zod@3.24.2: {} + zod@4.1.12: {} diff --git a/src/__snapshots__/reporter.test.ts.snap b/src/__snapshots__/reporter.test.ts.snap new file mode 100644 index 0000000..66bf095 --- /dev/null +++ b/src/__snapshots__/reporter.test.ts.snap @@ -0,0 +1,39 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`reporter should test the error output 1`] = ` +"Errors found while parsing environment: +[CONFIG_OBJECT]: This is a very special object that holds configuration. + ✖ Invalid input: expected string, received number + → at a + ✖ Invalid input: expected number, received undefined + → at b + ✖ Invalid input: expected string, received number + → at c.d + (received "{\\"a\\":123,\\"c\\":{\\"d\\":21}}") + +[FOO]: + ✖ Invalid input: expected number, received string + (received "bar") + +[DO_NOT_USE_LEGACY]: + ✖ This var is deprecated. + (received "this value shall not exist") + +[DO_NOT_USE_THIS_KEY]: + ✖ This var is deprecated. + (received "this value shall not exist") + +[SOME_WITH_DEPRECATED_DESCRIPTION]: Some description + ✖ Invalid input: expected string, received undefined + (received undefined) + +[SOME_WITH_DEPRECATED_DESCRIPTION_AND_NEW_DESCRIPTION]: New description + ✖ Invalid input: expected string, received undefined + (received undefined) + +[PORT]: + ✖ Too big: expected number to be <=65535 + (received undefined) + (used default of 70000) +" +`; diff --git a/src/extra-schemas.ts b/src/extra-schemas.ts index ac90064..891daf0 100644 --- a/src/extra-schemas.ts +++ b/src/extra-schemas.ts @@ -1,8 +1,8 @@ -import * as z from "zod"; +import * as z from "zod/v4"; export const port = () => z.number().int().nonnegative().lte(65535); export const deprecate = () => z - .undefined({ invalid_type_error: "This var is deprecated." }) + .undefined({ error: "This var is deprecated." }) .transform(() => undefined as never); diff --git a/src/parse-env.test.ts b/src/parse-env.test.ts index e9f0f95..3ad29b3 100644 --- a/src/parse-env.test.ts +++ b/src/parse-env.test.ts @@ -480,7 +480,7 @@ describe("parseCore", () => { ).toThrow(); }); - const schemasWithDefaults: [z.ZodTypeAny, any][] = [ + const schemasWithDefaults: [z.ZodType, any][] = [ [z.number(), 5], [z.object({ a: z.string(), b: z.bigint() }), { a: "ok", b: 4n }], ]; diff --git a/src/parse-env.ts b/src/parse-env.ts index 1c9b1ca..bb71701 100644 --- a/src/parse-env.ts +++ b/src/parse-env.ts @@ -1,25 +1,17 @@ -import * as z from "zod"; - +import { $ZodType, $ZodDefault, $ZodError } from "zod/v4/core"; import { getSchemaWithPreprocessor } from "./preprocessors.js"; import { + ErrorWithContext, makeDefaultReporter, - errorMap, - type TokenFormatters, - type ErrorWithContext, - type Reporter, + Reporter, + TokenFormatters, } from "./reporter.js"; - +import type * as z from "zod"; import type { DeepReadonlyObject } from "./util/type-helpers.js"; -export type SimpleSchema = z.ZodType< - TOut, - z.ZodTypeDef, - TIn ->; +export type SimpleSchema = $ZodType; -export type DetailedSpec< - TSchema extends SimpleSchema = SimpleSchema, -> = +export type DetailedSpec = TSchema extends SimpleSchema ? { /** @@ -31,6 +23,7 @@ export type DetailedSpec< /** * A description of this env var that's provided as help text if the * passed value fails validation, or is required but missing. + * @deprecated use e.g. `z.string().meta({ description: 'Your description' })` instead */ description?: string; @@ -110,6 +103,20 @@ export type ParseEnv = >( reporterOrTokenFormatters?: Reporter | TokenFormatters, ) => DeepReadonlyObject>; +const handleDeprecation = (type: $ZodType) => { + if ((type as z.ZodType).meta()?.deprecated) { + throw new $ZodError([ + { + code: "invalid_type", + message: "This var is deprecated.", + input: type, + path: [], + expected: "undefined", + }, + ]); + } +}; + /** * Parses the passed environment object using the provided map of Zod schemas * and returns the immutably-typed, parsed environment. @@ -124,15 +131,14 @@ export function parseEnvImpl>( schemas: T, reporterOrTokenFormatters: Reporter | TokenFormatters, ): DeepReadonlyObject> { + const parsed: Record = {} as DeepReadonlyObject< + ParsedSchema + >; const reporter = typeof reporterOrTokenFormatters === "function" ? reporterOrTokenFormatters : makeDefaultReporter(reporterOrTokenFormatters); - const parsed: Record = {} as DeepReadonlyObject< - ParsedSchema - >; - const errors: ErrorWithContext[] = []; for (const [key, schemaOrSpec] of Object.entries(schemas)) { @@ -141,10 +147,16 @@ export function parseEnvImpl>( let defaultUsed = false; let defaultValue: unknown; try { - if (schemaOrSpec instanceof z.ZodType) { - if (envValue == null && schemaOrSpec instanceof z.ZodDefault) { + handleDeprecation( + "schema" in schemaOrSpec ? schemaOrSpec.schema : schemaOrSpec, + ); + + if (schemaOrSpec instanceof $ZodType) { + if (envValue == null && schemaOrSpec instanceof $ZodDefault) { defaultUsed = true; - defaultValue = schemaOrSpec._def.defaultValue(); + const spec = schemaOrSpec._zod; + defaultValue = spec.def.defaultValue; + // we "unwrap" the default value ourselves and pass it to the schema. // in the very unlikely case that the value isn't stable AND // validation fails, this ensures the default value we report is the @@ -153,21 +165,17 @@ export function parseEnvImpl>( // we invoked the default getter and got 0.7, and then ran the parser // against a missing env var and it generated another default of 0.4, // we'd report a default value that _should_ have passed.) - parsed[key] = schemaOrSpec.parse(defaultValue, { errorMap }); + parsed[key] = (spec.def.innerType as z.ZodType).parse(defaultValue); } else { - parsed[key] = getSchemaWithPreprocessor(schemaOrSpec).parse( - envValue, - { errorMap }, - ); + parsed[key] = getSchemaWithPreprocessor(schemaOrSpec).parse(envValue); } } else if (envValue == null) { [defaultUsed, defaultValue] = resolveDefaultValueForSpec( schemaOrSpec.defaults, env["NODE_ENV"], ); - if (defaultUsed) { - parsed[key] = schemaOrSpec.schema.parse(defaultValue, { errorMap }); + parsed[key] = (schemaOrSpec.schema as z.ZodType).parse(defaultValue); } else { // if there's no default, pass our envValue through the // schema-with-preprocessor (it's an edge case, but our schema might @@ -175,13 +183,11 @@ export function parseEnvImpl>( // `null` for us). parsed[key] = getSchemaWithPreprocessor(schemaOrSpec.schema).parse( envValue, - { errorMap }, ); } } else { parsed[key] = getSchemaWithPreprocessor(schemaOrSpec.schema).parse( envValue, - { errorMap }, ); } } catch (e) { diff --git a/src/preprocessors.ts b/src/preprocessors.ts index 9ccfed1..f2d59c4 100644 --- a/src/preprocessors.ts +++ b/src/preprocessors.ts @@ -1,102 +1,56 @@ import * as z from "zod"; - import { assertNever } from "./util/type-helpers.js"; - -const { ZodFirstPartyTypeKind: TypeName } = z; +import { + bigInt, + boolean, + date, + identity, + json, + nullProcessor, + number, +} from "./util/processing.js"; +import type * as zCore from "zod/v4/core"; /** * Given a Zod schema, returns a function that tries to convert a string (or * undefined!) to a valid input type for the schema. */ export function getPreprocessorByZodType( - schema: z.ZodFirstPartySchemaTypes, + _schema: zCore.$ZodType, ): (arg: string | undefined) => unknown { - const def = schema._def; - const { typeName } = def; - - switch (typeName) { - case TypeName.ZodString: - case TypeName.ZodEnum: - case TypeName.ZodUndefined: - return (arg) => arg; - - case TypeName.ZodNumber: - return (arg) => { - if (typeof arg === "string" && /^-?\d+(\.\d+)?$/.test(arg)) { - return Number(arg); - } - return arg; - }; - - case TypeName.ZodBigInt: - return (arg) => { - if (typeof arg === "string" && /^-?\d+$/.test(arg)) { - return BigInt(arg); - } - return arg; - }; - - // env vars that act as flags might be declared in a number of ways, - // including simply `SOME_VALUE=` (with no RHS). the latter convention - // doesn't seem to be in widespread use with node, though. (that's probably - // because it results in the env var being present as the empty string, - // which is falsy.) - // - // this preprocessor is kind of a hedge -- it accepts a few different - // specific values to signify true or false. i can think of two other - // options: - // - coerce any value that's not `undefined` to `true` (or maybe any value - // that's not `undefined` or `false` or `0`, but again the complexity - // piles up quickly here). - // - coerce *only* 'true' and 'false' to their respective values. this could - // be complemented by a custom schema called 'flag' or something else that - // handles a looser coercion case (for now this is easy for users to do in - // their own code according to their needs). - // - // for now, this hedge seems to work fine, but it might be worth revisiting. - case TypeName.ZodBoolean: - return (arg) => { - if (typeof arg === "string") { - // eslint-disable-next-line default-case - switch (arg) { - case "true": - case "yes": - case "1": - return true; - case "false": - case "no": - case "0": - return false; - } - } - return arg; - }; - - case TypeName.ZodArray: - case TypeName.ZodObject: - case TypeName.ZodTuple: - case TypeName.ZodRecord: - case TypeName.ZodIntersection: - return (arg) => { - // neither `undefined` nor the empty string are valid json. - if (!arg) return arg; - // the one circumstance (so far) when i think a preprocessor should be - // able to throw is if we're coercing to json but it's invalid -- this - // way the error message will be more informative (rather than just - // "expected x, got string"). in the future `getPreprocessor` could - // maybe be refined to return a result type instead, but let's not - // overengineer things for now. - return JSON.parse(arg); - }; - - case TypeName.ZodEffects: - return getPreprocessorByZodType(def.schema); - - case TypeName.ZodDefault: - // eslint-disable-next-line unicorn/consistent-destructuring -- false positive + const schema = _schema as zCore.$ZodTypes; + const { + _zod: { def }, + } = schema; + + switch (def.type) { + case "pipe": + return getPreprocessorByZodType(def.in); + case "string": + case "enum": + case "undefined": + return identity; + + case "number": + return number; + + case "bigint": + return bigInt; + + case "boolean": + return boolean; + + case "array": + case "object": + case "tuple": + case "record": + case "intersection": + return json; + + case "default": return getPreprocessorByZodType(def.innerType); - case TypeName.ZodOptional: { + case "optional": { const { innerType } = def; const pp = getPreprocessorByZodType(innerType); return (arg) => { @@ -105,7 +59,7 @@ export function getPreprocessorByZodType( }; } - case TypeName.ZodNullable: { + case "nullable": { const { innerType } = def; const pp = getPreprocessorByZodType(innerType); return (arg) => { @@ -115,54 +69,34 @@ export function getPreprocessorByZodType( }; } - case TypeName.ZodDate: - return (arg) => { - // calling the 0-arity Date constructor makes a new Date with the - // current time, which definitely isn't what we want here. but calling - // the 1-arity Date constructor, even with `undefined`, should result in - // "invalid date" for values that aren't parseable. we filter out - // `undefined` anyway, though-- it makes typescript happier. - if (arg == null) return arg; - return new Date(arg); - }; + case "date": + return date; - case TypeName.ZodLiteral: - switch (typeof def.value) { + case "literal": + switch (typeof def.values?.[0]) { case "number": - return getPreprocessorByZodType({ - _def: { typeName: TypeName.ZodNumber }, - } as z.ZodFirstPartySchemaTypes); + return getPreprocessorByZodType(z.number()); case "string": - return getPreprocessorByZodType({ - _def: { typeName: TypeName.ZodString }, - } as z.ZodFirstPartySchemaTypes); + return getPreprocessorByZodType(z.string()); case "boolean": - return getPreprocessorByZodType({ - _def: { typeName: TypeName.ZodBoolean }, - } as z.ZodFirstPartySchemaTypes); + return getPreprocessorByZodType(z.boolean()); default: return (arg) => arg; } - case TypeName.ZodNull: - return (arg) => { - // coerce undefined to null. - if (arg == null) return null; - return arg; - }; + case "null": + return nullProcessor; - case TypeName.ZodDiscriminatedUnion: - case TypeName.ZodUnion: - case TypeName.ZodNativeEnum: + case "union": throw new Error( - `Zod type not yet supported: "${typeName}" (PRs welcome)`, + `Zod type not yet supported: "${def.type}" (PRs welcome)`, ); - case TypeName.ZodAny: - case TypeName.ZodUnknown: + case "any": + case "unknown": throw new Error( [ - `Zod type not supported: ${typeName}`, + `Zod type not supported: ${def.type}`, "You can use `z.string()` or `z.string().optional()` instead of the above type.", "(Environment variables are already constrained to `string | undefined`.)", ].join("\n"), @@ -171,23 +105,29 @@ export function getPreprocessorByZodType( // some of these types could maybe be supported (if only via the identity // function), but don't necessarily represent something meaningful as a // top-level schema passed to znv. - case TypeName.ZodVoid: - case TypeName.ZodNever: - case TypeName.ZodLazy: - case TypeName.ZodFunction: - case TypeName.ZodPromise: - case TypeName.ZodMap: - case TypeName.ZodSet: - case TypeName.ZodNaN: - case TypeName.ZodCatch: - case TypeName.ZodBranded: - case TypeName.ZodPipeline: - case TypeName.ZodSymbol: - case TypeName.ZodReadonly: - throw new Error(`Zod type not supported: ${typeName}`); - + case "success": + case "catch": + case "nan": + case "template_literal": + case "custom": + case "nonoptional": + case "prefault": + case "transform": + case "file": + case "void": + case "never": + case "lazy": + case "promise": + case "map": + case "set": + case "symbol": + case "function": + case "readonly": + throw new Error( + `Zod type not yet supported: "${def.type}" (PRs welcome)`, + ); default: { - assertNever(typeName); + assertNever(def); } } } @@ -196,9 +136,6 @@ export function getPreprocessorByZodType( * Given a Zod schema, return the schema wrapped in a preprocessor that tries to * convert a string to the schema's input type. */ -export function getSchemaWithPreprocessor(schema: z.ZodTypeAny) { - return z.preprocess( - getPreprocessorByZodType(schema) as (arg: unknown) => unknown, - schema, - ); +export function getSchemaWithPreprocessor(schema: zCore.$ZodType) { + return z.preprocess(getPreprocessorByZodType(schema), schema); } diff --git a/src/reporter.test.ts b/src/reporter.test.ts new file mode 100644 index 0000000..f4231e0 --- /dev/null +++ b/src/reporter.test.ts @@ -0,0 +1,49 @@ +import { z } from "zod"; +import { deprecate, parseEnv, port } from "./compat.js"; + +describe("reporter", () => { + it("should test the error output", () => { + expect(() => + parseEnv( + { + FOO: "bar", + CONFIG_OBJECT: JSON.stringify({ a: 123, c: { d: 21 } }), + DO_NOT_USE_THIS_KEY: "this value shall not exist", + DO_NOT_USE_LEGACY: "this value shall not exist", + }, + { + CONFIG_OBJECT: z + .object({ + a: z.string(), + b: z.number(), + c: z.object({ + d: z.string(), + }), + }) + .meta({ + description: + "This is a very special object that holds configuration.", + }), + BLA: z.string().default("bar"), + FOO: z.number(), + DO_NOT_USE_LEGACY: deprecate(), + DO_NOT_USE_THIS_KEY: z.string().meta({ + deprecated: true, + }), + SOME_WITH_DEPRECATED_DESCRIPTION: { + description: "Some description", + schema: z.string(), + }, + SOME_WITH_DEPRECATED_DESCRIPTION_AND_NEW_DESCRIPTION: { + description: "Some description", + schema: z.string().meta({ description: "New description" }), + }, + PORT: { + schema: port(), + defaults: { _: 70000 }, + }, + }, + ), + ).toThrowErrorMatchingSnapshot(); + }); +}); diff --git a/src/reporter.ts b/src/reporter.ts index 8445d65..122d124 100644 --- a/src/reporter.ts +++ b/src/reporter.ts @@ -1,20 +1,6 @@ -import { ZodError, ZodErrorMap, ZodIssueCode } from "zod"; +import { $ZodError, toDotPath } from "zod/v4/core"; import type { Schemas } from "./parse-env.js"; - -// Even though we also have our own formatter, we pass a custom error map to -// Zod's `.parse()` for two reasons: -// - to ensure that no other consumer of zod in the codebase has set a default -// error map that might override our formatting -// - to return slightly friendlier error messages in some common scenarios. -export const errorMap: ZodErrorMap = (issue, ctx) => { - if ( - issue.code === ZodIssueCode.invalid_type && - issue.received === "undefined" - ) { - return { message: "This field is required." }; - } - return { message: ctx.defaultError }; -}; +import type * as z from "zod/v4"; export interface ErrorWithContext { /** The env var name. */ @@ -57,14 +43,37 @@ export function makeDefaultReporter(formatters: TokenFormatters) { return reporter; } +// this is zod's `prettifyError`, but with formatting for the object path. +function prettifyError( + error: $ZodError, + formatObjectPath: (string: string) => string = String, +): string { + const lines: string[] = []; + // sort by path length + const issues = [...error.issues].sort( + (a, b) => a.path.length - b.path.length, + ); + + // Process each issue + for (const issue of issues) { + lines.push(`✖ ${issue.message}`); + if (issue.path?.length) { + lines.push(`→ at ${formatObjectPath(toDotPath(issue.path))}`); + } + } + + // Convert Map to formatted string + return lines.map((l) => indent(l, 2)).join("\n"); +} + export function reportErrors( errors: ErrorWithContext[], schemas: Schemas, { formatVarName = String, - formatObjKey = String, formatReceivedValue = String, formatDefaultValue = String, + formatObjKey = String, formatHeader = String, }: TokenFormatters = {}, ): string { @@ -72,26 +81,26 @@ export function reportErrors( ({ key, receivedValue, error, defaultUsed, defaultValue }) => { let title = `[${formatVarName(key)}]:`; - const desc = schemas[key]?.description; + const typeSchema = ( + schemas[key] && "schema" in schemas[key] + ? schemas[key].schema + : schemas[key] + ) as z.ZodType; + const meta = typeSchema.meta(); + const desc = + meta?.description ?? + (schemas[key] && "description" in schemas[key] + ? // eslint-disable-next-line @typescript-eslint/no-deprecated + schemas[key].description + : undefined); if (desc) { title += ` ${desc}`; } const message: string[] = [title]; - if (error instanceof ZodError) { - const { formErrors, fieldErrors } = error.flatten(); - for (const fe of formErrors) message.push(indent(fe, 2)); - const fieldErrorEntries = Object.entries(fieldErrors); - if (fieldErrorEntries.length > 0) { - message.push(indent("Errors on object keys:", 2)); - for (const [objKey, keyErrors] of fieldErrorEntries) { - message.push(indent(`[${formatObjKey(objKey)}]:`, 4)); - if (keyErrors) { - for (const fe of keyErrors) message.push(indent(fe, 6)); - } - } - } + if (error instanceof $ZodError) { + message.push(prettifyError(error, formatObjKey)); } else if (error instanceof Error) { message.push(...error.message.split("\n").map((l) => indent(l, 2))); } else { @@ -109,7 +118,7 @@ export function reportErrors( ? "undefined" : JSON.stringify(receivedValue), )})`, - 2, + 4, ), ); @@ -121,12 +130,12 @@ export function reportErrors( ? "undefined" : JSON.stringify(defaultValue), )})`, - 2, + 4, ), ); } - return message.map((l) => indent(l, 2)).join("\n"); + return message.join("\n"); }, ); diff --git a/src/util/processing.ts b/src/util/processing.ts new file mode 100644 index 0000000..cd8d900 --- /dev/null +++ b/src/util/processing.ts @@ -0,0 +1,80 @@ +type Input = string | undefined | null; + +export const number = (arg: Input) => { + if (typeof arg === "string" && /^-?\d+(\.\d+)?$/.test(arg)) { + return Number(arg); + } + return arg; +}; + +export const bigInt = (arg: Input) => { + if (typeof arg === "string" && /^-?\d+(\.\d+)?$/.test(arg)) { + return BigInt(arg); + } + return arg; +}; + +export const identity = (arg: Input) => arg; + +// env vars that act as flags might be declared in a number of ways, +// including simply `SOME_VALUE=` (with no RHS). the latter convention +// doesn't seem to be in widespread use with node, though. (that's probably +// because it results in the env var being present as the empty string, +// which is falsy.) +// +// this preprocessor is kind of a hedge -- it accepts a few different +// specific values to signify true or false. i can think of two other +// options: +// - coerce any value that's not `undefined` to `true` (or maybe any value +// that's not `undefined` or `false` or `0`, but again the complexity +// piles up quickly here). +// - coerce *only* 'true' and 'false' to their respective values. this could +// be complemented by a custom schema called 'flag' or something else that +// handles a looser coercion case (for now this is easy for users to do in +// their own code according to their needs). +// +// for now, this hedge seems to work fine, but it might be worth revisiting. +export const boolean = (arg: Input) => { + if (typeof arg === "string") { + // eslint-disable-next-line default-case + switch (arg) { + case "true": + case "yes": + case "1": + return true; + case "false": + case "no": + case "0": + return false; + } + } + return arg; +}; + +export const json = (arg: Input) => { + // neither `undefined` nor the empty string are valid json. + if (!arg) return arg; + // the one circumstance (so far) when i think a preprocessor should be + // able to throw is if we're coercing to json but it's invalid -- this + // way the error message will be more informative (rather than just + // "expected x, got string"). in the future `getPreprocessor` could + // maybe be refined to return a result type instead, but let's not + // overengineer things for now. + return JSON.parse(arg); +}; + +export const date = (arg: Input) => { + // calling the 0-arity Date constructor makes a new Date with the + // current time, which definitely isn't what we want here. but calling + // the 1-arity Date constructor, even with `undefined`, should result in + // "invalid date" for values that aren't parseable. we filter out + // `undefined` anyway, though-- it makes typescript happier. + if (arg == null) return arg; + return new Date(arg); +}; + +export const nullProcessor = (arg: Input) => { + // coerce undefined to null. + if (arg == null) return null; + return arg; +};