Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: automatically decode url params #2311

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
231 changes: 231 additions & 0 deletions docs/canary/concepts/server-configuration.md
Original file line number Diff line number Diff line change
@@ -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<Deno.ServeTlsOptions>;
}
```

And for completeness here are the remaining two types:

```ts
export type RenderFunction = (
ctx: RenderContext,
render: InnerRenderFunction,
) => void | Promise<void>;

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
<a href="/about">About</a>;
```

Rendered HTML:

```html
<a href="/foo/bar/about">About</a>
```

The `basePath` is also applied to the `src` and `srcset` attribute of
`<img>`-tags, the `href` attribute of `<link>` and the `src` attribute of
`<script>` tags.

### automaticallyDecodeUrlParams

This is useful if you are tired of writing handlers like this:

```ts
export const handler: Handlers<ProductsPageProps> = {
GET(_req, ctx) {
const productName = decodeURIComponent(ctx.params["product"]);
```

Setting this flag will cause Fresh to try to decode the parameters for you. If
it fails, you will receive the encoded value for that particular parameter.

## Server

Now that Deno has stabilized [Deno.serve](https://deno.land/api?s=Deno.serve)
and Fresh has switched to using this API, all server configuration options are
embedded in `server` inside the `FreshConfig`. The fully expanded set of
parameters looks like this:

```ts
server: {
/** Server private key in PEM format */
cert: string;

/** Cert chain in PEM format */
key: string;

/** The port to listen on.
*
* @default {8000} */
port?: number;

/** A literal IP address or host name that can be resolved to an IP address.
*
* __Note about `0.0.0.0`__ While listening `0.0.0.0` works on all platforms,
* the browsers on Windows don't work with the address `0.0.0.0`.
* You should show the message like `server running on localhost:8080` instead of
* `server running on 0.0.0.0:8080` if your program supports Windows.
*
* @default {"0.0.0.0"} */
hostname?: string;

/** An {@linkcode AbortSignal} to close the server and all connections. */
signal?: AbortSignal;

/** Sets `SO_REUSEPORT` on POSIX systems. */
reusePort?: boolean;

/** The handler to invoke when route handlers throw an error. */
onError?: (error: unknown) => Response | Promise<Response>;

/** The callback which is called when the server starts listening. */
onListen?: (params: { hostname: string; port: number }) => void;
}
```

Use these to configure your server as you see fit.
2 changes: 1 addition & 1 deletion docs/toc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ const toc: RawTableOfContents = {
["deployment", "Deployment", "link:latest"],
["plugins", "Plugins", "link:latest"],
["updating", "Updating Fresh", "link:latest"],
["server-configuration", "Server configuration", "link:latest"],
["server-configuration", "Server configuration", "link:canary"],
],
},
integrations: {
Expand Down
5 changes: 4 additions & 1 deletion src/server/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
);
Expand Down
15 changes: 14 additions & 1 deletion src/server/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ export function getParamsAndRoute(
staticRoutes,
routes,
}: RouterOptions,
automaticallyDecodeUrlParams: boolean,
): (
url: URL,
) => RouteResult {
Expand Down Expand Up @@ -189,9 +190,21 @@ export function getParamsAndRoute(
const res = route.pattern.exec(url);

if (res !== null) {
let decodedParams: Record<string, string | undefined> = {};
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,
};
}
Expand Down
6 changes: 6 additions & 0 deletions src/server/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (
Expand Down
2 changes: 2 additions & 0 deletions tests/fixture/fresh.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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,
Expand Down
5 changes: 5 additions & 0 deletions tests/fixture/routes/decode-params/[id].tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { defineRoute } from "$fresh/src/server/defines.ts";

export default defineRoute((req, ctx) => {
return ctx.params.id;
});
35 changes: 35 additions & 0 deletions tests/fixture_param_decode/deno.json
Original file line number Diff line number Diff line change
@@ -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/[email protected]",
"preact/": "https://esm.sh/[email protected]/",
"@preact/signals": "https://esm.sh/*@preact/[email protected]",
"@preact/signals-core": "https://esm.sh/*@preact/[email protected]",
"$std/": "https://deno.land/[email protected]/"
},
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "preact"
}
}
8 changes: 8 additions & 0 deletions tests/fixture_param_decode/dev.ts
Original file line number Diff line number Diff line change
@@ -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);
3 changes: 3 additions & 0 deletions tests/fixture_param_decode/fresh.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { defineConfig } from "$fresh/server.ts";

export default defineConfig({ router: { automaticallyDecodeUrlParams: true } });
17 changes: 17 additions & 0 deletions tests/fixture_param_decode/fresh.gen.ts
Original file line number Diff line number Diff line change
@@ -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;
13 changes: 13 additions & 0 deletions tests/fixture_param_decode/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/// <reference no-default-lib="true" />
/// <reference lib="dom" />
/// <reference lib="dom.iterable" />
/// <reference lib="dom.asynciterable" />
/// <reference lib="deno.ns" />

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);
Loading
Loading