@robsun/create-keystone-app 0.2.15 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/README.md +46 -44
  2. package/dist/create-keystone-app.js +347 -10
  3. package/dist/create-module.js +1217 -1187
  4. package/package.json +1 -1
  5. package/template/.claude/skills/keystone-implement/SKILL.md +113 -0
  6. package/template/.claude/skills/keystone-implement/references/CHECKLIST.md +91 -0
  7. package/template/.claude/skills/keystone-implement/references/PATTERNS.md +1088 -0
  8. package/template/.claude/skills/keystone-implement/references/SCHEMA.md +135 -0
  9. package/template/.claude/skills/keystone-implement/references/TESTING.md +231 -0
  10. package/template/.claude/skills/keystone-requirements/SKILL.md +296 -0
  11. package/template/.claude/skills/keystone-requirements/references/CONFIRM_TEMPLATE.md +170 -0
  12. package/template/.claude/skills/keystone-requirements/references/SCHEMA.md +135 -0
  13. package/template/.eslintrc.js +3 -0
  14. package/template/.github/workflows/ci.yml +30 -0
  15. package/template/.github/workflows/release.yml +32 -0
  16. package/template/.golangci.yml +11 -0
  17. package/template/README.md +82 -81
  18. package/template/apps/server/README.md +8 -0
  19. package/template/apps/server/cmd/server/main.go +27 -185
  20. package/template/apps/server/config.example.yaml +31 -1
  21. package/template/apps/server/config.yaml +31 -1
  22. package/template/apps/server/go.mod +61 -19
  23. package/template/apps/server/go.sum +185 -32
  24. package/template/apps/server/internal/frontend/embed.go +3 -8
  25. package/template/apps/server/internal/modules/example/README.md +18 -0
  26. package/template/apps/server/internal/modules/example/api/handler/handler_test.go +9 -0
  27. package/template/apps/server/internal/modules/example/api/handler/item_handler.go +468 -165
  28. package/template/apps/server/internal/modules/example/bootstrap/seeds/item.go +217 -8
  29. package/template/apps/server/internal/modules/example/domain/models/item.go +40 -7
  30. package/template/apps/server/internal/modules/example/domain/service/approval_callback.go +68 -0
  31. package/template/apps/server/internal/modules/example/domain/service/approval_schema.go +41 -0
  32. package/template/apps/server/internal/modules/example/domain/service/errors.go +20 -22
  33. package/template/apps/server/internal/modules/example/domain/service/item_service.go +267 -7
  34. package/template/apps/server/internal/modules/example/domain/service/item_service_test.go +281 -0
  35. package/template/apps/server/internal/modules/example/i18n/keys.go +32 -20
  36. package/template/apps/server/internal/modules/example/i18n/locales/en-US.json +30 -18
  37. package/template/apps/server/internal/modules/example/i18n/locales/zh-CN.json +30 -18
  38. package/template/apps/server/internal/modules/example/infra/exporter/item_exporter.go +119 -0
  39. package/template/apps/server/internal/modules/example/infra/importer/item_importer.go +77 -0
  40. package/template/apps/server/internal/modules/example/infra/repository/item_repository.go +99 -49
  41. package/template/apps/server/internal/modules/example/module.go +171 -97
  42. package/template/apps/server/internal/modules/example/tests/integration_test.go +7 -0
  43. package/template/apps/server/internal/modules/manifest.go +7 -7
  44. package/template/apps/web/README.md +4 -2
  45. package/template/apps/web/package.json +1 -1
  46. package/template/apps/web/src/app.config.ts +8 -6
  47. package/template/apps/web/src/index.css +7 -3
  48. package/template/apps/web/src/main.tsx +2 -5
  49. package/template/apps/web/src/modules/example/help/en-US/faq.md +27 -0
  50. package/template/apps/web/src/modules/example/help/en-US/items.md +30 -0
  51. package/template/apps/web/src/modules/example/help/en-US/overview.md +31 -0
  52. package/template/apps/web/src/modules/example/help/zh-CN/faq.md +27 -0
  53. package/template/apps/web/src/modules/example/help/zh-CN/items.md +31 -0
  54. package/template/apps/web/src/modules/example/help/zh-CN/overview.md +32 -0
  55. package/template/apps/web/src/modules/example/locales/en-US/example.json +99 -32
  56. package/template/apps/web/src/modules/example/locales/zh-CN/example.json +85 -18
  57. package/template/apps/web/src/modules/example/pages/ExampleItemsPage.tsx +840 -237
  58. package/template/apps/web/src/modules/example/services/exampleItems.ts +79 -8
  59. package/template/apps/web/src/modules/example/types.ts +14 -1
  60. package/template/apps/web/src/modules/index.ts +1 -0
  61. package/template/apps/web/vite.config.ts +9 -3
  62. package/template/docs/CONVENTIONS.md +17 -17
  63. package/template/package.json +4 -5
  64. package/template/pnpm-lock.yaml +76 -5
  65. package/template/scripts/build.bat +15 -3
  66. package/template/scripts/build.sh +9 -3
  67. package/template/scripts/check-help.js +249 -0
  68. package/template/scripts/compress-assets.js +89 -0
  69. package/template/scripts/test.bat +23 -0
  70. package/template/scripts/test.sh +16 -0
  71. package/template/.claude/skills/keystone-dev/SKILL.md +0 -90
  72. package/template/.claude/skills/keystone-dev/references/ADVANCED_PATTERNS.md +0 -716
  73. package/template/.claude/skills/keystone-dev/references/APPROVAL.md +0 -121
  74. package/template/.claude/skills/keystone-dev/references/CAPABILITIES.md +0 -261
  75. package/template/.claude/skills/keystone-dev/references/CHECKLIST.md +0 -285
  76. package/template/.claude/skills/keystone-dev/references/GOTCHAS.md +0 -390
  77. package/template/.claude/skills/keystone-dev/references/PATTERNS.md +0 -605
  78. package/template/.claude/skills/keystone-dev/references/TEMPLATES.md +0 -2710
  79. package/template/.claude/skills/keystone-dev/references/TESTING.md +0 -44
  80. package/template/.codex/skills/keystone-dev/SKILL.md +0 -90
  81. package/template/.codex/skills/keystone-dev/references/ADVANCED_PATTERNS.md +0 -716
  82. package/template/.codex/skills/keystone-dev/references/APPROVAL.md +0 -121
  83. package/template/.codex/skills/keystone-dev/references/CAPABILITIES.md +0 -261
  84. package/template/.codex/skills/keystone-dev/references/CHECKLIST.md +0 -285
  85. package/template/.codex/skills/keystone-dev/references/GOTCHAS.md +0 -390
  86. package/template/.codex/skills/keystone-dev/references/PATTERNS.md +0 -605
  87. package/template/.codex/skills/keystone-dev/references/TEMPLATES.md +0 -2710
  88. package/template/.codex/skills/keystone-dev/references/TESTING.md +0 -44
  89. package/template/apps/server/internal/app/routes/module_routes.go +0 -16
  90. package/template/apps/server/internal/app/routes/routes.go +0 -226
  91. package/template/apps/server/internal/app/startup/startup.go +0 -74
  92. package/template/apps/server/internal/frontend/handler.go +0 -122
  93. package/template/apps/server/internal/modules/registry.go +0 -145
  94. package/template/apps/web/src/modules/example/help/faq.md +0 -23
  95. package/template/apps/web/src/modules/example/help/items.md +0 -26
  96. package/template/apps/web/src/modules/example/help/overview.md +0 -25
