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 @@ - - HTML page - - -

Original content

- + + + HTML page + + +

Original content

+ + diff --git a/packages/vite-plugin-cloudflare/playground/assets/package.json b/packages/vite-plugin-cloudflare/playground/assets/package.json index d7104c7e04..438c52c383 100644 --- a/packages/vite-plugin-cloudflare/playground/assets/package.json +++ b/packages/vite-plugin-cloudflare/playground/assets/package.json @@ -3,9 +3,13 @@ "private": true, "type": "module", "scripts": { - "build": "vite build --app", + "build": "vite build", + "build:no-client-entry": "vite build -c vite.config.no-client-entry.ts", + "build:public-dir-only": "vite build -c vite.config.public-dir-only.ts", "check:types": "tsc --build", "dev": "vite dev", + "dev:no-client-entry": "vite dev -c vite.config.no-client-entry.ts", + "dev:public-dir-only": "vite dev -c vite.config.public-dir-only.ts", "preview": "vite preview" }, "devDependencies": { diff --git a/packages/vite-plugin-cloudflare/playground/assets/src/index.html b/packages/vite-plugin-cloudflare/playground/assets/src/index.html index 901c90e343..0307a10b9b 100644 --- a/packages/vite-plugin-cloudflare/playground/assets/src/index.html +++ b/packages/vite-plugin-cloudflare/playground/assets/src/index.html @@ -11,6 +11,7 @@ >Imported asset with URL query param +
  • Inline asset
  • Transformed HTML asset
  • 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 @@ + + + + HTML page + + +

    HTML page

    + + diff --git a/packages/vite-plugin-cloudflare/playground/custom-build-app/src/index.ts b/packages/vite-plugin-cloudflare/playground/custom-build-app/src/index.ts index 1a54ac9e9b..22b35ee27a 100644 --- a/packages/vite-plugin-cloudflare/playground/custom-build-app/src/index.ts +++ b/packages/vite-plugin-cloudflare/playground/custom-build-app/src/index.ts @@ -1,5 +1,5 @@ export default { async fetch() { - return new Response("Hello World!"); + return new Response("Worker response"); }, } satisfies ExportedHandler; diff --git a/packages/vite-plugin-cloudflare/playground/custom-build-app/vite.config.ts b/packages/vite-plugin-cloudflare/playground/custom-build-app/vite.config.ts index 8f9f196363..5ffa6603be 100644 --- a/packages/vite-plugin-cloudflare/playground/custom-build-app/vite.config.ts +++ b/packages/vite-plugin-cloudflare/playground/custom-build-app/vite.config.ts @@ -1,3 +1,4 @@ +import assert from "node:assert"; import { cloudflare } from "@cloudflare/vite-plugin"; import { defineConfig } from "vite"; @@ -5,12 +6,19 @@ export default defineConfig({ builder: { async buildApp(builder) { const workerEnvironment = builder.environments.worker; + const clientEnvironment = builder.environments.client; - if (workerEnvironment) { - builder.config.logger.info("__before-build__"); - await builder.build(workerEnvironment); - builder.config.logger.info("__after-build__"); - } + assert(workerEnvironment, `No "worker" environment`); + assert(clientEnvironment, `No "client" environment`); + + builder.config.logger.info("__before-build__"); + await builder.build(workerEnvironment); + builder.config.logger.info("__after-build__"); + + await builder.build(clientEnvironment); + + // The output `wrangler.json` will always include an `assets` field so will fail to run if there is no client build. + // To build correctly without assets, a custom `buildApp` would need to remove this field. }, }, plugins: [cloudflare({ inspectorPort: false, persistState: false })], diff --git a/packages/vite-plugin-cloudflare/src/build.ts b/packages/vite-plugin-cloudflare/src/build.ts new file mode 100644 index 0000000000..23a37b7fca --- /dev/null +++ b/packages/vite-plugin-cloudflare/src/build.ts @@ -0,0 +1,172 @@ +import assert from "node:assert"; +import * as fs from "node:fs"; +import * as path from "node:path"; +import colors from "picocolors"; +import type { ResolvedPluginConfig } from "./plugin-config"; +import type * as vite from "vite"; +import type { Unstable_Config } from "wrangler"; + +export function createBuildApp( + resolvedPluginConfig: ResolvedPluginConfig +): (builder: vite.ViteBuilder) => Promise { + return async (builder) => { + const clientEnvironment = builder.environments.client; + assert(clientEnvironment, `No "client" environment`); + const defaultHtmlPath = path.resolve(builder.config.root, "index.html"); + const hasClientEntry = + clientEnvironment.config.build.rollupOptions.input || + fs.existsSync(defaultHtmlPath); + + if (resolvedPluginConfig.type === "assets-only") { + if (hasClientEntry) { + await builder.build(clientEnvironment); + } else if (getHasPublicAssets(builder.config)) { + await fallbackBuild(builder, clientEnvironment); + } + + // Return early as there are no Workers to build + return; + } + + const workerEnvironments = Object.keys(resolvedPluginConfig.workers).map( + (environmentName) => { + const environment = builder.environments[environmentName]; + assert(environment, `"${environmentName}" environment not found`); + + return environment; + } + ); + + await Promise.all( + workerEnvironments.map((environment) => builder.build(environment)) + ); + + const { entryWorkerEnvironmentName } = resolvedPluginConfig; + const entryWorkerEnvironment = + builder.environments[entryWorkerEnvironmentName]; + assert( + entryWorkerEnvironment, + `No "${entryWorkerEnvironmentName}" environment` + ); + const entryWorkerBuildDirectory = path.resolve( + builder.config.root, + entryWorkerEnvironment.config.build.outDir + ); + const entryWorkerManifest = loadViteManifest(entryWorkerBuildDirectory); + const importedAssetPaths = getImportedAssetPaths(entryWorkerManifest); + + if (hasClientEntry) { + await builder.build(clientEnvironment); + } else if (importedAssetPaths.size || getHasPublicAssets(builder.config)) { + await fallbackBuild(builder, clientEnvironment); + } else { + const entryWorkerConfigPath = path.join( + entryWorkerBuildDirectory, + "wrangler.json" + ); + const workerConfig = JSON.parse( + fs.readFileSync(entryWorkerConfigPath, "utf-8") + ) as Unstable_Config; + // Remove `assets` field as there are no assets + workerConfig.assets = undefined; + fs.writeFileSync(entryWorkerConfigPath, JSON.stringify(workerConfig)); + + // Return early as there is no client build + return; + } + + const clientBuildDirectory = path.resolve( + builder.config.root, + clientEnvironment.config.build.outDir + ); + const movedAssetPaths: string[] = []; + + // Move assets imported in the entry Worker to the client build + for (const assetPath of importedAssetPaths) { + const src = path.join(entryWorkerBuildDirectory, assetPath); + const dest = path.join(clientBuildDirectory, assetPath); + + if (!fs.existsSync(src)) { + continue; + } + + if (fs.existsSync(dest)) { + fs.unlinkSync(src); + } else { + const destDir = path.dirname(dest); + fs.mkdirSync(destDir, { recursive: true }); + fs.renameSync(src, dest); + movedAssetPaths.push(dest); + } + } + + if (movedAssetPaths.length) { + builder.config.logger.info( + [ + `${colors.green("✓")} ${movedAssetPaths.length} asset${movedAssetPaths.length > 1 ? "s" : ""} moved from "${entryWorkerEnvironmentName}" to "client" build output.`, + ...movedAssetPaths.map((assetPath) => + colors.dim(path.relative(builder.config.root, assetPath)) + ), + ].join("\n") + ); + } + }; +} + +function getHasPublicAssets({ publicDir }: vite.ResolvedConfig): boolean { + let hasPublicAssets = false; + + if (publicDir) { + try { + const files = fs.readdirSync(publicDir); + + if (files.length) { + hasPublicAssets = true; + } + } catch (error) {} + } + + return hasPublicAssets; +} + +async function fallbackBuild( + builder: vite.ViteBuilder, + environment: vite.BuildEnvironment +): Promise { + const fallbackEntryName = "__cloudflare_fallback_entry__"; + + environment.config.build.rollupOptions = { + input: "virtual:__cloudflare_fallback_entry__", + logLevel: "silent", + output: { + entryFileNames: fallbackEntryName, + }, + }; + + await builder.build(environment); + + const fallbackEntryPath = path.resolve( + builder.config.root, + environment.config.build.outDir, + fallbackEntryName + ); + + fs.unlinkSync(fallbackEntryPath); +} + +function loadViteManifest(directory: string) { + const contents = fs.readFileSync( + path.resolve(directory, ".vite", "manifest.json"), + "utf-8" + ); + + return JSON.parse(contents) as vite.Manifest; +} + +function getImportedAssetPaths(viteManifest: vite.Manifest): Set { + const assetPaths = Object.values(viteManifest).flatMap( + (chunk) => chunk.assets ?? [] + ); + + return new Set(assetPaths); +} diff --git a/packages/vite-plugin-cloudflare/src/cloudflare-environment.ts b/packages/vite-plugin-cloudflare/src/cloudflare-environment.ts index 8d0bb53ade..c242c2aa0c 100644 --- a/packages/vite-plugin-cloudflare/src/cloudflare-environment.ts +++ b/packages/vite-plugin-cloudflare/src/cloudflare-environment.ts @@ -133,7 +133,7 @@ const target = "es2022"; export function createCloudflareEnvironmentOptions( workerConfig: WorkerConfig, userConfig: vite.UserConfig, - environmentName: string + environment: { name: string; isEntry: boolean } ): vite.EnvironmentOptions { return { resolve: { @@ -155,16 +155,12 @@ export function createCloudflareEnvironmentOptions( return new vite.BuildEnvironment(name, config); }, target, - // We need to enable `emitAssets` in order to support additional modules defined by `rules` emitAssets: true, - outDir: getOutputDirectory(userConfig, environmentName), + manifest: environment.isEntry, + outDir: getOutputDirectory(userConfig, environment.name), copyPublicDir: false, ssr: true, rollupOptions: { - // Note: vite starts dev pre-bundling crawling from either optimizeDeps.entries or rollupOptions.input - // so the input value here serves both as the build input as well as the starting point for - // dev pre-bundling crawling (were we not to set this input field we'd have to appropriately set - // optimizeDeps.entries in the dev config) input: workerConfig.main, }, }, diff --git a/packages/vite-plugin-cloudflare/src/index.ts b/packages/vite-plugin-cloudflare/src/index.ts index b5f21fb82b..4f196beb25 100644 --- a/packages/vite-plugin-cloudflare/src/index.ts +++ b/packages/vite-plugin-cloudflare/src/index.ts @@ -13,6 +13,7 @@ import { matchAdditionalModule, } from "./additional-modules"; import { hasAssetsConfigChanged } from "./asset-config"; +import { createBuildApp } from "./build"; import { createCloudflareEnvironmentOptions, initRunners, @@ -85,9 +86,6 @@ export function cloudflare(pluginConfig: PluginConfig = {}): vite.Plugin[] { const nodeJsCompatWarningsMap = new Map(); - // This is set when the client environment is built to determine if the entry Worker should include assets - let hasClientBuild = false; - return [ { name: "vite-plugin-cloudflare", @@ -140,7 +138,13 @@ export function cloudflare(pluginConfig: PluginConfig = {}): vite.Plugin[] { createCloudflareEnvironmentOptions( workerConfig, userConfig, - environmentName + { + name: environmentName, + isEntry: + resolvedPluginConfig.type === "workers" && + environmentName === + resolvedPluginConfig.entryWorkerEnvironmentName, + } ), ]; } @@ -156,42 +160,7 @@ export function cloudflare(pluginConfig: PluginConfig = {}): vite.Plugin[] { builder: { buildApp: userConfig.builder?.buildApp ?? - (async (builder) => { - const clientEnvironment = builder.environments.client; - const defaultHtmlPath = path.resolve( - builder.config.root, - "index.html" - ); - - if ( - clientEnvironment && - (clientEnvironment.config.build.rollupOptions.input || - fs.existsSync(defaultHtmlPath)) - ) { - await builder.build(clientEnvironment); - } - - if (resolvedPluginConfig.type === "workers") { - const workerEnvironments = Object.keys( - resolvedPluginConfig.workers - ).map((environmentName) => { - const environment = builder.environments[environmentName]; - - assert( - environment, - `${environmentName} environment not found` - ); - - return environment; - }); - - await Promise.all( - workerEnvironments.map((environment) => - builder.build(environment) - ) - ); - } - }), + createBuildApp(resolvedPluginConfig), }, }; }, @@ -237,7 +206,7 @@ export function cloudflare(pluginConfig: PluginConfig = {}): vite.Plugin[] { this.environment.name === resolvedPluginConfig.entryWorkerEnvironmentName; - if (isEntryWorker && hasClientBuild) { + if (isEntryWorker) { const workerOutputDirectory = this.environment.config.build.outDir; const clientOutputDirectory = resolvedViteConfig.environments.client?.build.outDir; @@ -310,11 +279,6 @@ export function cloudflare(pluginConfig: PluginConfig = {}): vite.Plugin[] { }); }, writeBundle() { - // This relies on the assumption that the client environment is built first - // Composable `buildApp` hooks could provide a more robust alternative in future - if (this.environment.name === "client") { - hasClientBuild = true; - } // These conditions ensure the deploy config is emitted once per application build as `writeBundle` is called for each environment. // If Vite introduces an additional hook that runs after the application has built then we could use that instead. if ( @@ -458,6 +422,20 @@ export function cloudflare(pluginConfig: PluginConfig = {}): vite.Plugin[] { }); }, }, + // Plugin to provide a fallback entry file + { + name: "vite-plugin-cloudflare:fallback-entry", + resolveId(source) { + if (source === "virtual:__cloudflare_fallback_entry__") { + return `\0virtual:__cloudflare_fallback_entry__`; + } + }, + load(id) { + if (id === "\0virtual:__cloudflare_fallback_entry__") { + return ``; + } + }, + }, // Plugin to support `.wasm?init` extension { name: "vite-plugin-cloudflare:wasm-helper",