diff --git a/README.md b/README.md index 3ba7edc..0e50894 100644 --- a/README.md +++ b/README.md @@ -45,15 +45,15 @@ php artisan importmap:install Next, we need to add the following component to our view or layout file: ```blade - + ``` Add that between your `` 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 - + ``` 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: @@ -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); @@ -186,6 +186,9 @@ Which will add the correct `links` tags to your head tag in the HTML document, l ``` +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. diff --git a/resources/views/tags.blade.php b/resources/views/components/tags.blade.php similarity index 52% rename from resources/views/tags.blade.php rename to resources/views/components/tags.blade.php index c61d39d..b632e40 100644 --- a/resources/views/tags.blade.php +++ b/resources/views/components/tags.blade.php @@ -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 + diff --git a/src/Commands/InstallCommand.php b/src/Commands/InstallCommand.php index 33a95c7..dcc9031 100644 --- a/src/Commands/InstallCommand.php +++ b/src/Commands/InstallCommand.php @@ -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; }); @@ -140,7 +140,7 @@ private function updateAppLayoutsUsingMix() $file, str_replace( "", - '', + '', File::get($file), ), )); @@ -157,7 +157,7 @@ private function updateAppLayoutsUsingVite() $file, preg_replace( '/\@vite.*/', - '', + '', File::get($file), ), )) @@ -182,7 +182,7 @@ private function appendImportmapTagsToLayoutsHead(): void $file, preg_replace( '/(\s*)(<\/head>)/', - "\\1 \n\\1\\2", + "\\1 \n\\1\\2", File::get($file), ), )); diff --git a/src/Http/Middleware/AddLinkHeadersForPreloadedPins.php b/src/Http/Middleware/AddLinkHeadersForPreloadedPins.php new file mode 100644 index 0000000..43b0b79 --- /dev/null +++ b/src/Http/Middleware/AddLinkHeadersForPreloadedPins.php @@ -0,0 +1,31 @@ +assetsResolver)) { + $response->header('Link', collect($preloaded) + ->map(fn ($url) => "<{$url}>; rel=\"modulepreload\"") + ->join(', ')); + } + }); + } +} diff --git a/src/Importmap.php b/src/Importmap.php index d62b8e7..5c227dc 100755 --- a/src/Importmap.php +++ b/src/Importmap.php @@ -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)); } diff --git a/src/ImportmapLaravelServiceProvider.php b/src/ImportmapLaravelServiceProvider.php index 69593d0..4ccb11d 100644 --- a/src/ImportmapLaravelServiceProvider.php +++ b/src/ImportmapLaravelServiceProvider.php @@ -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 { @@ -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) @@ -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'); + }); } } diff --git a/src/View/Components/Tags.php b/src/View/Components/Tags.php deleted file mode 100644 index 9222f2e..0000000 --- a/src/View/Components/Tags.php +++ /dev/null @@ -1,28 +0,0 @@ - $this->importmap?->asArray($resolver) ?? ImportmapFacade::asArray($resolver), - 'preloadedModules' => $this->importmap?->preloadedModulePaths($resolver) ?? ImportmapFacade::preloadedModulePaths($resolver), - ]); - } -} diff --git a/tests/ImportmapTest.php b/tests/ImportmapTest.php index f794ef5..b61c1b5 100644 --- a/tests/ImportmapTest.php +++ b/tests/ImportmapTest.php @@ -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); @@ -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); } } diff --git a/tests/PreloadingWithLinkHeadersTest.php b/tests/PreloadingWithLinkHeadersTest.php new file mode 100644 index 0000000..1e37f40 --- /dev/null +++ b/tests/PreloadingWithLinkHeadersTest.php @@ -0,0 +1,54 @@ +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( + '; rel="modulepreload", ; rel="modulepreload", ; rel="modulepreload", ; rel="modulepreload", ; rel="modulepreload", ; rel="modulepreload", ; rel="modulepreload", ; rel="modulepreload", ; rel="modulepreload"', + $response->headers->get('Link'), + ); + } +} diff --git a/tests/TagsComponentTest.php b/tests/TagsComponentTest.php index 073a135..f2b623b 100644 --- a/tests/TagsComponentTest.php +++ b/tests/TagsComponentTest.php @@ -33,14 +33,14 @@ protected function setUp(): void /** @test */ public function generates_tags_without_nonce() { - $this->blade('') + $this->blade('') ->assertSee('', escape: false); } /** @test */ public function uses_given_csp_nonce() { - $this->blade('') + $this->blade('') ->assertSee('', escape: false); } @@ -51,7 +51,7 @@ public function uses_custom_map() $importmap->pin('foo', preload: true); $importmap->pin('bar', preload: true); - $this->blade('', ['importmap' => $importmap]) + $this->blade('', ['importmap' => $importmap]) ->assertSee('', escape: false) ->assertSee('', escape: false) ->assertDontSee('', escape: false); diff --git a/tests/TestCase.php b/tests/TestCase.php index 15d69bf..c681f3d 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -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'; diff --git a/tests/fixtures/npm/single-quote-importmap.php b/tests/fixtures/npm/single-quote-importmap.php index 53e0b2b..eeda064 100644 --- a/tests/fixtures/npm/single-quote-importmap.php +++ b/tests/fixtures/npm/single-quote-importmap.php @@ -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);