diff --git a/.changeset/major-snails-post.md b/.changeset/major-snails-post.md new file mode 100644 index 000000000000..785a16759b0a --- /dev/null +++ b/.changeset/major-snails-post.md @@ -0,0 +1,20 @@ +--- +"@cloudflare/vite-plugin": minor +--- + +Add support for child environments. + +This is to support React Server Components via [@vitejs/plugin-rsc](https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-rsc) and frameworks that build on top of it. A `childEnvironments` option is now added to the plugin config to enable using multiple environments within a single Worker. The parent environment can import modules from a child environment in order to access a separate module graph. For a typical RSC use case, the plugin might be configured as in the following example: + +```ts +export default defineConfig({ + plugins: [ + cloudflare({ + viteEnvironment: { + name: "rsc", + childEnvironments: ["ssr"], + }, + }), + ], +}); +``` diff --git a/packages/vite-plugin-cloudflare/playground/child-environment/__tests__/child-environment.spec.ts b/packages/vite-plugin-cloudflare/playground/child-environment/__tests__/child-environment.spec.ts new file mode 100644 index 000000000000..5bd72537ea28 --- /dev/null +++ b/packages/vite-plugin-cloudflare/playground/child-environment/__tests__/child-environment.spec.ts @@ -0,0 +1,7 @@ +import { expect, test } from "vitest"; +import { getTextResponse, isBuild } from "../../__test-utils__"; + +test.runIf(!isBuild)("can import module from child environment", async () => { + const response = await getTextResponse(); + expect(response).toBe("Hello from the child environment"); +}); diff --git a/packages/vite-plugin-cloudflare/playground/child-environment/package.json b/packages/vite-plugin-cloudflare/playground/child-environment/package.json new file mode 100644 index 000000000000..bc9f2685dd74 --- /dev/null +++ b/packages/vite-plugin-cloudflare/playground/child-environment/package.json @@ -0,0 +1,19 @@ +{ + "name": "@playground/child-environment", + "private": true, + "type": "module", + "scripts": { + "build": "vite build", + "check:type": "tsc --build", + "dev": "vite dev", + "preview": "vite preview" + }, + "devDependencies": { + "@cloudflare/vite-plugin": "workspace:*", + "@cloudflare/workers-tsconfig": "workspace:*", + "@cloudflare/workers-types": "catalog:default", + "typescript": "catalog:default", + "vite": "catalog:vite-plugin", + "wrangler": "workspace:*" + } +} diff --git a/packages/vite-plugin-cloudflare/playground/child-environment/src/child-environment-module.ts b/packages/vite-plugin-cloudflare/playground/child-environment/src/child-environment-module.ts new file mode 100644 index 000000000000..8d520baac4e1 --- /dev/null +++ b/packages/vite-plugin-cloudflare/playground/child-environment/src/child-environment-module.ts @@ -0,0 +1,6 @@ +// @ts-expect-error - no types +import { getEnvironmentName } from "virtual:environment-name"; + +export function getMessage() { + return `Hello from the ${getEnvironmentName()} environment`; +} diff --git a/packages/vite-plugin-cloudflare/playground/child-environment/src/index.ts b/packages/vite-plugin-cloudflare/playground/child-environment/src/index.ts new file mode 100644 index 000000000000..8d53086b3e79 --- /dev/null +++ b/packages/vite-plugin-cloudflare/playground/child-environment/src/index.ts @@ -0,0 +1,18 @@ +declare global { + // In real world usage, this is accessed by `@vitejs/plugin-rsc` + function __VITE_ENVIRONMENT_RUNNER_IMPORT__( + environmentName: string, + id: string + ): Promise; +} + +export default { + async fetch() { + const childEnvironmentModule = (await __VITE_ENVIRONMENT_RUNNER_IMPORT__( + "child", + "./src/child-environment-module" + )) as { getMessage: () => string }; + + return new Response(childEnvironmentModule.getMessage()); + }, +} satisfies ExportedHandler; diff --git a/packages/vite-plugin-cloudflare/playground/child-environment/tsconfig.json b/packages/vite-plugin-cloudflare/playground/child-environment/tsconfig.json new file mode 100644 index 000000000000..b52af703bdc2 --- /dev/null +++ b/packages/vite-plugin-cloudflare/playground/child-environment/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.node.json" }, + { "path": "./tsconfig.worker.json" } + ] +} diff --git a/packages/vite-plugin-cloudflare/playground/child-environment/tsconfig.node.json b/packages/vite-plugin-cloudflare/playground/child-environment/tsconfig.node.json new file mode 100644 index 000000000000..773be9834af5 --- /dev/null +++ b/packages/vite-plugin-cloudflare/playground/child-environment/tsconfig.node.json @@ -0,0 +1,4 @@ +{ + "extends": ["@cloudflare/workers-tsconfig/base.json"], + "include": ["vite.config.ts", "__tests__"] +} diff --git a/packages/vite-plugin-cloudflare/playground/child-environment/tsconfig.worker.json b/packages/vite-plugin-cloudflare/playground/child-environment/tsconfig.worker.json new file mode 100644 index 000000000000..da43778b826f --- /dev/null +++ b/packages/vite-plugin-cloudflare/playground/child-environment/tsconfig.worker.json @@ -0,0 +1,4 @@ +{ + "extends": ["@cloudflare/workers-tsconfig/worker.json"], + "include": ["src"] +} diff --git a/packages/vite-plugin-cloudflare/playground/child-environment/turbo.json b/packages/vite-plugin-cloudflare/playground/child-environment/turbo.json new file mode 100644 index 000000000000..6556dcf3e5e5 --- /dev/null +++ b/packages/vite-plugin-cloudflare/playground/child-environment/turbo.json @@ -0,0 +1,9 @@ +{ + "$schema": "http://turbo.build/schema.json", + "extends": ["//"], + "tasks": { + "build": { + "outputs": ["dist/**"] + } + } +} diff --git a/packages/vite-plugin-cloudflare/playground/child-environment/vite.config.ts b/packages/vite-plugin-cloudflare/playground/child-environment/vite.config.ts new file mode 100644 index 000000000000..6a7bc6827e7a --- /dev/null +++ b/packages/vite-plugin-cloudflare/playground/child-environment/vite.config.ts @@ -0,0 +1,28 @@ +import { cloudflare } from "@cloudflare/vite-plugin"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [ + cloudflare({ + inspectorPort: false, + persistState: false, + viteEnvironment: { + name: "parent", + childEnvironments: ["child"], + }, + }), + { + name: "virtual-module-plugin", + resolveId(source) { + if (source === "virtual:environment-name") { + return "\0virtual:environment-name"; + } + }, + load(id) { + if (id === "\0virtual:environment-name") { + return `export function getEnvironmentName() { return ${JSON.stringify(this.environment.name)} }`; + } + }, + }, + ], +}); diff --git a/packages/vite-plugin-cloudflare/playground/child-environment/wrangler.jsonc b/packages/vite-plugin-cloudflare/playground/child-environment/wrangler.jsonc new file mode 100644 index 000000000000..940fd1ab07d9 --- /dev/null +++ b/packages/vite-plugin-cloudflare/playground/child-environment/wrangler.jsonc @@ -0,0 +1,5 @@ +{ + "$schema": "./node_modules/wrangler/config-schema.json", + "name": "worker", + "main": "./src/index.ts", +} diff --git a/packages/vite-plugin-cloudflare/src/cloudflare-environment.ts b/packages/vite-plugin-cloudflare/src/cloudflare-environment.ts index 35d58fe4830b..db5b18b9292c 100644 --- a/packages/vite-plugin-cloudflare/src/cloudflare-environment.ts +++ b/packages/vite-plugin-cloudflare/src/cloudflare-environment.ts @@ -3,9 +3,11 @@ import { CoreHeaders } from "miniflare"; import * as vite from "vite"; import { additionalModuleRE } from "./plugins/additional-modules"; import { + ENVIRONMENT_NAME_HEADER, GET_EXPORT_TYPES_PATH, INIT_PATH, IS_ENTRY_WORKER_HEADER, + IS_PARENT_ENVIRONMENT_HEADER, UNKNOWN_HOST, VIRTUAL_WORKER_ENTRY, WORKER_ENTRY_PATH_HEADER, @@ -97,7 +99,7 @@ export class CloudflareDevEnvironment extends vite.DevEnvironment { async initRunner( miniflare: Miniflare, workerConfig: ResolvedWorkerConfig, - isEntryWorker: boolean + options: { isEntryWorker: boolean; isParentEnvironment: boolean } ): Promise { const response = await miniflare.dispatchFetch( new URL(INIT_PATH, UNKNOWN_HOST), @@ -105,7 +107,9 @@ export class CloudflareDevEnvironment extends vite.DevEnvironment { headers: { [CoreHeaders.ROUTE_OVERRIDE]: workerConfig.name, [WORKER_ENTRY_PATH_HEADER]: encodeURIComponent(workerConfig.main), - [IS_ENTRY_WORKER_HEADER]: String(isEntryWorker), + [IS_ENTRY_WORKER_HEADER]: String(options.isEntryWorker), + [ENVIRONMENT_NAME_HEADER]: this.name, + [IS_PARENT_ENVIRONMENT_HEADER]: String(options.isParentEnvironment), upgrade: "websocket", }, } @@ -170,14 +174,6 @@ const defaultConditions = ["workerd", "worker", "module", "browser"]; // workerd uses [v8 version 14.2 as of 2025-10-17](https://developers.cloudflare.com/workers/platform/changelog/#2025-10-17) const target = "es2024"; -const rollupOptions: vite.Rollup.RollupOptions = { - input: { - [MAIN_ENTRY_NAME]: VIRTUAL_WORKER_ENTRY, - }, - // workerd checks the types of the exports so we need to ensure that additional exports are not added to the entry module - preserveEntrySignatures: "strict", -}; - // TODO: consider removing in next major to use default extensions const resolveExtensions = [ ".mjs", @@ -198,6 +194,7 @@ export function createCloudflareEnvironmentOptions({ mode, environmentName, isEntryWorker, + isParentEnvironment, hasNodeJsCompat, }: { workerConfig: ResolvedWorkerConfig; @@ -205,8 +202,18 @@ export function createCloudflareEnvironmentOptions({ mode: vite.ConfigEnv["mode"]; environmentName: string; isEntryWorker: boolean; + isParentEnvironment: boolean; hasNodeJsCompat: boolean; }): vite.EnvironmentOptions { + const rollupOptions: vite.Rollup.RollupOptions = isParentEnvironment + ? { + input: { + [MAIN_ENTRY_NAME]: VIRTUAL_WORKER_ENTRY, + }, + // workerd checks the types of the exports so we need to ensure that additional exports are not added to the entry module + preserveEntrySignatures: "strict", + } + : {}; const define = getProcessEnvReplacements(hasNodeJsCompat, mode); return { @@ -323,19 +330,39 @@ export function initRunners( viteDevServer: vite.ViteDevServer, miniflare: Miniflare ): Promise | undefined { - return Promise.all( - [...resolvedPluginConfig.environmentNameToWorkerMap].map( - ([environmentName, worker]) => { - debuglog("Initializing worker:", worker.config.name); - const isEntryWorker = - environmentName === resolvedPluginConfig.entryWorkerEnvironmentName; - - return ( - viteDevServer.environments[ - environmentName - ] as CloudflareDevEnvironment - ).initRunner(miniflare, worker.config, isEntryWorker); - } - ) - ); + const initPromises = [ + ...resolvedPluginConfig.environmentNameToWorkerMap, + ].flatMap(([environmentName, worker]) => { + debuglog("Initializing worker:", worker.config.name); + const isEntryWorker = + environmentName === resolvedPluginConfig.entryWorkerEnvironmentName; + + const childEnvironmentNames = + resolvedPluginConfig.environmentNameToChildEnvironmentNamesMap.get( + environmentName + ) ?? []; + + const parentInit = ( + viteDevServer.environments[environmentName] as CloudflareDevEnvironment + ).initRunner(miniflare, worker.config, { + isEntryWorker, + isParentEnvironment: true, + }); + + const childInits = childEnvironmentNames.map((childEnvironmentName) => { + debuglog("Initializing child environment:", childEnvironmentName); + return ( + viteDevServer.environments[ + childEnvironmentName + ] as CloudflareDevEnvironment + ).initRunner(miniflare, worker.config, { + isEntryWorker: false, + isParentEnvironment: false, + }); + }); + + return [parentInit, ...childInits]; + }); + + return Promise.all(initPromises); } diff --git a/packages/vite-plugin-cloudflare/src/context.ts b/packages/vite-plugin-cloudflare/src/context.ts index fec784f6856d..9464e6af1d78 100644 --- a/packages/vite-plugin-cloudflare/src/context.ts +++ b/packages/vite-plugin-cloudflare/src/context.ts @@ -9,6 +9,7 @@ import type { PreviewResolvedConfig, ResolvedPluginConfig, ResolvedWorkerConfig, + Worker, WorkersResolvedConfig, } from "./plugin-config"; import type { MiniflareOptions } from "miniflare"; @@ -141,12 +142,47 @@ export class PluginContext { return this.#resolvedViteConfig; } + isChildEnvironment(environmentName: string): boolean { + if (this.resolvedPluginConfig.type !== "workers") { + return false; + } + + for (const childEnvironmentNames of this.resolvedPluginConfig.environmentNameToChildEnvironmentNamesMap.values()) { + if (childEnvironmentNames.includes(environmentName)) { + return true; + } + } + + return false; + } + + #getWorker(environmentName: string): Worker | undefined { + if (this.resolvedPluginConfig.type !== "workers") { + return undefined; + } + + const worker = + this.resolvedPluginConfig.environmentNameToWorkerMap.get(environmentName); + + if (worker) { + return worker; + } + + // Check if this is a child environment and, if so, return the parent's Worker + for (const [parentEnvironmentName, childEnvironmentNames] of this + .resolvedPluginConfig.environmentNameToChildEnvironmentNamesMap) { + if (childEnvironmentNames.includes(environmentName)) { + return this.resolvedPluginConfig.environmentNameToWorkerMap.get( + parentEnvironmentName + ); + } + } + + return undefined; + } + getWorkerConfig(environmentName: string): ResolvedWorkerConfig | undefined { - return this.resolvedPluginConfig.type === "workers" - ? this.resolvedPluginConfig.environmentNameToWorkerMap.get( - environmentName - )?.config - : undefined; + return this.#getWorker(environmentName)?.config; } get allWorkerConfigs(): Unstable_Config[] { @@ -173,11 +209,7 @@ export class PluginContext { } getNodeJsCompat(environmentName: string): NodeJsCompat | undefined { - return this.resolvedPluginConfig.type === "workers" - ? this.resolvedPluginConfig.environmentNameToWorkerMap.get( - environmentName - )?.nodeJsCompat - : undefined; + return this.#getWorker(environmentName)?.nodeJsCompat; } } diff --git a/packages/vite-plugin-cloudflare/src/miniflare-options.ts b/packages/vite-plugin-cloudflare/src/miniflare-options.ts index 59f927ad9015..1405558bb157 100644 --- a/packages/vite-plugin-cloudflare/src/miniflare-options.ts +++ b/packages/vite-plugin-cloudflare/src/miniflare-options.ts @@ -26,6 +26,7 @@ import { import { getContainerOptions, getDockerPath } from "./containers"; import { getInputInspectorPort } from "./debug"; import { additionalModuleRE } from "./plugins/additional-modules"; +import { ENVIRONMENT_NAME_HEADER } from "./shared"; import { withTrailingSlash } from "./utils"; import type { CloudflareDevEnvironment } from "./cloudflare-environment"; import type { ContainerTagToOptionsMap } from "./containers"; @@ -381,10 +382,17 @@ export async function getDevMiniflareOptions( } : {}), __VITE_INVOKE_MODULE__: async (request) => { + const targetEnvironmentName = request.headers.get( + ENVIRONMENT_NAME_HEADER + ); + assert( + targetEnvironmentName, + `Expected ${ENVIRONMENT_NAME_HEADER} header` + ); const payload = (await request.json()) as vite.CustomPayload; const devEnvironment = viteDevServer.environments[ - environmentName + targetEnvironmentName ] as CloudflareDevEnvironment; const result = await devEnvironment.hot.handleInvoke(payload); diff --git a/packages/vite-plugin-cloudflare/src/plugin-config.ts b/packages/vite-plugin-cloudflare/src/plugin-config.ts index 3f28cb870629..6f3fec2016aa 100644 --- a/packages/vite-plugin-cloudflare/src/plugin-config.ts +++ b/packages/vite-plugin-cloudflare/src/plugin-config.ts @@ -25,7 +25,7 @@ import type { Unstable_Config } from "wrangler"; export type PersistState = boolean | { path: string }; interface BaseWorkerConfig { - viteEnvironment?: { name?: string }; + viteEnvironment?: { name?: string; childEnvironments?: string[] }; } interface EntryWorkerConfig extends BaseWorkerConfig { @@ -112,6 +112,7 @@ export interface WorkersResolvedConfig extends BaseResolvedConfig { configPaths: Set; cloudflareEnv: string | undefined; environmentNameToWorkerMap: Map; + environmentNameToChildEnvironmentNamesMap: Map; entryWorkerEnvironmentName: string; staticRouting: StaticRouting | undefined; rawConfigs: { @@ -338,6 +339,18 @@ export function resolvePluginConfig( [entryWorkerEnvironmentName, resolveWorker(entryWorkerConfig)], ]); + const environmentNameToChildEnvironmentNamesMap = new Map(); + + const entryWorkerChildEnvironments = + pluginConfig.viteEnvironment?.childEnvironments; + + if (entryWorkerChildEnvironments) { + environmentNameToChildEnvironmentNamesMap.set( + entryWorkerEnvironmentName, + entryWorkerChildEnvironments + ); + } + const auxiliaryWorkersResolvedConfigs: WorkerResolvedConfig[] = []; for (const auxiliaryWorker of pluginConfig.auxiliaryWorkers ?? []) { @@ -374,6 +387,16 @@ export function resolvePluginConfig( workerEnvironmentName, resolveWorker(workerResolvedConfig.config as ResolvedWorkerConfig) ); + + const auxiliaryWorkerChildEnvironments = + auxiliaryWorker.viteEnvironment?.childEnvironments; + + if (auxiliaryWorkerChildEnvironments) { + environmentNameToChildEnvironmentNamesMap.set( + workerEnvironmentName, + auxiliaryWorkerChildEnvironments + ); + } } return { @@ -382,6 +405,7 @@ export function resolvePluginConfig( cloudflareEnv, configPaths, environmentNameToWorkerMap, + environmentNameToChildEnvironmentNamesMap, entryWorkerEnvironmentName, staticRouting, remoteBindings: pluginConfig.remoteBindings ?? true, diff --git a/packages/vite-plugin-cloudflare/src/plugins/config.ts b/packages/vite-plugin-cloudflare/src/plugins/config.ts index 3b436e29959c..9f47ce24c51b 100644 --- a/packages/vite-plugin-cloudflare/src/plugins/config.ts +++ b/packages/vite-plugin-cloudflare/src/plugins/config.ts @@ -11,6 +11,8 @@ import { hasLocalDevVarsFileChanged } from "../dev-vars"; import { createPlugin, debuglog, getOutputDirectory } from "../utils"; import { validateWorkerEnvironmentOptions } from "../vite-config"; import { getWarningForWorkersConfigs } from "../workers-configs"; +import type { PluginContext } from "../context"; +import type { EnvironmentOptions, UserConfig } from "vite"; /** * Plugin to handle configuration and config file watching @@ -46,43 +48,7 @@ export const configPlugin = createPlugin("config", (ctx) => { deny: [...defaultDeniedFiles, ".dev.vars", ".dev.vars.*"], }, }, - environments: - ctx.resolvedPluginConfig.type === "workers" - ? { - ...Object.fromEntries( - [...ctx.resolvedPluginConfig.environmentNameToWorkerMap].map( - ([environmentName, worker]) => { - return [ - environmentName, - createCloudflareEnvironmentOptions({ - workerConfig: worker.config, - userConfig, - mode: env.mode, - environmentName, - isEntryWorker: - ctx.resolvedPluginConfig.type === "workers" && - environmentName === - ctx.resolvedPluginConfig - .entryWorkerEnvironmentName, - hasNodeJsCompat: - ctx.getNodeJsCompat(environmentName) !== undefined, - }), - ]; - } - ) - ), - client: { - build: { - outDir: getOutputDirectory(userConfig, "client"), - }, - optimizeDeps: { - // Some frameworks allow users to mix client and server code in the same file and then extract the server code. - // As the dependency optimization may happen before the server code is extracted, we should exclude Cloudflare built-ins from client optimization. - exclude: [...cloudflareBuiltInModules], - }, - }, - } - : undefined, + environments: getEnvironmentsConfig(ctx, userConfig, env.mode), builder: { buildApp: userConfig.builder?.buildApp ?? @@ -171,3 +137,76 @@ export const configPlugin = createPlugin("config", (ctx) => { }, }; }); + +/** + * Generates the environment configuration for all Worker environments. + */ +function getEnvironmentsConfig( + ctx: PluginContext, + userConfig: UserConfig, + mode: string +): Record | undefined { + if (ctx.resolvedPluginConfig.type !== "workers") { + return undefined; + } + + const workersConfig = ctx.resolvedPluginConfig; + + const workerEnvironments = Object.fromEntries( + [...workersConfig.environmentNameToWorkerMap].flatMap( + ([environmentName, worker]) => { + const childEnvironmentNames = + workersConfig.environmentNameToChildEnvironmentNamesMap.get( + environmentName + ) ?? []; + + const sharedOptions = { + workerConfig: worker.config, + userConfig, + mode, + hasNodeJsCompat: ctx.getNodeJsCompat(environmentName) !== undefined, + }; + + const parentConfig = [ + environmentName, + createCloudflareEnvironmentOptions({ + ...sharedOptions, + environmentName, + isEntryWorker: + environmentName === workersConfig.entryWorkerEnvironmentName, + isParentEnvironment: true, + }), + ] as const; + + const childConfigs = childEnvironmentNames.map( + (childEnvironmentName) => + [ + childEnvironmentName, + createCloudflareEnvironmentOptions({ + ...sharedOptions, + environmentName: childEnvironmentName, + isEntryWorker: false, + isParentEnvironment: false, + }), + ] as const + ); + + return [parentConfig, ...childConfigs]; + } + ) + ); + + return { + ...workerEnvironments, + client: { + build: { + outDir: getOutputDirectory(userConfig, "client"), + }, + optimizeDeps: { + // Some frameworks allow users to mix client and server code in the same file and then extract the server code. + // As the dependency optimization may happen before the server code is extracted, we should exclude Cloudflare built-ins from client optimization. + exclude: [...cloudflareBuiltInModules], + }, + }, + }; +} diff --git a/packages/vite-plugin-cloudflare/src/plugins/output-config.ts b/packages/vite-plugin-cloudflare/src/plugins/output-config.ts index 9477a3c2437c..47ca92da1596 100644 --- a/packages/vite-plugin-cloudflare/src/plugins/output-config.ts +++ b/packages/vite-plugin-cloudflare/src/plugins/output-config.ts @@ -16,6 +16,11 @@ export const outputConfigPlugin = createPlugin("output-config", (ctx) => { generateBundle(_, bundle) { assertIsNotPreview(ctx); + // Child environments should not emit wrangler.json or .dev.vars files + if (ctx.isChildEnvironment(this.environment.name)) { + return; + } + let outputConfig: Unstable_RawConfig | undefined; if (ctx.resolvedPluginConfig.type === "workers") { diff --git a/packages/vite-plugin-cloudflare/src/plugins/virtual-modules.ts b/packages/vite-plugin-cloudflare/src/plugins/virtual-modules.ts index c091392b68ed..c0ab2d9efd7e 100644 --- a/packages/vite-plugin-cloudflare/src/plugins/virtual-modules.ts +++ b/packages/vite-plugin-cloudflare/src/plugins/virtual-modules.ts @@ -15,7 +15,10 @@ export const VIRTUAL_CLIENT_FALLBACK_ENTRY = `${virtualPrefix}client-fallback-en export const virtualModulesPlugin = createPlugin("virtual-modules", (ctx) => { return { applyToEnvironment(environment) { - return ctx.getWorkerConfig(environment.name) !== undefined; + return ( + !ctx.isChildEnvironment(environment.name) && + ctx.getWorkerConfig(environment.name) !== undefined + ); }, async resolveId(source) { if (source === VIRTUAL_WORKER_ENTRY || source === VIRTUAL_EXPORT_TYPES) { diff --git a/packages/vite-plugin-cloudflare/src/shared.ts b/packages/vite-plugin-cloudflare/src/shared.ts index c295f551848f..489d11a4da01 100644 --- a/packages/vite-plugin-cloudflare/src/shared.ts +++ b/packages/vite-plugin-cloudflare/src/shared.ts @@ -8,6 +8,8 @@ export const GET_EXPORT_TYPES_PATH = // headers export const WORKER_ENTRY_PATH_HEADER = "__VITE_WORKER_ENTRY_PATH__"; export const IS_ENTRY_WORKER_HEADER = "__VITE_IS_ENTRY_WORKER__"; +export const ENVIRONMENT_NAME_HEADER = "__VITE_ENVIRONMENT_NAME__"; +export const IS_PARENT_ENVIRONMENT_HEADER = "__VITE_IS_PARENT_ENVIRONMENT__"; // virtual modules export const virtualPrefix = "virtual:cloudflare/"; diff --git a/packages/vite-plugin-cloudflare/src/workers/runner-worker/index.ts b/packages/vite-plugin-cloudflare/src/workers/runner-worker/index.ts index 3703f9733d98..4bb4962c63a3 100644 --- a/packages/vite-plugin-cloudflare/src/workers/runner-worker/index.ts +++ b/packages/vite-plugin-cloudflare/src/workers/runner-worker/index.ts @@ -7,6 +7,7 @@ import { GET_EXPORT_TYPES_PATH, INIT_PATH, IS_ENTRY_WORKER_HEADER, + IS_PARENT_ENVIRONMENT_HEADER, WORKER_ENTRY_PATH_HEADER, } from "../../shared"; import { stripInternalEnv } from "./env"; @@ -213,29 +214,36 @@ export function createWorkerEntrypointWrapper( // Initialize the module runner if (url.pathname === INIT_PATH) { - const workerEntryPathHeader = request.headers.get( - WORKER_ENTRY_PATH_HEADER + const isParentEnvironmentHeader = request.headers.get( + IS_PARENT_ENVIRONMENT_HEADER ); - if (!workerEntryPathHeader) { - throw new Error( - `Unexpected error: "${WORKER_ENTRY_PATH_HEADER}" header not set.` + // Only set Worker variables when initializing the parent environment + if (isParentEnvironmentHeader === "true") { + const workerEntryPathHeader = request.headers.get( + WORKER_ENTRY_PATH_HEADER ); - } - const isEntryWorkerHeader = request.headers.get( - IS_ENTRY_WORKER_HEADER - ); + if (!workerEntryPathHeader) { + throw new Error( + `Unexpected error: "${WORKER_ENTRY_PATH_HEADER}" header not set.` + ); + } - if (!isEntryWorkerHeader) { - throw new Error( - `Unexpected error: "${IS_ENTRY_WORKER_HEADER}" header not set.` + const isEntryWorkerHeader = request.headers.get( + IS_ENTRY_WORKER_HEADER ); + + if (!isEntryWorkerHeader) { + throw new Error( + `Unexpected error: "${IS_ENTRY_WORKER_HEADER}" header not set.` + ); + } + + workerEntryPath = decodeURIComponent(workerEntryPathHeader); + isEntryWorker = isEntryWorkerHeader === "true"; } - // Set the Worker entry path - workerEntryPath = decodeURIComponent(workerEntryPathHeader); - isEntryWorker = isEntryWorkerHeader === "true"; const stub = this.env.__VITE_RUNNER_OBJECT__.get("singleton"); // Forward the request to the Durable Object to initialize the module runner and return the WebSocket diff --git a/packages/vite-plugin-cloudflare/src/workers/runner-worker/module-runner.ts b/packages/vite-plugin-cloudflare/src/workers/runner-worker/module-runner.ts index e0b707fb6f02..c5933aaa73cd 100644 --- a/packages/vite-plugin-cloudflare/src/workers/runner-worker/module-runner.ts +++ b/packages/vite-plugin-cloudflare/src/workers/runner-worker/module-runner.ts @@ -1,7 +1,9 @@ import { DurableObject } from "cloudflare:workers"; import { ModuleRunner, ssrModuleExportsKey } from "vite/module-runner"; import { + ENVIRONMENT_NAME_HEADER, INIT_PATH, + IS_PARENT_ENVIRONMENT_HEADER, UNKNOWN_HOST, VIRTUAL_EXPORT_TYPES, VIRTUAL_WORKER_ENTRY, @@ -14,6 +16,14 @@ import type { ModuleRunnerOptions, } from "vite/module-runner"; +declare global { + // This global variable is accessed by `@vitejs/plugin-rsc` + var __VITE_ENVIRONMENT_RUNNER_IMPORT__: ( + environmentName: string, + id: string + ) => Promise; +} + /** * Custom `ModuleRunner`. * The `cachedModule` method is overridden to ensure compatibility with the Workers runtime. @@ -21,21 +31,28 @@ import type { // @ts-expect-error: `cachedModule` is private class CustomModuleRunner extends ModuleRunner { #env: WrapperEnv; + #environmentName: string; constructor( options: ModuleRunnerOptions, evaluator: ModuleEvaluator, - env: WrapperEnv + env: WrapperEnv, + environmentName: string ) { super(options, evaluator); this.#env = env; + this.#environmentName = environmentName; } override async cachedModule( url: string, importer?: string ): Promise { const stub = this.#env.__VITE_RUNNER_OBJECT__.get("singleton"); - const moduleId = await stub.getFetchedModuleId(url, importer); + const moduleId = await stub.getFetchedModuleId( + this.#environmentName, + url, + importer + ); const module = this.evaluatedModules.getModuleById(moduleId); if (!module) { @@ -46,22 +63,26 @@ class CustomModuleRunner extends ModuleRunner { } } -/** Module runner instance */ -let moduleRunner: CustomModuleRunner | undefined; +/** Module runner instances keyed by environment name */ +const moduleRunners = new Map(); + +/** The parent environment name (set explicitly via IS_PARENT_ENVIRONMENT_HEADER) */ +let parentEnvironmentName: string | undefined; + +interface EnvironmentState { + webSocket: WebSocket; + concurrentModuleNodePromises: Map>; +} /** * Durable Object that creates the module runner and handles WebSocket communication with the Vite dev server. */ export class __VITE_RUNNER_OBJECT__ extends DurableObject { - /** WebSocket connection to the Vite dev server */ - #webSocket?: WebSocket; - #concurrentModuleNodePromises = new Map< - string, - Promise - >(); + /** Per-environment state containing WebSocket and concurrent module node promises */ + #environments = new Map(); /** - * Handles fetch requests to initialize the module runner. + * Handles fetch requests to initialize a module runner for an environment. * Creates a WebSocket pair for communication with the Vite dev server and initializes the ModuleRunner. * @param request - The incoming fetch request * @returns Response with WebSocket @@ -76,45 +97,91 @@ export class __VITE_RUNNER_OBJECT__ extends DurableObject { ); } - if (moduleRunner) { - throw new Error(`Module runner already initialized`); + const environmentName = request.headers.get(ENVIRONMENT_NAME_HEADER); + + if (!environmentName) { + throw new Error( + `__VITE_RUNNER_OBJECT__ received request without ${ENVIRONMENT_NAME_HEADER} header` + ); + } + + if (moduleRunners.has(environmentName)) { + throw new Error( + `Module runner already initialized for environment: ${environmentName}` + ); + } + + const isParentEnvironment = + request.headers.get(IS_PARENT_ENVIRONMENT_HEADER) === "true"; + + if (isParentEnvironment) { + parentEnvironmentName = environmentName; } const { 0: client, 1: server } = new WebSocketPair(); server.accept(); - this.#webSocket = server; - moduleRunner = await createModuleRunner(this.env, this.#webSocket); + + const environmentState: EnvironmentState = { + webSocket: server, + concurrentModuleNodePromises: new Map(), + }; + this.#environments.set(environmentName, environmentState); + + const moduleRunner = await createModuleRunner( + this.env, + environmentState.webSocket, + environmentName + ); + moduleRunners.set(environmentName, moduleRunner); return new Response(null, { status: 101, webSocket: client }); } /** - * Sends data to the Vite dev server via the WebSocket. + * Sends data to the Vite dev server via the WebSocket for a specific environment. + * @param environmentName - The environment name * @param data - The data to send as a string * @throws Error if the WebSocket is not initialized */ - send(data: string): void { - if (!this.#webSocket) { - throw new Error(`Module runner WebSocket not initialized`); + send(environmentName: string, data: string): void { + const environmentState = this.#environments.get(environmentName); + + if (!environmentState) { + throw new Error( + `Module runner WebSocket not initialized for environment: ${environmentName}` + ); } - this.#webSocket.send(data); + environmentState.webSocket.send(data); } /** * Based on the implementation of `cachedModule` from Vite's `ModuleRunner`. * Running this in the DO enables us to share promises across invocations. + * @param environmentName - The environment name * @param url - The module URL * @param importer - The module's importer * @returns The ID of the fetched module */ async getFetchedModuleId( + environmentName: string, url: string, importer: string | undefined ): Promise { + const moduleRunner = moduleRunners.get(environmentName); + if (!moduleRunner) { - throw new Error(`Module runner not initialized`); + throw new Error( + `Module runner not initialized for environment: ${environmentName}` + ); + } + + const environmentState = this.#environments.get(environmentName); + if (!environmentState) { + throw new Error( + `Environment state not found for environment: ${environmentName}` + ); } - let cached = this.#concurrentModuleNodePromises.get(url); + let cached = environmentState.concurrentModuleNodePromises.get(url); if (!cached) { const cachedModule = moduleRunner.evaluatedModules.getModuleByUrl(url); @@ -122,9 +189,9 @@ export class __VITE_RUNNER_OBJECT__ extends DurableObject { // @ts-expect-error: `getModuleInformation` is private .getModuleInformation(url, importer, cachedModule) .finally(() => { - this.#concurrentModuleNodePromises.delete(url); + environmentState.concurrentModuleNodePromises.delete(url); }) as Promise; - this.#concurrentModuleNodePromises.set(url, cached); + environmentState.concurrentModuleNodePromises.set(url, cached); } else { // @ts-expect-error: `debug` is private moduleRunner.debug?.("[module runner] using cached module info for", url); @@ -140,9 +207,14 @@ export class __VITE_RUNNER_OBJECT__ extends DurableObject { * Creates a new module runner instance with a WebSocket transport. * @param env - The wrapper env * @param webSocket - WebSocket connection for communication with Vite dev server + * @param environmentName - The name of the environment this runner is for * @returns Configured module runner instance */ -async function createModuleRunner(env: WrapperEnv, webSocket: WebSocket) { +async function createModuleRunner( + env: WrapperEnv, + webSocket: WebSocket, + environmentName: string +) { return new CustomModuleRunner( { sourcemapInterceptor: "prepareStackTrace", @@ -166,12 +238,15 @@ async function createModuleRunner(env: WrapperEnv, webSocket: WebSocket) { // This is because `import.meta.send` may be called within a Worker's request context. // Directly using a WebSocket created in another context would be forbidden. const stub = env.__VITE_RUNNER_OBJECT__.get("singleton"); - stub.send(JSON.stringify(data)); + stub.send(environmentName, JSON.stringify(data)); }, async invoke(data) { const response = await env.__VITE_INVOKE_MODULE__.fetch( new Request(UNKNOWN_HOST, { method: "POST", + headers: { + [ENVIRONMENT_NAME_HEADER]: environmentName, + }, body: JSON.stringify(data), }) ); @@ -207,7 +282,8 @@ async function createModuleRunner(env: WrapperEnv, webSocket: WebSocket) { return import(filepath); }, }, - env + env, + environmentName ); } @@ -222,6 +298,12 @@ export async function getWorkerEntryExport( workerEntryPath: string, exportName: string ): Promise { + if (!parentEnvironmentName) { + throw new Error(`Parent environment not initialized`); + } + + const moduleRunner = moduleRunners.get(parentEnvironmentName); + if (!moduleRunner) { throw new Error(`Module runner not initialized`); } @@ -243,6 +325,12 @@ export async function getWorkerEntryExport( } export async function getWorkerEntryExportTypes() { + if (!parentEnvironmentName) { + throw new Error(`Parent environment not initialized`); + } + + const moduleRunner = moduleRunners.get(parentEnvironmentName); + if (!moduleRunner) { throw new Error(`Module runner not initialized`); } @@ -252,3 +340,28 @@ export async function getWorkerEntryExportTypes() { return getExportTypes(module); } + +/** + * Imports a module from a specific environment's module runner. + * @param environmentName - The name of the environment to import from + * @param id - The module ID to import + * @returns The imported module + * @throws Error if the environment's module runner has not been initialized + */ +async function importFromEnvironment( + environmentName: string, + id: string +): Promise { + const moduleRunner = moduleRunners.get(environmentName); + + if (!moduleRunner) { + throw new Error( + `Module runner not initialized for environment: ${environmentName}` + ); + } + + return moduleRunner.import(id); +} + +// Register the import function globally for use from worker code +globalThis.__VITE_ENVIRONMENT_RUNNER_IMPORT__ = importFromEnvironment; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6880fb4a2936..c7586d3d48d5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2472,6 +2472,27 @@ importers: specifier: workspace:* version: link:../../../wrangler + packages/vite-plugin-cloudflare/playground/child-environment: + devDependencies: + '@cloudflare/vite-plugin': + specifier: workspace:* + version: link:../.. + '@cloudflare/workers-tsconfig': + specifier: workspace:* + version: link:../../../workers-tsconfig + '@cloudflare/workers-types': + specifier: catalog:default + version: 4.20260114.0 + typescript: + specifier: catalog:default + version: 5.8.3 + vite: + specifier: catalog:vite-plugin + version: 7.1.12(@types/node@20.19.9)(jiti@2.6.0)(lightningcss@1.30.2)(yaml@2.8.1) + wrangler: + specifier: workspace:* + version: link:../../../wrangler + packages/vite-plugin-cloudflare/playground/cloudflare-env: devDependencies: '@cloudflare/vite-plugin': @@ -4778,7 +4799,7 @@ packages: resolution: {integrity: sha512-FNcunDuTmEfQTLRLtA6zz+buIXUHj1soPvSWzzQFBC+n2lsy+CGf/NIrR3SEPCmsVNQj70/Jx2lViCpq+09YpQ==} '@cloudflare/kv-asset-handler@0.4.1': - resolution: {integrity: sha512-Nu8ahitGFFJztxUml9oD/DLb7Z28C8cd8F46IVQ7y5Btz575pvMY8AqZsXkX7Gds29eCKdMgIHjIvzskHgPSFg==} + resolution: {integrity: sha512-Nu8ahitGFFJztxUml9oD/DLb7Z28C8cd8F46IVQ7y5Btz575pvMY8AqZsXkX7Gds29eCKdMgIHjIvzskHgPSFg==, tarball: https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.4.1.tgz} engines: {node: '>=18.0.0'} '@cloudflare/playwright@0.0.10': @@ -4817,7 +4838,7 @@ packages: react: ^15.0.0-0 || ^16.0.0-0 || ^17.0.0-0 '@cloudflare/unenv-preset@2.7.13': - resolution: {integrity: sha512-NulO1H8R/DzsJguLC0ndMuk4Ufv0KSlN+E54ay9rn9ZCQo0kpAPwwh3LhgpZ96a3Dr6L9LqW57M4CqC34iLOvw==} + resolution: {integrity: sha512-NulO1H8R/DzsJguLC0ndMuk4Ufv0KSlN+E54ay9rn9ZCQo0kpAPwwh3LhgpZ96a3Dr6L9LqW57M4CqC34iLOvw==, tarball: https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.7.13.tgz} peerDependencies: unenv: 2.0.0-rc.24 workerd: ^1.20251202.0 @@ -4837,104 +4858,104 @@ packages: resolution: {integrity: sha512-H8q/Msk+9Fga6iqqmff7i4mi+kraBCQWFbMEaKIRq3+HBNN5gkpizk05DSG6iIHVxCG1M3WR1FkN9CQ0ZtK4Cw==} '@cloudflare/vitest-pool-workers@0.10.15': - resolution: {integrity: sha512-eISef+JvqC5xr6WBv2+kc6WEjxuKSrZ1MdMuIwdb4vsh8olqw7WHW5pLBL/UzAhbLVlXaAL1uH9UyxIlFkJe7w==} + resolution: {integrity: sha512-eISef+JvqC5xr6WBv2+kc6WEjxuKSrZ1MdMuIwdb4vsh8olqw7WHW5pLBL/UzAhbLVlXaAL1uH9UyxIlFkJe7w==, tarball: https://registry.npmjs.org/@cloudflare/vitest-pool-workers/-/vitest-pool-workers-0.10.15.tgz} peerDependencies: '@vitest/runner': 2.0.x - 3.2.x '@vitest/snapshot': 2.0.x - 3.2.x vitest: 2.0.x - 3.2.x '@cloudflare/workerd-darwin-64@1.20251210.0': - resolution: {integrity: sha512-Nn9X1moUDERA9xtFdCQ2XpQXgAS9pOjiCxvOT8sVx9UJLAiBLkfSCGbpsYdarODGybXCpjRlc77Yppuolvt7oQ==} + resolution: {integrity: sha512-Nn9X1moUDERA9xtFdCQ2XpQXgAS9pOjiCxvOT8sVx9UJLAiBLkfSCGbpsYdarODGybXCpjRlc77Yppuolvt7oQ==, tarball: https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20251210.0.tgz} engines: {node: '>=16'} cpu: [x64] os: [darwin] '@cloudflare/workerd-darwin-64@1.20260111.0': - resolution: {integrity: sha512-UGAjrGLev2/CMLZy7b+v1NIXA4Hupc/QJBFlJwMqldywMcJ/iEqvuUYYuVI2wZXuXeWkgmgFP87oFDQsg78YTQ==} + resolution: {integrity: sha512-UGAjrGLev2/CMLZy7b+v1NIXA4Hupc/QJBFlJwMqldywMcJ/iEqvuUYYuVI2wZXuXeWkgmgFP87oFDQsg78YTQ==, tarball: https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260111.0.tgz} engines: {node: '>=16'} cpu: [x64] os: [darwin] '@cloudflare/workerd-darwin-64@1.20260114.0': - resolution: {integrity: sha512-HNlsRkfNgardCig2P/5bp/dqDECsZ4+NU5XewqArWxMseqt3C5daSuptI620s4pn7Wr0ZKg7jVLH0PDEBkA+aA==} + resolution: {integrity: sha512-HNlsRkfNgardCig2P/5bp/dqDECsZ4+NU5XewqArWxMseqt3C5daSuptI620s4pn7Wr0ZKg7jVLH0PDEBkA+aA==, tarball: https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260114.0.tgz} engines: {node: '>=16'} cpu: [x64] os: [darwin] '@cloudflare/workerd-darwin-arm64@1.20251210.0': - resolution: {integrity: sha512-Mg8iYIZQFnbevq/ls9eW/eneWTk/EE13Pej1MwfkY5et0jVpdHnvOLywy/o+QtMJFef1AjsqXGULwAneYyBfHw==} + resolution: {integrity: sha512-Mg8iYIZQFnbevq/ls9eW/eneWTk/EE13Pej1MwfkY5et0jVpdHnvOLywy/o+QtMJFef1AjsqXGULwAneYyBfHw==, tarball: https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20251210.0.tgz} engines: {node: '>=16'} cpu: [arm64] os: [darwin] '@cloudflare/workerd-darwin-arm64@1.20260111.0': - resolution: {integrity: sha512-YFAZwidLCQVa6rKCCaiWrhA+eh87a7MUhyd9lat3KSbLBAGpYM+ORpyTXpi2Gjm3j6Mp1e/wtzcFTSeMIy2UqA==} + resolution: {integrity: sha512-YFAZwidLCQVa6rKCCaiWrhA+eh87a7MUhyd9lat3KSbLBAGpYM+ORpyTXpi2Gjm3j6Mp1e/wtzcFTSeMIy2UqA==, tarball: https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260111.0.tgz} engines: {node: '>=16'} cpu: [arm64] os: [darwin] '@cloudflare/workerd-darwin-arm64@1.20260114.0': - resolution: {integrity: sha512-qyE1UdFnAlxzb+uCfN/d9c8icch7XRiH49/DjoqEa+bCDihTuRS7GL1RmhVIqHJhb3pX3DzxmKgQZBDBL83Inw==} + resolution: {integrity: sha512-qyE1UdFnAlxzb+uCfN/d9c8icch7XRiH49/DjoqEa+bCDihTuRS7GL1RmhVIqHJhb3pX3DzxmKgQZBDBL83Inw==, tarball: https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260114.0.tgz} engines: {node: '>=16'} cpu: [arm64] os: [darwin] '@cloudflare/workerd-linux-64@1.20251210.0': - resolution: {integrity: sha512-kjC2fCZhZ2Gkm1biwk2qByAYpGguK5Gf5ic8owzSCUw0FOUfQxTZUT9Lp3gApxsfTLbbnLBrX/xzWjywH9QR4g==} + resolution: {integrity: sha512-kjC2fCZhZ2Gkm1biwk2qByAYpGguK5Gf5ic8owzSCUw0FOUfQxTZUT9Lp3gApxsfTLbbnLBrX/xzWjywH9QR4g==, tarball: https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20251210.0.tgz} engines: {node: '>=16'} cpu: [x64] os: [linux] '@cloudflare/workerd-linux-64@1.20260111.0': - resolution: {integrity: sha512-zx1GW6FwfOBjCV7QUCRzGRkViUtn3Is/zaaVPmm57xyy9sjtInx6/SdeBr2Y45tx9AnOP1CnaOFFdmH1P7VIEg==} + resolution: {integrity: sha512-zx1GW6FwfOBjCV7QUCRzGRkViUtn3Is/zaaVPmm57xyy9sjtInx6/SdeBr2Y45tx9AnOP1CnaOFFdmH1P7VIEg==, tarball: https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260111.0.tgz} engines: {node: '>=16'} cpu: [x64] os: [linux] '@cloudflare/workerd-linux-64@1.20260114.0': - resolution: {integrity: sha512-Z0BLvAj/JPOabzads2ddDEfgExWTlD22pnwsuNbPwZAGTSZeQa3Y47eGUWyHk+rSGngknk++S7zHTGbKuG7RRg==} + resolution: {integrity: sha512-Z0BLvAj/JPOabzads2ddDEfgExWTlD22pnwsuNbPwZAGTSZeQa3Y47eGUWyHk+rSGngknk++S7zHTGbKuG7RRg==, tarball: https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260114.0.tgz} engines: {node: '>=16'} cpu: [x64] os: [linux] '@cloudflare/workerd-linux-arm64@1.20251210.0': - resolution: {integrity: sha512-2IB37nXi7PZVQLa1OCuO7/6pNxqisRSO8DmCQ5x/3sezI5op1vwOxAcb1osAnuVsVN9bbvpw70HJvhKruFJTuA==} + resolution: {integrity: sha512-2IB37nXi7PZVQLa1OCuO7/6pNxqisRSO8DmCQ5x/3sezI5op1vwOxAcb1osAnuVsVN9bbvpw70HJvhKruFJTuA==, tarball: https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20251210.0.tgz} engines: {node: '>=16'} cpu: [arm64] os: [linux] '@cloudflare/workerd-linux-arm64@1.20260111.0': - resolution: {integrity: sha512-wFVKxNvCyjRaAcgiSnJNJAmIos3p3Vv6Uhf4pFUZ9JIxr69GNlLWlm9SdCPvtwNFAjzSoDaKzDwjj5xqpuCS6Q==} + resolution: {integrity: sha512-wFVKxNvCyjRaAcgiSnJNJAmIos3p3Vv6Uhf4pFUZ9JIxr69GNlLWlm9SdCPvtwNFAjzSoDaKzDwjj5xqpuCS6Q==, tarball: https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260111.0.tgz} engines: {node: '>=16'} cpu: [arm64] os: [linux] '@cloudflare/workerd-linux-arm64@1.20260114.0': - resolution: {integrity: sha512-kPUmEtUxUWlr9PQ64kuhdK0qyo8idPe5IIXUgi7xCD7mDd6EOe5J7ugDpbfvfbYKEjx4DpLvN2t45izyI/Sodw==} + resolution: {integrity: sha512-kPUmEtUxUWlr9PQ64kuhdK0qyo8idPe5IIXUgi7xCD7mDd6EOe5J7ugDpbfvfbYKEjx4DpLvN2t45izyI/Sodw==, tarball: https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260114.0.tgz} engines: {node: '>=16'} cpu: [arm64] os: [linux] '@cloudflare/workerd-windows-64@1.20251210.0': - resolution: {integrity: sha512-Uaz6/9XE+D6E7pCY4OvkCuJHu7HcSDzeGcCGY1HLhojXhHd7yL52c3yfiyJdS8hPatiAa0nn5qSI/42+aTdDSw==} + resolution: {integrity: sha512-Uaz6/9XE+D6E7pCY4OvkCuJHu7HcSDzeGcCGY1HLhojXhHd7yL52c3yfiyJdS8hPatiAa0nn5qSI/42+aTdDSw==, tarball: https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20251210.0.tgz} engines: {node: '>=16'} cpu: [x64] os: [win32] '@cloudflare/workerd-windows-64@1.20260111.0': - resolution: {integrity: sha512-zWgd77L7OI1BxgBbG+2gybDahIMgPX5iNo6e3LqcEz1Xm3KfiqgnDyMBcxeQ7xDrj7fHUGAlc//QnKvDchuUoQ==} + resolution: {integrity: sha512-zWgd77L7OI1BxgBbG+2gybDahIMgPX5iNo6e3LqcEz1Xm3KfiqgnDyMBcxeQ7xDrj7fHUGAlc//QnKvDchuUoQ==, tarball: https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260111.0.tgz} engines: {node: '>=16'} cpu: [x64] os: [win32] '@cloudflare/workerd-windows-64@1.20260114.0': - resolution: {integrity: sha512-MJnKgm6i1jZGyt2ZHQYCnRlpFTEZcK2rv9y7asS3KdVEXaDgGF8kOns5u6YL6/+eMogfZuHRjfDS+UqRTUYIFA==} + resolution: {integrity: sha512-MJnKgm6i1jZGyt2ZHQYCnRlpFTEZcK2rv9y7asS3KdVEXaDgGF8kOns5u6YL6/+eMogfZuHRjfDS+UqRTUYIFA==, tarball: https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260114.0.tgz} engines: {node: '>=16'} cpu: [x64] os: [win32] '@cloudflare/workers-types@4.20260114.0': - resolution: {integrity: sha512-Q3YG2tAPvN0Z9LueWZp8RyBIJYAppb2+knSh9WXagm/W6XERGKtYfMV0z9Ij5bJksmvvR/R9jTjjEqbK27kd8g==} + resolution: {integrity: sha512-Q3YG2tAPvN0Z9LueWZp8RyBIJYAppb2+knSh9WXagm/W6XERGKtYfMV0z9Ij5bJksmvvR/R9jTjjEqbK27kd8g==, tarball: https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20260114.0.tgz} '@colors/colors@1.5.0': resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==}