0
Fork 0
mirror of https://github.com/withastro/astro.git synced 2024-12-23 21:53:55 -05:00

Avoid implicit head injection when there is a head element in the tree (#6638)

* Avoid implicit head injection when there is a head element in the tree

* more

* only do it once

* Update the tests

* Update more tests

* update compiler version

* See if scope stuff can be removed now

* Move up where head injection occurs

* Remove result scoping
This commit is contained in:
Matthew Phillips 2023-03-24 11:17:25 -04:00 committed by GitHub
parent 8bd0ca08cd
commit 7daef9a299
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 260 additions and 300 deletions

View file

@ -0,0 +1,5 @@
---
'astro': patch
---
Avoid implicit head injection when a head is in the tree

View file

@ -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",

View file

@ -1578,6 +1578,7 @@ export interface SSRMetadata {
hasHydrationScript: boolean;
hasDirectives: Set<string>;
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<SSRElement>;
scripts: Set<SSRElement>;
links: Set<SSRElement>;
propagation: Map<string, PropagationHint>;
componentMetadata: Map<string, SSRComponentMetadata>;
propagators: Map<AstroComponentFactory, AstroComponentInstance>;
extraHead: Array<string>;
cookies: AstroCookies | undefined;

View file

@ -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,

View file

@ -14,12 +14,12 @@ export function deserializeManifest(serializedManifest: SerializedSSRManifest):
}
const assets = new Set<string>(serializedManifest.assets);
const propagation = new Map(serializedManifest.propagation);
const componentMetadata = new Map(serializedManifest.componentMetadata);
return {
...serializedManifest,
assets,
propagation,
componentMetadata,
routes,
};
}

View file

@ -193,7 +193,7 @@ export class App {
request,
origin: url.origin,
pathname,
propagation: this.#manifest.propagation,
componentMetadata: this.#manifest.componentMetadata,
scripts,
links,
route: routeData,

View file

@ -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<string, string>;
assets: Set<string>;
propagation: SSRResult['propagation'];
componentMetadata: SSRResult['componentMetadata'];
}
export type SerializedSSRManifest = Omit<SSRManifest, 'routes' | 'assets' | 'propagation'> & {
export type SerializedSSRManifest = Omit<SSRManifest, 'routes' | 'assets' | 'componentMetadata'> & {
routes: SerializedRouteInfo[];
assets: string[];
propagation: readonly [string, PropagationHint][];
componentMetadata: [string, SSRComponentMetadata][];
};
export type AdapterCreateExports<T = any> = (

View file

@ -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,

View file

@ -1,4 +1,5 @@
import type { GetModuleInfo, ModuleInfo } from 'rollup';
import type { ViteDevServer } from 'vite';
import { resolvedPagesVirtualModuleId } from '../app/index.js';

View file

@ -77,7 +77,7 @@ export interface BuildInternals {
staticFiles: Set<string>;
// 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(),
};
}

View file

@ -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));

View file

@ -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),

View file

@ -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 }),

View file

@ -11,7 +11,7 @@ export interface RenderContext {
scripts?: Set<SSRElement>;
links?: Set<SSRElement>;
styles?: Set<SSRElement>;
propagation?: SSRResult['propagation'];
componentMetadata?: SSRResult['componentMetadata'];
route?: RouteData;
status?: number;
}

View file

@ -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,

View file

@ -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<SSRResult['propagation']> {
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);
}
}
}

View file

@ -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<Response> {
@ -154,7 +154,7 @@ export async function renderPage(options: SSROptions): Promise<Response> {
// 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<Response> {
scripts,
links,
styles,
propagation: propagationMap,
componentMetadata: metadata,
route: options.route,
});

View file

@ -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<SSRResult['componentMetadata']> {
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);
}
}
}

View file

@ -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<SSRElement>;
scripts?: Set<SSRElement>;
styles?: Set<SSRElement>;
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<SSRElement>(),
scripts: args.scripts ?? new Set<SSRElement>(),
links: args.links ?? new Set<SSRElement>(),
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,
};

View file

@ -145,6 +145,7 @@ export default function astroJSX(): PluginObj {
clientOnlyComponents: [],
hydratedComponents: [],
scripts: [],
containsHead: false,
propagation: 'none',
pageOptions: {},
};

View file

@ -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';

View file

@ -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:

View file

@ -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<string> {
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';
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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';

View file

@ -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<string, any> = { ...(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 <head> 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) {

View file

@ -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;
}

View file

@ -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<typeof renderTemplate>;
export type ComponentSlots = Record<string, ComponentSlotValue>;
@ -32,8 +31,7 @@ export async function renderSlot(
fallback?: ComponentSlotValue | RenderTemplateResult
): Promise<string> {
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) {

View file

@ -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: {},
};

View file

@ -10,6 +10,7 @@ export interface PluginMetadata {
hydratedComponents: TransformResult['hydratedComponents'];
clientOnlyComponents: TransformResult['clientOnlyComponents'];
scripts: TransformResult['scripts'];
containsHead: TransformResult['containsHead'];
propagation: PropagationHint;
pageOptions: PageOptions;
};

View file

@ -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<string> = 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;
},
},
};
},
},
};
}

View file

@ -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<string>()) {
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);
// <head> 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';
}
}
}
}
},
},
};
},
},
};
}

View file

@ -178,6 +178,7 @@ export default function markdown({ settings, logging }: AstroPluginOptions): Plu
clientOnlyComponents: [],
scripts: [],
propagation: 'none',
containsHead: false,
pageOptions: {},
} as PluginMetadata['astro'],
vite: {

View file

@ -41,8 +41,13 @@ describe('Content Collections - render()', () => {
const launchWeekEntry = blog.find(post => post.id === 'promo/launch-week.mdx');
const { Content } = await launchWeekEntry.render();
---
<h1>testing</h1>
<Content />
<html>
<head><title>Testing</title></head>
<body>
<h1>testing</h1>
<Content />
</body>
</html>
`,
},
root
@ -250,8 +255,13 @@ description: Astro is launching this week!
---
import { Content } from '../launch-week.ts';
---
<h1>Testing</h1>
<Content />
<html>
<head><title>Testing</title></head>
<body>
<h1>Testing</h1>
<Content />
</body>
</html>
`,
},
root

View file

@ -49,8 +49,13 @@ describe('head injection', () => {
import { renderEntry } from '../common/head.js';
const Head = renderEntry();
---
<h1>testing</h1>
<Head />
<html>
<head><title>Testing</title></head>
<body>
<h1>testing</h1>
<Head />
</body>
</html>
`,
},
root

View file

@ -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: