@haposoft/cafekit 0.3.2 → 0.3.5

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.
@@ -0,0 +1,403 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * AST-Based Analysis Script
5
+ * Phân tích function signatures, type changes, và breaking changes
6
+ *
7
+ * Usage:
8
+ * node ast-analyze.js <file-path>
9
+ * node ast-analyze.js src/services/authService.ts
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+
15
+ // Try to use @babel/parser if available, otherwise provide instructions
16
+ let parser, traverse;
17
+ try {
18
+ parser = require('@babel/parser');
19
+ traverse = require('@babel/traverse').default;
20
+ } catch (e) {
21
+ console.error('❌ Dependencies not installed');
22
+ console.error('Run: npm install --save-dev @babel/parser @babel/traverse');
23
+ process.exit(1);
24
+ }
25
+
26
+ function analyzeFunctionSignatures(filePath) {
27
+ if (!fs.existsSync(filePath)) {
28
+ console.error(`❌ File not found: ${filePath}`);
29
+ process.exit(1);
30
+ }
31
+
32
+ const code = fs.readFileSync(filePath, 'utf-8');
33
+
34
+ let ast;
35
+ try {
36
+ ast = parser.parse(code, {
37
+ sourceType: 'module',
38
+ plugins: ['typescript', 'jsx', 'decorators-legacy']
39
+ });
40
+ } catch (e) {
41
+ console.error(`❌ Parse error: ${e.message}`);
42
+ process.exit(1);
43
+ }
44
+
45
+ const functions = [];
46
+ const classes = [];
47
+ const interfaces = [];
48
+ const types = [];
49
+
50
+ traverse(ast, {
51
+ // Function declarations
52
+ FunctionDeclaration(path) {
53
+ functions.push({
54
+ type: 'function',
55
+ name: path.node.id?.name || 'anonymous',
56
+ params: path.node.params.map(p => getParamName(p)),
57
+ paramCount: path.node.params.length,
58
+ async: path.node.async,
59
+ generator: path.node.generator,
60
+ line: path.node.loc.start.line,
61
+ exported: isExported(path)
62
+ });
63
+ },
64
+
65
+ // Arrow functions
66
+ ArrowFunctionExpression(path) {
67
+ if (path.parent.type === 'VariableDeclarator') {
68
+ functions.push({
69
+ type: 'arrow',
70
+ name: path.parent.id.name,
71
+ params: path.node.params.map(p => getParamName(p)),
72
+ paramCount: path.node.params.length,
73
+ async: path.node.async,
74
+ line: path.node.loc.start.line,
75
+ exported: isExported(path.parentPath.parentPath)
76
+ });
77
+ }
78
+ },
79
+
80
+ // Class declarations
81
+ ClassDeclaration(path) {
82
+ const methods = [];
83
+ path.node.body.body.forEach(member => {
84
+ if (member.type === 'ClassMethod') {
85
+ methods.push({
86
+ name: member.key.name,
87
+ kind: member.kind, // constructor, method, get, set
88
+ params: member.params.map(p => getParamName(p)),
89
+ paramCount: member.params.length,
90
+ async: member.async,
91
+ static: member.static,
92
+ line: member.loc.start.line
93
+ });
94
+ }
95
+ });
96
+
97
+ classes.push({
98
+ name: path.node.id.name,
99
+ methods,
100
+ line: path.node.loc.start.line,
101
+ exported: isExported(path)
102
+ });
103
+ },
104
+
105
+ // TypeScript interfaces
106
+ TSInterfaceDeclaration(path) {
107
+ const properties = path.node.body.body.map(prop => ({
108
+ name: prop.key?.name || 'unknown',
109
+ optional: prop.optional,
110
+ type: getTypeAnnotation(prop.typeAnnotation)
111
+ }));
112
+
113
+ interfaces.push({
114
+ name: path.node.id.name,
115
+ properties,
116
+ line: path.node.loc.start.line,
117
+ exported: isExported(path)
118
+ });
119
+ },
120
+
121
+ // TypeScript type aliases
122
+ TSTypeAliasDeclaration(path) {
123
+ types.push({
124
+ name: path.node.id.name,
125
+ line: path.node.loc.start.line,
126
+ exported: isExported(path)
127
+ });
128
+ }
129
+ });
130
+
131
+ return {
132
+ file: filePath,
133
+ functions,
134
+ classes,
135
+ interfaces,
136
+ types,
137
+ summary: {
138
+ totalFunctions: functions.length,
139
+ totalClasses: classes.length,
140
+ totalInterfaces: interfaces.length,
141
+ totalTypes: types.length,
142
+ exportedFunctions: functions.filter(f => f.exported).length,
143
+ asyncFunctions: functions.filter(f => f.async).length
144
+ }
145
+ };
146
+ }
147
+
148
+ function getParamName(param) {
149
+ if (param.type === 'Identifier') {
150
+ return param.name;
151
+ } else if (param.type === 'AssignmentPattern') {
152
+ return `${getParamName(param.left)} = ${param.right.value || '...'}`;
153
+ } else if (param.type === 'RestElement') {
154
+ return `...${param.argument.name}`;
155
+ } else if (param.type === 'ObjectPattern') {
156
+ return `{ ${param.properties.map(p => p.key?.name || '?').join(', ')} }`;
157
+ }
158
+ return 'unknown';
159
+ }
160
+
161
+ function getTypeAnnotation(typeAnnotation) {
162
+ if (!typeAnnotation) return 'any';
163
+ const type = typeAnnotation.typeAnnotation;
164
+ if (!type) return 'any';
165
+
166
+ if (type.type === 'TSStringKeyword') return 'string';
167
+ if (type.type === 'TSNumberKeyword') return 'number';
168
+ if (type.type === 'TSBooleanKeyword') return 'boolean';
169
+ if (type.type === 'TSAnyKeyword') return 'any';
170
+ if (type.type === 'TSTypeReference') return type.typeName.name;
171
+
172
+ return 'unknown';
173
+ }
174
+
175
+ function isExported(path) {
176
+ let current = path;
177
+ while (current) {
178
+ if (current.node.type === 'ExportNamedDeclaration' ||
179
+ current.node.type === 'ExportDefaultDeclaration') {
180
+ return true;
181
+ }
182
+ current = current.parentPath;
183
+ }
184
+ return false;
185
+ }
186
+
187
+ function compareAnalysis(before, after) {
188
+ const changes = {
189
+ functionsAdded: [],
190
+ functionsRemoved: [],
191
+ functionsModified: [],
192
+ interfacesAdded: [],
193
+ interfacesRemoved: [],
194
+ interfacesModified: []
195
+ };
196
+
197
+ // Compare functions
198
+ const beforeFuncs = new Map(before.functions.map(f => [f.name, f]));
199
+ const afterFuncs = new Map(after.functions.map(f => [f.name, f]));
200
+
201
+ // Find added functions
202
+ afterFuncs.forEach((func, name) => {
203
+ if (!beforeFuncs.has(name)) {
204
+ changes.functionsAdded.push(func);
205
+ }
206
+ });
207
+
208
+ // Find removed functions
209
+ beforeFuncs.forEach((func, name) => {
210
+ if (!afterFuncs.has(name)) {
211
+ changes.functionsRemoved.push(func);
212
+ }
213
+ });
214
+
215
+ // Find modified functions
216
+ beforeFuncs.forEach((beforeFunc, name) => {
217
+ const afterFunc = afterFuncs.get(name);
218
+ if (afterFunc && beforeFunc.paramCount !== afterFunc.paramCount) {
219
+ changes.functionsModified.push({
220
+ name,
221
+ before: beforeFunc,
222
+ after: afterFunc,
223
+ breaking: true,
224
+ reason: `Parameter count changed: ${beforeFunc.paramCount} → ${afterFunc.paramCount}`
225
+ });
226
+ }
227
+ });
228
+
229
+ // Compare interfaces
230
+ const beforeInterfaces = new Map(before.interfaces.map(i => [i.name, i]));
231
+ const afterInterfaces = new Map(after.interfaces.map(i => [i.name, i]));
232
+
233
+ afterInterfaces.forEach((iface, name) => {
234
+ if (!beforeInterfaces.has(name)) {
235
+ changes.interfacesAdded.push(iface);
236
+ } else {
237
+ const beforeIface = beforeInterfaces.get(name);
238
+ const beforeProps = new Set(beforeIface.properties.map(p => p.name));
239
+ const afterProps = new Set(iface.properties.map(p => p.name));
240
+
241
+ const added = [...afterProps].filter(p => !beforeProps.has(p));
242
+ const removed = [...beforeProps].filter(p => !afterProps.has(p));
243
+
244
+ if (added.length > 0 || removed.length > 0) {
245
+ changes.interfacesModified.push({
246
+ name,
247
+ propertiesAdded: added,
248
+ propertiesRemoved: removed,
249
+ breaking: removed.length > 0
250
+ });
251
+ }
252
+ }
253
+ });
254
+
255
+ beforeInterfaces.forEach((iface, name) => {
256
+ if (!afterInterfaces.has(name)) {
257
+ changes.interfacesRemoved.push(iface);
258
+ }
259
+ });
260
+
261
+ return changes;
262
+ }
263
+
264
+ function printAnalysis(analysis) {
265
+ console.log('\n📊 AST Analysis Results\n');
266
+ console.log(`File: ${analysis.file}`);
267
+ console.log(`\n📈 Summary:`);
268
+ console.log(` Functions: ${analysis.summary.totalFunctions} (${analysis.summary.exportedFunctions} exported, ${analysis.summary.asyncFunctions} async)`);
269
+ console.log(` Classes: ${analysis.summary.totalClasses}`);
270
+ console.log(` Interfaces: ${analysis.summary.totalInterfaces}`);
271
+ console.log(` Types: ${analysis.summary.totalTypes}`);
272
+
273
+ if (analysis.functions.length > 0) {
274
+ console.log(`\n🔧 Functions:`);
275
+ analysis.functions.forEach(func => {
276
+ const badge = func.exported ? '📤' : ' ';
277
+ const asyncBadge = func.async ? 'async ' : '';
278
+ console.log(` ${badge} ${asyncBadge}${func.name}(${func.params.join(', ')}) - line ${func.line}`);
279
+ });
280
+ }
281
+
282
+ if (analysis.classes.length > 0) {
283
+ console.log(`\n🏗️ Classes:`);
284
+ analysis.classes.forEach(cls => {
285
+ console.log(` ${cls.exported ? '📤' : ' '} ${cls.name} - line ${cls.line}`);
286
+ cls.methods.forEach(method => {
287
+ const badge = method.static ? 'static ' : '';
288
+ const asyncBadge = method.async ? 'async ' : '';
289
+ console.log(` ${badge}${asyncBadge}${method.name}(${method.params.join(', ')})`);
290
+ });
291
+ });
292
+ }
293
+
294
+ if (analysis.interfaces.length > 0) {
295
+ console.log(`\n📋 Interfaces:`);
296
+ analysis.interfaces.forEach(iface => {
297
+ console.log(` ${iface.exported ? '📤' : ' '} ${iface.name} - line ${iface.line}`);
298
+ iface.properties.forEach(prop => {
299
+ const optional = prop.optional ? '?' : '';
300
+ console.log(` ${prop.name}${optional}: ${prop.type}`);
301
+ });
302
+ });
303
+ }
304
+ }
305
+
306
+ function printComparison(changes) {
307
+ console.log('\n🔍 Changes Detected\n');
308
+
309
+ let hasChanges = false;
310
+
311
+ if (changes.functionsAdded.length > 0) {
312
+ hasChanges = true;
313
+ console.log('✅ Functions Added:');
314
+ changes.functionsAdded.forEach(func => {
315
+ console.log(` + ${func.name}(${func.params.join(', ')})`);
316
+ });
317
+ }
318
+
319
+ if (changes.functionsRemoved.length > 0) {
320
+ hasChanges = true;
321
+ console.log('\n❌ Functions Removed:');
322
+ changes.functionsRemoved.forEach(func => {
323
+ console.log(` - ${func.name}(${func.params.join(', ')})`);
324
+ });
325
+ }
326
+
327
+ if (changes.functionsModified.length > 0) {
328
+ hasChanges = true;
329
+ console.log('\n⚠️ Functions Modified (BREAKING):');
330
+ changes.functionsModified.forEach(change => {
331
+ console.log(` ~ ${change.name}`);
332
+ console.log(` Before: ${change.before.params.join(', ')}`);
333
+ console.log(` After: ${change.after.params.join(', ')}`);
334
+ console.log(` Reason: ${change.reason}`);
335
+ });
336
+ }
337
+
338
+ if (changes.interfacesAdded.length > 0) {
339
+ hasChanges = true;
340
+ console.log('\n✅ Interfaces Added:');
341
+ changes.interfacesAdded.forEach(iface => {
342
+ console.log(` + ${iface.name}`);
343
+ });
344
+ }
345
+
346
+ if (changes.interfacesRemoved.length > 0) {
347
+ hasChanges = true;
348
+ console.log('\n❌ Interfaces Removed:');
349
+ changes.interfacesRemoved.forEach(iface => {
350
+ console.log(` - ${iface.name}`);
351
+ });
352
+ }
353
+
354
+ if (changes.interfacesModified.length > 0) {
355
+ hasChanges = true;
356
+ console.log('\n⚠️ Interfaces Modified:');
357
+ changes.interfacesModified.forEach(change => {
358
+ console.log(` ~ ${change.name} ${change.breaking ? '(BREAKING)' : ''}`);
359
+ if (change.propertiesAdded.length > 0) {
360
+ console.log(` Added: ${change.propertiesAdded.join(', ')}`);
361
+ }
362
+ if (change.propertiesRemoved.length > 0) {
363
+ console.log(` Removed: ${change.propertiesRemoved.join(', ')}`);
364
+ }
365
+ });
366
+ }
367
+
368
+ if (!hasChanges) {
369
+ console.log('✅ No semantic changes detected');
370
+ }
371
+ }
372
+
373
+ // Main
374
+ const args = process.argv.slice(2);
375
+
376
+ if (args.length === 0) {
377
+ console.error('Usage: node ast-analyze.js <file-path> [--compare <before-file>]');
378
+ console.error('Example: node ast-analyze.js src/services/authService.ts');
379
+ console.error('Example: node ast-analyze.js src/services/authService.ts --compare /tmp/before.ts');
380
+ process.exit(1);
381
+ }
382
+
383
+ const filePath = args[0];
384
+ const compareMode = args.includes('--compare');
385
+ const beforeFile = compareMode ? args[args.indexOf('--compare') + 1] : null;
386
+
387
+ if (compareMode && beforeFile) {
388
+ const before = analyzeFunctionSignatures(beforeFile);
389
+ const after = analyzeFunctionSignatures(filePath);
390
+ const changes = compareAnalysis(before, after);
391
+ printComparison(changes);
392
+ } else {
393
+ const analysis = analyzeFunctionSignatures(filePath);
394
+ printAnalysis(analysis);
395
+ }
396
+
397
+ // Export for programmatic use
398
+ if (typeof module !== 'undefined' && module.exports) {
399
+ module.exports = {
400
+ analyzeFunctionSignatures,
401
+ compareAnalysis
402
+ };
403
+ }