Skip to content

Commit

Permalink
feat: export html parts renderers
Browse files Browse the repository at this point in the history
  • Loading branch information
Vladimir Mozgovoy committed Mar 4, 2024
1 parent 8a9d66e commit 67de491
Show file tree
Hide file tree
Showing 8 changed files with 245 additions and 150 deletions.
49 changes: 49 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -438,3 +438,52 @@ app.get((req, res) => {
}));
})
```

## Alternative usage

With parts renderers `generateRenderContent`, `renderHeadContent`, `renderBodyContent` via html streaming:

```js
import express from 'express';
import {createRenderFunction} from '@gravity-ui/app-layout';

const app = express();

const renderLayout = createRenderFunction();

app.get('/', async function (req, res) {
res.writeHead(200, {
'Content-Type': 'text/html',
'Transfer-Encoding': 'chunked',
});

const content = generateRenderContent(plugins, {
title: 'Home page',
});

const {htmlAttributes, helpers, bodyContent} = content;

res.write(`
<!DOCTYPE html>
<html ${helpers.attrs({...htmlAttributes})}>
<head>
${renderHeadContent(content)}
</head>
<body ${helpers.attrs({...bodyContent.attributes})}>
${renderBodyContent(content)}
`);

const data = await getUserData();

res.write(`
${content.renderHelpers.renderInlineScript(`
window.__DATA__ = ${htmlescape(data)};
`)}
</body>
</html>
`);
res.end();
});

