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:
parent
8bd0ca08cd
commit
7daef9a299
37 changed files with 260 additions and 300 deletions
5
.changeset/tender-hounds-juggle.md
Normal file
5
.changeset/tender-hounds-juggle.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'astro': patch
|
||||
---
|
||||
|
||||
Avoid implicit head injection when a head is in the tree
|
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -193,7 +193,7 @@ export class App {
|
|||
request,
|
||||
origin: url.origin,
|
||||
pathname,
|
||||
propagation: this.#manifest.propagation,
|
||||
componentMetadata: this.#manifest.componentMetadata,
|
||||
scripts,
|
||||
links,
|
||||
route: routeData,
|
||||
|
|
|
@ -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> = (
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import type { GetModuleInfo, ModuleInfo } from 'rollup';
|
||||
import type { ViteDevServer } from 'vite';
|
||||
|
||||
import { resolvedPagesVirtualModuleId } from '../app/index.js';
|
||||
|
||||
|
|
|
@ -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(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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 }),
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
});
|
||||
|
||||
|
|
47
packages/astro/src/core/render/dev/metadata.ts
Normal file
47
packages/astro/src/core/render/dev/metadata.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -145,6 +145,7 @@ export default function astroJSX(): PluginObj {
|
|||
clientOnlyComponents: [],
|
||||
hydratedComponents: [],
|
||||
scripts: [],
|
||||
containsHead: false,
|
||||
propagation: 'none',
|
||||
pageOptions: {},
|
||||
};
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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: {},
|
||||
};
|
||||
|
|
|
@ -10,6 +10,7 @@ export interface PluginMetadata {
|
|||
hydratedComponents: TransformResult['hydratedComponents'];
|
||||
clientOnlyComponents: TransformResult['clientOnlyComponents'];
|
||||
scripts: TransformResult['scripts'];
|
||||
containsHead: TransformResult['containsHead'];
|
||||
propagation: PropagationHint;
|
||||
pageOptions: PageOptions;
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
116
packages/astro/src/vite-plugin-head/index.ts
Normal file
116
packages/astro/src/vite-plugin-head/index.ts
Normal 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';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
|
@ -178,6 +178,7 @@ export default function markdown({ settings, logging }: AstroPluginOptions): Plu
|
|||
clientOnlyComponents: [],
|
||||
scripts: [],
|
||||
propagation: 'none',
|
||||
containsHead: false,
|
||||
pageOptions: {},
|
||||
} as PluginMetadata['astro'],
|
||||
vite: {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in a new issue