diff --git a/.changeset/kind-icons-destroy.md b/.changeset/kind-icons-destroy.md new file mode 100644 index 000000000..150c4dbf6 --- /dev/null +++ b/.changeset/kind-icons-destroy.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Properly support trailingSlash: never with a base diff --git a/packages/astro/src/core/config/schema.ts b/packages/astro/src/core/config/schema.ts index f9cddbf40..cf4f9a412 100644 --- a/packages/astro/src/core/config/schema.ts +++ b/packages/astro/src/core/config/schema.ts @@ -77,8 +77,7 @@ export const AstroConfigSchema = z.object({ base: z .string() .optional() - .default(ASTRO_CONFIG_DEFAULTS.base) - .transform((val) => prependForwardSlash(appendForwardSlash(trimSlashes(val)))), + .default(ASTRO_CONFIG_DEFAULTS.base), trailingSlash: z .union([z.literal('always'), z.literal('never'), z.literal('ignore')]) .optional() @@ -326,6 +325,12 @@ export function createRelativeSchema(cmd: string, fileProtocolRoot: URL) { ) { config.build.client = new URL('./dist/client/', config.outDir); } + const trimmedBase = trimSlashes(config.base); + if(trimmedBase.length && config.trailingSlash === 'never') { + config.base = prependForwardSlash(trimmedBase); + } else { + config.base = prependForwardSlash(appendForwardSlash(trimmedBase)); + } return config; }); diff --git a/packages/astro/src/core/routing/manifest/create.ts b/packages/astro/src/core/routing/manifest/create.ts index 3a995b43b..52dbef8f0 100644 --- a/packages/astro/src/core/routing/manifest/create.ts +++ b/packages/astro/src/core/routing/manifest/create.ts @@ -61,7 +61,7 @@ function getParts(part: string, file: string) { return result; } -function getPattern(segments: RoutePart[][], addTrailingSlash: AstroConfig['trailingSlash']) { +function getPattern(segments: RoutePart[][], base: string, addTrailingSlash: AstroConfig['trailingSlash']) { const pathname = segments .map((segment) => { if (segment.length === 1 && segment[0].spread) { @@ -93,7 +93,11 @@ function getPattern(segments: RoutePart[][], addTrailingSlash: AstroConfig['trai const trailing = addTrailingSlash && segments.length ? getTrailingSlashPattern(addTrailingSlash) : '$'; - return new RegExp(`^${pathname || '\\/'}${trailing}`); + let initial = '\\/'; + if(addTrailingSlash === 'never' && base !== '/') { + initial = ''; + } + return new RegExp(`^${pathname || initial}${trailing}`); } function getTrailingSlashPattern(addTrailingSlash: AstroConfig['trailingSlash']): string { @@ -306,7 +310,7 @@ export function createRouteManifest( components.push(item.file); const component = item.file; const trailingSlash = item.isPage ? settings.config.trailingSlash : 'never'; - const pattern = getPattern(segments, trailingSlash); + const pattern = getPattern(segments, settings.config.base, trailingSlash); const generate = getRouteGenerator(segments, trailingSlash); const pathname = segments.every((segment) => segment.length === 1 && !segment[0].dynamic) ? `/${segments.map((segment) => segment[0].content).join('/')}` @@ -367,7 +371,7 @@ export function createRouteManifest( const isPage = type === 'page'; const trailingSlash = isPage ? config.trailingSlash : 'never'; - const pattern = getPattern(segments, trailingSlash); + const pattern = getPattern(segments, settings.config.base, trailingSlash); const generate = getRouteGenerator(segments, trailingSlash); const pathname = segments.every((segment) => segment.length === 1 && !segment[0].dynamic) ? `/${segments.map((segment) => segment[0].content).join('/')}` diff --git a/packages/astro/src/vite-plugin-astro-server/base.ts b/packages/astro/src/vite-plugin-astro-server/base.ts index 7be3acb9f..db7af73cb 100644 --- a/packages/astro/src/vite-plugin-astro-server/base.ts +++ b/packages/astro/src/vite-plugin-astro-server/base.ts @@ -15,6 +15,7 @@ export function baseMiddleware( const site = config.site ? new URL(config.base, config.site) : undefined; const devRootURL = new URL(config.base, 'http://localhost'); const devRoot = site ? site.pathname : devRootURL.pathname; + const devRootReplacement = devRoot.endsWith('/') ? '/' : ''; return function devBaseMiddleware(req, res, next) { const url = req.url!; @@ -22,7 +23,7 @@ export function baseMiddleware( const pathname = decodeURI(new URL(url, 'http://localhost').pathname); if (pathname.startsWith(devRoot)) { - req.url = url.replace(devRoot, '/'); + req.url = url.replace(devRoot, devRootReplacement); return next(); } diff --git a/packages/astro/src/vite-plugin-astro-server/plugin.ts b/packages/astro/src/vite-plugin-astro-server/plugin.ts index e0cf2bcb8..589a74e74 100644 --- a/packages/astro/src/vite-plugin-astro-server/plugin.ts +++ b/packages/astro/src/vite-plugin-astro-server/plugin.ts @@ -50,8 +50,10 @@ export default function createVitePluginAstroServer({ }); } viteServer.middlewares.use(async (req, res) => { - if (!req.url || !req.method) { - throw new Error('Incomplete request'); + if (req.url === undefined || !req.method) { + res.writeHead(500, 'Incomplete request'); + res.end(); + return; } handleRequest(env, manifest, serverController, req, res); }); diff --git a/packages/astro/src/vite-plugin-astro-server/request.ts b/packages/astro/src/vite-plugin-astro-server/request.ts index 29964f031..b0480f98f 100644 --- a/packages/astro/src/vite-plugin-astro-server/request.ts +++ b/packages/astro/src/vite-plugin-astro-server/request.ts @@ -7,6 +7,7 @@ import { collectErrorMetadata } from '../core/errors/dev/index.js'; import { createSafeError } from '../core/errors/index.js'; import { error } from '../core/logger/core.js'; import * as msg from '../core/messages.js'; +import { removeTrailingForwardSlash } from '../core/path.js'; import { runWithErrorHandling } from './controller.js'; import { handle500Response } from './response.js'; import { handleRoute, matchRoute } from './route.js'; @@ -23,11 +24,17 @@ export async function handleRequest( const { config } = settings; const origin = `${moduleLoader.isHttps() ? 'https' : 'http'}://${req.headers.host}`; const buildingToSSR = config.output === 'server'; + const url = new URL(origin + req.url); - const pathname = decodeURI(url.pathname); + let pathname: string; + if(config.trailingSlash === 'never' && !req.url) { + pathname = ''; + } else { + pathname = decodeURI(url.pathname); + } // Add config.base back to url before passing it to SSR - url.pathname = config.base.substring(0, config.base.length - 1) + url.pathname; + url.pathname = removeTrailingForwardSlash(config.base) + url.pathname; // HACK! @astrojs/image uses query params for the injected route in `dev` if (!buildingToSSR && pathname !== '/_image') { diff --git a/packages/astro/test/astro-global.test.js b/packages/astro/test/astro-global.test.js index b8aa3ddb0..d49868584 100644 --- a/packages/astro/test/astro-global.test.js +++ b/packages/astro/test/astro-global.test.js @@ -25,7 +25,10 @@ describe('Astro Global', () => { }); it('Astro.request.url', async () => { - const html = await fixture.fetch('/blog/?foo=42').then((res) => res.text()); + const res = await await fixture.fetch('/blog/?foo=42'); + expect(res.status).to.equal(200); + + const html = await res.text(); const $ = cheerio.load(html); expect($('#pathname').text()).to.equal('/blog/'); expect($('#searchparams').text()).to.equal('{}'); diff --git a/packages/astro/test/units/dev/base.test.js b/packages/astro/test/units/dev/base.test.js new file mode 100644 index 000000000..503b41002 --- /dev/null +++ b/packages/astro/test/units/dev/base.test.js @@ -0,0 +1,106 @@ +import { expect } from 'chai'; + +import { runInContainer } from '../../../dist/core/dev/index.js'; +import { createFs, createRequestAndResponse } from '../test-utils.js'; + +const root = new URL('../../fixtures/alias/', import.meta.url); + +describe('base configuration', () => { + describe('with trailingSlash: "never"', () => { + describe('index route', () => { + it('Requests that include a trailing slash 404', async () => { + const fs = createFs({ + '/src/pages/index.astro': `