Skip to content

Commit

Permalink
Merge pull request #44 from tonysm/preload-default
Browse files Browse the repository at this point in the history
Preload default
  • Loading branch information
tonysm authored Jan 6, 2024
2 parents 288b3b2 + a47604c commit 69dadc7
Show file tree
Hide file tree
Showing 12 changed files with 126 additions and 46 deletions.
11 changes: 7 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,15 @@ php artisan importmap:install
Next, we need to add the following component to our view or layout file:

```blade
<x-importmap-tags />
<x-importmap::tags />
```

Add that between your `<head>` tags. The `entrypoint` should be the "main" file, commonly the `resources/js/app.js` file, which will be mapped to the `app` module (use the module name, not the file).

By default the `x-importmap-tags` component assumes your entrypoint module is `app`, which matches the existing `resources/js/app.js` file from Laravel's default scaffolding. You may want to customize the entrypoint, which you can do with the `entrypoint` prop:
By default the `x-importmap::tags` component assumes your entrypoint module is `app`, which matches the existing `resources/js/app.js` file from Laravel's default scaffolding. You may want to customize the entrypoint, which you can do with the `entrypoint` prop:

```blade
<x-importmap-tags entrypoint="admin" />
<x-importmap::tags entrypoint="admin" />
```

The package will automatically map the `resources/js` folder to your `public/js` folder using Laravel's symlink feature. All you have to do after installing the package is run:
Expand Down Expand Up @@ -173,7 +173,7 @@ The version is added as a comment to your pin so you know which version was impo

### Preloading Modules

To avoid the waterfall effect where the browser has to load one file after another before it can get to the deepest nested import, we support [modulepreload links](https://developers.google.com/web/updates/2017/12/modulepreload). Pinned modules can be preloaded by appending `preload: true` to the pin, like so:
To avoid the waterfall effect where the browser has to load one file after another before it can get to the deepest nested import, we use [modulepreload links](https://developers.google.com/web/updates/2017/12/modulepreload) by default. If you don't want to preload a dependency, because you want to load it on-demand for efficiency, append `preload: false` to the pin.

```php
Importmap::pinAllFrom("resources/js/", to: "js/", preload: true);
Expand All @@ -186,6 +186,9 @@ Which will add the correct `links` tags to your head tag in the HTML document, l
<link rel="modulepreload" href="https://unpkg.com/alpinejs@3.8.1/dist/module.esm.js">
```

You may add the `AddLinkHeadersForPreloadedPins` middleware to the `web` routes group so these preloaded links are sent as a `Link` header.
Add the `Tonysm\ImportmapLaravel\Http\Middleware\AddLinkHeadersForPreloadedPins` to the `web` route group so the preloaded modules are sent as the Link headers, which are used in [HTTP/2 Server Push](https://datatracker.ietf.org/doc/html/rfc7540#section-8.2) and [Resource Hints](https://html.spec.whatwg.org/#linkTypes) to push resources to the client as early as possible. Some web servers can pick up this `Link` header and convert them to [Early Hints](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/103) responses.

## Dependency Maintenance Commands

Maintaining a healthy dependency list can be tricky. Here are a couple of commands to help you with this task.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
@props(['entrypoint' => 'app', 'nonce' => null, 'importmap' => null])

@php
$resolver = new \Tonysm\ImportmapLaravel\AssetResolver();
$importmaps = $importmap?->asArray($resolver) ?? \Tonysm\ImportmapLaravel\Facades\Importmap::asArray($resolver);
$preloadedModules = $importmap?->preloadedModulePaths($resolver) ?? \Tonysm\ImportmapLaravel\Facades\Importmap::preloadedModulePaths($resolver);
@endphp

<script type="importmap" data-turbo-track="reload"@if ($nonce) nonce="{{ $nonce }}" @endif>
@json($importmaps, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
</script>
Expand Down
8 changes: 4 additions & 4 deletions src/Commands/InstallCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ private function deleteNpmRelatedFiles(): void
private function publishImportmapFile(): void
{
$this->displayTask('publishing the `routes/importmap.php` file', function () {
File::copy(dirname(__DIR__, 2).join(DIRECTORY_SEPARATOR, ['', 'stubs', 'routes', 'importmap.php']), base_path(join(DIRECTORY_SEPARATOR, ['routes', 'importmap.php'])));
File::copy(dirname(__DIR__, 2).implode(DIRECTORY_SEPARATOR, ['', 'stubs', 'routes', 'importmap.php']), base_path(implode(DIRECTORY_SEPARATOR, ['routes', 'importmap.php'])));

return self::SUCCESS;
});
Expand Down Expand Up @@ -140,7 +140,7 @@ private function updateAppLayoutsUsingMix()
$file,
str_replace(
"<script src=\"{{ mix('js/app.js') }}\" defer></script>",
'<x-importmap-tags />',
'<x-importmap::tags />',
File::get($file),
),
));
Expand All @@ -157,7 +157,7 @@ private function updateAppLayoutsUsingVite()
$file,
preg_replace(
'/\@vite.*/',
'<x-importmap-tags />',
'<x-importmap::tags />',
File::get($file),
),
))
Expand All @@ -182,7 +182,7 @@ private function appendImportmapTagsToLayoutsHead(): void
$file,
preg_replace(
'/(\s*)(<\/head>)/',
"\\1 <x-importmap-tags />\n\\1\\2",
"\\1 <x-importmap::tags />\n\\1\\2",
File::get($file),
),
));
Expand Down
31 changes: 31 additions & 0 deletions src/Http/Middleware/AddLinkHeadersForPreloadedPins.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

