@astrojs/cloudflare 13.0.0-beta.7 → 13.0.0-beta.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/entrypoints/{image-service.js → image-service-external.js} +2 -2
- package/dist/entrypoints/image-service-workerd.d.ts +14 -0
- package/dist/entrypoints/image-service-workerd.js +11 -0
- package/dist/entrypoints/image-transform-endpoint.js +21 -2
- package/dist/entrypoints/preview.js +4 -7
- package/dist/esbuild-plugin-astro-frontmatter.js +2 -1
- package/dist/index.d.ts +7 -29
- package/dist/index.js +65 -50
- package/dist/prerender-types.d.ts +10 -0
- package/dist/prerenderer.d.ts +4 -1
- package/dist/prerenderer.js +36 -8
- package/dist/utils/generate-routes-json.d.ts +1 -8
- package/dist/utils/generate-routes-json.js +0 -203
- package/dist/utils/handler.js +15 -2
- package/dist/utils/image-binding-transform.js +14 -6
- package/dist/utils/image-config.d.ts +17 -4
- package/dist/utils/image-config.js +30 -8
- package/dist/utils/prerender-constants.d.ts +2 -0
- package/dist/utils/prerender-constants.js +2 -0
- package/dist/utils/prerender.d.ts +3 -0
- package/dist/utils/prerender.js +34 -1
- package/dist/utils/static-image-collection.d.ts +7 -0
- package/dist/utils/static-image-collection.js +51 -0
- package/dist/vite-plugin-config.d.ts +7 -0
- package/dist/wrangler.d.ts +3 -3
- package/package.json +8 -8
- package/dist/entrypoints/image-endpoint.d.ts +0 -3
- package/dist/entrypoints/image-endpoint.js +0 -29
- /package/dist/entrypoints/{image-service.d.ts → image-service-external.d.ts} +0 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { LocalImageService } from 'astro';
|
|
2
|
+
/**
|
|
3
|
+
* Workerd-compatible image service stub.
|
|
4
|
+
*
|
|
5
|
+
* Handles getURL/getHTMLAttributes/addStaticImage in the workerd prerender
|
|
6
|
+
* environment without importing Sharp. Actual image transforms are handled
|
|
7
|
+
* elsewhere:
|
|
8
|
+
* - `compile`: Sharp runs on the Node side via the generation pipeline
|
|
9
|
+
* - `cloudflare-binding`: image-transform-endpoint uses the IMAGES binding directly
|
|
10
|
+
*
|
|
11
|
+
* transform() is a passthrough so the generic endpoint can call it in dev.
|
|
12
|
+
*/
|
|
13
|
+
declare const service: LocalImageService;
|
|
14
|
+
export default service;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { baseService } from "astro/assets";
|
|
2
|
+
const service = {
|
|
3
|
+
...baseService,
|
|
4
|
+
async transform(inputBuffer, transform) {
|
|
5
|
+
return { data: inputBuffer, format: transform.format };
|
|
6
|
+
}
|
|
7
|
+
};
|
|
8
|
+
var image_service_workerd_default = service;
|
|
9
|
+
export {
|
|
10
|
+
image_service_workerd_default as default
|
|
11
|
+
};
|
|
@@ -1,8 +1,27 @@
|
|
|
1
|
-
import { transform } from "../utils/image-binding-transform.js";
|
|
2
1
|
import { env } from "cloudflare:workers";
|
|
2
|
+
import { transform } from "../utils/image-binding-transform.js";
|
|
3
3
|
const prerender = false;
|
|
4
4
|
const GET = async (ctx) => {
|
|
5
|
-
|
|
5
|
+
const cache = caches.default;
|
|
6
|
+
if (cache) {
|
|
7
|
+
const cached = await cache.match(ctx.request.url);
|
|
8
|
+
if (cached) return cached;
|
|
9
|
+
}
|
|
10
|
+
const response = await transform(ctx.request.url, env.IMAGES, env.ASSETS);
|
|
11
|
+
if (!response.ok) return response;
|
|
12
|
+
const headers = new Headers(response.headers);
|
|
13
|
+
headers.set("Cache-Control", "public, max-age=31536000, immutable");
|
|
14
|
+
const cachedResponse = new Response(response.body, {
|
|
15
|
+
status: response.status,
|
|
16
|
+
headers
|
|
17
|
+
});
|
|
18
|
+
if (cache) {
|
|
19
|
+
const cfContext = ctx.locals.cfContext;
|
|
20
|
+
if (cfContext) {
|
|
21
|
+
cfContext.waitUntil(cache.put(ctx.request.url, cachedResponse.clone()));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return cachedResponse;
|
|
6
25
|
};
|
|
7
26
|
export {
|
|
8
27
|
GET,
|
|
@@ -5,7 +5,6 @@ import { fileURLToPath } from "node:url";
|
|
|
5
5
|
import { cloudflare as cfVitePlugin } from "@cloudflare/vite-plugin";
|
|
6
6
|
import colors from "piccolore";
|
|
7
7
|
import { performance } from "node:perf_hooks";
|
|
8
|
-
import { cloudflareConfigCustomizer } from "../wrangler.js";
|
|
9
8
|
const createPreviewServer = async ({
|
|
10
9
|
logger,
|
|
11
10
|
base,
|
|
@@ -17,10 +16,6 @@ const createPreviewServer = async ({
|
|
|
17
16
|
}) => {
|
|
18
17
|
const startServerTime = performance.now();
|
|
19
18
|
let previewServer;
|
|
20
|
-
const cfPluginConfig = {
|
|
21
|
-
viteEnvironment: { name: "ssr" },
|
|
22
|
-
config: cloudflareConfigCustomizer()
|
|
23
|
-
};
|
|
24
19
|
try {
|
|
25
20
|
previewServer = await preview({
|
|
26
21
|
configFile: false,
|
|
@@ -37,7 +32,9 @@ const createPreviewServer = async ({
|
|
|
37
32
|
open: false,
|
|
38
33
|
allowedHosts: []
|
|
39
34
|
},
|
|
40
|
-
plugins: [
|
|
35
|
+
plugins: [
|
|
36
|
+
cfVitePlugin({ ...globalThis.astroCloudflareOptions, viteEnvironment: { name: "ssr" } })
|
|
37
|
+
]
|
|
41
38
|
});
|
|
42
39
|
} catch (err) {
|
|
43
40
|
if (err instanceof Error) {
|
|
@@ -83,7 +80,7 @@ function serverStart({
|
|
|
83
80
|
host,
|
|
84
81
|
base
|
|
85
82
|
}) {
|
|
86
|
-
const version = "13.0.0-beta.
|
|
83
|
+
const version = "13.0.0-beta.9";
|
|
87
84
|
const localPrefix = `${colors.dim("\u2503")} Local `;
|
|
88
85
|
const networkPrefix = `${colors.dim("\u2503")} Network `;
|
|
89
86
|
const emptyPrefix = " ".repeat(11);
|
|
@@ -9,8 +9,9 @@ function astroFrontmatterScanPlugin() {
|
|
|
9
9
|
const code = await readFile(args.path, "utf-8");
|
|
10
10
|
const frontmatterMatch = FRONTMATTER_RE.exec(code);
|
|
11
11
|
if (frontmatterMatch) {
|
|
12
|
+
const contents = frontmatterMatch[1].replace(/\breturn\b/g, "throw ");
|
|
12
13
|
return {
|
|
13
|
-
contents
|
|
14
|
+
contents,
|
|
14
15
|
loader: "ts"
|
|
15
16
|
};
|
|
16
17
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,33 +1,10 @@
|
|
|
1
|
+
import { type PluginConfig } from '@cloudflare/vite-plugin';
|
|
1
2
|
import type { AstroIntegration } from 'astro';
|
|
2
|
-
import { type
|
|
3
|
+
import { type ImageServiceConfig } from './utils/image-config.js';
|
|
3
4
|
export type { Runtime } from './utils/handler.js';
|
|
4
|
-
export
|
|
5
|
+
export interface Options extends Pick<PluginConfig, 'auxiliaryWorkers' | 'configPath' | 'inspectorPort' | 'persistState' | 'remoteBindings'> {
|
|
5
6
|
/** Options for handling images. */
|
|
6
|
-
imageService?:
|
|
7
|
-
/** Configuration for `_routes.json` generation. A _routes.json file controls when your Function is invoked. This file will include three different properties:
|
|
8
|
-
*
|
|
9
|
-
* - version: Defines the version of the schema. Currently there is only one version of the schema (version 1), however, we may add more in the future and aim to be backwards compatible.
|
|
10
|
-
* - include: Defines routes that will be invoked by Functions. Accepts wildcard behavior.
|
|
11
|
-
* - exclude: Defines routes that will not be invoked by Functions. Accepts wildcard behavior. `exclude` always take priority over `include`.
|
|
12
|
-
*
|
|
13
|
-
* Wildcards match any number of path segments (slashes). For example, `/users/*` will match everything after the `/users/` path.
|
|
14
|
-
*
|
|
15
|
-
*/
|
|
16
|
-
routes?: {
|
|
17
|
-
/** Extend `_routes.json` */
|
|
18
|
-
extend: {
|
|
19
|
-
/** Paths which should be routed to the SSR function */
|
|
20
|
-
include?: {
|
|
21
|
-
/** Generally this is in pathname format, but does support wildcards, e.g. `/users`, `/products/*` */
|
|
22
|
-
pattern: string;
|
|
23
|
-
}[];
|
|
24
|
-
/** Paths which should be routed as static assets */
|
|
25
|
-
exclude?: {
|
|
26
|
-
/** Generally this is in pathname format, but does support wildcards, e.g. `/static`, `/assets/*`, `/images/avatar.jpg` */
|
|
27
|
-
pattern: string;
|
|
28
|
-
}[];
|
|
29
|
-
};
|
|
30
|
-
};
|
|
7
|
+
imageService?: ImageServiceConfig;
|
|
31
8
|
/**
|
|
32
9
|
* By default, Astro will be configured to use Cloudflare KV to store session data. The KV namespace
|
|
33
10
|
* will be automatically provisioned when you deploy.
|
|
@@ -48,5 +25,6 @@ export type Options = {
|
|
|
48
25
|
* See https://developers.cloudflare.com/images/transform-images/bindings/ for more details.
|
|
49
26
|
*/
|
|
50
27
|
imagesBindingName?: string;
|
|
51
|
-
|
|
52
|
-
|
|
28
|
+
experimental?: Pick<NonNullable<PluginConfig['experimental']>, 'headersAndRedirectsDevModeSupport'>;
|
|
29
|
+
}
|
|
30
|
+
export default function createIntegration({ imageService, sessionKVBindingName, imagesBindingName, ...cloudflareOptions }?: Options): AstroIntegration;
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { createReadStream, existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import { appendFile, stat } from "node:fs/promises";
|
|
2
|
+
import { appendFile, rm, stat } from "node:fs/promises";
|
|
3
3
|
import { createInterface } from "node:readline/promises";
|
|
4
4
|
import { removeLeadingForwardSlash } from "@astrojs/internal-helpers/path";
|
|
5
5
|
import { createRedirectsFromAstroRoutes, printAsRedirects } from "@astrojs/underscore-redirects";
|
|
6
6
|
import { cloudflare as cfVitePlugin } from "@cloudflare/vite-plugin";
|
|
7
7
|
import { astroFrontmatterScanPlugin } from "./esbuild-plugin-astro-frontmatter.js";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
8
|
+
import { getParts } from "./utils/generate-routes-json.js";
|
|
9
|
+
import {
|
|
10
|
+
normalizeImageServiceConfig,
|
|
11
|
+
setImageConfig
|
|
12
|
+
} from "./utils/image-config.js";
|
|
10
13
|
import { createConfigPlugin } from "./vite-plugin-config.js";
|
|
11
14
|
import {
|
|
12
15
|
cloudflareConfigCustomizer,
|
|
@@ -16,21 +19,36 @@ import {
|
|
|
16
19
|
import { parseEnv } from "node:util";
|
|
17
20
|
import { sessionDrivers } from "astro/config";
|
|
18
21
|
import { createCloudflarePrerenderer } from "./prerenderer.js";
|
|
19
|
-
|
|
22
|
+
import { createRequire } from "node:module";
|
|
23
|
+
function createIntegration({
|
|
24
|
+
imageService,
|
|
25
|
+
sessionKVBindingName = DEFAULT_SESSION_KV_BINDING_NAME,
|
|
26
|
+
imagesBindingName = DEFAULT_IMAGES_BINDING_NAME,
|
|
27
|
+
...cloudflareOptions
|
|
28
|
+
} = {}) {
|
|
20
29
|
let _config;
|
|
21
|
-
let finalBuildOutput;
|
|
22
30
|
let _routes;
|
|
23
|
-
|
|
24
|
-
|
|
31
|
+
let _isFullyStatic = false;
|
|
32
|
+
let cfPluginConfig;
|
|
33
|
+
const { buildService, runtimeService } = normalizeImageServiceConfig(imageService);
|
|
34
|
+
const needsImagesBinding = runtimeService === "cloudflare-binding";
|
|
25
35
|
return {
|
|
26
36
|
name: "@astrojs/cloudflare",
|
|
27
37
|
hooks: {
|
|
28
38
|
"astro:config:setup": ({ command, config, updateConfig, logger, addWatchFile }) => {
|
|
39
|
+
if (!!process.versions.webcontainer) {
|
|
40
|
+
throw new Error("`workerd` does not run on Stackblitz.");
|
|
41
|
+
}
|
|
29
42
|
let session = config.session;
|
|
30
|
-
|
|
43
|
+
const isCompile = buildService === "compile";
|
|
44
|
+
if (needsImagesBinding) {
|
|
31
45
|
logger.info(
|
|
32
46
|
`Enabling image processing with Cloudflare Images for production with the "${imagesBindingName}" Images binding.`
|
|
33
47
|
);
|
|
48
|
+
} else if (isCompile) {
|
|
49
|
+
logger.info(
|
|
50
|
+
`Enabling compile-time image optimization. Images will be pre-optimized at build time.`
|
|
51
|
+
);
|
|
34
52
|
}
|
|
35
53
|
if (!session?.driver) {
|
|
36
54
|
logger.info(
|
|
@@ -44,34 +62,37 @@ function createIntegration(args) {
|
|
|
44
62
|
ttl: session?.ttl
|
|
45
63
|
};
|
|
46
64
|
}
|
|
47
|
-
const
|
|
48
|
-
|
|
65
|
+
const needsImagesBindingForDev = isCompile && command === "dev";
|
|
66
|
+
cfPluginConfig = {
|
|
49
67
|
config: cloudflareConfigCustomizer({
|
|
50
|
-
sessionKVBindingName
|
|
51
|
-
imagesBindingName:
|
|
68
|
+
sessionKVBindingName,
|
|
69
|
+
imagesBindingName: needsImagesBinding || needsImagesBindingForDev ? imagesBindingName : false
|
|
52
70
|
}),
|
|
53
71
|
experimental: {
|
|
54
72
|
prerenderWorker: {
|
|
55
73
|
config(_, { entryWorkerConfig }) {
|
|
56
74
|
return {
|
|
57
75
|
...entryWorkerConfig,
|
|
58
|
-
|
|
59
|
-
|
|
76
|
+
name: "prerender",
|
|
77
|
+
...needsImagesBinding && !entryWorkerConfig.images && {
|
|
78
|
+
images: { binding: imagesBindingName }
|
|
79
|
+
}
|
|
60
80
|
};
|
|
61
81
|
}
|
|
62
82
|
}
|
|
63
83
|
}
|
|
64
84
|
};
|
|
85
|
+
if (command === "preview") {
|
|
86
|
+
globalThis.astroCloudflareOptions = cfPluginConfig;
|
|
87
|
+
}
|
|
65
88
|
updateConfig({
|
|
66
89
|
build: {
|
|
67
|
-
client: new URL(`./client/`, config.outDir),
|
|
68
|
-
server: new URL("./_worker.js/", config.outDir),
|
|
69
90
|
redirects: false
|
|
70
91
|
},
|
|
71
92
|
session,
|
|
72
93
|
vite: {
|
|
73
94
|
plugins: [
|
|
74
|
-
cfVitePlugin(cfPluginConfig),
|
|
95
|
+
cfVitePlugin({ ...cfPluginConfig, viteEnvironment: { name: "ssr" } }),
|
|
75
96
|
{
|
|
76
97
|
name: "@astrojs/cloudflare:cf-imports",
|
|
77
98
|
enforce: "pre",
|
|
@@ -110,6 +131,7 @@ function createIntegration(args) {
|
|
|
110
131
|
"astro > neotraverse/modern",
|
|
111
132
|
"astro > piccolore",
|
|
112
133
|
"astro/app",
|
|
134
|
+
"astro/assets",
|
|
113
135
|
"astro/compiler-runtime"
|
|
114
136
|
],
|
|
115
137
|
exclude: [
|
|
@@ -150,22 +172,32 @@ function createIntegration(args) {
|
|
|
150
172
|
}
|
|
151
173
|
},
|
|
152
174
|
createConfigPlugin({
|
|
153
|
-
sessionKVBindingName
|
|
175
|
+
sessionKVBindingName,
|
|
176
|
+
compileImageConfig: isCompile && command !== "dev" ? {
|
|
177
|
+
base: config.base,
|
|
178
|
+
assetsPrefix: typeof config.build.assetsPrefix === "string" ? config.build.assetsPrefix : void 0,
|
|
179
|
+
imageServiceEntrypoint: "@astrojs/cloudflare/image-service-workerd",
|
|
180
|
+
buildAssets: config.build.assets ?? "_astro"
|
|
181
|
+
} : null
|
|
154
182
|
})
|
|
155
183
|
]
|
|
156
184
|
},
|
|
157
|
-
image: setImageConfig(
|
|
185
|
+
image: setImageConfig(imageService, config.image, command, logger)
|
|
158
186
|
});
|
|
187
|
+
if (cloudflareOptions.configPath) {
|
|
188
|
+
addWatchFile(createRequire(import.meta.url).resolve(cloudflareOptions.configPath));
|
|
189
|
+
}
|
|
159
190
|
addWatchFile(new URL("./wrangler.toml", config.root));
|
|
160
191
|
addWatchFile(new URL("./wrangler.json", config.root));
|
|
161
192
|
addWatchFile(new URL("./wrangler.jsonc", config.root));
|
|
162
193
|
},
|
|
163
194
|
"astro:routes:resolved": ({ routes }) => {
|
|
164
195
|
_routes = routes;
|
|
196
|
+
const nonInternalRoutes = routes.filter((route) => route.origin !== "internal");
|
|
197
|
+
_isFullyStatic = nonInternalRoutes.length > 0 && nonInternalRoutes.every((route) => route.isPrerendered);
|
|
165
198
|
},
|
|
166
|
-
"astro:config:done": ({ setAdapter, config,
|
|
199
|
+
"astro:config:done": ({ setAdapter, config, injectTypes, logger }) => {
|
|
167
200
|
_config = config;
|
|
168
|
-
finalBuildOutput = buildOutput;
|
|
169
201
|
injectTypes({
|
|
170
202
|
filename: "cloudflare.d.ts",
|
|
171
203
|
content: '/// <reference types="@astrojs/cloudflare/types.d.ts" />'
|
|
@@ -176,18 +208,18 @@ function createIntegration(args) {
|
|
|
176
208
|
edgeMiddleware: false,
|
|
177
209
|
buildOutput: "server"
|
|
178
210
|
},
|
|
179
|
-
|
|
211
|
+
entrypointResolution: "auto",
|
|
180
212
|
previewEntrypoint: "@astrojs/cloudflare/entrypoints/preview",
|
|
181
213
|
supportedAstroFeatures: {
|
|
182
214
|
serverOutput: "stable",
|
|
183
215
|
hybridOutput: "stable",
|
|
184
|
-
staticOutput: "
|
|
216
|
+
staticOutput: "stable",
|
|
185
217
|
i18nDomains: "experimental",
|
|
186
218
|
sharpImageService: {
|
|
187
219
|
support: "limited",
|
|
188
220
|
message: "When using a custom image service, ensure it is compatible with the Cloudflare Workers runtime.",
|
|
189
221
|
// Only 'custom' could potentially use sharp at runtime.
|
|
190
|
-
suppress:
|
|
222
|
+
suppress: buildService === "custom" ? "default" : "all"
|
|
191
223
|
},
|
|
192
224
|
envGetSecret: "stable"
|
|
193
225
|
}
|
|
@@ -212,7 +244,9 @@ function createIntegration(args) {
|
|
|
212
244
|
serverDir: _config.build.server,
|
|
213
245
|
clientDir: _config.build.client,
|
|
214
246
|
base: _config.base,
|
|
215
|
-
trailingSlash: _config.trailingSlash
|
|
247
|
+
trailingSlash: _config.trailingSlash,
|
|
248
|
+
cfPluginConfig,
|
|
249
|
+
hasCompileImageService: buildService === "compile"
|
|
216
250
|
})
|
|
217
251
|
);
|
|
218
252
|
},
|
|
@@ -229,14 +263,12 @@ function createIntegration(args) {
|
|
|
229
263
|
vite.build.rollupOptions.output.banner ||= "globalThis.process ??= {}; globalThis.process.env ??= {};";
|
|
230
264
|
vite.define = {
|
|
231
265
|
"process.env": "process.env",
|
|
232
|
-
"globalThis.__ASTRO_IMAGES_BINDING_NAME": JSON.stringify(
|
|
233
|
-
args?.imagesBindingName ?? "IMAGES"
|
|
234
|
-
),
|
|
266
|
+
"globalThis.__ASTRO_IMAGES_BINDING_NAME": JSON.stringify(imagesBindingName),
|
|
235
267
|
...vite.define
|
|
236
268
|
};
|
|
237
269
|
}
|
|
238
270
|
},
|
|
239
|
-
"astro:build:done": async ({
|
|
271
|
+
"astro:build:done": async ({ dir, logger, assets }) => {
|
|
240
272
|
let redirectsExists = false;
|
|
241
273
|
try {
|
|
242
274
|
const redirectsStat = await stat(new URL("./_redirects", _config.build.client));
|
|
@@ -263,26 +295,6 @@ function createIntegration(args) {
|
|
|
263
295
|
}
|
|
264
296
|
}
|
|
265
297
|
}
|
|
266
|
-
let routesExists = false;
|
|
267
|
-
try {
|
|
268
|
-
const routesStat = await stat(new URL("./_routes.json", _config.build.client));
|
|
269
|
-
if (routesStat.isFile()) {
|
|
270
|
-
routesExists = true;
|
|
271
|
-
}
|
|
272
|
-
} catch (_error) {
|
|
273
|
-
routesExists = false;
|
|
274
|
-
}
|
|
275
|
-
if (!routesExists) {
|
|
276
|
-
await createRoutesFile(
|
|
277
|
-
_config,
|
|
278
|
-
logger,
|
|
279
|
-
_routes,
|
|
280
|
-
pages,
|
|
281
|
-
redirects,
|
|
282
|
-
args?.routes?.extend?.include,
|
|
283
|
-
args?.routes?.extend?.exclude
|
|
284
|
-
);
|
|
285
|
-
}
|
|
286
298
|
const trueRedirects = createRedirectsFromAstroRoutes({
|
|
287
299
|
config: _config,
|
|
288
300
|
routeToDynamicTargetMap: new Map(
|
|
@@ -291,7 +303,7 @@ function createIntegration(args) {
|
|
|
291
303
|
)
|
|
292
304
|
),
|
|
293
305
|
dir,
|
|
294
|
-
buildOutput:
|
|
306
|
+
buildOutput: _isFullyStatic ? "static" : "server",
|
|
295
307
|
assets
|
|
296
308
|
});
|
|
297
309
|
if (!trueRedirects.empty()) {
|
|
@@ -304,6 +316,9 @@ function createIntegration(args) {
|
|
|
304
316
|
logger.error("Failed to write _redirects file");
|
|
305
317
|
}
|
|
306
318
|
}
|
|
319
|
+
if (_isFullyStatic) {
|
|
320
|
+
await rm(_config.build.server, { recursive: true, force: true });
|
|
321
|
+
}
|
|
307
322
|
delete process.env.CLOUDFLARE_VITE_BUILD;
|
|
308
323
|
}
|
|
309
324
|
}
|
|
@@ -19,4 +19,14 @@ export interface PrerenderRequest {
|
|
|
19
19
|
url: string;
|
|
20
20
|
routeData: SerializedRouteData;
|
|
21
21
|
}
|
|
22
|
+
export interface SerializedStaticImageEntry {
|
|
23
|
+
originalPath: string;
|
|
24
|
+
originalSrcPath: string | undefined;
|
|
25
|
+
transforms: Array<{
|
|
26
|
+
hash: string;
|
|
27
|
+
finalPath: string;
|
|
28
|
+
transform: Record<string, any>;
|
|
29
|
+
}>;
|
|
30
|
+
}
|
|
31
|
+
export type StaticImagesResponse = SerializedStaticImageEntry[];
|
|
22
32
|
export {};
|
package/dist/prerenderer.d.ts
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import type { AstroConfig, AstroPrerenderer } from 'astro';
|
|
2
|
+
import { type PluginConfig } from '@cloudflare/vite-plugin';
|
|
2
3
|
interface CloudflarePrerendererOptions {
|
|
3
4
|
root: AstroConfig['root'];
|
|
4
5
|
serverDir: AstroConfig['build']['server'];
|
|
5
6
|
clientDir: AstroConfig['build']['client'];
|
|
6
7
|
base: AstroConfig['base'];
|
|
7
8
|
trailingSlash: AstroConfig['trailingSlash'];
|
|
9
|
+
cfPluginConfig: PluginConfig;
|
|
10
|
+
hasCompileImageService: boolean;
|
|
8
11
|
}
|
|
9
12
|
/**
|
|
10
13
|
* Creates a prerenderer that uses Cloudflare's workerd runtime via a preview server.
|
|
11
14
|
* This allows prerendering to happen in the same runtime that will serve the pages.
|
|
12
15
|
*/
|
|
13
|
-
export declare function createCloudflarePrerenderer({ root, serverDir, clientDir, base, trailingSlash, }: CloudflarePrerendererOptions): AstroPrerenderer;
|
|
16
|
+
export declare function createCloudflarePrerenderer({ root, serverDir, clientDir, base, trailingSlash, cfPluginConfig, hasCompileImageService, }: CloudflarePrerendererOptions): AstroPrerenderer;
|
|
14
17
|
export {};
|
package/dist/prerenderer.js
CHANGED
|
@@ -2,15 +2,20 @@ import { preview } from "vite";
|
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
3
|
import { mkdir } from "node:fs/promises";
|
|
4
4
|
import { cloudflare as cfVitePlugin } from "@cloudflare/vite-plugin";
|
|
5
|
-
import { cloudflareConfigCustomizer } from "./wrangler.js";
|
|
6
5
|
import { serializeRouteData, deserializeRouteData } from "astro/app/manifest";
|
|
7
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
STATIC_PATHS_ENDPOINT,
|
|
8
|
+
PRERENDER_ENDPOINT,
|
|
9
|
+
STATIC_IMAGES_ENDPOINT
|
|
10
|
+
} from "./utils/prerender-constants.js";
|
|
8
11
|
function createCloudflarePrerenderer({
|
|
9
12
|
root,
|
|
10
13
|
serverDir,
|
|
11
14
|
clientDir,
|
|
12
15
|
base,
|
|
13
|
-
trailingSlash
|
|
16
|
+
trailingSlash,
|
|
17
|
+
cfPluginConfig,
|
|
18
|
+
hasCompileImageService
|
|
14
19
|
}) {
|
|
15
20
|
let previewServer;
|
|
16
21
|
let serverUrl;
|
|
@@ -18,10 +23,6 @@ function createCloudflarePrerenderer({
|
|
|
18
23
|
name: "@astrojs/cloudflare:prerenderer",
|
|
19
24
|
async setup() {
|
|
20
25
|
await mkdir(clientDir, { recursive: true });
|
|
21
|
-
const cfPluginConfig = {
|
|
22
|
-
viteEnvironment: { name: "prerender" },
|
|
23
|
-
config: cloudflareConfigCustomizer()
|
|
24
|
-
};
|
|
25
26
|
previewServer = await preview({
|
|
26
27
|
configFile: false,
|
|
27
28
|
base,
|
|
@@ -36,7 +37,7 @@ function createCloudflarePrerenderer({
|
|
|
36
37
|
// Let the OS pick a free port
|
|
37
38
|
open: false
|
|
38
39
|
},
|
|
39
|
-
plugins: [cfVitePlugin(cfPluginConfig)]
|
|
40
|
+
plugins: [cfVitePlugin({ ...cfPluginConfig, viteEnvironment: { name: "prerender" } })]
|
|
40
41
|
});
|
|
41
42
|
const address = previewServer.httpServer.address();
|
|
42
43
|
if (address && typeof address === "object") {
|
|
@@ -75,6 +76,33 @@ function createCloudflarePrerenderer({
|
|
|
75
76
|
});
|
|
76
77
|
return response;
|
|
77
78
|
},
|
|
79
|
+
collectStaticImages: hasCompileImageService ? async () => {
|
|
80
|
+
const response = await fetch(`${serverUrl}${STATIC_IMAGES_ENDPOINT}`, {
|
|
81
|
+
method: "POST",
|
|
82
|
+
headers: { "Content-Type": "application/json" }
|
|
83
|
+
});
|
|
84
|
+
if (!response.ok) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
`Failed to get static images from the Cloudflare prerender server (${response.status}: ${response.statusText}).`
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
const entries = await response.json();
|
|
90
|
+
const { default: sharpService } = await import("astro/assets/services/sharp");
|
|
91
|
+
globalThis.astroAsset ??= {};
|
|
92
|
+
globalThis.astroAsset.imageService = sharpService;
|
|
93
|
+
const staticImages = /* @__PURE__ */ new Map();
|
|
94
|
+
for (const entry of entries) {
|
|
95
|
+
const transforms = /* @__PURE__ */ new Map();
|
|
96
|
+
for (const t of entry.transforms) {
|
|
97
|
+
transforms.set(t.hash, { finalPath: t.finalPath, transform: t.transform });
|
|
98
|
+
}
|
|
99
|
+
staticImages.set(entry.originalPath, {
|
|
100
|
+
originalSrcPath: entry.originalSrcPath,
|
|
101
|
+
transforms
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
return staticImages;
|
|
105
|
+
} : void 0,
|
|
78
106
|
async teardown() {
|
|
79
107
|
if (previewServer) {
|
|
80
108
|
await previewServer.close();
|
|
@@ -1,9 +1,2 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { RoutePart } from 'astro';
|
|
2
2
|
export declare function getParts(part: string): RoutePart[];
|
|
3
|
-
export declare function createRoutesFile(_config: AstroConfig, logger: AstroIntegrationLogger, routes: IntegrationResolvedRoute[], pages: {
|
|
4
|
-
pathname: string;
|
|
5
|
-
}[], redirects: IntegrationResolvedRoute['segments'][], includeExtends: {
|
|
6
|
-
pattern: string;
|
|
7
|
-
}[] | undefined, excludeExtends: {
|
|
8
|
-
pattern: string;
|
|
9
|
-
}[] | undefined): Promise<void>;
|
|
@@ -1,13 +1,3 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
|
-
import { writeFile } from "node:fs/promises";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
5
|
-
import {
|
|
6
|
-
prependForwardSlash,
|
|
7
|
-
removeLeadingForwardSlash,
|
|
8
|
-
removeTrailingForwardSlash
|
|
9
|
-
} from "@astrojs/internal-helpers/path";
|
|
10
|
-
import { glob } from "tinyglobby";
|
|
11
1
|
const ROUTE_DYNAMIC_SPLIT = /\[(.+?\(.+?\)|.+?)\]/;
|
|
12
2
|
const ROUTE_SPREAD = /^\.{3}.+$/;
|
|
13
3
|
function getParts(part) {
|
|
@@ -27,199 +17,6 @@ function getParts(part) {
|
|
|
27
17
|
});
|
|
28
18
|
return result;
|
|
29
19
|
}
|
|
30
|
-
async function writeRoutesFileToOutDir(_config, logger, include, exclude) {
|
|
31
|
-
try {
|
|
32
|
-
await writeFile(
|
|
33
|
-
new URL("./_routes.json", _config.outDir),
|
|
34
|
-
JSON.stringify(
|
|
35
|
-
{
|
|
36
|
-
version: 1,
|
|
37
|
-
include,
|
|
38
|
-
exclude
|
|
39
|
-
},
|
|
40
|
-
null,
|
|
41
|
-
2
|
|
42
|
-
),
|
|
43
|
-
"utf-8"
|
|
44
|
-
);
|
|
45
|
-
} catch (_error) {
|
|
46
|
-
logger.error("There was an error writing the '_routes.json' file to the output directory.");
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
function segmentsToCfSyntax(segments, _config) {
|
|
50
|
-
const pathSegments = [];
|
|
51
|
-
if (removeLeadingForwardSlash(removeTrailingForwardSlash(_config.base)).length > 0) {
|
|
52
|
-
pathSegments.push(removeLeadingForwardSlash(removeTrailingForwardSlash(_config.base)));
|
|
53
|
-
}
|
|
54
|
-
for (const segment of segments.flat()) {
|
|
55
|
-
if (segment.dynamic) pathSegments.push("*");
|
|
56
|
-
else pathSegments.push(segment.content);
|
|
57
|
-
}
|
|
58
|
-
return pathSegments;
|
|
59
|
-
}
|
|
60
|
-
class TrieNode {
|
|
61
|
-
children = /* @__PURE__ */ new Map();
|
|
62
|
-
isEndOfPath = false;
|
|
63
|
-
hasWildcardChild = false;
|
|
64
|
-
}
|
|
65
|
-
class PathTrie {
|
|
66
|
-
root;
|
|
67
|
-
returnHasWildcard = false;
|
|
68
|
-
constructor() {
|
|
69
|
-
this.root = new TrieNode();
|
|
70
|
-
}
|
|
71
|
-
insert(thisPath) {
|
|
72
|
-
let node = this.root;
|
|
73
|
-
for (const segment of thisPath) {
|
|
74
|
-
if (segment === "*") {
|
|
75
|
-
node.hasWildcardChild = true;
|
|
76
|
-
break;
|
|
77
|
-
}
|
|
78
|
-
if (!node.children.has(segment)) {
|
|
79
|
-
node.children.set(segment, new TrieNode());
|
|
80
|
-
}
|
|
81
|
-
node = node.children.get(segment);
|
|
82
|
-
}
|
|
83
|
-
node.isEndOfPath = true;
|
|
84
|
-
}
|
|
85
|
-
/**
|
|
86
|
-
* Depth-first search (dfs), traverses the "graph" segment by segment until the end or wildcard (*).
|
|
87
|
-
* It makes sure that all necessary paths are returned, but not paths with an existing wildcard prefix.
|
|
88
|
-
* e.g. if we have a path like /foo/* and /foo/bar, we only want to return /foo/*
|
|
89
|
-
*/
|
|
90
|
-
dfs(node, thisPath, allPaths) {
|
|
91
|
-
if (node.hasWildcardChild) {
|
|
92
|
-
this.returnHasWildcard = true;
|
|
93
|
-
allPaths.push([...thisPath, "*"]);
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
if (node.isEndOfPath) {
|
|
97
|
-
allPaths.push([...thisPath]);
|
|
98
|
-
}
|
|
99
|
-
for (const [segment, childNode] of node.children) {
|
|
100
|
-
this.dfs(childNode, [...thisPath, segment], allPaths);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
/**
|
|
104
|
-
* The reduce function is used to remove unnecessary paths from the trie.
|
|
105
|
-
* It receives a trie node to compare with the current node.
|
|
106
|
-
*/
|
|
107
|
-
reduce(compNode, node) {
|
|
108
|
-
if (node.hasWildcardChild || compNode.hasWildcardChild) return;
|
|
109
|
-
for (const [segment, childNode] of node.children) {
|
|
110
|
-
if (childNode.children.size === 0) continue;
|
|
111
|
-
const compChildNode = compNode.children.get(segment);
|
|
112
|
-
if (compChildNode === void 0) {
|
|
113
|
-
childNode.hasWildcardChild = true;
|
|
114
|
-
continue;
|
|
115
|
-
}
|
|
116
|
-
this.reduce(compChildNode, childNode);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
reduceAllPaths(compTrie) {
|
|
120
|
-
this.reduce(compTrie.root, this.root);
|
|
121
|
-
return this;
|
|
122
|
-
}
|
|
123
|
-
getAllPaths() {
|
|
124
|
-
const allPaths = [];
|
|
125
|
-
this.dfs(this.root, [], allPaths);
|
|
126
|
-
return [allPaths, this.returnHasWildcard];
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
async function createRoutesFile(_config, logger, routes, pages, redirects, includeExtends, excludeExtends) {
|
|
130
|
-
const includePaths = [];
|
|
131
|
-
const excludePaths = [];
|
|
132
|
-
const assetsPath = segmentsToCfSyntax(
|
|
133
|
-
[
|
|
134
|
-
[{ content: _config.build.assets, dynamic: false, spread: false }],
|
|
135
|
-
[{ content: "", dynamic: true, spread: false }]
|
|
136
|
-
],
|
|
137
|
-
_config
|
|
138
|
-
);
|
|
139
|
-
excludePaths.push(assetsPath);
|
|
140
|
-
for (const redirect of redirects) {
|
|
141
|
-
excludePaths.push(segmentsToCfSyntax(redirect, _config));
|
|
142
|
-
}
|
|
143
|
-
if (existsSync(fileURLToPath(_config.publicDir))) {
|
|
144
|
-
const staticFiles = await glob(`**/*`, {
|
|
145
|
-
cwd: fileURLToPath(_config.publicDir),
|
|
146
|
-
dot: true
|
|
147
|
-
});
|
|
148
|
-
for (const staticFile of staticFiles) {
|
|
149
|
-
if (["_headers", "_redirects", "_routes.json"].includes(staticFile)) continue;
|
|
150
|
-
const staticPath = staticFile;
|
|
151
|
-
const segments = removeLeadingForwardSlash(staticPath).split(path.sep).filter(Boolean).map((s) => {
|
|
152
|
-
return getParts(s);
|
|
153
|
-
});
|
|
154
|
-
excludePaths.push(segmentsToCfSyntax(segments, _config));
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
let hasPrerendered404 = false;
|
|
158
|
-
for (const route of routes) {
|
|
159
|
-
const convertedPath = segmentsToCfSyntax(route.segments, _config);
|
|
160
|
-
if (route.pathname === "/404" && route.isPrerendered === true) hasPrerendered404 = true;
|
|
161
|
-
switch (route.type) {
|
|
162
|
-
case "page":
|
|
163
|
-
if (route.isPrerendered === false) includePaths.push(convertedPath);
|
|
164
|
-
break;
|
|
165
|
-
case "endpoint":
|
|
166
|
-
if (route.isPrerendered === false) includePaths.push(convertedPath);
|
|
167
|
-
else excludePaths.push(convertedPath);
|
|
168
|
-
break;
|
|
169
|
-
case "redirect":
|
|
170
|
-
excludePaths.push(convertedPath);
|
|
171
|
-
break;
|
|
172
|
-
default:
|
|
173
|
-
includePaths.push(convertedPath);
|
|
174
|
-
break;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
for (const page of pages) {
|
|
178
|
-
if (page.pathname === "404") hasPrerendered404 = true;
|
|
179
|
-
const pageSegments = removeLeadingForwardSlash(page.pathname).split(path.posix.sep).filter(Boolean).map((s) => {
|
|
180
|
-
return getParts(s);
|
|
181
|
-
});
|
|
182
|
-
excludePaths.push(segmentsToCfSyntax(pageSegments, _config));
|
|
183
|
-
}
|
|
184
|
-
const includeTrie = new PathTrie();
|
|
185
|
-
for (const includePath of includePaths) {
|
|
186
|
-
includeTrie.insert(includePath);
|
|
187
|
-
}
|
|
188
|
-
const excludeTrie = new PathTrie();
|
|
189
|
-
for (const excludePath of excludePaths) {
|
|
190
|
-
if (excludePath[0] === "*") continue;
|
|
191
|
-
excludeTrie.insert(excludePath);
|
|
192
|
-
}
|
|
193
|
-
const [deduplicatedIncludePaths, includedPathsHaveWildcard] = includeTrie.reduceAllPaths(excludeTrie).getAllPaths();
|
|
194
|
-
const [deduplicatedExcludePaths, _excludedPathsHaveWildcard] = excludeTrie.reduceAllPaths(includeTrie).getAllPaths();
|
|
195
|
-
const CLOUDFLARE_COMBINED_LIMIT = 100;
|
|
196
|
-
const AUTOMATIC_INCLUDE_RULES_COUNT = deduplicatedIncludePaths.length;
|
|
197
|
-
const EXTENDED_INCLUDE_RULES_COUNT = includeExtends?.length ?? 0;
|
|
198
|
-
const INCLUDE_RULES_COUNT = AUTOMATIC_INCLUDE_RULES_COUNT + EXTENDED_INCLUDE_RULES_COUNT;
|
|
199
|
-
const AUTOMATIC_EXCLUDE_RULES_COUNT = deduplicatedExcludePaths.length;
|
|
200
|
-
const EXTENDED_EXCLUDE_RULES_COUNT = excludeExtends?.length ?? 0;
|
|
201
|
-
const EXCLUDE_RULES_COUNT = AUTOMATIC_EXCLUDE_RULES_COUNT + EXTENDED_EXCLUDE_RULES_COUNT;
|
|
202
|
-
const OPTION2_TOTAL_COUNT = INCLUDE_RULES_COUNT + (includedPathsHaveWildcard ? EXCLUDE_RULES_COUNT : 0);
|
|
203
|
-
if (!hasPrerendered404 || OPTION2_TOTAL_COUNT > CLOUDFLARE_COMBINED_LIMIT) {
|
|
204
|
-
await writeRoutesFileToOutDir(
|
|
205
|
-
_config,
|
|
206
|
-
logger,
|
|
207
|
-
["/*"].concat(includeExtends?.map((entry) => entry.pattern) ?? []),
|
|
208
|
-
deduplicatedExcludePaths.map((thisPath) => `${prependForwardSlash(thisPath.join("/"))}`).slice(
|
|
209
|
-
0,
|
|
210
|
-
CLOUDFLARE_COMBINED_LIMIT - EXTENDED_INCLUDE_RULES_COUNT - EXTENDED_EXCLUDE_RULES_COUNT - 1
|
|
211
|
-
).concat(excludeExtends?.map((entry) => entry.pattern) ?? [])
|
|
212
|
-
);
|
|
213
|
-
} else {
|
|
214
|
-
await writeRoutesFileToOutDir(
|
|
215
|
-
_config,
|
|
216
|
-
logger,
|
|
217
|
-
deduplicatedIncludePaths.map((thisPath) => `${prependForwardSlash(thisPath.join("/"))}`).concat(includeExtends?.map((entry) => entry.pattern) ?? []),
|
|
218
|
-
includedPathsHaveWildcard ? deduplicatedExcludePaths.map((thisPath) => `${prependForwardSlash(thisPath.join("/"))}`).concat(excludeExtends?.map((entry) => entry.pattern) ?? []) : []
|
|
219
|
-
);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
20
|
export {
|
|
223
|
-
createRoutesFile,
|
|
224
21
|
getParts
|
|
225
22
|
};
|
package/dist/utils/handler.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { env as globalEnv } from "cloudflare:workers";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
sessionKVBindingName,
|
|
4
|
+
compileImageConfig,
|
|
5
|
+
isPrerender
|
|
6
|
+
} from "virtual:astro-cloudflare:config";
|
|
3
7
|
import { createApp } from "astro/app/entrypoint";
|
|
4
8
|
import { setGetEnv } from "astro/env/setup";
|
|
5
9
|
import { createGetEnv } from "../utils/env.js";
|
|
@@ -7,18 +11,27 @@ import {
|
|
|
7
11
|
isStaticPathsRequest,
|
|
8
12
|
isPrerenderRequest,
|
|
9
13
|
handleStaticPathsRequest,
|
|
10
|
-
handlePrerenderRequest
|
|
14
|
+
handlePrerenderRequest,
|
|
15
|
+
isStaticImagesRequest,
|
|
16
|
+
handleStaticImagesRequest
|
|
11
17
|
} from "./prerender.js";
|
|
12
18
|
setGetEnv(createGetEnv(globalEnv));
|
|
13
19
|
const app = createApp();
|
|
14
20
|
async function handle(request, env, context) {
|
|
15
21
|
if (isPrerender) {
|
|
22
|
+
if (compileImageConfig) {
|
|
23
|
+
const { installAddStaticImage } = await import("./static-image-collection.js");
|
|
24
|
+
installAddStaticImage(compileImageConfig);
|
|
25
|
+
}
|
|
16
26
|
if (isStaticPathsRequest(request)) {
|
|
17
27
|
return handleStaticPathsRequest(app);
|
|
18
28
|
}
|
|
19
29
|
if (isPrerenderRequest(request)) {
|
|
20
30
|
return handlePrerenderRequest(app, request);
|
|
21
31
|
}
|
|
32
|
+
if (isStaticImagesRequest(request)) {
|
|
33
|
+
return handleStaticImagesRequest();
|
|
34
|
+
}
|
|
22
35
|
}
|
|
23
36
|
const { pathname: requestPathname } = new URL(request.url);
|
|
24
37
|
if (env[sessionKVBindingName]) {
|
|
@@ -13,17 +13,25 @@ async function transform(rawUrl, images, assets) {
|
|
|
13
13
|
return new Response(null, { status: 404 });
|
|
14
14
|
}
|
|
15
15
|
const input = images.input(content.body);
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
const supportedFormats = {
|
|
17
|
+
jpeg: "image/jpeg",
|
|
18
|
+
jpg: "image/jpeg",
|
|
19
|
+
png: "image/png",
|
|
20
|
+
gif: "image/gif",
|
|
21
|
+
webp: "image/webp",
|
|
22
|
+
avif: "image/avif"
|
|
23
|
+
};
|
|
24
|
+
const outputFormat = supportedFormats[url.searchParams.get("f") ?? ""];
|
|
25
|
+
if (!outputFormat) {
|
|
26
|
+
return new Response(`Unsupported format: ${url.searchParams.get("f")}`, { status: 400 });
|
|
19
27
|
}
|
|
20
28
|
return (await input.transform({
|
|
21
|
-
width: url.searchParams.has("w") ? parseInt(url.searchParams.get("w")) : void 0,
|
|
22
|
-
height: url.searchParams.has("h") ? parseInt(url.searchParams.get("h")) : void 0,
|
|
29
|
+
width: url.searchParams.has("w") ? Number.parseInt(url.searchParams.get("w")) : void 0,
|
|
30
|
+
height: url.searchParams.has("h") ? Number.parseInt(url.searchParams.get("h")) : void 0,
|
|
23
31
|
// `quality` is documented, but doesn't appear to work in manual testing...
|
|
24
32
|
// quality: url.searchParams.get('q'),
|
|
25
33
|
fit: url.searchParams.get("fit")
|
|
26
|
-
}).output({ format:
|
|
34
|
+
}).output({ format: outputFormat })).response();
|
|
27
35
|
}
|
|
28
36
|
export {
|
|
29
37
|
transform
|
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
import type { AstroConfig, AstroIntegrationLogger, HookParameters } from 'astro';
|
|
2
|
-
export type
|
|
3
|
-
export
|
|
2
|
+
export type ImageServiceMode = 'passthrough' | 'cloudflare' | 'cloudflare-binding' | 'compile' | 'custom';
|
|
3
|
+
export type ImageServiceConfig = ImageServiceMode | {
|
|
4
|
+
build: 'compile';
|
|
5
|
+
runtime?: 'passthrough' | 'cloudflare-binding';
|
|
6
|
+
};
|
|
7
|
+
/** Normalize string | compound config into separate build/runtime modes. */
|
|
8
|
+
export declare function normalizeImageServiceConfig(config: ImageServiceConfig | undefined): {
|
|
9
|
+
buildService: ImageServiceMode;
|
|
10
|
+
runtimeService: ImageServiceMode;
|
|
11
|
+
};
|
|
12
|
+
export declare function setImageConfig(service: ImageServiceConfig | undefined, config: AstroConfig['image'], command: HookParameters<'astro:config:setup'>['command'], logger: AstroIntegrationLogger): {
|
|
4
13
|
service: import("astro").ImageServiceConfig<Record<string, any>>;
|
|
5
14
|
endpoint: {
|
|
15
|
+
entrypoint: string;
|
|
16
|
+
} | {
|
|
6
17
|
route: string;
|
|
7
18
|
entrypoint?: string | undefined;
|
|
8
19
|
};
|
|
@@ -19,9 +30,11 @@ export declare function setImageConfig(service: ImageService, config: AstroConfi
|
|
|
19
30
|
objectPosition?: string | undefined;
|
|
20
31
|
breakpoints?: number[] | undefined;
|
|
21
32
|
} | {
|
|
22
|
-
service:
|
|
33
|
+
service: {
|
|
34
|
+
entrypoint: string;
|
|
35
|
+
};
|
|
23
36
|
endpoint: {
|
|
24
|
-
entrypoint: string
|
|
37
|
+
entrypoint: string;
|
|
25
38
|
};
|
|
26
39
|
domains: string[];
|
|
27
40
|
remotePatterns: {
|
|
@@ -1,16 +1,37 @@
|
|
|
1
|
-
import { passthroughImageService
|
|
1
|
+
import { passthroughImageService } from "astro/config";
|
|
2
|
+
function normalizeImageServiceConfig(config) {
|
|
3
|
+
if (!config || typeof config === "string") {
|
|
4
|
+
const mode = config ?? "cloudflare-binding";
|
|
5
|
+
return {
|
|
6
|
+
buildService: mode,
|
|
7
|
+
runtimeService: mode === "compile" ? "passthrough" : mode
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
return {
|
|
11
|
+
buildService: "compile",
|
|
12
|
+
runtimeService: config.runtime ?? "passthrough"
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
const GENERIC_ENDPOINT = { entrypoint: "astro/assets/endpoint/generic" };
|
|
16
|
+
const WORKERD_IMAGE_SERVICE = { entrypoint: "@astrojs/cloudflare/image-service-workerd" };
|
|
2
17
|
function setImageConfig(service, config, command, logger) {
|
|
3
|
-
|
|
18
|
+
const { buildService, runtimeService } = normalizeImageServiceConfig(service);
|
|
19
|
+
switch (buildService) {
|
|
4
20
|
case "passthrough":
|
|
5
|
-
return {
|
|
21
|
+
return {
|
|
22
|
+
...config,
|
|
23
|
+
service: passthroughImageService(),
|
|
24
|
+
endpoint: command === "dev" ? GENERIC_ENDPOINT : config.endpoint
|
|
25
|
+
};
|
|
6
26
|
case "cloudflare":
|
|
7
27
|
return {
|
|
8
28
|
...config,
|
|
9
|
-
service:
|
|
29
|
+
service: { entrypoint: "@astrojs/cloudflare/image-service" }
|
|
10
30
|
};
|
|
11
31
|
case "cloudflare-binding":
|
|
12
32
|
return {
|
|
13
33
|
...config,
|
|
34
|
+
service: WORKERD_IMAGE_SERVICE,
|
|
14
35
|
endpoint: {
|
|
15
36
|
entrypoint: "@astrojs/cloudflare/image-transform-endpoint"
|
|
16
37
|
}
|
|
@@ -18,10 +39,10 @@ function setImageConfig(service, config, command, logger) {
|
|
|
18
39
|
case "compile":
|
|
19
40
|
return {
|
|
20
41
|
...config,
|
|
21
|
-
service:
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
42
|
+
service: WORKERD_IMAGE_SERVICE,
|
|
43
|
+
// Dev: IMAGES binding (via Cloudflare Vite plugin) for real transforms.
|
|
44
|
+
// Build: endpoint depends on runtime - `cloudflare-binding` uses IMAGES, `passthrough` uses generic.
|
|
45
|
+
endpoint: command === "dev" || runtimeService === "cloudflare-binding" ? { entrypoint: "@astrojs/cloudflare/image-transform-endpoint" } : GENERIC_ENDPOINT
|
|
25
46
|
};
|
|
26
47
|
case "custom":
|
|
27
48
|
return { ...config };
|
|
@@ -36,5 +57,6 @@ function setImageConfig(service, config, command, logger) {
|
|
|
36
57
|
}
|
|
37
58
|
}
|
|
38
59
|
export {
|
|
60
|
+
normalizeImageServiceConfig,
|
|
39
61
|
setImageConfig
|
|
40
62
|
};
|
|
@@ -5,3 +5,5 @@
|
|
|
5
5
|
export declare const STATIC_PATHS_ENDPOINT = "/__astro_static_paths";
|
|
6
6
|
/** Internal endpoint for rendering a specific page during prerendering */
|
|
7
7
|
export declare const PRERENDER_ENDPOINT = "/__astro_prerender";
|
|
8
|
+
/** Internal endpoint for fetching static images collected in workerd during `compile` builds */
|
|
9
|
+
export declare const STATIC_IMAGES_ENDPOINT = "/__astro_static_images";
|
|
@@ -32,3 +32,6 @@ export declare function handleStaticPathsRequest(app: BaseApp): Promise<Response
|
|
|
32
32
|
* Handles a prerender request, rendering the specified page.
|
|
33
33
|
*/
|
|
34
34
|
export declare function handlePrerenderRequest(app: BaseApp, request: Request): Promise<Response>;
|
|
35
|
+
export declare function isStaticImagesRequest(request: Request): boolean;
|
|
36
|
+
/** Serializes the global staticImages map collected in workerd back to the Node-side build. */
|
|
37
|
+
export declare function handleStaticImagesRequest(): Response;
|
package/dist/utils/prerender.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { serializeRouteData, deserializeRouteData } from "astro/app/manifest";
|
|
2
2
|
import { StaticPaths } from "astro:static-paths";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
STATIC_PATHS_ENDPOINT,
|
|
5
|
+
PRERENDER_ENDPOINT,
|
|
6
|
+
STATIC_IMAGES_ENDPOINT
|
|
7
|
+
} from "./prerender-constants.js";
|
|
4
8
|
function isStaticPathsRequest(request) {
|
|
5
9
|
const { pathname } = new URL(request.url);
|
|
6
10
|
return pathname === STATIC_PATHS_ENDPOINT && request.method === "POST";
|
|
@@ -35,9 +39,38 @@ async function handlePrerenderRequest(app, request) {
|
|
|
35
39
|
});
|
|
36
40
|
return app.render(prerenderRequest, { routeData });
|
|
37
41
|
}
|
|
42
|
+
function isStaticImagesRequest(request) {
|
|
43
|
+
const { pathname } = new URL(request.url);
|
|
44
|
+
return pathname === STATIC_IMAGES_ENDPOINT && request.method === "POST";
|
|
45
|
+
}
|
|
46
|
+
function handleStaticImagesRequest() {
|
|
47
|
+
const staticImages = globalThis.astroAsset?.staticImages;
|
|
48
|
+
if (!staticImages || staticImages.size === 0) {
|
|
49
|
+
return new Response("[]", {
|
|
50
|
+
headers: { "Content-Type": "application/json" }
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
const entries = [];
|
|
54
|
+
for (const [originalPath, { originalSrcPath, transforms }] of staticImages) {
|
|
55
|
+
const serializedTransforms = [];
|
|
56
|
+
for (const [hash, { finalPath, transform }] of transforms) {
|
|
57
|
+
serializedTransforms.push({
|
|
58
|
+
hash,
|
|
59
|
+
finalPath,
|
|
60
|
+
transform
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
entries.push({ originalPath, originalSrcPath, transforms: serializedTransforms });
|
|
64
|
+
}
|
|
65
|
+
return new Response(JSON.stringify(entries), {
|
|
66
|
+
headers: { "Content-Type": "application/json" }
|
|
67
|
+
});
|
|
68
|
+
}
|
|
38
69
|
export {
|
|
39
70
|
handlePrerenderRequest,
|
|
71
|
+
handleStaticImagesRequest,
|
|
40
72
|
handleStaticPathsRequest,
|
|
41
73
|
isPrerenderRequest,
|
|
74
|
+
isStaticImagesRequest,
|
|
42
75
|
isStaticPathsRequest
|
|
43
76
|
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { CompileImageConfig } from '../vite-plugin-config.js';
|
|
2
|
+
/**
|
|
3
|
+
* Installs `globalThis.astroAsset.addStaticImage` for use inside workerd
|
|
4
|
+
* during prerendering. This mirrors the logic in astro's vite-plugin-assets.ts
|
|
5
|
+
* but uses only workerd-safe APIs (no node: imports).
|
|
6
|
+
*/
|
|
7
|
+
export declare function installAddStaticImage(config: CompileImageConfig): void;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { joinPaths, prependForwardSlash, removeBase } from "@astrojs/internal-helpers/path";
|
|
2
|
+
import { hashTransform, propsToFilename } from "astro/assets";
|
|
3
|
+
import { isESMImportedImage } from "astro/assets/utils";
|
|
4
|
+
function installAddStaticImage(config) {
|
|
5
|
+
if (globalThis.astroAsset?.addStaticImage) return;
|
|
6
|
+
if (!globalThis.astroAsset) {
|
|
7
|
+
globalThis.astroAsset = { referencedImages: /* @__PURE__ */ new Set() };
|
|
8
|
+
}
|
|
9
|
+
globalThis.astroAsset.addStaticImage = (options, hashProperties, _originalFSPath) => {
|
|
10
|
+
if (!globalThis.astroAsset.staticImages) {
|
|
11
|
+
globalThis.astroAsset.staticImages = /* @__PURE__ */ new Map();
|
|
12
|
+
}
|
|
13
|
+
const ESMImportedImageSrc = isESMImportedImage(options.src) ? options.src.src : options.src;
|
|
14
|
+
const finalOriginalPath = removeBase(
|
|
15
|
+
removeBase(ESMImportedImageSrc, config.base),
|
|
16
|
+
config.assetsPrefix ?? ""
|
|
17
|
+
);
|
|
18
|
+
const hash = hashTransform(options, config.imageServiceEntrypoint, hashProperties);
|
|
19
|
+
let finalFilePath;
|
|
20
|
+
let transformsForPath = globalThis.astroAsset.staticImages.get(finalOriginalPath);
|
|
21
|
+
const transformForHash = transformsForPath?.transforms.get(hash);
|
|
22
|
+
if (transformsForPath && transformForHash) {
|
|
23
|
+
finalFilePath = transformForHash.finalPath;
|
|
24
|
+
} else {
|
|
25
|
+
finalFilePath = prependForwardSlash(
|
|
26
|
+
joinPaths(
|
|
27
|
+
isESMImportedImage(options.src) ? "" : config.buildAssets,
|
|
28
|
+
prependForwardSlash(propsToFilename(finalOriginalPath, options, hash))
|
|
29
|
+
)
|
|
30
|
+
);
|
|
31
|
+
if (!transformsForPath) {
|
|
32
|
+
globalThis.astroAsset.staticImages.set(finalOriginalPath, {
|
|
33
|
+
originalSrcPath: _originalFSPath,
|
|
34
|
+
transforms: /* @__PURE__ */ new Map()
|
|
35
|
+
});
|
|
36
|
+
transformsForPath = globalThis.astroAsset.staticImages.get(finalOriginalPath);
|
|
37
|
+
}
|
|
38
|
+
transformsForPath.transforms.set(hash, {
|
|
39
|
+
finalPath: finalFilePath,
|
|
40
|
+
transform: options
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
if (config.assetsPrefix) {
|
|
44
|
+
return encodeURI(joinPaths(config.assetsPrefix, finalFilePath));
|
|
45
|
+
}
|
|
46
|
+
return encodeURI(prependForwardSlash(joinPaths(config.base, finalFilePath)));
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export {
|
|
50
|
+
installAddStaticImage
|
|
51
|
+
};
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import type { PluginOption } from 'vite';
|
|
2
|
+
export interface CompileImageConfig {
|
|
3
|
+
base: string;
|
|
4
|
+
assetsPrefix: string | undefined;
|
|
5
|
+
imageServiceEntrypoint: string;
|
|
6
|
+
buildAssets: string;
|
|
7
|
+
}
|
|
2
8
|
export interface Config {
|
|
3
9
|
sessionKVBindingName: string;
|
|
10
|
+
compileImageConfig: CompileImageConfig | null;
|
|
4
11
|
isPrerender: boolean;
|
|
5
12
|
}
|
|
6
13
|
export declare function createConfigPlugin(config: Omit<Config, 'isPrerender'>): PluginOption;
|
package/dist/wrangler.d.ts
CHANGED
|
@@ -3,12 +3,12 @@ export declare const DEFAULT_SESSION_KV_BINDING_NAME = "SESSION";
|
|
|
3
3
|
export declare const DEFAULT_IMAGES_BINDING_NAME = "IMAGES";
|
|
4
4
|
export declare const DEFAULT_ASSETS_BINDING_NAME = "ASSETS";
|
|
5
5
|
interface CloudflareConfigOptions {
|
|
6
|
-
sessionKVBindingName
|
|
7
|
-
imagesBindingName
|
|
6
|
+
sessionKVBindingName: string | undefined;
|
|
7
|
+
imagesBindingName: string | false | undefined;
|
|
8
8
|
}
|
|
9
9
|
/**
|
|
10
10
|
* Returns a config customizer that sets up the Astro Cloudflare defaults.
|
|
11
11
|
* Sets the main entrypoint and adds bindings for auto-provisioning.
|
|
12
12
|
*/
|
|
13
|
-
export declare function cloudflareConfigCustomizer(options
|
|
13
|
+
export declare function cloudflareConfigCustomizer(options: CloudflareConfigOptions): PluginConfig['config'];
|
|
14
14
|
export {};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astrojs/cloudflare",
|
|
3
|
-
"description": "Deploy your site to Cloudflare Workers
|
|
4
|
-
"version": "13.0.0-beta.
|
|
3
|
+
"description": "Deploy your site to Cloudflare Workers",
|
|
4
|
+
"version": "13.0.0-beta.9",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
7
7
|
"author": "withastro",
|
|
@@ -23,9 +23,9 @@
|
|
|
23
23
|
"./entrypoints/server": "./dist/entrypoints/server.js",
|
|
24
24
|
"./entrypoints/preview": "./dist/entrypoints/preview.js",
|
|
25
25
|
"./entrypoints/server.js": "./dist/entrypoints/server.js",
|
|
26
|
-
"./image-service": "./dist/entrypoints/image-service.js",
|
|
27
|
-
"./image-endpoint": "./dist/entrypoints/image-endpoint.js",
|
|
26
|
+
"./image-service": "./dist/entrypoints/image-service-external.js",
|
|
28
27
|
"./image-transform-endpoint": "./dist/entrypoints/image-transform-endpoint.js",
|
|
28
|
+
"./image-service-workerd": "./dist/entrypoints/image-service-workerd.js",
|
|
29
29
|
"./handler": "./dist/utils/handler.js",
|
|
30
30
|
"./types.d.ts": "./types.d.ts",
|
|
31
31
|
"./package.json": "./package.json"
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"types.d.ts"
|
|
36
36
|
],
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@cloudflare/vite-plugin": "^1.
|
|
38
|
+
"@cloudflare/vite-plugin": "^1.25.2",
|
|
39
39
|
"piccolore": "^0.1.3",
|
|
40
40
|
"tinyglobby": "^0.2.15",
|
|
41
41
|
"vite": "^7.3.1",
|
|
@@ -47,11 +47,11 @@
|
|
|
47
47
|
"wrangler": "^4.61.1"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
|
-
"@cloudflare/workers-types": "^4.
|
|
50
|
+
"@cloudflare/workers-types": "^4.20260228.0",
|
|
51
51
|
"@types/node": "^25.2.2",
|
|
52
52
|
"cheerio": "1.2.0",
|
|
53
|
-
"devalue": "^5.6.
|
|
54
|
-
"astro": "6.0.0-beta.
|
|
53
|
+
"devalue": "^5.6.3",
|
|
54
|
+
"astro": "6.0.0-beta.15",
|
|
55
55
|
"astro-scripts": "0.0.14"
|
|
56
56
|
},
|
|
57
57
|
"publishConfig": {
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { imageConfig } from "astro:assets";
|
|
2
|
-
import { isRemotePath } from "@astrojs/internal-helpers/path";
|
|
3
|
-
import { isRemoteAllowed } from "@astrojs/internal-helpers/remote";
|
|
4
|
-
const prerender = false;
|
|
5
|
-
const GET = (ctx) => {
|
|
6
|
-
const href = ctx.url.searchParams.get("href");
|
|
7
|
-
if (!href) {
|
|
8
|
-
return new Response("Missing 'href' query parameter", {
|
|
9
|
-
status: 400,
|
|
10
|
-
statusText: "Missing 'href' query parameter"
|
|
11
|
-
});
|
|
12
|
-
}
|
|
13
|
-
if (isRemotePath(href)) {
|
|
14
|
-
if (isRemoteAllowed(href, imageConfig) === false) {
|
|
15
|
-
return new Response("Forbidden", { status: 403 });
|
|
16
|
-
} else {
|
|
17
|
-
return Response.redirect(href, 302);
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
const proxied = new URL(href, ctx.url.origin);
|
|
21
|
-
if (proxied.origin !== ctx.url.origin) {
|
|
22
|
-
return new Response("Forbidden", { status: 403 });
|
|
23
|
-
}
|
|
24
|
-
return fetch(proxied);
|
|
25
|
-
};
|
|
26
|
-
export {
|
|
27
|
-
GET,
|
|
28
|
-
prerender
|
|
29
|
-
};
|
|
File without changes
|