Skip to content

Commit dffab10

Browse files
authored
Merge branch 'feat/environment-api' into env-api-cf-entrypoint
2 parents e607fb9 + d5979c2 commit dffab10

66 files changed

Lines changed: 1505 additions & 26 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/astro/src/content/vite-plugin-content-imports.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { extname } from 'node:path';
33
import { pathToFileURL } from 'node:url';
44
import * as devalue from 'devalue';
55
import type { PluginContext } from 'rollup';
6-
import { isRunnableDevEnvironment, type Plugin, type RunnableDevEnvironment } from 'vite';
6+
import type { Plugin, RunnableDevEnvironment } from 'vite';
77
import { getProxyCode } from '../assets/utils/proxy.js';
88
import { AstroError } from '../core/errors/errors.js';
99
import { AstroErrorData } from '../core/errors/index.js';
@@ -158,9 +158,6 @@ export const _internal = {
158158
}
159159
},
160160
configureServer(viteServer) {
161-
if (!isRunnableDevEnvironment(viteServer.environments.ssr)) {
162-
return;
163-
}
164161
viteServer.watcher.on('all', async (event, entry) => {
165162
if (CHOKIDAR_MODIFIED_EVENTS.includes(event)) {
166163
const environment = viteServer.environments.ssr;

packages/astro/src/core/dev/dev.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { performance } from 'node:perf_hooks';
55
import { green } from 'kleur/colors';
66
import { gt, major, minor, patch } from 'semver';
77
import type * as vite from 'vite';
8-
import { isRunnableDevEnvironment } from 'vite';
98
import { getDataStoreFile, globalContentLayer } from '../../content/content-layer.js';
109
import { attachContentServerListeners } from '../../content/index.js';
1110
import { MutableDataStore } from '../../content/mutable-data-store.js';
@@ -96,9 +95,7 @@ export default async function dev(inlineConfig: AstroInlineConfig): Promise<DevS
9695
if (!store) {
9796
logger.error('content', 'Failed to create data store');
9897
}
99-
if (isRunnableDevEnvironment(restart.container.viteServer.environments.ssr)) {
100-
await attachContentServerListeners(restart.container);
101-
}
98+
await attachContentServerListeners(restart.container);
10299

103100
const config = globalContentConfigObserver.get();
104101
if (config.status === 'error') {

packages/astro/src/vite-plugin-head/index.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { ModuleInfo } from 'rollup';
22
import type * as vite from 'vite';
3-
import { type DevEnvironment, isRunnableDevEnvironment } from 'vite';
3+
import type { DevEnvironment } from 'vite';
44
import { getParentModuleInfos, getTopLevelPageModuleInfos } from '../core/build/graph.js';
55
import type { BuildInternals } from '../core/build/internal.js';
66
import type { AstroBuildPlugin } from '../core/build/plugin.js';
@@ -48,9 +48,6 @@ export default function configHeadVitePlugin(): vite.Plugin {
4848
enforce: 'pre',
4949
apply: 'serve',
5050
configureServer(server) {
51-
if (!isRunnableDevEnvironment(server.environments.ssr)) {
52-
return;
53-
}
5451
environment = server.environments.ssr;
5552
},
5653
resolveId(source, importer) {
@@ -63,7 +60,7 @@ export default function configHeadVitePlugin(): vite.Plugin {
6360
if (result) {
6461
let info = this.getModuleInfo(result.id);
6562
const astro = info && getAstroMetadata(info);
66-
if (astro && isRunnableDevEnvironment(environment)) {
63+
if (astro) {
6764
if (astro.propagation === 'self' || astro.propagation === 'in-tree') {
6865
propagateMetadata.call(this, importer, 'propagation', 'in-tree');
6966
}
@@ -77,9 +74,6 @@ export default function configHeadVitePlugin(): vite.Plugin {
7774
}
7875
},
7976
transform(source, id) {
80-
if (!isRunnableDevEnvironment(environment)) {
81-
return;
82-
}
8377
// TODO This could probably be removed now that this is handled in resolveId
8478
let info = this.getModuleInfo(id);
8579
if (info && getAstroMetadata(info)?.containsHead) {

packages/integrations/cloudflare/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"./entrypoints/middleware.js": "./dist/entrypoints/middleware.js",
2424
"./image-service": "./dist/entrypoints/image-service.js",
2525
"./image-endpoint": "./dist/entrypoints/image-endpoint.js",
26+
"./image-transform-endpoint": "./dist/entrypoints/image-transform-endpoint.js",
2627
"./handler": "./dist/utils/handler.js",
2728
"./package.json": "./package.json"
2829
},
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import type { APIRoute } from 'astro';
2+
import { transform } from '../utils/image-binding-transform.js';
3+
4+
export const prerender = false;
5+
6+
// @ts-expect-error The Header types between libdom and @cloudflare/workers-types are causing issues
7+
export const GET: APIRoute = async (ctx) => {
8+
// @ts-expect-error The runtime locals types are not populated here
9+
return transform(ctx.request.url, ctx.locals.runtime.env.IMAGES, ctx.locals.runtime.env.ASSETS);
10+
};

packages/integrations/cloudflare/src/index.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,23 @@ export type Options = {
101101
*/
102102
sessionKVBindingName?: string;
103103

104+
/**
105+
* When configured as `cloudflare-binding`, the Cloudflare Images binding will be used to transform images:
106+
* - https://developers.cloudflare.com/images/transform-images/bindings/
107+
*
108+
* By default, this will use the "IMAGES" binding name, but this can be customised in your `wrangler.json`:
109+
*
110+
* ```json
111+
* {
112+
* "images": {
113+
* "binding": "IMAGES" // <-- this should match `imagesBindingName`
114+
* }
115+
* }
116+
* ```
117+
*
118+
*/
119+
imagesBindingName?: string;
120+
104121
/**
105122
* This configuration option allows you to specify a custom entryPoint for your Cloudflare Worker.
106123
* The entry point is the file that will be executed when your Worker is invoked.
@@ -165,6 +182,17 @@ export default function createIntegration(args?: Options): AstroIntegration {
165182
}) => {
166183
let session = config.session;
167184

185+
if (args?.imageService === 'cloudflare-binding') {
186+
const bindingName = args?.imagesBindingName ?? 'IMAGES';
187+
188+
logger.info(
189+
`Enabling image processing with Cloudflare Images for production with the "${bindingName}" Images binding.`,
190+
);
191+
logger.info(
192+
`If you see the error "Invalid binding \`${bindingName}\`" in your build output, you need to add the binding to your wrangler config file.`,
193+
);
194+
}
195+
168196
if (!session?.driver) {
169197
logger.info(
170198
`Enabling sessions with Cloudflare KV with the "${SESSION_KV_BINDING_NAME}" KV binding.`,
@@ -356,7 +384,12 @@ export default function createIntegration(args?: Options): AstroIntegration {
356384
vite.define = {
357385
'process.env': 'process.env',
358386
// Allows the request handler to know what the binding name is
359-
'globalThis.__ASTRO_SESSION_BINDING_NAME': JSON.stringify(SESSION_KV_BINDING_NAME),
387+
'globalThis.__ASTRO_SESSION_BINDING_NAME': JSON.stringify(
388+
args?.sessionKVBindingName ?? 'SESSION',
389+
),
390+
'globalThis.__ASTRO_IMAGES_BINDING_NAME': JSON.stringify(
391+
args?.imagesBindingName ?? 'IMAGES',
392+
),
360393
...vite.define,
361394
};
362395
}

packages/integrations/cloudflare/src/utils/handler.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ declare global {
3131
// This is not a real global, but is injected using Vite define to allow us to specify the session binding name in the config.
3232
var __ASTRO_SESSION_BINDING_NAME: string;
3333

34+
// This is not a real global, but is injected using Vite define to allow us to specify the Images binding name in the config.
35+
// eslint-disable-next-line no-var
36+
var __ASTRO_IMAGES_BINDING_NAME: string;
37+
3438
// Just used to pass the KV binding to unstorage.
3539
var __env__: Partial<Env>;
3640
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// @ts-expect-error Not sure how to make this typecheck properly
2+
import { imageConfig } from 'astro:assets';
3+
import { isRemotePath } from '@astrojs/internal-helpers/path';
4+
import { isRemoteAllowed } from '@astrojs/internal-helpers/remote';
5+
6+
import type {
7+
Fetcher,
8+
ImagesBinding,
9+
ImageTransform,
10+
ReadableStream,
11+
} from '@cloudflare/workers-types';
12+
13+
export async function transform(rawUrl: string, images: ImagesBinding, assets: Fetcher) {
14+
const url = new URL(rawUrl);
15+
16+
const href = url.searchParams.get('href');
17+
18+
if (!href || (isRemotePath(href) && !isRemoteAllowed(href, imageConfig))) {
19+
return new Response('Forbidden', { status: 403 });
20+
}
21+
22+
const imageSrc = new URL(href, url.origin);
23+
const content = await (isRemotePath(href) ? fetch(imageSrc) : assets.fetch(imageSrc));
24+
if (!content.body) {
25+
return new Response(null, { status: 404 });
26+
}
27+
const input = images.input(content.body as ReadableStream);
28+
29+
const format = url.searchParams.get('f');
30+
31+
if (!format || !['avif', 'webp', 'jpeg'].includes(format)) {
32+
return new Response(`The "${format}" format is not supported`, { status: 400 });
33+
}
34+
35+
return (
36+
await input
37+
.transform({
38+
width: url.searchParams.has('w') ? parseInt(url.searchParams.get('w')!) : undefined,
39+
height: url.searchParams.has('h') ? parseInt(url.searchParams.get('h')!) : undefined,
40+
// `quality` is documented, but doesn't appear to work in manual testing...
41+
// quality: url.searchParams.get('q'),
42+
fit: url.searchParams.get('fit') as ImageTransform['fit'],
43+
})
44+
.output({ format: `image/${format as 'webp' | 'avif' | 'jpeg'}` })
45+
).response();
46+
}

packages/integrations/cloudflare/src/utils/image-config.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import type { AstroConfig, AstroIntegrationLogger, HookParameters } from 'astro';
22
import { passthroughImageService, sharpImageService } from 'astro/config';
33

4-
export type ImageService = 'passthrough' | 'cloudflare' | 'compile' | 'custom';
4+
export type ImageService =
5+
| 'passthrough'
6+
| 'cloudflare'
7+
| 'cloudflare-binding'
8+
| 'compile'
9+
| 'custom';
510

611
export function setImageConfig(
712
service: ImageService,
@@ -21,6 +26,13 @@ export function setImageConfig(
2126
? sharpImageService()
2227
: { entrypoint: '@astrojs/cloudflare/image-service' },
2328
};
29+
case 'cloudflare-binding':
30+
return {
31+
...config,
32+
endpoint: {
33+
entrypoint: '@astrojs/cloudflare/image-transform-endpoint',
34+
},
35+
};
2436

2537
case 'compile':
2638
return {

packages/integrations/cloudflare/test/fixtures/vite-plugin/astro.config.mjs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,19 @@
22
import cloudflare from '@astrojs/cloudflare';
33
import { defineConfig } from 'astro/config';
44

5+
import mdx from '@astrojs/mdx';
6+
import { fileURLToPath } from 'node:url';
57

68
export default defineConfig({
79
adapter: cloudflare({
10+
imageService: 'cloudflare-binding',
811
}),
12+
vite: {
13+
resolve: {
14+
alias: {
15+
'@images': fileURLToPath(new URL('./images', import.meta.url)),
16+
},
17+
},
18+
},
19+
integrations: [mdx()],
920
});

0 commit comments

Comments
 (0)