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.
- package/README.md +154 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +439 -0
- package/dist/cli/index.js.map +1 -0
- package/package.json +171 -0
- package/template/.agents/skills/writing-hookify-rules/SKILL.md +408 -0
- package/template/.ai-context.md +368 -0
- package/template/.claude/hookify.block-dangerous-commands.local.md +41 -0
- package/template/.claude/hookify.check-api-types.local.md +111 -0
- package/template/.claude/hookify.check-duplicate-types.local.md +287 -0
- package/template/.claude/hookify.check-test-assertions.local.md +93 -0
- package/template/.claude/hookify.remind-add-tests.local.md +209 -0
- package/template/.claude/hookify.remind-db-migration.local.md +58 -0
- package/template/.claude/hookify.remind-run-tests.local.md +52 -0
- package/template/.claude/hookify.warn-any-type.local.md +203 -0
- package/template/.claude/hookify.warn-console-log.local.md +73 -0
- package/template/.claude/rules/00-project-config.md +147 -0
- package/template/.claude/rules/01-file-types.md +127 -0
- package/template/.claude/rules/02-git-workflow.md +167 -0
- package/template/.claude/rules/10-api-type-inference.md +555 -0
- package/template/.claude/rules/20-server-api.md +349 -0
- package/template/.claude/rules/21-server-entrypoint.md +257 -0
- package/template/.claude/rules/30-client-components.md +314 -0
- package/template/.claude/rules/31-client-services.md +768 -0
- package/template/.claude/rules/32-client-state-zustand.md +324 -0
- package/template/.claude/rules/33-client-app-entry.md +276 -0
- package/template/.claude/rules/34-client-admin-module.md +445 -0
- package/template/.claude/rules/35-cli-module.md +259 -0
- package/template/.claude/rules/40-admin-module.md +495 -0
- package/template/.claude/rules/40-shared-types.md +450 -0
- package/template/.claude/rules/50-websocket.md +245 -0
- package/template/.claude/rules/51-sse.md +372 -0
- package/template/.claude/rules/52-cloudflare-realtime.md +384 -0
- package/template/.claude/rules/60-testing-standards.md +245 -0
- package/template/.claude/rules/61-hono-testing.md +200 -0
- package/template/.claude/scripts/README.md +187 -0
- package/template/.claude/scripts/check-duplicate-types.ts +188 -0
- package/template/.claude/scripts/post-edit-check-incremental.sh +61 -0
- package/template/.claude/scripts/post-edit-check.sh +22 -0
- package/template/.claude/scripts/pre-tool-use-check.sh +86 -0
- package/template/.claude/scripts/test-pre-tool-use.sh +73 -0
- package/template/.claude/scripts/validate-git-changes.sh +38 -0
- package/template/.claude/settings.json +32 -0
- package/template/.claude/skills/writing-hookify-rules/SKILL.md +408 -0
- package/template/.dev-context-compact.md +54 -0
- package/template/.dev-context.md +621 -0
- package/template/.dev-guide.md +243 -0
- package/template/.env.example +60 -0
- package/template/.eslintrc.routes.json +39 -0
- package/template/.husky/_/husky.sh +27 -0
- package/template/.husky/commit-msg +4 -0
- package/template/.husky/post-commit +4 -0
- package/template/.husky/pre-commit +19 -0
- package/template/.husky/pre-push +12 -0
- package/template/.prettierignore +22 -0
- package/template/.prettierrc +8 -0
- package/template/.sessions/3/2026-03-26T05-41-09-154Z_03bb597f-3898-4a2e-8c08-5746bbe8d95e.jsonl +18 -0
- package/template/.sessions/3/2026-03-26T05-48-30-052Z_12de4774-9cb3-494c-9852-bd1e6b0003c3.jsonl +6 -0
- package/template/.vscode/extensions.json +8 -0
- package/template/.vscode/settings.json +4 -0
- package/template/.workspaces/3/hello.py +1 -0
- package/template/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/803c2e37182b913f8857edf6d8776515cef4af6d48fcb25913e457f4b14ffa97.sqlite +0 -0
- package/template/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/803c2e37182b913f8857edf6d8776515cef4af6d48fcb25913e457f4b14ffa97.sqlite-shm +0 -0
- package/template/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/803c2e37182b913f8857edf6d8776515cef4af6d48fcb25913e457f4b14ffa97.sqlite-wal +0 -0
- package/template/CLAUDE.md +296 -0
- package/template/DESIGN.md +260 -0
- package/template/DEV_CONTEXT_GUIDE.md +366 -0
- package/template/DEV_TOOLS_README.md +396 -0
- package/template/FRAMEWORK_HISTORY.md +34 -0
- package/template/PROJECT_SUMMARY.md +325 -0
- package/template/QUICKSTART.md +305 -0
- package/template/README.md +154 -0
- package/template/STRUCTURE.txt +81 -0
- package/template/admin.html +13 -0
- package/template/auth-inject.html +33 -0
- package/template/commitlint.config.js +12 -0
- package/template/docs/PERMISSION_ARCHITECTURE.md +279 -0
- package/template/docs/PERMISSION_EXAMPLES.md +230 -0
- package/template/docs/PERMISSION_SYSTEM.md +301 -0
- package/template/docs/SMART_TEST.md +171 -0
- package/template/docs/runtime-abstract.md +253 -0
- package/template/drizzle/0000_rainy_boomer.sql +101 -0
- package/template/drizzle/0001_add_todo_attachments.sql +12 -0
- package/template/drizzle/0002_chilly_magneto.sql +89 -0
- package/template/drizzle/meta/0000_snapshot.json +652 -0
- package/template/drizzle/meta/0001_snapshot.json +735 -0
- package/template/drizzle/meta/0002_snapshot.json +1198 -0
- package/template/drizzle/meta/_journal.json +27 -0
- package/template/drizzle.config.ts +13 -0
- package/template/eslint-rules/README-no-middleware-in-routes.md +154 -0
- package/template/eslint-rules/__tests__/enforce-valid-method.test.ts +251 -0
- package/template/eslint-rules/__tests__/layer-boundary.test.ts +102 -0
- package/template/eslint-rules/__tests__/module-boundary.test.ts +77 -0
- package/template/eslint-rules/__tests__/no-direct-zod-import-in-file-routes.test.ts +45 -0
- package/template/eslint-rules/__tests__/no-new-old-service-naming.test.ts +76 -0
- package/template/eslint-rules/e2e-test-location.js +153 -0
- package/template/eslint-rules/enforce-valid-method.js +217 -0
- package/template/eslint-rules/flat-routes-services.js +69 -0
- package/template/eslint-rules/framework-protect.js +186 -0
- package/template/eslint-rules/layer-boundary.js +164 -0
- package/template/eslint-rules/limit-type-complexity.js +130 -0
- package/template/eslint-rules/middleware-location.js +162 -0
- package/template/eslint-rules/module-boundary.js +125 -0
- package/template/eslint-rules/no-ambiguous-file-paths.js +64 -0
- package/template/eslint-rules/no-any-on-apiclient.js +173 -0
- package/template/eslint-rules/no-boolean-success.js +43 -0
- package/template/eslint-rules/no-deep-relative-imports.js +69 -0
- package/template/eslint-rules/no-direct-fetch.js +122 -0
- package/template/eslint-rules/no-direct-ws-sse.js +57 -0
- package/template/eslint-rules/no-direct-zod-import-in-file-routes.js +54 -0
- package/template/eslint-rules/no-disable-direct-fetch.js +57 -0
- package/template/eslint-rules/no-disable-type-safe-client.js +58 -0
- package/template/eslint-rules/no-inline-schema.js +220 -0
- package/template/eslint-rules/no-middleware-in-routes.js +81 -0
- package/template/eslint-rules/no-new-old-service-naming.js +123 -0
- package/template/eslint-rules/no-type-assertion-in-rpc.js +99 -0
- package/template/eslint-rules/no-type-assertion-on-shared-types.js +109 -0
- package/template/eslint-rules/no-util-functions-in-service.js +128 -0
- package/template/eslint-rules/prefer-shared-types.js +352 -0
- package/template/eslint-rules/protect-ws-sse-interface.js +66 -0
- package/template/eslint-rules/require-antd-generic-types.js +73 -0
- package/template/eslint-rules/require-file-openapi-props.js +114 -0
- package/template/eslint-rules/require-hono-chain-syntax.js +129 -0
- package/template/eslint-rules/require-nullable-for-optional.js +99 -0
- package/template/eslint-rules/require-response-helpers.js +140 -0
- package/template/eslint-rules/require-type-safe-test-client.js +106 -0
- package/template/eslint-rules/route-location.js +72 -0
- package/template/eslint.config.js +231 -0
- package/template/index.html +17 -0
- package/template/lint-scripts/DIRECTORY_RULES.md +335 -0
- package/template/lint-scripts/README.md +299 -0
- package/template/lint-scripts/TESTING.md +273 -0
- package/template/lint-scripts/ai-context-generator.ts +159 -0
- package/template/lint-scripts/check-framework-modify.sh +8 -0
- package/template/lint-scripts/check-menu-permissions.js +242 -0
- package/template/lint-scripts/check-refs.ts +165 -0
- package/template/lint-scripts/check-route-auth-complete.js +260 -0
- package/template/lint-scripts/check-route-auth.js +212 -0
- package/template/lint-scripts/check-route-permissions.js +210 -0
- package/template/lint-scripts/config/project.config.ts +358 -0
- package/template/lint-scripts/dev-context-analyzer.ts +707 -0
- package/template/lint-scripts/dev-guide-generator.ts +830 -0
- package/template/lint-scripts/dev-server.ts +58 -0
- package/template/lint-scripts/eslint-route-auth.js +158 -0
- package/template/lint-scripts/eslint-routes-permission.js +147 -0
- package/template/lint-scripts/fix-route-auth.js +245 -0
- package/template/lint-scripts/framework/check-modify.ts +152 -0
- package/template/lint-scripts/framework/hash-utils.ts +180 -0
- package/template/lint-scripts/framework/init-baseline.ts +84 -0
- package/template/lint-scripts/framework/update-baseline.ts +148 -0
- package/template/lint-scripts/post-commit-track.ts +66 -0
- package/template/lint-scripts/quick-test.sh +27 -0
- package/template/lint-scripts/smart-test.ts +268 -0
- package/template/lint-scripts/test-history.ts +360 -0
- package/template/lint-scripts/test-tracker.ts +350 -0
- package/template/lint-scripts/validate-all.ts +250 -0
- package/template/lint-scripts/validators/api-coverage.validator.ts +355 -0
- package/template/lint-scripts/validators/client-rpc.validator.ts +190 -0
- package/template/lint-scripts/validators/client-tests.validator.ts +155 -0
- package/template/lint-scripts/validators/console-log.validator.ts +122 -0
- package/template/lint-scripts/validators/directory-structure.validator.ts +181 -0
- package/template/lint-scripts/validators/imports.validator.ts +180 -0
- package/template/lint-scripts/validators/index.ts +271 -0
- package/template/lint-scripts/validators/md-refs.validator.ts +165 -0
- package/template/lint-scripts/validators/module-tests.validator.ts +112 -0
- package/template/lint-scripts/validators/sensitive.validator.ts +185 -0
- package/template/lint-scripts/validators/server-rpc.validator.ts +141 -0
- package/template/lint-scripts/validators/test-quality.validator.ts +257 -0
- package/template/lint-scripts/validators/todos.validator.ts +126 -0
- package/template/lint-scripts/watch-validator.ts +131 -0
- package/template/modules.config.ts +71 -0
- package/template/package-lock.json +14554 -0
- package/template/package.json +158 -0
- package/template/patches/@hono+zod-openapi+1.2.2.patch +67 -0
- package/template/patches/@hono+zod-validator+0.7.6.patch +13 -0
- package/template/patches/hono+4.12.16.patch +246 -0
- package/template/patches/typescript+5.8.3.patch +26 -0
- package/template/playwright.config.ts +60 -0
- package/template/pnpm-lock.yaml +7137 -0
- package/template/postcss.config.js +6 -0
- package/template/scripts/create-template.ts +1724 -0
- package/template/src/admin/App.tsx +59 -0
- package/template/src/admin/components/AccountSwitcher.tsx +104 -0
- package/template/src/admin/components/CaptchaModal.tsx +150 -0
- package/template/src/admin/components/NotificationDrawer.tsx +213 -0
- package/template/src/admin/components/PageHeader.tsx +45 -0
- package/template/src/admin/components/PermissionConfigEditor.tsx +237 -0
- package/template/src/admin/components/PermissionGuard.tsx +108 -0
- package/template/src/admin/components/PermissionTree.tsx +193 -0
- package/template/src/admin/components/ProtectedRoute.tsx +33 -0
- package/template/src/admin/components/StatsCard.tsx +44 -0
- package/template/src/admin/components/UserFormModal.tsx +67 -0
- package/template/src/admin/components/UserTable.tsx +72 -0
- package/template/src/admin/components/__tests__/PageHeader.test.tsx +48 -0
- package/template/src/admin/components/__tests__/PermissionConfigEditor.test.tsx +147 -0
- package/template/src/admin/components/__tests__/PermissionGuard.test.tsx +221 -0
- package/template/src/admin/components/__tests__/PermissionTree.test.tsx +187 -0
- package/template/src/admin/components/__tests__/ProtectedRoute.test.tsx +129 -0
- package/template/src/admin/components/__tests__/StatsCard.test.tsx +44 -0
- package/template/src/admin/components/__tests__/UserFormModal.test.tsx +125 -0
- package/template/src/admin/components/__tests__/UserTable.test.tsx +89 -0
- package/template/src/admin/components/index.ts +7 -0
- package/template/src/admin/hooks/__tests__/useAuditLogs.test.ts +109 -0
- package/template/src/admin/hooks/__tests__/usePermissions.test.tsx +327 -0
- package/template/src/admin/hooks/__tests__/usePermissionsBranches.test.ts +198 -0
- package/template/src/admin/hooks/__tests__/useRoles.test.ts +261 -0
- package/template/src/admin/hooks/useAdminNotifications.ts +156 -0
- package/template/src/admin/hooks/useAuditLogs.ts +44 -0
- package/template/src/admin/hooks/useConfig.ts +168 -0
- package/template/src/admin/hooks/usePermissions.ts +156 -0
- package/template/src/admin/hooks/useRoles.ts +120 -0
- package/template/src/admin/layouts/Header.tsx +93 -0
- package/template/src/admin/layouts/Layout.tsx +23 -0
- package/template/src/admin/layouts/Sidebar.tsx +120 -0
- package/template/src/admin/main.tsx +18 -0
- package/template/src/admin/pages/ContentPage.tsx +252 -0
- package/template/src/admin/pages/DashboardPage.tsx +141 -0
- package/template/src/admin/pages/DisputesPage.tsx +261 -0
- package/template/src/admin/pages/LoginPage.tsx +121 -0
- package/template/src/admin/pages/MediaTestPage.tsx +524 -0
- package/template/src/admin/pages/OrdersPage.tsx +302 -0
- package/template/src/admin/pages/PermissionsPage.tsx +95 -0
- package/template/src/admin/pages/RegisterPage.tsx +103 -0
- package/template/src/admin/pages/RolesPage.tsx +330 -0
- package/template/src/admin/pages/SettingsPage.tsx +73 -0
- package/template/src/admin/pages/SystemLogsPage.tsx +218 -0
- package/template/src/admin/pages/TestCaptchaPage.tsx +120 -0
- package/template/src/admin/pages/TicketsPage.tsx +292 -0
- package/template/src/admin/pages/UsersPage.tsx +282 -0
- package/template/src/admin/pages/__tests__/ContentPage.test.tsx +213 -0
- package/template/src/admin/pages/__tests__/DashboardPage.test.tsx +153 -0
- package/template/src/admin/pages/__tests__/DisputesPage.test.tsx +196 -0
- package/template/src/admin/pages/__tests__/LoginPage.test.tsx +112 -0
- package/template/src/admin/pages/__tests__/OrdersPage.test.tsx +217 -0
- package/template/src/admin/pages/__tests__/PermissionsPage.test.tsx +119 -0
- package/template/src/admin/pages/__tests__/RegisterPage.test.tsx +179 -0
- package/template/src/admin/pages/__tests__/RolesPage.test.tsx +256 -0
- package/template/src/admin/pages/__tests__/SettingsPage.test.tsx +66 -0
- package/template/src/admin/pages/__tests__/SystemLogsPage.test.tsx +125 -0
- package/template/src/admin/pages/__tests__/TicketsPage.test.tsx +202 -0
- package/template/src/admin/pages/__tests__/UsersPage.test.tsx +229 -0
- package/template/src/admin/services/README.md +263 -0
- package/template/src/admin/services/TESTING.md +323 -0
- package/template/src/admin/services/__tests__/skipLoading.test.ts +146 -0
- package/template/src/admin/services/apiClient.ts +66 -0
- package/template/src/admin/services/requestInterceptor.ts +127 -0
- package/template/src/admin/services/types.ts +5 -0
- package/template/src/admin/stores/__tests__/adminStore.test.ts +184 -0
- package/template/src/admin/stores/__tests__/adminStoreBranches.test.ts +148 -0
- package/template/src/admin/stores/__tests__/captchaStore.test.ts +75 -0
- package/template/src/admin/stores/__tests__/captchaStoreBranches.test.ts +37 -0
- package/template/src/admin/stores/__tests__/loadingStore.test.ts +68 -0
- package/template/src/admin/stores/adminStore.ts +88 -0
- package/template/src/admin/stores/captchaStore.ts +47 -0
- package/template/src/admin/stores/loadingStore.ts +27 -0
- package/template/src/cli/index.ts +22 -0
- package/template/src/cli/modules/config/index.ts +132 -0
- package/template/src/cli/modules/index.ts +12 -0
- package/template/src/cli/modules/notification/index.ts +86 -0
- package/template/src/cli/modules/todo/index.ts +60 -0
- package/template/src/cli/rpc/client.ts +23 -0
- package/template/src/cli/rpc/index.ts +1 -0
- package/template/src/cli/utils/api.ts +17 -0
- package/template/src/cli/utils/auto-command.ts +229 -0
- package/template/src/cli/utils/index.ts +8 -0
- package/template/src/cli/utils/logger.ts +79 -0
- package/template/src/client/App.tsx +24 -0
- package/template/src/client/Layout.tsx +27 -0
- package/template/src/client/components/AuthButton.tsx +41 -0
- package/template/src/client/components/ConnectionStatus.tsx +75 -0
- package/template/src/client/components/EmptyState.tsx +26 -0
- package/template/src/client/components/Footer.tsx +17 -0
- package/template/src/client/components/LoadingSpinner.tsx +26 -0
- package/template/src/client/components/MessageCard.tsx +61 -0
- package/template/src/client/components/Navigation.tsx +65 -0
- package/template/src/client/components/StatusBadge.tsx +50 -0
- package/template/src/client/components/__tests__/App.test.tsx +79 -0
- package/template/src/client/components/__tests__/AuthButton.test.tsx +119 -0
- package/template/src/client/components/__tests__/ConnectionStatus.test.tsx +188 -0
- package/template/src/client/components/__tests__/EmptyState.test.tsx +70 -0
- package/template/src/client/components/__tests__/Footer.test.tsx +22 -0
- package/template/src/client/components/__tests__/LoadingSpinner.test.tsx +75 -0
- package/template/src/client/components/__tests__/MessageCard.test.tsx +142 -0
- package/template/src/client/components/__tests__/Navigation.test.tsx +40 -0
- package/template/src/client/components/__tests__/StatusBadge.test.tsx +107 -0
- package/template/src/client/components/index.ts +6 -0
- package/template/src/client/index.css +27 -0
- package/template/src/client/main.tsx +25 -0
- package/template/src/client/pages/ContentDetailPage.tsx +142 -0
- package/template/src/client/pages/ContentListPage.tsx +169 -0
- package/template/src/client/pages/NotificationPage.tsx +288 -0
- package/template/src/client/pages/TodoPage.tsx +389 -0
- package/template/src/client/pages/WebSocketPage.tsx +239 -0
- package/template/src/client/pages/__tests__/ContentDetailPage.test.tsx +176 -0
- package/template/src/client/pages/__tests__/ContentListPage.test.tsx +202 -0
- package/template/src/client/pages/__tests__/NotificationPage.test.tsx +246 -0
- package/template/src/client/pages/__tests__/TodoPage.test.tsx +433 -0
- package/template/src/client/pages/__tests__/WebSocketPage.test.tsx +378 -0
- package/template/src/client/services/apiClient.ts +66 -0
- package/template/src/client/stores/__tests__/authStore.test.ts +139 -0
- package/template/src/client/stores/__tests__/chatWSStore.test.ts +416 -0
- package/template/src/client/stores/__tests__/notificationStore.test.ts +453 -0
- package/template/src/client/stores/__tests__/todoStore.test.ts +509 -0
- package/template/src/client/stores/authStore.ts +51 -0
- package/template/src/client/stores/chatWSStore.ts +122 -0
- package/template/src/client/stores/notificationStore.ts +199 -0
- package/template/src/client/stores/todoStore.ts +207 -0
- package/template/src/server/__tests__/integration/todos-api.test.ts +138 -0
- package/template/src/server/app.ts +105 -0
- package/template/src/server/config.ts +86 -0
- package/template/src/server/core/__tests__/realtime-core.test.ts +329 -0
- package/template/src/server/core/__tests__/realtime-scanner.test.ts +210 -0
- package/template/src/server/core/__tests__/runtime-node.test.ts +330 -0
- package/template/src/server/core/__tests__/runtime.test.ts +219 -0
- package/template/src/server/core/__tests__/typed-runtime.test.ts +178 -0
- package/template/src/server/core/durable-objects/RealtimeDO.ts +206 -0
- package/template/src/server/core/index.ts +83 -0
- package/template/src/server/core/module-loader.ts +281 -0
- package/template/src/server/core/realtime-core.ts +152 -0
- package/template/src/server/core/realtime-scanner.ts +99 -0
- package/template/src/server/core/runtime-cloudflare.ts +207 -0
- package/template/src/server/core/runtime-node.ts +170 -0
- package/template/src/server/core/runtime.ts +129 -0
- package/template/src/server/core/typed-runtime.ts +80 -0
- package/template/src/server/db/__tests__/driver.test.ts +203 -0
- package/template/src/server/db/config.ts +1 -0
- package/template/src/server/db/driver-cloudflare.ts +50 -0
- package/template/src/server/db/driver.ts +116 -0
- package/template/src/server/db/index.ts +3 -0
- package/template/src/server/db/init.ts +412 -0
- package/template/src/server/db/schema/api-endpoints.ts +23 -0
- package/template/src/server/db/schema/contents.ts +31 -0
- package/template/src/server/db/schema/disputes.ts +39 -0
- package/template/src/server/db/schema/index.ts +14 -0
- package/template/src/server/db/schema/notifications.ts +16 -0
- package/template/src/server/db/schema/orders.ts +30 -0
- package/template/src/server/db/schema/permission-audit-logs.ts +17 -0
- package/template/src/server/db/schema/permission-route-mappings.ts +22 -0
- package/template/src/server/db/schema/permissions.ts +17 -0
- package/template/src/server/db/schema/role-permissions.ts +22 -0
- package/template/src/server/db/schema/roles.ts +17 -0
- package/template/src/server/db/schema/tickets.ts +62 -0
- package/template/src/server/db/schema/todo-attachments.ts +22 -0
- package/template/src/server/db/schema/todos.ts +21 -0
- package/template/src/server/db/schema/user-roles.ts +17 -0
- package/template/src/server/db/seeds/index.ts +1 -0
- package/template/src/server/db/seeds/permission-data.ts +354 -0
- package/template/src/server/db/test-setup.ts +647 -0
- package/template/src/server/entries/cloudflare.ts +71 -0
- package/template/src/server/entries/node.ts +190 -0
- package/template/src/server/index.ts +62 -0
- package/template/src/server/middleware/__tests__/auth-simple.test.ts +147 -0
- package/template/src/server/middleware/__tests__/auth.test.ts +276 -0
- package/template/src/server/middleware/__tests__/captcha.test.ts +258 -0
- package/template/src/server/middleware/__tests__/error-handler.test.ts +364 -0
- package/template/src/server/middleware/__tests__/error-response-format.test.ts +80 -0
- package/template/src/server/middleware/__tests__/permission.test.ts +312 -0
- package/template/src/server/middleware/audit-log.ts +101 -0
- package/template/src/server/middleware/auth.ts +235 -0
- package/template/src/server/middleware/captcha.ts +166 -0
- package/template/src/server/middleware/cors.ts +30 -0
- package/template/src/server/middleware/error-handler.ts +135 -0
- package/template/src/server/middleware/index.ts +24 -0
- package/template/src/server/middleware/logger.ts +73 -0
- package/template/src/server/middleware/permission.ts +127 -0
- package/template/src/server/middleware/rate-limit.ts +69 -0
- package/template/src/server/middleware/realtime-env.ts +12 -0
- package/template/src/server/module-admin/__tests__/admin-routes.test.ts +360 -0
- package/template/src/server/module-admin/__tests__/admin-service.test.ts +222 -0
- package/template/src/server/module-admin/__tests__/audit-log-routes.test.ts +261 -0
- package/template/src/server/module-admin/__tests__/export-routes.test.ts +160 -0
- package/template/src/server/module-admin/__tests__/media-routes.test.ts +122 -0
- package/template/src/server/module-admin/module.ts +40 -0
- package/template/src/server/module-admin/routes/admin-notification-routes.ts +197 -0
- package/template/src/server/module-admin/routes/admin-routes.ts +20 -0
- package/template/src/server/module-admin/routes/auth-routes.ts +86 -0
- package/template/src/server/module-admin/routes/export-routes.ts +207 -0
- package/template/src/server/module-admin/routes/media-routes.ts +78 -0
- package/template/src/server/module-admin/routes/system-routes.ts +89 -0
- package/template/src/server/module-admin/routes/user-management-routes.ts +143 -0
- package/template/src/server/module-admin/services/admin-service.ts +268 -0
- package/template/src/server/module-captcha/__tests__/captcha-route.test.ts +35 -0
- package/template/src/server/module-captcha/__tests__/captcha-service.test.ts +53 -0
- package/template/src/server/module-captcha/module.ts +35 -0
- package/template/src/server/module-captcha/routes/captcha-routes.ts +68 -0
- package/template/src/server/module-captcha/services/captcha-service.ts +3 -0
- package/template/src/server/module-chat/__tests__/chat-routes.test.ts +39 -0
- package/template/src/server/module-chat/__tests__/chat-rpc.test.ts +185 -0
- package/template/src/server/module-chat/__tests__/chat-service.test.ts +64 -0
- package/template/src/server/module-chat/module.ts +27 -0
- package/template/src/server/module-chat/routes/chat-routes.ts +46 -0
- package/template/src/server/module-chat/services/chat-service.ts +38 -0
- package/template/src/server/module-content/__tests__/content-route.test.ts +40 -0
- package/template/src/server/module-content/__tests__/content-service.test.ts +107 -0
- package/template/src/server/module-content/module.ts +39 -0
- package/template/src/server/module-content/routes/content-routes.ts +182 -0
- package/template/src/server/module-content/routes/public-content-routes.ts +58 -0
- package/template/src/server/module-content/services/content-service.ts +224 -0
- package/template/src/server/module-dispute/__tests__/dispute-route.test.ts +43 -0
- package/template/src/server/module-dispute/__tests__/dispute-service.test.ts +135 -0
- package/template/src/server/module-dispute/module.ts +30 -0
- package/template/src/server/module-dispute/routes/dispute-routes.ts +153 -0
- package/template/src/server/module-dispute/services/dispute-service.ts +255 -0
- package/template/src/server/module-file/__tests__/file-routes.test.ts +284 -0
- package/template/src/server/module-file/__tests__/file-storage-service.test.ts +323 -0
- package/template/src/server/module-file/module.ts +28 -0
- package/template/src/server/module-file/routes/file-routes.ts +288 -0
- package/template/src/server/module-notifications/__tests__/notification-route-rpc.test.ts +375 -0
- package/template/src/server/module-notifications/__tests__/notification-service.test.ts +241 -0
- package/template/src/server/module-notifications/__tests__/sse-rpc.test.ts +289 -0
- package/template/src/server/module-notifications/module.ts +32 -0
- package/template/src/server/module-notifications/routes/notification-routes.ts +190 -0
- package/template/src/server/module-notifications/services/notification-service.ts +104 -0
- package/template/src/server/module-order/__tests__/order-route.test.ts +258 -0
- package/template/src/server/module-order/__tests__/order-service.test.ts +222 -0
- package/template/src/server/module-order/module.ts +30 -0
- package/template/src/server/module-order/routes/order-routes.ts +202 -0
- package/template/src/server/module-order/services/order-service.ts +268 -0
- package/template/src/server/module-permission/__tests__/permission-middleware.test.ts +90 -0
- package/template/src/server/module-permission/__tests__/permission-routes.test.ts +330 -0
- package/template/src/server/module-permission/__tests__/permission-service-impl.test.ts +388 -0
- package/template/src/server/module-permission/__tests__/permission-service.test.ts +190 -0
- package/template/src/server/module-permission/__tests__/role-routes.test.ts +215 -0
- package/template/src/server/module-permission/module.ts +68 -0
- package/template/src/server/module-permission/routes/audit-log-routes.ts +76 -0
- package/template/src/server/module-permission/routes/permission-routes.ts +184 -0
- package/template/src/server/module-permission/routes/role-routes.ts +263 -0
- package/template/src/server/module-permission/services/audit-log-service.ts +108 -0
- package/template/src/server/module-permission/services/permission-service-impl.ts +233 -0
- package/template/src/server/module-permission/services/permission-service.ts +256 -0
- package/template/src/server/module-permission/services/role-service.ts +122 -0
- package/template/src/server/module-ticket/__tests__/ticket-route.test.ts +173 -0
- package/template/src/server/module-ticket/__tests__/ticket-service.test.ts +201 -0
- package/template/src/server/module-ticket/module.ts +30 -0
- package/template/src/server/module-ticket/routes/ticket-routes.ts +210 -0
- package/template/src/server/module-ticket/services/ticket-service.ts +297 -0
- package/template/src/server/module-todos/__tests__/todo-service.test.ts +196 -0
- package/template/src/server/module-todos/__tests__/todos-file-upload.test.ts +439 -0
- package/template/src/server/module-todos/__tests__/todos-route-rpc.test.ts +575 -0
- package/template/src/server/module-todos/module.ts +30 -0
- package/template/src/server/module-todos/routes/todos-routes.ts +260 -0
- package/template/src/server/module-todos/services/todo-service.ts +202 -0
- package/template/src/server/route-registry.ts +47 -0
- package/template/src/server/test-utils/test-client.ts +66 -0
- package/template/src/server/test-utils/test-server.ts +131 -0
- package/template/src/server/types/bindings.ts +10 -0
- package/template/src/server/utils/__tests__/app-error.test.ts +374 -0
- package/template/src/server/utils/__tests__/auth.test.ts +76 -0
- package/template/src/server/utils/__tests__/captcha.test.ts +80 -0
- package/template/src/server/utils/__tests__/file-storage.test.ts +472 -0
- package/template/src/server/utils/__tests__/logger.test.ts +221 -0
- package/template/src/server/utils/__tests__/route-helpers.test.ts +264 -0
- package/template/src/server/utils/app-error.ts +355 -0
- package/template/src/server/utils/auth.ts +65 -0
- package/template/src/server/utils/captcha.ts +119 -0
- package/template/src/server/utils/date.ts +50 -0
- package/template/src/server/utils/env.ts +4 -0
- package/template/src/server/utils/file-storage.ts +462 -0
- package/template/src/server/utils/generate.ts +26 -0
- package/template/src/server/utils/id-helpers.ts +5 -0
- package/template/src/server/utils/logger.ts +139 -0
- package/template/src/server/utils/permission-utils.ts +6 -0
- package/template/src/server/utils/response.ts +40 -0
- package/template/src/server/utils/route-helpers.ts +200 -0
- package/template/src/server/utils/uuid.ts +8 -0
- package/template/src/server/utils/ws-helper.ts +30 -0
- package/template/src/shared/constants/index.ts +1 -0
- package/template/src/shared/constants/resource-types.ts +72 -0
- package/template/src/shared/core/__tests__/sse-client.test.ts +376 -0
- package/template/src/shared/core/__tests__/ws-client.test.ts +468 -0
- package/template/src/shared/core/api-request.ts +192 -0
- package/template/src/shared/core/api-schemas.ts +41 -0
- package/template/src/shared/core/index.ts +37 -0
- package/template/src/shared/core/module-manifest.ts +120 -0
- package/template/src/shared/core/protocol-types.ts +26 -0
- package/template/src/shared/core/sse-client.ts +185 -0
- package/template/src/shared/core/ws-client.ts +236 -0
- package/template/src/shared/hooks/index.ts +2 -0
- package/template/src/shared/hooks/useSSE.ts +57 -0
- package/template/src/shared/hooks/useWebSocket.ts +66 -0
- package/template/src/shared/index.ts +5 -0
- package/template/src/shared/modules/admin/index.ts +1 -0
- package/template/src/shared/modules/admin/schemas.ts +100 -0
- package/template/src/shared/modules/audit/index.ts +1 -0
- package/template/src/shared/modules/audit/schemas.ts +24 -0
- package/template/src/shared/modules/captcha/index.ts +1 -0
- package/template/src/shared/modules/captcha/schemas.ts +19 -0
- package/template/src/shared/modules/chat/index.ts +38 -0
- package/template/src/shared/modules/content/index.ts +1 -0
- package/template/src/shared/modules/content/schemas.ts +53 -0
- package/template/src/shared/modules/dispute/index.ts +1 -0
- package/template/src/shared/modules/dispute/schemas.ts +62 -0
- package/template/src/shared/modules/files/index.ts +11 -0
- package/template/src/shared/modules/files/schemas.ts +44 -0
- package/template/src/shared/modules/index.ts +70 -0
- package/template/src/shared/modules/notifications/index.ts +26 -0
- package/template/src/shared/modules/notifications/schemas.ts +73 -0
- package/template/src/shared/modules/order/index.ts +1 -0
- package/template/src/shared/modules/order/schemas.ts +63 -0
- package/template/src/shared/modules/permission/index.ts +42 -0
- package/template/src/shared/modules/permission/permission-dependencies.ts +50 -0
- package/template/src/shared/modules/permission/permissions.ts +275 -0
- package/template/src/shared/modules/permission/schemas.ts +92 -0
- package/template/src/shared/modules/permission/types.ts +23 -0
- package/template/src/shared/modules/role/index.ts +1 -0
- package/template/src/shared/modules/role/schemas.ts +46 -0
- package/template/src/shared/modules/ticket/index.ts +1 -0
- package/template/src/shared/modules/ticket/schemas.ts +77 -0
- package/template/src/shared/modules/todos/index.ts +20 -0
- package/template/src/shared/modules/todos/schemas.ts +65 -0
- package/template/src/shared/schemas/index.ts +79 -0
- package/template/src/types/global.d.ts +14 -0
- package/template/src/vite-env.d.ts +21 -0
- package/template/tailwind.config.js +11 -0
- package/template/tests/e2e/admin-crud.spec.ts +81 -0
- package/template/tests/e2e/case-o1.spec.ts +108 -0
- package/template/tests/e2e/global-setup.ts +73 -0
- package/template/tests/e2e/global-teardown.ts +28 -0
- package/template/tests/e2e/notification.spec.ts +458 -0
- package/template/tests/e2e/todo.spec.ts +623 -0
- package/template/tests/e2e/websocket.spec.ts +254 -0
- package/template/tsconfig.json +35 -0
- package/template/tsup.config.ts +95 -0
- package/template/vite-plugins.ts +113 -0
- package/template/vite.config.ts +96 -0
- package/template/vitest.config.ts +53 -0
- package/template/vitest.integration.config.ts +24 -0
- package/template/vitest.integration.setup.ts +22 -0
- package/template/vitest.setup.ts +74 -0
- package/template/wrangler.toml +33 -0
|
@@ -0,0 +1,1724 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模块模板生成器
|
|
3
|
+
*
|
|
4
|
+
* 用法:
|
|
5
|
+
* npm run create:module <name> [options]
|
|
6
|
+
*
|
|
7
|
+
* 选项:
|
|
8
|
+
* --with-db 包含数据库操作
|
|
9
|
+
* --sse SSE (Server-Sent Events) 模板
|
|
10
|
+
* --ws WebSocket 模板
|
|
11
|
+
* --admin 创建管理后台模块 (module-admin-xxx)
|
|
12
|
+
*
|
|
13
|
+
* 示例:
|
|
14
|
+
* npm run create:module product # 基础模板(无数据库)
|
|
15
|
+
* npm run create:module product --with-db # 数据库模板
|
|
16
|
+
* npm run create:module notifications --sse # SSE 模板
|
|
17
|
+
* npm run create:module chat --ws # WebSocket 模板
|
|
18
|
+
* npm run create:module users --admin # 管理后台模块 (module-admin-users)
|
|
19
|
+
* npm run create:module reports --admin --with-db # 管理后台数据库模块
|
|
20
|
+
*
|
|
21
|
+
* 自动操作:
|
|
22
|
+
* 1. 创建模块目录结构
|
|
23
|
+
* 2. 创建 schema 文件 (shared/modules/{name}/schemas.ts)
|
|
24
|
+
* 3. 更新 app.ts 导入和注册路由
|
|
25
|
+
* 4. 更新 shared/modules/index.ts 导出
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import fs from 'fs'
|
|
29
|
+
import path from 'path'
|
|
30
|
+
import { fileURLToPath } from 'url'
|
|
31
|
+
|
|
32
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
33
|
+
const __dirname = path.dirname(__filename)
|
|
34
|
+
const templateDir = path.resolve(__dirname, '..')
|
|
35
|
+
|
|
36
|
+
interface CreateOptions {
|
|
37
|
+
name: string
|
|
38
|
+
withDatabase: boolean
|
|
39
|
+
withSSE: boolean
|
|
40
|
+
withWebSocket: boolean
|
|
41
|
+
prefix: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface ModifiedFileResult {
|
|
45
|
+
success: boolean
|
|
46
|
+
diff?: string
|
|
47
|
+
addedLines?: number
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface CreatedFile {
|
|
51
|
+
path: string
|
|
52
|
+
type: 'created' | 'modified'
|
|
53
|
+
diff?: string
|
|
54
|
+
lineCount?: number
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function generateClientUsageExample(name: string, options: CreateOptions): string {
|
|
58
|
+
const pascalName = toPascalCase(name)
|
|
59
|
+
const camelName = toCamelCase(name)
|
|
60
|
+
const kebabName = toKebabCase(name)
|
|
61
|
+
|
|
62
|
+
if (options.withSSE) {
|
|
63
|
+
return `
|
|
64
|
+
// 📡 SSE 客户端使用示例
|
|
65
|
+
|
|
66
|
+
// 1️⃣ React Hook 方式(推荐)
|
|
67
|
+
import { useSSE } from '@shared/hooks'
|
|
68
|
+
import { apiClient } from '@client/services/apiClient'
|
|
69
|
+
import type { ${pascalName}SSEProtocol, ${pascalName}Event } from '@shared/modules/${kebabName}'
|
|
70
|
+
|
|
71
|
+
function ${pascalName}Component() {
|
|
72
|
+
const { status, connect, disconnect, client } = useSSE<${pascalName}SSEProtocol>(
|
|
73
|
+
() => apiClient.api['${kebabName}s'].stream.$sse()
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
connect()
|
|
78
|
+
return () => disconnect()
|
|
79
|
+
}, [connect, disconnect])
|
|
80
|
+
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
if (!client) return
|
|
83
|
+
|
|
84
|
+
// 🎯 类型安全的事件监听
|
|
85
|
+
const unsubscribe = client.on('message', (event: ${pascalName}Event) => {
|
|
86
|
+
console.log('收到事件:', event)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
return unsubscribe
|
|
90
|
+
}, [client])
|
|
91
|
+
|
|
92
|
+
return <div>SSE 状态: {status}</div>
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 2️⃣ 直接使用 apiClient
|
|
96
|
+
import { apiClient } from '@client/services/apiClient'
|
|
97
|
+
|
|
98
|
+
// ========== 基础 SSE 连接 ==========
|
|
99
|
+
|
|
100
|
+
const sseClient = apiClient.api['${kebabName}s'].stream.$sse()
|
|
101
|
+
|
|
102
|
+
// 监听连接状态
|
|
103
|
+
sseClient.onStatusChange(status => {
|
|
104
|
+
console.log('SSE 状态:', status)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
// 监听事件(带类型推导)
|
|
108
|
+
sseClient.on('message', (event: ${pascalName}Event) => {
|
|
109
|
+
console.log('收到事件:', event)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
// 关闭连接
|
|
113
|
+
sseClient.abort()
|
|
114
|
+
`
|
|
115
|
+
} else if (options.withWebSocket) {
|
|
116
|
+
return `
|
|
117
|
+
// 🔌 WebSocket 客户端使用示例
|
|
118
|
+
|
|
119
|
+
// 1️⃣ React Hook 方式(推荐)
|
|
120
|
+
import { useWebSocket } from '@shared/hooks'
|
|
121
|
+
import { apiClient } from '@client/services/apiClient'
|
|
122
|
+
import type { ${pascalName}Message } from '@shared/modules/${kebabName}'
|
|
123
|
+
|
|
124
|
+
function ${pascalName}Component() {
|
|
125
|
+
const { status, connect, disconnect, call, emit, on } = useWebSocket(
|
|
126
|
+
apiClient.api['${kebabName}s'].ws
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
connect()
|
|
131
|
+
return () => disconnect()
|
|
132
|
+
}, [connect, disconnect])
|
|
133
|
+
|
|
134
|
+
useEffect(() => {
|
|
135
|
+
// 🎯 类型安全的事件监听 - 根据 type 自动推导 payload 类型
|
|
136
|
+
const unsubscribe = on('message', (message: ${pascalName}Message) => {
|
|
137
|
+
// TypeScript 能够根据 message.type 自动推导 payload 类型
|
|
138
|
+
switch (message.type) {
|
|
139
|
+
case 'chat':
|
|
140
|
+
// message.payload 类型自动推导为 ChatPayload
|
|
141
|
+
console.log('聊天消息:', message.payload)
|
|
142
|
+
break
|
|
143
|
+
case 'notification':
|
|
144
|
+
// message.payload 类型自动推导为 NotificationPayload
|
|
145
|
+
console.log('通知:', message.payload)
|
|
146
|
+
break
|
|
147
|
+
case 'system':
|
|
148
|
+
// message.payload 类型自动推导为 SystemPayload
|
|
149
|
+
console.log('系统消息:', message.payload)
|
|
150
|
+
break
|
|
151
|
+
}
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
return unsubscribe
|
|
155
|
+
}, [on])
|
|
156
|
+
|
|
157
|
+
// 🎯 类型安全的 RPC 调用
|
|
158
|
+
const handleEcho = async () => {
|
|
159
|
+
// 参数和返回值都有类型检查
|
|
160
|
+
const result = await call('echo', { message: 'Hello' })
|
|
161
|
+
console.log('Echo 结果:', result)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// 🎯 类型安全的事件发送
|
|
165
|
+
const handleSend = () => {
|
|
166
|
+
emit('message', { id: '1', type: 'chat', payload: { text: 'Hello' } })
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<div>
|
|
171
|
+
<p>WebSocket 状态: {status}</p>
|
|
172
|
+
<button onClick={handleEcho}>Echo</button>
|
|
173
|
+
<button onClick={handleSend}>发送消息</button>
|
|
174
|
+
</div>
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 2️⃣ 直接使用 apiClient
|
|
179
|
+
import { apiClient } from '@client/services/apiClient'
|
|
180
|
+
|
|
181
|
+
// ========== 基础 WebSocket 连接 ==========
|
|
182
|
+
|
|
183
|
+
const wsClient = apiClient.api['${kebabName}s'].ws.$ws()
|
|
184
|
+
|
|
185
|
+
// 监听连接状态
|
|
186
|
+
wsClient.onStatusChange(status => {
|
|
187
|
+
console.log('WebSocket 状态:', status)
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
// 发送消息(带类型检查)
|
|
191
|
+
wsClient.emit('message', { id: '1', type: 'chat', payload: { text: 'Hello' } })
|
|
192
|
+
|
|
193
|
+
// 监听消息(带类型推导)
|
|
194
|
+
wsClient.on('message', (message: ${pascalName}Message) => {
|
|
195
|
+
if (message.type === 'chat') {
|
|
196
|
+
// TypeScript 知道 message.payload 是 ChatPayload 类型
|
|
197
|
+
console.log('聊天:', message.payload)
|
|
198
|
+
}
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
// RPC 调用(带类型检查)
|
|
202
|
+
const result = await wsClient.call('echo', { message: 'Hello' })
|
|
203
|
+
|
|
204
|
+
// 关闭连接
|
|
205
|
+
wsClient.close()
|
|
206
|
+
|
|
207
|
+
// ========== 🔐 带认证的 WebSocket 连接 ==========
|
|
208
|
+
|
|
209
|
+
// 方式 1: 使用 header 选项传递认证 token
|
|
210
|
+
const authWsClient = apiClient.api['${kebabName}s'].ws.$ws({
|
|
211
|
+
header: {
|
|
212
|
+
Authorization: 'Bearer your-token-here',
|
|
213
|
+
},
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
// 方式 2: 在创建 apiClient 时配置默认 headers
|
|
217
|
+
// 参考 src/client/services/apiClient.ts
|
|
218
|
+
|
|
219
|
+
// ========== 🛡️ 需要登录的 WebSocket 路由示例 ==========
|
|
220
|
+
|
|
221
|
+
// 假设路由配置了 authMiddleware 中间件:
|
|
222
|
+
// middleware: [authMiddleware()]
|
|
223
|
+
|
|
224
|
+
// 带认证的 WebSocket 连接会自动验证 token
|
|
225
|
+
// - 连接时验证 token
|
|
226
|
+
// - 无效 token 会导致连接被拒绝
|
|
227
|
+
|
|
228
|
+
// 示例:聊天室 WebSocket(需要登录)
|
|
229
|
+
const chatWs = apiClient.api.chat.ws.$ws({
|
|
230
|
+
header: {
|
|
231
|
+
Authorization: 'Bearer user-token',
|
|
232
|
+
},
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
chatWs.on('message', (message) => {
|
|
236
|
+
console.log('收到消息:', message)
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
chatWs.emit('message', { id: '1', type: 'chat', payload: { text: 'Hello' } })
|
|
240
|
+
`
|
|
241
|
+
} else {
|
|
242
|
+
return `
|
|
243
|
+
// 📦 REST API 客户端使用示例
|
|
244
|
+
|
|
245
|
+
// 1️⃣ React Hook 方式(推荐使用 SWR 或 React Query)
|
|
246
|
+
import useSWR from 'swr'
|
|
247
|
+
import { apiClient } from '@client/services/apiClient'
|
|
248
|
+
import type { ${pascalName}, Create${pascalName}Input, Update${pascalName}Input } from '@shared/modules/${kebabName}'
|
|
249
|
+
|
|
250
|
+
function ${pascalName}Component() {
|
|
251
|
+
// 🎯 类型安全的列表查询
|
|
252
|
+
const { data: listData, mutate } = useSWR(
|
|
253
|
+
'${kebabName}s-list',
|
|
254
|
+
async () => {
|
|
255
|
+
const res = await apiClient.api['${kebabName}s'].$get()
|
|
256
|
+
const data = await res.json()
|
|
257
|
+
return data.success ? data.data : []
|
|
258
|
+
}
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
// 🎯 类型安全的创建操作
|
|
262
|
+
const handleCreate = async (input: Create${pascalName}Input) => {
|
|
263
|
+
const res = await apiClient.api['${kebabName}s'].$post({ json: input })
|
|
264
|
+
const data = await res.json()
|
|
265
|
+
if (data.success) {
|
|
266
|
+
mutate() // 刷新列表
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// 🎯 类型安全的更新操作
|
|
271
|
+
const handleUpdate = async (id: string, input: Update${pascalName}Input) => {
|
|
272
|
+
const res = await apiClient.api['${kebabName}s'][':id'].$put({
|
|
273
|
+
param: { id },
|
|
274
|
+
json: input,
|
|
275
|
+
})
|
|
276
|
+
const data = await res.json()
|
|
277
|
+
if (data.success) {
|
|
278
|
+
mutate()
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// 🎯 类型安全的删除操作
|
|
283
|
+
const handleDelete = async (id: string) => {
|
|
284
|
+
const res = await apiClient.api['${kebabName}s'][':id'].$delete({
|
|
285
|
+
param: { id },
|
|
286
|
+
})
|
|
287
|
+
const data = await res.json()
|
|
288
|
+
if (data.success) {
|
|
289
|
+
mutate()
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return (
|
|
294
|
+
<div>
|
|
295
|
+
{listData?.map(item => (
|
|
296
|
+
<div key={item.id}>
|
|
297
|
+
{item.name}
|
|
298
|
+
<button onClick={() => handleUpdate(item.id, { name: 'Updated' })}>更新</button>
|
|
299
|
+
<button onClick={() => handleDelete(item.id)}>删除</button>
|
|
300
|
+
</div>
|
|
301
|
+
))}
|
|
302
|
+
</div>
|
|
303
|
+
)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// 2️⃣ 直接使用 apiClient
|
|
307
|
+
import { apiClient } from '@client/services/apiClient'
|
|
308
|
+
import type { ${pascalName}, Create${pascalName}Input } from '@shared/modules/${kebabName}'
|
|
309
|
+
|
|
310
|
+
// ========== 基础 CRUD 操作 ==========
|
|
311
|
+
|
|
312
|
+
// 获取列表
|
|
313
|
+
const listRes = await apiClient.api['${kebabName}s'].$get()
|
|
314
|
+
const listData = await listRes.json()
|
|
315
|
+
if (listData.success) {
|
|
316
|
+
console.log('列表:', listData.data) // data 类型为 ${pascalName}[]
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// 获取单个
|
|
320
|
+
const getRes = await apiClient.api['${kebabName}s'][':id'].$get({
|
|
321
|
+
param: { id: '1' },
|
|
322
|
+
})
|
|
323
|
+
const getData = await getRes.json()
|
|
324
|
+
if (getData.success) {
|
|
325
|
+
console.log('详情:', getData.data) // data 类型为 ${pascalName}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// 创建
|
|
329
|
+
const createRes = await apiClient.api['${kebabName}s'].$post({
|
|
330
|
+
json: { name: 'New ${pascalName}' } as Create${pascalName}Input,
|
|
331
|
+
})
|
|
332
|
+
const createData = await createRes.json()
|
|
333
|
+
if (createData.success) {
|
|
334
|
+
console.log('创建成功:', createData.data)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// 更新
|
|
338
|
+
const updateRes = await apiClient.api['${kebabName}s'][':id'].$put({
|
|
339
|
+
param: { id: '1' },
|
|
340
|
+
json: { name: 'Updated ${pascalName}' },
|
|
341
|
+
})
|
|
342
|
+
const updateData = await updateRes.json()
|
|
343
|
+
if (updateData.success) {
|
|
344
|
+
console.log('更新成功:', updateData.data)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// 删除
|
|
348
|
+
const deleteRes = await apiClient.api['${kebabName}s'][':id'].$delete({
|
|
349
|
+
param: { id: '1' },
|
|
350
|
+
})
|
|
351
|
+
const deleteData = await deleteRes.json()
|
|
352
|
+
if (deleteData.success) {
|
|
353
|
+
console.log('删除成功')
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// ========== 🔐 带认证的请求(Header 示例) ==========
|
|
357
|
+
|
|
358
|
+
// 方式 1: 使用 header 选项传递认证 token
|
|
359
|
+
const authRes = await apiClient.api['${kebabName}s'].$get({
|
|
360
|
+
header: {
|
|
361
|
+
Authorization: 'Bearer your-token-here',
|
|
362
|
+
},
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
// 方式 2: 在创建 apiClient 时配置默认 headers
|
|
366
|
+
// 参考 src/client/services/apiClient.ts
|
|
367
|
+
|
|
368
|
+
// ========== 🛡️ 需要登录的路由示例 ==========
|
|
369
|
+
|
|
370
|
+
// 假设路由配置了 authMiddleware 中间件:
|
|
371
|
+
// middleware: [authMiddleware({ requiredRole: 'admin' })]
|
|
372
|
+
|
|
373
|
+
// 带认证的请求会自动验证 token
|
|
374
|
+
// - 401: 未提供有效的认证 token
|
|
375
|
+
// - 403: 权限不足(如需要 admin 角色但用户是普通用户)
|
|
376
|
+
|
|
377
|
+
// 示例:获取当前用户信息(需要登录)
|
|
378
|
+
const meRes = await apiClient.api.admin.me.$get({
|
|
379
|
+
header: {
|
|
380
|
+
Authorization: 'Bearer admin-token', // 或 'Bearer user-token'
|
|
381
|
+
},
|
|
382
|
+
})
|
|
383
|
+
const meData = await meRes.json()
|
|
384
|
+
if (meData.success) {
|
|
385
|
+
console.log('当前用户:', meData.data)
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// 示例:管理员操作(需要 admin 角色)
|
|
389
|
+
const adminRes = await apiClient.api.admin.stats.$get({
|
|
390
|
+
header: {
|
|
391
|
+
Authorization: 'Bearer admin-token', // 必须是 admin token
|
|
392
|
+
},
|
|
393
|
+
})
|
|
394
|
+
const adminData = await adminRes.json()
|
|
395
|
+
if (adminData.success) {
|
|
396
|
+
console.log('系统统计:', adminData.data)
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// ========== ⚠️ 错误处理示例 ==========
|
|
400
|
+
|
|
401
|
+
try {
|
|
402
|
+
const res = await apiClient.api.admin.stats.$get({
|
|
403
|
+
header: {
|
|
404
|
+
Authorization: 'Bearer invalid-token',
|
|
405
|
+
},
|
|
406
|
+
})
|
|
407
|
+
const data = await res.json()
|
|
408
|
+
if (!data.success) {
|
|
409
|
+
console.log('错误:', data.error) // "Unauthorized: Invalid authentication token"
|
|
410
|
+
}
|
|
411
|
+
} catch (error) {
|
|
412
|
+
console.error('请求失败:', error)
|
|
413
|
+
}
|
|
414
|
+
`
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function toPascalCase(str: string): string {
|
|
419
|
+
return str
|
|
420
|
+
.split(/[-_\s]+/)
|
|
421
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
422
|
+
.join('')
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function toCamelCase(str: string): string {
|
|
426
|
+
const pascal = toPascalCase(str)
|
|
427
|
+
return pascal.charAt(0).toLowerCase() + pascal.slice(1)
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function toKebabCase(str: string): string {
|
|
431
|
+
return str
|
|
432
|
+
.replace(/([a-z])([A-Z])/g, '$1-$2')
|
|
433
|
+
.replace(/[\s_]+/g, '-')
|
|
434
|
+
.toLowerCase()
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// ============================================
|
|
438
|
+
// Schema 模板
|
|
439
|
+
// ============================================
|
|
440
|
+
|
|
441
|
+
function generateBasicSchema(name: string): string {
|
|
442
|
+
const pascalName = toPascalCase(name)
|
|
443
|
+
|
|
444
|
+
return `import { z } from '@hono/zod-openapi'
|
|
445
|
+
|
|
446
|
+
export const ${pascalName}Schema = z.object({
|
|
447
|
+
id: z.string(),
|
|
448
|
+
name: z.string(),
|
|
449
|
+
createdAt: z.string().optional(),
|
|
450
|
+
updatedAt: z.string().optional(),
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
export const Create${pascalName}Schema = ${pascalName}Schema.omit({ id: true, createdAt: true, updatedAt: true })
|
|
454
|
+
|
|
455
|
+
export const Update${pascalName}Schema = ${pascalName}Schema.partial().omit({ id: true, createdAt: true, updatedAt: true })
|
|
456
|
+
|
|
457
|
+
export const ${pascalName}ListSchema = z.array(${pascalName}Schema)
|
|
458
|
+
|
|
459
|
+
export const DeleteResultSchema = z.object({ message: z.string() })
|
|
460
|
+
|
|
461
|
+
export type ${pascalName} = z.infer<typeof ${pascalName}Schema>
|
|
462
|
+
export type Create${pascalName}Input = z.infer<typeof Create${pascalName}Schema>
|
|
463
|
+
export type Update${pascalName}Input = z.infer<typeof Update${pascalName}Schema>
|
|
464
|
+
export type DeleteResult = z.infer<typeof DeleteResultSchema>
|
|
465
|
+
`
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function generateSSESchema(name: string): string {
|
|
469
|
+
const pascalName = toPascalCase(name)
|
|
470
|
+
|
|
471
|
+
return `import { z } from '@hono/zod-openapi'
|
|
472
|
+
|
|
473
|
+
export const ${pascalName}EventSchema = z.object({
|
|
474
|
+
id: z.string(),
|
|
475
|
+
type: z.enum(['created', 'updated', 'deleted']),
|
|
476
|
+
data: z.unknown(),
|
|
477
|
+
timestamp: z.string(),
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
export const ${pascalName}SSEProtocolSchema = z.object({
|
|
481
|
+
events: z.object({
|
|
482
|
+
message: ${pascalName}EventSchema,
|
|
483
|
+
ping: z.object({ timestamp: z.number() }),
|
|
484
|
+
connected: z.object({ timestamp: z.number() }),
|
|
485
|
+
}),
|
|
486
|
+
})
|
|
487
|
+
|
|
488
|
+
export type ${pascalName}Event = z.infer<typeof ${pascalName}EventSchema>
|
|
489
|
+
export type ${pascalName}SSEProtocol = z.infer<typeof ${pascalName}SSEProtocolSchema>
|
|
490
|
+
`
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function generateWebSocketSchema(name: string): string {
|
|
494
|
+
const pascalName = toPascalCase(name)
|
|
495
|
+
|
|
496
|
+
return `import { z } from '@hono/zod-openapi'
|
|
497
|
+
|
|
498
|
+
export const ${pascalName}MessageSchema = z.object({
|
|
499
|
+
id: z.string(),
|
|
500
|
+
type: z.string(),
|
|
501
|
+
payload: z.unknown(),
|
|
502
|
+
timestamp: z.string(),
|
|
503
|
+
senderId: z.string().optional(),
|
|
504
|
+
})
|
|
505
|
+
|
|
506
|
+
export const ${pascalName}ConnectionSchema = z.object({
|
|
507
|
+
userId: z.string().optional(),
|
|
508
|
+
roomId: z.string().optional(),
|
|
509
|
+
})
|
|
510
|
+
|
|
511
|
+
export const WebSocketStatusSchema = z.object({
|
|
512
|
+
connectedClients: z.number(),
|
|
513
|
+
})
|
|
514
|
+
|
|
515
|
+
export type ${pascalName}Message = z.infer<typeof ${pascalName}MessageSchema>
|
|
516
|
+
export type ${pascalName}Connection = z.infer<typeof ${pascalName}ConnectionSchema>
|
|
517
|
+
export type WebSocketStatus = z.infer<typeof WebSocketStatusSchema>
|
|
518
|
+
`
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// ============================================
|
|
522
|
+
// 路由模板
|
|
523
|
+
// ============================================
|
|
524
|
+
|
|
525
|
+
function generateBasicRouteTemplate(name: string): string {
|
|
526
|
+
const pascalName = toPascalCase(name)
|
|
527
|
+
const camelName = toCamelCase(name)
|
|
528
|
+
const kebabName = toKebabCase(name)
|
|
529
|
+
|
|
530
|
+
return `import { createRoute } from '@hono/zod-openapi'
|
|
531
|
+
import { OpenAPIHono } from '@hono/zod-openapi'
|
|
532
|
+
import * as ${camelName}Service from '../services/${kebabName}-service'
|
|
533
|
+
import { successResponse, errorResponse } from '../../utils/route-helpers'
|
|
534
|
+
import {
|
|
535
|
+
${pascalName}Schema,
|
|
536
|
+
Create${pascalName}Schema,
|
|
537
|
+
Update${pascalName}Schema,
|
|
538
|
+
${pascalName}ListSchema,
|
|
539
|
+
DeleteResultSchema,
|
|
540
|
+
} from '@shared/modules/${kebabName}'
|
|
541
|
+
|
|
542
|
+
const listRoute = createRoute({
|
|
543
|
+
method: 'get',
|
|
544
|
+
path: '/${kebabName}s',
|
|
545
|
+
tags: ['${kebabName}s'],
|
|
546
|
+
responses: {
|
|
547
|
+
200: successResponse(${pascalName}ListSchema, 'List all ${kebabName}s'),
|
|
548
|
+
500: errorResponse('Internal server error'),
|
|
549
|
+
},
|
|
550
|
+
})
|
|
551
|
+
|
|
552
|
+
const getRoute = createRoute({
|
|
553
|
+
method: 'get',
|
|
554
|
+
path: '/${kebabName}s/{id}',
|
|
555
|
+
tags: ['${kebabName}s'],
|
|
556
|
+
request: {
|
|
557
|
+
params: ${pascalName}Schema.pick({ id: true }),
|
|
558
|
+
},
|
|
559
|
+
responses: {
|
|
560
|
+
200: successResponse(${pascalName}Schema, 'Get ${kebabName} by id'),
|
|
561
|
+
404: errorResponse('${pascalName} not found'),
|
|
562
|
+
500: errorResponse('Internal server error'),
|
|
563
|
+
},
|
|
564
|
+
})
|
|
565
|
+
|
|
566
|
+
const createRouteDef = createRoute({
|
|
567
|
+
method: 'post',
|
|
568
|
+
path: '/${kebabName}s',
|
|
569
|
+
tags: ['${kebabName}s'],
|
|
570
|
+
request: {
|
|
571
|
+
body: {
|
|
572
|
+
content: {
|
|
573
|
+
'application/json': {
|
|
574
|
+
schema: Create${pascalName}Schema,
|
|
575
|
+
},
|
|
576
|
+
},
|
|
577
|
+
},
|
|
578
|
+
},
|
|
579
|
+
responses: {
|
|
580
|
+
201: successResponse(${pascalName}Schema, 'Create ${kebabName}'),
|
|
581
|
+
400: errorResponse('Invalid input'),
|
|
582
|
+
500: errorResponse('Internal server error'),
|
|
583
|
+
},
|
|
584
|
+
})
|
|
585
|
+
|
|
586
|
+
const updateRoute = createRoute({
|
|
587
|
+
method: 'put',
|
|
588
|
+
path: '/${kebabName}s/{id}',
|
|
589
|
+
tags: ['${kebabName}s'],
|
|
590
|
+
request: {
|
|
591
|
+
params: ${pascalName}Schema.pick({ id: true }),
|
|
592
|
+
body: {
|
|
593
|
+
content: {
|
|
594
|
+
'application/json': {
|
|
595
|
+
schema: Update${pascalName}Schema,
|
|
596
|
+
},
|
|
597
|
+
},
|
|
598
|
+
},
|
|
599
|
+
},
|
|
600
|
+
responses: {
|
|
601
|
+
200: successResponse(${pascalName}Schema, 'Update ${kebabName}'),
|
|
602
|
+
404: errorResponse('${pascalName} not found'),
|
|
603
|
+
400: errorResponse('Invalid input'),
|
|
604
|
+
500: errorResponse('Internal server error'),
|
|
605
|
+
},
|
|
606
|
+
})
|
|
607
|
+
|
|
608
|
+
const deleteRoute = createRoute({
|
|
609
|
+
method: 'delete',
|
|
610
|
+
path: '/${kebabName}s/{id}',
|
|
611
|
+
tags: ['${kebabName}s'],
|
|
612
|
+
request: {
|
|
613
|
+
params: ${pascalName}Schema.pick({ id: true }),
|
|
614
|
+
},
|
|
615
|
+
responses: {
|
|
616
|
+
200: successResponse(DeleteResultSchema, '${pascalName} deleted'),
|
|
617
|
+
404: errorResponse('${pascalName} not found'),
|
|
618
|
+
500: errorResponse('Internal server error'),
|
|
619
|
+
},
|
|
620
|
+
})
|
|
621
|
+
|
|
622
|
+
export const ${camelName}Routes = new OpenAPIHono()
|
|
623
|
+
.openapi(listRoute, async c => {
|
|
624
|
+
const result = await ${camelName}Service.getAll()
|
|
625
|
+
return c.json({ success: true, data: result })
|
|
626
|
+
})
|
|
627
|
+
.openapi(getRoute, async c => {
|
|
628
|
+
const { id } = c.req.valid('param')
|
|
629
|
+
const result = await ${camelName}Service.getById(id)
|
|
630
|
+
if (!result) {
|
|
631
|
+
return c.json({ success: false, error: '${pascalName} not found' }, 404)
|
|
632
|
+
}
|
|
633
|
+
return c.json({ success: true, data: result })
|
|
634
|
+
})
|
|
635
|
+
.openapi(createRouteDef, async c => {
|
|
636
|
+
const body = c.req.valid('json')
|
|
637
|
+
const result = await ${camelName}Service.create(body)
|
|
638
|
+
return c.json({ success: true, data: result }, 201)
|
|
639
|
+
})
|
|
640
|
+
.openapi(updateRoute, async c => {
|
|
641
|
+
const { id } = c.req.valid('param')
|
|
642
|
+
const body = c.req.valid('json')
|
|
643
|
+
const result = await ${camelName}Service.update(id, body)
|
|
644
|
+
if (!result) {
|
|
645
|
+
return c.json({ success: false, error: '${pascalName} not found' }, 404)
|
|
646
|
+
}
|
|
647
|
+
return c.json({ success: true, data: result })
|
|
648
|
+
})
|
|
649
|
+
.openapi(deleteRoute, async c => {
|
|
650
|
+
const { id } = c.req.valid('param')
|
|
651
|
+
const result = await ${camelName}Service.delete${pascalName}(id)
|
|
652
|
+
if (!result) {
|
|
653
|
+
return c.json({ success: false, error: '${pascalName} not found' }, 404)
|
|
654
|
+
}
|
|
655
|
+
return c.json({ success: true, data: { message: 'Deleted successfully' } })
|
|
656
|
+
})
|
|
657
|
+
`
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function generateDatabaseRouteTemplate(name: string): string {
|
|
661
|
+
const pascalName = toPascalCase(name)
|
|
662
|
+
const camelName = toCamelCase(name)
|
|
663
|
+
const kebabName = toKebabCase(name)
|
|
664
|
+
|
|
665
|
+
return `import { createRoute } from '@hono/zod-openapi'
|
|
666
|
+
import { OpenAPIHono } from '@hono/zod-openapi'
|
|
667
|
+
import * as ${camelName}Service from '../services/${kebabName}-service'
|
|
668
|
+
import { successResponse, errorResponse } from '../../utils/route-helpers'
|
|
669
|
+
import {
|
|
670
|
+
${pascalName}Schema,
|
|
671
|
+
Create${pascalName}Schema,
|
|
672
|
+
Update${pascalName}Schema,
|
|
673
|
+
${pascalName}ListSchema,
|
|
674
|
+
DeleteResultSchema,
|
|
675
|
+
} from '@shared/modules/${kebabName}'
|
|
676
|
+
|
|
677
|
+
const listRoute = createRoute({
|
|
678
|
+
method: 'get',
|
|
679
|
+
path: '/${kebabName}s',
|
|
680
|
+
tags: ['${kebabName}s'],
|
|
681
|
+
responses: {
|
|
682
|
+
200: successResponse(${pascalName}ListSchema, 'List all ${kebabName}s'),
|
|
683
|
+
500: errorResponse('Internal server error'),
|
|
684
|
+
},
|
|
685
|
+
})
|
|
686
|
+
|
|
687
|
+
const getRoute = createRoute({
|
|
688
|
+
method: 'get',
|
|
689
|
+
path: '/${kebabName}s/{id}',
|
|
690
|
+
tags: ['${kebabName}s'],
|
|
691
|
+
request: {
|
|
692
|
+
params: ${pascalName}Schema.pick({ id: true }),
|
|
693
|
+
},
|
|
694
|
+
responses: {
|
|
695
|
+
200: successResponse(${pascalName}Schema, 'Get ${kebabName} by id'),
|
|
696
|
+
404: errorResponse('${pascalName} not found'),
|
|
697
|
+
500: errorResponse('Internal server error'),
|
|
698
|
+
},
|
|
699
|
+
})
|
|
700
|
+
|
|
701
|
+
const createRouteDef = createRoute({
|
|
702
|
+
method: 'post',
|
|
703
|
+
path: '/${kebabName}s',
|
|
704
|
+
tags: ['${kebabName}s'],
|
|
705
|
+
request: {
|
|
706
|
+
body: {
|
|
707
|
+
content: {
|
|
708
|
+
'application/json': {
|
|
709
|
+
schema: Create${pascalName}Schema,
|
|
710
|
+
},
|
|
711
|
+
},
|
|
712
|
+
},
|
|
713
|
+
},
|
|
714
|
+
responses: {
|
|
715
|
+
201: successResponse(${pascalName}Schema, 'Create ${kebabName}'),
|
|
716
|
+
400: errorResponse('Invalid input'),
|
|
717
|
+
500: errorResponse('Internal server error'),
|
|
718
|
+
},
|
|
719
|
+
})
|
|
720
|
+
|
|
721
|
+
const updateRoute = createRoute({
|
|
722
|
+
method: 'put',
|
|
723
|
+
path: '/${kebabName}s/{id}',
|
|
724
|
+
tags: ['${kebabName}s'],
|
|
725
|
+
request: {
|
|
726
|
+
params: ${pascalName}Schema.pick({ id: true }),
|
|
727
|
+
body: {
|
|
728
|
+
content: {
|
|
729
|
+
'application/json': {
|
|
730
|
+
schema: Update${pascalName}Schema,
|
|
731
|
+
},
|
|
732
|
+
},
|
|
733
|
+
},
|
|
734
|
+
},
|
|
735
|
+
responses: {
|
|
736
|
+
200: successResponse(${pascalName}Schema, 'Update ${kebabName}'),
|
|
737
|
+
404: errorResponse('${pascalName} not found'),
|
|
738
|
+
400: errorResponse('Invalid input'),
|
|
739
|
+
500: errorResponse('Internal server error'),
|
|
740
|
+
},
|
|
741
|
+
})
|
|
742
|
+
|
|
743
|
+
const deleteRoute = createRoute({
|
|
744
|
+
method: 'delete',
|
|
745
|
+
path: '/${kebabName}s/{id}',
|
|
746
|
+
tags: ['${kebabName}s'],
|
|
747
|
+
request: {
|
|
748
|
+
params: ${pascalName}Schema.pick({ id: true }),
|
|
749
|
+
},
|
|
750
|
+
responses: {
|
|
751
|
+
200: successResponse(DeleteResultSchema, '${pascalName} deleted'),
|
|
752
|
+
404: errorResponse('${pascalName} not found'),
|
|
753
|
+
500: errorResponse('Internal server error'),
|
|
754
|
+
},
|
|
755
|
+
})
|
|
756
|
+
|
|
757
|
+
export const ${camelName}Routes = new OpenAPIHono()
|
|
758
|
+
.openapi(listRoute, async c => {
|
|
759
|
+
const result = await ${camelName}Service.getAll()
|
|
760
|
+
return c.json({ success: true, data: result })
|
|
761
|
+
})
|
|
762
|
+
.openapi(getRoute, async c => {
|
|
763
|
+
const { id } = c.req.valid('param')
|
|
764
|
+
const result = await ${camelName}Service.getById(id)
|
|
765
|
+
if (!result) {
|
|
766
|
+
return c.json({ success: false, error: '${pascalName} not found' }, 404)
|
|
767
|
+
}
|
|
768
|
+
return c.json({ success: true, data: result })
|
|
769
|
+
})
|
|
770
|
+
.openapi(createRouteDef, async c => {
|
|
771
|
+
const body = c.req.valid('json')
|
|
772
|
+
const result = await ${camelName}Service.create(body)
|
|
773
|
+
return c.json({ success: true, data: result }, 201)
|
|
774
|
+
})
|
|
775
|
+
.openapi(updateRoute, async c => {
|
|
776
|
+
const { id } = c.req.valid('param')
|
|
777
|
+
const body = c.req.valid('json')
|
|
778
|
+
const result = await ${camelName}Service.update(id, body)
|
|
779
|
+
if (!result) {
|
|
780
|
+
return c.json({ success: false, error: '${pascalName} not found' }, 404)
|
|
781
|
+
}
|
|
782
|
+
return c.json({ success: true, data: result })
|
|
783
|
+
})
|
|
784
|
+
.openapi(deleteRoute, async c => {
|
|
785
|
+
const { id } = c.req.valid('param')
|
|
786
|
+
const result = await ${camelName}Service.delete${pascalName}(id)
|
|
787
|
+
if (!result) {
|
|
788
|
+
return c.json({ success: false, error: '${pascalName} not found' }, 404)
|
|
789
|
+
}
|
|
790
|
+
return c.json({ success: true, data: { message: 'Deleted successfully' } })
|
|
791
|
+
})
|
|
792
|
+
`
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function generateSSERouteTemplate(name: string): string {
|
|
796
|
+
const pascalName = toPascalCase(name)
|
|
797
|
+
const camelName = toCamelCase(name)
|
|
798
|
+
const kebabName = toKebabCase(name)
|
|
799
|
+
|
|
800
|
+
return `import { createRoute } from '@hono/zod-openapi'
|
|
801
|
+
import { OpenAPIHono } from '@hono/zod-openapi'
|
|
802
|
+
import * as ${camelName}Service from '../services/${kebabName}-service'
|
|
803
|
+
import { ${pascalName}SSEProtocolSchema } from '@shared/modules/${kebabName}'
|
|
804
|
+
import { getRuntimeAdapter } from '@server/core/runtime'
|
|
805
|
+
|
|
806
|
+
const streamRoute = createRoute({
|
|
807
|
+
method: 'get',
|
|
808
|
+
path: '/${kebabName}s/stream',
|
|
809
|
+
tags: ['${kebabName}s'],
|
|
810
|
+
responses: {
|
|
811
|
+
200: {
|
|
812
|
+
description: 'SSE stream for ${kebabName} events',
|
|
813
|
+
content: {
|
|
814
|
+
'text/event-stream': { schema: ${pascalName}SSEProtocolSchema },
|
|
815
|
+
},
|
|
816
|
+
},
|
|
817
|
+
},
|
|
818
|
+
})
|
|
819
|
+
|
|
820
|
+
export const ${camelName}Routes = new OpenAPIHono()
|
|
821
|
+
.openapi(streamRoute, async _c => {
|
|
822
|
+
const adapter = getRuntimeAdapter()
|
|
823
|
+
if ('handleSSERequest' in adapter && typeof adapter.handleSSERequest === 'function') {
|
|
824
|
+
return (
|
|
825
|
+
adapter as { handleSSERequest: () => Response | Promise<Response> }
|
|
826
|
+
).handleSSERequest()
|
|
827
|
+
}
|
|
828
|
+
return new Response('SSE not supported', { status: 500 })
|
|
829
|
+
})
|
|
830
|
+
`
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
function generateWebSocketRouteTemplate(name: string): string {
|
|
834
|
+
const pascalName = toPascalCase(name)
|
|
835
|
+
const camelName = toCamelCase(name)
|
|
836
|
+
const kebabName = toKebabCase(name)
|
|
837
|
+
|
|
838
|
+
return `import { createRoute } from '@hono/zod-openapi'
|
|
839
|
+
import { OpenAPIHono } from '@hono/zod-openapi'
|
|
840
|
+
import type { AppBindings } from '../../types/bindings'
|
|
841
|
+
import { getRuntimeAdapter } from '@server/core/runtime'
|
|
842
|
+
import { ${pascalName}MessageSchema, WebSocketStatusSchema } from '@shared/modules/${kebabName}'
|
|
843
|
+
import { successResponse } from '@server/utils/route-helpers'
|
|
844
|
+
|
|
845
|
+
const statusRoute = createRoute({
|
|
846
|
+
method: 'get',
|
|
847
|
+
path: '/${kebabName}s/ws/status',
|
|
848
|
+
tags: ['${kebabName}s'],
|
|
849
|
+
responses: {
|
|
850
|
+
200: successResponse(WebSocketStatusSchema, 'Get WebSocket status'),
|
|
851
|
+
},
|
|
852
|
+
})
|
|
853
|
+
|
|
854
|
+
const wsRoute = createRoute({
|
|
855
|
+
method: 'get',
|
|
856
|
+
path: '/${kebabName}s/ws',
|
|
857
|
+
tags: ['${kebabName}s'],
|
|
858
|
+
responses: {
|
|
859
|
+
200: {
|
|
860
|
+
content: {
|
|
861
|
+
websocket: {
|
|
862
|
+
schema: ${pascalName}MessageSchema,
|
|
863
|
+
},
|
|
864
|
+
},
|
|
865
|
+
description: 'WebSocket endpoint for ${kebabName}',
|
|
866
|
+
},
|
|
867
|
+
},
|
|
868
|
+
})
|
|
869
|
+
|
|
870
|
+
export const ${camelName}Routes = new OpenAPIHono<{ Bindings: AppBindings }>()
|
|
871
|
+
.openapi(statusRoute, async c => {
|
|
872
|
+
return c.json({ success: true, data: { connectedClients: 0 } })
|
|
873
|
+
})
|
|
874
|
+
.openapi(wsRoute, async _c => {
|
|
875
|
+
const adapter = getRuntimeAdapter()
|
|
876
|
+
if (
|
|
877
|
+
'handleWebSocketRequest' in adapter &&
|
|
878
|
+
typeof adapter.handleWebSocketRequest === 'function'
|
|
879
|
+
) {
|
|
880
|
+
return (
|
|
881
|
+
adapter as { handleWebSocketRequest: () => Response | Promise<Response> }
|
|
882
|
+
).handleWebSocketRequest()
|
|
883
|
+
}
|
|
884
|
+
return new Response('WebSocket not supported', { status: 500 })
|
|
885
|
+
})
|
|
886
|
+
|
|
887
|
+
export type ${pascalName}RoutesType = typeof ${camelName}Routes
|
|
888
|
+
`
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// ============================================
|
|
892
|
+
// 服务模板
|
|
893
|
+
// ============================================
|
|
894
|
+
|
|
895
|
+
function generateBasicServiceTemplate(name: string): string {
|
|
896
|
+
const pascalName = toPascalCase(name)
|
|
897
|
+
const camelName = toCamelCase(name)
|
|
898
|
+
|
|
899
|
+
return `/**
|
|
900
|
+
* ${pascalName} 服务层 (基础模板 - 无数据库)
|
|
901
|
+
*
|
|
902
|
+
* 职责:
|
|
903
|
+
* - 业务逻辑处理
|
|
904
|
+
* - 数据验证
|
|
905
|
+
* - 内存存储
|
|
906
|
+
*/
|
|
907
|
+
|
|
908
|
+
import type { ${pascalName}, Create${pascalName}Input, Update${pascalName}Input } from '@shared/modules/${name}'
|
|
909
|
+
|
|
910
|
+
type ${pascalName}Row = ${pascalName}
|
|
911
|
+
|
|
912
|
+
const ${camelName}Store = new Map<string, ${pascalName}Row>()
|
|
913
|
+
let idCounter = 0
|
|
914
|
+
|
|
915
|
+
export const getAll = async (): Promise<${pascalName}Row[]> => {
|
|
916
|
+
return Array.from(${camelName}Store.values())
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
export const getById = async (id: string): Promise<${pascalName}Row | null> => {
|
|
920
|
+
return ${camelName}Store.get(id) || null
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
export const create = async (data: Create${pascalName}Input): Promise<${pascalName}Row> => {
|
|
924
|
+
const id = \`\${++idCounter}\`
|
|
925
|
+
const item: ${pascalName}Row = {
|
|
926
|
+
id,
|
|
927
|
+
...data,
|
|
928
|
+
createdAt: new Date().toISOString(),
|
|
929
|
+
updatedAt: new Date().toISOString(),
|
|
930
|
+
}
|
|
931
|
+
${camelName}Store.set(id, item)
|
|
932
|
+
return item
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
export const update = async (id: string, data: Update${pascalName}Input): Promise<${pascalName}Row | null> => {
|
|
936
|
+
const existing = ${camelName}Store.get(id)
|
|
937
|
+
if (!existing) return null
|
|
938
|
+
|
|
939
|
+
const updated: ${pascalName}Row = {
|
|
940
|
+
...existing,
|
|
941
|
+
...data,
|
|
942
|
+
updatedAt: new Date().toISOString(),
|
|
943
|
+
}
|
|
944
|
+
${camelName}Store.set(id, updated)
|
|
945
|
+
return updated
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
export const delete${pascalName} = async (id: string): Promise<boolean> => {
|
|
949
|
+
return ${camelName}Store.delete(id)
|
|
950
|
+
}
|
|
951
|
+
`
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
function generateDatabaseServiceTemplate(name: string): string {
|
|
955
|
+
const pascalName = toPascalCase(name)
|
|
956
|
+
const camelName = toCamelCase(name)
|
|
957
|
+
const kebabName = toKebabCase(name)
|
|
958
|
+
|
|
959
|
+
return `/**
|
|
960
|
+
* ${pascalName} 服务层 (数据库模板)
|
|
961
|
+
*
|
|
962
|
+
* 职责:
|
|
963
|
+
* - 业务逻辑处理
|
|
964
|
+
* - 数据验证
|
|
965
|
+
* - 数据库操作
|
|
966
|
+
*
|
|
967
|
+
* TODO: 需要先创建数据库表定义
|
|
968
|
+
* 1. 在 src/server/db/schema/index.ts 中添加表定义
|
|
969
|
+
* 2. 取消注释下面的导入
|
|
970
|
+
*/
|
|
971
|
+
|
|
972
|
+
// TODO: 取消注释以下导入
|
|
973
|
+
// import { db } from '@server/db'
|
|
974
|
+
// import { eq } from 'drizzle-orm'
|
|
975
|
+
// import { ${kebabName}s } from '@server/db/schema'
|
|
976
|
+
|
|
977
|
+
import type { ${pascalName}, Create${pascalName}Input, Update${pascalName}Input } from '@shared/modules/${name}'
|
|
978
|
+
|
|
979
|
+
type ${pascalName}Row = ${pascalName}
|
|
980
|
+
|
|
981
|
+
export const getAll = async (): Promise<${pascalName}Row[]> => {
|
|
982
|
+
// TODO: 实现数据库查询
|
|
983
|
+
// return await db.select().from(${kebabName}s)
|
|
984
|
+
return []
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
export const getById = async (_id: string): Promise<${pascalName}Row | null> => {
|
|
988
|
+
// TODO: 实现数据库查询
|
|
989
|
+
// const result = await db.select().from(${kebabName}s).where(eq(${kebabName}s.id, id))
|
|
990
|
+
// return result[0] || null
|
|
991
|
+
return null
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
export const create = async (data: Create${pascalName}Input): Promise<${pascalName}Row> => {
|
|
995
|
+
// TODO: 实现数据库插入
|
|
996
|
+
// const result = await db.insert(${kebabName}s).values(data).returning()
|
|
997
|
+
// return result[0]
|
|
998
|
+
return {
|
|
999
|
+
id: 'temp',
|
|
1000
|
+
...data,
|
|
1001
|
+
createdAt: new Date().toISOString(),
|
|
1002
|
+
updatedAt: new Date().toISOString(),
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
export const update = async (_id: string, _data: Update${pascalName}Input): Promise<${pascalName}Row | null> => {
|
|
1007
|
+
// TODO: 实现数据库更新
|
|
1008
|
+
// const result = await db
|
|
1009
|
+
// .update(${kebabName}s)
|
|
1010
|
+
// .set({ ...data, updatedAt: new Date() })
|
|
1011
|
+
// .where(eq(${kebabName}s.id, id))
|
|
1012
|
+
// .returning()
|
|
1013
|
+
// return result[0] || null
|
|
1014
|
+
return null
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
export const delete${pascalName} = async (_id: string): Promise<boolean> => {
|
|
1018
|
+
// TODO: 实现数据库删除
|
|
1019
|
+
// const result = await db.delete(${kebabName}s).where(eq(${kebabName}s.id, id)).returning()
|
|
1020
|
+
// return result.length > 0
|
|
1021
|
+
return false
|
|
1022
|
+
}
|
|
1023
|
+
`
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
function generateSSEServiceTemplate(name: string): string {
|
|
1027
|
+
const pascalName = toPascalCase(name)
|
|
1028
|
+
const camelName = toCamelCase(name)
|
|
1029
|
+
const kebabName = toKebabCase(name)
|
|
1030
|
+
|
|
1031
|
+
return `/**
|
|
1032
|
+
* ${pascalName} 服务层 (SSE 模板)
|
|
1033
|
+
*
|
|
1034
|
+
* 职责:
|
|
1035
|
+
* - 事件广播
|
|
1036
|
+
* - 业务逻辑处理
|
|
1037
|
+
*
|
|
1038
|
+
* 注意:SSE 连接管理由框架 runtime 层处理
|
|
1039
|
+
* 使用 realtime.broadcast() 广播消息
|
|
1040
|
+
*/
|
|
1041
|
+
|
|
1042
|
+
import { realtime } from '@server/core'
|
|
1043
|
+
import type { ${pascalName}Event } from '@shared/modules/${kebabName}'
|
|
1044
|
+
|
|
1045
|
+
export async function broadcast${pascalName}Event(event: ${pascalName}Event): Promise<void> {
|
|
1046
|
+
await realtime.broadcast('message', event)
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
export async function sendTestEvent(): Promise<void> {
|
|
1050
|
+
const event: ${pascalName}Event = {
|
|
1051
|
+
id: crypto.randomUUID(),
|
|
1052
|
+
type: 'test',
|
|
1053
|
+
payload: { message: 'Test event' },
|
|
1054
|
+
createdAt: new Date().toISOString(),
|
|
1055
|
+
}
|
|
1056
|
+
await broadcast${pascalName}Event(event)
|
|
1057
|
+
}
|
|
1058
|
+
`
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
function generateWebSocketServiceTemplate(name: string): string {
|
|
1062
|
+
const pascalName = toPascalCase(name)
|
|
1063
|
+
|
|
1064
|
+
return `/**
|
|
1065
|
+
* ${pascalName} 服务层 (WebSocket 模板)
|
|
1066
|
+
*
|
|
1067
|
+
* 职责:
|
|
1068
|
+
* - WebSocket 连接管理
|
|
1069
|
+
* - 消息广播
|
|
1070
|
+
* - 房间管理
|
|
1071
|
+
*
|
|
1072
|
+
* 注意:此模板使用运行时适配器处理 WebSocket
|
|
1073
|
+
* 实际连接管理在 runtime 层实现
|
|
1074
|
+
*/
|
|
1075
|
+
|
|
1076
|
+
import type { ${pascalName}Message } from '@shared/modules/${name}'
|
|
1077
|
+
|
|
1078
|
+
export const getConnectedClientsCount = (): number => {
|
|
1079
|
+
// TODO: 实现连接计数
|
|
1080
|
+
return 0
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
export const broadcast = async (_message: ${pascalName}Message): Promise<void> => {
|
|
1084
|
+
// TODO: 实现消息广播
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
export const broadcastToRoom = async (_roomId: string, _message: ${pascalName}Message): Promise<void> => {
|
|
1088
|
+
// TODO: 实现房间消息广播
|
|
1089
|
+
}
|
|
1090
|
+
`
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// ============================================
|
|
1094
|
+
// 测试模板
|
|
1095
|
+
// ============================================
|
|
1096
|
+
|
|
1097
|
+
function generateBasicServiceTestTemplate(name: string): string {
|
|
1098
|
+
const pascalName = toPascalCase(name)
|
|
1099
|
+
const kebabName = toKebabCase(name)
|
|
1100
|
+
|
|
1101
|
+
return `import { describe, it, expect, beforeEach } from 'vitest'
|
|
1102
|
+
import * as service from '../services/${kebabName}-service'
|
|
1103
|
+
|
|
1104
|
+
describe('${pascalName} Service', () => {
|
|
1105
|
+
beforeEach(() => {
|
|
1106
|
+
// Reset store before each test
|
|
1107
|
+
})
|
|
1108
|
+
|
|
1109
|
+
describe('getAll', () => {
|
|
1110
|
+
it('should return an array', async () => {
|
|
1111
|
+
const result = await service.getAll()
|
|
1112
|
+
expect(Array.isArray(result)).toBe(true)
|
|
1113
|
+
})
|
|
1114
|
+
})
|
|
1115
|
+
|
|
1116
|
+
describe('getById', () => {
|
|
1117
|
+
it('should return null for non-existent id', async () => {
|
|
1118
|
+
const result = await service.getById('non-existent')
|
|
1119
|
+
expect(result).toBeNull()
|
|
1120
|
+
})
|
|
1121
|
+
})
|
|
1122
|
+
|
|
1123
|
+
describe('create', () => {
|
|
1124
|
+
it('should create a new item', async () => {
|
|
1125
|
+
const data = { name: 'Test Item' }
|
|
1126
|
+
const result = await service.create(data)
|
|
1127
|
+
expect(result).toBeDefined()
|
|
1128
|
+
expect(result.id).toBeDefined()
|
|
1129
|
+
expect(result.name).toBe('Test Item')
|
|
1130
|
+
})
|
|
1131
|
+
})
|
|
1132
|
+
|
|
1133
|
+
describe('update', () => {
|
|
1134
|
+
it('should return null for non-existent id', async () => {
|
|
1135
|
+
const result = await service.update('non-existent', {})
|
|
1136
|
+
expect(result).toBeNull()
|
|
1137
|
+
})
|
|
1138
|
+
})
|
|
1139
|
+
|
|
1140
|
+
describe('delete', () => {
|
|
1141
|
+
it('should return false for non-existent id', async () => {
|
|
1142
|
+
const result = await service.delete${pascalName}('non-existent')
|
|
1143
|
+
expect(result).toBe(false)
|
|
1144
|
+
})
|
|
1145
|
+
})
|
|
1146
|
+
})
|
|
1147
|
+
`
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
function generateSSEServiceTestTemplate(name: string): string {
|
|
1151
|
+
const pascalName = toPascalCase(name)
|
|
1152
|
+
const kebabName = toKebabCase(name)
|
|
1153
|
+
|
|
1154
|
+
return `import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
1155
|
+
import * as service from '../services/${kebabName}-service'
|
|
1156
|
+
import type { ${pascalName}Event } from '@shared/modules/${kebabName}'
|
|
1157
|
+
|
|
1158
|
+
vi.mock('@server/core', () => ({
|
|
1159
|
+
realtime: {
|
|
1160
|
+
broadcast: vi.fn(),
|
|
1161
|
+
},
|
|
1162
|
+
}))
|
|
1163
|
+
|
|
1164
|
+
describe('${pascalName} Service (SSE)', () => {
|
|
1165
|
+
beforeEach(() => {
|
|
1166
|
+
vi.clearAllMocks()
|
|
1167
|
+
})
|
|
1168
|
+
|
|
1169
|
+
describe('broadcast${pascalName}Event', () => {
|
|
1170
|
+
it('should call realtime.broadcast with event', async () => {
|
|
1171
|
+
const event: ${pascalName}Event = {
|
|
1172
|
+
id: '1',
|
|
1173
|
+
type: 'created',
|
|
1174
|
+
data: { name: 'Test' },
|
|
1175
|
+
timestamp: new Date().toISOString(),
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
await service.broadcast${pascalName}Event(event)
|
|
1179
|
+
|
|
1180
|
+
const { realtime } = await import('@server/core')
|
|
1181
|
+
expect(realtime.broadcast).toHaveBeenCalledWith('message', event)
|
|
1182
|
+
})
|
|
1183
|
+
})
|
|
1184
|
+
|
|
1185
|
+
describe('sendTestEvent', () => {
|
|
1186
|
+
it('should broadcast a test event', async () => {
|
|
1187
|
+
await service.sendTestEvent()
|
|
1188
|
+
|
|
1189
|
+
const { realtime } = await import('@server/core')
|
|
1190
|
+
expect(realtime.broadcast).toHaveBeenCalled()
|
|
1191
|
+
})
|
|
1192
|
+
})
|
|
1193
|
+
})
|
|
1194
|
+
`
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
function generateWebSocketServiceTestTemplate(name: string): string {
|
|
1198
|
+
const pascalName = toPascalCase(name)
|
|
1199
|
+
const kebabName = toKebabCase(name)
|
|
1200
|
+
|
|
1201
|
+
return `import { describe, it, expect } from 'vitest'
|
|
1202
|
+
import * as service from '../services/${kebabName}-service'
|
|
1203
|
+
|
|
1204
|
+
describe('${pascalName} Service (WebSocket)', () => {
|
|
1205
|
+
describe('getConnectedClientsCount', () => {
|
|
1206
|
+
it('should return a number', async () => {
|
|
1207
|
+
const count = service.getConnectedClientsCount()
|
|
1208
|
+
expect(typeof count).toBe('number')
|
|
1209
|
+
expect(count).toBeGreaterThanOrEqual(0)
|
|
1210
|
+
})
|
|
1211
|
+
})
|
|
1212
|
+
})
|
|
1213
|
+
`
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
function generateBasicRouteTestTemplate(name: string): string {
|
|
1217
|
+
const pascalName = toPascalCase(name)
|
|
1218
|
+
const kebabName = toKebabCase(name)
|
|
1219
|
+
|
|
1220
|
+
return `import { describe, it, expect } from 'vitest'
|
|
1221
|
+
import { createTestClient } from '../../test-utils/test-client'
|
|
1222
|
+
|
|
1223
|
+
describe('${pascalName} Routes', () => {
|
|
1224
|
+
describe('GET /api/${kebabName}s', () => {
|
|
1225
|
+
it('should return list of ${kebabName}s', async () => {
|
|
1226
|
+
const client = createTestClient()
|
|
1227
|
+
const res = await client.api['${kebabName}s'].$get()
|
|
1228
|
+
expect(res.status).toBe(200)
|
|
1229
|
+
|
|
1230
|
+
const data = await res.json()
|
|
1231
|
+
expect(data.success).toBe(true)
|
|
1232
|
+
if (data.success) {
|
|
1233
|
+
expect(Array.isArray(data.data)).toBe(true)
|
|
1234
|
+
}
|
|
1235
|
+
})
|
|
1236
|
+
})
|
|
1237
|
+
|
|
1238
|
+
describe('GET /api/${kebabName}s/:id', () => {
|
|
1239
|
+
it('should return 404 for non-existent ${kebabName}', async () => {
|
|
1240
|
+
const client = createTestClient()
|
|
1241
|
+
const res = await client.api['${kebabName}s'][':id'].$get({
|
|
1242
|
+
param: { id: 'non-existent' },
|
|
1243
|
+
})
|
|
1244
|
+
expect(res.status).toBe(404)
|
|
1245
|
+
expect(typeof res.status).toBe('number')
|
|
1246
|
+
})
|
|
1247
|
+
})
|
|
1248
|
+
})
|
|
1249
|
+
`
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
function generateSSERouteTestTemplate(name: string): string {
|
|
1253
|
+
const pascalName = toPascalCase(name)
|
|
1254
|
+
const kebabName = toKebabCase(name)
|
|
1255
|
+
|
|
1256
|
+
return `import { describe, it, expect, beforeAll, afterAll } from 'vitest'
|
|
1257
|
+
import { createApp } from '../../app'
|
|
1258
|
+
import { createTestClient } from '../../test-utils/test-client'
|
|
1259
|
+
import { createTestServer } from '../../test-utils/test-server'
|
|
1260
|
+
import { setRuntimeAdapter } from '@server/core/runtime'
|
|
1261
|
+
import { getNodeRuntimeAdapter } from '@server/core/runtime-node'
|
|
1262
|
+
import { createSSEClient } from '@shared/core/sse-client'
|
|
1263
|
+
|
|
1264
|
+
setRuntimeAdapter(getNodeRuntimeAdapter())
|
|
1265
|
+
|
|
1266
|
+
describe('${pascalName} Routes (SSE)', () => {
|
|
1267
|
+
let testServer: Awaited<ReturnType<typeof createTestServer>>
|
|
1268
|
+
let client: ReturnType<typeof createTestClient>
|
|
1269
|
+
|
|
1270
|
+
beforeAll(async () => {
|
|
1271
|
+
const app = createApp()
|
|
1272
|
+
testServer = await createTestServer(app, ['/api/${kebabName}s/stream'])
|
|
1273
|
+
client = createTestClient(\`http://localhost:\${testServer.port}\`, {
|
|
1274
|
+
sse: (url: string | URL) => createSSEClient(url),
|
|
1275
|
+
})
|
|
1276
|
+
}, 15000)
|
|
1277
|
+
|
|
1278
|
+
afterAll(async () => {
|
|
1279
|
+
await testServer.close()
|
|
1280
|
+
}, 15000)
|
|
1281
|
+
|
|
1282
|
+
describe('GET /api/${kebabName}s/stream', () => {
|
|
1283
|
+
it('should use $sse() method for type-safe SSE connection', async () => {
|
|
1284
|
+
const conn = client.api['${kebabName}s'].stream.$sse()
|
|
1285
|
+
|
|
1286
|
+
expect(['connecting', 'open', 'closed']).toContain(conn.status)
|
|
1287
|
+
|
|
1288
|
+
conn.abort()
|
|
1289
|
+
})
|
|
1290
|
+
})
|
|
1291
|
+
})
|
|
1292
|
+
`
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
function generateWebSocketRouteTestTemplate(name: string): string {
|
|
1296
|
+
const pascalName = toPascalCase(name)
|
|
1297
|
+
const kebabName = toKebabCase(name)
|
|
1298
|
+
|
|
1299
|
+
return `import { describe, it, expect } from 'vitest'
|
|
1300
|
+
import { createTestClient } from '../../test-utils/test-client'
|
|
1301
|
+
|
|
1302
|
+
describe('${pascalName} Routes (WebSocket)', () => {
|
|
1303
|
+
describe('GET /api/${kebabName}s/ws/status', () => {
|
|
1304
|
+
it('should return WebSocket status', async () => {
|
|
1305
|
+
const client = createTestClient()
|
|
1306
|
+
const res = await client.api['${kebabName}s'].ws.status.$get()
|
|
1307
|
+
|
|
1308
|
+
expect(res.status).toBe(200)
|
|
1309
|
+
const data = await res.json()
|
|
1310
|
+
expect(data.success).toBe(true)
|
|
1311
|
+
expect(typeof data.data.connectedClients).toBe('number')
|
|
1312
|
+
})
|
|
1313
|
+
})
|
|
1314
|
+
})
|
|
1315
|
+
`
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
function generateModuleIndexTemplate(): string {
|
|
1319
|
+
return `export * from './schemas'
|
|
1320
|
+
`
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
// ============================================
|
|
1324
|
+
// 更新 app.ts 和 shared/modules/index.ts
|
|
1325
|
+
// ============================================
|
|
1326
|
+
|
|
1327
|
+
function updateAppTs(name: string, prefix: string = ''): ModifiedFileResult {
|
|
1328
|
+
const appPath = path.join(templateDir, 'src/server/app.ts')
|
|
1329
|
+
if (!fs.existsSync(appPath)) {
|
|
1330
|
+
console.log(`⚠️ app.ts 不存在: ${appPath}`)
|
|
1331
|
+
return { success: false }
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
const camelName = toCamelCase(name)
|
|
1335
|
+
const kebabName = toKebabCase(name)
|
|
1336
|
+
const modulePath = prefix ? `module-${prefix}-${kebabName}` : `module-${kebabName}`
|
|
1337
|
+
const routeFileName = prefix ? `${prefix}-${kebabName}-routes` : `${kebabName}-routes`
|
|
1338
|
+
|
|
1339
|
+
let content = fs.readFileSync(appPath, 'utf-8')
|
|
1340
|
+
|
|
1341
|
+
if (content.includes(`${camelName}Routes`)) {
|
|
1342
|
+
console.log(`⚠️ app.ts 已经包含 ${camelName}Routes`)
|
|
1343
|
+
return { success: false }
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
const diff: string[] = []
|
|
1347
|
+
let addedLines = 0
|
|
1348
|
+
|
|
1349
|
+
// 添加导入语句
|
|
1350
|
+
const importRegex = /(import \{[^}]+\} from '\.\/module-[^']+'\n)/
|
|
1351
|
+
const lastImportMatch = content.match(importRegex)
|
|
1352
|
+
|
|
1353
|
+
const importLine = `import { ${camelName}Routes } from './${modulePath}/routes/${routeFileName}'`
|
|
1354
|
+
|
|
1355
|
+
if (lastImportMatch) {
|
|
1356
|
+
const lastImportIndex = content.lastIndexOf(lastImportMatch[0]) + lastImportMatch[0].length
|
|
1357
|
+
content = content.slice(0, lastImportIndex) + `${importLine}\n` + content.slice(lastImportIndex)
|
|
1358
|
+
diff.push(`+ ${importLine}`)
|
|
1359
|
+
addedLines++
|
|
1360
|
+
} else {
|
|
1361
|
+
const firstImportEnd = content.indexOf('\n', content.indexOf('import'))
|
|
1362
|
+
content =
|
|
1363
|
+
content.slice(0, firstImportEnd + 1) + `${importLine}\n` + content.slice(firstImportEnd + 1)
|
|
1364
|
+
diff.push(`+ ${importLine}`)
|
|
1365
|
+
addedLines++
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
// 添加路由注册
|
|
1369
|
+
const routeRegex = /(\.route\('\/api', [a-zA-Z]+Routes\)\n)/
|
|
1370
|
+
const lastRouteMatch = content.match(routeRegex)
|
|
1371
|
+
|
|
1372
|
+
const routeLine = ` .route('/api', ${camelName}Routes)`
|
|
1373
|
+
|
|
1374
|
+
if (lastRouteMatch) {
|
|
1375
|
+
const lastRouteIndex = content.lastIndexOf(lastRouteMatch[0]) + lastRouteMatch[0].length
|
|
1376
|
+
content = content.slice(0, lastRouteIndex) + `${routeLine}\n` + content.slice(lastRouteIndex)
|
|
1377
|
+
diff.push(`+ ${routeLine}`)
|
|
1378
|
+
addedLines++
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
fs.writeFileSync(appPath, content)
|
|
1382
|
+
return { success: true, diff: diff.join('\n'), addedLines }
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
function updateSharedModulesIndex(name: string, options: CreateOptions): ModifiedFileResult {
|
|
1386
|
+
const indexPath = path.join(templateDir, 'src/shared/modules/index.ts')
|
|
1387
|
+
if (!fs.existsSync(indexPath)) {
|
|
1388
|
+
console.log(`⚠️ shared/modules/index.ts 不存在: ${indexPath}`)
|
|
1389
|
+
return { success: false }
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
const pascalName = toPascalCase(name)
|
|
1393
|
+
const kebabName = toKebabCase(name)
|
|
1394
|
+
const moduleExportName = options.prefix ? `${options.prefix}-${kebabName}` : kebabName
|
|
1395
|
+
|
|
1396
|
+
let content = fs.readFileSync(indexPath, 'utf-8')
|
|
1397
|
+
|
|
1398
|
+
if (content.includes(`from './${moduleExportName}'`)) {
|
|
1399
|
+
console.log(`⚠️ shared/modules/index.ts 已经包含 ${moduleExportName} 模块`)
|
|
1400
|
+
return { success: false }
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
let exports: string
|
|
1404
|
+
|
|
1405
|
+
if (options.withSSE) {
|
|
1406
|
+
exports = `
|
|
1407
|
+
export {
|
|
1408
|
+
${pascalName}EventSchema,
|
|
1409
|
+
${pascalName}SubscriptionSchema,
|
|
1410
|
+
type ${pascalName}Event,
|
|
1411
|
+
type ${pascalName}Subscription,
|
|
1412
|
+
} from './${moduleExportName}'`
|
|
1413
|
+
} else if (options.withWebSocket) {
|
|
1414
|
+
exports = `
|
|
1415
|
+
export {
|
|
1416
|
+
${pascalName}MessageSchema,
|
|
1417
|
+
${pascalName}ConnectionSchema,
|
|
1418
|
+
WebSocketStatusSchema,
|
|
1419
|
+
type ${pascalName}Message,
|
|
1420
|
+
type ${pascalName}Connection,
|
|
1421
|
+
type WebSocketStatus,
|
|
1422
|
+
} from './${moduleExportName}'`
|
|
1423
|
+
} else {
|
|
1424
|
+
exports = `
|
|
1425
|
+
export {
|
|
1426
|
+
${pascalName}Schema,
|
|
1427
|
+
Create${pascalName}Schema,
|
|
1428
|
+
Update${pascalName}Schema,
|
|
1429
|
+
${pascalName}ListSchema,
|
|
1430
|
+
DeleteResultSchema,
|
|
1431
|
+
type ${pascalName},
|
|
1432
|
+
type Create${pascalName}Input,
|
|
1433
|
+
type Update${pascalName}Input,
|
|
1434
|
+
type DeleteResult,
|
|
1435
|
+
} from './${moduleExportName}'`
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
const addedLines = exports.split('\n').length
|
|
1439
|
+
const diff = exports
|
|
1440
|
+
.trim()
|
|
1441
|
+
.split('\n')
|
|
1442
|
+
.map(line => `+ ${line}`)
|
|
1443
|
+
.join('\n')
|
|
1444
|
+
|
|
1445
|
+
content = content.trimEnd() + exports + '\n'
|
|
1446
|
+
|
|
1447
|
+
fs.writeFileSync(indexPath, content)
|
|
1448
|
+
return { success: true, diff, addedLines }
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
// ============================================
|
|
1452
|
+
// 主创建函数
|
|
1453
|
+
// ============================================
|
|
1454
|
+
|
|
1455
|
+
function createModule(options: CreateOptions): CreatedFile[] {
|
|
1456
|
+
const { name, withDatabase, withSSE, withWebSocket, prefix } = options
|
|
1457
|
+
const kebabName = toKebabCase(name)
|
|
1458
|
+
const moduleName = prefix ? `module-${prefix}-${kebabName}` : `module-${kebabName}`
|
|
1459
|
+
const moduleDir = path.join(templateDir, 'src/server', moduleName)
|
|
1460
|
+
const sharedModuleDir = path.join(
|
|
1461
|
+
templateDir,
|
|
1462
|
+
'src/shared/modules',
|
|
1463
|
+
prefix ? `${prefix}-${kebabName}` : kebabName
|
|
1464
|
+
)
|
|
1465
|
+
|
|
1466
|
+
const createdFiles: CreatedFile[] = []
|
|
1467
|
+
|
|
1468
|
+
if (fs.existsSync(moduleDir)) {
|
|
1469
|
+
console.log(`⚠️ 模块已存在: ${moduleDir}`)
|
|
1470
|
+
return []
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
// 创建目录结构
|
|
1474
|
+
const routesDir = path.join(moduleDir, 'routes')
|
|
1475
|
+
const servicesDir = path.join(moduleDir, 'services')
|
|
1476
|
+
const testsDir = path.join(moduleDir, '__tests__')
|
|
1477
|
+
|
|
1478
|
+
fs.mkdirSync(routesDir, { recursive: true })
|
|
1479
|
+
fs.mkdirSync(servicesDir, { recursive: true })
|
|
1480
|
+
fs.mkdirSync(testsDir, { recursive: true })
|
|
1481
|
+
fs.mkdirSync(sharedModuleDir, { recursive: true })
|
|
1482
|
+
|
|
1483
|
+
// 选择模板
|
|
1484
|
+
let schemaTemplate: string
|
|
1485
|
+
let routeTemplate: string
|
|
1486
|
+
let serviceTemplate: string
|
|
1487
|
+
let serviceTestTemplate: string
|
|
1488
|
+
let routeTestTemplate: string
|
|
1489
|
+
|
|
1490
|
+
if (withSSE) {
|
|
1491
|
+
schemaTemplate = generateSSESchema(name)
|
|
1492
|
+
routeTemplate = generateSSERouteTemplate(name)
|
|
1493
|
+
serviceTemplate = generateSSEServiceTemplate(name)
|
|
1494
|
+
serviceTestTemplate = generateSSEServiceTestTemplate(name)
|
|
1495
|
+
routeTestTemplate = generateSSERouteTestTemplate(name)
|
|
1496
|
+
} else if (withWebSocket) {
|
|
1497
|
+
schemaTemplate = generateWebSocketSchema(name)
|
|
1498
|
+
routeTemplate = generateWebSocketRouteTemplate(name)
|
|
1499
|
+
serviceTemplate = generateWebSocketServiceTemplate(name)
|
|
1500
|
+
serviceTestTemplate = generateWebSocketServiceTestTemplate(name)
|
|
1501
|
+
routeTestTemplate = generateWebSocketRouteTestTemplate(name)
|
|
1502
|
+
} else if (withDatabase) {
|
|
1503
|
+
schemaTemplate = generateBasicSchema(name)
|
|
1504
|
+
routeTemplate = generateDatabaseRouteTemplate(name)
|
|
1505
|
+
serviceTemplate = generateDatabaseServiceTemplate(name)
|
|
1506
|
+
serviceTestTemplate = generateBasicServiceTestTemplate(name)
|
|
1507
|
+
routeTestTemplate = generateBasicRouteTestTemplate(name)
|
|
1508
|
+
} else {
|
|
1509
|
+
schemaTemplate = generateBasicSchema(name)
|
|
1510
|
+
routeTemplate = generateBasicRouteTemplate(name)
|
|
1511
|
+
serviceTemplate = generateBasicServiceTemplate(name)
|
|
1512
|
+
serviceTestTemplate = generateBasicServiceTestTemplate(name)
|
|
1513
|
+
routeTestTemplate = generateBasicRouteTestTemplate(name)
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
// 创建文件
|
|
1517
|
+
const schemaFile = path.join(sharedModuleDir, 'schemas.ts')
|
|
1518
|
+
fs.writeFileSync(schemaFile, schemaTemplate)
|
|
1519
|
+
createdFiles.push({ path: schemaFile, type: 'created' })
|
|
1520
|
+
|
|
1521
|
+
const sharedIndexFile = path.join(sharedModuleDir, 'index.ts')
|
|
1522
|
+
fs.writeFileSync(sharedIndexFile, generateModuleIndexTemplate())
|
|
1523
|
+
createdFiles.push({ path: sharedIndexFile, type: 'created' })
|
|
1524
|
+
|
|
1525
|
+
const routeFileName = prefix ? `${prefix}-${kebabName}-routes` : `${kebabName}-routes`
|
|
1526
|
+
const routeFile = path.join(routesDir, `${routeFileName}.ts`)
|
|
1527
|
+
fs.writeFileSync(routeFile, routeTemplate)
|
|
1528
|
+
createdFiles.push({ path: routeFile, type: 'created' })
|
|
1529
|
+
|
|
1530
|
+
const serviceFileName = prefix ? `${prefix}-${kebabName}-service` : `${kebabName}-service`
|
|
1531
|
+
const serviceFile = path.join(servicesDir, `${serviceFileName}.ts`)
|
|
1532
|
+
fs.writeFileSync(serviceFile, serviceTemplate)
|
|
1533
|
+
createdFiles.push({ path: serviceFile, type: 'created' })
|
|
1534
|
+
|
|
1535
|
+
const serviceTestFile = path.join(testsDir, `${serviceFileName}.test.ts`)
|
|
1536
|
+
fs.writeFileSync(serviceTestFile, serviceTestTemplate)
|
|
1537
|
+
createdFiles.push({ path: serviceTestFile, type: 'created' })
|
|
1538
|
+
|
|
1539
|
+
const routeTestFile = path.join(testsDir, `${routeFileName}.test.ts`)
|
|
1540
|
+
fs.writeFileSync(routeTestFile, routeTestTemplate)
|
|
1541
|
+
createdFiles.push({ path: routeTestFile, type: 'created' })
|
|
1542
|
+
|
|
1543
|
+
// 更新 app.ts
|
|
1544
|
+
const appResult = updateAppTs(name, prefix)
|
|
1545
|
+
if (appResult.success) {
|
|
1546
|
+
createdFiles.push({
|
|
1547
|
+
path: path.join(templateDir, 'src/server/app.ts'),
|
|
1548
|
+
type: 'modified',
|
|
1549
|
+
diff: appResult.diff,
|
|
1550
|
+
lineCount: appResult.addedLines,
|
|
1551
|
+
})
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
// 更新 shared/modules/index.ts
|
|
1555
|
+
const indexResult = updateSharedModulesIndex(name, options)
|
|
1556
|
+
if (indexResult.success) {
|
|
1557
|
+
createdFiles.push({
|
|
1558
|
+
path: path.join(templateDir, 'src/shared/modules/index.ts'),
|
|
1559
|
+
type: 'modified',
|
|
1560
|
+
diff: indexResult.diff,
|
|
1561
|
+
lineCount: indexResult.addedLines,
|
|
1562
|
+
})
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
return createdFiles
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
function parseArgs(args: string[]): CreateOptions {
|
|
1569
|
+
const options: CreateOptions = {
|
|
1570
|
+
name: '',
|
|
1571
|
+
withDatabase: false,
|
|
1572
|
+
withSSE: false,
|
|
1573
|
+
withWebSocket: false,
|
|
1574
|
+
prefix: '',
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
for (let i = 0; i < args.length; i++) {
|
|
1578
|
+
const arg = args[i]
|
|
1579
|
+
|
|
1580
|
+
if (arg === '--with-db') {
|
|
1581
|
+
options.withDatabase = true
|
|
1582
|
+
} else if (arg === '--sse') {
|
|
1583
|
+
options.withSSE = true
|
|
1584
|
+
} else if (arg === '--ws') {
|
|
1585
|
+
options.withWebSocket = true
|
|
1586
|
+
} else if (arg === '--admin') {
|
|
1587
|
+
options.prefix = 'admin'
|
|
1588
|
+
} else if (!arg.startsWith('--')) {
|
|
1589
|
+
options.name = arg
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
return options
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
function main(): void {
|
|
1597
|
+
const args = process.argv.slice(2)
|
|
1598
|
+
|
|
1599
|
+
if (args.length < 1) {
|
|
1600
|
+
console.log(`
|
|
1601
|
+
用法:
|
|
1602
|
+
npm run create:module <name> [options]
|
|
1603
|
+
|
|
1604
|
+
选项:
|
|
1605
|
+
--with-db 包含数据库操作
|
|
1606
|
+
--sse SSE (Server-Sent Events) 模板
|
|
1607
|
+
--ws WebSocket 模板
|
|
1608
|
+
--admin 创建管理后台模块 (module-admin-xxx)
|
|
1609
|
+
|
|
1610
|
+
示例:
|
|
1611
|
+
npm run create:module product # 基础模板(无数据库)
|
|
1612
|
+
npm run create:module product --with-db # 数据库模板
|
|
1613
|
+
npm run create:module notifications --sse # SSE 模板
|
|
1614
|
+
npm run create:module chat --ws # WebSocket 模板
|
|
1615
|
+
npm run create:module users --admin # 管理后台模块 (module-admin-users)
|
|
1616
|
+
npm run create:module reports --admin --with-db # 管理后台数据库模块
|
|
1617
|
+
|
|
1618
|
+
自动操作:
|
|
1619
|
+
1. 创建模块目录结构 (src/server/module-{name}/ 或 src/server/module-admin-{name}/)
|
|
1620
|
+
2. 创建 schema 文件 (src/shared/modules/{name}/schemas.ts)
|
|
1621
|
+
3. 更新 app.ts 导入和注册路由
|
|
1622
|
+
4. 更新 shared/modules/index.ts 导出
|
|
1623
|
+
`)
|
|
1624
|
+
process.exit(1)
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
const options = parseArgs(args)
|
|
1628
|
+
|
|
1629
|
+
if (!options.name) {
|
|
1630
|
+
console.log(`❌ 请提供模块名称`)
|
|
1631
|
+
process.exit(1)
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
// 检查互斥选项
|
|
1635
|
+
const selectedOptions = [options.withDatabase, options.withSSE, options.withWebSocket].filter(
|
|
1636
|
+
Boolean
|
|
1637
|
+
).length
|
|
1638
|
+
if (selectedOptions > 1) {
|
|
1639
|
+
console.log(`❌ 只能选择一个模板类型: --with-db, --sse, 或 --ws`)
|
|
1640
|
+
process.exit(1)
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
const createdFiles = createModule(options)
|
|
1644
|
+
|
|
1645
|
+
if (createdFiles.length > 0) {
|
|
1646
|
+
const created = createdFiles.filter(f => f.type === 'created')
|
|
1647
|
+
const modified = createdFiles.filter(f => f.type === 'modified')
|
|
1648
|
+
|
|
1649
|
+
let templateType = '基础模板(无数据库)'
|
|
1650
|
+
if (options.withDatabase) templateType = '数据库模板'
|
|
1651
|
+
if (options.withSSE) templateType = 'SSE 模板'
|
|
1652
|
+
if (options.withWebSocket) templateType = 'WebSocket 模板'
|
|
1653
|
+
|
|
1654
|
+
const moduleType = options.prefix ? `管理后台模块 (${options.prefix})` : '普通模块'
|
|
1655
|
+
|
|
1656
|
+
console.log(`\n✅ 模块创建成功!`)
|
|
1657
|
+
console.log(` 模块类型: ${moduleType}`)
|
|
1658
|
+
console.log(` 模板类型: ${templateType}\n`)
|
|
1659
|
+
|
|
1660
|
+
if (created.length > 0) {
|
|
1661
|
+
console.log('📄 创建的文件:')
|
|
1662
|
+
created.forEach(file => {
|
|
1663
|
+
const relativePath = path.relative(templateDir, file.path)
|
|
1664
|
+
console.log(` ${relativePath}`)
|
|
1665
|
+
})
|
|
1666
|
+
console.log()
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
if (modified.length > 0) {
|
|
1670
|
+
console.log('✏️ 修改的文件:')
|
|
1671
|
+
modified.forEach(file => {
|
|
1672
|
+
const relativePath = path.relative(templateDir, file.path)
|
|
1673
|
+
const lineInfo = file.lineCount ? ` (+${file.lineCount} 行)` : ''
|
|
1674
|
+
console.log(` ${relativePath}${lineInfo}`)
|
|
1675
|
+
if (file.diff) {
|
|
1676
|
+
console.log()
|
|
1677
|
+
console.log(' Diff:')
|
|
1678
|
+
file.diff.split('\n').forEach(line => {
|
|
1679
|
+
console.log(` ${line}`)
|
|
1680
|
+
})
|
|
1681
|
+
}
|
|
1682
|
+
console.log()
|
|
1683
|
+
})
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
// 显示客户端使用示例
|
|
1687
|
+
console.log('💻 客户端调用示例:')
|
|
1688
|
+
console.log(generateClientUsageExample(options.name, options))
|
|
1689
|
+
|
|
1690
|
+
console.log('📚 相关文档:')
|
|
1691
|
+
console.log(' - .claude/rules/31-client-services.md - 客户端服务使用规范')
|
|
1692
|
+
if (options.withSSE) {
|
|
1693
|
+
console.log(' - .claude/rules/51-sse.md - SSE 开发规范')
|
|
1694
|
+
console.log(' - src/shared/hooks/useSSE.ts - React Hook 封装')
|
|
1695
|
+
console.log(' - src/shared/core/sse-client.ts - SSE 客户端实现')
|
|
1696
|
+
} else if (options.withWebSocket) {
|
|
1697
|
+
console.log(' - .claude/rules/50-websocket.md - WebSocket 开发规范')
|
|
1698
|
+
console.log(' - src/shared/hooks/useWebSocket.ts - React Hook 封装')
|
|
1699
|
+
console.log(' - src/shared/core/ws-client.ts - WebSocket 客户端实现')
|
|
1700
|
+
} else {
|
|
1701
|
+
console.log(' - .claude/rules/10-api-type-inference.md - API 类型推导规范')
|
|
1702
|
+
}
|
|
1703
|
+
console.log()
|
|
1704
|
+
|
|
1705
|
+
console.log('🚀 下一步:')
|
|
1706
|
+
if (options.withDatabase) {
|
|
1707
|
+
console.log(' 1. 在 src/server/db/schema/ 创建数据库表定义')
|
|
1708
|
+
console.log(' 2. 实现服务层数据库操作')
|
|
1709
|
+
} else if (options.withSSE) {
|
|
1710
|
+
console.log(' 1. 实现 SSE 事件触发逻辑')
|
|
1711
|
+
console.log(' 2. 添加事件过滤逻辑')
|
|
1712
|
+
} else if (options.withWebSocket) {
|
|
1713
|
+
console.log(' 1. 实现 WebSocket 消息处理')
|
|
1714
|
+
console.log(' 2. 添加房间管理逻辑')
|
|
1715
|
+
} else {
|
|
1716
|
+
console.log(' 1. 实现服务层业务逻辑')
|
|
1717
|
+
console.log(' 2. 添加数据存储')
|
|
1718
|
+
}
|
|
1719
|
+
console.log(' 3. 运行测试: npm test')
|
|
1720
|
+
console.log()
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
main()
|