0
Fork 0
mirror of https://github.com/withastro/astro.git synced 2024-12-23 21:53:55 -05:00

fix: better dev routing with base using middleware (#3942)

This commit is contained in:
Allan Chain 2022-07-22 01:37:26 +08:00 committed by GitHub
parent ef9c4152b2
commit 21462feb4a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 157 additions and 42 deletions

View file

@ -0,0 +1,5 @@
---
'astro': patch
---
Use a base middleware for better base path handling in dev.

View file

@ -114,38 +114,14 @@ async function handle404Response(
req: http.IncomingMessage, req: http.IncomingMessage,
res: http.ServerResponse res: http.ServerResponse
) { ) {
const site = config.site ? new URL(config.base, config.site) : undefined;
const devRoot = site ? site.pathname : '/';
const pathname = decodeURI(new URL(origin + req.url).pathname); const pathname = decodeURI(new URL(origin + req.url).pathname);
let html = '';
if (pathname === '/' && !pathname.startsWith(devRoot)) {
html = subpathNotUsedTemplate(devRoot, pathname);
} else {
// HACK: redirect without the base path for assets in publicDir
const redirectTo =
req.method === 'GET' &&
config.base !== '/' &&
pathname.startsWith(config.base) &&
pathname.replace(config.base, '/');
if (redirectTo && redirectTo !== '/') { const html = notFoundTemplate({
const response = new Response(null, { statusCode: 404,
status: 302, title: 'Not found',
headers: { tabTitle: '404: Not Found',
Location: redirectTo, pathname,
}, });
});
await writeWebResponse(res, response);
return;
}
html = notFoundTemplate({
statusCode: 404,
title: 'Not found',
tabTitle: '404: Not Found',
pathname,
});
}
writeHtmlResponse(res, 404, html); writeHtmlResponse(res, 404, html);
} }
@ -179,6 +155,44 @@ function log404(logging: LogOptions, pathname: string) {
info(logging, 'serve', msg.req({ url: pathname, statusCode: 404 })); info(logging, 'serve', msg.req({ url: pathname, statusCode: 404 }));
} }
export function baseMiddleware(
config: AstroConfig,
logging: LogOptions
): vite.Connect.NextHandleFunction {
const site = config.site ? new URL(config.base, config.site) : undefined;
const devRoot = site ? site.pathname : '/';
return function devBaseMiddleware(req, res, next) {
const url = req.url!;
const pathname = decodeURI(new URL(url, 'http://vitejs.dev').pathname);
if (pathname.startsWith(devRoot)) {
req.url = url.replace(devRoot, '/');
return next();
}
if (pathname === '/' || pathname === '/index.html') {
log404(logging, pathname);
const html = subpathNotUsedTemplate(devRoot, pathname);
return writeHtmlResponse(res, 404, html);
}
if (req.headers.accept?.includes('text/html')) {
log404(logging, pathname);
const html = notFoundTemplate({
statusCode: 404,
title: 'Not found',
tabTitle: '404: Not Found',
pathname,
});
return writeHtmlResponse(res, 404, html);
}
next();
};
}
/** The main logic to route dev server requests to pages in Astro. */ /** The main logic to route dev server requests to pages in Astro. */
async function handleRequest( async function handleRequest(
routeCache: RouteCache, routeCache: RouteCache,
@ -190,8 +204,6 @@ async function handleRequest(
res: http.ServerResponse res: http.ServerResponse
) { ) {
const reqStart = performance.now(); const reqStart = performance.now();
const site = config.site ? new URL(config.base, config.site) : undefined;
const devRoot = site ? site.pathname : '/';
const origin = `${viteServer.config.server.https ? 'https' : 'http'}://${req.headers.host}`; const origin = `${viteServer.config.server.https ? 'https' : 'http'}://${req.headers.host}`;
const buildingToSSR = isBuildingToSSR(config); const buildingToSSR = isBuildingToSSR(config);
// Ignore `.html` extensions and `index.html` in request URLS to ensure that // Ignore `.html` extensions and `index.html` in request URLS to ensure that
@ -199,10 +211,12 @@ async function handleRequest(
// build formats, and is necessary based on how the manifest tracks build targets. // build formats, and is necessary based on how the manifest tracks build targets.
const url = new URL(origin + req.url?.replace(/(index)?\.html$/, '')); const url = new URL(origin + req.url?.replace(/(index)?\.html$/, ''));
const pathname = decodeURI(url.pathname); const pathname = decodeURI(url.pathname);
const rootRelativeUrl = pathname.substring(devRoot.length - 1);
// Add config.base back to url before passing it to SSR
url.pathname = config.base.substring(0, config.base.length - 1) + url.pathname;
// HACK! @astrojs/image uses query params for the injected route in `dev` // HACK! @astrojs/image uses query params for the injected route in `dev`
if (!buildingToSSR && rootRelativeUrl !== '/_image') { if (!buildingToSSR && pathname !== '/_image') {
// Prevent user from depending on search params when not doing SSR. // Prevent user from depending on search params when not doing SSR.
// NOTE: Create an array copy here because deleting-while-iterating // NOTE: Create an array copy here because deleting-while-iterating
// creates bugs where not all search params are removed. // creates bugs where not all search params are removed.
@ -236,13 +250,9 @@ async function handleRequest(
let filePath: URL | undefined; let filePath: URL | undefined;
try { try {
if (!pathname.startsWith(devRoot)) {
log404(logging, pathname);
return handle404Response(origin, config, req, res);
}
// Attempt to match the URL to a valid page route. // Attempt to match the URL to a valid page route.
// If that fails, switch the response to a 404 response. // If that fails, switch the response to a 404 response.
let route = matchRoute(rootRelativeUrl, manifest); let route = matchRoute(pathname, manifest);
const statusCode = route ? 200 : 404; const statusCode = route ? 200 : 404;
if (!route) { if (!route) {
@ -264,7 +274,7 @@ async function handleRequest(
mod, mod,
route, route,
routeCache, routeCache,
pathname: rootRelativeUrl, pathname: pathname,
logging, logging,
ssr: isBuildingToSSR(config), ssr: isBuildingToSSR(config),
}); });
@ -289,7 +299,7 @@ async function handleRequest(
logging, logging,
mode: 'development', mode: 'development',
origin, origin,
pathname: rootRelativeUrl, pathname: pathname,
request, request,
route: routeCustom404, route: routeCustom404,
routeCache, routeCache,
@ -307,7 +317,7 @@ async function handleRequest(
logging, logging,
mode: 'development', mode: 'development',
origin, origin,
pathname: rootRelativeUrl, pathname: pathname,
route, route,
routeCache, routeCache,
viteServer, viteServer,
@ -390,6 +400,12 @@ export default function createPlugin({ config, logging }: AstroPluginOptions): v
route: '', route: '',
handle: forceTextCSSForStylesMiddleware, handle: forceTextCSSForStylesMiddleware,
}); });
if (config.base !== '/') {
viteServer.middlewares.stack.unshift({
route: '',
handle: baseMiddleware(config, logging),
});
}
viteServer.middlewares.use(async (req, res) => { viteServer.middlewares.use(async (req, res) => {
if (!req.url || !req.method) { if (!req.url || !req.method) {
throw new Error('Incomplete request'); throw new Error('Incomplete request');

View file

@ -0,0 +1,8 @@
{
"name": "@test/public-base-404",
"version": "0.0.0",
"private": true,
"dependencies": {
"astro": "workspace:*"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 457 B

View file

@ -0,0 +1,8 @@
<html lang="en">
<head>
<title>Not Found</title>
</head>
<body>
<h1>404</h1>
</body>
</html>

View file

@ -0,0 +1,8 @@
<html lang="en">
<head>
<title>This Site</title>
</head>
<body>
<img src="/twitter.png" />
</body>
</html>

View file

@ -0,0 +1,64 @@
import { expect } from 'chai';
import * as cheerio from 'cheerio';
import { loadFixture } from './test-utils.js';
describe('Public dev with base', () => {
/** @type {import('./test-utils').Fixture} */
let fixture;
/** @type {import('./test-utils').DevServer} */
let devServer;
let $;
before(async () => {
fixture = await loadFixture({
root: './fixtures/public-base-404/',
site: 'http://example.com/',
base: '/blog'
});
devServer = await fixture.startDevServer();
});
it('200 when loading /@vite/client', async () => {
const response = await fixture.fetch('/@vite/client', {
redirect: 'manual'
});
expect(response.status).to.equal(200);
const content = await response.text()
expect(content).to.contain('vite')
});
it('200 when loading /blog/twitter.png', async () => {
const response = await fixture.fetch('/blog/twitter.png', {
redirect: 'manual'
});
expect(response.status).to.equal(200);
});
it('custom 404 page when loading /blog/blog/', async () => {
const response = await fixture.fetch('/blog/blog/');
const html = await response.text()
$ = cheerio.load(html);
expect($('h1').text()).to.equal('404');
});
it('default 404 hint page when loading /', async () => {
const response = await fixture.fetch('/');
expect(response.status).to.equal(404);
const html = await response.text()
$ = cheerio.load(html);
expect($('a').first().text()).to.equal('/blog/');
});
it('default 404 page when loading /none/', async () => {
const response = await fixture.fetch('/none/', {
headers: {
accept: 'text/html,*/*'
}
});
expect(response.status).to.equal(404);
const html = await response.text()
$ = cheerio.load(html);
expect($('h1').text()).to.equal('404: Not found');
expect($('pre').text()).to.equal('Path: /none/');
});
});

View file

@ -1618,6 +1618,12 @@ importers:
'@astrojs/preact': link:../../../../integrations/preact '@astrojs/preact': link:../../../../integrations/preact
astro: link:../../.. astro: link:../../..
packages/astro/test/fixtures/public-base-404:
specifiers:
astro: workspace:*
dependencies:
astro: link:../../..
packages/astro/test/fixtures/react-component: packages/astro/test/fixtures/react-component:
specifiers: specifiers:
'@astrojs/react': workspace:* '@astrojs/react': workspace:*