Skip to content

Commit

Permalink
add workers assets to vitest integration (#6835)
Browse files Browse the repository at this point in the history
* add worker with static assets to vitest fixtures

* it! works! (add miniflare/vitest plumbing)

* add assets options to miniflare README.md

* add assets only vitest fixture

* changeset

* add assets to unstable_getMiniflareWorkerOptions

* add todo for rpc entrypoint self
  • Loading branch information
emily-shen authored Oct 7, 2024
1 parent 54924a4 commit 5c50949
Show file tree
Hide file tree
Showing 30 changed files with 285 additions and 28 deletions.
7 changes: 7 additions & 0 deletions .changeset/brave-needles-refuse.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"miniflare": patch
---

fix: rename asset plugin options slightly to match wrangler.toml better

Renamed `path` -> `directory`, `bindingName` -> `binding`.
9 changes: 9 additions & 0 deletions .changeset/breezy-tips-sparkle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@cloudflare/vitest-pool-workers": patch
---

feature: enable asset routing in the vitest integration for Workers + static assets

Integration tests (using the SELF binding) in Workers + assets projects will now return static assets if present on that path. Previously all requests went to the user worker regardless of whether static assets would have been served in production.

Unit style tests intentionally do not return static assets.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# ✅ workers-with-assets-only

This example contains assets without a Worker script.

An asset-only project can only be tested integration-style using the SELF binding, as there is no Worker to import and unit test.
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Hello, World!</title>
</head>
<body>
<h1>Asset index.html</h1>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { env, SELF } from "cloudflare:test";
import { describe, expect, it } from "vitest";

// There is no Worker so we can't import one and unit test
it("can test asset serving (integration style)", async () => {
let response = await SELF.fetch("http://example.com/index.html");
expect(await response.text()).toContain("Asset index.html");

// no such asset
response = await SELF.fetch("http://example.com/message");
expect(await response.text()).toBeFalsy();
expect(response.status).toBe(404);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extends": "../../tsconfig.workerd-test.json",
"include": ["./**/*.ts"]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extends": "../tsconfig.node.json",
"include": ["./*.ts"]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { defineWorkersProject } from "@cloudflare/vitest-pool-workers/config";

export default defineWorkersProject({
test: {
poolOptions: {
workers: {
wrangler: { configPath: "./wrangler.toml" },
},
},
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#:schema node_modules/wrangler/config-schema.json
name = "workers-static-assets-only"
compatibility_date = "2024-09-19"
compatibility_flags = ["nodejs_compat"]

[assets]
directory = "./public"
html_handling = "none"
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# ✅ workers-static-assets-with-user-worker

This Worker contains assets as well as a Worker script

Integration tests should hit assets, but unit tests which directly import the Worker should not.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<p>binding.html</p>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Hello, World!</title>
</head>
<body>
<h1>Asset index.html</h1>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
interface Env {
ASSETS: Fetcher;
}
15 changes: 15 additions & 0 deletions fixtures/vitest-pool-workers-examples/workers-assets/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export default {
async fetch(request, env, ctx): Promise<Response> {
const url = new URL(request.url);
switch (url.pathname) {
case "/message":
return new Response("Hello, World!");
case "/random":
return new Response(crypto.randomUUID());
case "/binding":
return env.ASSETS.fetch(request);
default:
return new Response(null, { status: 404 });
}
},
} satisfies ExportedHandler<Env>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extends": "../../tsconfig.workerd.json",
"include": ["./**/*.ts"]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import {
createExecutionContext,
env,
SELF,
waitOnExecutionContext,
} from "cloudflare:test";
import { describe, expect, it } from "vitest";
import worker from "../src";

// This will improve in the next major version of `@cloudflare/workers-types`,
// but for now you'll need to do something like this to get a correctly-typed
// `Request` to pass to `createPagesEventContext()`.
const IncomingRequest = Request<unknown, IncomingRequestCfProperties>;

describe("Hello World user worker", () => {
describe("unit test style", async () => {
it('responds with "Hello, World!', async () => {
const request = new IncomingRequest("http://example.com/message");
// Create an empty context to pass to `worker.fetch()`.
const ctx = createExecutionContext();
const response = await worker.fetch(request, env, ctx);
// Wait for all `Promise`s passed to `ctx.waitUntil()` to settle before running test assertions
await waitOnExecutionContext(ctx);
expect(await response.text()).toMatchInlineSnapshot(`"Hello, World!"`);
});
it("does not get assets directly if importing Worker directly", async () => {
const request = new IncomingRequest("http://example.com/");
const ctx = createExecutionContext();
const response = await worker.fetch(request, env, ctx);
await waitOnExecutionContext(ctx);
expect(response.status).toBe(404);
});

it("can still access assets via binding", async () => {
const request = new IncomingRequest("http://example.com/binding");
const ctx = createExecutionContext();
const response = await worker.fetch(request, env, ctx);
await waitOnExecutionContext(ctx);
expect(await response.text()).toContain("binding.html");
});
});

describe("integration test style", async () => {
it('responds with "Hello, World!" (integration style)', async () => {
const response = await SELF.fetch("http://example.com/message");
expect(await response.text()).toMatchInlineSnapshot(`"Hello, World!"`);
});
it("does get assets directly is using SELF", async () => {
const response = await SELF.fetch("http://example.com/");
expect(await response.text()).toContain("Asset index.html");
});
it("can also get assets via binding", async () => {
const response = await SELF.fetch("http://example.com/binding");
expect(await response.text()).toContain("binding.html");
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
declare module "cloudflare:test" {
interface ProvidedEnv extends Env {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extends": "../../tsconfig.workerd-test.json",
"include": ["./**/*.ts", "../src/env.d.ts"]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extends": "../tsconfig.node.json",
"include": ["./*.ts"]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { defineWorkersProject } from "@cloudflare/vitest-pool-workers/config";

export default defineWorkersProject({
test: {
poolOptions: {
workers: {
wrangler: { configPath: "./wrangler.toml" },
},
},
},
});
11 changes: 11 additions & 0 deletions fixtures/vitest-pool-workers-examples/workers-assets/wrangler.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#:schema node_modules/wrangler/config-schema.json
name = "workers-static-assets-with-user-worker"
main = "src/index.ts"
compatibility_date = "2024-09-19"
compatibility_flags = ["nodejs_compat"]

[assets]
directory = "./public"
binding = "ASSETS"
html_handling = "auto-trailing-slash"
not_found_handling = "none"
11 changes: 11 additions & 0 deletions packages/miniflare/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,17 @@ parameter in module format Workers.
have at most one consumer. If a `string[]` of queue names is specified,
default consumer options will be used.

#### Assets

- `directory?: string`
Path to serve Workers static asset files from.

- `binding?: string`
Binding name to inject as a `Fetcher` binding to allow access to static assets from within the Worker.

- `assetOptions?: { html_handling?: HTMLHandlingOptions, not_found_handling?: NotFoundHandlingOptions}`
Configuration for file-based asset routing - see [docs](https://developers.cloudflare.com/workers/static-assets/routing/#routing-configuration) for options

#### Analytics Engine, Sending Email, Vectorize and Workers for Platforms

_Not yet supported_
Expand Down
20 changes: 16 additions & 4 deletions packages/miniflare/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1131,6 +1131,13 @@ export class Miniflare {
innerBindings: Worker_Binding[];
}[] = [];

// This will be the user worker or the vitest pool worker wrapping the user worker
// The asset plugin needs this so that it can set the binding between the router worker and the user worker
if (this.#workerOpts[0].assets.assets) {
this.#workerOpts[0].assets.assets.workerName =
this.#workerOpts[0].core.name;
}

for (let i = 0; i < allWorkerOpts.length; i++) {
const previousWorkerOpts = allPreviousWorkerOpts?.[i];
const workerOpts = allWorkerOpts[i];
Expand Down Expand Up @@ -1283,10 +1290,15 @@ export class Miniflare {
const globalServices = getGlobalServices({
sharedOptions: sharedOpts.core,
allWorkerRoutes,
// if Workers + Assets project, point to router Worker service rather than user Worker
fallbackWorkerName: this.#workerOpts[0].assets.assets
? ROUTER_SERVICE_NAME
: getUserServiceName(this.#workerOpts[0].core.name),
// if Workers + Assets project but NOT Vitest, point to router Worker instead
// if Vitest with assets, the self binding on the test runner will point to RW
fallbackWorkerName:
this.#workerOpts[0].assets.assets &&
!this.#workerOpts[0].core.name?.startsWith(
"vitest-pool-workers-runner-"
)
? ROUTER_SERVICE_NAME
: getUserServiceName(this.#workerOpts[0].core.name),
loopbackPort,
log: this.#log,
proxyBindings,
Expand Down
14 changes: 7 additions & 7 deletions packages/miniflare/src/plugins/assets/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,24 +32,24 @@ import { AssetsOptionsSchema } from "./schema";
export const ASSETS_PLUGIN: Plugin<typeof AssetsOptionsSchema> = {
options: AssetsOptionsSchema,
async getBindings(options: z.infer<typeof AssetsOptionsSchema>) {
if (!options.assets?.bindingName) {
if (!options.assets?.binding) {
return [];
}
return [
{
// binding between User Worker and Asset Worker
name: options.assets.bindingName,
name: options.assets.binding,
service: { name: ASSETS_SERVICE_NAME },
},
];
},

async getNodeBindings(options) {
if (!options.assets?.bindingName) {
if (!options.assets?.binding) {
return {};
}
return {
[options.assets.bindingName]: new ProxyNodeBinding(),
[options.assets.binding]: new ProxyNodeBinding(),
};
},

Expand All @@ -61,11 +61,11 @@ export const ASSETS_PLUGIN: Plugin<typeof AssetsOptionsSchema> = {
const storageServiceName = `${ASSETS_PLUGIN_NAME}:storage`;
const storageService: Service = {
name: storageServiceName,
disk: { path: options.assets.path, writable: true },
disk: { path: options.assets.directory, writable: true },
};

const { encodedAssetManifest, assetsReverseMap } = await buildAssetManifest(
options.assets.path
options.assets.directory
);

const namespaceService: Service = {
Expand Down Expand Up @@ -113,7 +113,7 @@ export const ASSETS_PLUGIN: Plugin<typeof AssetsOptionsSchema> = {
},
{
name: "CONFIG",
json: JSON.stringify(options.assets.assetConfig),
json: JSON.stringify(options.assets.assetConfig ?? {}),
},
],
},
Expand Down
10 changes: 6 additions & 4 deletions packages/miniflare/src/plugins/assets/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ import { PathSchema } from "../../shared";
export const AssetsOptionsSchema = z.object({
assets: z
.object({
// User worker name or vitest runner - this is only ever set inside miniflare
// The assets plugin needs access to the worker name to create the router worker - user worker binding
workerName: z.string().optional(),
path: PathSchema,
bindingName: z.string().optional(),
routingConfig: RoutingConfigSchema,
assetConfig: AssetConfigSchema,
directory: PathSchema,
binding: z.string().optional(),
routingConfig: RoutingConfigSchema.optional(),
assetConfig: AssetConfigSchema.optional(),
})
.optional(),
});
Loading

0 comments on commit 5c50949

Please sign in to comment.