From f51f38768132ac8d0151417600872ea2b987deb7 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Sun, 4 Jan 2026 22:28:09 +0900 Subject: [PATCH 1/2] Implement hookform --- bun.lock | 219 +++- package.json | 2 + .../src/__tests__/generate-zod.test.ts | 967 ++++++++++++++++++ packages/generator/src/generate-zod.ts | 160 ++- packages/hookform/README.md | 202 ++++ packages/hookform/bunfig.toml | 2 + packages/hookform/package.json | 51 + .../__tests__/api-form-validation.test.tsx | 193 ++++ .../hookform/src/__tests__/api-form.test.tsx | 365 +++++++ .../__tests__/fetch-default-values.test.tsx | 274 +++++ .../__tests__/use-api-form-context.test.tsx | 348 +++++++ packages/hookform/src/api-form.tsx | 437 ++++++++ packages/hookform/src/index.ts | 27 + packages/hookform/src/types.ts | 310 ++++++ packages/hookform/tsconfig.json | 12 + packages/hookform/vite.config.ts | 56 + packages/zod/src/index.ts | 40 +- packages/zod/src/schema-struct.ts | 29 + 18 files changed, 3673 insertions(+), 21 deletions(-) create mode 100644 packages/hookform/README.md create mode 100644 packages/hookform/bunfig.toml create mode 100644 packages/hookform/package.json create mode 100644 packages/hookform/src/__tests__/api-form-validation.test.tsx create mode 100644 packages/hookform/src/__tests__/api-form.test.tsx create mode 100644 packages/hookform/src/__tests__/fetch-default-values.test.tsx create mode 100644 packages/hookform/src/__tests__/use-api-form-context.test.tsx create mode 100644 packages/hookform/src/api-form.tsx create mode 100644 packages/hookform/src/index.ts create mode 100644 packages/hookform/src/types.ts create mode 100644 packages/hookform/tsconfig.json create mode 100644 packages/hookform/vite.config.ts diff --git a/bun.lock b/bun.lock index d8fa40a..32c7d9a 100644 --- a/bun.lock +++ b/bun.lock @@ -121,6 +121,37 @@ "typescript": "^5.9", }, }, + "packages/hookform": { + "name": "@devup-api/hookform", + "version": "0.0.1", + "dependencies": { + "@devup-api/fetch": "workspace:^", + "@devup-api/zod": "workspace:^", + "@hookform/resolvers": ">=3.0.0", + "react-hook-form": ">=7.0.0", + }, + "devDependencies": { + "@tanstack/react-query": "^5.90.16", + "@testing-library/react": "^16.0.0", + "@types/node": "^25.0", + "@types/react": "^19.2", + "bun-test-env-dom": "^1.0.3", + "happy-dom": "^20.0.11", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "rollup-plugin-preserve-directives": "^0.4", + "typescript": "^5.9", + "vite": "^7.3", + "vite-plugin-dts": "^4.5", + "zod": "^3.25", + }, + "peerDependencies": { + "@tanstack/react-query": ">=5.0.0", + "react": "*", + "react-hook-form": "*", + "zod": "*", + }, + }, "packages/next-plugin": { "name": "@devup-api/next-plugin", "version": "0.1.8", @@ -222,7 +253,7 @@ }, "packages/zod": { "name": "@devup-api/zod", - "version": "0.1.13", + "version": "0.0.1", "dependencies": { "@devup-api/fetch": "workspace:^", "zod": ">=4", @@ -304,6 +335,8 @@ "@devup-api/generator": ["@devup-api/generator@workspace:packages/generator"], + "@devup-api/hookform": ["@devup-api/hookform@workspace:packages/hookform"], + "@devup-api/next-plugin": ["@devup-api/next-plugin@workspace:packages/next-plugin"], "@devup-api/react-query": ["@devup-api/react-query@workspace:packages/react-query"], @@ -386,6 +419,8 @@ "@happy-dom/global-registrator": ["@happy-dom/global-registrator@20.0.11", "", { "dependencies": { "@types/node": "^20.0.0", "happy-dom": "^20.0.11" } }, "sha512-GqNqiShBT/lzkHTMC/slKBrvN0DsD4Di8ssBk4aDaVgEn+2WMzE6DXxq701ndSXj7/0cJ8mNT71pM7Bnrr6JRw=="], + "@hookform/resolvers": ["@hookform/resolvers@5.2.2", "", { "dependencies": { "@standard-schema/utils": "^0.3.0" }, "peerDependencies": { "react-hook-form": "^7.55.0" } }, "sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA=="], + "@img/colour": ["@img/colour@1.0.0", "", {}, "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw=="], "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], @@ -452,6 +487,14 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@microsoft/api-extractor": ["@microsoft/api-extractor@7.55.2", "", { "dependencies": { "@microsoft/api-extractor-model": "7.32.2", "@microsoft/tsdoc": "~0.16.0", "@microsoft/tsdoc-config": "~0.18.0", "@rushstack/node-core-library": "5.19.1", "@rushstack/rig-package": "0.6.0", "@rushstack/terminal": "0.19.5", "@rushstack/ts-command-line": "5.1.5", "diff": "~8.0.2", "lodash": "~4.17.15", "minimatch": "10.0.3", "resolve": "~1.22.1", "semver": "~7.5.4", "source-map": "~0.6.1", "typescript": "5.8.2" }, "bin": { "api-extractor": "bin/api-extractor" } }, "sha512-1jlWO4qmgqYoVUcyh+oXYRztZde/pAi7cSVzBz/rc+S7CoVzDasy8QE13dx6sLG4VRo8SfkkLbFORR6tBw4uGQ=="], + + "@microsoft/api-extractor-model": ["@microsoft/api-extractor-model@7.32.2", "", { "dependencies": { "@microsoft/tsdoc": "~0.16.0", "@microsoft/tsdoc-config": "~0.18.0", "@rushstack/node-core-library": "5.19.1" } }, "sha512-Ussc25rAalc+4JJs9HNQE7TuO9y6jpYQX9nWD1DhqUzYPBr3Lr7O9intf+ZY8kD5HnIqeIRJX7ccCT0QyBy2Ww=="], + + "@microsoft/tsdoc": ["@microsoft/tsdoc@0.16.0", "", {}, "sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA=="], + + "@microsoft/tsdoc-config": ["@microsoft/tsdoc-config@0.18.0", "", { "dependencies": { "@microsoft/tsdoc": "0.16.0", "ajv": "~8.12.0", "jju": "~1.4.0", "resolve": "~1.22.2" } }, "sha512-8N/vClYyfOH+l4fLkkr9+myAoR6M7akc8ntBJ4DJdWH2b09uVfr71+LTMpNyG19fNqWDg8KEDZhx5wxuqHyGjw=="], + "@module-federation/error-codes": ["@module-federation/error-codes@0.21.6", "", {}, "sha512-MLJUCQ05KnoVl8xd6xs9a5g2/8U+eWmVxg7xiBMeR0+7OjdWUbHwcwgVFatRIwSZvFgKHfWEiI7wsU1q1XbTRQ=="], "@module-federation/runtime": ["@module-federation/runtime@0.21.6", "", { "dependencies": { "@module-federation/error-codes": "0.21.6", "@module-federation/runtime-core": "0.21.6", "@module-federation/sdk": "0.21.6" } }, "sha512-+caXwaQqwTNh+CQqyb4mZmXq7iEemRDrTZQGD+zyeH454JAYnJ3s/3oDFizdH6245pk+NiqDyOOkHzzFQorKhQ=="], @@ -486,6 +529,8 @@ "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.53", "", {}, "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ=="], + "@rollup/pluginutils": ["@rollup/pluginutils@5.3.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q=="], + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.53.5", "", { "os": "android", "cpu": "arm" }, "sha512-iDGS/h7D8t7tvZ1t6+WPK04KD0MwzLZrG0se1hzBjSi5fyxlsiggoJHwh18PCFNn7tG43OWb6pdZ6Y+rMlmyNQ=="], "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.53.5", "", { "os": "android", "cpu": "arm64" }, "sha512-wrSAViWvZHBMMlWk6EJhvg8/rjxzyEhEdgfMMjREHEq11EtJ6IP6yfcCH57YAEca2Oe3FNCE9DSTgU70EIGmVw=="], @@ -562,11 +607,23 @@ "@rspack/plugin-react-refresh": ["@rspack/plugin-react-refresh@1.5.3", "", { "dependencies": { "error-stack-parser": "^2.1.4", "html-entities": "^2.6.0" }, "peerDependencies": { "react-refresh": ">=0.10.0 <1.0.0", "webpack-hot-middleware": "2.x" }, "optionalPeers": ["webpack-hot-middleware"] }, "sha512-VOnQMf3YOHkTqJ0+BJbrYga4tQAWNwoAnkgwRauXB4HOyCc5wLfBs9DcOFla/2usnRT3Sq6CMVhXmdPobwAoTA=="], + "@rushstack/node-core-library": ["@rushstack/node-core-library@5.19.1", "", { "dependencies": { "ajv": "~8.13.0", "ajv-draft-04": "~1.0.0", "ajv-formats": "~3.0.1", "fs-extra": "~11.3.0", "import-lazy": "~4.0.0", "jju": "~1.4.0", "resolve": "~1.22.1", "semver": "~7.5.4" }, "peerDependencies": { "@types/node": "*" }, "optionalPeers": ["@types/node"] }, "sha512-ESpb2Tajlatgbmzzukg6zyAhH+sICqJR2CNXNhXcEbz6UGCQfrKCtkxOpJTftWc8RGouroHG0Nud1SJAszvpmA=="], + + "@rushstack/problem-matcher": ["@rushstack/problem-matcher@0.1.1", "", { "peerDependencies": { "@types/node": "*" }, "optionalPeers": ["@types/node"] }, "sha512-Fm5XtS7+G8HLcJHCWpES5VmeMyjAKaWeyZU5qPzZC+22mPlJzAsOxymHiWIfuirtPckX3aptWws+K2d0BzniJA=="], + + "@rushstack/rig-package": ["@rushstack/rig-package@0.6.0", "", { "dependencies": { "resolve": "~1.22.1", "strip-json-comments": "~3.1.1" } }, "sha512-ZQmfzsLE2+Y91GF15c65L/slMRVhF6Hycq04D4TwtdGaUAbIXXg9c5pKA5KFU7M4QMaihoobp9JJYpYcaY3zOw=="], + + "@rushstack/terminal": ["@rushstack/terminal@0.19.5", "", { "dependencies": { "@rushstack/node-core-library": "5.19.1", "@rushstack/problem-matcher": "0.1.1", "supports-color": "~8.1.1" }, "peerDependencies": { "@types/node": "*" }, "optionalPeers": ["@types/node"] }, "sha512-6k5tpdB88G0K7QrH/3yfKO84HK9ggftfUZ51p7fePyCE7+RLLHkWZbID9OFWbXuna+eeCFE7AkKnRMHMxNbz7Q=="], + + "@rushstack/ts-command-line": ["@rushstack/ts-command-line@5.1.5", "", { "dependencies": { "@rushstack/terminal": "0.19.5", "@types/argparse": "1.0.38", "argparse": "~1.0.9", "string-argv": "~0.3.1" } }, "sha512-YmrFTFUdHXblYSa+Xc9OO9FsL/XFcckZy0ycQ6q7VSBsVs5P0uD9vcges5Q9vctGlVdu27w+Ct6IuJ458V0cTQ=="], + + "@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="], + "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="], - "@tanstack/query-core": ["@tanstack/query-core@5.90.12", "", {}, "sha512-T1/8t5DhV/SisWjDnaiU2drl6ySvsHj1bHBCWNXd+/T+Hh1cf6JodyEYMd5sgwm+b/mETT4EV3H+zCVczCU5hg=="], + "@tanstack/query-core": ["@tanstack/query-core@5.90.16", "", {}, "sha512-MvtWckSVufs/ja463/K4PyJeqT+HMlJWtw6PrCpywznd2NSgO3m4KwO9RqbFqGg6iDE8vVMFWMeQI4Io3eEYww=="], - "@tanstack/react-query": ["@tanstack/react-query@5.90.12", "", { "dependencies": { "@tanstack/query-core": "5.90.12" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-graRZspg7EoEaw0a8faiUASCyJrqjKPdqJ9EwuDRUF9mEYJ1YPczI9H+/agJ0mOJkPCJDk0lsz5QTrLZ/jQ2rg=="], + "@tanstack/react-query": ["@tanstack/react-query@5.90.16", "", { "dependencies": { "@tanstack/query-core": "5.90.16" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-bpMGOmV4OPmif7TNMteU/Ehf/hoC0Kf98PDc0F4BZkFrEapRMEqI/V6YS0lyzwSV6PQpY1y4xxArUIfBW5LVxQ=="], "@testing-library/dom": ["@testing-library/dom@10.4.1", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="], @@ -580,6 +637,8 @@ "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], + "@types/argparse": ["@types/argparse@1.0.38", "", {}, "sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA=="], + "@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="], "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], @@ -612,6 +671,22 @@ "@vitejs/plugin-react": ["@vitejs/plugin-react@5.1.2", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.53", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ=="], + "@volar/language-core": ["@volar/language-core@2.4.27", "", { "dependencies": { "@volar/source-map": "2.4.27" } }, "sha512-DjmjBWZ4tJKxfNC1F6HyYERNHPYS7L7OPFyCrestykNdUZMFYzI9WTyvwPcaNaHlrEUwESHYsfEw3isInncZxQ=="], + + "@volar/source-map": ["@volar/source-map@2.4.27", "", {}, "sha512-ynlcBReMgOZj2i6po+qVswtDUeeBRCTgDurjMGShbm8WYZgJ0PA4RmtebBJ0BCYol1qPv3GQF6jK7C9qoVc7lg=="], + + "@volar/typescript": ["@volar/typescript@2.4.27", "", { "dependencies": { "@volar/language-core": "2.4.27", "path-browserify": "^1.0.1", "vscode-uri": "^3.0.8" } }, "sha512-eWaYCcl/uAPInSK2Lze6IqVWaBu/itVqR5InXcHXFyles4zO++Mglt3oxdgj75BDcv1Knr9Y93nowS8U3wqhxg=="], + + "@vue/compiler-core": ["@vue/compiler-core@3.5.26", "", { "dependencies": { "@babel/parser": "^7.28.5", "@vue/shared": "3.5.26", "entities": "^7.0.0", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-vXyI5GMfuoBCnv5ucIT7jhHKl55Y477yxP6fc4eUswjP8FG3FFVFd41eNDArR+Uk3QKn2Z85NavjaxLxOC19/w=="], + + "@vue/compiler-dom": ["@vue/compiler-dom@3.5.26", "", { "dependencies": { "@vue/compiler-core": "3.5.26", "@vue/shared": "3.5.26" } }, "sha512-y1Tcd3eXs834QjswshSilCBnKGeQjQXB6PqFn/1nxcQw4pmG42G8lwz+FZPAZAby6gZeHSt/8LMPfZ4Rb+Bd/A=="], + + "@vue/compiler-vue2": ["@vue/compiler-vue2@2.7.16", "", { "dependencies": { "de-indent": "^1.0.2", "he": "^1.2.0" } }, "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A=="], + + "@vue/language-core": ["@vue/language-core@2.2.0", "", { "dependencies": { "@volar/language-core": "~2.4.11", "@vue/compiler-dom": "^3.5.0", "@vue/compiler-vue2": "^2.7.16", "@vue/shared": "^3.5.0", "alien-signals": "^0.4.9", "minimatch": "^9.0.3", "muggle-string": "^0.4.1", "path-browserify": "^1.0.1" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-O1ZZFaaBGkKbsRfnVH1ifOK1/1BUkyK+3SQsfnh6PmMmD4qJcTU8godCeA96jjDRTL6zgnK7YzCHfaUlH2r0Mw=="], + + "@vue/shared": ["@vue/shared@3.5.26", "", {}, "sha512-7Z6/y3uFI5PRoKeorTOSXKcDj0MSasfNNltcslbFrPpcw6aXRUALq4IfJlaTRspiWIUOEZbrpM+iQGmCOiWe4A=="], + "@webassemblyjs/ast": ["@webassemblyjs/ast@1.14.1", "", { "dependencies": { "@webassemblyjs/helper-numbers": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2" } }, "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ=="], "@webassemblyjs/floating-point-hex-parser": ["@webassemblyjs/floating-point-hex-parser@1.13.2", "", {}, "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA=="], @@ -650,20 +725,30 @@ "acorn-import-phases": ["acorn-import-phases@1.0.4", "", { "peerDependencies": { "acorn": "^8.14.0" } }, "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ=="], - "ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], + "ajv": ["ajv@8.12.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js": "^4.2.2" } }, "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA=="], + + "ajv-draft-04": ["ajv-draft-04@1.0.0", "", { "peerDependencies": { "ajv": "^8.5.0" }, "optionalPeers": ["ajv"] }, "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw=="], - "ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="], + "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], "ajv-keywords": ["ajv-keywords@5.1.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3" }, "peerDependencies": { "ajv": "^8.8.2" } }, "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw=="], + "alien-signals": ["alien-signals@0.4.14", "", {}, "sha512-itUAVzhczTmP2U5yX67xVpsbbOiquusbWVyA9N+sy6+r6YVbFkahXvNCeEPWEOMhwDYwbVbGHFkVL03N9I5g+Q=="], + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], + "argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], + "aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="], + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.9.9", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-V8fbOCSeOFvlDj7LLChUcqbZrdKD9RU/VR260piF1790vT0mfLSwGc/Qzxv3IqiTukOpNtItePa0HBpMAj7MDg=="], + "brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], @@ -680,6 +765,10 @@ "commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], + "compare-versions": ["compare-versions@6.1.1", "", {}, "sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg=="], + + "confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="], + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], "core-js": ["core-js@3.47.0", "", {}, "sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg=="], @@ -690,18 +779,24 @@ "csstype-extra": ["csstype-extra@0.1.21", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-+bNbaI4AB6Sh9MeWlzHe+OPPDttu8oM8g6oyOSfoDmzjn6jdaixBdo6Sbu64apL9WEUfiuXuvDiVfqoSnyolBA=="], + "de-indent": ["de-indent@1.0.2", "", {}, "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + "diff": ["diff@8.0.2", "", {}, "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg=="], + "dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="], "electron-to-chromium": ["electron-to-chromium@1.5.267", "", {}, "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw=="], "enhanced-resolve": ["enhanced-resolve@5.18.4", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q=="], + "entities": ["entities@7.0.0", "", {}, "sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ=="], + "error-stack-parser": ["error-stack-parser@2.1.4", "", { "dependencies": { "stackframe": "^1.3.4" } }, "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ=="], "es-module-lexer": ["es-module-lexer@2.0.0", "", {}, "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw=="], @@ -716,16 +811,24 @@ "estraverse": ["estraverse@4.3.0", "", {}, "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="], + "estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + "events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="], + "exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + "fs-extra": ["fs-extra@11.3.3", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg=="], + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], "glob": ["glob@13.0.0", "", { "dependencies": { "minimatch": "^10.1.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA=="], @@ -738,16 +841,26 @@ "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + + "he": ["he@1.2.0", "", { "bin": { "he": "bin/he" } }, "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="], + "html-entities": ["html-entities@2.6.0", "", {}, "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ=="], "husky": ["husky@9.1.7", "", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="], + "import-lazy": ["import-lazy@4.0.0", "", {}, "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw=="], + "indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="], + "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], + "jest-worker": ["jest-worker@27.5.1", "", { "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg=="], "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], + "jju": ["jju@1.4.0", "", {}, "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA=="], + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], @@ -758,12 +871,22 @@ "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + "jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], + + "kolorist": ["kolorist@1.8.0", "", {}, "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ=="], + "loader-runner": ["loader-runner@4.3.1", "", {}, "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q=="], - "lru-cache": ["lru-cache@11.2.4", "", {}, "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg=="], + "local-pkg": ["local-pkg@1.1.2", "", { "dependencies": { "mlly": "^1.7.4", "pkg-types": "^2.3.0", "quansync": "^0.2.11" } }, "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A=="], + + "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], + + "lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="], + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="], "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], @@ -772,12 +895,16 @@ "min-indent": ["min-indent@1.0.1", "", {}, "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="], - "minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="], + "minimatch": ["minimatch@10.0.3", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw=="], "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], + "mlly": ["mlly@1.8.0", "", { "dependencies": { "acorn": "^8.15.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.1" } }, "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "muggle-string": ["muggle-string@0.4.1", "", {}, "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ=="], + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], "neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="], @@ -792,16 +919,28 @@ "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], + "path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="], + + "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + "path-scurry": ["path-scurry@2.0.1", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA=="], + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], - "postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], + "pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="], + + "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], "pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="], + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + + "quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="], + "randombytes": ["randombytes@2.1.0", "", { "dependencies": { "safe-buffer": "^5.1.0" } }, "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ=="], "react": ["react@19.2.3", "", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="], @@ -810,6 +949,8 @@ "react-error-boundary": ["react-error-boundary@3.1.4", "", { "dependencies": { "@babel/runtime": "^7.12.5" }, "peerDependencies": { "react": ">=16.13.1" } }, "sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA=="], + "react-hook-form": ["react-hook-form@7.70.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-COOMajS4FI3Wuwrs3GPpi/Jeef/5W1DRR84Yl5/ShlT3dKVFUfoGiEZ/QE6Uw8P4T2/CLJdcTVYKvWBMQTEpvw=="], + "react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], "react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="], @@ -818,8 +959,12 @@ "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + "resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], + "rollup": ["rollup@4.53.5", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.53.5", "@rollup/rollup-android-arm64": "4.53.5", "@rollup/rollup-darwin-arm64": "4.53.5", "@rollup/rollup-darwin-x64": "4.53.5", "@rollup/rollup-freebsd-arm64": "4.53.5", "@rollup/rollup-freebsd-x64": "4.53.5", "@rollup/rollup-linux-arm-gnueabihf": "4.53.5", "@rollup/rollup-linux-arm-musleabihf": "4.53.5", "@rollup/rollup-linux-arm64-gnu": "4.53.5", "@rollup/rollup-linux-arm64-musl": "4.53.5", "@rollup/rollup-linux-loong64-gnu": "4.53.5", "@rollup/rollup-linux-ppc64-gnu": "4.53.5", "@rollup/rollup-linux-riscv64-gnu": "4.53.5", "@rollup/rollup-linux-riscv64-musl": "4.53.5", "@rollup/rollup-linux-s390x-gnu": "4.53.5", "@rollup/rollup-linux-x64-gnu": "4.53.5", "@rollup/rollup-linux-x64-musl": "4.53.5", "@rollup/rollup-openharmony-arm64": "4.53.5", "@rollup/rollup-win32-arm64-msvc": "4.53.5", "@rollup/rollup-win32-ia32-msvc": "4.53.5", "@rollup/rollup-win32-x64-gnu": "4.53.5", "@rollup/rollup-win32-x64-msvc": "4.53.5", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-iTNAbFSlRpcHeeWu73ywU/8KuU/LZmNCSxp6fjQkJBD3ivUb8tpDrXhIxEzA05HlYMEwmtaUnb3RP+YNv162OQ=="], + "rollup-plugin-preserve-directives": ["rollup-plugin-preserve-directives@0.4.0", "", { "dependencies": { "@rollup/pluginutils": "^5.1.0", "magic-string": "^0.30.5" }, "peerDependencies": { "rollup": "2.x || 3.x || 4.x" } }, "sha512-gx4nBxYm5BysmEQS+e2tAMrtFxrGvk+Pe5ppafRibQi0zlW7VYAbEGk6IKDw9sJGPdFWgVTE0o4BU4cdG0Fylg=="], + "rsbuild-example": ["rsbuild-example@workspace:examples/rsbuild"], "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], @@ -828,7 +973,7 @@ "schema-utils": ["schema-utils@4.3.3", "", { "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", "ajv-formats": "^2.1.1", "ajv-keywords": "^5.1.0" } }, "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA=="], - "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "semver": ["semver@7.5.4", "", { "dependencies": { "lru-cache": "^6.0.0" }, "bin": { "semver": "bin/semver.js" } }, "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA=="], "serialize-javascript": ["serialize-javascript@6.0.2", "", { "dependencies": { "randombytes": "^2.1.0" } }, "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g=="], @@ -840,14 +985,22 @@ "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], + "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], + "stackframe": ["stackframe@1.3.4", "", {}, "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw=="], + "string-argv": ["string-argv@0.3.2", "", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="], + "strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="], + "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + "styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="], "supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], "terser": ["terser@5.44.1", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw=="], @@ -860,14 +1013,24 @@ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "ufo": ["ufo@1.6.1", "", {}, "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA=="], + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], + "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + "vite": ["vite@7.3.0", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg=="], "vite-example": ["vite-example@workspace:examples/vite"], + "vite-plugin-dts": ["vite-plugin-dts@4.5.4", "", { "dependencies": { "@microsoft/api-extractor": "^7.50.1", "@rollup/pluginutils": "^5.1.4", "@volar/typescript": "^2.4.11", "@vue/language-core": "2.2.0", "compare-versions": "^6.1.1", "debug": "^4.4.0", "kolorist": "^1.8.0", "local-pkg": "^1.0.0", "magic-string": "^0.30.17" }, "peerDependencies": { "typescript": "*", "vite": "*" }, "optionalPeers": ["vite"] }, "sha512-d4sOM8M/8z7vRXHHq/ebbblfaxENjogAAekcfcDCCwAyvGqnPrc7f4NZbvItS+g4WTgerW0xDwSz5qz11JT3vg=="], + + "vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="], + "watchpack": ["watchpack@2.4.4", "", { "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" } }, "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA=="], "webpack": ["webpack@5.104.0", "", { "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", "@types/json-schema": "^7.0.15", "@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1", "acorn": "^8.15.0", "acorn-import-phases": "^1.0.3", "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", "enhanced-resolve": "^5.17.4", "es-module-lexer": "^2.0.0", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", "loader-runner": "^4.3.1", "mime-types": "^2.1.27", "neo-async": "^2.6.2", "schema-utils": "^4.3.3", "tapable": "^2.3.0", "terser-webpack-plugin": "^5.3.16", "watchpack": "^2.4.4", "webpack-sources": "^3.3.3" }, "bin": { "webpack": "bin/webpack.js" } }, "sha512-5DeICTX8BVgNp6afSPYXAFjskIgWGlygQH58bcozPOXgo2r/6xx39Y1+cULZ3gTxUYQP88jmwLj2anu4Xaq84g=="], @@ -876,9 +1039,9 @@ "whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="], - "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + "yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], - "zod": ["zod@4.3.5", "", {}, "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g=="], + "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], @@ -886,7 +1049,9 @@ "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@devup-api/zod/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "@devup-api/react-query/@tanstack/react-query": ["@tanstack/react-query@5.90.12", "", { "dependencies": { "@tanstack/query-core": "5.90.12" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-graRZspg7EoEaw0a8faiUASCyJrqjKPdqJ9EwuDRUF9mEYJ1YPczI9H+/agJ0mOJkPCJDk0lsz5QTrLZ/jQ2rg=="], + + "@devup-api/zod/zod": ["zod@4.3.5", "", {}, "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g=="], "@devup-ui/next-plugin/next": ["next@16.0.10", "", { "dependencies": { "@next/env": "16.0.10", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.0.10", "@next/swc-darwin-x64": "16.0.10", "@next/swc-linux-arm64-gnu": "16.0.10", "@next/swc-linux-arm64-musl": "16.0.10", "@next/swc-linux-x64-gnu": "16.0.10", "@next/swc-linux-x64-musl": "16.0.10", "@next/swc-win32-arm64-msvc": "16.0.10", "@next/swc-win32-x64-msvc": "16.0.10", "sharp": "^0.34.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-RtWh5PUgI+vxlV3HdR+IfWA1UUHu0+Ram/JBO4vWB54cVPentCD0e+lxyAYEsDTqGGMg7qpjhKh6dc6aW7W/sA=="], @@ -894,15 +1059,39 @@ "@happy-dom/global-registrator/@types/node": ["@types/node@20.19.27", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug=="], + "@microsoft/api-extractor/typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="], + "@rsbuild/core/@swc/helpers": ["@swc/helpers@0.5.17", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A=="], + "@rushstack/node-core-library/ajv": ["ajv@8.13.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js": "^4.4.1" } }, "sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA=="], + "@testing-library/jest-dom/dom-accessibility-api": ["dom-accessibility-api@0.6.3", "", {}, "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w=="], + "@vue/language-core/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "ajv-formats/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], + "esrecurse/estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + "glob/minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="], + "happy-dom/@types/node": ["@types/node@20.19.27", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug=="], - "vite/postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], + "mlly/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], + + "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], + + "path-scurry/lru-cache": ["lru-cache@11.2.4", "", {}, "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg=="], + + "schema-utils/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], + + "schema-utils/ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="], + + "sharp/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + + "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + + "@devup-api/react-query/@tanstack/react-query/@tanstack/query-core": ["@tanstack/query-core@5.90.12", "", {}, "sha512-T1/8t5DhV/SisWjDnaiU2drl6ySvsHj1bHBCWNXd+/T+Hh1cf6JodyEYMd5sgwm+b/mETT4EV3H+zCVczCU5hg=="], "@devup-ui/next-plugin/next/@next/env": ["@next/env@16.0.10", "", {}, "sha512-8tuaQkyDVgeONQ1MeT9Mkk8pQmZapMKFh5B+OrFUlG3rVmYTXcXlBetBgTurKXGaIZvkoqRT9JL5K3phXcgang=="], @@ -922,8 +1111,12 @@ "@devup-ui/next-plugin/next/@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.0.10", "", { "os": "win32", "cpu": "x64" }, "sha512-E+njfCoFLb01RAFEnGZn6ERoOqhK1Gl3Lfz1Kjnj0Ulfu7oJbuMyvBKNj/bw8XZnenHDASlygTjZICQW+rYW1Q=="], + "@devup-ui/next-plugin/next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], + "@happy-dom/global-registrator/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], "happy-dom/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + + "mlly/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], } } diff --git a/package.json b/package.json index d6dd515..9b07029 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,8 @@ "author": "JeongMin Oh", "license": "Apache-2.0", "scripts": { + "test": "bun test", + "test:coverage": "bun test --coverage", "lint": "biome check", "lint:fix": "biome check --write", "prepare": "husky", diff --git a/packages/generator/src/__tests__/generate-zod.test.ts b/packages/generator/src/__tests__/generate-zod.test.ts index db186f8..507fa15 100644 --- a/packages/generator/src/__tests__/generate-zod.test.ts +++ b/packages/generator/src/__tests__/generate-zod.test.ts @@ -2923,3 +2923,970 @@ describe('generateZodTypeDeclarations - $ref resolution', () => { expect(result).toContain('z.ZodNumber') }) }) + +// ============================================================================= +// Inline Request Body Schema (non-$ref) +// ============================================================================= + +describe('generateZodSchemas - inline request body schema', () => { + test('handles requestBody with inline schema (no $ref)', () => { + const result = generateZodSchemas({ + 'openapi.json': createDocument({ + paths: { + '/users': { + post: { + operationId: 'createUser', + requestBody: { + content: { + 'application/json': { + // Inline schema, not a $ref + schema: { + type: 'object', + properties: { + name: { type: 'string' }, + age: { type: 'integer' }, + }, + required: ['name'], + }, + }, + }, + }, + responses: { '201': { description: 'Created' } }, + }, + }, + }, + }), + }) + + // Should still generate pathSchemas even without $ref + expect(result).toContain('pathSchemas') + expect(result).toContain('post') + }) + + test('handles requestBody with no application/json content', () => { + const result = generateZodSchemas({ + 'openapi.json': createDocument({ + paths: { + '/upload': { + post: { + operationId: 'uploadFile', + requestBody: { + content: { + 'multipart/form-data': { + schema: { + type: 'object', + properties: { + file: { type: 'string', format: 'binary' }, + }, + }, + }, + }, + }, + responses: { '200': { description: 'Success' } }, + }, + }, + }, + }), + }) + + // Should handle gracefully when no application/json content + expect(result).toContain('pathSchemas') + }) + + test('handles requestBody with empty content', () => { + const result = generateZodSchemas({ + 'openapi.json': createDocument({ + paths: { + '/test': { + post: { + operationId: 'testEmpty', + requestBody: { + content: {}, + }, + responses: { '200': { description: 'Success' } }, + }, + }, + }, + }), + }) + + expect(result).toContain('pathSchemas') + }) +}) + +// ============================================================================= +// generateZodTypeDeclarations - Edge Cases for schemaToZodType +// ============================================================================= + +describe('generateZodTypeDeclarations - schemaToZodType edge cases', () => { + test('handles empty allOf in type declaration', () => { + const result = generateZodTypeDeclarations({ + 'openapi.json': createDocument({ + paths: { + '/test': { + get: { + responses: { + '200': { + description: 'Success', + content: { + 'application/json': { + schema: { $ref: '#/components/schemas/Test' }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: { + Test: { allOf: [] }, + }, + }, + }), + }) + + expect(result).toContain('z.ZodUnknown') + }) + + test('handles single allOf in type declaration', () => { + const result = generateZodTypeDeclarations({ + 'openapi.json': createDocument({ + paths: { + '/test': { + get: { + responses: { + '200': { + description: 'Success', + content: { + 'application/json': { + schema: { $ref: '#/components/schemas/Test' }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: { + Test: { + allOf: [{ type: 'string' }], + }, + }, + }, + }), + }) + + expect(result).toContain('z.ZodString') + expect(result).not.toContain('z.ZodIntersection') + }) + + test('handles empty oneOf in type declaration', () => { + const result = generateZodTypeDeclarations({ + 'openapi.json': createDocument({ + paths: { + '/test': { + get: { + responses: { + '200': { + description: 'Success', + content: { + 'application/json': { + schema: { $ref: '#/components/schemas/Test' }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: { + Test: { oneOf: [] }, + }, + }, + }), + }) + + expect(result).toContain('z.ZodUnknown') + }) + + test('handles single oneOf in type declaration', () => { + const result = generateZodTypeDeclarations({ + 'openapi.json': createDocument({ + paths: { + '/test': { + get: { + responses: { + '200': { + description: 'Success', + content: { + 'application/json': { + schema: { $ref: '#/components/schemas/Test' }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: { + Test: { + oneOf: [{ type: 'number' }], + }, + }, + }, + }), + }) + + expect(result).toContain('z.ZodNumber') + expect(result).not.toContain('z.ZodUnion') + }) + + test('handles empty anyOf in type declaration', () => { + const result = generateZodTypeDeclarations({ + 'openapi.json': createDocument({ + paths: { + '/test': { + get: { + responses: { + '200': { + description: 'Success', + content: { + 'application/json': { + schema: { $ref: '#/components/schemas/Test' }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: { + Test: { anyOf: [] }, + }, + }, + }), + }) + + expect(result).toContain('z.ZodUnknown') + }) + + test('handles single anyOf in type declaration', () => { + const result = generateZodTypeDeclarations({ + 'openapi.json': createDocument({ + paths: { + '/test': { + get: { + responses: { + '200': { + description: 'Success', + content: { + 'application/json': { + schema: { $ref: '#/components/schemas/Test' }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: { + Test: { + anyOf: [{ type: 'boolean' }], + }, + }, + }, + }), + }) + + expect(result).toContain('z.ZodBoolean') + expect(result).not.toContain('z.ZodUnion') + }) + + test('handles $ref that fails to resolve in type declaration', () => { + const result = generateZodTypeDeclarations({ + 'openapi.json': createDocument({ + paths: { + '/test': { + get: { + responses: { + '200': { + description: 'Success', + content: { + 'application/json': { + schema: { $ref: '#/components/schemas/Test' }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: { + Test: { + type: 'object', + properties: { + // External ref that can't be resolved + external: { $ref: 'external.json#/schemas/Foo' }, + }, + }, + }, + }, + }), + }) + + expect(result).toContain('z.ZodUnknown') + }) + + test('handles nullable allOf in type declaration', () => { + const result = generateZodTypeDeclarations({ + 'openapi.json': createDocument({ + paths: { + '/test': { + get: { + responses: { + '200': { + description: 'Success', + content: { + 'application/json': { + schema: { $ref: '#/components/schemas/Test' }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: { + Test: { + allOf: [{ type: 'string' }, { type: 'object' }], + nullable: true, + }, + }, + }, + }), + }) + + expect(result).toContain('z.ZodNullable { + const result = generateZodTypeDeclarations({ + 'openapi.json': createDocument({ + paths: { + '/test': { + get: { + responses: { + '200': { + description: 'Success', + content: { + 'application/json': { + schema: { $ref: '#/components/schemas/Test' }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: { + Test: { + oneOf: [{ type: 'string' }, { type: 'number' }], + nullable: true, + }, + }, + }, + }), + }) + + expect(result).toContain('z.ZodNullable { + test('handles $ref to non-existent schema (schemaName exists but not in schemaRefs)', () => { + const result = generateZodSchemas({ + 'openapi.json': createDocument({ + paths: { + '/test': { + get: { + responses: { + '200': { + description: 'Success', + content: { + 'application/json': { + schema: { $ref: '#/components/schemas/Test' }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: { + Test: { + type: 'object', + properties: { + // References a schema that doesn't exist in components + missing: { $ref: '#/components/schemas/NonExistent' }, + }, + }, + }, + }, + }), + }) + + // Should fall back to z.unknown() when schema not found + expect(result).toContain('z.unknown()') + }) + + test('handles object property with $ref that has default value', () => { + const result = generateZodSchemas( + { + 'openapi.json': createDocument({ + paths: { + '/test': { + post: { + requestBody: { + content: { + 'application/json': { + schema: { $ref: '#/components/schemas/Test' }, + }, + }, + }, + responses: { '200': { description: 'Success' } }, + }, + }, + }, + components: { + schemas: { + WithDefault: { + type: 'string', + default: 'defaultValue', + }, + Test: { + type: 'object', + properties: { + refWithDefault: { $ref: '#/components/schemas/WithDefault' }, + }, + }, + }, + }, + }), + }, + { requestDefaultNonNullable: true }, + ) + + // With requestDefaultNonNullable: true and default value, should not be optional + expect(result).toContain('requestSchemas') + }) + + test('handles nested nullable allOf in schemaToZod', () => { + const result = generateZodSchemas({ + 'openapi.json': createDocument({ + paths: { + '/test': { + get: { + responses: { + '200': { + description: 'Success', + content: { + 'application/json': { + schema: { $ref: '#/components/schemas/Test' }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: { + Test: { + allOf: [{ type: 'string' }, { type: 'object' }], + nullable: true, + }, + }, + }, + }), + }) + + expect(result).toContain('z.intersection') + expect(result).toContain('.nullable()') + }) + + test('handles nullable oneOf in schemaToZod', () => { + const result = generateZodSchemas({ + 'openapi.json': createDocument({ + paths: { + '/test': { + get: { + responses: { + '200': { + description: 'Success', + content: { + 'application/json': { + schema: { $ref: '#/components/schemas/Test' }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: { + Test: { + oneOf: [{ type: 'string' }, { type: 'number' }], + nullable: true, + }, + }, + }, + }), + }) + + expect(result).toContain('z.union') + expect(result).toContain('.nullable()') + }) + + test('handles nullable single allOf', () => { + const result = generateZodSchemas({ + 'openapi.json': createDocument({ + paths: { + '/test': { + get: { + responses: { + '200': { + description: 'Success', + content: { + 'application/json': { + schema: { $ref: '#/components/schemas/Test' }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: { + Test: { + allOf: [{ type: 'string' }], + nullable: true, + }, + }, + }, + }), + }) + + expect(result).toContain('z.string()') + expect(result).toContain('.nullable()') + }) + + test('handles nullable single oneOf', () => { + const result = generateZodSchemas({ + 'openapi.json': createDocument({ + paths: { + '/test': { + get: { + responses: { + '200': { + description: 'Success', + content: { + 'application/json': { + schema: { $ref: '#/components/schemas/Test' }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: { + Test: { + oneOf: [{ type: 'number' }], + nullable: true, + }, + }, + }, + }), + }) + + expect(result).toContain('z.number()') + expect(result).toContain('.nullable()') + }) + + test('handles nullable enum', () => { + const result = generateZodSchemas({ + 'openapi.json': createDocument({ + paths: { + '/test': { + get: { + responses: { + '200': { + description: 'Success', + content: { + 'application/json': { + schema: { $ref: '#/components/schemas/Test' }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: { + Test: { + type: 'string', + enum: ['a', 'b'], + nullable: true, + }, + }, + }, + }), + }) + + expect(result).toContain('z.enum') + expect(result).toContain('.nullable()') + }) + + test('handles nullable single enum (literal)', () => { + const result = generateZodSchemas({ + 'openapi.json': createDocument({ + paths: { + '/test': { + get: { + responses: { + '200': { + description: 'Success', + content: { + 'application/json': { + schema: { $ref: '#/components/schemas/Test' }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: { + Test: { + type: 'string', + enum: ['only'], + nullable: true, + }, + }, + }, + }), + }) + + expect(result).toContain('z.literal') + expect(result).toContain('.nullable()') + }) + + test('handles nullable array', () => { + const result = generateZodSchemas({ + 'openapi.json': createDocument({ + paths: { + '/test': { + get: { + responses: { + '200': { + description: 'Success', + content: { + 'application/json': { + schema: { $ref: '#/components/schemas/Test' }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: { + Test: { + type: 'array', + items: { type: 'string' }, + nullable: true, + }, + }, + }, + }), + }) + + expect(result).toContain('z.array') + expect(result).toContain('.nullable()') + }) + + test('handles nullable array without items', () => { + const result = generateZodSchemas({ + 'openapi.json': createDocument({ + paths: { + '/test': { + get: { + responses: { + '200': { + description: 'Success', + content: { + 'application/json': { + schema: { $ref: '#/components/schemas/Test' }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: { + Test: { + type: 'array', + nullable: true, + }, + }, + }, + }), + }) + + expect(result).toContain('z.array(z.unknown())') + expect(result).toContain('.nullable()') + }) + + test('handles nullable object', () => { + const result = generateZodSchemas({ + 'openapi.json': createDocument({ + paths: { + '/test': { + get: { + responses: { + '200': { + description: 'Success', + content: { + 'application/json': { + schema: { $ref: '#/components/schemas/Test' }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: { + Test: { + type: 'object', + properties: { id: { type: 'string' } }, + nullable: true, + }, + }, + }, + }), + }) + + expect(result).toContain('z.object') + expect(result).toContain('.nullable()') + }) + + test('handles nullable boolean', () => { + const result = generateZodSchemas({ + 'openapi.json': createDocument({ + paths: { + '/test': { + get: { + responses: { + '200': { + description: 'Success', + content: { + 'application/json': { + schema: { $ref: '#/components/schemas/Test' }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: { + Test: { + type: 'boolean', + nullable: true, + }, + }, + }, + }), + }) + + expect(result).toContain('z.boolean()') + expect(result).toContain('.nullable()') + }) + + test('handles nullable number', () => { + const result = generateZodSchemas({ + 'openapi.json': createDocument({ + paths: { + '/test': { + get: { + responses: { + '200': { + description: 'Success', + content: { + 'application/json': { + schema: { $ref: '#/components/schemas/Test' }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: { + Test: { + type: 'number', + nullable: true, + }, + }, + }, + }), + }) + + expect(result).toContain('z.number()') + expect(result).toContain('.nullable()') + }) + + test('handles nullable integer', () => { + const result = generateZodSchemas({ + 'openapi.json': createDocument({ + paths: { + '/test': { + get: { + responses: { + '200': { + description: 'Success', + content: { + 'application/json': { + schema: { $ref: '#/components/schemas/Test' }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: { + Test: { + type: 'integer', + nullable: true, + }, + }, + }, + }), + }) + + expect(result).toContain('z.number().int()') + expect(result).toContain('.nullable()') + }) + + test('handles path with parameters', () => { + const result = generateZodSchemas({ + 'openapi.json': createDocument({ + paths: { + '/users/{userId}/posts/{postId}': { + get: { + operationId: 'getUserPost', + responses: { + '200': { + description: 'Success', + content: { + 'application/json': { + schema: { $ref: '#/components/schemas/Post' }, + }, + }, + }, + }, + }, + put: { + operationId: 'updateUserPost', + requestBody: { + content: { + 'application/json': { + schema: { $ref: '#/components/schemas/UpdatePost' }, + }, + }, + }, + responses: { '200': { description: 'Success' } }, + }, + }, + }, + components: { + schemas: { + Post: { type: 'object', properties: { title: { type: 'string' } } }, + UpdatePost: { + type: 'object', + properties: { title: { type: 'string' } }, + }, + }, + }, + }), + }) + + // Path with parameters should be normalized with case conversion + expect(result).toContain('pathSchemas') + expect(result).toContain('Post') + expect(result).toContain('UpdatePost') + }) + + test('handles path parameters with snake_case conversion', () => { + const result = generateZodSchemas( + { + 'openapi.json': createDocument({ + paths: { + '/users/{user_id}': { + post: { + operationId: 'create_user_item', + requestBody: { + content: { + 'application/json': { + schema: { $ref: '#/components/schemas/Item' }, + }, + }, + }, + responses: { '200': { description: 'Success' } }, + }, + }, + }, + components: { + schemas: { + Item: { + type: 'object', + properties: { name: { type: 'string' } }, + }, + }, + }, + }), + }, + { convertCase: 'snake' }, + ) + + expect(result).toContain('requestSchemas') + }) +}) diff --git a/packages/generator/src/generate-zod.ts b/packages/generator/src/generate-zod.ts index 84435b9..c50bb09 100644 --- a/packages/generator/src/generate-zod.ts +++ b/packages/generator/src/generate-zod.ts @@ -1,5 +1,6 @@ import type { DevupApiTypeGeneratorOptions } from '@devup-api/core' import type { OpenAPIV3_1 } from 'openapi-types' +import { convertCase } from './convert-case' import { wrapInterfaceKeyGuard } from './wrap-interface-key-guard' // ============================================================================= @@ -454,23 +455,51 @@ interface SchemaInfo { type: string // TypeScript Zod type } +interface PathSchemaMapping { + schemaName: string | null + operationId: string | null +} + interface CollectedSchemas { requestSchemas: Record responseSchemas: Record errorSchemas: Record + pathMappings: Record< + 'get' | 'post' | 'put' | 'delete' | 'patch', + Record + > } /** * Collect schema names used in request, response, and error positions + * Also collects path to schema mappings for hookform integration */ -function collectSchemaUsage(schema: OpenAPIV3_1.Document): { +function collectSchemaUsage( + schema: OpenAPIV3_1.Document, + options?: DevupApiTypeGeneratorOptions, +): { requestSchemaNames: Set responseSchemaNames: Set errorSchemaNames: Set + pathMappings: Record< + 'get' | 'post' | 'put' | 'delete' | 'patch', + Record + > } { const requestSchemaNames = new Set() const responseSchemaNames = new Set() const errorSchemaNames = new Set() + const pathMappings: Record< + 'get' | 'post' | 'put' | 'delete' | 'patch', + Record + > = { + get: {}, + post: {}, + put: {}, + delete: {}, + patch: {}, + } + const convertCaseType = options?.convertCase ?? 'camel' const collectSchemaNames = ( schemaObj: OpenAPIV3_1.SchemaObject | OpenAPIV3_1.ReferenceObject, @@ -508,8 +537,23 @@ function collectSchemaUsage(schema: OpenAPIV3_1.Document): { } } + // Helper to get direct schema name from request body + const getRequestBodySchemaName = ( + requestBody: OpenAPIV3_1.RequestBodyObject | OpenAPIV3_1.ReferenceObject, + ): string | null => { + if ('$ref' in requestBody) { + return extractSchemaNameFromRef(requestBody.$ref) + } + const content = requestBody.content + const jsonContent = content?.['application/json'] + if (jsonContent?.schema && '$ref' in jsonContent.schema) { + return extractSchemaNameFromRef(jsonContent.schema.$ref) + } + return null + } + if (schema.paths) { - for (const pathItem of Object.values(schema.paths)) { + for (const [path, pathItem] of Object.entries(schema.paths)) { if (!pathItem) continue const methods = ['get', 'post', 'put', 'delete', 'patch'] as const @@ -517,8 +561,21 @@ function collectSchemaUsage(schema: OpenAPIV3_1.Document): { const operation = pathItem[method] if (!operation) continue - // Collect request body schemas + // Normalize path for case conversion + const normalizedPath = path.replace(/\{([^}]+)\}/g, (_, param) => { + return `{${convertCase(param, convertCaseType)}}` + }) + + // Get operationId if exists + const operationId = operation.operationId + ? convertCase(operation.operationId, convertCaseType) + : null + + // Collect request body schemas and path mappings + let requestSchemaName: string | null = null if (operation.requestBody) { + requestSchemaName = getRequestBodySchemaName(operation.requestBody) + if ('$ref' in operation.requestBody) { const schemaName = extractSchemaNameFromRef( operation.requestBody.$ref, @@ -535,6 +592,16 @@ function collectSchemaUsage(schema: OpenAPIV3_1.Document): { } } + // Store path mapping + const mapping: PathSchemaMapping = { + schemaName: requestSchemaName, + operationId, + } + pathMappings[method][normalizedPath] = mapping + if (operationId) { + pathMappings[method][operationId] = mapping + } + // Collect response and error schemas if (operation.responses) { for (const [statusCode, response] of Object.entries( @@ -567,7 +634,12 @@ function collectSchemaUsage(schema: OpenAPIV3_1.Document): { } } - return { requestSchemaNames, responseSchemaNames, errorSchemaNames } + return { + requestSchemaNames, + responseSchemaNames, + errorSchemaNames, + pathMappings, + } } /** @@ -578,8 +650,12 @@ function generateSchemasForDocument( _serverName: string, options?: DevupApiTypeGeneratorOptions, ): CollectedSchemas { - const { requestSchemaNames, responseSchemaNames, errorSchemaNames } = - collectSchemaUsage(schema) + const { + requestSchemaNames, + responseSchemaNames, + errorSchemaNames, + pathMappings, + } = collectSchemaUsage(schema, options) const requestSchemas: Record = {} const responseSchemas: Record = {} @@ -647,7 +723,7 @@ function generateSchemasForDocument( } } - return { requestSchemas, responseSchemas, errorSchemas } + return { requestSchemas, responseSchemas, errorSchemas, pathMappings } } // ============================================================================= @@ -763,6 +839,34 @@ export function generateZodSchemas( lines.push(errorEntries || '') lines.push('};') lines.push('') + + // Path schemas object (maps path/operationId to request schema) + const methods = ['post', 'put', 'patch', 'delete'] as const + for (const method of methods) { + const pathEntries: string[] = [] + const methodMappings = collected.pathMappings[method] + + for (const [pathKey, mapping] of Object.entries(methodMappings)) { + if ( + mapping.schemaName && + collected.requestSchemas[mapping.schemaName] + ) { + pathEntries.push( + ` ${wrapInterfaceKeyGuard(pathKey)}: ${safeServerName}_request_${mapping.schemaName}`, + ) + } + } + + if (pathEntries.length > 0) { + lines.push(`export const ${safeServerName}_${method}PathSchemas = {`) + lines.push(pathEntries.join(',\n')) + lines.push('};') + lines.push('') + } else { + lines.push(`export const ${safeServerName}_${method}PathSchemas = {};`) + lines.push('') + } + } } // Generate combined schemas export @@ -786,6 +890,27 @@ export function generateZodSchemas( `export const responseSchemas = ${safeServerName}_responseSchemas;`, ) lines.push(`export const errorSchemas = ${safeServerName}_errorSchemas;`) + lines.push('') + lines.push('// Path to schema mappings') + lines.push( + `export const postPathSchemas = ${safeServerName}_postPathSchemas;`, + ) + lines.push( + `export const putPathSchemas = ${safeServerName}_putPathSchemas;`, + ) + lines.push( + `export const patchPathSchemas = ${safeServerName}_patchPathSchemas;`, + ) + lines.push( + `export const deletePathSchemas = ${safeServerName}_deletePathSchemas;`, + ) + lines.push('') + lines.push('export const pathSchemas = {') + lines.push(' post: postPathSchemas,') + lines.push(' put: putPathSchemas,') + lines.push(' patch: patchPathSchemas,') + lines.push(' delete: deletePathSchemas,') + lines.push('};') } else { // Multiple servers - export as nested object lines.push('export const schemas = {') @@ -814,6 +939,27 @@ export function generateZodSchemas( lines.push( `export const errorSchemas = ${safeDefaultServer}_errorSchemas;`, ) + lines.push('') + lines.push('// Path to schema mappings (first server)') + lines.push( + `export const postPathSchemas = ${safeDefaultServer}_postPathSchemas;`, + ) + lines.push( + `export const putPathSchemas = ${safeDefaultServer}_putPathSchemas;`, + ) + lines.push( + `export const patchPathSchemas = ${safeDefaultServer}_patchPathSchemas;`, + ) + lines.push( + `export const deletePathSchemas = ${safeDefaultServer}_deletePathSchemas;`, + ) + lines.push('') + lines.push('export const pathSchemas = {') + lines.push(' post: postPathSchemas,') + lines.push(' put: putPathSchemas,') + lines.push(' patch: patchPathSchemas,') + lines.push(' delete: deletePathSchemas,') + lines.push('};') } } diff --git a/packages/hookform/README.md b/packages/hookform/README.md new file mode 100644 index 0000000..0a99878 --- /dev/null +++ b/packages/hookform/README.md @@ -0,0 +1,202 @@ +# @devup-api/hookform + +Type-safe form components for devup-api with react-hook-form integration and automatic Zod validation. + +## Installation + +```bash +npm install @devup-api/hookform @devup-api/fetch react-hook-form zod +``` + +## Features + +- **Automatic Zod Validation**: Uses Zod schemas generated from your OpenAPI spec +- **FormProvider Integration**: Children can access form context via `useFormContext` +- **Type-Safe**: Full TypeScript support with inferred types from OpenAPI +- **Easy API Submission**: Handles form submission to your API endpoints automatically + +## Usage + +### Basic Setup + +```tsx +import { createApi } from '@devup-api/fetch' +import { ApiForm, useFormContext } from '@devup-api/hookform' + +const api = createApi('https://api.example.com') + +// Form fields component using form context +function FormFields() { + const { register, formState: { errors, isSubmitting } } = useFormContext() + + return ( + <> + + {errors.name && {errors.name.message}} + + + {errors.email && {errors.email.message}} + + + + ) +} + +// Main form component +function CreateUserForm() { + return ( + { + console.log('User created:', data) + }} + onError={(error) => { + console.error('Failed:', error) + }} + > + + + ) +} +``` + +### With Default Values + +```tsx + console.log('Updated:', data)} +> + + +``` + +### With Validation Mode + +```tsx + { + console.log('Validation failed:', errors) + }} + onSuccess={(data) => console.log('Created:', data)} +> + + +``` + +### Reset Form After Success + +```tsx + console.log('Created:', data)} +> + + +``` + +### Custom Form Props + +```tsx + console.log('Created:', data)} +> + + +``` + +## Props + +| Prop | Type | Description | +|------|------|-------------| +| `api` | `DevupApi` | The API client instance from `@devup-api/fetch` | +| `method` | `'post' \| 'put' \| 'patch' \| 'delete'` | HTTP method for form submission | +| `path` | `string` | API path or operationId | +| `openapi` | `string` | Server name for multi-server setups (default: 'openapi.json') | +| `requestOptions` | `object` | Additional request options (params, query, headers) | +| `onSuccess` | `(data) => void` | Called when API request succeeds | +| `onError` | `(error) => void` | Called when API request fails | +| `onValidationError` | `(errors) => void` | Called when form validation fails | +| `children` | `ReactNode` | Form content | +| `defaultValues` | `object` | Default values for form fields | +| `mode` | `'onSubmit' \| 'onBlur' \| 'onChange' \| 'onTouched' \| 'all'` | Validation mode (default: 'onSubmit') | +| `formOptions` | `UseFormProps` | Additional react-hook-form options | +| `formProps` | `FormHTMLAttributes` | HTML form element props | +| `resetOnSuccess` | `boolean` | Reset form after successful submission (default: false) | + +## Form Context + +Children components can access form context using react-hook-form's `useFormContext`: + +```tsx +import { useFormContext } from '@devup-api/hookform' + +function FormField({ name }: { name: string }) { + const { register, formState: { errors } } = useFormContext() + + return ( +
+ + {errors[name] && {errors[name].message}} +
+ ) +} +``` + +## How Validation Works + +The `ApiForm` component automatically uses Zod schemas generated from your OpenAPI spec: + +1. When you specify a `path` and `method`, the component looks up the corresponding request body schema +2. The schema is used with `@hookform/resolvers/zod` for validation +3. Form submission is blocked if validation fails +4. If no schema is found, the form submits without validation + +## Type Safety + +All props are fully typed based on your OpenAPI schema: + +- `path` is typed to only accept valid paths for the specified method +- `requestOptions` types match the endpoint's params/query/headers +- `onSuccess` receives the typed response data +- `onError` receives the typed error response +- `defaultValues` must match the request body schema + +## Re-exported from react-hook-form + +For convenience, the following are re-exported from react-hook-form: + +- `useFormContext` +- `useWatch` +- `useFieldArray` +- `useController` +- `Controller` + +## License + +Apache 2.0 diff --git a/packages/hookform/bunfig.toml b/packages/hookform/bunfig.toml new file mode 100644 index 0000000..5455450 --- /dev/null +++ b/packages/hookform/bunfig.toml @@ -0,0 +1,2 @@ +[test] +preload = ["bun-test-env-dom"] diff --git a/packages/hookform/package.json b/packages/hookform/package.json new file mode 100644 index 0000000..f4ec567 --- /dev/null +++ b/packages/hookform/package.json @@ -0,0 +1,51 @@ +{ + "name": "@devup-api/hookform", + "version": "0.0.1", + "license": "Apache-2.0", + "type": "module", + "exports": { + ".": { + "import": "./dist/index.js", + "require": "./dist/index.cjs", + "types": "./dist/index.d.ts" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsc && vite build", + "test": "bun test", + "test:coverage": "bun test --coverage" + }, + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@devup-api/fetch": "workspace:^", + "@devup-api/zod": "workspace:^", + "@hookform/resolvers": ">=3.0.0", + "react-hook-form": ">=7.0.0" + }, + "peerDependencies": { + "@tanstack/react-query": ">=5.0.0", + "react": "*", + "react-hook-form": "*", + "zod": "*" + }, + "devDependencies": { + "@tanstack/react-query": "^5.90.16", + "@testing-library/react": "^16.0.0", + "@types/node": "^25.0", + "@types/react": "^19.2", + "bun-test-env-dom": "^1.0.3", + "happy-dom": "^20.0.11", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "typescript": "^5.9", + "zod": "^3.25", + "rollup-plugin-preserve-directives": "^0.4", + "vite": "^7.3", + "vite-plugin-dts": "^4.5" + } +} diff --git a/packages/hookform/src/__tests__/api-form-validation.test.tsx b/packages/hookform/src/__tests__/api-form-validation.test.tsx new file mode 100644 index 0000000..08e8645 --- /dev/null +++ b/packages/hookform/src/__tests__/api-form-validation.test.tsx @@ -0,0 +1,193 @@ +/** biome-ignore-all lint/suspicious/noExplicitAny: any is used to allow for flexibility in the type */ +import { afterEach, beforeEach, expect, mock, test } from 'bun:test' +import { z } from 'zod' + +// Mock pathSchemas with test schema BEFORE importing ApiForm +const testSchema = z.object({ + name: z.string().min(1, 'Name is required'), + email: z.string().email('Invalid email'), +}) + +mock.module('@devup-api/zod', () => ({ + pathSchemas: { + post: { + '/validated-test': testSchema, + }, + put: {}, + patch: {}, + delete: {}, + }, +})) + +// Import after mock setup +import { createApi } from '@devup-api/fetch' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { fireEvent, render, waitFor } from '@testing-library/react' +import type { ReactNode } from 'react' +import { useFormContext } from 'react-hook-form' +import { ApiForm } from '../api-form' + +const originalFetch = globalThis.fetch + +// Create a new QueryClient for each test +function createTestQueryClient() { + return new QueryClient({ + defaultOptions: { + queries: { retry: false }, + mutations: { retry: false }, + }, + }) +} + +// Wrapper component with QueryClientProvider +function TestWrapper({ + children, + queryClient, +}: { + children: ReactNode + queryClient: QueryClient +}) { + return ( + {children} + ) +} + +beforeEach(() => { + globalThis.fetch = mock(() => + Promise.resolve( + new Response(JSON.stringify({ id: 1, name: 'test' }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }), + ), + ) as unknown as typeof fetch +}) + +afterEach(() => { + globalThis.fetch = originalFetch +}) + +function FormFields() { + const { register } = useFormContext() + return ( + <> + + + + + ) +} + +test('ApiForm validates form data with Zod schema and calls onValidationError on failure', async () => { + const api = createApi({ baseUrl: 'https://api.example.com' }) + const queryClient = createTestQueryClient() + const onValidationError = mock(() => {}) + const onSuccess = mock(() => {}) + + const { getByTestId } = render( + + + + + , + ) + + // Submit without filling required fields - should trigger validation error + const submitButton = getByTestId('submit-button') + fireEvent.click(submitButton) + + await waitFor( + () => { + expect(onValidationError).toHaveBeenCalled() + }, + { timeout: 5000 }, + ) + + // onSuccess should not be called due to validation failure + expect(onSuccess).not.toHaveBeenCalled() +}) + +test('ApiForm submits successfully when Zod validation passes', async () => { + const api = createApi({ baseUrl: 'https://api.example.com' }) + const queryClient = createTestQueryClient() + const onValidationError = mock(() => {}) + const onSuccess = mock(() => {}) + + const { getByTestId } = render( + + + + + , + ) + + const submitButton = getByTestId('submit-button') + fireEvent.click(submitButton) + + await waitFor( + () => { + expect(onSuccess).toHaveBeenCalled() + }, + { timeout: 5000 }, + ) + + // onValidationError should not be called when validation passes + expect(onValidationError).not.toHaveBeenCalled() +}) + +test('ApiForm shows validation errors for invalid email format', async () => { + const api = createApi({ baseUrl: 'https://api.example.com' }) + const queryClient = createTestQueryClient() + const onValidationError = mock(() => {}) + const onSuccess = mock(() => {}) + + const { getByTestId } = render( + + + + + , + ) + + // Fill name but invalid email + const nameInput = getByTestId('name-input') as HTMLInputElement + const emailInput = getByTestId('email-input') as HTMLInputElement + + fireEvent.change(nameInput, { target: { value: 'John Doe' } }) + fireEvent.change(emailInput, { target: { value: 'invalid-email' } }) + + const submitButton = getByTestId('submit-button') + fireEvent.click(submitButton) + + await waitFor( + () => { + expect(onValidationError).toHaveBeenCalled() + }, + { timeout: 5000 }, + ) + + // Verify validation errors contain email error + const callArgs = (onValidationError as any).mock.calls[0][0] + expect(callArgs.email).toBeDefined() +}) diff --git a/packages/hookform/src/__tests__/api-form.test.tsx b/packages/hookform/src/__tests__/api-form.test.tsx new file mode 100644 index 0000000..0d0f271 --- /dev/null +++ b/packages/hookform/src/__tests__/api-form.test.tsx @@ -0,0 +1,365 @@ +/** biome-ignore-all lint/suspicious/noExplicitAny: any is used to allow for flexibility in the type */ +import { afterEach, beforeEach, expect, mock, test } from 'bun:test' +import { createApi } from '@devup-api/fetch' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { fireEvent, render, waitFor } from '@testing-library/react' +import type { ReactNode } from 'react' +import { useFormContext } from 'react-hook-form' +import { ApiForm } from '../api-form' + +const originalFetch = globalThis.fetch + +// Create a new QueryClient for each test +function createTestQueryClient() { + return new QueryClient({ + defaultOptions: { + queries: { retry: false }, + mutations: { retry: false }, + }, + }) +} + +// Wrapper component with QueryClientProvider +function TestWrapper({ + children, + queryClient, +}: { + children: ReactNode + queryClient: QueryClient +}) { + return ( + {children} + ) +} + +beforeEach(() => { + globalThis.fetch = mock(() => + Promise.resolve( + new Response(JSON.stringify({ id: 1, name: 'test' }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }), + ), + ) as unknown as typeof fetch +}) + +afterEach(() => { + globalThis.fetch = originalFetch +}) + +// Test component that uses form context +function FormFields() { + const { register } = useFormContext() + return ( + <> + + + + + ) +} + +test('ApiForm renders children correctly', () => { + const api = createApi({ baseUrl: 'https://api.example.com' }) + const queryClient = createTestQueryClient() + + const { getByTestId } = render( + + + + + , + ) + + expect(getByTestId('name-input')).toBeDefined() + expect(getByTestId('email-input')).toBeDefined() + expect(getByTestId('submit-button')).toBeDefined() +}) + +test('ApiForm submits form data via API', async () => { + const api = createApi({ baseUrl: 'https://api.example.com' }) + const queryClient = createTestQueryClient() + const onSuccess = mock(() => {}) + + const { getByTestId } = render( + + + + + , + ) + + const nameInput = getByTestId('name-input') as HTMLInputElement + const emailInput = getByTestId('email-input') as HTMLInputElement + const submitButton = getByTestId('submit-button') + + fireEvent.change(nameInput, { target: { value: 'John Doe' } }) + fireEvent.change(emailInput, { target: { value: 'john@example.com' } }) + fireEvent.click(submitButton) + + await waitFor( + () => { + expect(onSuccess).toHaveBeenCalled() + }, + { timeout: 5000 }, + ) + + expect(onSuccess).toHaveBeenCalledWith({ id: 1, name: 'test' }) +}) + +test('ApiForm calls onError when API returns error', async () => { + globalThis.fetch = mock(() => + Promise.resolve( + new Response(JSON.stringify({ message: 'Error occurred' }), { + status: 400, + headers: { 'Content-Type': 'application/json' }, + }), + ), + ) as unknown as typeof fetch + + const api = createApi({ baseUrl: 'https://api.example.com' }) + const queryClient = createTestQueryClient() + const onError = mock(() => {}) + + const { getByTestId } = render( + + + + + , + ) + + const submitButton = getByTestId('submit-button') + fireEvent.click(submitButton) + + await waitFor( + () => { + expect(onError).toHaveBeenCalled() + }, + { timeout: 5000 }, + ) +}) + +test('ApiForm supports different HTTP methods', async () => { + const api = createApi({ baseUrl: 'https://api.example.com' }) + const methods = ['post', 'put', 'patch', 'delete'] as const + + for (const method of methods) { + const queryClient = createTestQueryClient() + const onSuccess = mock(() => {}) + + const { getByTestId, unmount } = render( + + + + + , + ) + + const submitButton = getByTestId('submit-button') + fireEvent.click(submitButton) + + await waitFor( + () => { + expect(onSuccess).toHaveBeenCalled() + }, + { timeout: 5000 }, + ) + + unmount() + } +}) + +test('ApiForm passes requestOptions to API call', async () => { + const api = createApi({ baseUrl: 'https://api.example.com' }) + const queryClient = createTestQueryClient() + const onSuccess = mock(() => {}) + + const { getByTestId } = render( + + + + + , + ) + + const submitButton = getByTestId('submit-button') + fireEvent.click(submitButton) + + await waitFor( + () => { + expect(onSuccess).toHaveBeenCalled() + }, + { timeout: 5000 }, + ) +}) + +test('ApiForm resets form on success when resetOnSuccess is true', async () => { + const api = createApi({ baseUrl: 'https://api.example.com' }) + const queryClient = createTestQueryClient() + const onSuccess = mock(() => {}) + + const { getByTestId } = render( + + + + + , + ) + + const nameInput = getByTestId('name-input') as HTMLInputElement + fireEvent.change(nameInput, { target: { value: 'John Doe' } }) + + expect(nameInput.value).toBe('John Doe') + + const submitButton = getByTestId('submit-button') + fireEvent.click(submitButton) + + await waitFor( + () => { + expect(onSuccess).toHaveBeenCalled() + }, + { timeout: 5000 }, + ) + + // Form should be reset + await waitFor(() => { + expect(nameInput.value).toBe('') + }) +}) + +test('ApiForm supports defaultValues', () => { + const api = createApi({ baseUrl: 'https://api.example.com' }) + const queryClient = createTestQueryClient() + + const { getByTestId } = render( + + + + + , + ) + + const nameInput = getByTestId('name-input') as HTMLInputElement + const emailInput = getByTestId('email-input') as HTMLInputElement + + expect(nameInput.value).toBe('Default Name') + expect(emailInput.value).toBe('default@example.com') +}) + +test('ApiForm supports formProps', () => { + const api = createApi({ baseUrl: 'https://api.example.com' }) + const queryClient = createTestQueryClient() + + const { container } = render( + + + + + , + ) + + const form = container.querySelector('form') + expect(form?.className).toBe('custom-form') + expect(form?.id).toBe('test-form') +}) + +test('ApiForm handles network errors', async () => { + globalThis.fetch = mock(() => + Promise.reject(new Error('Network error')), + ) as unknown as typeof fetch + + const api = createApi({ baseUrl: 'https://api.example.com' }) + const queryClient = createTestQueryClient() + const onError = mock(() => {}) + + const { getByTestId } = render( + + + + + , + ) + + const submitButton = getByTestId('submit-button') + fireEvent.click(submitButton) + + await waitFor( + () => { + expect(onError).toHaveBeenCalled() + }, + { timeout: 5000 }, + ) +}) + +test('ApiForm calls onValidationError when form validation fails', async () => { + // This test requires a zod schema to be set up for validation + // Since we don't have pathSchemas mocked, validation won't fail + // This test just ensures the callback prop is accepted + const api = createApi({ baseUrl: 'https://api.example.com' }) + const queryClient = createTestQueryClient() + const onValidationError = mock(() => {}) + const onSuccess = mock(() => {}) + + const { getByTestId } = render( + + + + + , + ) + + // Without schema validation, form will submit successfully + const submitButton = getByTestId('submit-button') + fireEvent.click(submitButton) + + await waitFor( + () => { + expect(onSuccess).toHaveBeenCalled() + }, + { timeout: 5000 }, + ) +}) diff --git a/packages/hookform/src/__tests__/fetch-default-values.test.tsx b/packages/hookform/src/__tests__/fetch-default-values.test.tsx new file mode 100644 index 0000000..32a3341 --- /dev/null +++ b/packages/hookform/src/__tests__/fetch-default-values.test.tsx @@ -0,0 +1,274 @@ +/** biome-ignore-all lint/suspicious/noExplicitAny: any is used for test flexibility */ +import { afterEach, expect, mock, test } from 'bun:test' +import { createApi } from '@devup-api/fetch' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { render, waitFor } from '@testing-library/react' +import type { ReactNode } from 'react' +import { useFormContext } from 'react-hook-form' +import { ApiForm, useApiFormContext } from '../api-form' + +const originalFetch = globalThis.fetch + +function createTestQueryClient() { + return new QueryClient({ + defaultOptions: { + queries: { retry: false }, + mutations: { retry: false }, + }, + }) +} + +function TestWrapper({ + children, + queryClient, +}: { + children: ReactNode + queryClient: QueryClient +}) { + return ( + {children} + ) +} + +afterEach(() => { + globalThis.fetch = originalFetch +}) + +function FormFields() { + const { register } = useFormContext() + return ( + <> + + + + + ) +} + +function FormFieldsWithLoading() { + const { form, isLoadingDefaultValues } = useApiFormContext() + const { register } = form + + return ( + <> + {isLoadingDefaultValues && ( + Loading defaults... + )} + + + + + ) +} + +test('ApiForm fetches default values with fetchDefaultValues config', async () => { + globalThis.fetch = mock(() => + Promise.resolve( + new Response( + JSON.stringify({ name: 'Fetched Name', email: 'fetched@example.com' }), + { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }, + ), + ), + ) as unknown as typeof fetch + + const api = createApi({ baseUrl: 'https://api.example.com' }) + const queryClient = createTestQueryClient() + + const { getByTestId } = render( + + + + + , + ) + + // Wait for default values to be fetched and populated + await waitFor( + () => { + const nameInput = getByTestId('name-input') as HTMLInputElement + expect(nameInput.value).toBe('Fetched Name') + }, + { timeout: 5000 }, + ) + + const emailInput = getByTestId('email-input') as HTMLInputElement + expect(emailInput.value).toBe('fetched@example.com') +}) + +test('ApiForm fetchDefaultValues with transform function', async () => { + globalThis.fetch = mock(() => + Promise.resolve( + new Response( + JSON.stringify({ + user: { name: 'Nested Name', email: 'nested@example.com' }, + }), + { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }, + ), + ), + ) as unknown as typeof fetch + + const api = createApi({ baseUrl: 'https://api.example.com' }) + const queryClient = createTestQueryClient() + + const { getByTestId } = render( + + response.user, + }} + > + + + , + ) + + await waitFor( + () => { + const nameInput = getByTestId('name-input') as HTMLInputElement + expect(nameInput.value).toBe('Nested Name') + }, + { timeout: 5000 }, + ) +}) + +test('ApiForm fetchDefaultValues handles fetch error gracefully', async () => { + globalThis.fetch = mock(() => + Promise.resolve( + new Response(JSON.stringify({ message: 'Not found' }), { + status: 404, + headers: { 'Content-Type': 'application/json' }, + }), + ), + ) as unknown as typeof fetch + + const api = createApi({ baseUrl: 'https://api.example.com' }) + const queryClient = createTestQueryClient() + + // Should not throw, form should still render + const { getByTestId } = render( + + + + + , + ) + + // Form should still be usable even if fetch fails + const nameInput = getByTestId('name-input') as HTMLInputElement + expect(nameInput).toBeDefined() +}) + +test('ApiForm without fetchDefaultValues uses provided defaultValues', () => { + globalThis.fetch = mock(() => + Promise.resolve( + new Response(JSON.stringify({ id: 1 }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }), + ), + ) as unknown as typeof fetch + + const api = createApi({ baseUrl: 'https://api.example.com' }) + const queryClient = createTestQueryClient() + + const { getByTestId } = render( + + + + + , + ) + + const nameInput = getByTestId('name-input') as HTMLInputElement + const emailInput = getByTestId('email-input') as HTMLInputElement + + expect(nameInput.value).toBe('Default Name') + expect(emailInput.value).toBe('default@example.com') +}) + +test('ApiForm isLoadingDefaultValues is true while fetching', async () => { + let resolvePromise: ((value: Response) => void) | undefined + const fetchPromise = new Promise((resolve) => { + resolvePromise = resolve + }) + + globalThis.fetch = mock(() => fetchPromise) as unknown as typeof fetch + + const api = createApi({ baseUrl: 'https://api.example.com' }) + const queryClient = createTestQueryClient() + + const { queryByTestId, getByTestId } = render( + + + + + , + ) + + // Should show loading initially + await waitFor(() => { + expect(queryByTestId('loading')).not.toBeNull() + }) + + // Resolve the fetch + resolvePromise?.( + new Response( + JSON.stringify({ name: 'Loaded', email: 'loaded@example.com' }), + { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }, + ), + ) + + // Loading should disappear after data loads + await waitFor( + () => { + const nameInput = getByTestId('name-input') as HTMLInputElement + expect(nameInput.value).toBe('Loaded') + }, + { timeout: 5000 }, + ) +}) diff --git a/packages/hookform/src/__tests__/use-api-form-context.test.tsx b/packages/hookform/src/__tests__/use-api-form-context.test.tsx new file mode 100644 index 0000000..83363a1 --- /dev/null +++ b/packages/hookform/src/__tests__/use-api-form-context.test.tsx @@ -0,0 +1,348 @@ +/** biome-ignore-all lint/suspicious/noExplicitAny: any is used for test flexibility */ +import { afterEach, beforeEach, expect, mock, spyOn, test } from 'bun:test' +import { createApi } from '@devup-api/fetch' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { fireEvent, render, waitFor } from '@testing-library/react' +import type { ReactNode } from 'react' +import { ApiForm, useApiFormContext } from '../api-form' + +const originalFetch = globalThis.fetch + +function createTestQueryClient() { + return new QueryClient({ + defaultOptions: { + queries: { retry: false }, + mutations: { retry: false }, + }, + }) +} + +function TestWrapper({ + children, + queryClient, +}: { + children: ReactNode + queryClient: QueryClient +}) { + return ( + {children} + ) +} + +beforeEach(() => { + globalThis.fetch = mock(() => + Promise.resolve( + new Response(JSON.stringify({ id: 1, name: 'test' }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }), + ), + ) as unknown as typeof fetch +}) + +afterEach(() => { + globalThis.fetch = originalFetch +}) + +// Component that uses new API structure: { form, mutation } +function FormFieldsWithContext() { + const { form, mutation, isLoadingDefaultValues } = useApiFormContext() + const { register } = form + const { isPending, isSuccess, isError, error, data } = mutation + + return ( + <> + + + + {isSuccess && Success!} + {isError && ( + {String(error) || 'Error'} + )} + {data && {JSON.stringify(data)}} + {isLoadingDefaultValues && ( + Loading... + )} + + ) +} + +test('useApiFormContext provides form and mutation objects', async () => { + const api = createApi({ baseUrl: 'https://api.example.com' }) + const queryClient = createTestQueryClient() + + const { getByTestId } = render( + + + + + , + ) + + // Form should work + const nameInput = getByTestId('name-input') as HTMLInputElement + expect(nameInput).toBeDefined() + + // Button should show initial state + const submitButton = getByTestId('submit-button') + expect(submitButton.textContent).toBe('Submit') +}) + +test('useApiFormContext mutation state updates on submit', async () => { + const api = createApi({ baseUrl: 'https://api.example.com' }) + const queryClient = createTestQueryClient() + const onSuccess = mock(() => {}) + + const { getByTestId, queryByTestId } = render( + + + + + , + ) + + const nameInput = getByTestId('name-input') as HTMLInputElement + fireEvent.change(nameInput, { target: { value: 'John' } }) + + const submitButton = getByTestId('submit-button') + fireEvent.click(submitButton) + + // Wait for success + await waitFor( + () => { + expect(queryByTestId('success-message')).not.toBeNull() + }, + { timeout: 5000 }, + ) + + expect(onSuccess).toHaveBeenCalled() +}) + +test('useApiFormContext shows error state on API failure', async () => { + globalThis.fetch = mock(() => + Promise.resolve( + new Response(JSON.stringify({ message: 'Failed' }), { + status: 400, + headers: { 'Content-Type': 'application/json' }, + }), + ), + ) as unknown as typeof fetch + + const api = createApi({ baseUrl: 'https://api.example.com' }) + const queryClient = createTestQueryClient() + const onError = mock(() => {}) + + const { getByTestId } = render( + + + + + , + ) + + const submitButton = getByTestId('submit-button') + fireEvent.click(submitButton) + + await waitFor( + () => { + expect(onError).toHaveBeenCalled() + }, + { timeout: 5000 }, + ) +}) + +test('useApiFormContext throws error when used outside ApiForm', () => { + function InvalidComponent() { + useApiFormContext() + return
Should not render
+ } + + // Suppress console.error for expected error + const consoleSpy = spyOn(console, 'error').mockImplementation(() => {}) + + expect(() => render()).toThrow( + 'useApiFormContext must be used within an ApiForm', + ) + + consoleSpy.mockRestore() +}) + +test('useApiFormContext mutation.mutate can be called directly', async () => { + const api = createApi({ baseUrl: 'https://api.example.com' }) + const queryClient = createTestQueryClient() + const onSuccess = mock(() => {}) + + function DirectMutateComponent() { + const { form, mutation } = useApiFormContext() + const { register } = form + + const handleDirectSubmit = () => { + mutation.mutate({ name: 'Direct', email: 'direct@test.com' } as any) + } + + return ( + <> + + + + ) + } + + const { getByTestId } = render( + + + + + , + ) + + const directButton = getByTestId('direct-submit') + fireEvent.click(directButton) + + await waitFor( + () => { + expect(onSuccess).toHaveBeenCalled() + }, + { timeout: 5000 }, + ) +}) + +test('useApiFormContext accesses nested mutation properties', async () => { + const api = createApi({ baseUrl: 'https://api.example.com' }) + const queryClient = createTestQueryClient() + + function NestedAccessComponent() { + const { form, mutation } = useApiFormContext() + const { register } = form + // Access nested properties + const pending = mutation.isPending + const success = mutation.isSuccess + + return ( + <> + + {String(pending)} + {String(success)} + + + ) + } + + const { getByTestId } = render( + + + + + , + ) + + expect(getByTestId('pending').textContent).toBe('false') + expect(getByTestId('success').textContent).toBe('false') + + fireEvent.click(getByTestId('submit-button')) + + await waitFor(() => { + expect(getByTestId('success').textContent).toBe('true') + }) +}) + +test('useApiFormContext proxy handles symbol access', async () => { + const api = createApi({ baseUrl: 'https://api.example.com' }) + const queryClient = createTestQueryClient() + let symbolResult: unknown = null + + function SymbolAccessComponent() { + const context = useApiFormContext() + // Access via Symbol.iterator or other symbol + symbolResult = (context as any)[Symbol.toStringTag] + return
Rendered
+ } + + const { getByTestId } = render( + + + + + , + ) + + expect(getByTestId('rendered')).toBeTruthy() + // Symbol access should return undefined or the actual value, not throw + expect(symbolResult).toBeUndefined() +}) + +test('useApiFormContext proxy handles null values', async () => { + const api = createApi({ baseUrl: 'https://api.example.com' }) + const queryClient = createTestQueryClient() + let errorValue: unknown = 'not-accessed' + + function NullAccessComponent() { + const { mutation } = useApiFormContext() + // Access error which should be null initially + errorValue = mutation.error + return ( +
+ {errorValue === null + ? 'null' + : errorValue === undefined + ? 'undefined' + : 'other'} +
+ ) + } + + const { getByTestId } = render( + + + + + , + ) + + expect(getByTestId('error-value')).toBeTruthy() + // Error should be null when no error has occurred + expect(errorValue).toBeNull() +}) + +test('useApiFormContext cleans up subscription on unmount', async () => { + const api = createApi({ baseUrl: 'https://api.example.com' }) + const queryClient = createTestQueryClient() + + function ContextConsumer() { + const { mutation } = useApiFormContext() + return
{String(mutation.isPending)}
+ } + + const { unmount, getByTestId } = render( + + + + + , + ) + + expect(getByTestId('pending').textContent).toBe('false') + + // Unmount triggers the unsubscribe cleanup + unmount() +}) diff --git a/packages/hookform/src/api-form.tsx b/packages/hookform/src/api-form.tsx new file mode 100644 index 0000000..66f9129 --- /dev/null +++ b/packages/hookform/src/api-form.tsx @@ -0,0 +1,437 @@ +'use client' +import type { + Additional, + ConditionalKeys, + DevupApiResponse, + DevupApiServers, + ExtractValue, +} from '@devup-api/fetch' +import { pathSchemas } from '@devup-api/zod' +import { zodResolver } from '@hookform/resolvers/zod' +import { skipToken, useMutation, useQuery } from '@tanstack/react-query' +import { + createContext, + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useSyncExternalStore, +} from 'react' +import type { FieldValues } from 'react-hook-form' +import { FormProvider, useForm } from 'react-hook-form' +import type { z } from 'zod' +import type { + ApiFormContextValue, + ApiFormProps, + ApiFormState, + ApiFormStore, + HttpMethod, + MethodApiStructKey, + MethodApiStructScope, +} from './types' + +// Context stores the store object (stable reference) +const ApiFormContext = createContext(null) + +/** + * Deep comparison for specific paths that were accessed + */ +function hasChangedForPaths( + prev: Record, + next: Record, + paths: Set, +): boolean { + for (const path of paths) { + const keys = path.split('.') + let prevVal: unknown = prev + let nextVal: unknown = next + + for (const key of keys) { + prevVal = (prevVal as Record)?.[key] + nextVal = (nextVal as Record)?.[key] + } + + if (!Object.is(prevVal, nextVal)) { + return true + } + } + return false +} + +/** + * Creates a Proxy that tracks accessed property paths + */ +function createTrackedProxy( + target: T, + accessed: Set, + parentPath = '', +): T { + return new Proxy(target, { + get(obj, prop) { + if (typeof prop === 'symbol') { + return Reflect.get(obj, prop) + } + + const path = parentPath ? `${parentPath}.${prop}` : prop + const value = Reflect.get(obj, prop) + + // Track primitive values and functions + if ( + value === null || + typeof value !== 'object' || + typeof value === 'function' + ) { + accessed.add(path) + return value + } + + // For objects, return a nested proxy + return createTrackedProxy(value as object, accessed, path) + }, + }) +} + +/** + * Hook to access ApiForm context with optimized re-renders + * + * Only re-renders when accessed properties change (like react-hook-form's formState) + * + * @example + * ```tsx + * function FormFields() { + * const { form, mutation } = useApiFormContext() + * const { register } = form + * const { isPending } = mutation // Only re-renders when isPending changes + * + * return ( + * <> + * + * + * + * ) + * } + * ``` + */ +export function useApiFormContext< + TFieldValues extends FieldValues = FieldValues, + TData = unknown, + TError = unknown, +>(): ApiFormContextValue { + const store = useContext(ApiFormContext) as ApiFormStore< + TFieldValues, + TData, + TError + > | null + + if (!store) { + throw new Error('useApiFormContext must be used within an ApiForm') + } + + // Track which properties are accessed + const accessedRef = useRef>(new Set()) + // Cache previous state for comparison + const prevStateRef = useRef | null>( + null, + ) + // Cache snapshot to return stable reference when unchanged + const snapshotRef = useRef | null>( + null, + ) + + const getSnapshot = useCallback(() => { + const currentState = store.getState() + + // First render - return current state + if (!prevStateRef.current || !snapshotRef.current) { + prevStateRef.current = currentState + snapshotRef.current = currentState + return currentState + } + + // Check if any accessed property has changed + if ( + accessedRef.current.size > 0 && + hasChangedForPaths( + prevStateRef.current as unknown as Record, + currentState as unknown as Record, + accessedRef.current, + ) + ) { + prevStateRef.current = currentState + snapshotRef.current = currentState + return currentState + } + + // No changes to accessed properties - return cached snapshot + return snapshotRef.current + }, [store]) + + const state = useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot) + + // Return a proxy that tracks property access + return useMemo( + () => + createTrackedProxy(state, accessedRef.current) as ApiFormContextValue< + TFieldValues, + TData, + TError + >, + [state], + ) +} + +/** + * ApiForm - A form component that integrates with @devup-api/fetch and @devup-api/zod + * + * Features: + * - Form validation using Zod schemas from OpenAPI spec + * - Form submission via react-query mutation + * - Auto-fetching default values + * - Optimized re-renders (only re-renders when accessed properties change) + * + * @example + * ```tsx + * import { createApi } from '@devup-api/fetch' + * import { ApiForm, useApiFormContext } from '@devup-api/hookform' + * + * const api = createApi('https://api.example.com') + * + * function FormFields() { + * const { form, mutation } = useApiFormContext() + * const { register } = form + * const { isPending } = mutation + * + * return ( + * <> + * + * + * + * ) + * } + * + * function CreateUserForm() { + * return ( + * console.log('Created:', data)} + * > + * + * + * ) + * } + * ``` + */ +export function ApiForm< + S extends ConditionalKeys, + M extends HttpMethod, + P extends MethodApiStructKey, + O extends Additional>, + TFieldValues extends FieldValues = ExtractValue extends FieldValues + ? ExtractValue + : FieldValues, +>({ + api, + method, + path, + openapi: _openapi, + requestOptions, + onSuccess, + onError, + onValidationError, + children, + defaultValues, + mode = 'onSubmit', + formOptions, + formProps, + resetOnSuccess = false, + queryClient, + fetchDefaultValues, +}: ApiFormProps) { + type TData = ExtractValue + type TError = ExtractValue + + // Store listeners for subscription + const listenersRef = useRef void>>(new Set()) + + // Refs to hold current state (for stable store.getState) + const formRef = useRef> | null>(null) + const mutationRef = useRef + > | null>(null) + const isLoadingDefaultValuesRef = useRef(false) + + // Notify all listeners + const notify = useCallback(() => { + for (const listener of listenersRef.current) { + listener() + } + }, []) + + // Fetch default values if configured + const defaultValuesQuery = useQuery({ + queryKey: fetchDefaultValues + ? [ + 'apiFormDefaultValues', + fetchDefaultValues.method ?? 'get', + fetchDefaultValues.path, + fetchDefaultValues.options, + ] + : ['apiFormDefaultValues', 'disabled'], + queryFn: fetchDefaultValues + ? async () => { + const fetchMethod = fetchDefaultValues.method ?? 'get' + // biome-ignore lint/suspicious/noExplicitAny: Dynamic method call + const result = await (api as any)[fetchMethod]( + fetchDefaultValues.path, + fetchDefaultValues.options, + ) + if (result.error) { + throw result.error + } + const data = fetchDefaultValues.transform + ? fetchDefaultValues.transform(result.data) + : result.data + return data as TFieldValues + } + : skipToken, + enabled: !!fetchDefaultValues, + ...(queryClient && { queryClient }), + }) + + // Determine actual default values + const resolvedDefaultValues = useMemo(() => { + if (fetchDefaultValues && defaultValuesQuery.data) { + return defaultValuesQuery.data + } + return defaultValues + }, [fetchDefaultValues, defaultValuesQuery.data, defaultValues]) + + // Get the Zod schema for this path/method combination + const schema = ( + pathSchemas as unknown as Record> + )?.[method]?.[path as string] as z.ZodType | undefined + + // Create form with optional Zod resolver + const methods = useForm({ + // biome-ignore lint/suspicious/noExplicitAny: Complex generic type inference + defaultValues: resolvedDefaultValues as any, + mode, + // biome-ignore lint/suspicious/noExplicitAny: Zod v3/v4 compatibility + resolver: schema ? zodResolver(schema as any) : undefined, + ...formOptions, + }) + + // Mutation for form submission + const mutation = useMutation({ + mutationKey: [method, path, requestOptions], + mutationFn: async (data: TFieldValues) => { + // biome-ignore lint/suspicious/noExplicitAny: Dynamic method call + const result: DevupApiResponse = await (api as any)[ + method + ](path, { + ...requestOptions, + body: data, + }) + if (result.error) { + throw result.error + } + return result.data as TData + }, + onSuccess: (data) => { + onSuccess?.(data) + if (resetOnSuccess) { + methods.reset() + } + }, + onError: (error) => { + onError?.(error) + }, + ...(queryClient && { queryClient }), + }) + + // Update refs with current values + formRef.current = methods + mutationRef.current = mutation + isLoadingDefaultValuesRef.current = defaultValuesQuery.isLoading + + // Reset form when fetchDefaultValues completes + useEffect(() => { + if (fetchDefaultValues && defaultValuesQuery.data) { + methods.reset(defaultValuesQuery.data) + } + }, [fetchDefaultValues, defaultValuesQuery.data, methods]) + + // Notify listeners when state changes + // biome-ignore lint/correctness/useExhaustiveDependencies: Intentionally trigger on state changes + useEffect(() => { + notify() + }, [ + mutation.isPending, + mutation.isSuccess, + mutation.isError, + mutation.error, + mutation.data, + defaultValuesQuery.isLoading, + notify, + ]) + + const handleSubmit = methods.handleSubmit( + (data) => { + // biome-ignore lint/suspicious/noExplicitAny: Complex generic type inference + mutation.mutate(data as any) + }, + (errors) => { + onValidationError?.(errors) + }, + ) + + // Create store object (stable reference - no dependencies) + // Note: getState is only called after component renders, so refs are always set + const store = useMemo>(() => { + return { + getState: () => { + // biome-ignore lint/style/noNonNullAssertion: Refs are guaranteed to be set before useSyncExternalStore calls getState + const form = formRef.current! + // biome-ignore lint/style/noNonNullAssertion: Refs are guaranteed to be set before useSyncExternalStore calls getState + const mut = mutationRef.current! + return { + form, + mutation: { + isPending: mut.isPending, + isSuccess: mut.isSuccess, + isError: mut.isError, + error: mut.error, + data: mut.data, + mutate: mut.mutate, + mutateAsync: mut.mutateAsync, + reset: mut.reset, + }, + isLoadingDefaultValues: isLoadingDefaultValuesRef.current, + } + }, + subscribe: (listener: () => void) => { + listenersRef.current.add(listener) + return () => { + listenersRef.current.delete(listener) + } + }, + } + }, []) + + return ( + + +
+ {children} +
+
+
+ ) +} diff --git a/packages/hookform/src/index.ts b/packages/hookform/src/index.ts new file mode 100644 index 0000000..6f1b5df --- /dev/null +++ b/packages/hookform/src/index.ts @@ -0,0 +1,27 @@ +export type { + FieldErrors, + FieldValues, + SubmitErrorHandler, + SubmitHandler, + UseFormReturn, +} from 'react-hook-form' +// Re-export useful types from react-hook-form for convenience +export { + Controller, + useController, + useFieldArray, + useFormContext, + useWatch, +} from 'react-hook-form' +export { ApiForm, useApiFormContext } from './api-form' +export type { + ApiFormContextValue, + ApiFormMutation, + ApiFormProps, + ApiFormState, + FetchDefaultValuesConfig, + FetchMethod, + HttpMethod, + MethodApiStructKey, + MethodApiStructScope, +} from './types' diff --git a/packages/hookform/src/types.ts b/packages/hookform/src/types.ts new file mode 100644 index 0000000..3951013 --- /dev/null +++ b/packages/hookform/src/types.ts @@ -0,0 +1,310 @@ +import type { + Additional, + ConditionalKeys, + DevupApi, + DevupApiServers, + DevupDeleteApiStructKey, + DevupDeleteApiStructScope, + DevupGetApiStructKey, + DevupGetApiStructScope, + DevupPatchApiStructKey, + DevupPatchApiStructScope, + DevupPostApiStructKey, + DevupPostApiStructScope, + DevupPutApiStructKey, + DevupPutApiStructScope, + ExtractValue, +} from '@devup-api/fetch' +import type { QueryClient, UseMutationResult } from '@tanstack/react-query' +import type { ReactNode } from 'react' +import type { + DefaultValues, + FieldValues, + Mode, + UseFormProps, + UseFormReturn, +} from 'react-hook-form' + +export type HttpMethod = 'post' | 'put' | 'patch' | 'delete' +export type FetchMethod = 'get' | 'post' | 'put' | 'patch' | 'delete' + +export type MethodApiStructScope< + S extends string, + M extends HttpMethod, +> = M extends 'post' + ? DevupPostApiStructScope + : M extends 'put' + ? DevupPutApiStructScope + : M extends 'patch' + ? DevupPatchApiStructScope + : M extends 'delete' + ? DevupDeleteApiStructScope + : never + +export type MethodApiStructKey< + S extends string, + M extends HttpMethod, +> = M extends 'post' + ? DevupPostApiStructKey + : M extends 'put' + ? DevupPutApiStructKey + : M extends 'patch' + ? DevupPatchApiStructKey + : M extends 'delete' + ? DevupDeleteApiStructKey + : never + +export type FetchMethodApiStructScope< + S extends string, + M extends FetchMethod, +> = M extends 'get' + ? DevupGetApiStructScope + : M extends 'post' + ? DevupPostApiStructScope + : M extends 'put' + ? DevupPutApiStructScope + : M extends 'patch' + ? DevupPatchApiStructScope + : M extends 'delete' + ? DevupDeleteApiStructScope + : never + +export type FetchMethodApiStructKey< + S extends string, + M extends FetchMethod, +> = M extends 'get' + ? DevupGetApiStructKey + : M extends 'post' + ? DevupPostApiStructKey + : M extends 'put' + ? DevupPutApiStructKey + : M extends 'patch' + ? DevupPatchApiStructKey + : M extends 'delete' + ? DevupDeleteApiStructKey + : never + +/** + * Configuration for auto-fetching default values + */ +export interface FetchDefaultValuesConfig< + S extends ConditionalKeys, + FM extends FetchMethod = 'get', + FP extends FetchMethodApiStructKey = FetchMethodApiStructKey, + FO extends Additional> = Additional< + FP, + FetchMethodApiStructScope + >, +> { + /** + * HTTP method for fetching default values + * @default 'get' + */ + method?: FM + /** + * API path or operationId for fetching default values + */ + path: FP + /** + * Request options for fetching (params, query, headers) + */ + options?: Omit + /** + * Transform the fetched response to form default values + */ + transform?: (response: ExtractValue) => unknown +} + +/** + * Mutation state and methods + */ +export interface ApiFormMutation< + TFieldValues extends FieldValues = FieldValues, + TData = unknown, + TError = unknown, +> { + /** + * Whether the mutation is pending + */ + isPending: boolean + /** + * Whether the mutation succeeded + */ + isSuccess: boolean + /** + * Whether the mutation failed + */ + isError: boolean + /** + * The error from the mutation + */ + error: TError | null + /** + * The data returned from the mutation + */ + data: TData | undefined + /** + * Execute the mutation with form data + */ + mutateAsync: UseMutationResult['mutateAsync'] + /** + * Execute the mutation with form data (non-throwing) + */ + mutate: UseMutationResult['mutate'] + /** + * Reset the mutation state + */ + reset: UseMutationResult['reset'] +} + +/** + * State snapshot for ApiForm + */ +export interface ApiFormState< + TFieldValues extends FieldValues = FieldValues, + TData = unknown, + TError = unknown, +> { + /** + * Form methods from react-hook-form + */ + form: UseFormReturn + /** + * Mutation state and methods + */ + mutation: ApiFormMutation + /** + * Whether default values are being fetched + */ + isLoadingDefaultValues: boolean +} + +/** + * Store interface for ApiForm (internal use) + */ +export interface ApiFormStore< + TFieldValues extends FieldValues = FieldValues, + TData = unknown, + TError = unknown, +> { + /** + * Get current state snapshot + */ + getState: () => ApiFormState + /** + * Subscribe to state changes + */ + subscribe: (listener: () => void) => () => void +} + +/** + * Context value provided by useApiFormContext + * Uses Proxy to track accessed properties for optimized re-renders + */ +export type ApiFormContextValue< + TFieldValues extends FieldValues = FieldValues, + TData = unknown, + TError = unknown, +> = ApiFormState + +export interface ApiFormProps< + S extends ConditionalKeys, + M extends HttpMethod, + P extends MethodApiStructKey, + O extends Additional>, + TFieldValues extends FieldValues = ExtractValue extends FieldValues + ? ExtractValue + : FieldValues, +> { + /** + * The API client instance from @devup-api/fetch + */ + api: DevupApi + + /** + * HTTP method for the form submission + */ + method: M + + /** + * API path or operationId for the endpoint + */ + path: P + + /** + * Optional server name for multi-server setups + * @default 'openapi.json' + */ + openapi?: S + + /** + * Additional request options (params, query, headers) + */ + requestOptions?: Omit + + /** + * Called when the API request succeeds + */ + onSuccess?: (data: ExtractValue) => void + + /** + * Called when the API request fails + */ + onError?: (error: ExtractValue) => void + + /** + * Called when form validation fails (before API request) + */ + onValidationError?: (errors: Record) => void + + /** + * Form children - can access form context via useFormContext + */ + children: ReactNode + + /** + * Default values for form fields + */ + defaultValues?: DefaultValues + + /** + * Validation mode + * @default 'onSubmit' + */ + mode?: Mode + + /** + * Additional react-hook-form options + */ + formOptions?: Omit< + UseFormProps, + 'defaultValues' | 'mode' | 'resolver' + > + + /** + * HTML form element props + */ + formProps?: Omit< + React.FormHTMLAttributes, + 'onSubmit' | 'children' + > + + /** + * Whether to reset form after successful submission + * @default false + */ + resetOnSuccess?: boolean + + /** + * Optional TanStack Query client for mutation management + * If not provided, will try to use QueryClientProvider context + * Falls back to basic state management if react-query is not available + */ + queryClient?: QueryClient + + /** + * Configuration for auto-fetching default values + * Uses the api client to fetch initial form values + */ + fetchDefaultValues?: FetchDefaultValuesConfig +} diff --git a/packages/hookform/tsconfig.json b/packages/hookform/tsconfig.json new file mode 100644 index 0000000..3b743d3 --- /dev/null +++ b/packages/hookform/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "declaration": true, + "declarationDir": "dist", + "jsx": "react-jsx" + }, + "include": ["src/**/*.ts", "src/**/*.tsx"], + "exclude": ["node_modules", "dist", "src/**/*.test.ts", "src/**/*.test.tsx"] +} diff --git a/packages/hookform/vite.config.ts b/packages/hookform/vite.config.ts new file mode 100644 index 0000000..c496cdf --- /dev/null +++ b/packages/hookform/vite.config.ts @@ -0,0 +1,56 @@ +import preserveDirectives from 'rollup-plugin-preserve-directives' +import { defineConfig } from 'vite' +import dts from 'vite-plugin-dts' + +export default defineConfig({ + plugins: [ + dts({ + entryRoot: 'src', + staticImport: true, + pathsToAliases: false, + exclude: [ + '**/__tests__/**/*', + '**/*.test.(tsx|ts|js|jsx)', + '**/*.test-d.(tsx|ts|js|jsx)', + 'vite.config.ts', + ], + include: ['**/src/**/*.ts', '**/src/**/*.tsx'], + copyDtsFiles: true, + compilerOptions: { + isolatedModules: false, + declaration: true, + }, + }), + ], + build: { + rollupOptions: { + onwarn: (warning) => { + if (warning.code === 'MODULE_LEVEL_DIRECTIVE') { + return + } + }, + plugins: [preserveDirectives()], + external: (source) => { + return !(source.includes('src') || source.startsWith('.')) + }, + + output: { + dir: 'dist', + preserveModules: true, + preserveModulesRoot: 'src', + + exports: 'named', + assetFileNames({ name }) { + return name?.replace(/^src\//g, '') ?? '' + }, + }, + }, + lib: { + formats: ['es', 'cjs'], + entry: { + index: 'src/index.ts', + }, + }, + outDir: 'dist', + }, +}) diff --git a/packages/zod/src/index.ts b/packages/zod/src/index.ts index 7561328..deb4c90 100644 --- a/packages/zod/src/index.ts +++ b/packages/zod/src/index.ts @@ -1,7 +1,11 @@ export * from '@devup-api/fetch' export * from './schema-struct' -import type { DevupZodAllSchemas, DevupZodSchemas } from './schema-struct' +import type { + DevupZodAllSchemas, + DevupZodPathSchemas, + DevupZodSchemas, +} from './schema-struct' // ============================================================================= // Runtime Exports (will be replaced by virtual file from bundler plugins) @@ -49,3 +53,37 @@ export const requestSchemas: DevupZodSchemas['request'] = */ export const errorSchemas: DevupZodSchemas['error'] = {} as DevupZodSchemas['error'] + +/** + * Path schemas - Zod schemas mapped by path/operationId for each HTTP method + * Used by @devup-api/hookform for automatic form validation + * @example + * import { pathSchemas } from '@devup-api/zod' + * const createUserSchema = pathSchemas.post['createUser'] + * // or pathSchemas.post['/users'] + */ +export const pathSchemas: DevupZodPathSchemas = {} as DevupZodPathSchemas + +/** + * POST path schemas - Zod schemas for POST request bodies + */ +export const postPathSchemas: DevupZodPathSchemas['post'] = + {} as DevupZodPathSchemas['post'] + +/** + * PUT path schemas - Zod schemas for PUT request bodies + */ +export const putPathSchemas: DevupZodPathSchemas['put'] = + {} as DevupZodPathSchemas['put'] + +/** + * PATCH path schemas - Zod schemas for PATCH request bodies + */ +export const patchPathSchemas: DevupZodPathSchemas['patch'] = + {} as DevupZodPathSchemas['patch'] + +/** + * DELETE path schemas - Zod schemas for DELETE request bodies + */ +export const deletePathSchemas: DevupZodPathSchemas['delete'] = + {} as DevupZodPathSchemas['delete'] diff --git a/packages/zod/src/schema-struct.ts b/packages/zod/src/schema-struct.ts index a03b030..facf9b8 100644 --- a/packages/zod/src/schema-struct.ts +++ b/packages/zod/src/schema-struct.ts @@ -107,6 +107,35 @@ export type DevupZodSchemaTypes< } } +// ============================================================================= +// Path Schema Types (for hookform integration) +// ============================================================================= + +// biome-ignore lint/suspicious/noEmptyInterface: empty interface for augmentation +export interface DevupZodPostPathSchemas {} + +// biome-ignore lint/suspicious/noEmptyInterface: empty interface for augmentation +export interface DevupZodPutPathSchemas {} + +// biome-ignore lint/suspicious/noEmptyInterface: empty interface for augmentation +export interface DevupZodPatchPathSchemas {} + +// biome-ignore lint/suspicious/noEmptyInterface: empty interface for augmentation +export interface DevupZodDeletePathSchemas {} + +/** + * Path schemas organized by HTTP method + * Maps path/operationId to request body Zod schema + */ +export type DevupZodPathSchemas< + T extends keyof DevupApiServers | (string & {}) = 'openapi.json', +> = { + post: ExtractValue> + put: ExtractValue> + patch: ExtractValue> + delete: ExtractValue> +} + // ============================================================================= // Cold Typing Support // ============================================================================= From 39b0cb991da4b9eb6a34877b476f940cb096b63f Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Sun, 4 Jan 2026 22:28:51 +0900 Subject: [PATCH 2/2] Implement hookform --- .changepacks/changepack_log_Ob3pmJphiH7bOr1xjABTd.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 .changepacks/changepack_log_Ob3pmJphiH7bOr1xjABTd.json diff --git a/.changepacks/changepack_log_Ob3pmJphiH7bOr1xjABTd.json b/.changepacks/changepack_log_Ob3pmJphiH7bOr1xjABTd.json new file mode 100644 index 0000000..8e1e301 --- /dev/null +++ b/.changepacks/changepack_log_Ob3pmJphiH7bOr1xjABTd.json @@ -0,0 +1 @@ +{"changes":{"packages/hookform/package.json":"Minor","packages/zod/package.json":"Patch","packages/generator/package.json":"Patch"},"note":"Support hookform","date":"2026-01-04T13:28:49.692436300Z"} \ No newline at end of file