diff --git a/.changeset/tidy-hoops-win.md b/.changeset/tidy-hoops-win.md new file mode 100644 index 0000000000..d0ee592693 --- /dev/null +++ b/.changeset/tidy-hoops-win.md @@ -0,0 +1,15 @@ +--- +"@cloudflare/vite-plugin": minor +--- + +Enhanced build support for Workers with assets. + +Assets that are imported in the entry Worker are now automatically moved to the client build output. This enables importing assets in your Worker and accessing them via the [assets binding](https://developers.cloudflare.com/workers/static-assets/binding/#binding). See [Static Asset Handling](https://vite.dev/guide/assets) to find out about all the ways you can import assets in Vite. + +Additionally, a broader range of build scenarios are now supported. These are: + +- Assets only build with client entry/entries +- Assets only build with no client entry/entries that includes `public` directory assets +- Worker(s) + assets build with client entry/entries +- Worker(s) + assets build with no client entry/entries that includes imported and/or `public` directory assets +- Worker(s) build with no assets diff --git a/packages/vite-plugin-cloudflare/playground/assets/__tests__/assets.spec.ts b/packages/vite-plugin-cloudflare/playground/assets/__tests__/assets.spec.ts index 12d4ce7c95..0228b8dcb2 100644 --- a/packages/vite-plugin-cloudflare/playground/assets/__tests__/assets.spec.ts +++ b/packages/vite-plugin-cloudflare/playground/assets/__tests__/assets.spec.ts @@ -1,34 +1,6 @@ import { expect, test } from "vitest"; -import { - getResponse, - getTextResponse, - isBuild, - page, - viteTestUrl, -} from "../../__test-utils__"; - -test("fetches public directory asset", async () => { - const response = await getResponse("/public-directory-asset"); - const contentType = await response.headerValue("content-type"); - const additionalHeader = await response.headerValue("additional-header"); - expect(contentType).toBe("image/svg+xml"); - expect(additionalHeader).toBe("public-directory-asset"); -}); - -// TODO: enable build test when assets are copied to client output directory -test.skipIf(isBuild)("fetches imported asset", async () => { - const response = await getResponse("/imported-asset"); - const contentType = await response.headerValue("content-type"); - const additionalHeader = await response.headerValue("additional-header"); - expect(contentType).toBe("image/svg+xml"); - expect(additionalHeader).toBe("imported-asset"); -}); - -// TODO: enable build test when assets are copied to client output directory -test.skipIf(isBuild)("fetches imported asset with url suffix", async () => { - const text = await getTextResponse("/imported-asset-url-suffix"); - expect(text).toBe(`The text content is "Text content"`); -}); +import { page, viteTestUrl } from "../../__test-utils__"; +import "./base-tests"; test("fetches transformed HTML asset", async () => { await page.goto(`${viteTestUrl}/transformed-html-asset`); diff --git a/packages/vite-plugin-cloudflare/playground/assets/__tests__/base-tests.ts b/packages/vite-plugin-cloudflare/playground/assets/__tests__/base-tests.ts new file mode 100644 index 0000000000..570f8950ce --- /dev/null +++ b/packages/vite-plugin-cloudflare/playground/assets/__tests__/base-tests.ts @@ -0,0 +1,31 @@ +import { expect, test } from "vitest"; +import { getResponse, getTextResponse } from "../../__test-utils__"; + +test("fetches public directory asset", async () => { + const response = await getResponse("/public-directory-asset"); + const contentType = await response.headerValue("content-type"); + const additionalHeader = await response.headerValue("additional-header"); + expect(contentType).toBe("image/svg+xml"); + expect(additionalHeader).toBe("public-directory-asset"); +}); + +test("fetches imported asset", async () => { + const response = await getResponse("/imported-asset"); + const contentType = await response.headerValue("content-type"); + const additionalHeader = await response.headerValue("additional-header"); + expect(contentType).toBe("image/svg+xml"); + expect(additionalHeader).toBe("imported-asset"); +}); + +test("fetches imported asset with url suffix", async () => { + const text = await getTextResponse("/imported-asset-url-suffix"); + expect(text).toBe(`The text content is "Text content"`); +}); + +test("fetches inline asset", async () => { + const response = await getResponse("/inline-asset"); + const contentType = await response.headerValue("content-type"); + const additionalHeader = await response.headerValue("additional-header"); + expect(contentType).toBe("image/svg+xml"); + expect(additionalHeader).toBe("inline-asset"); +}); diff --git a/packages/vite-plugin-cloudflare/playground/assets/__tests__/no-client-entry/assets.spec.ts b/packages/vite-plugin-cloudflare/playground/assets/__tests__/no-client-entry/assets.spec.ts new file mode 100644 index 0000000000..14a7605a1c --- /dev/null +++ b/packages/vite-plugin-cloudflare/playground/assets/__tests__/no-client-entry/assets.spec.ts @@ -0,0 +1,18 @@ +import * as fs from "node:fs"; +import * as path from "node:path"; +import { expect, test, vi } from "vitest"; +import { isBuild, testDir } from "../../../__test-utils__"; +import "../base-tests"; + +test.runIf(isBuild)("deletes fallback client entry file", async () => { + const fallbackEntryPath = path.join( + testDir, + "dist", + "client", + "__cloudflare_fallback_entry__" + ); + + await vi.waitFor(() => { + expect(fs.existsSync(fallbackEntryPath)).toBe(false); + }); +}); diff --git a/packages/vite-plugin-cloudflare/playground/assets/__tests__/public-dir-only/assets.spec.ts b/packages/vite-plugin-cloudflare/playground/assets/__tests__/public-dir-only/assets.spec.ts new file mode 100644 index 0000000000..170a161e3c --- /dev/null +++ b/packages/vite-plugin-cloudflare/playground/assets/__tests__/public-dir-only/assets.spec.ts @@ -0,0 +1,22 @@ +import * as fs from "node:fs"; +import * as path from "node:path"; +import { expect, test, vi } from "vitest"; +import { getResponse, isBuild, testDir } from "../../../__test-utils__"; + +test("fetches public directory asset", async () => { + const response = await getResponse("/public-image.svg"); + const contentType = await response.headerValue("content-type"); + expect(contentType).toBe("image/svg+xml"); +}); + +test.runIf(isBuild)("deletes fallback client entry file", async () => { + const fallbackEntryPath = path.join( + testDir, + "dist", + "__cloudflare_fallback_entry__" + ); + + await vi.waitFor(() => { + expect(fs.existsSync(fallbackEntryPath)).toBe(false); + }); +}); diff --git a/packages/vite-plugin-cloudflare/playground/assets/html-page.html b/packages/vite-plugin-cloudflare/playground/assets/html-page.html index 7aa83ca572..6b20da9aa2 100644 --- a/packages/vite-plugin-cloudflare/playground/assets/html-page.html +++ b/packages/vite-plugin-cloudflare/playground/assets/html-page.html @@ -1,7 +1,9 @@ -
-diff --git a/packages/vite-plugin-cloudflare/playground/assets/src/index.ts b/packages/vite-plugin-cloudflare/playground/assets/src/index.ts index b823fcb121..b0e20c6569 100644 --- a/packages/vite-plugin-cloudflare/playground/assets/src/index.ts +++ b/packages/vite-plugin-cloudflare/playground/assets/src/index.ts @@ -1,6 +1,7 @@ import html from "./index.html?raw"; import importedImage from "./imported-image.svg"; import importedText from "./imported-text.txt?url"; +import inlineImage from "./inline-image.svg?inline"; interface Env { ASSETS: Fetcher; @@ -42,6 +43,13 @@ export default { return new Response(`The text content is "${textContent}"`); } + case "/inline-asset": { + const response = await env.ASSETS.fetch(new URL(inlineImage, origin)); + const modifiedResponse = new Response(response.body, response); + modifiedResponse.headers.append("additional-header", "inline-asset"); + + return modifiedResponse; + } case "/transformed-html-asset": { const response = await env.ASSETS.fetch(new URL("/html-page", origin)); diff --git a/packages/vite-plugin-cloudflare/playground/assets/src/inline-image.svg b/packages/vite-plugin-cloudflare/playground/assets/src/inline-image.svg new file mode 100644 index 0000000000..d9bc4c08d1 --- /dev/null +++ b/packages/vite-plugin-cloudflare/playground/assets/src/inline-image.svg @@ -0,0 +1,8 @@ + + + + diff --git a/packages/vite-plugin-cloudflare/playground/assets/tsconfig.node.json b/packages/vite-plugin-cloudflare/playground/assets/tsconfig.node.json index 773be9834a..da3546b786 100644 --- a/packages/vite-plugin-cloudflare/playground/assets/tsconfig.node.json +++ b/packages/vite-plugin-cloudflare/playground/assets/tsconfig.node.json @@ -1,4 +1,9 @@ { "extends": ["@cloudflare/workers-tsconfig/base.json"], - "include": ["vite.config.ts", "__tests__"] + "include": [ + "vite.config.ts", + "vite.config.no-client-entry.ts", + "vite.config.public-dir-only.ts", + "__tests__" + ] } diff --git a/packages/vite-plugin-cloudflare/playground/assets/vite.config.no-client-entry.ts b/packages/vite-plugin-cloudflare/playground/assets/vite.config.no-client-entry.ts new file mode 100644 index 0000000000..78add49e03 --- /dev/null +++ b/packages/vite-plugin-cloudflare/playground/assets/vite.config.no-client-entry.ts @@ -0,0 +1,13 @@ +import { cloudflare } from "@cloudflare/vite-plugin"; +import { defineConfig } from "vite"; + +export default defineConfig({ + environments: { + worker: { + build: { + assetsInlineLimit: 0, + }, + }, + }, + plugins: [cloudflare({ inspectorPort: false, persistState: false })], +}); diff --git a/packages/vite-plugin-cloudflare/playground/assets/vite.config.public-dir-only.ts b/packages/vite-plugin-cloudflare/playground/assets/vite.config.public-dir-only.ts new file mode 100644 index 0000000000..1c085feaed --- /dev/null +++ b/packages/vite-plugin-cloudflare/playground/assets/vite.config.public-dir-only.ts @@ -0,0 +1,19 @@ +import { cloudflare } from "@cloudflare/vite-plugin"; +import { defineConfig } from "vite"; + +export default defineConfig({ + environments: { + worker: { + build: { + assetsInlineLimit: 0, + }, + }, + }, + plugins: [ + cloudflare({ + configPath: "./wrangler.public-dir-only.jsonc", + inspectorPort: false, + persistState: false, + }), + ], +}); diff --git a/packages/vite-plugin-cloudflare/playground/assets/wrangler.public-dir-only.jsonc b/packages/vite-plugin-cloudflare/playground/assets/wrangler.public-dir-only.jsonc new file mode 100644 index 0000000000..e50c20d886 --- /dev/null +++ b/packages/vite-plugin-cloudflare/playground/assets/wrangler.public-dir-only.jsonc @@ -0,0 +1,4 @@ +{ + "name": "public-only", + "compatibility_date": "2024-12-30", +} diff --git a/packages/vite-plugin-cloudflare/playground/custom-build-app/__tests__/custom-build-app.spec.ts b/packages/vite-plugin-cloudflare/playground/custom-build-app/__tests__/custom-build-app.spec.ts index ae3fe7ba73..1a1921af06 100644 --- a/packages/vite-plugin-cloudflare/playground/custom-build-app/__tests__/custom-build-app.spec.ts +++ b/packages/vite-plugin-cloudflare/playground/custom-build-app/__tests__/custom-build-app.spec.ts @@ -1,8 +1,19 @@ import { expect, test } from "vitest"; -import { getTextResponse, isBuild, serverLogs } from "../../__test-utils__"; +import { + getTextResponse, + isBuild, + page, + serverLogs, +} from "../../__test-utils__"; -test("returns the correct response", async () => { - expect(await getTextResponse()).toEqual("Hello World!"); +test("returns the index.html page", async () => { + const content = await page.textContent("h1"); + expect(content).toBe("HTML page"); +}); + +test("returns the Worker response", async () => { + const response = await getTextResponse("/another-path"); + expect(response).toBe("Worker response"); }); test.runIf(isBuild)("runs a custom buildApp function", async () => { diff --git a/packages/vite-plugin-cloudflare/playground/custom-build-app/index.html b/packages/vite-plugin-cloudflare/playground/custom-build-app/index.html new file mode 100644 index 0000000000..218e981ef0 --- /dev/null +++ b/packages/vite-plugin-cloudflare/playground/custom-build-app/index.html @@ -0,0 +1,9 @@ + + +
+
+ +
+