From 768199c7f279005f9390415b88b87da11b50d466 Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Wed, 2 Mar 2022 16:00:53 -0600 Subject: [PATCH] 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 --- packages/astro/package.json | 2 +- packages/astro/src/@types/astro.ts | 11 ---- packages/astro/src/core/config.ts | 9 ---- packages/astro/src/core/render/core.ts | 11 +++- packages/astro/src/runtime/server/index.ts | 50 +++++++++---------- .../astro/src/vite-plugin-astro/compile.ts | 4 -- 6 files changed, 34 insertions(+), 53 deletions(-) diff --git a/packages/astro/package.json b/packages/astro/package.json index b4600a409149..2b4a216bc827 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -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", diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index 193965b98205..e945b8dd629b 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -515,17 +515,6 @@ export type Params = Record; export type Props = Record; -export interface RenderPageOptions { - request: { - params?: Params; - url: URL; - canonicalURL: URL; - }; - children: any[]; - props: Props; - css?: string[]; -} - type Body = string; export interface EndpointOutput { diff --git a/packages/astro/src/core/config.ts b/packages/astro/src/core/config.ts index b75184e5e26a..d9d73f36c26d 100644 --- a/packages/astro/src/core/config.ts +++ b/packages/astro/src/core/config.ts @@ -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 new URL(addTrailingSlash(val), fileProtocolRoot)), - layouts: z - .string() - .default('./src/layouts') - .transform((val) => new URL(addTrailingSlash(val), fileProtocolRoot)), public: z .string() .default('./public') diff --git a/packages/astro/src/core/render/core.ts b/packages/astro/src/core/render/core.ts index 18b26d069697..ba13defb1526 100644 --- a/packages/astro/src/core/render/core.ts +++ b/packages/astro/src/core/render/core.ts @@ -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'; @@ -100,7 +100,14 @@ export async function render(opts: RenderOptions): Promise { 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("") == -1) { + html = await renderHead(result) + html; + } + // cleanup internal state flags + html = html.replace("", ''); // inject if missing (TODO: is a more robust check needed for comments, etc.?) if (!legacyBuild && !/) { 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); + + // injected by compiler + // Must be handled at the end of the rendering process + if (template.indexOf('') > -1) { + template = template.replace('', 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 - let headPos = template.indexOf(''); - if (headPos === -1) { - return links.join('\n') + styles.join('\n') + scripts.join('\n') + template; // if no , 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' + ''); } export async function renderAstroComponent(component: InstanceType) { diff --git a/packages/astro/src/vite-plugin-astro/compile.ts b/packages/astro/src/vite-plugin-astro/compile.ts index ea7caaebd85a..4385d94682b7 100644 --- a/packages/astro/src/vite-plugin-astro/compile.ts +++ b/packages/astro/src/vite-plugin-astro/compile.ts @@ -33,11 +33,8 @@ function safelyReplaceImportPlaceholder(code: string) { const configCache = new WeakMap(); async function compile(config: AstroConfig, filename: string, source: string, viteTransform: TransformHook, opts: { ssr: boolean }): Promise { - // pages and layouts should be transformed as full documents (implicit 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(); @@ -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,