Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ You may also automate this process by adding it to your Composer "post-update-cm

## Adding Custom AI Guidelines

To augment Laravel Boost with your own custom AI guidelines, add `.blade.php` files to your application's `.ai/guidelines/*` directory. These files will automatically be included with Laravel Boost's guidelines when you run `boost:install`.
To augment Laravel Boost with your own custom AI guidelines, add `.blade.php` or `.md` files to your application's `.ai/guidelines/*` directory. These files will automatically be included with Laravel Boost's guidelines when you run `boost:install`.

### Overriding Boost AI Guidelines

Expand Down
55 changes: 37 additions & 18 deletions src/Install/GuidelineComposer.php
Original file line number Diff line number Diff line change
Expand Up @@ -244,14 +244,38 @@ protected function guidelinesDir(string $dirPath, bool $thirdParty = false): arr
$finder = Finder::create()
->files()
->in($dirPath)
->name('*.blade.php');
->name('*.blade.php')
->name('*.md');
} catch (DirectoryNotFoundException) {
return [];
}

return array_map(fn (SplFileInfo $file): array => $this->guideline($file->getRealPath(), $thirdParty), iterator_to_array($finder));
}

protected function renderContent(string $content, string $path): string
{
$isBladeTemplate = str_ends_with($path, '.blade.php');

if (! $isBladeTemplate) {
return $content;
}

// Temporarily replace backticks and PHP opening tags with placeholders before Blade processing
// This prevents Blade from trying to execute PHP code examples and supports inline code
$placeholders = [
'`' => '___SINGLE_BACKTICK___',
'<?php' => '___OPEN_PHP_TAG___',
];

$content = str_replace(array_keys($placeholders), array_values($placeholders), $content);
$rendered = Blade::render($content, [
'assist' => $this->guidelineAssist,
]);

return str_replace(array_values($placeholders), array_keys($placeholders), $rendered);
}

/**
* @return array{content: string, name: string, description: string, path: ?string, custom: bool, third_party: bool}
*/
Expand All @@ -272,18 +296,8 @@ protected function guideline(string $path, bool $thirdParty = false): array
$content = file_get_contents($path);
$content = $this->processBoostSnippets($content);

// Temporarily replace backticks and PHP opening tags with placeholders before Blade processing
// This prevents Blade from trying to execute PHP code examples and supports inline code
$placeholders = [
'`' => '___SINGLE_BACKTICK___',
'<?php' => '___OPEN_PHP_TAG___',
];
$rendered = $this->renderContent($content, $path);

$content = str_replace(array_keys($placeholders), array_values($placeholders), $content);
$rendered = Blade::render($content, [
'assist' => $this->guidelineAssist,
]);
$rendered = str_replace(array_values($placeholders), array_keys($placeholders), $rendered);
$rendered = str_replace(array_keys($this->storedSnippets), array_values($this->storedSnippets), $rendered);

$this->storedSnippets = []; // Clear for next use
Expand All @@ -298,7 +312,7 @@ protected function guideline(string $path, bool $thirdParty = false): array

return [
'content' => trim($rendered),
'name' => str_replace('.blade.php', '', basename($path)),
'name' => str_replace(['.blade.php', '.md'], '', basename($path)),
'description' => $description,
'path' => $path,
'custom' => str_contains($path, $this->customGuidelinePath()),
Expand Down Expand Up @@ -326,16 +340,21 @@ protected function processBoostSnippets(string $content): string

protected function prependPackageGuidelinePath(string $path): string
{
$path = preg_replace('/\.blade\.php$/', '', $path);

return str_replace('/', DIRECTORY_SEPARATOR, __DIR__.'/../../.ai/'.$path.'.blade.php');
return $this->prependGuidelinePath($path, __DIR__.'/../../.ai/');
}

protected function prependUserGuidelinePath(string $path): string
{
$path = preg_replace('/\.blade\.php$/', '', $path);
return $this->prependGuidelinePath($path, $this->customGuidelinePath());
}

private function prependGuidelinePath(string $path, string $basePath): string
{
if (! str_ends_with($path, '.md') && ! str_ends_with($path, '.blade.php')) {
$path .= '.blade.php';
}

return str_replace('/', DIRECTORY_SEPARATOR, $this->customGuidelinePath($path.'.blade.php'));
return str_replace('/', DIRECTORY_SEPARATOR, $basePath.$path);
}

protected function guidelinePath(string $path): ?string
Expand Down
47 changes: 45 additions & 2 deletions tests/Feature/Install/GuidelineComposerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Laravel\Roster\Package;
use Laravel\Roster\PackageCollection;
use Laravel\Roster\Roster;
use function Pest\testDirectory;

beforeEach(function (): void {
$this->roster = Mockery::mock(Roster::class);
Expand Down Expand Up @@ -268,7 +269,7 @@
$composer = Mockery::mock(GuidelineComposer::class, [$this->roster, $this->herd])->makePartial();
$composer
->shouldReceive('customGuidelinePath')
->andReturnUsing(fn ($path = ''): string => realpath(\Pest\testDirectory('fixtures/.ai/guidelines')).'/'.ltrim((string) $path, '/'));
->andReturnUsing(fn ($path = ''): string => realpath(testDirectory('fixtures/.ai/guidelines')).'/'.ltrim((string) $path, '/'));

expect($composer->compose())
->toContain('=== .ai/custom-rule rules ===')
Expand All @@ -291,7 +292,7 @@
$composer = Mockery::mock(GuidelineComposer::class, [$this->roster, $this->herd])->makePartial();
$composer
->shouldReceive('customGuidelinePath')
->andReturnUsing(fn ($path = ''): string => realpath(\Pest\testDirectory('fixtures/.ai/guidelines')).'/'.ltrim((string) $path, '/'));
->andReturnUsing(fn ($path = ''): string => realpath(testDirectory('fixtures/.ai/guidelines')).'/'.ltrim((string) $path, '/'));

$guidelines = $composer->compose();
$overrideStringCount = substr_count((string) $guidelines, 'Thanks though, appreciate you');
Expand Down Expand Up @@ -382,3 +383,45 @@
'yarn' => [NodePackageManager::YARN, 'yarn'],
'bun' => [NodePackageManager::BUN, 'bun'],
]);

test('renderContent handles blade and markdown files correctly', function (): void {
$packages = new PackageCollection([
new Package(Packages::LARAVEL, 'laravel/framework', '11.0.0'),
]);

$this->roster->shouldReceive('packages')->andReturn($packages);
$this->nodePackageManager = NodePackageManager::NPM;

$composer = Mockery::mock(GuidelineComposer::class, [$this->roster, $this->herd])->makePartial();
$composer
->shouldReceive('customGuidelinePath')
->andReturnUsing(fn ($path = ''): string => realpath(testDirectory('fixtures/.ai/guidelines')).'/'.ltrim((string) $path, '/'));

$guidelines = $composer->compose();

expect($guidelines)
// Preserves backticks in blade templates
->toContain('=== .ai/test-blade-with-backticks rules ===')
->not->toContain('=== .ai/test-blade-with-backticks.md rules ===')
->toContain('`artisan make:model`')
->toContain('`php artisan migrate`')
->toContain('`Model::query()`')
->toContain('`route(\'home\')`')
->toContain('`config(\'app.name\')`')
// Preserves PHP tags in blade templates
->toContain('=== .ai/test-blade-with-php-tags rules ===')
->not->toContain('=== .ai/test-blade-with-backticks.blade.php rules ===')
->toContain('<?php')
->toContain('namespace App\Models;')
->toContain('class User extends Model')
// Does not process markdown files with blade
->toContain('=== .ai/test-markdown rules ===')
->toContain('# Markdown File Test')
->toContain('This is a plain markdown file')
->toContain('Use `code` in backticks')
->toContain('echo "Hello World";')
// Processes blade variables correctly
->toContain('=== .ai/test-blade-with-assist rules ===')
->toContain('Run `npm install` to install dependencies')
->toContain('Package manager: npm');
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Blade with @assist variable

Run `{{ $assist->nodePackageManager() }} install` to install dependencies.

Package manager: {{ $assist->nodePackageManager() }}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Blade Template with Backticks

Use `artisan make:model` to create models.
Run `php artisan migrate` for migrations.

Code example with backticks:
- `Model::query()`
- `route('home')`
- `config('app.name')`
14 changes: 14 additions & 0 deletions tests/fixtures/.ai/guidelines/test-blade-with-php-tags.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Blade Template with PHP Tags

Example PHP code:

<?php

namespace App\Models;

class User extends Model
{
// Model code
}

Use this pattern in your code.
13 changes: 13 additions & 0 deletions tests/fixtures/.ai/guidelines/test-markdown.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Markdown File Test

This is a plain markdown file.

Use `code` in backticks.

Example code:
```php
<?php
echo "Hello World";
```

This should not be processed by Blade.