Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/moody-turkeys-agree.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'astro': patch
---

Fixes an issue with the development server, where changes to the middleware weren't picked, and it required a full restart of the server.
9 changes: 9 additions & 0 deletions packages/astro/e2e/fixtures/hmr/src/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { defineMiddleware } from 'astro:middleware';

export const onRequest = defineMiddleware((_context, next) => {
const response = next();
response.then((res) => {
res.headers.set('x-test-middleware', 'before');
});
return response;
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<html>
<head>
<title>Middleware HMR Test</title>
</head>
<body>
<h1>Middleware HMR</h1>
</body>
</html>
20 changes: 20 additions & 0 deletions packages/astro/e2e/hmr.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,23 @@ test.describe('Styles', () => {
await expect(h).toHaveCSS('color', 'rgb(0, 0, 0)');
});
});

test.describe('Middleware', () => {
test('middleware changes are picked up without restart', async ({ astro }) => {
// Verify original middleware header
const res1 = await astro.fetch('/middleware-test');
expect(res1.headers.get('x-test-middleware')).toBe('before');

// Edit middleware to change the header value
await astro.editFile('./src/middleware.ts', (original) =>
original.replace("'before'", "'after'"),
);

// Wait briefly for HMR to propagate
await new Promise((resolve) => setTimeout(resolve, 1000));

// Verify the new header value is returned without server restart
const res2 = await astro.fetch('/middleware-test');
expect(res2.headers.get('x-test-middleware')).toBe('after');
});
});
8 changes: 8 additions & 0 deletions packages/astro/src/core/app/dev/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ export class DevApp extends BaseApp<NonRunnablePipeline> {
return true;
}

/**
* Clears the cached middleware so it is re-resolved on the next request.
* Called via HMR when middleware files change.
*/
clearMiddleware(): void {
this.pipeline.clearMiddleware();
}

/**
* Updates the routes list when files change during development.
* Called via HMR when new pages are added/removed.
Expand Down
7 changes: 7 additions & 0 deletions packages/astro/src/core/app/entrypoints/virtual/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ export const createApp: CreateApp = ({ streaming } = {}) => {
if (!currentDevApp) return;
currentDevApp.pipeline.routeCache.clearAll();
});

// Listen for middleware file changes via HMR.
// Clear the cached middleware so it is re-resolved on the next request.
import.meta.hot.on('astro:middleware-updated', () => {
if (!currentDevApp) return;
currentDevApp.clearMiddleware();
});
}

return currentDevApp;
Expand Down
8 changes: 8 additions & 0 deletions packages/astro/src/core/base-pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,14 @@ export abstract class Pipeline {
}
}

/**
* Clears the cached middleware so it is re-resolved on the next request.
* Called via HMR when middleware files change during development.
*/
clearMiddleware() {
this.resolvedMiddleware = undefined;
}

async getActions(): Promise<SSRActions> {
if (this.resolvedActions) {
return this.resolvedActions;
Expand Down
34 changes: 33 additions & 1 deletion packages/astro/src/core/middleware/vite-plugin.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import type { Plugin as VitePlugin } from 'vite';
import { fileURLToPath } from 'node:url';
import {
normalizePath as viteNormalizePath,
type ViteDevServer,
type Plugin as VitePlugin,
} from 'vite';
import { getServerOutputDirectory } from '../../prerender/utils.js';
import type { AstroSettings } from '../../types/astro.js';
import { addRollupInput } from '../build/add-rollup-input.js';
Expand All @@ -21,6 +26,8 @@ export function vitePluginMiddleware({ settings }: { settings: AstroSettings }):
settings.middlewares.pre.length > 0 || settings.middlewares.post.length > 0;
let userMiddlewareIsPresent = false;

const normalizedSrcDir = viteNormalizePath(fileURLToPath(settings.config.srcDir));

return {
name: '@astro/plugin-middleware',
applyToEnvironment(environment) {
Expand All @@ -30,6 +37,31 @@ export function vitePluginMiddleware({ settings }: { settings: AstroSettings }):
environment.name === ASTRO_VITE_ENVIRONMENT_NAMES.prerender
);
},
configureServer(server: ViteDevServer) {
server.watcher.on('change', (path) => {
const normalizedPath = viteNormalizePath(path);
// Check if the changed file is a middleware file under srcDir
if (!normalizedPath.startsWith(normalizedSrcDir)) return;
const relativePath = normalizedPath.slice(normalizedSrcDir.length);
// Dot ensures we match "middleware.ts" but not e.g. "middleware-utils.ts"
if (!relativePath.startsWith(`${MIDDLEWARE_PATH_SEGMENT_NAME}.`)) return;

for (const name of [
ASTRO_VITE_ENVIRONMENT_NAMES.ssr,
ASTRO_VITE_ENVIRONMENT_NAMES.astro,
] as const) {
const environment = server.environments[name];
if (!environment) continue;

const virtualMod = environment.moduleGraph.getModuleById(MIDDLEWARE_RESOLVED_MODULE_ID);
if (virtualMod) {
environment.moduleGraph.invalidateModule(virtualMod);
}

environment.hot.send('astro:middleware-updated', {});
}
});
},
resolveId: {
filter: {
id: new RegExp(`^${MIDDLEWARE_MODULE_ID}$`),
Expand Down
8 changes: 8 additions & 0 deletions packages/astro/src/vite-plugin-app/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,14 @@ export class AstroServerApp extends BaseApp<RunnablePipeline> {
this.pipeline.clearRouteCache();
}

/**
* Clears the cached middleware so it is re-resolved on the next request.
* Called via HMR when middleware files change.
*/
clearMiddleware(): void {
this.pipeline.clearMiddleware();
}
Comment thread
seroperson marked this conversation as resolved.

async devMatch(pathname: string): Promise<DevMatch | undefined> {
const matchedRoute = await matchRoute(
pathname,
Expand Down
7 changes: 7 additions & 0 deletions packages/astro/src/vite-plugin-app/createAstroServerApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,13 @@ export default async function createAstroServerApp(
app.clearRouteCache();
actualLogger.debug('router', 'Route cache cleared due to content change');
});

// Listen for middleware file changes via HMR.
// Clear the cached middleware so it is re-resolved on the next request.
import.meta.hot.on('astro:middleware-updated', () => {
app.clearMiddleware();
actualLogger.debug('router', 'Middleware cache cleared due to file change');
});
}

return {
Expand Down
Loading