@cloudflare/pages-shared 0.0.1 → 0.0.4

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.
@@ -0,0 +1,395 @@
1
+ import {
2
+ FoundResponse,
3
+ InternalServerErrorResponse,
4
+ MethodNotAllowedResponse,
5
+ MovedPermanentlyResponse,
6
+ NotAcceptableResponse,
7
+ NotFoundResponse,
8
+ NotModifiedResponse,
9
+ OkResponse,
10
+ PermanentRedirectResponse,
11
+ SeeOtherResponse,
12
+ TemporaryRedirectResponse
13
+ } from "./responses";
14
+ import { generateRulesMatcher, replacer } from "./rulesEngine";
15
+ export const ASSET_PRESERVATION_CACHE = "assetPreservationCache";
16
+ const CACHE_CONTROL_PRESERVATION = "public, s-maxage=604800";
17
+ export const CACHE_CONTROL_BROWSER = "public, max-age=0, must-revalidate";
18
+ export const REDIRECTS_VERSION = 1;
19
+ export const HEADERS_VERSION = 2;
20
+ export const HEADERS_VERSION_V1 = 1;
21
+ export const ANALYTICS_VERSION = 1;
22
+ export function normaliseHeaders(headers) {
23
+ if (headers.version === HEADERS_VERSION) {
24
+ return headers.rules;
25
+ } else if (headers.version === HEADERS_VERSION_V1) {
26
+ return Object.keys(headers.rules).reduce(
27
+ (acc, key) => {
28
+ acc[key] = {
29
+ set: headers.rules[key]
30
+ };
31
+ return acc;
32
+ },
33
+ {}
34
+ );
35
+ } else {
36
+ return {};
37
+ }
38
+ }
39
+ export async function generateHandler({
40
+ request,
41
+ metadata,
42
+ xServerEnvHeader,
43
+ logError,
44
+ findAssetEntryForPath,
45
+ getAssetKey,
46
+ negotiateContent,
47
+ fetchAsset,
48
+ generateNotFoundResponse = async (notFoundRequest, notFoundFindAssetEntryForPath, notFoundServeAsset) => {
49
+ let assetEntry;
50
+ if (assetEntry = await notFoundFindAssetEntryForPath("/index.html")) {
51
+ return notFoundServeAsset(assetEntry, { preserve: false });
52
+ }
53
+ return new NotFoundResponse();
54
+ },
55
+ attachAdditionalHeaders = () => {
56
+ },
57
+ caches,
58
+ waitUntil
59
+ }) {
60
+ const url = new URL(request.url);
61
+ const { protocol, host, search } = url;
62
+ let { pathname } = url;
63
+ const earlyHintsCache = metadata.deploymentId ? await caches?.open(`eh:${metadata.deploymentId}`) : void 0;
64
+ const headerRules = metadata.headers ? normaliseHeaders(metadata.headers) : {};
65
+ const staticRules = metadata.redirects?.version === REDIRECTS_VERSION ? metadata.redirects.staticRules || {} : {};
66
+ const staticRedirectsMatcher = () => {
67
+ const withHostMatch = staticRules[`https://${host}${pathname}`];
68
+ const withoutHostMatch = staticRules[pathname];
69
+ if (withHostMatch && withoutHostMatch) {
70
+ if (withHostMatch.lineNumber < withoutHostMatch.lineNumber) {
71
+ return withHostMatch;
72
+ } else {
73
+ return withoutHostMatch;
74
+ }
75
+ }
76
+ return withHostMatch || withoutHostMatch;
77
+ };
78
+ const generateRedirectsMatcher = () => generateRulesMatcher(
79
+ metadata.redirects?.version === REDIRECTS_VERSION ? metadata.redirects.rules : {},
80
+ ({ status, to }, replacements) => ({
81
+ status,
82
+ to: replacer(to, replacements)
83
+ })
84
+ );
85
+ let assetEntry;
86
+ async function generateResponse() {
87
+ const match = staticRedirectsMatcher() || generateRedirectsMatcher()({ request })[0];
88
+ if (match) {
89
+ const { status, to } = match;
90
+ const destination = new URL(to, request.url);
91
+ const location = destination.origin === new URL(request.url).origin ? `${destination.pathname}${destination.search || search}${destination.hash}` : `${destination.href}${destination.search ? "" : search}${destination.hash}`;
92
+ switch (status) {
93
+ case 301:
94
+ return new MovedPermanentlyResponse(location);
95
+ case 303:
96
+ return new SeeOtherResponse(location);
97
+ case 307:
98
+ return new TemporaryRedirectResponse(location);
99
+ case 308:
100
+ return new PermanentRedirectResponse(location);
101
+ case 302:
102
+ default:
103
+ return new FoundResponse(location);
104
+ }
105
+ }
106
+ if (!request.method.match(/^(get|head)$/i)) {
107
+ return new MethodNotAllowedResponse();
108
+ }
109
+ try {
110
+ pathname = globalThis.decodeURIComponent(pathname);
111
+ } catch (err) {
112
+ }
113
+ if (pathname.endsWith("/")) {
114
+ if (assetEntry = await findAssetEntryForPath(`${pathname}index.html`)) {
115
+ return serveAsset(assetEntry);
116
+ } else if (pathname.endsWith("/index/")) {
117
+ return new PermanentRedirectResponse(
118
+ `/${pathname.slice(1, -"index/".length)}${search}`
119
+ );
120
+ } else if (assetEntry = await findAssetEntryForPath(
121
+ `${pathname.replace(/\/$/, ".html")}`
122
+ )) {
123
+ return new PermanentRedirectResponse(
124
+ `/${pathname.slice(1, -1)}${search}`
125
+ );
126
+ } else {
127
+ return notFound();
128
+ }
129
+ }
130
+ if (assetEntry = await findAssetEntryForPath(pathname)) {
131
+ if (pathname.endsWith(".html")) {
132
+ const extensionlessPath = pathname.slice(0, -".html".length);
133
+ if (extensionlessPath.endsWith("/index")) {
134
+ return new PermanentRedirectResponse(
135
+ `${extensionlessPath.replace(/\/index$/, "/")}${search}`
136
+ );
137
+ } else if (await findAssetEntryForPath(extensionlessPath) || extensionlessPath === "/") {
138
+ return serveAsset(assetEntry);
139
+ } else {
140
+ return new PermanentRedirectResponse(`${extensionlessPath}${search}`);
141
+ }
142
+ } else {
143
+ return serveAsset(assetEntry);
144
+ }
145
+ } else if (pathname.endsWith("/index")) {
146
+ return new PermanentRedirectResponse(
147
+ `/${pathname.slice(1, -"index".length)}${search}`
148
+ );
149
+ } else if (assetEntry = await findAssetEntryForPath(`${pathname}.html`)) {
150
+ return serveAsset(assetEntry);
151
+ } else if (hasFileExtension(pathname)) {
152
+ return notFound();
153
+ }
154
+ if (assetEntry = await findAssetEntryForPath(`${pathname}/index.html`)) {
155
+ return new PermanentRedirectResponse(`${pathname}/${search}`);
156
+ } else {
157
+ return notFound();
158
+ }
159
+ }
160
+ async function attachHeaders(response) {
161
+ const existingHeaders = new Headers(response.headers);
162
+ const extraHeaders = new Headers({
163
+ "access-control-allow-origin": "*",
164
+ "referrer-policy": "strict-origin-when-cross-origin",
165
+ ...existingHeaders.has("content-type") ? { "x-content-type-options": "nosniff" } : {}
166
+ });
167
+ const headers = new Headers({
168
+ ...Object.fromEntries(existingHeaders.entries()),
169
+ ...Object.fromEntries(extraHeaders.entries())
170
+ });
171
+ const headersMatcher = generateRulesMatcher(
172
+ headerRules,
173
+ ({ set = {}, unset = [] }, replacements) => {
174
+ const replacedSet = {};
175
+ Object.keys(set).forEach((key) => {
176
+ replacedSet[key] = replacer(set[key], replacements);
177
+ });
178
+ return {
179
+ set: replacedSet,
180
+ unset
181
+ };
182
+ }
183
+ );
184
+ const matches = headersMatcher({ request });
185
+ const setMap = /* @__PURE__ */ new Set();
186
+ matches.forEach(({ set = {}, unset = [] }) => {
187
+ Object.keys(set).forEach((key) => {
188
+ if (setMap.has(key.toLowerCase())) {
189
+ headers.append(key, set[key]);
190
+ } else {
191
+ headers.set(key, set[key]);
192
+ setMap.add(key.toLowerCase());
193
+ }
194
+ });
195
+ unset.forEach((key) => {
196
+ headers.delete(key);
197
+ });
198
+ });
199
+ if (earlyHintsCache) {
200
+ const preEarlyHintsHeaders = new Headers(headers);
201
+ const earlyHintsCacheKey = `${protocol}//${host}${pathname}`;
202
+ const earlyHintsResponse = await earlyHintsCache.match(
203
+ earlyHintsCacheKey
204
+ );
205
+ if (earlyHintsResponse) {
206
+ const earlyHintsLinkHeader = earlyHintsResponse.headers.get("Link");
207
+ if (earlyHintsLinkHeader) {
208
+ headers.set("Link", earlyHintsLinkHeader);
209
+ }
210
+ }
211
+ const clonedResponse = response.clone();
212
+ if (waitUntil) {
213
+ waitUntil(
214
+ (async () => {
215
+ try {
216
+ const links = [];
217
+ const transformedResponse = new HTMLRewriter().on("link[rel=preconnect],link[rel=preload]", {
218
+ element(element) {
219
+ const href = element.getAttribute("href") || void 0;
220
+ const rel = element.getAttribute("rel") || void 0;
221
+ const as = element.getAttribute("as") || void 0;
222
+ if (href && !href.startsWith("data:") && rel) {
223
+ links.push({ href, rel, as });
224
+ }
225
+ }
226
+ }).transform(clonedResponse);
227
+ await transformedResponse.text();
228
+ links.forEach(({ href, rel, as }) => {
229
+ let link = `<${href}>; rel="${rel}"`;
230
+ if (as) {
231
+ link += `; as=${as}`;
232
+ }
233
+ preEarlyHintsHeaders.append("Link", link);
234
+ });
235
+ const linkHeader = preEarlyHintsHeaders.get("Link");
236
+ if (linkHeader) {
237
+ await earlyHintsCache.put(
238
+ earlyHintsCacheKey,
239
+ new Response(null, { headers: { Link: linkHeader } })
240
+ );
241
+ }
242
+ } catch (err) {
243
+ }
244
+ })()
245
+ );
246
+ }
247
+ }
248
+ return new Response(
249
+ [101, 204, 205, 304].includes(response.status) ? null : response.body,
250
+ {
251
+ headers,
252
+ status: response.status,
253
+ statusText: response.statusText
254
+ }
255
+ );
256
+ }
257
+ return await attachHeaders(await generateResponse());
258
+ async function serveAsset(servingAssetEntry, options = { preserve: true }) {
259
+ let content;
260
+ try {
261
+ content = negotiateContent(request, servingAssetEntry);
262
+ } catch (err) {
263
+ return new NotAcceptableResponse();
264
+ }
265
+ const assetKey = getAssetKey(servingAssetEntry, content);
266
+ const etag = `"${assetKey}"`;
267
+ const weakEtag = `W/${etag}`;
268
+ const ifNoneMatch = request.headers.get("if-none-match");
269
+ if (ifNoneMatch === weakEtag || ifNoneMatch === etag) {
270
+ return new NotModifiedResponse();
271
+ }
272
+ try {
273
+ const asset = await fetchAsset(assetKey);
274
+ const headers = {
275
+ etag,
276
+ "content-type": asset.contentType
277
+ };
278
+ let encodeBody = "automatic";
279
+ if (xServerEnvHeader) {
280
+ headers["x-server-env"] = xServerEnvHeader;
281
+ }
282
+ if (content.encoding) {
283
+ encodeBody = "manual";
284
+ headers["cache-control"] = "no-transform";
285
+ headers["content-encoding"] = content.encoding;
286
+ }
287
+ const response = new OkResponse(
288
+ request.method === "HEAD" ? null : asset.body,
289
+ {
290
+ headers,
291
+ encodeBody
292
+ }
293
+ );
294
+ if (isCacheable(request)) {
295
+ response.headers.append("cache-control", CACHE_CONTROL_BROWSER);
296
+ }
297
+ attachAdditionalHeaders(response, content, servingAssetEntry, asset);
298
+ if (isPreview(new URL(request.url))) {
299
+ response.headers.set("x-robots-tag", "noindex");
300
+ }
301
+ if (options.preserve) {
302
+ const preservedResponse = new Response(
303
+ [101, 204, 205, 304].includes(response.status) ? null : response.clone().body,
304
+ response
305
+ );
306
+ preservedResponse.headers.set(
307
+ "cache-control",
308
+ CACHE_CONTROL_PRESERVATION
309
+ );
310
+ preservedResponse.headers.set("x-robots-tag", "noindex");
311
+ if (waitUntil && caches) {
312
+ waitUntil(
313
+ caches.open(ASSET_PRESERVATION_CACHE).then(
314
+ (assetPreservationCache) => assetPreservationCache.put(request.url, preservedResponse)
315
+ ).catch((err) => {
316
+ logError(err);
317
+ })
318
+ );
319
+ }
320
+ }
321
+ if (asset.contentType.startsWith("text/html") && metadata.analytics?.version === ANALYTICS_VERSION) {
322
+ return new HTMLRewriter().on("body", {
323
+ element(e) {
324
+ e.append(
325
+ `<!-- Cloudflare Pages Analytics --><script defer src='https://static.cloudflareinsights.com/beacon.min.js' data-cf-beacon='{"token": "${metadata.analytics?.token}"}'><\/script><!-- Cloudflare Pages Analytics -->`,
326
+ { html: true }
327
+ );
328
+ }
329
+ }).transform(response);
330
+ }
331
+ return response;
332
+ } catch (err) {
333
+ logError(err);
334
+ return new InternalServerErrorResponse(err);
335
+ }
336
+ }
337
+ async function notFound() {
338
+ if (caches) {
339
+ const assetPreservationCache = await caches.open(
340
+ ASSET_PRESERVATION_CACHE
341
+ );
342
+ const preservedResponse = await assetPreservationCache.match(request.url);
343
+ if (preservedResponse) {
344
+ return preservedResponse;
345
+ }
346
+ }
347
+ let cwd = pathname;
348
+ while (cwd) {
349
+ cwd = cwd.slice(0, cwd.lastIndexOf("/"));
350
+ if (assetEntry = await findAssetEntryForPath(`${cwd}/404.html`)) {
351
+ let content;
352
+ try {
353
+ content = negotiateContent(request, assetEntry);
354
+ } catch (err) {
355
+ return new NotAcceptableResponse();
356
+ }
357
+ const assetKey = getAssetKey(assetEntry, content);
358
+ try {
359
+ const { body, contentType } = await fetchAsset(assetKey);
360
+ const response = new NotFoundResponse(body);
361
+ response.headers.set("content-type", contentType);
362
+ return response;
363
+ } catch (err) {
364
+ logError(err);
365
+ return new InternalServerErrorResponse(err);
366
+ }
367
+ }
368
+ }
369
+ return await generateNotFoundResponse(
370
+ request,
371
+ findAssetEntryForPath,
372
+ serveAsset
373
+ );
374
+ }
375
+ }
376
+ export function parseQualityWeightedList(list = "") {
377
+ const items = {};
378
+ list.replace(/\s/g, "").split(",").forEach((el) => {
379
+ const [item, weight] = el.split(";q=");
380
+ items[item] = weight ? parseFloat(weight) : 1;
381
+ });
382
+ return items;
383
+ }
384
+ function isCacheable(request) {
385
+ return !request.headers.has("authorization") && !request.headers.has("range");
386
+ }
387
+ function hasFileExtension(path) {
388
+ return /\/.+\.[a-z0-9]+$/i.test(path);
389
+ }
390
+ function isPreview(url) {
391
+ if (url.hostname.endsWith(".pages.dev")) {
392
+ return url.hostname.split(".").length > 3 ? true : false;
393
+ }
394
+ return false;
395
+ }
File without changes
@@ -0,0 +1,15 @@
1
+ globalThis.URL = function(globalURL) {
2
+ PatchedURL.prototype = globalURL.prototype;
3
+ PatchedURL.createObjectURL = globalURL.createObjectURL;
4
+ PatchedURL.revokeObjectURL = globalURL.revokeObjectURL;
5
+ return PatchedURL;
6
+ function PatchedURL(input, base) {
7
+ const url = new URL(encodeURI(input), base);
8
+ return new Proxy(url, {
9
+ get(target, prop) {
10
+ return globalThis.decodeURIComponent(target[prop]);
11
+ }
12
+ });
13
+ }
14
+ }(URL);
15
+ export {};
@@ -0,0 +1,123 @@
1
+ function mergeHeaders(base, extra) {
2
+ base = new Headers(base ?? {});
3
+ extra = new Headers(extra ?? {});
4
+ return new Headers({
5
+ ...Object.fromEntries(base.entries()),
6
+ ...Object.fromEntries(extra.entries())
7
+ });
8
+ }
9
+ export class OkResponse extends Response {
10
+ constructor(...[body, init]) {
11
+ super(body, {
12
+ ...init,
13
+ status: 200,
14
+ statusText: "OK"
15
+ });
16
+ }
17
+ }
18
+ export class MovedPermanentlyResponse extends Response {
19
+ constructor(location, init) {
20
+ super(`Redirecting to ${location}`, {
21
+ ...init,
22
+ status: 301,
23
+ statusText: "Moved Permanently",
24
+ headers: mergeHeaders(init?.headers, {
25
+ location
26
+ })
27
+ });
28
+ }
29
+ }
30
+ export class FoundResponse extends Response {
31
+ constructor(location, init) {
32
+ super(`Redirecting to ${location}`, {
33
+ ...init,
34
+ status: 302,
35
+ statusText: "Found",
36
+ headers: mergeHeaders(init?.headers, {
37
+ location
38
+ })
39
+ });
40
+ }
41
+ }
42
+ export class NotModifiedResponse extends Response {
43
+ constructor(...[_body, _init]) {
44
+ super(void 0, {
45
+ status: 304,
46
+ statusText: "Not Modified"
47
+ });
48
+ }
49
+ }
50
+ export class PermanentRedirectResponse extends Response {
51
+ constructor(location, init) {
52
+ super(void 0, {
53
+ ...init,
54
+ status: 308,
55
+ statusText: "Permanent Redirect",
56
+ headers: mergeHeaders(init?.headers, {
57
+ location
58
+ })
59
+ });
60
+ }
61
+ }
62
+ export class NotFoundResponse extends Response {
63
+ constructor(...[body, init]) {
64
+ super(body, {
65
+ ...init,
66
+ status: 404,
67
+ statusText: "Not Found"
68
+ });
69
+ }
70
+ }
71
+ export class MethodNotAllowedResponse extends Response {
72
+ constructor(...[body, init]) {
73
+ super(body, {
74
+ ...init,
75
+ status: 405,
76
+ statusText: "Method Not Allowed"
77
+ });
78
+ }
79
+ }
80
+ export class NotAcceptableResponse extends Response {
81
+ constructor(...[body, init]) {
82
+ super(body, {
83
+ ...init,
84
+ status: 406,
85
+ statusText: "Not Acceptable"
86
+ });
87
+ }
88
+ }
89
+ export class InternalServerErrorResponse extends Response {
90
+ constructor(err, init) {
91
+ let body = void 0;
92
+ if (globalThis.DEBUG) {
93
+ body = `${err.message}
94
+
95
+ ${err.stack}`;
96
+ }
97
+ super(body, {
98
+ ...init,
99
+ status: 500,
100
+ statusText: "Internal Server Error"
101
+ });
102
+ }
103
+ }
104
+ export class SeeOtherResponse extends Response {
105
+ constructor(location, init) {
106
+ super(`Redirecting to ${location}`, {
107
+ ...init,
108
+ status: 303,
109
+ statusText: "See Other",
110
+ headers: mergeHeaders(init?.headers, { location })
111
+ });
112
+ }
113
+ }
114
+ export class TemporaryRedirectResponse extends Response {
115
+ constructor(location, init) {
116
+ super(`Redirecting to ${location}`, {
117
+ ...init,
118
+ status: 307,
119
+ statusText: "Temporary Redirect",
120
+ headers: mergeHeaders(init?.headers, { location })
121
+ });
122
+ }
123
+ }
@@ -0,0 +1,44 @@
1
+ const ESCAPE_REGEX_CHARACTERS = /[-/\\^$*+?.()|[\]{}]/g;
2
+ const escapeRegex = (str) => {
3
+ return str.replace(ESCAPE_REGEX_CHARACTERS, "\\$&");
4
+ };
5
+ const HOST_PLACEHOLDER_REGEX = /(?<=^https:\\\/\\\/[^/]*?):([^\\]+)(?=\\)/g;
6
+ const PLACEHOLDER_REGEX = /:(\w+)/g;
7
+ export const replacer = (str, replacements) => {
8
+ for (const [replacement, value] of Object.entries(replacements)) {
9
+ str = str.replaceAll(`:${replacement}`, value);
10
+ }
11
+ return str;
12
+ };
13
+ export const generateRulesMatcher = (rules, replacerFn = (match) => match) => {
14
+ if (!rules)
15
+ return () => [];
16
+ const compiledRules = Object.entries(rules).map(([rule, match]) => {
17
+ const crossHost = rule.startsWith("https://");
18
+ rule = rule.split("*").map(escapeRegex).join("(?<splat>.*)");
19
+ const host_matches = rule.matchAll(HOST_PLACEHOLDER_REGEX);
20
+ for (const host_match of host_matches) {
21
+ rule = rule.split(host_match[0]).join(`(?<${host_match[1]}>[^/.]+)`);
22
+ }
23
+ const path_matches = rule.matchAll(PLACEHOLDER_REGEX);
24
+ for (const path_match of path_matches) {
25
+ rule = rule.split(path_match[0]).join(`(?<${path_match[1]}>[^/]+)`);
26
+ }
27
+ rule = "^" + rule + "$";
28
+ try {
29
+ const regExp = new RegExp(rule);
30
+ return [{ crossHost, regExp }, match];
31
+ } catch {
32
+ }
33
+ }).filter((value) => value !== void 0);
34
+ return ({ request }) => {
35
+ const { pathname, host } = new URL(request.url);
36
+ return compiledRules.map(([{ crossHost, regExp }, match]) => {
37
+ const test = crossHost ? `https://${host}${pathname}` : pathname;
38
+ const result = regExp.exec(test);
39
+ if (result) {
40
+ return replacerFn(match, result.groups || {});
41
+ }
42
+ }).filter((value) => value !== void 0);
43
+ };
44
+ };
@@ -0,0 +1,10 @@
1
+ export const polyfill = (environment) => {
2
+ Object.entries(environment).map(([name, value]) => {
3
+ Object.defineProperty(globalThis, name, {
4
+ value,
5
+ configurable: true,
6
+ enumerable: true,
7
+ writable: true
8
+ });
9
+ });
10
+ };
@@ -0,0 +1,13 @@
1
+ import {
2
+ fetch as miniflareFetch,
3
+ Headers as MiniflareHeaders,
4
+ Request as MiniflareRequest,
5
+ Response as MiniflareResponse
6
+ } from "@miniflare/core";
7
+ import { polyfill } from ".";
8
+ polyfill({
9
+ fetch: miniflareFetch,
10
+ Headers: MiniflareHeaders,
11
+ Request: MiniflareRequest,
12
+ Response: MiniflareResponse
13
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -10,7 +10,7 @@ export function createMetadataObject({
10
10
  headers,
11
11
  webAnalyticsToken,
12
12
  deploymentId,
13
- logger = () => {
13
+ logger = (_message) => {
14
14
  }
15
15
  }) {
16
16
  return {
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "@cloudflare/pages-shared",
3
- "version": "0.0.1",
3
+ "version": "0.0.4",
4
4
  "files": [
5
5
  "tsconfig.json",
6
+ "src/**/*",
6
7
  "dist/**/*"
7
8
  ],
8
9
  "scripts": {
@@ -34,9 +35,15 @@
34
35
  "node_modules/(?!find-up|locate-path|p-locate|p-limit|p-timeout|p-queue|yocto-queue|path-exists|execa|strip-final-newline|npm-run-path|path-key|onetime|mimic-fn|human-signals|is-stream|get-port|supports-color|pretty-bytes)"
35
36
  ]
36
37
  },
38
+ "dependencies": {
39
+ "@miniflare/core": "2.8.1"
40
+ },
37
41
  "devDependencies": {
38
- "@cloudflare/workers-types": "^3.16.0",
42
+ "@miniflare/cache": "2.8.1",
43
+ "@miniflare/html-rewriter": "2.8.1",
44
+ "@types/service-worker-mock": "^2.0.1",
39
45
  "concurrently": "^7.3.0",
40
- "glob": "^8.0.3"
46
+ "glob": "^8.0.3",
47
+ "service-worker-mock": "^2.0.5"
41
48
  }
42
49
  }