Skip to content

Commit

Permalink
Add additional scoping for head buffering
Browse files Browse the repository at this point in the history
  • Loading branch information
matthewp committed Feb 6, 2023
1 parent db2c59f commit c9dcdd3
Show file tree
Hide file tree
Showing 17 changed files with 118 additions and 18 deletions.
3 changes: 2 additions & 1 deletion packages/astro/src/content/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { prependForwardSlash } from '../core/path.js';
import {
createComponent,
createHeadAndContent,
createScopedResult,
renderComponent,
renderScriptElement,
renderStyleElement,
Expand Down Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions packages/astro/src/runtime/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export { renderJSX } from './jsx.js';
export {
addAttribute,
createHeadAndContent,
createScopedResult,
defineScriptVars,
Fragment,
maybeRenderHead,
Expand Down
7 changes: 4 additions & 3 deletions packages/astro/src/runtime/server/jsx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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 '';
Expand Down
6 changes: 3 additions & 3 deletions packages/astro/src/runtime/server/render/astro/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions packages/astro/src/runtime/server/render/astro/instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { isPromise } from '../../util.js';
import { renderChild } from '../any.js';
import { isAPropagatingComponent } from './factory.js';
import { isHeadAndContent } from './head-and-content.js';
import { addScopeFlag, removeScopeFlag, ScopeFlags } from '../scope.js';

type ComponentProps = Record<string | number, any>;
type ComponentSlotValue = () => ReturnType<typeof renderTemplate>;
Expand All @@ -33,6 +34,7 @@ export class AstroComponentInstance {
this.props = props;
this.factory = factory;
this.slotValues = {};
addScopeFlag(result, ScopeFlags.Slot);
for (const name in slots) {
this.slotValues[name] = slots[name]();
}
Expand Down
8 changes: 6 additions & 2 deletions packages/astro/src/runtime/server/render/head.ts
Original file line number Diff line number Diff line change
@@ -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[]) => {
Expand Down Expand Up @@ -55,10 +56,13 @@ export function* maybeRenderHead(result: SSRResult) {
// 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: {
case ScopeFlags.JSX | ScopeFlags.Slot | ScopeFlags.Astro:
// TODO Explain
case ScopeFlags.JSX | ScopeFlags.Astro | ScopeFlags.HeadBuffer: {
return;
}
}


// This is an instruction informing the page rendering that head might need rendering.
// This allows the page to deduplicate head injections.
Expand Down
1 change: 1 addition & 0 deletions packages/astro/src/runtime/server/render/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export { renderHTMLElement } from './dom.js';
export { maybeRenderHead, renderHead } from './head.js';
export { renderPage } from './page.js';
export { renderSlot } from './slot.js';
export { createScopedResult } from './scope.js';
export { renderScriptElement, renderStyleElement, renderUniqueStylesheet } from './tags.js';
export type { RenderInstruction } from './types';
export { addAttribute, defineScriptVars, voidElementNames } from './util.js';
3 changes: 3 additions & 0 deletions packages/astro/src/runtime/server/render/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
import { chunkToByteArray, encoder, HTMLParts } from './common.js';
import { renderComponent } from './component.js';
import { maybeRenderHead } from './head.js';
import { addScopeFlag, removeScopeFlag, ScopeFlags } from './scope.js';

const needsHeadRenderingSymbol = Symbol.for('astro.needsHeadRendering');

Expand Down Expand Up @@ -55,6 +56,7 @@ async function iterableToHTMLBytes(
// to be propagated up.
async function bufferHeadContent(result: SSRResult) {
const iterator = result.propagators.values();
addScopeFlag(result, ScopeFlags.HeadBuffer);
while (true) {
const { value, done } = iterator.next();
if (done) {
Expand All @@ -65,6 +67,7 @@ async function bufferHeadContent(result: SSRResult) {
result.extraHead.push(returnValue.head);
}
}
removeScopeFlag(result, ScopeFlags.HeadBuffer);
}

export async function renderPage(
Expand Down
31 changes: 31 additions & 0 deletions packages/astro/src/runtime/server/render/scope.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
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
} 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;
}
7 changes: 4 additions & 3 deletions packages/astro/src/runtime/server/render/slot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { RenderInstruction } from './types.js';

import { HTMLString, markHTMLString } from '../escape.js';
import { renderChild } from './any.js';
import { ScopeFlags } from './util.js';
import { ScopeFlags, addScopeFlag, removeScopeFlag } from './scope.js';

const slotString = Symbol.for('astro:slot-string');

Expand All @@ -27,7 +27,7 @@ export async function renderSlot(
fallback?: any
): Promise<string> {
if (slotted) {
result.scope |= ScopeFlags.Slot;
addScopeFlag(result, ScopeFlags.Slot);
let iterator = renderChild(slotted);
let content = '';
let instructions: null | RenderInstruction[] = null;
Expand All @@ -42,7 +42,8 @@ export async function renderSlot(
}
}
// Remove the flag since we are now outside of the scope.
result.scope &= ~ScopeFlags.Slot;
removeScopeFlag(result, ScopeFlags.Slot);

return markHTMLString(new SlotString(content, instructions));
}
return fallback;
Expand Down
6 changes: 0 additions & 6 deletions packages/astro/src/runtime/server/render/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
8 changes: 8 additions & 0 deletions packages/integrations/mdx/test/css-head-mdx.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,13 @@ 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);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
---
---
<span style={{fontVariant: "small-caps"}}><slot /></span>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
title: Testing
---

<SmallCaps>A test file</SmallCaps>
Original file line number Diff line number Diff line change
@@ -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>
Original file line number Diff line number Diff line change
@@ -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>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
html {
font-weight: bolder;
}

0 comments on commit c9dcdd3

Please sign in to comment.