mirror of
https://github.com/withastro/astro.git
synced 2025-03-10 23:01:26 -05:00
feat(i18n): apply specific routing logic only to pages (#9091)
This commit is contained in:
parent
60e8210b0c
commit
536c6c9fd3
8 changed files with 115 additions and 6 deletions
5
.changeset/empty-turtles-wave.md
Normal file
5
.changeset/empty-turtles-wave.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'astro': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
The `routingStrategy` `prefix-always` should not force its logic to endpoints. This fixes some regression with `astro:assets` and `@astrojs/rss`.
|
|
@ -6,7 +6,7 @@ import type {
|
||||||
SSRElement,
|
SSRElement,
|
||||||
SSRManifest,
|
SSRManifest,
|
||||||
} from '../../@types/astro.js';
|
} from '../../@types/astro.js';
|
||||||
import { createI18nMiddleware } from '../../i18n/middleware.js';
|
import { createI18nMiddleware, i18nPipelineHook } from '../../i18n/middleware.js';
|
||||||
import type { SinglePageBuiltModule } from '../build/types.js';
|
import type { SinglePageBuiltModule } from '../build/types.js';
|
||||||
import { getSetCookiesFromResponse } from '../cookies/index.js';
|
import { getSetCookiesFromResponse } from '../cookies/index.js';
|
||||||
import { consoleLogDestination } from '../logger/console.js';
|
import { consoleLogDestination } from '../logger/console.js';
|
||||||
|
@ -179,6 +179,7 @@ export class App {
|
||||||
} else {
|
} else {
|
||||||
this.#pipeline.setMiddlewareFunction(i18nMiddleware);
|
this.#pipeline.setMiddlewareFunction(i18nMiddleware);
|
||||||
}
|
}
|
||||||
|
this.#pipeline.onBeforeRenderRoute(i18nPipelineHook);
|
||||||
} else {
|
} else {
|
||||||
if (mod.onRequest) {
|
if (mod.onRequest) {
|
||||||
this.#pipeline.setMiddlewareFunction(mod.onRequest as MiddlewareEndpointHandler);
|
this.#pipeline.setMiddlewareFunction(mod.onRequest as MiddlewareEndpointHandler);
|
||||||
|
|
|
@ -30,7 +30,7 @@ import {
|
||||||
removeLeadingForwardSlash,
|
removeLeadingForwardSlash,
|
||||||
removeTrailingForwardSlash,
|
removeTrailingForwardSlash,
|
||||||
} from '../../core/path.js';
|
} from '../../core/path.js';
|
||||||
import { createI18nMiddleware } from '../../i18n/middleware.js';
|
import { createI18nMiddleware, i18nPipelineHook } from '../../i18n/middleware.js';
|
||||||
import { runHookBuildGenerated } from '../../integrations/index.js';
|
import { runHookBuildGenerated } from '../../integrations/index.js';
|
||||||
import { getOutputDirectory, isServerLikeOutput } from '../../prerender/utils.js';
|
import { getOutputDirectory, isServerLikeOutput } from '../../prerender/utils.js';
|
||||||
import { PAGE_SCRIPT_ID } from '../../vite-plugin-scripts/index.js';
|
import { PAGE_SCRIPT_ID } from '../../vite-plugin-scripts/index.js';
|
||||||
|
@ -289,6 +289,7 @@ async function generatePage(
|
||||||
} else {
|
} else {
|
||||||
pipeline.setMiddlewareFunction(i18nMiddleware);
|
pipeline.setMiddlewareFunction(i18nMiddleware);
|
||||||
}
|
}
|
||||||
|
pipeline.onBeforeRenderRoute(i18nPipelineHook);
|
||||||
} else if (onRequest) {
|
} else if (onRequest) {
|
||||||
pipeline.setMiddlewareFunction(onRequest as MiddlewareEndpointHandler);
|
pipeline.setMiddlewareFunction(onRequest as MiddlewareEndpointHandler);
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,12 @@ type EndpointResultHandler = (
|
||||||
result: Response
|
result: Response
|
||||||
) => Promise<Response> | Response;
|
) => Promise<Response> | Response;
|
||||||
|
|
||||||
|
type PipelineHooks = {
|
||||||
|
before: PipelineHookFunction[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PipelineHookFunction = (ctx: RenderContext, mod: ComponentInstance | undefined) => void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is the basic class of a pipeline.
|
* This is the basic class of a pipeline.
|
||||||
*
|
*
|
||||||
|
@ -23,6 +29,9 @@ type EndpointResultHandler = (
|
||||||
export class Pipeline {
|
export class Pipeline {
|
||||||
env: Environment;
|
env: Environment;
|
||||||
#onRequest?: MiddlewareEndpointHandler;
|
#onRequest?: MiddlewareEndpointHandler;
|
||||||
|
#hooks: PipelineHooks = {
|
||||||
|
before: [],
|
||||||
|
};
|
||||||
/**
|
/**
|
||||||
* The handler accepts the *original* `Request` and result returned by the endpoint.
|
* The handler accepts the *original* `Request` and result returned by the endpoint.
|
||||||
* It must return a `Response`.
|
* It must return a `Response`.
|
||||||
|
@ -75,6 +84,9 @@ export class Pipeline {
|
||||||
renderContext: RenderContext,
|
renderContext: RenderContext,
|
||||||
componentInstance: ComponentInstance | undefined
|
componentInstance: ComponentInstance | undefined
|
||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
|
for (const hook of this.#hooks.before) {
|
||||||
|
hook(renderContext, componentInstance);
|
||||||
|
}
|
||||||
const result = await this.#tryRenderRoute(
|
const result = await this.#tryRenderRoute(
|
||||||
renderContext,
|
renderContext,
|
||||||
this.env,
|
this.env,
|
||||||
|
@ -158,4 +170,12 @@ export class Pipeline {
|
||||||
throw new Error(`Couldn't find route of type [${renderContext.route.type}]`);
|
throw new Error(`Couldn't find route of type [${renderContext.route.type}]`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store a function that will be called before starting the rendering phase.
|
||||||
|
* @param fn
|
||||||
|
*/
|
||||||
|
onBeforeRenderRoute(fn: PipelineHookFunction) {
|
||||||
|
this.#hooks.before.push(fn);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
import { appendForwardSlash, joinPaths } from '@astrojs/internal-helpers/path';
|
import { appendForwardSlash, joinPaths } from '@astrojs/internal-helpers/path';
|
||||||
import type { MiddlewareEndpointHandler, SSRManifest } from '../@types/astro.js';
|
import type { MiddlewareEndpointHandler, RouteData, SSRManifest } from '../@types/astro.js';
|
||||||
|
import type { RouteInfo } from '../core/app/types.js';
|
||||||
|
import type { PipelineHookFunction } from '../core/pipeline.js';
|
||||||
|
|
||||||
|
const routeDataSymbol = Symbol.for('astro.routeData');
|
||||||
|
|
||||||
// Checks if the pathname doesn't have any locale, exception for the defaultLocale, which is ignored on purpose
|
// Checks if the pathname doesn't have any locale, exception for the defaultLocale, which is ignored on purpose
|
||||||
function checkIsLocaleFree(pathname: string, locales: string[]): boolean {
|
function checkIsLocaleFree(pathname: string, locales: string[]): boolean {
|
||||||
|
@ -26,9 +30,19 @@ export function createI18nMiddleware(
|
||||||
return await next();
|
return await next();
|
||||||
}
|
}
|
||||||
|
|
||||||
const { locales, defaultLocale, fallback } = i18n;
|
const routeData = Reflect.get(context.request, routeDataSymbol);
|
||||||
const url = context.url;
|
if (routeData) {
|
||||||
|
// If the route we're processing is not a page, then we ignore it
|
||||||
|
if (
|
||||||
|
(routeData as RouteData).type !== 'page' &&
|
||||||
|
(routeData as RouteData).type !== 'fallback'
|
||||||
|
) {
|
||||||
|
return await next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = context.url;
|
||||||
|
const { locales, defaultLocale, fallback } = i18n;
|
||||||
const response = await next();
|
const response = await next();
|
||||||
|
|
||||||
if (response instanceof Response) {
|
if (response instanceof Response) {
|
||||||
|
@ -83,3 +97,10 @@ export function createI18nMiddleware(
|
||||||
return response;
|
return response;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This pipeline hook attaches a `RouteData` object to the `Request`
|
||||||
|
*/
|
||||||
|
export const i18nPipelineHook: PipelineHookFunction = (ctx) => {
|
||||||
|
Reflect.set(ctx.request, routeDataSymbol, ctx.route);
|
||||||
|
};
|
||||||
|
|
|
@ -20,7 +20,7 @@ import {
|
||||||
import { createRequest } from '../core/request.js';
|
import { createRequest } from '../core/request.js';
|
||||||
import { matchAllRoutes } from '../core/routing/index.js';
|
import { matchAllRoutes } from '../core/routing/index.js';
|
||||||
import { isPage, resolveIdToUrl } from '../core/util.js';
|
import { isPage, resolveIdToUrl } from '../core/util.js';
|
||||||
import { createI18nMiddleware } from '../i18n/middleware.js';
|
import { createI18nMiddleware, i18nPipelineHook } from '../i18n/middleware.js';
|
||||||
import { getSortedPreloadedMatches } from '../prerender/routing.js';
|
import { getSortedPreloadedMatches } from '../prerender/routing.js';
|
||||||
import { isServerLikeOutput } from '../prerender/utils.js';
|
import { isServerLikeOutput } from '../prerender/utils.js';
|
||||||
import { PAGE_SCRIPT_ID } from '../vite-plugin-scripts/index.js';
|
import { PAGE_SCRIPT_ID } from '../vite-plugin-scripts/index.js';
|
||||||
|
@ -289,6 +289,7 @@ export async function handleRoute({
|
||||||
} else {
|
} else {
|
||||||
pipeline.setMiddlewareFunction(i18Middleware);
|
pipeline.setMiddlewareFunction(i18Middleware);
|
||||||
}
|
}
|
||||||
|
pipeline.onBeforeRenderRoute(i18nPipelineHook);
|
||||||
} else if (onRequest) {
|
} else if (onRequest) {
|
||||||
pipeline.setMiddlewareFunction(onRequest);
|
pipeline.setMiddlewareFunction(onRequest);
|
||||||
}
|
}
|
||||||
|
|
7
packages/astro/test/fixtures/i18n-routing-prefix-always/src/pages/test.json.js
vendored
Normal file
7
packages/astro/test/fixtures/i18n-routing-prefix-always/src/pages/test.json.js
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
export const GET = () => {
|
||||||
|
return new Response(JSON.stringify({ lorem: 'ipsum' }), {
|
||||||
|
headers: {
|
||||||
|
'content-type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
|
@ -992,3 +992,56 @@ describe('[SSR] i18n routing', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('i18n routing does not break assets and endpoints', () => {
|
||||||
|
describe('assets', () => {
|
||||||
|
/** @type {import('./test-utils').Fixture} */
|
||||||
|
let fixture;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
fixture = await loadFixture({
|
||||||
|
root: './fixtures/core-image-base/',
|
||||||
|
experimental: {
|
||||||
|
i18n: {
|
||||||
|
defaultLocale: 'en',
|
||||||
|
locales: ['en', 'es'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
base: '/blog',
|
||||||
|
});
|
||||||
|
await fixture.build();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render the image', async () => {
|
||||||
|
const html = await fixture.readFile('/index.html');
|
||||||
|
const $ = cheerio.load(html);
|
||||||
|
const src = $('#local img').attr('src');
|
||||||
|
expect(src.length).to.be.greaterThan(0);
|
||||||
|
expect(src.startsWith('/blog')).to.be.true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('endpoint', () => {
|
||||||
|
/** @type {import('./test-utils').Fixture} */
|
||||||
|
let fixture;
|
||||||
|
|
||||||
|
let app;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
fixture = await loadFixture({
|
||||||
|
root: './fixtures/i18n-routing-prefix-always/',
|
||||||
|
output: 'server',
|
||||||
|
adapter: testAdapter(),
|
||||||
|
});
|
||||||
|
await fixture.build();
|
||||||
|
app = await fixture.loadTestAdapterApp();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the expected data', async () => {
|
||||||
|
let request = new Request('http://example.com/new-site/test.json');
|
||||||
|
let response = await app.render(request);
|
||||||
|
expect(response.status).to.equal(200);
|
||||||
|
expect(await response.text()).includes('lorem');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
Loading…
Add table
Reference in a new issue