-
-
Notifications
You must be signed in to change notification settings - Fork 2.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Node.js standalone mode + support for astro preview (#5056)
* wip * Deprecate buildConfig and move to config.build * Implement the standalone server * Stay backwards compat * Add changesets * correctly merge URLs * Get config earlier * update node tests * Return the preview server * update remaining tests * swap usage and config ordering * Update packages/astro/src/@types/astro.ts Co-authored-by: Sarah Rainsberger <[email protected]> * Update .changeset/metal-pumas-walk.md Co-authored-by: Sarah Rainsberger <[email protected]> * Update .changeset/metal-pumas-walk.md Co-authored-by: Sarah Rainsberger <[email protected]> * Update .changeset/stupid-points-refuse.md Co-authored-by: Sarah Rainsberger <[email protected]> * Update .changeset/stupid-points-refuse.md Co-authored-by: Sarah Rainsberger <[email protected]> * Link to build.server config Co-authored-by: Fred K. Schott <[email protected]> Co-authored-by: Sarah Rainsberger <[email protected]>
- Loading branch information
1 parent
1ae1b9b
commit 28b0857
Showing
10 changed files
with
342 additions
and
83 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
import fs from 'fs'; | ||
import http from 'http'; | ||
import https from 'https'; | ||
import { fileURLToPath } from 'url'; | ||
import send from 'send'; | ||
|
||
interface CreateServerOptions { | ||
client: URL; | ||
port: number; | ||
host: string | undefined; | ||
} | ||
|
||
export function createServer({ client, port, host }: CreateServerOptions, handler: http.RequestListener) { | ||
const listener: http.RequestListener = (req, res) => { | ||
if(req.url) { | ||
const fileURL = new URL('.' + req.url, client); | ||
|
||
const stream = send(req, fileURLToPath(fileURL), { | ||
dotfiles: 'deny', | ||
}); | ||
|
||
let forwardError = false; | ||
|
||
stream.on('error', err => { | ||
if(forwardError) { | ||
// eslint-disable-next-line no-console | ||
console.error(err.toString()); | ||
res.writeHead(500); | ||
res.end('Internal server error'); | ||
return; | ||
} | ||
// File not found, forward to the SSR handler | ||
handler(req, res); | ||
}); | ||
|
||
stream.on('file', () => { | ||
forwardError = true; | ||
}); | ||
stream.pipe(res); | ||
} else { | ||
handler(req, res); | ||
} | ||
}; | ||
|
||
let httpServer: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse> | | ||
https.Server<typeof http.IncomingMessage, typeof http.ServerResponse>; | ||
|
||
if(process.env.SERVER_CERT_PATH && process.env.SERVER_KEY_PATH) { | ||
httpServer = https.createServer({ | ||
key: fs.readFileSync(process.env.SERVER_KEY_PATH), | ||
cert: fs.readFileSync(process.env.SERVER_CERT_PATH), | ||
}, listener); | ||
} else { | ||
httpServer = http.createServer(listener); | ||
} | ||
httpServer.listen(port, host); | ||
|
||
// Resolves once the server is closed | ||
const closed = new Promise<void>((resolve, reject) => { | ||
httpServer.addListener('close', resolve); | ||
httpServer.addListener('error', reject); | ||
}); | ||
|
||
return { | ||
host, | ||
port, | ||
closed() { | ||
return closed; | ||
}, | ||
server: httpServer, | ||
stop: async () => { | ||
await new Promise((resolve, reject) => { | ||
httpServer.close((err) => (err ? reject(err) : resolve(undefined))); | ||
}); | ||
}, | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,24 +1,48 @@ | ||
import type { AstroAdapter, AstroIntegration } from 'astro'; | ||
import type { Options, UserOptions } from './types'; | ||
|
||
export function getAdapter(): AstroAdapter { | ||
export function getAdapter(options: Options): AstroAdapter { | ||
return { | ||
name: '@astrojs/node', | ||
serverEntrypoint: '@astrojs/node/server.js', | ||
previewEntrypoint: '@astrojs/node/preview.js', | ||
exports: ['handler'], | ||
args: options | ||
}; | ||
} | ||
|
||
export default function createIntegration(): AstroIntegration { | ||
export default function createIntegration(userOptions: UserOptions): AstroIntegration { | ||
if(!userOptions?.mode) { | ||
throw new Error(`[@astrojs/node] Setting the 'mode' option is required.`) | ||
} | ||
|
||
let needsBuildConfig = false; | ||
let _options: Options; | ||
return { | ||
name: '@astrojs/node', | ||
hooks: { | ||
'astro:config:done': ({ setAdapter, config }) => { | ||
setAdapter(getAdapter()); | ||
needsBuildConfig = !config.build?.server; | ||
_options = { | ||
...userOptions, | ||
client: config.build.client?.toString(), | ||
server: config.build.server?.toString(), | ||
host: config.server.host, | ||
port: config.server.port, | ||
}; | ||
setAdapter(getAdapter(_options)); | ||
|
||
if (config.output === 'static') { | ||
console.warn(`[@astrojs/node] \`output: "server"\` is required to use this adapter.`); | ||
} | ||
}, | ||
'astro:build:start': ({ buildConfig }) => { | ||
// Backwards compat | ||
if(needsBuildConfig) { | ||
_options.client = buildConfig.client.toString(); | ||
_options.server = buildConfig.server.toString(); | ||
} | ||
} | ||
}, | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import type { NodeApp } from 'astro/app/node'; | ||
import type { IncomingMessage, ServerResponse } from 'http'; | ||
import type { Readable } from 'stream'; | ||
|
||
export default function(app: NodeApp) { | ||
return async function(req: IncomingMessage, res: ServerResponse, next?: (err?: unknown) => void) { | ||
try { | ||
const route = app.match(req); | ||
|
||
if (route) { | ||
try { | ||
const response = await app.render(req); | ||
await writeWebResponse(app, res, response); | ||
} catch (err: unknown) { | ||
if (next) { | ||
next(err); | ||
} else { | ||
throw err; | ||
} | ||
} | ||
} else if (next) { | ||
return next(); | ||
} else { | ||
res.writeHead(404); | ||
res.end('Not found'); | ||
} | ||
} catch (err: unknown) { | ||
if (!res.headersSent) { | ||
res.writeHead(500, `Server error`); | ||
res.end(); | ||
} | ||
} | ||
}; | ||
} | ||
|
||
async function writeWebResponse(app: NodeApp, res: ServerResponse, webResponse: Response) { | ||
const { status, headers, body } = webResponse; | ||
|
||
if (app.setCookieHeaders) { | ||
const setCookieHeaders: Array<string> = Array.from(app.setCookieHeaders(webResponse)); | ||
if (setCookieHeaders.length) { | ||
res.setHeader('Set-Cookie', setCookieHeaders); | ||
} | ||
} | ||
|
||
res.writeHead(status, Object.fromEntries(headers.entries())); | ||
if (body) { | ||
for await (const chunk of body as unknown as Readable) { | ||
res.write(chunk); | ||
} | ||
} | ||
res.end(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
import type { CreatePreviewServer } from 'astro'; | ||
import type { createExports } from './server'; | ||
import http from 'http'; | ||
import { fileURLToPath } from 'url'; | ||
import { createServer } from './http-server.js'; | ||
|
||
const preview: CreatePreviewServer = async function({ | ||
client, | ||
serverEntrypoint, | ||
host, | ||
port, | ||
}) { | ||
type ServerModule = ReturnType<typeof createExports>; | ||
type MaybeServerModule = Partial<ServerModule>; | ||
let ssrHandler: ServerModule['handler']; | ||
try { | ||
process.env.ASTRO_NODE_AUTOSTART = 'disabled'; | ||
const ssrModule: MaybeServerModule = await import(serverEntrypoint.toString()); | ||
if(typeof ssrModule.handler === 'function') { | ||
ssrHandler = ssrModule.handler; | ||
} else { | ||
throw new Error(`The server entrypoint doesn't have a handler. Are you sure this is the right file?`); | ||
} | ||
} catch(_err) { | ||
throw new Error(`The server entrypoint ${fileURLToPath} does not exist. Have you ran a build yet?`); | ||
} | ||
|
||
const handler: http.RequestListener = (req, res) => { | ||
ssrHandler(req, res, (ssrErr: any) => { | ||
if (ssrErr) { | ||
res.writeHead(500); | ||
res.end(ssrErr.toString()); | ||
} else { | ||
res.writeHead(404); | ||
res.end(); | ||
} | ||
}); | ||
}; | ||
|
||
const server = createServer({ | ||
client, | ||
port, | ||
host, | ||
}, handler); | ||
|
||
// eslint-disable-next-line no-console | ||
console.log(`Preview server listening on http://${host}:${port}`); | ||
|
||
return server; | ||
} | ||
|
||
export { | ||
preview as default | ||
}; |
Oops, something went wrong.