From 9bd5f6d6ce05adfd93c05004cbf6e9a68a30e072 Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Sun, 17 Dec 2023 20:23:07 -0800 Subject: [PATCH 1/6] fix: ensure assets are served gzip in preview --- .changeset/serious-pears-joke.md | 5 +++ packages/kit/src/exports/vite/index.js | 25 ----------- .../kit/src/exports/vite/preview/index.js | 45 +++++++++++++++++++ 3 files changed, 50 insertions(+), 25 deletions(-) create mode 100644 .changeset/serious-pears-joke.md diff --git a/.changeset/serious-pears-joke.md b/.changeset/serious-pears-joke.md new file mode 100644 index 000000000000..209cd02a8d49 --- /dev/null +++ b/.changeset/serious-pears-joke.md @@ -0,0 +1,5 @@ +--- +"@sveltejs/kit": patch +--- + +fix: ensure assets are served gzip in preview diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index dbd9c6c1c17a..0c944af5b407 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -626,31 +626,6 @@ function kit({ svelte_config }) { * @see https://vitejs.dev/guide/api-plugin.html#configurepreviewserver */ configurePreviewServer(vite) { - // generated client assets and the contents of `static` - // should we use Vite's built-in asset server for this? - // we would need to set the outDir to do so - const { paths } = svelte_config.kit; - const assets = paths.assets ? SVELTE_KIT_ASSETS : paths.base; - vite.middlewares.use( - scoped( - assets, - sirv(join(svelte_config.kit.outDir, 'output/client'), { - setHeaders: (res, pathname) => { - if (pathname.startsWith(`/${svelte_config.kit.appDir}/immutable`)) { - res.setHeader('cache-control', 'public,max-age=31536000,immutable'); - } - if (vite_config.preview.cors) { - res.setHeader('Access-Control-Allow-Origin', '*'); - res.setHeader( - 'Access-Control-Allow-Headers', - 'Origin, Content-Type, Accept, Range' - ); - } - } - }) - ) - ); - return preview(vite, vite_config, svelte_config); }, diff --git a/packages/kit/src/exports/vite/preview/index.js b/packages/kit/src/exports/vite/preview/index.js index fe44e4524deb..b7cac4eedc9d 100644 --- a/packages/kit/src/exports/vite/preview/index.js +++ b/packages/kit/src/exports/vite/preview/index.js @@ -49,6 +49,29 @@ export async function preview(vite, vite_config, svelte_config) { }); return () => { + // generated client assets and the contents of `static` + // should we use Vite's built-in asset server for this? + // we would need to set the outDir to do so + vite.middlewares.use( + scoped( + assets, + sirv(join(svelte_config.kit.outDir, 'output/client'), { + setHeaders: (res, pathname) => { + if (pathname.startsWith(`/${svelte_config.kit.appDir}/immutable`)) { + res.setHeader('cache-control', 'public,max-age=31536000,immutable'); + } + if (vite_config.preview.cors) { + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader( + 'Access-Control-Allow-Headers', + 'Origin, Content-Type, Accept, Range' + ); + } + } + }) + ) + ); + // prerendered dependencies vite.middlewares.use( mutable(join(svelte_config.kit.outDir, 'output/prerendered/dependencies')) @@ -159,3 +182,25 @@ const mutable = (dir) => function is_file(path) { return fs.existsSync(path) && !fs.statSync(path).isDirectory(); } + +/** + * @param {string} scope + * @param {(req: import('http').IncomingMessage, res: import('http').ServerResponse, next: () => void) => void} handler + * @returns {(req: import('http').IncomingMessage, res: import('http').ServerResponse, next: () => void) => void} + */ +function scoped(scope, handler) { + if (scope === '') return handler; + + return (req, res, next) => { + if (req.url?.startsWith(scope)) { + const original_url = req.url; + req.url = req.url.slice(scope.length); + handler(req, res, () => { + req.url = original_url; + next(); + }); + } else { + next(); + } + }; +} From 4a894cb5aecb6763343bdadfedcdab74d1f8c972 Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Sun, 17 Dec 2023 20:36:04 -0800 Subject: [PATCH 2/6] format --- packages/kit/src/exports/vite/preview/index.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/kit/src/exports/vite/preview/index.js b/packages/kit/src/exports/vite/preview/index.js index b7cac4eedc9d..0bc2ee9354f0 100644 --- a/packages/kit/src/exports/vite/preview/index.js +++ b/packages/kit/src/exports/vite/preview/index.js @@ -62,10 +62,7 @@ export async function preview(vite, vite_config, svelte_config) { } if (vite_config.preview.cors) { res.setHeader('Access-Control-Allow-Origin', '*'); - res.setHeader( - 'Access-Control-Allow-Headers', - 'Origin, Content-Type, Accept, Range' - ); + res.setHeader('Access-Control-Allow-Headers', 'Origin, Content-Type, Accept, Range'); } } }) From 51fea064e0abb138f23ec57076fb7cb3c326ad73 Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Sun, 17 Dec 2023 20:40:16 -0800 Subject: [PATCH 3/6] lint --- packages/kit/src/exports/vite/index.js | 26 +------------------------- 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index 0c944af5b407..7c0b8f5a4de9 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -1,5 +1,5 @@ import fs from 'node:fs'; -import path, { join } from 'node:path'; +import path from 'node:path'; import { svelte } from '@sveltejs/vite-plugin-svelte'; import colors from 'kleur'; @@ -19,14 +19,12 @@ import { dev } from './dev/index.js'; import { is_illegal, module_guard, normalize_id } from './graph_analysis/index.js'; import { preview } from './preview/index.js'; import { get_config_aliases, get_env, strip_virtual_prefix } from './utils.js'; -import { SVELTE_KIT_ASSETS } from '../../constants.js'; import { write_client_manifest } from '../../core/sync/write_client_manifest.js'; import prerender from '../../core/postbuild/prerender.js'; import analyse from '../../core/postbuild/analyse.js'; import { s } from '../../utils/misc.js'; import { hash } from '../../runtime/hash.js'; import { dedent, isSvelte5Plus } from '../../core/sync/utils.js'; -import sirv from 'sirv'; import { env_dynamic_private, env_dynamic_public, @@ -918,25 +916,3 @@ const create_service_worker_module = (config) => dedent` export const prerendered = []; export const version = ${s(config.kit.version.name)}; `; - -/** - * @param {string} scope - * @param {(req: import('http').IncomingMessage, res: import('http').ServerResponse, next: () => void) => void} handler - * @returns {(req: import('http').IncomingMessage, res: import('http').ServerResponse, next: () => void) => void} - */ -function scoped(scope, handler) { - if (scope === '') return handler; - - return (req, res, next) => { - if (req.url?.startsWith(scope)) { - const original_url = req.url; - req.url = req.url.slice(scope.length); - handler(req, res, () => { - req.url = original_url; - next(); - }); - } else { - next(); - } - }; -} From c46feb93c9d5ad4d26e56cb50c8dcf32e308b252 Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Sun, 17 Dec 2023 21:57:25 -0800 Subject: [PATCH 4/6] fix --- .../kit/src/exports/vite/preview/index.js | 173 +++++++++++------- 1 file changed, 107 insertions(+), 66 deletions(-) diff --git a/packages/kit/src/exports/vite/preview/index.js b/packages/kit/src/exports/vite/preview/index.js index 0bc2ee9354f0..24a90790c060 100644 --- a/packages/kit/src/exports/vite/preview/index.js +++ b/packages/kit/src/exports/vite/preview/index.js @@ -7,6 +7,7 @@ import { loadEnv, normalizePath } from 'vite'; import { getRequest, setResponse } from '../../../exports/node/index.js'; import { installPolyfills } from '../../../exports/node/polyfills.js'; import { SVELTE_KIT_ASSETS } from '../../../constants.js'; +import { not_found } from '../utils.js'; /** @typedef {import('http').IncomingMessage} Req */ /** @typedef {import('http').ServerResponse} Res */ @@ -21,6 +22,7 @@ export async function preview(vite, vite_config, svelte_config) { installPolyfills(); const { paths } = svelte_config.kit; + const base = paths.base; const assets = paths.assets ? SVELTE_KIT_ASSETS : paths.base; const protocol = vite_config.preview.https ? 'https' : 'http'; @@ -49,104 +51,143 @@ export async function preview(vite, vite_config, svelte_config) { }); return () => { + // Remove the base middleware. It screws with the URL. + // It also only lets through requests beginning with the base path, so that requests beginning + // with the assets URL never reach us. We could serve assets separately before the base + // middleware, but we'd need that to occur after the compression middleware, so would need to + // insert it manually into the stack, which would be at least as bad as doing this. + for (let i = vite.middlewares.stack.length - 1; i > 0; i--) { + // @ts-expect-error using internals + if (vite.middlewares.stack[i].handle.name === 'viteBaseMiddleware') { + vite.middlewares.stack.splice(i, 1); + } + } + // generated client assets and the contents of `static` - // should we use Vite's built-in asset server for this? - // we would need to set the outDir to do so vite.middlewares.use( scoped( assets, sirv(join(svelte_config.kit.outDir, 'output/client'), { setHeaders: (res, pathname) => { + // only apply to immutable directory, not e.g. version.json if (pathname.startsWith(`/${svelte_config.kit.appDir}/immutable`)) { res.setHeader('cache-control', 'public,max-age=31536000,immutable'); } - if (vite_config.preview.cors) { - res.setHeader('Access-Control-Allow-Origin', '*'); - res.setHeader('Access-Control-Allow-Headers', 'Origin, Content-Type, Accept, Range'); - } } }) ) ); + vite.middlewares.use((req, res, next) => { + const original_url = /** @type {string} */ (req.url); + const { pathname, search } = new URL(original_url, 'http://dummy'); + + // if `paths.base === '/a/b/c`, then the root route is `/a/b/c/`, + // regardless of the `trailingSlash` route option + if (base.length > 1 && pathname === base) { + let location = base + '/'; + if (search) location += search; + res.writeHead(307, { + location + }); + res.end(); + return; + } + + if (pathname.startsWith(base)) { + next(); + } else { + res.statusCode = 404; + not_found(req, res, base); + } + }); + // prerendered dependencies vite.middlewares.use( - mutable(join(svelte_config.kit.outDir, 'output/prerendered/dependencies')) + scoped(base, mutable(join(svelte_config.kit.outDir, 'output/prerendered/dependencies'))) ); // prerendered pages (we can't just use sirv because we need to // preserve the correct trailingSlash behaviour) - vite.middlewares.use((req, res, next) => { - let if_none_match_value = req.headers['if-none-match']; + vite.middlewares.use( + scoped(base, (req, res, next) => { + let if_none_match_value = req.headers['if-none-match']; - if (if_none_match_value?.startsWith('W/"')) { - if_none_match_value = if_none_match_value.substring(2); - } + if (if_none_match_value?.startsWith('W/"')) { + if_none_match_value = if_none_match_value.substring(2); + } - if (if_none_match_value === etag) { - res.statusCode = 304; - res.end(); - return; - } + if (if_none_match_value === etag) { + res.statusCode = 304; + res.end(); + return; + } - const { pathname, search } = new URL(/** @type {string} */ (req.url), 'http://dummy'); + const { pathname, search } = new URL(/** @type {string} */ (req.url), 'http://dummy'); - let filename = normalizePath( - join(svelte_config.kit.outDir, 'output/prerendered/pages' + pathname) - ); - let prerendered = is_file(filename); + let filename = normalizePath( + join(svelte_config.kit.outDir, 'output/prerendered/pages' + pathname) + ); + let prerendered = is_file(filename); - if (!prerendered) { - const has_trailing_slash = pathname.endsWith('/'); - const html_filename = `${filename}${has_trailing_slash ? 'index.html' : '.html'}`; + if (!prerendered) { + const has_trailing_slash = pathname.endsWith('/'); + const html_filename = `${filename}${has_trailing_slash ? 'index.html' : '.html'}`; - /** @type {string | undefined} */ - let redirect; + /** @type {string | undefined} */ + let redirect; - if (is_file(html_filename)) { - filename = html_filename; - prerendered = true; - } else if (has_trailing_slash) { - if (is_file(filename.slice(0, -1) + '.html')) { - redirect = pathname.slice(0, -1); + if (is_file(html_filename)) { + filename = html_filename; + prerendered = true; + } else if (has_trailing_slash) { + if (is_file(filename.slice(0, -1) + '.html')) { + redirect = pathname.slice(0, -1); + } + } else if (is_file(filename + '/index.html')) { + redirect = pathname + '/'; } - } else if (is_file(filename + '/index.html')) { - redirect = pathname + '/'; - } - if (redirect) { - if (search) redirect += search; - res.writeHead(307, { - location: redirect - }); + if (redirect) { + if (search) redirect += search; + res.writeHead(307, { + location: redirect + }); - res.end(); + res.end(); - return; + return; + } } - } - if (prerendered) { - res.writeHead(200, { - 'content-type': lookup(pathname) || 'text/html', - etag - }); + if (prerendered) { + res.writeHead(200, { + 'content-type': lookup(pathname) || 'text/html', + etag + }); - fs.createReadStream(filename).pipe(res); - } else { - next(); - } - }); + fs.createReadStream(filename).pipe(res); + } else { + next(); + } + }) + ); // SSR vite.middlewares.use(async (req, res) => { const host = req.headers['host']; - req.url = req.originalUrl; - const request = await getRequest({ - base: `${protocol}://${host}`, - request: req - }); + let request; + + try { + request = await getRequest({ + base: `${protocol}://${host}`, + request: req + }); + } catch (/** @type {any} */ err) { + res.statusCode = err.status || 400; + return res.end('Invalid request body'); + } setResponse( res, @@ -175,15 +216,10 @@ const mutable = (dir) => }) : (_req, _res, next) => next(); -/** @param {string} path */ -function is_file(path) { - return fs.existsSync(path) && !fs.statSync(path).isDirectory(); -} - /** * @param {string} scope - * @param {(req: import('http').IncomingMessage, res: import('http').ServerResponse, next: () => void) => void} handler - * @returns {(req: import('http').IncomingMessage, res: import('http').ServerResponse, next: () => void) => void} + * @param {Handler} handler + * @returns {Handler} */ function scoped(scope, handler) { if (scope === '') return handler; @@ -201,3 +237,8 @@ function scoped(scope, handler) { } }; } + +/** @param {string} path */ +function is_file(path) { + return fs.existsSync(path) && !fs.statSync(path).isDirectory(); +} From 18a6806691f8c7db9d2b96951a6b7fcd88b32dfd Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Mon, 18 Dec 2023 11:16:31 -0800 Subject: [PATCH 5/6] sync other changes --- packages/kit/src/exports/vite/preview/index.js | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/packages/kit/src/exports/vite/preview/index.js b/packages/kit/src/exports/vite/preview/index.js index 24a90790c060..7552257651cf 100644 --- a/packages/kit/src/exports/vite/preview/index.js +++ b/packages/kit/src/exports/vite/preview/index.js @@ -177,17 +177,10 @@ export async function preview(vite, vite_config, svelte_config) { vite.middlewares.use(async (req, res) => { const host = req.headers['host']; - let request; - - try { - request = await getRequest({ - base: `${protocol}://${host}`, - request: req - }); - } catch (/** @type {any} */ err) { - res.statusCode = err.status || 400; - return res.end('Invalid request body'); - } + const request = await getRequest({ + base: `${protocol}://${host}`, + request: req + }); setResponse( res, From 38592124d7a0a2d0cb6ff99d0bfa0bb255c354d4 Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Mon, 18 Dec 2023 11:18:18 -0800 Subject: [PATCH 6/6] update comment --- packages/kit/src/exports/vite/preview/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/kit/src/exports/vite/preview/index.js b/packages/kit/src/exports/vite/preview/index.js index 7552257651cf..997922bb5272 100644 --- a/packages/kit/src/exports/vite/preview/index.js +++ b/packages/kit/src/exports/vite/preview/index.js @@ -54,8 +54,8 @@ export async function preview(vite, vite_config, svelte_config) { // Remove the base middleware. It screws with the URL. // It also only lets through requests beginning with the base path, so that requests beginning // with the assets URL never reach us. We could serve assets separately before the base - // middleware, but we'd need that to occur after the compression middleware, so would need to - // insert it manually into the stack, which would be at least as bad as doing this. + // middleware, but we'd need that to occur after the compression and cors middlewares, so would + // need to insert it manually into the stack, which would be at least as bad as doing this. for (let i = vite.middlewares.stack.length - 1; i > 0; i--) { // @ts-expect-error using internals if (vite.middlewares.stack[i].handle.name === 'viteBaseMiddleware') {