refactor dev to use vite server (#2494)
This commit is contained in:
parent
9c48c2b58e
commit
d7149f9b2f
21 changed files with 222 additions and 449 deletions
7
.changeset/empty-snails-allow.md
Normal file
7
.changeset/empty-snails-allow.md
Normal 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!
|
3
LICENSE
3
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)
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 */
|
|
@ -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();
|
||||
|
|
|
@ -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 '';
|
||||
|
|
|
@ -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')) {
|
||||
|
|
156
packages/astro/src/vite-plugin-astro-server/index.ts
Normal file
156
packages/astro/src/vite-plugin-astro-server/index.ts
Normal 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);
|
||||
});
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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) */
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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')) {
|
||||
|
|
|
@ -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),
|
||||
|
|
35
yarn.lock
35
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"
|
||||
|
|
Loading…
Reference in a new issue