Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/fix-wasm-query-string-windows.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"wrangler": patch
---

Strip query strings from module names before writing to disk

When bundling modules with query string suffixes (e.g. `.wasm?module`), the `?` character was included in the output filename. Since `?` is not a valid filename character on Windows, this caused an ENOENT error during `wrangler dev`. This was particularly visible when using Prisma Client with the D1 adapter, which imports `.wasm?module` files.

The fix strips query strings from module names before writing them to disk, while preserving correct module resolution.
9 changes: 9 additions & 0 deletions .changeset/funny-mails-hammer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"wrangler": minor
"miniflare": minor
"@cloudflare/vite-plugin": minor
---

Enable container egress interception in local dev without the `experimental` compatibility flag

Container local development now always prepares the egress interceptor sidecar image needed for `interceptOutboundHttp()`. This makes container-to-Worker interception available by default in Wrangler, Miniflare, and the Cloudflare Vite plugin.
6 changes: 6 additions & 0 deletions .changeset/late-humans-win.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@cloudflare/local-explorer-ui": patch
"miniflare": patch
---

Update `@hey-api/openapi-ts` to ^0.94.0
12 changes: 5 additions & 7 deletions packages/containers-shared/src/images.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ import type {
WranglerLogger,
} from "./types";

const DEFAULT_CONTAINER_EGRESS_INTERCEPTOR_IMAGE =
"cloudflare/proxy-everything:4dc6c7f@sha256:9621ef445ef120409e5d95bbd845ab2fa0f613636b59a01d998f5704f4096ae2";
export const DEFAULT_CONTAINER_EGRESS_INTERCEPTOR_IMAGE =
"cloudflare/proxy-everything:3f5e832@sha256:816255f5b6ebdc2cdcddb578d803121e7ee9cfe178442da07725d75a66cdcf37";

export function getEgressInterceptorImage(): string {
return (
Expand Down Expand Up @@ -114,7 +114,6 @@ export async function prepareContainerImagesForDev(args: {
}) => void;
logger: WranglerLogger | ViteLogger;
isVite: boolean;
compatibilityFlags?: string[];
}): Promise<void> {
const {
dockerPath,
Expand Down Expand Up @@ -171,10 +170,9 @@ export async function prepareContainerImagesForDev(args: {
}
}

// Pull the egress interceptor image if experimental flag is enabled.
// This image is used to intercept outbound HTTP from containers and
// route it back to workerd (e.g. for interceptOutboundHttp).
if (!aborted && args.compatibilityFlags?.includes("experimental")) {
// Pull the egress interceptor image used to intercept outbound HTTP from
// containers and route it back to workerd (e.g. for interceptOutboundHttp).
if (!aborted) {
await pullEgressInterceptorImage(dockerPath);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ function getFrameworkTestConfig(pm: string): NamedFrameworkTestConfig[] {
{
name: "astro:pages",
argv: ["--platform", "pages"],
quarantine: true,
testCommitMessage: true,
unsupportedOSs: ["win32"],
verifyDeploy: {
Expand All @@ -65,6 +66,7 @@ function getFrameworkTestConfig(pm: string): NamedFrameworkTestConfig[] {
{
name: "astro:workers",
argv: ["--platform", "workers"],
quarantine: true,
testCommitMessage: true,
unsupportedOSs: ["win32"],
verifyDeploy: {
Expand Down Expand Up @@ -720,6 +722,7 @@ function getExperimentalFrameworkTestConfig(
{
name: "astro:workers",
argv: ["--platform", "workers"],
quarantine: true,
testCommitMessage: true,
unsupportedOSs: ["win32"],
verifyDeploy: {
Expand Down
2 changes: 1 addition & 1 deletion packages/local-explorer-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
"devDependencies": {
"@cloudflare/eslint-config-shared": "workspace:*",
"@cloudflare/workers-tsconfig": "workspace:*",
"@hey-api/openapi-ts": "^0.91.1",
"@hey-api/openapi-ts": "catalog:default",
"@tanstack/react-router-devtools": "^1.158.0",
"@tanstack/router-plugin": "^1.158.0",
"@types/react": "^19.2.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/miniflare/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
"@cloudflare/workers-types": "catalog:default",
"@cloudflare/workers-utils": "workspace:*",
"@cloudflare/workflows-shared": "workspace:*",
"@hey-api/openapi-ts": "^0.90.0",
"@hey-api/openapi-ts": "catalog:default",
"@microsoft/api-extractor": "^7.52.8",
"@puppeteer/browsers": "^2.10.6",
"@types/debug": "^4.1.7",
Expand Down
17 changes: 4 additions & 13 deletions packages/miniflare/src/plugins/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import path from "node:path";
import { Readable } from "node:stream";
import tls from "node:tls";
import { TextEncoder } from "node:util";
import { DEFAULT_CONTAINER_EGRESS_INTERCEPTOR_IMAGE } from "@cloudflare/containers-shared";
import { bold } from "kleur/colors";
import { MockAgent } from "undici";
import SCRIPT_ENTRY from "worker:core/entry";
Expand Down Expand Up @@ -908,10 +909,7 @@ export const CORE_PLUGIN: Plugin<
);
}
),
containerEngine: getContainerEngine(
options.containerEngine,
options.compatibilityFlags
),
containerEngine: getContainerEngine(options.containerEngine),
},
});
}
Expand Down Expand Up @@ -1214,9 +1212,6 @@ function getWorkerScript(
}
}

const DEFAULT_CONTAINER_EGRESS_INTERCEPTOR_IMAGE =
"cloudflare/proxy-everything:4dc6c7f@sha256:9621ef445ef120409e5d95bbd845ab2fa0f613636b59a01d998f5704f4096ae2";

/**
* Returns the default containerEgressInterceptorImage. It's used for
* container network interception for local dev.
Expand All @@ -1234,8 +1229,7 @@ function getContainerEgressInterceptorImage(): string {
* @returns The container engine, defaulting to the default docker socket located on linux/macOS at `unix:///var/run/docker.sock`
*/
function getContainerEngine(
engineOrSocketPath: Worker_ContainerEngine | string | undefined,
compatibilityFlags?: string[]
engineOrSocketPath: Worker_ContainerEngine | string | undefined
): Worker_ContainerEngine {
if (!engineOrSocketPath) {
// TODO: workerd does not support win named pipes
Expand All @@ -1245,13 +1239,10 @@ function getContainerEngine(
: "unix:///var/run/docker.sock";
}

// TODO: Once the feature becomes GA, we should remove the experimental requirement.
// Egress interceptor is to support direct connectivity between the Container and Workers,
// it spawns a container in the same network namespace as the local dev container and
// intercepts traffic to redirect to Workerd.
const egressImage = compatibilityFlags?.includes("experimental")
? getContainerEgressInterceptorImage()
: undefined;
const egressImage = getContainerEgressInterceptorImage();

if (typeof engineOrSocketPath === "string") {
return {
Expand Down
3 changes: 0 additions & 3 deletions packages/vite-plugin-cloudflare/src/plugins/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,9 +208,6 @@ export const devPlugin = createPlugin("dev", (ctx) => {
onContainerImagePreparationEnd: () => {},
logger: viteDevServer.config.logger,
isVite: true,
compatibilityFlags: ctx.allWorkerConfigs.flatMap(
(c) => c.compatibility_flags
),
});

containerImageTags = new Set(containerTagToOptionsMap.keys());
Expand Down
3 changes: 0 additions & 3 deletions packages/vite-plugin-cloudflare/src/plugins/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,6 @@ export const previewPlugin = createPlugin("preview", (ctx) => {
onContainerImagePreparationEnd: () => {},
logger: vitePreviewServer.config.logger,
isVite: true,
compatibilityFlags: ctx.allWorkerConfigs.flatMap(
(c) => c.compatibility_flags
),
});

const containerImageTags = new Set(containerTagToOptionsMap.keys());
Expand Down
1 change: 1 addition & 0 deletions packages/workers-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"build": "tsup",
"check:lint": "eslint . --max-warnings=0 --cache",
"check:type": "tsc -p ./tsconfig.json",
"deploy": "echo 'no deploy'",
"dev": "concurrently -c black,blue --kill-others-on-fail false \"pnpm tsup --watch src\" \"pnpm run check:type --watch --preserveWatchOutput\"",
"test": "vitest",
"test:ci": "vitest run",
Expand Down
2 changes: 1 addition & 1 deletion packages/wrangler/e2e/containers.dev.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ for (const source of imageSource) {
name: `${workerName}`,
main: "src/index.ts",
compatibility_date: "2025-04-03",
compatibility_flags: ["experimental", "enable_ctx_exports"],
compatibility_flags: ["enable_ctx_exports"],
containers: [
{
image: "./Dockerfile",
Expand Down
41 changes: 41 additions & 0 deletions packages/wrangler/src/__tests__/deploy/formats.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,47 @@ describe("deploy", () => {
expect(std.warn).toMatchInlineSnapshot(`""`);
});

it("should strip query string suffixes from module names (esm)", async () => {
writeWranglerConfig();
fs.writeFileSync(
"./index.js",
`import hello from './hello.wasm?module'; export default {};`
);
fs.writeFileSync("./hello.wasm", "SOME WASM CONTENT");
mockSubDomainRequest();
mockUploadWorkerRequest({
expectedType: "esm",
expectedBindings: [],
expectedModules: {
"./94b240d0d692281e6467aa42043986e5c7eea034-hello.wasm":
"SOME WASM CONTENT",
},
});
await runWrangler("deploy index.js");
expect(std.err).toMatchInlineSnapshot(`""`);
});

it("should strip query string suffixes from module names with preserve_file_names (esm)", async () => {
writeWranglerConfig({
preserve_file_names: true,
});
fs.writeFileSync(
"./index.js",
`import hello from './hello.wasm?module'; export default {};`
);
fs.writeFileSync("./hello.wasm", "SOME WASM CONTENT");
mockSubDomainRequest();
mockUploadWorkerRequest({
expectedType: "esm",
expectedBindings: [],
expectedModules: {
"./hello.wasm": "SOME WASM CONTENT",
},
});
await runWrangler("deploy index.js");
expect(std.err).toMatchInlineSnapshot(`""`);
});

describe("inject process.env.NODE_ENV", () => {
beforeEach(() => {
vi.stubEnv("NODE_ENV", "some-node-env");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,6 @@ export class LocalRuntimeController extends RuntimeController {
},
logger: logger,
isVite: false,
compatibilityFlags: data.config.compatibilityFlags,
});
if (this.containerBeingBuilt) {
this.containerBeingBuilt.abortRequested = false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,6 @@ export class MultiworkerRuntimeController extends LocalRuntimeController {
},
logger: logger,
isVite: false,
compatibilityFlags: data.config.compatibilityFlags,
});
if (this.containerBeingBuilt) {
this.containerBeingBuilt.abortRequested = false;
Expand Down
30 changes: 21 additions & 9 deletions packages/wrangler/src/deployment-bundle/module-collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@ export const RuleTypeToModuleType: Record<ConfigModuleRuleType, CfModuleType> =

export const ModuleTypeToRuleType = flipObject(RuleTypeToModuleType);

// Strip query string suffixes (e.g. `?module`) from module paths so that
// file paths and module names don't contain characters invalid on Windows.
function stripQueryString(modulePath: string): string {
const queryIndex = modulePath.indexOf("?");
return queryIndex !== -1 ? modulePath.slice(0, queryIndex) : modulePath;
}

// This is a combination of an esbuild plugin and a mutable array
// that we use to collect module references from source code.
// There will be modules that _shouldn't_ be inlined directly into
Expand Down Expand Up @@ -202,10 +209,11 @@ export function createModuleCollector(props: {

// take the file and massage it to a
// transportable/manageable format
const cleanedPath = stripQueryString(args.path);
const filePath = path.join(
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
props.wrangler1xLegacyModuleReferences!.rootDirectory,
args.path
cleanedPath
);
const fileContent = (await readFile(
filePath
Expand All @@ -215,8 +223,8 @@ export function createModuleCollector(props: {
.update(fileContent)
.digest("hex");
const fileName = props.preserveFileNames
? args.path
: `./${fileHash}-${path.basename(args.path)}`;
? cleanedPath
: `./${fileHash}-${path.basename(cleanedPath)}`;

const { rule } =
rulesMatchers.find(({ regex }) => regex.test(fileName)) || {};
Expand Down Expand Up @@ -257,13 +265,17 @@ export function createModuleCollector(props: {
// take the file and massage it to a
// transportable/manageable format

let filePath = path.join(args.resolveDir, args.path);
// Strip query string suffixes (e.g. `?module`) from the import
// path. The `?` character is not valid in filenames on Windows
// and would cause ENOENT errors when writing modules to disk.
const cleanedPath = stripQueryString(args.path);
let filePath = path.join(args.resolveDir, cleanedPath);

// If this was a found additional module, mark it as external.
// Note, there's no need to watch the file here as we already
// watch all `foundModulePaths` with `wrangler:modules-watch`.
if (foundModulePaths.includes(filePath)) {
return { path: args.path, external: true };
return { path: cleanedPath, external: true };
}
// For JavaScript module rules, we only register this onResolve
// callback if `findAdditionalModules` is true. If we didn't
Expand All @@ -277,7 +289,7 @@ export function createModuleCollector(props: {
// and if so, validate the import against the package.json exports
// and resolve the file path to the correct file.
try {
const resolved = await build.resolve(args.path, {
const resolved = await build.resolve(cleanedPath, {
kind: args.kind,
importer: args.importer,
resolveDir: args.resolveDir,
Expand All @@ -295,7 +307,7 @@ export function createModuleCollector(props: {

// Next try to resolve using the node module resolution algorithm
try {
const resolved = resolveSync(args.path, {
const resolved = resolveSync(cleanedPath, {
basedir: args.resolveDir,
});
filePath = resolved;
Expand All @@ -314,8 +326,8 @@ export function createModuleCollector(props: {
.update(fileContent)
.digest("hex");
const fileName = props.preserveFileNames
? args.path
: `./${fileHash}-${path.basename(args.path)}`;
? cleanedPath
: `./${fileHash}-${path.basename(cleanedPath)}`;

// add the module to the array
modules.push({
Expand Down
Loading
Loading