UNPKG

@sveltejs/kit

Version:

SvelteKit is the fastest way to build Svelte apps

284 lines (241 loc) • 8.1 kB
/** @import { NextHandleFunction } from 'connect' */ /** @import { PreviewServer } from 'vite' */ /** @import { ValidatedConfig, ServerModule } from 'types' */ import fs from 'node:fs'; import { join } from 'node:path'; import { pathToFileURL } from 'node:url'; import { lookup } from '../../../utils/mime.js'; import sirv from 'sirv'; import { loadEnv, normalizePath } from 'vite'; import { createReadableStream, getRequest, setResponse } from '../../../exports/node/index.js'; import { SVELTE_KIT_ASSETS } from '../../../constants.js'; import { relative_pathname } from '../../../utils/url.js'; import { is_chrome_devtools_request, not_found } from '../utils.js'; import { set_error_stack } from '../../../utils/error.js'; import * as e from '../../../messages/build-errors.js'; /** * @param {PreviewServer} vite * @param {ValidatedConfig} svelte_config */ export async function preview(vite, svelte_config) { const { paths } = svelte_config; const base = paths.base; const assets = paths.assets ? SVELTE_KIT_ASSETS : paths.base; const protocol = vite.config.preview.https ? 'https' : 'http'; const etag = `"${Date.now()}"`; const dir = join(svelte_config.outDir, 'output/server'); if (!fs.existsSync(`${dir}/manifest.js`)) { e.preview_build_missing({ dir }, { stackless: true }); } const instrumentation = join(dir, 'instrumentation.server.js'); if (fs.existsSync(instrumentation)) { await import(pathToFileURL(instrumentation).href); } /** @type {ServerModule} */ const { configure } = await import(pathToFileURL(join(dir, 'index.js')).href); /** @type {{ manifest: import('types').SSRManifest }} */ const { manifest } = await import(pathToFileURL(join(dir, 'manifest.js')).href); /** @type {import('types').ServerInstance} */ let server; try { server = await configure({ manifest, env: loadEnv(vite.config.mode, svelte_config.env.dir, ''), read: (file) => createReadableStream(`${dir}/${file}`), assets }); await server.init(); } catch (error) { // Vite erases the error message when starting the preview server so we store // it in the stack instead. This ensures errors thrown using `stackless` // are still readable if (error instanceof Error) set_error_stack(error, error.message); throw error; } const emulator = await svelte_config.adapter?.emulate?.(); 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 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( scoped( assets, sirv(join(svelte_config.outDir, 'output/client'), { setHeaders: (res, pathname) => { // only apply to immutable directory, not e.g. version.json if (pathname.startsWith(`/${svelte_config.appDir}/immutable`)) { res.setHeader('cache-control', 'public,max-age=31536000,immutable'); } } }) ) ); 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 (is_chrome_devtools_request(pathname, res)) { return; } if (pathname.startsWith(base)) { next(); } else { res.statusCode = 404; not_found(req, res, base); } }); // prerendered dependencies vite.middlewares.use( scoped(base, mutable(join(svelte_config.outDir, 'output/prerendered/dependencies'))) ); // 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']; 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; } const { pathname, search } = new URL(/** @type {string} */ (req.url), 'http://dummy'); const dir = pathname.startsWith(`/${svelte_config.appDir}/remote/`) ? 'data' : 'pages'; const root = join(svelte_config.outDir, `output/prerendered/${dir}`); let decoded = pathname; try { decoded = decodeURI(pathname); } catch { // malformed URI } let filename = normalizePath(join(root, decoded)); let prerendered = is_file(filename, root); 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, root)) { filename = html_filename; prerendered = true; } else if (has_trailing_slash) { if (is_file(filename.slice(0, -1) + '.html', root)) { redirect = pathname.slice(0, -1); } } else if (is_file(filename + '/index.html', root)) { redirect = pathname + '/'; } if (redirect) { res.writeHead(308, { // relative so (possibly invisible) path prefixes are preserved location: relative_pathname(pathname, redirect) + search }); res.end(); return; } } if (prerendered) { res.writeHead(200, { 'content-type': lookup(pathname) || 'text/html', etag }); fs.createReadStream(filename).pipe(res); } else { next(); } }) ); // SSR vite.middlewares.use(async (req, res) => { const host = req.headers[':authority'] || req.headers.host; const request = (svelte_config.adapter?.vite?.getRequest ?? getRequest)({ base: `${protocol}://${host}`, request: req, response: res }); (svelte_config.adapter?.vite?.setResponse ?? setResponse)( res, await server.respond(request, { getClientAddress: () => { const { remoteAddress } = req.socket; if (remoteAddress) return remoteAddress; throw new Error('Could not determine clientAddress'); }, read: (file) => { if (file in manifest.server_assets) { return fs.readFileSync(join(dir, file)); } return fs.readFileSync(join(svelte_config.files.assets, file)); }, emulator }) ); }); }; } /** * @param {string} dir * @returns {NextHandleFunction} */ const mutable = (dir) => fs.existsSync(dir) ? sirv(dir, { etag: true, maxAge: 0 }) : (_req, _res, next) => next(); /** * @param {string} scope * @param {NextHandleFunction} handler * @returns {NextHandleFunction} */ 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 * @param {string} root * @returns {boolean} */ function is_file(path, root) { return ( fs.existsSync(root) && fs.existsSync(path) && !fs.statSync(path).isDirectory() && // Decoding can introduce path separators on Windows. Check containment after resolving them. normalizePath(fs.realpathSync(path)).startsWith(`${normalizePath(fs.realpathSync(root))}/`) ); }