diff --git a/apps/tests/package.json b/apps/tests/package.json index 387c3378c..006f07e82 100644 --- a/apps/tests/package.json +++ b/apps/tests/package.json @@ -14,7 +14,7 @@ "test:all": "npm run unit:ci && npm run e2e" }, "dependencies": { - "@solidjs/meta": "^0.29.4", + "@kobalte/core": "^0.13.11", "@solidjs/router": "^0.15.3", "@solidjs/start": "workspace:*", "@solidjs/testing-library": "^0.8.10", @@ -24,6 +24,7 @@ "@vitest/ui": "^4.0.10", "jsdom": "^25.0.1", "lodash": "^4.17.21", + "lucide-solid": "^0.577.0", "solid-js": "next", "vite": "^7.1.10", "vite-plugin-solid": "^3.0.0-next.0", diff --git a/apps/tests/src/app.css b/apps/tests/src/app.css index a4d2e55c4..94c75beb9 100644 --- a/apps/tests/src/app.css +++ b/apps/tests/src/app.css @@ -1,5 +1,6 @@ body { - font-family: Gordita, Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; + font-family: + Gordita, Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; } a { @@ -59,3 +60,168 @@ p { .increment:active { background-color: rgba(68, 107, 158, 0.2); } + +.hydration-scroll-root { + color-scheme: light; + color: #1e2b21; + background: radial-gradient(circle at top, #e8fff5 0%, #f7f6f0 35%, #efe9de 100%); +} + +.hydration-scroll-root a { + margin-right: 0; +} + +.hydration-scroll-shell { + min-height: 100vh; + padding: 16px; +} + +.hydration-scroll-frame { + max-width: 960px; + margin: 0 auto; +} + +.hydration-scroll-header { + position: sticky; + top: 12px; + z-index: 10; + display: flex; + flex-direction: column; + gap: 12px; + padding: 16px; + border: 1px solid rgba(30, 43, 33, 0.12); + border-radius: 20px; + background: rgba(255, 253, 247, 0.9); + backdrop-filter: blur(14px); + box-shadow: 0 18px 40px rgba(42, 53, 43, 0.1); +} + +.hydration-scroll-header-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; +} + +.hydration-scroll-brand { + font-size: 0.85rem; + font-weight: 700; + letter-spacing: 0.26em; + text-transform: uppercase; +} + +.hydration-scroll-desktop-nav { + display: none; + gap: 10px; +} + +.hydration-scroll-desktop-nav a, +.hydration-scroll-mobile-nav a { + padding: 10px 14px; + border-radius: 999px; + background: rgba(30, 43, 33, 0.06); +} + +.hydration-scroll-trigger { + display: inline-flex; + align-items: center; + gap: 10px; + padding: 11px 14px; + border: 1px solid rgba(30, 43, 33, 0.15); + border-radius: 999px; + background: #fff; + font-size: 0.75rem; + font-weight: 700; + letter-spacing: 0.18em; + text-transform: uppercase; + color: #3a4b40; +} + +.hydration-scroll-trigger-label { + display: inline-flex; + align-items: center; +} + +.hydration-scroll-chevron { + width: 16px; + height: 16px; + flex: none; +} + +.hydration-scroll-panel { + padding-top: 12px; + border-top: 1px solid rgba(30, 43, 33, 0.12); +} + +.hydration-scroll-mobile-nav { + display: flex; + flex-direction: column; + gap: 10px; +} + +.hydration-scroll-main { + padding: 32px 0 64px; +} + +.hydration-scroll-hero { + padding: 24px; + border-radius: 28px; + background: rgba(255, 253, 247, 0.78); + border: 1px solid rgba(30, 43, 33, 0.12); +} + +.hydration-scroll-eyebrow { + margin: 0 0 12px; + font-size: 0.78rem; + font-weight: 700; + letter-spacing: 0.22em; + text-transform: uppercase; + color: #54675b; +} + +.hydration-scroll-hero h1 { + margin: 0 0 16px; + font-size: clamp(2.4rem, 6vw, 4.6rem); + line-height: 0.95; +} + +.hydration-scroll-hero p { + margin: 0; + max-width: 48rem; + line-height: 1.7; +} + +.hydration-scroll-stack { + display: grid; + gap: 16px; + margin-top: 24px; +} + +.hydration-scroll-panel-card { + padding: 20px 22px; + border-radius: 22px; + background: rgba(255, 255, 255, 0.66); + border: 1px solid rgba(30, 43, 33, 0.1); +} + +.hydration-scroll-panel-card h2 { + margin: 0 0 10px; + font-size: 1.1rem; +} + +.hydration-scroll-panel-card p { + margin: 0; + max-width: none; + line-height: 1.6; +} + +@media (min-width: 960px) { + .hydration-scroll-desktop-nav { + display: flex; + } + + .hydration-scroll-trigger, + .hydration-scroll-panel { + display: none; + } +} diff --git a/apps/tests/src/app.tsx b/apps/tests/src/app.tsx index 83532f5ee..0124207fe 100644 --- a/apps/tests/src/app.tsx +++ b/apps/tests/src/app.tsx @@ -1,7 +1,7 @@ -import { MetaProvider, Title } from "@solidjs/meta"; import { Router } from "@solidjs/router"; import { FileRoutes } from "@solidjs/start/router"; import { Loading } from "solid-js"; +import { MetaProvider, Title } from "./meta"; import "./app.css"; export default function App() { @@ -56,6 +56,9 @@ export default function App() {
  • Text Plain Response
  • +
  • + Hydration Scroll Repro +
  • referencing multiple export named functions in the same file diff --git a/apps/tests/src/e2e/hydration-scroll.test.ts b/apps/tests/src/e2e/hydration-scroll.test.ts new file mode 100644 index 000000000..1c6fd9419 --- /dev/null +++ b/apps/tests/src/e2e/hydration-scroll.test.ts @@ -0,0 +1,41 @@ +import { expect, test } from "@playwright/test"; + +const isHydrationMismatch = (text: string) => + text.includes("Hydration Mismatch") || + text.includes("Hydration mismatch") || + text.includes("Unable to find DOM nodes for hydration key"); + +test.describe("SSR Hydration Scroll Repro", () => { + test("should not emit hydration mismatches on the first downward scroll", async ({ page }) => { + const mismatchMessages: string[] = []; + + page.on("console", msg => { + if (msg.type() === "error" || msg.type() === "warning") { + const text = msg.text(); + if (isHydrationMismatch(text)) { + mismatchMessages.push(text); + } + } + }); + + page.on("pageerror", error => { + if (isHydrationMismatch(error.message)) { + mismatchMessages.push(error.message); + } + }); + + await page.setViewportSize({ width: 390, height: 844 }); + await page.goto("/hydration-scroll-repro"); + + await expect(page.getByTestId("hydration-scroll-trigger")).toBeVisible(); + await expect(page.getByTestId("hydration-scroll-chevron")).toBeVisible(); + + await page.mouse.wheel(0, 1000); + await page.waitForTimeout(250); + + expect( + mismatchMessages, + "Expected no hydration mismatch after the first downward scroll", + ).toEqual([]); + }); +}); diff --git a/apps/tests/src/meta.tsx b/apps/tests/src/meta.tsx new file mode 100644 index 000000000..6c7a87072 --- /dev/null +++ b/apps/tests/src/meta.tsx @@ -0,0 +1,20 @@ +import { children, createRenderEffect, type JSX, type ParentProps } from "solid-js"; + +export function MetaProvider(props: ParentProps) { + return props.children; +} + +export function Title(props: { children?: JSX.Element }) { + const resolved = children(() => props.children); + + if (!import.meta.env.SSR) { + createRenderEffect(() => { + const value = resolved.toArray().join(""); + if (value) { + document.title = value; + } + }); + } + + return null; +} diff --git a/apps/tests/src/routes/[...404].tsx b/apps/tests/src/routes/[...404].tsx index f1d7221c8..493e40c4b 100644 --- a/apps/tests/src/routes/[...404].tsx +++ b/apps/tests/src/routes/[...404].tsx @@ -1,4 +1,4 @@ -import { Title } from "@solidjs/meta"; +import { Title } from "../meta"; import { HttpStatusCode } from "@solidjs/start"; import type { APIEvent } from "@solidjs/start/server"; diff --git a/apps/tests/src/routes/hydration-scroll-repro.tsx b/apps/tests/src/routes/hydration-scroll-repro.tsx new file mode 100644 index 000000000..8f7945fec --- /dev/null +++ b/apps/tests/src/routes/hydration-scroll-repro.tsx @@ -0,0 +1,109 @@ +import * as Collapsible from "@kobalte/core/collapsible"; +import { A, useLocation } from "@solidjs/router"; +import { ChevronDown } from "lucide-solid"; +import { createEffect, createSignal, For, splitProps, type ParentProps } from "solid-js"; +import { Title } from "../meta"; + +const navigation = [ + { href: "/", label: "Home" }, + { href: "/hydration-scroll-repro", label: "Repro" }, +] as const; + +const cards = Array.from({ length: 24 }, (_, index) => index + 1); + +function ScrollReproTrigger( + props: ParentProps, +) { + const [local, rest] = splitProps(props, ["children", "class"]); + + return ( + + {local.children} + + + ); +} + +function ScrollReproContent( + props: ParentProps, +) { + const [local, rest] = splitProps(props, ["children", "class"]); + + return ( + + {local.children} + + ); +} + +export default function HydrationScrollRepro() { + const location = useLocation(); + const [isMobileNavOpen, setIsMobileNavOpen] = createSignal(false); + + createEffect(() => { + location.pathname; + setIsMobileNavOpen(false); + }); + + return ( +
    + Hydration Scroll Repro +
    +
    +
    + +
    + + Alpha Shell Repro + + + + + + Menu + +
    + + + + + +
    + +
    +
    +

    SolidStart hydration regression guard

    +

    Scroll after first load and watch for hydration warnings.

    +

    + This route keeps the reproduction focused on a sticky shell, a Kobalte collapsible + trigger, and the Lucide chevron icon rendered inside that trigger. +

    +
    + +
    + + {card => ( +
    +

    Scroll target {card}

    +

    + The page is intentionally tall so the first vertical scroll happens after + hydration has started and the sticky shell stays mounted while the page moves. +

    +
    + )} +
    +
    +
    +
    +
    +
    + ); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0cddbd5f1..3a81f986c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -270,9 +270,9 @@ importers: apps/tests: dependencies: - '@solidjs/meta': - specifier: ^0.29.4 - version: 0.29.4(solid-js@2.0.0-beta.2) + '@kobalte/core': + specifier: ^0.13.11 + version: 0.13.11(solid-js@2.0.0-beta.2) '@solidjs/router': specifier: ^0.15.3 version: 0.15.4(solid-js@2.0.0-beta.2) @@ -300,6 +300,9 @@ importers: lodash: specifier: ^4.17.21 version: 4.17.21 + lucide-solid: + specifier: ^0.577.0 + version: 0.577.0(solid-js@2.0.0-beta.2) solid-js: specifier: next version: 2.0.0-beta.2 @@ -4077,6 +4080,11 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + lucide-solid@0.577.0: + resolution: {integrity: sha512-r/rsauBlyNjFlUhXCkD544tOH1GgcFFupw9oP2zZT4BiFkHoO3MTr12QfKBrS5zCRIhktc/qY2tRr925hFlNuQ==} + peerDependencies: + solid-js: ^1.4.7 + luxon@3.6.1: resolution: {integrity: sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==} engines: {node: '>=12'} @@ -6389,11 +6397,21 @@ snapshots: '@floating-ui/dom': 1.6.11 solid-js: 1.9.9 + '@corvu/utils@0.2.0(solid-js@2.0.0-beta.2)': + dependencies: + '@floating-ui/dom': 1.6.11 + solid-js: 2.0.0-beta.2 + '@corvu/utils@0.4.2(solid-js@1.9.9)': dependencies: '@floating-ui/dom': 1.6.11 solid-js: 1.9.9 + '@corvu/utils@0.4.2(solid-js@2.0.0-beta.2)': + dependencies: + '@floating-ui/dom': 1.6.11 + solid-js: 2.0.0-beta.2 + '@dabh/diagnostics@2.0.3': dependencies: colorspace: 1.1.4 @@ -6758,6 +6776,18 @@ snapshots: solid-presence: 0.1.8(solid-js@1.9.9) solid-prevent-scroll: 0.1.7(solid-js@1.9.9) + '@kobalte/core@0.13.11(solid-js@2.0.0-beta.2)': + dependencies: + '@floating-ui/dom': 1.6.11 + '@internationalized/date': 3.5.4 + '@internationalized/number': 3.5.3 + '@kobalte/utils': 0.9.1(solid-js@2.0.0-beta.2) + '@solid-primitives/props': 3.1.11(solid-js@2.0.0-beta.2) + '@solid-primitives/resize-observer': 2.0.26(solid-js@2.0.0-beta.2) + solid-js: 2.0.0-beta.2 + solid-presence: 0.1.8(solid-js@2.0.0-beta.2) + solid-prevent-scroll: 0.1.7(solid-js@2.0.0-beta.2) + '@kobalte/utils@0.9.1(solid-js@1.9.9)': dependencies: '@solid-primitives/event-listener': 2.3.3(solid-js@1.9.9) @@ -6769,6 +6799,17 @@ snapshots: '@solid-primitives/utils': 6.3.2(solid-js@1.9.9) solid-js: 1.9.9 + '@kobalte/utils@0.9.1(solid-js@2.0.0-beta.2)': + dependencies: + '@solid-primitives/event-listener': 2.3.3(solid-js@2.0.0-beta.2) + '@solid-primitives/keyed': 1.2.2(solid-js@2.0.0-beta.2) + '@solid-primitives/map': 0.4.11(solid-js@2.0.0-beta.2) + '@solid-primitives/media': 2.2.9(solid-js@2.0.0-beta.2) + '@solid-primitives/props': 3.1.11(solid-js@2.0.0-beta.2) + '@solid-primitives/refs': 1.1.2(solid-js@2.0.0-beta.2) + '@solid-primitives/utils': 6.3.2(solid-js@2.0.0-beta.2) + solid-js: 2.0.0-beta.2 + '@manypkg/find-root@1.1.0': dependencies: '@babel/runtime': 7.28.4 @@ -7525,15 +7566,29 @@ snapshots: '@solid-primitives/utils': 6.3.2(solid-js@1.9.9) solid-js: 1.9.9 + '@solid-primitives/event-listener@2.3.3(solid-js@2.0.0-beta.2)': + dependencies: + '@solid-primitives/utils': 6.3.2(solid-js@2.0.0-beta.2) + solid-js: 2.0.0-beta.2 + '@solid-primitives/keyed@1.2.2(solid-js@1.9.9)': dependencies: solid-js: 1.9.9 + '@solid-primitives/keyed@1.2.2(solid-js@2.0.0-beta.2)': + dependencies: + solid-js: 2.0.0-beta.2 + '@solid-primitives/map@0.4.11(solid-js@1.9.9)': dependencies: '@solid-primitives/trigger': 1.0.11(solid-js@1.9.9) solid-js: 1.9.9 + '@solid-primitives/map@0.4.11(solid-js@2.0.0-beta.2)': + dependencies: + '@solid-primitives/trigger': 1.0.11(solid-js@2.0.0-beta.2) + solid-js: 2.0.0-beta.2 + '@solid-primitives/media@2.2.9(solid-js@1.9.9)': dependencies: '@solid-primitives/event-listener': 2.3.3(solid-js@1.9.9) @@ -7542,16 +7597,34 @@ snapshots: '@solid-primitives/utils': 6.3.2(solid-js@1.9.9) solid-js: 1.9.9 + '@solid-primitives/media@2.2.9(solid-js@2.0.0-beta.2)': + dependencies: + '@solid-primitives/event-listener': 2.3.3(solid-js@2.0.0-beta.2) + '@solid-primitives/rootless': 1.4.5(solid-js@2.0.0-beta.2) + '@solid-primitives/static-store': 0.0.8(solid-js@2.0.0-beta.2) + '@solid-primitives/utils': 6.3.2(solid-js@2.0.0-beta.2) + solid-js: 2.0.0-beta.2 + '@solid-primitives/props@3.1.11(solid-js@1.9.9)': dependencies: '@solid-primitives/utils': 6.3.2(solid-js@1.9.9) solid-js: 1.9.9 + '@solid-primitives/props@3.1.11(solid-js@2.0.0-beta.2)': + dependencies: + '@solid-primitives/utils': 6.3.2(solid-js@2.0.0-beta.2) + solid-js: 2.0.0-beta.2 + '@solid-primitives/refs@1.1.2(solid-js@1.9.9)': dependencies: '@solid-primitives/utils': 6.3.2(solid-js@1.9.9) solid-js: 1.9.9 + '@solid-primitives/refs@1.1.2(solid-js@2.0.0-beta.2)': + dependencies: + '@solid-primitives/utils': 6.3.2(solid-js@2.0.0-beta.2) + solid-js: 2.0.0-beta.2 + '@solid-primitives/resize-observer@2.0.26(solid-js@1.9.9)': dependencies: '@solid-primitives/event-listener': 2.3.3(solid-js@1.9.9) @@ -7560,16 +7633,34 @@ snapshots: '@solid-primitives/utils': 6.3.2(solid-js@1.9.9) solid-js: 1.9.9 + '@solid-primitives/resize-observer@2.0.26(solid-js@2.0.0-beta.2)': + dependencies: + '@solid-primitives/event-listener': 2.3.3(solid-js@2.0.0-beta.2) + '@solid-primitives/rootless': 1.4.5(solid-js@2.0.0-beta.2) + '@solid-primitives/static-store': 0.0.8(solid-js@2.0.0-beta.2) + '@solid-primitives/utils': 6.3.2(solid-js@2.0.0-beta.2) + solid-js: 2.0.0-beta.2 + '@solid-primitives/rootless@1.4.5(solid-js@1.9.9)': dependencies: '@solid-primitives/utils': 6.3.2(solid-js@1.9.9) solid-js: 1.9.9 + '@solid-primitives/rootless@1.4.5(solid-js@2.0.0-beta.2)': + dependencies: + '@solid-primitives/utils': 6.3.2(solid-js@2.0.0-beta.2) + solid-js: 2.0.0-beta.2 + '@solid-primitives/static-store@0.0.8(solid-js@1.9.9)': dependencies: '@solid-primitives/utils': 6.3.2(solid-js@1.9.9) solid-js: 1.9.9 + '@solid-primitives/static-store@0.0.8(solid-js@2.0.0-beta.2)': + dependencies: + '@solid-primitives/utils': 6.3.2(solid-js@2.0.0-beta.2) + solid-js: 2.0.0-beta.2 + '@solid-primitives/transition-group@1.1.2(solid-js@1.9.9)': dependencies: solid-js: 1.9.9 @@ -7579,10 +7670,19 @@ snapshots: '@solid-primitives/utils': 6.3.2(solid-js@1.9.9) solid-js: 1.9.9 + '@solid-primitives/trigger@1.0.11(solid-js@2.0.0-beta.2)': + dependencies: + '@solid-primitives/utils': 6.3.2(solid-js@2.0.0-beta.2) + solid-js: 2.0.0-beta.2 + '@solid-primitives/utils@6.3.2(solid-js@1.9.9)': dependencies: solid-js: 1.9.9 + '@solid-primitives/utils@6.3.2(solid-js@2.0.0-beta.2)': + dependencies: + solid-js: 2.0.0-beta.2 + '@solidjs/meta@0.29.4(solid-js@1.9.9)': dependencies: solid-js: 1.9.9 @@ -9640,6 +9740,10 @@ snapshots: dependencies: yallist: 3.1.1 + lucide-solid@0.577.0(solid-js@2.0.0-beta.2): + dependencies: + solid-js: 2.0.0-beta.2 + luxon@3.6.1: {} lz-string@1.5.0: {} @@ -10820,11 +10924,21 @@ snapshots: '@corvu/utils': 0.4.2(solid-js@1.9.9) solid-js: 1.9.9 + solid-presence@0.1.8(solid-js@2.0.0-beta.2): + dependencies: + '@corvu/utils': 0.4.2(solid-js@2.0.0-beta.2) + solid-js: 2.0.0-beta.2 + solid-prevent-scroll@0.1.7(solid-js@1.9.9): dependencies: '@corvu/utils': 0.2.0(solid-js@1.9.9) solid-js: 1.9.9 + solid-prevent-scroll@0.1.7(solid-js@2.0.0-beta.2): + dependencies: + '@corvu/utils': 0.2.0(solid-js@2.0.0-beta.2) + solid-js: 2.0.0-beta.2 + solid-refresh@0.6.3(solid-js@1.9.9): dependencies: '@babel/generator': 7.28.5