diff --git a/examples/minimal/astro.config.mjs b/examples/minimal/astro.config.mjs
index 882e6515a..4537da20a 100644
--- a/examples/minimal/astro.config.mjs
+++ b/examples/minimal/astro.config.mjs
@@ -1,4 +1,10 @@
import { defineConfig } from 'astro/config';
+import node from "@astrojs/node";
// https://astro.build/config
-export default defineConfig({});
+export default defineConfig({
+ output: "server",
+ adapter: node({
+ mode: "standalone"
+ })
+});
diff --git a/examples/minimal/package.json b/examples/minimal/package.json
index 642dfd2b0..51b6607b9 100644
--- a/examples/minimal/package.json
+++ b/examples/minimal/package.json
@@ -11,6 +11,7 @@
"astro": "astro"
},
"dependencies": {
+ "@astrojs/node": "^5.1.1",
"astro": "^2.4.1"
}
}
diff --git a/examples/minimal/src/component/Deep.astro b/examples/minimal/src/component/Deep.astro
new file mode 100644
index 000000000..940b69e68
--- /dev/null
+++ b/examples/minimal/src/component/Deep.astro
@@ -0,0 +1,3 @@
+
+
+
diff --git a/examples/minimal/src/component/Tabs.astro b/examples/minimal/src/component/Tabs.astro
new file mode 100644
index 000000000..c01894bb4
--- /dev/null
+++ b/examples/minimal/src/component/Tabs.astro
@@ -0,0 +1,10 @@
+---
+const { tab } = Astro.props;
+---
+
+
+Tab {tab}
diff --git a/examples/minimal/src/layouts/Default.astro b/examples/minimal/src/layouts/Default.astro
new file mode 100644
index 000000000..ad57a8b31
--- /dev/null
+++ b/examples/minimal/src/layouts/Default.astro
@@ -0,0 +1,132 @@
+---
+import { Outlet } from 'astro/components';
+---
+
+
+
+
+
+
+
+ Astro
+
+
+
+
+
+
+
+ {() => new Promise((resolve) => setTimeout(resolve, 1000))}
+
+ {() => new Promise((resolve) => setTimeout(resolve, 1000))}
+
+
+
+
+
+
+
+
+
diff --git a/examples/minimal/src/pages/a/[tab].astro b/examples/minimal/src/pages/a/[tab].astro
new file mode 100644
index 000000000..1b53d85ce
--- /dev/null
+++ b/examples/minimal/src/pages/a/[tab].astro
@@ -0,0 +1,22 @@
+---
+import Layout from '../../layouts/Default.astro';
+import Tab from '../../component/Tabs.astro';
+const { tab = '' } = Astro.params;
+---
+
+
+
+
Page A
+
+
+
+
+
+
diff --git a/examples/minimal/src/pages/a/index.astro b/examples/minimal/src/pages/a/index.astro
new file mode 100644
index 000000000..dfbed4c69
--- /dev/null
+++ b/examples/minimal/src/pages/a/index.astro
@@ -0,0 +1,21 @@
+---
+import Layout from '../../layouts/Default.astro';
+import Tab from '../../component/Tabs.astro';
+---
+
+
+
+
Page A
+
+
+
+
+
+
diff --git a/examples/minimal/src/pages/b.astro b/examples/minimal/src/pages/b.astro
new file mode 100644
index 000000000..9a935f9a4
--- /dev/null
+++ b/examples/minimal/src/pages/b.astro
@@ -0,0 +1,20 @@
+---
+import Layout from '../layouts/Default.astro';
+import Deep from '../component/Deep.astro';
+---
+
+
+
+ Page B
+ No delay
+
+
+
+
diff --git a/examples/minimal/src/pages/c.astro b/examples/minimal/src/pages/c.astro
new file mode 100644
index 000000000..45c5f9c91
--- /dev/null
+++ b/examples/minimal/src/pages/c.astro
@@ -0,0 +1,22 @@
+---
+import Layout from '../layouts/Default.astro';
+import { setTimeout as sleep } from 'node:timers/promises';
+---
+
+
+
+
Page C
+
This is fast
+ {sleep(1000)}
+
This is slow
+
+
+
+
diff --git a/examples/minimal/src/pages/index.astro b/examples/minimal/src/pages/index.astro
index 7264ff502..5b0032ab8 100644
--- a/examples/minimal/src/pages/index.astro
+++ b/examples/minimal/src/pages/index.astro
@@ -1,15 +1,19 @@
---
+import Layout from '../layouts/Default.astro';
---
-
-
-
-
-
-
- Astro
-
-
- Astro
-
-
+
+
+
Home
+
Streaming partials!
+
+
+
+
diff --git a/packages/astro/components/Outlet.astro b/packages/astro/components/Outlet.astro
new file mode 100644
index 000000000..347e372b4
--- /dev/null
+++ b/packages/astro/components/Outlet.astro
@@ -0,0 +1,14 @@
+---
+interface Props {
+ id: string;
+}
+
+const { id } = Astro.props;
+
+// @ts-ignore untyped internals
+// $$result.outlets.set(id, $$slots.default);
+---
+
+`} />`}
+/>
diff --git a/packages/astro/components/index.ts b/packages/astro/components/index.ts
index 864c7cc3b..b2b0068cf 100644
--- a/packages/astro/components/index.ts
+++ b/packages/astro/components/index.ts
@@ -1,2 +1,3 @@
export { default as Code } from './Code.astro';
export { default as Debug } from './Debug.astro';
+export { default as Outlet } from './Outlet.astro';
diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts
index 0f8cf4240..70ec81d9f 100644
--- a/packages/astro/src/@types/astro.ts
+++ b/packages/astro/src/@types/astro.ts
@@ -1750,6 +1750,7 @@ export interface SSRMetadata {
hasDirectives: Set;
hasRenderedHead: boolean;
headInTree: boolean;
+ request: Request;
}
/**
@@ -1775,7 +1776,9 @@ export interface SSRResult {
links: Set;
componentMetadata: Map;
propagators: Map;
+ outletPropagators: Map;
extraHead: Array;
+ outlets: Map;
cookies: AstroCookies | undefined;
createAstro(
Astro: AstroGlobalPartial,
diff --git a/packages/astro/src/core/render/result.ts b/packages/astro/src/core/render/result.ts
index 598ec116f..63fd2c4d1 100644
--- a/packages/astro/src/core/render/result.ts
+++ b/packages/astro/src/core/render/result.ts
@@ -163,7 +163,9 @@ export function createResult(args: CreateResultArgs): SSRResult {
links: args.links ?? new Set(),
componentMetadata,
propagators: new Map(),
+ outletPropagators: new Map(),
extraHead: [],
+ outlets: new Map(),
scope: 0,
cookies,
/** This function returns the `Astro` faux-global */
@@ -260,6 +262,7 @@ export function createResult(args: CreateResultArgs): SSRResult {
hasRenderedHead: false,
hasDirectives: new Set(),
headInTree: false,
+ request
},
response,
};
diff --git a/packages/astro/src/runtime/server/astro-component.ts b/packages/astro/src/runtime/server/astro-component.ts
index 44428b929..3ddc2d8f4 100644
--- a/packages/astro/src/runtime/server/astro-component.ts
+++ b/packages/astro/src/runtime/server/astro-component.ts
@@ -7,7 +7,7 @@ function validateArgs(args: unknown[]): args is Parameters) => {
if (!validateArgs(args)) {
@@ -22,6 +22,7 @@ function baseCreateComponent(cb: AstroComponentFactory, moduleId?: string): Astr
// Add a flag to this callback to mark it as an Astro component
fn.isAstroComponentFactory = true;
fn.moduleId = moduleId;
+ fn.hash = hash;
return fn;
}
@@ -29,20 +30,23 @@ interface CreateComponentOptions {
factory: AstroComponentFactory;
moduleId?: string;
propagation?: PropagationHint;
+ hash?: string;
}
function createComponentWithOptions(opts: CreateComponentOptions) {
const cb = baseCreateComponent(opts.factory, opts.moduleId);
cb.propagation = opts.propagation;
+ cb.hash = opts.hash;
return cb;
}
// Used in creating the component. aka the main export.
export function createComponent(
arg1: AstroComponentFactory | CreateComponentOptions,
- moduleId?: string
+ moduleId?: string,
+ hash?: string
) {
if (typeof arg1 === 'function') {
- return baseCreateComponent(arg1, moduleId);
+ return baseCreateComponent(arg1, moduleId, hash);
} else {
return createComponentWithOptions(arg1);
}
diff --git a/packages/astro/src/runtime/server/render/astro/factory.ts b/packages/astro/src/runtime/server/render/astro/factory.ts
index 6d1b08563..c61b35407 100644
--- a/packages/astro/src/runtime/server/render/astro/factory.ts
+++ b/packages/astro/src/runtime/server/render/astro/factory.ts
@@ -14,6 +14,7 @@ export interface AstroComponentFactory {
isAstroComponentFactory?: boolean;
moduleId?: string | undefined;
propagation?: PropagationHint;
+ hash?: string;
}
export function isAstroComponentFactory(obj: any): obj is AstroComponentFactory {
diff --git a/packages/astro/src/runtime/server/render/astro/instance.ts b/packages/astro/src/runtime/server/render/astro/instance.ts
index 47ce7f495..1a9f87f18 100644
--- a/packages/astro/src/runtime/server/render/astro/instance.ts
+++ b/packages/astro/src/runtime/server/render/astro/instance.ts
@@ -81,6 +81,12 @@ export function createAstroComponentInstance(
) {
validateComponentProps(props, displayName);
const instance = new AstroComponentInstance(result, props, slots, factory);
+ if (result._metadata.request.headers.has('x-astro-outlet')) {
+ for (const [name, slot] of Object.entries(slots)) {
+ result.outletPropagators.set(`${factory.hash} ${name}`, slot);
+ }
+ return instance;
+ }
if (isAPropagatingComponent(result, factory) && !result.propagators.has(factory)) {
result.propagators.set(factory, instance);
}
diff --git a/packages/astro/src/runtime/server/render/page.ts b/packages/astro/src/runtime/server/render/page.ts
index ce2d165a7..df80c86d1 100644
--- a/packages/astro/src/runtime/server/render/page.ts
+++ b/packages/astro/src/runtime/server/render/page.ts
@@ -67,6 +67,23 @@ async function bufferHeadContent(result: SSRResult) {
}
}
+// Recursively calls component instances that might have slots to be propagated up.
+async function bufferSlottedContent(result: SSRResult, outlet: string) {
+ const iterator = result.outletPropagators.entries();
+ let promises = [];
+ while (true) {
+ const { value: [name, slot] = [], done } = iterator.next();
+ if (done) {
+ break;
+ }
+ const returnValue = await slot(result);
+ result.outlets.set(name, returnValue);
+ if (name === outlet) {
+ break;
+ }
+ }
+}
+
export async function renderPage(
result: SSRResult,
componentFactory: AstroComponentFactory | NonAstroPageComponent,
@@ -131,7 +148,66 @@ export async function renderPage(
result.componentMetadata.get(componentFactory.moduleId!)?.containsHead ?? false;
const factoryReturnValue = await componentFactory(result, props, children);
const factoryIsHeadAndContent = isHeadAndContent(factoryReturnValue);
- if (isRenderTemplateResult(factoryReturnValue) || factoryIsHeadAndContent) {
+ if (result._metadata.request.headers.get('x-astro-outlet') && (isRenderTemplateResult(factoryReturnValue) || factoryIsHeadAndContent)) {
+ const outlet = result._metadata.request.headers.get('x-astro-outlet')!;
+ result.scripts.clear();
+ await bufferSlottedContent(result, outlet);
+
+ if (!result.outlets.get(outlet)) {
+ let init = result.response;
+ let headers = new Headers(init.headers);
+ let response = createResponse(null, { ...init, headers, status: 404 });
+ return response;
+ }
+
+ let init = result.response;
+ let headers = new Headers(init.headers);
+ let body: BodyInit;
+
+ if (streaming) {
+ body = new ReadableStream({
+ start(controller) {
+ async function read() {
+ try {
+ const template = result.outlets.get(outlet);
+ for await (const chunk of renderAstroTemplateResult(template)) {
+ const bytes = chunkToByteArray(result, chunk);
+ controller.enqueue(bytes);
+ }
+ controller.close();
+ } catch (e) {
+ // We don't have a lot of information downstream, and upstream we can't catch the error properly
+ // So let's add the location here
+ if (AstroError.is(e) && !e.loc) {
+ e.setLocation({
+ file: route?.component,
+ });
+ }
+
+ controller.error(e);
+ }
+ }
+ read();
+ },
+ });
+ } else {
+ let iterable = (async function* () {
+ for (const [key, slot] of result.outlets) {
+ yield `\n`;
+ for await (const chunk of renderAstroTemplateResult(slot)) {
+ const bytes = chunkToByteArray(result, chunk);
+ yield bytes;
+ }
+ yield '\n\n\n';
+ }
+ })()
+ body = await iterableToHTMLBytes(result, iterable);
+ headers.set('Content-Length', body.byteLength.toString());
+ }
+
+ let response = createResponse(body, { ...init, headers });
+ return response;
+ } else if (isRenderTemplateResult(factoryReturnValue) || factoryIsHeadAndContent) {
// Wait for head content to be buffered up
await bufferHeadContent(result);
const templateResult = factoryIsHeadAndContent
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index a39fb6cbe..652780c80 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -375,6 +375,9 @@ importers:
examples/minimal:
dependencies:
+ '@astrojs/node':
+ specifier: ^5.1.1
+ version: link:../../packages/integrations/node
astro:
specifier: ^2.4.1
version: link:../../packages/astro