Skip to content

Commit ea39c8f

Browse files
committed
fix(vite): keep the ?raw query on HMR imports
Under HMR, `import svg from './icon.svg?raw'` reached the device without its query. The served-module rewriter routes a specifier by its path and drops the query, so the device fetched `/ns/m/src/icon.svg`, got the asset module (a URL) instead of the file's text, and `?raw` JSON imports were inlined as the parsed object. The /ns/m route also discarded the query on the way in, so it could not have served the text either. The rewriter now routes a `?raw` specifier like the plain file and keeps `?raw` on the resulting /ns/m URL, and leaves `?raw` JSON imports alone. The /ns/m route serves a `?raw` request straight from Vite's raw load (`export default "<text>"`). The `raw=1` diagnostics flag is not a `?raw` import. Imports without `?raw` are unchanged.
1 parent aeac327 commit ea39c8f

5 files changed

Lines changed: 88 additions & 6 deletions

File tree

‎packages/vite/hmr/server/constants.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
// Generic query suffix
55
export const QUERY_PATTERN = /\?.*$/;
66

7+
// Vite's `?raw` import query (its own `rawRE`), which loads a file's text as
8+
// `export default "..."`. Doesn't match the `raw=1` diagnostics flag.
9+
export const RAW_QUERY_PATTERN = /[?&]raw(?:&|$)/;
10+
711
// Detect whether Vite's variable dynamic import helper is already inlined
812
export const VARIABLE_DYNAMIC_IMPORT_HELPER_PATTERN = /__variableDynamicImportRuntimeHelper\s*=/;
913

‎packages/vite/hmr/server/rewrite-imports.spec.ts‎

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
import { describe, expect, it } from 'vitest';
1+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'fs';
2+
import { tmpdir } from 'os';
3+
import { join } from 'path';
4+
import { afterAll, describe, expect, it } from 'vitest';
25

36
import { rewriteImports } from './rewrite-imports.js';
47

@@ -43,3 +46,30 @@ describe('rewriteImports — glued re-export normalization', () => {
4346
expect(out).toContain(`export const banner = 'hi';`);
4447
});
4548
});
49+
50+
describe('rewriteImports — ?raw imports', () => {
51+
const ORIGIN = 'http://localhost:5173';
52+
const root = mkdtempSync(join(tmpdir(), 'ns-rewrite-raw-'));
53+
mkdirSync(join(root, 'node_modules', 'icons', 'font'), { recursive: true });
54+
writeFileSync(join(root, 'node_modules', 'icons', 'font', 'icons.json'), '{ "house": 61713 }');
55+
afterAll(() => rmSync(root, { recursive: true, force: true }));
56+
const rewrite = (code: string) => rewriteImports(code, '/src/icons.ts', new Map(), new Map(), root, false, undefined, ORIGIN, true);
57+
58+
it('keeps ?raw on an app file import', () => {
59+
// Vite adds `import` to the query of non-JS imports.
60+
const out = rewrite(`import svg from "/src/icon.svg?import&raw";\n`);
61+
expect(out).toContain(`from "/ns/m/src/icon.svg?raw"`);
62+
});
63+
64+
it('keeps ?raw on a node_modules JSON import instead of inlining the parsed object', () => {
65+
const out = rewrite(`import icons from "/node_modules/icons/font/icons.json?import&raw";\n`);
66+
expect(out).toContain(`from "${ORIGIN}/ns/m/node_modules/icons/font/icons.json?raw"`);
67+
expect(out).not.toContain('const icons =');
68+
});
69+
70+
it('leaves imports without ?raw as they were', () => {
71+
const out = rewrite(`import svg from "/src/icon.svg?import";\nimport icons from "/node_modules/icons/font/icons.json?import";\n`);
72+
expect(out).toContain(`from "/ns/m/src/icon.svg"`);
73+
expect(out).toContain('const icons = { "house": 61713 };');
74+
});
75+
});

