From e52345c79f89fbc8afba7cea8fa00b2a6eb82ef1 Mon Sep 17 00:00:00 2001 From: Muhammad Date: Fri, 6 Sep 2024 22:04:09 +0700 Subject: [PATCH 1/4] test: add test to `examples/solid-query` --- examples/solid-query/.test-dev.test.ts | 2 + examples/solid-query/.test-preview.test.ts | 2 + examples/solid-query/.testRun.ts | 70 +++++++++++++++++++++ examples/solid-query/pages/index/Movies.tsx | 2 +- 4 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 examples/solid-query/.test-dev.test.ts create mode 100644 examples/solid-query/.test-preview.test.ts create mode 100644 examples/solid-query/.testRun.ts diff --git a/examples/solid-query/.test-dev.test.ts b/examples/solid-query/.test-dev.test.ts new file mode 100644 index 0000000..f77ae53 --- /dev/null +++ b/examples/solid-query/.test-dev.test.ts @@ -0,0 +1,2 @@ +import { testRun } from "./.testRun"; +testRun("pnpm run dev"); diff --git a/examples/solid-query/.test-preview.test.ts b/examples/solid-query/.test-preview.test.ts new file mode 100644 index 0000000..ed102ea --- /dev/null +++ b/examples/solid-query/.test-preview.test.ts @@ -0,0 +1,2 @@ +import { testRun } from "./.testRun"; +testRun("pnpm run preview"); diff --git a/examples/solid-query/.testRun.ts b/examples/solid-query/.testRun.ts new file mode 100644 index 0000000..6dd5ca1 --- /dev/null +++ b/examples/solid-query/.testRun.ts @@ -0,0 +1,70 @@ +export { testRun }; + +import { test, expect, run, page, partRegex, getServerUrl, autoRetry } from "@brillout/test-e2e"; +const dataHk = partRegex`data-hk=${/[0-9-]+/}`; + +function testRun(cmd: `pnpm run ${"dev" | "preview"}`) { + run(cmd); + + const content = "Return of the Jedi"; + const loading = "Loading movies..."; + const titleDefault = "My Vike + Solid App"; + const titleOverriden = "6 Star Wars movies"; + const titleAsScript = ``; + const description = partRegex``; + test("HTML (as user)", async () => { + const html = await fetchAsUser("/"); + expect(html).toContain(content); + expect(html).toContain(loading); + expect(html).toContain(titleAsScript); + expect(getTitle(html)).toBe(titleDefault); + expect(html.split("").length).toBe(2); + expect(html).not.toMatch(description); + }); + test("HTML (as bot)", async () => { + const html = await fetchAsBot("/"); + expect(html).toContain(content); + expect(html).not.toContain(loading); + expect(html).not.toContain(titleAsScript); + expect(getTitle(html)).toBe(titleOverriden); + expect(html.split("<title>").length).toBe(2); + expect(html).toMatch(description); + }); + test("DOM", async () => { + await page.goto(getServerUrl() + "/"); + const body = await page.textContent("body"); + // Playwright seems to await the HTML stream + expect(body).not.toContain(loading); + expect(body).toContain(content); + await testCounter(); + }); +} + +function getTitle(html: string) { + const title = html.match(/<title>(.*?)<\/title>/i)?.[1]; + return title; +} + +async function testCounter() { + // autoRetry() for awaiting client-side code loading & executing + await autoRetry( + async () => { + expect(await page.textContent("button")).toBe("Counter 0"); + await page.click("button"); + expect(await page.textContent("button")).toContain("Counter 1"); + }, + { timeout: 5 * 1000 }, + ); +} + +async function fetchAsBot(pathname: string) { + return await fetchHtml(pathname, "curl/8.5.0"); +} +async function fetchAsUser(pathname: string) { + return await fetchHtml(pathname, "chrome"); +} +async function fetchHtml(pathname: string, userAgent: string) { + const response = await fetch(getServerUrl() + pathname, { headers: { ["User-Agent"]: userAgent } }); + const html = await response.text(); + return html; +} diff --git a/examples/solid-query/pages/index/Movies.tsx b/examples/solid-query/pages/index/Movies.tsx index 07713d1..52cf93b 100644 --- a/examples/solid-query/pages/index/Movies.tsx +++ b/examples/solid-query/pages/index/Movies.tsx @@ -17,7 +17,7 @@ export function Movies() { }; return ( - <QueryBoundary query={query} loadingFallback={<p>Loading movies ...</p>}> + <QueryBoundary query={query} loadingFallback={"Loading movies..."}> {(movies) => ( <> <Config title={`${movies.length} Star Wars movies`} /> From 72b20ec699ee6dd53dc0ba4333bc3bc614f18795 Mon Sep 17 00:00:00 2001 From: Muhammad <phonzammizaki@gmail.com> Date: Fri, 6 Sep 2024 23:51:36 +0700 Subject: [PATCH 2/4] fix(useConfig): add support for `useConfig()` with HTML streaming --- .../hooks/useConfig/useConfig-server.ts | 19 +++++++++++++++++-- .../vike-solid/integration/onRenderHtml.tsx | 14 +++++++++++--- packages/vike-solid/types/Config.ts | 1 + packages/vike-solid/types/PageContext.ts | 3 ++- 4 files changed, 31 insertions(+), 6 deletions(-) diff --git a/packages/vike-solid/hooks/useConfig/useConfig-server.ts b/packages/vike-solid/hooks/useConfig/useConfig-server.ts index 04464d5..056cfda 100644 --- a/packages/vike-solid/hooks/useConfig/useConfig-server.ts +++ b/packages/vike-solid/hooks/useConfig/useConfig-server.ts @@ -1,7 +1,7 @@ export { useConfig }; import type { PageContext } from "vike/types"; import type { PageContextInternal } from "../../types/PageContext.js"; -import type { ConfigFromHook } from "../../types/Config.js"; +import type { ConfigFromHook, Stream } from "../../types/Config.js"; import { usePageContext } from "../usePageContext.js"; import { getPageContext } from "vike/getPageContext"; import { objectKeys } from "../../utils/objectKeys.js"; @@ -20,7 +20,14 @@ function useConfig(): (config: ConfigFromHook) => void { // Component pageContext = usePageContext(); - return (config: ConfigFromHook) => setPageContextConfigFromHook(config, pageContext); + return (config: ConfigFromHook) => { + if (!pageContext._headAlreadySet) { + setPageContextConfigFromHook(config, pageContext); + } else { + // <head> already sent to the browser => send DOM-manipulating scripts during HTML streaming + apply(config, pageContext._stream!); + } + }; } const configsClientSide = ["title"]; @@ -44,3 +51,11 @@ function setPageContextConfigFromHook(config: ConfigFromHook, pageContext: PageC } }); } + +function apply(config: ConfigFromHook, stream: Stream) { + const { title } = config; + if (title) { + const htmlSnippet = `<script>document.title = ${JSON.stringify(title)}</script>`; + stream.write(htmlSnippet); + } +} diff --git a/packages/vike-solid/integration/onRenderHtml.tsx b/packages/vike-solid/integration/onRenderHtml.tsx index aef1b9b..79c907a 100644 --- a/packages/vike-solid/integration/onRenderHtml.tsx +++ b/packages/vike-solid/integration/onRenderHtml.tsx @@ -40,16 +40,24 @@ const onRenderHtml: OnRenderHtmlAsync = async ( </html>`; }; -function getPageHtml(pageContext: PageContextServer) { +function getPageHtml(pageContext: PageContextServer & PageContextInternal) { let pageHtml: string | ReturnType<typeof dangerouslySkipEscape> | TPipe = ""; if (pageContext.Page) { if (!pageContext.config.stream) { pageHtml = dangerouslySkipEscape(renderToString(() => getPageElement(pageContext))); } else if (pageContext.config.stream === "web") { - pageHtml = renderToStream(() => getPageElement(pageContext)).pipeTo; + pageHtml = renderToStream(() => getPageElement(pageContext), { + onCompleteShell(info) { + pageContext._stream ??= info; + }, + }).pipeTo; stampPipe(pageHtml, "web-stream"); } else { - pageHtml = renderToStream(() => getPageElement(pageContext)).pipe; + pageHtml = renderToStream(() => getPageElement(pageContext), { + onCompleteShell(info) { + pageContext._stream ??= info; + }, + }).pipe; stampPipe(pageHtml, "node-stream"); } } diff --git a/packages/vike-solid/types/Config.ts b/packages/vike-solid/types/Config.ts index ca4b84b..14250b1 100644 --- a/packages/vike-solid/types/Config.ts +++ b/packages/vike-solid/types/Config.ts @@ -186,3 +186,4 @@ export type ConfigFromHook = PickWithoutGetter< >; export type ConfigFromHookResolved = Omit<ConfigFromHook, ConfigsCumulative> & Pick<Vike.ConfigResolved, ConfigsCumulative>; +export type Stream = { write: (v: string) => void }; diff --git a/packages/vike-solid/types/PageContext.ts b/packages/vike-solid/types/PageContext.ts index 14cb4d3..7ba5ab9 100644 --- a/packages/vike-solid/types/PageContext.ts +++ b/packages/vike-solid/types/PageContext.ts @@ -1,5 +1,5 @@ import type { JSX } from "solid-js"; -import type { ConfigFromHookResolved } from "./Config"; +import type { ConfigFromHookResolved, Stream } from "./Config"; // https://vike.dev/pageContext#typescript declare global { @@ -15,4 +15,5 @@ declare global { export type PageContextInternal = { _configFromHook?: ConfigFromHookResolved; _headAlreadySet?: boolean; + _stream?: Stream; }; From 711742cae563fc888b064b8377a4c6a84d0b042d Mon Sep 17 00:00:00 2001 From: Muhammad <phonzammizaki@gmail.com> Date: Sat, 7 Sep 2024 00:04:47 +0700 Subject: [PATCH 3/4] feat: add support for checking crawlers/bots and integrate `renderToStringAsync()` - Implemented detection for crawlers and bots - Updated rendering process to use `renderToStringAsync()` when accessed by crawlers/bots for improved SEO --- .../vike-solid/integration/onRenderHtml.tsx | 17 +++++++++++++---- packages/vike-solid/package.json | 1 + packages/vike-solid/types/isBot.d.ts | 4 ++++ pnpm-lock.yaml | 9 +++++++++ 4 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 packages/vike-solid/types/isBot.d.ts diff --git a/packages/vike-solid/integration/onRenderHtml.tsx b/packages/vike-solid/integration/onRenderHtml.tsx index 79c907a..10fe525 100644 --- a/packages/vike-solid/integration/onRenderHtml.tsx +++ b/packages/vike-solid/integration/onRenderHtml.tsx @@ -1,5 +1,5 @@ // https://vike.dev/onRenderHtml -import { generateHydrationScript, renderToStream, renderToString } from "solid-js/web"; +import { generateHydrationScript, renderToStream, renderToString, renderToStringAsync } from "solid-js/web"; import { dangerouslySkipEscape, escapeInject, stampPipe } from "vike/server"; import { getHeadSetting } from "./getHeadSetting.js"; import { getPageElement } from "./getPageElement.js"; @@ -10,6 +10,7 @@ import type { PageContextInternal } from "../types/PageContext.js"; import type { Head } from "../types/Config.js"; import type { JSX } from "solid-js/jsx-runtime"; import { isCallable } from "../utils/isCallable.js"; +import isBot from "isbot-fast"; export { onRenderHtml }; @@ -18,7 +19,7 @@ type TPipe = Parameters<typeof stampPipe>[0]; const onRenderHtml: OnRenderHtmlAsync = async ( pageContext: PageContextServer & PageContextInternal, ): ReturnType<OnRenderHtmlAsync> => { - const pageHtml = getPageHtml(pageContext); + const pageHtml = await getPageHtml(pageContext); const headHtml = getHeadHtml(pageContext); @@ -40,10 +41,18 @@ const onRenderHtml: OnRenderHtmlAsync = async ( </html>`; }; -function getPageHtml(pageContext: PageContextServer & PageContextInternal) { +async function getPageHtml(pageContext: PageContextServer & PageContextInternal) { let pageHtml: string | ReturnType<typeof dangerouslySkipEscape> | TPipe = ""; + const userAgent: string | undefined = + pageContext.headers?.["user-agent"] || + // TODO/eventually: remove old way of acccessing the User Agent header. + // @ts-ignore + pageContext.userAgent; + if (pageContext.Page) { - if (!pageContext.config.stream) { + if (userAgent && isBot(userAgent)) { + pageHtml = dangerouslySkipEscape(await renderToStringAsync(() => getPageElement(pageContext))); + } else if (!pageContext.config.stream) { pageHtml = dangerouslySkipEscape(renderToString(() => getPageElement(pageContext))); } else if (pageContext.config.stream === "web") { pageHtml = renderToStream(() => getPageElement(pageContext), { diff --git a/packages/vike-solid/package.json b/packages/vike-solid/package.json index f57e9c1..9e00a92 100644 --- a/packages/vike-solid/package.json +++ b/packages/vike-solid/package.json @@ -11,6 +11,7 @@ "release:commit": "LANG=en_US release-me commit" }, "dependencies": { + "isbot-fast": "^1.2.0", "vite-plugin-solid": "^2.10.2" }, "peerDependencies": { diff --git a/packages/vike-solid/types/isBot.d.ts b/packages/vike-solid/types/isBot.d.ts new file mode 100644 index 0000000..6cae9c1 --- /dev/null +++ b/packages/vike-solid/types/isBot.d.ts @@ -0,0 +1,4 @@ +declare module "isbot-fast" { + function isBot(userAgent: string): boolean; + export = isBot; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7676aac..3520e8c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -95,6 +95,9 @@ importers: packages/vike-solid: dependencies: + isbot-fast: + specifier: ^1.2.0 + version: 1.2.0 vite-plugin-solid: specifier: ^2.10.2 version: 2.10.2(solid-js@1.8.22)(vite@5.4.2(@types/node@22.4.0)(terser@5.31.6)) @@ -1906,6 +1909,10 @@ packages: resolution: {integrity: sha512-uKua1wfy3Yt+YqsD6mTUEa2zSi3G1oPlqTflgaPJ7z63vUGN5pxFpnQfeSLMFnJDEsdvOtkp1rUWkYjB4YfhgA==} engines: {node: '>=12.13'} + isbot-fast@1.2.0: + resolution: {integrity: sha512-twjuQzy2gKMDVfKGQyQqrx6Uy4opu/fiVUTTpdqtFsd7OQijIp5oXvb27n5EemYXaijh5fomndJt/SPRLsEdSg==} + engines: {node: '>=6.0.0'} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -4490,6 +4497,8 @@ snapshots: is-what@4.1.15: {} + isbot-fast@1.2.0: {} + isexe@2.0.0: {} jackspeak@3.4.3: From dc83aeb1619b64baa100606235fe86a13bed1796 Mon Sep 17 00:00:00 2001 From: Muhammad <phonzammizaki@gmail.com> Date: Sat, 7 Sep 2024 16:02:12 +0700 Subject: [PATCH 4/4] chore: update to vike@0.4.195 --- examples/full/package.json | 2 +- examples/minimal/package.json | 2 +- examples/solid-query/package.json | 2 +- examples/solid-query/pages/+config.ts | 2 +- packages/vike-solid-query/package.json | 6 +++--- packages/vike-solid/+config.ts | 2 +- packages/vike-solid/package.json | 4 ++-- pnpm-lock.yaml | 26 +++++++++++++------------- 8 files changed, 23 insertions(+), 23 deletions(-) diff --git a/examples/full/package.json b/examples/full/package.json index 3155e87..b49c2ed 100644 --- a/examples/full/package.json +++ b/examples/full/package.json @@ -9,7 +9,7 @@ "dependencies": { "node-fetch": "^3.3.2", "solid-js": "^1.8.21", - "vike": "^0.4.191", + "vike": "^0.4.195", "vike-solid": "workspace:*" }, "devDependencies": { diff --git a/examples/minimal/package.json b/examples/minimal/package.json index ccd2f38..3210469 100644 --- a/examples/minimal/package.json +++ b/examples/minimal/package.json @@ -6,7 +6,7 @@ }, "dependencies": { "solid-js": "^1.8.21", - "vike": "^0.4.191", + "vike": "^0.4.195", "vike-solid": "workspace:*" }, "devDependencies": { diff --git a/examples/solid-query/package.json b/examples/solid-query/package.json index bc66cf1..171bc92 100644 --- a/examples/solid-query/package.json +++ b/examples/solid-query/package.json @@ -9,7 +9,7 @@ "@tanstack/solid-query": "5.52.2", "node-fetch": "^3.3.2", "solid-js": "^1.8.22", - "vike": "^0.4.191", + "vike": "^0.4.195", "vike-solid": "workspace:^", "vike-solid-query": "workspace:^" }, diff --git a/examples/solid-query/pages/+config.ts b/examples/solid-query/pages/+config.ts index 976f059..181eaab 100644 --- a/examples/solid-query/pages/+config.ts +++ b/examples/solid-query/pages/+config.ts @@ -8,6 +8,6 @@ export default { passToClient: ["routeParams"], stream: true, - injectScriptsAt: "STREAM", + injectScriptsAt: "HTML_STREAM", extends: [vikeSolid, vikeSolidQuery], } satisfies Config; diff --git a/packages/vike-solid-query/package.json b/packages/vike-solid-query/package.json index b532df0..e10b288 100644 --- a/packages/vike-solid-query/package.json +++ b/packages/vike-solid-query/package.json @@ -13,7 +13,7 @@ "peerDependencies": { "@tanstack/solid-query": ">=5.0.0", "solid-js": "^1.8.7", - "vike-solid": ">=0.7.3" + "vike-solid": ">=0.7.4" }, "devDependencies": { "@brillout/release-me": "^0.4.0", @@ -26,8 +26,8 @@ "solid-js": "^1.8.22", "tsup": "^8.2.4", "typescript": "^5.5.4", - "vike": "^0.4.193", - "vike-solid": "^0.7.3", + "vike": "^0.4.195", + "vike-solid": "^0.7.4", "vite": "5.4.2" }, "exports": { diff --git a/packages/vike-solid/+config.ts b/packages/vike-solid/+config.ts index 32adb98..dcf152b 100644 --- a/packages/vike-solid/+config.ts +++ b/packages/vike-solid/+config.ts @@ -6,7 +6,7 @@ import { ssrEffect } from "./integration/ssrEffect.js"; const config = { name: "vike-solid", require: { - vike: ">=0.4.191", + vike: ">=0.4.195", }, // https://vike.dev/onRenderHtml diff --git a/packages/vike-solid/package.json b/packages/vike-solid/package.json index 9e00a92..1963f36 100644 --- a/packages/vike-solid/package.json +++ b/packages/vike-solid/package.json @@ -16,7 +16,7 @@ }, "peerDependencies": { "solid-js": "^1.8.7", - "vike": ">=0.4.191", + "vike": ">=0.4.195", "vite": ">=5.0.0" }, "devDependencies": { @@ -35,7 +35,7 @@ "solid-js": "^1.8.22", "tslib": "^2.7.0", "typescript": "^5.5.4", - "vike": "^0.4.193", + "vike": "^0.4.195", "vite": "^5.4.2" }, "exports": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3520e8c..6918dd9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -36,8 +36,8 @@ importers: specifier: ^1.8.21 version: 1.8.22 vike: - specifier: ^0.4.191 - version: 0.4.193(vite@5.4.2(@types/node@22.4.0)(terser@5.31.6)) + specifier: ^0.4.195 + version: 0.4.195(vite@5.4.2(@types/node@22.4.0)(terser@5.31.6)) vike-solid: specifier: link:../../packages/vike-solid version: link:../../packages/vike-solid @@ -55,8 +55,8 @@ importers: specifier: ^1.8.21 version: 1.8.22 vike: - specifier: ^0.4.191 - version: 0.4.193(vite@5.4.2(@types/node@22.4.0)(terser@5.31.6)) + specifier: ^0.4.195 + version: 0.4.195(vite@5.4.2(@types/node@22.4.0)(terser@5.31.6)) vike-solid: specifier: link:../../packages/vike-solid version: link:../../packages/vike-solid @@ -77,8 +77,8 @@ importers: specifier: ^1.8.22 version: 1.8.22 vike: - specifier: ^0.4.191 - version: 0.4.193(vite@5.4.2(@types/node@22.4.0)(terser@5.31.6)) + specifier: ^0.4.195 + version: 0.4.195(vite@5.4.2(@types/node@22.4.0)(terser@5.31.6)) vike-solid: specifier: link:../../packages/vike-solid version: link:../../packages/vike-solid @@ -148,8 +148,8 @@ importers: specifier: ^5.5.4 version: 5.5.4 vike: - specifier: ^0.4.193 - version: 0.4.193(vite@5.4.2(@types/node@22.4.0)(terser@5.31.6)) + specifier: ^0.4.195 + version: 0.4.195(vite@5.4.2(@types/node@22.4.0)(terser@5.31.6)) vite: specifier: ^5.4.2 version: 5.4.2(@types/node@22.4.0)(terser@5.31.6) @@ -187,8 +187,8 @@ importers: specifier: ^5.5.4 version: 5.5.4 vike: - specifier: ^0.4.193 - version: 0.4.193(vite@5.4.2(@types/node@22.4.0)(terser@5.31.6)) + specifier: ^0.4.195 + version: 0.4.195(vite@5.4.2(@types/node@22.4.0)(terser@5.31.6)) vike-solid: specifier: link:../vike-solid version: link:../vike-solid @@ -2601,8 +2601,8 @@ packages: validate-npm-package-license@3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} - vike@0.4.193: - resolution: {integrity: sha512-nrqSXfocmm10asmoYrczO9iO4aZZ9SNygbpDTn0emmOPUFxROW3MUd8XcuBWJh6QVUTN4KiZYZDZGBMgPfhdbg==} + vike@0.4.195: + resolution: {integrity: sha512-I8ltb12bSz6aU2Rf+lEzvVVvcKedTav+cnGYTsposfzg5GIVc9YPeUBV8zgg93jUosxgIG0UExMh7EGuR3gQeg==} engines: {node: '>=18.0.0'} hasBin: true peerDependencies: @@ -5117,7 +5117,7 @@ snapshots: spdx-correct: 3.2.0 spdx-expression-parse: 3.0.1 - vike@0.4.193(vite@5.4.2(@types/node@22.4.0)(terser@5.31.6)): + vike@0.4.195(vite@5.4.2(@types/node@22.4.0)(terser@5.31.6)): dependencies: '@brillout/import': 0.2.3 '@brillout/json-serializer': 0.5.13