Skip to content

Commit b4c5bed

Browse files
authored
Support loading guidelines and skills from vendor packages (#566)
* Use vendor package guidelines * wip * Add node_modules skills detection * De-duplicate vendor core for alias packages and fix .md override resolution * Refactor * Refactor * Update Inertia Guideline * fix wayfinder * Use implode for path joining consistency * Add laravel echo and other first party packages * Fix code styling * Update Npm.php
1 parent 12a0b57 commit b4c5bed

File tree

17 files changed

+710
-31
lines changed

17 files changed

+710
-31
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,16 @@
1+
# Inertia
2+
3+
- Inertia creates fully client-side rendered SPAs without modern SPA complexity, leveraging existing server-side patterns.
4+
- Components live in `{{ $assist->inertia()->pagesDirectory() }}` (unless specified in `vite.config.js`). Use `Inertia::render()` for server-side routing instead of Blade views.
5+
- ALWAYS use `search-docs` tool for version-specific Inertia documentation and updated code examples.
6+
@if($assist->hasPackage(\Laravel\Roster\Enums\Packages::INERTIA_REACT))
7+
- IMPORTANT: Activate `inertia-react-development` when working with Inertia client-side patterns.
8+
@elseif($assist->hasPackage(\Laravel\Roster\Enums\Packages::INERTIA_VUE))
9+
- IMPORTANT: Activate `inertia-vue-development` when working with Inertia Vue client-side patterns.
10+
@elseif($assist->hasPackage(\Laravel\Roster\Enums\Packages::INERTIA_SVELTE))
11+
- IMPORTANT: Activate `inertia-svelte-development` when working with Inertia Svelte client-side patterns.
12+
@endif
13+
114
# Inertia v1
215

316
- Inertia v1 does not support the following v2 features: deferred props, infinite scrolling (merging props + `WhenVisible`), lazy loading on scroll, polling, or prefetching. Do not use these.

.ai/inertia-laravel/2/core.blade.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,16 @@
1+
# Inertia
2+
3+
- Inertia creates fully client-side rendered SPAs without modern SPA complexity, leveraging existing server-side patterns.
4+
- Components live in `{{ $assist->inertia()->pagesDirectory() }}` (unless specified in `vite.config.js`). Use `Inertia::render()` for server-side routing instead of Blade views.
5+
- ALWAYS use `search-docs` tool for version-specific Inertia documentation and updated code examples.
6+
@if($assist->hasPackage(\Laravel\Roster\Enums\Packages::INERTIA_REACT))
7+
- IMPORTANT: Activate `inertia-react-development` when working with Inertia client-side patterns.
8+
@elseif($assist->hasPackage(\Laravel\Roster\Enums\Packages::INERTIA_VUE))
9+
- IMPORTANT: Activate `inertia-vue-development` when working with Inertia Vue client-side patterns.
10+
@elseif($assist->hasPackage(\Laravel\Roster\Enums\Packages::INERTIA_SVELTE))
11+
- IMPORTANT: Activate `inertia-svelte-development` when working with Inertia Svelte client-side patterns.
12+
@endif
13+
114
# Inertia v2
215

316
- Use all Inertia features from v1 and v2. Check the documentation before making changes to ensure the correct approach.

.ai/inertia-laravel/core.blade.php

Lines changed: 0 additions & 12 deletions
This file was deleted.

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
"illuminate/support": "^11.45.3|^12.41.1",
2222
"laravel/mcp": "^0.5.1",
2323
"laravel/prompts": "^0.3.10",
24-
"laravel/roster": "^0.4.0"
24+
"laravel/roster": "^0.5.0"
2525
},
2626
"require-dev": {
2727
"laravel/pint": "^1.27.0",

src/Install/Concerns/DiscoverPackagePaths.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
namespace Laravel\Boost\Install\Concerns;
66

77
use Illuminate\Support\Collection;
8+
use Laravel\Boost\Support\Composer;
9+
use Laravel\Boost\Support\Npm;
810
use Laravel\Roster\Enums\Packages;
911
use Laravel\Roster\Package;
1012
use Laravel\Roster\Roster;
@@ -96,4 +98,15 @@ protected function getBoostAiPath(): string
9698
{
9799
return __DIR__.'/../../../.ai';
98100
}
101+
102+
protected function resolveFirstPartyBoostPath(Package $package, string $subpath): ?string
103+
{
104+
if (! Composer::isFirstPartyPackage($package->rawName()) && ! Npm::isFirstPartyPackage($package->rawName())) {
105+
return null;
106+
}
107+
108+
$path = implode(DIRECTORY_SEPARATOR, [$package->path(), 'resources', 'boost', $subpath]);
109+
110+
return is_dir($path) ? $path : null;
111+
}
99112
}

