diff --git a/.changeset/tender-hounds-juggle.md b/.changeset/tender-hounds-juggle.md new file mode 100644 index 000000000000..8ea518f0dd54 --- /dev/null +++ b/.changeset/tender-hounds-juggle.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Avoid implicit head injection when a head is in the tree diff --git a/packages/astro/package.json b/packages/astro/package.json index e730dfb86491..246e360d3c8b 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -106,7 +106,7 @@ "test:e2e:match": "playwright test -g" }, "dependencies": { - "@astrojs/compiler": "^1.2.0", + "@astrojs/compiler": "^1.3.0", "@astrojs/language-server": "^0.28.3", "@astrojs/markdown-remark": "^2.1.2", "@astrojs/telemetry": "^2.1.0", diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index 4eca60b3448d..ebb15de1d857 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -1578,6 +1578,7 @@ export interface SSRMetadata { hasHydrationScript: boolean; hasDirectives: Set; hasRenderedHead: boolean; + headInTree: boolean; } /** @@ -1592,11 +1593,16 @@ export interface SSRMetadata { */ export type PropagationHint = 'none' | 'self' | 'in-tree'; +export type SSRComponentMetadata = { + propagation: PropagationHint, + containsHead: boolean +}; + export interface SSRResult { styles: Set; scripts: Set; links: Set; - propagation: Map; + componentMetadata: Map; propagators: Map; extraHead: Array; cookies: AstroCookies | undefined; diff --git a/packages/astro/src/content/runtime.ts b/packages/astro/src/content/runtime.ts index d58e4be62639..a8d18e06f3aa 100644 --- a/packages/astro/src/content/runtime.ts +++ b/packages/astro/src/content/runtime.ts @@ -4,7 +4,6 @@ import { prependForwardSlash } from '../core/path.js'; import { createComponent, createHeadAndContent, - createScopedResult, renderComponent, renderScriptElement, renderStyleElement, @@ -180,7 +179,7 @@ async function render({ return createHeadAndContent( unescapeHTML(styles + links + scripts) as any, renderTemplate`${renderComponent( - createScopedResult(result), + result, 'Content', mod.Content, props, diff --git a/packages/astro/src/core/app/common.ts b/packages/astro/src/core/app/common.ts index 4b14321762af..6fd13d9b9086 100644 --- a/packages/astro/src/core/app/common.ts +++ b/packages/astro/src/core/app/common.ts @@ -14,12 +14,12 @@ export function deserializeManifest(serializedManifest: SerializedSSRManifest): } const assets = new Set(serializedManifest.assets); - const propagation = new Map(serializedManifest.propagation); + const componentMetadata = new Map(serializedManifest.componentMetadata); return { ...serializedManifest, assets, - propagation, + componentMetadata, routes, }; } diff --git a/packages/astro/src/core/app/index.ts b/packages/astro/src/core/app/index.ts index ca66d87e890a..e43e9a8ff73c 100644 --- a/packages/astro/src/core/app/index.ts +++ b/packages/astro/src/core/app/index.ts @@ -193,7 +193,7 @@ export class App { request, origin: url.origin, pathname, - propagation: this.#manifest.propagation, + componentMetadata: this.#manifest.componentMetadata, scripts, links, route: routeData, diff --git a/packages/astro/src/core/app/types.ts b/packages/astro/src/core/app/types.ts index cf85d0387bce..46cb34e80eb0 100644 --- a/packages/astro/src/core/app/types.ts +++ b/packages/astro/src/core/app/types.ts @@ -1,7 +1,7 @@ import type { MarkdownRenderingOptions } from '@astrojs/markdown-remark'; import type { ComponentInstance, - PropagationHint, + SSRComponentMetadata, RouteData, SerializedRouteData, SSRLoadedRenderer, @@ -36,13 +36,13 @@ export interface SSRManifest { renderers: SSRLoadedRenderer[]; entryModules: Record; assets: Set; - propagation: SSRResult['propagation']; + componentMetadata: SSRResult['componentMetadata']; } -export type SerializedSSRManifest = Omit & { +export type SerializedSSRManifest = Omit & { routes: SerializedRouteInfo[]; assets: string[]; - propagation: readonly [string, PropagationHint][]; + componentMetadata: [string, SSRComponentMetadata][]; }; export type AdapterCreateExports = ( diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts index 3cd2b6a64ebb..52e4204a999d 100644 --- a/packages/astro/src/core/build/generate.ts +++ b/packages/astro/src/core/build/generate.ts @@ -405,7 +405,7 @@ async function generatePath( origin, pathname, request: createRequest({ url, headers: new Headers(), logging, ssr }), - propagation: internals.propagation, + componentMetadata: internals.componentMetadata, scripts, links, route: pageData.route, diff --git a/packages/astro/src/core/build/graph.ts b/packages/astro/src/core/build/graph.ts index 6d7335c0955a..6d404b572ee0 100644 --- a/packages/astro/src/core/build/graph.ts +++ b/packages/astro/src/core/build/graph.ts @@ -1,4 +1,5 @@ import type { GetModuleInfo, ModuleInfo } from 'rollup'; +import type { ViteDevServer } from 'vite'; import { resolvedPagesVirtualModuleId } from '../app/index.js'; diff --git a/packages/astro/src/core/build/internal.ts b/packages/astro/src/core/build/internal.ts index 693180d37b51..d4372c0a2081 100644 --- a/packages/astro/src/core/build/internal.ts +++ b/packages/astro/src/core/build/internal.ts @@ -77,7 +77,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']; + componentMetadata: SSRResult['componentMetadata']; } /** @@ -107,7 +107,7 @@ export function createBuildInternals(): BuildInternals { discoveredClientOnlyComponents: new Map(), discoveredScripts: new Set(), staticFiles: new Set(), - propagation: new Map(), + componentMetadata: new Map(), }; } diff --git a/packages/astro/src/core/build/plugins/index.ts b/packages/astro/src/core/build/plugins/index.ts index bb7af6238635..982a90ce348e 100644 --- a/packages/astro/src/core/build/plugins/index.ts +++ b/packages/astro/src/core/build/plugins/index.ts @@ -1,5 +1,5 @@ import { astroConfigBuildPlugin } from '../../../content/vite-plugin-content-assets.js'; -import { astroHeadPropagationBuildPlugin } from '../../../vite-plugin-head-propagation/index.js'; +import { astroHeadBuildPlugin } from '../../../vite-plugin-head/index.js'; import type { AstroBuildPluginContainer } from '../plugin'; import { pluginAliasResolve } from './plugin-alias-resolve.js'; import { pluginAnalyzer } from './plugin-analyzer.js'; @@ -18,7 +18,7 @@ export function registerAllPlugins({ internals, options, register }: AstroBuildP register(pluginInternals(internals)); register(pluginPages(options, internals)); register(pluginCSS(options, internals)); - register(astroHeadPropagationBuildPlugin(options, internals)); + register(astroHeadBuildPlugin(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 c8883a438c03..78a1217e045c 100644 --- a/packages/astro/src/core/build/plugins/plugin-ssr.ts +++ b/packages/astro/src/core/build/plugins/plugin-ssr.ts @@ -209,7 +209,7 @@ function buildManifest( base: settings.config.base, markdown: settings.config.markdown, pageMap: null as any, - propagation: Array.from(internals.propagation), + componentMetadata: Array.from(internals.componentMetadata), renderers: [], entryModules, assets: staticFiles.map((s) => settings.config.base + s), diff --git a/packages/astro/src/core/create-vite.ts b/packages/astro/src/core/create-vite.ts index 32717ff7cc3c..1879daac83b3 100644 --- a/packages/astro/src/core/create-vite.ts +++ b/packages/astro/src/core/create-vite.ts @@ -16,7 +16,7 @@ import { vitePluginAstroServer } from '../vite-plugin-astro-server/index.js'; import astroVitePlugin from '../vite-plugin-astro/index.js'; import configAliasVitePlugin from '../vite-plugin-config-alias/index.js'; import envVitePlugin from '../vite-plugin-env/index.js'; -import astroHeadPropagationPlugin from '../vite-plugin-head-propagation/index.js'; +import astroHeadPlugin from '../vite-plugin-head/index.js'; import htmlVitePlugin from '../vite-plugin-html/index.js'; import { astroInjectEnvTsPlugin } from '../vite-plugin-inject-env-ts/index.js'; import astroIntegrationsContainerPlugin from '../vite-plugin-integrations-container/index.js'; @@ -121,7 +121,7 @@ export async function createVite( astroPostprocessVitePlugin({ settings }), astroIntegrationsContainerPlugin({ settings, logging }), astroScriptsPageSSRPlugin({ settings }), - astroHeadPropagationPlugin({ settings }), + astroHeadPlugin({ settings }), astroScannerPlugin({ settings }), astroInjectEnvTsPlugin({ settings, logging, fs }), astroContentVirtualModPlugin({ settings }), diff --git a/packages/astro/src/core/render/context.ts b/packages/astro/src/core/render/context.ts index 6e453fea0c3f..f6a82e9ca439 100644 --- a/packages/astro/src/core/render/context.ts +++ b/packages/astro/src/core/render/context.ts @@ -11,7 +11,7 @@ export interface RenderContext { scripts?: Set; links?: Set; styles?: Set; - propagation?: SSRResult['propagation']; + componentMetadata?: SSRResult['componentMetadata']; route?: RouteData; status?: number; } diff --git a/packages/astro/src/core/render/core.ts b/packages/astro/src/core/render/core.ts index 862ada7c8b9b..f3b8e2866b1d 100644 --- a/packages/astro/src/core/render/core.ts +++ b/packages/astro/src/core/render/core.ts @@ -98,7 +98,7 @@ export async function renderPage(mod: ComponentInstance, ctx: RenderContext, env params, props: pageProps, pathname: ctx.pathname, - propagation: ctx.propagation, + componentMetadata: ctx.componentMetadata, resolve: env.resolve, renderers: env.renderers, request: ctx.request, diff --git a/packages/astro/src/core/render/dev/head.ts b/packages/astro/src/core/render/dev/head.ts deleted file mode 100644 index 56f110a2050e..000000000000 --- a/packages/astro/src/core/render/dev/head.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { SSRResult } from '../../../@types/astro'; - -import type { ModuleInfo, ModuleLoader } from '../../module-loader/index'; - -import { getAstroMetadata } from '../../../vite-plugin-astro/index.js'; -import { viteID } from '../../util.js'; -import { crawlGraph } from './vite.js'; - -export async function getPropagationMap( - filePath: URL, - loader: ModuleLoader -): Promise { - const map: SSRResult['propagation'] = new Map(); - - const rootID = viteID(filePath); - addInjection(map, loader.getModuleInfo(rootID)); - for await (const moduleNode of crawlGraph(loader, rootID, true)) { - const id = moduleNode.id; - if (id) { - addInjection(map, loader.getModuleInfo(id)); - } - } - - return map; -} - -function addInjection(map: SSRResult['propagation'], modInfo: ModuleInfo | null) { - if (modInfo) { - const astro = getAstroMetadata(modInfo); - if (astro && astro.propagation) { - map.set(modInfo.id, astro.propagation); - } - } -} diff --git a/packages/astro/src/core/render/dev/index.ts b/packages/astro/src/core/render/dev/index.ts index 92d374d3eead..8d26f2e3f5a5 100644 --- a/packages/astro/src/core/render/dev/index.ts +++ b/packages/astro/src/core/render/dev/index.ts @@ -15,7 +15,7 @@ import { createRenderContext, renderPage as coreRenderPage } from '../index.js'; import { filterFoundRenderers, loadRenderer } from '../renderer.js'; import { getStylesForURL } from './css.js'; import type { DevelopmentEnvironment } from './environment'; -import { getPropagationMap } from './head.js'; +import { getComponentMetadata } from './metadata.js'; import { getScriptsForURL } from './scripts.js'; export { createDevelopmentEnvironment } from './environment.js'; export type { DevelopmentEnvironment }; @@ -142,9 +142,9 @@ async function getScriptsAndStyles({ env, filePath }: GetScriptsAndStylesParams) }); }); - const propagationMap = await getPropagationMap(filePath, env.loader); + const metadata = await getComponentMetadata(filePath, env.loader); - return { scripts, styles, links, propagationMap }; + return { scripts, styles, links, metadata }; } export async function renderPage(options: SSROptions): Promise { @@ -154,7 +154,7 @@ export async function renderPage(options: SSROptions): Promise { // The new instances are passed through. options.env.renderers = renderers; - const { scripts, links, styles, propagationMap } = await getScriptsAndStyles({ + const { scripts, links, styles, metadata } = await getScriptsAndStyles({ env: options.env, filePath: options.filePath, }); @@ -166,7 +166,7 @@ export async function renderPage(options: SSROptions): Promise { scripts, links, styles, - propagation: propagationMap, + componentMetadata: metadata, route: options.route, }); diff --git a/packages/astro/src/core/render/dev/metadata.ts b/packages/astro/src/core/render/dev/metadata.ts new file mode 100644 index 000000000000..a79c381867d8 --- /dev/null +++ b/packages/astro/src/core/render/dev/metadata.ts @@ -0,0 +1,47 @@ +import type { SSRResult, SSRComponentMetadata } from '../../../@types/astro'; + +import type { ModuleInfo, ModuleLoader } from '../../module-loader/index'; + +import { getAstroMetadata } from '../../../vite-plugin-astro/index.js'; +import { viteID } from '../../util.js'; +import { crawlGraph } from './vite.js'; + +export async function getComponentMetadata( + filePath: URL, + loader: ModuleLoader +): Promise { + const map: SSRResult['componentMetadata'] = new Map(); + + const rootID = viteID(filePath); + addMetadata(map, loader.getModuleInfo(rootID)); + for await (const moduleNode of crawlGraph(loader, rootID, true)) { + const id = moduleNode.id; + if (id) { + addMetadata(map, loader.getModuleInfo(id)); + } + } + + return map; +} + +function addMetadata( + map: SSRResult['componentMetadata'], + modInfo: ModuleInfo | null +) { + if (modInfo) { + const astro = getAstroMetadata(modInfo); + if(astro) { + let metadata: SSRComponentMetadata = { + containsHead: false, + propagation: 'none' + }; + if(astro.propagation) { + metadata.propagation = astro.propagation; + } + if(astro.containsHead) { + metadata.containsHead = astro.containsHead; + } + map.set(modInfo.id, metadata); + } + } +} diff --git a/packages/astro/src/core/render/result.ts b/packages/astro/src/core/render/result.ts index 523a0776a48a..651ccc74c56d 100644 --- a/packages/astro/src/core/render/result.ts +++ b/packages/astro/src/core/render/result.ts @@ -10,9 +10,7 @@ import type { SSRResult, } from '../../@types/astro'; import { - createScopedResult, renderSlot, - ScopeFlags, stringifyChunk, type ComponentSlots, } from '../../runtime/server/index.js'; @@ -48,7 +46,7 @@ export interface CreateResultArgs { links?: Set; scripts?: Set; styles?: Set; - propagation?: SSRResult['propagation']; + componentMetadata?: SSRResult['componentMetadata']; request: Request; status: number; } @@ -95,7 +93,7 @@ class Slots { public async render(name: string, args: any[] = []) { if (!this.#slots || !this.has(name)) return; - const scoped = createScopedResult(this.#result, ScopeFlags.RenderSlot); + const result = this.#result; if (!Array.isArray(args)) { warn( this.#loggingOpts, @@ -104,24 +102,24 @@ class Slots { ); } else if (args.length > 0) { const slotValue = this.#slots[name]; - const component = typeof slotValue === 'function' ? await slotValue(scoped) : await slotValue; + const component = typeof slotValue === 'function' ? await slotValue(result) : await slotValue; // Astro const expression = getFunctionExpression(component); if (expression) { const slot = () => expression(...args); - return await renderSlot(scoped, slot).then((res) => (res != null ? String(res) : res)); + return await renderSlot(result, slot).then((res) => (res != null ? String(res) : res)); } // JSX if (typeof component === 'function') { - return await renderJSX(scoped, (component as any)(...args)).then((res) => + return await renderJSX(result, (component as any)(...args)).then((res) => res != null ? String(res) : res ); } } - const content = await renderSlot(scoped, this.#slots[name]); - const outHTML = stringifyChunk(scoped, content); + const content = await renderSlot(result, this.#slots[name]); + const outHTML = stringifyChunk(result, content); return outHTML; } @@ -150,6 +148,7 @@ export function createResult(args: CreateResultArgs): SSRResult { // Astro.cookies is defined lazily to avoid the cost on pages that do not use it. let cookies: AstroCookies | undefined = undefined; + let componentMetadata = args.componentMetadata ?? new Map(); // Create the result object that will be passed into the render function. // This object starts here as an empty shell (not yet the result) but then @@ -158,7 +157,7 @@ export function createResult(args: CreateResultArgs): SSRResult { styles: args.styles ?? new Set(), scripts: args.scripts ?? new Set(), links: args.links ?? new Set(), - propagation: args.propagation ?? new Map(), + componentMetadata, propagators: new Map(), extraHead: [], scope: 0, @@ -248,6 +247,7 @@ export function createResult(args: CreateResultArgs): SSRResult { hasHydrationScript: false, hasRenderedHead: false, hasDirectives: new Set(), + headInTree: false, }, response, }; diff --git a/packages/astro/src/jsx/babel.ts b/packages/astro/src/jsx/babel.ts index 88b01ad8de8a..861914336a14 100644 --- a/packages/astro/src/jsx/babel.ts +++ b/packages/astro/src/jsx/babel.ts @@ -145,6 +145,7 @@ export default function astroJSX(): PluginObj { clientOnlyComponents: [], hydratedComponents: [], scripts: [], + containsHead: false, propagation: 'none', pageOptions: {}, }; diff --git a/packages/astro/src/runtime/server/index.ts b/packages/astro/src/runtime/server/index.ts index cae48fd41830..ebc234ac200e 100644 --- a/packages/astro/src/runtime/server/index.ts +++ b/packages/astro/src/runtime/server/index.ts @@ -5,13 +5,10 @@ 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, @@ -26,7 +23,6 @@ export { renderTemplate, renderToString, renderUniqueStylesheet, - ScopeFlags, stringifyChunk, voidElementNames, } from './render/index.js'; diff --git a/packages/astro/src/runtime/server/jsx.ts b/packages/astro/src/runtime/server/jsx.ts index 0823b54d73a2..c8da19eaf23a 100644 --- a/packages/astro/src/runtime/server/jsx.ts +++ b/packages/astro/src/runtime/server/jsx.ts @@ -12,7 +12,6 @@ import { } from './index.js'; import { HTMLParts } from './render/common.js'; import type { ComponentIterable } from './render/component'; -import { createScopedResult, ScopeFlags } from './render/scope.js'; const ClientOnlyPlaceholder = 'astro-client-only'; @@ -95,8 +94,7 @@ Did you forget to import the component or is it possible there is a typo?`); props[key] = value; } } - const scoped = createScopedResult(result, ScopeFlags.JSX); - const html = markHTMLString(await renderToString(scoped, vnode.type as any, props, slots)); + const html = markHTMLString(await renderToString(result, vnode.type as any, props, slots)); return html; } 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 eccf7b4e7ebf..045329c0f7b4 100644 --- a/packages/astro/src/runtime/server/render/astro/factory.ts +++ b/packages/astro/src/runtime/server/render/astro/factory.ts @@ -3,7 +3,6 @@ import type { HeadAndContent } from './head-and-content'; import type { RenderTemplateResult } from './render-template'; import { HTMLParts } from '../common.js'; -import { createScopedResult, ScopeFlags } from '../scope.js'; import { isHeadAndContent } from './head-and-content.js'; import { renderAstroTemplateResult } from './render-template.js'; @@ -28,8 +27,7 @@ export async function renderToString( props: any, children: any ): Promise { - const scoped = createScopedResult(result, ScopeFlags.Astro); - const factoryResult = await componentFactory(scoped, props, children); + const factoryResult = await componentFactory(result, props, children); if (factoryResult instanceof Response) { const response = factoryResult; @@ -50,8 +48,8 @@ export function isAPropagatingComponent( factory: AstroComponentFactory ): boolean { let hint: PropagationHint = factory.propagation || 'none'; - if (factory.moduleId && result.propagation.has(factory.moduleId) && hint === 'none') { - hint = result.propagation.get(factory.moduleId)!; + if(factory.moduleId && result.componentMetadata.has(factory.moduleId) && hint === 'none') { + hint = result.componentMetadata.get(factory.moduleId)!.propagation; } return hint === 'in-tree' || hint === 'self'; } diff --git a/packages/astro/src/runtime/server/render/astro/instance.ts b/packages/astro/src/runtime/server/render/astro/instance.ts index 9a0839e5140f..47ce7f49569a 100644 --- a/packages/astro/src/runtime/server/render/astro/instance.ts +++ b/packages/astro/src/runtime/server/render/astro/instance.ts @@ -5,7 +5,6 @@ import type { AstroComponentFactory, AstroFactoryReturnValue } from './factory.j import { HydrationDirectiveProps } from '../../hydration.js'; import { isPromise } from '../../util.js'; import { renderChild } from '../any.js'; -import { createScopedResult, ScopeFlags } from '../scope.js'; import { isAPropagatingComponent } from './factory.js'; import { isHeadAndContent } from './head-and-content.js'; @@ -31,9 +30,8 @@ export class AstroComponentInstance { this.props = props; this.factory = factory; this.slotValues = {}; - const scoped = createScopedResult(result, ScopeFlags.Slot); for (const name in slots) { - const value = slots[name](scoped); + const value = slots[name](result); this.slotValues[name] = () => value; } } diff --git a/packages/astro/src/runtime/server/render/common.ts b/packages/astro/src/runtime/server/render/common.ts index 3c0f8d50dff1..6892a0f34515 100644 --- a/packages/astro/src/runtime/server/render/common.ts +++ b/packages/astro/src/runtime/server/render/common.ts @@ -9,7 +9,6 @@ import { type PrescriptType, } from '../scripts.js'; import { renderAllHeadContent } from './head.js'; -import { hasScopeFlag, ScopeFlags } from './scope.js'; import { isSlotString, type SlotString } from './slot.js'; export const Fragment = Symbol.for('astro:fragment'); @@ -50,52 +49,9 @@ export function stringifyChunk(result: SSRResult, chunk: string | SlotString | R return renderAllHeadContent(result); } case 'maybe-head': { - if (result._metadata.hasRenderedHead) { + if (result._metadata.hasRenderedHead || result._metadata.headInTree) { 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 rendered within JSX, head will be injected by the page itself. - case ScopeFlags.JSX | ScopeFlags.Astro: { - if (hasScopeFlag(result, ScopeFlags.JSX)) { - return ''; - } - break; - } - - // If the current scope is with Astro.slots.render() - case ScopeFlags.Slot: - case ScopeFlags.Slot | ScopeFlags.HeadBuffer: { - if (hasScopeFlag(result, ScopeFlags.RenderSlot)) { - return ''; - } - break; - } - - // Nested element inside of JSX during head buffering phase - case ScopeFlags.HeadBuffer: { - if (hasScopeFlag(result, ScopeFlags.JSX | ScopeFlags.HeadBuffer)) { - return ''; - } - break; - } - - // Astro.slots.render() 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); } } diff --git a/packages/astro/src/runtime/server/render/index.ts b/packages/astro/src/runtime/server/render/index.ts index 4f7e14c9d304..33e3dfe8ff00 100644 --- a/packages/astro/src/runtime/server/render/index.ts +++ b/packages/astro/src/runtime/server/render/index.ts @@ -10,7 +10,6 @@ export { renderComponent, renderComponentToIterable } from './component.js'; export { renderHTMLElement } from './dom.js'; export { maybeRenderHead, renderHead } from './head.js'; export { renderPage } from './page.js'; -export { addScopeFlag, createScopedResult, removeScopeFlag, ScopeFlags } from './scope.js'; export { renderSlot, type ComponentSlots } from './slot.js'; export { renderScriptElement, renderStyleElement, renderUniqueStylesheet } from './tags.js'; export type { RenderInstruction } from './types'; diff --git a/packages/astro/src/runtime/server/render/page.ts b/packages/astro/src/runtime/server/render/page.ts index 857cfdd6a90e..c703b79c5dc2 100644 --- a/packages/astro/src/runtime/server/render/page.ts +++ b/packages/astro/src/runtime/server/render/page.ts @@ -15,7 +15,6 @@ import { import { chunkToByteArray, encoder, HTMLParts } from './common.js'; import { renderComponent } from './component.js'; import { maybeRenderHead } from './head.js'; -import { createScopedResult, ScopeFlags } from './scope.js'; const needsHeadRenderingSymbol = Symbol.for('astro.needsHeadRendering'); @@ -56,13 +55,12 @@ 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(scoped); + const returnValue = await value.init(result); if (isHeadAndContent(returnValue)) { result.extraHead.push(returnValue.head); } @@ -81,7 +79,16 @@ export async function renderPage( const pageProps: Record = { ...(props ?? {}), 'server:root': true }; let output: ComponentIterable; + let head = ''; try { + if (nonAstroPageNeedsHeadInjection(componentFactory)) { + const parts = new HTMLParts(); + for await(const chunk of maybeRenderHead(result)) { + parts.append(chunk, result); + } + head = parts.toString(); + } + const renderResult = await renderComponent( result, componentFactory.name, @@ -106,11 +113,7 @@ export async function renderPage( // Accumulate the HTML string and append the head if necessary. const bytes = await iterableToHTMLBytes(result, output, async (parts) => { - if (nonAstroPageNeedsHeadInjection(componentFactory)) { - for await (let chunk of maybeRenderHead(result)) { - parts.append(chunk, result); - } - } + parts.append(head, result); }); return new Response(bytes, { @@ -120,6 +123,9 @@ export async function renderPage( ]), }); } + // Mark if this page component contains a within its tree. If it does + // We avoid implicit head injection entirely. + result._metadata.headInTree = result.componentMetadata.get(componentFactory.moduleId!)?.containsHead ?? false; const factoryReturnValue = await componentFactory(result, props, children); const factoryIsHeadAndContent = isHeadAndContent(factoryReturnValue); if (isRenderTemplateResult(factoryReturnValue) || factoryIsHeadAndContent) { diff --git a/packages/astro/src/runtime/server/render/scope.ts b/packages/astro/src/runtime/server/render/scope.ts deleted file mode 100644 index fce96c0e79fc..000000000000 --- a/packages/astro/src/runtime/server/render/scope.ts +++ /dev/null @@ -1,36 +0,0 @@ -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 hasScopeFlag(result: SSRResult, flag: ScopeFlagValues) { - return (result.scope & flag) === 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 5e21d1e07c3d..cd6bc064bf5f 100644 --- a/packages/astro/src/runtime/server/render/slot.ts +++ b/packages/astro/src/runtime/server/render/slot.ts @@ -4,7 +4,6 @@ import type { RenderInstruction } from './types.js'; import { HTMLString, markHTMLString } from '../escape.js'; import { renderChild } from './any.js'; -import { createScopedResult, ScopeFlags } from './scope.js'; type RenderTemplateResult = ReturnType; export type ComponentSlots = Record; @@ -32,8 +31,7 @@ export async function renderSlot( fallback?: ComponentSlotValue | RenderTemplateResult ): Promise { if (slotted) { - const scoped = createScopedResult(result, ScopeFlags.Slot); - let iterator = renderChild(typeof slotted === 'function' ? slotted(scoped) : slotted); + let iterator = renderChild(typeof slotted === 'function' ? slotted(result) : slotted); let content = ''; let instructions: null | RenderInstruction[] = null; for await (const chunk of iterator) { diff --git a/packages/astro/src/vite-plugin-astro/index.ts b/packages/astro/src/vite-plugin-astro/index.ts index 750061c05111..70c4da61181d 100644 --- a/packages/astro/src/vite-plugin-astro/index.ts +++ b/packages/astro/src/vite-plugin-astro/index.ts @@ -153,6 +153,7 @@ export default function astro({ settings, logging }: AstroPluginOptions): vite.P clientOnlyComponents: transformResult.clientOnlyComponents, hydratedComponents: transformResult.hydratedComponents, scripts: transformResult.scripts, + containsHead: transformResult.containsHead, propagation: 'none', pageOptions: {}, }; diff --git a/packages/astro/src/vite-plugin-astro/types.ts b/packages/astro/src/vite-plugin-astro/types.ts index ebb1c7d395fd..8b74d7f33865 100644 --- a/packages/astro/src/vite-plugin-astro/types.ts +++ b/packages/astro/src/vite-plugin-astro/types.ts @@ -10,6 +10,7 @@ export interface PluginMetadata { hydratedComponents: TransformResult['hydratedComponents']; clientOnlyComponents: TransformResult['clientOnlyComponents']; scripts: TransformResult['scripts']; + containsHead: TransformResult['containsHead']; propagation: PropagationHint; pageOptions: PageOptions; }; diff --git a/packages/astro/src/vite-plugin-head-propagation/index.ts b/packages/astro/src/vite-plugin-head-propagation/index.ts deleted file mode 100644 index 6dbd761690fb..000000000000 --- a/packages/astro/src/vite-plugin-head-propagation/index.ts +++ /dev/null @@ -1,112 +0,0 @@ -import type { ModuleInfo } from 'rollup'; -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 type * as vite from 'vite'; -import { walkParentInfos } from '../core/build/graph.js'; -import { getAstroMetadata } from '../vite-plugin-astro/index.js'; - -const injectExp = /^\/\/\s*astro-head-inject/; -/** - * If any component is marked as doing head injection, walk up the tree - * and mark parent Astro components as having head injection in the tree. - * This is used at runtime to determine if we should wait for head content - * to be populated before rendering the entire tree. - */ -export default function configHeadPropagationVitePlugin({ - settings, -}: { - settings: AstroSettings; -}): vite.Plugin { - function addHeadInjectionInTree( - graph: vite.ModuleGraph, - id: string, - getInfo: (id: string) => ModuleInfo | null, - seen: Set = new Set() - ) { - const mod = server.moduleGraph.getModuleById(id); - for (const parent of mod?.importers || []) { - if (parent.id) { - if (seen.has(parent.id)) { - continue; - } else { - seen.add(parent.id); - } - const info = getInfo(parent.id); - if (info?.meta.astro) { - const astroMetadata = getAstroMetadata(info); - if (astroMetadata) { - astroMetadata.propagation = 'in-tree'; - } - } - addHeadInjectionInTree(graph, parent.id, getInfo, seen); - } - } - } - - let server: vite.ViteDevServer; - return { - name: 'astro:head-propagation', - configureServer(_server) { - server = _server; - }, - transform(source, id) { - if (!server) { - return; - } - - if (injectExp.test(source)) { - addHeadInjectionInTree(server.moduleGraph, id, (child) => this.getModuleInfo(child)); - } - }, - }; -} - -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/astro/src/vite-plugin-head/index.ts b/packages/astro/src/vite-plugin-head/index.ts new file mode 100644 index 000000000000..f0a3cd2b343e --- /dev/null +++ b/packages/astro/src/vite-plugin-head/index.ts @@ -0,0 +1,116 @@ +import type * as vite from 'vite'; +import type { ModuleInfo } from 'rollup'; +import type { AstroSettings, SSRResult, SSRComponentMetadata } from '../@types/astro'; +import type { AstroBuildPlugin } from '../core/build/plugin.js'; +import type { StaticBuildOptions } from '../core/build/types'; +import type { PluginMetadata } from '../vite-plugin-astro/types'; + +import { getTopLevelPages, walkParentInfos } from '../core/build/graph.js'; +import type { BuildInternals } from '../core/build/internal.js'; +import { getAstroMetadata } from '../vite-plugin-astro/index.js'; + +const injectExp = /^\/\/\s*astro-head-inject/; + +export default function configHeadVitePlugin({ + settings, +}: { + settings: AstroSettings; +}): vite.Plugin { + let server: vite.ViteDevServer; + + function propagateMetadata< + P extends keyof PluginMetadata['astro'], + V extends PluginMetadata['astro'][P] + >(this: { getModuleInfo(id: string): ModuleInfo | null }, id: string, prop: P, value: V, seen = new Set()) { + if(seen.has(id)) return; + seen.add(id); + const mod = server.moduleGraph.getModuleById(id); + const info = this.getModuleInfo(id); + if (info?.meta.astro) { + const astroMetadata = getAstroMetadata(info) + if(astroMetadata) { + Reflect.set(astroMetadata, prop, value); + } + } + + for (const parent of mod?.importers || []) { + if(parent.id) { + propagateMetadata.call(this, parent.id, prop, value, seen); + } + } + } + + + return { + name: 'astro:head-metadata', + configureServer(_server) { + server = _server; + }, + transform(source, id) { + if (!server) { + return; + } + + let info = this.getModuleInfo(id); + if(info && getAstroMetadata(info)?.containsHead) { + propagateMetadata.call(this, id, 'containsHead', true); + } + + if (injectExp.test(source)) { + propagateMetadata.call(this, id, 'propagation', 'in-tree'); + } + }, + }; +} + +export function astroHeadBuildPlugin( + options: StaticBuildOptions, + internals: BuildInternals +): AstroBuildPlugin { + return { + build: 'ssr', + hooks: { + 'build:before'() { + return { + vitePlugin: { + name: 'astro:head-metadata-build', + generateBundle(_opts, bundle) { + const map: SSRResult['componentMetadata'] = internals.componentMetadata; + function getOrCreateMetadata(id: string): SSRComponentMetadata { + if(map.has(id)) return map.get(id)!; + const metadata: SSRComponentMetadata = { + propagation: 'none', + containsHead: false + }; + map.set(id, metadata); + return metadata; + } + + for (const [,output] of Object.entries(bundle)) { + if (output.type !== 'chunk') continue; + for (const [id, mod] of Object.entries(output.modules)) { + const modinfo = this.getModuleInfo(id); + + // tag in the tree + if(modinfo && getAstroMetadata(modinfo)?.containsHead) { + for(const [pageInfo] of getTopLevelPages(id, this)) { + let metadata = getOrCreateMetadata(pageInfo.id); + metadata.containsHead = true; + } + } + + // Head propagation (aka bubbling) + if (mod.code && injectExp.test(mod.code)) { + for (const [info] of walkParentInfos(id, this)) { + getOrCreateMetadata(info.id).propagation = 'in-tree'; + } + } + } + } + }, + }, + }; + }, + }, + }; +} diff --git a/packages/astro/src/vite-plugin-markdown/index.ts b/packages/astro/src/vite-plugin-markdown/index.ts index 7f99d9e1ba73..3e22c59ec2a4 100644 --- a/packages/astro/src/vite-plugin-markdown/index.ts +++ b/packages/astro/src/vite-plugin-markdown/index.ts @@ -178,6 +178,7 @@ export default function markdown({ settings, logging }: AstroPluginOptions): Plu clientOnlyComponents: [], scripts: [], propagation: 'none', + containsHead: false, pageOptions: {}, } as PluginMetadata['astro'], vite: { diff --git a/packages/astro/test/units/dev/collections-renderentry.test.js b/packages/astro/test/units/dev/collections-renderentry.test.js index 730ec194f6d6..d44f65af4eb0 100644 --- a/packages/astro/test/units/dev/collections-renderentry.test.js +++ b/packages/astro/test/units/dev/collections-renderentry.test.js @@ -41,8 +41,13 @@ describe('Content Collections - render()', () => { const launchWeekEntry = blog.find(post => post.id === 'promo/launch-week.mdx'); const { Content } = await launchWeekEntry.render(); --- -

testing

- + + Testing + +

testing

+ + + `, }, root @@ -250,8 +255,13 @@ description: Astro is launching this week! --- import { Content } from '../launch-week.ts'; --- -

Testing

- + + Testing + +

Testing

+ + + `, }, root diff --git a/packages/astro/test/units/dev/head-injection.test.js b/packages/astro/test/units/dev/head-injection.test.js index e98c6108c447..9d76e0a911f2 100644 --- a/packages/astro/test/units/dev/head-injection.test.js +++ b/packages/astro/test/units/dev/head-injection.test.js @@ -49,8 +49,13 @@ describe('head injection', () => { import { renderEntry } from '../common/head.js'; const Head = renderEntry(); --- -

testing

- + + Testing + +

testing

+ + + `, }, root diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 441fe16a7535..63f82fce0890 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -424,7 +424,7 @@ importers: packages/astro: specifiers: - '@astrojs/compiler': ^1.2.0 + '@astrojs/compiler': ^1.3.0 '@astrojs/language-server': ^0.28.3 '@astrojs/markdown-remark': ^2.1.2 '@astrojs/telemetry': ^2.1.0 @@ -517,7 +517,7 @@ importers: yargs-parser: ^21.0.1 zod: ^3.17.3 dependencies: - '@astrojs/compiler': 1.2.0 + '@astrojs/compiler': 1.3.0 '@astrojs/language-server': 0.28.3 '@astrojs/markdown-remark': link:../markdown/remark '@astrojs/telemetry': link:../telemetry @@ -4185,8 +4185,8 @@ packages: /@astrojs/compiler/0.31.4: resolution: {integrity: sha512-6bBFeDTtPOn4jZaiD3p0f05MEGQL9pw2Zbfj546oFETNmjJFWO3nzHz6/m+P53calknCvyVzZ5YhoBLIvzn5iw==} - /@astrojs/compiler/1.2.0: - resolution: {integrity: sha512-O8yPCyuq+PU9Fjht2tIW6WzSWiq8qDF1e8uAX2x+SOGFzKqOznp52UlDG2mSf+ekf0Z3R34sb64O7SgX+asTxg==} + /@astrojs/compiler/1.3.0: + resolution: {integrity: sha512-VxSj3gh/UTB/27rkRCT7SvyGjWtuxUO7Jf7QqDduch7j/gr/uA5P/Q5I/4zIIrZjy2yQAKyKLoox2QI2mM/BSA==} dev: false /@astrojs/language-server/0.28.3: