summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Emanuele Stoppa <my.burning@gmail.com> 2023-08-10 11:49:52 +0100
committerGravatar Emanuele Stoppa <my.burning@gmail.com> 2023-08-10 11:49:52 +0100
commit91fa61a497eb2aeabfe2d07c4988deca2614a0e4 (patch)
tree16f96e2e26b3c83aa9059fa0f020952768a51d78
parent846715ef969073c7371ee5975f30f75ab2e71fa4 (diff)
parentc1239103afceb1c45e8696dec48a6410be5985c1 (diff)
downloadastro-91fa61a497eb2aeabfe2d07c4988deca2614a0e4.tar.gz
astro-91fa61a497eb2aeabfe2d07c4988deca2614a0e4.tar.zst
astro-91fa61a497eb2aeabfe2d07c4988deca2614a0e4.zip
Merge remote-tracking branch 'origin/main' into next
-rw-r--r--packages/integrations/cloudflare/README.md5
-rw-r--r--packages/integrations/cloudflare/src/index.ts90
-rw-r--r--packages/integrations/cloudflare/test/fixtures/routesJson/astro.config.mjs11
-rw-r--r--packages/integrations/cloudflare/test/fixtures/routesJson/package.json9
-rw-r--r--packages/integrations/cloudflare/test/fixtures/routesJson/src/dynamicOnly/pages/another.astro5
-rw-r--r--packages/integrations/cloudflare/test/fixtures/routesJson/src/dynamicOnly/pages/index.astro5
-rw-r--r--packages/integrations/cloudflare/test/fixtures/routesJson/src/mixed/pages/a/[...rest].astro5
-rw-r--r--packages/integrations/cloudflare/test/fixtures/routesJson/src/mixed/pages/a/[id].astro5
-rw-r--r--packages/integrations/cloudflare/test/fixtures/routesJson/src/mixed/pages/a/endpoint.ts1
-rw-r--r--packages/integrations/cloudflare/test/fixtures/routesJson/src/mixed/pages/a/index.astro1
-rw-r--r--packages/integrations/cloudflare/test/fixtures/routesJson/src/mixed/pages/b/index.html1
-rw-r--r--packages/integrations/cloudflare/test/fixtures/routesJson/src/mixed/public/public.txt1
-rw-r--r--packages/integrations/cloudflare/test/fixtures/routesJson/src/staticOnly/pages/index.astro1
-rw-r--r--packages/integrations/cloudflare/test/prerender.test.js26
-rw-r--r--packages/integrations/cloudflare/test/routesJson.js78
15 files changed, 228 insertions, 16 deletions
diff --git a/packages/integrations/cloudflare/README.md b/packages/integrations/cloudflare/README.md
index 7f4292d97..45f8e01ba 100644
--- a/packages/integrations/cloudflare/README.md
+++ b/packages/integrations/cloudflare/README.md
@@ -106,7 +106,10 @@ Cloudflare has support for adding custom [headers](https://developers.cloudflare
### Custom `_routes.json`
-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.
+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.
## Troubleshooting
diff --git a/packages/integrations/cloudflare/src/index.ts b/packages/integrations/cloudflare/src/index.ts
index a3bb76fbb..46d87a3eb 100644
--- a/packages/integrations/cloudflare/src/index.ts
+++ b/packages/integrations/cloudflare/src/index.ts
@@ -59,6 +59,11 @@ const SHIM = `globalThis.process = {
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;
@@ -253,6 +258,32 @@ export default function createIntegration(args?: Options): AstroIntegration {
// cloudflare to handle static files and support _redirects configuration
// (without calling the function)
if (!routesExists) {
+ const functionEndpoints = routes
+ // Certain route types, when their prerender option is set to false, a run on the server as function invocations
+ .filter((route) => potentialFunctionRouteTypes.includes(route.type) && !route.prerender)
+ .map((route) => {
+ const includePattern =
+ '/' +
+ route.segments
+ .flat()
+ .map((segment) => (segment.dynamic ? '*' : segment.content))
+ .join('/');
+
+ const regexp = new RegExp(
+ '^\\/' +
+ route.segments
+ .flat()
+ .map((segment) => (segment.dynamic ? '(.*)' : segment.content))
+ .join('\\/') +
+ '$'
+ );
+
+ return {
+ includePattern,
+ regexp,
+ };
+ });
+
const staticPathList: Array<string> = (
await glob(`${fileURLToPath(_buildConfig.client)}/**/*`, {
cwd: fileURLToPath(_config.outDir),
@@ -260,7 +291,7 @@ export default function createIntegration(args?: Options): AstroIntegration {
})
)
.filter((file: string) => cloudflareSpecialFiles.indexOf(file) < 0)
- .map((file: string) => `/${file}`);
+ .map((file: string) => `/${file.replace(/\\/g, '/')}`);
for (let page of pages) {
let pagePath = prependForwardSlash(page.pathname);
@@ -323,13 +354,41 @@ export default function createIntegration(args?: Options): AstroIntegration {
);
}
+ staticPathList.push(...routes.filter((r) => r.type === 'redirect').map((r) => r.route));
+
+ // In order to product the shortest list of patterns, we first try to
+ // include all function endpoints, and then exclude all static paths
+ let include = deduplicatePatterns(
+ functionEndpoints.map((endpoint) => endpoint.includePattern)
+ );
+ let exclude = deduplicatePatterns(
+ staticPathList.filter((file: string) =>
+ functionEndpoints.some((endpoint) => endpoint.regexp.test(file))
+ )
+ );
+
+ // Cloudflare requires at least one include pattern:
+ // https://developers.cloudflare.com/pages/platform/functions/routing/#limits
+ // So we add a pattern that we immediately exclude again
+ if (include.length === 0) {
+ include = ['/'];
+ exclude = ['/'];
+ }
+
+ // If using only an exclude list would produce a shorter list of patterns,
+ // we use that instead
+ if (include.length + exclude.length > staticPathList.length) {
+ include = ['/*'];
+ exclude = deduplicatePatterns(staticPathList);
+ }
+
await fs.promises.writeFile(
new URL('./_routes.json', _config.outDir),
JSON.stringify(
{
version: 1,
- include: ['/*'],
- exclude: staticPathList,
+ include,
+ exclude,
},
null,
2
@@ -344,3 +403,28 @@ 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;
+ });
+}
diff --git a/packages/integrations/cloudflare/test/fixtures/routesJson/astro.config.mjs b/packages/integrations/cloudflare/test/fixtures/routesJson/astro.config.mjs
new file mode 100644
index 000000000..66b50c098
--- /dev/null
+++ b/packages/integrations/cloudflare/test/fixtures/routesJson/astro.config.mjs
@@ -0,0 +1,11 @@
+import { defineConfig } from 'astro/config';
+import cloudflare from '@astrojs/cloudflare';
+
+export default defineConfig({
+ adapter: cloudflare({ mode: 'directory' }),
+ output: 'hybrid',
+ redirects: {
+ '/a/redirect': '/',
+ },
+ srcDir: process.env.SRC
+});
diff --git a/packages/integrations/cloudflare/test/fixtures/routesJson/package.json b/packages/integrations/cloudflare/test/fixtures/routesJson/package.json
new file mode 100644
index 000000000..4ff746f02
--- /dev/null
+++ b/packages/integrations/cloudflare/test/fixtures/routesJson/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "@test/astro-cloudflare-routes-json",
+ "version": "0.0.0",
+ "private": true,
+ "dependencies": {
+ "@astrojs/cloudflare": "workspace:*",
+ "astro": "workspace:*"
+ }
+}
diff --git a/packages/integrations/cloudflare/test/fixtures/routesJson/src/dynamicOnly/pages/another.astro b/packages/integrations/cloudflare/test/fixtures/routesJson/src/dynamicOnly/pages/another.astro
new file mode 100644
index 000000000..9a2306b86
--- /dev/null
+++ b/packages/integrations/cloudflare/test/fixtures/routesJson/src/dynamicOnly/pages/another.astro
@@ -0,0 +1,5 @@
+---
+export const prerender=false;
+---
+
+ok
diff --git a/packages/integrations/cloudflare/test/fixtures/routesJson/src/dynamicOnly/pages/index.astro b/packages/integrations/cloudflare/test/fixtures/routesJson/src/dynamicOnly/pages/index.astro
new file mode 100644
index 000000000..9a2306b86
--- /dev/null
+++ b/packages/integrations/cloudflare/test/fixtures/routesJson/src/dynamicOnly/pages/index.astro
@@ -0,0 +1,5 @@
+---
+export const prerender=false;
+---
+
+ok
diff --git a/packages/integrations/cloudflare/test/fixtures/routesJson/src/mixed/pages/a/[...rest].astro b/packages/integrations/cloudflare/test/fixtures/routesJson/src/mixed/pages/a/[...rest].astro
new file mode 100644
index 000000000..9a2306b86
--- /dev/null
+++ b/packages/integrations/cloudflare/test/fixtures/routesJson/src/mixed/pages/a/[...rest].astro
@@ -0,0 +1,5 @@
+---
+export const prerender=false;
+---
+
+ok
diff --git a/packages/integrations/cloudflare/test/fixtures/routesJson/src/mixed/pages/a/[id].astro b/packages/integrations/cloudflare/test/fixtures/routesJson/src/mixed/pages/a/[id].astro
new file mode 100644
index 000000000..9a2306b86
--- /dev/null
+++ b/packages/integrations/cloudflare/test/fixtures/routesJson/src/mixed/pages/a/[id].astro
@@ -0,0 +1,5 @@
+---
+export const prerender=false;
+---
+
+ok
diff --git a/packages/integrations/cloudflare/test/fixtures/routesJson/src/mixed/pages/a/endpoint.ts b/packages/integrations/cloudflare/test/fixtures/routesJson/src/mixed/pages/a/endpoint.ts
new file mode 100644
index 000000000..d43d0cd2a
--- /dev/null
+++ b/packages/integrations/cloudflare/test/fixtures/routesJson/src/mixed/pages/a/endpoint.ts
@@ -0,0 +1 @@
+export const prerender = false;
diff --git a/packages/integrations/cloudflare/test/fixtures/routesJson/src/mixed/pages/a/index.astro b/packages/integrations/cloudflare/test/fixtures/routesJson/src/mixed/pages/a/index.astro
new file mode 100644
index 000000000..9766475a4
--- /dev/null
+++ b/packages/integrations/cloudflare/test/fixtures/routesJson/src/mixed/pages/a/index.astro
@@ -0,0 +1 @@
+ok
diff --git a/packages/integrations/cloudflare/test/fixtures/routesJson/src/mixed/pages/b/index.html b/packages/integrations/cloudflare/test/fixtures/routesJson/src/mixed/pages/b/index.html
new file mode 100644
index 000000000..9766475a4
--- /dev/null
+++ b/packages/integrations/cloudflare/test/fixtures/routesJson/src/mixed/pages/b/index.html
@@ -0,0 +1 @@
+ok
diff --git a/packages/integrations/cloudflare/test/fixtures/routesJson/src/mixed/public/public.txt b/packages/integrations/cloudflare/test/fixtures/routesJson/src/mixed/public/public.txt
new file mode 100644
index 000000000..9766475a4
--- /dev/null
+++ b/packages/integrations/cloudflare/test/fixtures/routesJson/src/mixed/public/public.txt
@@ -0,0 +1 @@
+ok
diff --git a/packages/integrations/cloudflare/test/fixtures/routesJson/src/staticOnly/pages/index.astro b/packages/integrations/cloudflare/test/fixtures/routesJson/src/staticOnly/pages/index.astro
new file mode 100644
index 000000000..9766475a4
--- /dev/null
+++ b/packages/integrations/cloudflare/test/fixtures/routesJson/src/staticOnly/pages/index.astro
@@ -0,0 +1 @@
+ok
diff --git a/packages/integrations/cloudflare/test/prerender.test.js b/packages/integrations/cloudflare/test/prerender.test.js
index 847bd950a..fe0721f27 100644
--- a/packages/integrations/cloudflare/test/prerender.test.js
+++ b/packages/integrations/cloudflare/test/prerender.test.js
@@ -18,13 +18,14 @@ describe('Prerendering', () => {
fixture.clean();
});
- it('includes prerendered routes in the routes.json config', async () => {
- const foundRoutes = JSON.parse(await fixture.readFile('/_routes.json')).exclude.map((r) =>
- r.replace(/\\/g, '/')
- );
- const expectedExcludedRoutes = ['/_worker.js', '/one/index.html', '/one/'];
+ it('includes non prerendered routes in the routes.json config', async () => {
+ const foundRoutes = JSON.parse(await fixture.readFile('/_routes.json'));
- expect(foundRoutes.every((element) => expectedExcludedRoutes.includes(element))).to.be.true;
+ expect(foundRoutes).to.deep.equal({
+ version: 1,
+ include: ['/'],
+ exclude: [],
+ });
});
});
@@ -45,12 +46,13 @@ describe('Hybrid rendering', () => {
delete process.env.PRERENDER;
});
- it('includes prerendered routes in the routes.json config', async () => {
- const foundRoutes = JSON.parse(await fixture.readFile('/_routes.json')).exclude.map((r) =>
- r.replace(/\\/g, '/')
- );
- const expectedExcludedRoutes = ['/_worker.js', '/index.html', '/'];
+ it('includes non prerendered routes in the routes.json config', async () => {
+ const foundRoutes = JSON.parse(await fixture.readFile('/_routes.json'));
- expect(foundRoutes.every((element) => expectedExcludedRoutes.includes(element))).to.be.true;
+ expect(foundRoutes).to.deep.equal({
+ version: 1,
+ include: ['/one'],
+ exclude: [],
+ });
});
});
diff --git a/packages/integrations/cloudflare/test/routesJson.js b/packages/integrations/cloudflare/test/routesJson.js
new file mode 100644
index 000000000..927e4c38e
--- /dev/null
+++ b/packages/integrations/cloudflare/test/routesJson.js
@@ -0,0 +1,78 @@
+import { expect } from 'chai';
+import { loadFixture } from './test-utils.js';
+
+/** @type {import('./test-utils.js').Fixture} */
+describe('_routes.json generation', () => {
+ after(() => {
+ delete process.env.SRC;
+ });
+
+ describe('of both functions and static files', () => {
+ let fixture;
+
+ before(async () => {
+ process.env.SRC = './src/mixed';
+ fixture = await loadFixture({
+ root: './fixtures/routesJson/',
+ });
+ await fixture.build();
+ });
+
+ it('creates `include` for functions and `exclude` for static files where needed', async () => {
+ const _routesJson = await fixture.readFile('/_routes.json');
+ const routes = JSON.parse(_routesJson);
+
+ expect(routes).to.deep.equal({
+ version: 1,
+ include: ['/a/*'],
+ exclude: ['/a/', '/a/redirect', '/a/index.html'],
+ });
+ });
+ });
+
+ describe('of only functions', () => {
+ let fixture;
+
+ before(async () => {
+ process.env.SRC = './src/dynamicOnly';
+ fixture = await loadFixture({
+ root: './fixtures/routesJson/',
+ });
+ await fixture.build();
+ });
+
+ it('creates a wildcard `include` and `exclude` only for the redirect', async () => {
+ const _routesJson = await fixture.readFile('/_routes.json');
+ const routes = JSON.parse(_routesJson);
+
+ expect(routes).to.deep.equal({
+ version: 1,
+ include: ['/*'],
+ exclude: ['/a/redirect'],
+ });
+ });
+ });
+
+ describe('of only static files', () => {
+ let fixture;
+
+ before(async () => {
+ process.env.SRC = './src/staticOnly';
+ fixture = await loadFixture({
+ root: './fixtures/routesJson/',
+ });
+ await fixture.build();
+ });
+
+ it('create only one `include` and `exclude` that are supposed to match nothing', async () => {
+ const _routesJson = await fixture.readFile('/_routes.json');
+ const routes = JSON.parse(_routesJson);
+
+ expect(routes).to.deep.equal({
+ version: 1,
+ include: ['/'],
+ exclude: ['/'],
+ });
+ });
+ });
+});