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:
Matthew Phillips 2023-02-07 10:56:32 -05:00 committed by GitHub
parent cee70f5c6a
commit d1f5611feb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 386 additions and 68 deletions

View 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.

View file

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

View file

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

View file

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

View file

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

View file

@ -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> = (

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

@ -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 = [];

View file

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

View file

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

View file

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

View file

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

View 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"
}
}

View file

@ -0,0 +1,3 @@
<p>
<slot />
</p>

View file

@ -0,0 +1,3 @@
---
---
<span style={{fontVariant: "small-caps"}}><slot /></span>

View file

@ -0,0 +1,8 @@
---
import { getEntryBySlug } from 'astro:content';
const launchWeek = await getEntryBySlug('blog', 'using-mdx');
const { Content } = await launchWeek.render();
---
<Content />

View file

@ -0,0 +1,6 @@
---
---
<script>
console.log('hoisted')
</script>

View file

@ -0,0 +1,3 @@
body {
color: red !important;
}

View file

@ -0,0 +1,6 @@
import './_styles.css';
import WithHoistedScripts from '../../components/WithHoistedScripts.astro';
# Using mdx
<WithHoistedScripts />

View file

@ -0,0 +1,5 @@
---
title: Testing
---
<SmallCaps>A test file</SmallCaps>

View 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>

View 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>

View 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>

View 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>

View file

@ -0,0 +1,3 @@
html {
font-weight: bolder;
}

View file

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