Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Preload default #44

Merged
merged 11 commits into from
Jan 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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/[email protected]/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);