UNPKG

@sveltejs/kit

Version:

SvelteKit is the fastest way to build Svelte apps

285 lines (240 loc) • 7.68 kB
/** @import { RemoteChunk } from 'types' */ import { join } from 'node:path'; import { pathToFileURL } from 'node:url'; import { validate_server_exports } from '../../utils/exports.js'; import { extract_svelte_config, load_vite_config } from '../config/index.js'; import { forked } from '../../utils/fork.js'; import { BODY_DEPENDENT_METHODS, ENDPOINT_METHODS } from '../../constants.js'; import { has_server_load, resolve_route } from '../../utils/routing.js'; import { check_feature } from '../features.js'; import { createReadableStream } from '@sveltejs/kit/node'; import { PageNodes } from '../../utils/page_nodes.js'; import { enable_verbose_errors } from '../../messages/internal/shared.js'; import * as e from '../../messages/build-errors.js'; import * as server_errors from '../../messages/server-errors.js'; import * as shared_errors from '../../messages/shared-errors.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>; * remotes: RemoteChunk[]; * vite_config_file: string | undefined; * }} opts */ async function analyse({ hash, manifest_path, manifest_data, server_manifest, tracked_features, env, remotes, vite_config_file }) { // this worker validates exports and resolves `entries` with Kit's own modules, rather than the // bundled server's copies that `configure` affects, so its shared errors need the full text too enable_verbose_errors(); /** @type {import('types').SSRManifest} */ const manifest = (await import(pathToFileURL(manifest_path).href)).manifest; const vite_config = await load_vite_config(vite_config_file); const config = extract_svelte_config(vite_config); const server_root = join(config.outDir, 'output'); /** @type {import('types').ServerModule} */ const { configure } = await import(pathToFileURL(`${server_root}/server/index.js`).href); await configure({ building: true, manifest, env, read: (file) => createReadableStream(`${server_root}/server/${file}`) }); /** @type {import('types').ServerMetadata} */ const metadata = { nodes: [], routes: new Map(), remotes: 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) { shared_errors.router_hash_page_options({ source: /** @type {string} */ (node.universal_id), options: options.map((o) => `'${o}'`).join(', ') }); } } metadata.nodes[node.index] = { has_server_load: has_server_load(node), has_universal_load: node.universal?.load !== undefined }; } // 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) { const endpoint_file = manifest_data.routes .find((r) => r.id === route.id) ?.endpoint?.file.split('/') .pop(); e.route_prerender_page_and_endpoint({ id: route.id, file: endpoint_file ?? '+server.js' }); } if (page?.config && endpoint?.config) { for (const key in { ...page.config, ...endpoint.config }) { if (JSON.stringify(page.config[key]) !== JSON.stringify(endpoint.config[key])) { e.route_config_mismatch({ id: route.id }); } } } 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)) }); } // analyse remotes for (const remote of remotes) { const loader = manifest.remotes[remote.hash]; const { default: functions } = await loader(); const exports = new Map(); for (const name in functions) { const internals = /** @type {import('types').RemoteInternals} */ (functions[name].__); const type = internals.type; exports.set(name, { type, dynamic: type !== 'prerender' || internals.dynamic }); } metadata.remotes.set(remote.hash, exports); } 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.fallback || /** @type {import('types').HttpMethod[]} */ (BODY_DEPENDENT_METHODS).some( (method) => mod[method] )) ) { server_errors.prerender_endpoint_methods({ methods: BODY_DEPENDENT_METHODS.join(', '), id: 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) ); const visited = new Set(); /** @param {string} id */ function visit(id) { if (visited.has(id)) return; visited.add(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); }