Skip to content

Add component properties from the @props directive to the repository #367

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 26 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
cb0aebd
Fix a bug with overrides vendor blade components
N1ebieski Apr 4, 2025
2003d2c
Merge branch 'main' of https://github.com/laravel/vs-code-extension i…
N1ebieski Apr 4, 2025
0dae2a6
Add component props from the @props directive to the repository
N1ebieski Apr 13, 2025
2daa589
fix undefined type
N1ebieski Apr 13, 2025
7248197
rename undefined to mixed
N1ebieski Apr 13, 2025
944e79b
fix for array items without key
N1ebieski Apr 13, 2025
0fb0c6b
refactoring
N1ebieski Apr 13, 2025
cdd76cf
Merge branch 'Fix-a-bug-with-overrides-vendor-blade-components-#33' i…
N1ebieski Apr 13, 2025
8bbd54b
fix for flux components
N1ebieski Apr 13, 2025
7b372fd
Merge branch 'Fix-a-bug-with-overrides-vendor-blade-components-#33' i…
N1ebieski Apr 13, 2025
3f13d29
fix null type
N1ebieski Apr 13, 2025
7a026f1
Fix a bug with overrides vendor blade components
N1ebieski Apr 13, 2025
a41d9c3
Merge branch 'Fix-a-bug-with-overrides-vendor-blade-components-#33' i…
N1ebieski Apr 13, 2025
7d695d5
Add component props from @props directive to repository
N1ebieski Apr 13, 2025
ef5cf36
Revert "fix for flux components"
N1ebieski Apr 13, 2025
35f31db
revert
N1ebieski Apr 13, 2025
427ac8d
Merge branch 'Fix-a-bug-with-overrides-vendor-blade-components-#33' i…
N1ebieski Apr 13, 2025
2b740ed
Add component props from @props directive to repository
N1ebieski Apr 13, 2025
559e4e5
wip
N1ebieski Apr 13, 2025
58f8dda
wip
N1ebieski Apr 13, 2025
55935d8
Add component props from @props directive to repository
N1ebieski Apr 13, 2025
565bf6b
refactoring
N1ebieski Apr 13, 2025
6fa1fa3
refactoring
N1ebieski Apr 14, 2025
f6ae23b
remove props duplicates
N1ebieski Apr 14, 2025
72a3d94
support for older versions of laravel
N1ebieski Apr 24, 2025
7ac6206
Merge branch 'main' of https://github.com/laravel/vs-code-extension i…
N1ebieski Jun 11, 2025
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 generate-templates.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
$content = file_get_contents($template);

$content = str_replace('\\', '\\\\', $content);
$content = str_replace('<?php', '', $content);
$content = preg_replace('/^<\?php/', '', $content);
$content = implode("\n", [
"// This file was generated from {$template}, do not edit directly",
'export default `',
Expand Down
236 changes: 219 additions & 17 deletions php-templates/blade-components.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public function all()
))->groupBy('key')->map(fn($items) => [
'isVendor' => $items->first()['isVendor'],
'paths' => $items->pluck('path')->values(),
'props' => $items->pluck('props')->values()->filter()->flatMap(fn($i) => $i),
'props' => $items->pluck('props')->unique()->values()->filter()->flatMap(fn($i) => $i),
]);

return [
Expand All @@ -32,11 +32,209 @@ public function all()
];
}

private function runConcurrently(\Illuminate\Support\Collection $items, \Closure $callback, int $concurrency = 8): array
{
if (app()->version() > 11 && \Composer\InstalledVersions::isInstalled('spatie/fork')) {
$tasks = $items
->split($concurrency)
->map(fn (\Illuminate\Support\Collection $chunk) => fn (): array => $callback($chunk))
->toArray();

$results = \Illuminate\Support\Facades\Concurrency::driver('fork')->run($tasks);

return array_merge(...$results);
}

return $callback($items);
}

