Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
brillout committed Aug 12, 2024
1 parent ed1c070 commit 99b1d85
Show file tree
Hide file tree
Showing 10 changed files with 81 additions and 86 deletions.
1 change: 0 additions & 1 deletion vike-solid/+config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ export default {
onRenderHtml: "import:vike-solid/renderer/onRenderHtml:onRenderHtml",
// https://vike.dev/onRenderClient
onRenderClient: "import:vike-solid/renderer/onRenderClient:onRenderClient",
// https://vike.dev/clientRouting

// https://vike.dev/clientRouting
clientRouting: true,
Expand Down
24 changes: 7 additions & 17 deletions vike-solid/hooks/useConfig/useConfig-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,37 +12,27 @@ import { getPageContext } from "vike/getPageContext";
* https://vike.dev/useConfig
*/
function useConfig(): (config: ConfigFromHook) => void {
const configSetter = (config: ConfigFromHook) => setConfigOverPageContext(config, pageContext);

// Vike hook
let pageContext = getPageContext() as PageContext & PageContextInternal;
if (pageContext) return configSetter;
if (pageContext) return (config: ConfigFromHook) => setPageContextConfigFromHook(config, pageContext);

// React component
pageContext = usePageContext();
return (config: ConfigFromHook) => {
if (!("_headAlreadySet" in pageContext)) {
configSetter(config);
setPageContextConfigFromHook(config, pageContext);
} else {
sideEffect(config);
apply(config);
}
};
}

const configsClientSide = ["title"] as const;
function setConfigOverPageContext(config: ConfigFromHook, pageContext: PageContextInternal) {
function setPageContextConfigFromHook(config: ConfigFromHook, pageContext: PageContextInternal) {
pageContext._configFromHook ??= {};

configsClientSide.forEach((configName) => {
const configValue = config[configName];
if (!configValue) return;
pageContext._configFromHook![configName] = configValue;
});
Object.assign(pageContext._configFromHook, config);
}

function sideEffect(config: ConfigFromHook) {
function apply(config: ConfigFromHook) {
const { title } = config;
if (title) {
window.document.title = title;
}
if (title) window.document.title = title;
}
49 changes: 23 additions & 26 deletions vike-solid/hooks/useConfig/useConfig-server.ts
Original file line number Diff line number Diff line change
@@ -1,57 +1,54 @@
export { useConfig };
export type ConfigFromHookCumulative = (typeof configsCumulative)[number];

import type { PageContext } from "vike/types";
import type { PageContextInternal } from "../../types/PageContext.js";
import type { ConfigFromHook } from "../../types/Config.js";
import { usePageContext } from "../usePageContext.js";
import { getPageContext } from "vike/getPageContext";
import { objectKeys } from "../../utils/objectKeys.js";
import { includes } from "../../utils/includes.js";

/**
* Set configurations inside React components and Vike hooks.
*
* https://vike.dev/useConfig
*/
function useConfig(): (config: ConfigFromHook) => void {
const configSetter = (config: ConfigFromHook) => setConfigOverPageContext(config, pageContext);

// Vike hook
let pageContext = getPageContext() as PageContext & PageContextInternal;
if (pageContext) return configSetter;
if (pageContext) return (config: ConfigFromHook) => setPageContextConfigFromHook(config, pageContext);

// React component
pageContext = usePageContext();
return (config: ConfigFromHook) => {
if (!pageContext._headAlreadySet) {
configSetter(config);
setPageContextConfigFromHook(config, pageContext);
} else {
throw new Error("Using useConfig() with HTML streaming isn't supported yet");
}
};
}

const configsHtmlOnly = ["Head", "description", "image"] as const;
const configsClientSide = ["title"];
const configsCumulative = ["Head"] as const;
const configsOverridable = ["title", "description", "image"] as const;
function setConfigOverPageContext(config: ConfigFromHook, pageContext: PageContext & PageContextInternal) {
function setPageContextConfigFromHook(config: ConfigFromHook, pageContext: PageContext & PageContextInternal) {
pageContext._configFromHook ??= {};

if (pageContext.isClientSideNavigation) {
// Remove HTML only configs which the client-side doesn't need (also avoiding serialization errors)
for (const configName of configsHtmlOnly) delete config[configName];
}

// Cumulative values
configsCumulative.forEach((configName) => {
const configValue = config[configName];
if (!configValue) return;
pageContext._configFromHook![configName] ??= [];
pageContext._configFromHook![configName].push(configValue);
});

// Overridable values
configsOverridable.forEach((configName) => {
const configValue = config[configName];
if (!configValue) return;
pageContext._configFromHook![configName] = configValue;
objectKeys(config).forEach((configName) => {
// Skip HTML only configs which the client-side doesn't need, saving KBs sent to the client as well as avoiding serialization errors.
if (pageContext.isClientSideNavigation && !configsClientSide.includes(configName)) return;

if (!includes(configsCumulative, configName)) {
// Overridable config
const configValue = config[configName];
if (configValue === undefined) return;
pageContext._configFromHook![configName] = configValue as any;
} else {
// Cumulative config
const configValue = config[configName];
if (!configValue) return;
pageContext._configFromHook![configName] ??= [];
pageContext._configFromHook![configName].push(configValue as any);
}
});
}
2 changes: 1 addition & 1 deletion vike-solid/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"type": "module",
"scripts": {
"dev": "rollup -c rollup.config.js --watch",
"dev:check-types": "tsc --noEmit --watch",
"dev:typecheck": "tsc --noEmit --watch",
"build": "tsc --noEmit && rollup -c rollup.config.js",
"release": "LANG=en_US release-me patch",
"release:minor": "LANG=en_US release-me minor",
Expand Down
29 changes: 11 additions & 18 deletions vike-solid/renderer/getHeadSetting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,22 @@ import type { PageContext } from "vike/types";
import type { PageContextInternal } from "../types/PageContext.js";
import type { ConfigFromHookResolved } from "../types/Config.js";

type HeadSetting = "favicon" | "lang" | "title" | "description" | "image";
// We use `any` instead of doing proper validation in order to save KBs sent to the client-side

type HeadSetting = Exclude<keyof ConfigFromHookResolved, "Head">;
type HeadSettingFromHook = HeadSetting & keyof ConfigFromHookResolved;
function getHeadSetting(
headSetting: HeadSetting,
pageContext: PageContext & PageContextInternal,
): undefined | null | string {
function getHeadSetting<T>(headSetting: HeadSetting, pageContext: PageContext & PageContextInternal): undefined | T {
// Set by useConfig()
{
const val: undefined | string = pageContext._configFromHook?.[headSetting as HeadSettingFromHook];
if (val !== undefined) return val;
const val = pageContext._configFromHook?.[headSetting as HeadSettingFromHook];
if (val !== undefined) return val as any;
}

const config = pageContext.configEntries[headSetting]?.[0];
if (!config) return undefined;
const val = config.configValue;
if (typeof val === "string") return val;
if (!val) return null;
// Set by +configName.js
const val = pageContext.config[headSetting];
if (isCallable(val)) {
const valStr = val(pageContext);
if (typeof valStr! !== "string") {
throw new Error(config.configDefinedAt + " should return a string");
}
return valStr;
return val(pageContext) as any;
} else {
throw new Error(config.configDefinedAt + " should be a string or a function returning a string");
return val as any;
}
}
4 changes: 2 additions & 2 deletions vike-solid/renderer/onRenderClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ const onRenderClient: OnRenderClientAsync = async (pageContext): ReturnType<OnRe
function updateDocument(pageContext: PageContextClient & PageContextInternal) {
pageContext._headAlreadySet = true;

const title = getHeadSetting("title", pageContext);
const lang = getHeadSetting("lang", pageContext);
const title = getHeadSetting<string | null>("title", pageContext);
const lang = getHeadSetting<string | null>("lang", pageContext);

// - We skip if `undefined` as we shouldn't remove values set by the Head setting.
// - Setting a default prevents the previous value to be leaked: upon client-side navigation, the value set by the previous page won't be removed if the next page doesn't override it.
Expand Down
12 changes: 6 additions & 6 deletions vike-solid/renderer/onRenderHtml.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,10 @@ function getPageHtml(pageContext: PageContextServer) {
function getHeadHtml(pageContext: PageContextServer & PageContextInternal) {
pageContext._headAlreadySet = true;

const title = getHeadSetting("title", pageContext);
const favicon = getHeadSetting("favicon", pageContext);
const description = getHeadSetting("description", pageContext);
const image = getHeadSetting("image", pageContext);
const title = getHeadSetting<string | null>("title", pageContext);
const favicon = getHeadSetting<string | null>("favicon", pageContext);
const description = getHeadSetting<string | null>("description", pageContext);
const image = getHeadSetting<string | null>("image", pageContext);

const titleTags = !title ? "" : escapeInject`<title>${title}</title><meta property="og:title" content="${title}">`;
const faviconTag = !favicon ? "" : escapeInject`<link rel="icon" href="${favicon}" />`;
Expand All @@ -67,7 +67,7 @@ function getHeadHtml(pageContext: PageContextServer & PageContextInternal) {
const imageTags = !image
? ""
: escapeInject`<meta property="og:image" content="${image}"><meta name="twitter:card" content="summary_large_image">`;
const viewportTag = dangerouslySkipEscape(getViewportTag(pageContext.config.viewport));
const viewportTag = dangerouslySkipEscape(getViewportTag(getHeadSetting<Viewport>("viewport", pageContext)));

const headElementsHtml = dangerouslySkipEscape(
[
Expand Down Expand Up @@ -113,7 +113,7 @@ function isElement(value: unknown): value is JSX.Element {
}

function getTagAttributes(pageContext: PageContextServer) {
let lang = getHeadSetting("lang", pageContext);
let lang = getHeadSetting<string | null>("lang", pageContext);
// Don't set `lang` to its default value if it's `null` (so that users can set it to `null` in order to remove the default value)
if (lang === undefined) lang = "en";

Expand Down
30 changes: 15 additions & 15 deletions vike-solid/types/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type {
} from "vike/types";
import type { TagAttributes } from "../utils/getTagAttributesString";
import type { Viewport } from "../renderer/onRenderHtml";
import type { ConfigFromHookCumulative } from "../hooks/useConfig/useConfig-server";

declare global {
namespace Vike {
Expand Down Expand Up @@ -51,7 +52,7 @@ declare global {
*
* https://vike.dev/title
*/
title?: string | ((pageContext: PageContext_) => string);
title?: string | null | ((pageContext: PageContext_) => string | null | undefined);

/**
* Set the page's description.
Expand All @@ -66,7 +67,7 @@ declare global {
*
* https://vike.dev/description
*/
description?: string | ((pageContext: PageContextServer) => string);
description?: string | null | ((pageContext: PageContextServer) => string | null | undefined);

/**
* Set the page's preview image upon URL sharing.
Expand All @@ -81,7 +82,7 @@ declare global {
*
* https://vike.dev/image
*/
image?: string | ((pageContext: PageContextServer) => string);
image?: string | null | ((pageContext: PageContextServer) => string | null | undefined);

/**
* Set the page's width shown to the user on mobile/tablet devices.
Expand All @@ -90,7 +91,7 @@ declare global {
*
* https://vike.dev/viewport
*/
viewport?: Viewport;
viewport?: Viewport | ((pageContext: PageContextServer) => Viewport | undefined);

/**
* Set the page's favicon.
Expand All @@ -104,7 +105,7 @@ declare global {
*
* https://vike.dev/favicon
*/
favicon?: string;
favicon?: string | null | ((pageContext: PageContextServer) => string | null | undefined);

/**
* Set the page's language (`<html lang>`).
Expand All @@ -113,21 +114,21 @@ declare global {
*
* https://vike.dev/lang
*/
lang?: string | ((pageContext: PageContext_) => string) | null;
lang?: string | null | ((pageContext: PageContext_) => string | null | undefined);

/**
* Add tag attributes such as `<html class="dark">`.
*
* https://vike.dev/htmlAttributes
*/
htmlAttributes?: TagAttributes;
htmlAttributes?: TagAttributes

/**
* Add tag attributes such as `<body class="dark">`.
*
* https://vike.dev/bodyAttributes
*/
bodyAttributes?: TagAttributes;
bodyAttributes?: TagAttributes

/**
* If `true`, the page is rendered twice: on the server-side (to HTML) and on the client-side (hydration).
Expand Down Expand Up @@ -174,10 +175,9 @@ export type Head = Component | JSX.Element;
type PickWithoutGetter<T, K extends keyof T> = {
[P in K]: Exclude<T[P], Function>;
};
export type ConfigFromHook = PickWithoutGetter<Vike.Config, "Head" | "title" | "description" | "image">;
export type ConfigFromHookResolved = {
Head?: Head[];
title?: string;
description?: string;
image?: string;
};
export type ConfigFromHook = PickWithoutGetter<
Vike.Config,
"Head" | "title" | "description" | "image" | "favicon" | "lang" | "viewport"
>;
export type ConfigFromHookResolved = Omit<ConfigFromHook, ConfigFromHookCumulative> &
Pick<Vike.ConfigResolved, ConfigFromHookCumulative>;
10 changes: 10 additions & 0 deletions vike-solid/utils/includes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// https://stackoverflow.com/questions/56565528/typescript-const-assertions-how-to-use-array-prototype-includes/74213179#74213179
/** Same as Array.prototype.includes() but with type inference */
export function includes<T>(values: readonly T[], x: unknown): x is T {
return values.includes(x as any);
}
/*
export function includes<Arr extends any[] | readonly any[]>(arr: Arr, el: unknown): el is Arr[number] {
return arr.includes(el as any)
}
*/
6 changes: 6 additions & 0 deletions vike-solid/utils/objectKeys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// https://stackoverflow.com/questions/52856496/typescript-object-keys-return-string
// https://github.com/sindresorhus/ts-extras/blob/main/source/object-keys.ts
/** Same as Object.keys() but with type inference */
export function objectKeys<T extends object>(obj: T): (keyof T)[] {
return Object.keys(obj) as any;
}

0 comments on commit 99b1d85

Please sign in to comment.