refactor dev to use vite server (#2494)

This commit is contained in:
Fred K. Schott 2022-01-31 11:44:40 -08:00 committed by GitHub
parent 9c48c2b58e
commit d7149f9b2f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 222 additions and 449 deletions

View file

@ -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!

View file

@ -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 This license applies to parts of the `packages/create-astro` and `packages/astro` subdirectories originating from the https://github.com/sveltejs/kit repository:
https://github.com/sveltejs/kit repository:
Copyright (c) 2020 [these people](https://github.com/sveltejs/kit/graphs/contributors) Copyright (c) 2020 [these people](https://github.com/sveltejs/kit/graphs/contributors)

View file

@ -73,7 +73,6 @@
"astring": "^1.7.5", "astring": "^1.7.5",
"ci-info": "^3.2.0", "ci-info": "^3.2.0",
"common-ancestor-path": "^1.0.1", "common-ancestor-path": "^1.0.1",
"connect": "^3.7.0",
"eol": "^0.9.1", "eol": "^0.9.1",
"es-module-lexer": "^0.9.3", "es-module-lexer": "^0.9.3",
"esbuild": "0.13.7", "esbuild": "0.13.7",

View file

@ -1,7 +1,7 @@
/* eslint-disable no-console */ /* eslint-disable no-console */
import type { AstroConfig } from '../@types/astro'; 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 * as colors from 'kleur/colors';
import fs from 'fs'; import fs from 'fs';

View file

@ -1,11 +1,11 @@
import type { AstroConfig } from '../@types/astro'; import type { AstroConfig } from '../@types/astro';
import type { AstroDevServer } from './dev';
import type { LogOptions } from './logger'; import type { LogOptions } from './logger';
import { builtinModules } from 'module'; import { builtinModules } from 'module';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import vite from './vite.js'; import vite from './vite.js';
import astroVitePlugin from '../vite-plugin-astro/index.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 astroPostprocessVitePlugin from '../vite-plugin-astro-postprocess/index.js';
import configAliasVitePlugin from '../vite-plugin-config-alias/index.js'; import configAliasVitePlugin from '../vite-plugin-config-alias/index.js';
import markdownVitePlugin from '../vite-plugin-markdown/index.js'; import markdownVitePlugin from '../vite-plugin-markdown/index.js';
@ -34,12 +34,11 @@ export type ViteConfigWithSSR = vite.InlineConfig & { ssr?: { external?: string[
interface CreateViteOptions { interface CreateViteOptions {
astroConfig: AstroConfig; astroConfig: AstroConfig;
devServer?: AstroDevServer;
logging: LogOptions; logging: LogOptions;
} }
/** Return a common starting point for all Vite actions */ /** 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 // First, start with the Vite configuration that Astro core needs
let viteConfig: ViteConfigWithSSR = { let viteConfig: ViteConfigWithSSR = {
cacheDir: fileURLToPath(new URL('./node_modules/.vite/', astroConfig.projectRoot)), // using local caches allows Astro to be used in monorepos, etc. 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: [ plugins: [
configAliasVitePlugin({ config: astroConfig }), configAliasVitePlugin({ config: astroConfig }),
astroVitePlugin({ config: astroConfig, devServer, logging }), astroVitePlugin({ config: astroConfig, logging }),
markdownVitePlugin({ config: astroConfig, devServer }), astroViteServerPlugin({ config: astroConfig, logging }),
markdownVitePlugin({ config: astroConfig }),
jsxVitePlugin({ config: astroConfig, logging }), jsxVitePlugin({ config: astroConfig, logging }),
astroPostprocessVitePlugin({ config: astroConfig, devServer }), astroPostprocessVitePlugin({ config: astroConfig }),
], ],
publicDir: fileURLToPath(astroConfig.public), publicDir: fileURLToPath(astroConfig.public),
root: fileURLToPath(astroConfig.projectRoot), root: fileURLToPath(astroConfig.projectRoot),

View file

@ -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 { polyfill } from '@astropub/webapi';
import type { AddressInfo } from 'net';
import { performance } from 'perf_hooks'; import { performance } from 'perf_hooks';
import stripAnsi from 'strip-ansi'; import type { AstroConfig } from '../../@types/astro';
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 { createVite } from '../create-vite.js'; import { createVite } from '../create-vite.js';
import * as msg from './messages.js'; import { defaultLogOptions, info, LogOptions } from '../logger.js';
import notFoundTemplate, { subpathNotUsedTemplate } from './template/4xx.js'; import vite from '../vite.js';
import serverErrorTemplate from './template/5xx.js'; import * as msg from '../messages.js';
export interface DevOptions { export interface DevOptions {
logging: LogOptions; logging: LogOptions;
} }
export interface DevServer { export interface DevServer {
hostname: string; address: AddressInfo;
port: number;
server: connect.Server;
stop(): Promise<void>; stop(): Promise<void>;
} }
/** `astro dev` */ /** `astro dev` */
export default async function dev(config: AstroConfig, options: DevOptions = { logging: defaultLogOptions }): Promise<DevServer> { 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, { polyfill(globalThis, {
exclude: 'window document', 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 { return {
hostname: server.hostname, address,
port: server.port, stop: () => viteServer.close(),
server: server.app,
stop: () => server.stop(),
}; };
} }
/** 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, well 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();
}
}

View file

@ -2,17 +2,18 @@
* Dev server messages (organized here to prevent clutter) * Dev server messages (organized here to prevent clutter)
*/ */
import type { AddressInfo } from 'net';
import { bold, dim, green, magenta, yellow } from 'kleur/colors'; import { bold, dim, green, magenta, yellow } from 'kleur/colors';
import { pad } from './util.js'; import { pad } from './dev/util.js';
/** Display */ /** 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; let color = dim;
if (statusCode >= 500) color = magenta; if (statusCode >= 500) color = magenta;
else if (statusCode >= 400) color = yellow; else if (statusCode >= 400) color = yellow;
else if (statusCode >= 300) color = dim; else if (statusCode >= 300) color = dim;
else if (statusCode >= 200) color = green; 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 */ /** Display */
@ -27,8 +28,10 @@ export function devStart({ startupTime }: { startupTime: number }): string {
} }
/** Display dev server host */ /** Display dev server host */
export function devHost({ host }: { host: string }): string { export function devHost({ address, https, site }: { address: AddressInfo; https: boolean; site: URL | undefined }): string {
return `Local: ${bold(magenta(host))}`; const rootPath = site ? site.pathname : '/';
const displayUrl = `${https ? 'https' : 'http'}://${address.address}:${address.port}${rootPath}`;
return `Local: ${bold(magenta(displayUrl))}`;
} }
/** Display port in use */ /** Display port in use */

View file

@ -7,9 +7,9 @@ import { performance } from 'perf_hooks';
import send from 'send'; import send from 'send';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import fs from 'fs'; import fs from 'fs';
import * as msg from '../dev/messages.js'; import * as msg from '../messages.js';
import { error, info } from '../logger.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'; import { appendForwardSlash, trimSlashes } from '../path.js';
interface PreviewOptions { interface PreviewOptions {
@ -43,7 +43,7 @@ export default async function preview(config: AstroConfig, { logging }: PreviewO
}; };
/** Base request URL. */ /** 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. // Create the preview server, send static files out of the `dist/` directory.
const server = http.createServer((req, res) => { const server = http.createServer((req, res) => {
@ -126,7 +126,7 @@ export default async function preview(config: AstroConfig, { logging }: PreviewO
httpServer = server.listen(port, hostname, () => { httpServer = server.listen(port, hostname, () => {
if (!showedListenMsg) { if (!showedListenMsg) {
info(logging, 'astro', msg.devStart({ startupTime: performance.now() - timerStart })); 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; showedListenMsg = true;
resolve(); resolve();

View file

@ -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 */ /** generate code frame from esbuild error */
export function codeFrame(src: string, loc: ErrorPayload['err']['loc']): string { export function codeFrame(src: string, loc: ErrorPayload['err']['loc']): string {
if (!loc) return ''; if (!loc) return '';

View file

@ -1,22 +1,20 @@
import type * as t from '@babel/types'; import type * as t from '@babel/types';
import type { Plugin } from '../core/vite'; import type { Plugin } from '../core/vite';
import type { AstroConfig } from '../@types/astro'; import type { AstroConfig } from '../@types/astro';
import type { AstroDevServer } from '../core/dev/index';
import * as babelTraverse from '@babel/traverse'; import * as babelTraverse from '@babel/traverse';
import * as babel from '@babel/core'; import * as babel from '@babel/core';
interface AstroPluginOptions { interface AstroPluginOptions {
config: AstroConfig; config: AstroConfig;
devServer?: AstroDevServer;
} }
// esbuild transforms the component-scoped Astro into Astro2, so need to check both. // esbuild transforms the component-scoped Astro into Astro2, so need to check both.
const validAstroGlobalNames = new Set(['Astro', 'Astro2']); const validAstroGlobalNames = new Set(['Astro', 'Astro2']);
export default function astro({ config, devServer }: AstroPluginOptions): Plugin { export default function astro({ config }: AstroPluginOptions): Plugin {
return { return {
name: '@astrojs/vite-plugin-astro-postprocess', name: 'astro:postprocess',
async transform(code, id) { async transform(code, id) {
// Currently only supported in ".astro" & ".md" files // Currently only supported in ".astro" & ".md" files
if (!id.endsWith('.astro') && !id.endsWith('.md')) { if (!id.endsWith('.astro') && !id.endsWith('.md')) {

View file

@ -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);
});
};
},
};
}

View file

@ -1,11 +1,10 @@
import type vite from '../core/vite'; import type vite from '../core/vite';
import type { AstroConfig } from '../@types/astro'; import type { AstroConfig } from '../@types/astro';
import type { LogOptions } from '../core/logger'; import type { LogOptions } from '../core/logger.js';
import esbuild from 'esbuild'; import esbuild from 'esbuild';
import npath from 'path'; import npath from 'path';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import { AstroDevServer } from '../core/dev/index.js';
import { getViteTransform, TransformHook } from './styles.js'; import { getViteTransform, TransformHook } from './styles.js';
import { parseAstroRequest } from './query.js'; import { parseAstroRequest } from './query.js';
import { cachedCompilation, invalidateCompilation } from './compile.js'; import { cachedCompilation, invalidateCompilation } from './compile.js';
@ -15,7 +14,6 @@ const FRONTMATTER_PARSE_REGEXP = /^\-\-\-(.*)^\-\-\-/ms;
interface AstroPluginOptions { interface AstroPluginOptions {
config: AstroConfig; config: AstroConfig;
logging: LogOptions; logging: LogOptions;
devServer?: AstroDevServer;
} }
/** Transform .astro files for Vite */ /** 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); const isBrowserPath = (path: string) => path.startsWith(srcRootWeb);
return { return {
name: '@astrojs/vite-plugin-astro', name: 'astro:build',
enforce: 'pre', // run transforms before other plugins can enforce: 'pre', // run transforms before other plugins can
configResolved(resolvedConfig) { configResolved(resolvedConfig) {
viteTransform = getViteTransform(resolvedConfig); viteTransform = getViteTransform(resolvedConfig);

View file

@ -1,5 +1,5 @@
import type { AstroConfig, RouteCache } from '../@types/astro'; 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 { ViteDevServer, Plugin as VitePlugin } from '../core/vite';
import type { OutputChunk, PreRenderedChunk } from 'rollup'; import type { OutputChunk, PreRenderedChunk } from 'rollup';
import type { AllPagesData } from '../core/build/types'; import type { AllPagesData } from '../core/build/types';

View file

@ -78,7 +78,7 @@ export default function configAliasVitePlugin(astroConfig: { projectRoot?: URL;
if (!configAlias) return {} as vite.PluginOption; if (!configAlias) return {} as vite.PluginOption;
return { return {
name: '@astrojs/vite-plugin-config-alias', name: 'astro:tsconfig-alias',
enforce: 'pre', enforce: 'pre',
async resolveId(sourceId: string, importer, options) { async resolveId(sourceId: string, importer, options) {
/** Resolved ID conditionally handled by any other resolver. (this gives priority to all other resolvers) */ /** Resolved ID conditionally handled by any other resolver. (this gives priority to all other resolvers) */

View file

@ -1,7 +1,7 @@
import type { TransformResult } from 'rollup'; import type { TransformResult } from 'rollup';
import type { Plugin, ResolvedConfig } from '../core/vite'; import type { Plugin, ResolvedConfig } from '../core/vite';
import type { AstroConfig, Renderer } from '../@types/astro'; 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 babel from '@babel/core';
import esbuild from 'esbuild'; import esbuild from 'esbuild';
@ -98,7 +98,7 @@ export default function jsx({ config, logging }: AstroPluginJSXOptions): Plugin
let viteConfig: ResolvedConfig; let viteConfig: ResolvedConfig;
return { return {
name: '@astrojs/vite-plugin-jsx', name: 'astro:jsx',
enforce: 'pre', // run transforms before other plugins enforce: 'pre', // run transforms before other plugins
configResolved(resolvedConfig) { configResolved(resolvedConfig) {
viteConfig = resolvedConfig; viteConfig = resolvedConfig;

View file

@ -4,17 +4,15 @@ import type { AstroConfig } from '../@types/astro';
import esbuild from 'esbuild'; import esbuild from 'esbuild';
import fs from 'fs'; import fs from 'fs';
import { transform } from '@astrojs/compiler'; import { transform } from '@astrojs/compiler';
import { AstroDevServer } from '../core/dev/index.js';
interface AstroPluginOptions { interface AstroPluginOptions {
config: AstroConfig; config: AstroConfig;
devServer?: AstroDevServer;
} }
/** Transform .astro files for Vite */ /** Transform .astro files for Vite */
export default function markdown({ config }: AstroPluginOptions): Plugin { export default function markdown({ config }: AstroPluginOptions): Plugin {
return { return {
name: '@astrojs/vite-plugin-markdown', name: 'astro:markdown',
enforce: 'pre', // run transforms before other plugins can enforce: 'pre', // run transforms before other plugins can
async load(id) { async load(id) {
if (id.endsWith('.md')) { if (id.endsWith('.md')) {

View file

@ -73,10 +73,10 @@ export async function loadFixture(inlineConfig) {
return { return {
build: (opts = {}) => build(config, { mode: 'development', logging: 'error', ...opts }), build: (opts = {}) => build(config, { mode: 'development', logging: 'error', ...opts }),
startDevServer: async (opts = {}) => { startDevServer: async (opts = {}) => {
const devServer = await dev(config, { logging: 'error', ...opts }); const devResult = await dev(config, { logging: 'error', ...opts });
config.devOptions.port = devServer.port; // update port config.devOptions.port = devResult.address.port; // update port
inlineConfig.devOptions.port = devServer.port; inlineConfig.devOptions.port = devResult.address.port;
return devServer; return devResult;
}, },
config, config,
fetch: (url, init) => fetch(`http://${config.devOptions.hostname}:${config.devOptions.port}${url.replace(/^\/?/, '/')}`, init), fetch: (url, init) => fetch(`http://${config.devOptions.hostname}:${config.devOptions.port}${url.replace(/^\/?/, '/')}`, init),

View file

@ -3185,16 +3185,6 @@ condense-whitespace@^1.0.0:
resolved "https://registry.yarnpkg.com/condense-whitespace/-/condense-whitespace-1.0.0.tgz#8376d98ef028e6cb2cd2468e28ce42c5c65ab1a9" resolved "https://registry.yarnpkg.com/condense-whitespace/-/condense-whitespace-1.0.0.tgz#8376d98ef028e6cb2cd2468e28ce42c5c65ab1a9"
integrity sha1-g3bZjvAo5sss0kaOKM5CxcZasak= 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: consola@^2.15.3:
version "2.15.3" version "2.15.3"
resolved "https://registry.yarnpkg.com/consola/-/consola-2.15.3.tgz#2e11f98d6a4be71ff72e0bdf07bd23e12cb61550" resolved "https://registry.yarnpkg.com/consola/-/consola-2.15.3.tgz#2e11f98d6a4be71ff72e0bdf07bd23e12cb61550"
@ -4479,19 +4469,6 @@ fill-range@^7.0.1:
dependencies: dependencies:
to-regex-range "^5.0.1" 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: find-babel-config@^1.2.0:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/find-babel-config/-/find-babel-config-1.2.0.tgz#a9b7b317eb5b9860cda9d54740a8c8337a2283a2" 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" resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b"
integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== 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: path-browserify@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd" 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/reset" "0.15.6"
"@unocss/vite" "0.15.6" "@unocss/vite" "0.15.6"
unpipe@1.0.0, unpipe@~1.0.0: unpipe@1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=
@ -9387,11 +9359,6 @@ util@^0.12.0:
safe-buffer "^5.1.2" safe-buffer "^5.1.2"
which-typed-array "^1.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: uuid@^2.0.1:
version "2.0.3" version "2.0.3"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a" resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a"