create-fullstack-scaffold 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (530) hide show
  1. package/README.md +154 -0
  2. package/dist/cli/index.d.ts +2 -0
  3. package/dist/cli/index.js +439 -0
  4. package/dist/cli/index.js.map +1 -0
  5. package/package.json +171 -0
  6. package/template/.agents/skills/writing-hookify-rules/SKILL.md +408 -0
  7. package/template/.ai-context.md +368 -0
  8. package/template/.claude/hookify.block-dangerous-commands.local.md +41 -0
  9. package/template/.claude/hookify.check-api-types.local.md +111 -0
  10. package/template/.claude/hookify.check-duplicate-types.local.md +287 -0
  11. package/template/.claude/hookify.check-test-assertions.local.md +93 -0
  12. package/template/.claude/hookify.remind-add-tests.local.md +209 -0
  13. package/template/.claude/hookify.remind-db-migration.local.md +58 -0
  14. package/template/.claude/hookify.remind-run-tests.local.md +52 -0
  15. package/template/.claude/hookify.warn-any-type.local.md +203 -0
  16. package/template/.claude/hookify.warn-console-log.local.md +73 -0
  17. package/template/.claude/rules/00-project-config.md +147 -0
  18. package/template/.claude/rules/01-file-types.md +127 -0
  19. package/template/.claude/rules/02-git-workflow.md +167 -0
  20. package/template/.claude/rules/10-api-type-inference.md +555 -0
  21. package/template/.claude/rules/20-server-api.md +349 -0
  22. package/template/.claude/rules/21-server-entrypoint.md +257 -0
  23. package/template/.claude/rules/30-client-components.md +314 -0
  24. package/template/.claude/rules/31-client-services.md +768 -0
  25. package/template/.claude/rules/32-client-state-zustand.md +324 -0
  26. package/template/.claude/rules/33-client-app-entry.md +276 -0
  27. package/template/.claude/rules/34-client-admin-module.md +445 -0
  28. package/template/.claude/rules/35-cli-module.md +259 -0
  29. package/template/.claude/rules/40-admin-module.md +495 -0
  30. package/template/.claude/rules/40-shared-types.md +450 -0
  31. package/template/.claude/rules/50-websocket.md +245 -0
  32. package/template/.claude/rules/51-sse.md +372 -0
  33. package/template/.claude/rules/52-cloudflare-realtime.md +384 -0
  34. package/template/.claude/rules/60-testing-standards.md +245 -0
  35. package/template/.claude/rules/61-hono-testing.md +200 -0
  36. package/template/.claude/scripts/README.md +187 -0
  37. package/template/.claude/scripts/check-duplicate-types.ts +188 -0
  38. package/template/.claude/scripts/post-edit-check-incremental.sh +61 -0
  39. package/template/.claude/scripts/post-edit-check.sh +22 -0
  40. package/template/.claude/scripts/pre-tool-use-check.sh +86 -0
  41. package/template/.claude/scripts/test-pre-tool-use.sh +73 -0
  42. package/template/.claude/scripts/validate-git-changes.sh +38 -0
  43. package/template/.claude/settings.json +32 -0
  44. package/template/.claude/skills/writing-hookify-rules/SKILL.md +408 -0
  45. package/template/.dev-context-compact.md +54 -0
  46. package/template/.dev-context.md +621 -0
  47. package/template/.dev-guide.md +243 -0
  48. package/template/.env.example +60 -0
  49. package/template/.eslintrc.routes.json +39 -0
  50. package/template/.husky/_/husky.sh +27 -0
  51. package/template/.husky/commit-msg +4 -0
  52. package/template/.husky/post-commit +4 -0
  53. package/template/.husky/pre-commit +19 -0
  54. package/template/.husky/pre-push +12 -0
  55. package/template/.prettierignore +22 -0
  56. package/template/.prettierrc +8 -0
  57. package/template/.sessions/3/2026-03-26T05-41-09-154Z_03bb597f-3898-4a2e-8c08-5746bbe8d95e.jsonl +18 -0
  58. package/template/.sessions/3/2026-03-26T05-48-30-052Z_12de4774-9cb3-494c-9852-bd1e6b0003c3.jsonl +6 -0
  59. package/template/.vscode/extensions.json +8 -0
  60. package/template/.vscode/settings.json +4 -0
  61. package/template/.workspaces/3/hello.py +1 -0
  62. package/template/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/803c2e37182b913f8857edf6d8776515cef4af6d48fcb25913e457f4b14ffa97.sqlite +0 -0
  63. package/template/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/803c2e37182b913f8857edf6d8776515cef4af6d48fcb25913e457f4b14ffa97.sqlite-shm +0 -0
  64. package/template/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/803c2e37182b913f8857edf6d8776515cef4af6d48fcb25913e457f4b14ffa97.sqlite-wal +0 -0
  65. package/template/CLAUDE.md +296 -0
  66. package/template/DESIGN.md +260 -0
  67. package/template/DEV_CONTEXT_GUIDE.md +366 -0
  68. package/template/DEV_TOOLS_README.md +396 -0
  69. package/template/FRAMEWORK_HISTORY.md +34 -0
  70. package/template/PROJECT_SUMMARY.md +325 -0
  71. package/template/QUICKSTART.md +305 -0
  72. package/template/README.md +154 -0
  73. package/template/STRUCTURE.txt +81 -0
  74. package/template/admin.html +13 -0
  75. package/template/auth-inject.html +33 -0
  76. package/template/commitlint.config.js +12 -0
  77. package/template/docs/PERMISSION_ARCHITECTURE.md +279 -0
  78. package/template/docs/PERMISSION_EXAMPLES.md +230 -0
  79. package/template/docs/PERMISSION_SYSTEM.md +301 -0
  80. package/template/docs/SMART_TEST.md +171 -0
  81. package/template/docs/runtime-abstract.md +253 -0
  82. package/template/drizzle/0000_rainy_boomer.sql +101 -0
  83. package/template/drizzle/0001_add_todo_attachments.sql +12 -0
  84. package/template/drizzle/0002_chilly_magneto.sql +89 -0
  85. package/template/drizzle/meta/0000_snapshot.json +652 -0
  86. package/template/drizzle/meta/0001_snapshot.json +735 -0
  87. package/template/drizzle/meta/0002_snapshot.json +1198 -0
  88. package/template/drizzle/meta/_journal.json +27 -0
  89. package/template/drizzle.config.ts +13 -0
  90. package/template/eslint-rules/README-no-middleware-in-routes.md +154 -0
  91. package/template/eslint-rules/__tests__/enforce-valid-method.test.ts +251 -0
  92. package/template/eslint-rules/__tests__/layer-boundary.test.ts +102 -0
  93. package/template/eslint-rules/__tests__/module-boundary.test.ts +77 -0
  94. package/template/eslint-rules/__tests__/no-direct-zod-import-in-file-routes.test.ts +45 -0
  95. package/template/eslint-rules/__tests__/no-new-old-service-naming.test.ts +76 -0
  96. package/template/eslint-rules/e2e-test-location.js +153 -0
  97. package/template/eslint-rules/enforce-valid-method.js +217 -0
  98. package/template/eslint-rules/flat-routes-services.js +69 -0
  99. package/template/eslint-rules/framework-protect.js +186 -0
  100. package/template/eslint-rules/layer-boundary.js +164 -0
  101. package/template/eslint-rules/limit-type-complexity.js +130 -0
  102. package/template/eslint-rules/middleware-location.js +162 -0
  103. package/template/eslint-rules/module-boundary.js +125 -0
  104. package/template/eslint-rules/no-ambiguous-file-paths.js +64 -0
  105. package/template/eslint-rules/no-any-on-apiclient.js +173 -0
  106. package/template/eslint-rules/no-boolean-success.js +43 -0
  107. package/template/eslint-rules/no-deep-relative-imports.js +69 -0
  108. package/template/eslint-rules/no-direct-fetch.js +122 -0
  109. package/template/eslint-rules/no-direct-ws-sse.js +57 -0
  110. package/template/eslint-rules/no-direct-zod-import-in-file-routes.js +54 -0
  111. package/template/eslint-rules/no-disable-direct-fetch.js +57 -0
  112. package/template/eslint-rules/no-disable-type-safe-client.js +58 -0
  113. package/template/eslint-rules/no-inline-schema.js +220 -0
  114. package/template/eslint-rules/no-middleware-in-routes.js +81 -0
  115. package/template/eslint-rules/no-new-old-service-naming.js +123 -0
  116. package/template/eslint-rules/no-type-assertion-in-rpc.js +99 -0
  117. package/template/eslint-rules/no-type-assertion-on-shared-types.js +109 -0
  118. package/template/eslint-rules/no-util-functions-in-service.js +128 -0
  119. package/template/eslint-rules/prefer-shared-types.js +352 -0
  120. package/template/eslint-rules/protect-ws-sse-interface.js +66 -0
  121. package/template/eslint-rules/require-antd-generic-types.js +73 -0
  122. package/template/eslint-rules/require-file-openapi-props.js +114 -0
  123. package/template/eslint-rules/require-hono-chain-syntax.js +129 -0
  124. package/template/eslint-rules/require-nullable-for-optional.js +99 -0
  125. package/template/eslint-rules/require-response-helpers.js +140 -0
  126. package/template/eslint-rules/require-type-safe-test-client.js +106 -0
  127. package/template/eslint-rules/route-location.js +72 -0
  128. package/template/eslint.config.js +231 -0
  129. package/template/index.html +17 -0
  130. package/template/lint-scripts/DIRECTORY_RULES.md +335 -0
  131. package/template/lint-scripts/README.md +299 -0
  132. package/template/lint-scripts/TESTING.md +273 -0
  133. package/template/lint-scripts/ai-context-generator.ts +159 -0
  134. package/template/lint-scripts/check-framework-modify.sh +8 -0
  135. package/template/lint-scripts/check-menu-permissions.js +242 -0
  136. package/template/lint-scripts/check-refs.ts +165 -0
  137. package/template/lint-scripts/check-route-auth-complete.js +260 -0
  138. package/template/lint-scripts/check-route-auth.js +212 -0
  139. package/template/lint-scripts/check-route-permissions.js +210 -0
  140. package/template/lint-scripts/config/project.config.ts +358 -0
  141. package/template/lint-scripts/dev-context-analyzer.ts +707 -0
  142. package/template/lint-scripts/dev-guide-generator.ts +830 -0
  143. package/template/lint-scripts/dev-server.ts +58 -0
  144. package/template/lint-scripts/eslint-route-auth.js +158 -0
  145. package/template/lint-scripts/eslint-routes-permission.js +147 -0
  146. package/template/lint-scripts/fix-route-auth.js +245 -0
  147. package/template/lint-scripts/framework/check-modify.ts +152 -0
  148. package/template/lint-scripts/framework/hash-utils.ts +180 -0
  149. package/template/lint-scripts/framework/init-baseline.ts +84 -0
  150. package/template/lint-scripts/framework/update-baseline.ts +148 -0
  151. package/template/lint-scripts/post-commit-track.ts +66 -0
  152. package/template/lint-scripts/quick-test.sh +27 -0
  153. package/template/lint-scripts/smart-test.ts +268 -0
  154. package/template/lint-scripts/test-history.ts +360 -0
  155. package/template/lint-scripts/test-tracker.ts +350 -0
  156. package/template/lint-scripts/validate-all.ts +250 -0
  157. package/template/lint-scripts/validators/api-coverage.validator.ts +355 -0
  158. package/template/lint-scripts/validators/client-rpc.validator.ts +190 -0
  159. package/template/lint-scripts/validators/client-tests.validator.ts +155 -0
  160. package/template/lint-scripts/validators/console-log.validator.ts +122 -0
  161. package/template/lint-scripts/validators/directory-structure.validator.ts +181 -0
  162. package/template/lint-scripts/validators/imports.validator.ts +180 -0
  163. package/template/lint-scripts/validators/index.ts +271 -0
  164. package/template/lint-scripts/validators/md-refs.validator.ts +165 -0
  165. package/template/lint-scripts/validators/module-tests.validator.ts +112 -0
  166. package/template/lint-scripts/validators/sensitive.validator.ts +185 -0
  167. package/template/lint-scripts/validators/server-rpc.validator.ts +141 -0
  168. package/template/lint-scripts/validators/test-quality.validator.ts +257 -0
  169. package/template/lint-scripts/validators/todos.validator.ts +126 -0
  170. package/template/lint-scripts/watch-validator.ts +131 -0
  171. package/template/modules.config.ts +71 -0
  172. package/template/package-lock.json +14554 -0
  173. package/template/package.json +158 -0
  174. package/template/patches/@hono+zod-openapi+1.2.2.patch +67 -0
  175. package/template/patches/@hono+zod-validator+0.7.6.patch +13 -0
  176. package/template/patches/hono+4.12.16.patch +246 -0
  177. package/template/patches/typescript+5.8.3.patch +26 -0
  178. package/template/playwright.config.ts +60 -0
  179. package/template/pnpm-lock.yaml +7137 -0
  180. package/template/postcss.config.js +6 -0
  181. package/template/scripts/create-template.ts +1724 -0
  182. package/template/src/admin/App.tsx +59 -0
  183. package/template/src/admin/components/AccountSwitcher.tsx +104 -0
  184. package/template/src/admin/components/CaptchaModal.tsx +150 -0
  185. package/template/src/admin/components/NotificationDrawer.tsx +213 -0
  186. package/template/src/admin/components/PageHeader.tsx +45 -0
  187. package/template/src/admin/components/PermissionConfigEditor.tsx +237 -0
  188. package/template/src/admin/components/PermissionGuard.tsx +108 -0
  189. package/template/src/admin/components/PermissionTree.tsx +193 -0
  190. package/template/src/admin/components/ProtectedRoute.tsx +33 -0
  191. package/template/src/admin/components/StatsCard.tsx +44 -0
  192. package/template/src/admin/components/UserFormModal.tsx +67 -0
  193. package/template/src/admin/components/UserTable.tsx +72 -0
  194. package/template/src/admin/components/__tests__/PageHeader.test.tsx +48 -0
  195. package/template/src/admin/components/__tests__/PermissionConfigEditor.test.tsx +147 -0
  196. package/template/src/admin/components/__tests__/PermissionGuard.test.tsx +221 -0
  197. package/template/src/admin/components/__tests__/PermissionTree.test.tsx +187 -0
  198. package/template/src/admin/components/__tests__/ProtectedRoute.test.tsx +129 -0
  199. package/template/src/admin/components/__tests__/StatsCard.test.tsx +44 -0
  200. package/template/src/admin/components/__tests__/UserFormModal.test.tsx +125 -0
  201. package/template/src/admin/components/__tests__/UserTable.test.tsx +89 -0
  202. package/template/src/admin/components/index.ts +7 -0
  203. package/template/src/admin/hooks/__tests__/useAuditLogs.test.ts +109 -0
  204. package/template/src/admin/hooks/__tests__/usePermissions.test.tsx +327 -0
  205. package/template/src/admin/hooks/__tests__/usePermissionsBranches.test.ts +198 -0
  206. package/template/src/admin/hooks/__tests__/useRoles.test.ts +261 -0
  207. package/template/src/admin/hooks/useAdminNotifications.ts +156 -0
  208. package/template/src/admin/hooks/useAuditLogs.ts +44 -0
  209. package/template/src/admin/hooks/useConfig.ts +168 -0
  210. package/template/src/admin/hooks/usePermissions.ts +156 -0
  211. package/template/src/admin/hooks/useRoles.ts +120 -0
  212. package/template/src/admin/layouts/Header.tsx +93 -0
  213. package/template/src/admin/layouts/Layout.tsx +23 -0
  214. package/template/src/admin/layouts/Sidebar.tsx +120 -0
  215. package/template/src/admin/main.tsx +18 -0
  216. package/template/src/admin/pages/ContentPage.tsx +252 -0
  217. package/template/src/admin/pages/DashboardPage.tsx +141 -0
  218. package/template/src/admin/pages/DisputesPage.tsx +261 -0
  219. package/template/src/admin/pages/LoginPage.tsx +121 -0
  220. package/template/src/admin/pages/MediaTestPage.tsx +524 -0
  221. package/template/src/admin/pages/OrdersPage.tsx +302 -0
  222. package/template/src/admin/pages/PermissionsPage.tsx +95 -0
  223. package/template/src/admin/pages/RegisterPage.tsx +103 -0
  224. package/template/src/admin/pages/RolesPage.tsx +330 -0
  225. package/template/src/admin/pages/SettingsPage.tsx +73 -0
  226. package/template/src/admin/pages/SystemLogsPage.tsx +218 -0
  227. package/template/src/admin/pages/TestCaptchaPage.tsx +120 -0
  228. package/template/src/admin/pages/TicketsPage.tsx +292 -0
  229. package/template/src/admin/pages/UsersPage.tsx +282 -0
  230. package/template/src/admin/pages/__tests__/ContentPage.test.tsx +213 -0
  231. package/template/src/admin/pages/__tests__/DashboardPage.test.tsx +153 -0
  232. package/template/src/admin/pages/__tests__/DisputesPage.test.tsx +196 -0
  233. package/template/src/admin/pages/__tests__/LoginPage.test.tsx +112 -0
  234. package/template/src/admin/pages/__tests__/OrdersPage.test.tsx +217 -0
  235. package/template/src/admin/pages/__tests__/PermissionsPage.test.tsx +119 -0
  236. package/template/src/admin/pages/__tests__/RegisterPage.test.tsx +179 -0
  237. package/template/src/admin/pages/__tests__/RolesPage.test.tsx +256 -0
  238. package/template/src/admin/pages/__tests__/SettingsPage.test.tsx +66 -0
  239. package/template/src/admin/pages/__tests__/SystemLogsPage.test.tsx +125 -0
  240. package/template/src/admin/pages/__tests__/TicketsPage.test.tsx +202 -0
  241. package/template/src/admin/pages/__tests__/UsersPage.test.tsx +229 -0
  242. package/template/src/admin/services/README.md +263 -0
  243. package/template/src/admin/services/TESTING.md +323 -0
  244. package/template/src/admin/services/__tests__/skipLoading.test.ts +146 -0
  245. package/template/src/admin/services/apiClient.ts +66 -0
  246. package/template/src/admin/services/requestInterceptor.ts +127 -0
  247. package/template/src/admin/services/types.ts +5 -0
  248. package/template/src/admin/stores/__tests__/adminStore.test.ts +184 -0
  249. package/template/src/admin/stores/__tests__/adminStoreBranches.test.ts +148 -0
  250. package/template/src/admin/stores/__tests__/captchaStore.test.ts +75 -0
  251. package/template/src/admin/stores/__tests__/captchaStoreBranches.test.ts +37 -0
  252. package/template/src/admin/stores/__tests__/loadingStore.test.ts +68 -0
  253. package/template/src/admin/stores/adminStore.ts +88 -0
  254. package/template/src/admin/stores/captchaStore.ts +47 -0
  255. package/template/src/admin/stores/loadingStore.ts +27 -0
  256. package/template/src/cli/index.ts +22 -0
  257. package/template/src/cli/modules/config/index.ts +132 -0
  258. package/template/src/cli/modules/index.ts +12 -0
  259. package/template/src/cli/modules/notification/index.ts +86 -0
  260. package/template/src/cli/modules/todo/index.ts +60 -0
  261. package/template/src/cli/rpc/client.ts +23 -0
  262. package/template/src/cli/rpc/index.ts +1 -0
  263. package/template/src/cli/utils/api.ts +17 -0
  264. package/template/src/cli/utils/auto-command.ts +229 -0
  265. package/template/src/cli/utils/index.ts +8 -0
  266. package/template/src/cli/utils/logger.ts +79 -0
  267. package/template/src/client/App.tsx +24 -0
  268. package/template/src/client/Layout.tsx +27 -0
  269. package/template/src/client/components/AuthButton.tsx +41 -0
  270. package/template/src/client/components/ConnectionStatus.tsx +75 -0
  271. package/template/src/client/components/EmptyState.tsx +26 -0
  272. package/template/src/client/components/Footer.tsx +17 -0
  273. package/template/src/client/components/LoadingSpinner.tsx +26 -0
  274. package/template/src/client/components/MessageCard.tsx +61 -0
  275. package/template/src/client/components/Navigation.tsx +65 -0
  276. package/template/src/client/components/StatusBadge.tsx +50 -0
  277. package/template/src/client/components/__tests__/App.test.tsx +79 -0
  278. package/template/src/client/components/__tests__/AuthButton.test.tsx +119 -0
  279. package/template/src/client/components/__tests__/ConnectionStatus.test.tsx +188 -0
  280. package/template/src/client/components/__tests__/EmptyState.test.tsx +70 -0
  281. package/template/src/client/components/__tests__/Footer.test.tsx +22 -0
  282. package/template/src/client/components/__tests__/LoadingSpinner.test.tsx +75 -0
  283. package/template/src/client/components/__tests__/MessageCard.test.tsx +142 -0
  284. package/template/src/client/components/__tests__/Navigation.test.tsx +40 -0
  285. package/template/src/client/components/__tests__/StatusBadge.test.tsx +107 -0
  286. package/template/src/client/components/index.ts +6 -0
  287. package/template/src/client/index.css +27 -0
  288. package/template/src/client/main.tsx +25 -0
  289. package/template/src/client/pages/ContentDetailPage.tsx +142 -0
  290. package/template/src/client/pages/ContentListPage.tsx +169 -0
  291. package/template/src/client/pages/NotificationPage.tsx +288 -0
  292. package/template/src/client/pages/TodoPage.tsx +389 -0
  293. package/template/src/client/pages/WebSocketPage.tsx +239 -0
  294. package/template/src/client/pages/__tests__/ContentDetailPage.test.tsx +176 -0
  295. package/template/src/client/pages/__tests__/ContentListPage.test.tsx +202 -0
  296. package/template/src/client/pages/__tests__/NotificationPage.test.tsx +246 -0
  297. package/template/src/client/pages/__tests__/TodoPage.test.tsx +433 -0
  298. package/template/src/client/pages/__tests__/WebSocketPage.test.tsx +378 -0
  299. package/template/src/client/services/apiClient.ts +66 -0
  300. package/template/src/client/stores/__tests__/authStore.test.ts +139 -0
  301. package/template/src/client/stores/__tests__/chatWSStore.test.ts +416 -0
  302. package/template/src/client/stores/__tests__/notificationStore.test.ts +453 -0
  303. package/template/src/client/stores/__tests__/todoStore.test.ts +509 -0
  304. package/template/src/client/stores/authStore.ts +51 -0
  305. package/template/src/client/stores/chatWSStore.ts +122 -0
  306. package/template/src/client/stores/notificationStore.ts +199 -0
  307. package/template/src/client/stores/todoStore.ts +207 -0
  308. package/template/src/server/__tests__/integration/todos-api.test.ts +138 -0
  309. package/template/src/server/app.ts +105 -0
  310. package/template/src/server/config.ts +86 -0
  311. package/template/src/server/core/__tests__/realtime-core.test.ts +329 -0
  312. package/template/src/server/core/__tests__/realtime-scanner.test.ts +210 -0
  313. package/template/src/server/core/__tests__/runtime-node.test.ts +330 -0
  314. package/template/src/server/core/__tests__/runtime.test.ts +219 -0
  315. package/template/src/server/core/__tests__/typed-runtime.test.ts +178 -0
  316. package/template/src/server/core/durable-objects/RealtimeDO.ts +206 -0
  317. package/template/src/server/core/index.ts +83 -0
  318. package/template/src/server/core/module-loader.ts +281 -0
  319. package/template/src/server/core/realtime-core.ts +152 -0
  320. package/template/src/server/core/realtime-scanner.ts +99 -0
  321. package/template/src/server/core/runtime-cloudflare.ts +207 -0
  322. package/template/src/server/core/runtime-node.ts +170 -0
  323. package/template/src/server/core/runtime.ts +129 -0
  324. package/template/src/server/core/typed-runtime.ts +80 -0
  325. package/template/src/server/db/__tests__/driver.test.ts +203 -0
  326. package/template/src/server/db/config.ts +1 -0
  327. package/template/src/server/db/driver-cloudflare.ts +50 -0
  328. package/template/src/server/db/driver.ts +116 -0
  329. package/template/src/server/db/index.ts +3 -0
  330. package/template/src/server/db/init.ts +412 -0
  331. package/template/src/server/db/schema/api-endpoints.ts +23 -0
  332. package/template/src/server/db/schema/contents.ts +31 -0
  333. package/template/src/server/db/schema/disputes.ts +39 -0
  334. package/template/src/server/db/schema/index.ts +14 -0
  335. package/template/src/server/db/schema/notifications.ts +16 -0
  336. package/template/src/server/db/schema/orders.ts +30 -0
  337. package/template/src/server/db/schema/permission-audit-logs.ts +17 -0
  338. package/template/src/server/db/schema/permission-route-mappings.ts +22 -0
  339. package/template/src/server/db/schema/permissions.ts +17 -0
  340. package/template/src/server/db/schema/role-permissions.ts +22 -0
  341. package/template/src/server/db/schema/roles.ts +17 -0
  342. package/template/src/server/db/schema/tickets.ts +62 -0
  343. package/template/src/server/db/schema/todo-attachments.ts +22 -0
  344. package/template/src/server/db/schema/todos.ts +21 -0
  345. package/template/src/server/db/schema/user-roles.ts +17 -0
  346. package/template/src/server/db/seeds/index.ts +1 -0
  347. package/template/src/server/db/seeds/permission-data.ts +354 -0
  348. package/template/src/server/db/test-setup.ts +647 -0
  349. package/template/src/server/entries/cloudflare.ts +71 -0
  350. package/template/src/server/entries/node.ts +190 -0
  351. package/template/src/server/index.ts +62 -0
  352. package/template/src/server/middleware/__tests__/auth-simple.test.ts +147 -0
  353. package/template/src/server/middleware/__tests__/auth.test.ts +276 -0
  354. package/template/src/server/middleware/__tests__/captcha.test.ts +258 -0
  355. package/template/src/server/middleware/__tests__/error-handler.test.ts +364 -0
  356. package/template/src/server/middleware/__tests__/error-response-format.test.ts +80 -0
  357. package/template/src/server/middleware/__tests__/permission.test.ts +312 -0
  358. package/template/src/server/middleware/audit-log.ts +101 -0
  359. package/template/src/server/middleware/auth.ts +235 -0
  360. package/template/src/server/middleware/captcha.ts +166 -0
  361. package/template/src/server/middleware/cors.ts +30 -0
  362. package/template/src/server/middleware/error-handler.ts +135 -0
  363. package/template/src/server/middleware/index.ts +24 -0
  364. package/template/src/server/middleware/logger.ts +73 -0
  365. package/template/src/server/middleware/permission.ts +127 -0
  366. package/template/src/server/middleware/rate-limit.ts +69 -0
  367. package/template/src/server/middleware/realtime-env.ts +12 -0
  368. package/template/src/server/module-admin/__tests__/admin-routes.test.ts +360 -0
  369. package/template/src/server/module-admin/__tests__/admin-service.test.ts +222 -0
  370. package/template/src/server/module-admin/__tests__/audit-log-routes.test.ts +261 -0
  371. package/template/src/server/module-admin/__tests__/export-routes.test.ts +160 -0
  372. package/template/src/server/module-admin/__tests__/media-routes.test.ts +122 -0
  373. package/template/src/server/module-admin/module.ts +40 -0
  374. package/template/src/server/module-admin/routes/admin-notification-routes.ts +197 -0
  375. package/template/src/server/module-admin/routes/admin-routes.ts +20 -0
  376. package/template/src/server/module-admin/routes/auth-routes.ts +86 -0
  377. package/template/src/server/module-admin/routes/export-routes.ts +207 -0
  378. package/template/src/server/module-admin/routes/media-routes.ts +78 -0
  379. package/template/src/server/module-admin/routes/system-routes.ts +89 -0
  380. package/template/src/server/module-admin/routes/user-management-routes.ts +143 -0
  381. package/template/src/server/module-admin/services/admin-service.ts +268 -0
  382. package/template/src/server/module-captcha/__tests__/captcha-route.test.ts +35 -0
  383. package/template/src/server/module-captcha/__tests__/captcha-service.test.ts +53 -0
  384. package/template/src/server/module-captcha/module.ts +35 -0
  385. package/template/src/server/module-captcha/routes/captcha-routes.ts +68 -0
  386. package/template/src/server/module-captcha/services/captcha-service.ts +3 -0
  387. package/template/src/server/module-chat/__tests__/chat-routes.test.ts +39 -0
  388. package/template/src/server/module-chat/__tests__/chat-rpc.test.ts +185 -0
  389. package/template/src/server/module-chat/__tests__/chat-service.test.ts +64 -0
  390. package/template/src/server/module-chat/module.ts +27 -0
  391. package/template/src/server/module-chat/routes/chat-routes.ts +46 -0
  392. package/template/src/server/module-chat/services/chat-service.ts +38 -0
  393. package/template/src/server/module-content/__tests__/content-route.test.ts +40 -0
  394. package/template/src/server/module-content/__tests__/content-service.test.ts +107 -0
  395. package/template/src/server/module-content/module.ts +39 -0
  396. package/template/src/server/module-content/routes/content-routes.ts +182 -0
  397. package/template/src/server/module-content/routes/public-content-routes.ts +58 -0
  398. package/template/src/server/module-content/services/content-service.ts +224 -0
  399. package/template/src/server/module-dispute/__tests__/dispute-route.test.ts +43 -0
  400. package/template/src/server/module-dispute/__tests__/dispute-service.test.ts +135 -0
  401. package/template/src/server/module-dispute/module.ts +30 -0
  402. package/template/src/server/module-dispute/routes/dispute-routes.ts +153 -0
  403. package/template/src/server/module-dispute/services/dispute-service.ts +255 -0
  404. package/template/src/server/module-file/__tests__/file-routes.test.ts +284 -0
  405. package/template/src/server/module-file/__tests__/file-storage-service.test.ts +323 -0
  406. package/template/src/server/module-file/module.ts +28 -0
  407. package/template/src/server/module-file/routes/file-routes.ts +288 -0
  408. package/template/src/server/module-notifications/__tests__/notification-route-rpc.test.ts +375 -0
  409. package/template/src/server/module-notifications/__tests__/notification-service.test.ts +241 -0
  410. package/template/src/server/module-notifications/__tests__/sse-rpc.test.ts +289 -0
  411. package/template/src/server/module-notifications/module.ts +32 -0
  412. package/template/src/server/module-notifications/routes/notification-routes.ts +190 -0
  413. package/template/src/server/module-notifications/services/notification-service.ts +104 -0
  414. package/template/src/server/module-order/__tests__/order-route.test.ts +258 -0
  415. package/template/src/server/module-order/__tests__/order-service.test.ts +222 -0
  416. package/template/src/server/module-order/module.ts +30 -0
  417. package/template/src/server/module-order/routes/order-routes.ts +202 -0
  418. package/template/src/server/module-order/services/order-service.ts +268 -0
  419. package/template/src/server/module-permission/__tests__/permission-middleware.test.ts +90 -0
  420. package/template/src/server/module-permission/__tests__/permission-routes.test.ts +330 -0
  421. package/template/src/server/module-permission/__tests__/permission-service-impl.test.ts +388 -0
  422. package/template/src/server/module-permission/__tests__/permission-service.test.ts +190 -0
  423. package/template/src/server/module-permission/__tests__/role-routes.test.ts +215 -0
  424. package/template/src/server/module-permission/module.ts +68 -0
  425. package/template/src/server/module-permission/routes/audit-log-routes.ts +76 -0
  426. package/template/src/server/module-permission/routes/permission-routes.ts +184 -0
  427. package/template/src/server/module-permission/routes/role-routes.ts +263 -0
  428. package/template/src/server/module-permission/services/audit-log-service.ts +108 -0
  429. package/template/src/server/module-permission/services/permission-service-impl.ts +233 -0
  430. package/template/src/server/module-permission/services/permission-service.ts +256 -0
  431. package/template/src/server/module-permission/services/role-service.ts +122 -0
  432. package/template/src/server/module-ticket/__tests__/ticket-route.test.ts +173 -0
  433. package/template/src/server/module-ticket/__tests__/ticket-service.test.ts +201 -0
  434. package/template/src/server/module-ticket/module.ts +30 -0
  435. package/template/src/server/module-ticket/routes/ticket-routes.ts +210 -0
  436. package/template/src/server/module-ticket/services/ticket-service.ts +297 -0
  437. package/template/src/server/module-todos/__tests__/todo-service.test.ts +196 -0
  438. package/template/src/server/module-todos/__tests__/todos-file-upload.test.ts +439 -0
  439. package/template/src/server/module-todos/__tests__/todos-route-rpc.test.ts +575 -0
  440. package/template/src/server/module-todos/module.ts +30 -0
  441. package/template/src/server/module-todos/routes/todos-routes.ts +260 -0
  442. package/template/src/server/module-todos/services/todo-service.ts +202 -0
  443. package/template/src/server/route-registry.ts +47 -0
  444. package/template/src/server/test-utils/test-client.ts +66 -0
  445. package/template/src/server/test-utils/test-server.ts +131 -0
  446. package/template/src/server/types/bindings.ts +10 -0
  447. package/template/src/server/utils/__tests__/app-error.test.ts +374 -0
  448. package/template/src/server/utils/__tests__/auth.test.ts +76 -0
  449. package/template/src/server/utils/__tests__/captcha.test.ts +80 -0
  450. package/template/src/server/utils/__tests__/file-storage.test.ts +472 -0
  451. package/template/src/server/utils/__tests__/logger.test.ts +221 -0
  452. package/template/src/server/utils/__tests__/route-helpers.test.ts +264 -0
  453. package/template/src/server/utils/app-error.ts +355 -0
  454. package/template/src/server/utils/auth.ts +65 -0
  455. package/template/src/server/utils/captcha.ts +119 -0
  456. package/template/src/server/utils/date.ts +50 -0
  457. package/template/src/server/utils/env.ts +4 -0
  458. package/template/src/server/utils/file-storage.ts +462 -0
  459. package/template/src/server/utils/generate.ts +26 -0
  460. package/template/src/server/utils/id-helpers.ts +5 -0
  461. package/template/src/server/utils/logger.ts +139 -0
  462. package/template/src/server/utils/permission-utils.ts +6 -0
  463. package/template/src/server/utils/response.ts +40 -0
  464. package/template/src/server/utils/route-helpers.ts +200 -0
  465. package/template/src/server/utils/uuid.ts +8 -0
  466. package/template/src/server/utils/ws-helper.ts +30 -0
  467. package/template/src/shared/constants/index.ts +1 -0
  468. package/template/src/shared/constants/resource-types.ts +72 -0
  469. package/template/src/shared/core/__tests__/sse-client.test.ts +376 -0
  470. package/template/src/shared/core/__tests__/ws-client.test.ts +468 -0
  471. package/template/src/shared/core/api-request.ts +192 -0
  472. package/template/src/shared/core/api-schemas.ts +41 -0
  473. package/template/src/shared/core/index.ts +37 -0
  474. package/template/src/shared/core/module-manifest.ts +120 -0
  475. package/template/src/shared/core/protocol-types.ts +26 -0
  476. package/template/src/shared/core/sse-client.ts +185 -0
  477. package/template/src/shared/core/ws-client.ts +236 -0
  478. package/template/src/shared/hooks/index.ts +2 -0
  479. package/template/src/shared/hooks/useSSE.ts +57 -0
  480. package/template/src/shared/hooks/useWebSocket.ts +66 -0
  481. package/template/src/shared/index.ts +5 -0
  482. package/template/src/shared/modules/admin/index.ts +1 -0
  483. package/template/src/shared/modules/admin/schemas.ts +100 -0
  484. package/template/src/shared/modules/audit/index.ts +1 -0
  485. package/template/src/shared/modules/audit/schemas.ts +24 -0
  486. package/template/src/shared/modules/captcha/index.ts +1 -0
  487. package/template/src/shared/modules/captcha/schemas.ts +19 -0
  488. package/template/src/shared/modules/chat/index.ts +38 -0
  489. package/template/src/shared/modules/content/index.ts +1 -0
  490. package/template/src/shared/modules/content/schemas.ts +53 -0
  491. package/template/src/shared/modules/dispute/index.ts +1 -0
  492. package/template/src/shared/modules/dispute/schemas.ts +62 -0
  493. package/template/src/shared/modules/files/index.ts +11 -0
  494. package/template/src/shared/modules/files/schemas.ts +44 -0
  495. package/template/src/shared/modules/index.ts +70 -0
  496. package/template/src/shared/modules/notifications/index.ts +26 -0
  497. package/template/src/shared/modules/notifications/schemas.ts +73 -0
  498. package/template/src/shared/modules/order/index.ts +1 -0
  499. package/template/src/shared/modules/order/schemas.ts +63 -0
  500. package/template/src/shared/modules/permission/index.ts +42 -0
  501. package/template/src/shared/modules/permission/permission-dependencies.ts +50 -0
  502. package/template/src/shared/modules/permission/permissions.ts +275 -0
  503. package/template/src/shared/modules/permission/schemas.ts +92 -0
  504. package/template/src/shared/modules/permission/types.ts +23 -0
  505. package/template/src/shared/modules/role/index.ts +1 -0
  506. package/template/src/shared/modules/role/schemas.ts +46 -0
  507. package/template/src/shared/modules/ticket/index.ts +1 -0
  508. package/template/src/shared/modules/ticket/schemas.ts +77 -0
  509. package/template/src/shared/modules/todos/index.ts +20 -0
  510. package/template/src/shared/modules/todos/schemas.ts +65 -0
  511. package/template/src/shared/schemas/index.ts +79 -0
  512. package/template/src/types/global.d.ts +14 -0
  513. package/template/src/vite-env.d.ts +21 -0
  514. package/template/tailwind.config.js +11 -0
  515. package/template/tests/e2e/admin-crud.spec.ts +81 -0
  516. package/template/tests/e2e/case-o1.spec.ts +108 -0
  517. package/template/tests/e2e/global-setup.ts +73 -0
  518. package/template/tests/e2e/global-teardown.ts +28 -0
  519. package/template/tests/e2e/notification.spec.ts +458 -0
  520. package/template/tests/e2e/todo.spec.ts +623 -0
  521. package/template/tests/e2e/websocket.spec.ts +254 -0
  522. package/template/tsconfig.json +35 -0
  523. package/template/tsup.config.ts +95 -0
  524. package/template/vite-plugins.ts +113 -0
  525. package/template/vite.config.ts +96 -0
  526. package/template/vitest.config.ts +53 -0
  527. package/template/vitest.integration.config.ts +24 -0
  528. package/template/vitest.integration.setup.ts +22 -0
  529. package/template/vitest.setup.ts +74 -0
  530. package/template/wrangler.toml +33 -0
