Skip to content

Commit ed88a44

Browse files
Fix SVG imports failing under square-bracket directories
Use URL-safe base64 encoding for image paths in virtual module IDs to avoid issues with special characters like square brackets being decoded by Vite's decodeURI function. Co-authored-by: valentinpalkovic <[email protected]>
1 parent 4197d5a commit ed88a44

File tree

1 file changed

+28
-7
lines changed

1 file changed

+28
-7
lines changed

src/plugins/next-image/plugin.ts

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import fs from "node:fs";
22
import { createRequire } from "node:module";
3-
import { decode, encode } from "node:querystring";
43
import { type FilterPattern, createFilter } from "@rollup/pluginutils";
54
import { imageSize } from "image-size";
65
import type { NextConfigComplete } from "next/dist/server/config-shared.js";
@@ -21,10 +20,29 @@ const warnOnce = (message: string) => {
2120
const includePattern = /\.(png|jpg|jpeg|gif|webp|avif|ico|bmp|svg)$/;
2221
const excludeImporterPattern = /\.(css|scss|sass)$/;
2322

23+
// Use null byte prefix for virtual module IDs
24+
// Use URL-safe base64 to encode the image path to avoid issues with special characters
25+
// like square brackets that are decoded by decodeURI
26+
const virtualImagePrefix = "\0virtual:next-image:";
2427
const virtualImage = "virtual:next-image";
2528
const virtualNextImage = "virtual:next/image";
2629
const virtualNextLegacyImage = "virtual:next/legacy/image";
2730

31+
// URL-safe base64 encoding/decoding functions
32+
function encodeBase64Url(str: string): string {
33+
const base64 = Buffer.from(str).toString("base64");
34+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
35+
}
36+
37+
function decodeBase64Url(str: string): string {
38+
// Add back padding if needed
39+
const padding = (4 - (str.length % 4)) % 4;
40+
const withPadding = str + "=".repeat(padding);
41+
// Convert URL-safe base64 back to standard base64
42+
const base64 = withPadding.replace(/-/g, "+").replace(/_/g, "/");
43+
return Buffer.from(base64, "base64").toString();
44+
}
45+
2846
const require = createRequire(import.meta.url);
2947

3048
export type NextImagePluginOptions = {
@@ -104,7 +122,7 @@ export function vitePluginNextImage(
104122
if (
105123
includePattern.test(source) &&
106124
!excludeImporterPattern.test(importer ?? "") &&
107-
!importer?.startsWith(virtualImage)
125+
!importer?.startsWith(virtualImagePrefix)
108126
) {
109127
const isAbsolute = path.isAbsolute(id);
110128
const imagePath = importer
@@ -119,7 +137,10 @@ export function vitePluginNextImage(
119137
return null;
120138
}
121139

122-
return `${virtualImage}?${encode({ imagePath })}`;
140+
// Use null byte prefix to embed the image path in the virtual module ID
141+
// Use URL-safe base64 encoding to avoid issues with special characters like
142+
// square brackets that get decoded by Vite's decodeURI
143+
return `${virtualImagePrefix}${encodeBase64Url(imagePath)}`;
123144
}
124145

125146
if (id === "next/image" && importer !== virtualNextImage) {
@@ -153,10 +174,10 @@ export function vitePluginNextImage(
153174
).toString("utf-8");
154175
}
155176

156-
const [source, query] = id.split("?");
157-
158-
if (virtualImage === source) {
159-
const imagePath = decode(query).imagePath as string;
177+
// Handle virtual image modules with null byte prefix
178+
if (id.startsWith(virtualImagePrefix)) {
179+
// Decode the URL-safe base64 encoded image path
180+
const imagePath = decodeBase64Url(id.slice(virtualImagePrefix.length));
160181

161182
const nextConfig = await nextConfigResolver.promise;
162183

0 commit comments

Comments
 (0)