From 7cedb0ffcd8b0c322ca4b0fac79b1e70c203bfd9 Mon Sep 17 00:00:00 2001 From: indy koning Date: Thu, 14 Nov 2024 17:00:59 +0100 Subject: [PATCH 1/7] Add support for HTTP/3 Early Hints --- composer.json | 3 ++- config/rapidez/routing.php | 4 ++++ src/RapidezServiceProvider.php | 10 ++++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index be1cca24b..ab03dc250 100644 --- a/composer.json +++ b/composer.json @@ -18,13 +18,14 @@ "require": { "php": "^8.1|^8.2|^8.3", "blade-ui-kit/blade-heroicons": "^2.4", - "mailerlite/laravel-elasticsearch": "^11.1", "illuminate/database": "^11.0", "illuminate/events": "^11.0", "illuminate/queue": "^11.0", "illuminate/support": "^11.0", + "justbetter/laravel-http3earlyhints": "*", "lcobucci/clock": "^2.0|^3.2", "lcobucci/jwt": "^4.0|^5.3", + "mailerlite/laravel-elasticsearch": "^11.1", "rapidez/blade-directives": "^0.6", "tormjens/eventy": "^0.8" }, diff --git a/config/rapidez/routing.php b/config/rapidez/routing.php index 3b75cab58..e896d72f2 100644 --- a/config/rapidez/routing.php +++ b/config/rapidez/routing.php @@ -23,4 +23,8 @@ // This does not cache the response, it caches the controller used for that page. 'cache_duration' => 3600, ], + + 'earlyhints' => [ + 'enabled' => env('EARLY_HINTS_ENABLED', true), + ] ]; diff --git a/src/RapidezServiceProvider.php b/src/RapidezServiceProvider.php index 5af9c583c..ac33fe4a6 100644 --- a/src/RapidezServiceProvider.php +++ b/src/RapidezServiceProvider.php @@ -146,6 +146,16 @@ protected function bootRoutes(): self RapidezFacade::addFallbackRoute(CmsPageController::class, 10); RapidezFacade::addFallbackRoute(LegacyFallbackController::class, 99999); + if (!app()->runningInConsole() && config('rapidez.routing.earlyhints.enabled', true)) { + $this->app->call(function (\Illuminate\Contracts\Http\Kernel $kernel) { + /** @var \Illuminate\Foundation\Http\Kernel $kernel */ + $middlewares = $kernel->getGlobalMiddleware(); + $middlewares[] = \JustBetter\Http3EarlyHints\Middleware\AddHttp3EarlyHints::class; + + $kernel->setGlobalMiddleware($middlewares); + }); + } + return $this; } From c63ad56b79c88429904fe99e0d485eec0dd4c3e6 Mon Sep 17 00:00:00 2001 From: indy koning Date: Fri, 15 Nov 2024 15:34:42 +0100 Subject: [PATCH 2/7] On initial page load run init before turbo:load has been fired --- resources/js/package.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/resources/js/package.js b/resources/js/package.js index 20f3a0f36..127a35e05 100644 --- a/resources/js/package.js +++ b/resources/js/package.js @@ -25,6 +25,10 @@ import './callbacks' import './vue-components' function init() { + if (document.body.contains(window.app.$el)) { + return; + } + // https://vuejs.org/api/application.html#app-config-performance Vue.config.performance = import.meta.env.VITE_PERFORMANCE == 'true' Vue.prototype.window = window @@ -152,3 +156,4 @@ function init() { document.dispatchEvent(event) } document.addEventListener('turbo:load', init) +setTimeout(init) From 17c2a3033a7b1501d0d594b5d4425addd9a8ad63 Mon Sep 17 00:00:00 2001 From: indy koning Date: Mon, 18 Nov 2024 17:25:40 +0100 Subject: [PATCH 3/7] Remove impact on LCP --- resources/js/package.js | 193 +++++++++++++++++++++------------------- 1 file changed, 102 insertions(+), 91 deletions(-) diff --git a/resources/js/package.js b/resources/js/package.js index 127a35e05..0021e74b9 100644 --- a/resources/js/package.js +++ b/resources/js/package.js @@ -24,6 +24,34 @@ import './cookies' import './callbacks' import './vue-components' +if (import.meta.env.VITE_DEBUG === 'true') { + document.addEventListener('vue:loaded', () => { + window.app.$on('notification-message', function (message, type, params, link) { + switch (type) { + case 'error': + console.error(...arguments) + break + case 'warning': + console.warn(...arguments) + break + case 'success': + case 'info': + default: + console.log(...arguments) + } + }) + }) +} + +document.addEventListener('vue:loaded', () => { + const lastStoreCode = useLocalStorage('last_store_code', window.config.store_code) + if (lastStoreCode.value !== window.config.store_code) { + clearAttributes() + clearSwatches() + lastStoreCode.value = window.config.store_code + } +}) + function init() { if (document.body.contains(window.app.$el)) { return; @@ -63,97 +91,80 @@ function init() { custom_attributes: [], } - window.app = new Vue({ - el: '#app', - data: { - custom: {}, - config: window.config, - loadingCount: 0, - loading: false, - loadAutocomplete: false, - csrfToken: document.querySelector('[name=csrf-token]').content, - cart: useCart(), - order: useOrder(), - user: useUser(), - mask: useMask(), - showTax: window.config.show_tax, - swatches: swatches, - scrollLock: useScrollLock(document.body), - }, - methods: { - search(value) { - if (value.length) { - Turbo.visit(window.url('/search?q=' + encodeURIComponent(value))) - } - }, - setSearchParams(url) { - window.history.pushState(window.history.state, '', new URL(url)) - }, - toggleScroll(bool = null) { - if (bool === null) { - this.scrollLock = !this.scrollLock - } else { - this.scrollLock = bool - } - }, - resizedPath(imagePath, size, store = null) { - if (!store) { - store = window.config.store - } - - let url = new URL(imagePath) - url = url.pathname.replace('/media', '') - - return `/storage/${store}/resizes/${size}/magento${url}` - }, - }, - computed: { - // Wrap the local storage in getter and setter functions so you do not have to interact using .value - guestEmail: wrapValue( - useLocalStorage('email', window.debug ? 'wayne@enterprises.com' : '', { serializer: StorageSerializers.string }), - ), - - loggedIn() { - return this.user?.is_logged_in - }, - - hasCart() { - return this.cart?.id && this.cart.items.length - }, - }, - watch: { - loadingCount: function (count) { - window.app.$data.loading = count > 0 - }, - }, - }) - - const lastStoreCode = useLocalStorage('last_store_code', window.config.store_code) - if (lastStoreCode.value !== window.config.store_code) { - clearAttributes() - clearSwatches() - lastStoreCode.value = window.config.store_code - } - - if (window.debug) { - window.app.$on('notification-message', function (message, type, params, link) { - switch (type) { - case 'error': - console.error(...arguments) - break - case 'warning': - console.warn(...arguments) - break - case 'success': - case 'info': - default: - console.log(...arguments) - } - }) - } - - const event = new CustomEvent('vue:loaded', { detail: { vue: window.app } }) - document.dispatchEvent(event) + requestAnimationFrame( + () => { + window.app = new Vue({ + el: '#app', + data: { + custom: {}, + config: window.config, + loadingCount: 0, + loading: false, + loadAutocomplete: false, + csrfToken: document.querySelector('[name=csrf-token]').content, + cart: useCart(), + order: useOrder(), + user: useUser(), + mask: useMask(), + showTax: window.config.show_tax, + swatches: swatches, + scrollLock: useScrollLock(document.body), + }, + methods: { + search(value) { + if (value.length) { + Turbo.visit(window.url('/search?q=' + encodeURIComponent(value))) + } + }, + setSearchParams(url) { + window.history.pushState(window.history.state, '', new URL(url)) + }, + toggleScroll(bool = null) { + if (bool === null) { + this.scrollLock = !this.scrollLock + } else { + this.scrollLock = bool + } + }, + resizedPath(imagePath, size, store = null) { + if (!store) { + store = window.config.store + } + + let url = new URL(imagePath) + url = url.pathname.replace('/media', '') + + return `/storage/${store}/resizes/${size}/magento${url}` + }, + }, + computed: { + // Wrap the local storage in getter and setter functions so you do not have to interact using .value + guestEmail: wrapValue( + useLocalStorage('email', window.debug ? 'wayne@enterprises.com' : '', { serializer: StorageSerializers.string }), + ), + + loggedIn() { + return this.user?.is_logged_in + }, + + hasCart() { + return this.cart?.id && this.cart.items.length + }, + }, + watch: { + loadingCount: function (count) { + window.app.$data.loading = count > 0 + }, + }, + }) + + setTimeout(() => { + const event = new CustomEvent('vue:loaded', { detail: { vue: window.app } }) + document.dispatchEvent(event) + }) + } + ) } + document.addEventListener('turbo:load', init) setTimeout(init) From 0d1783fb2772902aef189a3f29a5d2edf089fdf5 Mon Sep 17 00:00:00 2001 From: indy koning Date: Wed, 20 Nov 2024 16:01:09 +0100 Subject: [PATCH 4/7] Add wait until vue loaded --- tests/Browser/CartTest.php | 3 +++ tests/Browser/CheckoutTest.php | 2 ++ tests/Browser/DialogTest.php | 1 + tests/Browser/HomepageTest.php | 2 +- tests/Browser/NewsletterTest.php | 1 + tests/DuskTestCaseSetup.php | 7 +++++++ 6 files changed, 15 insertions(+), 1 deletion(-) diff --git a/tests/Browser/CartTest.php b/tests/Browser/CartTest.php index 604b38a9d..44b51a5fa 100644 --- a/tests/Browser/CartTest.php +++ b/tests/Browser/CartTest.php @@ -15,6 +15,7 @@ public function addSimpleProduct() $this->browse(function (Browser $browser) { $browser->addProductToCart($this->testProduct->url) ->visit('/cart') + ->waitUntilVueLoaded() ->waitUntilIdle() ->waitFor('@cart-content', 15) ->waitUntilIdle() @@ -43,6 +44,7 @@ public function changeProductQty() { $this->browse(function (Browser $browser) { $browser->visit('/cart') + ->waitUntilVueLoaded() ->waitUntilIdle() ->waitFor('@cart-content', 15) ->waitUntilIdle() @@ -60,6 +62,7 @@ public function removeProduct() { $this->browse(function (Browser $browser) { $browser->visit('/cart') + ->waitUntilVueLoaded() ->waitUntilIdle() ->waitFor('@cart-content', 15) ->waitUntilIdle() diff --git a/tests/Browser/CheckoutTest.php b/tests/Browser/CheckoutTest.php index 4784c3988..ad83f049b 100644 --- a/tests/Browser/CheckoutTest.php +++ b/tests/Browser/CheckoutTest.php @@ -34,6 +34,7 @@ public function checkoutAsUser() // Go through checkout as guest and log in. $this->browse(function (Browser $browser) use ($email) { $browser->waitForReload(fn ($browser) => $browser->visit('/'), 4) + ->waitUntilVueLoaded() ->waitUntilIdle() ->waitFor('@account_menu') ->click('@account_menu') @@ -71,6 +72,7 @@ public function doCheckoutLogin(Browser $browser, $email = false, $password = fa { $browser ->visit('/checkout') + ->waitUntilVueLoaded() ->waitUntilIdle() ->type('@email', $email ?: 'wayne@enterprises.com') ->waitUntilIdle(); diff --git a/tests/Browser/DialogTest.php b/tests/Browser/DialogTest.php index a4e0ab516..53190b94a 100644 --- a/tests/Browser/DialogTest.php +++ b/tests/Browser/DialogTest.php @@ -14,6 +14,7 @@ public function test() { $this->browse(function (Browser $browser) { $browser->visit('/?show-cookie-notice') + ->waitUntilVueLoaded() ->waitUntilIdle() ->assertSee('Accept cookies') ->waitForReload(function (Browser $browser) { diff --git a/tests/Browser/HomepageTest.php b/tests/Browser/HomepageTest.php index 32df98ba8..d9d2b8582 100644 --- a/tests/Browser/HomepageTest.php +++ b/tests/Browser/HomepageTest.php @@ -13,7 +13,7 @@ class HomepageTest extends DuskTestCase public function homepage() { $this->browse(function (Browser $browser) { - $browser->visit('/')->assertSee('All rights reserved.'); + $browser->visit('/')->waitUntilVueLoaded()->assertSee('All rights reserved.'); }); } } diff --git a/tests/Browser/NewsletterTest.php b/tests/Browser/NewsletterTest.php index 54a466b73..0dffd043b 100644 --- a/tests/Browser/NewsletterTest.php +++ b/tests/Browser/NewsletterTest.php @@ -18,6 +18,7 @@ public function test() $this->browse(function (Browser $browser) { $email = $this->faker->email; $browser->visit('/') + ->waitUntilVueLoaded() ->scrollIntoView('@newsletter') ->waitUntilIdle() ->type('@newsletter-email', $email) diff --git a/tests/DuskTestCaseSetup.php b/tests/DuskTestCaseSetup.php index 1556d074e..74c031f0e 100644 --- a/tests/DuskTestCaseSetup.php +++ b/tests/DuskTestCaseSetup.php @@ -44,6 +44,13 @@ protected function setUp(): void return $this; }); + Browser::macro('waitUntilVueLoaded', function () { + /** @var Browser $this */ + $this->waitUntil('document.body.contains(window.app.$el) || await new Promise((resolve, reject) => document.addEventListener("vue:loaded", resolve))', 120); + + return $this; + }); + Browser::macro('assertFormValid', function ($selector) { /** @var Browser $this */ $fullSelector = $this->resolver->format($selector); From e6fa8e718a4846222775daa0864dba4308d01acb Mon Sep 17 00:00:00 2001 From: indy koning Date: Thu, 21 Nov 2024 12:57:24 +0100 Subject: [PATCH 5/7] Wait until add-to-cart is enabled --- tests/DuskTestCaseSetup.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/DuskTestCaseSetup.php b/tests/DuskTestCaseSetup.php index 74c031f0e..a86c53e72 100644 --- a/tests/DuskTestCaseSetup.php +++ b/tests/DuskTestCaseSetup.php @@ -67,12 +67,14 @@ protected function setUp(): void /** @var Browser $this */ if ($productUrl) { $this - ->visit($productUrl); + ->visit($productUrl) + ->waitUntilVueLoaded(); } // @phpstan-ignore-next-line $this ->waitUntilIdle() + ->waitUntilEnabled('@add-to-cart') ->pressAndWaitFor('@add-to-cart', 60) ->waitForText('Added', 60) ->waitUntilIdle(); From 8b8fedabcfb5e37dbe1b1714d045385e7a571850 Mon Sep 17 00:00:00 2001 From: indy koning Date: Thu, 21 Nov 2024 13:24:40 +0100 Subject: [PATCH 6/7] Raise timeout --- tests/DuskTestCaseSetup.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/DuskTestCaseSetup.php b/tests/DuskTestCaseSetup.php index a86c53e72..041d71f3c 100644 --- a/tests/DuskTestCaseSetup.php +++ b/tests/DuskTestCaseSetup.php @@ -75,7 +75,7 @@ protected function setUp(): void $this ->waitUntilIdle() ->waitUntilEnabled('@add-to-cart') - ->pressAndWaitFor('@add-to-cart', 60) + ->pressAndWaitFor('@add-to-cart', 120) ->waitForText('Added', 60) ->waitUntilIdle(); From 9c2adcc16e051d94ac52a849f7368d97e8d8fde4 Mon Sep 17 00:00:00 2001 From: indy koning Date: Tue, 26 Nov 2024 13:29:53 +0100 Subject: [PATCH 7/7] Improve order of loading --- resources/js/package.js | 11 ++++++++--- resources/views/components/productlist.blade.php | 2 +- tests/DuskTestCaseSetup.php | 12 ++++++++---- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/resources/js/package.js b/resources/js/package.js index a6d7bfed8..d39aa9752 100644 --- a/resources/js/package.js +++ b/resources/js/package.js @@ -15,11 +15,10 @@ import useMask from './stores/useMask' import { swatches, clear as clearSwatches } from './stores/useSwatches' import { clear as clearAttributes } from './stores/useAttributes.js' import './vue' -import { computed } from 'vue' import './fetch' import './filters' import './mixins' -import './turbolinks' +(() => import('./turbolinks'))() import './cookies' import './callbacks' import './vue-components' @@ -116,7 +115,7 @@ function init() { Turbo.visit(window.url('/search?q=' + encodeURIComponent(value))) } }, - + setSearchParams(url) { window.history.pushState(window.history.state, '', new URL(url)) }, @@ -164,6 +163,12 @@ function init() { window.app.$data.loading = count > 0 }, }, + mounted() { + setTimeout(() => { + const event = new CustomEvent('vue:mounted', { detail: { vue: window.app } }) + document.dispatchEvent(event) + }) + } }) setTimeout(() => { diff --git a/resources/views/components/productlist.blade.php b/resources/views/components/productlist.blade.php index 341a264f0..7e64018e6 100644 --- a/resources/views/components/productlist.blade.php +++ b/resources/views/components/productlist.blade.php @@ -24,7 +24,7 @@
-
+
diff --git a/tests/DuskTestCaseSetup.php b/tests/DuskTestCaseSetup.php index 041d71f3c..6ae47bdc8 100644 --- a/tests/DuskTestCaseSetup.php +++ b/tests/DuskTestCaseSetup.php @@ -46,7 +46,10 @@ protected function setUp(): void Browser::macro('waitUntilVueLoaded', function () { /** @var Browser $this */ - $this->waitUntil('document.body.contains(window.app.$el) || await new Promise((resolve, reject) => document.addEventListener("vue:loaded", resolve))', 120); + $this + ->waitUntilIdle() + ->waitUntilTrueForDuration('document.body.contains(window.app?.$el) && window.app?._isMounted && console.log("mounted") === undefined', 10, 1) + ->waitUntilIdle(); return $this; }); @@ -68,15 +71,16 @@ protected function setUp(): void if ($productUrl) { $this ->visit($productUrl) - ->waitUntilVueLoaded(); + ->waitUntilVueLoaded() + ->waitUntilIdle(); } // @phpstan-ignore-next-line $this ->waitUntilIdle() - ->waitUntilEnabled('@add-to-cart') + ->waitUntilEnabled('@add-to-cart', 200) ->pressAndWaitFor('@add-to-cart', 120) - ->waitForText('Added', 60) + ->waitForText(__('Added'), 120) ->waitUntilIdle(); return $this;