Skip to content

Commit 98fbf57

Browse files
authored
Merge pull request #2 from HichemTab-tech/add-support-for-pnpm-and-yarn
Add support for pnpm and yarn
2 parents 3bf918f + ee45d19 commit 98fbf57

File tree

11 files changed

+2122
-93
lines changed

11 files changed

+2122
-93
lines changed

src/Roster.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ public static function scan(?string $basePath = null): self
138138
->scan()
139139
->each(fn ($item) => $roster->add($item));
140140

141-
(new PackageLock($basePath.'package-lock.json'))
141+
(new PackageLock($basePath))
142142
->scan()
143143
->each(fn ($item) => $roster->add($item));
144144

src/Scanners/BasePackageScanner.php

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
<?php
2+
3+
namespace Laravel\Roster\Scanners;
4+
5+
use Illuminate\Support\Collection;
6+
use Illuminate\Support\Facades\Log;
7+
use Laravel\Roster\Approach;
8+
use Laravel\Roster\Enums\Approaches;
9+
use Laravel\Roster\Enums\Packages;
10+
use Laravel\Roster\Package;
11+
12+
abstract class BasePackageScanner
13+
{
14+
/**
15+
* Map of package names to enums
16+
*
17+
* @var array<string, Packages|Approaches|array<int, Packages|Approaches>>
18+
*/
19+
protected array $map = [
20+
'alpinejs' => Packages::ALPINEJS,
21+
'@inertiajs/react' => [Packages::INERTIA, Packages::INERTIA_REACT],
22+
'@inertiajs/svelte' => [Packages::INERTIA, Packages::INERTIA_SVELTE],
23+
'@inertiajs/vue3' => [Packages::INERTIA, Packages::INERTIA_VUE],
24+
'laravel-echo' => Packages::ECHO,
25+
'@laravel/vite-plugin-wayfinder' => [Packages::WAYFINDER, Packages::WAYFINDER_VITE],
26+
'react' => Packages::REACT,
27+
'tailwindcss' => [Packages::TAILWINDCSS],
28+
'vue' => Packages::VUE,
29+
];
30+
31+
public function __construct(protected string $path) {}
32+
33+
/**
34+
* @return \Illuminate\Support\Collection<int, \Laravel\Roster\Package|\Laravel\Roster\Approach>
35+
*/
36+
abstract public function scan(): Collection;
37+
38+
/**
39+
* Check if the scanner can handle the given path
40+
*/
41+
abstract public function canScan(): bool;
42+
43+
/**
44+
* Process dependencies and add them to the mapped items collection
45+
*
46+
* @param array<string, string> $dependencies
47+
* @param Collection<int, Package|Approach> $mappedItems
48+
*/
49+
protected function processDependencies(array $dependencies, Collection $mappedItems, bool $isDev): void
50+
{
51+
foreach ($dependencies as $packageName => $version) {
52+
$mappedPackage = $this->map[$packageName] ?? null;
53+
if (is_null($mappedPackage)) {
54+
continue;
55+
}
56+
57+
if (! is_array($mappedPackage)) {
58+
$mappedPackage = [$mappedPackage];
59+
}
60+
61+
foreach ($mappedPackage as $mapped) {
62+
$niceVersion = preg_replace('/[^0-9.]/', '', $version) ?? '';
63+
$mappedItems->push(match (get_class($mapped)) {
64+
Packages::class => new Package($mapped, $niceVersion, $isDev),
65+
Approaches::class => new Approach($mapped),
66+
default => throw new \InvalidArgumentException('Unsupported mapping')
67+
});
68+
}
69+
}
70+
}
71+
72+
/**
73+
* Common file validation logic
74+
*/
75+
protected function validateFile(string $path, string $type = 'Package'): ?string
76+
{
77+
if (! file_exists($path)) {
78+
Log::warning("Failed to scan $type: $path");
79+
80+
return null;
81+
}
82+
83+
if (! is_readable($path)) {
84+
Log::warning("File not readable: $path");
85+
86+
return null;
87+
}
88+
89+
$contents = file_get_contents($path);
90+
if ($contents === false) {
91+
Log::warning("Failed to read $type: $path");
92+
93+
return null;
94+
}
95+
96+
return $contents;
97+
}
98+
}

src/Scanners/BunPackageLock.php

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php
2+
3+
namespace Laravel\Roster\Scanners;
4+
5+
use Illuminate\Support\Collection;
6+
use Illuminate\Support\Facades\Log;
7+
8+
class BunPackageLock extends BasePackageScanner
9+
{
10+
/**
11+
* @return \Illuminate\Support\Collection<int, \Laravel\Roster\Package|\Laravel\Roster\Approach>
12+
*/
13+
public function scan(): Collection
14+
{
15+
$mappedItems = collect();
16+
$lockFilePath = $this->path.'bun.lock';
17+
18+
$contents = $this->validateFile($lockFilePath);
19+
if ($contents === null) {
20+
return $mappedItems;
21+
}
22+
23+
// Remove trailing commas before decoding
24+
/** @var string $contents */
25+
$contents = preg_replace('/,\s*([]}])/m', '$1', $contents);
26+
$json = json_decode($contents, true);
27+
if (json_last_error() !== JSON_ERROR_NONE || ! is_array($json)) {
28+
Log::warning('Failed to decode Package: '.$lockFilePath.'. '.json_last_error_msg());
29+
30+
return $mappedItems;
31+
}
32+
33+
/** @var array<string, array<string, mixed>> $json */
34+
if (! isset($json['workspaces']['']) || ! isset($json['packages'])) {
35+
Log::warning('Malformed bun.lock');
36+
37+
return $mappedItems;
38+
}
39+
40+
/** @var array<string, mixed> $workspace */
41+
$workspace = $json['workspaces'][''];
42+
43+
/** @var array<string, string> $dependencies */
44+
$dependencies = $workspace['dependencies'] ?? [];
45+
/** @var array<string, string> $devDependencies */
46+
$devDependencies = $workspace['devDependencies'] ?? [];
47+
48+
$this->processDependencies($dependencies, $mappedItems, false);
49+
$this->processDependencies($devDependencies, $mappedItems, true);
50+
51+
return $mappedItems;
52+
}
53+
54+
/**
55+
* Check if the scanner can handle the given path
56+
*/
57+
public function canScan(): bool
58+
{
59+
return file_exists($this->path.'bun.lock');
60+
}
61+
}

src/Scanners/NpmPackageLock.php

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
namespace Laravel\Roster\Scanners;
4+
5+
use Illuminate\Support\Collection;
6+
use Illuminate\Support\Facades\Log;
7+
8+
class NpmPackageLock extends BasePackageScanner
9+
{
10+
/**
11+
* @return \Illuminate\Support\Collection<int, \Laravel\Roster\Package|\Laravel\Roster\Approach>
12+
*/
13+
public function scan(): Collection
14+
{
15+
$mappedItems = collect();
16+
$lockFilePath = $this->path.'package-lock.json';
17+
18+
$contents = $this->validateFile($lockFilePath);
19+
if ($contents === null) {
20+
return $mappedItems;
21+
}
22+
23+
$json = json_decode($contents, true);
24+
if (json_last_error() !== JSON_ERROR_NONE || ! is_array($json)) {
25+
Log::warning('Failed to decode Package: '.$lockFilePath.'. '.json_last_error_msg());
26+
27+
return $mappedItems;
28+
}
29+
30+
if (! array_key_exists('packages', $json)) {
31+
Log::warning('Malformed package-lock');
32+
33+
return $mappedItems;
34+
}
35+
36+
$dependencies = $json['packages']['']['dependencies'] ?? [];
37+
$devDependencies = $json['packages']['']['devDependencies'] ?? [];
38+
39+
$this->processDependencies($dependencies, $mappedItems, false);
40+
$this->processDependencies($devDependencies, $mappedItems, true);
41+
42+
return $mappedItems;
43+
}
44+
45+
/**
46+
* Check if the scanner can handle the given path
47+
*/
48+
public function canScan(): bool
49+
{
50+
return file_exists($this->path.'package-lock.json');
51+
}
52+
}

src/Scanners/PackageLock.php

Lines changed: 14 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -3,33 +3,11 @@
33
namespace Laravel\Roster\Scanners;
44

55
use Illuminate\Support\Collection;
6-
use Illuminate\Support\Facades\Log;
7-
use Laravel\Roster\Approach;
8-
use Laravel\Roster\Enums\Approaches;
9-
use Laravel\Roster\Enums\Packages;
10-
use Laravel\Roster\Package;
116

127
class PackageLock
138
{
149
/**
15-
* Map of npm / package.json package names to enums
16-
*
17-
* @var array<string, Packages|Approaches|array<int, Packages|Approaches>>
18-
*/
19-
protected array $map = [
20-
'alpinejs' => Packages::ALPINEJS,
21-
'@inertiajs/react' => [Packages::INERTIA, Packages::INERTIA_REACT],
22-
'@inertiajs/svelte' => [Packages::INERTIA, Packages::INERTIA_SVELTE],
23-
'@inertiajs/vue3' => [Packages::INERTIA, Packages::INERTIA_VUE],
24-
'laravel-echo' => Packages::ECHO,
25-
'@laravel/vite-plugin-wayfinder' => [Packages::WAYFINDER, Packages::WAYFINDER_VITE],
26-
'react' => Packages::REACT,
27-
'tailwindcss' => [Packages::TAILWINDCSS],
28-
'vue' => Packages::VUE,
29-
];
30-
31-
/**
32-
* @param string $path - package-lock.json
10+
* @param string $path - Base path to scan for lock files (package-lock.json, pnpm-lock.yaml, yarn.lock, ...)
3311
*/
3412
public function __construct(protected string $path) {}
3513

@@ -38,75 +16,20 @@ public function __construct(protected string $path) {}
3816
*/
3917
public function scan(): Collection
4018
{
41-
$mappedItems = collect([]);
42-
43-
if (! file_exists($this->path)) {
44-
Log::warning('Failed to scan Package: '.$this->path);
45-
46-
return $mappedItems;
47-
}
48-
49-
if (! is_readable($this->path)) {
50-
Log::warning('File not readable: '.$this->path);
51-
52-
return $mappedItems;
53-
}
54-
55-
$contents = file_get_contents($this->path);
56-
if ($contents === false) {
57-
Log::warning('Failed to read Package: '.$this->path);
58-
59-
return $mappedItems;
60-
}
61-
62-
$json = json_decode($contents, true);
63-
if (json_last_error() !== JSON_ERROR_NONE || ! is_array($json)) {
64-
Log::warning('Failed to decode Package: '.$this->path.'. '.json_last_error_msg());
65-
66-
return $mappedItems;
67-
}
68-
69-
if (! array_key_exists('packages', $json)) {
70-
Log::warning('Malformed package-lock');
71-
72-
return $mappedItems;
73-
}
74-
75-
$dependencies = $json['packages']['']['dependencies'] ?? [];
76-
$devDependencies = $json['packages']['']['devDependencies'] ?? [];
77-
78-
$this->processDependencies($dependencies, $mappedItems, false);
79-
$this->processDependencies($devDependencies, $mappedItems, true);
80-
81-
return $mappedItems;
82-
}
83-
84-
/**
85-
* Process dependencies and add them to the mapped items collection
86-
*
87-
* @param array<string, string> $dependencies
88-
* @param Collection<int, Package|Approach> $mappedItems
89-
*/
90-
private function processDependencies(array $dependencies, Collection $mappedItems, bool $isDev): void
91-
{
92-
foreach ($dependencies as $packageName => $version) {
93-
$mappedPackage = $this->map[$packageName] ?? null;
94-
if (is_null($mappedPackage)) {
95-
continue;
96-
}
97-
98-
if (! is_array($mappedPackage)) {
99-
$mappedPackage = [$mappedPackage];
100-
}
101-
102-
foreach ($mappedPackage as $mapped) {
103-
$niceVersion = preg_replace('/[^0-9.]/', '', $version) ?? '';
104-
$mappedItems->push(match (get_class($mapped)) {
105-
Packages::class => new Package($mapped, $niceVersion, $isDev),
106-
Approaches::class => new Approach($mapped),
107-
default => throw new \InvalidArgumentException('Unsupported mapping')
108-
});
19+
// Priority order: npm -> pnpm -> yarn -> bun
20+
$scanners = [
21+
new NpmPackageLock($this->path),
22+
new PnpmPackageLock($this->path),
23+
new YarnPackageLock($this->path),
24+
new BunPackageLock($this->path),
25+
];
26+
27+
foreach ($scanners as $scanner) {
28+
if ($scanner->canScan()) {
29+
return $scanner->scan();
10930
}
11031
}
32+
33+
return collect();
11134
}
11235
}

0 commit comments

Comments
 (0)