create-project.mjs 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233
  1. #!/usr/bin/env node
  2. import { constants as fsConstants } from 'node:fs'
  3. import fs from 'node:fs/promises'
  4. import path from 'node:path'
  5. import process from 'node:process'
  6. import { fileURLToPath } from 'node:url'
  7. const __filename = fileURLToPath(import.meta.url)
  8. const __dirname = path.dirname(__filename)
  9. const repoRoot = path.resolve(__dirname, '../..')
  10. const catalogPath = path.join(__dirname, 'module-catalog.json')
  11. const backendParentArtifacts = {
  12. root: 'forge-server',
  13. framework: 'forge-framework',
  14. dependencies: 'forge-dependencies',
  15. starterParent: 'forge-starter-parent',
  16. pluginParent: 'forge-plugin-parent',
  17. flowParent: 'forge-flow',
  18. businessParent: 'forge-business',
  19. }
  20. const rootModuleArtifacts = [
  21. 'forge-admin-server',
  22. 'forge-framework',
  23. 'forge-report-server',
  24. 'forge-app-server',
  25. 'forge-flow',
  26. 'forge-business',
  27. ]
  28. const flowModuleArtifacts = [
  29. 'forge-flow-client',
  30. 'forge-flow-server',
  31. ]
  32. const businessModuleArtifacts = [
  33. 'forge-business-core',
  34. ]
  35. const ignoredNames = new Set([
  36. '.git',
  37. '.idea',
  38. '.vscode',
  39. '.DS_Store',
  40. '.vite',
  41. 'node_modules',
  42. 'dist',
  43. 'deploy',
  44. 'target',
  45. 'logs',
  46. '.pnpm-store',
  47. '__pycache__',
  48. ])
  49. const ignoredFileNames = new Set([
  50. 'application-dev.yml',
  51. '.env.local',
  52. '.flattened-pom.xml',
  53. ])
  54. const ignoredTemplatePathPrefixes = [
  55. 'forge-server/db/migration',
  56. ]
  57. const projectContextNames = [
  58. 'AGENTS.md',
  59. '.agents',
  60. 'code-copilot',
  61. ]
  62. const binaryExtensions = new Set([
  63. '.png',
  64. '.jpg',
  65. '.jpeg',
  66. '.gif',
  67. '.webp',
  68. '.ico',
  69. '.woff',
  70. '.woff2',
  71. '.ttf',
  72. '.eot',
  73. '.xdb',
  74. '.jar',
  75. '.zip',
  76. '.gz',
  77. '.pdf',
  78. ])
  79. main().catch((error) => {
  80. console.error(`\n[forge:create] ${error.message}`)
  81. process.exit(1)
  82. })
  83. async function main() {
  84. const catalog = await readJson(catalogPath)
  85. const args = parseArgs(process.argv.slice(2))
  86. if (args.help) {
  87. printHelp(catalog)
  88. return
  89. }
  90. const options = normalizeOptions(args, catalog)
  91. const selection = resolveSelection(catalog, options.preset, options.includeModuleIds)
  92. const artifactMap = buildArtifactMap(catalog, options)
  93. const applicationClassMap = buildApplicationClassMap(options)
  94. const outputRoot = path.resolve(process.cwd(), options.target)
  95. const adminServerArtifactId = artifactMap['forge-admin-server']
  96. await assertWritableTarget(outputRoot, options.force)
  97. await fs.mkdir(outputRoot, { recursive: true })
  98. const serverDirName = `${options.artifactPrefix}-server`
  99. const serverRoot = path.join(outputRoot, serverDirName)
  100. const selectedArtifacts = collectSelectedArtifacts(catalog, selection)
  101. await copyBackend(serverRoot)
  102. await pruneBackend(serverRoot, catalog, selection)
  103. await pruneBackendSourceGlue(serverRoot, selectedArtifacts)
  104. await patchBackendPoms(serverRoot, catalog, selection, selectedArtifacts)
  105. await rewritePomGroupIds(serverRoot, options.groupId)
  106. await rewriteRootPomArtifact(serverRoot, `${options.artifactPrefix}-server`)
  107. if (selection.frontendIds.has('admin-ui')) {
  108. await copyProjectDir(path.join(repoRoot, 'forge-admin-ui'), path.join(outputRoot, `${options.projectName}-admin-ui`))
  109. }
  110. if (selection.frontendIds.has('report-ui')) {
  111. await copyProjectDir(path.join(repoRoot, 'forge-report-ui'), path.join(outputRoot, `${options.projectName}-report-ui`))
  112. }
  113. await copyOptionalRootFiles(outputRoot)
  114. await copyProjectContextFiles(outputRoot)
  115. await writeGeneratedConfig(outputRoot, options, selection, catalog)
  116. const replacements = buildTextReplacements(artifactMap, applicationClassMap, options, selection)
  117. const hasReportUi = selection?.frontendIds?.has('report-ui')
  118. await rewriteTextFiles(outputRoot, replacements, hasReportUi)
  119. await moveJavaPackageDirectories(outputRoot, 'com.mdframe.forge', options.basePackage)
  120. await renameFilesByBasename(outputRoot, applicationClassMap, '.java')
  121. await renameArtifactDirectories(serverRoot, artifactMap)
  122. await writeSelectedSqlBundle(serverRoot, catalog, selection, replacements, options)
  123. printSummary(outputRoot, options, selection, catalog, adminServerArtifactId)
  124. }
  125. function parseArgs(argv) {
  126. const args = {
  127. _: [],
  128. }
  129. for (let index = 0; index < argv.length; index += 1) {
  130. const arg = argv[index]
  131. if (arg === '--') {
  132. continue
  133. }
  134. if (arg === '--help' || arg === '-h') {
  135. args.help = true
  136. continue
  137. }
  138. if (!arg.startsWith('--')) {
  139. args._.push(arg)
  140. continue
  141. }
  142. const eqIndex = arg.indexOf('=')
  143. if (eqIndex > -1) {
  144. args[arg.slice(2, eqIndex)] = arg.slice(eqIndex + 1)
  145. continue
  146. }
  147. const key = arg.slice(2)
  148. const next = argv[index + 1]
  149. if (!next || next.startsWith('--')) {
  150. args[key] = true
  151. continue
  152. }
  153. args[key] = next
  154. index += 1
  155. }
  156. return args
  157. }
  158. function normalizeOptions(args, catalog) {
  159. const target = args._[0]
  160. if (!target) {
  161. throw new Error('缺少目标目录。示例:pnpm forge:create -- ../smart-factory --base-package com.company.smartfactory')
  162. }
  163. const projectName = normalizeProjectName(args['project-name'] || path.basename(path.resolve(target)))
  164. const artifactPrefix = normalizeProjectName(args['artifact-prefix'] || projectName)
  165. const stripModulePrefix = args['strip-module-prefix']
  166. ? normalizeProjectName(args['strip-module-prefix'])
  167. : ''
  168. const moduleArtifactPrefix = normalizeProjectName(
  169. args['module-artifact-prefix'] || deriveModuleArtifactPrefix(artifactPrefix, stripModulePrefix),
  170. )
  171. const displayName = String(args['display-name'] || projectName)
  172. const preset = String(args.preset || 'ai-report')
  173. const basePackage = String(args['base-package'] || '').trim()
  174. const groupId = String(args['group-id'] || basePackage).trim()
  175. const databaseName = String(args['database-name'] || toSnakeCase(projectName)).trim()
  176. const includeModuleIds = parseModuleList(args.include || args['include-modules'])
  177. // 前端配置参数
  178. const adminTitle = String(args['admin-title'] || displayName)
  179. const adminApiPrefix = String(args['admin-api-prefix'] || '/api')
  180. const adminPort = String(args['admin-port'] || '5173')
  181. const adminProxyTarget = String(args['admin-proxy-target'] || 'http://localhost:8580')
  182. const adminPublicPath = String(args['admin-public-path'] || '/')
  183. const adminBaseUrl = String(args['admin-base-url'] || '/')
  184. if (!catalog.presets[preset]) {
  185. throw new Error(`未知 preset:${preset}。可选值:${Object.keys(catalog.presets).join(', ')}`)
  186. }
  187. for (const moduleId of includeModuleIds) {
  188. if (!catalog.modules[moduleId]) {
  189. throw new Error(`未知 include 模块:${moduleId}。可选模块见 scripts/forge-create/module-catalog.json`)
  190. }
  191. }
  192. if (!isValidJavaPackage(basePackage)) {
  193. throw new Error('请通过 --base-package 指定合法 Java 包名,例如 com.company.smartfactory')
  194. }
  195. if (!isValidJavaPackage(groupId)) {
  196. throw new Error('请通过 --group-id 指定合法 Maven groupId,默认等于 --base-package')
  197. }
  198. if (!/^[a-z][a-z0-9_]*$/.test(databaseName)) {
  199. throw new Error('数据库名只能使用小写字母、数字和下划线,并且必须以字母开头')
  200. }
  201. return {
  202. target,
  203. projectName,
  204. artifactPrefix,
  205. displayName,
  206. preset,
  207. basePackage,
  208. groupId,
  209. databaseName,
  210. includeModuleIds,
  211. moduleArtifactPrefix,
  212. stripModulePrefix,
  213. force: args.force === true || args.force === 'true',
  214. // 前端配置
  215. adminTitle,
  216. adminApiPrefix,
  217. adminPort,
  218. adminProxyTarget,
  219. adminPublicPath,
  220. adminBaseUrl,
  221. }
  222. }
  223. function deriveModuleArtifactPrefix(artifactPrefix, stripModulePrefix) {
  224. if (!stripModulePrefix) {
  225. return artifactPrefix
  226. }
  227. if (artifactPrefix === stripModulePrefix) {
  228. throw new Error('--strip-module-prefix 不能等于完整 artifact 前缀,否则子模块前缀为空')
  229. }
  230. if (!artifactPrefix.startsWith(`${stripModulePrefix}-`)) {
  231. throw new Error(`--strip-module-prefix 必须是 artifact 前缀的开头:${artifactPrefix}`)
  232. }
  233. return artifactPrefix.slice(stripModulePrefix.length + 1)
  234. }
  235. function parseModuleList(value) {
  236. if (!value) {
  237. return []
  238. }
  239. return String(value)
  240. .split(/[,\s]+/)
  241. .map(item => item.trim())
  242. .filter(Boolean)
  243. }
  244. function normalizeProjectName(value) {
  245. const normalized = String(value || '')
  246. .trim()
  247. .replace(/_/g, '-')
  248. .toLowerCase()
  249. if (!/^[a-z][a-z0-9-]*$/.test(normalized)) {
  250. throw new Error(`项目英文名不合法:${value}。只允许小写字母、数字、中横线,并且必须以字母开头`)
  251. }
  252. return normalized
  253. }
  254. function isValidJavaPackage(value) {
  255. return /^[a-z_][a-z0-9_]*(\.[a-z_][a-z0-9_]*)+$/.test(value)
  256. }
  257. function resolveSelection(catalog, presetName, includeModuleIds = []) {
  258. const preset = catalog.presets[presetName]
  259. const selectedModuleIds = new Set()
  260. const frontendIds = new Set()
  261. const visit = (moduleId) => {
  262. const moduleInfo = catalog.modules[moduleId]
  263. if (!moduleInfo) {
  264. throw new Error(`模块清单缺少定义:${moduleId}`)
  265. }
  266. if (moduleInfo.type === 'frontend') {
  267. frontendIds.add(moduleId)
  268. return
  269. }
  270. if (selectedModuleIds.has(moduleId)) {
  271. return
  272. }
  273. selectedModuleIds.add(moduleId)
  274. for (const dependency of moduleInfo.dependencies || []) {
  275. visit(dependency)
  276. }
  277. }
  278. for (const root of preset.roots) {
  279. visit(root)
  280. }
  281. for (const moduleId of includeModuleIds) {
  282. visit(moduleId)
  283. }
  284. return {
  285. presetName,
  286. selectedModuleIds,
  287. frontendIds,
  288. }
  289. }
  290. function collectSelectedArtifacts(catalog, selection) {
  291. const artifacts = new Set(Object.values(backendParentArtifacts))
  292. for (const moduleId of selection.selectedModuleIds) {
  293. const artifactId = catalog.modules[moduleId]?.artifactId
  294. if (artifactId) {
  295. artifacts.add(artifactId)
  296. }
  297. }
  298. if (![...selection.selectedModuleIds].some(id => catalog.modules[id]?.type === 'flow')) {
  299. artifacts.delete(backendParentArtifacts.flowParent)
  300. }
  301. if (![...selection.selectedModuleIds].some(id => catalog.modules[id]?.type === 'business')) {
  302. artifacts.delete(backendParentArtifacts.businessParent)
  303. }
  304. return artifacts
  305. }
  306. function buildArtifactMap(catalog, options) {
  307. const modulePrefix = options.moduleArtifactPrefix
  308. const map = {
  309. [backendParentArtifacts.framework]: `${modulePrefix}-framework`,
  310. [backendParentArtifacts.dependencies]: `${modulePrefix}-dependencies`,
  311. [backendParentArtifacts.starterParent]: `${modulePrefix}-starter-parent`,
  312. [backendParentArtifacts.pluginParent]: `${modulePrefix}-plugin-parent`,
  313. [backendParentArtifacts.flowParent]: `${modulePrefix}-flow`,
  314. [backendParentArtifacts.businessParent]: `${modulePrefix}-business`,
  315. 'forge-admin': `${modulePrefix}-admin`,
  316. 'forge-report': `${modulePrefix}-report`,
  317. 'forge-starter-property': `${modulePrefix}-starter-property`,
  318. }
  319. for (const moduleInfo of Object.values(catalog.modules)) {
  320. if (!moduleInfo.artifactId) {
  321. continue
  322. }
  323. const next = moduleInfo.artifactId
  324. .replace(/^forge-/, `${modulePrefix}-`)
  325. .replace(/-parent$/, '-parent')
  326. map[moduleInfo.artifactId] = next
  327. }
  328. return map
  329. }
  330. function buildApplicationClassMap(options) {
  331. const prefix = toPascalCase(options.projectName)
  332. return {
  333. ForgeAdminApplication: `${prefix}AdminApplication`,
  334. ForgeReportApplication: `${prefix}ReportApplication`,
  335. ForgeAppServerApplication: `${prefix}AppServerApplication`,
  336. ForgeFlowApplication: `${prefix}FlowApplication`,
  337. }
  338. }
  339. /**
  340. * 将字符串转换为 PascalCase(大驼峰)
  341. * 支持:kebab-case、snake_case、space 分隔、混合大小写
  342. * 示例:my-app → MyApp, my_app → MyApp, my app → MyApp, MyApp → MyApp
  343. */
  344. function toPascalCase(str) {
  345. if (!str) return '';
  346. return str
  347. // 1. 把所有分隔符(- _ 空格)替换成空格,统一处理
  348. .replace(/[-_\s]+/g, ' ')
  349. // 2. 每个单词首字母大写,其余小写
  350. .replace(/\b\w+/g, (word) => {
  351. return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
  352. })
  353. // 3. 去掉所有空格
  354. .replace(/ /g, '');
  355. }
  356. async function copyBackend(serverRoot) {
  357. await copyProjectDir(path.join(repoRoot, 'forge-server'), serverRoot)
  358. }
  359. async function copyProjectDir(source, target) {
  360. await fs.cp(source, target, {
  361. recursive: true,
  362. filter: sourcePath => shouldCopyPath(sourcePath),
  363. })
  364. }
  365. function shouldCopyPath(sourcePath) {
  366. const baseName = path.basename(sourcePath)
  367. if (ignoredNames.has(baseName) || ignoredFileNames.has(baseName)) {
  368. return false
  369. }
  370. return !isIgnoredTemplatePath(sourcePath)
  371. }
  372. function isIgnoredTemplatePath(sourcePath) {
  373. const relativePath = path.relative(repoRoot, sourcePath).split(path.sep).join('/')
  374. return ignoredTemplatePathPrefixes.some(prefix => relativePath === prefix || relativePath.startsWith(`${prefix}/`))
  375. }
  376. async function pruneBackend(serverRoot, catalog, selection) {
  377. const selectedArtifacts = collectSelectedArtifacts(catalog, selection)
  378. await pruneModuleDirectories(serverRoot, rootModuleArtifacts, selectedArtifacts)
  379. await pruneModuleDirectories(
  380. path.join(serverRoot, 'forge-framework/forge-plugin-parent'),
  381. artifactsByType(catalog, 'plugin'),
  382. selectedArtifacts,
  383. )
  384. await pruneModuleDirectories(
  385. path.join(serverRoot, 'forge-framework/forge-starter-parent'),
  386. artifactsByType(catalog, 'starter'),
  387. selectedArtifacts,
  388. )
  389. await pruneModuleDirectories(path.join(serverRoot, 'forge-flow'), flowModuleArtifacts, selectedArtifacts)
  390. await pruneModuleDirectories(path.join(serverRoot, 'forge-business'), businessModuleArtifacts, selectedArtifacts)
  391. }
  392. async function pruneBackendSourceGlue(serverRoot, selectedArtifacts) {
  393. const adminServerRoot = path.join(serverRoot, 'forge-admin-server')
  394. if (!selectedArtifacts.has('forge-plugin-generator')) {
  395. await fs.rm(path.join(adminServerRoot, 'src/main/java/com/mdframe/forge/admin/bridge'), {
  396. recursive: true,
  397. force: true,
  398. })
  399. }
  400. else if (!selectedArtifacts.has('forge-plugin-ai')) {
  401. await writeFallbackAiClientAdapter(adminServerRoot)
  402. }
  403. if (!selectedArtifacts.has('forge-plugin-ai')) {
  404. await fs.rm(path.join(adminServerRoot, 'src/main/java/com/mdframe/forge/admin/ai'), {
  405. recursive: true,
  406. force: true,
  407. })
  408. }
  409. }
  410. async function writeFallbackAiClientAdapter(adminServerRoot) {
  411. const adapterFile = path.join(adminServerRoot, 'src/main/java/com/mdframe/forge/admin/bridge/AiClientAdapterImpl.java')
  412. const content = `package com.mdframe.forge.admin.bridge;
  413. import com.mdframe.forge.plugin.generator.service.AiClientAdapter;
  414. import org.slf4j.Logger;
  415. import org.slf4j.LoggerFactory;
  416. import org.springframework.stereotype.Component;
  417. import reactor.core.publisher.Flux;
  418. import java.util.Map;
  419. /**
  420. * Fallback AI adapter for generated projects that keep generator but exclude plugin-ai.
  421. */
  422. @Component
  423. public class AiClientAdapterImpl implements AiClientAdapter {
  424. private static final Logger log = LoggerFactory.getLogger(AiClientAdapterImpl.class);
  425. private static final String FALLBACK_REASON = "plugin-ai is not included in this generated project";
  426. @Override
  427. public AiClientResult call(String agentCode, String message, Map<String, String> contextVars) {
  428. return fallback(agentCode);
  429. }
  430. @Override
  431. public AiClientResult call(String agentCode, String message, Map<String, String> contextVars,
  432. Integer timeoutSeconds) {
  433. return fallback(agentCode);
  434. }
  435. @Override
  436. public Flux<String> stream(String userInput, String agentCode, String message, Map<String, String> contextVars) {
  437. return emptyStream(agentCode);
  438. }
  439. @Override
  440. public Flux<String> stream(String userInput, String sessionId, String agentCode, String message,
  441. Map<String, String> contextVars) {
  442. return emptyStream(agentCode);
  443. }
  444. @Override
  445. public Flux<String> stream(String userInput, String sessionId, String agentCode, String message,
  446. Map<String, String> contextVars, Long providerId, Long modelId,
  447. Double temperature, Integer maxTokens) {
  448. return emptyStream(agentCode);
  449. }
  450. @Override
  451. public String loadContextSpec(String agentCode) {
  452. return "";
  453. }
  454. private AiClientResult fallback(String agentCode) {
  455. log.warn("[AiClientAdapter] Skip AI call because plugin-ai is not included: agentCode={}", agentCode);
  456. return AiClientResult.fallback(FALLBACK_REASON);
  457. }
  458. private Flux<String> emptyStream(String agentCode) {
  459. log.warn("[AiClientAdapter] Skip AI stream because plugin-ai is not included: agentCode={}", agentCode);
  460. return Flux.empty();
  461. }
  462. }
  463. `
  464. await fs.writeFile(adapterFile, content)
  465. }
  466. function artifactsByType(catalog, type) {
  467. return Object.values(catalog.modules)
  468. .filter(moduleInfo => moduleInfo.type === type)
  469. .map(moduleInfo => moduleInfo.artifactId)
  470. }
  471. async function pruneModuleDirectories(parentDir, artifactIds, selectedArtifacts) {
  472. if (!(await exists(parentDir))) {
  473. return
  474. }
  475. for (const artifactId of artifactIds) {
  476. if (!selectedArtifacts.has(artifactId)) {
  477. await fs.rm(path.join(parentDir, artifactId), { recursive: true, force: true })
  478. }
  479. }
  480. }
  481. async function patchBackendPoms(serverRoot, catalog, selection, selectedArtifacts) {
  482. const selectedRootModules = rootModuleArtifacts.filter((artifactId) => {
  483. if (artifactId === 'forge-framework') {
  484. return true
  485. }
  486. return selectedArtifacts.has(artifactId)
  487. })
  488. await replacePomModules(path.join(serverRoot, 'pom.xml'), selectedRootModules)
  489. await replacePomModules(path.join(serverRoot, 'forge-framework/pom.xml'), [
  490. 'forge-dependencies',
  491. 'forge-starter-parent',
  492. 'forge-plugin-parent',
  493. ])
  494. await replacePomModules(
  495. path.join(serverRoot, 'forge-framework/forge-plugin-parent/pom.xml'),
  496. artifactsByType(catalog, 'plugin').filter(artifactId => selectedArtifacts.has(artifactId)),
  497. )
  498. await replacePomModules(
  499. path.join(serverRoot, 'forge-framework/forge-starter-parent/pom.xml'),
  500. artifactsByType(catalog, 'starter').filter(artifactId => selectedArtifacts.has(artifactId)),
  501. )
  502. if (selectedArtifacts.has('forge-flow')) {
  503. await replacePomModules(
  504. path.join(serverRoot, 'forge-flow/pom.xml'),
  505. flowModuleArtifacts.filter(artifactId => selectedArtifacts.has(artifactId)),
  506. )
  507. }
  508. if (selectedArtifacts.has('forge-business')) {
  509. await replacePomModules(
  510. path.join(serverRoot, 'forge-business/pom.xml'),
  511. businessModuleArtifacts.filter(artifactId => selectedArtifacts.has(artifactId)),
  512. )
  513. }
  514. const pomFiles = await collectFiles(serverRoot, filePath => path.basename(filePath) === 'pom.xml')
  515. for (const pomFile of pomFiles) {
  516. await prunePomDependencies(pomFile, selectedArtifacts)
  517. }
  518. await patchBusinessCoreDependency(serverRoot, selectedArtifacts)
  519. }
  520. async function replacePomModules(pomFile, modules) {
  521. if (!(await exists(pomFile))) {
  522. return
  523. }
  524. const content = await fs.readFile(pomFile, 'utf8')
  525. const moduleContent = [
  526. ' <modules>',
  527. ...modules.map(moduleName => ` <module>${moduleName}</module>`),
  528. ' </modules>',
  529. ].join('\n')
  530. const nextContent = content.includes('<modules>')
  531. ? content.replace(/[\t ]*<modules>[\s\S]*?<\/modules>/, moduleContent)
  532. : content
  533. await fs.writeFile(pomFile, nextContent)
  534. }
  535. async function prunePomDependencies(pomFile, selectedArtifacts) {
  536. const content = await fs.readFile(pomFile, 'utf8')
  537. const nextContent = content.replace(/[\t ]*<dependency>[\s\S]*?<groupId>com\.mdframe\.forge<\/groupId>[\s\S]*?<artifactId>(forge[^<]+)<\/artifactId>[\s\S]*?<\/dependency>\s*/g, (block, artifactId) => {
  538. return selectedArtifacts.has(artifactId) ? block : ''
  539. })
  540. if (nextContent !== content) {
  541. await fs.writeFile(pomFile, nextContent)
  542. }
  543. }
  544. async function patchBusinessCoreDependency(serverRoot, selectedArtifacts) {
  545. if (!selectedArtifacts.has('forge-business-core') || !selectedArtifacts.has('forge-admin-server')) {
  546. return
  547. }
  548. await ensurePomDependency(
  549. path.join(serverRoot, 'forge-admin-server/pom.xml'),
  550. 'com.mdframe.forge',
  551. 'forge-business-core',
  552. '${revision}',
  553. )
  554. }
  555. async function ensurePomDependency(pomFile, groupId, artifactId, version) {
  556. if (!(await exists(pomFile))) {
  557. return
  558. }
  559. const content = await fs.readFile(pomFile, 'utf8')
  560. if (content.includes(`<artifactId>${artifactId}</artifactId>`)) {
  561. return
  562. }
  563. const dependencyLines = [
  564. '',
  565. ' <dependency>',
  566. ` <groupId>${groupId}</groupId>`,
  567. ` <artifactId>${artifactId}</artifactId>`,
  568. ]
  569. if (version) {
  570. dependencyLines.push(` <version>${version}</version>`)
  571. }
  572. dependencyLines.push(' </dependency>')
  573. const dependency = `${dependencyLines.join('\n')}\n`
  574. const nextContent = content.replace(/\s*<\/dependencies>/, `${dependency} </dependencies>`)
  575. if (nextContent !== content) {
  576. await fs.writeFile(pomFile, nextContent)
  577. }
  578. }
  579. async function rewritePomGroupIds(serverRoot, groupId) {
  580. const pomFiles = await collectFiles(serverRoot, filePath => path.basename(filePath) === 'pom.xml')
  581. for (const pomFile of pomFiles) {
  582. const content = await fs.readFile(pomFile, 'utf8')
  583. const nextContent = content.split('com.mdframe.forge').join(groupId)
  584. if (nextContent !== content) {
  585. await fs.writeFile(pomFile, nextContent)
  586. }
  587. }
  588. }
  589. async function rewriteRootPomArtifact(serverRoot, rootArtifactId) {
  590. const pomFiles = await collectFiles(serverRoot, filePath => path.basename(filePath) === 'pom.xml')
  591. for (const pomFile of pomFiles) {
  592. const content = await fs.readFile(pomFile, 'utf8')
  593. const nextContent = content
  594. .split('<artifactId>forge-server</artifactId>').join(`<artifactId>${rootArtifactId}</artifactId>`)
  595. .split('<artifactId>forge</artifactId>').join(`<artifactId>${rootArtifactId}</artifactId>`)
  596. .split('<name>forge-server</name>').join(`<name>${rootArtifactId}</name>`)
  597. .split('<name>forge</name>').join(`<name>${rootArtifactId}</name>`)
  598. .split('<description>forge-server</description>').join(`<description>${rootArtifactId}</description>`)
  599. .split('<description>forge</description>').join(`<description>${rootArtifactId}</description>`)
  600. if (nextContent !== content) {
  601. await fs.writeFile(pomFile, nextContent)
  602. }
  603. }
  604. }
  605. function buildTextReplacements(artifactMap, applicationClassMap, options, selection) {
  606. const snakeName = toSnakeCase(options.projectName)
  607. const serverDirName = `${options.artifactPrefix}-server`
  608. const adminServerArtifactId = artifactMap['forge-admin-server']
  609. const reportServerArtifactId = artifactMap['forge-report-server']
  610. const reportPath = `/${options.projectName}-report`
  611. const hasReportUi = selection?.frontendIds?.has('report-ui')
  612. const replacements = [
  613. ['com.mdframe.forge', options.basePackage],
  614. ['com/mdframe/forge', options.basePackage.replaceAll('.', '/')],
  615. ['Forge Admin', options.displayName],
  616. ['企业级中后台基础框架', options.displayName],
  617. ['forge-project', options.projectName],
  618. ['cd forge-server &&', `cd ${serverDirName} &&`],
  619. ['cd forge-server ', `cd ${serverDirName} `],
  620. ['cd forge-server\n', `cd ${serverDirName}\n`],
  621. ['cd forge &&', `cd ${serverDirName} &&`],
  622. ['cd forge ', `cd ${serverDirName} `],
  623. ['cd forge\n', `cd ${serverDirName}\n`],
  624. ['forge-server/ # 后端根目录', `${serverDirName}/ # 后端根目录`],
  625. ['forge/ # 后端根目录', `${serverDirName}/ # 后端根目录`],
  626. ['forge-server/ # 后端根工程', `${serverDirName}/ # 后端根工程`],
  627. ['forge/ # 后端根工程', `${serverDirName}/ # 后端根工程`],
  628. ['forge-server/\n', `${serverDirName}/\n`],
  629. ['forge/\n', `${serverDirName}/\n`],
  630. ['CREATE DATABASE forge ', `CREATE DATABASE ${options.databaseName} `],
  631. ['mysql -u root -p forge ', `mysql -u root -p ${options.databaseName} `],
  632. ['forge-admin-ui', `${options.projectName}-admin-ui`],
  633. ['forge-server/forge-admin/', `${serverDirName}/${adminServerArtifactId}/`],
  634. ['forge/forge-admin/', `${serverDirName}/${adminServerArtifactId}/`],
  635. ['forge-admin/', `${adminServerArtifactId}/`],
  636. ['forge-server/db', `${serverDirName}/db`],
  637. ['forge/db', `${serverDirName}/db`],
  638. ['forge-server/scripts', `${serverDirName}/scripts`],
  639. ['forge/scripts', `${serverDirName}/scripts`],
  640. ['forge-server/var', `${serverDirName}/var`],
  641. ['forge/var', `${serverDirName}/var`],
  642. ['forge_admin_new', options.databaseName],
  643. ['forge_admin', options.databaseName],
  644. ['forge_flow', `${options.databaseName}_flow`],
  645. ['forge_schema_history', `${snakeName}_schema_history`],
  646. ['vue-naive-admin', `${options.projectName}-admin-ui`],
  647. ['com.forge', options.basePackage],
  648. // 前端环境变量替换 - 基础配置
  649. ['VITE_TITLE=企业级中后台基础框架', `VITE_TITLE=${options.adminTitle || options.displayName}`],
  650. ['VITE_TITLE=Forge Admin', `VITE_TITLE=${options.adminTitle || options.displayName}`],
  651. ['VITE_HTTP_PORT=3000', `VITE_HTTP_PORT=${options.adminPort || '5173'}`],
  652. ['VITE_HTTP_PORT=5173', `VITE_HTTP_PORT=${options.adminPort || '5173'}`],
  653. ['VITE_HTTP_PORT=5174', `VITE_HTTP_PORT=${options.adminPort || '5174'}`],
  654. // 路径替换
  655. ['VITE_PUBLIC_PATH=/forge\n', `VITE_PUBLIC_PATH=${options.adminPublicPath || '/'}\n`],
  656. ['VITE_BASE_URL=/forge\n', `VITE_BASE_URL=${options.adminBaseUrl || '/'}\n`],
  657. ['VITE_PUBLIC_PATH=/forge', `VITE_PUBLIC_PATH=${options.adminPublicPath || '/'}`],
  658. ['VITE_BASE_URL=/forge', `VITE_BASE_URL=${options.adminBaseUrl || '/'}`],
  659. // API 前缀替换
  660. ['VITE_REQUEST_PREFIX=/forge-api', `VITE_REQUEST_PREFIX=${options.adminApiPrefix || '/api'}`],
  661. ['VITE_REQUEST_PREFIX=/dev-api', `VITE_REQUEST_PREFIX=${options.adminApiPrefix || '/api'}`],
  662. ['VITE_REQUEST_PREFIX=/api', `VITE_REQUEST_PREFIX=${options.adminApiPrefix || '/api'}`],
  663. // 代理地址替换
  664. ['VITE_HTTP_PROXY_TARGET=http://localhost:8580', `VITE_HTTP_PROXY_TARGET=${options.adminProxyTarget || 'http://localhost:8580'}`],
  665. ['VITE_HTTP_PROXY_TARGET=http://127.0.0.1:8580/', `VITE_HTTP_PROXY_TARGET=${options.adminProxyTarget || 'http://localhost:8580'}/`],
  666. ['VITE_HTTP_PROXY_TARGET=http://127.0.0.1:8580', `VITE_HTTP_PROXY_TARGET=${options.adminProxyTarget || 'http://localhost:8580'}`],
  667. ]
  668. // 只有选择了报表模块才替换报表相关配置
  669. if (hasReportUi) {
  670. replacements.push(
  671. ['forge-report-ui', `${options.projectName}-report-ui`],
  672. ['forge-server/forge-report/', `${serverDirName}/${reportServerArtifactId}/`],
  673. ['forge/forge-report/', `${serverDirName}/${reportServerArtifactId}/`],
  674. ['forge-report/', `${reportServerArtifactId}/`],
  675. ['forge_report', `${snakeName}_report`],
  676. ['forge_pc_001', `${snakeName}_pc_001`],
  677. ['/forge-report', reportPath],
  678. ['VITE_SSO_TARGET_CLIENT=forge_report', `VITE_SSO_TARGET_CLIENT=${snakeName}_report`],
  679. ['VITE_SSO_TARGET_CLIENT=forge_website_report', `VITE_SSO_TARGET_CLIENT=${snakeName}_website_report`],
  680. ['"forge_report":', `"${snakeName}_report":`],
  681. ['"forge_website_report":', `"${snakeName}_website_report":`],
  682. ['VITE_REPORT_UI_PATH_PREFIX=/forge-report', `VITE_REPORT_UI_PATH_PREFIX=/${options.projectName}-report`],
  683. ['VITE_SSO_BRIDGE_ROUTE=/report/design', `VITE_SSO_BRIDGE_ROUTE=/${options.projectName}-report/design`],
  684. ['http://81.70.22.48:8084/forge-report', `http://localhost:8084/${options.projectName}-report`],
  685. ['http://localhost:3021/forge-report', `http://localhost:8084/${options.projectName}-report`],
  686. ['localhost:3021', `localhost:8084`],
  687. )
  688. }
  689. for (const [from, to] of Object.entries(applicationClassMap)) {
  690. replacements.push([from, to])
  691. }
  692. for (const [from, to] of Object.entries(artifactMap).sort((a, b) => b[0].length - a[0].length)) {
  693. replacements.push([`forge-server/${from}`, `${serverDirName}/${to}`])
  694. replacements.push([`forge/${from}`, `${serverDirName}/${to}`])
  695. }
  696. for (const [from, to] of Object.entries(artifactMap).sort((a, b) => b[0].length - a[0].length)) {
  697. replacements.push([from, to])
  698. }
  699. for (const targetName of Object.values(artifactMap).sort((a, b) => b.length - a.length)) {
  700. replacements.push([`forge-server/${targetName}`, `${serverDirName}/${targetName}`])
  701. replacements.push([`forge/${targetName}`, `${serverDirName}/${targetName}`])
  702. }
  703. return replacements
  704. }
  705. async function rewriteTextFiles(rootDir, replacements, hasReportUi) {
  706. const files = await collectFiles(rootDir, filePath => !isBinaryFile(filePath))
  707. for (const file of files) {
  708. let content
  709. try {
  710. content = await fs.readFile(file, 'utf8')
  711. }
  712. catch {
  713. continue
  714. }
  715. let nextContent = applyTextReplacements(content, replacements)
  716. // 如果没有报表模块,删除 SSO 相关的配置行
  717. if (!hasReportUi) {
  718. nextContent = removeSsoConfigLines(nextContent)
  719. }
  720. if (nextContent !== content) {
  721. await fs.writeFile(file, nextContent)
  722. }
  723. }
  724. }
  725. function removeSsoConfigLines(content) {
  726. const lines = content.split('\n')
  727. const filteredLines = lines.filter(line => {
  728. const trimmed = line.trim()
  729. // 跳过 SSO 相关的配置行(包括注释掉的)
  730. if (trimmed.startsWith('#') && (
  731. trimmed.includes('VITE_SSO_') ||
  732. trimmed.includes('VITE_REPORT_UI_')
  733. )) {
  734. return false
  735. }
  736. if (trimmed.startsWith('VITE_SSO_') ||
  737. trimmed.startsWith('VITE_REPORT_UI_')) {
  738. return false
  739. }
  740. return true
  741. })
  742. return filteredLines.join('\n')
  743. }
  744. function applyTextReplacements(content, replacements) {
  745. let nextContent = content
  746. for (const [from, to] of replacements) {
  747. nextContent = nextContent.split(from).join(to)
  748. }
  749. return nextContent
  750. }
  751. function isBinaryFile(filePath) {
  752. return binaryExtensions.has(path.extname(filePath).toLowerCase())
  753. }
  754. async function moveJavaPackageDirectories(rootDir, oldPackage, newPackage) {
  755. const oldPackagePath = oldPackage.replaceAll('.', path.sep)
  756. const newPackagePath = newPackage.replaceAll('.', path.sep)
  757. const javaRoots = await collectDirectories(rootDir, dirPath => /src[/\\](main|test)[/\\]java$/.test(dirPath))
  758. for (const javaRoot of javaRoots) {
  759. const oldDir = path.join(javaRoot, oldPackagePath)
  760. if (!(await exists(oldDir))) {
  761. continue
  762. }
  763. const newDir = path.join(javaRoot, newPackagePath)
  764. await fs.mkdir(path.dirname(newDir), { recursive: true })
  765. if (await exists(newDir)) {
  766. await mergeDirectory(oldDir, newDir)
  767. await fs.rm(oldDir, { recursive: true, force: true })
  768. }
  769. else {
  770. await fs.rename(oldDir, newDir)
  771. }
  772. await removeEmptyParents(path.dirname(oldDir), javaRoot)
  773. }
  774. }
  775. async function mergeDirectory(source, target) {
  776. await fs.mkdir(target, { recursive: true })
  777. const entries = await fs.readdir(source, { withFileTypes: true })
  778. for (const entry of entries) {
  779. const sourcePath = path.join(source, entry.name)
  780. const targetPath = path.join(target, entry.name)
  781. if (entry.isDirectory()) {
  782. await mergeDirectory(sourcePath, targetPath)
  783. }
  784. else {
  785. await fs.rename(sourcePath, targetPath)
  786. }
  787. }
  788. }
  789. async function removeEmptyParents(startDir, stopDir) {
  790. let current = startDir
  791. while (current.startsWith(stopDir) && current !== stopDir) {
  792. try {
  793. await fs.rmdir(current)
  794. }
  795. catch {
  796. break
  797. }
  798. current = path.dirname(current)
  799. }
  800. }
  801. async function renameFilesByBasename(rootDir, basenameMap, extension = '') {
  802. const files = await collectFiles(rootDir, filePath => Object.hasOwn(basenameMap, path.basename(filePath, extension)))
  803. for (const file of files) {
  804. const basename = path.basename(file, extension)
  805. const nextBasename = basenameMap[basename]
  806. if (!nextBasename || nextBasename === basename) {
  807. continue
  808. }
  809. const target = path.join(path.dirname(file), `${nextBasename}${extension}`)
  810. if (await exists(target)) {
  811. continue
  812. }
  813. await fs.rename(file, target)
  814. }
  815. }
  816. async function renameArtifactDirectories(serverRoot, artifactMap) {
  817. const dirs = await collectDirectories(serverRoot, () => true)
  818. dirs.sort((a, b) => b.length - a.length)
  819. for (const dir of dirs) {
  820. const baseName = path.basename(dir)
  821. const nextBaseName = artifactMap[baseName]
  822. if (!nextBaseName || nextBaseName === baseName) {
  823. continue
  824. }
  825. const target = path.join(path.dirname(dir), nextBaseName)
  826. if (await exists(target)) {
  827. continue
  828. }
  829. await fs.rename(dir, target)
  830. }
  831. }
  832. async function copyOptionalRootFiles(outputRoot) {
  833. for (const fileName of ['LICENSE', '.gitignore']) {
  834. const source = path.join(repoRoot, fileName)
  835. if (await exists(source)) {
  836. await fs.copyFile(source, path.join(outputRoot, fileName))
  837. }
  838. }
  839. }
  840. async function copyProjectContextFiles(outputRoot) {
  841. for (const name of projectContextNames) {
  842. const source = path.join(repoRoot, name)
  843. if (await exists(source)) {
  844. await copyProjectDir(source, path.join(outputRoot, name))
  845. }
  846. }
  847. }
  848. async function writeGeneratedConfig(outputRoot, options, selection, catalog) {
  849. const config = {
  850. projectName: options.projectName,
  851. displayName: options.displayName,
  852. basePackage: options.basePackage,
  853. groupId: options.groupId,
  854. artifactPrefix: options.artifactPrefix,
  855. moduleArtifactPrefix: options.moduleArtifactPrefix,
  856. stripModulePrefix: options.stripModulePrefix,
  857. databaseName: options.databaseName,
  858. preset: options.preset,
  859. includedModules: options.includeModuleIds,
  860. modules: [...selection.selectedModuleIds].sort(),
  861. frontends: [...selection.frontendIds].sort(),
  862. }
  863. await fs.writeFile(
  864. path.join(outputRoot, 'forge.config.json'),
  865. `${JSON.stringify(config, null, 2)}\n`,
  866. )
  867. const presetDescription = catalog.presets[options.preset]?.description || options.preset
  868. const modulePrefixLine = options.moduleArtifactPrefix === options.artifactPrefix
  869. ? ''
  870. : `- 子模块 artifact 前缀: ${options.moduleArtifactPrefix}
  871. `
  872. const adminServerArtifactId = `${options.moduleArtifactPrefix}-admin-server`
  873. const readme = `# ${options.displayName}
  874. 该工程由 Forge 项目装配器生成。
  875. ## 生成参数
  876. - preset: ${options.preset} - ${presetDescription}
  877. - Java 包名: ${options.basePackage}
  878. - Maven groupId: ${options.groupId}
  879. - artifact 前缀: ${options.artifactPrefix}
  880. ${modulePrefixLine}
  881. - 数据库名: ${options.databaseName}
  882. ${options.includeModuleIds.length ? `- 额外模块: ${options.includeModuleIds.join(', ')}
  883. ` : ''}
  884. ## 常用命令
  885. \`\`\`bash
  886. cd ${options.artifactPrefix}-server
  887. mvn -pl ${adminServerArtifactId} -am compile -DskipTests
  888. \`\`\`
  889. \`\`\`bash
  890. cd ${options.projectName}-admin-ui
  891. pnpm install
  892. pnpm dev
  893. \`\`\`
  894. ${selection.frontendIds.has('report-ui')
  895. ? `\`\`\`bash
  896. cd ${options.projectName}-report-ui
  897. pnpm install
  898. pnpm dev
  899. \`\`\`
  900. `
  901. : ''}
  902. ## 说明
  903. 生成器按 preset 裁剪 Maven 子模块、starter、plugin、前端工程和模块 SQL 资源。管理端前端内部页面暂未做菜单级裁剪,后续应结合 seed 菜单数据和路由清单继续细化。
  904. `
  905. await fs.writeFile(path.join(outputRoot, 'README.md'), readme)
  906. }
  907. async function writeSelectedSqlBundle(serverRoot, catalog, selection, replacements, options) {
  908. const entries = await collectSelectedSqlEntries(catalog, selection)
  909. const dbRoot = path.join(serverRoot, 'db')
  910. const moduleSqlRoot = path.join(dbRoot, 'module')
  911. await fs.mkdir(moduleSqlRoot, { recursive: true })
  912. const manifest = []
  913. let index = 1
  914. for (const entry of entries) {
  915. const sourceFile = path.join(repoRoot, entry.path)
  916. if (!(await exists(sourceFile))) {
  917. continue
  918. }
  919. const fileName = `${String(index).padStart(3, '0')}__${sanitizeFileName(entry.moduleId)}__${path.basename(entry.path)}`
  920. const targetFile = path.join(moduleSqlRoot, fileName)
  921. const content = applyTextReplacements(await fs.readFile(sourceFile, 'utf8'), replacements)
  922. await fs.writeFile(targetFile, content)
  923. manifest.push({
  924. order: index,
  925. module: entry.moduleId,
  926. source: entry.path,
  927. file: `module/${fileName}`,
  928. })
  929. index += 1
  930. }
  931. await fs.writeFile(
  932. path.join(dbRoot, 'manifest.json'),
  933. `${JSON.stringify({ scripts: manifest }, null, 2)}\n`,
  934. )
  935. await fs.writeFile(path.join(dbRoot, 'README.md'), buildDatabaseReadme(options))
  936. }
  937. async function collectSelectedSqlEntries(catalog, selection) {
  938. const entries = []
  939. const seen = new Set()
  940. const add = (moduleId, relativePath) => {
  941. if (seen.has(relativePath)) {
  942. return
  943. }
  944. seen.add(relativePath)
  945. entries.push({ moduleId, path: relativePath })
  946. }
  947. const requiredSeedRoot = path.join(repoRoot, 'forge-server/db/seed/required')
  948. const requiredSeeds = await collectFiles(requiredSeedRoot, filePath => path.extname(filePath) === '.sql')
  949. for (const filePath of requiredSeeds.sort()) {
  950. add('required-seed', path.relative(repoRoot, filePath))
  951. }
  952. for (const moduleId of selection.selectedModuleIds) {
  953. const moduleInfo = catalog.modules[moduleId]
  954. if (!moduleInfo?.path || moduleInfo.type === 'frontend') {
  955. continue
  956. }
  957. if (moduleId === 'admin-server') {
  958. continue
  959. }
  960. const moduleRoot = path.join(repoRoot, moduleInfo.path)
  961. const sqlFiles = await collectFiles(moduleRoot, (filePath) => {
  962. const normalized = filePath.split(path.sep).join('/')
  963. const baseName = path.basename(filePath)
  964. return path.extname(filePath) === '.sql'
  965. && !normalized.includes('/target/')
  966. && !normalized.includes('/templates/')
  967. && !baseName.includes('example')
  968. && !baseName.startsWith('test_')
  969. })
  970. for (const filePath of sqlFiles.sort()) {
  971. add(moduleId, path.relative(repoRoot, filePath))
  972. }
  973. }
  974. return entries
  975. }
  976. function sanitizeFileName(value) {
  977. return String(value).replace(/[^a-zA-Z0-9_-]/g, '_')
  978. }
  979. function buildDatabaseReadme(options) {
  980. return `# 数据库脚本
  981. \`manifest.json\` 和 \`module/\` 按本次选择的模块收集 SQL 资源,数据库名已替换为 \`${options.databaseName}\`。
  982. 数据库初始化统一使用后端根工程脚本:
  983. \`\`\`bash
  984. bash scripts/db/init-db.sh \\
  985. --host 127.0.0.1 \\
  986. --port 3306 \\
  987. --database ${options.databaseName} \\
  988. --user root \\
  989. --password your_password
  990. \`\`\`
  991. 如需额外执行本目录按模块收集的 SQL,追加 \`--with-module\`。当前仓库历史大脚本 \`forge-admin-server/sql/初始化脚本.sql\` 还没有拆成 core/module 两层,默认初始化仍会先执行该大脚本,避免基础表缺失。
  992. `
  993. }
  994. async function assertWritableTarget(outputRoot, force) {
  995. if (!(await exists(outputRoot))) {
  996. return
  997. }
  998. const entries = await fs.readdir(outputRoot)
  999. if (entries.length === 0) {
  1000. return
  1001. }
  1002. if (!force) {
  1003. throw new Error(`目标目录非空:${outputRoot}。如需覆盖请加 --force`)
  1004. }
  1005. await fs.rm(outputRoot, { recursive: true, force: true })
  1006. }
  1007. async function collectFiles(rootDir, predicate) {
  1008. const result = []
  1009. if (!(await exists(rootDir))) {
  1010. return result
  1011. }
  1012. const entries = await fs.readdir(rootDir, { withFileTypes: true })
  1013. for (const entry of entries) {
  1014. const entryPath = path.join(rootDir, entry.name)
  1015. if (entry.isDirectory()) {
  1016. result.push(...await collectFiles(entryPath, predicate))
  1017. continue
  1018. }
  1019. if (predicate(entryPath)) {
  1020. result.push(entryPath)
  1021. }
  1022. }
  1023. return result
  1024. }
  1025. async function collectDirectories(rootDir, predicate) {
  1026. const result = []
  1027. if (!(await exists(rootDir))) {
  1028. return result
  1029. }
  1030. const entries = await fs.readdir(rootDir, { withFileTypes: true })
  1031. for (const entry of entries) {
  1032. if (!entry.isDirectory()) {
  1033. continue
  1034. }
  1035. const entryPath = path.join(rootDir, entry.name)
  1036. if (predicate(entryPath)) {
  1037. result.push(entryPath)
  1038. }
  1039. result.push(...await collectDirectories(entryPath, predicate))
  1040. }
  1041. return result
  1042. }
  1043. async function exists(filePath) {
  1044. try {
  1045. await fs.access(filePath, fsConstants.F_OK)
  1046. return true
  1047. }
  1048. catch {
  1049. return false
  1050. }
  1051. }
  1052. async function readJson(filePath) {
  1053. return JSON.parse(await fs.readFile(filePath, 'utf8'))
  1054. }
  1055. function toSnakeCase(value) {
  1056. return String(value)
  1057. .replace(/([a-z0-9])([A-Z])/g, '$1_$2')
  1058. .replace(/-/g, '_')
  1059. .replace(/[^a-zA-Z0-9_]/g, '_')
  1060. .replace(/_+/g, '_')
  1061. .replace(/^_+|_+$/g, '')
  1062. .toLowerCase()
  1063. }
  1064. function printHelp(catalog) {
  1065. console.log(`Forge 项目装配器
  1066. 用法:
  1067. pnpm forge:create -- <target-dir> --base-package com.company.project [options]
  1068. 参数:
  1069. --preset ${Object.keys(catalog.presets).join(' | ')},默认 ai-report
  1070. --project-name 项目英文名,默认取目标目录名
  1071. --display-name 系统中文名,默认等于项目英文名
  1072. --base-package 新 Java 包名,必填,例如 com.company.smartfactory
  1073. --group-id Maven groupId,默认等于 base-package
  1074. --artifact-prefix Maven artifactId 前缀,默认等于 project-name
  1075. --module-artifact-prefix 子模块 artifactId 前缀,默认等于 artifact-prefix
  1076. --strip-module-prefix 从子模块 artifactId 前缀中剥离指定前缀,例如 nmg-lt
  1077. --database-name 数据库名,默认由 project-name 转 snake_case
  1078. --include 额外模块 ID,逗号分隔,例如 business-core
  1079. --force 目标目录非空时覆盖
  1080. 示例:
  1081. pnpm forge:create -- ../smart-factory \\
  1082. --preset ai-report \\
  1083. --display-name 智慧工厂管理平台 \\
  1084. --base-package com.company.smartfactory
  1085. `)
  1086. }
  1087. function printSummary(outputRoot, options, selection, catalog, adminServerArtifactId) {
  1088. const modules = [...selection.selectedModuleIds].sort()
  1089. const frontends = [...selection.frontendIds].sort()
  1090. console.log('\n[forge:create] 生成完成')
  1091. console.log(`目标目录:${outputRoot}`)
  1092. console.log(`preset:${options.preset} - ${catalog.presets[options.preset].description}`)
  1093. if (options.moduleArtifactPrefix !== options.artifactPrefix) {
  1094. console.log(`子模块 artifact 前缀:${options.moduleArtifactPrefix}`)
  1095. }
  1096. console.log(`后端模块:${modules.join(', ')}`)
  1097. console.log(`前端工程:${frontends.length ? frontends.join(', ') : '无'}`)
  1098. console.log('\n建议验证:')
  1099. console.log(` cd ${path.join(outputRoot, `${options.artifactPrefix}-server`)}`)
  1100. console.log(` mvn -pl ${adminServerArtifactId} -am compile -DskipTests`)
  1101. if (selection.frontendIds.has('admin-ui')) {
  1102. console.log(` cd ${path.join(outputRoot, `${options.projectName}-admin-ui`)}`)
  1103. console.log(' pnpm install && pnpm build')
  1104. }
  1105. }