Skip to content

Commit 648cff7

Browse files
authored
fix(vite): decode percent-encoded /ns/m request paths (#11483)
[skip ci]
1 parent 3552172 commit 648cff7

4 files changed

Lines changed: 60 additions & 0 deletions

File tree

‎packages/vite/hmr/frameworks/vue/server/sfc-route-serve.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ export function registerSfcServeRoute(server: ViteDevServer, options: RegisterSf
5757
}
5858
if (pathStyle && pathStyle !== '/' && !pathParam) {
5959
if (!pathStyle.startsWith('/')) pathStyle = '/' + pathStyle;
60+
// URL.pathname stays percent-encoded; decode before the query is appended.
61+
try {
62+
pathStyle = decodeURI(pathStyle);
63+
} catch {}
6064
// Include endpoint query for variant-style requests (e.g. /ns/sfc/Comp.vue?vue&type=template)
6165
pathParam = pathStyle + (urlObj.search || '');
6266
}

‎packages/vite/hmr/frameworks/vue/server/websocket-sfc.spec.ts‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,17 @@ describe('registerSfcHandlers', () => {
8888
expect(res.body).toContain('export { default } from "/ns/asm?path=%2Fsrc%2FApp.vue";');
8989
});
9090

91+
it('decodes percent-encoded path-style specs (device-encoded bracketed files)', async () => {
92+
const { sfc, transformRequest } = mount();
93+
transformRequest.mockResolvedValue({ code: 'export default {}' });
94+
const res = makeRes();
95+
await sfc({ url: '/ns/sfc/src/pages/%5Bid%5D.vue' }, res, vi.fn());
96+
expect(transformRequest).toHaveBeenCalledWith('/src/pages/[id].vue?vue');
97+
expect(res.statusCode).toBe(200);
98+
expect(res.body).toContain('path=/src/pages/[id].vue');
99+
expect(res.body).toContain(`export * from "/ns/asm?path=${encodeURIComponent('/src/pages/[id].vue')}";`);
100+
});
101+
91102
it('returns an empty module for style variants', async () => {
92103
const { sfc, transformRequest } = mount();
93104
transformRequest.mockResolvedValue({ code: '/* css */' });

‎packages/vite/hmr/server/websocket-ns-m-request.spec.ts‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,42 @@ describe('createNsMRequestContext', () => {
1919
expect(result.value.bootTaggedRequest).toBe(false);
2020
});
2121

22+
it('decodes percent-encoded pathname specs (device-encoded bracketed files)', () => {
23+
const encoded = createNsMRequestContext('/ns/m/packages/app/src/app/demo/%5Bid%5D.tsrx?import', '/workspace', '/src/');
24+
const raw = createNsMRequestContext('/ns/m/packages/app/src/app/demo/[id].tsrx?import', '/workspace', '/src/');
25+
26+
expect(encoded.kind).toBe('context');
27+
expect(raw.kind).toBe('context');
28+
if (encoded.kind !== 'context' || raw.kind !== 'context') {
29+
return;
30+
}
31+
32+
expect(encoded.value.spec).toBe('/packages/app/src/app/demo/[id].tsrx');
33+
expect(encoded.value.spec).toBe(raw.value.spec);
34+
});
35+
36+
it('does not double-decode the already-decoded ?path= spec', () => {
37+
const result = createNsMRequestContext('/ns/m/?path=/app/demo/%255Bid%255D.tsrx', '/workspace', '/src/');
38+
39+
expect(result.kind).toBe('context');
40+
if (result.kind !== 'context') {
41+
return;
42+
}
43+
44+
expect(result.value.spec).toBe('/app/demo/%5Bid%5D.tsrx');
45+
});
46+
47+
it('leaves encoded reserved characters encoded, matching Vite', () => {
48+
const result = createNsMRequestContext('/ns/m/app/a%3Fb%23c%2Fd.ts', '/workspace', '/src/');
49+
50+
expect(result.kind).toBe('context');
51+
if (result.kind !== 'context') {
52+
return;
53+
}
54+
55+
expect(result.value.spec).toBe('/app/a%3Fb%23c%2Fd.ts');
56+
});
57+
2258
it('returns a response module for blocked build-time node_modules imports', () => {
2359
const result = createNsMRequestContext('/ns/m/node_modules/vite/dist/index.js', '/workspace', '/src/');
2460

‎packages/vite/hmr/server/websocket-ns-m-request.ts‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,12 +89,14 @@ export function createNsMRequestContext(requestUrl: string, serverRoot: string,
8989
let spec = urlObj.searchParams.get('path') || '';
9090
let forcedVer = urlObj.searchParams.get('v');
9191
let bootTaggedRequest = false;
92+
let specFromPathname = false;
9293

9394
if (!spec) {
9495
const base = '/ns/m';
9596
const rest = urlObj.pathname.slice(base.length);
9697
if (rest && rest !== '/') {
9798
spec = rest;
99+
specFromPathname = true;
98100
}
99101
}
100102

@@ -115,6 +117,13 @@ export function createNsMRequestContext(requestUrl: string, serverRoot: string,
115117
}
116118

117119
spec = spec.replace(/[?#].*$/, '');
120+
// URL.pathname stays percent-encoded (URLSearchParams already decoded
121+
// `path`). decodeURI matches Vite's transform middleware.
122+
if (specFromPathname) {
123+
try {
124+
spec = decodeURI(spec);
125+
} catch {}
126+
}
118127
const decorated = collapseLegacyNsMTags(spec, 'inbound-request-spec');
119128
spec = decorated.cleanedSpec;
120129
bootTaggedRequest = decorated.bootTaggedRequest;

0 commit comments

Comments
 (0)