@parseme/cli 0.0.3 → 0.0.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.
- package/README.md +182 -187
- package/dist/cli/cli.js +144 -0
- package/dist/{analyzers → core/analyzers}/ast-analyzer.d.ts +1 -1
- package/dist/{analyzers → core/analyzers}/ast-analyzer.js +14 -27
- package/dist/core/analyzers/framework-detector.d.ts +7 -0
- package/dist/core/analyzers/framework-detector.js +165 -0
- package/dist/{analyzers → core/analyzers}/pattern-detector.d.ts +2 -5
- package/dist/{analyzers → core/analyzers}/pattern-detector.js +134 -49
- package/dist/{analyzers → core/analyzers}/project-analyzer.d.ts +2 -0
- package/dist/{analyzers → core/analyzers}/project-analyzer.js +12 -9
- package/dist/core/config.d.ts +19 -0
- package/dist/{config.js → core/config.js} +79 -91
- package/dist/{builders → core}/context-builder.d.ts +4 -11
- package/dist/core/context-builder.js +225 -0
- package/dist/{generator.d.ts → core/generator.d.ts} +0 -3
- package/dist/{generator.js → core/generator.js} +12 -17
- package/dist/core/types/analyzer-types.d.ts +38 -0
- package/dist/core/types/analyzer-types.js +2 -0
- package/dist/core/types/config-types.d.ts +36 -0
- package/dist/core/types/config-types.js +2 -0
- package/dist/core/types/generator-types.d.ts +6 -0
- package/dist/core/types/generator-types.js +2 -0
- package/dist/core/types/index.d.ts +4 -0
- package/dist/core/types/index.js +5 -0
- package/dist/core/types/project-types.d.ts +30 -0
- package/dist/core/types/project-types.js +2 -0
- package/dist/core/types.d.ts +1 -0
- package/dist/core/types.js +2 -0
- package/dist/index.d.ts +3 -4
- package/dist/index.js +2 -2
- package/dist/utils/file-collector.d.ts +23 -0
- package/dist/utils/file-collector.js +61 -0
- package/dist/utils/file-filter.d.ts +30 -0
- package/dist/utils/file-filter.js +99 -0
- package/dist/{analyzers/git-analyzer.d.ts → utils/git.d.ts} +1 -4
- package/dist/{analyzers/git-analyzer.js → utils/git.js} +0 -4
- package/dist/{prompt.d.ts → utils/prompt.d.ts} +0 -1
- package/dist/{prompt.js → utils/prompt.js} +0 -9
- package/package.json +12 -8
- package/dist/analyzers/framework-detector.d.ts +0 -12
- package/dist/analyzers/framework-detector.js +0 -180
- package/dist/builders/context-builder.js +0 -386
- package/dist/cli.js +0 -145
- package/dist/config.d.ts +0 -44
- package/dist/types.d.ts +0 -84
- package/dist/types.js +0 -2
- /package/dist/{cli.d.ts → cli/cli.d.ts} +0 -0
package/dist/cli/cli.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { ParsemeConfig } from '../core/config.js';
|
|
5
|
+
import { ParsemeGenerator } from '../core/generator.js';
|
|
6
|
+
import { prompt } from '../utils/prompt.js';
|
|
7
|
+
const program = new Command();
|
|
8
|
+
async function promptForMissingConfig(config) {
|
|
9
|
+
return { ...config };
|
|
10
|
+
}
|
|
11
|
+
program.name('parseme').description('AI Project Context Generator').version('0.1.0');
|
|
12
|
+
// Generate command
|
|
13
|
+
program
|
|
14
|
+
.command('generate')
|
|
15
|
+
.alias('g')
|
|
16
|
+
.description('Generate project context using config file')
|
|
17
|
+
.option('-c, --config <path>', 'Config file path')
|
|
18
|
+
.option('-o, --output <path>', 'Output file path')
|
|
19
|
+
.option('-r, --root <path>', 'Root directory to analyze')
|
|
20
|
+
.option('--context-dir <path>', 'Context directory path (default: parseme-context)')
|
|
21
|
+
.option('--file-types <types...>', 'File types to analyze (e.g., ts tsx js jsx)')
|
|
22
|
+
.option('--exclude <patterns...>', 'Exclude patterns (glob)')
|
|
23
|
+
.option('--no-git', 'Disable git integration (info and file discovery)')
|
|
24
|
+
.option('--max-depth <number>', 'Maximum directory depth', parseInt)
|
|
25
|
+
.action(async (options) => {
|
|
26
|
+
try {
|
|
27
|
+
// Convert CLI options to config format
|
|
28
|
+
const cliOptions = {
|
|
29
|
+
...(options.output && { outputPath: options.output }),
|
|
30
|
+
...(options.root && { rootDir: options.root }),
|
|
31
|
+
...(options.contextDir && { contextDir: options.contextDir }),
|
|
32
|
+
...(options.fileTypes && { analyzeFileTypes: options.fileTypes }),
|
|
33
|
+
...(options.exclude && { excludePatterns: options.exclude }),
|
|
34
|
+
...(options.git === false && { includeGitInfo: false, useGitForFiles: false }),
|
|
35
|
+
...(options.maxDepth && { maxDepth: options.maxDepth }),
|
|
36
|
+
};
|
|
37
|
+
const configFromFile = await ParsemeConfig.fromFile(options.config, {
|
|
38
|
+
showWarnings: true,
|
|
39
|
+
throwOnNotFound: true,
|
|
40
|
+
});
|
|
41
|
+
const interactiveConfig = await promptForMissingConfig(configFromFile.get());
|
|
42
|
+
// Merge: CLI options > interactive prompts > config file > defaults
|
|
43
|
+
const finalConfig = {
|
|
44
|
+
...interactiveConfig,
|
|
45
|
+
...cliOptions,
|
|
46
|
+
};
|
|
47
|
+
const config = new ParsemeConfig(finalConfig);
|
|
48
|
+
const generator = new ParsemeGenerator(config.get());
|
|
49
|
+
await generator.generateToFile();
|
|
50
|
+
console.log('Context generated successfully');
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
if (error instanceof Error && error.message.includes('No configuration file found')) {
|
|
54
|
+
console.error(error.message);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
console.error('Failed to generate context:', error);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
// Init command
|
|
62
|
+
program
|
|
63
|
+
.command('init')
|
|
64
|
+
.alias('i')
|
|
65
|
+
.description('Initialize parseme configuration')
|
|
66
|
+
.option('-f, --force', 'Overwrite existing config')
|
|
67
|
+
.option('--format <format>', 'Config format: json, ts, or js', 'json')
|
|
68
|
+
.action(async (options) => {
|
|
69
|
+
try {
|
|
70
|
+
// Validate format
|
|
71
|
+
if (!['js', 'ts', 'json'].includes(options.format)) {
|
|
72
|
+
console.error('Invalid format. Use js, ts, or json');
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
const configPath = join(process.cwd(), `parseme.config.${options.format}`);
|
|
76
|
+
// Check if config already exists
|
|
77
|
+
if (!options.force) {
|
|
78
|
+
try {
|
|
79
|
+
const fs = await import('fs/promises');
|
|
80
|
+
await fs.access(configPath);
|
|
81
|
+
console.log('Configuration file already exists. Use --force to overwrite.');
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// File doesn't exist, continue
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// Build config with only user-specified values
|
|
89
|
+
const userConfig = {};
|
|
90
|
+
// Set defaults that would normally be prompted for
|
|
91
|
+
const defaultExcludePatterns = ['node_modules/**', 'dist/**', '.git/**'];
|
|
92
|
+
userConfig.contextDir = 'parseme-context';
|
|
93
|
+
userConfig.excludePatterns = defaultExcludePatterns;
|
|
94
|
+
// Only prompt if interactive (TTY) and not in CI
|
|
95
|
+
if (process.stdin.isTTY && !process.env.CI) {
|
|
96
|
+
// Ask about context directory path
|
|
97
|
+
// Set context directory to what user entered (or default if they pressed enter)
|
|
98
|
+
userConfig.contextDir = await prompt({
|
|
99
|
+
message: 'Context directory path',
|
|
100
|
+
defaultValue: 'parseme-context',
|
|
101
|
+
});
|
|
102
|
+
// Ask about exclude patterns
|
|
103
|
+
const excludePatternsAnswer = await prompt({
|
|
104
|
+
message: 'Exclude patterns (comma-separated glob patterns - in git repositories, additional to git-tracked files)',
|
|
105
|
+
defaultValue: defaultExcludePatterns.join(', '),
|
|
106
|
+
});
|
|
107
|
+
// Always set exclude patterns to what user entered (or defaults if they pressed enter)
|
|
108
|
+
userConfig.excludePatterns = excludePatternsAnswer
|
|
109
|
+
.split(',')
|
|
110
|
+
.map((p) => p.trim())
|
|
111
|
+
.filter((p) => p.length > 0);
|
|
112
|
+
}
|
|
113
|
+
const config = new ParsemeConfig(userConfig);
|
|
114
|
+
await config.save(configPath);
|
|
115
|
+
console.log(`Configuration file created at ${configPath}`);
|
|
116
|
+
if (options.format === 'ts') {
|
|
117
|
+
console.log('For TypeScript configs, ensure tsx or ts-node is available to load .ts files');
|
|
118
|
+
}
|
|
119
|
+
console.log('Tip: Add "parseme": "parseme" to your package.json scripts for easier manual execution or hook integration');
|
|
120
|
+
console.log('Tip: Add parseme as a git hook to keep context auto-updated! See README for setup instructions.');
|
|
121
|
+
console.log('');
|
|
122
|
+
console.log('Tip: Add this section to your README.md to help AI agents find the context:');
|
|
123
|
+
console.log('');
|
|
124
|
+
console.log('## Instructions For AI Agents');
|
|
125
|
+
console.log('This project includes AI-optimized documentation for efficient context providing:');
|
|
126
|
+
console.log('- `PARSEME.md` - Project overview with links to detailed context files');
|
|
127
|
+
console.log('- `parseme-context/` - Structured data files (AST analysis, dependencies, routes, git info)');
|
|
128
|
+
console.log('');
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
console.error('Failed to create configuration:', error);
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
// If no command provided, show error and available commands
|
|
136
|
+
if (process.argv.length <= 2) {
|
|
137
|
+
console.error('No command specified.\n');
|
|
138
|
+
console.error('Available commands:');
|
|
139
|
+
console.error(' parseme generate (or parseme g) - Generate project context');
|
|
140
|
+
console.error(' parseme init (or parseme i) - Initialize parseme configuration');
|
|
141
|
+
console.error('\nUse "parseme --help" for more information');
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
program.parse();
|
|
@@ -2,7 +2,7 @@ import type { ParsemeConfig } from '../config.js';
|
|
|
2
2
|
import type { FileAnalysis } from '../types.js';
|
|
3
3
|
export declare class ASTAnalyzer {
|
|
4
4
|
private readonly config;
|
|
5
|
-
private readonly
|
|
5
|
+
private readonly fileCollector;
|
|
6
6
|
private readonly patternDetector;
|
|
7
7
|
constructor(config: ParsemeConfig);
|
|
8
8
|
analyzeProject(rootDir: string): Promise<FileAnalysis[]>;
|
|
@@ -1,45 +1,32 @@
|
|
|
1
1
|
import { readFile } from 'fs/promises';
|
|
2
|
-
import {
|
|
2
|
+
import { join, extname } from 'path';
|
|
3
3
|
import { parse } from '@babel/parser';
|
|
4
4
|
import traverse from '@babel/traverse';
|
|
5
5
|
import * as t from '@babel/types';
|
|
6
|
-
import { glob } from 'glob';
|
|
7
|
-
import ignore from 'ignore';
|
|
8
6
|
import { PatternDetector } from './pattern-detector.js';
|
|
7
|
+
import { FileCollector } from '../../utils/file-collector.js';
|
|
9
8
|
export class ASTAnalyzer {
|
|
10
9
|
config;
|
|
11
|
-
|
|
10
|
+
fileCollector;
|
|
12
11
|
patternDetector;
|
|
13
12
|
constructor(config) {
|
|
14
13
|
this.config = config;
|
|
15
|
-
this.
|
|
16
|
-
|
|
17
|
-
this.ig.add(configData.excludePatterns || []);
|
|
18
|
-
this.patternDetector = new PatternDetector(config);
|
|
14
|
+
this.fileCollector = new FileCollector(config);
|
|
15
|
+
this.patternDetector = new PatternDetector();
|
|
19
16
|
}
|
|
20
17
|
async analyzeProject(rootDir) {
|
|
21
|
-
const
|
|
22
|
-
const patterns = configData.includePatterns || ['**/*.ts', '**/*.js'];
|
|
23
|
-
const files = await glob(patterns, {
|
|
24
|
-
cwd: rootDir,
|
|
25
|
-
absolute: true,
|
|
26
|
-
ignore: configData.excludePatterns,
|
|
27
|
-
});
|
|
18
|
+
const result = await this.fileCollector.getCodeFiles(rootDir);
|
|
28
19
|
const analyses = [];
|
|
29
|
-
for (const file of files) {
|
|
30
|
-
const
|
|
31
|
-
// Skip if ignored
|
|
32
|
-
if (this.ig.ignores(relativePath)) {
|
|
33
|
-
continue;
|
|
34
|
-
}
|
|
20
|
+
for (const file of result.files) {
|
|
21
|
+
const filePath = join(rootDir, file);
|
|
35
22
|
try {
|
|
36
|
-
const analysis = await this.analyzeFile(
|
|
23
|
+
const analysis = await this.analyzeFile(filePath, file);
|
|
37
24
|
if (analysis) {
|
|
38
25
|
analyses.push(analysis);
|
|
39
26
|
}
|
|
40
27
|
}
|
|
41
28
|
catch (error) {
|
|
42
|
-
console.warn(`Failed to analyze ${
|
|
29
|
+
console.warn(`Failed to analyze ${file}:`, error);
|
|
43
30
|
// Continue with other files
|
|
44
31
|
}
|
|
45
32
|
}
|
|
@@ -58,7 +45,7 @@ export class ASTAnalyzer {
|
|
|
58
45
|
const patterns = this.patternDetector.analyzePatterns(ast, relativePath, content);
|
|
59
46
|
const analysis = {
|
|
60
47
|
path: relativePath,
|
|
61
|
-
type: this.determineFileType(relativePath,
|
|
48
|
+
type: this.determineFileType(relativePath, patterns),
|
|
62
49
|
exports: [],
|
|
63
50
|
imports: [],
|
|
64
51
|
functions: [],
|
|
@@ -116,8 +103,8 @@ export class ASTAnalyzer {
|
|
|
116
103
|
});
|
|
117
104
|
return analysis;
|
|
118
105
|
}
|
|
119
|
-
catch
|
|
120
|
-
console.warn(`Failed to parse ${relativePath}
|
|
106
|
+
catch {
|
|
107
|
+
console.warn(`Failed to parse ${relativePath}`);
|
|
121
108
|
return null;
|
|
122
109
|
}
|
|
123
110
|
}
|
|
@@ -144,7 +131,7 @@ export class ASTAnalyzer {
|
|
|
144
131
|
],
|
|
145
132
|
});
|
|
146
133
|
}
|
|
147
|
-
determineFileType(relativePath,
|
|
134
|
+
determineFileType(relativePath, patterns) {
|
|
148
135
|
// Use pattern analysis to determine file type dynamically
|
|
149
136
|
if (patterns.endpoints.length > 0) {
|
|
150
137
|
return 'route';
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
export class FrameworkDetector {
|
|
2
|
+
frameworks = [
|
|
3
|
+
// Backend frameworks
|
|
4
|
+
{
|
|
5
|
+
name: 'nestjs',
|
|
6
|
+
detectionKeys: ['@nestjs/core', '@nestjs/common'],
|
|
7
|
+
versionKey: '@nestjs/core',
|
|
8
|
+
builtInFeatures: ['decorators', 'dependency-injection', 'modules'],
|
|
9
|
+
featureMap: {
|
|
10
|
+
'@nestjs/typeorm': 'orm',
|
|
11
|
+
'@nestjs/mongoose': 'orm',
|
|
12
|
+
'@nestjs/passport': 'authentication',
|
|
13
|
+
'@nestjs/jwt': 'jwt',
|
|
14
|
+
'@nestjs/swagger': 'swagger',
|
|
15
|
+
'@nestjs/graphql': 'graphql',
|
|
16
|
+
'@nestjs/websockets': 'websockets',
|
|
17
|
+
'@nestjs/microservices': 'microservices',
|
|
18
|
+
'@nestjs/testing': 'testing',
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
name: 'fastify',
|
|
23
|
+
detectionKeys: ['fastify'],
|
|
24
|
+
versionKey: 'fastify',
|
|
25
|
+
featureMap: {
|
|
26
|
+
'@fastify/cors': 'cors',
|
|
27
|
+
'@fastify/helmet': 'security',
|
|
28
|
+
'@fastify/rate-limit': 'rate-limiting',
|
|
29
|
+
'@fastify/multipart': 'file-upload',
|
|
30
|
+
'@fastify/static': 'static-files',
|
|
31
|
+
'@fastify/jwt': 'jwt',
|
|
32
|
+
'@fastify/session': 'sessions',
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: 'express',
|
|
37
|
+
detectionKeys: ['express'],
|
|
38
|
+
versionKey: 'express',
|
|
39
|
+
featureMap: {
|
|
40
|
+
'express-session': 'sessions',
|
|
41
|
+
passport: 'authentication',
|
|
42
|
+
'express-rate-limit': 'rate-limiting',
|
|
43
|
+
helmet: 'security',
|
|
44
|
+
cors: 'cors',
|
|
45
|
+
'body-parser': 'body-parsing',
|
|
46
|
+
'express-validator': 'validation',
|
|
47
|
+
multer: 'file-upload',
|
|
48
|
+
'express-static': 'static-files',
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
// Fullstack frameworks
|
|
52
|
+
{
|
|
53
|
+
name: 'next.js',
|
|
54
|
+
detectionKeys: ['next'],
|
|
55
|
+
versionKey: 'next',
|
|
56
|
+
builtInFeatures: ['ssr', 'routing', 'api-routes', 'file-based-routing'],
|
|
57
|
+
featureMap: {
|
|
58
|
+
'next-auth': 'authentication',
|
|
59
|
+
'@vercel/analytics': 'analytics',
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: 'nuxt.js',
|
|
64
|
+
detectionKeys: ['nuxt'],
|
|
65
|
+
versionKey: 'nuxt',
|
|
66
|
+
builtInFeatures: ['ssr', 'routing', 'api-routes', 'file-based-routing', 'auto-imports'],
|
|
67
|
+
featureMap: {
|
|
68
|
+
'@nuxt/content': 'content-management',
|
|
69
|
+
'@nuxtjs/auth': 'authentication',
|
|
70
|
+
'@nuxtjs/auth-next': 'authentication',
|
|
71
|
+
'@pinia/nuxt': 'state-management-pinia',
|
|
72
|
+
'@nuxt/image': 'image-optimization',
|
|
73
|
+
'@nuxtjs/tailwindcss': 'tailwind',
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
// Frontend frameworks
|
|
77
|
+
{
|
|
78
|
+
name: 'react',
|
|
79
|
+
detectionKeys: ['react', 'react-dom'],
|
|
80
|
+
versionKey: 'react',
|
|
81
|
+
featureMap: {
|
|
82
|
+
'react-router': 'routing',
|
|
83
|
+
'react-router-dom': 'routing',
|
|
84
|
+
redux: 'state-management-redux',
|
|
85
|
+
'@reduxjs/toolkit': 'state-management-redux',
|
|
86
|
+
zustand: 'state-management-zustand',
|
|
87
|
+
'react-query': 'data-fetching',
|
|
88
|
+
'@tanstack/react-query': 'data-fetching',
|
|
89
|
+
'@testing-library/react': 'testing',
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: 'vue',
|
|
94
|
+
detectionKeys: (deps) => !!deps['vue'] || Object.keys(deps).some((dep) => dep.startsWith('@vue/')),
|
|
95
|
+
versionKey: 'vue',
|
|
96
|
+
featureMap: {
|
|
97
|
+
'vue-router': 'routing',
|
|
98
|
+
pinia: 'state-management-pinia',
|
|
99
|
+
vuex: 'state-management-vuex',
|
|
100
|
+
'@vue/test-utils': 'testing',
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
name: 'angular',
|
|
105
|
+
detectionKeys: ['@angular/core'],
|
|
106
|
+
versionKey: '@angular/core',
|
|
107
|
+
builtInFeatures: ['decorators', 'dependency-injection', 'typescript'],
|
|
108
|
+
featureMap: {
|
|
109
|
+
'@angular/router': 'routing',
|
|
110
|
+
'@angular/forms': 'forms',
|
|
111
|
+
'@angular/common/http': 'http-client',
|
|
112
|
+
'@angular/common': 'http-client',
|
|
113
|
+
'@ngrx/store': 'state-management-ngrx',
|
|
114
|
+
'@angular/material': 'material-design',
|
|
115
|
+
'@angular/animations': 'animations',
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: 'svelte',
|
|
120
|
+
detectionKeys: (deps) => !!deps['svelte'] || Object.keys(deps).some((dep) => dep.startsWith('@sveltejs/')),
|
|
121
|
+
versionKey: 'svelte',
|
|
122
|
+
featureMap: {
|
|
123
|
+
'@sveltejs/kit': 'sveltekit',
|
|
124
|
+
'@sveltejs/adapter-auto': 'sveltekit',
|
|
125
|
+
'svelte-routing': 'routing',
|
|
126
|
+
'@testing-library/svelte': 'testing',
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
];
|
|
130
|
+
async detect(projectInfo) {
|
|
131
|
+
// Only check dependencies (not devDependencies) for framework detection
|
|
132
|
+
// This prevents false positives from libraries that have frameworks in devDependencies for testing
|
|
133
|
+
const deps = projectInfo.dependencies || {};
|
|
134
|
+
const detectedFrameworks = [];
|
|
135
|
+
for (const config of this.frameworks) {
|
|
136
|
+
if (this.shouldDetect(config, deps)) {
|
|
137
|
+
detectedFrameworks.push(this.buildFrameworkInfo(config, deps));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return detectedFrameworks;
|
|
141
|
+
}
|
|
142
|
+
shouldDetect(config, deps) {
|
|
143
|
+
if (typeof config.detectionKeys === 'function') {
|
|
144
|
+
return config.detectionKeys(deps);
|
|
145
|
+
}
|
|
146
|
+
return config.detectionKeys.some((key) => deps[key]);
|
|
147
|
+
}
|
|
148
|
+
buildFrameworkInfo(config, deps) {
|
|
149
|
+
const features = [...(config.builtInFeatures || [])];
|
|
150
|
+
// Detect features based on feature map
|
|
151
|
+
for (const [depKey, feature] of Object.entries(config.featureMap)) {
|
|
152
|
+
if (deps[depKey]) {
|
|
153
|
+
// Avoid duplicates
|
|
154
|
+
if (!features.includes(feature)) {
|
|
155
|
+
features.push(feature);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
name: config.name,
|
|
161
|
+
version: deps[config.versionKey],
|
|
162
|
+
features,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import * as t from '@babel/types';
|
|
2
|
-
import type { ParsemeConfig } from '../config.js';
|
|
3
2
|
import type { ComponentInfo, RouteInfo } from '../types.js';
|
|
4
3
|
export interface PatternAnalysis {
|
|
5
4
|
endpoints: EndpointInfo[];
|
|
@@ -11,8 +10,6 @@ export interface PatternAnalysis {
|
|
|
11
10
|
utilities: UtilityInfo[];
|
|
12
11
|
}
|
|
13
12
|
export interface EndpointInfo extends RouteInfo {
|
|
14
|
-
type: 'rest' | 'graphql' | 'websocket' | 'rpc' | 'unknown';
|
|
15
|
-
framework?: string;
|
|
16
13
|
decorator?: string;
|
|
17
14
|
}
|
|
18
15
|
export interface ServiceInfo {
|
|
@@ -51,8 +48,8 @@ export interface UtilityInfo {
|
|
|
51
48
|
type: 'helper' | 'lib' | 'hook' | 'composable';
|
|
52
49
|
}
|
|
53
50
|
export declare class PatternDetector {
|
|
54
|
-
private readonly config;
|
|
55
|
-
constructor(config: ParsemeConfig);
|
|
56
51
|
analyzePatterns(ast: t.File, filePath: string, _content: string): PatternAnalysis;
|
|
57
52
|
private hasJSXReturn;
|
|
53
|
+
private extractNextJSRoutePath;
|
|
54
|
+
private extractNuxtRoutePath;
|
|
58
55
|
}
|
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import traverse from '@babel/traverse';
|
|
2
2
|
import * as t from '@babel/types';
|
|
3
3
|
export class PatternDetector {
|
|
4
|
-
config;
|
|
5
|
-
constructor(config) {
|
|
6
|
-
this.config = config;
|
|
7
|
-
}
|
|
8
4
|
analyzePatterns(ast, filePath, _content) {
|
|
9
5
|
const analysis = {
|
|
10
6
|
endpoints: [],
|
|
@@ -15,27 +11,8 @@ export class PatternDetector {
|
|
|
15
11
|
middleware: [],
|
|
16
12
|
utilities: [],
|
|
17
13
|
};
|
|
18
|
-
//
|
|
14
|
+
// Analyze patterns in the AST
|
|
19
15
|
traverse.default(ast, {
|
|
20
|
-
// Detect Express-style routes: app.get(), router.post(), etc.
|
|
21
|
-
CallExpression: (path) => {
|
|
22
|
-
if (t.isMemberExpression(path.node.callee) && t.isIdentifier(path.node.callee.property)) {
|
|
23
|
-
const methodName = path.node.callee.property.name;
|
|
24
|
-
const httpMethods = ['get', 'post', 'put', 'delete', 'patch', 'options', 'head', 'all'];
|
|
25
|
-
if (httpMethods.includes(methodName)) {
|
|
26
|
-
const routeArg = path.node.arguments[0];
|
|
27
|
-
const routePath = t.isStringLiteral(routeArg) ? routeArg.value : '/';
|
|
28
|
-
analysis.endpoints.push({
|
|
29
|
-
method: methodName.toUpperCase(),
|
|
30
|
-
path: routePath,
|
|
31
|
-
handler: 'callback',
|
|
32
|
-
file: filePath,
|
|
33
|
-
line: path.node.loc?.start.line || 0,
|
|
34
|
-
type: 'rest',
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
},
|
|
39
16
|
// Detect decorator-based routes: @Get(), @Post(), etc.
|
|
40
17
|
ClassMethod: (path) => {
|
|
41
18
|
const decorators = path.node.decorators;
|
|
@@ -54,7 +31,6 @@ export class PatternDetector {
|
|
|
54
31
|
handler: t.isIdentifier(path.node.key) ? path.node.key.name : 'anonymous',
|
|
55
32
|
file: filePath,
|
|
56
33
|
line: path.node.loc?.start.line || 0,
|
|
57
|
-
type: 'rest',
|
|
58
34
|
decorator: callee.name,
|
|
59
35
|
});
|
|
60
36
|
}
|
|
@@ -63,6 +39,107 @@ export class PatternDetector {
|
|
|
63
39
|
});
|
|
64
40
|
}
|
|
65
41
|
},
|
|
42
|
+
// Detect Express/Fastify-style routes: app.get(), router.post(), fastify.get(), etc.
|
|
43
|
+
CallExpression: (path) => {
|
|
44
|
+
const { callee, arguments: args } = path.node;
|
|
45
|
+
if (t.isMemberExpression(callee) && t.isIdentifier(callee.property)) {
|
|
46
|
+
const httpMethods = ['get', 'post', 'put', 'delete', 'patch', 'options', 'head'];
|
|
47
|
+
const methodName = callee.property.name;
|
|
48
|
+
if (httpMethods.includes(methodName) && args.length >= 2) {
|
|
49
|
+
const routePath = args[0];
|
|
50
|
+
// Only detect if:
|
|
51
|
+
// 1. First argument is a string literal (route path)
|
|
52
|
+
// 2. Object is a known route handler name
|
|
53
|
+
if (t.isStringLiteral(routePath) && t.isIdentifier(callee.object)) {
|
|
54
|
+
const objectName = callee.object.name;
|
|
55
|
+
const routeObjectNames = [
|
|
56
|
+
'app',
|
|
57
|
+
'router',
|
|
58
|
+
'server',
|
|
59
|
+
'fastify',
|
|
60
|
+
'express',
|
|
61
|
+
'route',
|
|
62
|
+
'api',
|
|
63
|
+
];
|
|
64
|
+
// Only detect if it's a common route object name
|
|
65
|
+
// This filters out axios.get(), client.get(), etc.
|
|
66
|
+
if (routeObjectNames.includes(objectName.toLowerCase())) {
|
|
67
|
+
analysis.endpoints.push({
|
|
68
|
+
method: methodName.toUpperCase(),
|
|
69
|
+
path: routePath.value,
|
|
70
|
+
handler: 'anonymous',
|
|
71
|
+
file: filePath,
|
|
72
|
+
line: path.node.loc?.start.line || 0,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Detect Nuxt.js server routes: defineEventHandler()
|
|
79
|
+
if (t.isIdentifier(callee) && callee.name === 'defineEventHandler') {
|
|
80
|
+
// Extract route path from file path
|
|
81
|
+
// Nuxt server routes are in server/api/ or server/routes/
|
|
82
|
+
const routePath = this.extractNuxtRoutePath(filePath);
|
|
83
|
+
analysis.endpoints.push({
|
|
84
|
+
method: 'GET/POST',
|
|
85
|
+
path: routePath,
|
|
86
|
+
handler: 'defineEventHandler',
|
|
87
|
+
file: filePath,
|
|
88
|
+
line: path.node.loc?.start.line || 0,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
// Detect TypeScript interfaces and type aliases
|
|
93
|
+
TSInterfaceDeclaration: (path) => {
|
|
94
|
+
const interfaceName = path.node.id.name;
|
|
95
|
+
const fields = path.node.body.body
|
|
96
|
+
.filter((member) => t.isTSPropertySignature(member))
|
|
97
|
+
.map((member) => {
|
|
98
|
+
if (t.isIdentifier(member.key)) {
|
|
99
|
+
return member.key.name;
|
|
100
|
+
}
|
|
101
|
+
return '';
|
|
102
|
+
})
|
|
103
|
+
.filter((name) => name !== '');
|
|
104
|
+
analysis.models.push({
|
|
105
|
+
name: interfaceName,
|
|
106
|
+
file: filePath,
|
|
107
|
+
line: path.node.loc?.start.line || 0,
|
|
108
|
+
fields,
|
|
109
|
+
type: 'interface',
|
|
110
|
+
});
|
|
111
|
+
},
|
|
112
|
+
TSTypeAliasDeclaration: (path) => {
|
|
113
|
+
const typeName = path.node.id.name;
|
|
114
|
+
analysis.models.push({
|
|
115
|
+
name: typeName,
|
|
116
|
+
file: filePath,
|
|
117
|
+
line: path.node.loc?.start.line || 0,
|
|
118
|
+
fields: [],
|
|
119
|
+
type: 'type',
|
|
120
|
+
});
|
|
121
|
+
},
|
|
122
|
+
// Detect Next.js API route handlers: export function GET/POST/etc.
|
|
123
|
+
ExportNamedDeclaration: (path) => {
|
|
124
|
+
const declaration = path.node.declaration;
|
|
125
|
+
if (t.isFunctionDeclaration(declaration) && declaration.id) {
|
|
126
|
+
const functionName = declaration.id.name;
|
|
127
|
+
const httpMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD'];
|
|
128
|
+
// Only detect if it's an HTTP method name and in an api directory
|
|
129
|
+
if (httpMethods.includes(functionName) && /\/api\//.test(filePath)) {
|
|
130
|
+
// Extract route path from file path
|
|
131
|
+
// Next.js API routes are in app/api/ or pages/api/
|
|
132
|
+
const routePath = this.extractNextJSRoutePath(filePath);
|
|
133
|
+
analysis.endpoints.push({
|
|
134
|
+
method: functionName,
|
|
135
|
+
path: routePath,
|
|
136
|
+
handler: functionName,
|
|
137
|
+
file: filePath,
|
|
138
|
+
line: declaration.loc?.start.line || 0,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
},
|
|
66
143
|
// Detect React components
|
|
67
144
|
FunctionDeclaration: (path) => {
|
|
68
145
|
const functionName = path.node.id?.name;
|
|
@@ -141,34 +218,42 @@ export class PatternDetector {
|
|
|
141
218
|
});
|
|
142
219
|
}
|
|
143
220
|
},
|
|
144
|
-
// Detect TypeScript interfaces
|
|
145
|
-
TSInterfaceDeclaration: (path) => {
|
|
146
|
-
const interfaceName = path.node.id.name;
|
|
147
|
-
const fields = path.node.body.body
|
|
148
|
-
.filter((member) => t.isTSPropertySignature(member) && t.isIdentifier(member.key))
|
|
149
|
-
.map((member) => member.key.name);
|
|
150
|
-
analysis.models.push({
|
|
151
|
-
name: interfaceName,
|
|
152
|
-
file: filePath,
|
|
153
|
-
line: path.node.loc?.start.line || 0,
|
|
154
|
-
fields,
|
|
155
|
-
type: 'interface',
|
|
156
|
-
});
|
|
157
|
-
},
|
|
158
|
-
// Detect type aliases
|
|
159
|
-
TSTypeAliasDeclaration: (path) => {
|
|
160
|
-
analysis.models.push({
|
|
161
|
-
name: path.node.id.name,
|
|
162
|
-
file: filePath,
|
|
163
|
-
line: path.node.loc?.start.line || 0,
|
|
164
|
-
fields: [],
|
|
165
|
-
type: 'type',
|
|
166
|
-
});
|
|
167
|
-
},
|
|
168
221
|
});
|
|
169
222
|
return analysis;
|
|
170
223
|
}
|
|
171
224
|
hasJSXReturn(body) {
|
|
172
225
|
return body.body.some((stmt) => t.isReturnStatement(stmt) && stmt.argument && t.isJSXElement(stmt.argument));
|
|
173
226
|
}
|
|
227
|
+
extractNextJSRoutePath(filePath) {
|
|
228
|
+
// Next.js API routes can be in:
|
|
229
|
+
// - app/api/[route]/route.ts (App Router)
|
|
230
|
+
// - pages/api/[route].ts (Pages Router)
|
|
231
|
+
// Try App Router pattern first
|
|
232
|
+
let match = filePath.match(/\/app\/api\/(.+)\/route\.[jt]sx?$/);
|
|
233
|
+
if (match) {
|
|
234
|
+
return `/api/${match[1]}`;
|
|
235
|
+
}
|
|
236
|
+
// Try Pages Router pattern
|
|
237
|
+
match = filePath.match(/\/pages\/api\/(.+)\.[jt]sx?$/);
|
|
238
|
+
if (match) {
|
|
239
|
+
return `/api/${match[1]}`;
|
|
240
|
+
}
|
|
241
|
+
// Fallback
|
|
242
|
+
return '/api/unknown';
|
|
243
|
+
}
|
|
244
|
+
extractNuxtRoutePath(filePath) {
|
|
245
|
+
// Nuxt.js server routes can be in:
|
|
246
|
+
// - server/api/[route].ts
|
|
247
|
+
// - server/routes/[route].ts
|
|
248
|
+
let match = filePath.match(/\/server\/api\/(.+)\.[jt]s$/);
|
|
249
|
+
if (match) {
|
|
250
|
+
return `/api/${match[1]}`;
|
|
251
|
+
}
|
|
252
|
+
match = filePath.match(/\/server\/routes\/(.+)\.[jt]s$/);
|
|
253
|
+
if (match) {
|
|
254
|
+
return `/${match[1]}`;
|
|
255
|
+
}
|
|
256
|
+
// Fallback
|
|
257
|
+
return '/api/unknown';
|
|
258
|
+
}
|
|
174
259
|
}
|
|
@@ -2,6 +2,7 @@ import type { ParsemeConfig } from '../config.js';
|
|
|
2
2
|
import type { ProjectInfo } from '../types.js';
|
|
3
3
|
export declare class ProjectAnalyzer {
|
|
4
4
|
private readonly config;
|
|
5
|
+
private readonly fileCollector;
|
|
5
6
|
constructor(config: ParsemeConfig);
|
|
6
7
|
analyze(rootDir: string): Promise<ProjectInfo>;
|
|
7
8
|
private detectProjectType;
|
|
@@ -11,4 +12,5 @@ export declare class ProjectAnalyzer {
|
|
|
11
12
|
private detectEntryPoints;
|
|
12
13
|
private detectOutputTargets;
|
|
13
14
|
private hasAppDependencies;
|
|
15
|
+
getAllProjectFiles(rootDir: string): Promise<string[]>;
|
|
14
16
|
}
|