namespace Tonysm\ImportmapLaravel\Http\Middleware;

use Tonysm\ImportmapLaravel\AssetResolver;
use Tonysm\ImportmapLaravel\Facades\Importmap;

class AddLinkHeadersForPreloadedPins
{
public function __construct(private AssetResolver $assetsResolver = new AssetResolver())
{
}

/**
* Sets the Link header for preloaded pins.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return \Illuminate\Http\Response
*/
public function handle($request, $next)
{
return tap($next($request), function ($response) {
if ($preloaded = Importmap::preloadedModulePaths($this->assetsResolver)) {
$response->header('Link', collect($preloaded)
->map(fn ($url) => "<{$url}>; rel=\"modulepreload\"")
->join(', '));
}
});
}
}
4 changes: 2 additions & 2 deletions src/Importmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@ public function __construct(public ?string $rootPath = null)
$this->directories = collect();
}

public function pin(string $name, string $to = null, bool $preload = false)
public function pin(string $name, ?string $to = null, bool $preload = true)
{
$this->packages->add(new MappedFile($name, path: $to ?: "js/{$name}.js", preload: $preload));
}

public function pinAllFrom(string $dir, string $under = null, string $to = null, bool $preload = false)
public function pinAllFrom(string $dir, ?string $under = null, ?string $to = null, bool $preload = true)
{
$this->directories->add(new MappedDirectory($dir, $under, $to, $preload));
}
Expand Down
12 changes: 10 additions & 2 deletions src/ImportmapLaravelServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

namespace Tonysm\ImportmapLaravel;

use Illuminate\View\Compilers\BladeCompiler;
use Spatie\LaravelPackageTools\Package;
use Spatie\LaravelPackageTools\PackageServiceProvider;
use Tonysm\ImportmapLaravel\View\Components;

class ImportmapLaravelServiceProvider extends PackageServiceProvider
{
Expand All @@ -19,7 +19,6 @@ public function configurePackage(Package $package): void
->name('importmap')
->hasConfigFile()
->hasViews()
->hasViewComponent('importmap', Components\Tags::class)
->hasCommand(Commands\InstallCommand::class)
->hasCommand(Commands\OptimizeCommand::class)
->hasCommand(Commands\ClearCacheCommand::class)
Expand Down Expand Up @@ -51,5 +50,14 @@ public function packageBooted()
public_path('js') => resource_path('js'),
]);
}

$this->configureComponents();
}

private function configureComponents()
{
$this->callAfterResolving('blade.compiler', function (BladeCompiler $blade) {
$blade->anonymousComponentPath(__DIR__.'/../resources/views/components', 'importmap');
});
}
}
28 changes: 0 additions & 28 deletions src/View/Components/Tags.php

This file was deleted.

6 changes: 4 additions & 2 deletions tests/ImportmapTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ protected function setUp(): void

$this->map = new Importmap(rootPath: __DIR__.DIRECTORY_SEPARATOR.'stubs'.DIRECTORY_SEPARATOR);

$this->map->pin('app');
$this->map->pin('editor', to: 'js/rich_text.js');
$this->map->pin('app', preload: false);
$this->map->pin('editor', to: 'js/rich_text.js', preload: false);
$this->map->pin('not_there', to: 'js/nowhere.js', preload: false);
$this->map->pin('md5', to: 'https://cdn.skypack.dev/md5', preload: true);

$this->map->pinAllFrom('resources/js/controllers', under: 'controllers', to: 'js/controllers', preload: true);
Expand Down Expand Up @@ -90,6 +91,7 @@ public function preload_modules_are_included_in_preload_tags()

$this->assertStringContainsString('md5', $preloadingModulePaths);
$this->assertStringContainsString('hello_controller', $preloadingModulePaths);
$this->assertStringNotContainsString('not_there', $preloadingModulePaths);
$this->assertStringNotContainsString('app', $preloadingModulePaths);
}
}
54 changes: 54 additions & 0 deletions tests/PreloadingWithLinkHeadersTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

