Skip to content

Commit 9d2def1

Browse files
committed
fix: plugin-vite: strip query from URLs before loading (#3895)
1 parent 86d6cde commit 9d2def1

4 files changed

Lines changed: 81 additions & 5 deletions

File tree

packages/plugin-vite/src/plugins/deno.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
import * as path from "@std/path";
1010
import * as babel from "@babel/core";
1111
import { httpAbsolute } from "./patches/http_absolute.ts";
12-
import { JS_REG, JSX_REG } from "../utils.ts";
12+
import { cleanId, JS_REG, JSX_REG } from "../utils.ts";
1313
import { builtinModules } from "node:module";
1414

1515
// @ts-ignore Workaround for https://github.com/denoland/deno/issues/30850
@@ -217,7 +217,9 @@ export function deno(): Plugin {
217217
return;
218218
}
219219

220-
const url = path.toFileUrl(id);
220+
// Vite appends suffixes like `?v=<hash>` to dependency ids. They are
221+
// not part of the file path and must be dropped before hitting the fs.
222+
const url = path.toFileUrl(cleanId(id));
221223

222224
const result = await loader.load(url.href, meta.type);
223225
if (result.kind === "external") {
@@ -257,7 +259,7 @@ export function deno(): Plugin {
257259
const { specifier } = parseDenoSpecifier(id);
258260
actualId = specifier;
259261
}
260-
actualId = actualId.replace("?commonjs-es-import", "");
262+
actualId = cleanId(actualId);
261263

262264
if (actualId.startsWith("\0")) {
263265
actualId = actualId.slice(1);

packages/plugin-vite/src/utils.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,18 @@ import type { ImportCheck } from "./plugins/verify_imports.ts";
55
export const JS_REG = /\.([tj]sx?|[mc]?[tj]s)(\?.*)?$/;
66
export const JSX_REG = /\.[tj]sx(\?.*)?$/;
77

8+
const QUERY_REG = /[?#].*$/s;
9+
10+
/**
11+
* Strip Vite's query and hash suffixes from a module id, so that it can be
12+
* treated as a file path again. Vite appends suffixes like `?v=<hash>` to
13+
* dependency ids when `optimizeDeps` is active, `?commonjs-es-import` for
14+
* interop shims and `?t=<timestamp>` on HMR updates.
15+
*/
16+
export function cleanId(id: string): string {
17+
return id.replace(QUERY_REG, "");
18+
}
19+
820
export function pathWithRoot(fileOrDir: string, root?: string): string {
921
if (path.isAbsolute(fileOrDir)) return fileOrDir;
1022

packages/plugin-vite/tests/dev_server_test.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -564,6 +564,65 @@ Deno.test({
564564
sanitizeResources: false,
565565
});
566566

567+
// issue: https://github.com/freshframework/fresh/issues/3895
568+
// Vite appends a `?v=<hash>` suffix to dependency ids when `optimizeDeps` is
569+
// active. The Deno loader must strip it instead of treating it as part of the
570+
// file path, in both the client and the ssr environment.
571+
integrationTest("vite dev - works with optimizeDeps enabled", async () => {
572+
const fixture = path.join(FIXTURE_DIR, "no_islands");
573+
await using tmp = await prepareDevServer(fixture, {
574+
config: `import { defineConfig } from "vite";
575+
import { fresh } from "@fresh/plugin-vite";
576+
577+
export default defineConfig({
578+
plugins: [fresh()],
579+
optimizeDeps: { include: ["preact"] },
580+
environments: {
581+
ssr: { optimizeDeps: { include: ["preact"] } },
582+
},
583+
});
584+
`,
585+
});
586+
587+
await launchDevServer(tmp.dir, async (address) => {
588+
// ssr environment
589+
const res = await fetch(address);
590+
const text = await res.text();
591+
expect(res.status).toEqual(200);
592+
expect(text).toContain("ok");
593+
594+
// client environment: walk the module graph from the client entry. Vite
595+
// versions dependency ids whether or not it pre-bundles them, so the
596+
// graph contains `?v=<hash>` urls that must still resolve to a file.
597+
const seen = new Set<string>();
598+
const queue = ["/@id/fresh:client-entry"];
599+
const failed: string[] = [];
600+
let versioned = 0;
601+
602+
while (queue.length > 0) {
603+
const url = queue.pop()!;
604+
if (seen.has(url) || url.startsWith("/@vite/")) continue;
605+
seen.add(url);
606+
607+
const modRes = await fetch(`${address}${url}`);
608+
const code = await modRes.text();
609+
if (modRes.status !== 200) {
610+
failed.push(`${modRes.status} ${url}`);
611+
continue;
612+
}
613+
614+
if (url.includes("?v=")) versioned++;
615+
616+
for (const match of code.matchAll(/from "(\/[^"]+)"/g)) {
617+
queue.push(match[1]);
618+
}
619+
}
620+
621+
expect(failed).toEqual([]);
622+
expect(versioned).toBeGreaterThan(0);
623+
});
624+
});
625+
567626
// issue: https://github.com/denoland/fresh/issues/3666
568627
integrationTest(
569628
"vite dev - basePath does not intercept Vite URLs",

packages/plugin-vite/tests/test_utils.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,10 @@ async function copyDir(from: string, to: string) {
4949
}
5050
}
5151

52-
export async function prepareDevServer(fixtureDir: string) {
52+
export async function prepareDevServer(
53+
fixtureDir: string,
54+
options: { config?: string } = {},
55+
) {
5356
const tmp = await withTmpDir({
5457
dir: path.join(import.meta.dirname!, ".."),
5558
prefix: "tmp_vite_",
@@ -59,7 +62,7 @@ export async function prepareDevServer(fixtureDir: string) {
5962

6063
await Deno.writeTextFile(
6164
path.join(tmp.dir, "vite.config.ts"),
62-
`import { defineConfig } from "vite";
65+
options.config ?? `import { defineConfig } from "vite";
6366
import { fresh } from "@fresh/plugin-vite";
6467
6568
export default defineConfig({

0 commit comments

Comments
 (0)