@sveltejs/kit
Version:
SvelteKit is the fastest way to build Svelte apps
659 lines (562 loc) • 20 kB
JavaScript
import fs from 'node:fs';
import path from 'node:path';
import process from 'node:process';
import { URL } from 'node:url';
import { AsyncLocalStorage } from 'node:async_hooks';
import colors from 'kleur';
import sirv from 'sirv';
import { isCSSRequest, loadEnv, buildErrorMessage } from 'vite';
import { createReadableStream, getRequest, setResponse } from '../../../exports/node/index.js';
import { installPolyfills } from '../../../exports/node/polyfills.js';
import { coalesce_to_error } from '../../../utils/error.js';
import { from_fs, posixify, resolve_entry, to_fs } from '../../../utils/filesystem.js';
import { load_error_page } from '../../../core/config/index.js';
import { SVELTE_KIT_ASSETS } from '../../../constants.js';
import * as sync from '../../../core/sync/sync.js';
import { get_mime_lookup, runtime_base } from '../../../core/utils.js';
import { compact } from '../../../utils/array.js';
import { not_found } from '../utils.js';
import { SCHEME } from '../../../utils/url.js';
import { check_feature } from '../../../utils/features.js';
import { escape_html } from '../../../utils/escape.js';
const cwd = process.cwd();
// vite-specifc queries that we should skip handling for css urls
const vite_css_query_regex = /(?:\?|&)(?:raw|url|inline)(?:&|$)/;
/**
* @param {import('vite').ViteDevServer} vite
* @param {import('vite').ResolvedConfig} vite_config
* @param {import('types').ValidatedConfig} svelte_config
* @return {Promise<Promise<() => void>>}
*/
export async function dev(vite, vite_config, svelte_config) {
installPolyfills();
const async_local_storage = new AsyncLocalStorage();
globalThis.__SVELTEKIT_TRACK__ = (label) => {
const context = async_local_storage.getStore();
if (!context || context.prerender === true) return;
check_feature(context.event.route.id, context.config, label, svelte_config.kit.adapter);
};
const fetch = globalThis.fetch;
globalThis.fetch = (info, init) => {
if (typeof info === 'string' && !SCHEME.test(info)) {
throw new Error(
`Cannot use relative URL (${info}) with global fetch — use \`event.fetch\` instead: https://svelte.dev/docs/kit/web-standards#fetch-apis`
);
}
return fetch(info, init);
};
sync.init(svelte_config, vite_config.mode);
/** @type {import('types').ManifestData} */
let manifest_data;
/** @type {import('@sveltejs/kit').SSRManifest} */
let manifest;
/** @type {Error | null} */
let manifest_error = null;
/** @param {string} url */
async function loud_ssr_load_module(url) {
try {
return await vite.ssrLoadModule(url, { fixStacktrace: true });
} catch (/** @type {any} */ err) {
const msg = buildErrorMessage(err, [colors.red(`Internal server error: ${err.message}`)]);
if (!vite.config.logger.hasErrorLogged(err)) {
vite.config.logger.error(msg, { error: err });
}
vite.ws.send({
type: 'error',
err: {
...err,
// these properties are non-enumerable and will
// not be serialized unless we explicitly include them
message: err.message,
stack: err.stack
}
});
throw err;
}
}
/** @param {string} id */
async function resolve(id) {
const url = id.startsWith('..') ? to_fs(path.posix.resolve(id)) : `/${id}`;
const module = await loud_ssr_load_module(url);
const module_node = await vite.moduleGraph.getModuleByUrl(url);
if (!module_node) throw new Error(`Could not find node for ${url}`);
return { module, module_node, url };
}
function update_manifest() {
try {
({ manifest_data } = sync.create(svelte_config));
if (manifest_error) {
manifest_error = null;
vite.ws.send({ type: 'full-reload' });
}
} catch (error) {
manifest_error = /** @type {Error} */ (error);
console.error(colors.bold().red(manifest_error.message));
vite.ws.send({
type: 'error',
err: {
message: manifest_error.message ?? 'Invalid routes',
stack: ''
}
});
return;
}
manifest = {
appDir: svelte_config.kit.appDir,
appPath: svelte_config.kit.appDir,
assets: new Set(manifest_data.assets.map((asset) => asset.file)),
mimeTypes: get_mime_lookup(manifest_data),
_: {
client: {
start: `${runtime_base}/client/entry.js`,
app: `${to_fs(svelte_config.kit.outDir)}/generated/client/app.js`,
imports: [],
stylesheets: [],
fonts: [],
uses_env_dynamic_public: true,
nodes:
svelte_config.kit.router.resolution === 'client'
? undefined
: manifest_data.nodes.map((node, i) => {
if (node.component || node.universal) {
return `${svelte_config.kit.paths.base}${to_fs(svelte_config.kit.outDir)}/generated/client/nodes/${i}.js`;
}
}),
// `css` is not necessary in dev, as the JS file from `nodes` will reference the CSS file
routes:
svelte_config.kit.router.resolution === 'client'
? undefined
: compact(
manifest_data.routes.map((route) => {
if (!route.page) return;
return {
id: route.id,
pattern: route.pattern,
params: route.params,
layouts: route.page.layouts.map((l) =>
l !== undefined ? [!!manifest_data.nodes[l].server, l] : undefined
),
errors: route.page.errors,
leaf: [!!manifest_data.nodes[route.page.leaf].server, route.page.leaf]
};
})
)
},
server_assets: new Proxy(
{},
{
has: (_, /** @type {string} */ file) => fs.existsSync(from_fs(file)),
get: (_, /** @type {string} */ file) => fs.statSync(from_fs(file)).size
}
),
nodes: manifest_data.nodes.map((node, index) => {
return async () => {
/** @type {import('types').SSRNode} */
const result = {};
result.index = index;
result.universal_id = node.universal;
result.server_id = node.server;
// these are unused in dev, but it's easier to include them
result.imports = [];
result.stylesheets = [];
result.fonts = [];
/** @type {import('vite').ModuleNode[]} */
const module_nodes = [];
if (node.component) {
result.component = async () => {
const { module_node, module } = await resolve(
/** @type {string} */ (node.component)
);
module_nodes.push(module_node);
return module.default;
};
}
if (node.universal) {
const { module, module_node } = await resolve(node.universal);
module_nodes.push(module_node);
result.universal = module;
}
if (node.server) {
const { module } = await resolve(node.server);
result.server = module;
}
// in dev we inline all styles to avoid FOUC. this gets populated lazily so that
// components/stylesheets loaded via import() during `load` are included
result.inline_styles = async () => {
/** @type {Set<import('vite').ModuleNode>} */
const deps = new Set();
for (const module_node of module_nodes) {
await find_deps(vite, module_node, deps);
}
/** @type {Record<string, string>} */
const styles = {};
for (const dep of deps) {
if (isCSSRequest(dep.url) && !vite_css_query_regex.test(dep.url)) {
const inlineCssUrl = dep.url.includes('?')
? dep.url.replace('?', '?inline&')
: dep.url + '?inline';
try {
const mod = await vite.ssrLoadModule(inlineCssUrl);
styles[dep.url] = mod.default;
} catch {
// this can happen with dynamically imported modules, I think
// because the Vite module graph doesn't distinguish between
// static and dynamic imports? TODO investigate, submit fix
}
}
}
return styles;
};
return result;
};
}),
prerendered_routes: new Set(),
routes: compact(
manifest_data.routes.map((route) => {
if (!route.page && !route.endpoint) return null;
const endpoint = route.endpoint;
return {
id: route.id,
pattern: route.pattern,
params: route.params,
page: route.page,
endpoint: endpoint
? async () => {
const url = path.resolve(cwd, endpoint.file);
return await loud_ssr_load_module(url);
}
: null,
endpoint_id: endpoint?.file
};
})
),
matchers: async () => {
/** @type {Record<string, import('@sveltejs/kit').ParamMatcher>} */
const matchers = {};
for (const key in manifest_data.matchers) {
const file = manifest_data.matchers[key];
const url = path.resolve(cwd, file);
const module = await vite.ssrLoadModule(url, { fixStacktrace: true });
if (module.match) {
matchers[key] = module.match;
} else {
throw new Error(`${file} does not export a \`match\` function`);
}
}
return matchers;
}
}
};
}
/** @param {Error} error */
function fix_stack_trace(error) {
try {
vite.ssrFixStacktrace(error);
} catch {
// ssrFixStacktrace can fail on StackBlitz web containers and we don't know why
// by ignoring it the line numbers are wrong, but at least we can show the error
}
return error.stack;
}
update_manifest();
/**
* @param {string} event
* @param {(file: string) => void} cb
*/
const watch = (event, cb) => {
vite.watcher.on(event, (file) => {
if (
file.startsWith(svelte_config.kit.files.routes + path.sep) ||
file.startsWith(svelte_config.kit.files.params + path.sep) ||
// in contrast to server hooks, client hooks are written to the client manifest
// and therefore need rebuilding when they are added/removed
file.startsWith(svelte_config.kit.files.hooks.client)
) {
cb(file);
}
});
};
/** @type {NodeJS.Timeout | null } */
let timeout = null;
/** @param {() => void} to_run */
const debounce = (to_run) => {
timeout && clearTimeout(timeout);
timeout = setTimeout(() => {
timeout = null;
to_run();
}, 100);
};
// flag to skip watchers if server is already restarting
let restarting = false;
// Debounce add/unlink events because in case of folder deletion or moves
// they fire in rapid succession, causing needless invocations.
watch('add', () => debounce(update_manifest));
watch('unlink', () => debounce(update_manifest));
watch('change', (file) => {
// Don't run for a single file if the whole manifest is about to get updated
if (timeout || restarting) return;
sync.update(svelte_config, manifest_data, file);
});
const { appTemplate, errorTemplate, serviceWorker, hooks } = svelte_config.kit.files;
// vite client only executes a full reload if the triggering html file path is index.html
// kit defaults to src/app.html, so unless user changed that to index.html
// send the vite client a full-reload event without path being set
if (appTemplate !== 'index.html') {
vite.watcher.on('change', (file) => {
if (file === appTemplate && !restarting) {
vite.ws.send({ type: 'full-reload' });
}
});
}
vite.watcher.on('all', (_, file) => {
if (
file === appTemplate ||
file === errorTemplate ||
file.startsWith(serviceWorker) ||
file.startsWith(hooks.server)
) {
sync.server(svelte_config);
}
});
// changing the svelte config requires restarting the dev server
// the config is only read on start and passed on to vite-plugin-svelte
// which needs up-to-date values to operate correctly
vite.watcher.on('change', async (file) => {
if (path.basename(file) === 'svelte.config.js') {
console.log(`svelte config changed, restarting vite dev-server. changed file: ${file}`);
restarting = true;
await vite.restart();
}
});
const assets = svelte_config.kit.paths.assets ? SVELTE_KIT_ASSETS : svelte_config.kit.paths.base;
const asset_server = sirv(svelte_config.kit.files.assets, {
dev: true,
etag: true,
maxAge: 0,
extensions: [],
setHeaders: (res) => {
res.setHeader('access-control-allow-origin', '*');
}
});
async function align_exports() {
// This shameful hack allows us to load runtime server code via Vite
// while apps load `HttpError` and `Redirect` in Node, without
// causing `instanceof` checks to fail
const control_module_node = await import('../../../runtime/control.js');
const control_module_vite = await vite.ssrLoadModule(`${runtime_base}/control.js`);
control_module_node.replace_implementations({
ActionFailure: control_module_vite.ActionFailure,
HttpError: control_module_vite.HttpError,
Redirect: control_module_vite.Redirect,
SvelteKitError: control_module_vite.SvelteKitError
});
}
await align_exports();
const ws_send = vite.ws.send;
/** @param {any} args */
vite.ws.send = function (...args) {
// We need to reapply the patch after Vite did dependency optimizations
// because that clears the module resolutions
if (args[0]?.type === 'full-reload' && args[0].path === '*') {
void align_exports();
}
return ws_send.apply(vite.ws, args);
};
vite.middlewares.use((req, res, next) => {
const base = `${vite.config.server.https ? 'https' : 'http'}://${
req.headers[':authority'] || req.headers.host
}`;
const decoded = decodeURI(new URL(base + req.url).pathname);
if (decoded.startsWith(assets)) {
const pathname = decoded.slice(assets.length);
const file = svelte_config.kit.files.assets + pathname;
if (fs.existsSync(file) && !fs.statSync(file).isDirectory()) {
if (has_correct_case(file, svelte_config.kit.files.assets)) {
req.url = encodeURI(pathname); // don't need query/hash
asset_server(req, res);
return;
}
}
}
next();
});
const env = loadEnv(vite_config.mode, svelte_config.kit.env.dir, '');
const emulator = await svelte_config.kit.adapter?.emulate?.();
return () => {
const serve_static_middleware = vite.middlewares.stack.find(
(middleware) =>
/** @type {function} */ (middleware.handle).name === 'viteServeStaticMiddleware'
);
// Vite will give a 403 on URLs like /test, /static, and /package.json preventing us from
// serving routes with those names. See https://github.com/vitejs/vite/issues/7363
remove_static_middlewares(vite.middlewares);
vite.middlewares.use(async (req, res) => {
// Vite's base middleware strips out the base path. Restore it
const original_url = req.url;
req.url = req.originalUrl;
try {
const base = `${vite.config.server.https ? 'https' : 'http'}://${
req.headers[':authority'] || req.headers.host
}`;
const decoded = decodeURI(new URL(base + req.url).pathname);
const file = posixify(path.resolve(decoded.slice(svelte_config.kit.paths.base.length + 1)));
const is_file = fs.existsSync(file) && !fs.statSync(file).isDirectory();
const allowed =
!vite_config.server.fs.strict ||
vite_config.server.fs.allow.some((dir) => file.startsWith(dir));
if (is_file && allowed) {
req.url = original_url;
// @ts-expect-error
serve_static_middleware.handle(req, res);
return;
}
if (!decoded.startsWith(svelte_config.kit.paths.base)) {
return not_found(req, res, svelte_config.kit.paths.base);
}
if (decoded === svelte_config.kit.paths.base + '/service-worker.js') {
const resolved = resolve_entry(svelte_config.kit.files.serviceWorker);
if (resolved) {
res.writeHead(200, {
'content-type': 'application/javascript'
});
res.end(`import '${svelte_config.kit.paths.base}${to_fs(resolved)}';`);
} else {
res.writeHead(404);
res.end('not found');
}
return;
}
// we have to import `Server` before calling `set_assets`
const { Server } = /** @type {import('types').ServerModule} */ (
await vite.ssrLoadModule(`${runtime_base}/server/index.js`, { fixStacktrace: true })
);
const { set_fix_stack_trace } = await vite.ssrLoadModule(
`${runtime_base}/shared-server.js`
);
set_fix_stack_trace(fix_stack_trace);
const { set_assets } = await vite.ssrLoadModule('__sveltekit/paths');
set_assets(assets);
const server = new Server(manifest);
await server.init({
env,
read: (file) => createReadableStream(from_fs(file))
});
const request = await getRequest({
base,
request: req
});
if (manifest_error) {
console.error(colors.bold().red(manifest_error.message));
const error_page = load_error_page(svelte_config);
/** @param {{ status: number; message: string }} opts */
const error_template = ({ status, message }) => {
return error_page
.replace(/%sveltekit\.status%/g, String(status))
.replace(/%sveltekit\.error\.message%/g, escape_html(message));
};
res.writeHead(500, {
'Content-Type': 'text/html; charset=utf-8'
});
res.end(
error_template({ status: 500, message: manifest_error.message ?? 'Invalid routes' })
);
return;
}
const rendered = 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(from_fs(file));
}
return fs.readFileSync(path.join(svelte_config.kit.files.assets, file));
},
before_handle: (event, config, prerender) => {
async_local_storage.enterWith({ event, config, prerender });
},
emulator
});
if (rendered.status === 404) {
// @ts-expect-error
serve_static_middleware.handle(req, res, () => {
void setResponse(res, rendered);
});
} else {
void setResponse(res, rendered);
}
} catch (e) {
const error = coalesce_to_error(e);
res.statusCode = 500;
res.end(fix_stack_trace(error));
}
});
};
}
/**
* @param {import('connect').Server} server
*/
function remove_static_middlewares(server) {
const static_middlewares = ['viteServeStaticMiddleware', 'viteServePublicMiddleware'];
for (let i = server.stack.length - 1; i > 0; i--) {
// @ts-expect-error using internals
if (static_middlewares.includes(server.stack[i].handle.name)) {
server.stack.splice(i, 1);
}
}
}
/**
* @param {import('vite').ViteDevServer} vite
* @param {import('vite').ModuleNode} node
* @param {Set<import('vite').ModuleNode>} deps
*/
async function find_deps(vite, node, deps) {
// since `ssrTransformResult.deps` contains URLs instead of `ModuleNode`s, this process is asynchronous.
// instead of using `await`, we resolve all branches in parallel.
/** @type {Promise<void>[]} */
const branches = [];
/** @param {import('vite').ModuleNode} node */
async function add(node) {
if (!deps.has(node)) {
deps.add(node);
await find_deps(vite, node, deps);
}
}
/** @param {string} url */
async function add_by_url(url) {
const node = await vite.moduleGraph.getModuleByUrl(url);
if (node) {
await add(node);
}
}
if (node.ssrTransformResult) {
if (node.ssrTransformResult.deps) {
node.ssrTransformResult.deps.forEach((url) => branches.push(add_by_url(url)));
}
if (node.ssrTransformResult.dynamicDeps) {
node.ssrTransformResult.dynamicDeps.forEach((url) => branches.push(add_by_url(url)));
}
} else {
node.importedModules.forEach((node) => branches.push(add(node)));
}
await Promise.all(branches);
}
/**
* Determine if a file is being requested with the correct case,
* to ensure consistent behaviour between dev and prod and across
* operating systems. Note that we can't use realpath here,
* because we don't want to follow symlinks
* @param {string} file
* @param {string} assets
* @returns {boolean}
*/
function has_correct_case(file, assets) {
if (file === assets) return true;
const parent = path.dirname(file);
if (fs.readdirSync(parent).includes(path.basename(file))) {
return has_correct_case(parent, assets);
}
return false;
}