private function getComponentPropsFromDirective(string $path): array
{
if (!\Illuminate\Support\Facades\File::exists($path)) {
return [];
}

$contents = \Illuminate\Support\Facades\File::get($path);

$match = str($contents)->match('/\@props\(\[(.*?)\]\)/s');

if ($match->isEmpty()) {
return [];
}

$parser = (new \PhpParser\ParserFactory)->createForNewestSupportedVersion();

$propsAsString = $match->wrap('[', ']')->toString();

try {
$ast = $parser->parse("<?php return {$propsAsString};");
} catch (\Throwable $e) {
return [];
}

$traverser = new \PhpParser\NodeTraverser();
$visitor = new class extends \PhpParser\NodeVisitorAbstract {
public array $props = [];

private function getClassConstNodeValue(\PhpParser\Node\Expr\ClassConstFetch $node): string
{
return match (true) {
$node->name instanceof \PhpParser\Node\Identifier => "{$node->class->toString()}::{$node->name->toString()}",
default => $node->class->toString(),
};
}

private function getConstNodeValue(\PhpParser\Node\Expr\ConstFetch $node): string
{
return $node->name->toString();
}

private function getStringNodeValue(\PhpParser\Node\Scalar\String_ $node): string
{
return $node->value;
}

private function getIntNodeValue(\PhpParser\Node\Scalar\Int_ $node): int
{
return $node->value;
}

private function getFloatNodeValue(\PhpParser\Node\Scalar\Float_ $node): float
{
return $node->value;
}

private function getNodeValue(\PhpParser\Node $node): mixed
{
return match (true) {
$node instanceof \PhpParser\Node\Expr\ConstFetch => $this->getConstNodeValue($node),
$node instanceof \PhpParser\Node\Expr\ClassConstFetch => $this->getClassConstNodeValue($node),
$node instanceof \PhpParser\Node\Scalar\String_ => $this->getStringNodeValue($node),
$node instanceof \PhpParser\Node\Scalar\Int_ => $this->getIntNodeValue($node),
$node instanceof \PhpParser\Node\Scalar\Float_ => $this->getFloatNodeValue($node),
$node instanceof \PhpParser\Node\Expr\Array_ => $this->getArrayNodeValue($node),
$node instanceof \PhpParser\Node\Expr\New_ => $this->getObjectNodeValue($node),
default => null
};
}

private function getObjectNodeValue(\PhpParser\Node\Expr\New_ $node): array
{
if (! $node->class instanceof \PhpParser\Node\Stmt\Class_) {
return [];
}

$array = [];

foreach ($node->class->getProperties() as $property) {
foreach ($property->props as $item) {
$array[$item->name->name] = $this->getNodeValue($item->default);
}
}

return array_filter($array);
}

private function getArrayNodeValue(\PhpParser\Node\Expr\Array_ $node): array
{
$array = [];
$i = 0;

foreach ($node->items as $item) {
$value = $this->getNodeValue($item->value);

$array[$item->key?->value ?? $i++] = $value;
}

return array_filter($array);
}

public function enterNode(\PhpParser\Node $node) {
if (
$node instanceof \PhpParser\Node\Stmt\Return_
&& $node->expr instanceof \PhpParser\Node\Expr\Array_
) {
foreach ($node->expr->items as $item) {
$this->props[] = match (true) {
$item->value instanceof \PhpParser\Node\Scalar\String_ => [
'name' => \Illuminate\Support\Str::kebab($item->key?->value ?? $item->value->value),
'type' => $item->key ? 'string' : 'mixed',
'hasDefault' => $item->key ? true : false,
'default' => $item->key ? $this->getStringNodeValue($item->value) : null,
],
$item->value instanceof \PhpParser\Node\Expr\ConstFetch => [
'name' => \Illuminate\Support\Str::kebab($item->key->value),
'type' => $item->value->name->toString() !== 'null' ? 'boolean' : 'mixed',
'hasDefault' => true,
'default' => $this->getConstNodeValue($item->value),
],
$item->value instanceof \PhpParser\Node\Expr\ClassConstFetch => [
'name' => \Illuminate\Support\Str::kebab($item->key->value),
'type' => $item->value->class->toString(),
'hasDefault' => true,
'default' => $this->getClassConstNodeValue($item->value),
],
$item->value instanceof \PhpParser\Node\Scalar\Int_ => [
'name' => \Illuminate\Support\Str::kebab($item->key->value),
'type' => 'integer',
'hasDefault' => true,
'default' => $this->getIntNodeValue($item->value),
],
$item->value instanceof \PhpParser\Node\Scalar\Float_ => [
'name' => \Illuminate\Support\Str::kebab($item->key->value),
'type' => 'float',
'hasDefault' => true,
'default' => $this->getFloatNodeValue($item->value),
],
$item->value instanceof \PhpParser\Node\Expr\Array_ => [
'name' => \Illuminate\Support\Str::kebab($item->key->value),
'type' => 'array',
'hasDefault' => true,
'default' => $this->getArrayNodeValue($item->value),
],
$item->value instanceof \PhpParser\Node\Expr\New_ => [
'name' => \Illuminate\Support\Str::kebab($item->key->value),
'type' => $item->value->class->toString(),
'hasDefault' => true,
'default' => $this->getObjectNodeValue($item->value),
],
default => null
};
}
}
}
};
$traverser->addVisitor($visitor);
$traverser->traverse($ast);

return array_filter($visitor->props);
}

private function mapComponentPropsFromDirective(array $files): array
{
if (! \Composer\InstalledVersions::isInstalled('nikic/php-parser')) {
return $files;
}

return $this->runConcurrently(
collect($files),
fn (\Illuminate\Support\Collection $files): array => $files->map(function (array $item): array {
$props = $this->getComponentPropsFromDirective($item['path']);

if ($props !== []) {
$item['props'] = $props;
}

return $item;
})->all()
);
}

protected function getStandardViews()
{
$path = resource_path('views/components');

return $this->findFiles($path, 'blade.php');
return $this->mapComponentPropsFromDirective($this->findFiles($path, 'blade.php'));
}

protected function findFiles($path, $extension, $keyCallback = null)
Expand Down Expand Up @@ -110,6 +308,9 @@ protected function getStandardClasses()
->map(fn($p) => [
'name' => \Illuminate\Support\Str::kebab($p->getName()),
'type' => (string) ($p->getType() ?? 'mixed'),
// We need to add hasDefault, because null can be also a default value,
// it can't be a flag of no default
'hasDefault' => $p->hasDefaultValue(),
'default' => $p->getDefaultValue() ?? $parameters[$p->getName()] ?? null,
]);

Expand Down Expand Up @@ -192,7 +393,7 @@ protected function getAnonymous()
}
}

return $components;
return $this->mapComponentPropsFromDirective($components);
}

protected function getVendorComponents(): array
Expand All @@ -209,24 +410,25 @@ protected function getVendorComponents(): array
$views = $finder->getHints();

foreach ($views as $key => $paths) {
// First is always optional override in the resources/views folder
$path = $paths[0] . '/components';
foreach ($paths as $path) {
$path .= '/components';

if (!is_dir($path)) {
continue;
}
if (!is_dir($path)) {
continue;
}

array_push(
$components,
...$this->findFiles(
$path,
'blade.php',
fn (\Illuminate\Support\Stringable $k) => $k->kebab()->prepend($key.'::'),
)
);
array_push(
$components,
...$this->findFiles(
$path,
'blade.php',
fn (\Illuminate\Support\Stringable $k) => $k->kebab()->prepend($key.'::'),
)
);
}
}

return $components;
return $this->mapComponentPropsFromDirective($components);
}

protected function handleIndexComponents($str)
Expand Down
3 changes: 2 additions & 1 deletion src/features/bladeComponent.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { getBladeComponents } from "@src/repositories/bladeComponents";
import { config } from "@src/support/config";
import { projectPath } from "@src/support/project";
import { defaultToString } from "@src/support/util";
import * as vscode from "vscode";
import { HoverProvider, LinkProvider } from "..";

Expand Down Expand Up @@ -131,7 +132,7 @@ export const hoverProvider: HoverProvider = (
[
"`" + prop.type + "` ",
"`" + prop.name + "`",
prop.default ? ` = ${prop.default}` : "",
prop.hasDefault ? ` = ${defaultToString(prop.default)}` : "",
].join(""),
),
);
Expand Down
1 change: 1 addition & 0 deletions src/repositories/bladeComponents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export interface BladeComponents {
props: {
name: string;
type: string;
hasDefault: boolean;
default: string | null;
}[];
};
Expand Down
21 changes: 21 additions & 0 deletions src/support/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,3 +146,24 @@ export const createIndexMapping = (
},
};
};

export const defaultToString = (value: any): string => {
switch (typeof value) {
case "object":
if (value === null) {
return "null";
}

const json: string = JSON.stringify(value, null, 2);

if (json.length > 1000) {
return "object";
}

return json;
case "function":
return "function";
default:
return value.toString();
}
};
Loading