Improve head injection behavior (#2436)

* feat: add renderHead util to server

* feat: remove `layouts` from config, Vite plugin

* fix: improve head injection during rendering

* chore: update compiler

* fix: do not escape links
This commit is contained in:
Nate Moore 2022-03-02 16:00:53 -06:00
parent 34c2d1e8fc
commit 7a66ddc60a
7 changed files with 38 additions and 57 deletions

View file

@ -58,7 +58,7 @@
"test:match": "mocha --timeout 20000 -g"
},
"dependencies": {
"@astrojs/compiler": "^0.12.0-next.5",
"@astrojs/compiler": "^0.12.0-next.8",
"@astrojs/language-server": "^0.8.6",
"@astrojs/markdown-remark": "^0.6.4",
"@astrojs/prism": "0.4.0",

View file

@ -515,17 +515,6 @@ export type Params = Record<string, string | undefined>;
export type Props = Record<string, unknown>;
export interface RenderPageOptions {
request: {
params?: Params;
url: URL;
canonicalURL: URL;
};
children: any[];
props: Props;
css?: string[];
}
type Body = string;
export interface EndpointOutput<Output extends Body = Body> {

View file

@ -26,11 +26,6 @@ export const AstroConfigSchema = z.object({
.optional()
.default('./src/pages')
.transform((val) => new URL(val)),
layouts: z
.string()
.optional()
.default('./src/layouts')
.transform((val) => new URL(val)),
public: z
.string()
.optional()
@ -99,10 +94,6 @@ export async function validateConfig(userConfig: any, root: string): Promise<Ast
.string()
.default('./src/pages')
.transform((val) => new URL(addTrailingSlash(val), fileProtocolRoot)),
layouts: z
.string()
.default('./src/layouts')
.transform((val) => new URL(addTrailingSlash(val), fileProtocolRoot)),
public: z
.string()
.default('./public')

View file

@ -1,7 +1,7 @@
import type { ComponentInstance, EndpointHandler, MarkdownRenderOptions, Params, Props, Renderer, RouteData, SSRElement } from '../../@types/astro';
import type { LogOptions } from '../logger.js';
import { renderEndpoint, renderPage } from '../../runtime/server/index.js';
import { renderEndpoint, renderHead, renderToString } from '../../runtime/server/index.js';
import { getParams } from '../routing/index.js';
import { createResult } from './result.js';
import { findPathItemByKey, RouteCache, callGetStaticPaths } from './route-cache.js';
@ -97,7 +97,14 @@ export async function render(opts: RenderOptions): Promise<string> {
scripts,
});
let html = await renderPage(result, Component, pageProps, null);
let html = await renderToString(result, Component, pageProps, null);
// handle final head injection if it hasn't happened already
if (html.indexOf("<!--astro:head:injected-->") == -1) {
html = await renderHead(result) + html;
}
// cleanup internal state flags
html = html.replace("<!--astro:head:injected-->", '');
// inject <!doctype html> if missing (TODO: is a more robust check needed for comments, etc.?)
if (!legacyBuild && !/<!doctype html/i.test(html)) {

View file

@ -71,7 +71,7 @@ export class AstroComponent {
const html = htmlParts[i];
const expression = expressions[i];
yield _render(unescapeHTML(html));
yield unescapeHTML(html);
yield _render(expression);
}
}
@ -405,20 +405,6 @@ export function defineScriptVars(vars: Record<any, any>) {
return output;
}
// Calls a component and renders it into a string of HTML
export async function renderToString(result: SSRResult, componentFactory: AstroComponentFactory, props: any, children: any) {
const Component = await componentFactory(result, props, children);
let template = await renderAstroComponent(Component);
return unescapeHTML(template);
}
// Filter out duplicate elements in our set
const uniqueElements = (item: any, index: number, all: any[]) => {
const props = JSON.stringify(item.props);
const children = item.children;
return index === all.findIndex((i) => JSON.stringify(i.props) === props && i.children == children);
};
// Renders an endpoint request to completion, returning the body.
export async function renderEndpoint(mod: EndpointHandler, params: any) {
const method = 'get';
@ -433,15 +419,34 @@ export async function renderEndpoint(mod: EndpointHandler, params: any) {
return body;
}
// Calls a component and renders it into a string of HTML
export async function renderToString(result: SSRResult, componentFactory: AstroComponentFactory, props: any, children: any) {
const Component = await componentFactory(result, props, children);
let template = await renderAstroComponent(Component);
// <!--astro:head--> injected by compiler
// Must be handled at the end of the rendering process
if (template.indexOf('<!--astro:head-->') > -1) {
template = template.replace('<!--astro:head-->', await renderHead(result));
}
return template;
}
// Filter out duplicate elements in our set
const uniqueElements = (item: any, index: number, all: any[]) => {
const props = JSON.stringify(item.props);
const children = item.children;
return index === all.findIndex((i) => JSON.stringify(i.props) === props && i.children == children);
};
// Renders a page to completion by first calling the factory callback, waiting for its result, and then appending
// styles and scripts into the head.
export async function renderPage(result: SSRResult, Component: AstroComponentFactory, props: any, children: any) {
const template = await renderToString(result, Component, props, children);
export async function renderHead(result: SSRResult) {
const styles = Array.from(result.styles)
.filter(uniqueElements)
.map((style) => {
const styleChildren = !result._metadata.legacyBuild ? '' : style.children;
return renderElement('style', {
children: styleChildren,
props: { ...style.props, 'astro-style': true },
@ -462,17 +467,10 @@ export async function renderPage(result: SSRResult, Component: AstroComponentFac
if (needsHydrationStyles) {
styles.push(renderElement('style', { props: { 'astro-style': true }, children: 'astro-root, astro-fragment { display: contents; }' }));
}
const links = Array.from(result.links)
.filter(uniqueElements)
.map((link) => renderElement('link', link, false));
// inject styles & scripts at end of <head>
let headPos = template.indexOf('</head>');
if (headPos === -1) {
return links.join('\n') + styles.join('\n') + scripts.join('\n') + template; // if no </head>, prepend styles & scripts
}
return template.substring(0, headPos) + links.join('\n') + styles.join('\n') + scripts.join('\n') + template.substring(headPos);
return unescapeHTML(links.join('\n') + styles.join('\n') + scripts.join('\n') + '\n' + '<!--astro:head:injected-->');
}
export async function renderAstroComponent(component: InstanceType<typeof AstroComponent>) {

View file

@ -33,11 +33,8 @@ function safelyReplaceImportPlaceholder(code: string) {
const configCache = new WeakMap<AstroConfig, CompilationCache>();
async function compile(config: AstroConfig, filename: string, source: string, viteTransform: TransformHook, opts: { ssr: boolean }): Promise<CompileResult> {
// pages and layouts should be transformed as full documents (implicit <head> <body> etc)
// everything else is treated as a fragment
const filenameURL = new URL(`file://${filename}`);
const normalizedID = fileURLToPath(filenameURL);
const isPage = normalizedID.startsWith(fileURLToPath(config.pages)) || normalizedID.startsWith(fileURLToPath(config.layouts));
const pathname = filenameURL.pathname.substr(config.projectRoot.pathname.length - 1);
let rawCSSDeps = new Set<string>();
@ -47,7 +44,6 @@ async function compile(config: AstroConfig, filename: string, source: string, vi
// use `sourcemap: "both"` so that sourcemap is included in the code
// result passed to esbuild, but also available in the catch handler.
const transformResult = await transform(source, {
as: isPage ? 'document' : 'fragment',
pathname,
projectRoot: config.projectRoot.toString(),
site: config.buildOptions.site,

View file

@ -137,10 +137,10 @@
jsonpointer "^5.0.0"
leven "^3.1.0"
"@astrojs/compiler@^0.12.0-next.5":
version "0.12.0-next.5"
resolved "https://registry.yarnpkg.com/@astrojs/compiler/-/compiler-0.12.0-next.5.tgz#4e6d27c74787777522395018f2497dab4a032c77"
integrity sha512-4YVPRrB9JJhxoNC9PWN2zpGE7SXRAXcyCouawbd24iyBl4g9aRoQN12XA0qQZkbea9/NNLe9f2yhFMubM2CrJQ==
"@astrojs/compiler@^0.12.0-next.8":
version "0.12.0-next.8"
resolved "https://registry.yarnpkg.com/@astrojs/compiler/-/compiler-0.12.0-next.8.tgz#a15792791790aaeeaf944797635a40a56f806975"
integrity sha512-HeREaw5OR5J7zML+/LxhrqUr57571kyNXL4HD2pU929oevhx3PQ37PQ0FkD5N65X9YfO+gcoEO6whl76vtSZag==
dependencies:
typescript "^4.3.5"