From 00f340f7c1709db777e80a8ea24d245909ff4486 Mon Sep 17 00:00:00 2001 From: Carmen Popoviciu Date: Thu, 15 Aug 2024 13:15:47 +0200 Subject: [PATCH] feat(miniflare): Implement KV/Assets plugin and Workers Assets simulator (#6403) This commit adds a new KV/Assets plugin and an Assets Worker in Miniflare, that are meant to emulate Workers with Assets in local development. --- .changeset/eight-terms-beg.md | 8 + .../public/about/index.html | 1 + .../workers-with-assets/public/index.html | 2 +- .../workers-with-assets/tests/index.test.ts | 20 +- packages/miniflare/README.md | 10 + packages/miniflare/src/plugins/kv/assets.ts | 99 +++++++++ packages/miniflare/src/plugins/kv/index.ts | 27 +++ .../miniflare/src/workers/kv/assets.worker.ts | 33 +++ .../miniflare/src/workers/kv/constants.ts | 15 ++ .../worker-with-custom-assets-bindings.ts | 14 ++ .../worker-with-default-assets-bindings.ts | 14 ++ .../miniflare/test/plugins/kv/assets.spec.ts | 192 ++++++++++++++++++ .../src/assets-manifest-no-op.ts | 13 ++ .../src/assets-manifest.ts | 11 + .../asset-server-worker/src/index.ts | 37 +++- packages/wrangler/src/dev/miniflare.ts | 3 + packages/wrangler/src/experimental-assets.ts | 3 +- 17 files changed, 494 insertions(+), 8 deletions(-) create mode 100644 .changeset/eight-terms-beg.md create mode 100644 fixtures/workers-with-assets/public/about/index.html create mode 100644 packages/miniflare/src/plugins/kv/assets.ts create mode 100644 packages/miniflare/src/workers/kv/assets.worker.ts create mode 100644 packages/miniflare/test/fixtures/assets/worker-with-custom-assets-bindings.ts create mode 100644 packages/miniflare/test/fixtures/assets/worker-with-default-assets-bindings.ts create mode 100644 packages/miniflare/test/plugins/kv/assets.spec.ts create mode 100644 packages/workers-shared/asset-server-worker/src/assets-manifest-no-op.ts create mode 100644 packages/workers-shared/asset-server-worker/src/assets-manifest.ts diff --git a/.changeset/eight-terms-beg.md b/.changeset/eight-terms-beg.md new file mode 100644 index 000000000000..e6865ceeaf38 --- /dev/null +++ b/.changeset/eight-terms-beg.md @@ -0,0 +1,8 @@ +--- +"miniflare": minor +"@cloudflare/workers-shared": minor +--- + +feat: Extend KV plugin behaviour to support Workers assets + +This commit extends Miniflare's KV plugin's behaviour to support Workers assets, and therefore enables the emulation of Workers with assets in local development. diff --git a/fixtures/workers-with-assets/public/about/index.html b/fixtures/workers-with-assets/public/about/index.html new file mode 100644 index 000000000000..14f9dffa9fbd --- /dev/null +++ b/fixtures/workers-with-assets/public/about/index.html @@ -0,0 +1 @@ +

Learn more about Workers with Assets soon!

diff --git a/fixtures/workers-with-assets/public/index.html b/fixtures/workers-with-assets/public/index.html index de28c4adee5c..b7351e6287a9 100644 --- a/fixtures/workers-with-assets/public/index.html +++ b/fixtures/workers-with-assets/public/index.html @@ -1 +1 @@ -

Hello Workers + Assets World!

+

Hello Workers + Assets World 🚀!

diff --git a/fixtures/workers-with-assets/tests/index.test.ts b/fixtures/workers-with-assets/tests/index.test.ts index cc0d36bb02b9..1c2708bdbb70 100644 --- a/fixtures/workers-with-assets/tests/index.test.ts +++ b/fixtures/workers-with-assets/tests/index.test.ts @@ -17,10 +17,22 @@ describe("[Workers + Assets] `wrangler dev`", () => { await stop?.(); }); - it("renders ", async ({ expect }) => { - const response = await fetch(`http://${ip}:${port}/`); - const text = await response.text(); + it("should respond with static asset content", async ({ expect }) => { + let response = await fetch(`http://${ip}:${port}/index.html`); + let text = await response.text(); expect(response.status).toBe(200); - expect(text).toContain(`Hello from Asset Server Worker 🚀`); + expect(text).toContain(`

Hello Workers + Assets World 🚀!

`); + + response = await fetch(`http://${ip}:${port}/about/index.html`); + text = await response.text(); + expect(response.status).toBe(200); + expect(text).toContain(`

Learn more about Workers with Assets soon!

`); + }); + + 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"); }); }); diff --git a/packages/miniflare/README.md b/packages/miniflare/README.md index e190962169d9..dfc379501ba9 100644 --- a/packages/miniflare/README.md +++ b/packages/miniflare/README.md @@ -528,6 +528,16 @@ parameter in module format Workers. If set, only files with paths _not_ matching these glob patterns will be served. + - `assetsPath?: string` + + Path to serve Workers assets from. + + - `assetsKVBindingName?: string` + Name of the binding to the KV namespace that the assets are in. If `assetsPath` is set, this binding will be injected into this Worker. + + - `assetsManifestBindingName?: string` + Name of the binding to an `ArrayBuffer` containing the binary-encoded assets manifest. If `assetsPath` is set, this binding will be injected into this Worker. + #### R2 - `r2Buckets?: Record | string[]` diff --git a/packages/miniflare/src/plugins/kv/assets.ts b/packages/miniflare/src/plugins/kv/assets.ts new file mode 100644 index 000000000000..74abe82edcab --- /dev/null +++ b/packages/miniflare/src/plugins/kv/assets.ts @@ -0,0 +1,99 @@ +import { KVOptionsSchema } from "miniflare"; +import SCRIPT_KV_ASSETS from "worker:kv/assets"; +import { z } from "zod"; +import { Service, Worker_Binding } from "../../runtime"; +import { getAssetsBindingsNames, SharedBindings } from "../../workers"; +import { kProxyNodeBinding } from "../shared"; +import { KV_PLUGIN_NAME } from "./constants"; + +export interface AssetsOptions { + assetsPath: string; + assetsKVBindingName?: string; + assetsManifestBindingName?: string; +} + +export function isWorkersWithAssets( + options: z.infer +): options is AssetsOptions { + return options.assetsPath !== undefined; +} + +const SERVICE_NAMESPACE_ASSET = `${KV_PLUGIN_NAME}:asset`; + +export function buildAssetsManifest(): Uint8Array { + const buffer = new ArrayBuffer(20); + const assetManifest = new Uint8Array(buffer); // [0, 0, 0, ..., 0, 0] + // this will signal to Asset Server Worker that its running in a + // local dev "context" + assetManifest.set([1], 0); // [1, 0, 0, ..., 0, 0] + + return assetManifest; +} + +export async function getAssetsBindings( + options: AssetsOptions +): Promise { + const assetsBindings = getAssetsBindingsNames( + options?.assetsKVBindingName, + options?.assetsManifestBindingName + ); + + const assetsManifest = buildAssetsManifest(); + + return [ + { + // this is the binding to the KV namespace that the assets are in. + name: assetsBindings.ASSETS_KV_NAMESPACE, + kvNamespace: { name: SERVICE_NAMESPACE_ASSET }, + }, + { + // this is the binding to an ArrayBuffer containing the binary-encoded + // assets manifest. + name: assetsBindings.ASSETS_MANIFEST, + data: assetsManifest, + }, + ]; +} + +export async function getAssetsNodeBindings( + options: AssetsOptions +): Promise> { + const assetsManifest = buildAssetsManifest(); + const assetsBindings = getAssetsBindingsNames( + options?.assetsKVBindingName, + options?.assetsManifestBindingName + ); + + return { + [assetsBindings.ASSETS_KV_NAMESPACE]: kProxyNodeBinding, + [assetsBindings.ASSETS_MANIFEST]: assetsManifest, + }; +} + +export function getAssetsServices(options: AssetsOptions): Service[] { + const storageServiceName = `${SERVICE_NAMESPACE_ASSET}:storage`; + const storageService: Service = { + name: storageServiceName, + disk: { path: options.assetsPath, writable: true }, + }; + const namespaceService: Service = { + name: SERVICE_NAMESPACE_ASSET, + worker: { + compatibilityDate: "2023-07-24", + compatibilityFlags: ["nodejs_compat"], + modules: [ + { + name: "assets.worker.js", + esModule: SCRIPT_KV_ASSETS(), + }, + ], + bindings: [ + { + name: SharedBindings.MAYBE_SERVICE_BLOBS, + service: { name: storageServiceName }, + }, + ], + }, + }; + return [storageService, namespaceService]; +} diff --git a/packages/miniflare/src/plugins/kv/index.ts b/packages/miniflare/src/plugins/kv/index.ts index 0fe2e3da4a47..708f43f0eb94 100644 --- a/packages/miniflare/src/plugins/kv/index.ts +++ b/packages/miniflare/src/plugins/kv/index.ts @@ -20,6 +20,12 @@ import { Plugin, SERVICE_LOOPBACK, } from "../shared"; +import { + getAssetsBindings, + getAssetsNodeBindings, + getAssetsServices, + isWorkersWithAssets, +} from "./assets"; import { KV_PLUGIN_NAME } from "./constants"; import { getSitesBindings, @@ -31,6 +37,11 @@ import { export const KVOptionsSchema = z.object({ kvNamespaces: z.union([z.record(z.string()), z.string().array()]).optional(), + // Workers + Assets + assetsPath: PathSchema.optional(), + assetsKVBindingName: z.string().optional(), + assetsManifestBindingName: z.string().optional(), + // Workers Sites sitePath: PathSchema.optional(), siteInclude: z.string().array().optional(), @@ -71,18 +82,29 @@ export const KV_PLUGIN: Plugin< bindings.push(...(await getSitesBindings(options))); } + if (isWorkersWithAssets(options)) { + bindings.push(...(await getAssetsBindings(options))); + } + return bindings; }, + async getNodeBindings(options) { const namespaces = namespaceKeys(options.kvNamespaces); const bindings = Object.fromEntries( namespaces.map((name) => [name, kProxyNodeBinding]) ); + if (isWorkersSitesEnabled(options)) { Object.assign(bindings, await getSitesNodeBindings(options)); } + + if (isWorkersWithAssets(options)) { + Object.assign(bindings, await getAssetsNodeBindings(options)); + } return bindings; }, + async getServices({ options, sharedOptions, @@ -151,8 +173,13 @@ export const KV_PLUGIN: Plugin< services.push(...getSitesServices(options)); } + if (isWorkersWithAssets(options)) { + services.push(...getAssetsServices(options)); + } + return services; }, + getPersistPath({ kvPersist }, tmpPath) { return getPersistPath(KV_PLUGIN_NAME, tmpPath, kvPersist); }, diff --git a/packages/miniflare/src/workers/kv/assets.worker.ts b/packages/miniflare/src/workers/kv/assets.worker.ts new file mode 100644 index 000000000000..33a2904afaa7 --- /dev/null +++ b/packages/miniflare/src/workers/kv/assets.worker.ts @@ -0,0 +1,33 @@ +import { SharedBindings } from "miniflare:shared"; +import { KVParams } from "./constants"; + +interface Env { + [SharedBindings.MAYBE_SERVICE_BLOBS]: Fetcher; +} + +export default >{ + async fetch(request, env) { + // Only permit reads + if (request.method !== "GET") { + const message = `Cannot ${request.method.toLowerCase()}() with Workers Assets namespace`; + return new Response(message, { status: 405, statusText: message }); + } + + // Decode key + const url = new URL(request.url); + let key = url.pathname.substring(1); // Strip leading "/" + + if (url.searchParams.get(KVParams.URL_ENCODED)?.toLowerCase() === "true") { + key = decodeURIComponent(key); + } + + const blobsService = env[SharedBindings.MAYBE_SERVICE_BLOBS]; + if (key === "" || key === "/") { + return new Response("Not Found", { + status: 404, + }); + } else { + return blobsService.fetch(new URL(key, "http://placeholder")); + } + }, +}; diff --git a/packages/miniflare/src/workers/kv/constants.ts b/packages/miniflare/src/workers/kv/constants.ts index 6ef100492e10..c211def10763 100644 --- a/packages/miniflare/src/workers/kv/constants.ts +++ b/packages/miniflare/src/workers/kv/constants.ts @@ -45,6 +45,7 @@ export function decodeSitesKey(key: string): string { ? decodeURIComponent(key.substring(SITES_NO_CACHE_PREFIX.length)) : key; } + export function isSitesRequest(request: { url: string }) { const url = new URL(request.url); return url.pathname.startsWith(`/${SITES_NO_CACHE_PREFIX}`); @@ -116,3 +117,17 @@ export function testSiteRegExps( if (regExps.exclude !== undefined) return !testRegExps(regExps.exclude, key); return true; } + +export function getAssetsBindingsNames( + // __STATIC_CONTENT and __STATIC_CONTENT_MANIFEST binding names are + // reserved for Workers Sites. Since we want to allow both sites and + // assets to work side by side, we cannot use the same binding name + // for assets. Therefore deferring to a different default naming here. + assetsKVBindingName = "__STATIC_ASSETS_CONTENT", + assetsManifestBindingName = "__STATIC_ASSETS_CONTENT_MANIFEST" +) { + return { + ASSETS_KV_NAMESPACE: assetsKVBindingName, + ASSETS_MANIFEST: assetsManifestBindingName, + } as const; +} diff --git a/packages/miniflare/test/fixtures/assets/worker-with-custom-assets-bindings.ts b/packages/miniflare/test/fixtures/assets/worker-with-custom-assets-bindings.ts new file mode 100644 index 000000000000..fd04480913de --- /dev/null +++ b/packages/miniflare/test/fixtures/assets/worker-with-custom-assets-bindings.ts @@ -0,0 +1,14 @@ +interface Env { + // custom kv binding name + CUSTOM_ASSETS_NAMESPACE: KVNamespace; +} + +export default { + async fetch(request: Request, env: Env) { + const url = new URL(request.url); + const { pathname } = url; + + const content = await env.CUSTOM_ASSETS_NAMESPACE.get(pathname); + return new Response(content); + }, +}; diff --git a/packages/miniflare/test/fixtures/assets/worker-with-default-assets-bindings.ts b/packages/miniflare/test/fixtures/assets/worker-with-default-assets-bindings.ts new file mode 100644 index 000000000000..cd4e43385bcc --- /dev/null +++ b/packages/miniflare/test/fixtures/assets/worker-with-default-assets-bindings.ts @@ -0,0 +1,14 @@ +interface Env { + // this is the default kv binding name + __STATIC_ASSETS_CONTENT: KVNamespace; +} + +export default { + async fetch(request: Request, env: Env) { + const url = new URL(request.url); + const { pathname } = url; + + const content = await env.__STATIC_ASSETS_CONTENT.get(pathname); + return new Response(content); + }, +}; diff --git a/packages/miniflare/test/plugins/kv/assets.spec.ts b/packages/miniflare/test/plugins/kv/assets.spec.ts new file mode 100644 index 000000000000..7d9d7ba3f2e9 --- /dev/null +++ b/packages/miniflare/test/plugins/kv/assets.spec.ts @@ -0,0 +1,192 @@ +import fs from "fs/promises"; +import path from "path"; +import anyTest, { Macro, TestFn } from "ava"; +import esbuild from "esbuild"; +import { Miniflare } from "miniflare"; +import { useTmp } from "../../test-shared"; + +const FIXTURES_PATH = path.resolve( + __dirname, + "../../../../test/fixtures/assets" +); +const MODULES_ENTRY_PATH = path.join( + FIXTURES_PATH, + "worker-with-default-assets-bindings.ts" +); + +interface Context { + modulesPath: string; +} + +const test = anyTest as TestFn; + +test.before(async (t) => { + // Build fixtures + const tmp = await useTmp(t); + await esbuild.build({ + entryPoints: [MODULES_ENTRY_PATH], + format: "esm", + bundle: true, + sourcemap: true, + outdir: tmp, + }); + t.context.modulesPath = path.join( + tmp, + "worker-with-default-assets-bindings.js" + ); +}); + +type Route = keyof typeof routeContents; +const routeContents = { + "/index.html": "

Index

", + "/a.txt": "a", + "/b/b.txt": "b", +}; + +const getAssetMacro: Macro<[Set], Context> = { + async exec(t, expectedRoutes) { + const tmp = await useTmp(t); + for (const [route, contents] of Object.entries(routeContents)) { + const routePath = path.join(tmp, route); + await fs.mkdir(path.dirname(routePath), { recursive: true }); + await fs.writeFile(routePath, contents, "utf8"); + } + + const mf = new Miniflare({ + scriptPath: t.context.modulesPath, + modules: true, + assetsPath: tmp, + }); + t.teardown(() => mf.dispose()); + + for (const [route, expectedContents] of Object.entries(routeContents)) { + const res = await mf.dispatchFetch(`http://localhost:8787${route}`); + const text = (await res.text()).trim(); + const expected = expectedRoutes.has(route as Route); + t.is(res.status, expected ? 200 : 404, `${route}: ${text}`); + if (expected) t.is(text, expectedContents, route); + } + }, +}; + +// Tests for checking different types of globs are matched correctly +const matchMacro: Macro<[string], Context> = { + async exec(t) { + const tmp = await useTmp(t); + const dir = path.join(tmp, "a", "b", "c"); + await fs.mkdir(dir, { recursive: true }); + await fs.writeFile(path.join(dir, "test.txt"), "test", "utf8"); + const mf = new Miniflare({ + scriptPath: t.context.modulesPath, + modules: true, + assetsPath: tmp, + }); + t.teardown(() => mf.dispose()); + + const res = await mf.dispatchFetch("http://localhost:8787/a/b/c/test.txt"); + t.is(res.status, 200); + await res.arrayBuffer(); + }, +}; + +const customBindingsMacro: Macro<[Set], Context> = { + async exec(t, expectedRoutes) { + const ENTRY_PATH = path.join( + FIXTURES_PATH, + "worker-with-custom-assets-bindings.ts" + ); + const tmp = await useTmp(t); + + for (const [route, contents] of Object.entries(routeContents)) { + const routePath = path.join(tmp, route); + await fs.mkdir(path.dirname(routePath), { recursive: true }); + await fs.writeFile(routePath, contents, "utf8"); + } + + await esbuild.build({ + entryPoints: [ENTRY_PATH], + format: "esm", + bundle: true, + sourcemap: true, + outdir: tmp, + }); + t.context.modulesPath = path.join( + tmp, + "worker-with-custom-assets-bindings.js" + ); + + const mf = new Miniflare({ + scriptPath: t.context.modulesPath, + modules: true, + assetsPath: tmp, + assetsKVBindingName: "CUSTOM_ASSETS_NAMESPACE", + }); + t.teardown(() => mf.dispose()); + + for (const [route, expectedContents] of Object.entries(routeContents)) { + const res = await mf.dispatchFetch(`http://localhost:8787${route}`); + const text = (await res.text()).trim(); + const expected = expectedRoutes.has(route as Route); + t.is(res.status, expected ? 200 : 404, `${route}: ${text}`); + if (expected) t.is(text, expectedContents, route); + } + }, +}; + +test( + "gets all assets", + getAssetMacro, + new Set(["/index.html", "/a.txt", "/b/b.txt"]) +); + +test("matches file name pattern", matchMacro, "test.txt"); +test("matches exact pattern", matchMacro, "a/b/c/test.txt"); +test("matches extension patterns", matchMacro, "*.txt"); +test("matches globstar patterns", matchMacro, "**/*.txt"); +test("matches wildcard directory patterns", matchMacro, "a/*/c/*.txt"); + +test("gets assets with percent-encoded paths", async (t) => { + // https://github.com/cloudflare/miniflare/issues/326 + const tmp = await useTmp(t); + const testPath = path.join(tmp, "ń.txt"); + await fs.writeFile(testPath, "test", "utf8"); + const mf = new Miniflare({ + scriptPath: t.context.modulesPath, + modules: true, + assetsPath: tmp, + }); + t.teardown(() => mf.dispose()); + const res = await mf.dispatchFetch("http://localhost:8787/ń.txt"); + t.is(await res.text(), "test"); +}); + +// skipping until we put caching in place +test.skip("doesn't cache assets", async (t) => { + const tmp = await useTmp(t); + const testPath = path.join(tmp, "test.txt"); + await fs.writeFile(testPath, "1", "utf8"); + + const mf = new Miniflare({ + scriptPath: t.context.modulesPath, + modules: true, + assetsPath: tmp, + }); + t.teardown(() => mf.dispose()); + + const res1 = await mf.dispatchFetch("http://localhost:8787/test.txt"); + const text1 = await res1.text(); + t.is(res1.headers.get("CF-Cache-Status"), "MISS"); + t.is(text1, "1"); + + await fs.writeFile(testPath, "2", "utf8"); + const res2 = await mf.dispatchFetch("http://localhost:8787/test.txt"); + const text2 = await res2.text(); + t.is(res2.headers.get("CF-Cache-Status"), "MISS"); + t.is(text2, "2"); +}); + +test( + "supports binding to a custom assets KV namespace", + customBindingsMacro, + new Set(["/index.html", "/a.txt", "/b/b.txt"]) +); diff --git a/packages/workers-shared/asset-server-worker/src/assets-manifest-no-op.ts b/packages/workers-shared/asset-server-worker/src/assets-manifest-no-op.ts new file mode 100644 index 000000000000..7e6993460607 --- /dev/null +++ b/packages/workers-shared/asset-server-worker/src/assets-manifest-no-op.ts @@ -0,0 +1,13 @@ +/** + * This is the NOOP version of `AssetsManifest`, and is meant to be used + * in local development only. + * + * The `NoopAssetsManifest` assumes a file path to file path mapping in + * concept, which is why its `get` fn will always return the given pathname + * unchanged. + */ +export class NoopAssetsManifest { + async get(pathname: string) { + return Promise.resolve(pathname); + } +} diff --git a/packages/workers-shared/asset-server-worker/src/assets-manifest.ts b/packages/workers-shared/asset-server-worker/src/assets-manifest.ts new file mode 100644 index 000000000000..87e47fafb180 --- /dev/null +++ b/packages/workers-shared/asset-server-worker/src/assets-manifest.ts @@ -0,0 +1,11 @@ +export class AssetsManifest { + private data: ArrayBuffer; + + constructor(data: ArrayBuffer) { + this.data = data; + } + + async get(pathname: string) { + return Promise.resolve(pathname); + } +} diff --git a/packages/workers-shared/asset-server-worker/src/index.ts b/packages/workers-shared/asset-server-worker/src/index.ts index afeacd84f98e..a3c565005646 100644 --- a/packages/workers-shared/asset-server-worker/src/index.ts +++ b/packages/workers-shared/asset-server-worker/src/index.ts @@ -1,5 +1,38 @@ +import { AssetsManifest } from "./assets-manifest"; +import { NoopAssetsManifest } from "./assets-manifest-no-op"; + +interface Env { + ASSETS_MANIFEST: ArrayBuffer; + ASSETS_KV_NAMESPACE: KVNamespace; +} + export default { - async fetch() { - return new Response("Hello from Asset Server Worker 🚀"); + async fetch(request: Request, env: Env) { + const { + // ASSETS_MANIFEST is a pipeline binding to an ArrayBuffer containing the + // binary-encoded site manifest + ASSETS_MANIFEST = new ArrayBuffer(0), + + // ASSETS_KV_NAMESPACE is a pipeline binding to the KV namespace that the + // assets are in. + ASSETS_KV_NAMESPACE, + } = env; + + const url = new URL(request.url); + const { pathname } = url; + + const isLocalDevContext = new Uint8Array(ASSETS_MANIFEST).at(0) === 1; + const assetsManifest = isLocalDevContext + ? new NoopAssetsManifest() + : new AssetsManifest(ASSETS_MANIFEST); + const assetEntry = await assetsManifest.get(pathname); + + const content = await ASSETS_KV_NAMESPACE.get(assetEntry); + + if (!content) { + return new Response("Not Found", { status: 404 }); + } + + return new Response(content); }, }; diff --git a/packages/wrangler/src/dev/miniflare.ts b/packages/wrangler/src/dev/miniflare.ts index e52149c04124..27ec5b853d03 100644 --- a/packages/wrangler/src/dev/miniflare.ts +++ b/packages/wrangler/src/dev/miniflare.ts @@ -931,6 +931,9 @@ function getAssetServerWorker( port: 0, }, ], + assetsPath: config.experimentalAssets.directory, + assetsKVBindingName: "ASSETS_KV_NAMESPACE", + assetsManifestBindingName: "ASSETS_MANIFEST", }, ]; } diff --git a/packages/wrangler/src/experimental-assets.ts b/packages/wrangler/src/experimental-assets.ts index 672e6ecb6931..0c0b8e9cb442 100644 --- a/packages/wrangler/src/experimental-assets.ts +++ b/packages/wrangler/src/experimental-assets.ts @@ -15,6 +15,7 @@ import { isJwtExpired } from "./pages/upload"; import { APIError } from "./parse"; import { urlSafe } from "./sites"; import type { Config } from "./config"; +import type { ExperimentalAssets } from "./config/environment"; export type AssetManifest = { [path: string]: { hash: string; size: number } }; @@ -313,7 +314,7 @@ export function getExperimentalAssetsBasePath( export function processExperimentalAssetsArg( args: { experimentalAssets: string | undefined }, config: Config -) { +): ExperimentalAssets | undefined { const experimentalAssets = args.experimentalAssets ? { directory: args.experimentalAssets } : config.experimental_assets;