Skip to content

Commit c08a710

Browse files
authored
Merge pull request #28 from tonysm/link-header
Link header
2 parents 08f2f66 + ef715d9 commit c08a710

9 files changed

+229
-4
lines changed

README.md

+61
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,67 @@ php artisan tailwindcss:download
126126
php artisan tailwindcss:build --prod
127127
```
128128

129+
### Preloading Assets as Link Header
130+
131+
If you want to preload the TailwindCSS asset, make sure to add the `AddLinkHeaderForPreloadedAssets` middleware to your `web` route group, such as:
132+
133+
```php
134+
<?php
135+
136+
namespace App\Http;
137+
138+
use Illuminate\Foundation\Http\Kernel as HttpKernel;
139+
140+
class Kernel extends HttpKernel
141+
{
142+
/** ... */
143+
protected $middlewareGroups = [
144+
'web' => [
145+
// ...
146+
\Tonysm\TailwindCss\Http\Middleware\AddLinkHeaderForPreloadedAssets::class,
147+
],
148+
149+
'api' => [
150+
// ...
151+
],
152+
];
153+
154+
// ...
155+
}
156+
```
157+
158+
The package will preload the asset by default. If you're linking an asset like:
159+
160+
```blade
161+
<link rel="stylesheet" href="{{ tailwindcss('css/app.css') }}">
162+
```
163+
164+
It will add a Link header to the HTTP response like:
165+
166+
```http
167+
Link: <http://localhost/css/app.css>; rel=preload; as=style
168+
```
169+
170+
It will keep any existing `Link` header as well.
171+
172+
If you want to disable preloading with the Link header, set the flag to `false`:
173+
174+
```blade
175+
<link rel="stylesheet" href="{{ tailwindcss('css/app.css', preload: false) }}">
176+
```
177+
178+
You may also change or set additional attributes:
179+
180+
```blade
181+
<link rel="stylesheet" href="{{ tailwindcss('css/app.css', preload: ['crossorigin' => 'anonymous']) }}">
182+
```
183+
184+
This will generate a preloading header like:
185+
186+
```http
187+
Link: <http://localhost/css/app.css>; rel=preload; as=style; crossorigin=anonymous
188+
```
189+
129190
### Mock Manifest When Testing
130191

131192
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:

src/Commands/InstallCommand.php

+32
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use Illuminate\Console\Command;
66
use Illuminate\Support\Facades\File;
7+
use Illuminate\Support\Str;
78
use Symfony\Component\Console\Terminal;
89

910
class InstallCommand extends Command
@@ -25,6 +26,7 @@ public function handle()
2526
$this->ensureTailwindConfigExists();
2627
$this->ensureTailwindCliBinaryExists();
2728
$this->addImportStylesToLayouts();
29+
$this->installMiddlewareAfter('SubstituteBindings::class', '\Tonysm\TailwindCss\Http\Middleware\AddLinkHeaderForPreloadedAssets::class');
2830
$this->addIngoreLines();
2931
$this->runFirstBuild();
3032

@@ -121,6 +123,36 @@ private function addImportStylesToLayouts()
121123
});
122124
}
123125

126+
/**
127+
* Install the middleware to a group in the application Http Kernel.
128+
*
129+
* @param string $after
130+
* @param string $name
131+
* @param string $group
132+
* @return void
133+
*/
134+
private function installMiddlewareAfter($after, $name, $group = 'web')
135+
{
136+
$httpKernel = file_get_contents(app_path('Http/Kernel.php'));
137+
138+
$middlewareGroups = Str::before(Str::after($httpKernel, '$middlewareGroups = ['), '];');
139+
$middlewareGroup = Str::before(Str::after($middlewareGroups, "'$group' => ["), '],');
140+
141+
if (! Str::contains($middlewareGroup, $name)) {
142+
$modifiedMiddlewareGroup = str_replace(
143+
$after.',',
144+
$after.','.PHP_EOL.' '.$name.',',
145+
$middlewareGroup,
146+
);
147+
148+
file_put_contents(app_path('Http/Kernel.php'), str_replace(
149+
$middlewareGroups,
150+
str_replace($middlewareGroup, $modifiedMiddlewareGroup, $middlewareGroups),
151+
$httpKernel
152+
));
153+
}
154+
}
155+
124156
private function replaceMixStylesToLayouts()
125157
{
126158
$this->existingLayoutFiles()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
namespace Tonysm\TailwindCss\Http\Middleware;
4+
5+
use Tonysm\TailwindCss\Manifest;
6+
7+
class AddLinkHeaderForPreloadedAssets
8+
{
9+
public function __construct(private Manifest $manifest)
10+
{
11+
}
12+
13+
public function handle($request, $next)
14+
{
15+
return tap($next($request), function ($response) {
16+
if (count($assets = $this->manifest->assetsForPreloading()) > 0) {
17+
$response->header('Link', trim(implode(', ', array_filter(array_merge(
18+
[$response->headers->get('Link', null)],
19+
collect($assets)->map(fn ($attributes, $asset) => implode('; ', array_merge(
20+
["<$asset>"],
21+
collect(array_merge(['rel' => 'preload', 'as' => 'style'], $attributes))
22+
->map(fn ($value, $key) => "{$key}={$value}")
23+
->all(),
24+
)))->all(),
25+
)))));
26+
}
27+
});
28+
}
29+
}

src/Manifest.php

+15-2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@
88

99
class Manifest
1010
{
11+
protected array $preloading = [];
12+
13+
public function assetsForPreloading(): array
14+
{
15+
return $this->preloading;
16+
}
17+
1118
public static function filename(): string
1219
{
1320
return basename(self::path());
@@ -18,7 +25,7 @@ public static function path(): string
1825
return config('tailwindcss.build.manifest_file_path');
1926
}
2027

21-
public function __invoke(string $path)
28+
public function __invoke(string $path, $preload = true)
2229
{
2330
static $manifests = [];
2431

@@ -50,6 +57,12 @@ public function __invoke(string $path)
5057
}
5158
}
5259

53-
return new HtmlString(asset($manifest[$path]));
60+
$asset = asset($manifest[$path]);
61+
62+
if ($preload) {
63+
$this->preloading[$asset] = is_array($preload) ? $preload : [];
64+
}
65+
66+
return new HtmlString($asset);
5467
}
5568
}

src/TailwindCssServiceProvider.php

+5
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,9 @@ public function configurePackage(Package $package): void
2424
Commands\WatchCommand::class,
2525
]);
2626
}
27+
28+
public function packageRegistered()
29+
{
30+
$this->app->scoped(Manifest::class);
31+
}
2732
}

src/helpers.php

+3-2
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@
88
* Get the path to a versioned TailwindCSS file.
99
*
1010
* @param string $path
11+
* @param bool|array $preload
1112
* @return \Illuminate\Support\HtmlString|string
1213
*/
13-
function tailwindcss(string $path): HtmlString|string
14+
function tailwindcss(string $path, $preload = true): HtmlString|string
1415
{
15-
return app(Manifest::class)($path);
16+
return app(Manifest::class)($path, $preload);
1617
}
1718
}

tests/PreloadingHeaderTest.php

+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?php
2+
3+
namespace Tonysm\TailwindCss\Tests;
4+
5+
use Illuminate\Http\Request;
6+
use Tonysm\TailwindCss\Http\Middleware\AddLinkHeaderForPreloadedAssets;
7+
8+
class PreloadingHeaderTest extends TestCase
9+
{
10+
/** @test */
11+
public function no_link_header_when_not_preloading()
12+
{
13+
config()->set('tailwindcss.build.manifest_file_path', __DIR__.'/stubs/test-manifest.json');
14+
15+
$tailwindcss = tailwindcss('css/app.css', preload: false);
16+
17+
$response = resolve(AddLinkHeaderForPreloadedAssets::class)->handle(
18+
Request::create('/'),
19+
fn () => response('hello world'),
20+
);
21+
22+
$this->assertEquals('http://localhost/css/app-123.css', (string) $tailwindcss);
23+
$this->assertEquals('hello world', $response->content());
24+
$this->assertNull($response->headers->get('Link', null));
25+
}
26+
27+
/** @test */
28+
public function adds_link_header_when_preloading()
29+
{
30+
config()->set('tailwindcss.build.manifest_file_path', __DIR__.'/stubs/test-manifest.json');
31+
32+
$tailwindcss = tailwindcss('css/app.css', preload: true);
33+
34+
$response = resolve(AddLinkHeaderForPreloadedAssets::class)->handle(
35+
Request::create('/'),
36+
fn () => response('hello world'),
37+
);
38+
39+
$this->assertEquals($asset = 'http://localhost/css/app-123.css', (string) $tailwindcss);
40+
$this->assertEquals('hello world', $response->content());
41+
$this->assertEquals("<{$asset}>; rel=preload; as=style", $response->headers->get('Link', null));
42+
}
43+
44+
/** @test */
45+
public function keeps_existing_preloading_link_header()
46+
{
47+
config()->set('tailwindcss.build.manifest_file_path', __DIR__.'/stubs/test-manifest.json');
48+
49+
$tailwindcss = tailwindcss('css/app.css', preload: true);
50+
51+
$response = resolve(AddLinkHeaderForPreloadedAssets::class)->handle(
52+
Request::create('/'),
53+
fn () => response('hello world')->withHeaders([
54+
'Link' => '</js/app.js>; rel=modulepreload',
55+
]),
56+
);
57+
58+
$this->assertEquals($asset = 'http://localhost/css/app-123.css', (string) $tailwindcss);
59+
$this->assertEquals('hello world', $response->content());
60+
$this->assertEquals("</js/app.js>; rel=modulepreload, <{$asset}>; rel=preload; as=style", $response->headers->get('Link', null));
61+
}
62+
63+
/** @test */
64+
public function adds_link_header_when_preloading_custom_attributes()
65+
{
66+
config()->set('tailwindcss.build.manifest_file_path', __DIR__.'/stubs/test-manifest.json');
67+
68+
$tailwindcss = tailwindcss('css/app.css', ['crossorigin' => 'anonymous']);
69+
70+
$response = resolve(AddLinkHeaderForPreloadedAssets::class)->handle(
71+
Request::create('/'),
72+
fn () => response('hello world'),
73+
);
74+
75+
$this->assertEquals($asset = 'http://localhost/css/app-123.css', (string) $tailwindcss);
76+
$this->assertEquals('hello world', $response->content());
77+
$this->assertEquals("<{$asset}>; rel=preload; as=style; crossorigin=anonymous", $response->headers->get('Link', null));
78+
}
79+
}

tests/TestCase.php

+2
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,7 @@ protected function getPackageProviders($app)
3333
public function getEnvironmentSetUp($app)
3434
{
3535
config()->set('database.default', 'testing');
36+
config()->set('app.url', 'http://localhost');
37+
config()->set('app.asset_url', 'http://localhost');
3638
}
3739
}

tests/stubs/test-manifest.json

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"/css/app.css": "/css/app-123.css"
3+
}

0 commit comments

Comments
 (0)