@robsun/create-keystone-app 0.1.18 → 0.2.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 +18 -5
- package/{bin → dist}/create-keystone-app.js +114 -116
- package/dist/create-module.js +638 -0
- package/package.json +11 -7
- package/template/README.md +17 -13
- package/template/apps/server/config.example.yaml +0 -1
- package/template/apps/server/config.yaml +0 -1
- package/template/apps/server/internal/modules/example/api/handler/item_handler.go +162 -0
- package/template/apps/server/internal/modules/example/bootstrap/migrations/item.go +21 -0
- package/template/apps/server/internal/modules/example/bootstrap/seeds/item.go +33 -0
- package/template/apps/server/internal/modules/example/domain/models/item.go +30 -0
- package/template/apps/server/internal/modules/{demo → example}/domain/service/errors.go +1 -1
- package/template/apps/server/internal/modules/example/domain/service/item_service.go +110 -0
- package/template/apps/server/internal/modules/example/infra/repository/item_repository.go +49 -0
- package/template/apps/server/internal/modules/example/module.go +55 -17
- package/template/apps/server/internal/modules/manifest.go +0 -2
- package/template/apps/web/src/app.config.ts +1 -1
- package/template/apps/web/src/main.tsx +1 -3
- package/template/apps/web/src/modules/example/help/faq.md +23 -0
- package/template/apps/web/src/modules/example/help/items.md +26 -0
- package/template/apps/web/src/modules/example/help/overview.md +18 -4
- package/template/apps/web/src/modules/example/pages/ExampleItemsPage.tsx +227 -0
- package/template/apps/web/src/modules/example/routes.tsx +33 -10
- package/template/apps/web/src/modules/example/services/exampleItems.ts +32 -0
- package/template/apps/web/src/modules/example/types.ts +10 -0
- package/template/docs/CONVENTIONS.md +44 -0
- package/template/docs/GETTING_STARTED.md +54 -0
- package/template/package.json +2 -1
- package/template/scripts/check-modules.js +7 -1
- package/template/apps/server/internal/modules/demo/api/handler/task_handler.go +0 -152
- package/template/apps/server/internal/modules/demo/bootstrap/migrations/task.go +0 -21
- package/template/apps/server/internal/modules/demo/bootstrap/seeds/task.go +0 -33
- package/template/apps/server/internal/modules/demo/domain/models/task.go +0 -30
- package/template/apps/server/internal/modules/demo/domain/service/task_service.go +0 -95
- package/template/apps/server/internal/modules/demo/infra/repository/task_repository.go +0 -49
- package/template/apps/server/internal/modules/demo/module.go +0 -91
- package/template/apps/server/internal/modules/example/handlers.go +0 -19
- package/template/apps/web/src/modules/demo/help/overview.md +0 -12
- package/template/apps/web/src/modules/demo/index.ts +0 -7
- package/template/apps/web/src/modules/demo/pages/DemoTasksPage.tsx +0 -185
- package/template/apps/web/src/modules/demo/routes.tsx +0 -43
- package/template/apps/web/src/modules/demo/services/demoTasks.ts +0 -28
- package/template/apps/web/src/modules/demo/types.ts +0 -9
- package/template/apps/web/src/modules/example/pages/ExamplePage.tsx +0 -41
- package/template/apps/web/src/modules/example/services/api.ts +0 -8
package/README.md
CHANGED
|
@@ -5,20 +5,32 @@
|
|
|
5
5
|
npx @robsun/create-keystone-app <dir> [options]
|
|
6
6
|
pnpm dlx @robsun/create-keystone-app <dir> [options]
|
|
7
7
|
```
|
|
8
|
+
不传 options 会进入交互式向导。
|
|
8
9
|
|
|
9
10
|
## 选项
|
|
10
11
|
- `<dir>`:目标目录(必填),可为新目录名或 `.`(当前目录)。
|
|
11
|
-
- `--profile <starter|full>`:模板档位,默认 `starter`。
|
|
12
|
-
- `--demo` / `--no-demo`:是否包含 Demo 模块(`full` 默认包含)。
|
|
13
12
|
- `--db <sqlite|postgres>`:数据库驱动(默认 `sqlite`)。
|
|
14
13
|
- `--queue <memory|redis>`:队列驱动(默认 `memory`)。
|
|
15
14
|
- `--storage <local|s3>`:存储驱动(默认 `local`)。
|
|
16
15
|
|
|
17
16
|
## 示例
|
|
18
17
|
```bash
|
|
19
|
-
npx @robsun/create-keystone-app my-app --
|
|
18
|
+
npx @robsun/create-keystone-app my-app --db=postgres --queue=redis --storage=s3
|
|
20
19
|
```
|
|
21
20
|
|
|
21
|
+
## 模块生成器
|
|
22
|
+
```bash
|
|
23
|
+
npx --package @robsun/create-keystone-app create-keystone-module <module-name> [options]
|
|
24
|
+
pnpm dlx --package @robsun/create-keystone-app create-keystone-module <module-name> [options]
|
|
25
|
+
```
|
|
26
|
+
不传 options 会进入交互式向导。
|
|
27
|
+
|
|
28
|
+
选项:
|
|
29
|
+
- `--frontend-only`:只生成前端模块。
|
|
30
|
+
- `--backend-only`:只生成后端模块。
|
|
31
|
+
- `--with-crud`:包含 CRUD 示例代码。
|
|
32
|
+
- `--skip-register`:跳过自动注册步骤。
|
|
33
|
+
|
|
22
34
|
## 初始化后操作
|
|
23
35
|
```bash
|
|
24
36
|
cd <dir>
|
|
@@ -28,6 +40,7 @@ pnpm web:dev
|
|
|
28
40
|
pnpm dev
|
|
29
41
|
```
|
|
30
42
|
|
|
31
|
-
## 端口与
|
|
43
|
+
## 端口与 Example
|
|
32
44
|
- Web 默认端口:`3000`;后端默认端口:`8080`。
|
|
33
|
-
-
|
|
45
|
+
- Example API:`/api/v1/example/items`。
|
|
46
|
+
- 权限:`example:item:view`、`example:item:manage`。
|
|
@@ -1,78 +1,140 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
const fs = require('fs');
|
|
3
3
|
const path = require('path');
|
|
4
|
+
const readline = require('readline/promises');
|
|
5
|
+
const { stdin: input, stdout: output } = require('process');
|
|
4
6
|
|
|
5
7
|
const usage = [
|
|
6
8
|
'Usage: create-keystone-app <dir> [options]',
|
|
7
9
|
'',
|
|
8
10
|
'Options:',
|
|
9
|
-
' --profile <starter|full> Template profile (default: starter)',
|
|
10
|
-
' --demo Include demo module',
|
|
11
|
-
' --no-demo Exclude demo module',
|
|
12
11
|
' --db <sqlite|postgres> Database driver (default: sqlite)',
|
|
13
12
|
' --queue <memory|redis> Queue driver (default: memory)',
|
|
14
13
|
' --storage <local|s3> Storage driver (default: local)',
|
|
15
14
|
' -h, --help Show help',
|
|
15
|
+
'',
|
|
16
|
+
'Run without options to use guided prompts.',
|
|
16
17
|
].join('\n');
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
console.
|
|
21
|
-
process.exit(0);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
if (!args.target) {
|
|
25
|
-
console.error(usage);
|
|
19
|
+
main().catch((err) => {
|
|
20
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
21
|
+
console.error(message);
|
|
26
22
|
process.exit(1);
|
|
27
|
-
}
|
|
23
|
+
});
|
|
28
24
|
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
25
|
+
async function main() {
|
|
26
|
+
const args = parseArgs(process.argv.slice(2));
|
|
27
|
+
if (args.help) {
|
|
28
|
+
console.log(usage);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
33
31
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
32
|
+
const canPrompt = isInteractive();
|
|
33
|
+
const rl = canPrompt ? readline.createInterface({ input, output }) : null;
|
|
34
|
+
let db;
|
|
35
|
+
let queue;
|
|
36
|
+
let storage;
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
if (!args.target) {
|
|
40
|
+
if (!rl) {
|
|
41
|
+
console.error(usage);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
args.target = await promptText(rl, 'Project directory', 'keystone-app');
|
|
45
|
+
}
|
|
41
46
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
: path.basename(targetDir);
|
|
47
|
+
db = normalizeChoice(args.db, ['sqlite', 'postgres'], 'db') || null;
|
|
48
|
+
queue = normalizeChoice(args.queue, ['memory', 'redis'], 'queue') || null;
|
|
49
|
+
storage = normalizeChoice(args.storage, ['local', 's3'], 'storage') || null;
|
|
46
50
|
|
|
47
|
-
if (
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
51
|
+
if (!db) {
|
|
52
|
+
db = rl ? await promptSelect(rl, 'Select database driver', ['sqlite', 'postgres'], 0) : 'sqlite';
|
|
53
|
+
}
|
|
54
|
+
if (!queue) {
|
|
55
|
+
queue = rl ? await promptSelect(rl, 'Select queue driver', ['memory', 'redis'], 0) : 'memory';
|
|
56
|
+
}
|
|
57
|
+
if (!storage) {
|
|
58
|
+
storage = rl ? await promptSelect(rl, 'Select storage driver', ['local', 's3'], 0) : 'local';
|
|
59
|
+
}
|
|
60
|
+
} finally {
|
|
61
|
+
if (rl) {
|
|
62
|
+
rl.close();
|
|
63
|
+
}
|
|
52
64
|
}
|
|
53
|
-
} else {
|
|
54
|
-
fs.mkdirSync(targetDir, { recursive: true });
|
|
55
|
-
}
|
|
56
65
|
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
66
|
+
const targetDir = path.resolve(process.cwd(), args.target);
|
|
67
|
+
const targetName = args.target === '.'
|
|
68
|
+
? path.basename(process.cwd())
|
|
69
|
+
: path.basename(targetDir);
|
|
70
|
+
|
|
71
|
+
if (fs.existsSync(targetDir)) {
|
|
72
|
+
const entries = fs.readdirSync(targetDir);
|
|
73
|
+
if (entries.length > 0) {
|
|
74
|
+
console.error(`Target directory is not empty: ${targetDir}`);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
} else {
|
|
78
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const templateDir = path.resolve(__dirname, '..', 'template');
|
|
82
|
+
copyDir(templateDir, targetDir, {
|
|
83
|
+
'__APP_NAME__': normalizePackageName(targetName),
|
|
84
|
+
'__RAW_NAME__': targetName,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
applyConfigOptions(targetDir, { db, queue, storage });
|
|
88
|
+
|
|
89
|
+
console.log(`Created ${targetName}`);
|
|
90
|
+
console.log('Next steps:');
|
|
91
|
+
console.log(` cd ${args.target}`);
|
|
92
|
+
console.log(' pnpm install');
|
|
93
|
+
console.log(' pnpm server:dev');
|
|
94
|
+
console.log(' pnpm web:dev');
|
|
95
|
+
console.log(' pnpm dev');
|
|
96
|
+
}
|
|
62
97
|
|
|
63
|
-
|
|
98
|
+
function isInteractive() {
|
|
99
|
+
return Boolean(input.isTTY && output.isTTY);
|
|
100
|
+
}
|
|
64
101
|
|
|
65
|
-
|
|
66
|
-
|
|
102
|
+
async function promptText(rl, label, defaultValue) {
|
|
103
|
+
while (true) {
|
|
104
|
+
const suffix = defaultValue ? ` (${defaultValue})` : '';
|
|
105
|
+
const answer = await rl.question(`${label}${suffix}: `);
|
|
106
|
+
const trimmed = answer.trim();
|
|
107
|
+
if (trimmed) {
|
|
108
|
+
return trimmed;
|
|
109
|
+
}
|
|
110
|
+
if (defaultValue) {
|
|
111
|
+
return defaultValue;
|
|
112
|
+
}
|
|
113
|
+
console.log('Please enter a value.');
|
|
114
|
+
}
|
|
67
115
|
}
|
|
68
116
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
117
|
+
async function promptSelect(rl, label, choices, defaultIndex) {
|
|
118
|
+
while (true) {
|
|
119
|
+
const options = choices.map((choice, index) => ` ${index + 1}) ${choice}`).join('\n');
|
|
120
|
+
const answer = await rl.question(
|
|
121
|
+
`${label}\n${options}\nSelect an option [${defaultIndex + 1}]: `
|
|
122
|
+
);
|
|
123
|
+
const normalized = answer.trim().toLowerCase();
|
|
124
|
+
if (!normalized) {
|
|
125
|
+
return choices[defaultIndex];
|
|
126
|
+
}
|
|
127
|
+
const numeric = Number(normalized);
|
|
128
|
+
if (Number.isInteger(numeric) && numeric >= 1 && numeric <= choices.length) {
|
|
129
|
+
return choices[numeric - 1];
|
|
130
|
+
}
|
|
131
|
+
const match = choices.find((choice) => choice.toLowerCase() === normalized);
|
|
132
|
+
if (match) {
|
|
133
|
+
return match;
|
|
134
|
+
}
|
|
135
|
+
console.log('Invalid selection. Try again.');
|
|
136
|
+
}
|
|
137
|
+
}
|
|
76
138
|
|
|
77
139
|
function normalizePackageName(name) {
|
|
78
140
|
const cleaned = String(name || '')
|
|
@@ -120,49 +182,6 @@ function shouldSkipDir(name) {
|
|
|
120
182
|
return name === 'node_modules' || name === '.git';
|
|
121
183
|
}
|
|
122
184
|
|
|
123
|
-
function stripDemo(targetDir) {
|
|
124
|
-
removePath(path.join(targetDir, 'apps', 'web', 'src', 'modules', 'demo'));
|
|
125
|
-
removePath(path.join(targetDir, 'apps', 'server', 'internal', 'modules', 'demo'));
|
|
126
|
-
|
|
127
|
-
updateFile(path.join(targetDir, 'apps', 'web', 'src', 'main.tsx'), (content) =>
|
|
128
|
-
content.replace(/^\s*import ['"]\.\/modules\/demo['"];?\r?\n/m, '')
|
|
129
|
-
);
|
|
130
|
-
updateFile(path.join(targetDir, 'apps', 'web', 'src', 'app.config.ts'), (content) =>
|
|
131
|
-
content.replace(/,\s*['"]demo['"]/, '')
|
|
132
|
-
);
|
|
133
|
-
updateFile(path.join(targetDir, 'apps', 'server', 'config.yaml'), (content) =>
|
|
134
|
-
content.replace(/\r?\n\s*-\s*['"]demo['"]\s*/g, '')
|
|
135
|
-
);
|
|
136
|
-
updateFile(path.join(targetDir, 'apps', 'server', 'config.example.yaml'), (content) =>
|
|
137
|
-
content.replace(/\r?\n\s*-\s*['"]demo['"]\s*/g, '')
|
|
138
|
-
);
|
|
139
|
-
updateFile(path.join(targetDir, 'README.md'), (content) =>
|
|
140
|
-
content.replace(/<!-- DEMO_START -->[\s\S]*?<!-- DEMO_END -->\s*/m, '')
|
|
141
|
-
);
|
|
142
|
-
|
|
143
|
-
const manifestPath = path.join(
|
|
144
|
-
targetDir,
|
|
145
|
-
'apps',
|
|
146
|
-
'server',
|
|
147
|
-
'internal',
|
|
148
|
-
'modules',
|
|
149
|
-
'manifest.go'
|
|
150
|
-
);
|
|
151
|
-
const manifest = `package modules
|
|
152
|
-
|
|
153
|
-
import (
|
|
154
|
-
\texample "__APP_NAME__/apps/server/internal/modules/example"
|
|
155
|
-
)
|
|
156
|
-
|
|
157
|
-
// RegisterAll wires the module registry for this app.
|
|
158
|
-
func RegisterAll() {
|
|
159
|
-
\tClear()
|
|
160
|
-
\tRegister(example.NewModule())
|
|
161
|
-
}
|
|
162
|
-
`;
|
|
163
|
-
fs.writeFileSync(manifestPath, manifest, 'utf8');
|
|
164
|
-
}
|
|
165
|
-
|
|
166
185
|
function applyConfigOptions(targetDir, options) {
|
|
167
186
|
const configFiles = [
|
|
168
187
|
path.join(targetDir, 'apps', 'server', 'config.yaml'),
|
|
@@ -172,7 +191,6 @@ function applyConfigOptions(targetDir, options) {
|
|
|
172
191
|
for (const filePath of configFiles) {
|
|
173
192
|
updateFile(filePath, (content) => applyYamlOptions(content, options));
|
|
174
193
|
}
|
|
175
|
-
|
|
176
194
|
}
|
|
177
195
|
|
|
178
196
|
function applyYamlOptions(content, options) {
|
|
@@ -202,12 +220,6 @@ function updateYamlSectionValue(content, section, key, value) {
|
|
|
202
220
|
return content.replace(pattern, `$1${value}$3`);
|
|
203
221
|
}
|
|
204
222
|
|
|
205
|
-
function removePath(target) {
|
|
206
|
-
if (fs.existsSync(target)) {
|
|
207
|
-
fs.rmSync(target, { recursive: true, force: true });
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
223
|
function updateFile(filePath, updater) {
|
|
212
224
|
if (!fs.existsSync(filePath)) {
|
|
213
225
|
return;
|
|
@@ -228,28 +240,14 @@ function shouldMakeExecutable(filePath) {
|
|
|
228
240
|
}
|
|
229
241
|
|
|
230
242
|
function parseArgs(argv) {
|
|
231
|
-
const out = {
|
|
243
|
+
const out = {};
|
|
232
244
|
for (let i = 0; i < argv.length; i++) {
|
|
233
245
|
const arg = argv[i];
|
|
234
|
-
if (arg === '--demo') {
|
|
235
|
-
out.demo = true;
|
|
236
|
-
continue;
|
|
237
|
-
}
|
|
238
|
-
if (arg === '--no-demo') {
|
|
239
|
-
out.demo = false;
|
|
240
|
-
continue;
|
|
241
|
-
}
|
|
242
246
|
if (arg === '--help' || arg === '-h') {
|
|
243
247
|
out.help = true;
|
|
244
248
|
continue;
|
|
245
249
|
}
|
|
246
250
|
|
|
247
|
-
const profile = readValueOption(arg, argv, i, '--profile');
|
|
248
|
-
if (profile) {
|
|
249
|
-
out.profile = profile.value;
|
|
250
|
-
i += profile.skip;
|
|
251
|
-
continue;
|
|
252
|
-
}
|
|
253
251
|
const db = readValueOption(arg, argv, i, '--db');
|
|
254
252
|
if (db) {
|
|
255
253
|
out.db = db.value;
|
|
@@ -315,4 +313,4 @@ function fail(message) {
|
|
|
315
313
|
console.error(message);
|
|
316
314
|
console.error(usage);
|
|
317
315
|
process.exit(1);
|
|
318
|
-
}
|
|
316
|
+
}
|