Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: improve support for Workers Assets beta #406

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
21 changes: 21 additions & 0 deletions .changeset/honest-ghosts-float.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
'@astrojs/cloudflare': minor
---

Adds experimental support for Cloudflare Workers Assets mode. To use this, update your settings as follows:

```diff
import cloudflare from '@astrojs/cloudflare';
import { defineConfig } from 'astro/config';

export default defineConfig({
output: 'server',
adapter: cloudflare({
+ experimental: {
+ workerAssets: true,
+ }
}),
});
```

Note: Currently Cloudflare Workers Assets mode, does not read any of `_headers`, `_redirects`, nor `_routes.json` files.
145 changes: 81 additions & 64 deletions packages/cloudflare/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,16 @@ export type Options = {
* for reference on how these file types are exported
*/
cloudflareModules?: boolean;

/**
* Lists all experimental features the adapter supports.
*/
experimental?: {
/**
* Enables support for Cloudflare Workers assets. Defaults to false.
*/
workerAssets?: boolean;
};
};

function wrapWithSlashes(path: string): string {
Expand Down Expand Up @@ -103,12 +113,17 @@ export default function createIntegration(args?: Options): AstroIntegration {
addWatchFile,
addMiddleware,
}) => {
let clientURL = new URL(`.${wrapWithSlashes(config.base)}`, config.outDir);
if (args?.experimental?.workerAssets) {
clientURL = new URL(`./assets/${wrapWithSlashes(config.base)}`, config.outDir);
}

updateConfig({
build: {
client: new URL(`.${wrapWithSlashes(config.base)}`, config.outDir),
client: clientURL,
server: new URL('./_worker.js/', config.outDir),
serverEntry: 'index.js',
redirects: false,
redirects: !!args?.experimental?.workerAssets,
},
vite: {
plugins: [
Expand Down Expand Up @@ -281,79 +296,81 @@ export default function createIntegration(args?: Options): AstroIntegration {
}
}

let redirectsExists = false;
try {
const redirectsStat = await stat(new URL('./_redirects', _config.outDir));
if (redirectsStat.isFile()) {
redirectsExists = true;
if (!args?.experimental?.workerAssets) {
let redirectsExists = false;
try {
const redirectsStat = await stat(new URL('./_redirects', _config.outDir));
if (redirectsStat.isFile()) {
redirectsExists = true;
}
} catch (error) {
redirectsExists = false;
}
} catch (error) {
redirectsExists = false;
}

const redirects: IntegrationRouteData['segments'][] = [];
if (redirectsExists) {
const rl = createInterface({
input: createReadStream(new URL('./_redirects', _config.outDir)),
crlfDelay: Number.POSITIVE_INFINITY,
});
const redirects: IntegrationRouteData['segments'][] = [];
if (redirectsExists) {
const rl = createInterface({
input: createReadStream(new URL('./_redirects', _config.outDir)),
crlfDelay: Number.POSITIVE_INFINITY,
});

for await (const line of rl) {
const parts = line.split(' ');
if (parts.length >= 2) {
const p = removeLeadingForwardSlash(parts[0])
.split('/')
.filter(Boolean)
.map((s: string) => {
const syntax = s
.replace(/\/:.*?(?=\/|$)/g, '/*')
// remove query params as they are not supported by cloudflare
.replace(/\?.*$/, '');
return getParts(syntax);
});
redirects.push(p);
for await (const line of rl) {
const parts = line.split(' ');
if (parts.length >= 2) {
const p = removeLeadingForwardSlash(parts[0])
.split('/')
.filter(Boolean)
.map((s: string) => {
const syntax = s
.replace(/\/:.*?(?=\/|$)/g, '/*')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Regex created in a for loop are wasteful. They can be created in a top-level function and re-used

// remove query params as they are not supported by cloudflare
.replace(/\?.*$/, '');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here

return getParts(syntax);
});
redirects.push(p);
}
}
}
}

let routesExists = false;
try {
const routesStat = await stat(new URL('./_routes.json', _config.outDir));
if (routesStat.isFile()) {
routesExists = true;
let routesExists = false;
try {
const routesStat = await stat(new URL('./_routes.json', _config.outDir));
if (routesStat.isFile()) {
routesExists = true;
}
} catch (error) {
routesExists = false;
}
} catch (error) {
routesExists = false;
}

if (!routesExists) {
await createRoutesFile(
_config,
logger,
routes,
pages,
redirects,
args?.routes?.extend?.include,
args?.routes?.extend?.exclude
);
}
if (!routesExists) {
await createRoutesFile(
_config,
logger,
routes,
pages,
redirects,
args?.routes?.extend?.include,
args?.routes?.extend?.exclude
);
}

const redirectRoutes: [IntegrationRouteData, string][] = [];
for (const route of routes) {
if (route.type === 'redirect') redirectRoutes.push([route, '']);
}
const redirectRoutes: [IntegrationRouteData, string][] = [];
for (const route of routes) {
if (route.type === 'redirect') redirectRoutes.push([route, '']);
}

const trueRedirects = createRedirectsFromAstroRoutes({
config: _config,
routeToDynamicTargetMap: new Map(Array.from(redirectRoutes)),
dir,
});
const trueRedirects = createRedirectsFromAstroRoutes({
config: _config,
routeToDynamicTargetMap: new Map(Array.from(redirectRoutes)),
dir,
});

if (!trueRedirects.empty()) {
try {
await appendFile(new URL('./_redirects', _config.outDir), trueRedirects.print());
} catch (error) {
logger.error('Failed to write _redirects file');
if (!trueRedirects.empty()) {
try {
await appendFile(new URL('./_redirects', _config.outDir), trueRedirects.print());
} catch (error) {
logger.error('Failed to write _redirects file');
}
}
}
},
Expand Down