diff --git a/src/core/generate.js b/src/core/generate.js index 8dac798..7939aca 100644 --- a/src/core/generate.js +++ b/src/core/generate.js @@ -384,6 +384,7 @@ const buildPlannedModuleExports = (selectedModules) => { const buildModuleMap = (swagger) => { const moduleMap = new Map() + const tagDescriptionMap = buildTagDescriptionMap(swagger.tags) for (const [apiPath, pathItem] of Object.entries(swagger.paths || {})) { for (const method of HTTP_METHOD_ORDER) { @@ -396,11 +397,17 @@ const buildModuleMap = (swagger) => { const moduleName = extractModuleName(apiPath, operation) const moduleKey = normalizeLookupKey(moduleName) const aliases = new Set([moduleName, toKebabCase(moduleName), ...(operation.tags || [])]) + const moduleDescription = resolveModuleDescription({ + moduleName, + operation, + tagDescriptionMap, + }) if (!moduleMap.has(moduleKey)) { moduleMap.set(moduleKey, { aliases, fileName: toKebabCase(moduleName), + moduleDescription, moduleName, operations: [], }) @@ -412,6 +419,10 @@ const buildModuleMap = (swagger) => { moduleInfo.aliases.add(alias) } + if (!moduleInfo.moduleDescription && moduleDescription) { + moduleInfo.moduleDescription = moduleDescription + } + moduleInfo.operations.push({ method, operation, @@ -851,10 +862,17 @@ const generateModuleFile = ({ moduleInfo, paramStyle, schemas, requestImport, sw AUTO_GENERATED_BANNER, `// Swagger: ${swaggerUrl}`, `// Module: ${moduleInfo.moduleName}`, + ] + + if (moduleInfo.moduleDescription) { + lines.push(`// Module description: ${escapeComment(moduleInfo.moduleDescription)}`) + } + + lines.push( `// Param style: ${paramStyle}`, '', `import request from '${requestImport}'`, - ] + ) if (needsBuildUrl) { lines.push(`import { buildUrl } from '${DEFAULT_SHARED_IMPORT}'`) @@ -1528,6 +1546,37 @@ const extractModuleName = (apiPath, operation) => { return operation.tags?.[0] || segments[0] || 'default' } +const buildTagDescriptionMap = (tags = []) => { + const descriptionMap = new Map() + + for (const tag of tags) { + const tagName = String(tag?.name || '').trim() + const tagDescription = String(tag?.description || '').trim() + + if (!tagName || !tagDescription) { + continue + } + + descriptionMap.set(normalizeLookupKey(tagName), tagDescription) + } + + return descriptionMap +} + +const resolveModuleDescription = ({ moduleName, operation, tagDescriptionMap }) => { + const candidates = [...(operation.tags || []), moduleName] + + for (const candidate of candidates) { + const description = tagDescriptionMap.get(normalizeLookupKey(candidate)) + + if (description) { + return description + } + } + + return '' +} + const getEndpointName = (apiPath) => { const segments = apiPath.split('/').filter(Boolean) diff --git a/test/generate.test.js b/test/generate.test.js index ff0b56c..565f238 100644 --- a/test/generate.test.js +++ b/test/generate.test.js @@ -218,6 +218,98 @@ test('generated index includes namespace exports and flattened re-exports', asyn } }) +test('generated module file includes module description from swagger tags', async () => { + const tempDir = await createTempDir() + + try { + const swaggerPath = path.join(tempDir, 'swagger.json') + const outputDir = path.join(tempDir, 'generated') + + await writeJson(swaggerPath, { + openapi: '3.0.0', + tags: [ + { + name: 'Ranking', + description: '排行榜', + }, + ], + paths: { + '/api/v1/Ranking/GetList': { + get: { + responses: { + 200: { description: 'OK' }, + }, + }, + }, + }, + }) + + await generateApiFiles({ + projectRoot: tempDir, + swaggerUrl: swaggerPath, + swaggerTimeoutMs: 1000, + outputDir, + requestImport: '../request', + paramStyle: 'object', + modules: [], + cleanOutput: true, + }) + + const rankingContent = await readFile(path.join(outputDir, 'ranking.js')) + + assert.match(rankingContent, /\/\/ Module: Ranking/) + assert.match(rankingContent, /\/\/ Module description: 排行榜/) + } finally { + await fs.rm(tempDir, { recursive: true, force: true }) + } +}) + +test('generated module file omits module description when swagger tags have no description', async () => { + const tempDir = await createTempDir() + + try { + const swaggerPath = path.join(tempDir, 'swagger.json') + const outputDir = path.join(tempDir, 'generated') + + await writeJson(swaggerPath, { + openapi: '3.0.0', + tags: [ + { + name: 'Ranking', + }, + ], + paths: { + '/api/v1/Ranking/GetList': { + get: { + tags: ['Ranking'], + responses: { + 200: { description: 'OK' }, + }, + }, + }, + }, + }) + + await generateApiFiles({ + projectRoot: tempDir, + swaggerUrl: swaggerPath, + swaggerTimeoutMs: 1000, + outputDir, + requestImport: '../request', + paramStyle: 'object', + modules: [], + cleanOutput: true, + }) + + const rankingContent = await readFile(path.join(outputDir, 'ranking.js')) + + assert.match(rankingContent, /\/\/ Module: Ranking/) + assert.doesNotMatch(rankingContent, /\/\/ Module description:/) + } finally { + await fs.rm(tempDir, { recursive: true, force: true }) + } +}) + test('generation can skip generated index.js when generateIndexFile is false', async () => { const tempDir = await createTempDir()