diff --git a/.changeset/rich-wombats-flash.md b/.changeset/rich-wombats-flash.md new file mode 100644 index 000000000000..d30acaa595a0 --- /dev/null +++ b/.changeset/rich-wombats-flash.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: remove `adapter.emulate` 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 204445e5e889..e941118305c2 100644 --- a/documentation/docs/25-build-and-deploy/99-writing-adapters.md +++ b/documentation/docs/25-build-and-deploy/99-writing-adapters.md @@ -21,14 +21,6 @@ export default function (options) { async adapt(builder) { // adapter implementation }, - async emulate() { - return { - async platform({ config, prerender }) { - // the returned object becomes `event.platform` during dev, build and - // preview. Its shape is that of `App.Platform` - } - } - }, supports: { read: ({ config, route }) => { // Return `true` if the route with the given `config` can use `read` @@ -51,7 +43,7 @@ export default function (options) { } ``` -Of these, `name` and `adapt` are required. `emulate`, `vite.plugins` and `supports` are optional. +Of these, `name` and `adapt` are required. `vite.plugins` and `supports` are optional. Within the `adapt` method, there are a number of things that an adapter should do: @@ -66,3 +58,44 @@ Within the `adapt` method, there are a number of things that an adapter should d - Put the user's static files and the generated JS/CSS in the correct location for the target platform Where possible, we recommend putting the adapter output under the `build/` directory with any intermediate output placed under `.svelte-kit/[adapter-name]`. + +## Configuring the development and preview experience + +By default, SvelteKit runs your server code through a Node.js runtime when running `vite dev` and `vite preview`. You can change this behaviour by adding a Vite plugin that has a `configureServer` and `configurePreviewServer` hook to route requests to [a different runtime](https://vite.dev/guide/api-environment-runtimes). + +The main Vite server environment SvelteKit uses is named `ssr`. You can change its settings by referencing it in the `config` hook of a Vite plugin. + +```js +// @errors: 2304 1005 1109 +config(userConfig) { + userConfig.environments.ssr = { ... } +} +``` + +You can also create your own server entry file by importing the `Server` class from `sveltekit:server`, the environment variables loaded by Vite through `env` from `sveltekit:env`, and your app-specific information as `manifest` from `sveltekit:server-manifest`. + +```js +import { env } from 'sveltekit:env'; +import { Server } from 'sveltekit:server'; +import { manifest } from 'sveltekit:server-manifest'; + +const server = new Server(manifest); + +await server.init({ env }); + +export default { + /** + * @param {Request} request + * @returns {Promise} + */ + async fetch(request) { + return await server.respond(request, { + getClientAddress: () => { + return request.headers.get('how-your-platform-exposes-the-remote-address') + } + }); + } +} + +import.meta.hot?.accept(); +``` diff --git a/packages/adapter-cloudflare/index.js b/packages/adapter-cloudflare/index.js index c39f57e61521..aab1eaf1b2e8 100644 --- a/packages/adapter-cloudflare/index.js +++ b/packages/adapter-cloudflare/index.js @@ -2,7 +2,7 @@ import { copyFileSync, existsSync, readFileSync, writeFileSync } from 'node:fs'; import path from 'node:path'; import process from 'node:process'; import { fileURLToPath } from 'node:url'; -import { getPlatformProxy, unstable_readConfig } from 'wrangler'; +import { unstable_readConfig } from 'wrangler'; import { is_building_for_cloudflare_pages, validate_worker_settings, @@ -178,40 +178,6 @@ export default function (options = {}) { writeFileSync(`${dest}/.assetsignore`, generate_assetsignore(), { flag: 'a' }); } }, - emulate() { - // we want to invoke `getPlatformProxy` only once, but await it only when it is accessed. - // If we would await it here, it would hang indefinitely because the platform proxy only resolves once a request happens - const get_emulated = async () => { - const proxy = await getPlatformProxy(options.platformProxy); - const platform = { - env: proxy.env, - ctx: proxy.ctx, - context: proxy.ctx, // deprecated in favor of ctx - caches: proxy.caches, - cf: proxy.cf - }; - /** @type {Record} */ - const env = {}; - const prerender_platform = /** @type {App.Platform} */ (/** @type {unknown} */ ({ env })); - for (const key in proxy.env) { - Object.defineProperty(env, key, { - get: () => { - throw new Error(`Cannot access platform.env.${key} in a prerenderable route`); - } - }); - } - return { platform, prerender_platform }; - }; - - let emulated; - - return { - platform: async ({ prerender }) => { - emulated ??= await get_emulated(); - return prerender ? emulated.prerender_platform : emulated.platform; - } - }; - }, supports: { read: () => true, instrumentation: () => true diff --git a/packages/adapter-cloudflare/test/apps/workers/test/test.js b/packages/adapter-cloudflare/test/apps/workers/test/test.js index 44832c70890f..4c6f2a79a28d 100644 --- a/packages/adapter-cloudflare/test/apps/workers/test/test.js +++ b/packages/adapter-cloudflare/test/apps/workers/test/test.js @@ -7,7 +7,8 @@ test('worker', async ({ page }) => { await expect(page.locator('h1')).toContainText('Sum: 3'); }); -test('ctx', async ({ request }) => { +// TODO: re-enable once we add the vite cloudflare plugin +test.skip('ctx', async ({ request }) => { const res = await request.get('/ctx'); expect(await res.text()).toBe('ctx works'); }); diff --git a/packages/adapter-cloudflare/test/utils.js b/packages/adapter-cloudflare/test/utils.js index dcb3ce52c9d6..59a1ec1b5ea1 100644 --- a/packages/adapter-cloudflare/test/utils.js +++ b/packages/adapter-cloudflare/test/utils.js @@ -11,7 +11,7 @@ export const config = { command: process.env.DEV ? 'pnpm dev' : 'pnpm build && pnpm preview', port: process.env.DEV ? 5173 : 8787 }, - retries: process.env.CI ? 2 : number_from_env('KIT_E2E_RETRIES', 0), + retries: process.env.CI ? 0 : number_from_env('KIT_E2E_RETRIES', 0), projects: [ { name: 'chromium' diff --git a/packages/kit/package.json b/packages/kit/package.json index 964f85bf42c8..3f144baea4a2 100644 --- a/packages/kit/package.json +++ b/packages/kit/package.json @@ -128,6 +128,9 @@ "types": "./types/index.d.ts", "import": "./src/exports/internal/server/index.js" }, + "./internal/devalue": { + "import": "./src/exports/internal/devalue.js" + }, "./node": { "types": "./types/index.d.ts", "import": "./src/exports/node/index.js" diff --git a/packages/kit/src/core/generate_manifest/index.js b/packages/kit/src/core/generate_manifest/index.js index b83aa8fef7e4..ad9233e65527 100644 --- a/packages/kit/src/core/generate_manifest/index.js +++ b/packages/kit/src/core/generate_manifest/index.js @@ -102,6 +102,7 @@ export function generate_manifest({ appDir: ${s(build_data.app_dir)}, appPath: ${s(build_data.app_path)}, assets: new Set(${s(assets)}), + base: ${s(build_data.base)}, mimeTypes: ${s(mime_types)}, _: { client: ${uneval(build_data.client)}, diff --git a/packages/kit/src/core/postbuild/analyse.js b/packages/kit/src/core/postbuild/analyse.js index 46e099222ac6..c6410df47ea9 100644 --- a/packages/kit/src/core/postbuild/analyse.js +++ b/packages/kit/src/core/postbuild/analyse.js @@ -6,7 +6,6 @@ import { extract_svelte_config, load_vite_config } from '../config/index.js'; import { forked } from '../../utils/fork.js'; import { ENDPOINT_METHODS } from '../../constants.js'; import { has_server_load, resolve_route } from '../../utils/routing.js'; -import { check_feature } from '../../utils/features.js'; import { createReadableStream } from '@sveltejs/kit/node'; import { PageNodes } from '../../utils/page_nodes.js'; @@ -116,13 +115,8 @@ async function analyse({ const prerender = page?.prerender ?? endpoint?.prerender; if (prerender !== true) { - for (const feature of list_features( - route, - manifest_data, - server_manifest, - tracked_features - )) { - check_feature(route.id, route_config, feature, config.adapter); + for (const _ of list_features(route, manifest_data, server_manifest, tracked_features)) { + // TODO: check feature while inside the environment } } diff --git a/packages/kit/src/core/postbuild/prerender.js b/packages/kit/src/core/postbuild/prerender.js index 491702c376af..ae9ab53c904a 100644 --- a/packages/kit/src/core/postbuild/prerender.js +++ b/packages/kit/src/core/postbuild/prerender.js @@ -164,8 +164,6 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, vit return { prerendered, prerender_map }; } - const emulator = await config.adapter?.emulate?.(); - /** @type {import('types').Logger} */ const log = logger({ verbose }); @@ -321,8 +319,7 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, vit // stuff in `static` return readFileSync(join(config.files.assets, file)); - }, - emulator + } }); const encoded_id = response.headers.get('x-sveltekit-routeid'); diff --git a/packages/kit/src/core/utils.js b/packages/kit/src/core/utils.js index fcda4f7ef72f..ecbaf09eef48 100644 --- a/packages/kit/src/core/utils.js +++ b/packages/kit/src/core/utils.js @@ -1,3 +1,4 @@ +/** @import { ViteDevServer } from 'vite' */ import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -112,3 +113,14 @@ export function list_files(dir, filter) { return files; } + +/** + * Gets the listening address of a Vite dev server. Otherwise, it throws an error + * @param {ViteDevServer} server + * @returns {string} + */ +export function get_address(server) { + const address = server.resolvedUrls?.local?.[0]; + if (!address) throw new Error('Failed to determine Vite dev server address'); + return address; +} diff --git a/packages/kit/src/exports/internal/devalue.js b/packages/kit/src/exports/internal/devalue.js new file mode 100644 index 000000000000..82c359405785 --- /dev/null +++ b/packages/kit/src/exports/internal/devalue.js @@ -0,0 +1,3 @@ +// reexporting this allows us to use devalue from generated modules since importing +// devalue directly doesn't work unless the user has it in their package.json +export * from 'devalue'; diff --git a/packages/kit/src/exports/public.d.ts b/packages/kit/src/exports/public.d.ts index fcd0eb51ef96..8ae4c86486b1 100644 --- a/packages/kit/src/exports/public.d.ts +++ b/packages/kit/src/exports/public.d.ts @@ -66,11 +66,6 @@ export interface Adapter { */ instrumentation?: () => boolean; }; - /** - * Creates an `Emulator`, which allows the adapter to influence the environment - * during dev, build and prerendering. - */ - emulate?: () => MaybePromise; vite?: { /** * Plugins provided by the adapter are placed before any of SvelteKit's own plugins. @@ -345,17 +340,6 @@ export interface Cookies { serialize: (name: string, value: string, opts: import('cookie').SerializeOptions) => string; } -/** - * A collection of functions that influence the environment during dev, build and prerendering - */ -export interface Emulator { - /** - * A function that is called with the current route `config` and `prerender` option - * and returns an `App.Platform` object - */ - platform?(details: { config: any; prerender: PrerenderOption }): MaybePromise; -} - export interface KitConfig { /** * Your [adapter](https://svelte.dev/docs/kit/adapters) is run when executing `vite build`. It determines how the output is converted for different platforms. @@ -1775,6 +1759,7 @@ export interface SSRManifest { appPath: string; /** Static files from `config.files.assets` and the service worker (if any). */ assets: Set; + base: string; mimeTypes: Record; /** @internal private fields */ diff --git a/packages/kit/src/exports/vite/dev/index.js b/packages/kit/src/exports/vite/dev/index.js index b6d340c45cb9..c81d21e04539 100644 --- a/packages/kit/src/exports/vite/dev/index.js +++ b/packages/kit/src/exports/vite/dev/index.js @@ -1,142 +1,59 @@ -/** @import { RequestEvent } from '@sveltejs/kit' */ -/** @import { PrerenderOption, UniversalNode } from 'types' */ -import process from 'node:process'; +/** @import { Adapter } from '@sveltejs/kit' */ +/** @import { DevContext, ManifestData, ValidatedConfig } from 'types' */ +/** @import { EnvironmentModuleNode, ModuleNode, ResolvedConfig, ViteDevServer } from 'vite' */ import fs from 'node:fs'; import path from 'node:path'; import { URL } from 'node:url'; -import { AsyncLocalStorage } from 'node:async_hooks'; import { styleText } from 'node:util'; import sirv from 'sirv'; -import { isCSSRequest, loadEnv, buildErrorMessage } from 'vite'; -import { createReadableStream, getRequest, setResponse } from '../../../exports/node/index.js'; +import { getRequest, setResponse } from '../../../exports/node/index.js'; import { coalesce_to_error } from '../../../utils/error.js'; import { resolve_entry } from '../../../utils/filesystem.js'; -import { load_and_validate_params } from '../../../utils/params.js'; -import { from_fs, to_fs } from '../../../utils/vite.js'; +import { to_fs } from '../../../utils/vite.js'; import { posixify } from '../../../utils/os.js'; import { load_error_page } from '../../../core/config/index.js'; -import { SRC_ROOT, SVELTE_KIT_ASSETS } from '../../../constants.js'; +import { SVELTE_KIT_ASSETS } from '../../../constants.js'; import * as sync from '../../../core/sync/sync.js'; -import { get_mime_lookup, get_runtime_base } from '../../../core/utils.js'; import '../../../utils/mime.js'; // extend mrmime with additional types (affects sirv too) -import { compact } from '../../../utils/array.js'; import { is_chrome_devtools_request, log_response, not_found } from '../utils.js'; -import { SCHEME } from '../../../utils/url.js'; -import { check_feature } from '../../../utils/features.js'; -import { escape_html } from '../../../utils/escape.js'; +import { escape_for_regexp, escape_html } from '../../../utils/escape.js'; +import { sveltekit_ipc, sveltekit_manifest_data } from '../module_ids.js'; // vite-specifc queries that we should skip handling for css urls const vite_css_query_regex = /(?:\?|&)(?:raw|url|inline)(?:&|$)/; /** - * @param {import('vite').ViteDevServer} vite - * @param {import('vite').ResolvedConfig} vite_config - * @param {import('types').ValidatedConfig} svelte_config - * @param {() => Array<{ hash: string, file: string }>} get_remotes + * @param {ViteDevServer} server + * @param {ResolvedConfig} vite_config + * @param {typeof import('vite')} vite + * @param {ValidatedConfig} svelte_config * @param {string} root The project root directory - * @return {Promise void>>} + * @param {DevContext} dev_context + * @return {() => void} */ -export async function dev(vite, vite_config, svelte_config, get_remotes, root) { - /** @type {AsyncLocalStorage<{ event: RequestEvent, config: any, prerender: PrerenderOption }>} */ - const async_local_storage = new AsyncLocalStorage(); - - globalThis.__SVELTEKIT_TRACK__ = (label) => { - const context = async_local_storage.getStore(); - if (!context || context.prerender === true) return; - - check_feature( - /** @type {string} */ (context.event.route.id), - context.config, - label, - svelte_config.kit.adapter - ); - }; - - const fetch = globalThis.fetch; - globalThis.fetch = (info, init) => { - if (typeof info === 'string' && !SCHEME.test(info)) { - throw new Error( - `Cannot use relative URL (${info}) with global fetch — use \`event.fetch\` instead: https://svelte.dev/docs/kit/web-standards#fetch-apis` - ); - } - - return fetch(info, init); - }; - +export function dev(server, vite_config, vite, svelte_config, root, dev_context) { sync.init(svelte_config, root); - /** @type {import('types').ManifestData} */ + /** @type {ManifestData} */ let manifest_data; - /** @type {import('@sveltejs/kit').SSRManifest} */ - let manifest; /** @type {Error | null} */ let manifest_error = null; - /** @param {string} url */ - async function loud_ssr_load_module(url) { - try { - return await vite.ssrLoadModule(url, { fixStacktrace: true }); - } catch (/** @type {any} */ err) { - const msg = buildErrorMessage(err, [ - styleText('red', `Internal server error: ${err.message}`) - ]); - - if (!vite.config.logger.hasErrorLogged(err)) { - vite.config.logger.error(msg, { error: err }); - } - - // TODO this is inadequate — it doesn't reliably show the overlay on every page load, - // and when it does appear it may immediately vanish. `vite.ws.send` broadcasts - // to all connected clients, even ones that are unaffected by the error. - // we need a more considered approach - vite.ws.send({ - type: 'error', - err: /** @type {import('vite').ErrorPayload['err']} */ ({ - ...err, - // these properties are non-enumerable and will - // not be serialized unless we explicitly include them - message: err.message, - stack: err.stack ?? '' - }) - }); - - throw err; - } - } - - /** @param {string} id */ - async function resolve(id) { - const url = id.startsWith('..') ? to_fs(path.resolve(id)) : `/${id}`; - - const module = await loud_ssr_load_module(url); - - const module_node = await vite.moduleGraph.getModuleByUrl(url); - if (!module_node) throw new Error(`Could not find node for ${url}`); - - return { module, module_node, url }; - } - - async function update_manifest() { + function update_manifest() { try { ({ manifest_data } = sync.create(svelte_config, root)); - - await load_and_validate_params({ - routes: manifest_data.routes, - params_path: manifest_data.params, - root, - load: (file) => loud_ssr_load_module(file) - }); + dev_context.manifest_data = manifest_data; if (manifest_error) { manifest_error = null; - vite.ws.send({ type: 'full-reload' }); + server.hot.send({ type: 'full-reload' }); } } catch (error) { manifest_error = /** @type {Error} */ (error); console.error(styleText(['bold', 'red'], manifest_error.message)); - vite.ws.send({ + server.hot.send({ type: 'error', err: { message: manifest_error.message ?? 'Invalid routes', @@ -147,229 +64,10 @@ export async function dev(vite, vite_config, svelte_config, get_remotes, root) { return; } - manifest = { - appDir: svelte_config.kit.appDir, - appPath: svelte_config.kit.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.kit.outDir)}/generated/client/app.js`, - imports: [], - stylesheets: [], - fonts: [], - uses_env_dynamic_public: true, - nodes: - svelte_config.kit.router.resolution === 'client' - ? undefined - : manifest_data.nodes.map((node, i) => { - if (node.component || node.universal) { - return `${svelte_config.kit.paths.base}${to_fs(svelte_config.kit.outDir)}/generated/client/nodes/${i}.js`; - } - }), - // `css` is not necessary in dev, as the JS file from `nodes` will reference the CSS file - routes: - svelte_config.kit.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 () => { - /** @type {import('types').SSRNode} */ - const result = {}; - 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 {import('vite').ModuleNode[]} */ - 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.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(); - - for (const module_node of module_nodes) { - await find_deps(vite, module_node, deps); - } - - /** @type {Record} */ - const styles = {}; - - for (const dep of deps) { - if (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 vite.ssrLoadModule(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; - }; - }), - prerendered_routes: new Set(), - get remotes() { - return Object.fromEntries( - get_remotes().map((remote) => [ - remote.hash, - () => vite.ssrLoadModule(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 vite.ssrLoadModule(url, { fixStacktrace: true }); - - if (!module.params) { - throw new Error( - `${manifest_data.params} does not export \`params\` from \`defineParams\`` - ); - } - - return module.params; - } - } - }; + invalidate_module(server, sveltekit_manifest_data); } - /** @param {Error} error */ - function fix_stack_trace(error) { - if (!error.stack) { - return; - } - - try { - vite.ssrFixStacktrace(error); - } catch { - // ssrFixStacktrace can fail on StackBlitz web containers and we don't know why - // by ignoring it the line numbers are wrong, but at least we can show the error - } - - if (error.stack) { - let end = 0; - - error.stack = error.stack - .replaceAll('\0', '') // remove null bytes from e.g. virtual module IDs, or the response will fail - .split('\n') - .map((line, i) => { - const match = /^ {4}at (?:[^ ]+ \((.+)\)|(.+))$/.exec(line); - if (!match) { - end = i + 1; - return line; - } - - const loc = match[1] ?? match[2]; - const file = loc.replace(/:\d+:\d+$/, ''); - - if (fs.existsSync(file)) { - if (!file.includes('node_modules') && !file.includes(SRC_ROOT)) { - end = i + 1; - } - - return line.replace(file, path.posix.relative(process.cwd(), file)); - } - - return line; - }) - .slice(0, end) - .join('\n'); - - return error.stack; - } - } - - await update_manifest(); + update_manifest(); const params_file = resolve_entry(svelte_config.kit.files.params); @@ -378,7 +76,7 @@ export async function dev(vite, vite_config, svelte_config, get_remotes, root) { * @param {(file: string) => void} cb */ const watch = (event, cb) => { - vite.watcher.on(event, (file) => { + server.watcher.on(event, (file) => { if ( file.startsWith(svelte_config.kit.files.routes + path.sep) || (params_file && file === params_file) || @@ -412,6 +110,16 @@ export async function dev(vite, vite_config, svelte_config, get_remotes, root) { // Unless it's a file where the trailing slash page option might have changed if (timeout || !/\+(page|layout|server).*$/.test(file)) return; sync.update(svelte_config, manifest_data, file, root); + + const nodes_page_options = manifest_data.nodes.map((node) => node.page_options); + const endpoints_page_options = manifest_data.routes.map( + (route) => route.endpoint?.page_options + ); + server.environments.ssr.hot.send('sveltekit:manifest-data', { + nodes_page_options, + endpoints_page_options + }); + invalidate_module(server, sveltekit_manifest_data); }); const { appTemplate, errorTemplate, serviceWorker, hooks } = svelte_config.kit.files; @@ -420,14 +128,14 @@ export async function dev(vite, vite_config, svelte_config, get_remotes, root) { // kit defaults to src/app.html, so unless user changed that to index.html // send the vite client a full-reload event without path being set if (appTemplate !== 'index.html') { - vite.watcher.on('change', (file) => { + server.watcher.on('change', (file) => { if (file === appTemplate) { - vite.ws.send({ type: 'full-reload' }); + server.ws.send({ type: 'full-reload' }); } }); } - vite.watcher.on('all', (_, file) => { + server.watcher.on('all', (_, file) => { if ( file === appTemplate || file === errorTemplate || @@ -439,6 +147,7 @@ export async function dev(vite, vite_config, svelte_config, get_remotes, root) { }); const assets = svelte_config.kit.paths.assets ? SVELTE_KIT_ASSETS : svelte_config.kit.paths.base; + const asset_server = sirv(svelte_config.kit.files.assets, { dev: true, etag: true, @@ -446,8 +155,8 @@ export async function dev(vite, vite_config, svelte_config, get_remotes, root) { extensions: [] }); - vite.middlewares.use((req, res, next) => { - const base = `${vite.config.server.https ? 'https' : 'http'}://${ + server.middlewares.use((req, res, next) => { + const base = `${server.config.server.https ? 'https' : 'http'}://${ req.headers[':authority'] || req.headers.host }`; @@ -455,7 +164,7 @@ export async function dev(vite, vite_config, svelte_config, get_remotes, root) { if (decoded.startsWith(assets)) { const pathname = decoded.slice(assets.length); - const file = svelte_config.kit.files.assets + pathname; + const file = path.join(svelte_config.kit.files.assets, pathname.slice(1)); if (fs.existsSync(file) && !fs.statSync(file).isDirectory()) { if (has_correct_case(file, svelte_config.kit.files.assets)) { @@ -469,29 +178,46 @@ export async function dev(vite, vite_config, svelte_config, get_remotes, root) { next(); }); - const env = loadEnv(vite_config.mode, svelte_config.kit.env.dir, ''); - const emulator = await svelte_config.kit.adapter?.emulate?.(); + const inline_styles_pathname = create_app_dir_matcher( + svelte_config.kit.paths.base, + svelte_config.kit.appDir, + '/inline-styles' + ); + const check_feature_pathname = create_app_dir_matcher( + svelte_config.kit.paths.base, + svelte_config.kit.appDir, + '/check-feature' + ); + + server.environments.ssr.hot.on('sveltekit:ssr-load-module-error', (error) => + display_ssr_error_on_client(server, vite, error) + ); return () => { - const serve_static_middleware = vite.middlewares.stack.find( + // ensure it has the correct server address + invalidate_module(server, sveltekit_ipc); + + const serve_static_middleware = server.middlewares.stack.find( (middleware) => /** @type {Function} */ (middleware.handle).name === 'viteServeStaticMiddleware' ); // Vite will give a 403 on URLs like /test, /static, and /package.json preventing us from // serving routes with those names. See https://github.com/vitejs/vite/issues/7363 - remove_static_middlewares(vite.middlewares); + remove_static_middlewares(server.middlewares); - vite.middlewares.use(async (req, res) => { + server.middlewares.use(async (req, res) => { // Vite's base middleware strips out the base path. Restore it const original_url = req.url; req.url = req.originalUrl; try { - const base = `${vite.config.server.https ? 'https' : 'http'}://${ + const base = `${server.config.server.https ? 'https' : 'http'}://${ req.headers[':authority'] || req.headers.host }`; - const decoded = decodeURI(new URL(base + req.url).pathname); + const url = new URL(base + req.url); + const decoded = decodeURI(url.pathname); + const file = posixify( path.resolve(root, decoded.slice(svelte_config.kit.paths.base.length + 1)) ); @@ -515,6 +241,39 @@ export async function dev(vite, vite_config, svelte_config, get_remotes, root) { return not_found(req, res, svelte_config.kit.paths.base); } + if (decoded.match(inline_styles_pathname)) { + const urls = url.searchParams.getAll('urls'); + const styles = await get_inline_css(server, urls, vite); + res.writeHead(200, { + 'content-type': 'application/json' + }); + res.end(JSON.stringify(styles)); + return; + } + + if (decoded.match(check_feature_pathname)) { + const route_id = url.searchParams.get('route_id'); + const config = url.searchParams.get('config'); + const feature = url.searchParams.get('feature'); + + if (!route_id || !config || !feature) { + res.writeHead(400); + res.end('Must have route_id, config, and feature query arguments'); + return; + } + + const result = check_feature( + route_id, + JSON.parse(config), + feature, + svelte_config.kit.adapter + ); + + res.writeHead(200); + res.end(result?.message); + return; + } + if (decoded === svelte_config.kit.paths.base + '/service-worker.js') { const resolved = resolve_entry(svelte_config.kit.files.serviceWorker); @@ -531,41 +290,6 @@ export async function dev(vite, vite_config, svelte_config, get_remotes, root) { return; } - const resolved_instrumentation = resolve_entry( - path.join(svelte_config.kit.files.src, 'instrumentation.server') - ); - - if (resolved_instrumentation) { - await vite.ssrLoadModule(resolved_instrumentation); - } - - // we have to import `Server` before calling `set_assets` - const { Server } = /** @type {import('types').ServerModule} */ ( - await vite.ssrLoadModule(`${get_runtime_base(root)}/server/index.js`, { - fixStacktrace: true - }) - ); - - const { set_fix_stack_trace } = await vite.ssrLoadModule( - `${get_runtime_base(root)}/server/internal.js` - ); - set_fix_stack_trace(fix_stack_trace); - - const { set_assets } = await vite.ssrLoadModule('$app/paths/internal/server'); - set_assets(assets); - - const server = new Server(manifest); - - await server.init({ - env, - read: (file) => createReadableStream(from_fs(file)) - }); - - const request = getRequest({ - base, - request: req - }); - if (manifest_error) { console.error(styleText(['bold', 'red'], manifest_error.message)); @@ -588,26 +312,18 @@ export async function dev(vite, vite_config, svelte_config, get_remotes, root) { return; } - const rendered = await server.respond(request, { - getClientAddress: () => { - const { remoteAddress } = req.socket; - if (remoteAddress) return remoteAddress; - throw new Error('Could not determine clientAddress'); - }, - read: (file) => { - if (file in manifest._.server_assets) { - return fs.readFileSync(from_fs(file)); - } - - return fs.readFileSync(path.join(svelte_config.kit.files.assets, file)); - }, - before_handle: async (event, config, prerender, handle) => { - // we need to use .run because .enterWith() is not supported in Cloudflare Workers - // see https://blog.cloudflare.com/workers-node-js-asynclocalstorage/ - return await async_local_storage.run({ event, config, prerender }, handle); - }, - emulator + // TODO: allow RunnableDevEnvironment too + if (!vite.isFetchableDevEnvironment(server.environments.ssr)) { + throw new Error( + 'The Vite configured dev SSR environment must be a FetchableDevEnvironment' + ); + } + + const request = getRequest({ + base, + request: req }); + const rendered = await server.environments.ssr.dispatchFetch(request); if (rendered.status === 404) { // @ts-expect-error @@ -622,7 +338,6 @@ export async function dev(vite, vite_config, svelte_config, get_remotes, root) { } catch (e) { const error = coalesce_to_error(e); res.statusCode = 500; - fix_stack_trace(error); res.end(error.stack || error.message); // handle `stackless` errors } }); @@ -632,7 +347,7 @@ export async function dev(vite, vite_config, svelte_config, get_remotes, root) { /** * @param {import('connect').Server} server */ -function remove_static_middlewares(server) { +export function remove_static_middlewares(server) { const static_middlewares = ['viteServeStaticMiddleware', 'viteServePublicMiddleware']; for (let i = server.stack.length - 1; i > 0; i--) { // @ts-expect-error using internals @@ -643,9 +358,9 @@ function remove_static_middlewares(server) { } /** - * @param {import('vite').ViteDevServer} vite - * @param {import('vite').ModuleNode | import('vite').EnvironmentModuleNode} node - * @param {Set} deps + * @param {ViteDevServer} vite + * @param {ModuleNode | EnvironmentModuleNode} node + * @param {Set} deps */ async function find_deps(vite, node, deps) { // since `ssrTransformResult.deps` contains URLs instead of `ModuleNode`s, this process is asynchronous. @@ -653,7 +368,7 @@ async function find_deps(vite, node, deps) { /** @type {Promise[]} */ const branches = []; - /** @param {import('vite').ModuleNode | import('vite').EnvironmentModuleNode} node */ + /** @param {ModuleNode | EnvironmentModuleNode} node */ async function add(node) { if (!deps.has(node)) { deps.add(node); @@ -663,23 +378,20 @@ async function find_deps(vite, node, deps) { /** @param {string} url */ async function add_by_url(url) { - const node = await get_server_module_by_url(vite, url); + const node = await vite.environments.ssr.moduleGraph.getModuleByUrl(url); if (node) { await add(node); } } - const transform_result = - /** @type {import('vite').ModuleNode} */ (node).ssrTransformResult || node.transformResult; - - if (transform_result) { - if (transform_result.deps) { - transform_result.deps.forEach((url) => branches.push(add_by_url(url))); + if (node.transformResult) { + if (node.transformResult.deps) { + node.transformResult.deps.forEach((url) => branches.push(add_by_url(url))); } - if (transform_result.dynamicDeps) { - transform_result.dynamicDeps.forEach((url) => branches.push(add_by_url(url))); + if (node.transformResult.dynamicDeps) { + node.transformResult.dynamicDeps.forEach((url) => branches.push(add_by_url(url))); } } else { node.importedModules.forEach((node) => branches.push(add(node))); @@ -688,16 +400,6 @@ async function find_deps(vite, node, deps) { await Promise.all(branches); } -/** - * @param {import('vite').ViteDevServer} vite - * @param {string} url - */ -function get_server_module_by_url(vite, url) { - return vite.environments - ? vite.environments.ssr.moduleGraph.getModuleByUrl(url) - : vite.moduleGraph.getModuleByUrl(url, true); -} - /** * Determine if a file is being requested with the correct case, * to ensure consistent behaviour between dev and prod and across @@ -707,7 +409,7 @@ function get_server_module_by_url(vite, url) { * @param {string} assets * @returns {boolean} */ -function has_correct_case(file, assets) { +export function has_correct_case(file, assets) { if (file === assets) return true; const parent = path.dirname(file); @@ -718,3 +420,117 @@ function has_correct_case(file, assets) { return false; } + +/** + * @param {ViteDevServer} server + * @param {string} id + * @returns {void} + */ +export function invalidate_module(server, id) { + for (const environment in server.environments) { + const module = server.environments[environment].moduleGraph.getModuleById(id); + if (module) { + server.environments[environment].moduleGraph.invalidateModule(module); + } + } +} + +/** + * Creates a RegExp to help match against app directory requests. + * @param {string} base + * @param {string} appDir + * @param {string} pattern + * @returns {RegExp} + */ +export function create_app_dir_matcher(base, appDir, pattern) { + return new RegExp(`^${escape_for_regexp(`${base}/${appDir}${pattern}`)}$`); +} + +/** + * @param {ViteDevServer} server + * @param {string[]} urls + * @param {typeof import('vite')} vite + * @returns {Promise>} + */ +async function get_inline_css(server, urls, vite) { + /** @type {Set} */ + const deps = new Set(); + + for (const url of urls) { + const module_node = await server.environments.ssr.moduleGraph.getModuleByUrl(url); + if (!module_node) throw new Error(`Could not find node for ${url}`); + await find_deps(server, module_node, deps); + } + + /** @type {Map} */ + const styles = new Map(); + + for (const dep of deps) { + if (vite.isCSSRequest(dep.url) && !vite_css_query_regex.test(dep.url)) { + const inline_css_url = dep.url.includes('?') + ? dep.url.replace('?', '?inline&') + : dep.url + '?inline'; + styles.set(dep.url, inline_css_url); + } + } + + return Object.fromEntries(styles); +} + +/** + * + * @param {string} route_id + * @param {unknown} config + * @param {string} feature + * @param {Adapter | undefined} adapter + * @returns { { message: string } | void } + */ +export function check_feature(route_id, config, feature, adapter) { + if (!adapter) return; + + switch (feature) { + case '$app/server:read': { + const supported = adapter.supports?.read?.({ + route: { id: route_id }, + config + }); + + if (!supported) { + return { + message: `Cannot use \`read\` from \`$app/server\` in ${route_id} when using ${adapter.name}. Please ensure that your adapter is up to date and supports this feature.` + }; + } + } + } +} + +/** + * @param {ViteDevServer} server + * @param {typeof import('vite')} vite + * @param {Error} err + * @returns {void} + */ +function display_ssr_error_on_client(server, vite, err) { + const msg = vite.buildErrorMessage(err, [ + styleText('red', `Internal server error: ${err.message}`) + ]); + + if (!server.config.logger.hasErrorLogged(err)) { + server.config.logger.error(msg, { error: err }); + } + + // TODO this is inadequate — it doesn't reliably show the overlay on every page load, + // and when it does appear it may immediately vanish. `server.ws.send` broadcasts + // to all connected clients, even ones that are unaffected by the error. + // we need a more considered approach + server.ws.send({ + type: 'error', + err: { + ...err, + // these properties are non-enumerable and will + // not be serialized unless we explicitly include them + message: err.message, + stack: err.stack ?? '' + } + }); +} diff --git a/packages/kit/src/exports/vite/dev/server.js b/packages/kit/src/exports/vite/dev/server.js new file mode 100644 index 000000000000..bfbc3eb787ed --- /dev/null +++ b/packages/kit/src/exports/vite/dev/server.js @@ -0,0 +1,57 @@ +import { AsyncLocalStorage } from 'node:async_hooks'; +import { DEV } from 'esm-env'; +import { Server as KitServer } from '../../../runtime/server/index.js'; +import { check_feature } from '../../../utils/features.js'; +import { SCHEME } from '../../../utils/url.js'; +import { manifest } from './ssr_manifest.js'; + +const async_local_storage = new AsyncLocalStorage(); + +/** @param {string} label */ +globalThis.__SVELTEKIT_TRACK__ = (label) => { + const context = async_local_storage.getStore(); + if (!context || context.prerender === true) return; + + // we can't await this because `read` has a synchronous signature + void check_feature(context.event.route.id, context.config, label); +}; + +const fetch = globalThis.fetch; +/** @type {typeof fetch} */ +globalThis.fetch = (info, init) => { + if (typeof info === 'string' && !SCHEME.test(info)) { + throw new Error( + `Cannot use relative URL (\${info}) with global fetch — use \`event.fetch\` instead: https://svelte.dev/docs/kit/web-standards#fetch-apis` + ); + } + + return fetch(info, init); +}; + +export class Server extends KitServer { + /** @type {import('types').InternalServer['respond']} */ + async respond(request, options) { + if (DEV) { + options.before_handle = async (event, config, prerender, handle) => { + // we need to use .run because .enterWith() is not supported in Cloudflare Workers + // see https://blog.cloudflare.com/workers-node-js-asynclocalstorage/ + return await async_local_storage.run({ event, config, prerender }, handle); + }; + } + + return await super.respond(request, options); + } +} + +import.meta.hot?.on('sveltekit:remote', async (hash) => { + const remote = (await manifest._.remotes[hash]()).default; + + /** @type {Map} */ + const exports = new Map(); + for (const name in remote) { + exports.set(name, { type: remote[name].__.type }); + } + const data = Object.fromEntries(exports); + + import.meta.hot?.send(`sveltekit:remote:${hash}`, data); +}); diff --git a/packages/kit/src/exports/vite/dev/ssr_entry.js b/packages/kit/src/exports/vite/dev/ssr_entry.js new file mode 100644 index 000000000000..6ca4e80d556f --- /dev/null +++ b/packages/kit/src/exports/vite/dev/ssr_entry.js @@ -0,0 +1,39 @@ +/** @import { InternalServer } from 'types' */ +import fs from 'node:fs'; +import path from 'node:path'; +import { env } from 'sveltekit:env'; +import { Server } from 'sveltekit:server'; +import { manifest } from 'sveltekit:server-manifest'; +import { createReadableStream } from '@sveltejs/kit/node'; +import { from_fs } from '../../../utils/vite.js'; + +/** @type {InternalServer} */ +const server = new Server(manifest); + +await server.init({ + env, + read: (file) => createReadableStream(from_fs(file)) +}); + +/** + * @param {Request} request + * @param {string | undefined} remote_address + * @returns {Promise} + */ +export async function respond(request, remote_address) { + return await server.respond(request, { + getClientAddress: () => { + if (remote_address) return remote_address; + throw new Error('Could not determine clientAddress'); + }, + read: (file) => { + if (file in manifest._.server_assets) { + return fs.readFileSync(from_fs(file)); + } + + return fs.readFileSync(path.join(__SVELTEKIT_FILES_ASSETS__, file)); + } + }); +} + +import.meta.hot?.accept(); diff --git a/packages/kit/src/exports/vite/dev/ssr_manifest.js b/packages/kit/src/exports/vite/dev/ssr_manifest.js new file mode 100644 index 000000000000..8726793563b0 --- /dev/null +++ b/packages/kit/src/exports/vite/dev/ssr_manifest.js @@ -0,0 +1,176 @@ +/** @import { SSRManifest } from '@sveltejs/kit' */ +import { server_assets } from '__sveltekit/server-assets'; +import { remotes } from '__sveltekit/remotes'; +import { manifest_data, mime_types } from '__sveltekit/manifest-data'; +import { get } from '__sveltekit/ipc'; +import { resolve } from './utils.js'; +import { to_fs } from '../../../utils/vite.js'; +import { compact } from '../../../utils/array.js'; +import { join } from '../../../utils/path.js'; + +/** @type {SSRManifest} */ +export const manifest = { + appDir: __SVELTEKIT_APP_DIR__, + appPath: `${__SVELTEKIT_PATHS_BASE__}/${__SVELTEKIT_APP_DIR__}`, + assets: new Set(manifest_data.assets.map((asset) => asset.file)), + base: __SVELTEKIT_PATHS_BASE__, + mimeTypes: mime_types, + _: { + client: { + start: `${__SVELTEKIT_RUNTIME__}/client/entry.js`, + app: `${to_fs(__SVELTEKIT_OUT_DIR__)}/generated/client/app.js`, + imports: [], + stylesheets: [], + fonts: [], + uses_env_dynamic_public: true, + nodes: __SVELTEKIT_CLIENT_ROUTING__ + ? undefined + : manifest_data.nodes.map((node, i) => { + if (node.component || node.universal) { + return `${__SVELTEKIT_PATHS_BASE__}${to_fs(__SVELTEKIT_OUT_DIR__)}/generated/client/nodes/${i}.js`; + } + }), + + // \`css\` is not necessary in dev, as the JS file from \`nodes\` will reference the CSS file + routes: __SVELTEKIT_CLIENT_ROUTING__ + ? 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, + nodes: manifest_data.nodes.map((node, i) => { + return async () => { + /** @type {import('types').SSRNode} */ + const result = {}; + result.index = i; + 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 {string[]} */ + const urls = []; + + if (node.component) { + result.component = async () => { + const { module, url } = await resolve( + join(__SVELTEKIT_ROOT__, /** @type {string} */ (node.component)) + ); + urls.push(url); + return module.default; + }; + } + + if (node.universal) { + if (node.page_options?.ssr === false) { + result.universal = node.page_options; + } else { + // TODO: explain why the file was loaded on the server if we fail to load it + const { module, url } = await resolve(join(__SVELTEKIT_ROOT__, node.universal)); + urls.push(url); + result.universal = module; + } + } + + if (node.server) { + const { module } = await resolve(join(__SVELTEKIT_ROOT__, 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 () => { + const search_params = new URLSearchParams(); + for (const url of urls) { + search_params.append('urls', url); + } + + const response = await get(`/inline-styles?${search_params}`); + if (!response.ok) { + throw new Error( + `Failed to fetch inline styles for node ${i}: ${response.status} ${response.statusText}. This should never happen` + ); + } + + /** @type {Record} */ + const styles = await response.json(); + + const importing_styles = Object.entries(styles).map(async ([dep_url, inline_css_url]) => { + return [ + dep_url, + await import(/* @vite-ignore */ inline_css_url).then((mod) => mod.default) + ]; + }); + + return Object.fromEntries(await Promise.all(importing_styles)); + }; + + return result; + }; + }), + prerendered_routes: new Set(), + get remotes() { + return Object.fromEntries( + remotes.map((remote) => [ + remote.hash, + () => + import(/* @vite-ignore */ join(__SVELTEKIT_ROOT__, 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 = join(__SVELTEKIT_ROOT__, endpoint.file); + const { module } = await resolve(url); + return module; + } + : null, + endpoint_id: endpoint?.file + }; + }) + ), + matchers: async () => { + if (!manifest_data.params) return {}; + + const url = join(__SVELTEKIT_ROOT__, manifest_data.params); + const { module } = await resolve(url); + + if (!module.params) { + throw new Error(`${manifest_data.params} does not export \`params\` from \`defineParams\``); + } + + return module.params; + } + } +}; diff --git a/packages/kit/src/exports/vite/dev/utils.js b/packages/kit/src/exports/vite/dev/utils.js new file mode 100644 index 000000000000..6754fb617080 --- /dev/null +++ b/packages/kit/src/exports/vite/dev/utils.js @@ -0,0 +1,27 @@ +import { to_fs } from '../../../utils/vite.js'; + +/** @param {string} url */ +export async function loud_ssr_load_module(url) { + try { + return await import(/* @vite-ignore */ url); + } catch (err) { + if (err instanceof Error) { + import.meta.hot?.send('sveltekit:ssr-load-module-error', { + ...err, + // these properties are non-enumerable and will not be + // serialized unless we explicitly include them + message: err.message, + stack: err.stack + }); + } + + throw err; + } +} + +/** @param {string} id */ +export async function resolve(id) { + const url = id.startsWith('..') ? to_fs(id) : `file:///${id}`; + const module = await loud_ssr_load_module(url); + return { module, url }; +} diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index 1ba25c2cf3ba..46edc75ca265 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -1,12 +1,15 @@ +/** @import { ModuleRunner } from 'vite/module-runner' */ /** @import { EnvVarConfig, KitConfig } from '@sveltejs/kit' */ /** @import { Options, SvelteConfig } from '@sveltejs/vite-plugin-svelte' */ /** @import { PreprocessorGroup } from 'svelte/compiler' */ -/** @import { BuildData, ManifestData, Prerendered, ServerMetadata, RemoteInternals, ValidatedConfig, ValidatedKitConfig } from 'types' */ -/** @import { Manifest, Plugin, ResolvedConfig, Rolldown, UserConfig, ViteDevServer } from 'vite' */ +/** @import { BuildData, DevContext, ManifestData, Prerendered, ServerMetadata, RemoteInternals, ValidatedConfig, ValidatedKitConfig } from 'types' */ +/** @import { Manifest, Plugin, ResolvedConfig, Rolldown, UserConfig } from 'vite' */ import fs from 'node:fs'; import path from 'node:path'; import process from 'node:process'; +import { pathToFileURL } from 'node:url'; import { styleText } from 'node:util'; +import * as devalue from 'devalue'; import { loadEnv } from 'vite'; import { exactRegex, prefixRegex } from 'rolldown/filter'; @@ -24,11 +27,17 @@ import { import * as sync from '../../core/sync/sync.js'; import { create_assets } from '../../core/sync/create_manifest_data/index.js'; import { load_and_validate_params } from '../../utils/params.js'; -import { runtime_directory, logger } from '../../core/utils.js'; +import { + logger, + get_mime_lookup, + get_address, + runtime_directory, + get_runtime_base +} from '../../core/utils.js'; import { generate_manifest } from '../../core/generate_manifest/index.js'; import { build_server_nodes } from './build/build_server.js'; import { find_deps, resolve_symlinks } from './build/utils.js'; -import { dev } from './dev/index.js'; +import { dev, invalidate_module } from './dev/index.js'; import { preview } from './preview/index.js'; import { error_for_missing_config, @@ -48,17 +57,24 @@ import { app_env_private, app_server, service_worker, + sveltekit_remotes, + sveltekit_traced, + sveltekit_manifest_data, + sveltekit_ipc, sveltekit_env, sveltekit_env_private, sveltekit_env_service_worker, sveltekit_env_public_client, - sveltekit_env_public_server + sveltekit_env_public_server, + sveltekit_server_assets } from './module_ids.js'; +import { to_fs } from '../../utils/vite.js'; import { import_peer } from '../../utils/import.js'; import { compact } from '../../utils/array.js'; import { should_ignore, has_children } from './static_analysis/utils.js'; import { process_config, split_config, validate_config } from '../../core/config/index.js'; import { treeshake_prerendered_remotes } from './build/remote.js'; +import { SVELTE_KIT_ASSETS } from '../../constants.js'; /** * The posix-ified root of the project based on the Vite configuration. @@ -67,6 +83,9 @@ import { treeshake_prerendered_remotes } from './build/remote.js'; */ let root; +/** @type {DevContext | null} */ +let dev_context = null; + /** @type {import('./types.js').EnforcedConfig} */ const enforced_config = { appType: true, @@ -301,6 +320,9 @@ function kit({ svelte_config }) { /** @type {import('node:path').ParsedPath} */ let parsed_service_worker; + /** @type {string | null} */ + let server_instrumentation; + /** @type {string} */ let normalized_cwd; /** @type {Array<{ alias: string, path: string }>} */ @@ -346,12 +368,12 @@ function kit({ svelte_config }) { out = `${out_dir}/output`; version_hash = hash(kit.version.name); - kit_global = is_build ? `globalThis.__sveltekit_${version_hash}` : 'globalThis.__sveltekit_dev'; env = loadEnv(config_env.mode, kit.env.dir, ''); + server_instrumentation = resolve_entry(path.join(kit.files.src, 'instrumentation.server')); vite = await import_peer('vite', root); @@ -396,8 +418,6 @@ function kit({ svelte_config }) { const client_hooks = resolve_entry(kit.files.hooks.client); if (client_hooks) allow.add(path.dirname(client_hooks)); - const generated = path.posix.join(out_dir, 'generated'); - // dev and preview config can be shared /** @type {UserConfig} */ const new_config = { @@ -411,7 +431,8 @@ function kit({ svelte_config }) { }, resolve: { alias: [ - { find: '__SERVER__', replacement: `${generated}/server` }, + { find: '__SERVER__', replacement: `${out_dir}/generated/server` }, + { find: '__SVELTEKIT__', replacement: `${runtime_directory}/..` }, { find: '$app', replacement: `${runtime_directory}/app` }, { find: '$env', replacement: `${runtime_directory}/env` }, { @@ -437,11 +458,12 @@ function kit({ svelte_config }) { preview: { cors: { preflightContinue: true } }, + // By default, only client environments inherit the top-level `optimizeDeps`. + // Therefore, we copy need to copy this setting to other environments + // in the adapter code if it uses `optimizeDeps` for "full-bundle mode" + // such as the Cloudflare adapter optimizeDeps: { - entries: [ - `${kit.files.routes}/**/+*.{svelte,js,ts}`, - `!${kit.files.routes}/**/+*server.*` - ], + entries: [`${kit.files.routes}/**/+*.{svelte,js,ts}`], exclude: [ // Without this SvelteKit will be prebundled on the client, which means we end up with two versions of Redirect etc. // Also see https://github.com/sveltejs/kit/issues/5952#issuecomment-1218844057 @@ -450,7 +472,9 @@ function kit({ svelte_config }) { // this does not affect app code, just handling of imported libraries that use $app or $env '$app', '$env' - ] + ], + // avoid Vite dev server reloading the first time a page is requested + include: ['@sveltejs/kit > devalue', '@sveltejs/kit > esm-env'] }, ssr: { noExternal: [ @@ -492,6 +516,7 @@ function kit({ svelte_config }) { const define = { __SVELTEKIT_APP_DIR__: s(posixify(kit.appDir)), + __SVELTEKIT_OUT_DIR__: s(out_dir), __SVELTEKIT_APP_VERSION__: s(kit.version.name), __SVELTEKIT_EMBEDDED__: s(kit.embedded), __SVELTEKIT_FORK_PRELOADS__: s(kit.experimental.forkPreloads), @@ -504,6 +529,8 @@ function kit({ svelte_config }) { __SVELTEKIT_SUPPORTS_ASYNC__: s( svelte_config.compilerOptions?.experimental?.async ?? false ), + __SVELTEKIT_ROOT__: s(root), + __SVELTEKIT_RUNTIME__: s(get_runtime_base(root)), __SVELTEKIT_DEV__: s(!is_build) }; @@ -522,12 +549,9 @@ function kit({ svelte_config }) { __SVELTEKIT_APP_VERSION_POLL_INTERVAL__: '0', __SVELTEKIT_PAYLOAD__: kit_global, // only relevant when bundleStrategy !== 'split' __SVELTEKIT_HAS_SERVER_LOAD__: 'true', - __SVELTEKIT_HAS_UNIVERSAL_LOAD__: 'true' + __SVELTEKIT_HAS_UNIVERSAL_LOAD__: 'true', + __SVELTEKIT_FILES_ASSETS__: s(posixify(kit.files.assets)) }; - - // These Kit dependencies are packaged as CommonJS, which means they must always be externalized. - // Without this, the tests will still pass but `pnpm dev` will fail in projects that link `@sveltejs/kit`. - /** @type {NonNullable} */ (new_config.ssr).external = ['cookie']; } // Vite's `define` is a compile-time text replacement, but Vitest strips @@ -579,6 +603,340 @@ function kit({ svelte_config }) { } }; + /** @type {ModuleRunner} */ + let runner; + + /** @type {string | undefined} */ + let remote_address; + + /** @type {Plugin} */ + const plugin_node_environment = { + name: 'vite-plugin-sveltekit-node-environment', + apply: 'serve', + config: { + // run it earlier so the adapter Vite plugins can override it easily + order: 'pre', + handler(config) { + /** @type {UserConfig} */ + const new_config = { + environments: { + ssr: { + dev: { + createEnvironment(name, config) { + return vite.createFetchableDevEnvironment(name, config, { + hot: true, + transport: vite.createServerHotChannel(), + async handleRequest(request) { + try { + /** @type {import('./dev/ssr_entry.js')} */ + const { respond } = await runner.import('__sveltekit/dev-server-entry.js'); + return await respond(request, remote_address); + } catch (error) { + // Vite doesn't log errors so we do it ourselves + console.error(error); + throw error; + } + } + }); + } + } + } + } + }; + + warn_overridden_config(config, new_config); + + return new_config; + } + }, + async configureServer(server) { + if (runner) await runner.close(); + runner = vite.createServerModuleRunner(server.environments.ssr); + + return () => { + server.middlewares.use((req, _res, next) => { + remote_address = req.socket.remoteAddress; + next(); + }); + }; + }, + resolveId: { + filter: { + id: exactRegex('__sveltekit/dev-server-entry.js') + }, + handler() { + return server_instrumentation + ? sveltekit_traced + : posixify(path.join(import.meta.dirname, 'dev/ssr_entry.js')); + } + } + }; + + /** @type {Map }>} */ + let server_assets; + + /** + * Allows us to access the filesystem synchronously from an environment that + * doesn't have `node:fs`. This is used to dynamically populate the server + * manifest's `server_assets` property. + * @type {Plugin} + */ + const plugin_server_filesystem = { + name: 'vite-plugin-sveltekit-dev-server-filesystem', + apply: 'serve', + configureServer(server) { + server_assets = new Map(); + + server.watcher.on('unlink', (filepath) => { + const key = get_server_asset_key(filepath); + if (server_assets.has(key)) server_assets.delete(key); + }); + }, + + applyToEnvironment(environment) { + return environment.config.consumer === 'server'; + }, + + load: { + order: 'pre', + handler(id) { + if (!dev_context) return; + + const [filepath, raw_query] = id.split('?', 2); + + const query = new URLSearchParams('?' + raw_query); + const absolute_filepath = posixify(path.resolve(root, filepath)); + + if ( + (query.has('url') || vite_config.assetsInclude(absolute_filepath)) && + fs.existsSync(absolute_filepath) + ) { + const key = get_server_asset_key(absolute_filepath); + // it should be a typed array for devalue to serialise it + const data = new Uint8Array(fs.readFileSync(absolute_filepath)); + const size = data.byteLength; + + // update it immediately + dev_context.server.environments.ssr.hot.send('sveltekit:server-assets', { + filepath: key, + size, + data: devalue.stringify(data) + }); + + // persist changes in case of server reload + server_assets.set(key, { size, data }); + invalidate_module(dev_context.server, sveltekit_server_assets); + invalidate_module(dev_context.server, '__sveltekit/server'); + } + } + } + }; + + /** @type {string | undefined} */ + let current_address; + + /** @type {Plugin} */ + const plugin_dev_ssr = { + name: 'vite-plugin-sveltekit-dev-ssr', + apply: 'serve', + configureServer(server) { + return () => { + server.middlewares.use((_req, _res, next) => { + // ensure the server address is up-to-date + const address = get_address(server); + if (address && address !== current_address) { + current_address = address; + server.environments.ssr.hot.send('sveltekit:server-address', current_address); + invalidate_module(server, sveltekit_ipc); + } + + next(); + }); + }; + }, + + applyToEnvironment(environment) { + return environment.config.consumer === 'server'; + }, + + resolveId: { + filter: { + id: [ + exactRegex('sveltekit:server-manifest'), + exactRegex('sveltekit:server'), + exactRegex('sveltekit:env'), + exactRegex('__sveltekit/server-assets') + ] + }, + handler(id) { + if (id === 'sveltekit:server-manifest') { + return path.join(import.meta.dirname, 'dev/ssr_manifest.js'); + } + + if (id === 'sveltekit:server') { + return path.join(import.meta.dirname, 'dev/server.js'); + } + + if (id === 'sveltekit:env') { + return id; + } + + if (id === '__sveltekit/server-assets') { + return sveltekit_server_assets; + } + } + }, + load: { + filter: { + id: [ + exactRegex('sveltekit:env'), + exactRegex(sveltekit_ipc), + exactRegex(sveltekit_remotes), + exactRegex(sveltekit_manifest_data), + exactRegex(sveltekit_traced), + exactRegex(sveltekit_server_assets) + ] + }, + handler(id) { + switch (id) { + case sveltekit_ipc: { + if (!dev_context) { + throw new Error('dev_context was not initialised. But this should never happen'); + } + + const app_path = posixify( + `${svelte_config.kit.paths.base}/${svelte_config.kit.appDir}` + ); + + const address = get_address(dev_context.server); + + return dedent` + // helps us avoid global fetch warnings we emit when the user uses it incorrectly + const native_fetch = globalThis.fetch; + + export function get(pathname) { + return native_fetch(new URL(\`${app_path}\${pathname}\`, address)); + } + + let address${address ? ` = '${address}'` : ''}; + import.meta.hot?.on('sveltekit:server-address', (update) => { address = update }); + `; + } + + case sveltekit_remotes: { + return dedent` + export const remotes = ${s(remotes)}; + + import.meta.hot?.on('sveltekit:remotes', (remote) => { + remotes.push(remote); + }); + `; + } + + case sveltekit_manifest_data: { + if (!dev_context) { + throw new Error('dev_context was not initialised. But this should never happen'); + } + + const { manifest_data } = dev_context; + + const assets = svelte_config.kit.paths.assets + ? SVELTE_KIT_ASSETS + : svelte_config.kit.paths.base; + + return dedent` + import { set_assets } from '__SERVER__/internal.js'; + import { load_and_validate_params } from '__SVELTEKIT__/utils/params.js'; + import { to_fs } from '__SVELTEKIT__/utils/vite.js'; + import { loud_ssr_load_module } from '__SVELTEKIT__/exports/vite/dev/utils.js'; + + set_assets(${s(assets)}); + + export const manifest_data = { + assets: ${s(manifest_data.assets)}, + hooks: { + client: ${s(manifest_data.hooks.client)}, + server: ${s(manifest_data.hooks.server)}, + universal: ${s(manifest_data.hooks.universal)} + }, + nodes: ${devalue.uneval(manifest_data.nodes)}, + routes: ${devalue.uneval(manifest_data.routes)}, + params: ${s(manifest_data.params)} + }; + + await load_and_validate_params({ + routes: manifest_data.routes, + params_path: manifest_data.params, + root: to_fs(__SVELTEKIT_ROOT__), + load: (file) => loud_ssr_load_module(file) + }); + + export const mime_types = ${s(get_mime_lookup(manifest_data))}; + + import.meta.hot?.on( + 'sveltekit:manifest-data', + ({ nodes_page_options, endpoints_page_options }) => { + for (let i = 0; i < nodes_page_options.length; i++) { + manifest_data.nodes[i].page_options = nodes_page_options[i]; + } + + for (let i = 0; i < endpoints_page_options.length; i++) { + const endpoint = manifest_data.routes[i].endpoint; + if (endpoint) endpoint.page_options = endpoints_page_options[i]; + } + } + ); + `; + } + + case sveltekit_traced: { + if (!server_instrumentation) { + throw new Error('Server instrumentation file not found. This should never happen'); + } + + return dedent` + import '${posixify(server_instrumentation)}'; + + const { respond } = await import('${import.meta.resolve('./dev/ssr_entry.js')}'); + export { respond }; + + import.meta.hot?.accept(); + `; + } + + case 'sveltekit:env': { + return `export const env = ${s(env)};`; + } + + case sveltekit_server_assets: { + /** @type {Array<[string, { size: number; data: Uint8Array }]>} */ + const entries = Array.from(server_assets); + + return dedent` + import * as devalue from '@sveltejs/kit/internal/devalue'; + + export const server_assets = { + ${entries + .map(([filepath, { size }]) => { + return `${s(filepath)}: ${size}`; + }) + .join(',\n')} + }; + + export const server_assets_content = ${devalue.uneval( + Object.fromEntries(entries.map(([filepath, { data }]) => [filepath, data])) + )}; + + import.meta.hot?.on('sveltekit:server-assets', async ({ filepath, size, data }) => { + server_assets[filepath] = size; + server_assets_content[filepath] = devalue.parse(data); + }); + `; + } + } + } + } + }; /** @type {string | null} */ let explicit_env_entry = null; @@ -646,7 +1004,9 @@ function kit({ svelte_config }) { return `${runtime_directory}/client/remote-functions/index.js`; } - return `\0virtual:${id}`; + if (id.startsWith('__sveltekit/') && id !== '__sveltekit/dev-server-entry.js') { + return `\0virtual:${id}`; + } } }, @@ -734,7 +1094,12 @@ function kit({ svelte_config }) { // ]), async handler(id, importer, options) { if (importer && !importer.endsWith('index.html')) { - const resolved = await this.resolve(id, importer, { ...options, skipSelf: true }); + const resolved = await this.resolve(id, importer, { + custom: options.custom, + isEntry: options.isEntry, + kind: options.kind, + skipSelf: true + }); if (resolved) { const normalized = normalize_id(resolved.id, normalized_aliases, normalized_cwd); @@ -783,18 +1148,17 @@ function kit({ svelte_config }) { if (!is_server_only) return; - // in dev, this doesn't exist, so we need to create it - manifest_data ??= sync.all(svelte_config, root).manifest_data; + const { nodes, hooks } = dev_context?.manifest_data ?? manifest_data; /** @type {Set} */ const entrypoints = new Set(); - for (const node of manifest_data.nodes) { + for (const node of nodes) { if (node.component) entrypoints.add(node.component); if (node.universal) entrypoints.add(node.universal); } - if (manifest_data.hooks.client) entrypoints.add(manifest_data.hooks.client); - if (manifest_data.hooks.universal) entrypoints.add(manifest_data.hooks.universal); + if (hooks.client) entrypoints.add(hooks.client); + if (hooks.universal) entrypoints.add(hooks.universal); // Walk up the import graph from the server-only module, looking for a chain // that leads back to a client entrypoint. We search all candidates (not just @@ -850,9 +1214,6 @@ function kit({ svelte_config }) { } }; - /** @type {ViteDevServer} */ - let dev_server; - /** @type {Array<{ hash: string, file: string }>} */ const remotes = []; @@ -896,10 +1257,6 @@ function kit({ svelte_config }) { } }, - configureServer(_dev_server) { - dev_server = _dev_server; - }, - async transform(code, id) { const normalized = normalize_id(id, normalized_aliases, normalized_cwd); if (!svelte_config.kit.moduleExtensions.some((ext) => normalized.endsWith(`.remote${ext}`))) { @@ -913,6 +1270,10 @@ function kit({ svelte_config }) { }; remotes.push(remote); + if (dev_context) { + dev_context.server.environments.ssr.hot.send('sveltekit:remotes', remote); + invalidate_module(dev_context.server, sveltekit_remotes); + } if (this.environment.config.consumer !== 'client') { // we need to add an `await Promise.resolve()` because if the user imports this function @@ -928,7 +1289,7 @@ function kit({ svelte_config }) { import * as $$_self_$$ from './${path.basename(id)}'; import { init_remote_functions as $$_init_$$ } from '@sveltejs/kit/internal/server'; - ${dev_server ? 'await Promise.resolve()' : ''} + ${dev_context?.server ? 'await Promise.resolve()' : ''} $$_init_$$($$_self_$$, ${s(file)}, ${s(remote.hash)}); @@ -939,7 +1300,7 @@ function kit({ svelte_config }) { `; // Emit a dedicated entry chunk for this remote in SSR builds (prod only) - if (!dev_server) { + if (!dev_context?.server) { remote_original_by_hash.set(remote.hash, id); if (!emitted_remote_hashes.has(remote.hash)) { this.emitFile({ @@ -962,11 +1323,24 @@ function kit({ svelte_config }) { // in dev, load the server module here (which will result in this hook // being called again with `opts.ssr === true` if the module isn't // already loaded) so we can determine what it exports - if (dev_server) { - const module = await dev_server.ssrLoadModule(id); + if (dev_context?.server) { + /** @type {PromiseWithResolvers>} */ + const load_ssr_remote = Promise.withResolvers(); + + const event = `sveltekit:remote:${remote.hash}`; + /** @param {Record} payload */ + const listener = (payload) => { + load_ssr_remote.resolve(payload); + dev_context?.server.environments.ssr.hot.off(event, listener); + }; + + dev_context.server.environments.ssr.hot.on(event, listener); + dev_context.server.environments.ssr.hot.send('sveltekit:remote', remote.hash); + + const exports = await load_ssr_remote.promise; - for (const [name, value] of Object.entries(module)) { - const type = value?.__?.type; + for (const [name, value] of Object.entries(exports)) { + const type = value.type; if (type) { map.set(name, type); } @@ -996,7 +1370,7 @@ function kit({ svelte_config }) { } result += '\n'; - if (dev_server) { + if (dev_context?.server) { result += `\nimport.meta.hot?.accept();\n`; } @@ -1033,7 +1407,7 @@ function kit({ svelte_config }) { /** @type {Prerendered} */ let prerendered; - /** @type {Set} */ + /** @type {Set} client build output and files from the static dir */ let build_files; /** @type {string} */ let service_worker_code; @@ -1510,8 +1884,13 @@ function kit({ svelte_config }) { * Adds the SvelteKit middleware to do SSR in dev mode. * @see https://vitejs.dev/guide/api-plugin.html#configureserver */ - async configureServer(server) { - return await dev(server, vite_config, svelte_config, () => remotes, root); + configureServer(server) { + // manifest_data will be populated after running the `dev` function below + dev_context = /** @type {DevContext} */ ({ + server + }); + + return dev(server, vite_config, vite, svelte_config, root, dev_context); }, /** @@ -1560,7 +1939,8 @@ function kit({ svelte_config }) { await load_and_validate_params({ routes: manifest_data.routes, params_path: manifest_data.params, - root + root, + load: (file) => import(pathToFileURL(file).href) }); const { output: server_chunks } = /** @type {Rolldown.RolldownOutput} */ ( @@ -1577,6 +1957,7 @@ function kit({ svelte_config }) { const build_data = { app_dir: kit.appDir, app_path: `${kit.paths.base.slice(1)}${kit.paths.base ? '/' : ''}${kit.appDir}`, + base: kit.paths.base, manifest_data, out_dir: out, service_worker: service_worker_entry_file ? 'service-worker.js' : null, // TODO make file configurable? @@ -1960,8 +2341,11 @@ function kit({ svelte_config }) { [ svelte_config.kit.adapter?.vite?.plugins, plugin_setup, + plugin_node_environment, plugin_remote_guard, plugin_remote, + plugin_server_filesystem, + plugin_dev_ssr, plugin_virtual_modules, process.env.TEST !== 'true' ? plugin_guard : undefined, plugin_service_worker, @@ -2041,3 +2425,8 @@ const create_service_worker_module = (config) => dedent` export const prerendered = []; export const version = ${s(config.kit.version.name)}; `; + +/** @param {string} filepath */ +function get_server_asset_key(filepath) { + return filepath.startsWith(root) ? posixify(path.relative(root, filepath)) : to_fs(filepath); +} diff --git a/packages/kit/src/exports/vite/module_ids.js b/packages/kit/src/exports/vite/module_ids.js index fa2daf11f587..48bcbf59c72b 100644 --- a/packages/kit/src/exports/vite/module_ids.js +++ b/packages/kit/src/exports/vite/module_ids.js @@ -9,6 +9,12 @@ export const sveltekit_env_service_worker = '\0virtual:__sveltekit/env/service-w export const service_worker = '\0virtual:service-worker'; +export const sveltekit_manifest_data = '\0virtual:__sveltekit/manifest-data'; +export const sveltekit_remotes = '\0virtual:__sveltekit/remotes'; +export const sveltekit_traced = '\0virtual:__sveltekit/traced'; +export const sveltekit_server_assets = '\0virtual:__sveltekit/server-assets'; +export const sveltekit_ipc = '\0virtual:__sveltekit/ipc'; + export const app_server = posixify( fileURLToPath(new URL('../../runtime/app/server/index.js', import.meta.url)) ); diff --git a/packages/kit/src/exports/vite/preview/index.js b/packages/kit/src/exports/vite/preview/index.js index e9b6051ef775..7c18a8cdf9c8 100644 --- a/packages/kit/src/exports/vite/preview/index.js +++ b/packages/kit/src/exports/vite/preview/index.js @@ -62,8 +62,6 @@ export async function preview(vite, vite_config, svelte_config) { throw error; } - const emulator = await svelte_config.kit.adapter?.emulate?.(); - return () => { // Remove the base middleware. It screws with the URL. // It also only lets through requests beginning with the base path, so that requests beginning @@ -223,8 +221,7 @@ export async function preview(vite, vite_config, svelte_config) { } return fs.readFileSync(join(svelte_config.kit.files.assets, file)); - }, - emulator + } }) ); }); diff --git a/packages/kit/src/exports/vite/types.d.ts b/packages/kit/src/exports/vite/types.d.ts index 900aa43541ea..47494f8ae61b 100644 --- a/packages/kit/src/exports/vite/types.d.ts +++ b/packages/kit/src/exports/vite/types.d.ts @@ -1,3 +1,27 @@ +import 'vite/types/customEvent.d.ts'; +import type { PageOptions } from './static_analysis/types.js'; + +declare module 'vite/types/customEvent.d.ts' { + interface CustomEventMap { + 'sveltekit:server-address': string; + 'sveltekit:remotes': { + hash: string; + file: string; + }; + 'sveltekit:remote': string; + 'sveltekit:server-assets': { + filepath: string; + size: number; + data: string; + }; + 'sveltekit:manifest-data': { + nodes_page_options: Array; + endpoints_page_options: Array; + }; + 'sveltekit:ssr-load-module-error': Error; + } +} + export interface EnforcedConfig { [key: string]: EnforcedConfig | true; } diff --git a/packages/kit/src/exports/vite/utils.js b/packages/kit/src/exports/vite/utils.js index 580d9643948a..74e9ae34d925 100644 --- a/packages/kit/src/exports/vite/utils.js +++ b/packages/kit/src/exports/vite/utils.js @@ -1,7 +1,7 @@ import path from 'node:path'; import { posixify } from '../../utils/os.js'; import { negotiate } from '../../utils/http.js'; -import { escape_html } from '../../utils/escape.js'; +import { escape_for_regexp, escape_html } from '../../utils/escape.js'; import { stackless } from '../../utils/error.js'; import { dedent } from '../../core/sync/utils.js'; import { @@ -49,13 +49,6 @@ export function get_config_aliases(config, root) { return alias; } -/** - * @param {string} str - */ -function escape_for_regexp(str) { - return str.replace(/[.*+?^${}()|[\]\\]/g, (match) => '\\' + match); -} - /** * Silently respond with 404 for Chrome DevTools workspaces request. * Chrome always requests this at the root, regardless of base path. diff --git a/packages/kit/src/runtime/server/respond.js b/packages/kit/src/runtime/server/respond.js index 188eab860e9e..ec895d32516b 100644 --- a/packages/kit/src/runtime/server/respond.js +++ b/packages/kit/src/runtime/server/respond.js @@ -223,13 +223,6 @@ export async function internal_respond(request, options, manifest, state) { set_internal }); - if (state.emulator?.platform) { - event.platform = await state.emulator.platform({ - config: {}, - prerender: !!state.prerendering?.fallback - }); - } - /** @type {string | null} */ let resolved_path = url.pathname; @@ -387,7 +380,7 @@ export async function internal_respond(request, options, manifest, state) { } } - if (state.before_handle || state.emulator?.platform) { + if (state.before_handle) { let config = {}; /** @type {import('types').PrerenderOption} */ @@ -402,13 +395,7 @@ export async function internal_respond(request, options, manifest, state) { prerender = page_nodes.prerender(); } - if (state.emulator?.platform) { - event.platform = await state.emulator.platform({ config, prerender }); - } - - if (state.before_handle) { - return await state.before_handle(event, config, prerender, handle); - } + return await state.before_handle(event, config, prerender, handle); } } diff --git a/packages/kit/src/types/ambient-private.d.ts b/packages/kit/src/types/ambient-private.d.ts index 777c356a5654..074d0ab3e431 100644 --- a/packages/kit/src/types/ambient-private.d.ts +++ b/packages/kit/src/types/ambient-private.d.ts @@ -21,6 +21,30 @@ declare module '__sveltekit/server' { export function set_read_implementation(fn: (path: string) => ReadableStream): void; } +/** Used to construct the SSR manifest in development from a Node-agnostic environment */ +declare module '__sveltekit/manifest-data' { + import { ManifestData } from 'types'; + + export const env: Record; + export const mime_types: Record; + export const manifest_data: ManifestData; +} + +/** Used to read the filesystem during development from an environment without `node:fs` */ +declare module '__sveltekit/server-assets' { + export const server_assets: Record; + export const server_assets_content: Record>; +} + +/** Used to identify remote functions processed by Vite from any environment */ +declare module '__sveltekit/remotes' { + export const remotes: Array<{ hash: string; file: string }>; +} + +declare module '__sveltekit/ipc' { + export function get(pathname: string): Promise; +} + declare module '__sveltekit/env' { // exported environment variables are defined in env.d.ts diff --git a/packages/kit/src/types/ambient.d.ts b/packages/kit/src/types/ambient.d.ts index d01f3b4a426d..2af030b56845 100644 --- a/packages/kit/src/types/ambient.d.ts +++ b/packages/kit/src/types/ambient.d.ts @@ -151,3 +151,56 @@ declare module '$app/types' { */ export type Asset = ReturnType; } + +/** + * Exports the `Server` class for creating custom server entry points. + * @example + * ```js + * import { env } from 'sveltekit:env'; + * import { Server } from 'sveltekit:server'; + * import { manifest } from 'sveltekit:server-manifest'; + * + * const server = new Server(manifest); + * + * await server.init({ env }); + * ``` + */ +declare module 'sveltekit:server' { + export { Server } from '@sveltejs/kit'; +} + +/** + * Exports the SSR manifest used to initialise the server. + * @example + * ```js + * import { env } from 'sveltekit:env'; + * import { Server } from 'sveltekit:server'; + * import { manifest } from 'sveltekit:server-manifest'; + * + * const server = new Server(manifest); + * + * await server.init({ env }); + * ``` + */ +declare module 'sveltekit:server-manifest' { + import { SSRManifest } from '@sveltejs/kit'; + + export const manifest: SSRManifest; +} + +/** + * Exports the environment variables loaded by Vite. Used when initialising the server. + * @example + * ```js + * import { env } from 'sveltekit:env'; + * import { Server } from 'sveltekit:server'; + * import { manifest } from 'sveltekit:server-manifest'; + * + * const server = new Server(manifest); + * + * await server.init({ env }); + * ``` + */ +declare module 'sveltekit:env' { + export const env: Record; +} diff --git a/packages/kit/src/types/global-private.d.ts b/packages/kit/src/types/global-private.d.ts index a0f028957fc9..eacf499c42ca 100644 --- a/packages/kit/src/types/global-private.d.ts +++ b/packages/kit/src/types/global-private.d.ts @@ -38,6 +38,15 @@ declare global { * Should only be used when bundleStrategy !== 'inline' to avoid SvelteKit runtime changing on every build, preventing cacheability. */ const __SVELTEKIT_PAYLOAD__: SvelteKitPayload; + /** + * The Vite `root` setting used to construct paths to nodes and components + * for the SSR manifest during development + */ + const __SVELTEKIT_ROOT__: string; + const __SVELTEKIT_OUT_DIR__: string; + const __SVELTEKIT_FILES_ASSETS__: string; + /** Path to the internal SvelteKit runtime directory */ + const __SVELTEKIT_RUNTIME__: string; /** * Whether the `experimental.async` flag is applied */ diff --git a/packages/kit/src/types/internal.d.ts b/packages/kit/src/types/internal.d.ts index 547a3f103758..80d7672a2dc1 100644 --- a/packages/kit/src/types/internal.d.ts +++ b/packages/kit/src/types/internal.d.ts @@ -16,7 +16,6 @@ import { Reroute, RequestEvent, SSRManifest, - Emulator, ServerInit, ClientInit, Transport, @@ -33,6 +32,7 @@ import { TrailingSlash } from './private.js'; import { Span } from '@opentelemetry/api'; +import { ViteDevServer } from 'vite'; import { PageOptions } from '../exports/vite/static_analysis/types.js'; import { SharedIterator } from '../utils/shared-iterator.js'; @@ -67,8 +67,11 @@ export interface AssetDependencies { } export interface BuildData { + /** The _app directory configured. */ app_dir: string; + /** Path to the _app directory, including any base path. */ app_path: string; + base: string; manifest_data: ManifestData; out_dir: string; service_worker: string | null; @@ -189,11 +192,13 @@ export class InternalServer extends Server { prerender: PrerenderOption, handle: () => Promise ) => Promise; - emulator?: Emulator; } ): Promise; } +/** + * Used to construct the SSR manifest + */ export interface ManifestData { /** Static files from `config.files.assets`. */ assets: Asset[]; @@ -564,7 +569,6 @@ export interface SSRState { prerender: PrerenderOption, handle: () => Promise ) => Promise; - emulator?: Emulator; } export type StrictBody = string | ArrayBufferView; @@ -745,6 +749,11 @@ export interface RequestStore { state: RequestState; } +export interface DevContext { + server: ViteDevServer; + manifest_data: ManifestData; +} + /** Type of the `__sveltekit_abc123` object in the init `