From fa7ed3f3a9ce89c1c46e637b584271a6e199d211 Mon Sep 17 00:00:00 2001
From: Matthew Phillips <matthew@skypack.dev>
Date: Thu, 23 Jun 2022 15:37:55 -0400
Subject: [PATCH] Remove post-rendering head injection (#3679)

* Remove post-rendering head injection

* Adds a changeset

* Use a layout component for vue
---
 .changeset/tasty-hornets-return.md            | 11 +++++
 .../src/components/Layout.astro               |  4 ++
 .../preact-component/src/pages/markdown.md    |  1 +
 .../src/components/Layout.astro               |  4 ++
 .../react-component/src/pages/markdown.md     |  1 +
 .../src/components/Layout.astro               |  4 ++
 .../solid-component/src/pages/markdown.md     |  1 +
 .../src/components/Layout.astro               |  4 ++
 .../svelte-component/src/pages/markdown.md    |  1 +
 .../vue-component/src/components/Layout.astro |  4 ++
 .../vue-component/src/pages/markdown.md       |  1 +
 packages/astro/package.json                   |  2 +-
 packages/astro/src/@types/astro.ts            |  1 -
 packages/astro/src/core/render/core.ts        |  6 ---
 packages/astro/src/core/render/result.ts      |  1 -
 packages/astro/src/runtime/server/index.ts    | 43 +++++++------------
 packages/astro/src/runtime/server/scripts.ts  |  2 +-
 packages/astro/test/0-css.test.js             |  2 +-
 .../astro/test/astro-partial-html.test.js     |  6 +++
 .../src/pages/with-head.astro                 |  9 ++++
 packages/webapi/mod.d.ts                      |  2 +-
 pnpm-lock.yaml                                |  8 ++--
 22 files changed, 74 insertions(+), 44 deletions(-)
 create mode 100644 .changeset/tasty-hornets-return.md
 create mode 100644 packages/astro/e2e/fixtures/preact-component/src/components/Layout.astro
 create mode 100644 packages/astro/e2e/fixtures/react-component/src/components/Layout.astro
 create mode 100644 packages/astro/e2e/fixtures/solid-component/src/components/Layout.astro
 create mode 100644 packages/astro/e2e/fixtures/svelte-component/src/components/Layout.astro
 create mode 100644 packages/astro/e2e/fixtures/vue-component/src/components/Layout.astro
 create mode 100644 packages/astro/test/fixtures/astro-partial-html/src/pages/with-head.astro

diff --git a/.changeset/tasty-hornets-return.md b/.changeset/tasty-hornets-return.md
new file mode 100644
index 0000000000..2852303110
--- /dev/null
+++ b/.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.
diff --git a/packages/astro/e2e/fixtures/preact-component/src/components/Layout.astro b/packages/astro/e2e/fixtures/preact-component/src/components/Layout.astro
new file mode 100644
index 0000000000..3c3cf4e4de
--- /dev/null
+++ b/packages/astro/e2e/fixtures/preact-component/src/components/Layout.astro
@@ -0,0 +1,4 @@
+<html>
+	<head><title>Preact component</title></head>
+	<body><slot></slot></body>
+</html>
diff --git a/packages/astro/e2e/fixtures/preact-component/src/pages/markdown.md b/packages/astro/e2e/fixtures/preact-component/src/pages/markdown.md
index c05e2ae527..7c521de772 100644
--- a/packages/astro/e2e/fixtures/preact-component/src/pages/markdown.md
+++ b/packages/astro/e2e/fixtures/preact-component/src/pages/markdown.md
@@ -1,4 +1,5 @@
 ---
+layout: ../components/Layout.astro
 setup: |
   import Counter from '../components/Counter.jsx';
   import PreactComponent from '../components/JSXComponent.jsx';
diff --git a/packages/astro/e2e/fixtures/react-component/src/components/Layout.astro b/packages/astro/e2e/fixtures/react-component/src/components/Layout.astro
new file mode 100644
index 0000000000..7c166b5321
--- /dev/null
+++ b/packages/astro/e2e/fixtures/react-component/src/components/Layout.astro
@@ -0,0 +1,4 @@
+<html>
+	<head><title>React component</title></head>
+	<body><slot></slot></body>
+</html>
diff --git a/packages/astro/e2e/fixtures/react-component/src/pages/markdown.md b/packages/astro/e2e/fixtures/react-component/src/pages/markdown.md
index 5461fc48a0..fbc685a5be 100644
--- a/packages/astro/e2e/fixtures/react-component/src/pages/markdown.md
+++ b/packages/astro/e2e/fixtures/react-component/src/pages/markdown.md
@@ -1,4 +1,5 @@
 ---
+layout: ../components/Layout.astro
 setup: |
   import Counter from '../components/Counter.jsx';
   import ReactComponent from '../components/JSXComponent.jsx';
diff --git a/packages/astro/e2e/fixtures/solid-component/src/components/Layout.astro b/packages/astro/e2e/fixtures/solid-component/src/components/Layout.astro
new file mode 100644
index 0000000000..63e0ff4490
--- /dev/null
+++ b/packages/astro/e2e/fixtures/solid-component/src/components/Layout.astro
@@ -0,0 +1,4 @@
+<html>
+	<head><title>Solid component</title></head>
+	<body><slot></slot></body>
+</html>
diff --git a/packages/astro/e2e/fixtures/solid-component/src/pages/markdown.md b/packages/astro/e2e/fixtures/solid-component/src/pages/markdown.md
index 22d5464811..21a779c9d8 100644
--- a/packages/astro/e2e/fixtures/solid-component/src/pages/markdown.md
+++ b/packages/astro/e2e/fixtures/solid-component/src/pages/markdown.md
@@ -1,4 +1,5 @@
 ---
+layout: ../components/Layout.astro
 setup: |
   import Counter from '../components/Counter.jsx';
   import SolidComponent from '../components/SolidComponent.jsx';
diff --git a/packages/astro/e2e/fixtures/svelte-component/src/components/Layout.astro b/packages/astro/e2e/fixtures/svelte-component/src/components/Layout.astro
new file mode 100644
index 0000000000..63e0ff4490
--- /dev/null
+++ b/packages/astro/e2e/fixtures/svelte-component/src/components/Layout.astro
@@ -0,0 +1,4 @@
+<html>
+	<head><title>Solid component</title></head>
+	<body><slot></slot></body>
+</html>
diff --git a/packages/astro/e2e/fixtures/svelte-component/src/pages/markdown.md b/packages/astro/e2e/fixtures/svelte-component/src/pages/markdown.md
index 0030bccd18..ebc4d87955 100644
--- a/packages/astro/e2e/fixtures/svelte-component/src/pages/markdown.md
+++ b/packages/astro/e2e/fixtures/svelte-component/src/pages/markdown.md
@@ -1,4 +1,5 @@
 ---
+layout: ../components/Layout.astro
 setup: |
   import Counter from '../components/Counter.svelte';
   import SvelteComponent from '../components/SvelteComponent.svelte';
diff --git a/packages/astro/e2e/fixtures/vue-component/src/components/Layout.astro b/packages/astro/e2e/fixtures/vue-component/src/components/Layout.astro
new file mode 100644
index 0000000000..285bc56e20
--- /dev/null
+++ b/packages/astro/e2e/fixtures/vue-component/src/components/Layout.astro
@@ -0,0 +1,4 @@
+<html>
+	<head><title>Vue component</title></head>
+	<body><slot></slot></body>
+</html>
diff --git a/packages/astro/e2e/fixtures/vue-component/src/pages/markdown.md b/packages/astro/e2e/fixtures/vue-component/src/pages/markdown.md
index 22698931a1..3ae0470aff 100644
--- a/packages/astro/e2e/fixtures/vue-component/src/pages/markdown.md
+++ b/packages/astro/e2e/fixtures/vue-component/src/pages/markdown.md
@@ -1,4 +1,5 @@
 ---
+layout: ../components/Layout.astro
 setup: |
   import Counter from '../components/Counter.vue';
   import VueComponent from '../components/VueComponent.vue';
diff --git a/packages/astro/package.json b/packages/astro/package.json
index d145c49794..326e64fa6a 100644
--- a/packages/astro/package.json
+++ b/packages/astro/package.json
@@ -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",
diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts
index d2ef923654..48cac0d12c 100644
--- a/packages/astro/src/@types/astro.ts
+++ b/packages/astro/src/@types/astro.ts
@@ -1004,7 +1004,6 @@ export interface SSRElement {
 export interface SSRMetadata {
 	renderers: SSRLoadedRenderer[];
 	pathname: string;
-	needsHydrationStyles: boolean;
 }
 
 export interface SSRResult {
diff --git a/packages/astro/src/core/render/core.ts b/packages/astro/src/core/render/core.ts
index 48d362924d..32641c0208 100644
--- a/packages/astro/src/core/render/core.ts
+++ b/packages/astro/src/core/render/core.ts
@@ -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)) {
diff --git a/packages/astro/src/core/render/result.ts b/packages/astro/src/core/render/result.ts
index 05ec344b91..457efe44a2 100644
--- a/packages/astro/src/core/render/result.ts
+++ b/packages/astro/src/core/render/result.ts
@@ -221,7 +221,6 @@ ${extra}`
 		},
 		resolve,
 		_metadata: {
-			needsHydrationStyles: false,
 			renderers,
 			pathname,
 		},
diff --git a/packages/astro/src/runtime/server/index.ts b/packages/astro/src/runtime/server/index.ts
index 1b78d71713..322e212df6 100644
--- a/packages/astro/src/runtime/server/index.ts
+++ b/packages/astro/src/runtime/server/index.ts
@@ -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[] = [];
@@ -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,
@@ -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,
@@ -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 = [];
 
diff --git a/packages/astro/src/runtime/server/scripts.ts b/packages/astro/src/runtime/server/scripts.ts
index 0446ed2c70..4fe7b9057e 100644
--- a/packages/astro/src/runtime/server/scripts.ts
+++ b/packages/astro/src/runtime/server/scripts.ts
@@ -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>`;
 	}
diff --git a/packages/astro/test/0-css.test.js b/packages/astro/test/0-css.test.js
index e1b317f32d..4b2862470c 100644
--- a/packages/astro/test/0-css.test.js
+++ b/packages/astro/test/0-css.test.js
@@ -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 () => {
diff --git a/packages/astro/test/astro-partial-html.test.js b/packages/astro/test/astro-partial-html.test.js
index 5ae2929ce9..484adc21c4 100644
--- a/packages/astro/test/astro-partial-html.test.js
+++ b/packages/astro/test/astro-partial-html.test.js
@@ -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);
+	});
 });
