From 869bb1fa1ee0d2e0565d128d69f2f3de8cf755a2 Mon Sep 17 00:00:00 2001 From: arktomson <2372929539@qq.com> Date: Sat, 21 Feb 2026 15:55:05 +0800 Subject: [PATCH 1/4] fix: escape virtual module import paths with apostrophes --- .../__tests__/resolveVirtualModules.test.ts | 45 +++++++++++++++++++ .../vite/plugins/resolveVirtualModules.ts | 12 ++++- 2 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 packages/wxt/src/core/builders/vite/plugins/__tests__/resolveVirtualModules.test.ts diff --git a/packages/wxt/src/core/builders/vite/plugins/__tests__/resolveVirtualModules.test.ts b/packages/wxt/src/core/builders/vite/plugins/__tests__/resolveVirtualModules.test.ts new file mode 100644 index 000000000..b8393e2ff --- /dev/null +++ b/packages/wxt/src/core/builders/vite/plugins/__tests__/resolveVirtualModules.test.ts @@ -0,0 +1,45 @@ +import fs from 'fs-extra'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { afterEach, describe, expect, it } from 'vitest'; +import { resolveVirtualModules } from '../resolveVirtualModules'; +import { fakeResolvedConfig } from '../../../../utils/testing/fake-objects'; + +const tempDirs: string[] = []; + +afterEach(async () => { + await Promise.all(tempDirs.splice(0).map((dir) => fs.remove(dir))); +}); + +describe('resolveVirtualModules', () => { + it.each([ + `import definition from 'virtual:user-background-entrypoint';`, + `import definition from "virtual:user-background-entrypoint";`, + ])( + 'should escape input paths when template contains %s', + async (template) => { + const wxtModuleDir = await fs.mkdtemp(join(tmpdir(), 'wxt-test-')); + tempDirs.push(wxtModuleDir); + + await fs.outputFile( + join(wxtModuleDir, 'dist/virtual/background-entrypoint.mjs'), + template, + ); + + const plugin = resolveVirtualModules( + fakeResolvedConfig({ wxtModuleDir }), + ).find( + (plugin) => plugin.name === 'wxt:resolve-virtual-background-entrypoint', + ); + + expect(plugin).toBeDefined(); + + const inputPath = `/tmp/foo'bar/background.ts`; + const code = await plugin!.load!( + '\0virtual:wxt-background-entrypoint?' + inputPath, + ); + + expect(code).toBe(`import definition from ${JSON.stringify(inputPath)};`); + }, + ); +}); diff --git a/packages/wxt/src/core/builders/vite/plugins/resolveVirtualModules.ts b/packages/wxt/src/core/builders/vite/plugins/resolveVirtualModules.ts index 4fa183c9b..3ac3f5f7f 100644 --- a/packages/wxt/src/core/builders/vite/plugins/resolveVirtualModules.ts +++ b/packages/wxt/src/core/builders/vite/plugins/resolveVirtualModules.ts @@ -14,6 +14,7 @@ import { resolve } from 'path'; export function resolveVirtualModules(config: ResolvedConfig): Plugin[] { return virtualModuleNames.map((name) => { const virtualId: `${VirtualModuleId}?` = `virtual:wxt-${name}?`; + const userVirtualId = `virtual:user-${name}`; const resolvedVirtualId = '\0' + virtualId; return { name: `wxt:resolve-virtual-${name}`, @@ -34,7 +35,16 @@ export function resolveVirtualModules(config: ResolvedConfig): Plugin[] { resolve(config.wxtModuleDir, `dist/virtual/${name}.mjs`), 'utf-8', ); - return template.replace(`virtual:user-${name}`, inputPath); + const escapedPath = JSON.stringify(inputPath); + const code = template + .replace(`'${userVirtualId}'`, escapedPath) + .replace(`"${userVirtualId}"`, escapedPath); + if (code === template) { + throw Error( + `Failed to resolve virtual module "${name}": expected template import "${userVirtualId}"`, + ); + } + return code; }, }; }); From 42ea731e5d245201a71030e5808412d596196483 Mon Sep 17 00:00:00 2001 From: Aaron Date: Sun, 26 Apr 2026 17:07:41 -0500 Subject: [PATCH 2/4] Update tests to use Node.js built-in modules and add test for double quotes --- bun.lock | 2 +- .../__tests__/resolveVirtualModules.test.ts | 30 +++++++++++-------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/bun.lock b/bun.lock index e13d5128a..db7d3d711 100644 --- a/bun.lock +++ b/bun.lock @@ -91,7 +91,7 @@ "@types/har-format": "*", }, "devDependencies": { - "@types/chrome": "^0.1.40", + "@types/chrome": "0.1.40", "@types/node": "^20.0.0", "nano-spawn": "^2.0.0", "typescript": "^5.9.3", diff --git a/packages/wxt/src/core/builders/vite/plugins/__tests__/resolveVirtualModules.test.ts b/packages/wxt/src/core/builders/vite/plugins/__tests__/resolveVirtualModules.test.ts index 7cec2c4f4..da13f7287 100644 --- a/packages/wxt/src/core/builders/vite/plugins/__tests__/resolveVirtualModules.test.ts +++ b/packages/wxt/src/core/builders/vite/plugins/__tests__/resolveVirtualModules.test.ts @@ -1,4 +1,4 @@ -import fs from 'fs-extra'; +import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, describe, expect, it } from 'vitest'; @@ -8,7 +8,9 @@ import { fakeResolvedConfig } from '../../../../utils/testing/fake-objects'; const tempDirs: string[] = []; afterEach(async () => { - await Promise.all(tempDirs.splice(0).map((dir) => fs.remove(dir))); + await Promise.all( + tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })), + ); }); describe('resolveVirtualModules', () => { @@ -18,13 +20,15 @@ describe('resolveVirtualModules', () => { ])( 'should escape input paths with apostrophes when encountering: %s', async (template) => { - const wxtModuleDir = await fs.mkdtemp(join(tmpdir(), 'wxt-test-')); + const wxtModuleDir = await mkdtemp(join(tmpdir(), 'wxt-test-')); tempDirs.push(wxtModuleDir); - await fs.outputFile( - join(wxtModuleDir, 'dist/virtual/background-entrypoint.mjs'), - template, + const filePath = join( + wxtModuleDir, + 'dist/virtual/background-entrypoint.mjs', ); + await mkdir(join(wxtModuleDir, 'dist/virtual'), { recursive: true }); + await writeFile(filePath, template); const plugin = resolveVirtualModules( fakeResolvedConfig({ wxtModuleDir }), @@ -35,7 +39,7 @@ describe('resolveVirtualModules', () => { expect(plugin).toBeDefined(); const inputPath = `/tmp/foo'bar/background.ts`; - const code = await plugin!.load!( + const code = await plugin!.load!.handler!( '\0virtual:wxt-background-entrypoint?' + inputPath, ); @@ -49,13 +53,15 @@ describe('resolveVirtualModules', () => { ])( 'should escape input paths with double quotes when encountering: %s', async (template) => { - const wxtModuleDir = await fs.mkdtemp(join(tmpdir(), 'wxt-test-')); + const wxtModuleDir = await mkdtemp(join(tmpdir(), 'wxt-test-')); tempDirs.push(wxtModuleDir); - await fs.outputFile( - join(wxtModuleDir, 'dist/virtual/background-entrypoint.mjs'), - template, + const filePath = join( + wxtModuleDir, + 'dist/virtual/background-entrypoint.mjs', ); + await mkdir(join(wxtModuleDir, 'dist/virtual'), { recursive: true }); + await writeFile(filePath, template); const plugin = resolveVirtualModules( fakeResolvedConfig({ wxtModuleDir }), @@ -66,7 +72,7 @@ describe('resolveVirtualModules', () => { expect(plugin).toBeDefined(); const inputPath = `/tmp/foo"bar/background.ts`; - const code = await plugin!.load!( + const code = await plugin!.load!.handler!( '\0virtual:wxt-background-entrypoint?' + inputPath, ); From 4f9a78959874c8e89df9b7ef3f418a8f0f281b0e Mon Sep 17 00:00:00 2001 From: Aaron Date: Sun, 26 Apr 2026 17:09:41 -0500 Subject: [PATCH 3/4] Use pathToFileURL for proper path escaping instead of JSON.stringify --- .../vite/plugins/__tests__/resolveVirtualModules.test.ts | 6 ++++-- .../src/core/builders/vite/plugins/resolveVirtualModules.ts | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/wxt/src/core/builders/vite/plugins/__tests__/resolveVirtualModules.test.ts b/packages/wxt/src/core/builders/vite/plugins/__tests__/resolveVirtualModules.test.ts index da13f7287..5b0224de0 100644 --- a/packages/wxt/src/core/builders/vite/plugins/__tests__/resolveVirtualModules.test.ts +++ b/packages/wxt/src/core/builders/vite/plugins/__tests__/resolveVirtualModules.test.ts @@ -43,7 +43,9 @@ describe('resolveVirtualModules', () => { '\0virtual:wxt-background-entrypoint?' + inputPath, ); - expect(code).toBe(`import definition from "/tmp/foo'bar/background.ts";`); + expect(code).toBe( + `import definition from file:///tmp/foo'bar/background.ts;`, + ); }, ); @@ -77,7 +79,7 @@ describe('resolveVirtualModules', () => { ); expect(code).toBe( - `import definition from "/tmp/foo\\"bar/background.ts";`, + `import definition from file:///tmp/foo%22bar/background.ts;`, ); }, ); diff --git a/packages/wxt/src/core/builders/vite/plugins/resolveVirtualModules.ts b/packages/wxt/src/core/builders/vite/plugins/resolveVirtualModules.ts index 272b988a5..6b886e4fc 100644 --- a/packages/wxt/src/core/builders/vite/plugins/resolveVirtualModules.ts +++ b/packages/wxt/src/core/builders/vite/plugins/resolveVirtualModules.ts @@ -1,4 +1,5 @@ import { readFile } from 'node:fs/promises'; +import { pathToFileURL } from 'node:url'; import { resolve } from 'path'; import type { Plugin } from 'vite'; import { ResolvedConfig } from '../../../../types'; @@ -41,7 +42,7 @@ export function resolveVirtualModules(config: ResolvedConfig): Plugin[] { resolve(config.wxtModuleDir, `dist/virtual/${name}.mjs`), 'utf-8', ); - const escapedPath = JSON.stringify(inputPath); + const escapedPath = pathToFileURL(inputPath).href; const code = template .replace(`'${userVirtualId}'`, escapedPath) .replace(`"${userVirtualId}"`, escapedPath); From 6362997b548ca177d3910bdc95ebc5b6498f7b79 Mon Sep 17 00:00:00 2001 From: Aaron Date: Sun, 26 Apr 2026 17:12:44 -0500 Subject: [PATCH 4/4] Always use double quotes to avoid apostrophe conflicts in paths --- .../vite/plugins/__tests__/resolveVirtualModules.test.ts | 4 ++-- .../src/core/builders/vite/plugins/resolveVirtualModules.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/wxt/src/core/builders/vite/plugins/__tests__/resolveVirtualModules.test.ts b/packages/wxt/src/core/builders/vite/plugins/__tests__/resolveVirtualModules.test.ts index 5b0224de0..00806c984 100644 --- a/packages/wxt/src/core/builders/vite/plugins/__tests__/resolveVirtualModules.test.ts +++ b/packages/wxt/src/core/builders/vite/plugins/__tests__/resolveVirtualModules.test.ts @@ -44,7 +44,7 @@ describe('resolveVirtualModules', () => { ); expect(code).toBe( - `import definition from file:///tmp/foo'bar/background.ts;`, + `import definition from "file:///tmp/foo'bar/background.ts";`, ); }, ); @@ -79,7 +79,7 @@ describe('resolveVirtualModules', () => { ); expect(code).toBe( - `import definition from file:///tmp/foo%22bar/background.ts;`, + `import definition from "file:///tmp/foo%22bar/background.ts";`, ); }, ); diff --git a/packages/wxt/src/core/builders/vite/plugins/resolveVirtualModules.ts b/packages/wxt/src/core/builders/vite/plugins/resolveVirtualModules.ts index 6b886e4fc..d465a7ea6 100644 --- a/packages/wxt/src/core/builders/vite/plugins/resolveVirtualModules.ts +++ b/packages/wxt/src/core/builders/vite/plugins/resolveVirtualModules.ts @@ -44,8 +44,8 @@ export function resolveVirtualModules(config: ResolvedConfig): Plugin[] { ); const escapedPath = pathToFileURL(inputPath).href; const code = template - .replace(`'${userVirtualId}'`, escapedPath) - .replace(`"${userVirtualId}"`, escapedPath); + .replace(`'${userVirtualId}'`, `"${escapedPath}"`) + .replace(`"${userVirtualId}"`, `"${escapedPath}"`); if (code === template) { throw Error( `Failed to resolve virtual module "${name}": expected template import "${userVirtualId}"`,