From 3486bcc06a19975e05b1fa28ffa139b18df11cf1 Mon Sep 17 00:00:00 2001 From: Samuel Macleod Date: Thu, 26 Mar 2026 23:29:03 +0000 Subject: [PATCH 1/2] [miniflare] Fix glob patterns matching double-extension filenames like foo.wasm.js (#8280) --- .changeset/fix-wasm-double-extension-glob.md | 11 +++++++++++ .../test/not-actually.wasm.test.ts | 10 ++++++++++ packages/miniflare/src/shared/matcher.ts | 10 ++++++++-- packages/miniflare/test/shared/matcher.spec.ts | 18 ++++++++++++++++++ 4 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 .changeset/fix-wasm-double-extension-glob.md create mode 100644 fixtures/vitest-pool-workers-examples/web-assembly/test/not-actually.wasm.test.ts diff --git a/.changeset/fix-wasm-double-extension-glob.md b/.changeset/fix-wasm-double-extension-glob.md new file mode 100644 index 0000000000..76cbcb0eec --- /dev/null +++ b/.changeset/fix-wasm-double-extension-glob.md @@ -0,0 +1,11 @@ +--- +"miniflare": patch +--- + +fix: glob patterns for module rules no longer match double-extension filenames like `foo.wasm.js` + +Previously, the `globsToRegExps` helper compiled glob patterns without a trailing `$` anchor. This caused patterns like `**/*.wasm` to match any path containing `.wasm` as a substring — including filenames such as `foo.wasm.js` or `main.wasm.test.ts`. + +When using `@cloudflare/vitest-pool-workers` with a `wrangler.configPath`, Wrangler's default `CompiledWasm` module rule (`**/*.wasm`) was silently applied to test files whose names contained `.wasm`, causing them to be loaded as WebAssembly binaries instead of JavaScript and failing at runtime. + +The fix restores the `$` end anchor in the compiled regex so that `**/*.wasm` only matches paths that literally end in `.wasm`, while the leading `^` remains absent to allow matching anywhere within an absolute path. diff --git a/fixtures/vitest-pool-workers-examples/web-assembly/test/not-actually.wasm.test.ts b/fixtures/vitest-pool-workers-examples/web-assembly/test/not-actually.wasm.test.ts new file mode 100644 index 0000000000..36eb0f7db7 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/web-assembly/test/not-actually.wasm.test.ts @@ -0,0 +1,10 @@ +// Regression test for https://github.com/cloudflare/workers-sdk/issues/8280 +// A test file whose name contains ".wasm" (but whose actual extension is .ts) +// must NOT be treated as a WebAssembly module by the module rules. +import { it } from "vitest"; + +it("loads .wasm.test.ts files as JavaScript, not as WebAssembly", ({ + expect, +}) => { + expect(true).toBe(true); +}); diff --git a/packages/miniflare/src/shared/matcher.ts b/packages/miniflare/src/shared/matcher.ts index 856af3ea25..33ae1a9eae 100644 --- a/packages/miniflare/src/shared/matcher.ts +++ b/packages/miniflare/src/shared/matcher.ts @@ -12,10 +12,16 @@ export function globsToRegExps(globs: string[] = []): MatcherRegExps { // ...however, we don't actually want to include the "g" flag, since it will // change `lastIndex` as paths are matched, and we want to reuse `RegExp`s. // So, reconstruct each `RegExp` without any flags. + // + // We also re-add the trailing "$" anchor that was stripped. Without it, a + // pattern like `**/*.wasm` would incorrectly match `foo.wasm.js` since the + // regex matches `foo.wasm` anywhere inside the string. The leading "^" is + // intentionally kept absent so the pattern can match anywhere within an + // absolute path (e.g. `**/*.wasm` still matches `/abs/path/to/foo.wasm`). if (glob.startsWith("!")) { - exclude.push(new RegExp(globToRegexp(glob.slice(1), opts), "")); + exclude.push(new RegExp(globToRegexp(glob.slice(1), opts).source + "$")); } else { - include.push(new RegExp(globToRegexp(glob, opts), "")); + include.push(new RegExp(globToRegexp(glob, opts).source + "$")); } } return { include, exclude }; diff --git a/packages/miniflare/test/shared/matcher.spec.ts b/packages/miniflare/test/shared/matcher.spec.ts index 107601596c..07b8b50632 100644 --- a/packages/miniflare/test/shared/matcher.spec.ts +++ b/packages/miniflare/test/shared/matcher.spec.ts @@ -33,3 +33,21 @@ test("globsToRegExps/testRegExps: matches glob patterns", ({ expect }) => { ) ).toBe(true); }); + +test("globsToRegExps/testRegExps: does not match double-extension paths (e.g. foo.wasm.js for **/*.wasm)", ({ + expect, +}) => { + // Regression test for https://github.com/cloudflare/workers-sdk/issues/8280 + // A pattern like **/*.wasm must NOT match foo.wasm.js — the extension must + // be anchored to the end of the path. + const wasmMatcher = globsToRegExps(["**/*.wasm"]); + + expect(testRegExps(wasmMatcher, "foo.wasm")).toBe(true); + expect(testRegExps(wasmMatcher, "path/to/foo.wasm")).toBe(true); + expect(testRegExps(wasmMatcher, "/absolute/path/to/foo.wasm")).toBe(true); + + // Must NOT match double-extension variants + expect(testRegExps(wasmMatcher, "foo.wasm.js")).toBe(false); + expect(testRegExps(wasmMatcher, "src/main.wasm.test.js")).toBe(false); + expect(testRegExps(wasmMatcher, "foo.wasm.map")).toBe(false); +}); From b85adb9ee3eceebcfef7a59d495aae02bf6618d4 Mon Sep 17 00:00:00 2001 From: Samuel Macleod Date: Fri, 27 Mar 2026 00:31:13 +0000 Subject: [PATCH 2/2] Scope endAnchor to module rules only, preserving KV Sites substring matching --- .../miniflare/src/plugins/core/modules.ts | 2 +- packages/miniflare/src/shared/matcher.ts | 23 +++++++++++-------- .../miniflare/test/shared/matcher.spec.ts | 17 ++++++++++---- 3 files changed, 28 insertions(+), 14 deletions(-) diff --git a/packages/miniflare/src/plugins/core/modules.ts b/packages/miniflare/src/plugins/core/modules.ts index 238c855039..dc91dc5d38 100644 --- a/packages/miniflare/src/plugins/core/modules.ts +++ b/packages/miniflare/src/plugins/core/modules.ts @@ -122,7 +122,7 @@ export function compileModuleRules(rules: ModuleRule[]) { if (finalisedTypes.has(rule.type)) continue; compiledRules.push({ type: rule.type, - include: globsToRegExps(rule.include), + include: globsToRegExps(rule.include, { endAnchor: true }), }); if (!rule.fallthrough) finalisedTypes.add(rule.type); } diff --git a/packages/miniflare/src/shared/matcher.ts b/packages/miniflare/src/shared/matcher.ts index 33ae1a9eae..580a238b3f 100644 --- a/packages/miniflare/src/shared/matcher.ts +++ b/packages/miniflare/src/shared/matcher.ts @@ -1,27 +1,32 @@ import globToRegexp from "glob-to-regexp"; import { MatcherRegExps } from "../workers"; -export function globsToRegExps(globs: string[] = []): MatcherRegExps { +export function globsToRegExps( + globs: string[] = [], + { endAnchor }: { endAnchor?: boolean } = {} +): MatcherRegExps { const include: RegExp[] = []; const exclude: RegExp[] = []; // Setting `flags: "g"` removes "^" and "$" from the generated regexp, // allowing matches anywhere in the path... // (https://github.com/fitzgen/glob-to-regexp/blob/2abf65a834259c6504ed3b80e85f893f8cd99127/index.js#L123-L127) const opts: globToRegexp.Options = { globstar: true, flags: "g" }; + // When `endAnchor` is true, we re-add the trailing "$" that was stripped. + // Without it, a pattern like `**/*.wasm` incorrectly matches `foo.wasm.js` + // since the regex matches `foo.wasm` anywhere inside the string. The leading + // "^" is intentionally kept absent so the pattern can match anywhere within + // an absolute path (e.g. `**/*.wasm` still matches `/abs/path/to/foo.wasm`). + const suffix = endAnchor ? "$" : ""; for (const glob of globs) { // ...however, we don't actually want to include the "g" flag, since it will // change `lastIndex` as paths are matched, and we want to reuse `RegExp`s. // So, reconstruct each `RegExp` without any flags. - // - // We also re-add the trailing "$" anchor that was stripped. Without it, a - // pattern like `**/*.wasm` would incorrectly match `foo.wasm.js` since the - // regex matches `foo.wasm` anywhere inside the string. The leading "^" is - // intentionally kept absent so the pattern can match anywhere within an - // absolute path (e.g. `**/*.wasm` still matches `/abs/path/to/foo.wasm`). if (glob.startsWith("!")) { - exclude.push(new RegExp(globToRegexp(glob.slice(1), opts).source + "$")); + exclude.push( + new RegExp(globToRegexp(glob.slice(1), opts).source + suffix) + ); } else { - include.push(new RegExp(globToRegexp(glob, opts).source + "$")); + include.push(new RegExp(globToRegexp(glob, opts).source + suffix)); } } return { include, exclude }; diff --git a/packages/miniflare/test/shared/matcher.spec.ts b/packages/miniflare/test/shared/matcher.spec.ts index 07b8b50632..6a05092aaa 100644 --- a/packages/miniflare/test/shared/matcher.spec.ts +++ b/packages/miniflare/test/shared/matcher.spec.ts @@ -34,13 +34,13 @@ test("globsToRegExps/testRegExps: matches glob patterns", ({ expect }) => { ).toBe(true); }); -test("globsToRegExps/testRegExps: does not match double-extension paths (e.g. foo.wasm.js for **/*.wasm)", ({ +test("globsToRegExps/testRegExps: endAnchor prevents matching double-extension paths", ({ expect, }) => { // Regression test for https://github.com/cloudflare/workers-sdk/issues/8280 - // A pattern like **/*.wasm must NOT match foo.wasm.js — the extension must - // be anchored to the end of the path. - const wasmMatcher = globsToRegExps(["**/*.wasm"]); + // With endAnchor, a pattern like **/*.wasm must NOT match foo.wasm.js — the + // extension must be anchored to the end of the path. + const wasmMatcher = globsToRegExps(["**/*.wasm"], { endAnchor: true }); expect(testRegExps(wasmMatcher, "foo.wasm")).toBe(true); expect(testRegExps(wasmMatcher, "path/to/foo.wasm")).toBe(true); @@ -51,3 +51,12 @@ test("globsToRegExps/testRegExps: does not match double-extension paths (e.g. fo expect(testRegExps(wasmMatcher, "src/main.wasm.test.js")).toBe(false); expect(testRegExps(wasmMatcher, "foo.wasm.map")).toBe(false); }); + +test("globsToRegExps/testRegExps: without endAnchor, matches substring patterns", ({ + expect, +}) => { + // KV Sites relies on patterns like "b" matching any path containing "b" + const matcher = globsToRegExps(["b"]); + expect(testRegExps(matcher, "b/b.txt")).toBe(true); + expect(testRegExps(matcher, "a.txt")).toBe(false); +});