diff --git a/.changeset/early-eggs-crash.md b/.changeset/early-eggs-crash.md new file mode 100644 index 000000000000..c76428fb68db --- /dev/null +++ b/.changeset/early-eggs-crash.md @@ -0,0 +1,9 @@ +--- +"@sveltejs/kit": minor +"@sveltejs/adapter-vercel": minor +"@sveltejs/adapter-node": minor +"@sveltejs/adapter-cloudflare": minor +--- + +feat: move `basePath` and `prerenderedRoutes` to the public SSRManifest api + \ No newline at end of file diff --git a/.changeset/flat-cases-see.md b/.changeset/flat-cases-see.md new file mode 100644 index 000000000000..bcee64ec0c38 --- /dev/null +++ b/.changeset/flat-cases-see.md @@ -0,0 +1,6 @@ +--- +"@sveltejs/kit": major +--- + +breaking: remove `SSRManifest` from public types + \ No newline at end of file diff --git a/.changeset/rare-schools-itch.md b/.changeset/rare-schools-itch.md new file mode 100644 index 000000000000..3e4cd3ad8926 --- /dev/null +++ b/.changeset/rare-schools-itch.md @@ -0,0 +1,6 @@ +--- +"@sveltejs/kit": minor +--- + +feat: allow adapters to override getRequest and setResponse + \ No newline at end of file diff --git a/documentation/docs/20-core-concepts/30-form-actions.md b/documentation/docs/20-core-concepts/30-form-actions.md index 1a3d2ad03e40..5d076a15c101 100644 --- a/documentation/docs/20-core-concepts/30-form-actions.md +++ b/documentation/docs/20-core-concepts/30-form-actions.md @@ -6,6 +6,8 @@ A `+page.server.js` file can export _actions_, which allow you to `POST` data to When using `
`, client-side JavaScript is optional, but you can easily _progressively enhance_ your form interactions with JavaScript to provide the best user experience. +> [!NOTE] The experimental [`form` remote function](remote-functions#form) covers the same use cases as form actions, adding type safety and [single-flight mutations](remote-functions#Single-flight-mutations). Form actions are feature-complete and will continue to work, but new development is focused on remote functions, which are intended to become the recommended way to communicate with the server. Consider remote functions for new projects, keeping in mind that the API may change while the feature is experimental. + ## Default actions In the simplest case, a page declares a `default` action: 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..342b51ae350d 100644 --- a/documentation/docs/25-build-and-deploy/99-writing-adapters.md +++ b/documentation/docs/25-build-and-deploy/99-writing-adapters.md @@ -13,6 +13,8 @@ type AdapterSpecificOptions = any; // @filename: index.js // ---cut--- +import { getRequest, setResponse } from '@sveltejs/kit/node'; + /** @param {AdapterSpecificOptions} options */ export default function (options) { /** @type {import('@sveltejs/kit').Adapter} */ @@ -40,6 +42,15 @@ export default function (options) { // Return `false if it can't, or throw a descriptive error. } }, + getRequest(options) { + const request = getRequest(options); + // modify the Request object here if needed + return request; + }, + setResponse(res, response) { + // handle WebSockets here, for example + setResponse(res, response); + }, vite: { plugins: { // add plugins here to integrate with Vite @@ -53,15 +64,15 @@ export default function (options) { } ``` -Of these, `name` and `adapt` are required. `emulate`, `vite`, and `supports` are optional. +Of these, `name` and `adapt` are required. `emulate`, `vite`, `getRequest`, `setResponse`, and `supports` are optional. Within the `adapt` method, there are a number of things that an adapter should do: - 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.writeServerEntrypoint()}` + - 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/packages/adapter-bun/index.js b/packages/adapter-bun/index.js index 4f6ccf666cf7..6b71d3531037 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.writeServerEntrypoint(`${tmp}/server.js`); + const virtual_files = { [manifest_file]: - `export const manifest = ${builder.generateManifest({ relativePath: './' })};\n` + + `export const app_dir = ${JSON.stringify(builder.getAppPath())};\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` + @@ -179,7 +183,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 d20d2fbb0ed1..19fd3d4d59e2 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'; @@ -184,7 +184,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-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..cc81b4c6b32e 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.writeServerEntrypoint(`${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..2c52b27be6e2 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.writeServerEntrypoint(`.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.writeServerEntrypoint(`${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..bf8c7db95494 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.writeServerEntrypoint(`${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 f79534e3dcad..313ee8553099 100644 --- a/packages/adapter-node/src/handler.js +++ b/packages/adapter-node/src/handler.js @@ -4,15 +4,15 @@ 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'; -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(); @@ -28,7 +28,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, @@ -52,16 +52,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..137247c51352 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.writeServerEntrypoint(`${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/CHANGELOG.md b/packages/kit/CHANGELOG.md index 41691e4f7560..6d41f032ad34 100644 --- a/packages/kit/CHANGELOG.md +++ b/packages/kit/CHANGELOG.md @@ -830,11 +830,17 @@ - feat: add `ErrorProps` to generated types ([#16272](https://github.com/sveltejs/kit/pull/16272)) +## 2.70.3 + +### Patch Changes + +- fix: avoid eagerly reading `$app/state` dependencies during module initialization ([`b61018d`](https://github.com/sveltejs/kit/commit/b61018d0526e827d1ef8cf8e21ef62b92230ae3d)) + ## 2.70.2 ### Patch Changes -- fix: prevent quadratic backtracking in `Accept` header content negotiation ([#1](https://github.com/homebase-garage/igeclouds.github.io/pull/1)) +- fix: prevent quadratic backtracking in `Accept` header content negotiation ([`82712fc`](https://github.com/sveltejs/kit/commit/82712fc02c24b1dcf5b25d7a52129cd8455f04f5)) ## 2.70.1 diff --git a/packages/kit/src/core/adapt/builder.js b/packages/kit/src/core/adapt/builder.js index 5be5e91482bd..12a290f5b3ff 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, @@ -112,17 +116,41 @@ export function create_builder({ config, prerendered, routes, + manifest: app_manifest, + get mimeTypes() { + 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 +212,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 is deprecated — it has been replaced with \`writeServerEntrypoint\` and \`builder.manifest\`. You may need to update your adapter` + ); + }, + + writeServerEntrypoint(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 +286,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 +307,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/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..6d199a8e3353 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 @@ -102,49 +102,48 @@ export function generate_manifest({ // prettier-ignore // String representation of - /** @template {import('@sveltejs/kit').SSRManifest} T */ + /** @template {import('types').SSRManifest} T */ const manifest_expr = dedent` { - appDir: ${s(build_data.app_dir)}, - appPath: ${s(build_data.app_path)}, + app_dir: ${s(build_data.app_dir)}, + app_path: ${s(build_data.app_path)}, assets: new Set(${s(assets)}), - mimeTypes: ${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)} - } + 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)} } `; diff --git a/packages/kit/src/core/postbuild/analyse.js b/packages/kit/src/core/postbuild/analyse.js index dde9f386195e..b28d8efe42de 100644 --- a/packages/kit/src/core/postbuild/analyse.js +++ b/packages/kit/src/core/postbuild/analyse.js @@ -34,7 +34,7 @@ async function analyse({ remotes, vite_config_file }) { - /** @type {import('@sveltejs/kit').SSRManifest} */ + /** @type {import('types').SSRManifest} */ const manifest = (await import(pathToFileURL(manifest_path).href)).manifest; const vite_config = await load_vite_config(vite_config_file); @@ -65,7 +65,7 @@ async function analyse({ remotes: new Map() }; - const nodes = await Promise.all(manifest._.nodes.map((loader) => loader())); + const nodes = await Promise.all(manifest.nodes.map((loader) => loader())); // analyse nodes for (const node of nodes) { @@ -88,7 +88,7 @@ async function analyse({ } // analyse routes - for (const route of manifest._.routes) { + for (const route of manifest.routes) { const page = route.page && analyse_page( @@ -147,7 +147,7 @@ async function analyse({ // analyse remotes for (const remote of remotes) { - const loader = manifest._.remotes[remote.hash]; + const loader = manifest.remotes[remote.hash]; const { default: functions } = await loader(); const exports = new Map(); diff --git a/packages/kit/src/core/postbuild/fallback.js b/packages/kit/src/core/postbuild/fallback.js index 1023d7890aec..335a72bfd049 100644 --- a/packages/kit/src/core/postbuild/fallback.js +++ b/packages/kit/src/core/postbuild/fallback.js @@ -24,7 +24,7 @@ async function generate_fallback({ manifest_path, env, out_dir, origin, assets } /** @type {import('types').ServerModule} */ const { Server } = await import(pathToFileURL(`${server_root}/server/index.js`).href); - /** @type {import('@sveltejs/kit').SSRManifest} */ + /** @type {import('types').SSRManifest} */ const manifest = (await import(pathToFileURL(manifest_path).href)).manifest; set_building(); diff --git a/packages/kit/src/core/postbuild/prerender.js b/packages/kit/src/core/postbuild/prerender.js index 66010667be86..2d07b4df5e17 100644 --- a/packages/kit/src/core/postbuild/prerender.js +++ b/packages/kit/src/core/postbuild/prerender.js @@ -48,7 +48,7 @@ async function prerender({ vite_config_file, is_tty }) { - /** @type {import('@sveltejs/kit').SSRManifest} */ + /** @type {import('types').SSRManifest} */ const manifest = (await import(pathToFileURL(manifest_path).href)).manifest; /** @type {import('types').ServerInternalModule} */ @@ -659,7 +659,7 @@ async function prerender({ /** @type {Array} */ const prerender_functions = []; - for (const loader of Object.values(manifest._.remotes)) { + for (const loader of Object.values(manifest.remotes)) { const module = await loader(); for (const fn of Object.values(module.default)) { 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..076bc4568e0b 100644 --- a/packages/kit/src/exports/public.d.ts +++ b/packages/kit/src/exports/public.d.ts @@ -12,11 +12,11 @@ import { RequestOptions, RouteSegment } from '../types/private.js'; -import { BuildData, SSRNodeLoader, SSRRoute, ValidatedConfig } from 'types'; +import { SSRManifest, ValidatedConfig } from 'types'; import { Plugin } from 'vite'; import { RouteId as AppRouteId, LayoutParams as AppLayoutParams } from '$app/types'; -import { ParamMatcher } from '@sveltejs/kit/params'; import { StandardSchemaV1 } from '@standard-schema/spec'; +import { getRequest, setResponse } from '@sveltejs/kit/node'; export { PrerenderOption } from '../types/private.js'; @@ -52,6 +52,20 @@ export interface Adapter { */ instrumentation?: () => boolean; }; + /** + * This function overrides the default behavior during dev and preview to + * convert an `http.IncomingMessage` to a `Request` object. + * To call the original setRequest function, import it from `@sveltejs/kit/node`. + * @since 3.0.0 + */ + getRequest?: typeof getRequest; + /** + * This function overrides the default behavior during dev and preview to + * write a `Response` object to an `http.ServerResponse`. + * To call the original setResponse function, import it from `@sveltejs/kit/node`. + * @since 3.0.0 + */ + setResponse?: typeof setResponse; /** * Creates an `Emulator`, which allows the adapter to influence the environment * during dev, build and prerendering. @@ -139,6 +153,10 @@ export interface Builder { prerendered: Prerendered; /** An array of all routes (including prerendered) */ routes: RouteDefinition[]; + /** The value of the `$app/manifest` module */ + manifest: typeof import('$app/manifest'); + /** A record of file extensions to MIME types */ + mimeTypes: Record; /** * Create separate functions that map to one or more routes of your app. @@ -166,8 +184,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.writeServerEntrypoint` 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 +200,19 @@ export interface Builder { /** Get the application path including any configured `base` path, e.g. `my-base-path/_app`. */ getAppPath: () => string; + /** + * Writes a javascript file that initialises the SvelteKit [server](https://svelte.dev/docs/kit/@sveltejs-kit#Server). + * @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()`. + */ + writeServerEntrypoint: ( + dest: string, + opts?: { + routes?: RouteDefinition[]; + serverDirectory?: string; + } + ) => void; /** * Write client assets to `dest`. * @param dest the destination folder @@ -669,32 +701,6 @@ export interface ServerInitOptions { read?: (file: string) => MaybePromise; } -/** - * Information required to instantiate a new `Server` instance. - */ -export interface SSRManifest { - /** The directory where SvelteKit keeps its stuff, including static assets (such as JS and CSS) and internally-used routes. */ - appDir: string; - /** The `base` and `appDir` settings combined without a leading slash. */ - appPath: string; - /** Static files from `config.files.assets` and the service worker (if any). */ - assets: Set; - mimeTypes: Record; - - /** @internal private fields */ - _: { - client: BuildData['client']; - nodes: SSRNodeLoader[]; - /** hashed filename -> import to that file */ - remotes: Record Promise<{ default: Record }>>; - routes: SSRRoute[]; - prerendered_routes: Set; - matchers: () => Promise>; - /** A `[file]: size` map of all assets imported by server code. */ - server_assets: Record; - }; -} - /** * The generic form of `PageServerLoad` and `LayoutServerLoad`. You should import those from `./$types` (see [generated types](https://svelte.dev/docs/kit/types#Generated-types)) * rather than using `ServerLoad` directly. diff --git a/packages/kit/src/exports/vite/build/index.js b/packages/kit/src/exports/vite/build/index.js index a9074a98c856..70aff33cffe9 100644 --- a/packages/kit/src/exports/vite/build/index.js +++ b/packages/kit/src/exports/vite/build/index.js @@ -836,6 +836,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) { @@ -845,12 +853,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 @@ -879,6 +885,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/dev/index.js b/packages/kit/src/exports/vite/dev/index.js index 42b901d3abcd..1db46c785c60 100644 --- a/packages/kit/src/exports/vite/dev/index.js +++ b/packages/kit/src/exports/vite/dev/index.js @@ -1,6 +1,6 @@ -/** @import { RequestEvent, SSRManifest } from '@sveltejs/kit' */ +/** @import { RequestEvent } from '@sveltejs/kit' */ /** @import { EnvironmentModuleNode, ErrorPayload, ViteDevServer } from 'vite' */ -/** @import { ManifestData, PrerenderOption, RemoteChunk, ServerModule, SSRNode, UniversalNode, ValidatedConfig } from 'types' */ +/** @import { ManifestData, PrerenderOption, RemoteChunk, ServerModule, SSRNode, UniversalNode, ValidatedConfig, SSRManifest } from 'types' */ import process from 'node:process'; import fs from 'node:fs'; import path from 'node:path'; @@ -181,176 +181,172 @@ export async function dev( } manifest = { - appDir: svelte_config.appDir, - appPath: svelte_config.appDir, + app_dir: svelte_config.appDir, + app_path: svelte_config.appDir, assets: new Set(manifest_data.assets.map((asset) => asset.file)), - mimeTypes: get_mime_lookup(manifest_data), - _: { - client: { - start: `${get_runtime_base(root)}/client/entry.js`, - app: `${to_fs(svelte_config.outDir)}/generated/dev/client/app.js`, - imports: [], - stylesheets: [], - fonts: [], - uses_env_dynamic_public: true, - nodes: - svelte_config.router.resolution === 'client' - ? undefined - : manifest_data.nodes.map((node, i) => { - if (node.component || node.universal) { - return `${svelte_config.paths.base}${to_fs(svelte_config.outDir)}/generated/dev/client/nodes/${i}.js`; - } - }), - // `css` is not necessary in dev, as the JS file from `nodes` will reference the CSS file - routes: - svelte_config.router.resolution === 'client' - ? undefined - : compact( - manifest_data.routes.map((route) => { - if (!route.page) return; - - return { - id: route.id, - pattern: route.pattern, - params: route.params, - layouts: route.page.layouts.map((l) => - l !== undefined ? [!!manifest_data.nodes[l].server, l] : undefined - ), - errors: route.page.errors, - leaf: [!!manifest_data.nodes[route.page.leaf].server, route.page.leaf] - }; - }) - ) - }, - server_assets: new Proxy( - {}, - { - has: (_, /** @type {string} */ file) => fs.existsSync(from_fs(file)), - get: (_, /** @type {string} */ file) => fs.statSync(from_fs(file)).size + mime_types: get_mime_lookup(manifest_data), + client: { + start: `${get_runtime_base(root)}/client/entry.js`, + app: `${to_fs(svelte_config.outDir)}/generated/dev/client/app.js`, + imports: [], + stylesheets: [], + fonts: [], + uses_env_dynamic_public: true, + nodes: + svelte_config.router.resolution === 'client' + ? undefined + : manifest_data.nodes.map((node, i) => { + if (node.component || node.universal) { + return `${svelte_config.paths.base}${to_fs(svelte_config.outDir)}/generated/dev/client/nodes/${i}.js`; + } + }), + // `css` is not necessary in dev, as the JS file from `nodes` will reference the CSS file + routes: + svelte_config.router.resolution === 'client' + ? undefined + : compact( + manifest_data.routes.map((route) => { + if (!route.page) return; + + return { + id: route.id, + pattern: route.pattern, + params: route.params, + layouts: route.page.layouts.map((l) => + l !== undefined ? [!!manifest_data.nodes[l].server, l] : undefined + ), + errors: route.page.errors, + leaf: [!!manifest_data.nodes[route.page.leaf].server, route.page.leaf] + }; + }) + ) + }, + server_assets: new Proxy( + {}, + { + has: (_, /** @type {string} */ file) => fs.existsSync(from_fs(file)), + get: (_, /** @type {string} */ file) => fs.statSync(from_fs(file)).size + } + ), + nodes: manifest_data.nodes.map((node, index) => { + return async () => { + const result = /** @type {SSRNode} */ ({}); + result.index = index; + result.universal_id = node.universal; + result.server_id = node.server; + + // these are unused in dev, but it's easier to include them + result.imports = []; + result.stylesheets = []; + result.fonts = []; + + /** @type {EnvironmentModuleNode[]} */ + const module_nodes = []; + + if (node.component) { + result.component = async () => { + const { module_node, module } = await resolve(/** @type {string} */ (node.component)); + + module_nodes.push(module_node); + + return module.default; + }; } - ), - nodes: manifest_data.nodes.map((node, index) => { - return async () => { - const result = /** @type {SSRNode} */ ({}); - result.index = index; - result.universal_id = node.universal; - result.server_id = node.server; - - // these are unused in dev, but it's easier to include them - result.imports = []; - result.stylesheets = []; - result.fonts = []; - - /** @type {EnvironmentModuleNode[]} */ - const module_nodes = []; - - if (node.component) { - result.component = async () => { - const { module_node, module } = await resolve( - /** @type {string} */ (node.component) - ); - - module_nodes.push(module_node); - - return module.default; - }; - } - if (node.universal) { - if (node.page_options?.ssr === false) { - result.universal = /** @type {UniversalNode} */ (node.page_options); - } else { - // TODO: explain why the file was loaded on the server if we fail to load it - const { module, module_node } = await resolve(node.universal); - module_nodes.push(module_node); - result.universal = module; - } + if (node.universal) { + if (node.page_options?.ssr === false) { + result.universal = /** @type {UniversalNode} */ (node.page_options); + } else { + // TODO: explain why the file was loaded on the server if we fail to load it + const { module, module_node } = await resolve(node.universal); + module_nodes.push(module_node); + result.universal = module; } + } - if (node.server) { - const { module } = await resolve(node.server); - result.server = module; - } + if (node.server) { + const { module } = await resolve(node.server); + result.server = module; + } - // in dev we inline all styles to avoid FOUC. this gets populated lazily so that - // components/stylesheets loaded via import() during `load` are included - result.inline_styles = async () => { - /** @type {Set} */ - const deps = new Set(); + // in dev we inline all styles to avoid FOUC. this gets populated lazily so that + // components/stylesheets loaded via import() during `load` are included + result.inline_styles = async () => { + /** @type {Set} */ + const deps = new Set(); - for (const module_node of module_nodes) { - await find_deps(vite_dev_server, module_node, deps); - } + for (const module_node of module_nodes) { + await find_deps(vite_dev_server, module_node, deps); + } - /** @type {Record} */ - const styles = {}; - - for (const dep of deps) { - if (vite.isCSSRequest(dep.url) && !vite_css_query_regex.test(dep.url)) { - const inlineCssUrl = dep.url.includes('?') - ? dep.url.replace('?', '?inline&') - : dep.url + '?inline'; - try { - const mod = await runner.import(inlineCssUrl); - styles[dep.url] = mod.default; - } catch { - // this can happen with dynamically imported modules, I think - // because the Vite module graph doesn't distinguish between - // static and dynamic imports? TODO investigate, submit fix - } + /** @type {Record} */ + const styles = {}; + + for (const dep of deps) { + if (vite.isCSSRequest(dep.url) && !vite_css_query_regex.test(dep.url)) { + const inlineCssUrl = dep.url.includes('?') + ? dep.url.replace('?', '?inline&') + : dep.url + '?inline'; + try { + const mod = await runner.import(inlineCssUrl); + styles[dep.url] = mod.default; + } catch { + // this can happen with dynamically imported modules, I think + // because the Vite module graph doesn't distinguish between + // static and dynamic imports? TODO investigate, submit fix } } + } - return styles; - }; - - return result; + return styles; }; - }), - prerendered_routes: new Set(), - get remotes() { - return Object.fromEntries( - get_remotes().map((remote) => [ - remote.hash, - () => runner.import(remote.file).then((module) => ({ default: module })) - ]) - ); - }, - routes: compact( - manifest_data.routes.map((route) => { - if (!route.page && !route.endpoint) return null; - - const endpoint = route.endpoint; - - return { - id: route.id, - pattern: route.pattern, - params: route.params, - page: route.page, - endpoint: endpoint - ? async () => { - const url = path.resolve(root, endpoint.file); - return await loud_ssr_load_module(url); - } - : null, - endpoint_id: endpoint?.file - }; - }) - ), - matchers: async () => { - if (!manifest_data.params) return {}; - const url = path.resolve(root, manifest_data.params); - const module = await runner.import(url); + return result; + }; + }), + prerendered_routes: new Set(), + get remotes() { + return Object.fromEntries( + get_remotes().map((remote) => [ + remote.hash, + () => runner.import(remote.file).then((module) => ({ default: module })) + ]) + ); + }, + routes: compact( + manifest_data.routes.map((route) => { + if (!route.page && !route.endpoint) return null; + + const endpoint = route.endpoint; + + return { + id: route.id, + pattern: route.pattern, + params: route.params, + page: route.page, + endpoint: endpoint + ? async () => { + const url = path.resolve(root, endpoint.file); + return await loud_ssr_load_module(url); + } + : null, + endpoint_id: endpoint?.file + }; + }) + ), + matchers: async () => { + if (!manifest_data.params) return {}; - if (!module.params) { - throw new Error( - `${manifest_data.params} does not export \`params\` from \`defineParams\`` - ); - } + const url = path.resolve(root, manifest_data.params); + const module = await runner.import(url); - return module.params; + if (!module.params) { + throw new Error( + `${manifest_data.params} does not export \`params\` from \`defineParams\`` + ); } + + return module.params; } }; } @@ -607,7 +603,7 @@ export async function dev( read: (file) => createReadableStream(from_fs(file)) }); - const request = getRequest({ + const request = (svelte_config.adapter?.getRequest ?? getRequest)({ base, request: req, response: res @@ -642,7 +638,7 @@ export async function dev( throw new Error('Could not determine clientAddress'); }, read: (file) => { - if (file in manifest._.server_assets) { + if (file in manifest.server_assets) { return fs.readFileSync(from_fs(file)); } @@ -660,11 +656,11 @@ export async function dev( // @ts-expect-error serve_static_middleware.handle(req, res, () => { log_dev_response(rendered.status, format_response(rendered.status, request)); - setResponse(res, rendered); + (svelte_config.adapter?.setResponse ?? setResponse)(res, rendered); }); } else { log_dev_response(rendered.status, format_response(rendered.status, request)); - setResponse(res, rendered); + (svelte_config.adapter?.setResponse ?? setResponse)(res, rendered); } } catch (e) { const error = coalesce_to_error(e); diff --git a/packages/kit/src/exports/vite/preview/index.js b/packages/kit/src/exports/vite/preview/index.js index 2c29442c6032..53012c87eb55 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('types').SSRManifest }} */ const { manifest } = await import(pathToFileURL(join(dir, 'manifest.js')).href); set_assets(assets); @@ -204,13 +205,13 @@ export async function preview(vite, svelte_config) { vite.middlewares.use(async (req, res) => { const host = req.headers[':authority'] || req.headers.host; - const request = getRequest({ + const request = (svelte_config.adapter?.getRequest ?? getRequest)({ base: `${protocol}://${host}`, request: req, response: res }); - setResponse( + (svelte_config.adapter?.setResponse ?? setResponse)( res, await server.respond(request, { getClientAddress: () => { @@ -219,7 +220,7 @@ export async function preview(vite, svelte_config) { throw new Error('Could not determine clientAddress'); }, read: (file) => { - if (file in manifest._.server_assets) { + if (file in manifest.server_assets) { return fs.readFileSync(join(dir, file)); } diff --git a/packages/kit/src/runtime/app/paths/server.js b/packages/kit/src/runtime/app/paths/server.js index 1804f22b6ec0..3628b9b4440d 100644 --- a/packages/kit/src/runtime/app/paths/server.js +++ b/packages/kit/src/runtime/app/paths/server.js @@ -83,8 +83,8 @@ export async function match(url) { resolved_path = resolved_path.slice(base.length) || '/'; } - const matchers = await manifest._.matchers(); - const result = find_route(resolved_path, manifest._.routes, matchers); + const matchers = await manifest.matchers(); + const result = find_route(resolved_path, manifest.routes, matchers); if (result) { return { diff --git a/packages/kit/src/runtime/app/server/index.js b/packages/kit/src/runtime/app/server/index.js index b1d97a5f3a6d..2ecec85b1282 100644 --- a/packages/kit/src/runtime/app/server/index.js +++ b/packages/kit/src/runtime/app/server/index.js @@ -59,9 +59,9 @@ export function read(asset) { : asset.slice(assets.length + 1) ); - if (file in manifest._.server_assets) { - const length = manifest._.server_assets[file]; - const type = manifest.mimeTypes[file.slice(file.lastIndexOf('.'))]; + if (file in manifest.server_assets) { + const length = manifest.server_assets[file]; + const type = manifest.mime_types[file.slice(file.lastIndexOf('.'))]; return new Response(read_implementation(file), { headers: { diff --git a/packages/kit/src/runtime/server/data/index.js b/packages/kit/src/runtime/server/data/index.js index 087d52c8fabd..b0c04f0a38db 100644 --- a/packages/kit/src/runtime/server/data/index.js +++ b/packages/kit/src/runtime/server/data/index.js @@ -13,7 +13,7 @@ import { with_version_header } from '../utils.js'; * @param {import('@sveltejs/kit').RequestEvent} event * @param {import('types').RequestState} state * @param {{ page: Pick | null }} route - * @param {import('@sveltejs/kit').SSRManifest} manifest + * @param {import('types').SSRManifest} manifest * @param {boolean[] | undefined} invalidated_data_nodes * @param {import('types').TrailingSlash} trailing_slash * @returns {Promise} @@ -52,7 +52,7 @@ export async function render_data( } // == because it could be undefined (in dev) or null (in build, because of JSON.stringify) - const node = n == undefined ? n : await manifest._.nodes[n](); + const node = n == undefined ? n : await manifest.nodes[n](); // load this. for the child, return as is. for the final result, stream things return load_server_data({ event: new_event, diff --git a/packages/kit/src/runtime/server/fetch.js b/packages/kit/src/runtime/server/fetch.js index 2d74f65084ee..a54233705c80 100644 --- a/packages/kit/src/runtime/server/fetch.js +++ b/packages/kit/src/runtime/server/fetch.js @@ -9,7 +9,7 @@ import { fork_state_for_subrequest } from './state.js'; /** * @param {{ * event: import('@sveltejs/kit').RequestEvent; - * manifest: import('@sveltejs/kit').SSRManifest; + * manifest: import('types').SSRManifest; * state: import('types').RequestState; * get_cookie_header: (url: URL, header: string | null) => string; * set_internal: (name: string, value: string, opts: import('./page/types.js').Cookie['options']) => void; @@ -87,24 +87,24 @@ export function create_fetch({ event, manifest, state, get_cookie_header, set_in ).slice(1); const filename_html = `${filename}/index.html`; // path may also match path/index.html - const is_asset = manifest.assets.has(filename) || filename in manifest._.server_assets; + const is_asset = manifest.assets.has(filename) || filename in manifest.server_assets; const is_asset_html = - manifest.assets.has(filename_html) || filename_html in manifest._.server_assets; + manifest.assets.has(filename_html) || filename_html in manifest.server_assets; if (is_asset || is_asset_html) { const file = is_asset ? filename : filename_html; if (state.read) { const type = is_asset - ? manifest.mimeTypes[filename.slice(filename.lastIndexOf('.'))] + ? manifest.mime_types[filename.slice(filename.lastIndexOf('.'))] : 'text/html'; return new Response(state.read(file), { headers: type ? { 'content-type': type } : {} }); - } else if (read_implementation && file in manifest._.server_assets) { - const length = manifest._.server_assets[file]; - const type = manifest.mimeTypes[file.slice(file.lastIndexOf('.'))]; + } else if (read_implementation && file in manifest.server_assets) { + const length = manifest.server_assets[file]; + const type = manifest.mime_types[file.slice(file.lastIndexOf('.'))]; return new Response(read_implementation(file), { headers: { @@ -193,7 +193,7 @@ function normalize_fetch_input(info, init, url) { /** * @param {Request} request - * @param {import('@sveltejs/kit').SSRManifest} manifest + * @param {import('types').SSRManifest} manifest * @param {import('types').RequestState} state * @returns {Promise} */ diff --git a/packages/kit/src/runtime/server/index.js b/packages/kit/src/runtime/server/index.js index 0aa0213086f7..2274671925b2 100644 --- a/packages/kit/src/runtime/server/index.js +++ b/packages/kit/src/runtime/server/index.js @@ -58,10 +58,10 @@ if (DEV) { } export class Server { - /** @type {import('@sveltejs/kit').SSRManifest} */ + /** @type {import('types').SSRManifest} */ #manifest; - /** @param {import('@sveltejs/kit').SSRManifest} manifest */ + /** @param {import('types').SSRManifest} manifest */ constructor(manifest) { this.#manifest = manifest; diff --git a/packages/kit/src/runtime/server/internal.js b/packages/kit/src/runtime/server/internal.js index 2871b0d1e578..943fc4546efb 100644 --- a/packages/kit/src/runtime/server/internal.js +++ b/packages/kit/src/runtime/server/internal.js @@ -1,5 +1,4 @@ -/** @import { SSRManifest } from '@sveltejs/kit'; */ -/** @import { ServerHooks, SSROptions } from 'types'; */ +/** @import { ServerHooks, SSROptions, SSRManifest } from 'types'; */ import { restore, save } from './dev.js'; import { has_data_suffix, diff --git a/packages/kit/src/runtime/server/page/index.js b/packages/kit/src/runtime/server/page/index.js index 11e40e0eb553..245b6db0d107 100644 --- a/packages/kit/src/runtime/server/page/index.js +++ b/packages/kit/src/runtime/server/page/index.js @@ -1,5 +1,5 @@ -/** @import { RequestEvent, SSRManifest } from '@sveltejs/kit' */ -/** @import { PageNodeIndexes, RequestState, RequiredResolveOptions, ServerDataNode, SSRNode } from 'types' */ +/** @import { RequestEvent } from '@sveltejs/kit' */ +/** @import { PageNodeIndexes, RequestState, RequiredResolveOptions, ServerDataNode, SSRNode, SSRManifest } from 'types' */ import { text } from '@sveltejs/kit'; import { Redirect } from '@sveltejs/kit/internal'; import { compact } from '../../../utils/array.js'; @@ -46,7 +46,7 @@ export async function render_page(event, state, page, manifest, nodes, resolve_o } if (is_action_json_request(event)) { - const node = await manifest._.nodes[page.leaf](); + const node = await manifest.nodes[page.leaf](); return handle_action_json_request(event, state, node?.server); } @@ -276,7 +276,7 @@ export async function render_page(event, state, page, manifest, nodes, resolve_o const status = error.status; for (const { error: index, idx } of nearest_error_pages(i, branch, page.errors)) { - const node = await manifest._.nodes[index](); + const node = await manifest.nodes[index](); data_serializer.set_max_nodes(idx); @@ -378,6 +378,6 @@ function load_error_components(ssr, branch, page, manifest) { if (!ssr) return undefined; return build_error_chain(branch, page.errors, (idx) => - manifest._.nodes[idx]?.().then((e) => e.component?.()) + manifest.nodes[idx]?.().then((e) => e.component?.()) ); } diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index 481631c5b7d8..ca1b10c82098 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -36,7 +36,7 @@ import { options } from '../internal.js'; * @param {{ * branch: Array; * fetched: Array; - * manifest: import('@sveltejs/kit').SSRManifest; + * manifest: import('types').SSRManifest; * page_config: { ssr: boolean; csr: boolean }; * status: number; * error: App.Error | null; @@ -72,7 +72,7 @@ export async function render_response({ } } - const { client } = manifest._; + const client = manifest.client; const modulepreloads = new Set(client?.imports); const stylesheets = new Set(client?.stylesheets); diff --git a/packages/kit/src/runtime/server/page/respond_with_error.js b/packages/kit/src/runtime/server/page/respond_with_error.js index a5e4f3b06e79..0fec4fcf9ac2 100644 --- a/packages/kit/src/runtime/server/page/respond_with_error.js +++ b/packages/kit/src/runtime/server/page/respond_with_error.js @@ -14,7 +14,7 @@ import { server_data_serializer } from './data_serializer.js'; * @param {{ * event: import('@sveltejs/kit').RequestEvent; * state: import('types').RequestState; - * manifest: import('@sveltejs/kit').SSRManifest; + * manifest: import('types').SSRManifest; * error: unknown; * resolve_opts: import('types').RequiredResolveOptions; * }} opts @@ -30,7 +30,7 @@ export async function respond_with_error({ event, state, manifest, error, resolv const fetched = []; try { const branch = []; - const default_layout = await manifest._.nodes[0](); // 0 is always the root layout + const default_layout = await manifest.nodes[0](); // 0 is always the root layout const nodes = new PageNodes([default_layout]); const ssr = nodes.ssr(); const csr = nodes.csr(); @@ -71,7 +71,7 @@ export async function respond_with_error({ event, state, manifest, error, resolv data }, { - node: await manifest._.nodes[1](), // 1 is always the root error + node: await manifest.nodes[1](), // 1 is always the root error data: null, server_data: null } diff --git a/packages/kit/src/runtime/server/page/server_routing.js b/packages/kit/src/runtime/server/page/server_routing.js index aa69cad3939a..142f7c75ef29 100644 --- a/packages/kit/src/runtime/server/page/server_routing.js +++ b/packages/kit/src/runtime/server/page/server_routing.js @@ -1,4 +1,4 @@ -/** @import { SSRManifest } from '@sveltejs/kit' */ +/** @import { SSRManifest } from 'types' */ import { base, assets } from '#app/paths'; import { relative } from '$app/paths/internal/server'; import { text } from '@sveltejs/kit'; @@ -9,7 +9,7 @@ import { get_relative_path } from '../../utils.js'; /** * @param {import('types').SSRClientRoute} route * @param {URL} url - * @param {NonNullable} client + * @param {NonNullable} client * @returns {string} */ export function generate_route_object(route, url, client) { @@ -66,19 +66,19 @@ function create_client_import(import_path, url) { * @returns {Promise} */ export async function resolve_route(resolved_path, url, manifest) { - if (!manifest._.client?.routes) { + if (!manifest.client?.routes) { return text('Server-side route resolution disabled', { status: 400 }); } try { - const matchers = await manifest._.matchers(); - const result = find_route(resolved_path, manifest._.client.routes, matchers); + const matchers = await manifest.matchers(); + const result = find_route(resolved_path, manifest.client.routes, matchers); return create_server_routing_response( result?.route ?? null, result?.params ?? {}, url, - manifest._.client + manifest.client ).response; } catch { return text('Error resolving route', { status: 500 }); @@ -103,25 +103,25 @@ export async function resolve_route(resolved_path, url, manifest) { * @returns {Response} */ export function resolve_route_by_id(route_id, url, manifest) { - if (!manifest._.client?.routes) { + if (!manifest.client?.routes) { return text('Server-side route resolution disabled', { status: 400 }); } try { - const route = manifest._.client.routes.find((r) => r.id === route_id); + const route = manifest.client.routes.find((r) => r.id === route_id); if (route) { - return create_server_routing_response(route, null, url, manifest._.client).response; + return create_server_routing_response(route, null, url, manifest.client).response; } // `client.routes` only contains routes with a `+page`, so a miss above doesn't mean the // route doesn't exist — it might be a `+server.js`-only route. `_.routes` includes those // (with `page: null`), so we can distinguish "exists but has no code" from "unknown". - if (manifest._.routes.some((r) => r.id === route_id && !r.page)) { + if (manifest.routes.some((r) => r.id === route_id && !r.page)) { return text('export const endpoint_only = true;', { headers: js_headers() }); } - return create_server_routing_response(null, null, url, manifest._.client).response; + return create_server_routing_response(null, null, url, manifest.client).response; } catch { return text('Error resolving route', { status: 500 }); } @@ -137,7 +137,7 @@ function js_headers() { * @param {import('types').SSRClientRoute | null} route * @param {Partial> | null} params * @param {URL} url - * @param {NonNullable} client + * @param {NonNullable} client * @returns {{response: Response, body: string}} */ export function create_server_routing_response(route, params, url, client) { @@ -164,7 +164,7 @@ export function create_server_routing_response(route, params, url, client) { * * @param {import('types').SSRClientRoute} route * @param {URL} url - * @param {NonNullable} client + * @param {NonNullable} client * @returns {string} */ function create_css_import(route, url, client) { diff --git a/packages/kit/src/runtime/server/remote-functions.js b/packages/kit/src/runtime/server/remote-functions.js index f8ac78e2a543..2a37a0cdbe9e 100644 --- a/packages/kit/src/runtime/server/remote-functions.js +++ b/packages/kit/src/runtime/server/remote-functions.js @@ -1,6 +1,6 @@ -/** @import { RequestEvent, SSRManifest } from '@sveltejs/kit' */ +/** @import { RequestEvent } from '@sveltejs/kit' */ /** @import { RemoteForm } from '$app/server' */ -/** @import { RemoteFormInternals, RemoteFunctionData, RemoteFunctionResponse, RemoteInternals, RequestState, ServerActionResult } from 'types' */ +/** @import { RemoteFormInternals, RemoteFunctionData, RemoteFunctionResponse, RemoteInternals, RequestState, ServerActionResult, SSRManifest } from 'types' */ import { error } from '@sveltejs/kit'; import { Redirect, SvelteKitError } from '@sveltejs/kit/internal'; @@ -164,7 +164,7 @@ export async function handle_remote_call(event, state, manifest, id) { */ async function handle_remote_call_internal(event, state, manifest, id) { const [hash, name, additional_args] = id.split('/'); - const remotes = manifest._.remotes; + const remotes = manifest.remotes; if (!Object.hasOwn(remotes, hash)) error(404); @@ -540,7 +540,7 @@ async function handle_remote_form_post_internal(event, state, manifest, id) { // keyed (`form.for(key)`) instance can — rejoin the remaining segments const [hash, name, ...rest] = id.split('/'); const action_id = rest.join('/'); - const remotes = manifest._.remotes; + const remotes = manifest.remotes; const module = Object.hasOwn(remotes, hash) ? await remotes[hash]() : undefined; let form = /** @type {RemoteForm} */ ( diff --git a/packages/kit/src/runtime/server/respond.js b/packages/kit/src/runtime/server/respond.js index 0f479b1eaeef..cb340f7377c5 100644 --- a/packages/kit/src/runtime/server/respond.js +++ b/packages/kit/src/runtime/server/respond.js @@ -86,7 +86,7 @@ export const respond = propagate_context(internal_respond); /** * @param {Request} request - * @param {import('@sveltejs/kit').SSRManifest} manifest + * @param {import('types').SSRManifest} manifest * @param {import('types').RequestState} state * @returns {Promise} */ @@ -264,7 +264,7 @@ export async function internal_respond(request, manifest, state) { resolved_path = (await hooks.reroute({ url: new URL(url), fetch: event.fetch })) ?? url.pathname; - if (!manifest._.routes.length && resolved_path !== url.pathname) { + if (!manifest.routes.length && resolved_path !== url.pathname) { state.rerouted_url = denormalise_url({ request_url: request.url, resolved_path, @@ -366,8 +366,8 @@ export async function internal_respond(request, manifest, state) { if (!state.prerendering?.fallback && !skip_route_resolution) { try { - const matchers = await manifest._.matchers(); - const result = find_route(resolved_path, manifest._.routes, matchers); + const matchers = await manifest.matchers(); + const result = find_route(resolved_path, manifest.routes, matchers); if (result) { route = result.route; @@ -621,7 +621,7 @@ export async function internal_respond(request, manifest, state) { branch: [ // include the root layout because it applies to every page { - node: /** @type {SSRNode} */ (await manifest._.nodes[0]()), + node: /** @type {SSRNode} */ (await manifest.nodes[0]()), data: null, server_data: null } @@ -687,7 +687,7 @@ export async function internal_respond(request, manifest, state) { ); } else { const allowed_methods = new Set(allowed_page_methods); - const node = await manifest._.nodes[route.page.leaf](); + const node = await manifest.nodes[route.page.leaf](); if (node?.server?.actions) { allowed_methods.add('POST'); } @@ -824,13 +824,13 @@ export async function internal_respond(request, manifest, state) { /** * @param {import('types').PageNodeIndexes} page - * @param {import('@sveltejs/kit').SSRManifest} manifest + * @param {import('types').SSRManifest} manifest */ export function load_page_nodes(page, manifest) { return Promise.all([ // we use == here rather than === because [undefined] serializes as "[null]" - ...page.layouts.map((n) => (n == undefined ? n : manifest._.nodes[n]())), - manifest._.nodes[page.leaf]() + ...page.layouts.map((n) => (n == undefined ? n : manifest.nodes[n]())), + manifest.nodes[page.leaf]() ]); } diff --git a/packages/kit/src/runtime/server/utils.js b/packages/kit/src/runtime/server/utils.js index 371f0e7ec6da..0d17af895e8d 100644 --- a/packages/kit/src/runtime/server/utils.js +++ b/packages/kit/src/runtime/server/utils.js @@ -98,13 +98,13 @@ export function serialize_uses(node) { /** * Returns `true` if the given path was prerendered - * @param {import('@sveltejs/kit').SSRManifest} manifest + * @param {import('types').SSRManifest} manifest * @param {string} pathname Should include the base and be decoded */ export function has_prerendered_path(manifest, pathname) { return ( - manifest._.prerendered_routes.has(pathname) || - (pathname.at(-1) === '/' && manifest._.prerendered_routes.has(pathname.slice(0, -1))) + manifest.prerendered_routes.has(pathname) || + (pathname.at(-1) === '/' && manifest.prerendered_routes.has(pathname.slice(0, -1))) ); } diff --git a/packages/kit/src/types/ambient-private.d.ts b/packages/kit/src/types/ambient-private.d.ts index 2aebbe2fab9a..74ffbddbebf3 100644 --- a/packages/kit/src/types/ambient-private.d.ts +++ b/packages/kit/src/types/ambient-private.d.ts @@ -1,7 +1,6 @@ /** Internal version of $app/server */ declare module '/server.js' { - import { SSRManifest } from '@sveltejs/kit'; - import { SSROptions, ServerHooks } from 'types'; + import { SSROptions, ServerHooks, SSRManifest } from 'types'; export const options: SSROptions; export const get_hooks: () => Promise>; diff --git a/packages/kit/src/types/internal.d.ts b/packages/kit/src/types/internal.d.ts index cdcfa5000b93..fa707c7f45d4 100644 --- a/packages/kit/src/types/internal.d.ts +++ b/packages/kit/src/types/internal.d.ts @@ -7,12 +7,12 @@ import { ServerInitOptions, Actions, RequestEvent, - SSRManifest, Emulator, HttpError } from '@sveltejs/kit'; import { RemoteFormIssue, RemoteQuery, RemoteLiveQuery } from '$app/server'; import { Config } from '@sveltejs/kit/vite'; +import { ParamMatcher } from '@sveltejs/kit/params'; import { ClientInit, Handle, @@ -457,6 +457,29 @@ export interface ServerNode { entries?: PrerenderEntryGenerator; } +/** + * Information required to instantiate a new `Server` instance. + */ +export interface SSRManifest { + /** The directory where SvelteKit keeps its stuff, including static assets (such as JS and CSS) and internally-used routes. */ + app_dir: string; + /** The `base` and `appDir` settings combined without a leading slash. */ + app_path: string; + /** Static files from `config.files.assets` and the service worker (if any). */ + assets: Set; + /** Map of file extensions to MIME types */ + mime_types: Record; + client: BuildData['client']; + nodes: SSRNodeLoader[]; + /** hashed filename -> import to that file */ + remotes: Record Promise<{ default: Record }>>; + routes: SSRRoute[]; + prerendered_routes: Set; + matchers: () => Promise>; + /** A `[file]: size` map of all assets imported by server code. */ + server_assets: Record; +} + export interface SSRNode { /** index into the `nodes` array in the generated `client/app.js`. */ index: number; diff --git a/packages/kit/test/apps/options/unit-test/vite.spec.js b/packages/kit/test/apps/options/unit-test/vite.spec.js index 5540dd897461..c2ac3c3efcdf 100644 --- a/packages/kit/test/apps/options/unit-test/vite.spec.js +++ b/packages/kit/test/apps/options/unit-test/vite.spec.js @@ -6,7 +6,7 @@ const timeout = 60_000; const cwd = path.resolve(import.meta.dirname, '..'); -test('no overridden options warning', () => { +test('no overridden options warning', { timeout }, () => { const result = spawnSync( 'pnpm', ['vitest', 'run', '--config', './vite.custom.config.js', '-t', 'noop'], diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index 4d5f2ee95351..087ba98dd990 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -5,7 +5,10 @@ declare module '@sveltejs/kit' { import type { Plugin } from 'vite'; import type { RouteId as AppRouteId, LayoutParams as AppLayoutParams } from '$app/types'; import type { StandardSchemaV1 } from '@standard-schema/spec'; + import type { getRequest, setResponse } from '@sveltejs/kit/node'; + import type { Component } from 'svelte'; import type { Config } from '@sveltejs/kit/vite'; + import type { ParamMatcher } from '@sveltejs/kit/params'; // @ts-ignore this is an optional peer dependency so could be missing. Written like this so dts-buddy preserves the ts-ignore type Span = import('@opentelemetry/api').Span; @@ -38,6 +41,20 @@ declare module '@sveltejs/kit' { */ instrumentation?: () => boolean; }; + /** + * This function overrides the default behavior during dev and preview to + * convert an `http.IncomingMessage` to a `Request` object. + * To call the original setRequest function, import it from `@sveltejs/kit/node`. + * @since 3.0.0 + */ + getRequest?: typeof getRequest; + /** + * This function overrides the default behavior during dev and preview to + * write a `Response` object to an `http.ServerResponse`. + * To call the original setResponse function, import it from `@sveltejs/kit/node`. + * @since 3.0.0 + */ + setResponse?: typeof setResponse; /** * Creates an `Emulator`, which allows the adapter to influence the environment * during dev, build and prerendering. @@ -125,6 +142,10 @@ declare module '@sveltejs/kit' { prerendered: Prerendered; /** An array of all routes (including prerendered) */ routes: RouteDefinition[]; + /** The value of the `$app/manifest` module */ + manifest: typeof import('$app/manifest'); + /** A record of file extensions to MIME types */ + mimeTypes: Record; /** * Create separate functions that map to one or more routes of your app. @@ -151,8 +172,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.writeServerEntrypoint` 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 +188,18 @@ declare module '@sveltejs/kit' { /** Get the application path including any configured `base` path, e.g. `my-base-path/_app`. */ getAppPath: () => string; + /** + * Writes a javascript file that initialises the SvelteKit [server](https://svelte.dev/docs/kit/@sveltejs-kit#Server). + * @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()`. + */ + writeServerEntrypoint: ( + dest: string, + opts?: { + routes?: RouteDefinition[]; + serverDirectory?: string; + } + ) => void; /** * Write client assets to `dest`. * @param dest the destination folder @@ -653,19 +687,6 @@ declare module '@sveltejs/kit' { read?: (file: string) => MaybePromise; } - /** - * Information required to instantiate a new `Server` instance. - */ - export interface SSRManifest { - /** The directory where SvelteKit keeps its stuff, including static assets (such as JS and CSS) and internally-used routes. */ - appDir: string; - /** The `base` and `appDir` settings combined without a leading slash. */ - appPath: string; - /** Static files from `config.files.assets` and the service worker (if any). */ - assets: Set; - mimeTypes: Record; - } - /** * The generic form of `PageServerLoad` and `LayoutServerLoad`. You should import those from `./$types` (see [generated types](https://svelte.dev/docs/kit/types#Generated-types)) * rather than using `ServerLoad` directly. @@ -896,6 +917,97 @@ declare module '@sveltejs/kit' { dynamic: boolean; rest: boolean; } + + /** @default 'never' */ + type TrailingSlash = 'never' | 'always' | 'ignore'; + interface Asset { + file: string; + size: number; + type: string | null; + } + + interface FontDependency { + /** emitted file path, relative to the client output directory */ + file: string; + /** the source file path relative to the project root, before hashing and character sanitization */ + filename: string; + } + + interface BuildData { + app_dir: string; + app_path: string; + manifest_data: ManifestData; + out_dir: string; + service_worker: string | null; + client: { + /** Path to the client entry point. */ + start: string; + /** Path to the generated `app.js` file that contains the client manifest. Only set in case of `bundleStrategy === 'split'`. */ + app?: string; + /** JS files that the client entry point relies on. */ + imports: string[]; + /** + * JS files that represent the entry points of the layouts/pages. + * An entry is undefined if the layout/page has no component or universal file (i.e. only has a `.server.js` file). + * Only set in case of `router.resolution === 'server'`. + */ + nodes?: Array; + /** + * CSS files referenced in the entry points of the layouts/pages. + * An entry is undefined if the layout/page has no component or universal file (i.e. only has a `.server.js` file) or if has no CSS. + * Only set in case of `router.resolution === 'server'`. + */ + css?: Array; + /** + * Contains the client route manifest in a form suitable for the server which is used for server-side route resolution. + * Notably, it contains all routes, regardless of whether they are prerendered or not (those are missing in the optimized server route manifest). + * Only set in case of `router.resolution === 'server'`. + */ + routes?: SSRClientRoute[]; + stylesheets: string[]; + fonts: FontDependency[]; + /** + * Whether the client uses public dynamic env vars — `$env/dynamic/public` or `$app/env/public`. + */ + uses_env_dynamic_public: boolean; + /** Only set in case of `bundleStrategy === 'inline'`. */ + inline?: { + script: string; + style: string | undefined; + }; + } | null; + server_manifest: import('vite').Manifest; + } + + interface ManifestData { + /** Static files from `config.files.assets`. */ + assets: Asset[]; + hooks: { + client: string | null; + server: string | null; + universal: string | null; + }; + nodes: PageNode[]; + routes: RouteData[]; + params: string | null; + } + + interface PageNode { + depth: number; + /** The `+page/layout.svelte`. */ + component?: string; // TODO supply default component if it's missing (bit of an edge case) + /** The `+page/layout.js/.ts`. */ + universal?: string; + /** The `+page/layout.server.js/ts`. */ + server?: string; + parent_id?: string; + parent?: PageNode; + /** Filled with the pages that reference this layout (if this is a layout). */ + child_pages?: PageNode[]; + /** The final page options for a node if it was statically analysable */ + page_options?: PageOptions | null; + } + type RecursiveRequired = { // Recursive implementation of TypeScript's Required utility type. // Will recursively continue until it reaches a primitive or Function @@ -905,6 +1017,158 @@ declare module '@sveltejs/kit' { : T[K]; // Use the exact type for everything else }; + interface RouteParam { + name: string; + matcher: string; + optional: boolean; + rest: boolean; + chained: boolean; + } + + /** + * Represents a route segment in the app. It can either be an intermediate node + * with only layout/error pages, or a leaf, at which point either `page` and `leaf` + * or `endpoint` is set. + */ + interface RouteData { + id: string; + parent: RouteData | null; + + segment: string; + pattern: RegExp; + params: RouteParam[]; + + layout: PageNode | null; + error: PageNode | null; + leaf: PageNode | null; + + page: { + layouts: Array; + errors: Array; + leaf: number; + } | null; + + endpoint: { + file: string; + /** The final page options for the endpoint if it was statically analysable */ + page_options: PageOptions | null; + } | null; + } + + type SSRComponentLoader = () => Promise; + + interface UniversalNode { + /** Is `null` in case static analysis succeeds but the node is ssr=false */ + load?: Load; + prerender?: PrerenderOption; + ssr?: boolean; + csr?: boolean; + trailingSlash?: TrailingSlash; + config?: Record; + entries?: PrerenderEntryGenerator; + } + + interface ServerNode { + load?: ServerLoad; + prerender?: PrerenderOption; + ssr?: boolean; + csr?: boolean; + trailingSlash?: TrailingSlash; + actions?: Actions; + config?: Record; + entries?: PrerenderEntryGenerator; + } + + /** + * Information required to instantiate a new `Server` instance. + */ + interface SSRManifest { + /** The directory where SvelteKit keeps its stuff, including static assets (such as JS and CSS) and internally-used routes. */ + app_dir: string; + /** The `base` and `appDir` settings combined without a leading slash. */ + app_path: string; + /** Static files from `config.files.assets` and the service worker (if any). */ + assets: Set; + /** Map of file extensions to MIME types */ + mime_types: Record; + client: BuildData['client']; + nodes: SSRNodeLoader[]; + /** hashed filename -> import to that file */ + remotes: Record Promise<{ default: Record }>>; + routes: SSRRoute[]; + prerendered_routes: Set; + matchers: () => Promise>; + /** A `[file]: size` map of all assets imported by server code. */ + server_assets: Record; + } + + interface SSRNode { + /** index into the `nodes` array in the generated `client/app.js`. */ + index: number; + /** external JS files that are loaded on the client. `imports[0]` is the entry point (e.g. `client/nodes/0.js`) */ + imports: string[]; + /** external CSS files that are loaded on the client */ + stylesheets: string[]; + /** external font files that are loaded on the client */ + fonts: FontDependency[]; + + universal_id?: string; + server_id?: string; + + /** + * During development, all styles are inlined for the page to avoid FOUC. + * But in production, this stores styles that are below the inline threshold. + * It returns a Promise during development because Vite needs to load the + * modules on demand. But in production, the contents have been precomputed + * during the build, so it can return synchronously. + */ + inline_styles?(): MaybePromise< + Record string)> + >; + /** Svelte component */ + component?: SSRComponentLoader; + /** +page.js or +layout.js */ + universal?: UniversalNode; + /** +page.server.js, +layout.server.js, or +server.js */ + server?: ServerNode; + } + + type SSRNodeLoader = () => Promise; + + interface PageNodeIndexes { + errors: Array; + layouts: Array; + leaf: number; + } + + type PrerenderEntryGenerator = () => MaybePromise>>; + + type SSREndpoint = Partial> & { + prerender?: PrerenderOption; + trailingSlash?: TrailingSlash; + config?: Record; + entries?: PrerenderEntryGenerator; + fallback?: RequestHandler; + }; + + interface SSRRoute { + id: string; + pattern: RegExp; + params: RouteParam[]; + page: PageNodeIndexes | null; + endpoint: (() => Promise) | null; + endpoint_id?: string; + } + + interface SSRClientRoute { + id: string; + pattern: RegExp; + params: RouteParam[]; + errors: Array; + layouts: Array<[has_server_load: boolean, node_id: number] | undefined>; + leaf: [has_server_load: boolean, node_id: number]; + } + type ValidatedConfig = RecursiveRequired> & { preprocess: Config['preprocess']; }; @@ -1060,7 +1324,19 @@ declare module '@sveltejs/kit' { wasNormalized: boolean; denormalize: (url?: string | URL) => URL; }; + type ValidPageOption = (typeof valid_page_options_array)[number]; + + type PageOptions = Partial<{ + [K in ValidPageOption]: K extends 'ssr' | 'csr' + ? boolean + : K extends 'prerender' + ? PrerenderOption + : K extends 'trailingSlash' + ? TrailingSlash + : any; + }>; export const VERSION: string; + const valid_page_options_array: readonly ["ssr", "prerender", "csr", "trailingSlash", "config", "entries", "load"]; export {}; }