refactor: susun semula struktur folder — Laravel source ke src/

This commit is contained in:
Saufi
2026-05-19 15:58:35 +08:00
parent f052251b94
commit bf53c71b45
10806 changed files with 1385379 additions and 121 deletions

21
vendor/laravel/roster/LICENSE.md vendored Normal file
View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) Taylor Otwell
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

74
vendor/laravel/roster/README.md vendored Normal file
View File

@@ -0,0 +1,74 @@
# Laravel Roster
<p align="center">
<a href="https://github.com/laravel/roster/actions"><img src="https://github.com/laravel/roster/workflows/tests/badge.svg" alt="Build Status"></a>
<a href="https://packagist.org/packages/laravel/roster"><img src="https://img.shields.io/packagist/dt/laravel/roster" alt="Total Downloads"></a>
<a href="https://packagist.org/packages/laravel/roster"><img src="https://img.shields.io/packagist/v/laravel/roster" alt="Latest Stable Version"></a>
<a href="https://packagist.org/packages/laravel/roster"><img src="https://img.shields.io/packagist/l/laravel/roster" alt="License"></a>
</p>
## Introduction
Laravel Roster detects which Laravel ecosystem packages are in use within a project, and gives you an easy to use API to work with that data.
## Installation
To get started, install Roster via Composer:
```bash
composer require laravel/roster --dev
```
## Usage
**Scan a directory**
Get a roster of installed packages by scanning a directory:
```php
use Laravel\Roster\Roster;
$roster = Roster::scan($directory);
```
**Query the roster**
```php
use Laravel\Roster\Packages;
// Get all packages
$roster->packages();
// Get only packages that will be used in production
$roster->packages()->production();
// Packages that are only used for dev
$roster->packages()->dev();
// Check if a package is in use
$roster->uses(Packages::INERTIA);
// Check if a particular version of a package is in use
$roster->usesVersion(Packages::INERTIA, '2.0.0', '>=');
// Detect which JavaScript package manager is in use
$packageManager = $roster->nodePackageManager();
```
## Contributing
Thank you for considering contributing to Roster! The contribution guide can be found in
the [Laravel documentation](https://laravel.com/docs/contributions).
## Code of Conduct
In order to ensure that the Laravel community is welcoming to all, please review and abide by
the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
## Security Vulnerabilities
Please review [our security policy](https://github.com/laravel/roster/security/policy) on how to report security
vulnerabilities.
## License
Laravel Roster is open-sourced software licensed under the [MIT license](LICENSE.md).

70
vendor/laravel/roster/composer.json vendored Normal file
View File

@@ -0,0 +1,70 @@
{
"name": "laravel/roster",
"description": "Detect packages & approaches in use within a Laravel project",
"keywords": [
"dev",
"laravel"
],
"homepage": "https://github.com/laravel/roster",
"license": "MIT",
"support": {
"issues": "https://github.com/laravel/roster/issues",
"source": "https://github.com/laravel/roster"
},
"require": {
"php": "^8.2",
"illuminate/console": "^11.0|^12.0|^13.0",
"illuminate/contracts": "^11.0|^12.0|^13.0",
"illuminate/routing": "^11.0|^12.0|^13.0",
"illuminate/support": "^11.0|^12.0|^13.0",
"symfony/yaml": "^7.2|^8.0"
},
"require-dev": {
"laravel/pint": "^1.14",
"mockery/mockery": "^1.6",
"orchestra/testbench": "^9.0|^10.0|^11.0",
"pestphp/pest": "^3.0|^4.1",
"phpstan/phpstan": "^2.0"
},
"autoload": {
"psr-4": {
"Laravel\\Roster\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
},
"extra": {
"branch-alias": {
"dev-master": "1.x-dev"
},
"laravel": {
"providers": [
"Laravel\\Roster\\RosterServiceProvider"
]
}
},
"config": {
"sort-packages": true,
"allow-plugins": {
"pestphp/pest-plugin": true
}
},
"scripts": {
"lint": [
"vendor/bin/pint",
"vendor/bin/phpstan"
],
"test": [
"vendor/bin/pest"
],
"check": [
"@composer lint",
"@composer test"
]
},
"minimum-stability": "dev",
"prefer-stable": true
}

20
vendor/laravel/roster/src/Approach.php vendored Normal file
View File

@@ -0,0 +1,20 @@
<?php
namespace Laravel\Roster;
use Laravel\Roster\Enums\Approaches;
class Approach
{
public function __construct(protected Approaches $approach) {}
public function name(): string
{
return $this->approach->name;
}
public function approach(): Approaches
{
return $this->approach;
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace Laravel\Roster\Console;
use Illuminate\Console\Command;
use Laravel\Roster\Roster;
class ScanCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'roster:scan {directory}';
protected $description = 'Detect packages & approaches in use and output as JSON';
public function handle(): int
{
$directory = $this->argument('directory');
if (! is_string($directory)) {
$this->error('Pass a directory');
return self::FAILURE;
}
if (! is_dir($directory) || ! is_readable($directory)) {
$this->error("Directory '{$directory}' isn't a directory");
return self::FAILURE;
}
$roster = Roster::scan($directory);
$this->line($roster->json());
return self::SUCCESS;
}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace Laravel\Roster\Enums;
enum Approaches: string
{
case ACTION = 'action';
case DDD = 'ddd';
case MODULAR = 'modular';
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Laravel\Roster\Enums;
enum Ides: string
{
case PHPSTORM = 'phpstorm';
case CURSOR = 'cursor';
case WINDSURF = 'windsurf';
case VSCODE = 'vscode';
case CLAUDE_CODE = 'claudecode';
case CODEX = 'codex';
case OPENCODE = 'opencode';
}

View File

@@ -0,0 +1,26 @@
<?php
namespace Laravel\Roster\Enums;
use Laravel\Roster\Scanners\BunPackageLock;
use Laravel\Roster\Scanners\NpmPackageLock;
use Laravel\Roster\Scanners\PnpmPackageLock;
use Laravel\Roster\Scanners\YarnPackageLock;
enum NodePackageManager: string
{
case NPM = 'npm';
case PNPM = 'pnpm';
case YARN = 'yarn';
case BUN = 'bun';
public function scanner(string $path): NpmPackageLock|PnpmPackageLock|YarnPackageLock|BunPackageLock
{
return match ($this) {
self::NPM => new NpmPackageLock($path),
self::PNPM => new PnpmPackageLock($path),
self::YARN => new YarnPackageLock($path),
self::BUN => new BunPackageLock($path),
};
}
}

View File

@@ -0,0 +1,9 @@
<?php
namespace Laravel\Roster\Enums;
enum PackageSource: string
{
case COMPOSER = 'composer';
case NPM = 'npm';
}

View File

@@ -0,0 +1,62 @@
<?php
namespace Laravel\Roster\Enums;
enum Packages: string
{
// BACKEND
case AI = 'ai';
case BOOST = 'boost';
case BREEZE = 'breeze';
case CASHIER = 'cashier';
case DUSK = 'dusk';
case ENVOY = 'envoy';
case FILAMENT = 'filament';
case FOLIO = 'folio';
case FORTIFY = 'fortify';
case FLUXUI_FREE = 'flux_free';
case FLUXUI_PRO = 'flux_pro';
case HORIZON = 'horizon';
case INERTIA_LARAVEL = 'inertia-laravel';
case LARASTAN = 'larastan';
case LARAVEL = 'laravel';
case LIVEWIRE = 'livewire';
case MCP = 'mcp';
case NIGHTWATCH = 'nightwatch';
case NOVA = 'nova';
case OCTANE = 'octane';
case PAIL = 'pail';
case PASSPORT = 'passport';
case PENNANT = 'pennant';
case PEST = 'pest';
case PHPUNIT = 'phpunit';
case PINT = 'pint';
case PROMPTS = 'prompts';
case PULSE = 'pulse';
case RECTOR = 'rector';
case REVERB = 'reverb';
case SAIL = 'sail';
case SANCTUM = 'sanctum';
case SCOUT = 'scout';
case SOCIALITE = 'socialite';
case STATAMIC = 'statamic';
case TELESCOPE = 'telescope';
case VOLT = 'volt';
case WAYFINDER = 'wayfinder';
case ZIGGY = 'ziggy';
// NPM
case ALPINEJS = 'alpinejs';
case ECHO = 'laravel-echo';
case ECHO_REACT = 'echo-react';
case ECHO_VUE = 'echo-vue';
case ESLINT = 'eslint';
case INERTIA_REACT = 'inertia-react';
case INERTIA_SVELTE = 'inertia-svelte';
case INERTIA_VUE = 'inertia-vue';
case PRETTIER = 'prettier';
case REACT = 'react';
case TAILWINDCSS = 'tailwindcss';
case VUE = 'vue';
case WAYFINDER_VITE = 'wayfinder_vite';
}

115
vendor/laravel/roster/src/Package.php vendored Normal file
View File

@@ -0,0 +1,115 @@
<?php
namespace Laravel\Roster;
use Laravel\Roster\Enums\Packages;
use Laravel\Roster\Enums\PackageSource;
class Package
{
protected bool $direct = false;
protected string $constraint = '';
protected ?PackageSource $source = null;
public function __construct(
protected Packages $package,
protected string $packageName,
protected string $version,
protected bool $dev = false,
protected ?string $path = null
) {
//
}
public function setDev(bool $dev = true): self
{
$this->dev = $dev;
return $this;
}
public function setDirect(bool $direct = true): self
{
$this->direct = $direct;
return $this;
}
public function setConstraint(string $constraint = ''): self
{
$this->constraint = $constraint;
return $this;
}
public function setSource(PackageSource $source): self
{
$this->source = $source;
return $this;
}
public function setPath(string $path): self
{
$this->path = $path;
return $this;
}
public function name(): string
{
return $this->package->name;
}
public function package(): Packages
{
return $this->package;
}
public function version(): string
{
return $this->version;
}
public function direct(): bool
{
return $this->direct;
}
public function indirect(): bool
{
return ! $this->direct;
}
public function constraint(): string
{
return $this->constraint;
}
public function majorVersion(): string
{
return explode('.', $this->version)[0];
}
public function isDev(): bool
{
return $this->dev;
}
public function source(): ?PackageSource
{
return $this->source;
}
public function path(): ?string
{
return $this->path;
}
public function rawName(): string
{
return $this->packageName;
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace Laravel\Roster;
use Illuminate\Support\Collection;
/**
* @extends Collection<int, Package>
*/
class PackageCollection extends Collection
{
public function dev(): static
{
return $this->filter(fn (Package $package) => $package->isDev());
}
public function production(): static
{
return $this->filter(fn (Package $package) => ! $package->isDev());
}
}

165
vendor/laravel/roster/src/Roster.php vendored Normal file
View File

@@ -0,0 +1,165 @@
<?php
namespace Laravel\Roster;
use Illuminate\Support\Collection;
use Laravel\Roster\Enums\Approaches;
use Laravel\Roster\Enums\NodePackageManager;
use Laravel\Roster\Enums\Packages;
use Laravel\Roster\Scanners\Composer;
use Laravel\Roster\Scanners\DirectoryStructure;
use Laravel\Roster\Scanners\PackageLock;
/**
* Package and approach detection service for Laravel projects.
*
* Scans composer.lock, package-lock.json, and directory structure to identify
* packages and development approaches in use.
*/
class Roster
{
/**
* @var Collection<int, \Laravel\Roster\Approach>
*/
protected Collection $approaches;
protected PackageCollection $packages;
protected ?NodePackageManager $nodePackageManager = null;
public function __construct()
{
$this->approaches = collect();
$this->packages = new PackageCollection;
}
/**
* @throws \InvalidArgumentException
*/
public function add(Package|Approach $item): self
{
return match (get_class($item)) {
Package::class => $this->addPackage($item),
Approach::class => $this->addApproach($item),
default => throw new \InvalidArgumentException('Unexpected match value'),
};
}
public function uses(Packages|Approaches $item): bool
{
return $this->findItem($item) !== null;
}
/**
* @throws \InvalidArgumentException
*/
public function usesVersion(Packages $package, string $version, string $operator = '='): bool
{
if (! preg_match('/[0-9]{1,}\.[0-9]{1,}\.[0-9]{1,}/', $version)) {
throw new \InvalidArgumentException('SEMVER required');
}
$validOperators = ['<', '<=', '>', '>=', '==', '=', '!=', '<>'];
if (! in_array($operator, $validOperators)) {
throw new \InvalidArgumentException('Invalid operator');
}
$package = $this->findItem($package);
if (is_null($package)) {
return false;
}
/** @var \Laravel\Roster\Package $package */
return version_compare($package->version(), $version, $operator);
}
protected function findItem(Packages|Approaches $item): Package|Approach|null
{
return match (get_class($item)) {
Packages::class => $this->package($item),
Approaches::class => $this->approach($item),
default => null,
};
}
protected function addPackage(Package $package): self
{
$this->packages->push($package);
return $this;
}
protected function addApproach(Approach $approach): self
{
$this->approaches->push($approach);
return $this;
}
/**
* @return Collection<int, \Laravel\Roster\Approach>
*/
public function approaches(): Collection
{
return $this->approaches;
}
public function packages(): PackageCollection
{
return $this->packages;
}
public function package(Packages $package): ?Package
{
return $this->packages->first(fn (Package $item) => $item->package()->value === $package->value);
}
public function approach(Approaches $approach): ?Approach
{
return $this->approaches->first(fn (Approach $item) => $item->approach()->value === $approach->value);
}
public function nodePackageManager(): ?NodePackageManager
{
return $this->nodePackageManager;
}
public function json(): string
{
return json_encode([
'approaches' => $this->approaches->map(fn (Approach $approach) => [
'name' => $approach->name(),
])->toArray(),
'packages' => $this->packages->map(fn (Package $package) => [
'name' => $package->name(),
'version' => $package->version(),
'source' => $package->source()?->value,
'path' => $package->path(),
])->toArray(),
'nodePackageManager' => $this->nodePackageManager?->value,
], JSON_PRETTY_PRINT) ?: '{}';
}
public static function scan(?string $basePath = null): self
{
$roster = new self;
$basePath = ($basePath ?? base_path()).DIRECTORY_SEPARATOR;
(new Composer($basePath.'composer.lock'))
->scan()
->each(fn ($item) => $roster->add($item));
$packageLock = new PackageLock($basePath);
$packageLock->scan()
->each(fn ($item) => $roster->add($item));
(new DirectoryStructure($basePath))
->scan()
->each(fn ($item) => $roster->add($item));
$roster->nodePackageManager = $packageLock->detect();
return $roster;
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace Laravel\Roster;
use Illuminate\Support\ServiceProvider;
class RosterServiceProvider extends ServiceProvider
{
/**
* Bootstrap any package services.
*/
public function boot(): void
{
$this->registerCommands();
}
/**
* Register the package's commands.
*/
protected function registerCommands(): void
{
if ($this->app->runningInConsole()) {
$this->commands([
Console\ScanCommand::class,
]);
}
}
}

View File

@@ -0,0 +1,186 @@
<?php
namespace Laravel\Roster\Scanners;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Laravel\Roster\Approach;
use Laravel\Roster\Enums\Approaches;
use Laravel\Roster\Enums\Packages;
use Laravel\Roster\Enums\PackageSource;
use Laravel\Roster\Package;
abstract class BasePackageScanner
{
/**
* Map of package names to enums
*
* @var array<string, Packages|Approaches>
*/
protected array $map = [
'alpinejs' => Packages::ALPINEJS,
'eslint' => Packages::ESLINT,
'@inertiajs/react' => Packages::INERTIA_REACT,
'@inertiajs/svelte' => Packages::INERTIA_SVELTE,
'@inertiajs/vue3' => Packages::INERTIA_VUE,
'laravel-echo' => Packages::ECHO,
'@laravel/echo-react' => Packages::ECHO_REACT,
'@laravel/echo-vue' => Packages::ECHO_VUE,
'@laravel/vite-plugin-wayfinder' => Packages::WAYFINDER_VITE,
'prettier' => Packages::PRETTIER,
'react' => Packages::REACT,
'tailwindcss' => Packages::TAILWINDCSS,
'vue' => Packages::VUE,
];
/** @var array<string, array{constraint: string, isDev: bool}>|null */
protected ?array $directPackages = null;
public function __construct(protected string $path) {}
/**
* Returns the expected lock file name
*/
abstract protected function lockFile(): string;
/**
* @return \Illuminate\Support\Collection<int, \Laravel\Roster\Package|\Laravel\Roster\Approach>
*/
abstract public function scan(): Collection;
/**
* Check if the scanner can handle the given path
*/
public function canScan(): bool
{
return file_exists($this->lockFilePath());
}
/**
* Get the file path of the lock file
*/
protected function lockFilePath(): string
{
return $this->path.$this->lockFile();
}
/**
* Process dependencies and add them to the mapped items collection
*
* @param array<string, string> $dependencies
* @param Collection<int, Package|Approach> $mappedItems
* @param ?callable $versionCb - callback to override version
*/
protected function processDependencies(array $dependencies, Collection $mappedItems, bool $isDev, ?callable $versionCb = null): void
{
$directPackages = $this->direct();
foreach ($dependencies as $packageName => $version) {
$mappedPackage = $this->map[$packageName] ?? null;
if (is_null($mappedPackage)) {
continue;
}
if (! is_null($versionCb)) {
$version = $versionCb($packageName, $version);
}
$direct = false;
$constraint = $version;
$packageIsDev = $isDev;
if (array_key_exists($packageName, $directPackages)) {
$direct = true;
$constraint = $directPackages[$packageName]['constraint'];
$packageIsDev = $directPackages[$packageName]['isDev'];
}
$niceVersion = preg_replace('/[^0-9.]/', '', $version) ?? '';
$mappedItems->push(match (get_class($mappedPackage)) {
Packages::class => (new Package($mappedPackage, $packageName, $niceVersion, $packageIsDev))->setDirect($direct)->setConstraint($constraint)->setSource(PackageSource::NPM)->setPath($this->computePath($packageName)),
Approaches::class => new Approach($mappedPackage),
default => throw new \InvalidArgumentException('Unsupported mapping')
});
}
}
/**
* Returns direct dependencies as defined in package.json
*
* @return array<string, array{constraint: string, isDev: bool}>
*/
protected function direct(): array
{
if ($this->directPackages !== null) {
return $this->directPackages;
}
$this->directPackages = [];
$filename = $this->path.'package.json';
if (! file_exists($filename) || ! is_readable($filename)) {
return $this->directPackages;
}
$contents = file_get_contents($filename);
if ($contents === false) {
return $this->directPackages;
}
$json = json_decode($contents, true);
if (json_last_error() !== JSON_ERROR_NONE || ! is_array($json)) {
return $this->directPackages;
}
foreach ((array) ($json['dependencies'] ?? []) as $name => $constraint) {
$this->directPackages[$name] = [
'constraint' => $constraint,
'isDev' => false,
];
}
foreach ((array) ($json['devDependencies'] ?? []) as $name => $constraint) {
$this->directPackages[$name] = [
'constraint' => $constraint,
'isDev' => true,
];
}
return $this->directPackages;
}
protected function computePath(string $packageName): string
{
$basePath = realpath($this->path) ?: $this->path;
return $basePath.DIRECTORY_SEPARATOR.'node_modules'.DIRECTORY_SEPARATOR
.str_replace('/', DIRECTORY_SEPARATOR, $packageName);
}
/**
* Common file validation logic
*/
protected function validateFile(string $path, string $type = 'Package'): ?string
{
if (! file_exists($path)) {
Log::warning("Failed to scan $type: $path");
return null;
}
if (! is_readable($path)) {
Log::warning("File not readable: $path");
return null;
}
$contents = file_get_contents($path);
if ($contents === false) {
Log::warning("Failed to read $type: $path");
return null;
}
return $contents;
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace Laravel\Roster\Scanners;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
class BunPackageLock extends BasePackageScanner
{
protected function lockFile(): string
{
return 'bun.lock';
}
/**
* @return \Illuminate\Support\Collection<int, \Laravel\Roster\Package|\Laravel\Roster\Approach>
*/
public function scan(): Collection
{
$mappedItems = collect();
$lockFilePath = $this->lockFilePath();
$contents = $this->validateFile($lockFilePath);
if ($contents === null) {
return $mappedItems;
}
// Remove trailing commas before decoding
/** @var string $contents */
$contents = preg_replace('/,\s*([]}])/m', '$1', $contents);
$json = json_decode($contents, true);
if (json_last_error() !== JSON_ERROR_NONE || ! is_array($json)) {
Log::warning('Failed to decode Package: '.$lockFilePath.'. '.json_last_error_msg());
return $mappedItems;
}
/** @var array<string, array<string, mixed>> $json */
if (! isset($json['workspaces']['']) || ! isset($json['packages'])) {
Log::warning('Malformed bun.lock');
return $mappedItems;
}
/** @var array<string, mixed> $workspace */
$workspace = $json['workspaces'][''];
/** @var array<string, string> $dependencies */
$dependencies = $workspace['dependencies'] ?? [];
/** @var array<string, string> $devDependencies */
$devDependencies = $workspace['devDependencies'] ?? [];
$this->processDependencies($dependencies, $mappedItems, false);
$this->processDependencies($devDependencies, $mappedItems, true);
return $mappedItems;
}
}

View File

@@ -0,0 +1,216 @@
<?php
namespace Laravel\Roster\Scanners;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Laravel\Roster\Approach;
use Laravel\Roster\Enums\Approaches;
use Laravel\Roster\Enums\Packages;
use Laravel\Roster\Enums\PackageSource;
use Laravel\Roster\Package;
class Composer
{
protected string $vendorDir = 'vendor';
/**
* Map of composer package names to enums
*
* @var array<string, Packages|Approaches>
*/
protected array $map = [
'filament/filament' => Packages::FILAMENT,
'inertiajs/inertia-laravel' => Packages::INERTIA_LARAVEL,
'larastan/larastan' => Packages::LARASTAN,
'laravel/ai' => Packages::AI,
'laravel/boost' => Packages::BOOST,
'laravel/breeze' => Packages::BREEZE,
'laravel/cashier' => Packages::CASHIER,
'laravel/dusk' => Packages::DUSK,
'laravel/envoy' => Packages::ENVOY,
'laravel/folio' => Packages::FOLIO,
'laravel/fortify' => Packages::FORTIFY,
'laravel/framework' => Packages::LARAVEL,
'laravel/horizon' => Packages::HORIZON,
'laravel/mcp' => Packages::MCP,
'laravel/nightwatch' => Packages::NIGHTWATCH,
'laravel/nova' => Packages::NOVA,
'laravel/octane' => Packages::OCTANE,
'laravel/pail' => Packages::PAIL,
'laravel/passport' => Packages::PASSPORT,
'laravel/pennant' => Packages::PENNANT,
'laravel/pint' => Packages::PINT,
'laravel/prompts' => Packages::PROMPTS,
'laravel/pulse' => Packages::PULSE,
'laravel/reverb' => Packages::REVERB,
'laravel/sail' => Packages::SAIL,
'laravel/sanctum' => Packages::SANCTUM,
'laravel/scout' => Packages::SCOUT,
'laravel/socialite' => Packages::SOCIALITE,
'laravel/telescope' => Packages::TELESCOPE,
'laravel/wayfinder' => Packages::WAYFINDER,
'livewire/flux' => Packages::FLUXUI_FREE,
'livewire/flux-pro' => Packages::FLUXUI_PRO,
'livewire/livewire' => Packages::LIVEWIRE,
'livewire/volt' => Packages::VOLT,
'pestphp/pest' => Packages::PEST,
'phpunit/phpunit' => Packages::PHPUNIT,
'rector/rector' => Packages::RECTOR,
'statamic/cms' => Packages::STATAMIC,
'tightenco/ziggy' => Packages::ZIGGY,
];
/** @var array<string, array{constraint: string, isDev: bool}> */
protected array $directPackages = [];
/**
* @param string $path - composer.lock
*/
public function __construct(protected string $path) {}
/**
* @return \Illuminate\Support\Collection<int, \Laravel\Roster\Package|\Laravel\Roster\Approach>
*/
public function scan(): Collection
{
$mappedItems = collect([]);
if (! file_exists($this->path)) {
Log::warning('Failed to scan Composer: '.$this->path);
return $mappedItems;
}
if (! is_readable($this->path)) {
Log::warning('File not readable: '.$this->path);
return $mappedItems;
}
$contents = file_get_contents($this->path);
if ($contents === false) {
Log::warning('Failed to read Composer: '.$this->path);
return $mappedItems;
}
$json = json_decode($contents, true);
if (json_last_error() !== JSON_ERROR_NONE || ! is_array($json)) {
Log::warning('Failed to decode Composer: '.$this->path.'. '.json_last_error_msg());
return $mappedItems;
}
if (! array_key_exists('packages', $json)) {
Log::warning('Malformed composer.lock');
return $mappedItems;
}
$this->directPackages = $this->direct();
$packages = $json['packages'] ?? [];
$devPackages = $json['packages-dev'] ?? [];
$this->processPackages($packages, $mappedItems, false);
$this->processPackages($devPackages, $mappedItems, true);
return $mappedItems;
}
/**
* Returns direct dependencies as defined in composer.json
*
* @return array<string, array{constraint: string, isDev: bool}>
* */
protected function direct(): array
{
$packages = [];
$filename = realpath(dirname($this->path)).DIRECTORY_SEPARATOR.'composer.json';
if (file_exists($filename) === false || is_readable($filename) === false) {
return $packages;
}
$json = file_get_contents($filename);
if ($json === false) {
return $packages;
}
$json = json_decode($json, true);
if (json_last_error() !== JSON_ERROR_NONE || ! is_array($json)) {
return $packages;
}
$config = $json['config'] ?? [];
$this->vendorDir = is_array($config) && isset($config['vendor-dir']) && is_string($config['vendor-dir'])
? $config['vendor-dir']
: 'vendor';
foreach ((array) ($json['require'] ?? []) as $name => $constraint) {
$packages[$name] = [
'constraint' => $constraint,
'isDev' => false,
];
}
foreach ((array) ($json['require-dev'] ?? []) as $name => $constraint) {
$packages[$name] = [
'constraint' => $constraint,
'isDev' => true,
];
}
return $packages;
}
/**
* Process packages and add them to the mapped items collection
*
* @param array<int, array<string, string>> $packages
* @param Collection<int, Package|Approach> $mappedItems
* @return Collection<int, Package|Approach>
*/
private function processPackages(array $packages, Collection $mappedItems, bool $isDev): Collection
{
foreach ($packages as $package) {
$packageName = $package['name'] ?? '';
$version = $package['version'] ?? '';
$mappedPackage = $this->map[$packageName] ?? null;
$direct = false;
$constraint = $version;
if (is_null($mappedPackage)) {
continue;
}
if (array_key_exists($packageName, $this->directPackages) === true) {
$direct = true;
$constraint = $this->directPackages[$packageName]['constraint'];
}
$niceVersion = preg_replace('/[^0-9.]/', '', $version) ?? '';
$mappedItems->push(match (get_class($mappedPackage)) {
Packages::class => (new Package($mappedPackage, $packageName, $niceVersion, $isDev))->setDirect($direct)->setConstraint($constraint)->setSource(PackageSource::COMPOSER)->setPath($this->computePath($packageName)),
Approaches::class => new Approach($mappedPackage),
default => throw new \InvalidArgumentException('Unsupported mapping')
});
}
return $mappedItems;
}
private function computePath(string $packageName): string
{
$vendorPath = str_replace('/', DIRECTORY_SEPARATOR, $this->vendorDir);
if (DIRECTORY_SEPARATOR === '/' && str_starts_with($vendorPath, DIRECTORY_SEPARATOR)
|| DIRECTORY_SEPARATOR === '\\' && preg_match('/^[A-Za-z]:[\\\\\\/]/', $vendorPath)) {
return $vendorPath.DIRECTORY_SEPARATOR
.str_replace('/', DIRECTORY_SEPARATOR, $packageName);
}
return realpath(dirname($this->path)).DIRECTORY_SEPARATOR
.$vendorPath.DIRECTORY_SEPARATOR
.str_replace('/', DIRECTORY_SEPARATOR, $packageName);
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace Laravel\Roster\Scanners;
use Illuminate\Support\Collection;
use Laravel\Roster\Approach;
use Laravel\Roster\Enums\Approaches;
class DirectoryStructure
{
public function __construct(protected string $path) {}
/**
* @return \Illuminate\Support\Collection<int, \Laravel\Roster\Package|\Laravel\Roster\Approach>
*/
public function scan(): Collection
{
$items = collect();
$actions = $this->path.DIRECTORY_SEPARATOR.'/app/'.DIRECTORY_SEPARATOR.'Actions';
if (is_dir($actions)) {
$items->push(new Approach(Approaches::ACTION));
}
$domains = $this->path.DIRECTORY_SEPARATOR.'/app/'.DIRECTORY_SEPARATOR.'Domains';
if (is_dir($domains)) {
$items->push(new Approach(Approaches::DDD));
}
if (is_dir($this->path.DIRECTORY_SEPARATOR.'modules') || is_dir($this->path.DIRECTORY_SEPARATOR.'Modules') || is_dir($this->path.DIRECTORY_SEPARATOR.'app-modules')) {
$items->push(new Approach(Approaches::MODULAR));
}
return $items;
}
}

View File

@@ -0,0 +1,59 @@
<?php
namespace Laravel\Roster\Scanners;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
class NpmPackageLock extends BasePackageScanner
{
protected function lockFile(): string
{
return 'package-lock.json';
}
/**
* @return \Illuminate\Support\Collection<int, \Laravel\Roster\Package|\Laravel\Roster\Approach>
*/
public function scan(): Collection
{
$mappedItems = collect();
$lockFilePath = $this->lockFilePath();
$contents = $this->validateFile($lockFilePath);
if ($contents === null) {
return $mappedItems;
}
$json = json_decode($contents, true);
if (json_last_error() !== JSON_ERROR_NONE || ! is_array($json)) {
Log::warning('Failed to decode Package: '.$lockFilePath.'. '.json_last_error_msg());
return $mappedItems;
}
if (! array_key_exists('packages', $json)) {
Log::warning('Malformed package-lock');
return $mappedItems;
}
$dependencies = $json['packages']['']['dependencies'] ?? [];
$devDependencies = $json['packages']['']['devDependencies'] ?? [];
$packages = array_filter($json['packages'], fn ($key) => $key !== '', ARRAY_FILTER_USE_KEY);
$versionCb = function (string $packageName, string $version) use ($packages): string {
$key = "node_modules/{$packageName}";
if (array_key_exists($key, $packages)) {
return $packages[$key]['version'];
}
return $version;
};
$this->processDependencies($dependencies, $mappedItems, false, $versionCb);
$this->processDependencies($devDependencies, $mappedItems, true, $versionCb);
return $mappedItems;
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace Laravel\Roster\Scanners;
use Illuminate\Support\Collection;
use Laravel\Roster\Enums\NodePackageManager;
class PackageLock
{
/**
* @param string $path - Base path to scan for lock files (package-lock.json, pnpm-lock.yaml, yarn.lock, ...)
*/
public function __construct(protected string $path) {}
/**
* @return \Illuminate\Support\Collection<int, \Laravel\Roster\Package|\Laravel\Roster\Approach>
*/
public function scan(): Collection
{
foreach (NodePackageManager::cases() as $case) {
$scanner = $case->scanner($this->path);
if ($scanner->canScan()) {
return $scanner->scan();
}
}
return collect();
}
public function detect(): NodePackageManager
{
foreach (NodePackageManager::cases() as $case) {
$scanner = $case->scanner($this->path);
if ($scanner->canScan()) {
return $case;
}
}
return NodePackageManager::NPM;
}
}

View File

@@ -0,0 +1,70 @@
<?php
namespace Laravel\Roster\Scanners;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Symfony\Component\Yaml\Yaml;
class PnpmPackageLock extends BasePackageScanner
{
protected function lockFile(): string
{
return 'pnpm-lock.yaml';
}
/**
* @return \Illuminate\Support\Collection<int, \Laravel\Roster\Package|\Laravel\Roster\Approach>
*/
public function scan(): Collection
{
$mappedItems = collect();
$lockFilePath = $this->lockFilePath();
$contents = $this->validateFile($lockFilePath, 'PNPM lock');
if ($contents === null) {
return $mappedItems;
}
try {
/** @var array<string, mixed> $parsed */
$parsed = Yaml::parse($contents);
} catch (\Exception $e) {
Log::error('Failed to parse YAML: '.$e->getMessage());
return $mappedItems;
}
/** @var array<string, string> $dependencies */
$dependencies = [];
/** @var array<string, string> $devDependencies */
$devDependencies = [];
/** @var array<string, array<string, mixed>> $importers */
$importers = $parsed['importers'] ?? [];
$root = $importers['.'] ?? [];
/** @var array<string, array<string, mixed>> $rootDependencies */
$rootDependencies = $root['dependencies'] ?? [];
/** @var array<string, array<string, mixed>> $rootDevDependencies */
$rootDevDependencies = $root['devDependencies'] ?? [];
foreach ($rootDependencies as $name => $data) {
if (isset($data['version'])) {
$dependencies[$name] = $data['version'];
}
}
foreach ($rootDevDependencies as $name => $data) {
if (isset($data['version'])) {
$devDependencies[$name] = $data['version'];
}
}
/** @var array<string, string> $dependencies */
/** @var array<string, string> $devDependencies */
$this->processDependencies($dependencies, $mappedItems, false);
$this->processDependencies($devDependencies, $mappedItems, true);
return $mappedItems;
}
}

View File

@@ -0,0 +1,95 @@
<?php
namespace Laravel\Roster\Scanners;
use Illuminate\Support\Collection;
use Laravel\Roster\Approach;
use Laravel\Roster\Package;
class YarnPackageLock extends BasePackageScanner
{
private const YARN_V1_HEADER = '/^("?)(@[^@"\/]+\/[^@"]+|[^@"]+)(@[^:"]+)?\1:$/';
private const YARN_V4_HEADER = '/^"(@?[^@"]+(?:\/[^@"]+)?)@npm:[^"]*":\s*$/';
private const YARN_V1_VERSION = '/^version\s+"([^"]+)"$/';
private const YARN_V4_VERSION = '/^version:\s+(.+)$/';
protected function lockFile(): string
{
return 'yarn.lock';
}
/**
* @return Collection<int, Package|Approach>
*/
public function scan(): Collection
{
$mappedItems = collect();
$lockFilePath = $this->lockFilePath();
$contents = $this->validateFile($lockFilePath, 'Yarn lock');
if ($contents === null) {
return $mappedItems;
}
$dependencies = [];
$lines = explode("\n", $contents);
$currentPackage = null;
foreach ($lines as $line) {
$line = trim($line);
if ($line === '' || str_starts_with($line, '#')) {
continue;
}
$packageName = $this->parsePackageHeader($line);
if ($packageName !== null) {
$currentPackage = $packageName;
continue;
}
$version = $this->parseVersion($line);
if ($currentPackage !== null && $version !== null) {
$dependencies[$currentPackage] = $version;
$currentPackage = null;
}
}
// Yarn lock does not distinguish devDependencies :/
$this->processDependencies($dependencies, $mappedItems, false);
return $mappedItems;
}
private function parsePackageHeader(string $line): ?string
{
if (preg_match(self::YARN_V1_HEADER, $line, $matches)) {
return $matches[2];
}
if (preg_match(self::YARN_V4_HEADER, $line, $matches)) {
return $matches[1];
}
return null;
}
private function parseVersion(string $line): ?string
{
if (preg_match(self::YARN_V1_VERSION, $line, $matches)) {
return $matches[1];
}
if (preg_match(self::YARN_V4_VERSION, $line, $matches)) {
return trim($matches[1]);
}
return null;
}
}