-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Allow users to run getPlatformProxy on static asset workers when the assets dir doesn't exist
#13116
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
dario-piotrowicz
merged 8 commits into
main
from
dario/allow-dev-and-tests-when-no-assets-dir-exist
Apr 30, 2026
Merged
Allow users to run getPlatformProxy on static asset workers when the assets dir doesn't exist
#13116
Changes from 7 commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
540cf88
Allow users to run local dev or tests on static asset workers when th…
dario-piotrowicz 71b33c1
Update logic to only allow non-existing dir for `getPlatformProxy`
dario-piotrowicz f2560fd
update broken usage of `getAssetsOptions`
dario-piotrowicz 0d39d81
Update packages/miniflare/src/plugins/assets/index.ts
dario-piotrowicz 61e5e4b
Update packages/miniflare/test/plugins/assets/index.spec.ts
dario-piotrowicz 467a75c
update compat dates to the latest workerd available one
dario-piotrowicz 9d30331
cache tmp assets dir
dario-piotrowicz e568780
include worker name in assets cache key
dario-piotrowicz File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| --- | ||
| "miniflare": patch | ||
| --- | ||
|
|
||
| Gracefully handle a missing assets directory by starting with zero assets | ||
|
|
||
| Previously, configuring Miniflare with an `assets.directory` that did not exist on disk would cause the asset services to fail to start. This is a common situation during `wrangler dev` when the assets directory is a build output that hasn't been generated yet. | ||
|
|
||
| Now, when the configured assets directory does not exist, Miniflare creates an empty temporary directory and starts the asset services with zero assets. Once the real directory is created and `setOptions()` is called (e.g. triggered by the file watcher), Miniflare reloads and begins serving the actual assets. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| --- | ||
| "wrangler": patch | ||
| --- | ||
|
|
||
| Allow `getPlatformProxy` and `unstable_getMiniflareWorkerOptions` to start when the assets directory does not exist yet | ||
|
|
||
| Previously, `getPlatformProxy` would catch and swallow `NonExistentAssetsDirError` internally when the configured assets directory was absent on disk. This has been refactored so that the directory-existence check is skipped entirely for `getPlatformProxy` and `unstable_getMiniflareWorkerOptions`, since these APIs are typically used at dev time in frameworks where the assets directory is a build output that may not exist yet. | ||
|
|
||
| `wrangler dev`, `wrangler deploy`, `wrangler versions upload`, and `wrangler triggers deploy` continue to require the assets directory to exist when specified. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| import fs from "node:fs/promises"; | ||
| import path from "node:path"; | ||
| import { Miniflare } from "miniflare"; | ||
| import { test } from "vitest"; | ||
| import { useDispose, useTmp } from "../../test-shared"; | ||
|
|
||
| // Minimal worker script. When assets are configured, all incoming `dispatchFetch` | ||
| // requests are automatically routed through the assets router service, which | ||
| // either serves a matched asset (200) or returns 404 (because `has_user_worker` | ||
| // defaults to false, so unmatched paths go straight to the asset worker's 404). | ||
| const WORKER_SCRIPT = `export default { | ||
| async fetch(request, env) { | ||
| return new Response("from user worker"); | ||
| } | ||
| }`; | ||
|
|
||
| function makeOptions(directory: string) { | ||
| return { | ||
| modules: true, | ||
| script: WORKER_SCRIPT, | ||
| compatibilityDate: "2026-04-29", | ||
| assets: { directory }, | ||
| }; | ||
| } | ||
|
|
||
| test("starts without error when assets directory does not exist", async ({ | ||
| expect, | ||
| }) => { | ||
| const tmp = await useTmp(); | ||
| // Create a path that does not exist on disk | ||
| const nonExistentDir = path.join(tmp, "does-not-exist"); | ||
|
|
||
| // Should not throw even though the directory is absent | ||
| const mf = new Miniflare(makeOptions(nonExistentDir)); | ||
| useDispose(mf); | ||
|
|
||
| // Empty manifest → asset not found → 404 | ||
| const res = await mf.dispatchFetch("http://example.com/test.txt"); | ||
| expect(res.status).toBe(404); | ||
| await res.arrayBuffer(); // consume body to avoid leaks | ||
| }); | ||
|
|
||
| test("starts without error when assets directory is empty", async ({ | ||
| expect, | ||
| }) => { | ||
| const tmp = await useTmp(); | ||
|
|
||
| const mf = new Miniflare(makeOptions(tmp)); | ||
| useDispose(mf); | ||
|
|
||
| const res = await mf.dispatchFetch("http://example.com/test.txt"); | ||
| expect(res.status).toBe(404); | ||
| await res.arrayBuffer(); | ||
| }); | ||
|
|
||
| test("serves files from assets directory", async ({ expect }) => { | ||
| const tmp = await useTmp(); | ||
| await fs.writeFile(path.join(tmp, "test.txt"), "hello from asset"); | ||
|
|
||
| const mf = new Miniflare(makeOptions(tmp)); | ||
| useDispose(mf); | ||
|
|
||
| const res = await mf.dispatchFetch("http://example.com/test.txt"); | ||
| expect(res.status).toBe(200); | ||
| expect(await res.text()).toBe("hello from asset"); | ||
| }); | ||
|
|
||
| // ─── Watch / reload behaviour ──────────────────────────────────────────────── | ||
|
|
||
| // This test simulates what happens during `wrangler dev` when the assets | ||
| // directory does not exist at startup but it is created afterwards | ||
| test("serves new assets after setOptions() once the directory is created", async ({ | ||
| expect, | ||
| }) => { | ||
| const tmp = await useTmp(); | ||
| // The assets directory does not exist yet (build hasn't run) | ||
| const assetsDir = path.join(tmp, "dist"); | ||
|
|
||
| const mf = new Miniflare(makeOptions(assetsDir)); | ||
| useDispose(mf); | ||
|
|
||
| // Initially no assets → 404 | ||
| const res1 = await mf.dispatchFetch("http://example.com/index.html"); | ||
| expect(res1.status).toBe(404); | ||
| await res1.arrayBuffer(); | ||
|
|
||
| // Create assets directory | ||
| await fs.mkdir(assetsDir, { recursive: true }); | ||
| await fs.writeFile(path.join(assetsDir, "index.html"), "<h1>Hello!</h1>"); | ||
|
|
||
| // Simulate wrangler's reaction to chokidar detecting the new directory: | ||
| // it calls setOptions() which re-invokes getServices() on all plugins, | ||
| // re-walks the assets directory, and rebuilds the manifest. | ||
| await mf.setOptions(makeOptions(assetsDir)); | ||
|
|
||
| // The asset should now be served | ||
| const res2 = await mf.dispatchFetch("http://example.com/index.html"); | ||
| expect(res2.status).toBe(200); | ||
| expect(await res2.text()).toContain("<h1>Hello!</h1>"); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,172 @@ | ||
| import * as fs from "node:fs"; | ||
| import * as path from "node:path"; | ||
| import { describe, it } from "vitest"; | ||
| import { | ||
| NonDirectoryAssetsDirError, | ||
| NonExistentAssetsDirError, | ||
| getAssetsOptions, | ||
| } from "../assets"; | ||
| import { runInTempDir } from "./helpers/run-in-tmp"; | ||
| import type { Config } from "@cloudflare/workers-utils"; | ||
|
|
||
| /** | ||
| * Creates a minimal Config object sufficient for `getAssetsOptions`. | ||
| * Only the fields actually read by the function need to be populated. | ||
| */ | ||
| function makeConfig( | ||
| overrides: Partial<{ | ||
| assets: { directory?: string; binding?: string }; | ||
| main: string; | ||
| configPath: string; | ||
| }> = {} | ||
| ): Config { | ||
| return { | ||
| assets: undefined, | ||
| main: undefined, | ||
| configPath: undefined, | ||
| compatibility_date: "2026-04-29", | ||
| compatibility_flags: [], | ||
| ...overrides, | ||
| } as unknown as Config; | ||
| } | ||
|
|
||
| describe("getAssetsOptions", () => { | ||
| runInTempDir(); | ||
|
|
||
| describe("validateDirectoryExistence: true (default — deploy path)", () => { | ||
| it("throws NonExistentAssetsDirError when the --assets directory does not exist", ({ | ||
| expect, | ||
| }) => { | ||
| expect(() => | ||
| getAssetsOptions({ | ||
| args: { assets: "dist" }, | ||
| config: makeConfig(), | ||
| validateDirectoryExistence: true, | ||
| }) | ||
| ).toThrow(NonExistentAssetsDirError); | ||
| }); | ||
|
|
||
| it("throws with a message referencing the CLI flag when --assets is used", ({ | ||
| expect, | ||
| }) => { | ||
| expect(() => | ||
| getAssetsOptions({ | ||
| args: { assets: "dist" }, | ||
| config: makeConfig(), | ||
| validateDirectoryExistence: true, | ||
| }) | ||
| ).toThrow( | ||
| /The directory specified by the "--assets" command line argument does not exist/ | ||
| ); | ||
| }); | ||
|
|
||
| it("throws with a message referencing the config file when assets.directory is used", ({ | ||
| expect, | ||
| }) => { | ||
| expect(() => | ||
| getAssetsOptions({ | ||
| args: { assets: undefined }, | ||
| config: makeConfig({ assets: { directory: "dist" } }), | ||
| validateDirectoryExistence: true, | ||
| }) | ||
| ).toThrow( | ||
| /The directory specified by the "assets.directory" field in your configuration file does not exist/ | ||
| ); | ||
| }); | ||
|
|
||
| it("throws NonDirectoryAssetsDirError when the path points to a file, not a directory", ({ | ||
| expect, | ||
| }) => { | ||
| fs.writeFileSync("not-a-dir.txt", ""); | ||
| expect(() => | ||
| getAssetsOptions({ | ||
| args: { assets: "not-a-dir.txt" }, | ||
| config: makeConfig(), | ||
| validateDirectoryExistence: true, | ||
| }) | ||
| ).toThrow(NonDirectoryAssetsDirError); | ||
| }); | ||
| }); | ||
|
|
||
| describe("validateDirectoryExistence: false (getPlatformProxy / unstable_getMiniflareWorkerOptions path)", () => { | ||
| it("does NOT throw when the assets directory does not exist", ({ | ||
| expect, | ||
| }) => { | ||
| expect(() => | ||
| getAssetsOptions({ | ||
| args: { assets: "dist" }, | ||
| config: makeConfig(), | ||
| validateDirectoryExistence: false, | ||
| }) | ||
| ).not.toThrow(); | ||
| }); | ||
|
|
||
| it("returns a valid AssetsOptions object even when the directory is absent", ({ | ||
| expect, | ||
| }) => { | ||
| const result = getAssetsOptions({ | ||
| args: { assets: "dist" }, | ||
| config: makeConfig(), | ||
| validateDirectoryExistence: false, | ||
| }); | ||
|
|
||
| expect(result).toBeDefined(); | ||
| expect(result?.directory).toBe(path.resolve(process.cwd(), "dist")); | ||
| // No _redirects / _headers since the directory doesn't exist | ||
| expect(result?._redirects).toBeUndefined(); | ||
| expect(result?._headers).toBeUndefined(); | ||
| }); | ||
|
|
||
| it("still throws NonDirectoryAssetsDirError when the path points to a file", ({ | ||
| expect, | ||
| }) => { | ||
| fs.writeFileSync("not-a-dir.txt", ""); | ||
| expect(() => | ||
| getAssetsOptions({ | ||
| args: { assets: "not-a-dir.txt" }, | ||
| config: makeConfig(), | ||
| validateDirectoryExistence: false, | ||
| }) | ||
| ).toThrow(NonDirectoryAssetsDirError); | ||
| }); | ||
|
|
||
| it("returns correct options when the directory exists and has files", ({ | ||
| expect, | ||
| }) => { | ||
| fs.mkdirSync("dist"); | ||
| fs.writeFileSync(path.join("dist", "_redirects"), "/old /new 301"); | ||
|
|
||
| const result = getAssetsOptions({ | ||
| args: { assets: "dist" }, | ||
| config: makeConfig(), | ||
| validateDirectoryExistence: false, | ||
| }); | ||
|
|
||
| expect(result?.directory).toBe(path.resolve(process.cwd(), "dist")); | ||
| expect(result?._redirects).toContain("/old /new 301"); | ||
| }); | ||
|
|
||
| it("works with assets from config rather than the CLI flag", ({ | ||
| expect, | ||
| }) => { | ||
| const result = getAssetsOptions({ | ||
| args: { assets: undefined }, | ||
| config: makeConfig({ assets: { directory: "nonexistent-dir" } }), | ||
| validateDirectoryExistence: false, | ||
| }); | ||
|
|
||
| expect(result).toBeDefined(); | ||
| expect(result?.directory).toContain("nonexistent-dir"); | ||
| }); | ||
|
|
||
| it("returns undefined when no assets are configured", ({ expect }) => { | ||
| const result = getAssetsOptions({ | ||
| args: { assets: undefined }, | ||
| config: makeConfig(), | ||
| validateDirectoryExistence: false, | ||
| }); | ||
|
|
||
| expect(result).toBeUndefined(); | ||
| }); | ||
| }); | ||
| }); |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.