@@ -0,0 +1,249 @@
1
+ #!/usr/bin/env node
2
+ const fs = require('fs')
3
+ const path = require('path')
4
+
5
+ const rootDir = path.resolve(__dirname, '..')
6
+ const webSrcDir = path.join(rootDir, 'apps', 'web', 'src')
7
+ const modulesDir = path.join(webSrcDir, 'modules')
8
+ const appConfigPath = path.join(webSrcDir, 'app.config.ts')
9
+
10
+ const stripBom = (content) =>
11
+ content.charCodeAt(0) === 0xfeff ? content.slice(1) : content
12
+ const readFile = (filePath) => stripBom(fs.readFileSync(filePath, 'utf8'))
13
+
14
+ const toRelative = (filePath) => path.relative(rootDir, filePath)
15
+
16
+ const walk = (dir, visitor) => {
17
+ if (!fs.existsSync(dir)) {
18
+ return
19
+ }
20
+ const entries = fs.readdirSync(dir, { withFileTypes: true })
21
+ for (const entry of entries) {
22
+ const fullPath = path.join(dir, entry.name)
23
+ if (entry.isDirectory()) {
24
+ walk(fullPath, visitor)
25
+ } else {
26
+ visitor(fullPath, entry)
27
+ }
28
+ }
29
+ }
30
+
31
+ const addKey = (map, key, filePath) => {
32
+ if (!map.has(key)) {
33
+ map.set(key, new Set())
34
+ }
35
+ map.get(key).add(filePath)
36
+ }
37
+
38
+ const parseSupportedLocales = () => {
39
+ const fallback = ['zh-CN', 'en-US']
40
+ if (!fs.existsSync(appConfigPath)) {
41
+ return fallback
42
+ }
43
+ const content = readFile(appConfigPath)
44
+ const match = content.match(/supportedLocales\s*:\s*\[([\s\S]*?)\]/m)
45
+ if (!match) {
46
+ return fallback
47
+ }
48
+ const values = []
49
+ const regex = /['"]([^'"]+)['"]/g
50
+ let current = regex.exec(match[1])
51
+ while (current) {
52
+ values.push(current[1])
53
+ current = regex.exec(match[1])
54
+ }
55
+ if (values.length === 0) {
56
+ return fallback
57
+ }
58
+ return Array.from(new Set(values))
59
+ }
60
+
61
+ const extractLocaleFromPath = (filePath, locales) => {
62
+ const normalized = filePath.split(path.sep).join('/')
63
+ const marker = '/help/'
64
+ const idx = normalized.lastIndexOf(marker)
65
+ if (idx < 0) {
66
+ return null
67
+ }
68
+ const rest = normalized.slice(idx + marker.length)
69
+ const locale = rest.split('/')[0]
70
+ if (!locale || !locales.includes(locale)) {
71
+ return null
72
+ }
73
+ return locale
74
+ }
75
+
76
+ const parseEnabledModules = () => {
77
+ const fallback = ['keystone']
78
+ if (!fs.existsSync(appConfigPath)) {
79
+ return fallback
80
+ }
81
+ const content = readFile(appConfigPath)
82
+ const match = content.match(/modules\s*:\s*\{[\s\S]*?enabled\s*:\s*\[([\s\S]*?)\]/m)
83
+ if (!match) {
84
+ return fallback
85
+ }
86
+ const items = []
87
+ const regex = /['"]([^'"]+)['"]/g
88
+ let current = regex.exec(match[1])
89
+ while (current) {
90
+ items.push(current[1])
91
+ current = regex.exec(match[1])
92
+ }
93
+ if (items.length === 0) {
94
+ return fallback
95
+ }
96
+ return Array.from(new Set(items))
97
+ }
98
+
99
+ const resolveModuleName = (filePath) => {
100
+ const normalized = filePath.split(path.sep).join('/')
101
+ const marker = '/modules/'
102
+ const idx = normalized.indexOf(marker)
103
+ if (idx < 0) {
104
+ return null
105
+ }
106
+ const rest = normalized.slice(idx + marker.length)
107
+ const parts = rest.split('/')
108
+ if (parts.length === 0) {
109
+ return null
110
+ }
111
+ return parts[0]
112
+ }
113
+
114
+ const collectRouteHelpKeys = (dir, enabledModules) => {
115
+ const result = new Map()
116
+ const routeFileRegex = /\.(ts|tsx)$/
117
+ const helpKeyRegex = /helpKey\s*:\s*['"]([^'"]+)['"]/g
118
+ walk(dir, (filePath) => {
119
+ if (!routeFileRegex.test(filePath)) {
120
+ return
121
+ }
122
+ const moduleName = resolveModuleName(filePath)
123
+ if (moduleName && enabledModules && !enabledModules.has(moduleName)) {
124
+ return
125
+ }
126
+ const content = readFile(filePath)
127
+ helpKeyRegex.lastIndex = 0
128
+ let match = helpKeyRegex.exec(content)
129
+ while (match) {
130
+ addKey(result, match[1], filePath)
131
+ match = helpKeyRegex.exec(content)
132
+ }
133
+ })
134
+ return result
135
+ }
136
+
137
+ const extractFrontmatter = (content) => {
138
+ if (!content.startsWith('---')) {
139
+ return null
140
+ }
141
+ const match = content.match(/^---\s*[\r\n]+([\s\S]*?)\r?\n---\s*/)
142
+ if (!match) {
143
+ return null
144
+ }
145
+ return match[1]
146
+ }
147
+
148
+ const collectDocHelpKeys = (dir, locales) => {
149
+ const result = new Map()
150
+ locales.forEach((locale) => result.set(locale, new Map()))
151
+ const warnings = []
152
+ walk(dir, (filePath) => {
153
+ if (!filePath.endsWith('.md')) {
154
+ return
155
+ }
156
+ const normalized = filePath.split(path.sep).join('/')
157
+ if (!normalized.includes('/help/')) {
158
+ return
159
+ }
160
+ const locale = extractLocaleFromPath(filePath, locales)
161
+ if (!locale) {
162
+ warnings.push(`Unknown locale path ${toRelative(filePath)}`)
163
+ return
164
+ }
165
+ const content = readFile(filePath)
166
+ const frontmatter = extractFrontmatter(content)
167
+ if (!frontmatter) {
168
+ return
169
+ }
170
+ const match = frontmatter.match(/^\s*helpKey\s*:\s*["']?([^"'\n]+)["']?/m)
171
+ if (!match) {
172
+ warnings.push(`Missing helpKey in ${toRelative(filePath)}`)
173
+ return
174
+ }
175
+ const bucket = result.get(locale)
176
+ if (!bucket) {
177
+ return
178
+ }
179
+ addKey(bucket, match[1], filePath)
180
+ })
181
+ return { keys: result, warnings }
182
+ }
183
+
184
+ try {
185
+ if (!fs.existsSync(modulesDir)) {
186
+ throw new Error('apps/web/src/modules not found')
187
+ }
188
+
189
+ const locales = parseSupportedLocales()
190
+ const enabledModules = new Set(parseEnabledModules())
191
+ const routeKeys = collectRouteHelpKeys(modulesDir, enabledModules)
192
+ const { keys: docKeys, warnings } = collectDocHelpKeys(webSrcDir, locales)
193
+
194
+ if (warnings.length > 0) {
195
+ console.warn('Help docs warnings:')
196
+ warnings.forEach((warning) => console.warn(` - ${warning}`))
197
+ }
198
+
199
+ const missingDocs = []
200
+ for (const [key, sources] of routeKeys.entries()) {
201
+ for (const locale of locales) {
202
+ const bucket = docKeys.get(locale)
203
+ if (!bucket || !bucket.has(key)) {
204
+ missingDocs.push({ key, locale, sources })
205
+ }
206
+ }
207
+ }
208
+
209
+ const duplicateDocs = []
210
+ for (const [locale, bucket] of docKeys.entries()) {
211
+ for (const [key, sources] of bucket.entries()) {
212
+ if (sources.size > 1) {
213
+ duplicateDocs.push({ key, locale, sources })
214
+ }
215
+ }
216
+ }
217
+
218
+ if (missingDocs.length === 0 && duplicateDocs.length === 0) {
219
+ console.log('Help docs are in sync.')
220
+ process.exit(0)
221
+ }
222
+
223
+ console.error('Help documentation check failed:')
224
+ if (missingDocs.length > 0) {
225
+ console.error('\nMissing help docs for route helpKey:')
226
+ missingDocs.forEach(({ key, locale, sources }) => {
227
+ console.error(` - ${key} (${locale})`)
228
+ sources.forEach((source) => {
229
+ console.error(` route: ${toRelative(source)}`)
230
+ })
231
+ })
232
+ }
233
+
234
+ if (duplicateDocs.length > 0) {
235
+ console.error('\nDuplicate helpKey entries found:')
236
+ duplicateDocs.forEach(({ key, locale, sources }) => {
237
+ console.error(` - ${key} (${locale})`)
238
+ sources.forEach((source) => {
239
+ console.error(` doc: ${toRelative(source)}`)
240
+ })
241
+ })
242
+ }
243
+
244
+ process.exit(1)
245
+ } catch (err) {
246
+ console.error('Failed to check help docs:')
247
+ console.error(err instanceof Error ? err.message : String(err))
248
+ process.exit(2)
249
+ }
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env node
2
+ const fs = require('fs')
3
+ const path = require('path')
4
+ const zlib = require('zlib')
5
+
6
+ const rootDir = path.resolve(__dirname, '..')
7
+ const distDir = path.join(rootDir, 'apps', 'server', 'internal', 'frontend', 'dist')
8
+
9
+ const compressibleExts = new Set([
10
+ '.js',
11
+ '.mjs',
12
+ '.css',
13
+ '.html',
14
+ '.json',
15
+ '.svg',
16
+ '.txt',
17
+ '.xml',
18
+ '.map',
19
+ ])
20
+
21
+ const skipExts = new Set([
22
+ '.br',
23
+ '.gz',
24
+ '.png',
25
+ '.jpg',
26
+ '.jpeg',
27
+ '.gif',
28
+ '.webp',
29
+ '.woff',
30
+ '.woff2',
31
+ '.ttf',
32
+ '.eot',
33
+ '.ico',
34
+ ])
35
+
36
+ if (!fs.existsSync(distDir)) {
37
+ console.error(`[compress-assets] dist not found: ${distDir}`)
38
+ process.exit(1)
39
+ }
40
+
41
+ const files = []
42
+ walk(distDir, (filePath) => files.push(filePath))
43
+
44
+ const brotliOptions = {
45
+ params: {
46
+ [zlib.constants.BROTLI_PARAM_QUALITY]: 11,
47
+ [zlib.constants.BROTLI_PARAM_MODE]: zlib.constants.BROTLI_MODE_TEXT,
48
+ },
49
+ }
50
+
51
+ let compressed = 0
52
+ for (const filePath of files) {
53
+ const ext = path.extname(filePath).toLowerCase()
54
+ if (skipExts.has(ext)) {
55
+ continue
56
+ }
57
+ if (!compressibleExts.has(ext)) {
58
+ continue
59
+ }
60
+ const data = fs.readFileSync(filePath)
61
+ const brPath = `${filePath}.br`
62
+ const gzPath = `${filePath}.gz`
63
+
64
+ if (!fs.existsSync(brPath)) {
65
+ const br = zlib.brotliCompressSync(data, brotliOptions)
66
+ fs.writeFileSync(brPath, br)
67
+ }
68
+ if (!fs.existsSync(gzPath)) {
69
+ const gz = zlib.gzipSync(data, { level: 9 })
70
+ fs.writeFileSync(gzPath, gz)
71
+ }
72
+ compressed += 1
73
+ }
74
+
75
+ console.log(`[compress-assets] precompressed ${compressed} asset(s)`)
76
+
77
+ function walk(dir, onFile) {
78
+ const entries = fs.readdirSync(dir, { withFileTypes: true })
79
+ for (const entry of entries) {
80
+ const fullPath = path.join(dir, entry.name)
81
+ if (entry.isDirectory()) {
82
+ walk(fullPath, onFile)
83
+ continue
84
+ }
85
+ if (entry.isFile()) {
86
+ onFile(fullPath)
87
+ }
88
+ }
89
+ }
@@ -22,6 +22,19 @@ if errorlevel 1 (
22
22
  echo [OK] Go found
23
23
 
24
24
  echo.
25
+ set "USE_LOCAL_KEYSTONE=0"
26
+ set "ADDED_KEYSTONE_REPLACE=0"
27
+ set "KEYSTONE_ROOT="
28
+ set "SCRIPT_DIR=%~dp0"
29
+ set "KEYSTONE_CANDIDATE=%SCRIPT_DIR%..\..\..\.."
30
+ for %%I in ("%KEYSTONE_CANDIDATE%") do set "KEYSTONE_CANDIDATE=%%~fI"
31
+ if exist "%KEYSTONE_CANDIDATE%\go.mod" (
32
+ findstr /C:"module github.com/robsuncn/keystone" "%KEYSTONE_CANDIDATE%\go.mod" >nul 2>&1
33
+ if not errorlevel 1 (
34
+ set "KEYSTONE_ROOT=%KEYSTONE_CANDIDATE%"
35
+ set "USE_LOCAL_KEYSTONE=1"
36
+ )
37
+ )
25
38
  echo Running tests...
26
39
  echo.
27
40
 
@@ -70,6 +83,13 @@ echo ----------------------------------------
70
83
  echo Backend Tests
71
84
  echo ----------------------------------------
72
85
  cd apps\server
86
+ if "%USE_LOCAL_KEYSTONE%"=="1" (
87
+ findstr /C:"replace github.com/robsuncn/keystone" go.mod >nul 2>&1
88
+ if errorlevel 1 (
89
+ go mod edit -replace github.com/robsuncn/keystone=%KEYSTONE_ROOT%
90
+ set "ADDED_KEYSTONE_REPLACE=1"
91
+ )
92
+ )
73
93
  go vet ./...
74
94
  if errorlevel 1 (
75
95
  echo [FAIL] Backend vet failed
@@ -84,6 +104,9 @@ if errorlevel 1 (
84
104
  ) else (
85
105
  echo [OK] Backend tests passed
86
106
  )
107
+ if "%ADDED_KEYSTONE_REPLACE%"=="1" (
108
+ go mod edit -dropreplace github.com/robsuncn/keystone
109
+ )
87
110
  cd ..\..
88
111
 
89
112
  echo.
@@ -23,6 +23,15 @@ if ! command -v go &> /dev/null; then
23
23
  fi
24
24
  echo -e "${GREEN}[OK]${NC} Go found"
25
25
 
26
+ USE_LOCAL_KEYSTONE=0
27
+ ADDED_KEYSTONE_REPLACE=0
28
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
29
+ KEYSTONE_CANDIDATE="$(cd "$SCRIPT_DIR/../../../.." && pwd)"
30
+ if [ -f "$KEYSTONE_CANDIDATE/go.mod" ] && grep -q "module github.com/robsuncn/keystone" "$KEYSTONE_CANDIDATE/go.mod"; then
31
+ KEYSTONE_ROOT="$KEYSTONE_CANDIDATE"
32
+ USE_LOCAL_KEYSTONE=1
33
+ fi
34
+
26
35
  echo ""
27
36
  echo "Running tests..."
28
37
  echo ""
@@ -70,6 +79,10 @@ echo "----------------------------------------"
70
79
  echo "Backend Tests"
71
80
  echo "----------------------------------------"
72
81
  cd apps/server
82
+ if [ "$USE_LOCAL_KEYSTONE" -eq 1 ] && ! grep -q "replace github.com/robsuncn/keystone" go.mod; then
83
+ go mod edit -replace github.com/robsuncn/keystone="$KEYSTONE_ROOT"
84
+ ADDED_KEYSTONE_REPLACE=1
85
+ fi
73
86
  if go vet ./...; then
74
87
  echo -e "${GREEN}[OK]${NC} Backend vet passed"
75
88
  else
@@ -83,6 +96,9 @@ else
83
96
  echo -e "${RED}[FAIL]${NC} Backend tests failed"
84
97
  FAILED=1
85
98
  fi
99
+ if [ "$ADDED_KEYSTONE_REPLACE" -eq 1 ]; then
100
+ go mod edit -dropreplace github.com/robsuncn/keystone
101
+ fi
86
102
  cd ../..
87
103
 
88
104
  echo ""
@@ -1,90 +0,0 @@
1
- ---
2
- name: keystone-dev
3
- description: Build and extend Keystone modules and features (frontend + backend) with consistent structure, permissions, i18n, help docs, and OpenAPI contracts. Use when adding new modules, routes, APIs, migrations, or updating the scaffold template in this repo.
4
- ---
5
- # Keystone Dev
6
-
7
- ## Core workflow
8
- 1) Identify the target app root: this repo's scaffold template or a generated app.
9
- 2) Create or update the module using the generator or the Example module as the baseline.
10
- 3) Add or update tests for new behavior before finishing implementation.
11
- 4) Wire frontend routes, menus, permissions, help docs, and i18n.
12
- 5) Wire backend module registration, permissions, i18n, routes, migrations, and seeds.
13
- 6) Update contracts and types when APIs change.
14
- 7) Run the relevant checks before finishing.
15
-
16
- ## Module creation
17
- - Use the generator when possible:
18
- ```bash
19
- pnpm create:module <module-name> [options]
20
-
21
- Options:
22
- --frontend-only Only generate the frontend module
23
- --backend-only Only generate the backend module
24
- --with-crud Include CRUD example code
25
- --with-approval Include approval workflow code (implies --with-crud)
26
- --skip-register Skip auto registration steps
27
- ```
28
- - Keep module names in kebab-case and consistent across frontend, backend, and permissions.
29
- - Prefer copying the Example module if you need a manual template.
30
-
31
- ## Approval workflow (--with-approval)
32
- When using `--with-approval`, the generator creates:
33
- - **Backend**: `domain/service/approval.go` (Submit/Cancel), `domain/service/callback.go` (OnApproved/OnRejected), `api/handler/approval.go`
34
- - **Frontend**: `components/ApprovalActions.tsx` with status tags and action buttons
35
- - **Model updates**: Adds `draft/pending/approved/rejected` statuses and `ApprovalInstanceID` field
36
- - **I18n**: Approval-related translations in both languages
37
- - **Routes**: `POST /:id/submit` and `POST /:id/cancel` endpoints
38
-
39
- After generation, register the approval callback in `module.go`:
40
- ```go
41
- func (m *Module) RegisterApprovalCallback(reg *approval.CallbackRegistry) error {
42
- return reg.Register(service.ApprovalBusinessType, service.New{Pascal}ApprovalCallback(m.repo))
43
- }
44
- ```
45
-
46
- ## Frontend checklist (apps/web)
47
- - Add module at `apps/web/src/modules/<module>`.
48
- - In `index.ts`, call `loadModuleLocales` and `registerModule`.
49
- - In `routes.tsx`, set `handle.menu`, `handle.permission`, `handle.helpKey`, and use `labelKey`/`breadcrumbKey` for i18n.
50
- - Add help docs under `apps/web/src/modules/<module>/help/**` and keep `helpKey` aligned.
51
- - Add locale files in `apps/web/src/modules/<module>/locales/zh-CN/*.json` and `en-US/*.json`.
52
-
53
- ## Backend checklist (apps/server)
54
- - Add module at `apps/server/internal/modules/<module>`.
55
- - Implement the Module interface in `module.go` and keep DDD layering (`api/`, `domain/`, `infra/`, `bootstrap/`).
56
- - Register permissions with `CreateMenuI18n`/`CreateActionI18n` using `{module}:{resource}:{action}`.
57
- - Register i18n in `RegisterI18n()` and provide `i18n/keys.go` + `i18n/locales/*.json`.
58
- - Register the module in `apps/server/internal/modules/manifest.go`.
59
- - Enable the module in `apps/server/config.yaml` (scaffold apps).
60
-
61
- ## Testing development
62
- - Backend: add unit tests in package folders (`*_test.go`) or in `tests/unit`/`tests/integration`/`tests/perf`.
63
- - Frontend: add component or page tests alongside modules (e.g. `*.test.tsx`).
64
- - Cover critical paths first (permissions, validation, and data mutations).
65
-
66
- ## Contracts and API types
67
- - Add or update OpenAPI specs under `contracts/NNN-<name>/`.
68
- - Update `CONTRACT_MAPPING` in `scripts/generate-api-types.mjs` for new contract folders.
69
- - Run `pnpm contracts:sync` and `pnpm -C packages/keystone-contracts generate` after contract changes.
70
-
71
- ## Quality gates
72
- - For scaffold changes: `pnpm -C packages/create-keystone-app/template test`.
73
- - For contracts: `pnpm contracts:check` and `pnpm -C packages/keystone-contracts typecheck`.
74
- - For backend changes: `go test ./...`.
75
-
76
- ## Key references
77
- - `packages/create-keystone-app/template/docs/CONVENTIONS.md`
78
- - `packages/create-keystone-app/template/docs/CODE_STYLE.md`
79
- - `packages/create-keystone-app/template/docs/I18N.md`
80
- - `packages/create-keystone-app/template/apps/web/src/modules/example`
81
- - `packages/create-keystone-app/template/apps/server/internal/modules/example`
82
-
83
- ## Advanced templates (in `references/`)
84
- - `TEMPLATES.md` - 基础 CRUD + 高级业务模板(多实体、审批流、导入导出、前端高级组件)
85
- - `ADVANCED_PATTERNS.md` - 高级开发模式(跨模块依赖、状态机、缓存、并发控制、任务编排)
86
- - `PATTERNS.md` - 后端开发模式(分页、事务、批量、软删除、预加载、审批集成)
87
- - `CHECKLIST.md` - 模块开发完整自检清单
88
- - `GOTCHAS.md` - 常见陷阱与解决方案
89
- - `CAPABILITIES.md` - 平台完整能力清单
90
- - `TESTING.md` - 测试开发指南