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:
parent
e9630b86b3
commit
b0cedc863e
7 changed files with 38 additions and 57 deletions
|
@ -56,7 +56,7 @@
|
||||||
"test:match": "mocha --timeout 20000 -g"
|
"test:match": "mocha --timeout 20000 -g"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/compiler": "^0.12.0-next.5",
|
"@astrojs/compiler": "^0.12.0-next.8",
|
||||||
"@astrojs/language-server": "^0.8.6",
|
"@astrojs/language-server": "^0.8.6",
|
||||||
"@astrojs/markdown-remark": "^0.6.2",
|
"@astrojs/markdown-remark": "^0.6.2",
|
||||||
"@astrojs/prism": "0.4.0",
|
"@astrojs/prism": "0.4.0",
|
||||||
|
|
|
@ -303,17 +303,6 @@ export type Params = Record<string, string | undefined>;
|
||||||
|
|
||||||
export type Props = Record<string, unknown>;
|
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;
|
type Body = string;
|
||||||
|
|
||||||
export interface EndpointOutput<Output extends Body = Body> {
|
export interface EndpointOutput<Output extends Body = Body> {
|
||||||
|
|
|
@ -26,11 +26,6 @@ export const AstroConfigSchema = z.object({
|
||||||
.optional()
|
.optional()
|
||||||
.default('./src/pages')
|
.default('./src/pages')
|
||||||
.transform((val) => new URL(val)),
|
.transform((val) => new URL(val)),
|
||||||
layouts: z
|
|
||||||
.string()
|
|
||||||
.optional()
|
|
||||||
.default('./src/layouts')
|
|
||||||
.transform((val) => new URL(val)),
|
|
||||||
public: z
|
public: z
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
|
@ -101,10 +96,6 @@ export async function validateConfig(userConfig: any, root: string): Promise<Ast
|
||||||
.string()
|
.string()
|
||||||
.default('./src/pages')
|
.default('./src/pages')
|
||||||
.transform((val) => new URL(addTrailingSlash(val), fileProtocolRoot)),
|
.transform((val) => new URL(addTrailingSlash(val), fileProtocolRoot)),
|
||||||
layouts: z
|
|
||||||
.string()
|
|
||||||
.default('./src/layouts')
|
|
||||||
.transform((val) => new URL(addTrailingSlash(val), fileProtocolRoot)),
|
|
||||||
public: z
|
public: z
|
||||||
.string()
|
.string()
|
||||||
.default('./public')
|
.default('./public')
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import type { ComponentInstance, EndpointHandler, MarkdownRenderOptions, Params, Props, Renderer, RouteData, SSRElement } from '../../@types/astro';
|
import type { ComponentInstance, EndpointHandler, MarkdownRenderOptions, Params, Props, Renderer, RouteData, SSRElement } from '../../@types/astro';
|
||||||
import type { LogOptions } from '../logger.js';
|
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 { getParams } from '../routing/index.js';
|
||||||
import { createResult } from './result.js';
|
import { createResult } from './result.js';
|
||||||
import { findPathItemByKey, RouteCache, callGetStaticPaths } from './route-cache.js';
|
import { findPathItemByKey, RouteCache, callGetStaticPaths } from './route-cache.js';
|
||||||
|
@ -97,7 +97,14 @@ export async function render(opts: RenderOptions): Promise<string> {
|
||||||
scripts,
|
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.?)
|
// inject <!doctype html> if missing (TODO: is a more robust check needed for comments, etc.?)
|
||||||
if (!legacyBuild && !/<!doctype html/i.test(html)) {
|
if (!legacyBuild && !/<!doctype html/i.test(html)) {
|
||||||
|
|
|
@ -71,7 +71,7 @@ export class AstroComponent {
|
||||||
const html = htmlParts[i];
|
const html = htmlParts[i];
|
||||||
const expression = expressions[i];
|
const expression = expressions[i];
|
||||||
|
|
||||||
yield _render(unescapeHTML(html));
|
yield unescapeHTML(html);
|
||||||
yield _render(expression);
|
yield _render(expression);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -405,20 +405,6 @@ export function defineScriptVars(vars: Record<any, any>) {
|
||||||
return output;
|
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.
|
// Renders an endpoint request to completion, returning the body.
|
||||||
export async function renderEndpoint(mod: EndpointHandler, params: any) {
|
export async function renderEndpoint(mod: EndpointHandler, params: any) {
|
||||||
const method = 'get';
|
const method = 'get';
|
||||||
|
@ -433,15 +419,34 @@ export async function renderEndpoint(mod: EndpointHandler, params: any) {
|
||||||
return body;
|
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
|
// Renders a page to completion by first calling the factory callback, waiting for its result, and then appending
|
||||||
// styles and scripts into the head.
|
// styles and scripts into the head.
|
||||||
export async function renderPage(result: SSRResult, Component: AstroComponentFactory, props: any, children: any) {
|
export async function renderHead(result: SSRResult) {
|
||||||
const template = await renderToString(result, Component, props, children);
|
|
||||||
const styles = Array.from(result.styles)
|
const styles = Array.from(result.styles)
|
||||||
.filter(uniqueElements)
|
.filter(uniqueElements)
|
||||||
.map((style) => {
|
.map((style) => {
|
||||||
const styleChildren = !result._metadata.legacyBuild ? '' : style.children;
|
const styleChildren = !result._metadata.legacyBuild ? '' : style.children;
|
||||||
|
|
||||||
return renderElement('style', {
|
return renderElement('style', {
|
||||||
children: styleChildren,
|
children: styleChildren,
|
||||||
props: { ...style.props, 'astro-style': true },
|
props: { ...style.props, 'astro-style': true },
|
||||||
|
@ -462,17 +467,10 @@ export async function renderPage(result: SSRResult, Component: AstroComponentFac
|
||||||
if (needsHydrationStyles) {
|
if (needsHydrationStyles) {
|
||||||
styles.push(renderElement('style', { props: { 'astro-style': true }, children: 'astro-root, astro-fragment { display: contents; }' }));
|
styles.push(renderElement('style', { props: { 'astro-style': true }, children: 'astro-root, astro-fragment { display: contents; }' }));
|
||||||
}
|
}
|
||||||
|
|
||||||
const links = Array.from(result.links)
|
const links = Array.from(result.links)
|
||||||
.filter(uniqueElements)
|
.filter(uniqueElements)
|
||||||
.map((link) => renderElement('link', link, false));
|
.map((link) => renderElement('link', link, false));
|
||||||
|
return unescapeHTML(links.join('\n') + styles.join('\n') + scripts.join('\n') + '\n' + '<!--astro:head:injected-->');
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function renderAstroComponent(component: InstanceType<typeof AstroComponent>) {
|
export async function renderAstroComponent(component: InstanceType<typeof AstroComponent>) {
|
||||||
|
|
|
@ -33,11 +33,8 @@ function safelyReplaceImportPlaceholder(code: string) {
|
||||||
const configCache = new WeakMap<AstroConfig, CompilationCache>();
|
const configCache = new WeakMap<AstroConfig, CompilationCache>();
|
||||||
|
|
||||||
async function compile(config: AstroConfig, filename: string, source: string, viteTransform: TransformHook, opts: { ssr: boolean }): Promise<CompileResult> {
|
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 filenameURL = new URL(`file://${filename}`);
|
||||||
const normalizedID = fileURLToPath(filenameURL);
|
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);
|
const pathname = filenameURL.pathname.substr(config.projectRoot.pathname.length - 1);
|
||||||
|
|
||||||
let rawCSSDeps = new Set<string>();
|
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
|
// use `sourcemap: "both"` so that sourcemap is included in the code
|
||||||
// result passed to esbuild, but also available in the catch handler.
|
// result passed to esbuild, but also available in the catch handler.
|
||||||
const transformResult = await transform(source, {
|
const transformResult = await transform(source, {
|
||||||
as: isPage ? 'document' : 'fragment',
|
|
||||||
pathname,
|
pathname,
|
||||||
projectRoot: config.projectRoot.toString(),
|
projectRoot: config.projectRoot.toString(),
|
||||||
site: config.buildOptions.site,
|
site: config.buildOptions.site,
|
||||||
|
|
|
@ -156,10 +156,10 @@
|
||||||
jsonpointer "^5.0.0"
|
jsonpointer "^5.0.0"
|
||||||
leven "^3.1.0"
|
leven "^3.1.0"
|
||||||
|
|
||||||
"@astrojs/compiler@^0.12.0-next.5":
|
"@astrojs/compiler@^0.12.0-next.8":
|
||||||
version "0.12.0-next.5"
|
version "0.12.0-next.8"
|
||||||
resolved "https://registry.yarnpkg.com/@astrojs/compiler/-/compiler-0.12.0-next.5.tgz#4e6d27c74787777522395018f2497dab4a032c77"
|
resolved "https://registry.yarnpkg.com/@astrojs/compiler/-/compiler-0.12.0-next.8.tgz#a15792791790aaeeaf944797635a40a56f806975"
|
||||||
integrity sha512-4YVPRrB9JJhxoNC9PWN2zpGE7SXRAXcyCouawbd24iyBl4g9aRoQN12XA0qQZkbea9/NNLe9f2yhFMubM2CrJQ==
|
integrity sha512-HeREaw5OR5J7zML+/LxhrqUr57571kyNXL4HD2pU929oevhx3PQ37PQ0FkD5N65X9YfO+gcoEO6whl76vtSZag==
|
||||||
dependencies:
|
dependencies:
|
||||||
typescript "^4.3.5"
|
typescript "^4.3.5"
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue