summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.changeset/brown-numbers-prove.md5
-rw-r--r--.changeset/many-ears-drum.md5
-rw-r--r--.changeset/rich-tigers-march.md5
-rw-r--r--.changeset/silent-snakes-shave.md5
-rw-r--r--.changeset/unlucky-cougars-heal.md5
-rw-r--r--examples/portfolio/src/components/Hero.astro4
-rw-r--r--examples/portfolio/src/components/Nav.astro4
-rw-r--r--packages/astro/CHANGELOG.md18
-rw-r--r--packages/astro/components/ViewTransitions.astro105
-rw-r--r--packages/astro/e2e/fixtures/view-transitions/src/pages/half-baked.astro24
-rw-r--r--packages/astro/e2e/fixtures/view-transitions/src/pages/one.astro1
-rw-r--r--packages/astro/e2e/fixtures/view-transitions/src/pages/three.astro3
-rw-r--r--packages/astro/e2e/view-transitions.test.js80
-rw-r--r--packages/astro/src/runtime/server/render/any.ts11
-rw-r--r--packages/astro/src/runtime/server/render/astro/render-template.ts18
-rw-r--r--packages/astro/src/runtime/server/render/common.ts4
-rw-r--r--packages/astro/src/runtime/server/render/component.ts24
-rw-r--r--packages/astro/src/runtime/server/render/util.ts54
-rw-r--r--packages/astro/test/astro-basic.test.js6
-rw-r--r--packages/astro/test/fixtures/astro-basic/src/components/OrderA.astro7
-rw-r--r--packages/astro/test/fixtures/astro-basic/src/components/OrderB.astro7
-rw-r--r--packages/astro/test/fixtures/astro-basic/src/components/OrderLast.astro1
-rw-r--r--packages/astro/test/fixtures/astro-basic/src/pages/order.astro13
-rw-r--r--packages/integrations/cloudflare/README.md49
-rw-r--r--packages/integrations/cloudflare/src/index.ts3
-rw-r--r--packages/integrations/cloudflare/src/server.advanced.ts4
-rw-r--r--packages/integrations/cloudflare/src/server.directory.ts4
-rw-r--r--packages/integrations/netlify/README.md2
-rw-r--r--packages/integrations/node/CHANGELOG.md9
-rw-r--r--packages/integrations/node/README.md2
-rw-r--r--packages/integrations/vercel/README.md2
31 files changed, 391 insertions, 93 deletions
diff --git a/.changeset/brown-numbers-prove.md b/.changeset/brown-numbers-prove.md
deleted file mode 100644
index 96db75af0..000000000
--- a/.changeset/brown-numbers-prove.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'astro': patch
----
-
-Displays a new config error if `outDir` is placed within `publicDir`.
diff --git a/.changeset/many-ears-drum.md b/.changeset/many-ears-drum.md
deleted file mode 100644
index f728408a0..000000000
--- a/.changeset/many-ears-drum.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'astro': patch
----
-
-Fix missing type for `imageConfig` export from `astro:assets`
diff --git a/.changeset/rich-tigers-march.md b/.changeset/rich-tigers-march.md
deleted file mode 100644
index fb698048f..000000000
--- a/.changeset/rich-tigers-march.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@astrojs/node': patch
----
-
-Fix an issue where `express` couldn't use the `handler` in `middleware` mode.
diff --git a/.changeset/silent-snakes-shave.md b/.changeset/silent-snakes-shave.md
new file mode 100644
index 000000000..3f8c8c3ee
--- /dev/null
+++ b/.changeset/silent-snakes-shave.md
@@ -0,0 +1,5 @@
+---
+'@astrojs/cloudflare': patch
+---
+
+Improve documentation and export the types needed to type the `runtime` object.
diff --git a/.changeset/unlucky-cougars-heal.md b/.changeset/unlucky-cougars-heal.md
deleted file mode 100644
index a6579499e..000000000
--- a/.changeset/unlucky-cougars-heal.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'astro': patch
----
-
-Make typing of `defineCollection` more permissive to support advanced union and intersection types
diff --git a/examples/portfolio/src/components/Hero.astro b/examples/portfolio/src/components/Hero.astro
index 21b5ed1bf..30460420a 100644
--- a/examples/portfolio/src/components/Hero.astro
+++ b/examples/portfolio/src/components/Hero.astro
@@ -8,13 +8,13 @@ interface Props {
const { align = 'center', tagline, title } = Astro.props;
---
-<header class:list={['hero stack gap-4', align]}>
+<div class:list={['hero stack gap-4', align]}>
<div class="stack gap-2">
<h1 class="title">{title}</h1>
{tagline && <p class="tagline">{tagline}</p>}
</div>
<slot />
-</header>
+</div>
<style>
.hero {
diff --git a/examples/portfolio/src/components/Nav.astro b/examples/portfolio/src/components/Nav.astro
index 2e9717884..bf9ac7869 100644
--- a/examples/portfolio/src/components/Nav.astro
+++ b/examples/portfolio/src/components/Nav.astro
@@ -36,7 +36,7 @@ const iconLinks: { label: string; href: string; icon: keyof typeof iconPaths }[]
</template>
</menu-button>
</div>
- <noscript class="menu-noscript">
+ <noscript>
<ul class="nav-items">
{
textLinks.map(({ label, href }) => (
@@ -60,7 +60,7 @@ const iconLinks: { label: string; href: string; icon: keyof typeof iconPaths }[]
}
</ul>
</noscript>
- <noscript style="display: contents;">
+ <noscript>
<div class="menu-footer">
<div class="socials">
{
diff --git a/packages/astro/CHANGELOG.md b/packages/astro/CHANGELOG.md
index eefbb2eef..d2161969f 100644
--- a/packages/astro/CHANGELOG.md
+++ b/packages/astro/CHANGELOG.md
@@ -553,6 +553,24 @@
- @astrojs/internal-helpers@0.2.0-beta.0
- @astrojs/markdown-remark@3.0.0-beta.0
+## 2.10.13
+
+### Patch Changes
+
+- [#8152](https://github.com/withastro/astro/pull/8152) [`582132328`](https://github.com/withastro/astro/commit/5821323285646aee7ff9194a505f708028e4db57) Thanks [@andremralves](https://github.com/andremralves)! - Displays a new config error if `outDir` is placed within `publicDir`.
+
+- [#8166](https://github.com/withastro/astro/pull/8166) [`fddd4dc71`](https://github.com/withastro/astro/commit/fddd4dc71af321bd6b4d01bb4b1b955284846e60) Thanks [@martrapp](https://github.com/martrapp)! - ViewTransitions: Fixes in the client-side router
+
+- [#8182](https://github.com/withastro/astro/pull/8182) [`cfc465dde`](https://github.com/withastro/astro/commit/cfc465ddebcc58d20f29ecffaa857a77525435a9) Thanks [@martrapp](https://github.com/martrapp)! - View Transitions: self link (`href=""`) does not trigger page reload
+
+- [#8171](https://github.com/withastro/astro/pull/8171) [`95120efbe`](https://github.com/withastro/astro/commit/95120efbe817163663492181cbeb225849354493) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Fix missing type for `imageConfig` export from `astro:assets`
+
+- [#8187](https://github.com/withastro/astro/pull/8187) [`273335cb0`](https://github.com/withastro/astro/commit/273335cb01615c3c06d46c02464f4496a81f8d0b) Thanks [@bluwy](https://github.com/bluwy)! - Fix Astro components parent-child render order
+
+- [#8184](https://github.com/withastro/astro/pull/8184) [`9142178b1`](https://github.com/withastro/astro/commit/9142178b113443749b87c1d259859b42a3d7a9c4) Thanks [@martrapp](https://github.com/martrapp)! - Fix: The scrolling behavior of ViewTransitions is now more similar to the expected browser behavior
+
+- [#8163](https://github.com/withastro/astro/pull/8163) [`179796405`](https://github.com/withastro/astro/commit/179796405e053b559d83f84507e5a465861a029a) Thanks [@delucis](https://github.com/delucis)! - Make typing of `defineCollection` more permissive to support advanced union and intersection types
+
## 2.10.12
### Patch Changes
diff --git a/packages/astro/components/ViewTransitions.astro b/packages/astro/components/ViewTransitions.astro
index 4b7a46551..612b89659 100644
--- a/packages/astro/components/ViewTransitions.astro
+++ b/packages/astro/components/ViewTransitions.astro
@@ -20,22 +20,21 @@ const { fallback = 'animate' } = Astro.props as Props;
type Events = 'astro:load' | 'astro:beforeload';
const persistState = (state: State) => history.replaceState(state, '');
+ const supportsViewTransitions = !!document.startViewTransition;
+ const transitionEnabledOnThisPage = () =>
+ !!document.querySelector('[name="astro-view-transitions-enabled"]');
+ const triggerEvent = (name: Events) => document.dispatchEvent(new Event(name));
+ const onload = () => triggerEvent('astro:load');
+ const PERSIST_ATTR = 'data-astro-transition-persist';
// The History API does not tell you if navigation is forward or back, so
// you can figure it using an index. On pushState the index is incremented so you
// can use that to determine popstate if going forward or back.
let currentHistoryIndex = history.state?.index || 0;
- if (!history.state) {
+ if (!history.state && transitionEnabledOnThisPage()) {
persistState({ index: currentHistoryIndex, scrollY: 0 });
}
- const supportsViewTransitions = !!document.startViewTransition;
- const transitionEnabledOnThisPage = () =>
- !!document.querySelector('[name="astro-view-transitions-enabled"]');
- const triggerEvent = (name: Events) => document.dispatchEvent(new Event(name));
- const onload = () => triggerEvent('astro:load');
- const PERSIST_ATTR = 'data-astro-transition-persist';
-
const throttle = (cb: (...args: any[]) => any, delay: number) => {
let wait = false;
// During the waiting time additional events are lost.
@@ -165,14 +164,21 @@ const { fallback = 'animate' } = Astro.props as Props;
}
}
+ // Simulate scroll behavior of Safari and
+ // Chromium based browsers (Chrome, Edge, Opera, ...)
+ scrollTo({ left: 0, top: 0, behavior: 'instant' });
+
if (state?.scrollY === 0 && location.hash) {
const id = decodeURIComponent(location.hash.slice(1));
- state.scrollY = document.getElementById(id)?.offsetTop || 0;
- }
- if (state?.scrollY != null) {
- scrollTo(0, state.scrollY);
- // Overwrite erroneous updates by the scroll handler during transition
- persistState(state);
+ const elem = document.getElementById(id);
+ // prefer scrollIntoView() over scrollTo() because it takes scroll-padding into account
+ if (elem) {
+ state.scrollY = elem.offsetTop;
+ persistState(state); // first guess, later updated by scroll handler
+ elem.scrollIntoView(); // for Firefox, this should better be {behavior: 'instant'}
+ }
+ } else if (state && state.scrollY !== 0) {
+ scrollTo(0, state.scrollY); // usings default scrollBehavior
}
triggerEvent('astro:beforeload');
@@ -274,34 +280,59 @@ const { fallback = 'animate' } = Astro.props as Props;
// that is going to another page within the same origin. Basically it determines
// same-origin navigation, but omits special key combos for new tabs, etc.
if (
- link &&
- link instanceof HTMLAnchorElement &&
- link.href &&
- (!link.target || link.target === '_self') &&
- link.origin === location.origin &&
- !(
- // Same page means same path and same query params
- (location.pathname === link.pathname && location.search === link.search)
- ) &&
- ev.button === 0 && // left clicks only
- !ev.metaKey && // new tab (mac)
- !ev.ctrlKey && // new tab (windows)
- !ev.altKey && // download
- !ev.shiftKey &&
- !ev.defaultPrevented &&
- transitionEnabledOnThisPage()
- ) {
- ev.preventDefault();
- navigate('forward', link.href, { index: ++currentHistoryIndex, scrollY: 0 });
- const newState: State = { index: currentHistoryIndex, scrollY };
- persistState({ index: currentHistoryIndex - 1, scrollY });
- history.pushState(newState, '', link.href);
+ !link ||
+ !(link instanceof HTMLAnchorElement) ||
+ !link.href ||
+ (link.target && link.target !== '_self') ||
+ link.origin !== location.origin ||
+ ev.button !== 0 || // left clicks only
+ ev.metaKey || // new tab (mac)
+ ev.ctrlKey || // new tab (windows)
+ ev.altKey || // download
+ ev.shiftKey || // new window
+ ev.defaultPrevented ||
+ !transitionEnabledOnThisPage()
+ )
+ // No page transitions in these cases,
+ // Let the browser standard action handle this
+ return;
+
+ // We do not need to handle same page links because there are no page transitions
+ // Same page means same path and same query params (but different hash)
+ if (location.pathname === link.pathname && location.search === link.search) {
+ if (link.hash) {
+ // The browser default action will handle navigations with hash fragments
+ return;
+ } else {
+ // Special case: self link without hash
+ // If handed to the browser it will reload the page
+ // But we want to handle it like any other same page navigation
+ // So we scroll to the top of the page but do not start page transitions
+ ev.preventDefault();
+ persistState({ ...history.state, scrollY });
+ scrollTo({ left: 0, top: 0, behavior: 'instant' });
+ if (location.hash) {
+ // last target was different
+ const newState: State = { index: ++currentHistoryIndex, scrollY: 0 };
+ history.pushState(newState, '', link.href);
+ }
+ return;
+ }
}
+
+ // these are the cases we will handle: same origin, different page
+ ev.preventDefault();
+ navigate('forward', link.href, { index: ++currentHistoryIndex, scrollY: 0 });
+ const newState: State = { index: currentHistoryIndex, scrollY };
+ persistState({ index: currentHistoryIndex - 1, scrollY });
+ history.pushState(newState, '', link.href);
});
+
addEventListener('popstate', (ev) => {
- if (!transitionEnabledOnThisPage()) {
+ if (!transitionEnabledOnThisPage() && ev.state) {
// The current page doesn't haven't View Transitions,
// respect that with a full page reload
+ // -- but only for transition managed by us (ev.state is set)
location.reload();
return;
}
diff --git a/packages/astro/e2e/fixtures/view-transitions/src/pages/half-baked.astro b/packages/astro/e2e/fixtures/view-transitions/src/pages/half-baked.astro
new file mode 100644
index 000000000..40298d125
--- /dev/null
+++ b/packages/astro/e2e/fixtures/view-transitions/src/pages/half-baked.astro
@@ -0,0 +1,24 @@
+---
+import { ViewTransitions } from 'astro:transitions';
+
+// For the test fixture, we import the script but we don't use the <ViewTransitions /> component
+// While this seems to be some strange mistake,
+// it might be realistic, e.g. in a configurable CommenHead component
+
+interface Props {
+ transitions?: string;
+}
+const { transitions } = Astro.props;
+---
+<html>
+ <head>
+ <title>Half-Baked</title>
+ {transitions && <ViewTransitions />}
+ </head>
+ <body>
+ <main>
+ <p id="half-baked">Half Baked</p>
+ <a id="click-hash" href="#click-hash">hash target</a>
+ </main>
+ </body>
+</html>
diff --git a/packages/astro/e2e/fixtures/view-transitions/src/pages/one.astro b/packages/astro/e2e/fixtures/view-transitions/src/pages/one.astro
index b24338d9d..3f9666c1d 100644
--- a/packages/astro/e2e/fixtures/view-transitions/src/pages/one.astro
+++ b/packages/astro/e2e/fixtures/view-transitions/src/pages/one.astro
@@ -7,6 +7,7 @@ import Layout from '../components/Layout.astro';
<a id="click-two" href="/two">go to 2</a>
<a id="click-three" href="/three">go to 3</a>
<a id="click-longpage" href="/long-page">go to long page</a>
+ <a id="click-self" href="">go to top</a>
<div id="test">test content</div>
</Layout>
diff --git a/packages/astro/e2e/fixtures/view-transitions/src/pages/three.astro b/packages/astro/e2e/fixtures/view-transitions/src/pages/three.astro
index 676e8b61b..eddc049a8 100644
--- a/packages/astro/e2e/fixtures/view-transitions/src/pages/three.astro
+++ b/packages/astro/e2e/fixtures/view-transitions/src/pages/three.astro
@@ -6,6 +6,9 @@
<main>
<p id="three">Page 3</p>
<a id="click-two" href="/two">go to 2</a>
+ <br/>
+ <a id="click-hash" href="#click-hash">hash target</a>
+ <p style="height: 150vh">Long paragraph</p>
</main>
</body>
</html>
diff --git a/packages/astro/e2e/view-transitions.test.js b/packages/astro/e2e/view-transitions.test.js
index 7aeb6502a..c681bfea0 100644
--- a/packages/astro/e2e/view-transitions.test.js
+++ b/packages/astro/e2e/view-transitions.test.js
@@ -112,6 +112,40 @@ test.describe('View Transitions', () => {
).toEqual(2);
});
+ test('Moving within a page without ViewTransitions does not trigger a full page navigation', async ({
+ page,
+ astro,
+ }) => {
+ const loads = [];
+ page.addListener('load', async (p) => {
+ loads.push(p.title());
+ });
+ // Go to page 1
+ await page.goto(astro.resolveUrl('/one'));
+ let p = page.locator('#one');
+ await expect(p, 'should have content').toHaveText('Page 1');
+
+ // Go to page 3 which does *not* have ViewTransitions enabled
+ await page.click('#click-three');
+ p = page.locator('#three');
+ await expect(p, 'should have content').toHaveText('Page 3');
+
+ // click a hash link to navigate further down the page
+ await page.click('#click-hash');
+ // still on page 3
+ p = page.locator('#three');
+ await expect(p, 'should have content').toHaveText('Page 3');
+
+ // check that we are further down the page
+ const Y = await page.evaluate(() => window.scrollY);
+ expect(Y, 'The target is further down the page').toBeGreaterThan(0);
+
+ expect(
+ loads.length,
+ 'There should be only 1 page load. The original, but no additional loads for the hash change'
+ ).toEqual(1);
+ });
+
test('Moving from a page without ViewTransitions w/ back button', async ({ page, astro }) => {
const loads = [];
page.addListener('load', (p) => {
@@ -190,6 +224,22 @@ test.describe('View Transitions', () => {
await expect(p, 'should have content').toHaveText('Page 1');
});
+ test('click self link (w/o hash) does not do navigation', async ({ page, astro }) => {
+ const loads = [];
+ page.addListener('load', (p) => {
+ loads.push(p.title());
+ });
+ // Go to page 1
+ await page.goto(astro.resolveUrl('/one'));
+ const p = page.locator('#one');
+ await expect(p, 'should have content').toHaveText('Page 1');
+
+ // Clicking href="" stays on page
+ await page.click('#click-self');
+ await expect(p, 'should have content').toHaveText('Page 1');
+ expect(loads.length, 'There should only be 1 page load').toEqual(1);
+ });
+
test('Scroll position restored on back button', async ({ page, astro }) => {
// Go to page 1
await page.goto(astro.resolveUrl('/long-page'));
@@ -316,4 +366,34 @@ test.describe('View Transitions', () => {
await expect(loads.length, 'There should only be 1 page load').toEqual(1);
});
+
+ test('Importing ViewTransitions w/o using the component must not mess with history', async ({
+ page,
+ astro,
+ }) => {
+ const loads = [];
+ page.addListener('load', async (p) => {
+ loads.push(p);
+ });
+ // Go to the half bakeed page
+ await page.goto(astro.resolveUrl('/half-baked'));
+ let p = page.locator('#half-baked');
+ await expect(p, 'should have content').toHaveText('Half Baked');
+
+ // click a hash link to navigate further down the page
+ await page.click('#click-hash');
+ // still on page
+ p = page.locator('#half-baked');
+ await expect(p, 'should have content').toHaveText('Half Baked');
+
+ // go back within same page without reloading
+ await page.goBack();
+ p = page.locator('#half-baked');
+ await expect(p, 'should have content').toHaveText('Half Baked');
+
+ expect(
+ loads.length,
+ 'There should be only 1 page load. No additional loads for going back on same page'
+ ).toEqual(1);
+ });
});
diff --git a/packages/astro/src/runtime/server/render/any.ts b/packages/astro/src/runtime/server/render/any.ts
index 7c181fecb..89c927576 100644
--- a/packages/astro/src/runtime/server/render/any.ts
+++ b/packages/astro/src/runtime/server/render/any.ts
@@ -2,6 +2,7 @@ import { escapeHTML, isHTMLString, markHTMLString } from '../escape.js';
import { isAstroComponentInstance, isRenderTemplateResult } from './astro/index.js';
import { isRenderInstance, type RenderDestination } from './common.js';
import { SlotString } from './slot.js';
+import { renderToBufferDestination } from './util.js';
export async function renderChild(destination: RenderDestination, child: any) {
child = await child;
@@ -10,8 +11,14 @@ export async function renderChild(destination: RenderDestination, child: any) {
} else if (isHTMLString(child)) {
destination.write(child);
} else if (Array.isArray(child)) {
- for (const c of child) {
- await renderChild(destination, c);
+ // Render all children eagerly and in parallel
+ const childRenders = child.map((c) => {
+ return renderToBufferDestination((bufferDestination) => {
+ return renderChild(bufferDestination, c);
+ });
+ });
+ for (const childRender of childRenders) {
+ await childRender.renderToFinalDestination(destination);
}
} else if (typeof child === 'function') {
// Special: If a child is a function, call it automatically.
diff --git a/packages/astro/src/runtime/server/render/astro/render-template.ts b/packages/astro/src/runtime/server/render/astro/render-template.ts
index 1d5af33fc..f12344fe8 100644
--- a/packages/astro/src/runtime/server/render/astro/render-template.ts
+++ b/packages/astro/src/runtime/server/render/astro/render-template.ts
@@ -2,6 +2,7 @@ import { markHTMLString } from '../../escape.js';
import { isPromise } from '../../util.js';
import { renderChild } from '../any.js';
import type { RenderDestination } from '../common.js';
+import { renderToBufferDestination } from '../util.js';
const renderTemplateResultSym = Symbol.for('astro.renderTemplateResult');
@@ -32,14 +33,23 @@ export class RenderTemplateResult {
}
async render(destination: RenderDestination) {
+ // Render all expressions eagerly and in parallel
+ const expRenders = this.expressions.map((exp) => {
+ return renderToBufferDestination((bufferDestination) => {
+ // Skip render if falsy, except the number 0
+ if (exp || exp === 0) {
+ return renderChild(bufferDestination, exp);
+ }
+ });
+ });
+
for (let i = 0; i < this.htmlParts.length; i++) {
const html = this.htmlParts[i];
- const exp = this.expressions[i];
+ const expRender = expRenders[i];
destination.write(markHTMLString(html));
- // Skip render if falsy, except the number 0
- if (exp || exp === 0) {
- await renderChild(destination, exp);
+ if (expRender) {
+ await expRender.renderToFinalDestination(destination);
}
}
}
diff --git a/packages/astro/src/runtime/server/render/common.ts b/packages/astro/src/runtime/server/render/common.ts
index d4c7a1242..7beaed2eb 100644
--- a/packages/astro/src/runtime/server/render/common.ts
+++ b/packages/astro/src/runtime/server/render/common.ts
@@ -37,9 +37,11 @@ export interface RenderDestination {
}
export interface RenderInstance {
- render(destination: RenderDestination): Promise<void> | void;
+ render: RenderFunction;
}
+export type RenderFunction = (destination: RenderDestination) => Promise<void> | void;
+
export const Fragment = Symbol.for('astro:fragment');
export const Renderer = Symbol.for('astro:renderer');
diff --git a/packages/astro/src/runtime/server/render/component.ts b/packages/astro/src/runtime/server/render/component.ts
index c92316d13..1c7701fd2 100644
--- a/packages/astro/src/runtime/server/render/component.ts
+++ b/packages/astro/src/runtime/server/render/component.ts
@@ -24,7 +24,6 @@ import {
Renderer,
chunkToString,
type RenderDestination,
- type RenderDestinationChunk,
type RenderInstance,
} from './common.js';
import { componentIsHTMLElement, renderHTMLElement } from './dom.js';
@@ -422,27 +421,12 @@ function renderAstroComponent(
slots: any = {}
): RenderInstance {
const instance = createAstroComponentInstance(result, displayName, Component, props, slots);
-
- // Eagerly render the component so they are rendered in parallel.
- // Render to buffer for now until our returned render function is called.
- const bufferChunks: RenderDestinationChunk[] = [];
- const bufferDestination: RenderDestination = {
- write: (chunk) => bufferChunks.push(chunk),
- };
- // Don't await for the render to finish to not block streaming
- const renderPromise = instance.render(bufferDestination);
-
return {
async render(destination) {
- // Write the buffered chunks to the real destination
- for (const chunk of bufferChunks) {
- destination.write(chunk);
- }
- // Free memory
- bufferChunks.length = 0;
- // Re-assign the real destination so `instance.render` will continue and write to the new destination
- bufferDestination.write = (chunk) => destination.write(chunk);
- await renderPromise;
+ // NOTE: This render call can't be pre-invoked outside of this function as it'll also initialize the slots
+ // recursively, which causes each Astro components in the tree to be called bottom-up, and is incorrect.
+ // The slots are initialized eagerly for head propagation.
+ await instance.render(destination);
},
};
}
diff --git a/packages/astro/src/runtime/server/render/util.ts b/packages/astro/src/runtime/server/render/util.ts
index e77f8ed8b..f9783fce3 100644
--- a/packages/astro/src/runtime/server/render/util.ts
+++ b/packages/astro/src/runtime/server/render/util.ts
@@ -1,4 +1,5 @@
import type { SSRElement } from '../../../@types/astro';
+import type { RenderDestination, RenderDestinationChunk, RenderFunction } from './common.js';
import { HTMLString, markHTMLString } from '../escape.js';
import { clsx } from 'clsx';
@@ -141,3 +142,56 @@ export function renderElement(
}
return `<${name}${internalSpreadAttributes(props, shouldEscape)}>${children}</${name}>`;
}
+
+/**
+ * Executes the `bufferRenderFunction` to prerender it into a buffer destination, and return a promise
+ * with an object containing the `renderToFinalDestination` function to flush the buffer to the final
+ * destination.
+ *
+ * @example
+ * ```ts
+ * // Render components in parallel ahead of time
+ * const finalRenders = [ComponentA, ComponentB].map((comp) => {
+ * return renderToBufferDestination(async (bufferDestination) => {
+ * await renderComponentToDestination(bufferDestination);
+ * });
+ * });
+ * // Render array of components serially
+ * for (const finalRender of finalRenders) {
+ * await finalRender.renderToFinalDestination(finalDestination);
+ * }
+ * ```
+ */
+export function renderToBufferDestination(bufferRenderFunction: RenderFunction): {
+ renderToFinalDestination: RenderFunction;
+} {
+ // Keep chunks in memory
+ const bufferChunks: RenderDestinationChunk[] = [];
+ const bufferDestination: RenderDestination = {
+ write: (chunk) => bufferChunks.push(chunk),
+ };
+
+ // Don't await for the render to finish to not block streaming
+ const renderPromise = bufferRenderFunction(bufferDestination);
+
+ // Return a closure that writes the buffered chunk
+ return {
+ async renderToFinalDestination(destination) {
+ // Write the buffered chunks to the real destination
+ for (const chunk of bufferChunks) {
+ destination.write(chunk);
+ }
+
+ // NOTE: We don't empty `bufferChunks` after it's written as benchmarks show
+ // that it causes poorer performance, likely due to forced memory re-allocation,
+ // instead of letting the garbage collector handle it automatically.
+ // (Unsure how this affects on limited memory machines)
+
+ // Re-assign the real destination so `instance.render` will continue and write to the new destination
+ bufferDestination.write = (chunk) => destination.write(chunk);
+
+ // Wait for render to finish entirely
+ await renderPromise;
+ },
+ };
+}
diff --git a/packages/astro/test/astro-basic.test.js b/packages/astro/test/astro-basic.test.js
index 92b6af410..3a75c1acc 100644
--- a/packages/astro/test/astro-basic.test.js
+++ b/packages/astro/test/astro-basic.test.js
@@ -102,6 +102,12 @@ describe('Astro basics', () => {
// will have already erred by now, but add test anyway
expect(await fixture.readFile('/special-“characters” -in-file/index.html')).to.be.ok;
});
+
+ it('renders the components top-down', async () => {
+ const html = await fixture.readFile('/order/index.html');
+ const $ = cheerio.load(html);
+ expect($('#rendered-order').text()).to.eq('Rendered order: A, B');
+ });
});
it('Supports void elements whose name is a string (#2062)', async () => {
diff --git a/packages/astro/test/fixtures/astro-basic/src/components/OrderA.astro b/packages/astro/test/fixtures/astro-basic/src/components/OrderA.astro
new file mode 100644
index 000000000..7caa0d868
--- /dev/null
+++ b/packages/astro/test/fixtures/astro-basic/src/components/OrderA.astro
@@ -0,0 +1,7 @@
+---
+globalThis.__ASTRO_TEST_ORDER__ ??= []
+globalThis.__ASTRO_TEST_ORDER__.push('A')
+---
+
+<p>A</p>
+<slot />
diff --git a/packages/astro/test/fixtures/astro-basic/src/components/OrderB.astro b/packages/astro/test/fixtures/astro-basic/src/components/OrderB.astro
new file mode 100644
index 000000000..b2984be5c
--- /dev/null
+++ b/packages/astro/test/fixtures/astro-basic/src/components/OrderB.astro
@@ -0,0 +1,7 @@
+---
+globalThis.__ASTRO_TEST_ORDER__ ??= []
+globalThis.__ASTRO_TEST_ORDER__.push('B')
+---
+
+<p>B</p>
+<slot />
diff --git a/packages/astro/test/fixtures/astro-basic/src/components/OrderLast.astro b/packages/astro/test/fixtures/astro-basic/src/components/OrderLast.astro
new file mode 100644
index 000000000..35cd932dc
--- /dev/null
+++ b/packages/astro/test/fixtures/astro-basic/src/components/OrderLast.astro
@@ -0,0 +1 @@
+<p id="rendered-order">Rendered order: {() => (globalThis.__ASTRO_TEST_ORDER__ ?? []).join(', ')}</p>
diff --git a/packages/astro/test/fixtures/astro-basic/src/pages/order.astro b/packages/astro/test/fixtures/astro-basic/src/pages/order.astro
new file mode 100644
index 000000000..ca5b2fdd5
--- /dev/null
+++ b/packages/astro/test/fixtures/astro-basic/src/pages/order.astro
@@ -0,0 +1,13 @@
+---
+import OrderA from "../components/OrderA.astro";
+import OrderB from "../components/OrderB.astro";
+import OrderLast from "../components/OrderLast.astro";
+
+globalThis.__ASTRO_TEST_ORDER__ = [];
+---
+
+<OrderA>
+ <OrderB>
+ <OrderLast />
+ </OrderB>
+</OrderA>
diff --git a/packages/integrations/cloudflare/README.md b/packages/integrations/cloudflare/README.md
index d8c1fd197..25cd7c376 100644
--- a/packages/integrations/cloudflare/README.md
+++ b/packages/integrations/cloudflare/README.md
@@ -89,12 +89,59 @@ It's then possible to update the preview script in your `package.json` to `"prev
You can access all the Cloudflare bindings and environment variables from Astro components and API routes through `Astro.locals`.
-```js
+If you're inside an `.astro` file, you access the runtime using the `Astro.locals` global:
+
+```astro
const env = Astro.locals.runtime.env;
```
+From an endpoint:
+
+```js
+// src/pages/api/someFile.js
+export function get(context) {
+ const runtime = context.locals.runtime;
+
+ return new Response('Some body');
+}
+```
+
Depending on your adapter mode (advanced = worker, directory = pages), the runtime object will look a little different due to differences in the Cloudflare API.
+If you're using the `advanced` runtime, you can type the `runtime` object as following:
+
+```ts
+// src/env.d.ts
+/// <reference types="astro/client" />
+import type { AdvancedRuntime } from '@astrojs/cloudflare';
+
+declare namespace App {
+ interface Locals extends AdvancedRuntime {
+ user: {
+ name: string;
+ surname: string;
+ };
+ }
+}
+```
+
+If you're using the `directory` runtime, you can type the `runtime` object as following:
+
+```ts
+// src/env.d.ts
+/// <reference types="astro/client" />
+import type { DirectoryRuntime } from '@astrojs/cloudflare';
+
+declare namespace App {
+ interface Locals extends DirectoryRuntime {
+ user: {
+ name: string;
+ surname: string;
+ };
+ }
+}
+```
+
## Environment Variables
See Cloudflare's documentation for [working with environment variables](https://developers.cloudflare.com/pages/platform/functions/bindings/#environment-variables).
diff --git a/packages/integrations/cloudflare/src/index.ts b/packages/integrations/cloudflare/src/index.ts
index 76e9092ca..718b1efa8 100644
--- a/packages/integrations/cloudflare/src/index.ts
+++ b/packages/integrations/cloudflare/src/index.ts
@@ -7,6 +7,9 @@ import { sep } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import glob from 'tiny-glob';
+export type { AdvancedRuntime } from './server.advanced';
+export type { DirectoryRuntime } from './server.directory';
+
type Options = {
mode: 'directory' | 'advanced';
functionPerRoute?: boolean;
diff --git a/packages/integrations/cloudflare/src/server.advanced.ts b/packages/integrations/cloudflare/src/server.advanced.ts
index 175756d6a..24358a5e0 100644
--- a/packages/integrations/cloudflare/src/server.advanced.ts
+++ b/packages/integrations/cloudflare/src/server.advanced.ts
@@ -12,7 +12,7 @@ type Env = {
name: string;
};
-interface WorkerRuntime {
+export interface AdvancedRuntime {
runtime: {
waitUntil: (promise: Promise<any>) => void;
env: Env;
@@ -57,7 +57,7 @@ export function createExports(manifest: SSRManifest) {
},
});
- const locals: WorkerRuntime = {
+ const locals: AdvancedRuntime = {
runtime: {
waitUntil: (promise: Promise<any>) => {
context.waitUntil(promise);
diff --git a/packages/integrations/cloudflare/src/server.directory.ts b/packages/integrations/cloudflare/src/server.directory.ts
index d4e4094de..64d820d99 100644
--- a/packages/integrations/cloudflare/src/server.directory.ts
+++ b/packages/integrations/cloudflare/src/server.directory.ts
@@ -7,7 +7,7 @@ if (!isNode) {
process.env = getProcessEnvProxy();
}
-interface FunctionRuntime {
+export interface DirectoryRuntime {
runtime: {
waitUntil: (promise: Promise<any>) => void;
env: EventContext<unknown, string, unknown>['env'];
@@ -54,7 +54,7 @@ export function createExports(manifest: SSRManifest) {
cf: request.cf,
});
- const locals: FunctionRuntime = {
+ const locals: DirectoryRuntime = {
runtime: {
waitUntil: (promise: Promise<any>) => {
context.waitUntil(promise);
diff --git a/packages/integrations/netlify/README.md b/packages/integrations/netlify/README.md
index 0e4c3ec93..c29d2d126 100644
--- a/packages/integrations/netlify/README.md
+++ b/packages/integrations/netlify/README.md
@@ -34,6 +34,8 @@ yarn astro add netlify
pnpm astro add netlify
```
+### Add dependencies manually
+
If you prefer to install the adapter manually instead, complete the following two steps:
1. Install the Netlify adapter to your project’s dependencies using your preferred package manager. If you’re using npm or aren’t sure, run this in the terminal:
diff --git a/packages/integrations/node/CHANGELOG.md b/packages/integrations/node/CHANGELOG.md
index 0eec21df7..516834e77 100644
--- a/packages/integrations/node/CHANGELOG.md
+++ b/packages/integrations/node/CHANGELOG.md
@@ -52,6 +52,15 @@
- Updated dependencies [[`1eae2e3f7`](https://github.com/withastro/astro/commit/1eae2e3f7d693c9dfe91c8ccfbe606d32bf2fb81), [`76ddef19c`](https://github.com/withastro/astro/commit/76ddef19ccab6e5f7d3a5740cd41acf10e334b38), [`9b4f70a62`](https://github.com/withastro/astro/commit/9b4f70a629f55e461759ba46f68af7097a2e9215), [`3fdf509b2`](https://github.com/withastro/astro/commit/3fdf509b2731a9b2f972d89291e57cf78d62c769), [`2f951cd40`](https://github.com/withastro/astro/commit/2f951cd403dfcc2c3ca6aae618ae3e1409516e32), [`c022a4217`](https://github.com/withastro/astro/commit/c022a4217a805d223c1494e9eda4e48bbf810388), [`67becaa58`](https://github.com/withastro/astro/commit/67becaa580b8f787df58de66b7008b7098f1209c), [`bc37331d8`](https://github.com/withastro/astro/commit/bc37331d8154e3e95a8df9131e4e014e78a7a9e7), [`dfc2d93e3`](https://github.com/withastro/astro/commit/dfc2d93e3c645995379358fabbdfa9aab99f43d8), [`3dc1ca2fa`](https://github.com/withastro/astro/commit/3dc1ca2fac8d9965cc5085a5d09e72ed87b4281a), [`1be84dfee`](https://github.com/withastro/astro/commit/1be84dfee3ce8e6f5cc624f99aec4e980f6fde37), [`35f01df79`](https://github.com/withastro/astro/commit/35f01df797d23315f2bee2fc3fd795adb0559c58), [`3fdf509b2`](https://github.com/withastro/astro/commit/3fdf509b2731a9b2f972d89291e57cf78d62c769), [`78de801f2`](https://github.com/withastro/astro/commit/78de801f21fd4ca1653950027d953bf08614566b), [`59d6e569f`](https://github.com/withastro/astro/commit/59d6e569f63e175c97e82e94aa7974febfb76f7c), [`7723c4cc9`](https://github.com/withastro/astro/commit/7723c4cc93298c2e6530e55da7afda048f22cf81), [`fb5cd6b56`](https://github.com/withastro/astro/commit/fb5cd6b56dc27a71366ed5e1ab8bfe9b8f96bac5), [`631b9c410`](https://github.com/withastro/astro/commit/631b9c410d5d66fa384674027ba95d69ebb5063f)]:
- astro@3.0.0-beta.0
+## 5.3.6
+
+### Patch Changes
+
+- [#8176](https://github.com/withastro/astro/pull/8176) [`d08c83ee3`](https://github.com/withastro/astro/commit/d08c83ee3fe0f10374264f61ee473255dcf0cd06) Thanks [@ematipico](https://github.com/ematipico)! - Fix an issue where `express` couldn't use the `handler` in `middleware` mode.
+
+- Updated dependencies [[`582132328`](https://github.com/withastro/astro/commit/5821323285646aee7ff9194a505f708028e4db57), [`fddd4dc71`](https://github.com/withastro/astro/commit/fddd4dc71af321bd6b4d01bb4b1b955284846e60), [`cfc465dde`](https://github.com/withastro/astro/commit/cfc465ddebcc58d20f29ecffaa857a77525435a9), [`95120efbe`](https://github.com/withastro/astro/commit/95120efbe817163663492181cbeb225849354493), [`273335cb0`](https://github.com/withastro/astro/commit/273335cb01615c3c06d46c02464f4496a81f8d0b), [`9142178b1`](https://github.com/withastro/astro/commit/9142178b113443749b87c1d259859b42a3d7a9c4), [`179796405`](https://github.com/withastro/astro/commit/179796405e053b559d83f84507e5a465861a029a)]:
+ - astro@2.10.13
+
## 5.3.5
### Patch Changes
diff --git a/packages/integrations/node/README.md b/packages/integrations/node/README.md
index 5d3642be6..5b49a2716 100644
--- a/packages/integrations/node/README.md
+++ b/packages/integrations/node/README.md
@@ -31,6 +31,8 @@ yarn astro add node
pnpm astro add node
```
+### Add dependencies manually
+
If you prefer to install the adapter manually instead, complete the following two steps:
1. Install the Node adapter to your project’s dependencies using your preferred package manager. If you’re using npm or aren’t sure, run this in the terminal:
diff --git a/packages/integrations/vercel/README.md b/packages/integrations/vercel/README.md
index 7f13fe6e7..b01a7c716 100644
--- a/packages/integrations/vercel/README.md
+++ b/packages/integrations/vercel/README.md
@@ -33,6 +33,8 @@ yarn astro add vercel
pnpm astro add vercel
```
+### Add dependencies manually
+
If you prefer to install the adapter manually instead, complete the following two steps:
1. Install the Vercel adapter to your project’s dependencies using your preferred package manager. If you’re using npm or aren’t sure, run this in the terminal: