summaryrefslogtreecommitdiff
path: root/packages/astro/src/vite-plugin-astro-server/index.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/astro/src/vite-plugin-astro-server/index.ts')
-rw-r--r--packages/astro/src/vite-plugin-astro-server/index.ts449
1 files changed, 10 insertions, 439 deletions
diff --git a/packages/astro/src/vite-plugin-astro-server/index.ts b/packages/astro/src/vite-plugin-astro-server/index.ts
index 0e09c9b96..a6baa6c2c 100644
--- a/packages/astro/src/vite-plugin-astro-server/index.ts
+++ b/packages/astro/src/vite-plugin-astro-server/index.ts
@@ -1,439 +1,10 @@
-import type http from 'http';
-import mime from 'mime';
-import type * as vite from 'vite';
-import type { AstroSettings, ManifestData } from '../@types/astro';
-import { DevelopmentEnvironment, SSROptions } from '../core/render/dev/index';
-
-import { Readable } from 'stream';
-import { attachToResponse, getSetCookiesFromResponse } from '../core/cookies/index.js';
-import { call as callEndpoint } from '../core/endpoint/dev/index.js';
-import { throwIfRedirectNotAllowed } from '../core/endpoint/index.js';
-import { collectErrorMetadata, getViteErrorPayload } from '../core/errors/dev/index.js';
-import type { ErrorWithMetadata } from '../core/errors/index.js';
-import { createSafeError } from '../core/errors/index.js';
-import { error, info, LogOptions, warn } from '../core/logger/core.js';
-import * as msg from '../core/messages.js';
-import { appendForwardSlash } from '../core/path.js';
-import { createDevelopmentEnvironment, preload, renderPage } from '../core/render/dev/index.js';
-import { getParamsAndProps, GetParamsAndPropsError } from '../core/render/index.js';
-import { createRequest } from '../core/request.js';
-import { createRouteManifest, matchAllRoutes } from '../core/routing/index.js';
-import { resolvePages } from '../core/util.js';
-import notFoundTemplate, { subpathNotUsedTemplate } from '../template/4xx.js';
-
-interface AstroPluginOptions {
- settings: AstroSettings;
- logging: LogOptions;
-}
-
-type AsyncReturnType<T extends (...args: any) => Promise<any>> = T extends (
- ...args: any
-) => Promise<infer R>
- ? R
- : any;
-
-function writeHtmlResponse(res: http.ServerResponse, statusCode: number, html: string) {
- res.writeHead(statusCode, {
- 'Content-Type': 'text/html; charset=utf-8',
- 'Content-Length': Buffer.byteLength(html, 'utf-8'),
- });
- res.write(html);
- res.end();
-}
-
-async function writeWebResponse(res: http.ServerResponse, webResponse: Response) {
- const { status, headers, body } = webResponse;
-
- let _headers = {};
- if ('raw' in headers) {
- // Node fetch allows you to get the raw headers, which includes multiples of the same type.
- // This is needed because Set-Cookie *must* be called for each cookie, and can't be
- // concatenated together.
- type HeadersWithRaw = Headers & {
- raw: () => Record<string, string[]>;
- };
-
- for (const [key, value] of Object.entries((headers as HeadersWithRaw).raw())) {
- res.setHeader(key, value);
- }
- } else {
- _headers = Object.fromEntries(headers.entries());
- }
-
- // Attach any set-cookie headers added via Astro.cookies.set()
- const setCookieHeaders = Array.from(getSetCookiesFromResponse(webResponse));
- if (setCookieHeaders.length) {
- res.setHeader('Set-Cookie', setCookieHeaders);
- }
- res.writeHead(status, _headers);
- if (body) {
- if (Symbol.for('astro.responseBody') in webResponse) {
- let stream = (webResponse as any)[Symbol.for('astro.responseBody')];
- for await (const chunk of stream) {
- res.write(chunk.toString());
- }
- } else if (body instanceof Readable) {
- body.pipe(res);
- return;
- } else if (typeof body === 'string') {
- res.write(body);
- } else {
- const reader = body.getReader();
- while (true) {
- const { done, value } = await reader.read();
- if (done) break;
- if (value) {
- res.write(value);
- }
- }
- }
- }
- res.end();
-}
-
-async function writeSSRResult(webResponse: Response, res: http.ServerResponse) {
- return writeWebResponse(res, webResponse);
-}
-
-async function handle404Response(
- origin: string,
- req: http.IncomingMessage,
- res: http.ServerResponse
-) {
- const pathname = decodeURI(new URL(origin + req.url).pathname);
-
- const html = notFoundTemplate({
- statusCode: 404,
- title: 'Not found',
- tabTitle: '404: Not Found',
- pathname,
- });
- writeHtmlResponse(res, 404, html);
-}
-
-async function handle500Response(
- viteServer: vite.ViteDevServer,
- origin: string,
- req: http.IncomingMessage,
- res: http.ServerResponse,
- err: ErrorWithMetadata
-) {
- res.on('close', () => setTimeout(() => viteServer.ws.send(getViteErrorPayload(err)), 200));
- if (res.headersSent) {
- res.write(`<script type="module" src="/@vite/client"></script>`);
- res.end();
- } else {
- writeHtmlResponse(
- res,
- 500,
- `<title>${err.name}</title><script type="module" src="/@vite/client"></script>`
- );
- }
-}
-
-function getCustom404Route({ config }: AstroSettings, manifest: ManifestData) {
- // For Windows compat, use relative page paths to match the 404 route
- const relPages = resolvePages(config).href.replace(config.root.href, '');
- const pattern = new RegExp(`${appendForwardSlash(relPages)}404.(astro|md)`);
- return manifest.routes.find((r) => r.component.match(pattern));
-}
-
-function log404(logging: LogOptions, pathname: string) {
- info(logging, 'serve', msg.req({ url: pathname, statusCode: 404 }));
-}
-
-export function baseMiddleware(
- settings: AstroSettings,
- logging: LogOptions
-): vite.Connect.NextHandleFunction {
- const { config } = settings;
- const site = config.site ? new URL(config.base, config.site) : undefined;
- const devRoot = site ? site.pathname : '/';
-
- return function devBaseMiddleware(req, res, next) {
- const url = req.url!;
-
- const pathname = decodeURI(new URL(url, 'http://vitejs.dev').pathname);
-
- if (pathname.startsWith(devRoot)) {
- req.url = url.replace(devRoot, '/');
- return next();
- }
-
- if (pathname === '/' || pathname === '/index.html') {
- log404(logging, pathname);
- const html = subpathNotUsedTemplate(devRoot, pathname);
- return writeHtmlResponse(res, 404, html);
- }
-
- if (req.headers.accept?.includes('text/html')) {
- log404(logging, pathname);
- const html = notFoundTemplate({
- statusCode: 404,
- title: 'Not found',
- tabTitle: '404: Not Found',
- pathname,
- });
- return writeHtmlResponse(res, 404, html);
- }
-
- next();
- };
-}
-
-async function matchRoute(pathname: string, env: DevelopmentEnvironment, manifest: ManifestData) {
- const { logging, settings, routeCache } = env;
- const matches = matchAllRoutes(pathname, manifest);
-
- for await (const maybeRoute of matches) {
- const filePath = new URL(`./${maybeRoute.component}`, settings.config.root);
- const preloadedComponent = await preload({ env, filePath });
- const [, mod] = preloadedComponent;
- // attempt to get static paths
- // if this fails, we have a bad URL match!
- const paramsAndPropsRes = await getParamsAndProps({
- mod,
- route: maybeRoute,
- routeCache,
- pathname: pathname,
- logging,
- ssr: settings.config.output === 'server',
- });
-
- if (paramsAndPropsRes !== GetParamsAndPropsError.NoMatchingStaticPath) {
- return {
- route: maybeRoute,
- filePath,
- preloadedComponent,
- mod,
- };
- }
- }
-
- if (matches.length) {
- warn(
- logging,
- 'getStaticPaths',
- `Route pattern matched, but no matching static path found. (${pathname})`
- );
- }
-
- log404(logging, pathname);
- const custom404 = getCustom404Route(settings, manifest);
-
- if (custom404) {
- const filePath = new URL(`./${custom404.component}`, settings.config.root);
- const preloadedComponent = await preload({ env, filePath });
- const [, mod] = preloadedComponent;
-
- return {
- route: custom404,
- filePath,
- preloadedComponent,
- mod,
- };
- }
-
- return undefined;
-}
-
-/** The main logic to route dev server requests to pages in Astro. */
-async function handleRequest(
- env: DevelopmentEnvironment,
- manifest: ManifestData,
- req: http.IncomingMessage,
- res: http.ServerResponse
-) {
- const { settings, viteServer } = env;
- const { config } = settings;
- const origin = `${viteServer.config.server.https ? 'https' : 'http'}://${req.headers.host}`;
- const buildingToSSR = config.output === 'server';
- // Ignore `.html` extensions and `index.html` in request URLS to ensure that
- // routing behavior matches production builds. This supports both file and directory
- // build formats, and is necessary based on how the manifest tracks build targets.
- const url = new URL(origin + req.url?.replace(/(index)?\.html$/, ''));
- const pathname = decodeURI(url.pathname);
-
- // Add config.base back to url before passing it to SSR
- url.pathname = config.base.substring(0, config.base.length - 1) + url.pathname;
-
- // HACK! @astrojs/image uses query params for the injected route in `dev`
- if (!buildingToSSR && pathname !== '/_image') {
- // Prevent user from depending on search params when not doing SSR.
- // NOTE: Create an array copy here because deleting-while-iterating
- // creates bugs where not all search params are removed.
- const allSearchParams = Array.from(url.searchParams);
- for (const [key] of allSearchParams) {
- url.searchParams.delete(key);
- }
- }
-
- let body: ArrayBuffer | undefined = undefined;
- if (!(req.method === 'GET' || req.method === 'HEAD')) {
- let bytes: Uint8Array[] = [];
- await new Promise((resolve) => {
- req.on('data', (part) => {
- bytes.push(part);
- });
- req.on('end', resolve);
- });
- body = Buffer.concat(bytes);
- }
-
- try {
- const matchedRoute = await matchRoute(pathname, env, manifest);
- return await handleRoute(matchedRoute, url, pathname, body, origin, env, manifest, req, res);
- } catch (_err) {
- // This is our last line of defense regarding errors where we still might have some information about the request
- // Our error should already be complete, but let's try to add a bit more through some guesswork
- const err = createSafeError(_err);
- const errorWithMetadata = collectErrorMetadata(err);
-
- error(env.logging, null, msg.formatErrorMessage(errorWithMetadata));
- handle500Response(viteServer, origin, req, res, errorWithMetadata);
- }
-}
-
-async function handleRoute(
- matchedRoute: AsyncReturnType<typeof matchRoute>,
- url: URL,
- pathname: string,
- body: ArrayBuffer | undefined,
- origin: string,
- env: DevelopmentEnvironment,
- manifest: ManifestData,
- req: http.IncomingMessage,
- res: http.ServerResponse
-): Promise<void> {
- const { logging, settings } = env;
- if (!matchedRoute) {
- return handle404Response(origin, req, res);
- }
-
- const { config } = settings;
- const filePath: URL | undefined = matchedRoute.filePath;
- const { route, preloadedComponent, mod } = matchedRoute;
- const buildingToSSR = config.output === 'server';
-
- // Headers are only available when using SSR.
- const request = createRequest({
- url,
- headers: buildingToSSR ? req.headers : new Headers(),
- method: req.method,
- body,
- logging,
- ssr: buildingToSSR,
- clientAddress: buildingToSSR ? req.socket.remoteAddress : undefined,
- });
-
- // attempt to get static paths
- // if this fails, we have a bad URL match!
- const paramsAndPropsRes = await getParamsAndProps({
- mod,
- route,
- routeCache: env.routeCache,
- pathname: pathname,
- logging,
- ssr: config.output === 'server',
- });
-
- const options: SSROptions = {
- env,
- filePath,
- origin,
- preload: preloadedComponent,
- pathname,
- request,
- route,
- };
-
- // Route successfully matched! Render it.
- if (route.type === 'endpoint') {
- const result = await callEndpoint(options);
- if (result.type === 'response') {
- if (result.response.headers.get('X-Astro-Response') === 'Not-Found') {
- const fourOhFourRoute = await matchRoute('/404', env, manifest);
- return handleRoute(
- fourOhFourRoute,
- new URL('/404', url),
- '/404',
- body,
- origin,
- env,
- manifest,
- req,
- res
- );
- }
- throwIfRedirectNotAllowed(result.response, config);
- await writeWebResponse(res, result.response);
- } else {
- let contentType = 'text/plain';
- // Dynamic routes don’t include `route.pathname`, so synthesize a path for these (e.g. 'src/pages/[slug].svg')
- const filepath =
- route.pathname ||
- route.segments.map((segment) => segment.map((p) => p.content).join('')).join('/');
- const computedMimeType = mime.getType(filepath);
- if (computedMimeType) {
- contentType = computedMimeType;
- }
- const response = new Response(result.body, {
- status: 200,
- headers: {
- 'Content-Type': `${contentType};charset=utf-8`,
- },
- });
- attachToResponse(response, result.cookies);
- await writeWebResponse(res, response);
- }
- } else {
- const result = await renderPage(options);
- throwIfRedirectNotAllowed(result, config);
- return await writeSSRResult(result, res);
- }
-}
-
-export default function createPlugin({ settings, logging }: AstroPluginOptions): vite.Plugin {
- return {
- name: 'astro:server',
- configureServer(viteServer) {
- let env = createDevelopmentEnvironment(settings, logging, viteServer);
- let manifest: ManifestData = createRouteManifest({ settings }, logging);
-
- /** rebuild the route cache + manifest, as needed. */
- function rebuildManifest(needsManifestRebuild: boolean, file: string) {
- env.routeCache.clearAll();
- if (needsManifestRebuild) {
- manifest = createRouteManifest({ settings }, logging);
- }
- }
- // Rebuild route manifest on file change, if needed.
- viteServer.watcher.on('add', rebuildManifest.bind(null, true));
- viteServer.watcher.on('unlink', rebuildManifest.bind(null, true));
- viteServer.watcher.on('change', rebuildManifest.bind(null, false));
- return () => {
- // Push this middleware to the front of the stack so that it can intercept responses.
- if (settings.config.base !== '/') {
- viteServer.middlewares.stack.unshift({
- route: '',
- handle: baseMiddleware(settings, logging),
- });
- }
- viteServer.middlewares.use(async (req, res) => {
- if (!req.url || !req.method) {
- throw new Error('Incomplete request');
- }
- handleRequest(env, manifest, req, res);
- });
- };
- },
- // HACK: hide `.tip` in Vite's ErrorOverlay and replace [vite] messages with [astro]
- transform(code, id, opts = {}) {
- if (opts.ssr) return;
- if (!id.includes('vite/dist/client/client.mjs')) return;
- return code
- .replace(/\.tip \{[^}]*\}/gm, '.tip {\n display: none;\n}')
- .replace(/\[vite\]/g, '[astro]');
- },
- };
-}
+export {
+ createController,
+ runWithErrorHandling
+} from './controller.js';
+export {
+ default as vitePluginAstroServer
+} from './plugin.js';
+export {
+ handleRequest
+} from './request.js';