diff --git a/.changeset/dull-ants-check.md b/.changeset/dull-ants-check.md new file mode 100644 index 000000000000..e0daf84400a6 --- /dev/null +++ b/.changeset/dull-ants-check.md @@ -0,0 +1,11 @@ +--- +"@cloudflare/workers-shared": minor +--- + +feat: Add basic Asset Worker behaviour + +This commit implements a basic Asset Worker behaviour, including: + +- headers handling +- `200`/`404`/`500` response handling +- fetching data from KV diff --git a/fixtures/workers-with-assets/public/README.md b/fixtures/workers-with-assets/public/README.md new file mode 100644 index 000000000000..a78f2f04a1e0 --- /dev/null +++ b/fixtures/workers-with-assets/public/README.md @@ -0,0 +1,57 @@ +# Workers with Assets + +Welcome to Workers + Assets YAY! + +Please proceed with much excitement ^.^ + + ................. + .......-++**********:. + ..-+****++**********#: + ...-+***+++====+********##= + ....:=****+======+***********##+ + ....-****+======+*************###+ + .:==+*+======+***************####= + ....-===--=====++****************#####: + ....:====-:::-==+******************#####+. + ..-====:::::::+*******************#####*.. + ..-===-:::::::-==+*****************######=.. + ..-===-::::::-=======***************######*. + ...-===-::::::-==========+************######*:. + ...-===-::::::-==----========+*********#######:.. + .-===-::::::--::::::::::-======+******#######-. . + ..:====::::::-=:::-*####*-:::-=======+**#######-. + .:====-:::::-==:::*#########-::========+**#####- + ..====-:::::-===-::###########*:::======*******+.. + ..=+====:::::=====::-############:::====+*******=... + ....:=++====:::-======-::###########*:::===+*******-.. + ...........:-==+**+=++++=============-:::*#########-::===********:.. + ....--=+************====+++===============:::=*####*=:::-=+*******=.. + .:*************###*=====++++================::::::::::-==********:.. + ..*****#############=======++++==========================+*******+... + ...+****#############*==:::-===++++========================********-. + ...+****###############+=::::=====+++++====================********+... + .=****################*=-:::-======+++++=================+********:.. + ..-****##################===:-=========++++++=============+********:.. + .+***####################===========+***#++++++=========+********=.. + .*###################+-.-=======+****##*==+++++++====+********=... + ...-+#############*=...-++=====+****####+=====++++++*********+... + ....-*######+:.....++++===******####==========+*######**+.. + ...+=.....:::-+++++******#####=========+***######*.. + .......::::::=++******#####+========***********:... + ....::::::...+*****######=======+**********##*. + ..:::::....-*****######+=====+**********#####*. + ...:::::...:******#####*+++++*********#########+. + ..:::::...+*****######+++++++=.=###############:. + .:::::..-*****######-..-=++=..-##########***###.. + ....::::..=****######=....::::..:########******##*.. + ...::::..+***######-.....::::...*##***********###=.. + ..::::...-**####*:......::::...###************###-.. + ..:::......=##*:......:::::...+##*************###: + ...::::...............:::::....+##*************####. + ...::::::::::::::..:::::::......**************###-.. + .::::::::::::::::::::::..... ...:***********###+.... + ..::::.......::::::...... ..:********###*:... + ........ ...=******###=.. + .=***###+.. + ..*####:... + ........ diff --git a/fixtures/workers-with-assets/public/lava-lamps.jpg b/fixtures/workers-with-assets/public/lava-lamps.jpg new file mode 100644 index 000000000000..fc66cad30f7d Binary files /dev/null and b/fixtures/workers-with-assets/public/lava-lamps.jpg differ diff --git a/fixtures/workers-with-assets/public/yay.txt b/fixtures/workers-with-assets/public/yay.txt new file mode 100644 index 000000000000..b84f6bc7c6bb --- /dev/null +++ b/fixtures/workers-with-assets/public/yay.txt @@ -0,0 +1,12 @@ + + .----------------. .----------------. .----------------. +| .--------------. || .--------------. || .--------------. | +| | ____ ____ | || | __ | || | ____ ____ | | +| | |_ _||_ _| | || | / \ | || | |_ _||_ _| | | +| | \ \ / / | || | / /\ \ | || | \ \ / / | | +| | \ \/ / | || | / ____ \ | || | \ \/ / | | +| | _| |_ | || | _/ / \ \_ | || | _| |_ | | +| | |______| | || ||____| |____|| || | |______| | | +| | | || | | || | | | +| '--------------' || '--------------' || '--------------' | + '----------------' '----------------' '----------------' diff --git a/fixtures/workers-with-assets/tests/index.test.ts b/fixtures/workers-with-assets/tests/index.test.ts index 1c2708bdbb70..f2ee9347a5aa 100644 --- a/fixtures/workers-with-assets/tests/index.test.ts +++ b/fixtures/workers-with-assets/tests/index.test.ts @@ -31,8 +31,86 @@ describe("[Workers + Assets] `wrangler dev`", () => { it("should not resolve '/' to '/index.html' ", async ({ expect }) => { let response = await fetch(`http://${ip}:${port}/`); - let text = await response.text(); expect(response.status).toBe(404); - expect(text).toContain("Not Found"); + }); + + it("should 404 if asset is not found in the asset manifest", async ({ + expect, + }) => { + let response = await fetch(`http://${ip}:${port}/hello.html`); + expect(response.status).toBe(404); + + response = await fetch(`http://${ip}:${port}/hello.txt`); + expect(response.status).toBe(404); + }); + + it("should handle content types correctly", async ({ expect }) => { + let response = await fetch(`http://${ip}:${port}/index.html`); + let text = await response.text(); + expect(response.status).toBe(200); + expect(response.headers.get("Content-Type")).toBe( + "text/html; charset=utf-8" + ); + + response = await fetch(`http://${ip}:${port}/README.md`); + text = await response.text(); + expect(response.status).toBe(200); + expect(response.headers.get("Content-Type")).toBe( + "text/markdown; charset=utf-8" + ); + expect(text).toContain(`Welcome to Workers + Assets YAY!`); + + response = await fetch(`http://${ip}:${port}/yay.txt`); + text = await response.text(); + expect(response.status).toBe(200); + expect(response.headers.get("Content-Type")).toBe( + "text/plain; charset=utf-8" + ); + expect(text).toContain(`.----------------.`); + + response = await fetch(`http://${ip}:${port}/lava-lamps.jpg`); + expect(response.status).toBe(200); + expect(response.headers.get("Content-Type")).toBe("image/jpeg"); + }); + + it("should only ever handle GET requests", async ({ expect }) => { + // as per https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods + // excl. TRACE and CONNECT which are not supported + + let response = await fetch(`http://${ip}:${port}/hello.html`, { + method: "HEAD", + }); + expect(response.status).toBe(405); + expect(response.statusText).toBe("Method Not Allowed"); + + response = await fetch(`http://${ip}:${port}/hello.html`, { + method: "POST", + }); + expect(response.status).toBe(405); + expect(response.statusText).toBe("Method Not Allowed"); + + response = await fetch(`http://${ip}:${port}/hello.html`, { + method: "PUT", + }); + expect(response.status).toBe(405); + expect(response.statusText).toBe("Method Not Allowed"); + + response = await fetch(`http://${ip}:${port}/hello.html`, { + method: "DELETE", + }); + expect(response.status).toBe(405); + expect(response.statusText).toBe("Method Not Allowed"); + + response = await fetch(`http://${ip}:${port}/hello.html`, { + method: "OPTIONS", + }); + expect(response.status).toBe(405); + expect(response.statusText).toBe("Method Not Allowed"); + + response = await fetch(`http://${ip}:${port}/hello.html`, { + method: "PATCH", + }); + expect(response.status).toBe(405); + expect(response.statusText).toBe("Method Not Allowed"); }); }); diff --git a/packages/miniflare/src/workers/kv/assets.worker.ts b/packages/miniflare/src/workers/kv/assets.worker.ts index b44d471024df..a8930f04e6a0 100644 --- a/packages/miniflare/src/workers/kv/assets.worker.ts +++ b/packages/miniflare/src/workers/kv/assets.worker.ts @@ -27,6 +27,15 @@ export default >{ // eslint-disable-next-line @typescript-eslint/no-unused-vars const { filePath, contentType } = entry; const blobsService = env[SharedBindings.MAYBE_SERVICE_BLOBS]; - return blobsService.fetch(new URL(filePath, "http://placeholder")); + const response = await blobsService.fetch( + new URL(filePath, "http://placeholder") + ); + const newResponse = new Response(response.body, response); + // ensure the runtime will return the metadata we need + newResponse.headers.append( + "cf-kv-metadata", + `{"contentType": "${contentType}"}` + ); + return newResponse; }, }; diff --git a/packages/workers-shared/asset-server-worker/src/assets-manifest.ts b/packages/workers-shared/asset-server-worker/src/assets-manifest.ts index ef2645bd84f2..ece502136999 100644 --- a/packages/workers-shared/asset-server-worker/src/assets-manifest.ts +++ b/packages/workers-shared/asset-server-worker/src/assets-manifest.ts @@ -4,6 +4,11 @@ const CONTENT_HASH_SIZE = 16; const TAIL_SIZE = 8; const ENTRY_SIZE = PATH_HASH_SIZE + CONTENT_HASH_SIZE + TAIL_SIZE; +export type AssetEntry = { + path: string; + contentHash: string; +}; + export class AssetsManifest { private data: ArrayBuffer; diff --git a/packages/workers-shared/asset-server-worker/src/constants.ts b/packages/workers-shared/asset-server-worker/src/constants.ts new file mode 100644 index 000000000000..4b3b135af1d1 --- /dev/null +++ b/packages/workers-shared/asset-server-worker/src/constants.ts @@ -0,0 +1,2 @@ +// have the browser check in with the server to make sure its local cache is valid before using it +export const CACHE_CONTROL_BROWSER = "public, max-age=0, must-revalidate"; diff --git a/packages/workers-shared/asset-server-worker/src/global.d.ts b/packages/workers-shared/asset-server-worker/src/global.d.ts new file mode 100644 index 000000000000..56e67727f369 --- /dev/null +++ b/packages/workers-shared/asset-server-worker/src/global.d.ts @@ -0,0 +1,9 @@ +type Env = { + // ASSETS_MANIFEST is a pipeline binding to an ArrayBuffer containing the + // binary-encoded site manifest + ASSETS_MANIFEST: ArrayBuffer; + + // ASSETS_KV_NAMESPACE is a pipeline binding to the KV namespace that the + // assets are in. + ASSETS_KV_NAMESPACE: KVNamespace; +}; diff --git a/packages/workers-shared/asset-server-worker/src/index.ts b/packages/workers-shared/asset-server-worker/src/index.ts index de06bf5caf9c..e43c4f08dd4c 100644 --- a/packages/workers-shared/asset-server-worker/src/index.ts +++ b/packages/workers-shared/asset-server-worker/src/index.ts @@ -1,38 +1,62 @@ +import { WorkerEntrypoint } from "cloudflare:workers"; import { AssetsManifest } from "./assets-manifest"; +import { + InternalServerErrorResponse, + MethodNotAllowedResponse, + NotFoundResponse, + OkResponse, +} from "./responses"; +import { getAdditionalHeaders, getMergedHeaders } from "./utils/headers"; +import { getAssetWithMetadataFromKV } from "./utils/kv"; -interface Env { - /** - * ASSETS_MANIFEST is a pipeline binding to an ArrayBuffer containing the - * binary-encoded site manifest - */ - ASSETS_MANIFEST: ArrayBuffer; - /** - * ASSETS_KV_NAMESPACE is a pipeline binding to the KV namespace that the - * assets are in. - */ - ASSETS_KV_NAMESPACE: KVNamespace; -} - -export default { - async fetch(request: Request, env: Env) { - const { ASSETS_MANIFEST, ASSETS_KV_NAMESPACE } = env; +export default class extends WorkerEntrypoint { + async fetch(request: Request) { + if (request.method.toLowerCase() !== "get") { + return new MethodNotAllowedResponse(); + } - const url = new URL(request.url); - const { pathname } = url; + try { + return this.handleRequest(request); + } catch (err) { + return new InternalServerErrorResponse(err); + } + } - const assetsManifest = new AssetsManifest(ASSETS_MANIFEST); - const assetKey = await assetsManifest.get(pathname); - if (!assetKey) { - return new Response("Not Found", { status: 404 }); + async handleRequest(request: Request) { + const assetEntry = await this.getAssetEntry(request); + if (!assetEntry) { + return new NotFoundResponse(); } - const content = await ASSETS_KV_NAMESPACE.get(assetKey); - if (!content) { + const assetResponse = await getAssetWithMetadataFromKV( + this.env.ASSETS_KV_NAMESPACE, + assetEntry + ); + + if (!assetResponse || !assetResponse.value) { throw new Error( - `Requested asset ${assetKey} exists in the asset manifest but not in the KV namespace.` + `Requested asset ${assetEntry} exists in the asset manifest but not in the KV namespace.` ); } - return new Response(content); - }, -}; + const { value: assetContent, metadata: assetMetadata } = assetResponse; + const additionalHeaders = getAdditionalHeaders( + assetEntry, + assetMetadata, + request + ); + const headers = getMergedHeaders(request.headers, additionalHeaders); + + return new OkResponse(assetContent, { headers }); + } + + private async getAssetEntry(request: Request) { + const url = new URL(request.url); + let { pathname } = url; + + const assetsManifest = new AssetsManifest(this.env.ASSETS_MANIFEST); + pathname = globalThis.decodeURIComponent(pathname); + + return await assetsManifest.get(pathname); + } +} diff --git a/packages/workers-shared/asset-server-worker/src/responses.ts b/packages/workers-shared/asset-server-worker/src/responses.ts new file mode 100644 index 000000000000..7db1ff993014 --- /dev/null +++ b/packages/workers-shared/asset-server-worker/src/responses.ts @@ -0,0 +1,46 @@ +export class OkResponse extends Response { + constructor(body: BodyInit | null, init?: ResponseInit) { + super(body, { + ...init, + status: 200, + }); + } +} + +export class NotFoundResponse extends Response { + constructor(...[body, init]: ConstructorParameters) { + super(body, { + ...init, + status: 404, + statusText: "Not Found", + }); + } +} + +export class MethodNotAllowedResponse extends Response { + constructor(...[body, init]: ConstructorParameters) { + super(body, { + ...init, + status: 405, + statusText: "Method Not Allowed", + }); + } +} + +export class InternalServerErrorResponse extends Response { + constructor(err: Error, init?: ResponseInit) { + super(undefined, { + ...init, + status: 500, + }); + } +} + +export class NotModifiedResponse extends Response { + constructor(...[_body, _init]: ConstructorParameters) { + super(undefined, { + status: 304, + statusText: "Not Modified", + }); + } +} diff --git a/packages/workers-shared/asset-server-worker/src/utils/headers.ts b/packages/workers-shared/asset-server-worker/src/utils/headers.ts new file mode 100644 index 000000000000..a34c659e9577 --- /dev/null +++ b/packages/workers-shared/asset-server-worker/src/utils/headers.ts @@ -0,0 +1,56 @@ +import { CACHE_CONTROL_BROWSER } from "../constants"; +import type { AssetMetadata } from "./kv"; + +/** + * Returns a Headers object that is the union of `existingHeaders` + * and `additionalHeaders`. Headers specified by `additionalHeaders` + * will override those specified by `existingHeaders`. + * + */ +export function getMergedHeaders( + existingHeaders: Headers, + additionalHeaders: Headers +) { + const mergedHeaders = new Headers(existingHeaders); + for (const [key, value] of additionalHeaders) { + // override existing headers + mergedHeaders.set(key, value); + } + + return mergedHeaders; +} + +/** + * Returns a Headers object that contains additional headers (to those + * present in the original request) that the Assets Server Worker + * should attach to its response. + * + */ +export function getAdditionalHeaders( + assetKey: string, + assetMetadata: AssetMetadata | null, + request: Request +) { + let contentType = assetMetadata?.contentType ?? "application/octet-stream"; + if (contentType.startsWith("text/") && !contentType.includes("charset")) { + contentType = `${contentType}; charset=utf-8`; + } + + const headers = new Headers({ + "Access-Control-Allow-Origin": "*", + "Content-Type": contentType, + "Referrer-Policy": "strict-origin-when-cross-origin", + "X-Content-Type-Options": "nosniff", + ETag: `${assetKey}`, + }); + + if (isCacheable(request)) { + headers.append("Cache-Control", CACHE_CONTROL_BROWSER); + } + + return headers; +} + +function isCacheable(request: Request) { + return !request.headers.has("authorization") && !request.headers.has("range"); +} diff --git a/packages/workers-shared/asset-server-worker/src/utils/kv.ts b/packages/workers-shared/asset-server-worker/src/utils/kv.ts new file mode 100644 index 000000000000..31142606b1a8 --- /dev/null +++ b/packages/workers-shared/asset-server-worker/src/utils/kv.ts @@ -0,0 +1,31 @@ +export type AssetMetadata = { + contentType: string; +}; + +export async function getAssetWithMetadataFromKV( + assetsKVNamespace: KVNamespace, + assetKey: string, + retries = 1 +) { + let attempts = 0; + + while (attempts <= retries) { + try { + return await assetsKVNamespace.getWithMetadata(assetKey, { + type: "stream", + cacheTtl: 31536000, // 1 year + }); + } catch (err) { + if (attempts >= retries) { + throw new Error( + `Requested asset ${assetKey} could not be fetched from KV namespace.` + ); + } + + // Exponential backoff, 1 second first time, then 2 second, then 4 second etc. + await new Promise((resolvePromise) => + setTimeout(resolvePromise, Math.pow(2, attempts++) * 1000) + ); + } + } +} diff --git a/packages/workers-shared/asset-server-worker/tests/headers.test.ts b/packages/workers-shared/asset-server-worker/tests/headers.test.ts new file mode 100644 index 000000000000..282f8fba77be --- /dev/null +++ b/packages/workers-shared/asset-server-worker/tests/headers.test.ts @@ -0,0 +1,143 @@ +import { getAdditionalHeaders, getMergedHeaders } from "../src/utils/headers"; +import type { AssetMetadata } from "../src/utils/kv"; + +describe("[Asset Worker] Response Headers", () => { + describe("getMergedHeaders()", () => { + it("should merge headers with override", () => { + const existingHeaders = new Headers({ + "Accept-Encoding": "gzip", + "Cache-Control": "max-age=180, public", + "Content-Type": "text/html; charset=utf-8", + }); + + const additionalHeaders = new Headers({ + "Accept-Encoding": "*", + "Content-Type": "text/javascript; charset=utf-8", + "Keep-Alive": "timeout=5, max=1000", + }); + + const mergedHeaders = getMergedHeaders( + existingHeaders, + additionalHeaders + ); + expect(mergedHeaders).toEqual( + new Headers({ + "Accept-Encoding": "*", + "Cache-Control": "max-age=180, public", + "Content-Type": "text/javascript; charset=utf-8", + "Keep-Alive": "timeout=5, max=1000", + }) + ); + }); + }); + + describe("getAdditionalHeaders()", () => { + it("should return the default headers the Asset Worker should set on every response", () => { + const request = new Request("https://example.com", { + method: "GET", + headers: { + "Accept-Encoding": "*", + }, + }); + const assetMetadata: AssetMetadata = { + contentType: "text/html; charset=utf-8", + }; + const additionalHeaders = getAdditionalHeaders( + "33a64df551425fcc55e4d42a148795d9f25f89d4", + assetMetadata, + request + ); + + expect(additionalHeaders).toEqual( + new Headers({ + "Access-Control-Allow-Origin": "*", + "Content-Type": "text/html; charset=utf-8", + "Referrer-Policy": "strict-origin-when-cross-origin", + "X-Content-Type-Options": "nosniff", + ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4", + "Cache-Control": "public, max-age=0, must-revalidate", + }) + ); + }); + + it("should default 'Content-Type' to 'application/octet-stream' if not specified by asset metadata", () => { + const request = new Request("https://example.com", { + method: "GET", + headers: { + "Accept-Encoding": "*", + }, + }); + const additionalHeaders = getAdditionalHeaders( + "33a64df551425fcc55e4d42a148795d9f25f89d4", + null, + request + ); + + expect(additionalHeaders).toEqual( + new Headers({ + "Access-Control-Allow-Origin": "*", + "Content-Type": "application/octet-stream", + "Referrer-Policy": "strict-origin-when-cross-origin", + "X-Content-Type-Options": "nosniff", + ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4", + "Cache-Control": "public, max-age=0, must-revalidate", + }) + ); + }); + + it("should set the 'charset' to 'utf-8' when appropriate, if not specified", () => { + const request = new Request("https://example.com", { + method: "GET", + headers: { + "Accept-Encoding": "*", + }, + }); + const assetMetadata: AssetMetadata = { contentType: "text/html" }; + const additionalHeaders = getAdditionalHeaders( + "33a64df551425fcc55e4d42a148795d9f25f89d4", + assetMetadata, + request + ); + + expect(additionalHeaders).toEqual( + new Headers({ + "Access-Control-Allow-Origin": "*", + "Content-Type": "text/html; charset=utf-8", + "Referrer-Policy": "strict-origin-when-cross-origin", + "X-Content-Type-Options": "nosniff", + ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4", + "Cache-Control": "public, max-age=0, must-revalidate", + }) + ); + }); + + it("should not set the 'Cache-Control' header, if 'Authorization' and 'Range' headers are present in the request", () => { + const request = new Request("https://example.com", { + method: "GET", + headers: { + "Accept-Encoding": "*", + Authorization: "Basic 123", + Range: "bytes=0-499", + }, + }); + const assetMetadata: AssetMetadata = { + contentType: "text/html; charset=utf-8", + }; + const additionalHeaders = getAdditionalHeaders( + "33a64df551425fcc55e4d42a148795d9f25f89d4", + assetMetadata, + request + ); + + expect(additionalHeaders).toEqual( + new Headers({ + "Access-Control-Allow-Origin": "*", + "Content-Type": "text/html; charset=utf-8", + "Referrer-Policy": "strict-origin-when-cross-origin", + "X-Content-Type-Options": "nosniff", + ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4", + }) + ); + }); + }); +}); diff --git a/packages/workers-shared/asset-server-worker/tests/kv.test.ts b/packages/workers-shared/asset-server-worker/tests/kv.test.ts new file mode 100644 index 000000000000..1435b1c390c0 --- /dev/null +++ b/packages/workers-shared/asset-server-worker/tests/kv.test.ts @@ -0,0 +1,75 @@ +import { getAssetWithMetadataFromKV } from "../src/utils/kv"; +import type { AssetMetadata } from "../src/utils/kv"; +import type { MockInstance } from "vitest"; + +describe("[Asset Worker] Fetching assets from KV", () => { + describe("getAssetWithMetadataFromKV()", () => { + let mockKVNamespace: KVNamespace; + let spy: MockInstance; + + beforeEach(() => { + mockKVNamespace = { + getWithMetadata: () => Promise.resolve(), + } as unknown as KVNamespace; + + spy = vi.spyOn(mockKVNamespace, "getWithMetadata"); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("should return the asset value and metadata, if asset was found in the KV store", async () => { + spy.mockReturnValueOnce( + Promise.resolve({ + value: "Hello world", + metadata: { + contentType: "text/html", + }, + }) as unknown as Promise< + KVNamespaceGetWithMetadataResult + > + ); + + const asset = await getAssetWithMetadataFromKV(mockKVNamespace, "abcd"); + expect(asset).toBeDefined(); + expect(asset?.value).toEqual("Hello world"); + expect(asset?.metadata).toEqual({ + contentType: "text/html", + }); + expect(spy).toHaveBeenCalledOnce(); + }); + + it("should throw an error if something went wrong while fetching the asset", async () => { + spy.mockReturnValue(Promise.reject("Oeps! Something went wrong")); + + await expect(() => + getAssetWithMetadataFromKV(mockKVNamespace, "abcd") + ).rejects.toThrowError( + "Requested asset abcd could not be fetched from KV namespace." + ); + }); + + it("should retry once by default if something went wrong while fetching the asset", async () => { + spy.mockReturnValue(Promise.reject("Oeps! Something went wrong")); + + await expect(() => + getAssetWithMetadataFromKV(mockKVNamespace, "abcd") + ).rejects.toThrowError( + "Requested asset abcd could not be fetched from KV namespace." + ); + expect(spy).toHaveBeenCalledTimes(2); + }); + + it("should support custom number of retries", async () => { + spy.mockReturnValue(Promise.reject("Oeps! Something went wrong")); + + await expect(() => + getAssetWithMetadataFromKV(mockKVNamespace, "abcd", 2) + ).rejects.toThrowError( + "Requested asset abcd could not be fetched from KV namespace." + ); + expect(spy).toHaveBeenCalledTimes(3); + }); + }); +}); diff --git a/packages/workers-shared/asset-server-worker/tests/tsconfig.json b/packages/workers-shared/asset-server-worker/tests/tsconfig.json new file mode 100644 index 000000000000..ff9b901a0662 --- /dev/null +++ b/packages/workers-shared/asset-server-worker/tests/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@cloudflare/workers-tsconfig/tsconfig.json", + "compilerOptions": { + "types": ["@cloudflare/workers-types/experimental", "vitest/globals"] + }, + "include": ["**/*.ts"] +} diff --git a/packages/workers-shared/asset-server-worker/wrangler.toml b/packages/workers-shared/asset-server-worker/wrangler.toml index 5c312ad0e107..e18ef43cc683 100644 --- a/packages/workers-shared/asset-server-worker/wrangler.toml +++ b/packages/workers-shared/asset-server-worker/wrangler.toml @@ -7,6 +7,6 @@ # to this file are persisted in wrangler as well, when necessary. # (see packages/wrangler/src/dev/miniflare.ts -> buildMiniflareOptions()) ## -name = "asset-server" +name = "asset-server-worker" main = "src/index.ts" compatibility_date = "2024-07-31" diff --git a/packages/workers-shared/package.json b/packages/workers-shared/package.json index 68ef808b5eff..2d17d003579e 100644 --- a/packages/workers-shared/package.json +++ b/packages/workers-shared/package.json @@ -22,15 +22,18 @@ "dist" ], "scripts": { - "build": "pnpm run clean && pnpm run bundle:asset-server:prod && pnpm run bundle:router:prod", - "bundle:asset-server": "esbuild asset-server-worker/src/index.ts --format=esm --bundle --outfile=dist/asset-server-worker.mjs --sourcemap=external", - "bundle:asset-server:prod": "pnpm run bundle:asset-server --minify", - "bundle:router": "esbuild router-worker/src/index.ts --format=esm --bundle --outfile=dist/router-worker.mjs --sourcemap=external", - "bundle:router:prod": "pnpm run bundle:router --minify", + "build": "pnpm run clean && pnpm run bundle:asset-worker:prod && pnpm run bundle:router-worker:prod", + "bundle:asset-worker": "esbuild asset-server-worker/src/index.ts --format=esm --bundle --outfile=dist/asset-server-worker.mjs --sourcemap=external --external:cloudflare:*", + "bundle:asset-worker:prod": "pnpm run bundle:asset-worker --minify", + "bundle:router-worker": "esbuild router-worker/src/index.ts --format=esm --bundle --outfile=dist/router-worker.mjs --sourcemap=external", + "bundle:router-worker:prod": "pnpm run bundle:router-worker --minify", "check:lint": "eslint . --max-warnings=0", - "check:type": "tsc", + "check:type": "pnpm run check:type:tests && tsc", + "check:type:tests": "tsc -p ./asset-server-worker/tests/tsconfig.json", "clean": "rimraf dist", - "dev": "pnpm run clean && concurrently -n bundle:asset-server,bundle:router -c blue,magenta \"pnpm run bundle:asset-server --watch\" \"pnpm run bundle:router --watch\"" + "dev": "pnpm run clean && concurrently -n bundle:asset-worker,bundle:router-worker -c blue,magenta \"pnpm run bundle:asset-worker --watch\" \"pnpm run bundle:router-worker --watch\"", + "test": "vitest", + "test:ci": "pnpm run test" }, "devDependencies": { "@cloudflare/eslint-config-worker": "workspace:*", @@ -39,7 +42,8 @@ "concurrently": "^8.2.2", "esbuild": "0.17.19", "rimraf": "^6.0.1", - "typescript": "^5.5.4" + "typescript": "^5.5.4", + "vitest": "2.0.5" }, "engines": { "node": ">=16.7.0" diff --git a/packages/workers-shared/tsconfig.json b/packages/workers-shared/tsconfig.json index f02acfbe9ca4..e112be387120 100644 --- a/packages/workers-shared/tsconfig.json +++ b/packages/workers-shared/tsconfig.json @@ -5,8 +5,8 @@ "sourceMap": true, "forceConsistentCasingInFileNames": true, "useUnknownInCatchVariables": false, - "types": ["@cloudflare/workers-types"] + "types": ["@cloudflare/workers-types/experimental"] }, - "include": ["**/*.ts"], - "exclude": ["node_modules", "dist"] + "include": ["**/*.ts", "vitest.config.mts"], + "exclude": ["node_modules", "dist", "**/tests", "**/*.test.ts"] } diff --git a/packages/workers-shared/vitest.config.mts b/packages/workers-shared/vitest.config.mts new file mode 100644 index 000000000000..93aa07a68a67 --- /dev/null +++ b/packages/workers-shared/vitest.config.mts @@ -0,0 +1,12 @@ +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + test: { + include: ["asset-server-worker/tests/**.{test,spec}.{ts,js}"], + globals: true, + }, + }) +); diff --git a/packages/wrangler/src/dev/miniflare.ts b/packages/wrangler/src/dev/miniflare.ts index 8a4b95c9e5c0..cd8ea270b710 100644 --- a/packages/wrangler/src/dev/miniflare.ts +++ b/packages/wrangler/src/dev/miniflare.ts @@ -412,7 +412,9 @@ export function buildMiniflareBindingOptions(config: MiniflareBindingsConfig): { // Setup service bindings to external services const serviceBindings: NonNullable = { ...config.serviceBindings, - ...(config.experimentalAssets ? { ASSET_SERVER: "asset-server" } : {}), + ...(config.experimentalAssets + ? { ASSET_SERVER: "asset-server-worker" } + : {}), }; const notFoundServices = new Set(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0a0088788194..36e9b1441bdd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1532,6 +1532,9 @@ importers: typescript: specifier: ^5.5.4 version: 5.5.4 + vitest: + specifier: 2.0.5 + version: 2.0.5(@types/node@20.12.12)(@vitest/ui@1.6.0(vitest@1.6.0)) packages/workers-tsconfig: {} @@ -1934,6 +1937,10 @@ packages: resolution: {integrity: sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==} engines: {node: '>=6.0.0'} + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + '@ava/typescript@4.1.0': resolution: {integrity: sha512-1iWZQ/nr9iflhLK9VN8H+1oDZqe93qxNnyYUz+jTzkYPAHc5fdZXBrqmNIgIfFhWYXK5OaQ5YtC7OmLeTNhVEg==} engines: {node: ^14.19 || ^16.15 || ^18 || ^20} @@ -3009,10 +3016,6 @@ packages: resolution: {integrity: sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==} engines: {node: '>=6.0.0'} - '@jridgewell/gen-mapping@0.3.2': - resolution: {integrity: sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==} - engines: {node: '>=6.0.0'} - '@jridgewell/gen-mapping@0.3.5': resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} engines: {node: '>=6.0.0'} @@ -3021,10 +3024,6 @@ packages: resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==} engines: {node: '>=6.0.0'} - '@jridgewell/set-array@1.1.0': - resolution: {integrity: sha512-SfJxIxNVYLTsKwzB3MoOQ1yxf4w/E6MdkvTgrgAt1bfxjSrLUoHMKrDOykwN14q65waezZIdqDneUIPh4/sKxg==} - engines: {node: '>=6.0.0'} - '@jridgewell/set-array@1.2.1': resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} engines: {node: '>=6.0.0'} @@ -3035,9 +3034,6 @@ packages: '@jridgewell/sourcemap-codec@1.5.0': resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} - '@jridgewell/trace-mapping@0.3.19': - resolution: {integrity: sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==} - '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} @@ -3880,24 +3876,39 @@ packages: '@vitest/expect@1.6.0': resolution: {integrity: sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==} + '@vitest/expect@2.0.5': + resolution: {integrity: sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA==} + + '@vitest/pretty-format@2.0.5': + resolution: {integrity: sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ==} + '@vitest/runner@1.5.0': resolution: {integrity: sha512-7HWwdxXP5yDoe7DTpbif9l6ZmDwCzcSIK38kTSIt6CFEpMjX4EpCgT6wUmS0xTXqMI6E/ONmfgRKmaujpabjZQ==} '@vitest/runner@1.6.0': resolution: {integrity: sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg==} + '@vitest/runner@2.0.5': + resolution: {integrity: sha512-TfRfZa6Bkk9ky4tW0z20WKXFEwwvWhRY+84CnSEtq4+3ZvDlJyY32oNTJtM7AW9ihW90tX/1Q78cb6FjoAs+ig==} + '@vitest/snapshot@1.5.0': resolution: {integrity: sha512-qpv3fSEuNrhAO3FpH6YYRdaECnnRjg9VxbhdtPwPRnzSfHVXnNzzrpX4cJxqiwgRMo7uRMWDFBlsBq4Cr+rO3A==} '@vitest/snapshot@1.6.0': resolution: {integrity: sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ==} + '@vitest/snapshot@2.0.5': + resolution: {integrity: sha512-SgCPUeDFLaM0mIUHfaArq8fD2WbaXG/zVXjRupthYfYGzc8ztbFbu6dUNOblBG7XLMR1kEhS/DNnfCZ2IhdDew==} + '@vitest/spy@1.5.0': resolution: {integrity: sha512-vu6vi6ew5N5MMHJjD5PoakMRKYdmIrNJmyfkhRpQt5d9Ewhw9nZ5Aqynbi3N61bvk9UvZ5UysMT6ayIrZ8GA9w==} '@vitest/spy@1.6.0': resolution: {integrity: sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw==} + '@vitest/spy@2.0.5': + resolution: {integrity: sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA==} + '@vitest/ui@1.6.0': resolution: {integrity: sha512-k3Lyo+ONLOgylctiGovRKy7V4+dIN2yxstX3eY5cWFXH6WP+ooVX79YSyi0GagdTQzLmT43BF27T0s6dOIPBXA==} peerDependencies: @@ -3909,6 +3920,9 @@ packages: '@vitest/utils@1.6.0': resolution: {integrity: sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==} + '@vitest/utils@2.0.5': + resolution: {integrity: sha512-d8HKbqIcya+GR67mkZbrzhS5kKhtp8dQLcmRZLGTscGVg7yImT82cIrhtn2L8+VujWcy6KZweApgNmPsTAO/UQ==} + '@volar/language-core@2.3.4': resolution: {integrity: sha512-wXBhY11qG6pCDAqDnbBRFIDSIwbqkWI7no+lj5+L7IlA7HRIjRP7YQLGzT0LF4lS6eHkMSsclXqy9DwYJasZTQ==} @@ -4177,6 +4191,10 @@ packages: assertion-error@1.1.0: resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + ast-types@0.13.4: resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} engines: {node: '>=4'} @@ -4405,6 +4423,10 @@ packages: resolution: {integrity: sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==} engines: {node: '>=4'} + chai@5.1.1: + resolution: {integrity: sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==} + engines: {node: '>=12'} + chainsaw@0.1.0: resolution: {integrity: sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==} @@ -4437,6 +4459,10 @@ packages: check-error@1.0.3: resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} + check-error@2.1.1: + resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} + engines: {node: '>= 16'} + chokidar@3.5.3: resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} engines: {node: '>= 8.10.0'} @@ -4823,6 +4849,10 @@ packages: resolution: {integrity: sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==} engines: {node: '>=6'} + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + deep-extend@0.6.0: resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} engines: {node: '>=4.0.0'} @@ -6381,6 +6411,9 @@ packages: loupe@2.3.7: resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} + loupe@3.1.1: + resolution: {integrity: sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==} + lower-case-first@1.0.2: resolution: {integrity: sha512-UuxaYakO7XeONbKrZf5FEgkantPf5DUqDayzP5VXZrtRPdH86s4kN47I8B3TW10S4QKiE3ziHNf3kRN//okHjA==} @@ -7077,6 +7110,10 @@ packages: pathval@1.1.1: resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} + pathval@2.0.0: + resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} + engines: {node: '>= 14.16'} + performance-now@2.1.0: resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} @@ -8058,6 +8095,9 @@ packages: tinybench@2.6.0: resolution: {integrity: sha512-N8hW3PG/3aOoZAN5V/NSAEDz0ZixDSSt5b/a05iqtpgfLWMSVuCo7w0k2vVvEjdrIoeGqZzweX2WlyioNIHchA==} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + tinycolor2@1.6.0: resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} @@ -8068,10 +8108,22 @@ packages: resolution: {integrity: sha512-Ud7uepAklqRH1bvwy22ynrliC7Dljz7Tm8M/0RBUW+YRa4YHhZ6e4PpgE+fu1zr/WqB1kbeuVrdfeuyIBpy4tw==} engines: {node: '>=14.0.0'} + tinypool@1.0.1: + resolution: {integrity: sha512-URZYihUbRPcGv95En+sz6MfghfIc2OJ1sv/RmhWZLouPY0/8Vo80viwPvg3dlaS9fuq7fQMEfgRRK7BBZThBEA==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@1.2.0: + resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} + engines: {node: '>=14.0.0'} + tinyspy@2.2.0: resolution: {integrity: sha512-d2eda04AN/cPOR89F7Xv5bK/jrQEhmcLFe6HFldoeO9AJtps+fqEnh486vnT/8y4bw38pSyxDcTCAq+Ks2aJTg==} engines: {node: '>=14.0.0'} + tinyspy@3.0.0: + resolution: {integrity: sha512-q5nmENpTHgiPVd1cJDDc9cVoYN5x4vCvwT3FMilvKPKneCBZAxn2YWQjDF0UMcE9k0Cay1gBiDfTMU0g+mPMQA==} + engines: {node: '>=14.0.0'} + title-case@2.1.1: resolution: {integrity: sha512-EkJoZ2O3zdCz3zJsYCsxyq2OC5hrxR9mfdd5I+w8h/tmFfeOxJ+vvkxsKxdmN0WtS9zLdHEgfgVOiMVgv+Po4Q==} @@ -8462,6 +8514,11 @@ packages: engines: {node: ^18.0.0 || >=20.0.0} hasBin: true + vite-node@2.0.5: + resolution: {integrity: sha512-LdsW4pxj0Ot69FAoXZ1yTnA9bjGohr2yNBU7QKRxpz8ITSkhuDl6h3zS/tvgz4qrNjeRnvrWeXQ8ZF7Um4W00Q==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + vite-plugin-dts@4.0.1: resolution: {integrity: sha512-JFbAKMjJdJbeXJVwQNoi8M26lP+5Ene4/ryv9w0Z7Ca5N0DdxYEak9V3C0tqwHO7WZ9JLbwMsuUZOqYIyBRwSQ==} engines: {node: ^14.18.0 || >=16.0.0} @@ -8570,6 +8627,31 @@ packages: jsdom: optional: true + vitest@2.0.5: + resolution: {integrity: sha512-8GUxONfauuIdeSl5f9GTgVEpg5BTOlplET4WEDaeY2QBiN8wSm68vxN/tb5z405OwppfoCavnwXafiaYBC/xOA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 2.0.5 + '@vitest/ui': 2.0.5 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + vscode-uri@3.0.8: resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==} @@ -8638,6 +8720,11 @@ packages: engines: {node: '>=8'} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + wide-align@1.1.5: resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} @@ -8846,6 +8933,11 @@ snapshots: '@jridgewell/gen-mapping': 0.1.1 '@jridgewell/trace-mapping': 0.3.25 + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + '@ava/typescript@4.1.0': dependencies: escape-string-regexp: 5.0.0 @@ -8980,8 +9072,8 @@ snapshots: '@babel/generator@7.22.15': dependencies: '@babel/types': 7.22.19 - '@jridgewell/gen-mapping': 0.3.2 - '@jridgewell/trace-mapping': 0.3.19 + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 jsesc: 2.5.2 '@babel/generator@7.24.5': @@ -10037,12 +10129,6 @@ snapshots: '@jridgewell/set-array': 1.2.1 '@jridgewell/sourcemap-codec': 1.5.0 - '@jridgewell/gen-mapping@0.3.2': - dependencies: - '@jridgewell/set-array': 1.1.0 - '@jridgewell/sourcemap-codec': 1.5.0 - '@jridgewell/trace-mapping': 0.3.19 - '@jridgewell/gen-mapping@0.3.5': dependencies: '@jridgewell/set-array': 1.2.1 @@ -10051,19 +10137,12 @@ snapshots: '@jridgewell/resolve-uri@3.1.0': {} - '@jridgewell/set-array@1.1.0': {} - '@jridgewell/set-array@1.2.1': {} '@jridgewell/sourcemap-codec@1.4.15': {} '@jridgewell/sourcemap-codec@1.5.0': {} - '@jridgewell/trace-mapping@0.3.19': - dependencies: - '@jridgewell/resolve-uri': 3.1.0 - '@jridgewell/sourcemap-codec': 1.5.0 - '@jridgewell/trace-mapping@0.3.25': dependencies: '@jridgewell/resolve-uri': 3.1.0 @@ -11122,7 +11201,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 6.10.0(typescript@5.5.4) '@typescript-eslint/utils': 6.10.0(eslint@8.49.0)(typescript@5.5.4) - debug: 4.3.6(supports-color@9.2.2) + debug: 4.3.5 eslint: 8.49.0 ts-api-utils: 1.0.3(typescript@5.5.4) optionalDependencies: @@ -11233,6 +11312,17 @@ snapshots: '@vitest/utils': 1.6.0 chai: 4.3.10 + '@vitest/expect@2.0.5': + dependencies: + '@vitest/spy': 2.0.5 + '@vitest/utils': 2.0.5 + chai: 5.1.1 + tinyrainbow: 1.2.0 + + '@vitest/pretty-format@2.0.5': + dependencies: + tinyrainbow: 1.2.0 + '@vitest/runner@1.5.0': dependencies: '@vitest/utils': 1.5.0 @@ -11245,6 +11335,11 @@ snapshots: p-limit: 5.0.0 pathe: 1.1.2 + '@vitest/runner@2.0.5': + dependencies: + '@vitest/utils': 2.0.5 + pathe: 1.1.2 + '@vitest/snapshot@1.5.0': dependencies: magic-string: 0.30.5 @@ -11257,6 +11352,12 @@ snapshots: pathe: 1.1.2 pretty-format: 29.7.0 + '@vitest/snapshot@2.0.5': + dependencies: + '@vitest/pretty-format': 2.0.5 + magic-string: 0.30.11 + pathe: 1.1.2 + '@vitest/spy@1.5.0': dependencies: tinyspy: 2.2.0 @@ -11265,6 +11366,10 @@ snapshots: dependencies: tinyspy: 2.2.0 + '@vitest/spy@2.0.5': + dependencies: + tinyspy: 3.0.0 + '@vitest/ui@1.6.0(vitest@1.6.0)': dependencies: '@vitest/utils': 1.6.0 @@ -11290,6 +11395,13 @@ snapshots: loupe: 2.3.7 pretty-format: 29.7.0 + '@vitest/utils@2.0.5': + dependencies: + '@vitest/pretty-format': 2.0.5 + estree-walker: 3.0.3 + loupe: 3.1.1 + tinyrainbow: 1.2.0 + '@volar/language-core@2.3.4': dependencies: '@volar/source-map': 2.3.4 @@ -11624,6 +11736,8 @@ snapshots: assertion-error@1.1.0: {} + assertion-error@2.0.1: {} + ast-types@0.13.4: dependencies: tslib: 2.5.3 @@ -11913,6 +12027,14 @@ snapshots: pathval: 1.1.1 type-detect: 4.0.8 + chai@5.1.1: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.1 + deep-eql: 5.0.2 + loupe: 3.1.1 + pathval: 2.0.0 + chainsaw@0.1.0: dependencies: traverse: 0.3.9 @@ -11964,6 +12086,8 @@ snapshots: dependencies: get-func-name: 2.0.2 + check-error@2.1.1: {} + chokidar@3.5.3: dependencies: anymatch: 3.1.2 @@ -12306,6 +12430,8 @@ snapshots: dependencies: type-detect: 4.0.8 + deep-eql@5.0.2: {} + deep-extend@0.6.0: {} deep-is@0.1.4: {} @@ -14179,6 +14305,10 @@ snapshots: dependencies: get-func-name: 2.0.2 + loupe@3.1.1: + dependencies: + get-func-name: 2.0.2 + lower-case-first@1.0.2: dependencies: lower-case: 1.1.4 @@ -14866,6 +14996,8 @@ snapshots: pathval@1.1.1: {} + pathval@2.0.0: {} + performance-now@2.1.0: {} pg-cloudflare@1.1.1: {} @@ -15933,6 +16065,8 @@ snapshots: tinybench@2.6.0: {} + tinybench@2.9.0: {} + tinycolor2@1.6.0: {} tinygradient@1.1.5: @@ -15942,8 +16076,14 @@ snapshots: tinypool@0.8.3: {} + tinypool@1.0.1: {} + + tinyrainbow@1.2.0: {} + tinyspy@2.2.0: {} + tinyspy@3.0.0: {} + title-case@2.1.1: dependencies: no-case: 2.3.2 @@ -16347,6 +16487,23 @@ snapshots: - supports-color - terser + vite-node@2.0.5(@types/node@20.12.12): + dependencies: + cac: 6.7.14 + debug: 4.3.6(supports-color@9.2.2) + pathe: 1.1.2 + tinyrainbow: 1.2.0 + vite: 5.0.12(@types/node@20.12.12) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - stylus + - sugarss + - supports-color + - terser + vite-plugin-dts@4.0.1(@types/node@20.12.12)(rollup@4.9.6)(typescript@5.5.4)(vite@5.0.12(@types/node@20.12.12)): dependencies: '@microsoft/api-extractor': 7.47.4(@types/node@20.12.12) @@ -16509,6 +16666,39 @@ snapshots: - supports-color - terser + vitest@2.0.5(@types/node@20.12.12)(@vitest/ui@1.6.0(vitest@1.6.0)): + dependencies: + '@ampproject/remapping': 2.3.0 + '@vitest/expect': 2.0.5 + '@vitest/pretty-format': 2.0.5 + '@vitest/runner': 2.0.5 + '@vitest/snapshot': 2.0.5 + '@vitest/spy': 2.0.5 + '@vitest/utils': 2.0.5 + chai: 5.1.1 + debug: 4.3.6(supports-color@9.2.2) + execa: 8.0.1 + magic-string: 0.30.11 + pathe: 1.1.2 + std-env: 3.7.0 + tinybench: 2.9.0 + tinypool: 1.0.1 + tinyrainbow: 1.2.0 + vite: 5.0.12(@types/node@20.12.12) + vite-node: 2.0.5(@types/node@20.12.12) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 20.12.12 + '@vitest/ui': 1.6.0(vitest@1.6.0) + transitivePeerDependencies: + - less + - lightningcss + - sass + - stylus + - sugarss + - supports-color + - terser + vscode-uri@3.0.8: {} vue-tsc@2.0.29(typescript@5.5.4): @@ -16599,6 +16789,11 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + wide-align@1.1.5: dependencies: string-width: 4.2.3