@sveltejs/kit
Version:
SvelteKit is the fastest way to build Svelte apps
163 lines (144 loc) • 4.93 kB
JavaScript
/** @import { BuildData, RemoteChunk, RouteData } from 'types' */
import fs from 'node:fs';
import path from 'node:path';
import { lookup as mime_lookup } from '../../utils/mime.js';
import { s } from '../../utils/misc.js';
import { get_mime_lookup } from '../utils.js';
import { resolve_symlinks } from '../../exports/vite/build/utils.js';
import { compact } from '../../utils/array.js';
import { join_relative } from '../../utils/filesystem.js';
import { dedent } from '../sync/utils.js';
import { find_server_assets } from './find_server_assets.js';
import { uneval } from 'devalue';
/**
* Generates the data used to write the server-side manifest.js file. This data is used in the Vite
* build process, to power routing, etc.
* @param {{
* build_data: BuildData;
* prerendered: string[];
* relative_path: string;
* routes: RouteData[];
* remotes: RemoteChunk[];
* root: string;
* }} opts
*/
export function generate_manifest({
build_data,
prerendered,
relative_path,
routes,
remotes,
root
}) {
/**
* @type {Map<any, number>} The new index of each node in the filtered nodes array
*/
const reindexed = new Map();
/**
* All nodes actually used in the routes definition (prerendered routes are omitted).
* Root layout/error is always included as they are needed for 404 and root errors.
* @type {Set<any>}
*/
const used_nodes = new Set([0, 1]);
const server_assets = find_server_assets(build_data, routes, root);
for (const route of routes) {
if (route.page) {
for (const i of route.page.layouts) used_nodes.add(i);
for (const i of route.page.errors) used_nodes.add(i);
used_nodes.add(route.page.leaf);
}
}
const node_paths = compact(
build_data.manifest_data.nodes.map((_, i) => {
if (used_nodes.has(i)) {
reindexed.set(i, reindexed.size);
return join_relative(relative_path, `/nodes/${i}.js`);
}
})
);
/** @type {(path: string) => string} */
const loader = (path) => `__memo(() => import('${path}'))`;
const assets = build_data.manifest_data.assets.map((asset) => asset.file);
if (build_data.service_worker) {
assets.push(build_data.service_worker);
}
const uses_matchers = build_data.manifest_data.routes.some((route) =>
route.params.some((param) => param.matcher)
);
/** @param {Array<number | undefined>} indexes */
function get_nodes(indexes) {
const string = indexes.map((n) => reindexed.get(n) ?? '').join(',');
// since JavaScript ignores trailing commas, we need to insert a dummy
// comma so that the array has the correct length if the last item
// is undefined
return `[${string},]`;
}
const mime_types = get_mime_lookup(build_data.manifest_data);
/** @type {Record<string, number>} */
const files = {};
for (const file of server_assets) {
files[file] = fs.statSync(path.resolve(build_data.out_dir, 'server', file)).size;
const ext = path.extname(file);
mime_types[ext] ??= mime_lookup(ext) || '';
}
// record extensions that only exist in prerendered output, e.g. a prerendered favicon.ico
for (const pathname of prerendered) {
const ext = path.extname(pathname);
if (ext) mime_types[ext] ??= mime_lookup(ext) || '';
}
// prettier-ignore
// String representation of
/** @template {import('types').SSRManifest} T */
const manifest_expr = dedent`
{
app_dir: ${s(build_data.app_dir)},
app_path: ${s(build_data.app_path)},
assets: new Set(${s(assets)}),
mime_types: ${s(mime_types)},
client: ${uneval(build_data.client)},
nodes: [
${(node_paths).map(loader).join(',\n')}
],
remotes: {
${remotes.map((remote) => `'${remote.hash}': ${loader(join_relative(relative_path, `chunks/remote-${remote.hash}.js`))}`).join(',\n')}
},
routes: [
${routes.map(route => {
if (!route.page && !route.endpoint) return;
return dedent`
{
id: ${s(route.id)},
pattern: ${route.pattern},
params: ${s(route.params)},
page: ${route.page ? `{ layouts: ${get_nodes(route.page.layouts)}, errors: ${get_nodes(route.page.errors)}, leaf: ${reindexed.get(route.page.leaf)} }` : 'null'},
endpoint: ${route.endpoint ? loader(join_relative(relative_path, resolve_symlinks(build_data.server_manifest, route.endpoint.file, root).chunk.file)) : 'null'}
}
`;
}).filter(Boolean).join(',\n')}
],
prerendered_routes: new Set(${s(prerendered)}),
matchers: async () => {
${
uses_matchers && build_data.manifest_data.params
? dedent`
const { params } = await import('${join_relative(relative_path, '/entries/params.js')}');
return params;
`
: 'return {};'
}
},
server_assets: ${s(files)}
}
`;
// Memoize the loaders to prevent Node from doing unnecessary work
// on every dynamic import call
return dedent`
(() => {
function __memo(fn) {
let value;
return () => value ??= (value = fn());
}
return ${manifest_expr}
})()
`;
}