summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/integrations/cloudflare/README.md26
-rw-r--r--packages/integrations/cloudflare/package.json8
-rw-r--r--packages/integrations/cloudflare/runtime.d.ts3
-rw-r--r--packages/integrations/cloudflare/src/index.ts149
-rw-r--r--packages/integrations/cloudflare/src/parser.ts134
-rw-r--r--packages/integrations/cloudflare/src/server.advanced.ts13
-rw-r--r--packages/integrations/cloudflare/src/server.directory.ts15
-rw-r--r--packages/integrations/cloudflare/test/cf.test.js38
-rw-r--r--packages/integrations/cloudflare/test/fixtures/cf/.dev.vars1
-rw-r--r--packages/integrations/cloudflare/test/fixtures/cf/astro.config.mjs8
-rw-r--r--packages/integrations/cloudflare/test/fixtures/cf/wrangler.toml4
11 files changed, 350 insertions, 49 deletions
diff --git a/packages/integrations/cloudflare/README.md b/packages/integrations/cloudflare/README.md
index f49824cd9..b627b44d9 100644
--- a/packages/integrations/cloudflare/README.md
+++ b/packages/integrations/cloudflare/README.md
@@ -142,7 +142,7 @@ declare namespace App {
}
```
-## Environment Variables
+### Environment Variables
See Cloudflare's documentation for [working with environment variables](https://developers.cloudflare.com/pages/platform/functions/bindings/#environment-variables).
@@ -159,6 +159,30 @@ export function GET({ params }) {
}
```
+### `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',
+ }),
+});
+```
+
## 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.
diff --git a/packages/integrations/cloudflare/package.json b/packages/integrations/cloudflare/package.json
index e00078a65..79fb83bb7 100644
--- a/packages/integrations/cloudflare/package.json
+++ b/packages/integrations/cloudflare/package.json
@@ -42,7 +42,13 @@
"@astrojs/underscore-redirects": "workspace:*",
"@cloudflare/workers-types": "^4.20230821.0",
"esbuild": "^0.19.2",
- "tiny-glob": "^0.2.9"
+ "tiny-glob": "^0.2.9",
+ "find-up": "^6.3.0",
+ "@iarna/toml": "^2.2.5",
+ "dotenv": "^16.3.1",
+ "@miniflare/cache": "^2.14.1",
+ "@miniflare/shared": "^2.14.1",
+ "@miniflare/storage-memory": "^2.14.1"
},
"peerDependencies": {
"astro": "workspace:^3.0.12"
diff --git a/packages/integrations/cloudflare/runtime.d.ts b/packages/integrations/cloudflare/runtime.d.ts
deleted file mode 100644
index e2a72940a..000000000
--- a/packages/integrations/cloudflare/runtime.d.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export type { WorkerRuntime, PagesRuntime } from './dist/runtime';
-
-export { getRuntime } from './dist/runtime';
diff --git a/packages/integrations/cloudflare/src/index.ts b/packages/integrations/cloudflare/src/index.ts
index 718b1efa8..c70c9c5aa 100644
--- a/packages/integrations/cloudflare/src/index.ts
+++ b/packages/integrations/cloudflare/src/index.ts
@@ -1,18 +1,31 @@
-import { createRedirectsFromAstroRoutes } from '@astrojs/underscore-redirects';
+import type { IncomingRequestCfProperties } from '@cloudflare/workers-types/experimental';
import type { AstroAdapter, AstroConfig, AstroIntegration, RouteData } from 'astro';
+
+import { createRedirectsFromAstroRoutes } from '@astrojs/underscore-redirects';
+import { CacheStorage } from '@miniflare/cache';
+import { NoOpLog } from '@miniflare/shared';
+import { MemoryStorage } from '@miniflare/storage-memory';
+import { AstroError } from 'astro/errors';
import esbuild from 'esbuild';
import * as fs from 'node:fs';
import * as os from 'node:os';
import { sep } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import glob from 'tiny-glob';
+import { getEnvVars } from './parser.js';
export type { AdvancedRuntime } from './server.advanced';
export type { DirectoryRuntime } from './server.directory';
type Options = {
- mode: 'directory' | 'advanced';
+ mode?: 'directory' | 'advanced';
functionPerRoute?: boolean;
+ /**
+ * 'off': current behaviour (wrangler is needed)
+ * 'local': use a static req.cf object, and env vars defined in wrangler.toml & .dev.vars (astro dev is enough)
+ * 'remote': use a dynamic real-live req.cf object, and env vars defined in wrangler.toml & .dev.vars (astro dev is enough)
+ */
+ runtime?: 'off' | 'local' | 'remote';
};
interface BuildConfig {
@@ -22,6 +35,17 @@ interface BuildConfig {
split?: boolean;
}
+class StorageFactory {
+ storages = new Map();
+
+ storage(namespace: string) {
+ let storage = this.storages.get(namespace);
+ if (storage) return storage;
+ this.storages.set(namespace, (storage = new MemoryStorage()));
+ return storage;
+ }
+}
+
export function getAdapter({
isModeDirectory,
functionPerRoute,
@@ -66,6 +90,73 @@ export function getAdapter({
};
}
+async function getCFObject(runtimeMode: string): Promise<IncomingRequestCfProperties | void> {
+ 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: {},
@@ -85,6 +176,7 @@ export default function createIntegration(args?: Options): AstroIntegration {
const isModeDirectory = args?.mode === 'directory';
const functionPerRoute = args?.functionPerRoute ?? false;
+ const runtimeMode = args?.runtime ?? 'off';
return {
name: '@astrojs/cloudflare',
@@ -105,15 +197,56 @@ export default function createIntegration(args?: Options): AstroIntegration {
_buildConfig = config.build;
if (config.output === 'static') {
- throw new Error(`
- [@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.
-
-`);
+ 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) {
- throw new Error(`
- [@astrojs/cloudflare] \`base: "${SERVER_BUILD_FOLDER}"\` is not allowed. Please change your \`base\` config to something else.`);
+ throw new AstroError(
+ '[@astrojs/cloudflare] `base: "${SERVER_BUILD_FOLDER}"` is not allowed. Please change your `base` config to something else.'
+ );
+ }
+ },
+ 'astro:server:setup': ({ server }) => {
+ if (runtimeMode !== 'off') {
+ server.middlewares.use(async function middleware(req, res, next) {
+ try {
+ const cf = await getCFObject(runtimeMode);
+ const vars = await getEnvVars();
+
+ const clientLocalsSymbol = Symbol.for('astro.locals');
+ Reflect.set(req, clientLocalsSymbol, {
+ runtime: {
+ env: {
+ // default binding for static assets will be dynamic once we support mocking of bindings
+ ASSETS: {},
+ // this is just a VAR for CF to change build behavior, on dev it should be 0
+ CF_PAGES: '0',
+ // will be fetched from git dynamically once we support mocking of bindings
+ CF_PAGES_BRANCH: 'TBA',
+ // will be fetched from git dynamically once we support mocking of bindings
+ CF_PAGES_COMMIT_SHA: 'TBA',
+ CF_PAGES_URL: `http://${req.headers.host}`,
+ ...vars,
+ },
+ cf: cf,
+ waitUntil: (_promise: Promise<any>) => {
+ return;
+ },
+ caches: new CacheStorage(
+ { cache: true, cachePersist: false },
+ new NoOpLog(),
+ new StorageFactory(),
+ {}
+ ),
+ },
+ });
+ next();
+ } catch {
+ next();
+ }
+ });
}
},
'astro:build:setup': ({ vite, target }) => {
diff --git a/packages/integrations/cloudflare/src/parser.ts b/packages/integrations/cloudflare/src/parser.ts
new file mode 100644
index 000000000..d7130ff9d
--- /dev/null
+++ b/packages/integrations/cloudflare/src/parser.ts
@@ -0,0 +1,134 @@
+/**
+ * This file is a derivative work of wrangler by Cloudflare
+ * An upstream request for exposing this API was made here:
+ * https://github.com/cloudflare/workers-sdk/issues/3897
+ *
+ * Until further notice, we will be using this file as a workaround
+ * TODO: Tackle this file, once their is an decision on the upstream request
+ */
+
+import * as fs from 'node:fs';
+import { resolve, dirname } from 'node:path';
+import { findUpSync } from 'find-up';
+import TOML from '@iarna/toml';
+import dotenv from 'dotenv';
+
+function findWranglerToml(
+ referencePath: string = process.cwd(),
+ preferJson = false
+): string | undefined {
+ if (preferJson) {
+ return (
+ findUpSync(`wrangler.json`, { cwd: referencePath }) ??
+ findUpSync(`wrangler.toml`, { cwd: referencePath })
+ );
+ }
+ return findUpSync(`wrangler.toml`, { cwd: referencePath });
+}
+type File = {
+ file?: string;
+ fileText?: string;
+};
+type Location = File & {
+ line: number;
+ column: number;
+ length?: number;
+ lineText?: string;
+ suggestion?: string;
+};
+type Message = {
+ text: string;
+ location?: Location;
+ notes?: Message[];
+ kind?: 'warning' | 'error';
+};
+class ParseError extends Error implements Message {
+ readonly text: string;
+ readonly notes: Message[];
+ readonly location?: Location;
+ readonly kind: 'warning' | 'error';
+
+ constructor({ text, notes, location, kind }: Message) {
+ super(text);
+ this.name = this.constructor.name;
+ this.text = text;
+ this.notes = notes ?? [];
+ this.location = location;
+ this.kind = kind ?? 'error';
+ }
+}
+const TOML_ERROR_NAME = 'TomlError';
+const TOML_ERROR_SUFFIX = ' at row ';
+type TomlError = Error & {
+ line: number;
+ col: number;
+};
+function parseTOML(input: string, file?: string): TOML.JsonMap | never {
+ try {
+ // Normalize CRLF to LF to avoid hitting https://github.com/iarna/iarna-toml/issues/33.
+ const normalizedInput = input.replace(/\r\n/g, '\n');
+ return TOML.parse(normalizedInput);
+ } catch (err) {
+ const { name, message, line, col } = err as TomlError;
+ if (name !== TOML_ERROR_NAME) {
+ throw err;
+ }
+ const text = message.substring(0, message.lastIndexOf(TOML_ERROR_SUFFIX));
+ const lineText = input.split('\n')[line];
+ const location = {
+ lineText,
+ line: line + 1,
+ column: col - 1,
+ file,
+ fileText: input,
+ };
+ throw new ParseError({ text, location });
+ }
+}
+
+export interface DotEnv {
+ path: string;
+ parsed: dotenv.DotenvParseOutput;
+}
+function tryLoadDotEnv(path: string): DotEnv | undefined {
+ try {
+ const parsed = dotenv.parse(fs.readFileSync(path));
+ return { path, parsed };
+ } catch (e) {
+ // logger.debug(`Failed to load .env file "${path}":`, e);
+ }
+}
+/**
+ * Loads a dotenv file from <path>, preferring to read <path>.<environment> if
+ * <environment> is defined and that file exists.
+ */
+
+export function loadDotEnv(path: string): DotEnv | undefined {
+ return tryLoadDotEnv(path);
+}
+function getVarsForDev(config: any, configPath: string | undefined): any {
+ const configDir = resolve(dirname(configPath ?? '.'));
+ const devVarsPath = resolve(configDir, '.dev.vars');
+ const loaded = loadDotEnv(devVarsPath);
+ if (loaded !== undefined) {
+ return {
+ ...config.vars,
+ ...loaded.parsed,
+ };
+ } else {
+ return config.vars;
+ }
+}
+export async function getEnvVars() {
+ let rawConfig;
+ const configPath = findWranglerToml(process.cwd(), false); // false = args.experimentalJsonConfig
+ if (!configPath) {
+ throw new Error('Could not find wrangler.toml');
+ }
+ // Load the configuration from disk if available
+ if (configPath?.endsWith('toml')) {
+ rawConfig = parseTOML(fs.readFileSync(configPath).toString(), configPath);
+ }
+ const vars = getVarsForDev(rawConfig, configPath);
+ return vars;
+}
diff --git a/packages/integrations/cloudflare/src/server.advanced.ts b/packages/integrations/cloudflare/src/server.advanced.ts
index 24358a5e0..6e305b1b9 100644
--- a/packages/integrations/cloudflare/src/server.advanced.ts
+++ b/packages/integrations/cloudflare/src/server.advanced.ts
@@ -44,19 +44,6 @@ export function createExports(manifest: SSRManifest) {
request.headers.get('cf-connecting-ip')
);
- // `getRuntime()` is deprecated, currently available additionally to new Astro.locals.runtime
- // TODO: remove `getRuntime()` in Astro 3.0
- Reflect.set(request, Symbol.for('runtime'), {
- env,
- name: 'cloudflare',
- caches,
- cf: request.cf,
- ...context,
- waitUntil: (promise: Promise<any>) => {
- context.waitUntil(promise);
- },
- });
-
const locals: AdvancedRuntime = {
runtime: {
waitUntil: (promise: Promise<any>) => {
diff --git a/packages/integrations/cloudflare/src/server.directory.ts b/packages/integrations/cloudflare/src/server.directory.ts
index 64d820d99..48c97392c 100644
--- a/packages/integrations/cloudflare/src/server.directory.ts
+++ b/packages/integrations/cloudflare/src/server.directory.ts
@@ -21,7 +21,7 @@ export function createExports(manifest: SSRManifest) {
const onRequest = async (context: EventContext<unknown, string, unknown>) => {
const request = context.request as CFRequest & Request;
- const { next, env } = context;
+ const { env } = context;
// TODO: remove this any cast in the future
// REF: the type cast to any is needed because the Cloudflare Env Type is not assignable to type 'ProcessEnv'
@@ -41,19 +41,6 @@ export function createExports(manifest: SSRManifest) {
request.headers.get('cf-connecting-ip')
);
- // `getRuntime()` is deprecated, currently available additionally to new Astro.locals.runtime
- // TODO: remove `getRuntime()` in Astro 3.0
- Reflect.set(request, Symbol.for('runtime'), {
- ...context,
- waitUntil: (promise: Promise<any>) => {
- context.waitUntil(promise);
- },
- name: 'cloudflare',
- next,
- caches,
- cf: request.cf,
- });
-
const locals: DirectoryRuntime = {
runtime: {
waitUntil: (promise: Promise<any>) => {
diff --git a/packages/integrations/cloudflare/test/cf.test.js b/packages/integrations/cloudflare/test/cf.test.js
index 64c406d12..53b1bbf2c 100644
--- a/packages/integrations/cloudflare/test/cf.test.js
+++ b/packages/integrations/cloudflare/test/cf.test.js
@@ -3,7 +3,7 @@ import { expect } from 'chai';
import * as cheerio from 'cheerio';
import cloudflare from '../dist/index.js';
-describe('Cf metadata and caches', () => {
+describe('Wrangler Cloudflare Runtime', () => {
/** @type {import('./test-utils').Fixture} */
let fixture;
/** @type {import('./test-utils').WranglerCLI} */
@@ -39,3 +39,39 @@ describe('Cf metadata and caches', () => {
expect($('#hasCache').text()).to.equal('true');
});
});
+
+describe('Astro Cloudflare Runtime', () => {
+ /** @type {import('./test-utils').Fixture} */
+ let fixture;
+ let devServer;
+
+ before(async () => {
+ fixture = await loadFixture({
+ root: './fixtures/cf/',
+ output: 'server',
+ adapter: cloudflare({
+ runtime: 'local',
+ }),
+ image: {
+ service: {
+ entrypoint: 'astro/assets/services/noop',
+ },
+ },
+ });
+ process.chdir('./test/fixtures/cf');
+ devServer = await fixture.startDevServer();
+ });
+
+ after(async () => {
+ await devServer.stop();
+ });
+
+ it('Populates CF, Vars & Bindings', async () => {
+ let res = await fixture.fetch('/');
+ expect(res.status).to.equal(200);
+ let html = await res.text();
+ let $ = cheerio.load(html);
+ expect($('#hasRuntime').text()).to.equal('true');
+ expect($('#hasCache').text()).to.equal('true');
+ });
+});
diff --git a/packages/integrations/cloudflare/test/fixtures/cf/.dev.vars b/packages/integrations/cloudflare/test/fixtures/cf/.dev.vars
new file mode 100644
index 000000000..9296c384b
--- /dev/null
+++ b/packages/integrations/cloudflare/test/fixtures/cf/.dev.vars
@@ -0,0 +1 @@
+DATABASE_URL="postgresql://lorem"
diff --git a/packages/integrations/cloudflare/test/fixtures/cf/astro.config.mjs b/packages/integrations/cloudflare/test/fixtures/cf/astro.config.mjs
deleted file mode 100644
index f92829843..000000000
--- a/packages/integrations/cloudflare/test/fixtures/cf/astro.config.mjs
+++ /dev/null
@@ -1,8 +0,0 @@
-import { defineConfig } from 'astro/config';
-import cloudflare from '@astrojs/cloudflare';
-
-
-export default defineConfig({
- adapter: cloudflare(),
- output: 'server',
-});
diff --git a/packages/integrations/cloudflare/test/fixtures/cf/wrangler.toml b/packages/integrations/cloudflare/test/fixtures/cf/wrangler.toml
new file mode 100644
index 000000000..ba0fa64c4
--- /dev/null
+++ b/packages/integrations/cloudflare/test/fixtures/cf/wrangler.toml
@@ -0,0 +1,4 @@
+name = "test"
+
+[vars]
+COOL = "ME"