github-mobile-reader 0.1.1 → 0.1.2

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/README.ko.md CHANGED
@@ -40,9 +40,13 @@ data
40
40
 
41
41
  - **의존성 제로 코어** — 파서는 Node.js ≥ 18이 있는 어디서나 동작합니다
42
42
  - **이중 출력 포맷** — CJS (`require`)와 ESM (`import`) 모두 지원, TypeScript 타입 포함
43
+ - **CLI** — `npx github-mobile-reader --repo owner/repo --pr 42` 로 어떤 PR이든 즉시 변환
43
44
  - **GitHub Action** — 레포에 YAML 파일 하나만 추가하면 PR마다 Reader 문서가 자동 생성됩니다
45
+ - **파일별 분리 출력** — 변경된 JS/TS 파일마다 독립적인 섹션으로 출력
46
+ - **JSX/Tailwind 인식** — `.jsx`/`.tsx` 파일은 컴포넌트 트리(`🎨 JSX Structure`)와 Tailwind 클래스 diff(`💅 Style Changes`)를 별도 섹션으로 분리 출력
44
47
  - **양방향 diff 추적** — 추가된 코드와 삭제된 코드를 각각 별도 섹션으로 표시
45
48
  - **보수적 설계** — 패턴이 애매할 때는 잘못된 정보를 보여주는 대신 덜 보여줍니다
49
+ - **보안 기본값** — 토큰은 `$GITHUB_TOKEN` 환경변수로만 읽음 — 셸 히스토리나 `ps` 목록에 노출되는 `--token` 플래그 없음
46
50
 
47
51
  ---
48
52
 
@@ -50,13 +54,75 @@ data
50
54
 
