Compare commits

...

3 commits

Author SHA1 Message Date
Matthew Phillips
919e22d99a Add a head bubbling example 2023-02-17 12:20:15 -05:00
Matthew Phillips
9fcc9fb526 Finish proof of concept 2023-02-17 12:15:13 -05:00
Matthew Phillips
515b378e73 Head bubbling progress 2023-02-17 10:25:59 -05:00
13 changed files with 176 additions and 11 deletions

View file

@ -0,0 +1,15 @@
{
"name": "@example/head-bubbling",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
"astro": "^2.0.11"
}
}

View file

@ -0,0 +1,14 @@
---
const title = 'Blog Post';
const description = 'Some description';
---
<head>
<meta property="og:title" content={title} />
<meta property="og:description" content={description} /></head>
</head>
<article>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas eget commodo lacus. Sed hendrerit vel tortor in viverra. Praesent a lectus ex. Cras hendrerit ligula in sapien euismod maximus. Duis vel consectetur nibh, sed tempus est. Nullam sit amet iaculis nisl. Suspendisse efficitur libero eu magna varius faucibus a a nulla. Aliquam pretium diam ut bibendum convallis. Nullam vel mi nunc. Duis rutrum odio a magna posuere, in semper nisl dictum. Proin malesuada arcu in mi finibus, eget blandit urna varius. Ut pulvinar malesuada euismod.</p>
<p>Suspendisse sed erat neque. Donec est ante, venenatis ut urna a, efficitur finibus leo. Cras ut pellentesque mi. Sed non nunc tincidunt, euismod erat eu, fringilla augue. Nunc in sem a nisi luctus lacinia ut interdum turpis. Pellentesque at bibendum nunc. Etiam id felis in erat egestas ultrices. Proin cursus nulla et ligula elementum iaculis. Aenean volutpat ullamcorper purus, vitae elementum urna interdum at. Aenean ex elit, porta vitae dictum ac, mattis at justo. Aenean non augue tincidunt tellus aliquam condimentum. Nullam eu ante sed turpis lobortis iaculis.</p>
</article>

1
examples/head-bubbling/src/env.d.ts vendored Normal file
View file

@ -0,0 +1 @@
/// <reference types="astro/client" />

View file

@ -0,0 +1,22 @@
---
import Post from '../components/Post.astro';
---
<html>
<head>
<title>Index page</title>
<style>
body {
font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
}
h1 {
color: salmon;
}
</style>
</head>
<body>
<h1>My Site</h1>
<Post />
</body>
</html>

View file

