Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: ensure assets are served gzip in preview #11377

Merged
merged 6 commits into from
Dec 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -626,31 +624,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'
);
}
benmccann marked this conversation as resolved.
Show resolved Hide resolved
}
})
)
);

return preview(vite, vite_config, svelte_config);
},

Expand Down Expand Up @@ -943,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();
}
};
}
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
Loading