diff --git a/docs/01-getting-started/01-installation.md b/docs/01-getting-started/01-installation.md index 827302826..d93f1476c 100644 --- a/docs/01-getting-started/01-installation.md +++ b/docs/01-getting-started/01-installation.md @@ -10,13 +10,12 @@ related: ### System Requirements -- Bun [1.0.4]( https://bun.sh/) or later +- Bun [1.0.4](https://bun.sh/) or later - macOS, Windows (including WSL), and Linux are supported. - ### Automatic Installation -We recommend starting a new Brisa app using `create-brisa-app``, which sets up everything automatically for you. +We recommend starting a new Brisa app using `create-brisa-app``, which sets up everything automatically for you. ```sh npx create-brisa-app @@ -55,9 +54,9 @@ You need to add the jsx-runtime of Brisa in your `tsconfig.json` file: ```json { "compilerOptions": { - // ...rest + // ...rest "jsx": "react-jsx", - "jsxImportSource": "brisa", + "jsxImportSource": "brisa" } } ``` @@ -70,7 +69,7 @@ Then, add an `index.tsx` file inside your `src/pages` folder. This will be your ```tsx export default function Page() { - return

Hello, Brisa!

+ return

Hello, Brisa!

; } ``` diff --git a/docs/01-getting-started/02-project-structure.md b/docs/01-getting-started/02-project-structure.md index 3b884c1ba..ab5febfba 100644 --- a/docs/01-getting-started/02-project-structure.md +++ b/docs/01-getting-started/02-project-structure.md @@ -8,33 +8,29 @@ This page provides an overview of the file and folder structure of a Brisa proje ## `src`-level folders -| | | -| ------------------------------------------------------------------------ | ---------------------------------- | -| [`pages`](/docs/building-your-application/routing#pages) | Pages Router | -| [`api`](/docs/building-your-application/routing#api) | Api Router | -| [`public`](/docs/building-your-application/optimizing/static-assets) | Static assets to be served | -| [`middleware`](/docs/building-your-application/configuring/middleware) | Middleware | -| [`layout`](/docs/building-your-application/configuring/layout) | Layout / Layouts | -| [`websocket`](/docs/building-your-application/configuring/websocket) | Websocket | -| [`i18n`](/docs/building-your-application/configuring/i18n) | Internationalization routing & translations | - - +| | | +| ---------------------------------------------------------------------- | ------------------------------------------- | +| [`pages`](/docs/building-your-application/routing#pages) | Pages Router | +| [`api`](/docs/building-your-application/routing#api) | Api Router | +| [`public`](/docs/building-your-application/optimizing/static-assets) | Static assets to be served | +| [`middleware`](/docs/building-your-application/configuring/middleware) | Middleware | +| [`layout`](/docs/building-your-application/configuring/layout) | Layout / Layouts | +| [`websocket`](/docs/building-your-application/configuring/websocket) | Websocket | +| [`i18n`](/docs/building-your-application/configuring/i18n) | Internationalization routing & translations | ## Top-level files -| | | -| ------------------------------------------------------------------------------------------- | --------------------------------------- | -| | | -| [`brisa.config.js`](/docs/app/api-reference/brisa-config-js) | Configuration file for Next. - - +| | | +| ------------------------------------------------------------ | ---------------------------- | +| | | +| [`brisa.config.js`](/docs/app/api-reference/brisa-config-js) | Configuration file for Next. | ### Special Files in `src/pages` -| | | | -| ----------------------------------------------------------------------------------------------------------- | ------------------- | ----------------- | -| [`_404`](/docs/pages/building-your-application/routing/custom-error#404-page) | `.js` `.jsx` `.tsx` | 404 Error Page | -| [`_500`](/docs/pages/building-your-application/routing/custom-error#500-page) | `.js` `.jsx` `.tsx` | 500 Error Page | +| | | | +| ----------------------------------------------------------------------------- | ------------------- | -------------- | +| [`_404`](/docs/pages/building-your-application/routing/custom-error#404-page) | `.js` `.jsx` `.tsx` | 404 Error Page | +| [`_500`](/docs/pages/building-your-application/routing/custom-error#500-page) | `.js` `.jsx` `.tsx` | 500 Error Page | ### Routes @@ -58,4 +54,4 @@ This page provides an overview of the file and folder structure of a Brisa proje | **File convention** | | | | [`[file]`](/docs/pages/building-your-application/routing/dynamic-routes) | `.js` `.jsx` `.tsx` | Dynamic route segment | | [`[...file]`](/docs/pages/building-your-application/routing/dynamic-routes#catch-all-segments) | `.js` `.jsx` `.tsx` | Catch-all route segment | -| [`[[...file]]`](/docs/pages/building-your-application/routing/dynamic-routes#optional-catch-all-segments) | `.js` `.jsx` `.tsx` | Optional catch-all route segment | \ No newline at end of file +| [`[[...file]]`](/docs/pages/building-your-application/routing/dynamic-routes#optional-catch-all-segments) | `.js` `.jsx` `.tsx` | Optional catch-all route segment | diff --git a/docs/01-getting-started/index.md b/docs/01-getting-started/index.md index e0d703444..bab7a5a0c 100644 --- a/docs/01-getting-started/index.md +++ b/docs/01-getting-started/index.md @@ -1,4 +1,4 @@ --- title: Getting Started description: Learn how to create full-stack web applications with Brisa. ---- \ No newline at end of file +--- diff --git a/docs/02-building-your-application/01-routing/01-pages-and-layouts.md b/docs/02-building-your-application/01-routing/01-pages-and-layouts.md index b9d5e8d64..dc7ad660e 100644 --- a/docs/02-building-your-application/01-routing/01-pages-and-layouts.md +++ b/docs/02-building-your-application/01-routing/01-pages-and-layouts.md @@ -13,7 +13,7 @@ In Brisa framework, a **page** is a [Brisa Component](/docs/components-details) ```jsx export default function About() { - return
About
+ return
About
; } ``` @@ -43,7 +43,6 @@ Brisa supports pages with dynamic routes. For example, if you create a file call The global layout is defined inside `/src/layout/index`. By default Brisa supports a default layout, but you can modify it here. - ```jsx filename="src/layout/index.js" import { RequestContext } from "brisa"; @@ -107,4 +106,4 @@ export default async function Layout({ children }: { children: JSX.Element }, { } ``` -The `fetch` is directly native and has no wrapper to control the cache. We recommend that you do not do the same `fetch` in several places, but use the [`context`](/docs/building-your-application/data-fetching/request-context) to store the data and consume it from any component. \ No newline at end of file +The `fetch` is directly native and has no wrapper to control the cache. We recommend that you do not do the same `fetch` in several places, but use the [`context`](/docs/building-your-application/data-fetching/request-context) to store the data and consume it from any component. diff --git a/docs/02-building-your-application/01-routing/02-dynamic-routes.md b/docs/02-building-your-application/01-routing/02-dynamic-routes.md index eee690751..f4e3b8cdd 100644 --- a/docs/02-building-your-application/01-routing/02-dynamic-routes.md +++ b/docs/02-building-your-application/01-routing/02-dynamic-routes.md @@ -17,7 +17,7 @@ For example, a blog could include the following route `src/pages/blog/[slug].js` ```jsx export default function Page(props, { route }) { - return

Post: {route.query.slug}

+ return

Post: {route.query.slug}

; } ``` @@ -54,4 +54,4 @@ The difference between **catch-all** and **optional catch-all** segments is that | `pages/shop/[[...slug]].js` | `/shop` | `{}` | | `pages/shop/[[...slug]].js` | `/shop/a` | `{ slug: ['a'] }` | | `pages/shop/[[...slug]].js` | `/shop/a/b` | `{ slug: ['a', 'b'] }` | -| `pages/shop/[[...slug]].js` | `/shop/a/b/c` | `{ slug: ['a', 'b', 'c'] }` | \ No newline at end of file +| `pages/shop/[[...slug]].js` | `/shop/a/b/c` | `{ slug: ['a', 'b', 'c'] }` | diff --git a/docs/02-building-your-application/01-routing/03-linking-and-navigating.md b/docs/02-building-your-application/01-routing/03-linking-and-navigating.md index f3b025713..2084358a1 100644 --- a/docs/02-building-your-application/01-routing/03-linking-and-navigating.md +++ b/docs/02-building-your-application/01-routing/03-linking-and-navigating.md @@ -19,7 +19,7 @@ export default function Home() { Blog Post - ) + ); } ``` @@ -29,7 +29,6 @@ The example above uses multiple `a` tags. Each one maps a path (`href`) to a kno - `/about` → `src/pages/about.js` - `/blog/hello-world` → `src/pages/blog/[slug].js` - ## Navigation to dynamic paths You can also use interpolation to create the path, which comes in handy for [dynamic route segments](/docs/building-your-application/routing/dynamic-routes). For example, to show a list of posts which have been passed to the component as a prop: @@ -40,26 +39,23 @@ export default function Posts({ posts }) { - ) + ); } ``` > [`encodeURIComponent`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent) is used in the example to keep the path utf-8 compatible. - ## I18n navigation If you have [i18n routing](/docs/routing/internationalization) enabled, during navigation you always have to forget about route translations and during the render of the page will be translated to correct translated page. ```jsx export default function Home() { - return About Us + return About Us; } ``` @@ -77,7 +73,7 @@ It is always possible to force a specific route in case you want to change the l ```jsx export default function Home() { - return About Us in Spanish + return About Us in Spanish; } ``` diff --git a/docs/02-building-your-application/01-routing/04-custom-error.md b/docs/02-building-your-application/01-routing/04-custom-error.md index 0de853650..3935fbda2 100644 --- a/docs/02-building-your-application/01-routing/04-custom-error.md +++ b/docs/02-building-your-application/01-routing/04-custom-error.md @@ -5,40 +5,38 @@ description: Override and extend the built-in Error page to handle custom errors ## 404 Page -To create a custom 404 page you can create a `src/pages/_404.js` file. +To create a custom 404 page you can create a `src/pages/_404.js` file. ```jsx filename="src/pages/_404.js" export default function Custom404() { - return

404 - Page Not Found

+ return

404 - Page Not Found

; } ``` > **Good to know**: In this page you can access to the `request context`, `fetch` data, change the `head` content (meta tags, etc), and change the `response headers`, in the same way of the rest of pages. - ## 500 Page To customize the 500 page you can create a `src/pages/_500.js` file. ```jsx filename="src/pages/_500.js" export default function Custom500({ error }, requestContext) { - return

500 - {error.message}

+ return

500 - {error.message}

; } ``` > **Good to know**: In this page you can access to the `request context`, `fetch` data, change the `head` content (meta tags, etc), and change the `response headers`, in the same way of the rest of pages. - ### Errors in component-level If you want to control errors at the component level instead of displaying a whole new page with the error, you can make the components have the error extension by adding the `ComponentName.error`: ```jsx export default function SomeComponent() { - return /* some JSX */ + return; /* some JSX */ } SomeComponent.error = ({ error }, requestContext) => { - return

Oops! {error.message}

-} + return

Oops! {error.message}

; +}; ``` diff --git a/docs/02-building-your-application/01-routing/05-api-routes.md b/docs/02-building-your-application/01-routing/05-api-routes.md index bae531a04..bf8f87b93 100644 --- a/docs/02-building-your-application/01-routing/05-api-routes.md +++ b/docs/02-building-your-application/01-routing/05-api-routes.md @@ -16,21 +16,21 @@ import { type RequestContext } from "brisa"; export function GET(request: RequestContext) { const responseData = JSON.stringify({ - message: "Hello world from Brisa!" - }) + message: "Hello world from Brisa!", + }); const responseOptions = { - headers: { "content-type": "application/json" } - } + headers: { "content-type": "application/json" }, + }; return new Response(responseData, responseOptions); } ``` - ## Query and parameters If we want for example to use a dynamic route for users and know which username it is: + - `/api/user/aralroca?id=3` → `src/api/user/[username].ts` We have access to the route through the `RequestContext` and we can access both the parameters and the query. @@ -39,7 +39,7 @@ We have access to the route through the `RequestContext` and we can access both import { type RequestContext } from "brisa"; export function GET({ route: { query, params } }: RequestContext) { - const { id } = params + const { id } = params; return new Response(`Hello world ${query.username} with id=${id}!`); } ``` @@ -48,7 +48,6 @@ export function GET({ route: { query, params } }: RequestContext) { The request that arrives is an extension of the native [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request), where apart from having everything that the request has, it has extra information of the request, such as the `i18n`, the `route` and more. If you want to know more take a look at [`request context`](/docs/building-your-application/data-fetching/request-context). - ## Response The [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) is the native one, so you can find out [here](https://developer.mozilla.org/en-US/docs/Web/API/Response) how it works. @@ -65,27 +64,26 @@ Example: import { type RequestContext } from "brisa"; export function GET({ i18n, route: { query, params } }: RequestContext) { - const { id } = params - return new Response(i18n.t('hello', { name: params.username, id })); + const { id } = params; + return new Response(i18n.t("hello", { name: params.username, id })); } ``` And this inside `src/i18n/index.ts` or `src/i18n.ts` file: - ```ts filename="src/i18n/index.ts" switcher export default { - locales: ['en', 'es'], - defaultLocale: 'en', + locales: ["en", "es"], + defaultLocale: "en", messages: { en: { - hello: 'Hello {{name}} with id={{id}}!', + hello: "Hello {{name}} with id={{id}}!", }, es: { - hello: '¡Hola {{name}} con id={{id}}!', + hello: "¡Hola {{name}} con id={{id}}!", }, }, -} +}; ``` ## Dynamic routes, catch all and optional catch all routes @@ -97,15 +95,14 @@ API Routes support [dynamic routes](/docs/building-your-application/routing/dyna It can be extended to catch all paths by adding three dots (`...`) inside the brackets. For example: - `/api/post/a` → `pages/api/post/[...slug].js` -- `/api/post/a/b` → `pages/api/post/[...slug].js` - `/api/post/a/b/c` and so on. → `pages/api/post/[...slug].js` +- `/api/post/a/b` → `pages/api/post/[...slug].js` + `/api/post/a/b/c` and so on. → `pages/api/post/[...slug].js` Catch all routes can be made optional by including the parameter in double brackets (`[[...slug]]`). -- `/api/post` → `pages/api/post/[[...slug]].js` -- `/api/post/a` → `pages/api/post/[[...slug]].js` -- `/api/post/a/b`, and so on. → `pages/api/post/[[...slug]].js` - +- `/api/post` → `pages/api/post/[[...slug]].js` +- `/api/post/a` → `pages/api/post/[[...slug]].js` +- `/api/post/a/b`, and so on. → `pages/api/post/[[...slug]].js` > **Good to know**: You can use names other than `slug`, such as: `[[...param]]` @@ -115,14 +112,14 @@ You can set CORS headers on a `Response` using the standard Web API methods: ```ts export async function GET(request: Request) { - return new Response('Hello, Brisa!', { + return new Response("Hello, Brisa!", { status: 200, headers: { - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', - 'Access-Control-Allow-Headers': 'Content-Type, Authorization', + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type, Authorization", }, - }) + }); } ``` diff --git a/package.json b/package.json index 05c455aac..26a43ff27 100644 --- a/package.json +++ b/package.json @@ -64,4 +64,4 @@ "peerDependencies": { "typescript": "5.2.2" } -} \ No newline at end of file +} diff --git a/src/__fixtures__/middleware.ts b/src/__fixtures__/middleware.ts index 7d135f63b..0fa520e1b 100644 --- a/src/__fixtures__/middleware.ts +++ b/src/__fixtures__/middleware.ts @@ -1,5 +1,7 @@ -export default async function middleware(request: Request) { - const url = new URL(request.url); +import { RequestContext } from "../types"; + +export default async function middleware(request: RequestContext) { + const url = new URL(request.finalURL); if (url.pathname !== "/test") return; diff --git a/src/cli/serve.tsx b/src/cli/serve.tsx index d793661e6..eb90bfd4e 100644 --- a/src/cli/serve.tsx +++ b/src/cli/serve.tsx @@ -3,7 +3,7 @@ import path from "node:path"; import LoadLayout from "../utils/load-layout"; import getRouteMatcher from "../utils/get-route-matcher"; -import { RequestContext, renderToReadableStream } from "../core"; +import { renderToReadableStream } from "../core"; import { LiveReloadScript } from "./dev-live-reload"; import { MatchedRoute, ServerWebSocket } from "bun"; import importFileIfExists from "../utils/import-file-if-exists"; @@ -11,6 +11,8 @@ import getConstants from "../constants"; import handleI18n from "../utils/handle-i18n"; import redirectTrailingSlash from "../utils/redirect-trailing-slash"; import getImportableFilepath from "../utils/get-importable-filepath"; +import extendRequestContext from "../utils/extend-request-context"; +import { RequestContext } from "../types"; const { IS_PRODUCTION, @@ -60,8 +62,8 @@ Bun.serve({ development: !IS_PRODUCTION, async fetch(req: Request, server) { if (server.upgrade(req)) return; - const request = new RequestContext(req); - const url = new URL(request.url); + const request = extendRequestContext({ originalRequest: req }); + const url = new URL(request.finalURL); const assetPath = path.join(ASSETS_DIR, url.pathname); const isHome = url.pathname === "/"; const isAnAsset = !isHome && fs.existsSync(assetPath); @@ -81,7 +83,7 @@ Bun.serve({ request.getIP = () => server.requestIP(req); return ( - handleRequest(request, isAnAsset) + handleRequest(request, isAnAsset, req) // 500 page .catch((error) => { const route500 = pagesRouter.reservedRoutes[PAGE_500]; @@ -121,9 +123,13 @@ console.log( ////////////////////// HELPERS /////////////////////// /////////////////////////////////////////////////////// -async function handleRequest(req: RequestContext, isAnAsset: boolean) { +async function handleRequest( + req: RequestContext, + isAnAsset: boolean, + originalReq: Request, +) { const locale = req.i18n.locale; - const url = new URL(req.url); + const url = new URL(req.finalURL); const pathname = url.pathname; const { route, isReservedPathname } = pagesRouter.match(req); const isApi = pathname.startsWith(locale ? `/${locale}/api/` : "/api/"); @@ -149,7 +155,7 @@ async function handleRequest(req: RequestContext, isAnAsset: boolean) { req.route = api.route; - const response = module[method]?.(req) + const response = module[method]?.(req); if (response) return response; } diff --git a/src/core/index.ts b/src/core/index.ts index c3c647464..71b5f030b 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -1,5 +1,4 @@ import renderToReadableStream from "./render-to-readable-stream"; -import RequestContext from "./request-context"; import dangerHTML from "./danger-html"; -export { renderToReadableStream, RequestContext, dangerHTML }; +export { renderToReadableStream, dangerHTML }; diff --git a/src/core/render-to-readable-stream/index.test.tsx b/src/core/render-to-readable-stream/index.test.tsx index 3a5cf5fd1..017818306 100644 --- a/src/core/render-to-readable-stream/index.test.tsx +++ b/src/core/render-to-readable-stream/index.test.tsx @@ -1,12 +1,14 @@ import { describe, it, expect, mock, afterEach, afterAll } from "bun:test"; import renderToReadableStream from "."; -import { RequestContext } from ".."; import streamToText from "../../__fixtures__/stream-to-text"; import dangerHTML from "../danger-html"; import getConstants from "../../constants"; -import { ComponentType } from "../../types"; +import { ComponentType, RequestContext } from "../../types"; +import extendRequestContext from "../../utils/extend-request-context"; -const testRequest = new RequestContext(new Request("http://test.com/")); +const testRequest = extendRequestContext({ + originalRequest: new Request("http://test.com/"), +}); const mockConsoleError = mock(() => {}); const consoleError = console.error; console.error = mockConsoleError; @@ -84,11 +86,11 @@ describe("brisa core", () => { it("should be possible to access to the request object inside components", async () => { const Component = ( { name, title }: { name: string; title: string }, - request: Request, + request: RequestContext, ) => (

