@robsun/create-keystone-app 0.2.13 → 0.4.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 +46 -43
- package/dist/create-keystone-app.js +347 -10
- package/dist/create-module.js +1219 -607
- package/package.json +22 -23
- 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 +81 -73
- 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 +60 -18
- package/template/apps/server/go.sum +183 -31
- 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 +10 -7
- 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 -103
- 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/TEMPLATES.md +0 -532
- package/template/.claude/skills/keystone-dev/references/TESTING.md +0 -44
- package/template/.codex/skills/keystone-dev/SKILL.md +0 -103
- 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/TEMPLATES.md +0 -532
- 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
package/README.md
CHANGED
|
@@ -1,50 +1,53 @@
|
|
|
1
|
-
# @robsun/create-keystone-app
|
|
2
|
-
|
|
3
|
-
## 用法
|
|
4
|
-
```bash
|
|
5
|
-
npx @robsun/create-keystone-app <dir> [options]
|
|
6
|
-
pnpm dlx @robsun/create-keystone-app <dir> [options]
|
|
7
|
-
```
|
|
8
|
-
不传 options 会进入交互式向导。
|
|
9
|
-
|
|
10
|
-
## 选项
|
|
11
|
-
- `<dir>`:目标目录(必填),可为新目录名或 `.`(当前目录)。
|
|
1
|
+
# @robsun/create-keystone-app
|
|
2
|
+
|
|
3
|
+
## 用法
|
|
4
|
+
```bash
|
|
5
|
+
npx @robsun/create-keystone-app <dir> [options]
|
|
6
|
+
pnpm dlx @robsun/create-keystone-app <dir> [options]
|
|
7
|
+
```
|
|
8
|
+
不传 options 会进入交互式向导。
|
|
9
|
+
|
|
10
|
+
## 选项
|
|
11
|
+
- `<dir>`:目标目录(必填),可为新目录名或 `.`(当前目录)。
|
|
12
12
|
- `--db <sqlite|postgres>`:数据库驱动(默认 `sqlite`)。
|
|
13
13
|
- `--queue <memory|redis>`:队列驱动(默认 `memory`)。
|
|
14
14
|
- `--storage <local|s3>`:存储驱动(默认 `local`)。
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
- `--
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
-
|
|
36
|
-
-
|
|
37
|
-
|
|
38
|
-
##
|
|
39
|
-
|
|
40
|
-
|
|
15
|
+
- `--with-example`:包含示例模块。
|
|
16
|
+
- `--with-ci`:包含 CI 工作流模板。
|
|
17
|
+
- `--verify`:生成后执行验证(安装依赖并构建)。
|
|
18
|
+
|
|
19
|
+
## 示例
|
|
20
|
+
```bash
|
|
21
|
+
npx @robsun/create-keystone-app my-app --db=postgres --queue=redis --storage=s3
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## 模块生成器
|
|
25
|
+
```bash
|
|
26
|
+
npx --package @robsun/create-keystone-app create-keystone-module <module-name> [options]
|
|
27
|
+
pnpm dlx --package @robsun/create-keystone-app create-keystone-module <module-name> [options]
|
|
28
|
+
```
|
|
29
|
+
不传 options 会进入交互式向导。
|
|
30
|
+
|
|
31
|
+
选项:
|
|
32
|
+
- `--frontend-only`:只生成前端模块。
|
|
33
|
+
- `--backend-only`:只生成后端模块。
|
|
34
|
+
- `--with-crud`:包含 CRUD 示例代码。
|
|
35
|
+
- `--with-approval`:包含审批流代码(自动启用 `--with-crud`)。
|
|
36
|
+
- `--skip-register`:跳过自动注册步骤。
|
|
37
|
+
|
|
38
|
+
## AI Skills
|
|
39
|
+
- Codex:`.codex/skills/keystone-dev`
|
|
40
|
+
- Claude:`.claude/skills/keystone-dev`
|
|
41
|
+
|
|
42
|
+
## 初始化后操作
|
|
43
|
+
```bash
|
|
44
|
+
cd <dir>
|
|
41
45
|
pnpm install
|
|
42
46
|
pnpm server:dev
|
|
43
|
-
pnpm web:dev
|
|
44
47
|
pnpm dev
|
|
45
48
|
```
|
|
46
|
-
|
|
47
|
-
## 端口与 Example
|
|
48
|
-
- Web 默认端口:`3000`;后端默认端口:`8080`。
|
|
49
|
-
- Example API:`/api/v1/example/items`。
|
|
50
|
-
- 权限:`example:item:view`、`example:item:manage`。
|
|
49
|
+
|
|
50
|
+
## 端口与 Example
|
|
51
|
+
- Web 默认端口:`3000`;后端默认端口:`8080`。
|
|
52
|
+
- Example API:`/api/v1/example/items`。
|
|
53
|
+
- 权限:`example:item:view`、`example:item:manage`。
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
const fs = require('fs');
|
|
3
3
|
const path = require('path');
|
|
4
|
+
const os = require('os');
|
|
4
5
|
const readline = require('readline/promises');
|
|
6
|
+
const { execFileSync } = require('child_process');
|
|
5
7
|
const { stdin: input, stdout: output } = require('process');
|
|
6
8
|
|
|
7
9
|
const usage = [
|
|
@@ -11,6 +13,10 @@ const usage = [
|
|
|
11
13
|
' --db <sqlite|postgres> Database driver (default: sqlite)',
|
|
12
14
|
' --queue <memory|redis> Queue driver (default: memory)',
|
|
13
15
|
' --storage <local|s3> Storage driver (default: local)',
|
|
16
|
+
' --with-example Include example module',
|
|
17
|
+
' --with-ci Include CI workflow templates',
|
|
18
|
+
' --verify Run post-generation verification',
|
|
19
|
+
' --local Use local keystone packages (for development)',
|
|
14
20
|
' -h, --help Show help',
|
|
15
21
|
'',
|
|
16
22
|
'Run without options to use guided prompts.',
|
|
@@ -34,6 +40,8 @@ async function main() {
|
|
|
34
40
|
let db;
|
|
35
41
|
let queue;
|
|
36
42
|
let storage;
|
|
43
|
+
let withExample;
|
|
44
|
+
let withCi;
|
|
37
45
|
|
|
38
46
|
try {
|
|
39
47
|
if (!args.target) {
|
|
@@ -47,6 +55,12 @@ async function main() {
|
|
|
47
55
|
db = normalizeChoice(args.db, ['sqlite', 'postgres'], 'db') || null;
|
|
48
56
|
queue = normalizeChoice(args.queue, ['memory', 'redis'], 'queue') || null;
|
|
49
57
|
storage = normalizeChoice(args.storage, ['local', 's3'], 'storage') || null;
|
|
58
|
+
if (typeof args.withExample === 'boolean') {
|
|
59
|
+
withExample = args.withExample;
|
|
60
|
+
}
|
|
61
|
+
if (typeof args.withCi === 'boolean') {
|
|
62
|
+
withCi = args.withCi;
|
|
63
|
+
}
|
|
50
64
|
|
|
51
65
|
if (!db) {
|
|
52
66
|
db = rl ? await promptSelect(rl, 'Select database driver', ['sqlite', 'postgres'], 0) : 'sqlite';
|
|
@@ -57,6 +71,12 @@ async function main() {
|
|
|
57
71
|
if (!storage) {
|
|
58
72
|
storage = rl ? await promptSelect(rl, 'Select storage driver', ['local', 's3'], 0) : 'local';
|
|
59
73
|
}
|
|
74
|
+
if (typeof withExample !== 'boolean') {
|
|
75
|
+
withExample = rl ? await promptConfirm(rl, 'Include example module', false) : false;
|
|
76
|
+
}
|
|
77
|
+
if (typeof withCi !== 'boolean') {
|
|
78
|
+
withCi = rl ? await promptConfirm(rl, 'Include CI workflows', false) : false;
|
|
79
|
+
}
|
|
60
80
|
} finally {
|
|
61
81
|
if (rl) {
|
|
62
82
|
rl.close();
|
|
@@ -85,14 +105,33 @@ async function main() {
|
|
|
85
105
|
});
|
|
86
106
|
|
|
87
107
|
applyConfigOptions(targetDir, { db, queue, storage });
|
|
108
|
+
applyExampleOption(targetDir, { withExample });
|
|
109
|
+
applyCiOption(targetDir, { withCi });
|
|
88
110
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
111
|
+
const localMode = Boolean(args.local);
|
|
112
|
+
if (localMode) {
|
|
113
|
+
const keystoneRoot = path.resolve(__dirname, '..', '..', '..');
|
|
114
|
+
applyLocalDev(targetDir, keystoneRoot);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (args.verify) {
|
|
118
|
+
runVerify(targetDir);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (localMode) {
|
|
122
|
+
console.log(`Created ${targetName} (local development mode)`);
|
|
123
|
+
console.log('Next steps:');
|
|
124
|
+
console.log(` cd ${args.target}`);
|
|
125
|
+
console.log(' pnpm install');
|
|
126
|
+
console.log(' pnpm dev');
|
|
127
|
+
} else {
|
|
128
|
+
console.log(`Created ${targetName}`);
|
|
129
|
+
console.log('Next steps:');
|
|
130
|
+
console.log(` cd ${args.target}`);
|
|
131
|
+
console.log(' pnpm install');
|
|
132
|
+
console.log(' pnpm server:dev');
|
|
133
|
+
console.log(' pnpm dev');
|
|
134
|
+
}
|
|
96
135
|
}
|
|
97
136
|
|
|
98
137
|
function isInteractive() {
|
|
@@ -136,6 +175,24 @@ async function promptSelect(rl, label, choices, defaultIndex) {
|
|
|
136
175
|
}
|
|
137
176
|
}
|
|
138
177
|
|
|
178
|
+
async function promptConfirm(rl, label, defaultValue) {
|
|
179
|
+
const fallback = defaultValue ? 'Y/n' : 'y/N';
|
|
180
|
+
while (true) {
|
|
181
|
+
const answer = await rl.question(`${label} (${fallback}): `);
|
|
182
|
+
const normalized = answer.trim().toLowerCase();
|
|
183
|
+
if (!normalized) {
|
|
184
|
+
return Boolean(defaultValue);
|
|
185
|
+
}
|
|
186
|
+
if (['y', 'yes'].includes(normalized)) {
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
if (['n', 'no'].includes(normalized)) {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
console.log('Invalid selection. Try again.');
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
139
196
|
function normalizePackageName(name) {
|
|
140
197
|
const cleaned = String(name || '')
|
|
141
198
|
.trim()
|
|
@@ -182,6 +239,79 @@ function shouldSkipDir(name) {
|
|
|
182
239
|
return name === 'node_modules' || name === '.git';
|
|
183
240
|
}
|
|
184
241
|
|
|
242
|
+
function applyLocalDev(targetDir, keystoneRoot) {
|
|
243
|
+
// Create .npmrc to deduplicate React in local development
|
|
244
|
+
const npmrcPath = path.join(targetDir, '.npmrc');
|
|
245
|
+
const npmrcContent = [
|
|
246
|
+
'# Deduplicate React to avoid "Invalid hook call" errors with linked packages',
|
|
247
|
+
'public-hoist-pattern[]=react',
|
|
248
|
+
'public-hoist-pattern[]=react-dom',
|
|
249
|
+
'public-hoist-pattern[]=dayjs',
|
|
250
|
+
'public-hoist-pattern[]=@types/react',
|
|
251
|
+
'public-hoist-pattern[]=@types/react-dom',
|
|
252
|
+
].join('\n') + '\n';
|
|
253
|
+
fs.writeFileSync(npmrcPath, npmrcContent, 'utf8');
|
|
254
|
+
|
|
255
|
+
// Update frontend package.json to use local packages
|
|
256
|
+
const webPkgPath = path.join(targetDir, 'apps', 'web', 'package.json');
|
|
257
|
+
if (fs.existsSync(webPkgPath)) {
|
|
258
|
+
const relPath = path.relative(path.dirname(webPkgPath), keystoneRoot).replace(/\\/g, '/');
|
|
259
|
+
updateFile(webPkgPath, (content) => {
|
|
260
|
+
const pkg = JSON.parse(content);
|
|
261
|
+
if (pkg.dependencies) {
|
|
262
|
+
if (pkg.dependencies['@robsun/keystone-web-core']) {
|
|
263
|
+
pkg.dependencies['@robsun/keystone-web-core'] = `link:${relPath}/packages/keystone-web-core`;
|
|
264
|
+
}
|
|
265
|
+
if (pkg.dependencies['@robsun/keystone-contracts']) {
|
|
266
|
+
pkg.dependencies['@robsun/keystone-contracts'] = `link:${relPath}/packages/keystone-contracts`;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return JSON.stringify(pkg, null, 2) + '\n';
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Update Vite config for local development
|
|
274
|
+
const viteConfigPath = path.join(targetDir, 'apps', 'web', 'vite.config.ts');
|
|
275
|
+
if (fs.existsSync(viteConfigPath)) {
|
|
276
|
+
const relPath = path.relative(path.join(targetDir, 'apps', 'web'), keystoneRoot).replace(/\\/g, '/');
|
|
277
|
+
updateFile(viteConfigPath, (content) => {
|
|
278
|
+
let updated = content;
|
|
279
|
+
// Add dedupe configuration to resolve section
|
|
280
|
+
if (updated.includes('resolve: {')) {
|
|
281
|
+
updated = updated.replace(
|
|
282
|
+
/resolve:\s*\{/,
|
|
283
|
+
`resolve: {\n dedupe: ['react', 'react-dom', 'dayjs'],`
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
// Add fs.allow to server section to allow accessing linked packages
|
|
287
|
+
if (updated.includes('server: {')) {
|
|
288
|
+
updated = updated.replace(
|
|
289
|
+
/server:\s*\{/,
|
|
290
|
+
`server: {\n fs: { allow: ['${relPath}', '.'] },`
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
return updated;
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Update backend go.mod to use local package
|
|
298
|
+
const goModPath = path.join(targetDir, 'apps', 'server', 'go.mod');
|
|
299
|
+
if (fs.existsSync(goModPath)) {
|
|
300
|
+
const relPath = path.relative(path.dirname(goModPath), keystoneRoot).replace(/\\/g, '/');
|
|
301
|
+
updateFile(goModPath, (content) => {
|
|
302
|
+
if (content.includes('replace github.com/robsuncn/keystone')) {
|
|
303
|
+
return content;
|
|
304
|
+
}
|
|
305
|
+
const lines = content.split('\n');
|
|
306
|
+
const requireIdx = lines.findIndex(l => l.startsWith('require '));
|
|
307
|
+
if (requireIdx !== -1) {
|
|
308
|
+
lines.splice(requireIdx, 0, `replace github.com/robsuncn/keystone => ${relPath}`, '');
|
|
309
|
+
}
|
|
310
|
+
return lines.join('\n');
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
185
315
|
function applyConfigOptions(targetDir, options) {
|
|
186
316
|
const configFiles = [
|
|
187
317
|
path.join(targetDir, 'apps', 'server', 'config.yaml'),
|
|
@@ -193,6 +323,156 @@ function applyConfigOptions(targetDir, options) {
|
|
|
193
323
|
}
|
|
194
324
|
}
|
|
195
325
|
|
|
326
|
+
function applyExampleOption(targetDir, options) {
|
|
327
|
+
const withExample = Boolean(options?.withExample);
|
|
328
|
+
const webModulesIndex = path.join(targetDir, 'apps', 'web', 'src', 'modules', 'index.ts');
|
|
329
|
+
const webConfigPath = path.join(targetDir, 'apps', 'web', 'src', 'app.config.ts');
|
|
330
|
+
const serverManifestPath = path.join(targetDir, 'apps', 'server', 'internal', 'modules', 'manifest.go');
|
|
331
|
+
const modulePath = resolveGoModulePath(path.join(targetDir, 'apps', 'server', 'go.mod'));
|
|
332
|
+
const serverConfigPaths = [
|
|
333
|
+
path.join(targetDir, 'apps', 'server', 'config.yaml'),
|
|
334
|
+
path.join(targetDir, 'apps', 'server', 'config.example.yaml'),
|
|
335
|
+
];
|
|
336
|
+
|
|
337
|
+
if (withExample) {
|
|
338
|
+
fs.mkdirSync(path.dirname(webModulesIndex), { recursive: true });
|
|
339
|
+
fs.writeFileSync(webModulesIndex, "import './example'\n", 'utf8');
|
|
340
|
+
updateFile(webConfigPath, enableExampleInWebConfig);
|
|
341
|
+
serverConfigPaths.forEach((filePath) => {
|
|
342
|
+
updateFile(filePath, addExampleModuleToYaml);
|
|
343
|
+
});
|
|
344
|
+
fs.writeFileSync(serverManifestPath, exampleManifestContent(modulePath), 'utf8');
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const exampleDirs = [
|
|
349
|
+
path.join(targetDir, 'apps', 'web', 'src', 'modules', 'example'),
|
|
350
|
+
path.join(targetDir, 'apps', 'server', 'internal', 'modules', 'example'),
|
|
351
|
+
];
|
|
352
|
+
exampleDirs.forEach((dirPath) => {
|
|
353
|
+
if (fs.existsSync(dirPath)) {
|
|
354
|
+
fs.rmSync(dirPath, { recursive: true, force: true });
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
fs.mkdirSync(path.dirname(webModulesIndex), { recursive: true });
|
|
359
|
+
fs.writeFileSync(webModulesIndex, '// Register application modules here.\n', 'utf8');
|
|
360
|
+
updateFile(webConfigPath, removeExampleFromWebConfig);
|
|
361
|
+
serverConfigPaths.forEach((filePath) => {
|
|
362
|
+
updateFile(filePath, removeExampleFromYaml);
|
|
363
|
+
});
|
|
364
|
+
fs.writeFileSync(serverManifestPath, emptyManifestContent(), 'utf8');
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function applyCiOption(targetDir, options) {
|
|
368
|
+
const withCi = Boolean(options?.withCi);
|
|
369
|
+
if (withCi) {
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
const paths = [
|
|
373
|
+
path.join(targetDir, '.github'),
|
|
374
|
+
path.join(targetDir, '.golangci.yml'),
|
|
375
|
+
path.join(targetDir, '.eslintrc.js'),
|
|
376
|
+
];
|
|
377
|
+
paths.forEach((target) => {
|
|
378
|
+
if (!fs.existsSync(target)) {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function enableExampleInWebConfig(content) {
|
|
386
|
+
let next = content;
|
|
387
|
+
if (!next.includes("'example'")) {
|
|
388
|
+
next = next.replace("enabled: ['keystone'],", "enabled: ['keystone', 'example'],");
|
|
389
|
+
}
|
|
390
|
+
if (!next.includes('example_item')) {
|
|
391
|
+
next = next.replace(
|
|
392
|
+
"{ value: 'general', label: 'General Flow' },",
|
|
393
|
+
"{ value: 'general', label: 'General Flow' },\n { value: 'example_item', label: 'Example Items' },"
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
return next;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function removeExampleFromWebConfig(content) {
|
|
400
|
+
let next = content;
|
|
401
|
+
next = next.replace("['keystone', 'example']", "['keystone']");
|
|
402
|
+
next = next.replace("['example', 'keystone']", "['keystone']");
|
|
403
|
+
next = next.replace(/\n\s*\{ value: 'example_item'[^}]*\},?/g, '');
|
|
404
|
+
return next;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function addExampleModuleToYaml(content) {
|
|
408
|
+
if (content.includes('"example"')) {
|
|
409
|
+
return content;
|
|
410
|
+
}
|
|
411
|
+
const pattern = /(modules:\s*[\s\S]*?enabled:\s*\n\s*-\s*"keystone")/m;
|
|
412
|
+
if (!pattern.test(content)) {
|
|
413
|
+
return content;
|
|
414
|
+
}
|
|
415
|
+
return content.replace(pattern, `$1\n - "example"`);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function removeExampleFromYaml(content) {
|
|
419
|
+
return content.replace(/\n\s*-\s*"example"\s*/g, '');
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function emptyManifestContent() {
|
|
423
|
+
return [
|
|
424
|
+
'package modules',
|
|
425
|
+
'',
|
|
426
|
+
'import servermodules "github.com/robsuncn/keystone/server/modules"',
|
|
427
|
+
'',
|
|
428
|
+
'// Register wires application modules into the shared registry.',
|
|
429
|
+
'func Register(registry *servermodules.Registry) {',
|
|
430
|
+
'\tif registry == nil {',
|
|
431
|
+
'\t\treturn',
|
|
432
|
+
'\t}',
|
|
433
|
+
'\tregistry.Register(servermodules.NewCoreModule())',
|
|
434
|
+
'}',
|
|
435
|
+
'',
|
|
436
|
+
].join('\n');
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function exampleManifestContent(modulePath) {
|
|
440
|
+
const importPath = modulePath
|
|
441
|
+
? `${modulePath}/internal/modules/example`
|
|
442
|
+
: '__APP_NAME__/apps/server/internal/modules/example';
|
|
443
|
+
return [
|
|
444
|
+
'package modules',
|
|
445
|
+
'',
|
|
446
|
+
'import (',
|
|
447
|
+
'\tservermodules "github.com/robsuncn/keystone/server/modules"',
|
|
448
|
+
'',
|
|
449
|
+
`\texample "${importPath}"`,
|
|
450
|
+
')',
|
|
451
|
+
'',
|
|
452
|
+
'// Register wires application modules into the shared registry.',
|
|
453
|
+
'func Register(registry *servermodules.Registry) {',
|
|
454
|
+
'\tif registry == nil {',
|
|
455
|
+
'\t\treturn',
|
|
456
|
+
'\t}',
|
|
457
|
+
'\tregistry.Register(servermodules.NewCoreModule())',
|
|
458
|
+
'\tregistry.Register(example.NewModule())',
|
|
459
|
+
'}',
|
|
460
|
+
'',
|
|
461
|
+
].join('\n');
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function resolveGoModulePath(goModPath) {
|
|
465
|
+
if (!fs.existsSync(goModPath)) {
|
|
466
|
+
return '';
|
|
467
|
+
}
|
|
468
|
+
const content = fs.readFileSync(goModPath, 'utf8');
|
|
469
|
+
const match = content.match(/^module\s+([^\s]+)\s*$/m);
|
|
470
|
+
if (!match) {
|
|
471
|
+
return '';
|
|
472
|
+
}
|
|
473
|
+
return match[1];
|
|
474
|
+
}
|
|
475
|
+
|
|
196
476
|
function applyYamlOptions(content, options) {
|
|
197
477
|
let next = content;
|
|
198
478
|
if (options.db) {
|
|
@@ -213,11 +493,23 @@ function applyYamlOptions(content, options) {
|
|
|
213
493
|
}
|
|
214
494
|
|
|
215
495
|
function updateYamlSectionValue(content, section, key, value) {
|
|
216
|
-
const
|
|
217
|
-
|
|
496
|
+
const sectionPattern = new RegExp(`(^${section}:[^\\n]*\\n)([\\s\\S]*?)(?=^\\S|\\Z)`, 'm');
|
|
497
|
+
const match = sectionPattern.exec(content);
|
|
498
|
+
if (!match) {
|
|
218
499
|
return content;
|
|
219
500
|
}
|
|
220
|
-
|
|
501
|
+
const block = match[2];
|
|
502
|
+
const keyPattern = new RegExp(`(^\\s*${key}:\\s*\")([^\"]*)(\")`, 'm');
|
|
503
|
+
if (!keyPattern.test(block)) {
|
|
504
|
+
return content;
|
|
505
|
+
}
|
|
506
|
+
const updatedBlock = block.replace(keyPattern, `$1${value}$3`);
|
|
507
|
+
return (
|
|
508
|
+
content.slice(0, match.index) +
|
|
509
|
+
match[1] +
|
|
510
|
+
updatedBlock +
|
|
511
|
+
content.slice(match.index + match[0].length)
|
|
512
|
+
);
|
|
221
513
|
}
|
|
222
514
|
|
|
223
515
|
function updateFile(filePath, updater) {
|
|
@@ -239,6 +531,35 @@ function shouldMakeExecutable(filePath) {
|
|
|
239
531
|
return normalized.includes('/.husky/');
|
|
240
532
|
}
|
|
241
533
|
|
|
534
|
+
function runVerify(targetDir) {
|
|
535
|
+
console.log('Running verification...');
|
|
536
|
+
runCmd('pnpm', ['install'], { cwd: targetDir });
|
|
537
|
+
runCmd('pnpm', ['-C', 'apps/web', 'build'], { cwd: targetDir });
|
|
538
|
+
runCmd('go', ['-C', 'apps/server', 'build', './...'], { cwd: targetDir });
|
|
539
|
+
console.log('Verification complete.');
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function runCmd(command, commandArgs, options) {
|
|
543
|
+
execFileSync(resolveCmd(command), commandArgs, { stdio: 'inherit', ...options });
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function resolveCmd(command) {
|
|
547
|
+
if (os.platform() !== 'win32') {
|
|
548
|
+
return command;
|
|
549
|
+
}
|
|
550
|
+
if (
|
|
551
|
+
path.isAbsolute(command) ||
|
|
552
|
+
command.includes('\\') ||
|
|
553
|
+
command.includes('/') ||
|
|
554
|
+
command.endsWith('.cmd') ||
|
|
555
|
+
command.endsWith('.exe') ||
|
|
556
|
+
command.endsWith('.bat')
|
|
557
|
+
) {
|
|
558
|
+
return command;
|
|
559
|
+
}
|
|
560
|
+
return `${command}.cmd`;
|
|
561
|
+
}
|
|
562
|
+
|
|
242
563
|
function parseArgs(argv) {
|
|
243
564
|
const out = {};
|
|
244
565
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -247,6 +568,22 @@ function parseArgs(argv) {
|
|
|
247
568
|
out.help = true;
|
|
248
569
|
continue;
|
|
249
570
|
}
|
|
571
|
+
if (arg === '--local') {
|
|
572
|
+
out.local = true;
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
if (arg === '--with-example') {
|
|
576
|
+
out.withExample = true;
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
579
|
+
if (arg === '--with-ci') {
|
|
580
|
+
out.withCi = true;
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
if (arg === '--verify') {
|
|
584
|
+
out.verify = true;
|
|
585
|
+
continue;
|
|
586
|
+
}
|
|
250
587
|
|
|
251
588
|
const db = readValueOption(arg, argv, i, '--db');
|
|
252
589
|
if (db) {
|