Skip to content

Commit

Permalink
Merge pull request #28 from tonysm/link-header
Browse files Browse the repository at this point in the history
Link header
  • Loading branch information
tonysm authored Jan 12, 2024
2 parents 08f2f66 + ef715d9 commit c08a710
Show file tree
Hide file tree
Showing 9 changed files with 229 additions and 4 deletions.
61 changes: 61 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<?php

namespace App\Http;

use Illuminate\Foundation\Http\Kernel as HttpKernel;

class Kernel extends HttpKernel
{
/** ... */
protected $middlewareGroups = [
'web' => [
// ...
\Tonysm\TailwindCss\Http\Middleware\AddLinkHeaderForPreloadedAssets::class,
],

'api' => [
// ...
],
];

// ...
}
```

The package will preload the asset by default. If you're linking an asset like:

```blade
<link rel="stylesheet" href="{{ tailwindcss('css/app.css') }}">
```

It will add a Link header to the HTTP response like:

```http
Link: <http://localhost/css/app.css>; 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
<link rel="stylesheet" href="{{ tailwindcss('css/app.css', preload: false) }}">
```

You may also change or set additional attributes:

```blade
<link rel="stylesheet" href="{{ tailwindcss('css/app.css', preload: ['crossorigin' => 'anonymous']) }}">
```

This will generate a preloading header like:

```http
Link: <http://localhost/css/app.css>; 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:
Expand Down
32 changes: 32 additions & 0 deletions src/Commands/InstallCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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();

Expand Down Expand Up @@ -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()
Expand Down
29 changes: 29 additions & 0 deletions src/Http/Middleware/AddLinkHeaderForPreloadedAssets.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

namespace Tonysm\TailwindCss\Http\Middleware;

use Tonysm\TailwindCss\Manifest;

class AddLinkHeaderForPreloadedAssets
{
public function __construct(private Manifest $manifest)
{
}

public function handle($request, $next)
{
return tap($next($request), function ($response) {
if (count($assets = $this->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(),
)))));
}
});
}
}
17 changes: 15 additions & 2 deletions src/Manifest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand All @@ -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 = [];

Expand Down Expand Up @@ -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);
}
}
5 changes: 5 additions & 0 deletions src/TailwindCssServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,9 @@ public function configurePackage(Package $package): void
Commands\WatchCommand::class,
]);
}

public function packageRegistered()
{
$this->app->scoped(Manifest::class);
}
}
5 changes: 3 additions & 2 deletions src/helpers.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
79 changes: 79 additions & 0 deletions tests/PreloadingHeaderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

namespace Tonysm\TailwindCss\Tests;

use Illuminate\Http\Request;
use Tonysm\TailwindCss\Http\Middleware\AddLinkHeaderForPreloadedAssets;

class PreloadingHeaderTest extends TestCase
{
/** @test */
public function no_link_header_when_not_preloading()
{
config()->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' => '</js/app.js>; rel=modulepreload',
]),
);

$this->assertEquals($asset = 'http://localhost/css/app-123.css', (string) $tailwindcss);
$this->assertEquals('hello world', $response->content());
$this->assertEquals("</js/app.js>; 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));
}
}
2 changes: 2 additions & 0 deletions tests/TestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
}
3 changes: 3 additions & 0 deletions tests/stubs/test-manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"/css/app.css": "/css/app-123.css"
}

0 comments on commit c08a710

Please sign in to comment.