From d1f5611febfd020cca4078c71bafe599015edd16 Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Tue, 7 Feb 2023 10:56:32 -0500 Subject: [PATCH] Add additional scoping for head buffering (#6152) * Add additional scoping for head buffering * Add test for direct usage of nested component * Add special scoping for Astro.scopes.render() * Generate propagation map during the build * Move to a maybeHead instruction * Properly serialize for SSR * More conservative scoping * Maybe had should honor result._metadata.hasRenderedHead * Properly type slots * Allow template result to be passed * Add changeset --- .changeset/pretty-bananas-own.md | 10 ++++ packages/astro/package.json | 2 +- packages/astro/src/content/internal.ts | 3 +- packages/astro/src/core/app/common.ts | 2 + packages/astro/src/core/app/index.ts | 1 + packages/astro/src/core/app/types.ts | 6 ++- packages/astro/src/core/build/generate.ts | 1 + packages/astro/src/core/build/internal.ts | 3 ++ .../astro/src/core/build/plugins/index.ts | 2 + .../src/core/build/plugins/plugin-ssr.ts | 1 + packages/astro/src/core/compile/compile.ts | 1 + packages/astro/src/core/render/result.ts | 19 +++---- packages/astro/src/runtime/server/index.ts | 7 ++- packages/astro/src/runtime/server/jsx.ts | 7 +-- .../runtime/server/render/astro/factory.ts | 6 +-- .../src/runtime/server/render/astro/index.ts | 2 +- .../runtime/server/render/astro/instance.ts | 18 +++---- .../astro/src/runtime/server/render/common.ts | 25 +++++++++ .../src/runtime/server/render/component.ts | 4 +- .../astro/src/runtime/server/render/head.ts | 13 ++--- .../astro/src/runtime/server/render/index.ts | 5 +- .../astro/src/runtime/server/render/page.ts | 4 +- .../astro/src/runtime/server/render/scope.ts | 32 +++++++++++ .../astro/src/runtime/server/render/slot.ts | 27 ++++++---- .../astro/src/runtime/server/render/types.ts | 8 ++- .../astro/src/runtime/server/render/util.ts | 6 --- .../src/vite-plugin-head-propagation/index.ts | 53 ++++++++++++++++++- .../mdx/test/css-head-mdx.test.js | 27 ++++++++++ .../test/fixtures/css-head-mdx/package.json | 10 ++++ .../css-head-mdx/src/components/P.astro | 3 ++ .../src/components/SmallCaps.astro | 3 ++ .../src/components/UsingMdx.astro | 8 +++ .../src/components/WithHoistedScripts.astro | 6 +++ .../css-head-mdx/src/content/blog/_styles.css | 3 ++ .../src/content/blog/using-mdx.mdx | 6 +++ .../css-head-mdx/src/content/posts/test.mdx | 5 ++ .../src/layouts/ContentLayout.astro | 24 +++++++++ .../src/pages/DirectContentUsage.astro | 17 ++++++ .../css-head-mdx/src/pages/posts/[post].astro | 18 +++++++ .../css-head-mdx/src/pages/remote.astro | 17 ++++++ .../css-head-mdx/src/styles/global.css | 3 ++ pnpm-lock.yaml | 36 +++++++++++-- 42 files changed, 386 insertions(+), 68 deletions(-) create mode 100644 .changeset/pretty-bananas-own.md create mode 100644 packages/astro/src/runtime/server/render/scope.ts create mode 100644 packages/integrations/mdx/test/fixtures/css-head-mdx/package.json create mode 100644 packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/P.astro create mode 100644 packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/SmallCaps.astro create mode 100644 packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/UsingMdx.astro create mode 100644 packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/WithHoistedScripts.astro create mode 100644 packages/integrations/mdx/test/fixtures/css-head-mdx/src/content/blog/_styles.css create mode 100644 packages/integrations/mdx/test/fixtures/css-head-mdx/src/content/blog/using-mdx.mdx create mode 100644 packages/integrations/mdx/test/fixtures/css-head-mdx/src/content/posts/test.mdx create mode 100644 packages/integrations/mdx/test/fixtures/css-head-mdx/src/layouts/ContentLayout.astro create mode 100644 packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/DirectContentUsage.astro create mode 100644 packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/posts/[post].astro create mode 100644 packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/remote.astro create mode 100644 packages/integrations/mdx/test/fixtures/css-head-mdx/src/styles/global.css diff --git a/.changeset/pretty-bananas-own.md b/.changeset/pretty-bananas-own.md new file mode 100644 index 000000000000..322ce96e24c8 --- /dev/null +++ b/.changeset/pretty-bananas-own.md @@ -0,0 +1,10 @@ +--- +'astro': patch +--- + +Fix MDX related head placement bugs + +This fixes a variety of head content placement bugs (such as page ``) related to MDX, especially when used in content collections. Issues fixed: + +- Head content being placed in the body instead of the head. +- Head content missing when rendering an MDX component from within a nested Astro component. diff --git a/packages/astro/package.json b/packages/astro/package.json index 9b189a73bd86..35b83d5a9341 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -99,7 +99,7 @@ "test:e2e:match": "playwright test -g" }, "dependencies": { - "@astrojs/compiler": "^1.0.1", + "@astrojs/compiler": "^1.1.0", "@astrojs/language-server": "^0.28.3", "@astrojs/markdown-remark": "^2.0.1", "@astrojs/telemetry": "^2.0.0", diff --git a/packages/astro/src/content/internal.ts b/packages/astro/src/content/internal.ts index 04280166feb3..ebfb971be44a 100644 --- a/packages/astro/src/content/internal.ts +++ b/packages/astro/src/content/internal.ts @@ -4,6 +4,7 @@ import { prependForwardSlash } from '../core/path.js'; import { createComponent, createHeadAndContent, + createScopedResult, renderComponent, renderScriptElement, renderStyleElement, @@ -169,7 +170,7 @@ async function render({ return createHeadAndContent( unescapeHTML(styles + links + scripts) as any, - renderTemplate`${renderComponent(result, 'Content', mod.Content, props, slots)}` + renderTemplate`${renderComponent(createScopedResult(result), 'Content', mod.Content, props, slots)}` ); }, propagation: 'self', diff --git a/packages/astro/src/core/app/common.ts b/packages/astro/src/core/app/common.ts index 2da98f1c6022..4b14321762af 100644 --- a/packages/astro/src/core/app/common.ts +++ b/packages/astro/src/core/app/common.ts @@ -14,10 +14,12 @@ export function deserializeManifest(serializedManifest: SerializedSSRManifest): } const assets = new Set(serializedManifest.assets); + const propagation = new Map(serializedManifest.propagation); return { ...serializedManifest, assets, + propagation, routes, }; } diff --git a/packages/astro/src/core/app/index.ts b/packages/astro/src/core/app/index.ts index 529a450f9531..615be50aa585 100644 --- a/packages/astro/src/core/app/index.ts +++ b/packages/astro/src/core/app/index.ts @@ -193,6 +193,7 @@ export class App { request, origin: url.origin, pathname, + propagation: this.#manifest.propagation, scripts, links, route: routeData, diff --git a/packages/astro/src/core/app/types.ts b/packages/astro/src/core/app/types.ts index 587c924b92de..cf85d0387bce 100644 --- a/packages/astro/src/core/app/types.ts +++ b/packages/astro/src/core/app/types.ts @@ -1,9 +1,11 @@ import type { MarkdownRenderingOptions } from '@astrojs/markdown-remark'; import type { ComponentInstance, + PropagationHint, RouteData, SerializedRouteData, SSRLoadedRenderer, + SSRResult, } from '../../@types/astro'; export type ComponentPath = string; @@ -34,11 +36,13 @@ export interface SSRManifest { renderers: SSRLoadedRenderer[]; entryModules: Record; assets: Set; + propagation: SSRResult['propagation']; } -export type SerializedSSRManifest = Omit & { +export type SerializedSSRManifest = Omit & { routes: SerializedRouteInfo[]; assets: string[]; + propagation: readonly [string, PropagationHint][]; }; export type AdapterCreateExports = ( diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts index 1a708aab3765..f5f72dc5ab93 100644 --- a/packages/astro/src/core/build/generate.ts +++ b/packages/astro/src/core/build/generate.ts @@ -376,6 +376,7 @@ async function generatePath( origin, pathname, request: createRequest({ url, headers: new Headers(), logging, ssr }), + propagation: internals.propagation, scripts, links, route: pageData.route, diff --git a/packages/astro/src/core/build/internal.ts b/packages/astro/src/core/build/internal.ts index dc660f4949e0..87c817ad4bca 100644 --- a/packages/astro/src/core/build/internal.ts +++ b/packages/astro/src/core/build/internal.ts @@ -4,6 +4,7 @@ import type { PageBuildData, ViteID } from './types'; import { PageOptions } from '../../vite-plugin-astro/types'; import { prependForwardSlash, removeFileExtension } from '../path.js'; import { viteID } from '../util.js'; +import { SSRResult } from '../../@types/astro'; export interface BuildInternals { /** @@ -66,6 +67,7 @@ export interface BuildInternals { staticFiles: Set; // The SSR entry chunk. Kept in internals to share between ssr/client build steps ssrEntryChunk?: OutputChunk; + propagation: SSRResult['propagation']; } /** @@ -95,6 +97,7 @@ export function createBuildInternals(): BuildInternals { discoveredClientOnlyComponents: new Set(), discoveredScripts: new Set(), staticFiles: new Set(), + propagation: new Map(), }; } diff --git a/packages/astro/src/core/build/plugins/index.ts b/packages/astro/src/core/build/plugins/index.ts index cf0f9bf7c66d..9aa3e20b5801 100644 --- a/packages/astro/src/core/build/plugins/index.ts +++ b/packages/astro/src/core/build/plugins/index.ts @@ -8,6 +8,7 @@ import { pluginInternals } from './plugin-internals.js'; import { pluginPages } from './plugin-pages.js'; import { pluginPrerender } from './plugin-prerender.js'; import { pluginSSR } from './plugin-ssr.js'; +import { astroHeadPropagationBuildPlugin } from '../../../vite-plugin-head-propagation/index.js'; export function registerAllPlugins({ internals, options, register }: AstroBuildPluginContainer) { register(pluginAliasResolve(internals)); @@ -15,6 +16,7 @@ export function registerAllPlugins({ internals, options, register }: AstroBuildP register(pluginInternals(internals)); register(pluginPages(options, internals)); register(pluginCSS(options, internals)); + register(astroHeadPropagationBuildPlugin(options, internals)); register(pluginPrerender(options, internals)); register(astroConfigBuildPlugin(options, internals)); register(pluginHoistedScripts(options, internals)); diff --git a/packages/astro/src/core/build/plugins/plugin-ssr.ts b/packages/astro/src/core/build/plugins/plugin-ssr.ts index cfc58d71b345..d4536b92c8ab 100644 --- a/packages/astro/src/core/build/plugins/plugin-ssr.ts +++ b/packages/astro/src/core/build/plugins/plugin-ssr.ts @@ -211,6 +211,7 @@ function buildManifest( contentDir: getContentPaths(settings.config).contentDir, }, pageMap: null as any, + propagation: Array.from(internals.propagation), renderers: [], entryModules, assets: staticFiles.map((s) => settings.config.base + s), diff --git a/packages/astro/src/core/compile/compile.ts b/packages/astro/src/core/compile/compile.ts index 7e8dc8a31927..a0199a2fb703 100644 --- a/packages/astro/src/core/compile/compile.ts +++ b/packages/astro/src/core/compile/compile.ts @@ -42,6 +42,7 @@ export async function compile({ sourcemap: 'both', internalURL: 'astro/server/index.js', astroGlobalArgs: JSON.stringify(astroConfig.site), + resultScopedSlot: true, preprocessStyle: createStylePreprocessor({ filename, viteConfig, diff --git a/packages/astro/src/core/render/result.ts b/packages/astro/src/core/render/result.ts index 3a6b0bf964d6..e236b35a4bc5 100644 --- a/packages/astro/src/core/render/result.ts +++ b/packages/astro/src/core/render/result.ts @@ -9,7 +9,7 @@ import type { SSRLoadedRenderer, SSRResult, } from '../../@types/astro'; -import { renderSlot, stringifyChunk } from '../../runtime/server/index.js'; +import { renderSlot, stringifyChunk, ScopeFlags, createScopedResult, ComponentSlots } from '../../runtime/server/index.js'; import { renderJSX } from '../../runtime/server/jsx.js'; import { AstroCookies } from '../cookies/index.js'; import { AstroError, AstroErrorData } from '../errors/index.js'; @@ -55,10 +55,10 @@ function getFunctionExpression(slot: any) { class Slots { #result: SSRResult; - #slots: Record | null; + #slots: ComponentSlots | null; #loggingOpts: LogOptions; - constructor(result: SSRResult, slots: Record | null, logging: LogOptions) { + constructor(result: SSRResult, slots: ComponentSlots | null, logging: LogOptions) { this.#result = result; this.#slots = slots; this.#loggingOpts = logging; @@ -89,6 +89,7 @@ class Slots { public async render(name: string, args: any[] = []) { if (!this.#slots || !this.has(name)) return; + const scoped = createScopedResult(this.#result, ScopeFlags.RenderSlot); if (!Array.isArray(args)) { warn( this.#loggingOpts, @@ -97,26 +98,26 @@ class Slots { ); } else if (args.length > 0) { const slotValue = this.#slots[name]; - const component = typeof slotValue === 'function' ? await slotValue() : await slotValue; + const component = typeof slotValue === 'function' ? await slotValue(scoped) : await slotValue; // Astro const expression = getFunctionExpression(component); if (expression) { - const slot = expression(...args); - return await renderSlot(this.#result, slot).then((res) => + const slot = () => expression(...args); + return await renderSlot(scoped, slot).then((res) => res != null ? String(res) : res ); } // JSX if (typeof component === 'function') { - return await renderJSX(this.#result, component(...args)).then((res) => + return await renderJSX(scoped, (component as any)(...args)).then((res) => res != null ? String(res) : res ); } } - const content = await renderSlot(this.#result, this.#slots[name]); - const outHTML = stringifyChunk(this.#result, content); + const content = await renderSlot(scoped, this.#slots[name]); + const outHTML = stringifyChunk(scoped, content); return outHTML; } diff --git a/packages/astro/src/runtime/server/index.ts b/packages/astro/src/runtime/server/index.ts index ccdfd5e1968f..aa9da8696b00 100644 --- a/packages/astro/src/runtime/server/index.ts +++ b/packages/astro/src/runtime/server/index.ts @@ -5,10 +5,13 @@ export { escapeHTML, HTMLBytes, HTMLString, markHTMLString, unescapeHTML } from export { renderJSX } from './jsx.js'; export { addAttribute, + addScopeFlag, createHeadAndContent, + createScopedResult, defineScriptVars, Fragment, maybeRenderHead, + removeScopeFlag, renderAstroTemplateResult as renderAstroComponent, renderComponent, renderComponentToIterable, @@ -23,15 +26,15 @@ export { renderTemplate, renderToString, renderUniqueStylesheet, + ScopeFlags, stringifyChunk, voidElementNames, } from './render/index.js'; export type { AstroComponentFactory, AstroComponentInstance, - AstroComponentSlots, - AstroComponentSlotsWithValues, RenderInstruction, + ComponentSlots } from './render/index.js'; import { markHTMLString } from './escape.js'; diff --git a/packages/astro/src/runtime/server/jsx.ts b/packages/astro/src/runtime/server/jsx.ts index 8b81a641ea35..665e4afec8fb 100644 --- a/packages/astro/src/runtime/server/jsx.ts +++ b/packages/astro/src/runtime/server/jsx.ts @@ -12,7 +12,7 @@ import { } from './index.js'; import { HTMLParts } from './render/common.js'; import type { ComponentIterable } from './render/component'; -import { ScopeFlags } from './render/util.js'; +import { createScopedResult, ScopeFlags } from './render/scope.js'; const ClientOnlyPlaceholder = 'astro-client-only'; @@ -95,8 +95,9 @@ 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)); + const scoped = createScopedResult(result, ScopeFlags.JSX); + const html = markHTMLString(await renderToString(scoped, vnode.type as any, props, slots)); + return html; } case !vnode.type && (vnode.type as any) !== 0: return ''; diff --git a/packages/astro/src/runtime/server/render/astro/factory.ts b/packages/astro/src/runtime/server/render/astro/factory.ts index 1f85fe45c72f..7f5b912472ef 100644 --- a/packages/astro/src/runtime/server/render/astro/factory.ts +++ b/packages/astro/src/runtime/server/render/astro/factory.ts @@ -3,7 +3,7 @@ import type { HeadAndContent } from './head-and-content'; import type { RenderTemplateResult } from './render-template'; import { HTMLParts } from '../common.js'; -import { ScopeFlags } from '../util.js'; +import { addScopeFlag, createScopedResult, ScopeFlags } from '../scope.js'; import { isHeadAndContent } from './head-and-content.js'; import { renderAstroTemplateResult } from './render-template.js'; @@ -28,8 +28,8 @@ export async function renderToString( props: any, children: any ): Promise { - result.scope |= ScopeFlags.Astro; - const factoryResult = await componentFactory(result, props, children); + const scoped = createScopedResult(result, ScopeFlags.Astro); + const factoryResult = await componentFactory(scoped, props, children); if (factoryResult instanceof Response) { const response = factoryResult; diff --git a/packages/astro/src/runtime/server/render/astro/index.ts b/packages/astro/src/runtime/server/render/astro/index.ts index 03d97b14875c..cbddf7876e8e 100644 --- a/packages/astro/src/runtime/server/render/astro/index.ts +++ b/packages/astro/src/runtime/server/render/astro/index.ts @@ -1,7 +1,7 @@ export type { AstroComponentFactory } from './factory'; export { isAstroComponentFactory, renderToString } from './factory.js'; export { createHeadAndContent, isHeadAndContent } from './head-and-content.js'; -export type { AstroComponentInstance, ComponentSlots, ComponentSlotsWithValues } from './instance'; +export type { AstroComponentInstance } from './instance'; export { createAstroComponentInstance, isAstroComponentInstance } from './instance.js'; export { isRenderTemplateResult, diff --git a/packages/astro/src/runtime/server/render/astro/instance.ts b/packages/astro/src/runtime/server/render/astro/instance.ts index d290abcc1640..171cc1a546de 100644 --- a/packages/astro/src/runtime/server/render/astro/instance.ts +++ b/packages/astro/src/runtime/server/render/astro/instance.ts @@ -1,17 +1,15 @@ import type { SSRResult } from '../../../../@types/astro'; import type { AstroComponentFactory, AstroFactoryReturnValue } from './factory.js'; -import type { renderTemplate } from './render-template.js'; +import type { ComponentSlots } from '../slot.js'; import { HydrationDirectiveProps } from '../../hydration.js'; import { isPromise } from '../../util.js'; import { renderChild } from '../any.js'; import { isAPropagatingComponent } from './factory.js'; import { isHeadAndContent } from './head-and-content.js'; +import { createScopedResult, ScopeFlags } from '../scope.js'; type ComponentProps = Record; -type ComponentSlotValue = () => ReturnType; -export type ComponentSlots = Record; -export type ComponentSlotsWithValues = Record>; const astroComponentInstanceSym = Symbol.for('astro.componentInstance'); @@ -20,7 +18,7 @@ export class AstroComponentInstance { private readonly result: SSRResult; private readonly props: ComponentProps; - private readonly slotValues: ComponentSlotsWithValues; + private readonly slotValues: ComponentSlots; private readonly factory: AstroComponentFactory; private returnValue: ReturnType | undefined; constructor( @@ -33,19 +31,21 @@ export class AstroComponentInstance { this.props = props; this.factory = factory; this.slotValues = {}; + const scoped = createScopedResult(result, ScopeFlags.Slot); for (const name in slots) { - this.slotValues[name] = slots[name](); + const value = slots[name](scoped); + this.slotValues[name] = () => value; } } - async init() { - this.returnValue = this.factory(this.result, this.props, this.slotValues); + async init(result: SSRResult) { + this.returnValue = this.factory(result, this.props, this.slotValues); return this.returnValue; } async *render() { if (this.returnValue === undefined) { - await this.init(); + await this.init(this.result); } let value: AstroFactoryReturnValue | undefined = this.returnValue; diff --git a/packages/astro/src/runtime/server/render/common.ts b/packages/astro/src/runtime/server/render/common.ts index def7dedd37e0..5283eaa12b14 100644 --- a/packages/astro/src/runtime/server/render/common.ts +++ b/packages/astro/src/runtime/server/render/common.ts @@ -10,6 +10,7 @@ import { } from '../scripts.js'; import { renderAllHeadContent } from './head.js'; import { isSlotString, type SlotString } from './slot.js'; +import { ScopeFlags } from './scope.js'; export const Fragment = Symbol.for('astro:fragment'); export const Renderer = Symbol.for('astro:renderer'); @@ -48,6 +49,30 @@ export function stringifyChunk(result: SSRResult, chunk: string | SlotString | R } return renderAllHeadContent(result); } + case 'maybe-head': { + if (result._metadata.hasRenderedHead) { + return ''; + } + + const scope = instruction.scope; + switch (scope) { + // JSX with an Astro slot + case ScopeFlags.JSX | ScopeFlags.Slot | ScopeFlags.Astro: + case ScopeFlags.JSX | ScopeFlags.Astro | ScopeFlags.HeadBuffer: + case ScopeFlags.JSX | ScopeFlags.Slot | ScopeFlags.Astro | ScopeFlags.HeadBuffer: { + return ''; + } + + // Astro.slots.render('default') should never render head content. + case ScopeFlags.RenderSlot | ScopeFlags.Astro: + case ScopeFlags.RenderSlot | ScopeFlags.Astro | ScopeFlags.JSX: + case ScopeFlags.RenderSlot | ScopeFlags.Astro | ScopeFlags.JSX | ScopeFlags.HeadBuffer: { + return ''; + } + } + + return renderAllHeadContent(result); + } } } else { if (isSlotString(chunk as string)) { diff --git a/packages/astro/src/runtime/server/render/component.ts b/packages/astro/src/runtime/server/render/component.ts index 432f45c75dca..f3428a754201 100644 --- a/packages/astro/src/runtime/server/render/component.ts +++ b/packages/astro/src/runtime/server/render/component.ts @@ -17,7 +17,7 @@ import { } from './astro/index.js'; import { Fragment, Renderer, stringifyChunk } from './common.js'; import { componentIsHTMLElement, renderHTMLElement } from './dom.js'; -import { renderSlot, renderSlots } from './slot.js'; +import { ComponentSlots, renderSlot, renderSlots } from './slot.js'; import { formatList, internalSpreadAttributes, renderElement, voidElementNames } from './util.js'; const rendererAliases = new Map([['solid', 'solid-js']]); @@ -331,7 +331,7 @@ function sanitizeElementName(tag: string) { return tag.trim().split(unsafe)[0].trim(); } -async function renderFragmentComponent(result: SSRResult, slots: any = {}) { +async function renderFragmentComponent(result: SSRResult, slots: ComponentSlots = {}) { const children = await renderSlot(result, slots?.default); if (children == null) { return children; diff --git a/packages/astro/src/runtime/server/render/head.ts b/packages/astro/src/runtime/server/render/head.ts index 0f39fe2192a8..4bd072ee7918 100644 --- a/packages/astro/src/runtime/server/render/head.ts +++ b/packages/astro/src/runtime/server/render/head.ts @@ -1,7 +1,8 @@ import type { SSRResult } from '../../../@types/astro'; import { markHTMLString } from '../escape.js'; -import { renderElement, ScopeFlags } from './util.js'; +import { renderElement } from './util.js'; +import { ScopeFlags } from './scope.js'; // Filter out duplicate elements in our set const uniqueElements = (item: any, index: number, all: any[]) => { @@ -52,15 +53,7 @@ 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. - switch (result.scope) { - case ScopeFlags.JSX | ScopeFlags.Slot | ScopeFlags.Astro: { - 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; + yield { type: 'maybe-head', result, scope: result.scope } as const; } diff --git a/packages/astro/src/runtime/server/render/index.ts b/packages/astro/src/runtime/server/render/index.ts index 012b421f5784..092feb86b258 100644 --- a/packages/astro/src/runtime/server/render/index.ts +++ b/packages/astro/src/runtime/server/render/index.ts @@ -1,8 +1,6 @@ export type { AstroComponentFactory, AstroComponentInstance, - ComponentSlots as AstroComponentSlots, - ComponentSlotsWithValues as AstroComponentSlotsWithValues, } from './astro/index'; export { createHeadAndContent, @@ -15,7 +13,8 @@ export { renderComponent, renderComponentToIterable } from './component.js'; export { renderHTMLElement } from './dom.js'; export { maybeRenderHead, renderHead } from './head.js'; export { renderPage } from './page.js'; -export { renderSlot } from './slot.js'; +export { renderSlot, type ComponentSlots } from './slot.js'; +export { createScopedResult, ScopeFlags, addScopeFlag, removeScopeFlag } from './scope.js'; export { renderScriptElement, renderStyleElement, renderUniqueStylesheet } from './tags.js'; export type { RenderInstruction } from './types'; export { addAttribute, defineScriptVars, voidElementNames } from './util.js'; diff --git a/packages/astro/src/runtime/server/render/page.ts b/packages/astro/src/runtime/server/render/page.ts index b949019d6967..a9c1af77bb9c 100644 --- a/packages/astro/src/runtime/server/render/page.ts +++ b/packages/astro/src/runtime/server/render/page.ts @@ -15,6 +15,7 @@ import { import { chunkToByteArray, encoder, HTMLParts } from './common.js'; import { renderComponent } from './component.js'; import { maybeRenderHead } from './head.js'; +import { addScopeFlag, createScopedResult, removeScopeFlag, ScopeFlags } from './scope.js'; const needsHeadRenderingSymbol = Symbol.for('astro.needsHeadRendering'); @@ -55,12 +56,13 @@ async function iterableToHTMLBytes( // to be propagated up. async function bufferHeadContent(result: SSRResult) { const iterator = result.propagators.values(); + const scoped = createScopedResult(result, ScopeFlags.HeadBuffer); while (true) { const { value, done } = iterator.next(); if (done) { break; } - const returnValue = await value.init(); + const returnValue = await value.init(scoped); if (isHeadAndContent(returnValue)) { result.extraHead.push(returnValue.head); } diff --git a/packages/astro/src/runtime/server/render/scope.ts b/packages/astro/src/runtime/server/render/scope.ts new file mode 100644 index 000000000000..fb40d40f1675 --- /dev/null +++ b/packages/astro/src/runtime/server/render/scope.ts @@ -0,0 +1,32 @@ +import type { SSRResult } from '../../../@types/astro'; + +export const ScopeFlags = { + Astro: 1 << 0, // 1 + JSX: 1 << 1, // 2 + Slot: 1 << 2, // 4 + HeadBuffer: 1 << 3, // 8 + RenderSlot: 1 << 4, // 16 +} as const; + +type ScopeFlagValues = (typeof ScopeFlags)[keyof typeof ScopeFlags]; + +export function addScopeFlag(result: SSRResult, flag: ScopeFlagValues) { + result.scope |= flag; +} + +export function removeScopeFlag(result: SSRResult, flag: ScopeFlagValues) { + result.scope &= ~flag; +} + +export function createScopedResult(result: SSRResult, flag?: ScopeFlagValues): SSRResult { + const scopedResult = Object.create(result, { + scope: { + writable: true, + value: result.scope + } + }); + if(flag != null) { + addScopeFlag(scopedResult, flag); + } + return scopedResult; +} diff --git a/packages/astro/src/runtime/server/render/slot.ts b/packages/astro/src/runtime/server/render/slot.ts index 32d0a2dc1299..aac133e6ef6f 100644 --- a/packages/astro/src/runtime/server/render/slot.ts +++ b/packages/astro/src/runtime/server/render/slot.ts @@ -1,9 +1,14 @@ import type { SSRResult } from '../../../@types/astro.js'; import type { RenderInstruction } from './types.js'; +import type { renderTemplate } from './astro/render-template.js'; import { HTMLString, markHTMLString } from '../escape.js'; import { renderChild } from './any.js'; -import { ScopeFlags } from './util.js'; +import { ScopeFlags, createScopedResult } from './scope.js'; + +type RenderTemplateResult = ReturnType; +export type ComponentSlots = Record; +export type ComponentSlotValue = (result: SSRResult) => RenderTemplateResult; const slotString = Symbol.for('astro:slot-string'); @@ -23,12 +28,12 @@ export function isSlotString(str: string): str is any { export async function renderSlot( result: SSRResult, - slotted: string, - fallback?: any + slotted: ComponentSlotValue | RenderTemplateResult, + fallback?: ComponentSlotValue | RenderTemplateResult ): Promise { if (slotted) { - result.scope |= ScopeFlags.Slot; - let iterator = renderChild(slotted); + const scoped = createScopedResult(result, ScopeFlags.Slot); + let iterator = renderChild(typeof slotted === 'function' ? slotted(scoped) : slotted); let content = ''; let instructions: null | RenderInstruction[] = null; for await (const chunk of iterator) { @@ -41,11 +46,13 @@ export async function renderSlot( 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; + + if(fallback) { + return renderSlot(result, fallback); + } + return ''; } interface RenderSlotsResult { @@ -53,13 +60,13 @@ interface RenderSlotsResult { children: Record; } -export async function renderSlots(result: SSRResult, slots: any = {}): Promise { +export async function renderSlots(result: SSRResult, slots: ComponentSlots = {}): Promise { let slotInstructions: RenderSlotsResult['slotInstructions'] = null; let children: RenderSlotsResult['children'] = {}; if (slots) { await Promise.all( Object.entries(slots).map(([key, value]) => - renderSlot(result, value as string).then((output: any) => { + renderSlot(result, value).then((output: any) => { if (output.instructions) { if (slotInstructions === null) { slotInstructions = []; diff --git a/packages/astro/src/runtime/server/render/types.ts b/packages/astro/src/runtime/server/render/types.ts index 3aa3a16e3cef..a16ec034b32d 100644 --- a/packages/astro/src/runtime/server/render/types.ts +++ b/packages/astro/src/runtime/server/render/types.ts @@ -12,4 +12,10 @@ export type RenderHeadInstruction = { result: SSRResult; }; -export type RenderInstruction = RenderDirectiveInstruction | RenderHeadInstruction; +export type MaybeRenderHeadInstruction = { + type: 'maybe-head'; + result: SSRResult; + scope: number; +} + +export type RenderInstruction = RenderDirectiveInstruction | RenderHeadInstruction | MaybeRenderHeadInstruction; diff --git a/packages/astro/src/runtime/server/render/util.ts b/packages/astro/src/runtime/server/render/util.ts index 7e5ca9a5d160..a95ef16f8722 100644 --- a/packages/astro/src/runtime/server/render/util.ts +++ b/packages/astro/src/runtime/server/render/util.ts @@ -128,9 +128,3 @@ export function renderElement( } return `<${name}${internalSpreadAttributes(props, shouldEscape)}>${children}`; } - -export const ScopeFlags = { - Astro: 1 << 0, - JSX: 1 << 1, - Slot: 1 << 2, -}; diff --git a/packages/astro/src/vite-plugin-head-propagation/index.ts b/packages/astro/src/vite-plugin-head-propagation/index.ts index ae8075e97e4f..eedefafd9ae9 100644 --- a/packages/astro/src/vite-plugin-head-propagation/index.ts +++ b/packages/astro/src/vite-plugin-head-propagation/index.ts @@ -1,8 +1,12 @@ import type { ModuleInfo } from 'rollup'; -import type { AstroSettings } from '../@types/astro'; +import type { AstroSettings, SSRResult } from '../@types/astro'; +import type { BuildInternals } from '../core/build/internal.js'; +import type { AstroBuildPlugin } from '../core/build/plugin.js'; +import type { StaticBuildOptions } from '../core/build/types'; import * as vite from 'vite'; import { getAstroMetadata } from '../vite-plugin-astro/index.js'; +import { walkParentInfos } from '../core/build/graph.js'; const injectExp = /^\/\/\s*astro-head-inject/; /** @@ -59,3 +63,50 @@ export default function configHeadPropagationVitePlugin({ }, }; } + +export function astroHeadPropagationBuildPlugin( + options: StaticBuildOptions, + internals: BuildInternals +): AstroBuildPlugin { + return { + build: 'ssr', + hooks: { + 'build:before'() { + const map: SSRResult['propagation'] = new Map(); + return { + vitePlugin: { + name: 'vite-plugin-head-propagation-build', + generateBundle(_opts, bundle) { + const appendPropagation = (info: ModuleInfo) => { + const astroMetadata = getAstroMetadata(info); + if(astroMetadata) { + astroMetadata.propagation = 'in-tree'; + map.set(info.id, 'in-tree'); + } + }; + + for(const [bundleId, output] of Object.entries(bundle)) { + if(output.type !== 'chunk') continue; + for(const [id, mod] of Object.entries(output.modules)) { + if (mod.code && injectExp.test(mod.code)) { + for(const [info] of walkParentInfos(id, this)) { + appendPropagation(info); + } + } + + const info = this.getModuleInfo(id); + if(info) { + appendPropagation(info); + } + } + } + + // Save the map to internals so it can be passed into SSR and generation + internals.propagation = map; + } + } + } + } + } + } +} diff --git a/packages/integrations/mdx/test/css-head-mdx.test.js b/packages/integrations/mdx/test/css-head-mdx.test.js index 2b1dcdfe7bcd..c38f23701d34 100644 --- a/packages/integrations/mdx/test/css-head-mdx.test.js +++ b/packages/integrations/mdx/test/css-head-mdx.test.js @@ -29,5 +29,32 @@ describe('Head injection w/ MDX', () => { const scripts = document.querySelectorAll('head script[type=module]'); expect(scripts).to.have.a.lengthOf(1); }); + + it('injects into the head for content collections', async () => { + const html = await fixture.readFile('/posts/test/index.html'); + const { document } = parseHTML(html); + + const links = document.querySelectorAll('head link[rel=stylesheet]'); + expect(links).to.have.a.lengthOf(1); + }); + + it('injects content from a component using Content#render()', async () => { + const html = await fixture.readFile('/DirectContentUsage/index.html'); + const { document } = parseHTML(html); + + const links = document.querySelectorAll('head link[rel=stylesheet]'); + expect(links).to.have.a.lengthOf(1); + + const scripts = document.querySelectorAll('head script[type=module]'); + expect(scripts).to.have.a.lengthOf(2); + }); + + it('Using component using slots.render() API', async () => { + const html = await fixture.readFile('/remote/index.html'); + const { document } = parseHTML(html); + + const links = document.querySelectorAll('head link[rel=stylesheet]'); + expect(links).to.have.a.lengthOf(1); + }); }); }); diff --git a/packages/integrations/mdx/test/fixtures/css-head-mdx/package.json b/packages/integrations/mdx/test/fixtures/css-head-mdx/package.json new file mode 100644 index 000000000000..3c3c1e5a572d --- /dev/null +++ b/packages/integrations/mdx/test/fixtures/css-head-mdx/package.json @@ -0,0 +1,10 @@ +{ + "name": "@test/mdx-css-head-mdx", + "version": "0.0.0", + "private": true, + "dependencies": { + "astro": "workspace:*", + "@astrojs/mdx": "workspace:*", + "astro-remote": "0.2.3" + } +} diff --git a/packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/P.astro b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/P.astro new file mode 100644 index 000000000000..071e08a12d42 --- /dev/null +++ b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/P.astro @@ -0,0 +1,3 @@ +

+ +

diff --git a/packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/SmallCaps.astro b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/SmallCaps.astro new file mode 100644 index 000000000000..a0bd6e1f1501 --- /dev/null +++ b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/SmallCaps.astro @@ -0,0 +1,3 @@ +--- +--- + diff --git a/packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/UsingMdx.astro b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/UsingMdx.astro new file mode 100644 index 000000000000..1804388b0524 --- /dev/null +++ b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/UsingMdx.astro @@ -0,0 +1,8 @@ +--- +import { getEntryBySlug } from 'astro:content'; + +const launchWeek = await getEntryBySlug('blog', 'using-mdx'); +const { Content } = await launchWeek.render(); +--- + + diff --git a/packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/WithHoistedScripts.astro b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/WithHoistedScripts.astro new file mode 100644 index 000000000000..0b8c4445f6fd --- /dev/null +++ b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/WithHoistedScripts.astro @@ -0,0 +1,6 @@ +--- +--- + + diff --git a/packages/integrations/mdx/test/fixtures/css-head-mdx/src/content/blog/_styles.css b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/content/blog/_styles.css new file mode 100644 index 000000000000..1379b29c06f0 --- /dev/null +++ b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/content/blog/_styles.css @@ -0,0 +1,3 @@ +body { + color: red !important; +} diff --git a/packages/integrations/mdx/test/fixtures/css-head-mdx/src/content/blog/using-mdx.mdx b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/content/blog/using-mdx.mdx new file mode 100644 index 000000000000..917fc3331288 --- /dev/null +++ b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/content/blog/using-mdx.mdx @@ -0,0 +1,6 @@ +import './_styles.css'; +import WithHoistedScripts from '../../components/WithHoistedScripts.astro'; + +# Using mdx + + diff --git a/packages/integrations/mdx/test/fixtures/css-head-mdx/src/content/posts/test.mdx b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/content/posts/test.mdx new file mode 100644 index 000000000000..0bb1153cab7c --- /dev/null +++ b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/content/posts/test.mdx @@ -0,0 +1,5 @@ +--- +title: Testing +--- + +A test file diff --git a/packages/integrations/mdx/test/fixtures/css-head-mdx/src/layouts/ContentLayout.astro b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/layouts/ContentLayout.astro new file mode 100644 index 000000000000..7b234e86859a --- /dev/null +++ b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/layouts/ContentLayout.astro @@ -0,0 +1,24 @@ +--- +export interface Props { + title: string; +} + +const { title } = Astro.props; +--- + + + + + + + + + {title} + + + + + + diff --git a/packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/DirectContentUsage.astro b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/DirectContentUsage.astro new file mode 100644 index 000000000000..cbf4295a7149 --- /dev/null +++ b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/DirectContentUsage.astro @@ -0,0 +1,17 @@ +--- +import UsingMdx from '../components/UsingMdx.astro' +--- + + + + + + + + Astro + + +

Astro

+ + + diff --git a/packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/posts/[post].astro b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/posts/[post].astro new file mode 100644 index 000000000000..7d6ca0ca4d37 --- /dev/null +++ b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/posts/[post].astro @@ -0,0 +1,18 @@ +--- +import { getCollection } from 'astro:content'; +import Layout from '../../layouts/ContentLayout.astro'; +import SmallCaps from '../../components/SmallCaps.astro'; + +export async function getStaticPaths() { + const entries = await getCollection('posts'); + return entries.map(entry => { + return {params: { post: entry.slug }, props: { entry }, + }}); +} + +const { entry } = Astro.props; +const { Content } = await entry.render(); +--- + + + diff --git a/packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/remote.astro b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/remote.astro new file mode 100644 index 000000000000..9a7b76a10283 --- /dev/null +++ b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/remote.astro @@ -0,0 +1,17 @@ +--- +import '../styles/global.css' +import Layout from '../layouts/One.astro'; +import Paragraph from '../components/P.astro'; +import { Markdown } from 'astro-remote' +--- + + +
+ + **Removing p component fixes the problem** + +
+
diff --git a/packages/integrations/mdx/test/fixtures/css-head-mdx/src/styles/global.css b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/styles/global.css new file mode 100644 index 000000000000..e1450526f7a2 --- /dev/null +++ b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/styles/global.css @@ -0,0 +1,3 @@ +html { + font-weight: bolder; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7577d37e1d5c..0c2ff98dc10f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -375,7 +375,7 @@ importers: packages/astro: specifiers: - '@astrojs/compiler': ^1.0.1 + '@astrojs/compiler': ^1.1.0 '@astrojs/language-server': ^0.28.3 '@astrojs/markdown-remark': ^2.0.1 '@astrojs/telemetry': ^2.0.0 @@ -465,7 +465,7 @@ importers: yargs-parser: ^21.0.1 zod: ^3.17.3 dependencies: - '@astrojs/compiler': 1.0.1 + '@astrojs/compiler': 1.1.0 '@astrojs/language-server': 0.28.3 '@astrojs/markdown-remark': link:../markdown/remark '@astrojs/telemetry': link:../telemetry @@ -2924,6 +2924,16 @@ importers: remark-toc: 8.0.1 vite: 4.1.1 + packages/integrations/mdx/test/fixtures/css-head-mdx: + specifiers: + '@astrojs/mdx': workspace:* + astro: workspace:* + astro-remote: 0.2.3 + dependencies: + '@astrojs/mdx': link:../../.. + astro: link:../../../../../astro + astro-remote: 0.2.3 + packages/integrations/mdx/test/fixtures/mdx-frontmatter-injection: specifiers: '@astrojs/mdx': workspace:* @@ -3863,8 +3873,8 @@ packages: /@astrojs/compiler/0.31.4: resolution: {integrity: sha512-6bBFeDTtPOn4jZaiD3p0f05MEGQL9pw2Zbfj546oFETNmjJFWO3nzHz6/m+P53calknCvyVzZ5YhoBLIvzn5iw==} - /@astrojs/compiler/1.0.1: - resolution: {integrity: sha512-77aacobLKcL98NmhK3OBS5EHIrX9gs1ckB/vGSIdkVZuB7u51V4jh05I6W0tSvG7/86tALv6QtHTRZ8rLhFTbQ==} + /@astrojs/compiler/1.1.0: + resolution: {integrity: sha512-C4kTwirys+HafufMqaxCbML2wqkGaXJM+5AekXh/v1IIOnMIdcEON9GBYsG6qa8aAmLhZ58aUZGPhzcA3Dx7Uw==} dev: false /@astrojs/language-server/0.28.3: @@ -7843,6 +7853,14 @@ packages: astro: link:packages/astro dev: false + /astro-remote/0.2.3: + resolution: {integrity: sha512-vsY736YjWhpFgx4KUxCBdK0QJmOk0W61VQwO7v6qmfGdIxZyx6N7hBNou57w2mw68hQSe5AbRs602pi05GDMHw==} + dependencies: + he: 1.2.0 + marked: 4.2.12 + ultrahtml: 0.1.3 + dev: false + /async-sema/3.1.1: resolution: {integrity: sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg==} dev: false @@ -11285,6 +11303,12 @@ packages: resolution: {integrity: sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw==} dev: false + /marked/4.2.12: + resolution: {integrity: sha512-yr8hSKa3Fv4D3jdZmtMMPghgVt6TWbk86WQaWhDloQjRSQhMMYCAro7jP7VDJrjjdV8pxVxMssXS8B8Y5DZ5aw==} + engines: {node: '>= 12'} + hasBin: true + dev: false + /matcher/3.0.0: resolution: {integrity: sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==} engines: {node: '>=10'} @@ -14742,6 +14766,10 @@ packages: resolution: {integrity: sha512-o0QVGuFg24FK765Qdd5kk0zU/U4dEsCtN/GSiwNI9i8xsSVtjIAOdTaVhLwZ1nrbWxFVMxNDDl+9fednsOMsBw==} dev: true + /ultrahtml/0.1.3: + resolution: {integrity: sha512-P24ulZdT9UKyQuKA1IApdAZ+F9lwruGvmKb4pG3+sMvR3CjN0pjawPnxuSABHQFB+XqnB35TVXzJPOBYjCv6Kw==} + dev: false + /unbox-primitive/1.0.2: resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} dependencies: