@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.
- package/README.md +46 -44
- package/dist/create-keystone-app.js +347 -10
- package/dist/create-module.js +1217 -1187
- package/package.json +1 -1
- package/template/.claude/skills/keystone-implement/SKILL.md +113 -0
- package/template/.claude/skills/keystone-implement/references/CHECKLIST.md +91 -0
- package/template/.claude/skills/keystone-implement/references/PATTERNS.md +1088 -0
- package/template/.claude/skills/keystone-implement/references/SCHEMA.md +135 -0
- package/template/.claude/skills/keystone-implement/references/TESTING.md +231 -0
- package/template/.claude/skills/keystone-requirements/SKILL.md +296 -0
- package/template/.claude/skills/keystone-requirements/references/CONFIRM_TEMPLATE.md +170 -0
- package/template/.claude/skills/keystone-requirements/references/SCHEMA.md +135 -0
- package/template/.eslintrc.js +3 -0
- package/template/.github/workflows/ci.yml +30 -0
- package/template/.github/workflows/release.yml +32 -0
- package/template/.golangci.yml +11 -0
- package/template/README.md +82 -81
- package/template/apps/server/README.md +8 -0
- package/template/apps/server/cmd/server/main.go +27 -185
- package/template/apps/server/config.example.yaml +31 -1
- package/template/apps/server/config.yaml +31 -1
- package/template/apps/server/go.mod +61 -19
- package/template/apps/server/go.sum +185 -32
- package/template/apps/server/internal/frontend/embed.go +3 -8
- package/template/apps/server/internal/modules/example/README.md +18 -0
- package/template/apps/server/internal/modules/example/api/handler/handler_test.go +9 -0
- package/template/apps/server/internal/modules/example/api/handler/item_handler.go +468 -165
- package/template/apps/server/internal/modules/example/bootstrap/seeds/item.go +217 -8
- package/template/apps/server/internal/modules/example/domain/models/item.go +40 -7
- package/template/apps/server/internal/modules/example/domain/service/approval_callback.go +68 -0
- package/template/apps/server/internal/modules/example/domain/service/approval_schema.go +41 -0
- package/template/apps/server/internal/modules/example/domain/service/errors.go +20 -22
- package/template/apps/server/internal/modules/example/domain/service/item_service.go +267 -7
- package/template/apps/server/internal/modules/example/domain/service/item_service_test.go +281 -0
- package/template/apps/server/internal/modules/example/i18n/keys.go +32 -20
- package/template/apps/server/internal/modules/example/i18n/locales/en-US.json +30 -18
- package/template/apps/server/internal/modules/example/i18n/locales/zh-CN.json +30 -18
- package/template/apps/server/internal/modules/example/infra/exporter/item_exporter.go +119 -0
- package/template/apps/server/internal/modules/example/infra/importer/item_importer.go +77 -0
- package/template/apps/server/internal/modules/example/infra/repository/item_repository.go +99 -49
- package/template/apps/server/internal/modules/example/module.go +171 -97
- package/template/apps/server/internal/modules/example/tests/integration_test.go +7 -0
- package/template/apps/server/internal/modules/manifest.go +7 -7
- package/template/apps/web/README.md +4 -2
- package/template/apps/web/package.json +1 -1
- package/template/apps/web/src/app.config.ts +8 -6
- package/template/apps/web/src/index.css +7 -3
- package/template/apps/web/src/main.tsx +2 -5
- package/template/apps/web/src/modules/example/help/en-US/faq.md +27 -0
- package/template/apps/web/src/modules/example/help/en-US/items.md +30 -0
- package/template/apps/web/src/modules/example/help/en-US/overview.md +31 -0
- package/template/apps/web/src/modules/example/help/zh-CN/faq.md +27 -0
- package/template/apps/web/src/modules/example/help/zh-CN/items.md +31 -0
- package/template/apps/web/src/modules/example/help/zh-CN/overview.md +32 -0
- package/template/apps/web/src/modules/example/locales/en-US/example.json +99 -32
- package/template/apps/web/src/modules/example/locales/zh-CN/example.json +85 -18
- package/template/apps/web/src/modules/example/pages/ExampleItemsPage.tsx +840 -237
- package/template/apps/web/src/modules/example/services/exampleItems.ts +79 -8
- package/template/apps/web/src/modules/example/types.ts +14 -1
- package/template/apps/web/src/modules/index.ts +1 -0
- package/template/apps/web/vite.config.ts +9 -3
- package/template/docs/CONVENTIONS.md +17 -17
- package/template/package.json +4 -5
- package/template/pnpm-lock.yaml +76 -5
- package/template/scripts/build.bat +15 -3
- package/template/scripts/build.sh +9 -3
- package/template/scripts/check-help.js +249 -0
- package/template/scripts/compress-assets.js +89 -0
- package/template/scripts/test.bat +23 -0
- package/template/scripts/test.sh +16 -0
- package/template/.claude/skills/keystone-dev/SKILL.md +0 -90
- package/template/.claude/skills/keystone-dev/references/ADVANCED_PATTERNS.md +0 -716
- package/template/.claude/skills/keystone-dev/references/APPROVAL.md +0 -121
- package/template/.claude/skills/keystone-dev/references/CAPABILITIES.md +0 -261
- package/template/.claude/skills/keystone-dev/references/CHECKLIST.md +0 -285
- package/template/.claude/skills/keystone-dev/references/GOTCHAS.md +0 -390
- package/template/.claude/skills/keystone-dev/references/PATTERNS.md +0 -605
- package/template/.claude/skills/keystone-dev/references/TEMPLATES.md +0 -2710
- package/template/.claude/skills/keystone-dev/references/TESTING.md +0 -44
- package/template/.codex/skills/keystone-dev/SKILL.md +0 -90
- package/template/.codex/skills/keystone-dev/references/ADVANCED_PATTERNS.md +0 -716
- package/template/.codex/skills/keystone-dev/references/APPROVAL.md +0 -121
- package/template/.codex/skills/keystone-dev/references/CAPABILITIES.md +0 -261
- package/template/.codex/skills/keystone-dev/references/CHECKLIST.md +0 -285
- package/template/.codex/skills/keystone-dev/references/GOTCHAS.md +0 -390
- package/template/.codex/skills/keystone-dev/references/PATTERNS.md +0 -605
- package/template/.codex/skills/keystone-dev/references/TEMPLATES.md +0 -2710
- package/template/.codex/skills/keystone-dev/references/TESTING.md +0 -44
- package/template/apps/server/internal/app/routes/module_routes.go +0 -16
- package/template/apps/server/internal/app/routes/routes.go +0 -226
- package/template/apps/server/internal/app/startup/startup.go +0 -74
- package/template/apps/server/internal/frontend/handler.go +0 -122
- package/template/apps/server/internal/modules/registry.go +0 -145
- package/template/apps/web/src/modules/example/help/faq.md +0 -23
- package/template/apps/web/src/modules/example/help/items.md +0 -26
- 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.
|
package/template/scripts/test.sh
CHANGED
|
@@ -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` - 测试开发指南
|