Fix Astro Preview Pathing Issues (#2338)
* Fix preview issues triggered by pageUrlFormat & trailingSlash options * format * changeset * nit: fix host message
This commit is contained in:
parent
cbac041442
commit
c0cb7eead5
5 changed files with 152 additions and 146 deletions
5
.changeset/spotty-turtles-complain.md
Normal file
5
.changeset/spotty-turtles-complain.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'astro': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix preview issues triggered by pageUrlFormat & trailingSlash options
|
|
@ -17,45 +17,45 @@ interface ErrorTemplateOptions {
|
||||||
/** Display all errors */
|
/** Display all errors */
|
||||||
export default function template({ title, pathname, statusCode = 404, tabTitle, body }: ErrorTemplateOptions): string {
|
export default function template({ title, pathname, statusCode = 404, tabTitle, body }: ErrorTemplateOptions): string {
|
||||||
return `<!doctype html>
|
return `<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>${tabTitle}</title>
|
<title>${tabTitle}</title>
|
||||||
<style>
|
<style>
|
||||||
${baseCSS}
|
${baseCSS}
|
||||||
|
|
||||||
.center {
|
.center {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
}
|
}
|
||||||
|
|
||||||
.statusCode {
|
.statusCode {
|
||||||
color: var(--orange);
|
color: var(--orange);
|
||||||
}
|
}
|
||||||
|
|
||||||
.astro {
|
.astro {
|
||||||
height: 120px;
|
height: 120px;
|
||||||
width: 120px;
|
width: 120px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main class="center">
|
<main class="center">
|
||||||
<svg class="astro" viewBox="0 0 256 256" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M163.008 18.929c1.944 2.413 2.935 5.67 4.917 12.181l43.309 142.27a180.277 180.277 0 00-51.778-17.53l-28.198-95.29a3.67 3.67 0 00-7.042.01l-27.857 95.232a180.225 180.225 0 00-52.01 17.557l43.52-142.281c1.99-6.502 2.983-9.752 4.927-12.16a15.999 15.999 0 016.484-4.798c2.872-1.154 6.271-1.154 13.07-1.154h31.085c6.807 0 10.211 0 13.086 1.157a16.004 16.004 0 016.487 4.806z" fill="white"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M168.19 180.151c-7.139 6.105-21.39 10.268-37.804 10.268-20.147 0-37.033-6.272-41.513-14.707-1.602 4.835-1.961 10.367-1.961 13.902 0 0-1.056 17.355 11.015 29.426 0-6.268 5.081-11.349 11.349-11.349 10.743 0 10.731 9.373 10.721 16.977v.679c0 11.542 7.054 21.436 17.086 25.606a23.27 23.27 0 01-2.339-10.2c0-11.008 6.463-15.107 13.974-19.87 5.976-3.79 12.616-8.001 17.192-16.449a31.024 31.024 0 003.743-14.82c0-3.299-.513-6.479-1.463-9.463z" fill="#ff5d01"></path></svg>
|
<svg class="astro" viewBox="0 0 256 256" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M163.008 18.929c1.944 2.413 2.935 5.67 4.917 12.181l43.309 142.27a180.277 180.277 0 00-51.778-17.53l-28.198-95.29a3.67 3.67 0 00-7.042.01l-27.857 95.232a180.225 180.225 0 00-52.01 17.557l43.52-142.281c1.99-6.502 2.983-9.752 4.927-12.16a15.999 15.999 0 016.484-4.798c2.872-1.154 6.271-1.154 13.07-1.154h31.085c6.807 0 10.211 0 13.086 1.157a16.004 16.004 0 016.487 4.806z" fill="white"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M168.19 180.151c-7.139 6.105-21.39 10.268-37.804 10.268-20.147 0-37.033-6.272-41.513-14.707-1.602 4.835-1.961 10.367-1.961 13.902 0 0-1.056 17.355 11.015 29.426 0-6.268 5.081-11.349 11.349-11.349 10.743 0 10.731 9.373 10.721 16.977v.679c0 11.542 7.054 21.436 17.086 25.606a23.27 23.27 0 01-2.339-10.2c0-11.008 6.463-15.107 13.974-19.87 5.976-3.79 12.616-8.001 17.192-16.449a31.024 31.024 0 003.743-14.82c0-3.299-.513-6.479-1.463-9.463z" fill="#ff5d01"></path></svg>
|
||||||
<h1>${statusCode ? `<span class="statusCode">${statusCode}: </span> ` : ''}<span class="statusMessage">${title}</span></h1>
|
<h1>${statusCode ? `<span class="statusCode">${statusCode}: </span> ` : ''}<span class="statusMessage">${title}</span></h1>
|
||||||
${
|
${
|
||||||
body ||
|
body ||
|
||||||
`
|
`
|
||||||
<pre>Path: ${encode(pathname)}</pre>
|
<pre>Path: ${encode(pathname)}</pre>
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
</html>`;
|
</html>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function subpathNotUsedTemplate(base: string, pathname: string) {
|
export function subpathNotUsedTemplate(base: string, pathname: string) {
|
||||||
|
@ -64,9 +64,16 @@ export function subpathNotUsedTemplate(base: string, pathname: string) {
|
||||||
statusCode: 404,
|
statusCode: 404,
|
||||||
title: 'Not found',
|
title: 'Not found',
|
||||||
tabTitle: '404: Not Found',
|
tabTitle: '404: Not Found',
|
||||||
body: `
|
body: `<p>In your <code>buildOptions.site</code> you have your base path set to <a href="${base}">${base}</a>. Do you want to go there instead?</p>
|
||||||
<p>In your <code>buildOptions.site</code> you have your base path set to <a href="${base}">${base}</a>. Do you want to go there instead?</p>
|
<p>Come to our <a href="https://astro.build/chat">Discord</a> if you need help.</p>`,
|
||||||
<p>Come to our <a href="https://astro.build/chat">Discord</a> if you need help.</p>
|
});
|
||||||
`,
|
}
|
||||||
|
|
||||||
|
export function notFoundTemplate(pathname: string, message = 'Not found') {
|
||||||
|
return template({
|
||||||
|
pathname,
|
||||||
|
statusCode: 404,
|
||||||
|
title: message,
|
||||||
|
tabTitle: `404: ${message}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,3 +34,7 @@ export function prependDotSlash(path: string) {
|
||||||
|
|
||||||
return './' + path;
|
return './' + path;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function trimSlashes(path: string) {
|
||||||
|
return path.replace(/^\/|\/$/g, '');
|
||||||
|
}
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
import type { AstroConfig } from '../../@types/astro';
|
import type { AstroConfig } from '../../@types/astro';
|
||||||
import type { LogOptions } from '../logger';
|
import type { LogOptions } from '../logger';
|
||||||
|
import type { Stats } from 'fs';
|
||||||
|
|
||||||
import http from 'http';
|
import http from 'http';
|
||||||
import { performance } from 'perf_hooks';
|
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 * as msg from '../dev/messages.js';
|
import * as msg from '../dev/messages.js';
|
||||||
import { error, info } from '../logger.js';
|
import { error, info } from '../logger.js';
|
||||||
import { subpathNotUsedTemplate, default as template } from '../dev/template/4xx.js';
|
import { subpathNotUsedTemplate, notFoundTemplate, default as template } from '../dev/template/4xx.js';
|
||||||
import { prependForwardSlash } from '../path.js';
|
import { appendForwardSlash, trimSlashes } from '../path.js';
|
||||||
import * as npath from 'path';
|
|
||||||
import * as fs from 'fs';
|
|
||||||
|
|
||||||
interface PreviewOptions {
|
interface PreviewOptions {
|
||||||
logging: LogOptions;
|
logging: LogOptions;
|
||||||
|
@ -23,100 +23,94 @@ export interface PreviewServer {
|
||||||
stop(): Promise<void>;
|
stop(): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
type SendStreamWithPath = send.SendStream & { path: string };
|
|
||||||
|
|
||||||
function removeBase(base: string, pathname: string) {
|
|
||||||
if (base === pathname) {
|
|
||||||
return '/';
|
|
||||||
}
|
|
||||||
let requrl = pathname.substr(base.length);
|
|
||||||
return prependForwardSlash(requrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The primary dev action */
|
/** The primary dev action */
|
||||||
export default async function preview(config: AstroConfig, { logging }: PreviewOptions): Promise<PreviewServer> {
|
export default async function preview(config: AstroConfig, { logging }: PreviewOptions): Promise<PreviewServer> {
|
||||||
const startServerTime = performance.now();
|
const startServerTime = performance.now();
|
||||||
const base = config.buildOptions.site ? new URL(config.buildOptions.site).pathname : '/';
|
const pageUrlFormat = config.buildOptions.pageUrlFormat;
|
||||||
|
const trailingSlash = config.devOptions.trailingSlash;
|
||||||
|
const forceTrailingSlash = trailingSlash === 'always';
|
||||||
|
const blockTrailingSlash = trailingSlash === 'never';
|
||||||
|
|
||||||
|
/** Default file served from a directory. */
|
||||||
|
const defaultFile = 'index.html';
|
||||||
|
|
||||||
|
const defaultOrigin = 'http://localhost';
|
||||||
|
|
||||||
|
const sendOptions = {
|
||||||
|
extensions: pageUrlFormat === 'file' ? ['html'] : false,
|
||||||
|
index: false,
|
||||||
|
root: fileURLToPath(config.dist),
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Base request URL. */
|
||||||
|
let baseURL = new URL(appendForwardSlash(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) => {
|
||||||
if (!req.url!.startsWith(base)) {
|
const requestURL = new URL(req.url as string, defaultOrigin);
|
||||||
|
|
||||||
|
// respond 404 to requests outside the base request directory
|
||||||
|
if (!requestURL.pathname.startsWith(baseURL.pathname)) {
|
||||||
res.statusCode = 404;
|
res.statusCode = 404;
|
||||||
res.end(subpathNotUsedTemplate(base, req.url!));
|
res.end(subpathNotUsedTemplate(baseURL.pathname, requestURL.pathname));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (config.devOptions.trailingSlash) {
|
/** Relative request path. */
|
||||||
case 'always': {
|
const pathname = requestURL.pathname.slice(baseURL.pathname.length - 1);
|
||||||
if (!req.url?.endsWith('/')) {
|
|
||||||
res.statusCode = 404;
|
|
||||||
res.end(
|
|
||||||
template({
|
|
||||||
title: 'Not found',
|
|
||||||
tabTitle: 'Not found',
|
|
||||||
pathname: req.url!,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'never': {
|
|
||||||
if (req.url?.endsWith('/')) {
|
|
||||||
res.statusCode = 404;
|
|
||||||
res.end(
|
|
||||||
template({
|
|
||||||
title: 'Not found',
|
|
||||||
tabTitle: 'Not found',
|
|
||||||
pathname: req.url!,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'ignore': {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let sendpath = removeBase(base, req.url!);
|
const isRoot = pathname === '/';
|
||||||
const sendOptions: send.SendOptions = {
|
const hasTrailingSlash = isRoot || pathname.endsWith('/');
|
||||||
root: fileURLToPath(config.dist),
|
|
||||||
|
let tryTrailingSlash = true;
|
||||||
|
let tryHtmlExtension = true;
|
||||||
|
|
||||||
|
let url: URL;
|
||||||
|
|
||||||
|
const onErr = (message: string) => {
|
||||||
|
res.statusCode = 404;
|
||||||
|
res.end(notFoundTemplate(pathname, message));
|
||||||
};
|
};
|
||||||
if (config.buildOptions.pageUrlFormat === 'file' && !sendpath.endsWith('.html')) {
|
|
||||||
sendOptions.index = false;
|
const onStat = (err: NodeJS.ErrnoException | null, stat: Stats) => {
|
||||||
const parts = sendpath.split('/');
|
switch (true) {
|
||||||
let lastPart = parts.pop();
|
// retry nonexistent paths without an html extension
|
||||||
switch (config.devOptions.trailingSlash) {
|
case err && tryHtmlExtension && hasTrailingSlash && !blockTrailingSlash:
|
||||||
case 'always': {
|
case err && tryHtmlExtension && !hasTrailingSlash && !forceTrailingSlash && !pathname.endsWith('.html'):
|
||||||
lastPart = parts.pop();
|
tryHtmlExtension = false;
|
||||||
break;
|
return fs.stat((url = new URL(url.pathname + '.html', url)), onStat);
|
||||||
}
|
|
||||||
case 'never': {
|
// 404 on nonexistent paths (that are yet handled)
|
||||||
// lastPart is the actually last part like `page`
|
case err !== null:
|
||||||
break;
|
return onErr('Path not found');
|
||||||
}
|
|
||||||
case 'ignore': {
|
// 404 on directories when a trailing slash is present but blocked
|
||||||
// this could end in slash, so resolve either way
|
case stat.isDirectory() && hasTrailingSlash && blockTrailingSlash && !isRoot:
|
||||||
if (lastPart === '') {
|
return onErr('Prohibited trailing slash');
|
||||||
lastPart = parts.pop();
|
|
||||||
}
|
// 404 on directories when a trailing slash is missing but forced
|
||||||
break;
|
case stat.isDirectory() && !hasTrailingSlash && forceTrailingSlash && !isRoot:
|
||||||
}
|
return onErr('Required trailing slash');
|
||||||
|
|
||||||
|
// retry on directories when a default file is missing but allowed (that are yet handled)
|
||||||
|
case stat.isDirectory() && tryTrailingSlash:
|
||||||
|
tryTrailingSlash = false;
|
||||||
|
return fs.stat((url = new URL(url.pathname + (url.pathname.endsWith('/') ? defaultFile : '/' + defaultFile), url)), onStat);
|
||||||
|
|
||||||
|
// 404 on existent directories (that are yet handled)
|
||||||
|
case stat.isDirectory():
|
||||||
|
return onErr('Path not found');
|
||||||
|
|
||||||
|
// handle existent paths
|
||||||
|
default:
|
||||||
|
send(req, fileURLToPath(url), {
|
||||||
|
extensions: false,
|
||||||
|
index: false,
|
||||||
|
}).pipe(res);
|
||||||
}
|
}
|
||||||
const part = lastPart || 'index';
|
};
|
||||||
sendpath = npath.sep + npath.join(...parts, `${part}.html`);
|
|
||||||
}
|
fs.stat((url = new URL(trimSlashes(pathname), config.dist)), onStat);
|
||||||
send(req, sendpath, sendOptions)
|
|
||||||
.once('directory', function (this: SendStreamWithPath, _res, path) {
|
|
||||||
if (config.buildOptions.pageUrlFormat === 'directory' && !path.endsWith('index.html')) {
|
|
||||||
return this.sendIndex(path);
|
|
||||||
} else {
|
|
||||||
this.error(404);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.pipe(res);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let { hostname, port } = config.devOptions;
|
let { hostname, port } = config.devOptions;
|
||||||
|
@ -132,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}${base}` }));
|
info(logging, 'astro', msg.devHost({ host: `http://${hostname}:${port}${baseURL.pathname}` }));
|
||||||
}
|
}
|
||||||
showedListenMsg = true;
|
showedListenMsg = true;
|
||||||
resolve();
|
resolve();
|
||||||
|
|
|
@ -30,17 +30,17 @@ describe('Preview Routing', () => {
|
||||||
expect(response.status).to.equal(404);
|
expect(response.status).to.equal(404);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('404 when loading subpath root with trailing slash', async () => {
|
it('200 when loading subpath root with trailing slash', async () => {
|
||||||
const response = await fixture.fetch('/blog/');
|
const response = await fixture.fetch('/blog/');
|
||||||
expect(response.status).to.equal(404);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('200 when loading subpath root without trailing slash', async () => {
|
|
||||||
const response = await fixture.fetch('/blog');
|
|
||||||
expect(response.status).to.equal(200);
|
expect(response.status).to.equal(200);
|
||||||
expect(response.redirected).to.equal(false);
|
expect(response.redirected).to.equal(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('404 when loading subpath root without trailing slash', async () => {
|
||||||
|
const response = await fixture.fetch('/blog');
|
||||||
|
expect(response.status).to.equal(404);
|
||||||
|
});
|
||||||
|
|
||||||
it('404 when loading another page with subpath used', async () => {
|
it('404 when loading another page with subpath used', async () => {
|
||||||
const response = await fixture.fetch('/blog/another/');
|
const response = await fixture.fetch('/blog/another/');
|
||||||
expect(response.status).to.equal(404);
|
expect(response.status).to.equal(404);
|
||||||
|
@ -92,7 +92,6 @@ describe('Preview Routing', () => {
|
||||||
it('404 when loading subpath root without trailing slash', async () => {
|
it('404 when loading subpath root without trailing slash', async () => {
|
||||||
const response = await fixture.fetch('/blog');
|
const response = await fixture.fetch('/blog');
|
||||||
expect(response.status).to.equal(404);
|
expect(response.status).to.equal(404);
|
||||||
expect(response.redirected).to.equal(false);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('200 when loading another page with subpath used', async () => {
|
it('200 when loading another page with subpath used', async () => {
|
||||||
|
@ -148,10 +147,9 @@ describe('Preview Routing', () => {
|
||||||
expect(response.status).to.equal(200);
|
expect(response.status).to.equal(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('200 when loading subpath root without trailing slash', async () => {
|
it('404 when loading subpath root without trailing slash', async () => {
|
||||||
const response = await fixture.fetch('/blog');
|
const response = await fixture.fetch('/blog');
|
||||||
expect(response.status).to.equal(200);
|
expect(response.status).to.equal(404);
|
||||||
expect(response.redirected).to.equal(false);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('200 when loading another page with subpath used', async () => {
|
it('200 when loading another page with subpath used', async () => {
|
||||||
|
@ -207,17 +205,17 @@ describe('Preview Routing', () => {
|
||||||
expect(response.status).to.equal(404);
|
expect(response.status).to.equal(404);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('404 when loading subpath root with trailing slash', async () => {
|
it('200 when loading subpath root with trailing slash', async () => {
|
||||||
const response = await fixture.fetch('/blog/');
|
const response = await fixture.fetch('/blog/');
|
||||||
expect(response.status).to.equal(404);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('200 when loading subpath root without trailing slash', async () => {
|
|
||||||
const response = await fixture.fetch('/blog');
|
|
||||||
expect(response.status).to.equal(200);
|
expect(response.status).to.equal(200);
|
||||||
expect(response.redirected).to.equal(false);
|
expect(response.redirected).to.equal(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('404 when loading subpath root without trailing slash', async () => {
|
||||||
|
const response = await fixture.fetch('/blog');
|
||||||
|
expect(response.status).to.equal(404);
|
||||||
|
});
|
||||||
|
|
||||||
it('404 when loading another page with subpath used', async () => {
|
it('404 when loading another page with subpath used', async () => {
|
||||||
const response = await fixture.fetch('/blog/another/');
|
const response = await fixture.fetch('/blog/another/');
|
||||||
expect(response.status).to.equal(404);
|
expect(response.status).to.equal(404);
|
||||||
|
@ -272,7 +270,6 @@ describe('Preview Routing', () => {
|
||||||
it('404 when loading subpath root without trailing slash', async () => {
|
it('404 when loading subpath root without trailing slash', async () => {
|
||||||
const response = await fixture.fetch('/blog');
|
const response = await fixture.fetch('/blog');
|
||||||
expect(response.status).to.equal(404);
|
expect(response.status).to.equal(404);
|
||||||
expect(response.redirected).to.equal(false);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('200 when loading another page with subpath used', async () => {
|
it('200 when loading another page with subpath used', async () => {
|
||||||
|
@ -331,10 +328,9 @@ describe('Preview Routing', () => {
|
||||||
expect(response.status).to.equal(200);
|
expect(response.status).to.equal(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('200 when loading subpath root without trailing slash', async () => {
|
it('404 when loading subpath root without trailing slash', async () => {
|
||||||
const response = await fixture.fetch('/blog');
|
const response = await fixture.fetch('/blog');
|
||||||
expect(response.status).to.equal(200);
|
expect(response.status).to.equal(404);
|
||||||
expect(response.redirected).to.equal(false);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('200 when loading another page with subpath used', async () => {
|
it('200 when loading another page with subpath used', async () => {
|
||||||
|
|
Loading…
Reference in a new issue