@robsun/create-keystone-app 0.2.0 → 0.2.2
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 +14 -0
- package/{bin → dist}/create-keystone-app.js +115 -38
- package/dist/create-module.js +638 -0
- package/package.json +11 -7
- package/template/README.md +12 -0
- package/template/apps/server/internal/modules/manifest.go +1 -1
- package/template/apps/web/package.json +1 -1
- package/template/apps/web/src/main.tsx +1 -2
- package/template/package.json +1 -0
- package/template/pnpm-lock.yaml +5 -4
package/README.md
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
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>`:目标目录(必填),可为新目录名或 `.`(当前目录)。
|
|
@@ -17,6 +18,19 @@ pnpm dlx @robsun/create-keystone-app <dir> [options]
|
|
|
17
18
|
npx @robsun/create-keystone-app my-app --db=postgres --queue=redis --storage=s3
|
|
18
19
|
```
|
|
19
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
|
+
|
|
20
34
|
## 初始化后操作
|
|
21
35
|
```bash
|
|
22
36
|
cd <dir>
|
|
@@ -1,6 +1,8 @@
|
|
|
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]',
|
|
@@ -10,53 +12,129 @@ const usage = [
|
|
|
10
12
|
' --queue <memory|redis> Queue driver (default: memory)',
|
|
11
13
|
' --storage <local|s3> Storage driver (default: local)',
|
|
12
14
|
' -h, --help Show help',
|
|
15
|
+
'',
|
|
16
|
+
'Run without options to use guided prompts.',
|
|
13
17
|
].join('\n');
|
|
14
18
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
console.
|
|
18
|
-
process.exit(0);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
if (!args.target) {
|
|
22
|
-
console.error(usage);
|
|
19
|
+
main().catch((err) => {
|
|
20
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
21
|
+
console.error(message);
|
|
23
22
|
process.exit(1);
|
|
24
|
-
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
async function main() {
|
|
26
|
+
const args = parseArgs(process.argv.slice(2));
|
|
27
|
+
if (args.help) {
|
|
28
|
+
console.log(usage);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
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
|
+
}
|
|
46
|
+
|
|
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;
|
|
25
50
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
+
}
|
|
64
|
+
}
|
|
29
65
|
|
|
30
|
-
const targetDir = path.resolve(process.cwd(), args.target);
|
|
31
|
-
const targetName = args.target === '.'
|
|
32
|
-
|
|
33
|
-
|
|
66
|
+
const targetDir = path.resolve(process.cwd(), args.target);
|
|
67
|
+
const targetName = args.target === '.'
|
|
68
|
+
? path.basename(process.cwd())
|
|
69
|
+
: path.basename(targetDir);
|
|
34
70
|
|
|
35
|
-
if (fs.existsSync(targetDir)) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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 });
|
|
40
79
|
}
|
|
41
|
-
|
|
42
|
-
|
|
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');
|
|
43
96
|
}
|
|
44
97
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
'__RAW_NAME__': targetName,
|
|
49
|
-
});
|
|
98
|
+
function isInteractive() {
|
|
99
|
+
return Boolean(input.isTTY && output.isTTY);
|
|
100
|
+
}
|
|
50
101
|
|
|
51
|
-
|
|
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
|
+
}
|
|
115
|
+
}
|
|
52
116
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
+
}
|
|
60
138
|
|
|
61
139
|
function normalizePackageName(name) {
|
|
62
140
|
const cleaned = String(name || '')
|
|
@@ -113,7 +191,6 @@ function applyConfigOptions(targetDir, options) {
|
|
|
113
191
|
for (const filePath of configFiles) {
|
|
114
192
|
updateFile(filePath, (content) => applyYamlOptions(content, options));
|
|
115
193
|
}
|
|
116
|
-
|
|
117
194
|
}
|
|
118
195
|
|
|
119
196
|
function applyYamlOptions(content, options) {
|
|
@@ -236,4 +313,4 @@ function fail(message) {
|
|
|
236
313
|
console.error(message);
|
|
237
314
|
console.error(usage);
|
|
238
315
|
process.exit(1);
|
|
239
|
-
}
|
|
316
|
+
}
|
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const fs = require('fs')
|
|
3
|
+
const path = require('path')
|
|
4
|
+
const readline = require('readline/promises')
|
|
5
|
+
const { stdin: input, stdout: output } = require('process')
|
|
6
|
+
|
|
7
|
+
const usage = [
|
|
8
|
+
'Usage: create-module <module-name> [options]',
|
|
9
|
+
'',
|
|
10
|
+
'Options:',
|
|
11
|
+
' --frontend-only Only generate the frontend module',
|
|
12
|
+
' --backend-only Only generate the backend module',
|
|
13
|
+
' --with-crud Include CRUD example code',
|
|
14
|
+
' --skip-register Skip auto registration steps',
|
|
15
|
+
' -h, --help Show help',
|
|
16
|
+
'',
|
|
17
|
+
'Run without options to use guided prompts.',
|
|
18
|
+
].join('\n')
|
|
19
|
+
|
|
20
|
+
main().catch((err) => {
|
|
21
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
22
|
+
console.error(message)
|
|
23
|
+
process.exit(1)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
async function main() {
|
|
27
|
+
const args = parseArgs(process.argv.slice(2))
|
|
28
|
+
if (args.help) {
|
|
29
|
+
console.log(usage)
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const canPrompt = isInteractive()
|
|
34
|
+
const rl = canPrompt ? readline.createInterface({ input, output }) : null
|
|
35
|
+
|
|
36
|
+
let includeFrontend = !args.backendOnly
|
|
37
|
+
let includeBackend = !args.frontendOnly
|
|
38
|
+
let withCrud = args.withCrud
|
|
39
|
+
let skipRegister = args.skipRegister
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
if (!args.target) {
|
|
43
|
+
if (!rl) {
|
|
44
|
+
console.error(usage)
|
|
45
|
+
process.exit(1)
|
|
46
|
+
}
|
|
47
|
+
args.target = await promptText(rl, 'Module name')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (args.frontendOnly && args.backendOnly) {
|
|
51
|
+
fail('Use either --frontend-only or --backend-only, not both.')
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!args.frontendOnly && !args.backendOnly && rl) {
|
|
55
|
+
const selection = await promptSelect(
|
|
56
|
+
rl,
|
|
57
|
+
'Generate which module(s)?',
|
|
58
|
+
['frontend + backend', 'frontend only', 'backend only'],
|
|
59
|
+
0
|
|
60
|
+
)
|
|
61
|
+
if (selection === 'frontend only') {
|
|
62
|
+
includeFrontend = true
|
|
63
|
+
includeBackend = false
|
|
64
|
+
} else if (selection === 'backend only') {
|
|
65
|
+
includeFrontend = false
|
|
66
|
+
includeBackend = true
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!args.withCrud && rl) {
|
|
71
|
+
withCrud = await promptConfirm(rl, 'Include CRUD example code', false)
|
|
72
|
+
}
|
|
73
|
+
if (!args.skipRegister && rl) {
|
|
74
|
+
const autoRegister = await promptConfirm(rl, 'Auto-register module', true)
|
|
75
|
+
skipRegister = !autoRegister
|
|
76
|
+
}
|
|
77
|
+
} finally {
|
|
78
|
+
if (rl) {
|
|
79
|
+
rl.close()
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const names = buildNames(args.target)
|
|
84
|
+
const rootDir = resolveProjectRoot(process.cwd(), includeFrontend, includeBackend)
|
|
85
|
+
|
|
86
|
+
const frontendSourceDir = path.join(rootDir, 'apps', 'web', 'src', 'modules', 'example')
|
|
87
|
+
const frontendTargetDir = path.join(rootDir, 'apps', 'web', 'src', 'modules', names.kebab)
|
|
88
|
+
const backendSourceDir = path.join(rootDir, 'apps', 'server', 'internal', 'modules', 'example')
|
|
89
|
+
const backendTargetDir = path.join(rootDir, 'apps', 'server', 'internal', 'modules', names.kebab)
|
|
90
|
+
|
|
91
|
+
if (includeFrontend) {
|
|
92
|
+
if (!fs.existsSync(frontendSourceDir)) {
|
|
93
|
+
fail(`Frontend example module not found at ${frontendSourceDir}`)
|
|
94
|
+
}
|
|
95
|
+
if (fs.existsSync(frontendTargetDir)) {
|
|
96
|
+
fail(`Frontend module already exists: ${frontendTargetDir}`)
|
|
97
|
+
}
|
|
98
|
+
copyModule(frontendSourceDir, frontendTargetDir, names, 'frontend')
|
|
99
|
+
if (!withCrud) {
|
|
100
|
+
pruneFrontendCrud(frontendTargetDir, names)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (includeBackend) {
|
|
105
|
+
if (!fs.existsSync(backendSourceDir)) {
|
|
106
|
+
fail(`Backend example module not found at ${backendSourceDir}`)
|
|
107
|
+
}
|
|
108
|
+
if (fs.existsSync(backendTargetDir)) {
|
|
109
|
+
fail(`Backend module already exists: ${backendTargetDir}`)
|
|
110
|
+
}
|
|
111
|
+
copyModule(backendSourceDir, backendTargetDir, names, 'backend')
|
|
112
|
+
if (!withCrud) {
|
|
113
|
+
pruneBackendCrud(backendTargetDir, names)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!skipRegister) {
|
|
118
|
+
if (includeFrontend) {
|
|
119
|
+
registerFrontendModule(path.join(rootDir, 'apps', 'web', 'src', 'main.tsx'), names)
|
|
120
|
+
}
|
|
121
|
+
if (includeBackend) {
|
|
122
|
+
registerBackendModule(
|
|
123
|
+
path.join(rootDir, 'apps', 'server', 'internal', 'modules', 'manifest.go'),
|
|
124
|
+
path.join(rootDir, 'apps', 'server', 'config.yaml'),
|
|
125
|
+
names
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const targets = []
|
|
131
|
+
if (includeFrontend) targets.push('frontend')
|
|
132
|
+
if (includeBackend) targets.push('backend')
|
|
133
|
+
|
|
134
|
+
console.log(`Created ${names.kebab} module (${targets.join(' + ')})`)
|
|
135
|
+
if (skipRegister) {
|
|
136
|
+
console.log('Registration skipped. Update the app entrypoints manually.')
|
|
137
|
+
} else {
|
|
138
|
+
console.log('Registration updated.')
|
|
139
|
+
}
|
|
140
|
+
if (!withCrud) {
|
|
141
|
+
console.log('Generated minimal scaffolding (no CRUD example).')
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function isInteractive() {
|
|
146
|
+
return Boolean(input.isTTY && output.isTTY)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function promptText(rl, label) {
|
|
150
|
+
while (true) {
|
|
151
|
+
const answer = await rl.question(`${label}: `)
|
|
152
|
+
const trimmed = answer.trim()
|
|
153
|
+
if (trimmed) {
|
|
154
|
+
return trimmed
|
|
155
|
+
}
|
|
156
|
+
console.log('Please enter a value.')
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function promptSelect(rl, label, choices, defaultIndex) {
|
|
161
|
+
while (true) {
|
|
162
|
+
const options = choices.map((choice, index) => ` ${index + 1}) ${choice}`).join('\n')
|
|
163
|
+
const answer = await rl.question(
|
|
164
|
+
`${label}\n${options}\nSelect an option [${defaultIndex + 1}]: `
|
|
165
|
+
)
|
|
166
|
+
const normalized = answer.trim().toLowerCase()
|
|
167
|
+
if (!normalized) {
|
|
168
|
+
return choices[defaultIndex]
|
|
169
|
+
}
|
|
170
|
+
const numeric = Number(normalized)
|
|
171
|
+
if (Number.isInteger(numeric) && numeric >= 1 && numeric <= choices.length) {
|
|
172
|
+
return choices[numeric - 1]
|
|
173
|
+
}
|
|
174
|
+
const match = choices.find((choice) => choice.toLowerCase() === normalized)
|
|
175
|
+
if (match) {
|
|
176
|
+
return match
|
|
177
|
+
}
|
|
178
|
+
console.log('Invalid selection. Try again.')
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function promptConfirm(rl, label, defaultValue) {
|
|
183
|
+
while (true) {
|
|
184
|
+
const suffix = defaultValue ? 'Y/n' : 'y/N'
|
|
185
|
+
const answer = await rl.question(`${label} (${suffix}): `)
|
|
186
|
+
const normalized = answer.trim().toLowerCase()
|
|
187
|
+
if (!normalized) {
|
|
188
|
+
return defaultValue
|
|
189
|
+
}
|
|
190
|
+
if (['y', 'yes'].includes(normalized)) {
|
|
191
|
+
return true
|
|
192
|
+
}
|
|
193
|
+
if (['n', 'no'].includes(normalized)) {
|
|
194
|
+
return false
|
|
195
|
+
}
|
|
196
|
+
console.log('Please enter y or n.')
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function buildNames(raw) {
|
|
201
|
+
const words = splitWords(raw)
|
|
202
|
+
if (words.length === 0) {
|
|
203
|
+
fail(`Invalid module name: ${raw}`)
|
|
204
|
+
}
|
|
205
|
+
const capitalized = words.map(capitalize)
|
|
206
|
+
const kebab = words.join('-')
|
|
207
|
+
const camel = words[0] + capitalized.slice(1).join('')
|
|
208
|
+
const pascal = capitalized.join('')
|
|
209
|
+
const lower = words.join('')
|
|
210
|
+
const display = capitalized.join(' ')
|
|
211
|
+
return {
|
|
212
|
+
raw,
|
|
213
|
+
words,
|
|
214
|
+
kebab,
|
|
215
|
+
camel,
|
|
216
|
+
pascal,
|
|
217
|
+
lower,
|
|
218
|
+
display,
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function splitWords(value) {
|
|
223
|
+
const normalized = String(value || '')
|
|
224
|
+
.trim()
|
|
225
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
226
|
+
.replace(/[^a-zA-Z0-9]+/g, ' ')
|
|
227
|
+
.trim()
|
|
228
|
+
if (!normalized) return []
|
|
229
|
+
return normalized.split(/\s+/).map((word) => word.toLowerCase())
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function capitalize(value) {
|
|
233
|
+
return value ? value[0].toUpperCase() + value.slice(1) : ''
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function resolveProjectRoot(startDir, includeFrontend, includeBackend) {
|
|
237
|
+
const candidates = [
|
|
238
|
+
startDir,
|
|
239
|
+
path.join(startDir, 'packages', 'create-keystone-app', 'template'),
|
|
240
|
+
path.join(startDir, 'template'),
|
|
241
|
+
]
|
|
242
|
+
for (const candidate of candidates) {
|
|
243
|
+
const frontendOk =
|
|
244
|
+
!includeFrontend || fs.existsSync(path.join(candidate, 'apps', 'web', 'src', 'modules', 'example'))
|
|
245
|
+
const backendOk =
|
|
246
|
+
!includeBackend || fs.existsSync(path.join(candidate, 'apps', 'server', 'internal', 'modules', 'example'))
|
|
247
|
+
if (frontendOk && backendOk) {
|
|
248
|
+
return candidate
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
fail('Could not find a project with apps/web and apps/server module templates.')
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function copyModule(srcDir, destDir, names, mode) {
|
|
255
|
+
copyDir(srcDir, destDir, (entryName) => renameSegment(entryName, names), (content, filePath) =>
|
|
256
|
+
transformContent(content, filePath, names, mode)
|
|
257
|
+
)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function copyDir(src, dest, renameEntry, transform) {
|
|
261
|
+
fs.mkdirSync(dest, { recursive: true })
|
|
262
|
+
const entries = fs.readdirSync(src, { withFileTypes: true })
|
|
263
|
+
for (const entry of entries) {
|
|
264
|
+
const srcPath = path.join(src, entry.name)
|
|
265
|
+
const nextName = renameEntry(entry.name)
|
|
266
|
+
const destPath = path.join(dest, nextName)
|
|
267
|
+
if (entry.isDirectory()) {
|
|
268
|
+
copyDir(srcPath, destPath, renameEntry, transform)
|
|
269
|
+
} else {
|
|
270
|
+
copyFile(srcPath, destPath, transform)
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function copyFile(src, dest, transform) {
|
|
276
|
+
const content = fs.readFileSync(src, 'utf8')
|
|
277
|
+
const next = transform ? transform(content, dest) : content
|
|
278
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true })
|
|
279
|
+
fs.writeFileSync(dest, next, 'utf8')
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function renameSegment(segment, names) {
|
|
283
|
+
if (segment === 'example') {
|
|
284
|
+
return names.kebab
|
|
285
|
+
}
|
|
286
|
+
return segment.replace(/Example/g, names.pascal).replace(/example/g, names.camel)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function transformContent(content, filePath, names, mode) {
|
|
290
|
+
const ext = path.extname(filePath).toLowerCase()
|
|
291
|
+
if (ext === '.go') {
|
|
292
|
+
return replaceInCode(
|
|
293
|
+
content,
|
|
294
|
+
[
|
|
295
|
+
['Example', names.pascal],
|
|
296
|
+
['example', names.lower],
|
|
297
|
+
],
|
|
298
|
+
(value) => replaceStringLiteral(value, names, mode === 'backend' ? names.lower : names.camel)
|
|
299
|
+
)
|
|
300
|
+
}
|
|
301
|
+
if (['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'].includes(ext)) {
|
|
302
|
+
return replaceInCode(
|
|
303
|
+
content,
|
|
304
|
+
[
|
|
305
|
+
['Example', names.pascal],
|
|
306
|
+
['example', names.camel],
|
|
307
|
+
],
|
|
308
|
+
(value) => replaceStringLiteral(value, names, names.camel)
|
|
309
|
+
)
|
|
310
|
+
}
|
|
311
|
+
return replaceText(content, names)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function replaceText(content, names) {
|
|
315
|
+
return applyReplacements(content, [
|
|
316
|
+
['Example', names.display],
|
|
317
|
+
['example', names.kebab],
|
|
318
|
+
])
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function replaceStringLiteral(value, names, pathWord) {
|
|
322
|
+
if (value.startsWith('./') || value.startsWith('../')) {
|
|
323
|
+
return applyReplacements(value, [
|
|
324
|
+
['Example', names.pascal],
|
|
325
|
+
['example', pathWord],
|
|
326
|
+
])
|
|
327
|
+
}
|
|
328
|
+
return applyReplacements(value, [
|
|
329
|
+
['Example', names.display],
|
|
330
|
+
['example', names.kebab],
|
|
331
|
+
])
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function replaceInCode(content, codeReplacements, stringReplacer) {
|
|
335
|
+
let out = ''
|
|
336
|
+
let i = 0
|
|
337
|
+
let start = 0
|
|
338
|
+
let inString = null
|
|
339
|
+
let escaped = false
|
|
340
|
+
while (i < content.length) {
|
|
341
|
+
const ch = content[i]
|
|
342
|
+
if (!inString) {
|
|
343
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
344
|
+
out += applyReplacements(content.slice(start, i), codeReplacements)
|
|
345
|
+
inString = ch
|
|
346
|
+
out += ch
|
|
347
|
+
i += 1
|
|
348
|
+
start = i
|
|
349
|
+
continue
|
|
350
|
+
}
|
|
351
|
+
i += 1
|
|
352
|
+
continue
|
|
353
|
+
}
|
|
354
|
+
if (escaped) {
|
|
355
|
+
escaped = false
|
|
356
|
+
i += 1
|
|
357
|
+
continue
|
|
358
|
+
}
|
|
359
|
+
if (ch === '\\' && inString !== '`') {
|
|
360
|
+
escaped = true
|
|
361
|
+
i += 1
|
|
362
|
+
continue
|
|
363
|
+
}
|
|
364
|
+
if (ch === inString) {
|
|
365
|
+
out += stringReplacer(content.slice(start, i))
|
|
366
|
+
out += ch
|
|
367
|
+
inString = null
|
|
368
|
+
i += 1
|
|
369
|
+
start = i
|
|
370
|
+
continue
|
|
371
|
+
}
|
|
372
|
+
i += 1
|
|
373
|
+
}
|
|
374
|
+
if (start < content.length) {
|
|
375
|
+
const tail = content.slice(start)
|
|
376
|
+
out += inString ? stringReplacer(tail) : applyReplacements(tail, codeReplacements)
|
|
377
|
+
}
|
|
378
|
+
return out
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function applyReplacements(value, replacements) {
|
|
382
|
+
let next = value
|
|
383
|
+
for (const [from, to] of replacements) {
|
|
384
|
+
next = next.split(from).join(to)
|
|
385
|
+
}
|
|
386
|
+
return next
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function pruneFrontendCrud(targetDir, names) {
|
|
390
|
+
const servicesDir = path.join(targetDir, 'services')
|
|
391
|
+
if (fs.existsSync(servicesDir)) {
|
|
392
|
+
fs.rmSync(servicesDir, { recursive: true, force: true })
|
|
393
|
+
}
|
|
394
|
+
const typesFile = path.join(targetDir, 'types.ts')
|
|
395
|
+
if (fs.existsSync(typesFile)) {
|
|
396
|
+
fs.rmSync(typesFile, { force: true })
|
|
397
|
+
}
|
|
398
|
+
const pageFile = path.join(targetDir, 'pages', `${names.pascal}ItemsPage.tsx`)
|
|
399
|
+
const content = [
|
|
400
|
+
"import { Card, Typography } from 'antd'",
|
|
401
|
+
'',
|
|
402
|
+
`export function ${names.pascal}ItemsPage() {`,
|
|
403
|
+
' return (',
|
|
404
|
+
` <Card title="${names.display}">`,
|
|
405
|
+
' <Typography.Paragraph type="secondary">',
|
|
406
|
+
` ${names.display} module is ready. Start building your pages here.`,
|
|
407
|
+
' </Typography.Paragraph>',
|
|
408
|
+
' </Card>',
|
|
409
|
+
' )',
|
|
410
|
+
'}',
|
|
411
|
+
'',
|
|
412
|
+
].join('\n')
|
|
413
|
+
fs.writeFileSync(pageFile, content, 'utf8')
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function pruneBackendCrud(targetDir, names) {
|
|
417
|
+
const dirs = ['api', 'bootstrap', 'domain', 'infra']
|
|
418
|
+
for (const dir of dirs) {
|
|
419
|
+
const fullPath = path.join(targetDir, dir)
|
|
420
|
+
if (fs.existsSync(fullPath)) {
|
|
421
|
+
fs.rmSync(fullPath, { recursive: true, force: true })
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
const moduleFile = path.join(targetDir, 'module.go')
|
|
425
|
+
const content = [
|
|
426
|
+
`package ${names.lower}`,
|
|
427
|
+
'',
|
|
428
|
+
'import (',
|
|
429
|
+
'\t"github.com/gin-gonic/gin"',
|
|
430
|
+
'\t"gorm.io/gorm"',
|
|
431
|
+
'',
|
|
432
|
+
'\t"github.com/robsuncn/keystone/domain/permissions"',
|
|
433
|
+
'\t"github.com/robsuncn/keystone/infra/jobs"',
|
|
434
|
+
')',
|
|
435
|
+
'',
|
|
436
|
+
'type Module struct{}',
|
|
437
|
+
'',
|
|
438
|
+
'func NewModule() *Module {',
|
|
439
|
+
'\treturn &Module{}',
|
|
440
|
+
'}',
|
|
441
|
+
'',
|
|
442
|
+
'func (m *Module) Name() string {',
|
|
443
|
+
`\treturn "${names.kebab}"`,
|
|
444
|
+
'}',
|
|
445
|
+
'',
|
|
446
|
+
'func (m *Module) RegisterRoutes(_ *gin.RouterGroup) {}',
|
|
447
|
+
'',
|
|
448
|
+
'func (m *Module) RegisterModels() []interface{} {',
|
|
449
|
+
'\treturn nil',
|
|
450
|
+
'}',
|
|
451
|
+
'',
|
|
452
|
+
'func (m *Module) RegisterPermissions(reg *permissions.Registry) error {',
|
|
453
|
+
'\tif reg == nil {',
|
|
454
|
+
'\t\treturn nil',
|
|
455
|
+
'\t}',
|
|
456
|
+
`\tif err := reg.CreateMenu("${names.kebab}:overview", "${names.display}", "${names.kebab}", 10); err != nil {`,
|
|
457
|
+
'\t\treturn err',
|
|
458
|
+
'\t}',
|
|
459
|
+
`\tif err := reg.CreateAction("${names.kebab}:overview:view", "View ${names.display}", "${names.kebab}", "${names.kebab}:overview"); err != nil {`,
|
|
460
|
+
'\t\treturn err',
|
|
461
|
+
'\t}',
|
|
462
|
+
'\treturn nil',
|
|
463
|
+
'}',
|
|
464
|
+
'',
|
|
465
|
+
'func (m *Module) RegisterJobs(_ *jobs.Registry) error {',
|
|
466
|
+
'\treturn nil',
|
|
467
|
+
'}',
|
|
468
|
+
'',
|
|
469
|
+
'func (m *Module) Migrate(_ *gorm.DB) error {',
|
|
470
|
+
'\treturn nil',
|
|
471
|
+
'}',
|
|
472
|
+
'',
|
|
473
|
+
'func (m *Module) Seed(_ *gorm.DB) error {',
|
|
474
|
+
'\treturn nil',
|
|
475
|
+
'}',
|
|
476
|
+
'',
|
|
477
|
+
].join('\n')
|
|
478
|
+
fs.writeFileSync(moduleFile, content, 'utf8')
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function registerFrontendModule(mainPath, names) {
|
|
482
|
+
if (!fs.existsSync(mainPath)) {
|
|
483
|
+
return
|
|
484
|
+
}
|
|
485
|
+
const content = fs.readFileSync(mainPath, 'utf8')
|
|
486
|
+
const importLine = `import './modules/${names.kebab}'`
|
|
487
|
+
if (content.includes(importLine)) {
|
|
488
|
+
return
|
|
489
|
+
}
|
|
490
|
+
const lines = content.split(/\r?\n/)
|
|
491
|
+
let lastImport = -1
|
|
492
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
493
|
+
if (/^\s*import\s/.test(lines[i])) {
|
|
494
|
+
lastImport = i
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
if (lastImport === -1) {
|
|
498
|
+
lines.unshift(importLine)
|
|
499
|
+
} else {
|
|
500
|
+
lines.splice(lastImport + 1, 0, importLine)
|
|
501
|
+
}
|
|
502
|
+
fs.writeFileSync(mainPath, lines.join('\n'), 'utf8')
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function registerBackendModule(manifestPath, configPath, names) {
|
|
506
|
+
updateManifest(manifestPath, names)
|
|
507
|
+
updateConfig(configPath, names)
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function updateManifest(manifestPath, names) {
|
|
511
|
+
if (!fs.existsSync(manifestPath)) {
|
|
512
|
+
return
|
|
513
|
+
}
|
|
514
|
+
const content = fs.readFileSync(manifestPath, 'utf8')
|
|
515
|
+
const lines = content.split(/\r?\n/)
|
|
516
|
+
const importStart = lines.findIndex((line) => line.trim() === 'import (')
|
|
517
|
+
if (importStart === -1) {
|
|
518
|
+
return
|
|
519
|
+
}
|
|
520
|
+
const importEnd = lines.findIndex((line, index) => index > importStart && line.trim() === ')')
|
|
521
|
+
if (importEnd === -1) {
|
|
522
|
+
return
|
|
523
|
+
}
|
|
524
|
+
const importBlock = lines.slice(importStart + 1, importEnd)
|
|
525
|
+
const exampleImport = importBlock.find((line) => line.includes('/internal/modules/example'))
|
|
526
|
+
if (!exampleImport) {
|
|
527
|
+
return
|
|
528
|
+
}
|
|
529
|
+
const pathMatch = exampleImport.match(/\"([^\"]+)\"/)
|
|
530
|
+
if (!pathMatch) {
|
|
531
|
+
return
|
|
532
|
+
}
|
|
533
|
+
const modulePath = pathMatch[1].replace(/\/example$/, `/${names.kebab}`)
|
|
534
|
+
const alias = names.lower
|
|
535
|
+
const importLine = `${alias} "${modulePath}"`
|
|
536
|
+
if (!importBlock.some((line) => line.includes(`/${names.kebab}"`))) {
|
|
537
|
+
lines.splice(importEnd, 0, importLine)
|
|
538
|
+
}
|
|
539
|
+
const registerLine = `\tRegister(${alias}.NewModule())`
|
|
540
|
+
if (!lines.some((line) => line.trim() === registerLine.trim())) {
|
|
541
|
+
const registerIndices = lines
|
|
542
|
+
.map((line, index) => ({ line, index }))
|
|
543
|
+
.filter(({ line }) => line.includes('Register('))
|
|
544
|
+
if (registerIndices.length > 0) {
|
|
545
|
+
lines.splice(registerIndices[registerIndices.length - 1].index + 1, 0, registerLine)
|
|
546
|
+
} else {
|
|
547
|
+
const clearIndex = lines.findIndex((line) => line.includes('Clear()'))
|
|
548
|
+
if (clearIndex !== -1) {
|
|
549
|
+
lines.splice(clearIndex + 1, 0, registerLine)
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
fs.writeFileSync(manifestPath, lines.join('\n'), 'utf8')
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function updateConfig(configPath, names) {
|
|
557
|
+
if (!fs.existsSync(configPath)) {
|
|
558
|
+
return
|
|
559
|
+
}
|
|
560
|
+
const content = fs.readFileSync(configPath, 'utf8')
|
|
561
|
+
if (content.includes(`- "${names.kebab}"`) || content.includes(`- '${names.kebab}'`)) {
|
|
562
|
+
return
|
|
563
|
+
}
|
|
564
|
+
const lines = content.split(/\r?\n/)
|
|
565
|
+
const modulesIndex = lines.findIndex((line) => line.trim() === 'modules:')
|
|
566
|
+
if (modulesIndex === -1) {
|
|
567
|
+
return
|
|
568
|
+
}
|
|
569
|
+
const enabledIndex = lines.findIndex((line, index) => index > modulesIndex && line.trim() === 'enabled:')
|
|
570
|
+
if (enabledIndex === -1) {
|
|
571
|
+
return
|
|
572
|
+
}
|
|
573
|
+
let insertIndex = enabledIndex + 1
|
|
574
|
+
let indent = null
|
|
575
|
+
for (let i = enabledIndex + 1; i < lines.length; i += 1) {
|
|
576
|
+
const line = lines[i]
|
|
577
|
+
if (!line.trim()) continue
|
|
578
|
+
if (!line.trim().startsWith('-')) {
|
|
579
|
+
insertIndex = i
|
|
580
|
+
break
|
|
581
|
+
}
|
|
582
|
+
if (indent === null) {
|
|
583
|
+
indent = line.match(/^\s*/)?.[0] ?? ''
|
|
584
|
+
}
|
|
585
|
+
insertIndex = i + 1
|
|
586
|
+
}
|
|
587
|
+
const prefix = indent ?? ' '
|
|
588
|
+
lines.splice(insertIndex, 0, `${prefix}- "${names.kebab}"`)
|
|
589
|
+
fs.writeFileSync(configPath, lines.join('\n'), 'utf8')
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function parseArgs(argv) {
|
|
593
|
+
const out = {
|
|
594
|
+
target: null,
|
|
595
|
+
frontendOnly: false,
|
|
596
|
+
backendOnly: false,
|
|
597
|
+
withCrud: false,
|
|
598
|
+
skipRegister: false,
|
|
599
|
+
help: false,
|
|
600
|
+
}
|
|
601
|
+
for (const arg of argv) {
|
|
602
|
+
if (arg === '--help' || arg === '-h') {
|
|
603
|
+
out.help = true
|
|
604
|
+
continue
|
|
605
|
+
}
|
|
606
|
+
if (arg === '--frontend-only') {
|
|
607
|
+
out.frontendOnly = true
|
|
608
|
+
continue
|
|
609
|
+
}
|
|
610
|
+
if (arg === '--backend-only') {
|
|
611
|
+
out.backendOnly = true
|
|
612
|
+
continue
|
|
613
|
+
}
|
|
614
|
+
if (arg === '--with-crud') {
|
|
615
|
+
out.withCrud = true
|
|
616
|
+
continue
|
|
617
|
+
}
|
|
618
|
+
if (arg === '--skip-register') {
|
|
619
|
+
out.skipRegister = true
|
|
620
|
+
continue
|
|
621
|
+
}
|
|
622
|
+
if (arg.startsWith('-')) {
|
|
623
|
+
fail(`Unknown option: ${arg}`)
|
|
624
|
+
}
|
|
625
|
+
if (!out.target) {
|
|
626
|
+
out.target = arg
|
|
627
|
+
continue
|
|
628
|
+
}
|
|
629
|
+
fail('Too many arguments.')
|
|
630
|
+
}
|
|
631
|
+
return out
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function fail(message) {
|
|
635
|
+
console.error(message)
|
|
636
|
+
console.error(usage)
|
|
637
|
+
process.exit(1)
|
|
638
|
+
}
|
package/package.json
CHANGED
|
@@ -1,19 +1,23 @@
|
|
|
1
|
-
{
|
|
1
|
+
{
|
|
2
2
|
"name": "@robsun/create-keystone-app",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
|
+
"scripts": {
|
|
5
|
+
"build": "node scripts/build.js",
|
|
6
|
+
"prepublishOnly": "node scripts/build.js && node scripts/prune-template-deps.js"
|
|
7
|
+
},
|
|
4
8
|
"publishConfig": {
|
|
5
9
|
"access": "public"
|
|
6
10
|
},
|
|
7
11
|
"bin": {
|
|
8
|
-
"create-keystone-app": "
|
|
12
|
+
"create-keystone-app": "dist/create-keystone-app.js",
|
|
13
|
+
"create-keystone-module": "dist/create-module.js"
|
|
9
14
|
},
|
|
10
15
|
"files": [
|
|
11
|
-
"
|
|
16
|
+
"dist",
|
|
12
17
|
"template",
|
|
13
18
|
"README.md"
|
|
14
19
|
],
|
|
15
20
|
"engines": {
|
|
16
21
|
"node": ">=18"
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
}
|
|
22
|
+
}
|
|
23
|
+
}
|
package/template/README.md
CHANGED
|
@@ -31,6 +31,18 @@ pnpm web:dev
|
|
|
31
31
|
- `pnpm test`:Web 类型检查 + Lint + Vitest + Go 测试。
|
|
32
32
|
- `pnpm clean`:清理构建产物。
|
|
33
33
|
|
|
34
|
+
## 模块生成器
|
|
35
|
+
```bash
|
|
36
|
+
pnpm create:module <module-name> [options]
|
|
37
|
+
```
|
|
38
|
+
不传 options 会进入交互式向导。
|
|
39
|
+
|
|
40
|
+
选项:
|
|
41
|
+
- `--frontend-only`:只生成前端模块。
|
|
42
|
+
- `--backend-only`:只生成后端模块。
|
|
43
|
+
- `--with-crud`:包含 CRUD 示例代码。
|
|
44
|
+
- `--skip-register`:跳过自动注册步骤。
|
|
45
|
+
|
|
34
46
|
## 目录结构
|
|
35
47
|
- `apps/web/`:React + Vite 壳与业务模块(`src/modules/*`)。
|
|
36
48
|
- `apps/server/`:Go API + 模块注册/迁移/种子数据(含 `config.yaml`)。
|
package/template/package.json
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
"build": "node scripts/build.js",
|
|
8
8
|
"build:web": "pnpm --filter web build",
|
|
9
9
|
"check-modules": "node scripts/check-modules.js",
|
|
10
|
+
"create:module": "pnpm dlx --package @robsun/create-keystone-app create-keystone-module",
|
|
10
11
|
"test": "pnpm check-modules && node scripts/test.js",
|
|
11
12
|
"lint": "pnpm --filter web lint",
|
|
12
13
|
"format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md,yml,yaml}\"",
|
package/template/pnpm-lock.yaml
CHANGED
|
@@ -27,8 +27,8 @@ importers:
|
|
|
27
27
|
specifier: ^6.1.0
|
|
28
28
|
version: 6.1.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
|
29
29
|
'@robsun/keystone-web-core':
|
|
30
|
-
specifier: 0.1.
|
|
31
|
-
version: 0.1.
|
|
30
|
+
specifier: 0.1.8
|
|
31
|
+
version: 0.1.8(@ant-design/icons@6.1.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/react@19.2.7)(antd@6.1.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react-router-dom@7.11.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(use-sync-external-store@1.6.0(react@19.2.3))(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2))
|
|
32
32
|
antd:
|
|
33
33
|
specifier: ^6.0.1
|
|
34
34
|
version: 6.1.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
|
@@ -924,7 +924,7 @@ packages:
|
|
|
924
924
|
react: '>=16.9.0'
|
|
925
925
|
react-dom: '>=16.9.0'
|
|
926
926
|
|
|
927
|
-
'@robsun/keystone-web-core@0.1.
|
|
927
|
+
'@robsun/keystone-web-core@0.1.8':
|
|
928
928
|
resolution: {integrity: sha512-r6Qfs1LxTZBv6GVIf5s6LREuTl/lskq8L3Y9YynUFKvL1tuZ3h3sThxPvvP7VQbBZn5As/XUv16kpqqVXX1HvQ==}
|
|
929
929
|
peerDependencies:
|
|
930
930
|
'@ant-design/icons': ^6.1.0
|
|
@@ -3744,7 +3744,7 @@ snapshots:
|
|
|
3744
3744
|
react: 19.2.3
|
|
3745
3745
|
react-dom: 19.2.3(react@19.2.3)
|
|
3746
3746
|
|
|
3747
|
-
'@robsun/keystone-web-core@0.1.
|
|
3747
|
+
'@robsun/keystone-web-core@0.1.8(@ant-design/icons@6.1.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/react@19.2.7)(antd@6.1.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react-router-dom@7.11.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(use-sync-external-store@1.6.0(react@19.2.3))(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2))':
|
|
3748
3748
|
dependencies:
|
|
3749
3749
|
'@ant-design/icons': 6.1.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
|
3750
3750
|
'@vitejs/plugin-react': 5.1.2(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2))
|
|
@@ -6027,3 +6027,4 @@ snapshots:
|
|
|
6027
6027
|
use-sync-external-store: 1.6.0(react@19.2.3)
|
|
6028
6028
|
|
|
6029
6029
|
zwitch@2.0.4: {}
|
|
6030
|
+
|