diff --git a/src/Illuminate/Foundation/Application.php b/src/Illuminate/Foundation/Application.php index 47d8ec45..211fce8e 100755 --- a/src/Illuminate/Foundation/Application.php +++ b/src/Illuminate/Foundation/Application.php @@ -45,7 +45,7 @@ class Application extends Container implements ApplicationContract, CachesConfig * * @var string */ - const VERSION = '11.15.0'; + const VERSION = '11.21.0'; /** * The base path for the Laravel installation. diff --git a/src/Illuminate/Foundation/Configuration/ApplicationBuilder.php b/src/Illuminate/Foundation/Configuration/ApplicationBuilder.php index 218e5210..6065424e 100644 --- a/src/Illuminate/Foundation/Configuration/ApplicationBuilder.php +++ b/src/Illuminate/Foundation/Configuration/ApplicationBuilder.php @@ -203,7 +203,7 @@ protected function buildRoutingCallback(array|string|null $web, } if (is_string($health)) { - Route::middleware('web')->get($health, function () { + Route::get($health, function () { Event::dispatch(new DiagnosingHealth); return View::file(__DIR__.'/../resources/health-up.blade.php'); @@ -300,6 +300,8 @@ protected function withCommandRouting(array $paths) $this->app->afterResolving(ConsoleKernel::class, function ($kernel) use ($paths) { $this->app->booted(fn () => $kernel->addCommandRoutePaths($paths)); }); + + return $this; } /** diff --git a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php index 9802ce10..6639f939 100644 --- a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php +++ b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php @@ -151,7 +151,7 @@ protected function installReverb() } $this->requireComposerPackages($this->option('composer'), [ - 'laravel/reverb:@beta', + 'laravel/reverb:^1.0', ]); $php = (new PhpExecutableFinder())->find(false) ?: 'php'; diff --git a/src/Illuminate/Foundation/Console/ComponentMakeCommand.php b/src/Illuminate/Foundation/Console/ComponentMakeCommand.php index 709c5360..faf86328 100644 --- a/src/Illuminate/Foundation/Console/ComponentMakeCommand.php +++ b/src/Illuminate/Foundation/Console/ComponentMakeCommand.php @@ -63,7 +63,7 @@ public function handle() protected function writeView() { $path = $this->viewPath( - str_replace('.', '/', 'components.'.$this->getView()).'.blade.php' + str_replace('.', '/', $this->getView()).'.blade.php' ); if (! $this->files->isDirectory(dirname($path))) { @@ -104,24 +104,33 @@ protected function buildClass($name) return str_replace( ['DummyView', '{{ view }}'], - 'view(\'components.'.$this->getView().'\')', + 'view(\''.$this->getView().'\')', parent::buildClass($name) ); } /** - * Get the view name relative to the components directory. + * Get the view name relative to the view path. * * @return string view */ protected function getView() { - $name = str_replace('\\', '/', $this->argument('name')); + $segments = explode('/', str_replace('\\', '/', $this->argument('name'))); - return collect(explode('/', $name)) - ->map(function ($part) { - return Str::kebab($part); - }) + $name = array_pop($segments); + + $path = is_string($this->option('path')) + ? explode('/', trim($this->option('path'), '/')) + : [ + 'components', + ...$segments, + ]; + + $path[] = $name; + + return collect($path) + ->map(fn ($segment) => Str::kebab($segment)) ->implode('.'); } @@ -167,9 +176,10 @@ protected function getDefaultNamespace($rootNamespace) protected function getOptions() { return [ - ['force', 'f', InputOption::VALUE_NONE, 'Create the class even if the component already exists'], ['inline', null, InputOption::VALUE_NONE, 'Create a component that renders an inline view'], ['view', null, InputOption::VALUE_NONE, 'Create an anonymous component with only a view'], + ['path', null, InputOption::VALUE_REQUIRED, 'The location where the component view should be created'], + ['force', 'f', InputOption::VALUE_NONE, 'Create the class even if the component already exists'], ]; } } diff --git a/src/Illuminate/Foundation/Console/ConfigShowCommand.php b/src/Illuminate/Foundation/Console/ConfigShowCommand.php index 2c571214..d3dd580e 100644 --- a/src/Illuminate/Foundation/Console/ConfigShowCommand.php +++ b/src/Illuminate/Foundation/Console/ConfigShowCommand.php @@ -33,9 +33,7 @@ public function handle() $config = $this->argument('config'); if (! config()->has($config)) { - $this->components->error("Configuration file or key {$config} does not exist."); - - return Command::FAILURE; + $this->fail("Configuration file or key {$config} does not exist."); } $this->newLine(); diff --git a/src/Illuminate/Foundation/Console/DownCommand.php b/src/Illuminate/Foundation/Console/DownCommand.php index 10d7dbfd..86b2c284 100644 --- a/src/Illuminate/Foundation/Console/DownCommand.php +++ b/src/Illuminate/Foundation/Console/DownCommand.php @@ -87,7 +87,7 @@ protected function getDownFilePayload() 'retry' => $this->getRetryTime(), 'refresh' => $this->option('refresh'), 'secret' => $this->getSecret(), - 'status' => (int) $this->option('status', 503), + 'status' => (int) ($this->option('status') ?? 503), 'template' => $this->option('render') ? $this->prerenderView() : null, ]; } diff --git a/src/Illuminate/Foundation/Console/EnvironmentDecryptCommand.php b/src/Illuminate/Foundation/Console/EnvironmentDecryptCommand.php index a173388f..d09d97d9 100644 --- a/src/Illuminate/Foundation/Console/EnvironmentDecryptCommand.php +++ b/src/Illuminate/Foundation/Console/EnvironmentDecryptCommand.php @@ -63,9 +63,7 @@ public function handle() $key = $this->option('key') ?: Env::get('LARAVEL_ENV_ENCRYPTION_KEY'); if (! $key) { - $this->components->error('A decryption key is required.'); - - return Command::FAILURE; + $this->fail('A decryption key is required.'); } $cipher = $this->option('cipher') ?: 'AES-256-CBC'; @@ -79,21 +77,15 @@ public function handle() $outputFile = $this->outputFilePath(); if (Str::endsWith($outputFile, '.encrypted')) { - $this->components->error('Invalid filename.'); - - return Command::FAILURE; + $this->fail('Invalid filename.'); } if (! $this->files->exists($encryptedFile)) { - $this->components->error('Encrypted environment file not found.'); - - return Command::FAILURE; + $this->fail('Encrypted environment file not found.'); } if ($this->files->exists($outputFile) && ! $this->option('force')) { - $this->components->error('Environment file already exists.'); - - return Command::FAILURE; + $this->fail('Environment file already exists.'); } try { @@ -104,9 +96,7 @@ public function handle() $encrypter->decrypt($this->files->get($encryptedFile)) ); } catch (Exception $e) { - $this->components->error($e->getMessage()); - - return Command::FAILURE; + $this->fail($e->getMessage()); } $this->components->info('Environment successfully decrypted.'); diff --git a/src/Illuminate/Foundation/Console/EnvironmentEncryptCommand.php b/src/Illuminate/Foundation/Console/EnvironmentEncryptCommand.php index d4e7f6aa..df0c038a 100644 --- a/src/Illuminate/Foundation/Console/EnvironmentEncryptCommand.php +++ b/src/Illuminate/Foundation/Console/EnvironmentEncryptCommand.php @@ -75,15 +75,11 @@ public function handle() } if (! $this->files->exists($environmentFile)) { - $this->components->error('Environment file not found.'); - - return Command::FAILURE; + $this->fail('Environment file not found.'); } if ($this->files->exists($encryptedFile) && ! $this->option('force')) { - $this->components->error('Encrypted environment file already exists.'); - - return Command::FAILURE; + $this->fail('Encrypted environment file already exists.'); } try { @@ -94,9 +90,7 @@ public function handle() $encrypter->encrypt($this->files->get($environmentFile)) ); } catch (Exception $e) { - $this->components->error($e->getMessage()); - - return Command::FAILURE; + $this->fail($e->getMessage()); } if ($this->option('prune')) { diff --git a/src/Illuminate/Foundation/Console/Kernel.php b/src/Illuminate/Foundation/Console/Kernel.php index 16fc49d8..eb9405ba 100644 --- a/src/Illuminate/Foundation/Console/Kernel.php +++ b/src/Illuminate/Foundation/Console/Kernel.php @@ -14,6 +14,7 @@ use Illuminate\Contracts\Debug\ExceptionHandler; use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Contracts\Foundation\Application; +use Illuminate\Foundation\Events\Terminating; use Illuminate\Support\Arr; use Illuminate\Support\Carbon; use Illuminate\Support\Env; @@ -212,6 +213,8 @@ public function handle($input, $output = null) */ public function terminate($input, $status) { + $this->events->dispatch(new Terminating); + $this->app->terminate(); if ($this->commandStartedAt === null) { diff --git a/src/Illuminate/Foundation/Console/NotificationMakeCommand.php b/src/Illuminate/Foundation/Console/NotificationMakeCommand.php index d9352655..d6105af9 100644 --- a/src/Illuminate/Foundation/Console/NotificationMakeCommand.php +++ b/src/Illuminate/Foundation/Console/NotificationMakeCommand.php @@ -4,8 +4,14 @@ use Illuminate\Console\Concerns\CreatesMatchingTest; use Illuminate\Console\GeneratorCommand; +use Illuminate\Support\Str; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +use function Laravel\Prompts\confirm; +use function Laravel\Prompts\text; #[AsCommand(name: 'make:notification')] class NotificationMakeCommand extends GeneratorCommand @@ -122,6 +128,33 @@ protected function getDefaultNamespace($rootNamespace) return $rootNamespace.'\Notifications'; } + /** + * Perform actions after the user was prompted for missing arguments. + * + * @param \Symfony\Component\Console\Input\InputInterface $input + * @param \Symfony\Component\Console\Output\OutputInterface $output + * @return void + */ + protected function afterPromptingForMissingArguments(InputInterface $input, OutputInterface $output) + { + if ($this->didReceiveOptions($input)) { + return; + } + + $wantsMarkdownView = confirm('Would you like to create a markdown view?'); + + if ($wantsMarkdownView) { + $defaultMarkdownView = collect(explode('/', str_replace('\\', '/', $this->argument('name')))) + ->map(fn ($path) => Str::kebab($path)) + ->prepend('mail') + ->implode('.'); + + $markdownView = text('What should the markdown view be named?', default: $defaultMarkdownView); + + $input->setOption('markdown', $markdownView); + } + } + /** * Get the console command options. * diff --git a/src/Illuminate/Foundation/Console/VendorPublishCommand.php b/src/Illuminate/Foundation/Console/VendorPublishCommand.php index ca7dc0d0..2fa8dcb8 100644 --- a/src/Illuminate/Foundation/Console/VendorPublishCommand.php +++ b/src/Illuminate/Foundation/Console/VendorPublishCommand.php @@ -309,7 +309,7 @@ protected function publishDirectory($from, $to) */ protected function moveManagedFiles($from, $manager) { - foreach ($manager->listContents('from://', true) as $file) { + foreach ($manager->listContents('from://', true)->sortByPath() as $file) { $path = Str::after($file['path'], 'from://'); if ( diff --git a/src/Illuminate/Foundation/Console/stubs/job.queued.stub b/src/Illuminate/Foundation/Console/stubs/job.queued.stub index 9a7cec52..eb18549d 100644 --- a/src/Illuminate/Foundation/Console/stubs/job.queued.stub +++ b/src/Illuminate/Foundation/Console/stubs/job.queued.stub @@ -2,15 +2,15 @@ namespace {{ namespace }}; -use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; +use Illuminate\Foundation\Queue\Queueable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; class {{ class }} implements ShouldQueue { - use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; + use Queueable; /** * Create a new job instance. diff --git a/src/Illuminate/Foundation/Events/Terminating.php b/src/Illuminate/Foundation/Events/Terminating.php new file mode 100644 index 00000000..a74a21e0 --- /dev/null +++ b/src/Illuminate/Foundation/Events/Terminating.php @@ -0,0 +1,8 @@ +dontReport, $this->internalDontReport); if (! is_null(Arr::first($dontReport, fn ($type) => $e instanceof $type))) { diff --git a/src/Illuminate/Foundation/Http/Kernel.php b/src/Illuminate/Foundation/Http/Kernel.php index 79d5a6d5..3443751f 100644 --- a/src/Illuminate/Foundation/Http/Kernel.php +++ b/src/Illuminate/Foundation/Http/Kernel.php @@ -7,6 +7,7 @@ use Illuminate\Contracts\Debug\ExceptionHandler; use Illuminate\Contracts\Foundation\Application; use Illuminate\Contracts\Http\Kernel as KernelContract; +use Illuminate\Foundation\Events\Terminating; use Illuminate\Foundation\Http\Events\RequestHandled; use Illuminate\Routing\Pipeline; use Illuminate\Routing\Router; @@ -210,6 +211,8 @@ protected function dispatchToRouter() */ public function terminate($request, $response) { + $this->app['events']->dispatch(new Terminating); + $this->terminateMiddleware($request, $response); $this->app->terminate(); diff --git a/src/Illuminate/Foundation/Mix.php b/src/Illuminate/Foundation/Mix.php index f06deb95..c465247a 100644 --- a/src/Illuminate/Foundation/Mix.php +++ b/src/Illuminate/Foundation/Mix.php @@ -2,7 +2,6 @@ namespace Illuminate\Foundation; -use Exception; use Illuminate\Support\HtmlString; use Illuminate\Support\Str; @@ -15,7 +14,7 @@ class Mix * @param string $manifestDirectory * @return \Illuminate\Support\HtmlString|string * - * @throws \Illuminate\Foundation\MixManifestNotFoundException + * @throws \Illuminate\Foundation\MixManifestNotFoundException|\Illuminate\Foundation\MixFileNotFoundException */ public function __invoke($path, $manifestDirectory = '') { @@ -58,7 +57,7 @@ public function __invoke($path, $manifestDirectory = '') $manifest = $manifests[$manifestPath]; if (! isset($manifest[$path])) { - $exception = new Exception("Unable to locate Mix file: {$path}."); + $exception = new MixFileNotFoundException("Unable to locate Mix file: {$path}."); if (! app('config')->get('app.debug')) { report($exception); diff --git a/src/Illuminate/Foundation/MixFileNotFoundException.php b/src/Illuminate/Foundation/MixFileNotFoundException.php new file mode 100644 index 00000000..4e0ea741 --- /dev/null +++ b/src/Illuminate/Foundation/MixFileNotFoundException.php @@ -0,0 +1,10 @@ +getKeyName() => $table->getKey(), + ...$data, + ]; + } + $this->assertThat( $this->getTable($table), new HasInDatabase($this->getConnection($connection, $table), $data) ); @@ -41,8 +48,15 @@ protected function assertDatabaseHas($table, array $data, $connection = null) * @param string|null $connection * @return $this */ - protected function assertDatabaseMissing($table, array $data, $connection = null) + protected function assertDatabaseMissing($table, array $data = [], $connection = null) { + if ($table instanceof Model) { + $data = [ + $table->getKeyName() => $table->getKey(), + ...$data, + ]; + } + $constraint = new ReverseConstraint( new HasInDatabase($this->getConnection($connection, $table), $data) ); @@ -157,11 +171,7 @@ protected function assertNotSoftDeleted($table, array $data = [], $connection = */ protected function assertModelExists($model) { - return $this->assertDatabaseHas( - $model->getTable(), - [$model->getKeyName() => $model->getKey()], - $model->getConnectionName() - ); + return $this->assertDatabaseHas($model); } /** @@ -172,11 +182,7 @@ protected function assertModelExists($model) */ protected function assertModelMissing($model) { - return $this->assertDatabaseMissing( - $model->getTable(), - [$model->getKeyName() => $model->getKey()], - $model->getConnectionName() - ); + return $this->assertDatabaseMissing($model); } /** @@ -199,8 +205,8 @@ public function expectsDatabaseQueryCount($expected, $connection = null) $this->beforeApplicationDestroyed(function () use (&$actual, $expected, $connectionInstance) { $this->assertSame( - $actual, $expected, + $actual, "Expected {$expected} database queries on the [{$connectionInstance->getName()}] connection. {$actual} occurred." ); }); diff --git a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithExceptionHandling.php b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithExceptionHandling.php index 67aac9d9..cb20d976 100644 --- a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithExceptionHandling.php +++ b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithExceptionHandling.php @@ -5,6 +5,7 @@ use Closure; use Illuminate\Contracts\Debug\ExceptionHandler; use Illuminate\Support\Testing\Fakes\ExceptionHandlerFake; +use Illuminate\Support\Traits\ReflectsClosures; use Illuminate\Testing\Assert; use Illuminate\Validation\ValidationException; use Symfony\Component\Console\Application as ConsoleApplication; @@ -13,6 +14,8 @@ trait InteractsWithExceptionHandling { + use ReflectsClosures; + /** * The original exception handler. * @@ -169,18 +172,22 @@ public function renderForConsole($output, Throwable $e) * Assert that the given callback throws an exception with the given message when invoked. * * @param \Closure $test - * @param class-string<\Throwable> $expectedClass + * @param \Closure|class-string<\Throwable> $expectedClass * @param string|null $expectedMessage * @return $this */ - protected function assertThrows(Closure $test, string $expectedClass = Throwable::class, ?string $expectedMessage = null) + protected function assertThrows(Closure $test, string|Closure $expectedClass = Throwable::class, ?string $expectedMessage = null) { + [$expectedClass, $expectedClassCallback] = $expectedClass instanceof Closure + ? [$this->firstClosureParameterType($expectedClass), $expectedClass] + : [$expectedClass, null]; + try { $test(); $thrown = false; } catch (Throwable $exception) { - $thrown = $exception instanceof $expectedClass; + $thrown = $exception instanceof $expectedClass && ($expectedClassCallback === null || $expectedClassCallback($exception)); $actualMessage = $exception->getMessage(); } diff --git a/src/Illuminate/Foundation/Testing/Concerns/MakesHttpRequests.php b/src/Illuminate/Foundation/Testing/Concerns/MakesHttpRequests.php index 78344769..c24799df 100644 --- a/src/Illuminate/Foundation/Testing/Concerns/MakesHttpRequests.php +++ b/src/Illuminate/Foundation/Testing/Concerns/MakesHttpRequests.php @@ -90,6 +90,34 @@ public function withHeader(string $name, string $value) return $this; } + /** + * Remove a header from the request. + * + * @param string $name + * @return $this + */ + public function withoutHeader(string $name) + { + unset($this->defaultHeaders[$name]); + + return $this; + } + + /** + * Remove headers from the request. + * + * @param array $headers + * @return $this + */ + public function withoutHeaders(array $headers) + { + foreach ($headers as $name) { + $this->withoutHeader($name); + } + + return $this; + } + /** * Add an authorization token for the request. * @@ -121,9 +149,7 @@ public function withBasicAuth(string $username, string $password) */ public function withoutToken() { - unset($this->defaultHeaders['Authorization']); - - return $this; + return $this->withoutHeader('Authorization'); } /** diff --git a/src/Illuminate/Foundation/Testing/Wormhole.php b/src/Illuminate/Foundation/Testing/Wormhole.php index 54fe0fa0..beac013a 100644 --- a/src/Illuminate/Foundation/Testing/Wormhole.php +++ b/src/Illuminate/Foundation/Testing/Wormhole.php @@ -24,6 +24,30 @@ public function __construct($value) $this->value = $value; } + /** + * Travel forward the given number of microseconds. + * + * @param callable|null $callback + * @return mixed + */ + public function microsecond($callback = null) + { + return $this->microseconds($callback); + } + + /** + * Travel forward the given number of microseconds. + * + * @param callable|null $callback + * @return mixed + */ + public function microseconds($callback = null) + { + Carbon::setTestNow(Carbon::now()->addMicroseconds($this->value)); + + return $this->handleCallback($callback); + } + /** * Travel forward the given number of milliseconds. * diff --git a/src/Illuminate/Foundation/Vite.php b/src/Illuminate/Foundation/Vite.php index 97e79b6d..747da7b1 100644 --- a/src/Illuminate/Foundation/Vite.php +++ b/src/Illuminate/Foundation/Vite.php @@ -2,10 +2,10 @@ namespace Illuminate\Foundation; -use Exception; use Illuminate\Contracts\Support\Htmlable; use Illuminate\Support\Collection; use Illuminate\Support\HtmlString; +use Illuminate\Support\Js; use Illuminate\Support\Str; use Illuminate\Support\Traits\Macroable; @@ -97,6 +97,20 @@ class Vite implements Htmlable */ protected static $manifests = []; + /** + * The prefetching strategy to use. + * + * @var null|'waterfall'|'aggressive' + */ + protected $prefetchStrategy = null; + + /** + * The number of assets to load concurrently when using the "waterfall" strategy. + * + * @var int + */ + protected $prefetchConcurrently = 3; + /** * Get the preloaded assets. * @@ -267,6 +281,47 @@ public function usePreloadTagAttributes($attributes) return $this; } + /** + * Use the "waterfall" prefetching strategy. + * + * @param int|null $concurrency + * @return $this + */ + public function useWaterfallPrefetching(?int $concurrency = null) + { + return $this->usePrefetchStrategy('waterfall', [ + 'concurrency' => $concurrency ?? $this->prefetchConcurrently, + ]); + } + + /** + * Use the "aggressive" prefetching strategy. + * + * @return $this + */ + public function useAggressivePrefetching() + { + return $this->usePrefetchStrategy('aggressive'); + } + + /** + * Set the prefetching strategy. + * + * @param 'waterfall'|'aggressive'|null $strategy + * @param array $config + * @return $this + */ + public function usePrefetchStrategy($strategy, $config = []) + { + $this->prefetchStrategy = $strategy; + + if ($strategy === 'waterfall') { + $this->prefetchConcurrently = $config['concurrency'] ?? $this->prefetchConcurrently; + } + + return $this; + } + /** * Generate Vite tags for an entrypoint. * @@ -364,7 +419,122 @@ public function __invoke($entrypoints, $buildDirectory = null) ->sortByDesc(fn ($args) => $this->isCssPath($args[1])) ->map(fn ($args) => $this->makePreloadTagForChunk(...$args)); - return new HtmlString($preloads->join('').$stylesheets->join('').$scripts->join('')); + $base = $preloads->join('').$stylesheets->join('').$scripts->join(''); + + if ($this->prefetchStrategy === null || $this->isRunningHot()) { + return new HtmlString($base); + } + + $discoveredImports = []; + + return collect($entrypoints) + ->flatMap(fn ($entrypoint) => collect($manifest[$entrypoint]['dynamicImports'] ?? []) + ->map(fn ($import) => $manifest[$import]) + ->filter(fn ($chunk) => str_ends_with($chunk['file'], '.js') || str_ends_with($chunk['file'], '.css')) + ->flatMap($f = function ($chunk) use (&$f, $manifest, &$discoveredImports) { + return collect([...$chunk['imports'] ?? [], ...$chunk['dynamicImports'] ?? []]) + ->reject(function ($import) use (&$discoveredImports) { + if (isset($discoveredImports[$import])) { + return true; + } + + return ! $discoveredImports[$import] = true; + }) + ->reduce( + fn ($chunks, $import) => $chunks->merge( + $f($manifest[$import]) + ), collect([$chunk])) + ->merge(collect($chunk['css'] ?? [])->map( + fn ($css) => collect($manifest)->first(fn ($chunk) => $chunk['file'] === $css) ?? [ + 'file' => $css, + ], + )); + }) + ->map(function ($chunk) use ($buildDirectory, $manifest) { + return collect([ + ...$this->resolvePreloadTagAttributes( + $chunk['src'] ?? null, + $url = $this->assetPath("{$buildDirectory}/{$chunk['file']}"), + $chunk, + $manifest, + ), + 'rel' => 'prefetch', + 'fetchpriority' => 'low', + 'href' => $url, + ])->reject( + fn ($value) => in_array($value, [null, false], true) + )->mapWithKeys(fn ($value, $key) => [ + $key = (is_int($key) ? $value : $key) => $value === true ? $key : $value, + ])->all(); + }) + ->reject(fn ($attributes) => isset($this->preloadedAssets[$attributes['href']]))) + ->unique('href') + ->values() + ->pipe(fn ($assets) => with(Js::from($assets), fn ($assets) => match ($this->prefetchStrategy) { + 'waterfall' => new HtmlString($base.<< + window.addEventListener('load', () => window.setTimeout(() => { + const makeLink = (asset) => { + const link = document.createElement('link') + + Object.keys(asset).forEach((attribute) => { + link.setAttribute(attribute, asset[attribute]) + }) + + return link + } + + const loadNext = (assets, count) => window.setTimeout(() => { + if (count > assets.length) { + count = assets.length + + if (count === 0) { + return + } + } + + const fragment = new DocumentFragment + + while (count > 0) { + const link = makeLink(assets.shift()) + fragment.append(link) + count-- + + if (assets.length) { + link.onload = () => loadNext(assets, 1) + link.error = () => loadNext(assets, 1) + } + } + + document.head.append(fragment) + }) + + loadNext({$assets}, {$this->prefetchConcurrently}) + })) + + HTML), + 'aggressive' => new HtmlString($base.<< + window.addEventListener('load', () => window.setTimeout(() => { + const makeLink = (asset) => { + const link = document.createElement('link') + + Object.keys(asset).forEach((attribute) => { + link.setAttribute(attribute, asset[attribute]) + }) + + return link + } + + const fragment = new DocumentFragment + {$assets}.forEach((asset) => fragment.append(makeLink(asset))) + document.head.append(fragment) + })) + + HTML), + })); } /** @@ -682,7 +852,7 @@ public function asset($asset, $buildDirectory = null) * @param string|null $buildDirectory * @return string * - * @throws \Exception + * @throws \Illuminate\Foundation\ViteException */ public function content($asset, $buildDirectory = null) { @@ -693,7 +863,7 @@ public function content($asset, $buildDirectory = null) $path = public_path($buildDirectory.'/'.$chunk['file']); if (! is_file($path) || ! file_exists($path)) { - throw new Exception("Unable to locate file from Vite manifest: {$path}."); + throw new ViteException("Unable to locate file from Vite manifest: {$path}."); } return file_get_contents($path); @@ -773,12 +943,12 @@ public function manifestHash($buildDirectory = null) * @param string $file * @return array * - * @throws \Exception + * @throws \Illuminate\Foundation\ViteException */ protected function chunk($manifest, $file) { if (! isset($manifest[$file])) { - throw new Exception("Unable to locate file in Vite manifest: {$file}."); + throw new ViteException("Unable to locate file in Vite manifest: {$file}."); } return $manifest[$file]; diff --git a/src/Illuminate/Foundation/ViteException.php b/src/Illuminate/Foundation/ViteException.php new file mode 100644 index 00000000..984736e2 --- /dev/null +++ b/src/Illuminate/Foundation/ViteException.php @@ -0,0 +1,10 @@ +