From f6ba533df6861e09d39a29f6ef7a80271bbc8d0a Mon Sep 17 00:00:00 2001 From: Alexander Niebuhr Date: Thu, 28 Sep 2023 18:04:49 +0200 Subject: [PATCH] chore(cloudflare): refactor structure, optimize patterns (#8654) --------- Co-authored-by: Sarah Rainsberger Co-authored-by: 100gle --- .changeset/olive-jeans-reply.md | 5 + packages/integrations/cloudflare/README.md | 212 ++++++++--------- packages/integrations/cloudflare/package.json | 11 +- .../src/{ => entrypoints}/server.advanced.ts | 2 +- .../src/{ => entrypoints}/server.directory.ts | 2 +- .../integrations/cloudflare/src/getAdapter.ts | 40 ++++ packages/integrations/cloudflare/src/index.ts | 221 +++--------------- .../src/utils/deduplicatePatterns.ts | 26 +++ .../cloudflare/src/utils/getCFObject.ts | 70 ++++++ .../cloudflare/src/{ => utils}/parser.ts | 0 .../src/utils/prependForwardSlash.ts | 3 + .../src/utils/rewriteWasmImportPath.ts | 29 +++ .../src/{ => utils}/wasm-module-loader.ts | 0 13 files changed, 313 insertions(+), 308 deletions(-) create mode 100644 .changeset/olive-jeans-reply.md rename packages/integrations/cloudflare/src/{ => entrypoints}/server.advanced.ts (97%) rename packages/integrations/cloudflare/src/{ => entrypoints}/server.directory.ts (97%) create mode 100644 packages/integrations/cloudflare/src/getAdapter.ts create mode 100644 packages/integrations/cloudflare/src/utils/deduplicatePatterns.ts create mode 100644 packages/integrations/cloudflare/src/utils/getCFObject.ts rename packages/integrations/cloudflare/src/{ => utils}/parser.ts (100%) create mode 100644 packages/integrations/cloudflare/src/utils/prependForwardSlash.ts create mode 100644 packages/integrations/cloudflare/src/utils/rewriteWasmImportPath.ts rename packages/integrations/cloudflare/src/{ => utils}/wasm-module-loader.ts (100%) diff --git a/.changeset/olive-jeans-reply.md b/.changeset/olive-jeans-reply.md new file mode 100644 index 000000000000..f719422261f4 --- /dev/null +++ b/.changeset/olive-jeans-reply.md @@ -0,0 +1,5 @@ +--- +'@astrojs/cloudflare': patch +--- + +Refactor codebase to enhance code readability and structure, to prioritize maintainability for long-term. diff --git a/packages/integrations/cloudflare/README.md b/packages/integrations/cloudflare/README.md index 3496f4e9b5e0..c0a967c4a0d9 100644 --- a/packages/integrations/cloudflare/README.md +++ b/packages/integrations/cloudflare/README.md @@ -25,7 +25,7 @@ npm install @astrojs/cloudflare 2. Add the following to your `astro.config.mjs` file: -```diff lang="ts" +```diff lang="js" // astro.config.mjs import { defineConfig } from 'astro/config'; + import cloudflare from '@astrojs/cloudflare'; @@ -38,30 +38,29 @@ npm install @astrojs/cloudflare ## Options -### Mode +### `mode` `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. +This configuration option defines how your Astro project is deployed to Cloudflare Pages. -#### `mode:directory` +- `advanced` mode picks up the `_worker.js` file in the `dist` folder +- `directory` mode picks up the files in the `functions` folder, by default only one `[[path]].js` file is generated -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. +Switching to directory mode allows you to add additional files manually such as [Cloudflare Pages Plugins](https://developers.cloudflare.com/pages/platform/functions/plugins/), [Cloudflare Pages Middleware](https://developers.cloudflare.com/pages/platform/functions/middleware/) or custom functions using [Cloudflare Pages Functions Routing](https://developers.cloudflare.com/pages/platform/functions/routing/). -```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. +To compile a separate bundle for each page, set the `functionPerRoute` option in your Cloudflare adapter config. This option requires some manual maintenance of the `functions` folder. Files emitted by Astro will overwrite existing files with identical names in the `functions` folder, 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. -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 lang="ts" +```diff lang="js" // astro.config.mjs import {defineConfig} from "astro/config"; import cloudflare from '@astrojs/cloudflare'; @@ -74,9 +73,9 @@ To instead compile a separate bundle for each page, set the `functionPerPath` op }) ``` -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. +This adapter doesn't support the [`edgeMiddleware`](https://docs.astro.build/en/reference/adapter-reference/#edgemiddleware) option. -### routes.strategy +### `routes.strategy` `routes.strategy: "auto" | "include" | "exclude"` @@ -130,16 +129,14 @@ There are three options available: } ``` -### routes.include - +### `routes.include` `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` `routes.exclude: string[]` default `[]` @@ -148,7 +145,7 @@ If you want to use the automatic `_routes.json` generation, but want to exclude 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 lang="ts" +```diff lang="js" // astro.config.mjs export default defineConfig({ adapter: cloudflare({ @@ -162,27 +159,71 @@ The following example automatically generates `_routes.json` while including and }); ``` -## Enabling Preview +### `wasmModuleImports` + +`wasmModuleImports: boolean` -In order for preview to work you must install `wrangler` +default: `false` -```sh -pnpm install wrangler --save-dev +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 this functionality in both the Cloudflare build and the Astro dev server. [Read more](#use-wasm-modules) + +```diff lang="js" +// astro.config.mjs +import {defineConfig} from "astro/config"; +import cloudflare from '@astrojs/cloudflare'; + +export default defineConfig({ + adapter: cloudflare({ ++ wasmModuleImports: true + }), + output: 'server' +}) ``` -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). +### `runtime` -## Access to the Cloudflare runtime +`runtime: "off" | "local" | "remote"` -You can access all the Cloudflare bindings and environment variables from Astro components and API routes through `Astro.locals`. +default `"off"` -If you're inside an `.astro` file, you access the runtime using the `Astro.locals` global: +Determines whether and how the Cloudflare Runtime is added to `astro dev`. + +The Cloudflare Runtime includes [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). Read more about [accessing the Cloudflare Runtime](#access-to-the-cloudflare-runtime). + +- `local`: uses bindings mocking and locally static placeholdes +- `remote`: uses remote bindings and a live fetched cf object +- `off`: no access to the Cloudflare runtime using `astro dev`. You can alternatively use [Preview with Wrangler](#preview-with-wrangler) + +```diff lang="js" +// astro.config.mjs +import { defineConfig } from 'astro/config'; +import cloudflare from '@astrojs/cloudflare'; + +export default defineConfig({ + output: 'server', + adapter: cloudflare({ ++ runtime: 'local', + }), +}); +``` + +## Cloudflare runtime + +Gives you access to [environment variables](https://developers.cloudflare.com/pages/platform/functions/bindings/#environment-variables). + +You can access the runtime from Astro components through `Astro.locals` inside any .astro` file. ```astro -const env = Astro.locals.runtime.env; +--- +// src/pages/index.astro +const runtime = Astro.locals.runtime; +--- +
{JSON.stringify(runtime.env)}
``` -From an endpoint: +You can access the runtime from API endpoints through `context.locals`: ```js // src/pages/api/someFile.js @@ -193,21 +234,24 @@ 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 have configured `mode: advanced`, you can type the `runtime` object using `AdvancedRuntime`: ```ts // src/env.d.ts /// -import type { AdvancedRuntime } from '@astrojs/cloudflare'; +type KVNamespace = import('@cloudflare/workers-types/experimental').KVNamespace; type ENV = { SERVER_URL: string; + KV_BINDING: KVNamespace; }; +type Runtime = import('@astrojs/cloudflare').AdvancedRuntime; + declare namespace App { - interface Locals extends AdvancedRuntime { + interface Locals extends Runtime { user: { name: string; surname: string; @@ -216,19 +260,22 @@ declare namespace App { } ``` -If you're using the `directory` runtime, you can type the `runtime` object as following: +If you have configured `mode: directory`, you can type the `runtime` object using `DirectoryRuntime`: ```ts // src/env.d.ts /// -import type { DirectoryRuntime } from '@astrojs/cloudflare'; +type KVNamespace = import('@cloudflare/workers-types/experimental').KVNamespace; type ENV = { SERVER_URL: string; + KV_BINDING: KVNamespace; }; +type Runtime = import('@astrojs/cloudflare').DirectoryRuntime; + declare namespace App { - interface Locals extends DirectoryRuntime { + interface Locals extends Runtime { user: { name: string; surname: string; @@ -237,71 +284,26 @@ declare namespace App { } ``` -### Environment Variables +## Platform -See Cloudflare's documentation for [working with environment variables](https://developers.cloudflare.com/pages/platform/functions/bindings/#environment-variables). +### Headers -```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(), - }; -} -``` +You can attach [custom headers](https://developers.cloudflare.com/pages/platform/headers/) to your responses by adding a `_headers` file in your Astro project's `public/` folder. This file will be copied to your build output directory. -### `cloudflare.runtime` +### Redirects -`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. +You can declare [custom redirects](https://developers.cloudflare.com/pages/platform/redirects/) using Cloudflare Pages. This allows you to redirect requests to a different URL. You can add a `_redirects` file in your Astro project's `public/` folder. This file will be copied to your build output directory. -- `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. +### Routes -```js -// astro.config.mjs -import { defineConfig } from 'astro/config'; -import cloudflare from '@astrojs/cloudflare'; +You can define which routes are invoking functions and which are static assets, using [Cloudflare routing](https://developers.cloudflare.com/pages/platform/functions/routing/#functions-invocation-routes) via a `_routes.json` file. This file is automatically generated by Astro. -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). - -Add `wasmModuleImports: true` to `astro.config.mjs` to enable in both the Cloudflare build and the Astro dev server. - -```diff lang="ts" - // 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 on creating a custom `routes.json`](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. @@ -321,17 +323,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: @@ -355,15 +346,18 @@ 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. +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 on enabling Node.js compatibility](https://developers.cloudflare.com/workers/runtime-apis/nodejs). -For detailed guidance, please refer to the [Cloudflare documentation](https://developers.cloudflare.com/workers/runtime-apis/nodejs). +## Preview with Wrangler -## Troubleshooting +To use [`wrangler`](https://developers.cloudflare.com/workers/wrangler/) to run your application locally, update the preview script: -For help, check out the `#support` channel on [Discord](https://astro.build/chat). Our friendly Support Squad members are here to help! +```json +//package.json +"preview": "wrangler pages dev ./dist" +``` -You can also check our [Astro Integration Documentation][astro-integration] for more on integrations. +[`wrangler`](https://developers.cloudflare.com/workers/wrangler/) gives you access to [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). Getting hot reloading or the astro dev server to work with Wrangler might require custom setup. See [community examples](https://github.com/withastro/roadmap/discussions/590). ### Meaningful error messages @@ -383,6 +377,12 @@ Currently, errors during running your application in Wrangler are not very usefu }); ``` +## 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. + ## Contributing This package is maintained by Astro's Core team. You're welcome to submit an issue or PR! diff --git a/packages/integrations/cloudflare/package.json b/packages/integrations/cloudflare/package.json index cc460ff6496b..88ff252d254d 100644 --- a/packages/integrations/cloudflare/package.json +++ b/packages/integrations/cloudflare/package.json @@ -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", diff --git a/packages/integrations/cloudflare/src/server.advanced.ts b/packages/integrations/cloudflare/src/entrypoints/server.advanced.ts similarity index 97% rename from packages/integrations/cloudflare/src/server.advanced.ts rename to packages/integrations/cloudflare/src/entrypoints/server.advanced.ts index ac6e0fe5547c..957c1791d107 100644 --- a/packages/integrations/cloudflare/src/server.advanced.ts +++ b/packages/integrations/cloudflare/src/entrypoints/server.advanced.ts @@ -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(); diff --git a/packages/integrations/cloudflare/src/server.directory.ts b/packages/integrations/cloudflare/src/entrypoints/server.directory.ts similarity index 97% rename from packages/integrations/cloudflare/src/server.directory.ts rename to packages/integrations/cloudflare/src/entrypoints/server.directory.ts index ffd4ba87a696..3542279b0490 100644 --- a/packages/integrations/cloudflare/src/server.directory.ts +++ b/packages/integrations/cloudflare/src/entrypoints/server.directory.ts @@ -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(); diff --git a/packages/integrations/cloudflare/src/getAdapter.ts b/packages/integrations/cloudflare/src/getAdapter.ts new file mode 100644 index 000000000000..0cc1263a17f1 --- /dev/null +++ b/packages/integrations/cloudflare/src/getAdapter.ts @@ -0,0 +1,40 @@ +import type { AstroAdapter, AstroFeatureMap } from 'astro'; + +export function getAdapter({ + isModeDirectory, + functionPerRoute, +}: { + isModeDirectory: boolean; + functionPerRoute: boolean; +}): AstroAdapter { + const astroFeatures = { + hybridOutput: 'stable', + staticOutput: 'unsupported', + serverOutput: 'stable', + assets: { + supportKind: 'stable', + isSharpCompatible: false, + isSquooshCompatible: false, + }, + } satisfies AstroFeatureMap; + + if (isModeDirectory) { + return { + name: '@astrojs/cloudflare', + serverEntrypoint: '@astrojs/cloudflare/entrypoints/server.directory.js', + exports: ['onRequest', 'manifest'], + adapterFeatures: { + functionPerRoute, + edgeMiddleware: false, + }, + supportedAstroFeatures: astroFeatures, + }; + } + + return { + name: '@astrojs/cloudflare', + serverEntrypoint: '@astrojs/cloudflare/entrypoints/server.advanced.js', + exports: ['default'], + supportedAstroFeatures: astroFeatures, + }; +} diff --git a/packages/integrations/cloudflare/src/index.ts b/packages/integrations/cloudflare/src/index.ts index fa2ea3198645..12ff00a54b04 100644 --- a/packages/integrations/cloudflare/src/index.ts +++ b/packages/integrations/cloudflare/src/index.ts @@ -1,5 +1,4 @@ -import type { IncomingRequestCfProperties } from '@cloudflare/workers-types/experimental'; -import type { AstroAdapter, AstroConfig, AstroIntegration, RouteData } from 'astro'; +import type { AstroConfig, AstroIntegration, RouteData } from 'astro'; import { createRedirectsFromAstroRoutes } from '@astrojs/underscore-redirects'; import { CacheStorage } from '@miniflare/cache'; @@ -9,14 +8,19 @@ import { AstroError } from 'astro/errors'; import esbuild from 'esbuild'; import * as fs from 'node:fs'; import * as os from 'node:os'; -import { basename, dirname, relative, sep } from 'node:path'; +import { dirname, relative, sep } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import glob from 'tiny-glob'; -import { getEnvVars } from './parser.js'; -import { wasmModuleLoader } from './wasm-module-loader.js'; +import { getAdapter } from './getAdapter.js'; +import { deduplicatePatterns } from './utils/deduplicatePatterns.js'; +import { getCFObject } from './utils/getCFObject.js'; +import { getEnvVars } from './utils/parser.js'; +import { prependForwardSlash } from './utils/prependForwardSlash.js'; +import { rewriteWasmImportPath } from './utils/rewriteWasmImportPath.js'; +import { wasmModuleLoader } from './utils/wasm-module-loader.js'; -export type { AdvancedRuntime } from './server.advanced.js'; -export type { DirectoryRuntime } from './server.directory.js'; +export type { AdvancedRuntime } from './entrypoints/server.advanced.js'; +export type { DirectoryRuntime } from './entrypoints/server.directory.js'; type Options = { mode?: 'directory' | 'advanced'; @@ -62,134 +66,13 @@ class StorageFactory { } } -export function getAdapter({ - isModeDirectory, - functionPerRoute, -}: { - isModeDirectory: boolean; - functionPerRoute: boolean; -}): AstroAdapter { - return isModeDirectory - ? { - name: '@astrojs/cloudflare', - serverEntrypoint: '@astrojs/cloudflare/server.directory.js', - exports: ['onRequest', 'manifest'], - adapterFeatures: { - functionPerRoute, - edgeMiddleware: false, - }, - supportedAstroFeatures: { - hybridOutput: 'stable', - staticOutput: 'unsupported', - serverOutput: 'stable', - assets: { - supportKind: 'stable', - isSharpCompatible: false, - isSquooshCompatible: false, - }, - }, - } - : { - name: '@astrojs/cloudflare', - serverEntrypoint: '@astrojs/cloudflare/server.advanced.js', - exports: ['default'], - supportedAstroFeatures: { - hybridOutput: 'stable', - staticOutput: 'unsupported', - serverOutput: 'stable', - assets: { - supportKind: 'stable', - isSharpCompatible: false, - isSquooshCompatible: false, - }, - }, - }; -} - -async function getCFObject(runtimeMode: string): Promise { - const CF_ENDPOINT = 'https://workers.cloudflare.com/cf.json'; - const CF_FALLBACK: IncomingRequestCfProperties = { - asOrganization: '', - asn: 395747, - colo: 'DFW', - city: 'Austin', - region: 'Texas', - regionCode: 'TX', - metroCode: '635', - postalCode: '78701', - country: 'US', - continent: 'NA', - timezone: 'America/Chicago', - latitude: '30.27130', - longitude: '-97.74260', - clientTcpRtt: 0, - httpProtocol: 'HTTP/1.1', - requestPriority: 'weight=192;exclusive=0', - tlsCipher: 'AEAD-AES128-GCM-SHA256', - tlsVersion: 'TLSv1.3', - tlsClientAuth: { - certPresented: '0', - certVerified: 'NONE', - certRevoked: '0', - certIssuerDN: '', - certSubjectDN: '', - certIssuerDNRFC2253: '', - certSubjectDNRFC2253: '', - certIssuerDNLegacy: '', - certSubjectDNLegacy: '', - certSerial: '', - certIssuerSerial: '', - certSKI: '', - certIssuerSKI: '', - certFingerprintSHA1: '', - certFingerprintSHA256: '', - certNotBefore: '', - certNotAfter: '', - }, - edgeRequestKeepAliveStatus: 0, - hostMetadata: undefined, - clientTrustScore: 99, - botManagement: { - corporateProxy: false, - verifiedBot: false, - ja3Hash: '25b4882c2bcb50cd6b469ff28c596742', - staticResource: false, - detectionIds: [], - score: 99, - }, - }; - - if (runtimeMode === 'local') { - return CF_FALLBACK; - } else if (runtimeMode === 'remote') { - try { - const res = await fetch(CF_ENDPOINT); - const cfText = await res.text(); - const storedCf = JSON.parse(cfText); - return storedCf; - } catch (e: any) { - return CF_FALLBACK; - } - } -} - -const SHIM = `globalThis.process = { - argv: [], - env: {}, -};`; - -const SERVER_BUILD_FOLDER = '/$server_build/'; - -/** - * These route types are candiates for being part of the `_routes.json` `include` array. - */ -const potentialFunctionRouteTypes = ['endpoint', 'page']; - export default function createIntegration(args?: Options): AstroIntegration { let _config: AstroConfig; let _buildConfig: BuildConfig; let _entryPoints = new Map(); + const SERVER_BUILD_FOLDER = '/$server_build/'; + const isModeDirectory = args?.mode === 'directory'; const functionPerRoute = args?.functionPerRoute ?? false; const runtimeMode = args?.runtime ?? 'off'; @@ -221,13 +104,13 @@ export default function createIntegration(args?: Options): AstroIntegration { _config = config; _buildConfig = config.build; - if (config.output === 'static') { + if (_config.output === 'static') { throw new AstroError( '[@astrojs/cloudflare] `output: "server"` or `output: "hybrid"` is required to use this adapter. Otherwise, this adapter is not necessary to deploy a static site to Cloudflare.' ); } - if (config.base === SERVER_BUILD_FOLDER) { + if (_config.base === SERVER_BUILD_FOLDER) { throw new AstroError( '[@astrojs/cloudflare] `base: "${SERVER_BUILD_FOLDER}"` is not allowed. Please change your `base` config to something else.' ); @@ -372,7 +255,10 @@ export default function createIntegration(args?: Options): AstroIntegration { bundle: true, minify: _config.vite?.build?.minify !== false, banner: { - js: SHIM, + js: `globalThis.process = { + argv: [], + env: {}, + };`, }, logOverride: { 'ignored-bare-import': 'silent', @@ -449,7 +335,10 @@ export default function createIntegration(args?: Options): AstroIntegration { bundle: true, minify: _config.vite?.build?.minify !== false, banner: { - js: SHIM, + js: `globalThis.process = { + argv: [], + env: {}, + };`, }, logOverride: { 'ignored-bare-import': 'silent', @@ -506,10 +395,14 @@ export default function createIntegration(args?: Options): AstroIntegration { // this creates a _routes.json, in case there is none present to enable // cloudflare to handle static files and support _redirects configuration - // (without calling the function) if (!routesExists) { + /** + * These route types are candiates for being part of the `_routes.json` `include` array. + */ + const potentialFunctionRouteTypes = ['endpoint', 'page']; + const functionEndpoints = routes - // Certain route types, when their prerender option is set to false, a run on the server as function invocations + // Certain route types, when their prerender option is set to false, run on the server as function invocations .filter((route) => potentialFunctionRouteTypes.includes(route.type) && !route.prerender) .map((route) => { const includePattern = @@ -672,59 +565,3 @@ export default function createIntegration(args?: Options): AstroIntegration { }, }; } - -function prependForwardSlash(path: string) { - return path[0] === '/' ? path : '/' + path; -} - -/** - * Remove duplicates and redundant patterns from an `include` or `exclude` list. - * Otherwise Cloudflare will throw an error on deployment. Plus, it saves more entries. - * E.g. `['/foo/*', '/foo/*', '/foo/bar'] => ['/foo/*']` - * @param patterns a list of `include` or `exclude` patterns - * @returns a deduplicated list of patterns - */ -function deduplicatePatterns(patterns: string[]) { - const openPatterns: RegExp[] = []; - - return [...new Set(patterns)] - .sort((a, b) => a.length - b.length) - .filter((pattern) => { - if (openPatterns.some((p) => p.test(pattern))) { - return false; - } - - if (pattern.endsWith('*')) { - openPatterns.push(new RegExp(`^${pattern.replace(/(\*\/)*\*$/g, '.*')}`)); - } - - return true; - }); -} - -/** - * - * @param relativePathToAssets - relative path from the final location for the current esbuild output bundle, to the assets directory. - */ -function rewriteWasmImportPath({ - relativePathToAssets, -}: { - relativePathToAssets: string; -}): esbuild.Plugin { - return { - name: 'wasm-loader', - setup(build) { - build.onResolve({ filter: /.*\.wasm.mjs$/ }, (args) => { - const updatedPath = [ - relativePathToAssets.replaceAll('\\', '/'), - basename(args.path).replace(/\.mjs$/, ''), - ].join('/'); - - return { - path: updatedPath, // change the reference to the changed module - external: true, // mark it as external in the bundle - }; - }); - }, - }; -} diff --git a/packages/integrations/cloudflare/src/utils/deduplicatePatterns.ts b/packages/integrations/cloudflare/src/utils/deduplicatePatterns.ts new file mode 100644 index 000000000000..37743fe55305 --- /dev/null +++ b/packages/integrations/cloudflare/src/utils/deduplicatePatterns.ts @@ -0,0 +1,26 @@ +/** + * Remove duplicates and redundant patterns from an `include` or `exclude` list. + * Otherwise Cloudflare will throw an error on deployment. Plus, it saves more entries. + * E.g. `['/foo/*', '/foo/*', '/foo/bar'] => ['/foo/*']` + * @param patterns a list of `include` or `exclude` patterns + * @returns a deduplicated list of patterns + */ +export function deduplicatePatterns(patterns: string[]) { + const openPatterns: RegExp[] = []; + + // A value in the set may only occur once; it is unique in the set's collection. + // ref: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set + return [...new Set(patterns)] + .sort((a, b) => a.length - b.length) + .filter((pattern) => { + if (openPatterns.some((p) => p.test(pattern))) { + return false; + } + + if (pattern.endsWith('*')) { + openPatterns.push(new RegExp(`^${pattern.replace(/(\*\/)*\*$/g, '.*')}`)); + } + + return true; + }); +} diff --git a/packages/integrations/cloudflare/src/utils/getCFObject.ts b/packages/integrations/cloudflare/src/utils/getCFObject.ts new file mode 100644 index 000000000000..7a4cd8a0cab7 --- /dev/null +++ b/packages/integrations/cloudflare/src/utils/getCFObject.ts @@ -0,0 +1,70 @@ +import type { IncomingRequestCfProperties } from '@cloudflare/workers-types/experimental'; + +export async function getCFObject( + runtimeMode: string +): Promise { + const CF_ENDPOINT = 'https://workers.cloudflare.com/cf.json'; + const CF_FALLBACK: IncomingRequestCfProperties = { + asOrganization: '', + asn: 395747, + colo: 'DFW', + city: 'Austin', + region: 'Texas', + regionCode: 'TX', + metroCode: '635', + postalCode: '78701', + country: 'US', + continent: 'NA', + timezone: 'America/Chicago', + latitude: '30.27130', + longitude: '-97.74260', + clientTcpRtt: 0, + httpProtocol: 'HTTP/1.1', + requestPriority: 'weight=192;exclusive=0', + tlsCipher: 'AEAD-AES128-GCM-SHA256', + tlsVersion: 'TLSv1.3', + tlsClientAuth: { + certPresented: '0', + certVerified: 'NONE', + certRevoked: '0', + certIssuerDN: '', + certSubjectDN: '', + certIssuerDNRFC2253: '', + certSubjectDNRFC2253: '', + certIssuerDNLegacy: '', + certSubjectDNLegacy: '', + certSerial: '', + certIssuerSerial: '', + certSKI: '', + certIssuerSKI: '', + certFingerprintSHA1: '', + certFingerprintSHA256: '', + certNotBefore: '', + certNotAfter: '', + }, + edgeRequestKeepAliveStatus: 0, + hostMetadata: undefined, + clientTrustScore: 99, + botManagement: { + corporateProxy: false, + verifiedBot: false, + ja3Hash: '25b4882c2bcb50cd6b469ff28c596742', + staticResource: false, + detectionIds: [], + score: 99, + }, + }; + + if (runtimeMode === 'local') { + return CF_FALLBACK; + } else if (runtimeMode === 'remote') { + try { + const res = await fetch(CF_ENDPOINT); + const cfText = await res.text(); + const storedCf = JSON.parse(cfText); + return storedCf; + } catch (e: any) { + return CF_FALLBACK; + } + } +} diff --git a/packages/integrations/cloudflare/src/parser.ts b/packages/integrations/cloudflare/src/utils/parser.ts similarity index 100% rename from packages/integrations/cloudflare/src/parser.ts rename to packages/integrations/cloudflare/src/utils/parser.ts diff --git a/packages/integrations/cloudflare/src/utils/prependForwardSlash.ts b/packages/integrations/cloudflare/src/utils/prependForwardSlash.ts new file mode 100644 index 000000000000..b66b588f384b --- /dev/null +++ b/packages/integrations/cloudflare/src/utils/prependForwardSlash.ts @@ -0,0 +1,3 @@ +export function prependForwardSlash(path: string) { + return path[0] === '/' ? path : '/' + path; +} diff --git a/packages/integrations/cloudflare/src/utils/rewriteWasmImportPath.ts b/packages/integrations/cloudflare/src/utils/rewriteWasmImportPath.ts new file mode 100644 index 000000000000..ada19bb56be2 --- /dev/null +++ b/packages/integrations/cloudflare/src/utils/rewriteWasmImportPath.ts @@ -0,0 +1,29 @@ +import esbuild from 'esbuild'; +import { basename } from 'node:path'; + +/** + * + * @param relativePathToAssets - relative path from the final location for the current esbuild output bundle, to the assets directory. + */ +export function rewriteWasmImportPath({ + relativePathToAssets, +}: { + relativePathToAssets: string; +}): esbuild.Plugin { + return { + name: 'wasm-loader', + setup(build) { + build.onResolve({ filter: /.*\.wasm.mjs$/ }, (args) => { + const updatedPath = [ + relativePathToAssets.replaceAll('\\', '/'), + basename(args.path).replace(/\.mjs$/, ''), + ].join('/'); + + return { + path: updatedPath, + external: true, // mark it as external in the bundle + }; + }); + }, + }; +} diff --git a/packages/integrations/cloudflare/src/wasm-module-loader.ts b/packages/integrations/cloudflare/src/utils/wasm-module-loader.ts similarity index 100% rename from packages/integrations/cloudflare/src/wasm-module-loader.ts rename to packages/integrations/cloudflare/src/utils/wasm-module-loader.ts