@ -99,7 +99,7 @@
"test:e2e:match": "playwright test -g"
},
"dependencies": {
"@astrojs/compiler": "^1.1.0",
"@astrojs/compiler": "0.0.0-head-bubbling-20230216202235",
"@astrojs/language-server": "^0.28.3",
"@astrojs/markdown-remark": "^2.0.1",
"@astrojs/telemetry": "^2.0.0",

View file

@ -43,6 +43,7 @@ export async function compile({
internalURL: 'astro/server/index.js',
astroGlobalArgs: JSON.stringify(astroConfig.site),
resultScopedSlot: true,
implicitHeadInjection: false,
preprocessStyle: createStylePreprocessor({
filename,
viteConfig,

View file

@ -11,6 +11,7 @@ import {
import { renderAllHeadContent } from './head.js';
import { hasScopeFlag, ScopeFlags } from './scope.js';
import { isSlotString, type SlotString } from './slot.js';
import { renderChild } from './any.js';
export const Fragment = Symbol.for('astro:fragment');
export const Renderer = Symbol.for('astro:renderer');
@ -18,12 +19,16 @@ export const Renderer = Symbol.for('astro:renderer');
export const encoder = new TextEncoder();
export const decoder = new TextDecoder();
export function isRenderInstruction(chunk: unknown): chunk is RenderInstruction {
return typeof chunk != null && typeof (chunk as any).type === 'string';
}
// Rendering produces either marked strings of HTML or instructions for hydration.
// These directive instructions bubble all the way up to renderPage so that we
// can ensure they are added only once, and as soon as possible.
export function stringifyChunk(result: SSRResult, chunk: string | SlotString | RenderInstruction) {
if (typeof (chunk as any).type === 'string') {
const instruction = chunk as RenderInstruction;
if (isRenderInstruction(chunk)) {
const instruction = chunk;
switch (instruction.type) {
case 'directive': {
const { hydration } = instruction;
@ -47,6 +52,7 @@ export function stringifyChunk(result: SSRResult, chunk: string | SlotString | R
if (result._metadata.hasRenderedHead) {
return '';
}
return renderAllHeadContent(result);
}
case 'maybe-head': {
@ -147,3 +153,11 @@ export function chunkToByteArray(
let stringified = stringifyChunk(result, chunk);
return encoder.encode(stringified.toString());
}
export async function renderToStringAsync(result: SSRResult, part: unknown): Promise<string> {
let out = '';
for await(const chunk of renderChild(part)) {
out += stringifyChunk(result, chunk);
}
return out;
}

View file

@ -12,10 +12,11 @@ import {
isRenderTemplateResult,
renderAstroTemplateResult,
} from './astro/index.js';
import { chunkToByteArray, encoder, HTMLParts } from './common.js';
import { chunkToByteArray, encoder, HTMLParts, renderToStringAsync } from './common.js';
import { renderComponent } from './component.js';
import { maybeRenderHead } from './head.js';
import { createScopedResult, ScopeFlags } from './scope.js';
import { renderChild } from './any.js';
const needsHeadRenderingSymbol = Symbol.for('astro.needsHeadRendering');
@ -64,7 +65,13 @@ async function bufferHeadContent(result: SSRResult) {
}
const returnValue = await value.init(scoped);
if (isHeadAndContent(returnValue)) {
result.extraHead.push(returnValue.head);
if(typeof returnValue.head === 'string') {
result.extraHead.push(returnValue.head);
} else {
const head = await renderToStringAsync(result, returnValue.head);
result.extraHead.push(head);
}
}
}
}
@ -151,6 +158,7 @@ export async function renderPage(
const bytes = chunkToByteArray(result, chunk);
controller.enqueue(bytes);
i++;
}
controller.close();

View file

@ -0,0 +1,15 @@
{
"name": "@test/head-bubbling",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
"astro": "workspace:*"
}
}

View file

@ -0,0 +1,14 @@
---
const title = 'Blog Post';
const description = 'Some description';
---
<head>
<meta property="og:title" content={title} />
<meta property="og:description" content={description} /></head>
</head>
<article>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas eget commodo lacus. Sed hendrerit vel tortor in viverra. Praesent a lectus ex. Cras hendrerit ligula in sapien euismod maximus. Duis vel consectetur nibh, sed tempus est. Nullam sit amet iaculis nisl. Suspendisse efficitur libero eu magna varius faucibus a a nulla. Aliquam pretium diam ut bibendum convallis. Nullam vel mi nunc. Duis rutrum odio a magna posuere, in semper nisl dictum. Proin malesuada arcu in mi finibus, eget blandit urna varius. Ut pulvinar malesuada euismod.</p>
<p>Suspendisse sed erat neque. Donec est ante, venenatis ut urna a, efficitur finibus leo. Cras ut pellentesque mi. Sed non nunc tincidunt, euismod erat eu, fringilla augue. Nunc in sem a nisi luctus lacinia ut interdum turpis. Pellentesque at bibendum nunc. Etiam id felis in erat egestas ultrices. Proin cursus nulla et ligula elementum iaculis. Aenean volutpat ullamcorper purus, vitae elementum urna interdum at. Aenean ex elit, porta vitae dictum ac, mattis at justo. Aenean non augue tincidunt tellus aliquam condimentum. Nullam eu ante sed turpis lobortis iaculis.</p>
</article>

View file

@ -0,0 +1,13 @@
---
import Post from '../components/Post.astro';
---
<html>
<head>
<title>Index page</title>
</head>
<body>
<h1>My Site</h1>
<Post />
</body>
</html>

View file

@ -0,0 +1,36 @@
import { expect } from 'chai';
import * as cheerio from 'cheerio';
import { loadFixture } from './test-utils.js';
describe('Head bubbling', () => {
/** @type {import('./test-utils').Fixture} */
let fixture;
before(async () => {
fixture = await loadFixture({
root: './fixtures/head-bubbling/',
});
await fixture.build();
});
describe('index page', () => {
/** @type {string} */
let html;
/** @type {ReturnType<typeof cheerio.load>} */
let $;
before(async () => {
html = await fixture.readFile(`/index.html`);
$ = cheerio.load(html);
});
it('Renders component head contents into the head', async () => {
const $metas = $('head meta');
expect($metas).to.have.a.lengthOf(2);
});
it('Body contents in the body', async () => {
expect($('body article')).to.have.a.lengthOf(1);
});
});
});

View file

@ -243,6 +243,12 @@ importers:
'@astrojs/node': link:../../packages/integrations/node
astro: link:../../packages/astro
examples/head-bubbling:
specifiers:
astro: ^2.0.11
dependencies:
astro: link:../../packages/astro
examples/integration:
specifiers:
astro: ^2.0.11
@ -375,7 +381,7 @@ importers:
packages/astro:
specifiers:
'@astrojs/compiler': ^1.1.0
'@astrojs/compiler': 0.0.0-head-bubbling-20230216202235
'@astrojs/language-server': ^0.28.3
'@astrojs/markdown-remark': ^2.0.1
'@astrojs/telemetry': ^2.0.0
@ -465,7 +471,7 @@ importers:
yargs-parser: ^21.0.1
zod: ^3.17.3
dependencies:
'@astrojs/compiler': 1.1.0
'@astrojs/compiler': 0.0.0-head-bubbling-20230216202235
'@astrojs/language-server': 0.28.3
'@astrojs/markdown-remark': link:../markdown/remark
'@astrojs/telemetry': link:../telemetry
@ -1857,6 +1863,12 @@ importers:
dependencies:
astro: link:../../..
packages/astro/test/fixtures/head-bubbling:
specifiers:
astro: workspace:*
dependencies:
astro: link:../../..
packages/astro/test/fixtures/head-injection:
specifiers:
astro: workspace:*
@ -3902,13 +3914,13 @@ packages:
sisteransi: 1.0.5
dev: false
/@astrojs/compiler/0.0.0-head-bubbling-20230216202235:
resolution: {integrity: sha512-3IFx8Y3T16yjlVLSElAOVvLrCFSs9pmLBD9quLM48MyKmJjamPmIctTGJ02ixNCOcZZrZPBjEPefTyrqNQ64Zw==}
dev: false
/@astrojs/compiler/0.31.4:
resolution: {integrity: sha512-6bBFeDTtPOn4jZaiD3p0f05MEGQL9pw2Zbfj546oFETNmjJFWO3nzHz6/m+P53calknCvyVzZ5YhoBLIvzn5iw==}
/@astrojs/compiler/1.1.0:
resolution: {integrity: sha512-C4kTwirys+HafufMqaxCbML2wqkGaXJM+5AekXh/v1IIOnMIdcEON9GBYsG6qa8aAmLhZ58aUZGPhzcA3Dx7Uw==}
dev: false
/@astrojs/language-server/0.28.3:
resolution: {integrity: sha512-fPovAX/X46eE2w03jNRMpQ7W9m2mAvNt4Ay65lD9wl1Z5vIQYxlg7Enp9qP225muTr4jSVB5QiLumFJmZMAaVA==}
hasBin: true