@astrojs/cloudflare 4.0.1 → 4.1.0

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.
@@ -1,5 +1,5 @@
1
- @astrojs/cloudflare:build: cache hit, replaying output 17da9601b72c4888
1
+ @astrojs/cloudflare:build: cache hit, replaying output 5b4d4f8481cf8489
2
2
  @astrojs/cloudflare:build: 
3
- @astrojs/cloudflare:build: > @astrojs/cloudflare@4.0.1 build /home/runner/work/astro/astro/packages/integrations/cloudflare
3
+ @astrojs/cloudflare:build: > @astrojs/cloudflare@4.1.0 build /home/runner/work/astro/astro/packages/integrations/cloudflare
4
4
  @astrojs/cloudflare:build: > astro-scripts build "src/**/*.ts" && tsc
5
5
  @astrojs/cloudflare:build: 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @astrojs/cloudflare
2
2
 
3
+ ## 4.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#5347](https://github.com/withastro/astro/pull/5347) [`743000cc7`](https://github.com/withastro/astro/commit/743000cc70274a2d2fed01c72e2ac51aa6b876a6) Thanks [@AirBorne04](https://github.com/AirBorne04)! - Now building for Cloudflare directory mode takes advantage of the standard asset handling from Cloudflare Pages, and therefore does not call a function script to deliver static assets anymore.
8
+ Also supports the use of `_routes.json`, `_redirects` and `_headers` files when placed into the `public` folder.
9
+
10
+ ### Patch Changes
11
+
12
+ - Updated dependencies [[`936c1e411`](https://github.com/withastro/astro/commit/936c1e411d77c69b2b60a061c54704200716800a), [`4b188132e`](https://github.com/withastro/astro/commit/4b188132ef68f8d9951cec86418ef50bb4df4a96), [`f5ed630bc`](https://github.com/withastro/astro/commit/f5ed630bca05ebbfcc6ac994ced3911e41daedcc)]:
13
+ - astro@1.6.11
14
+
3
15
  ## 4.0.1
4
16
 
5
17
  ### Patch Changes
package/README.md CHANGED
@@ -25,7 +25,8 @@ npm install @astrojs/cloudflare
25
25
 
26
26
  2. Add the following to your `astro.config.mjs` file:
27
27
 
28
- ```js title="astro.config.mjs" ins={2, 5-6}
28
+ ```js ins={3, 6-7}
29
+ // astro.config.mjs
29
30
  import { defineConfig } from 'astro/config';
30
31
  import cloudflare from '@astrojs/cloudflare';
31
32
 
@@ -66,7 +67,7 @@ In order for preview to work you must install `wrangler`
66
67
  $ pnpm install wrangler --save-dev
67
68
  ```
68
69
 
69
- It's then possible to update the preview script in your `package.json` to `"preview": "wrangler pages dev ./dist"`.This will allow you 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).
70
+ It's then possible to update the preview script in your `package.json` to `"preview": "wrangler pages dev ./dist"`. This will allow you 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).
70
71
 
71
72
  ## Access to the Cloudflare runtime
72
73
 
@@ -107,6 +108,14 @@ export function get({ params }) {
107
108
  }
108
109
  ```
109
110
 
111
+ ## Headers, Redirects and function invocation routes
112
+
113
+ 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.
114
+
115
+ ### Custom `_routes.json`
116
+
117
+ By default, `@astrojs/cloudflare` will generate a `_routes.json` file that lists all files from your `dist/` folder and redirects from the `_redirects` file in the `exclude` array. 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.
118
+
110
119
  ## Troubleshooting
111
120
 
112
121
  For help, check out the `#support` channel on [Discord](https://astro.build/chat). Our friendly Support Squad members are here to help!
package/dist/index.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import esbuild from "esbuild";
2
2
  import * as fs from "fs";
3
+ import * as os from "os";
4
+ import glob from "tiny-glob";
3
5
  import { fileURLToPath } from "url";
4
6
  function getAdapter(isModeDirectory) {
5
7
  return isModeDirectory ? {
@@ -16,6 +18,7 @@ const SHIM = `globalThis.process = {
16
18
  argv: [],
17
19
  env: {},
18
20
  };`;
21
+ const SERVER_BUILD_FOLDER = "/$server_build/";
19
22
  function createIntegration(args) {
20
23
  let _config;
21
24
  let _buildConfig;
@@ -28,8 +31,8 @@ function createIntegration(args) {
28
31
  needsBuildConfig = !config.build.client;
29
32
  updateConfig({
30
33
  build: {
31
- client: new URL("./static/", config.outDir),
32
- server: new URL("./", config.outDir),
34
+ client: new URL(`.${config.base}`, config.outDir),
35
+ server: new URL(`.${SERVER_BUILD_FOLDER}`, config.outDir),
33
36
  serverEntry: "_worker.js"
34
37
  }
35
38
  });
@@ -44,6 +47,10 @@ function createIntegration(args) {
44
47
 
45
48
  `);
46
49
  }
50
+ if (config.base === SERVER_BUILD_FOLDER) {
51
+ throw new Error(`
52
+ [@astrojs/cloudflare] \`base: "${SERVER_BUILD_FOLDER}"\` is not allowed. Please change your \`base\` config to something else.`);
53
+ }
47
54
  },
48
55
  "astro:build:setup": ({ vite, target }) => {
49
56
  if (target === "server") {
@@ -63,19 +70,18 @@ function createIntegration(args) {
63
70
  },
64
71
  "astro:build:start": ({ buildConfig }) => {
65
72
  if (needsBuildConfig) {
66
- buildConfig.client = new URL("./static/", _config.outDir);
67
- buildConfig.server = new URL("./", _config.outDir);
73
+ buildConfig.client = new URL(`.${_config.base}`, _config.outDir);
74
+ buildConfig.server = new URL(`.${SERVER_BUILD_FOLDER}`, _config.outDir);
68
75
  buildConfig.serverEntry = "_worker.js";
69
76
  }
70
77
  },
71
78
  "astro:build:done": async () => {
72
- const entryUrl = new URL(_buildConfig.serverEntry, _buildConfig.server);
73
- const pkg = fileURLToPath(entryUrl);
79
+ const entryPath = fileURLToPath(new URL(_buildConfig.serverEntry, _buildConfig.server)), entryUrl = new URL(_buildConfig.serverEntry, _config.outDir), buildPath = fileURLToPath(entryUrl);
74
80
  await esbuild.build({
75
81
  target: "es2020",
76
82
  platform: "browser",
77
- entryPoints: [pkg],
78
- outfile: pkg,
83
+ entryPoints: [entryPath],
84
+ outfile: buildPath,
79
85
  allowOverwrite: true,
80
86
  format: "esm",
81
87
  bundle: true,
@@ -84,8 +90,55 @@ function createIntegration(args) {
84
90
  js: SHIM
85
91
  }
86
92
  });
87
- const chunksUrl = new URL("./chunks", _buildConfig.server);
88
- await fs.promises.rm(chunksUrl, { recursive: true, force: true });
93
+ const serverUrl = new URL(_buildConfig.server);
94
+ await fs.promises.rm(serverUrl, { recursive: true, force: true });
95
+ const cloudflareSpecialFiles = ["_headers", "_redirects", "_routes.json"];
96
+ if (_config.base !== "/") {
97
+ for (const file of cloudflareSpecialFiles) {
98
+ try {
99
+ await fs.promises.rename(
100
+ new URL(file, _buildConfig.client),
101
+ new URL(file, _config.outDir)
102
+ );
103
+ } catch (e) {
104
+ }
105
+ }
106
+ }
107
+ const routesExists = await fs.promises.stat(new URL("./_routes.json", _config.outDir)).then((stat) => stat.isFile()).catch(() => false);
108
+ if (!routesExists) {
109
+ const staticPathList = (await glob(`${fileURLToPath(_buildConfig.client)}/**/*`, {
110
+ cwd: fileURLToPath(_config.outDir),
111
+ filesOnly: true
112
+ })).filter((file) => cloudflareSpecialFiles.indexOf(file) < 0).map((file) => `/${file}`);
113
+ const redirectsExists = await fs.promises.stat(new URL("./_redirects", _config.outDir)).then((stat) => stat.isFile()).catch(() => false);
114
+ if (redirectsExists) {
115
+ const redirects = (await fs.promises.readFile(new URL("./_redirects", _config.outDir), "utf-8")).split(os.EOL).map((line) => {
116
+ const parts = line.split(" ");
117
+ if (parts.length < 2) {
118
+ return null;
119
+ } else {
120
+ return parts[0].replace(/\/:.*?(?=\/|$)/g, "/*").replace(/\?.*$/, "");
121
+ }
122
+ }).filter(
123
+ (line, index, arr) => line !== null && arr.indexOf(line) === index
124
+ );
125
+ if (redirects.length > 0) {
126
+ staticPathList.push(...redirects);
127
+ }
128
+ }
129
+ await fs.promises.writeFile(
130
+ new URL("./_routes.json", _config.outDir),
131
+ JSON.stringify(
132
+ {
133
+ version: 1,
134
+ include: ["/*"],
135
+ exclude: staticPathList
136
+ },
137
+ null,
138
+ 2
139
+ )
140
+ );
141
+ }
89
142
  if (isModeDirectory) {
90
143
  const functionsUrl = new URL(`file://${process.cwd()}/functions/`);
91
144
  await fs.promises.mkdir(functionsUrl, { recursive: true });
@@ -5,10 +5,9 @@ function createExports(manifest) {
5
5
  const app = new App(manifest, false);
6
6
  const fetch = async (request, env, context) => {
7
7
  process.env = env;
8
- const { origin, pathname } = new URL(request.url);
8
+ const { pathname } = new URL(request.url);
9
9
  if (manifest.assets.has(pathname)) {
10
- const assetRequest = new Request(`${origin}/static/${app.removeBase(pathname)}`, request);
11
- return env.ASSETS.fetch(assetRequest);
10
+ return env.ASSETS.fetch(request);
12
11
  }
13
12
  let routeData = app.match(request, { matchNotFound: true });
14
13
  if (routeData) {
@@ -9,10 +9,9 @@ function createExports(manifest) {
9
9
  ...runtimeEnv
10
10
  }) => {
11
11
  process.env = runtimeEnv.env;
12
- const { origin, pathname } = new URL(request.url);
12
+ const { pathname } = new URL(request.url);
13
13
  if (manifest.assets.has(pathname)) {
14
- const assetRequest = new Request(`${origin}/static/${app.removeBase(pathname)}`, request);
15
- return next(assetRequest);
14
+ return next(request);
16
15
  }
17
16
  let routeData = app.match(request, { matchNotFound: true });
18
17
  if (routeData) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@astrojs/cloudflare",
3
3
  "description": "Deploy your site to cloudflare workers or cloudflare pages",
4
- "version": "4.0.1",
4
+ "version": "4.1.0",
5
5
  "type": "module",
6
6
  "types": "./dist/index.d.ts",
7
7
  "author": "withastro",
@@ -28,13 +28,14 @@
28
28
  "./package.json": "./package.json"
29
29
  },
30
30
  "dependencies": {
31
- "esbuild": "^0.14.42"
31
+ "esbuild": "^0.14.42",
32
+ "tiny-glob": "^0.2.9"
32
33
  },
33
34
  "peerDependencies": {
34
- "astro": "^1.6.5"
35
+ "astro": "^1.6.11"
35
36
  },
36
37
  "devDependencies": {
37
- "astro": "1.6.5",
38
+ "astro": "1.6.11",
38
39
  "astro-scripts": "0.0.9",
39
40
  "chai": "^4.3.6",
40
41
  "cheerio": "^1.0.0-rc.11",
package/src/index.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import type { AstroAdapter, AstroConfig, AstroIntegration } from 'astro';
2
2
  import esbuild from 'esbuild';
3
3
  import * as fs from 'fs';
4
+ import * as os from 'os';
5
+ import glob from 'tiny-glob';
4
6
  import { fileURLToPath } from 'url';
5
7
 
6
8
  type Options = {
@@ -32,6 +34,8 @@ const SHIM = `globalThis.process = {
32
34
  env: {},
33
35
  };`;
34
36
 
37
+ const SERVER_BUILD_FOLDER = '/$server_build/';
38
+
35
39
  export default function createIntegration(args?: Options): AstroIntegration {
36
40
  let _config: AstroConfig;
37
41
  let _buildConfig: BuildConfig;
@@ -45,8 +49,8 @@ export default function createIntegration(args?: Options): AstroIntegration {
45
49
  needsBuildConfig = !config.build.client;
46
50
  updateConfig({
47
51
  build: {
48
- client: new URL('./static/', config.outDir),
49
- server: new URL('./', config.outDir),
52
+ client: new URL(`.${config.base}`, config.outDir),
53
+ server: new URL(`.${SERVER_BUILD_FOLDER}`, config.outDir),
50
54
  serverEntry: '_worker.js',
51
55
  },
52
56
  });
@@ -62,6 +66,11 @@ export default function createIntegration(args?: Options): AstroIntegration {
62
66
 
63
67
  `);
64
68
  }
69
+
70
+ if (config.base === SERVER_BUILD_FOLDER) {
71
+ throw new Error(`
72
+ [@astrojs/cloudflare] \`base: "${SERVER_BUILD_FOLDER}"\` is not allowed. Please change your \`base\` config to something else.`);
73
+ }
65
74
  },
66
75
  'astro:build:setup': ({ vite, target }) => {
67
76
  if (target === 'server') {
@@ -84,19 +93,20 @@ export default function createIntegration(args?: Options): AstroIntegration {
84
93
  'astro:build:start': ({ buildConfig }) => {
85
94
  // Backwards compat
86
95
  if (needsBuildConfig) {
87
- buildConfig.client = new URL('./static/', _config.outDir);
88
- buildConfig.server = new URL('./', _config.outDir);
96
+ buildConfig.client = new URL(`.${_config.base}`, _config.outDir);
97
+ buildConfig.server = new URL(`.${SERVER_BUILD_FOLDER}`, _config.outDir);
89
98
  buildConfig.serverEntry = '_worker.js';
90
99
  }
91
100
  },
92
101
  'astro:build:done': async () => {
93
- const entryUrl = new URL(_buildConfig.serverEntry, _buildConfig.server);
94
- const pkg = fileURLToPath(entryUrl);
102
+ const entryPath = fileURLToPath(new URL(_buildConfig.serverEntry, _buildConfig.server)),
103
+ entryUrl = new URL(_buildConfig.serverEntry, _config.outDir),
104
+ buildPath = fileURLToPath(entryUrl);
95
105
  await esbuild.build({
96
106
  target: 'es2020',
97
107
  platform: 'browser',
98
- entryPoints: [pkg],
99
- outfile: pkg,
108
+ entryPoints: [entryPath],
109
+ outfile: buildPath,
100
110
  allowOverwrite: true,
101
111
  format: 'esm',
102
112
  bundle: true,
@@ -107,8 +117,90 @@ export default function createIntegration(args?: Options): AstroIntegration {
107
117
  });
108
118
 
109
119
  // throw the server folder in the bin
110
- const chunksUrl = new URL('./chunks', _buildConfig.server);
111
- await fs.promises.rm(chunksUrl, { recursive: true, force: true });
120
+ const serverUrl = new URL(_buildConfig.server);
121
+ await fs.promises.rm(serverUrl, { recursive: true, force: true });
122
+
123
+ // move cloudflare specific files to the root
124
+ const cloudflareSpecialFiles = ['_headers', '_redirects', '_routes.json'];
125
+ if (_config.base !== '/') {
126
+ for (const file of cloudflareSpecialFiles) {
127
+ try {
128
+ await fs.promises.rename(
129
+ new URL(file, _buildConfig.client),
130
+ new URL(file, _config.outDir)
131
+ );
132
+ } catch (e) {
133
+ // ignore
134
+ }
135
+ }
136
+ }
137
+
138
+ const routesExists = await fs.promises
139
+ .stat(new URL('./_routes.json', _config.outDir))
140
+ .then((stat) => stat.isFile())
141
+ .catch(() => false);
142
+
143
+ // this creates a _routes.json, in case there is none present to enable
144
+ // cloudflare to handle static files and support _redirects configuration
145
+ // (without calling the function)
146
+ if (!routesExists) {
147
+ const staticPathList: Array<string> = (
148
+ await glob(`${fileURLToPath(_buildConfig.client)}/**/*`, {
149
+ cwd: fileURLToPath(_config.outDir),
150
+ filesOnly: true,
151
+ })
152
+ )
153
+ .filter((file: string) => cloudflareSpecialFiles.indexOf(file) < 0)
154
+ .map((file: string) => `/${file}`);
155
+
156
+ const redirectsExists = await fs.promises
157
+ .stat(new URL('./_redirects', _config.outDir))
158
+ .then((stat) => stat.isFile())
159
+ .catch(() => false);
160
+
161
+ // convert all redirect source paths into a list of routes
162
+ // and add them to the static path
163
+ if (redirectsExists) {
164
+ const redirects = (
165
+ await fs.promises.readFile(new URL('./_redirects', _config.outDir), 'utf-8')
166
+ )
167
+ .split(os.EOL)
168
+ .map((line) => {
169
+ const parts = line.split(' ');
170
+ if (parts.length < 2) {
171
+ return null;
172
+ } else {
173
+ // convert /products/:id to /products/*
174
+ return (
175
+ parts[0]
176
+ .replace(/\/:.*?(?=\/|$)/g, '/*')
177
+ // remove query params as they are not supported by cloudflare
178
+ .replace(/\?.*$/, '')
179
+ );
180
+ }
181
+ })
182
+ .filter(
183
+ (line, index, arr) => line !== null && arr.indexOf(line) === index
184
+ ) as string[];
185
+
186
+ if (redirects.length > 0) {
187
+ staticPathList.push(...redirects);
188
+ }
189
+ }
190
+
191
+ await fs.promises.writeFile(
192
+ new URL('./_routes.json', _config.outDir),
193
+ JSON.stringify(
194
+ {
195
+ version: 1,
196
+ include: ['/*'],
197
+ exclude: staticPathList,
198
+ },
199
+ null,
200
+ 2
201
+ )
202
+ );
203
+ }
112
204
 
113
205
  if (isModeDirectory) {
114
206
  const functionsUrl = new URL(`file://${process.cwd()}/functions/`);
@@ -15,12 +15,11 @@ export function createExports(manifest: SSRManifest) {
15
15
  const fetch = async (request: Request, env: Env, context: any) => {
16
16
  process.env = env as any;
17
17
 
18
- const { origin, pathname } = new URL(request.url);
18
+ const { pathname } = new URL(request.url);
19
19
 
20
- // static assets
20
+ // static assets fallback, in case default _routes.json is not used
21
21
  if (manifest.assets.has(pathname)) {
22
- const assetRequest = new Request(`${origin}/static/${app.removeBase(pathname)}`, request);
23
- return env.ASSETS.fetch(assetRequest);
22
+ return env.ASSETS.fetch(request);
24
23
  }
25
24
 
26
25
  let routeData = app.match(request, { matchNotFound: true });
@@ -17,11 +17,10 @@ export function createExports(manifest: SSRManifest) {
17
17
  } & Record<string, unknown>) => {
18
18
  process.env = runtimeEnv.env as any;
19
19
 
20
- const { origin, pathname } = new URL(request.url);
21
- // static assets
20
+ const { pathname } = new URL(request.url);
21
+ // static assets fallback, in case default _routes.json is not used
22
22
  if (manifest.assets.has(pathname)) {
23
- const assetRequest = new Request(`${origin}/static/${app.removeBase(pathname)}`, request);
24
- return next(assetRequest);
23
+ return next(request);
25
24
  }
26
25
 
27
26
  let routeData = app.match(request, { matchNotFound: true });