Skip to content

Commit 264c6a6

Browse files
committed
feat: Add intelligent project template system supporting PHP and JavaScript frameworks with embedded detection logic
1 parent fa6a8f5 commit 264c6a6

18 files changed

+3001
-166
lines changed

src/Template/Analysis/Analyzer/ComposerAnalyzer.php

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ public function __construct(
2121
private ProjectStructureDetector $structureDetector,
2222
) {}
2323

24-
#[\Override]
2524
public function analyze(FSPath $projectRoot): ?AnalysisResult
2625
{
2726
if (!$this->canAnalyze($projectRoot)) {
@@ -62,19 +61,16 @@ public function analyze(FSPath $projectRoot): ?AnalysisResult
6261
);
6362
}
6463

65-
#[\Override]
6664
public function canAnalyze(FSPath $projectRoot): bool
6765
{
6866
return $projectRoot->join('composer.json')->exists();
6967
}
7068

71-
#[\Override]
7269
public function getPriority(): int
7370
{
7471
return 50; // Medium priority - let specific framework analyzers go first
7572
}
7673

77-
#[\Override]
7874
public function getName(): string
7975
{
8076
return 'composer';

src/Template/Analysis/Analyzer/FallbackAnalyzer.php

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ public function __construct(
1919
private ProjectStructureDetector $structureDetector,
2020
) {}
2121

22-
#[\Override]
2322
public function analyze(FSPath $projectRoot): ?AnalysisResult
2423
{
2524
// This analyzer always provides a result as a fallback
@@ -42,20 +41,17 @@ public function analyze(FSPath $projectRoot): ?AnalysisResult
4241
);
4342
}
4443

45-
#[\Override]
4644
public function canAnalyze(FSPath $projectRoot): bool
4745
{
4846
// This analyzer can always analyze any project as a fallback
4947
return true;
5048
}
5149

52-
#[\Override]
5350
public function getPriority(): int
5451
{
5552
return 1; // Lowest priority - only used when no other analyzers match
5653
}
5754

