diff options
author | 2024-09-02 17:40:53 +0100 | |
---|---|---|
committer | 2024-09-02 17:40:53 +0100 | |
commit | a1d78b75aa86e496534a7d8e90deffbcac07ca48 (patch) | |
tree | 9052792e64dc977bb2e60b645c2131feaaa3bb02 /packages/integrations/node/src | |
parent | 3ab3b4efbcdd2aabea5f949deedf51a5acefae59 (diff) | |
parent | cd542109ba5b39598da6573f128c6783a6701215 (diff) | |
download | astro-a1d78b75aa86e496534a7d8e90deffbcac07ca48.tar.gz astro-a1d78b75aa86e496534a7d8e90deffbcac07ca48.tar.zst astro-a1d78b75aa86e496534a7d8e90deffbcac07ca48.zip |
Merge branch 'main' into next
Diffstat (limited to 'packages/integrations/node/src')
-rw-r--r-- | packages/integrations/node/src/index.ts | 81 | ||||
-rw-r--r-- | packages/integrations/node/src/log-listening-on.ts | 88 | ||||
-rw-r--r-- | packages/integrations/node/src/middleware.ts | 41 | ||||
-rw-r--r-- | packages/integrations/node/src/preview.ts | 61 | ||||
-rw-r--r-- | packages/integrations/node/src/serve-app.ts | 52 | ||||
-rw-r--r-- | packages/integrations/node/src/serve-static.ts | 125 | ||||
-rw-r--r-- | packages/integrations/node/src/server.ts | 31 | ||||
-rw-r--r-- | packages/integrations/node/src/standalone.ts | 92 | ||||
-rw-r--r-- | packages/integrations/node/src/types.ts | 39 |
9 files changed, 0 insertions, 610 deletions
diff --git a/packages/integrations/node/src/index.ts b/packages/integrations/node/src/index.ts deleted file mode 100644 index 42a2ed91f..000000000 --- a/packages/integrations/node/src/index.ts +++ /dev/null @@ -1,81 +0,0 @@ -import type { AstroAdapter, AstroIntegration } from 'astro'; -import { AstroError } from 'astro/errors'; -import type { Options, UserOptions } from './types.js'; - -export function getAdapter(options: Options): AstroAdapter { - return { - name: '@astrojs/node', - serverEntrypoint: '@astrojs/node/server.js', - previewEntrypoint: '@astrojs/node/preview.js', - exports: ['handler', 'startServer', 'options'], - args: options, - supportedAstroFeatures: { - hybridOutput: 'stable', - staticOutput: 'stable', - serverOutput: 'stable', - assets: { - supportKind: 'stable', - isSharpCompatible: true, - }, - i18nDomains: 'experimental', - envGetSecret: 'stable', - }, - }; -} - -// TODO: remove once we don't use a TLA anymore -async function shouldExternalizeAstroEnvSetup() { - try { - await import('astro/env/setup'); - return false; - } catch { - return true; - } -} - -export default function createIntegration(userOptions: UserOptions): AstroIntegration { - if (!userOptions?.mode) { - throw new AstroError(`Setting the 'mode' option is required.`); - } - - let _options: Options; - return { - name: '@astrojs/node', - hooks: { - 'astro:config:setup': async ({ updateConfig, config }) => { - updateConfig({ - image: { - endpoint: config.image.endpoint ?? 'astro/assets/endpoint/node', - }, - vite: { - ssr: { - noExternal: ['@astrojs/node'], - ...((await shouldExternalizeAstroEnvSetup()) - ? { - external: ['astro/env/setup'], - } - : {}), - }, - }, - }); - }, - 'astro:config:done': ({ setAdapter, config, logger }) => { - _options = { - ...userOptions, - client: config.build.client?.toString(), - server: config.build.server?.toString(), - host: config.server.host, - port: config.server.port, - assets: config.build.assets, - }; - setAdapter(getAdapter(_options)); - - if (config.output === 'static') { - logger.warn( - `\`output: "server"\` or \`output: "hybrid"\` is required to use this adapter.`, - ); - } - }, - }, - }; -} diff --git a/packages/integrations/node/src/log-listening-on.ts b/packages/integrations/node/src/log-listening-on.ts deleted file mode 100644 index 7e299740c..000000000 --- a/packages/integrations/node/src/log-listening-on.ts +++ /dev/null @@ -1,88 +0,0 @@ -import type http from 'node:http'; -import https from 'node:https'; -import type { AddressInfo } from 'node:net'; -import os from 'node:os'; -import type { AstroIntegrationLogger } from 'astro'; -import type { Options } from './types.js'; - -export async function logListeningOn( - logger: AstroIntegrationLogger, - server: http.Server | https.Server, - options: Pick<Options, 'host'>, -) { - await new Promise<void>((resolve) => server.once('listening', resolve)); - const protocol = server instanceof https.Server ? 'https' : 'http'; - // Allow to provide host value at runtime - const host = getResolvedHostForHttpServer( - process.env.HOST !== undefined && process.env.HOST !== '' ? process.env.HOST : options.host, - ); - const { port } = server.address() as AddressInfo; - const address = getNetworkAddress(protocol, host, port); - - if (host === undefined) { - logger.info( - `Server listening on \n local: ${address.local[0]} \t\n network: ${address.network[0]}\n`, - ); - } else { - logger.info(`Server listening on ${address.local[0]}`); - } -} - -function getResolvedHostForHttpServer(host: string | boolean) { - if (host === false) { - // Use a secure default - return 'localhost'; - } else if (host === true) { - // If passed --host in the CLI without arguments - return undefined; // undefined typically means 0.0.0.0 or :: (listen on all IPs) - } else { - return host; - } -} - -interface NetworkAddressOpt { - local: string[]; - network: string[]; -} - -const wildcardHosts = new Set(['0.0.0.0', '::', '0000:0000:0000:0000:0000:0000:0000:0000']); - -// this code from vite https://github.com/vitejs/vite/blob/d09bbd093a4b893e78f0bbff5b17c7cf7821f403/packages/vite/src/node/utils.ts#L892-L914 -export function getNetworkAddress( - protocol: 'http' | 'https' = 'http', - hostname: string | undefined, - port: number, - base?: string, -) { - const NetworkAddress: NetworkAddressOpt = { - local: [], - network: [], - }; - Object.values(os.networkInterfaces()) - .flatMap((nInterface) => nInterface ?? []) - .filter( - (detail) => - detail && - detail.address && - (detail.family === 'IPv4' || - // @ts-expect-error Node 18.0 - 18.3 returns number - detail.family === 4), - ) - .forEach((detail) => { - let host = detail.address.replace( - '127.0.0.1', - hostname === undefined || wildcardHosts.has(hostname) ? 'localhost' : hostname, - ); - // ipv6 host - if (host.includes(':')) { - host = `[${host}]`; - } - const url = `${protocol}://${host}:${port}${base ? base : ''}`; - if (detail.address.includes('127.0.0.1')) { - NetworkAddress.local.push(url); - } else { - NetworkAddress.network.push(url); - } - }); - return NetworkAddress; -} diff --git a/packages/integrations/node/src/middleware.ts b/packages/integrations/node/src/middleware.ts deleted file mode 100644 index 5cc4c4a46..000000000 --- a/packages/integrations/node/src/middleware.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { NodeApp } from 'astro/app/node'; -import { createAppHandler } from './serve-app.js'; -import type { RequestHandler } from './types.js'; - -/** - * Creates a middleware that can be used with Express, Connect, etc. - * - * Similar to `createAppHandler` but can additionally be placed in the express - * chain as an error middleware. - * - * https://expressjs.com/en/guide/using-middleware.html#middleware.error-handling - */ -export default function createMiddleware(app: NodeApp): RequestHandler { - const handler = createAppHandler(app); - const logger = app.getAdapterLogger(); - // using spread args because express trips up if the function's - // stringified body includes req, res, next, locals directly - return async function (...args) { - // assume normal invocation at first - const [req, res, next, locals] = args; - // short circuit if it is an error invocation - if (req instanceof Error) { - const error = req; - if (next) { - return next(error); - } else { - throw error; - } - } - try { - await handler(req, res, next, locals); - } catch (err) { - logger.error(`Could not render ${req.url}`); - console.error(err); - if (!res.headersSent) { - res.writeHead(500, `Server error`); - res.end(); - } - } - }; -} diff --git a/packages/integrations/node/src/preview.ts b/packages/integrations/node/src/preview.ts deleted file mode 100644 index 518155c4a..000000000 --- a/packages/integrations/node/src/preview.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { fileURLToPath } from 'node:url'; -import type { CreatePreviewServer } from 'astro'; -import { AstroError } from 'astro/errors'; -import { logListeningOn } from './log-listening-on.js'; -import type { createExports } from './server.js'; -import { createServer } from './standalone.js'; - -type ServerModule = ReturnType<typeof createExports>; -type MaybeServerModule = Partial<ServerModule>; - -const createPreviewServer: CreatePreviewServer = async function (preview) { - let ssrHandler: ServerModule['handler']; - let options: ServerModule['options']; - try { - process.env.ASTRO_NODE_AUTOSTART = 'disabled'; - const ssrModule: MaybeServerModule = await import(preview.serverEntrypoint.toString()); - if (typeof ssrModule.handler === 'function') { - ssrHandler = ssrModule.handler; - options = ssrModule.options!; - } else { - throw new AstroError( - `The server entrypoint doesn't have a handler. Are you sure this is the right file?`, - ); - } - } catch (err) { - if ((err as any).code === 'ERR_MODULE_NOT_FOUND') { - throw new AstroError( - `The server entrypoint ${fileURLToPath( - preview.serverEntrypoint, - )} does not exist. Have you ran a build yet?`, - ); - } else { - throw err; - } - } - const host = preview.host ?? 'localhost'; - const port = preview.port ?? 4321; - const server = createServer(ssrHandler, host, port); - - // If user specified custom headers append a listener - // to the server to add those headers to response - if (preview.headers) { - server.server.addListener('request', (_, res) => { - if (res.statusCode === 200) { - for (const [name, value] of Object.entries(preview.headers ?? {})) { - if (value) res.setHeader(name, value); - } - } - }); - } - - logListeningOn(preview.logger, server.server, options); - await new Promise<void>((resolve, reject) => { - server.server.once('listening', resolve); - server.server.once('error', reject); - server.server.listen(port, host); - }); - return server; -}; - -export { createPreviewServer as default }; diff --git a/packages/integrations/node/src/serve-app.ts b/packages/integrations/node/src/serve-app.ts deleted file mode 100644 index 72b4e0fd6..000000000 --- a/packages/integrations/node/src/serve-app.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { AsyncLocalStorage } from 'node:async_hooks'; -import { NodeApp } from 'astro/app/node'; -import type { RequestHandler } from './types.js'; - -/** - * Creates a Node.js http listener for on-demand rendered pages, compatible with http.createServer and Connect middleware. - * If the next callback is provided, it will be called if the request does not have a matching route. - * Intended to be used in both standalone and middleware mode. - */ -export function createAppHandler(app: NodeApp): RequestHandler { - /** - * Keep track of the current request path using AsyncLocalStorage. - * Used to log unhandled rejections with a helpful message. - */ - const als = new AsyncLocalStorage<string>(); - const logger = app.getAdapterLogger(); - process.on('unhandledRejection', (reason) => { - const requestUrl = als.getStore(); - logger.error(`Unhandled rejection while rendering ${requestUrl}`); - console.error(reason); - }); - - return async (req, res, next, locals) => { - let request: Request; - try { - request = NodeApp.createRequest(req); - } catch (err) { - logger.error(`Could not render ${req.url}`); - console.error(err); - res.statusCode = 500; - res.end('Internal Server Error'); - return; - } - - const routeData = app.match(request); - if (routeData) { - const response = await als.run(request.url, () => - app.render(request, { - addCookieHeader: true, - locals, - routeData, - }), - ); - await NodeApp.writeResponse(response, res); - } else if (next) { - return next(); - } else { - const response = await app.render(req); - await NodeApp.writeResponse(response, res); - } - }; -} diff --git a/packages/integrations/node/src/serve-static.ts b/packages/integrations/node/src/serve-static.ts deleted file mode 100644 index 725f7afa6..000000000 --- a/packages/integrations/node/src/serve-static.ts +++ /dev/null @@ -1,125 +0,0 @@ -import fs from 'node:fs'; -import type { IncomingMessage, ServerResponse } from 'node:http'; -import path from 'node:path'; -import url from 'node:url'; -import type { NodeApp } from 'astro/app/node'; -import send from 'send'; -import type { Options } from './types.js'; - -// check for a dot followed by a extension made up of lowercase characters -const isSubresourceRegex = /.+\.[a-z]+$/i; - -/** - * Creates a Node.js http listener for static files and prerendered pages. - * In standalone mode, the static handler is queried first for the static files. - * If one matching the request path is not found, it relegates to the SSR handler. - * Intended to be used only in the standalone mode. - */ -export function createStaticHandler(app: NodeApp, options: Options) { - const client = resolveClientDir(options); - /** - * @param ssr The SSR handler to be called if the static handler does not find a matching file. - */ - return (req: IncomingMessage, res: ServerResponse, ssr: () => unknown) => { - if (req.url) { - const [urlPath, urlQuery] = req.url.split('?'); - const filePath = path.join(client, app.removeBase(urlPath)); - - let pathname: string; - let isDirectory = false; - try { - isDirectory = fs.lstatSync(filePath).isDirectory(); - } catch {} - - const { trailingSlash = 'ignore' } = options; - - const hasSlash = urlPath.endsWith('/'); - switch (trailingSlash) { - case 'never': - if (isDirectory && urlPath != '/' && hasSlash) { - pathname = urlPath.slice(0, -1) + (urlQuery ? '?' + urlQuery : ''); - res.statusCode = 301; - res.setHeader('Location', pathname); - return res.end(); - } else pathname = urlPath; - // intentionally fall through - case 'ignore': - { - if (isDirectory && !hasSlash) { - pathname = urlPath + '/index.html'; - } else pathname = urlPath; - } - break; - case 'always': - // trailing slash is not added to "subresources" - if (!hasSlash && !isSubresourceRegex.test(urlPath)) { - pathname = urlPath + '/' + (urlQuery ? '?' + urlQuery : ''); - res.statusCode = 301; - res.setHeader('Location', pathname); - return res.end(); - } else pathname = urlPath; - break; - } - // app.removeBase sometimes returns a path without a leading slash - pathname = prependForwardSlash(app.removeBase(pathname)); - - const stream = send(req, pathname, { - root: client, - dotfiles: pathname.startsWith('/.well-known/') ? 'allow' : 'deny', - }); - - let forwardError = false; - - stream.on('error', (err) => { - if (forwardError) { - console.error(err.toString()); - res.writeHead(500); - res.end('Internal server error'); - return; - } - // File not found, forward to the SSR handler - ssr(); - }); - stream.on('headers', (_res: ServerResponse) => { - // assets in dist/_astro are hashed and should get the immutable header - if (pathname.startsWith(`/${options.assets}/`)) { - // This is the "far future" cache header, used for static files whose name includes their digest hash. - // 1 year (31,536,000 seconds) is convention. - // Taken from https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#immutable - _res.setHeader('Cache-Control', 'public, max-age=31536000, immutable'); - } - }); - stream.on('file', () => { - forwardError = true; - }); - stream.pipe(res); - } else { - ssr(); - } - }; -} - -function resolveClientDir(options: Options) { - const clientURLRaw = new URL(options.client); - const serverURLRaw = new URL(options.server); - const rel = path.relative(url.fileURLToPath(serverURLRaw), url.fileURLToPath(clientURLRaw)); - - // walk up the parent folders until you find the one that is the root of the server entry folder. This is how we find the client folder relatively. - const serverFolder = path.basename(options.server); - let serverEntryFolderURL = path.dirname(import.meta.url); - while (!serverEntryFolderURL.endsWith(serverFolder)) { - serverEntryFolderURL = path.dirname(serverEntryFolderURL); - } - const serverEntryURL = serverEntryFolderURL + '/entry.mjs'; - const clientURL = new URL(appendForwardSlash(rel), serverEntryURL); - const client = url.fileURLToPath(clientURL); - return client; -} - -function prependForwardSlash(pth: string) { - return pth.startsWith('/') ? pth : '/' + pth; -} - -function appendForwardSlash(pth: string) { - return pth.endsWith('/') ? pth : pth + '/'; -} diff --git a/packages/integrations/node/src/server.ts b/packages/integrations/node/src/server.ts deleted file mode 100644 index 93d75d360..000000000 --- a/packages/integrations/node/src/server.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { SSRManifest } from 'astro'; -import { NodeApp, applyPolyfills } from 'astro/app/node'; -import { setGetEnv } from 'astro/env/setup'; -import createMiddleware from './middleware.js'; -import { createStandaloneHandler } from './standalone.js'; -import startServer from './standalone.js'; -import type { Options } from './types.js'; - -// This needs to run first because some internals depend on `crypto` -applyPolyfills(); -setGetEnv((key) => process.env[key]); - -export function createExports(manifest: SSRManifest, options: Options) { - const app = new NodeApp(manifest); - options.trailingSlash = manifest.trailingSlash; - return { - options: options, - handler: - options.mode === 'middleware' ? createMiddleware(app) : createStandaloneHandler(app, options), - startServer: () => startServer(app, options), - }; -} - -export function start(manifest: SSRManifest, options: Options) { - if (options.mode !== 'standalone' || process.env.ASTRO_NODE_AUTOSTART === 'disabled') { - return; - } - - const app = new NodeApp(manifest); - startServer(app, options); -} diff --git a/packages/integrations/node/src/standalone.ts b/packages/integrations/node/src/standalone.ts deleted file mode 100644 index 76e672d2f..000000000 --- a/packages/integrations/node/src/standalone.ts +++ /dev/null @@ -1,92 +0,0 @@ -import fs from 'node:fs'; -import http from 'node:http'; -import https from 'node:https'; -import type { PreviewServer } from 'astro'; -import type { NodeApp } from 'astro/app/node'; -import enableDestroy from 'server-destroy'; -import { logListeningOn } from './log-listening-on.js'; -import { createAppHandler } from './serve-app.js'; -import { createStaticHandler } from './serve-static.js'; -import type { Options } from './types.js'; - -// Used to get Host Value at Runtime -export const hostOptions = (host: Options['host']): string => { - if (typeof host === 'boolean') { - return host ? '0.0.0.0' : 'localhost'; - } - return host; -}; - -export default function standalone(app: NodeApp, options: Options) { - const port = process.env.PORT ? Number(process.env.PORT) : options.port ?? 8080; - const host = process.env.HOST ?? hostOptions(options.host); - const handler = createStandaloneHandler(app, options); - const server = createServer(handler, host, port); - server.server.listen(port, host); - if (process.env.ASTRO_NODE_LOGGING !== 'disabled') { - logListeningOn(app.getAdapterLogger(), server.server, options); - } - return { - server, - done: server.closed(), - }; -} - -// also used by server entrypoint -export function createStandaloneHandler(app: NodeApp, options: Options) { - const appHandler = createAppHandler(app); - const staticHandler = createStaticHandler(app, options); - return (req: http.IncomingMessage, res: http.ServerResponse) => { - try { - // validate request path - decodeURI(req.url!); - } catch { - res.writeHead(400); - res.end('Bad request.'); - return; - } - staticHandler(req, res, () => appHandler(req, res)); - }; -} - -// also used by preview entrypoint -export function createServer(listener: http.RequestListener, host: string, port: number) { - let httpServer: http.Server | https.Server; - - 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); - } - enableDestroy(httpServer); - - // Resolves once the server is closed - const closed = new Promise<void>((resolve, reject) => { - httpServer.addListener('close', resolve); - httpServer.addListener('error', reject); - }); - - const previewable = { - host, - port, - closed() { - return closed; - }, - async stop() { - await new Promise((resolve, reject) => { - httpServer.destroy((err) => (err ? reject(err) : resolve(undefined))); - }); - }, - } satisfies PreviewServer; - - return { - server: httpServer, - ...previewable, - }; -} diff --git a/packages/integrations/node/src/types.ts b/packages/integrations/node/src/types.ts deleted file mode 100644 index 010053de5..000000000 --- a/packages/integrations/node/src/types.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { IncomingMessage, ServerResponse } from 'node:http'; -import type { SSRManifest } from 'astro'; -import type { NodeApp } from 'astro/app/node'; - -export interface UserOptions { - /** - * Specifies the mode that the adapter builds to. - * - * - 'middleware' - Build to middleware, to be used within another Node.js server, such as Express. - * - 'standalone' - Build to a standalone server. The server starts up just by running the built script. - */ - mode: 'middleware' | 'standalone'; -} - -export interface Options extends UserOptions { - host: string | boolean; - port: number; - server: string; - client: string; - assets: string; - trailingSlash?: SSRManifest['trailingSlash']; -} - -export interface CreateServerOptions { - app: NodeApp; - assets: string; - client: URL; - port: number; - host: string | undefined; - removeBase: (pathname: string) => string; -} - -export type RequestHandler = (...args: RequestHandlerParams) => void | Promise<void>; -export type RequestHandlerParams = [ - req: IncomingMessage, - res: ServerResponse, - next?: (err?: unknown) => void, - locals?: object, -]; |