Skip to content

Commit f8e3d8b

Browse files
authored
fix: ensure assets are served gzip in preview (#11377)
1 parent 102e4a5 commit f8e3d8b

File tree

3 files changed

+132
-100
lines changed

3 files changed

+132
-100
lines changed

.changeset/serious-pears-joke.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@sveltejs/kit": patch
3+
---
4+
5+
fix: ensure assets are served gzip in preview

packages/kit/src/exports/vite/index.js

+1-50
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import fs from 'node:fs';
2-
import path, { join } from 'node:path';
2+
import path from 'node:path';
33

44
import { svelte } from '@sveltejs/vite-plugin-svelte';
55
import colors from 'kleur';
@@ -19,14 +19,12 @@ import { dev } from './dev/index.js';
1919
import { is_illegal, module_guard, normalize_id } from './graph_analysis/index.js';
2020
import { preview } from './preview/index.js';
2121
import { get_config_aliases, get_env, strip_virtual_prefix } from './utils.js';
22-
import { SVELTE_KIT_ASSETS } from '../../constants.js';
2322
import { write_client_manifest } from '../../core/sync/write_client_manifest.js';
2423
import prerender from '../../core/postbuild/prerender.js';
2524
import analyse from '../../core/postbuild/analyse.js';
2625
import { s } from '../../utils/misc.js';
2726
import { hash } from '../../runtime/hash.js';
2827
import { dedent, isSvelte5Plus } from '../../core/sync/utils.js';
29-
import sirv from 'sirv';
3028
import {
3129
env_dynamic_private,
3230
env_dynamic_public,
@@ -622,31 +620,6 @@ function kit({ svelte_config }) {
622620
* @see https://vitejs.dev/guide/api-plugin.html#configurepreviewserver
623621
*/
624622
configurePreviewServer(vite) {
625-
// generated client assets and the contents of `static`
626-
// should we use Vite's built-in asset server for this?
627-
// we would need to set the outDir to do so
628-
const { paths } = svelte_config.kit;
629-
const assets = paths.assets ? SVELTE_KIT_ASSETS : paths.base;
630-
vite.middlewares.use(
631-
scoped(
632-
assets,
633-
sirv(join(svelte_config.kit.outDir, 'output/client'), {
634-
setHeaders: (res, pathname) => {
635-
if (pathname.startsWith(`/${svelte_config.kit.appDir}/immutable`)) {
636-
res.setHeader('cache-control', 'public,max-age=31536000,immutable');
637-
}
638-
if (vite_config.preview.cors) {
639-
res.setHeader('Access-Control-Allow-Origin', '*');
640-
res.setHeader(
641-
'Access-Control-Allow-Headers',
642-
'Origin, Content-Type, Accept, Range'
643-
);
644-
}
645-
}
646-
})
647-
)
648-
);
649-
650623
return preview(vite, vite_config, svelte_config);
651624
},
652625

@@ -939,25 +912,3 @@ const create_service_worker_module = (config) => dedent`
939912
export const prerendered = [];
940913
export const version = ${s(config.kit.version.name)};
941914
`;
942-
943-
/**
944-
* @param {string} scope
945-
* @param {(req: import('http').IncomingMessage, res: import('http').ServerResponse, next: () => void) => void} handler
946-
* @returns {(req: import('http').IncomingMessage, res: import('http').ServerResponse, next: () => void) => void}
947-
*/
948-
function scoped(scope, handler) {
949-
if (scope === '') return handler;
950-
951-
return (req, res, next) => {
952-
if (req.url?.startsWith(scope)) {
953-
const original_url = req.url;
954-
req.url = req.url.slice(scope.length);
955-
handler(req, res, () => {
956-
req.url = original_url;
957-
next();
958-
});
959-
} else {
960-
next();
961-
}
962-
};
963-
}

packages/kit/src/exports/vite/preview/index.js

+126-50
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { loadEnv, normalizePath } from 'vite';
77
import { getRequest, setResponse } from '../../../exports/node/index.js';
88
import { installPolyfills } from '../../../exports/node/polyfills.js';
99
import { SVELTE_KIT_ASSETS } from '../../../constants.js';
10+
import { not_found } from '../utils.js';
1011

1112
/** @typedef {import('http').IncomingMessage} Req */
1213
/** @typedef {import('http').ServerResponse} Res */
@@ -21,6 +22,7 @@ export async function preview(vite, vite_config, svelte_config) {
2122
installPolyfills();
2223

2324
const { paths } = svelte_config.kit;
25+
const base = paths.base;
2426
const assets = paths.assets ? SVELTE_KIT_ASSETS : paths.base;
2527

2628
const protocol = vite_config.preview.https ? 'https' : 'http';
@@ -49,79 +51,131 @@ export async function preview(vite, vite_config, svelte_config) {
4951
});
5052

5153
return () => {
52-
// prerendered dependencies
54+
// Remove the base middleware. It screws with the URL.
55+
// It also only lets through requests beginning with the base path, so that requests beginning
56+
// with the assets URL never reach us. We could serve assets separately before the base
57+
// middleware, but we'd need that to occur after the compression and cors middlewares, so would
58+
// need to insert it manually into the stack, which would be at least as bad as doing this.
59+
for (let i = vite.middlewares.stack.length - 1; i > 0; i--) {
60+
// @ts-expect-error using internals
61+
if (vite.middlewares.stack[i].handle.name === 'viteBaseMiddleware') {
62+
vite.middlewares.stack.splice(i, 1);
63+
}
64+
}
65+
66+
// generated client assets and the contents of `static`
5367
vite.middlewares.use(
54-
mutable(join(svelte_config.kit.outDir, 'output/prerendered/dependencies'))
68+
scoped(
69+
assets,
70+
sirv(join(svelte_config.kit.outDir, 'output/client'), {
71+
setHeaders: (res, pathname) => {
72+
// only apply to immutable directory, not e.g. version.json
73+
if (pathname.startsWith(`/${svelte_config.kit.appDir}/immutable`)) {
74+
res.setHeader('cache-control', 'public,max-age=31536000,immutable');
75+
}
76+
}
77+
})
78+
)
5579
);
5680

57-
// prerendered pages (we can't just use sirv because we need to
58-
// preserve the correct trailingSlash behaviour)
5981
vite.middlewares.use((req, res, next) => {
60-
let if_none_match_value = req.headers['if-none-match'];
61-
62-
if (if_none_match_value?.startsWith('W/"')) {
63-
if_none_match_value = if_none_match_value.substring(2);
64-
}
65-
66-
if (if_none_match_value === etag) {
67-
res.statusCode = 304;
82+
const original_url = /** @type {string} */ (req.url);
83+
const { pathname, search } = new URL(original_url, 'http://dummy');
84+
85+
// if `paths.base === '/a/b/c`, then the root route is `/a/b/c/`,
86+
// regardless of the `trailingSlash` route option
87+
if (base.length > 1 && pathname === base) {
88+
let location = base + '/';
89+
if (search) location += search;
90+
res.writeHead(307, {
91+
location
92+
});
6893
res.end();
6994
return;
7095
}
7196

72-
const { pathname, search } = new URL(/** @type {string} */ (req.url), 'http://dummy');
97+
if (pathname.startsWith(base)) {
98+
next();
99+
} else {
100+
res.statusCode = 404;
101+
not_found(req, res, base);
102+
}
103+
});
73104

74-
let filename = normalizePath(
75-
join(svelte_config.kit.outDir, 'output/prerendered/pages' + pathname)
76-
);
77-
let prerendered = is_file(filename);
105+
// prerendered dependencies
106+
vite.middlewares.use(
107+
scoped(base, mutable(join(svelte_config.kit.outDir, 'output/prerendered/dependencies')))
108+
);
78109

79-
if (!prerendered) {
80-
const has_trailing_slash = pathname.endsWith('/');
81-
const html_filename = `${filename}${has_trailing_slash ? 'index.html' : '.html'}`;
110+
// prerendered pages (we can't just use sirv because we need to
111+
// preserve the correct trailingSlash behaviour)
112+
vite.middlewares.use(
113+
scoped(base, (req, res, next) => {
114+
let if_none_match_value = req.headers['if-none-match'];
82115

83-
/** @type {string | undefined} */
84-
let redirect;
116+
if (if_none_match_value?.startsWith('W/"')) {
117+
if_none_match_value = if_none_match_value.substring(2);
118+
}
85119

86-
if (is_file(html_filename)) {
87-
filename = html_filename;
88-
prerendered = true;
89-
} else if (has_trailing_slash) {
90-
if (is_file(filename.slice(0, -1) + '.html')) {
91-
redirect = pathname.slice(0, -1);
92-
}
93-
} else if (is_file(filename + '/index.html')) {
94-
redirect = pathname + '/';
120+
if (if_none_match_value === etag) {
121+
res.statusCode = 304;
122+
res.end();
123+
return;
95124
}
96125

97-
if (redirect) {
98-
if (search) redirect += search;
99-
res.writeHead(307, {
100-
location: redirect
101-
});
126+
const { pathname, search } = new URL(/** @type {string} */ (req.url), 'http://dummy');
127+
128+
let filename = normalizePath(
129+
join(svelte_config.kit.outDir, 'output/prerendered/pages' + pathname)
130+
);
131+
let prerendered = is_file(filename);
132+
133+
if (!prerendered) {
134+
const has_trailing_slash = pathname.endsWith('/');
135+
const html_filename = `${filename}${has_trailing_slash ? 'index.html' : '.html'}`;
136+
137+
/** @type {string | undefined} */
138+
let redirect;
139+
140+
if (is_file(html_filename)) {
141+
filename = html_filename;
142+
prerendered = true;
143+
} else if (has_trailing_slash) {
144+
if (is_file(filename.slice(0, -1) + '.html')) {
145+
redirect = pathname.slice(0, -1);
146+
}
147+
} else if (is_file(filename + '/index.html')) {
148+
redirect = pathname + '/';
149+
}
102150

103-
res.end();
151+
if (redirect) {
152+
if (search) redirect += search;
153+
res.writeHead(307, {
154+
location: redirect
155+
});
104156

105-
return;
157+
res.end();
158+
159+
return;
160+
}
106161
}
107-
}
108162

109-
if (prerendered) {
110-
res.writeHead(200, {
111-
'content-type': lookup(pathname) || 'text/html',
112-
etag
113-
});
163+
if (prerendered) {
164+
res.writeHead(200, {
165+
'content-type': lookup(pathname) || 'text/html',
166+
etag
167+
});
114168

115-
fs.createReadStream(filename).pipe(res);
116-
} else {
117-
next();
118-
}
119-
});
169+
fs.createReadStream(filename).pipe(res);
170+
} else {
171+
next();
172+
}
173+
})
174+
);
120175

121176
// SSR
122177
vite.middlewares.use(async (req, res) => {
123178
const host = req.headers['host'];
124-
req.url = req.originalUrl;
125179

126180
const request = await getRequest({
127181
base: `${protocol}://${host}`,
@@ -155,6 +209,28 @@ const mutable = (dir) =>
155209
})
156210
: (_req, _res, next) => next();
157211

212+
/**
213+
* @param {string} scope
214+
* @param {Handler} handler
215+
* @returns {Handler}
216+
*/
217+
function scoped(scope, handler) {
218+
if (scope === '') return handler;
219+
220+
return (req, res, next) => {
221+
if (req.url?.startsWith(scope)) {
222+
const original_url = req.url;
223+
req.url = req.url.slice(scope.length);
224+
handler(req, res, () => {
225+
req.url = original_url;
226+
next();
227+
});
228+
} else {
229+
next();
230+
}
231+
};
232+
}
233+
158234
/** @param {string} path */
159235
function is_file(path) {
160236
return fs.existsSync(path) && !fs.statSync(path).isDirectory();

0 commit comments

Comments
 (0)