From b0cedc863e959efc2894484cfc0ec04537d369c1 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 -- yarn.lock | 8 +-- 7 files changed, 38 insertions(+), 57 deletions(-) diff --git a/packages/astro/package.json b/packages/astro/package.json index 0734e67a6..23508301e 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -56,7 +56,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.2", "@astrojs/prism": "0.4.0", diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index a70c4ad15..cb3d2b950 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -303,17 +303,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 41898fe28..05728527f 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() @@ -101,10 +96,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 554fbfb8a..c7ccef245 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'; @@ -97,7 +97,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 d584dc296..7fd4ebe00 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, diff --git a/yarn.lock b/yarn.lock index 82bebfd65..b812207e2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -156,10 +156,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"