src/Install/GuidelineComposer.php

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
use Laravel\Boost\Install\Concerns\DiscoverPackagePaths;
1111
use Laravel\Boost\Support\Composer;
1212
use Laravel\Roster\Package;
13-
use Laravel\Roster\PackageCollection;
1413
use Laravel\Roster\Roster;
1514
use Symfony\Component\Finder\Exception\DirectoryNotFoundException;
1615
use Symfony\Component\Finder\Finder;
@@ -169,13 +168,23 @@ protected function getConditionalGuidelines(): Collection
169168
->mapWithKeys(fn ($config, $key): array => [$key => $this->guideline($config['path'])]);
170169
}
171170

172-
protected function getPackageGuidelines(): PackageCollection
171+
protected function getPackageGuidelines(): Collection
173172
{
174173
return $this->roster->packages()
175174
->reject(fn (Package $package): bool => $this->shouldExcludePackage($package))
176-
->flatMap(function ($package): Collection {
175+
->flatMap(function (Package $package): Collection {
177176
$guidelineDir = $this->normalizePackageName($package->name());
178-
$guidelines = collect([$guidelineDir.'/core' => $this->guideline($guidelineDir.'/core')]);
177+
178+
$vendorPath = $this->resolveFirstPartyBoostPath($package, 'guidelines');
179+
180+
$vendorCorePath = $vendorPath !== null
181+
? implode(DIRECTORY_SEPARATOR, [$vendorPath, 'core'])
182+
: null;
183+
184+
$guidelines = collect([
185+
$guidelineDir.'/core' => $this->resolveGuideline($vendorCorePath, $guidelineDir.'/core'),
186+
]);
187+
179188
$packageGuidelines = $this->guidelinesDir($guidelineDir.'/'.$package->majorVersion());
180189

181190
foreach ($packageGuidelines as $guideline) {
@@ -191,6 +200,22 @@ protected function getPackageGuidelines(): PackageCollection
191200
});
192201
}
193202

203+
/**
204+
* @return array{content: string, name: string, description: string, path: ?string, custom: bool, third_party: bool}
205+
*/
206+
private function resolveGuideline(?string $vendorPath, string $guidelineKey): array
207+
{
208+
if ($vendorPath !== null) {
209+
foreach (['.blade.php', '.md'] as $ext) {
210+
if (file_exists($vendorPath.$ext)) {
211+
return $this->guideline($vendorPath.$ext, false, $guidelineKey);
212+
}
213+
}
214+
}
215+
216+
return $this->guideline($guidelineKey);
217+
}
218+
194219
/**
195220
* @return Collection<string, array>
196221
*/
@@ -199,6 +224,10 @@ protected function getThirdPartyGuidelines(): Collection
199224
$guidelines = collect();
200225

201226
foreach (Composer::packagesDirectoriesWithBoostGuidelines() as $package => $path) {
227+
if (Composer::isFirstPartyPackage($package)) {
228+
continue;
229+
}
230+
202231
foreach ($this->guidelinesDir($path, true) as $guideline) {
203232
$guidelines->put($package, $guideline);
204233
}
@@ -242,9 +271,9 @@ protected function guidelinesDir(string $dirPath, bool $thirdParty = false): arr
242271
/**
243272
* @return array{content: string, name: string, description: string, path: ?string, custom: bool, third_party: bool}
244273
*/
245-
protected function guideline(string $path, bool $thirdParty = false): array
274+
protected function guideline(string $path, bool $thirdParty = false, ?string $overrideKey = null): array
246275
{
247-
$path = $this->guidelinePath($path);
276+
$path = $this->guidelinePath($path, $overrideKey);
248277

249278
if ($path === null) {
250279
return [
@@ -304,7 +333,7 @@ private function prependGuidelinePath(string $path, string $basePath): string
304333
return str_replace('/', DIRECTORY_SEPARATOR, $basePath.$path);
305334
}
306335

307-
protected function guidelinePath(string $path): ?string
336+
protected function guidelinePath(string $path, ?string $overrideKey = null): ?string
308337
{
309338
// Relative path, prepend our package path to it
310339
if (! file_exists($path)) {
@@ -322,6 +351,18 @@ protected function guidelinePath(string $path): ?string
322351
return $path;
323352
}
324353

354+
if ($overrideKey !== null) {
355+
foreach (['.blade.php', '.md'] as $ext) {
356+
$customPath = $this->prependUserGuidelinePath($overrideKey.$ext);
357+
358+
if (file_exists($customPath)) {
359+
return $customPath;
360+
}
361+
}
362+
363+
return $path;
364+
}
365+
325366
// The path is not a custom guideline, check if the user has an override for this
326367
$basePath = realpath(__DIR__.'/../../');
327368
$relativePath = Str::of($path)

src/Install/SkillComposer.php

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use Laravel\Boost\Concerns\RendersBladeGuidelines;
1010
use Laravel\Boost\Install\Concerns\DiscoverPackagePaths;
1111
use Laravel\Boost\Support\Composer;
12+
use Laravel\Roster\Package;
1213
use Laravel\Roster\Roster;
1314
use Symfony\Component\Yaml\Yaml;
1415

@@ -63,12 +64,28 @@ public function skills(): Collection
6364
*/
6465
protected function getBoostSkills(): Collection
6566
{
66-
return $this->discoverPackagePaths($this->getBoostAiPath())
67-
->flatMap(fn (array $package): Collection => $this->discoverSkillsFromPath(
68-
$package['path'],
69-
$package['name'],
70-
$package['version']
71-
));
67+
/** @var Collection<string, Skill> $skills */
68+
$skills = $this->getRoster()->packages()
69+
->reject(fn (Package $package): bool => $this->shouldExcludePackage($package))
70+
->collect()
71+
->flatMap(function (Package $package): Collection {
72+
$name = $this->normalizePackageName($package->name());
73+
74+
$vendorSkillPath = $this->resolveFirstPartyBoostPath($package, 'skills');
75+
76+
$vendorSkills = $vendorSkillPath !== null
77+
? $this->discoverSkillsFromDirectory($vendorSkillPath, $name)
78+
: collect();
79+
80+
$aiPath = $this->getBoostAiPath().DIRECTORY_SEPARATOR.$name;
81+
$aiSkills = is_dir($aiPath)
82+
? $this->discoverSkillsFromPath($aiPath, $name, $package->majorVersion())
83+
: collect();
84+
85+
return $aiSkills->merge($vendorSkills);
86+
});
87+
88+
return $skills;
7289
}
7390

7491
/**
@@ -77,6 +94,7 @@ protected function getBoostSkills(): Collection
7794
protected function getThirdPartySkills(): Collection
7895
{
7996
$skills = collect(Composer::packagesDirectoriesWithBoostSkills())
97+
->reject(fn (string $path, string $package): bool => Composer::isFirstPartyPackage($package))
8098
->flatMap(fn (string $path, string $package): Collection => $this->discoverSkillsFromDirectory($path, $package));
8199

82100
$selectedPackages = $this->config->aiGuidelines ?? [];

src/Install/ThirdPartyPackage.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ public static function discover(): Collection
3333
));
3434

3535
return collect($allPackageNames)
36+
->reject(fn (string $name): bool => Composer::isFirstPartyPackage($name))
3637
->mapWithKeys(fn (string $name): array => [
3738
$name => new self(
3839
name: $name,

src/Support/Composer.php

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,29 @@
66

77
class Composer
88
{
9+
/** @var array<int, string> */
10+
public const FIRST_PARTY_PACKAGES = [
11+
'laravel/framework',
12+
'laravel/folio',
13+
'laravel/mcp',
14+
'laravel/pennant',
15+
'laravel/pint',
16+
'laravel/sail',
17+
'laravel/wayfinder',
18+
'livewire/livewire',
19+
'livewire/flux',
20+
'livewire/flux-pro',
21+
'livewire/volt',
22+
'inertiajs/inertia-laravel',
23+
'pestphp/pest',
24+
'phpunit/phpunit',
25+
];
26+
27+
public static function isFirstPartyPackage(string $composerName): bool
28+
{
29+
return in_array($composerName, self::FIRST_PARTY_PACKAGES, true);
30+
}
31+
932
public static function packagesDirectories(): array
1033
{
1134
return collect(static::packages())
@@ -48,9 +71,9 @@ public static function packagesDirectoriesWithBoostSkills(): array
4871
}
4972

5073
/**
51-
* @param string|null $subpath Optional subpath under resources/boost/ (e.g., 'guidelines')
74+
* @return array<string, string>
5275
*/
53-
private static function packagesDirectoriesWithBoostSubpath(?string $subpath = null): array
76+
private static function packagesDirectoriesWithBoostSubpath(string $subpath): array
5477
{
5578
return collect(self::packagesDirectories())
5679
->map(fn (string $path): string => implode(DIRECTORY_SEPARATOR, array_filter([

src/Support/Npm.php

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Laravel\Boost\Support;
6+
7+
class Npm
8+
{
9+
/** @var array<int, string> */
10+
public const FIRST_PARTY_SCOPES = [
11+
'@inertiajs',
12+
'@laravel',
13+
];
14+
15+
/** @var array<int, string> */
16+
public const FIRST_PARTY_PACKAGES = [
17+
'laravel-echo',
18+
'laravel-precognition',
19+
'laravel-vite-plugin',
20+
];
21+
22+
public static function isFirstPartyPackage(string $npmName): bool
23+
{
24+
if (collect(self::FIRST_PARTY_SCOPES)->contains(fn (string $scope): bool => str_starts_with($npmName, $scope.'/'))) {
25+
return true;
26+
}
27+
28+
return in_array($npmName, self::FIRST_PARTY_PACKAGES, true);
29+
}
30+
31+
/**
32+
* @return array<string, string>
33+
*/
34+
public static function packagesDirectories(): array
35+
{
36+
return collect(static::packages())
37+
->mapWithKeys(fn (string $key, string $package): array => [$package => implode(DIRECTORY_SEPARATOR, [
38+
base_path('node_modules'),
39+
str_replace('/', DIRECTORY_SEPARATOR, $package),
40+
])])
41+
->filter(fn (string $path): bool => is_dir($path))
42+
->toArray();
43+
}
44+
45+
/**
46+
* @return array<string, string>
47+
*/
48+
public static function packages(): array
49+
{
50+
$packageJsonPath = base_path('package.json');
51+
52+
if (! file_exists($packageJsonPath)) {
53+
return [];
54+
}
55+
56+
$packageData = json_decode(file_get_contents($packageJsonPath), true);
57+
58+
if (json_last_error() !== JSON_ERROR_NONE) {
59+
return [];
60+
}
61+
62+
return collect($packageData['dependencies'] ?? [])
63+
->merge($packageData['devDependencies'] ?? [])
64+
->mapWithKeys(fn (string $key, string $package): array => [$package => $key])
65+
->toArray();
66+
}
67+
68+
/**
69+
* @return array<string, string>
70+
*/
71+
public static function packagesDirectoriesWithBoostGuidelines(): array
72+
{
73+
return self::packagesDirectoriesWithBoostSubpath('guidelines');
74+
}
75+
76+
/**
77+
* @return array<string, string>
78+
*/
79+
public static function packagesDirectoriesWithBoostSkills(): array
80+
{
81+
return self::packagesDirectoriesWithBoostSubpath('skills');
82+
}
83+
84+
/**
85+
* @return array<string, string>
86+
*/
87+
private static function packagesDirectoriesWithBoostSubpath(string $subpath): array
88+
{
89+
return collect(self::packagesDirectories())
90+
->map(fn (string $path): string => implode(DIRECTORY_SEPARATOR, array_filter([
91+
$path,
92+
'resources',
93+
'boost',
94+
$subpath,
95+
])))
96+
->filter(fn (string $path): bool => is_dir($path))
97+
->toArray();
98+
}
99+
}

0 commit comments

Comments
 (0)