아이템파인더

#!/usr/bin/env node const fs = require('fs'); const path = require('path'); const MAPPER_ITEM_TAGS = ['select', 'insert', 'update', 'delete']; const TARGET_COLUMN_KEYS = ['plainColumn', 'encColumn', 'hashColumn']; function printUsage() { console.log([ 'Usage:', ' node index.js [output-md-path]', '', 'Example:', ' node index.js "D:\\prj\\mybatis" "D:\\prj\\vsix2\\crypto-targets.json"', '', 'Output:', ' xml파일별카운트.md' ].join('\n')); } function assertReadableDirectory(dirPath, label) { if (!dirPath) { throw new Error(`${label} is required.`); } const stat = fs.statSync(dirPath); if (!stat.isDirectory()) { throw new Error(`${label} is not a directory: ${dirPath}`); } } function assertReadableFile(filePath, label) { if (!filePath) { throw new Error(`${label} is required.`); } const stat = fs.statSync(filePath); if (!stat.isFile()) { throw new Error(`${label} is not a file: ${filePath}`); } } function walkXmlFiles(rootDir) { const xmlFiles = []; const stack = [rootDir]; while (stack.length > 0) { const current = stack.pop(); const entries = fs.readdirSync(current, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(current, entry.name); if (entry.isDirectory()) { stack.push(fullPath); } else if (entry.isFile() && entry.name.toLowerCase().endsWith('.xml')) { xmlFiles.push(fullPath); } } } return xmlFiles.sort((a, b) => a.localeCompare(b)); } function stripXmlComments(xml) { return xml.replace(//g, ''); } function readTextFile(filePath) { return fs.readFileSync(filePath, 'utf8').replace(/^\uFEFF/, ''); } function getAttribute(tagText, name) { const attrPattern = new RegExp(`\\b${escapeRegExp(name)}\\s*=\\s*("([^"]*)"|'([^']*)')`, 'i'); const match = tagText.match(attrPattern); return match ? (match[2] || match[3] || '') : ''; } function getNamespace(xml) { const mapperMatch = xml.match(/]*>/i); return mapperMatch ? getAttribute(mapperMatch[0], 'namespace') : ''; } function extractTaggedBlocks(xml, tagName) { const blocks = []; const pattern = new RegExp(`<${tagName}\\b[^>]*>[\\s\\S]*?<\\/${tagName}>`, 'gi'); let match; while ((match = pattern.exec(xml)) !== null) { const block = match[0]; const openTagMatch = block.match(new RegExp(`^<${tagName}\\b[^>]*>`, 'i')); const closeTagPattern = new RegExp(`<\\/${tagName}>\\s*$`, 'i'); blocks.push({ raw: block, openTag: openTagMatch ? openTagMatch[0] : '', body: block .replace(new RegExp(`^<${tagName}\\b[^>]*>`, 'i'), '') .replace(closeTagPattern, '') }); } return blocks; } function loadMapperFile(filePath) { const xml = stripXmlComments(readTextFile(filePath)); const namespace = getNamespace(xml); const sqlFragments = []; const items = []; for (const block of extractTaggedBlocks(xml, 'sql')) { const id = getAttribute(block.openTag, 'id'); if (!id) { continue; } sqlFragments.push({ filePath, namespace, id, body: block.body }); } for (const tagName of MAPPER_ITEM_TAGS) { for (const block of extractTaggedBlocks(xml, tagName)) { const id = getAttribute(block.openTag, 'id') || `${tagName}@${items.length + 1}`; items.push({ filePath, namespace, tagName, id, body: block.body }); } } return { filePath, namespace, sqlFragments, items }; } function buildSqlFragmentIndex(mapperFiles) { const index = new Map(); for (const mapperFile of mapperFiles) { for (const fragment of mapperFile.sqlFragments) { const indexedFragment = { namespace: fragment.namespace, body: fragment.body }; if (fragment.namespace) { index.set(`${fragment.namespace}.${fragment.id}`, indexedFragment); } index.set(fragment.id, indexedFragment); } } return index; } function resolveIncludeRefId(refId, currentNamespace, fragmentIndex) { if (!refId) { return null; } const trimmed = refId.trim(); if (currentNamespace && !trimmed.includes('.')) { const namespaced = `${currentNamespace}.${trimmed}`; if (fragmentIndex.has(namespaced)) { return namespaced; } } if (fragmentIndex.has(trimmed)) { return trimmed; } return null; } function extractIncludeProperties(includeTag) { const properties = {}; const pattern = /]*\/?>/gi; let match; while ((match = pattern.exec(includeTag)) !== null) { const name = getAttribute(match[0], 'name'); const value = getAttribute(match[0], 'value'); if (name) { properties[name] = value; } } return properties; } function applyIncludeProperties(fragmentBody, properties) { return Object.keys(properties).reduce((body, name) => { const placeholder = new RegExp(`\\$\\{\\s*${escapeRegExp(name)}\\s*\\}`, 'g'); return body.replace(placeholder, properties[name]); }, fragmentBody); } function expandIncludes(text, currentNamespace, fragmentIndex, includeStack) { return text.replace(/]*\/>|]*>[\s\S]*?<\/include>/gi, (includeTag) => { const refId = getAttribute(includeTag, 'refid'); const resolvedKey = resolveIncludeRefId(refId, currentNamespace, fragmentIndex); if (!resolvedKey) { return includeTag; } if (includeStack.includes(resolvedKey)) { return ''; } const fragment = fragmentIndex.get(resolvedKey); const fragmentBody = applyIncludeProperties(fragment.body, extractIncludeProperties(includeTag)); return expandIncludes( fragmentBody, fragment.namespace || currentNamespace, fragmentIndex, includeStack.concat(resolvedKey) ); }); } function escapeRegExp(value) { return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } function makeBoundaryRegex(value) { return new RegExp(`(^|[^A-Za-z0-9_])${escapeRegExp(value)}([^A-Za-z0-9_]|$)`, 'i'); } function loadTargets(jsonPath) { const parsed = JSON.parse(readTextFile(jsonPath)); const rows = Array.isArray(parsed) ? parsed : (parsed.targets || parsed.items || []); if (!Array.isArray(rows)) { throw new Error('Target JSON must be an array or contain a targets/items array.'); } const targets = []; for (const row of rows) { if (!row || typeof row !== 'object') { continue; } const tableName = typeof row.tableName === 'string' ? row.tableName.trim() : ''; const columns = TARGET_COLUMN_KEYS .map((key) => typeof row[key] === 'string' ? row[key].trim() : '') .filter(Boolean); if (!tableName || columns.length === 0) { continue; } targets.push({ tableName, columns: Array.from(new Set(columns)), tableRegex: makeBoundaryRegex(tableName), columnRegexes: Array.from(new Set(columns)).map(makeBoundaryRegex) }); } if (targets.length === 0) { throw new Error('No valid targets found. Each target needs tableName and at least one column value.'); } return targets; } function itemMatchesAnyTarget(queryText, targets) { return targets.some((target) => { if (!target.tableRegex.test(queryText)) { return false; } return target.columnRegexes.some((columnRegex) => columnRegex.test(queryText)); }); } function countMatchesByXml(mapperFiles, fragmentIndex, targets) { const counts = new Map(); for (const mapperFile of mapperFiles) { const matchedItemKeys = new Set(); for (const item of mapperFile.items) { const expandedBody = expandIncludes(item.body, item.namespace, fragmentIndex, []); if (itemMatchesAnyTarget(expandedBody, targets)) { matchedItemKeys.add(`${item.tagName}:${item.id}`); } } if (matchedItemKeys.size > 0) { counts.set(mapperFile.filePath, matchedItemKeys.size); } } return counts; } function toMarkdown(counts, rootDir) { const lines = [ '# xml파일별카운트', '', '| xml파일명 | 카운트 |', '| --- | ---: |' ]; const rows = Array.from(counts.entries()) .sort(([fileA], [fileB]) => fileA.localeCompare(fileB)) .map(([filePath, count]) => { const relativePath = path.relative(rootDir, filePath) || path.basename(filePath); return `| ${relativePath.replace(/\\/g, '/')} | ${count} |`; }); return lines.concat(rows).concat('').join('\n'); } function main() { const args = process.argv.slice(2); if (args.includes('-h') || args.includes('--help')) { printUsage(); return; } const rootDir = args[0] ? path.resolve(args[0]) : ''; const jsonPath = args[1] ? path.resolve(args[1]) : ''; const outputPath = args[2] ? path.resolve(args[2]) : path.resolve(process.cwd(), 'xml파일별카운트.md'); assertReadableDirectory(rootDir, 'xml-root-dir'); assertReadableFile(jsonPath, 'targets-json-path'); const targets = loadTargets(jsonPath); const xmlFiles = walkXmlFiles(rootDir); const mapperFiles = xmlFiles.map(loadMapperFile); const fragmentIndex = buildSqlFragmentIndex(mapperFiles); const counts = countMatchesByXml(mapperFiles, fragmentIndex, targets); const markdown = toMarkdown(counts, rootDir); fs.writeFileSync(outputPath, markdown, 'utf8'); console.log(`Scanned XML files: ${xmlFiles.length}`); console.log(`Valid targets: ${targets.length}`); console.log(`Matched XML files: ${counts.size}`); console.log(`Output: ${outputPath}`); } try { main(); } catch (error) { console.error(`Error: ${error.message}`); printUsage(); process.exit(1); } 패키지json { "name": "itemfinder", "version": "1.0.0", "description": "Count MyBatis mapper items that reference target tables and columns.", "main": "index.js", "bin": { "itemfinder": "./index.js" }, "scripts": { "start": "node index.js" }, "engines": { "node": ">=16 <17" }, "license": "UNLICENSED" }

댓글

이 블로그의 인기 게시물

food eff privacy

판다 스픽 , 개인정보 처리방침 , Panda Speak Privacy Term

판다 수학 개인정보 처리방침 , Privacy , Panda Math