Enhance ManualServers functionality: add user-provided information display and credential handling; improve TagsInput validation; update navigation properties

This commit is contained in:
2026-03-13 05:58:51 -04:00
parent 0f36dba867
commit a8b6e5cfba
8 changed files with 951 additions and 19 deletions

View File

@@ -96,19 +96,26 @@ class FilamentInput
->disabled($setting->disabled ?? false)
->hint($setting->hint ?? '')
->hintColor('primary')
->helperText($setting->description ?? '');
->helperText($setting->description ?? '')
->live(onBlur: true);
// For required TagsInput, validate that array has at least one item
// Standard 'required' rule considers empty arrays as valid
// For required TagsInput, add custom validation
if ($setting->required ?? false) {
$input->required()
->rule(function () {
$input->rules([
function () {
return function (string $attribute, $value, \Closure $fail) {
if (!is_array($value) || empty($value)) {
$fail('The :attribute field must have at least 1 items.');
// Check if value is an array and has at least one item
if (!is_array($value)) {
$fail('The :attribute field is required.');
return;
}
if (count($value) === 0) {
$fail('The :attribute field is required.');
return;
}
};
});
}
]);
}
return $input;

View File

@@ -12,8 +12,8 @@ use Paymenter\Extensions\Servers\ManualServers\ManualServers;
class ManualServersManage extends Page implements HasForms
{
use InteractsWithForms;
protected static string|\BackedEnum|null $navigationIcon = 'ri-hard-drive-3-line';
protected string $view = 'manual-servers::admin.pages.manual-servers-manage';
protected static ?string $navigationIcon = 'ri-hard-drive-3-line';
protected static string $view = 'manual-servers::admin.pages.manual-servers-manage';
protected static bool $shouldRegisterNavigation = false;
protected static ?string $slug = 'manual-servers-manage/{service}';
public ?Service $service = null;

View File

@@ -13,9 +13,9 @@ class ManualServersQueue extends Page implements HasTable
{
use InteractsWithTable;
protected static string|\BackedEnum|null $navigationIcon = 'ri-hard-drive-3-line';
protected string $view = 'manual-servers::admin.pages.manual-servers-queue';
protected static string|\UnitEnum|null $navigationGroup = 'Extensions';
protected static ?string $navigationIcon = 'ri-hard-drive-3-line';
protected static string $view = 'manual-servers::admin.pages.manual-servers-queue';
protected static ?string $navigationGroup = 'Extensions';
protected static ?string $navigationLabel = 'Manual Servers';
protected static ?int $navigationSort = 100;

View File

@@ -162,6 +162,70 @@ class ManualServers extends Server
return $config;
}
/**
* Get configuration options for checkout (user-provided data during purchase)
*
* @param Product $product
* @return array
*/
public function getCheckoutConfig($product = null): array
{
if (!$product) {
return [];
}
// Create a temporary service object to use getCredentialFieldsForService
$tempService = new Service(['product_id' => $product->id]);
$tempService->setRelation('product', $product);
$fields = $this->getCredentialFieldsForService($tempService);
// Convert to checkout config format
$checkoutFields = [];
foreach ($fields as $field) {
$checkoutFields[] = [
'name' => $field['name'],
'type' => $field['type'],
'label' => $field['label'],
'required' => true,
'placeholder' => 'Enter ' . strtolower($field['label']),
];
}
return $checkoutFields;
}
/**
* Get configuration options for admin when managing a service
* This allows admins to enter credentials and other service-specific data
*
* @param Service $service
* @return array
*/
public function getUserConfig(Service $service): array
{
$fields = $this->getCredentialFieldsForService($service);
// Get existing credentials
$credentialsJson = $service->properties->where('key', 'credentials_data')->first()?->value ?? '[]';
$existingCredentials = json_decode($credentialsJson, true) ?? [];
// Format fields for the admin interface
$configFields = [];
foreach ($fields as $field) {
$configFields[] = [
'name' => $field['name'],
'type' => $field['type'],
'label' => $field['label'],
'required' => false,
'default' => $existingCredentials[$field['name']] ?? '',
];
}
return $configFields;
}
public function createServer(Service $service, $params = []): bool
{
$processes = $this->getProcessesForService($service);
@@ -181,9 +245,20 @@ class ManualServers extends Server
['key' => 'processes_data'],
['value' => json_encode($processData)]
);
// Extract credential values from params (checkout form data)
$credentialFields = $this->getCredentialFieldsForService($service);
$credentials = [];
foreach ($credentialFields as $field) {
$fieldName = $field['name'];
if (isset($params[$fieldName])) {
$credentials[$fieldName] = $params[$fieldName];
}
}
$service->properties()->updateOrCreate(
['key' => 'credentials_data'],
['value' => json_encode([])]
['value' => json_encode($credentials)]
);
$service->properties()->updateOrCreate(
['key' => 'deployed_at'],
@@ -370,13 +445,30 @@ class ManualServers extends Server
$credentialsJson = $service->properties->where('key', 'credentials_data')->first()?->value ?? '[]';
$deployedAt = $service->properties->where('key', 'deployed_at')->first()?->value;
$feedback = $service->properties->where('key', 'feedback')->first()?->value ?? '';
// Read credentials from individual properties
$credentialFields = $this->getCredentialFieldsForService($service);
$credentials = [];
foreach ($credentialFields as $field) {
$fieldName = $field['name'];
$value = $service->properties->where('key', $fieldName)->first()?->value;
if ($value !== null) {
$credentials[$fieldName] = $value;
}
}
// Fallback to credentials_data JSON if individual properties are empty
if (empty($credentials)) {
$credentials = json_decode($credentialsJson, true) ?? [];
}
return [
'status' => $status,
'processes' => json_decode($processesJson, true) ?? [],
'credentials' => json_decode($credentialsJson, true) ?? [],
'credentials' => $credentials,
'deployed_at' => $deployedAt,
'feedback' => $feedback,
'credential_fields' => $this->getCredentialFieldsForService($service),
'credential_fields' => $credentialFields,
'buttons' => $this->getButtonsForService($service),
'instructions' => $service->product->settings->where('key', 'instructions')->first()?->value ?? '',
];

View File

@@ -65,6 +65,52 @@
</div>
</x-filament::section>
@if(!empty($this->credentialData))
<x-filament::section>
<x-slot name="heading">User-Provided Information</x-slot>
<x-slot name="description">
Information entered by the client during checkout
</x-slot>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
@foreach($this->credentialData as $key => $value)
@php
$fieldConfig = collect($this->credentialFields)->where('name', $key)->first();
$label = $fieldConfig['label'] ?? ucfirst(str_replace('_', ' ', $key));
$isPassword = ($fieldConfig['type'] ?? 'text') === 'password';
@endphp
<div class="p-4 rounded-lg border border-gray-200 dark:border-white/10 bg-gray-50 dark:bg-white/5">
<p class="text-xs font-medium text-gray-500 dark:text-gray-400 mb-2">{{ $label }}</p>
<div class="flex items-start justify-between gap-3">
<div class="flex-1 min-w-0">
@if($isPassword)
<p class="font-mono text-sm" x-data="{ show: false }">
<span x-show="!show" class="text-gray-400">••••••••</span>
<span x-show="show" class="break-all">{{ $value }}</span>
<x-filament::link @click="show = !show" size="xs" class="ml-2">
<span x-show="!show">Show</span>
<span x-show="show">Hide</span>
</x-filament::link>
</p>
@else
<p class="font-mono text-sm break-all">{{ $value ?: 'N/A' }}</p>
@endif
</div>
<x-filament::button
size="xs"
color="gray"
x-data
x-on:click="navigator.clipboard.writeText('{{ addslashes($value) }}'); $tooltip('Copied!')">
Copy
</x-filament::button>
</div>
</div>
@endforeach
</div>
</x-filament::section>
@endif
@if(empty($this->processData))
<x-filament::section>
<x-slot name="heading">Initialize Setup Flow</x-slot>

View File

@@ -1,3 +0,0 @@
*
!data/
!.gitignore

View File

@@ -0,0 +1,440 @@
<?php
error_reporting(E_ERROR | E_PARSE);
define('LARAVEL_START', microtime(true));
require_once __DIR__ . '/../autoload.php';
class LaravelVsCode
{
public static function relativePath($path)
{
if (!str_contains($path, base_path())) {
return (string) $path;
}
return ltrim(str_replace(base_path(), '', realpath($path) ?: $path), DIRECTORY_SEPARATOR);
}
public static function isVendor($path)
{
return str_contains($path, base_path("vendor"));
}
public static function outputMarker($key)
{
return '__VSCODE_LARAVEL_' . $key . '__';
}
public static function startupError(\Throwable $e)
{
throw new Error(self::outputMarker('STARTUP_ERROR') . ': ' . $e->getMessage());
}
}
try {
$app = require_once __DIR__ . '/../../bootstrap/app.php';
} catch (\Throwable $e) {
LaravelVsCode::startupError($e);
exit(1);
}
$app->register(new class($app) extends \Illuminate\Support\ServiceProvider
{
public function boot()
{
config([
'logging.channels.null' => [
'driver' => 'monolog',
'handler' => \Monolog\Handler\NullHandler::class,
],
'logging.default' => 'null',
]);
}
});
try {
$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
$kernel->bootstrap();
} catch (\Throwable $e) {
LaravelVsCode::startupError($e);
exit(1);
}
echo LaravelVsCode::outputMarker('START_OUTPUT');
$components = new class {
protected $autoloaded = [];
protected $prefixes = [];
public function __construct()
{
$this->autoloaded = require base_path("vendor/composer/autoload_psr4.php");
}
public function all()
{
$components = collect(array_merge(
$this->getStandardClasses(),
$this->getStandardViews(),
$this->getNamespaced(),
$this->getAnonymousNamespaced(),
$this->getAnonymous(),
$this->getAliases(),
$this->getVendorComponents(),
))->groupBy('key')->pipe($this->setProps(...))->map(fn($items) => [
'isVendor' => $items->first()['isVendor'],
'paths' => $items->pluck('path')->unique()->values(),
'props' => $this->formatProps($items),
]);
return [
'components' => $components,
'prefixes' => $this->prefixes,
];
}
protected function formatProps($items)
{
$props = $items->pluck('props');
if ($codeBlock = $props->firstWhere(fn ($prop) => is_string($prop))) {
return $codeBlock;
}
return $props->values()->filter()->flatMap(fn($i) => $i);
}
protected function getStandardViews()
{
$path = resource_path('views/components');
return $this->findFiles($path, 'blade.php');
}
protected function findFiles($path, $extension, $keyCallback = null)
{
if (!is_dir($path)) {
return [];
}
$files = \Symfony\Component\Finder\Finder::create()
->files()
->name("*." . $extension)
->in($path);
$components = [];
$pathRealPath = realpath($path);
foreach ($files as $file) {
$realPath = $file->getRealPath();
$key = str($realPath)
->replace($pathRealPath, '')
->ltrim('/\\')
->replace('.' . $extension, '')
->replace(['/', '\\'], '.')
->pipe(fn($str) => $this->handleIndexComponents($str));
$components[] = [
"path" => LaravelVsCode::relativePath($realPath),
"isVendor" => LaravelVsCode::isVendor($realPath),
"key" => $keyCallback ? $keyCallback($key) : $key,
];
}
return $components;
}
protected function getStandardClasses()
{
$path = app_path('View/Components');
$appNamespace = collect($this->autoloaded)
->filter(fn($paths) => in_array(app_path(), $paths))
->keys()
->first() ?? '';
return collect($this->findFiles(
$path,
'php',
fn($key) => $key->explode('.')
->map(fn($p) => \Illuminate\Support\Str::kebab($p))
->implode('.'),
))->map(function ($item) use ($appNamespace) {
$class = str($item['path'])
->after('View/Components/')
->replace('.php', '')
->replace('/', '\\')
->prepend($appNamespace . 'View\\Components\\')
->toString();
if (!class_exists($class)) {
return $item;
}
$reflection = new \ReflectionClass($class);
$parameters = collect($reflection->getConstructor()?->getParameters() ?? [])
->filter(fn($p) => $p->isPromoted())
->flatMap(fn($p) => [$p->getName() => $p->isOptional() ? $p->getDefaultValue() : null])
->all();
$props = collect($reflection->getProperties())
->filter(fn($p) => $p->isPublic() && $p->getDeclaringClass()->getName() === $class)
->map(fn($p) => [
'name' => \Illuminate\Support\Str::kebab($p->getName()),
'type' => (string) ($p->getType() ?? 'mixed'),
'default' => $p->getDefaultValue() ?? $parameters[$p->getName()] ?? null,
]);
[$except, $props] = $props->partition(fn($p) => $p['name'] === 'except');
if ($except->isNotEmpty()) {
$except = $except->first()['default'];
$props = $props->reject(fn($p) => in_array($p['name'], $except));
}
return [
...$item,
'props' => $props,
];
})->all();
}
protected function getAliases()
{
$components = [];
foreach (\Illuminate\Support\Facades\Blade::getClassComponentAliases() as $key => $class) {
if (class_exists($class)) {
$reflection = new ReflectionClass($class);
$components[] = [
"path" => LaravelVsCode::relativePath($reflection->getFileName()),
"isVendor" => LaravelVsCode::isVendor($reflection->getFileName()),
"key" => $key,
];
}
}
return $components;
}
protected function getAnonymousNamespaced()
{
$components = [];
foreach (\Illuminate\Support\Facades\Blade::getAnonymousComponentNamespaces() as $key => $dir) {
$path = collect([$dir, resource_path('views/' . $dir)])->first(fn($p) => is_dir($p));
if (!$path) {
continue;
}
array_push(
$components,
...$this->findFiles(
$path,
'blade.php',
fn($k) => $k->kebab()->prepend($key . "::"),
)
);
}
return $components;
}
protected function getAnonymous()
{
$components = [];
foreach (\Illuminate\Support\Facades\Blade::getAnonymousComponentPaths() as $item) {
array_push(
$components,
...$this->findFiles(
$item['path'],
'blade.php',
function (\Illuminate\Support\Stringable $key) use ($item) {
$prefix = $item['prefix'] ? $item['prefix'] . '::' : '';
$key = $key->kebab();
$keys = [];
$keys[] = $key->prepend($prefix);
if ($item['prefix'] === 'flux') {
$keys[] = $key->prepend('flux:');
}
return $keys;
},
)
);
if (!in_array($item['prefix'], $this->prefixes)) {
$this->prefixes[] = $item['prefix'];
}
}
return $components;
}
protected function getVendorComponents(): array
{
$components = [];
/** @var \Illuminate\View\Factory $view */
$view = \Illuminate\Support\Facades\App::make('view');
/** @var \Illuminate\View\FileViewFinder $finder */
$finder = $view->getFinder();
/** @var array<string, array<int, string>> $views */
$views = $finder->getHints();
foreach ($views as $key => $paths) {
foreach ($paths as $path) {
$path .= '/components';
if (!is_dir($path)) {
continue;
}
array_push(
$components,
...$this->findFiles(
$path,
'blade.php',
fn (\Illuminate\Support\Stringable $k) => $k->kebab()->prepend($key.'::'),
)
);
}
}
return $components;
}
protected function handleIndexComponents($str)
{
if ($str->endsWith('.index')) {
return $str->replaceLast('.index', '');
}
if (!$str->contains('.')) {
return $str;
}
$parts = $str->explode('.');
if ($parts->slice(-2)->unique()->count() === 1) {
$parts->pop();
return str($parts->implode('.'));
}
return $str;
}
protected function getNamespaced()
{
$namespaced = \Illuminate\Support\Facades\Blade::getClassComponentNamespaces();
$components = [];
foreach ($namespaced as $key => $classNamespace) {
$path = $this->getNamespacePath($classNamespace);
if (!$path) {
continue;
}
array_push(
$components,
...$this->findFiles(
$path,
'php',
fn($k) => $k->kebab()->prepend($key . "::"),
)
);
}
return $components;
}
protected function getNamespacePath($classNamespace)
{
foreach ($this->autoloaded as $ns => $paths) {
if (!str_starts_with($classNamespace, $ns)) {
continue;
}
foreach ($paths as $p) {
$dir = str($classNamespace)
->replace($ns, '')
->replace('\\', '/')
->prepend($p . DIRECTORY_SEPARATOR)
->toString();
if (is_dir($dir)) {
return $dir;
}
}
return null;
}
return null;
}
protected function setProps($groups)
{
try {
$compiler = app('blade.compiler');
} catch (\Throwable $e) {
return $groups;
}
return $groups->map(function ($group) use ($compiler) {
return $group->transform(function ($component) use ($compiler) {
if (isset($component['props'])) {
return $component;
}
if (!str($component['path'])->endsWith('.blade.php')) {
return $component;
}
if (! $props = $this->parseProps($compiler, $component)) {
return $component;
}
return array_merge($component, ['props' => $props]);
});
});
}
protected function parseProps($compiler, array $component): ?string
{
$content = file_get_contents(base_path($component['path']));
$result = '';
$compiler->directive('props', function ($expression) use (&$result) {
return $result = $expression;
});
$compiler->compileString($content);
if (empty($result)) {
return null;
}
return '@props('.$result.')';
}
};
echo json_encode($components->all());
echo LaravelVsCode::outputMarker('END_OUTPUT');
exit(0);

View File

@@ -0,0 +1,350 @@
<?php
error_reporting(E_ERROR | E_PARSE);
define('LARAVEL_START', microtime(true));
require_once __DIR__ . '/../autoload.php';
class LaravelVsCode
{
public static function relativePath($path)
{
if (!str_contains($path, base_path())) {
return (string) $path;
}
return ltrim(str_replace(base_path(), '', realpath($path) ?: $path), DIRECTORY_SEPARATOR);
}
public static function isVendor($path)
{
return str_contains($path, base_path("vendor"));
}
public static function outputMarker($key)
{
return '__VSCODE_LARAVEL_' . $key . '__';
}
public static function startupError(\Throwable $e)
{
throw new Error(self::outputMarker('STARTUP_ERROR') . ': ' . $e->getMessage());
}
}
try {
$app = require_once __DIR__ . '/../../bootstrap/app.php';
} catch (\Throwable $e) {
LaravelVsCode::startupError($e);
exit(1);
}
$app->register(new class($app) extends \Illuminate\Support\ServiceProvider
{
public function boot()
{
config([
'logging.channels.null' => [
'driver' => 'monolog',
'handler' => \Monolog\Handler\NullHandler::class,
],
'logging.default' => 'null',
]);
}
});
try {
$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
$kernel->bootstrap();
} catch (\Throwable $e) {
LaravelVsCode::startupError($e);
exit(1);
}
echo LaravelVsCode::outputMarker('START_OUTPUT');
$livewire = new class {
protected $namespaces;
protected $paths;
protected $extensions = [".blade.php", ".php", ".js", ".global.css", ".css", ".test.php"];
public function __construct()
{
$this->namespaces = collect(
config("livewire.component_namespaces", [])
)->map(LaravelVsCode::relativePath(...));
$this->paths = collect($this->namespaces->values())
->merge(config("livewire.component_locations", []))
->unique()
->map(LaravelVsCode::relativePath(...));
}
public function parse(\Illuminate\Support\Collection $views)
{
return $this->isVersionFour()
? $this->parseLivewireFour($views)
: $this->parseLivewireThree($views);
}
protected function parseLivewireFour(\Illuminate\Support\Collection $views)
{
return $views
->map(function (array $view) {
if (!$this->pathExists($view["path"])) {
return $view;
}
if (is_null($component = $this->getComponent($key = $this->key($view)))) {
return $view;
}
$files = $this->files($view);
if (count($files) === 1 && !str($view['path'])->endsWith('.blade.php')) {
return null;
}
return array_merge($view, [
"key" => $key,
'livewire' => [
'props' => $this->getProps($component),
'files' => $files,
],
]);
})
->whereNotNull()
->unique('key')
->values();
}
protected function parseLivewireThree(\Illuminate\Support\Collection $views)
{
return $views->map(function (array $view) {
if (!str($view['key'])->startsWith('livewire.')) {
return $view;
}
$key = str($view['key'])->after('livewire.')->value();
if (is_null($component = $this->getComponent($key))) {
return $view;
}
return array_merge($view, [
'livewire' => [
'props' => $this->getProps($component),
'files' => [$view['path']],
],
]);
});
}
protected function isVersionFour(): bool
{
return property_exists(\Livewire\LivewireManager::class, "v4") &&
\Livewire\LivewireManager::$v4;
}
protected function pathExists(string $path): bool
{
return $this->paths->contains(fn (string $item) => str($path)->contains($item));
}
protected function getComponent(string $key)
{
try {
return app("livewire")->new($key);
} catch (\Throwable $e) {
return null;
}
}
protected function getProps($component): array
{
return array_map(function ($prop) use ($component) {
$reflection = new \ReflectionProperty($component, $prop);
return [
'name' => $prop,
'type' => (string) $reflection->getType() ?: 'mixed',
'hasDefaultValue' => $reflection->hasDefaultValue(),
'defaultValue' => $this->formatDefaultValue($reflection->getDefaultValue()),
];
}, array_keys($component->all()));
}
protected function formatDefaultValue(mixed $value)
{
return is_string($value) ? "'{$value}'": $value;
}
protected function key(array $view): string
{
return str($view["key"])
->replace("⚡", "")
->when($this->isMfc($view), fn ($key) => $key->beforeLast("."))
->value();
}
protected function files(array $view): array
{
if (! $this->isMfc($view)) {
return [$view['path']];
}
$filePathWithoutExtension = str($view["path"])->replace($this->extensions, "");
return collect($this->extensions)
->map(fn (string $extension) => $filePathWithoutExtension->append($extension))
->filter(fn (string $path) => \Illuminate\Support\Facades\File::exists($path))
->all();
}
protected function isMfc(array $view): bool
{
$directory = str($view["path"])
->replace("⚡", "")
->dirname()
->afterLast(DIRECTORY_SEPARATOR);
$file = str($view["path"])
->replace("⚡", "")
->basename()
->replace($this->extensions, "");
$class = str($view["path"])
->dirname()
->append(DIRECTORY_SEPARATOR . $file . ".php");
return $directory->is($file)
&& \Illuminate\Support\Facades\File::exists($class);
}
};
$blade = new class ($livewire) {
public function __construct(protected $livewire)
{
//
}
public function getAllViews()
{
$finder = app("view")->getFinder();
$paths = collect($finder->getPaths())->flatMap(fn($path) => $this->findViews($path));
$hints = collect($finder->getHints())
->filter(
fn ($_, $key) => ! (strlen($key) === 32 && ctype_xdigit($key))
)->flatMap(
fn($paths, $key) => collect($paths)->flatMap(
fn($path) => collect($this->findViews($path))->map(
fn($value) => array_merge($value, ["key" => "{$key}::{$value["key"]}"])
)
)
);
[$local, $vendor] = $paths
->merge($hints)
->values()
->partition(fn($v) => !$v["isVendor"]);
return $local
->sortBy("key", SORT_NATURAL)
->merge($vendor->sortBy("key", SORT_NATURAL))
->pipe($this->livewire->parse(...));
}
public function getAllComponents()
{
$namespaced = \Illuminate\Support\Facades\Blade::getClassComponentNamespaces();
$autoloaded = require base_path("vendor/composer/autoload_psr4.php");
$components = [];
foreach ($namespaced as $key => $ns) {
$path = null;
foreach ($autoloaded as $namespace => $paths) {
if (str_starts_with($ns, $namespace)) {
foreach ($paths as $p) {
$test = str($ns)->replace($namespace, '')->replace('\\', '/')->prepend($p . DIRECTORY_SEPARATOR)->toString();
if (is_dir($test)) {
$path = $test;
break;
}
}
break;
}
}
if (!$path) {
continue;
}
$files = \Symfony\Component\Finder\Finder::create()
->files()
->name("*.php")
->in($path);
foreach ($files as $file) {
$realPath = $file->getRealPath();
$components[] = [
"path" => LaravelVsCode::relativePath($realPath),
"isVendor" => str_contains($realPath, base_path("vendor")),
"key" => str($realPath)
->replace(realpath($path), "")
->replace(".php", "")
->ltrim(DIRECTORY_SEPARATOR)
->replace(DIRECTORY_SEPARATOR, ".")
->kebab()
->prepend($key . "::"),
];
}
}
return $components;
}
protected function findViews($path)
{
$paths = [];
if (!is_dir($path)) {
return $paths;
}
$finder = app("view")->getFinder();
$extensions = array_map(fn($extension) => ".{$extension}", $finder->getExtensions());
$files = \Symfony\Component\Finder\Finder::create()
->files()
->name(array_map(fn ($ext) => "*{$ext}", $extensions))
->in($path);
foreach ($files as $file) {
$paths[] = [
"path" => LaravelVsCode::relativePath($file->getRealPath()),
"isVendor" => str_contains($file->getRealPath(), base_path("vendor")),
"key" => str($file->getRealPath())
->replace(realpath($path), "")
->replace($extensions, "")
->ltrim(DIRECTORY_SEPARATOR)
->replace(DIRECTORY_SEPARATOR, "."),
];
}
return $paths;
}
};
echo json_encode($blade->getAllViews()->merge($blade->getAllComponents()));
echo LaravelVsCode::outputMarker('END_OUTPUT');
exit(0);