summaryrefslogtreecommitdiff
path: root/packages/integrations/node/src
diff options
context:
space:
mode:
authorGravatar Matt Kane <m@mk.gg> 2024-09-02 17:40:53 +0100
committerGravatar Matt Kane <m@mk.gg> 2024-09-02 17:40:53 +0100
commita1d78b75aa86e496534a7d8e90deffbcac07ca48 (patch)
tree9052792e64dc977bb2e60b645c2131feaaa3bb02 /packages/integrations/node/src
parent3ab3b4efbcdd2aabea5f949deedf51a5acefae59 (diff)
parentcd542109ba5b39598da6573f128c6783a6701215 (diff)
downloadastro-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.ts81
-rw-r--r--packages/integrations/node/src/log-listening-on.ts88
-rw-r--r--packages/integrations/node/src/middleware.ts41
-rw-r--r--packages/integrations/node/src/preview.ts61
-rw-r--r--packages/integrations/node/src/serve-app.ts52
-rw-r--r--packages/integrations/node/src/serve-static.ts125
-rw-r--r--packages/integrations/node/src/server.ts31
-rw-r--r--packages/integrations/node/src/standalone.ts92
-rw-r--r--packages/integrations/node/src/types.ts39
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,
-];