summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.changeset/empty-snails-allow.md7
-rw-r--r--LICENSE3
-rw-r--r--packages/astro/package.json1
-rw-r--r--packages/astro/src/cli/index.ts2
-rw-r--r--packages/astro/src/core/create-vite.ts12
-rw-r--r--packages/astro/src/core/dev/index.ts397
-rw-r--r--packages/astro/src/core/messages.ts (renamed from packages/astro/src/core/dev/messages.ts)13
-rw-r--r--packages/astro/src/core/preview/index.ts8
-rw-r--r--packages/astro/src/core/util.ts5
-rw-r--r--packages/astro/src/template/4xx.ts (renamed from packages/astro/src/core/dev/template/4xx.ts)0
-rw-r--r--packages/astro/src/template/5xx.ts (renamed from packages/astro/src/core/dev/template/5xx.ts)0
-rw-r--r--packages/astro/src/template/css.ts (renamed from packages/astro/src/core/dev/template/css.ts)0
-rw-r--r--packages/astro/src/vite-plugin-astro-postprocess/index.ts6
-rw-r--r--packages/astro/src/vite-plugin-astro-server/index.ts156
-rw-r--r--packages/astro/src/vite-plugin-astro/index.ts6
-rw-r--r--packages/astro/src/vite-plugin-build-html/index.ts2
-rw-r--r--packages/astro/src/vite-plugin-config-alias/index.ts2
-rw-r--r--packages/astro/src/vite-plugin-jsx/index.ts4
-rw-r--r--packages/astro/src/vite-plugin-markdown/index.ts4
-rw-r--r--packages/astro/test/test-utils.js8
-rw-r--r--yarn.lock35
21 files changed, 222 insertions, 449 deletions
diff --git a/.changeset/empty-snails-allow.md b/.changeset/empty-snails-allow.md
new file mode 100644
index 000000000..261629367
--- /dev/null
+++ b/.changeset/empty-snails-allow.md
@@ -0,0 +1,7 @@
+---
+'astro': minor
+---
+
+Refactor dev server to use vite server internally.
+
+This should be an invisible change, and no breaking changes are expected from this change. However, it is a big enough refactor that some unexpected changes may occur. If you've experienced a regression in the dev server, it is most likely a bug!
diff --git a/LICENSE b/LICENSE
index 417b4ded9..53be9db53 100644
--- a/LICENSE
+++ b/LICENSE
@@ -37,8 +37,7 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
"""
-This license applies to parts of the `packages/create-astro` subdirectory originating from the
-https://github.com/sveltejs/kit repository:
+This license applies to parts of the `packages/create-astro` and `packages/astro` subdirectories originating from the https://github.com/sveltejs/kit repository:
Copyright (c) 2020 [these people](https://github.com/sveltejs/kit/graphs/contributors)
diff --git a/packages/astro/package.json b/packages/astro/package.json
index a8d5deb45..bd21e98d1 100644
--- a/packages/astro/package.json
+++ b/packages/astro/package.json
@@ -73,7 +73,6 @@
"astring": "^1.7.5",
"ci-info": "^3.2.0",
"common-ancestor-path": "^1.0.1",
- "connect": "^3.7.0",
"eol": "^0.9.1",
"es-module-lexer": "^0.9.3",
"esbuild": "0.13.7",
diff --git a/packages/astro/src/cli/index.ts b/packages/astro/src/cli/index.ts
index e764e96b7..ae8b0ac56 100644
--- a/packages/astro/src/cli/index.ts
+++ b/packages/astro/src/cli/index.ts
@@ -1,7 +1,7 @@
/* eslint-disable no-console */
import type { AstroConfig } from '../@types/astro';
-import type { LogOptions } from '../core/logger';
+import type { LogOptions } from '../core/logger.js';
import * as colors from 'kleur/colors';
import fs from 'fs';
diff --git a/packages/astro/src/core/create-vite.ts b/packages/astro/src/core/create-vite.ts
index c8b504ad6..61eb5edc7 100644
--- a/packages/astro/src/core/create-vite.ts
+++ b/packages/astro/src/core/create-vite.ts
@@ -1,11 +1,11 @@
import type { AstroConfig } from '../@types/astro';
-import type { AstroDevServer } from './dev';
import type { LogOptions } from './logger';
import { builtinModules } from 'module';
import { fileURLToPath } from 'url';
import vite from './vite.js';
import astroVitePlugin from '../vite-plugin-astro/index.js';
+import astroViteServerPlugin from '../vite-plugin-astro-server/index.js';
import astroPostprocessVitePlugin from '../vite-plugin-astro-postprocess/index.js';
import configAliasVitePlugin from '../vite-plugin-config-alias/index.js';
import markdownVitePlugin from '../vite-plugin-markdown/index.js';
@@ -34,12 +34,11 @@ export type ViteConfigWithSSR = vite.InlineConfig & { ssr?: { external?: string[
interface CreateViteOptions {
astroConfig: AstroConfig;
- devServer?: AstroDevServer;
logging: LogOptions;
}
/** Return a common starting point for all Vite actions */
-export async function createVite(inlineConfig: ViteConfigWithSSR, { astroConfig, logging, devServer }: CreateViteOptions): Promise<ViteConfigWithSSR> {
+export async function createVite(inlineConfig: ViteConfigWithSSR, { astroConfig, logging }: CreateViteOptions): Promise<ViteConfigWithSSR> {
// First, start with the Vite configuration that Astro core needs
let viteConfig: ViteConfigWithSSR = {
cacheDir: fileURLToPath(new URL('./node_modules/.vite/', astroConfig.projectRoot)), // using local caches allows Astro to be used in monorepos, etc.
@@ -50,10 +49,11 @@ export async function createVite(inlineConfig: ViteConfigWithSSR, { astroConfig,
},
plugins: [
configAliasVitePlugin({ config: astroConfig }),
- astroVitePlugin({ config: astroConfig, devServer, logging }),
- markdownVitePlugin({ config: astroConfig, devServer }),
+ astroVitePlugin({ config: astroConfig, logging }),
+ astroViteServerPlugin({ config: astroConfig, logging }),
+ markdownVitePlugin({ config: astroConfig }),
jsxVitePlugin({ config: astroConfig, logging }),
- astroPostprocessVitePlugin({ config: astroConfig, devServer }),
+ astroPostprocessVitePlugin({ config: astroConfig }),
],
publicDir: fileURLToPath(astroConfig.public),
root: fileURLToPath(astroConfig.projectRoot),
diff --git a/packages/astro/src/core/dev/index.ts b/packages/astro/src/core/dev/index.ts
index 6fb0e3e3e..ae1b57997 100644
--- a/packages/astro/src/core/dev/index.ts
+++ b/packages/astro/src/core/dev/index.ts
@@ -1,398 +1,41 @@
-import type { NextFunction } from 'connect';
-import type http from 'http';
-import type { AstroConfig, ManifestData, RouteCache, RouteData } from '../../@types/astro';
-import type { LogOptions } from '../logger';
-import type { HmrContext, ModuleNode } from '../vite';
-
-import path from 'path';
-import { fileURLToPath } from 'url';
-import { promisify } from 'util';
-import connect from 'connect';
-import mime from 'mime';
import { polyfill } from '@astropub/webapi';
+import type { AddressInfo } from 'net';
import { performance } from 'perf_hooks';
-import stripAnsi from 'strip-ansi';
-import vite from '../vite.js';
-import { defaultLogOptions, error, info } from '../logger.js';
-import { ssr } from '../ssr/index.js';
-import { STYLE_EXTENSIONS } from '../ssr/css.js';
-import { collectResources } from '../ssr/html.js';
-import { createRouteManifest, matchRoute } from '../ssr/routing.js';
+import type { AstroConfig } from '../../@types/astro';
import { createVite } from '../create-vite.js';
-import * as msg from './messages.js';
-import notFoundTemplate, { subpathNotUsedTemplate } from './template/4xx.js';
-import serverErrorTemplate from './template/5xx.js';
+import { defaultLogOptions, info, LogOptions } from '../logger.js';
+import vite from '../vite.js';
+import * as msg from '../messages.js';
export interface DevOptions {
logging: LogOptions;
}
export interface DevServer {
- hostname: string;
- port: number;
- server: connect.Server;
+ address: AddressInfo;
stop(): Promise<void>;
}
/** `astro dev` */
export default async function dev(config: AstroConfig, options: DevOptions = { logging: defaultLogOptions }): Promise<DevServer> {
- // polyfill WebAPIs to globalThis for Node v12, Node v14, and Node v16
+ const devStart = performance.now();
+ // polyfill WebAPIs for Node.js runtime
polyfill(globalThis, {
exclude: 'window document',
});
+ // start the server
+ const viteUserConfig = vite.mergeConfig({ mode: 'development' }, config.vite || {});
+ const viteConfig = await createVite(viteUserConfig, { astroConfig: config, logging: options.logging });
+ const viteServer = await vite.createServer(viteConfig);
+ await viteServer.listen(config.devOptions.port);
+ const address = viteServer.httpServer!.address() as AddressInfo;
+ // Log to console
+ const site = config.buildOptions.site ? new URL(config.buildOptions.site) : undefined;
+ info(options.logging, 'astro', msg.devStart({ startupTime: performance.now() - devStart }));
+ info(options.logging, 'astro', msg.devHost({ address, site, https: !!viteUserConfig.server?.https }));
- // start dev server
- const server = new AstroDevServer(config, options);
- await server.start();
-
- // attempt shutdown
- process.on('SIGTERM', () => server.stop());
return {
- hostname: server.hostname,
- port: server.port,
- server: server.app,
- stop: () => server.stop(),
+ address,
+ stop: () => viteServer.close(),
};
}
-
-/** Dev server */
-export class AstroDevServer {
- app: connect.Server = connect();
- config: AstroConfig;
- devRoot: string;
- hostname: string;
- httpServer: http.Server | undefined;
- logging: LogOptions;
- manifest: ManifestData;
- mostRecentRoute?: RouteData;
- origin: string;
- port: number;
- routeCache: RouteCache = {};
- site: URL | undefined;
- url: URL;
- viteServer: vite.ViteDevServer | undefined;
-
- constructor(config: AstroConfig, options: DevOptions) {
- this.config = config;
- this.hostname = config.devOptions.hostname || 'localhost';
- this.logging = options.logging;
- this.port = config.devOptions.port;
- this.origin = `http://${this.hostname}:${this.port}`;
- this.site = config.buildOptions.site ? new URL(config.buildOptions.site) : undefined;
- this.devRoot = this.site ? this.site.pathname : '/';
- this.url = new URL(this.devRoot, this.origin);
- this.manifest = createRouteManifest({ config }, this.logging);
- }
-
- async start() {
- const devStart = performance.now();
-
- // Setup the dev server and connect it to Vite (via middleware)
- this.viteServer = await this.createViteServer();
- this.app.use(this.viteServer.middlewares);
- this.app.use((req, res, next) => this.handleRequest(req, res, next));
- this.app.use((req, res, next) => this.renderError(req, res, next));
-
- // Listen on port (and retry if taken)
- await this.listen(devStart);
- }
-
- async stop() {
- if (this.viteServer) {
- await this.viteServer.close();
- }
- if (this.httpServer) {
- await promisify(this.httpServer.close.bind(this.httpServer))();
- }
- }
-
- public async handleHotUpdate({ file, modules }: HmrContext): Promise<void | ModuleNode[]> {
- const { viteServer } = this;
- if (!viteServer) throw new Error(`AstroDevServer.start() not called`);
-
- for (const module of modules) {
- viteServer.moduleGraph.invalidateModule(module);
- }
-
- const route = this.mostRecentRoute;
- const [pathname, search = undefined] = (route?.pathname ?? '/').split('?');
-
- if (!route) {
- viteServer.ws.send({
- type: 'full-reload',
- });
- return [];
- }
-
- try {
- const filePath = new URL(`./${route.component}`, this.config.projectRoot);
- // try to update the most recent route
- const html = await ssr({
- astroConfig: this.config,
- filePath,
- logging: this.logging,
- mode: 'development',
- origin: this.origin,
- pathname,
- route,
- routeCache: this.routeCache,
- viteServer,
- });
-
- // collect style tags to be reloaded (needed for Tailwind HMR, etc.)
- let invalidatedModules: vite.ModuleNode[] = [];
- await Promise.all(
- collectResources(html)
- .filter(({ href }) => {
- if (!href) return false;
- const ext = path.extname(href);
- return STYLE_EXTENSIONS.has(ext);
- })
- .map(async ({ href }) => {
- const viteModule =
- viteServer.moduleGraph.getModuleById(`${href}?direct`) ||
- (await viteServer.moduleGraph.getModuleByUrl(`${href}?direct`)) ||
- viteServer.moduleGraph.getModuleById(href) ||
- (await viteServer.moduleGraph.getModuleByUrl(href));
- if (viteModule) {
- invalidatedModules.push(viteModule);
- viteServer.moduleGraph.invalidateModule(viteModule);
- }
- })
- );
-
- // TODO: log update
- viteServer.ws.send({
- type: 'custom',
- event: 'astro:reload',
- data: { html },
- });
-
- for (const viteModule of invalidatedModules) {
- // Note: from the time viteServer.moduleGraph.invalidateModule() is called above until now, CSS
- // is building in the background. For things like Tailwind, this can take some time. If the
- // CSS is still processing by the time HMR fires, we’ll end up with stale styles on the page.
- // TODO: fix this hack by figuring out how to add these styles to the { modules } above
- setTimeout(() => {
- viteServer.ws.send({
- type: 'update',
- updates: [
- {
- type: viteModule.type === 'js' ? 'js-update' : 'css-update',
- path: viteModule.id || viteModule.file || viteModule.url,
- acceptedPath: viteModule.url,
- timestamp: Date.now(),
- },
- ],
- });
- }, 150);
- }
-
- return [];
- } catch (e) {
- const err = e as Error;
- // eslint-disable-next-line
- console.error(err.stack);
- viteServer.ws.send({
- type: 'full-reload',
- });
- return [];
- }
- }
-
- /** Expose dev server to this.port */
- public listen(devStart: number): Promise<void> {
- let showedPortTakenMsg = false;
- return new Promise<void>((resolve, reject) => {
- const appListen = () => {
- this.httpServer = this.app.listen(this.port, this.hostname, () => {
- info(this.logging, 'astro', msg.devStart({ startupTime: performance.now() - devStart }));
- info(this.logging, 'astro', msg.devHost({ host: `http://${this.hostname}:${this.port}${this.devRoot}` }));
- resolve();
- });
- this.httpServer?.on('error', onError);
- };
-
- const onError = (err: NodeJS.ErrnoException) => {
- if (err.code && err.code === 'EADDRINUSE') {
- if (!showedPortTakenMsg) {
- info(this.logging, 'astro', msg.portInUse({ port: this.port }));
- showedPortTakenMsg = true; // only print this once
- }
- this.port++;
- return appListen(); // retry
- } else {
- error(this.logging, 'astro', err.stack);
- this.httpServer?.removeListener('error', onError);
- reject(err); // reject
- }
- };
-
- appListen();
- });
- }
-
- private async createViteServer() {
- const viteConfig = await createVite(
- vite.mergeConfig(
- {
- mode: 'development',
- server: {
- middlewareMode: 'ssr',
- host: this.hostname,
- },
- },
- this.config.vite || {}
- ),
- { astroConfig: this.config, logging: this.logging, devServer: this }
- );
- const viteServer = await vite.createServer(viteConfig);
-
- const pagesDirectory = fileURLToPath(this.config.pages);
- viteServer.watcher.on('add', (file) => {
- // Only rebuild routes if new file is a page.
- if (!file.startsWith(pagesDirectory)) {
- return;
- }
- this.routeCache = {};
- this.manifest = createRouteManifest({ config: this.config }, this.logging);
- });
- viteServer.watcher.on('unlink', (file) => {
- // Only rebuild routes if deleted file is a page.
- if (!file.startsWith(pagesDirectory)) {
- return;
- }
- this.routeCache = {};
- this.manifest = createRouteManifest({ config: this.config }, this.logging);
- });
- viteServer.watcher.on('change', () => {
- // No need to rebuild routes on file content changes.
- // However, we DO want to clear the cache in case
- // the change caused a getStaticPaths() return to change.
- this.routeCache = {};
- });
-
- return viteServer;
- }
-
- /** The primary router (runs before Vite, in case we need to modify or intercept anything) */
- private async handleRequest(req: http.IncomingMessage, res: http.ServerResponse, next: NextFunction) {
- if (!this.viteServer) throw new Error(`AstroDevServer.start() not called`);
-
- let [pathname, search = undefined] = (req.url || '/').split('?'); // original request
- const reqStart = performance.now();
- let filePath: URL | undefined;
- try {
- let routePathname: string = pathname;
- // If using a subpath, ensure that the user has included the pathname
- // such as /blog in the URL.
- if (this.devRoot !== '/') {
- if (pathname.startsWith(this.devRoot)) {
- // This includes the subpath, so strip off the subpath so that
- // matchRoute finds this route.
- routePathname = pathname.substr(this.devRoot.length) || '';
- if (!routePathname.startsWith('/')) {
- routePathname = '/' + routePathname;
- }
- } else {
- // Not using the subpath, so forward to Vite's middleware
- next();
- return;
- }
- }
-
- const route = matchRoute(routePathname, this.manifest);
-
- // 404: continue to Vite
- if (!route) {
- // Send through, stripping off the `/blog/` part so that Vite matches it.
- const newPathname = routePathname.startsWith('/') ? routePathname : '/' + routePathname;
- req.url = newPathname;
- next();
- return;
- }
- // handle .astro and .md pages
- filePath = new URL(`./${route.component}`, this.config.projectRoot);
- const html = await ssr({
- astroConfig: this.config,
- filePath,
- logging: this.logging,
- mode: 'development',
- origin: this.origin,
- pathname: routePathname,
- route,
- routeCache: this.routeCache,
- viteServer: this.viteServer,
- });
- this.mostRecentRoute = route;
- info(this.logging, 'astro', msg.req({ url: pathname, statusCode: 200, reqTime: performance.now() - reqStart }));
- res.writeHead(200, {
- 'Content-Type': mime.getType('.html') as string,
- 'Content-Length': Buffer.byteLength(html, 'utf8'),
- });
- res.write(html);
- res.end();
- } catch (err: any) {
- const statusCode = 500;
- await this.viteServer.moduleGraph.invalidateAll();
- this.viteServer.ws.send({ type: 'error', err });
- let html = serverErrorTemplate({
- statusCode,
- title: 'Internal Error',
- tabTitle: '500: Error',
- message: stripAnsi(err.message),
- url: err.url || undefined,
- stack: stripAnsi(err.stack),
- });
- html = await this.viteServer.transformIndexHtml(pathname, html, pathname);
- info(this.logging, 'astro', msg.req({ url: pathname, statusCode: 500, reqTime: performance.now() - reqStart }));
- res.writeHead(statusCode, {
- 'Content-Type': mime.getType('.html') as string,
- 'Content-Length': Buffer.byteLength(html, 'utf8'),
- });
- res.write(html);
- res.end();
- }
- }
-
- /** Render error page */
- private async renderError(req: http.IncomingMessage, res: http.ServerResponse, next: NextFunction) {
- if (!this.viteServer) throw new Error(`AstroDevServer.start() not called`);
-
- const pathname = req.url || '/';
- const reqStart = performance.now();
- let html = '';
- const statusCode = 404;
-
- // attempt to load user-given page
- const relPages = this.config.pages.href.replace(this.config.projectRoot.href, '');
- const userDefined404 = this.manifest.routes.find((route) => route.component === relPages + '404.astro');
- if (userDefined404) {
- html = await ssr({
- astroConfig: this.config,
- filePath: new URL(`./${userDefined404.component}`, this.config.projectRoot),
- logging: this.logging,
- mode: 'development',
- pathname: `/${userDefined404.component}`,
- origin: this.origin,
- routeCache: this.routeCache,
- viteServer: this.viteServer,
- });
- }
- // if not found, fall back to default template
- else {
- if (pathname === '/' && !pathname.startsWith(this.devRoot)) {
- html = subpathNotUsedTemplate(this.devRoot, pathname);
- } else {
- html = notFoundTemplate({ statusCode, title: 'Not found', tabTitle: '404: Not Found', pathname });
- }
- }
- info(this.logging, 'astro', msg.req({ url: pathname, statusCode, reqTime: performance.now() - reqStart }));
- res.writeHead(statusCode, {
- 'Content-Type': mime.getType('.html') as string,
- 'Content-Length': Buffer.byteLength(html, 'utf8'),
- });
- res.write(html);
- res.end();
- }
-}
diff --git a/packages/astro/src/core/dev/messages.ts b/packages/astro/src/core/messages.ts
index b7247dec3..e2272bb9b 100644
--- a/packages/astro/src/core/dev/messages.ts
+++ b/packages/astro/src/core/messages.ts
@@ -2,17 +2,18 @@
* Dev server messages (organized here to prevent clutter)
*/
+import type { AddressInfo } from 'net';
import { bold, dim, green, magenta, yellow } from 'kleur/colors';
-import { pad } from './util.js';
+import { pad } from './dev/util.js';
/** Display */
-export function req({ url, statusCode, reqTime }: { url: string; statusCode: number; reqTime: number }): string {
+export function req({ url, statusCode, reqTime }: { url: string; statusCode: number; reqTime?: number }): string {
let color = dim;
if (statusCode >= 500) color = magenta;
else if (statusCode >= 400) color = yellow;
else if (statusCode >= 300) color = dim;
else if (statusCode >= 200) color = green;
- return `${color(statusCode)} ${pad(url, 40)} ${dim(Math.round(reqTime) + 'ms')}`;
+ return `${color(statusCode)} ${pad(url, 40)} ${reqTime ? dim(Math.round(reqTime) + 'ms') : ''}`;
}
/** Display */
@@ -27,8 +28,10 @@ export function devStart({ startupTime }: { startupTime: number }): string {
}
/** Display dev server host */
-export function devHost({ host }: { host: string }): string {
- return `Local: ${bold(magenta(host))}`;
+export function devHost({ address, https, site }: { address: AddressInfo; https: boolean; site: URL | undefined }): string {
+ const rootPath = site ? site.pathname : '/';
+ const displayUrl = `${https ? 'https' : 'http'}://${address.address}:${address.port}${rootPath}`;
+ return `Local: ${bold(magenta(displayUrl))}`;
}
/** Display port in use */
diff --git a/packages/astro/src/core/preview/index.ts b/packages/astro/src/core/preview/index.ts
index 60296bc73..5dff165e2 100644
--- a/packages/astro/src/core/preview/index.ts
+++ b/packages/astro/src/core/preview/index.ts
@@ -7,9 +7,9 @@ import { performance } from 'perf_hooks';
import send from 'send';
import { fileURLToPath } from 'url';
import fs from 'fs';
-import * as msg from '../dev/messages.js';
+import * as msg from '../messages.js';
import { error, info } from '../logger.js';
-import { subpathNotUsedTemplate, notFoundTemplate, default as template } from '../dev/template/4xx.js';
+import { subpathNotUsedTemplate, notFoundTemplate, default as template } from '../../template/4xx.js';
import { appendForwardSlash, trimSlashes } from '../path.js';
interface PreviewOptions {
@@ -43,7 +43,7 @@ export default async function preview(config: AstroConfig, { logging }: PreviewO
};
/** Base request URL. */
- let baseURL = new URL(appendForwardSlash(config.buildOptions.site || ''), defaultOrigin);
+ let baseURL = new URL(config.buildOptions.site || '/', defaultOrigin);
// Create the preview server, send static files out of the `dist/` directory.
const server = http.createServer((req, res) => {
@@ -126,7 +126,7 @@ export default async function preview(config: AstroConfig, { logging }: PreviewO
httpServer = server.listen(port, hostname, () => {
if (!showedListenMsg) {
info(logging, 'astro', msg.devStart({ startupTime: performance.now() - timerStart }));
- info(logging, 'astro', msg.devHost({ host: `http://${hostname}:${port}${baseURL.pathname}` }));
+ info(logging, 'astro', msg.devHost({ address: { family: 'ipv4', address: hostname, port }, https: false, site: baseURL }));
}
showedListenMsg = true;
resolve();
diff --git a/packages/astro/src/core/util.ts b/packages/astro/src/core/util.ts
index 8f7939abd..58c7fc9d0 100644
--- a/packages/astro/src/core/util.ts
+++ b/packages/astro/src/core/util.ts
@@ -48,6 +48,11 @@ export function parseNpmName(spec: string): { scope?: string; name: string; subp
};
}
+/** Coalesce any throw variable to an Error instance. */
+export function createSafeError(err: any): Error {
+ return err instanceof Error || (err && err.name && err.message) ? err : new Error(JSON.stringify(err));
+}
+
/** generate code frame from esbuild error */
export function codeFrame(src: string, loc: ErrorPayload['err']['loc']): string {
if (!loc) return '';
diff --git a/packages/astro/src/core/dev/template/4xx.ts b/packages/astro/src/template/4xx.ts
index 7f90b6e16..7f90b6e16 100644
--- a/packages/astro/src/core/dev/template/4xx.ts
+++ b/packages/astro/src/template/4xx.ts
diff --git a/packages/astro/src/core/dev/template/5xx.ts b/packages/astro/src/template/5xx.ts
index ee1f17fa6..ee1f17fa6 100644
--- a/packages/astro/src/core/dev/template/5xx.ts
+++ b/packages/astro/src/template/5xx.ts
diff --git a/packages/astro/src/core/dev/template/css.ts b/packages/astro/src/template/css.ts
index 87fbfd1b8..87fbfd1b8 100644
--- a/packages/astro/src/core/dev/template/css.ts
+++ b/packages/astro/src/template/css.ts
diff --git a/packages/astro/src/vite-plugin-astro-postprocess/index.ts b/packages/astro/src/vite-plugin-astro-postprocess/index.ts
index 96087966f..807e68c70 100644
--- a/packages/astro/src/vite-plugin-astro-postprocess/index.ts
+++ b/packages/astro/src/vite-plugin-astro-postprocess/index.ts
@@ -1,22 +1,20 @@
import type * as t from '@babel/types';
import type { Plugin } from '../core/vite';
import type { AstroConfig } from '../@types/astro';
-import type { AstroDevServer } from '../core/dev/index';
import * as babelTraverse from '@babel/traverse';
import * as babel from '@babel/core';
interface AstroPluginOptions {
config: AstroConfig;
- devServer?: AstroDevServer;
}
// esbuild transforms the component-scoped Astro into Astro2, so need to check both.
const validAstroGlobalNames = new Set(['Astro', 'Astro2']);
-export default function astro({ config, devServer }: AstroPluginOptions): Plugin {
+export default function astro({ config }: AstroPluginOptions): Plugin {
return {
- name: '@astrojs/vite-plugin-astro-postprocess',
+ name: 'astro:postprocess',
async transform(code, id) {
// Currently only supported in ".astro" & ".md" files
if (!id.endsWith('.astro') && !id.endsWith('.md')) {
diff --git a/packages/astro/src/vite-plugin-astro-server/index.ts b/packages/astro/src/vite-plugin-astro-server/index.ts
new file mode 100644
index 000000000..701457f93
--- /dev/null
+++ b/packages/astro/src/vite-plugin-astro-server/index.ts
@@ -0,0 +1,156 @@
+import type vite from '../core/vite';
+import type http from 'http';
+import type { AstroConfig, ManifestData, RouteCache, RouteData } from '../@types/astro';
+import { info, LogOptions } from '../core/logger.js';
+import { fileURLToPath } from 'url';
+import { createRouteManifest, matchRoute } from '../core/ssr/routing.js';
+import mime from 'mime';
+import stripAnsi from 'strip-ansi';
+import { createSafeError } from '../core/util.js';
+import { ssr } from '../core/ssr/index.js';
+import * as msg from '../core/messages.js';
+
+import notFoundTemplate, { subpathNotUsedTemplate } from '../template/4xx.js';
+import serverErrorTemplate from '../template/5xx.js';
+
+interface AstroPluginOptions {
+ config: AstroConfig;
+ logging: LogOptions;
+}
+
+const BAD_VITE_MIDDLEWARE = ['viteIndexHtmlMiddleware', 'vite404Middleware', 'viteSpaFallbackMiddleware'];
+function removeViteHttpMiddleware(server: vite.Connect.Server) {
+ for (let i = server.stack.length - 1; i > 0; i--) {
+ // @ts-expect-error using internals until https://github.com/vitejs/vite/pull/4640 is merged
+ if (BAD_VITE_MIDDLEWARE.includes(server.stack[i].handle.name)) {
+ server.stack.splice(i, 1);
+ }
+ }
+}
+
+function writeHtmlResponse(res: http.ServerResponse, statusCode: number, html: string) {
+ res.writeHead(statusCode, {
+ 'Content-Type': mime.getType('.html') as string,
+ 'Content-Length': Buffer.byteLength(html, 'utf8'),
+ });
+ res.write(html);
+ res.end();
+}
+
+async function handle404Response(origin: string, config: AstroConfig, req: http.IncomingMessage, res: http.ServerResponse) {
+ const site = config.buildOptions.site ? new URL(config.buildOptions.site) : undefined;
+ const devRoot = site ? site.pathname : '/';
+ const pathname = decodeURI(new URL(origin + req.url).pathname);
+ let html = '';
+ if (pathname === '/' && !pathname.startsWith(devRoot)) {
+ html = subpathNotUsedTemplate(devRoot, pathname);
+ } else {
+ 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: any) {
+ const pathname = decodeURI(new URL(origin + req.url).pathname);
+ const html = serverErrorTemplate({
+ statusCode: 500,
+ title: 'Internal Error',
+ tabTitle: '500: Error',
+ message: stripAnsi(err.message),
+ url: err.url || undefined,
+ stack: stripAnsi(err.stack),
+ });
+ const transformedHtml = await viteServer.transformIndexHtml(pathname, html, pathname);
+ writeHtmlResponse(res, 500, transformedHtml);
+}
+
+/** The main logic to route dev server requests to pages in Astro. */
+async function handleRequest(
+ routeCache: RouteCache,
+ viteServer: vite.ViteDevServer,
+ logging: LogOptions,
+ manifest: ManifestData,
+ config: AstroConfig,
+ req: http.IncomingMessage,
+ res: http.ServerResponse
+) {
+ const reqStart = performance.now();
+ const site = config.buildOptions.site ? new URL(config.buildOptions.site) : undefined;
+ const devRoot = site ? site.pathname : '/';
+ const origin = `${viteServer.config.server.https ? 'https' : 'http'}://${req.headers.host}`;
+ const pathname = decodeURI(new URL(origin + req.url).pathname);
+ const rootRelativeUrl = pathname.substring(devRoot.length - 1);
+ try {
+ if (!pathname.startsWith(devRoot)) {
+ info(logging, 'astro', msg.req({ url: pathname, statusCode: 404 }));
+ return handle404Response(origin, config, req, res);
+ }
+ // Attempt to match the URL to a valid page route.
+ // If that fails, switch the response to a 404 response.
+ let route = matchRoute(rootRelativeUrl, manifest);
+ const statusCode = route ? 200 : 404;
+ // If no match found, lookup a custom 404 page to render, if one exists.
+ if (!route) {
+ const relPages = config.pages.href.replace(config.projectRoot.href, '');
+ route = manifest.routes.find((r) => r.component === relPages + '404.astro');
+ }
+ // If still no match is found, respond with a generic 404 page.
+ if (!route) {
+ info(logging, 'astro', msg.req({ url: pathname, statusCode: 404 }));
+ handle404Response(origin, config, req, res);
+ return;
+ }
+ // Route successfully matched! Render it.
+ const html = await ssr({
+ astroConfig: config,
+ filePath: new URL(`./${route.component}`, config.projectRoot),
+ logging,
+ mode: 'development',
+ origin,
+ pathname: rootRelativeUrl,
+ route,
+ routeCache: routeCache,
+ viteServer: viteServer,
+ });
+ info(logging, 'astro', msg.req({ url: pathname, statusCode, reqTime: performance.now() - reqStart }));
+ writeHtmlResponse(res, statusCode, html);
+ } catch (_err: any) {
+ info(logging, 'astro', msg.req({ url: pathname, statusCode: 500 }));
+ const err = createSafeError(_err);
+ handle500Response(viteServer, origin, req, res, err);
+ }
+}
+
+export default function createPlugin({ config, logging }: AstroPluginOptions): vite.Plugin {
+ return {
+ name: 'astro:server',
+ configureServer(viteServer) {
+ const pagesDirectory = fileURLToPath(config.pages);
+ let routeCache: RouteCache = {};
+ let manifest: ManifestData = createRouteManifest({ config: config }, logging);
+ /** rebuild the route cache + manifest if the changed file impacts routing. */
+ function rebuildManifestIfNeeded(file: string) {
+ if (file.startsWith(pagesDirectory)) {
+ routeCache = {};
+ manifest = createRouteManifest({ config: config }, logging);
+ }
+ }
+ // Rebuild route manifest on file change, if needed.
+ viteServer.watcher.on('add', rebuildManifestIfNeeded);
+ viteServer.watcher.on('unlink', rebuildManifestIfNeeded);
+ // No need to rebuild routes on content-only changes.
+ // However, we DO want to clear the cache in case
+ // the change caused a getStaticPaths() return to change.
+ viteServer.watcher.on('change', () => (routeCache = {}));
+ return () => {
+ removeViteHttpMiddleware(viteServer.middlewares);
+ viteServer.middlewares.use(async (req, res) => {
+ if (!req.url || !req.method) {
+ throw new Error('Incomplete request');
+ }
+ handleRequest(routeCache, viteServer, logging, manifest, config, req, res);
+ });
+ };
+ },
+ };
+}
diff --git a/packages/astro/src/vite-plugin-astro/index.ts b/packages/astro/src/vite-plugin-astro/index.ts
index a252a8e30..c19f79842 100644
--- a/packages/astro/src/vite-plugin-astro/index.ts
+++ b/packages/astro/src/vite-plugin-astro/index.ts
@@ -1,11 +1,10 @@
import type vite from '../core/vite';
import type { AstroConfig } from '../@types/astro';
-import type { LogOptions } from '../core/logger';
+import type { LogOptions } from '../core/logger.js';
import esbuild from 'esbuild';
import npath from 'path';
import { fileURLToPath } from 'url';
-import { AstroDevServer } from '../core/dev/index.js';
import { getViteTransform, TransformHook } from './styles.js';
import { parseAstroRequest } from './query.js';
import { cachedCompilation, invalidateCompilation } from './compile.js';
@@ -15,7 +14,6 @@ const FRONTMATTER_PARSE_REGEXP = /^\-\-\-(.*)^\-\-\-/ms;
interface AstroPluginOptions {
config: AstroConfig;
logging: LogOptions;
- devServer?: AstroDevServer;
}
/** Transform .astro files for Vite */
@@ -36,7 +34,7 @@ export default function astro({ config, logging }: AstroPluginOptions): vite.Plu
const isBrowserPath = (path: string) => path.startsWith(srcRootWeb);
return {
- name: '@astrojs/vite-plugin-astro',
+ name: 'astro:build',
enforce: 'pre', // run transforms before other plugins can
configResolved(resolvedConfig) {
viteTransform = getViteTransform(resolvedConfig);
diff --git a/packages/astro/src/vite-plugin-build-html/index.ts b/packages/astro/src/vite-plugin-build-html/index.ts
index 22bad8073..9a0bc6957 100644
--- a/packages/astro/src/vite-plugin-build-html/index.ts
+++ b/packages/astro/src/vite-plugin-build-html/index.ts
@@ -1,5 +1,5 @@
import type { AstroConfig, RouteCache } from '../@types/astro';
-import type { LogOptions } from '../core/logger';
+import type { LogOptions } from '../core/logger.js';
import type { ViteDevServer, Plugin as VitePlugin } from '../core/vite';
import type { OutputChunk, PreRenderedChunk } from 'rollup';
import type { AllPagesData } from '../core/build/types';
diff --git a/packages/astro/src/vite-plugin-config-alias/index.ts b/packages/astro/src/vite-plugin-config-alias/index.ts
index 15ae0bffc..c6fa6fd81 100644
--- a/packages/astro/src/vite-plugin-config-alias/index.ts
+++ b/packages/astro/src/vite-plugin-config-alias/index.ts
@@ -78,7 +78,7 @@ export default function configAliasVitePlugin(astroConfig: { projectRoot?: URL;
if (!configAlias) return {} as vite.PluginOption;
return {
- name: '@astrojs/vite-plugin-config-alias',
+ name: 'astro:tsconfig-alias',
enforce: 'pre',
async resolveId(sourceId: string, importer, options) {
/** Resolved ID conditionally handled by any other resolver. (this gives priority to all other resolvers) */
diff --git a/packages/astro/src/vite-plugin-jsx/index.ts b/packages/astro/src/vite-plugin-jsx/index.ts
index a1efb6ffd..642aa865d 100644
--- a/packages/astro/src/vite-plugin-jsx/index.ts
+++ b/packages/astro/src/vite-plugin-jsx/index.ts
@@ -1,7 +1,7 @@
import type { TransformResult } from 'rollup';
import type { Plugin, ResolvedConfig } from '../core/vite';
import type { AstroConfig, Renderer } from '../@types/astro';
-import type { LogOptions } from '../core/logger';
+import type { LogOptions } from '../core/logger.js';
import babel from '@babel/core';
import esbuild from 'esbuild';
@@ -98,7 +98,7 @@ export default function jsx({ config, logging }: AstroPluginJSXOptions): Plugin
let viteConfig: ResolvedConfig;
return {
- name: '@astrojs/vite-plugin-jsx',
+ name: 'astro:jsx',
enforce: 'pre', // run transforms before other plugins
configResolved(resolvedConfig) {
viteConfig = resolvedConfig;
diff --git a/packages/astro/src/vite-plugin-markdown/index.ts b/packages/astro/src/vite-plugin-markdown/index.ts
index 9b3ccaca8..99888e8b9 100644
--- a/packages/astro/src/vite-plugin-markdown/index.ts
+++ b/packages/astro/src/vite-plugin-markdown/index.ts
@@ -4,17 +4,15 @@ import type { AstroConfig } from '../@types/astro';
import esbuild from 'esbuild';
import fs from 'fs';
import { transform } from '@astrojs/compiler';
-import { AstroDevServer } from '../core/dev/index.js';
interface AstroPluginOptions {
config: AstroConfig;
- devServer?: AstroDevServer;
}
/** Transform .astro files for Vite */
export default function markdown({ config }: AstroPluginOptions): Plugin {
return {
- name: '@astrojs/vite-plugin-markdown',
+ name: 'astro:markdown',
enforce: 'pre', // run transforms before other plugins can
async load(id) {
if (id.endsWith('.md')) {
diff --git a/packages/astro/test/test-utils.js b/packages/astro/test/test-utils.js
index 88b46908d..f14d7b311 100644
--- a/packages/astro/test/test-utils.js
+++ b/packages/astro/test/test-utils.js
@@ -73,10 +73,10 @@ export async function loadFixture(inlineConfig) {
return {
build: (opts = {}) => build(config, { mode: 'development', logging: 'error', ...opts }),
startDevServer: async (opts = {}) => {
- const devServer = await dev(config, { logging: 'error', ...opts });
- config.devOptions.port = devServer.port; // update port
- inlineConfig.devOptions.port = devServer.port;
- return devServer;
+ const devResult = await dev(config, { logging: 'error', ...opts });
+ config.devOptions.port = devResult.address.port; // update port
+ inlineConfig.devOptions.port = devResult.address.port;
+ return devResult;
},
config,
fetch: (url, init) => fetch(`http://${config.devOptions.hostname}:${config.devOptions.port}${url.replace(/^\/?/, '/')}`, init),
diff --git a/yarn.lock b/yarn.lock
index 53b001b37..f46009e71 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3185,16 +3185,6 @@ condense-whitespace@^1.0.0:
resolved "https://registry.yarnpkg.com/condense-whitespace/-/condense-whitespace-1.0.0.tgz#8376d98ef028e6cb2cd2468e28ce42c5c65ab1a9"
integrity sha1-g3bZjvAo5sss0kaOKM5CxcZasak=
-connect@^3.7.0:
- version "3.7.0"
- resolved "https://registry.yarnpkg.com/connect/-/connect-3.7.0.tgz#5d49348910caa5e07a01800b030d0c35f20484f8"
- integrity sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==
- dependencies:
- debug "2.6.9"
- finalhandler "1.1.2"
- parseurl "~1.3.3"
- utils-merge "1.0.1"
-
consola@^2.15.3:
version "2.15.3"
resolved "https://registry.yarnpkg.com/consola/-/consola-2.15.3.tgz#2e11f98d6a4be71ff72e0bdf07bd23e12cb61550"
@@ -4479,19 +4469,6 @@ fill-range@^7.0.1:
dependencies:
to-regex-range "^5.0.1"
-finalhandler@1.1.2:
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d"
- integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==
- dependencies:
- debug "2.6.9"
- encodeurl "~1.0.2"
- escape-html "~1.0.3"
- on-finished "~2.3.0"
- parseurl "~1.3.3"
- statuses "~1.5.0"
- unpipe "~1.0.0"
-
find-babel-config@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/find-babel-config/-/find-babel-config-1.2.0.tgz#a9b7b317eb5b9860cda9d54740a8c8337a2283a2"
@@ -7168,11 +7145,6 @@ parse5@^6.0.0, parse5@^6.0.1:
resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b"
integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==
-parseurl@~1.3.3:
- version "1.3.3"
- resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
- integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
-
path-browserify@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd"
@@ -9329,7 +9301,7 @@ unocss@^0.15.5:
"@unocss/reset" "0.15.6"
"@unocss/vite" "0.15.6"
-unpipe@1.0.0, unpipe@~1.0.0:
+unpipe@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=
@@ -9387,11 +9359,6 @@ util@^0.12.0:
safe-buffer "^5.1.2"
which-typed-array "^1.1.2"
-utils-merge@1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
- integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=
-
uuid@^2.0.1:
version "2.0.3"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a"