@@ -0,0 +1,468 @@
1
+ /**
2
+ * @framework-baseline dac723f73f2c7757
3
+ */
4
+
5
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
6
+
7
+ type TestProtocol = {
8
+ rpc: {
9
+ echo: { in: { message: string }; out: { message: string; timestamp: number } }
10
+ ping: { in: Record<string, never>; out: { pong: boolean } }
11
+ }
12
+ events: {
13
+ broadcast: { text: string }
14
+ notification: { title: string }
15
+ }
16
+ }
17
+
18
+ let savedWebSocket: typeof globalThis.WebSocket | undefined
19
+
20
+ beforeEach(() => {
21
+ vi.useFakeTimers()
22
+ savedWebSocket = globalThis.WebSocket
23
+ delete (globalThis as Record<string, unknown>).WebSocket
24
+ })
25
+
26
+ afterEach(() => {
27
+ vi.useRealTimers()
28
+ if (savedWebSocket !== undefined) {
29
+ globalThis.WebSocket = savedWebSocket
30
+ } else {
31
+ delete (globalThis as Record<string, unknown>).WebSocket
32
+ }
33
+ })
34
+
35
+ async function getModule() {
36
+ vi.resetModules()
37
+ const mod = await import('../ws-client')
38
+ return mod
39
+ }
40
+
41
+ describe('WSClientImpl', () => {
42
+ describe('constructor and status', () => {
43
+ it('should start in connecting state with StubWebSocket', async () => {
44
+ const { WSClientImpl } = await getModule()
45
+ const client = new WSClientImpl<TestProtocol>('ws://localhost')
46
+ expect(client.status).toBe('connecting')
47
+ })
48
+
49
+ it('should transition to open on ws.onopen', async () => {
50
+ const { WSClientImpl } = await getModule()
51
+ const client = new WSClientImpl<TestProtocol>('ws://localhost')
52
+ const ws = (
53
+ client as unknown as { ws: { onopen: ((e: Event) => void) | null; readyState: number } }
54
+ ).ws
55
+
56
+ ws.readyState = 1
57
+ ws.onopen?.(new Event('open'))
58
+ expect(client.status).toBe('open')
59
+ })
60
+
61
+ it('should transition to closed on ws.onclose', async () => {
62
+ const { WSClientImpl } = await getModule()
63
+ const client = new WSClientImpl<TestProtocol>('ws://localhost')
64
+ const ws = (
65
+ client as unknown as {
66
+ ws: {
67
+ onopen: ((e: Event) => void) | null
68
+ onclose: ((e: CloseEvent) => void) | null
69
+ readyState: number
70
+ }
71
+ }
72
+ ).ws
73
+
74
+ ws.readyState = 1
75
+ ws.onopen?.(new Event('open'))
76
+ ws.readyState = 3
77
+ ws.onclose?.(new Event('close') as CloseEvent)
78
+ expect(client.status).toBe('closed')
79
+ })
80
+
81
+ it('should transition to closed on ws.onerror', async () => {
82
+ const { WSClientImpl } = await getModule()
83
+ const client = new WSClientImpl<TestProtocol>('ws://localhost')
84
+ const ws = (client as unknown as { ws: { onerror: ((e: Event) => void) | null } }).ws
85
+
86
+ ws.onerror?.(new Event('error'))
87
+ expect(client.status).toBe('closed')
88
+ })
89
+
90
+ it('should not double-transition if already closed on onclose', async () => {
91
+ const { WSClientImpl } = await getModule()
92
+ const client = new WSClientImpl<TestProtocol>('ws://localhost')
93
+ const ws = (
94
+ client as unknown as {
95
+ ws: { onclose: ((e: CloseEvent) => void) | null; readyState: number }
96
+ }
97
+ ).ws
98
+
99
+ ws.readyState = 3
100
+ ws.onclose?.(new Event('close') as CloseEvent)
101
+ expect(client.status).toBe('closed')
102
+ })
103
+
104
+ it('should not double-transition if already open on onopen', async () => {
105
+ const { WSClientImpl } = await getModule()
106
+ const client = new WSClientImpl<TestProtocol>('ws://localhost')
107
+ const ws = (
108
+ client as unknown as { ws: { onopen: ((e: Event) => void) | null; readyState: number } }
109
+ ).ws
110
+
111
+ ws.readyState = 1
112
+ ws.onopen?.(new Event('open'))
113
+ const handler = vi.fn()
114
+ client.onStatusChange(handler)
115
+ handler.mockClear()
116
+ ws.onopen?.(new Event('open'))
117
+ expect(handler).not.toHaveBeenCalled()
118
+ })
119
+
120
+ it('should not transition on onerror if already closed', async () => {
121
+ const { WSClientImpl } = await getModule()
122
+ const client = new WSClientImpl<TestProtocol>('ws://localhost')
123
+ const ws = (client as unknown as { ws: { onerror: ((e: Event) => void) | null } }).ws
124
+
125
+ ws.onerror?.(new Event('error'))
126
+ const handler = vi.fn()
127
+ client.onStatusChange(handler)
128
+ handler.mockClear()
129
+ ws.onerror?.(new Event('error'))
130
+ expect(handler).not.toHaveBeenCalled()
131
+ })
132
+
133
+ it('should return null for getSocket with StubWebSocket', async () => {
134
+ const { WSClientImpl } = await getModule()
135
+ const client = new WSClientImpl<TestProtocol>('ws://localhost')
136
+ expect(client.getSocket()).toBeNull()
137
+ })
138
+ })
139
+
140
+ describe('call()', () => {
141
+ it('should send RPC request and resolve with result', async () => {
142
+ const { WSClientImpl } = await getModule()
143
+ const client = new WSClientImpl<TestProtocol>('ws://localhost')
144
+ const ws = (
145
+ client as unknown as {
146
+ ws: {
147
+ send: (d: string) => void
148
+ onopen: ((e: Event) => void) | null
149
+ onmessage: ((e: MessageEvent) => void) | null
150
+ readyState: number
151
+ }
152
+ }
153
+ ).ws
154
+ const sendSpy = vi.spyOn(ws, 'send')
155
+
156
+ ws.readyState = 1
157
+ ws.onopen?.(new Event('open'))
158
+
159
+ const callPromise = client.call('echo', { message: 'hello' })
160
+ expect(sendSpy).toHaveBeenCalledTimes(1)
161
+ const sent = JSON.parse(sendSpy.mock.calls[0][0] as string)
162
+ expect(sent.method).toBe('echo')
163
+
164
+ ws.onmessage?.({
165
+ data: JSON.stringify({ id: sent.id, result: { message: 'hello', timestamp: 123 } }),
166
+ } as MessageEvent)
167
+ const result = await callPromise
168
+ expect(result).toEqual({ message: 'hello', timestamp: 123 })
169
+ })
170
+
171
+ it('should reject on RPC error response', async () => {
172
+ const { WSClientImpl } = await getModule()
173
+ const client = new WSClientImpl<TestProtocol>('ws://localhost')
174
+ const ws = (
175
+ client as unknown as {
176
+ ws: {
177
+ send: (d: string) => void
178
+ onopen: ((e: Event) => void) | null
179
+ onmessage: ((e: MessageEvent) => void) | null
180
+ readyState: number
181
+ }
182
+ }
183
+ ).ws
184
+ const sendSpy = vi.spyOn(ws, 'send')
185
+
186
+ ws.readyState = 1
187
+ ws.onopen?.(new Event('open'))
188
+
189
+ const callPromise = client.call('echo', { message: 'fail' })
190
+ const sent = JSON.parse(sendSpy.mock.calls[0][0] as string)
191
+ ws.onmessage?.({
192
+ data: JSON.stringify({ id: sent.id, error: 'Something went wrong' }),
193
+ } as MessageEvent)
194
+ await expect(callPromise).rejects.toThrow('Something went wrong')
195
+ })
196
+
197
+ it('should reject on timeout', async () => {
198
+ const { WSClientImpl } = await getModule()
199
+ const client = new WSClientImpl<TestProtocol>('ws://localhost')
200
+ const ws = (
201
+ client as unknown as {
202
+ ws: { send: (d: string) => void; onopen: ((e: Event) => void) | null; readyState: number }
203
+ }
204
+ ).ws
205
+ vi.spyOn(ws, 'send')
206
+
207
+ ws.readyState = 1
208
+ ws.onopen?.(new Event('open'))
209
+
210
+ const callPromise = client.call('echo', { message: 'timeout' }, 1000)
211
+ vi.advanceTimersByTime(1001)
212
+ await expect(callPromise).rejects.toThrow('RPC Timeout: echo')
213
+ })
214
+
215
+ it('should clear timeout timer on resolve', async () => {
216
+ const { WSClientImpl } = await getModule()
217
+ const client = new WSClientImpl<TestProtocol>('ws://localhost')
218
+ const ws = (
219
+ client as unknown as {
220
+ ws: {
221
+ send: (d: string) => void
222
+ onopen: ((e: Event) => void) | null
223
+ onmessage: ((e: MessageEvent) => void) | null
224
+ readyState: number
225
+ }
226
+ }
227
+ ).ws
228
+ const sendSpy = vi.spyOn(ws, 'send')
229
+
230
+ ws.readyState = 1
231
+ ws.onopen?.(new Event('open'))
232
+
233
+ const callPromise = client.call('echo', { message: 'hello' }, 1000)
234
+ const sent = JSON.parse(sendSpy.mock.calls[0][0] as string)
235
+ ws.onmessage?.({
236
+ data: JSON.stringify({ id: sent.id, result: { message: 'hello', timestamp: 1 } }),
237
+ } as MessageEvent)
238
+ await callPromise
239
+ vi.advanceTimersByTime(2000)
240
+ })
241
+
242
+ it('should buffer messages when socket is not open', async () => {
243
+ const { WSClientImpl } = await getModule()
244
+ const client = new WSClientImpl<TestProtocol>('ws://localhost')
245
+ const ws = (
246
+ client as unknown as {
247
+ ws: {
248
+ send: (d: string) => void
249
+ onopen: ((e: Event) => void) | null
250
+ onmessage: ((e: MessageEvent) => void) | null
251
+ readyState: number
252
+ }
253
+ }
254
+ ).ws
255
+ const sendSpy = vi.spyOn(ws, 'send')
256
+
257
+ const callPromise = client.call('echo', { message: 'buffered' }, 5000)
258
+ expect(sendSpy).not.toHaveBeenCalled()
259
+
260
+ ws.readyState = 1
261
+ ws.onopen?.(new Event('open'))
262
+ expect(sendSpy).toHaveBeenCalledTimes(1)
263
+ const sent = JSON.parse(sendSpy.mock.calls[0][0] as string)
264
+ ws.onmessage?.({
265
+ data: JSON.stringify({ id: sent.id, result: { message: 'buffered', timestamp: 1 } }),
266
+ } as MessageEvent)
267
+ await callPromise
268
+ })
269
+ })
270
+
271
+ describe('on()', () => {
272
+ it('should register event handler and receive events', async () => {
273
+ const { WSClientImpl } = await getModule()
274
+ const client = new WSClientImpl<TestProtocol>('ws://localhost')
275
+ const ws = (client as unknown as { ws: { onmessage: ((e: MessageEvent) => void) | null } }).ws
276
+
277
+ const handler = vi.fn()
278
+ client.on('broadcast', handler)
279
+
280
+ ws.onmessage?.({
281
+ data: JSON.stringify({ type: 'broadcast', payload: { text: 'hello' } }),
282
+ } as MessageEvent)
283
+ expect(handler).toHaveBeenCalledWith({ text: 'hello' })
284
+ })
285
+
286
+ it('should support multiple handlers for same event', async () => {
287
+ const { WSClientImpl } = await getModule()
288
+ const client = new WSClientImpl<TestProtocol>('ws://localhost')
289
+ const ws = (client as unknown as { ws: { onmessage: ((e: MessageEvent) => void) | null } }).ws
290
+
291
+ const handler1 = vi.fn()
292
+ const handler2 = vi.fn()
293
+ client.on('broadcast', handler1)
294
+ client.on('broadcast', handler2)
295
+
296
+ ws.onmessage?.({
297
+ data: JSON.stringify({ type: 'broadcast', payload: { text: 'hi' } }),
298
+ } as MessageEvent)
299
+ expect(handler1).toHaveBeenCalledWith({ text: 'hi' })
300
+ expect(handler2).toHaveBeenCalledWith({ text: 'hi' })
301
+ })
302
+
303
+ it('should return unsubscribe function that removes handler', async () => {
304
+ const { WSClientImpl } = await getModule()
305
+ const client = new WSClientImpl<TestProtocol>('ws://localhost')
306
+ const ws = (client as unknown as { ws: { onmessage: ((e: MessageEvent) => void) | null } }).ws
307
+
308
+ const handler = vi.fn()
309
+ const unsub = client.on('broadcast', handler)
310
+ unsub()
311
+
312
+ ws.onmessage?.({
313
+ data: JSON.stringify({ type: 'broadcast', payload: { text: 'hello' } }),
314
+ } as MessageEvent)
315
+ expect(handler).not.toHaveBeenCalled()
316
+ })
317
+
318
+ it('should not call handlers for different event types', async () => {
319
+ const { WSClientImpl } = await getModule()
320
+ const client = new WSClientImpl<TestProtocol>('ws://localhost')
321
+ const ws = (client as unknown as { ws: { onmessage: ((e: MessageEvent) => void) | null } }).ws
322
+
323
+ const handler = vi.fn()
324
+ client.on('broadcast', handler)
325
+
326
+ ws.onmessage?.({
327
+ data: JSON.stringify({ type: 'notification', payload: { title: 'test' } }),
328
+ } as MessageEvent)
329
+ expect(handler).not.toHaveBeenCalled()
330
+ })
331
+ })
332
+
333
+ describe('emit()', () => {
334
+ it('should send typed event when socket is open', async () => {
335
+ const { WSClientImpl } = await getModule()
336
+ const client = new WSClientImpl<TestProtocol>('ws://localhost')
337
+ const ws = (
338
+ client as unknown as {
339
+ ws: { send: (d: string) => void; onopen: ((e: Event) => void) | null; readyState: number }
340
+ }
341
+ ).ws
342
+ const sendSpy = vi.spyOn(ws, 'send')
343
+
344
+ ws.readyState = 1
345
+ ws.onopen?.(new Event('open'))
346
+
347
+ client.emit('broadcast', { text: 'hello world' })
348
+ expect(sendSpy).toHaveBeenCalledTimes(1)
349
+ const sent = JSON.parse(sendSpy.mock.calls[0][0] as string)
350
+ expect(sent).toEqual({ type: 'broadcast', payload: { text: 'hello world' } })
351
+ })
352
+
353
+ it('should buffer event when socket is not open', async () => {
354
+ const { WSClientImpl } = await getModule()
355
+ const client = new WSClientImpl<TestProtocol>('ws://localhost')
356
+ const ws = (
357
+ client as unknown as {
358
+ ws: { send: (d: string) => void; onopen: ((e: Event) => void) | null; readyState: number }
359
+ }
360
+ ).ws
361
+ const sendSpy = vi.spyOn(ws, 'send')
362
+
363
+ client.emit('broadcast', { text: 'buffered' })
364
+ expect(sendSpy).not.toHaveBeenCalled()
365
+
366
+ ws.readyState = 1
367
+ ws.onopen?.(new Event('open'))
368
+ expect(sendSpy).toHaveBeenCalledTimes(1)
369
+ })
370
+ })
371
+
372
+ describe('onStatusChange()', () => {
373
+ it('should call handler on status transitions', async () => {
374
+ const { WSClientImpl } = await getModule()
375
+ const client = new WSClientImpl<TestProtocol>('ws://localhost')
376
+ const ws = (
377
+ client as unknown as {
378
+ ws: {
379
+ onopen: ((e: Event) => void) | null
380
+ onclose: ((e: CloseEvent) => void) | null
381
+ readyState: number
382
+ }
383
+ }
384
+ ).ws
385
+
386
+ const handler = vi.fn()
387
+ client.onStatusChange(handler)
388
+
389
+ ws.readyState = 1
390
+ ws.onopen?.(new Event('open'))
391
+ expect(handler).toHaveBeenCalledWith('open')
392
+
393
+ ws.readyState = 3
394
+ ws.onclose?.(new Event('close') as CloseEvent)
395
+ expect(handler).toHaveBeenCalledWith('closed')
396
+ })
397
+
398
+ it('should return unsubscribe function', async () => {
399
+ const { WSClientImpl } = await getModule()
400
+ const client = new WSClientImpl<TestProtocol>('ws://localhost')
401
+ const ws = (
402
+ client as unknown as { ws: { onopen: ((e: Event) => void) | null; readyState: number } }
403
+ ).ws
404
+
405
+ const handler = vi.fn()
406
+ const unsub = client.onStatusChange(handler)
407
+ unsub()
408
+
409
+ ws.readyState = 1
410
+ ws.onopen?.(new Event('open'))
411
+ expect(handler).not.toHaveBeenCalled()
412
+ })
413
+ })
414
+
415
+ describe('close()', () => {
416
+ it('should close the underlying socket', async () => {
417
+ const { WSClientImpl } = await getModule()
418
+ const client = new WSClientImpl<TestProtocol>('ws://localhost')
419
+ const ws = (client as unknown as { ws: { close: () => void } }).ws
420
+ const closeSpy = vi.spyOn(ws, 'close')
421
+
422
+ client.close()
423
+ expect(closeSpy).toHaveBeenCalledTimes(1)
424
+ })
425
+ })
426
+
427
+ describe('handleMessage edge cases', () => {
428
+ it('should ignore messages without id or type', async () => {
429
+ const { WSClientImpl } = await getModule()
430
+ const client = new WSClientImpl<TestProtocol>('ws://localhost')
431
+ const ws = (client as unknown as { ws: { onmessage: ((e: MessageEvent) => void) | null } }).ws
432
+
433
+ const handler = vi.fn()
434
+ client.on('broadcast', handler)
435
+
436
+ ws.onmessage?.({ data: JSON.stringify({ method: 'test', payload: 'data' }) } as MessageEvent)
437
+ expect(handler).not.toHaveBeenCalled()
438
+ })
439
+
440
+ it('should ignore unknown RPC response ids', async () => {
441
+ const { WSClientImpl } = await getModule()
442
+ const client = new WSClientImpl<TestProtocol>('ws://localhost')
443
+ const ws = (client as unknown as { ws: { onmessage: ((e: MessageEvent) => void) | null } }).ws
444
+
445
+ ws.onmessage?.({ data: JSON.stringify({ id: 'unknown-id', result: 'data' }) } as MessageEvent)
446
+ })
447
+
448
+ it('should handle invalid JSON gracefully', async () => {
449
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
450
+ const { WSClientImpl } = await getModule()
451
+ const client = new WSClientImpl<TestProtocol>('ws://localhost')
452
+ const ws = (client as unknown as { ws: { onmessage: ((e: MessageEvent) => void) | null } }).ws
453
+
454
+ ws.onmessage?.({ data: 'not-json' } as MessageEvent)
455
+ expect(consoleSpy).toHaveBeenCalled()
456
+ consoleSpy.mockRestore()
457
+ })
458
+ })
459
+ })
460
+
461
+ describe('createWSClient', () => {
462
+ it('should create a WSClient instance', async () => {
463
+ const { createWSClient } = await getModule()
464
+ const client = createWSClient<TestProtocol>('ws://localhost')
465
+ expect(client).toBeDefined()
466
+ expect(client.status).toBe('connecting')
467
+ })
468
+ })
@@ -0,0 +1,192 @@
1
+ /**
2
+ * @framework-baseline a1b2c3d4e5f6g7h8
3
+ * @framework-modify
4
+ * @reason 新增 API 请求包装器,提供链式调用的增强功能(loading、重试、错误处理)
5
+ * @impact 所有使用 api() 函数的 API 调用都会受到影响,提供统一的请求处理能力
6
+ *
7
+ * API 请求包装器
8
+ *
9
+ * 提供链式调用的 API 请求增强功能:
10
+ * - withLoading(): 显示 loading 状态
11
+ * - withRetry(): 自动重试
12
+ * - silentError(): 静默错误
13
+ * - json(): 解析响应并自动处理错误
14
+ */
15
+
16
+ import type { ApiSuccess, ApiError as ApiErrorType } from './api-schemas'
17
+
18
+ export interface ApiRequestOptions {
19
+ loading?: boolean | string
20
+ retry?: number
21
+ retryDelay?: number
22
+ silentError?: boolean
23
+ }
24
+
25
+ export class ApiRequestError extends Error {
26
+ readonly code?: string
27
+ readonly status?: number
28
+ readonly details?: Array<{ field?: string; message: string; code?: string }>
29
+ readonly timestamp?: string
30
+
31
+ constructor(error: ApiErrorType) {
32
+ super(error.error)
33
+ this.name = 'ApiRequestError'
34
+ this.code = error.code
35
+ this.status = error.status
36
+ this.details = error.details
37
+ this.timestamp = error.timestamp
38
+ }
39
+ }
40
+
41
+ type LoadingStore = {
42
+ startLoading: (text?: string) => void
43
+ stopLoading: () => void
44
+ }
45
+
46
+ type MessageApi = {
47
+ error: (msg: string) => void
48
+ }
49
+
50
+ let loadingStore: LoadingStore | null = null
51
+ let messageApi: MessageApi | null = null
52
+
53
+ export function setupApiRequestDeps(deps: { loadingStore: LoadingStore; messageApi: MessageApi }) {
54
+ loadingStore = deps.loadingStore
55
+ messageApi = deps.messageApi
56
+ }
57
+
58
+ interface HonoClientResponse<T> {
59
+ ok: boolean
60
+ status: number
61
+ statusText: string
62
+ json(): Promise<T>
63
+ }
64
+
65
+ export class ApiRequest<T> {
66
+ private options: ApiRequestOptions = {}
67
+ private requestPromise: Promise<HonoClientResponse<ApiSuccess<T> | ApiErrorType>>
68
+
69
+ constructor(promise: Promise<HonoClientResponse<ApiSuccess<T> | ApiErrorType>>) {
70
+ this.requestPromise = promise
71
+ }
72
+
73
+ withLoading(text?: boolean | string): this {
74
+ this.options.loading = text === undefined ? true : text
75
+ return this
76
+ }
77
+
78
+ withRetry(count: number, delay = 1000): this {
79
+ this.options.retry = count
80
+ this.options.retryDelay = delay
81
+ return this
82
+ }
83
+
84
+ silentError(): this {
85
+ this.options.silentError = true
86
+ return this
87
+ }
88
+
89
+ async json(): Promise<T> {
90
+ const { loading, retry = 0, retryDelay = 1000, silentError } = this.options
91
+
92
+ if (loading !== undefined && loading !== false && loadingStore) {
93
+ const text = typeof loading === 'string' ? loading : undefined
94
+ loadingStore.startLoading(text)
95
+ }
96
+
97
+ let lastError: Error | null = null
98
+ let attempts = 0
99
+
100
+ while (attempts <= retry) {
101
+ try {
102
+ const response = await this.requestPromise
103
+
104
+ if (!response.ok) {
105
+ const errorData = await this.tryParseError(response)
106
+ throw new ApiRequestError(errorData)
107
+ }
108
+
109
+ const result = await response.json()
110
+
111
+ if (result.success) {
112
+ if (loading !== undefined && loading !== false && loadingStore) {
113
+ loadingStore.stopLoading()
114
+ }
115
+ return result.data
116
+ }
117
+
118
+ throw new ApiRequestError(result)
119
+ } catch (error) {
120
+ lastError = error as Error
121
+ attempts++
122
+
123
+ if (attempts <= retry && this.shouldRetry(error)) {
124
+ await this.delay(retryDelay)
125
+ continue
126
+ }
127
+
128
+ break
129
+ }
130
+ }
131
+
132
+ if (loading !== undefined && loading !== false && loadingStore) {
133
+ loadingStore.stopLoading()
134
+ }
135
+
136
+ const error = lastError!
137
+
138
+ if (!silentError) {
139
+ if (error instanceof ApiRequestError) {
140
+ messageApi?.error(error.message)
141
+ } else {
142
+ messageApi?.error('请求失败,请稍后重试')
143
+ }
144
+ }
145
+
146
+ throw error
147
+ }
148
+
149
+ private async tryParseError(response: HonoClientResponse<unknown>): Promise<ApiErrorType> {
150
+ try {
151
+ const data = (await response.json()) as unknown
152
+ if (data && typeof data === 'object' && 'error' in data) {
153
+ return data as ApiErrorType
154
+ }
155
+ } catch {
156
+ // ignore
157
+ }
158
+ return {
159
+ success: false,
160
+ error: `HTTP Error: ${response.status} ${response.statusText}`,
161
+ status: response.status,
162
+ }
163
+ }
164
+
165
+ private shouldRetry(error: unknown): boolean {
166
+ if (error instanceof ApiRequestError) {
167
+ const status = error.status
168
+ if (status === 401 || status === 403 || status === 404 || status === 422) {
169
+ return false
170
+ }
171
+ return true
172
+ }
173
+ return error instanceof TypeError
174
+ }
175
+
176
+ private delay(ms: number): Promise<void> {
177
+ return new Promise(resolve => setTimeout(resolve, ms))
178
+ }
179
+ }
180
+
181
+ type ExtractJsonType<T> = T extends { json(): Promise<infer J> } ? J : never
182
+ type ExtractData<T> = T extends ApiSuccess<infer D> ? D : never
183
+
184
+ export function api<T extends { json(): Promise<unknown> }>(
185
+ promise: Promise<T>
186
+ ): ApiRequest<ExtractData<ExtractJsonType<T>>> {
187
+ return new ApiRequest<ExtractData<ExtractJsonType<T>>>(
188
+ promise as unknown as Promise<
189
+ HonoClientResponse<ApiSuccess<ExtractData<ExtractJsonType<T>>> | ApiErrorType>
190
+ >
191
+ )
192
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * @framework-baseline 295608e4d0c3a8de
3
+ *
4
+ * 此文件属于框架层代码。如需修改,请添加以下说明:
5
+ *
6
+ * @framework-modify
7
+ * @reason [必填] 修改原因
8
+ * @impact [必填] 影响范围
9
+ */
10
+
11
+ import { z } from '@hono/zod-openapi'
12
+
13
+ export const ApiSuccessSchema = <T extends z.ZodTypeAny>(dataSchema: T) =>
14
+ z.object({
15
+ success: z.literal(true),
16
+ data: dataSchema,
17
+ timestamp: z.string(),
18
+ })
19
+
20
+ export const ApiErrorDetailSchema = z.object({
21
+ field: z.string().optional(),
22
+ message: z.string(),
23
+ code: z.string().optional(),
24
+ })
25
+
26
+ export const ApiErrorSchema = z.object({
27
+ success: z.literal(false),
28
+ error: z.string(),
29
+ code: z.string().optional(),
30
+ status: z.number().optional(),
31
+ details: z.array(ApiErrorDetailSchema).optional(),
32
+ timestamp: z.string().optional(),
33
+ })
34
+
35
+ export const ApiResponseSchema = <T extends z.ZodTypeAny>(dataSchema: T) =>
36
+ z.union([ApiSuccessSchema(dataSchema), ApiErrorSchema])
37
+
38
+ export type ApiSuccess<T> = { success: true; data: T; timestamp: string }
39
+ export type ApiErrorDetail = z.infer<typeof ApiErrorDetailSchema>
40
+ export type ApiError = z.infer<typeof ApiErrorSchema>
41
+ export type ApiResponse<T> = ApiSuccess<T> | ApiError