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 +75 -7
- package/README.md +76 -8
- package/dist/action.js +156 -18
- package/dist/cli.js +552 -0
- package/dist/index.d.mts +15 -1
- package/dist/index.d.ts +15 -1
- package/dist/index.js +177 -11
- package/dist/index.mjs +167 -10
- package/package.json +7 -3
package/dist/cli.js
ADDED
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
+
mod
|
|
24
|
+
));
|
|
25
|
+
|
|
26
|
+
// src/cli.ts
|
|
27
|
+
var fs = __toESM(require("fs"));
|
|
28
|
+
var path = __toESM(require("path"));
|
|
29
|
+
|
|
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
|
+
}
|
|
156
|
+
function filterDiffLines(diffText) {
|
|
157
|
+
const lines = diffText.split("\n");
|
|
158
|
+
const added = lines.filter((l) => l.startsWith("+") && !l.startsWith("+++") && l.trim() !== "+").map((l) => l.substring(1));
|
|
159
|
+
const removed = lines.filter((l) => l.startsWith("-") && !l.startsWith("---") && l.trim() !== "-").map((l) => l.substring(1));
|
|
160
|
+
return { added, removed };
|
|
161
|
+
}
|
|
162
|
+
function normalizeCode(lines) {
|
|
163
|
+
return lines.map((line) => {
|
|
164
|
+
let normalized = line;
|
|
165
|
+
normalized = normalized.replace(/\/\/.*$/, "");
|
|
166
|
+
normalized = normalized.replace(/\/\*.*?\*\//, "");
|
|
167
|
+
normalized = normalized.trim();
|
|
168
|
+
normalized = normalized.replace(/;$/, "");
|
|
169
|
+
return normalized;
|
|
170
|
+
}).filter((line) => line.length > 0);
|
|
171
|
+
}
|
|
172
|
+
function getIndentDepth(line) {
|
|
173
|
+
const match = line.match(/^(\s*)/);
|
|
174
|
+
if (!match) return 0;
|
|
175
|
+
return Math.floor(match[1].length / 2);
|
|
176
|
+
}
|
|
177
|
+
function isChaining(line, prevLine) {
|
|
178
|
+
if (!prevLine) return false;
|
|
179
|
+
if (!line.trim().startsWith(".")) return false;
|
|
180
|
+
if (!prevLine.match(/[)\}]$/)) return false;
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
function extractChainMethod(line) {
|
|
184
|
+
const match = line.match(/\.(\w+)\(/);
|
|
185
|
+
if (match) return `${match[1]}()`;
|
|
186
|
+
return line.trim();
|
|
187
|
+
}
|
|
188
|
+
function simplifyCallback(methodCall) {
|
|
189
|
+
const arrowMatch = methodCall.match(/(\w+)\((\w+)\s*=>\s*(\w+)\.(\w+)\)/);
|
|
190
|
+
if (arrowMatch) {
|
|
191
|
+
const [, method, param, , prop] = arrowMatch;
|
|
192
|
+
return `${method}(${param} \u2192 ${prop})`;
|
|
193
|
+
}
|
|
194
|
+
const callbackMatch = methodCall.match(/(\w+)\([^)]+\)/);
|
|
195
|
+
if (callbackMatch) return `${callbackMatch[1]}(callback)`;
|
|
196
|
+
return methodCall;
|
|
197
|
+
}
|
|
198
|
+
function isConditional(line) {
|
|
199
|
+
return /^(if|else|switch)\s*[\(\{]/.test(line.trim());
|
|
200
|
+
}
|
|
201
|
+
function isLoop(line) {
|
|
202
|
+
return /^(for|while)\s*\(/.test(line.trim());
|
|
203
|
+
}
|
|
204
|
+
function isFunctionDeclaration(line) {
|
|
205
|
+
const t = line.trim();
|
|
206
|
+
return (
|
|
207
|
+
// function foo() / async function foo()
|
|
208
|
+
/^(async\s+)?function\s+\w+/.test(t) || // const foo = () => / const foo = async () => / const foo = async (x: T) =>
|
|
209
|
+
/^(const|let|var)\s+\w+\s*=\s*(async\s*)?\(/.test(t) || // const foo = function / const foo = async function
|
|
210
|
+
/^(const|let|var)\s+\w+\s*=\s*(async\s+)?function/.test(t)
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
function shouldIgnore(line) {
|
|
214
|
+
const ignorePatterns = [
|
|
215
|
+
/^import\s+/,
|
|
216
|
+
/^export\s+/,
|
|
217
|
+
/^type\s+/,
|
|
218
|
+
/^interface\s+/,
|
|
219
|
+
/^console\./,
|
|
220
|
+
/^return$/,
|
|
221
|
+
/^throw\s+/
|
|
222
|
+
];
|
|
223
|
+
return ignorePatterns.some((p) => p.test(line.trim()));
|
|
224
|
+
}
|
|
225
|
+
function extractRoot(line) {
|
|
226
|
+
const assignMatch = line.match(/(?:const|let|var)\s+(\w+)\s*=\s*(\w+)/);
|
|
227
|
+
if (assignMatch) return assignMatch[2];
|
|
228
|
+
const callMatch = line.match(/^(\w+)\(/);
|
|
229
|
+
if (callMatch) return `${callMatch[1]}()`;
|
|
230
|
+
const methodMatch = line.match(/^(\w+)\./);
|
|
231
|
+
if (methodMatch) return methodMatch[1];
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
function parseToFlowTree(lines) {
|
|
235
|
+
const roots = [];
|
|
236
|
+
let currentChain = null;
|
|
237
|
+
let prevLine = null;
|
|
238
|
+
let baseDepth = -1;
|
|
239
|
+
for (let i = 0; i < lines.length; i++) {
|
|
240
|
+
const line = lines[i];
|
|
241
|
+
if (shouldIgnore(line)) {
|
|
242
|
+
prevLine = line;
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
const depth = getIndentDepth(lines[i]);
|
|
246
|
+
if (baseDepth === -1) baseDepth = depth;
|
|
247
|
+
const relativeDepth = depth - baseDepth;
|
|
248
|
+
if (isChaining(line, prevLine)) {
|
|
249
|
+
const method = extractChainMethod(line);
|
|
250
|
+
const simplified = simplifyCallback(method);
|
|
251
|
+
if (currentChain) {
|
|
252
|
+
const chainNode = {
|
|
253
|
+
type: "chain",
|
|
254
|
+
name: simplified,
|
|
255
|
+
children: [],
|
|
256
|
+
depth: relativeDepth,
|
|
257
|
+
priority: 1 /* CHAINING */
|
|
258
|
+
};
|
|
259
|
+
let parent = currentChain;
|
|
260
|
+
while (parent.children.length > 0 && parent.children[parent.children.length - 1].depth >= relativeDepth) {
|
|
261
|
+
const last = parent.children[parent.children.length - 1];
|
|
262
|
+
if (last.children.length > 0) parent = last;
|
|
263
|
+
else break;
|
|
264
|
+
}
|
|
265
|
+
parent.children.push(chainNode);
|
|
266
|
+
}
|
|
267
|
+
prevLine = line;
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
if (isFunctionDeclaration(line)) {
|
|
271
|
+
const funcMatch = line.match(/(?:function|const|let|var)\s+(\w+)/);
|
|
272
|
+
roots.push({
|
|
273
|
+
type: "function",
|
|
274
|
+
name: funcMatch ? `${funcMatch[1]}()` : "function()",
|
|
275
|
+
children: [],
|
|
276
|
+
depth: relativeDepth,
|
|
277
|
+
priority: 4 /* FUNCTION */
|
|
278
|
+
});
|
|
279
|
+
currentChain = null;
|
|
280
|
+
prevLine = line;
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
const root = extractRoot(line);
|
|
284
|
+
if (root) {
|
|
285
|
+
currentChain = {
|
|
286
|
+
type: "root",
|
|
287
|
+
name: root,
|
|
288
|
+
children: [],
|
|
289
|
+
depth: relativeDepth,
|
|
290
|
+
priority: 1 /* CHAINING */
|
|
291
|
+
};
|
|
292
|
+
roots.push(currentChain);
|
|
293
|
+
} else if (isConditional(line)) {
|
|
294
|
+
const condMatch = line.match(/(if|else|switch)\s*\(([^)]+)\)/);
|
|
295
|
+
const condName = condMatch ? `${condMatch[1]} (${condMatch[2]})` : line.trim();
|
|
296
|
+
roots.push({
|
|
297
|
+
type: "condition",
|
|
298
|
+
name: condName,
|
|
299
|
+
children: [],
|
|
300
|
+
depth: relativeDepth,
|
|
301
|
+
priority: 2 /* CONDITIONAL */
|
|
302
|
+
});
|
|
303
|
+
currentChain = null;
|
|
304
|
+
} else if (isLoop(line)) {
|
|
305
|
+
roots.push({
|
|
306
|
+
type: "loop",
|
|
307
|
+
name: "loop",
|
|
308
|
+
children: [],
|
|
309
|
+
depth: relativeDepth,
|
|
310
|
+
priority: 3 /* LOOP */
|
|
311
|
+
});
|
|
312
|
+
currentChain = null;
|
|
313
|
+
}
|
|
314
|
+
prevLine = line;
|
|
315
|
+
}
|
|
316
|
+
return roots;
|
|
317
|
+
}
|
|
318
|
+
function renderFlowTree(nodes, indent = 0) {
|
|
319
|
+
const lines = [];
|
|
320
|
+
const prefix = indent === 0 ? "" : " ".repeat((indent - 1) * 4) + " \u2514\u2500 ";
|
|
321
|
+
for (const node of nodes) {
|
|
322
|
+
lines.push(prefix + node.name);
|
|
323
|
+
if (node.children.length > 0) {
|
|
324
|
+
lines.push(...renderFlowTree(node.children, indent + 1));
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return lines;
|
|
328
|
+
}
|
|
329
|
+
function generateReaderMarkdown(diffText, meta = {}) {
|
|
330
|
+
const { added, removed } = filterDiffLines(diffText);
|
|
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);
|
|
336
|
+
const flowTree = parseToFlowTree(normalizedAdded);
|
|
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) : [];
|
|
342
|
+
const sections = [];
|
|
343
|
+
const lang = isJSX ? "tsx" : "typescript";
|
|
344
|
+
sections.push("# \u{1F4D6} GitHub Reader View\n");
|
|
345
|
+
sections.push("> Generated by **github-mobile-reader**");
|
|
346
|
+
if (meta.repo) sections.push(`> Repository: ${meta.repo}`);
|
|
347
|
+
if (meta.pr) sections.push(`> Pull Request: #${meta.pr}`);
|
|
348
|
+
if (meta.commit) sections.push(`> Commit: \`${meta.commit}\``);
|
|
349
|
+
if (meta.file) sections.push(`> File: \`${meta.file}\``);
|
|
350
|
+
sections.push("\n");
|
|
351
|
+
if (flowTree.length > 0) {
|
|
352
|
+
sections.push("## \u{1F9E0} Logical Flow\n");
|
|
353
|
+
sections.push("```");
|
|
354
|
+
sections.push(...renderFlowTree(flowTree));
|
|
355
|
+
sections.push("```\n");
|
|
356
|
+
}
|
|
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()) {
|
|
369
|
+
sections.push("## \u2705 Added Code\n");
|
|
370
|
+
sections.push(`\`\`\`${lang}`);
|
|
371
|
+
sections.push(rawCode);
|
|
372
|
+
sections.push("```\n");
|
|
373
|
+
}
|
|
374
|
+
if (removedCode.trim()) {
|
|
375
|
+
sections.push("## \u274C Removed Code\n");
|
|
376
|
+
sections.push(`\`\`\`${lang}`);
|
|
377
|
+
sections.push(removedCode);
|
|
378
|
+
sections.push("```\n");
|
|
379
|
+
}
|
|
380
|
+
sections.push("---");
|
|
381
|
+
sections.push("\u{1F6E0} Auto-generated by [github-mobile-reader](https://github.com/your-org/github-mobile-reader). Do not edit manually.");
|
|
382
|
+
return sections.join("\n");
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// src/cli.ts
|
|
386
|
+
function parseArgs() {
|
|
387
|
+
const args = process.argv.slice(2);
|
|
388
|
+
const get = (flag) => {
|
|
389
|
+
const idx = args.indexOf(flag);
|
|
390
|
+
return idx !== -1 ? args[idx + 1] : void 0;
|
|
391
|
+
};
|
|
392
|
+
const repo = get("--repo");
|
|
393
|
+
if (!repo) {
|
|
394
|
+
console.error("Error: --repo <owner/repo> is required");
|
|
395
|
+
console.error("");
|
|
396
|
+
console.error("Examples:");
|
|
397
|
+
console.error(" npx github-mobile-reader --repo 3rdflr/-FE- --pr 5");
|
|
398
|
+
console.error(" npx github-mobile-reader --repo 3rdflr/-FE- --all");
|
|
399
|
+
process.exit(1);
|
|
400
|
+
}
|
|
401
|
+
if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.\-]+$/.test(repo)) {
|
|
402
|
+
console.error('Error: --repo must be in "owner/repo" format (e.g. "3rdflr/my-app")');
|
|
403
|
+
process.exit(1);
|
|
404
|
+
}
|
|
405
|
+
const rawOut = get("--out") ?? "./reader-output";
|
|
406
|
+
if (path.isAbsolute(rawOut) || rawOut.includes("..")) {
|
|
407
|
+
console.error('Error: --out must be a relative path without ".." (e.g. "./reader-output")');
|
|
408
|
+
process.exit(1);
|
|
409
|
+
}
|
|
410
|
+
if (args.includes("--token")) {
|
|
411
|
+
console.error("Error: --token flag is not supported for security reasons.");
|
|
412
|
+
console.error(" Set the GITHUB_TOKEN environment variable instead:");
|
|
413
|
+
console.error(" export GITHUB_TOKEN=ghp_xxxx");
|
|
414
|
+
process.exit(1);
|
|
415
|
+
}
|
|
416
|
+
return {
|
|
417
|
+
repo,
|
|
418
|
+
pr: get("--pr") ? Number(get("--pr")) : void 0,
|
|
419
|
+
all: args.includes("--all"),
|
|
420
|
+
token: process.env.GITHUB_TOKEN,
|
|
421
|
+
out: rawOut,
|
|
422
|
+
limit: Number(get("--limit") ?? "10")
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
async function githubFetch(url, token, accept = "application/vnd.github+json") {
|
|
426
|
+
const headers = { Accept: accept };
|
|
427
|
+
if (token) headers["Authorization"] = `token ${token}`;
|
|
428
|
+
const resp = await fetch(url, { headers });
|
|
429
|
+
if (!resp.ok) {
|
|
430
|
+
if (resp.status === 404) throw new Error(`Not found: ${url}`);
|
|
431
|
+
if (resp.status === 401) throw new Error("Authentication failed. Set the GITHUB_TOKEN environment variable.");
|
|
432
|
+
if (resp.status === 403) throw new Error("Rate limit or permission error. Set GITHUB_TOKEN for higher rate limits.");
|
|
433
|
+
throw new Error(`GitHub API error (status ${resp.status})`);
|
|
434
|
+
}
|
|
435
|
+
return resp;
|
|
436
|
+
}
|
|
437
|
+
async function getPRList(repo, token, limit = 10) {
|
|
438
|
+
const url = `https://api.github.com/repos/${repo}/pulls?state=all&per_page=${limit}&sort=updated&direction=desc`;
|
|
439
|
+
const resp = await githubFetch(url, token);
|
|
440
|
+
const data = await resp.json();
|
|
441
|
+
return data.map((pr) => ({ number: pr.number, title: pr.title }));
|
|
442
|
+
}
|
|
443
|
+
async function getPRMeta(repo, prNumber, token) {
|
|
444
|
+
const url = `https://api.github.com/repos/${repo}/pulls/${prNumber}`;
|
|
445
|
+
const resp = await githubFetch(url, token);
|
|
446
|
+
const data = await resp.json();
|
|
447
|
+
return { title: data.title, head: data.head.sha.slice(0, 7) };
|
|
448
|
+
}
|
|
449
|
+
var JS_TS_EXT = /\.(js|jsx|ts|tsx|mjs|cjs)$/;
|
|
450
|
+
async function getPRFileDiffs(repo, prNumber, token) {
|
|
451
|
+
const url = `https://api.github.com/repos/${repo}/pulls/${prNumber}`;
|
|
452
|
+
const resp = await githubFetch(url, token, "application/vnd.github.v3.diff");
|
|
453
|
+
const rawDiff = await resp.text();
|
|
454
|
+
const chunks = rawDiff.split(/(?=^diff --git )/m).filter(Boolean);
|
|
455
|
+
return chunks.map((chunk) => {
|
|
456
|
+
const match = chunk.match(/^diff --git a\/(.+?) b\//m);
|
|
457
|
+
return match ? { filename: match[1], diff: chunk } : null;
|
|
458
|
+
}).filter((item) => item !== null && JS_TS_EXT.test(item.filename));
|
|
459
|
+
}
|
|
460
|
+
async function processPR(repo, prNumber, outDir, token) {
|
|
461
|
+
process.stdout.write(` Fetching PR #${prNumber}...`);
|
|
462
|
+
const [fileDiffs, meta] = await Promise.all([
|
|
463
|
+
getPRFileDiffs(repo, prNumber, token),
|
|
464
|
+
getPRMeta(repo, prNumber, token)
|
|
465
|
+
]);
|
|
466
|
+
if (fileDiffs.length === 0) {
|
|
467
|
+
console.log(` \u2014 JS/TS \uBCC0\uACBD \uC5C6\uC74C (\uC2A4\uD0B5)`);
|
|
468
|
+
return "";
|
|
469
|
+
}
|
|
470
|
+
const sections = [];
|
|
471
|
+
sections.push(`# \u{1F4D6} PR #${prNumber} \u2014 ${meta.title}
|
|
472
|
+
`);
|
|
473
|
+
sections.push(`> Repository: ${repo} `);
|
|
474
|
+
sections.push(`> Commit: \`${meta.head}\` `);
|
|
475
|
+
sections.push(`> \uBCC0\uACBD\uB41C JS/TS \uD30C\uC77C: ${fileDiffs.length}\uAC1C
|
|
476
|
+
`);
|
|
477
|
+
sections.push("---\n");
|
|
478
|
+
for (const { filename, diff } of fileDiffs) {
|
|
479
|
+
const section = generateReaderMarkdown(diff, {
|
|
480
|
+
pr: String(prNumber),
|
|
481
|
+
commit: meta.head,
|
|
482
|
+
file: filename,
|
|
483
|
+
repo
|
|
484
|
+
});
|
|
485
|
+
const withoutHeader = section.replace(/^# 📖.*\n/, "").replace(/^> Generated by.*\n/m, "").replace(/^> Repository:.*\n/m, "").replace(/^> Pull Request:.*\n/m, "").replace(/^> Commit:.*\n/m, "").replace(/^> File:.*\n/m, "").replace(/^\n+/, "");
|
|
486
|
+
sections.push(`## \u{1F4C4} \`${filename}\`
|
|
487
|
+
`);
|
|
488
|
+
sections.push(withoutHeader);
|
|
489
|
+
sections.push("\n---\n");
|
|
490
|
+
}
|
|
491
|
+
sections.push("\u{1F6E0} Auto-generated by [github-mobile-reader](https://github.com/3rdflr/github-mobile-reader). Do not edit manually.");
|
|
492
|
+
const markdown = sections.join("\n");
|
|
493
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
494
|
+
const outPath = path.join(outDir, `pr-${prNumber}.md`);
|
|
495
|
+
fs.writeFileSync(outPath, markdown, "utf8");
|
|
496
|
+
console.log(` \u2713 "${meta.title}" (${fileDiffs.length}\uAC1C \uD30C\uC77C)`);
|
|
497
|
+
return outPath;
|
|
498
|
+
}
|
|
499
|
+
async function main() {
|
|
500
|
+
const opts = parseArgs();
|
|
501
|
+
console.log(`
|
|
502
|
+
\u{1F4D6} github-mobile-reader CLI`);
|
|
503
|
+
console.log(` repo : ${opts.repo}`);
|
|
504
|
+
console.log(` out : ${opts.out}`);
|
|
505
|
+
if (!opts.token) {
|
|
506
|
+
console.log(` auth : none (60 req/hr limit \u2014 use --token or GITHUB_TOKEN for more)
|
|
507
|
+
`);
|
|
508
|
+
} else {
|
|
509
|
+
console.log(` auth : token provided
|
|
510
|
+
`);
|
|
511
|
+
}
|
|
512
|
+
if (opts.pr) {
|
|
513
|
+
const outPath = await processPR(opts.repo, opts.pr, opts.out, opts.token);
|
|
514
|
+
if (outPath) console.log(`
|
|
515
|
+
\u2705 Done \u2192 ${outPath}
|
|
516
|
+
`);
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
if (opts.all) {
|
|
520
|
+
console.log(` Fetching PR list (limit: ${opts.limit})...`);
|
|
521
|
+
const prs = await getPRList(opts.repo, opts.token, opts.limit);
|
|
522
|
+
if (prs.length === 0) {
|
|
523
|
+
console.log(" No PRs found.");
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
console.log(` Found ${prs.length} PR(s)
|
|
527
|
+
`);
|
|
528
|
+
const results = [];
|
|
529
|
+
for (const pr of prs) {
|
|
530
|
+
try {
|
|
531
|
+
const outPath = await processPR(opts.repo, pr.number, opts.out, opts.token);
|
|
532
|
+
if (outPath) results.push(outPath);
|
|
533
|
+
} catch (err) {
|
|
534
|
+
console.log(` \u2717 PR #${pr.number} skipped: ${err.message}`);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
console.log(`
|
|
538
|
+
\u2705 Done \u2014 ${results.length} file(s) written to ${opts.out}/
|
|
539
|
+
`);
|
|
540
|
+
results.forEach((p) => console.log(` ${p}`));
|
|
541
|
+
console.log("");
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
console.error("Error: specify --pr <number> or --all");
|
|
545
|
+
process.exit(1);
|
|
546
|
+
}
|
|
547
|
+
main().catch((err) => {
|
|
548
|
+
console.error(`
|
|
549
|
+
\u274C ${err.message}
|
|
550
|
+
`);
|
|
551
|
+
process.exit(1);
|
|
552
|
+
});
|
package/dist/index.d.mts
CHANGED
|
@@ -31,6 +31,20 @@ interface ReaderMarkdownMeta {
|
|
|
31
31
|
file?: string;
|
|
32
32
|
repo?: string;
|
|
33
33
|
}
|
|
34
|
+
interface ClassNameChange {
|
|
35
|
+
component: string;
|
|
36
|
+
added: string[];
|
|
37
|
+
removed: string[];
|
|
38
|
+
}
|
|
39
|
+
declare function isJSXFile(filename: string): boolean;
|
|
40
|
+
declare function hasJSXContent(lines: string[]): boolean;
|
|
41
|
+
declare function isClassNameOnlyLine(line: string): boolean;
|
|
42
|
+
declare function extractClassName(line: string): string | null;
|
|
43
|
+
declare function parseClassNameChanges(addedLines: string[], removedLines: string[]): ClassNameChange[];
|
|
44
|
+
declare function renderStyleChanges(changes: ClassNameChange[]): string[];
|
|
45
|
+
declare function isJSXElement(line: string): boolean;
|
|
46
|
+
declare function extractJSXComponentName(line: string): string;
|
|
47
|
+
declare function parseJSXToFlowTree(lines: string[]): FlowNode[];
|
|
34
48
|
/**
|
|
35
49
|
* Step 1: Filter diff lines — added (+) and removed (-) separately
|
|
36
50
|
*/
|
|
@@ -59,4 +73,4 @@ declare function parseDiffToLogicalFlow(diffText: string): ParseResult;
|
|
|
59
73
|
*/
|
|
60
74
|
declare function generateReaderMarkdown(diffText: string, meta?: ReaderMarkdownMeta): string;
|
|
61
75
|
|
|
62
|
-
export { type FlowNode, type ParseResult, Priority, type ReaderMarkdownMeta, filterDiffLines, generateReaderMarkdown, normalizeCode, parseDiffToLogicalFlow, parseToFlowTree, renderFlowTree };
|
|
76
|
+
export { type ClassNameChange, type FlowNode, type ParseResult, Priority, type ReaderMarkdownMeta, extractClassName, extractJSXComponentName, filterDiffLines, generateReaderMarkdown, hasJSXContent, isClassNameOnlyLine, isJSXElement, isJSXFile, normalizeCode, parseClassNameChanges, parseDiffToLogicalFlow, parseJSXToFlowTree, parseToFlowTree, renderFlowTree, renderStyleChanges };
|
package/dist/index.d.ts
CHANGED
|
@@ -31,6 +31,20 @@ interface ReaderMarkdownMeta {
|
|
|
31
31
|
file?: string;
|
|
32
32
|
repo?: string;
|
|
33
33
|
}
|
|
34
|
+
interface ClassNameChange {
|
|
35
|
+
component: string;
|
|
36
|
+
added: string[];
|
|
37
|
+
removed: string[];
|
|
38
|
+
}
|
|
39
|
+
declare function isJSXFile(filename: string): boolean;
|
|
40
|
+
declare function hasJSXContent(lines: string[]): boolean;
|
|
41
|
+
declare function isClassNameOnlyLine(line: string): boolean;
|
|
42
|
+
declare function extractClassName(line: string): string | null;
|
|
43
|
+
declare function parseClassNameChanges(addedLines: string[], removedLines: string[]): ClassNameChange[];
|
|
44
|
+
declare function renderStyleChanges(changes: ClassNameChange[]): string[];
|
|
45
|
+
declare function isJSXElement(line: string): boolean;
|
|
46
|
+
declare function extractJSXComponentName(line: string): string;
|
|
47
|
+
declare function parseJSXToFlowTree(lines: string[]): FlowNode[];
|
|
34
48
|
/**
|
|
35
49
|
* Step 1: Filter diff lines — added (+) and removed (-) separately
|
|
36
50
|
*/
|
|
@@ -59,4 +73,4 @@ declare function parseDiffToLogicalFlow(diffText: string): ParseResult;
|
|
|
59
73
|
*/
|
|
60
74
|
declare function generateReaderMarkdown(diffText: string, meta?: ReaderMarkdownMeta): string;
|
|
61
75
|
|
|
62
|
-
export { type FlowNode, type ParseResult, Priority, type ReaderMarkdownMeta, filterDiffLines, generateReaderMarkdown, normalizeCode, parseDiffToLogicalFlow, parseToFlowTree, renderFlowTree };
|
|
76
|
+
export { type ClassNameChange, type FlowNode, type ParseResult, Priority, type ReaderMarkdownMeta, extractClassName, extractJSXComponentName, filterDiffLines, generateReaderMarkdown, hasJSXContent, isClassNameOnlyLine, isJSXElement, isJSXFile, normalizeCode, parseClassNameChanges, parseDiffToLogicalFlow, parseJSXToFlowTree, parseToFlowTree, renderFlowTree, renderStyleChanges };
|