Skip to content

Commit

Permalink
chore(cloudflare): refactor structure, optimize patterns
Browse files Browse the repository at this point in the history
Refactors server directory structure by creating `entrypoints` directory and moving `server.advanced` and `server.directory` into it. Changes all respective imports to reflect this movement.

The server's code imports from `./util.js` are refactored to `../util.js` due to the server files' move into the `entrypoints` directory.

In `index.ts`, cleans up unused imports, and refactors code by moving utility functions that were previously inline to the new `./utils` directory.

The code refactoring and restructuring allows for easier navigation and enhancement. The server and utility functions separation follows the single responsibility principle, favoring modular design. The pattern optimization aids in a smoother deployment process by eliminating redundant patterns.
  • Loading branch information
alexanderniebuhr committed Sep 26, 2023
1 parent 60684fa commit b1cb44a
Show file tree
Hide file tree
Showing 24 changed files with 286 additions and 331 deletions.
5 changes: 5 additions & 0 deletions .changeset/olive-jeans-reply.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@astrojs/cloudflare': patch
---

Refactor codebase to enhance code readability and structure, to prioritize maintainability for long-term.
206 changes: 77 additions & 129 deletions packages/integrations/cloudflare/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,50 +38,47 @@ export default defineConfig({

## Options

### Mode

`mode: "advanced" | "directory"`
### `mode: "advanced" | "directory"`

default `"advanced"`

Cloudflare Pages has 2 different modes for deploying functions, `advanced` mode which picks up the `_worker.js` in `dist`, or a directory mode where pages will compile the worker out of a functions folder in the project root. For most projects the adapter default of `advanced` will be sufficient; the `dist` folder will contain your compiled project.
Cloudflare Pages has 2 different modes for deploying functions, `advanced` mode which picks up the `_worker.js` in the `dist` folder, or a directory mode where pages will compile the worker out of a `functions` folder in the project root.

#### `mode:directory`

Switching to directory mode allows you to use [pages plugins](https://developers.cloudflare.com/pages/platform/functions/plugins/) such as [Sentry](https://developers.cloudflare.com/pages/platform/functions/plugins/sentry/) or write custom code to enable logging.

```ts
```js
// astro.config.mjs
export default defineConfig({
adapter: cloudflare({ mode: 'directory' }),
});
```

In `directory` mode, the adapter will compile the client-side part of your app the same way as in `advanced` mode by default, but moves the worker script into a `functions` folder in the project root. In this case, the adapter will only ever place a `[[path]].js` in that folder, allowing you to add additional plugins and pages middleware which can be checked into version control.
In `directory` mode, the adapter will compile the client-side part of your app the same way as in `advanced` mode by default, but moves the worker script into a `functions` folder in the project root. In this case, the adapter will only ever place a `[[path]].js` in that folder, allowing you to add additional files manually.

To instead compile a separate bundle for each page, set the `functionPerPath` option in your Cloudflare adapter config. This option requires some manual maintenance of the `functions` folder. Files emitted by Astro will overwrite existing `functions` files with identical names, so you must choose unique file names for each file you manually add. Additionally, the adapter will never empty the `functions` folder of outdated files, so you must clean up the folder manually when you remove pages.

```diff
```js ins={7-8}
// astro.config.mjs
import {defineConfig} from "astro/config";
import cloudflare from '@astrojs/cloudflare';

export default defineConfig({
adapter: cloudflare({
mode: 'directory',
+ functionPerRoute: true
functionPerRoute: true
})
})
```

Note that this adapter does not support using [Cloudflare Pages Middleware](https://developers.cloudflare.com/pages/platform/functions/middleware/). Astro will bundle the [Astro middleware](https://docs.astro.build/en/guides/middleware/) into each page.

### routes.strategy
Astro will bundle the [Astro middleware](https://docs.astro.build/en/guides/middleware/) into each page bundle.

`routes.strategy: "auto" | "include" | "exclude"`
### `routes.strategy: "auto" | "include" | "exclude"`

default `"auto"`

Determines how `routes.json` will be generated if no [custom `_routes.json`](#custom-_routesjson) is provided.
Determines how `routes.json` will be generated if no custom [`_routes.json`](#custom-_routesjson) is provided.

There are three options available:

Expand Down Expand Up @@ -129,56 +126,92 @@ There are three options available:
}
```

### routes.include

`routes.include: string[]`
### `routes.include: string[]`

default `[]`

If you want to use the automatic `_routes.json` generation, but want to include additional routes (e.g. when having custom functions in the `functions` folder), you can use the `routes.include` option to add additional routes to the `include` array.

### routes.exclude

`routes.exclude: string[]`
### `routes.exclude: string[]`

default `[]`

If you want to use the automatic `_routes.json` generation, but want to exclude additional routes, you can use the `routes.exclude` option to add additional routes to the `exclude` array.

The following example automatically generates `_routes.json` while including and excluding additional routes. Note that that is only necessary if you have custom functions in the `functions` folder that are not handled by Astro.

```diff
```js ins={5-9}
// astro.config.mjs
export default defineConfig({
adapter: cloudflare({
mode: 'directory',
+ routes: {
+ strategy: 'include',
+ include: ['/users/*'], // handled by custom function: functions/users/[id].js
+ exclude: ['/users/faq'], // handled by static page: pages/users/faq.astro
+ },
routes: {
strategy: 'include',
include: ['/users/*'], // handled by custom function: functions/users/[id].js
exclude: ['/users/faq'], // handled by static page: pages/users/faq.astro
},
}),
});
```

## Enabling Preview
### `runtime: "off" | "local" | "remote"`

default `"off"`

In order for preview to work you must install `wrangler`
This optional flag enables the Astro dev server to support all [Cloudflare bindings](https://developers.cloudflare.com/pages/platform/functions/bindings), [environment variables](https://developers.cloudflare.com/pages/platform/functions/bindings/#environment-variables), and the [cf object](https://developers.cloudflare.com/workers/runtime-apis/request/#incomingrequestcfproperties), avoiding the need for Wrangler. [Read more](#access-to-the-cloudflare-runtime)

```sh
pnpm install wrangler --save-dev
- `local`: uses bindings mocking and locally static files
- `remote`: uses real remote bindings and a live fetched cf object
- `off`: legacy behaviour, use [`wrangler`](https://developers.cloudflare.com/workers/wrangler/) to preview with runtime

```js
// astro.config.mjs
import { defineConfig } from 'astro/config';
import cloudflare from '@astrojs/cloudflare';

export default defineConfig({
output: 'server',
adapter: cloudflare({
runtime: 'off' | 'local' | 'remote',
}),
});
```

It's then possible to update the preview script in your `package.json` to `"preview": "wrangler pages dev ./dist"`. This will allow you to run your entire application locally with [Wrangler](https://github.com/cloudflare/wrangler2), which supports secrets, environment variables, KV namespaces, Durable Objects and [all other supported Cloudflare bindings](https://developers.cloudflare.com/pages/platform/functions/#adding-bindings).
### `wasmModuleImports: boolean`

default: `false`

Whether or not to import `.wasm` files [directly as ES modules](https://github.com/WebAssembly/esm-integration/tree/main/proposals/esm-integration), using the `.wasm?module` import syntax.

Add `wasmModuleImports: true` to `astro.config.mjs` to enable in both the Cloudflare build and the Astro dev server. [Read more](#use-wasm-modules)

```diff
// astro.config.mjs
import {defineConfig} from "astro/config";
import cloudflare from '@astrojs/cloudflare';

export default defineConfig({
adapter: cloudflare({
+ wasmModuleImports: true
}),
output: 'server'
})
```

## Access to the Cloudflare runtime

You can access all the Cloudflare bindings and environment variables from Astro components and API routes through `Astro.locals`.
You can access the runtime from Astro components and API routes through `Astro.locals`.

### Access

If you're inside an `.astro` file, you access the runtime using the `Astro.locals` global:

```astro
const env = Astro.locals.runtime.env;
---
// src/pages/index.astro
const runtime = Astro.locals.runtime;
---
<div></div>
```

From an endpoint:
Expand All @@ -192,14 +225,14 @@ export function GET(context) {
}
```

Depending on your adapter mode (advanced = worker, directory = pages), the runtime object will look a little different due to differences in the Cloudflare API.
### Typing

If you're using the `advanced` runtime, you can type the `runtime` object as following:
If you're using the `advanced` mode, you can type the `runtime` object as following:

```ts
// src/env.d.ts
/// <reference types="astro/client" />
import type { AdvancedRuntime } from '@astrojs/cloudflare';
type AdvancedRuntime = import('@astrojs/cloudflare').AdvancedRuntime;

type ENV = {
SERVER_URL: string;
Expand All @@ -215,12 +248,12 @@ declare namespace App {
}
```

If you're using the `directory` runtime, you can type the `runtime` object as following:
If you're using the `directory` mode, you can type the `runtime` object as following:

```ts
// src/env.d.ts
/// <reference types="astro/client" />
import type { DirectoryRuntime } from '@astrojs/cloudflare';
type DirectoryRuntime = import('@astrojs/cloudflare').DirectoryRuntime;

type ENV = {
SERVER_URL: string;
Expand All @@ -236,71 +269,16 @@ declare namespace App {
}
```

### Environment Variables

See Cloudflare's documentation for [working with environment variables](https://developers.cloudflare.com/pages/platform/functions/bindings/#environment-variables).

```js
// pages/[id].json.js

export function GET({ params }) {
// Access environment variables per request inside a function
const serverUrl = import.meta.env.SERVER_URL;
const result = await fetch(serverUrl + "/user/" + params.id);
return {
body: await result.text(),
};
}
```

### `cloudflare.runtime`

`runtime: "off" | "local" | "remote"`
default `"off"`

This optional flag enables the Astro dev server to populate environment variables and the Cloudflare Request Object, avoiding the need for Wrangler.

- `local`: environment variables are available, but the request object is populated from a static placeholder value.
- `remote`: environment variables and the live, fetched request object are available.
- `off`: the Astro dev server will populate neither environment variables nor the request object. Use Wrangler to access Cloudflare bindings and environment variables.

```js
// astro.config.mjs
import { defineConfig } from 'astro/config';
import cloudflare from '@astrojs/cloudflare';

export default defineConfig({
output: 'server',
adapter: cloudflare({
runtime: 'off' | 'local' | 'remote',
}),
});
```

## Wasm module imports

`wasmModuleImports: boolean`

default: `false`

Whether or not to import `.wasm` files [directly as ES modules](https://github.com/WebAssembly/esm-integration/tree/main/proposals/esm-integration).
## Headers, Redirects and function invocation routes

Add `wasmModuleImports: true` to `astro.config.mjs` to enable in both the Cloudflare build and the Astro dev server.
Cloudflare has support for adding custom [headers](https://developers.cloudflare.com/pages/platform/headers/), configuring static [redirects](https://developers.cloudflare.com/pages/platform/redirects/) and defining which routes should [invoke functions](https://developers.cloudflare.com/pages/platform/functions/routing/#function-invocation-routes). Cloudflare looks for `_headers`, `_redirects`, and `_routes.json` files in your build output directory to configure these features. This means they should be placed in your Astro project’s `public/` directory.

```diff
// astro.config.mjs
import {defineConfig} from "astro/config";
import cloudflare from '@astrojs/cloudflare';
### Custom `_routes.json`

export default defineConfig({
adapter: cloudflare({
+ wasmModuleImports: true
}),
output: 'server'
})
```
By default, `@astrojs/cloudflare` will generate a `_routes.json` file with `include` and `exclude` rules based on your applications's dynamic and static routes.
This will enable Cloudflare to serve files and process static redirects without a function invocation. Creating a custom `_routes.json` will override this automatic optimization. See [Cloudflare's documentation](https://developers.cloudflare.com/pages/platform/functions/routing/#create-a-_routesjson-file) for more details.

Once enabled, you can import a web assembly module in Astro with a `.wasm?module` import.
## Use Wasm modules

The following is an example of importing a Wasm module that then responds to requests by adding the request's number parameters together.

Expand All @@ -320,17 +298,6 @@ export async function GET(context) {

While this example is trivial, Wasm can be used to accelerate computationally intensive operations which do not involve significant I/O such as embedding an image processing library.

## Headers, Redirects and function invocation routes

Cloudflare has support for adding custom [headers](https://developers.cloudflare.com/pages/platform/headers/), configuring static [redirects](https://developers.cloudflare.com/pages/platform/redirects/) and defining which routes should [invoke functions](https://developers.cloudflare.com/pages/platform/functions/routing/#function-invocation-routes). Cloudflare looks for `_headers`, `_redirects`, and `_routes.json` files in your build output directory to configure these features. This means they should be placed in your Astro project’s `public/` directory.

### Custom `_routes.json`

By default, `@astrojs/cloudflare` will generate a `_routes.json` file with `include` and `exclude` rules based on your applications's dynamic and static routes.
This will enable Cloudflare to serve files and process static redirects without a function invocation. Creating a custom `_routes.json` will override this automatic optimization and, if not configured manually, cause function invocations that will count against the request limits of your Cloudflare plan.

See [Cloudflare's documentation](https://developers.cloudflare.com/pages/platform/functions/routing/#create-a-_routesjson-file) for more details.

## Node.js compatibility

Astro's Cloudflare adapter allows you to use any Node.js runtime API supported by Cloudflare:
Expand All @@ -354,33 +321,14 @@ export const prerender = false;
import { Buffer } from 'node:buffer';
```

Additionally, you'll need to enable the Compatibility Flag in Cloudflare. The configuration for this flag may vary based on where you deploy your Astro site.

For detailed guidance, please refer to the [Cloudflare documentation](https://developers.cloudflare.com/workers/runtime-apis/nodejs).
Additionally, you'll need to enable the Compatibility Flag in Cloudflare. The configuration for this flag may vary based on where you deploy your Astro site. For detailed guidance, please refer to the [Cloudflare documentation](https://developers.cloudflare.com/workers/runtime-apis/nodejs).

## Troubleshooting

For help, check out the `#support` channel on [Discord](https://astro.build/chat). Our friendly Support Squad members are here to help!

You can also check our [Astro Integration Documentation][astro-integration] for more on integrations.

### Meaningful error messages

Currently, errors during running your application in Wrangler are not very useful, due to the minification of your code. For better debugging, you can add `vite.build.minify = false` setting to your `astro.config.js`

```js
export default defineConfig({
adapter: cloudflare(),
output: 'server',

vite: {
build: {
minify: false,
},
},
});
```

## Contributing

This package is maintained by Astro's Core team. You're welcome to submit an issue or PR!
Expand Down
11 changes: 3 additions & 8 deletions packages/integrations/cloudflare/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,12 @@
"homepage": "https://docs.astro.build/en/guides/integrations-guide/cloudflare/",
"exports": {
".": "./dist/index.js",
"./runtime": {
"types": "./runtime.d.ts",
"default": "./dist/runtime.js"
},
"./server.advanced.js": "./dist/server.advanced.js",
"./server.directory.js": "./dist/server.directory.js",
"./entrypoints/server.advanced.js": "./dist/entrypoints/server.advanced.js",
"./entrypoints/server.directory.js": "./dist/entrypoints/server.directory.js",
"./package.json": "./package.json"
},
"files": [
"dist",
"runtime.d.ts"
"dist"
],
"scripts": {
"build": "astro-scripts build \"src/**/*.ts\" && tsc",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Request as CFRequest, ExecutionContext } from '@cloudflare/workers-types';
import type { SSRManifest } from 'astro';
import { App } from 'astro/app';
import { getProcessEnvProxy, isNode } from './util.js';
import { getProcessEnvProxy, isNode } from '../util.js';

if (!isNode) {
process.env = getProcessEnvProxy();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Request as CFRequest, EventContext } from '@cloudflare/workers-types';
import type { SSRManifest } from 'astro';
import { App } from 'astro/app';
import { getProcessEnvProxy, isNode } from './util.js';
import { getProcessEnvProxy, isNode } from '../util.js';

if (!isNode) {
process.env = getProcessEnvProxy();
Expand Down
Loading

0 comments on commit b1cb44a

Please sign in to comment.