Add additional scoping for head buffering (#6152)
* Add additional scoping for head buffering * Add test for direct usage of nested component * Add special scoping for Astro.scopes.render() * Generate propagation map during the build * Move to a maybeHead instruction * Properly serialize for SSR * More conservative scoping * Maybe had should honor result._metadata.hasRenderedHead * Properly type slots * Allow template result to be passed * Add changeset
This commit is contained in:
parent
cee70f5c6a
commit
d1f5611feb
42 changed files with 386 additions and 68 deletions
10
.changeset/pretty-bananas-own.md
Normal file
10
.changeset/pretty-bananas-own.md
Normal file
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
'astro': patch
|
||||
---
|
||||
|
||||
Fix MDX related head placement bugs
|
||||
|
||||
This fixes a variety of head content placement bugs (such as page `<link>`) related to MDX, especially when used in content collections. Issues fixed:
|
||||
|
||||
- Head content being placed in the body instead of the head.
|
||||
- Head content missing when rendering an MDX component from within a nested Astro component.
|
|
@ -99,7 +99,7 @@
|
|||
"test:e2e:match": "playwright test -g"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/compiler": "^1.0.1",
|
||||
"@astrojs/compiler": "^1.1.0",
|
||||
"@astrojs/language-server": "^0.28.3",
|
||||
"@astrojs/markdown-remark": "^2.0.1",
|
||||
"@astrojs/telemetry": "^2.0.0",
|
||||
|
|
|
@ -4,6 +4,7 @@ import { prependForwardSlash } from '../core/path.js';
|
|||
import {
|
||||
createComponent,
|
||||
createHeadAndContent,
|
||||
createScopedResult,
|
||||
renderComponent,
|
||||
renderScriptElement,
|
||||
renderStyleElement,
|
||||
|
@ -169,7 +170,7 @@ async function render({
|
|||
|
||||
return createHeadAndContent(
|
||||
unescapeHTML(styles + links + scripts) as any,
|
||||
renderTemplate`${renderComponent(result, 'Content', mod.Content, props, slots)}`
|
||||
renderTemplate`${renderComponent(createScopedResult(result), 'Content', mod.Content, props, slots)}`
|
||||
);
|
||||
},
|
||||
propagation: 'self',
|
||||
|
|
|
@ -14,10 +14,12 @@ export function deserializeManifest(serializedManifest: SerializedSSRManifest):
|
|||
}
|
||||
|
||||
const assets = new Set<string>(serializedManifest.assets);
|
||||
const propagation = new Map(serializedManifest.propagation);
|
||||
|
||||
return {
|
||||
...serializedManifest,
|
||||
assets,
|
||||
propagation,
|
||||
routes,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -193,6 +193,7 @@ export class App {
|
|||
request,
|
||||
origin: url.origin,
|
||||
pathname,
|
||||
propagation: this.#manifest.propagation,
|
||||
scripts,
|
||||
links,
|
||||
route: routeData,
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import type { MarkdownRenderingOptions } from '@astrojs/markdown-remark';
|
||||
import type {
|
||||
ComponentInstance,
|
||||
PropagationHint,
|
||||
RouteData,
|
||||
SerializedRouteData,
|
||||
SSRLoadedRenderer,
|
||||
SSRResult,
|
||||
} from '../../@types/astro';
|
||||
|
||||
export type ComponentPath = string;
|
||||
|
@ -34,11 +36,13 @@ export interface SSRManifest {
|
|||
renderers: SSRLoadedRenderer[];
|
||||
entryModules: Record<string, string>;
|
||||
assets: Set<string>;
|
||||
propagation: SSRResult['propagation'];
|
||||
}
|
||||
|
||||
export type SerializedSSRManifest = Omit<SSRManifest, 'routes' | 'assets'> & {
|
||||
export type SerializedSSRManifest = Omit<SSRManifest, 'routes' | 'assets' | 'propagation'> & {
|
||||
routes: SerializedRouteInfo[];
|
||||
assets: string[];
|
||||
propagation: readonly [string, PropagationHint][];
|
||||
};
|
||||
|
||||
export type AdapterCreateExports<T = any> = (
|
||||
|
|
|
@ -376,6 +376,7 @@ async function generatePath(
|
|||
origin,
|
||||
pathname,
|
||||
request: createRequest({ url, headers: new Headers(), logging, ssr }),
|
||||
propagation: internals.propagation,
|
||||
scripts,
|
||||
links,
|
||||
route: pageData.route,
|
||||
|
|
|
@ -4,6 +4,7 @@ import type { PageBuildData, ViteID } from './types';
|
|||
import { PageOptions } from '../../vite-plugin-astro/types';
|
||||
import { prependForwardSlash, removeFileExtension } from '../path.js';
|
||||
import { viteID } from '../util.js';
|
||||
import { SSRResult } from '../../@types/astro';
|
||||
|
||||
export interface BuildInternals {
|
||||
/**
|
||||
|
@ -66,6 +67,7 @@ export interface BuildInternals {
|
|||
staticFiles: Set<string>;
|
||||
// The SSR entry chunk. Kept in internals to share between ssr/client build steps
|
||||
ssrEntryChunk?: OutputChunk;
|
||||
propagation: SSRResult['propagation'];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -95,6 +97,7 @@ export function createBuildInternals(): BuildInternals {
|
|||
discoveredClientOnlyComponents: new Set(),
|
||||
discoveredScripts: new Set(),
|
||||
staticFiles: new Set(),
|
||||
propagation: new Map(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import { pluginInternals } from './plugin-internals.js';
|
|||
import { pluginPages } from './plugin-pages.js';
|
||||
import { pluginPrerender } from './plugin-prerender.js';
|
||||
import { pluginSSR } from './plugin-ssr.js';
|
||||
import { astroHeadPropagationBuildPlugin } from '../../../vite-plugin-head-propagation/index.js';
|
||||
|
||||
export function registerAllPlugins({ internals, options, register }: AstroBuildPluginContainer) {
|
||||
register(pluginAliasResolve(internals));
|
||||
|
@ -15,6 +16,7 @@ export function registerAllPlugins({ internals, options, register }: AstroBuildP
|
|||
register(pluginInternals(internals));
|
||||
register(pluginPages(options, internals));
|
||||
register(pluginCSS(options, internals));
|
||||
register(astroHeadPropagationBuildPlugin(options, internals));
|
||||
register(pluginPrerender(options, internals));
|
||||
register(astroConfigBuildPlugin(options, internals));
|
||||
register(pluginHoistedScripts(options, internals));
|
||||
|
|
|
@ -211,6 +211,7 @@ function buildManifest(
|
|||
contentDir: getContentPaths(settings.config).contentDir,
|
||||
},
|
||||
pageMap: null as any,
|
||||
propagation: Array.from(internals.propagation),
|
||||
renderers: [],
|
||||
entryModules,
|
||||
assets: staticFiles.map((s) => settings.config.base + s),
|
||||
|
|
|
@ -42,6 +42,7 @@ export async function compile({
|
|||
sourcemap: 'both',
|
||||
internalURL: 'astro/server/index.js',
|
||||
astroGlobalArgs: JSON.stringify(astroConfig.site),
|
||||
resultScopedSlot: true,
|
||||
preprocessStyle: createStylePreprocessor({
|
||||
filename,
|
||||
viteConfig,
|
||||
|
|
|
@ -9,7 +9,7 @@ import type {
|
|||
SSRLoadedRenderer,
|
||||
SSRResult,
|
||||
} from '../../@types/astro';
|
||||
import { renderSlot, stringifyChunk } from '../../runtime/server/index.js';
|
||||
import { renderSlot, stringifyChunk, ScopeFlags, createScopedResult, ComponentSlots } from '../../runtime/server/index.js';
|
||||
import { renderJSX } from '../../runtime/server/jsx.js';
|
||||
import { AstroCookies } from '../cookies/index.js';
|
||||
import { AstroError, AstroErrorData } from '../errors/index.js';
|
||||
|
@ -55,10 +55,10 @@ function getFunctionExpression(slot: any) {
|
|||
|
||||
class Slots {
|
||||
#result: SSRResult;
|
||||
#slots: Record<string, any> | null;
|
||||
#slots: ComponentSlots | null;
|
||||
#loggingOpts: LogOptions;
|
||||
|
||||
constructor(result: SSRResult, slots: Record<string, any> | null, logging: LogOptions) {
|
||||
constructor(result: SSRResult, slots: ComponentSlots | null, logging: LogOptions) {
|
||||
this.#result = result;
|
||||
this.#slots = slots;
|
||||
this.#loggingOpts = logging;
|
||||
|
@ -89,6 +89,7 @@ class Slots {
|
|||
public async render(name: string, args: any[] = []) {
|
||||
if (!this.#slots || !this.has(name)) return;
|
||||
|
||||
const scoped = createScopedResult(this.#result, ScopeFlags.RenderSlot);
|
||||
if (!Array.isArray(args)) {
|
||||
warn(
|
||||
this.#loggingOpts,
|
||||
|
@ -97,26 +98,26 @@ class Slots {
|
|||
);
|
||||
} else if (args.length > 0) {
|
||||
const slotValue = this.#slots[name];
|
||||
const component = typeof slotValue === 'function' ? await slotValue() : await slotValue;
|
||||
const component = typeof slotValue === 'function' ? await slotValue(scoped) : await slotValue;
|
||||
|
||||
// Astro
|
||||
const expression = getFunctionExpression(component);
|
||||
if (expression) {
|
||||
const slot = expression(...args);
|
||||
return await renderSlot(this.#result, slot).then((res) =>
|
||||
const slot = () => expression(...args);
|
||||
return await renderSlot(scoped, slot).then((res) =>
|
||||
res != null ? String(res) : res
|
||||
);
|
||||
}
|
||||
// JSX
|
||||
if (typeof component === 'function') {
|
||||
return await renderJSX(this.#result, component(...args)).then((res) =>
|
||||
return await renderJSX(scoped, (component as any)(...args)).then((res) =>
|
||||
res != null ? String(res) : res
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const content = await renderSlot(this.#result, this.#slots[name]);
|
||||
const outHTML = stringifyChunk(this.#result, content);
|
||||
const content = await renderSlot(scoped, this.#slots[name]);
|
||||
const outHTML = stringifyChunk(scoped, content);
|
||||
|
||||
return outHTML;
|
||||
}
|
||||
|
|
|
@ -5,10 +5,13 @@ export { escapeHTML, HTMLBytes, HTMLString, markHTMLString, unescapeHTML } from
|
|||
export { renderJSX } from './jsx.js';
|
||||
export {
|
||||
addAttribute,
|
||||
addScopeFlag,
|
||||
createHeadAndContent,
|
||||
createScopedResult,
|
||||
defineScriptVars,
|
||||
Fragment,
|
||||
maybeRenderHead,
|
||||
removeScopeFlag,
|
||||
renderAstroTemplateResult as renderAstroComponent,
|
||||
renderComponent,
|
||||
renderComponentToIterable,
|
||||
|
@ -23,15 +26,15 @@ export {
|
|||
renderTemplate,
|
||||
renderToString,
|
||||
renderUniqueStylesheet,
|
||||
ScopeFlags,
|
||||
stringifyChunk,
|
||||
voidElementNames,
|
||||
} from './render/index.js';
|
||||
export type {
|
||||
AstroComponentFactory,
|
||||
AstroComponentInstance,
|
||||
AstroComponentSlots,
|
||||
AstroComponentSlotsWithValues,
|
||||
RenderInstruction,
|
||||
ComponentSlots
|
||||
} from './render/index.js';
|
||||
|
||||
import { markHTMLString } from './escape.js';
|
||||
|
|
|
@ -12,7 +12,7 @@ import {
|
|||
} from './index.js';
|
||||
import { HTMLParts } from './render/common.js';
|
||||
import type { ComponentIterable } from './render/component';
|
||||
import { ScopeFlags } from './render/util.js';
|
||||
import { createScopedResult, ScopeFlags } from './render/scope.js';
|
||||
|
||||
const ClientOnlyPlaceholder = 'astro-client-only';
|
||||
|
||||
|
@ -95,8 +95,9 @@ Did you forget to import the component or is it possible there is a typo?`);
|
|||
props[key] = value;
|
||||
}
|
||||
}
|
||||
result.scope |= ScopeFlags.JSX;
|
||||
return markHTMLString(await renderToString(result, vnode.type as any, props, slots));
|
||||
const scoped = createScopedResult(result, ScopeFlags.JSX);
|
||||
const html = markHTMLString(await renderToString(scoped, vnode.type as any, props, slots));
|
||||
return html;
|
||||
}
|
||||
case !vnode.type && (vnode.type as any) !== 0:
|
||||
return '';
|
||||
|
|
|
@ -3,7 +3,7 @@ import type { HeadAndContent } from './head-and-content';
|
|||
import type { RenderTemplateResult } from './render-template';
|
||||
|
||||
import { HTMLParts } from '../common.js';
|
||||
import { ScopeFlags } from '../util.js';
|
||||
import { addScopeFlag, createScopedResult, ScopeFlags } from '../scope.js';
|
||||
import { isHeadAndContent } from './head-and-content.js';
|
||||
import { renderAstroTemplateResult } from './render-template.js';
|
||||
|
||||
|
@ -28,8 +28,8 @@ export async function renderToString(
|
|||
props: any,
|
||||
children: any
|
||||
): Promise<string> {
|
||||
result.scope |= ScopeFlags.Astro;
|
||||
const factoryResult = await componentFactory(result, props, children);
|
||||
const scoped = createScopedResult(result, ScopeFlags.Astro);
|
||||
const factoryResult = await componentFactory(scoped, props, children);
|
||||
|
||||
if (factoryResult instanceof Response) {
|
||||
const response = factoryResult;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
export type { AstroComponentFactory } from './factory';
|
||||
export { isAstroComponentFactory, renderToString } from './factory.js';
|
||||
export { createHeadAndContent, isHeadAndContent } from './head-and-content.js';
|
||||
export type { AstroComponentInstance, ComponentSlots, ComponentSlotsWithValues } from './instance';
|
||||
export type { AstroComponentInstance } from './instance';
|
||||
export { createAstroComponentInstance, isAstroComponentInstance } from './instance.js';
|
||||
export {
|
||||
isRenderTemplateResult,
|
||||
|
|
|
@ -1,17 +1,15 @@
|
|||
import type { SSRResult } from '../../../../@types/astro';
|
||||
import type { AstroComponentFactory, AstroFactoryReturnValue } from './factory.js';
|
||||
import type { renderTemplate } from './render-template.js';
|
||||
import type { ComponentSlots } from '../slot.js';
|
||||
|
||||
import { HydrationDirectiveProps } from '../../hydration.js';
|
||||
import { isPromise } from '../../util.js';
|
||||
import { renderChild } from '../any.js';
|
||||
import { isAPropagatingComponent } from './factory.js';
|
||||
import { isHeadAndContent } from './head-and-content.js';
|
||||
import { createScopedResult, ScopeFlags } from '../scope.js';
|
||||
|
||||
type ComponentProps = Record<string | number, any>;
|
||||
type ComponentSlotValue = () => ReturnType<typeof renderTemplate>;
|
||||
export type ComponentSlots = Record<string, ComponentSlotValue>;
|
||||
export type ComponentSlotsWithValues = Record<string, ReturnType<ComponentSlotValue>>;
|
||||
|
||||
const astroComponentInstanceSym = Symbol.for('astro.componentInstance');
|
||||
|
||||
|
@ -20,7 +18,7 @@ export class AstroComponentInstance {
|
|||
|
||||
private readonly result: SSRResult;
|
||||
private readonly props: ComponentProps;
|
||||
private readonly slotValues: ComponentSlotsWithValues;
|
||||
private readonly slotValues: ComponentSlots;
|
||||
private readonly factory: AstroComponentFactory;
|
||||
private returnValue: ReturnType<AstroComponentFactory> | undefined;
|
||||
constructor(
|
||||
|
@ -33,19 +31,21 @@ export class AstroComponentInstance {
|
|||
this.props = props;
|
||||
this.factory = factory;
|
||||
this.slotValues = {};
|
||||
const scoped = createScopedResult(result, ScopeFlags.Slot);
|
||||
for (const name in slots) {
|
||||
this.slotValues[name] = slots[name]();
|
||||
const value = slots[name](scoped);
|
||||
this.slotValues[name] = () => value;
|
||||
}
|
||||
}
|
||||
|
||||
async init() {
|
||||
this.returnValue = this.factory(this.result, this.props, this.slotValues);
|
||||
async init(result: SSRResult) {
|
||||
this.returnValue = this.factory(result, this.props, this.slotValues);
|
||||
return this.returnValue;
|
||||
}
|
||||
|
||||
async *render() {
|
||||
if (this.returnValue === undefined) {
|
||||
await this.init();
|
||||
await this.init(this.result);
|
||||
}
|
||||
|
||||
let value: AstroFactoryReturnValue | undefined = this.returnValue;
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
} from '../scripts.js';
|
||||
import { renderAllHeadContent } from './head.js';
|
||||
import { isSlotString, type SlotString } from './slot.js';
|
||||
import { ScopeFlags } from './scope.js';
|
||||
|
||||
export const Fragment = Symbol.for('astro:fragment');
|
||||
export const Renderer = Symbol.for('astro:renderer');
|
||||
|
@ -48,6 +49,30 @@ export function stringifyChunk(result: SSRResult, chunk: string | SlotString | R
|
|||
}
|
||||
return renderAllHeadContent(result);
|
||||
}
|
||||
case 'maybe-head': {
|
||||
if (result._metadata.hasRenderedHead) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const scope = instruction.scope;
|
||||
switch (scope) {
|
||||
// JSX with an Astro slot
|
||||
case ScopeFlags.JSX | ScopeFlags.Slot | ScopeFlags.Astro:
|
||||
case ScopeFlags.JSX | ScopeFlags.Astro | ScopeFlags.HeadBuffer:
|
||||
case ScopeFlags.JSX | ScopeFlags.Slot | ScopeFlags.Astro | ScopeFlags.HeadBuffer: {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Astro.slots.render('default') should never render head content.
|
||||
case ScopeFlags.RenderSlot | ScopeFlags.Astro:
|
||||
case ScopeFlags.RenderSlot | ScopeFlags.Astro | ScopeFlags.JSX:
|
||||
case ScopeFlags.RenderSlot | ScopeFlags.Astro | ScopeFlags.JSX | ScopeFlags.HeadBuffer: {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
return renderAllHeadContent(result);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (isSlotString(chunk as string)) {
|
||||
|
|
|
@ -17,7 +17,7 @@ import {
|
|||
} from './astro/index.js';
|
||||
import { Fragment, Renderer, stringifyChunk } from './common.js';
|
||||
import { componentIsHTMLElement, renderHTMLElement } from './dom.js';
|
||||
import { renderSlot, renderSlots } from './slot.js';
|
||||
import { ComponentSlots, renderSlot, renderSlots } from './slot.js';
|
||||
import { formatList, internalSpreadAttributes, renderElement, voidElementNames } from './util.js';
|
||||
|
||||
const rendererAliases = new Map([['solid', 'solid-js']]);
|
||||
|
@ -331,7 +331,7 @@ function sanitizeElementName(tag: string) {
|
|||
return tag.trim().split(unsafe)[0].trim();
|
||||
}
|
||||
|
||||
async function renderFragmentComponent(result: SSRResult, slots: any = {}) {
|
||||
async function renderFragmentComponent(result: SSRResult, slots: ComponentSlots = {}) {
|
||||
const children = await renderSlot(result, slots?.default);
|
||||
if (children == null) {
|
||||
return children;
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import type { SSRResult } from '../../../@types/astro';
|
||||
|
||||
import { markHTMLString } from '../escape.js';
|
||||
import { renderElement, ScopeFlags } from './util.js';
|
||||
import { renderElement } from './util.js';
|
||||
import { ScopeFlags } from './scope.js';
|
||||
|
||||
// Filter out duplicate elements in our set
|
||||
const uniqueElements = (item: any, index: number, all: any[]) => {
|
||||
|
@ -52,15 +53,7 @@ export function* maybeRenderHead(result: SSRResult) {
|
|||
return;
|
||||
}
|
||||
|
||||
// Don't render the head inside of a JSX component that's inside of an Astro component
|
||||
// as the Astro component will be the one to render the head.
|
||||
switch (result.scope) {
|
||||
case ScopeFlags.JSX | ScopeFlags.Slot | ScopeFlags.Astro: {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// This is an instruction informing the page rendering that head might need rendering.
|
||||
// This allows the page to deduplicate head injections.
|
||||
yield { type: 'head', result } as const;
|
||||
yield { type: 'maybe-head', result, scope: result.scope } as const;
|
||||
}
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
export type {
|
||||
AstroComponentFactory,
|
||||
AstroComponentInstance,
|
||||
ComponentSlots as AstroComponentSlots,
|
||||
ComponentSlotsWithValues as AstroComponentSlotsWithValues,
|
||||
} from './astro/index';
|
||||
export {
|
||||
createHeadAndContent,
|
||||
|
@ -15,7 +13,8 @@ export { renderComponent, renderComponentToIterable } from './component.js';
|
|||
export { renderHTMLElement } from './dom.js';
|
||||
export { maybeRenderHead, renderHead } from './head.js';
|
||||
export { renderPage } from './page.js';
|
||||
export { renderSlot } from './slot.js';
|
||||
export { renderSlot, type ComponentSlots } from './slot.js';
|
||||
export { createScopedResult, ScopeFlags, addScopeFlag, removeScopeFlag } from './scope.js';
|
||||
export { renderScriptElement, renderStyleElement, renderUniqueStylesheet } from './tags.js';
|
||||
export type { RenderInstruction } from './types';
|
||||
export { addAttribute, defineScriptVars, voidElementNames } from './util.js';
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
import { chunkToByteArray, encoder, HTMLParts } from './common.js';
|
||||
import { renderComponent } from './component.js';
|
||||
import { maybeRenderHead } from './head.js';
|
||||
import { addScopeFlag, createScopedResult, removeScopeFlag, ScopeFlags } from './scope.js';
|
||||
|
||||
const needsHeadRenderingSymbol = Symbol.for('astro.needsHeadRendering');
|
||||
|
||||
|
@ -55,12 +56,13 @@ async function iterableToHTMLBytes(
|
|||
// to be propagated up.
|
||||
async function bufferHeadContent(result: SSRResult) {
|
||||
const iterator = result.propagators.values();
|
||||
const scoped = createScopedResult(result, ScopeFlags.HeadBuffer);
|
||||
while (true) {
|
||||
const { value, done } = iterator.next();
|
||||
if (done) {
|
||||
break;
|
||||
}
|
||||
const returnValue = await value.init();
|
||||
const returnValue = await value.init(scoped);
|
||||
if (isHeadAndContent(returnValue)) {
|
||||
result.extraHead.push(returnValue.head);
|
||||
}
|
||||
|
|
32
packages/astro/src/runtime/server/render/scope.ts
Normal file
32
packages/astro/src/runtime/server/render/scope.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
import type { SSRResult } from '../../../@types/astro';
|
||||
|
||||
export const ScopeFlags = {
|
||||
Astro: 1 << 0, // 1
|
||||
JSX: 1 << 1, // 2
|
||||
Slot: 1 << 2, // 4
|
||||
HeadBuffer: 1 << 3, // 8
|
||||
RenderSlot: 1 << 4, // 16
|
||||
} as const;
|
||||
|
||||
type ScopeFlagValues = (typeof ScopeFlags)[keyof typeof ScopeFlags];
|
||||
|
||||
export function addScopeFlag(result: SSRResult, flag: ScopeFlagValues) {
|
||||
result.scope |= flag;
|
||||
}
|
||||
|
||||
export function removeScopeFlag(result: SSRResult, flag: ScopeFlagValues) {
|
||||
result.scope &= ~flag;
|
||||
}
|
||||
|
||||
export function createScopedResult(result: SSRResult, flag?: ScopeFlagValues): SSRResult {
|
||||
const scopedResult = Object.create(result, {
|
||||
scope: {
|
||||
writable: true,
|
||||
value: result.scope
|
||||
}
|
||||
});
|
||||
if(flag != null) {
|
||||
addScopeFlag(scopedResult, flag);
|
||||
}
|
||||
return scopedResult;
|
||||
}
|
|
@ -1,9 +1,14 @@
|
|||
import type { SSRResult } from '../../../@types/astro.js';
|
||||
import type { RenderInstruction } from './types.js';
|
||||
import type { renderTemplate } from './astro/render-template.js';
|
||||
|
||||
import { HTMLString, markHTMLString } from '../escape.js';
|
||||
import { renderChild } from './any.js';
|
||||
import { ScopeFlags } from './util.js';
|
||||
import { ScopeFlags, createScopedResult } from './scope.js';
|
||||
|
||||
type RenderTemplateResult = ReturnType<typeof renderTemplate>;
|
||||
export type ComponentSlots = Record<string, ComponentSlotValue>;
|
||||
export type ComponentSlotValue = (result: SSRResult) => RenderTemplateResult;
|
||||
|
||||
const slotString = Symbol.for('astro:slot-string');
|
||||
|
||||
|
@ -23,12 +28,12 @@ export function isSlotString(str: string): str is any {
|
|||
|
||||
export async function renderSlot(
|
||||
result: SSRResult,
|
||||
slotted: string,
|
||||
fallback?: any
|
||||
slotted: ComponentSlotValue | RenderTemplateResult,
|
||||
fallback?: ComponentSlotValue | RenderTemplateResult
|
||||
): Promise<string> {
|
||||
if (slotted) {
|
||||
result.scope |= ScopeFlags.Slot;
|
||||
let iterator = renderChild(slotted);
|
||||
const scoped = createScopedResult(result, ScopeFlags.Slot);
|
||||
let iterator = renderChild(typeof slotted === 'function' ? slotted(scoped) : slotted);
|
||||
let content = '';
|
||||
let instructions: null | RenderInstruction[] = null;
|
||||
for await (const chunk of iterator) {
|
||||
|
@ -41,11 +46,13 @@ export async function renderSlot(
|
|||
content += chunk;
|
||||
}
|
||||
}
|
||||
// Remove the flag since we are now outside of the scope.
|
||||
result.scope &= ~ScopeFlags.Slot;
|
||||
return markHTMLString(new SlotString(content, instructions));
|
||||
}
|
||||
return fallback;
|
||||
|
||||
if(fallback) {
|
||||
return renderSlot(result, fallback);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
interface RenderSlotsResult {
|
||||
|
@ -53,13 +60,13 @@ interface RenderSlotsResult {
|
|||
children: Record<string, string>;
|
||||
}
|
||||
|
||||
export async function renderSlots(result: SSRResult, slots: any = {}): Promise<RenderSlotsResult> {
|
||||
export async function renderSlots(result: SSRResult, slots: ComponentSlots = {}): Promise<RenderSlotsResult> {
|
||||
let slotInstructions: RenderSlotsResult['slotInstructions'] = null;
|
||||
let children: RenderSlotsResult['children'] = {};
|
||||
if (slots) {
|
||||
await Promise.all(
|
||||
Object.entries(slots).map(([key, value]) =>
|
||||
renderSlot(result, value as string).then((output: any) => {
|
||||
renderSlot(result, value).then((output: any) => {
|
||||
if (output.instructions) {
|
||||
if (slotInstructions === null) {
|
||||
slotInstructions = [];
|
||||
|
|
|
@ -12,4 +12,10 @@ export type RenderHeadInstruction = {
|
|||
result: SSRResult;
|
||||
};
|
||||
|
||||
export type RenderInstruction = RenderDirectiveInstruction | RenderHeadInstruction;
|
||||
export type MaybeRenderHeadInstruction = {
|
||||
type: 'maybe-head';
|
||||
result: SSRResult;
|
||||
scope: number;
|
||||
}
|
||||
|
||||
export type RenderInstruction = RenderDirectiveInstruction | RenderHeadInstruction | MaybeRenderHeadInstruction;
|
||||
|
|
|
@ -128,9 +128,3 @@ export function renderElement(
|
|||
}
|
||||
return `<${name}${internalSpreadAttributes(props, shouldEscape)}>${children}</${name}>`;
|
||||
}
|
||||
|
||||
export const ScopeFlags = {
|
||||
Astro: 1 << 0,
|
||||
JSX: 1 << 1,
|
||||
Slot: 1 << 2,
|
||||
};
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
import type { ModuleInfo } from 'rollup';
|
||||
import type { AstroSettings } from '../@types/astro';
|
||||
import type { AstroSettings, SSRResult } from '../@types/astro';
|
||||
import type { BuildInternals } from '../core/build/internal.js';
|
||||
import type { AstroBuildPlugin } from '../core/build/plugin.js';
|
||||
import type { StaticBuildOptions } from '../core/build/types';
|
||||
|
||||
import * as vite from 'vite';
|
||||
import { getAstroMetadata } from '../vite-plugin-astro/index.js';
|
||||
import { walkParentInfos } from '../core/build/graph.js';
|
||||
|
||||
const injectExp = /^\/\/\s*astro-head-inject/;
|
||||
/**
|
||||
|
@ -59,3 +63,50 @@ export default function configHeadPropagationVitePlugin({
|
|||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function astroHeadPropagationBuildPlugin(
|
||||
options: StaticBuildOptions,
|
||||
internals: BuildInternals
|
||||
): AstroBuildPlugin {
|
||||
return {
|
||||
build: 'ssr',
|
||||
hooks: {
|
||||
'build:before'() {
|
||||
const map: SSRResult['propagation'] = new Map();
|
||||
return {
|
||||
vitePlugin: {
|
||||
name: 'vite-plugin-head-propagation-build',
|
||||
generateBundle(_opts, bundle) {
|
||||
const appendPropagation = (info: ModuleInfo) => {
|
||||
const astroMetadata = getAstroMetadata(info);
|
||||
if(astroMetadata) {
|
||||
astroMetadata.propagation = 'in-tree';
|
||||
map.set(info.id, 'in-tree');
|
||||
}
|
||||
};
|
||||
|
||||
for(const [bundleId, output] of Object.entries(bundle)) {
|
||||
if(output.type !== 'chunk') continue;
|
||||
for(const [id, mod] of Object.entries(output.modules)) {
|
||||
if (mod.code && injectExp.test(mod.code)) {
|
||||
for(const [info] of walkParentInfos(id, this)) {
|
||||
appendPropagation(info);
|
||||
}
|
||||
}
|
||||
|
||||
const info = this.getModuleInfo(id);
|
||||
if(info) {
|
||||
appendPropagation(info);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save the map to internals so it can be passed into SSR and generation
|
||||
internals.propagation = map;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,5 +29,32 @@ describe('Head injection w/ MDX', () => {
|
|||
const scripts = document.querySelectorAll('head script[type=module]');
|
||||
expect(scripts).to.have.a.lengthOf(1);
|
||||
});
|
||||
|
||||
it('injects into the head for content collections', async () => {
|
||||
const html = await fixture.readFile('/posts/test/index.html');
|
||||
const { document } = parseHTML(html);
|
||||
|
||||
const links = document.querySelectorAll('head link[rel=stylesheet]');
|
||||
expect(links).to.have.a.lengthOf(1);
|
||||
});
|
||||
|
||||
it('injects content from a component using Content#render()', async () => {
|
||||
const html = await fixture.readFile('/DirectContentUsage/index.html');
|
||||
const { document } = parseHTML(html);
|
||||
|
||||
const links = document.querySelectorAll('head link[rel=stylesheet]');
|
||||
expect(links).to.have.a.lengthOf(1);
|
||||
|
||||
const scripts = document.querySelectorAll('head script[type=module]');
|
||||
expect(scripts).to.have.a.lengthOf(2);
|
||||
});
|
||||
|
||||
it('Using component using slots.render() API', async () => {
|
||||
const html = await fixture.readFile('/remote/index.html');
|
||||
const { document } = parseHTML(html);
|
||||
|
||||
const links = document.querySelectorAll('head link[rel=stylesheet]');
|
||||
expect(links).to.have.a.lengthOf(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
10
packages/integrations/mdx/test/fixtures/css-head-mdx/package.json
vendored
Normal file
10
packages/integrations/mdx/test/fixtures/css-head-mdx/package.json
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"name": "@test/mdx-css-head-mdx",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"astro": "workspace:*",
|
||||
"@astrojs/mdx": "workspace:*",
|
||||
"astro-remote": "0.2.3"
|
||||
}
|
||||
}
|
3
packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/P.astro
vendored
Normal file
3
packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/P.astro
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
<p>
|
||||
<slot />
|
||||
</p>
|
3
packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/SmallCaps.astro
vendored
Normal file
3
packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/SmallCaps.astro
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
---
|
||||
---
|
||||
<span style={{fontVariant: "small-caps"}}><slot /></span>
|
8
packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/UsingMdx.astro
vendored
Normal file
8
packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/UsingMdx.astro
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
import { getEntryBySlug } from 'astro:content';
|
||||
|
||||
const launchWeek = await getEntryBySlug('blog', 'using-mdx');
|
||||
const { Content } = await launchWeek.render();
|
||||
---
|
||||
|
||||
<Content />
|
6
packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/WithHoistedScripts.astro
vendored
Normal file
6
packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/WithHoistedScripts.astro
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
---
|
||||
|
||||
<script>
|
||||
console.log('hoisted')
|
||||
</script>
|
3
packages/integrations/mdx/test/fixtures/css-head-mdx/src/content/blog/_styles.css
vendored
Normal file
3
packages/integrations/mdx/test/fixtures/css-head-mdx/src/content/blog/_styles.css
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
body {
|
||||
color: red !important;
|
||||
}
|
6
packages/integrations/mdx/test/fixtures/css-head-mdx/src/content/blog/using-mdx.mdx
vendored
Normal file
6
packages/integrations/mdx/test/fixtures/css-head-mdx/src/content/blog/using-mdx.mdx
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
import './_styles.css';
|
||||
import WithHoistedScripts from '../../components/WithHoistedScripts.astro';
|
||||
|
||||
# Using mdx
|
||||
|
||||
<WithHoistedScripts />
|
5
packages/integrations/mdx/test/fixtures/css-head-mdx/src/content/posts/test.mdx
vendored
Normal file
5
packages/integrations/mdx/test/fixtures/css-head-mdx/src/content/posts/test.mdx
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Testing
|
||||
---
|
||||
|
||||
<SmallCaps>A test file</SmallCaps>
|
24
packages/integrations/mdx/test/fixtures/css-head-mdx/src/layouts/ContentLayout.astro
vendored
Normal file
24
packages/integrations/mdx/test/fixtures/css-head-mdx/src/layouts/ContentLayout.astro
vendored
Normal file
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
export interface Props {
|
||||
title: string;
|
||||
}
|
||||
|
||||
const { title } = Astro.props;
|
||||
---
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<title>{title}</title>
|
||||
<style is:global>
|
||||
@import "../styles/global.css";
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<slot />
|
||||
</body>
|
||||
</html>
|
17
packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/DirectContentUsage.astro
vendored
Normal file
17
packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/DirectContentUsage.astro
vendored
Normal file
|
@ -0,0 +1,17 @@
|
|||
---
|
||||
import UsingMdx from '../components/UsingMdx.astro'
|
||||
---
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<title>Astro</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Astro</h1>
|
||||
<UsingMdx />
|
||||
</body>
|
||||
</html>
|
18
packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/posts/[post].astro
vendored
Normal file
18
packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/posts/[post].astro
vendored
Normal file
|
@ -0,0 +1,18 @@
|
|||
---
|
||||
import { getCollection } from 'astro:content';
|
||||
import Layout from '../../layouts/ContentLayout.astro';
|
||||
import SmallCaps from '../../components/SmallCaps.astro';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const entries = await getCollection('posts');
|
||||
return entries.map(entry => {
|
||||
return {params: { post: entry.slug }, props: { entry },
|
||||
}});
|
||||
}
|
||||
|
||||
const { entry } = Astro.props;
|
||||
const { Content } = await entry.render();
|
||||
---
|
||||
<Layout title="">
|
||||
<Content components={{ SmallCaps }} />
|
||||
</Layout>
|
17
packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/remote.astro
vendored
Normal file
17
packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/remote.astro
vendored
Normal file
|
@ -0,0 +1,17 @@
|
|||
---
|
||||
import '../styles/global.css'
|
||||
import Layout from '../layouts/One.astro';
|
||||
import Paragraph from '../components/P.astro';
|
||||
import { Markdown } from 'astro-remote'
|
||||
---
|
||||
|
||||
<Layout title="Welcome to Astro.">
|
||||
<main>
|
||||
<Markdown
|
||||
components={{
|
||||
p: Paragraph,
|
||||
}}>
|
||||
**Removing p component fixes the problem**
|
||||
</Markdown>
|
||||
</main>
|
||||
</Layout>
|
3
packages/integrations/mdx/test/fixtures/css-head-mdx/src/styles/global.css
vendored
Normal file
3
packages/integrations/mdx/test/fixtures/css-head-mdx/src/styles/global.css
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
html {
|
||||
font-weight: bolder;
|
||||
}
|
|
@ -375,7 +375,7 @@ importers:
|
|||
|
||||
packages/astro:
|
||||
specifiers:
|
||||
'@astrojs/compiler': ^1.0.1
|
||||
'@astrojs/compiler': ^1.1.0
|
||||
'@astrojs/language-server': ^0.28.3
|
||||
'@astrojs/markdown-remark': ^2.0.1
|
||||
'@astrojs/telemetry': ^2.0.0
|
||||
|
@ -465,7 +465,7 @@ importers:
|
|||
yargs-parser: ^21.0.1
|
||||
zod: ^3.17.3
|
||||
dependencies:
|
||||
'@astrojs/compiler': 1.0.1
|
||||
'@astrojs/compiler': 1.1.0
|
||||
'@astrojs/language-server': 0.28.3
|
||||
'@astrojs/markdown-remark': link:../markdown/remark
|
||||
'@astrojs/telemetry': link:../telemetry
|
||||
|
@ -2924,6 +2924,16 @@ importers:
|
|||
remark-toc: 8.0.1
|
||||
vite: 4.1.1
|
||||
|
||||
packages/integrations/mdx/test/fixtures/css-head-mdx:
|
||||
specifiers:
|
||||
'@astrojs/mdx': workspace:*
|
||||
astro: workspace:*
|
||||
astro-remote: 0.2.3
|
||||
dependencies:
|
||||
'@astrojs/mdx': link:../../..
|
||||
astro: link:../../../../../astro
|
||||
astro-remote: 0.2.3
|
||||
|
||||
packages/integrations/mdx/test/fixtures/mdx-frontmatter-injection:
|
||||
specifiers:
|
||||
'@astrojs/mdx': workspace:*
|
||||
|
@ -3863,8 +3873,8 @@ packages:
|
|||
/@astrojs/compiler/0.31.4:
|
||||
resolution: {integrity: sha512-6bBFeDTtPOn4jZaiD3p0f05MEGQL9pw2Zbfj546oFETNmjJFWO3nzHz6/m+P53calknCvyVzZ5YhoBLIvzn5iw==}
|
||||
|
||||
/@astrojs/compiler/1.0.1:
|
||||
resolution: {integrity: sha512-77aacobLKcL98NmhK3OBS5EHIrX9gs1ckB/vGSIdkVZuB7u51V4jh05I6W0tSvG7/86tALv6QtHTRZ8rLhFTbQ==}
|
||||
/@astrojs/compiler/1.1.0:
|
||||
resolution: {integrity: sha512-C4kTwirys+HafufMqaxCbML2wqkGaXJM+5AekXh/v1IIOnMIdcEON9GBYsG6qa8aAmLhZ58aUZGPhzcA3Dx7Uw==}
|
||||
dev: false
|
||||
|
||||
/@astrojs/language-server/0.28.3:
|
||||
|
@ -7843,6 +7853,14 @@ packages:
|
|||
astro: link:packages/astro
|
||||
dev: false
|
||||
|
||||
/astro-remote/0.2.3:
|
||||
resolution: {integrity: sha512-vsY736YjWhpFgx4KUxCBdK0QJmOk0W61VQwO7v6qmfGdIxZyx6N7hBNou57w2mw68hQSe5AbRs602pi05GDMHw==}
|
||||
dependencies:
|
||||
he: 1.2.0
|
||||
marked: 4.2.12
|
||||
ultrahtml: 0.1.3
|
||||
dev: false
|
||||
|
||||
/async-sema/3.1.1:
|
||||
resolution: {integrity: sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg==}
|
||||
dev: false
|
||||
|
@ -11285,6 +11303,12 @@ packages:
|
|||
resolution: {integrity: sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw==}
|
||||
dev: false
|
||||
|
||||
/marked/4.2.12:
|
||||
resolution: {integrity: sha512-yr8hSKa3Fv4D3jdZmtMMPghgVt6TWbk86WQaWhDloQjRSQhMMYCAro7jP7VDJrjjdV8pxVxMssXS8B8Y5DZ5aw==}
|
||||
engines: {node: '>= 12'}
|
||||
hasBin: true
|
||||
dev: false
|
||||
|
||||
/matcher/3.0.0:
|
||||
resolution: {integrity: sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==}
|
||||
engines: {node: '>=10'}
|
||||
|
@ -14742,6 +14766,10 @@ packages:
|
|||
resolution: {integrity: sha512-o0QVGuFg24FK765Qdd5kk0zU/U4dEsCtN/GSiwNI9i8xsSVtjIAOdTaVhLwZ1nrbWxFVMxNDDl+9fednsOMsBw==}
|
||||
dev: true
|
||||
|
||||
/ultrahtml/0.1.3:
|
||||
resolution: {integrity: sha512-P24ulZdT9UKyQuKA1IApdAZ+F9lwruGvmKb4pG3+sMvR3CjN0pjawPnxuSABHQFB+XqnB35TVXzJPOBYjCv6Kw==}
|
||||
dev: false
|
||||
|
||||
/unbox-primitive/1.0.2:
|
||||
resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==}
|
||||
dependencies:
|
||||
|
|
Loading…
Reference in a new issue