Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(dev)!: rework _server.tsx #486

Merged
merged 3 commits into from
Feb 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion packages/pages/etc/pages.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ import { default as React_2 } from "react";
// @public
export type Attributes = Record<string, string>;

// @internal
export interface ClientRenderTemplate {
render(pageContext: PageContext<any>): Promise<string>;
}

// @internal
export interface ClientServerRenderTemplates {
clientRenderTemplatePath: string;
Expand Down Expand Up @@ -153,8 +158,10 @@ export type Render<T extends TemplateRenderProps<T>> = (props: T) => string;
export const renderHeadConfigToString: (headConfig: HeadConfig) => string;

// @internal
export interface RenderTemplate {
export interface ServerRenderTemplate {
indexHtml: string;
render(pageContext: PageContext<any>): Promise<string>;
replacementTag: string;
}

// @public
Expand Down
53 changes: 35 additions & 18 deletions packages/pages/src/common/src/template/hydration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,63 +100,60 @@ const makeAbsolute = (path: string): string => {
* For the most part, injects data into the <head> tag. It also provides validation.
*
* @param clientHydrationString if this is undefined then hydration is skipped
* @param serverHtml
* @param indexHtml
* @param appLanguage
* @param headConfig
* @returns the server template with injected html
*/
const getCommonInjectedServerHtml = (
const getCommonInjectedIndexHtml = (
clientHydrationString: string | undefined,
serverHtml: string,
indexHtml: string,
appLanguage: string,
headConfig?: HeadConfig
): string => {
// Add the language to the <html> tag if it exists
serverHtml = serverHtml.replace("<!--app-lang-->", appLanguage);
indexHtml = indexHtml.replace("<!--app-lang-->", appLanguage);

if (clientHydrationString) {
serverHtml = injectIntoHead(
serverHtml,
indexHtml = injectIntoEndOfHead(
indexHtml,
`<script type="module">${clientHydrationString}</script>`
);
}

if (headConfig) {
serverHtml = injectIntoHead(
serverHtml,
renderHeadConfigToString(headConfig)
);
indexHtml = injectIntoHead(indexHtml, renderHeadConfigToString(headConfig));
}

return serverHtml;
return indexHtml;
};

/**
* Use for the Vite dev server.
*
* @param clientHydrationString
* @param serverHtml
* @param indexHtml
* @param appLanguage
* @param headConfig
* @returns the server template to render in the Vite dev environment
*/
export const getServerTemplateDev = (
export const getIndexTemplateDev = (
clientHydrationString: string | undefined,
serverHtml: string,
indexHtml: string,
appLanguage: string,
headConfig?: HeadConfig
): string => {
return getCommonInjectedServerHtml(
return getCommonInjectedIndexHtml(
clientHydrationString,
serverHtml,
indexHtml,
appLanguage,
headConfig
);
};

/**
* Used for the Deno plugin execution context. The major difference between this function
* and {@link getServerTemplateDev} is that it also injects the CSS import tags which is
* and {@link getIndexTemplateDev} is that it also injects the CSS import tags which is
* not required by Vite since those are injected automatically by the Vite dev server.
*
* @param clientHydrationString
Expand All @@ -176,7 +173,7 @@ export const getServerTemplatePlugin = (
appLanguage: string,
headConfig?: HeadConfig
) => {
let html = getCommonInjectedServerHtml(
let html = getCommonInjectedIndexHtml(
clientHydrationString,
serverHtml,
appLanguage,
Expand Down Expand Up @@ -249,3 +246,23 @@ const injectIntoHead = (html: string, stringToInject: string): string => {
html.slice(openingHeadIndex)
);
};

const closingHeadTag = "</head>";

/**
* Finds the ending </head> tag and injects the input string into it.
* @param html
*/
const injectIntoEndOfHead = (html: string, stringToInject: string): string => {
const closingHeadIndex = html.indexOf(closingHeadTag);

if (closingHeadIndex === -1) {
throw new Error("_server.tsx: No head tag is defined");
}

return (
html.slice(0, closingHeadIndex) +
stringToInject +
html.slice(closingHeadIndex)
);
};
15 changes: 8 additions & 7 deletions packages/pages/src/common/src/template/internal/_server.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,18 @@ import * as ReactDOMServer from "react-dom/server";
import * as React from "react";
import { PageContext } from "../types.js";

export { render };

const render = async (pageContext: PageContext<any>) => {
export const render = async (pageContext: PageContext<any>) => {
const { Page, pageProps } = pageContext;
const viewHtml = ReactDOMServer.renderToString(<Page {...pageProps} />);

return `<!DOCTYPE html>
return ReactDOMServer.renderToString(<Page {...pageProps} />);
};

export const replacementTag = "<!--YEXT-SERVER-->";

export const indexHtml = `<!DOCTYPE html>
<html lang="<!--app-lang-->">
<head></head>
<body>
<div id="reactele">${viewHtml}</div>
<div id="reactele">${replacementTag}</div>
</body>
</html>`;
};
20 changes: 18 additions & 2 deletions packages/pages/src/common/src/template/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,11 +247,27 @@ export interface ClientServerRenderTemplates {
}

/**
* The type of the client/server render templates.
* The type of the server render template.
*
* @internal
*/
export interface RenderTemplate {
export interface ServerRenderTemplate {
/** The render function required by the render templates */
render(pageContext: PageContext<any>): Promise<string>;

/** The index.html entrypoint for your template */
indexHtml: string;

/** The tag in indexHtml to replace with the contents of render */
replacementTag: string;
}

/**
* The type of the client render template.
*
* @internal
*/
export interface ClientRenderTemplate {
/** The render function required by the render templates */
render(pageContext: PageContext<any>): Promise<string>;
}
Expand Down
43 changes: 24 additions & 19 deletions packages/pages/src/dev/server/middleware/sendAppHTML.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ViteDevServer } from "vite";
import { TemplateModuleInternal } from "../../../common/src/template/internal/types.js";
import {
RenderTemplate,
ServerRenderTemplate,
TemplateRenderProps,
} from "../../../common/src/template/types.js";
import { getLang } from "../../../common/src/template/head.js";
Expand All @@ -11,7 +11,7 @@ import { getGlobalClientServerRenderTemplates } from "../../../common/src/templa
import { ProjectStructure } from "../../../common/src/project/structure.js";
import {
getHydrationTemplateDev,
getServerTemplateDev,
getIndexTemplateDev,
} from "../../../common/src/template/hydration.js";

/**
Expand All @@ -38,19 +38,6 @@ export default async function sendAppHTML(
projectStructure.getTemplatePaths()
);

const serverRenderTemplateModule = (await vite.ssrLoadModule(
clientServerRenderTemplates.serverRenderTemplatePath
)) as RenderTemplate;

const getServerHtml = async () => {
// using this wrapper function prevents SRR client-server mistmatches if
// the template modifies props
return await serverRenderTemplateModule.render({
Page: templateModuleInternal.default!,
pageProps: props,
});
};

const headConfig = templateModuleInternal.getHeadConfig
? templateModuleInternal.getHeadConfig(props)
: undefined;
Expand All @@ -62,18 +49,36 @@ export default async function sendAppHTML(
templateModuleInternal.config.hydrate
);

const clientInjectedServerHtml = getServerTemplateDev(
const serverRenderTemplateModule = (await vite.ssrLoadModule(
clientServerRenderTemplates.serverRenderTemplatePath
)) as ServerRenderTemplate;

const clientInjectedIndexHtml = getIndexTemplateDev(
clientHydrationString,
await getServerHtml(),
serverRenderTemplateModule.indexHtml,
getLang(headConfig, props),
headConfig
);

const html = await vite.transformIndexHtml(
const transformedIndexHtml = await vite.transformIndexHtml(
// vite decodes request urls when caching proxy requests so we have to
// load the transform request with a decoded uri
decodeURIComponent(pathname),
clientInjectedServerHtml
clientInjectedIndexHtml
);

const getServerHtml = async () => {
// using this wrapper function prevents SSR client-server mistmatches if
// the template modifies props
return await serverRenderTemplateModule.render({
Page: templateModuleInternal.default!,
pageProps: props,
});
};

const html = transformedIndexHtml.replace(
serverRenderTemplateModule.replacementTag,
await getServerHtml()
);

// Send the rendered HTML back.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { describe, it, expect, vi } from "vitest";
import React from "react";
import { TemplateModuleInternal } from "../../../../common/src/template/internal/types.js";
import {
RenderTemplate,
ServerRenderTemplate,
TemplateProps,
Manifest,
} from "../../../../common/src/template/types.js";
Expand Down Expand Up @@ -40,18 +40,18 @@ const baseProps: TemplateProps = {
},
};

const serverRenderTemplate: RenderTemplate = {
const serverRenderTemplate: ServerRenderTemplate = {
render: () => {
return Promise.resolve(
`<!DOCTYPE html>
<html lang="<!--app-lang-->">
<head></head>
<body>
<div id="reactele"></div>
</body>
</html>`
);
return Promise.resolve("");
},
indexHtml: `<!DOCTYPE html>
<html lang="<!--app-lang-->">
<head></head>
<body>
<div id="reactele"><!--REPLACE-ME></div>
</body>
</html>`,
replacementTag: "<!--REPLACE-ME>",
};

describe("generateResponses", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
TemplateRenderProps,
Manifest,
TemplateModule,
RenderTemplate,
ServerRenderTemplate,
} from "../../../../common/src/template/types.js";
import { getRelativePrefixToRootFromPath } from "../../../../common/src/template/paths.js";
import { reactWrapper } from "./wrapper.js";
Expand Down Expand Up @@ -49,7 +49,7 @@ export const readTemplateModules = async (
/** The render template information needed by the plugin execution */
export interface PluginRenderTemplates {
/** The server render module */
server: RenderTemplate;
server: ServerRenderTemplate;
/** The client render relative path */
client: string;
}
Expand Down Expand Up @@ -80,12 +80,14 @@ export const getPluginRenderTemplates = async (
// caches dynamically imported plugin render template modules. Without this, dynamically imported
// modules will leak some memory during generation. This can cause issues on a publish with a large
// number of generations.
const pluginRenderTemplatesCache = new Map<string, RenderTemplate>();
const pluginRenderTemplatesCache = new Map<string, ServerRenderTemplate>();

const importRenderTemplate = async (path: string): Promise<RenderTemplate> => {
const importRenderTemplate = async (
path: string
): Promise<ServerRenderTemplate> => {
let module = pluginRenderTemplatesCache.get(path);
if (!module) {
module = (await import(path)) as RenderTemplate;
module = (await import(path)) as ServerRenderTemplate;
pluginRenderTemplatesCache.set(path, module);
}
return module;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,6 @@ export const reactWrapper = async <T extends TemplateRenderProps>(
`${templateModuleInternal.templateName}.tsx`
);

const serverHtml = await pluginRenderTemplates.server.render({
Page: templateModuleInternal.default!,
pageProps: props,
});

let clientHydrationString;
if (hydrate) {
clientHydrationString = getHydrationTemplate(
Expand All @@ -46,9 +41,19 @@ export const reactWrapper = async <T extends TemplateRenderProps>(
);
}

const serverHtml = await pluginRenderTemplates.server.render({
Page: templateModuleInternal.default!,
pageProps: props,
});

const html = pluginRenderTemplates.server.indexHtml.replace(
pluginRenderTemplates.server.replacementTag,
serverHtml
);

const clientInjectedServerHtml = getServerTemplatePlugin(
clientHydrationString,
serverHtml,
html,
templateFilepath,
manifest.bundlerManifest,
getLang(headConfig, props),
Expand Down
Loading