diff --git a/examples/full/.testRun.ts b/examples/full/.testRun.ts index 59deff3..72f5ec7 100644 --- a/examples/full/.testRun.ts +++ b/examples/full/.testRun.ts @@ -2,6 +2,7 @@ export { testRun }; import { test, expect, run, fetchHtml, page, getServerUrl, autoRetry, partRegex } from "@brillout/test-e2e"; import assert from "node:assert"; +const dataHk = partRegex`data-hk="${/[0-9-]+/}"`; let isProd: boolean; @@ -15,6 +16,7 @@ function testRun(cmd: `pnpm run ${"dev" | "preview"}`) { title: "My Vike + Solid App", text: "Rendered to HTML.", counter: true, + image: true, }); testUrl({ @@ -22,6 +24,7 @@ function testRun(cmd: `pnpm run ${"dev" | "preview"}`) { title: "6 Star Wars Movies", description: "All the 6 movies from the Star Wars franchise", text: "A New Hope", + image: true, }); testUrl({ @@ -41,6 +44,8 @@ function testRun(cmd: `pnpm run ${"dev" | "preview"}`) { }); testNavigationBetweenWithSSRAndWithoutSSR(); + + testUseConfig(); } function testNavigationBetweenWithSSRAndWithoutSSR() { @@ -91,6 +96,7 @@ function testUrl({ text, counter, noSSR, + image, }: { url: string; title: string; @@ -98,6 +104,7 @@ function testUrl({ text: string; counter?: true; noSSR?: true; + image?: true; }) { test(url + " (HTML)", async () => { const html = await fetchHtml(url); @@ -105,13 +112,17 @@ function testUrl({ expect(html).toContain(text); } - const dataHkHash = /[0-9-]+/; - expect(getTitle(html)).toBe(title); - expect(html).toMatch(partRegex``); + expect(html).toMatch(partRegex``); if (!description) description = "Demo showcasing Vike + Solid"; expect(html).toMatch(partRegex``); + + if (image) { + expect(html).toMatch(partRegex``); + } else { + expect(html).not.toContain("og:image"); + } }); test(url + " (Hydration)", async () => { await page.goto(getServerUrl() + url); @@ -123,6 +134,32 @@ function testUrl({ }); } +function testUseConfig() { + test("useConfig() HTML", async () => { + const html = await fetchHtml("/images"); + expect(html).toMatch( + partRegex``, + ); + expect(html).toMatch( + partRegex``, + ); + }); + test("useConfig() hydration", async () => { + await page.goto(getServerUrl() + "/"); + await testCounter(); + ensureWasClientSideRouted("/pages/index"); + await page.click('a:has-text("useConfig()")'); + await testCounter(); + ensureWasClientSideRouted("/pages/index"); + await page.goto(getServerUrl() + "/images"); + await testCounter(); + }); +} + function getTitle(html: string) { const title = html.match(/(.*?)<\/title>/i)?.[1]; return title; diff --git a/examples/full/pages/+Layout.tsx b/examples/full/pages/+Layout.tsx index a5f676c..150717b 100644 --- a/examples/full/pages/+Layout.tsx +++ b/examples/full/pages/+Layout.tsx @@ -19,6 +19,7 @@ export function Layout(props: { children?: JSX.Element }) { <Link href="/without-ssr">Without SSR</Link> <Link href="/starship">Nested Layout 1</Link> <Link href="/stardust">Nested Layout 2</Link> + <Link href="/images">useConfig()</Link> </Sidebar> <Content>{props.children}</Content> </div> diff --git a/examples/full/pages/images/+Page.tsx b/examples/full/pages/images/+Page.tsx new file mode 100644 index 0000000..d7cd5cc --- /dev/null +++ b/examples/full/pages/images/+Page.tsx @@ -0,0 +1,47 @@ +export { Page }; + +import { Head } from "vike-solid/Head"; +import logoOld from "../../assets/logo.svg"; +import logoNew from "../../assets/logo-new.svg"; +import { Counter } from "../../components/Counter"; + +function Page() { + return ( + <> + <p> + Page showcasing an <code><Image></code> component that adds/teleports structured data ( + <code><script type="application/ld+json"></code>) to <code><head></code>, see HTML. + </p> + <div> + New logo: <Image src={logoNew} author="brillout" /> + </div> + <br /> + <div> + Old logo: <Image src={logoOld} author="Romuald Brillout" /> + </div> + <br /> + <Counter /> + </> + ); +} + +function Image({ src, author }: { src: string; author: string }) { + return ( + <> + <img src={src} height={48} style={{ "vertical-align": "middle", "margin-left": "10px" }} /> + <Head> + <script + type="application/ld+json" + innerHTML={JSON.stringify({ + "@context": "https://schema.org/", + contentUrl: { src }, + creator: { + "@type": "Person", + name: author, + }, + })} + ></script> + </Head> + </> + ); +} diff --git a/examples/full/pages/index/+Page.tsx b/examples/full/pages/index/+Page.tsx index da91dff..7232688 100644 --- a/examples/full/pages/index/+Page.tsx +++ b/examples/full/pages/index/+Page.tsx @@ -1,4 +1,8 @@ +export { Page }; + import { clientOnly } from "vike-solid/clientOnly"; +import { Config } from "vike-solid/Config"; +import image from "../../assets/logo-new.svg"; const ClientOnlyCounter = clientOnly(() => import("./Counter")); const ClientOnlyCounterSlow = clientOnly(async () => { @@ -8,9 +12,10 @@ const ClientOnlyCounterSlow = clientOnly(async () => { return import("./Counter"); }); -export default function Page() { +function Page() { return ( <> + <Config image={image}></Config> <h1>My Vike + Solid App</h1> This page is: <ul> diff --git a/examples/full/pages/star-wars/index/+data.ts b/examples/full/pages/star-wars/index/+data.ts index c3bbeb2..4ab76e0 100644 --- a/examples/full/pages/star-wars/index/+data.ts +++ b/examples/full/pages/star-wars/index/+data.ts @@ -3,14 +3,27 @@ export { data }; export type Data = Awaited<ReturnType<typeof data>>; import fetch from "node-fetch"; +import { useConfig } from "vike-solid/useConfig"; import type { Movie, MovieDetails } from "../types.js"; +import image from "../../../assets/logo-new.svg"; const data = async () => { + const config = useConfig(); + const response = await fetch("https://brillout.github.io/star-wars/api/films.json"); const moviesData = (await response.json()) as MovieDetails[]; + + const n = moviesData.length; + config({ + title: `${n} Star Wars Movies`, // <title> + description: `All the ${n} movies from the Star Wars franchise`, // <meta name="description"> + image, // <meta property="og:image"> + }); + // We remove data we don't need because the data is passed to the client; we should // minimize what is sent over the network. const movies = minimize(moviesData); + return movies; }; diff --git a/examples/full/pages/star-wars/index/+description.ts b/examples/full/pages/star-wars/index/+description.ts deleted file mode 100644 index e2be061..0000000 --- a/examples/full/pages/star-wars/index/+description.ts +++ /dev/null @@ -1,9 +0,0 @@ -export { description }; - -import type { Data } from "./+data.js"; -import type { PageContext } from "vike/types"; - -function description(pageContext: PageContext<Data>) { - const movies = pageContext.data; - return `All the ${movies.length} movies from the Star Wars franchise`; -} diff --git a/examples/full/pages/star-wars/index/+title.ts b/examples/full/pages/star-wars/index/+title.ts deleted file mode 100644 index f410050..0000000 --- a/examples/full/pages/star-wars/index/+title.ts +++ /dev/null @@ -1,9 +0,0 @@ -export { title }; - -import type { Data } from "./+data.js"; -import type { PageContext } from "vike/types"; - -function title(pageContext: PageContext<Data>) { - const movies = pageContext.data; - return `${movies.length} Star Wars Movies`; -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e05c241..aa2a18c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -69,7 +69,7 @@ importers: dependencies: vite-plugin-solid: specifier: ^2.10.2 - version: 2.10.2(solid-js@1.8.18)(vite@5.4.0(@types/node@18.17.4)) + version: 2.10.2(solid-js@1.8.18)(vite@5.4.0(@types/node@20.14.11)) devDependencies: '@babel/core': specifier: ^7.24.9 @@ -90,8 +90,8 @@ importers: specifier: ^15.2.3 version: 15.2.3(rollup@4.19.0) '@types/node': - specifier: ^18.17.4 - version: 18.17.4 + specifier: ^20.14.11 + version: 20.14.11 babel-preset-solid: specifier: ^1.8.18 version: 1.8.18(@babel/core@7.24.9) @@ -115,10 +115,10 @@ importers: version: 5.5.3 vike: specifier: ^0.4.183 - version: 0.4.183(vite@5.4.0(@types/node@18.17.4)) + version: 0.4.183(vite@5.4.0(@types/node@20.14.11)) vite: specifier: ^5.4.0 - version: 5.4.0(@types/node@18.17.4) + version: 5.4.0(@types/node@20.14.11) packages: @@ -1390,9 +1390,6 @@ packages: '@types/estree@1.0.5': resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} - '@types/node@18.17.4': - resolution: {integrity: sha512-ATL4WLgr7/W40+Sp1WnNTSKbgVn6Pvhc/2RHAdt8fl6NsQyp4oPCi2eKcGOvA494bwf1K/W6nGgZ9TwDqvpjdw==} - '@types/node@20.14.11': resolution: {integrity: sha512-kprQpL8MMeszbz6ojB5/tU8PLN4kesnN8Gjzw349rDlNgsSzg90lAVj3llK99Dh7JON+t9AuscPPFW6mPbTnSA==} @@ -3706,12 +3703,9 @@ snapshots: '@types/estree@1.0.5': {} - '@types/node@18.17.4': {} - '@types/node@20.14.11': dependencies: undici-types: 5.26.5 - optional: true '@types/normalize-package-data@2.4.4': {} @@ -4686,8 +4680,7 @@ snapshots: uglify-js@3.18.0: optional: true - undici-types@5.26.5: - optional: true + undici-types@5.26.5: {} unicode-canonical-property-names-ecmascript@2.0.0: {} @@ -4713,23 +4706,6 @@ snapshots: spdx-correct: 3.2.0 spdx-expression-parse: 3.0.1 - vike@0.4.183(vite@5.4.0(@types/node@18.17.4)): - dependencies: - '@brillout/import': 0.2.3 - '@brillout/json-serializer': 0.5.13 - '@brillout/picocolors': 1.0.14 - '@brillout/require-shim': 0.1.2 - '@brillout/vite-plugin-server-entry': 0.4.7 - acorn: 8.12.1 - cac: 6.7.14 - es-module-lexer: 1.5.4 - esbuild: 0.19.12 - fast-glob: 3.3.2 - semver: 7.6.3 - sirv: 2.0.4 - source-map-support: 0.5.21 - vite: 5.4.0(@types/node@18.17.4) - vike@0.4.183(vite@5.4.0(@types/node@20.14.11)): dependencies: '@brillout/import': 0.2.3 @@ -4747,7 +4723,7 @@ snapshots: source-map-support: 0.5.21 vite: 5.4.0(@types/node@20.14.11) - vite-plugin-solid@2.10.2(solid-js@1.8.18)(vite@5.4.0(@types/node@18.17.4)): + vite-plugin-solid@2.10.2(solid-js@1.8.18)(vite@5.4.0(@types/node@20.14.11)): dependencies: '@babel/core': 7.24.9 '@types/babel__core': 7.20.5 @@ -4755,20 +4731,11 @@ snapshots: merge-anything: 5.1.7 solid-js: 1.8.18 solid-refresh: 0.6.3(solid-js@1.8.18) - vite: 5.4.0(@types/node@18.17.4) - vitefu: 0.2.5(vite@5.4.0(@types/node@18.17.4)) + vite: 5.4.0(@types/node@20.14.11) + vitefu: 0.2.5(vite@5.4.0(@types/node@20.14.11)) transitivePeerDependencies: - supports-color - vite@5.4.0(@types/node@18.17.4): - dependencies: - esbuild: 0.21.5 - postcss: 8.4.41 - rollup: 4.19.0 - optionalDependencies: - '@types/node': 18.17.4 - fsevents: 2.3.3 - vite@5.4.0(@types/node@20.14.11): dependencies: esbuild: 0.21.5 @@ -4778,9 +4745,9 @@ snapshots: '@types/node': 20.14.11 fsevents: 2.3.3 - vitefu@0.2.5(vite@5.4.0(@types/node@18.17.4)): + vitefu@0.2.5(vite@5.4.0(@types/node@20.14.11)): optionalDependencies: - vite: 5.4.0(@types/node@18.17.4) + vite: 5.4.0(@types/node@20.14.11) web-streams-polyfill@3.2.1: {} diff --git a/vike-solid/components/Config/Config-client.ts b/vike-solid/components/Config/Config-client.ts new file mode 100644 index 0000000..edb4074 --- /dev/null +++ b/vike-solid/components/Config/Config-client.ts @@ -0,0 +1,17 @@ +export { Config }; + +// Same as ./Config-server.ts but importing useConfig-client.js + +import { useConfig } from "../../hooks/useConfig/useConfig-client.js"; +import type { ConfigFromHook } from "../../types/Config.js"; + +/** + * Set configurations inside React components. + * + * https://vike.dev/useConfig + */ +function Config(props: ConfigFromHook): null { + const config = useConfig(); + config(props); + return null; +} diff --git a/vike-solid/components/Config/Config-server.ts b/vike-solid/components/Config/Config-server.ts new file mode 100644 index 0000000..b2f331d --- /dev/null +++ b/vike-solid/components/Config/Config-server.ts @@ -0,0 +1,17 @@ +export { Config }; + +// Same as ./Config-client.ts but importing useConfig-server.js + +import { useConfig } from "../../hooks/useConfig/useConfig-server.js"; +import type { ConfigFromHook } from "../../types/Config.js"; + +/** + * Set configurations inside React components. + * + * https://vike.dev/useConfig + */ +function Config(props: ConfigFromHook): null { + const config = useConfig(); + config(props); + return null; +} diff --git a/vike-solid/components/Head/Head-client.ts b/vike-solid/components/Head/Head-client.ts new file mode 100644 index 0000000..1e79b7d --- /dev/null +++ b/vike-solid/components/Head/Head-client.ts @@ -0,0 +1,4 @@ +// https://vike.dev/Head#only-html +export function Head(): null { + return null; +} diff --git a/vike-solid/components/Head/Head-server.ts b/vike-solid/components/Head/Head-server.ts new file mode 100644 index 0000000..6713efe --- /dev/null +++ b/vike-solid/components/Head/Head-server.ts @@ -0,0 +1,17 @@ +export { Head }; + +import { useConfig } from "../../hooks/useConfig/useConfig-server.js"; +import type { JSX } from "solid-js/jsx-runtime"; + +/** + * Add arbitrary `<head>` tags. + * + * (The children are teleported to `<head>`.) + * + * https://vike.dev/Head + */ +function Head({ children }: { children: JSX.Element }): null { + const config = useConfig(); + config({ Head: children }); + return null; +} diff --git a/vike-solid/package.json b/vike-solid/package.json index 95d08fc..07c1c07 100644 --- a/vike-solid/package.json +++ b/vike-solid/package.json @@ -24,7 +24,7 @@ "@brillout/release-me": "^0.3.9", "@rollup/plugin-babel": "^6.0.4", "@rollup/plugin-node-resolve": "^15.2.3", - "@types/node": "^18.17.4", + "@types/node": "^20.14.11", "babel-preset-solid": "^1.8.18", "bumpp": "^9.4.1", "rollup": "^4.19.0", @@ -44,6 +44,14 @@ "browser": "./dist/hooks/useConfig/useConfig-client.js", "default": "./dist/hooks/useConfig/useConfig-server.js" }, + "./Config": { + "browser": "./dist/components/Config/Config-client.js", + "default": "./dist/components/Config/Config-server.js" + }, + "./Head": { + "browser": "./dist/components/Head/Head-client.js", + "default": "./dist/components/Head/Head-server.js" + }, "./clientOnly": "./dist/helpers/clientOnly.js", "./renderer/onRenderHtml": "./dist/renderer/onRenderHtml.js", "./renderer/onRenderClient": "./dist/renderer/onRenderClient.js", @@ -71,6 +79,12 @@ "useConfig": [ "dist/hooks/useConfig/useConfig-server.d.ts" ], + "Config": [ + "./dist/components/Config/Config-server.d.ts" + ], + "Head": [ + "./dist/components/Head/Head-server.d.ts" + ], "clientOnly": [ "dist/helpers/clientOnly.d.ts" ] diff --git a/vike-solid/rollup.config.js b/vike-solid/rollup.config.js index 0fe5c68..9c6ca68 100644 --- a/vike-solid/rollup.config.js +++ b/vike-solid/rollup.config.js @@ -11,6 +11,10 @@ export default [ "hooks/useData": "./hooks/useData.tsx", "hooks/useConfig/useConfig-client": "./hooks/useConfig/useConfig-client.ts", "hooks/useConfig/useConfig-server": "./hooks/useConfig/useConfig-server.ts", + "components/Config/Config-client": "./components/Config/Config-client.ts", + "components/Config/Config-server": "./components/Config/Config-server.ts", + "components/Head/Head-client": "./components/Head/Head-client.ts", + "components/Head/Head-server": "./components/Head/Head-server.ts", "helpers/clientOnly": "./helpers/clientOnly.tsx", }, ssr: true, @@ -23,6 +27,10 @@ export default [ "hooks/useData": "./hooks/useData.tsx", "hooks/useConfig/useConfig-client": "./hooks/useConfig/useConfig-client.ts", "hooks/useConfig/useConfig-server": "./hooks/useConfig/useConfig-server.ts", + "components/Config/Config-client": "./components/Config/Config-client.ts", + "components/Config/Config-server": "./components/Config/Config-server.ts", + "components/Head/Head-client": "./components/Head/Head-client.ts", + "components/Head/Head-server": "./components/Head/Head-server.ts", }, ssr: false, external: ["vike/server", "vike/plugin", "vike/getPageContext"], @@ -34,6 +42,10 @@ export default [ "./hooks/useData.tsx", "./hooks/useConfig/useConfig-client.ts", "./hooks/useConfig/useConfig-server.ts", + "./components/Config/Config-client.ts", + "./components/Config/Config-server.ts", + "./components/Head/Head-client.ts", + "./components/Head/Head-server.ts", "./helpers/clientOnly.tsx", "./vite-plugin-vike-solid.ts", ],