diff --git a/packages/astro/test/fixtures/astro-partial-html/src/pages/with-head.astro b/packages/astro/test/fixtures/astro-partial-html/src/pages/with-head.astro
new file mode 100644
index 0000000000..fbbcecd1e9
--- /dev/null
+++ b/packages/astro/test/fixtures/astro-partial-html/src/pages/with-head.astro
@@ -0,0 +1,9 @@
+<html>
+	<head>
+		<title>testing</title>
+		<style>body { color: blue; }</style>
+	</head>
+	<body>
+		<h1>testing</h1>
+	</body>
+</html>
diff --git a/packages/webapi/mod.d.ts b/packages/webapi/mod.d.ts
index a3c49dc5c4..b385e82a5e 100644
--- a/packages/webapi/mod.d.ts
+++ b/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;
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 70337edc66..c927429624 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -463,7 +463,7 @@ importers:
 
   packages/astro:
     specifiers:
-      '@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
@@ -547,7 +547,7 @@ importers:
       yargs-parser: ^21.0.1
       zod: ^3.17.3
     dependencies:
-      '@astrojs/compiler': 0.16.1
+      '@astrojs/compiler': 0.17.0
       '@astrojs/language-server': 0.13.4
       '@astrojs/markdown-remark': link:../markdown/remark
       '@astrojs/prism': link:../astro-prism
@@ -2439,8 +2439,8 @@ packages:
       leven: 3.1.0
     dev: true
 
-  /@astrojs/compiler/0.16.1:
-    resolution: {integrity: sha512-6l5j9b/sEdyqRUvwJpp+SmlAkNO5WeISuNEXnyH9aGwzIAdqgLB2boAJef9lWadlOjG8rSPO29WHRa3qS2Okew==}
+  /@astrojs/compiler/0.17.0:
+    resolution: {integrity: sha512-3q6Yw6CGDfUwheDS29cHjQxn57ql0X98DskU6ym3bw/FdD8RMbGi0Es1Evlh+WHig948LUcYq19EHAMZO3bP3w==}
     dev: false
 
   /@astrojs/language-server/0.13.4: