From c272a59130102fafe1b5501b2e560dbde0c56b50 Mon Sep 17 00:00:00 2001 From: Reed von Redwitz Date: Wed, 14 Feb 2024 10:53:10 +0100 Subject: [PATCH 1/3] feat: automatically decode url params --- src/server/context.ts | 5 ++- src/server/router.ts | 15 ++++++- src/server/types.ts | 6 +++ tests/fixture/fresh.gen.ts | 2 + tests/fixture/routes/decode-params/[id].tsx | 5 +++ tests/fixture_param_decode/deno.json | 35 ++++++++++++++++ tests/fixture_param_decode/dev.ts | 8 ++++ tests/fixture_param_decode/fresh.config.ts | 3 ++ tests/fixture_param_decode/fresh.gen.ts | 17 ++++++++ tests/fixture_param_decode/main.ts | 13 ++++++ .../routes/decode-params/[id].tsx | 5 +++ tests/main_test.ts | 40 +++++++++++++++++++ 12 files changed, 152 insertions(+), 2 deletions(-) create mode 100644 tests/fixture/routes/decode-params/[id].tsx create mode 100644 tests/fixture_param_decode/deno.json create mode 100755 tests/fixture_param_decode/dev.ts create mode 100644 tests/fixture_param_decode/fresh.config.ts create mode 100644 tests/fixture_param_decode/fresh.gen.ts create mode 100644 tests/fixture_param_decode/main.ts create mode 100644 tests/fixture_param_decode/routes/decode-params/[id].tsx diff --git a/src/server/context.ts b/src/server/context.ts index f76fc97ab4a..757f1125aa2 100644 --- a/src/server/context.ts +++ b/src/server/context.ts @@ -168,7 +168,10 @@ export class ServerContext { const withMiddlewares = composeMiddlewares( this.#extractResult.middlewares, handlers.errorHandler, - router.getParamsAndRoute(handlers), + router.getParamsAndRoute( + handlers, + this.#state.config.router?.automaticallyDecodeUrlParams ?? false, + ), renderNotFound, basePath, ); diff --git a/src/server/router.ts b/src/server/router.ts index ae0a17a72b1..96b83cbb047 100644 --- a/src/server/router.ts +++ b/src/server/router.ts @@ -148,6 +148,7 @@ export function getParamsAndRoute( staticRoutes, routes, }: RouterOptions, + automaticallyDecodeUrlParams: boolean, ): ( url: URL, ) => RouteResult { @@ -189,9 +190,21 @@ export function getParamsAndRoute( const res = route.pattern.exec(url); if (res !== null) { + let decodedParams: Record = {}; + if (automaticallyDecodeUrlParams) { + for (const [key, value] of Object.entries(res.pathname.groups)) { + try { + decodedParams[key] = value ? decodeURIComponent(value) : value; + } catch { + decodedParams[key] = value; + } + } + } else { + decodedParams = res.pathname.groups; + } return { route: route, - params: res.pathname.groups, + params: decodedParams, isPartial, }; } diff --git a/src/server/types.ts b/src/server/types.ts index 492eaf87299..b8293408531 100644 --- a/src/server/types.ts +++ b/src/server/types.ts @@ -142,6 +142,12 @@ export interface RouterOptions { * @default {undefined} */ basePath?: string; + /** + * If true, Fresh will try to automatically decode URL parameters. Instead of receiving `Hello%20World`, your route would receive `Hello World`. + * If decoding a particular parameter fails, it will be left as is. + * @default {false} + */ + automaticallyDecodeUrlParams?: boolean; } export type RenderFunction = ( diff --git a/tests/fixture/fresh.gen.ts b/tests/fixture/fresh.gen.ts index e60cd48d357..fe5c7d304d3 100644 --- a/tests/fixture/fresh.gen.ts +++ b/tests/fixture/fresh.gen.ts @@ -20,6 +20,7 @@ import * as $books_id_ from "./routes/books/[id].tsx"; import * as $connInfo from "./routes/connInfo.ts"; import * as $ctx_config from "./routes/ctx_config.tsx"; import * as $ctx_config_props from "./routes/ctx_config_props.tsx"; +import * as $decode_params_id_ from "./routes/decode-params/[id].tsx"; import * as $error_boundary from "./routes/error_boundary.tsx"; import * as $event_handler_string from "./routes/event_handler_string.tsx"; import * as $event_handler_string_island from "./routes/event_handler_string_island.tsx"; @@ -128,6 +129,7 @@ const manifest = { "./routes/connInfo.ts": $connInfo, "./routes/ctx_config.tsx": $ctx_config, "./routes/ctx_config_props.tsx": $ctx_config_props, + "./routes/decode-params/[id].tsx": $decode_params_id_, "./routes/error_boundary.tsx": $error_boundary, "./routes/event_handler_string.tsx": $event_handler_string, "./routes/event_handler_string_island.tsx": $event_handler_string_island, diff --git a/tests/fixture/routes/decode-params/[id].tsx b/tests/fixture/routes/decode-params/[id].tsx new file mode 100644 index 00000000000..a31229cd06d --- /dev/null +++ b/tests/fixture/routes/decode-params/[id].tsx @@ -0,0 +1,5 @@ +import { defineRoute } from "$fresh/src/server/defines.ts"; + +export default defineRoute((req, ctx) => { + return ctx.params.id; +}); diff --git a/tests/fixture_param_decode/deno.json b/tests/fixture_param_decode/deno.json new file mode 100644 index 00000000000..e34d56c98ba --- /dev/null +++ b/tests/fixture_param_decode/deno.json @@ -0,0 +1,35 @@ +{ + "lock": false, + "tasks": { + "check": "deno fmt --check && deno lint && deno check **/*.ts && deno check **/*.tsx", + "cli": "echo \"import '\\$fresh/src/dev/cli.ts'\" | deno run --unstable -A -", + "manifest": "deno task cli manifest $(pwd)", + "start": "deno run -A --watch=static/,routes/ dev.ts", + "build": "deno run -A dev.ts build", + "preview": "deno run -A main.ts", + "update": "deno run -A -r https://fresh.deno.dev/update ." + }, + "lint": { + "rules": { + "tags": [ + "fresh", + "recommended" + ] + } + }, + "exclude": [ + "**/_fresh/*" + ], + "imports": { + "$fresh/": "file:///Users/reed/code/denoland/fresh/", + "preact": "https://esm.sh/preact@10.19.2", + "preact/": "https://esm.sh/preact@10.19.2/", + "@preact/signals": "https://esm.sh/*@preact/signals@1.2.1", + "@preact/signals-core": "https://esm.sh/*@preact/signals-core@1.5.0", + "$std/": "https://deno.land/std@0.211.0/" + }, + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "preact" + } +} diff --git a/tests/fixture_param_decode/dev.ts b/tests/fixture_param_decode/dev.ts new file mode 100755 index 00000000000..ae73946d7e4 --- /dev/null +++ b/tests/fixture_param_decode/dev.ts @@ -0,0 +1,8 @@ +#!/usr/bin/env -S deno run -A --watch=static/,routes/ + +import dev from "$fresh/dev.ts"; +import config from "./fresh.config.ts"; + +import "$std/dotenv/load.ts"; + +await dev(import.meta.url, "./main.ts", config); diff --git a/tests/fixture_param_decode/fresh.config.ts b/tests/fixture_param_decode/fresh.config.ts new file mode 100644 index 00000000000..0318b8b2d0d --- /dev/null +++ b/tests/fixture_param_decode/fresh.config.ts @@ -0,0 +1,3 @@ +import { defineConfig } from "$fresh/server.ts"; + +export default defineConfig({ router: { automaticallyDecodeUrlParams: true } }); diff --git a/tests/fixture_param_decode/fresh.gen.ts b/tests/fixture_param_decode/fresh.gen.ts new file mode 100644 index 00000000000..d1e93417862 --- /dev/null +++ b/tests/fixture_param_decode/fresh.gen.ts @@ -0,0 +1,17 @@ +// DO NOT EDIT. This file is generated by Fresh. +// This file SHOULD be checked into source version control. +// This file is automatically updated during development when running `dev.ts`. + +import * as $decode_params_id_ from "./routes/decode-params/[id].tsx"; + +import { type Manifest } from "$fresh/server.ts"; + +const manifest = { + routes: { + "./routes/decode-params/[id].tsx": $decode_params_id_, + }, + islands: {}, + baseUrl: import.meta.url, +} satisfies Manifest; + +export default manifest; diff --git a/tests/fixture_param_decode/main.ts b/tests/fixture_param_decode/main.ts new file mode 100644 index 00000000000..675f529bbcc --- /dev/null +++ b/tests/fixture_param_decode/main.ts @@ -0,0 +1,13 @@ +/// +/// +/// +/// +/// + +import "$std/dotenv/load.ts"; + +import { start } from "$fresh/server.ts"; +import manifest from "./fresh.gen.ts"; +import config from "./fresh.config.ts"; + +await start(manifest, config); diff --git a/tests/fixture_param_decode/routes/decode-params/[id].tsx b/tests/fixture_param_decode/routes/decode-params/[id].tsx new file mode 100644 index 00000000000..a31229cd06d --- /dev/null +++ b/tests/fixture_param_decode/routes/decode-params/[id].tsx @@ -0,0 +1,5 @@ +import { defineRoute } from "$fresh/src/server/defines.ts"; + +export default defineRoute((req, ctx) => { + return ctx.params.id; +}); diff --git a/tests/main_test.ts b/tests/main_test.ts index 252ac46b25f..9c5e95168e0 100644 --- a/tests/main_test.ts +++ b/tests/main_test.ts @@ -1235,3 +1235,43 @@ Deno.test("should not be able to override __FRSH_STATE", async () => { assert(!didError); }); }); + +Deno.test("param with encoding -- control", async () => { + await withFakeServe("./tests/fixture/dev.ts", async (server) => { + const doc = await server.getHtml(`/decode-params/Hello%20World`); + assertEquals( + doc.querySelector("body")?.textContent, + "Hello%20World", + ); + }); +}); + +Deno.test("param with bad encoding -- control", async () => { + await withFakeServe("./tests/fixture_param_decode/dev.ts", async (server) => { + const doc = await server.getHtml(`/decode-params/%E0%A4%A`); + assertEquals( + doc.querySelector("body")?.textContent, + "%E0%A4%A", + ); + }); +}); + +Deno.test("param with encoding -- fix", async () => { + await withFakeServe("./tests/fixture_param_decode/dev.ts", async (server) => { + const doc = await server.getHtml(`/decode-params/Hello%20World`); + assertEquals( + doc.querySelector("body")?.textContent, + "Hello World", + ); + }, { loadConfig: true }); +}); + +Deno.test("param with bad encoding -- fix", async () => { + await withFakeServe("./tests/fixture_param_decode/dev.ts", async (server) => { + const doc = await server.getHtml(`/decode-params/%E0%A4%A`); + assertEquals( + doc.querySelector("body")?.textContent, + "%E0%A4%A", + ); + }, { loadConfig: true }); +}); From b34570eef94fc06770bf50347121c4fb7b409023 Mon Sep 17 00:00:00 2001 From: Reed von Redwitz Date: Wed, 14 Feb 2024 11:00:15 +0100 Subject: [PATCH 2/3] update documentation --- docs/canary/concepts/server-configuration.md | 231 +++++++++++++++++++ docs/toc.ts | 2 +- 2 files changed, 232 insertions(+), 1 deletion(-) create mode 100644 docs/canary/concepts/server-configuration.md diff --git a/docs/canary/concepts/server-configuration.md b/docs/canary/concepts/server-configuration.md new file mode 100644 index 00000000000..78ca5d1482c --- /dev/null +++ b/docs/canary/concepts/server-configuration.md @@ -0,0 +1,231 @@ +--- +description: | + The ability to configure the core Fresh server leads to its flexibility. +--- + +In this page we discuss how the server can be configured during startup. + +The signature of the primary method looks like this: + +```ts main.ts +export async function start(manifest: Manifest, config: FreshConfig = {}); +``` + +## Configuration + +`Manifest` comes from `fresh.gen.ts`, so nothing to do there. `config` is where +things get interesting. +[`FreshConfig`](https://deno.land/x/fresh/server.ts?s=FreshConfig) looks like +this: + +```ts +export interface FreshConfig { + build?: { + /** + * The directory to write generated files to when `dev.ts build` is run. + * This can be an absolute path, a file URL or a relative path. + */ + outDir?: string; + /** + * This sets the target environment for the generated code. Newer + * language constructs will be transformed to match the specified + * support range. See https://esbuild.github.io/api/#target + * @default {"es2022"} + */ + target?: string | string[]; + }; + render?: RenderFunction; + plugins?: Plugin[]; + staticDir?: string; + router?: RouterOptions; + server?: Partial; +} +``` + +And for completeness here are the remaining two types: + +```ts +export type RenderFunction = ( + ctx: RenderContext, + render: InnerRenderFunction, +) => void | Promise; + +export interface RouterOptions { + /** + * Controls whether Fresh will append a trailing slash to the URL. + * @default {false} + */ + trailingSlash?: boolean; + /** + * Configures the pattern of files to ignore in islands and routes. + * + * By default Fresh will ignore test files, + * for example files with a `.test.ts` or a `_test.ts` suffix. + * + * @default {/(?:[^/]*_|[^/]*\.|)test\.(?:ts|tsx|mts|js|mjs|jsx|)\/*$/} + */ + ignoreFilePattern?: RegExp; + /** + * Serve fresh from a base path instead of from the root. + * "/foo/bar" -> http://localhost:8000/foo/bar + * @default {undefined} + */ + basePath?: string; + /** + * If true, Fresh will try to automatically decode URL parameters. Instead of receiving `Hello%20World`, your route would receive `Hello World`. + * If decoding a particular parameter fails, it will be left as is. + * @default {false} + */ + automaticallyDecodeUrlParams?: boolean; +} +``` + +## Build + +### outDir + +As the comment suggests, this can be used to configure where generated files are +written: + +```tsx +await dev(import.meta.url, "./main.ts", { + build: { + outDir: Deno.env.get("FRESH_TEST_OUTDIR") ?? undefined, + }, +}); +``` + +### target + +This should be a valid ES Build target. + +```tsx +await dev(import.meta.url, "./main.ts", { + build: { + target: "es2015", + }, +}); +``` + +## Plugins + +See the [docs](/docs/concepts/plugins) on this topic for more detail. But as a +quick example, you can do something like this to load plugins: + +```ts main.ts +await start(manifest, { plugins: [twindPlugin(twindConfig)] }); +``` + +## StaticDir + +This allows you to specify the location where your site's static assets are +stored. Here's an example: + +```ts main.ts +await start(manifest, { staticDir: "./custom_static" }); +``` + +## Render + +This is by far the most complicated option currently available. It allows you to +configure how your components get rendered. + +## RouterOptions + +### TrailingSlash + +By default Fresh uses URLs like `https://www.example.com/about`. If you'd like, +you can configure this to `https://www.example.com/about/` by using the +`trailingSlash` setting. + +```ts main.ts +await start(manifest, { router: { trailingSlash: true } }); +``` + +### ignoreFilePattern + +By default Fresh ignores test files which are co-located next routes and +islands. If you want, you can change the pattern Fresh uses ignore these files + +### basePath + +This setting allows you to serve a Fresh app from sub-path of a domain. A value +of `/foo/bar` would serve the app from `http://localhost:8000/foo/bar` instead +of `http://localhost:8000/` for example. + +The `basePath` will be automatically applied to absolute links in your app. For +example, when the `basePath` is `/foo/bar`, linking to `/about` will +automatically become `/foo/bar/about`. + +```jsx +About; +``` + +Rendered HTML: + +```html +About +``` + +The `basePath` is also applied to the `src` and `srcset` attribute of +``-tags, the `href` attribute of `` and the `src` attribute of +`