diff --git a/packages/vite/src/client/client.ts b/packages/vite/src/client/client.ts index b909c1328a9a3b..4e57c9de3f765a 100644 --- a/packages/vite/src/client/client.ts +++ b/packages/vite/src/client/client.ts @@ -625,9 +625,16 @@ export function injectQuery(url: string, queryToInject: string): string { // can't use pathname from URL since it may be relative like ../ const pathname = url.replace(/#.*$/, '').replace(/\?.*$/, '') - const { search, hash } = new URL(url, 'http://vitejs.dev') + const { searchParams, hash } = new URL(url, 'http://vitejs.dev') - return `${pathname}?${queryToInject}${search ? `&` + search.slice(1) : ''}${ + // clean up existing query to avoid ?import&import etc. + searchParams.delete(queryToInject.split('=')[0]) + const search = searchParams + .toString() + // clean up blank string values (e.g. ?vue= becomes ?vue) + .replace(/=(&|$)/g, '$1') + + return `${pathname}?${queryToInject}${search ? `&` + search : ''}${ hash || '' }` } diff --git a/packages/vite/src/node/__tests__/utils.spec.ts b/packages/vite/src/node/__tests__/utils.spec.ts index 4b80e20814b82f..4357503c102c20 100644 --- a/packages/vite/src/node/__tests__/utils.spec.ts +++ b/packages/vite/src/node/__tests__/utils.spec.ts @@ -111,6 +111,72 @@ describe('injectQuery', () => { '/usr/vite/東京 %20 hello?direct', ) }) + + test('path with injected query already present', () => { + expect(injectQuery('/usr/vite/query?direct', 'direct')).toEqual( + '/usr/vite/query?direct', + ) + }) + + test('path with injected query already present multiple times', () => { + expect(injectQuery('/usr/vite/query?direct&direct', 'direct')).toEqual( + '/usr/vite/query?direct', + ) + }) + + test('path with injected query already present when providing a key-value pair for the injected query', () => { + expect(injectQuery('/usr/vite/query?foo', 'foo=bar')).toEqual( + '/usr/vite/query?foo=bar', + ) + }) + + test('path with injected query already present with a value', () => { + expect(injectQuery('/usr/vite/query?direct=value', 'direct')).toEqual( + '/usr/vite/query?direct', + ) + }) + + test('path with injected query already present with a value when providing a key-value pair for the injected query', () => { + expect(injectQuery('/usr/vite/query?foo=oldValue', 'foo=newValue')).toEqual( + '/usr/vite/query?foo=newValue', + ) + }) + + test('path with injected query already present, along with other query params at the start', () => { + expect( + injectQuery( + '/usr/vite/file.vue?vue&type=template&lang.js&direct', + 'direct', + ), + ).toEqual('/usr/vite/file.vue?direct&vue&type=template&lang.js') + }) + + test('path with injected query already present, along with other query params at the end', () => { + expect( + injectQuery( + '/usr/vite/file.vue?direct&vue&type=template&lang.js', + 'direct', + ), + ).toEqual('/usr/vite/file.vue?direct&vue&type=template&lang.js') + }) + + test('path with injected query already present with a value defined, along with other query params at the start', () => { + expect( + injectQuery( + '/usr/vite/file.vue?vue&type=template&lang.js&direct=value', + 'direct', + ), + ).toEqual('/usr/vite/file.vue?direct&vue&type=template&lang.js') + }) + + test('path with injected query already present with a value defined, along with other query params at the end', () => { + expect( + injectQuery( + '/usr/vite/file.vue?direct=value&vue&type=template&lang.js', + 'direct', + ), + ).toEqual('/usr/vite/file.vue?direct&vue&type=template&lang.js') + }) }) describe('resolveHostname', () => { diff --git a/packages/vite/src/node/utils.ts b/packages/vite/src/node/utils.ts index be53f4c0636e6c..1c780cc6942b64 100644 --- a/packages/vite/src/node/utils.ts +++ b/packages/vite/src/node/utils.ts @@ -320,10 +320,18 @@ export function injectQuery(url: string, queryToInject: string): string { url.replace(replacePercentageRE, '%25'), 'relative:///', ) - const { search, hash } = resolvedUrl + const { searchParams, hash } = resolvedUrl let pathname = cleanUrl(url) pathname = isWindows ? slash(pathname) : pathname - return `${pathname}?${queryToInject}${search ? `&` + search.slice(1) : ''}${ + + // clean up existing query to avoid ?import&import etc. + searchParams.delete(queryToInject.split('=')[0]) + const search = searchParams + .toString() + // clean up blank string values (e.g. ?vue= becomes ?vue) + .replace(/=(&|$)/g, '$1') + + return `${pathname}?${queryToInject}${search ? `&` + search : ''}${ hash ?? '' }` }