create-fullstack-scaffold 0.4.9 → 0.4.10
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/dist/cli/index.js +499 -100
- package/dist/cli/index.js.map +1 -1
- package/package.json +2 -2
- package/template/CLAUDE.md +18 -6
- package/template/drizzle/0004_add_merchants_products.sql +30 -0
- package/template/drizzle/meta/_journal.json +7 -0
- package/template/eslint-rules/module-boundary.js +6 -2
- package/template/eslint-rules/no-cross-module-service-import.js +105 -0
- package/template/eslint-rules/no-disable-type-safe-client.js +5 -0
- package/template/eslint.config.js +9 -0
- package/template/lint-scripts/config/project.config.ts +21 -0
- package/template/lint-scripts/validate-all.ts +48 -12
- package/template/lint-scripts/validators/index.ts +29 -0
- package/template/lint-scripts/validators/module-public-api.validator.ts +103 -0
- package/template/lint-scripts/validators/schema-uniqueness.validator.ts +147 -0
- package/template/modules.config.ts +2 -0
- package/template/package.json +2 -0
- package/template/playwright.config.ts +14 -0
- package/template/src/admin/App.tsx +40 -3
- package/template/src/admin/components/LanguageSwitcher.tsx +22 -0
- package/template/src/admin/components/NotificationDrawer.tsx +108 -38
- package/template/src/admin/components/StatsCard.tsx +3 -1
- package/template/src/admin/components/ThemeToggle.tsx +28 -0
- package/template/src/admin/hooks/useRoles.ts +2 -2
- package/template/src/admin/i18n/index.ts +18 -0
- package/template/src/admin/i18n/locales/en-US.json +381 -0
- package/template/src/admin/i18n/locales/zh-CN.json +381 -0
- package/template/src/admin/i18n/useLanguage.ts +21 -0
- package/template/src/admin/layouts/Header.tsx +37 -16
- package/template/src/admin/layouts/Layout.tsx +7 -3
- package/template/src/admin/layouts/Sidebar.tsx +70 -68
- package/template/src/admin/main.tsx +1 -0
- package/template/src/admin/pages/ContentPage.tsx +68 -56
- package/template/src/admin/pages/DashboardPage.tsx +82 -76
- package/template/src/admin/pages/DisputesPage.tsx +61 -53
- package/template/src/admin/pages/LoginPage.tsx +41 -33
- package/template/src/admin/pages/OrdersPage.tsx +69 -64
- package/template/src/admin/pages/PermissionsPage.tsx +10 -8
- package/template/src/admin/pages/PluginDashboardPage.tsx +3 -1
- package/template/src/admin/pages/PluginManagementPage.tsx +3 -1
- package/template/src/admin/pages/RegisterPage.tsx +18 -16
- package/template/src/admin/pages/RolesPage.tsx +51 -49
- package/template/src/admin/pages/SettingsPage.tsx +22 -22
- package/template/src/admin/pages/SystemLogsPage.tsx +32 -30
- package/template/src/admin/pages/TicketsPage.tsx +67 -59
- package/template/src/admin/pages/UsersPage.tsx +63 -53
- package/template/src/admin/pages/__tests__/RolesPage.test.tsx +2 -2
- package/template/src/admin/stores/themeStore.ts +32 -0
- package/template/src/client/index.css +73 -0
- package/template/src/merchant/App.tsx +2 -0
- package/template/src/merchant/components/MerchantGuard.tsx +2 -6
- package/template/src/merchant/components/__tests__/MerchantGuard.test.tsx +117 -0
- package/template/src/merchant/layouts/Header.tsx +2 -2
- package/template/src/merchant/pages/DashboardPage.tsx +2 -2
- package/template/src/merchant/pages/DisputesPage.tsx +14 -10
- package/template/src/merchant/pages/LoginPage.tsx +49 -0
- package/template/src/merchant/pages/OrdersPage.tsx +17 -10
- package/template/src/merchant/pages/ProductsPage.tsx +31 -8
- package/template/src/merchant/pages/SettingsPage.tsx +3 -7
- package/template/src/merchant/stores/__tests__/merchantStore.test.ts +317 -0
- package/template/src/merchant/stores/merchantStore.ts +24 -27
- package/template/src/server/db/schema/index.ts +2 -0
- package/template/src/server/db/schema/merchants.ts +26 -0
- package/template/src/server/db/schema/products.ts +25 -0
- package/template/src/server/db/test-setup.ts +237 -0
- package/template/src/server/middleware/__tests__/audit-log.test.ts +144 -0
- package/template/src/server/middleware/__tests__/cors.test.ts +84 -0
- package/template/src/server/middleware/__tests__/logger.test.ts +117 -0
- package/template/src/server/middleware/__tests__/rate-limit.test.ts +73 -0
- package/template/src/server/middleware/__tests__/realtime-env.test.ts +56 -0
- package/template/src/server/middleware/__tests__/tenant-isolation.test.ts +150 -0
- package/template/src/server/module-admin/__tests__/admin-routes.test.ts +1 -1
- package/template/src/server/module-admin/module.ts +4 -0
- package/template/src/server/module-admin/routes/admin-notification-routes.ts +3 -3
- package/template/src/server/module-admin/routes/user-management-routes.ts +2 -2
- package/template/src/server/module-auth/__tests__/auth-routes.test.ts +315 -0
- package/template/src/server/module-auth/__tests__/profile-routes.test.ts +83 -0
- package/template/src/server/module-auth/module.ts +2 -0
- package/template/src/server/module-content/module.ts +1 -0
- package/template/src/server/module-content/routes/content-routes.ts +2 -2
- package/template/src/server/module-dispute/module.ts +1 -0
- package/template/src/server/module-dispute/routes/dispute-routes.ts +2 -2
- package/template/src/server/module-merchant/__tests__/merchant-routes.test.ts +218 -0
- package/template/src/server/module-merchant/__tests__/merchant-service.test.ts +183 -0
- package/template/src/server/module-merchant/index.ts +10 -0
- package/template/src/server/module-merchant/module.ts +33 -0
- package/template/src/server/module-merchant/routes/merchant-routes.ts +138 -0
- package/template/src/server/module-merchant/services/merchant-service.ts +292 -0
- package/template/src/server/module-notifications/index.ts +18 -0
- package/template/src/server/module-notifications/module.ts +2 -0
- package/template/src/server/module-order/module.ts +1 -0
- package/template/src/server/module-order/routes/order-routes.ts +2 -2
- package/template/src/server/module-permission/routes/role-routes.ts +3 -3
- package/template/src/server/module-plugin/__tests__/admin-category-service.test.ts +181 -0
- package/template/src/server/module-plugin/__tests__/admin-plugin-service.test.ts +277 -0
- package/template/src/server/module-plugin/__tests__/admin-stats-service.test.ts +170 -0
- package/template/src/server/module-plugin/__tests__/plugin-review-service.test.ts +260 -0
- package/template/src/server/module-plugin/__tests__/plugin-seed-service.test.ts +170 -0
- package/template/src/server/module-plugin/module.ts +3 -0
- package/template/src/server/module-tenant/__tests__/tenant-routes.test.ts +142 -0
- package/template/src/server/module-tenant/__tests__/tenant-service.test.ts +152 -0
- package/template/src/server/module-tenant/module.ts +1 -0
- package/template/src/server/module-tenant/services/tenant-service.ts +1 -37
- package/template/src/server/module-ticket/module.ts +1 -0
- package/template/src/server/module-ticket/routes/ticket-routes.ts +2 -2
- package/template/src/server/module-todos/module.ts +3 -0
- package/template/src/server/route-registry.ts +4 -0
- package/template/src/server/utils/__tests__/date.test.ts +130 -0
- package/template/src/server/utils/__tests__/env.test.ts +25 -0
- package/template/src/server/utils/__tests__/generate.test.ts +82 -0
- package/template/src/server/utils/__tests__/id-helpers.test.ts +29 -0
- package/template/src/server/utils/__tests__/json.test.ts +49 -0
- package/template/src/server/utils/__tests__/permission-utils.test.ts +26 -0
- package/template/src/server/utils/__tests__/uuid.test.ts +25 -0
- package/template/src/shared/core/module-manifest.ts +15 -0
- package/template/src/shared/modules/admin/schemas.ts +1 -1
- package/template/src/shared/modules/content/schemas.ts +2 -2
- package/template/src/shared/modules/dispute/schemas.ts +2 -2
- package/template/src/shared/modules/index.ts +181 -1
- package/template/src/shared/modules/merchant/index.ts +12 -0
- package/template/src/shared/modules/merchant/schemas.ts +63 -0
- package/template/src/shared/modules/order/schemas.ts +2 -2
- package/template/src/shared/modules/permission/index.ts +1 -1
- package/template/src/shared/modules/permission/schemas.ts +1 -1
- package/template/src/shared/modules/role/schemas.ts +2 -2
- package/template/src/shared/modules/ticket/schemas.ts +2 -2
- package/template/src/shared/schemas/index.ts +74 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-fullstack-scaffold",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.10",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"create-fullstack-scaffold": "dist/cli/index.js"
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
"validate:modules": "npx tsx src/server/core/module-loader.ts",
|
|
87
87
|
"check:refs": "node --import tsx/esm lint-scripts/check-refs.ts",
|
|
88
88
|
"create:module": "node --import tsx/esm scripts/create-template.ts",
|
|
89
|
-
"postinstall": "patch-package",
|
|
89
|
+
"postinstall": "node -e \"try{require.resolve('patch-package')&&require('child_process').execSync('npx patch-package',{stdio:'pipe'})}catch(e){}\"",
|
|
90
90
|
"prepare": "husky",
|
|
91
91
|
"db:generate": "drizzle-kit generate",
|
|
92
92
|
"db:migrate": "drizzle-kit migrate",
|
package/template/CLAUDE.md
CHANGED
|
@@ -33,6 +33,17 @@ src/
|
|
|
33
33
|
│ ├── hooks/ # Custom hooks
|
|
34
34
|
│ ├── pages/ # Page components
|
|
35
35
|
│ └── App.tsx
|
|
36
|
+
├── merchant/ # Merchant dashboard (Ant Design)
|
|
37
|
+
│ ├── components/ # Merchant components
|
|
38
|
+
│ ├── stores/ # Merchant state (merchantStore)
|
|
39
|
+
│ ├── pages/ # Merchant pages
|
|
40
|
+
│ ├── layouts/ # Merchant layouts
|
|
41
|
+
│ └── App.tsx
|
|
42
|
+
├── admin/ # Admin dashboard (Ant Design)
|
|
43
|
+
│ ├── components/ # Admin components
|
|
44
|
+
│ ├── stores/ # Admin state
|
|
45
|
+
│ ├── pages/ # Admin pages
|
|
46
|
+
│ └── App.tsx
|
|
36
47
|
├── server/ # Hono backend
|
|
37
48
|
│ ├── module-todos/ # Todo module
|
|
38
49
|
│ ├── module-chat/ # WebSocket chat module
|
|
@@ -173,12 +184,12 @@ Each module under `src/server/module-*/` has a `module.ts` manifest declaring:
|
|
|
173
184
|
|
|
174
185
|
#### Module Categories
|
|
175
186
|
|
|
176
|
-
| Category | Modules
|
|
177
|
-
| ------------- |
|
|
178
|
-
| core | todos
|
|
179
|
-
| communication | chat, notifications
|
|
180
|
-
| business | order, ticket, dispute, content
|
|
181
|
-
| system | permission, admin, captcha, file
|
|
187
|
+
| Category | Modules |
|
|
188
|
+
| ------------- | ----------------------------------------- |
|
|
189
|
+
| core | todos |
|
|
190
|
+
| communication | chat, notifications |
|
|
191
|
+
| business | order, ticket, dispute, content, merchant |
|
|
192
|
+
| system | permission, admin, captcha, file |
|
|
182
193
|
|
|
183
194
|
#### Dependency Graph
|
|
184
195
|
|
|
@@ -194,6 +205,7 @@ order ──→ permission
|
|
|
194
205
|
ticket ──→ permission
|
|
195
206
|
dispute ──→ permission
|
|
196
207
|
content ──→ permission
|
|
208
|
+
merchant ──→ auth + permission
|
|
197
209
|
```
|
|
198
210
|
|
|
199
211
|
#### Validation
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
-- Create merchants table
|
|
2
|
+
CREATE TABLE `merchants` (
|
|
3
|
+
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
|
4
|
+
`user_id` text NOT NULL,
|
|
5
|
+
`tenant_id` integer NOT NULL,
|
|
6
|
+
`business_name` text NOT NULL,
|
|
7
|
+
`business_type` text NOT NULL,
|
|
8
|
+
`status` text NOT NULL DEFAULT 'active',
|
|
9
|
+
`description` text,
|
|
10
|
+
`phone` text,
|
|
11
|
+
`email` text,
|
|
12
|
+
`address` text,
|
|
13
|
+
`password` text NOT NULL,
|
|
14
|
+
`created_at` text NOT NULL,
|
|
15
|
+
`updated_at` text NOT NULL
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
-- Create products table
|
|
19
|
+
CREATE TABLE `products` (
|
|
20
|
+
`id` text PRIMARY KEY NOT NULL,
|
|
21
|
+
`merchant_id` integer NOT NULL,
|
|
22
|
+
`name` text NOT NULL,
|
|
23
|
+
`description` text,
|
|
24
|
+
`price` real NOT NULL,
|
|
25
|
+
`status` text NOT NULL DEFAULT 'active',
|
|
26
|
+
`stock` integer NOT NULL DEFAULT 0,
|
|
27
|
+
`image_url` text,
|
|
28
|
+
`created_at` integer NOT NULL,
|
|
29
|
+
`updated_at` integer NOT NULL
|
|
30
|
+
);
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* ESLint 规则:模块边界约束
|
|
7
7
|
*
|
|
8
8
|
* 规则:
|
|
9
|
-
* 1. client、cli、admin
|
|
9
|
+
* 1. client、cli、admin、merchant、tenant 模块之间不能存在直接引用关系
|
|
10
10
|
* 2. 模块间共享代码必须通过 shared 目录
|
|
11
11
|
* 3. 违规引用将报错并提示使用 shared 中转
|
|
12
12
|
*/
|
|
@@ -15,7 +15,7 @@ export const moduleBoundary = {
|
|
|
15
15
|
meta: {
|
|
16
16
|
type: 'problem',
|
|
17
17
|
docs: {
|
|
18
|
-
description: 'Enforce boundary between client, cli and
|
|
18
|
+
description: 'Enforce boundary between client, cli, admin, merchant and tenant modules',
|
|
19
19
|
recommended: true,
|
|
20
20
|
},
|
|
21
21
|
messages: {
|
|
@@ -35,12 +35,16 @@ export const moduleBoundary = {
|
|
|
35
35
|
client: '/client/',
|
|
36
36
|
cli: '/cli/',
|
|
37
37
|
admin: '/admin/',
|
|
38
|
+
merchant: '/merchant/',
|
|
39
|
+
tenant: '/tenant/',
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
const moduleNames = {
|
|
41
43
|
client: 'client',
|
|
42
44
|
cli: 'cli',
|
|
43
45
|
admin: 'admin',
|
|
46
|
+
merchant: 'merchant',
|
|
47
|
+
tenant: 'tenant',
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
function getCurrentModule(filePath) {
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Prevent cross-module service internal imports
|
|
3
|
+
* @author create-biomimic-app
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict'
|
|
7
|
+
|
|
8
|
+
const FORBIDDEN_INTERNAL_PATHS = ['services', 'routes', 'middleware']
|
|
9
|
+
|
|
10
|
+
export const noCrossModuleServiceImport = {
|
|
11
|
+
meta: {
|
|
12
|
+
type: 'problem',
|
|
13
|
+
docs: {
|
|
14
|
+
description: 'Prevent modules from importing service internals from other modules',
|
|
15
|
+
recommended: true,
|
|
16
|
+
},
|
|
17
|
+
messages: {
|
|
18
|
+
crossModuleServiceImport:
|
|
19
|
+
"Cross-module service import detected: '{{importPath}}'. " +
|
|
20
|
+
"Import from '{{otherModule}}' should use its public API ('@server/module-{{otherModule}}') instead of internal paths.",
|
|
21
|
+
},
|
|
22
|
+
schema: [],
|
|
23
|
+
},
|
|
24
|
+
create(context) {
|
|
25
|
+
const filename = context.filename || context.getFilename()
|
|
26
|
+
|
|
27
|
+
const currentModuleMatch = filename.match(/\/(module-[^/]+)\//)
|
|
28
|
+
if (!currentModuleMatch) return {}
|
|
29
|
+
|
|
30
|
+
const currentModule = currentModuleMatch[1]
|
|
31
|
+
|
|
32
|
+
function checkImport(importPath, node) {
|
|
33
|
+
let normalizedPath = importPath
|
|
34
|
+
|
|
35
|
+
if (importPath.startsWith('.')) {
|
|
36
|
+
const baseParts = filename.split('/')
|
|
37
|
+
baseParts.pop()
|
|
38
|
+
const parts = [...baseParts]
|
|
39
|
+
|
|
40
|
+
for (const segment of importPath.split('/')) {
|
|
41
|
+
if (segment === '..') {
|
|
42
|
+
parts.pop()
|
|
43
|
+
} else if (segment !== '.') {
|
|
44
|
+
parts.push(segment)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
normalizedPath = parts.join('/')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const aliasMatch = normalizedPath.match(/@server\/(module-([^/]+))(?:\/(.+))?$/)
|
|
52
|
+
const relativeMatch = normalizedPath.match(/(module-([^/]+))\/(.+)$/)
|
|
53
|
+
|
|
54
|
+
const match = aliasMatch || relativeMatch
|
|
55
|
+
if (!match) return
|
|
56
|
+
|
|
57
|
+
const otherModuleName = aliasMatch ? aliasMatch[2] : relativeMatch[2]
|
|
58
|
+
const subPath = aliasMatch ? aliasMatch[3] : relativeMatch[3]
|
|
59
|
+
|
|
60
|
+
if (otherModuleName === currentModule.replace('module-', '')) return
|
|
61
|
+
if (!subPath) return
|
|
62
|
+
|
|
63
|
+
if (subPath === 'module.ts' || subPath === 'index.ts' || subPath === 'index') return
|
|
64
|
+
|
|
65
|
+
const firstSegment = subPath.split('/')[0]
|
|
66
|
+
if (FORBIDDEN_INTERNAL_PATHS.includes(firstSegment)) {
|
|
67
|
+
context.report({
|
|
68
|
+
node,
|
|
69
|
+
messageId: 'crossModuleServiceImport',
|
|
70
|
+
data: {
|
|
71
|
+
importPath,
|
|
72
|
+
otherModule: otherModuleName,
|
|
73
|
+
},
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
ImportDeclaration(node) {
|
|
80
|
+
const source = node.source
|
|
81
|
+
if (!source || source.type !== 'Literal') return
|
|
82
|
+
if (typeof source.value !== 'string') return
|
|
83
|
+
checkImport(source.value, node)
|
|
84
|
+
},
|
|
85
|
+
ImportExpression(node) {
|
|
86
|
+
if (node.source.type !== 'Literal') return
|
|
87
|
+
if (typeof node.source.value !== 'string') return
|
|
88
|
+
checkImport(node.source.value, node)
|
|
89
|
+
},
|
|
90
|
+
CallExpression(node) {
|
|
91
|
+
if (
|
|
92
|
+
node.callee.type === 'Identifier' &&
|
|
93
|
+
node.callee.name === 'require' &&
|
|
94
|
+
node.arguments.length > 0 &&
|
|
95
|
+
node.arguments[0].type === 'Literal' &&
|
|
96
|
+
typeof node.arguments[0].value === 'string'
|
|
97
|
+
) {
|
|
98
|
+
checkImport(node.arguments[0].value, node)
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export default noCrossModuleServiceImport
|
|
@@ -26,8 +26,13 @@ export const noDisableTypeSafeClient = {
|
|
|
26
26
|
const isMiddlewareTest =
|
|
27
27
|
filename.includes('/middleware/__tests__') || filename.includes('/middleware/')
|
|
28
28
|
|
|
29
|
+
const isFileUploadTest =
|
|
30
|
+
filename.includes('file-upload') ||
|
|
31
|
+
filename.includes('file-routes.test')
|
|
32
|
+
|
|
29
33
|
const isRouteTestFile =
|
|
30
34
|
!isMiddlewareTest &&
|
|
35
|
+
!isFileUploadTest &&
|
|
31
36
|
(filename.includes('-rpc.test.') ||
|
|
32
37
|
filename.includes('-route.test.') ||
|
|
33
38
|
(filename.includes('__tests__') && filename.includes('.test.')))
|
|
@@ -36,6 +36,9 @@ import { moduleBoundary } from './eslint-rules/module-boundary.js'
|
|
|
36
36
|
import { limitTypeComplexity } from './eslint-rules/limit-type-complexity.js'
|
|
37
37
|
import { requireAntdGenericTypes } from './eslint-rules/require-antd-generic-types.js'
|
|
38
38
|
import { noDeepRelativeImports } from './eslint-rules/no-deep-relative-imports.js'
|
|
39
|
+
import { noCrossModuleServiceImport } from './eslint-rules/no-cross-module-service-import.js'
|
|
40
|
+
import { noDisableTypeSafeClient } from './eslint-rules/no-disable-type-safe-client.js'
|
|
41
|
+
import { routeLocation } from './eslint-rules/route-location.js'
|
|
39
42
|
|
|
40
43
|
const localRules = {
|
|
41
44
|
rules: {
|
|
@@ -71,6 +74,9 @@ const localRules = {
|
|
|
71
74
|
'limit-type-complexity': limitTypeComplexity,
|
|
72
75
|
'require-antd-generic-types': requireAntdGenericTypes,
|
|
73
76
|
'no-deep-relative-imports': noDeepRelativeImports,
|
|
77
|
+
'no-cross-module-service-import': noCrossModuleServiceImport,
|
|
78
|
+
'no-disable-type-safe-client': noDisableTypeSafeClient,
|
|
79
|
+
'route-location': routeLocation,
|
|
74
80
|
},
|
|
75
81
|
}
|
|
76
82
|
|
|
@@ -118,6 +124,8 @@ export default tseslint.config(
|
|
|
118
124
|
'local-rules/flat-routes-services': 'error',
|
|
119
125
|
'local-rules/no-middleware-in-routes': 'error',
|
|
120
126
|
'local-rules/no-new-old-service-naming': 'error',
|
|
127
|
+
'local-rules/no-cross-module-service-import': 'error',
|
|
128
|
+
'local-rules/route-location': 'error',
|
|
121
129
|
'local-rules/limit-type-complexity': ['warn', { maxRouteChainLength: 15 }],
|
|
122
130
|
},
|
|
123
131
|
},
|
|
@@ -153,6 +161,7 @@ export default tseslint.config(
|
|
|
153
161
|
rules: {
|
|
154
162
|
'no-console': 'off',
|
|
155
163
|
'local-rules/require-type-safe-test-client': 'error',
|
|
164
|
+
'local-rules/no-disable-type-safe-client': 'error',
|
|
156
165
|
'local-rules/require-hono-chain-syntax': 'off',
|
|
157
166
|
},
|
|
158
167
|
},
|
|
@@ -16,6 +16,8 @@ import type {
|
|
|
16
16
|
TestQualityConfig,
|
|
17
17
|
ClientTestsConfig,
|
|
18
18
|
MdRefsConfig,
|
|
19
|
+
SchemaUniquenessConfig,
|
|
20
|
+
ModulePublicApiConfig,
|
|
19
21
|
} from '../validators/index.js'
|
|
20
22
|
|
|
21
23
|
// ============================================
|
|
@@ -338,6 +340,23 @@ export const consoleLogConfig: ConsoleLogConfig = {
|
|
|
338
340
|
checkDirs: ['src/server'],
|
|
339
341
|
}
|
|
340
342
|
|
|
343
|
+
// ============================================
|
|
344
|
+
// Schema 命名唯一性验证配置
|
|
345
|
+
// ============================================
|
|
346
|
+
export const schemaUniquenessConfig: SchemaUniquenessConfig = {
|
|
347
|
+
modulesDir: 'src/shared/modules',
|
|
348
|
+
checkDirs: ['src/shared/modules'],
|
|
349
|
+
ignoreDirs: ['node_modules', 'dist'],
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// ============================================
|
|
353
|
+
// 模块公共 API 验证配置
|
|
354
|
+
// ============================================
|
|
355
|
+
export const modulePublicApiConfig: ModulePublicApiConfig = {
|
|
356
|
+
serverDir: 'src/server',
|
|
357
|
+
checkDirs: ['src/server'],
|
|
358
|
+
}
|
|
359
|
+
|
|
341
360
|
// ============================================
|
|
342
361
|
// 统一导出
|
|
343
362
|
// ============================================
|
|
@@ -353,6 +372,8 @@ export const projectConfig = {
|
|
|
353
372
|
clientTests: clientTestsConfig,
|
|
354
373
|
mdRefs: mdRefsConfig,
|
|
355
374
|
consoleLog: consoleLogConfig,
|
|
375
|
+
schemaUniqueness: schemaUniquenessConfig,
|
|
376
|
+
modulePublicApi: modulePublicApiConfig,
|
|
356
377
|
} as const
|
|
357
378
|
|
|
358
379
|
export default projectConfig
|
|
@@ -28,6 +28,14 @@ import {
|
|
|
28
28
|
formatAPICoverageErrors,
|
|
29
29
|
} from './validators/api-coverage.validator.js'
|
|
30
30
|
import { validateConsoleLog, formatConsoleLogErrors } from './validators/console-log.validator.js'
|
|
31
|
+
import {
|
|
32
|
+
validateSchemaUniqueness,
|
|
33
|
+
formatSchemaUniquenessErrors,
|
|
34
|
+
} from './validators/schema-uniqueness.validator.js'
|
|
35
|
+
import {
|
|
36
|
+
validateModulePublicApi,
|
|
37
|
+
formatModulePublicApiErrors,
|
|
38
|
+
} from './validators/module-public-api.validator.js'
|
|
31
39
|
import projectConfig from './config/project.config.js'
|
|
32
40
|
|
|
33
41
|
interface ValidatorResult {
|
|
@@ -41,7 +49,7 @@ async function runAllValidators(): Promise<ValidatorResult[]> {
|
|
|
41
49
|
const results: ValidatorResult[] = []
|
|
42
50
|
|
|
43
51
|
// 1. TODO 验证
|
|
44
|
-
console.log('🔍 [1/
|
|
52
|
+
console.log('🔍 [1/14] Checking TODO/FIXME comments...')
|
|
45
53
|
const todoErrors = validateTodos(projectConfig.todos, rootPath)
|
|
46
54
|
results.push({
|
|
47
55
|
name: 'TODO/FIXME',
|
|
@@ -55,7 +63,7 @@ async function runAllValidators(): Promise<ValidatorResult[]> {
|
|
|
55
63
|
}
|
|
56
64
|
|
|
57
65
|
// 2. 敏感信息验证
|
|
58
|
-
console.log('🔍 [2/
|
|
66
|
+
console.log('🔍 [2/14] Checking for sensitive data...')
|
|
59
67
|
const sensitiveErrors = await validateSensitive(projectConfig.sensitive, rootPath)
|
|
60
68
|
results.push({
|
|
61
69
|
name: 'Sensitive Data',
|
|
@@ -69,7 +77,7 @@ async function runAllValidators(): Promise<ValidatorResult[]> {
|
|
|
69
77
|
}
|
|
70
78
|
|
|
71
79
|
// 3. 导入路径验证
|
|
72
|
-
console.log('🔍 [3/
|
|
80
|
+
console.log('🔍 [3/14] Checking import paths...')
|
|
73
81
|
const importErrors = validateImports(projectConfig.imports, rootPath)
|
|
74
82
|
results.push({
|
|
75
83
|
name: 'Import Paths',
|
|
@@ -83,7 +91,7 @@ async function runAllValidators(): Promise<ValidatorResult[]> {
|
|
|
83
91
|
}
|
|
84
92
|
|
|
85
93
|
// 4. 服务端 RPC 验证
|
|
86
|
-
console.log('🔍 [4/
|
|
94
|
+
console.log('🔍 [4/14] Checking server RPC patterns...')
|
|
87
95
|
const serverRPCErrors = validateServerRPC(projectConfig.serverRPC, rootPath)
|
|
88
96
|
results.push({
|
|
89
97
|
name: 'Server RPC',
|
|
@@ -97,7 +105,7 @@ async function runAllValidators(): Promise<ValidatorResult[]> {
|
|
|
97
105
|
}
|
|
98
106
|
|
|
99
107
|
// 5. 客户端 RPC 验证
|
|
100
|
-
console.log('🔍 [5/
|
|
108
|
+
console.log('🔍 [5/14] Checking client RPC usage...')
|
|
101
109
|
const clientRPCErrors = validateClientRPC(projectConfig.clientRPC, rootPath)
|
|
102
110
|
results.push({
|
|
103
111
|
name: 'Client RPC',
|
|
@@ -111,7 +119,7 @@ async function runAllValidators(): Promise<ValidatorResult[]> {
|
|
|
111
119
|
}
|
|
112
120
|
|
|
113
121
|
// 6. 目录结构验证
|
|
114
|
-
console.log('🔍 [6/
|
|
122
|
+
console.log('🔍 [6/14] Checking directory structure...')
|
|
115
123
|
const { directoryErrors, forbiddenErrors } = validateDirectoryStructure(
|
|
116
124
|
projectConfig.directory,
|
|
117
125
|
rootPath
|
|
@@ -129,7 +137,7 @@ async function runAllValidators(): Promise<ValidatorResult[]> {
|
|
|
129
137
|
}
|
|
130
138
|
|
|
131
139
|
// 7. 模块测试文件验证
|
|
132
|
-
console.log('🔍 [7/
|
|
140
|
+
console.log('🔍 [7/14] Checking module test files...')
|
|
133
141
|
const moduleTestErrors = validateModuleTests(projectConfig.moduleTests, rootPath)
|
|
134
142
|
results.push({
|
|
135
143
|
name: 'Module Tests',
|
|
@@ -143,7 +151,7 @@ async function runAllValidators(): Promise<ValidatorResult[]> {
|
|
|
143
151
|
}
|
|
144
152
|
|
|
145
153
|
// 8. 测试质量验证
|
|
146
|
-
console.log('🔍 [8/
|
|
154
|
+
console.log('🔍 [8/14] Checking test quality...')
|
|
147
155
|
const { errors: testQualityErrors, warnings: testQualityWarnings } = validateTestQuality(
|
|
148
156
|
projectConfig.testQuality,
|
|
149
157
|
rootPath
|
|
@@ -161,7 +169,7 @@ async function runAllValidators(): Promise<ValidatorResult[]> {
|
|
|
161
169
|
}
|
|
162
170
|
|
|
163
171
|
// 9. Client 测试覆盖验证
|
|
164
|
-
console.log('🔍 [9/
|
|
172
|
+
console.log('🔍 [9/14] Checking client test coverage...')
|
|
165
173
|
const clientTestErrors = validateClientTests(projectConfig.clientTests, rootPath)
|
|
166
174
|
results.push({
|
|
167
175
|
name: 'Client Tests',
|
|
@@ -175,7 +183,7 @@ async function runAllValidators(): Promise<ValidatorResult[]> {
|
|
|
175
183
|
}
|
|
176
184
|
|
|
177
185
|
// 10. API 覆盖率验证
|
|
178
|
-
console.log('🔍 [10/
|
|
186
|
+
console.log('🔍 [10/14] Checking API coverage...')
|
|
179
187
|
const apiCoverageErrors = validateAPICoverage(projectConfig.moduleTests, rootPath)
|
|
180
188
|
results.push({
|
|
181
189
|
name: 'API Coverage',
|
|
@@ -189,7 +197,7 @@ async function runAllValidators(): Promise<ValidatorResult[]> {
|
|
|
189
197
|
}
|
|
190
198
|
|
|
191
199
|
// 11. Markdown 引用路径验证
|
|
192
|
-
console.log('🔍 [11/
|
|
200
|
+
console.log('🔍 [11/14] Checking markdown references...')
|
|
193
201
|
const mdRefErrors = validateMdRefs(projectConfig.mdRefs, rootPath)
|
|
194
202
|
results.push({
|
|
195
203
|
name: 'Markdown References',
|
|
@@ -203,7 +211,7 @@ async function runAllValidators(): Promise<ValidatorResult[]> {
|
|
|
203
211
|
}
|
|
204
212
|
|
|
205
213
|
// 12. Console.log 验证
|
|
206
|
-
console.log('🔍 [12/
|
|
214
|
+
console.log('🔍 [12/14] Checking for console.log statements...')
|
|
207
215
|
const consoleLogErrors = validateConsoleLog(projectConfig.consoleLog, rootPath)
|
|
208
216
|
results.push({
|
|
209
217
|
name: 'Console.log',
|
|
@@ -216,6 +224,34 @@ async function runAllValidators(): Promise<ValidatorResult[]> {
|
|
|
216
224
|
console.log(' ✅ No console.log statements found\n')
|
|
217
225
|
}
|
|
218
226
|
|
|
227
|
+
// 13. Schema 命名唯一性验证
|
|
228
|
+
console.log('🔍 [13/14] Checking schema name uniqueness across modules...')
|
|
229
|
+
const schemaUniquenessErrors = validateSchemaUniqueness(projectConfig.schemaUniqueness, rootPath)
|
|
230
|
+
results.push({
|
|
231
|
+
name: 'Schema Uniqueness',
|
|
232
|
+
passed: schemaUniquenessErrors.length === 0,
|
|
233
|
+
errors: schemaUniquenessErrors.length,
|
|
234
|
+
})
|
|
235
|
+
if (schemaUniquenessErrors.length > 0) {
|
|
236
|
+
console.error(formatSchemaUniquenessErrors(schemaUniquenessErrors))
|
|
237
|
+
} else {
|
|
238
|
+
console.log(' ✅ No cross-module schema name collisions\n')
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// 14. 模块公共 API 验证
|
|
242
|
+
console.log('🔍 [14/14] Checking module public API barrels...')
|
|
243
|
+
const modulePublicApiErrors = validateModulePublicApi(projectConfig.modulePublicApi, rootPath)
|
|
244
|
+
results.push({
|
|
245
|
+
name: 'Module Public API',
|
|
246
|
+
passed: modulePublicApiErrors.length === 0,
|
|
247
|
+
errors: modulePublicApiErrors.length,
|
|
248
|
+
})
|
|
249
|
+
if (modulePublicApiErrors.length > 0) {
|
|
250
|
+
console.error(formatModulePublicApiErrors(modulePublicApiErrors))
|
|
251
|
+
} else {
|
|
252
|
+
console.log(' ✅ All depended-upon modules have index.ts barrels\n')
|
|
253
|
+
}
|
|
254
|
+
|
|
219
255
|
return results
|
|
220
256
|
}
|
|
221
257
|
|
|
@@ -269,3 +269,32 @@ export interface ConsoleLogError {
|
|
|
269
269
|
message: string
|
|
270
270
|
content: string
|
|
271
271
|
}
|
|
272
|
+
|
|
273
|
+
// ============================================
|
|
274
|
+
// Schema 命名唯一性验证配置
|
|
275
|
+
// ============================================
|
|
276
|
+
export interface SchemaUniquenessConfig {
|
|
277
|
+
modulesDir: string
|
|
278
|
+
checkDirs: string[]
|
|
279
|
+
ignoreDirs: string[]
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export interface SchemaUniquenessError {
|
|
283
|
+
exportName: string
|
|
284
|
+
modules: string[]
|
|
285
|
+
suggestion: string
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ============================================
|
|
289
|
+
// 模块公共 API 验证配置
|
|
290
|
+
// ============================================
|
|
291
|
+
export interface ModulePublicApiConfig {
|
|
292
|
+
serverDir: string
|
|
293
|
+
checkDirs: string[]
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export interface ModulePublicApiError {
|
|
297
|
+
module: string
|
|
298
|
+
dependedBy: string[]
|
|
299
|
+
suggestion: string
|
|
300
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module Public API 验证器
|
|
3
|
+
*
|
|
4
|
+
* 确保被其他模块依赖(dependsOn)的服务端模块都有 index.ts 公共 API barrel 文件。
|
|
5
|
+
* 模块应通过干净的公共接口被消费,而非深层内部导入。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFileSync, readdirSync, existsSync } from 'node:fs'
|
|
9
|
+
import { join } from 'node:path'
|
|
10
|
+
|
|
11
|
+
export interface ModulePublicApiConfig {
|
|
12
|
+
serverDir: string
|
|
13
|
+
checkDirs: string[]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ModulePublicApiError {
|
|
17
|
+
module: string
|
|
18
|
+
dependedBy: string[]
|
|
19
|
+
suggestion: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface ParsedManifest {
|
|
23
|
+
name: string
|
|
24
|
+
dependsOn: string[]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function parseManifest(filePath: string): ParsedManifest | null {
|
|
28
|
+
const content = readFileSync(filePath, 'utf-8')
|
|
29
|
+
|
|
30
|
+
const nameMatch = content.match(/name:\s*['"`]([^'"`]+)['"`]/)
|
|
31
|
+
if (!nameMatch) return null
|
|
32
|
+
|
|
33
|
+
const dependsOnMatch = content.match(/dependsOn:\s*\[([^\]]*)\]/)
|
|
34
|
+
const dependsOn = dependsOnMatch
|
|
35
|
+
? [...dependsOnMatch[1].matchAll(/['"`]([^'"`]+)['"`]/g)].map(m => m[1])
|
|
36
|
+
: []
|
|
37
|
+
|
|
38
|
+
return { name: nameMatch[1], dependsOn }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function validateModulePublicApi(
|
|
42
|
+
config: ModulePublicApiConfig,
|
|
43
|
+
rootPath: string
|
|
44
|
+
): ModulePublicApiError[] {
|
|
45
|
+
const serverPath = join(rootPath, config.serverDir)
|
|
46
|
+
if (!existsSync(serverPath)) return []
|
|
47
|
+
|
|
48
|
+
const entries = readdirSync(serverPath, { withFileTypes: true })
|
|
49
|
+
const moduleDirs = entries
|
|
50
|
+
.filter(e => e.isDirectory() && e.name.startsWith('module-'))
|
|
51
|
+
.map(e => e.name)
|
|
52
|
+
|
|
53
|
+
const manifests = new Map<string, ParsedManifest>()
|
|
54
|
+
for (const dir of moduleDirs) {
|
|
55
|
+
const manifestPath = join(serverPath, dir, 'module.ts')
|
|
56
|
+
if (!existsSync(manifestPath)) continue
|
|
57
|
+
const parsed = parseManifest(manifestPath)
|
|
58
|
+
if (parsed) manifests.set(parsed.name, parsed)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const dependedUpon = new Map<string, string[]>()
|
|
62
|
+
for (const [moduleName, manifest] of manifests) {
|
|
63
|
+
for (const dep of manifest.dependsOn) {
|
|
64
|
+
const consumers = dependedUpon.get(dep) ?? []
|
|
65
|
+
consumers.push(moduleName)
|
|
66
|
+
dependedUpon.set(dep, consumers)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const errors: ModulePublicApiError[] = []
|
|
71
|
+
for (const [depName, consumers] of dependedUpon) {
|
|
72
|
+
const barrelPath = join(serverPath, `module-${depName}`, 'index.ts')
|
|
73
|
+
if (!existsSync(barrelPath)) {
|
|
74
|
+
errors.push({
|
|
75
|
+
module: depName,
|
|
76
|
+
dependedBy: consumers,
|
|
77
|
+
suggestion: `Create template/src/server/module-${depName}/index.ts to expose a clean public API for consumers: ${consumers.join(
|
|
78
|
+
', '
|
|
79
|
+
)}`,
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return errors
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function formatModulePublicApiErrors(errors: ModulePublicApiError[]): string {
|
|
88
|
+
if (errors.length === 0) return ''
|
|
89
|
+
|
|
90
|
+
let output = `❌ Found ${errors.length} module(s) missing public API barrel (index.ts):\n\n`
|
|
91
|
+
|
|
92
|
+
for (const err of errors) {
|
|
93
|
+
output += ` module-${err.module}\n`
|
|
94
|
+
output += ` → Depended by: ${err.dependedBy.join(', ')}\n`
|
|
95
|
+
output += ` → Fix: ${err.suggestion}\n\n`
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
output += '📋 Guidelines:\n'
|
|
99
|
+
output += ' Every module listed in dependsOn should have an index.ts barrel file\n'
|
|
100
|
+
output += ' that re-exports its public API (routes, services, types).\n'
|
|
101
|
+
|
|
102
|
+
return output
|
|
103
|
+
}
|