Skip to content

Commit

Permalink
fix: ensure assets are served gzip in preview (#11377)
Browse files Browse the repository at this point in the history
  • Loading branch information
benmccann authored Dec 19, 2023
1 parent 102e4a5 commit f8e3d8b
Show file tree
Hide file tree
Showing 3 changed files with 132 additions and 100 deletions.
5 changes: 5 additions & 0 deletions .changeset/serious-pears-joke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@sveltejs/kit": patch
---

fix: ensure assets are served gzip in preview
51 changes: 1 addition & 50 deletions packages/kit/src/exports/vite/index.js
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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,
Expand Down Expand Up @@ -622,31 +620,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);
},

Expand Down Expand Up @@ -939,25 +912,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();
}
};
}
176 changes: 126 additions & 50 deletions packages/kit/src/exports/vite/preview/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand All @@ -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';
Expand Down Expand Up @@ -49,79 +51,131 @@ export async function preview(vite, vite_config, svelte_config) {
});

return () => {
// prerendered dependencies
// 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 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') {
vite.middlewares.stack.splice(i, 1);
}
}

// generated client assets and the contents of `static`
vite.middlewares.use(
mutable(join(svelte_config.kit.outDir, 'output/prerendered/dependencies'))
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');
}
}
})
)
);

// 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'];

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;
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;
}

const { pathname, search } = new URL(/** @type {string} */ (req.url), 'http://dummy');
if (pathname.startsWith(base)) {
next();
} else {
res.statusCode = 404;
not_found(req, res, base);
}
});

let filename = normalizePath(
join(svelte_config.kit.outDir, 'output/prerendered/pages' + pathname)
);
let prerendered = is_file(filename);
// prerendered dependencies
vite.middlewares.use(
scoped(base, mutable(join(svelte_config.kit.outDir, 'output/prerendered/dependencies')))
);

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

/** @type {string | undefined} */
let redirect;
if (if_none_match_value?.startsWith('W/"')) {
if_none_match_value = if_none_match_value.substring(2);
}

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 + '/';
if (if_none_match_value === etag) {
res.statusCode = 304;
res.end();
return;
}

if (redirect) {
if (search) redirect += search;
res.writeHead(307, {
location: redirect
});
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);

if (!prerendered) {
const has_trailing_slash = pathname.endsWith('/');
const html_filename = `${filename}${has_trailing_slash ? 'index.html' : '.html'}`;

/** @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);
}
} else if (is_file(filename + '/index.html')) {
redirect = pathname + '/';
}

res.end();
if (redirect) {
if (search) redirect += search;
res.writeHead(307, {
location: redirect
});

return;
res.end();

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}`,
Expand Down Expand Up @@ -155,6 +209,28 @@ const mutable = (dir) =>
})
: (_req, _res, next) => next();

/**
* @param {string} scope
* @param {Handler} handler
* @returns {Handler}
*/
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();
}
};
}

/** @param {string} path */
function is_file(path) {
return fs.existsSync(path) && !fs.statSync(path).isDirectory();
Expand Down

0 comments on commit f8e3d8b

Please sign in to comment.