Hello {name}

-

The URL is: {request.url}

+

The URL is: {request.finalURL}

); const element = ; @@ -108,7 +110,7 @@ describe("brisa core", () => { { name }: { name: string }, request: RequestContext, ) => { - const url = new URL(request.url); + const url = new URL(request.finalURL); const query = new URLSearchParams(url.search); const testName = query.get("name") || name; @@ -123,7 +125,9 @@ describe("brisa core", () => { const stream2 = await renderToReadableStream( element, - new RequestContext(new Request("http://test.com/?name=Test")), + extendRequestContext({ + originalRequest: new Request("http://test.com/?name=Test"), + }), ); const result2 = await streamToText(stream2); const expected2 = "
Hello Test
"; @@ -302,7 +306,9 @@ describe("brisa core", () => { }); it("should inject the hrefLang attributes if the i18n is enabled and have hrefLangOrigin defined", () => { - const req = new RequestContext(new Request(testRequest)); + const req = extendRequestContext({ + originalRequest: new Request(testRequest), + }); const i18n = { locale: "es", locales: ["en", "es"], @@ -578,7 +584,7 @@ describe("brisa core", () => { const element = Test; const stream = renderToReadableStream(element, testRequest); const result = await streamToText(stream); - testRequest.i18n = undefined; + testRequest.i18n = {} as any; expect(result).toEqual(`Test`); }); @@ -592,7 +598,7 @@ describe("brisa core", () => { const element = Test; const stream = renderToReadableStream(element, testRequest); const result = await streamToText(stream); - testRequest.i18n = undefined; + testRequest.i18n = undefined as any; expect(result).toEqual(`Test`); }); @@ -647,7 +653,9 @@ describe("brisa core", () => { }); it("should render the head element with the canonical", () => { - const req = new RequestContext(new Request(testRequest)); + const req = extendRequestContext({ + originalRequest: new Request(testRequest), + }); const element = ( @@ -673,7 +681,9 @@ describe("brisa core", () => { }); it("should render the head element with the title replacing the original title", () => { - const req = new RequestContext(new Request(testRequest)); + const req = extendRequestContext({ + originalRequest: new Request(testRequest), + }); const element = ( @@ -699,7 +709,9 @@ describe("brisa core", () => { }); it("should allow multiple ids outside the head (not ideal but should not break the render)", () => { - const req = new RequestContext(new Request(testRequest)); + const req = extendRequestContext({ + originalRequest: new Request(testRequest), + }); const element = ( diff --git a/src/core/render-to-readable-stream/index.ts b/src/core/render-to-readable-stream/index.ts index b1e02ed74..3d3b6a20a 100644 --- a/src/core/render-to-readable-stream/index.ts +++ b/src/core/render-to-readable-stream/index.ts @@ -1,8 +1,12 @@ -import type { Props, ComponentType, JSXNode } from "../../types"; +import type { + Props, + ComponentType, + JSXNode, + RequestContext, +} from "../../types"; import extendStreamController, { Controller, } from "../../utils/extend-stream-controller"; -import RequestContext from "../request-context"; import { injectUnsuspenseScript } from "../inject-unsuspense-script" assert { type: "macro" }; import renderAttributes from "../../utils/render-attributes"; import generateHrefLang from "../../utils/generate-href-lang"; diff --git a/src/core/request-context/index.ts b/src/core/request-context/index.ts deleted file mode 100644 index c3cdeef20..000000000 --- a/src/core/request-context/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { MatchedRoute, ServerWebSocket, SocketAddress } from "bun"; -import { I18nFromRequest } from "../../types"; - -export default class RequestContext extends Request { - constructor(request: Request, route?: MatchedRoute) { - super(request); - this.route = route; - this.context = new Map(); - this.ws = globalThis.ws; - this.i18n = { - defaultLocale: '', - locales: [], - locale: '', - t: () => '', - } - this.getIP = () => null; - } - - route?: MatchedRoute; - context: Map; - i18n: I18nFromRequest; - getIP: () => SocketAddress | null; - ws?: ServerWebSocket; -} diff --git a/src/create-brisa-app/package.json b/src/create-brisa-app/package.json index b9b6b093f..faaca1838 100644 --- a/src/create-brisa-app/package.json +++ b/src/create-brisa-app/package.json @@ -17,4 +17,4 @@ "bin": { "create-brisa-app": "./create-brisa-app.sh" } -} \ No newline at end of file +} diff --git a/src/types/index.d.ts b/src/types/index.d.ts index 19852407e..f9c7ac89d 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -6,6 +6,7 @@ export interface RequestContext extends Request { i18n: I18nFromRequest; ws?: ServerWebSocket; getIP: () => SocketAddress | null; + finalURL: string; } type Props = Record & { diff --git a/src/utils/adapt-router-to-page-translations/index.test.ts b/src/utils/adapt-router-to-page-translations/index.test.ts index ab77d8284..36b7911e3 100644 --- a/src/utils/adapt-router-to-page-translations/index.test.ts +++ b/src/utils/adapt-router-to-page-translations/index.test.ts @@ -2,8 +2,9 @@ import { describe, expect, it, beforeEach, afterEach } from "bun:test"; import path from "node:path"; import adaptRouterToPageTranslations from "."; import getRouteMatcher from "../get-route-matcher"; -import { RequestContext } from "../../core"; import getConstants from "../../constants"; +import extendRequestContext from "../extend-request-context"; +import { RequestContext } from "../../types"; const PAGES_DIR = path.join( import.meta.dir, @@ -26,7 +27,7 @@ const pages = { const router = getRouteMatcher(PAGES_DIR, ["/_404"]); const createRequest = (url: string) => { - const request = new RequestContext(new Request(url)); + const request = extendRequestContext({ originalRequest: new Request(url) }); request.i18n = { locale: "es", defaultLocale: "es", @@ -115,7 +116,9 @@ describe("utils", () => { it("should detect all the translated routes", () => { const mockRouter = { match: (v: RequestContext) => - typeof v.url === "string" ? new URL(v.url).pathname : null, + typeof v.finalURL === "string" + ? new URL(v.finalURL).pathname + : null, }; const mockPages = { "/somepage": { es: "/alguna-pagina", it: "/qualsiasi-pagina" }, @@ -160,7 +163,9 @@ describe("utils", () => { it("should detect all the translated routes with locale prefix", () => { const mockRouter = { match: (v) => - typeof v.url === "string" ? new URL(v.url).pathname : null, + typeof v.finalURL === "string" + ? new URL(v.finalURL).pathname + : null, }; const mockPages = { "/somepage": { es: "/alguna-pagina", it: "/qualsiasi-pagina" }, @@ -207,7 +212,9 @@ describe("utils", () => { it("should detect all the translated routes with trailingSlash", () => { const mockRouter = { match: (v) => - typeof v.url === "string" ? new URL(v.url).pathname : null, + typeof v.finalURL === "string" + ? new URL(v.finalURL).pathname + : null, }; const mockPages = { "/somepage": { es: "/alguna-pagina", it: "/qualsiasi-pagina" }, diff --git a/src/utils/adapt-router-to-page-translations/index.ts b/src/utils/adapt-router-to-page-translations/index.ts index 8393f21ad..c53b915fd 100644 --- a/src/utils/adapt-router-to-page-translations/index.ts +++ b/src/utils/adapt-router-to-page-translations/index.ts @@ -1,5 +1,10 @@ -import { RequestContext } from "../../core"; -import { I18nConfig, RouterType, Translations } from "../../types"; +import extendRequestContext from "../extend-request-context"; +import { + I18nConfig, + RequestContext, + RouterType, + Translations, +} from "../../types"; import routeMatchPathname from "../route-match-pathname"; import substituteI18nRouteValues from "../substitute-i18n-route-values"; @@ -14,10 +19,10 @@ export default function adaptRouterToPageTranslations( const translations = Object.fromEntries(translationsEntries); const match = (req: RequestContext) => { - const url = new URL(req.url); + const url = new URL(req.finalURL); const userLocale = req.i18n?.locale; const newReq = (url: string) => - new RequestContext(new Request(url, req), req.route); + extendRequestContext({ originalRequest: req, finalURL: url }); url.pathname = url.pathname .replace(`/${userLocale}`, "") diff --git a/src/core/request-context/index.test.ts b/src/utils/extend-request-context/index.test.ts similarity index 59% rename from src/core/request-context/index.test.ts rename to src/utils/extend-request-context/index.test.ts index f8bab0cd1..678a91190 100644 --- a/src/core/request-context/index.test.ts +++ b/src/utils/extend-request-context/index.test.ts @@ -1,16 +1,19 @@ import { describe, it, expect } from "bun:test"; -import RequestContext from "."; +import extendRequestContext from "."; describe("brisa core", () => { - describe("RequestContext", () => { - it("should create a new RequestContext", () => { + describe("extend request context", () => { + it("should extent the request", () => { const request = new Request("https://example.com"); const route = { path: "/", } as any; - const requestContext = new RequestContext(request, route); + const requestContext = extendRequestContext({ + originalRequest: request, + route, + }); expect(requestContext.route).toEqual(route); - expect(requestContext.url).toEqual(request.url); + expect(requestContext.finalURL).toEqual(request.finalURL); expect(requestContext.context).toBeInstanceOf(Map); }); @@ -19,7 +22,10 @@ describe("brisa core", () => { const route = { path: "/", } as any; - const requestContext = new RequestContext(request, route); + const requestContext = extendRequestContext({ + originalRequest: request, + route, + }); requestContext.context.set("foo", "bar"); expect(requestContext.context.get("foo")).toBe("bar"); }); diff --git a/src/utils/extend-request-context/index.ts b/src/utils/extend-request-context/index.ts new file mode 100644 index 000000000..9c3a028f7 --- /dev/null +++ b/src/utils/extend-request-context/index.ts @@ -0,0 +1,51 @@ +// @ts-nocheck +import { MatchedRoute } from "bun"; +import { I18nFromRequest, RequestContext } from "../../types"; + +type ExtendRequestContext = { + originalRequest: Request; + currentRequestContext?: RequestContext; + route?: MatchedRoute; + i18n?: I18nFromRequest; + finalURL?: string; +}; + +export default function extendRequestContext({ + originalRequest, + currentRequestContext, + route, + i18n, + finalURL, +}: ExtendRequestContext): RequestContext { + // finalURL + originalRequest.finalURL = + currentRequestContext?.finalURL ?? + finalURL ?? + originalRequest.finalURL ?? + originalRequest.url; + + // route + originalRequest.route = + currentRequestContext?.route ?? route ?? originalRequest.route; + + // context + originalRequest.context = + currentRequestContext?.context ?? + originalRequest.context ?? + new Map(); + + // ws + originalRequest.ws = globalThis.ws; + + // i18n + originalRequest.i18n = currentRequestContext?.i18n ?? + originalRequest.i18n ?? + i18n ?? { + defaultLocale: "", + locales: [], + locale: "", + t: () => "", + }; + + return originalRequest as RequestContext; +} diff --git a/src/utils/generate-href-lang/index.test.ts b/src/utils/generate-href-lang/index.test.ts index f1433a603..382364d44 100644 --- a/src/utils/generate-href-lang/index.test.ts +++ b/src/utils/generate-href-lang/index.test.ts @@ -1,8 +1,8 @@ import { describe, it, expect, mock, afterEach } from "bun:test"; import generateHrefLang from "."; import getConstants from "../../constants"; -import { RequestContext } from "../../core"; import { MatchedRoute } from "bun"; +import extendRequestContext from "../extend-request-context"; const warn = console.warn.bind(console); @@ -29,10 +29,10 @@ describe("utils", () => { }, }, }; - const input = new RequestContext( - new Request("https://www.example.com/somepage"), - { name: "/somepage" } as MatchedRoute, - ); + const input = extendRequestContext({ + originalRequest: new Request("https://www.example.com/somepage"), + route: { name: "/somepage" } as MatchedRoute, + }); input.i18n = { ...getConstants().I18N_CONFIG, locale: "es", @@ -62,10 +62,10 @@ describe("utils", () => { }; const mockWarn = mock((v) => v); console.warn = mockWarn; - const input = new RequestContext( - new Request("https://www.example.com/somepage"), - { name: "/somepage" } as MatchedRoute, - ); + const input = extendRequestContext({ + originalRequest: new Request("https://www.example.com/somepage"), + route: { name: "/somepage" } as MatchedRoute, + }); input.i18n = { ...getConstants().I18N_CONFIG, locale: "es", @@ -91,10 +91,10 @@ describe("utils", () => { hrefLangOrigin: "https://www.example.com", }, }; - const input = new RequestContext( - new Request("https://www.example.com/somepage"), - { name: "/somepage" } as MatchedRoute, - ); + const input = extendRequestContext({ + originalRequest: new Request("https://www.example.com/somepage"), + route: { name: "/somepage" } as MatchedRoute, + }); input.i18n = { ...getConstants().I18N_CONFIG, locale: "es", @@ -119,10 +119,10 @@ describe("utils", () => { hrefLangOrigin: "https://www.example.com", }, }; - const input = new RequestContext( - new Request("https://www.example.com/somepage/1"), - { name: "/somepage/[id]" } as MatchedRoute, - ); + const input = extendRequestContext({ + originalRequest: new Request("https://www.example.com/somepage/1"), + route: { name: "/somepage/[id]" } as MatchedRoute, + }); input.i18n = { ...getConstants().I18N_CONFIG, locale: "es", @@ -148,10 +148,10 @@ describe("utils", () => { hrefLangOrigin: "https://www.example.com", }, }; - const input = new RequestContext( - new Request("https://www.example.com/somepage/1"), - { name: "/somepage/[id]" } as MatchedRoute, - ); + const input = extendRequestContext({ + originalRequest: new Request("https://www.example.com/somepage/1"), + route: { name: "/somepage/[id]" } as MatchedRoute, + }); input.i18n = { ...getConstants().I18N_CONFIG, locale: "en", @@ -176,10 +176,10 @@ describe("utils", () => { hrefLangOrigin: "https://www.example.com", }, }; - const input = new RequestContext( - new Request("https://www.example.com/somepage"), - { name: "/somepage" } as MatchedRoute, - ); + const input = extendRequestContext({ + originalRequest: new Request("https://www.example.com/somepage"), + route: { name: "/somepage" } as MatchedRoute, + }); input.i18n = { ...getConstants().I18N_CONFIG, locale: undefined, @@ -203,10 +203,10 @@ describe("utils", () => { hrefLangOrigin: "https://www.example.com", }, }; - const input = new RequestContext( - new Request("https://www.example.com/somepage/1/2/3"), - { name: "/somepage/[[...catchAll]]" } as MatchedRoute, - ); + const input = extendRequestContext({ + originalRequest: new Request("https://www.example.com/somepage/1/2/3"), + route: { name: "/somepage/[[...catchAll]]" } as MatchedRoute, + }); input.i18n = { ...getConstants().I18N_CONFIG, locale: "en", @@ -232,10 +232,12 @@ describe("utils", () => { hrefLangOrigin: "https://www.example.com", }, }; - const input = new RequestContext( - new Request("https://www.example.com/somepage/1/settings/2/3"), - { name: "/somepage/[id]/settings/[...rest]" } as MatchedRoute, - ); + const input = extendRequestContext({ + originalRequest: new Request( + "https://www.example.com/somepage/1/settings/2/3", + ), + route: { name: "/somepage/[id]/settings/[...rest]" } as MatchedRoute, + }); input.i18n = { ...getConstants().I18N_CONFIG, locale: "en", @@ -264,10 +266,10 @@ describe("utils", () => { hrefLangOrigin: "https://www.example.com", }, }; - const input = new RequestContext( - new Request("https://www.example.com/somepage"), - { name: "/somepage" } as MatchedRoute, - ); + const input = extendRequestContext({ + originalRequest: new Request("https://www.example.com/somepage"), + route: { name: "/somepage" } as MatchedRoute, + }); input.i18n = { ...getConstants().I18N_CONFIG, locale: "en", @@ -302,10 +304,12 @@ describe("utils", () => { }, }, }; - const input = new RequestContext( - new Request("https://www.example.com/somepage/1/settings/2/3"), - { name: "/somepage/[id]/settings/[...rest]" } as MatchedRoute, - ); + const input = extendRequestContext({ + originalRequest: new Request( + "https://www.example.com/somepage/1/settings/2/3", + ), + route: { name: "/somepage/[id]/settings/[...rest]" } as MatchedRoute, + }); input.i18n = { ...getConstants().I18N_CONFIG, locale: "en", @@ -345,10 +349,12 @@ describe("utils", () => { }, }, }; - const input = new RequestContext( - new Request("https://www.example.com/en/somepage/1/settings/2/3"), - { name: "/somepage/[id]/settings/[...rest]" } as MatchedRoute, - ); + const input = extendRequestContext({ + originalRequest: new Request( + "https://www.example.com/en/somepage/1/settings/2/3", + ), + route: { name: "/somepage/[id]/settings/[...rest]" } as MatchedRoute, + }); input.i18n = { ...globalThis.mockConstants.I18N_CONFIG, locale: "en", @@ -397,10 +403,10 @@ describe("utils", () => { }, }; - const input = new RequestContext( - new Request("https://test.com/es/sobre-nosotros"), - { name: "/a" } as MatchedRoute, - ); + const input = extendRequestContext({ + originalRequest: new Request("https://test.com/es/sobre-nosotros"), + route: { name: "/a" } as MatchedRoute, + }); input.i18n = { ...getConstants().I18N_CONFIG, locale: "es", @@ -432,10 +438,10 @@ describe("utils", () => { }, }; - const input = new RequestContext( - new Request("https://test.com/es/not-found"), - { name: "/_404" } as MatchedRoute, - ); + const input = extendRequestContext({ + originalRequest: new Request("https://test.com/es/not-found"), + route: { name: "/_404" } as MatchedRoute, + }); input.i18n = { ...getConstants().I18N_CONFIG, locale: "es", @@ -478,22 +484,26 @@ describe("utils", () => { ...globalThis.mockConstants.I18N_CONFIG, locale: "en", }; - const home = new RequestContext( - new Request("https://www.example.com/en/"), - { name: "/" } as MatchedRoute, - ); - const homeWithoutTrailingSlash = new RequestContext( - new Request("https://www.example.com/en"), - { name: "/" } as MatchedRoute, - ); - const withTrailingSlash = new RequestContext( - new Request("https://www.example.com/en/somepage/1/settings/2/3/"), - { name: "/somepage/[id]/settings/[...rest]" } as MatchedRoute, - ); - const withoutTrailingSlash = new RequestContext( - new Request("https://www.example.com/en/somepage/1/settings/2/3"), - { name: "/somepage/[id]/settings/[...rest]" } as MatchedRoute, - ); + const home = extendRequestContext({ + originalRequest: new Request("https://www.example.com/en/"), + route: { name: "/" } as MatchedRoute, + }); + const homeWithoutTrailingSlash = extendRequestContext({ + originalRequest: new Request("https://www.example.com/en"), + route: { name: "/" } as MatchedRoute, + }); + const withTrailingSlash = extendRequestContext({ + originalRequest: new Request( + "https://www.example.com/en/somepage/1/settings/2/3/", + ), + route: { name: "/somepage/[id]/settings/[...rest]" } as MatchedRoute, + }); + const withoutTrailingSlash = extendRequestContext({ + originalRequest: new Request( + "https://www.example.com/en/somepage/1/settings/2/3", + ), + route: { name: "/somepage/[id]/settings/[...rest]" } as MatchedRoute, + }); home.i18n = i18n; withTrailingSlash.i18n = i18n; @@ -558,22 +568,26 @@ describe("utils", () => { ...globalThis.mockConstants.I18N_CONFIG, locale: "en", }; - const home = new RequestContext( - new Request("https://www.example.com/en/"), - { name: "/" } as MatchedRoute, - ); - const homeWithoutTrailingSlash = new RequestContext( - new Request("https://www.example.com/en"), - { name: "/" } as MatchedRoute, - ); - const withTrailingSlash = new RequestContext( - new Request("https://www.example.com/en/somepage/1/settings/2/3/"), - { name: "/somepage/[id]/settings/[...rest]" } as MatchedRoute, - ); - const withoutTrailingSlash = new RequestContext( - new Request("https://www.example.com/en/somepage/1/settings/2/3"), - { name: "/somepage/[id]/settings/[...rest]" } as MatchedRoute, - ); + const home = extendRequestContext({ + originalRequest: new Request("https://www.example.com/en/"), + route: { name: "/" } as MatchedRoute, + }); + const homeWithoutTrailingSlash = extendRequestContext({ + originalRequest: new Request("https://www.example.com/en"), + route: { name: "/" } as MatchedRoute, + }); + const withTrailingSlash = extendRequestContext({ + originalRequest: new Request( + "https://www.example.com/en/somepage/1/settings/2/3/", + ), + route: { name: "/somepage/[id]/settings/[...rest]" } as MatchedRoute, + }); + const withoutTrailingSlash = extendRequestContext({ + originalRequest: new Request( + "https://www.example.com/en/somepage/1/settings/2/3", + ), + route: { name: "/somepage/[id]/settings/[...rest]" } as MatchedRoute, + }); home.i18n = i18n; withTrailingSlash.i18n = i18n; diff --git a/src/utils/generate-href-lang/index.ts b/src/utils/generate-href-lang/index.ts index 19420fe82..c4df90c43 100644 --- a/src/utils/generate-href-lang/index.ts +++ b/src/utils/generate-href-lang/index.ts @@ -1,5 +1,5 @@ -import { RequestContext } from "../../core"; import getConstants from "../../constants"; +import { RequestContext } from "../../types"; import substituteI18nRouteValues from "../substitute-i18n-route-values"; export default function generateHrefLang(request: RequestContext) { @@ -21,8 +21,9 @@ export default function generateHrefLang(request: RequestContext) { const url = getURLInAnotherLang(domain, lang, request); const urlWithoutTrailingSlash = url.toString().replace(/\/$/, ""); - const finalUrl = `${urlWithoutTrailingSlash}${CONFIG.trailingSlash ? "/" : "" - }`; + const finalUrl = `${urlWithoutTrailingSlash}${ + CONFIG.trailingSlash ? "/" : "" + }`; return ``; }) @@ -56,7 +57,7 @@ function getURLInAnotherLang( request: RequestContext, ) { const { I18N_CONFIG, LOCALES_SET } = getConstants(); - const paths = new URL(request.url).pathname.split("/"); + const paths = new URL(request.finalURL).pathname.split("/"); const page = LOCALES_SET.has(paths[1]) ? paths.join("/").slice(3) : paths.join("/"); diff --git a/src/utils/get-locale-from-request/index.test.ts b/src/utils/get-locale-from-request/index.test.ts index b78276928..8f617f98d 100644 --- a/src/utils/get-locale-from-request/index.test.ts +++ b/src/utils/get-locale-from-request/index.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect, beforeEach, afterEach } from "bun:test"; -import { RequestContext } from "../../core"; import getLocaleFromRequest from "."; +import extendRequestContext from "../extend-request-context"; describe("utils", () => { beforeEach(() => { @@ -19,21 +19,27 @@ describe("utils", () => { describe("getLocaleFromRequest", () => { it("should return locale from request", () => { - const request = new RequestContext(new Request("https://example.com/ru")); + const request = extendRequestContext({ + originalRequest: new Request("https://example.com/ru"), + }); const locale = getLocaleFromRequest(request); expect(locale).toBe("ru"); }); it("should return default locale if not locale", () => { - const request = new RequestContext(new Request("https://example.com")); + const request = extendRequestContext({ + originalRequest: new Request("https://example.com"), + }); const locale = getLocaleFromRequest(request); expect(locale).toBe("en"); }); it("should return default locale if locale is not supported", () => { - const request = new RequestContext(new Request("https://example.com/ua")); + const request = extendRequestContext({ + originalRequest: new Request("https://example.com/ua"), + }); const locale = getLocaleFromRequest(request); expect(locale).toBe("en"); @@ -45,7 +51,9 @@ describe("utils", () => { defaultLocale: "ru", }, }; - const request = new RequestContext(new Request("https://example.com")); + const request = extendRequestContext({ + originalRequest: new Request("https://example.com"), + }); const locale = getLocaleFromRequest(request); expect(locale).toBe("ru"); @@ -57,48 +65,50 @@ describe("utils", () => { defaultLocale: "ru", }, }; - const request = new RequestContext(new Request("https://example.com/ua")); + const request = extendRequestContext({ + originalRequest: new Request("https://example.com/ua"), + }); const locale = getLocaleFromRequest(request); expect(locale).toBe("ru"); }); it("should return the browser language as default locale if locale is not supported", () => { - const request = new RequestContext( - new Request("https://example.com/ua", { + const request = extendRequestContext({ + originalRequest: new Request("https://example.com/ua", { headers: { "Accept-Language": "ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7", }, }), - ); + }); const locale = getLocaleFromRequest(request); expect(locale).toBe("ru"); }); it("should return the BRISA_LOCALE cookie as default locale if locale is not supported", () => { - const request = new RequestContext( - new Request("https://example.com/ua", { + const request = extendRequestContext({ + originalRequest: new Request("https://example.com/ua", { headers: { "Accept-Language": "es-ES,es;q=0.9,en-US;q=0.8,en;q=0.7", Cookie: "BRISA_LOCALE=ru", }, }), - ); + }); const locale = getLocaleFromRequest(request); expect(locale).toBe("ru"); }); it("should return the browser language if the BRISA_LOCALE cookie is not supported locale", () => { - const request = new RequestContext( - new Request("https://example.com/ua", { + const request = extendRequestContext({ + originalRequest: new Request("https://example.com/ua", { headers: { "Accept-Language": "ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7", Cookie: "BRISA_LOCALE=ua", }, }), - ); + }); const locale = getLocaleFromRequest(request); expect(locale).toBe("ru"); diff --git a/src/utils/get-locale-from-request/index.ts b/src/utils/get-locale-from-request/index.ts index 9a877d0e9..d869c7599 100644 --- a/src/utils/get-locale-from-request/index.ts +++ b/src/utils/get-locale-from-request/index.ts @@ -1,9 +1,9 @@ import getConstants from "../../constants"; -import { I18nConfig } from "../../types"; +import { I18nConfig, RequestContext } from "../../types"; -export default function getLocaleFromRequest(request: Request): string { +export default function getLocaleFromRequest(request: RequestContext): string { const { I18N_CONFIG = {}, LOCALES_SET } = getConstants(); - const { pathname } = new URL(request.url); + const { pathname } = new URL(request.finalURL); const [, locale] = pathname.split("/"); if (LOCALES_SET.has(locale)) return locale; @@ -33,8 +33,11 @@ function getLocalesFromAcceptLanguage(request: Request): string[] | undefined { return acceptLanguage?.split(",").map((locale) => locale.split(";")[0]); } -function getDefaultLocale(request: Request, I18N_CONFIG: I18nConfig): string { - const domain = new URL(request.url).hostname; +function getDefaultLocale( + request: RequestContext, + I18N_CONFIG: I18nConfig, +): string { + const domain = new URL(request.finalURL).hostname; const domainDefaultLocale = I18N_CONFIG.domains?.[domain]?.defaultLocale; return domainDefaultLocale ?? I18N_CONFIG.defaultLocale; diff --git a/src/utils/get-route-matcher/index.ts b/src/utils/get-route-matcher/index.ts index 9506d57d9..d8ee1d9d5 100644 --- a/src/utils/get-route-matcher/index.ts +++ b/src/utils/get-route-matcher/index.ts @@ -1,5 +1,5 @@ import { MatchedRoute } from "bun"; -import { RouterType } from "../../types"; +import { RequestContext, RouterType } from "../../types"; export default function getRouteMatcher( dir: string, @@ -11,8 +11,8 @@ export default function getRouteMatcher( dir, }); const reservedPathnamesSet = new Set(reservedPathnames); - const routeMatcher = (req: Request) => { - const url = new URL(req.url); + const routeMatcher = (req: RequestContext) => { + const url = new URL(req.finalURL); if (locale) { url.pathname = url.pathname.replace(`/${locale}`, ""); diff --git a/src/utils/handle-i18n/index.test.ts b/src/utils/handle-i18n/index.test.ts index 96f0711a4..071e023b9 100644 --- a/src/utils/handle-i18n/index.test.ts +++ b/src/utils/handle-i18n/index.test.ts @@ -1,7 +1,7 @@ import { afterEach, beforeEach, describe, expect, it } from "bun:test"; import path from "node:path"; import handleI18n from "."; -import { RequestContext } from "../../core"; +import extendRequestContext from "../extend-request-context"; const rootDir = path.join(import.meta.dir, "..", "..", "__fixtures__"); const pagesDir = path.join(rootDir, "pages"); @@ -36,7 +36,7 @@ describe("handleI18n util", () => { }; const req = new Request("https://example.com"); const { response, pagesRouter, rootRouter } = handleI18n( - new RequestContext(req), + extendRequestContext({ originalRequest: req }), ); expect(response).toBeUndefined(); @@ -45,50 +45,54 @@ describe("handleI18n util", () => { }); it("should redirect to default locale if there is no locale in the URL", () => { - const req = new RequestContext(new Request("https://example.com")); + const req = extendRequestContext({ + originalRequest: new Request("https://example.com"), + }); const { response } = handleI18n(req); expect(response?.status).toBe(301); expect(response?.headers.get("location")).toBe("/en"); }); it("should redirect to the browser language as default locale if there is no locale in the URL", () => { - const req = new RequestContext( - new Request("https://example.com", { + const req = extendRequestContext({ + originalRequest: new Request("https://example.com", { headers: { "Accept-Language": "ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7", }, }), - ); + }); const { response } = handleI18n(req); expect(response?.status).toBe(301); expect(response?.headers.get("location")).toBe("/ru"); }); it("should redirect to the last browser language as default locale if there is no locale in the URL", () => { - const req = new RequestContext( - new Request("https://example.com", { + const req = extendRequestContext({ + originalRequest: new Request("https://example.com", { headers: { "Accept-Language": "es-ES,es;q=0.9,de-CH;q=0.7,de;q=0.6,pt;q=0.5,ru-RU;q=0.4", }, }), - ); + }); const { response } = handleI18n(req); expect(response?.status).toBe(301); expect(response?.headers.get("location")).toBe("/ru"); }); it("should redirect with pathname and query params if there is no locale in the URL", () => { - const req = new RequestContext( - new Request("https://example.com/somepage?foo=bar"), - ); + const req = extendRequestContext({ + originalRequest: new Request("https://example.com/somepage?foo=bar"), + }); const { response } = handleI18n(req); expect(response?.status).toBe(301); expect(response?.headers.get("location")).toBe("/en/somepage?foo=bar"); }); it("should not redirect if there is locale in the URL", () => { - const req = new RequestContext(new Request("https://example.com/en/")); + const req = extendRequestContext({ + originalRequest: new Request("https://example.com/en/"), + }); const { response, pagesRouter, rootRouter } = handleI18n(req); expect(response).toBeUndefined(); expect(pagesRouter).toBeDefined(); @@ -96,7 +100,9 @@ describe("handleI18n util", () => { }); it("should not redirect if there is locale in the URL without trailings slash", () => { - const req = new RequestContext(new Request("https://example.com/en")); + const req = extendRequestContext({ + originalRequest: new Request("https://example.com/en"), + }); const { response, pagesRouter, rootRouter } = handleI18n(req); expect(response).toBeUndefined(); expect(pagesRouter).toBeDefined(); @@ -104,9 +110,9 @@ describe("handleI18n util", () => { }); it("should not redirect if there is locale in the URL with pathname and query params", () => { - const req = new RequestContext( - new Request("https://example.com/en/somepage?foo=bar"), - ); + const req = extendRequestContext({ + originalRequest: new Request("https://example.com/en/somepage?foo=bar"), + }); const { response, pagesRouter, rootRouter } = handleI18n(req); expect(response).toBeUndefined(); expect(pagesRouter).toBeDefined(); @@ -114,9 +120,9 @@ describe("handleI18n util", () => { }); it("should pageRouter that handleI18n returns works with locale", () => { - const req = new RequestContext( - new Request("https://example.com/en/somepage?foo=bar"), - ); + const req = extendRequestContext({ + originalRequest: new Request("https://example.com/en/somepage?foo=bar"), + }); const { pagesRouter } = handleI18n(req); const { route: pagesRoute } = pagesRouter?.match(req) || {}; @@ -153,7 +159,7 @@ describe("handleI18n util", () => { }; const req = new Request("https://example.com"); const { response, pagesRouter, rootRouter } = handleI18n( - new RequestContext(req), + extendRequestContext({ originalRequest: req }), ); expect(response).toBeUndefined(); @@ -162,34 +168,36 @@ describe("handleI18n util", () => { }); it("should redirect to default locale if there is no locale in the URL", () => { - const req = new RequestContext(new Request("https://example.com")); + const req = extendRequestContext({ + originalRequest: new Request("https://example.com"), + }); const { response } = handleI18n(req); expect(response?.status).toBe(301); expect(response?.headers.get("location")).toBe("/en/"); }); it("should redirect to the browser language as default locale if there is no locale in the URL", () => { - const req = new RequestContext( - new Request("https://example.com", { + const req = extendRequestContext({ + originalRequest: new Request("https://example.com", { headers: { "Accept-Language": "ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7", }, }), - ); + }); const { response } = handleI18n(req); expect(response?.status).toBe(301); expect(response?.headers.get("location")).toBe("/ru/"); }); it("should redirect to the last browser language as default locale if there is no locale in the URL", () => { - const req = new RequestContext( - new Request("https://example.com", { + const req = extendRequestContext({ + originalRequest: new Request("https://example.com", { headers: { "Accept-Language": "es-ES,es;q=0.9,de-CH;q=0.7,de;q=0.6,pt;q=0.5,ru-RU;q=0.4", }, }), - ); + }); const { response } = handleI18n(req); expect(response?.status).toBe(301); expect(response?.headers.get("location")).toBe("/ru/"); diff --git a/src/utils/handle-i18n/index.ts b/src/utils/handle-i18n/index.ts index b9c80adb8..75629335c 100644 --- a/src/utils/handle-i18n/index.ts +++ b/src/utils/handle-i18n/index.ts @@ -1,9 +1,9 @@ -import { RequestContext } from "../../core"; import getLocaleFromRequest from "../get-locale-from-request"; import getRouteMatcher from "../get-route-matcher"; import getConstants from "../../constants"; import translateCore from "../translate-core"; import adaptRouterToPageTranslations from "../adapt-router-to-page-translations"; +import { RequestContext } from "../../types"; export default function handleI18n(req: RequestContext): { response?: Response; @@ -18,7 +18,7 @@ export default function handleI18n(req: RequestContext): { if (!defaultLocale || !locales?.length) return {}; const locale = getLocaleFromRequest(req); - const url = new URL(req.url); + const url = new URL(req.finalURL); const [, localeFromUrl] = url.pathname.split("/"); const pathname = url.pathname.replace(/\/$/, ""); diff --git a/src/utils/load-layout/index.test.tsx b/src/utils/load-layout/index.test.tsx index 135c7da24..085fa3e10 100644 --- a/src/utils/load-layout/index.test.tsx +++ b/src/utils/load-layout/index.test.tsx @@ -1,13 +1,16 @@ import { describe, it, expect, afterEach } from "bun:test"; import LoadLayout from "."; import path from "node:path"; -import { RequestContext, renderToReadableStream } from "../../core"; +import { renderToReadableStream } from "../../core"; import streamToText from "../../__fixtures__/stream-to-text"; import getImportableFilepath from "../get-importable-filepath"; import getRootDir from "../get-root-dir"; +import extendRequestContext from "../extend-request-context"; const join = path.join; -const testRequest = new RequestContext(new Request("https://test.com")); +const testRequest = extendRequestContext({ + originalRequest: new Request("https://test.com"), +}); describe("utils", () => { afterEach(() => { diff --git a/src/utils/redirect-trailing-slash/index.test.ts b/src/utils/redirect-trailing-slash/index.test.ts index ef0c7f6d1..96cc0748f 100644 --- a/src/utils/redirect-trailing-slash/index.test.ts +++ b/src/utils/redirect-trailing-slash/index.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect, afterEach } from "bun:test"; import redirectTrailingSlash from "."; -import { RequestContext } from "../../core"; +import extendRequestContext from "../extend-request-context"; describe("utils", () => { describe("redirectTrailingSlash", () => { @@ -14,9 +14,9 @@ describe("utils", () => { trailingSlash: false, }, }; - const request = new RequestContext( - new Request("https://example.com/foo/"), - ); + const request = extendRequestContext({ + originalRequest: new Request("https://example.com/foo/"), + }); const response = redirectTrailingSlash(request); expect(response?.status).toBe(301); expect(response?.headers.get("location")).toBe("https://example.com/foo"); @@ -28,11 +28,11 @@ describe("utils", () => { trailingSlash: false, }, }; - const request = new RequestContext( - new Request("https://example.com/"), - ); + const request = extendRequestContext({ + originalRequest: new Request("https://example.com/"), + }); const response = redirectTrailingSlash(request); - expect(response).not.toBeDefined() + expect(response).not.toBeDefined(); }); it("should NOT redirect the home trailingSlash=false + without trailing slash", () => { @@ -41,11 +41,11 @@ describe("utils", () => { trailingSlash: false, }, }; - const request = new RequestContext( - new Request("https://example.com/"), - ); + const request = extendRequestContext({ + originalRequest: new Request("https://example.com/"), + }); const response = redirectTrailingSlash(request); - expect(response).not.toBeDefined() + expect(response).not.toBeDefined(); }); it("should NOT redirect the home trailingSlash=true + trailing slash", () => { @@ -54,11 +54,11 @@ describe("utils", () => { trailingSlash: true, }, }; - const request = new RequestContext( - new Request("https://example.com/"), - ); + const request = extendRequestContext({ + originalRequest: new Request("https://example.com/"), + }); const response = redirectTrailingSlash(request); - expect(response).not.toBeDefined() + expect(response).not.toBeDefined(); }); it("should NOT redirect the home trailingSlash=true + without trailing slash", () => { @@ -67,11 +67,11 @@ describe("utils", () => { trailingSlash: true, }, }; - const request = new RequestContext( - new Request("https://example.com/"), - ); + const request = extendRequestContext({ + originalRequest: new Request("https://example.com/"), + }); const response = redirectTrailingSlash(request); - expect(response).not.toBeDefined() + expect(response).not.toBeDefined(); }); it("should redirect with trailing slash", () => { @@ -80,9 +80,9 @@ describe("utils", () => { trailingSlash: true, }, }; - const request = new RequestContext( - new Request("https://example.com/foo"), - ); + const request = extendRequestContext({ + originalRequest: new Request("https://example.com/foo"), + }); const response = redirectTrailingSlash(request); expect(response?.status).toBe(301); expect(response?.headers.get("location")).toBe( @@ -96,9 +96,9 @@ describe("utils", () => { trailingSlash: true, }, }; - const request = new RequestContext( - new Request("https://example.com/foo/"), - ); + const request = extendRequestContext({ + originalRequest: new Request("https://example.com/foo/"), + }); const response = redirectTrailingSlash(request); expect(response).toBeUndefined(); }); @@ -109,9 +109,9 @@ describe("utils", () => { trailingSlash: false, }, }; - const request = new RequestContext( - new Request("https://example.com/foo"), - ); + const request = extendRequestContext({ + originalRequest: new Request("https://example.com/foo"), + }); const response = redirectTrailingSlash(request); expect(response).toBeUndefined(); }); diff --git a/src/utils/redirect-trailing-slash/index.ts b/src/utils/redirect-trailing-slash/index.ts index 61ff8c85f..ea84612f7 100644 --- a/src/utils/redirect-trailing-slash/index.ts +++ b/src/utils/redirect-trailing-slash/index.ts @@ -1,14 +1,14 @@ -import { RequestContext } from "../../core"; import getConstants from "../../constants"; +import { RequestContext } from "../../types"; export default function redirectTrailingSlash( request: RequestContext, ): Response | undefined { const { CONFIG } = getConstants(); const { trailingSlash } = CONFIG; - const url = new URL(request.url); + const url = new URL(request.finalURL); const { pathname } = url; - const isHome = pathname === "/" + const isHome = pathname === "/"; if (trailingSlash && !pathname.endsWith("/") && !isHome) { return redirect(new URL(pathname + "/", url).toString()); diff --git a/src/utils/render-attributes/index.test.ts b/src/utils/render-attributes/index.test.ts index f70b4fad6..0d80a741e 100644 --- a/src/utils/render-attributes/index.test.ts +++ b/src/utils/render-attributes/index.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect, afterEach } from "bun:test"; import renderAttributes from "."; -import { RequestContext } from "../../core"; import getConstants from "../../constants"; +import extendRequestContext from "../extend-request-context"; describe("utils", () => { describe("renderAttributes", () => { @@ -10,7 +10,9 @@ describe("utils", () => { }); it("should render attributes", () => { - const request = new RequestContext(new Request("https://example.com")); + const request = extendRequestContext({ + originalRequest: new Request("https://example.com"), + }); const attributes = renderAttributes({ props: { foo: "bar", @@ -23,7 +25,9 @@ describe("utils", () => { }); it('should render the "a" href attribute with the locale as prefix', () => { - const request = new RequestContext(new Request("https://example.com/ru")); + const request = extendRequestContext({ + originalRequest: new Request("https://example.com/ru"), + }); request.i18n = { locale: "ru", @@ -44,7 +48,9 @@ describe("utils", () => { }); it('should add the lang attribute in the "html" tag', () => { - const request = new RequestContext(new Request("https://example.com/ru")); + const request = extendRequestContext({ + originalRequest: new Request("https://example.com/ru"), + }); request.i18n = { locale: "ru", @@ -85,7 +91,9 @@ describe("utils", () => { }, }; - const request = new RequestContext(new Request("https://example.com/es")); + const request = extendRequestContext({ + originalRequest: new Request("https://example.com/es"), + }); request.i18n = { locale: "es", @@ -143,7 +151,9 @@ describe("utils", () => { }, }; - const request = new RequestContext(new Request("https://example.com/es")); + const request = extendRequestContext({ + originalRequest: new Request("https://example.com/es"), + }); request.i18n = { locale: "es", @@ -189,7 +199,9 @@ describe("utils", () => { }, }; - const request = new RequestContext(new Request("https://example.com")); + const request = extendRequestContext({ + originalRequest: new Request("https://example.com"), + }); const imgSrc = (src: string) => renderAttributes({ @@ -234,7 +246,9 @@ describe("utils", () => { }, }; - const request = new RequestContext(new Request("https://example.com")); + const request = extendRequestContext({ + originalRequest: new Request("https://example.com"), + }); const imgSrc = (src: string) => renderAttributes({ diff --git a/src/utils/render-attributes/index.ts b/src/utils/render-attributes/index.ts index e2fdc2e61..fe2b939a0 100644 --- a/src/utils/render-attributes/index.ts +++ b/src/utils/render-attributes/index.ts @@ -1,6 +1,5 @@ -import { RequestContext } from "../../core"; import getConstants from "../../constants"; -import { I18nConfig, Props, Translations } from "../../types"; +import { I18nConfig, Props, RequestContext, Translations } from "../../types"; import routeMatchPathname from "../route-match-pathname"; import substituteI18nRouteValues from "../substitute-i18n-route-values";