diff --git a/.changeset/shy-cats-heal.md b/.changeset/shy-cats-heal.md new file mode 100644 index 000000000000..33d75275159f --- /dev/null +++ b/.changeset/shy-cats-heal.md @@ -0,0 +1,5 @@ +--- +'@astrojs/node': patch +--- + +Fixes support for prerendering and query params diff --git a/packages/integrations/node/package.json b/packages/integrations/node/package.json index 39060dcf3094..a9a6a9c0492a 100644 --- a/packages/integrations/node/package.json +++ b/packages/integrations/node/package.json @@ -31,13 +31,15 @@ }, "dependencies": { "@astrojs/webapi": "^2.0.0", - "send": "^0.18.0" + "send": "^0.18.0", + "server-destroy": "^1.0.1" }, "peerDependencies": { "astro": "workspace:^2.0.6" }, "devDependencies": { "@types/send": "^0.17.1", + "@types/server-destroy": "^1.0.1", "astro": "workspace:*", "astro-scripts": "workspace:*", "chai": "^4.3.6", diff --git a/packages/integrations/node/src/http-server.ts b/packages/integrations/node/src/http-server.ts index 8eea3c170e80..fee30aaecc82 100644 --- a/packages/integrations/node/src/http-server.ts +++ b/packages/integrations/node/src/http-server.ts @@ -3,6 +3,7 @@ import http from 'http'; import https from 'https'; import send from 'send'; import { fileURLToPath } from 'url'; +import enableDestroy from 'server-destroy'; interface CreateServerOptions { client: URL; @@ -19,6 +20,7 @@ export function createServer( if (req.url) { let pathname = removeBase(req.url); pathname = pathname[0] === '/' ? pathname : '/' + pathname; + pathname = new URL(pathname, `http://${host}:${port}`).pathname; const stream = send(req, encodeURI(decodeURI(pathname)), { root: fileURLToPath(client), dotfiles: pathname.startsWith('/.well-known/') ? 'allow' : 'deny', @@ -63,6 +65,7 @@ export function createServer( httpServer = http.createServer(listener); } httpServer.listen(port, host); + enableDestroy(httpServer); // Resolves once the server is closed const closed = new Promise((resolve, reject) => { @@ -79,7 +82,7 @@ export function createServer( server: httpServer, stop: async () => { await new Promise((resolve, reject) => { - httpServer.close((err) => (err ? reject(err) : resolve(undefined))); + httpServer.destroy((err) => (err ? reject(err) : resolve(undefined))); }); }, }; diff --git a/packages/integrations/node/src/index.ts b/packages/integrations/node/src/index.ts index bbe5e44b4cb6..d882f34fb3ab 100644 --- a/packages/integrations/node/src/index.ts +++ b/packages/integrations/node/src/index.ts @@ -6,7 +6,7 @@ export function getAdapter(options: Options): AstroAdapter { name: '@astrojs/node', serverEntrypoint: '@astrojs/node/server.js', previewEntrypoint: '@astrojs/node/preview.js', - exports: ['handler'], + exports: ['handler', 'startServer'], args: options, }; } diff --git a/packages/integrations/node/src/server.ts b/packages/integrations/node/src/server.ts index e0ccf6599ed2..c4c79f6a8ec2 100644 --- a/packages/integrations/node/src/server.ts +++ b/packages/integrations/node/src/server.ts @@ -13,6 +13,7 @@ export function createExports(manifest: SSRManifest, options: Options) { const app = new NodeApp(manifest); return { handler: middleware(app, options.mode), + startServer: () => startServer(app, options) }; } diff --git a/packages/integrations/node/src/standalone.ts b/packages/integrations/node/src/standalone.ts index 789269860308..d3ab3679308d 100644 --- a/packages/integrations/node/src/standalone.ts +++ b/packages/integrations/node/src/standalone.ts @@ -55,8 +55,12 @@ export default function startServer(app: NodeApp, options: Options) { ); const protocol = server.server instanceof https.Server ? 'https' : 'http'; + // eslint-disable-next-line no-console console.log(`Server listening on ${protocol}://${host}:${port}`); - return server.closed(); + return { + server, + done: server.closed() + }; } diff --git a/packages/integrations/node/test/fixtures/prerender/package.json b/packages/integrations/node/test/fixtures/prerender/package.json new file mode 100644 index 000000000000..350077973049 --- /dev/null +++ b/packages/integrations/node/test/fixtures/prerender/package.json @@ -0,0 +1,9 @@ +{ + "name": "@test/nodejs-encoded", + "version": "0.0.0", + "private": true, + "dependencies": { + "astro": "workspace:*", + "@astrojs/node": "workspace:*" + } +} diff --git a/packages/integrations/node/test/fixtures/prerender/src/pages/one.astro b/packages/integrations/node/test/fixtures/prerender/src/pages/one.astro new file mode 100644 index 000000000000..f3a26721d7d3 --- /dev/null +++ b/packages/integrations/node/test/fixtures/prerender/src/pages/one.astro @@ -0,0 +1,10 @@ +--- +--- + + + One + + +

One

+ + diff --git a/packages/integrations/node/test/fixtures/prerender/src/pages/two.astro b/packages/integrations/node/test/fixtures/prerender/src/pages/two.astro new file mode 100644 index 000000000000..beb6e8d788bd --- /dev/null +++ b/packages/integrations/node/test/fixtures/prerender/src/pages/two.astro @@ -0,0 +1,11 @@ +--- +export const prerender = true; +--- + + + Two + + +

Two

+ + diff --git a/packages/integrations/node/test/prerender.test.js b/packages/integrations/node/test/prerender.test.js new file mode 100644 index 000000000000..e5c94391f74a --- /dev/null +++ b/packages/integrations/node/test/prerender.test.js @@ -0,0 +1,60 @@ +import nodejs from '../dist/index.js'; +import { loadFixture, createRequestAndResponse } from './test-utils.js'; +import { expect } from 'chai'; +import * as cheerio from 'cheerio'; +import { fetch } from 'undici'; + +describe('Prerendering', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + let server; + + before(async () => { + process.env.ASTRO_NODE_AUTOSTART = 'disabled'; + fixture = await loadFixture({ + root: './fixtures/prerender/', + output: 'server', + adapter: nodejs({ mode: 'standalone' }), + }); + await fixture.build(); + const { startServer } = await await load(); + let res = startServer(); + server = res.server; + }); + + after(async () => { + await server.stop(); + }); + + async function load() { + const mod = await import('./fixtures/prerender/dist/server/entry.mjs'); + return mod; + } + + it('Can render SSR route', async () => { + const res = await fetch(`http://${server.host}:${server.port}/one`); + const html = await res.text(); + const $ = cheerio.load(html); + + expect(res.status).to.equal(200); + expect($('h1').text()).to.equal('One'); + }); + + it('Can render prerendered route', async () => { + const res = await fetch(`http://${server.host}:${server.port}/two`); + const html = await res.text(); + const $ = cheerio.load(html); + + expect(res.status).to.equal(200); + expect($('h1').text()).to.equal('Two'); + }); + + it('Can render prerendered route with query params', async () => { + const res = await fetch(`http://${server.host}:${server.port}/two?foo=bar`); + const html = await res.text(); + const $ = cheerio.load(html); + + expect(res.status).to.equal(200); + expect($('h1').text()).to.equal('Two'); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ac4eedde9dde..606d6a752cf7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3070,6 +3070,7 @@ importers: specifiers: '@astrojs/webapi': ^2.0.0 '@types/send': ^0.17.1 + '@types/server-destroy': ^1.0.1 astro: workspace:* astro-scripts: workspace:* chai: ^4.3.6 @@ -3077,12 +3078,15 @@ importers: mocha: ^9.2.2 node-mocks-http: ^1.11.0 send: ^0.18.0 + server-destroy: ^1.0.1 undici: ^5.14.0 dependencies: '@astrojs/webapi': link:../../webapi send: 0.18.0 + server-destroy: 1.0.1 devDependencies: '@types/send': 0.17.1 + '@types/server-destroy': 1.0.1 astro: link:../../astro astro-scripts: link:../../../scripts chai: 4.3.7 @@ -3115,6 +3119,14 @@ importers: '@astrojs/node': link:../../.. astro: link:../../../../../astro + packages/integrations/node/test/fixtures/prerender: + specifiers: + '@astrojs/node': workspace:* + astro: workspace:* + dependencies: + '@astrojs/node': link:../../.. + astro: link:../../../../../astro + packages/integrations/node/test/fixtures/url-protocol: specifiers: '@astrojs/node': workspace:*