diff --git a/.changeset/smooth-flies-begin.md b/.changeset/smooth-flies-begin.md new file mode 100644 index 000000000000..80d6c5efc17a --- /dev/null +++ b/.changeset/smooth-flies-begin.md @@ -0,0 +1,10 @@ +--- +"@sveltejs/adapter-cloudflare": major +"@sveltejs/adapter-netlify": major +"@sveltejs/adapter-node": major +"@sveltejs/adapter-vercel": major +"@sveltejs/kit": major +--- + +breaking: replace the `builder.generateManifest` with `builder.generateServerInstance` and `builder.manifest` + \ No newline at end of file diff --git a/documentation/docs/25-build-and-deploy/99-writing-adapters.md b/documentation/docs/25-build-and-deploy/99-writing-adapters.md index a4002696a018..77be9c8aeeb4 100644 --- a/documentation/docs/25-build-and-deploy/99-writing-adapters.md +++ b/documentation/docs/25-build-and-deploy/99-writing-adapters.md @@ -60,8 +60,8 @@ Within the `adapt` method, there are a number of things that an adapter should d - Clear out the build directory - Write SvelteKit output with `builder.writeClient`, `builder.writeServer`, and `builder.writePrerendered` - Output code that: - - Imports `Server` from `${builder.getServerDirectory()}/index.js` - - Instantiates the app with a manifest generated with `builder.generateManifest({ relativePath })` + - Imports a `server` instance generated by `${builder.generateServerInstance()}` + - Runs `await server.init({ env, read })` - Listens for requests from the platform, converts them to a standard [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) if necessary, calls the `server.respond(request, { getClientAddress })` function to generate a [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) and responds with it - expose any platform-specific information to SvelteKit via the `platform` option passed to `server.respond` - Bundle the output to avoid needing to install dependencies on the target platform, if necessary diff --git a/documentation/docs/60-appendix/35-migrating-to-sveltekit-3.md b/documentation/docs/60-appendix/35-migrating-to-sveltekit-3.md index 71426de4b435..d99fb5dbb27d 100644 --- a/documentation/docs/60-appendix/35-migrating-to-sveltekit-3.md +++ b/documentation/docs/60-appendix/35-migrating-to-sveltekit-3.md @@ -481,6 +481,7 @@ For adapter authors, there are some additional changes: - `builder.createEntries` has been removed — use `builder.writeClient`, `builder.writeServer` and `builder.writePrerendered` directly - `builder.compress` returns a list of compressed files - `builder.mkdirp` and `builder.rimraf` are deprecated in favour of `node:fs` methods +- `builder.generateManifest` has been removed — use `builder.generateServerInstance` to replace it, and `builder.manifest` to access the manifest ## Responses diff --git a/packages/adapter-bun/index.js b/packages/adapter-bun/index.js index abb7579f3f57..ae34a56c7230 100644 --- a/packages/adapter-bun/index.js +++ b/packages/adapter-bun/index.js @@ -136,9 +136,13 @@ export default function (opts = {}) { const manifest_file = path.resolve(server, 'manifest.js'); const server_options_file = path.resolve(src_dir, 'options.js'); + const tmp = builder.getBuildDirectory('bun-tmp'); + fs.mkdirSync(tmp, { recursive: true }); + builder.generateServerInstance(`${tmp}/server.js`); + const virtual_files = { [manifest_file]: - `export const manifest = ${builder.generateManifest({ relativePath: './' })};\n` + + `export const app_dir = ${JSON.stringify(builder.config.appDir)};\n` + `export const base = ${JSON.stringify(builder.config.paths.base || '/')};\n` + `export const embed = ${JSON.stringify(!!buildOptions.compile)};\n` + `export const env_prefix = ${JSON.stringify(envPrefix)};\n` + @@ -198,7 +202,7 @@ export default function (opts = {}) { name: 'adapter-bun', setup(build) { build.onResolve({ filter: /^(SERVER|MANIFEST|ROUTES|SERVER_OPTIONS)$/ }, ({ path }) => { - if (path === 'SERVER') return { path: `${server}/index.js` }; + if (path === 'SERVER') return { path: `${tmp}/server.js` }; if (path === 'MANIFEST') return { path: manifest_file }; if (path === 'ROUTES') return { path: routes_file }; if (path === 'SERVER_OPTIONS') return { path: server_options_file }; diff --git a/packages/adapter-bun/internal.d.ts b/packages/adapter-bun/internal.d.ts index b3e0ceed137a..ce532903b579 100644 --- a/packages/adapter-bun/internal.d.ts +++ b/packages/adapter-bun/internal.d.ts @@ -1,5 +1,5 @@ declare module 'MANIFEST' { - export const manifest: import('@sveltejs/kit').SSRManifest; + export const app_dir: string; export const base: string; export const embed: boolean; export const env_prefix: string; @@ -12,7 +12,7 @@ declare module 'ROUTES' { } declare module 'SERVER' { - export { Server } from '@sveltejs/kit'; + export const server: import('@sveltejs/kit').Server; } declare module 'SERVER_OPTIONS' { diff --git a/packages/adapter-bun/src/handler.js b/packages/adapter-bun/src/handler.js index d773849f2d56..6fa5d8c9a18a 100644 --- a/packages/adapter-bun/src/handler.js +++ b/packages/adapter-bun/src/handler.js @@ -1,11 +1,9 @@ /** @import { Server as BunServer } from 'bun' */ -import { Server } from 'SERVER'; -import { manifest, origin, env_prefix } from 'MANIFEST'; +import { server } from 'SERVER'; +import { origin, env_prefix } from 'MANIFEST'; import { server_assets } from 'ROUTES'; import { env, number_env } from './env.js'; -const server = new Server(manifest); - const address_header = env('ADDRESS_HEADER', '').toLowerCase(); const protocol_header = env('PROTOCOL_HEADER', '').toLowerCase(); const host_header = env('HOST_HEADER', '').toLowerCase(); diff --git a/packages/adapter-bun/src/routes-util.js b/packages/adapter-bun/src/routes-util.js index 942165cbdc13..3017e26a38f1 100644 --- a/packages/adapter-bun/src/routes-util.js +++ b/packages/adapter-bun/src/routes-util.js @@ -1,5 +1,5 @@ /** @import { BunFile, BunRequest, Serve } from 'bun' */ -import { manifest, base, embed } from 'MANIFEST'; +import { app_dir, base, embed } from 'MANIFEST'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -174,7 +174,7 @@ function file_route(file, meta, extra_headers = {}) { * @returns {Array<[string, RouteHandler]>} */ export function client_asset(url, filename = url, meta) { - const immutable = url.startsWith(`${manifest.appDir}/immutable/`); + const immutable = url.startsWith(`${app_dir}/immutable/`); const route = file_route( resolve_file('client', filename), meta, diff --git a/packages/adapter-bun/test/adapter.spec.ts b/packages/adapter-bun/test/adapter.spec.ts index a5963fa9f94f..f5fa75f984c5 100644 --- a/packages/adapter-bun/test/adapter.spec.ts +++ b/packages/adapter-bun/test/adapter.spec.ts @@ -121,7 +121,7 @@ describe('Bun build configuration', () => { const files = bun_build.mock.calls[0][0].files; expect(files[manifest_file]).toBe( - 'export const manifest = {"appDir":"_app"};\n' + + 'export const app_dir = "_app";\n' + 'export const base = "/docs";\n' + 'export const embed = false;\n' + 'export const env_prefix = "APP_";\n' + @@ -130,7 +130,9 @@ describe('Bun build configuration', () => { expect(files[options_file]).toBe( 'export default {"hostname":"127.0.0.1","port":4000,"development":true};' ); - expect(builder.generateManifest).toHaveBeenCalledWith({ relativePath: './' }); + expect(builder.generateServerInstance).toHaveBeenCalledWith( + '.svelte-kit/output/bun-tmp/server.js' + ); }); test('resolves generated runtime modules through the Bun plugin', async () => { @@ -144,7 +146,7 @@ describe('Bun build configuration', () => { ); const resolve_module = on_resolve.mock.calls[0][1]; expect(resolve_module({ path: 'SERVER' })).toEqual({ - path: '.svelte-kit/output/server/index.js' + path: '.svelte-kit/output/bun-tmp/server.js' }); expect(resolve_module({ path: 'MANIFEST' })).toEqual({ path: manifest_file }); expect(resolve_module({ path: 'ROUTES' })).toEqual({ path: routes_file }); @@ -576,7 +578,7 @@ function create_builder({ instrumentation?: boolean; } = {}) { return { - config: { outDir: '.svelte-kit', paths: { base, origin } }, + config: { outDir: '.svelte-kit', paths: { base, origin }, appDir: '_app' }, routes, prerendered: { pages: new Map(prerendered_pages), @@ -588,6 +590,9 @@ function create_builder({ warn: mock((_message: string) => {}), info: mock((_message: string) => {}) }, + getAppPath: () => `${base}/_app`, + generateServerInstance: mock(() => {}), + getBuildDirectory: (dir: string) => `.svelte-kit/output/${dir}`, getServerDirectory: () => '.svelte-kit/output/server', writeClient: mock(() => client_files), writePrerendered: mock(() => prerendered_files), diff --git a/packages/adapter-bun/test/handler.spec.ts b/packages/adapter-bun/test/handler.spec.ts index bc1bd272827f..7735ede19973 100644 --- a/packages/adapter-bun/test/handler.spec.ts +++ b/packages/adapter-bun/test/handler.spec.ts @@ -14,7 +14,6 @@ afterEach(() => { test('initializes SvelteKit with Bun environment variables and server-readable assets', async () => { const loaded = await load_handler(); - expect(loaded.construct).toHaveBeenCalledWith(loaded.manifest); expect(loaded.init).toHaveBeenCalledWith({ env: Bun.env, read: expect.any(Function) @@ -196,23 +195,18 @@ async function load_handler({ envPrefix = '', response = new Response('ok') }: { origin?: string; envPrefix?: string; response?: Response } = {}) { - const manifest = { appDir: '_app' }; const stream = new ReadableStream(); const asset = { stream: mock(() => stream) }; - const construct = mock((_value: unknown) => {}); const init = mock(async (_options: any) => {}); const respond = mock(async (_request: Request, _options: any) => response); class Server { - constructor(value: unknown) { - construct(value); - } init = init; respond = respond; } - mock.module('SERVER', () => ({ Server })); - mock_manifest({ manifest, origin, env_prefix: envPrefix }); + mock.module('SERVER', () => ({ server: new Server() })); + mock_manifest({ app_dir: '_app', origin, env_prefix: envPrefix }); mock_routes({ server_assets: new Map([['asset.txt', asset]]) }); const request_ip = mock((_request: Request): any => ({ @@ -227,9 +221,7 @@ async function load_handler({ return { handler, - manifest, stream, - construct, init, respond, request_ip, diff --git a/packages/adapter-bun/test/mocks.ts b/packages/adapter-bun/test/mocks.ts index 150e00cbd4d4..d1366ccad4b3 100644 --- a/packages/adapter-bun/test/mocks.ts +++ b/packages/adapter-bun/test/mocks.ts @@ -5,19 +5,19 @@ import { mock } from 'bun:test'; // exports that src modules pull from these build-generated specifiers export function mock_manifest({ - manifest, + app_dir, base, embed, origin, env_prefix = '' }: { - manifest?: unknown; + app_dir?: string; base?: string; embed?: boolean; origin?: string; env_prefix?: string; } = {}) { - mock.module('MANIFEST', () => ({ manifest, base, embed, origin, env_prefix })); + mock.module('MANIFEST', () => ({ app_dir, base, embed, origin, env_prefix })); } export function mock_routes({ diff --git a/packages/adapter-bun/test/routes.spec.ts b/packages/adapter-bun/test/routes.spec.ts index 1c736217457c..23fbc87a94c0 100644 --- a/packages/adapter-bun/test/routes.spec.ts +++ b/packages/adapter-bun/test/routes.spec.ts @@ -78,7 +78,7 @@ test('a root deployment registers routes with a leading slash', async () => { }); test('immutable SvelteKit assets receive a long-lived cache policy', async () => { - const { routes } = await load_routes({ appDir: '_app' }); + const { routes } = await load_routes({ app_dir: '_app' }); const request = new Request('http://localhost/'); const immutable = ( @@ -283,8 +283,8 @@ test('prerendered redirects retain their status and location', async () => { expect((handler as any).GET.headers.get('location')).toBe('/new'); }); -async function load_routes({ base = '/', embed = false, appDir = '_app' } = {}) { - mock_manifest({ manifest: { appDir }, base, embed }); +async function load_routes({ base = '/', embed = false, app_dir = '_app' } = {}) { + mock_manifest({ app_dir, base, embed }); // the real Bun.file runs, with the spy recording resolved paths; the files it // points at need not exist because nothing reads their contents const file = spyOn(Bun, 'file'); diff --git a/packages/adapter-cloudflare/files/internal.d.ts b/packages/adapter-cloudflare/files/internal.d.ts index 6c79569f7f7f..d3e8d81fe7e8 100644 --- a/packages/adapter-cloudflare/files/internal.d.ts +++ b/packages/adapter-cloudflare/files/internal.d.ts @@ -1,12 +1,17 @@ declare module 'SERVER' { - export { Server } from '@sveltejs/kit'; + export const server: import('@sveltejs/kit').Server; } -declare module 'MANIFEST' { - import { SSRManifest } from '@sveltejs/kit'; - - export const manifest: SSRManifest; - export const prerendered: Set; - export const app_path: string; - export const base_path: string; +namespace Cloudflare { + interface Env { + ASSETS_BINDING: { + fetch: typeof fetch; + }; + [key: string]: string | undefined; + } } + +declare const BASE_PATH: string; +declare const APP_PATH: string; +declare const PRERENDERED: Set; +declare const MANIFEST_ASSETS: Set; diff --git a/packages/adapter-cloudflare/files/worker.js b/packages/adapter-cloudflare/files/worker.js index 911af0cfb971..47b478476762 100644 --- a/packages/adapter-cloudflare/files/worker.js +++ b/packages/adapter-cloudflare/files/worker.js @@ -1,13 +1,12 @@ -import { Server } from 'SERVER'; -import { manifest, prerendered, base_path } from 'MANIFEST'; +import { server } from 'SERVER'; import { env } from 'cloudflare:workers'; -const server = new Server(manifest); +const base_path = BASE_PATH; +const assets = MANIFEST_ASSETS; +const prerendered = PRERENDERED; -const app_path = `/${manifest.appPath}`; - -const immutable = `${app_path}/immutable/`; -const version_file = `${app_path}/version.json`; +const immutable = `/${APP_PATH}/immutable/`; +const version_file = `/${APP_PATH}/version.json`; /** * We don't know the origin until we receive a request, but @@ -17,11 +16,10 @@ const version_file = `${app_path}/version.json`; let origin; const initialized = server.init({ - env: /** @type {Record} */ (env), + env, read: async (file) => { const url = `${origin}/${file}`; - const response = - await /** @type {{ ASSETS: { fetch: typeof fetch } }} */ (env).ASSETS.fetch(url); + const response = await env.ASSETS_BINDING.fetch(url); if (!response.ok) { throw new Error( @@ -33,13 +31,8 @@ const initialized = server.init({ } }); +/** @type {import('@cloudflare/workers-types').ExportedHandler} */ export default { - /** - * @param {Request} req - * @param {{ ASSETS: { fetch: typeof fetch } }} env - * @param {ExecutionContext} ctx - * @returns {Promise} - */ async fetch(req, env, ctx) { if (!origin) { origin = new URL(req.url).origin; @@ -61,8 +54,7 @@ export default { let is_static_asset = false; const filename = stripped_pathname.slice(base_path.length + 1); if (filename) { - is_static_asset = - manifest.assets.has(filename) || manifest.assets.has(filename + '/index.html'); + is_static_asset = assets.has(filename) || assets.has(filename + '/index.html'); } let location = pathname.at(-1) === '/' ? stripped_pathname : pathname + '/'; @@ -73,7 +65,7 @@ export default { pathname === version_file || pathname.startsWith(immutable) ) { - let res = await env.ASSETS.fetch(req); + let res = await env.ASSETS_BINDING.fetch(req); // `_headers` applies cache regardless of status, so we need to ensure an // error response does not get cached if (res.status >= 400) { diff --git a/packages/adapter-cloudflare/index.js b/packages/adapter-cloudflare/index.js index d137538e7194..74e90850e1fe 100644 --- a/packages/adapter-cloudflare/index.js +++ b/packages/adapter-cloudflare/index.js @@ -103,12 +103,7 @@ export default function (options = {}) { // worker const worker_dest_dir = path.dirname(worker_dest); - writeFileSync( - `${tmp}/manifest.js`, - `export const manifest = ${builder.generateManifest({ relativePath: path.posix.relative(tmp, builder.getServerDirectory()) })};\n\n` + - `export const prerendered = new Set(${JSON.stringify(builder.prerendered.paths)});\n\n` + - `export const base_path = ${JSON.stringify(builder.config.paths.base)};\n` - ); + builder.generateServerInstance(`${tmp}/server.js`); builder.copy(`${files}/worker.js`, worker_dest, { replace: { // the paths returned by the Wrangler config might be Windows paths, @@ -116,9 +111,12 @@ export default function (options = {}) { // will be interpreted as escape characters and create an incorrect import path. // We also need to ensure the relative imports start with ./ since Wrangler // errors if a relative import looks like a package import - SERVER: `./${posixify(path.relative(worker_dest_dir, builder.getServerDirectory()))}/index.js`, - MANIFEST: `./${posixify(path.relative(worker_dest_dir, tmp))}/manifest.js`, - ASSETS: assets_binding + SERVER: `./${posixify(path.relative(worker_dest_dir, tmp))}/server.js`, + BASE_PATH: JSON.stringify(builder.config.paths.base), + APP_PATH: JSON.stringify(builder.getAppPath()), + MANIFEST_ASSETS: `new Set(${JSON.stringify(builder.manifest.assets.map((a) => a.path))})`, + PRERENDERED: `new Set(${JSON.stringify(builder.prerendered.paths)})`, + ASSETS_BINDING: assets_binding } }); if (builder.hasServerInstrumentationFile()) { diff --git a/packages/adapter-netlify/files/edge.js b/packages/adapter-netlify/files/edge.js index 518593892b06..b38d8b34e1cd 100644 --- a/packages/adapter-netlify/files/edge.js +++ b/packages/adapter-netlify/files/edge.js @@ -1,7 +1,4 @@ -import { Server } from '0SERVER'; -import { manifest } from 'MANIFEST'; - -const server = new Server(manifest); +import { server } from '0SERVER'; /** * We don't know the origin until we receive a request, but diff --git a/packages/adapter-netlify/files/serverless.js b/packages/adapter-netlify/files/serverless.js index 56502dc95c6c..7397719ef335 100644 --- a/packages/adapter-netlify/files/serverless.js +++ b/packages/adapter-netlify/files/serverless.js @@ -1,14 +1,11 @@ -import { Server } from '0SERVER'; import { createReadableStream } from '@sveltejs/kit/node'; import process from 'node:process'; /** - * @param {import('@sveltejs/kit').SSRManifest} manifest + * @param {import('@sveltejs/kit').Server} server * @returns {(request: Request, context: import('@netlify/types').Context) => Promise} */ -export function init(manifest) { - const server = new Server(manifest); - +export function init(server) { /** @type {Promise | null} */ let init_promise = server.init({ env: process.env, diff --git a/packages/adapter-netlify/index.js b/packages/adapter-netlify/index.js index c08cda021813..a0708832a35c 100644 --- a/packages/adapter-netlify/index.js +++ b/packages/adapter-netlify/index.js @@ -1,6 +1,6 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; -import { join, posix } from 'node:path'; -import { fileURLToPath, pathToFileURL } from 'node:url'; +import { join } from 'node:path'; +import { fileURLToPath } from 'node:url'; import { builtinModules } from 'node:module'; import process from 'node:process'; import toml from '@iarna/toml'; @@ -264,12 +264,12 @@ function write_frameworks_config({ builder }) { * }} opts */ function generate_serverless_function({ builder, routes, patterns, name, type, exclude }) { - const manifest = builder.generateManifest({ - relativePath: '../server', - routes + builder.generateServerInstance(`.netlify/v1/server-${name}.js`, { + routes, + serverDirectory: '.netlify/v1/server' }); - const fn = generate_serverless_function_module(manifest, type); + const fn = generate_serverless_function_module(name, type); const config = generate_config_export(name, patterns, exclude); if (builder.hasServerInstrumentationFile()) { @@ -288,11 +288,11 @@ function generate_serverless_function({ builder, routes, patterns, name, type, e } /** - * @param {string} manifest + * @param {string} name * @param {ServerlessFunctionType} type * @returns {string} */ -function generate_serverless_function_module(manifest, type) { +function generate_serverless_function_module(name, type) { if (type === 'catch-all') { // Netlify encodes the response body but `fetch` automatically decodes it. // So, we need to remove the `content-encoding` header to allow Netlify @@ -300,10 +300,11 @@ function generate_serverless_function_module(manifest, type) { return `\ import { applyReroute } from '@sveltejs/kit/adapter'; import { init } from '../serverless.js'; +import { server } from '../server-${name}.js'; const original_url_header = \`x-sveltekit-original-url-\${process.env.NETLIFY_FUNCTIONS_TOKEN}\` -const respond = init(${manifest}); +const respond = init(server); export default async (request, context) => { const catch_all_response = await respond(request, context); @@ -329,10 +330,11 @@ export default async (request, context) => { if (type === 'split') { return `\ import { init } from '../serverless.js'; +import { server } from '../server-${name}.js'; const original_url_header = \`x-sveltekit-original-url-\${process.env.NETLIFY_FUNCTIONS_TOKEN}\` -const respond = init(${manifest}); +const respond = init(server); export default async (request, context) => { if (request.headers.has(original_url_header)) { @@ -348,8 +350,9 @@ export default async (request, context) => { return `\ import { init } from '../serverless.js'; +import { server } from '../server-${name}.js'; -export default init(${manifest}); +export default init(server); `; } @@ -422,23 +425,14 @@ async function generate_edge_functions({ builder }) { mkdirSync('.netlify/v1/edge-functions', { recursive: true }); builder.log.minor('Generating Edge Function...'); - const relativePath = posix.relative(tmp, builder.getServerDirectory()); builder.copy(`${files}/edge.js`, `${tmp}/entry.js`, { replace: { - '0SERVER': `${relativePath}/index.js`, - MANIFEST: './manifest.js' + '0SERVER': `./server.js` } }); - const manifest = builder.generateManifest({ - relativePath - }); - - writeFileSync(`${tmp}/manifest.js`, `export const manifest = ${manifest};\n`); - - /** @type {{ assets: Set }} */ - const { assets } = (await import(pathToFileURL(`${tmp}/manifest.js`).href)).manifest; + builder.generateServerInstance(`${tmp}/server.js`); const path = '/*'; // We only need to specify paths without the trailing slash because @@ -449,7 +443,7 @@ async function generate_edge_functions({ builder }) { `/${builder.getAppPath()}/version.json`, // the base root and `trailingSlash: 'always'` pages are recorded with a trailing slash ...builder.prerendered.paths.map((path) => (path === '/' ? path : path.replace(/\/$/, ''))), - ...Array.from(assets).flatMap((asset) => { + ...Array.from(builder.manifest.assets).flatMap(({ path: asset }) => { if (asset.endsWith('/index.html')) { const dir = asset.replace(/\/index\.html$/, ''); return [`${builder.config.paths.base}/${asset}`, `${builder.config.paths.base}/${dir}`]; diff --git a/packages/adapter-netlify/internal.d.ts b/packages/adapter-netlify/internal.d.ts index 55da8ba1fbf5..97df9e32dfe3 100644 --- a/packages/adapter-netlify/internal.d.ts +++ b/packages/adapter-netlify/internal.d.ts @@ -1,9 +1,3 @@ declare module '0SERVER' { - export { Server } from '@sveltejs/kit'; -} - -declare module 'MANIFEST' { - import { SSRManifest } from '@sveltejs/kit'; - - export const manifest: SSRManifest; + export const server: import('@sveltejs/kit').Server; } diff --git a/packages/adapter-node/index.d.ts b/packages/adapter-node/index.d.ts index 71daeab1361c..4166b9c9b3ae 100644 --- a/packages/adapter-node/index.d.ts +++ b/packages/adapter-node/index.d.ts @@ -5,6 +5,11 @@ declare global { const ENV_PREFIX: string; const PRECOMPRESS: boolean; const ORIGIN: string | undefined; + const UNCOMPRESSED_EXTENSIONS: Set; + const BASE_PATH: string; + const APP_PATH: string; + const PRERENDERED: Set; + const MIME_TYPES: Record; } interface AdapterOptions { diff --git a/packages/adapter-node/index.js b/packages/adapter-node/index.js index 4ce90cf944c7..f1e4725e5640 100644 --- a/packages/adapter-node/index.js +++ b/packages/adapter-node/index.js @@ -1,4 +1,4 @@ -import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { mkdirSync, readFileSync, rmSync } from 'node:fs'; import { extname } from 'node:path'; import { fileURLToPath } from 'node:url'; import { rolldown } from 'rolldown'; @@ -65,16 +65,6 @@ export default function (opts = {}) { const dir_id = `${entries}/dir.js`; - writeFileSync( - `${server}/manifest.js`, - [ - `export const manifest = ${builder.generateManifest({ relativePath: './' })};`, - `export const prerendered = new Set(${JSON.stringify(builder.prerendered.paths)});`, - `export const base = ${JSON.stringify(builder.config.paths.base)};`, - `export const uncompressed_extensions = new Set(${JSON.stringify([...uncompressed_extensions])});` - ].join('\n\n') - ); - /** @type {Record} */ const input = { index: `${entries}/index.js`, @@ -86,6 +76,20 @@ export default function (opts = {}) { input['instrumentation.server'] = `${server}/instrumentation.server.js`; } + builder.generateServerInstance(`${server}/server.js`); + + /** @type {Record} */ + const defines = { + UNCOMPRESSED_EXTENSIONS: `new Set(${JSON.stringify([...uncompressed_extensions])})`, + BASE_PATH: JSON.stringify(builder.config.paths.base), + APP_PATH: JSON.stringify(builder.getAppPath()), + PRERENDERED: `new Set(${JSON.stringify(builder.prerendered.paths)})`, + MIME_TYPES: JSON.stringify(builder.mimeTypes), + ORIGIN: JSON.stringify(builder.config.paths.origin) || 'undefined', + ENV_PREFIX: JSON.stringify(envPrefix), + PRECOMPRESS: JSON.stringify(precompress) + }; + // we bundle the Vite output so that deployments only need // their production dependencies. Anything in devDependencies // will get included in the bundled code @@ -111,9 +115,11 @@ export default function (opts = {}) { { // resolve the app's server and manifest, generated above name: 'adapter-node-resolve-app', - resolveId(id) { - if (id === 'SERVER') return `${server}/index.js`; - if (id === 'MANIFEST') return `${server}/manifest.js`; + resolveId: { + filter: { id: /^SERVER$/ }, + handler() { + return `${server}/server.js`; + } } }, { @@ -125,13 +131,13 @@ export default function (opts = {}) { filter: { id: new RegExp(escape_regex(entries)) }, handler(_code, _id, { magicString }) { if (!magicString) throw new Error('experimental.nativeMagicString is not enabled'); - magicString - .replace(/\bENV_PREFIX\b/g, JSON.stringify(envPrefix)) - .replace(/\bPRECOMPRESS\b/g, JSON.stringify(precompress)) - .replace( - /\bORIGIN\b/g, - JSON.stringify(builder.config.paths.origin) || 'undefined' - ); + + for (const [from, to] of Object.entries(defines)) { + // remove $& and $N substitutions by replacing every $ with $$ + const value = to.replace(/\$/g, '$$$$'); + magicString.replace(new RegExp(`\\b${from}\\b`, 'g'), value); + } + return { code: magicString, map: magicString.generateMap().toString() diff --git a/packages/adapter-node/internal.d.ts b/packages/adapter-node/internal.d.ts index 8748941c8a6f..8897747bd7c3 100644 --- a/packages/adapter-node/internal.d.ts +++ b/packages/adapter-node/internal.d.ts @@ -1,12 +1,3 @@ -declare module 'MANIFEST' { - import { SSRManifest } from '@sveltejs/kit'; - - export const base: string; - export const uncompressed_extensions: Set; - export const manifest: SSRManifest; - export const prerendered: Set; -} - declare module 'SERVER' { - export { Server } from '@sveltejs/kit'; + export const server: import('@sveltejs/kit').Server; } diff --git a/packages/adapter-node/src/handler.js b/packages/adapter-node/src/handler.js index 28ab9d49b650..5aa8920b4402 100644 --- a/packages/adapter-node/src/handler.js +++ b/packages/adapter-node/src/handler.js @@ -6,17 +6,17 @@ import process from 'node:process'; import sirv from 'sirv'; import { parse as polka_url_parser } from '@polka/url'; import { getRequest, setResponse, createReadableStream } from '@sveltejs/kit/node'; -import { Server } from 'SERVER'; -import { manifest, prerendered, base, uncompressed_extensions } from 'MANIFEST'; +import { server } from 'SERVER'; import { dir } from './dir.js'; import { env, env_prefix } from './env.js'; import { parse_as_bytes } from './utils.js'; /** @typedef {(req: IncomingMessage, res: ServerResponse, next: () => void | Promise) => void | Promise} Middleware */ -const server = new Server(manifest); - const origin = ORIGIN; +const uncompressed_extensions = UNCOMPRESSED_EXTENSIONS; +const prerendered = PRERENDERED; +const mime_types = MIME_TYPES; const xff_depth = parseInt(env('XFF_DEPTH', '1')); const address_header = env('ADDRESS_HEADER', '').toLowerCase(); @@ -32,7 +32,7 @@ if (isNaN(body_size_limit)) { ); } -const asset_dir = `${dir}/client${base}`; +const asset_dir = `${dir}/client${BASE_PATH}`; await server.init({ env: process.env, @@ -56,16 +56,12 @@ function serve(path, client = false) { } // `sirv` uses its own bundled `mrmime`, which the manifest's added types never reach - let type = manifest.mimeTypes[pathname.slice(pathname.lastIndexOf('.'))]; + let type = mime_types[pathname.slice(pathname.lastIndexOf('.'))]; if (type === 'text/html') type += ';charset=utf-8'; if (type) res.setHeader('content-type', type); // only apply to build directory, not e.g. version.json - if ( - client && - pathname.startsWith(`/${manifest.appPath}/immutable/`) && - res.statusCode === 200 - ) { + if (client && pathname.startsWith(`/${APP_PATH}/immutable/`) && res.statusCode === 200) { res.setHeader('cache-control', 'public,max-age=31536000,immutable'); } } diff --git a/packages/adapter-vercel/files/serverless.js b/packages/adapter-vercel/files/serverless.js index d6aa640edf16..8e3bdc096974 100644 --- a/packages/adapter-vercel/files/serverless.js +++ b/packages/adapter-vercel/files/serverless.js @@ -1,10 +1,7 @@ import { createReadableStream } from '@sveltejs/kit/node'; -import { Server } from 'SERVER'; -import { manifest } from 'MANIFEST'; +import { server } from 'SERVER'; import process from 'node:process'; -const server = new Server(manifest); - await server.init({ env: process.env, read: createReadableStream diff --git a/packages/adapter-vercel/index.js b/packages/adapter-vercel/index.js index 99a7b67c6ec5..daa085d3384c 100644 --- a/packages/adapter-vercel/index.js +++ b/packages/adapter-vercel/index.js @@ -61,11 +61,9 @@ const plugin = function (defaults = {}) { builder.copy(proxy, entrypoint); } - const relativePath = path.posix.relative(tmp, builder.getServerDirectory()); builder.copy(`${files}/serverless.js`, proxy ? `${tmp}/serverless.js` : entrypoint, { replace: { - SERVER: `${relativePath}/index.js`, - MANIFEST: './manifest.js' + SERVER: `./server.js` } }); if (builder.hasServerInstrumentationFile()) { @@ -74,11 +72,7 @@ const plugin = function (defaults = {}) { instrumentation: `${builder.getServerDirectory()}/instrumentation.server.js` }); } - - write( - `${tmp}/manifest.js`, - `export const manifest = ${builder.generateManifest({ relativePath, routes })};\n` - ); + builder.generateServerInstance(`${tmp}/server.js`, { routes }); await create_function_bundle(builder, entrypoint, dir, config); diff --git a/packages/adapter-vercel/internal.d.ts b/packages/adapter-vercel/internal.d.ts index 537f7cc041d1..8897747bd7c3 100644 --- a/packages/adapter-vercel/internal.d.ts +++ b/packages/adapter-vercel/internal.d.ts @@ -1,8 +1,3 @@ declare module 'SERVER' { - export { Server } from '@sveltejs/kit'; -} - -declare module 'MANIFEST' { - import { SSRManifest } from '@sveltejs/kit'; - export const manifest: SSRManifest; + export const server: import('@sveltejs/kit').Server; } diff --git a/packages/kit/src/core/adapt/builder.js b/packages/kit/src/core/adapt/builder.js index 5be5e91482bd..2a0790567988 100644 --- a/packages/kit/src/core/adapt/builder.js +++ b/packages/kit/src/core/adapt/builder.js @@ -13,19 +13,21 @@ import { rmSync, statSync } from 'node:fs'; -import { extname, resolve, join, dirname, relative } from 'node:path'; +import path from 'node:path'; import { pipeline } from 'node:stream'; import { promisify } from 'node:util'; import zlib from 'node:zlib'; -import { copy, walk } from '../../utils/filesystem.js'; +import { copy, relative_path, walk } from '../../utils/filesystem.js'; import { posixify } from '../../utils/os.js'; import { generate_manifest } from '../generate_manifest/index.js'; import { get_route_segments } from '../../utils/routing.js'; import generate_fallback from '../postbuild/fallback.js'; -import { write } from '../sync/utils.js'; +import { dedent, write } from '../sync/utils.js'; import { find_server_assets } from '../generate_manifest/find_server_assets.js'; import { create_exported_declarations } from '../env.js'; import { handle_issues, validate } from '../../exports/internal/env.js'; +import { get_mime_lookup } from '../utils.js'; +import { lookup as mime_lookup } from '../../utils/mime.js'; const pipe = promisify(pipeline); const extensions = [ @@ -51,6 +53,7 @@ const extensions = [ * route_data: RouteData[]; * prerendered: Prerendered; * prerender_map: PrerenderMap; + * app_manifest: typeof import('$app/manifest'); * log: Logger; * vite_config: ResolvedConfig; * remotes: RemoteChunk[]; @@ -65,6 +68,7 @@ export function create_builder({ route_data, prerendered, prerender_map, + app_manifest, log, vite_config, remotes, @@ -103,6 +107,12 @@ export function create_builder({ return facade; }); + // $app/manifest cannot include the service worker because it's used by the service worker itself, + // but the adapter needs to know about it so we add it here. + if (build_data.service_worker) { + app_manifest.assets.push({ path: build_data.service_worker }); + } + return { log, rimraf: (dir) => rmSync(dir, { force: true, recursive: true }), @@ -112,17 +122,42 @@ export function create_builder({ config, prerendered, routes, + manifest: app_manifest, + get mimeTypes() { + // TODO - make the `generate_manifest` function return data instead of a string, and retrieve mime types from there + const mime_types = get_mime_lookup(build_data.manifest_data); + const server_assets = find_server_assets( + build_data, + route_data.filter((route) => prerender_map.get(route.id) !== true), + vite_config.root + ); + /** @type {Record} */ + const files = {}; + for (const file of server_assets) { + files[file] = 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.paths) { + const ext = path.extname(pathname); + if (ext) mime_types[ext] ??= mime_lookup(ext) || ''; + } + return mime_types; + }, async compress(directory) { if (!existsSync(directory)) { return []; } - const files = [...walk(directory)].filter((file) => extensions.includes(extname(file))); + const files = [...walk(directory)].filter((file) => extensions.includes(path.extname(file))); await Promise.all( files.flatMap((file) => { - const abs = resolve(directory, file); + const abs = path.resolve(directory, file); return [compress_file(abs, 'gz'), compress_file(abs, 'br')]; }) ); @@ -184,17 +219,34 @@ export function create_builder({ write(`${dest}/env.js`, `export const env=${payload}`); }, - generateManifest({ relativePath, routes: subset }) { - return generate_manifest({ - build_data, - prerendered: prerendered.paths, - relative_path: relativePath, - routes: subset - ? subset.map((route) => /** @type {import('types').RouteData} */ (lookup.get(route))) - : route_data.filter((route) => prerender_map.get(route.id) !== true), - remotes, - root: vite_config.root - }); + generateManifest() { + throw new Error( + 'The `generateManifest` adapter API has been removed — use `generateServerInstance` or `builder.manifest` instead. You may need to update your adapter' + ); + }, + + generateServerInstance(dest, { routes: subset, serverDirectory } = {}) { + const relative = relative_path( + path.dirname(dest), + serverDirectory ?? this.getServerDirectory() + ); + write( + dest, + dedent` + import { Server } from '${relative}/index.js'; + const manifest = ${generate_manifest({ + build_data, + prerendered: prerendered.paths, + relative_path: relative, + routes: subset + ? subset.map((route) => /** @type {import('types').RouteData} */ (lookup.get(route))) + : route_data.filter((route) => prerender_map.get(route.id) !== true), + remotes, + root: vite_config.root + })}; + export const server = new Server(manifest); + ` + ); }, getBuildDirectory(name) { @@ -241,7 +293,7 @@ export function create_builder({ instrument({ entrypoint, instrumentation, - start = join(dirname(entrypoint), 'start.js'), + start = path.join(path.dirname(entrypoint), 'start.js'), module = { exports: ['default'] } @@ -262,8 +314,10 @@ export function create_builder({ copy(`${entrypoint}.map`, `${start}.map`); } - const relative_instrumentation = posixify(relative(dirname(entrypoint), instrumentation)); - const relative_start = posixify(relative(dirname(entrypoint), start)); + const relative_instrumentation = posixify( + path.relative(path.dirname(entrypoint), instrumentation) + ); + const relative_start = posixify(path.relative(path.dirname(entrypoint), start)); const facade = 'generateText' in module diff --git a/packages/kit/src/core/adapt/builder.spec.js b/packages/kit/src/core/adapt/builder.spec.js index af76faba6d45..226e6436db42 100644 --- a/packages/kit/src/core/adapt/builder.spec.js +++ b/packages/kit/src/core/adapt/builder.spec.js @@ -54,8 +54,9 @@ test('copy files', () => { }); test('compress files', async () => { - // @ts-expect-error - we don't need the whole config for this test const builder = create_builder({ + // @ts-expect-error - we don't need the whole config for this test + build_data: {}, route_data: [] }); @@ -77,8 +78,9 @@ test('compress files', async () => { }); test('compress returns an empty array for a directory that does not exist', async () => { - // @ts-expect-error - we don't need the whole config for this test const builder = create_builder({ + // @ts-expect-error - we don't need the whole config for this test + build_data: {}, route_data: [] }); @@ -100,8 +102,11 @@ test('instrument generates facade with posix paths', () => { const entrypoint = join(dest, 'index.js'); const instrumentation = join(dest, 'server', 'instrumentation.server.js'); - // @ts-expect-error - we don't need the whole config for this test - const builder = create_builder({ route_data: [] }); + const builder = create_builder({ + // @ts-expect-error - we don't need the whole config for this test + build_data: {}, + route_data: [] + }); builder.instrument({ entrypoint, diff --git a/packages/kit/src/core/adapt/index.js b/packages/kit/src/core/adapt/index.js index 46f1fd5bc640..7b6c0850a26a 100644 --- a/packages/kit/src/core/adapt/index.js +++ b/packages/kit/src/core/adapt/index.js @@ -7,6 +7,7 @@ import { create_builder } from './builder.js'; * @param {import('types').ServerMetadata} server_metadata * @param {import('types').Prerendered} prerendered * @param {import('types').PrerenderMap} prerender_map + * @param {typeof import('$app/manifest')} app_manifest * @param {import('types').Logger} log * @param {import('types').RemoteChunk[]} remotes * @param {import('vite').ResolvedConfig} vite_config @@ -18,6 +19,7 @@ export async function adapt( server_metadata, prerendered, prerender_map, + app_manifest, log, remotes, vite_config, @@ -48,6 +50,7 @@ export async function adapt( route_data: build_data.manifest_data.routes.filter((route) => route.page || route.endpoint), prerendered, prerender_map, + app_manifest, log, remotes, vite_config, diff --git a/packages/kit/src/core/generate_manifest/index.js b/packages/kit/src/core/generate_manifest/index.js index f524e1da70d2..9edced21991d 100644 --- a/packages/kit/src/core/generate_manifest/index.js +++ b/packages/kit/src/core/generate_manifest/index.js @@ -1,4 +1,4 @@ -/** @import { RemoteChunk } from 'types' */ +/** @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'; @@ -15,10 +15,10 @@ 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: import('types').BuildData; + * build_data: BuildData; * prerendered: string[]; * relative_path: string; - * routes: import('types').RouteData[]; + * routes: RouteData[]; * remotes: RemoteChunk[]; * root: string; * }} opts diff --git a/packages/kit/src/core/sync/write_app_manifest.js b/packages/kit/src/core/sync/write_app_manifest.js index 8a0fe6b07b18..3a16fb5c2495 100644 --- a/packages/kit/src/core/sync/write_app_manifest.js +++ b/packages/kit/src/core/sync/write_app_manifest.js @@ -1,4 +1,5 @@ /** @import { Asset, ManifestData, RouteData } from 'types' */ +/** @import { ManifestRoute } from '$app/manifest' */ import { s } from '../../utils/misc.js'; import { is_app_route, is_endpoint_route, is_page_route } from './create_manifest_data/index.js'; @@ -60,11 +61,11 @@ function stringify_assets(assets) { /** * @param {RouteData[] | undefined} routes - * @returns {Array<{ id: string; page: boolean; endpoint: boolean }>} + * @returns {ManifestRoute[]} */ export function get_manifest_routes(routes) { return ( - routes?.filter(is_app_route).map((route) => ({ + routes?.filter(is_app_route).map((route) => /** @type {ManifestRoute} */ ({ id: route.id, page: is_page_route(route), endpoint: is_endpoint_route(route) diff --git a/packages/kit/src/exports/public.d.ts b/packages/kit/src/exports/public.d.ts index 8fde64196aeb..83070be7a60f 100644 --- a/packages/kit/src/exports/public.d.ts +++ b/packages/kit/src/exports/public.d.ts @@ -139,6 +139,17 @@ export interface Builder { prerendered: Prerendered; /** An array of all routes (including prerendered) */ routes: RouteDefinition[]; + /** + * The value of the `$app/manifest` module. + * The only difference is `manifest.assets` also includes the service worker, if it exists. + * @since 3.0.0 + */ + manifest: typeof import('$app/manifest'); + /** + * A record of file extensions to MIME types + * @since 3.0.0 + */ + mimeTypes: Record; /** * Create separate functions that map to one or more routes of your app. @@ -166,8 +177,9 @@ export interface Builder { * Generate a server-side manifest to initialise the SvelteKit [server](https://svelte.dev/docs/kit/@sveltejs-kit#Server) with. * @param opts * @param opts.relativePath A relative path to the base directory of the server build output + * @deprecated removed in 3.0. Use `builder.generateServerInstance` or `builder.manifest` instead */ - generateManifest: (opts: { relativePath: string; routes?: RouteDefinition[] }) => string; + generateManifest?: (opts: { relativePath: string; routes?: RouteDefinition[] }) => string; /** * Resolve a path to the `name` directory inside `outDir`, e.g. `/path/to/.svelte-kit/my-adapter`. @@ -181,6 +193,20 @@ export interface Builder { /** Get the application path including any configured `base` path, e.g. `my-base-path/_app`. */ getAppPath: () => string; + /** + * Generates a module exposing a SvelteKit [Server](https://svelte.dev/docs/kit/@sveltejs-kit#Server) instance. + * @param dest + * @param opts.routes A subset of the routes to include in the server's manifest + * @param opts.serverDirectory The directory containing the server code. Defaults to `getServerDirectory()`. + * @since 3.0.0 + */ + generateServerInstance: ( + dest: string, + opts?: { + routes?: RouteDefinition[]; + serverDirectory?: string; + } + ) => void; /** * Write client assets to `dest`. * @param dest the destination folder diff --git a/packages/kit/src/exports/vite/build/index.js b/packages/kit/src/exports/vite/build/index.js index 84b850a50702..db7686ac2de7 100644 --- a/packages/kit/src/exports/vite/build/index.js +++ b/packages/kit/src/exports/vite/build/index.js @@ -862,6 +862,14 @@ export function plugin_compile( // defer until after other buildApp hooks have run finalise = async () => { + /** @type {typeof import('$app/manifest')} */ + const app_manifest = { + assets: manifest_data.assets.map((asset) => ({ path: asset.file })), + immutable: immutable ?? [], + prerendered: prerendered_paths, + routes: get_manifest_routes(manifest_data.routes) + }; + // defer creating the service worker to avoid other plugins from // overwriting it if they run a client environment build if (service_worker_entry_file) { @@ -871,12 +879,10 @@ export function plugin_compile( builder.environments.serviceWorker.config.define = { ...builder.environments.serviceWorker.config.define, - __SVELTEKIT_MANIFEST_ASSETS__: s( - manifest_data.assets.map((asset) => ({ path: asset.file })) - ), - __SVELTEKIT_MANIFEST_IMMUTABLE__: s(immutable), - __SVELTEKIT_MANIFEST_PRERENDERED__: s(prerendered_paths), - __SVELTEKIT_MANIFEST_ROUTES__: s(get_manifest_routes(manifest_data.routes)) + __SVELTEKIT_MANIFEST_ASSETS__: s(app_manifest.assets), + __SVELTEKIT_MANIFEST_IMMUTABLE__: s(app_manifest.immutable), + __SVELTEKIT_MANIFEST_PRERENDERED__: s(app_manifest.prerendered), + __SVELTEKIT_MANIFEST_ROUTES__: s(app_manifest.routes) }; // we have to overwrite this because it can't be configured per environment in the config hook @@ -905,6 +911,7 @@ export function plugin_compile( metadata, prerendered, prerender_results.prerender_map, + app_manifest, log, remotes, vite_config, diff --git a/packages/kit/src/exports/vite/preview/index.js b/packages/kit/src/exports/vite/preview/index.js index 2c29442c6032..4e0187c9e1b7 100644 --- a/packages/kit/src/exports/vite/preview/index.js +++ b/packages/kit/src/exports/vite/preview/index.js @@ -43,6 +43,7 @@ export async function preview(vite, svelte_config) { /** @type {ServerModule} */ const { Server } = await import(pathToFileURL(join(dir, 'index.js')).href); + /** @type {{ manifest: import('@sveltejs/kit').SSRManifest }} */ const { manifest } = await import(pathToFileURL(join(dir, 'manifest.js')).href); set_assets(assets); diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index 09c909a2a620..f060bdc20629 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -125,6 +125,17 @@ declare module '@sveltejs/kit' { prerendered: Prerendered; /** An array of all routes (including prerendered) */ routes: RouteDefinition[]; + /** + * The value of the `$app/manifest` module. + * The only difference is `manifest.assets` also includes the service worker, if it exists. + * @since 3.0.0 + */ + manifest: typeof import('$app/manifest'); + /** + * A record of file extensions to MIME types + * @since 3.0.0 + */ + mimeTypes: Record; /** * Create separate functions that map to one or more routes of your app. @@ -151,8 +162,9 @@ declare module '@sveltejs/kit' { /** * Generate a server-side manifest to initialise the SvelteKit [server](https://svelte.dev/docs/kit/@sveltejs-kit#Server) with. * @param opts.relativePath A relative path to the base directory of the server build output + * @deprecated removed in 3.0. Use `builder.generateServerInstance` or `builder.manifest` instead */ - generateManifest: (opts: { relativePath: string; routes?: RouteDefinition[] }) => string; + generateManifest?: (opts: { relativePath: string; routes?: RouteDefinition[] }) => string; /** * Resolve a path to the `name` directory inside `outDir`, e.g. `/path/to/.svelte-kit/my-adapter`. @@ -166,6 +178,19 @@ declare module '@sveltejs/kit' { /** Get the application path including any configured `base` path, e.g. `my-base-path/_app`. */ getAppPath: () => string; + /** + * Generates a module exposing a SvelteKit [Server](https://svelte.dev/docs/kit/@sveltejs-kit#Server) instance. + * @param opts.routes A subset of the routes to include in the server's manifest + * @param opts.serverDirectory The directory containing the server code. Defaults to `getServerDirectory()`. + * @since 3.0.0 + */ + generateServerInstance: ( + dest: string, + opts?: { + routes?: RouteDefinition[]; + serverDirectory?: string; + } + ) => void; /** * Write client assets to `dest`. * @param dest the destination folder