Skip to content

Commit

Permalink
Remove post-rendering head injection (#3679)
Browse files Browse the repository at this point in the history
* Remove post-rendering head injection

* Adds a changeset

* Use a layout component for vue
  • Loading branch information
matthewp committed Jun 23, 2022
1 parent 446f8c4 commit fa7ed3f
Show file tree
Hide file tree
Showing 22 changed files with 74 additions and 44 deletions.
11 changes: 11 additions & 0 deletions .changeset/tasty-hornets-return.md
@@ -0,0 +1,11 @@
---
'astro': patch
---

Moves head injection to happen during rendering

This change makes it so that head injection; to insert component stylesheets, hoisted scripts, for example, to happen during rendering than as a post-rendering step.

This is to enable streaming. This change will only be noticeable if you are rendering your `<head>` element inside of a framework component. If that is the case then the head items will be injected before the first non-head element in an Astro file instead.

In the future we may offer a `<Astro.Head>` component as a way to control where these scripts/styles are inserted.
@@ -0,0 +1,4 @@
<html>
<head><title>Preact component</title></head>
<body><slot></slot></body>
</html>
@@ -1,4 +1,5 @@
---
layout: ../components/Layout.astro
setup: |
import Counter from '../components/Counter.jsx';
import PreactComponent from '../components/JSXComponent.jsx';
Expand Down
@@ -0,0 +1,4 @@
<html>
<head><title>React component</title></head>
<body><slot></slot></body>
</html>
@@ -1,4 +1,5 @@
---
layout: ../components/Layout.astro
setup: |
import Counter from '../components/Counter.jsx';
import ReactComponent from '../components/JSXComponent.jsx';
Expand Down
@@ -0,0 +1,4 @@
<html>
<head><title>Solid component</title></head>
<body><slot></slot></body>
</html>
@@ -1,4 +1,5 @@
---
layout: ../components/Layout.astro
setup: |
import Counter from '../components/Counter.jsx';
import SolidComponent from '../components/SolidComponent.jsx';
Expand Down
@@ -0,0 +1,4 @@
<html>
<head><title>Solid component</title></head>
<body><slot></slot></body>
</html>
@@ -1,4 +1,5 @@
---
layout: ../components/Layout.astro
setup: |
import Counter from '../components/Counter.svelte';
import SvelteComponent from '../components/SvelteComponent.svelte';
Expand Down
@@ -0,0 +1,4 @@
<html>
<head><title>Vue component</title></head>
<body><slot></slot></body>
</html>
@@ -1,4 +1,5 @@
---
layout: ../components/Layout.astro
setup: |
import Counter from '../components/Counter.vue';
import VueComponent from '../components/VueComponent.vue';
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/package.json
Expand Up @@ -78,7 +78,7 @@
"test:e2e:match": "playwright test -g"
},
"dependencies": {
"@astrojs/compiler": "^0.16.1",
"@astrojs/compiler": "^0.17.0",
"@astrojs/language-server": "^0.13.4",
"@astrojs/markdown-remark": "^0.11.3",
"@astrojs/prism": "0.4.1",
Expand Down
1 change: 0 additions & 1 deletion packages/astro/src/@types/astro.ts
Expand Up @@ -1004,7 +1004,6 @@ export interface SSRElement {
export interface SSRMetadata {
renderers: SSRLoadedRenderer[];
pathname: string;
needsHydrationStyles: boolean;
}

export interface SSRResult {
Expand Down
6 changes: 0 additions & 6 deletions packages/astro/src/core/render/core.ts
Expand Up @@ -161,12 +161,6 @@ export async function render(
}

let html = page.html;
// 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 (!/<!doctype html/i.test(html)) {
Expand Down
1 change: 0 additions & 1 deletion packages/astro/src/core/render/result.ts
Expand Up @@ -221,7 +221,6 @@ ${extra}`
},
resolve,
_metadata: {
needsHydrationStyles: false,
renderers,
pathname,
},
Expand Down
43 changes: 15 additions & 28 deletions packages/astro/src/runtime/server/index.ts
Expand Up @@ -344,7 +344,6 @@ If you're still stuck, please open an issue on GitHub or join us at https://astr
{ renderer: renderer!, result, astroId, props },
metadata as Required<AstroComponentMetadata>
);
result._metadata.needsHydrationStyles = true;

// Render template if not all astro fragments are provided.
let unrenderedSlots: string[] = [];
Expand Down Expand Up @@ -590,16 +589,6 @@ Update your code to remove this warning.`);
return handler.call(mod, proxy, request);
}

async function replaceHeadInjection(result: SSRResult, html: string): Promise<string> {
let template = html;
// <!--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;
}

// Calls a component and renders it into a string of HTML
export async function renderToString(
result: SSRResult,
Expand Down Expand Up @@ -627,8 +616,7 @@ export async function renderPage(
const response = await componentFactory(result, props, children);

if (isAstroComponent(response)) {
let template = await renderAstroComponent(response);
const html = await replaceHeadInjection(result, template);
let html = await renderAstroComponent(response);
return {
type: 'html',
html,
Expand Down Expand Up @@ -660,37 +648,36 @@ const uniqueElements = (item: any, index: number, all: any[]) => {
);
};

// Renders a page to completion by first calling the factory callback, waiting for its result, and then appending
// styles and scripts into the head.
const alreadyHeadRenderedResults = new WeakSet<SSRResult>();
export async function renderHead(result: SSRResult): Promise<string> {
alreadyHeadRenderedResults.add(result);
const styles = Array.from(result.styles)
.filter(uniqueElements)
.map((style) => renderElement('style', style));
let needsHydrationStyles = result._metadata.needsHydrationStyles;
const scripts = Array.from(result.scripts)
.filter(uniqueElements)
.map((script, i) => {
if ('data-astro-component-hydration' in script.props) {
needsHydrationStyles = true;
}
return renderElement('script', script);
});
if (needsHydrationStyles) {
styles.push(
renderElement('style', {
props: {},
children: 'astro-island, astro-slot { display: contents; }',
})
);
}
const links = Array.from(result.links)
.filter(uniqueElements)
.map((link) => renderElement('link', link, false));
return markHTMLString(
links.join('\n') + styles.join('\n') + scripts.join('\n') + '\n' + '<!--astro:head:injected-->'
links.join('\n') + styles.join('\n') + scripts.join('\n')
);
}

// This function is called by Astro components that do not contain a <head> component
// This accomodates the fact that using a <head> is optional in Astro, so this
// is called before a component's first non-head HTML element. If the head was
// already injected it is a noop.
export function maybeRenderHead(result: SSRResult): string | Promise<string> {
if(alreadyHeadRenderedResults.has(result)) {
return '';
}
return renderHead(result);
}

export async function renderAstroComponent(component: InstanceType<typeof AstroComponent>) {
let template = [];

Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/runtime/server/scripts.ts
Expand Up @@ -59,7 +59,7 @@ export function getPrescripts(type: PrescriptType, directive: string): string {
// deps to be loaded immediately.
switch (type) {
case 'both':
return `<script>${getDirectiveScriptText(directive) + islandScript}</script>`;
return `<style>astro-island,astro-slot{display:contents}</style><script>${getDirectiveScriptText(directive) + islandScript}</script>`;
case 'directive':
return `<script>${getDirectiveScriptText(directive)}</script>`;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/test/0-css.test.js
Expand Up @@ -65,7 +65,7 @@ describe('CSS', function () {

it('Using hydrated components adds astro-island styles', async () => {
const inline = $('style').html();
expect(inline).to.include('display: contents');
expect(inline).to.include('display:contents');
});

it('<style lang="sass">', async () => {
Expand Down
6 changes: 6 additions & 0 deletions packages/astro/test/astro-partial-html.test.js
Expand Up @@ -40,4 +40,10 @@ describe('Partial HTML', async () => {
const allInjectedStyles = $('style[data-astro-injected]').text().replace(/\s*/g, '');
expect(allInjectedStyles).to.match(/h1{color:red;}/);
});

it('pages with a head, injection happens inside', async () => {
const html = await fixture.fetch('/with-head').then((res) => res.text());
const $ = cheerio.load(html);
expect($('style')).to.have.lengthOf(1);
});
});
@@ -0,0 +1,9 @@
<html>
<head>
<title>testing</title>
<style>body { color: blue; }</style>
</head>
<body>
<h1>testing</h1>
</body>
</html>
2 changes: 1 addition & 1 deletion packages/webapi/mod.d.ts
@@ -1,5 +1,5 @@
export { pathToPosix } from './lib/utils';
export { AbortController, AbortSignal, alert, atob, Blob, btoa, ByteLengthQueuingStrategy, cancelAnimationFrame, cancelIdleCallback, CanvasRenderingContext2D, CharacterData, clearTimeout, Comment, CountQueuingStrategy, CSSStyleSheet, CustomElementRegistry, CustomEvent, Document, DocumentFragment, DOMException, Element, Event, EventTarget, fetch, File, FormData, Headers, HTMLBodyElement, HTMLCanvasElement, HTMLDivElement, HTMLDocument, HTMLElement, HTMLHeadElement, HTMLHtmlElement, HTMLImageElement, HTMLSpanElement, HTMLStyleElement, HTMLTemplateElement, HTMLUnknownElement, Image, ImageData, IntersectionObserver, MediaQueryList, MutationObserver, Node, NodeFilter, NodeIterator, OffscreenCanvas, ReadableByteStreamController, ReadableStream, ReadableStreamBYOBReader, ReadableStreamBYOBRequest, ReadableStreamDefaultController, ReadableStreamDefaultReader, Request, requestAnimationFrame, requestIdleCallback, ResizeObserver, Response, setTimeout, ShadowRoot, structuredClone, StyleSheet, Text, TransformStream, TreeWalker, URLPattern, Window, WritableStream, WritableStreamDefaultController, WritableStreamDefaultWriter } from './mod.js';
export { AbortController, AbortSignal, alert, atob, Blob, btoa, ByteLengthQueuingStrategy, cancelAnimationFrame, cancelIdleCallback, CanvasRenderingContext2D, CharacterData, clearTimeout, Comment, CountQueuingStrategy, CSSStyleSheet, CustomElementRegistry, CustomEvent, Document, DocumentFragment, DOMException, Element, Event, EventTarget, fetch, File, FormData, Headers, HTMLBodyElement, HTMLCanvasElement, HTMLDivElement, HTMLDocument, HTMLElement, HTMLHeadElement, HTMLHtmlElement, HTMLImageElement, HTMLSpanElement, HTMLStyleElement, HTMLTemplateElement, HTMLUnknownElement, Image, ImageData, IntersectionObserver, MediaQueryList, MutationObserver, Node, NodeFilter, NodeIterator, OffscreenCanvas, ReadableByteStreamController, ReadableStream, ReadableStreamBYOBReader, ReadableStreamBYOBRequest, ReadableStreamDefaultController, ReadableStreamDefaultReader, Request, requestAnimationFrame, requestIdleCallback, ResizeObserver, Response, setTimeout, ShadowRoot, structuredClone, StyleSheet, Text, TransformStream, TreeWalker, URLPattern, Window, WritableStream, WritableStreamDefaultController, WritableStreamDefaultWriter, } from './mod.js';
export declare const polyfill: {
(target: any, options?: PolyfillOptions): any;
internals(target: any, name: string): any;
Expand Down
8 changes: 4 additions & 4 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit fa7ed3f

Please sign in to comment.