|
| 1 | +<?php |
| 2 | + |
| 3 | +namespace App\Console\Commands; |
| 4 | + |
| 5 | +use App\Models\Registry; |
| 6 | +use Illuminate\Console\Command; |
| 7 | +use Illuminate\Support\Facades\Artisan; |
| 8 | +use Illuminate\Support\Facades\File; |
| 9 | +use Illuminate\Support\Str; |
| 10 | + |
| 11 | +class RegistryBuildCommand extends Command |
| 12 | +{ |
| 13 | + /** |
| 14 | + * The name and signature of the console command. |
| 15 | + * |
| 16 | + * @var string |
| 17 | + */ |
| 18 | + protected $signature = 'registry:build'; |
| 19 | + |
| 20 | + /** |
| 21 | + * The console command description. |
| 22 | + * |
| 23 | + * @var string |
| 24 | + */ |
| 25 | + protected $description = 'Scan resources/js/registry directory and auto-generate the RegistrySeeder'; |
| 26 | + |
| 27 | + /** |
| 28 | + * Execute the console command. |
| 29 | + */ |
| 30 | + public function handle(): int |
| 31 | + { |
| 32 | + $this->info('Scanning resources/js/registry/new-york...'); |
| 33 | + |
| 34 | + $registryDir = resource_path('js/registry/new-york'); |
| 35 | + if (! File::isDirectory($registryDir)) { |
| 36 | + $this->error("Registry directory not found at: {$registryDir}"); |
| 37 | + |
| 38 | + return Command::FAILURE; |
| 39 | + } |
| 40 | + |
| 41 | + $files = File::allFiles($registryDir); |
| 42 | + $registryItems = []; |
| 43 | + |
| 44 | + foreach ($files as $file) { |
| 45 | + $relativePath = 'resources/js/registry/new-york/'.str_replace('\\', '/', $file->getRelativePathname()); |
| 46 | + $pathParts = explode('/', str_replace('\\', '/', $file->getRelativePath())); |
| 47 | + $filename = $file->getFilenameWithoutExtension(); |
| 48 | + |
| 49 | + if (empty($pathParts[0])) { |
| 50 | + continue; |
| 51 | + } |
| 52 | + |
| 53 | + $itemType = 'registry:hook'; |
| 54 | + $category = $pathParts[0]; |
| 55 | + |
| 56 | + if ($pathParts[0] === 'hooks') { |
| 57 | + $itemType = 'registry:hook'; |
| 58 | + $category = 'hooks'; |
| 59 | + } elseif ($pathParts[0] === 'lib') { |
| 60 | + $itemType = 'registry:lib'; |
| 61 | + $category = 'lib'; |
| 62 | + } elseif ($pathParts[0] === 'components') { |
| 63 | + if (isset($pathParts[1]) && $pathParts[1] === 'ui') { |
| 64 | + $itemType = 'registry:ui'; |
| 65 | + $category = $pathParts[2] ?? 'components'; |
| 66 | + } else { |
| 67 | + $itemType = 'registry:block'; |
| 68 | + $category = 'blocks'; |
| 69 | + } |
| 70 | + } |
| 71 | + |
| 72 | + $name = ($itemType === 'registry:block') ? ($pathParts[1] ?? $filename) : $filename; |
| 73 | + |
| 74 | + if (isset($registryItems[$name])) { |
| 75 | + $registryItems[$name]['files'][] = [ |
| 76 | + 'path' => $relativePath, |
| 77 | + 'type' => $itemType, |
| 78 | + ]; |
| 79 | + if (in_array($itemType, ['registry:ui', 'registry:block'], true)) { |
| 80 | + $registryItems[$name]['type'] = $itemType; |
| 81 | + $registryItems[$name]['categories'] = [$category]; |
| 82 | + } |
| 83 | + } else { |
| 84 | + $registryItems[$name] = [ |
| 85 | + 'name' => $name, |
| 86 | + 'title' => Str::headline($name), |
| 87 | + 'type' => $itemType, |
| 88 | + 'categories' => [$category], |
| 89 | + 'files' => [ |
| 90 | + [ |
| 91 | + 'path' => $relativePath, |
| 92 | + 'type' => $itemType, |
| 93 | + ], |
| 94 | + ], |
| 95 | + ]; |
| 96 | + } |
| 97 | + } |
| 98 | + |
| 99 | + $this->info('Resolving file contents and dependencies...'); |
| 100 | + |
| 101 | + foreach ($registryItems as $name => &$item) { |
| 102 | + $dependencies = []; |
| 103 | + $registryDependencies = []; |
| 104 | + |
| 105 | + foreach ($item['files'] as &$fileInfo) { |
| 106 | + $filePath = base_path($fileInfo['path']); |
| 107 | + if (! File::exists($filePath)) { |
| 108 | + continue; |
| 109 | + } |
| 110 | + $content = File::get($filePath); |
| 111 | + $fileInfo['content'] = $content; |
| 112 | + |
| 113 | + // Parse import statements |
| 114 | + preg_match_all('/import\s+(?:[^"\']*?\s+from\s+)?["\'"]([^"\']+)["\'"]/', $content, $matches); |
| 115 | + |
| 116 | + if (! empty($matches[1])) { |
| 117 | + foreach ($matches[1] as $importPath) { |
| 118 | + if (str_starts_with($importPath, '@/registry/new-york/')) { |
| 119 | + // Local registry import |
| 120 | + $depName = basename($importPath); |
| 121 | + $depName = preg_replace('/\.(tsx|ts|js|jsx)$/', '', $depName); |
| 122 | + |
| 123 | + // Skip if it's an internal file of the same block |
| 124 | + $isInternal = false; |
| 125 | + foreach ($item['files'] as $f) { |
| 126 | + $fName = basename($f['path']); |
| 127 | + $fName = preg_replace('/\.(tsx|ts|js|jsx)$/', '', $fName); |
| 128 | + if ($fName === $depName) { |
| 129 | + $isInternal = true; |
| 130 | + break; |
| 131 | + } |
| 132 | + } |
| 133 | + |
| 134 | + if (! $isInternal) { |
| 135 | + $registryDependencies[] = $depName; |
| 136 | + } |
| 137 | + } elseif (str_starts_with($importPath, '@/components/ui/')) { |
| 138 | + $depName = basename($importPath); |
| 139 | + $depName = preg_replace('/\.(tsx|ts|js|jsx)$/', '', $depName); |
| 140 | + $registryDependencies[] = $depName; |
| 141 | + } elseif (str_starts_with($importPath, '@/lib/utils')) { |
| 142 | + $registryDependencies[] = 'utils'; |
| 143 | + } elseif (! str_starts_with($importPath, '.') && ! str_starts_with($importPath, '@/')) { |
| 144 | + // NPM Dependency |
| 145 | + $parts = explode('/', $importPath); |
| 146 | + $packageName = $parts[0]; |
| 147 | + if (str_starts_with($packageName, '@') && isset($parts[1])) { |
| 148 | + $packageName .= '/'.$parts[1]; |
| 149 | + } |
| 150 | + |
| 151 | + if (! in_array($packageName, ['react', 'react-dom'])) { |
| 152 | + $dependencies[] = $packageName; |
| 153 | + } |
| 154 | + } |
| 155 | + } |
| 156 | + } |
| 157 | + } |
| 158 | + |
| 159 | + $item['dependencies'] = array_values(array_unique($dependencies)); |
| 160 | + $item['registryDependencies'] = array_values(array_unique($registryDependencies)); |
| 161 | + $item['author'] = 'designbycode'; |
| 162 | + $item['meta'] = [ |
| 163 | + 'category' => $item['categories'][0] ?? 'components', |
| 164 | + 'version' => '1.0.0', |
| 165 | + ]; |
| 166 | + } |
| 167 | + unset($item); |
| 168 | + |
| 169 | + // Resolve local registryDependencies to full URLs |
| 170 | + $scannedNames = array_keys($registryItems); |
| 171 | + foreach ($registryItems as $name => &$item) { |
| 172 | + $resolvedRegDeps = []; |
| 173 | + foreach ($item['registryDependencies'] as $dep) { |
| 174 | + if (in_array($dep, $scannedNames, true)) { |
| 175 | + $resolvedRegDeps[] = url("r/{$dep}.json"); |
| 176 | + } else { |
| 177 | + $resolvedRegDeps[] = $dep; |
| 178 | + } |
| 179 | + } |
| 180 | + $item['registryDependencies'] = $resolvedRegDeps; |
| 181 | + } |
| 182 | + unset($item); |
| 183 | + |
| 184 | + $databaseRecords = []; |
| 185 | + foreach ($registryItems as $name => $item) { |
| 186 | + try { |
| 187 | + $parsed = Registry::fromRegistry($item); |
| 188 | + $attributes = $parsed->toArray(); |
| 189 | + unset($attributes['id'], $attributes['created_at'], $attributes['updated_at'], $attributes['deleted_at'], $attributes['user_id']); |
| 190 | + $databaseRecords[] = $attributes; |
| 191 | + } catch (\Exception $e) { |
| 192 | + $this->error("Failed to parse registry item [{$name}]: ".$e->getMessage()); |
| 193 | + } |
| 194 | + } |
| 195 | + |
| 196 | + $this->info('Generating RegistrySeeder.php...'); |
| 197 | + |
| 198 | + $itemsPhpCode = $this->exportArray($databaseRecords, 12); |
| 199 | + |
| 200 | + $seederTemplate = <<<PHP |
| 201 | +<?php |
| 202 | +
|
| 203 | +namespace Database\Seeders; |
| 204 | +
|
| 205 | +use App\Models\Registry; |
| 206 | +use App\Models\User; |
| 207 | +use Illuminate\Database\Seeder; |
| 208 | +
|
| 209 | +class RegistrySeeder extends Seeder |
| 210 | +{ |
| 211 | + /** |
| 212 | + * Run the database seeds. |
| 213 | + */ |
| 214 | + public function run(): void |
| 215 | + { |
| 216 | + \$userId = User::first()?->id ?? 1; |
| 217 | +
|
| 218 | + \$items = [ |
| 219 | +{$itemsPhpCode} |
| 220 | + ]; |
| 221 | +
|
| 222 | + \$total = 0; |
| 223 | +
|
| 224 | + foreach (\$items as \$item) { |
| 225 | + Registry::updateOrCreate( |
| 226 | + ['name' => \$item['name']], |
| 227 | + array_merge(\$item, ['user_id' => \$userId]) |
| 228 | + ); |
| 229 | + \$total++; |
| 230 | + } |
| 231 | +
|
| 232 | + \$this->command->info("Seeded {\$total} registry items."); |
| 233 | + } |
| 234 | +} |
| 235 | +PHP; |
| 236 | + |
| 237 | + $seederPath = database_path('seeders/RegistrySeeder.php'); |
| 238 | + File::put($seederPath, $seederTemplate); |
| 239 | + |
| 240 | + $this->info("RegistrySeeder.php generated successfully at {$seederPath}."); |
| 241 | + |
| 242 | + $this->info('Running RegistrySeeder to seed/update database...'); |
| 243 | + Artisan::call('db:seed', [ |
| 244 | + '--class' => 'RegistrySeeder', |
| 245 | + '--force' => true, |
| 246 | + ]); |
| 247 | + |
| 248 | + $this->info(Artisan::output()); |
| 249 | + $this->info('Registry build completed successfully!'); |
| 250 | + |
| 251 | + return Command::SUCCESS; |
| 252 | + } |
| 253 | + |
| 254 | + /** |
| 255 | + * Export array to PHP array string with short syntax. |
| 256 | + */ |
| 257 | + private function exportArray(array $array, int $indent = 12): string |
| 258 | + { |
| 259 | + $indentStr = str_repeat(' ', $indent); |
| 260 | + $lines = []; |
| 261 | + $isAssociative = array_keys($array) !== range(0, count($array) - 1); |
| 262 | + |
| 263 | + foreach ($array as $key => $value) { |
| 264 | + $renderedKey = $isAssociative ? var_export($key, true).' => ' : ''; |
| 265 | + if (is_array($value)) { |
| 266 | + $renderedValue = "[\n".$this->exportArray($value, $indent + 4)."\n".str_repeat(' ', $indent).']'; |
| 267 | + } elseif (is_null($value)) { |
| 268 | + $renderedValue = 'null'; |
| 269 | + } else { |
| 270 | + $renderedValue = var_export($value, true); |
| 271 | + } |
| 272 | + $lines[] = $indentStr.$renderedKey.$renderedValue.','; |
| 273 | + } |
| 274 | + |
| 275 | + return implode("\n", $lines); |
| 276 | + } |
| 277 | +} |
0 commit comments