diff --git a/bifrost/lib/turbolinks/mergeHead.ts b/bifrost/lib/turbolinks/mergeHead.ts index 8e7c685..01af97c 100644 --- a/bifrost/lib/turbolinks/mergeHead.ts +++ b/bifrost/lib/turbolinks/mergeHead.ts @@ -8,18 +8,22 @@ const allHeadScriptsEverRun: { [outerHTML: string]: ElementDetails } = {}; let firstMerge = true; let lastTrackedScriptSignature: string; -export const recordExistingHeadScripts = instrument("recordExistingHeadScripts", function recordExistingHeadScripts(categorizedHead?: CategorizedHead) { - categorizedHead ||= categorizeHead(document.head); - // record all existing head scripts as having been run, because they were run by browser, not mergeHead - for (const element of categorizedHead.scripts) { +// Scripts already in the document were run by the browser, so mergeHead must never run them again (e.g. when restoring a snapshot) +export const recordExistingHeadScripts = instrument("recordExistingHeadScripts", function recordExistingHeadScripts() { + for (const element of categorizeHead(document.head).scripts) { allHeadScriptsEverRun[element.outerHTML] = { tracked: elementIsTracked(element), }; } +}); + +// Only a wrapped SSR page defines the tracked-element baseline. A vite page has none, so its first merge into a wrapped page must never reload. +export function recordTrackedElementBaseline() { lastTrackedScriptSignature = - lastTrackedScriptSignature || trackedElementSignature(categorizedHead); + lastTrackedScriptSignature || + trackedElementSignature(categorizeHead(document.head)); firstMerge = false; -}); +} // Returns function which resolves when all new blocking head scripts have loaded export function mergeHead(head: HTMLHeadElement) { diff --git a/bifrost/renderer/bifrost/onBeforeRenderClient.ts b/bifrost/renderer/bifrost/onBeforeRenderClient.ts index c51cb8d..07a75e4 100644 --- a/bifrost/renderer/bifrost/onBeforeRenderClient.ts +++ b/bifrost/renderer/bifrost/onBeforeRenderClient.ts @@ -1,12 +1,18 @@ import { PageContextClient } from "vike/types"; import { Turbolinks } from "../../lib/turbolinks"; import { setBodyAttributes } from "../../lib/elementUtils"; +import { recordExistingHeadScripts } from "../../lib/turbolinks/mergeHead"; import { instrument } from "../../lib/diagnostic.client"; export default instrument("bifrostOnBeforeRenderClient", async function bifrostOnBeforeRenderClient( pageContext: PageContextClient ) { - if (!pageContext.isHydration && !pageContext.errorWhileRendering) { + if (pageContext.isHydration) { + // Snapshots clone the whole head, so anything already here (including the inline Turbolinks stub) would re-run on restore + recordExistingHeadScripts(); + return; + } + if (!pageContext.errorWhileRendering) { pageContext._shouldEmitBeforeRender = true; await Turbolinks._vikeBeforeRender(pageContext._turbolinksVisit); diff --git a/bifrost/renderer/headHtmlBegin.ts b/bifrost/renderer/headHtmlBegin.ts index 5013550..833b01b 100644 --- a/bifrost/renderer/headHtmlBegin.ts +++ b/bifrost/renderer/headHtmlBegin.ts @@ -2,7 +2,7 @@ // Register turbolinks global for ios to hook into // Also allows Turbolinks.visit / Turbolinks.controller.viewWillRender to be called before Turbolinks is started (Vike loads async) -const turbolinksIOSCompat = `window.Turbolinks = {controller:{restorationIdentifier: '',started:false,viewWillRender:(...a)=>{addEventListener("turbolinks:start", () => {window.Turbolinks.controller.viewWillRender(...a)})}},visit:(...a)=>{addEventListener("turbolinks:start", () => {window.Turbolinks.visit(...a)})}};`; +const turbolinksIOSCompat = `if(!(window.Turbolinks&&window.Turbolinks.controller&&window.Turbolinks.controller.started)){window.Turbolinks = {controller:{restorationIdentifier: '',started:false,viewWillRender:(...a)=>{addEventListener("turbolinks:start", () => {window.Turbolinks.controller.viewWillRender(...a)})}},visit:(...a)=>{addEventListener("turbolinks:start", () => {window.Turbolinks.visit(...a)})}}};`; /// emit turbolinks:load on DOMContentLoaded const turbolinksLoadEvent = `addEventListener("DOMContentLoaded", () => { diff --git a/bifrost/renderer/wrapped/onBeforeRenderClient.ts b/bifrost/renderer/wrapped/onBeforeRenderClient.ts index 3d1c2ca..7b328ff 100644 --- a/bifrost/renderer/wrapped/onBeforeRenderClient.ts +++ b/bifrost/renderer/wrapped/onBeforeRenderClient.ts @@ -4,6 +4,7 @@ import { Turbolinks } from "../../lib/turbolinks"; import { mergeHead, recordExistingHeadScripts, + recordTrackedElementBaseline, } from "../../lib/turbolinks/mergeHead"; import { setBodyAttributes, @@ -37,6 +38,7 @@ export default instrument("wrappedOnBeforeRenderClient", async function wrappedO proxyLayoutInfo: pageContext.proxyLayoutInfo, }); recordExistingHeadScripts(); + recordTrackedElementBaseline(); return; } diff --git a/tests/e2e/specs/e2e.spec.ts b/tests/e2e/specs/e2e.spec.ts index 0de8c6e..2df0a59 100644 --- a/tests/e2e/specs/e2e.spec.ts +++ b/tests/e2e/specs/e2e.spec.ts @@ -1136,6 +1136,43 @@ test.describe("back button restoration", () => { await customProxy.goForward(); }); + test("history nav keeps working after entering on a vite page", async ({ + page, + }) => { + ensureAllNetworkSucceeds(page); + const first = { + title: "first page", + content: "first page body content", + links: [{ title: "second page", content: "second page body content" }], + }; + const customProxy = new CustomProxyPage(page, first); + + await page.goto("./vite-page", { waitUntil: "networkidle" }); + await waitForTurbolinksInit(page); + await ensureNoBrowserNavigation(page, async () => { + const loaded = waitForConsoleLog(page, (m) => m.text() === T.load); + await page.evaluate( + (path) => (window as any).Turbolinks.visit(path), + toPath(first) + ); + await loaded; + }); + customProxy.pageData = first; + await expect(page).toHaveTitle("first page"); + + await customProxy.clickLink("second page"); + + await ensureNoBrowserNavigation(page, async () => { + await customProxy.goBack(); + // restoring the snapshot must not re-run the vite page's inline Turbolinks stub + expect( + await page.evaluate("window.Turbolinks.controller.started") + ).toBe(true); + await customProxy.goForward(); + await customProxy.goBack(); + }); + }); + test("restores layout", async ({ page, context }) => { const customProxy = new CustomProxyPage(page, { title: "first page",