51
55
  1. [빠른 시작](#빠른-시작)
52
56
  2. [언어 지원](#언어-지원)
53
- 3. [GitHub Action (권장)](#github-action-권장)
54
- 4. [npm 라이브러리 사용법](#npm-라이브러리-사용법)
55
- 5. [출력 형식](#출력-형식)
56
- 6. [API 레퍼런스](#api-레퍼런스)
57
- 7. [파서 동작 원리](#파서-동작-원리)
58
- 8. [기여하기](#기여하기)
59
- 9. [라이선스](#라이선스)
57
+ 3. [CLI 사용법](#cli-사용법)
58
+ 4. [GitHub Action (권장)](#github-action-권장)
59
+ 5. [npm 라이브러리 사용법](#npm-라이브러리-사용법)
60
+ 6. [출력 형식](#출력-형식)
61
+ 7. [API 레퍼런스](#api-레퍼런스)
62
+ 8. [파서 동작 원리](#파서-동작-원리)
63
+ 9. [기여하기](#기여하기)
64
+ 10. [라이선스](#라이선스)
65
+
66
+ ---
67
+
68
+ ## CLI 사용법
69
+
70
+ 터미널에서 `github-mobile-reader`를 바로 실행할 수 있습니다 — 별도 설정 파일 불필요. GitHub에서 PR diff를 받아 모바일 친화적인 Markdown으로 변환하고 `./reader-output/`에 PR별로 파일을 저장합니다.
71
+
72
+ ### 인증 (토큰 설정)
73
+
74
+ CLI 실행 **전에** 환경변수로 GitHub 토큰을 설정하세요:
75
+
76
+ ```bash
77
+ export GITHUB_TOKEN=ghp_xxxx
78
+ npx github-mobile-reader --repo owner/repo --pr 42
79
+ ```
80
+
81
+ > **보안 안내:** CLI는 `--token` 플래그를 지원하지 않습니다. 커맨드라인 인자로 시크릿을 전달하면 셸 히스토리와 `ps` 출력에 토큰이 노출됩니다. 반드시 환경변수를 사용하세요.
82
+
83
+ ### 단일 PR
84
+
85
+ ```bash
86
+ npx github-mobile-reader --repo owner/repo --pr 42
87
+ ```
88
+
89
+ ### 최근 PR 전체
90
+
91
+ ```bash
92
+ npx github-mobile-reader --repo owner/repo --all
93
+ ```
94
+
95
+ ### 옵션
96
+
97
+ | 플래그 | 기본값 | 설명 |
98
+ | ----------- | ------------------ | ------------------------------------------------------- |
99
+ | `--repo` | *(필수)* | `owner/repo` 형식의 레포지토리 |
100
+ | `--pr` | — | 특정 PR 번호 하나 처리 |
101
+ | `--all` | — | 최근 PR 전체 처리 (`--limit`와 함께 사용) |
102
+ | `--out` | `./reader-output` | 생성된 `.md` 파일 저장 경로 — 상대 경로만 허용, `..` 불가 |
103
+ | `--limit` | `10` | `--all` 사용 시 가져올 PR 최대 개수 |
104
+
105
+ 토큰: `$GITHUB_TOKEN` 환경변수에서 읽음 (미인증 시 60 req/hr, 인증 시 5 000 req/hr).
106
+
107
+ ### 출력 결과
108
+
109
+ PR마다 `reader-output/pr-<번호>.md` 파일 하나가 생성됩니다.
110
+
111
+ JSX/TSX 파일은 추가 섹션이 생성됩니다:
112
+
113
+ ```
114
+ # 📖 PR #42 — My Feature
115
+
116
+ ## 📄 `src/App.tsx`
117
+
118
+ ### 🧠 Logical Flow ← JS 로직 트리
119
+ ### 🎨 JSX Structure ← 컴포넌트 계층 구조 (JSX/TSX 전용)
120
+ ### 💅 Style Changes ← 추가/제거된 Tailwind 클래스 (JSX/TSX 전용)
121
+ ### ✅ Added Code
122
+ ### ❌ Removed Code
123
+ ```
124
+
125
+ > **참고:** `reader-output/`는 기본적으로 `.gitignore`에 포함되어 있습니다 — 생성된 파일은 로컬에만 저장되며 레포지토리에 커밋되지 않습니다.
60
126
 
61
127
  ---
62
128
 
@@ -487,8 +553,10 @@ github-mobile-reader/
487
553
  │ ├── parser.ts ← 핵심 diff → logical flow 파서
488
554
  │ ├── index.ts ← npm 공개 API
489
555
  │ ├── action.ts ← GitHub Action 진입점
556
+ │ ├── cli.ts ← CLI 진입점 (npx github-mobile-reader)
490
557
  │ └── test.ts ← 스모크 테스트 (33개)
491
558
  ├── dist/ ← 컴파일 결과물 (자동 생성, 수정 금지)
559
+ ├── reader-output/ ← CLI 출력 디렉토리 (gitignore됨)
492
560
  ├── .github/
493
561
  │ └── workflows/
494
562
  │ └── mobile-reader.yml ← 사용자용 예시 워크플로우
package/README.md CHANGED
@@ -38,9 +38,13 @@ data
38
38
 
39
39
  - **Zero-dependency core** — the parser runs anywhere Node.js ≥ 18 is available
40
40
  - **Dual output format** — CJS (`require`) and ESM (`import`) with full TypeScript types
41
+ - **CLI** — `npx github-mobile-reader --repo owner/repo --pr 42` fetches and converts any PR instantly
41
42
  - **GitHub Action** — drop one YAML block into any repo and get auto-generated Reader docs on every PR
43
+ - **File-by-file output** — each changed JS/TS file gets its own independent section in the output
44
+ - **JSX/Tailwind aware** — `.jsx`/`.tsx` files get a component tree (`🎨 JSX Structure`) and a Tailwind class diff (`💅 Style Changes`) instead of one unreadable blob
42
45
  - **Tracks both sides of a diff** — shows added _and_ removed code in separate sections
43
46
  - **Conservative by design** — when a pattern is ambiguous, the library shows less rather than showing something wrong
47
+ - **Secure by default** — token is read from `$GITHUB_TOKEN` only; no flag that leaks to shell history or `ps`
44
48
 
45
49
  ---
46
50
 
@@ -48,13 +52,14 @@ data
48
52
 
49
53
  1. [Quick Start](#quick-start)
50
54
  2. [Language Support](#language-support)
51
- 3. [GitHub Action (recommended)](#github-action-recommended)
52
- 4. [npm Library Usage](#npm-library-usage)
53
- 5. [Output Format](#output-format)
54
- 6. [API Reference](#api-reference)
55
- 7. [How the Parser Works](#how-the-parser-works)
56
- 8. [Contributing](#contributing)
57
- 9. [License](#license)
55
+ 3. [CLI Usage](#cli-usage)
56
+ 4. [GitHub Action (recommended)](#github-action-recommended)
57
+ 5. [npm Library Usage](#npm-library-usage)
58
+ 6. [Output Format](#output-format)
59
+ 7. [API Reference](#api-reference)
60
+ 8. [How the Parser Works](#how-the-parser-works)
61
+ 9. [Contributing](#contributing)
62
+ 10. [License](#license)
58
63
 
59
64
  ---
60
65
 
@@ -129,6 +134,67 @@ If you'd like to contribute an adapter for your language, see [Contributing](#co
129
134
 
130
135
  ---
131
136
 
137
+ ## CLI Usage
138
+
139
+ Run `github-mobile-reader` directly from your terminal — no setup, no config file. It fetches a PR diff from GitHub, converts it to mobile-friendly Markdown, and saves one file per PR to `./reader-output/`.
140
+
141
+ ### Authentication
142
+
143
+ Set your GitHub token as an environment variable **before** running the CLI:
144
+
145
+ ```bash
146
+ export GITHUB_TOKEN=ghp_xxxx
147
+ npx github-mobile-reader --repo owner/repo --pr 42
148
+ ```
149
+
150
+ > **Security note:** The CLI does not accept a `--token` flag. Passing secrets as command-line arguments exposes them in shell history and `ps` output. Always use the environment variable.
151
+
152
+ ### Single PR
153
+
154
+ ```bash
155
+ npx github-mobile-reader --repo owner/repo --pr 42
156
+ ```
157
+
158
+ ### All recent PRs
159
+
160
+ ```bash
161
+ npx github-mobile-reader --repo owner/repo --all
162
+ ```
163
+
164
+ ### Options
165
+
166
+ | Flag | Default | Description |
167
+ | --------- | ----------------- | ------------------------------------------------- |
168
+ | `--repo` | *(required)* | Repository in `owner/repo` format |
169
+ | `--pr` | — | Process a single PR by number |
170
+ | `--all` | — | Process all recent PRs (use with `--limit`) |
171
+ | `--out` | `./reader-output` | Output directory — relative paths only, no `..` |
172
+ | `--limit` | `10` | Max number of PRs to fetch when using `--all` |
173
+
174
+ Token: read from `$GITHUB_TOKEN` environment variable (60 req/hr unauthenticated, 5 000 req/hr authenticated).
175
+
176
+ ### Output
177
+
178
+ Each PR produces one file: `reader-output/pr-<number>.md`.
179
+
180
+ Inside that file, every changed JS/TS file gets its own section. JSX/TSX files get two extra sections:
181
+
182
+ ```
183
+ # 📖 PR #42 — My Feature
184
+
185
+ ## 📄 `src/App.tsx`
186
+
187
+ ### 🧠 Logical Flow ← JS logic tree
188
+ ### 🎨 JSX Structure ← component hierarchy (JSX/TSX only)
189
+ ### 💅 Style Changes ← added/removed Tailwind classes (JSX/TSX only)
190
+ ### ✅ Added Code
191
+ ### ❌ Removed Code
192
+ ```
193
+
194
+ > **Note:** `reader-output/` is gitignored by default — the generated files are local only and not committed to your repository.
195
+
196
+ ---
197
+
132
198
  ## Quick Start
133
199
 
134
200
  ```bash
@@ -482,8 +548,10 @@ github-mobile-reader/
482
548
  ├── src/
483
549
  │ ├── parser.ts ← core diff → logical flow parser
484
550
  │ ├── index.ts ← public npm API surface
485
- └── action.ts ← GitHub Action entry point
551
+ ├── action.ts ← GitHub Action entry point
552
+ │ └── cli.ts ← CLI entry point (npx github-mobile-reader)
486
553
  ├── dist/ ← compiled output (auto-generated, do not edit)
554
+ ├── reader-output/ ← CLI output directory (gitignored)
487
555
  ├── .github/
488
556
  │ └── workflows/
489
557
  │ └── mobile-reader.yml ← example workflow for consumers
package/dist/action.js CHANGED
@@ -28,6 +28,131 @@ var path = __toESM(require("path"));
28
28
  var import_child_process = require("child_process");
29
29
 
30
30
  // src/parser.ts
31
+ function isJSXFile(filename) {
32
+ return /\.(jsx|tsx)$/.test(filename);
33
+ }
34
+ function hasJSXContent(lines) {
35
+ return lines.some((l) => /<[A-Z][A-Za-z]*[\s/>]/.test(l) || /return\s*\(/.test(l));
36
+ }
37
+ function isClassNameOnlyLine(line) {
38
+ return /^className=/.test(line.trim());
39
+ }
40
+ function extractClassName(line) {
41
+ const staticMatch = line.match(/className="([^"]*)"/);
42
+ if (staticMatch) return staticMatch[1];
43
+ const ternaryMatch = line.match(/className=\{[^?]+\?\s*"([^"]*)"\s*:\s*"([^"]*)"\}/);
44
+ if (ternaryMatch) return `${ternaryMatch[1]} ${ternaryMatch[2]}`;
45
+ const templateMatch = line.match(/className=\{`([^`]*)`\}/);
46
+ if (templateMatch) {
47
+ const raw = templateMatch[1];
48
+ const literals = raw.replace(/\$\{[^}]*\}/g, " ").trim();
49
+ const exprStrings = [...raw.matchAll(/"([^"]*)"/g)].map((m) => m[1]);
50
+ return [literals, ...exprStrings].filter(Boolean).join(" ");
51
+ }
52
+ return null;
53
+ }
54
+ function extractComponentFromLine(line) {
55
+ const tagMatch = line.match(/<([A-Za-z][A-Za-z0-9.]*)/);
56
+ if (tagMatch) return tagMatch[1];
57
+ return "unknown";
58
+ }
59
+ function parseClassNameChanges(addedLines, removedLines) {
60
+ const componentMap = /* @__PURE__ */ new Map();
61
+ for (const line of addedLines.filter((l) => /className=/.test(l))) {
62
+ const cls = extractClassName(line);
63
+ const comp = extractComponentFromLine(line);
64
+ if (!cls) continue;
65
+ if (!componentMap.has(comp)) componentMap.set(comp, { added: /* @__PURE__ */ new Set(), removed: /* @__PURE__ */ new Set() });
66
+ cls.split(/\s+/).filter(Boolean).forEach((c) => componentMap.get(comp).added.add(c));
67
+ }
68
+ for (const line of removedLines.filter((l) => /className=/.test(l))) {
69
+ const cls = extractClassName(line);
70
+ const comp = extractComponentFromLine(line);
71
+ if (!cls) continue;
72
+ if (!componentMap.has(comp)) componentMap.set(comp, { added: /* @__PURE__ */ new Set(), removed: /* @__PURE__ */ new Set() });
73
+ cls.split(/\s+/).filter(Boolean).forEach((c) => componentMap.get(comp).removed.add(c));
74
+ }
75
+ const changes = [];
76
+ for (const [comp, { added, removed }] of componentMap) {
77
+ const pureAdded = [...added].filter((c) => !removed.has(c));
78
+ const pureRemoved = [...removed].filter((c) => !added.has(c));
79
+ if (pureAdded.length === 0 && pureRemoved.length === 0) continue;
80
+ changes.push({ component: comp, added: pureAdded, removed: pureRemoved });
81
+ }
82
+ return changes;
83
+ }
84
+ function renderStyleChanges(changes) {
85
+ const lines = [];
86
+ for (const change of changes) {
87
+ lines.push(`**${change.component}**`);
88
+ if (change.added.length > 0) lines.push(` + ${change.added.join(" ")}`);
89
+ if (change.removed.length > 0) lines.push(` - ${change.removed.join(" ")}`);
90
+ }
91
+ return lines;
92
+ }
93
+ function isJSXElement(line) {
94
+ const t = line.trim();
95
+ return /^<[A-Za-z]/.test(t) || /^<\/[A-Za-z]/.test(t);
96
+ }
97
+ function isJSXClosing(line) {
98
+ return /^<\/[A-Za-z]/.test(line.trim());
99
+ }
100
+ function isJSXSelfClosing(line) {
101
+ return /\/>[\s]*$/.test(line.trim());
102
+ }
103
+ function extractJSXComponentName(line) {
104
+ const trimmed = line.trim();
105
+ const closingMatch = trimmed.match(/^<\/([A-Za-z][A-Za-z0-9.]*)/);
106
+ if (closingMatch) return `/${closingMatch[1]}`;
107
+ const nameMatch = trimmed.match(/^<([A-Za-z][A-Za-z0-9.]*)/);
108
+ if (!nameMatch) return trimmed;
109
+ const name = nameMatch[1];
110
+ const eventProps = [];
111
+ for (const m of trimmed.matchAll(/\b(on[A-Z]\w+)=/g)) {
112
+ eventProps.push(m[1]);
113
+ }
114
+ return eventProps.length > 0 ? `${name}(${eventProps.join(", ")})` : name;
115
+ }
116
+ function shouldIgnoreJSX(line) {
117
+ const t = line.trim();
118
+ return isClassNameOnlyLine(t) || /^style=/.test(t) || /^aria-/.test(t) || /^data-/.test(t) || /^strokeLinecap=/.test(t) || /^strokeLinejoin=/.test(t) || /^strokeWidth=/.test(t) || /^viewBox=/.test(t) || /^fill=/.test(t) || /^stroke=/.test(t) || /^d="/.test(t) || t === "{" || t === "}" || t === "(" || t === ")" || t === "<>" || t === "</>" || /^\{\/\*/.test(t);
119
+ }
120
+ function parseJSXToFlowTree(lines) {
121
+ const roots = [];
122
+ const stack = [];
123
+ for (const line of lines) {
124
+ if (!isJSXElement(line)) continue;
125
+ if (shouldIgnoreJSX(line)) continue;
126
+ const depth = getIndentDepth(line);
127
+ if (isJSXClosing(line)) {
128
+ while (stack.length > 0 && stack[stack.length - 1].depth >= depth) {
129
+ stack.pop();
130
+ }
131
+ continue;
132
+ }
133
+ const name = extractJSXComponentName(line);
134
+ const selfClosing = isJSXSelfClosing(line);
135
+ const node = {
136
+ type: "call",
137
+ name,
138
+ children: [],
139
+ depth,
140
+ priority: 5 /* OTHER */
141
+ };
142
+ while (stack.length > 0 && stack[stack.length - 1].depth >= depth) {
143
+ stack.pop();
144
+ }
145
+ if (stack.length === 0) {
146
+ roots.push(node);
147
+ } else {
148
+ stack[stack.length - 1].node.children.push(node);
149
+ }
150
+ if (!selfClosing) {
151
+ stack.push({ node, depth });
152
+ }
153
+ }
154
+ return roots;
155
+ }
31
156
  function filterDiffLines(diffText) {
32
157
  const lines = diffText.split("\n");
33
158
  const added = lines.filter((l) => l.startsWith("+") && !l.startsWith("+++") && l.trim() !== "+").map((l) => l.substring(1));
@@ -201,19 +326,21 @@ function renderFlowTree(nodes, indent = 0) {
201
326
  }
202
327
  return lines;
203
328
  }
204
- function parseDiffToLogicalFlow(diffText) {
329
+ function generateReaderMarkdown(diffText, meta = {}) {
205
330
  const { added, removed } = filterDiffLines(diffText);
206
- const normalizedAdded = normalizeCode(added);
331
+ const isJSX = Boolean(
332
+ meta.file && isJSXFile(meta.file) || hasJSXContent(added)
333
+ );
334
+ const addedForFlow = isJSX ? added.filter((l) => !isClassNameOnlyLine(l)) : added;
335
+ const normalizedAdded = normalizeCode(addedForFlow);
207
336
  const flowTree = parseToFlowTree(normalizedAdded);
208
- return {
209
- root: flowTree,
210
- rawCode: added.join("\n"),
211
- removedCode: removed.join("\n")
212
- };
213
- }
214
- function generateReaderMarkdown(diffText, meta = {}) {
215
- const result = parseDiffToLogicalFlow(diffText);
337
+ const rawCode = addedForFlow.join("\n");
338
+ const removedForCode = isJSX ? removed.filter((l) => !isClassNameOnlyLine(l)) : removed;
339
+ const removedCode = removedForCode.join("\n");
340
+ const classNameChanges = isJSX ? parseClassNameChanges(added, removed) : [];
341
+ const jsxTree = isJSX ? parseJSXToFlowTree(added) : [];
216
342
  const sections = [];
343
+ const lang = isJSX ? "tsx" : "typescript";
217
344
  sections.push("# \u{1F4D6} GitHub Reader View\n");
218
345
  sections.push("> Generated by **github-mobile-reader**");
219
346
  if (meta.repo) sections.push(`> Repository: ${meta.repo}`);
@@ -221,22 +348,33 @@ function generateReaderMarkdown(diffText, meta = {}) {
221
348
  if (meta.commit) sections.push(`> Commit: \`${meta.commit}\``);
222
349
  if (meta.file) sections.push(`> File: \`${meta.file}\``);
223
350
  sections.push("\n");
224
- if (result.root.length > 0) {
351
+ if (flowTree.length > 0) {
225
352
  sections.push("## \u{1F9E0} Logical Flow\n");
226
353
  sections.push("```");
227
- sections.push(...renderFlowTree(result.root));
354
+ sections.push(...renderFlowTree(flowTree));
228
355
  sections.push("```\n");
229
356
  }
230
- if (result.rawCode.trim()) {
357
+ if (isJSX && jsxTree.length > 0) {
358
+ sections.push("## \u{1F3A8} JSX Structure\n");
359
+ sections.push("```");
360
+ sections.push(...renderFlowTree(jsxTree));
361
+ sections.push("```\n");
362
+ }
363
+ if (isJSX && classNameChanges.length > 0) {
364
+ sections.push("## \u{1F485} Style Changes\n");
365
+ sections.push(...renderStyleChanges(classNameChanges));
366
+ sections.push("");
367
+ }
368
+ if (rawCode.trim()) {
231
369
  sections.push("## \u2705 Added Code\n");
232
- sections.push("```typescript");
233
- sections.push(result.rawCode);
370
+ sections.push(`\`\`\`${lang}`);
371
+ sections.push(rawCode);
234
372
  sections.push("```\n");
235
373
  }
236
- if (result.removedCode.trim()) {
374
+ if (removedCode.trim()) {
237
375
  sections.push("## \u274C Removed Code\n");
238
- sections.push("```typescript");
239
- sections.push(result.removedCode);
376
+ sections.push(`\`\`\`${lang}`);
377
+ sections.push(removedCode);
240
378
  sections.push("```\n");
241
379
  }
242
380
  sections.push("---");