namespace Tonysm\ImportmapLaravel\Tests;

use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Tonysm\ImportmapLaravel\AssetResolver;
use Tonysm\ImportmapLaravel\Http\Middleware\AddLinkHeadersForPreloadedPins;
use Tonysm\ImportmapLaravel\Importmap;

class PreloadingWithLinkHeadersTest extends TestCase
{
/** @test */
public function doesnt_set_link_header_when_no_pins_are_preloaded(): void
{
$this->swap(Importmap::class, $map = new Importmap(rootPath: __DIR__.DIRECTORY_SEPARATOR.'stubs'.DIRECTORY_SEPARATOR));

$map->pin('app', preload: false);
$map->pin('editor', to: 'js/rich_text.js', preload: false);
$map->pinAllFrom('resources/js/', under: 'controllers', to: 'js/', preload: false);

$response = (new AddLinkHeadersForPreloadedPins())->handle(new Request(), function () {
return new Response('Hello World');
});

$this->assertNull($response->headers->get('Link'));
}

/** @test */
public function sets_link_header_when_pins_are_preloaded(): void
{
$this->swap(Importmap::class, $map = new Importmap(rootPath: __DIR__.DIRECTORY_SEPARATOR.'stubs'.DIRECTORY_SEPARATOR));

$map->pin('app', preload: true);
$map->pin('editor', to: 'js/rich_text.js', preload: false);
$map->pinAllFrom('resources/js/', under: 'controllers', to: 'js/', preload: true);

$resolver = new class () extends AssetResolver {
public function __invoke($module)
{
return 'http://localhost/'.str_replace(['.js'], ['-123123.js'], $module);
}
};

$response = (new AddLinkHeadersForPreloadedPins($resolver))->handle(new Request(), function () {
return new Response('Hello World');
});

$this->assertEquals(
'<http://localhost/js/app-123123.js>; rel="modulepreload", <http://localhost/js/app-123123.js>; rel="modulepreload", <http://localhost/js/controllers/hello_controller-123123.js>; rel="modulepreload", <http://localhost/js/controllers/index-123123.js>; rel="modulepreload", <http://localhost/js/controllers/utilities/md5_controller-123123.js>; rel="modulepreload", <http://localhost/js/helpers/requests/index-123123.js>; rel="modulepreload", <http://localhost/js/libs/vendor/alpine-123123.js>; rel="modulepreload", <http://localhost/js/spina/controllers/another_controller-123123.js>; rel="modulepreload", <http://localhost/js/spina/controllers/deeper/again_controller-123123.js>; rel="modulepreload"',
$response->headers->get('Link'),
);
}
}
6 changes: 3 additions & 3 deletions tests/TagsComponentTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,14 @@ protected function setUp(): void
/** @test */
public function generates_tags_without_nonce()
{
$this->blade('<x-importmap-tags />')
$this->blade('<x-importmap::tags />')
->assertSee('<link rel="modulepreload" href="https://cdn.skypack.dev/md5" />', escape: false);
}

/** @test */
public function uses_given_csp_nonce()
{
$this->blade('<x-importmap-tags nonce="h3ll0" />')
$this->blade('<x-importmap::tags nonce="h3ll0" />')
->assertSee('<link rel="modulepreload" href="https://cdn.skypack.dev/md5" nonce="h3ll0" />', escape: false);
}

Expand All @@ -51,7 +51,7 @@ public function uses_custom_map()
$importmap->pin('foo', preload: true);
$importmap->pin('bar', preload: true);

$this->blade('<x-importmap-tags :importmap="$importmap" />', ['importmap' => $importmap])
$this->blade('<x-importmap::tags :importmap="$importmap" />', ['importmap' => $importmap])
->assertSee('<link rel="modulepreload" href="'.asset('js/foo.js').'" />', escape: false)
->assertSee('<link rel="modulepreload" href="'.asset('js/bar.js').'" />', escape: false)
->assertDontSee('<link rel="modulepreload" href="https://cdn.skypack.dev/md5" />', escape: false);
Expand Down
1 change: 1 addition & 0 deletions tests/TestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ protected function getPackageProviders($app)
public function getEnvironmentSetUp($app)
{
config()->set('database.default', 'testing');
config()->set('app.url', 'http://localhost');

/*
$migration = include __DIR__.'/../database/migrations/create_importmap-laravel_table.php.stub';
Expand Down
2 changes: 1 addition & 1 deletion tests/fixtures/npm/single-quote-importmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
use Tonysm\ImportmapLaravel\Facades\Importmap;

Importmap::pin('md5', to: 'https://cdn.skypack.dev/md5', preload: true);
Importmap::pin('not_there', to: 'nowhere.js');
Importmap::pin('not_there', to: 'nowhere.js', preload: false);

0 comments on commit 69dadc7

Please sign in to comment.