signature = 'bundle:validate { --no-css : Do not validate CSS } { --no-html : Do not validate HTML } { --no-json : Do not validate JSON } { --r|recursive : Also validate sub-bundles } { --source-disk= : Use specified content disk - Defaults to ' . env('CONTENT_DISK') . ' } { path? : Path to a specific bundle to validate - Default to / } '; parent::__construct(); } /** * Execute the console command. */ public function handle() { $this->selectDisk() ->validateCss() ->selectBundles() ->validateJson() ->validateHtml() ->showReport(); if ( !empty($this->invalidCss) || !empty($this->invalidJson) || !empty($this->invalidHtml) ) { exit(1); } } /** * Return an associative array containing available bundles metadata files * as keys and json-decoded validation schemas as values */ private function getJsonValidators() { return array_map(function ($path) { return json_decode(file_get_contents($path)); }, config('json_validator')); } /** * Perform json validation on selected bundles */ private function validateJson(): self { if ($this->option('no-json')) { return $this; } $this->line('Validating JSON...'); $bundles = $this->bundles; $validators = $this->getJsonValidators(); progress( label: 'Validating... ', steps: $bundles, callback: fn (Bundle $bundle, $progress) => $this->handleBundleJsonValidation($bundle, $progress, $validators) ); return $this; } /** * Handle specific bundle */ private function handleBundleJsonValidation(Bundle $bundle, $progress, $validators) { foreach ($validators as $metadataFilename => $schema) { $filepath = $bundle->metadata($metadataFilename)->getFilename(); if (!$this->sourceDisk->exists($filepath)) { continue; } $source = $this->sourceDisk->get($filepath) ?? '{}'; $data = json_decode($source); $validator = new Validator(); $validator->validate($data, $schema); if (!$validator->isValid()) { $this->invalidJson[$bundle->getPath()] = [ 'metadata' => $metadataFilename, 'errors' => $validator->getErrors(), ]; } } $progress->label(sprintf('Validated %s', $bundle->getPath())); } private function validateCss(): self { if ($this->option('no-css')) { return $this; } $this->line('Validating CSS...'); $file = public_path(Vite::asset('resources/css/app.css')); $cacheKey = sprintf('css_validation_%s', Str::slug($file)); $result = Cache::remember($cacheKey, now()->addMonth(), function () use ($file) { $content = file_get_contents($file); return HtmlValidator::validateCss($content); }); $errors = collect($result['messages'])->where('type', '=', 'error'); if ($errors->count() > 0) { $this->invalidCss = $result; Cache::forever('css_is_valid', false); } else { Cache::forever('css_is_valid', true); } return $this; } /** * Perform json validation on selected bundles */ private function validateHtml(): self { if ($this->option('no-html')) { return $this; } $this->line('Validating HTML...'); $bundles = $this->bundles; progress( label: 'Validating... ', steps: $bundles, callback: fn (Bundle $bundle, $progress) => $this->handleBundleHtmlValidation($bundle, $progress) ); return $this; } private function handleBundleHtmlValidation(Bundle $bundle, $progress) { $rendered = $bundle->render(); $changed = false; foreach ($rendered as $path => $content) { $result = HtmlValidator::validateHtml($content); $errors = collect($result['messages'])->where('type', '=', 'error'); $valid = $errors->count() === 0; $cacheKey = sprintf('html_is_valid_%s', Str::slug($path)); if (Cache::get($cacheKey) !== $valid) { $changed = true; Cache::forever($cacheKey, $valid); } if (!$valid) { $this->invalidHtml[$path] = $result; } } if ($changed) { $bundle->touch(); } $progress->label(sprintf('Validated %s', $bundle->getPath())); } /** * Show a report in case of need */ private function showReport() { $this->showCssReport(); $this->showJsonReport(); $this->showHtmlReport(); } /** * Show a report in case of need */ private function showJsonReport() { if ($this->option('no-json')) { return $this; } if (empty($this->invalidJson)) { $this->info('Checked JSON files are valid'); } else { $count = count($this->invalidJson); $this->error(sprintf('%d invalid JSON %s reported', $count, Str::plural('file', $count))); $this->newLine(2); foreach ($this->invalidJson as $bundlePath => $data) { $this->line($bundlePath); $this->line(sprintf(' > In %s:', $data['metadata'])); foreach ($data['errors'] as $error) { $this->line(sprintf(' - %s', $error['message'])); } } } } private function showCssReport() { if ($this->option('no-css')) { return $this; } if (empty($this->invalidCss)) { $this->info('Checked CSS files are valid'); } else { $count = count($this->invalidCss); $this->error(sprintf('%d CSS %s reported', $count, Str::plural('error', $count))); $this->newLine(); $data = $this->invalidCss; foreach ($data['messages'] as $message) { $type = $message['type']; $subType = $message['subType'] ?? $type; $decoration = match ($subType) { 'error' => 'error', 'info' => 'comment', 'warning' => 'comment' }; $this->newLine(); $this->line(sprintf(' > <%1$s>%2$s: %3$s', $decoration, $subType, $message['message'])); $this->line(' > > ' . $message['extract']); } } } private function showHtmlReport() { if ($this->option('no-html')) { return $this; } if (empty($this->invalidHtml)) { $this->info('Checked HTML files are valid'); } else { $count = count($this->invalidHtml); $this->error(sprintf('%d invalid HTML %s reported', $count, Str::plural('file', $count))); $this->newLine(2); foreach ($this->invalidHtml as $path => $data) { $this->newLine(2); $this->comment($path); foreach ($data['messages'] as $message) { $type = $message['type']; $subType = $message['subType'] ?? $type; $decoration = match ($subType) { 'error' => 'error', 'info' => 'comment', 'warning' => 'comment' }; $this->newLine(); $this->line(sprintf(' > <%1$s>%2$s: %3$s', $decoration, $subType, $message['message'])); $this->line(' > > ' . $message['extract']); } } } } }