@astrojs/cloudflare 0.0.0-cf-assets-20231021193132
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/README.md +412 -0
- package/dist/entrypoints/image-service.d.ts +6 -0
- package/dist/entrypoints/image-service.js +340 -0
- package/dist/entrypoints/server.advanced.d.ts +21 -0
- package/dist/entrypoints/server.advanced.js +44 -0
- package/dist/entrypoints/server.directory.d.ts +14 -0
- package/dist/entrypoints/server.directory.js +46 -0
- package/dist/getAdapter.d.ts +5 -0
- package/dist/getAdapter.js +31 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +517 -0
- package/dist/util.d.ts +2 -0
- package/dist/util.js +13 -0
- package/dist/utils/deduplicatePatterns.d.ts +8 -0
- package/dist/utils/deduplicatePatterns.js +23 -0
- package/dist/utils/getCFObject.d.ts +2 -0
- package/dist/utils/getCFObject.js +67 -0
- package/dist/utils/parser.d.ts +27 -0
- package/dist/utils/parser.js +149 -0
- package/dist/utils/prependForwardSlash.d.ts +1 -0
- package/dist/utils/prependForwardSlash.js +3 -0
- package/dist/utils/rewriteWasmImportPath.d.ts +8 -0
- package/dist/utils/rewriteWasmImportPath.js +23 -0
- package/dist/utils/wasm-module-loader.d.ts +14 -0
- package/dist/utils/wasm-module-loader.js +101 -0
- package/package.json +65 -0
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
function isESMImportedImage(src) {
|
|
2
|
+
return typeof src === 'object';
|
|
3
|
+
}
|
|
4
|
+
function isRemotePath(src) {
|
|
5
|
+
return /^(http|ftp|https|ws):?\/\//.test(src) || src.startsWith('data:');
|
|
6
|
+
}
|
|
7
|
+
function matchHostname(url, hostname, allowWildcard) {
|
|
8
|
+
if (!hostname) {
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
else if (!allowWildcard || !hostname.startsWith('*')) {
|
|
12
|
+
return hostname === url.hostname;
|
|
13
|
+
}
|
|
14
|
+
else if (hostname.startsWith('**.')) {
|
|
15
|
+
const slicedHostname = hostname.slice(2); // ** length
|
|
16
|
+
return slicedHostname !== url.hostname && url.hostname.endsWith(slicedHostname);
|
|
17
|
+
}
|
|
18
|
+
else if (hostname.startsWith('*.')) {
|
|
19
|
+
const slicedHostname = hostname.slice(1); // * length
|
|
20
|
+
const additionalSubdomains = url.hostname
|
|
21
|
+
.replace(slicedHostname, '')
|
|
22
|
+
.split('.')
|
|
23
|
+
.filter(Boolean);
|
|
24
|
+
return additionalSubdomains.length === 1;
|
|
25
|
+
}
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
export function matchPort(url, port) {
|
|
29
|
+
return !port || port === url.port;
|
|
30
|
+
}
|
|
31
|
+
export function matchProtocol(url, protocol) {
|
|
32
|
+
return !protocol || protocol === url.protocol.slice(0, -1);
|
|
33
|
+
}
|
|
34
|
+
export function matchPathname(url, pathname, allowWildcard) {
|
|
35
|
+
if (!pathname) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
else if (!allowWildcard || !pathname.endsWith('*')) {
|
|
39
|
+
return pathname === url.pathname;
|
|
40
|
+
}
|
|
41
|
+
else if (pathname.endsWith('/**')) {
|
|
42
|
+
const slicedPathname = pathname.slice(0, -2); // ** length
|
|
43
|
+
return slicedPathname !== url.pathname && url.pathname.startsWith(slicedPathname);
|
|
44
|
+
}
|
|
45
|
+
else if (pathname.endsWith('/*')) {
|
|
46
|
+
const slicedPathname = pathname.slice(0, -1); // * length
|
|
47
|
+
const additionalPathChunks = url.pathname
|
|
48
|
+
.replace(slicedPathname, '')
|
|
49
|
+
.split('/')
|
|
50
|
+
.filter(Boolean);
|
|
51
|
+
return additionalPathChunks.length === 1;
|
|
52
|
+
}
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
function matchPattern(url, remotePattern) {
|
|
56
|
+
return (matchProtocol(url, remotePattern.protocol) &&
|
|
57
|
+
matchHostname(url, remotePattern.hostname, true) &&
|
|
58
|
+
matchPort(url, remotePattern.port) &&
|
|
59
|
+
matchPathname(url, remotePattern.pathname, true));
|
|
60
|
+
}
|
|
61
|
+
function isRemoteAllowed(src, { domains = [], remotePatterns = [], }) {
|
|
62
|
+
if (!isRemotePath(src))
|
|
63
|
+
return false;
|
|
64
|
+
const url = new URL(src);
|
|
65
|
+
return (domains.some((domain) => matchHostname(url, domain)) ||
|
|
66
|
+
remotePatterns.some((remotePattern) => matchPattern(url, remotePattern)));
|
|
67
|
+
}
|
|
68
|
+
const VALID_SUPPORTED_FORMATS = [
|
|
69
|
+
'jpeg',
|
|
70
|
+
'jpg',
|
|
71
|
+
'png',
|
|
72
|
+
'tiff',
|
|
73
|
+
'webp',
|
|
74
|
+
'gif',
|
|
75
|
+
'svg',
|
|
76
|
+
'avif',
|
|
77
|
+
];
|
|
78
|
+
const DEFAULT_OUTPUT_FORMAT = 'webp';
|
|
79
|
+
function isString(path) {
|
|
80
|
+
return typeof path === 'string' || path instanceof String;
|
|
81
|
+
}
|
|
82
|
+
function removeTrailingForwardSlash(path) {
|
|
83
|
+
return path.endsWith('/') ? path.slice(0, path.length - 1) : path;
|
|
84
|
+
}
|
|
85
|
+
function removeLeadingForwardSlash(path) {
|
|
86
|
+
return path.startsWith('/') ? path.substring(1) : path;
|
|
87
|
+
}
|
|
88
|
+
function trimSlashes(path) {
|
|
89
|
+
return path.replace(/^\/|\/$/g, '');
|
|
90
|
+
}
|
|
91
|
+
function joinPaths(...paths) {
|
|
92
|
+
return paths
|
|
93
|
+
.filter(isString)
|
|
94
|
+
.map((path, i) => {
|
|
95
|
+
if (i === 0) {
|
|
96
|
+
return removeTrailingForwardSlash(path);
|
|
97
|
+
}
|
|
98
|
+
else if (i === paths.length - 1) {
|
|
99
|
+
return removeLeadingForwardSlash(path);
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
return trimSlashes(path);
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
.join('/');
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Returns the final dimensions of an image based on the user's options.
|
|
109
|
+
*
|
|
110
|
+
* For local images:
|
|
111
|
+
* - If the user specified both width and height, we'll use those.
|
|
112
|
+
* - If the user specified only one of them, we'll use the original image's aspect ratio to calculate the other.
|
|
113
|
+
* - If the user didn't specify either, we'll use the original image's dimensions.
|
|
114
|
+
*
|
|
115
|
+
* For remote images:
|
|
116
|
+
* - Widths and heights are always required, so we'll use the user's specified width and height.
|
|
117
|
+
*/
|
|
118
|
+
function getTargetDimensions(options) {
|
|
119
|
+
let targetWidth = options.width;
|
|
120
|
+
let targetHeight = options.height;
|
|
121
|
+
if (isESMImportedImage(options.src)) {
|
|
122
|
+
const aspectRatio = options.src.width / options.src.height;
|
|
123
|
+
if (targetHeight && !targetWidth) {
|
|
124
|
+
// If we have a height but no width, use height to calculate the width
|
|
125
|
+
targetWidth = Math.round(targetHeight * aspectRatio);
|
|
126
|
+
}
|
|
127
|
+
else if (targetWidth && !targetHeight) {
|
|
128
|
+
// If we have a width but no height, use width to calculate the height
|
|
129
|
+
targetHeight = Math.round(targetWidth / aspectRatio);
|
|
130
|
+
}
|
|
131
|
+
else if (!targetWidth && !targetHeight) {
|
|
132
|
+
// If we have neither width or height, use the original image's dimensions
|
|
133
|
+
targetWidth = options.src.width;
|
|
134
|
+
targetHeight = options.src.height;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// TypeScript doesn't know this, but because of previous hooks we always know that targetWidth and targetHeight are defined
|
|
138
|
+
return {
|
|
139
|
+
targetWidth: targetWidth,
|
|
140
|
+
targetHeight: targetHeight,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
const service = {
|
|
144
|
+
validateOptions: (options /*, imageConfig: AstroConfig['image']*/) => {
|
|
145
|
+
// add custom global CF image service options
|
|
146
|
+
// const serviceConfig = imageConfig.service.config;
|
|
147
|
+
// need to add checks for limits
|
|
148
|
+
// https://developers.cloudflare.com/images/image-resizing/format-limitations/#format-limitations
|
|
149
|
+
// `src` is missing or is `undefined`.
|
|
150
|
+
if (!options.src || (typeof options.src !== 'string' && typeof options.src !== 'object')) {
|
|
151
|
+
throw new Error('ExpectedImage');
|
|
152
|
+
// throw new AstroError({
|
|
153
|
+
// ...AstroErrorData.ExpectedImage,
|
|
154
|
+
// message: AstroErrorData.ExpectedImage.message(
|
|
155
|
+
// JSON.stringify(options.src),
|
|
156
|
+
// typeof options.src,
|
|
157
|
+
// JSON.stringify(options, (_, v) => (v === undefined ? null : v))
|
|
158
|
+
// ),
|
|
159
|
+
// });
|
|
160
|
+
}
|
|
161
|
+
if (!isESMImportedImage(options.src)) {
|
|
162
|
+
// User passed an `/@fs/` path or a filesystem path instead of the full image.
|
|
163
|
+
if (options.src.startsWith('/@fs/') ||
|
|
164
|
+
(!isRemotePath(options.src) && !options.src.startsWith('/'))) {
|
|
165
|
+
throw new Error('LocalImageUsedWrongly');
|
|
166
|
+
// throw new AstroError({
|
|
167
|
+
// ...AstroErrorData.LocalImageUsedWrongly,
|
|
168
|
+
// message: AstroErrorData.LocalImageUsedWrongly.message(options.src),
|
|
169
|
+
// });
|
|
170
|
+
}
|
|
171
|
+
// For remote images, width and height are explicitly required as we can't infer them from the file
|
|
172
|
+
let missingDimension;
|
|
173
|
+
if (!options.width && !options.height) {
|
|
174
|
+
missingDimension = 'both';
|
|
175
|
+
}
|
|
176
|
+
else if (!options.width && options.height) {
|
|
177
|
+
missingDimension = 'width';
|
|
178
|
+
}
|
|
179
|
+
else if (options.width && !options.height) {
|
|
180
|
+
missingDimension = 'height';
|
|
181
|
+
}
|
|
182
|
+
if (missingDimension) {
|
|
183
|
+
throw new Error('MissingImageDimension');
|
|
184
|
+
// throw new AstroError({
|
|
185
|
+
// ...AstroErrorData.MissingImageDimension,
|
|
186
|
+
// message: AstroErrorData.MissingImageDimension.message(missingDimension, options.src),
|
|
187
|
+
// });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
if (!VALID_SUPPORTED_FORMATS.includes(options.src.format)) {
|
|
192
|
+
throw new Error('UnsupportedImageFormat');
|
|
193
|
+
// throw new AstroError({
|
|
194
|
+
// ...AstroErrorData.UnsupportedImageFormat,
|
|
195
|
+
// message: AstroErrorData.UnsupportedImageFormat.message(
|
|
196
|
+
// options.src.format,
|
|
197
|
+
// options.src.src,
|
|
198
|
+
// VALID_SUPPORTED_FORMATS
|
|
199
|
+
// ),
|
|
200
|
+
// });
|
|
201
|
+
}
|
|
202
|
+
if (options.widths && options.densities) {
|
|
203
|
+
throw new Error('IncompatibleDescriptorOptions');
|
|
204
|
+
// throw new AstroError(AstroErrorData.IncompatibleDescriptorOptions);
|
|
205
|
+
}
|
|
206
|
+
// We currently do not support processing SVGs, so whenever the input format is a SVG, force the output to also be one
|
|
207
|
+
if (options.src.format === 'svg') {
|
|
208
|
+
options.format = 'svg';
|
|
209
|
+
}
|
|
210
|
+
if ((options.src.format === 'svg' && options.format !== 'svg') ||
|
|
211
|
+
(options.src.format !== 'svg' && options.format === 'svg')) {
|
|
212
|
+
throw new Error('UnsupportedImageConversion');
|
|
213
|
+
// throw new AstroError(AstroErrorData.UnsupportedImageConversion);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
// If the user didn't specify a format, we'll default to `webp`. It offers the best ratio of compatibility / quality
|
|
217
|
+
// In the future, hopefully we can replace this with `avif`, alas, Edge. See https://caniuse.com/avif
|
|
218
|
+
if (!options.format) {
|
|
219
|
+
options.format = DEFAULT_OUTPUT_FORMAT;
|
|
220
|
+
}
|
|
221
|
+
// Sometimes users will pass number generated from division, which can result in floating point numbers
|
|
222
|
+
if (options.width)
|
|
223
|
+
options.width = Math.round(options.width);
|
|
224
|
+
if (options.height)
|
|
225
|
+
options.height = Math.round(options.height);
|
|
226
|
+
return options;
|
|
227
|
+
},
|
|
228
|
+
getHTMLAttributes: (options) => {
|
|
229
|
+
const { targetWidth, targetHeight } = getTargetDimensions(options);
|
|
230
|
+
const { src, width, height, format, quality, densities, widths, formats, ...attributes } = options;
|
|
231
|
+
return {
|
|
232
|
+
...attributes,
|
|
233
|
+
width: targetWidth,
|
|
234
|
+
height: targetHeight,
|
|
235
|
+
loading: attributes.loading ?? 'lazy',
|
|
236
|
+
decoding: attributes.decoding ?? 'async',
|
|
237
|
+
};
|
|
238
|
+
},
|
|
239
|
+
getSrcSet(options) {
|
|
240
|
+
const srcSet = [];
|
|
241
|
+
const { targetWidth } = getTargetDimensions(options);
|
|
242
|
+
const { widths, densities } = options;
|
|
243
|
+
const targetFormat = options.format ?? DEFAULT_OUTPUT_FORMAT;
|
|
244
|
+
// NOTE: Depending on the cloudflare fit value, the image might never be enlarged
|
|
245
|
+
// For remote images, we don't know the original image's dimensions, so we cannot know the maximum width
|
|
246
|
+
// It is ultimately the user's responsibility to make sure they don't request images larger than the original
|
|
247
|
+
let imageWidth = options.width;
|
|
248
|
+
let maxWidth = Infinity;
|
|
249
|
+
// However, if it's an imported image, we can use the original image's width as a maximum width
|
|
250
|
+
if (isESMImportedImage(options.src)) {
|
|
251
|
+
imageWidth = options.src.width;
|
|
252
|
+
maxWidth = imageWidth;
|
|
253
|
+
}
|
|
254
|
+
// Since `widths` and `densities` ultimately control the width and height of the image,
|
|
255
|
+
// we don't want the dimensions the user specified, we'll create those ourselves.
|
|
256
|
+
const { width: transformWidth, height: transformHeight, ...transformWithoutDimensions } = options;
|
|
257
|
+
// Collect widths to generate from specified densities or widths
|
|
258
|
+
const allWidths = [];
|
|
259
|
+
if (densities) {
|
|
260
|
+
// Densities can either be specified as numbers, or descriptors (ex: '1x'), we'll convert them all to numbers
|
|
261
|
+
const densityValues = densities.map((density) => {
|
|
262
|
+
if (typeof density === 'number') {
|
|
263
|
+
return density;
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
return parseFloat(density);
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
// Calculate the widths for each density, rounding to avoid floats.
|
|
270
|
+
const densityWidths = densityValues
|
|
271
|
+
.sort()
|
|
272
|
+
.map((density) => Math.round(targetWidth * density));
|
|
273
|
+
allWidths.push(...densityWidths.map((width, index) => ({
|
|
274
|
+
maxTargetWidth: Math.min(width, maxWidth),
|
|
275
|
+
descriptor: `${densityValues[index]}x`,
|
|
276
|
+
})));
|
|
277
|
+
}
|
|
278
|
+
else if (widths) {
|
|
279
|
+
allWidths.push(...widths.map((width) => ({
|
|
280
|
+
maxTargetWidth: Math.min(width, maxWidth),
|
|
281
|
+
descriptor: `${width}w`,
|
|
282
|
+
})));
|
|
283
|
+
}
|
|
284
|
+
// Caution: The logic below is a bit tricky, as we need to make sure we don't generate the same image multiple times
|
|
285
|
+
// When making changes, make sure to test with different combinations of local/remote images widths, densities, and dimensions etc.
|
|
286
|
+
for (const { maxTargetWidth, descriptor } of allWidths) {
|
|
287
|
+
const srcSetTransform = { ...transformWithoutDimensions };
|
|
288
|
+
// Only set the width if it's different from the original image's width, to avoid generating the same image multiple times
|
|
289
|
+
if (maxTargetWidth !== imageWidth) {
|
|
290
|
+
srcSetTransform.width = maxTargetWidth;
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
// If the width is the same as the original image's width, and we have both dimensions, it probably means
|
|
294
|
+
// it's a remote image, so we'll use the user's specified dimensions to avoid recreating the original image unnecessarily
|
|
295
|
+
if (options.width && options.height) {
|
|
296
|
+
srcSetTransform.width = options.width;
|
|
297
|
+
srcSetTransform.height = options.height;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
srcSet.push({
|
|
301
|
+
transform: srcSetTransform,
|
|
302
|
+
descriptor,
|
|
303
|
+
attributes: {
|
|
304
|
+
type: `image/${targetFormat}`,
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
return srcSet;
|
|
309
|
+
},
|
|
310
|
+
getURL: (options, imageConfig) => {
|
|
311
|
+
const resizingParams = [];
|
|
312
|
+
if (options.width)
|
|
313
|
+
resizingParams.push(`width=${options.width}`);
|
|
314
|
+
if (options.height)
|
|
315
|
+
resizingParams.push(`height=${options.height}`);
|
|
316
|
+
if (options.quality)
|
|
317
|
+
resizingParams.push(`quality=${options.quality}`);
|
|
318
|
+
if (options.fit)
|
|
319
|
+
resizingParams.push(`fit=${options.fit}`);
|
|
320
|
+
if (options.format)
|
|
321
|
+
resizingParams.push(`format=${options.format}`);
|
|
322
|
+
let imageSource = '';
|
|
323
|
+
if (isESMImportedImage(options.src)) {
|
|
324
|
+
imageSource = options.src.src;
|
|
325
|
+
}
|
|
326
|
+
else if (isRemoteAllowed(options.src, imageConfig)) {
|
|
327
|
+
imageSource = options.src;
|
|
328
|
+
}
|
|
329
|
+
else {
|
|
330
|
+
// If it's not an imported image, nor is it allowed using the current domains or remote patterns, we'll just return the original URL
|
|
331
|
+
return options.src;
|
|
332
|
+
}
|
|
333
|
+
const imageEndpoint = joinPaths(
|
|
334
|
+
//@ts-ignore
|
|
335
|
+
import.meta.env.BASE_URL, '/cdn-cgi/image', resizingParams.join(','), imageSource);
|
|
336
|
+
console.log(imageEndpoint);
|
|
337
|
+
return imageEndpoint;
|
|
338
|
+
},
|
|
339
|
+
};
|
|
340
|
+
export default service;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { CacheStorage, ExecutionContext, Request as CFRequest } from '@cloudflare/workers-types';
|
|
2
|
+
import type { SSRManifest } from 'astro';
|
|
3
|
+
type Env = {
|
|
4
|
+
ASSETS: {
|
|
5
|
+
fetch: (req: Request) => Promise<Response>;
|
|
6
|
+
};
|
|
7
|
+
};
|
|
8
|
+
export interface AdvancedRuntime<T extends object = object> {
|
|
9
|
+
runtime: {
|
|
10
|
+
waitUntil: (promise: Promise<any>) => void;
|
|
11
|
+
env: Env & T;
|
|
12
|
+
cf: CFRequest['cf'];
|
|
13
|
+
caches: CacheStorage;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export declare function createExports(manifest: SSRManifest): {
|
|
17
|
+
default: {
|
|
18
|
+
fetch: (request: Request & CFRequest, env: Env, context: ExecutionContext) => Promise<Response>;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { App } from 'astro/app';
|
|
2
|
+
import { getProcessEnvProxy, isNode } from '../util.js';
|
|
3
|
+
if (!isNode) {
|
|
4
|
+
process.env = getProcessEnvProxy();
|
|
5
|
+
}
|
|
6
|
+
export function createExports(manifest) {
|
|
7
|
+
const app = new App(manifest);
|
|
8
|
+
const fetch = async (request, env, context) => {
|
|
9
|
+
// TODO: remove this any cast in the future
|
|
10
|
+
// REF: the type cast to any is needed because the Cloudflare Env Type is not assignable to type 'ProcessEnv'
|
|
11
|
+
process.env = env;
|
|
12
|
+
const { pathname } = new URL(request.url);
|
|
13
|
+
// static assets fallback, in case default _routes.json is not used
|
|
14
|
+
if (manifest.assets.has(pathname)) {
|
|
15
|
+
return env.ASSETS.fetch(request);
|
|
16
|
+
}
|
|
17
|
+
let routeData = app.match(request, { matchNotFound: true });
|
|
18
|
+
if (routeData) {
|
|
19
|
+
Reflect.set(request, Symbol.for('astro.clientAddress'), request.headers.get('cf-connecting-ip'));
|
|
20
|
+
const locals = {
|
|
21
|
+
runtime: {
|
|
22
|
+
waitUntil: (promise) => {
|
|
23
|
+
context.waitUntil(promise);
|
|
24
|
+
},
|
|
25
|
+
env: env,
|
|
26
|
+
cf: request.cf,
|
|
27
|
+
caches: caches,
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
let response = await app.render(request, routeData, locals);
|
|
31
|
+
if (app.setCookieHeaders) {
|
|
32
|
+
for (const setCookieHeader of app.setCookieHeaders(response)) {
|
|
33
|
+
response.headers.append('Set-Cookie', setCookieHeader);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return response;
|
|
37
|
+
}
|
|
38
|
+
return new Response(null, {
|
|
39
|
+
status: 404,
|
|
40
|
+
statusText: 'Not found',
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
return { default: { fetch } };
|
|
44
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { CacheStorage, EventContext, Request as CFRequest } from '@cloudflare/workers-types';
|
|
2
|
+
import type { SSRManifest } from 'astro';
|
|
3
|
+
export interface DirectoryRuntime<T extends object = object> {
|
|
4
|
+
runtime: {
|
|
5
|
+
waitUntil: (promise: Promise<any>) => void;
|
|
6
|
+
env: EventContext<unknown, string, unknown>['env'] & T;
|
|
7
|
+
cf: CFRequest['cf'];
|
|
8
|
+
caches: CacheStorage;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export declare function createExports(manifest: SSRManifest): {
|
|
12
|
+
onRequest: (context: EventContext<unknown, string, unknown>) => Promise<Response | import("@cloudflare/workers-types").Response>;
|
|
13
|
+
manifest: SSRManifest;
|
|
14
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { App } from 'astro/app';
|
|
2
|
+
import { getProcessEnvProxy, isNode } from '../util.js';
|
|
3
|
+
if (!isNode) {
|
|
4
|
+
process.env = getProcessEnvProxy();
|
|
5
|
+
}
|
|
6
|
+
export function createExports(manifest) {
|
|
7
|
+
const app = new App(manifest);
|
|
8
|
+
const onRequest = async (context) => {
|
|
9
|
+
const request = context.request;
|
|
10
|
+
const { env } = context;
|
|
11
|
+
// TODO: remove this any cast in the future
|
|
12
|
+
// REF: the type cast to any is needed because the Cloudflare Env Type is not assignable to type 'ProcessEnv'
|
|
13
|
+
process.env = env;
|
|
14
|
+
const { pathname } = new URL(request.url);
|
|
15
|
+
// static assets fallback, in case default _routes.json is not used
|
|
16
|
+
if (manifest.assets.has(pathname)) {
|
|
17
|
+
return env.ASSETS.fetch(request);
|
|
18
|
+
}
|
|
19
|
+
let routeData = app.match(request, { matchNotFound: true });
|
|
20
|
+
if (routeData) {
|
|
21
|
+
Reflect.set(request, Symbol.for('astro.clientAddress'), request.headers.get('cf-connecting-ip'));
|
|
22
|
+
const locals = {
|
|
23
|
+
runtime: {
|
|
24
|
+
waitUntil: (promise) => {
|
|
25
|
+
context.waitUntil(promise);
|
|
26
|
+
},
|
|
27
|
+
env: context.env,
|
|
28
|
+
cf: request.cf,
|
|
29
|
+
caches: caches,
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
let response = await app.render(request, routeData, locals);
|
|
33
|
+
if (app.setCookieHeaders) {
|
|
34
|
+
for (const setCookieHeader of app.setCookieHeaders(response)) {
|
|
35
|
+
response.headers.append('Set-Cookie', setCookieHeader);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return response;
|
|
39
|
+
}
|
|
40
|
+
return new Response(null, {
|
|
41
|
+
status: 404,
|
|
42
|
+
statusText: 'Not found',
|
|
43
|
+
});
|
|
44
|
+
};
|
|
45
|
+
return { onRequest, manifest };
|
|
46
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export function getAdapter({ isModeDirectory, functionPerRoute, }) {
|
|
2
|
+
const astroFeatures = {
|
|
3
|
+
hybridOutput: 'stable',
|
|
4
|
+
staticOutput: 'unsupported',
|
|
5
|
+
serverOutput: 'stable',
|
|
6
|
+
assets: {
|
|
7
|
+
supportKind: 'stable',
|
|
8
|
+
// FIXME: UNDO THIS BEFORE RELEASE
|
|
9
|
+
isSharpCompatible: true,
|
|
10
|
+
isSquooshCompatible: false,
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
if (isModeDirectory) {
|
|
14
|
+
return {
|
|
15
|
+
name: '@astrojs/cloudflare',
|
|
16
|
+
serverEntrypoint: '@astrojs/cloudflare/entrypoints/server.directory.js',
|
|
17
|
+
exports: ['onRequest', 'manifest'],
|
|
18
|
+
adapterFeatures: {
|
|
19
|
+
functionPerRoute,
|
|
20
|
+
edgeMiddleware: false,
|
|
21
|
+
},
|
|
22
|
+
supportedAstroFeatures: astroFeatures,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
name: '@astrojs/cloudflare',
|
|
27
|
+
serverEntrypoint: '@astrojs/cloudflare/entrypoints/server.advanced.js',
|
|
28
|
+
exports: ['default'],
|
|
29
|
+
supportedAstroFeatures: astroFeatures,
|
|
30
|
+
};
|
|
31
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { AstroIntegration } from 'astro';
|
|
2
|
+
export type { AdvancedRuntime } from './entrypoints/server.advanced.js';
|
|
3
|
+
export type { DirectoryRuntime } from './entrypoints/server.directory.js';
|
|
4
|
+
type Options = {
|
|
5
|
+
mode?: 'directory' | 'advanced';
|
|
6
|
+
functionPerRoute?: boolean;
|
|
7
|
+
/** Configure automatic `routes.json` generation */
|
|
8
|
+
routes?: {
|
|
9
|
+
/** Strategy for generating `include` and `exclude` patterns
|
|
10
|
+
* - `auto`: Will use the strategy that generates the least amount of entries.
|
|
11
|
+
* - `include`: For each page or endpoint in your application that is not prerendered, an entry in the `include` array will be generated. For each page that is prerendered and whoose path is matched by an `include` entry, an entry in the `exclude` array will be generated.
|
|
12
|
+
* - `exclude`: One `"/*"` entry in the `include` array will be generated. For each page that is prerendered, an entry in the `exclude` array will be generated.
|
|
13
|
+
* */
|
|
14
|
+
strategy?: 'auto' | 'include' | 'exclude';
|
|
15
|
+
/** Additional `include` patterns */
|
|
16
|
+
include?: string[];
|
|
17
|
+
/** Additional `exclude` patterns */
|
|
18
|
+
exclude?: string[];
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Going forward only the object API should be used. The modes work as known before:
|
|
22
|
+
* 'off': current behaviour (wrangler is needed)
|
|
23
|
+
* 'local': use a static req.cf object, and env vars defined in wrangler.toml & .dev.vars (astro dev is enough)
|
|
24
|
+
* 'remote': use a dynamic real-live req.cf object, and env vars defined in wrangler.toml & .dev.vars (astro dev is enough)
|
|
25
|
+
*/
|
|
26
|
+
runtime?: 'off' | 'local' | 'remote' | {
|
|
27
|
+
mode: 'off';
|
|
28
|
+
} | {
|
|
29
|
+
mode: 'remote';
|
|
30
|
+
} | {
|
|
31
|
+
mode: 'local';
|
|
32
|
+
persistTo?: string;
|
|
33
|
+
};
|
|
34
|
+
wasmModuleImports?: boolean;
|
|
35
|
+
};
|
|
36
|
+
export default function createIntegration(args?: Options): AstroIntegration;
|