Fix head injection misplacement with Astro.slots.render() (#6196)

* Fix head injection misplacement with Astro.slots.render()

* Adding a changeset

* Fix case of JSX with no layout

* missing break
This commit is contained in:
Matthew Phillips 2023-02-09 11:50:04 -05:00 committed by GitHub
parent 0c3485ab07
commit 3390cb8444
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 216 additions and 32 deletions

View file

@ -0,0 +1,5 @@
---
'astro': patch
---
Fix head injection misplacement with Astro.slots.render()

View file

@ -9,7 +9,7 @@ import {
PrescriptType, PrescriptType,
} from '../scripts.js'; } from '../scripts.js';
import { renderAllHeadContent } from './head.js'; import { renderAllHeadContent } from './head.js';
import { ScopeFlags } from './scope.js'; import { hasScopeFlag, ScopeFlags } from './scope.js';
import { isSlotString, type SlotString } from './slot.js'; import { isSlotString, type SlotString } from './slot.js';
export const Fragment = Symbol.for('astro:fragment'); export const Fragment = Symbol.for('astro:fragment');
@ -63,7 +63,23 @@ export function stringifyChunk(result: SSRResult, chunk: string | SlotString | R
return ''; return '';
} }
// Astro.slots.render('default') should never render head content. // 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: {
if(hasScopeFlag(result, ScopeFlags.RenderSlot)) {
return '';
}
break;
}
// Astro.slots.render() should never render head content.
case ScopeFlags.RenderSlot | ScopeFlags.Astro: case ScopeFlags.RenderSlot | ScopeFlags.Astro:
case ScopeFlags.RenderSlot | ScopeFlags.Astro | ScopeFlags.JSX: case ScopeFlags.RenderSlot | ScopeFlags.Astro | ScopeFlags.JSX:
case ScopeFlags.RenderSlot | ScopeFlags.Astro | ScopeFlags.JSX | ScopeFlags.HeadBuffer: { case ScopeFlags.RenderSlot | ScopeFlags.Astro | ScopeFlags.JSX | ScopeFlags.HeadBuffer: {

View file

@ -18,6 +18,10 @@ export function removeScopeFlag(result: SSRResult, flag: ScopeFlagValues) {
result.scope &= ~flag; result.scope &= ~flag;
} }
export function hasScopeFlag(result: SSRResult, flag: ScopeFlagValues) {
return (result.scope & flag) === flag;
}
export function createScopedResult(result: SSRResult, flag?: ScopeFlagValues): SSRResult { export function createScopedResult(result: SSRResult, flag?: ScopeFlagValues): SSRResult {
const scopedResult = Object.create(result, { const scopedResult = Object.create(result, {
scope: { scope: {

View file

@ -4,7 +4,7 @@ import type { RenderInstruction } from './types.js';
import { HTMLString, markHTMLString } from '../escape.js'; import { HTMLString, markHTMLString } from '../escape.js';
import { renderChild } from './any.js'; import { renderChild } from './any.js';
import { createScopedResult, ScopeFlags } from './scope.js'; import { createScopedResult, hasScopeFlag, ScopeFlags } from './scope.js';
type RenderTemplateResult = ReturnType<typeof renderTemplate>; type RenderTemplateResult = ReturnType<typeof renderTemplate>;
export type ComponentSlots = Record<string, ComponentSlotValue>; export type ComponentSlots = Record<string, ComponentSlotValue>;

View file

@ -1,5 +1,5 @@
{ {
"name": "@test/head-injection-md", "name": "@test/head-injection",
"version": "0.0.0", "version": "0.0.0",
"private": true, "private": true,
"dependencies": { "dependencies": {

View file

@ -0,0 +1,8 @@
<style>
div {
font-weight: bolder;
}
</style>
<div>
<slot />
</div>

View file

@ -0,0 +1,12 @@
---
const html = await Astro.slots.render('slot-name');
---
<div class="p-sample">
<Fragment set:html={html} />
</div>
<style>
.p-sample {
color: red;
}
</style>

View file

@ -0,0 +1,7 @@
<!DOCTYPE html>
<html>
<head> </head>
<body>
<slot />
</body>
</html>

View file

@ -0,0 +1,25 @@
---
export interface Props {
title: string;
subtitle: string;
content?: string;
}
const {
title,
subtitle = await Astro.slots.render("subtitle"),
content = await Astro.slots.render("content"),
} = Astro.props;
---
<style>
section {
background: slategrey;
}
</style>
<section>
<div>
{title && <h1>{title}</h1>}
{subtitle && <p set:html={subtitle} />}
{content && <div set:html={content} />}
</div>
</section>

View file

@ -0,0 +1,24 @@
---
import Layout from '../components/Layout.astro';
import SlotsRender from '../components/SlotsRender.astro';
---
<Layout>
<SlotsRender
title="Lorem ipsum lorem"
subtitle="At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti"
>
<Fragment slot="content">
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore
magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat.
</p>
<p class="mt-4">
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur
sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
</p>
</Fragment>
</SlotsRender>
</Layout>

View file

@ -0,0 +1,11 @@
---
import Layout from "../components/SlotRenderLayout.astro";
import RegularSlot from "../components/RegularSlot.astro"
---
<Layout>
<RegularSlot>
<RegularSlot>
<p slot="slot-name">Paragraph.</p>
</RegularSlot>
</RegularSlot>
</Layout>

View file

@ -0,0 +1,9 @@
---
import Layout from "../components/SlotRenderLayout.astro";
import Component from "../components/SlotRenderComponent.astro"
---
<Layout>
<Component>
<p slot="slot-name">Paragraph.</p>
</Component>
</Layout>

View file

@ -1,27 +0,0 @@
import { expect } from 'chai';
import * as cheerio from 'cheerio';
import { loadFixture } from './test-utils.js';
describe('Head injection with markdown', () => {
/** @type {import('./test-utils').Fixture} */
let fixture;
before(async () => {
fixture = await loadFixture({
root: './fixtures/head-injection-md/',
});
});
describe('build', () => {
before(async () => {
await fixture.build();
});
it('only injects head content once', async () => {
const html = await fixture.readFile(`/index.html`);
const $ = cheerio.load(html);
expect($('link[rel=stylesheet]')).to.have.a.lengthOf(1);
});
});
});

View file

@ -0,0 +1,55 @@
import { expect } from 'chai';
import * as cheerio from 'cheerio';
import { loadFixture } from './test-utils.js';
describe('Head injection', () => {
/** @type {import('./test-utils').Fixture} */
let fixture;
before(async () => {
fixture = await loadFixture({
root: './fixtures/head-injection/',
});
});
describe('build', () => {
before(async () => {
await fixture.build();
});
describe('Markdown', () => {
it('only injects head content once', async () => {
const html = await fixture.readFile(`/index.html`);
const $ = cheerio.load(html);
expect($('head link[rel=stylesheet]')).to.have.a.lengthOf(1);
});
});
describe('Astro components', () => {
it('Using slots within slots', async () => {
const html = await fixture.readFile('/with-slot-in-slot/index.html');
const $ = cheerio.load(html);
expect($('head link[rel=stylesheet]')).to.have.a.lengthOf(1);
expect($('body link[rel=stylesheet]')).to.have.a.lengthOf(0);
});
it('Using slots with Astro.slots.render()', async () => {
const html = await fixture.readFile('/with-slot-render/index.html');
const $ = cheerio.load(html);
expect($('head link[rel=stylesheet]')).to.have.a.lengthOf(1);
expect($('body link[rel=stylesheet]')).to.have.a.lengthOf(0);
});
it('Using slots within slots using Astro.slots.render()', async () => {
const html = await fixture.readFile('/with-slot-in-render-slot/index.html');
const $ = cheerio.load(html);
expect($('head link[rel=stylesheet]')).to.have.a.lengthOf(2);
expect($('body link[rel=stylesheet]')).to.have.a.lengthOf(0);
});
});
});
});

View file

@ -3,6 +3,7 @@ import mdx from '@astrojs/mdx';
import { expect } from 'chai'; import { expect } from 'chai';
import { parseHTML } from 'linkedom'; import { parseHTML } from 'linkedom';
import { loadFixture } from '../../../astro/test/test-utils.js'; import { loadFixture } from '../../../astro/test/test-utils.js';
import * as cheerio from 'cheerio';
describe('Head injection w/ MDX', () => { describe('Head injection w/ MDX', () => {
let fixture; let fixture;
@ -56,5 +57,17 @@ describe('Head injection w/ MDX', () => {
const links = document.querySelectorAll('head link[rel=stylesheet]'); const links = document.querySelectorAll('head link[rel=stylesheet]');
expect(links).to.have.a.lengthOf(1); expect(links).to.have.a.lengthOf(1);
}); });
it('Using component but no layout', async () => {
const html = await fixture.readFile('/noLayoutWithComponent/index.html');
// Using cheerio here because linkedom doesn't support head tag injection
const $ = cheerio.load(html);
const headLinks = $('head link[rel=stylesheet]');
expect(headLinks).to.have.a.lengthOf(1);
const bodyLinks = $('body link[rel=stylesheet]');
expect(bodyLinks).to.have.a.lengthOf(0);
});
}); });
}); });

View file

@ -0,0 +1,22 @@
---
title: 'Lorem'
description: 'Lorem ipsum dolor sit amet'
pubDate: 'Jul 02 2022'
---
import MyComponent from '../components/HelloWorld.astro';
## Lorem
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
## Lorem 2
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
<MyComponent />
## Lorem 3
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

View file

@ -1849,7 +1849,7 @@ importers:
dependencies: dependencies:
astro: link:../../.. astro: link:../../..
packages/astro/test/fixtures/head-injection-md: packages/astro/test/fixtures/head-injection:
specifiers: specifiers:
astro: workspace:* astro: workspace:*
dependencies: dependencies: