@astrojs/cloudflare 7.0.2 → 7.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -115,8 +115,12 @@ If you're using the `advanced` runtime, you can type the `runtime` object as fol
115
115
  /// <reference types="astro/client" />
116
116
  import type { AdvancedRuntime } from '@astrojs/cloudflare';
117
117
 
118
+ type ENV = {
119
+ SERVER_URL: string;
120
+ };
121
+
118
122
  declare namespace App {
119
- interface Locals extends AdvancedRuntime {
123
+ interface Locals extends AdvancedRuntime<ENV> {
120
124
  user: {
121
125
  name: string;
122
126
  surname: string;
@@ -132,8 +136,12 @@ If you're using the `directory` runtime, you can type the `runtime` object as fo
132
136
  /// <reference types="astro/client" />
133
137
  import type { DirectoryRuntime } from '@astrojs/cloudflare';
134
138
 
139
+ type ENV = {
140
+ SERVER_URL: string;
141
+ };
142
+
135
143
  declare namespace App {
136
- interface Locals extends DirectoryRuntime {
144
+ interface Locals extends DirectoryRuntime<ENV> {
137
145
  user: {
138
146
  name: string;
139
147
  surname: string;
@@ -142,7 +150,7 @@ declare namespace App {
142
150
  }
143
151
  ```
144
152
 
145
- ## Environment Variables
153
+ ### Environment Variables
146
154
 
147
155
  See Cloudflare's documentation for [working with environment variables](https://developers.cloudflare.com/pages/platform/functions/bindings/#environment-variables).
148
156
 
@@ -159,6 +167,30 @@ export function GET({ params }) {
159
167
  }
160
168
  ```
161
169
 
170
+ ### `cloudflare.runtime`
171
+
172
+ `runtime: "off" | "local" | "remote"`
173
+ default `"off"`
174
+
175
+ This optional flag enables the Astro dev server to populate environment variables and the Cloudflare Request Object, avoiding the need for Wrangler.
176
+
177
+ - `local`: environment variables are available, but the request object is populated from a static placeholder value.
178
+ - `remote`: environment variables and the live, fetched request object are available.
179
+ - `off`: the Astro dev server will populate neither environment variables nor the request object. Use Wrangler to access Cloudflare bindings and environment variables.
180
+
181
+ ```js
182
+ // astro.config.mjs
183
+ import { defineConfig } from 'astro/config';
184
+ import cloudflare from '@astrojs/cloudflare';
185
+
186
+ export default defineConfig({
187
+ output: 'server',
188
+ adapter: cloudflare({
189
+ runtime: 'off' | 'local' | 'remote',
190
+ }),
191
+ });
192
+ ```
193
+
162
194
  ## Headers, Redirects and function invocation routes
163
195
 
164
196
  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.
package/dist/index.d.ts CHANGED
@@ -1,9 +1,15 @@
1
1
  import type { AstroAdapter, AstroIntegration } from 'astro';
2
- export type { AdvancedRuntime } from './server.advanced';
3
- export type { DirectoryRuntime } from './server.directory';
2
+ export type { AdvancedRuntime } from './server.advanced.js';
3
+ export type { DirectoryRuntime } from './server.directory.js';
4
4
  type Options = {
5
- mode: 'directory' | 'advanced';
5
+ mode?: 'directory' | 'advanced';
6
6
  functionPerRoute?: boolean;
7
+ /**
8
+ * 'off': current behaviour (wrangler is needed)
9
+ * 'local': use a static req.cf object, and env vars defined in wrangler.toml & .dev.vars (astro dev is enough)
10
+ * 'remote': use a dynamic real-live req.cf object, and env vars defined in wrangler.toml & .dev.vars (astro dev is enough)
11
+ */
12
+ runtime?: 'off' | 'local' | 'remote';
7
13
  };
8
14
  export declare function getAdapter({ isModeDirectory, functionPerRoute, }: {
9
15
  isModeDirectory: boolean;
package/dist/index.js CHANGED
@@ -1,10 +1,25 @@
1
1
  import { createRedirectsFromAstroRoutes } from "@astrojs/underscore-redirects";
2
+ import { CacheStorage } from "@miniflare/cache";
3
+ import { NoOpLog } from "@miniflare/shared";
4
+ import { MemoryStorage } from "@miniflare/storage-memory";
5
+ import { AstroError } from "astro/errors";
2
6
  import esbuild from "esbuild";
3
7
  import * as fs from "node:fs";
4
8
  import * as os from "node:os";
5
9
  import { sep } from "node:path";
6
10
  import { fileURLToPath, pathToFileURL } from "node:url";
7
11
  import glob from "tiny-glob";
12
+ import { getEnvVars } from "./parser.js";
13
+ class StorageFactory {
14
+ storages = /* @__PURE__ */ new Map();
15
+ storage(namespace) {
16
+ let storage = this.storages.get(namespace);
17
+ if (storage)
18
+ return storage;
19
+ this.storages.set(namespace, storage = new MemoryStorage());
20
+ return storage;
21
+ }
22
+ }
8
23
  function getAdapter({
9
24
  isModeDirectory,
10
25
  functionPerRoute
@@ -43,6 +58,71 @@ function getAdapter({
43
58
  }
44
59
  };
45
60
  }
61
+ async function getCFObject(runtimeMode) {
62
+ const CF_ENDPOINT = "https://workers.cloudflare.com/cf.json";
63
+ const CF_FALLBACK = {
64
+ asOrganization: "",
65
+ asn: 395747,
66
+ colo: "DFW",
67
+ city: "Austin",
68
+ region: "Texas",
69
+ regionCode: "TX",
70
+ metroCode: "635",
71
+ postalCode: "78701",
72
+ country: "US",
73
+ continent: "NA",
74
+ timezone: "America/Chicago",
75
+ latitude: "30.27130",
76
+ longitude: "-97.74260",
77
+ clientTcpRtt: 0,
78
+ httpProtocol: "HTTP/1.1",
79
+ requestPriority: "weight=192;exclusive=0",
80
+ tlsCipher: "AEAD-AES128-GCM-SHA256",
81
+ tlsVersion: "TLSv1.3",
82
+ tlsClientAuth: {
83
+ certPresented: "0",
84
+ certVerified: "NONE",
85
+ certRevoked: "0",
86
+ certIssuerDN: "",
87
+ certSubjectDN: "",
88
+ certIssuerDNRFC2253: "",
89
+ certSubjectDNRFC2253: "",
90
+ certIssuerDNLegacy: "",
91
+ certSubjectDNLegacy: "",
92
+ certSerial: "",
93
+ certIssuerSerial: "",
94
+ certSKI: "",
95
+ certIssuerSKI: "",
96
+ certFingerprintSHA1: "",
97
+ certFingerprintSHA256: "",
98
+ certNotBefore: "",
99
+ certNotAfter: ""
100
+ },
101
+ edgeRequestKeepAliveStatus: 0,
102
+ hostMetadata: void 0,
103
+ clientTrustScore: 99,
104
+ botManagement: {
105
+ corporateProxy: false,
106
+ verifiedBot: false,
107
+ ja3Hash: "25b4882c2bcb50cd6b469ff28c596742",
108
+ staticResource: false,
109
+ detectionIds: [],
110
+ score: 99
111
+ }
112
+ };
113
+ if (runtimeMode === "local") {
114
+ return CF_FALLBACK;
115
+ } else if (runtimeMode === "remote") {
116
+ try {
117
+ const res = await fetch(CF_ENDPOINT);
118
+ const cfText = await res.text();
119
+ const storedCf = JSON.parse(cfText);
120
+ return storedCf;
121
+ } catch (e) {
122
+ return CF_FALLBACK;
123
+ }
124
+ }
125
+ }
46
126
  const SHIM = `globalThis.process = {
47
127
  argv: [],
48
128
  env: {},
@@ -55,6 +135,7 @@ function createIntegration(args) {
55
135
  let _entryPoints = /* @__PURE__ */ new Map();
56
136
  const isModeDirectory = args?.mode === "directory";
57
137
  const functionPerRoute = args?.functionPerRoute ?? false;
138
+ const runtimeMode = args?.runtime ?? "off";
58
139
  return {
59
140
  name: "@astrojs/cloudflare",
60
141
  hooks: {
@@ -73,14 +154,54 @@ function createIntegration(args) {
73
154
  _config = config;
74
155
  _buildConfig = config.build;
75
156
  if (config.output === "static") {
76
- throw new Error(`
77
- [@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.
78
-
79
- `);
157
+ throw new AstroError(
158
+ '[@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.'
159
+ );
80
160
  }
81
161
  if (config.base === SERVER_BUILD_FOLDER) {
82
- throw new Error(`
83
- [@astrojs/cloudflare] \`base: "${SERVER_BUILD_FOLDER}"\` is not allowed. Please change your \`base\` config to something else.`);
162
+ throw new AstroError(
163
+ '[@astrojs/cloudflare] `base: "${SERVER_BUILD_FOLDER}"` is not allowed. Please change your `base` config to something else.'
164
+ );
165
+ }
166
+ },
167
+ "astro:server:setup": ({ server }) => {
168
+ if (runtimeMode !== "off") {
169
+ server.middlewares.use(async function middleware(req, res, next) {
170
+ try {
171
+ const cf = await getCFObject(runtimeMode);
172
+ const vars = await getEnvVars();
173
+ const clientLocalsSymbol = Symbol.for("astro.locals");
174
+ Reflect.set(req, clientLocalsSymbol, {
175
+ runtime: {
176
+ env: {
177
+ // default binding for static assets will be dynamic once we support mocking of bindings
178
+ ASSETS: {},
179
+ // this is just a VAR for CF to change build behavior, on dev it should be 0
180
+ CF_PAGES: "0",
181
+ // will be fetched from git dynamically once we support mocking of bindings
182
+ CF_PAGES_BRANCH: "TBA",
183
+ // will be fetched from git dynamically once we support mocking of bindings
184
+ CF_PAGES_COMMIT_SHA: "TBA",
185
+ CF_PAGES_URL: `http://${req.headers.host}`,
186
+ ...vars
187
+ },
188
+ cf,
189
+ waitUntil: (_promise) => {
190
+ return;
191
+ },
192
+ caches: new CacheStorage(
193
+ { cache: true, cachePersist: false },
194
+ new NoOpLog(),
195
+ new StorageFactory(),
196
+ {}
197
+ )
198
+ }
199
+ });
200
+ next();
201
+ } catch {
202
+ next();
203
+ }
204
+ });
84
205
  }
85
206
  },
86
207
  "astro:build:setup": ({ vite, target }) => {
@@ -0,0 +1,19 @@
1
+ /**
2
+ * This file is a derivative work of wrangler by Cloudflare
3
+ * An upstream request for exposing this API was made here:
4
+ * https://github.com/cloudflare/workers-sdk/issues/3897
5
+ *
6
+ * Until further notice, we will be using this file as a workaround
7
+ * TODO: Tackle this file, once their is an decision on the upstream request
8
+ */
9
+ import dotenv from 'dotenv';
10
+ export interface DotEnv {
11
+ path: string;
12
+ parsed: dotenv.DotenvParseOutput;
13
+ }
14
+ /**
15
+ * Loads a dotenv file from <path>, preferring to read <path>.<environment> if
16
+ * <environment> is defined and that file exists.
17
+ */
18
+ export declare function loadDotEnv(path: string): DotEnv | undefined;
19
+ export declare function getEnvVars(): Promise<any>;
package/dist/parser.js ADDED
@@ -0,0 +1,87 @@
1
+ import TOML from "@iarna/toml";
2
+ import dotenv from "dotenv";
3
+ import { findUpSync } from "find-up";
4
+ import * as fs from "node:fs";
5
+ import { dirname, resolve } from "node:path";
6
+ function findWranglerToml(referencePath = process.cwd(), preferJson = false) {
7
+ if (preferJson) {
8
+ return findUpSync(`wrangler.json`, { cwd: referencePath }) ?? findUpSync(`wrangler.toml`, { cwd: referencePath });
9
+ }
10
+ return findUpSync(`wrangler.toml`, { cwd: referencePath });
11
+ }
12
+ class ParseError extends Error {
13
+ text;
14
+ notes;
15
+ location;
16
+ kind;
17
+ constructor({ text, notes, location, kind }) {
18
+ super(text);
19
+ this.name = this.constructor.name;
20
+ this.text = text;
21
+ this.notes = notes ?? [];
22
+ this.location = location;
23
+ this.kind = kind ?? "error";
24
+ }
25
+ }
26
+ const TOML_ERROR_NAME = "TomlError";
27
+ const TOML_ERROR_SUFFIX = " at row ";
28
+ function parseTOML(input, file) {
29
+ try {
30
+ const normalizedInput = input.replace(/\r\n/g, "\n");
31
+ return TOML.parse(normalizedInput);
32
+ } catch (err) {
33
+ const { name, message, line, col } = err;
34
+ if (name !== TOML_ERROR_NAME) {
35
+ throw err;
36
+ }
37
+ const text = message.substring(0, message.lastIndexOf(TOML_ERROR_SUFFIX));
38
+ const lineText = input.split("\n")[line];
39
+ const location = {
40
+ lineText,
41
+ line: line + 1,
42
+ column: col - 1,
43
+ file,
44
+ fileText: input
45
+ };
46
+ throw new ParseError({ text, location });
47
+ }
48
+ }
49
+ function tryLoadDotEnv(path) {
50
+ try {
51
+ const parsed = dotenv.parse(fs.readFileSync(path));
52
+ return { path, parsed };
53
+ } catch (e) {
54
+ }
55
+ }
56
+ function loadDotEnv(path) {
57
+ return tryLoadDotEnv(path);
58
+ }
59
+ function getVarsForDev(config, configPath) {
60
+ const configDir = resolve(dirname(configPath ?? "."));
61
+ const devVarsPath = resolve(configDir, ".dev.vars");
62
+ const loaded = loadDotEnv(devVarsPath);
63
+ if (loaded !== void 0) {
64
+ return {
65
+ ...config.vars,
66
+ ...loaded.parsed
67
+ };
68
+ } else {
69
+ return config.vars;
70
+ }
71
+ }
72
+ async function getEnvVars() {
73
+ let rawConfig;
74
+ const configPath = findWranglerToml(process.cwd(), false);
75
+ if (!configPath) {
76
+ throw new Error("Could not find wrangler.toml");
77
+ }
78
+ if (configPath?.endsWith("toml")) {
79
+ rawConfig = parseTOML(fs.readFileSync(configPath).toString(), configPath);
80
+ }
81
+ const vars = getVarsForDev(rawConfig, configPath);
82
+ return vars;
83
+ }
84
+ export {
85
+ getEnvVars,
86
+ loadDotEnv
87
+ };
@@ -4,12 +4,11 @@ type Env = {
4
4
  ASSETS: {
5
5
  fetch: (req: Request) => Promise<Response>;
6
6
  };
7
- name: string;
8
7
  };
9
- export interface AdvancedRuntime {
8
+ export interface AdvancedRuntime<T extends object = object> {
10
9
  runtime: {
11
10
  waitUntil: (promise: Promise<any>) => void;
12
- env: Env;
11
+ env: Env & T;
13
12
  cf: CFRequest['cf'];
14
13
  caches: typeof caches;
15
14
  };
@@ -18,16 +18,6 @@ function createExports(manifest) {
18
18
  Symbol.for("astro.clientAddress"),
19
19
  request.headers.get("cf-connecting-ip")
20
20
  );
21
- Reflect.set(request, Symbol.for("runtime"), {
22
- env,
23
- name: "cloudflare",
24
- caches,
25
- cf: request.cf,
26
- ...context,
27
- waitUntil: (promise) => {
28
- context.waitUntil(promise);
29
- }
30
- });
31
21
  const locals = {
32
22
  runtime: {
33
23
  waitUntil: (promise) => {
@@ -1,9 +1,9 @@
1
1
  import type { Request as CFRequest, EventContext } from '@cloudflare/workers-types';
2
2
  import type { SSRManifest } from 'astro';
3
- export interface DirectoryRuntime {
3
+ export interface DirectoryRuntime<T extends object = object> {
4
4
  runtime: {
5
5
  waitUntil: (promise: Promise<any>) => void;
6
- env: EventContext<unknown, string, unknown>['env'];
6
+ env: EventContext<unknown, string, unknown>['env'] & T;
7
7
  cf: CFRequest['cf'];
8
8
  caches: typeof caches;
9
9
  };
@@ -7,7 +7,7 @@ function createExports(manifest) {
7
7
  const app = new App(manifest);
8
8
  const onRequest = async (context) => {
9
9
  const request = context.request;
10
- const { next, env } = context;
10
+ const { env } = context;
11
11
  process.env = env;
12
12
  const { pathname } = new URL(request.url);
13
13
  if (manifest.assets.has(pathname)) {
@@ -20,16 +20,6 @@ function createExports(manifest) {
20
20
  Symbol.for("astro.clientAddress"),
21
21
  request.headers.get("cf-connecting-ip")
22
22
  );
23
- Reflect.set(request, Symbol.for("runtime"), {
24
- ...context,
25
- waitUntil: (promise) => {
26
- context.waitUntil(promise);
27
- },
28
- name: "cloudflare",
29
- next,
30
- caches,
31
- cf: request.cf
32
- });
33
23
  const locals = {
34
24
  runtime: {
35
25
  waitUntil: (promise) => {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@astrojs/cloudflare",
3
3
  "description": "Deploy your site to Cloudflare Workers/Pages",
4
- "version": "7.0.2",
4
+ "version": "7.1.1",
5
5
  "type": "module",
6
6
  "types": "./dist/index.d.ts",
7
7
  "author": "withastro",
@@ -33,20 +33,27 @@
33
33
  ],
34
34
  "dependencies": {
35
35
  "@cloudflare/workers-types": "^4.20230821.0",
36
+ "@iarna/toml": "^2.2.5",
37
+ "@miniflare/cache": "^2.14.1",
38
+ "@miniflare/shared": "^2.14.1",
39
+ "@miniflare/storage-memory": "^2.14.1",
40
+ "dotenv": "^16.3.1",
36
41
  "esbuild": "^0.19.2",
42
+ "find-up": "^6.3.0",
37
43
  "tiny-glob": "^0.2.9",
38
44
  "@astrojs/underscore-redirects": "0.3.0"
39
45
  },
40
46
  "peerDependencies": {
41
- "astro": "^3.0.9"
47
+ "astro": "^3.1.1"
42
48
  },
43
49
  "devDependencies": {
50
+ "@types/iarna__toml": "^2.0.2",
44
51
  "chai": "^4.3.7",
45
52
  "cheerio": "1.0.0-rc.12",
46
53
  "kill-port": "^2.0.1",
47
54
  "mocha": "^10.2.0",
48
55
  "wrangler": "^3.5.1",
49
- "astro": "3.0.9",
56
+ "astro": "3.1.1",
50
57
  "astro-scripts": "0.0.14"
51
58
  },
52
59
  "scripts": {
package/runtime.d.ts DELETED
@@ -1,3 +0,0 @@
1
- export type { WorkerRuntime, PagesRuntime } from './dist/runtime';
2
-
3
- export { getRuntime } from './dist/runtime';