11import fs from "node:fs" ;
22import { createRequire } from "node:module" ;
3- import { decode , encode } from "node:querystring" ;
43import { type FilterPattern , createFilter } from "@rollup/pluginutils" ;
54import { imageSize } from "image-size" ;
65import type { NextConfigComplete } from "next/dist/server/config-shared.js" ;
@@ -21,10 +20,29 @@ const warnOnce = (message: string) => {
2120const includePattern = / \. ( p n g | j p g | j p e g | g i f | w e b p | a v i f | i c o | b m p | s v g ) $ / ;
2221const excludeImporterPattern = / \. ( c s s | s c s s | s a s s ) $ / ;
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:" ;
2427const virtualImage = "virtual:next-image" ;
2528const virtualNextImage = "virtual:next/image" ;
2629const 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+
2846const require = createRequire ( import . meta. url ) ;
2947
3048export 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