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:
parent
ef9c4152b2
commit
21462feb4a
8 changed files with 157 additions and 42 deletions
5
.changeset/dull-eagles-beg.md
Normal file
5
.changeset/dull-eagles-beg.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'astro': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Use a base middleware for better base path handling in dev.
|
|
@ -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');
|
||||||
|
|
8
packages/astro/test/fixtures/public-base-404/package.json
vendored
Normal file
8
packages/astro/test/fixtures/public-base-404/package.json
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"name": "@test/public-base-404",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"astro": "workspace:*"
|
||||||
|
}
|
||||||
|
}
|
BIN
packages/astro/test/fixtures/public-base-404/public/twitter.png
vendored
Normal file
BIN
packages/astro/test/fixtures/public-base-404/public/twitter.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 457 B |
8
packages/astro/test/fixtures/public-base-404/src/pages/404.astro
vendored
Normal file
8
packages/astro/test/fixtures/public-base-404/src/pages/404.astro
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>Not Found</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>404</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
8
packages/astro/test/fixtures/public-base-404/src/pages/index.astro
vendored
Normal file
8
packages/astro/test/fixtures/public-base-404/src/pages/index.astro
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>This Site</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<img src="/twitter.png" />
|
||||||
|
</body>
|
||||||
|
</html>
|
64
packages/astro/test/public-base-404.test.js
Normal file
64
packages/astro/test/public-base-404.test.js
Normal 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/');
|
||||||
|
});
|
||||||
|
});
|
|
@ -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:*
|
||||||
|
|
Loading…
Reference in a new issue