Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .changeset/cold-cooks-battle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
'@astrojs/node': minor
---

Adds a new experimental configuration option `experimentalDisableStreaming` to allow you to opt out of Astro's default [HTML streaming](https://docs.astro.build/en/guides/on-demand-rendering/#html-streaming) for pages rendered on demand.

HTML streaming helps with performance and generally provides a better visitor experience. In most cases, disabling streaming is not recommended.

However, when you need to disable HTML streaming (e.g. your host only supports non-streamed HTML caching at the CDN level), you can now opt out of the default behavior:

```diff
import { defineConfig } from 'astro/config';
import node from '@astrojs/node';

export default defineConfig({
adapter: node({
mode: 'standalone',
+ experimentalDisableStreaming: true,
}),
});
```
4 changes: 2 additions & 2 deletions packages/integrations/node/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import type { Options } from './types.js';
setGetEnv((key) => process.env[key]);

export function createExports(manifest: SSRManifest, options: Options) {
const app = new NodeApp(manifest);
const app = new NodeApp(manifest, !options.experimentalDisableStreaming);
let headersMap: NodeAppHeadersJson | undefined = undefined;
if (options.experimentalStaticHeaders) {
headersMap = readHeadersJson(manifest.outDir);
Expand Down Expand Up @@ -41,7 +41,7 @@ export function start(manifest: SSRManifest, options: Options) {
headersMap = readHeadersJson(manifest.outDir);
}

const app = new NodeApp(manifest);
const app = new NodeApp(manifest, !options.experimentalDisableStreaming);
if (headersMap) {
app.setHeadersMap(headersMap);
}
Expand Down
4 changes: 4 additions & 0 deletions packages/integrations/node/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ export interface UserOptions {
* - 'standalone' - Build to a standalone server. The server starts up just by running the built script.
*/
mode: 'middleware' | 'standalone';
/**
* Disables HTML streaming. This is useful for example if there are constraints from your host.
*/
experimentalDisableStreaming?: boolean;

/**
* If enabled, the adapter will save [static headers in the framework API file](https://docs.netlify.com/frameworks-api/#headers).
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
import Wrapper from "../components/Wrapper.astro";
import Wait from "../components/Wait.astro";
---

<Wrapper>
<Wait />
</Wrapper>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
await new Promise(r => setTimeout(r, 500))
---

<p>hello world</p>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<div>
<slot />
</div>
217 changes: 132 additions & 85 deletions packages/integrations/node/test/headers.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,124 +7,171 @@ describe('Node Adapter Headers', () => {
/** @type {import('./test-utils').Fixture} */
let fixture;

before(async () => {
fixture = await loadFixture({
root: './fixtures/headers/',
output: 'server',
adapter: nodejs({ mode: 'middleware' }),
describe('streaming', () => {
before(async () => {
fixture = await loadFixture({
root: './fixtures/headers/',
output: 'server',
adapter: nodejs({ mode: 'middleware' }),
});
await fixture.build();
});
await fixture.build();
});

it('Endpoint Simple Headers', async () => {
await runTest('/endpoints/simple', {
'content-type': 'text/plain;charset=utf-8',
'x-hello': 'world',
it('Endpoint Simple Headers', async () => {
await runTest('/endpoints/simple', {
'content-type': 'text/plain;charset=utf-8',
'x-hello': 'world',
});
});
});

it('Endpoint Astro Single Cookie Header', async () => {
await runTest('/endpoints/astro-cookies-single', {
'content-type': 'text/plain;charset=utf-8',
'set-cookie': 'from1=astro1',
it('Endpoint Astro Single Cookie Header', async () => {
await runTest('/endpoints/astro-cookies-single', {
'content-type': 'text/plain;charset=utf-8',
'set-cookie': 'from1=astro1',
});
});
});

it('Endpoint Astro Multi Cookie Header', async () => {
await runTest('/endpoints/astro-cookies-multi', {
'content-type': 'text/plain;charset=utf-8',
'set-cookie': ['from1=astro1', 'from2=astro2'],
it('Endpoint Astro Multi Cookie Header', async () => {
await runTest('/endpoints/astro-cookies-multi', {
'content-type': 'text/plain;charset=utf-8',
'set-cookie': ['from1=astro1', 'from2=astro2'],
});
});
});

it('Endpoint Response Single Cookie Header', async () => {
await runTest('/endpoints/response-cookies-single', {
'content-type': 'text/plain;charset=utf-8',
'set-cookie': 'hello1=world1',
it('Endpoint Response Single Cookie Header', async () => {
await runTest('/endpoints/response-cookies-single', {
'content-type': 'text/plain;charset=utf-8',
'set-cookie': 'hello1=world1',
});
});
});

it('Endpoint Response Multi Cookie Header', async () => {
await runTest('/endpoints/response-cookies-multi', {
'content-type': 'text/plain;charset=utf-8',
'set-cookie': ['hello1=world1', 'hello2=world2'],
it('Endpoint Response Multi Cookie Header', async () => {
await runTest('/endpoints/response-cookies-multi', {
'content-type': 'text/plain;charset=utf-8',
'set-cookie': ['hello1=world1', 'hello2=world2'],
});
});
});

it('Endpoint Complex Headers Kitchen Sink', async () => {
await runTest('/endpoints/kitchen-sink', {
'content-type': 'text/plain;charset=utf-8',
'x-single': 'single',
'x-triple': 'one, two, three',
'set-cookie': ['hello1=world1', 'hello2=world2'],
it('Endpoint Complex Headers Kitchen Sink', async () => {
await runTest('/endpoints/kitchen-sink', {
'content-type': 'text/plain;charset=utf-8',
'x-single': 'single',
'x-triple': 'one, two, three',
'set-cookie': ['hello1=world1', 'hello2=world2'],
});
});
});

it('Endpoint Astro and Response Single Cookie Header', async () => {
await runTest('/endpoints/astro-response-cookie-single', {
'content-type': 'text/plain;charset=utf-8',
'set-cookie': ['from1=response1', 'from1=astro1'],
it('Endpoint Astro and Response Single Cookie Header', async () => {
await runTest('/endpoints/astro-response-cookie-single', {
'content-type': 'text/plain;charset=utf-8',
'set-cookie': ['from1=response1', 'from1=astro1'],
});
});
});

it('Endpoint Astro and Response Multi Cookie Header', async () => {
await runTest('/endpoints/astro-response-cookie-multi', {
'content-type': 'text/plain;charset=utf-8',
'set-cookie': ['from1=response1', 'from2=response2', 'from3=astro1', 'from4=astro2'],
it('Endpoint Astro and Response Multi Cookie Header', async () => {
await runTest('/endpoints/astro-response-cookie-multi', {
'content-type': 'text/plain;charset=utf-8',
'set-cookie': ['from1=response1', 'from2=response2', 'from3=astro1', 'from4=astro2'],
});
});
});

it('Endpoint Response Empty Headers Object', async () => {
await runTest('/endpoints/response-empty-headers-object', {
'content-type': 'text/plain;charset=UTF-8',
it('Endpoint Response Empty Headers Object', async () => {
await runTest('/endpoints/response-empty-headers-object', {
'content-type': 'text/plain;charset=UTF-8',
});
});
});

it('Endpoint Response undefined Headers Object', async () => {
await runTest('/endpoints/response-undefined-headers-object', {
'content-type': 'text/plain;charset=UTF-8',
it('Endpoint Response undefined Headers Object', async () => {
await runTest('/endpoints/response-undefined-headers-object', {
'content-type': 'text/plain;charset=UTF-8',
});
});
});

it('Component Astro Single Cookie Header', async () => {
await runTest('/astro/component-astro-cookies-single', {
'content-type': 'text/html',
'set-cookie': 'from1=astro1',
it('Component Astro Single Cookie Header', async () => {
await runTest('/astro/component-astro-cookies-single', {
'content-type': 'text/html',
'set-cookie': 'from1=astro1',
});
});
});

it('Component Astro Multi Cookie Header', async () => {
await runTest('/astro/component-astro-cookies-multi', {
'content-type': 'text/html',
'set-cookie': ['from1=astro1', 'from2=astro2'],
it('Component Astro Multi Cookie Header', async () => {
await runTest('/astro/component-astro-cookies-multi', {
'content-type': 'text/html',
'set-cookie': ['from1=astro1', 'from2=astro2'],
});
});
});

it('Component Response Single Cookie Header', async () => {
await runTest('/astro/component-response-cookies-single', {
'content-type': 'text/html',
'set-cookie': 'from1=value1',
it('Component Response Single Cookie Header', async () => {
await runTest('/astro/component-response-cookies-single', {
'content-type': 'text/html',
'set-cookie': 'from1=value1',
});
});
});

it('Component Response Multi Cookie Header', async () => {
await runTest('/astro/component-response-cookies-multi', {
'content-type': 'text/html',
'set-cookie': ['from1=value1', 'from2=value2'],
it('Component Response Multi Cookie Header', async () => {
await runTest('/astro/component-response-cookies-multi', {
'content-type': 'text/html',
'set-cookie': ['from1=value1', 'from2=value2'],
});
});
});

it('Component Astro and Response Single Cookie Header', async () => {
await runTest('/astro/component-astro-response-cookie-single', {
'content-type': 'text/html',
'set-cookie': ['from1=response1', 'from1=astro1'],
it('Component Astro and Response Single Cookie Header', async () => {
await runTest('/astro/component-astro-response-cookie-single', {
'content-type': 'text/html',
'set-cookie': ['from1=response1', 'from1=astro1'],
});
});

it('Component Astro and Response Multi Cookie Header', async () => {
await runTest('/astro/component-astro-response-cookie-multi', {
'content-type': 'text/html',
'set-cookie': ['from1=response1', 'from2=response2', 'from3=astro1', 'from4=astro2'],
});
});

// TODO: needs e2e tests to check real headers
it('sends several chunks', async () => {
const { handler } = await import('./fixtures/headers/dist/server/entry.mjs');

const { req, res, done } = createRequestAndResponse({
method: 'GET',
url: '/astro/component-simple',
});

handler(req, res);

req.send();

const chunks = await done;
assert.equal(chunks.length, 3);
});
});

it('Component Astro and Response Multi Cookie Header', async () => {
await runTest('/astro/component-astro-response-cookie-multi', {
'content-type': 'text/html',
'set-cookie': ['from1=response1', 'from2=response2', 'from3=astro1', 'from4=astro2'],
describe('without streaming', () => {
before(async () => {
fixture = await loadFixture({
root: './fixtures/headers/',
output: 'server',
adapter: nodejs({ mode: 'middleware', experimentalDisableStreaming: true }),
});
await fixture.build();
});

// TODO: needs e2e tests to check real headers
it('sends a single chunk', async () => {
const { handler } = await import('./fixtures/headers/dist/server/entry.mjs?cachebust=0');

const { req, res, done } = createRequestAndResponse({
method: 'GET',
url: '/astro/component-simple',
});

handler(req, res);

req.send();

const chunks = await done;
assert.equal(chunks.length, 1);
});
});
});
Expand Down
1 change: 1 addition & 0 deletions packages/integrations/node/test/test-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export function createRequestAndResponse(reqOptions) {
return { req, res, done, text };
}

/** @returns {Promise<Array<Buffer>>} */
function toPromise(res) {
return new Promise((resolve) => {
// node-mocks-http doesn't correctly handle non-Buffer typed arrays,
Expand Down
Loading