Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/short-chefs-stare.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': major
---

feat: use the Vite Environment API in development, preview, build analysis and prerendering
9 changes: 4 additions & 5 deletions packages/kit/src/core/adapt/builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ const extensions = [
* log: Logger;
* vite_config: ResolvedConfig;
* remotes: RemoteChunk[];
* out: string;
* explicit_env_config: Record<string, EnvVarConfig<any>> | null;
* }} opts
* @returns {Builder}
Expand All @@ -62,6 +63,7 @@ export function create_builder({
log,
vite_config,
remotes,
out,
explicit_env_config
}) {
/** @type {Map<RouteDefinition, RouteData>} */
Expand Down Expand Up @@ -131,14 +133,11 @@ export function create_builder({

async generateFallback(dest) {
const manifest_path = `${config.kit.outDir}/output/server/manifest-full.js`;
const env = loadEnv(vite_config.mode, config.kit.env.dir, '');

const fallback = await generate_fallback({
manifest_path,
env,
out_dir: config.kit.outDir,
origin: config.kit.paths.origin || 'http://sveltekit-prerender',
assets: config.kit.files.assets
root: vite_config.root,
out
});

if (existsSync(dest)) {
Expand Down
3 changes: 3 additions & 0 deletions packages/kit/src/core/adapt/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { create_builder } from './builder.js';
* @param {import('types').Logger} log
* @param {import('types').RemoteChunk[]} remotes
* @param {import('vite').ResolvedConfig} vite_config
* @param {string} out
* @param {Record<string, import('@sveltejs/kit').EnvVarConfig<any>> | null} explicit_env_config
*/
export async function adapt(
Expand All @@ -21,6 +22,7 @@ export async function adapt(
log,
remotes,
vite_config,
out,
explicit_env_config
) {
const { name, adapt } = config.kit.adapter;
Expand All @@ -37,6 +39,7 @@ export async function adapt(
log,
remotes,
vite_config,
out,
explicit_env_config
});

Expand Down
7 changes: 7 additions & 0 deletions packages/kit/src/core/postbuild/ambient.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
declare module '__SERVER__/index.js' {
export { Server } from '@sveltejs/kit';
}

declare module '__SERVER__/env.js' {
export * from '__sveltekit/env';
}
299 changes: 48 additions & 251 deletions packages/kit/src/core/postbuild/analyse.js
Original file line number Diff line number Diff line change
@@ -1,269 +1,66 @@
/** @import { RemoteChunk } from 'types' */
import { join } from 'node:path';
import { pathToFileURL } from 'node:url';
import { validate_server_exports } from '../../utils/exports.js';
/** @import { ManifestData, ServerMetadata } from 'types' */
/** @import { Manifest } from 'vite' */
import * as devalue from 'devalue';
import { extract_svelte_config, load_vite_config } from '../config/index.js';
import { forked } from '../../utils/fork.js';
import { ENDPOINT_METHODS } from '../../constants.js';
import { has_server_load, resolve_route } from '../../utils/routing.js';
import { createReadableStream } from '@sveltejs/kit/node';
import { PageNodes } from '../../utils/page_nodes.js';
import { create_build_server } from '../../exports/vite/build/vite_server.js';
import { get_address } from '../utils.js';

export default forked(import.meta.url, analyse);

const analysis_entry = import.meta.resolve('./analysis_entry.js');

/**
* @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
* @param {object} opts Arguments must be serialisable via the structured clone algorithm
* @param {string} opts.manifest_path
* @param {ManifestData} opts.manifest_data
* @param {Manifest} opts.server_manifest
* @param {Record<string, string[]>} opts.tracked_features
* @param {string} opts.out
* @param {RemoteChunk[]} opts.remotes
* @param {string} opts.root
* @param {string | undefined} opts.vite_config_file
* @returns {Promise<{ metadata: ServerMetadata }>}
*/
async function analyse({
hash,
manifest_path,
manifest_data,
server_manifest,
tracked_features,
env,
remotes,
out,
root,
vite_config_file
}) {
/** @type {import('@sveltejs/kit').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).kit;
const server_root = join(config.outDir, 'output');

/** @type {import('types').ServerInternalModule} */
const internal = await import(pathToFileURL(`${server_root}/server/internal.js`).href);

// configure `import { building } from '$app/env'` —
// essential we do this before analysing the code
internal.set_building();

// set `read` and `manifest`, in case they're used in initialisation
internal.set_manifest(manifest);
internal.set_read_implementation((file) => createReadableStream(`${server_root}/server/${file}`));

// `set_env` lives in a separate module that imports the user's `src/env` config. We import it
// *after* `set_building()` so that `building`-dependent expressions resolve correctly
/** @type {import('__sveltekit/env')} */
const { set_env } = await import(pathToFileURL(`${server_root}/server/env.js`).href);
set_env(env);

/** @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) {
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),
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) {
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 _ of list_features(route, manifest_data, server_manifest, tracked_features)) {
// TODO: check feature while inside the environment
}
}

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.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)
);

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);
const config = extract_svelte_config(vite_config);

const server = await create_build_server({
name: 'analyse',
svelte_config: config,
out,
root,
manifest_path,
server_path: analysis_entry
});

await server.listen();

const response = await fetch(get_address(server), {
method: 'POST',
headers: {
'content-type': 'application/json'
},
body: devalue.stringify({
server_manifest,
tracked_features,
manifest_data,
hash: config.kit.router.type === 'hash'
})
});

await server.close();

return { metadata: devalue.parse(await response.text()) };
}
Loading
Loading