UNPKG

@sveltejs/kit

Version:

SvelteKit is the fastest way to build Svelte apps

250 lines (210 loc) 7 kB
import { join } from 'node:path'; import { pathToFileURL } from 'node:url'; import { validate_server_exports } from '../../utils/exports.js'; import { load_config } from '../config/index.js'; import { forked } from '../../utils/fork.js'; import { installPolyfills } from '../../exports/node/polyfills.js'; import { ENDPOINT_METHODS } from '../../constants.js'; import { filter_private_env, filter_public_env } from '../../utils/env.js'; import { has_server_load, resolve_route } from '../../utils/routing.js'; import { check_feature } from '../../utils/features.js'; import { createReadableStream } from '@sveltejs/kit/node'; import { PageNodes } from '../../utils/page_nodes.js'; export default forked(import.meta.url, analyse); /** * @param {{ * hash: boolean; * manifest_path: string; * manifest_data: import('types').ManifestData; * server_manifest: import('vite').Manifest; * tracked_features: Record<string, string[]>; * env: Record<string, string> * }} opts */ async function analyse({ hash, manifest_path, manifest_data, server_manifest, tracked_features, env }) { /** @type {import('@sveltejs/kit').SSRManifest} */ const manifest = (await import(pathToFileURL(manifest_path).href)).manifest; /** @type {import('types').ValidatedKitConfig} */ const config = (await load_config()).kit; const server_root = join(config.outDir, 'output'); /** @type {import('types').ServerInternalModule} */ const internal = await import(pathToFileURL(`${server_root}/server/internal.js`).href); installPolyfills(); // configure `import { building } from '$app/environment'` — // essential we do this before analysing the code internal.set_building(); // set env, in case it's used in initialisation const { publicPrefix: public_prefix, privatePrefix: private_prefix } = config.env; const private_env = filter_private_env(env, { public_prefix, private_prefix }); const public_env = filter_public_env(env, { public_prefix, private_prefix }); internal.set_private_env(private_env); internal.set_public_env(public_env); internal.set_safe_public_env(public_env); internal.set_manifest(manifest); internal.set_read_implementation((file) => createReadableStream(`${server_root}/server/${file}`)); /** @type {import('types').ServerMetadata} */ const metadata = { nodes: [], routes: new Map() }; const nodes = await Promise.all(manifest._.nodes.map((loader) => loader())); // analyse nodes for (const node of nodes) { if (hash && node.universal) { const options = Object.keys(node.universal).filter((o) => o !== 'load'); if (options.length > 0) { throw new Error( `Page options are ignored when \`router.type === 'hash'\` (${node.universal_id} has ${options .filter((o) => o !== 'load') .map((o) => `'${o}'`) .join(', ')})` ); } } metadata.nodes[node.index] = { has_server_load: has_server_load(node) }; } // analyse routes for (const route of manifest._.routes) { const page = route.page && analyse_page( route.page.layouts.map((n) => (n === undefined ? n : nodes[n])), nodes[route.page.leaf] ); const endpoint = route.endpoint && analyse_endpoint(route, await route.endpoint()); if (page?.prerender && endpoint?.prerender) { throw new Error(`Cannot prerender a route with both +page and +server files (${route.id})`); } if (page?.config && endpoint?.config) { for (const key in { ...page.config, ...endpoint.config }) { if (JSON.stringify(page.config[key]) !== JSON.stringify(endpoint.config[key])) { throw new Error( `Mismatched route config for ${route.id} — the +page and +server files must export the same config, if any` ); } } } const route_config = page?.config ?? endpoint?.config ?? {}; const prerender = page?.prerender ?? endpoint?.prerender; if (prerender !== true) { for (const feature of list_features( route, manifest_data, server_manifest, tracked_features )) { check_feature(route.id, route_config, feature, config.adapter); } } const page_methods = page?.methods ?? []; const api_methods = endpoint?.methods ?? []; const entries = page?.entries ?? endpoint?.entries; metadata.routes.set(route.id, { config: route_config, methods: Array.from(new Set([...page_methods, ...api_methods])), page: { methods: page_methods }, api: { methods: api_methods }, prerender, entries: entries && (await entries()).map((entry_object) => resolve_route(route.id, entry_object)) }); } return metadata; } /** * @param {import('types').SSRRoute} route * @param {import('types').SSREndpoint} mod */ function analyse_endpoint(route, mod) { validate_server_exports(mod, route.id); if (mod.prerender && (mod.POST || mod.PATCH || mod.PUT || mod.DELETE)) { throw new Error( `Cannot prerender a +server file with POST, PATCH, PUT, or DELETE (${route.id})` ); } /** @type {Array<import('types').HttpMethod | '*'>} */ const methods = []; for (const method of /** @type {import('types').HttpMethod[]} */ (ENDPOINT_METHODS)) { if (mod[method]) methods.push(method); } if (mod.fallback) { methods.push('*'); } return { config: mod.config, entries: mod.entries, methods, prerender: mod.prerender ?? false }; } /** * @param {Array<import('types').SSRNode | undefined>} layouts * @param {import('types').SSRNode} leaf */ function analyse_page(layouts, leaf) { /** @type {Array<'GET' | 'POST'>} */ const methods = ['GET']; if (leaf.server?.actions) methods.push('POST'); const nodes = new PageNodes([...layouts, leaf]); nodes.validate(); return { config: nodes.get_config(), entries: leaf.universal?.entries ?? leaf.server?.entries, methods, prerender: nodes.prerender() }; } /** * @param {import('types').SSRRoute} route * @param {import('types').ManifestData} manifest_data * @param {import('vite').Manifest} server_manifest * @param {Record<string, string[]>} tracked_features */ function list_features(route, manifest_data, server_manifest, tracked_features) { const features = new Set(); const route_data = /** @type {import('types').RouteData} */ ( manifest_data.routes.find((r) => r.id === route.id) ); /** @param {string} id */ function visit(id) { const chunk = server_manifest[id]; if (!chunk) return; if (chunk.file in tracked_features) { for (const feature of tracked_features[chunk.file]) { features.add(feature); } } if (chunk.imports) { for (const id of chunk.imports) { visit(id); } } } let page_node = route_data?.leaf; while (page_node) { if (page_node.server) visit(page_node.server); page_node = page_node.parent ?? null; } if (route_data.endpoint) { visit(route_data.endpoint.file); } if (manifest_data.hooks.server) { // TODO if hooks.server.js imports `read`, it will be in the entry chunk // we don't currently account for that case visit(manifest_data.hooks.server); } return Array.from(features); }