mirror of
https://github.com/withastro/astro.git
synced 2024-12-16 21:46:22 -05:00
feature(astrojs/cloudflare): add support for splitted
SSR bundles (#7464)
* initial commit * try to fix windows * output files directly into the correct folder * allow for rest parameters * use fixed hook * improve tests * apply doc's team suggestions for README Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca> * try to fix prerendering * apply doc's team suggestion for changeset Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca> * bump to minor * readme update * resolve review comments * optimize memory allocation * resolve review comments * add removed link, to make sure old docs keep same * resolve comment Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca> --------- Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca> Co-authored-by: Chris Swithinbank <swithinbank@gmail.com>
This commit is contained in:
parent
7ae6e89292
commit
1a59185ddd
16 changed files with 337 additions and 45 deletions
7
.changeset/healthy-books-study.md
Normal file
7
.changeset/healthy-books-study.md
Normal file
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
'@astrojs/cloudflare': minor
|
||||
---
|
||||
|
||||
Split Support in Cloudflare
|
||||
|
||||
Adds support for configuring `build.split` when using the Cloudflare adapter
|
|
@ -48,7 +48,11 @@ Cloudflare Pages has 2 different modes for deploying functions, `advanced` mode
|
|||
|
||||
For most projects the adapter default of `advanced` will be sufficient; the `dist` folder will contain your compiled project. Switching to directory mode allows you to use [pages plugins](https://developers.cloudflare.com/pages/platform/functions/plugins/) such as [Sentry](https://developers.cloudflare.com/pages/platform/functions/plugins/sentry/) or write custom code to enable logging.
|
||||
|
||||
In directory mode the adapter will compile the client side part of your app the same way, but moves the worker script into a `functions` folder in the project root. The adapter will only ever place a `[[path]].js` in that folder, allowing you to add additional plugins and pages middleware which can be checked into version control. Cloudflare documentation contains more information about [writing custom functions](https://developers.cloudflare.com/pages/platform/functions/).
|
||||
In directory mode, the adapter will compile the client side part of your app the same way by default, but moves the worker script into a `functions` folder in the project root. In this case, the adapter will only ever place a `[[path]].js` in that folder, allowing you to add additional plugins and pages middleware which can be checked into version control.
|
||||
|
||||
With the build configuration `split: true`, the adapter instead compiles a separate bundle for each page. This option requires some manual maintenance of the `functions` folder. Files emitted by Astro will overwrite existing `functions` files with identical names, so you must choose unique file names for each file you manually add. Additionally, the adapter will never empty the `functions` folder of outdated files, so you must clean up the folder manually when you remove pages.
|
||||
|
||||
Note that this adapter does not support using [Cloudflare Pages Middleware](https://developers.cloudflare.com/pages/platform/functions/middleware/). Astro will bundle the [Astro middleware](https://docs.astro.build/en/guides/middleware/) into each page.
|
||||
|
||||
```ts
|
||||
// directory mode
|
||||
|
|
|
@ -35,7 +35,8 @@
|
|||
"build": "astro-scripts build \"src/**/*.ts\" && tsc",
|
||||
"build:ci": "astro-scripts build \"src/**/*.ts\"",
|
||||
"dev": "astro-scripts dev \"src/**/*.ts\"",
|
||||
"test": "mocha --exit --timeout 30000 test/"
|
||||
"test": "mocha --exit --timeout 30000 test/",
|
||||
"test:match": "mocha --exit --timeout 30000 -g"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/underscore-redirects": "^0.1.0",
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { createRedirectsFromAstroRoutes } from '@astrojs/underscore-redirects';
|
||||
import type { AstroAdapter, AstroConfig, AstroIntegration } from 'astro';
|
||||
import type { AstroAdapter, AstroConfig, AstroIntegration, RouteData } from 'astro';
|
||||
import esbuild from 'esbuild';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import { dirname } from 'path';
|
||||
import glob from 'tiny-glob';
|
||||
import { fileURLToPath, pathToFileURL } from 'url';
|
||||
|
||||
|
@ -14,20 +15,21 @@ interface BuildConfig {
|
|||
server: URL;
|
||||
client: URL;
|
||||
serverEntry: string;
|
||||
split?: boolean;
|
||||
}
|
||||
|
||||
export function getAdapter(isModeDirectory: boolean): AstroAdapter {
|
||||
return isModeDirectory
|
||||
? {
|
||||
name: '@astrojs/cloudflare',
|
||||
serverEntrypoint: '@astrojs/cloudflare/server.directory.js',
|
||||
exports: ['onRequest'],
|
||||
}
|
||||
name: '@astrojs/cloudflare',
|
||||
serverEntrypoint: '@astrojs/cloudflare/server.directory.js',
|
||||
exports: ['onRequest', 'manifest'],
|
||||
}
|
||||
: {
|
||||
name: '@astrojs/cloudflare',
|
||||
serverEntrypoint: '@astrojs/cloudflare/server.advanced.js',
|
||||
exports: ['default'],
|
||||
};
|
||||
name: '@astrojs/cloudflare',
|
||||
serverEntrypoint: '@astrojs/cloudflare/server.advanced.js',
|
||||
exports: ['default'],
|
||||
};
|
||||
}
|
||||
|
||||
const SHIM = `globalThis.process = {
|
||||
|
@ -41,6 +43,7 @@ export default function createIntegration(args?: Options): AstroIntegration {
|
|||
let _config: AstroConfig;
|
||||
let _buildConfig: BuildConfig;
|
||||
const isModeDirectory = args?.mode === 'directory';
|
||||
let _entryPoints = new Map<RouteData, URL>();
|
||||
|
||||
return {
|
||||
name: '@astrojs/cloudflare',
|
||||
|
@ -90,35 +93,99 @@ export default function createIntegration(args?: Options): AstroIntegration {
|
|||
vite.ssr.target = 'webworker';
|
||||
}
|
||||
},
|
||||
'astro:build:ssr': ({ manifest, entryPoints }) => {
|
||||
_entryPoints = entryPoints;
|
||||
},
|
||||
'astro:build:done': async ({ pages, routes, dir }) => {
|
||||
const entryPath = fileURLToPath(new URL(_buildConfig.serverEntry, _buildConfig.server));
|
||||
const entryUrl = new URL(_buildConfig.serverEntry, _config.outDir);
|
||||
const buildPath = fileURLToPath(entryUrl);
|
||||
// A URL for the final build path after renaming
|
||||
const finalBuildUrl = pathToFileURL(buildPath.replace(/\.mjs$/, '.js'));
|
||||
const functionsUrl = new URL('functions/', _config.root);
|
||||
|
||||
await esbuild.build({
|
||||
target: 'es2020',
|
||||
platform: 'browser',
|
||||
conditions: ['workerd', 'worker', 'browser'],
|
||||
entryPoints: [entryPath],
|
||||
outfile: buildPath,
|
||||
allowOverwrite: true,
|
||||
format: 'esm',
|
||||
bundle: true,
|
||||
minify: _config.vite?.build?.minify !== false,
|
||||
banner: {
|
||||
js: SHIM,
|
||||
},
|
||||
logOverride: {
|
||||
'ignored-bare-import': 'silent',
|
||||
},
|
||||
});
|
||||
if (isModeDirectory) {
|
||||
await fs.promises.mkdir(functionsUrl, { recursive: true });
|
||||
}
|
||||
|
||||
// Rename to worker.js
|
||||
await fs.promises.rename(buildPath, finalBuildUrl);
|
||||
if (isModeDirectory && _buildConfig.split) {
|
||||
const entryPointsRouteData = [..._entryPoints.keys()]
|
||||
const entryPointsURL = [..._entryPoints.values()]
|
||||
const entryPaths = entryPointsURL.map((entry) => fileURLToPath(entry));
|
||||
const outputDir = fileURLToPath(new URL('.astro', _buildConfig.server));
|
||||
|
||||
// throw the server folder in the bin
|
||||
// NOTE: AFAIK, esbuild keeps the order of the entryPoints array
|
||||
const { outputFiles } = await esbuild.build({
|
||||
target: 'es2020',
|
||||
platform: 'browser',
|
||||
conditions: ['workerd', 'worker', 'browser'],
|
||||
entryPoints: entryPaths,
|
||||
outdir: outputDir,
|
||||
allowOverwrite: true,
|
||||
format: 'esm',
|
||||
bundle: true,
|
||||
minify: _config.vite?.build?.minify !== false,
|
||||
banner: {
|
||||
js: SHIM,
|
||||
},
|
||||
logOverride: {
|
||||
'ignored-bare-import': 'silent',
|
||||
},
|
||||
write: false,
|
||||
});
|
||||
|
||||
// loop through all bundled files and write them to the functions folder
|
||||
for (const [index, outputFile] of outputFiles.entries()) {
|
||||
// we need to make sure the filename in the functions folder
|
||||
// matches to cloudflares routing capabilities (see their docs)
|
||||
// IN: src/pages/[language]/files/[...path].astro
|
||||
// OUT: [language]/files/[[path]].js
|
||||
const fileName = entryPointsRouteData[index].component
|
||||
.replace('src/pages/', '')
|
||||
.replace('.astro', '.js')
|
||||
.replace(/(\[\.\.\.)(\w+)(\])/g, (_match, _p1, p2, _p3) => {
|
||||
return `[[${p2}]]`;
|
||||
});
|
||||
|
||||
const fileUrl = new URL(fileName, functionsUrl)
|
||||
const newFileDir = dirname(fileURLToPath(fileUrl));
|
||||
if (!fs.existsSync(newFileDir)) {
|
||||
fs.mkdirSync(newFileDir, { recursive: true });
|
||||
}
|
||||
await fs.promises.writeFile(fileUrl, outputFile.contents);
|
||||
}
|
||||
|
||||
} else {
|
||||
const entryPath = fileURLToPath(new URL(_buildConfig.serverEntry, _buildConfig.server));
|
||||
const entryUrl = new URL(_buildConfig.serverEntry, _config.outDir);
|
||||
const buildPath = fileURLToPath(entryUrl);
|
||||
// A URL for the final build path after renaming
|
||||
const finalBuildUrl = pathToFileURL(buildPath.replace(/\.mjs$/, '.js'));
|
||||
|
||||
await esbuild.build({
|
||||
target: 'es2020',
|
||||
platform: 'browser',
|
||||
conditions: ['workerd', 'worker', 'browser'],
|
||||
entryPoints: [entryPath],
|
||||
outfile: buildPath,
|
||||
allowOverwrite: true,
|
||||
format: 'esm',
|
||||
bundle: true,
|
||||
minify: _config.vite?.build?.minify !== false,
|
||||
banner: {
|
||||
js: SHIM,
|
||||
},
|
||||
logOverride: {
|
||||
'ignored-bare-import': 'silent',
|
||||
},
|
||||
});
|
||||
|
||||
// Rename to worker.js
|
||||
await fs.promises.rename(buildPath, finalBuildUrl);
|
||||
|
||||
if (isModeDirectory) {
|
||||
const directoryUrl = new URL('[[path]].js', functionsUrl);
|
||||
await fs.promises.rename(finalBuildUrl, directoryUrl);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// // // throw the server folder in the bin
|
||||
const serverUrl = new URL(_buildConfig.server);
|
||||
await fs.promises.rm(serverUrl, { recursive: true, force: true });
|
||||
|
||||
|
@ -225,14 +292,6 @@ export default function createIntegration(args?: Options): AstroIntegration {
|
|||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (isModeDirectory) {
|
||||
const functionsUrl = new URL('functions/', _config.root);
|
||||
await fs.promises.mkdir(functionsUrl, { recursive: true });
|
||||
|
||||
const directoryUrl = new URL('[[path]].js', functionsUrl);
|
||||
await fs.promises.rename(finalBuildUrl, directoryUrl);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -61,5 +61,5 @@ export function createExports(manifest: SSRManifest) {
|
|||
});
|
||||
};
|
||||
|
||||
return { onRequest };
|
||||
return { onRequest, manifest };
|
||||
}
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
import { loadFixture } from './test-utils.js';
|
||||
import { expect } from 'chai';
|
||||
import cloudflare from '../dist/index.js';
|
||||
|
||||
/** @type {import('./test-utils').Fixture} */
|
||||
describe('Cloudflare SSR split', () => {
|
||||
let fixture;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/split/',
|
||||
adapter: cloudflare({ mode: 'directory' }),
|
||||
output: "server",
|
||||
build: {
|
||||
split: true,
|
||||
excludeMiddleware: false
|
||||
},
|
||||
vite: {
|
||||
build: {
|
||||
minify: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
await fixture.build();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
fixture.clean();
|
||||
});
|
||||
|
||||
it('generates functions folders inside the project root, and checks that each page is emitted by astro', async () => {
|
||||
expect(await fixture.pathExists('../functions')).to.be.true;
|
||||
expect(await fixture.pathExists('../functions/index.js')).to.be.true;
|
||||
expect(await fixture.pathExists('../functions/blog/cool.js')).to.be.true;
|
||||
expect(await fixture.pathExists('../functions/blog/[post].js')).to.be.true;
|
||||
expect(await fixture.pathExists('../functions/[person]/[car].js')).to.be.true;
|
||||
expect(await fixture.pathExists('../functions/files/[[path]].js')).to.be.true;
|
||||
expect(await fixture.pathExists('../functions/[language]/files/[[path]].js')).to.be.true;
|
||||
});
|
||||
|
||||
it('generates pre-rendered files', async () => {
|
||||
expect(await fixture.pathExists('./prerender/index.html')).to.be.true;
|
||||
});
|
||||
});
|
9
packages/integrations/cloudflare/test/fixtures/split/package.json
vendored
Normal file
9
packages/integrations/cloudflare/test/fixtures/split/package.json
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"name": "@test/astro-cloudflare-split",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "workspace:*",
|
||||
"astro": "workspace:*"
|
||||
}
|
||||
}
|
10
packages/integrations/cloudflare/test/fixtures/split/src/middleware.ts
vendored
Normal file
10
packages/integrations/cloudflare/test/fixtures/split/src/middleware.ts
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { defineMiddleware } from "astro/middleware";
|
||||
|
||||
export const onRequest = defineMiddleware(({ locals, request }, next) => {
|
||||
// intercept response data from a request
|
||||
// optionally, transform the response by modifying `locals`
|
||||
locals.title = "New title"
|
||||
|
||||
// return a Response or the result of calling `next()`
|
||||
return next()
|
||||
});
|
37
packages/integrations/cloudflare/test/fixtures/split/src/pages/[language]/files/[...path].astro
vendored
Normal file
37
packages/integrations/cloudflare/test/fixtures/split/src/pages/[language]/files/[...path].astro
vendored
Normal file
|
@ -0,0 +1,37 @@
|
|||
---
|
||||
const files = [
|
||||
{
|
||||
slug: undefined,
|
||||
title: 'Root level',
|
||||
},
|
||||
{
|
||||
slug: 'test.png',
|
||||
title: "One level"
|
||||
},
|
||||
{
|
||||
slug: 'assets/test.png',
|
||||
title: "Two levels"
|
||||
},
|
||||
{
|
||||
slug: 'assets/images/test.png',
|
||||
title: 'Three levels',
|
||||
}
|
||||
];
|
||||
|
||||
const { path } = Astro.params;
|
||||
const page = files.find((page) => page.slug === path);
|
||||
const { title } = page;
|
||||
|
||||
---
|
||||
<html>
|
||||
<body>
|
||||
<h1>Files / Rest Parameters / {title}</h1>
|
||||
<p>DEBUG: {path} </p>
|
||||
<p><a href="/">index</a></p>
|
||||
</body>
|
||||
<style>
|
||||
h1 {
|
||||
background-color: yellow;
|
||||
}
|
||||
</style>
|
||||
</html>
|
14
packages/integrations/cloudflare/test/fixtures/split/src/pages/[person]/[car].astro
vendored
Normal file
14
packages/integrations/cloudflare/test/fixtures/split/src/pages/[person]/[car].astro
vendored
Normal file
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
const { person, car } = Astro.params;
|
||||
---
|
||||
<html>
|
||||
<body>
|
||||
<h1> {person} / {car}</h1>
|
||||
<p><a href="/">index</a></p>
|
||||
</body>
|
||||
<style>
|
||||
h1 {
|
||||
background-color: blue;
|
||||
}
|
||||
</style>
|
||||
</html>
|
14
packages/integrations/cloudflare/test/fixtures/split/src/pages/blog/[post].astro
vendored
Normal file
14
packages/integrations/cloudflare/test/fixtures/split/src/pages/blog/[post].astro
vendored
Normal file
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
const { post } = Astro.params;
|
||||
---
|
||||
<html>
|
||||
<body>
|
||||
<h1>Blog / {post}</h1>
|
||||
<p><a href="/">index</a></p>
|
||||
</body>
|
||||
<style>
|
||||
h1 {
|
||||
background-color: pink;
|
||||
}
|
||||
</style>
|
||||
</html>
|
11
packages/integrations/cloudflare/test/fixtures/split/src/pages/blog/cool.astro
vendored
Normal file
11
packages/integrations/cloudflare/test/fixtures/split/src/pages/blog/cool.astro
vendored
Normal file
|
@ -0,0 +1,11 @@
|
|||
<html>
|
||||
<body>
|
||||
<h1>Blog / Cool</h1>
|
||||
<p><a href="/">index</a></p>
|
||||
</body>
|
||||
<style>
|
||||
h1 {
|
||||
background-color: orange;
|
||||
}
|
||||
</style>
|
||||
</html>
|
37
packages/integrations/cloudflare/test/fixtures/split/src/pages/files/[...path].astro
vendored
Normal file
37
packages/integrations/cloudflare/test/fixtures/split/src/pages/files/[...path].astro
vendored
Normal file
|
@ -0,0 +1,37 @@
|
|||
---
|
||||
const files = [
|
||||
{
|
||||
slug: undefined,
|
||||
title: 'Root level',
|
||||
},
|
||||
{
|
||||
slug: 'test.png',
|
||||
title: "One level"
|
||||
},
|
||||
{
|
||||
slug: 'assets/test.png',
|
||||
title: "Two levels"
|
||||
},
|
||||
{
|
||||
slug: 'assets/images/test.png',
|
||||
title: 'Three levels',
|
||||
}
|
||||
];
|
||||
|
||||
const { path } = Astro.params;
|
||||
const page = files.find((page) => page.slug === path);
|
||||
const { title } = page;
|
||||
|
||||
---
|
||||
<html>
|
||||
<body>
|
||||
<h1>Files / Rest Parameters / {title}</h1>
|
||||
<p>DEBUG: {path} </p>
|
||||
<p><a href="/">index</a></p>
|
||||
</body>
|
||||
<style>
|
||||
h1 {
|
||||
background-color: yellow;
|
||||
}
|
||||
</style>
|
||||
</html>
|
22
packages/integrations/cloudflare/test/fixtures/split/src/pages/index.astro
vendored
Normal file
22
packages/integrations/cloudflare/test/fixtures/split/src/pages/index.astro
vendored
Normal file
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
const data = Astro.locals;
|
||||
---
|
||||
<html>
|
||||
<body>
|
||||
<h1>Index</h1>
|
||||
<p>Middleware ({data.title})</p>
|
||||
<p><a href="/prerender/">prerender</a></p>
|
||||
<p><a href="/blog/cool/">sub-route</a></p>
|
||||
<p><a href="/blog/dynamic-post/">dynamic route in static sub-route</a></p>
|
||||
<p><a href="/mustermann/bmw/">dynamic route in dynamic sub-route</a></p>
|
||||
<p><a href="/files/">rest parameters root level</a></p>
|
||||
<p><a href="/files/test.png/">rest parameters one level</a></p>
|
||||
<p><a href="/files/assets/test.png/">rest parameters two level</a></p>
|
||||
<p><a href="/files/assets/images/test.png/">rest parameters three level</a></p>
|
||||
</body>
|
||||
<style>
|
||||
h1 {
|
||||
background-color: red;
|
||||
}
|
||||
</style>
|
||||
</html>
|
14
packages/integrations/cloudflare/test/fixtures/split/src/pages/prerender.astro
vendored
Normal file
14
packages/integrations/cloudflare/test/fixtures/split/src/pages/prerender.astro
vendored
Normal file
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
export const prerender = true;
|
||||
---
|
||||
<html>
|
||||
<body>
|
||||
<h1>Prerender</h1>
|
||||
<p><a href="/">index</a></p>
|
||||
</body>
|
||||
<style>
|
||||
h1 {
|
||||
background-color: yellow;
|
||||
}
|
||||
</style>
|
||||
</html>
|
|
@ -3675,6 +3675,15 @@ importers:
|
|||
specifier: workspace:*
|
||||
version: link:../../../../../astro
|
||||
|
||||
packages/integrations/cloudflare/test/fixtures/split:
|
||||
dependencies:
|
||||
'@astrojs/cloudflare':
|
||||
specifier: workspace:*
|
||||
version: link:../../..
|
||||
astro:
|
||||
specifier: workspace:*
|
||||
version: link:../../../../../astro
|
||||
|
||||
packages/integrations/cloudflare/test/fixtures/with-solid-js:
|
||||
dependencies:
|
||||
'@astrojs/cloudflare':
|
||||
|
|
Loading…
Reference in a new issue