‎packages/vite/hmr/server/rewrite-imports.ts‎

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,10 @@ export function rewriteImports(code: string, importerPath: string, sfcFileMap: M
143143
// Inline JSON imports (package.json, config.json, etc.)
144144
// This must happen BEFORE other rewrites because JSON imports get a ?import query added by Vite
145145
result = result.replace(/import\s+(\w+)\s+from\s+["']([^"']+\.json(?:\?[^"']*)?)["'];?/g, (match, varName, jsonPath) => {
146+
// `?raw` wants the file's text, not the parsed object; it's served below.
147+
if (PAT.RAW_QUERY_PATTERN.test(jsonPath)) {
148+
return match;
149+
}
146150
try {
147151
// Remove query params like ?import
148152
const cleanPath = jsonPath.split('?')[0];
@@ -534,14 +538,26 @@ export function rewriteImports(code: string, importerPath: string, sfcFileMap: M
534538
return `${prefix}${spec}${suffix}`;
535539
};
536540

541+
// A `?raw` import (`import svg from './icon.svg?raw'`, which Vite emits as
542+
// `?import&raw`) is routed like the plain file, then keeps `?raw` on its
543+
// /ns/m URL so the route serves the file's text (see websocket-ns-m.ts).
544+
// The routing above drops queries, which served the asset module instead.
545+
const replaceImport = (match: string, prefix: string, spec: string, suffix: string): string => {
546+
if (typeof spec !== 'string' || !PAT.RAW_QUERY_PATTERN.test(spec)) {
547+
return replaceVueImport(match, prefix, spec, suffix);
548+
}
549+
const routed = replaceVueImport(match, prefix, spec.replace(PAT.QUERY_PATTERN, ''), suffix);
550+
return routed.replace(/(\/ns\/m\/[^"'?\s]+)(?=["'])/, '$1?raw');
551+
};
552+
537553
// Apply to all import/export patterns (but only .vue files will be rewritten)
538-
result = result.replace(PAT.IMPORT_PATTERN_1, replaceVueImport);
539-
result = result.replace(PAT.IMPORT_PATTERN_2, replaceVueImport);
540-
result = result.replace(PAT.EXPORT_PATTERN, replaceVueImport);
541-
result = result.replace(PAT.IMPORT_PATTERN_3, replaceVueImport);
554+
result = result.replace(PAT.IMPORT_PATTERN_1, replaceImport);
555+
result = result.replace(PAT.IMPORT_PATTERN_2, replaceImport);
556+
result = result.replace(PAT.EXPORT_PATTERN, replaceImport);
557+
result = result.replace(PAT.IMPORT_PATTERN_3, replaceImport);
542558
// Side-effect imports (import "spec") — must run AFTER named-import patterns
543559
// since IMPORT_PATTERN_1 already handles `import ... from "spec"`.
544-
result = result.replace(PAT.IMPORT_PATTERN_SIDE_EFFECT, replaceVueImport);
560+
result = result.replace(PAT.IMPORT_PATTERN_SIDE_EFFECT, replaceImport);
545561
result = ensureDynamicHmrImportHelper(result);
546562

547563
// Extra guard: map any lingering dynamic import('@') to a safe stub module path

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,24 @@ describe('registerNsModuleServerRoute', () => {
9898
expect(ensureInitialGraphPopulationStarted).toHaveBeenCalledTimes(1);
9999
});
100100

101+
it('serves the text Vite loads for a ?raw import, without the device passes', async () => {
102+
const sharedTransformRequest = vi.fn(async (url: string) => (url === '/src/icon.svg?raw' ? ({ code: 'export default "<svg/>"' } as any) : null));
103+
const processCodeForDevice = vi.fn((code: string) => code);
104+
const { handler } = mount({ sharedTransformRequest, processCodeForDevice });
105+
const res = makeRes();
106+
await handler({ url: '/ns/m/src/icon.svg?raw' }, res, vi.fn());
107+
expect(res.statusCode).toBe(200);
108+
expect(res.body).toBe('export default "<svg/>"');
109+
expect(processCodeForDevice).not.toHaveBeenCalled();
110+
});
111+
112+
it('does not read the raw=1 diagnostics flag as ?raw', async () => {
113+
const sharedTransformRequest = vi.fn(async () => null);
114+
const { handler } = mount({ sharedTransformRequest });
115+
await handler({ url: '/ns/m/src/app.ts?raw=1' }, makeRes(), vi.fn());
116+
expect(sharedTransformRequest.mock.calls.map(([url]) => url)).not.toContain('/src/app.ts?raw');
117+
});
118+
101119
it('serves a deterministic stub for the anomalous "@" import path', async () => {
102120
const { handler } = mount();
103121
const res = makeRes();

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { buildPiniaVendorShim, buildVueVendorShim } from './vendor-bare-module-s
1010
import { collapseLegacyNsMTags } from './websocket-ns-m-paths.js';
1111
import { createNsMRequestContext, resolveFsAnchoredSpec, resolveNsMTransformedModule } from './websocket-ns-m-request.js';
1212
import { setDeviceModuleHeaders } from './route-helpers.js';
13+
import { RAW_QUERY_PATTERN } from './constants.js';
1314
import { CSS_MODULE_RE, buildCssRegisterSnippetFromVar, normalizeCssForDevice } from './css-device-module.js';
1415
import { assertNoOptimizedArtifacts, buildBootProgressSnippet, canonicalizeRtImports, dedupeRtNamedImportsAgainstDestructures, deduplicateLinkerImports, ensureDestructureCoreImports, ensureGuardPlainDynamicImports, ensureVariableDynamicImportHelper, expandStarExports, hoistTopLevelStaticImports, MODULE_IMPORT_ANALYSIS_PLUGINS, wrapCommonJsModuleForDevice, ensureWorkerEntryGlobalsImport, classifyServedRequest } from './websocket-served-module-helpers.js';
1516
import { cleanCode, collectImportDependencies, isWorkerEntryModuleId, processCodeForDevice, rewriteImports } from './websocket-device-transform.js';
@@ -168,6 +169,19 @@ export function registerNsModuleServerRoute(server: ViteDevServer, options: Regi
168169
} catch {}
169170
}
170171

172+
// `?raw` imports keep the query on their /ns/m URL (rewriteImports).
173+
// Vite's raw load is the file's text as `export default "..."`,
174+
// which needs none of the device passes below.
175+
if (RAW_QUERY_PATTERN.test(urlObj.search)) {
176+
const rawSpec = resolveFsAnchoredSpec(spec, serverRoot, monorepoWorkspaceRoot) || spec;
177+
const rawRes = await sharedTransformRequest(`${rawSpec}?raw`);
178+
if (rawRes?.code) {
179+
res.statusCode = 200;
180+
res.end(rawRes.code);
181+
return;
182+
}
183+
}
184+
171185
// Worker-realm serves must NOT take the deps-bundle bridge: the
172186
// bundle evaluates the full dependency closure in the worker realm,
173187
// where Angular's injectables reject the entry evaluation

0 commit comments

Comments
 (0)