From 0f06d2d642759a0f01b4ed8c92340381c92b52a2 Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Thu, 2 Feb 2023 12:40:26 -0500 Subject: [PATCH 1/4] Prevent eager rendering of head content in multi-level MDX layout --- packages/astro/src/@types/astro.ts | 1 + packages/astro/src/core/render/result.ts | 1 + packages/astro/src/runtime/server/jsx.ts | 2 ++ .../astro/src/runtime/server/render/astro/factory.ts | 2 ++ packages/astro/src/runtime/server/render/common.ts | 2 ++ packages/astro/src/runtime/server/render/head.ts | 10 +++++++++- packages/astro/src/runtime/server/render/util.ts | 5 +++++ packages/integrations/mdx/test/css-head-mdx.test.js | 4 ++-- 8 files changed, 24 insertions(+), 3 deletions(-) diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index 7fab1556fe90..1843945af646 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -1449,6 +1449,7 @@ export interface SSRResult { ): AstroGlobal; resolve: (s: string) => Promise; response: ResponseInit; + scope: 0 | 1 | 2 | 3; _metadata: SSRMetadata; } diff --git a/packages/astro/src/core/render/result.ts b/packages/astro/src/core/render/result.ts index 06c5e0698343..3a6b0bf964d6 100644 --- a/packages/astro/src/core/render/result.ts +++ b/packages/astro/src/core/render/result.ts @@ -156,6 +156,7 @@ export function createResult(args: CreateResultArgs): SSRResult { propagation: args.propagation ?? new Map(), propagators: new Map(), extraHead: [], + scope: 0, cookies, /** This function returns the `Astro` faux-global */ createAstro( diff --git a/packages/astro/src/runtime/server/jsx.ts b/packages/astro/src/runtime/server/jsx.ts index 651ccc945bc1..74ac5ac30bba 100644 --- a/packages/astro/src/runtime/server/jsx.ts +++ b/packages/astro/src/runtime/server/jsx.ts @@ -11,6 +11,7 @@ import { voidElementNames, } from './index.js'; import { HTMLParts } from './render/common.js'; +import { ScopeFlags } from './render/util.js'; import type { ComponentIterable } from './render/component'; const ClientOnlyPlaceholder = 'astro-client-only'; @@ -94,6 +95,7 @@ Did you forget to import the component or is it possible there is a typo?`); props[key] = value; } } + result.scope |= ScopeFlags.JSX; return markHTMLString(await renderToString(result, vnode.type as any, props, slots)); } case !vnode.type && (vnode.type as any) !== 0: diff --git a/packages/astro/src/runtime/server/render/astro/factory.ts b/packages/astro/src/runtime/server/render/astro/factory.ts index 0d8ffb7876a2..a1e7d0611343 100644 --- a/packages/astro/src/runtime/server/render/astro/factory.ts +++ b/packages/astro/src/runtime/server/render/astro/factory.ts @@ -5,6 +5,7 @@ import type { RenderTemplateResult } from './render-template'; import { HTMLParts } from '../common.js'; import { isHeadAndContent } from './head-and-content.js'; import { renderAstroTemplateResult } from './render-template.js'; +import { ScopeFlags } from '../util.js'; export type AstroFactoryReturnValue = RenderTemplateResult | Response | HeadAndContent; @@ -27,6 +28,7 @@ export async function renderToString( props: any, children: any ): Promise { + result.scope |= ScopeFlags.Astro; const factoryResult = await componentFactory(result, props, children); if (factoryResult instanceof Response) { diff --git a/packages/astro/src/runtime/server/render/common.ts b/packages/astro/src/runtime/server/render/common.ts index def7dedd37e0..84ca12ccd554 100644 --- a/packages/astro/src/runtime/server/render/common.ts +++ b/packages/astro/src/runtime/server/render/common.ts @@ -43,6 +43,8 @@ export function stringifyChunk(result: SSRResult, chunk: string | SlotString | R } } case 'head': { + // Head should not be rendered when the scope is within a single component. + // That is, we are rendering a component and not within a page. if (result._metadata.hasRenderedHead) { return ''; } diff --git a/packages/astro/src/runtime/server/render/head.ts b/packages/astro/src/runtime/server/render/head.ts index ade6a7355c48..6ca2be8b9e08 100644 --- a/packages/astro/src/runtime/server/render/head.ts +++ b/packages/astro/src/runtime/server/render/head.ts @@ -1,7 +1,9 @@ import type { SSRResult } from '../../../@types/astro'; import { markHTMLString } from '../escape.js'; -import { renderElement } from './util.js'; +import { renderElement, ScopeFlags } from './util.js'; + +const AstroAndJSXScope = ScopeFlags.Astro | ScopeFlags.JSX; // Filter out duplicate elements in our set const uniqueElements = (item: any, index: number, all: any[]) => { @@ -52,6 +54,12 @@ export function* maybeRenderHead(result: SSRResult) { return; } + // Don't render the head inside of a JSX component that's inside of an Astro component + // as the Astro component will be the one to render the head. + if(result.scope & AstroAndJSXScope) { + return; + } + // This is an instruction informing the page rendering that head might need rendering. // This allows the page to deduplicate head injections. yield { type: 'head', result } as const; diff --git a/packages/astro/src/runtime/server/render/util.ts b/packages/astro/src/runtime/server/render/util.ts index a95ef16f8722..cc8bd2c95f27 100644 --- a/packages/astro/src/runtime/server/render/util.ts +++ b/packages/astro/src/runtime/server/render/util.ts @@ -128,3 +128,8 @@ export function renderElement( } return `<${name}${internalSpreadAttributes(props, shouldEscape)}>${children}`; } + +export const ScopeFlags = { + Astro: 1, + JSX: 2 +}; diff --git a/packages/integrations/mdx/test/css-head-mdx.test.js b/packages/integrations/mdx/test/css-head-mdx.test.js index a6492c3ba17b..2b1dcdfe7bcd 100644 --- a/packages/integrations/mdx/test/css-head-mdx.test.js +++ b/packages/integrations/mdx/test/css-head-mdx.test.js @@ -23,10 +23,10 @@ describe('Head injection w/ MDX', () => { const html = await fixture.readFile('/indexThree/index.html'); const { document } = parseHTML(html); - const links = document.querySelectorAll('link[rel=stylesheet]'); + const links = document.querySelectorAll('head link[rel=stylesheet]'); expect(links).to.have.a.lengthOf(1); - const scripts = document.querySelectorAll('script[type=module]'); + const scripts = document.querySelectorAll('head script[type=module]'); expect(scripts).to.have.a.lengthOf(1); }); }); From 3ec53830ff2b9d5f98fb0ec98b003ccf14de1e2d Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Thu, 2 Feb 2023 12:44:32 -0500 Subject: [PATCH 2/4] Adding a changeset --- .changeset/lucky-hounds-rhyme.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/lucky-hounds-rhyme.md diff --git a/.changeset/lucky-hounds-rhyme.md b/.changeset/lucky-hounds-rhyme.md new file mode 100644 index 000000000000..51b1c012b13f --- /dev/null +++ b/.changeset/lucky-hounds-rhyme.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Fixes head contents being placed in body in MDX components From c4e6ef1ec0ef636dd04da30a791adbd57f9dd93f Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Thu, 2 Feb 2023 12:45:33 -0500 Subject: [PATCH 3/4] Remove old comment --- packages/astro/src/runtime/server/render/common.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/astro/src/runtime/server/render/common.ts b/packages/astro/src/runtime/server/render/common.ts index 84ca12ccd554..def7dedd37e0 100644 --- a/packages/astro/src/runtime/server/render/common.ts +++ b/packages/astro/src/runtime/server/render/common.ts @@ -43,8 +43,6 @@ export function stringifyChunk(result: SSRResult, chunk: string | SlotString | R } } case 'head': { - // Head should not be rendered when the scope is within a single component. - // That is, we are rendering a component and not within a page. if (result._metadata.hasRenderedHead) { return ''; } From c202b3a5fd76711dbf51384409e7c010c7c426d2 Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Thu, 2 Feb 2023 13:55:31 -0500 Subject: [PATCH 4/4] Keep track of slot position as well --- packages/astro/src/@types/astro.ts | 5 ++++- packages/astro/src/runtime/server/jsx.ts | 2 +- packages/astro/src/runtime/server/render/head.ts | 8 ++++---- packages/astro/src/runtime/server/render/slot.ts | 6 +++++- packages/astro/src/runtime/server/render/util.ts | 5 +++-- 5 files changed, 17 insertions(+), 9 deletions(-) diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index 1843945af646..ea3e5cd3cf71 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -1449,7 +1449,10 @@ export interface SSRResult { ): AstroGlobal; resolve: (s: string) => Promise; response: ResponseInit; - scope: 0 | 1 | 2 | 3; + // Bits 1 = astro, 2 = jsx, 4 = slot + // As rendering occurs these bits are manipulated to determine where content + // is within a slot. This is used for head injection. + scope: number; _metadata: SSRMetadata; } diff --git a/packages/astro/src/runtime/server/jsx.ts b/packages/astro/src/runtime/server/jsx.ts index 74ac5ac30bba..365345a56623 100644 --- a/packages/astro/src/runtime/server/jsx.ts +++ b/packages/astro/src/runtime/server/jsx.ts @@ -95,7 +95,7 @@ Did you forget to import the component or is it possible there is a typo?`); props[key] = value; } } - result.scope |= ScopeFlags.JSX; + result.scope |= ScopeFlags.JSX; return markHTMLString(await renderToString(result, vnode.type as any, props, slots)); } case !vnode.type && (vnode.type as any) !== 0: diff --git a/packages/astro/src/runtime/server/render/head.ts b/packages/astro/src/runtime/server/render/head.ts index 6ca2be8b9e08..72be58623401 100644 --- a/packages/astro/src/runtime/server/render/head.ts +++ b/packages/astro/src/runtime/server/render/head.ts @@ -3,8 +3,6 @@ import type { SSRResult } from '../../../@types/astro'; import { markHTMLString } from '../escape.js'; import { renderElement, ScopeFlags } from './util.js'; -const AstroAndJSXScope = ScopeFlags.Astro | ScopeFlags.JSX; - // Filter out duplicate elements in our set const uniqueElements = (item: any, index: number, all: any[]) => { const props = JSON.stringify(item.props); @@ -56,8 +54,10 @@ export function* maybeRenderHead(result: SSRResult) { // Don't render the head inside of a JSX component that's inside of an Astro component // as the Astro component will be the one to render the head. - if(result.scope & AstroAndJSXScope) { - return; + switch(result.scope) { + case ScopeFlags.JSX | ScopeFlags.Slot | ScopeFlags.Astro: { + return; + } } // This is an instruction informing the page rendering that head might need rendering. diff --git a/packages/astro/src/runtime/server/render/slot.ts b/packages/astro/src/runtime/server/render/slot.ts index cd9225be42dc..1e2e946c3515 100644 --- a/packages/astro/src/runtime/server/render/slot.ts +++ b/packages/astro/src/runtime/server/render/slot.ts @@ -3,6 +3,7 @@ import type { RenderInstruction } from './types.js'; import { HTMLString, markHTMLString } from '../escape.js'; import { renderChild } from './any.js'; +import { ScopeFlags } from './util.js'; const slotString = Symbol.for('astro:slot-string'); @@ -20,8 +21,9 @@ export function isSlotString(str: string): str is any { return !!(str as any)[slotString]; } -export async function renderSlot(_result: any, slotted: string, fallback?: any): Promise { +export async function renderSlot(result: SSRResult, slotted: string, fallback?: any): Promise { if (slotted) { + result.scope |= ScopeFlags.Slot; let iterator = renderChild(slotted); let content = ''; let instructions: null | RenderInstruction[] = null; @@ -35,6 +37,8 @@ export async function renderSlot(_result: any, slotted: string, fallback?: any): content += chunk; } } + // Remove the flag since we are now outside of the scope. + result.scope &= ~ScopeFlags.Slot; return markHTMLString(new SlotString(content, instructions)); } return fallback; diff --git a/packages/astro/src/runtime/server/render/util.ts b/packages/astro/src/runtime/server/render/util.ts index cc8bd2c95f27..3986f9d50aa7 100644 --- a/packages/astro/src/runtime/server/render/util.ts +++ b/packages/astro/src/runtime/server/render/util.ts @@ -130,6 +130,7 @@ export function renderElement( } export const ScopeFlags = { - Astro: 1, - JSX: 2 + Astro: 1 << 0, + JSX: 1 << 1, + Slot: 1 << 2 };