diff --git a/README.md b/README.md index 64c2a03..fd36f73 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,67 @@ php artisan tailwindcss:download php artisan tailwindcss:build --prod ``` +### Preloading Assets as Link Header + +If you want to preload the TailwindCSS asset, make sure to add the `AddLinkHeaderForPreloadedAssets` middleware to your `web` route group, such as: + +```php + [ + // ... + \Tonysm\TailwindCss\Http\Middleware\AddLinkHeaderForPreloadedAssets::class, + ], + + 'api' => [ + // ... + ], + ]; + + // ... +} +``` + +The package will preload the asset by default. If you're linking an asset like: + +```blade + +``` + +It will add a Link header to the HTTP response like: + +```http +Link: ; rel=preload; as=style +``` + +It will keep any existing `Link` header as well. + +If you want to disable preloading with the Link header, set the flag to `false`: + +```blade + +``` + +You may also change or set additional attributes: + +```blade + +``` + +This will generate a preloading header like: + +```http +Link: ; rel=preload; as=style; crossorigin=anonymous +``` + ### Mock Manifest When Testing The `tailwindcss()` function will throw an exception when the manifest file is missing. However, we don't always need the manifest file when running our tests. You may use the `InteractsWithTailwind` trait in your main TestCase to disable that exception throwing: diff --git a/src/Commands/InstallCommand.php b/src/Commands/InstallCommand.php index 49f7e47..d380ca9 100644 --- a/src/Commands/InstallCommand.php +++ b/src/Commands/InstallCommand.php @@ -4,6 +4,7 @@ use Illuminate\Console\Command; use Illuminate\Support\Facades\File; +use Illuminate\Support\Str; use Symfony\Component\Console\Terminal; class InstallCommand extends Command @@ -25,6 +26,7 @@ public function handle() $this->ensureTailwindConfigExists(); $this->ensureTailwindCliBinaryExists(); $this->addImportStylesToLayouts(); + $this->installMiddlewareAfter('SubstituteBindings::class', '\Tonysm\TailwindCss\Http\Middleware\AddLinkHeaderForPreloadedAssets::class'); $this->addIngoreLines(); $this->runFirstBuild(); @@ -121,6 +123,36 @@ private function addImportStylesToLayouts() }); } + /** + * Install the middleware to a group in the application Http Kernel. + * + * @param string $after + * @param string $name + * @param string $group + * @return void + */ + private function installMiddlewareAfter($after, $name, $group = 'web') + { + $httpKernel = file_get_contents(app_path('Http/Kernel.php')); + + $middlewareGroups = Str::before(Str::after($httpKernel, '$middlewareGroups = ['), '];'); + $middlewareGroup = Str::before(Str::after($middlewareGroups, "'$group' => ["), '],'); + + if (! Str::contains($middlewareGroup, $name)) { + $modifiedMiddlewareGroup = str_replace( + $after.',', + $after.','.PHP_EOL.' '.$name.',', + $middlewareGroup, + ); + + file_put_contents(app_path('Http/Kernel.php'), str_replace( + $middlewareGroups, + str_replace($middlewareGroup, $modifiedMiddlewareGroup, $middlewareGroups), + $httpKernel + )); + } + } + private function replaceMixStylesToLayouts() { $this->existingLayoutFiles() diff --git a/src/Http/Middleware/AddLinkHeaderForPreloadedAssets.php b/src/Http/Middleware/AddLinkHeaderForPreloadedAssets.php new file mode 100644 index 0000000..69ae176 --- /dev/null +++ b/src/Http/Middleware/AddLinkHeaderForPreloadedAssets.php @@ -0,0 +1,29 @@ +manifest->assetsForPreloading()) > 0) { + $response->header('Link', trim(implode(', ', array_filter(array_merge( + [$response->headers->get('Link', null)], + collect($assets)->map(fn ($attributes, $asset) => implode('; ', array_merge( + ["<$asset>"], + collect(array_merge(['rel' => 'preload', 'as' => 'style'], $attributes)) + ->map(fn ($value, $key) => "{$key}={$value}") + ->all(), + )))->all(), + ))))); + } + }); + } +} diff --git a/src/Manifest.php b/src/Manifest.php index 6ebe83d..a364bf6 100644 --- a/src/Manifest.php +++ b/src/Manifest.php @@ -8,6 +8,13 @@ class Manifest { + protected array $preloading = []; + + public function assetsForPreloading(): array + { + return $this->preloading; + } + public static function filename(): string { return basename(self::path()); @@ -18,7 +25,7 @@ public static function path(): string return config('tailwindcss.build.manifest_file_path'); } - public function __invoke(string $path) + public function __invoke(string $path, $preload = true) { static $manifests = []; @@ -50,6 +57,12 @@ public function __invoke(string $path) } } - return new HtmlString(asset($manifest[$path])); + $asset = asset($manifest[$path]); + + if ($preload) { + $this->preloading[$asset] = is_array($preload) ? $preload : []; + } + + return new HtmlString($asset); } } diff --git a/src/TailwindCssServiceProvider.php b/src/TailwindCssServiceProvider.php index 7dac793..9f11a74 100644 --- a/src/TailwindCssServiceProvider.php +++ b/src/TailwindCssServiceProvider.php @@ -24,4 +24,9 @@ public function configurePackage(Package $package): void Commands\WatchCommand::class, ]); } + + public function packageRegistered() + { + $this->app->scoped(Manifest::class); + } } diff --git a/src/helpers.php b/src/helpers.php index e055063..ce701e0 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -8,10 +8,11 @@ * Get the path to a versioned TailwindCSS file. * * @param string $path + * @param bool|array $preload * @return \Illuminate\Support\HtmlString|string */ - function tailwindcss(string $path): HtmlString|string + function tailwindcss(string $path, $preload = true): HtmlString|string { - return app(Manifest::class)($path); + return app(Manifest::class)($path, $preload); } } diff --git a/tests/PreloadingHeaderTest.php b/tests/PreloadingHeaderTest.php new file mode 100644 index 0000000..9c8edf9 --- /dev/null +++ b/tests/PreloadingHeaderTest.php @@ -0,0 +1,79 @@ +set('tailwindcss.build.manifest_file_path', __DIR__.'/stubs/test-manifest.json'); + + $tailwindcss = tailwindcss('css/app.css', preload: false); + + $response = resolve(AddLinkHeaderForPreloadedAssets::class)->handle( + Request::create('/'), + fn () => response('hello world'), + ); + + $this->assertEquals('http://localhost/css/app-123.css', (string) $tailwindcss); + $this->assertEquals('hello world', $response->content()); + $this->assertNull($response->headers->get('Link', null)); + } + + /** @test */ + public function adds_link_header_when_preloading() + { + config()->set('tailwindcss.build.manifest_file_path', __DIR__.'/stubs/test-manifest.json'); + + $tailwindcss = tailwindcss('css/app.css', preload: true); + + $response = resolve(AddLinkHeaderForPreloadedAssets::class)->handle( + Request::create('/'), + fn () => response('hello world'), + ); + + $this->assertEquals($asset = 'http://localhost/css/app-123.css', (string) $tailwindcss); + $this->assertEquals('hello world', $response->content()); + $this->assertEquals("<{$asset}>; rel=preload; as=style", $response->headers->get('Link', null)); + } + + /** @test */ + public function keeps_existing_preloading_link_header() + { + config()->set('tailwindcss.build.manifest_file_path', __DIR__.'/stubs/test-manifest.json'); + + $tailwindcss = tailwindcss('css/app.css', preload: true); + + $response = resolve(AddLinkHeaderForPreloadedAssets::class)->handle( + Request::create('/'), + fn () => response('hello world')->withHeaders([ + 'Link' => '; rel=modulepreload', + ]), + ); + + $this->assertEquals($asset = 'http://localhost/css/app-123.css', (string) $tailwindcss); + $this->assertEquals('hello world', $response->content()); + $this->assertEquals("; rel=modulepreload, <{$asset}>; rel=preload; as=style", $response->headers->get('Link', null)); + } + + /** @test */ + public function adds_link_header_when_preloading_custom_attributes() + { + config()->set('tailwindcss.build.manifest_file_path', __DIR__.'/stubs/test-manifest.json'); + + $tailwindcss = tailwindcss('css/app.css', ['crossorigin' => 'anonymous']); + + $response = resolve(AddLinkHeaderForPreloadedAssets::class)->handle( + Request::create('/'), + fn () => response('hello world'), + ); + + $this->assertEquals($asset = 'http://localhost/css/app-123.css', (string) $tailwindcss); + $this->assertEquals('hello world', $response->content()); + $this->assertEquals("<{$asset}>; rel=preload; as=style; crossorigin=anonymous", $response->headers->get('Link', null)); + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php index db458c9..a0eb903 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -33,5 +33,7 @@ protected function getPackageProviders($app) public function getEnvironmentSetUp($app) { config()->set('database.default', 'testing'); + config()->set('app.url', 'http://localhost'); + config()->set('app.asset_url', 'http://localhost'); } } diff --git a/tests/stubs/test-manifest.json b/tests/stubs/test-manifest.json new file mode 100644 index 0000000..9c4a9dd --- /dev/null +++ b/tests/stubs/test-manifest.json @@ -0,0 +1,3 @@ +{ + "/css/app.css": "/css/app-123.css" +}