Use Cloudflare Pages to serve static assets and support _headers
, _redirects
and _routes.json
(#5347)
Co-authored-by: AirBorne04 <unknown> Co-authored-by: Chris Swithinbank <swithinbank@gmail.com>
This commit is contained in:
parent
ef2ffc7ae9
commit
743000cc70
7 changed files with 127 additions and 20 deletions
6
.changeset/quick-items-jog.md
Normal file
6
.changeset/quick-items-jog.md
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
---
|
||||||
|
'@astrojs/cloudflare': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
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.
|
||||||
|
Also supports the use of `_routes.json`, `_redirects` and `_headers` files when placed into the `public` folder.
|
|
@ -66,7 +66,7 @@ In order for preview to work you must install `wrangler`
|
||||||
$ pnpm install wrangler --save-dev
|
$ pnpm install wrangler --save-dev
|
||||||
```
|
```
|
||||||
|
|
||||||
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).
|
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).
|
||||||
|
|
||||||
## Access to the Cloudflare runtime
|
## Access to the Cloudflare runtime
|
||||||
|
|
||||||
|
@ -107,6 +107,14 @@ export function get({ params }) {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 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 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.
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
For help, check out the `#support` channel on [Discord](https://astro.build/chat). Our friendly Support Squad members are here to help!
|
For help, check out the `#support` channel on [Discord](https://astro.build/chat). Our friendly Support Squad members are here to help!
|
||||||
|
|
|
@ -34,7 +34,8 @@
|
||||||
"test": "mocha --exit --timeout 30000 test/"
|
"test": "mocha --exit --timeout 30000 test/"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.14.42"
|
"esbuild": "^0.14.42",
|
||||||
|
"tiny-glob": "^0.2.9"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"astro": "^1.6.10"
|
"astro": "^1.6.10"
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import type { AstroAdapter, AstroConfig, AstroIntegration } from 'astro';
|
import type { AstroAdapter, AstroConfig, AstroIntegration } from 'astro';
|
||||||
import esbuild from 'esbuild';
|
import esbuild from 'esbuild';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
|
import * as os from 'os';
|
||||||
|
import glob from 'tiny-glob';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
type Options = {
|
type Options = {
|
||||||
|
@ -32,6 +34,8 @@ const SHIM = `globalThis.process = {
|
||||||
env: {},
|
env: {},
|
||||||
};`;
|
};`;
|
||||||
|
|
||||||
|
const SERVER_BUILD_FOLDER = '/$server_build/';
|
||||||
|
|
||||||
export default function createIntegration(args?: Options): AstroIntegration {
|
export default function createIntegration(args?: Options): AstroIntegration {
|
||||||
let _config: AstroConfig;
|
let _config: AstroConfig;
|
||||||
let _buildConfig: BuildConfig;
|
let _buildConfig: BuildConfig;
|
||||||
|
@ -45,8 +49,8 @@ export default function createIntegration(args?: Options): AstroIntegration {
|
||||||
needsBuildConfig = !config.build.client;
|
needsBuildConfig = !config.build.client;
|
||||||
updateConfig({
|
updateConfig({
|
||||||
build: {
|
build: {
|
||||||
client: new URL('./static/', config.outDir),
|
client: new URL(`.${config.base}`, config.outDir),
|
||||||
server: new URL('./', config.outDir),
|
server: new URL(`.${SERVER_BUILD_FOLDER}`, config.outDir),
|
||||||
serverEntry: '_worker.js',
|
serverEntry: '_worker.js',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -62,6 +66,11 @@ export default function createIntegration(args?: Options): AstroIntegration {
|
||||||
|
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.`);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
'astro:build:setup': ({ vite, target }) => {
|
'astro:build:setup': ({ vite, target }) => {
|
||||||
if (target === 'server') {
|
if (target === 'server') {
|
||||||
|
@ -84,19 +93,20 @@ export default function createIntegration(args?: Options): AstroIntegration {
|
||||||
'astro:build:start': ({ buildConfig }) => {
|
'astro:build:start': ({ buildConfig }) => {
|
||||||
// Backwards compat
|
// Backwards compat
|
||||||
if (needsBuildConfig) {
|
if (needsBuildConfig) {
|
||||||
buildConfig.client = new URL('./static/', _config.outDir);
|
buildConfig.client = new URL(`.${_config.base}`, _config.outDir);
|
||||||
buildConfig.server = new URL('./', _config.outDir);
|
buildConfig.server = new URL(`.${SERVER_BUILD_FOLDER}`, _config.outDir);
|
||||||
buildConfig.serverEntry = '_worker.js';
|
buildConfig.serverEntry = '_worker.js';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'astro:build:done': async () => {
|
'astro:build:done': async () => {
|
||||||
const entryUrl = new URL(_buildConfig.serverEntry, _buildConfig.server);
|
const entryPath = fileURLToPath(new URL(_buildConfig.serverEntry, _buildConfig.server)),
|
||||||
const pkg = fileURLToPath(entryUrl);
|
entryUrl = new URL(_buildConfig.serverEntry, _config.outDir),
|
||||||
|
buildPath = fileURLToPath(entryUrl);
|
||||||
await esbuild.build({
|
await esbuild.build({
|
||||||
target: 'es2020',
|
target: 'es2020',
|
||||||
platform: 'browser',
|
platform: 'browser',
|
||||||
entryPoints: [pkg],
|
entryPoints: [entryPath],
|
||||||
outfile: pkg,
|
outfile: buildPath,
|
||||||
allowOverwrite: true,
|
allowOverwrite: true,
|
||||||
format: 'esm',
|
format: 'esm',
|
||||||
bundle: true,
|
bundle: true,
|
||||||
|
@ -107,8 +117,90 @@ export default function createIntegration(args?: Options): AstroIntegration {
|
||||||
});
|
});
|
||||||
|
|
||||||
// throw the server folder in the bin
|
// throw the server folder in the bin
|
||||||
const chunksUrl = new URL('./chunks', _buildConfig.server);
|
const serverUrl = new URL(_buildConfig.server);
|
||||||
await fs.promises.rm(chunksUrl, { recursive: true, force: true });
|
await fs.promises.rm(serverUrl, { recursive: true, force: true });
|
||||||
|
|
||||||
|
// move cloudflare specific files to the root
|
||||||
|
const cloudflareSpecialFiles = ['_headers', '_redirects', '_routes.json'];
|
||||||
|
if (_config.base !== '/') {
|
||||||
|
for (const file of cloudflareSpecialFiles) {
|
||||||
|
try {
|
||||||
|
await fs.promises.rename(
|
||||||
|
new URL(file, _buildConfig.client),
|
||||||
|
new URL(file, _config.outDir)
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const routesExists = await fs.promises
|
||||||
|
.stat(new URL('./_routes.json', _config.outDir))
|
||||||
|
.then((stat) => stat.isFile())
|
||||||
|
.catch(() => false);
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
const staticPathList: Array<string> = (
|
||||||
|
await glob(`${fileURLToPath(_buildConfig.client)}/**/*`, {
|
||||||
|
cwd: fileURLToPath(_config.outDir),
|
||||||
|
filesOnly: true,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.filter((file: string) => cloudflareSpecialFiles.indexOf(file) < 0)
|
||||||
|
.map((file: string) => `/${file}`);
|
||||||
|
|
||||||
|
const redirectsExists = await fs.promises
|
||||||
|
.stat(new URL('./_redirects', _config.outDir))
|
||||||
|
.then((stat) => stat.isFile())
|
||||||
|
.catch(() => false);
|
||||||
|
|
||||||
|
// convert all redirect source paths into a list of routes
|
||||||
|
// and add them to the static path
|
||||||
|
if (redirectsExists) {
|
||||||
|
const redirects = (
|
||||||
|
await fs.promises.readFile(new URL('./_redirects', _config.outDir), 'utf-8')
|
||||||
|
)
|
||||||
|
.split(os.EOL)
|
||||||
|
.map((line) => {
|
||||||
|
const parts = line.split(' ');
|
||||||
|
if (parts.length < 2) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
// convert /products/:id to /products/*
|
||||||
|
return (
|
||||||
|
parts[0]
|
||||||
|
.replace(/\/:.*?(?=\/|$)/g, '/*')
|
||||||
|
// remove query params as they are not supported by cloudflare
|
||||||
|
.replace(/\?.*$/, '')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(
|
||||||
|
(line, index, arr) => line !== null && arr.indexOf(line) === index
|
||||||
|
) as string[];
|
||||||
|
|
||||||
|
if (redirects.length > 0) {
|
||||||
|
staticPathList.push(...redirects);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
new URL('./_routes.json', _config.outDir),
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
version: 1,
|
||||||
|
include: ['/*'],
|
||||||
|
exclude: staticPathList,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (isModeDirectory) {
|
if (isModeDirectory) {
|
||||||
const functionsUrl = new URL(`file://${process.cwd()}/functions/`);
|
const functionsUrl = new URL(`file://${process.cwd()}/functions/`);
|
||||||
|
|
|
@ -15,12 +15,11 @@ export function createExports(manifest: SSRManifest) {
|
||||||
const fetch = async (request: Request, env: Env, context: any) => {
|
const fetch = async (request: Request, env: Env, context: any) => {
|
||||||
process.env = env as any;
|
process.env = env as any;
|
||||||
|
|
||||||
const { origin, pathname } = new URL(request.url);
|
const { pathname } = new URL(request.url);
|
||||||
|
|
||||||
// static assets
|
// static assets fallback, in case default _routes.json is not used
|
||||||
if (manifest.assets.has(pathname)) {
|
if (manifest.assets.has(pathname)) {
|
||||||
const assetRequest = new Request(`${origin}/static/${app.removeBase(pathname)}`, request);
|
return env.ASSETS.fetch(request);
|
||||||
return env.ASSETS.fetch(assetRequest);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let routeData = app.match(request, { matchNotFound: true });
|
let routeData = app.match(request, { matchNotFound: true });
|
||||||
|
|
|
@ -17,11 +17,10 @@ export function createExports(manifest: SSRManifest) {
|
||||||
} & Record<string, unknown>) => {
|
} & Record<string, unknown>) => {
|
||||||
process.env = runtimeEnv.env as any;
|
process.env = runtimeEnv.env as any;
|
||||||
|
|
||||||
const { origin, pathname } = new URL(request.url);
|
const { pathname } = new URL(request.url);
|
||||||
// static assets
|
// static assets fallback, in case default _routes.json is not used
|
||||||
if (manifest.assets.has(pathname)) {
|
if (manifest.assets.has(pathname)) {
|
||||||
const assetRequest = new Request(`${origin}/static/${app.removeBase(pathname)}`, request);
|
return next(request);
|
||||||
return next(assetRequest);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let routeData = app.match(request, { matchNotFound: true });
|
let routeData = app.match(request, { matchNotFound: true });
|
||||||
|
|
|
@ -2554,9 +2554,11 @@ importers:
|
||||||
cheerio: ^1.0.0-rc.11
|
cheerio: ^1.0.0-rc.11
|
||||||
esbuild: ^0.14.42
|
esbuild: ^0.14.42
|
||||||
mocha: ^9.2.2
|
mocha: ^9.2.2
|
||||||
|
tiny-glob: ^0.2.9
|
||||||
wrangler: ^2.0.23
|
wrangler: ^2.0.23
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.14.54
|
esbuild: 0.14.54
|
||||||
|
tiny-glob: 0.2.9
|
||||||
devDependencies:
|
devDependencies:
|
||||||
astro: link:../../astro
|
astro: link:../../astro
|
||||||
astro-scripts: link:../../../scripts
|
astro-scripts: link:../../../scripts
|
||||||
|
|
Loading…
Reference in a new issue