app.listen(3000);
```
5 changes: 4 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
export {createRenderFunction, generateRenderContent} from './render.js';
export {generateRenderContent} from './utils/generateRenderContent';
export {renderHeadContent} from './utils/renderHeadContent';
export {renderBodyContent} from './utils/renderBodyContent';
export {createRenderFunction} from './render.js';
export {
createGoogleAnalyticsPlugin,
createYandexMetrikaPlugin,
Expand Down
153 changes: 10 additions & 143 deletions src/render.ts
Original file line number Diff line number Diff line change
@@ -1,155 +1,22 @@
import htmlescape from 'htmlescape';

import type {Icon, Meta, Plugin, RenderContent, RenderParams} from './types.js';
import {attrs, getRenderHelpers, hasProperty} from './utils.js';

function getRootClassName(theme?: string) {
if (!theme) {
return [];
}
const classes = ['g-root'];
if (theme) {
classes.push(`g-root_theme_${theme}`);
}
return classes;
}

const defaultIcon: Icon = {
type: 'image/png',
sizes: '16x16',
href: '/favicon.png',
};

const defaultMeta: Meta[] = [
{
name: 'viewport',
content: 'width=device-width, initial-scale=1.0',
},
];

/**
* Generates the content to be rendered in an HTML page.
* @param plugins - An array of plugins.
* @param params - An object containing various parameters for rendering the content.
* @returns An object containing the generated content for an HTML page.
*/
export function generateRenderContent<Plugins extends Plugin[], Data>(
plugins: Plugins | undefined,
params: RenderParams<Data, Plugins>,
): RenderContent {
const helpers = getRenderHelpers(params);
const htmlAttributes: Record<string, string> = {};
const meta = params.meta ?? [];
// in terms of sets: meta = params.meta ∪ (defaultMeta ∖ params.meta)
defaultMeta.forEach((defaultMetaItem) => {
if (!meta.find(({name}) => name === defaultMetaItem.name)) {
meta.push(defaultMetaItem);
}
});
const styleSheets = params.styleSheets || [];
const scripts = params.scripts || [];
const inlineStyleSheets = params.inlineStyleSheets || [];
const inlineScripts = params.inlineScripts || [];
const links = params.links || [];

inlineScripts.unshift(`window.__DATA__ = ${htmlescape(params.data || {})};`);

const content = params.bodyContent ?? {};
const {theme, className} = content;
const bodyClasses = Array.from(
new Set([...getRootClassName(theme), ...(className ? className.split(' ') : [])]),
);
const bodyContent = {
className: bodyClasses,
root: content.root,
beforeRoot: content.beforeRoot ? [content.beforeRoot] : [],
afterRoot: content.afterRoot ? [content.afterRoot] : [],
};

const {lang, isMobile, title, pluginsOptions = {}} = params;
for (const plugin of plugins ?? []) {
plugin.apply({
options: hasProperty(pluginsOptions, plugin.name)
? pluginsOptions[plugin.name]
: undefined,
renderContent: {
htmlAttributes,
meta,
links,
styleSheets,
scripts,
inlineStyleSheets,
inlineScripts,
bodyContent,
},
commonOptions: {title, lang, isMobile},
utils: helpers,
});
}

if (lang) {
htmlAttributes.lang = lang;
}

return {
htmlAttributes,
meta,
links,
styleSheets,
scripts,
inlineStyleSheets,
inlineScripts,
bodyContent,
};
}
import type {Plugin, RenderParams} from './types.js';
import {generateRenderContent} from './utils/generateRenderContent';
import {renderBodyContent} from './utils/renderBodyContent';
import {renderHeadContent} from './utils/renderHeadContent';

export function createRenderFunction<Plugins extends Plugin[]>(plugins?: Plugins) {
return function render<Data>(params: RenderParams<Data, Plugins>) {
const {
htmlAttributes,
meta,
styleSheets,
scripts,
inlineStyleSheets,
inlineScripts,
links,
bodyContent,
} = generateRenderContent(plugins, params);

const helpers = getRenderHelpers(params);
const content = generateRenderContent(plugins, params);

const icon: Icon = {
...defaultIcon,
...params.icon,
};
const {htmlAttributes, helpers, bodyContent} = content;

return `
<!DOCTYPE html>
<html ${attrs({...htmlAttributes})}>
<html ${helpers.attrs({...htmlAttributes})}>
<head>
<meta charset="utf-8">
<title>${params.title}</title>
<link ${attrs({rel: 'icon', type: icon.type, sizes: icon.sizes, href: icon.href})}>
${[
...scripts.map(({src, crossOrigin}) =>
helpers.renderLink({href: src, crossOrigin, rel: 'preload', as: 'script'}),
),
...links.map((link) => helpers.renderLink(link)),
...meta.map((metaData) => helpers.renderMeta(metaData)),
...styleSheets.map((style) => helpers.renderStyle(style)),
...inlineStyleSheets.map((text) => helpers.renderInlineStyle(text)),
...scripts.map((script) => helpers.renderScript(script)),
...inlineScripts.map((text) => helpers.renderInlineScript(text)),
]
.filter(Boolean)
.join('\n')}
${renderHeadContent(content)}
</head>
<body ${attrs({class: bodyContent.className.filter(Boolean).join(' ')})}>
${bodyContent.beforeRoot.join('\n')}
<div id="root">
${bodyContent.root ?? ''}
</div>
${bodyContent.afterRoot.join('\n')}
<body ${helpers.attrs({...bodyContent.attributes})}>
${renderBodyContent(content)}
</body>
</html>
`.trim();
Expand Down
20 changes: 14 additions & 6 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ export interface CommonOptions {
isMobile?: boolean;
}

export interface BodyContent {
attributes: Record<string, string>;
className: string[];
beforeRoot: string[];
root?: string;
afterRoot: string[];
}

export interface RenderContent {
htmlAttributes: Record<string, string>;
meta: Meta[];
Expand All @@ -44,12 +52,10 @@ export interface RenderContent {
styleSheets: Stylesheet[];
inlineScripts: string[];
inlineStyleSheets: string[];
bodyContent: {
className: string[];
beforeRoot: string[];
root?: string;
afterRoot: string[];
};
bodyContent: BodyContent;
helpers: RenderHelpers;
icon: Icon;
title: string;
}

export interface RenderHelpers {
Expand All @@ -59,6 +65,7 @@ export interface RenderHelpers {
renderInlineStyle(content: string): string;
renderMeta(meta: Meta): string;
renderLink(link: Link): string;
attrs(obj: Attributes): string;
}
export interface Plugin<Options = any, Name extends string = string> {

Check warning on line 70 in src/types.ts

View workflow job for this annotation

GitHub Actions / Verify Files

Unexpected any. Specify a different type
name: Name;
Expand All @@ -71,6 +78,7 @@ export interface Plugin<Options = any, Name extends string = string> {
}
export interface RenderParams<Data, Plugins extends Plugin[] = []> extends CommonOptions {
data?: Data;
skipRenderDataScript?: boolean;
icon?: Icon;
nonce?: string;
// content
Expand Down
1 change: 1 addition & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export function getRenderHelpers(params: {nonce?: string}): RenderHelpers {
renderInlineStyle,
renderMeta,
renderLink,
attrs,
};
}

Expand Down
Loading

0 comments on commit 67de491

Please sign in to comment.