58-
#[\Override]
5955
public function getName(): string
6056
{
6157
return 'fallback';

src/Template/Analysis/Analyzer/LaravelAnalyzer.php

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ public function __construct(
3636
private ProjectStructureDetector $structureDetector,
3737
) {}
3838

39-
#[\Override]
4039
public function analyze(FSPath $projectRoot): ?AnalysisResult
4140
{
4241
if (!$this->canAnalyze($projectRoot)) {
@@ -80,7 +79,6 @@ public function analyze(FSPath $projectRoot): ?AnalysisResult
8079
);
8180
}
8281

83-
#[\Override]
8482
public function canAnalyze(FSPath $projectRoot): bool
8583
{
8684
// Must have composer.json to be a Laravel project
@@ -93,13 +91,11 @@ public function canAnalyze(FSPath $projectRoot): bool
9391
return $composer !== null && $this->composerReader->hasPackage($composer, 'laravel/framework');
9492
}
9593

96-
#[\Override]
9794
public function getPriority(): int
9895
{
9996
return 100; // High priority - specific framework detection should run first
10097
}
10198

102-
#[\Override]
10399
public function getName(): string
104100
{
105101
return 'laravel';
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Butschster\ContextGenerator\Template\Analysis\Analyzer;
6+
7+
use Butschster\ContextGenerator\Application\FSPath;
8+
use Butschster\ContextGenerator\Template\Analysis\AnalysisResult;
9+
use Butschster\ContextGenerator\Template\Analysis\ProjectAnalyzerInterface;
10+
use Butschster\ContextGenerator\Template\Analysis\Util\ProjectStructureDetector;
11+
use Spiral\Files\FilesInterface;
12+
13+
/**
14+
* Analyzes JavaScript/Node.js projects using package.json
15+
*/
16+
final readonly class PackageJsonAnalyzer implements ProjectAnalyzerInterface
17+
{
18+
/**
19+
* Framework detection patterns in package.json dependencies
20+
*/
21+
private const array FRAMEWORK_PATTERNS = [
22+
'react' => ['react', 'react-dom'],
23+
'vue' => ['vue'],
24+
'next' => ['next'],
25+
'nuxt' => ['nuxt', '@nuxt/kit'],
26+
'express' => ['express'],
27+
'angular' => ['@angular/core'],
28+
'svelte' => ['svelte'],
29+
'gatsby' => ['gatsby'],
30+
];
31+
32+
public function __construct(
33+
private FilesInterface $files,
34+
private ProjectStructureDetector $structureDetector,
35+
) {}
36+
37+
public function analyze(FSPath $projectRoot): ?AnalysisResult
38+
{
39+
if (!$this->canAnalyze($projectRoot)) {
40+
return null;
41+
}
42+
43+
$packageJson = $this->readPackageJson($projectRoot);
44+
45+
if ($packageJson === null) {
46+
return null;
47+
}
48+
49+
$detectedFramework = $this->detectFramework($packageJson);
50+
$existingDirs = $this->structureDetector->detectExistingDirectories($projectRoot);
51+
52+
if ($detectedFramework === null) {
53+
// Generic Node.js project
54+
return new AnalysisResult(
55+
analyzerName: $this->getName(),
56+
detectedType: 'node',
57+
confidence: 0.6,
58+
suggestedTemplates: ['node'],
59+
metadata: [
60+
'packageJson' => $packageJson,
61+
'existingDirectories' => $existingDirs,
62+
'packageName' => $packageJson['name'] ?? null,
63+
'scripts' => $packageJson['scripts'] ?? [],
64+
],
65+
);
66+
}
67+
68+
$confidence = $this->calculateFrameworkConfidence($packageJson, $detectedFramework);
69+
70+
return new AnalysisResult(
71+
analyzerName: $this->getName(),
72+
detectedType: $detectedFramework,
73+
confidence: $confidence,
74+
suggestedTemplates: [$detectedFramework],
75+
metadata: [
76+
'packageJson' => $packageJson,
77+
'detectedFramework' => $detectedFramework,
78+
'existingDirectories' => $existingDirs,
79+
'packageName' => $packageJson['name'] ?? null,
80+
'scripts' => $packageJson['scripts'] ?? [],
81+
'dependencies' => $this->getAllDependencies($packageJson),
82+
],
83+
);
84+
}
85+
86+
public function canAnalyze(FSPath $projectRoot): bool
87+
{
88+
return $projectRoot->join('package.json')->exists();
89+
}
90+
91+
public function getPriority(): int
92+
{
93+
return 80; // High priority for JavaScript framework detection
94+
}
95+
96+
public function getName(): string
97+
{
98+
return 'package-json';
99+
}
100+
101+
/**
102+
* Read and parse package.json file
103+
*/
104+
private function readPackageJson(FSPath $projectRoot): ?array
105+
{
106+
$packagePath = $projectRoot->join('package.json');
107+
108+
if (!$packagePath->exists()) {
109+
return null;
110+
}
111+
112+
$content = $this->files->read($packagePath->toString());
113+
114+
if ($content === '') {
115+
return null;
116+
}
117+
118+
$decoded = \json_decode($content, true);
119+
120+
if (!\is_array($decoded)) {
121+
return null;
122+
}
123+
124+
return $decoded;
125+
}
126+
127+
/**
128+
* Detect the framework based on package.json dependencies
129+
*/
130+
private function detectFramework(array $packageJson): ?string
131+
{
132+
$allDependencies = $this->getAllDependencies($packageJson);
133+
134+
// Check for framework-specific packages
135+
foreach (self::FRAMEWORK_PATTERNS as $framework => $patterns) {
136+
foreach ($patterns as $pattern) {
137+
if (\array_key_exists($pattern, $allDependencies)) {
138+
return $framework;
139+
}
140+
}
141+
}
142+
143+
return null;
144+
}
145+
146+
/**
147+
* Calculate confidence score for detected framework
148+
*/
149+
private function calculateFrameworkConfidence(array $packageJson, string $framework): float
150+
{
151+
$confidence = 0.7; // Base confidence for framework detection
152+
153+
// Boost confidence if multiple framework packages are present
154+
$frameworkPatterns = self::FRAMEWORK_PATTERNS[$framework] ?? [];
155+
$allDependencies = $this->getAllDependencies($packageJson);
156+
157+
$matchCount = 0;
158+
foreach ($frameworkPatterns as $pattern) {
159+
if (\array_key_exists($pattern, $allDependencies)) {
160+
$matchCount++;
161+
}
162+
}
163+
164+
if ($matchCount > 1) {
165+
$confidence += 0.2;
166+
}
167+
168+
// Boost confidence if there are relevant scripts
169+
$scripts = $packageJson['scripts'] ?? [];
170+
if ($this->hasRelevantScripts($scripts, $framework)) {
171+
$confidence += 0.1;
172+
}
173+
174+
return \min($confidence, 1.0);
175+
}
176+
177+
/**
178+
* Check if scripts are relevant to the detected framework
179+
*/
180+
private function hasRelevantScripts(array $scripts, string $framework): bool
181+
{
182+
$relevantScripts = match ($framework) {
183+
'react' => ['start', 'build', 'test'],
184+
'vue' => ['serve', 'build', 'test'],
185+
'next' => ['dev', 'build', 'start'],
186+
'nuxt' => ['dev', 'build', 'generate'],
187+
'express' => ['start', 'dev'],
188+
'angular' => ['ng', 'start', 'build'],
189+
default => ['start', 'build'],
190+
};
191+
192+
foreach ($relevantScripts as $script) {
193+
if (isset($scripts[$script])) {
194+
return true;
195+
}
196+
}
197+
198+
return false;
199+
}
200+
201+
/**
202+
* Get all dependencies from package.json
203+
*/
204+
private function getAllDependencies(array $packageJson): array
205+
{
206+
$dependencies = [];
207+
208+
if (isset($packageJson['dependencies'])) {
209+
$dependencies = \array_merge($dependencies, $packageJson['dependencies']);
210+
}
211+
212+
if (isset($packageJson['devDependencies'])) {
213+
$dependencies = \array_merge($dependencies, $packageJson['devDependencies']);
214+
}
215+
216+
if (isset($packageJson['peerDependencies'])) {
217+
$dependencies = \array_merge($dependencies, $packageJson['peerDependencies']);
218+
}
219+
220+
return $dependencies;
221+
}
222+
}

0 commit comments

Comments
 (0)