@sveltejs/kit
Version:
SvelteKit is the fastest way to build Svelte apps
197 lines (170 loc) • 6.47 kB
JavaScript
/** @import { ParamValue } from '@sveltejs/kit/params' */
/** @import { SSRManifest } from 'types' */
import { base, assets } from '#app/paths';
import { relative } from '$app/paths/internal/server';
import { text } from '@sveltejs/kit';
import * as devalue from 'devalue';
import { s } from '../../../utils/misc.js';
import { find_route } from '../../../utils/routing.js';
import { SVELTE_KIT_ASSETS } from '../../../constants.js';
import { manifest } from '../internal.js';
/**
* @param {import('types').SSRClientRoute} route
* @param {URL} url
* @param {NonNullable<SSRManifest['client']>} client
* @returns {string}
*/
export function generate_route_object(route, url, client) {
const { errors, layouts, leaf } = route;
const paths = resolve_paths(url.pathname);
const nodes = [...errors, ...layouts.map((l) => l?.[1]), leaf[1]]
.filter((n) => typeof n === 'number')
.map((n) => `'${n}': () => ${create_client_import(client.nodes?.[n], paths)}`)
.join(',\n\t\t');
// stringified version of
/* @type {import('types').CSRRouteServer} */
return [
`{\n\tid: ${s(route.id)}`,
`errors: ${s(route.errors)}`,
`layouts: ${s(route.layouts)}`,
`leaf: ${s(route.leaf)}`,
`nodes: {\n\t\t${nodes}\n\t}\n}`
].join(',\n\t');
}
/**
* The `base` and `assets` prefixes for client paths in a document at `pathname`. With
* `paths.relative`, they're relative to that document so the app also works when served
* from IPFS, the internet archive, or behind a proxy.
* @param {string} pathname
*/
export function resolve_paths(pathname) {
if (!relative) return { base, assets };
const segments = pathname.slice(base.length).split('/').slice(2);
const relative_base = segments.map(() => '..').join('/') || '.';
return {
base: relative_base,
// same-origin assets are relative too, except for the placeholder used by `vite preview`
assets: !assets || (assets[0] === '/' && assets !== SVELTE_KIT_ASSETS) ? relative_base : assets
};
}
/**
* @param {string} path a root-absolute dev path (e.g. `/@fs/...`) or a prod path relative to `assets`
* @param {ReturnType<typeof resolve_paths>} paths
*/
export function client_path(path, { base, assets }) {
return path[0] === '/' ? base + path : `${assets}/${path}`;
}
/**
* @param {string | undefined} import_path
* @param {ReturnType<typeof resolve_paths>} paths
*/
function create_client_import(import_path, paths) {
return import_path ? `import('${client_path(import_path, paths)}')` : 'Promise.resolve({})';
}
/**
* @param {string} resolved_path
* @param {URL} url
* @returns {Promise<Response>}
*/
export async function resolve_route(resolved_path, url) {
if (!manifest.client?.routes) {
return text('Server-side route resolution disabled', { status: 400 });
}
try {
const matchers = await manifest.matchers();
const result = find_route(resolved_path, manifest.client.routes, matchers);
return create_server_routing_response(
result?.route ?? null,
result?.params ?? {},
url,
manifest.client
).response;
} catch {
return text('Error resolving route', { status: 500 });
}
}
/**
* Resolve a route-ID resolution request (`/_app/routes/<id>/__route.js`) to a
* JS module containing the route's node loaders. Params are always `{}` since
* this endpoint exists to support `preloadCode(routeId)`, which doesn't need them.
*
* The module has one of three shapes, which the client uses to tell three cases apart:
*
* - `export const route = {...}` — a page route, with loaders to import
* - `export const endpoint_only = true` — a real route with no `+page`, so there is
* nothing to preload, but the client can cache that fact and stop asking
* - an empty module — no such route
*
* @param {string} route_id
* @param {URL} url
* @returns {Response}
*/
export function resolve_route_by_id(route_id, url) {
if (!manifest.client?.routes) {
return text('Server-side route resolution disabled', { status: 400 });
}
try {
const route = manifest.client.routes.find((r) => r.id === route_id);
if (route) {
return create_server_routing_response(route, null, url, manifest.client).response;
}
// `client.routes` only contains routes with a `+page`, so a miss above doesn't mean the
// route doesn't exist — it might be a `+server.js`-only route. `_.routes` includes those
// (with `page: null`), so we can distinguish "exists but has no code" from "unknown".
if (manifest.routes.some((r) => r.id === route_id && !r.page)) {
return text('export const endpoint_only = true;', { headers: js_headers() });
}
return create_server_routing_response(null, null, url, manifest.client).response;
} catch {
return text('Error resolving route', { status: 500 });
}
}
function js_headers() {
return new Headers({
'content-type': 'application/javascript; charset=utf-8'
});
}
/**
* @param {import('types').SSRClientRoute | null} route
* @param {Partial<Record<string, ParamValue>> | null} params
* @param {URL} url
* @param {NonNullable<SSRManifest['client']>} client
* @returns {{response: Response, body: string}}
*/
export function create_server_routing_response(route, params, url, client) {
const headers = js_headers();
let body = '';
if (route) {
const csr_route = generate_route_object(route, url, client);
body = `${create_css_import(route, url, client)}export const route = ${csr_route};`;
if (params !== null) {
body += `\nexport const params = ${devalue.uneval(params)}`;
}
}
return { response: text(body, { headers }), body };
}
/**
* This function generates the client-side import for the CSS files that are
* associated with the current route. Vite takes care of that when using
* client-side route resolution, but for server-side resolution it does
* not know about the CSS files automatically.
*
* @param {import('types').SSRClientRoute} route
* @param {URL} url
* @param {NonNullable<SSRManifest['client']>} client
* @returns {string}
*/
function create_css_import(route, url, client) {
const { errors, layouts, leaf } = route;
const paths = resolve_paths(url.pathname);
let css = '';
for (const node of [...errors, ...layouts.map((l) => l?.[1]), leaf[1]]) {
if (typeof node !== 'number') continue;
const node_css = client.css?.[node];
for (const css_path of node_css ?? []) {
css += `'${client_path(css_path, paths)}',`;
}
}
if (!css) return '';
return `${create_client_import(client.start, paths)}.then(x => x.load_css([${css}]));\n`;
}