diff --git a/package-lock.json b/package-lock.json index b5448f0..9715380 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "github-mobile-reader", - "version": "0.1.6", + "version": "0.1.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "github-mobile-reader", - "version": "0.1.6", + "version": "0.1.7", "license": "MIT", "devDependencies": { "@types/node": "^20.0.0", diff --git a/package.json b/package.json index b4d169e..1855007 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "github-mobile-reader", - "version": "0.1.6", + "version": "0.1.7", "description": "Transform git diffs into mobile-friendly Markdown — no more horizontal scrolling when reviewing code on your phone.", "keywords": [ "github", diff --git a/src/parser.ts b/src/parser.ts index 4493e1f..33eff20 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -51,7 +51,7 @@ export type SymbolKind = export interface SymbolDiff { name: string; kind: SymbolKind; - status: "added" | "removed" | "modified"; + status: "added" | "removed" | "modified" | "moved"; addedLines: string[]; removedLines: string[]; } @@ -66,6 +66,97 @@ export interface PropsChange { removed: string[]; } +// ── Test file helpers ────────────────────────────────────────────────────────── + +/** + * Returns true if the filename looks like a test/spec file. + */ +export function isTestFile(filename: string): boolean { + return /\.(test|spec)\.(js|jsx|ts|tsx)$/.test(filename); +} + +/** + * Returns true if the filename looks like a config file (vitest, jest, etc.) + */ +export function isConfigFile(filename: string): boolean { + return /(?:vitest|jest|vite|tsconfig|eslint|prettier|babel|webpack|rollup)\.config\.(js|ts|cjs|mjs)$/.test(filename) + || /\.config\.(js|ts|cjs|mjs)$/.test(filename); +} + +interface TestCase { + suite: string; // describe block name + name: string; // it/test block name +} + +/** + * Extract describe/it/test block names from raw diff lines. + */ +export function extractTestCases(addedLines: string[]): TestCase[] { + const results: TestCase[] = []; + let currentSuite = ""; + + for (const line of addedLines) { + const t = line.trim(); + + // describe('suite name', ...) or describe("suite name", ...) + const suiteMatch = t.match(/^describe\s*\(\s*['"`](.+?)['"`]/); + if (suiteMatch) { + currentSuite = suiteMatch[1]; + continue; + } + + // it('test name', ...) or test('test name', ...) + const caseMatch = t.match(/^(?:it|test)\s*\(\s*['"`](.+?)['"`]/); + if (caseMatch) { + results.push({ suite: currentSuite, name: caseMatch[1] }); + } + } + + return results; +} + +/** + * Generate a readable markdown summary for a test file diff. + * Groups test cases by suite and lists them clearly. + */ +export function generateTestFileSummary( + addedLines: string[], + removedLines: string[], +): string[] { + const sections: string[] = []; + + const addedCases = extractTestCases(addedLines); + const removedCases = extractTestCases(removedLines); + + // Group added cases by suite + const suiteMap = new Map(); + for (const { suite, name } of addedCases) { + const key = suite || "(root)"; + if (!suiteMap.has(key)) suiteMap.set(key, []); + suiteMap.get(key)!.push(name); + } + + if (suiteMap.size > 0) { + for (const [suite, cases] of suiteMap) { + sections.push(`**테스트: \`${suite}\`**`); + cases.forEach((c) => sections.push(` + ${c}`)); + sections.push(""); + } + } + + // Removed test cases + if (removedCases.length > 0) { + sections.push("**제거된 테스트**"); + removedCases.forEach(({ suite, name }) => { + const label = suite ? `${suite} > ${name}` : name; + sections.push(` - ${label}`); + }); + sections.push(""); + } + + return sections; +} + // ── JSX / Tailwind helpers ───────────────────────────────────────────────────── export function isJSXFile(filename: string): boolean { @@ -1067,8 +1158,8 @@ export function generateSymbolSections( isJSX: boolean, ): string[] { const sections: string[] = []; - const STATUS_ICON = { added: "✅", removed: "❌", modified: "✏️" }; - const STATUS_LABEL = { added: "새로 추가", removed: "제거됨", modified: "변경됨" }; + const STATUS_ICON = { added: "✅", removed: "❌", modified: "✏️", moved: "📦" }; + const STATUS_LABEL = { added: "새로 추가", removed: "제거됨", modified: "변경됨", moved: "다른 파일로 이동됨" }; // Walk the list in order: attach each setup variable to the nearest // preceding significant symbol so it appears as an inline Context line. @@ -1122,11 +1213,11 @@ export function generateSymbolSections( props.added.forEach((p) => lines.push(`Props+ \`${abbreviateProp(p)}\``)); props.removed.forEach((p) => lines.push(`Props- \`${abbreviateProp(p)}\``)); - // Behavioral summary - if (sym.status !== "removed") { + // Behavioral summary (skip for moved — content lives in the destination file) + if (sym.status !== "removed" && sym.status !== "moved") { buildBehaviorSummary(sym.addedLines).forEach((l) => lines.push(`+ ${l}`)); } - if (sym.status !== "added" && sym.removedLines.length > 0) { + if (sym.status !== "added" && sym.status !== "moved" && sym.removedLines.length > 0) { buildBehaviorSummary(sym.removedLines, "removed").slice(0, 4) .forEach((l) => lines.push(`- ${l}`)); } @@ -1224,6 +1315,41 @@ export function generateReaderMarkdown( ): string { const { added, removed } = filterDiffLines(diffText); + // ── Test file shortcut ─────────────────────────────────── + if (meta.file && isTestFile(meta.file)) { + const sections: string[] = []; + sections.push(...generateTestFileSummary(added, removed)); + sections.push("---"); + sections.push( + "🛠 Auto-generated by [github-mobile-reader](https://github.com/3rdflr/github-mobile-reader). Do not edit manually.", + ); + return sections.join("\n"); + } + + // ── Config file shortcut ────────────────────────────────── + if (meta.file && isConfigFile(meta.file)) { + const sections: string[] = []; + const configImports = extractImportChanges(added, removed); + if (configImports.added.length > 0) { + sections.push("**플러그인/설정 추가**"); + configImports.added.forEach((i) => sections.push(`+ \`${i}\``)); + sections.push(""); + } + if (configImports.removed.length > 0) { + sections.push("**플러그인/설정 제거**"); + configImports.removed.forEach((i) => sections.push(`- \`${i}\``)); + sections.push(""); + } + if (sections.length === 0) { + sections.push("설정값 변경"); + } + sections.push("---"); + sections.push( + "🛠 Auto-generated by [github-mobile-reader](https://github.com/3rdflr/github-mobile-reader). Do not edit manually.", + ); + return sections.join("\n"); + } + // ── Detect JSX mode ────────────────────────────────────── const isJSX = Boolean( (meta.file && isJSXFile(meta.file)) || hasJSXContent(added), @@ -1233,10 +1359,18 @@ export function generateReaderMarkdown( const hunks = parseDiffHunks(diffText); const symbolDiffs = attributeLinesToSymbols(hunks); + // ── Detect moved symbols (removed here, imported elsewhere) ── + const fileImportChanges = extractImportChanges(added, removed); + const newlyImported = new Set(fileImportChanges.added); + for (const sym of symbolDiffs) { + if (sym.status === "removed" && newlyImported.has(sym.name)) { + sym.status = "moved"; + } + } + const sections: string[] = []; // ── File-level import changes ───────────────────────────── - const fileImportChanges = extractImportChanges(added, removed); if (fileImportChanges.added.length > 0 || fileImportChanges.removed.length > 0) { sections.push("**Import 변화**"); fileImportChanges.added.forEach((i) => sections.push(`+ \`${i}\``));