create-pardx-scaffold 0.1.5 → 0.1.7
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/package.json
CHANGED
|
@@ -59,6 +59,13 @@ function main() {
|
|
|
59
59
|
copied++;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
// 确保脚手架模板根目录包含 .gitignore
|
|
63
|
+
const gitignoreSrc = path.join(REPO_ROOT, '.gitignore');
|
|
64
|
+
const gitignoreDest = path.join(TEMPLATE_ROOT, '.gitignore');
|
|
65
|
+
if (fs.existsSync(gitignoreSrc)) {
|
|
66
|
+
fs.copyFileSync(gitignoreSrc, gitignoreDest);
|
|
67
|
+
}
|
|
68
|
+
|
|
62
69
|
console.log('export-scaffold: copied', copied, 'files to packages/create-pardx-scaffold/template/');
|
|
63
70
|
}
|
|
64
71
|
|
|
@@ -38,15 +38,17 @@ const rl = readline.createInterface({
|
|
|
38
38
|
const question = (query) => new Promise((resolve) => rl.question(query, resolve));
|
|
39
39
|
|
|
40
40
|
// Project configuration
|
|
41
|
+
// 注意:API 端口在 apps/api/config.local.yaml 的 app.port 配置;Web 端口由 Next.js 自动配置,此处不配置
|
|
41
42
|
const config = {
|
|
42
43
|
projectName: '',
|
|
43
44
|
projectDescription: '',
|
|
44
45
|
authorName: '',
|
|
45
46
|
authorEmail: '',
|
|
46
47
|
databaseUrl: '',
|
|
48
|
+
readDatabaseUrl: '',
|
|
47
49
|
redisUrl: '',
|
|
48
|
-
|
|
49
|
-
|
|
50
|
+
rabbitmqUrl: '',
|
|
51
|
+
baseHost: '127.0.0.1',
|
|
50
52
|
};
|
|
51
53
|
|
|
52
54
|
async function main() {
|
|
@@ -81,26 +83,31 @@ async function main() {
|
|
|
81
83
|
|
|
82
84
|
log.header('\n📦 Configuration');
|
|
83
85
|
|
|
84
|
-
config.apiPort = await question(
|
|
85
|
-
`${colors.cyan}API port${colors.reset} [3100]: `
|
|
86
|
-
);
|
|
87
|
-
config.apiPort = config.apiPort.trim() || '3100';
|
|
88
|
-
|
|
89
|
-
config.webPort = await question(
|
|
90
|
-
`${colors.cyan}Web port${colors.reset} [3000]: `
|
|
91
|
-
);
|
|
92
|
-
config.webPort = config.webPort.trim() || '3000';
|
|
93
|
-
|
|
94
86
|
config.databaseUrl = await question(
|
|
95
87
|
`${colors.cyan}Database URL${colors.reset} [postgresql://user:password@localhost:5432/dbname]: `
|
|
96
88
|
);
|
|
97
89
|
config.databaseUrl = config.databaseUrl.trim() || 'postgresql://user:password@localhost:5432/dbname';
|
|
98
90
|
|
|
91
|
+
config.readDatabaseUrl = await question(
|
|
92
|
+
`${colors.cyan}Read Database URL${colors.reset} [same as Database URL]: `
|
|
93
|
+
);
|
|
94
|
+
config.readDatabaseUrl = config.readDatabaseUrl.trim() || config.databaseUrl;
|
|
95
|
+
|
|
99
96
|
config.redisUrl = await question(
|
|
100
97
|
`${colors.cyan}Redis URL${colors.reset} [redis://localhost:6379]: `
|
|
101
98
|
);
|
|
102
99
|
config.redisUrl = config.redisUrl.trim() || 'redis://localhost:6379';
|
|
103
100
|
|
|
101
|
+
config.rabbitmqUrl = await question(
|
|
102
|
+
`${colors.cyan}RabbitMQ URL${colors.reset} [amqp://localhost:5672]: `
|
|
103
|
+
);
|
|
104
|
+
config.rabbitmqUrl = config.rabbitmqUrl.trim() || 'amqp://localhost:5672';
|
|
105
|
+
|
|
106
|
+
config.baseHost = await question(
|
|
107
|
+
`${colors.cyan}Base Host${colors.reset} [127.0.0.1]: `
|
|
108
|
+
);
|
|
109
|
+
config.baseHost = config.baseHost.trim() || '127.0.0.1';
|
|
110
|
+
|
|
104
111
|
rl.close();
|
|
105
112
|
|
|
106
113
|
// Display configuration summary
|
|
@@ -108,10 +115,11 @@ async function main() {
|
|
|
108
115
|
console.log(` Project Name: ${colors.green}${config.projectName}${colors.reset}`);
|
|
109
116
|
console.log(` Description: ${config.projectDescription}`);
|
|
110
117
|
console.log(` Author: ${config.authorName} <${config.authorEmail}>`);
|
|
111
|
-
console.log(`
|
|
112
|
-
console.log(` Web Port: ${config.webPort}`);
|
|
118
|
+
console.log(` Base Host: ${config.baseHost}`);
|
|
113
119
|
console.log(` Database: ${config.databaseUrl}`);
|
|
120
|
+
console.log(` Read DB: ${config.readDatabaseUrl}`);
|
|
114
121
|
console.log(` Redis: ${config.redisUrl}`);
|
|
122
|
+
console.log(` RabbitMQ: ${config.rabbitmqUrl}`);
|
|
115
123
|
|
|
116
124
|
// Apply configuration
|
|
117
125
|
log.header('\n🔧 Applying Configuration');
|
|
@@ -158,27 +166,9 @@ async function applyConfiguration() {
|
|
|
158
166
|
});
|
|
159
167
|
log.success('API package.json updated');
|
|
160
168
|
|
|
161
|
-
// Create .env files
|
|
162
|
-
log.info('Creating environment files...');
|
|
163
|
-
|
|
164
|
-
path.join(rootDir, 'apps/web/.env.local'),
|
|
165
|
-
{
|
|
166
|
-
NEXT_PUBLIC_API_BASE_URL: `http://localhost:${config.apiPort}/api`,
|
|
167
|
-
}
|
|
168
|
-
);
|
|
169
|
-
createEnvFile(
|
|
170
|
-
path.join(rootDir, 'apps/api/.env'),
|
|
171
|
-
{
|
|
172
|
-
NODE_ENV: 'development',
|
|
173
|
-
PORT: config.apiPort,
|
|
174
|
-
HOST: '0.0.0.0',
|
|
175
|
-
DATABASE_URL: config.databaseUrl,
|
|
176
|
-
REDIS_URL: config.redisUrl,
|
|
177
|
-
JWT_SECRET: generateRandomSecret(),
|
|
178
|
-
JWT_EXPIRES_IN: '7d',
|
|
179
|
-
CORS_ORIGIN: `http://localhost:${config.webPort}`,
|
|
180
|
-
}
|
|
181
|
-
);
|
|
169
|
+
// Create .env files from .env.example
|
|
170
|
+
log.info('Creating environment files from .env.example...');
|
|
171
|
+
createEnvFromExample(rootDir, config);
|
|
182
172
|
log.success('Environment files created');
|
|
183
173
|
|
|
184
174
|
// Update README
|
|
@@ -198,11 +188,117 @@ function updatePackageJson(filePath, updates) {
|
|
|
198
188
|
fs.writeFileSync(filePath, JSON.stringify(packageJson, null, 2) + '\n');
|
|
199
189
|
}
|
|
200
190
|
|
|
191
|
+
/**
|
|
192
|
+
* 解析 .env.example 文件,提取 KEY=VALUE 行(含注释、空行)
|
|
193
|
+
*/
|
|
194
|
+
function parseEnvExample(content) {
|
|
195
|
+
const lines = [];
|
|
196
|
+
const vars = new Map();
|
|
197
|
+
for (const line of content.split(/\n/)) {
|
|
198
|
+
const trimmed = line.trimEnd();
|
|
199
|
+
if (!trimmed || trimmed.startsWith('#')) {
|
|
200
|
+
lines.push({ type: 'raw', value: trimmed });
|
|
201
|
+
} else {
|
|
202
|
+
const match = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
|
|
203
|
+
if (match) {
|
|
204
|
+
const key = match[1];
|
|
205
|
+
let value = match[2];
|
|
206
|
+
if (value.startsWith('"') && value.endsWith('"')) {
|
|
207
|
+
value = value.slice(1, -1);
|
|
208
|
+
}
|
|
209
|
+
lines.push({ type: 'var', key, raw: trimmed });
|
|
210
|
+
vars.set(key, value);
|
|
211
|
+
} else {
|
|
212
|
+
lines.push({ type: 'raw', value: trimmed });
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return { lines, vars };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* 根据 apps/api/.env.example 和 apps/web/.env.example 生成 .env 文件
|
|
221
|
+
*/
|
|
222
|
+
function createEnvFromExample(rootDir, config) {
|
|
223
|
+
const apiExamplePath = path.join(rootDir, 'apps/api/.env.example');
|
|
224
|
+
const webExamplePath = path.join(rootDir, 'apps/web/.env.example');
|
|
225
|
+
|
|
226
|
+
// 变量替换映射(apps/api),参考 apps/api/.env.example
|
|
227
|
+
// API 端口由 apps/api/config.local.yaml 的 app.port 配置,此处不覆盖 API_BASE_URL / INTERNAL_API_BASE_URL
|
|
228
|
+
const apiReplacements = {
|
|
229
|
+
BASE_HOST: config.baseHost,
|
|
230
|
+
DATABASE_URL: config.databaseUrl,
|
|
231
|
+
READ_DATABASE_URL: config.readDatabaseUrl,
|
|
232
|
+
REDIS_URL: config.redisUrl,
|
|
233
|
+
RABBITMQ_URL: config.rabbitmqUrl,
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
if (fs.existsSync(apiExamplePath)) {
|
|
237
|
+
const content = fs.readFileSync(apiExamplePath, 'utf8');
|
|
238
|
+
const { lines, vars } = parseEnvExample(content);
|
|
239
|
+
const outLines = [];
|
|
240
|
+
for (const item of lines) {
|
|
241
|
+
if (item.type === 'raw') {
|
|
242
|
+
outLines.push(item.value);
|
|
243
|
+
} else {
|
|
244
|
+
let val = apiReplacements[item.key];
|
|
245
|
+
if (val === undefined && item.key === 'RABBITMQ_EVENTS_URL') {
|
|
246
|
+
const orig = vars.get(item.key) || '';
|
|
247
|
+
val = orig.replace(/\$\{BASE_HOST\}/g, config.baseHost);
|
|
248
|
+
} else if (val === undefined) {
|
|
249
|
+
val = null;
|
|
250
|
+
}
|
|
251
|
+
const final = val !== null && val !== undefined ? `${item.key}=${val}` : item.raw;
|
|
252
|
+
outLines.push(final);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
const apiEnvPath = path.join(rootDir, 'apps/api/.env');
|
|
256
|
+
fs.writeFileSync(apiEnvPath, outLines.join('\n') + '\n');
|
|
257
|
+
|
|
258
|
+
// 追加 JWT_SECRET(若 .env.example 中无)
|
|
259
|
+
const apiEnvContent = fs.readFileSync(apiEnvPath, 'utf8');
|
|
260
|
+
if (!/^JWT_SECRET=/m.test(apiEnvContent)) {
|
|
261
|
+
fs.appendFileSync(apiEnvPath, `\n# JWT\nJWT_SECRET=${generateRandomSecret()}\nJWT_EXPIRES_IN=7d\n`);
|
|
262
|
+
}
|
|
263
|
+
} else {
|
|
264
|
+
createEnvFile(path.join(rootDir, 'apps/api/.env'), {
|
|
265
|
+
NODE_ENV: 'development',
|
|
266
|
+
DATABASE_URL: config.databaseUrl,
|
|
267
|
+
REDIS_URL: config.redisUrl,
|
|
268
|
+
RABBITMQ_URL: config.rabbitmqUrl,
|
|
269
|
+
JWT_SECRET: generateRandomSecret(),
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// apps/web:NEXT_PUBLIC_API_BASE_URL 等由 .env.example 提供,Next.js 端口自动配置,此处不覆盖
|
|
274
|
+
const webReplacements = {};
|
|
275
|
+
|
|
276
|
+
if (fs.existsSync(webExamplePath)) {
|
|
277
|
+
const content = fs.readFileSync(webExamplePath, 'utf8');
|
|
278
|
+
const { lines } = parseEnvExample(content);
|
|
279
|
+
const outLines = [];
|
|
280
|
+
for (const item of lines) {
|
|
281
|
+
if (item.type === 'raw') {
|
|
282
|
+
outLines.push(item.value);
|
|
283
|
+
} else {
|
|
284
|
+
const val = webReplacements[item.key] ?? null;
|
|
285
|
+
const final = val !== null ? `${item.key}=${val}` : item.raw;
|
|
286
|
+
outLines.push(final);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
fs.writeFileSync(
|
|
290
|
+
path.join(rootDir, 'apps/web/.env.local'),
|
|
291
|
+
outLines.join('\n') + '\n'
|
|
292
|
+
);
|
|
293
|
+
} else {
|
|
294
|
+
createEnvFile(path.join(rootDir, 'apps/web/.env.local'), {});
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
201
298
|
function createEnvFile(filePath, variables) {
|
|
202
299
|
const content = Object.entries(variables)
|
|
203
300
|
.map(([key, value]) => `${key}=${value}`)
|
|
204
301
|
.join('\n') + '\n';
|
|
205
|
-
|
|
206
302
|
fs.writeFileSync(filePath, content);
|
|
207
303
|
}
|
|
208
304
|
|
|
@@ -213,9 +309,9 @@ function updateReadme(filePath) {
|
|
|
213
309
|
}
|
|
214
310
|
|
|
215
311
|
let content = fs.readFileSync(filePath, 'utf8');
|
|
216
|
-
content = content.replace(/# PardxAI Monorepo Scaffold
|
|
312
|
+
content = content.replace(/# PardxAI Monorepo Scaffold[^\n]*/, `# ${config.projectName}`);
|
|
217
313
|
content = content.replace(
|
|
218
|
-
/A production-ready monorepo scaffold
|
|
314
|
+
/A comprehensive production-ready monorepo scaffold with complete implementations\./,
|
|
219
315
|
config.projectDescription
|
|
220
316
|
);
|
|
221
317
|
fs.writeFileSync(filePath, content);
|