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"
+}