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 @@
+